@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.
- package/README.md +48 -468
- package/package.json +4 -3
- package/skills/task-context-summary/SKILL.md +34 -0
- package/skills/wolfpack-pi-task-delegation/SKILL.md +29 -60
- package/src/extension.ts +165 -251
- package/src/gateway-client.ts +385 -0
- package/src/metrics-cli.ts +20 -0
- package/src/metrics.ts +288 -0
- package/src/task-artifacts.ts +153 -0
- package/src/task-board.ts +365 -0
- package/src/task-inbox.ts +141 -0
- package/src/auto-notify.ts +0 -24
- package/src/protocol.ts +0 -73
- package/src/store.ts +0 -494
- package/src/stores/filesystem.ts +0 -40
- package/src/task-communication.ts +0 -68
- package/src/transports/wolfpack.ts +0 -29
- package/src/types.ts +0 -114
|
@@ -1,75 +1,44 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wolfpack-pi-task-delegation
|
|
3
|
-
description: Use
|
|
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
|
-
#
|
|
6
|
+
# wolfpack pi task delegation
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
##
|
|
10
|
+
## gateway requirements and addressing
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
##
|
|
18
|
+
## workflow
|
|
20
19
|
|
|
21
|
-
1.
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
to wait. Continue local work if there is other useful work to do.
|
|
36
|
+
## do not
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
25
|
-
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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({
|
|
45
|
-
status: StringEnum(["completed", "failed", "cancelled"
|
|
46
|
-
summary: Type.String({
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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: "
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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: "
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
parameters: DoneParams,
|
|
274
|
-
async execute(_toolCallId, params,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
...(
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
183
|
+
registerAgentTaskTools(pi);
|
|
293
184
|
}
|
|
294
185
|
|
|
295
|
-
function
|
|
296
|
-
return {
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
}
|