mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add trace logging and tighten daemon log defaults (#933)
* Add trace logging and tighten daemon log defaults * Clean up daemon trace logging shape * Clean up daemon trace logging * Route provider turn-id checks through shared helper * Fix supervisor log config test paths on Windows * Expect resolved supervisor log path on Windows
This commit is contained in:
@@ -39,7 +39,14 @@ In any worktree-style or portless setup, never assume default ports.
|
||||
|
||||
### Daemon logs
|
||||
|
||||
Check `$PASEO_HOME/daemon.log` for trace-level logs.
|
||||
Check `$PASEO_HOME/daemon.log` for daemon logs. The default level is `info`; set
|
||||
`PASEO_LOG_LEVEL=trace` before launching the daemon when you need full provider,
|
||||
session, and agent-manager traces for stuck-state debugging.
|
||||
|
||||
The supervisor rotates `daemon.log`. Persisted `log.file.rotate` settings in
|
||||
`$PASEO_HOME/config.json` win first. Without persisted config, the optional
|
||||
`PASEO_LOG_ROTATE_SIZE` and `PASEO_LOG_ROTATE_COUNT` env vars override the
|
||||
defaults. The default rotation is `10m` x `3` files everywhere.
|
||||
|
||||
## paseo.json service scripts
|
||||
|
||||
|
||||
@@ -10,12 +10,9 @@ import {
|
||||
import { resolvePaseoHome } from "../src/server/paseo-home.js";
|
||||
import { loadPersistedConfig } from "../src/server/persisted-config.js";
|
||||
import { runSupervisor } from "./supervisor.js";
|
||||
import { resolveSupervisorLogFile } from "./supervisor-log-config.js";
|
||||
import { applySherpaLoaderEnv } from "../src/server/speech/providers/local/sherpa/sherpa-runtime-env.js";
|
||||
|
||||
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
|
||||
const DEFAULT_LOG_ROTATE_SIZE = "10m";
|
||||
const DEFAULT_LOG_ROTATE_MAX_FILES = 2;
|
||||
|
||||
interface DaemonRunnerConfig {
|
||||
devMode: boolean;
|
||||
workerArgs: string[];
|
||||
@@ -77,28 +74,6 @@ function resolvePackagedNodeEntrypointRunnerPath(currentScriptPath: string): str
|
||||
return existsSync(runnerPath) ? runnerPath : null;
|
||||
}
|
||||
|
||||
function resolveSupervisorLogFile(
|
||||
paseoHome: string,
|
||||
persistedConfig: ReturnType<typeof loadPersistedConfig>,
|
||||
) {
|
||||
const configuredFile = persistedConfig.log?.file;
|
||||
const configuredPath = configuredFile?.path;
|
||||
let logPath = path.join(paseoHome, DEFAULT_DAEMON_LOG_FILENAME);
|
||||
if (configuredPath) {
|
||||
logPath = path.isAbsolute(configuredPath)
|
||||
? configuredPath
|
||||
: path.resolve(paseoHome, configuredPath);
|
||||
}
|
||||
|
||||
return {
|
||||
path: logPath,
|
||||
rotate: {
|
||||
maxSize: configuredFile?.rotate?.maxSize ?? DEFAULT_LOG_ROTATE_SIZE,
|
||||
maxFiles: configuredFile?.rotate?.maxFiles ?? DEFAULT_LOG_ROTATE_MAX_FILES,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = parseConfig(process.argv.slice(2));
|
||||
const workerEntry = config.devMode ? resolveDevWorkerEntry() : resolveWorkerEntry();
|
||||
@@ -113,7 +88,7 @@ async function main(): Promise<void> {
|
||||
|
||||
const paseoHome = resolvePaseoHome(workerEnv);
|
||||
const persistedConfig = loadPersistedConfig(paseoHome);
|
||||
const supervisorLogFile = resolveSupervisorLogFile(paseoHome, persistedConfig);
|
||||
const supervisorLogFile = resolveSupervisorLogFile(paseoHome, persistedConfig, workerEnv);
|
||||
|
||||
try {
|
||||
await acquirePidLock(paseoHome, null, {
|
||||
|
||||
42
packages/server/scripts/supervisor-log-config.ts
Normal file
42
packages/server/scripts/supervisor-log-config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import path from "node:path";
|
||||
|
||||
import type { loadPersistedConfig } from "../src/server/persisted-config.js";
|
||||
|
||||
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
|
||||
const DEFAULT_LOG_ROTATE_SIZE = "10m";
|
||||
const DEFAULT_LOG_ROTATE_MAX_FILES = 3;
|
||||
|
||||
export function resolveSupervisorLogFile(
|
||||
paseoHome: string,
|
||||
persistedConfig: ReturnType<typeof loadPersistedConfig>,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
) {
|
||||
const configuredFile = persistedConfig.log?.file;
|
||||
const configuredPath = configuredFile?.path;
|
||||
const envRotateSize = env.PASEO_LOG_ROTATE_SIZE?.trim();
|
||||
const envRotateMaxFiles = parseOptionalPositiveInteger(env.PASEO_LOG_ROTATE_COUNT);
|
||||
let logPath = path.join(paseoHome, DEFAULT_DAEMON_LOG_FILENAME);
|
||||
if (configuredPath) {
|
||||
logPath = path.isAbsolute(configuredPath)
|
||||
? configuredPath
|
||||
: path.resolve(paseoHome, configuredPath);
|
||||
}
|
||||
|
||||
return {
|
||||
path: logPath,
|
||||
rotate: {
|
||||
maxSize: configuredFile?.rotate?.maxSize ?? envRotateSize ?? DEFAULT_LOG_ROTATE_SIZE,
|
||||
maxFiles:
|
||||
configuredFile?.rotate?.maxFiles ?? envRotateMaxFiles ?? DEFAULT_LOG_ROTATE_MAX_FILES,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseOptionalPositiveInteger(value: string | undefined): number | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parsed = Number.parseInt(value.trim(), 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { isPlatform } from "../src/test-utils/platform.js";
|
||||
import { resolveSupervisorLogFile } from "./supervisor-log-config.js";
|
||||
|
||||
const repoRoot = path.resolve(fileURLToPath(new URL("../../..", import.meta.url)));
|
||||
const supervisorPath = fileURLToPath(new URL("./supervisor.ts", import.meta.url));
|
||||
@@ -87,6 +88,57 @@ async function runSupervisorFixture(options: {
|
||||
}
|
||||
|
||||
describe("supervisor durable logging", () => {
|
||||
test("resolves rotation defaults", () => {
|
||||
const paseoHome = path.join(path.sep, "tmp", "paseo-home");
|
||||
const logFile = resolveSupervisorLogFile(paseoHome, {}, {});
|
||||
|
||||
expect(logFile).toEqual({
|
||||
path: path.join(paseoHome, "daemon.log"),
|
||||
rotate: { maxSize: "10m", maxFiles: 3 },
|
||||
});
|
||||
});
|
||||
|
||||
test("lets persisted rotation override env rotation defaults", () => {
|
||||
const paseoHome = path.join(path.sep, "tmp", "paseo-home");
|
||||
const logFile = resolveSupervisorLogFile(
|
||||
paseoHome,
|
||||
{
|
||||
log: {
|
||||
file: {
|
||||
path: "logs/daemon.log",
|
||||
rotate: { maxSize: "25m", maxFiles: 4 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
PASEO_LOG_ROTATE_SIZE: "200m",
|
||||
PASEO_LOG_ROTATE_COUNT: "12",
|
||||
},
|
||||
);
|
||||
|
||||
expect(logFile).toEqual({
|
||||
path: path.resolve(paseoHome, "logs", "daemon.log"),
|
||||
rotate: { maxSize: "25m", maxFiles: 4 },
|
||||
});
|
||||
});
|
||||
|
||||
test("uses env rotation when persisted rotation is absent", () => {
|
||||
const paseoHome = path.join(path.sep, "tmp", "paseo-home");
|
||||
const logFile = resolveSupervisorLogFile(
|
||||
paseoHome,
|
||||
{},
|
||||
{
|
||||
PASEO_LOG_ROTATE_SIZE: "50m",
|
||||
PASEO_LOG_ROTATE_COUNT: "8",
|
||||
},
|
||||
);
|
||||
|
||||
expect(logFile).toEqual({
|
||||
path: path.join(paseoHome, "daemon.log"),
|
||||
rotate: { maxSize: "50m", maxFiles: 8 },
|
||||
});
|
||||
});
|
||||
|
||||
test("writes supervised worker stdout and stderr to daemon.log", async () => {
|
||||
const result = await runSupervisorFixture({
|
||||
workerSource: `
|
||||
|
||||
@@ -716,6 +716,7 @@ test("createAgent passes daemon launch env through the provider launch context",
|
||||
modeId: "auto",
|
||||
});
|
||||
expect(client.lastLaunchContext).toEqual({
|
||||
agentId: snapshot.id,
|
||||
env: {
|
||||
PASEO_AGENT_ID: snapshot.id,
|
||||
},
|
||||
@@ -1179,6 +1180,7 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
|
||||
},
|
||||
});
|
||||
expect(client.lastResumeLaunchContext).toEqual({
|
||||
agentId: resumed.id,
|
||||
env: {
|
||||
PASEO_AGENT_ID: resumed.id,
|
||||
},
|
||||
@@ -1283,6 +1285,7 @@ test("reloadAgentSession passes daemon launch env through the provider launch co
|
||||
});
|
||||
|
||||
expect(client.lastCreateLaunchContext).toEqual({
|
||||
agentId: snapshot.id,
|
||||
env: {
|
||||
PASEO_AGENT_ID: snapshot.id,
|
||||
},
|
||||
@@ -1293,6 +1296,7 @@ test("reloadAgentSession passes daemon launch env through the provider launch co
|
||||
});
|
||||
|
||||
expect(client.lastResumeLaunchContext).toEqual({
|
||||
agentId: snapshot.id,
|
||||
env: {
|
||||
PASEO_AGENT_ID: snapshot.id,
|
||||
},
|
||||
|
||||
@@ -10,30 +10,31 @@ import type { Logger } from "pino";
|
||||
import { z } from "zod";
|
||||
import type { TerminalManager } from "../../terminal/terminal-manager.js";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentCreateSessionOptions,
|
||||
AgentFeature,
|
||||
AgentLaunchContext,
|
||||
AgentSlashCommand,
|
||||
AgentMode,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
AgentPermissionResult,
|
||||
AgentPersistenceHandle,
|
||||
AgentPromptInput,
|
||||
AgentProvider,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
AgentRuntimeInfo,
|
||||
ListPersistedAgentsOptions,
|
||||
PersistedAgentDescriptor,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentCreateSessionOptions,
|
||||
type AgentFeature,
|
||||
type AgentLaunchContext,
|
||||
type AgentSlashCommand,
|
||||
type AgentMode,
|
||||
type AgentPermissionRequest,
|
||||
type AgentPermissionResponse,
|
||||
type AgentPermissionResult,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPromptInput,
|
||||
type AgentProvider,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
type AgentUsage,
|
||||
type AgentRuntimeInfo,
|
||||
type ListPersistedAgentsOptions,
|
||||
type PersistedAgentDescriptor,
|
||||
} from "./agent-sdk-types.js";
|
||||
import { buildArchivedAgentRecord, type ArchivedStoredAgentRecord } from "./agent-archive.js";
|
||||
import type { StoredAgentRecord, AgentStorage } from "./agent-storage.js";
|
||||
@@ -1005,18 +1006,28 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId: agent.activeForegroundTurnId ?? undefined,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
pendingPermissions: agent.pendingPermissions.size,
|
||||
},
|
||||
"closeAgent: start",
|
||||
"agent.manager.close.start",
|
||||
);
|
||||
const closedAgent = this.prepareAgentForClosure(agent, "agent closed");
|
||||
await agent.session.close();
|
||||
this.timelineStore.delete(agentId);
|
||||
await this.persistSnapshot(closedAgent);
|
||||
this.emitClosedAgent(closedAgent, { persist: false });
|
||||
this.logger.trace({ agentId }, "closeAgent: completed");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: closedAgent.provider,
|
||||
sessionId: closedAgent.persistence?.sessionId ?? undefined,
|
||||
},
|
||||
"agent.manager.close.complete",
|
||||
);
|
||||
}
|
||||
|
||||
async archiveAgent(agentId: string): Promise<{ archivedAt: string }> {
|
||||
@@ -1470,22 +1481,28 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: existingAgent.provider,
|
||||
sessionId: existingAgent.persistence?.sessionId ?? undefined,
|
||||
turnId: existingAgent.activeForegroundTurnId ?? undefined,
|
||||
lifecycle: existingAgent.lifecycle,
|
||||
activeForegroundTurnId: existingAgent.activeForegroundTurnId,
|
||||
hasPendingForegroundRun: this.foregroundRuns.hasPendingRun(agentId),
|
||||
promptType: typeof prompt === "string" ? "string" : "structured",
|
||||
hasRunOptions: Boolean(options),
|
||||
},
|
||||
"streamAgent: requested",
|
||||
"agent.manager.stream.request",
|
||||
);
|
||||
if (existingAgent.activeForegroundTurnId || this.foregroundRuns.hasPendingRun(agentId)) {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: existingAgent.provider,
|
||||
sessionId: existingAgent.persistence?.sessionId ?? undefined,
|
||||
turnId: existingAgent.activeForegroundTurnId ?? undefined,
|
||||
lifecycle: existingAgent.lifecycle,
|
||||
hasPendingForegroundRun: this.foregroundRuns.hasPendingRun(agentId),
|
||||
},
|
||||
"streamAgent: rejected because a foreground run is already in flight",
|
||||
"agent.manager.stream.reject",
|
||||
);
|
||||
throw new Error(`Agent ${agentId} already has an active run`);
|
||||
}
|
||||
@@ -1522,10 +1539,13 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
},
|
||||
"streamAgent: started",
|
||||
"agent.manager.stream.start",
|
||||
);
|
||||
|
||||
turnStream = this.foregroundRuns.createTurnStream(turnId);
|
||||
@@ -1577,11 +1597,14 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: agent.provider,
|
||||
sessionId: mutableAgent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
lifecycle: mutableAgent.lifecycle,
|
||||
terminalError,
|
||||
pendingReplacement: mutableAgent.pendingReplacement,
|
||||
},
|
||||
"finalizeForegroundTurn: applying terminal state",
|
||||
"agent.manager.finalize",
|
||||
);
|
||||
if (!shouldHoldBusyForReplacement) {
|
||||
this.touchUpdatedAt(mutableAgent);
|
||||
@@ -2371,6 +2394,16 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
private enqueueSessionEvent(agentId: string, event: AgentStreamEvent): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: event.provider,
|
||||
sessionId: this.agents.get(agentId)?.persistence?.sessionId ?? undefined,
|
||||
turnId: getAgentStreamEventTurnId(event),
|
||||
event,
|
||||
},
|
||||
"agent.manager.enqueue",
|
||||
);
|
||||
const previous = this.sessionEventTails.get(agentId) ?? Promise.resolve();
|
||||
const next = previous
|
||||
.catch(() => undefined)
|
||||
@@ -2382,6 +2415,16 @@ export class AgentManager {
|
||||
if (current.session == null) {
|
||||
return;
|
||||
}
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: event.provider,
|
||||
sessionId: current.persistence?.sessionId ?? undefined,
|
||||
turnId: getAgentStreamEventTurnId(event),
|
||||
event,
|
||||
},
|
||||
"agent.manager.dequeue",
|
||||
);
|
||||
await this.dispatchSessionEvent(current, event);
|
||||
return;
|
||||
})
|
||||
@@ -2405,8 +2448,19 @@ export class AgentManager {
|
||||
agent: ActiveManagedAgent,
|
||||
event: AgentStreamEvent,
|
||||
): Promise<void> {
|
||||
const turnId = (event as { turnId?: string }).turnId;
|
||||
const turnId = getAgentStreamEventTurnId(event);
|
||||
const matchingWaiters = this.foregroundRuns.getMatchingWaiters(agent, turnId);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: event.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
matchingWaiterCount: matchingWaiters.length,
|
||||
event,
|
||||
},
|
||||
"agent.manager.dispatch_session_event",
|
||||
);
|
||||
|
||||
const shouldNotifyWaiters = await this.handleStreamEvent(agent, event);
|
||||
|
||||
@@ -2417,6 +2471,18 @@ export class AgentManager {
|
||||
this.foregroundRuns.notifyWaiters(matchingWaiters, event, {
|
||||
terminal: isTurnTerminalEvent(event),
|
||||
});
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: event.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
notifiedWaiterCount: matchingWaiters.length,
|
||||
terminal: isTurnTerminalEvent(event),
|
||||
event,
|
||||
},
|
||||
"agent.manager.notify_waiters",
|
||||
);
|
||||
}
|
||||
|
||||
private async resolveInitialPersistedTitle(
|
||||
@@ -2529,7 +2595,7 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
private notifyForegroundTurnWaiters(agentId: string, event: AgentStreamEvent): void {
|
||||
const turnId = (event as { turnId?: string }).turnId;
|
||||
const turnId = getAgentStreamEventTurnId(event);
|
||||
if (turnId == null) {
|
||||
return;
|
||||
}
|
||||
@@ -2540,6 +2606,16 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
this.foregroundRuns.notifyAgentWaiters(agent, event);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: event.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
event,
|
||||
},
|
||||
"agent.manager.notify_waiters.coalesced",
|
||||
);
|
||||
}
|
||||
|
||||
private async handleStreamEvent(
|
||||
@@ -2547,8 +2623,9 @@ export class AgentManager {
|
||||
event: AgentStreamEvent,
|
||||
options?: HandleStreamEventOptions,
|
||||
): Promise<boolean> {
|
||||
const eventTurnId = (event as { turnId?: string }).turnId;
|
||||
const eventTurnId = getAgentStreamEventTurnId(event);
|
||||
const isForegroundEvent = Boolean(eventTurnId && agent.activeForegroundTurnId === eventTurnId);
|
||||
this.traceHandleStreamEventStart(agent, event, eventTurnId, isForegroundEvent);
|
||||
if (
|
||||
eventTurnId &&
|
||||
isTurnTerminalEvent(event) &&
|
||||
@@ -2561,6 +2638,7 @@ export class AgentManager {
|
||||
if (!options?.fromHistory) {
|
||||
this.touchUpdatedAt(agent);
|
||||
if (this.agentStreamCoalescer.handle(agent.id, event)) {
|
||||
this.traceCoalescerBuffered(agent, event, eventTurnId);
|
||||
return false;
|
||||
}
|
||||
this.agentStreamCoalescer.flushFor(agent.id);
|
||||
@@ -2588,9 +2666,71 @@ export class AgentManager {
|
||||
this.dispatchStream(agent.id, event);
|
||||
}
|
||||
|
||||
this.traceHandleStreamEventEnd(agent, event, eventTurnId, flags);
|
||||
|
||||
return flags.shouldNotifyWaiters;
|
||||
}
|
||||
|
||||
private traceHandleStreamEventStart(
|
||||
agent: ActiveManagedAgent,
|
||||
event: AgentStreamEvent,
|
||||
turnId: string | undefined,
|
||||
isForegroundEvent: boolean,
|
||||
): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: event.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
isForegroundEvent,
|
||||
event,
|
||||
},
|
||||
"agent.manager.handle_stream_event.start",
|
||||
);
|
||||
}
|
||||
|
||||
private traceCoalescerBuffered(
|
||||
agent: ActiveManagedAgent,
|
||||
event: AgentStreamEvent,
|
||||
turnId: string | undefined,
|
||||
): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: event.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
event,
|
||||
},
|
||||
"agent.manager.coalescer.buffer",
|
||||
);
|
||||
}
|
||||
|
||||
private traceHandleStreamEventEnd(
|
||||
agent: ActiveManagedAgent,
|
||||
event: AgentStreamEvent,
|
||||
turnId: string | undefined,
|
||||
flags: StreamEventFlags,
|
||||
): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: event.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
shouldDispatchEvent: flags.shouldDispatchEvent,
|
||||
shouldNotifyWaiters: flags.shouldNotifyWaiters,
|
||||
event,
|
||||
},
|
||||
"agent.manager.handle_stream_event.end",
|
||||
);
|
||||
}
|
||||
|
||||
private dispatchStreamEventByType(params: {
|
||||
agent: ActiveManagedAgent;
|
||||
event: AgentStreamEvent;
|
||||
@@ -2747,11 +2887,13 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId: eventTurnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
eventTurnId,
|
||||
},
|
||||
"handleStreamEvent: turn_completed",
|
||||
"agent.manager.turn.completed",
|
||||
);
|
||||
agent.lastUsage = event.usage;
|
||||
agent.lastError = undefined;
|
||||
@@ -2778,6 +2920,9 @@ export class AgentManager {
|
||||
this.logger.warn(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId: eventTurnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
eventTurnId,
|
||||
@@ -2818,11 +2963,14 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId: eventTurnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
eventTurnId,
|
||||
},
|
||||
"handleStreamEvent: turn_canceled",
|
||||
"agent.manager.turn.canceled",
|
||||
);
|
||||
if (!isForegroundEvent && !agent.pendingReplacement) {
|
||||
agent.lifecycle = "idle";
|
||||
@@ -2843,11 +2991,13 @@ export class AgentManager {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId: eventTurnId,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
eventTurnId,
|
||||
},
|
||||
"handleStreamEvent: turn_started",
|
||||
"agent.manager.turn.started",
|
||||
);
|
||||
if (!isForegroundEvent) {
|
||||
agent.lifecycle = "running";
|
||||
@@ -2993,6 +3143,20 @@ export class AgentManager {
|
||||
|
||||
this.syncFeaturesFromSession(agent);
|
||||
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: agent.id,
|
||||
provider: agent.provider,
|
||||
sessionId: agent.persistence?.sessionId ?? undefined,
|
||||
turnId: agent.activeForegroundTurnId ?? undefined,
|
||||
lifecycle: agent.lifecycle,
|
||||
activeForegroundTurnId: agent.activeForegroundTurnId,
|
||||
pendingPermissions: agent.pendingPermissions.size,
|
||||
persist: options?.persist !== false,
|
||||
},
|
||||
"agent.manager.emit_state",
|
||||
);
|
||||
|
||||
this.dispatch({
|
||||
type: "agent_state",
|
||||
agent: { ...agent },
|
||||
@@ -3120,6 +3284,18 @@ export class AgentManager {
|
||||
event: AgentStreamEvent,
|
||||
metadata?: { seq?: number; epoch?: string },
|
||||
): void {
|
||||
const agent = this.agents.get(agentId);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: event.provider,
|
||||
sessionId: agent?.persistence?.sessionId ?? undefined,
|
||||
turnId: getAgentStreamEventTurnId(event),
|
||||
metadata,
|
||||
event,
|
||||
},
|
||||
"agent.manager.dispatch_stream",
|
||||
);
|
||||
this.dispatch({ type: "agent_stream", agentId, event, ...metadata });
|
||||
}
|
||||
|
||||
@@ -3215,6 +3391,7 @@ export class AgentManager {
|
||||
|
||||
private buildLaunchContext(agentId: string): AgentLaunchContext {
|
||||
return {
|
||||
agentId,
|
||||
env: {
|
||||
PASEO_AGENT_ID: agentId,
|
||||
},
|
||||
|
||||
@@ -17,6 +17,19 @@ export function startAgentRun(
|
||||
logger: Logger,
|
||||
options?: StartAgentRunOptions,
|
||||
): { outOfBand: boolean } {
|
||||
const snapshot = agentManager.getAgent(agentId);
|
||||
logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: snapshot?.provider,
|
||||
providerSessionId: snapshot?.persistence?.sessionId ?? undefined,
|
||||
turnId: snapshot?.activeForegroundTurnId ?? undefined,
|
||||
promptType: typeof prompt === "string" ? "string" : "structured",
|
||||
hasRunOptions: Boolean(options?.runOptions),
|
||||
replaceRunning: Boolean(options?.replaceRunning),
|
||||
},
|
||||
"agent.session.start_stream.request",
|
||||
);
|
||||
// Out-of-band commands (e.g. /goal pause) must run WITHOUT canceling an
|
||||
// in-flight turn — replaceAgentRun would interrupt the running turn. The
|
||||
// intercept lives at this layer so it covers every prompt entrypoint.
|
||||
@@ -28,12 +41,38 @@ export function startAgentRun(
|
||||
const iterator = shouldReplace
|
||||
? agentManager.replaceAgentRun(agentId, prompt, runOptions)
|
||||
: agentManager.streamAgent(agentId, prompt, runOptions);
|
||||
logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: snapshot?.provider,
|
||||
providerSessionId: snapshot?.persistence?.sessionId ?? undefined,
|
||||
shouldReplace,
|
||||
},
|
||||
"agent.session.start_stream.iterator_returned",
|
||||
);
|
||||
void (async () => {
|
||||
try {
|
||||
for await (const _ of iterator) {
|
||||
// Events are broadcast via AgentManager subscribers.
|
||||
}
|
||||
logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: snapshot?.provider,
|
||||
providerSessionId: snapshot?.persistence?.sessionId ?? undefined,
|
||||
},
|
||||
"agent.session.iterator.drained",
|
||||
);
|
||||
} catch (error) {
|
||||
logger.trace(
|
||||
{
|
||||
agentId,
|
||||
provider: snapshot?.provider,
|
||||
providerSessionId: snapshot?.persistence?.sessionId ?? undefined,
|
||||
err: error,
|
||||
},
|
||||
"agent.session.iterator.error",
|
||||
);
|
||||
logger.error({ err: error, agentId }, "Agent stream failed");
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -358,6 +358,10 @@ export type AgentStreamEvent =
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
export function getAgentStreamEventTurnId(event: AgentStreamEvent): string | undefined {
|
||||
return "turnId" in event ? event.turnId : undefined;
|
||||
}
|
||||
|
||||
export type AgentPermissionRequestKind = "tool" | "plan" | "question" | "mode" | "other";
|
||||
|
||||
export type AgentPermissionUpdate = AgentMetadata;
|
||||
@@ -476,6 +480,7 @@ export interface AgentSessionConfig {
|
||||
}
|
||||
|
||||
export interface AgentLaunchContext {
|
||||
agentId?: string;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import type { AgentStreamEvent } from "./agent-sdk-types.js";
|
||||
import { getAgentStreamEventTurnId, type AgentStreamEvent } from "./agent-sdk-types.js";
|
||||
|
||||
export interface ForegroundTurnWaiter {
|
||||
turnId: string;
|
||||
@@ -105,7 +105,7 @@ export class ForegroundRunState {
|
||||
event: AgentStreamEvent,
|
||||
options?: { terminal?: boolean },
|
||||
): void {
|
||||
const waiters = this.getMatchingWaiters(agent, eventTurnId(event));
|
||||
const waiters = this.getMatchingWaiters(agent, getAgentStreamEventTurnId(event));
|
||||
this.notifyWaiters(waiters, event, { terminal: options?.terminal ?? false });
|
||||
}
|
||||
|
||||
@@ -226,7 +226,3 @@ function settlePendingForegroundRun(pendingRun: PendingForegroundRun): void {
|
||||
pendingRun.settled = true;
|
||||
pendingRun.resolveSettled();
|
||||
}
|
||||
|
||||
function eventTurnId(event: AgentStreamEvent): string | undefined {
|
||||
return (event as { turnId?: string }).turnId;
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ function prepareConfiguredOverrideSession(
|
||||
|
||||
test("ACP setModel only uses config-option fallback when the matching select choice contains the model", async () => {
|
||||
const logger = createTestLogger();
|
||||
const childLogger = { warn: vi.fn() };
|
||||
const childLogger = { trace: vi.fn(), warn: vi.fn() };
|
||||
vi.spyOn(logger, "child").mockReturnValue(asInternals<typeof logger>(childLogger));
|
||||
const session = createSessionWithConfig({}, logger);
|
||||
const setSessionConfigOption = vi.fn(async () => ({
|
||||
@@ -653,7 +653,7 @@ describe("ACPAgentSession Zed parity", () => {
|
||||
expect(await validSession.getCurrentMode()).toBe("default");
|
||||
|
||||
const logger = createTestLogger();
|
||||
const childLogger = { warn: vi.fn() };
|
||||
const childLogger = { trace: vi.fn(), warn: vi.fn() };
|
||||
vi.spyOn(logger, "child").mockReturnValue(asInternals<typeof logger>(childLogger));
|
||||
const invalidSession = createSessionWithConfig(
|
||||
{ modeId: "acceptEdits", model: "opus" },
|
||||
|
||||
@@ -54,35 +54,36 @@ import {
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentLaunchContext,
|
||||
AgentMetadata,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionRequestKind,
|
||||
AgentPermissionResponse,
|
||||
AgentPersistenceHandle,
|
||||
AgentPromptContentBlock,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentRuntimeInfo,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
ListModesOptions,
|
||||
ListModelsOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
ToolCallDetail,
|
||||
ToolCallTimelineItem,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentLaunchContext,
|
||||
type AgentMetadata,
|
||||
type AgentMode,
|
||||
type AgentModelDefinition,
|
||||
type AgentPermissionRequest,
|
||||
type AgentPermissionRequestKind,
|
||||
type AgentPermissionResponse,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPromptContentBlock,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentRuntimeInfo,
|
||||
type AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentSlashCommand,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
type AgentUsage,
|
||||
type ListModesOptions,
|
||||
type ListModelsOptions,
|
||||
type ListPersistedAgentsOptions,
|
||||
type McpServerConfig,
|
||||
type PersistedAgentDescriptor,
|
||||
type ToolCallDetail,
|
||||
type ToolCallTimelineItem,
|
||||
} from "../agent-sdk-types.js";
|
||||
import {
|
||||
createProviderEnvSpec,
|
||||
@@ -265,6 +266,7 @@ interface ACPAgentSessionOptions {
|
||||
) => Promise<void>;
|
||||
capabilities: AgentCapabilityFlags;
|
||||
handle?: AgentPersistenceHandle;
|
||||
agentId?: string;
|
||||
launchEnv?: Record<string, string>;
|
||||
waitForInitialCommands?: boolean;
|
||||
initialCommandsWaitTimeoutMs?: number;
|
||||
@@ -519,7 +521,10 @@ export class ACPAgentClient implements AgentClient {
|
||||
constructor(options: ACPAgentClientOptions) {
|
||||
this.provider = options.provider;
|
||||
this.capabilities = options.capabilities ?? DEFAULT_ACP_CAPABILITIES;
|
||||
this.logger = options.logger.child({ module: "agent", provider: options.provider });
|
||||
this.logger = options.logger.child({
|
||||
module: "agent",
|
||||
provider: options.provider,
|
||||
});
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
this.defaultCommand = options.defaultCommand;
|
||||
this.defaultModes = options.defaultModes ?? [];
|
||||
@@ -557,6 +562,7 @@ export class ACPAgentClient implements AgentClient {
|
||||
beforeModeWriter: this.beforeModeWriter,
|
||||
thinkingOptionWriter: this.thinkingOptionWriter,
|
||||
capabilities: this.capabilities,
|
||||
agentId: launchContext?.agentId,
|
||||
launchEnv: launchContext?.env,
|
||||
waitForInitialCommands: this.waitForInitialCommands,
|
||||
initialCommandsWaitTimeoutMs: this.initialCommandsWaitTimeoutMs,
|
||||
@@ -603,6 +609,7 @@ export class ACPAgentClient implements AgentClient {
|
||||
thinkingOptionWriter: this.thinkingOptionWriter,
|
||||
capabilities: this.capabilities,
|
||||
handle,
|
||||
agentId: launchContext?.agentId,
|
||||
launchEnv: launchContext?.env,
|
||||
waitForInitialCommands: this.waitForInitialCommands,
|
||||
initialCommandsWaitTimeoutMs: this.initialCommandsWaitTimeoutMs,
|
||||
@@ -842,6 +849,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
sessionId: string,
|
||||
thinkingOptionId: string,
|
||||
) => Promise<void>;
|
||||
private readonly agentId?: string;
|
||||
private readonly launchEnv?: Record<string, string>;
|
||||
private readonly subscribers = new Set<(event: AgentStreamEvent) => void>();
|
||||
private readonly pendingPermissions = new Map<string, PendingPermission>();
|
||||
@@ -894,6 +902,7 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
this.beforeModeWriter = options.beforeModeWriter;
|
||||
this.thinkingOptionWriter = options.thinkingOptionWriter;
|
||||
this.availableModes = options.defaultModes;
|
||||
this.agentId = options.agentId;
|
||||
this.launchEnv = options.launchEnv;
|
||||
this.initialHandle = options.handle;
|
||||
this.config = { ...config, provider: options.provider };
|
||||
@@ -1567,11 +1576,31 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
}
|
||||
|
||||
async sessionUpdate(params: SessionNotification): Promise<void> {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: this.provider,
|
||||
sessionId: params.sessionId,
|
||||
rawEvent: params,
|
||||
},
|
||||
"provider.acp.raw_event",
|
||||
);
|
||||
if (params.sessionId !== this.sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const events = this.translateSessionUpdate(params.update);
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: this.provider,
|
||||
sessionId: this.sessionId,
|
||||
turnId: this.activeForegroundTurnId ?? undefined,
|
||||
rawEvent: params,
|
||||
events,
|
||||
},
|
||||
"provider.acp.parsed_event",
|
||||
);
|
||||
if (this.replayingHistory) {
|
||||
for (const event of events) {
|
||||
if (event.type === "timeline") {
|
||||
@@ -2002,6 +2031,16 @@ export class ACPAgentSession implements AgentSession, ACPClient {
|
||||
}
|
||||
|
||||
private pushEvent(event: AgentStreamEvent): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: this.provider,
|
||||
sessionId: this.sessionId,
|
||||
turnId: getAgentStreamEventTurnId(event) ?? this.activeForegroundTurnId ?? undefined,
|
||||
event,
|
||||
},
|
||||
"provider.acp.event_emit",
|
||||
);
|
||||
for (const subscriber of this.subscribers) {
|
||||
subscriber(event);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,11 @@ import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
|
||||
import type { AgentSession, AgentStreamEvent } from "../../agent-sdk-types.js";
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentSession,
|
||||
type AgentStreamEvent,
|
||||
} from "../../agent-sdk-types.js";
|
||||
import { isProviderAvailable } from "../../../daemon-e2e/agent-configs.js";
|
||||
import { ClaudeAgentClient } from "./agent.js";
|
||||
|
||||
@@ -37,15 +41,14 @@ function isTerminalEvent(event: AgentStreamEvent): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
// turnId is optional on AgentStreamEvent — this narrows to events where it's present.
|
||||
type EventWithTurnId = AgentStreamEvent & { turnId: string };
|
||||
|
||||
function hasTurnId(event: AgentStreamEvent): event is EventWithTurnId {
|
||||
return "turnId" in event && typeof (event as Record<string, unknown>).turnId === "string";
|
||||
return getAgentStreamEventTurnId(event) !== undefined;
|
||||
}
|
||||
|
||||
function eventsForTurn(events: AgentStreamEvent[], turnId: string): AgentStreamEvent[] {
|
||||
return events.filter((e) => hasTurnId(e) && e.turnId === turnId);
|
||||
return events.filter((e) => getAgentStreamEventTurnId(e) === turnId);
|
||||
}
|
||||
|
||||
function userMessagesWithText(events: AgentStreamEvent[], text: string): AgentStreamEvent[] {
|
||||
@@ -157,7 +160,7 @@ function assertInvariants(events: AgentStreamEvent[], foregroundTurnIds: string[
|
||||
// Invariant 2: Every turn_started has exactly one matching terminal
|
||||
const turnStartedIds = events
|
||||
.filter((e) => e.type === "turn_started" && hasTurnId(e))
|
||||
.map((e) => (e as EventWithTurnId).turnId);
|
||||
.map((e) => e.turnId);
|
||||
|
||||
for (const turnId of turnStartedIds) {
|
||||
const terminals = eventsForTurn(events, turnId).filter(isTerminalEvent);
|
||||
@@ -277,7 +280,7 @@ test("Test 3: Lifecycle doesn't get stuck in running", async () => {
|
||||
|
||||
// Any turn_started after terminal must have a different turnId
|
||||
for (const ts of afterTerminal.filter((e) => e.type === "turn_started" && hasTurnId(e))) {
|
||||
expect((ts as EventWithTurnId).turnId).not.toBe(turnId);
|
||||
expect(ts.turnId).not.toBe(turnId);
|
||||
}
|
||||
|
||||
assertInvariants(events, [turnId]);
|
||||
@@ -313,8 +316,9 @@ test("Test 4: Autonomous run", async () => {
|
||||
|
||||
// Autonomous turn_started with a different turnId
|
||||
const autoStarts = afterForeground.filter(
|
||||
(e) => e.type === "turn_started" && hasTurnId(e) && e.turnId !== fgTurnId,
|
||||
) as EventWithTurnId[];
|
||||
(e): e is EventWithTurnId =>
|
||||
e.type === "turn_started" && hasTurnId(e) && e.turnId !== fgTurnId,
|
||||
);
|
||||
if (autoStarts.length === 0) {
|
||||
assertInvariants(events, [fgTurnId]);
|
||||
return;
|
||||
|
||||
@@ -42,34 +42,35 @@ import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "../prov
|
||||
import { renderPromptAttachmentAsText } from "../../prompt-attachments.js";
|
||||
import { claudeQuery, type ClaudeOptions, type ClaudeQueryFactory } from "./query.js";
|
||||
|
||||
import type {
|
||||
AgentPermissionAction,
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentCreateSessionOptions,
|
||||
AgentLaunchContext,
|
||||
AgentMetadata,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionRequestKind,
|
||||
AgentPermissionResponse,
|
||||
AgentPermissionUpdate,
|
||||
AgentPersistenceHandle,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
AgentRuntimeInfo,
|
||||
ListModelsOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentPermissionAction,
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentCreateSessionOptions,
|
||||
type AgentLaunchContext,
|
||||
type AgentMetadata,
|
||||
type AgentMode,
|
||||
type AgentModelDefinition,
|
||||
type AgentPermissionRequest,
|
||||
type AgentPermissionRequestKind,
|
||||
type AgentPermissionResponse,
|
||||
type AgentPermissionUpdate,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentSlashCommand,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
type AgentUsage,
|
||||
type AgentRuntimeInfo,
|
||||
type ListModelsOptions,
|
||||
type ListPersistedAgentsOptions,
|
||||
type McpServerConfig,
|
||||
type PersistedAgentDescriptor,
|
||||
} from "../../agent-sdk-types.js";
|
||||
import {
|
||||
createProviderEnv,
|
||||
@@ -250,6 +251,7 @@ interface ClaudeAgentSessionOptions {
|
||||
defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
runtimeSettings?: ProviderRuntimeSettings;
|
||||
handle?: AgentPersistenceHandle;
|
||||
agentId?: string;
|
||||
launchEnv?: Record<string, string>;
|
||||
persistSession?: boolean;
|
||||
logger: Logger;
|
||||
@@ -1152,8 +1154,6 @@ export function readEventIdentifiers(message: SDKMessage): EventIdentifiers {
|
||||
};
|
||||
}
|
||||
|
||||
const claudeDebug = process.env.PASEO_CLAUDE_DEBUG === "1";
|
||||
|
||||
export class ClaudeAgentClient implements AgentClient {
|
||||
readonly provider = "claude" as const;
|
||||
readonly capabilities = CLAUDE_CAPABILITIES;
|
||||
@@ -1181,6 +1181,7 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
return new ClaudeAgentSession(claudeConfig, {
|
||||
defaults: this.defaults,
|
||||
runtimeSettings: this.runtimeSettings,
|
||||
agentId: launchContext?.agentId,
|
||||
launchEnv: launchContext?.env,
|
||||
persistSession: options?.persistSession,
|
||||
logger: this.logger,
|
||||
@@ -1209,6 +1210,7 @@ export class ClaudeAgentClient implements AgentClient {
|
||||
defaults: this.defaults,
|
||||
runtimeSettings: this.runtimeSettings,
|
||||
handle,
|
||||
agentId: launchContext?.agentId,
|
||||
launchEnv: launchContext?.env,
|
||||
logger: this.logger,
|
||||
queryFactory: this.queryFactory,
|
||||
@@ -1477,6 +1479,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
private readonly config: ClaudeAgentConfig;
|
||||
private readonly launchEnv?: Record<string, string>;
|
||||
private readonly agentId?: string;
|
||||
private readonly defaults?: { agents?: Record<string, AgentDefinition> };
|
||||
private readonly runtimeSettings?: ProviderRuntimeSettings;
|
||||
private readonly persistSession?: boolean;
|
||||
@@ -1526,10 +1529,11 @@ class ClaudeAgentSession implements AgentSession {
|
||||
constructor(config: ClaudeAgentConfig, options: ClaudeAgentSessionOptions) {
|
||||
this.config = config;
|
||||
this.launchEnv = options.launchEnv;
|
||||
this.agentId = options.agentId;
|
||||
this.defaults = options.defaults;
|
||||
this.runtimeSettings = options.runtimeSettings;
|
||||
this.persistSession = options.persistSession;
|
||||
this.logger = options.logger;
|
||||
this.logger = options.logger.child({ agentId: this.agentId });
|
||||
this.queryFactory = options.queryFactory;
|
||||
this.resolveBinary = options.resolveBinary;
|
||||
const handle = options.handle;
|
||||
@@ -1869,13 +1873,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
async close(): Promise<void> {
|
||||
this.logger.trace(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
turnState: this.turnState,
|
||||
hasQuery: Boolean(this.query),
|
||||
hasInput: Boolean(this.input),
|
||||
hasActiveForegroundTurnId: Boolean(this.activeForegroundTurnId),
|
||||
},
|
||||
"Claude session close: start",
|
||||
"provider.claude.session_close.start",
|
||||
);
|
||||
this.closed = true;
|
||||
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
||||
@@ -1910,8 +1917,13 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
}
|
||||
this.logger.trace(
|
||||
{ claudeSessionId: this.claudeSessionId, turnState: this.turnState },
|
||||
"Claude session close: completed",
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnState: this.turnState,
|
||||
},
|
||||
"provider.claude.session_close.complete",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2188,16 +2200,41 @@ class ClaudeAgentSession implements AgentSession {
|
||||
label: string,
|
||||
): Promise<void> {
|
||||
if (!promise) {
|
||||
this.logger.trace({ label }, "Claude query operation skipped (no promise)");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
label,
|
||||
},
|
||||
"provider.claude.query_operation.skip",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
this.logger.trace({ label }, "Claude query operation wait start");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
label,
|
||||
},
|
||||
"provider.claude.query_operation.start",
|
||||
);
|
||||
try {
|
||||
await withTimeout(promise, 3_000, "timeout");
|
||||
this.logger.trace(
|
||||
{ label, durationMs: Date.now() - startedAt },
|
||||
"Claude query operation settled",
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
label,
|
||||
durationMs: Date.now() - startedAt,
|
||||
},
|
||||
"provider.claude.query_operation.settled",
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error, label }, "Claude query operation did not settle cleanly");
|
||||
@@ -2593,7 +2630,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
const pump = this.runQueryPump().catch((error) => {
|
||||
this.logger.trace({ err: error }, "Claude query pump exited unexpectedly");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
err: error,
|
||||
},
|
||||
"provider.claude.query_pump.exit_unexpected",
|
||||
);
|
||||
});
|
||||
|
||||
this.queryPumpPromise = pump;
|
||||
@@ -2609,24 +2655,34 @@ class ClaudeAgentSession implements AgentSession {
|
||||
try {
|
||||
activeQuery = await this.ensureQuery();
|
||||
} catch (error) {
|
||||
this.logger.trace({ err: error }, "Failed to initialize Claude query pump");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
err: error,
|
||||
},
|
||||
"provider.claude.query_pump.init_failed",
|
||||
);
|
||||
this.failActiveTurns(error instanceof Error ? error.message : "Claude stream failed");
|
||||
return;
|
||||
}
|
||||
|
||||
let consecutiveInterruptAbortRecoveries = 0;
|
||||
const logRawMessage = (message: SDKMessage): void => {
|
||||
if (!claudeDebug) {
|
||||
return;
|
||||
}
|
||||
this.logger.trace(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
messageType: message.type,
|
||||
messageSubtype: "subtype" in message ? message.subtype : undefined,
|
||||
messageUuid: "uuid" in message ? message.uuid : undefined,
|
||||
rawEvent: message,
|
||||
},
|
||||
"Claude query pump: raw SDK message",
|
||||
"provider.claude.raw_event",
|
||||
);
|
||||
};
|
||||
const handlePumpedMessage = async (message: SDKMessage): Promise<boolean> => {
|
||||
@@ -2739,16 +2795,18 @@ class ClaudeAgentSession implements AgentSession {
|
||||
const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? null;
|
||||
const identifiers = readEventIdentifiers(message);
|
||||
|
||||
if (claudeDebug) {
|
||||
this.logger.trace(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
messageType: message.type,
|
||||
turnId,
|
||||
},
|
||||
"Claude query pump: SDK message",
|
||||
);
|
||||
}
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: turnId ?? undefined,
|
||||
messageType: message.type,
|
||||
identifiers,
|
||||
rawEvent: message,
|
||||
},
|
||||
"provider.claude.parsed_event",
|
||||
);
|
||||
|
||||
const messageEvents = this.translateMessageToEvents(message, {
|
||||
suppressAssistantText: true,
|
||||
@@ -2816,7 +2874,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
|
||||
this.logger.warn(
|
||||
{
|
||||
claudeSessionId: this.claudeSessionId,
|
||||
error: staleResumeError,
|
||||
},
|
||||
"Claude resumed session no longer exists; invalidating persisted session",
|
||||
@@ -2846,7 +2903,15 @@ class ClaudeAgentSession implements AgentSession {
|
||||
private async interruptActiveTurn(): Promise<void> {
|
||||
const queryToInterrupt = this.query;
|
||||
if (!queryToInterrupt || typeof queryToInterrupt.interrupt !== "function") {
|
||||
this.logger.trace("interruptActiveTurn: no query to interrupt");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: this.activeForegroundTurnId ?? this.autonomousTurn?.id ?? undefined,
|
||||
},
|
||||
"provider.claude.interrupt.no_query",
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.pendingInterruptAbort = true;
|
||||
@@ -3455,6 +3520,16 @@ class ClaudeAgentSession implements AgentSession {
|
||||
private notifySubscribers(event: AgentStreamEvent): void {
|
||||
const turnId = this.activeForegroundTurnId ?? this.autonomousTurn?.id;
|
||||
const tagged = turnId ? { ...event, turnId } : event;
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "claude",
|
||||
sessionId: this.claudeSessionId,
|
||||
turnId: getAgentStreamEventTurnId(tagged),
|
||||
event: tagged,
|
||||
},
|
||||
"provider.claude.event_emit",
|
||||
);
|
||||
for (const callback of this.subscribers) {
|
||||
try {
|
||||
callback(tagged);
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import type {
|
||||
AgentPermissionAction,
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentCreateSessionOptions,
|
||||
AgentFeature,
|
||||
AgentLaunchContext,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
McpServerConfig,
|
||||
AgentPersistenceHandle,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
AgentPermissionResult,
|
||||
AgentPromptContentBlock,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentRuntimeInfo,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
ToolCallTimelineItem,
|
||||
AgentUsage,
|
||||
ListModelsOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
PersistedAgentDescriptor,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentPermissionAction,
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentCreateSessionOptions,
|
||||
type AgentFeature,
|
||||
type AgentLaunchContext,
|
||||
type AgentMode,
|
||||
type AgentModelDefinition,
|
||||
type McpServerConfig,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPermissionRequest,
|
||||
type AgentPermissionResponse,
|
||||
type AgentPermissionResult,
|
||||
type AgentPromptContentBlock,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentRuntimeInfo,
|
||||
type AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentSlashCommand,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
type ToolCallTimelineItem,
|
||||
type AgentUsage,
|
||||
type ListModelsOptions,
|
||||
type ListPersistedAgentsOptions,
|
||||
type PersistedAgentDescriptor,
|
||||
} from "../agent-sdk-types.js";
|
||||
import type { Logger } from "pino";
|
||||
import { homedir } from "node:os";
|
||||
@@ -55,7 +56,10 @@ import { findExecutable, isCommandAvailable } from "../../../utils/executable.js
|
||||
import { spawnProcess } from "../../../utils/spawn.js";
|
||||
import { extractCodexTerminalSessionId, nonEmptyString } from "./tool-call-mapper-utils.js";
|
||||
import { buildCodexFeatures, codexModelSupportsFastMode } from "./codex-feature-definitions.js";
|
||||
import { CodexAppServerClient } from "./codex/app-server-transport.js";
|
||||
import {
|
||||
CodexAppServerClient,
|
||||
type CodexAppServerTraceContext,
|
||||
} from "./codex/app-server-transport.js";
|
||||
import {
|
||||
renderProviderImageOutputAsAssistantMarkdown,
|
||||
type ProviderImageOutput,
|
||||
@@ -183,6 +187,7 @@ interface CodexAppServerAgentDeps {
|
||||
_createCodexClient?: (
|
||||
child: ChildProcessWithoutNullStreams,
|
||||
logger: Logger,
|
||||
getTraceContext: () => CodexAppServerTraceContext,
|
||||
) => CodexAppServerClientLike;
|
||||
}
|
||||
|
||||
@@ -2653,8 +2658,13 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
private readonly deps: CodexAppServerAgentDeps = {},
|
||||
private readonly ephemeral: boolean = false,
|
||||
private readonly goalsEnabled: boolean = false,
|
||||
private readonly agentId?: string,
|
||||
) {
|
||||
this.logger = logger.child({ module: "agent", provider: CODEX_PROVIDER });
|
||||
this.logger = logger.child({
|
||||
module: "agent",
|
||||
provider: CODEX_PROVIDER,
|
||||
agentId: this.agentId,
|
||||
});
|
||||
if (config.modeId === undefined) {
|
||||
throw new Error("Codex agent requires modeId to be specified");
|
||||
}
|
||||
@@ -2691,7 +2701,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
async connect(): Promise<void> {
|
||||
if (this.connected) return;
|
||||
const child = await this.spawnAppServer();
|
||||
this.client = new CodexAppServerClient(child, this.logger);
|
||||
this.client = new CodexAppServerClient(child, this.logger, () => this.traceContext());
|
||||
this.client.setNotificationHandler((method, params) => this.handleNotification(method, params));
|
||||
this.registerRequestHandlers();
|
||||
|
||||
@@ -2709,6 +2719,14 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
this.connected = true;
|
||||
}
|
||||
|
||||
private traceContext(): CodexAppServerTraceContext {
|
||||
return {
|
||||
agentId: this.agentId,
|
||||
sessionId: this.currentThreadId ?? undefined,
|
||||
turnId: this.activeForegroundTurnId ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async loadCollaborationModes(): Promise<void> {
|
||||
if (!this.client) return;
|
||||
try {
|
||||
@@ -2729,7 +2747,16 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.trace({ error }, "Failed to load collaboration modes");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: CODEX_PROVIDER,
|
||||
sessionId: this.currentThreadId,
|
||||
turnId: this.activeForegroundTurnId ?? undefined,
|
||||
error,
|
||||
},
|
||||
"provider.codex.metadata.collaboration_modes_failed",
|
||||
);
|
||||
this.collaborationModes = [];
|
||||
}
|
||||
this.refreshResolvedCollaborationMode();
|
||||
@@ -2761,7 +2788,16 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
this.cachedSkills = skills;
|
||||
} catch (error) {
|
||||
this.logger.trace({ error }, "Failed to load skills list");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: CODEX_PROVIDER,
|
||||
sessionId: this.currentThreadId,
|
||||
turnId: this.activeForegroundTurnId ?? undefined,
|
||||
error,
|
||||
},
|
||||
"provider.codex.metadata.skills_failed",
|
||||
);
|
||||
this.cachedSkills = [];
|
||||
}
|
||||
}
|
||||
@@ -3657,6 +3693,16 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
private notifySubscribers(event: AgentStreamEvent): void {
|
||||
const turnId = this.activeForegroundTurnId;
|
||||
const tagged = turnId ? { ...event, turnId } : event;
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: CODEX_PROVIDER,
|
||||
sessionId: this.currentThreadId,
|
||||
turnId: getAgentStreamEventTurnId(tagged),
|
||||
event: tagged,
|
||||
},
|
||||
"provider.codex.event_emit",
|
||||
);
|
||||
for (const callback of this.subscribers) {
|
||||
try {
|
||||
callback(tagged);
|
||||
@@ -3672,6 +3718,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
|
||||
private handleNotification(method: string, params: unknown): void {
|
||||
const parsed = CodexNotificationSchema.parse({ method, params });
|
||||
this.traceParsedNotification(method, params, parsed);
|
||||
switch (parsed.kind) {
|
||||
case "thread_started":
|
||||
this.handleThreadStartedNotification(parsed);
|
||||
@@ -3728,6 +3775,25 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
private traceParsedNotification(
|
||||
method: string,
|
||||
params: unknown,
|
||||
parsed: z.infer<typeof CodexNotificationSchema>,
|
||||
): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: CODEX_PROVIDER,
|
||||
sessionId: this.currentThreadId,
|
||||
turnId: this.activeForegroundTurnId ?? undefined,
|
||||
method,
|
||||
params,
|
||||
parsed,
|
||||
},
|
||||
"provider.codex.parsed_event",
|
||||
);
|
||||
}
|
||||
|
||||
private getSubAgentCallIdForThread(threadId: string | null | undefined): string | null {
|
||||
if (!threadId || threadId === this.currentThreadId) {
|
||||
return null;
|
||||
@@ -4320,7 +4386,17 @@ class CodexAppServerAgentSession implements AgentSession {
|
||||
return;
|
||||
}
|
||||
this.warnedUnknownNotificationMethods.add(method);
|
||||
this.logger.trace({ method, params }, "Unhandled Codex app-server notification method");
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: CODEX_PROVIDER,
|
||||
sessionId: this.currentThreadId,
|
||||
turnId: this.activeForegroundTurnId ?? undefined,
|
||||
method,
|
||||
params,
|
||||
},
|
||||
"provider.codex.event_unhandled",
|
||||
);
|
||||
}
|
||||
|
||||
private warnInvalidNotificationPayload(method: string, params: unknown): void {
|
||||
@@ -4598,7 +4674,14 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
const launchPrefix = await resolveCodexLaunchPrefix(this.runtimeSettings);
|
||||
const versionOutput = await resolveBinaryVersion(launchPrefix.command);
|
||||
const enabled = codexVersionAtLeast(versionOutput, CODEX_GOALS_MIN_VERSION);
|
||||
this.logger.trace({ versionOutput, enabled }, "Resolved codex goals feature gate");
|
||||
this.logger.trace(
|
||||
{
|
||||
provider: CODEX_PROVIDER,
|
||||
versionOutput,
|
||||
enabled,
|
||||
},
|
||||
"provider.codex.config.goals_resolved",
|
||||
);
|
||||
return enabled;
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error }, "Failed to probe codex version for goals gate");
|
||||
@@ -4611,7 +4694,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
|
||||
private async spawnAppServer(
|
||||
launchEnv?: Record<string, string>,
|
||||
options?: { goalsEnabled?: boolean },
|
||||
options?: { goalsEnabled?: boolean; agentId?: string },
|
||||
): Promise<ChildProcessWithoutNullStreams> {
|
||||
const launchPrefix = await resolveCodexLaunchPrefix(this.runtimeSettings);
|
||||
const args = [...launchPrefix.args, "app-server"];
|
||||
@@ -4620,10 +4703,12 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
}
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: options?.agentId,
|
||||
provider: CODEX_PROVIDER,
|
||||
launchPrefix,
|
||||
goalsEnabled: options?.goalsEnabled === true,
|
||||
},
|
||||
"Spawning Codex app server",
|
||||
"provider.codex.spawn",
|
||||
);
|
||||
const child = spawnProcess(launchPrefix.command, args, {
|
||||
detached: process.platform !== "win32",
|
||||
@@ -4655,10 +4740,12 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
sessionConfig,
|
||||
null,
|
||||
this.logger,
|
||||
() => this.spawnAppServer(launchContext?.env, { goalsEnabled }),
|
||||
() =>
|
||||
this.spawnAppServer(launchContext?.env, { goalsEnabled, agentId: launchContext?.agentId }),
|
||||
this.sessionDeps(),
|
||||
options?.persistSession === false,
|
||||
goalsEnabled,
|
||||
launchContext?.agentId,
|
||||
);
|
||||
await session.connect();
|
||||
return session;
|
||||
@@ -4681,10 +4768,12 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
merged,
|
||||
handle,
|
||||
this.logger,
|
||||
() => this.spawnAppServer(launchContext?.env, { goalsEnabled }),
|
||||
() =>
|
||||
this.spawnAppServer(launchContext?.env, { goalsEnabled, agentId: launchContext?.agentId }),
|
||||
this.sessionDeps(),
|
||||
false,
|
||||
goalsEnabled,
|
||||
launchContext?.agentId,
|
||||
);
|
||||
await session.connect();
|
||||
return session;
|
||||
@@ -4695,7 +4784,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
): Promise<PersistedAgentDescriptor[]> {
|
||||
const child = await this.spawnAppServer();
|
||||
const client =
|
||||
this.deps._createCodexClient?.(child, this.logger) ??
|
||||
this.deps._createCodexClient?.(child, this.logger, () => ({})) ??
|
||||
new CodexAppServerClient(child, this.logger);
|
||||
|
||||
try {
|
||||
|
||||
@@ -35,6 +35,12 @@ interface PendingRequest {
|
||||
type RequestHandler = (params: unknown) => unknown;
|
||||
type NotificationHandler = (method: string, params: unknown) => void;
|
||||
|
||||
export interface CodexAppServerTraceContext {
|
||||
agentId?: string;
|
||||
sessionId?: string;
|
||||
turnId?: string;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value != null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -54,6 +60,24 @@ function isJsonRpcNotification(msg: unknown): msg is JsonRpcNotification {
|
||||
return typeof msg.method === "string" && msg.id === undefined;
|
||||
}
|
||||
|
||||
function readProviderSessionId(params: unknown): string | undefined {
|
||||
if (!isRecord(params)) {
|
||||
return undefined;
|
||||
}
|
||||
return typeof params.threadId === "string" ? params.threadId : undefined;
|
||||
}
|
||||
|
||||
function readProviderTurnId(params: unknown): string | undefined {
|
||||
if (!isRecord(params)) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof params.turnId === "string") {
|
||||
return params.turnId;
|
||||
}
|
||||
const turn = params.turn;
|
||||
return isRecord(turn) && typeof turn.id === "string" ? turn.id : undefined;
|
||||
}
|
||||
|
||||
export class CodexAppServerClient {
|
||||
private readonly rl: readline.Interface;
|
||||
private readonly pending = new Map<number, PendingRequest>();
|
||||
@@ -66,6 +90,7 @@ export class CodexAppServerClient {
|
||||
constructor(
|
||||
private readonly child: ChildProcessWithoutNullStreams,
|
||||
private readonly logger: Logger,
|
||||
private readonly getTraceContext: () => CodexAppServerTraceContext = () => ({}),
|
||||
) {
|
||||
this.rl = readline.createInterface({ input: child.stdout });
|
||||
this.rl.on("line", (line) => {
|
||||
@@ -224,6 +249,19 @@ export class CodexAppServerClient {
|
||||
}
|
||||
|
||||
if (isJsonRpcNotification(raw)) {
|
||||
const traceContext = this.getTraceContext();
|
||||
this.logger.trace(
|
||||
{
|
||||
provider: "codex",
|
||||
agentId: traceContext.agentId,
|
||||
sessionId: traceContext.sessionId ?? readProviderSessionId(raw.params),
|
||||
turnId: traceContext.turnId ?? readProviderTurnId(raw.params),
|
||||
method: raw.method,
|
||||
params: raw.params,
|
||||
rawEvent: raw,
|
||||
},
|
||||
"provider.codex.raw_event",
|
||||
);
|
||||
this.notificationHandler?.(raw.method, raw.params);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,33 +13,34 @@ import { findExecutable, isCommandAvailable } from "../../../utils/executable.js
|
||||
import type { Logger } from "pino";
|
||||
import { z } from "zod";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentCreateSessionOptions,
|
||||
AgentLaunchContext,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
AgentPersistenceHandle,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentRuntimeInfo,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
ListModelsOptions,
|
||||
ListModesOptions,
|
||||
ListPersistedAgentsOptions,
|
||||
McpServerConfig,
|
||||
PersistedAgentDescriptor,
|
||||
ToolCallDetail,
|
||||
ToolCallTimelineItem,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentCreateSessionOptions,
|
||||
type AgentLaunchContext,
|
||||
type AgentMode,
|
||||
type AgentModelDefinition,
|
||||
type AgentPermissionRequest,
|
||||
type AgentPermissionResponse,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentRuntimeInfo,
|
||||
type AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentSlashCommand,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
type AgentUsage,
|
||||
type ListModelsOptions,
|
||||
type ListModesOptions,
|
||||
type ListPersistedAgentsOptions,
|
||||
type McpServerConfig,
|
||||
type PersistedAgentDescriptor,
|
||||
type ToolCallDetail,
|
||||
type ToolCallTimelineItem,
|
||||
} from "../agent-sdk-types.js";
|
||||
import { createProviderEnvSpec, type ProviderRuntimeSettings } from "../provider-launch-config.js";
|
||||
import { withTimeout } from "../../../utils/promise-timeout.js";
|
||||
@@ -966,7 +967,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
|
||||
async createSession(
|
||||
config: AgentSessionConfig,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
launchContext?: AgentLaunchContext,
|
||||
options?: AgentCreateSessionOptions,
|
||||
): Promise<AgentSession> {
|
||||
const openCodeConfig = this.assertConfig(config);
|
||||
@@ -1004,6 +1005,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
new Map(this.modelContextWindows),
|
||||
acquisition.release,
|
||||
options?.persistSession,
|
||||
launchContext?.agentId,
|
||||
);
|
||||
} catch (error) {
|
||||
acquisition.release();
|
||||
@@ -1014,7 +1016,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
async resumeSession(
|
||||
handle: AgentPersistenceHandle,
|
||||
overrides?: Partial<AgentSessionConfig>,
|
||||
_launchContext?: AgentLaunchContext,
|
||||
launchContext?: AgentLaunchContext,
|
||||
): Promise<AgentSession> {
|
||||
const cwd = overrides?.cwd ?? (handle.metadata?.cwd as string);
|
||||
if (!cwd) {
|
||||
@@ -1046,6 +1048,7 @@ export class OpenCodeAgentClient implements AgentClient {
|
||||
new Map(this.modelContextWindows),
|
||||
acquisition.release,
|
||||
undefined,
|
||||
launchContext?.agentId,
|
||||
);
|
||||
} catch (error) {
|
||||
acquisition.release();
|
||||
@@ -1277,6 +1280,28 @@ export interface OpenCodeEventTranslationState {
|
||||
onAssistantModelContextWindowResolved?: (contextWindowMaxTokens: number) => void;
|
||||
}
|
||||
|
||||
interface OpenCodeTraceData {
|
||||
turnId?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
type OpenCodeTraceMessage =
|
||||
| "provider.opencode.prompt_async.start"
|
||||
| "provider.opencode.prompt_async.response"
|
||||
| "provider.opencode.prompt_async.throw"
|
||||
| "provider.opencode.subscribe.start"
|
||||
| "provider.opencode.subscribe.ready"
|
||||
| "provider.opencode.stream.eof"
|
||||
| "provider.opencode.turn.fail_eof"
|
||||
| "provider.opencode.subscribe.error"
|
||||
| "provider.opencode.raw_event"
|
||||
| "provider.opencode.event.skip"
|
||||
| "provider.opencode.parsed_event"
|
||||
| "provider.opencode.parsed_event.skip_active"
|
||||
| "provider.opencode.event.terminal"
|
||||
| "provider.opencode.finish_foreground_turn"
|
||||
| "provider.opencode.event_emit";
|
||||
|
||||
type OpenCodeToolPartEventPart = Extract<
|
||||
Extract<OpenCodeEvent, { type: "message.part.updated" }>["properties"]["part"],
|
||||
{ type: "tool" }
|
||||
@@ -2141,18 +2166,6 @@ function createDeferred<T>(): Deferred<T> {
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
const OPENCODE_TRACE_ENABLED = process.env.PASEO_OPENCODE_TRACE === "1";
|
||||
|
||||
function traceOpenCode(tag: string, data: Record<string, unknown> = {}): void {
|
||||
if (!OPENCODE_TRACE_ENABLED) return;
|
||||
const line = JSON.stringify({ ts: new Date().toISOString(), tag, ...data }, (_k, v) => {
|
||||
if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack };
|
||||
if (typeof v === "bigint") return v.toString();
|
||||
return v;
|
||||
});
|
||||
process.stderr.write(`[opencode-trace] ${line}\n`);
|
||||
}
|
||||
|
||||
function unwrapOpenCodeGlobalEvent(event: unknown): OpenCodeEvent | null {
|
||||
const record = readOpenCodeRecord(event);
|
||||
if (!record) {
|
||||
@@ -2217,11 +2230,12 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
modelContextWindowsByModelKey: ReadonlyMap<string, number> = new Map(),
|
||||
releaseServer?: () => void,
|
||||
persistSession = true,
|
||||
private readonly agentId?: string,
|
||||
) {
|
||||
this.config = config;
|
||||
this.client = client;
|
||||
this.sessionId = sessionId;
|
||||
this.logger = logger;
|
||||
this.logger = logger.child({ agentId: this.agentId });
|
||||
this.modelContextWindowsByModelKey = modelContextWindowsByModelKey;
|
||||
this.currentMode = normalizeOpenCodeModeId(config.modeId);
|
||||
this.releaseServer = releaseServer ?? null;
|
||||
@@ -2483,7 +2497,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
// SDK input validation) is caught alongside async rejections. A plain
|
||||
// `.then().catch()` chain would let a sync throw escape unhandled.
|
||||
void (async () => {
|
||||
traceOpenCode("promptAsync.start", {
|
||||
this.traceOpenCode("provider.opencode.prompt_async.start", {
|
||||
turnId,
|
||||
sessionId: this.sessionId,
|
||||
model,
|
||||
@@ -2509,7 +2523,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
...(effectiveMode ? { agent: effectiveMode } : {}),
|
||||
...(effectiveVariant ? { variant: effectiveVariant } : {}),
|
||||
});
|
||||
traceOpenCode("promptAsync.response", {
|
||||
this.traceOpenCode("provider.opencode.prompt_async.response", {
|
||||
turnId,
|
||||
hasError: promptResponse.error !== undefined,
|
||||
error: promptResponse.error,
|
||||
@@ -2526,7 +2540,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
traceOpenCode("promptAsync.throw", {
|
||||
this.traceOpenCode("provider.opencode.prompt_async.throw", {
|
||||
turnId,
|
||||
error:
|
||||
error instanceof Error
|
||||
@@ -2560,7 +2574,11 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
turnAbortController: AbortController,
|
||||
subscriptionReady: Deferred<void>,
|
||||
): Promise<void> {
|
||||
traceOpenCode("subscribe.start", { turnId, sessionId: this.sessionId, cwd: this.config.cwd });
|
||||
this.traceOpenCode("provider.opencode.subscribe.start", {
|
||||
turnId,
|
||||
sessionId: this.sessionId,
|
||||
cwd: this.config.cwd,
|
||||
});
|
||||
try {
|
||||
const result = await this.client.global.event({
|
||||
signal: turnAbortController.signal,
|
||||
@@ -2572,7 +2590,10 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
eventCount += 1;
|
||||
if (!subscriptionReadyResolved) {
|
||||
subscriptionReadyResolved = true;
|
||||
traceOpenCode("subscribe.ready", { turnId, sessionId: this.sessionId });
|
||||
this.traceOpenCode("provider.opencode.subscribe.ready", {
|
||||
turnId,
|
||||
sessionId: this.sessionId,
|
||||
});
|
||||
subscriptionReady.resolve();
|
||||
}
|
||||
const shouldContinue = await this.consumeOpenCodeStreamEvent({
|
||||
@@ -2586,7 +2607,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
}
|
||||
}
|
||||
|
||||
traceOpenCode("stream.eof", {
|
||||
this.traceOpenCode("provider.opencode.stream.eof", {
|
||||
turnId,
|
||||
eventCount,
|
||||
aborted: turnAbortController.signal.aborted,
|
||||
@@ -2594,7 +2615,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
});
|
||||
|
||||
if (!turnAbortController.signal.aborted && this.activeForegroundTurnId === turnId) {
|
||||
traceOpenCode("turn.fail.eof", { turnId, eventCount });
|
||||
this.traceOpenCode("provider.opencode.turn.fail_eof", { turnId, eventCount });
|
||||
if (!subscriptionReadyResolved) {
|
||||
subscriptionReady.reject(new Error("OpenCode event stream ended before it became ready"));
|
||||
}
|
||||
@@ -2608,7 +2629,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
traceOpenCode("subscribe.error", {
|
||||
this.traceOpenCode("provider.opencode.subscribe.error", {
|
||||
turnId,
|
||||
error:
|
||||
error instanceof Error ? { name: error.name, message: error.message } : String(error),
|
||||
@@ -2649,19 +2670,20 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
}): Promise<boolean> {
|
||||
const { rawEvent, eventCount, turnId, turnAbortController } = params;
|
||||
const event = unwrapOpenCodeGlobalEvent(rawEvent);
|
||||
traceOpenCode("event.raw", {
|
||||
this.traceOpenCode("provider.opencode.raw_event", {
|
||||
turnId,
|
||||
n: eventCount,
|
||||
type: event?.type,
|
||||
rawType: readOpenCodeRecord(rawEvent)?.type,
|
||||
directory: readOpenCodeRecord(rawEvent)?.directory,
|
||||
properties: event ? (event as { properties?: unknown }).properties : undefined,
|
||||
rawEvent,
|
||||
properties: event?.properties,
|
||||
});
|
||||
if (!event) {
|
||||
return true;
|
||||
}
|
||||
if (turnAbortController.signal.aborted || this.activeForegroundTurnId !== turnId) {
|
||||
traceOpenCode("event.skip", {
|
||||
this.traceOpenCode("provider.opencode.event.skip", {
|
||||
turnId,
|
||||
n: eventCount,
|
||||
aborted: turnAbortController.signal.aborted,
|
||||
@@ -2672,16 +2694,17 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
|
||||
this.armRetryFailureTimerForStatus(event, turnId);
|
||||
const translated = await this.translateEvent(event);
|
||||
traceOpenCode("event.translated", {
|
||||
this.traceOpenCode("provider.opencode.parsed_event", {
|
||||
turnId,
|
||||
n: eventCount,
|
||||
count: translated.length,
|
||||
types: translated.map((t) => t.type),
|
||||
events: translated,
|
||||
});
|
||||
|
||||
for (const e of translated) {
|
||||
if (this.activeForegroundTurnId !== turnId) {
|
||||
traceOpenCode("event.translated.skip-active", { turnId, type: e.type });
|
||||
this.traceOpenCode("provider.opencode.parsed_event.skip_active", { turnId, type: e.type });
|
||||
return false;
|
||||
}
|
||||
if (e.type === "timeline" && e.item.type === "tool_call") {
|
||||
@@ -2689,7 +2712,10 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
}
|
||||
const terminalEvent = toTerminalTurnEvent(e);
|
||||
if (terminalEvent) {
|
||||
traceOpenCode("event.terminal", { turnId, type: terminalEvent.type });
|
||||
this.traceOpenCode("provider.opencode.event.terminal", {
|
||||
turnId,
|
||||
type: terminalEvent.type,
|
||||
});
|
||||
this.finishForegroundTurn(terminalEvent, turnId);
|
||||
return false;
|
||||
}
|
||||
@@ -2703,7 +2729,7 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
event: Extract<AgentStreamEvent, { type: "turn_completed" | "turn_failed" | "turn_canceled" }>,
|
||||
turnId: string,
|
||||
): void {
|
||||
traceOpenCode("finishForegroundTurn", {
|
||||
this.traceOpenCode("provider.opencode.finish_foreground_turn", {
|
||||
turnId,
|
||||
activeTurnId: this.activeForegroundTurnId,
|
||||
type: event.type,
|
||||
@@ -2803,6 +2829,10 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
private notifySubscribers(event: AgentStreamEvent, turnIdOverride?: string): void {
|
||||
const turnId = turnIdOverride ?? this.activeForegroundTurnId;
|
||||
const tagged = turnId ? { ...event, turnId } : event;
|
||||
this.traceOpenCode("provider.opencode.event_emit", {
|
||||
turnId: getAgentStreamEventTurnId(tagged),
|
||||
event: tagged,
|
||||
});
|
||||
for (const callback of this.subscribers) {
|
||||
try {
|
||||
callback(tagged);
|
||||
@@ -2816,6 +2846,19 @@ class OpenCodeAgentSession implements AgentSession {
|
||||
return `opencode-turn-${this.nextTurnOrdinal++}`;
|
||||
}
|
||||
|
||||
private traceOpenCode(msg: OpenCodeTraceMessage, data: OpenCodeTraceData = {}): void {
|
||||
this.logger.trace(
|
||||
{
|
||||
agentId: this.agentId,
|
||||
provider: "opencode",
|
||||
sessionId: this.sessionId,
|
||||
turnId: data.turnId ?? this.activeForegroundTurnId ?? undefined,
|
||||
...data,
|
||||
},
|
||||
msg,
|
||||
);
|
||||
}
|
||||
|
||||
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
|
||||
const response = await this.client.session.messages({
|
||||
sessionID: this.sessionId,
|
||||
|
||||
@@ -30,29 +30,30 @@ import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
|
||||
import { z } from "zod";
|
||||
|
||||
import type {
|
||||
AgentCapabilityFlags,
|
||||
AgentClient,
|
||||
AgentLaunchContext,
|
||||
AgentMetadata,
|
||||
AgentMode,
|
||||
AgentModelDefinition,
|
||||
AgentPermissionRequest,
|
||||
AgentPermissionResponse,
|
||||
AgentPersistenceHandle,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentRuntimeInfo,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentSlashCommand,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
ListModesOptions,
|
||||
ListModelsOptions,
|
||||
ToolCallDetail,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentCapabilityFlags,
|
||||
type AgentClient,
|
||||
type AgentLaunchContext,
|
||||
type AgentMetadata,
|
||||
type AgentMode,
|
||||
type AgentModelDefinition,
|
||||
type AgentPermissionRequest,
|
||||
type AgentPermissionResponse,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentRuntimeInfo,
|
||||
type AgentSession,
|
||||
type AgentSessionConfig,
|
||||
type AgentSlashCommand,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
type AgentUsage,
|
||||
type ListModesOptions,
|
||||
type ListModelsOptions,
|
||||
type ToolCallDetail,
|
||||
} from "../agent-sdk-types.js";
|
||||
import type { ProviderRuntimeSettings } from "../provider-launch-config.js";
|
||||
import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
|
||||
@@ -738,21 +739,6 @@ function parsePersistenceMetadata(metadata: AgentMetadata | undefined): PiPersis
|
||||
return {};
|
||||
}
|
||||
|
||||
function getStreamEventTurnId(event: AgentStreamEvent): string | undefined {
|
||||
switch (event.type) {
|
||||
case "turn_started":
|
||||
case "turn_completed":
|
||||
case "turn_failed":
|
||||
case "turn_canceled":
|
||||
case "timeline":
|
||||
case "permission_requested":
|
||||
case "permission_resolved":
|
||||
return event.turnId;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isPiRequestAbortError(error: unknown): boolean {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
return true;
|
||||
@@ -1045,7 +1031,7 @@ export class PiDirectAgentSession implements AgentSession {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTurnId = getStreamEventTurnId(event);
|
||||
const eventTurnId = getAgentStreamEventTurnId(event);
|
||||
if (turnId && eventTurnId && eventTurnId !== turnId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type {
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentRunResult,
|
||||
AgentStreamEvent,
|
||||
AgentTimelineItem,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentRunResult,
|
||||
type AgentStreamEvent,
|
||||
type AgentTimelineItem,
|
||||
} from "../agent-sdk-types.js";
|
||||
|
||||
export type ProviderFinalTextReducer = (params: {
|
||||
@@ -44,7 +45,7 @@ export async function runProviderTurn({
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
const eventTurnId = "turnId" in event ? event.turnId : undefined;
|
||||
const eventTurnId = getAgentStreamEventTurnId(event);
|
||||
if (turnId && eventTurnId && eventTurnId !== turnId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type {
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentSession,
|
||||
AgentStreamEvent,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentSession,
|
||||
type AgentStreamEvent,
|
||||
} from "../../agent-sdk-types.js";
|
||||
|
||||
function isTerminalEvent(event: AgentStreamEvent): boolean {
|
||||
@@ -29,7 +30,7 @@ export async function* streamSession(
|
||||
};
|
||||
|
||||
const matchesTurn = (event: AgentStreamEvent): boolean => {
|
||||
const eventTurnId = (event as { turnId?: string }).turnId;
|
||||
const eventTurnId = getAgentStreamEventTurnId(event);
|
||||
return turnId == null || eventTurnId == null || eventTurnId === turnId;
|
||||
};
|
||||
|
||||
|
||||
@@ -106,6 +106,31 @@ describe("resolveLogConfig", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults file output to info when log.file is present without a level", () => {
|
||||
const config: PersistedConfig = {
|
||||
log: {
|
||||
console: {
|
||||
level: "warn",
|
||||
},
|
||||
file: {
|
||||
path: "daemon.log",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveLogConfig(config, { paseoHome })).toEqual({
|
||||
level: "info",
|
||||
console: {
|
||||
level: "warn",
|
||||
format: "json",
|
||||
},
|
||||
file: {
|
||||
level: "info",
|
||||
path: path.resolve(paseoHome, "daemon.log"),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadConfig logger config", () => {
|
||||
|
||||
@@ -43,7 +43,7 @@ const LOG_LEVEL_PRIORITIES: Record<LogLevel, number> = {
|
||||
|
||||
const DEFAULT_CONSOLE_LEVEL: LogLevel = "info";
|
||||
const DEFAULT_CONSOLE_FORMAT: LogFormat = "json";
|
||||
const DEFAULT_FILE_LEVEL: LogLevel = "debug";
|
||||
const DEFAULT_FILE_LEVEL: LogLevel = "info";
|
||||
const DEFAULT_DAEMON_LOG_FILENAME = "daemon.log";
|
||||
const REDACT_PATHS = [
|
||||
"authorization",
|
||||
|
||||
@@ -119,16 +119,17 @@ import {
|
||||
StructuredAgentResponseError,
|
||||
generateStructuredAgentResponseWithFallback,
|
||||
} from "./agent/agent-response-loop.js";
|
||||
import type {
|
||||
AgentPersistenceHandle,
|
||||
AgentPermissionResponse,
|
||||
AgentProvider,
|
||||
AgentPromptContentBlock,
|
||||
AgentPromptInput,
|
||||
AgentRunOptions,
|
||||
AgentSessionConfig,
|
||||
AgentStreamEvent,
|
||||
ProviderSnapshotEntry,
|
||||
import {
|
||||
getAgentStreamEventTurnId,
|
||||
type AgentPersistenceHandle,
|
||||
type AgentPermissionResponse,
|
||||
type AgentProvider,
|
||||
type AgentPromptContentBlock,
|
||||
type AgentPromptInput,
|
||||
type AgentRunOptions,
|
||||
type AgentSessionConfig,
|
||||
type AgentStreamEvent,
|
||||
type ProviderSnapshotEntry,
|
||||
} from "./agent/agent-sdk-types.js";
|
||||
import type { StoredAgentRecord } from "./agent/agent-storage.js";
|
||||
import type { AgentStorage } from "./agent/agent-storage.js";
|
||||
@@ -895,7 +896,7 @@ export class Session {
|
||||
void this.initializeAgentMcp();
|
||||
this.subscribeToAgentEvents();
|
||||
|
||||
this.sessionLogger.trace("Session created");
|
||||
this.sessionLogger.trace({}, "agent.session.lifecycle.created");
|
||||
}
|
||||
|
||||
updateAppVersion(appVersion: string | null): void {
|
||||
@@ -1017,15 +1018,20 @@ export class Session {
|
||||
private async interruptAgentIfRunning(agentId: string): Promise<void> {
|
||||
const snapshot = this.agentManager.getAgent(agentId);
|
||||
if (!snapshot) {
|
||||
this.sessionLogger.trace({ agentId }, "interruptAgentIfRunning: agent not found");
|
||||
this.sessionLogger.trace({ agentId }, "agent.session.interrupt.not_found");
|
||||
throw new Error(`Agent ${agentId} not found`);
|
||||
}
|
||||
|
||||
const hasInFlightRun = this.agentManager.hasInFlightRun(agentId);
|
||||
if (!hasInFlightRun) {
|
||||
this.sessionLogger.trace(
|
||||
{ agentId, lifecycle: snapshot.lifecycle, hasInFlightRun },
|
||||
"interruptAgentIfRunning: skipping because agent is not running",
|
||||
{
|
||||
agentId,
|
||||
provider: snapshot.provider,
|
||||
lifecycle: snapshot.lifecycle,
|
||||
hasInFlightRun,
|
||||
},
|
||||
"agent.session.interrupt.skip_not_running",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1070,7 +1076,7 @@ export class Session {
|
||||
promptType: typeof prompt === "string" ? "string" : "structured",
|
||||
hasRunOptions: Boolean(runOptions),
|
||||
},
|
||||
"startAgentStream: requested",
|
||||
"agent.session.start_stream.request",
|
||||
);
|
||||
let iterator: AsyncGenerator<AgentStreamEvent>;
|
||||
try {
|
||||
@@ -1080,7 +1086,7 @@ export class Session {
|
||||
: this.agentManager.streamAgent(agentId, prompt, runOptions);
|
||||
this.sessionLogger.trace(
|
||||
{ agentId, shouldReplace },
|
||||
"startAgentStream: agent iterator returned",
|
||||
"agent.session.start_stream.iterator_returned",
|
||||
);
|
||||
} catch (error) {
|
||||
this.handleAgentRunError(agentId, error, "Failed to start agent run");
|
||||
@@ -1092,9 +1098,9 @@ export class Session {
|
||||
for await (const _ of iterator) {
|
||||
// Events are forwarded via the session's AgentManager subscription.
|
||||
}
|
||||
this.sessionLogger.trace({ agentId }, "startAgentStream: iterator drained");
|
||||
this.sessionLogger.trace({ agentId }, "agent.session.iterator.drained");
|
||||
} catch (error) {
|
||||
this.sessionLogger.trace({ agentId, err: error }, "startAgentStream: iterator threw");
|
||||
this.sessionLogger.trace({ agentId, err: error }, "agent.session.iterator.error");
|
||||
this.handleAgentRunError(agentId, error, "Agent stream failed");
|
||||
}
|
||||
})();
|
||||
@@ -1135,10 +1141,7 @@ export class Session {
|
||||
|
||||
this.agentTools = (await this.agentMcpClient.tools()) as ToolSet;
|
||||
const agentToolCount = Object.keys(this.agentTools ?? {}).length;
|
||||
this.sessionLogger.trace(
|
||||
{ agentToolCount },
|
||||
`Agent MCP initialized with ${agentToolCount} tools`,
|
||||
);
|
||||
this.sessionLogger.trace({ agentToolCount }, "agent.session.mcp_init");
|
||||
} catch (error) {
|
||||
this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
|
||||
}
|
||||
@@ -1210,6 +1213,16 @@ export class Session {
|
||||
this.unsubscribeAgentEvents = this.agentManager.subscribe(
|
||||
(event) => {
|
||||
if (event.type === "agent_state") {
|
||||
this.sessionLogger.trace(
|
||||
{
|
||||
agentId: event.agent.id,
|
||||
provider: event.agent.provider,
|
||||
providerSessionId: event.agent.persistence?.sessionId ?? undefined,
|
||||
turnId: event.agent.activeForegroundTurnId ?? undefined,
|
||||
lifecycle: event.agent.lifecycle,
|
||||
},
|
||||
"agent.session.forward_update",
|
||||
);
|
||||
void this.forwardAgentUpdate(event.agent);
|
||||
return;
|
||||
}
|
||||
@@ -1260,6 +1273,17 @@ export class Session {
|
||||
if (!serializedEvent) {
|
||||
return;
|
||||
}
|
||||
this.sessionLogger.trace(
|
||||
{
|
||||
agentId: event.agentId,
|
||||
provider: event.event.provider,
|
||||
turnId: getAgentStreamEventTurnId(event.event),
|
||||
seq: event.seq,
|
||||
epoch: event.epoch,
|
||||
event: event.event,
|
||||
},
|
||||
"agent.session.forward_stream",
|
||||
);
|
||||
|
||||
const payload = {
|
||||
agentId: event.agentId,
|
||||
@@ -1612,8 +1636,11 @@ export class Session {
|
||||
}
|
||||
try {
|
||||
this.sessionLogger.trace(
|
||||
{ messageType: msg.type, payloadBytes: JSON.stringify(msg).length },
|
||||
"inbound message",
|
||||
{
|
||||
messageType: msg.type,
|
||||
payloadBytes: JSON.stringify(msg).length,
|
||||
},
|
||||
"agent.session.inbound",
|
||||
);
|
||||
try {
|
||||
await this.dispatchInboundMessage(msg);
|
||||
@@ -7180,8 +7207,12 @@ export class Session {
|
||||
|
||||
const prompt = this.buildAgentPrompt(msg.text, msg.images, msg.attachments);
|
||||
this.sessionLogger.trace(
|
||||
{ agentId, messageId: msg.messageId, textPrefix: msg.text.slice(0, 80) },
|
||||
"send_agent_message_request: dispatching shared sendPromptToAgent",
|
||||
{
|
||||
agentId,
|
||||
messageId: msg.messageId,
|
||||
textPrefix: msg.text.slice(0, 80),
|
||||
},
|
||||
"agent.session.send_agent_message",
|
||||
);
|
||||
let dispatchResult: { outOfBand: boolean };
|
||||
try {
|
||||
@@ -7966,8 +7997,11 @@ export class Session {
|
||||
*/
|
||||
private emit(msg: SessionOutboundMessage): void {
|
||||
this.sessionLogger.trace(
|
||||
{ messageType: msg.type, payloadBytes: JSON.stringify(msg).length },
|
||||
"outbound message",
|
||||
{
|
||||
messageType: msg.type,
|
||||
payloadBytes: JSON.stringify(msg).length,
|
||||
},
|
||||
"agent.session.outbound",
|
||||
);
|
||||
if (
|
||||
msg.type === "audio_output" &&
|
||||
@@ -8035,7 +8069,7 @@ export class Session {
|
||||
* Clean up session resources
|
||||
*/
|
||||
public async cleanup(): Promise<void> {
|
||||
this.sessionLogger.trace("Cleaning up");
|
||||
this.sessionLogger.trace({}, "agent.session.lifecycle.cleanup");
|
||||
|
||||
if (this.unsubscribeAgentEvents) {
|
||||
this.unsubscribeAgentEvents();
|
||||
|
||||
Reference in New Issue
Block a user