@sgtbeatdown/pi-tasks 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,75 +1,44 @@
1
1
  ---
2
2
  name: wolfpack-pi-task-delegation
3
- description: Use this whenever the user asks to open, spawn, create, or use a Wolfpack/Pi subagent to do work, delegate work to another session, or check delegated task results. This skill connects existing Wolfpack session-control knowledge with the agent_task communication tools so agents spawn/select sessions, send work with agent_task_send, and avoid terminal-output completion polling.
3
+ description: Use when opening, selecting, delegating to, checking, or cleaning up a Wolfpack Pi subagent through the gateway-backed agent task tools.
4
4
  ---
5
5
 
6
- # Wolfpack Pi Task Delegation
6
+ # wolfpack pi task delegation
7
7
 
8
- This skill is the Wolfpack transport guidance for the task communication layer. It does not replace `wolfpack-tailnet-control`; use that
9
- skill/instructions for opening, selecting, inspecting, or controlling Wolfpack
10
- sessions.
8
+ use `wolfpack-tailnet-control` for session control. this skill covers gateway task lifecycle, curated context, and parent verification. Wolfpack's [task gateway guide](https://github.com/almogdepaz/wolfpack/blob/main/docs/task-gateway.md) is the canonical route, trust, retention, and federation reference.
11
9
 
12
- ## Goal
10
+ ## gateway requirements and addressing
13
11
 
14
- When the user says something like “open a subagent and do X”, create or select a
15
- Wolfpack/Pi session using existing Wolfpack control, then send X through the
16
- structured `agent_task_*` task communication tools. Keep Wolfpack terminals
17
- visible and steerable, but treat task state/results as the protocol.
12
+ - every participating Pi process needs this package and a reachable local Wolfpack v1 gateway.
13
+ - `WOLFPACK_SESSION_NAME` is the caller selector; the local gateway resolves it to a stable broker ID. `WOLFPACK_PORT` selects the local port and defaults to `18790`.
14
+ - address a target as `to: { machine, sessionId }`, retaining the stable broker `sessionId` from structured Wolfpack session control rather than a terminal label.
15
+ - use `machine: "local"` for same-machine work. For remote work, use only the receiver's canonical HTTPS Tailnet origin, such as `https://worker.example.ts.net`; never a terminal name, arbitrary URL, path, query, fragment, credentials, or unexpected port.
16
+ - v1 trusts local processes and trusted Tailnet machines. Pi calls only its local gateway; gateways use direct fetch federation. JWT federation is unsupported, so do not promise remote delivery where Wolfpack global JWT requires credentials.
18
17
 
19
- ## Workflow
18
+ ## workflow
20
19
 
21
- 1. If the request includes opening/spawning/creating a subagent, use the
22
- existing Wolfpack control workflow (`wolfpack-tailnet-control`) to create the
23
- session. Prefer the canonical CLI path:
20
+ 1. create or select an existing Pi session with canonical Wolfpack session control. Read its structured response and retain the stable broker `sessionId`.
21
+ 2. send concise opaque task instructions with `agent_task_send`. Parent normally authors optional Markdown `context.summary` and deliberately selects `context.refs` with `path`, optional `selector`, and `purpose`; refs are metadata, not copied files or transcript. Use `task-context-summary` only for recovery/reuse.
22
+ 3. include `role`, `metadata`, enforceable `preflight.requiredProject`, bounded timeout, idempotency key, and `onCompletePrompt` only when they apply. Timeout is 1000ms through 24h; default is 30 minutes. Task/context limits are 16 KiB each, assignment envelope 48 KiB, and request body 64 KiB.
23
+ 4. after durable receipt, keep working. Do not call `agent_task_wait` unless the user explicitly asks to block. Use `agent_task_status` or `agent_task_inbox` for structured follow-up.
24
+ 5. use `agent_task_message` for durable `question`, `answer`, or `information` flow. One question may be unresolved globally; answers link to it. A receiver question accepted by the sender ends the receiver turn; a parent question does not end the parent turn.
25
+ 6. assignees call `agent_task_done` exactly once as their final action with `completed`, `failed`, or `cancelled`, concise summary, and optional structured result/error/paths-only artifact metadata. No prose completion afterward.
26
+ 7. parent independently verifies files, diff, tests, and artifacts before reporting success. Then use `agent_task_inbox({ ack: true })`; remote acknowledgment is two-phase and failed propagation leaves the task visible for explicit repair. Cleanup applies only to sessions this parent spawned and only after result verification and acknowledgment. Retain reusable sessions while review or correction is pending.
24
27
 
25
- ```bash
26
- wolfpack agent spawn <project> --prompt 'you are a task worker. wait for structured pi.task.assignment.v1 assignments; finish assigned work only with agent_task_done.' --json
27
- ```
28
+ ## delivery and recovery
28
29
 
29
- `wolfpack session open <project> --prompt ... --json` is a deprecated alias;
30
- use it only when that is the available command in the environment.
30
+ `agent_task_send` returns after durable receiver-gateway receipt, not model execution. A remote initial send gets one initial attempt. The receiver stores a provisional receipt, and Pi sees the assignment only after sender receipt confirmation. Later peer events get four total attempts: initial plus retries around 1, 2, and 4 seconds with jitter. Retry exhaustion is a surfaced local delivery failure; v1 has no queue, scheduler, or offline dispatch.
31
31
 
32
- 2. Parse the structured JSON response from Wolfpack for the created session
33
- handle/name/id. Treat it as opaque. Do not infer the target from browser UI
34
- labels, terminal prose, or pane previews.
32
+ The sender gateway owns canonical order, expiry, and terminal choice; the first accepted terminal event wins. Sender timeout causes best-effort remote cancellation. The local extension polls every five seconds and injects only while idle with no pending messages. It stores `{ taskId, eventId }` in structured custom messages, replays missing events after restart, and records delivery only after structural insertion. Do not call that exactly-once execution.
35
33
 
36
- 3. Send the real work with `agent_task_send` to the selected/spawned session.
37
- The task text should be the user’s requested work, not the bootstrap prompt.
34
+ A real second peer is not currently available for live operational verification. Isolated two-server tests cover the direct peer protocol, but do not claim that a particular remote deployment is reachable.
38
35
 
39
- 4. Return immediately with the task id and target session unless the user asked
40
- to wait. Continue local work if there is other useful work to do.
36
+ ## do not
41
37
 
42
- 5. Use structured task tools for follow-up:
43
- - `agent_task_status` for one task
44
- - `agent_task_wait` only when the user wants the result now
45
- - `agent_task_inbox` to check completed delegated work
46
- - `agent_task_cancel` to cancel non-terminal tasks
47
-
48
- 6. In target sessions, finish assigned tasks with `agent_task_done`. After
49
- `agent_task_done`, do not send extra prose; the tool result is the completion
50
- channel.
51
-
52
- ## Do Not
53
-
54
- - Do not poll terminal output to decide that delegated work is complete.
55
- - Do not ask the target to report completion in prose.
56
- - Do not use `wolfpack session wait` for task completion; it waits for literal
57
- terminal text and is the wrong protocol for this package.
58
- - Do not duplicate Wolfpack session-control rules here. If session control is
59
- ambiguous, consult/use `wolfpack-tailnet-control`.
60
-
61
- ## Examples
62
-
63
- User: “open a subagent and inspect the auth middleware”
64
-
65
- Expected approach:
66
- 1. Spawn a Wolfpack child session with the existing Wolfpack control workflow.
67
- 2. Call `agent_task_send` with the spawned session as `to` and “inspect the auth
68
- middleware” as `task`.
69
- 3. Tell the user the task id and that you’ll check structured results later.
70
-
71
- User: “check on the subagent”
72
-
73
- Expected approach:
74
- - Use `agent_task_inbox` or `agent_task_status`; do not read terminal prose to
75
- infer done-ness.
38
+ - do not use terminal output as task state or completion evidence.
39
+ - do not ask workers to complete in prose.
40
+ - do not inject task context while Pi is busy or has pending messages.
41
+ - do not copy whole plans, ref contents, or transcripts into task context.
42
+ - do not use filesystem task storage or retain old `mustReturn`, `rejected`, or semantic completion contracts.
43
+ - do not promise JWT federation, artifact byte transfer, or background retry/offline dispatch.
44
+ - do not make workers close their own sessions.
package/src/extension.ts CHANGED
@@ -3,306 +3,220 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-
3
3
  import { Text } from "@earendil-works/pi-tui";
4
4
  import { Type } from "typebox";
5
5
 
6
- import { buildBackgroundTaskNotificationPrompt, selectUnpromptedTerminalTasks } from "./auto-notify";
7
- import type { TaskCommunicationLayer } from "./task-communication";
8
- import { createFilesystemTaskStore } from "./stores/filesystem";
9
- import { createWolfpackTaskTransport } from "./transports/wolfpack";
10
- import { buildAssignment, compactTaskResult } from "./protocol";
11
- import type { AgentTaskRecord, TaskResultPayload, TerminalTaskStatus } from "./types";
12
- import { DEFAULT_TIMEOUT_MS, TASK_PROTOCOL_VERSION } from "./types";
13
-
6
+ import {
7
+ DEFAULT_TASK_TIMEOUT_MS,
8
+ GatewayClientError,
9
+ MAX_TASK_TIMEOUT_MS,
10
+ MIN_TASK_TIMEOUT_MS,
11
+ createWolfpackGatewayClient,
12
+ type TaskCompletionInput,
13
+ type TaskStatus,
14
+ type WolfpackGatewayClient,
15
+ } from "./gateway-client";
16
+ import { deliverTaskInbox, restoredInboxCursor } from "./task-inbox";
17
+
18
+ const BACKGROUND_POLL_MS = 5_000;
14
19
  const WAIT_POLL_MS = 250;
15
- const BACKGROUND_POLL_MS = 5000;
16
-
17
- const SendParams = Type.Object({
18
- to: Type.String({ description: "Target session, worker, or transport-specific address" }),
19
- task: Type.String({ description: "Task instructions to send to the target" }),
20
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 86_400_000 })),
21
- idempotencyKey: Type.Optional(Type.String()),
22
- });
20
+ const SUMMARY_MAX_CHARS = 1_200;
23
21
 
24
- const TaskIdParams = Type.Object({
25
- taskId: Type.String({ description: "Task id, e.g. task_..." }),
22
+ const AddressParams = Type.Object({
23
+ machine: Type.String({ minLength: 1, description: "local or a canonical Wolfpack machine identity" }),
24
+ sessionId: Type.String({ minLength: 1, description: "Stable opaque Wolfpack broker session id" }),
26
25
  });
27
26
 
28
- const WaitParams = Type.Object({
29
- taskId: Type.String({ description: "Task id, e.g. task_..." }),
30
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 86_400_000 })),
27
+ const ContextRefParams = Type.Object({
28
+ path: Type.String({ minLength: 1 }),
29
+ selector: Type.Optional(Type.String()),
30
+ purpose: Type.Optional(Type.String()),
31
31
  });
32
32
 
33
- const InboxParams = Type.Object({
34
- ack: Type.Optional(Type.Boolean({ default: false })),
35
- includeAcknowledged: Type.Optional(Type.Boolean({ default: false })),
33
+ const SendParams = Type.Object({
34
+ to: AddressParams,
35
+ task: Type.String({ minLength: 1, maxLength: 16 * 1024 }),
36
+ context: Type.Optional(Type.Object({
37
+ summary: Type.Optional(Type.String({ minLength: 1, maxLength: 16 * 1024 })),
38
+ refs: Type.Optional(Type.Array(ContextRefParams)),
39
+ })),
40
+ role: Type.Optional(Type.String()),
41
+ preflight: Type.Optional(Type.Object({ requiredProject: Type.Optional(Type.String()) })),
42
+ metadata: Type.Optional(Type.Object({
43
+ phaseId: Type.Optional(Type.String()),
44
+ issueId: Type.Optional(Type.String()),
45
+ verificationTier: Type.Optional(Type.String()),
46
+ rootCause: Type.Optional(Type.String()),
47
+ })),
48
+ onCompletePrompt: Type.Optional(Type.String({ maxLength: 4_000 })),
49
+ timeoutMs: Type.Optional(Type.Integer({ minimum: MIN_TASK_TIMEOUT_MS, maximum: MAX_TASK_TIMEOUT_MS })),
50
+ idempotencyKey: Type.Optional(Type.String({ minLength: 1 })),
36
51
  });
37
52
 
38
- const CancelParams = Type.Object({
39
- taskId: Type.String({ description: "Task id, e.g. task_..." }),
40
- reason: Type.Optional(Type.String()),
53
+ const TaskIdParams = Type.Object({ taskId: Type.String({ minLength: 1 }) });
54
+ const WaitParams = Type.Object({ taskId: Type.String({ minLength: 1 }), timeoutMs: Type.Optional(Type.Integer({ minimum: MIN_TASK_TIMEOUT_MS, maximum: MAX_TASK_TIMEOUT_MS })) });
55
+ const InboxParams = Type.Object({ ack: Type.Optional(Type.Boolean({ default: false })), includeAcknowledged: Type.Optional(Type.Boolean({ default: false })) });
56
+ const MessageParams = Type.Object({
57
+ taskId: Type.String({ minLength: 1 }),
58
+ type: StringEnum(["question", "answer", "information"] as const),
59
+ message: Type.String({ minLength: 1, maxLength: 16 * 1024 }),
60
+ replyToMessageId: Type.Optional(Type.String({ minLength: 1 })),
41
61
  });
42
-
43
62
  const DoneParams = Type.Object({
44
- taskId: Type.String({ description: "Task id from the assignment" }),
45
- status: StringEnum(["completed", "failed", "cancelled", "rejected"] as const),
46
- summary: Type.String({ description: "Compact summary, max 1200 chars" }),
47
- error: Type.Optional(
48
- Type.Object({
49
- code: Type.String(),
50
- message: Type.String(),
51
- retryable: Type.Boolean(),
52
- }),
53
- ),
63
+ taskId: Type.String({ minLength: 1 }),
64
+ status: StringEnum(["completed", "failed", "cancelled"] as const),
65
+ summary: Type.String({ minLength: 1, maxLength: SUMMARY_MAX_CHARS }),
54
66
  result: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
55
- artifacts: Type.Optional(
56
- Type.Array(
57
- Type.Object({
58
- name: Type.String(),
59
- path: Type.Optional(Type.String()),
60
- mimeType: Type.Optional(Type.String()),
61
- description: Type.Optional(Type.String()),
62
- }),
63
- ),
64
- ),
67
+ error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String(), retryable: Type.Boolean() })),
68
+ artifacts: Type.Optional(Type.Array(Type.Object({ path: Type.String({ minLength: 1 }), mimeType: Type.Optional(Type.String()), description: Type.Optional(Type.String()) }))),
65
69
  });
66
70
 
67
- export function createDefaultTaskCommunicationLayer(pi: ExtensionAPI): TaskCommunicationLayer {
68
- return {
69
- store: createFilesystemTaskStore(),
70
- transport: createWolfpackTaskTransport({
71
- exec: (command, args, options) => pi.exec(command, [...args], options),
72
- }),
73
- };
74
- }
75
-
76
- export function registerAgentTaskTools(pi: ExtensionAPI, communication: TaskCommunicationLayer): void {
77
- const { store, transport } = communication;
71
+ export function registerAgentTaskTools(pi: ExtensionAPI, client: WolfpackGatewayClient = createWolfpackGatewayClient()): void {
78
72
  let backgroundTimer: ReturnType<typeof setInterval> | undefined;
79
- const notifiedTaskIds = new Set<string>();
80
- const autoPromptedTaskIds = new Set<string>();
81
73
 
82
- pi.on("session_start", async (_event, ctx) => {
83
- const sessionName = transport.getCurrentSessionName(process.env);
84
- ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("dim", `tasks: ${sessionName}`));
74
+ const refreshInbox = async (ctx: ExtensionContext): Promise<void> => {
75
+ const cursor = restoredInboxCursor(ctx.sessionManager.getEntries());
76
+ await deliverTaskInbox(pi, client, ctx, cursor);
77
+ };
85
78
 
79
+ pi.on("session_start", async (_event, ctx) => {
80
+ ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("dim", `tasks: ${client.callerSession}`));
86
81
  backgroundTimer = setInterval(() => {
87
- void refreshInboxStatus(ctx.cwd, sessionName, ctx).catch(() => {
88
- ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("warning", "tasks: unavailable"));
89
- });
82
+ void refreshInbox(ctx).catch(() => ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("warning", "tasks: unavailable")));
90
83
  }, BACKGROUND_POLL_MS);
91
-
92
- await refreshInboxStatus(ctx.cwd, sessionName, ctx).catch(() => undefined);
84
+ await refreshInbox(ctx).catch(() => undefined);
93
85
  });
94
-
95
86
  pi.on("agent_end", async (_event, ctx) => {
96
- const sessionName = transport.getCurrentSessionName(process.env);
97
- setTimeout(() => {
98
- void refreshInboxStatus(ctx.cwd, sessionName, ctx).catch(() => undefined);
99
- }, 0);
87
+ setTimeout(() => { void refreshInbox(ctx).catch(() => undefined); }, 0);
100
88
  });
101
-
102
- pi.on("session_shutdown", async () => {
103
- if (backgroundTimer) {
104
- clearInterval(backgroundTimer);
105
- backgroundTimer = undefined;
106
- }
89
+ pi.on("session_shutdown", () => {
90
+ if (backgroundTimer) clearInterval(backgroundTimer);
91
+ backgroundTimer = undefined;
107
92
  });
108
93
 
109
- async function refreshInboxStatus(projectDir: string, sessionName: string, ctx: ExtensionContext): Promise<void> {
110
- const inbox = await store.listInbox(projectDir, sessionName, { includeAcknowledged: false });
111
- const count = inbox.length;
112
- ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg(count > 0 ? "accent" : "dim", `tasks: ${count} inbox`));
113
- for (const task of inbox) {
114
- if (notifiedTaskIds.has(task.id)) continue;
115
- notifiedTaskIds.add(task.id);
116
- ctx.ui.notify(`task ${task.id} ${task.status}: ${task.targetSession}`, task.status === "completed" ? "info" : "warning");
117
- }
118
- triggerIdleInboxPrompt(inbox, ctx);
119
- }
120
-
121
- function triggerIdleInboxPrompt(inbox: readonly AgentTaskRecord[], ctx: ExtensionContext): void {
122
- if (!ctx.isIdle() || ctx.hasPendingMessages()) return;
123
-
124
- const tasksToPrompt = selectUnpromptedTerminalTasks(inbox, autoPromptedTaskIds);
125
- if (tasksToPrompt.length === 0) return;
126
-
127
- try {
128
- pi.sendUserMessage(buildBackgroundTaskNotificationPrompt(tasksToPrompt));
129
- for (const task of tasksToPrompt) {
130
- autoPromptedTaskIds.add(task.id);
131
- }
132
- } catch {
133
- ctx.ui.notify("task results ready; parent is busy, will retry", "info");
134
- }
135
- }
136
-
137
94
  pi.registerTool({
138
- name: "agent_task_send",
139
- label: "Send Agent Task",
140
- description: "Send a structured task to another agent/session/worker and return immediately without waiting.",
141
- promptSnippet: "Send nonblocking structured tasks to other agents/sessions/workers",
142
- promptGuidelines: [
143
- "Use agent_task_send instead of natural-language polling when delegating work to another agent/session/worker",
144
- "After agent_task_send returns, keep working unless the user explicitly asks to wait for the task.",
145
- ],
95
+ name: "agent_task_send", label: "Send Agent Task", description: "Durably send a bounded task to an existing Wolfpack Pi session.",
96
+ promptSnippet: "Send nonblocking structured tasks through the local Wolfpack gateway",
97
+ promptGuidelines: ["Use agent_task_send for normal nonblocking delegation; do not wait unless the user explicitly asks."],
146
98
  parameters: SendParams,
147
- async execute(_toolCallId, params, signal, _onUpdate, ctx) {
148
- const parentSession = transport.getCurrentSessionName(process.env);
149
-
150
- const createdTask = await store.createOrReuseDispatchedTask({
151
- projectDir: ctx.cwd,
152
- parentSession,
153
- targetSession: params.to,
154
- taskText: params.task,
155
- assignment: (taskId: string) => buildAssignment({ taskId, fromSession: parentSession, instructions: params.task }),
156
- timeoutMs: params.timeoutMs ?? DEFAULT_TIMEOUT_MS,
157
- idempotencyKey: params.idempotencyKey,
158
- targetTaskProtocol: TASK_PROTOCOL_VERSION,
159
- });
160
- const task = createdTask.task;
161
- if (!createdTask.created) {
162
- return taskToolResult(compactTaskResult(task, await store.readTaskResult(ctx.cwd, task.id)));
163
- }
164
-
165
- const assignment = buildAssignment({ taskId: task.id, fromSession: parentSession, instructions: params.task });
166
- const dispatch = await transport.dispatchTask({ projectDir: ctx.cwd, task, target: params.to, assignment, signal });
167
- if (!dispatch.ok) {
168
- const rejected = await store.completeTask(ctx.cwd, task.id, "rejected", {
169
- summary: `dispatch failed: ${dispatch.message}`,
170
- error: {
171
- code: "dispatch_failed",
172
- message: dispatch.message,
173
- retryable: dispatch.retryable,
174
- },
175
- });
176
- return taskToolResult(compactTaskResult(rejected, await store.readTaskResult(ctx.cwd, task.id)));
177
- }
178
-
179
- return taskToolResult({
180
- schemaVersion: 1,
181
- taskId: task.id,
182
- status: task.status,
183
- summary: `dispatched to ${params.to}`,
184
- artifacts: [task.assignmentRef].filter((value): value is string => Boolean(value)),
185
- error: null,
186
- });
187
- },
188
- renderResult(result, _options, theme) {
189
- return new Text(theme.fg("accent", getText(result)));
190
- },
99
+ async execute(_toolCallId, params, signal) {
100
+ try {
101
+ const receipt = await client.send(params, signal);
102
+ return toolResult(receipt, `## task received\n- task: \`${receipt.taskId}\`\n- target: \`${params.to.machine}/${params.to.sessionId}\`\n${warnings(receipt.warnings)}`);
103
+ } catch (error) { return clientError(error); }
104
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
191
105
  });
192
106
 
193
107
  pi.registerTool({
194
- name: "agent_task_status",
195
- label: "Task Status",
196
- description: "Read compact structured status for a task.",
197
- parameters: TaskIdParams,
198
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
199
- const task = await store.expireTaskIfOverdue(ctx.cwd, params.taskId);
200
- return taskToolResult(compactTaskResult(task, await store.readTaskResult(ctx.cwd, params.taskId)));
201
- },
108
+ name: "agent_task_status", label: "Task Status", description: "Read compact structured task status from Wolfpack.", parameters: TaskIdParams,
109
+ async execute(_toolCallId, params, signal) {
110
+ try { const status = await client.status(params.taskId, signal); return statusResult(status); } catch (error) { return clientError(error); }
111
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
202
112
  });
203
113
 
204
114
  pi.registerTool({
205
- name: "agent_task_wait",
206
- label: "Wait Agent Task",
207
- description: "Wait in tool code for a task to become terminal. Use only when the user wants the result now.",
208
- parameters: WaitParams,
209
- async execute(_toolCallId, params, _signal, onUpdate, ctx) {
210
- const waitMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
211
- const sessionName = transport.getCurrentSessionName(process.env);
212
- const progressTimer = setInterval(() => {
213
- onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
214
- }, 2000);
115
+ name: "agent_task_wait", label: "Wait Agent Task", description: "Block only when explicitly invoked until a task reaches a terminal state or this wait times out.", parameters: WaitParams,
116
+ async execute(_toolCallId, params, signal, onUpdate) {
117
+ const deadline = Date.now() + (params.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS);
215
118
  try {
216
- const task = await store.waitForTask(ctx.cwd, params.taskId, { timeoutMs: waitMs, pollMs: WAIT_POLL_MS, ackParentSession: sessionName });
217
- return taskToolResult(compactTaskResult(task, await store.readTaskResult(ctx.cwd, params.taskId)));
218
- } catch (error) {
219
- const task = await store.readTask(ctx.cwd, params.taskId);
220
- return taskToolResult({
221
- schemaVersion: 1,
222
- taskId: task.id,
223
- status: task.status,
224
- summary: error instanceof Error ? error.message : "task wait failed",
225
- artifacts: task.resultRef ? [task.resultRef] : [],
226
- error: { code: "wait_timeout", message: "task wait timed out", retryable: true },
227
- });
228
- } finally {
229
- clearInterval(progressTimer);
230
- }
231
- },
119
+ for (;;) {
120
+ throwIfAborted(signal);
121
+ const status = await client.status(params.taskId, signal);
122
+ if (terminal(status.status)) return statusResult(status);
123
+ if (Date.now() >= deadline) return toolResult({ taskId: params.taskId, status: status.status, error: { code: "WAIT_TIMEOUT", retryable: true } }, `## task wait\n- task: \`${params.taskId}\`\n- status: ${status.status}\n- wait timed out`);
124
+ onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
125
+ await sleep(Math.min(WAIT_POLL_MS, Math.max(1, deadline - Date.now())), signal);
126
+ }
127
+ } catch (error) { return clientError(error); }
128
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
232
129
  });
233
130
 
234
131
  pi.registerTool({
235
- name: "agent_task_inbox",
236
- label: "Task Inbox",
237
- description: "List terminal tasks for this parent session. Results are only shown to the model when this tool is called.",
238
- parameters: InboxParams,
239
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
240
- const sessionName = transport.getCurrentSessionName(process.env);
241
- const tasks = await store.listInbox(ctx.cwd, sessionName, { includeAcknowledged: params.includeAcknowledged ?? false });
242
- const compact = [];
243
- for (const task of tasks) {
244
- compact.push(compactTaskResult(task, await store.readTaskResult(ctx.cwd, task.id)));
245
- if (params.ack) {
246
- await store.ackTask(ctx.cwd, task.id, sessionName);
132
+ name: "agent_task_inbox", label: "Task Inbox", description: "Read inbox task events and optionally acknowledge terminal parent tasks.", parameters: InboxParams,
133
+ async execute(_toolCallId, params, signal) {
134
+ try {
135
+ let cursor = "0";
136
+ const taskIds = new Set<string>();
137
+ for (;;) {
138
+ const page = await client.inbox(cursor, params.includeAcknowledged ?? false, signal);
139
+ for (const event of page.events) taskIds.add(event.taskId);
140
+ if (!page.hasMore) break;
141
+ cursor = page.nextCursor;
247
142
  }
248
- }
249
- return taskToolResult({ tasks: compact });
250
- },
143
+ const tasks = await Promise.all([...taskIds].map((taskId) => client.status(taskId, signal)));
144
+ if (params.ack) await Promise.all(tasks.filter((task) => terminal(task.status)).map((task) => client.ack(task.task.taskId, signal)));
145
+ return toolResult({ tasks }, `## task inbox\n${tasks.length === 0 ? "- empty" : tasks.map((task) => `- \`${task.task.taskId}\`: ${task.status}${task.completion ? ` — ${task.completion.summary}` : ""}`).join("\n")}`);
146
+ } catch (error) { return clientError(error); }
147
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
251
148
  });
252
149
 
253
150
  pi.registerTool({
254
- name: "agent_task_cancel",
255
- label: "Cancel Agent Task",
256
- description: "Cancel a non-terminal task in the shared task store.",
257
- parameters: CancelParams,
258
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
259
- const task = await store.cancelTask(ctx.cwd, params.taskId, params.reason);
260
- return taskToolResult(compactTaskResult(task, await store.readTaskResult(ctx.cwd, params.taskId)));
261
- },
151
+ name: "agent_task_message", label: "Message Agent Task", description: "Durably send a question, answer, or information event for a task.", parameters: MessageParams,
152
+ async execute(_toolCallId, params, signal) {
153
+ try {
154
+ const [identity, status] = await Promise.all([client.sessionStatus(signal), client.status(params.taskId, signal)]);
155
+ const receipt = await client.message(params, signal);
156
+ const receiverQuestion = params.type === "question" && identity.sessionId === status.task.target.sessionId;
157
+ return { ...toolResult(receipt, `## task ${params.type}\n- task: \`${params.taskId}\`\n${params.message}`), ...(receiverQuestion ? { terminate: true } : {}) };
158
+ } catch (error) { return clientError(error); }
159
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
262
160
  });
263
161
 
264
162
  pi.registerTool({
265
- name: "agent_task_done",
266
- label: "Complete Agent Task",
267
- description: "Finish an assigned task with structured status. Use exactly once as the final action for assigned tasks.",
268
- promptSnippet: "Complete an assigned task with a terminating structured result",
269
- promptGuidelines: [
270
- "Use agent_task_done exactly once as the final action for pi.task.assignment.v1 tasks.",
271
- "After calling agent_task_done, do not emit another assistant response in prose.",
272
- ],
273
- parameters: DoneParams,
274
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
275
- const status = params.status as TerminalTaskStatus;
276
- const payload: TaskResultPayload = {
277
- summary: params.summary,
278
- ...(params.result && { result: params.result }),
279
- ...(params.error && { error: params.error }),
280
- ...(params.artifacts && { artifacts: params.artifacts }),
281
- };
282
- const task = await store.completeTask(ctx.cwd, params.taskId, status, payload);
283
- return {
284
- ...taskToolResult(compactTaskResult(task, await store.readTaskResult(ctx.cwd, params.taskId))),
285
- terminate: true,
286
- };
287
- },
163
+ name: "agent_task_cancel", label: "Cancel Agent Task", description: "Request cancellation for a non-terminal task through Wolfpack.", parameters: TaskIdParams,
164
+ async execute(_toolCallId, params, signal) {
165
+ try { const receipt = await client.cancel(params.taskId, signal); return toolResult(receipt, `## task cancellation requested\n- task: \`${params.taskId}\`${warnings(receipt.warnings)}`); } catch (error) { return clientError(error); }
166
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
167
+ });
168
+
169
+ pi.registerTool({
170
+ name: "agent_task_done", label: "Complete Agent Task", description: "Finish an assigned task with a structured terminal result. Use exactly once as the final action.",
171
+ promptSnippet: "Complete an assigned task with a terminating structured result", promptGuidelines: ["Use agent_task_done exactly once as the final action for an assigned task."], parameters: DoneParams,
172
+ async execute(_toolCallId, params, signal) {
173
+ try {
174
+ const result: TaskCompletionInput = { summary: params.summary, ...(params.result && { result: params.result }), ...(params.error && { error: params.error }), ...(params.artifacts && { artifacts: params.artifacts }) };
175
+ const receipt = await client.complete({ taskId: params.taskId, status: params.status, result }, signal);
176
+ return { ...toolResult(receipt, `## task ${params.status}\n- task: \`${params.taskId}\`\n- ${params.summary}${warnings(receipt.warnings)}`), terminate: true };
177
+ } catch (error) { return clientError(error); }
178
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
288
179
  });
289
180
  }
290
181
 
291
182
  export default function piTasks(pi: ExtensionAPI): void {
292
- registerAgentTaskTools(pi, createDefaultTaskCommunicationLayer(pi));
183
+ registerAgentTaskTools(pi);
293
184
  }
294
185
 
295
- function taskToolResult(details: unknown): AgentToolResult<unknown> {
296
- return {
297
- content: [{ type: "text", text: JSON.stringify(details) }],
298
- details,
299
- };
186
+ function statusResult(status: TaskStatus): AgentToolResult<unknown> {
187
+ return toolResult(status, `## task status\n- task: \`${status.task.taskId}\`\n- status: ${status.status}${status.completion ? `\n- summary: ${status.completion.summary}` : ""}${warnings(status.warnings)}`);
188
+ }
189
+
190
+ function toolResult(details: unknown, markdown: string): AgentToolResult<unknown> {
191
+ return { content: [{ type: "text", text: markdown }], details };
300
192
  }
301
193
 
302
- function getText(result: { readonly content?: readonly unknown[] }): string {
194
+ function clientError(error: unknown): AgentToolResult<unknown> {
195
+ const gateway = error instanceof GatewayClientError ? error : new GatewayClientError("CLIENT_ERROR", error instanceof Error ? error.message : "task gateway request failed", true);
196
+ return toolResult({ error: { code: gateway.code, message: gateway.message, retryable: gateway.retryable } }, `## task gateway error\n- ${gateway.code}: ${gateway.message}`);
197
+ }
198
+
199
+ function warnings(values: readonly { readonly code: string; readonly message: string }[]): string {
200
+ return values.length === 0 ? "" : `\n## gateway warnings\n${values.map((value) => `- ${value.code}: ${value.message}`).join("\n")}`;
201
+ }
202
+
203
+ function terminal(status: string): boolean {
204
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "timed_out";
205
+ }
206
+
207
+ function text(result: { readonly content?: readonly unknown[] }): string {
303
208
  const first = result.content?.[0];
304
- if (!first || typeof first !== "object" || !("type" in first) || first.type !== "text" || !("text" in first)) {
305
- return "";
306
- }
307
- return typeof first.text === "string" ? first.text : "";
209
+ return first && typeof first === "object" && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" ? first.text : "";
210
+ }
211
+
212
+ function throwIfAborted(signal: AbortSignal | undefined): void {
213
+ if (signal?.aborted) throw new GatewayClientError("ABORTED", "task wait was cancelled", true);
214
+ }
215
+
216
+ async function sleep(milliseconds: number, signal: AbortSignal | undefined): Promise<void> {
217
+ await new Promise<void>((resolve, reject) => {
218
+ const timeout = setTimeout(resolve, milliseconds);
219
+ const abort = (): void => { clearTimeout(timeout); reject(new GatewayClientError("ABORTED", "task wait was cancelled", true)); };
220
+ signal?.addEventListener("abort", abort, { once: true });
221
+ });
308
222
  }