@sgtbeatdown/pi-tasks 0.1.6 → 0.1.7

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,409 +0,0 @@
1
- export const DEFAULT_WOLFPACK_PORT = 18_790;
2
- export const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
3
- export const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1000;
4
- export const MIN_TASK_TIMEOUT_MS = 1_000;
5
- export const MAX_TASK_TIMEOUT_MS = 86_400_000;
6
- // Initial limits; benchmark representative task/context payloads before adjusting them.
7
- export const MAX_TASK_BYTES = 16 * 1024;
8
- export const MAX_CONTEXT_SUMMARY_BYTES = 16 * 1024;
9
- export const MAX_ASSIGNMENT_ENVELOPE_BYTES = 48 * 1024;
10
- export const MAX_HTTP_BODY_BYTES = 64 * 1024;
11
-
12
- export type GatewayErrorCode =
13
- | "ABORTED"
14
- | "TIMEOUT"
15
- | "NETWORK_ERROR"
16
- | "MALFORMED_RESPONSE"
17
- | "INVALID_RESPONSE"
18
- | string;
19
-
20
- export class GatewayClientError extends Error {
21
- readonly code: GatewayErrorCode;
22
- readonly retryable: boolean;
23
- readonly path?: string;
24
-
25
- constructor(code: GatewayErrorCode, message: string, retryable: boolean, path?: string) {
26
- super(message);
27
- this.name = "GatewayClientError";
28
- this.code = code;
29
- this.retryable = retryable;
30
- this.path = path;
31
- }
32
- }
33
-
34
- export interface TaskAddress {
35
- readonly machine: string;
36
- readonly sessionId: string;
37
- }
38
-
39
- export interface ContextRef {
40
- readonly path: string;
41
- readonly selector?: string;
42
- readonly purpose?: string;
43
- }
44
-
45
- export interface TaskContext {
46
- readonly summary?: string;
47
- readonly refs?: readonly ContextRef[];
48
- }
49
-
50
- export interface TaskWarning {
51
- readonly code: string;
52
- readonly message: string;
53
- }
54
-
55
- export interface TaskAssignment {
56
- readonly taskId: string;
57
- readonly source: TaskAddress;
58
- readonly target: TaskAddress;
59
- readonly task: string;
60
- readonly createdAt: string;
61
- readonly expiresAt: string;
62
- readonly context?: TaskContext;
63
- readonly role?: string;
64
- readonly metadata?: Record<string, string>;
65
- readonly onCompletePrompt?: string;
66
- }
67
-
68
- export interface TaskEvent {
69
- readonly id: string;
70
- readonly taskId: string;
71
- readonly type: string;
72
- readonly actor: "parent" | "receiver" | "sender";
73
- readonly source: TaskAddress;
74
- readonly destination: TaskAddress;
75
- readonly sequence: string;
76
- readonly occurredAt: string;
77
- readonly message?: string;
78
- readonly replyToMessageId?: string;
79
- readonly payload: Record<string, unknown>;
80
- readonly completion?: TaskCompletion;
81
- }
82
-
83
- export interface TaskCompletion {
84
- readonly summary: string;
85
- readonly result?: Record<string, unknown>;
86
- readonly error?: { readonly code: string; readonly message: string; readonly retryable: boolean };
87
- readonly artifacts?: readonly { readonly machine: string; readonly project: string; readonly path: string; readonly mimeType?: string; readonly description?: string }[];
88
- readonly warnings: readonly TaskWarning[];
89
- }
90
-
91
- export interface TaskStatus {
92
- readonly task: TaskAssignment;
93
- readonly status: string;
94
- readonly events: readonly TaskEvent[];
95
- readonly completion?: TaskCompletion;
96
- readonly warnings: readonly TaskWarning[];
97
- }
98
-
99
- export interface TaskReceipt {
100
- readonly taskId: string;
101
- readonly eventId: string;
102
- readonly sequence: string;
103
- readonly warnings: readonly TaskWarning[];
104
- }
105
-
106
- export interface InboxPage {
107
- readonly events: readonly TaskEvent[];
108
- readonly nextCursor: string;
109
- readonly hasMore: boolean;
110
- }
111
-
112
- export interface WolfpackGatewayClientOptions {
113
- readonly baseUrl?: string;
114
- readonly port?: string | undefined;
115
- readonly sessionName?: string | undefined;
116
- readonly timeoutMs?: number;
117
- }
118
-
119
- export interface SendTaskInput {
120
- readonly to: TaskAddress;
121
- readonly task: string;
122
- readonly context?: TaskContext;
123
- readonly role?: string;
124
- readonly preflight?: { readonly requiredProject?: string };
125
- readonly metadata?: Record<string, string>;
126
- readonly onCompletePrompt?: string;
127
- readonly timeoutMs?: number;
128
- readonly idempotencyKey?: string;
129
- }
130
-
131
- export interface MessageTaskInput {
132
- readonly taskId: string;
133
- readonly type: "question" | "answer" | "information";
134
- readonly message: string;
135
- readonly replyToMessageId?: string;
136
- }
137
-
138
- export interface TaskCompletionInput {
139
- readonly summary: string;
140
- readonly result?: Record<string, unknown>;
141
- readonly error?: { readonly code: string; readonly message: string; readonly retryable: boolean };
142
- readonly artifacts?: readonly { readonly path: string; readonly mimeType?: string; readonly description?: string }[];
143
- }
144
-
145
- export interface CompleteTaskInput {
146
- readonly taskId: string;
147
- readonly status: "completed" | "failed" | "cancelled";
148
- readonly result: TaskCompletionInput;
149
- }
150
-
151
- export interface WolfpackSessionStatus {
152
- readonly session: string;
153
- readonly sessionId: string;
154
- readonly terminal: { readonly exists: boolean; readonly alive: boolean; readonly status: string };
155
- }
156
-
157
- export interface WolfpackGatewayClient {
158
- readonly callerSession: string;
159
- request(method: "GET" | "POST", path: string, body?: unknown, signal?: AbortSignal): Promise<unknown>;
160
- sessionStatus(signal?: AbortSignal): Promise<WolfpackSessionStatus>;
161
- send(input: SendTaskInput, signal?: AbortSignal): Promise<TaskReceipt>;
162
- status(taskId: string, signal?: AbortSignal): Promise<TaskStatus>;
163
- inbox(cursor: string, includeAcknowledged: boolean, signal?: AbortSignal): Promise<InboxPage>;
164
- message(input: MessageTaskInput, signal?: AbortSignal): Promise<TaskReceipt>;
165
- complete(input: CompleteTaskInput, signal?: AbortSignal): Promise<TaskReceipt>;
166
- cancel(taskId: string, signal?: AbortSignal): Promise<TaskReceipt>;
167
- delivered(taskId: string, eventId: string, signal?: AbortSignal): Promise<TaskReceipt>;
168
- ack(taskId: string, signal?: AbortSignal): Promise<TaskReceipt>;
169
- }
170
-
171
- export function createWolfpackGatewayClient(options: WolfpackGatewayClientOptions = {}): WolfpackGatewayClient {
172
- const baseUrl = options.baseUrl ?? `http://127.0.0.1:${portFrom(options.port ?? process.env.WOLFPACK_PORT)}`;
173
- const callerSession = options.sessionName ?? process.env.WOLFPACK_SESSION_NAME ?? "unknown-session";
174
- const requestTimeoutMs = boundedRequestTimeout(options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
175
-
176
- async function request(method: "GET" | "POST", path: string, body?: unknown, signal?: AbortSignal): Promise<unknown> {
177
- const serializedBody = body === undefined ? undefined : serializeBody(body);
178
- const timeout = new AbortController();
179
- const timeoutId = setTimeout(() => timeout.abort(), requestTimeoutMs);
180
- const controller = new AbortController();
181
- const abort = (): void => controller.abort();
182
- signal?.addEventListener("abort", abort, { once: true });
183
- timeout.signal.addEventListener("abort", abort, { once: true });
184
- try {
185
- const response = await fetch(new URL(path, baseUrl), {
186
- method,
187
- headers: body === undefined ? undefined : { "content-type": "application/json" },
188
- body: serializedBody,
189
- signal: controller.signal,
190
- });
191
- const parsed = await parseJson(response);
192
- if (isErrorEnvelope(parsed)) throw new GatewayClientError(parsed.error.code, parsed.error.message, parsed.error.retryable, parsed.error.path);
193
- if (!response.ok) throw new GatewayClientError("MALFORMED_RESPONSE", `wolfpack returned HTTP ${response.status} without a task error envelope`, true);
194
- if (!isOkEnvelope(parsed)) throw new GatewayClientError("MALFORMED_RESPONSE", "wolfpack returned an invalid task response", true);
195
- return parsed;
196
- } catch (error) {
197
- if (error instanceof GatewayClientError) throw error;
198
- if (signal?.aborted) throw new GatewayClientError("ABORTED", "wolfpack request was cancelled", true);
199
- if (timeout.signal.aborted) throw new GatewayClientError("TIMEOUT", `wolfpack request exceeded ${requestTimeoutMs}ms`, true);
200
- throw new GatewayClientError("NETWORK_ERROR", error instanceof Error ? error.message : "wolfpack request failed", true);
201
- } finally {
202
- clearTimeout(timeoutId);
203
- signal?.removeEventListener("abort", abort);
204
- }
205
- }
206
-
207
- return {
208
- callerSession,
209
- request,
210
- async sessionStatus(signal) {
211
- return parseSessionStatus(await request("GET", query("/api/session-control/status", { session: callerSession }), undefined, signal));
212
- },
213
- async send(input, signal) {
214
- validateSend(input);
215
- return parseReceipt(await request("POST", "/api/tasks/v1/send", { callerSession, ...input }, signal));
216
- },
217
- async status(taskId, signal) {
218
- validateId(taskId, "task id");
219
- return parseStatus(await request("GET", query("/api/tasks/v1/status", { callerSession, taskId }), undefined, signal));
220
- },
221
- async inbox(cursor, includeAcknowledged, signal) {
222
- if (!decimal(cursor)) throw new GatewayClientError("INVALID_REQUEST", "inbox cursor must be a decimal delivery sequence", false);
223
- return parseInbox(await request("GET", query("/api/tasks/v1/inbox", { callerSession, cursor, includeAcknowledged: String(includeAcknowledged) }), undefined, signal));
224
- },
225
- async message(input, signal) {
226
- validateMessage(input);
227
- return parseReceipt(await request("POST", "/api/tasks/v1/message", { callerSession, ...input }, signal));
228
- },
229
- async complete(input, signal) {
230
- validateComplete(input);
231
- return parseReceipt(await request("POST", "/api/tasks/v1/complete", { callerSession, ...input }, signal));
232
- },
233
- async cancel(taskId, signal) {
234
- validateId(taskId, "task id");
235
- return parseReceipt(await request("POST", "/api/tasks/v1/cancel", { callerSession, taskId }, signal));
236
- },
237
- async delivered(taskId, eventId, signal) {
238
- validateId(taskId, "task id");
239
- validateId(eventId, "event id");
240
- return parseReceipt(await request("POST", "/api/tasks/v1/delivered", { callerSession, taskId, eventId }, signal));
241
- },
242
- async ack(taskId, signal) {
243
- validateId(taskId, "task id");
244
- return parseReceipt(await request("POST", "/api/tasks/v1/ack", { callerSession, taskId }, signal));
245
- },
246
- };
247
- }
248
-
249
- function portFrom(value: string | undefined): number {
250
- const port = value === undefined ? DEFAULT_WOLFPACK_PORT : Number(value);
251
- return Number.isInteger(port) && port > 0 && port < 65_536 ? port : DEFAULT_WOLFPACK_PORT;
252
- }
253
-
254
- function boundedRequestTimeout(value: number): number {
255
- return Number.isInteger(value) && value >= 1 && value <= 60_000 ? value : DEFAULT_REQUEST_TIMEOUT_MS;
256
- }
257
-
258
- function query(path: string, parameters: Record<string, string>): string {
259
- const search = new URLSearchParams(parameters);
260
- return `${path}?${search.toString()}`;
261
- }
262
-
263
- async function parseJson(response: Response): Promise<unknown> {
264
- try {
265
- return await response.json();
266
- } catch {
267
- throw new GatewayClientError("MALFORMED_RESPONSE", "wolfpack returned invalid JSON", true);
268
- }
269
- }
270
-
271
- function serializeBody(body: unknown): string {
272
- let serialized: string;
273
- try {
274
- serialized = JSON.stringify(body);
275
- } catch {
276
- throw new GatewayClientError("INVALID_REQUEST", "task request must be JSON-serializable", false);
277
- }
278
- if (bytes(serialized) > MAX_HTTP_BODY_BYTES) throw new GatewayClientError("INVALID_REQUEST", "task request exceeds the 64KiB initial HTTP limit", false);
279
- return serialized;
280
- }
281
-
282
- function validateSend(input: SendTaskInput): void {
283
- const path = sendValidationPath(input);
284
- if (path !== undefined) throw new GatewayClientError("INVALID_REQUEST", "task and target address are required and bounded", false, path);
285
- if (input.context?.summary !== undefined && (!nonEmpty(input.context.summary) || bytes(input.context.summary) > MAX_CONTEXT_SUMMARY_BYTES)) throw new GatewayClientError("INVALID_REQUEST", "context summary exceeds the 16KiB initial limit", false, "/context/summary");
286
- if (bytes(JSON.stringify(input)) > MAX_ASSIGNMENT_ENVELOPE_BYTES) throw new GatewayClientError("INVALID_REQUEST", "assignment envelope exceeds the 48KiB initial limit", false, "");
287
- const timeoutMs = input.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
288
- if (!Number.isInteger(timeoutMs) || timeoutMs < MIN_TASK_TIMEOUT_MS || timeoutMs > MAX_TASK_TIMEOUT_MS) throw new GatewayClientError("INVALID_REQUEST", "task timeout must be from 1000ms through 24h", false, "/timeoutMs");
289
- }
290
-
291
- function sendValidationPath(input: SendTaskInput): string | undefined {
292
- if (!isRecord(input.to)) return "/to";
293
- if (!nonEmpty(input.to.machine)) return "/to/machine";
294
- if (!nonEmpty(input.to.sessionId)) return "/to/sessionId";
295
- if (!nonEmpty(input.task) || bytes(input.task) > MAX_TASK_BYTES) return "/task";
296
- if (input.preflight?.requiredProject !== undefined && !nonEmpty(input.preflight.requiredProject)) return "/preflight/requiredProject";
297
- if (input.context?.refs !== undefined) {
298
- if (!Array.isArray(input.context.refs)) return "/context/refs";
299
- for (const [index, ref] of input.context.refs.entries()) {
300
- if (!isRecord(ref)) return `/context/refs/${index}`;
301
- if (!nonEmpty(ref.path)) return `/context/refs/${index}/path`;
302
- }
303
- }
304
- return undefined;
305
- }
306
-
307
- function validateMessage(input: MessageTaskInput): void {
308
- validateId(input.taskId, "task id");
309
- if (!nonEmpty(input.message) || bytes(input.message) > MAX_TASK_BYTES || !["question", "answer", "information"].includes(input.type) || (input.replyToMessageId !== undefined && !nonEmpty(input.replyToMessageId))) throw new GatewayClientError("INVALID_REQUEST", "task message is invalid or exceeds the 16KiB initial limit", false);
310
- }
311
-
312
- function validateComplete(input: CompleteTaskInput): void {
313
- validateId(input.taskId, "task id");
314
- if (!nonEmpty(input.result.summary) || bytes(input.result.summary) > MAX_TASK_BYTES || !["completed", "failed", "cancelled"].includes(input.status)) throw new GatewayClientError("INVALID_REQUEST", "terminal task result is invalid or exceeds the 16KiB initial limit", false);
315
- if (input.result.artifacts?.some((artifact) => !nonEmpty(artifact.path))) throw new GatewayClientError("INVALID_REQUEST", "artifact paths must be non-empty", false);
316
- }
317
-
318
- function validateId(value: string, label: string): void {
319
- if (!nonEmpty(value)) throw new GatewayClientError("INVALID_REQUEST", `${label} is required`, false);
320
- }
321
-
322
- function parseSessionStatus(value: unknown): WolfpackSessionStatus {
323
- if (!isRecord(value) || value.ok !== true || !nonEmpty(value.session) || !nonEmpty(value.sessionId) || !isRecord(value.terminal) || typeof value.terminal.exists !== "boolean" || typeof value.terminal.alive !== "boolean" || !nonEmpty(value.terminal.status)) throw invalidResponse("invalid structured Wolfpack session status");
324
- return { session: value.session, sessionId: value.sessionId, terminal: { exists: value.terminal.exists, alive: value.terminal.alive, status: value.terminal.status } };
325
- }
326
-
327
- function parseReceipt(value: unknown): TaskReceipt {
328
- if (!isRecord(value) || !nonEmpty(value.taskId) || !nonEmpty(value.eventId) || !decimal(value.sequence) || (value.warnings !== undefined && !warnings(value.warnings))) throw invalidResponse("invalid task acknowledgement");
329
- return { taskId: value.taskId, eventId: value.eventId, sequence: value.sequence, warnings: value.warnings ?? [] };
330
- }
331
-
332
- function parseStatus(value: unknown): TaskStatus {
333
- if (!isRecord(value) || !assignment(value.task) || !nonEmpty(value.status) || !events(value.events) || !warnings(value.warnings) || (value.completion !== undefined && !completion(value.completion))) throw invalidResponse("invalid task status response");
334
- return { task: value.task, status: value.status, events: value.events, warnings: value.warnings, ...(value.completion && { completion: value.completion }) };
335
- }
336
-
337
- function parseInbox(value: unknown): InboxPage {
338
- if (!isRecord(value) || !events(value.events) || !decimal(value.nextCursor) || typeof value.hasMore !== "boolean") throw invalidResponse("invalid task inbox response");
339
- return { events: value.events, nextCursor: value.nextCursor, hasMore: value.hasMore };
340
- }
341
-
342
- function invalidResponse(message: string): GatewayClientError {
343
- return new GatewayClientError("INVALID_RESPONSE", message, true);
344
- }
345
-
346
- function isOkEnvelope(value: unknown): value is Record<string, unknown> & { readonly ok: true } {
347
- return isRecord(value) && value.ok === true;
348
- }
349
-
350
- function isErrorEnvelope(value: unknown): value is { readonly ok: false; readonly error: { readonly code: string; readonly message: string; readonly retryable: boolean; readonly path?: string } } {
351
- return isRecord(value) && value.ok === false && isRecord(value.error) && nonEmpty(value.error.code) && typeof value.error.message === "string" && typeof value.error.retryable === "boolean"
352
- && (value.error.path === undefined || jsonPointer(value.error.path));
353
- }
354
-
355
- function jsonPointer(value: unknown): value is string {
356
- return typeof value === "string" && /^(?:|\/(?:[^~/]|~[01])*)*$/.test(value);
357
- }
358
-
359
- function assignment(value: unknown): value is TaskAssignment {
360
- return isRecord(value) && nonEmpty(value.taskId) && isAddress(value.source) && isAddress(value.target) && nonEmpty(value.task) && iso(value.createdAt) && iso(value.expiresAt)
361
- && (value.context === undefined || context(value.context)) && (value.role === undefined || nonEmpty(value.role)) && (value.metadata === undefined || stringRecord(value.metadata)) && (value.onCompletePrompt === undefined || nonEmpty(value.onCompletePrompt));
362
- }
363
-
364
- function context(value: unknown): value is TaskContext {
365
- return isRecord(value) && (value.summary === undefined || nonEmpty(value.summary)) && (value.refs === undefined || (Array.isArray(value.refs) && value.refs.every((ref) => isRecord(ref) && nonEmpty(ref.path) && (ref.selector === undefined || typeof ref.selector === "string") && (ref.purpose === undefined || typeof ref.purpose === "string"))));
366
- }
367
-
368
- function events(value: unknown): value is readonly TaskEvent[] {
369
- return Array.isArray(value) && value.every((event) => isRecord(event) && nonEmpty(event.id) && nonEmpty(event.taskId) && nonEmpty(event.type) && (event.actor === "parent" || event.actor === "receiver" || event.actor === "sender") && isAddress(event.source) && isAddress(event.destination) && decimal(event.sequence) && iso(event.occurredAt) && isRecord(event.payload) && (event.message === undefined || typeof event.message === "string") && (event.replyToMessageId === undefined || nonEmpty(event.replyToMessageId)) && (event.completion === undefined || completion(event.completion)));
370
- }
371
-
372
- function completion(value: unknown): value is TaskCompletion {
373
- return isRecord(value) && nonEmpty(value.summary) && warnings(value.warnings)
374
- && (value.result === undefined || isRecord(value.result))
375
- && (value.error === undefined || (isRecord(value.error) && nonEmpty(value.error.code) && typeof value.error.message === "string" && typeof value.error.retryable === "boolean"))
376
- && (value.artifacts === undefined || (Array.isArray(value.artifacts) && value.artifacts.every((artifact) => isRecord(artifact) && nonEmpty(artifact.machine) && nonEmpty(artifact.project) && nonEmpty(artifact.path) && (artifact.mimeType === undefined || typeof artifact.mimeType === "string") && (artifact.description === undefined || typeof artifact.description === "string"))));
377
- }
378
-
379
- function warnings(value: unknown): value is readonly TaskWarning[] {
380
- return Array.isArray(value) && value.every((warning) => isRecord(warning) && nonEmpty(warning.code) && typeof warning.message === "string");
381
- }
382
-
383
- function stringRecord(value: unknown): value is Record<string, string> {
384
- return isRecord(value) && Object.values(value).every((field) => typeof field === "string");
385
- }
386
-
387
- function isAddress(value: unknown): value is TaskAddress {
388
- return isRecord(value) && nonEmpty(value.machine) && nonEmpty(value.sessionId);
389
- }
390
-
391
- function isRecord(value: unknown): value is Record<string, unknown> {
392
- return value !== null && typeof value === "object" && !Array.isArray(value);
393
- }
394
-
395
- function nonEmpty(value: unknown): value is string {
396
- return typeof value === "string" && value.length > 0;
397
- }
398
-
399
- function decimal(value: unknown): value is string {
400
- return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
401
- }
402
-
403
- function iso(value: unknown): value is string {
404
- return typeof value === "string" && !Number.isNaN(Date.parse(value));
405
- }
406
-
407
- function bytes(value: string): number {
408
- return new TextEncoder().encode(value).byteLength;
409
- }
@@ -1,241 +0,0 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
- import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { Text } from "@earendil-works/pi-tui";
4
- import { Type } from "typebox";
5
-
6
- import { abortableSleep } from "./abortable-sleep";
7
- import {
8
- DEFAULT_TASK_TIMEOUT_MS,
9
- GatewayClientError,
10
- MAX_TASK_TIMEOUT_MS,
11
- MIN_TASK_TIMEOUT_MS,
12
- createWolfpackGatewayClient,
13
- type TaskCompletionInput,
14
- type TaskStatus,
15
- type WolfpackGatewayClient,
16
- } from "./gateway-client";
17
- import { deliverTaskInbox, restoredInboxCursor } from "./legacy-task-inbox";
18
-
19
- const BACKGROUND_POLL_MS = 5_000;
20
- const WAIT_POLL_MS = 250;
21
- const SUMMARY_MAX_CHARS = 1_200;
22
-
23
- const AddressParams = Type.Object({
24
- machine: Type.String({ minLength: 1, description: "local or a canonical Wolfpack machine identity" }),
25
- sessionId: Type.String({ minLength: 1, description: "Stable opaque Wolfpack broker session id" }),
26
- });
27
-
28
- const ContextRefParams = Type.Object({
29
- path: Type.String({ minLength: 1 }),
30
- selector: Type.Optional(Type.String()),
31
- purpose: Type.Optional(Type.String()),
32
- });
33
-
34
- const SendParams = Type.Object({
35
- to: AddressParams,
36
- task: Type.String({ minLength: 1, maxLength: 16 * 1024 }),
37
- context: Type.Optional(Type.Object({
38
- summary: Type.Optional(Type.String({ minLength: 1, maxLength: 16 * 1024 })),
39
- refs: Type.Optional(Type.Array(ContextRefParams)),
40
- })),
41
- role: Type.Optional(Type.String()),
42
- preflight: Type.Optional(Type.Object({ requiredProject: Type.Optional(Type.String()) })),
43
- metadata: Type.Optional(Type.Object({
44
- phaseId: Type.Optional(Type.String()),
45
- issueId: Type.Optional(Type.String()),
46
- verificationTier: Type.Optional(Type.String()),
47
- rootCause: Type.Optional(Type.String()),
48
- })),
49
- onCompletePrompt: Type.Optional(Type.String({ maxLength: 4_000 })),
50
- timeoutMs: Type.Optional(Type.Integer({ minimum: MIN_TASK_TIMEOUT_MS, maximum: MAX_TASK_TIMEOUT_MS })),
51
- idempotencyKey: Type.Optional(Type.String({ minLength: 1 })),
52
- });
53
-
54
- const TaskIdParams = Type.Object({ taskId: Type.String({ minLength: 1 }) });
55
- const WaitParams = Type.Object({ taskId: Type.String({ minLength: 1 }), timeoutMs: Type.Optional(Type.Integer({ minimum: MIN_TASK_TIMEOUT_MS, maximum: MAX_TASK_TIMEOUT_MS })) });
56
- const InboxParams = Type.Object({ includeAcknowledged: Type.Optional(Type.Boolean({ default: false })) });
57
- const MessageParams = Type.Object({
58
- taskId: Type.String({ minLength: 1 }),
59
- type: StringEnum(["question", "answer", "information"] as const),
60
- message: Type.String({ minLength: 1, maxLength: 16 * 1024 }),
61
- replyToMessageId: Type.Optional(Type.String({ minLength: 1 })),
62
- });
63
- const DoneParams = Type.Object({
64
- taskId: Type.String({ minLength: 1 }),
65
- status: StringEnum(["completed", "failed", "cancelled"] as const),
66
- summary: Type.String({ minLength: 1, maxLength: SUMMARY_MAX_CHARS }),
67
- result: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
68
- error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String(), retryable: Type.Boolean() })),
69
- artifacts: Type.Optional(Type.Array(Type.Object({ path: Type.String({ minLength: 1, description: "artifact paths must name receiver-project-relative regular files to inspect; artifacts are not a changed-file list." }), mimeType: Type.Optional(Type.String()), description: Type.Optional(Type.String()) }))),
70
- });
71
-
72
- export function registerAgentTaskTools(pi: ExtensionAPI, client: WolfpackGatewayClient = createWolfpackGatewayClient()): void {
73
- let backgroundTimer: ReturnType<typeof setInterval> | undefined;
74
- let inboxContext: ExtensionContext | undefined;
75
- const refreshInbox = createSingleFlightInboxRefresh(async () => {
76
- if (!inboxContext) return;
77
- const cursor = restoredInboxCursor(inboxContext.sessionManager.getEntries());
78
- await deliverTaskInbox(pi, client, inboxContext, cursor);
79
- });
80
-
81
- pi.on("session_start", async (_event, ctx) => {
82
- inboxContext = ctx;
83
- ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("dim", `tasks: ${client.callerSession}`));
84
- backgroundTimer = setInterval(() => {
85
- void refreshInbox().catch(() => ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("warning", "tasks: unavailable")));
86
- }, BACKGROUND_POLL_MS);
87
- await refreshInbox().catch(() => undefined);
88
- });
89
- pi.on("agent_end", async (_event, ctx) => {
90
- inboxContext = ctx;
91
- setTimeout(() => { void refreshInbox().catch(() => undefined); }, 0);
92
- });
93
- pi.on("session_shutdown", () => {
94
- if (backgroundTimer) clearInterval(backgroundTimer);
95
- backgroundTimer = undefined;
96
- inboxContext = undefined;
97
- });
98
-
99
- pi.registerTool({
100
- name: "agent_task_send", label: "Send Agent Task", description: "Durably send a bounded task to an existing Wolfpack Pi session.",
101
- promptSnippet: "Send nonblocking structured tasks through the local Wolfpack gateway",
102
- promptGuidelines: ["Use agent_task_send for normal nonblocking delegation; do not wait unless the user explicitly asks."],
103
- parameters: SendParams,
104
- async execute(_toolCallId, params, signal) {
105
- try {
106
- const receipt = await client.send(params, signal);
107
- return toolResult(receipt, `## task accepted\n- task: \`${receipt.taskId}\`\n- target: \`${params.to.machine}/${params.to.sessionId}\`\n- delivery: pending adapter insertion (confirmed by \`task.delivered\`)\n${warnings(receipt.warnings)}`);
108
- } catch (error) { return clientError(error); }
109
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
110
- });
111
-
112
- pi.registerTool({
113
- name: "agent_task_status", label: "Task Status", description: "Read compact structured task status from Wolfpack.", parameters: TaskIdParams,
114
- async execute(_toolCallId, params, signal) {
115
- try { const status = await client.status(params.taskId, signal); return statusResult(status); } catch (error) { return clientError(error); }
116
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
117
- });
118
-
119
- pi.registerTool({
120
- 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,
121
- async execute(_toolCallId, params, signal, onUpdate) {
122
- const deadline = Date.now() + (params.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS);
123
- try {
124
- for (;;) {
125
- throwIfAborted(signal);
126
- const status = await client.status(params.taskId, signal);
127
- if (terminal(status.status)) return statusResult(status);
128
- 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`);
129
- onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
130
- await abortableSleep(Math.min(WAIT_POLL_MS, Math.max(1, deadline - Date.now())), signal, () => new GatewayClientError("ABORTED", "task wait was cancelled", true));
131
- }
132
- } catch (error) { return clientError(error); }
133
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
134
- });
135
-
136
- pi.registerTool({
137
- name: "agent_task_inbox", label: "Task Inbox", description: "Read inbox task events without acknowledging them.", parameters: InboxParams,
138
- async execute(_toolCallId, params, signal) {
139
- try {
140
- let cursor = "0";
141
- const taskIds = new Set<string>();
142
- for (;;) {
143
- const page = await client.inbox(cursor, params.includeAcknowledged ?? false, signal);
144
- for (const event of page.events) taskIds.add(event.taskId);
145
- if (!page.hasMore) break;
146
- cursor = page.nextCursor;
147
- }
148
- const tasks = await Promise.all([...taskIds].map((taskId) => client.status(taskId, signal)));
149
- 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")}`);
150
- } catch (error) { return clientError(error); }
151
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
152
- });
153
-
154
- pi.registerTool({
155
- name: "agent_task_ack", label: "Acknowledge Agent Task", description: "Acknowledge one independently verified terminal task.", parameters: TaskIdParams,
156
- async execute(_toolCallId, params, signal) {
157
- try { return toolResult(await client.ack(params.taskId, signal), `## task acknowledged\n- task: \`${params.taskId}\``); } catch (error) { return clientError(error); }
158
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
159
- });
160
-
161
- pi.registerTool({
162
- name: "agent_task_message", label: "Message Agent Task", description: "Durably send a question, answer, or information event for a task.", parameters: MessageParams,
163
- async execute(_toolCallId, params, signal) {
164
- try {
165
- const [identity, status] = await Promise.all([client.sessionStatus(signal), client.status(params.taskId, signal)]);
166
- const receipt = await client.message(params, signal);
167
- const receiverQuestion = params.type === "question" && identity.sessionId === status.task.target.sessionId;
168
- return { ...toolResult(receipt, `## task ${params.type}\n- task: \`${params.taskId}\`\n${params.message}`), ...(receiverQuestion ? { terminate: true } : {}) };
169
- } catch (error) { return clientError(error); }
170
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
171
- });
172
-
173
- pi.registerTool({
174
- name: "agent_task_cancel", label: "Cancel Agent Task", description: "Request cancellation for a non-terminal task through Wolfpack.", parameters: TaskIdParams,
175
- async execute(_toolCallId, params, signal) {
176
- 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); }
177
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
178
- });
179
-
180
- pi.registerTool({
181
- name: "agent_task_done", label: "Complete Agent Task", description: "Finish an assigned task with a structured terminal result. Report source modifications in result.changedFiles; artifacts are receiver-project-relative regular files to inspect, not a changed-file list. Use exactly once as the final action.",
182
- promptSnippet: "Complete an assigned task: report changed files in result.changedFiles and artifacts separately", promptGuidelines: ["Report source modifications under result.changedFiles. Reserve artifacts for receiver-project-relative regular files a parent should inspect, not changed-file lists.", "Use agent_task_done exactly once as the final action for an assigned task."], parameters: DoneParams,
183
- async execute(_toolCallId, params, signal) {
184
- try {
185
- const result: TaskCompletionInput = { summary: params.summary, ...(params.result && { result: params.result }), ...(params.error && { error: params.error }), ...(params.artifacts && { artifacts: params.artifacts }) };
186
- const receipt = await client.complete({ taskId: params.taskId, status: params.status, result }, signal);
187
- return { ...toolResult(receipt, `## task ${params.status}\n- task: \`${params.taskId}\`\n- ${params.summary}${warnings(receipt.warnings)}`), terminate: true };
188
- } catch (error) { return clientError(error); }
189
- }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
190
- });
191
- }
192
-
193
- export function createSingleFlightInboxRefresh(refresh: () => Promise<void>): () => Promise<void> {
194
- let inFlight: Promise<void> | undefined;
195
- return (): Promise<void> => {
196
- if (inFlight) return inFlight;
197
- const current = refresh().finally(() => {
198
- if (inFlight === current) inFlight = undefined;
199
- });
200
- inFlight = current;
201
- return current;
202
- };
203
- }
204
-
205
- export default function piTasks(pi: ExtensionAPI): void {
206
- registerAgentTaskTools(pi);
207
- }
208
-
209
- function statusResult(status: TaskStatus): AgentToolResult<unknown> {
210
- return toolResult(status, `## task status\n- task: \`${status.task.taskId}\`\n- status: ${status.status}${status.completion ? `\n- summary: ${status.completion.summary}` : ""}${warnings(status.warnings)}`);
211
- }
212
-
213
- function toolResult(details: unknown, markdown: string): AgentToolResult<unknown> {
214
- return { content: [{ type: "text", text: markdown }], details };
215
- }
216
-
217
- function clientError(error: unknown): AgentToolResult<unknown> {
218
- const gateway = error instanceof GatewayClientError ? error : new GatewayClientError("CLIENT_ERROR", error instanceof Error ? error.message : "task gateway request failed", true);
219
- const path = gateway.path === undefined ? {} : { path: gateway.path };
220
- return toolResult(
221
- { error: { code: gateway.code, message: gateway.message, retryable: gateway.retryable, ...path } },
222
- `## task gateway error\n- ${gateway.code}: ${gateway.message}${gateway.path === undefined ? "" : `\n- path: \`${gateway.path}\``}`,
223
- );
224
- }
225
-
226
- function warnings(values: readonly { readonly code: string; readonly message: string }[]): string {
227
- return values.length === 0 ? "" : `\n## gateway warnings\n${values.map((value) => `- ${value.code}: ${value.message}`).join("\n")}`;
228
- }
229
-
230
- function terminal(status: string): boolean {
231
- return status === "completed" || status === "failed" || status === "cancelled" || status === "timed_out";
232
- }
233
-
234
- function text(result: { readonly content?: readonly unknown[] }): string {
235
- const first = result.content?.[0];
236
- return first && typeof first === "object" && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" ? first.text : "";
237
- }
238
-
239
- function throwIfAborted(signal: AbortSignal | undefined): void {
240
- if (signal?.aborted) throw new GatewayClientError("ABORTED", "task wait was cancelled", true);
241
- }