diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 09bf07cfa..718aedffc 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -246,7 +246,6 @@ function normalizeAgentSnapshot( requiresAttention: snapshot.requiresAttention ?? false, attentionReason: snapshot.attentionReason ?? null, attentionTimestamp, - parentAgentId: snapshot.parentAgentId, archivedAt, }; } diff --git a/packages/app/src/hooks/use-aggregated-agents.ts b/packages/app/src/hooks/use-aggregated-agents.ts index 192197a6b..a0ba5380e 100644 --- a/packages/app/src/hooks/use-aggregated-agents.ts +++ b/packages/app/src/hooks/use-aggregated-agents.ts @@ -62,11 +62,6 @@ export function useAggregatedAgents(): AggregatedAgentsResult { } const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId; for (const agent of agents.values()) { - // Use agent's own lastActivityAt field directly - // Skip child agents - only show root agents on homepage - if (agent.parentAgentId) { - continue; - } const nextAgent: AggregatedAgent = { id: agent.id, serverId, @@ -79,7 +74,6 @@ export function useAggregatedAgents(): AggregatedAgentsResult { requiresAttention: agent.requiresAttention, attentionReason: agent.attentionReason, attentionTimestamp: agent.attentionTimestamp, - parentAgentId: agent.parentAgentId, archivedAt: agent.archivedAt, }; allAgents.push(nextAgent); diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 76b4a6db4..6ef96d195 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -98,7 +98,6 @@ export interface Agent { requiresAttention?: boolean; attentionReason?: "finished" | "error" | "permission" | null; attentionTimestamp?: Date | null; - parentAgentId?: string | null; archivedAt?: Date | null; } @@ -839,7 +838,6 @@ export const useSessionStore = create()( requiresAttention: agent.requiresAttention ?? false, attentionReason: agent.attentionReason ?? null, attentionTimestamp: agent.attentionTimestamp ?? null, - parentAgentId: agent.parentAgentId, }); } return entries; diff --git a/packages/app/src/types/agent-directory.ts b/packages/app/src/types/agent-directory.ts index 67190b53c..f3a412921 100644 --- a/packages/app/src/types/agent-directory.ts +++ b/packages/app/src/types/agent-directory.ts @@ -12,6 +12,5 @@ export interface AgentDirectoryEntry { requiresAttention?: boolean; attentionReason?: "finished" | "error" | "permission" | null; attentionTimestamp?: Date | null; - parentAgentId?: string | null; archivedAt?: Date | null; } diff --git a/packages/app/src/utils/agent-grouping.test.ts b/packages/app/src/utils/agent-grouping.test.ts index 54fd231e2..0d33aaede 100644 --- a/packages/app/src/utils/agent-grouping.test.ts +++ b/packages/app/src/utils/agent-grouping.test.ts @@ -16,7 +16,6 @@ function makeAgent(overrides: Partial = {}): AggregatedAgent { requiresAttention: overrides.requiresAttention ?? false, attentionReason: overrides.attentionReason ?? null, attentionTimestamp: overrides.attentionTimestamp ?? null, - parentAgentId: overrides.parentAgentId ?? null, } as AggregatedAgent; } diff --git a/packages/app/src/utils/tool-call-parsers.ts b/packages/app/src/utils/tool-call-parsers.ts index 205596a7f..87f534b6d 100644 --- a/packages/app/src/utils/tool-call-parsers.ts +++ b/packages/app/src/utils/tool-call-parsers.ts @@ -1162,12 +1162,8 @@ const TOOL_NAME_MAP: Record = { read_file: "Read", apply_patch: "Edit", paseo_worktree_setup: "Setup", - "agent-control.set_title": "Set title", - "agent-control.set_branch": "Set branch", set_title: "Set title", set_branch: "Set branch", - "mcp__agent-control__set_title": "Set title", - "mcp__agent-control__set_branch": "Set branch", thinking: "Thinking", }; diff --git a/packages/cli/docs/type-audit.md b/packages/cli/docs/type-audit.md index 62449e5a2..0bb1b528b 100644 --- a/packages/cli/docs/type-audit.md +++ b/packages/cli/docs/type-audit.md @@ -55,7 +55,7 @@ Notes: ### `packages/cli/src/commands/agent/inspect.ts` Inline types: -- `AgentSnapshotLike` (snapshot fields + `lastUsage`, `capabilities`, `availableModes`, `pendingPermissions`, `parentAgentId`) +- `AgentSnapshotLike` (snapshot fields + `lastUsage`, `capabilities`, `availableModes`, `pendingPermissions`) Recommended server types: - `AgentSnapshotPayload` (overall snapshot shape). **Not exported**. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 7acd27c6a..13fb37c86 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -37,9 +37,11 @@ export function createCli(): Command { // Primary agent commands (top-level) program .command('ls') - .description('List agents. By default shows running agents in current directory.') + .description('List agents. By default shows background agents (without ui=true) in current directory.') .option('-a, --all', 'Include all statuses (not just running)') .option('-g, --global', 'Show agents from all directories (not just current)') + .option('--label ', 'Filter by label (can be used multiple times)', collectMultiple, []) + .option('--ui', 'Show only UI agents (equivalent to --label ui=true)') .option('--json', 'Output in JSON format') .option('--host ', 'Daemon host:port (default: localhost:6767)') .action((options, command) => { @@ -62,6 +64,8 @@ export function createCli(): Command { .option('--base ', 'Base branch for worktree (default: current branch)') .option('--image ', 'Attach image(s) to the initial prompt (can be used multiple times)', collectMultiple, []) .option('--cwd ', 'Working directory (default: current)') + .option('--label ', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, []) + .option('--ui', 'Mark as UI agent (equivalent to --label ui=true)') .option('--host ', 'Daemon host:port (default: localhost:6767)') .action(withOutput(runRunCommand)) diff --git a/packages/cli/src/commands/agent/index.ts b/packages/cli/src/commands/agent/index.ts index 3130ecb56..4c820418c 100644 --- a/packages/cli/src/commands/agent/index.ts +++ b/packages/cli/src/commands/agent/index.ts @@ -14,12 +14,19 @@ import { withOutput } from '../../output/index.js' export function createAgentCommand(): Command { const agent = new Command('agent').description('Manage agents (advanced operations)') + // Helper function to collect multiple option values into an array + const collectMultiple = (value: string, previous: string[]): string[] => { + return previous.concat([value]) + } + // Primary agent commands (same as top-level) agent .command('ls') - .description('List agents. By default shows running agents in current directory.') + .description('List agents. By default shows background agents (without ui=true) in current directory.') .option('-a, --all', 'Include all statuses (not just running)') .option('-g, --global', 'Show agents from all directories (not just current)') + .option('--label ', 'Filter by label (can be used multiple times)', collectMultiple, []) + .option('--ui', 'Show only UI agents (equivalent to --label ui=true)') .option('--json', 'Output in JSON format') .option('--host ', 'Daemon host:port (default: localhost:6767)') .action((options, command) => { @@ -39,6 +46,8 @@ export function createAgentCommand(): Command { .option('--model ', 'Model to use (e.g., claude-sonnet-4-20250514, claude-3-5-haiku-20241022)') .option('--mode ', 'Provider-specific mode (e.g., plan, default, bypass)') .option('--cwd ', 'Working directory (default: current)') + .option('--label ', 'Add label(s) to the agent (can be used multiple times)', collectMultiple, []) + .option('--ui', 'Mark as UI agent (equivalent to --label ui=true)') .option('--host ', 'Daemon host:port (default: localhost:6767)') .action(withOutput(runRunCommand)) diff --git a/packages/cli/src/commands/agent/inspect.ts b/packages/cli/src/commands/agent/inspect.ts index d52486146..3952c15c8 100644 --- a/packages/cli/src/commands/agent/inspect.ts +++ b/packages/cli/src/commands/agent/inspect.ts @@ -34,7 +34,6 @@ interface AgentInspect { id: string tool: string }> - parentAgentId: string | null } /** Key-value row for table display */ @@ -122,7 +121,6 @@ function toInspectData(snapshot: AgentSnapshotPayload): AgentInspect { id: p.id, tool: p.name ?? 'unknown', })), - parentAgentId: snapshot.parentAgentId ?? null, } } @@ -177,11 +175,6 @@ function toInspectRows(agent: AgentInspect): InspectRow[] { rows.push({ key: 'PendingPermissions', value: '[]' }) } - rows.push({ - key: 'ParentAgentId', - value: agent.parentAgentId ?? 'null', - }) - return rows } diff --git a/packages/cli/src/commands/agent/ls.ts b/packages/cli/src/commands/agent/ls.ts index c8512c83e..530b095fe 100644 --- a/packages/cli/src/commands/agent/ls.ts +++ b/packages/cli/src/commands/agent/ls.ts @@ -82,6 +82,10 @@ export interface AgentLsOptions extends CommandOptions { status?: string /** Filter by specific cwd (overrides default cwd filtering) */ cwd?: string + /** Filter by labels (key=value format) */ + label?: string[] + /** Filter to UI agents only (equivalent to --label ui=true) */ + ui?: boolean } /** @@ -145,6 +149,46 @@ export async function runLsCommand( }) } + // Label filtering: + // Parse --label flags and --ui flag + // --ui is equivalent to --label ui=true + // By default (no --ui flag), show background agents (those WITHOUT ui=true) + const labelFilters: Record = {} + if (options.label) { + for (const labelStr of options.label) { + const eqIndex = labelStr.indexOf('=') + if (eqIndex !== -1) { + const key = labelStr.slice(0, eqIndex) + const value = labelStr.slice(eqIndex + 1) + labelFilters[key] = value + } + } + } + // Add ui=true filter if --ui flag is set + if (options.ui) { + labelFilters['ui'] = 'true' + } + + // Apply label filtering + if (Object.keys(labelFilters).length > 0) { + // Filter to agents that have ALL specified labels (AND semantics) + agents = agents.filter((a) => { + const agentLabels = (a as any).labels as Record | undefined + for (const [key, value] of Object.entries(labelFilters)) { + if (!agentLabels || agentLabels[key] !== value) { + return false + } + } + return true + }) + } else { + // Default: show background agents only (those without ui=true) + agents = agents.filter((a) => { + const agentLabels = (a as any).labels as Record | undefined + return !agentLabels || agentLabels['ui'] !== 'true' + }) + } + await client.close() // Sort agents: running first, then idle, then others; within each group, most recent first diff --git a/packages/cli/src/commands/agent/run.ts b/packages/cli/src/commands/agent/run.ts index 023a813c6..7c1b96752 100644 --- a/packages/cli/src/commands/agent/run.ts +++ b/packages/cli/src/commands/agent/run.ts @@ -37,6 +37,8 @@ export interface AgentRunOptions extends CommandOptions { base?: string image?: string[] cwd?: string + label?: string[] + ui?: boolean } function toRunResult(agent: AgentSnapshotPayload): AgentRunResult { @@ -127,6 +129,31 @@ export async function runRunCommand( } : undefined + // Build labels from --label and --ui flags + // --ui is syntactic sugar for --label ui=true + // If explicit --label ui=... is provided, it takes precedence over --ui + const labels: Record = {} + if (options.label) { + for (const labelStr of options.label) { + const eqIndex = labelStr.indexOf('=') + if (eqIndex === -1) { + const error: CommandError = { + code: 'INVALID_LABEL', + message: `Invalid label format: ${labelStr}`, + details: 'Labels must be in key=value format', + } + throw error + } + const key = labelStr.slice(0, eqIndex) + const value = labelStr.slice(eqIndex + 1) + labels[key] = value + } + } + // Add ui=true if --ui flag is set and ui label not already set + if (options.ui && !('ui' in labels)) { + labels['ui'] = 'true' + } + // Create the agent const agent = await client.createAgent({ provider: (options.provider as 'claude' | 'codex' | 'opencode') ?? 'claude', @@ -138,6 +165,7 @@ export async function runRunCommand( images, git, worktreeName: options.worktree, + labels: Object.keys(labels).length > 0 ? labels : undefined, }) await client.close() diff --git a/packages/server/scripts/test-codex-mcp-agents.ts b/packages/server/scripts/test-codex-mcp-agents.ts deleted file mode 100644 index 254d62665..000000000 --- a/packages/server/scripts/test-codex-mcp-agents.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { z } from "zod"; - -async function main() { - // The bearer token is the base64-encoded credentials - const bearerToken = Buffer.from("mo:bo").toString("base64"); - - const transport = new StdioClientTransport({ - command: "codex", - args: ["mcp-server"], - env: { - ...process.env, - // Set the bearer token env var - PASEO_AGENT_CONTROL_TOKEN: bearerToken - }, - }); - - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: { elicitation: {} } } - ); - - // Listen for events - client.setNotificationHandler( - z.object({ - method: z.literal("codex/event"), - params: z.object({ msg: z.any() }), - }).passthrough(), - (data) => { - const event = (data.params as { msg: unknown }).msg as { type?: string }; - if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") { - process.stdout.write("MCP Event: " + JSON.stringify(event, null, 2) + "\n"); - } - } - ); - - await client.connect(transport); - - // Use correct route (/mcp/agents) and bearer token env var - process.stdout.write("\n=== Testing HTTP MCP server with correct route and bearer token ===\n\n"); - - try { - const result = await client.callTool({ - name: "codex", - arguments: { - prompt: "List all the MCP tools you have available. Just list them, don't use any.", - sandbox: "danger-full-access", - "approval-policy": "never", - config: { - mcp_servers: { - "agent-control": { - url: "http://localhost:6767/mcp/agents", - bearer_token_env_var: "PASEO_AGENT_CONTROL_TOKEN" - } - } - } - } - }, undefined, { timeout: 60000 }); - - process.stdout.write("\n=== RESULT ===\n"); - const content = (result as { content: { text?: string }[] }).content; - for (const item of content) { - if (item.text) { - process.stdout.write(item.text + "\n"); - } - } - } catch (error) { - process.stderr.write("Error: " + String(error) + "\n"); - } - - await client.close(); -} - -main().catch((error) => { - process.stderr.write(String(error) + "\n"); - process.exitCode = 1; -}); diff --git a/packages/server/scripts/test-codex-mcp-basic-auth.ts b/packages/server/scripts/test-codex-mcp-basic-auth.ts deleted file mode 100644 index 9e0865c06..000000000 --- a/packages/server/scripts/test-codex-mcp-basic-auth.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { z } from "zod"; - -async function main() { - const transport = new StdioClientTransport({ - command: "codex", - args: ["mcp-server"], - env: { - ...process.env, - // Try passing credentials via env var that Codex might read - AGENT_CONTROL_AUTH: "mo:bo" - }, - }); - - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: { elicitation: {} } } - ); - - // Listen for events - client.setNotificationHandler( - z.object({ - method: z.literal("codex/event"), - params: z.object({ msg: z.any() }), - }).passthrough(), - (data) => { - const event = (data.params as { msg: unknown }).msg as { type?: string }; - if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") { - process.stdout.write("MCP Event: " + JSON.stringify(event, null, 2) + "\n"); - } - } - ); - - await client.connect(transport); - - // Try passing MCP server config via the config parameter with headers - process.stdout.write("\n=== Testing HTTP MCP server with basic auth in config ===\n\n"); - - try { - // Create base64 encoded credentials - const credentials = Buffer.from("mo:bo").toString("base64"); - - const result = await client.callTool({ - name: "codex", - arguments: { - prompt: "List all the MCP tools you have available. Just list them, don't use any.", - sandbox: "danger-full-access", - "approval-policy": "never", - config: { - mcp_servers: { - "agent-control": { - url: "http://localhost:6767/mcp/agent-control", - // Try various ways to pass auth - headers: { - "Authorization": `Basic ${credentials}` - } - } - } - } - } - }, undefined, { timeout: 60000 }); - - process.stdout.write("\n=== RESULT ===\n"); - const content = (result as { content: { text?: string }[] }).content; - for (const item of content) { - if (item.text) { - process.stdout.write(item.text + "\n"); - } - } - } catch (error) { - process.stderr.write("Error: " + String(error) + "\n"); - } - - await client.close(); -} - -main().catch((error) => { - process.stderr.write(String(error) + "\n"); - process.exitCode = 1; -}); diff --git a/packages/server/scripts/test-codex-mcp-bearer.ts b/packages/server/scripts/test-codex-mcp-bearer.ts deleted file mode 100644 index e3c1eec58..000000000 --- a/packages/server/scripts/test-codex-mcp-bearer.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { z } from "zod"; - -async function main() { - // Create base64 encoded credentials - const credentials = Buffer.from("mo:bo").toString("base64"); - - const transport = new StdioClientTransport({ - command: "codex", - args: ["mcp-server"], - env: { - ...process.env, - // Set the bearer token env var that we will reference - AGENT_CONTROL_TOKEN: `Basic ${credentials}` - }, - }); - - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: { elicitation: {} } } - ); - - // Listen for events - client.setNotificationHandler( - z.object({ - method: z.literal("codex/event"), - params: z.object({ msg: z.any() }), - }).passthrough(), - (data) => { - const event = (data.params as { msg: unknown }).msg as { type?: string }; - if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") { - process.stdout.write("MCP Event: " + JSON.stringify(event, null, 2) + "\n"); - } - } - ); - - await client.connect(transport); - - // Try passing MCP server config with bearer_token_env_var - process.stdout.write("\n=== Testing HTTP MCP server with bearer_token_env_var ===\n\n"); - - try { - const result = await client.callTool({ - name: "codex", - arguments: { - prompt: "List all the MCP tools you have available. Just list them, don't use any.", - sandbox: "danger-full-access", - "approval-policy": "never", - config: { - mcp_servers: { - "agent-control": { - url: "http://localhost:6767/mcp/agent-control", - bearer_token_env_var: "AGENT_CONTROL_TOKEN" - } - } - } - } - }, undefined, { timeout: 60000 }); - - process.stdout.write("\n=== RESULT ===\n"); - const content = (result as { content: { text?: string }[] }).content; - for (const item of content) { - if (item.text) { - process.stdout.write(item.text + "\n"); - } - } - } catch (error) { - process.stderr.write("Error: " + String(error) + "\n"); - } - - await client.close(); -} - -main().catch((error) => { - process.stderr.write(String(error) + "\n"); - process.exitCode = 1; -}); diff --git a/packages/server/scripts/test-codex-mcp-http-headers.ts b/packages/server/scripts/test-codex-mcp-http-headers.ts deleted file mode 100644 index a054cab0b..000000000 --- a/packages/server/scripts/test-codex-mcp-http-headers.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { z } from "zod"; - -async function main() { - // The basic auth header - const basicAuth = `Basic ${Buffer.from("mo:bo").toString("base64")}`; - - const transport = new StdioClientTransport({ - command: "codex", - args: ["mcp-server"], - env: { - ...process.env, - }, - }); - - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: { elicitation: {} } } - ); - - // Listen for events - client.setNotificationHandler( - z.object({ - method: z.literal("codex/event"), - params: z.object({ msg: z.any() }), - }).passthrough(), - (data) => { - const event = (data.params as { msg: unknown }).msg as { type?: string }; - if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") { - process.stdout.write("MCP Event: " + JSON.stringify(event, null, 2) + "\n"); - } - } - ); - - await client.connect(transport); - - // Test using http_headers with Authorization - process.stdout.write("\n=== Testing HTTP MCP server with http_headers and Authorization ===\n\n"); - - try { - const result = await client.callTool({ - name: "codex", - arguments: { - prompt: "List all the MCP tools you have available. Just list them, don't use any.", - sandbox: "danger-full-access", - "approval-policy": "never", - config: { - mcp_servers: { - "agent-control": { - url: "http://localhost:6767/mcp/agents", - http_headers: { - Authorization: basicAuth - } - } - } - } - } - }, undefined, { timeout: 60000 }); - - process.stdout.write("\n=== RESULT ===\n"); - const content = (result as { content: { text?: string }[] }).content; - for (const item of content) { - if (item.text) { - process.stdout.write(item.text + "\n"); - } - } - } catch (error) { - process.stderr.write("Error: " + String(error) + "\n"); - } - - await client.close(); -} - -main().catch((error) => { - process.stderr.write(String(error) + "\n"); - process.exitCode = 1; -}); diff --git a/packages/server/scripts/test-codex-mcp-http.ts b/packages/server/scripts/test-codex-mcp-http.ts deleted file mode 100644 index 485974e05..000000000 --- a/packages/server/scripts/test-codex-mcp-http.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { z } from "zod"; - -async function main() { - const transport = new StdioClientTransport({ - command: "codex", - args: ["mcp-server"], - env: { ...process.env }, - }); - - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: { elicitation: {} } } - ); - - // Listen for events - client.setNotificationHandler( - z.object({ - method: z.literal("codex/event"), - params: z.object({ msg: z.any() }), - }).passthrough(), - (data) => { - const event = (data.params as { msg: unknown }).msg as { type?: string }; - if (event.type === "mcp_startup_update" || event.type === "mcp_startup_complete") { - process.stdout.write("MCP Event: " + JSON.stringify(event, null, 2) + "\n"); - } - } - ); - - await client.connect(transport); - - // Try passing MCP server config via the config parameter with HTTP URL - process.stdout.write("\n=== Testing HTTP MCP server config (agent-control style) ===\n\n"); - - try { - const result = await client.callTool({ - name: "codex", - arguments: { - prompt: "List all the MCP tools you have available. Just list them, don't use any.", - sandbox: "danger-full-access", - "approval-policy": "never", - config: { - mcp_servers: { - "agent-control": { - url: "http://localhost:6767/mcp/agent-control", - type: "http" - } - } - } - } - }, undefined, { timeout: 60000 }); - - process.stdout.write("\n=== RESULT ===\n"); - const content = (result as { content: { text?: string }[] }).content; - for (const item of content) { - if (item.text) { - process.stdout.write(item.text + "\n"); - } - } - } catch (error) { - process.stderr.write("Error: " + String(error) + "\n"); - } - - await client.close(); -} - -main().catch((error) => { - process.stderr.write(String(error) + "\n"); - process.exitCode = 1; -}); diff --git a/packages/server/scripts/verify-codex-mcp-config.ts b/packages/server/scripts/verify-codex-mcp-config.ts deleted file mode 100644 index 5a0a53fe9..000000000 --- a/packages/server/scripts/verify-codex-mcp-config.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Quick script to verify buildCodexMcpConfig includes MCP servers - -import { buildPaseoDaemonConfigFromEnv } from "../src/server/config.js"; - -const config = buildPaseoDaemonConfigFromEnv(); - -process.stdout.write("=== agentControlMcp config ===\n"); -process.stdout.write(JSON.stringify(config.agentControlMcp, null, 2) + "\n"); - -// Simulate what buildCodexMcpConfig does -const mcpServers: Record = {}; - -if (config.agentControlMcp) { - const agentControlUrl = config.agentControlMcp.url; - mcpServers["agent-control"] = { - url: agentControlUrl, - ...(config.agentControlMcp.headers ? { http_headers: config.agentControlMcp.headers } : {}), - }; -} - -mcpServers["playwright"] = { - command: "npx", - args: ["@playwright/mcp", "--headless", "--isolated"], -}; - -process.stdout.write("\n=== Built MCP servers config ===\n"); -process.stdout.write(JSON.stringify(mcpServers, null, 2) + "\n"); diff --git a/packages/server/src/client/daemon-client-v2.ts b/packages/server/src/client/daemon-client-v2.ts index 4f77bb00c..7eaf8810b 100644 --- a/packages/server/src/client/daemon-client-v2.ts +++ b/packages/server/src/client/daemon-client-v2.ts @@ -163,6 +163,7 @@ export type CreateAgentRequestOptions = { git?: GitSetupOptions; worktreeName?: string; requestId?: string; + labels?: Record; } & AgentConfigOverrides; type VoiceConversationLoadedPayload = VoiceConversationLoadedMessage["payload"]; @@ -774,6 +775,9 @@ export class DaemonClientV2 { : {}), ...(options.git ? { git: options.git } : {}), ...(options.worktreeName ? { worktreeName: options.worktreeName } : {}), + ...(options.labels && Object.keys(options.labels).length > 0 + ? { labels: options.labels } + : {}), }); const statusPromise = this.waitFor( @@ -2587,6 +2591,7 @@ function resolveAgentConfig(options: CreateAgentRequestOptions): AgentSessionCon git: _git, worktreeName: _worktreeName, requestId: _requestId, + labels: _labels, ...overrides } = options; diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 3b68aa4c8..f7acd1452 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -24,7 +24,6 @@ import type { AgentTimelineItem, AgentUsage, AgentRuntimeInfo, - AgentControlMcpConfig, ListPersistedAgentsOptions, PersistedAgentDescriptor, } from "./agent-sdk-types.js"; @@ -58,7 +57,6 @@ export type AgentManagerOptions = { maxTimelineItems?: number; idFactory?: () => string; registry?: AgentStorage; - agentControlMcp?: AgentControlMcpConfig; onAgentAttention?: AgentAttentionCallback; logger: Logger; }; @@ -101,11 +99,14 @@ type ManagedAgentBase = { lastUsage?: AgentUsage; lastError?: string; attention: AttentionState; - parentAgentId?: string; /** * Internal agents are hidden from listings and don't trigger notifications. */ internal?: boolean; + /** + * User-defined labels for categorizing agents (e.g., { ui: "true" }). + */ + labels?: Record; }; type ManagedAgentWithSession = ManagedAgentBase & { @@ -189,7 +190,6 @@ export class AgentManager { private readonly idFactory: () => string; private readonly registry?: AgentStorage; private readonly previousStatuses = new Map(); - private readonly agentControlMcp?: AgentControlMcpConfig; private onAgentAttention?: AgentAttentionCallback; private logger: Logger; @@ -198,7 +198,6 @@ export class AgentManager { options?.maxTimelineItems ?? DEFAULT_MAX_TIMELINE_ITEMS; this.idFactory = options?.idFactory ?? (() => randomUUID()); this.registry = options?.registry; - this.agentControlMcp = options?.agentControlMcp; this.onAgentAttention = options?.onAgentAttention; this.logger = options.logger.child({ module: "agent", component: "agent-manager" }); if (options?.clients) { @@ -311,15 +310,17 @@ export class AgentManager { async createAgent( config: AgentSessionConfig, - agentId?: string + agentId?: string, + options?: { labels?: Record } ): Promise { - const normalizedConfig = await this.normalizeConfig(config); + const normalizedConfig = await this.normalizeConfig(config, { labels: options?.labels }); const client = this.requireClient(normalizedConfig.provider); const session = await client.createSession(normalizedConfig); return this.registerSession( session, normalizedConfig, - agentId ?? this.idFactory() + agentId ?? this.idFactory(), + { labels: options?.labels } ); } @@ -327,7 +328,12 @@ export class AgentManager { handle: AgentPersistenceHandle, overrides?: Partial, agentId?: string, - timestamps?: { createdAt?: Date; updatedAt?: Date; lastUserMessageAt?: Date | null } + options?: { + createdAt?: Date; + updatedAt?: Date; + lastUserMessageAt?: Date | null; + labels?: Record; + } ): Promise { const metadata = (handle.metadata ?? {}) as Partial; const mergedConfig = { @@ -346,7 +352,7 @@ export class AgentManager { session, normalizedConfig, agentId ?? this.idFactory(), - timestamps + options ); } @@ -795,15 +801,17 @@ export class AgentManager { session: AgentSession, config: AgentSessionConfig, agentId: string, - timestamps?: { createdAt?: Date; updatedAt?: Date; lastUserMessageAt?: Date | null } + options?: { + createdAt?: Date; + updatedAt?: Date; + lastUserMessageAt?: Date | null; + labels?: Record; + } ): Promise { if (this.agents.has(agentId)) { throw new Error(`Agent with id ${agentId} already exists`); } - // Inform the session of its managed agent ID for MCP parent-child relationships - session.setManagedAgentId?.(agentId); - const now = new Date(); const managed = { id: agentId, @@ -814,8 +822,8 @@ export class AgentManager { config, runtimeInfo: undefined, lifecycle: "initializing", - createdAt: timestamps?.createdAt ?? now, - updatedAt: timestamps?.updatedAt ?? now, + createdAt: options?.createdAt ?? now, + updatedAt: options?.updatedAt ?? now, availableModes: [], currentModeId: null, pendingPermissions: new Map(), @@ -823,10 +831,10 @@ export class AgentManager { timeline: [], persistence: session.describePersistence(), historyPrimed: false, - lastUserMessageAt: timestamps?.lastUserMessageAt ?? null, + lastUserMessageAt: options?.lastUserMessageAt ?? null, attention: { requiresAttention: false }, - parentAgentId: config.parentAgentId, internal: config.internal ?? false, + labels: options?.labels, } as ActiveManagedAgent; this.agents.set(agentId, managed); @@ -1093,7 +1101,8 @@ export class AgentManager { private async normalizeConfig( - config: AgentSessionConfig + config: AgentSessionConfig, + options?: { labels?: Record } ): Promise { const normalized: AgentSessionConfig = { ...config }; @@ -1107,14 +1116,9 @@ export class AgentManager { normalized.model = trimmed.length > 0 ? trimmed : undefined; } - if (!normalized.agentControlMcp && this.agentControlMcp) { - normalized.agentControlMcp = this.agentControlMcp; - } - - if ( - normalized.paseoPromptInstructions === undefined && - normalized.agentControlMcp - ) { + // Inject paseoPromptInstructions for UI agents (with ui=true label) + const isUiAgent = options?.labels?.ui === "true"; + if (isUiAgent) { normalized.paseoPromptInstructions = getSelfIdentificationInstructions({ cwd: normalized.cwd, }); diff --git a/packages/server/src/server/agent/agent-mcp.e2e.test.ts b/packages/server/src/server/agent/agent-mcp.e2e.test.ts index d991b6a77..56c455a9d 100644 --- a/packages/server/src/server/agent/agent-mcp.e2e.test.ts +++ b/packages/server/src/server/agent/agent-mcp.e2e.test.ts @@ -102,9 +102,6 @@ describe("agent MCP end-to-end", () => { mcpDebug: false, agentClients: {}, agentStoragePath: path.join(paseoHome, "agents"), - agentControlMcp: { - url: `http://127.0.0.1:${port}/mcp/agents`, - }, }; const previousCodexSessionDir = process.env.CODEX_SESSION_DIR; @@ -235,9 +232,6 @@ describe("agent MCP end-to-end", () => { mcpDebug: false, agentClients: {}, agentStoragePath: path.join(paseoHome, "agents"), - agentControlMcp: { - url: `http://127.0.0.1:${port}/mcp/agents`, - }, }; const previousCodexSessionDir = process.env.CODEX_SESSION_DIR; diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index f09680f06..6992b1c35 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -44,6 +44,7 @@ export function toStoredAgentRecord( ? agent.lastUserMessageAt.toISOString() : null, title: options?.title ?? null, + labels: agent.labels && Object.keys(agent.labels).length > 0 ? agent.labels : undefined, lastStatus: agent.lifecycle, lastModeId: agent.currentModeId ?? config?.modeId ?? null, config: config ?? null, @@ -56,7 +57,6 @@ export function toStoredAgentRecord( attentionTimestamp: agent.attention.requiresAttention ? agent.attention.attentionTimestamp.toISOString() : null, - parentAgentId: agent.parentAgentId ?? null, internal: options?.internal, } satisfies StoredAgentRecord; } @@ -85,7 +85,7 @@ export function toAgentPayload( pendingPermissions: sanitizePendingPermissions(agent.pendingPermissions), persistence: sanitizePersistenceHandle(agent.persistence), title: options?.title ?? null, - parentAgentId: agent.parentAgentId ?? null, + labels: agent.labels && Object.keys(agent.labels).length > 0 ? agent.labels : undefined, }; const usage = sanitizeUsage(agent.lastUsage); diff --git a/packages/server/src/server/agent/agent-response-loop.e2e.test.ts b/packages/server/src/server/agent/agent-response-loop.e2e.test.ts index d8f52e484..3d8016248 100644 --- a/packages/server/src/server/agent/agent-response-loop.e2e.test.ts +++ b/packages/server/src/server/agent/agent-response-loop.e2e.test.ts @@ -154,7 +154,6 @@ describe("getStructuredAgentResponse (e2e)", () => { cwd = mkdtempSync(path.join(tmpdir(), "agent-response-loop-")); manager = new AgentManager({ clients: createAllClients(logger), - agentControlMcp: { url: agentMcpServer.url }, logger, }); }); diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index f92996763..5a60841e9 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -171,11 +171,6 @@ export type AgentCommandResult = { usage?: AgentUsage; }; -export type AgentControlMcpConfig = { - url: string; - headers?: Record; -}; - export type ListPersistedAgentsOptions = { limit?: number; }; @@ -201,7 +196,6 @@ export type AgentSessionConfig = { networkAccess?: boolean; webSearch?: boolean; reasoningEffort?: string; - agentControlMcp?: AgentControlMcpConfig; /** * Paseo-owned instructions injected into the first user prompt via * .... @@ -215,7 +209,6 @@ export type AgentSessionConfig = { claude?: Partial; }; mcpServers?: AgentMetadata; - parentAgentId?: string; /** * Internal agents are hidden from listings and don't trigger notifications. * They are used for ephemeral system tasks like commit/PR generation. @@ -239,12 +232,6 @@ export interface AgentSession { describePersistence(): AgentPersistenceHandle | null; interrupt(): Promise; close(): Promise; - /** - * Set the managed agent ID for this session. This is called by AgentManager - * after registration to allow the session to include its ID in MCP requests - * (for parent-child agent relationships). - */ - setManagedAgentId?(agentId: string): void; /** * List available slash commands for this session. * Commands are provider-specific - Claude supports skills and built-in commands. diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index 986db189a..1bf8810cd 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -37,6 +37,7 @@ const STORED_AGENT_SCHEMA = z.object({ lastActivityAt: z.string().optional(), lastUserMessageAt: z.string().nullable().optional(), title: z.string().nullable().optional(), + labels: z.record(z.string()).optional(), lastStatus: AgentStatusSchema.default("closed"), lastModeId: z.string().nullable().optional(), config: SERIALIZABLE_CONFIG_SCHEMA, @@ -53,7 +54,6 @@ const STORED_AGENT_SCHEMA = z.object({ requiresAttention: z.boolean().optional(), attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), attentionTimestamp: z.string().nullable().optional(), - parentAgentId: z.string().nullable().optional(), internal: z.boolean().optional(), archivedAt: z.string().nullable().optional(), }); diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 847fca848..153e44f50 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -40,7 +40,7 @@ export interface AgentMcpServerOptions { paseoHome?: string; /** * ID of the agent that is connecting to this MCP server. - * When set, create_agent will auto-inject this as parentAgentId. + * Used for cwd/mode inheritance when agents spawn child agents. */ callerAgentId?: string; logger: Logger; @@ -350,12 +350,6 @@ export async function createAgentMcpServer( .describe( "Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately." ), - parentAgentId: z - .string() - .optional() - .describe( - "Optional parent agent ID. When set, this agent is a child of the specified parent agent." - ), }; const createAgentInputSchema = callerAgentId @@ -400,12 +394,10 @@ export async function createAgentMcpServer( worktreeName?: string; background?: boolean; title: string; - parentAgentId?: string; }; let resolvedCwd: string; let resolvedMode: string | undefined; - let resolvedParentAgentId: string | undefined; if (callerAgentId) { const parentAgent = agentManager.getAgent(callerAgentId); @@ -413,7 +405,6 @@ export async function createAgentMcpServer( throw new Error(`Parent agent ${callerAgentId} not found`); } resolvedCwd = parentAgent.cwd; - resolvedParentAgentId = callerAgentId; const provider: AgentProvider = agentType ?? "claude"; const parentMode = parentAgent.currentModeId; @@ -430,14 +421,12 @@ export async function createAgentMcpServer( initialMode: string; worktreeName?: string; baseBranch?: string; - parentAgentId?: string; }; const { cwd, initialMode, worktreeName, baseBranch, - parentAgentId, } = topLevelArgs; resolvedCwd = expandPath(cwd); @@ -457,7 +446,6 @@ export async function createAgentMcpServer( } resolvedMode = initialMode; - resolvedParentAgentId = parentAgentId; } const provider: AgentProvider = agentType ?? "claude"; @@ -467,7 +455,6 @@ export async function createAgentMcpServer( cwd: resolvedCwd, modeId: resolvedMode, title: normalizedTitle ?? undefined, - parentAgentId: resolvedParentAgentId, }); if (initialPrompt) { diff --git a/packages/server/src/server/agent/providers/claude-agent-commands.test.ts b/packages/server/src/server/agent/providers/claude-agent-commands.test.ts index 4a5cb6348..c32433df2 100644 --- a/packages/server/src/server/agent/providers/claude-agent-commands.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent-commands.test.ts @@ -26,9 +26,6 @@ const hasClaudeCredentials = provider: "claude", cwd: process.cwd(), modeId: "plan", - agentControlMcp: { - url: "http://localhost:6767/mcp", // Placeholder - not actually used in plan mode - }, }; beforeAll(async () => { diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index 4f42f5453..782d13195 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -240,7 +240,6 @@ async function startAgentMcpServer(): Promise { provider: "claude", cwd, modeId: options?.modeId, - agentControlMcp: { url: agentMcpServer.url }, extra: { claude: { sandbox: { enabled: true, autoAllowBashIfSandboxed: false }, diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 29baf0fbf..b2dec4a2b 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -116,16 +116,6 @@ type ClaudeAgentSessionOptions = { logger: Logger; }; -function appendCallerAgentId(url: string, agentId: string): string { - try { - const parsed = new URL(url); - parsed.searchParams.set("callerAgentId", agentId); - return parsed.toString(); - } catch { - const separator = url.includes("?") ? "&" : "?"; - return `${url}${separator}callerAgentId=${encodeURIComponent(agentId)}`; - } -} export function extractUserMessageText(content: unknown): string | null { if (typeof content === "string") { @@ -228,25 +218,6 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial 0) { - agentControlMcp.headers = normalizedHeaders; - } - } - result.agentControlMcp = agentControlMcp; - } - } if (isMetadata(metadata.extra)) { const extra: AgentSessionConfig["extra"] = {}; if (isMetadata(metadata.extra.codex)) { @@ -262,9 +233,6 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial | null = null; private cachedRuntimeInfo: AgentRuntimeInfo | null = null; private lastOptionsModel: string | null = null; - private managedAgentId: string | null = null; constructor( config: ClaudeAgentConfig, @@ -722,10 +689,6 @@ class ClaudeAgentSession implements AgentSession { this.input = null; } - setManagedAgentId(agentId: string): void { - this.managedAgentId = agentId; - } - async listCommands(): Promise { const q = await this.ensureQuery(); const commands = await q.supportedCommands(); @@ -819,28 +782,11 @@ class ClaudeAgentSession implements AgentSession { ...this.config.extra?.claude, }; - // Always include the agent-control MCP server so agents can launch other agents - if (!this.config.agentControlMcp) { - throw new Error("agentControlMcp is required for ClaudeAgentSession"); - } - const agentControlConfig = this.config.agentControlMcp; - const agentControlUrl = this.managedAgentId - ? appendCallerAgentId(agentControlConfig.url, this.managedAgentId) - : agentControlConfig.url; - const defaultMcpServers: Record = { - "agent-control": { - type: "http", - url: agentControlUrl, - ...(agentControlConfig.headers ? { headers: agentControlConfig.headers } : {}), - }, - }; - if (this.config.mcpServers) { const normalizedUserServers = this.normalizeMcpServers(this.config.mcpServers); - // Merge user-provided MCP servers with defaults, user servers take precedence - base.mcpServers = { ...defaultMcpServers, ...normalizedUserServers }; - } else { - base.mcpServers = defaultMcpServers; + if (normalizedUserServers) { + base.mcpServers = normalizedUserServers; + } } if (this.config.model) { diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts index e5d86e74f..ce36beb7c 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -1859,11 +1859,6 @@ const PermissionParamsSchema = z type PermissionParams = z.infer; -const AgentControlMcpConfigSchema = z.object({ - url: z.string(), - headers: z.record(z.string()).optional(), -}); - type AgentSessionExtra = NonNullable; const AgentSessionExtraSchema = z.object({ @@ -1883,10 +1878,8 @@ const AgentSessionConfigSchema = z networkAccess: z.boolean().optional(), webSearch: z.boolean().optional(), reasoningEffort: z.string().optional(), - agentControlMcp: AgentControlMcpConfigSchema.optional(), extra: AgentSessionExtraSchema.optional(), mcpServers: z.record(z.unknown()).optional(), - parentAgentId: z.string().optional(), }) .passthrough(); @@ -2718,7 +2711,6 @@ function buildCodexMcpConfig( config: AgentSessionConfig, prompt: string, modeId: string, - managedAgentId?: string, experimentalResume?: string | null ): { prompt: string; @@ -2762,21 +2754,6 @@ function buildCodexMcpConfig( // Build MCP servers configuration const mcpServers: Record = {}; - // Add agent-control MCP server (HTTP-based) if configured - if (config.agentControlMcp) { - let agentControlUrl = config.agentControlMcp.url; - // Append caller agent ID to URL if this is a managed agent - if (managedAgentId) { - const separator = agentControlUrl.includes("?") ? "&" : "?"; - agentControlUrl = `${agentControlUrl}${separator}callerAgentId=${encodeURIComponent(managedAgentId)}`; - } - mcpServers["agent-control"] = { - url: agentControlUrl, - tool_timeout_sec: 600, // 10 min timeout for child agents - ...(config.agentControlMcp.headers ? { http_headers: config.agentControlMcp.headers } : {}), - }; - } - // Merge MCP servers from extra.codex.mcp_servers (legacy location) const extraCodex = config.extra?.codex as Record | undefined; if (extraCodex?.mcp_servers && typeof extraCodex.mcp_servers === "object") { @@ -3037,7 +3014,6 @@ class CodexMcpAgentSession implements AgentSession { private turnState: TurnState | null = null; private pendingPatchChanges = new Map(); private patchChangesByCallId = new Map(); - private managedAgentId: string | null = null; private resumeHandle: AgentPersistenceHandle | null = null; private pendingResumeFile: string | null = null; @@ -3541,10 +3517,6 @@ class CodexMcpAgentSession implements AgentSession { this.conversationId = null; } - setManagedAgentId(agentId: string): void { - this.managedAgentId = agentId; - } - async listCommands(): Promise { const [skills, prompts] = await Promise.all([ listCodexSkills(this.config.cwd), @@ -3591,7 +3563,6 @@ class CodexMcpAgentSession implements AgentSession { this.config, prompt, this.currentMode, - this.managedAgentId ?? undefined, resumeFile ); const attempt = async (arguments_: CodexToolArguments) => @@ -3631,7 +3602,7 @@ class CodexMcpAgentSession implements AgentSession { ); if (isMissingConversationIdResponse(response)) { const replayPrompt = this.buildResumePrompt(prompt); - const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode, this.managedAgentId ?? undefined); + const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode); const attempt = async (arguments_: CodexToolArguments) => this.client.callTool( { name: "codex", arguments: arguments_ }, @@ -3654,7 +3625,7 @@ class CodexMcpAgentSession implements AgentSession { } catch (error) { if (isMissingConversationIdError(error)) { const replayPrompt = this.buildResumePrompt(prompt); - const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode, this.managedAgentId ?? undefined); + const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode); const attempt = async (arguments_: CodexToolArguments) => this.client.callTool( { name: "codex", arguments: arguments_ }, diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 61b878758..e7cc81443 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -56,7 +56,6 @@ import { printPairingQrIfEnabled } from "./pairing-qr.js"; import { startRelayTransport, type RelayTransportController } from "./relay-transport.js"; import type { AgentClient, - AgentControlMcpConfig, AgentProvider, } from "./agent/agent-sdk-types.js"; @@ -78,7 +77,6 @@ export type PaseoDaemonConfig = { mcpDebug: boolean; agentClients: Partial>; agentStoragePath: string; - agentControlMcp: AgentControlMcpConfig; relayEnabled?: boolean; relayEndpoint?: string; appBaseUrl?: string; @@ -211,7 +209,6 @@ export async function createPaseoDaemon( ...config.agentClients, }, registry: agentStorage, - agentControlMcp: config.agentControlMcp, logger, }); @@ -378,10 +375,7 @@ export async function createPaseoDaemon( agentStorage, downloadTokenStore, config.paseoHome, - { - agentMcpUrl: config.agentControlMcp.url, - agentMcpHeaders: config.agentControlMcp.headers, - }, + agentMcpRoute, { allowedOrigins }, { stt: sttService, tts: ttsService }, terminalManager diff --git a/packages/server/src/server/config.ts b/packages/server/src/server/config.ts index 2077e34d6..8f4e234a0 100644 --- a/packages/server/src/server/config.ts +++ b/packages/server/src/server/config.ts @@ -80,9 +80,6 @@ export function loadConfig( agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE, agentMcpAllowedHosts: [mcpListen, `localhost:${mcpListen.split(":")[1]}`], mcpDebug: env.MCP_DEBUG === "1", - agentControlMcp: { - url: `http://${mcpListen}${DEFAULT_AGENT_MCP_ROUTE}`, - }, agentStoragePath: path.join(paseoHome, "agents"), staticDir: "public", agentClients: {}, diff --git a/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts b/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts index 32768bf55..a96b17bd8 100644 --- a/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/daemon-restart-resume.e2e.test.ts @@ -57,9 +57,6 @@ async function startDaemon(options: { mcpDebug: false, agentClients: {}, agentStoragePath: path.join(options.paseoHome, "agents"), - agentControlMcp: { - url: `http://127.0.0.1:${port}/mcp/agents`, - }, openai: process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : undefined, }; diff --git a/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts b/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts index 78dfec102..fd3b58db7 100644 --- a/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/orchestration.e2e.test.ts @@ -1,22 +1,16 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest"; -import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync, readFileSync, readdirSync } from "fs"; +import { mkdtempSync, rmSync } from "fs"; import { tmpdir } from "os"; import path from "path"; import { createDaemonTestContext, type DaemonTestContext, } from "../test-utils/index.js"; -import type { AgentTimelineItem } from "../agent/agent-sdk-types.js"; -import type { AgentSnapshotPayload, SessionOutboundMessage } from "../messages.js"; function tmpCwd(): string { return mkdtempSync(path.join(tmpdir(), "daemon-e2e-")); } -// Use gpt-5.1-codex-mini with low reasoning effort for faster test execution -const CODEX_TEST_MODEL = "gpt-5.1-codex-mini"; -const CODEX_TEST_REASONING_EFFORT = "low"; - describe("daemon E2E", () => { let ctx: DaemonTestContext; @@ -29,140 +23,10 @@ describe("daemon E2E", () => { }, 60000); describe("multi-agent orchestration", () => { - test( - "parent agent creates child agent via agent-control MCP", - async () => { - const cwd = tmpCwd(); - const childCwd = tmpCwd(); - - // Create parent Codex agent - const parent = await ctx.client.createAgent({ - provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT, - cwd, - title: "Parent Agent", - }); - - expect(parent.id).toBeTruthy(); - expect(parent.status).toBe("idle"); - - // Clear message queue before sending prompt - ctx.client.clearMessageQueue(); - - // Prompt the parent to create a child agent using agent-control MCP - const prompt = [ - `Use the create_agent tool from the agent-control MCP server to create a new codex agent.`, - `Set the cwd to: ${childCwd}`, - `Set the title to: Child Agent`, - `Set agentType to: codex`, - `Do NOT set an initialPrompt - just create the agent.`, - `After creating the agent, reply with "CREATED" followed by the child's agentId.`, - ].join(" "); - - await ctx.client.sendMessage(parent.id, prompt); - - // Wait for parent to complete - const afterCreate = await ctx.client.waitForAgentIdle( - parent.id, - 120000 - ); - expect(afterCreate.status).toBe("idle"); - - // Verify timeline contains a tool call to create_agent - const queue = ctx.client.getMessageQueue(); - const timelineItems: AgentTimelineItem[] = []; - for (const m of queue) { - if ( - m.type === "agent_stream" && - m.payload.agentId === parent.id && - m.payload.event.type === "timeline" - ) { - timelineItems.push(m.payload.event.item); - } - } - - // Should have a tool call to create_agent from agent-control - const hasCreateAgentCall = timelineItems.some( - (item) => - item.type === "tool_call" && - item.name === "agent-control.create_agent" - ); - expect(hasCreateAgentCall).toBe(true); - - // Now verify we can see both agents via session_state - // Send a list_persisted_agents_request to trigger session_state refresh - // Or we can check the queue for agent_state messages - const agentStateMessages = queue.filter( - (m) => m.type === "agent_state" - ); - - // Extract unique agent IDs from state messages - const agentIds = new Set(); - for (const m of agentStateMessages) { - if (m.type === "agent_state") { - agentIds.add(m.payload.id); - } - } - - // Should have at least 2 agents (parent + child) - expect(agentIds.size).toBeGreaterThanOrEqual(2); - expect(agentIds.has(parent.id)).toBe(true); - - // Get the child agent ID from the tool call output - const createAgentCall = timelineItems.find( - (item) => - item.type === "tool_call" && - item.name === "agent-control.create_agent" - ); - - let childAgentId: string | null = null; - if ( - createAgentCall && - createAgentCall.type === "tool_call" && - createAgentCall.output - ) { - const output = createAgentCall.output as unknown; - const tryExtract = (value: unknown): string | null => { - if (!value) return null; - if (typeof value === "string") { - try { - return tryExtract(JSON.parse(value)); - } catch { - return null; - } - } - if (typeof value !== "object") return null; - const asObj = value as Record; - const direct = asObj.agentId; - if (typeof direct === "string") return direct; - const structured = asObj.structuredContent; - if (structured && typeof structured === "object") { - const nested = (structured as Record).agentId; - if (typeof nested === "string") return nested; - } - if (typeof structured === "string") { - try { - return tryExtract(JSON.parse(structured)); - } catch { - return null; - } - } - return null; - }; - - childAgentId = tryExtract(output); - } - - // Verify we found the child agent ID - expect(childAgentId).toBeTruthy(); - expect(agentIds.has(childAgentId!)).toBe(true); - - // Cleanup - rmSync(cwd, { recursive: true, force: true }); - rmSync(childCwd, { recursive: true, force: true }); - }, - 300000 // 5 minute timeout for multi-agent E2E - ); + // TODO: Re-implement orchestration tests with new Paseo MCP + // The old agent-control MCP has been removed + test("placeholder for future orchestration tests", async () => { + expect(true).toBe(true); + }); }); - - }); diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index 8b7c6be7b..8bb5c5379 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -77,10 +77,16 @@ export function buildSessionConfig( export function extractTimestamps( record: StoredAgentRecord -): { createdAt: Date; updatedAt: Date; lastUserMessageAt: Date | null } { +): { + createdAt: Date; + updatedAt: Date; + lastUserMessageAt: Date | null; + labels?: Record; +} { return { createdAt: new Date(record.createdAt), updatedAt: new Date(record.lastActivityAt ?? record.updatedAt), lastUserMessageAt: record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null, + labels: record.labels, }; } diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index f5940aecb..eefcf6a4e 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -104,11 +104,6 @@ import { getProjectIcon } from "../utils/project-icon.js"; import { expandTilde } from "../utils/path.js"; import type pino from "pino"; -type AgentMcpClientConfig = { - agentMcpUrl: string; - agentMcpHeaders?: Record; -}; - const execAsync = promisify(exec); const READ_ONLY_GIT_ENV: NodeJS.ProcessEnv = { ...process.env, @@ -294,7 +289,7 @@ export class Session { private agentTools: ToolSet | null = null; private agentManager: AgentManager; private readonly agentStorage: AgentStorage; - private readonly agentMcpConfig: AgentMcpClientConfig; + private readonly agentMcpRoute: string; private readonly downloadTokenStore: DownloadTokenStore; private readonly pushTokenStore: PushTokenStore; private readonly providerRegistry: ReturnType; @@ -317,7 +312,7 @@ export class Session { paseoHome: string, agentManager: AgentManager, agentStorage: AgentStorage, - agentMcpConfig: AgentMcpClientConfig, + agentMcpRoute: string, stt: OpenAISTT | null, tts: OpenAITTS | null, terminalManager: TerminalManager | null, @@ -331,7 +326,7 @@ export class Session { this.paseoHome = paseoHome; this.agentManager = agentManager; this.agentStorage = agentStorage; - this.agentMcpConfig = agentMcpConfig; + this.agentMcpRoute = agentMcpRoute; this.terminalManager = terminalManager; this.voiceConversationStore = voiceConversationStore; this.abortController = new AbortController(); @@ -505,15 +500,10 @@ export class Session { */ private async initializeAgentMcp(): Promise { try { + // Connect to the local MCP server using localhost + const agentMcpUrl = `http://127.0.0.1:6767${this.agentMcpRoute}`; const transport = new StreamableHTTPClientTransport( - new URL(this.agentMcpConfig.agentMcpUrl), - this.agentMcpConfig.agentMcpHeaders - ? { - requestInit: { - headers: this.agentMcpConfig.agentMcpHeaders, - }, - } - : undefined + new URL(agentMcpUrl) ); this.agentMcpClient = await experimental_createMCPClient({ @@ -632,7 +622,6 @@ export class Session { requiresAttention: record.requiresAttention ?? false, attentionReason: record.attentionReason ?? null, attentionTimestamp: record.attentionTimestamp ?? null, - parentAgentId: record.parentAgentId ?? null, archivedAt: record.archivedAt ?? null, }; } @@ -1326,7 +1315,7 @@ export class Session { private async handleCreateAgentRequest( msg: Extract ): Promise { - const { config, worktreeName, requestId, initialPrompt, git, images } = msg; + const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg; this.sessionLogger.info( { cwd: config.cwd, provider: config.provider, worktreeName }, `Creating agent in ${config.cwd} (${config.provider})${ @@ -1356,9 +1345,14 @@ export class Session { const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig( config, git, - worktreeName + worktreeName, + labels + ); + const snapshot = await this.agentManager.createAgent( + sessionConfig, + undefined, + { labels } ); - const snapshot = await this.agentManager.createAgent(sessionConfig); await this.forwardAgentState(snapshot); const trimmedPrompt = initialPrompt?.trim(); @@ -1584,7 +1578,8 @@ export class Session { private async buildAgentSessionConfig( config: AgentSessionConfig, gitOptions?: GitSetupOptions, - legacyWorktreeName?: string + legacyWorktreeName?: string, + _labels?: Record ): Promise<{ sessionConfig: AgentSessionConfig; worktreeConfig?: WorktreeConfig }> { let cwd = expandTilde(config.cwd); const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName); diff --git a/packages/server/src/server/test-utils/paseo-daemon.ts b/packages/server/src/server/test-utils/paseo-daemon.ts index b144090b5..5e4c67a7e 100644 --- a/packages/server/src/server/test-utils/paseo-daemon.ts +++ b/packages/server/src/server/test-utils/paseo-daemon.ts @@ -64,9 +64,6 @@ export async function createTestPaseoDaemon( mcpDebug: false, agentClients: {}, agentStoragePath: path.join(paseoHome, "agents"), - agentControlMcp: { - url: `http://127.0.0.1:${port}/mcp/agents`, - }, relayEnabled: options.relayEnabled ?? false, relayEndpoint: options.relayEndpoint ?? "relay.paseo.sh:443", appBaseUrl: "https://app.paseo.sh", diff --git a/packages/server/src/server/websocket-server.ts b/packages/server/src/server/websocket-server.ts index 437a8e738..3b86b9010 100644 --- a/packages/server/src/server/websocket-server.ts +++ b/packages/server/src/server/websocket-server.ts @@ -10,11 +10,6 @@ import type pino from "pino"; import type { WSOutboundMessage } from "./messages.js"; import { WebSocketSessionBridge } from "./websocket-session-bridge.js"; -type AgentMcpClientConfig = { - agentMcpUrl: string; - agentMcpHeaders?: Record; -}; - type WebSocketServerConfig = { allowedOrigins: Set; }; @@ -34,7 +29,7 @@ export class VoiceAssistantWebSocketServer { agentStorage: AgentStorage, downloadTokenStore: DownloadTokenStore, paseoHome: string, - agentMcpConfig: AgentMcpClientConfig, + agentMcpRoute: string, wsConfig: WebSocketServerConfig, speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null }, terminalManager?: TerminalManager | null @@ -46,7 +41,7 @@ export class VoiceAssistantWebSocketServer { agentStorage, downloadTokenStore, paseoHome, - agentMcpConfig, + agentMcpRoute, speech, terminalManager ); diff --git a/packages/server/src/server/websocket-session-bridge.ts b/packages/server/src/server/websocket-session-bridge.ts index 60c479aa4..9cd7a1267 100644 --- a/packages/server/src/server/websocket-session-bridge.ts +++ b/packages/server/src/server/websocket-session-bridge.ts @@ -19,11 +19,6 @@ import type { OpenAITTS } from "./agent/tts-openai.js"; import type { TerminalManager } from "../terminal/terminal-manager.js"; import type pino from "pino"; -type AgentMcpClientConfig = { - agentMcpUrl: string; - agentMcpHeaders?: Record; -}; - export class WebSocketSessionBridge { private readonly logger: pino.Logger; private readonly sessions: Map = new Map(); @@ -34,7 +29,7 @@ export class WebSocketSessionBridge { private readonly paseoHome: string; private readonly pushTokenStore: PushTokenStore; private readonly pushService: PushService; - private readonly agentMcpConfig: AgentMcpClientConfig; + private readonly agentMcpRoute: string; private readonly stt: OpenAISTT | null; private readonly tts: OpenAITTS | null; private readonly terminalManager: TerminalManager | null; @@ -46,7 +41,7 @@ export class WebSocketSessionBridge { agentStorage: AgentStorage, downloadTokenStore: DownloadTokenStore, paseoHome: string, - agentMcpConfig: AgentMcpClientConfig, + agentMcpRoute: string, speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null }, terminalManager?: TerminalManager | null ) { @@ -55,7 +50,7 @@ export class WebSocketSessionBridge { this.agentStorage = agentStorage; this.downloadTokenStore = downloadTokenStore; this.paseoHome = paseoHome; - this.agentMcpConfig = agentMcpConfig; + this.agentMcpRoute = agentMcpRoute; this.stt = speech?.stt ?? null; this.tts = speech?.tts ?? null; this.terminalManager = terminalManager ?? null; @@ -91,7 +86,7 @@ export class WebSocketSessionBridge { this.paseoHome, this.agentManager, this.agentStorage, - this.agentMcpConfig, + this.agentMcpRoute, this.stt, this.tts, this.terminalManager, diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index efa7e3d02..95e4704a3 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -63,12 +63,6 @@ const AgentSessionConfigSchema = z.object({ networkAccess: z.boolean().optional(), webSearch: z.boolean().optional(), reasoningEffort: z.string().optional(), - agentControlMcp: z - .object({ - url: z.string(), - headers: z.record(z.string()).optional(), - }) - .optional(), extra: z .object({ codex: z.record(z.unknown()).optional(), @@ -247,10 +241,10 @@ export const AgentSnapshotPayloadSchema = z.object({ lastUsage: AgentUsageSchema.optional(), lastError: z.string().optional(), title: z.string().nullable(), + labels: z.record(z.string()).optional(), requiresAttention: z.boolean().optional(), attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(), attentionTimestamp: z.string().nullable().optional(), - parentAgentId: z.string().nullable().optional(), archivedAt: z.string().nullable().optional(), }); @@ -385,6 +379,7 @@ export const CreateAgentRequestMessageSchema = z.object({ mimeType: z.string(), // e.g., "image/jpeg", "image/png" })).optional(), git: GitSetupOptionsSchema.optional(), + labels: z.record(z.string()).optional(), requestId: z.string(), });