@sgtbeatdown/pi-tasks 0.1.0

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 ADDED
@@ -0,0 +1,514 @@
1
+ # pi-tasks
2
+
3
+ Structured task communication for Pi agents.
4
+
5
+ `pi-tasks` lets one Pi session hand work to another agent, session, process, or
6
+ worker and get durable structured results back. The core model is transport- and
7
+ runner-neutral: local sessions, HTTP workers, queues, hosted control planes, and
8
+ terminal multiplexers can all implement the same task lifecycle.
9
+
10
+ ## what this is
11
+
12
+ A task lifecycle and communication layer:
13
+
14
+ 1. create a structured assignment
15
+ 2. persist task state in a store
16
+ 3. dispatch the assignment through a transport
17
+ 4. let the sender keep working
18
+ 5. record status/results in structured files/data, not terminal prose
19
+ 6. require the assignee to finish through the task protocol
20
+
21
+ ## what this is not
22
+
23
+ This is **not** a subagent spawner. Use any process manager, terminal/session
24
+ manager, HTTP worker pool, queue, cron job, or custom runner to create/host
25
+ agents. `pi-tasks` only gives those agents a shared task protocol.
26
+
27
+ ## mental model
28
+
29
+ ```text
30
+ parent pi session
31
+ agent_task_send
32
+ ↓
33
+ TaskStore creates task + durable refs
34
+ TaskTransport delivers assignment text to target
35
+ ↓
36
+ target runner/session/worker
37
+ receives pi.task.assignment.v1
38
+ does work
39
+ completes task through agent_task_done or equivalent store/server completion API
40
+ ↓
41
+ parent pi session
42
+ agent_task_status / agent_task_wait / agent_task_inbox
43
+ ```
44
+
45
+ The `to` field is transport-specific:
46
+
47
+ - session name
48
+ - worker id
49
+ - queue/topic name
50
+ - tmux pane
51
+ - process id
52
+ - service-specific address
53
+
54
+ ## requirements for any runner
55
+
56
+ For task communication to work, you need exactly these pieces:
57
+
58
+ 1. **running target** — an already-open Pi session, worker, process, or service.
59
+ `pi-tasks` does not open or close it.
60
+ 2. **transport** — code that delivers the assignment text to that target. This
61
+ can be `tmux paste-buffer`, HTTP POST, queue publish, file write, ssh, etc.
62
+ 3. **reachable task store** — parent and target must read/write the same task
63
+ state. With `createFilesystemTaskStore`, that means the same cwd and shared
64
+ `.pi/tasks` directory. For multi-host setups without shared disk, implement a
65
+ remote `TaskStore`.
66
+ 4. **completion capability** — Pi targets need this extension loaded so they have
67
+ `agent_task_done`. Non-Pi workers need an equivalent completion API/store
68
+ write.
69
+ 5. **preserved task id** — the target must complete the exact `taskId` from the
70
+ `pi.task.assignment.v1` assignment.
71
+
72
+ If those are true, session lifecycle is irrelevant. Open agents with tmux,
73
+ screen, ssh, a process manager, or manually; `pi-tasks` only handles the task
74
+ protocol once the agents exist.
75
+
76
+ ## install
77
+
78
+ Install locally into Pi:
79
+
80
+ ```bash
81
+ pi install npm:@sgtbeatdown/pi-tasks
82
+ ```
83
+
84
+ Temporary one-off run:
85
+
86
+ ```bash
87
+ pi -e npm:@sgtbeatdown/pi-tasks
88
+ ```
89
+
90
+ Every participating Pi session must load this extension. If a target Pi session
91
+ does not have `agent_task_done`, the sender can still create/dispatch the task,
92
+ but it will remain non-terminal until timeout or cancellation.
93
+
94
+ ## quick start: local/custom transport
95
+
96
+ Most integrations reuse the filesystem store and provide only a transport:
97
+
98
+ ```ts
99
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
100
+ import { registerAgentTaskTools } from "@sgtbeatdown/pi-tasks/src/extension";
101
+ import { createFilesystemTaskStore } from "@sgtbeatdown/pi-tasks/src/stores/filesystem";
102
+ import type { TaskTransport } from "@sgtbeatdown/pi-tasks/src/task-communication";
103
+
104
+ function createCliTransport(pi: ExtensionAPI): TaskTransport {
105
+ return {
106
+ name: "local-cli",
107
+ getCurrentSessionName(env) {
108
+ return env.PI_TASK_SESSION ?? env.USER ?? "unknown-session";
109
+ },
110
+ async dispatchTask({ target, assignment, signal }) {
111
+ const result = await pi.exec("my-session-send", [target, assignment], { signal });
112
+ if (result.code === 0) return { ok: true };
113
+ return {
114
+ ok: false,
115
+ message: result.stderr || result.stdout || "my-session-send failed",
116
+ retryable: true,
117
+ };
118
+ },
119
+ };
120
+ }
121
+
122
+ export default function extension(pi: ExtensionAPI): void {
123
+ registerAgentTaskTools(pi, {
124
+ store: createFilesystemTaskStore({ tasksDir: ".pi/tasks" }),
125
+ transport: createCliTransport(pi),
126
+ });
127
+ }
128
+ ```
129
+
130
+ Then call `agent_task_send` from the parent session:
131
+
132
+ ```json
133
+ {
134
+ "to": "worker-a",
135
+ "task": "inspect the auth middleware and report risks",
136
+ "timeoutMs": 1800000
137
+ }
138
+ ```
139
+
140
+ The parent should keep working and later use `agent_task_status`,
141
+ `agent_task_wait`, or `agent_task_inbox`. Do **not** infer completion from
142
+ terminal output.
143
+
144
+ ## tmux recipe: already-open pi agents
145
+
146
+ Use this when you manually open Pi agents in tmux panes and only need task
147
+ communication between them.
148
+
149
+ ### 1. create a tmux transport extension
150
+
151
+ Create `.pi/extensions/pi-tasks-tmux.ts` in the project:
152
+
153
+ ```ts
154
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
155
+ import { registerAgentTaskTools } from "@sgtbeatdown/pi-tasks/src/extension";
156
+ import { createFilesystemTaskStore } from "@sgtbeatdown/pi-tasks/src/stores/filesystem";
157
+ import type { TaskTransport } from "@sgtbeatdown/pi-tasks/src/task-communication";
158
+
159
+ function createTmuxTransport(pi: ExtensionAPI): TaskTransport {
160
+ return {
161
+ name: "tmux",
162
+ getCurrentSessionName(env) {
163
+ return env.PI_TASK_SESSION ?? env.TMUX_PANE ?? `pid-${process.pid}`;
164
+ },
165
+ async dispatchTask({ target, assignment, task, signal }) {
166
+ const bufferName = `pi-task-${task.id}`;
167
+ const setBuffer = await pi.exec("tmux", ["set-buffer", "-b", bufferName, assignment], { signal });
168
+ if (setBuffer.code !== 0) {
169
+ return {
170
+ ok: false,
171
+ message: setBuffer.stderr || setBuffer.stdout || "tmux set-buffer failed",
172
+ retryable: true,
173
+ };
174
+ }
175
+
176
+ const paste = await pi.exec("tmux", ["paste-buffer", "-d", "-b", bufferName, "-t", target], { signal });
177
+ if (paste.code !== 0) {
178
+ return {
179
+ ok: false,
180
+ message: paste.stderr || paste.stdout || "tmux paste-buffer failed",
181
+ retryable: true,
182
+ };
183
+ }
184
+
185
+ const enter = await pi.exec("tmux", ["send-keys", "-t", target, "Enter"], { signal });
186
+ if (enter.code !== 0) {
187
+ return {
188
+ ok: false,
189
+ message: enter.stderr || enter.stdout || "tmux send-keys failed",
190
+ retryable: true,
191
+ };
192
+ }
193
+
194
+ return { ok: true };
195
+ },
196
+ };
197
+ }
198
+
199
+ export default function extension(pi: ExtensionAPI): void {
200
+ registerAgentTaskTools(pi, {
201
+ // All tmux panes must run in the same project cwd so they share .pi/tasks.
202
+ store: createFilesystemTaskStore({ tasksDir: ".pi/tasks" }),
203
+ transport: createTmuxTransport(pi),
204
+ });
205
+ }
206
+ ```
207
+
208
+ This transport does not open panes. It only pastes assignment text into an
209
+ already-running pane and presses Enter.
210
+
211
+ ### 2. open the agents yourself
212
+
213
+ Start each Pi agent in the same repo/cwd and load the extension. Give each one a
214
+ stable session name:
215
+
216
+ ```bash
217
+ # pane 1
218
+ PI_TASK_SESSION=parent pi -e ./.pi/extensions/pi-tasks-tmux.ts
219
+
220
+ # pane 2
221
+ PI_TASK_SESSION=worker-a pi -e ./.pi/extensions/pi-tasks-tmux.ts
222
+ ```
223
+
224
+ If the extension is installed with `pi install`, you can omit `-e`; the important
225
+ parts are same cwd, same `.pi/tasks`, and distinct `PI_TASK_SESSION` values.
226
+
227
+ ### 3. find the target pane id
228
+
229
+ From inside tmux:
230
+
231
+ ```bash
232
+ tmux list-panes -a -F '#{pane_id} #{pane_current_path} #{pane_title}'
233
+ ```
234
+
235
+ Pane ids look like `%12`. Use that pane id as `agent_task_send.to`.
236
+
237
+ ### 4. send a task
238
+
239
+ From the parent Pi session, call:
240
+
241
+ ```json
242
+ {
243
+ "to": "%12",
244
+ "task": "inspect the auth middleware and report risks",
245
+ "timeoutMs": 1800000
246
+ }
247
+ ```
248
+
249
+ Expected flow:
250
+
251
+ 1. parent creates `.pi/tasks/<taskId>/...`
252
+ 2. tmux transport pastes the assignment into pane `%12`
253
+ 3. worker receives `pi.task.assignment.v1`
254
+ 4. worker does the work
255
+ 5. worker calls `agent_task_done` with that `taskId`
256
+ 6. parent reads the result via `agent_task_status`, `agent_task_wait`, or
257
+ `agent_task_inbox`
258
+
259
+ ### tmux troubleshooting
260
+
261
+ - If the target cannot complete the task, confirm it loaded the extension and has
262
+ `agent_task_done` available.
263
+ - If status never changes, confirm parent and worker are in the same cwd and both
264
+ can see the same `.pi/tasks/<taskId>` directory.
265
+ - If dispatch fails, run `tmux paste-buffer` manually against the same target id;
266
+ the pane id may be stale.
267
+ - Do not use terminal output as the source of truth. The task result is the store
268
+ state, not visible prose in the pane.
269
+
270
+ ## using with non-pi workers
271
+
272
+ A non-Pi worker can participate if it implements the same lifecycle:
273
+
274
+ 1. receive `pi.task.assignment.v1`
275
+ 2. do the assigned work
276
+ 3. write/report a terminal result with the same status and payload semantics
277
+ 4. never overwrite an already-terminal task
278
+
279
+ Example HTTP transport from a Pi extension:
280
+
281
+ ```ts
282
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
283
+ import { registerAgentTaskTools } from "@sgtbeatdown/pi-tasks/src/extension";
284
+ import { createFilesystemTaskStore } from "@sgtbeatdown/pi-tasks/src/stores/filesystem";
285
+ import type { TaskTransport } from "@sgtbeatdown/pi-tasks/src/task-communication";
286
+
287
+ const httpTransport: TaskTransport = {
288
+ name: "http",
289
+ getCurrentSessionName(env) {
290
+ return env.PI_TASK_SESSION ?? "unknown-session";
291
+ },
292
+ async dispatchTask({ target, assignment, task, signal }) {
293
+ const response = await fetch(`https://tasks.example/sessions/${target}/assignments`, {
294
+ method: "POST",
295
+ headers: { "content-type": "application/json" },
296
+ body: JSON.stringify({ taskId: task.id, assignment }),
297
+ signal,
298
+ });
299
+
300
+ if (response.ok) return { ok: true };
301
+
302
+ return {
303
+ ok: false,
304
+ message: await response.text(),
305
+ retryable: response.status >= 500,
306
+ };
307
+ },
308
+ };
309
+
310
+ export default function extension(pi: ExtensionAPI): void {
311
+ registerAgentTaskTools(pi, {
312
+ store: createFilesystemTaskStore({ tasksDir: ".pi/tasks" }),
313
+ transport: httpTransport,
314
+ });
315
+ }
316
+ ```
317
+
318
+ For non-Pi workers, expose a completion endpoint or let workers use your store
319
+ implementation directly. The worker should report a terminal result equivalent
320
+ to:
321
+
322
+ ```json
323
+ {
324
+ "taskId": "task_...",
325
+ "status": "completed",
326
+ "summary": "finished the assigned work",
327
+ "result": {}
328
+ }
329
+ ```
330
+
331
+ ## file inbox / polling workers
332
+
333
+ A pushed message is not required. A transport can durably drop assignments into a
334
+ file inbox or queue and return success once the assignment is written.
335
+
336
+ One simple design:
337
+
338
+ - store tasks in `.pi/tasks`
339
+ - transport writes assignment text to `.pi/task-inbox/<target>/<taskId>.txt`
340
+ - worker watches its inbox directory
341
+ - worker reads the assignment, does the work, then completes through a store API
342
+ or by invoking a Pi session that has `agent_task_done`
343
+
344
+ Completion is separate from dispatch. Dispatch means “assignment delivered,” not
345
+ “work finished.”
346
+
347
+ ## when to implement a custom store
348
+
349
+ Do **not** implement a custom store just because you have a new dispatch
350
+ mechanism. Implement `TaskStore` only when task state itself must be somewhere
351
+ else:
352
+
353
+ - central HTTP service
354
+ - Redis/Postgres/SQLite shared by multiple machines
355
+ - multi-host queue system
356
+ - hosted control plane
357
+
358
+ If only delivery changes, keep the filesystem store.
359
+
360
+ ## included wolfpack transport
361
+
362
+ This package includes a Wolfpack transport for convenience. It is only a delivery
363
+ adapter:
364
+
365
+ ```bash
366
+ wolfpack session send <target-session> <assignment>
367
+ ```
368
+
369
+ It does not own storage. The default exported extension uses the generic
370
+ filesystem store at `.pi/tasks/` plus the included Wolfpack transport.
371
+
372
+ If you are using Wolfpack, install this extension in every participating Pi
373
+ session and target a session name/id with `agent_task_send`.
374
+
375
+ If you are not using Wolfpack, register the tools with your own `{ store,
376
+ transport }` composition.
377
+
378
+ ## tools
379
+
380
+ | tool | purpose |
381
+ | --- | --- |
382
+ | `agent_task_send` | create a task and dispatch it through the configured transport |
383
+ | `agent_task_status` | read compact structured status for one task |
384
+ | `agent_task_wait` | wait in tool code for a terminal result when the user wants it now |
385
+ | `agent_task_inbox` | list terminal tasks for the current parent session |
386
+ | `agent_task_cancel` | cancel a non-terminal task |
387
+ | `agent_task_done` | assignee-side structured completion; terminates the target response |
388
+
389
+ ### completion contract
390
+
391
+ Assigned Pi agents must call `agent_task_done` as their final action:
392
+
393
+ ```json
394
+ {
395
+ "taskId": "task_...",
396
+ "status": "completed",
397
+ "summary": "inspected auth middleware; no bypass found",
398
+ "result": {
399
+ "filesReviewed": ["src/auth.ts"]
400
+ }
401
+ }
402
+ ```
403
+
404
+ Non-Pi workers should complete through an equivalent store/server API with the
405
+ same terminal status and payload semantics.
406
+
407
+ Valid terminal statuses:
408
+
409
+ - `completed`
410
+ - `failed`
411
+ - `cancelled`
412
+ - `timed_out`
413
+ - `rejected`
414
+
415
+ Terminal completion is first-writer-wins.
416
+
417
+ ## task protocol
418
+
419
+ Assignments are delivered as human-readable text plus a structured envelope with:
420
+
421
+ - `type: "pi.task.assignment.v1"`
422
+ - `taskId`
423
+ - `fromSession`
424
+ - `instructions`
425
+ - protocol requirements telling the assignee to use `agent_task_done`
426
+
427
+ Task state includes:
428
+
429
+ - `pending`
430
+ - `dispatched`
431
+ - `running`
432
+ - terminal status
433
+ - assignment/result refs
434
+ - event history
435
+ - optional result payload/artifacts/error
436
+
437
+ Filesystem layout:
438
+
439
+ ```text
440
+ <tasksDir>/<taskId>/
441
+ ├── task.json
442
+ ├── assignment.json
443
+ ├── events.jsonl
444
+ └── result.json
445
+ ```
446
+
447
+ Generic filesystem storage defaults to `.pi/tasks/` when used directly.
448
+
449
+ ## architecture
450
+
451
+ `pi-tasks` separates storage from delivery:
452
+
453
+ ```ts
454
+ interface TaskStore {
455
+ createOrReuseDispatchedTask(...): Promise<...>;
456
+ readTask(...): Promise<...>;
457
+ readTaskResult(...): Promise<...>;
458
+ expireTaskIfOverdue(...): Promise<...>;
459
+ waitForTask(...): Promise<...>;
460
+ listInbox(...): Promise<...>;
461
+ ackTask(...): Promise<...>;
462
+ cancelTask(...): Promise<...>;
463
+ completeTask(...): Promise<...>;
464
+ }
465
+
466
+ interface TaskTransport {
467
+ getCurrentSessionName(env): string;
468
+ dispatchTask(input): Promise<DispatchTaskResult>;
469
+ }
470
+ ```
471
+
472
+ Registration composes both:
473
+
474
+ ```ts
475
+ registerAgentTaskTools(pi, {
476
+ store: createFilesystemTaskStore({ tasksDir: ".pi/tasks" }),
477
+ transport: myTransport,
478
+ });
479
+ ```
480
+
481
+ ## exported building blocks
482
+
483
+ - `src/extension.ts`
484
+ - `registerAgentTaskTools`
485
+ - `createDefaultTaskCommunicationLayer`
486
+ - `src/task-communication.ts`
487
+ - `TaskStore`
488
+ - `TaskTransport`
489
+ - `TaskCommunicationLayer`
490
+ - `src/stores/filesystem.ts`
491
+ - `createFilesystemTaskStore`
492
+ - `src/transports/wolfpack.ts`
493
+ - `createWolfpackTaskTransport`
494
+ - `src/protocol.ts`
495
+ - `buildAssignment`
496
+ - `compactTaskResult`
497
+ - `src/store.ts`
498
+ - low-level filesystem store functions, if you need finer control than
499
+ `createFilesystemTaskStore`
500
+
501
+ ## development
502
+
503
+ ```bash
504
+ bun install
505
+ bun test
506
+ bun run typecheck
507
+ ```
508
+
509
+ ## current limitations
510
+
511
+ - no built-in HTTP/Redis transport yet
512
+ - no runtime transport selector yet; compose a store/transport in an extension
513
+ - default extension uses the generic filesystem store plus the included Wolfpack transport
514
+ - package is source-first; install with `pi install npm:@sgtbeatdown/pi-tasks`
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@sgtbeatdown/pi-tasks",
3
+ "version": "0.1.0",
4
+ "description": "Structured task communication for Pi agents with pluggable stores and transports.",
5
+ "module": "src/extension.ts",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/almogdepaz/pi-tasks.git"
9
+ },
10
+ "keywords": [
11
+ "pi-package",
12
+ "pi",
13
+ "agent",
14
+ "tasks"
15
+ ],
16
+ "files": [
17
+ "README.md",
18
+ "src",
19
+ "skills",
20
+ "tsconfig.json"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "peerDependencies": {
26
+ "@earendil-works/pi-ai": "*",
27
+ "@earendil-works/pi-coding-agent": "*",
28
+ "@earendil-works/pi-tui": "*",
29
+ "typebox": "*"
30
+ },
31
+ "devDependencies": {
32
+ "@earendil-works/pi-ai": "^0.80.10",
33
+ "@earendil-works/pi-coding-agent": "^0.80.10",
34
+ "@earendil-works/pi-tui": "^0.80.10",
35
+ "@types/bun": "^1.3.14",
36
+ "typebox": "^1.3.6",
37
+ "typescript": "^7.0.2"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./src/extension.ts"
42
+ ],
43
+ "skills": [
44
+ "./skills"
45
+ ]
46
+ },
47
+ "scripts": {
48
+ "test": "bun test",
49
+ "typecheck": "tsc --noEmit"
50
+ },
51
+ "type": "module"
52
+ }
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: wolfpack-pi-task-delegation
3
+ description: Use this whenever the user asks to open, spawn, create, or use a Wolfpack/Pi subagent to do work, delegate work to another session, or check delegated task results. This skill connects existing Wolfpack session-control knowledge with the agent_task communication tools so agents spawn/select sessions, send work with agent_task_send, and avoid terminal-output completion polling.
4
+ ---
5
+
6
+ # Wolfpack Pi Task Delegation
7
+
8
+ This skill is the Wolfpack transport guidance for the task communication layer. It does not replace `wolfpack-tailnet-control`; use that
9
+ skill/instructions for opening, selecting, inspecting, or controlling Wolfpack
10
+ sessions.
11
+
12
+ ## Goal
13
+
14
+ When the user says something like “open a subagent and do X”, create or select a
15
+ Wolfpack/Pi session using existing Wolfpack control, then send X through the
16
+ structured `agent_task_*` task communication tools. Keep Wolfpack terminals
17
+ visible and steerable, but treat task state/results as the protocol.
18
+
19
+ ## Workflow
20
+
21
+ 1. If the request includes opening/spawning/creating a subagent, use the
22
+ existing Wolfpack control workflow (`wolfpack-tailnet-control`) to create the
23
+ session. Prefer the canonical CLI path:
24
+
25
+ ```bash
26
+ wolfpack agent spawn <project> --prompt 'you are a task worker. wait for structured pi.task.assignment.v1 assignments; finish assigned work only with agent_task_done.' --json
27
+ ```
28
+
29
+ `wolfpack session open <project> --prompt ... --json` is a deprecated alias;
30
+ use it only when that is the available command in the environment.
31
+
32
+ 2. Parse the structured JSON response from Wolfpack for the created session
33
+ handle/name/id. Treat it as opaque. Do not infer the target from browser UI
34
+ labels, terminal prose, or pane previews.
35
+
36
+ 3. Send the real work with `agent_task_send` to the selected/spawned session.
37
+ The task text should be the user’s requested work, not the bootstrap prompt.
38
+
39
+ 4. Return immediately with the task id and target session unless the user asked
40
+ to wait. Continue local work if there is other useful work to do.
41
+
42
+ 5. Use structured task tools for follow-up:
43
+ - `agent_task_status` for one task
44
+ - `agent_task_wait` only when the user wants the result now
45
+ - `agent_task_inbox` to check completed delegated work
46
+ - `agent_task_cancel` to cancel non-terminal tasks
47
+
48
+ 6. In target sessions, finish assigned tasks with `agent_task_done`. After
49
+ `agent_task_done`, do not send extra prose; the tool result is the completion
50
+ channel.
51
+
52
+ ## Do Not
53
+
54
+ - Do not poll terminal output to decide that delegated work is complete.
55
+ - Do not ask the target to report completion in prose.
56
+ - Do not use `wolfpack session wait` for task completion; it waits for literal
57
+ terminal text and is the wrong protocol for this package.
58
+ - Do not duplicate Wolfpack session-control rules here. If session control is
59
+ ambiguous, consult/use `wolfpack-tailnet-control`.
60
+
61
+ ## Examples
62
+
63
+ User: “open a subagent and inspect the auth middleware”
64
+
65
+ Expected approach:
66
+ 1. Spawn a Wolfpack child session with the existing Wolfpack control workflow.
67
+ 2. Call `agent_task_send` with the spawned session as `to` and “inspect the auth
68
+ middleware” as `task`.
69
+ 3. Tell the user the task id and that you’ll check structured results later.
70
+
71
+ User: “check on the subagent”
72
+
73
+ Expected approach:
74
+ - Use `agent_task_inbox` or `agent_task_status`; do not read terminal prose to
75
+ infer done-ness.
@@ -0,0 +1,24 @@
1
+ import type { AgentTaskRecord } from "./types";
2
+
3
+ export function selectUnpromptedTerminalTasks(
4
+ tasks: readonly AgentTaskRecord[],
5
+ autoPromptedTaskIds: ReadonlySet<string>,
6
+ ): readonly AgentTaskRecord[] {
7
+ return tasks.filter((task) => !autoPromptedTaskIds.has(task.id));
8
+ }
9
+
10
+ export function buildBackgroundTaskNotificationPrompt(tasks: readonly AgentTaskRecord[]): string {
11
+ const taskLines = tasks.map((task) => {
12
+ const summary = task.error?.message ?? task.status;
13
+ return `- ${task.id}: status=${task.status}, target=${task.targetSession}, summary=${summary}`;
14
+ });
15
+
16
+ return [
17
+ "background task results are ready for this parent session.",
18
+ "call `agent_task_inbox` with `{ ack: true }` now to read and acknowledge the structured results.",
19
+ "summarize the completed task results to the user concisely.",
20
+ "do not call agent_task_wait; these tasks already reached a terminal state.",
21
+ "tasks:",
22
+ ...taskLines,
23
+ ].join("\n");
24
+ }