@sgtbeatdown/pi-tasks 0.1.3 → 0.1.5
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 +2 -0
- package/package.json +1 -1
- package/skills/wolfpack-pi-task-delegation/SKILL.md +15 -0
- package/src/abortable-sleep.ts +16 -0
- package/src/extension.ts +48 -11
- package/src/legacy-extension.ts +2 -9
- package/src/task-core.ts +22 -2
- package/src/task-inbox.ts +0 -13
- package/src/task-protocol.ts +1 -0
package/README.md
CHANGED
|
@@ -36,6 +36,8 @@ Wolfpack's relay owns durable mailbox delivery and peer forwarding; Pi owns task
|
|
|
36
36
|
|
|
37
37
|
Set `PI_TASK_WORKER=1` only when launching a task-only worker. The exact value enables a fail-closed model `tool_call` gate; an absent value or any other value leaves ordinary interactive Pi behavior unchanged. Before assignment, only `agent_task_inbox`, `agent_task_status`, and `agent_task_wait` are allowed. Other current and future model tools are blocked with the stable reason `PI_TASK_WORKER_ASSIGNMENT_REQUIRED`. Explicit user `!`/`!!` shell commands are outside Pi's model `tool_call` event and are not intercepted.
|
|
38
38
|
|
|
39
|
+
`PI_TASK_WORKER=1` sessions are leaf roles. Workers cannot call `agent_task_send`; workers cannot call `agent_task_cancel`; workers cannot call `agent_task_ack`. These coordinator-tool attempts are blocked with the stable reason `PI_TASK_WORKER_COORDINATION_FORBIDDEN`, not the pre-assignment reason. `agent_task_message` is limited to the eligible incorporated assignment named by its input `taskId`. Generic role-orchestration guidance applies only to non-worker coordinators.
|
|
40
|
+
|
|
39
41
|
The gate opens only when the current Pi session contains a structured `pi-tasks-event` entry for `task.created` and the same event belongs to a locally persisted active task targeted to the current endpoint. Rendered prompt text, terminal output, unknown events, foreign tasks, and stale terminal tasks do not authorize execution. On restart, durable session evidence reopens the gate only while the matching local task remains eligible.
|
|
40
42
|
|
|
41
43
|
Preflighting `agent_task_done` marks that task as closing before sibling calls are preflighted. Ordinary tools remain blocked for a closing, pending-terminal, accepted-terminal, or `delivery_blocked` task; an idempotent `agent_task_done` retry for that same assigned task remains allowed. Another independently active assignment can still authorize work.
|
package/package.json
CHANGED
|
@@ -29,8 +29,23 @@ The default `agent_task_send` schema is exactly `to`, `task`, and optional `time
|
|
|
29
29
|
|
|
30
30
|
For a multi-step project phase, retain one persistent implementer and one persistent read-only reviewer. Reuse a healthy role session for corrections and follow-up review; a completed task finishes one assignment, not the underlying session. Do not rotate a healthy role session for routine corrections. Rotate only for phase completion, material context degradation, harness failure, or required specialist independence. Keep at most one active assignment per role unless the user explicitly approves parallel work.
|
|
31
31
|
|
|
32
|
+
`PI_TASK_WORKER=1` sessions are leaf roles. Workers cannot call `agent_task_send`; workers cannot call `agent_task_cancel`; workers cannot call `agent_task_ack`. These coordinator-tool attempts are blocked with the stable reason `PI_TASK_WORKER_COORDINATION_FORBIDDEN`, not the pre-assignment reason. They may use `agent_task_message` only for the eligible incorporated assignment named by its input `taskId`. Generic role-orchestration guidance applies only to non-worker coordinators. Task workers must not create, rotate, or close Wolfpack sessions through shell or session-control tools.
|
|
33
|
+
|
|
32
34
|
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
35
|
|
|
36
|
+
## delegation lifecycle
|
|
37
|
+
|
|
38
|
+
| event | owner | command | session outcome |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| assignment completion | worker | `agent_task_done` | unchanged |
|
|
41
|
+
| task acknowledgment | parent | `agent_task_ack` | unchanged |
|
|
42
|
+
| role session retention | parent | `none` | retained for reuse |
|
|
43
|
+
| role session teardown | parent | `wolfpack kill <session-or-id> --json` | exact stable session id terminated |
|
|
44
|
+
| teardown verification | parent | `wolfpack list --json` | exact stable session id absent |
|
|
45
|
+
| Pi exit | none | `/quit` | not Wolfpack teardown |
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
34
49
|
## v2 workflow
|
|
35
50
|
|
|
36
51
|
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";
|
|
@@ -17,7 +18,9 @@ const TARGET_NOT_REGISTERED_CODE = "TARGET_NOT_REGISTERED";
|
|
|
17
18
|
const WAIT_POLL_MS = 250;
|
|
18
19
|
const SUMMARY_MAX_CHARS = 1_200;
|
|
19
20
|
const PRE_ASSIGNMENT_TOOLS = new Set(["agent_task_inbox", "agent_task_status", "agent_task_wait"]);
|
|
21
|
+
const COORDINATOR_ONLY_TASK_TOOLS = new Set(["agent_task_send", "agent_task_cancel", "agent_task_ack"]);
|
|
20
22
|
export const WORKER_GATE_DENIAL_CODE = "PI_TASK_WORKER_ASSIGNMENT_REQUIRED";
|
|
23
|
+
export const WORKER_COORDINATION_FORBIDDEN_CODE = "PI_TASK_WORKER_COORDINATION_FORBIDDEN";
|
|
21
24
|
|
|
22
25
|
const EndpointParams = Type.Object({
|
|
23
26
|
relay: Type.String({ minLength: 1, description: "relay identifier" }),
|
|
@@ -129,11 +132,12 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
129
132
|
|
|
130
133
|
pi.on("tool_call", async (event, context) => {
|
|
131
134
|
if (!workerGateEnabled || PRE_ASSIGNMENT_TOOLS.has(event.toolName)) return undefined;
|
|
135
|
+
if (COORDINATOR_ONLY_TASK_TOOLS.has(event.toolName)) return { block: true, reason: WORKER_COORDINATION_FORBIDDEN_CODE };
|
|
132
136
|
try {
|
|
133
137
|
const activeCore = await configuredCore(context.signal);
|
|
134
138
|
const assignments = assignedWorkerTasks(activeCore, context.sessionManager.getEntries());
|
|
139
|
+
const taskId = inputTaskId(event.input);
|
|
135
140
|
if (event.toolName === "agent_task_done") {
|
|
136
|
-
const taskId = isRecord(event.input) && typeof event.input.taskId === "string" ? event.input.taskId : undefined;
|
|
137
141
|
const assigned = taskId === undefined ? undefined : assignments.find((task) => task.taskId === taskId);
|
|
138
142
|
if (assigned === undefined || (!eligibleWorkerTask(assigned, closingTaskIds) && assigned.terminalDelivery.state === "not_submitted" && !closingTaskIds.has(assigned.taskId))) {
|
|
139
143
|
return { block: true, reason: WORKER_GATE_DENIAL_CODE };
|
|
@@ -141,6 +145,10 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
141
145
|
closingTaskIds.add(assigned.taskId);
|
|
142
146
|
return undefined;
|
|
143
147
|
}
|
|
148
|
+
if (event.toolName === "agent_task_message") {
|
|
149
|
+
const assigned = taskId === undefined ? undefined : assignments.find((task) => task.taskId === taskId);
|
|
150
|
+
return assigned !== undefined && eligibleWorkerTask(assigned, closingTaskIds) ? undefined : { block: true, reason: WORKER_GATE_DENIAL_CODE };
|
|
151
|
+
}
|
|
144
152
|
if (assignments.some((task) => eligibleWorkerTask(task, closingTaskIds))) return undefined;
|
|
145
153
|
} catch {
|
|
146
154
|
// Authorization evidence unavailable or malformed: fail closed with the stable public reason.
|
|
@@ -205,7 +213,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
205
213
|
if (terminal(task.status)) return toolResult(task, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}`);
|
|
206
214
|
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
215
|
onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
|
|
208
|
-
await
|
|
216
|
+
await abortableSleep(Math.max(1, Math.min(WAIT_POLL_MS, deadline - Date.now())), signal, () => new Error("task wait was cancelled"));
|
|
209
217
|
}
|
|
210
218
|
} catch (error) { return taskError(error); }
|
|
211
219
|
}, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
@@ -228,7 +236,17 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
228
236
|
});
|
|
229
237
|
pi.registerTool({
|
|
230
238
|
name: "agent_task_cancel", label: "Cancel Agent Task", description: "Persist cancellation before relay submission.", parameters: TaskIdParams,
|
|
231
|
-
async execute(_id, params, signal) {
|
|
239
|
+
async execute(_id, params, signal) {
|
|
240
|
+
try {
|
|
241
|
+
const activeCore = await configuredCore(signal);
|
|
242
|
+
try {
|
|
243
|
+
await activeCore.submitIntent({ taskId: params.taskId, type: "task.cancelled", payload: {} }, signal);
|
|
244
|
+
return toolResult({ taskId: params.taskId }, `## task cancellation requested\n- task: \`${params.taskId}\``);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
return blockedCancellationResult(activeCore, params.taskId, error) ?? taskError(error);
|
|
247
|
+
}
|
|
248
|
+
} catch (error) { return taskError(error); }
|
|
249
|
+
},
|
|
232
250
|
renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
233
251
|
});
|
|
234
252
|
pi.registerTool({
|
|
@@ -275,6 +293,29 @@ function taskError(error: unknown): AgentToolResult<unknown> {
|
|
|
275
293
|
return toolResult({ error: { code: "TASK_ERROR", message, retryable: true } }, `## task error\n- ${message}`);
|
|
276
294
|
}
|
|
277
295
|
|
|
296
|
+
function blockedCancellationResult(core: TaskCore, taskId: string, error: unknown): AgentToolResult<unknown> | undefined {
|
|
297
|
+
if (!(error instanceof TaskOutboxDeliveryError) || error.code !== TARGET_NOT_REGISTERED_CODE || error.retryable || error.details?.taskId !== taskId) return undefined;
|
|
298
|
+
const task = core.getTask(taskId);
|
|
299
|
+
if (task?.status !== "cancelled" || !sameEndpoint(task.origin, core.endpoint)) return undefined;
|
|
300
|
+
const details = { ...error.details };
|
|
301
|
+
delete details.taskId;
|
|
302
|
+
delete details.envelopeId;
|
|
303
|
+
delete details.target;
|
|
304
|
+
delete details.blockedAt;
|
|
305
|
+
return toolResult({
|
|
306
|
+
taskId,
|
|
307
|
+
status: "cancelled",
|
|
308
|
+
warnings: [{
|
|
309
|
+
code: error.code,
|
|
310
|
+
message: error.message,
|
|
311
|
+
retryable: false,
|
|
312
|
+
delivery: "blocked",
|
|
313
|
+
target: task.target,
|
|
314
|
+
details,
|
|
315
|
+
}],
|
|
316
|
+
}, `## task cancelled\n- task: \`${taskId}\`\n- canonical status: cancelled\n- target delivery: blocked; receiver incorporation not confirmed`);
|
|
317
|
+
}
|
|
318
|
+
|
|
278
319
|
function outboxFailureStatus(error: unknown): "tasks: relay unavailable" | "tasks: outbox degraded" {
|
|
279
320
|
return error instanceof TaskOutboxDeliveryError && error.code === TARGET_NOT_REGISTERED_CODE ? "tasks: outbox degraded" : "tasks: relay unavailable";
|
|
280
321
|
}
|
|
@@ -303,6 +344,10 @@ function sameEndpoint(left: TaskEndpoint, right: TaskEndpoint): boolean {
|
|
|
303
344
|
return left.relay === right.relay && left.id === right.id;
|
|
304
345
|
}
|
|
305
346
|
|
|
347
|
+
function inputTaskId(input: unknown): string | undefined {
|
|
348
|
+
return isRecord(input) && typeof input.taskId === "string" ? input.taskId : undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
306
351
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
307
352
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
308
353
|
}
|
|
@@ -311,11 +356,3 @@ function text(result: { readonly content?: readonly unknown[] }): string {
|
|
|
311
356
|
const first = result.content?.[0];
|
|
312
357
|
return first && typeof first === "object" && "type" in first && first.type === "text" && "text" in first && typeof first.text === "string" ? first.text : "";
|
|
313
358
|
}
|
|
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-core.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_RELAY_PAYLOAD_BYTES,
|
|
3
|
+
ORIGIN_CANCELLATION_OPERATION,
|
|
3
4
|
PARENT_ACKNOWLEDGMENT_OPERATION,
|
|
4
5
|
TASK_PROTOCOL_VERSION,
|
|
5
6
|
TERMINAL_INTENT_OPERATION,
|
|
@@ -141,12 +142,20 @@ export function createTaskCore(options: TaskCoreOptions): TaskCore {
|
|
|
141
142
|
if (!RECEIVER_INTENT_TYPES.has(input.type) || !isRecord(input.payload)) throw new TaskProtocolError("INVALID_INTENT", "intent type or payload is invalid");
|
|
142
143
|
let persistedEnvelopeIds: readonly string[] = [];
|
|
143
144
|
let receiverTerminal = false;
|
|
145
|
+
let originCancellation = false;
|
|
144
146
|
options.store.transaction(() => {
|
|
145
147
|
const task = requiredTask(options.store, input.taskId);
|
|
146
148
|
receiverTerminal = !sameEndpoint(options.endpoint, task.origin) && TERMINAL_EVENTS.has(input.type);
|
|
149
|
+
originCancellation = sameEndpoint(options.endpoint, task.origin) && input.type === "task.cancelled";
|
|
147
150
|
persistedEnvelopeIds = persistIntent(options, clock.now, ids, task, input);
|
|
148
151
|
});
|
|
149
|
-
|
|
152
|
+
try {
|
|
153
|
+
await flush(options, signal, new Set(persistedEnvelopeIds));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (originCancellation) throwIfCancellationDeliveryBlocked(options, input.taskId, persistedEnvelopeIds);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
if (originCancellation) throwIfCancellationDeliveryBlocked(options, input.taskId, persistedEnvelopeIds);
|
|
150
159
|
if (receiverTerminal) throwIfTerminalDeliveryBlocked(options.store, input.taskId);
|
|
151
160
|
},
|
|
152
161
|
async recordInsertion(input, signal) {
|
|
@@ -242,7 +251,8 @@ function persistReceivedEnvelope(options: TaskCoreOptions, now: () => number, id
|
|
|
242
251
|
|
|
243
252
|
function persistIntent(options: TaskCoreOptions, now: () => number, ids: () => string, task: TaskRecord, input: SubmitIntentInput): readonly string[] {
|
|
244
253
|
if (sameEndpoint(options.endpoint, task.origin)) {
|
|
245
|
-
|
|
254
|
+
const operation = input.type === "task.cancelled" ? ORIGIN_CANCELLATION_OPERATION : undefined;
|
|
255
|
+
return canonicalize(options, now, ids, task, { intentId: ids(), taskId: input.taskId, type: input.type, payload: input.payload }, input.type, operation);
|
|
246
256
|
}
|
|
247
257
|
const envelopeId = ids();
|
|
248
258
|
const intent: TaskIntent = { intentId: ids(), taskId: input.taskId, type: input.type, payload: input.payload };
|
|
@@ -339,6 +349,16 @@ function parsePayload(envelope: RelayEnvelope): unknown {
|
|
|
339
349
|
try { return JSON.parse(envelope.payload) as unknown; } catch { throw new TaskProtocolError("INVALID_PAYLOAD", "relay envelope payload is not valid task protocol JSON"); }
|
|
340
350
|
}
|
|
341
351
|
|
|
352
|
+
function throwIfCancellationDeliveryBlocked(options: TaskCoreOptions, taskId: string, envelopeIds: readonly string[]): void {
|
|
353
|
+
const task = requiredTask(options.store, taskId);
|
|
354
|
+
const blocked = options.store.quarantinedOutbox().find((record) => envelopeIds.includes(record.envelope.envelopeId) && sameEndpoint(record.envelope.target, task.target));
|
|
355
|
+
if (blocked === undefined) return;
|
|
356
|
+
throw new TaskOutboxDeliveryError(blocked.errorCode, blocked.reason, {
|
|
357
|
+
retryable: false,
|
|
358
|
+
details: { ...blocked.details, taskId, envelopeId: blocked.envelope.envelopeId, target: task.target, blockedAt: blocked.quarantinedAt },
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
342
362
|
function throwIfTerminalDeliveryBlocked(store: TaskStore, taskId: string): void {
|
|
343
363
|
const delivery = requiredTask(store, taskId).terminalDelivery;
|
|
344
364
|
if (delivery.state !== "delivery_blocked") return;
|
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
|
-
}
|
package/src/task-protocol.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const TASK_PROTOCOL_VERSION = "pi-tasks/v2" as const;
|
|
2
2
|
export const MAX_RELAY_PAYLOAD_BYTES = 48 * 1024;
|
|
3
|
+
export const ORIGIN_CANCELLATION_OPERATION = "origin_cancellation";
|
|
3
4
|
export const PARENT_ACKNOWLEDGMENT_OPERATION = "parent_acknowledgment";
|
|
4
5
|
export const TERMINAL_INTENT_OPERATION = "terminal_intent";
|
|
5
6
|
|