Confirm create-agent start before returning
Wait for the initial background turn to actually start before create returns a snapshot, without waiting for completion. Freshly created agents now surface start failures as create failures, and both the WS/client and MCP surfaces reflect the shared create-agent lifecycle helper.
This commit is contained in:
@@ -11,6 +11,14 @@ import pino from "pino";
|
||||
import { withTimeout } from "../../utils/promise-timeout.js";
|
||||
import { createPaseoDaemon, type PaseoDaemonConfig } from "../bootstrap.js";
|
||||
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
|
||||
import type {
|
||||
AgentClient,
|
||||
AgentPersistenceHandle,
|
||||
AgentRunResult,
|
||||
AgentSession,
|
||||
AgentSessionConfig,
|
||||
AgentStreamEvent,
|
||||
} from "./agent-sdk-types.js";
|
||||
|
||||
interface StructuredContent {
|
||||
[key: string]: unknown;
|
||||
@@ -19,6 +27,7 @@ interface StructuredContent {
|
||||
interface McpToolResult {
|
||||
structuredContent?: StructuredContent;
|
||||
content?: Array<{ structuredContent?: StructuredContent } | StructuredContent>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
interface McpClient {
|
||||
@@ -276,6 +285,222 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
test("create_agent with background initialPrompt reflects running state once the first turn starts", async () => {
|
||||
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
|
||||
const port = await getAvailablePort();
|
||||
|
||||
const daemonConfig: PaseoDaemonConfig = {
|
||||
listen: `127.0.0.1:${port}`,
|
||||
paseoHome,
|
||||
corsAllowedOrigins: [],
|
||||
hostnames: true,
|
||||
mcpEnabled: true,
|
||||
staticDir,
|
||||
mcpDebug: false,
|
||||
agentClients: createTestAgentClients(),
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
};
|
||||
|
||||
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
||||
await daemon.start();
|
||||
|
||||
const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`);
|
||||
|
||||
let agentId: string | null = null;
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "create_agent",
|
||||
args: {
|
||||
cwd: agentCwd,
|
||||
title: "MCP background create",
|
||||
provider: "codex/gpt-5.4-mini",
|
||||
mode: "full-access",
|
||||
initialPrompt: "Run exactly: sleep 30",
|
||||
background: true,
|
||||
},
|
||||
});
|
||||
|
||||
const payload = getStructuredContent(result);
|
||||
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||
expect(agentId).toBeTruthy();
|
||||
expect(payload?.status).toBe("running");
|
||||
|
||||
const statusResult = await client.callTool({
|
||||
name: "get_agent_status",
|
||||
args: { agentId },
|
||||
});
|
||||
const statusPayload = getStructuredContent(statusResult);
|
||||
expect(statusPayload?.status).toBe("running");
|
||||
} finally {
|
||||
if (agentId) {
|
||||
await client.callTool({ name: "kill_agent", args: { agentId } });
|
||||
}
|
||||
await client.close();
|
||||
await daemon.stop();
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
await rm(staticDir, { recursive: true, force: true });
|
||||
await rm(agentCwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
test("create_agent propagates initial-turn start failure instead of returning success", async () => {
|
||||
class StartTurnFailureSession implements AgentSession {
|
||||
readonly provider = "codex" as const;
|
||||
readonly id = "mcp-start-turn-failure-session";
|
||||
readonly capabilities = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
supportsRewindConversation: false,
|
||||
supportsRewindFiles: false,
|
||||
supportsRewindBoth: false,
|
||||
} as const;
|
||||
|
||||
async run(): Promise<AgentRunResult> {
|
||||
return {
|
||||
sessionId: this.id,
|
||||
finalText: "",
|
||||
timeline: [],
|
||||
};
|
||||
}
|
||||
|
||||
async startTurn(): Promise<{ turnId: string }> {
|
||||
throw new Error("Initial turn failed to start");
|
||||
}
|
||||
|
||||
subscribe(_callback: (event: AgentStreamEvent) => void): () => void {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
|
||||
yield* [];
|
||||
}
|
||||
|
||||
async getRuntimeInfo() {
|
||||
return {
|
||||
provider: "codex" as const,
|
||||
sessionId: this.id,
|
||||
model: "gpt-5.4-mini",
|
||||
modeId: "full-access",
|
||||
};
|
||||
}
|
||||
|
||||
async getAvailableModes(): Promise<
|
||||
Array<{ id: string; label: string; description: string }>
|
||||
> {
|
||||
return [{ id: "full-access", label: "Full access", description: "No prompts" }];
|
||||
}
|
||||
|
||||
async getCurrentMode(): Promise<string | null> {
|
||||
return "full-access";
|
||||
}
|
||||
|
||||
async setMode(): Promise<void> {}
|
||||
|
||||
getPendingPermissions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async respondToPermission(): Promise<void> {}
|
||||
|
||||
describePersistence(): AgentPersistenceHandle | null {
|
||||
return { provider: "codex", sessionId: this.id };
|
||||
}
|
||||
|
||||
async interrupt(): Promise<void> {}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
|
||||
class StartTurnFailureClient implements AgentClient {
|
||||
readonly provider = "codex" as const;
|
||||
readonly capabilities = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
supportsRewindConversation: false,
|
||||
supportsRewindFiles: false,
|
||||
supportsRewindBoth: false,
|
||||
} as const;
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async createSession(_config: AgentSessionConfig): Promise<AgentSession> {
|
||||
return new StartTurnFailureSession();
|
||||
}
|
||||
|
||||
async resumeSession(
|
||||
_handle: AgentPersistenceHandle,
|
||||
_config?: Partial<AgentSessionConfig>,
|
||||
): Promise<AgentSession> {
|
||||
return new StartTurnFailureSession();
|
||||
}
|
||||
}
|
||||
|
||||
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
|
||||
const port = await getAvailablePort();
|
||||
|
||||
const daemonConfig: PaseoDaemonConfig = {
|
||||
listen: `127.0.0.1:${port}`,
|
||||
paseoHome,
|
||||
corsAllowedOrigins: [],
|
||||
hostnames: true,
|
||||
mcpEnabled: true,
|
||||
staticDir,
|
||||
mcpDebug: false,
|
||||
agentClients: {
|
||||
...createTestAgentClients(),
|
||||
codex: new StartTurnFailureClient(),
|
||||
},
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
};
|
||||
|
||||
const daemon = await createPaseoDaemon(daemonConfig, pino({ level: "silent" }));
|
||||
await daemon.start();
|
||||
|
||||
const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`);
|
||||
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "create_agent",
|
||||
args: {
|
||||
cwd: agentCwd,
|
||||
title: "MCP start failure",
|
||||
provider: "codex/gpt-5.4-mini",
|
||||
mode: "full-access",
|
||||
initialPrompt: "Run exactly: sleep 30",
|
||||
background: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const contentItem = result.content?.[0];
|
||||
const contentText: string | undefined =
|
||||
contentItem != null && typeof contentItem === "object"
|
||||
? Reflect.get(contentItem, "text")
|
||||
: undefined;
|
||||
expect(contentText ?? "").toContain("Initial turn failed to start");
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.stop();
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
await rm(staticDir, { recursive: true, force: true });
|
||||
await rm(agentCwd, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
test("create_agent with worktree is async and boots terminals only after setup success", async () => {
|
||||
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import type { AgentPromptInput, AgentRunOptions } from "./agent-sdk-types.js";
|
||||
import type { AgentManager } from "./agent-manager.js";
|
||||
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
|
||||
import type { AgentStorage } from "./agent-storage.js";
|
||||
import { ensureAgentLoaded } from "./agent-loading.js";
|
||||
|
||||
@@ -137,6 +137,31 @@ export interface SendPromptToAgentParams {
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface StartCreatedAgentInitialPromptParams {
|
||||
agentManager: AgentManager;
|
||||
agentId: string;
|
||||
snapshot?: ManagedAgent;
|
||||
prompt: AgentPromptInput | null;
|
||||
runOptions?: AgentRunOptions;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
const AGENT_RUN_START_TIMEOUT_MS = 15_000;
|
||||
|
||||
export async function waitForAgentRunStartWithTimeout(
|
||||
agentManager: AgentManager,
|
||||
agentId: string,
|
||||
): Promise<void> {
|
||||
const startAbort = new AbortController();
|
||||
const startTimeout = setTimeout(() => startAbort.abort("timeout"), AGENT_RUN_START_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
await agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
|
||||
} finally {
|
||||
clearTimeout(startTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full send-prompt orchestration: (optional unarchive) → load → (optional
|
||||
* mode change) → start run.
|
||||
@@ -177,6 +202,39 @@ export async function sendPromptToAgent(
|
||||
});
|
||||
}
|
||||
|
||||
export async function startCreatedAgentInitialPrompt(
|
||||
params: StartCreatedAgentInitialPromptParams,
|
||||
): Promise<ManagedAgent> {
|
||||
const currentSnapshot = params.agentManager.getAgent(params.agentId) ?? params.snapshot ?? null;
|
||||
if (!currentSnapshot) {
|
||||
throw new Error(`Agent ${params.agentId} not found`);
|
||||
}
|
||||
|
||||
if (params.prompt === null) {
|
||||
return currentSnapshot;
|
||||
}
|
||||
|
||||
const dispatchResult = startAgentRun(
|
||||
params.agentManager,
|
||||
params.agentId,
|
||||
params.prompt,
|
||||
params.logger,
|
||||
{
|
||||
runOptions: params.runOptions,
|
||||
},
|
||||
);
|
||||
|
||||
if (!dispatchResult.outOfBand) {
|
||||
await waitForAgentRunStartWithTimeout(params.agentManager, params.agentId);
|
||||
}
|
||||
|
||||
const refreshedSnapshot = params.agentManager.getAgent(params.agentId) ?? params.snapshot ?? null;
|
||||
if (!refreshedSnapshot) {
|
||||
throw new Error(`Agent ${params.agentId} not found`);
|
||||
}
|
||||
return refreshedSnapshot;
|
||||
}
|
||||
|
||||
export interface SetupFinishNotificationParams {
|
||||
agentManager: AgentManager;
|
||||
agentStorage: AgentStorage;
|
||||
|
||||
@@ -124,7 +124,11 @@ interface TestDeps {
|
||||
function buildAgentManagerSpies() {
|
||||
return {
|
||||
createAgent: vi.fn(),
|
||||
waitForAgentEvent: vi.fn(),
|
||||
waitForAgentEvent: vi.fn().mockResolvedValue({
|
||||
status: "idle",
|
||||
permission: null,
|
||||
lastMessage: null,
|
||||
}),
|
||||
setAgentMode: vi.fn().mockResolvedValue(undefined),
|
||||
setAgentModel: vi.fn().mockResolvedValue(undefined),
|
||||
setAgentThinkingOption: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -141,8 +145,10 @@ function buildAgentManagerSpies() {
|
||||
appendTimelineItem: vi.fn().mockResolvedValue(undefined),
|
||||
emitLiveTimelineItem: vi.fn().mockResolvedValue(undefined),
|
||||
hasInFlightRun: vi.fn().mockReturnValue(false),
|
||||
tryRunOutOfBand: vi.fn().mockReturnValue(false),
|
||||
subscribe: vi.fn().mockReturnValue(() => {}),
|
||||
streamAgent: vi.fn(() => (async function* noop() {})()),
|
||||
waitForAgentRunStart: vi.fn().mockResolvedValue(undefined),
|
||||
respondToPermission: vi.fn(),
|
||||
cancelAgentRun: vi.fn(),
|
||||
getPendingPermissions: vi.fn(),
|
||||
|
||||
@@ -75,7 +75,11 @@ import {
|
||||
toScheduleSummary,
|
||||
waitForAgentWithTimeout,
|
||||
} from "./mcp-shared.js";
|
||||
import { sendPromptToAgent, setupFinishNotification, startAgentRun } from "./agent-prompt.js";
|
||||
import {
|
||||
sendPromptToAgent,
|
||||
setupFinishNotification,
|
||||
startCreatedAgentInitialPrompt,
|
||||
} from "./agent-prompt.js";
|
||||
import type { GitHubService } from "../../services/github-service.js";
|
||||
import type { WorkspaceGitService } from "../workspace-git-service.js";
|
||||
import type { CreatePaseoWorktreeInput } from "../paseo-worktree-service.js";
|
||||
@@ -1105,8 +1109,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
logger: childLogger,
|
||||
});
|
||||
|
||||
let liveSnapshot = snapshot;
|
||||
try {
|
||||
startAgentRun(agentManager, snapshot.id, trimmedPrompt, childLogger);
|
||||
liveSnapshot = await startCreatedAgentInitialPrompt({
|
||||
agentManager,
|
||||
agentId: snapshot.id,
|
||||
snapshot,
|
||||
prompt: trimmedPrompt,
|
||||
logger: childLogger,
|
||||
});
|
||||
if (notifyOnFinish && callerAgentId) {
|
||||
setupFinishNotification({
|
||||
agentManager,
|
||||
@@ -1124,12 +1135,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
});
|
||||
|
||||
const responseData = {
|
||||
agentId: snapshot.id,
|
||||
agentId: liveSnapshot.id,
|
||||
type: provider,
|
||||
status: result.status,
|
||||
cwd: snapshot.cwd,
|
||||
currentModeId: snapshot.currentModeId,
|
||||
availableModes: snapshot.availableModes,
|
||||
cwd: liveSnapshot.cwd,
|
||||
currentModeId: liveSnapshot.currentModeId,
|
||||
availableModes: liveSnapshot.availableModes,
|
||||
lastMessage: result.lastMessage,
|
||||
permission: sanitizePermissionRequest(result.permission),
|
||||
};
|
||||
@@ -1143,18 +1154,20 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
}
|
||||
} catch (error) {
|
||||
childLogger.error({ err: error, agentId: snapshot.id }, "Failed to run initial prompt");
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Return immediately if background=true
|
||||
const currentSnapshot = agentManager.getAgent(snapshot.id) ?? liveSnapshot;
|
||||
const response = {
|
||||
content: [],
|
||||
structuredContent: ensureValidJson({
|
||||
agentId: snapshot.id,
|
||||
agentId: currentSnapshot.id,
|
||||
type: provider,
|
||||
status: snapshot.lifecycle,
|
||||
cwd: snapshot.cwd,
|
||||
currentModeId: snapshot.currentModeId,
|
||||
availableModes: snapshot.availableModes,
|
||||
status: currentSnapshot.lifecycle,
|
||||
cwd: currentSnapshot.cwd,
|
||||
currentModeId: currentSnapshot.currentModeId,
|
||||
availableModes: currentSnapshot.availableModes,
|
||||
lastMessage: null,
|
||||
permission: null,
|
||||
}),
|
||||
|
||||
@@ -121,6 +121,192 @@ test("DaemonClient surfaces password auth failures from WebSocket close reasons"
|
||||
}
|
||||
});
|
||||
|
||||
test("createAgent without an initial prompt returns an idle snapshot", async () => {
|
||||
const daemon = await createTestPaseoDaemon();
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${daemon.port}/ws`,
|
||||
appVersion: "0.1.82",
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.fetchAgents({ subscribe: { subscriptionId: "create-no-prompt" } });
|
||||
|
||||
const agent = await client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: tmpCwd(),
|
||||
title: "No prompt agent",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.4-mini",
|
||||
});
|
||||
|
||||
expect(agent.status).toBe("idle");
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("createAgent with background initialPrompt returns a running snapshot before turn completion", async () => {
|
||||
const daemon = await createTestPaseoDaemon();
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${daemon.port}/ws`,
|
||||
appVersion: "0.1.82",
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.fetchAgents({ subscribe: { subscriptionId: "create-background-prompt" } });
|
||||
|
||||
const agent = await client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: tmpCwd(),
|
||||
title: "Background prompt agent",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.4-mini",
|
||||
initialPrompt: "Run exactly: sleep 30",
|
||||
});
|
||||
|
||||
expect(agent.status).toBe("running");
|
||||
|
||||
const fetchedWhileRunning = await client.fetchAgent(agent.id);
|
||||
expect(fetchedWhileRunning?.agent.status).toBe("running");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
|
||||
const fetchedAfterCompletion = await client.fetchAgent(agent.id);
|
||||
expect(fetchedAfterCompletion?.agent.status).toBe("idle");
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("createAgent fails when the initial turn cannot start", async () => {
|
||||
class StartTurnFailureSession implements AgentSession {
|
||||
readonly provider = "codex" as const;
|
||||
readonly id = "start-turn-failure-session";
|
||||
readonly capabilities = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
supportsRewindConversation: false,
|
||||
supportsRewindFiles: false,
|
||||
supportsRewindBoth: false,
|
||||
} as const;
|
||||
|
||||
async run(): Promise<AgentRunResult> {
|
||||
return {
|
||||
sessionId: this.id,
|
||||
finalText: "",
|
||||
timeline: [],
|
||||
};
|
||||
}
|
||||
|
||||
async startTurn(): Promise<{ turnId: string }> {
|
||||
throw new Error("Initial turn failed to start");
|
||||
}
|
||||
|
||||
subscribe(): () => void {
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {
|
||||
yield* [];
|
||||
}
|
||||
|
||||
async getRuntimeInfo() {
|
||||
return {
|
||||
provider: "codex" as const,
|
||||
sessionId: this.id,
|
||||
model: "gpt-5.4-mini",
|
||||
modeId: "full-access",
|
||||
};
|
||||
}
|
||||
|
||||
async getAvailableModes(): Promise<Array<{ id: string; label: string; description: string }>> {
|
||||
return [{ id: "full-access", label: "Full access", description: "No prompts" }];
|
||||
}
|
||||
|
||||
async getCurrentMode(): Promise<string | null> {
|
||||
return "full-access";
|
||||
}
|
||||
|
||||
async setMode(): Promise<void> {}
|
||||
|
||||
getPendingPermissions() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async respondToPermission(): Promise<void> {}
|
||||
|
||||
describePersistence(): AgentPersistenceHandle | null {
|
||||
return { provider: "codex", sessionId: this.id };
|
||||
}
|
||||
|
||||
async interrupt(): Promise<void> {}
|
||||
|
||||
async close(): Promise<void> {}
|
||||
}
|
||||
|
||||
class StartTurnFailureClient implements AgentClient {
|
||||
readonly provider = "codex" as const;
|
||||
readonly capabilities = {
|
||||
supportsStreaming: false,
|
||||
supportsSessionPersistence: true,
|
||||
supportsDynamicModes: false,
|
||||
supportsMcpServers: false,
|
||||
supportsReasoningStream: false,
|
||||
supportsToolInvocations: false,
|
||||
supportsRewindConversation: false,
|
||||
supportsRewindFiles: false,
|
||||
supportsRewindBoth: false,
|
||||
} as const;
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async createSession(_config: AgentSessionConfig): Promise<AgentSession> {
|
||||
return new StartTurnFailureSession();
|
||||
}
|
||||
|
||||
async resumeSession(): Promise<AgentSession> {
|
||||
return new StartTurnFailureSession();
|
||||
}
|
||||
}
|
||||
|
||||
const daemon = await createTestPaseoDaemon({
|
||||
agentClients: { codex: new StartTurnFailureClient() },
|
||||
});
|
||||
const client = new DaemonClient({
|
||||
url: `ws://127.0.0.1:${daemon.port}/ws`,
|
||||
appVersion: "0.1.82",
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.fetchAgents({ subscribe: { subscriptionId: "create-start-failure" } });
|
||||
|
||||
await expect(
|
||||
client.createAgent({
|
||||
provider: "codex",
|
||||
cwd: tmpCwd(),
|
||||
title: "Start failure agent",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.4-mini",
|
||||
initialPrompt: "Run exactly: sleep 30",
|
||||
}),
|
||||
).rejects.toThrow("Initial turn failed to start");
|
||||
} finally {
|
||||
await client.close();
|
||||
await daemon.close();
|
||||
}
|
||||
});
|
||||
|
||||
function waitForSignal<T>(
|
||||
timeoutMs: number,
|
||||
setup: (resolve: (value: T) => void, reject: (error: Error) => void) => () => void,
|
||||
|
||||
@@ -68,6 +68,8 @@ import { ensureAgentLoaded } from "./agent/agent-loading.js";
|
||||
import {
|
||||
formatSystemNotificationPrompt,
|
||||
sendPromptToAgent,
|
||||
startCreatedAgentInitialPrompt,
|
||||
waitForAgentRunStartWithTimeout,
|
||||
unarchiveAgentState,
|
||||
} from "./agent/agent-prompt.js";
|
||||
import { experimental_createMCPClient } from "ai";
|
||||
@@ -3188,7 +3190,7 @@ export class Session {
|
||||
createdWorktree,
|
||||
});
|
||||
|
||||
await this.sendInitialCreateAgentPrompt({
|
||||
const liveSnapshot = await this.sendInitialCreateAgentPrompt({
|
||||
snapshot,
|
||||
trimmedPrompt,
|
||||
images,
|
||||
@@ -3199,12 +3201,12 @@ export class Session {
|
||||
});
|
||||
|
||||
if (requestId) {
|
||||
const agentPayload = await this.buildAgentPayload(snapshot);
|
||||
const agentPayload = await this.buildAgentPayload(liveSnapshot);
|
||||
this.emit({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "agent_created",
|
||||
agentId: snapshot.id,
|
||||
agentId: liveSnapshot.id,
|
||||
requestId,
|
||||
agent: agentPayload,
|
||||
},
|
||||
@@ -3250,20 +3252,20 @@ export class Session {
|
||||
}
|
||||
|
||||
private async sendInitialCreateAgentPrompt(params: {
|
||||
snapshot: { id: string; cwd: string };
|
||||
snapshot: ManagedAgent;
|
||||
trimmedPrompt: string | undefined;
|
||||
images: Array<{ data: string; mimeType: string }> | undefined;
|
||||
attachments: AgentAttachment[] | undefined;
|
||||
clientMessageId: string | undefined;
|
||||
outputSchema: Record<string, unknown> | undefined;
|
||||
explicitTitle: string | null;
|
||||
}): Promise<void> {
|
||||
}): Promise<ManagedAgent> {
|
||||
const { snapshot, trimmedPrompt, images, attachments, clientMessageId, outputSchema } = params;
|
||||
const hasPrompt = Boolean(trimmedPrompt);
|
||||
const hasImages = (images?.length ?? 0) > 0;
|
||||
const hasAttachments = (attachments?.length ?? 0) > 0;
|
||||
if (!hasPrompt && !hasImages && !hasAttachments) {
|
||||
return;
|
||||
return snapshot;
|
||||
}
|
||||
scheduleAgentMetadataGeneration({
|
||||
agentManager: this.agentManager,
|
||||
@@ -3275,18 +3277,18 @@ export class Session {
|
||||
paseoHome: this.paseoHome,
|
||||
logger: this.sessionLogger,
|
||||
});
|
||||
const prompt = this.buildAgentPrompt(trimmedPrompt || "", images, attachments);
|
||||
|
||||
const started = await this.handleSendAgentMessage(
|
||||
snapshot.id,
|
||||
trimmedPrompt || "",
|
||||
resolveClientMessageId(clientMessageId),
|
||||
images,
|
||||
attachments,
|
||||
outputSchema ? { outputSchema } : undefined,
|
||||
);
|
||||
if (!started.ok) {
|
||||
throw new Error(started.error);
|
||||
}
|
||||
return await startCreatedAgentInitialPrompt({
|
||||
agentManager: this.agentManager,
|
||||
agentId: snapshot.id,
|
||||
snapshot,
|
||||
prompt,
|
||||
runOptions: outputSchema ? { outputSchema } : undefined,
|
||||
logger: this.sessionLogger.child({
|
||||
clientMessageId: resolveClientMessageId(clientMessageId),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private async handleResumeAgentRequest(
|
||||
@@ -7814,11 +7816,8 @@ export class Session {
|
||||
return;
|
||||
}
|
||||
|
||||
const startAbort = new AbortController();
|
||||
const startTimeoutMs = 15_000;
|
||||
const startTimeout = setTimeout(() => startAbort.abort("timeout"), startTimeoutMs);
|
||||
try {
|
||||
await this.agentManager.waitForAgentRunStart(agentId, { signal: startAbort.signal });
|
||||
await waitForAgentRunStartWithTimeout(this.agentManager, agentId);
|
||||
} catch (error) {
|
||||
this.emit({
|
||||
type: "send_agent_message_response",
|
||||
@@ -7830,8 +7829,6 @@ export class Session {
|
||||
},
|
||||
});
|
||||
return;
|
||||
} finally {
|
||||
clearTimeout(startTimeout);
|
||||
}
|
||||
|
||||
this.emit({
|
||||
|
||||
Reference in New Issue
Block a user