@sgtbeatdown/pi-tasks 0.1.3 → 0.1.4
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/package.json
CHANGED
|
@@ -31,6 +31,19 @@ For a multi-step project phase, retain one persistent implementer and one persis
|
|
|
31
31
|
|
|
32
32
|
After every verified and acknowledged terminal task, explicitly retain a parent-spawned session only when concrete follow-up is likely; otherwise close it through canonical Wolfpack session control. Workers never close their own sessions. terminal `send` is only for explicit human steering; it is not task state, completion evidence, or a substitute for `agent_task_message`.
|
|
33
33
|
|
|
34
|
+
## delegation lifecycle
|
|
35
|
+
|
|
36
|
+
| event | owner | command | session outcome |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| assignment completion | worker | `agent_task_done` | unchanged |
|
|
39
|
+
| task acknowledgment | parent | `agent_task_ack` | unchanged |
|
|
40
|
+
| role session retention | parent | `none` | retained for reuse |
|
|
41
|
+
| role session teardown | parent | `wolfpack kill <session-or-id> --json` | exact stable session id terminated |
|
|
42
|
+
| teardown verification | parent | `wolfpack list --json` | exact stable session id absent |
|
|
43
|
+
| Pi exit | none | `/quit` | not Wolfpack teardown |
|
|
44
|
+
|
|
45
|
+
Assignment completion ends the assigned task, not the reusable role session. When no further reuse is likely, the parent—not the worker—must pass the exact stable session ID to `wolfpack kill <session-or-id> --json`, then inspect `wolfpack list --json` and verify that ID is absent. `/quit` exits Pi; it is not Wolfpack teardown. Workers never close themselves.
|
|
46
|
+
|
|
34
47
|
## v2 workflow
|
|
35
48
|
|
|
36
49
|
1. create or select the role session, verify its structured project and terminal readiness, then obtain its registered `taskEndpoint`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function abortableSleep(milliseconds: number, signal: AbortSignal | undefined, cancellationError: () => Error): Promise<void> {
|
|
2
|
+
if (signal?.aborted) throw cancellationError();
|
|
3
|
+
await new Promise<void>((resolve, reject) => {
|
|
4
|
+
const removeAbortListener = (): void => signal?.removeEventListener("abort", abort);
|
|
5
|
+
const timeout = setTimeout(() => {
|
|
6
|
+
removeAbortListener();
|
|
7
|
+
resolve();
|
|
8
|
+
}, milliseconds);
|
|
9
|
+
const abort = (): void => {
|
|
10
|
+
clearTimeout(timeout);
|
|
11
|
+
removeAbortListener();
|
|
12
|
+
reject(cancellationError());
|
|
13
|
+
};
|
|
14
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
15
|
+
});
|
|
16
|
+
}
|
package/src/extension.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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 { abortableSleep } from "./abortable-sleep";
|
|
6
7
|
import { deliverTaskInbox, incorporatedTaskEvents } from "./task-inbox";
|
|
7
8
|
import { TaskOutboxDeliveryError, TaskProtocolError } from "./task-protocol";
|
|
8
9
|
import { createWolfpackTaskCore } from "./wolfpack-task-relay";
|
|
@@ -205,7 +206,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
205
206
|
if (terminal(task.status)) return toolResult(task, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}`);
|
|
206
207
|
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`);
|
|
207
208
|
onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
|
|
208
|
-
await
|
|
209
|
+
await abortableSleep(Math.max(1, Math.min(WAIT_POLL_MS, deadline - Date.now())), signal, () => new Error("task wait was cancelled"));
|
|
209
210
|
}
|
|
210
211
|
} catch (error) { return taskError(error); }
|
|
211
212
|
}, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
@@ -311,11 +312,3 @@ function text(result: { readonly content?: readonly unknown[] }): string {
|
|
|
311
312
|
const first = result.content?.[0];
|
|
312
313
|
return first && typeof first === "object" && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" ? first.text : "";
|
|
313
314
|
}
|
|
314
|
-
|
|
315
|
-
async function sleep(milliseconds: number, signal: AbortSignal | undefined): Promise<void> {
|
|
316
|
-
if (signal?.aborted) throw new Error("task wait was cancelled");
|
|
317
|
-
await new Promise<void>((resolve, reject) => {
|
|
318
|
-
const timer = setTimeout(resolve, Math.max(1, milliseconds));
|
|
319
|
-
signal?.addEventListener("abort", () => { clearTimeout(timer); reject(new Error("task wait was cancelled")); }, { once: true });
|
|
320
|
-
});
|
|
321
|
-
}
|
package/src/legacy-extension.ts
CHANGED
|
@@ -3,6 +3,7 @@ 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 { abortableSleep } from "./abortable-sleep";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_TASK_TIMEOUT_MS,
|
|
8
9
|
GatewayClientError,
|
|
@@ -126,7 +127,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, client: WolfpackGateway
|
|
|
126
127
|
if (terminal(status.status)) return statusResult(status);
|
|
127
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`);
|
|
128
129
|
onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
|
|
129
|
-
await
|
|
130
|
+
await abortableSleep(Math.min(WAIT_POLL_MS, Math.max(1, deadline - Date.now())), signal, () => new GatewayClientError("ABORTED", "task wait was cancelled", true));
|
|
130
131
|
}
|
|
131
132
|
} catch (error) { return clientError(error); }
|
|
132
133
|
}, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
@@ -238,11 +239,3 @@ function text(result: { readonly content?: readonly unknown[] }): string {
|
|
|
238
239
|
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
239
240
|
if (signal?.aborted) throw new GatewayClientError("ABORTED", "task wait was cancelled", true);
|
|
240
241
|
}
|
|
241
|
-
|
|
242
|
-
async function sleep(milliseconds: number, signal: AbortSignal | undefined): Promise<void> {
|
|
243
|
-
await new Promise<void>((resolve, reject) => {
|
|
244
|
-
const timeout = setTimeout(resolve, milliseconds);
|
|
245
|
-
const abort = (): void => { clearTimeout(timeout); reject(new GatewayClientError("ABORTED", "task wait was cancelled", true)); };
|
|
246
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
247
|
-
});
|
|
248
|
-
}
|
package/src/task-inbox.ts
CHANGED
|
@@ -55,15 +55,6 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function restoredInboxCursor(entries: readonly unknown[]): string {
|
|
59
|
-
let cursor = "0";
|
|
60
|
-
for (const entry of entries) {
|
|
61
|
-
if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== TASK_CURSOR_CUSTOM_TYPE || !isRecord(entry.data) || !decimal(entry.data.cursor)) continue;
|
|
62
|
-
cursor = entry.data.cursor;
|
|
63
|
-
}
|
|
64
|
-
return cursor;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
function inboxEvent(delivery: RelayDelivery): TaskEvent {
|
|
68
59
|
try {
|
|
69
60
|
const payload = JSON.parse(delivery.envelope.payload) as unknown;
|
|
@@ -113,7 +104,3 @@ function key(taskId: string, eventId: string): string {
|
|
|
113
104
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
114
105
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
115
106
|
}
|
|
116
|
-
|
|
117
|
-
function decimal(value: unknown): value is string {
|
|
118
|
-
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
119
|
-
}
|