mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add detached agents and heartbeat scheduling (#1266)
This commit is contained in:
@@ -14,14 +14,14 @@ Each agent in `AgentManager` carries a `lastStatus` of `initializing`, `idle`, `
|
|||||||
|
|
||||||
## Relationships
|
## Relationships
|
||||||
|
|
||||||
Agents can launch other agents via the `create_agent` MCP tool. When they do, the daemon stamps the new agent with a label `paseo.parent-agent-id` pointing back at the caller (`packages/server/src/server/agent/mcp-server.ts:804`). 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. 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`.
|
||||||
|
|
||||||
There is exactly one relationship type today: `parentAgentId`. The daemon does not distinguish between:
|
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`.
|
||||||
|
|
||||||
- **Subagents** — children that exist as part of the parent's work (e.g. orchestration tasks the parent delegates and waits on)
|
- **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** — children launched to take over from the parent (e.g. handoffs, fire-and-forget delegations)
|
- **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.
|
||||||
|
|
||||||
Both look the same in storage. This is an accepted limitation — see [Limitations](#limitations).
|
`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.
|
||||||
|
|
||||||
## Archive
|
## Archive
|
||||||
|
|
||||||
@@ -77,12 +77,6 @@ We considered universal decoupling (no tab close ever archives, archive is alway
|
|||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
### Detached agents are cascade-archived
|
|
||||||
|
|
||||||
The daemon can't tell a "subagent" apart from a "detached agent" — both carry `paseo.parent-agent-id`. So when you archive an agent that previously launched a detached child (e.g. via `/paseo-handoff`), cascade will archive the detached child too, even though semantically it should outlive the originator.
|
|
||||||
|
|
||||||
Until a richer relation model lands (e.g. a `relation: "subagent" | "detached"` field on creation, or a separate channel for handoff launches), this trade-off stands. Workaround: don't archive an agent whose work was handed off, or unarchive the detached child afterward.
|
|
||||||
|
|
||||||
### Subagent accumulation under long-lived parents
|
### Subagent accumulation under long-lived parents
|
||||||
|
|
||||||
A parent that spawns many subagents will see the track grow. There's no automatic cleanup for completed subagents — the user prunes via the archive button on each row. A bulk gesture (e.g. "archive all idle children") could land later if this becomes a real problem.
|
A parent that spawns many subagents will see the track grow. There's no automatic cleanup for completed subagents — the user prunes via the archive button on each row. A bulk gesture (e.g. "archive all idle children") could land later if this becomes a real problem.
|
||||||
@@ -99,11 +93,11 @@ $PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json
|
|||||||
|
|
||||||
Each agent is a single JSON file. Fields relevant to this doc:
|
Each agent is a single JSON file. Fields relevant to this doc:
|
||||||
|
|
||||||
| Field | Type | Meaning |
|
| Field | Type | Meaning |
|
||||||
| --------------------------------- | ------------- | ------------------------------------------------------------- |
|
| --------------------------------- | ------------- | ----------------------------------------------------------------------------------------- |
|
||||||
| `id` | `string` | Stable identifier |
|
| `id` | `string` | Stable identifier |
|
||||||
| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) |
|
| `archivedAt` | `string?` | Soft-delete timestamp (ISO 8601) |
|
||||||
| `labels["paseo.parent-agent-id"]` | `string?` | Parent agent ID, set automatically by `create_agent` MCP tool |
|
| `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` |
|
| `lastStatus` | `AgentStatus` | `initializing` / `idle` / `running` / `error` / `closed` |
|
||||||
|
|
||||||
See [`docs/data-model.md`](./data-model.md) for the full agent record.
|
See [`docs/data-model.md`](./data-model.md) for the full agent record.
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here
|
|||||||
- **Provider** — Agent backend (Claude Code, Codex, Copilot, OpenCode, Pi). UI: "Provider". Code: `ProviderSnapshotEntry` (`packages/protocol/src/messages.ts:198`).
|
- **Provider** — Agent backend (Claude Code, Codex, Copilot, OpenCode, Pi). UI: "Provider". Code: `ProviderSnapshotEntry` (`packages/protocol/src/messages.ts:198`).
|
||||||
- **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`).
|
- **Model** — A specific LLM offered by a provider. UI: "Model" / "Select model". Code: `AgentModelDefinition` (`packages/protocol/src/messages.ts:187`).
|
||||||
- **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`).
|
- **Terminal** — Workspace-scoped PTY shell streamed over the binary mux channel. UI: "Terminal". Code: `TerminalStreamFrame` (`packages/protocol/src/terminal-stream-protocol.ts`).
|
||||||
- **Schedule** — Cron-style trigger that creates remote agents. UI: CLI only (`paseo schedule`). Code: `ScheduleCreateRequest` (re-exported from `packages/protocol/src/messages.ts`). Don't confuse with: Loop (iterative re-execution of one agent).
|
- **Schedule** — Cron-style trigger that creates new agents. UI: CLI/MCP (`paseo schedule`, `create_schedule`). Don't confuse with: Heartbeat (cron prompt back into the same agent) or Loop (iterative re-execution of one agent).
|
||||||
|
- **Heartbeat** — Cron-style prompt sent back into the same agent/conversation. MCP: `create_heartbeat`. Use for reminders and babysitting where the status should return inline.
|
||||||
- **Mode** — Provider-specific operational mode (plan, default, full-access, …). UI: icon-only. Code: `modeId` in `AgentSessionConfig` (`packages/protocol/src/messages.ts:257`).
|
- **Mode** — Provider-specific operational mode (plan, default, full-access, …). UI: icon-only. Code: `modeId` in `AgentSessionConfig` (`packages/protocol/src/messages.ts:257`).
|
||||||
- **Attachment** — GitHub PR or Issue bound to an agent prompt. UI: "Attach issue or PR". Code: `AgentAttachment` (`packages/protocol/src/messages.ts:782`).
|
- **Attachment** — GitHub PR or Issue bound to an agent prompt. UI: "Attach issue or PR". Code: `AgentAttachment` (`packages/protocol/src/messages.ts:782`).
|
||||||
- **Composer** — The whole prompt surface for sending work to an agent. Code: `Composer` (`packages/app/src/composer/index.tsx`). Don't call this "message input" except for the text-entry subcomponent.
|
- **Composer** — The whole prompt surface for sending work to an agent. Code: `Composer` (`packages/app/src/composer/index.tsx`). Don't call this "message input" except for the text-entry subcomponent.
|
||||||
|
|||||||
@@ -232,18 +232,16 @@ To confirm the submission landed, inspect the EAS workflow with `npx eas workflo
|
|||||||
|
|
||||||
The user rarely opens the Expo dashboard. A failed EAS build or submit/review job can sit silently until users complain about a stale version. After every stable release, set up a long-delay babysit that re-checks GitHub Actions, EAS builds, and the EAS `Release Mobile` workflow for the release tag. If any build is `ERRORED`/`CANCELED`, any workflow is `FAILURE`, or any required submit/review job fails, surface it immediately. If all builds are `FINISHED` and all required submit/review jobs are `SUCCESS`, confirm and stop.
|
The user rarely opens the Expo dashboard. A failed EAS build or submit/review job can sit silently until users complain about a stale version. After every stable release, set up a long-delay babysit that re-checks GitHub Actions, EAS builds, and the EAS `Release Mobile` workflow for the release tag. If any build is `ERRORED`/`CANCELED`, any workflow is `FAILURE`, or any required submit/review job fails, surface it immediately. If all builds are `FINISHED` and all required submit/review jobs are `SUCCESS`, confirm and stop.
|
||||||
|
|
||||||
**Use a heartbeat schedule, never a new-agent schedule.** Babysitting fires back into the current conversation as a wake-up prompt — `target: "self"` in `mcp__paseo__create_schedule`. Never use `target: "new-agent"`. A new agent spawns a fresh conversation the user has to find and read; a heartbeat surfaces the build status inline in the conversation that owns the release, where it is impossible to miss. If you find yourself reaching for `new-agent` for a release babysit, you are about to ship a status report into a void.
|
**Use `create_heartbeat`, never `create_schedule`, for release babysitting.** Babysitting fires back into the current conversation as a wake-up prompt. `create_schedule` starts a fresh agent the user has to find and read; `create_heartbeat` surfaces the build status inline in the conversation that owns the release, where it is impossible to miss. If you find yourself reaching for `create_schedule` for a release babysit, you are about to ship a status report into a void.
|
||||||
|
|
||||||
Pattern:
|
Pattern:
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
// mcp__paseo__create_schedule arguments
|
// mcp__paseo__create_heartbeat arguments
|
||||||
{
|
{
|
||||||
"name": "vX.Y.Z release babysit heartbeat",
|
"name": "vX.Y.Z release babysit heartbeat",
|
||||||
"every": "15m",
|
"cron": "*/15 * * * *",
|
||||||
"maxRuns": 8, // covers ~2h of build + store-submission window
|
"maxRuns": 8, // covers ~2h of build + store-submission window
|
||||||
"target": "self", // heartbeat, NOT "new-agent"
|
|
||||||
"cwd": "/path/to/paseo",
|
|
||||||
"prompt": "Heartbeat: check vX.Y.Z release. Run gh run list, eas build:list, eas workflow:runs, and eas workflow:view for the matching Release Mobile run. Report concisely. The release is not done until desktop/APK workflows are green, EAS builds are FINISHED, Android submit_android is SUCCESS, and iOS submit_ios + submit_ios_for_review are SUCCESS. Flag any ERRORED/FAILED/CANCELED/FAILURE loudly.",
|
"prompt": "Heartbeat: check vX.Y.Z release. Run gh run list, eas build:list, eas workflow:runs, and eas workflow:view for the matching Release Mobile run. Report concisely. The release is not done until desktop/APK workflows are green, EAS builds are FINISHED, Android submit_android is SUCCESS, and iOS submit_ios + submit_ios_for_review are SUCCESS. Flag any ERRORED/FAILED/CANCELED/FAILURE loudly.",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export interface CreateAgentFromMcpInput {
|
|||||||
mode?: string;
|
mode?: string;
|
||||||
background: boolean;
|
background: boolean;
|
||||||
notifyOnFinish: boolean;
|
notifyOnFinish: boolean;
|
||||||
|
detached?: boolean;
|
||||||
callerAgentId?: string;
|
callerAgentId?: string;
|
||||||
callerContext?: {
|
callerContext?: {
|
||||||
lockedCwd?: string;
|
lockedCwd?: string;
|
||||||
@@ -256,11 +257,12 @@ async function resolveMcpCreateAgent(
|
|||||||
parent: parentAgent,
|
parent: parentAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
const labels = mergeLabels(
|
const labels = mergeLabels({
|
||||||
input.callerAgentId,
|
callerAgentId: input.callerAgentId,
|
||||||
input.callerContext?.childAgentDefaultLabels,
|
detached: input.detached ?? false,
|
||||||
input.labels,
|
childAgentDefaultLabels: input.callerContext?.childAgentDefaultLabels,
|
||||||
);
|
labels: input.labels,
|
||||||
|
});
|
||||||
|
|
||||||
const trimmedPrompt = input.initialPrompt.trim();
|
const trimmedPrompt = input.initialPrompt.trim();
|
||||||
return {
|
return {
|
||||||
@@ -469,15 +471,21 @@ async function createMcpWorktree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeLabels(
|
function mergeLabels(params: {
|
||||||
callerAgentId: string | undefined,
|
callerAgentId: string | undefined;
|
||||||
childAgentDefaultLabels: Record<string, string> | undefined,
|
detached: boolean;
|
||||||
labels: Record<string, string> | undefined,
|
childAgentDefaultLabels: Record<string, string> | undefined;
|
||||||
): Record<string, string> | undefined {
|
labels: Record<string, string> | undefined;
|
||||||
|
}): Record<string, string> | undefined {
|
||||||
const mergedLabels = {
|
const mergedLabels = {
|
||||||
...(callerAgentId ? { [PARENT_AGENT_ID_LABEL]: callerAgentId } : {}),
|
...(!params.detached && params.callerAgentId
|
||||||
...childAgentDefaultLabels,
|
? { [PARENT_AGENT_ID_LABEL]: params.callerAgentId }
|
||||||
...labels,
|
: {}),
|
||||||
|
...params.childAgentDefaultLabels,
|
||||||
|
...params.labels,
|
||||||
};
|
};
|
||||||
|
if (params.detached) {
|
||||||
|
delete mergedLabels[PARENT_AGENT_ID_LABEL];
|
||||||
|
}
|
||||||
return Object.keys(mergedLabels).length > 0 ? mergedLabels : undefined;
|
return Object.keys(mergedLabels).length > 0 ? mergedLabels : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ async function createChildAgent(args?: Partial<StructuredContent>): Promise<stri
|
|||||||
title: "Parity child",
|
title: "Parity child",
|
||||||
provider: "claude/claude-test-model",
|
provider: "claude/claude-test-model",
|
||||||
initialPrompt: "say done and stop",
|
initialPrompt: "say done and stop",
|
||||||
background: true,
|
notifyOnFinish: false,
|
||||||
...args,
|
...args,
|
||||||
});
|
});
|
||||||
return str(payload.agentId);
|
return str(payload.agentId);
|
||||||
@@ -286,6 +286,17 @@ describe("Suite A: Core Fixes", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("create_agent with detached true omits the parent agent label", async () => {
|
||||||
|
let agentId: string | null = null;
|
||||||
|
try {
|
||||||
|
agentId = await createChildAgent({ detached: true });
|
||||||
|
const snapshot = daemonHandle.daemon.agentManager.getAgent(agentId);
|
||||||
|
expect(snapshot?.labels?.[PARENT_AGENT_ID_LABEL]).toBeUndefined();
|
||||||
|
} finally {
|
||||||
|
await archiveAgentIfPresent(agentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("agentManager.createAgent injects paseo MCP using the daemon listen target", async () => {
|
test("agentManager.createAgent injects paseo MCP using the daemon listen target", async () => {
|
||||||
let agentId: string | null = null;
|
let agentId: string | null = null;
|
||||||
try {
|
try {
|
||||||
@@ -565,7 +576,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
try {
|
try {
|
||||||
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Parity schedule list",
|
name: "Parity schedule list",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
});
|
});
|
||||||
@@ -591,7 +602,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
try {
|
try {
|
||||||
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Parity provider schedule",
|
name: "Parity provider schedule",
|
||||||
provider: "codex/gpt-5.4",
|
provider: "codex/gpt-5.4",
|
||||||
});
|
});
|
||||||
@@ -613,7 +624,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
try {
|
try {
|
||||||
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Parity inspect schedule",
|
name: "Parity inspect schedule",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
});
|
});
|
||||||
@@ -638,7 +649,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
try {
|
try {
|
||||||
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Parity pause schedule",
|
name: "Parity pause schedule",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
});
|
});
|
||||||
@@ -665,7 +676,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
try {
|
try {
|
||||||
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
const created = await callToolStructured(topLevelClient, "create_schedule", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Parity delete schedule",
|
name: "Parity delete schedule",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
});
|
});
|
||||||
@@ -682,14 +693,13 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create_schedule target self with callerAgentId", async () => {
|
test("create_heartbeat targets the scoped agent", async () => {
|
||||||
let scheduleId: string | null = null;
|
let scheduleId: string | null = null;
|
||||||
try {
|
try {
|
||||||
const created = await callToolStructured(agentScopedClient, "create_schedule", {
|
const created = await callToolStructured(agentScopedClient, "create_heartbeat", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Parity self schedule",
|
name: "Parity heartbeat",
|
||||||
target: "self",
|
|
||||||
});
|
});
|
||||||
scheduleId = str(created.id);
|
scheduleId = str(created.id);
|
||||||
expect(created.target).toMatchObject({
|
expect(created.target).toMatchObject({
|
||||||
@@ -706,7 +716,7 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
try {
|
try {
|
||||||
const created = await callToolStructured(agentScopedClient, "create_schedule", {
|
const created = await callToolStructured(agentScopedClient, "create_schedule", {
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
provider: "codex/gpt-5.4",
|
provider: "codex/gpt-5.4",
|
||||||
});
|
});
|
||||||
scheduleId = str(created.id);
|
scheduleId = str(created.id);
|
||||||
@@ -722,16 +732,15 @@ describe("Suite C: Schedule Tools", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("create_schedule target self without callerAgentId throws", async () => {
|
test("create_heartbeat without callerAgentId throws", async () => {
|
||||||
await expectToolError(
|
await expectToolError(
|
||||||
topLevelClient,
|
topLevelClient,
|
||||||
"create_schedule",
|
"create_heartbeat",
|
||||||
{
|
{
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
target: "self",
|
|
||||||
},
|
},
|
||||||
/requires a caller agent/i,
|
/requires an agent-scoped session/i,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1692,6 +1692,141 @@ describe("create_agent MCP tool", () => {
|
|||||||
await rm(baseDir, { recursive: true, force: true });
|
await rm(baseDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rejects background from caller agents and defaults notify-on-finish on", 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({
|
||||||
|
title: "Child",
|
||||||
|
provider: "codex/gpt-5.4",
|
||||||
|
initialPrompt: "Do work",
|
||||||
|
background: false,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Unrecognized key/);
|
||||||
|
|
||||||
|
const parsed = await tool.inputSchema.safeParseAsync({
|
||||||
|
title: "Child",
|
||||||
|
provider: "codex/gpt-5.4",
|
||||||
|
initialPrompt: "Do work",
|
||||||
|
});
|
||||||
|
expect(parsed.success).toBe(true);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error("Expected caller create_agent input to parse");
|
||||||
|
}
|
||||||
|
expect(parsed.data).toMatchObject({
|
||||||
|
detached: false,
|
||||||
|
notifyOnFinish: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns notify-on-finish guidance for caller-created agents", async () => {
|
||||||
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
|
const parentAgent = {
|
||||||
|
id: "parent-agent",
|
||||||
|
cwd: existingCwd,
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
spies.agentManager.createAgent.mockResolvedValue(childAgent);
|
||||||
|
|
||||||
|
const server = await createAgentMcpServer({
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
providerSnapshotManager: createOpenCodeManager().manager,
|
||||||
|
callerAgentId: "parent-agent",
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = registeredTool(server, "create_agent");
|
||||||
|
const response = await tool.handler({
|
||||||
|
title: "Child",
|
||||||
|
provider: "codex/gpt-5.4",
|
||||||
|
initialPrompt: "Do work",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.structuredContent.guidance).toBe(
|
||||||
|
"You will get notified when the created 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("creates detached caller agents without a parent label", async () => {
|
||||||
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
|
spies.agentManager.getAgent.mockReturnValue({
|
||||||
|
id: "parent-agent",
|
||||||
|
cwd: existingCwd,
|
||||||
|
provider: "codex",
|
||||||
|
currentModeId: "full-access",
|
||||||
|
} as ManagedAgent);
|
||||||
|
spies.agentManager.createAgent.mockResolvedValue({
|
||||||
|
id: "detached-agent",
|
||||||
|
cwd: existingCwd,
|
||||||
|
lifecycle: "idle",
|
||||||
|
currentModeId: null,
|
||||||
|
availableModes: [],
|
||||||
|
config: { title: "Detached" },
|
||||||
|
} as ManagedAgent);
|
||||||
|
|
||||||
|
const server = await createAgentMcpServer({
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
providerSnapshotManager: createOpenCodeManager().manager,
|
||||||
|
callerAgentId: "parent-agent",
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tool = registeredTool(server, "create_agent");
|
||||||
|
await tool.handler({
|
||||||
|
title: "Detached",
|
||||||
|
provider: "codex/gpt-5.4",
|
||||||
|
initialPrompt: "Take over",
|
||||||
|
detached: true,
|
||||||
|
labels: {
|
||||||
|
[PARENT_AGENT_ID_LABEL]: "spoofed-parent",
|
||||||
|
source: "handoff",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
cwd: existingCwd,
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
labels: {
|
||||||
|
source: "handoff",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts provider features from caller agents and passes them through createAgent", async () => {
|
it("accepts provider features from caller agents and passes them through createAgent", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
spies.agentManager.getAgent.mockReturnValue({
|
spies.agentManager.getAgent.mockReturnValue({
|
||||||
@@ -1721,7 +1856,6 @@ describe("create_agent MCP tool", () => {
|
|||||||
title: "Child",
|
title: "Child",
|
||||||
provider: "codex/gpt-5.4",
|
provider: "codex/gpt-5.4",
|
||||||
initialPrompt: "Do work",
|
initialPrompt: "Do work",
|
||||||
background: true,
|
|
||||||
settings: { features: { fast_mode: true } },
|
settings: { features: { fast_mode: true } },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2139,7 +2273,7 @@ describe("update_agent MCP tool", () => {
|
|||||||
describe("create_schedule MCP tool", () => {
|
describe("create_schedule MCP tool", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
it("requires provider for new-agent schedules", async () => {
|
it("requires provider for schedules", async () => {
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
const create = vi.fn(async (input: CreateScheduleInput) => createStoredSchedule(input));
|
const create = vi.fn(async (input: CreateScheduleInput) => createStoredSchedule(input));
|
||||||
const server = await createAgentMcpServer({
|
const server = await createAgentMcpServer({
|
||||||
@@ -2154,7 +2288,7 @@ describe("create_schedule MCP tool", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
tool.handler({
|
tool.handler({
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
name: "Default schedule",
|
name: "Default schedule",
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow("provider is required when target is new-agent");
|
).rejects.toThrow("provider is required when target is new-agent");
|
||||||
@@ -2175,12 +2309,12 @@ describe("create_schedule MCP tool", () => {
|
|||||||
|
|
||||||
await tool.handler({
|
await tool.handler({
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
});
|
});
|
||||||
await tool.handler({
|
await tool.handler({
|
||||||
prompt: "say hello again",
|
prompt: "say hello again",
|
||||||
every: "10m",
|
cron: "*/10 * * * *",
|
||||||
provider: "codex/gpt-5.4",
|
provider: "codex/gpt-5.4",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2239,8 +2373,7 @@ describe("create_schedule MCP tool", () => {
|
|||||||
|
|
||||||
const response = await tool.handler({
|
const response = await tool.handler({
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "5m",
|
cron: "*/5 * * * *",
|
||||||
target: "new-agent",
|
|
||||||
provider: "opencode/openai/gpt-5.5",
|
provider: "opencode/openai/gpt-5.5",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2251,83 +2384,6 @@ describe("create_schedule MCP tool", () => {
|
|||||||
expectOutputSchemaAccepts(tool, response.structuredContent);
|
expectOutputSchemaAccepts(tool, response.structuredContent);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts a blank cron field when every is provided", async () => {
|
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
|
||||||
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
|
|
||||||
createStoredSchedule(scheduleInput),
|
|
||||||
);
|
|
||||||
const server = await createAgentMcpServer({
|
|
||||||
agentManager,
|
|
||||||
agentStorage,
|
|
||||||
providerSnapshotManager: createOpenCodeManager().manager,
|
|
||||||
scheduleService: { create } as unknown as ScheduleService,
|
|
||||||
logger,
|
|
||||||
});
|
|
||||||
const tool = registeredTool(server, "create_schedule");
|
|
||||||
|
|
||||||
await invokeToolWithParsedInput(tool, {
|
|
||||||
prompt: "say hello",
|
|
||||||
every: "10m",
|
|
||||||
cron: "",
|
|
||||||
provider: "codex",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(create).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
cadence: { type: "every", everyMs: 600000 },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
{
|
|
||||||
label: "whitespace cron field",
|
|
||||||
input: { prompt: "say hello", every: "10m", cron: " ", provider: "codex" },
|
|
||||||
cadence: { type: "every", everyMs: 600000 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "blank every field for cron cadence",
|
|
||||||
input: {
|
|
||||||
prompt: "say hello",
|
|
||||||
every: "",
|
|
||||||
cron: "*/10 * * * *",
|
|
||||||
provider: "codex",
|
|
||||||
},
|
|
||||||
cadence: { type: "cron", expression: "*/10 * * * *" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "whitespace every field for cron cadence",
|
|
||||||
input: {
|
|
||||||
prompt: "say hello",
|
|
||||||
every: " ",
|
|
||||||
cron: "*/10 * * * *",
|
|
||||||
provider: "codex",
|
|
||||||
},
|
|
||||||
cadence: { type: "cron", expression: "*/10 * * * *" },
|
|
||||||
},
|
|
||||||
])("normalizes create_schedule blank cadence input for $label", async ({ input, cadence }) => {
|
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
|
||||||
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
|
|
||||||
createStoredSchedule(scheduleInput),
|
|
||||||
);
|
|
||||||
const server = await createAgentMcpServer({
|
|
||||||
agentManager,
|
|
||||||
agentStorage,
|
|
||||||
providerSnapshotManager: createOpenCodeManager().manager,
|
|
||||||
scheduleService: { create } as unknown as ScheduleService,
|
|
||||||
logger,
|
|
||||||
});
|
|
||||||
const tool = registeredTool(server, "create_schedule");
|
|
||||||
|
|
||||||
await invokeToolWithParsedInput(tool, input);
|
|
||||||
|
|
||||||
expect(create).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
cadence,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes timezone through cron create_schedule input", async () => {
|
it("passes timezone through cron create_schedule input", async () => {
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
|
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
|
||||||
@@ -2360,7 +2416,7 @@ describe("create_schedule MCP tool", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("still rejects both real every and cron inputs", async () => {
|
it("rejects removed create_schedule every input", async () => {
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
const create = vi.fn();
|
const create = vi.fn();
|
||||||
const server = await createAgentMcpServer({
|
const server = await createAgentMcpServer({
|
||||||
@@ -2372,19 +2428,17 @@ describe("create_schedule MCP tool", () => {
|
|||||||
});
|
});
|
||||||
const tool = registeredTool(server, "create_schedule");
|
const tool = registeredTool(server, "create_schedule");
|
||||||
|
|
||||||
await expect(
|
const parsed = await tool.inputSchema.safeParseAsync({
|
||||||
invokeToolWithParsedInput(tool, {
|
prompt: "say hello",
|
||||||
prompt: "say hello",
|
every: "10m",
|
||||||
every: "10m",
|
provider: "codex",
|
||||||
cron: "*/10 * * * *",
|
});
|
||||||
provider: "codex",
|
expect(parsed.success).toBe(false);
|
||||||
}),
|
|
||||||
).rejects.toThrow("Specify exactly one of every or cron");
|
|
||||||
|
|
||||||
expect(create).not.toHaveBeenCalled();
|
expect(create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects create_schedule timezone without cron", async () => {
|
it("rejects create_schedule without cron", async () => {
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
const create = vi.fn();
|
const create = vi.fn();
|
||||||
const server = await createAgentMcpServer({
|
const server = await createAgentMcpServer({
|
||||||
@@ -2397,13 +2451,11 @@ describe("create_schedule MCP tool", () => {
|
|||||||
const tool = registeredTool(server, "create_schedule");
|
const tool = registeredTool(server, "create_schedule");
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
invokeToolWithParsedInput(tool, {
|
tool.handler({
|
||||||
prompt: "say hello",
|
prompt: "say hello",
|
||||||
every: "10m",
|
|
||||||
timezone: "America/New_York",
|
|
||||||
provider: "codex",
|
provider: "codex",
|
||||||
}),
|
}),
|
||||||
).rejects.toThrow("timezone can only be used with cron");
|
).rejects.toThrow(/cron/);
|
||||||
|
|
||||||
expect(create).not.toHaveBeenCalled();
|
expect(create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -2431,17 +2483,55 @@ describe("create_schedule MCP tool", () => {
|
|||||||
|
|
||||||
expect(create).not.toHaveBeenCalled();
|
expect(create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
describe("create_heartbeat MCP tool", () => {
|
||||||
{
|
const logger = createTestLogger();
|
||||||
label: "missing both cadence fields",
|
|
||||||
input: { prompt: "say hello", provider: "codex" },
|
it("creates a self-targeted cron heartbeat", async () => {
|
||||||
},
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
{
|
spies.agentManager.getAgent.mockReturnValue({
|
||||||
label: "blank cadence fields",
|
id: "parent-agent",
|
||||||
input: { prompt: "say hello", every: " ", cron: "", provider: "codex" },
|
provider: "codex",
|
||||||
},
|
cwd: REPO_CWD,
|
||||||
])("still rejects create_schedule when $label", async ({ input }) => {
|
lifecycle: "idle",
|
||||||
|
currentModeId: "build",
|
||||||
|
availableModes: [],
|
||||||
|
config: { title: "Parent agent" },
|
||||||
|
} as ManagedAgent);
|
||||||
|
const create = vi.fn(async (input: CreateScheduleInput) => createStoredSchedule(input));
|
||||||
|
const server = await createAgentMcpServer({
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
providerSnapshotManager: createOpenCodeManager().manager,
|
||||||
|
scheduleService: { create } as unknown as ScheduleService,
|
||||||
|
callerAgentId: "parent-agent",
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
const tool = registeredTool(server, "create_heartbeat");
|
||||||
|
|
||||||
|
await invokeToolWithParsedInput(tool, {
|
||||||
|
prompt: "check status",
|
||||||
|
cron: "*/15 * * * *",
|
||||||
|
timezone: "America/New_York",
|
||||||
|
name: "status heartbeat",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
prompt: "check status",
|
||||||
|
cadence: {
|
||||||
|
type: "cron",
|
||||||
|
expression: "*/15 * * * *",
|
||||||
|
timezone: "America/New_York",
|
||||||
|
},
|
||||||
|
target: { type: "agent", agentId: "parent-agent" },
|
||||||
|
name: "status heartbeat",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires an agent-scoped session", async () => {
|
||||||
const { agentManager, agentStorage } = createTestDeps();
|
const { agentManager, agentStorage } = createTestDeps();
|
||||||
const create = vi.fn();
|
const create = vi.fn();
|
||||||
const server = await createAgentMcpServer({
|
const server = await createAgentMcpServer({
|
||||||
@@ -2451,11 +2541,14 @@ describe("create_schedule MCP tool", () => {
|
|||||||
scheduleService: { create } as unknown as ScheduleService,
|
scheduleService: { create } as unknown as ScheduleService,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
const tool = registeredTool(server, "create_schedule");
|
const tool = registeredTool(server, "create_heartbeat");
|
||||||
|
|
||||||
await expect(invokeToolWithParsedInput(tool, input)).rejects.toThrow(
|
await expect(
|
||||||
"Specify exactly one of every or cron",
|
tool.handler({
|
||||||
);
|
prompt: "check status",
|
||||||
|
cron: "*/15 * * * *",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("create_heartbeat requires an agent-scoped session");
|
||||||
|
|
||||||
expect(create).not.toHaveBeenCalled();
|
expect(create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -3131,7 +3224,7 @@ describe("speak MCP tool", () => {
|
|||||||
});
|
});
|
||||||
const tool = registeredTool(server, "speak");
|
const tool = registeredTool(server, "speak");
|
||||||
await expect(tool.handler({ text: "Hello." })).rejects.toThrow(
|
await expect(tool.handler({ text: "Hello." })).rejects.toThrow(
|
||||||
"No speak handler registered for caller agent",
|
"No speak handler registered for your session",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -514,6 +514,28 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
registerRawTool(name, relaxMcpToolOutputSchema(config), (async (args: never, extra: never) =>
|
registerRawTool(name, relaxMcpToolOutputSchema(config), (async (args: never, extra: never) =>
|
||||||
addModelVisibleStructuredContent(await handler(args, extra))) as typeof handler);
|
addModelVisibleStructuredContent(await handler(args, extra))) as typeof handler);
|
||||||
|
|
||||||
|
const buildCronScheduleCadence = (input: {
|
||||||
|
cron: string | undefined;
|
||||||
|
timezone?: string;
|
||||||
|
}): ScheduleCadence => {
|
||||||
|
const expression = input.cron?.trim() ?? "";
|
||||||
|
if (!expression) {
|
||||||
|
throw new Error("cron is required");
|
||||||
|
}
|
||||||
|
const timezone = normalizeScheduleTimeZoneArg(input.timezone);
|
||||||
|
return {
|
||||||
|
type: "cron",
|
||||||
|
expression,
|
||||||
|
...(timezone !== undefined ? { timezone } : {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildScheduleExpiry = (expiresIn: string | undefined): string | undefined => {
|
||||||
|
return expiresIn === undefined
|
||||||
|
? undefined
|
||||||
|
: new Date(Date.now() + parseDurationString(expiresIn)).toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
const resolveCallerAgent = () => {
|
const resolveCallerAgent = () => {
|
||||||
if (!callerAgentId) {
|
if (!callerAgentId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -541,7 +563,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
if (opts?.required) {
|
if (opts?.required) {
|
||||||
throw new Error("cwd is required");
|
throw new Error("cwd is required");
|
||||||
}
|
}
|
||||||
throw new Error("cwd is required when no caller agent is available");
|
throw new Error("cwd is required outside an agent-scoped session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return expandUserPath(trimmedCwd);
|
return expandUserPath(trimmedCwd);
|
||||||
@@ -699,7 +721,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Optional working directory. Defaults to the caller agent working directory."),
|
.describe("Optional working directory. Defaults to your current working directory."),
|
||||||
title: z
|
title: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
@@ -718,19 +740,19 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
.trim()
|
.trim()
|
||||||
.min(1, "initialPrompt is required")
|
.min(1, "initialPrompt is required")
|
||||||
.describe("Required first task to run immediately after creation."),
|
.describe("Required first task to run immediately after creation."),
|
||||||
background: z
|
detached: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(false)
|
||||||
.describe(
|
.describe(
|
||||||
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
|
"If true, the created agent stands on its own: it does not appear in your subagent track and is not archived with you.",
|
||||||
),
|
),
|
||||||
notifyOnFinish: z
|
notifyOnFinish: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(true)
|
||||||
.describe(
|
.describe(
|
||||||
"Send a notification prompt to the caller agent when this agent finishes, errors, or needs permission. Requires a caller agent context.",
|
"Get notified when the created agent finishes, errors, or needs permission. Set false only for truly fire-and-forget agents.",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -787,7 +809,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(false)
|
||||||
.describe(
|
.describe(
|
||||||
"Send a notification prompt to the caller agent when this agent finishes, errors, or needs permission. Requires a caller agent context.",
|
"Agent-scoped only: get notified when the created agent finishes, errors, or needs permission.",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -806,6 +828,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
"Draft provider settings used to compute available features.",
|
"Draft provider settings used to compute available features.",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
type AgentToAgentCreateAgentArgs = z.infer<typeof agentToAgentCreateAgentArgsSchema>;
|
||||||
type TopLevelCreateAgentArgs = z.infer<typeof topLevelCreateAgentArgsSchema>;
|
type TopLevelCreateAgentArgs = z.infer<typeof topLevelCreateAgentArgsSchema>;
|
||||||
|
|
||||||
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
|
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
|
||||||
@@ -832,7 +855,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
}
|
}
|
||||||
const handler = resolveSpeakHandler?.(callerAgentId) ?? null;
|
const handler = resolveSpeakHandler?.(callerAgentId) ?? null;
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
throw new Error(`No speak handler registered for caller agent '${callerAgentId}'`);
|
throw new Error(`No speak handler registered for your session '${callerAgentId}'`);
|
||||||
}
|
}
|
||||||
await handler({
|
await handler({
|
||||||
text: args.text,
|
text: args.text,
|
||||||
@@ -867,11 +890,29 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
availableModes: z.array(ProviderModeSchema),
|
availableModes: z.array(ProviderModeSchema),
|
||||||
lastMessage: z.string().nullable().optional(),
|
lastMessage: z.string().nullable().optional(),
|
||||||
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
|
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
|
||||||
|
guidance: z.string().optional(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async (args: unknown) => {
|
async (args: unknown) => {
|
||||||
const { parsedArgs, worktree } = resolveCreateAgentToolArgs(args);
|
const resolvedArgs = resolveCreateAgentToolArgs(args);
|
||||||
const { snapshot, background, initialPromptStarted } = await createAgentCommand(
|
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;
|
||||||
|
} else {
|
||||||
|
requestedBackground = resolvedArgs.parsedArgs.background;
|
||||||
|
notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish ?? false;
|
||||||
|
detached = false;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
snapshot,
|
||||||
|
background: createdInBackground,
|
||||||
|
initialPromptStarted,
|
||||||
|
} = await createAgentCommand(
|
||||||
{
|
{
|
||||||
agentManager,
|
agentManager,
|
||||||
agentStorage,
|
agentStorage,
|
||||||
@@ -892,8 +933,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
features: parsedArgs.settings?.features,
|
features: parsedArgs.settings?.features,
|
||||||
labels: parsedArgs.labels,
|
labels: parsedArgs.labels,
|
||||||
mode: parsedArgs.settings?.modeId,
|
mode: parsedArgs.settings?.modeId,
|
||||||
background: parsedArgs.background ?? false,
|
background: requestedBackground,
|
||||||
notifyOnFinish: parsedArgs.notifyOnFinish ?? false,
|
notifyOnFinish,
|
||||||
|
detached,
|
||||||
callerAgentId,
|
callerAgentId,
|
||||||
callerContext,
|
callerContext,
|
||||||
worktree,
|
worktree,
|
||||||
@@ -901,7 +943,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!background && initialPromptStarted) {
|
if (!createdInBackground && initialPromptStarted) {
|
||||||
const result = await waitForAgentWithTimeout(agentManager, snapshot.id, {
|
const result = await waitForAgentWithTimeout(agentManager, snapshot.id, {
|
||||||
waitForActive: true,
|
waitForActive: true,
|
||||||
});
|
});
|
||||||
@@ -930,8 +972,12 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return immediately if background=true
|
// Return immediately for async creation.
|
||||||
const currentSnapshot = agentManager.getAgent(snapshot.id) ?? snapshot;
|
const currentSnapshot = agentManager.getAgent(snapshot.id) ?? snapshot;
|
||||||
|
const guidance =
|
||||||
|
callerAgentId && notifyOnFinish && initialPromptStarted
|
||||||
|
? "You will get notified when the created agent finishes, errors, or needs permission. Do not call wait_for_agent or poll for status; continue with other work until the notification arrives."
|
||||||
|
: undefined;
|
||||||
const response = {
|
const response = {
|
||||||
content: [],
|
content: [],
|
||||||
structuredContent: ensureValidJson({
|
structuredContent: ensureValidJson({
|
||||||
@@ -943,26 +989,36 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
availableModes: currentSnapshot.availableModes,
|
availableModes: currentSnapshot.availableModes,
|
||||||
lastMessage: null,
|
lastMessage: null,
|
||||||
permission: null,
|
permission: null,
|
||||||
|
...(guidance ? { guidance } : {}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function resolveCreateAgentToolArgs(args: unknown): {
|
type ResolvedCreateAgentToolArgs =
|
||||||
parsedArgs:
|
| {
|
||||||
| z.infer<typeof agentToAgentCreateAgentArgsSchema>
|
kind: "agent-scoped";
|
||||||
| z.infer<typeof topLevelCreateAgentArgsSchema>;
|
parsedArgs: AgentToAgentCreateAgentArgs;
|
||||||
worktree: ReturnType<typeof resolveTopLevelCreateAgentWorktree>;
|
worktree: undefined;
|
||||||
} {
|
}
|
||||||
|
| {
|
||||||
|
kind: "top-level";
|
||||||
|
parsedArgs: TopLevelCreateAgentArgs;
|
||||||
|
worktree: ReturnType<typeof resolveTopLevelCreateAgentWorktree>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveCreateAgentToolArgs(args: unknown): ResolvedCreateAgentToolArgs {
|
||||||
if (callerAgentId) {
|
if (callerAgentId) {
|
||||||
return {
|
return {
|
||||||
|
kind: "agent-scoped",
|
||||||
parsedArgs: agentToAgentCreateAgentArgsSchema.parse(args),
|
parsedArgs: agentToAgentCreateAgentArgsSchema.parse(args),
|
||||||
worktree: undefined,
|
worktree: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const parsedArgs = topLevelCreateAgentArgsSchema.parse(args);
|
const parsedArgs = topLevelCreateAgentArgsSchema.parse(args);
|
||||||
return {
|
return {
|
||||||
|
kind: "top-level",
|
||||||
parsedArgs,
|
parsedArgs,
|
||||||
worktree: resolveTopLevelCreateAgentWorktree(parsedArgs),
|
worktree: resolveTopLevelCreateAgentWorktree(parsedArgs),
|
||||||
};
|
};
|
||||||
@@ -1088,7 +1144,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
.optional()
|
.optional()
|
||||||
.default(false)
|
.default(false)
|
||||||
.describe(
|
.describe(
|
||||||
"Send a notification prompt to the caller agent when this agent finishes, errors, or needs permission.",
|
"Agent-scoped only: get notified when this run finishes, errors, or needs permission.",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
@@ -1402,7 +1458,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Optional working directory. Defaults to the caller agent cwd."),
|
.describe("Optional working directory. Defaults to your current working directory."),
|
||||||
all: z.boolean().optional().describe("List terminals across all working directories."),
|
all: z.boolean().optional().describe("List terminals across all working directories."),
|
||||||
},
|
},
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
@@ -1450,7 +1506,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Optional working directory. Defaults to the caller agent cwd."),
|
.describe("Optional working directory. Defaults to your current working directory."),
|
||||||
name: z.string().optional().describe("Optional terminal name."),
|
name: z.string().optional().describe("Optional terminal name."),
|
||||||
},
|
},
|
||||||
outputSchema: TerminalSummarySchema.shape,
|
outputSchema: TerminalSummarySchema.shape,
|
||||||
@@ -1591,22 +1647,18 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
"create_schedule",
|
"create_schedule",
|
||||||
{
|
{
|
||||||
title: "Create schedule",
|
title: "Create schedule",
|
||||||
description: "Create a recurring schedule that runs on an agent or a new agent.",
|
description: "Create a recurring schedule that starts a new agent on a cron cadence.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
prompt: z.string().trim().min(1, "prompt is required"),
|
prompt: z.string().trim().min(1, "prompt is required"),
|
||||||
every: z.string().optional(),
|
cron: z.string().trim().min(1, "cron is required"),
|
||||||
cron: z.string().optional(),
|
|
||||||
timezone: z
|
timezone: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
||||||
"IANA time zone for cron cadence; requires cron. For example: America/New_York.",
|
|
||||||
),
|
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
target: z.enum(["self", "new-agent"]).optional(),
|
provider: AgentProviderEnum.describe(
|
||||||
provider: AgentProviderEnum.optional().describe(
|
|
||||||
"Provider, or provider/model (for example: codex or codex/gpt-5.4).",
|
"Provider, or provider/model (for example: codex or codex/gpt-5.4).",
|
||||||
),
|
),
|
||||||
cwd: z.string().optional(),
|
cwd: z.string().optional(),
|
||||||
@@ -1615,70 +1667,71 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
},
|
},
|
||||||
outputSchema: ScheduleSummarySchema.shape,
|
outputSchema: ScheduleSummarySchema.shape,
|
||||||
},
|
},
|
||||||
async ({ prompt, every, cron, timezone, name, target, provider, cwd, maxRuns, expiresIn }) => {
|
async ({ prompt, cron, timezone, name, provider, cwd, maxRuns, expiresIn }) => {
|
||||||
if (!scheduleService) {
|
if (!scheduleService) {
|
||||||
throw new Error("Schedule service is not configured");
|
throw new Error("Schedule service is not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedEvery = normalizeScheduleCadenceArg(every);
|
const expiresAt = buildScheduleExpiry(expiresIn);
|
||||||
const normalizedCron = normalizeScheduleCadenceArg(cron);
|
|
||||||
const normalizedTimeZone = normalizeScheduleTimeZoneArg(timezone);
|
|
||||||
const cadenceCount =
|
|
||||||
Number(normalizedEvery !== undefined) + Number(normalizedCron !== undefined);
|
|
||||||
if (cadenceCount !== 1) {
|
|
||||||
throw new Error("Specify exactly one of every or cron");
|
|
||||||
}
|
|
||||||
if (normalizedTimeZone !== undefined && normalizedCron === undefined) {
|
|
||||||
throw new Error("timezone can only be used with cron");
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleTarget =
|
|
||||||
target === "self"
|
|
||||||
? (() => {
|
|
||||||
const callerAgent = resolveCallerAgent();
|
|
||||||
if (!callerAgentId || !callerAgent) {
|
|
||||||
throw new Error("target=self requires a caller agent");
|
|
||||||
}
|
|
||||||
const trimmedCwd = cwd?.trim();
|
|
||||||
if (trimmedCwd && expandUserPath(trimmedCwd) !== callerAgent.cwd) {
|
|
||||||
throw new Error("cwd can only differ from the caller agent when target=new-agent");
|
|
||||||
}
|
|
||||||
if (provider !== undefined) {
|
|
||||||
const resolved = resolveScheduleProviderAndModel({
|
|
||||||
provider,
|
|
||||||
defaultProvider: callerAgent.provider,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
resolved.provider !== callerAgent.provider ||
|
|
||||||
(resolved.model !== undefined && resolved.model !== callerAgent.config.model)
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"provider can only differ from the caller agent when target=new-agent",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { type: "agent" as const, agentId: callerAgentId };
|
|
||||||
})()
|
|
||||||
: (() => {
|
|
||||||
return resolveNewAgentScheduleTarget({ provider, cwd });
|
|
||||||
})();
|
|
||||||
|
|
||||||
const schedule = await scheduleService.create({
|
const schedule = await scheduleService.create({
|
||||||
prompt: prompt.trim(),
|
prompt: prompt.trim(),
|
||||||
cadence:
|
cadence: buildCronScheduleCadence({
|
||||||
normalizedEvery !== undefined
|
cron,
|
||||||
? { type: "every" as const, everyMs: parseDurationString(normalizedEvery) }
|
...(timezone !== undefined ? { timezone } : {}),
|
||||||
: {
|
}),
|
||||||
type: "cron" as const,
|
target: resolveNewAgentScheduleTarget({ provider, cwd }),
|
||||||
expression: normalizedCron!,
|
|
||||||
...(normalizedTimeZone !== undefined ? { timezone: normalizedTimeZone } : {}),
|
|
||||||
},
|
|
||||||
target: scheduleTarget,
|
|
||||||
...(name?.trim() ? { name: name.trim() } : {}),
|
...(name?.trim() ? { name: name.trim() } : {}),
|
||||||
...(maxRuns === undefined ? {} : { maxRuns }),
|
...(maxRuns === undefined ? {} : { maxRuns }),
|
||||||
...(expiresIn === undefined
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
||||||
? {}
|
});
|
||||||
: { expiresAt: new Date(Date.now() + parseDurationString(expiresIn)).toISOString() }),
|
|
||||||
|
return {
|
||||||
|
content: [],
|
||||||
|
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
registerTool(
|
||||||
|
"create_heartbeat",
|
||||||
|
{
|
||||||
|
title: "Create heartbeat",
|
||||||
|
description: "Create a recurring heartbeat that sends you a prompt on a cron cadence.",
|
||||||
|
inputSchema: {
|
||||||
|
prompt: z.string().trim().min(1, "prompt is required"),
|
||||||
|
cron: z.string().trim().min(1, "cron is required"),
|
||||||
|
timezone: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.optional()
|
||||||
|
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
||||||
|
name: z.string().optional(),
|
||||||
|
maxRuns: z.number().int().positive().optional(),
|
||||||
|
expiresIn: z.string().optional(),
|
||||||
|
},
|
||||||
|
outputSchema: ScheduleSummarySchema.shape,
|
||||||
|
},
|
||||||
|
async ({ prompt, cron, timezone, name, maxRuns, expiresIn }) => {
|
||||||
|
if (!scheduleService) {
|
||||||
|
throw new Error("Schedule service is not configured");
|
||||||
|
}
|
||||||
|
if (!callerAgentId) {
|
||||||
|
throw new Error("create_heartbeat requires an agent-scoped session");
|
||||||
|
}
|
||||||
|
resolveCallerAgent();
|
||||||
|
|
||||||
|
const expiresAt = buildScheduleExpiry(expiresIn);
|
||||||
|
const schedule = await scheduleService.create({
|
||||||
|
prompt: prompt.trim(),
|
||||||
|
cadence: buildCronScheduleCadence({
|
||||||
|
cron,
|
||||||
|
...(timezone !== undefined ? { timezone } : {}),
|
||||||
|
}),
|
||||||
|
target: { type: "agent", agentId: callerAgentId },
|
||||||
|
...(name?.trim() ? { name: name.trim() } : {}),
|
||||||
|
...(maxRuns === undefined ? {} : { maxRuns }),
|
||||||
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -2027,7 +2080,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Optional repository cwd. Defaults to the caller agent cwd."),
|
.describe("Optional repository cwd. Defaults to your current working directory."),
|
||||||
},
|
},
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
worktrees: z.array(WorktreeSummarySchema),
|
worktrees: z.array(WorktreeSummarySchema),
|
||||||
@@ -2131,7 +2184,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
|
|||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe("Optional repository cwd. Defaults to the caller agent cwd."),
|
.describe("Optional repository cwd. Defaults to your current working directory."),
|
||||||
worktreePath: z.string().optional(),
|
worktreePath: z.string().optional(),
|
||||||
worktreeSlug: z.string().optional(),
|
worktreeSlug: z.string().optional(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Single agent. Reads the situation you're in. Gives a judgment. You decide what t
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Read the **paseo** skill — provider for the advisor comes from orchestration preferences unless the user names one.
|
Read the **paseo** skill. Before choosing a provider, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named a provider in this request. Do not create the advisor until you have read it.
|
||||||
|
|
||||||
## Picking the advisor
|
## Picking the advisor
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ The purpose is to step back, not double down. The committee may propose a comple
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Read the **paseo** skill. Contrast is the point of a committee, so pick across providers deliberately rather than using whatever the default category would resolve to.
|
Read the **paseo** skill. Before choosing committee members, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named providers in this request. Do not create committee agents until you have read it.
|
||||||
|
|
||||||
|
Contrast is the point of a committee, so pick across providers deliberately using the configured preferences rather than hardcoded defaults.
|
||||||
|
|
||||||
## Composition
|
## Composition
|
||||||
|
|
||||||
Two members with different reasoning styles:
|
Two members with different reasoning styles, selected from orchestration preferences:
|
||||||
|
|
||||||
- **Claude Opus** with extended thinking on
|
- one planning/research-strength provider
|
||||||
- **Codex GPT-5.4** with thinking on
|
- one contrasting high-reasoning provider
|
||||||
|
|
||||||
Override only when the user explicitly asks for different members.
|
Override only when the user explicitly asks for different members.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ This usually runs for hours, often overnight. By the time agents are working, yo
|
|||||||
|
|
||||||
This is a Paseo skill. Load the **paseo** skill first — it carries the surface (worktrees, agents, waiting, scheduling, preferences). Every agent you spawn reads it too.
|
This is a Paseo skill. Load the **paseo** skill first — it carries the surface (worktrees, agents, waiting, scheduling, preferences). Every agent you spawn reads it too.
|
||||||
|
|
||||||
|
Before choosing any provider, read `~/.paseo/orchestration-preferences.json`. Do not create planner, reviewer, researcher, implementer, or auditor agents until you have read it.
|
||||||
|
|
||||||
**Do not use your own subagents.** All agents in this skill are Paseo agents, spawned through the Paseo MCP. Your harness's native subagent stack is not in play here.
|
**Do not use your own subagents.** All agents in this skill are Paseo agents, spawned through the Paseo MCP. Your harness's native subagent stack is not in play here.
|
||||||
|
|
||||||
The role and phase-type vocabulary lives in the roles reference shipped with this skill (`references/roles.md`).
|
The role and phase-type vocabulary lives in the roles reference shipped with this skill (`references/roles.md`).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Transfer the current task — context, decisions, failed attempts, constraints
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Read the **paseo** skill — provider for the receiving agent comes from orchestration preferences unless the user names one.
|
Read the **paseo** skill. Before choosing a provider, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named a provider in this request. Do not create the receiving agent until you have read it.
|
||||||
|
|
||||||
## Parsing arguments
|
## Parsing arguments
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ The receiving agent has zero context. Include:
|
|||||||
[Imperative description.]
|
[Imperative description.]
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
[Why this task exists, background needed.]
|
[Why this task exists, required context.]
|
||||||
|
|
||||||
## Relevant files
|
## Relevant files
|
||||||
- `path/to/file.ts` — [what it is and why it matters]
|
- `path/to/file.ts` — [what it is and why it matters]
|
||||||
@@ -54,6 +54,8 @@ The receiving agent has zero context. Include:
|
|||||||
|
|
||||||
## Launch
|
## Launch
|
||||||
|
|
||||||
Create the agent via Paseo with a `[Handoff] <task>` title, the briefing as initial prompt, and cwd set to the worktree path if `--worktree`.
|
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.
|
||||||
|
|
||||||
|
Handoff agents are siblings/root agents, not your subagents. They must survive you being archived and must not appear in your subagent track.
|
||||||
|
|
||||||
Don't wait by default — the user decides whether to follow along or move on. Tell them the agent ID and how to follow along (the paseo skill explains).
|
Don't wait by default — the user decides whether to follow along or move on. Tell them the agent ID and how to follow along (the paseo skill explains).
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ A loop is a worker/verifier cycle: launch a worker → check verification → re
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Read the **paseo** skill for orchestration preferences — worker and verifier providers come from preferences unless the user names them.
|
Read the **paseo** skill. Before choosing worker or verifier providers, read `~/.paseo/orchestration-preferences.json` unless the user explicitly named providers in this request. Do not start the loop until you have read it.
|
||||||
|
|
||||||
Loops are a CLI primitive: `paseo loop run`. Manage with `paseo loop ls`, `paseo loop inspect <id>`, `paseo loop logs <id>`, `paseo loop stop <id>`.
|
Loops are a CLI primitive: `paseo loop run`. Manage with `paseo loop ls`, `paseo loop inspect <id>`, `paseo loop logs <id>`, `paseo loop stop <id>`.
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,21 @@ Returns `{ branchName, worktreePath }`. Pass `cwd` to target a specific repo.
|
|||||||
|
|
||||||
## Agents
|
## Agents
|
||||||
|
|
||||||
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`, `settings`. Returns `{ agentId, … }`.
|
**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `notifyOnFinish`, `settings`, `detached`. 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.
|
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`.
|
Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the returned `worktreePath`.
|
||||||
|
|
||||||
**`send_agent_prompt`** — `{ agentId, prompt }`. Blocks by default; pass `background: true` to fire-and-forget.
|
### 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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**`send_agent_prompt`** — `{ agentId, prompt }`. Use for follow-ups to an existing agent.
|
||||||
|
|
||||||
**`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 } }`.
|
**`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 } }`.
|
||||||
|
|
||||||
@@ -44,9 +52,11 @@ Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the
|
|||||||
|
|
||||||
Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look for `fast_mode` and pass `settings: { features: { "fast_mode": true } }` to `create_agent` or `update_agent`.
|
Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look for `fast_mode` and pass `settings: { features: { "fast_mode": true } }` to `create_agent` or `update_agent`.
|
||||||
|
|
||||||
## Heartbeats
|
## Schedules and heartbeats
|
||||||
|
|
||||||
**`create_schedule`** — required: `prompt`. Pick one of `cron` or `every` (`"5m"`, `"1h"`). Optional: `name`, `target` (`self` | `new-agent`), `provider`, `maxRuns`, `expiresIn`. Use for periodic checks on long-running work or recurring maintenance.
|
**`create_schedule`** — starts a new agent on a cron cadence. Required: `prompt`, `cron`, `provider`. Optional: `timezone`, `name`, `cwd`, `maxRuns`, `expiresIn`. Use when the recurring work should live in fresh agents.
|
||||||
|
|
||||||
|
**`create_heartbeat`** — sends you a prompt on a cron cadence. Required: `prompt`, `cron`. Optional: `timezone`, `name`, `maxRuns`, `expiresIn`. Use for reminders, PR/build babysitting, and status checks that should return to this conversation.
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
|
|
||||||
@@ -54,7 +64,7 @@ Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look f
|
|||||||
|
|
||||||
## Orchestration preferences
|
## Orchestration preferences
|
||||||
|
|
||||||
User-specific configuration at `~/.paseo/orchestration-preferences.json`. **Any paseo skill that picks an agent reads this file.** Never hardcode a provider string in another skill — resolve through this file.
|
User-specific configuration at `~/.paseo/orchestration-preferences.json`. **Before any Paseo skill chooses a provider or creates an agent, it must read this file.** Reading means an actual file read, not relying on these examples or defaults. Never hardcode a provider string in another skill — resolve through this file.
|
||||||
|
|
||||||
Two parts:
|
Two parts:
|
||||||
|
|
||||||
@@ -84,7 +94,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.
|
Agents take time — 10–30+ minutes is routine. Favor asynchronous workflows.
|
||||||
|
|
||||||
For every `create_agent` or `send_agent_prompt`, pass `background: true` and `notifyOnFinish: true`. Paseo delivers a notification to your conversation when the 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 `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.
|
||||||
|
|
||||||
Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you.
|
Don't poll `list_agents` or `get_agent_status` to "check on" a running agent. The notification will tell you.
|
||||||
|
|
||||||
@@ -97,7 +107,7 @@ paseo run --provider codex/gpt-5.4 --mode full-access --worktree feat/x "<prompt
|
|||||||
paseo send <agent-id> "<follow-up>"
|
paseo send <agent-id> "<follow-up>"
|
||||||
paseo ls
|
paseo ls
|
||||||
paseo worktree ls
|
paseo worktree ls
|
||||||
paseo schedule create --every 5m "ping main build"
|
paseo schedule create --cron "*/15 * * * *" "ping main build"
|
||||||
```
|
```
|
||||||
|
|
||||||
Discover with `paseo --help` and `paseo <cmd> --help`.
|
Discover with `paseo --help` and `paseo <cmd> --help`.
|
||||||
|
|||||||
Reference in New Issue
Block a user