@sgtbeatdown/pi-tasks 0.1.4 → 0.1.6
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 +13 -2
- package/package.json +1 -1
- package/skills/wolfpack-pi-task-delegation/SKILL.md +34 -10
- package/src/extension.ts +144 -16
- package/src/index.ts +1 -1
- package/src/task-core.ts +139 -29
- package/src/task-inbox.ts +58 -17
- package/src/task-protocol.ts +17 -0
package/README.md
CHANGED
|
@@ -26,16 +26,27 @@ Wolfpack's relay owns durable mailbox delivery and peer forwarding; Pi owns task
|
|
|
26
26
|
|
|
27
27
|
### v2 delegation workflow
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Configure Pi role models with `WOLFPACK_IMPLEMENTER_MODEL` and `WOLFPACK_REVIEWER_MODEL`; they default to `openai-codex/gpt-5.6-terra` and `openai-codex/gpt-5.6-sol`, respectively. Explicit user or project choices override those defaults.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
IMPLEMENTER_MODEL="${WOLFPACK_IMPLEMENTER_MODEL:-openai-codex/gpt-5.6-terra}"
|
|
33
|
+
REVIEWER_MODEL="${WOLFPACK_REVIEWER_MODEL:-openai-codex/gpt-5.6-sol}"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
1. create or select a role session. For a disposable worker, omit an initial assignment prompt and pass the resolved role model: `wolfpack agent spawn <project> --name <task-role> --model "$IMPLEMENTER_MODEL" --json` (or `"$REVIEWER_MODEL"` for review). Put all worker instructions in `agent_task_send.task` so the new Pi process can become idle before assignment.
|
|
30
37
|
2. verify structured session readiness, wait for extension registration, and read `taskEndpoint` from `wolfpack session status <session> --json`.
|
|
31
38
|
3. call `agent_task_send` with that endpoint and the complete task instructions. Keep working; use `agent_task_status` or `agent_task_inbox` for structured evidence, and call `agent_task_wait` only when the user explicitly asks to block.
|
|
32
39
|
4. use `agent_task_message` for durable questions, answers, and information. The receiver calls `agent_task_done` as its final action; no completion prose follows.
|
|
33
40
|
5. independently verify the result, call `agent_task_ack({ taskId })` once for that terminal task, then explicitly retain or close only the role sessions the parent spawned.
|
|
34
41
|
|
|
42
|
+
Coordinator-capable agents may delegate further when justified, and the spawning coordinator owns each child's lifecycle. After terminal completion and acknowledgment, deliberately retain the child or run `wolfpack kill <stable-session-id> --json`, then verify that exact ID is absent from `wolfpack list --json`. Never use `wolfpack session send`, `/exit`, or `/quit` for cleanup; terminal input is not Wolfpack teardown.
|
|
43
|
+
|
|
35
44
|
### v2 worker-only execution gate
|
|
36
45
|
|
|
37
46
|
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
47
|
|
|
48
|
+
`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.
|
|
49
|
+
|
|
39
50
|
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
51
|
|
|
41
52
|
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.
|
|
@@ -138,7 +149,7 @@ On start or resume it rebuilds incorporated IDs from the full durable session en
|
|
|
138
149
|
|
|
139
150
|
## parent workflow
|
|
140
151
|
|
|
141
|
-
1. create or select a Wolfpack Pi session with the canonical session-control workflow and retain its stable broker ID. Spawn a disposable worker without an initial
|
|
152
|
+
1. create or select a Wolfpack Pi session with the canonical session-control workflow and retain its stable broker ID. Spawn a disposable worker without an initial assignment prompt using `wolfpack agent spawn <project> --name <task-role> --model "$IMPLEMENTER_MODEL" --json` (or `"$REVIEWER_MODEL"` for review) so it reaches idle before assignment; put worker instructions in `agent_task_send.task`.
|
|
142
153
|
2. before remote dispatch, complete Wolfpack's [Live-peer readiness checklist](https://github.com/almogdepaz/wolfpack/blob/main/docs/task-gateway.md#live-peer-readiness-checklist) and retain operator-recorded package/reload evidence; only then check the target's local requirements and send compact instructions with curated context and selected refs when they save receiver investigation.
|
|
143
154
|
3. keep working; use `agent_task_status` or `agent_task_inbox` for structured follow-up, and `agent_task_message` for questions, answers, and decisions.
|
|
144
155
|
4. verify cited files, diffs, tests, and paths-only artifact metadata independently before reporting success.
|
package/package.json
CHANGED
|
@@ -7,27 +7,51 @@ description: Use when opening, selecting, delegating to, checking, or cleaning u
|
|
|
7
7
|
|
|
8
8
|
use `wolfpack-tailnet-control` for session control. this skill covers endpoint-owned v2 task lifecycle, structured handoffs, and parent verification. The package default is the v2 extension described by Wolfpack's [relay v2 control-api contract](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary).
|
|
9
9
|
|
|
10
|
+
## choose one assignment mode
|
|
11
|
+
|
|
12
|
+
choose exactly one mode for the initial assignment:
|
|
13
|
+
|
|
14
|
+
- **full-startup mode:** spawn with the complete `--plan`, `--prompt-file`, or concise `--prompt`. do not call `agent_task_send` for that same work.
|
|
15
|
+
- **endpoint mode:** spawn without a startup plan or prompt, obtain the structured `taskEndpoint`, then put the complete assignment in one `agent_task_send.task`.
|
|
16
|
+
|
|
17
|
+
never combine a plan-driven spawn with a narrower endpoint task. prefer one cohesive implementation handoff per approved PR or phase; split only at a real approval, design, isolation, or blocker boundary—not per issue, commit, finding, or verification checkpoint. the remaining v2 workflow below describes endpoint mode.
|
|
18
|
+
|
|
19
|
+
## role model selection
|
|
20
|
+
|
|
21
|
+
Pi role models are explicit and configurable at spawn time:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
IMPLEMENTER_MODEL="${WOLFPACK_IMPLEMENTER_MODEL:-openai-codex/gpt-5.6-terra}"
|
|
25
|
+
REVIEWER_MODEL="${WOLFPACK_REVIEWER_MODEL:-openai-codex/gpt-5.6-sol}"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Pass `--model "$IMPLEMENTER_MODEL"` when spawning the editing implementer and `--model "$REVIEWER_MODEL"` when spawning the read-only reviewer. an explicit user or project model choice overrides the environment/default. never infer role or model from the session name, and never omit the resolved model because the current parent default may differ.
|
|
29
|
+
|
|
10
30
|
## v2 requirements and addressing
|
|
11
31
|
|
|
12
32
|
- load this package's default extension in every participating Pi process and set `WOLFPACK_SESSION_NAME`. Set `WOLFPACK_PORT` only when the local Wolfpack control port differs from `18790`.
|
|
13
33
|
- address `agent_task_send` targets only as `to: { relay, id }`. For the default Wolfpack adapter, the relay is `wolfpack-pi-tasks-v2` and the ID is opaque.
|
|
14
|
-
- create a disposable worker without an initial
|
|
34
|
+
- create a disposable worker without an initial assignment prompt: `wolfpack agent spawn <project> --name <task-role> --model "$IMPLEMENTER_MODEL" --json` (or `"$REVIEWER_MODEL"` for review). Do not start a disposable worker with a blocking “wait for assignments” prompt. Put the complete instructions in `agent_task_send.task`.
|
|
15
35
|
- after the target extension registers, run structured session control (`wolfpack session status <session> --json`) and read its `taskEndpoint`. Do not derive endpoint IDs from session names, broker IDs, terminal labels, output, or prose.
|
|
16
36
|
- pass the returned endpoint without translation:
|
|
17
37
|
|
|
18
38
|
```json
|
|
19
39
|
{
|
|
20
40
|
"to": { "relay": "wolfpack-pi-tasks-v2", "id": "target-opaque-endpoint-id" },
|
|
21
|
-
"task": "implement the
|
|
22
|
-
"timeoutMs":
|
|
41
|
+
"task": "implement the approved phase and run focused tests",
|
|
42
|
+
"timeoutMs": 3600000
|
|
23
43
|
}
|
|
24
44
|
```
|
|
25
45
|
|
|
26
|
-
The default `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`. v1-only fields such as context, role, metadata, preflight, idempotency keys, and completion prompts are not silently accepted or translated.
|
|
46
|
+
The default `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`. v1-only fields such as context, role, metadata, preflight, idempotency keys, and completion prompts are not silently accepted or translated. use at least `3600000` milliseconds for coding assignments; timeout is a failure deadline, not a progress-poll interval.
|
|
27
47
|
|
|
28
48
|
## phase roles and handoffs
|
|
29
49
|
|
|
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.
|
|
50
|
+
For a multi-step project phase, retain one persistent implementer and one persistent read-only reviewer. Give the implementer the whole approved phase, including required commit ordering and focused verification, rather than opening one task per issue or checkpoint. 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.
|
|
51
|
+
|
|
52
|
+
A coordinator-capable agent may delegate further when the quality or throughput gain justifies it. The spawning coordinator owns each child's complete lifecycle: record the stable session ID, acknowledge its terminal task once, then deliberately retain the child or close it with `wolfpack kill <stable-session-id> --json` and verify that exact ID is absent from `wolfpack list --json`.
|
|
53
|
+
|
|
54
|
+
`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.
|
|
31
55
|
|
|
32
56
|
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
57
|
|
|
@@ -38,17 +62,17 @@ After every verified and acknowledged terminal task, explicitly retain a parent-
|
|
|
38
62
|
| assignment completion | worker | `agent_task_done` | unchanged |
|
|
39
63
|
| task acknowledgment | parent | `agent_task_ack` | unchanged |
|
|
40
64
|
| role session retention | parent | `none` | retained for reuse |
|
|
41
|
-
| role session teardown |
|
|
42
|
-
| teardown verification |
|
|
43
|
-
| Pi exit | none | `/quit` | not Wolfpack teardown |
|
|
65
|
+
| role session teardown | spawning coordinator | `wolfpack kill <stable-session-id> --json` | exact stable session id terminated |
|
|
66
|
+
| teardown verification | spawning coordinator | `wolfpack list --json` | exact stable session id absent |
|
|
67
|
+
| Pi exit | none | `/exit` or `/quit` | not Wolfpack teardown |
|
|
44
68
|
|
|
45
|
-
Assignment completion ends the assigned task, not the reusable role session. When no further reuse is likely, the
|
|
69
|
+
Assignment completion ends the assigned task, not the reusable role session. When no further reuse is likely, the coordinator that spawned the role—not the worker itself—must pass the exact stable session ID to `wolfpack kill <stable-session-id> --json`, then inspect `wolfpack list --json` and verify that ID is absent. Never use `wolfpack session send`, `/exit`, or `/quit` for cleanup: they are terminal interaction, not Wolfpack teardown. Do not guess lifecycle commands. Workers never close themselves.
|
|
46
70
|
|
|
47
71
|
## v2 workflow
|
|
48
72
|
|
|
49
73
|
1. create or select the role session, verify its structured project and terminal readiness, then obtain its registered `taskEndpoint`.
|
|
50
74
|
2. call `agent_task_send` with the opaque endpoint and concise instructions in `task`. The tool returns after relay acceptance only, not Pi insertion or model execution. If submission reports a retryable delivery error after local persistence, use the structured task ID from the error and inspect it rather than creating an unrelated replacement.
|
|
51
|
-
3. keep working. Use `agent_task_status` or `agent_task_inbox`
|
|
75
|
+
3. keep working. Use `agent_task_status` or `agent_task_inbox` only at meaningful checkpoints or when progress evidence is needed; do not turn status checks into lifecycle chatter. Call `agent_task_wait` only when the user explicitly asks to block.
|
|
52
76
|
4. use `agent_task_message` for durable `question`, `answer`, or `information` flow. Do not use rendered text, terminal output, or logs as lifecycle evidence.
|
|
53
77
|
5. assignees call `agent_task_done` as their final action with the assigned task ID, terminal status, concise summary, and optional structured result, error, and artifact declarations. Report source modifications in `result.changedFiles`; artifacts are receiver-project-relative regular files for a parent to inspect, not changed-file lists: `{ "result": { "changedFiles": ["src/extension.ts"] }, "artifacts": [{ "path": "verification/task-2.md" }] }`. No prose completion afterward.
|
|
54
78
|
6. the parent independently verifies files, diff, tests, and artifacts. Then call `agent_task_ack({ taskId })` for that one terminal task and explicitly retain or close the spawned role session.
|
package/src/extension.ts
CHANGED
|
@@ -5,9 +5,9 @@ import { Type } from "typebox";
|
|
|
5
5
|
|
|
6
6
|
import { abortableSleep } from "./abortable-sleep";
|
|
7
7
|
import { deliverTaskInbox, incorporatedTaskEvents } from "./task-inbox";
|
|
8
|
-
import { TaskOutboxDeliveryError, TaskProtocolError } from "./task-protocol";
|
|
8
|
+
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskOutboxDeliveryError, TaskProtocolError } from "./task-protocol";
|
|
9
9
|
import { createWolfpackTaskCore } from "./wolfpack-task-relay";
|
|
10
|
-
import type { TaskCore } from "./task-core";
|
|
10
|
+
import type { SubmitIntentInput, SubmitIntentOutcome, TaskCore } from "./task-core";
|
|
11
11
|
import type { TaskEndpoint, TaskSnapshot } from "./task-protocol";
|
|
12
12
|
|
|
13
13
|
const DEFAULT_TASK_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
@@ -18,7 +18,9 @@ const TARGET_NOT_REGISTERED_CODE = "TARGET_NOT_REGISTERED";
|
|
|
18
18
|
const WAIT_POLL_MS = 250;
|
|
19
19
|
const SUMMARY_MAX_CHARS = 1_200;
|
|
20
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"]);
|
|
21
22
|
export const WORKER_GATE_DENIAL_CODE = "PI_TASK_WORKER_ASSIGNMENT_REQUIRED";
|
|
23
|
+
export const WORKER_COORDINATION_FORBIDDEN_CODE = "PI_TASK_WORKER_COORDINATION_FORBIDDEN";
|
|
22
24
|
|
|
23
25
|
const EndpointParams = Type.Object({
|
|
24
26
|
relay: Type.String({ minLength: 1, description: "relay identifier" }),
|
|
@@ -62,7 +64,6 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
62
64
|
let inboxContext: ExtensionContext | undefined;
|
|
63
65
|
let backgroundTimer: ReturnType<typeof setInterval> | undefined;
|
|
64
66
|
let lifecycleEpoch = 0;
|
|
65
|
-
const pendingInsertions = new Set<string>();
|
|
66
67
|
const closingTaskIds = new Set<string>();
|
|
67
68
|
const workerGateEnabled = process.env.PI_TASK_WORKER === "1";
|
|
68
69
|
const configuredCore: ConfiguredCoreFactory = core === undefined ? createConfiguredCoreLoader(createCore) : async (): Promise<TaskCore> => core;
|
|
@@ -102,9 +103,10 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
102
103
|
sendMessage(message, options) { if (isCurrent()) pi.sendMessage(message, options); },
|
|
103
104
|
appendEntry(customType, data) { if (isCurrent()) pi.appendEntry(customType, data); },
|
|
104
105
|
}, guardedCore, {
|
|
106
|
+
isIdle: (): boolean => isCurrent() && context.isIdle(),
|
|
105
107
|
hasPendingMessages: (): boolean => !isCurrent() || context.hasPendingMessages(),
|
|
106
108
|
sessionManager: context.sessionManager,
|
|
107
|
-
},
|
|
109
|
+
}, signal);
|
|
108
110
|
return outboxError;
|
|
109
111
|
});
|
|
110
112
|
const refreshLifecycle = async (context: ExtensionContext, epoch: number): Promise<void> => {
|
|
@@ -130,11 +132,12 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
130
132
|
|
|
131
133
|
pi.on("tool_call", async (event, context) => {
|
|
132
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 };
|
|
133
136
|
try {
|
|
134
137
|
const activeCore = await configuredCore(context.signal);
|
|
135
138
|
const assignments = assignedWorkerTasks(activeCore, context.sessionManager.getEntries());
|
|
139
|
+
const taskId = inputTaskId(event.input);
|
|
136
140
|
if (event.toolName === "agent_task_done") {
|
|
137
|
-
const taskId = isRecord(event.input) && typeof event.input.taskId === "string" ? event.input.taskId : undefined;
|
|
138
141
|
const assigned = taskId === undefined ? undefined : assignments.find((task) => task.taskId === taskId);
|
|
139
142
|
if (assigned === undefined || (!eligibleWorkerTask(assigned, closingTaskIds) && assigned.terminalDelivery.state === "not_submitted" && !closingTaskIds.has(assigned.taskId))) {
|
|
140
143
|
return { block: true, reason: WORKER_GATE_DENIAL_CODE };
|
|
@@ -142,6 +145,10 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
142
145
|
closingTaskIds.add(assigned.taskId);
|
|
143
146
|
return undefined;
|
|
144
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
|
+
}
|
|
145
152
|
if (assignments.some((task) => eligibleWorkerTask(task, closingTaskIds))) return undefined;
|
|
146
153
|
} catch {
|
|
147
154
|
// Authorization evidence unavailable or malformed: fail closed with the stable public reason.
|
|
@@ -150,7 +157,6 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
150
157
|
});
|
|
151
158
|
|
|
152
159
|
pi.on("session_start", async (_event, context) => {
|
|
153
|
-
pendingInsertions.clear();
|
|
154
160
|
closingTaskIds.clear();
|
|
155
161
|
const epoch = ++lifecycleEpoch;
|
|
156
162
|
inboxContext = context;
|
|
@@ -166,8 +172,12 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
166
172
|
inboxContext = context;
|
|
167
173
|
await refreshLifecycle(context, epoch);
|
|
168
174
|
});
|
|
175
|
+
pi.on("agent_settled", async (_event, context) => {
|
|
176
|
+
const epoch = lifecycleEpoch;
|
|
177
|
+
inboxContext = context;
|
|
178
|
+
await refreshLifecycle(context, epoch);
|
|
179
|
+
});
|
|
169
180
|
pi.on("session_shutdown", () => {
|
|
170
|
-
pendingInsertions.clear();
|
|
171
181
|
closingTaskIds.clear();
|
|
172
182
|
lifecycleEpoch += 1;
|
|
173
183
|
if (backgroundTimer) clearInterval(backgroundTimer);
|
|
@@ -187,10 +197,13 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
187
197
|
});
|
|
188
198
|
pi.registerTool({
|
|
189
199
|
name: "agent_task_status", label: "Task Status", description: "Read local endpoint-owned task state; status is unavailable while its origin is offline.", parameters: TaskIdParams,
|
|
190
|
-
async execute(_id, params, signal) {
|
|
200
|
+
async execute(_id, params, signal, _onUpdate, context) {
|
|
191
201
|
try {
|
|
192
|
-
const
|
|
193
|
-
|
|
202
|
+
const activeCore = await configuredCore(signal);
|
|
203
|
+
const task = activeCore.getTask(params.taskId);
|
|
204
|
+
if (!task) return taskError(new Error("unknown local task"));
|
|
205
|
+
const deliveryEvidence = taskDeliveryEvidence(task);
|
|
206
|
+
return toolResult({ ...task, deliveryEvidence }, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}${receiverAssignment(activeCore, task, context)}${deliveryEvidenceText(deliveryEvidence)}`);
|
|
194
207
|
} catch (error) { return taskError(error); }
|
|
195
208
|
}, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
196
209
|
});
|
|
@@ -213,11 +226,12 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
213
226
|
});
|
|
214
227
|
pi.registerTool({
|
|
215
228
|
name: "agent_task_inbox", label: "Task Inbox", description: "Read local task records after processing relay deliveries without acknowledging task lifecycle.", parameters: Type.Object({}),
|
|
216
|
-
async execute(_id, _params, signal) {
|
|
229
|
+
async execute(_id, _params, signal, _onUpdate, context) {
|
|
217
230
|
try {
|
|
218
231
|
await refreshInbox(signal);
|
|
219
|
-
const
|
|
220
|
-
|
|
232
|
+
const activeCore = await configuredCore(signal);
|
|
233
|
+
const tasks = activeCore.listTasks();
|
|
234
|
+
return toolResult({ tasks }, `## task inbox\n${tasks.map((task) => `- \`${task.taskId}\`: ${task.status}${receiverAssignment(activeCore, task, context)}`).join("\n") || "- empty"}`);
|
|
221
235
|
} catch (error) { return taskError(error); }
|
|
222
236
|
},
|
|
223
237
|
renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
@@ -229,7 +243,17 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
229
243
|
});
|
|
230
244
|
pi.registerTool({
|
|
231
245
|
name: "agent_task_cancel", label: "Cancel Agent Task", description: "Persist cancellation before relay submission.", parameters: TaskIdParams,
|
|
232
|
-
async execute(_id, params, signal) {
|
|
246
|
+
async execute(_id, params, signal) {
|
|
247
|
+
try {
|
|
248
|
+
const activeCore = await configuredCore(signal);
|
|
249
|
+
try {
|
|
250
|
+
await activeCore.submitIntent({ taskId: params.taskId, type: "task.cancelled", payload: {} }, signal);
|
|
251
|
+
return toolResult({ taskId: params.taskId }, `## task cancellation requested\n- task: \`${params.taskId}\``);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return blockedCancellationResult(activeCore, params.taskId, error) ?? taskError(error);
|
|
254
|
+
}
|
|
255
|
+
} catch (error) { return taskError(error); }
|
|
256
|
+
},
|
|
233
257
|
renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
234
258
|
});
|
|
235
259
|
pi.registerTool({
|
|
@@ -241,8 +265,32 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
241
265
|
name: "agent_task_done", label: "Complete Agent Task", description: "Persist a terminal task intent before relay submission.", parameters: DoneParams,
|
|
242
266
|
async execute(_id, params, signal) {
|
|
243
267
|
try {
|
|
244
|
-
|
|
245
|
-
|
|
268
|
+
const activeCore = await configuredCore(signal);
|
|
269
|
+
let submission: SubmitIntentOutcome | undefined;
|
|
270
|
+
try {
|
|
271
|
+
const input: SubmitIntentInput = { 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 }) } };
|
|
272
|
+
if (activeCore.submitIntentWithOutcome) submission = await activeCore.submitIntentWithOutcome(input, signal);
|
|
273
|
+
else await activeCore.submitIntent(input, signal);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
return blockedDoneResult(activeCore, params.taskId, params.status, error) ?? taskError(error);
|
|
276
|
+
}
|
|
277
|
+
const task = activeCore.getTask(params.taskId);
|
|
278
|
+
if (!task) return taskError(new Error("unknown local task"));
|
|
279
|
+
if (submission?.authority === "origin" && submission.canonicalEvent.type === "task.late_terminal") {
|
|
280
|
+
return {
|
|
281
|
+
...toolResult({ taskId: params.taskId, requestedStatus: params.status, observedCanonicalStatus: task.status, canonicalEvent: submission.canonicalEvent }, `## canonical late terminal recorded\n- task: \`${params.taskId}\`\n- requested status: ${params.status}\n- canonical status remains: ${task.status}\n- ${params.summary}`),
|
|
282
|
+
terminate: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
if (submission?.authority === "origin" && task.status === params.status) {
|
|
286
|
+
return {
|
|
287
|
+
...toolResult({ taskId: params.taskId, requestedStatus: params.status, observedCanonicalStatus: task.status, canonicalCompletion: { state: "confirmed", status: task.status } }, `## task ${task.status}\n- task: \`${params.taskId}\`\n- canonical status: ${task.status}\n- ${params.summary}`),
|
|
288
|
+
terminate: true,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const deliveryOutcome = task.terminalDelivery.state === "accepted" ? "accepted" : "recorded";
|
|
292
|
+
const result = toolResult({ taskId: params.taskId, requestedStatus: params.status, observedCanonicalStatus: task.status, terminalDelivery: task.terminalDelivery }, `## terminal intent ${deliveryOutcome}\n- task: \`${params.taskId}\`\n- requested status: ${params.status}\n- observed canonical status: ${task.status}\n- ${params.summary}`);
|
|
293
|
+
return task.terminalDelivery.state === "accepted" || terminal(task.status) ? { ...result, terminate: true } : result;
|
|
246
294
|
} catch (error) { return taskError(error); }
|
|
247
295
|
}, renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
248
296
|
});
|
|
@@ -264,6 +312,37 @@ export default function piTasks(pi: ExtensionAPI): void {
|
|
|
264
312
|
registerAgentTaskTools(pi);
|
|
265
313
|
}
|
|
266
314
|
|
|
315
|
+
interface TaskDeliveryEvidence {
|
|
316
|
+
readonly receiverPersistence: "not_confirmed" | "confirmed";
|
|
317
|
+
readonly piInsertion: "not_confirmed" | "blocked" | "confirmed";
|
|
318
|
+
readonly wakeAcceptance: "not_confirmed" | "pending" | "confirmed";
|
|
319
|
+
readonly modelExecution: "not_evidenced";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function taskDeliveryEvidence(task: TaskSnapshot): TaskDeliveryEvidence {
|
|
323
|
+
let receiverPersistence: TaskDeliveryEvidence["receiverPersistence"] = "not_confirmed";
|
|
324
|
+
let piInsertion: TaskDeliveryEvidence["piInsertion"] = "not_confirmed";
|
|
325
|
+
let wakeAcceptance: TaskDeliveryEvidence["wakeAcceptance"] = "not_confirmed";
|
|
326
|
+
const assignmentEventId = task.events.find((event) => event.type === "task.created")?.eventId;
|
|
327
|
+
for (const event of task.events) {
|
|
328
|
+
if (event.type !== "task.delivery_receipt" || event.payload.eventId !== assignmentEventId) continue;
|
|
329
|
+
if (event.payload.stage === TaskDeliveryStage.receiverPersisted) receiverPersistence = "confirmed";
|
|
330
|
+
if (event.payload.stage === TaskDeliveryStage.piInsertion && event.payload.state === TaskDeliveryEvidenceState.blocked) piInsertion = "blocked";
|
|
331
|
+
if (event.payload.stage === TaskDeliveryStage.piInserted || event.payload.stage === undefined) {
|
|
332
|
+
receiverPersistence = "confirmed";
|
|
333
|
+
piInsertion = "confirmed";
|
|
334
|
+
}
|
|
335
|
+
if (event.payload.stage === TaskDeliveryStage.wakeRequested) wakeAcceptance = "pending";
|
|
336
|
+
if (event.payload.stage === TaskDeliveryStage.wakeAccepted) wakeAcceptance = "confirmed";
|
|
337
|
+
}
|
|
338
|
+
return { receiverPersistence, piInsertion, wakeAcceptance, modelExecution: "not_evidenced" };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function deliveryEvidenceText(evidence: TaskDeliveryEvidence): string {
|
|
342
|
+
const insertion = evidence.piInsertion === "blocked" ? "blocked; retryable" : evidence.piInsertion;
|
|
343
|
+
return `\n- receiver persistence: ${evidence.receiverPersistence}\n- Pi insertion: ${insertion}\n- wake acceptance: ${evidence.wakeAcceptance}\n- model execution: ${evidence.modelExecution}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
267
346
|
function toolResult(details: unknown, markdown: string): AgentToolResult<unknown> {
|
|
268
347
|
return { content: [{ type: "text", text: markdown }], details };
|
|
269
348
|
}
|
|
@@ -276,6 +355,44 @@ function taskError(error: unknown): AgentToolResult<unknown> {
|
|
|
276
355
|
return toolResult({ error: { code: "TASK_ERROR", message, retryable: true } }, `## task error\n- ${message}`);
|
|
277
356
|
}
|
|
278
357
|
|
|
358
|
+
function blockedCancellationResult(core: TaskCore, taskId: string, error: unknown): AgentToolResult<unknown> | undefined {
|
|
359
|
+
if (!(error instanceof TaskOutboxDeliveryError) || error.details?.taskId !== taskId) return undefined;
|
|
360
|
+
const task = core.getTask(taskId);
|
|
361
|
+
const warning = task === undefined ? undefined : blockedDeliveryWarning(error, task.target);
|
|
362
|
+
if (task?.status !== "cancelled" || !sameEndpoint(task.origin, core.endpoint) || warning === undefined) return undefined;
|
|
363
|
+
return toolResult({ taskId, status: "cancelled", warnings: [warning] }, `## task cancelled\n- task: \`${taskId}\`\n- canonical status: cancelled\n- target delivery: blocked; receiver incorporation not confirmed`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function blockedDoneResult(core: TaskCore, taskId: string, requestedStatus: string, error: unknown): AgentToolResult<unknown> | undefined {
|
|
367
|
+
if (!(error instanceof TaskOutboxDeliveryError)) return undefined;
|
|
368
|
+
const task = core.getTask(taskId);
|
|
369
|
+
if (task === undefined) return undefined;
|
|
370
|
+
if (!sameEndpoint(task.origin, core.endpoint) && task.terminalDelivery.state === "delivery_blocked" && task.terminalDelivery.intentType === `task.${requestedStatus}`) {
|
|
371
|
+
return toolResult({
|
|
372
|
+
taskId,
|
|
373
|
+
status: task.status,
|
|
374
|
+
terminalDelivery: task.terminalDelivery,
|
|
375
|
+
error: { code: error.code, message: error.message, retryable: error.retryable },
|
|
376
|
+
}, `## terminal intent delivery blocked\n- task: \`${taskId}\`\n- canonical status: ${task.status}\n- canonical completion not confirmed; retry remains allowed`);
|
|
377
|
+
}
|
|
378
|
+
const warning = blockedDeliveryWarning(error, task.target);
|
|
379
|
+
if (!sameEndpoint(task.origin, core.endpoint) || task.status !== requestedStatus || warning === undefined) return undefined;
|
|
380
|
+
return {
|
|
381
|
+
...toolResult({ taskId, status: task.status, warnings: [warning] }, `## task ${task.status}\n- task: \`${taskId}\`\n- canonical status: ${task.status}\n- target delivery: blocked; receiver incorporation not confirmed`),
|
|
382
|
+
terminate: true,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function blockedDeliveryWarning(error: TaskOutboxDeliveryError, target: TaskEndpoint): Readonly<Record<string, unknown>> | undefined {
|
|
387
|
+
if (error.code !== TARGET_NOT_REGISTERED_CODE || error.retryable) return undefined;
|
|
388
|
+
const details = { ...error.details };
|
|
389
|
+
delete details.taskId;
|
|
390
|
+
delete details.envelopeId;
|
|
391
|
+
delete details.target;
|
|
392
|
+
delete details.blockedAt;
|
|
393
|
+
return { code: error.code, message: error.message, retryable: false, delivery: "blocked", target, details };
|
|
394
|
+
}
|
|
395
|
+
|
|
279
396
|
function outboxFailureStatus(error: unknown): "tasks: relay unavailable" | "tasks: outbox degraded" {
|
|
280
397
|
return error instanceof TaskOutboxDeliveryError && error.code === TARGET_NOT_REGISTERED_CODE ? "tasks: outbox degraded" : "tasks: relay unavailable";
|
|
281
398
|
}
|
|
@@ -284,6 +401,13 @@ function terminal(status: string): boolean {
|
|
|
284
401
|
return ["completed", "failed", "cancelled", "timed_out"].includes(status);
|
|
285
402
|
}
|
|
286
403
|
|
|
404
|
+
function receiverAssignment(core: TaskCore, task: TaskSnapshot, context: Pick<ExtensionContext, "sessionManager">): string {
|
|
405
|
+
if (task.status !== "active" || sameEndpoint(task.origin, core.endpoint) || !sameEndpoint(task.target, core.endpoint)) return "";
|
|
406
|
+
const created = task.events.find((event) => event.type === "task.created");
|
|
407
|
+
if (!created || !incorporatedTaskEvents(context.sessionManager.getEntries()).some((evidence) => evidence.taskId === task.taskId && evidence.eventId === created.eventId)) return "";
|
|
408
|
+
return `\n\n## task assignment\n${task.task}`;
|
|
409
|
+
}
|
|
410
|
+
|
|
287
411
|
function assignedWorkerTasks(core: TaskCore, entries: readonly unknown[]): readonly TaskSnapshot[] {
|
|
288
412
|
const assignments = new Map<string, TaskSnapshot>();
|
|
289
413
|
for (const evidence of incorporatedTaskEvents(entries)) {
|
|
@@ -304,6 +428,10 @@ function sameEndpoint(left: TaskEndpoint, right: TaskEndpoint): boolean {
|
|
|
304
428
|
return left.relay === right.relay && left.id === right.id;
|
|
305
429
|
}
|
|
306
430
|
|
|
431
|
+
function inputTaskId(input: unknown): string | undefined {
|
|
432
|
+
return isRecord(input) && typeof input.taskId === "string" ? input.taskId : undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
307
435
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
308
436
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
309
437
|
}
|
package/src/index.ts
CHANGED
|
@@ -31,5 +31,5 @@ export type {
|
|
|
31
31
|
TaskRecord,
|
|
32
32
|
TaskRelay,
|
|
33
33
|
} from "./task-protocol";
|
|
34
|
-
export type { TaskCore, TaskCoreOptions } from "./task-core";
|
|
34
|
+
export type { SubmitIntentOutcome, TaskCore, TaskCoreOptions } from "./task-core";
|
|
35
35
|
export type { TaskStore, TaskStoreOptions } from "./task-store";
|
package/src/task-core.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
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,
|
|
7
|
+
TaskDeliveryEvidenceState,
|
|
8
|
+
TaskDeliveryStage,
|
|
6
9
|
TaskEnvelopeKind,
|
|
7
10
|
TaskOutboxDeliveryError,
|
|
8
11
|
TaskProtocolError,
|
|
@@ -20,6 +23,7 @@ import type {
|
|
|
20
23
|
import type { TaskStore } from "./task-store";
|
|
21
24
|
|
|
22
25
|
const RECEIVE_PAGE_SIZE = 100;
|
|
26
|
+
const DELIVERY_EVIDENCE_OPERATION = "delivery_evidence";
|
|
23
27
|
const TARGET_NOT_REGISTERED_CODE = "TARGET_NOT_REGISTERED";
|
|
24
28
|
const TERMINAL_STATUSES = new Set<TaskRecord["status"]>(["completed", "failed", "cancelled", "timed_out"]);
|
|
25
29
|
const TERMINAL_EVENTS = new Set(["task.completed", "task.failed", "task.cancelled", "task.timed_out"]);
|
|
@@ -52,6 +56,10 @@ export interface SubmitIntentInput {
|
|
|
52
56
|
readonly payload: Record<string, unknown>;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
export type SubmitIntentOutcome =
|
|
60
|
+
| { readonly authority: "origin"; readonly canonicalEvent: { readonly type: string; readonly reused: boolean } }
|
|
61
|
+
| { readonly authority: "receiver" };
|
|
62
|
+
|
|
55
63
|
export interface TaskCore {
|
|
56
64
|
readonly endpoint: TaskEndpoint;
|
|
57
65
|
connect(signal?: AbortSignal): Promise<void>;
|
|
@@ -62,6 +70,7 @@ export interface TaskCore {
|
|
|
62
70
|
receive(signal?: AbortSignal): Promise<readonly RelayDelivery[]>;
|
|
63
71
|
acknowledgeRelayDelivery(cursor: string, signal?: AbortSignal): Promise<void>;
|
|
64
72
|
submitIntent(input: SubmitIntentInput, signal?: AbortSignal): Promise<void>;
|
|
73
|
+
submitIntentWithOutcome?(input: SubmitIntentInput, signal?: AbortSignal): Promise<SubmitIntentOutcome>;
|
|
65
74
|
recordInsertion(input: { readonly taskId: string; readonly eventId: string }, signal?: AbortSignal): Promise<void>;
|
|
66
75
|
evaluateTimeouts(signal?: AbortSignal): Promise<void>;
|
|
67
76
|
acknowledgeParent(taskId: string, signal?: AbortSignal): Promise<void>;
|
|
@@ -75,6 +84,36 @@ type ReceivedEnvelope =
|
|
|
75
84
|
export function createTaskCore(options: TaskCoreOptions): TaskCore {
|
|
76
85
|
const clock = options.clock ?? { now: (): number => Date.now() };
|
|
77
86
|
const ids = options.ids ?? (() => crypto.randomUUID());
|
|
87
|
+
const submitIntentWithOutcome = async (input: SubmitIntentInput, signal?: AbortSignal): Promise<SubmitIntentOutcome> => {
|
|
88
|
+
if (!RECEIVER_INTENT_TYPES.has(input.type) || !isRecord(input.payload)) throw new TaskProtocolError("INVALID_INTENT", "intent type or payload is invalid");
|
|
89
|
+
let persistedEnvelopeIds: readonly string[] = [];
|
|
90
|
+
let outcome: SubmitIntentOutcome | undefined;
|
|
91
|
+
let receiverTerminal = false;
|
|
92
|
+
let originCancellation = false;
|
|
93
|
+
const evidence = deliveryEvidence(input);
|
|
94
|
+
options.store.transaction(() => {
|
|
95
|
+
const task = requiredTask(options.store, input.taskId);
|
|
96
|
+
receiverTerminal = !sameEndpoint(options.endpoint, task.origin) && TERMINAL_EVENTS.has(input.type);
|
|
97
|
+
originCancellation = sameEndpoint(options.endpoint, task.origin) && input.type === "task.cancelled";
|
|
98
|
+
const persisted = persistIntent(options, clock.now, ids, task, input, evidence === undefined ? undefined : deliveryEvidenceOperation(evidence));
|
|
99
|
+
persistedEnvelopeIds = persisted.envelopeIds;
|
|
100
|
+
outcome = persisted.outcome;
|
|
101
|
+
});
|
|
102
|
+
if (evidence !== undefined) {
|
|
103
|
+
await flushDeliveryEvidence(options, signal, input.taskId, persistedEnvelopeIds, evidence);
|
|
104
|
+
} else {
|
|
105
|
+
try {
|
|
106
|
+
await flush(options, signal, new Set(persistedEnvelopeIds));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (originCancellation) throwIfCancellationDeliveryBlocked(options, input.taskId, persistedEnvelopeIds);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
if (originCancellation) throwIfCancellationDeliveryBlocked(options, input.taskId, persistedEnvelopeIds);
|
|
112
|
+
}
|
|
113
|
+
if (receiverTerminal) throwIfTerminalDeliveryBlocked(options.store, input.taskId);
|
|
114
|
+
if (outcome === undefined) throw new TaskProtocolError("INVALID_INTENT", "task intent outcome was not persisted");
|
|
115
|
+
return outcome;
|
|
116
|
+
};
|
|
78
117
|
|
|
79
118
|
return {
|
|
80
119
|
endpoint: options.endpoint,
|
|
@@ -137,27 +176,21 @@ export function createTaskCore(options: TaskCoreOptions): TaskCore {
|
|
|
137
176
|
await options.relay.acknowledgeDelivery({ endpoint: options.endpoint, cursor }, signal);
|
|
138
177
|
options.store.transaction(() => { options.store.setReceiveCursor(cursor); });
|
|
139
178
|
},
|
|
140
|
-
async submitIntent(input, signal) {
|
|
141
|
-
|
|
142
|
-
let persistedEnvelopeIds: readonly string[] = [];
|
|
143
|
-
let receiverTerminal = false;
|
|
144
|
-
options.store.transaction(() => {
|
|
145
|
-
const task = requiredTask(options.store, input.taskId);
|
|
146
|
-
receiverTerminal = !sameEndpoint(options.endpoint, task.origin) && TERMINAL_EVENTS.has(input.type);
|
|
147
|
-
persistedEnvelopeIds = persistIntent(options, clock.now, ids, task, input);
|
|
148
|
-
});
|
|
149
|
-
await flush(options, signal, new Set(persistedEnvelopeIds));
|
|
150
|
-
if (receiverTerminal) throwIfTerminalDeliveryBlocked(options.store, input.taskId);
|
|
151
|
-
},
|
|
179
|
+
async submitIntent(input, signal) { await submitIntentWithOutcome(input, signal); },
|
|
180
|
+
submitIntentWithOutcome,
|
|
152
181
|
async recordInsertion(input, signal) {
|
|
153
182
|
const task = requiredTask(options.store, input.taskId);
|
|
183
|
+
const evidence: DeliveryEvidence = { eventId: input.eventId, stage: TaskDeliveryStage.piInserted, state: TaskDeliveryEvidenceState.confirmed };
|
|
154
184
|
let persistedEnvelopeIds: readonly string[] = [];
|
|
155
185
|
options.store.transaction(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
186
|
+
options.store.putInsertionReceipt(input.taskId, input.eventId);
|
|
187
|
+
persistedEnvelopeIds = persistIntent(options, clock.now, ids, task, {
|
|
188
|
+
taskId: input.taskId,
|
|
189
|
+
type: "task.delivery_receipt",
|
|
190
|
+
payload: { eventId: input.eventId, stage: evidence.stage, state: evidence.state },
|
|
191
|
+
}, deliveryEvidenceOperation(evidence)).envelopeIds;
|
|
159
192
|
});
|
|
160
|
-
|
|
193
|
+
await flushDeliveryEvidence(options, signal, input.taskId, persistedEnvelopeIds, evidence);
|
|
161
194
|
},
|
|
162
195
|
async evaluateTimeouts(signal) {
|
|
163
196
|
const persistedEnvelopeIds: string[] = [];
|
|
@@ -166,7 +199,7 @@ export function createTaskCore(options: TaskCoreOptions): TaskCore {
|
|
|
166
199
|
options.store.transaction(() => {
|
|
167
200
|
const task = options.store.getTask(candidate.taskId);
|
|
168
201
|
if (!task || !sameEndpoint(task.origin, options.endpoint) || TERMINAL_STATUSES.has(task.status) || task.expiresAt > clock.now()) return;
|
|
169
|
-
persistedEnvelopeIds.push(...canonicalize(options, clock.now, ids, task, { intentId: ids(), taskId: task.taskId, type: "task.cancelled", payload: {} }, "task.timed_out"));
|
|
202
|
+
persistedEnvelopeIds.push(...canonicalize(options, clock.now, ids, task, { intentId: ids(), taskId: task.taskId, type: "task.cancelled", payload: {} }, "task.timed_out").envelopeIds);
|
|
170
203
|
});
|
|
171
204
|
}
|
|
172
205
|
if (persistedEnvelopeIds.length > 0) await flush(options, signal, new Set(persistedEnvelopeIds));
|
|
@@ -177,7 +210,7 @@ export function createTaskCore(options: TaskCoreOptions): TaskCore {
|
|
|
177
210
|
const task = requiredTask(options.store, taskId);
|
|
178
211
|
if (!sameEndpoint(task.origin, options.endpoint)) throw new TaskProtocolError("NOT_ORIGIN", "only origin may acknowledge a task");
|
|
179
212
|
if (!TERMINAL_STATUSES.has(task.status)) throw new TaskProtocolError("TASK_NOT_TERMINAL", "only a terminal task may be acknowledged", { retryable: false });
|
|
180
|
-
persistedEnvelopeIds = canonicalize(options, clock.now, ids, task, { intentId: ids(), taskId, type: "task.information", payload: {} }, "task.parent_acknowledged", PARENT_ACKNOWLEDGMENT_OPERATION);
|
|
213
|
+
persistedEnvelopeIds = canonicalize(options, clock.now, ids, task, { intentId: ids(), taskId, type: "task.information", payload: {} }, "task.parent_acknowledged", PARENT_ACKNOWLEDGMENT_OPERATION).envelopeIds;
|
|
181
214
|
});
|
|
182
215
|
await flush(options, signal, new Set(persistedEnvelopeIds));
|
|
183
216
|
},
|
|
@@ -235,41 +268,80 @@ function persistReceivedEnvelope(options: TaskCoreOptions, now: () => number, id
|
|
|
235
268
|
options.store.appendEvent(received.event);
|
|
236
269
|
return [];
|
|
237
270
|
}
|
|
238
|
-
if (received.kind === "intent") return canonicalize(options, now, ids, received.task, received.intent, received.intent.type);
|
|
271
|
+
if (received.kind === "intent") return canonicalize(options, now, ids, received.task, received.intent, received.intent.type).envelopeIds;
|
|
239
272
|
if (options.store.appendEvent(received.event) && TERMINAL_EVENTS.has(received.event.type)) options.store.setStatus(received.task.taskId, statusFor(received.event.type));
|
|
240
273
|
return [];
|
|
241
274
|
}
|
|
242
275
|
|
|
243
|
-
|
|
276
|
+
interface PersistedIntent {
|
|
277
|
+
readonly envelopeIds: readonly string[];
|
|
278
|
+
readonly outcome: SubmitIntentOutcome;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function persistIntent(options: TaskCoreOptions, now: () => number, ids: () => string, task: TaskRecord, input: SubmitIntentInput, reservedOperation?: string): PersistedIntent {
|
|
244
282
|
if (sameEndpoint(options.endpoint, task.origin)) {
|
|
245
|
-
|
|
283
|
+
const operation = input.type === "task.cancelled" ? ORIGIN_CANCELLATION_OPERATION : reservedOperation;
|
|
284
|
+
const canonical = canonicalize(options, now, ids, task, { intentId: ids(), taskId: input.taskId, type: input.type, payload: input.payload }, input.type, operation);
|
|
285
|
+
return { envelopeIds: canonical.envelopeIds, outcome: { authority: "origin", canonicalEvent: { type: canonical.eventType, reused: canonical.reused } } };
|
|
246
286
|
}
|
|
247
287
|
const envelopeId = ids();
|
|
248
288
|
const intent: TaskIntent = { intentId: ids(), taskId: input.taskId, type: input.type, payload: input.payload };
|
|
249
|
-
|
|
289
|
+
const operation = TERMINAL_EVENTS.has(input.type) ? TERMINAL_INTENT_OPERATION : reservedOperation;
|
|
290
|
+
if (operation !== undefined) {
|
|
250
291
|
const reservation = options.store.reserveTaskOperation({
|
|
251
292
|
taskId: input.taskId,
|
|
252
|
-
operation
|
|
293
|
+
operation,
|
|
253
294
|
logicalId: intent.intentId,
|
|
254
295
|
logicalType: intent.type,
|
|
255
296
|
envelopeIds: [envelopeId],
|
|
256
297
|
});
|
|
257
298
|
if (!reservation.created) {
|
|
258
|
-
if (reservation.record.logicalType !== intent.type) {
|
|
299
|
+
if (operation === TERMINAL_INTENT_OPERATION && reservation.record.logicalType !== intent.type) {
|
|
259
300
|
throw new TaskProtocolError("TERMINAL_INTENT_CONFLICT", "terminal task intent conflicts with the existing terminal action", {
|
|
260
301
|
retryable: false,
|
|
261
302
|
details: { taskId: input.taskId, existingType: reservation.record.logicalType, requestedType: intent.type },
|
|
262
303
|
});
|
|
263
304
|
}
|
|
264
|
-
return reservation.record.envelopeIds;
|
|
305
|
+
return { envelopeIds: reservation.record.envelopeIds, outcome: { authority: "receiver" } };
|
|
265
306
|
}
|
|
266
307
|
}
|
|
267
308
|
options.store.putIntent(intent.intentId, input.taskId, envelopeId);
|
|
268
309
|
options.store.putOutbox(envelope(envelopeId, options.endpoint, task.origin, input.taskId, TaskEnvelopeKind.intent, intent));
|
|
269
|
-
return [envelopeId];
|
|
310
|
+
return { envelopeIds: [envelopeId], outcome: { authority: "receiver" } };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface DeliveryEvidence {
|
|
314
|
+
readonly eventId: string;
|
|
315
|
+
readonly stage: TaskDeliveryStage;
|
|
316
|
+
readonly state: TaskDeliveryEvidenceState;
|
|
317
|
+
readonly retryable?: true;
|
|
270
318
|
}
|
|
271
319
|
|
|
272
|
-
function
|
|
320
|
+
function deliveryEvidence(input: SubmitIntentInput): DeliveryEvidence | undefined {
|
|
321
|
+
if (input.type !== "task.delivery_receipt" || typeof input.payload.eventId !== "string") return undefined;
|
|
322
|
+
const stage = input.payload.stage;
|
|
323
|
+
const state = input.payload.state;
|
|
324
|
+
if (stage === TaskDeliveryStage.piInsertion && state === TaskDeliveryEvidenceState.blocked && input.payload.retryable === true) {
|
|
325
|
+
return { eventId: input.payload.eventId, stage, state, retryable: true };
|
|
326
|
+
}
|
|
327
|
+
if ((stage === TaskDeliveryStage.receiverPersisted || stage === TaskDeliveryStage.piInserted || stage === TaskDeliveryStage.wakeRequested || stage === TaskDeliveryStage.wakeAccepted)
|
|
328
|
+
&& state === TaskDeliveryEvidenceState.confirmed) {
|
|
329
|
+
return { eventId: input.payload.eventId, stage, state };
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function deliveryEvidenceOperation(input: DeliveryEvidence): string {
|
|
335
|
+
return `${DELIVERY_EVIDENCE_OPERATION}:${input.eventId}:${input.stage}:${input.state}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface Canonicalization {
|
|
339
|
+
readonly envelopeIds: readonly string[];
|
|
340
|
+
readonly eventType: string;
|
|
341
|
+
readonly reused: boolean;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function canonicalize(options: TaskCoreOptions, now: () => number, ids: () => string, task: TaskRecord, intent: TaskIntent, requestedType: string, operation?: string): Canonicalization {
|
|
273
345
|
const terminal = TERMINAL_EVENTS.has(requestedType);
|
|
274
346
|
const type = terminal && TERMINAL_STATUSES.has(task.status) ? "task.late_terminal" : requestedType;
|
|
275
347
|
const sequence = String(task.events.length + 1);
|
|
@@ -279,13 +351,23 @@ function canonicalize(options: TaskCoreOptions, now: () => number, ids: () => st
|
|
|
279
351
|
const persistedEnvelopeIds = originEnvelope === undefined ? [targetEnvelope.envelopeId] : [targetEnvelope.envelopeId, originEnvelope.envelopeId];
|
|
280
352
|
if (operation !== undefined) {
|
|
281
353
|
const reservation = options.store.reserveTaskOperation({ taskId: task.taskId, operation, logicalId: canonical.eventId, logicalType: canonical.type, envelopeIds: persistedEnvelopeIds });
|
|
282
|
-
if (!reservation.created) return reservation.record.envelopeIds;
|
|
354
|
+
if (!reservation.created) return { envelopeIds: reservation.record.envelopeIds, eventType: reservation.record.logicalType, reused: true };
|
|
283
355
|
}
|
|
284
356
|
options.store.appendEvent(canonical);
|
|
285
357
|
if (TERMINAL_EVENTS.has(type)) options.store.setStatus(task.taskId, statusFor(type));
|
|
286
358
|
options.store.putOutbox(targetEnvelope);
|
|
287
359
|
if (originEnvelope !== undefined) options.store.putOutbox(originEnvelope);
|
|
288
|
-
return persistedEnvelopeIds;
|
|
360
|
+
return { envelopeIds: persistedEnvelopeIds, eventType: type, reused: false };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function flushDeliveryEvidence(options: TaskCoreOptions, signal: AbortSignal | undefined, taskId: string, envelopeIds: readonly string[], evidence: DeliveryEvidence): Promise<void> {
|
|
364
|
+
try {
|
|
365
|
+
if (envelopeIds.length > 0) await flush(options, signal, new Set(envelopeIds));
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throwIfDeliveryEvidenceBlocked(options.store, taskId, envelopeIds, evidence);
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
throwIfDeliveryEvidenceBlocked(options.store, taskId, envelopeIds, evidence);
|
|
289
371
|
}
|
|
290
372
|
|
|
291
373
|
async function flush(options: TaskCoreOptions, signal: AbortSignal | undefined, reportedEnvelopeIds?: ReadonlySet<string>): Promise<void> {
|
|
@@ -339,6 +421,34 @@ function parsePayload(envelope: RelayEnvelope): unknown {
|
|
|
339
421
|
try { return JSON.parse(envelope.payload) as unknown; } catch { throw new TaskProtocolError("INVALID_PAYLOAD", "relay envelope payload is not valid task protocol JSON"); }
|
|
340
422
|
}
|
|
341
423
|
|
|
424
|
+
function throwIfCancellationDeliveryBlocked(options: TaskCoreOptions, taskId: string, envelopeIds: readonly string[]): void {
|
|
425
|
+
const task = requiredTask(options.store, taskId);
|
|
426
|
+
const blocked = options.store.quarantinedOutbox().find((record) => envelopeIds.includes(record.envelope.envelopeId) && sameEndpoint(record.envelope.target, task.target));
|
|
427
|
+
if (blocked === undefined) return;
|
|
428
|
+
throw new TaskOutboxDeliveryError(blocked.errorCode, blocked.reason, {
|
|
429
|
+
retryable: false,
|
|
430
|
+
details: { ...blocked.details, taskId, envelopeId: blocked.envelope.envelopeId, target: task.target, blockedAt: blocked.quarantinedAt },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function throwIfDeliveryEvidenceBlocked(store: TaskStore, taskId: string, envelopeIds: readonly string[], evidence: DeliveryEvidence): void {
|
|
435
|
+
const blocked = store.quarantinedOutbox().find((record) => envelopeIds.includes(record.envelope.envelopeId));
|
|
436
|
+
if (blocked === undefined) return;
|
|
437
|
+
throw new TaskOutboxDeliveryError(blocked.errorCode, "task delivery evidence is blocked", {
|
|
438
|
+
retryable: false,
|
|
439
|
+
details: {
|
|
440
|
+
...blocked.details,
|
|
441
|
+
taskId,
|
|
442
|
+
eventId: evidence.eventId,
|
|
443
|
+
stage: evidence.stage,
|
|
444
|
+
state: evidence.state,
|
|
445
|
+
envelopeId: blocked.envelope.envelopeId,
|
|
446
|
+
target: blocked.envelope.target,
|
|
447
|
+
blockedAt: blocked.quarantinedAt,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
342
452
|
function throwIfTerminalDeliveryBlocked(store: TaskStore, taskId: string): void {
|
|
343
453
|
const delivery = requiredTask(store, taskId).terminalDelivery;
|
|
344
454
|
if (delivery.state !== "delivery_blocked") return;
|
package/src/task-inbox.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import type { TaskCore } from "./task-core";
|
|
2
|
-
import { TaskEnvelopeKind, TaskProtocolError } from "./task-protocol";
|
|
2
|
+
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskEnvelopeKind, TaskProtocolError } from "./task-protocol";
|
|
3
3
|
import type { RelayDelivery, TaskEvent } from "./task-protocol";
|
|
4
4
|
|
|
5
5
|
const TASK_EVENT_CUSTOM_TYPE = "pi-tasks-event";
|
|
6
|
+
const TASK_WAKE_CUSTOM_TYPE = "pi-tasks-wake";
|
|
6
7
|
const TASK_CURSOR_CUSTOM_TYPE = "pi-tasks-relay-cursor";
|
|
7
8
|
|
|
8
9
|
interface InboxContext {
|
|
10
|
+
readonly isIdle: () => boolean;
|
|
9
11
|
readonly hasPendingMessages: () => boolean;
|
|
10
12
|
readonly sessionManager: { readonly getEntries: () => readonly unknown[] };
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
interface InboxPi {
|
|
14
|
-
sendMessage(
|
|
16
|
+
sendMessage(
|
|
17
|
+
message: { readonly customType: string; readonly content: string; readonly display: boolean; readonly details: TaskEventDetails },
|
|
18
|
+
options: { readonly triggerTurn: false } | { readonly triggerTurn: true; readonly deliverAs: "followUp" },
|
|
19
|
+
): void;
|
|
15
20
|
appendEntry(customType: string, data: { readonly cursor: string }): void;
|
|
16
21
|
}
|
|
17
22
|
|
|
@@ -20,8 +25,12 @@ export interface TaskEventDetails {
|
|
|
20
25
|
readonly eventId: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
type DeliveryEvidencePayload =
|
|
29
|
+
| { readonly stage: typeof TaskDeliveryStage.receiverPersisted | typeof TaskDeliveryStage.wakeRequested | typeof TaskDeliveryStage.wakeAccepted; readonly state: typeof TaskDeliveryEvidenceState.confirmed }
|
|
30
|
+
| { readonly stage: typeof TaskDeliveryStage.piInsertion; readonly state: typeof TaskDeliveryEvidenceState.blocked; readonly retryable: true };
|
|
31
|
+
|
|
32
|
+
/** Persists model-visible Pi evidence before advancing the relay cursor, then starts one separate turn. */
|
|
33
|
+
export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: InboxContext, signal?: AbortSignal): Promise<void> {
|
|
25
34
|
if (context.hasPendingMessages()) return;
|
|
26
35
|
const deliveries = await core.receive(signal);
|
|
27
36
|
for (const delivery of deliveries) {
|
|
@@ -33,28 +42,52 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
|
|
|
33
42
|
const event = inboxEvent(delivery);
|
|
34
43
|
if (!isKnownEvent(event.type)) throw new TaskProtocolError("UNKNOWN_EVENT", `unknown task inbox event type: ${event.type}`);
|
|
35
44
|
if (context.hasPendingMessages()) return;
|
|
45
|
+
const eventDetails = { taskId: event.taskId, eventId: event.eventId };
|
|
36
46
|
const eventKey = key(event.taskId, event.eventId);
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
if (event.type === "task.created") {
|
|
48
|
+
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.receiverPersisted, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
49
|
+
}
|
|
50
|
+
let incorporated = incorporatedEvents(context.sessionManager.getEntries()).has(eventKey);
|
|
39
51
|
if (!incorporated && isModelVisible(event.type)) {
|
|
40
|
-
if (!
|
|
52
|
+
if (!context.isIdle()) return;
|
|
53
|
+
pi.sendMessage({
|
|
54
|
+
customType: TASK_EVENT_CUSTOM_TYPE,
|
|
55
|
+
content: renderTaskEvent(event),
|
|
56
|
+
display: true,
|
|
57
|
+
details: eventDetails,
|
|
58
|
+
}, { triggerTurn: false });
|
|
59
|
+
incorporated = incorporatedEvents(context.sessionManager.getEntries()).has(eventKey);
|
|
60
|
+
if (!incorporated) {
|
|
61
|
+
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.piInsertion, state: TaskDeliveryEvidenceState.blocked, retryable: true }, signal);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (isModelVisible(event.type)) {
|
|
66
|
+
await core.recordInsertion(eventDetails, signal);
|
|
67
|
+
let wakeAccepted = taskMessageKeys(context.sessionManager.getEntries(), TASK_WAKE_CUSTOM_TYPE).has(eventKey);
|
|
68
|
+
if (!wakeAccepted) {
|
|
69
|
+
if (!context.isIdle()) return;
|
|
70
|
+
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.wakeRequested, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
41
71
|
pi.sendMessage({
|
|
42
|
-
customType:
|
|
43
|
-
content:
|
|
44
|
-
display:
|
|
45
|
-
details:
|
|
72
|
+
customType: TASK_WAKE_CUSTOM_TYPE,
|
|
73
|
+
content: "Process the pending Pi task event.",
|
|
74
|
+
display: false,
|
|
75
|
+
details: eventDetails,
|
|
46
76
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
47
|
-
|
|
77
|
+
wakeAccepted = taskMessageKeys(context.sessionManager.getEntries(), TASK_WAKE_CUSTOM_TYPE).has(eventKey);
|
|
78
|
+
if (!wakeAccepted) return;
|
|
48
79
|
}
|
|
49
|
-
|
|
50
|
-
pendingInsertions.delete(eventKey);
|
|
80
|
+
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.wakeAccepted, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
51
81
|
}
|
|
52
|
-
if (isModelVisible(event.type)) await core.recordInsertion({ taskId: event.taskId, eventId: event.eventId }, signal);
|
|
53
82
|
await core.acknowledgeRelayDelivery(delivery.cursor, signal);
|
|
54
83
|
pi.appendEntry(TASK_CURSOR_CUSTOM_TYPE, { cursor: delivery.cursor });
|
|
55
84
|
}
|
|
56
85
|
}
|
|
57
86
|
|
|
87
|
+
async function recordDeliveryEvidence(core: TaskCore, event: TaskEventDetails, evidence: DeliveryEvidencePayload, signal?: AbortSignal): Promise<void> {
|
|
88
|
+
await core.submitIntent({ taskId: event.taskId, type: "task.delivery_receipt", payload: { eventId: event.eventId, ...evidence } }, signal);
|
|
89
|
+
}
|
|
90
|
+
|
|
58
91
|
function inboxEvent(delivery: RelayDelivery): TaskEvent {
|
|
59
92
|
try {
|
|
60
93
|
const payload = JSON.parse(delivery.envelope.payload) as unknown;
|
|
@@ -81,16 +114,24 @@ function renderTaskEvent(event: TaskEvent): string {
|
|
|
81
114
|
}
|
|
82
115
|
|
|
83
116
|
export function incorporatedTaskEvents(entries: readonly unknown[]): readonly TaskEventDetails[] {
|
|
117
|
+
return taskMessageDetails(entries, TASK_EVENT_CUSTOM_TYPE);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function taskMessageDetails(entries: readonly unknown[], customType: string): readonly TaskEventDetails[] {
|
|
84
121
|
const events: TaskEventDetails[] = [];
|
|
85
122
|
for (const entry of entries) {
|
|
86
|
-
if (!isRecord(entry) || entry.type !== "custom_message" || entry.customType !==
|
|
123
|
+
if (!isRecord(entry) || entry.type !== "custom_message" || entry.customType !== customType || !isRecord(entry.details) || typeof entry.details.taskId !== "string" || typeof entry.details.eventId !== "string") continue;
|
|
87
124
|
events.push({ taskId: entry.details.taskId, eventId: entry.details.eventId });
|
|
88
125
|
}
|
|
89
126
|
return events;
|
|
90
127
|
}
|
|
91
128
|
|
|
129
|
+
function taskMessageKeys(entries: readonly unknown[], customType: string): Set<string> {
|
|
130
|
+
return new Set(taskMessageDetails(entries, customType).map((event) => key(event.taskId, event.eventId)));
|
|
131
|
+
}
|
|
132
|
+
|
|
92
133
|
function incorporatedEvents(entries: readonly unknown[]): Set<string> {
|
|
93
|
-
return
|
|
134
|
+
return taskMessageKeys(entries, TASK_EVENT_CUSTOM_TYPE);
|
|
94
135
|
}
|
|
95
136
|
|
|
96
137
|
function isTaskEvent(value: unknown): value is TaskEvent {
|
package/src/task-protocol.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
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
|
|
|
7
|
+
export const TaskDeliveryStage = {
|
|
8
|
+
receiverPersisted: "receiver_persisted",
|
|
9
|
+
piInsertion: "pi_insertion",
|
|
10
|
+
piInserted: "pi_inserted",
|
|
11
|
+
wakeRequested: "wake_requested",
|
|
12
|
+
wakeAccepted: "wake_accepted",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const TaskDeliveryEvidenceState = {
|
|
16
|
+
confirmed: "confirmed",
|
|
17
|
+
blocked: "blocked",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
6
20
|
export interface TaskEndpoint {
|
|
7
21
|
readonly relay: string;
|
|
8
22
|
readonly id: string;
|
|
@@ -104,6 +118,9 @@ export interface TaskRecord {
|
|
|
104
118
|
|
|
105
119
|
export type TerminalTaskIntentType = "task.completed" | "task.failed" | "task.cancelled";
|
|
106
120
|
|
|
121
|
+
export type TaskDeliveryStage = (typeof TaskDeliveryStage)[keyof typeof TaskDeliveryStage];
|
|
122
|
+
export type TaskDeliveryEvidenceState = (typeof TaskDeliveryEvidenceState)[keyof typeof TaskDeliveryEvidenceState];
|
|
123
|
+
|
|
107
124
|
export interface TaskIntent {
|
|
108
125
|
readonly intentId: string;
|
|
109
126
|
readonly taskId: string;
|