mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ab41fbd9a | ||
|
|
ed6caa11c0 | ||
|
|
74c8942a28 | ||
|
|
3eb1ba7d73 | ||
|
|
1d2c8b1648 | ||
|
|
88914ccba6 | ||
|
|
f79101a1f0 |
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.83 - 2026-05-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- Creating an agent via MCP now waits for it to actually start, so failures surface as a clear create error
|
||||
- Scheduling an agent via MCP no longer rejects blank cadence placeholders
|
||||
- Draft messages show the agent mode chip again on models without thinking options
|
||||
|
||||
## 0.1.82 - 2026-05-26
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-s1HwJgifRYWfnIVit2JVrJmFSyu8lL8TpX67I/33dQQ=
|
||||
sha256-WAxd4FSYNGYPT7v0QYTkYF5AacMrpYU+zjMyMTcWufo=
|
||||
|
||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paseo",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"workspaces": [
|
||||
@@ -36323,7 +36323,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
@@ -36544,10 +36544,10 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/server": "0.1.82",
|
||||
"@getpaseo/server": "0.1.83",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -36902,7 +36902,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@getpaseo/cli": "*",
|
||||
@@ -37263,7 +37263,7 @@
|
||||
},
|
||||
"packages/expo-two-way-audio": {
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.25",
|
||||
@@ -37299,7 +37299,7 @@
|
||||
},
|
||||
"packages/highlight": {
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"dependencies": {
|
||||
"@lezer/common": "^1.5.0",
|
||||
"@lezer/cpp": "^1.1.5",
|
||||
@@ -37637,7 +37637,7 @@
|
||||
},
|
||||
"packages/relay": {
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1",
|
||||
"tweetnacl": "^1.0.3",
|
||||
@@ -37964,12 +37964,12 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
||||
"@getpaseo/highlight": "0.1.82",
|
||||
"@getpaseo/relay": "0.1.82",
|
||||
"@getpaseo/highlight": "0.1.83",
|
||||
"@getpaseo/relay": "0.1.83",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
@@ -38843,7 +38843,7 @@
|
||||
},
|
||||
"packages/website": {
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.29.1",
|
||||
"@cloudflare/workers-types": "^4.20260317.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paseo",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"private": true,
|
||||
"description": "Paseo: voice-controlled development environment with OpenAI Realtime API",
|
||||
"keywords": [
|
||||
|
||||
@@ -126,10 +126,7 @@ async function expectReplacementDraftMatchesPreviousSetup(page: Page): Promise<v
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Select model (Ten second stream)" }),
|
||||
).toBeVisible();
|
||||
// TODO(boudra): the replacement draft's mode picker stopped rendering after
|
||||
// the composer refactor — the model carries over but modes aren't surfaced
|
||||
// in the draft's provider snapshot. Restore this assertion once the draft
|
||||
// mode picker is fixed.
|
||||
await expect(page.getByRole("button", { name: "Select agent mode (Load test)" })).toBeVisible();
|
||||
}
|
||||
|
||||
async function createAgentFromReplacementDraft(page: Page): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/app",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"private": true,
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -195,17 +195,20 @@ function resolveHasAnyControl({
|
||||
canSelectModel,
|
||||
thinkingOptions,
|
||||
features,
|
||||
hasDesktopExtras,
|
||||
}: {
|
||||
providerOptions: AgentControlOption[] | undefined;
|
||||
canSelectModel: boolean;
|
||||
thinkingOptions: AgentControlOption[] | undefined;
|
||||
features: AgentFeature[] | undefined;
|
||||
hasDesktopExtras: boolean;
|
||||
}) {
|
||||
return (
|
||||
Boolean(providerOptions?.length) ||
|
||||
canSelectModel ||
|
||||
Boolean(thinkingOptions?.length) ||
|
||||
Boolean(features?.length)
|
||||
Boolean(features?.length) ||
|
||||
hasDesktopExtras
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,6 +443,7 @@ function ControlledAgentControls({
|
||||
canSelectModel,
|
||||
thinkingOptions,
|
||||
features,
|
||||
hasDesktopExtras: desktopExtras !== null && desktopExtras !== undefined,
|
||||
});
|
||||
|
||||
const modelDisabled = disabled;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/cli",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"description": "Paseo CLI - control your AI coding agents from the command line",
|
||||
"bin": {
|
||||
"paseo": "bin/paseo"
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^1.0.0",
|
||||
"@getpaseo/server": "0.1.82",
|
||||
"@getpaseo/server": "0.1.83",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^12.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/desktop",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"private": true,
|
||||
"description": "Paseo desktop app (Electron wrapper)",
|
||||
"homepage": "https://paseo.sh",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/expo-two-way-audio",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"description": "Native module for two way audio streaming",
|
||||
"keywords": [
|
||||
"ExpoTwoWayAudio",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/highlight",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/relay",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"description": "Paseo relay for bridging daemon and client connections",
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/server",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"description": "Paseo backend server",
|
||||
"files": [
|
||||
"dist/server",
|
||||
@@ -63,8 +63,8 @@
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.17.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
||||
"@getpaseo/highlight": "0.1.82",
|
||||
"@getpaseo/relay": "0.1.82",
|
||||
"@getpaseo/highlight": "0.1.83",
|
||||
"@getpaseo/relay": "0.1.83",
|
||||
"@isaacs/ttlcache": "^2.1.4",
|
||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||
"@opencode-ai/sdk": "1.14.46",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -94,6 +94,15 @@ function registeredTool(
|
||||
return { ...tool, handler };
|
||||
}
|
||||
|
||||
async function invokeToolWithParsedInput(
|
||||
tool: RegisteredMcpToolWithHandler,
|
||||
input: Record<string, unknown>,
|
||||
) {
|
||||
const parsed = await tool.inputSchema.safeParseAsync(input);
|
||||
expect(parsed.success).toBe(true);
|
||||
return tool.handler(parsed.data);
|
||||
}
|
||||
|
||||
function agentsOf(response: {
|
||||
structuredContent: LooseStructuredContent;
|
||||
}): Array<Record<string, unknown>> {
|
||||
@@ -115,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),
|
||||
@@ -132,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(),
|
||||
@@ -1718,6 +1733,131 @@ describe("create_schedule MCP tool", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts a blank cron field when every is provided", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
|
||||
createStoredSchedule(scheduleInput),
|
||||
);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
scheduleService: { create } as unknown as ScheduleService,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_schedule");
|
||||
|
||||
await invokeToolWithParsedInput(tool, {
|
||||
prompt: "say hello",
|
||||
every: "10m",
|
||||
cron: "",
|
||||
provider: "codex",
|
||||
});
|
||||
|
||||
expect(create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cadence: { type: "every", everyMs: 600000 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "whitespace cron field",
|
||||
input: { prompt: "say hello", every: "10m", cron: " ", provider: "codex" },
|
||||
cadence: { type: "every", everyMs: 600000 },
|
||||
},
|
||||
{
|
||||
label: "blank every field for cron cadence",
|
||||
input: {
|
||||
prompt: "say hello",
|
||||
every: "",
|
||||
cron: "*/10 * * * *",
|
||||
provider: "codex",
|
||||
},
|
||||
cadence: { type: "cron", expression: "*/10 * * * *" },
|
||||
},
|
||||
{
|
||||
label: "whitespace every field for cron cadence",
|
||||
input: {
|
||||
prompt: "say hello",
|
||||
every: " ",
|
||||
cron: "*/10 * * * *",
|
||||
provider: "codex",
|
||||
},
|
||||
cadence: { type: "cron", expression: "*/10 * * * *" },
|
||||
},
|
||||
])("normalizes create_schedule blank cadence input for $label", async ({ input, cadence }) => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
|
||||
createStoredSchedule(scheduleInput),
|
||||
);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
scheduleService: { create } as unknown as ScheduleService,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_schedule");
|
||||
|
||||
await invokeToolWithParsedInput(tool, input);
|
||||
|
||||
expect(create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cadence,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("still rejects both real every and cron inputs", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const create = vi.fn();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
scheduleService: { create } as unknown as ScheduleService,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_schedule");
|
||||
|
||||
await expect(
|
||||
invokeToolWithParsedInput(tool, {
|
||||
prompt: "say hello",
|
||||
every: "10m",
|
||||
cron: "*/10 * * * *",
|
||||
provider: "codex",
|
||||
}),
|
||||
).rejects.toThrow("Specify exactly one of every or cron");
|
||||
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "missing both cadence fields",
|
||||
input: { prompt: "say hello", provider: "codex" },
|
||||
},
|
||||
{
|
||||
label: "blank cadence fields",
|
||||
input: { prompt: "say hello", every: " ", cron: "", provider: "codex" },
|
||||
},
|
||||
])("still rejects create_schedule when $label", async ({ input }) => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const create = vi.fn();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
scheduleService: { create } as unknown as ScheduleService,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_schedule");
|
||||
|
||||
await expect(invokeToolWithParsedInput(tool, input)).rejects.toThrow(
|
||||
"Specify exactly one of every or cron",
|
||||
);
|
||||
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("update_schedule MCP tool", () => {
|
||||
@@ -1795,6 +1935,66 @@ describe("update_schedule MCP tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a blank cron field when updating every cadence", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const stored = makeStoredSchedule();
|
||||
const update = vi.fn(async (_input: UpdateScheduleInput) => stored);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
scheduleService: { update } as unknown as ScheduleService,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "update_schedule");
|
||||
|
||||
await invokeToolWithParsedInput(tool, {
|
||||
id: "schedule-1",
|
||||
every: "10m",
|
||||
cron: "",
|
||||
});
|
||||
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
id: "schedule-1",
|
||||
cadence: { type: "every", everyMs: 600000 },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
label: "whitespace cron field",
|
||||
input: { id: "schedule-1", every: "10m", cron: " " },
|
||||
cadence: { type: "every", everyMs: 600000 },
|
||||
},
|
||||
{
|
||||
label: "blank every field for cron cadence",
|
||||
input: { id: "schedule-1", every: "", cron: "*/10 * * * *" },
|
||||
cadence: { type: "cron", expression: "*/10 * * * *" },
|
||||
},
|
||||
{
|
||||
label: "whitespace every field for cron cadence",
|
||||
input: { id: "schedule-1", every: " ", cron: "*/10 * * * *" },
|
||||
cadence: { type: "cron", expression: "*/10 * * * *" },
|
||||
},
|
||||
])("normalizes update_schedule blank cadence input for $label", async ({ input, cadence }) => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const stored = makeStoredSchedule();
|
||||
const update = vi.fn(async (_input: UpdateScheduleInput) => stored);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
scheduleService: { update } as unknown as ScheduleService,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "update_schedule");
|
||||
|
||||
await invokeToolWithParsedInput(tool, input);
|
||||
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
id: "schedule-1",
|
||||
cadence,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects both every and cron", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const update = 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";
|
||||
@@ -370,15 +374,31 @@ interface ScheduleUpdateToolInput {
|
||||
clearExpires?: boolean;
|
||||
}
|
||||
|
||||
function normalizeScheduleCadenceArg(value: string | undefined): string | undefined {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function resolveScheduleUpdateCadence(input: ScheduleUpdateToolInput): ScheduleCadence | undefined {
|
||||
if (input.every !== undefined && input.cron !== undefined) {
|
||||
const every = normalizeScheduleCadenceArg(input.every);
|
||||
const cron = normalizeScheduleCadenceArg(input.cron);
|
||||
|
||||
if (every !== undefined && cron !== undefined) {
|
||||
throw new Error("Specify at most one of every or cron");
|
||||
}
|
||||
if (input.every !== undefined) {
|
||||
return { type: "every", everyMs: parseDurationString(input.every) };
|
||||
if (every !== undefined) {
|
||||
return { type: "every", everyMs: parseDurationString(every) };
|
||||
}
|
||||
if (input.cron !== undefined) {
|
||||
return { type: "cron", expression: input.cron.trim() };
|
||||
if (cron !== undefined) {
|
||||
return { type: "cron", expression: cron };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1089,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,
|
||||
@@ -1108,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),
|
||||
};
|
||||
@@ -1127,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,
|
||||
}),
|
||||
@@ -1779,7 +1808,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
throw new Error("Schedule service is not configured");
|
||||
}
|
||||
|
||||
const cadenceCount = Number(every !== undefined) + Number(cron !== undefined);
|
||||
const normalizedEvery = normalizeScheduleCadenceArg(every);
|
||||
const normalizedCron = normalizeScheduleCadenceArg(cron);
|
||||
const cadenceCount =
|
||||
Number(normalizedEvery !== undefined) + Number(normalizedCron !== undefined);
|
||||
if (cadenceCount !== 1) {
|
||||
throw new Error("Specify exactly one of every or cron");
|
||||
}
|
||||
@@ -1817,9 +1849,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
|
||||
const schedule = await scheduleService.create({
|
||||
prompt: prompt.trim(),
|
||||
cadence: every
|
||||
? { type: "every" as const, everyMs: parseDurationString(every) }
|
||||
: { type: "cron" as const, expression: cron!.trim() },
|
||||
cadence:
|
||||
normalizedEvery !== undefined
|
||||
? { type: "every" as const, everyMs: parseDurationString(normalizedEvery) }
|
||||
: { type: "cron" as const, expression: normalizedCron! },
|
||||
target: scheduleTarget,
|
||||
...(name?.trim() ? { name: name.trim() } : {}),
|
||||
...(maxRuns === undefined ? {} : { maxRuns }),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@getpaseo/website",
|
||||
"version": "0.1.82",
|
||||
"version": "0.1.83",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user