Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/shell/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChatSidebar, type ChatSidebarTab } from "./ChatSidebar";
import { TopBar } from "./TopBar";
import { WindowTitleBar } from "./WindowTitleBar";
import { SpotifyMobileWidget } from "../../features/spotify/components/SpotifyMiniPlayer";
import { AgentDebugPanel } from "../../features/agents/components/AgentDebugPanel";
import { ChatNotificationBubbles } from "../../features/chats/components/ChatNotificationBubbles";
import {
getTrackerPanelWidthForProfile,
Expand Down Expand Up @@ -1022,6 +1023,7 @@ export function AppShell() {
<OnboardingTutorial />
</Suspense>
)}
<AgentDebugPanel />
<SpotifyMobileWidget />
</div>
</div>
Expand Down
106 changes: 97 additions & 9 deletions src/engine/agents-runtime/executor/agent-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import type {
LLMToolCall,
LLMToolDefinition,
} from "../../generation-core/llm/base-provider.js";
import type { AgentResult, AgentContext, AgentResultType } from "../../contracts/types/agent";
import type { AgentResult, AgentContext, AgentResultType, AgentDebugEntry } from "../../contracts/types/agent";
import { getDefaultAgentPrompt } from "../../contracts/constants/agent-prompts";
import { DEFAULT_AGENT_CONTEXT_SIZE, DEFAULT_AGENT_MAX_TOKENS, MAX_AGENT_MAX_TOKENS, MIN_AGENT_MAX_TOKENS } from "../../contracts/types/agent";
const isDebugAgentsEnabled = () => false;
const logger = {
debug: (..._args: unknown[]) => {},
info: (..._args: unknown[]) => {},
warn: (..._args: unknown[]) => {},
error: (..._args: unknown[]) => {},
isLevelEnabled: (_level: string) => false,
type Logger = {
debug: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
isLevelEnabled: (level: string) => boolean;
};

const MAX_AGENT_CONTEXT_MESSAGES = 200;
Expand Down Expand Up @@ -56,6 +55,34 @@ export function normalizeAgentContextSize(value: unknown, fallback = DEFAULT_AGE
return Math.max(1, Math.min(MAX_AGENT_CONTEXT_MESSAGES, Math.trunc(parsed)));
}

function createLogger(enabled: boolean): Logger {
const log = (level: "debug" | "info" | "warn" | "error", args: unknown[]) => {
if (!enabled) return;
const target =
typeof console !== "undefined" && typeof console[level] === "function"
? console[level]
: typeof console !== "undefined" && typeof console.log === "function"
? console.log
: undefined;
target?.(...args);
};

return {
debug: (...args: unknown[]) => log("debug", args),
info: (...args: unknown[]) => log("info", args),
warn: (...args: unknown[]) => log("warn", args),
error: (...args: unknown[]) => log("error", args),
isLevelEnabled: () => enabled,
};
}

function emitDebug(context: AgentContext, entry: Omit<AgentDebugEntry, "timestamp"> & { timestamp?: number }) {
context.debugSink?.({
...entry,
timestamp: entry.timestamp ?? Date.now(),
});
}

function redactSensitiveValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map((item) => redactSensitiveValue(item));
Expand Down Expand Up @@ -118,6 +145,8 @@ export async function executeAgent(
toolContext?: AgentToolContext,
): Promise<AgentResult> {
const startTime = Date.now();
const logger = createLogger(context.debugMode === true);
const emit = (entry: Omit<AgentDebugEntry, "timestamp"> & { timestamp?: number }) => emitDebug(context, entry);

try {
const template = config.promptTemplate || getDefaultAgentPrompt(config.type);
Expand Down Expand Up @@ -151,16 +180,25 @@ export async function executeAgent(
toolContext,
streamResponses,
startTime,
context,
context.signal,
);
}

// Call LLM (streaming to avoid proxy timeouts, no tools)
logger.info(`[agent] ${config.type} (${config.name}) — ${model}`);
emit({
level: "info",
phase: config.phase,
message: "agent-start",
args: [config.type, config.name, model],
});
for (const msg of messages) {
logger.debug(`[agent] [${msg.role}] ${msg.content}`);
emit({ level: "debug", phase: config.phase, message: "prompt-message", args: [msg.role, msg.content] });
}
logger.debug(`[agent] ═══ END PROMPT — temperature=${temperature} maxTokens=${maxTokens} ═══\n`);
emit({ level: "debug", phase: config.phase, message: "prompt-end", args: [temperature, maxTokens] });

let responseText = "";
const result = await provider.chatComplete(messages, {
Expand All @@ -182,6 +220,18 @@ export async function executeAgent(

logger.info(`[agent] ${config.type} done (${responseText.length} chars, ${durationMs}ms)`);
logger.debug(`[agent] ${config.type} raw response: ${responseText.slice(0, 500)}`);
emit({
level: "info",
phase: config.phase,
message: "agent-complete",
args: [config.type, responseText.length, durationMs],
});
emit({
level: "debug",
phase: config.phase,
message: "raw-response",
args: [config.type, responseText.slice(0, 500)],
});

// Parse the result based on agent type
const parsed = parseAgentResponse(config, responseText);
Expand Down Expand Up @@ -215,12 +265,15 @@ async function executeAgentWithTools(
toolContext: AgentToolContext,
streamResponses: boolean,
startTime: number,
context: AgentContext,
signal?: AbortSignal,
): Promise<AgentResult> {
const MAX_TOOL_ROUNDS = 5;
const loopMessages = [...initialMessages];
let totalTokens = 0;
const debugAgentsEnabled = isDebugAgentsEnabled() && logger.isLevelEnabled("debug");
const logger = createLogger(context.debugMode === true);
const debugAgentsEnabled = logger.isLevelEnabled("debug");
const emit = (entry: Omit<AgentDebugEntry, "timestamp"> & { timestamp?: number }) => emitDebug(context, entry);

for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const result = await provider.chatComplete(loopMessages, {
Expand Down Expand Up @@ -261,6 +314,12 @@ async function executeAgentWithTools(
// Execute each tool call and append results
for (const tc of result.toolCalls) {
logger.info("[agent-tools] %s calling: %s", config.type, tc.function.name);
emit({
level: "info",
phase: config.phase,
message: "tool-call",
toolCall: { name: tc.function.name, arguments: formatToolPayloadForLog(tc.function.arguments), allowed: true },
});
if (debugAgentsEnabled) {
logger.debug("[agent-tools] %s args: %s", config.type, formatToolPayloadForLog(tc.function.arguments));
}
Expand All @@ -272,6 +331,12 @@ async function executeAgentWithTools(
throw err;
}
logger.info("[agent-tools] %s %s completed", config.type, tc.function.name);
emit({
level: "info",
phase: config.phase,
message: "tool-result",
toolResult: { name: tc.function.name, result: formatToolPayloadForLog(toolResult), success: true },
});
if (debugAgentsEnabled) {
logger.debug("[agent-tools] %s result: %s", config.type, formatToolPayloadForLog(toolResult));
}
Expand Down Expand Up @@ -325,6 +390,8 @@ export async function executeAgentBatch(
model: string,
): Promise<AgentResult[]> {
if (configs.length === 0) return [];
const logger = createLogger(context.debugMode === true);
const emit = (entry: Omit<AgentDebugEntry, "timestamp"> & { timestamp?: number }) => emitDebug(context, entry);
const isolatedConfigs = configs.filter(shouldRunAgentIndividually);
if (isolatedConfigs.length === configs.length) {
logger.info(
Expand Down Expand Up @@ -396,12 +463,26 @@ export async function executeAgentBatch(
logger.info(
`[agent-batch] maxTokens: ${batchMaxTokens} (sum=${rawBatchMaxTokens} from [${perAgentTokens.join(", ")}]${provider.maxTokensOverrideValue !== null ? `, capped at ${provider.maxTokensOverrideValue}` : ""})`,
);
emit({
level: "info",
phase: configs[0]!.phase,
message: "batch-start",
agents: configs.map((c) => ({
type: c.type,
name: c.name,
model,
maxTokens: normalizeAgentMaxTokens(c.settings.maxTokens),
})),
batchMaxTokens,
});

logger.debug(`\n[agent-batch] ═══ BATCH PROMPT — [${configs.map((c) => c.type).join(", ")}] — ${model} ═══`);
for (const msg of messages) {
logger.debug(`[agent-batch] [${msg.role}] ${msg.content}`);
emit({ level: "debug", phase: configs[0]!.phase, message: "batch-prompt-message", args: [msg.role, msg.content] });
}
logger.debug(`[agent-batch] ═══ END BATCH PROMPT — temperature=${temperature} maxTokens=${batchMaxTokens} ═══\n`);
emit({ level: "debug", phase: configs[0]!.phase, message: "batch-prompt-end", args: [temperature, batchMaxTokens] });

// Use streaming (onToken) to keep the connection alive — avoids proxy
// timeouts (e.g. Cloudflare 524) on large batch responses.
Expand All @@ -428,6 +509,13 @@ export async function executeAgentBatch(

logger.info(`[agent-batch] Got response (${responseText.length} chars, ${durationMs}ms, ${totalTokens} tokens)`);
logger.debug(`[agent-batch] ${responseText}`);
emit({
level: "info",
phase: configs[0]!.phase,
message: "batch-response",
args: [responseText.length, durationMs, totalTokens],
});
emit({ level: "debug", phase: configs[0]!.phase, message: "batch-raw-response", args: [responseText] });

// Parse the batched response into individual results
const { parsed, failed } = parseBatchResponse(configs, responseText, durationMs, totalTokens);
Expand Down
11 changes: 11 additions & 0 deletions src/engine/agents-runtime/knowledge/knowledge-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,17 @@ export async function executeKnowledgeRouter(
options: KnowledgeRouterCandidateOptions = {},
): Promise<AgentResult> {
const startTime = Date.now();
const logger = {
debug: (...args: unknown[]) => {
if (baseContext.debugMode === true) console.debug(...args);
},
warn: (...args: unknown[]) => {
if (baseContext.debugMode === true) console.warn(...args);
},
error: (...args: unknown[]) => {
if (baseContext.debugMode === true) console.error(...args);
},
};

// Empty input → no work, no LLM call.
if (entries.length === 0) {
Expand Down
52 changes: 46 additions & 6 deletions src/engine/agents-runtime/pipeline/agent-pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AgentContext, AgentResult } from "../../contracts/types/agent";
import type { AgentContext, AgentDebugEntry, AgentResult } from "../../contracts/types/agent";
import type { BaseLLMProvider } from "../../generation-core/llm/base-provider.js";
import {
executeAgent,
Expand All @@ -7,11 +7,26 @@ import {
type AgentToolContext,
} from "../executor/agent-executor.js";

const logger = {
debug: (..._args: unknown[]) => {},
warn: (..._args: unknown[]) => {},
error: (..._args: unknown[]) => {},
};
function createLogger(enabled: boolean) {
return {
debug: (...args: unknown[]) => {
if (enabled) console.debug(...args);
},
warn: (...args: unknown[]) => {
if (enabled) console.warn(...args);
},
error: (...args: unknown[]) => {
if (enabled) console.error(...args);
},
};
}

function emitDebug(context: AgentContext, entry: Omit<AgentDebugEntry, "timestamp"> & { timestamp?: number }) {
context.debugSink?.({
...entry,
timestamp: entry.timestamp ?? Date.now(),
});
}

/** A fully resolved agent ready for execution. */
export interface ResolvedAgent extends AgentExecConfig {
Expand Down Expand Up @@ -143,6 +158,18 @@ async function executeGroup(
context: AgentContext,
onResult?: AgentResultCallback,
): Promise<AgentResult[]> {
const logger = createLogger(context.debugMode === true);
emitDebug(context, {
level: "debug",
phase: group.agents[0]?.phase ?? "unknown",
message: "group-start",
agents: group.agents.map((agent) => ({
type: agent.type,
name: agent.name,
model: agent.model,
maxTokens: normalizeAgentMaxParallelJobs(agent.maxParallelJobs),
})),
});
const groupContext = buildAgentContext(group.agents[0]!, context);
// Separate tool-using agents (can't be batched) from regular agents
const toolAgents = group.agents.filter((a) => a.toolContext?.tools.length);
Expand Down Expand Up @@ -202,7 +229,14 @@ async function executePhase(
const phaseAgents = agents.filter((a) => a.phase === phase);
if (phaseAgents.length === 0) return [];

const logger = createLogger(context.debugMode === true);
const groups = groupByProviderModel(phaseAgents).flatMap(splitGroupForParallelJobs);
emitDebug(context, {
level: "debug",
phase,
message: "phase-groups",
args: [phaseAgents.length, groups.length],
});

logger.debug(
'[agent-pipeline] Phase "%s": %d agents → %d job group(s) %j',
Expand Down Expand Up @@ -238,6 +272,12 @@ async function executePhase(
String(entry.reason),
);
}
emitDebug(context, {
level: "error",
phase,
message: "group-error",
args: [group.agents.map((a) => a.type).join(", "), String(entry.reason)],
});
for (const agent of group.agents) {
const errorResult: AgentResult = {
agentId: agent.id,
Expand Down
31 changes: 31 additions & 0 deletions src/engine/contracts/types/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,33 @@ export interface AgentResult {
error: string | null;
}

/** Structured debug entry emitted by the agent runtime. */
export interface AgentDebugEntry {
level?: "debug" | "info" | "warn" | "error";
phase: string;
message?: string;
args?: unknown[];
agents?: Array<{
type: string;
name: string;
model: string;
maxTokens: number;
}>;
results?: AgentResult[];
toolCall?: {
name: string;
arguments: string;
allowed: boolean;
};
toolResult?: {
name: string;
result: string;
success: boolean;
};
batchMaxTokens?: number;
timestamp: number;
}

/** Shared context passed to every agent. */
export interface AgentContext {
chatId: string;
Expand Down Expand Up @@ -134,6 +161,8 @@ export interface AgentContext {
} | null;
/** The agent's own persistent memory (key-value) */
memory: Record<string, unknown>;
/** Optional sink for structured runtime debug entries. */
debugSink?: (entry: Omit<AgentDebugEntry, "timestamp"> & { timestamp?: number }) => void;
/** Lorebook entries activated for this generation (read context) */
activatedLorebookEntries: Array<{ id: string; name: string; content: string; tag: string }> | null;
/** All lorebook IDs the agent can write to */
Expand All @@ -146,6 +175,8 @@ export interface AgentContext {
parallelResults?: AgentResult[];
/** Whether internal agent LLM calls should use transport streaming. */
streaming?: boolean;
/** Whether agent runtime logging should emit to the console. */
debugMode?: boolean;
/** Abort signal — when triggered, agent execution should stop. Typed as `any` to avoid DOM/Node lib dependency. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
signal?: any;
Expand Down
4 changes: 4 additions & 0 deletions src/engine/generation/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export interface GenerationAgentRuntimeInput {
persona: GenerationPersonaContext | null;
activatedLorebookEntries: Array<{ id: string; name: string; content: string; tag: string }>;
chatSummary: string | null;
debugMode?: boolean;
debugSink?: AgentContext["debugSink"];
signal?: AbortSignal;
agentTypes?: Set<string>;
}
Expand Down Expand Up @@ -627,6 +629,8 @@ async function buildAgentContext(deps: AgentDeps, input: GenerationAgentRuntimeI
activatedLorebookEntries: input.activatedLorebookEntries,
writableLorebookIds: null,
chatSummary: input.chatSummary,
debugMode: input.debugMode === true,
debugSink: input.debugSink,
streaming: true,
signal: input.signal,
};
Expand Down
Loading