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:
@@ -48,7 +48,7 @@ The heart of Paseo. A Node.js process that:
|
||||
- Listens for WebSocket connections from clients
|
||||
- Manages agent lifecycle (create, run, stop, resume, archive)
|
||||
- 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
|
||||
|
||||
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/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/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/relay-transport.ts` | Outbound relay connection with E2E encryption |
|
||||
| `server/schedule/` | Cron-based scheduled agents |
|
||||
@@ -289,6 +290,8 @@ All providers:
|
||||
- Map tool calls to a normalized `ToolCallDetail` type
|
||||
- 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
|
||||
|
||||
1. Client sends `CreateAgentRequestMessage` with config (prompt, cwd, provider, model, mode)
|
||||
|
||||
@@ -438,7 +438,7 @@ Required fields for ACP providers:
|
||||
- `label`
|
||||
- `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
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
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,
|
||||
ImportProviderSessionInput,
|
||||
} from "./agent-sdk-types.js";
|
||||
import type { PaseoToolCatalog } from "./tools/types.js";
|
||||
import type { ProviderDefinition } from "./provider-registry.js";
|
||||
|
||||
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 () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
|
||||
@@ -64,6 +64,7 @@ import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
|
||||
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
|
||||
import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
|
||||
import { resolveCreateAgentTitles } from "./create-agent-title.js";
|
||||
import type { PaseoToolCatalogFactory } from "./tools/types.js";
|
||||
|
||||
const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000;
|
||||
const INTERRUPT_SESSION_TIMEOUT_MS = 2_000;
|
||||
@@ -202,6 +203,8 @@ export interface AgentManagerOptions {
|
||||
terminalManager?: TerminalManager | null;
|
||||
mcpBaseUrl?: string;
|
||||
mcpAuthToken?: string;
|
||||
paseoToolsEnabled?: boolean;
|
||||
paseoToolCatalogFactory?: PaseoToolCatalogFactory;
|
||||
appendSystemPrompt?: string;
|
||||
agentStreamCoalesceWindowMs?: number;
|
||||
rescueTimeouts?: AgentManagerRescueTimeouts;
|
||||
@@ -521,6 +524,8 @@ export class AgentManager {
|
||||
private readonly agentStreamCoalescer: AgentStreamCoalescer;
|
||||
private mcpBaseUrl: string | null;
|
||||
private readonly mcpAuthToken: string | null;
|
||||
private paseoToolsEnabled = true;
|
||||
private paseoToolCatalogFactory: PaseoToolCatalogFactory | null = null;
|
||||
private appendSystemPrompt: string;
|
||||
private onAgentAttention?: AgentAttentionCallback;
|
||||
private onAgentArchived?: AgentArchivedCallback;
|
||||
@@ -536,6 +541,7 @@ export class AgentManager {
|
||||
this.onWorkspaceStateMayHaveChanged = options?.onWorkspaceStateMayHaveChanged;
|
||||
this.mcpBaseUrl = options?.mcpBaseUrl ?? null;
|
||||
this.mcpAuthToken = options?.mcpAuthToken ?? null;
|
||||
this.configurePaseoTools(options);
|
||||
this.appendSystemPrompt = options.appendSystemPrompt ?? "";
|
||||
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
||||
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 {
|
||||
this.clients.set(provider, client);
|
||||
}
|
||||
@@ -594,6 +605,14 @@ export class AgentManager {
|
||||
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
|
||||
* 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 { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId);
|
||||
this.requireEnabledProvider(storedConfig.provider);
|
||||
const launchContext = this.buildLaunchContext(resolvedAgentId, options?.env);
|
||||
const client = await this.requireAvailableClient({
|
||||
provider: storedConfig.provider,
|
||||
});
|
||||
const launchContext = await this.buildLaunchContext(resolvedAgentId, client, options?.env);
|
||||
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
||||
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, {
|
||||
labels: options?.labels,
|
||||
initialTitle: options?.initialTitle,
|
||||
@@ -948,7 +968,6 @@ export class AgentManager {
|
||||
resolvedAgentId,
|
||||
);
|
||||
|
||||
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
||||
const client = this.requireClient(handle.provider);
|
||||
const available = await client.isAvailable();
|
||||
if (!available) {
|
||||
@@ -956,7 +975,9 @@ export class AgentManager {
|
||||
`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);
|
||||
}
|
||||
|
||||
@@ -982,13 +1003,14 @@ export class AgentManager {
|
||||
},
|
||||
resolvedAgentId,
|
||||
);
|
||||
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
||||
const launchContext = await this.buildLaunchContext(resolvedAgentId, client);
|
||||
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
||||
const imported = await client.importSession(
|
||||
{
|
||||
providerHandleId: input.providerHandleId,
|
||||
cwd: input.cwd,
|
||||
},
|
||||
{ config: launchConfig, storedConfig, launchContext },
|
||||
{ config: providerLaunchConfig, storedConfig, launchContext },
|
||||
);
|
||||
const importedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(imported.config));
|
||||
const timelineRows = buildImportedTimelineRows(imported.timeline);
|
||||
@@ -1036,11 +1058,12 @@ export class AgentManager {
|
||||
provider,
|
||||
} as AgentSessionConfig;
|
||||
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
|
||||
? await client.resumeSession(handle, launchConfig, launchContext)
|
||||
: await client.createSession(launchConfig, launchContext);
|
||||
? await client.resumeSession(handle, providerLaunchConfig, launchContext)
|
||||
: await client.createSession(providerLaunchConfig, launchContext);
|
||||
|
||||
this.agentStreamCoalescer.flushAndDiscard(agentId);
|
||||
// Remove the existing agent entry before swapping sessions
|
||||
@@ -3696,14 +3719,33 @@ export class AgentManager {
|
||||
: next;
|
||||
}
|
||||
|
||||
private buildLaunchContext(agentId: string, env?: Record<string, string>): AgentLaunchContext {
|
||||
return {
|
||||
private async buildLaunchContext(
|
||||
agentId: string,
|
||||
client: AgentClient,
|
||||
env?: Record<string, string>,
|
||||
): Promise<AgentLaunchContext> {
|
||||
const context: AgentLaunchContext = {
|
||||
agentId,
|
||||
env: {
|
||||
...env,
|
||||
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> {
|
||||
|
||||
@@ -89,6 +89,63 @@ async function createMcpClient(url: string, authToken?: string): Promise<McpClie
|
||||
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: {
|
||||
client: McpClient;
|
||||
agentId: string;
|
||||
@@ -256,6 +313,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
|
||||
const port = await getAvailablePort();
|
||||
const recorder: LaunchRecorder = { recordedLaunches: [] };
|
||||
|
||||
const daemonConfig: PaseoDaemonConfig = {
|
||||
listen: `127.0.0.1:${port}`,
|
||||
@@ -265,7 +323,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
mcpEnabled: true,
|
||||
staticDir,
|
||||
mcpDebug: false,
|
||||
agentClients: createTestAgentClients(),
|
||||
agentClients: createMcpRecordingAgentClients(recorder),
|
||||
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 disabledAgentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-disabled-"));
|
||||
const disabledPort = await getAvailablePort();
|
||||
const disabledRecorder: LaunchRecorder = { recordedLaunches: [] };
|
||||
const disabledDaemonConfig: PaseoDaemonConfig = {
|
||||
listen: `127.0.0.1:${disabledPort}`,
|
||||
paseoHome: disabledPaseoHome,
|
||||
@@ -287,7 +346,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
mcpInjectIntoAgents: false,
|
||||
staticDir: disabledStaticDir,
|
||||
mcpDebug: false,
|
||||
agentClients: createTestAgentClients(),
|
||||
agentClients: createMcpRecordingAgentClients(disabledRecorder),
|
||||
agentStoragePath: path.join(disabledPaseoHome, "agents"),
|
||||
};
|
||||
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;
|
||||
expect(agentId).toBeTruthy();
|
||||
|
||||
const injectedAgent = daemon.agentManager.getAgent(agentId!);
|
||||
expect(injectedAgent?.config.mcpServers).toMatchObject({
|
||||
expect(recorder.recordedLaunches.at(-1)?.mcpServers).toMatchObject({
|
||||
paseo: {
|
||||
type: "http",
|
||||
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({
|
||||
name: "create_agent",
|
||||
@@ -337,6 +397,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
typeof disabledPayload?.agentId === "string" ? disabledPayload.agentId : null;
|
||||
expect(disabledAgentId).toBeTruthy();
|
||||
|
||||
expect(disabledRecorder.recordedLaunches.at(-1)?.mcpServers?.paseo).toBeUndefined();
|
||||
const disabledAgent = disabledDaemon.agentManager.getAgent(disabledAgentId!);
|
||||
expect(disabledAgent?.config.mcpServers?.paseo).toBeUndefined();
|
||||
} finally {
|
||||
@@ -364,6 +425,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||
const agentCwd = await mkdtemp(path.join(os.tmpdir(), "paseo-agent-cwd-"));
|
||||
const port = await getAvailablePort();
|
||||
const recorder: LaunchRecorder = { recordedLaunches: [] };
|
||||
|
||||
const daemonConfig: PaseoDaemonConfig = {
|
||||
listen: `0.0.0.0:${port}`,
|
||||
@@ -373,7 +435,7 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
mcpEnabled: true,
|
||||
staticDir,
|
||||
mcpDebug: false,
|
||||
agentClients: createTestAgentClients(),
|
||||
agentClients: createMcpRecordingAgentClients(recorder),
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
};
|
||||
|
||||
@@ -399,13 +461,14 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||
expect(agentId).toBeTruthy();
|
||||
|
||||
const injectedAgent = daemon.agentManager.getAgent(agentId!);
|
||||
expect(injectedAgent?.config.mcpServers).toMatchObject({
|
||||
expect(recorder.recordedLaunches.at(-1)?.mcpServers).toMatchObject({
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: `http://127.0.0.1:${port}/mcp/agents?callerAgentId=${agentId!}`,
|
||||
},
|
||||
});
|
||||
const injectedAgent = daemon.agentManager.getAgent(agentId!);
|
||||
expect(injectedAgent?.config.mcpServers?.paseo).toBeUndefined();
|
||||
} finally {
|
||||
if (agentId) {
|
||||
await client.callTool({ name: "kill_agent", args: { agentId } });
|
||||
@@ -568,6 +631,23 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
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> {
|
||||
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`);
|
||||
|
||||
let agentId: string | null = null;
|
||||
try {
|
||||
const result = await client.callTool({
|
||||
name: "create_agent",
|
||||
@@ -618,14 +699,25 @@ describe("agent MCP end-to-end (offline)", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.isError).toBe(true);
|
||||
const contentItem = result.content?.[0];
|
||||
const contentText: string | undefined =
|
||||
contentItem != null && typeof contentItem === "object"
|
||||
? Reflect.get(contentItem, "text")
|
||||
: undefined;
|
||||
expect(contentText ?? "").toContain("Initial turn failed to start");
|
||||
const payload = getStructuredContent(result);
|
||||
agentId = typeof payload?.agentId === "string" ? payload.agentId : null;
|
||||
expect(agentId).toBeTruthy();
|
||||
|
||||
await waitForAgentCompletion({ client, agentId: agentId! });
|
||||
const statusResult = await client.callTool({
|
||||
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 {
|
||||
if (agentId) {
|
||||
await client.callTool({ name: "kill_agent", args: { agentId } });
|
||||
}
|
||||
await client.close();
|
||||
await daemon.stop();
|
||||
await rm(paseoHome, { recursive: true, force: true });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { AgentProviderNotice } from "@getpaseo/protocol/agent-types";
|
||||
import type { AgentAttachment } from "@getpaseo/protocol/messages";
|
||||
import type { PaseoToolCatalog } from "./tools/types.js";
|
||||
|
||||
export type { AgentProviderNotice };
|
||||
|
||||
@@ -169,6 +170,7 @@ export interface AgentCapabilityFlags {
|
||||
supportsSessionListing?: boolean;
|
||||
supportsDynamicModes: boolean;
|
||||
supportsMcpServers: boolean;
|
||||
supportsNativePaseoTools?: boolean;
|
||||
supportsReasoningStream: boolean;
|
||||
supportsToolInvocations: boolean;
|
||||
supportsRewindConversation?: boolean;
|
||||
@@ -577,6 +579,11 @@ export interface AgentSessionConfig {
|
||||
export interface AgentLaunchContext {
|
||||
agentId?: 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 {
|
||||
|
||||
@@ -722,7 +722,7 @@ describe("create_agent MCP tool", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires an explicit workspace", async () => {
|
||||
it("rejects partial explicit workspace shape", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
@@ -740,12 +740,10 @@ describe("create_agent MCP tool", () => {
|
||||
initialPrompt: "test",
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(
|
||||
parsed.error.issues.some(
|
||||
(issue: { path: Array<string | number> }) => issue.path[0] === "workspace",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(parsed.success).toBe(true);
|
||||
await expect(tool.handler(parsed.data)).rejects.toThrow(
|
||||
"relationship and workspace must be provided together",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
initialPrompt: "test",
|
||||
}),
|
||||
).rejects.toThrow("relationship subagent requires an agent-scoped MCP session");
|
||||
).rejects.toThrow("relationship subagent requires an agent-scoped tool session");
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
@@ -774,7 +772,7 @@ describe("create_agent MCP tool", () => {
|
||||
provider: "codex/gpt-5.4",
|
||||
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 () => {
|
||||
|
||||
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>;
|
||||
@@ -105,6 +105,11 @@ import { AgentManager } from "./agent/agent-manager.js";
|
||||
import { AgentStorage } from "./agent/agent-storage.js";
|
||||
import { attachAgentStoragePersistence } from "./persistence-hooks.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 { bootstrapWorkspaceRegistries } from "./workspace-registry-bootstrap.js";
|
||||
import { WorkspaceReconciliationService } from "./workspace-reconciliation-service.js";
|
||||
@@ -858,82 +863,96 @@ export async function createPaseoDaemon(
|
||||
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;
|
||||
let agentMcpBaseUrl: string | null = null;
|
||||
if (mcpEnabled) {
|
||||
const agentMcpRoute = "/mcp/agents";
|
||||
|
||||
const createAgentMcpSession = async (callerAgentId?: string) => {
|
||||
const agentMcpServer = await createAgentMcpServer({
|
||||
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: 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,
|
||||
});
|
||||
const agentMcpServer = await createAgentMcpServer(
|
||||
createAgentToolHostDependencies({ callerAgentId }),
|
||||
);
|
||||
|
||||
// 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
|
||||
@@ -1087,8 +1106,10 @@ export async function createPaseoDaemon(
|
||||
const mcpBaseUrl = mcpEnabled ? createAgentMcpBaseUrl(boundListenTarget) : null;
|
||||
agentMcpBaseUrl = config.mcpInjectIntoAgents === false ? null : mcpBaseUrl;
|
||||
agentManager.setMcpBaseUrl(agentMcpBaseUrl);
|
||||
agentManager.setPaseoToolsEnabled(config.mcpInjectIntoAgents !== false);
|
||||
daemonConfigStore.onFieldChange("mcp.injectIntoAgents", (value) => {
|
||||
agentManager.setMcpBaseUrl(value ? mcpBaseUrl : null);
|
||||
agentManager.setPaseoToolsEnabled(value !== false);
|
||||
});
|
||||
daemonConfigStore.onFieldChange("appendSystemPrompt", (value) => {
|
||||
agentManager.setAppendSystemPrompt(typeof value === "string" ? value : "");
|
||||
|
||||
@@ -3,7 +3,6 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { basename, normalize, resolve, sep } from "path";
|
||||
import { homedir } from "node:os";
|
||||
import type { ToolSet } from "ai";
|
||||
import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities";
|
||||
import {
|
||||
serializeAgentStreamEvent,
|
||||
@@ -49,8 +48,6 @@ import {
|
||||
resolveFirstAgentPromptTitle,
|
||||
} from "./agent/create-agent-title.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 { ScriptHealthState } from "./script-health-monitor.js";
|
||||
import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
|
||||
@@ -537,9 +534,6 @@ export class Session {
|
||||
private readonly paseoHome: string;
|
||||
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 readonly agentStorage: AgentStorage;
|
||||
private readonly projectRegistry: ProjectRegistry;
|
||||
@@ -552,7 +546,6 @@ export class Session {
|
||||
private readonly gitMutation: GitMutationService;
|
||||
private readonly workspaceProvisioning: WorkspaceProvisioningService;
|
||||
private readonly daemonConfigStore: DaemonConfigStore;
|
||||
private readonly mcpBaseUrl: string | null;
|
||||
private readonly pushTokenStore: PushTokenStore;
|
||||
private unsubscribeAgentEvents: (() => void) | null = null;
|
||||
private unsubscribeTerminalWorkspaceContributionEvents: (() => void) | null = null;
|
||||
@@ -619,7 +612,6 @@ export class Session {
|
||||
generateWorkspaceName,
|
||||
workspaceGitService,
|
||||
daemonConfigStore,
|
||||
mcpBaseUrl,
|
||||
stt,
|
||||
sttLanguage,
|
||||
tts,
|
||||
@@ -791,7 +783,6 @@ export class Session {
|
||||
logger: this.sessionLogger,
|
||||
});
|
||||
this.daemonConfigStore = daemonConfigStore;
|
||||
this.mcpBaseUrl = mcpBaseUrl ?? null;
|
||||
this.terminalManager = terminalManager;
|
||||
this.terminalController = new TerminalSessionController({
|
||||
terminalManager,
|
||||
@@ -908,8 +899,6 @@ export class Session {
|
||||
dictation,
|
||||
});
|
||||
|
||||
// Initialize agent MCP client asynchronously
|
||||
void this.initializeAgentMcp();
|
||||
this.subscribeToAgentEvents();
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -5667,17 +5625,6 @@ export class Session {
|
||||
|
||||
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.checkoutSession.cleanup();
|
||||
|
||||
@@ -37,6 +37,13 @@ const TEST_CAPABILITIES: AgentCapabilityFlags = {
|
||||
};
|
||||
|
||||
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> {
|
||||
promise: Promise<T>;
|
||||
@@ -798,13 +805,7 @@ class FakeAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
async getAvailableModes(): Promise<AgentMode[]> {
|
||||
return [
|
||||
{ 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" },
|
||||
];
|
||||
return TEST_MODES;
|
||||
}
|
||||
|
||||
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: "sonnet", label: "Sonnet", isDefault: false },
|
||||
],
|
||||
modes: [],
|
||||
modes: TEST_MODES,
|
||||
};
|
||||
}
|
||||
if (this.provider === "codex") {
|
||||
@@ -1207,12 +1208,12 @@ class FakeAgentClient implements AgentClient {
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
modes: [],
|
||||
modes: TEST_MODES,
|
||||
};
|
||||
}
|
||||
return {
|
||||
models: [{ provider: this.provider, id: "test-model", label: "Test Model", isDefault: true }],
|
||||
modes: [],
|
||||
modes: TEST_MODES,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user