perf(agent-tools): shrink MCP tool catalog context cost (#1939)
Drop advertised MCP output schemas (no supported harness forwards them to the model) and remove the redundant wait_for_agent tool. Core agent tool catalog drops ~20.5k -> ~5.6k estimated tokens; ~24.9k -> ~9k with browser tools. Runtime structuredContent is preserved via server-side text serialization. Also corrects the agent-wait timeout guidance for blocking callers.
This commit is contained in:
@@ -173,6 +173,19 @@ The supervisor rotates `daemon.log`. Persisted `log.file.rotate` settings in
|
|||||||
`PASEO_LOG_ROTATE_SIZE` and `PASEO_LOG_ROTATE_COUNT` env vars override the
|
`PASEO_LOG_ROTATE_SIZE` and `PASEO_LOG_ROTATE_COUNT` env vars override the
|
||||||
defaults. The default rotation is `10m` x `3` files everywhere.
|
defaults. The default rotation is `10m` x `3` files everywhere.
|
||||||
|
|
||||||
|
### Agent Tool Catalog Measurement
|
||||||
|
|
||||||
|
Measure the MCP `tools/list` payload that Paseo injects into agents with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run measure:agent-tools --workspace=@getpaseo/server
|
||||||
|
```
|
||||||
|
|
||||||
|
The command reports compact JSON bytes, estimated tokens, field totals, largest
|
||||||
|
tools, and the browser-tools delta. It defaults to the agent-scoped catalog; use
|
||||||
|
`-- --scope=top-level` for the unaffiliated `/mcp/agents` shape and `-- --json`
|
||||||
|
for machine-readable output.
|
||||||
|
|
||||||
## paseo.json service scripts
|
## paseo.json service scripts
|
||||||
|
|
||||||
`worktree.setup` and `worktree.teardown` accept either a multiline shell script or an array
|
`worktree.setup` and `worktree.teardown` accept either a multiline shell script or an array
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"start": "node dist/scripts/supervisor-entrypoint.js",
|
"start": "node dist/scripts/supervisor-entrypoint.js",
|
||||||
"typecheck": "tsgo -p tsconfig.server.typecheck.json --noEmit",
|
"typecheck": "tsgo -p tsconfig.server.typecheck.json --noEmit",
|
||||||
"generate:config-schema": "tsx scripts/generate-config-schema.ts",
|
"generate:config-schema": "tsx scripts/generate-config-schema.ts",
|
||||||
|
"measure:agent-tools": "tsx scripts/measure-agent-tools-context.ts",
|
||||||
"speech:models": "tsx scripts/list-speech-models.ts",
|
"speech:models": "tsx scripts/list-speech-models.ts",
|
||||||
"speech:download": "tsx scripts/download-speech-models.ts",
|
"speech:download": "tsx scripts/download-speech-models.ts",
|
||||||
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
||||||
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
import { createAgentMcpServer } from "../src/server/agent/mcp-server.js";
|
||||||
|
import type { AgentManager, ManagedAgent } from "../src/server/agent/agent-manager.js";
|
||||||
|
import type { AgentStorage, StoredAgentRecord } from "../src/server/agent/agent-storage.js";
|
||||||
|
import type { AgentProvider, ProviderSnapshotEntry } from "../src/server/agent/agent-sdk-types.js";
|
||||||
|
import type { ProviderSnapshotManager } from "../src/server/agent/provider-snapshot-manager.js";
|
||||||
|
import type { BrowserToolsBroker } from "../src/server/browser-tools/broker.js";
|
||||||
|
import type { BrowserToolsResponsePayload } from "../src/server/browser-tools/errors.js";
|
||||||
|
|
||||||
|
type CatalogScope = "agent" | "top-level";
|
||||||
|
|
||||||
|
interface CliOptions {
|
||||||
|
format: "markdown" | "json";
|
||||||
|
scope: CatalogScope;
|
||||||
|
top: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldSize {
|
||||||
|
field: string;
|
||||||
|
bytes: number;
|
||||||
|
estimatedTokens: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolSize {
|
||||||
|
name: string;
|
||||||
|
bytes: number;
|
||||||
|
estimatedTokens: number;
|
||||||
|
fields: FieldSize[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CatalogMeasurement {
|
||||||
|
label: string;
|
||||||
|
browserToolsEnabled: boolean;
|
||||||
|
toolCount: number;
|
||||||
|
bytes: number;
|
||||||
|
estimatedTokens: number;
|
||||||
|
withoutOutputSchemas: {
|
||||||
|
bytes: number;
|
||||||
|
estimatedTokens: number;
|
||||||
|
savedBytes: number;
|
||||||
|
savedEstimatedTokens: number;
|
||||||
|
};
|
||||||
|
fields: FieldSize[];
|
||||||
|
tools: ToolSize[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TOP_COUNT = 15;
|
||||||
|
const MEASUREMENT_AGENT_ID = "agent-tools-measurement";
|
||||||
|
const MEASUREMENT_CWD = "/tmp/paseo-agent-tools-measurement";
|
||||||
|
const MEASUREMENT_WORKSPACE_ID = "workspace_agent_tools_measurement";
|
||||||
|
const FIELD_NAMES = [
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"inputSchema",
|
||||||
|
"outputSchema",
|
||||||
|
"annotations",
|
||||||
|
"execution",
|
||||||
|
"_meta",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function parseCliOptions(args: string[]): CliOptions {
|
||||||
|
let format: CliOptions["format"] = "markdown";
|
||||||
|
let scope: CatalogScope = "agent";
|
||||||
|
let top = DEFAULT_TOP_COUNT;
|
||||||
|
|
||||||
|
for (const arg of args) {
|
||||||
|
if (arg === "--json") {
|
||||||
|
format = "json";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--markdown") {
|
||||||
|
format = "markdown";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("--scope=")) {
|
||||||
|
const value = arg.slice("--scope=".length);
|
||||||
|
if (value !== "agent" && value !== "top-level") {
|
||||||
|
throw new Error("--scope must be agent or top-level");
|
||||||
|
}
|
||||||
|
scope = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg.startsWith("--top=")) {
|
||||||
|
const value = Number.parseInt(arg.slice("--top=".length), 10);
|
||||||
|
if (!Number.isInteger(value) || value < 1) {
|
||||||
|
throw new Error("--top must be a positive integer");
|
||||||
|
}
|
||||||
|
top = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--help") {
|
||||||
|
printHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown argument: ${arg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { format, scope, top };
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp(): void {
|
||||||
|
process.stdout.write(`Usage: npm run measure:agent-tools -- [options]
|
||||||
|
|
||||||
|
Measures the MCP tools/list payload for Paseo agent tools.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--scope=agent|top-level Catalog shape to measure. Defaults to agent.
|
||||||
|
--top=N Number of largest tools to show. Defaults to ${DEFAULT_TOP_COUNT}.
|
||||||
|
--json Emit machine-readable JSON.
|
||||||
|
--markdown Emit Markdown. This is the default.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateTokens(text: string): number {
|
||||||
|
return Math.ceil(Buffer.byteLength(text, "utf8") / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureJson(value: unknown): { bytes: number; estimatedTokens: number } {
|
||||||
|
const text = JSON.stringify(value);
|
||||||
|
return {
|
||||||
|
bytes: Buffer.byteLength(text, "utf8"),
|
||||||
|
estimatedTokens: estimateTokens(text),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureField(field: string, value: unknown): FieldSize {
|
||||||
|
return {
|
||||||
|
field,
|
||||||
|
...measureJson({ [field]: value }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripOutputSchemas(tools: Tool[]): Tool[] {
|
||||||
|
return tools.map((tool) => {
|
||||||
|
const { outputSchema: _outputSchema, ...rest } = tool;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureTool(tool: Tool): ToolSize {
|
||||||
|
const fields = FIELD_NAMES.flatMap((field) =>
|
||||||
|
Object.hasOwn(tool, field) ? [measureField(field, tool[field])] : [],
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: tool.name,
|
||||||
|
...measureJson(tool),
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sumFieldSizes(tools: ToolSize[]): FieldSize[] {
|
||||||
|
const totals = new Map<string, { bytes: number; estimatedTokens: number }>();
|
||||||
|
for (const tool of tools) {
|
||||||
|
for (const field of tool.fields) {
|
||||||
|
const current = totals.get(field.field) ?? { bytes: 0, estimatedTokens: 0 };
|
||||||
|
totals.set(field.field, {
|
||||||
|
bytes: current.bytes + field.bytes,
|
||||||
|
estimatedTokens: current.estimatedTokens + field.estimatedTokens,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...totals.entries()]
|
||||||
|
.map(([field, size]) => ({
|
||||||
|
field,
|
||||||
|
bytes: size.bytes,
|
||||||
|
estimatedTokens: size.estimatedTokens,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.bytes - a.bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureCatalog(params: {
|
||||||
|
label: string;
|
||||||
|
browserToolsEnabled: boolean;
|
||||||
|
scope: CatalogScope;
|
||||||
|
}): Promise<CatalogMeasurement> {
|
||||||
|
const server = await createAgentMcpServer({
|
||||||
|
agentManager: new MeasurementAgentManager() as unknown as AgentManager,
|
||||||
|
agentStorage: new MeasurementAgentStorage() as unknown as AgentStorage,
|
||||||
|
providerSnapshotManager:
|
||||||
|
new MeasurementProviderSnapshotManager() as unknown as ProviderSnapshotManager,
|
||||||
|
browserToolsEnabled: params.browserToolsEnabled,
|
||||||
|
browserToolsBroker: new MeasurementBrowserToolsBroker() as unknown as BrowserToolsBroker,
|
||||||
|
callerAgentId: params.scope === "agent" ? MEASUREMENT_AGENT_ID : undefined,
|
||||||
|
logger: pino({ level: "silent" }),
|
||||||
|
});
|
||||||
|
const client = new Client({ name: "paseo-agent-tools-measurement", version: "0.0.0" });
|
||||||
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||||
|
|
||||||
|
await server.connect(serverTransport);
|
||||||
|
await client.connect(clientTransport);
|
||||||
|
try {
|
||||||
|
const result = await client.listTools();
|
||||||
|
const size = measureJson(result.tools);
|
||||||
|
const strippedSize = measureJson(stripOutputSchemas(result.tools));
|
||||||
|
const tools = result.tools.map(measureTool).sort((a, b) => b.bytes - a.bytes);
|
||||||
|
return {
|
||||||
|
label: params.label,
|
||||||
|
browserToolsEnabled: params.browserToolsEnabled,
|
||||||
|
toolCount: result.tools.length,
|
||||||
|
...size,
|
||||||
|
withoutOutputSchemas: {
|
||||||
|
...strippedSize,
|
||||||
|
savedBytes: size.bytes - strippedSize.bytes,
|
||||||
|
savedEstimatedTokens: size.estimatedTokens - strippedSize.estimatedTokens,
|
||||||
|
},
|
||||||
|
fields: sumFieldSizes(tools),
|
||||||
|
tools,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await client.close();
|
||||||
|
await server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInteger(value: number): string {
|
||||||
|
return value.toLocaleString("en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMarkdown(measurements: CatalogMeasurement[], top: number): string {
|
||||||
|
const [withoutBrowser, withBrowser] = measurements;
|
||||||
|
const delta =
|
||||||
|
withoutBrowser && withBrowser
|
||||||
|
? {
|
||||||
|
tools: withBrowser.toolCount - withoutBrowser.toolCount,
|
||||||
|
bytes: withBrowser.bytes - withoutBrowser.bytes,
|
||||||
|
estimatedTokens: withBrowser.estimatedTokens - withoutBrowser.estimatedTokens,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push("# Paseo Agent Tool Catalog Context");
|
||||||
|
lines.push("");
|
||||||
|
lines.push("Token counts are estimates from compact JSON bytes / 4.");
|
||||||
|
lines.push("");
|
||||||
|
lines.push("| Catalog | Tools | Bytes | Estimated tokens | Output-schema savings |");
|
||||||
|
lines.push("| --- | ---: | ---: | ---: | ---: |");
|
||||||
|
for (const measurement of measurements) {
|
||||||
|
lines.push(
|
||||||
|
`| ${measurement.label} | ${formatInteger(measurement.toolCount)} | ${formatInteger(
|
||||||
|
measurement.bytes,
|
||||||
|
)} | ${formatInteger(measurement.estimatedTokens)} | ${formatInteger(
|
||||||
|
measurement.withoutOutputSchemas.savedEstimatedTokens,
|
||||||
|
)} est. tokens |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (delta) {
|
||||||
|
lines.push(
|
||||||
|
`| Browser-tool delta | +${formatInteger(delta.tools)} | +${formatInteger(
|
||||||
|
delta.bytes,
|
||||||
|
)} | +${formatInteger(delta.estimatedTokens)} | |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const measurement of measurements) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`## ${measurement.label}`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("### Field totals");
|
||||||
|
lines.push("");
|
||||||
|
lines.push("| Field | Bytes | Estimated tokens |");
|
||||||
|
lines.push("| --- | ---: | ---: |");
|
||||||
|
for (const field of measurement.fields) {
|
||||||
|
lines.push(
|
||||||
|
`| ${field.field} | ${formatInteger(field.bytes)} | ${formatInteger(
|
||||||
|
field.estimatedTokens,
|
||||||
|
)} |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
lines.push(`### Largest ${Math.min(top, measurement.tools.length)} tools`);
|
||||||
|
lines.push("");
|
||||||
|
lines.push("| Tool | Bytes | Estimated tokens | Description | Input schema | Output schema |");
|
||||||
|
lines.push("| --- | ---: | ---: | ---: | ---: | ---: |");
|
||||||
|
for (const tool of measurement.tools.slice(0, top)) {
|
||||||
|
const fields = new Map(tool.fields.map((field) => [field.field, field.estimatedTokens]));
|
||||||
|
lines.push(
|
||||||
|
`| ${tool.name} | ${formatInteger(tool.bytes)} | ${formatInteger(
|
||||||
|
tool.estimatedTokens,
|
||||||
|
)} | ${formatInteger(fields.get("description") ?? 0)} | ${formatInteger(
|
||||||
|
fields.get("inputSchema") ?? 0,
|
||||||
|
)} | ${formatInteger(fields.get("outputSchema") ?? 0)} |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
return `${lines.join("\n")}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasurementAgentManager {
|
||||||
|
private readonly agent = {
|
||||||
|
id: MEASUREMENT_AGENT_ID,
|
||||||
|
cwd: MEASUREMENT_CWD,
|
||||||
|
workspaceId: MEASUREMENT_WORKSPACE_ID,
|
||||||
|
provider: "codex",
|
||||||
|
currentModeId: "default",
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd: MEASUREMENT_CWD,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public getAgent(agentId: string): ManagedAgent | null {
|
||||||
|
return agentId === MEASUREMENT_AGENT_ID ? (this.agent as unknown as ManagedAgent) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public listAgents(): ManagedAgent[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasurementAgentStorage {
|
||||||
|
public async list(): Promise<StoredAgentRecord[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasurementProviderSnapshotManager {
|
||||||
|
public listRegisteredProviderIds(): AgentProvider[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasProvider(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getProviderLabel(provider: AgentProvider): string {
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listProviders(): Promise<ProviderSnapshotEntry[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getProvider(): Promise<ProviderSnapshotEntry> {
|
||||||
|
throw new Error("Provider catalog is not used by the measurement script");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listModels(): Promise<[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listModes(): Promise<[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasurementBrowserToolsBroker {
|
||||||
|
public async execute(): Promise<BrowserToolsResponsePayload> {
|
||||||
|
throw new Error("Browser tools are not executed by the measurement script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const options = parseCliOptions(process.argv.slice(2));
|
||||||
|
const measurements = await Promise.all([
|
||||||
|
measureCatalog({
|
||||||
|
label: "Core tools",
|
||||||
|
browserToolsEnabled: false,
|
||||||
|
scope: options.scope,
|
||||||
|
}),
|
||||||
|
measureCatalog({
|
||||||
|
label: "Core + browser tools",
|
||||||
|
browserToolsEnabled: true,
|
||||||
|
scope: options.scope,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (options.format === "json") {
|
||||||
|
process.stdout.write(`${JSON.stringify({ scope: options.scope, measurements }, null, 2)}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stdout.write(formatMarkdown(measurements, options.top));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error: unknown) => {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
process.stderr.write(`${message}\n`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -146,24 +146,21 @@ function createMcpRecordingAgentClients(recorder: LaunchRecorder) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForAgentCompletion(options: {
|
async function assertAgentNotRunning(options: {
|
||||||
client: McpClient;
|
client: McpClient;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const waitResult = await options.client.callTool({
|
const statusResult = await options.client.callTool({
|
||||||
name: "wait_for_agent",
|
name: "get_agent_status",
|
||||||
args: { agentId: options.agentId },
|
args: { agentId: options.agentId },
|
||||||
});
|
});
|
||||||
const payload = getStructuredContent(waitResult);
|
const payload = getStructuredContent(statusResult);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw new Error("wait_for_agent returned no structured payload");
|
throw new Error("get_agent_status returned no structured payload");
|
||||||
}
|
|
||||||
if (payload.permission) {
|
|
||||||
throw new Error(`Unexpected permission while waiting: ${JSON.stringify(payload.permission)}`);
|
|
||||||
}
|
}
|
||||||
const status = payload.status;
|
const status = payload.status;
|
||||||
if (status === "running" || status === "initializing") {
|
if (status === "running" || status === "initializing") {
|
||||||
throw new Error(`Agent still running after wait_for_agent (status=${status})`);
|
throw new Error(`Agent still running after blocking create_agent (status=${status})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +215,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||||
expect(agentId).toBeTruthy();
|
expect(agentId).toBeTruthy();
|
||||||
|
|
||||||
await waitForAgentCompletion({ client, agentId: agentId! });
|
await assertAgentNotRunning({ client, agentId: agentId! });
|
||||||
|
|
||||||
if (existsSync(filePath)) {
|
if (existsSync(filePath)) {
|
||||||
const contents = await readFile(filePath, "utf8");
|
const contents = await readFile(filePath, "utf8");
|
||||||
@@ -703,7 +700,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
|||||||
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||||
expect(agentId).toBeTruthy();
|
expect(agentId).toBeTruthy();
|
||||||
|
|
||||||
await waitForAgentCompletion({ client, agentId: agentId! });
|
await assertAgentNotRunning({ client, agentId: agentId! });
|
||||||
const statusResult = await client.callTool({
|
const statusResult = await client.callTool({
|
||||||
name: "get_agent_status",
|
name: "get_agent_status",
|
||||||
args: { agentId },
|
args: { agentId },
|
||||||
|
|||||||
@@ -6,12 +6,10 @@ import { realpathSync, rmSync } from "node:fs";
|
|||||||
import { access, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
import { access, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||||||
import { join, resolve as resolvePath } from "node:path";
|
import { join, resolve as resolvePath } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import Ajv from "ajv";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||||
import { createAgentMcpServer } from "./mcp-server.js";
|
import { createAgentMcpServer } from "./mcp-server.js";
|
||||||
import { createPaseoToolCatalog } from "./tools/paseo-tools.js";
|
|
||||||
import { AgentManager, type ManagedAgent } from "./agent-manager.js";
|
import { AgentManager, type ManagedAgent } from "./agent-manager.js";
|
||||||
import { AgentStorage, type StoredAgentRecord } from "./agent-storage.js";
|
import { AgentStorage, type StoredAgentRecord } from "./agent-storage.js";
|
||||||
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
|
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
|
||||||
@@ -83,7 +81,6 @@ interface LooseContentBlock {
|
|||||||
|
|
||||||
interface RegisteredMcpTool {
|
interface RegisteredMcpTool {
|
||||||
inputSchema: LooseInputSchema;
|
inputSchema: LooseInputSchema;
|
||||||
outputSchema?: unknown;
|
|
||||||
callback?: (
|
callback?: (
|
||||||
input: unknown,
|
input: unknown,
|
||||||
extra?: unknown,
|
extra?: unknown,
|
||||||
@@ -136,24 +133,20 @@ async function invokeToolWithParsedInput(
|
|||||||
return tool.handler(parsed.data);
|
return tool.handler(parsed.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectOutputSchemaAccepts(tool: RegisteredMcpTool, data: unknown): void {
|
|
||||||
expect(tool.outputSchema).toBeDefined();
|
|
||||||
const jsonSchema = z.toJSONSchema(tool.outputSchema as z.ZodType, {
|
|
||||||
target: "draft-07",
|
|
||||||
unrepresentable: "any",
|
|
||||||
io: "input",
|
|
||||||
});
|
|
||||||
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
||||||
const validate = ajv.compile(jsonSchema);
|
|
||||||
expect(validate(data), JSON.stringify(validate.errors, null, 2)).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function agentsOf(response: {
|
function agentsOf(response: {
|
||||||
structuredContent: LooseStructuredContent;
|
structuredContent: LooseStructuredContent;
|
||||||
}): Array<Record<string, unknown>> {
|
}): Array<Record<string, unknown>> {
|
||||||
return z.array(z.record(z.string(), z.unknown())).parse(response.structuredContent.agents);
|
return z.array(z.record(z.string(), z.unknown())).parse(response.structuredContent.agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectSingleTextContent(response: { content?: LooseContentBlock[] }): string {
|
||||||
|
const content = response.content ?? [];
|
||||||
|
expect(content).toHaveLength(1);
|
||||||
|
const block = content[0];
|
||||||
|
expect(block?.type).toBe("text");
|
||||||
|
return z.string().min(1).parse(block?.text);
|
||||||
|
}
|
||||||
|
|
||||||
async function waitForWorkspaceTitle(
|
async function waitForWorkspaceTitle(
|
||||||
workspaceRecords: Map<string, PersistedWorkspaceRecord>,
|
workspaceRecords: Map<string, PersistedWorkspaceRecord>,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
@@ -753,7 +746,7 @@ function createPaseoWorktreeForMcpTest(options: {
|
|||||||
describe("browser MCP tools", () => {
|
describe("browser MCP tools", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
it("calls registered tools through the MCP SDK with listed output schemas", async () => {
|
it("omits output schemas from tools/list and keeps tool call content model-visible", async () => {
|
||||||
const agentManager = new BoundaryAgentManagerFake();
|
const agentManager = new BoundaryAgentManagerFake();
|
||||||
const agentStorage = new BoundaryAgentStorageFake();
|
const agentStorage = new BoundaryAgentStorageFake();
|
||||||
const broker = new FakeBrowserToolsBroker({
|
const broker = new FakeBrowserToolsBroker({
|
||||||
@@ -802,17 +795,15 @@ describe("browser MCP tools", () => {
|
|||||||
expect(listAgentsResult.structuredContent).toEqual({
|
expect(listAgentsResult.structuredContent).toEqual({
|
||||||
agents: [],
|
agents: [],
|
||||||
});
|
});
|
||||||
|
expectSingleTextContent(browserResult);
|
||||||
|
expect(expectSingleTextContent(listAgentsResult)).toContain('"agents": []');
|
||||||
|
|
||||||
const listedTools = await client.listTools();
|
const listedTools = await client.listTools();
|
||||||
const toolsByName = new Map(listedTools.tools.map((tool) => [tool.name, tool]));
|
expect(listedTools.tools.map((tool) => tool.name)).toEqual(
|
||||||
const catalog = createPaseoToolCatalog(serverOptions);
|
expect.arrayContaining(["browser_list_tabs", "list_agents"]),
|
||||||
|
);
|
||||||
for (const tool of catalog.tools.values()) {
|
for (const tool of listedTools.tools) {
|
||||||
if (tool.outputSchema !== undefined) {
|
expect(tool, `${tool.name} outputSchema`).not.toHaveProperty("outputSchema");
|
||||||
expect(toolsByName.get(tool.name)?.outputSchema, `${tool.name} outputSchema`).toEqual(
|
|
||||||
expect.objectContaining({ type: "object" }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
@@ -1308,7 +1299,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("advertises create_agent output schema that accepts full provider modes", async () => {
|
it("returns create_agent structured content with full provider modes", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
spies.agentManager.createAgent.mockResolvedValue({
|
spies.agentManager.createAgent.mockResolvedValue({
|
||||||
id: "mode-agent",
|
id: "mode-agent",
|
||||||
@@ -1344,7 +1335,24 @@ describe("create_agent MCP tool", () => {
|
|||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expectOutputSchemaAccepts(tool, response.structuredContent);
|
expect(response.structuredContent).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
agentId: "mode-agent",
|
||||||
|
type: "codex",
|
||||||
|
status: "idle",
|
||||||
|
cwd: REPO_CWD,
|
||||||
|
currentModeId: "build",
|
||||||
|
availableModes: [
|
||||||
|
{
|
||||||
|
id: "build",
|
||||||
|
label: "Build",
|
||||||
|
description: null,
|
||||||
|
icon: "hammer",
|
||||||
|
colorTier: "dangerous",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("requires provider as provider/model and rejects the old model field", async () => {
|
it("requires provider as provider/model and rejects the old model field", async () => {
|
||||||
@@ -2904,7 +2912,7 @@ describe("create_agent MCP tool", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(response.structuredContent.guidance).toBe(
|
expect(response.structuredContent.guidance).toBe(
|
||||||
"You will get notified when the created agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
|
"You will get notified when the created agent finishes, errors, or needs permission. Do not poll for status; continue with other work until the notification arrives.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3364,7 +3372,7 @@ describe("send_agent_prompt MCP tool", () => {
|
|||||||
expect(spies.agentManager.subscribe).toHaveBeenCalledTimes(1);
|
expect(spies.agentManager.subscribe).toHaveBeenCalledTimes(1);
|
||||||
expect(spies.agentManager.waitForAgentEvent).not.toHaveBeenCalled();
|
expect(spies.agentManager.waitForAgentEvent).not.toHaveBeenCalled();
|
||||||
expect(response.structuredContent.guidance).toBe(
|
expect(response.structuredContent.guidance).toBe(
|
||||||
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
|
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not poll for status; continue with other work until the notification arrives.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3816,7 +3824,7 @@ describe("create_schedule MCP tool", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("advertises create_schedule output schema that accepts inherited feature values", async () => {
|
it("returns create_schedule structured content with inherited feature values", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
spies.agentManager.getAgent.mockReturnValue({
|
spies.agentManager.getAgent.mockReturnValue({
|
||||||
id: "parent-agent",
|
id: "parent-agent",
|
||||||
@@ -3854,7 +3862,6 @@ describe("create_schedule MCP tool", () => {
|
|||||||
type: "new-agent",
|
type: "new-agent",
|
||||||
config: { featureValues: { auto_accept: true } },
|
config: { featureValues: { auto_accept: true } },
|
||||||
});
|
});
|
||||||
expectOutputSchemaAccepts(tool, response.structuredContent);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes timezone through cron create_schedule input", async () => {
|
it("passes timezone through cron create_schedule input", async () => {
|
||||||
@@ -4872,7 +4879,6 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
`get_agent_status response failed AgentSnapshotPayloadSchema: ${JSON.stringify(parsed.error.issues, null, 2)}`,
|
`get_agent_status response failed AgentSnapshotPayloadSchema: ${JSON.stringify(parsed.error.issues, null, 2)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expectOutputSchemaAccepts(tool, response.structuredContent);
|
|
||||||
expect(response.structuredContent.status).toBe("idle");
|
expect(response.structuredContent.status).toBe("idle");
|
||||||
expect(snapshot).toEqual(
|
expect(snapshot).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -5200,7 +5206,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits list_agents payloads that satisfy the declared output schema", async () => {
|
it("emits list_agents payloads that satisfy the agent list schema", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
spies.agentManager.listAgents.mockReturnValue([createManagedAgent()]);
|
spies.agentManager.listAgents.mockReturnValue([createManagedAgent()]);
|
||||||
@@ -5231,7 +5237,7 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("emits list_pending_permissions payloads that satisfy the declared output schema", async () => {
|
it("emits list_pending_permissions payloads that satisfy the permission schema", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
spies.agentManager.listAgents.mockReturnValue([
|
spies.agentManager.listAgents.mockReturnValue([
|
||||||
createManagedAgent({
|
createManagedAgent({
|
||||||
@@ -5288,7 +5294,6 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expectOutputSchemaAccepts(tool, response.structuredContent);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads archived agents before reading get_agent_activity", async () => {
|
it("loads archived agents before reading get_agent_activity", async () => {
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
title: tool.title,
|
title: tool.title,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
inputSchema: tool.inputSchema,
|
inputSchema: tool.inputSchema,
|
||||||
outputSchema: tool.outputSchema,
|
|
||||||
},
|
},
|
||||||
async (args: unknown, context?: McpToolContext) =>
|
async (args: unknown, context?: McpToolContext) =>
|
||||||
toMcpToolResult(await catalog.executeTool(tool.name, args, { signal: context?.signal })),
|
toMcpToolResult(await catalog.executeTool(tool.name, args, { signal: context?.signal })),
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export async function waitForAgentWithTimeout(
|
|||||||
});
|
});
|
||||||
const recentActivity = curateAgentActivity(recent.items);
|
const recentActivity = curateAgentActivity(recent.items);
|
||||||
const waitedSeconds = Math.round(AGENT_WAIT_TIMEOUT_MS / 1000);
|
const waitedSeconds = Math.round(AGENT_WAIT_TIMEOUT_MS / 1000);
|
||||||
const message = `Awaiting the agent timed out after ${waitedSeconds}s. This does not mean the agent failed - call wait_for_agent again to continue waiting.\n\nRecent activity:\n${recentActivity}`;
|
const message = `Awaiting the agent timed out after ${waitedSeconds}s. This does not mean the agent failed - it is still running. Call get_agent_status to check on it, or continue with other work if you will receive a finish notification.\n\nRecent activity:\n${recentActivity}`;
|
||||||
return {
|
return {
|
||||||
status: snapshot?.lifecycle ?? "idle",
|
status: snapshot?.lifecycle ?? "idle",
|
||||||
permission: null,
|
permission: null,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ensureValidJson } from "../../json-utils.js";
|
|||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
|
||||||
import type { AgentMode, AgentProvider } from "../agent-sdk-types.js";
|
import type { AgentMode, AgentProvider } from "../agent-sdk-types.js";
|
||||||
import type { AgentManager, WaitForAgentResult } from "../agent-manager.js";
|
import type { AgentManager } from "../agent-manager.js";
|
||||||
import {
|
import {
|
||||||
AgentFeatureSchema,
|
AgentFeatureSchema,
|
||||||
AgentPermissionRequestPayloadSchema,
|
AgentPermissionRequestPayloadSchema,
|
||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
killTerminalsForWorkspace,
|
killTerminalsForWorkspace,
|
||||||
type ArchiveDependencies,
|
type ArchiveDependencies,
|
||||||
} from "../../workspace-archive-service.js";
|
} from "../../workspace-archive-service.js";
|
||||||
import { WaitForAgentTracker } from "../wait-for-agent-tracker.js";
|
|
||||||
import { createAgentCommand, type CreateAgentFromMcpInput } from "../create-agent/create.js";
|
import { createAgentCommand, type CreateAgentFromMcpInput } from "../create-agent/create.js";
|
||||||
import type { VoiceCallerContext, VoiceSpeakHandler } from "../../voice-types.js";
|
import type { VoiceCallerContext, VoiceSpeakHandler } from "../../voice-types.js";
|
||||||
import type { FirstAgentContext } from "../../messages.js";
|
import type { FirstAgentContext } from "../../messages.js";
|
||||||
@@ -436,7 +435,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
logger,
|
logger,
|
||||||
} = options;
|
} = options;
|
||||||
const childLogger = logger.child({ module: "agent", component: "paseo-tool-catalog" });
|
const childLogger = logger.child({ module: "agent", component: "paseo-tool-catalog" });
|
||||||
const waitTracker = new WaitForAgentTracker(logger);
|
|
||||||
const callerContext = callerAgentId ? (resolveCallerContext?.(callerAgentId) ?? null) : null;
|
const callerContext = callerAgentId ? (resolveCallerContext?.(callerAgentId) ?? null) : null;
|
||||||
|
|
||||||
const parseToolInput = async (tool: PaseoToolDefinition, input: unknown): Promise<unknown> => {
|
const parseToolInput = async (tool: PaseoToolDefinition, input: unknown): Promise<unknown> => {
|
||||||
@@ -1145,7 +1143,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
const currentSnapshot = agentManager.getAgent(snapshot.id) ?? snapshot;
|
const currentSnapshot = agentManager.getAgent(snapshot.id) ?? snapshot;
|
||||||
const guidance =
|
const guidance =
|
||||||
callerAgentId && notifyOnFinish && initialPromptStarted
|
callerAgentId && notifyOnFinish && initialPromptStarted
|
||||||
? "You will get notified when the created agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives."
|
? "You will get notified when the created agent finishes, errors, or needs permission. Do not poll for status; continue with other work until the notification arrives."
|
||||||
: undefined;
|
: undefined;
|
||||||
const response = {
|
const response = {
|
||||||
content: [],
|
content: [],
|
||||||
@@ -1409,83 +1407,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerTool(
|
|
||||||
"wait_for_agent",
|
|
||||||
{
|
|
||||||
title: "Wait for agent",
|
|
||||||
description:
|
|
||||||
"Block until the agent requests permission or the current run completes. Returns the pending permission (if any) and recent activity summary.",
|
|
||||||
inputSchema: {
|
|
||||||
agentId: z.string().describe("Agent identifier returned by the create_agent tool"),
|
|
||||||
},
|
|
||||||
outputSchema: {
|
|
||||||
agentId: z.string(),
|
|
||||||
status: AgentStatusEnum,
|
|
||||||
permission: AgentPermissionRequestPayloadSchema.nullable(),
|
|
||||||
lastMessage: z.string().nullable(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ agentId }, { signal }) => {
|
|
||||||
const abortController = new AbortController();
|
|
||||||
const cleanupFns: Array<() => void> = [];
|
|
||||||
|
|
||||||
const cleanup = () => {
|
|
||||||
while (cleanupFns.length) {
|
|
||||||
const fn = cleanupFns.pop();
|
|
||||||
try {
|
|
||||||
fn?.();
|
|
||||||
} catch {
|
|
||||||
// ignore cleanup errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const forwardExternalAbort = () => {
|
|
||||||
if (!abortController.signal.aborted) {
|
|
||||||
const reason = signal?.reason ?? new Error("wait_for_agent aborted");
|
|
||||||
abortController.abort(reason);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
forwardExternalAbort();
|
|
||||||
} else {
|
|
||||||
signal.addEventListener("abort", forwardExternalAbort, { once: true });
|
|
||||||
cleanupFns.push(() => signal.removeEventListener("abort", forwardExternalAbort));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unregister = waitTracker.register(agentId, (reason) => {
|
|
||||||
if (!abortController.signal.aborted) {
|
|
||||||
abortController.abort(new Error(reason ?? "wait_for_agent cancelled"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cleanupFns.push(unregister);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result: WaitForAgentResult = await waitForAgentWithTimeout(agentManager, agentId, {
|
|
||||||
signal: abortController.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const validJson = ensureValidJson({
|
|
||||||
agentId,
|
|
||||||
status: result.status,
|
|
||||||
permission: sanitizePermissionRequest(result.permission),
|
|
||||||
lastMessage: result.lastMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = {
|
|
||||||
content: [],
|
|
||||||
structuredContent: validJson,
|
|
||||||
};
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
registerTool(
|
registerTool(
|
||||||
"send_agent_prompt",
|
"send_agent_prompt",
|
||||||
{
|
{
|
||||||
@@ -1508,9 +1429,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
background = Boolean(callerAgentId),
|
background = Boolean(callerAgentId),
|
||||||
notifyOnFinish = Boolean(callerAgentId),
|
notifyOnFinish = Boolean(callerAgentId),
|
||||||
}) => {
|
}) => {
|
||||||
if (agentManager.hasInFlightRun(agentId)) {
|
|
||||||
waitTracker.cancel(agentId, "Agent run interrupted by new prompt");
|
|
||||||
}
|
|
||||||
const shouldNotifyOnFinish = Boolean(callerAgentId && notifyOnFinish && background);
|
const shouldNotifyOnFinish = Boolean(callerAgentId && notifyOnFinish && background);
|
||||||
|
|
||||||
await sendPromptToAgent({
|
await sendPromptToAgent({
|
||||||
@@ -1565,7 +1483,7 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
...(shouldNotifyOnFinish
|
...(shouldNotifyOnFinish
|
||||||
? {
|
? {
|
||||||
guidance:
|
guidance:
|
||||||
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
|
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not poll for status; continue with other work until the notification arrives.",
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
@@ -1705,9 +1623,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
{ agentManager, logger: childLogger },
|
{ agentManager, logger: childLogger },
|
||||||
agentId,
|
agentId,
|
||||||
);
|
);
|
||||||
if (cancelled) {
|
|
||||||
waitTracker.cancel(agentId, "Agent run cancelled");
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({ success: cancelled }),
|
structuredContent: ensureValidJson({ success: cancelled }),
|
||||||
@@ -1737,7 +1652,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
},
|
},
|
||||||
agentId,
|
agentId,
|
||||||
);
|
);
|
||||||
waitTracker.cancel(agentId, "Agent archived");
|
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({ success: true }),
|
structuredContent: ensureValidJson({ success: true }),
|
||||||
@@ -1759,7 +1673,6 @@ export function createPaseoToolCatalog(options: PaseoToolHostDependencies): Pase
|
|||||||
},
|
},
|
||||||
async ({ agentId }) => {
|
async ({ agentId }) => {
|
||||||
await closeAgentCommand({ agentManager }, agentId);
|
await closeAgentCommand({ agentManager }, agentId);
|
||||||
waitTracker.cancel(agentId, "Agent terminated");
|
|
||||||
return {
|
return {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({ success: true }),
|
structuredContent: ensureValidJson({ success: true }),
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
|
||||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
|
||||||
|
|
||||||
describe("WaitForAgentTracker", () => {
|
|
||||||
const logger = createTestLogger();
|
|
||||||
|
|
||||||
it("registers and cancels waiters per agent", () => {
|
|
||||||
const tracker = new WaitForAgentTracker(logger);
|
|
||||||
const cancelA = vi.fn();
|
|
||||||
const cancelB = vi.fn();
|
|
||||||
|
|
||||||
tracker.register("agent-a", cancelA);
|
|
||||||
tracker.register("agent-a", cancelB);
|
|
||||||
|
|
||||||
expect(tracker.cancel("agent-a", "test")).toBe(true);
|
|
||||||
expect(cancelA).toHaveBeenCalledWith("test");
|
|
||||||
expect(cancelB).toHaveBeenCalledWith("test");
|
|
||||||
|
|
||||||
// Subsequent cancels should no-op once cleared
|
|
||||||
expect(tracker.cancel("agent-a")).toBe(false);
|
|
||||||
|
|
||||||
// Unregister removes callback without triggering it
|
|
||||||
const cancelC = vi.fn();
|
|
||||||
const unregisterC = tracker.register("agent-b", cancelC);
|
|
||||||
unregisterC();
|
|
||||||
expect(tracker.cancel("agent-b")).toBe(false);
|
|
||||||
expect(cancelC).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("supports cancelling all waiters", () => {
|
|
||||||
const tracker = new WaitForAgentTracker(logger);
|
|
||||||
const cancelA = vi.fn();
|
|
||||||
const cancelB = vi.fn();
|
|
||||||
|
|
||||||
tracker.register("agent-a", cancelA);
|
|
||||||
tracker.register("agent-b", cancelB);
|
|
||||||
|
|
||||||
expect(tracker.cancelAll()).toBe(2);
|
|
||||||
expect(cancelA).toHaveBeenCalled();
|
|
||||||
expect(cancelB).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import type { Logger } from "pino";
|
|
||||||
|
|
||||||
export type WaitForAgentCanceler = (agentId: string, reason?: string) => boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks long-running wait_for_agent tool calls so they can be cancelled
|
|
||||||
* explicitly (e.g., when a voice barge-in aborts the current turn).
|
|
||||||
*/
|
|
||||||
export class WaitForAgentTracker {
|
|
||||||
private waiters = new Map<string, Set<(reason?: string) => void>>();
|
|
||||||
private logger: Logger;
|
|
||||||
|
|
||||||
constructor(logger: Logger) {
|
|
||||||
this.logger = logger.child({ module: "agent", component: "wait-for-agent-tracker" });
|
|
||||||
}
|
|
||||||
|
|
||||||
register(agentId: string, cancel: (reason?: string) => void): () => void {
|
|
||||||
if (!this.waiters.has(agentId)) {
|
|
||||||
this.waiters.set(agentId, new Set());
|
|
||||||
}
|
|
||||||
const waitersForAgent = this.waiters.get(agentId)!;
|
|
||||||
waitersForAgent.add(cancel);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const current = this.waiters.get(agentId);
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
current.delete(cancel);
|
|
||||||
if (current.size === 0) {
|
|
||||||
this.waiters.delete(agentId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(agentId: string, reason?: string): boolean {
|
|
||||||
const waitersForAgent = this.waiters.get(agentId);
|
|
||||||
if (!waitersForAgent || waitersForAgent.size === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.waiters.delete(agentId);
|
|
||||||
for (const cancel of waitersForAgent) {
|
|
||||||
try {
|
|
||||||
cancel(reason);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn({ err: error, agentId }, "Cancel callback failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelAll(reason?: string): number {
|
|
||||||
let cancelled = 0;
|
|
||||||
for (const agentId of Array.from(this.waiters.keys())) {
|
|
||||||
if (this.cancel(agentId, reason)) {
|
|
||||||
cancelled += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,38 +35,6 @@ const WORKSPACE_CONTEXT_MESSAGE =
|
|||||||
const URL_WHITESPACE_PATTERN = /\s/;
|
const URL_WHITESPACE_PATTERN = /\s/;
|
||||||
const NON_HTTP_EXPLICIT_SCHEME_PATTERN = /^(?!https?:\/\/)[a-z][a-z0-9+.-]*:\/\//i;
|
const NON_HTTP_EXPLICIT_SCHEME_PATTERN = /^(?!https?:\/\/)[a-z][a-z0-9+.-]*:\/\//i;
|
||||||
|
|
||||||
const BrowserToolOutputSchema = {
|
|
||||||
ok: z.boolean(),
|
|
||||||
result: z.unknown().optional(),
|
|
||||||
error: z
|
|
||||||
.object({
|
|
||||||
code: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
retryable: z.boolean(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
dialogs: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
type: z.enum(["alert", "confirm", "prompt", "beforeunload"]),
|
|
||||||
message: z.string(),
|
|
||||||
defaultValue: z.string().optional(),
|
|
||||||
action: z.enum(["accepted", "dismissed"]),
|
|
||||||
promptText: z.string().optional(),
|
|
||||||
timestamp: z.number(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
context: z
|
|
||||||
.object({
|
|
||||||
agentId: z.string().optional(),
|
|
||||||
cwd: z.string().optional(),
|
|
||||||
workspaceId: z.string().optional(),
|
|
||||||
browserId: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const BrowserHttpUrlInputSchema = z
|
const BrowserHttpUrlInputSchema = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -103,7 +71,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
description:
|
description:
|
||||||
"List open Paseo browser tabs for this agent's workspace across connected browser automation hosts. Use returned browserId values with tab-scoped tools.",
|
"List open Paseo browser tabs for this agent's workspace across connected browser automation hosts. Use returned browserId values with tab-scoped tools.",
|
||||||
inputSchema: {},
|
inputSchema: {},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -133,7 +100,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
url: BrowserHttpUrlInputSchema.optional(),
|
url: BrowserHttpUrlInputSchema.optional(),
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ url }) => {
|
async ({ url }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -163,7 +129,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ browserId }) => {
|
async ({ browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -196,7 +161,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
doubleClick: z.boolean().optional(),
|
doubleClick: z.boolean().optional(),
|
||||||
modifiers: z.array(BrowserClickModifierInputSchema).optional(),
|
modifiers: z.array(BrowserClickModifierInputSchema).optional(),
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ ref, browserId, button, doubleClick, modifiers }) => {
|
async ({ ref, browserId, button, doubleClick, modifiers }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -231,7 +195,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
value: z.string(),
|
value: z.string(),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ ref, value, browserId }) => {
|
async ({ ref, value, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -260,7 +223,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
description:
|
description:
|
||||||
"Wait until a Paseo browser tab contains text or reaches a URL fragment. Use browserId from browser_new_tab or browser_list_tabs; waits up to 5s by default on the browser host.",
|
"Wait until a Paseo browser tab contains text or reaches a URL fragment. Use browserId from browser_new_tab or browser_list_tabs; waits up to 5s by default on the browser host.",
|
||||||
inputSchema: BrowserWaitInputSchema,
|
inputSchema: BrowserWaitInputSchema,
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ text, url, timeoutMs, browserId }) => {
|
async ({ text, url, timeoutMs, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -295,7 +257,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
ref: BrowserRefInputSchema.optional(),
|
ref: BrowserRefInputSchema.optional(),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ text, ref, browserId }) => {
|
async ({ text, ref, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -328,7 +289,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
ref: BrowserRefInputSchema.optional(),
|
ref: BrowserRefInputSchema.optional(),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ key, ref, browserId }) => {
|
async ({ key, ref, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -357,7 +317,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
description:
|
description:
|
||||||
"Navigate a Paseo browser tab to a URL. Use browserId from browser_new_tab or browser_list_tabs; pass an http(s) URL or a scheme-less host URL, which is treated as http.",
|
"Navigate a Paseo browser tab to a URL. Use browserId from browser_new_tab or browser_list_tabs; pass an http(s) URL or a scheme-less host URL, which is treated as http.",
|
||||||
inputSchema: { url: BrowserHttpUrlInputSchema, browserId: BrowserAutomationBrowserIdSchema },
|
inputSchema: { url: BrowserHttpUrlInputSchema, browserId: BrowserAutomationBrowserIdSchema },
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ url, browserId }) => {
|
async ({ url, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -407,7 +366,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
title: toolConfig.title,
|
title: toolConfig.title,
|
||||||
description: toolConfig.description,
|
description: toolConfig.description,
|
||||||
inputSchema: { browserId: BrowserAutomationBrowserIdSchema },
|
inputSchema: { browserId: BrowserAutomationBrowserIdSchema },
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ browserId }) => {
|
async ({ browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -438,7 +396,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
fullPage: z.boolean().default(false),
|
fullPage: z.boolean().default(false),
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ browserId, fullPage }) => {
|
async ({ browserId, fullPage }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -470,7 +427,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
filePaths: z.array(z.string().min(1)).min(1),
|
filePaths: z.array(z.string().min(1)).min(1),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ ref, filePaths, browserId }) => {
|
async ({ ref, filePaths, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -507,7 +463,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
title: toolConfig.title,
|
title: toolConfig.title,
|
||||||
description: toolConfig.description,
|
description: toolConfig.description,
|
||||||
inputSchema: { ref: BrowserRefInputSchema, browserId: BrowserAutomationBrowserIdSchema },
|
inputSchema: { ref: BrowserRefInputSchema, browserId: BrowserAutomationBrowserIdSchema },
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ ref, browserId }) => {
|
async ({ ref, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -540,7 +495,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
value: z.string(),
|
value: z.string(),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ ref, value, browserId }) => {
|
async ({ ref, value, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -573,7 +527,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
targetRef: BrowserRefInputSchema,
|
targetRef: BrowserRefInputSchema,
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ sourceRef, targetRef, browserId }) => {
|
async ({ sourceRef, targetRef, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -605,7 +558,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
maxEntries: z.number().int().positive().max(200).optional(),
|
maxEntries: z.number().int().positive().max(200).optional(),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ maxEntries, browserId }) => {
|
async ({ maxEntries, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -637,7 +589,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
ref: BrowserRefInputSchema.optional(),
|
ref: BrowserRefInputSchema.optional(),
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ function: functionSource, ref, browserId }) => {
|
async ({ function: functionSource, ref, browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -671,7 +622,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
deltaX: z.number(),
|
deltaX: z.number(),
|
||||||
deltaY: z.number(),
|
deltaY: z.number(),
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ browserId, ref, deltaX, deltaY }) => {
|
async ({ browserId, ref, deltaX, deltaY }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -705,7 +655,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
width: z.number().int().positive(),
|
width: z.number().int().positive(),
|
||||||
height: z.number().int().positive(),
|
height: z.number().int().positive(),
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ browserId, width, height }) => {
|
async ({ browserId, width, height }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
@@ -736,7 +685,6 @@ export function registerBrowserTools(options: RegisterBrowserToolsOptions): void
|
|||||||
inputSchema: {
|
inputSchema: {
|
||||||
browserId: BrowserAutomationBrowserIdSchema,
|
browserId: BrowserAutomationBrowserIdSchema,
|
||||||
},
|
},
|
||||||
outputSchema: BrowserToolOutputSchema,
|
|
||||||
},
|
},
|
||||||
async ({ browserId }) => {
|
async ({ browserId }) => {
|
||||||
const context = resolveBrowserToolContext(options);
|
const context = resolveBrowserToolContext(options);
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may
|
|||||||
| Tool | Function |
|
| Tool | Function |
|
||||||
| -------------------- | ---------------------------------------------------------------------------------------------------- |
|
| -------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
| `create_agent` | Create an agent tied to a working directory, optionally with initial settings or a new git worktree. |
|
| `create_agent` | Create an agent tied to a working directory, optionally with initial settings or a new git worktree. |
|
||||||
| `wait_for_agent` | Block until an agent requests permission or finishes its current run. |
|
|
||||||
| `send_agent_prompt` | Send a task to a running agent. |
|
| `send_agent_prompt` | Send a task to a running agent. |
|
||||||
| `get_agent_status` | Return the latest snapshot for an agent. |
|
| `get_agent_status` | Return the latest snapshot for an agent. |
|
||||||
| `list_agents` | List recent agents as compact metadata. |
|
| `list_agents` | List recent agents as compact metadata. |
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ If the file is missing, use sensible defaults and tell the user once.
|
|||||||
|
|
||||||
Agents take time — 10–30+ minutes is routine. Favor asynchronous workflows.
|
Agents take time — 10–30+ minutes is routine. Favor asynchronous workflows.
|
||||||
|
|
||||||
For agent-scoped `create_agent` and background `send_agent_prompt`, leave `notifyOnFinish` omitted or set it to `true` unless the work is truly fire-and-forget. You will get notified when the target agent finishes, errors, or needs permission. **You must not call `wait_for_agent` on a notify-on-finish agent.** Move on to other work. The notification arrives on its own.
|
For agent-scoped `create_agent` and background `send_agent_prompt`, leave `notifyOnFinish` omitted or set it to `true` unless the work is truly fire-and-forget. You will get notified when the target agent finishes, errors, or needs permission. Move on to other work. The notification arrives on its own.
|
||||||
|
|
||||||
Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you.
|
Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user