@sgtbeatdown/pi-tasks 0.1.0 → 0.1.2

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/src/extension.ts CHANGED
@@ -3,306 +3,264 @@ 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";
6
+ import { deliverTaskInbox } from "./task-inbox";
7
+ import { TaskOutboxDeliveryError, TaskProtocolError } from "./task-protocol";
8
+ import { createWolfpackTaskCore } from "./wolfpack-task-relay";
9
+ import type { TaskCore } from "./task-core";
13
10
 
11
+ const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1_000;
12
+ const MIN_TASK_TIMEOUT_MS = 1_000;
13
+ const MAX_TASK_TIMEOUT_MS = 86_400_000;
14
+ const BACKGROUND_POLL_MS = 5_000;
15
+ const TARGET_NOT_REGISTERED_CODE = "TARGET_NOT_REGISTERED";
14
16
  const WAIT_POLL_MS = 250;
15
- const BACKGROUND_POLL_MS = 5000;
17
+ const SUMMARY_MAX_CHARS = 1_200;
16
18
 
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
- });
23
-
24
- const TaskIdParams = Type.Object({
25
- taskId: Type.String({ description: "Task id, e.g. task_..." }),
19
+ const EndpointParams = Type.Object({
20
+ relay: Type.String({ minLength: 1, description: "relay identifier" }),
21
+ id: Type.String({ minLength: 1, description: "opaque endpoint identifier" }),
26
22
  });
27
-
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 })),
31
- });
32
-
33
- const InboxParams = Type.Object({
34
- ack: Type.Optional(Type.Boolean({ default: false })),
35
- includeAcknowledged: Type.Optional(Type.Boolean({ default: false })),
36
- });
37
-
38
- const CancelParams = Type.Object({
39
- taskId: Type.String({ description: "Task id, e.g. task_..." }),
40
- reason: Type.Optional(Type.String()),
23
+ const SendParams = Type.Object({
24
+ to: EndpointParams,
25
+ task: Type.String({ minLength: 1, maxLength: 16 * 1024 }),
26
+ timeoutMs: Type.Optional(Type.Integer({ minimum: MIN_TASK_TIMEOUT_MS, maximum: MAX_TASK_TIMEOUT_MS })),
41
27
  });
42
-
28
+ const TaskIdParams = Type.Object({ taskId: Type.String({ minLength: 1 }) });
29
+ const WaitParams = Type.Object({ taskId: Type.String({ minLength: 1 }), timeoutMs: Type.Optional(Type.Integer({ minimum: MIN_TASK_TIMEOUT_MS, maximum: MAX_TASK_TIMEOUT_MS })) });
30
+ const MessageParams = Type.Object({ taskId: Type.String({ minLength: 1 }), type: StringEnum(["question", "answer", "information"] as const), message: Type.String({ minLength: 1, maxLength: 16 * 1024 }) });
43
31
  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
- ),
32
+ taskId: Type.String({ minLength: 1 }), status: StringEnum(["completed", "failed", "cancelled"] as const), summary: Type.String({ minLength: 1, maxLength: SUMMARY_MAX_CHARS }),
54
33
  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
- ),
34
+ error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String(), retryable: Type.Boolean() })),
35
+ artifacts: Type.Optional(Type.Array(Type.Object({ path: Type.String({ minLength: 1, description: "artifact paths are receiver-project-relative regular files" }), mimeType: Type.Optional(Type.String()), description: Type.Optional(Type.String()) }))),
65
36
  });
66
37
 
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
- }),
38
+ export type ConfiguredCoreFactory = (signal?: AbortSignal) => Promise<TaskCore>;
39
+
40
+ /** Reuses a successful configured core while allowing the next lifecycle operation to retry a failed setup. */
41
+ export function createConfiguredCoreLoader(factory: ConfiguredCoreFactory): ConfiguredCoreFactory {
42
+ let corePromise: Promise<TaskCore> | undefined;
43
+ return async (signal?: AbortSignal): Promise<TaskCore> => {
44
+ if (corePromise) return corePromise;
45
+ const candidate = Promise.resolve().then(() => factory(signal));
46
+ corePromise = candidate;
47
+ try {
48
+ return await candidate;
49
+ } catch (error) {
50
+ if (corePromise === candidate) corePromise = undefined;
51
+ throw error;
52
+ }
73
53
  };
74
54
  }
75
55
 
76
- export function registerAgentTaskTools(pi: ExtensionAPI, communication: TaskCommunicationLayer): void {
77
- const { store, transport } = communication;
56
+ /** Registers v2 endpoint-owned tools using a configured durable Wolfpack relay by default. */
57
+ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefined = undefined, createCore: ConfiguredCoreFactory = (signal) => createWolfpackTaskCore({}, signal)): void {
58
+ let inboxContext: ExtensionContext | undefined;
78
59
  let backgroundTimer: ReturnType<typeof setInterval> | undefined;
79
- const notifiedTaskIds = new Set<string>();
80
- const autoPromptedTaskIds = new Set<string>();
81
-
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}`));
85
-
86
- backgroundTimer = setInterval(() => {
87
- void refreshInboxStatus(ctx.cwd, sessionName, ctx).catch(() => {
88
- ctx.ui.setStatus("wolfpack-tasks", ctx.ui.theme.fg("warning", "tasks: unavailable"));
89
- });
90
- }, BACKGROUND_POLL_MS);
91
-
92
- await refreshInboxStatus(ctx.cwd, sessionName, ctx).catch(() => undefined);
93
- });
94
-
95
- 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);
100
- });
101
-
102
- pi.on("session_shutdown", async () => {
103
- if (backgroundTimer) {
104
- clearInterval(backgroundTimer);
105
- backgroundTimer = undefined;
60
+ let lifecycleEpoch = 0;
61
+ const configuredCore: ConfiguredCoreFactory = core === undefined ? createConfiguredCoreLoader(createCore) : async (): Promise<TaskCore> => core;
62
+ const refreshInbox = createSingleFlightInboxRefresh(async (signal) => {
63
+ const context = inboxContext;
64
+ if (!context) return undefined;
65
+ const activeCore = await configuredCore(signal);
66
+ if (inboxContext !== context) return undefined;
67
+ let outboxError: unknown;
68
+ try {
69
+ await activeCore.flushOutbox(signal);
70
+ } catch (error) {
71
+ outboxError = error;
106
72
  }
107
- });
108
-
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");
73
+ if (inboxContext !== context) return undefined;
74
+ try {
75
+ await activeCore.evaluateTimeouts(signal);
76
+ } catch (error) {
77
+ outboxError ??= error;
117
78
  }
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
-
79
+ if (inboxContext !== context) return undefined;
80
+ const isCurrent = (): boolean => inboxContext === context;
81
+ const guardedCore: TaskCore = {
82
+ ...activeCore,
83
+ async receive(receiveSignal) {
84
+ const deliveries = await activeCore.receive(receiveSignal);
85
+ return isCurrent() ? deliveries : [];
86
+ },
87
+ async acknowledgeRelayDelivery(cursor, acknowledgementSignal) {
88
+ if (isCurrent()) await activeCore.acknowledgeRelayDelivery(cursor, acknowledgementSignal);
89
+ },
90
+ async recordInsertion(input, insertionSignal) {
91
+ if (isCurrent()) await activeCore.recordInsertion(input, insertionSignal);
92
+ },
93
+ };
94
+ await deliverTaskInbox({
95
+ sendMessage(message, options) { if (isCurrent()) pi.sendMessage(message, options); },
96
+ appendEntry(customType, data) { if (isCurrent()) pi.appendEntry(customType, data); },
97
+ }, guardedCore, {
98
+ hasPendingMessages: (): boolean => !isCurrent() || context.hasPendingMessages(),
99
+ sessionManager: context.sessionManager,
100
+ }, signal);
101
+ return outboxError;
102
+ });
103
+ const refreshLifecycle = async (context: ExtensionContext, epoch: number): Promise<void> => {
104
+ const isCurrent = (): boolean => lifecycleEpoch === epoch && inboxContext === context;
127
105
  try {
128
- pi.sendUserMessage(buildBackgroundTaskNotificationPrompt(tasksToPrompt));
129
- for (const task of tasksToPrompt) {
130
- autoPromptedTaskIds.add(task.id);
106
+ const activeCore = await configuredCore();
107
+ let connectionError: unknown;
108
+ try {
109
+ await activeCore.connect();
110
+ } catch (error) {
111
+ connectionError = error;
131
112
  }
132
- } catch {
133
- ctx.ui.notify("task results ready; parent is busy, will retry", "info");
113
+ if (!isCurrent()) return;
114
+ const outboxError = await refreshInbox();
115
+ if (!isCurrent()) return;
116
+ const lifecycleError = outboxError ?? connectionError;
117
+ const status = lifecycleError === undefined ? undefined : outboxFailureStatus(lifecycleError);
118
+ context.ui.setStatus("pi-tasks", status === undefined ? undefined : context.ui.theme.fg("warning", status));
119
+ } catch (error) {
120
+ if (isCurrent()) context.ui.setStatus("pi-tasks", context.ui.theme.fg("warning", outboxFailureStatus(error)));
134
121
  }
135
- }
136
-
137
- 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
- ],
146
- 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
- }
122
+ };
178
123
 
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
- },
124
+ pi.on("session_start", async (_event, context) => {
125
+ const epoch = ++lifecycleEpoch;
126
+ inboxContext = context;
127
+ if (backgroundTimer) clearInterval(backgroundTimer);
128
+ backgroundTimer = setInterval(() => {
129
+ const currentContext = inboxContext;
130
+ if (currentContext) void refreshLifecycle(currentContext, epoch);
131
+ }, BACKGROUND_POLL_MS);
132
+ await refreshLifecycle(context, epoch);
133
+ });
134
+ pi.on("agent_end", async (_event, context) => {
135
+ const epoch = lifecycleEpoch;
136
+ inboxContext = context;
137
+ await refreshLifecycle(context, epoch);
138
+ });
139
+ pi.on("session_shutdown", () => {
140
+ lifecycleEpoch += 1;
141
+ if (backgroundTimer) clearInterval(backgroundTimer);
142
+ backgroundTimer = undefined;
143
+ inboxContext = undefined;
191
144
  });
192
145
 
193
146
  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)));
147
+ name: "agent_task_send", label: "Send Agent Task", description: "Persist an endpoint-owned task and submit its opaque assignment envelope to a relay.", parameters: SendParams,
148
+ async execute(_id, params, signal) {
149
+ try {
150
+ const sent = await (await configuredCore(signal)).createTask({ target: params.to, task: params.task, timeoutMs: params.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS }, signal);
151
+ return toolResult(sent, `## task accepted\n- task: \`${sent.taskId}\`\n- target: \`${params.to.relay}/${params.to.id}\`\n- delivery: relay acceptance only`);
152
+ } catch (error) { return taskError(error); }
201
153
  },
154
+ renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
202
155
  });
203
-
204
156
  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);
157
+ name: "agent_task_status", label: "Task Status", description: "Read local endpoint-owned task state; status is unavailable while its origin is offline.", parameters: TaskIdParams,
158
+ async execute(_id, params, signal) {
215
159
  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
- },
160
+ const task = (await configuredCore(signal)).getTask(params.taskId);
161
+ return task ? toolResult(task, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}`) : taskError(new Error("unknown local task"));
162
+ } catch (error) { return taskError(error); }
163
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
232
164
  });
233
-
234
165
  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);
166
+ name: "agent_task_wait", label: "Wait Agent Task", description: "Wait for a locally-known endpoint-owned task to reach a terminal state.", parameters: WaitParams,
167
+ async execute(_id, params, signal, onUpdate) {
168
+ const deadline = Date.now() + (params.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS);
169
+ try {
170
+ for (;;) {
171
+ await refreshInbox(signal);
172
+ const task = (await configuredCore(signal)).getTask(params.taskId);
173
+ if (!task) return taskError(new Error("unknown local task"));
174
+ if (terminal(task.status)) return toolResult(task, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}`);
175
+ if (Date.now() >= deadline) return toolResult({ taskId: params.taskId, status: task.status }, `## task wait\n- task: \`${params.taskId}\`\n- status: ${task.status}\n- wait timed out`);
176
+ onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
177
+ await sleep(Math.min(WAIT_POLL_MS, deadline - Date.now()), signal);
247
178
  }
248
- }
249
- return taskToolResult({ tasks: compact });
250
- },
179
+ } catch (error) { return taskError(error); }
180
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
251
181
  });
252
-
253
182
  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)));
183
+ name: "agent_task_inbox", label: "Task Inbox", description: "Read local task records after processing relay deliveries without acknowledging task lifecycle.", parameters: Type.Object({}),
184
+ async execute(_id, _params, signal) {
185
+ try {
186
+ await refreshInbox(signal);
187
+ const tasks = (await configuredCore(signal)).listTasks();
188
+ return toolResult({ tasks }, `## task inbox\n${tasks.map((task) => `- \`${task.taskId}\`: ${task.status}`).join("\n") || "- empty"}`);
189
+ } catch (error) { return taskError(error); }
261
190
  },
191
+ renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
262
192
  });
263
-
264
193
  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
- },
194
+ name: "agent_task_message", label: "Message Agent Task", description: "Persist a message intent before relay submission.", parameters: MessageParams,
195
+ async execute(_id, params, signal) { try { await (await configuredCore(signal)).submitIntent({ taskId: params.taskId, type: `task.${params.type}`, payload: { message: params.message } }, signal); return toolResult({ taskId: params.taskId }, `## task ${params.type}\n- task: \`${params.taskId}\``); } catch (error) { return taskError(error); } },
196
+ renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
197
+ });
198
+ pi.registerTool({
199
+ name: "agent_task_cancel", label: "Cancel Agent Task", description: "Persist cancellation before relay submission.", parameters: TaskIdParams,
200
+ async execute(_id, params, signal) { try { await (await configuredCore(signal)).submitIntent({ taskId: params.taskId, type: "task.cancelled", payload: {} }, signal); return toolResult({ taskId: params.taskId }, `## task cancellation requested\n- task: \`${params.taskId}\``); } catch (error) { return taskError(error); } },
201
+ renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
202
+ });
203
+ pi.registerTool({
204
+ name: "agent_task_ack", label: "Acknowledge Agent Task", description: "Record parent acknowledgement as an origin-owned logical event.", parameters: TaskIdParams,
205
+ async execute(_id, params, signal) { try { await (await configuredCore(signal)).acknowledgeParent(params.taskId, signal); return toolResult({ taskId: params.taskId }, `## task acknowledged\n- task: \`${params.taskId}\``); } catch (error) { return taskError(error); } },
206
+ renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
207
+ });
208
+ pi.registerTool({
209
+ name: "agent_task_done", label: "Complete Agent Task", description: "Persist a terminal task intent before relay submission.", parameters: DoneParams,
210
+ async execute(_id, params, signal) {
211
+ try {
212
+ await (await configuredCore(signal)).submitIntent({ taskId: params.taskId, type: `task.${params.status}`, payload: { summary: params.summary, ...(params.result === undefined ? {} : { result: params.result }), ...(params.error === undefined ? {} : { error: params.error }), ...(params.artifacts === undefined ? {} : { artifacts: params.artifacts }) } }, signal);
213
+ return { ...toolResult({ taskId: params.taskId }, `## task ${params.status}\n- task: \`${params.taskId}\`\n- ${params.summary}`), terminate: true };
214
+ } catch (error) { return taskError(error); }
215
+ }, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
288
216
  });
289
217
  }
290
218
 
219
+ export function createSingleFlightInboxRefresh<TValue>(refresh: (signal?: AbortSignal) => Promise<TValue>): (signal?: AbortSignal) => Promise<TValue> {
220
+ let inFlight: Promise<TValue> | undefined;
221
+ return (signal): Promise<TValue> => {
222
+ if (inFlight) return inFlight;
223
+ const current = refresh(signal).finally(() => {
224
+ if (inFlight === current) inFlight = undefined;
225
+ });
226
+ inFlight = current;
227
+ return current;
228
+ };
229
+ }
230
+
291
231
  export default function piTasks(pi: ExtensionAPI): void {
292
- registerAgentTaskTools(pi, createDefaultTaskCommunicationLayer(pi));
232
+ registerAgentTaskTools(pi);
293
233
  }
294
234
 
295
- function taskToolResult(details: unknown): AgentToolResult<unknown> {
296
- return {
297
- content: [{ type: "text", text: JSON.stringify(details) }],
298
- details,
299
- };
235
+ function toolResult(details: unknown, markdown: string): AgentToolResult<unknown> {
236
+ return { content: [{ type: "text", text: markdown }], details };
300
237
  }
301
238
 
302
- function getText(result: { readonly content?: readonly unknown[] }): string {
303
- const first = result.content?.[0];
304
- if (!first || typeof first !== "object" || !("type" in first) || first.type !== "text" || !("text" in first)) {
305
- return "";
239
+ function taskError(error: unknown): AgentToolResult<unknown> {
240
+ const message = error instanceof Error ? error.message : "task operation failed";
241
+ if (error instanceof TaskProtocolError) {
242
+ return toolResult({ ...error.details, error: { code: error.code, message, retryable: error.retryable } }, `## task error\n- ${message}`);
306
243
  }
307
- return typeof first.text === "string" ? first.text : "";
244
+ return toolResult({ error: { code: "TASK_ERROR", message, retryable: true } }, `## task error\n- ${message}`);
245
+ }
246
+
247
+ function outboxFailureStatus(error: unknown): "tasks: relay unavailable" | "tasks: outbox degraded" {
248
+ return error instanceof TaskOutboxDeliveryError && error.code === TARGET_NOT_REGISTERED_CODE ? "tasks: outbox degraded" : "tasks: relay unavailable";
249
+ }
250
+
251
+ function terminal(status: string): boolean {
252
+ return ["completed", "failed", "cancelled", "timed_out"].includes(status);
253
+ }
254
+
255
+ function text(result: { readonly content?: readonly unknown[] }): string {
256
+ const first = result.content?.[0];
257
+ return first && typeof first === "object" && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" ? first.text : "";
258
+ }
259
+
260
+ async function sleep(milliseconds: number, signal: AbortSignal | undefined): Promise<void> {
261
+ if (signal?.aborted) throw new Error("task wait was cancelled");
262
+ await new Promise<void>((resolve, reject) => {
263
+ const timer = setTimeout(resolve, Math.max(1, milliseconds));
264
+ signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("task wait was cancelled")); }, { once: true });
265
+ });
308
266
  }