refactor: replace agent-control MCP with labels system and Paseo MCP

This commit is contained in:
Mohamed Boudra
2026-01-29 11:31:15 +07:00
parent dfda6daeed
commit c015fdbef1
40 changed files with 171 additions and 797 deletions

View File

@@ -246,7 +246,6 @@ function normalizeAgentSnapshot(
requiresAttention: snapshot.requiresAttention ?? false,
attentionReason: snapshot.attentionReason ?? null,
attentionTimestamp,
parentAgentId: snapshot.parentAgentId,
archivedAt,
};
}

View File

@@ -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);

View File

@@ -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<SessionStore>()(
requiresAttention: agent.requiresAttention ?? false,
attentionReason: agent.attentionReason ?? null,
attentionTimestamp: agent.attentionTimestamp ?? null,
parentAgentId: agent.parentAgentId,
});
}
return entries;

View File

@@ -12,6 +12,5 @@ export interface AgentDirectoryEntry {
requiresAttention?: boolean;
attentionReason?: "finished" | "error" | "permission" | null;
attentionTimestamp?: Date | null;
parentAgentId?: string | null;
archivedAt?: Date | null;
}

View File

@@ -16,7 +16,6 @@ function makeAgent(overrides: Partial<AggregatedAgent> = {}): AggregatedAgent {
requiresAttention: overrides.requiresAttention ?? false,
attentionReason: overrides.attentionReason ?? null,
attentionTimestamp: overrides.attentionTimestamp ?? null,
parentAgentId: overrides.parentAgentId ?? null,
} as AggregatedAgent;
}

View File

@@ -1162,12 +1162,8 @@ const TOOL_NAME_MAP: Record<string, string> = {
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",
};

View File

@@ -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**.

View File

@@ -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 <key=value>', '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 <host>', 'Daemon host:port (default: localhost:6767)')
.action((options, command) => {
@@ -62,6 +64,8 @@ export function createCli(): Command {
.option('--base <branch>', 'Base branch for worktree (default: current branch)')
.option('--image <path>', 'Attach image(s) to the initial prompt (can be used multiple times)', collectMultiple, [])
.option('--cwd <path>', 'Working directory (default: current)')
.option('--label <key=value>', '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 <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runRunCommand))

View File

@@ -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 <key=value>', '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 <host>', 'Daemon host:port (default: localhost:6767)')
.action((options, command) => {
@@ -39,6 +46,8 @@ export function createAgentCommand(): Command {
.option('--model <model>', 'Model to use (e.g., claude-sonnet-4-20250514, claude-3-5-haiku-20241022)')
.option('--mode <mode>', 'Provider-specific mode (e.g., plan, default, bypass)')
.option('--cwd <path>', 'Working directory (default: current)')
.option('--label <key=value>', '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 <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runRunCommand))

View File

@@ -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
}

View File

@@ -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<string, string> = {}
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<string, string> | 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<string, string> | undefined
return !agentLabels || agentLabels['ui'] !== 'true'
})
}
await client.close()
// Sort agents: running first, then idle, then others; within each group, most recent first

View File

@@ -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<string, string> = {}
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()

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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;
});

View File

@@ -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<string, unknown> = {};
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");

View File

@@ -163,6 +163,7 @@ export type CreateAgentRequestOptions = {
git?: GitSetupOptions;
worktreeName?: string;
requestId?: string;
labels?: Record<string, string>;
} & 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;

View File

@@ -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<string, string>;
};
type ManagedAgentWithSession = ManagedAgentBase & {
@@ -189,7 +190,6 @@ export class AgentManager {
private readonly idFactory: () => string;
private readonly registry?: AgentStorage;
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
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<string, string> }
): Promise<ManagedAgent> {
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<AgentSessionConfig>,
agentId?: string,
timestamps?: { createdAt?: Date; updatedAt?: Date; lastUserMessageAt?: Date | null }
options?: {
createdAt?: Date;
updatedAt?: Date;
lastUserMessageAt?: Date | null;
labels?: Record<string, string>;
}
): Promise<ManagedAgent> {
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
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<string, string>;
}
): Promise<ManagedAgent> {
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<string, string> }
): Promise<AgentSessionConfig> {
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,
});

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,
});
});

View File

@@ -171,11 +171,6 @@ export type AgentCommandResult = {
usage?: AgentUsage;
};
export type AgentControlMcpConfig = {
url: string;
headers?: Record<string, string>;
};
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
* <paseo-instructions>...</paseo-instructions>.
@@ -215,7 +209,6 @@ export type AgentSessionConfig = {
claude?: Partial<ClaudeAgentOptions>;
};
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<void>;
close(): Promise<void>;
/**
* 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.

View File

@@ -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(),
});

View File

@@ -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) {

View File

@@ -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 () => {

View File

@@ -240,7 +240,6 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
provider: "claude",
cwd,
modeId: options?.modeId,
agentControlMcp: { url: agentMcpServer.url },
extra: {
claude: {
sandbox: { enabled: true, autoAllowBashIfSandboxed: false },

View File

@@ -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<Age
if (typeof metadata.reasoningEffort === "string") {
result.reasoningEffort = metadata.reasoningEffort;
}
if (isMetadata(metadata.agentControlMcp)) {
const url = metadata.agentControlMcp.url;
const headers = metadata.agentControlMcp.headers;
if (typeof url === "string") {
const agentControlMcp: AgentSessionConfig["agentControlMcp"] = { url };
if (isMetadata(headers)) {
const normalizedHeaders: { [key: string]: string } = {};
for (const [key, value] of Object.entries(headers)) {
if (typeof value === "string") {
normalizedHeaders[key] = value;
}
}
if (Object.keys(normalizedHeaders).length > 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<Age
if (isMetadata(metadata.mcpServers)) {
result.mcpServers = metadata.mcpServers;
}
if (typeof metadata.parentAgentId === "string") {
result.parentAgentId = metadata.parentAgentId;
}
return result;
}
@@ -432,7 +400,6 @@ class ClaudeAgentSession implements AgentSession {
private activeTurnPromise: Promise<void> | 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<AgentSlashCommand[]> {
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<string, ClaudeMcpServerConfig> = {
"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) {

View File

@@ -1859,11 +1859,6 @@ const PermissionParamsSchema = z
type PermissionParams = z.infer<typeof PermissionParamsSchema>;
const AgentControlMcpConfigSchema = z.object({
url: z.string(),
headers: z.record(z.string()).optional(),
});
type AgentSessionExtra = NonNullable<AgentSessionConfig["extra"]>;
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<string, CodexMcpServerConfig> = {};
// 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<string, unknown> | 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<string, PatchFileChange[]>();
private patchChangesByCallId = new Map<string, PatchFileChange[]>();
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<AgentSlashCommand[]> {
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_ },

View File

@@ -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<Record<AgentProvider, AgentClient>>;
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

View File

@@ -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: {},

View File

@@ -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,
};

View File

@@ -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<string>();
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<string, unknown>;
const direct = asObj.agentId;
if (typeof direct === "string") return direct;
const structured = asObj.structuredContent;
if (structured && typeof structured === "object") {
const nested = (structured as Record<string, unknown>).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);
});
});
});

View File

@@ -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<string, string>;
} {
return {
createdAt: new Date(record.createdAt),
updatedAt: new Date(record.lastActivityAt ?? record.updatedAt),
lastUserMessageAt: record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null,
labels: record.labels,
};
}

View File

@@ -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<string, string>;
};
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<typeof buildProviderRegistry>;
@@ -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<void> {
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<SessionInboundMessage, { type: "create_agent_request" }>
): Promise<void> {
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<string, string>
): Promise<{ sessionConfig: AgentSessionConfig; worktreeConfig?: WorktreeConfig }> {
let cwd = expandTilde(config.cwd);
const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);

View File

@@ -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",

View File

@@ -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<string, string>;
};
type WebSocketServerConfig = {
allowedOrigins: Set<string>;
};
@@ -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
);

View File

@@ -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<string, string>;
};
export class WebSocketSessionBridge {
private readonly logger: pino.Logger;
private readonly sessions: Map<WebSocket, Session> = 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,

View File

@@ -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(),
});