Prepare providers for direct Paseo tools (#1707)

* refactor(agent): decouple Paseo tools from MCP

Keep MCP as the fallback adapter while exposing the same runtime catalog for providers that can register tools directly.

* fix(protocol): keep native tool support server-internal

* fix(i18n): sync Japanese shortcut labels

* fix(agent): preserve schedule provider error
This commit is contained in:
Mohamed Boudra
2026-06-25 13:20:51 +08:00
committed by GitHub
parent 53e39e6d23
commit eaecd8d0a7
14 changed files with 3237 additions and 2764 deletions
+5 -2
View File
@@ -48,7 +48,7 @@ The heart of Paseo. A Node.js process that:
- Listens for WebSocket connections from clients - Listens for WebSocket connections from clients
- Manages agent lifecycle (create, run, stop, resume, archive) - Manages agent lifecycle (create, run, stop, resume, archive)
- Streams agent output in real time via a timeline model - Streams agent output in real time via a timeline model
- Exposes an MCP server for agent-to-agent control - Provides agent-to-agent tools through a transport-neutral tool catalog, with MCP as one adapter
- Optionally connects outbound to a relay for remote access - Optionally connects outbound to a relay for remote access
All paths are under `packages/server/src/`. All paths are under `packages/server/src/`.
@@ -62,7 +62,8 @@ All paths are under `packages/server/src/`.
| `server/session.ts` | Per-client session state, timeline subscriptions, terminal operations | | `server/session.ts` | Per-client session state, timeline subscriptions, terminal operations |
| `server/agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | | `server/agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management |
| `server/agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | | `server/agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` |
| `server/agent/mcp-server.ts` | MCP server for sub-agent creation, permissions, timeouts | | `server/agent/tools/` | Transport-neutral Paseo tool catalog for subagents, permissions, worktrees |
| `server/agent/mcp-server.ts` | Thin MCP adapter that registers the Paseo tool catalog with the MCP SDK |
| `server/agent/providers/` | Provider adapters (see "Agent providers" below) | | `server/agent/providers/` | Provider adapters (see "Agent providers" below) |
| `server/relay-transport.ts` | Outbound relay connection with E2E encryption | | `server/relay-transport.ts` | Outbound relay connection with E2E encryption |
| `server/schedule/` | Cron-based scheduled agents | | `server/schedule/` | Cron-based scheduled agents |
@@ -289,6 +290,8 @@ All providers:
- Map tool calls to a normalized `ToolCallDetail` type - Map tool calls to a normalized `ToolCallDetail` type
- Expose provider-specific modes (plan, default, full-access) - Expose provider-specific modes (plan, default, full-access)
Providers that can accept native tool definitions should set `supportsNativePaseoTools` and read `launchContext.paseoTools`. The daemon then passes the shared Paseo tool catalog directly and removes the internal Paseo MCP server from that provider launch config. Providers that only support MCP continue to receive the same tools through the MCP fallback at `/mcp/agents`.
## Data flow: running an agent ## Data flow: running an agent
1. Client sends `CreateAgentRequestMessage` with config (prompt, cwd, provider, model, mode) 1. Client sends `CreateAgentRequestMessage` with config (prompt, cwd, provider, model, mode)
+1 -1
View File
@@ -438,7 +438,7 @@ Required fields for ACP providers:
- `label` - `label`
- `command` — the command to spawn the agent process (must support ACP over stdio) - `command` — the command to spawn the agent process (must support ACP over stdio)
By default, Paseo injects its internal MCP server into ACP providers so agents can use Paseo tools such as subagent creation. Some ACP adapters cannot create sessions when `mcpServers` is non-empty. Disable injected MCP for those providers with `params.supportsMcpServers: false`: Paseo tools such as subagent creation come from the shared internal tool catalog. ACP providers receive those tools through the MCP fallback by default because ACP exposes `mcpServers`, not Paseo's native tool catalog. Some ACP adapters cannot create sessions when `mcpServers` is non-empty. Disable injected MCP for those providers with `params.supportsMcpServers: false`:
```json ```json
{ {
+2
View File
@@ -18,6 +18,8 @@ Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types.
Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`), and `omp` (a Pi-compatible built-in backed by the Pi adapter). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct. Existing direct providers: `claude` (in `providers/claude/agent.ts`), `codex` (`codex-app-server-agent.ts`), `opencode` (`opencode-agent.ts`), `pi` (`providers/pi/agent.ts`), and `omp` (a Pi-compatible built-in backed by the Pi adapter). The dev-only `mock` provider (`mock-load-test-agent.ts`) is also direct.
Paseo tools are not implemented as MCP tools internally. They live in a shared tool catalog under `packages/server/src/server/agent/tools/`; MCP is only the fallback adapter. A provider that can register runtime tools directly should set `supportsNativePaseoTools: true` and consume `launchContext.paseoTools` in `createSession`/`resumeSession`. When native tools are present, `AgentManager` strips the internal Paseo MCP server from the provider launch config so the provider does not receive the same tools twice. Providers that only know MCP should keep `supportsMcpServers: true` and let the daemon inject `/mcp/agents`.
Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages. Pi is a process-backed provider. Paseo requires the user to have the `pi` binary installed and talks to it through `pi --mode rpc`; the server package does not embed Pi's SDK/runtime packages.
Paseo's per-agent and daemon-wide system prompts are passed to Pi with `--append-system-prompt`, so Pi keeps its default coding prompt while receiving Paseo's additional instructions. Paseo's per-agent and daemon-wide system prompts are passed to Pi with `--append-system-prompt`, so Pi keeps its default coding prompt while receiving Paseo's additional instructions.
@@ -33,6 +33,7 @@ import type {
AgentTimelineItem, AgentTimelineItem,
ImportProviderSessionInput, ImportProviderSessionInput,
} from "./agent-sdk-types.js"; } from "./agent-sdk-types.js";
import type { PaseoToolCatalog } from "./tools/types.js";
import type { ProviderDefinition } from "./provider-registry.js"; import type { ProviderDefinition } from "./provider-registry.js";
interface Deferred<T> { interface Deferred<T> {
@@ -1054,6 +1055,84 @@ test("createAgent injects paseo MCP server only into provider launch config", as
}); });
}); });
test("createAgent passes native Paseo tools through launch context without internal MCP", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents");
const storage = new AgentStorage(storagePath, logger);
const paseoTools: PaseoToolCatalog = {
tools: new Map(),
getTool: () => undefined,
executeTool: async () => {
throw new Error("No tools registered in test catalog");
},
};
class NativeToolsClient extends TestAgentClient {
override readonly capabilities = {
...TEST_CAPABILITIES,
supportsMcpServers: true,
supportsNativePaseoTools: true,
};
lastConfig: AgentSessionConfig | null = null;
lastLaunchContext: AgentLaunchContext | undefined;
override async createSession(
config: AgentSessionConfig,
launchContext?: AgentLaunchContext,
): Promise<AgentSession> {
this.lastConfig = config;
this.lastLaunchContext = launchContext;
return new TestAgentSession(config);
}
}
const client = new NativeToolsClient();
const manager = new AgentManager({
clients: {
codex: client,
},
registry: storage,
logger,
mcpBaseUrl: "http://127.0.0.1:6767/mcp/agents",
paseoToolCatalogFactory: () => paseoTools,
idFactory: () => "00000000-0000-4000-8000-000000000106",
});
const snapshot = await manager.createAgent({
provider: "codex",
cwd: workdir,
mcpServers: {
custom: {
type: "stdio",
command: "custom-mcp",
},
},
});
expect(client.lastLaunchContext?.paseoTools).toBe(paseoTools);
expect(client.lastConfig?.mcpServers).toEqual({
custom: {
type: "stdio",
command: "custom-mcp",
},
});
expect(snapshot.config.mcpServers).toEqual({
custom: {
type: "stdio",
command: "custom-mcp",
},
});
const stored = await storage.get(snapshot.id);
expect(stored?.config?.mcpServers).toEqual({
custom: {
type: "stdio",
command: "custom-mcp",
},
});
});
test("createAgent injects the MCP auth token as a bearer header into the launch config", async () => { test("createAgent injects the MCP auth token as a bearer header into the launch config", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
const storagePath = join(workdir, "agents"); const storagePath = join(workdir, "agents");
@@ -64,6 +64,7 @@ import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
import { isSystemInjectedEnvelope } from "./agent-prompt.js"; import { isSystemInjectedEnvelope } from "./agent-prompt.js";
import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js"; import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
import { resolveCreateAgentTitles } from "./create-agent-title.js"; import { resolveCreateAgentTitles } from "./create-agent-title.js";
import type { PaseoToolCatalogFactory } from "./tools/types.js";
const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000; const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000;
const INTERRUPT_SESSION_TIMEOUT_MS = 2_000; const INTERRUPT_SESSION_TIMEOUT_MS = 2_000;
@@ -202,6 +203,8 @@ export interface AgentManagerOptions {
terminalManager?: TerminalManager | null; terminalManager?: TerminalManager | null;
mcpBaseUrl?: string; mcpBaseUrl?: string;
mcpAuthToken?: string; mcpAuthToken?: string;
paseoToolsEnabled?: boolean;
paseoToolCatalogFactory?: PaseoToolCatalogFactory;
appendSystemPrompt?: string; appendSystemPrompt?: string;
agentStreamCoalesceWindowMs?: number; agentStreamCoalesceWindowMs?: number;
rescueTimeouts?: AgentManagerRescueTimeouts; rescueTimeouts?: AgentManagerRescueTimeouts;
@@ -521,6 +524,8 @@ export class AgentManager {
private readonly agentStreamCoalescer: AgentStreamCoalescer; private readonly agentStreamCoalescer: AgentStreamCoalescer;
private mcpBaseUrl: string | null; private mcpBaseUrl: string | null;
private readonly mcpAuthToken: string | null; private readonly mcpAuthToken: string | null;
private paseoToolsEnabled = true;
private paseoToolCatalogFactory: PaseoToolCatalogFactory | null = null;
private appendSystemPrompt: string; private appendSystemPrompt: string;
private onAgentAttention?: AgentAttentionCallback; private onAgentAttention?: AgentAttentionCallback;
private onAgentArchived?: AgentArchivedCallback; private onAgentArchived?: AgentArchivedCallback;
@@ -536,6 +541,7 @@ export class AgentManager {
this.onWorkspaceStateMayHaveChanged = options?.onWorkspaceStateMayHaveChanged; this.onWorkspaceStateMayHaveChanged = options?.onWorkspaceStateMayHaveChanged;
this.mcpBaseUrl = options?.mcpBaseUrl ?? null; this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
this.mcpAuthToken = options?.mcpAuthToken ?? null; this.mcpAuthToken = options?.mcpAuthToken ?? null;
this.configurePaseoTools(options);
this.appendSystemPrompt = options.appendSystemPrompt ?? ""; this.appendSystemPrompt = options.appendSystemPrompt ?? "";
this.logger = options.logger.child({ module: "agent", component: "agent-manager" }); this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
this.rescueTimeouts = { this.rescueTimeouts = {
@@ -558,6 +564,11 @@ export class AgentManager {
}); });
} }
private configurePaseoTools(options: AgentManagerOptions): void {
this.paseoToolsEnabled = options.paseoToolsEnabled ?? true;
this.paseoToolCatalogFactory = options.paseoToolCatalogFactory ?? null;
}
registerClient(provider: AgentProvider, client: AgentClient): void { registerClient(provider: AgentProvider, client: AgentClient): void {
this.clients.set(provider, client); this.clients.set(provider, client);
} }
@@ -594,6 +605,14 @@ export class AgentManager {
this.mcpBaseUrl = url; this.mcpBaseUrl = url;
} }
setPaseoToolsEnabled(enabled: boolean): void {
this.paseoToolsEnabled = enabled;
}
setPaseoToolCatalogFactory(factory: PaseoToolCatalogFactory | null): void {
this.paseoToolCatalogFactory = factory;
}
/** /**
* Capability token the daemon's own MCP clients must present to the Agent MCP * Capability token the daemon's own MCP clients must present to the Agent MCP
* endpoint when a daemon password is configured. Read by the per-client * endpoint when a daemon password is configured. Read by the per-client
@@ -898,12 +917,13 @@ export class AgentManager {
const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent"); const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent");
const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId); const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId);
this.requireEnabledProvider(storedConfig.provider); this.requireEnabledProvider(storedConfig.provider);
const launchContext = this.buildLaunchContext(resolvedAgentId, options?.env);
const client = await this.requireAvailableClient({ const client = await this.requireAvailableClient({
provider: storedConfig.provider, provider: storedConfig.provider,
}); });
const launchContext = await this.buildLaunchContext(resolvedAgentId, client, options?.env);
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
const createOptions = this.buildCreateSessionOptions(options); const createOptions = this.buildCreateSessionOptions(options);
const session = await client.createSession(launchConfig, launchContext, createOptions); const session = await client.createSession(providerLaunchConfig, launchContext, createOptions);
return this.registerSession(session, storedConfig, resolvedAgentId, { return this.registerSession(session, storedConfig, resolvedAgentId, {
labels: options?.labels, labels: options?.labels,
initialTitle: options?.initialTitle, initialTitle: options?.initialTitle,
@@ -948,7 +968,6 @@ export class AgentManager {
resolvedAgentId, resolvedAgentId,
); );
const launchContext = this.buildLaunchContext(resolvedAgentId);
const client = this.requireClient(handle.provider); const client = this.requireClient(handle.provider);
const available = await client.isAvailable(); const available = await client.isAvailable();
if (!available) { if (!available) {
@@ -956,7 +975,9 @@ export class AgentManager {
`Provider '${handle.provider}' is not available. Please ensure the CLI is installed.`, `Provider '${handle.provider}' is not available. Please ensure the CLI is installed.`,
); );
} }
const session = await client.resumeSession(handle, launchConfig, launchContext); const launchContext = await this.buildLaunchContext(resolvedAgentId, client);
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
const session = await client.resumeSession(handle, providerLaunchConfig, launchContext);
return this.registerSession(session, storedConfig, resolvedAgentId, options); return this.registerSession(session, storedConfig, resolvedAgentId, options);
} }
@@ -982,13 +1003,14 @@ export class AgentManager {
}, },
resolvedAgentId, resolvedAgentId,
); );
const launchContext = this.buildLaunchContext(resolvedAgentId); const launchContext = await this.buildLaunchContext(resolvedAgentId, client);
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
const imported = await client.importSession( const imported = await client.importSession(
{ {
providerHandleId: input.providerHandleId, providerHandleId: input.providerHandleId,
cwd: input.cwd, cwd: input.cwd,
}, },
{ config: launchConfig, storedConfig, launchContext }, { config: providerLaunchConfig, storedConfig, launchContext },
); );
const importedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(imported.config)); const importedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(imported.config));
const timelineRows = buildImportedTimelineRows(imported.timeline); const timelineRows = buildImportedTimelineRows(imported.timeline);
@@ -1036,11 +1058,12 @@ export class AgentManager {
provider, provider,
} as AgentSessionConfig; } as AgentSessionConfig;
const { storedConfig, launchConfig } = await this.prepareSessionConfig(refreshConfig, agentId); const { storedConfig, launchConfig } = await this.prepareSessionConfig(refreshConfig, agentId);
const launchContext = this.buildLaunchContext(agentId); const launchContext = await this.buildLaunchContext(agentId, client);
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
const session = handle const session = handle
? await client.resumeSession(handle, launchConfig, launchContext) ? await client.resumeSession(handle, providerLaunchConfig, launchContext)
: await client.createSession(launchConfig, launchContext); : await client.createSession(providerLaunchConfig, launchContext);
this.agentStreamCoalescer.flushAndDiscard(agentId); this.agentStreamCoalescer.flushAndDiscard(agentId);
// Remove the existing agent entry before swapping sessions // Remove the existing agent entry before swapping sessions
@@ -3696,14 +3719,33 @@ export class AgentManager {
: next; : next;
} }
private buildLaunchContext(agentId: string, env?: Record<string, string>): AgentLaunchContext { private async buildLaunchContext(
return { agentId: string,
client: AgentClient,
env?: Record<string, string>,
): Promise<AgentLaunchContext> {
const context: AgentLaunchContext = {
agentId, agentId,
env: { env: {
...env, ...env,
PASEO_AGENT_ID: agentId, PASEO_AGENT_ID: agentId,
}, },
}; };
if (
this.paseoToolsEnabled &&
client.capabilities.supportsNativePaseoTools &&
this.paseoToolCatalogFactory
) {
context.paseoTools = await this.paseoToolCatalogFactory({ callerAgentId: agentId });
}
return context;
}
private resolveProviderLaunchConfig(
launchConfig: AgentSessionConfig,
launchContext: AgentLaunchContext,
): AgentSessionConfig {
return launchContext.paseoTools ? stripInternalPaseoMcpServer(launchConfig) : launchConfig;
} }
private async requireAvailableClient(options: { provider: AgentProvider }): Promise<AgentClient> { private async requireAvailableClient(options: { provider: AgentProvider }): Promise<AgentClient> {
@@ -89,6 +89,63 @@ async function createMcpClient(url: string, authToken?: string): Promise<McpClie
return { callTool: boundCallTool, close: () => rawClient.close() }; return { callTool: boundCallTool, close: () => rawClient.close() };
} }
interface LaunchRecorder {
recordedLaunches: AgentSessionConfig[];
}
class RecordingAgentClient implements AgentClient {
readonly provider: AgentClient["provider"];
readonly capabilities: AgentClient["capabilities"];
constructor(
private readonly inner: AgentClient,
private readonly recorder: LaunchRecorder,
) {
this.provider = inner.provider;
this.capabilities = {
...inner.capabilities,
supportsMcpServers: true,
supportsNativePaseoTools: false,
};
}
async createSession(
...args: Parameters<AgentClient["createSession"]>
): ReturnType<AgentClient["createSession"]> {
this.recorder.recordedLaunches.push(args[0]);
return this.inner.createSession(...args);
}
async resumeSession(
...args: Parameters<AgentClient["resumeSession"]>
): ReturnType<AgentClient["resumeSession"]> {
return this.inner.resumeSession(...args);
}
async fetchCatalog(
...args: Parameters<AgentClient["fetchCatalog"]>
): ReturnType<AgentClient["fetchCatalog"]> {
return this.inner.fetchCatalog(...args);
}
async isAvailable(): Promise<boolean> {
return this.inner.isAvailable();
}
}
function createMcpRecordingAgentClients(recorder: LaunchRecorder) {
const clients = createTestAgentClients();
const claude = clients.claude;
if (!claude) {
throw new Error("Fake Claude client is not configured");
}
return {
...clients,
claude: new RecordingAgentClient(claude, recorder),
};
}
async function waitForAgentCompletion(options: { async function waitForAgentCompletion(options: {
client: McpClient; client: McpClient;
agentId: string; agentId: string;
@@ -256,6 +313,7 @@ describe("agent MCP end-to-end (offline)", () => {
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-")); const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
const port = await getAvailablePort(); const port = await getAvailablePort();
const recorder: LaunchRecorder = { recordedLaunches: [] };
const daemonConfig: PaseoDaemonConfig = { const daemonConfig: PaseoDaemonConfig = {
listen: `127.0.0.1:${port}`, listen: `127.0.0.1:${port}`,
@@ -265,7 +323,7 @@ describe("agent MCP end-to-end (offline)", () => {
mcpEnabled: true, mcpEnabled: true,
staticDir, staticDir,
mcpDebug: false, mcpDebug: false,
agentClients: createTestAgentClients(), agentClients: createMcpRecordingAgentClients(recorder),
agentStoragePath: path.join(paseoHome, "agents"), agentStoragePath: path.join(paseoHome, "agents"),
}; };
@@ -278,6 +336,7 @@ describe("agent MCP end-to-end (offline)", () => {
const disabledStaticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-disabled-")); const disabledStaticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-disabled-"));
const disabledAgentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-disabled-")); const disabledAgentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-disabled-"));
const disabledPort = await getAvailablePort(); const disabledPort = await getAvailablePort();
const disabledRecorder: LaunchRecorder = { recordedLaunches: [] };
const disabledDaemonConfig: PaseoDaemonConfig = { const disabledDaemonConfig: PaseoDaemonConfig = {
listen: `127.0.0.1:${disabledPort}`, listen: `127.0.0.1:${disabledPort}`,
paseoHome: disabledPaseoHome, paseoHome: disabledPaseoHome,
@@ -287,7 +346,7 @@ describe("agent MCP end-to-end (offline)", () => {
mcpInjectIntoAgents: false, mcpInjectIntoAgents: false,
staticDir: disabledStaticDir, staticDir: disabledStaticDir,
mcpDebug: false, mcpDebug: false,
agentClients: createTestAgentClients(), agentClients: createMcpRecordingAgentClients(disabledRecorder),
agentStoragePath: path.join(disabledPaseoHome, "agents"), agentStoragePath: path.join(disabledPaseoHome, "agents"),
}; };
const disabledDaemon = await createPaseoDaemon(disabledDaemonConfig, pino({ level: "silent" })); const disabledDaemon = await createPaseoDaemon(disabledDaemonConfig, pino({ level: "silent" }));
@@ -313,13 +372,14 @@ describe("agent MCP end-to-end (offline)", () => {
agentId = typeof payload?.agentId === "string" ? payload.agentId : null; agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
expect(agentId).toBeTruthy(); expect(agentId).toBeTruthy();
const injectedAgent = daemon.agentManager.getAgent(agentId!); expect(recorder.recordedLaunches.at(-1)?.mcpServers).toMatchObject({
expect(injectedAgent?.config.mcpServers).toMatchObject({
paseo: { paseo: {
type: "http", type: "http",
url: `http://127.0.0.1:${port}/mcp/agents?callerAgentId=${agentId!}`, url: `http://127.0.0.1:${port}/mcp/agents?callerAgentId=${agentId!}`,
}, },
}); });
const injectedAgent = daemon.agentManager.getAgent(agentId!);
expect(injectedAgent?.config.mcpServers?.paseo).toBeUndefined();
const disabledResult = await disabledClient.callTool({ const disabledResult = await disabledClient.callTool({
name: "create_agent", name: "create_agent",
@@ -337,6 +397,7 @@ describe("agent MCP end-to-end (offline)", () => {
typeof disabledPayload?.agentId === "string" ? disabledPayload.agentId : null; typeof disabledPayload?.agentId === "string" ? disabledPayload.agentId : null;
expect(disabledAgentId).toBeTruthy(); expect(disabledAgentId).toBeTruthy();
expect(disabledRecorder.recordedLaunches.at(-1)?.mcpServers?.paseo).toBeUndefined();
const disabledAgent = disabledDaemon.agentManager.getAgent(disabledAgentId!); const disabledAgent = disabledDaemon.agentManager.getAgent(disabledAgentId!);
expect(disabledAgent?.config.mcpServers?.paseo).toBeUndefined(); expect(disabledAgent?.config.mcpServers?.paseo).toBeUndefined();
} finally { } finally {
@@ -364,6 +425,7 @@ describe("agent MCP end-to-end (offline)", () => {
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-")); const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
const port = await getAvailablePort(); const port = await getAvailablePort();
const recorder: LaunchRecorder = { recordedLaunches: [] };
const daemonConfig: PaseoDaemonConfig = { const daemonConfig: PaseoDaemonConfig = {
listen: `0.0.0.0:${port}`, listen: `0.0.0.0:${port}`,
@@ -373,7 +435,7 @@ describe("agent MCP end-to-end (offline)", () => {
mcpEnabled: true, mcpEnabled: true,
staticDir, staticDir,
mcpDebug: false, mcpDebug: false,
agentClients: createTestAgentClients(), agentClients: createMcpRecordingAgentClients(recorder),
agentStoragePath: path.join(paseoHome, "agents"), agentStoragePath: path.join(paseoHome, "agents"),
}; };
@@ -399,13 +461,14 @@ describe("agent MCP end-to-end (offline)", () => {
agentId = typeof payload?.agentId === "string" ? payload.agentId : null; agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
expect(agentId).toBeTruthy(); expect(agentId).toBeTruthy();
const injectedAgent = daemon.agentManager.getAgent(agentId!); expect(recorder.recordedLaunches.at(-1)?.mcpServers).toMatchObject({
expect(injectedAgent?.config.mcpServers).toMatchObject({
paseo: { paseo: {
type: "http", type: "http",
url: `http://127.0.0.1:${port}/mcp/agents?callerAgentId=${agentId!}`, url: `http://127.0.0.1:${port}/mcp/agents?callerAgentId=${agentId!}`,
}, },
}); });
const injectedAgent = daemon.agentManager.getAgent(agentId!);
expect(injectedAgent?.config.mcpServers?.paseo).toBeUndefined();
} finally { } finally {
if (agentId) { if (agentId) {
await client.callTool({ name: "kill_agent", args: { agentId } }); await client.callTool({ name: "kill_agent", args: { agentId } });
@@ -568,6 +631,23 @@ describe("agent MCP end-to-end (offline)", () => {
return true; return true;
} }
async fetchCatalog(): Promise<{
models: Array<{ provider: "codex"; id: string; label: string; isDefault: boolean }>;
modes: Array<{ id: string; label: string; description: string }>;
}> {
return {
models: [
{
provider: "codex",
id: "gpt-5.4-mini",
label: "gpt-5.4-mini",
isDefault: true,
},
],
modes: [{ id: "full-access", label: "Full access", description: "No prompts" }],
};
}
async createSession(_config: AgentSessionConfig): Promise<AgentSession> { async createSession(_config: AgentSessionConfig): Promise<AgentSession> {
return new StartTurnFailureSession(); return new StartTurnFailureSession();
} }
@@ -605,6 +685,7 @@ describe("agent MCP end-to-end (offline)", () => {
const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`); const client = await createMcpClient(`http://127.0.0.1:${port}/mcp/agents`);
let agentId: string | null = null;
try { try {
const result = await client.callTool({ const result = await client.callTool({
name: "create_agent", name: "create_agent",
@@ -618,14 +699,25 @@ describe("agent MCP end-to-end (offline)", () => {
}, },
}); });
expect(result.isError).toBe(true); const payload = getStructuredContent(result);
const contentItem = result.content?.[0]; agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
const contentText: string | undefined = expect(agentId).toBeTruthy();
contentItem != null && typeof contentItem === "object"
? Reflect.get(contentItem, "text") await waitForAgentCompletion({ client, agentId: agentId! });
: undefined; const statusResult = await client.callTool({
expect(contentText ?? "").toContain("Initial turn failed to start"); name: "get_agent_status",
args: { agentId },
});
const statusPayload = getStructuredContent(statusResult);
expect(statusPayload?.status).toBe("error");
const snapshot = statusPayload?.snapshot;
const lastError =
snapshot && typeof snapshot === "object" ? Reflect.get(snapshot, "lastError") : undefined;
expect(lastError).toContain("Initial turn failed to start");
} finally { } finally {
if (agentId) {
await client.callTool({ name: "kill_agent", args: { agentId } });
}
await client.close(); await client.close();
await daemon.stop(); await daemon.stop();
await rm(paseoHome, { recursive: true, force: true }); await rm(paseoHome, { recursive: true, force: true });
@@ -1,6 +1,7 @@
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk"; import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types"; import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
import type { AgentAttachment } from "@getpaseo/protocol/messages"; import type { AgentAttachment } from "@getpaseo/protocol/messages";
import type { PaseoToolCatalog } from "./tools/types.js";
export type { AgentProviderNotice }; export type { AgentProviderNotice };
@@ -169,6 +170,7 @@ export interface AgentCapabilityFlags {
supportsSessionListing?: boolean; supportsSessionListing?: boolean;
supportsDynamicModes: boolean; supportsDynamicModes: boolean;
supportsMcpServers: boolean; supportsMcpServers: boolean;
supportsNativePaseoTools?: boolean;
supportsReasoningStream: boolean; supportsReasoningStream: boolean;
supportsToolInvocations: boolean; supportsToolInvocations: boolean;
supportsRewindConversation?: boolean; supportsRewindConversation?: boolean;
@@ -577,6 +579,11 @@ export interface AgentSessionConfig {
export interface AgentLaunchContext { export interface AgentLaunchContext {
agentId?: string; agentId?: string;
env?: Record<string, string>; env?: Record<string, string>;
/**
* Runtime-only internal Paseo tools. This must never be persisted into
* AgentSessionConfig; providers may adapt it to their native tool surface.
*/
paseoTools?: PaseoToolCatalog;
} }
export interface AgentCreateSessionOptions { export interface AgentCreateSessionOptions {
@@ -722,7 +722,7 @@ describe("create_agent MCP tool", () => {
).toBe(true); ).toBe(true);
}); });
it("requires an explicit workspace", async () => { it("rejects partial explicit workspace shape", async () => {
const { agentManager, agentStorage } = createTestDeps(); const { agentManager, agentStorage } = createTestDeps();
const server = await createAgentMcpServer({ const server = await createAgentMcpServer({
agentManager, agentManager,
@@ -740,12 +740,10 @@ describe("create_agent MCP tool", () => {
initialPrompt: "test", initialPrompt: "test",
}); });
expect(parsed.success).toBe(false); expect(parsed.success).toBe(true);
expect( await expect(tool.handler(parsed.data)).rejects.toThrow(
parsed.error.issues.some( "relationship and workspace must be provided together",
(issue: { path: Array<string | number> }) => issue.path[0] === "workspace", );
),
).toBe(true);
}); });
it("rejects caller-only relationship and workspace intents without a caller agent", async () => { it("rejects caller-only relationship and workspace intents without a caller agent", async () => {
@@ -765,7 +763,7 @@ describe("create_agent MCP tool", () => {
provider: "codex/gpt-5.4", provider: "codex/gpt-5.4",
initialPrompt: "test", initialPrompt: "test",
}), }),
).rejects.toThrow("relationship subagent requires an agent-scoped MCP session"); ).rejects.toThrow("relationship subagent requires an agent-scoped tool session");
await expect( await expect(
tool.handler({ tool.handler({
@@ -774,7 +772,7 @@ describe("create_agent MCP tool", () => {
provider: "codex/gpt-5.4", provider: "codex/gpt-5.4",
initialPrompt: "test", initialPrompt: "test",
}), }),
).rejects.toThrow("workspace current requires an agent-scoped MCP session"); ).rejects.toThrow("workspace current requires an agent-scoped tool session");
}); });
it("requires a caller workspace for current workspace intent", async () => { it("requires a caller workspace for current workspace intent", async () => {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
import type { z } from "zod";
export interface PaseoToolExecutionContext {
signal?: AbortSignal;
}
export interface PaseoToolResult {
content: Array<{ type: string; text?: string; [key: string]: unknown }>;
structuredContent?: unknown;
isError?: boolean;
}
export interface PaseoToolConfig {
title?: string;
description?: string;
inputSchema?: z.ZodRawShape | z.ZodType;
outputSchema?: z.ZodRawShape | z.ZodType;
}
export interface PaseoToolDefinition extends PaseoToolConfig {
name: string;
description: string;
handler: (input: unknown, context: PaseoToolExecutionContext) => Promise<PaseoToolResult>;
}
export interface PaseoToolCatalog {
tools: ReadonlyMap<string, PaseoToolDefinition>;
getTool(name: string): PaseoToolDefinition | undefined;
executeTool(
name: string,
input: unknown,
context?: PaseoToolExecutionContext,
): Promise<PaseoToolResult>;
}
export interface PaseoToolRuntimeContext {
callerAgentId?: string;
enableVoiceTools?: boolean;
voiceOnly?: boolean;
}
export type PaseoToolCatalogFactory = (
context: PaseoToolRuntimeContext,
) => PaseoToolCatalog | Promise<PaseoToolCatalog>;
+91 -70
View File
@@ -105,6 +105,11 @@ import { AgentManager } from "./agent/agent-manager.js";
import { AgentStorage } from "./agent/agent-storage.js"; import { AgentStorage } from "./agent/agent-storage.js";
import { attachAgentStoragePersistence } from "./persistence-hooks.js"; import { attachAgentStoragePersistence } from "./persistence-hooks.js";
import { createAgentMcpServer } from "./agent/mcp-server.js"; import { createAgentMcpServer } from "./agent/mcp-server.js";
import {
createPaseoToolCatalog,
type PaseoToolHostDependencies,
} from "./agent/tools/paseo-tools.js";
import type { PaseoToolRuntimeContext } from "./agent/tools/types.js";
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js"; import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js"; import { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js"; import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
@@ -858,82 +863,96 @@ export async function createPaseoDaemon(
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal, emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
}); });
const createPaseoWorktreeForTools = async (
input: Parameters<typeof createPaseoWorktreeWorkflow>[1],
serviceOptions?: Parameters<typeof createPaseoWorktreeWorkflow>[2],
) => {
return createPaseoWorktreeWorkflow(
{
paseoHome: config.paseoHome,
worktreesRoot: config.worktreesRoot,
createPaseoWorktree: async (workflowInput, workflowOptions) => {
return createRegisteredPaseoWorktree(workflowInput, {
github,
...(workflowOptions?.resolveDefaultBranch
? {
resolveDefaultBranch: workflowOptions.resolveDefaultBranch,
}
: {}),
projectRegistry,
workspaceRegistry,
workspaceGitService,
});
},
warmWorkspaceGitData: async (workspace) => {
await Promise.all(
wsServer
?.listActiveSessions()
.map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? [],
);
},
emitWorkspaceUpdateForWorkspaceId: async (workspaceId) => {
await emitWorkspaceUpdatesExternal([workspaceId]);
},
cacheWorkspaceSetupSnapshot: () => {},
emit: emitExternalSessionMessage,
sessionLogger: logger,
terminalManager,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
serviceProxy,
scriptRuntimeStore,
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
getDaemonTcpHost: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.host : null),
serviceProxyPublicBaseUrl,
onScriptsChanged: null,
},
input,
serviceOptions,
);
};
const createAgentToolHostDependencies = (
runtime: PaseoToolRuntimeContext,
): PaseoToolHostDependencies => ({
agentManager,
agentStorage,
terminalManager,
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
scheduleService,
providerSnapshotManager,
github,
workspaceGitService,
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
listActiveWorkspaces: listActiveWorkspacesExternal,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
markWorkspaceArchiving: markWorkspaceArchivingExternal,
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
ensureWorkspaceForCreate: ensureWorkspaceForCreateExternal,
createPaseoWorktree: createPaseoWorktreeForTools,
paseoHome: config.paseoHome,
worktreesRoot: config.worktreesRoot,
callerAgentId: runtime.callerAgentId,
enableVoiceTools: runtime.enableVoiceTools,
voiceOnly: runtime.voiceOnly,
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null,
logger,
});
const createAgentToolCatalog = (runtime: PaseoToolRuntimeContext) =>
createPaseoToolCatalog(createAgentToolHostDependencies(runtime));
agentManager.setPaseoToolCatalogFactory(createAgentToolCatalog);
agentManager.setPaseoToolsEnabled(config.mcpInjectIntoAgents !== false);
const mcpEnabled = config.mcpEnabled ?? true; const mcpEnabled = config.mcpEnabled ?? true;
let agentMcpBaseUrl: string | null = null; let agentMcpBaseUrl: string | null = null;
if (mcpEnabled) { if (mcpEnabled) {
const agentMcpRoute = "/mcp/agents"; const agentMcpRoute = "/mcp/agents";
const createAgentMcpSession = async (callerAgentId?: string) => { const createAgentMcpSession = async (callerAgentId?: string) => {
const agentMcpServer = await createAgentMcpServer({ const agentMcpServer = await createAgentMcpServer(
agentManager, createAgentToolHostDependencies({ callerAgentId }),
agentStorage, );
terminalManager,
getDaemonTcpPort: () => (boundListenTarget?.type === "tcp" ? boundListenTarget.port : null),
scheduleService,
providerSnapshotManager,
github,
workspaceGitService,
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
listActiveWorkspaces: listActiveWorkspacesExternal,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
markWorkspaceArchiving: markWorkspaceArchivingExternal,
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
ensureWorkspaceForCreate: ensureWorkspaceForCreateExternal,
createPaseoWorktree: async (input, serviceOptions) => {
return createPaseoWorktreeWorkflow(
{
paseoHome: config.paseoHome,
worktreesRoot: config.worktreesRoot,
createPaseoWorktree: async (workflowInput, workflowOptions) => {
return createRegisteredPaseoWorktree(workflowInput, {
github,
...(workflowOptions?.resolveDefaultBranch
? {
resolveDefaultBranch: workflowOptions.resolveDefaultBranch,
}
: {}),
projectRegistry,
workspaceRegistry,
workspaceGitService,
});
},
warmWorkspaceGitData: async (workspace) => {
await Promise.all(
wsServer
?.listActiveSessions()
.map((session) => session.warmWorkspaceGitDataForWorkspace(workspace)) ?? [],
);
},
emitWorkspaceUpdateForWorkspaceId: async (workspaceId) => {
await emitWorkspaceUpdatesExternal([workspaceId]);
},
cacheWorkspaceSetupSnapshot: () => {},
emit: emitExternalSessionMessage,
sessionLogger: logger,
terminalManager,
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
serviceProxy,
scriptRuntimeStore,
getDaemonTcpPort: () =>
boundListenTarget?.type === "tcp" ? boundListenTarget.port : null,
getDaemonTcpHost: () =>
boundListenTarget?.type === "tcp" ? boundListenTarget.host : null,
serviceProxyPublicBaseUrl,
onScriptsChanged: null,
},
input,
serviceOptions,
);
},
paseoHome: config.paseoHome,
worktreesRoot: config.worktreesRoot,
callerAgentId,
enableVoiceTools: false,
resolveSpeakHandler: (agentId) => wsServer?.resolveVoiceSpeakHandler(agentId) ?? null,
resolveCallerContext: (agentId) => wsServer?.resolveVoiceCallerContext(agentId) ?? null,
logger,
});
// Stateless mode: each HTTP request builds a fresh server + transport that is // Stateless mode: each HTTP request builds a fresh server + transport that is
// torn down when the response closes, so no per-session state is retained between // torn down when the response closes, so no per-session state is retained between
@@ -1087,8 +1106,10 @@ export async function createPaseoDaemon(
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null; const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl; agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
agentManager.setMcpBaseUrl(agentMcpBaseUrl); agentManager.setMcpBaseUrl(agentMcpBaseUrl);
agentManager.setPaseoToolsEnabled(config.mcpInjectIntoAgents !== false);
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => { daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null); agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
agentManager.setPaseoToolsEnabled(value !== false);
}); });
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => { daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : ""); agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
-53
View File
@@ -3,7 +3,6 @@ import { v4 as uuidv4 } from "uuid";
import { stat } from "node:fs/promises"; import { stat } from "node:fs/promises";
import { basename, normalize, resolve, sep } from "path"; import { basename, normalize, resolve, sep } from "path";
import { homedir } from "node:os"; import { homedir } from "node:os";
import type { ToolSet } from "ai";
import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities"; import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities";
import { import {
serializeAgentStreamEvent, serializeAgentStreamEvent,
@@ -49,8 +48,6 @@ import {
resolveFirstAgentPromptTitle, resolveFirstAgentPromptTitle,
} from "./agent/create-agent-title.js"; } from "./agent/create-agent-title.js";
import { respondToAgentPermission } from "./agent/permission-response.js"; import { respondToAgentPermission } from "./agent/permission-response.js";
import { experimental_createMCPClient } from "ai";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js"; import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
import type { ScriptHealthState } from "./script-health-monitor.js"; import type { ScriptHealthState } from "./script-health-monitor.js";
import { spawnWorkspaceScript } from "./worktree-bootstrap.js"; import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
@@ -537,9 +534,6 @@ export class Session {
private readonly paseoHome: string; private readonly paseoHome: string;
private readonly worktreesRoot: string | undefined; private readonly worktreesRoot: string | undefined;
// Per-session MCP client and tools
private agentMcpClient: Awaited<ReturnType<typeof experimental_createMCPClient>> | null = null;
private agentTools: ToolSet | null = null;
private agentManager: AgentManager; private agentManager: AgentManager;
private readonly agentStorage: AgentStorage; private readonly agentStorage: AgentStorage;
private readonly projectRegistry: ProjectRegistry; private readonly projectRegistry: ProjectRegistry;
@@ -552,7 +546,6 @@ export class Session {
private readonly gitMutation: GitMutationService; private readonly gitMutation: GitMutationService;
private readonly workspaceProvisioning: WorkspaceProvisioningService; private readonly workspaceProvisioning: WorkspaceProvisioningService;
private readonly daemonConfigStore: DaemonConfigStore; private readonly daemonConfigStore: DaemonConfigStore;
private readonly mcpBaseUrl: string | null;
private readonly pushTokenStore: PushTokenStore; private readonly pushTokenStore: PushTokenStore;
private unsubscribeAgentEvents: (() => void) | null = null; private unsubscribeAgentEvents: (() => void) | null = null;
private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null; private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null;
@@ -619,7 +612,6 @@ export class Session {
generateWorkspaceName, generateWorkspaceName,
workspaceGitService, workspaceGitService,
daemonConfigStore, daemonConfigStore,
mcpBaseUrl,
stt, stt,
sttLanguage, sttLanguage,
tts, tts,
@@ -791,7 +783,6 @@ export class Session {
logger: this.sessionLogger, logger: this.sessionLogger,
}); });
this.daemonConfigStore = daemonConfigStore; this.daemonConfigStore = daemonConfigStore;
this.mcpBaseUrl = mcpBaseUrl ?? null;
this.terminalManager = terminalManager; this.terminalManager = terminalManager;
this.terminalController = new TerminalSessionController({ this.terminalController = new TerminalSessionController({
terminalManager, terminalManager,
@@ -908,8 +899,6 @@ export class Session {
dictation, dictation,
}); });
// Initialize agent MCP client asynchronously
void this.initializeAgentMcp();
this.subscribeToAgentEvents(); this.subscribeToAgentEvents();
this.sessionLogger.trace({}, "agent.session.lifecycle.created"); this.sessionLogger.trace({}, "agent.session.lifecycle.created");
@@ -1118,37 +1107,6 @@ export class Session {
}); });
} }
/**
* Initialize Agent MCP client for this session using the daemon's HTTP MCP endpoint.
*/
private async initializeAgentMcp(): Promise<void> {
try {
if (!this.mcpBaseUrl) {
this.sessionLogger.info(
"Skipping Agent MCP initialization because no MCP base URL is configured",
);
return;
}
const authToken = this.agentManager.getMcpAuthToken();
const transport = new StreamableHTTPClientTransport(
new URL(this.mcpBaseUrl),
authToken
? { requestInit: { headers: { Authorization: `Bearer ${authToken}` } } }
: undefined,
);
this.agentMcpClient = await experimental_createMCPClient({
transport,
});
this.agentTools = (await this.agentMcpClient.tools()) as ToolSet;
const agentToolCount = Object.keys(this.agentTools ?? {}).length;
this.sessionLogger.trace({ agentToolCount }, "agent.session.mcp_init");
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to initialize Agent MCP");
}
}
/** /**
* Subscribe to AgentManager events and forward them to the client * Subscribe to AgentManager events and forward them to the client
*/ */
@@ -5667,17 +5625,6 @@ export class Session {
await this.voiceSession.cleanup(); await this.voiceSession.cleanup();
// Close MCP clients
if (this.agentMcpClient) {
try {
await this.agentMcpClient.close();
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to close Agent MCP client");
}
this.agentMcpClient = null;
this.agentTools = null;
}
this.terminalController.dispose(); this.terminalController.dispose();
this.checkoutSession.cleanup(); this.checkoutSession.cleanup();
@@ -37,6 +37,13 @@ const TEST_CAPABILITIES: AgentCapabilityFlags = {
}; };
const TEST_FEATURE_ID = "test_feature"; const TEST_FEATURE_ID = "test_feature";
const TEST_MODES: AgentMode[] = [
{ id: "bypassPermissions", label: "Bypass", description: "No permissions" },
{ id: "default", label: "Default", description: "Ask for permissions" },
{ id: "full-access", label: "Full access", description: "No prompts" },
{ id: "auto", label: "Auto", description: "Ask/allow based on policy" },
{ id: "always-ask", label: "Always Ask", description: "Always prompt" },
];
interface Deferred<T> { interface Deferred<T> {
promise: Promise<T>; promise: Promise<T>;
@@ -798,13 +805,7 @@ class FakeAgentSession implements AgentSession {
} }
async getAvailableModes(): Promise<AgentMode[]> { async getAvailableModes(): Promise<AgentMode[]> {
return [ return TEST_MODES;
{ id: "bypassPermissions", label: "Bypass", description: "No permissions" },
{ id: "default", label: "Default", description: "Ask for permissions" },
{ id: "full-access", label: "Full access", description: "No prompts" },
{ id: "auto", label: "Auto", description: "Ask/allow based on policy" },
{ id: "always-ask", label: "Always Ask", description: "Always prompt" },
];
} }
async getCurrentMode(): Promise<string | null> { async getCurrentMode(): Promise<string | null> {
@@ -1194,7 +1195,7 @@ class FakeAgentClient implements AgentClient {
{ provider: this.provider, id: "haiku", label: "Haiku", isDefault: true }, { provider: this.provider, id: "haiku", label: "Haiku", isDefault: true },
{ provider: this.provider, id: "sonnet", label: "Sonnet", isDefault: false }, { provider: this.provider, id: "sonnet", label: "Sonnet", isDefault: false },
], ],
modes: [], modes: TEST_MODES,
}; };
} }
if (this.provider === "codex") { if (this.provider === "codex") {
@@ -1207,12 +1208,12 @@ class FakeAgentClient implements AgentClient {
isDefault: true, isDefault: true,
}, },
], ],
modes: [], modes: TEST_MODES,
}; };
} }
return { return {
models: [{ provider: this.provider, id: "test-model", label: "Test Model", isDefault: true }], models: [{ provider: this.provider, id: "test-model", label: "Test Model", isDefault: true }],
modes: [], modes: TEST_MODES,
}; };
} }