@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/README.md +69 -447
- package/package.json +9 -4
- package/skills/task-context-summary/SKILL.md +34 -0
- package/skills/wolfpack-pi-task-delegation/SKILL.md +34 -56
- package/src/extension.ts +217 -259
- package/src/gateway-client.ts +409 -0
- package/src/in-memory-task-relay.ts +114 -0
- package/src/index.ts +35 -0
- package/src/legacy-extension.ts +248 -0
- package/src/legacy-task-inbox.ts +148 -0
- package/src/metrics-cli.ts +20 -0
- package/src/metrics.ts +288 -0
- package/src/sqlite-database.ts +68 -0
- package/src/task-artifacts.ts +153 -0
- package/src/task-board.ts +365 -0
- package/src/task-core.ts +383 -0
- package/src/task-inbox.ts +109 -0
- package/src/task-protocol.ts +129 -0
- package/src/task-relay-conformance.ts +23 -0
- package/src/task-store.ts +238 -0
- package/src/v1-compat-extension.ts +1 -0
- package/src/wolfpack-task-relay.ts +319 -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
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 {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
|
17
|
+
const SUMMARY_MAX_CHARS = 1_200;
|
|
16
18
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum:
|
|
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({
|
|
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
|
-
|
|
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
|
-
),
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
106
|
+
const activeCore = await configuredCore();
|
|
107
|
+
let connectionError: unknown;
|
|
108
|
+
try {
|
|
109
|
+
await activeCore.connect();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
connectionError = error;
|
|
131
112
|
}
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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: "
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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: "
|
|
206
|
-
|
|
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
|
|
217
|
-
return
|
|
218
|
-
} catch (error) {
|
|
219
|
-
|
|
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: "
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
await
|
|
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
|
-
|
|
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: "
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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: "
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
...
|
|
285
|
-
|
|
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
|
|
232
|
+
registerAgentTaskTools(pi);
|
|
293
233
|
}
|
|
294
234
|
|
|
295
|
-
function
|
|
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
|
|
303
|
-
const
|
|
304
|
-
if (
|
|
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
|
|
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
|
}
|