mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Clean up MCP agent orchestration inputs
Require relationship and workspace on create_agent, and reuse the same worktree target union for create_worktree. Agent-scoped prompt follow-ups now default to background finish notifications so callers can continue without polling.
This commit is contained in:
@@ -14,16 +14,23 @@ Each agent in `AgentManager` carries a `lastStatus` of `initializing`, `idle`, `
|
||||
|
||||
## Relationships
|
||||
|
||||
Agents can launch other agents via the agent-scoped `create_agent` MCP tool. Agent-scoped creation is always asynchronous. By default, the daemon stamps the created agent with a label `paseo.parent-agent-id` pointing back at the agent that created it. The client surfaces that as `agent.parentAgentId`.
|
||||
Agents can launch other agents via the agent-scoped `create_agent` MCP tool. Agent-scoped creation is always asynchronous. `relationship` and `workspace` are separate decisions:
|
||||
|
||||
Agent-scoped `create_agent` accepts `detached: true` for agents that should stand on their own. The daemon still uses the creating agent for cwd/config inheritance, but does not write `paseo.parent-agent-id`.
|
||||
- `relationship` decides whether the new agent belongs under the caller.
|
||||
- `workspace` decides where the new agent lives and whether a new workspace/worktree is created.
|
||||
|
||||
- **Subagents** — created with `detached: false` or omitted. They exist as part of the creating agent's work, appear in that agent's subagent track, and are archived with it.
|
||||
- **Detached agents** — created with `detached: true`. They take over as sibling/root agents (e.g. handoffs, fire-and-forget delegations), do not appear in the creating agent's subagent track, and are not archived with it.
|
||||
`relationship: { kind: "subagent" }` stamps the created agent with `paseo.parent-agent-id`, pointing back at the creating agent. The client surfaces that as `agent.parentAgentId`. This requires an agent-scoped MCP session.
|
||||
|
||||
`relationship: { kind: "detached" }` creates a sibling/root agent (e.g. handoffs, fire-and-forget delegations). The daemon may still use the creating agent for cwd/config inheritance, but it does not write `paseo.parent-agent-id`.
|
||||
|
||||
- **Subagents** — exist as part of the creating agent's work, appear in that agent's subagent track, and are archived with it.
|
||||
- **Detached agents** — stand on their own, do not appear in the creating agent's subagent track, and are not archived with it.
|
||||
|
||||
`workspace: { kind: "current" }` uses the caller's workspace and can optionally override the runtime cwd. It requires an agent-scoped MCP session. `workspace: { kind: "create", source: { kind: "directory" | "worktree", ... } }` creates a new workspace for the new agent; worktree creation goes through the Paseo worktree workflow and stamps the agent with that fresh workspace id.
|
||||
|
||||
Users can also detach an existing subagent from the subagents track. Detach removes the `paseo.parent-agent-id` label only: it does not stop, archive, move, or restart the agent. The agent keeps its current `cwd` and `workspaceId`, leaves the former parent's track, and behaves like a root agent for tab close, workspace activity, and future parent archive.
|
||||
|
||||
`notifyOnFinish` defaults to `true` for agent-scoped creation because most subagents are delegated work the creating agent needs to hear back from. Set it to `false` only for truly fire-and-forget agents.
|
||||
`notifyOnFinish` defaults to `true` for agent-scoped creation and background prompt follow-ups because most delegated work needs to report back to the creating agent. Set it to `false` only for truly fire-and-forget agents or prompts.
|
||||
|
||||
## Archive
|
||||
|
||||
@@ -106,11 +113,11 @@ $PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json
|
||||
|
||||
Each agent is a single JSON file. Fields relevant to this doc:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --------------------------------- | ------------- | ----------------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | Stable identifier |
|
||||
| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) |
|
||||
| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically by agent-scoped `create_agent` unless `detached: true` |
|
||||
| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` |
|
||||
| Field | Type | Meaning |
|
||||
| --------------------------------- | ------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `id` | `string` | Stable identifier |
|
||||
| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) |
|
||||
| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically by `create_agent` when `relationship.kind === "subagent"` |
|
||||
| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` |
|
||||
|
||||
See [`docs/data-model.md`](./data-model.md) for the full agent record.
|
||||
|
||||
@@ -51,7 +51,7 @@ Each agent is stored as a separate JSON file, grouped by project directory.
|
||||
| `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp |
|
||||
| `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp |
|
||||
| `title` | `string?` | User-visible title |
|
||||
| `labels` | `Record<string, string>` | Key-value labels (default `{}`). `paseo.parent-agent-id` set automatically when launched via the `create_agent` MCP tool — see [agent-lifecycle.md](./agent-lifecycle.md) |
|
||||
| `labels` | `Record<string, string>` | Key-value labels (default `{}`). `paseo.parent-agent-id` is set automatically for `create_agent` subagent relationships — see [agent-lifecycle.md](./agent-lifecycle.md) |
|
||||
| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` |
|
||||
| `lastModeId` | `string?` | Last active mode ID |
|
||||
| `config` | `SerializableConfig?` | Agent session configuration (see below) |
|
||||
|
||||
@@ -46,9 +46,7 @@ interface CreateAgentCommandDependencies {
|
||||
terminalManager?: TerminalManager | null;
|
||||
providerSnapshotManager: ProviderSnapshotManager;
|
||||
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
|
||||
// Mints a fresh workspace for a cwd and returns its id. Used when an agent is
|
||||
// created with no parent and no worktree: it owns a brand-new workspace rather
|
||||
// than being attributed to an existing same-cwd workspace by path.
|
||||
// Mints a fresh directory workspace for a cwd and returns its id.
|
||||
ensureWorkspaceForCreate?: (cwd: string) => Promise<string>;
|
||||
}
|
||||
|
||||
@@ -81,6 +79,7 @@ export interface CreateAgentFromMcpInput {
|
||||
title: string;
|
||||
initialPrompt: string;
|
||||
cwd?: string;
|
||||
workspaceId?: string;
|
||||
thinking?: string;
|
||||
features?: Record<string, unknown>;
|
||||
labels?: Record<string, string>;
|
||||
@@ -245,15 +244,15 @@ async function resolveMcpCreateAgent(
|
||||
initialPrompt: input.initialPrompt,
|
||||
});
|
||||
|
||||
// A child agent created in its parent's working tree belongs to the parent's
|
||||
// workspace. When a new worktree is created the child lives in that fresh
|
||||
// workspace, so it is stamped with the new worktree's workspaceId instead
|
||||
// (mirrors the session path) — keeping the agent discoverable by
|
||||
// workspaceId-scoped archive. With neither a parent nor a worktree, the agent
|
||||
// mints its own workspace; ownership is never resolved from cwd.
|
||||
// MCP callers resolve workspace ownership before this point. Worktree
|
||||
// creation wins because the new agent lives in the fresh worktree workspace.
|
||||
// Otherwise use the explicit workspace id, then the parent workspace for
|
||||
// direct internal callers. Ownership is never resolved from cwd.
|
||||
const workspaceId = setupContinuation
|
||||
? createdWorkspaceId
|
||||
: (parentAgent?.workspaceId ?? (await ensureWorkspaceForMcpCreate(dependencies, resolvedCwd)));
|
||||
: (input.workspaceId ??
|
||||
parentAgent?.workspaceId ??
|
||||
(await ensureWorkspaceForMcpCreate(dependencies, resolvedCwd)));
|
||||
|
||||
const { modeId: resolvedMode, featureValues: resolvedFeatures } =
|
||||
await dependencies.providerSnapshotManager.resolveCreateConfig({
|
||||
|
||||
@@ -201,20 +201,24 @@ async function makeCwd(prefix: string): Promise<string> {
|
||||
|
||||
async function createTopLevelAgent(args?: Partial<StructuredContent>): Promise<string> {
|
||||
const cwd = typeof args?.cwd === "string" ? args.cwd : await makeCwd("agent-cwd");
|
||||
const { cwd: _cwd, ...rest } = args ?? {};
|
||||
const payload = await callToolStructured(topLevelClient, "create_agent", {
|
||||
cwd,
|
||||
relationship: { kind: "detached" },
|
||||
workspace: { kind: "create", source: { kind: "directory", path: cwd } },
|
||||
title: "Parity agent",
|
||||
provider: "claude/claude-test-model",
|
||||
initialPrompt: "say done and stop",
|
||||
settings: { modeId: "bypassPermissions" },
|
||||
background: true,
|
||||
...args,
|
||||
...rest,
|
||||
});
|
||||
return str(payload.agentId);
|
||||
}
|
||||
|
||||
async function createChildAgent(args?: Partial<StructuredContent>): Promise<string> {
|
||||
const payload = await callToolStructured(agentScopedClient, "create_agent", {
|
||||
relationship: { kind: "subagent" },
|
||||
workspace: { kind: "current" },
|
||||
title: "Parity child",
|
||||
provider: "claude/claude-test-model",
|
||||
initialPrompt: "say done and stop",
|
||||
@@ -288,7 +292,8 @@ beforeAll(async () => {
|
||||
topLevelClient = await createMcpClient(`http://127.0.0.1:${daemonHandle.port}/mcp/agents`);
|
||||
|
||||
const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
|
||||
cwd: parentAgentCwd,
|
||||
relationship: { kind: "detached" },
|
||||
workspace: { kind: "create", source: { kind: "directory", path: parentAgentCwd } },
|
||||
title: "MCP parity parent",
|
||||
provider: "claude/claude-test-model",
|
||||
initialPrompt: "say done and stop",
|
||||
@@ -338,10 +343,10 @@ describe("Suite A: Core Fixes", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("create_agent with detached true omits the parent agent label", async () => {
|
||||
test("create_agent with detached relationship omits the parent agent label", async () => {
|
||||
let agentId: string | null = null;
|
||||
try {
|
||||
agentId = await createChildAgent({ detached: true });
|
||||
agentId = await createChildAgent({ relationship: { kind: "detached" } });
|
||||
const snapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
|
||||
expect(snapshot?.labels?.[PARENT_AGENT_ID_LABEL]).toBeUndefined();
|
||||
} finally {
|
||||
@@ -836,9 +841,9 @@ describe("Suite E: Worktree Tools", () => {
|
||||
const created = await callToolStructured(topLevelClient, "create_worktree", {
|
||||
cwd: worktreeRepoCwd,
|
||||
target: {
|
||||
mode: "branch-off",
|
||||
newBranch: branchName,
|
||||
base: "main",
|
||||
kind: "branch-off",
|
||||
worktreeSlug: branchName,
|
||||
baseBranch: "main",
|
||||
},
|
||||
});
|
||||
worktreePath = str(created.worktreePath);
|
||||
@@ -867,9 +872,9 @@ describe("Suite E: Worktree Tools", () => {
|
||||
const created = await callToolStructured(topLevelClient, "create_worktree", {
|
||||
cwd: worktreeRepoCwd,
|
||||
target: {
|
||||
mode: "branch-off",
|
||||
newBranch: branchName,
|
||||
base: "main",
|
||||
kind: "branch-off",
|
||||
worktreeSlug: branchName,
|
||||
baseBranch: "main",
|
||||
},
|
||||
});
|
||||
worktreePath = str(created.worktreePath);
|
||||
@@ -900,9 +905,9 @@ describe("Suite E: Worktree Tools", () => {
|
||||
const created = await callToolStructured(topLevelClient, "create_worktree", {
|
||||
cwd: worktreeRepoCwd,
|
||||
target: {
|
||||
mode: "branch-off",
|
||||
newBranch: branchName,
|
||||
base: "main",
|
||||
kind: "branch-off",
|
||||
worktreeSlug: branchName,
|
||||
baseBranch: "main",
|
||||
},
|
||||
});
|
||||
worktreePath = str(created.worktreePath);
|
||||
|
||||
@@ -629,6 +629,29 @@ describe("terminal MCP tools", () => {
|
||||
describe("create_agent MCP tool", () => {
|
||||
const logger = createTestLogger();
|
||||
const existingCwd = process.cwd();
|
||||
const detachedDirectoryWorkspace = (path = existingCwd) => ({
|
||||
relationship: { kind: "detached" as const },
|
||||
workspace: { kind: "create" as const, source: { kind: "directory" as const, path } },
|
||||
});
|
||||
const detachedWorktreeWorkspace = (
|
||||
cwd: string,
|
||||
target:
|
||||
| { kind: "branch-off"; worktreeSlug?: string; baseBranch?: string }
|
||||
| { kind: "checkout-branch"; branch: string }
|
||||
| { kind: "checkout-pr"; githubPrNumber: number },
|
||||
) => ({
|
||||
relationship: { kind: "detached" as const },
|
||||
workspace: { kind: "create" as const, source: { kind: "worktree" as const, cwd, target } },
|
||||
});
|
||||
const subagentCurrentWorkspace = (cwd?: string) => ({
|
||||
relationship: { kind: "subagent" as const },
|
||||
workspace: { kind: "current" as const, ...(cwd ? { cwd } : {}) },
|
||||
});
|
||||
const detachedCurrentWorkspace = (cwd?: string) => ({
|
||||
relationship: { kind: "detached" as const },
|
||||
workspace: { kind: "current" as const, ...(cwd ? { cwd } : {}) },
|
||||
});
|
||||
const ensureWorkspaceForCreate = async () => "workspace-created";
|
||||
|
||||
it("requires a concise title no longer than 60 characters", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
@@ -636,13 +659,14 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const missingTitle = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "test",
|
||||
@@ -651,7 +675,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(missingTitle.error.issues[0].path).toEqual(["title"]);
|
||||
|
||||
const tooLong = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
provider: "codex/gpt-5.4",
|
||||
title: "x".repeat(61),
|
||||
@@ -661,7 +685,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(tooLong.error.issues[0].path).toEqual(["title"]);
|
||||
|
||||
const ok = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
provider: "codex/gpt-5.4",
|
||||
title: "Short title",
|
||||
@@ -676,11 +700,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
provider: "codex/gpt-5.4",
|
||||
title: "Short title",
|
||||
@@ -693,6 +718,88 @@ describe("create_agent MCP tool", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires an explicit workspace", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
relationship: { kind: "detached" },
|
||||
title: "Short title",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "test",
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
expect(
|
||||
parsed.error.issues.some(
|
||||
(issue: { path: Array<string | number> }) => issue.path[0] === "workspace",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects caller-only relationship and workspace intents without a caller agent", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Short title",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "test",
|
||||
}),
|
||||
).rejects.toThrow("relationship subagent requires an agent-scoped MCP session");
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
...detachedCurrentWorkspace(),
|
||||
title: "Short title",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "test",
|
||||
}),
|
||||
).rejects.toThrow("workspace current requires an agent-scoped MCP session");
|
||||
});
|
||||
|
||||
it("requires a caller workspace for current workspace intent", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent);
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
callerAgentId: "parent-agent",
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
}),
|
||||
).rejects.toThrow("Caller agent parent-agent has no current workspace");
|
||||
});
|
||||
|
||||
it("accepts provider features and passes them through createAgent", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
spies.agentManager.createAgent.mockResolvedValue({
|
||||
@@ -708,11 +815,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
const input = {
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Feature test",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -732,7 +840,7 @@ describe("create_agent MCP tool", () => {
|
||||
featureValues: { fast_mode: true },
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
{ workspaceId: "workspace-created" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -760,11 +868,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
const response = await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Mode test",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -780,12 +889,13 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
const missingProvider = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
title: "Short title",
|
||||
initialPrompt: "test",
|
||||
@@ -798,7 +908,7 @@ describe("create_agent MCP tool", () => {
|
||||
).toBe(true);
|
||||
|
||||
const providerWithoutModel = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
title: "Short title",
|
||||
provider: "codex",
|
||||
@@ -807,7 +917,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(providerWithoutModel.success).toBe(false);
|
||||
|
||||
const providerWithEmptyModel = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
title: "Short title",
|
||||
provider: "codex/",
|
||||
@@ -816,7 +926,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(providerWithEmptyModel.success).toBe(false);
|
||||
|
||||
const providerWithEmptyProvider = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
title: "Short title",
|
||||
provider: "/gpt-5.4",
|
||||
@@ -826,7 +936,7 @@ describe("create_agent MCP tool", () => {
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
settings: { modeId: "default" },
|
||||
title: "Short title",
|
||||
provider: "codex/gpt-5.4",
|
||||
@@ -836,7 +946,7 @@ describe("create_agent MCP tool", () => {
|
||||
).rejects.toThrow("Unrecognized key");
|
||||
});
|
||||
|
||||
it("accepts optional worktree intent fields in create_agent input validation", async () => {
|
||||
it("accepts worktree workspace intent in create_agent input validation", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
@@ -847,20 +957,19 @@ describe("create_agent MCP tool", () => {
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedWorktreeWorkspace(existingCwd, {
|
||||
kind: "checkout-pr",
|
||||
githubPrNumber: 42,
|
||||
}),
|
||||
title: "Short title",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "test",
|
||||
worktreeName: "review-42",
|
||||
action: "checkout",
|
||||
refName: "head-ref",
|
||||
githubPrNumber: 42,
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts each create_worktree target mode", async () => {
|
||||
it("accepts each create_worktree target kind", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
@@ -871,9 +980,10 @@ describe("create_agent MCP tool", () => {
|
||||
const tool = registeredTool(server, "create_worktree");
|
||||
|
||||
for (const target of [
|
||||
{ mode: "branch-off", newBranch: "feature-x", base: "main" },
|
||||
{ mode: "checkout-branch", branch: "head-ref" },
|
||||
{ mode: "checkout-pr", prNumber: 42 },
|
||||
{ kind: "branch-off", worktreeSlug: "feature-x", baseBranch: "main" },
|
||||
{ kind: "branch-off" },
|
||||
{ kind: "checkout-branch", branch: "head-ref" },
|
||||
{ kind: "checkout-pr", githubPrNumber: 42 },
|
||||
] as const) {
|
||||
const parsed = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, target });
|
||||
expect(parsed.success).toBe(true);
|
||||
@@ -903,13 +1013,14 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
cwd: "/path/that/does/not/exist",
|
||||
...detachedDirectoryWorkspace("/path/that/does/not/exist"),
|
||||
title: "Short title",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -932,11 +1043,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: " Fix auth bug ",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -948,7 +1060,7 @@ describe("create_agent MCP tool", () => {
|
||||
title: "Fix auth bug",
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
{ workspaceId: "workspace-created" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -967,11 +1079,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: " Fix auth ",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -982,7 +1095,7 @@ describe("create_agent MCP tool", () => {
|
||||
title: "Fix auth",
|
||||
}),
|
||||
undefined,
|
||||
undefined,
|
||||
{ workspaceId: "workspace-created" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1001,11 +1114,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Config test",
|
||||
initialPrompt: "Do work",
|
||||
provider: "codex/gpt-5.4",
|
||||
@@ -1022,7 +1136,7 @@ describe("create_agent MCP tool", () => {
|
||||
thinkingOptionId: "think-hard",
|
||||
}),
|
||||
undefined,
|
||||
{ labels: { source: "mcp" } },
|
||||
{ labels: { source: "mcp" }, workspaceId: "workspace-created" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1077,12 +1191,14 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: repoDir,
|
||||
...detachedWorktreeWorkspace(repoDir, {
|
||||
kind: "branch-off",
|
||||
worktreeSlug: "agent-worktree",
|
||||
baseBranch: "main",
|
||||
}),
|
||||
title: "Worktree agent",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
worktreeName: "agent-worktree",
|
||||
baseBranch: "main",
|
||||
background: true,
|
||||
});
|
||||
|
||||
@@ -1156,12 +1272,13 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: repoDir,
|
||||
...detachedWorktreeWorkspace(repoDir, {
|
||||
kind: "branch-off",
|
||||
baseBranch: "main",
|
||||
}),
|
||||
title: "Worktree agent",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Fix workspace creation naming",
|
||||
action: "branch-off",
|
||||
baseBranch: "main",
|
||||
background: true,
|
||||
});
|
||||
|
||||
@@ -1241,12 +1358,13 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: repoDir,
|
||||
...detachedWorktreeWorkspace(repoDir, {
|
||||
kind: "checkout-branch",
|
||||
branch: "existing-feature",
|
||||
}),
|
||||
title: "Checkout agent",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Rename this checkout from the prompt",
|
||||
action: "checkout",
|
||||
refName: "existing-feature",
|
||||
background: true,
|
||||
});
|
||||
|
||||
@@ -1333,11 +1451,13 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: REPO_CWD,
|
||||
...detachedWorktreeWorkspace(REPO_CWD, {
|
||||
kind: "checkout-pr",
|
||||
githubPrNumber: 123,
|
||||
}),
|
||||
title: "PR agent",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Rename this PR branch from prompt",
|
||||
githubPrNumber: 123,
|
||||
background: true,
|
||||
});
|
||||
|
||||
@@ -1408,7 +1528,7 @@ describe("create_agent MCP tool", () => {
|
||||
const tool = registeredTool(server, "create_worktree");
|
||||
const response = await tool.handler({
|
||||
cwd: repoDir,
|
||||
target: { mode: "branch-off", newBranch: "tool-worktree", base: "main" },
|
||||
target: { kind: "branch-off", worktreeSlug: "tool-worktree", baseBranch: "main" },
|
||||
});
|
||||
|
||||
expect(response.structuredContent.branchName).toBe("tool-worktree");
|
||||
@@ -1483,7 +1603,7 @@ describe("create_agent MCP tool", () => {
|
||||
const archiveTool = registeredTool(server, "archive_worktree");
|
||||
const created = await createTool.handler({
|
||||
cwd: repoDir,
|
||||
target: { mode: "branch-off", newBranch: "archive-tool-worktree", base: "main" },
|
||||
target: { kind: "branch-off", worktreeSlug: "archive-tool-worktree", baseBranch: "main" },
|
||||
});
|
||||
const createdWorktreePath = z.string().parse(created.structuredContent.worktreePath);
|
||||
listActiveWorkspaces.mockImplementation(async () => [
|
||||
@@ -1585,7 +1705,7 @@ describe("create_agent MCP tool", () => {
|
||||
const archiveTool = registeredTool(server, "archive_worktree");
|
||||
const created = await createTool.handler({
|
||||
cwd: repoDir,
|
||||
target: { mode: "branch-off", newBranch: "archive-multi-worktree", base: "main" },
|
||||
target: { kind: "branch-off", worktreeSlug: "archive-multi-worktree", baseBranch: "main" },
|
||||
});
|
||||
const worktreePath = z.string().parse(created.structuredContent.worktreePath);
|
||||
|
||||
@@ -1661,7 +1781,7 @@ describe("create_agent MCP tool", () => {
|
||||
const archiveTool = registeredTool(server, "archive_worktree");
|
||||
const created = await createTool.handler({
|
||||
cwd: repoDir,
|
||||
target: { mode: "branch-off", newBranch: "archive-slug-worktree", base: "main" },
|
||||
target: { kind: "branch-off", worktreeSlug: "archive-slug-worktree", baseBranch: "main" },
|
||||
});
|
||||
|
||||
const response = await archiveTool.handler({
|
||||
@@ -1736,7 +1856,7 @@ describe("create_agent MCP tool", () => {
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Custom provider agent",
|
||||
settings: { modeId: "default" },
|
||||
provider: "zai/custom-model",
|
||||
@@ -1754,6 +1874,7 @@ describe("create_agent MCP tool", () => {
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "voice-agent",
|
||||
cwd: baseDir,
|
||||
workspaceId: "wks_voice",
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent);
|
||||
@@ -1780,7 +1901,7 @@ describe("create_agent MCP tool", () => {
|
||||
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: "subdir",
|
||||
...subagentCurrentWorkspace("subdir"),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -1796,6 +1917,7 @@ describe("create_agent MCP tool", () => {
|
||||
[PARENT_AGENT_ID_LABEL]: "voice-agent",
|
||||
source: "voice",
|
||||
},
|
||||
workspaceId: "wks_voice",
|
||||
},
|
||||
);
|
||||
await rm(baseDir, { recursive: true, force: true });
|
||||
@@ -1806,6 +1928,7 @@ describe("create_agent MCP tool", () => {
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent);
|
||||
@@ -1821,6 +1944,7 @@ describe("create_agent MCP tool", () => {
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await expect(
|
||||
tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -1829,6 +1953,7 @@ describe("create_agent MCP tool", () => {
|
||||
).rejects.toThrow(/Unrecognized key/);
|
||||
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -1838,7 +1963,8 @@ describe("create_agent MCP tool", () => {
|
||||
throw new Error("Expected caller create_agent input to parse");
|
||||
}
|
||||
expect(parsed.data).toMatchObject({
|
||||
detached: false,
|
||||
relationship: { kind: "subagent" },
|
||||
workspace: { kind: "current" },
|
||||
notifyOnFinish: true,
|
||||
});
|
||||
});
|
||||
@@ -1848,6 +1974,7 @@ describe("create_agent MCP tool", () => {
|
||||
const parentAgent = {
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent;
|
||||
@@ -1876,6 +2003,7 @@ describe("create_agent MCP tool", () => {
|
||||
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
const response = await tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -1891,6 +2019,7 @@ describe("create_agent MCP tool", () => {
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent);
|
||||
@@ -1913,10 +2042,10 @@ describe("create_agent MCP tool", () => {
|
||||
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
...detachedCurrentWorkspace(),
|
||||
title: "Detached",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Take over",
|
||||
detached: true,
|
||||
labels: {
|
||||
[PARENT_AGENT_ID_LABEL]: "spoofed-parent",
|
||||
source: "handoff",
|
||||
@@ -1932,6 +2061,7 @@ describe("create_agent MCP tool", () => {
|
||||
labels: {
|
||||
source: "handoff",
|
||||
},
|
||||
workspaceId: "wks_parent",
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1941,6 +2071,7 @@ describe("create_agent MCP tool", () => {
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "claude",
|
||||
currentModeId: "bypassPermissions",
|
||||
} as ManagedAgent);
|
||||
@@ -1967,6 +2098,7 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
const input = {
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -1989,6 +2121,7 @@ describe("create_agent MCP tool", () => {
|
||||
labels: {
|
||||
[PARENT_AGENT_ID_LABEL]: "parent-agent",
|
||||
},
|
||||
workspaceId: "wks_parent",
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -2018,6 +2151,7 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
const result = await tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "codex/gpt-5.4",
|
||||
initialPrompt: "Do work",
|
||||
@@ -2047,11 +2181,12 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Injected config test",
|
||||
settings: { modeId: "auto" },
|
||||
provider: "codex/gpt-5.4",
|
||||
@@ -2065,7 +2200,7 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
expect(configArg.mcpServers).toBeUndefined();
|
||||
expect(agentIdArg).toBeUndefined();
|
||||
expect(optionsArg).toBeUndefined();
|
||||
expect(optionsArg).toEqual({ workspaceId: "workspace-created" });
|
||||
});
|
||||
|
||||
it("rejects an explicit mode that is not valid for the target provider", async () => {
|
||||
@@ -2078,13 +2213,14 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: providerSnapshot.manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
await expect(
|
||||
tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Bad mode",
|
||||
provider: "opencode/gpt-5.4",
|
||||
settings: { modeId: "bypassPermissions" },
|
||||
@@ -2125,12 +2261,13 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: provStub.manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Dynamic mode",
|
||||
provider: "codex/gpt-5.4",
|
||||
settings: { modeId: "dynamic" },
|
||||
@@ -2140,7 +2277,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ modeId: "dynamic" }),
|
||||
undefined,
|
||||
undefined,
|
||||
{ workspaceId: "workspace-created" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2163,12 +2300,13 @@ describe("create_agent MCP tool", () => {
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: providerSnapshot.manager,
|
||||
ensureWorkspaceForCreate,
|
||||
logger,
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
|
||||
await tool.handler({
|
||||
cwd: existingCwd,
|
||||
...detachedDirectoryWorkspace(existingCwd),
|
||||
title: "Legacy mode",
|
||||
provider: "opencode/gpt-5.4",
|
||||
settings: { modeId: "full-access" },
|
||||
@@ -2178,7 +2316,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ modeId: "build", featureValues: { auto_accept: true } }),
|
||||
undefined,
|
||||
undefined,
|
||||
{ workspaceId: "workspace-created" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2187,6 +2325,7 @@ describe("create_agent MCP tool", () => {
|
||||
const parentAgent = {
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "claude",
|
||||
currentModeId: "bypassPermissions",
|
||||
} as ManagedAgent;
|
||||
@@ -2214,6 +2353,7 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "claude/claude-sonnet-4-20250514",
|
||||
initialPrompt: "Do work",
|
||||
@@ -2237,6 +2377,7 @@ describe("create_agent MCP tool", () => {
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "claude",
|
||||
currentModeId: "bypassPermissions",
|
||||
} as ManagedAgent);
|
||||
@@ -2258,6 +2399,7 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
const tool = registeredTool(server, "create_agent");
|
||||
await tool.handler({
|
||||
...subagentCurrentWorkspace(),
|
||||
title: "Child",
|
||||
provider: "opencode/gpt-5.4",
|
||||
settings: { modeId: "build" },
|
||||
@@ -2272,6 +2414,151 @@ describe("create_agent MCP tool", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("send_agent_prompt MCP tool", () => {
|
||||
const logger = createTestLogger();
|
||||
const existingCwd = process.cwd();
|
||||
|
||||
it("defaults agent-scoped prompts to background finish notifications", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
const parentAgent = {
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent;
|
||||
const childAgent = {
|
||||
id: "child-agent",
|
||||
cwd: existingCwd,
|
||||
lifecycle: "running",
|
||||
currentModeId: null,
|
||||
availableModes: [],
|
||||
config: { title: "Child" },
|
||||
} as ManagedAgent;
|
||||
spies.agentManager.getAgent.mockImplementation((agentId: string) => {
|
||||
if (agentId === "parent-agent") return parentAgent;
|
||||
if (agentId === "child-agent") return childAgent;
|
||||
return null;
|
||||
});
|
||||
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
callerAgentId: "parent-agent",
|
||||
logger,
|
||||
});
|
||||
|
||||
const tool = registeredTool(server, "send_agent_prompt");
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
agentId: "child-agent",
|
||||
prompt: "Follow up",
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
if (!parsed.success) {
|
||||
throw new Error("Expected caller send_agent_prompt input to parse");
|
||||
}
|
||||
expect(parsed.data).toMatchObject({
|
||||
background: true,
|
||||
notifyOnFinish: true,
|
||||
});
|
||||
|
||||
const response = await tool.handler(parsed.data as Record<string, unknown>);
|
||||
|
||||
expect(spies.agentManager.subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(spies.agentManager.waitForAgentEvent).not.toHaveBeenCalled();
|
||||
expect(response.structuredContent.guidance).toBe(
|
||||
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps top-level prompts blocking by default", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "child-agent",
|
||||
cwd: existingCwd,
|
||||
lifecycle: "idle",
|
||||
currentModeId: null,
|
||||
availableModes: [],
|
||||
config: { title: "Child" },
|
||||
} as ManagedAgent);
|
||||
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
logger,
|
||||
});
|
||||
|
||||
const tool = registeredTool(server, "send_agent_prompt");
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
agentId: "child-agent",
|
||||
prompt: "Follow up",
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
if (!parsed.success) {
|
||||
throw new Error("Expected top-level send_agent_prompt input to parse");
|
||||
}
|
||||
expect(parsed.data).toMatchObject({
|
||||
background: false,
|
||||
notifyOnFinish: false,
|
||||
});
|
||||
|
||||
await tool.handler(parsed.data as Record<string, unknown>);
|
||||
|
||||
expect(spies.agentManager.subscribe).not.toHaveBeenCalled();
|
||||
expect(spies.agentManager.waitForAgentEvent).toHaveBeenCalledWith(
|
||||
"child-agent",
|
||||
expect.objectContaining({ waitForActive: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not arm a finish notification for blocking agent-scoped prompts", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
const parentAgent = {
|
||||
id: "parent-agent",
|
||||
cwd: existingCwd,
|
||||
workspaceId: "wks_parent",
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent;
|
||||
const childAgent = {
|
||||
id: "child-agent",
|
||||
cwd: existingCwd,
|
||||
lifecycle: "idle",
|
||||
currentModeId: null,
|
||||
availableModes: [],
|
||||
config: { title: "Child" },
|
||||
} as ManagedAgent;
|
||||
spies.agentManager.getAgent.mockImplementation((agentId: string) => {
|
||||
if (agentId === "parent-agent") return parentAgent;
|
||||
if (agentId === "child-agent") return childAgent;
|
||||
return null;
|
||||
});
|
||||
|
||||
const server = await createAgentMcpServer({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
providerSnapshotManager: createOpenCodeManager().manager,
|
||||
callerAgentId: "parent-agent",
|
||||
logger,
|
||||
});
|
||||
|
||||
const tool = registeredTool(server, "send_agent_prompt");
|
||||
await invokeToolWithParsedInput(tool, {
|
||||
agentId: "child-agent",
|
||||
prompt: "Follow up",
|
||||
background: false,
|
||||
});
|
||||
|
||||
expect(spies.agentManager.subscribe).not.toHaveBeenCalled();
|
||||
expect(spies.agentManager.waitForAgentEvent).toHaveBeenCalledWith(
|
||||
"child-agent",
|
||||
expect.objectContaining({ waitForActive: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("update_agent MCP tool", () => {
|
||||
const logger = createTestLogger();
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
type ArchiveDependencies,
|
||||
} from "../workspace-archive-service.js";
|
||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||
import { createAgentCommand } from "./create-agent/create.js";
|
||||
import { createAgentCommand, type CreateAgentFromMcpInput } from "./create-agent/create.js";
|
||||
import type { VoiceCallerContext, VoiceSpeakHandler } from "../voice-types.js";
|
||||
import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../path-utils.js";
|
||||
import type { TerminalManager } from "../../terminal/terminal-manager.js";
|
||||
@@ -100,8 +100,7 @@ export interface AgentMcpServerOptions {
|
||||
markWorkspaceArchiving?: ArchiveDependencies["markWorkspaceArchiving"];
|
||||
clearWorkspaceArchiving?: ArchiveDependencies["clearWorkspaceArchiving"];
|
||||
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
|
||||
// Mints a fresh workspace for a cwd and returns its id, used when an agent is
|
||||
// created with no parent and no worktree.
|
||||
// Mints a fresh directory workspace for a cwd and returns its id.
|
||||
ensureWorkspaceForCreate?: (cwd: string) => Promise<string>;
|
||||
paseoHome?: string;
|
||||
worktreesRoot?: string;
|
||||
@@ -747,11 +746,91 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
.describe("Draft provider feature values."),
|
||||
})
|
||||
.strict();
|
||||
const agentToAgentInputSchema = {
|
||||
cwd: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional working directory. Defaults to your current working directory."),
|
||||
const AgentRelationshipInputSchema = z.discriminatedUnion("kind", [
|
||||
z
|
||||
.object({ kind: z.literal("subagent") })
|
||||
.strict()
|
||||
.describe("Create a child agent under this agent's subagent track."),
|
||||
z
|
||||
.object({ kind: z.literal("detached") })
|
||||
.strict()
|
||||
.describe("Create a root agent that does not appear in this agent's subagent track."),
|
||||
]);
|
||||
const AgentCreateWorktreeTargetInputSchema = z.discriminatedUnion("kind", [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("branch-off"),
|
||||
worktreeSlug: z
|
||||
.string()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("Optional worktree branch/slug. Omit to let Paseo generate one."),
|
||||
baseBranch: z
|
||||
.string()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("Optional base branch. Defaults to the repository default branch."),
|
||||
})
|
||||
.strict()
|
||||
.describe("Create a new branch in a new Paseo worktree."),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("checkout-branch"),
|
||||
branch: z.string().min(1).describe("Existing branch to check out."),
|
||||
})
|
||||
.strict()
|
||||
.describe("Check out an existing branch in a new Paseo worktree."),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("checkout-pr"),
|
||||
githubPrNumber: z.number().int().positive().describe("GitHub pull request number."),
|
||||
})
|
||||
.strict()
|
||||
.describe("Check out a GitHub pull request in a new Paseo worktree."),
|
||||
]);
|
||||
const AgentWorkspaceInputSchema = z.discriminatedUnion("kind", [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("current"),
|
||||
cwd: z.string().optional().describe("Optional runtime cwd. Defaults to the caller's cwd."),
|
||||
})
|
||||
.strict()
|
||||
.describe("Use the caller's current workspace."),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("create"),
|
||||
source: z.discriminatedUnion("kind", [
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("directory"),
|
||||
path: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional directory path. Defaults to the caller's cwd."),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
kind: z.literal("worktree"),
|
||||
cwd: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional source repository. Defaults to the caller's cwd."),
|
||||
target: AgentCreateWorktreeTargetInputSchema,
|
||||
})
|
||||
.strict(),
|
||||
]),
|
||||
})
|
||||
.strict()
|
||||
.describe("Create a new workspace for the agent."),
|
||||
]);
|
||||
const commonCreateAgentInputSchema = {
|
||||
relationship: AgentRelationshipInputSchema.describe(
|
||||
"Whether the created agent is a subagent under you or a detached root agent.",
|
||||
),
|
||||
workspace: AgentWorkspaceInputSchema.describe(
|
||||
"Workspace ownership/location for the created agent.",
|
||||
),
|
||||
title: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -770,13 +849,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
.trim()
|
||||
.min(1, "initialPrompt is required")
|
||||
.describe("Required first task to run immediately after creation."),
|
||||
detached: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"If true, the created agent stands on its own: it does not appear in your subagent track and is not archived with you.",
|
||||
),
|
||||
};
|
||||
const agentToAgentInputSchema = {
|
||||
...commonCreateAgentInputSchema,
|
||||
notifyOnFinish: z
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -785,48 +860,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
"Get notified when the created agent finishes, errors, or needs permission. Set false only for truly fire-and-forget agents.",
|
||||
),
|
||||
};
|
||||
|
||||
const topLevelInputSchema = {
|
||||
cwd: z
|
||||
.string()
|
||||
.describe("Required working directory for the agent (absolute, relative, or ~)."),
|
||||
title: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Title is required")
|
||||
.max(60, "Title must be 60 characters or fewer")
|
||||
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
|
||||
provider: ProviderModelInputSchema.describe(
|
||||
"Required provider/model pair, for example codex/gpt-5.4.",
|
||||
),
|
||||
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
|
||||
settings: CreateAgentSettingsInputSchema.optional().describe(
|
||||
"Initial runtime settings for the new agent.",
|
||||
),
|
||||
initialPrompt: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "initialPrompt is required")
|
||||
.describe("Required first task to run immediately after creation."),
|
||||
worktreeName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional git worktree branch name (lowercase alphanumerics + hyphen)."),
|
||||
baseBranch: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Required when worktreeName is set: the base branch to diff/merge against."),
|
||||
refName: z.string().min(1).optional().describe("Optional source ref for worktree creation."),
|
||||
action: z
|
||||
.enum(["branch-off", "checkout"])
|
||||
.optional()
|
||||
.describe("Optional worktree creation action."),
|
||||
githubPrNumber: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.describe("Optional GitHub pull request number to checkout."),
|
||||
...commonCreateAgentInputSchema,
|
||||
background: z
|
||||
.boolean()
|
||||
.optional()
|
||||
@@ -846,6 +881,48 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
|
||||
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
|
||||
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
|
||||
const commonSendAgentPromptInputSchema = {
|
||||
agentId: z.string(),
|
||||
prompt: z.string(),
|
||||
sessionMode: z.string().optional().describe("Optional mode to set before running the prompt."),
|
||||
};
|
||||
const agentToAgentSendAgentPromptInputSchema = {
|
||||
...commonSendAgentPromptInputSchema,
|
||||
background: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(
|
||||
"Run agent in background. Agent-scoped default is true so you can continue until the finish notification arrives. Set false only when you need a blocking response.",
|
||||
),
|
||||
notifyOnFinish: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe(
|
||||
"Get notified when the prompted agent finishes, errors, or needs permission. Set false only for truly fire-and-forget prompts.",
|
||||
),
|
||||
};
|
||||
const topLevelSendAgentPromptInputSchema = {
|
||||
...commonSendAgentPromptInputSchema,
|
||||
background: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
|
||||
),
|
||||
notifyOnFinish: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"Agent-scoped only: get notified when the prompted agent finishes, errors, or needs permission.",
|
||||
),
|
||||
};
|
||||
const sendAgentPromptInputSchema = callerAgentId
|
||||
? agentToAgentSendAgentPromptInputSchema
|
||||
: topLevelSendAgentPromptInputSchema;
|
||||
const inspectProviderInputSchema = {
|
||||
provider: ProviderOrProviderModelInputSchema.describe(
|
||||
"Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).",
|
||||
@@ -909,7 +986,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
{
|
||||
title: "Create agent",
|
||||
description:
|
||||
"Create an agent tied to a working directory. Requires provider/model, for example codex/gpt-5.4. Do not guess; call list_providers and list_models first if uncertain. Optionally run an initial prompt immediately or create a git worktree for the agent.",
|
||||
"Create an agent. Requires relationship, workspace, provider/model (for example codex/gpt-5.4), and an initial prompt. Do not guess; call list_providers and list_models first if uncertain.",
|
||||
inputSchema: createAgentInputSchema,
|
||||
outputSchema: {
|
||||
agentId: z.string(),
|
||||
@@ -924,19 +1001,19 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
},
|
||||
},
|
||||
async (args: unknown) => {
|
||||
const resolvedArgs = resolveCreateAgentToolArgs(args);
|
||||
const resolvedArgs = await resolveCreateAgentToolArgs(args);
|
||||
const { parsedArgs, worktree } = resolvedArgs;
|
||||
let requestedBackground: boolean;
|
||||
let notifyOnFinish: boolean;
|
||||
let detached: boolean;
|
||||
if (resolvedArgs.kind === "agent-scoped") {
|
||||
requestedBackground = true;
|
||||
notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish;
|
||||
detached = resolvedArgs.parsedArgs.detached;
|
||||
notifyOnFinish = parsedArgs.notifyOnFinish;
|
||||
detached = resolvedArgs.relationship.kind === "detached";
|
||||
} else {
|
||||
requestedBackground = resolvedArgs.parsedArgs.background;
|
||||
notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish ?? false;
|
||||
detached = false;
|
||||
detached = resolvedArgs.parsedArgs.relationship.kind === "detached";
|
||||
}
|
||||
const {
|
||||
snapshot,
|
||||
@@ -961,7 +1038,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
provider: parsedArgs.provider,
|
||||
title: parsedArgs.title,
|
||||
initialPrompt: parsedArgs.initialPrompt,
|
||||
cwd: parsedArgs.cwd,
|
||||
cwd: resolvedArgs.cwd,
|
||||
workspaceId: resolvedArgs.workspaceId,
|
||||
thinking: parsedArgs.settings?.thinkingOptionId,
|
||||
features: parsedArgs.settings?.features,
|
||||
labels: parsedArgs.labels,
|
||||
@@ -1033,48 +1111,113 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
| {
|
||||
kind: "agent-scoped";
|
||||
parsedArgs: AgentToAgentCreateAgentArgs;
|
||||
worktree: undefined;
|
||||
relationship: AgentToAgentCreateAgentArgs["relationship"];
|
||||
cwd: string | undefined;
|
||||
workspaceId: string | undefined;
|
||||
worktree: CreateAgentFromMcpInput["worktree"];
|
||||
}
|
||||
| {
|
||||
kind: "top-level";
|
||||
parsedArgs: TopLevelCreateAgentArgs;
|
||||
worktree: ReturnType<typeof resolveTopLevelCreateAgentWorktree>;
|
||||
cwd: string | undefined;
|
||||
workspaceId: string | undefined;
|
||||
worktree: CreateAgentFromMcpInput["worktree"];
|
||||
};
|
||||
|
||||
function resolveCreateAgentToolArgs(args: unknown): ResolvedCreateAgentToolArgs {
|
||||
async function resolveCreateAgentToolArgs(args: unknown): Promise<ResolvedCreateAgentToolArgs> {
|
||||
if (callerAgentId) {
|
||||
const parsed = agentToAgentCreateAgentArgsSchema.parse(args);
|
||||
const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace);
|
||||
return {
|
||||
kind: "agent-scoped",
|
||||
parsedArgs: agentToAgentCreateAgentArgsSchema.parse(args),
|
||||
worktree: undefined,
|
||||
parsedArgs: parsed,
|
||||
relationship: parsed.relationship,
|
||||
cwd,
|
||||
workspaceId,
|
||||
worktree,
|
||||
};
|
||||
}
|
||||
const parsedArgs = topLevelCreateAgentArgsSchema.parse(args);
|
||||
if (parsedArgs.relationship.kind === "subagent") {
|
||||
throw new Error("relationship subagent requires an agent-scoped MCP session");
|
||||
}
|
||||
const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsedArgs.workspace);
|
||||
return {
|
||||
kind: "top-level",
|
||||
parsedArgs,
|
||||
worktree: resolveTopLevelCreateAgentWorktree(parsedArgs),
|
||||
cwd,
|
||||
workspaceId,
|
||||
worktree,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTopLevelCreateAgentWorktree(args: TopLevelCreateAgentArgs):
|
||||
| {
|
||||
worktreeName?: string;
|
||||
baseBranch?: string;
|
||||
refName?: string;
|
||||
action?: "branch-off" | "checkout";
|
||||
githubPrNumber?: number;
|
||||
async function resolveCreateAgentWorkspace(
|
||||
workspace: AgentToAgentCreateAgentArgs["workspace"] | TopLevelCreateAgentArgs["workspace"],
|
||||
): Promise<{
|
||||
cwd: string | undefined;
|
||||
workspaceId: string | undefined;
|
||||
worktree: CreateAgentFromMcpInput["worktree"];
|
||||
}> {
|
||||
if (workspace.kind === "current") {
|
||||
if (!callerAgentId) {
|
||||
throw new Error("workspace current requires an agent-scoped MCP session");
|
||||
}
|
||||
| undefined {
|
||||
const callerAgent = resolveCallerAgent();
|
||||
if (!callerAgent?.workspaceId) {
|
||||
throw new Error(`Caller agent ${callerAgentId} has no current workspace`);
|
||||
}
|
||||
return {
|
||||
cwd: workspace.cwd,
|
||||
workspaceId: callerAgent.workspaceId,
|
||||
worktree: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (workspace.source.kind === "directory") {
|
||||
const cwd = resolveScopedCwd(workspace.source.path, { required: true });
|
||||
if (!options.ensureWorkspaceForCreate) {
|
||||
throw new Error("Workspace creation is not configured");
|
||||
}
|
||||
return {
|
||||
cwd,
|
||||
workspaceId: await options.ensureWorkspaceForCreate(cwd),
|
||||
worktree: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const cwd = resolveScopedCwd(workspace.source.cwd, { required: true });
|
||||
return {
|
||||
worktreeName: args.worktreeName,
|
||||
baseBranch: args.baseBranch,
|
||||
refName: args.refName,
|
||||
action: args.action,
|
||||
githubPrNumber: args.githubPrNumber,
|
||||
cwd,
|
||||
workspaceId: undefined,
|
||||
worktree: resolveCreateAgentWorktree(workspace.source.target),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCreateAgentWorktree(
|
||||
target: z.infer<typeof AgentCreateWorktreeTargetInputSchema>,
|
||||
): NonNullable<CreateAgentFromMcpInput["worktree"]> {
|
||||
switch (target.kind) {
|
||||
case "branch-off":
|
||||
return {
|
||||
action: "branch-off",
|
||||
worktreeName: target.worktreeSlug,
|
||||
baseBranch: target.baseBranch,
|
||||
};
|
||||
case "checkout-branch":
|
||||
return {
|
||||
action: "checkout",
|
||||
refName: target.branch,
|
||||
};
|
||||
case "checkout-pr":
|
||||
return {
|
||||
action: "checkout",
|
||||
githubPrNumber: target.githubPrNumber,
|
||||
};
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
registerTool(
|
||||
"wait_for_agent",
|
||||
{
|
||||
@@ -1157,40 +1300,27 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
{
|
||||
title: "Send agent prompt",
|
||||
description:
|
||||
"Send a task to a running agent. Returns immediately after the agent begins processing.",
|
||||
inputSchema: {
|
||||
agentId: z.string(),
|
||||
prompt: z.string(),
|
||||
sessionMode: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional mode to set before running the prompt."),
|
||||
background: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
|
||||
),
|
||||
notifyOnFinish: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe(
|
||||
"Agent-scoped only: get notified when this run finishes, errors, or needs permission.",
|
||||
),
|
||||
},
|
||||
"Send a task to a running agent. Agent-scoped callers run in background by default; top-level callers wait by default.",
|
||||
inputSchema: sendAgentPromptInputSchema,
|
||||
outputSchema: {
|
||||
success: z.boolean(),
|
||||
status: AgentStatusEnum,
|
||||
lastMessage: z.string().nullable().optional(),
|
||||
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
|
||||
guidance: z.string().optional(),
|
||||
},
|
||||
},
|
||||
async ({ agentId, prompt, sessionMode, background = false, notifyOnFinish = false }) => {
|
||||
async ({
|
||||
agentId,
|
||||
prompt,
|
||||
sessionMode,
|
||||
background = Boolean(callerAgentId),
|
||||
notifyOnFinish = Boolean(callerAgentId),
|
||||
}) => {
|
||||
if (agentManager.hasInFlightRun(agentId)) {
|
||||
waitTracker.cancel(agentId, "Agent run interrupted by new prompt");
|
||||
}
|
||||
const shouldNotifyOnFinish = Boolean(callerAgentId && notifyOnFinish && background);
|
||||
|
||||
await sendPromptToAgent({
|
||||
agentManager,
|
||||
@@ -1201,7 +1331,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
logger: childLogger,
|
||||
});
|
||||
|
||||
if (notifyOnFinish && callerAgentId) {
|
||||
if (shouldNotifyOnFinish && callerAgentId) {
|
||||
setupFinishNotification({
|
||||
agentManager,
|
||||
agentStorage,
|
||||
@@ -1241,6 +1371,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
status: currentSnapshot?.lifecycle ?? "idle",
|
||||
lastMessage: null,
|
||||
permission: null,
|
||||
...(shouldNotifyOnFinish
|
||||
? {
|
||||
guidance:
|
||||
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives.",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const validJson = ensureValidJson(responseData);
|
||||
|
||||
@@ -2151,33 +2287,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
"Create a Paseo-managed git worktree. Branch off a new branch, check out an existing branch, or check out a GitHub PR.",
|
||||
inputSchema: {
|
||||
cwd: z.string().optional().describe("Repository directory. Defaults to the agent's cwd."),
|
||||
target: z
|
||||
.discriminatedUnion("mode", [
|
||||
z
|
||||
.object({
|
||||
mode: z.literal("branch-off"),
|
||||
newBranch: z.string().min(1).describe("Name for the new branch."),
|
||||
base: z
|
||||
.string()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("Base ref. Defaults to the repo's default branch."),
|
||||
})
|
||||
.describe("Create a new branch off a base."),
|
||||
z
|
||||
.object({
|
||||
mode: z.literal("checkout-branch"),
|
||||
branch: z.string().min(1).describe("Existing branch to check out."),
|
||||
})
|
||||
.describe("Check out an existing branch."),
|
||||
z
|
||||
.object({
|
||||
mode: z.literal("checkout-pr"),
|
||||
prNumber: z.number().int().positive().describe("Pull request number."),
|
||||
})
|
||||
.describe("Check out a GitHub pull request."),
|
||||
])
|
||||
.describe("What the worktree should contain."),
|
||||
target: AgentCreateWorktreeTargetInputSchema.describe("What the worktree should contain."),
|
||||
},
|
||||
outputSchema: {
|
||||
branchName: z.string(),
|
||||
@@ -2420,9 +2530,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
||||
}
|
||||
|
||||
type McpCreateWorktreeTarget =
|
||||
| { mode: "branch-off"; newBranch: string; base?: string }
|
||||
| { mode: "checkout-branch"; branch: string }
|
||||
| { mode: "checkout-pr"; prNumber: number };
|
||||
| { kind: "branch-off"; worktreeSlug?: string; baseBranch?: string }
|
||||
| { kind: "checkout-branch"; branch: string }
|
||||
| { kind: "checkout-pr"; githubPrNumber: number };
|
||||
|
||||
interface ArchiveWorktreeCommandContext {
|
||||
agentManager: AgentManager;
|
||||
@@ -2489,18 +2599,18 @@ function createMcpWorktreeCommandInput(
|
||||
target: McpCreateWorktreeTarget,
|
||||
): CreatePaseoWorktreeCommandInput {
|
||||
const base = { cwd: repoRoot } as const;
|
||||
switch (target.mode) {
|
||||
switch (target.kind) {
|
||||
case "branch-off":
|
||||
return {
|
||||
...base,
|
||||
worktreeSlug: target.newBranch,
|
||||
worktreeSlug: target.worktreeSlug,
|
||||
action: "branch-off",
|
||||
...(target.base ? { refName: target.base } : {}),
|
||||
...(target.baseBranch ? { refName: target.baseBranch } : {}),
|
||||
};
|
||||
case "checkout-branch":
|
||||
return { ...base, action: "checkout", refName: target.branch };
|
||||
case "checkout-pr":
|
||||
return { ...base, action: "checkout", githubPrNumber: target.prNumber };
|
||||
return { ...base, action: "checkout", githubPrNumber: target.githubPrNumber };
|
||||
default:
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
@@ -54,7 +54,14 @@ The receiving agent has zero context. Include:
|
||||
|
||||
## Launch
|
||||
|
||||
Create the agent via Paseo with a `[Handoff] <task>` title, the briefing as initial prompt, `detached: true`, and cwd set to the worktree path if `--worktree`. Leave `notifyOnFinish` omitted unless the user explicitly wants no callback.
|
||||
Create the agent via Paseo with a `[Handoff] <task>` title, the briefing as initial prompt, and `relationship: { kind: "detached" }`.
|
||||
|
||||
Use `workspace` for placement:
|
||||
|
||||
- No worktree: `workspace: { kind: "current" }`.
|
||||
- Worktree: `workspace: { kind: "create", source: { kind: "worktree", target: { kind: "branch-off", worktreeSlug: "<short-task-branch>" } } }`.
|
||||
|
||||
Leave `notifyOnFinish` omitted unless the user explicitly wants no callback.
|
||||
|
||||
Handoff agents are siblings/root agents, not your subagents. They must survive you being archived and must not appear in your subagent track.
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ Paseo is a daemon that supervises AI coding agents on your machine. Control it t
|
||||
|
||||
## Worktrees
|
||||
|
||||
**`create_worktree`** — three modes:
|
||||
**`create_worktree`** — same target union as `create_agent.workspace.source.worktree.target`:
|
||||
|
||||
- From a PR: `{ githubPrNumber: 503 }`.
|
||||
- Branch off a base: `{ action: "branch-off", branchName: "fix/foo", baseBranch: "main" }`.
|
||||
- Checkout an existing ref: `{ action: "checkout", refName: "feat/bar" }`.
|
||||
- From a PR: `{ target: { kind: "checkout-pr", githubPrNumber: 503 } }`.
|
||||
- Branch off a base: `{ target: { kind: "branch-off", worktreeSlug: "fix/foo", baseBranch: "main" } }`.
|
||||
- Checkout an existing branch: `{ target: { kind: "checkout-branch", branch: "feat/bar" } }`.
|
||||
|
||||
Returns `{ branchName, worktreePath }`. Pass `cwd` to target a specific repo.
|
||||
|
||||
@@ -20,21 +20,30 @@ Returns `{ branchName, worktreePath }`. Pass `cwd` to target a specific repo.
|
||||
|
||||
## Agents
|
||||
|
||||
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `notifyOnFinish`, `settings`, `detached`. Returns `{ agentId, … }`.
|
||||
**`create_agent`** — required: `relationship`, `workspace`, `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `notifyOnFinish`, `settings`, `labels`. Returns `{ agentId, … }`.
|
||||
|
||||
Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent.
|
||||
|
||||
Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the returned `worktreePath`.
|
||||
To create a new worktree and launch an agent in it, use `create_agent.workspace.source.kind = "worktree"`. Use `create_worktree` separately only when you need a worktree without launching an agent.
|
||||
|
||||
### Agent relationships
|
||||
|
||||
Agents you create default to **your subagents**: omit `detached` or pass `detached: false`. Use this for advisors, committee members, planners, implementers, auditors, loop workers, and any agent whose lifetime belongs to your task. Subagents appear under you and are archived with you.
|
||||
`relationship` controls parentage only:
|
||||
|
||||
Pass `detached: true` only when the agent you create should stand on its own, not help you finish your task. Use this for handoffs and fire-and-forget delegations the user may continue after you are archived. Detached agents do not appear in your subagent track and are not archived with you.
|
||||
- `{ kind: "subagent" }` — child under your subagents track. Use for advisors, committee members, planners, implementers, auditors, loop workers, and any agent whose lifetime belongs to your task.
|
||||
- `{ kind: "detached" }` — root/sibling agent. Use for handoffs and fire-and-forget delegations the user may continue after you are archived.
|
||||
|
||||
For subagents, leave `notifyOnFinish` omitted or set it to `true`. You will get notified when the created agent finishes, errors, or needs permission. Set `notifyOnFinish: false` only when the created agent is truly fire-and-forget and you do not need to follow up.
|
||||
`workspace` controls placement only:
|
||||
|
||||
**`send_agent_prompt`** — `{ agentId, prompt }`. Use for follow-ups to an existing agent.
|
||||
- `{ kind: "current" }` — same workspace as the caller, with optional `cwd`.
|
||||
- `{ kind: "create", source: { kind: "directory", path?: string } }` — new workspace rooted at a directory.
|
||||
- `{ kind: "create", source: { kind: "worktree", cwd?: string, target: { kind: "branch-off", worktreeSlug?: string, baseBranch?: string } } }`
|
||||
- `{ kind: "create", source: { kind: "worktree", cwd?: string, target: { kind: "checkout-branch", branch: string } } }`
|
||||
- `{ kind: "create", source: { kind: "worktree", cwd?: string, target: { kind: "checkout-pr", githubPrNumber: number } } }`
|
||||
|
||||
Agent-scoped `create_agent` defaults `notifyOnFinish` to true. Set it to `false` only for truly fire-and-forget agents.
|
||||
|
||||
**`send_agent_prompt`** — `{ agentId, prompt }`. Use for follow-ups to an existing agent. Agent-scoped prompt calls default to `background: true` and `notifyOnFinish: true`; top-level calls default to blocking with no callback. For a synchronous follow-up, pass `background: false` and use the returned result.
|
||||
|
||||
**`update_agent`** — `{ agentId, name?, labels?, settings? }`. Use `settings` for runtime changes on an existing agent: `modeId`, `model`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }`.
|
||||
|
||||
@@ -94,7 +103,7 @@ If the file is missing, use sensible defaults and tell the user once.
|
||||
|
||||
Agents take time — 10–30+ minutes is routine. Favor asynchronous workflows.
|
||||
|
||||
For `create_agent`, leave `notifyOnFinish` omitted or set it to `true` unless the created agent is truly fire-and-forget. You will get notified when the created agent finishes, errors, or needs permission. **You must not call `wait_for_agent` on a notify-on-finish agent.** Move on to other work. The notification arrives on its own.
|
||||
For agent-scoped `create_agent` and background `send_agent_prompt`, leave `notifyOnFinish` omitted or set it to `true` unless the work is truly fire-and-forget. You will get notified when the target agent finishes, errors, or needs permission. **You must not call `wait_for_agent` on a notify-on-finish agent.** Move on to other work. The notification arrives on its own.
|
||||
|
||||
Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user