pi-background-tasks 0.7.6 → 0.9.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/PUBLISHING.md +7 -7
- package/README.md +238 -17
- package/TESTING.md +94 -0
- package/TEST_PLAN.md +28 -4
- package/extensions/delegate-child.ts +1 -0
- package/package.json +10 -4
- package/src/core/common.ts +41 -0
- package/src/core/context/parent-snapshot.ts +142 -0
- package/src/core/context/token-budget.ts +890 -0
- package/src/core/context/visible-conversation-v2.ts +551 -0
- package/src/core/delegate/artifacts.ts +479 -0
- package/src/core/delegate/budget.ts +370 -0
- package/src/core/delegate/hook-contract-evidence.json +18 -0
- package/src/core/delegate/hook-contract.ts +153 -0
- package/src/core/delegate/launch.ts +460 -0
- package/src/core/delegate/result-package.ts +443 -0
- package/src/core/delegate/runner.ts +406 -0
- package/src/core/delegate/seed.ts +411 -0
- package/src/core/delegate/types.ts +304 -0
- package/src/core/fusion/artifacts.ts +64 -4
- package/src/core/fusion/budget.ts +464 -65
- package/src/core/fusion/context.ts +115 -511
- package/src/core/fusion/orchestrator.ts +184 -18
- package/src/core/fusion/pi-child.ts +473 -8
- package/src/core/fusion/prompts.ts +156 -4
- package/src/core/fusion/types.ts +237 -37
- package/src/core/fusion/web-fetch.ts +904 -0
- package/src/core/fusion/workflows.ts +130 -0
- package/src/core/registry.ts +174 -0
- package/src/delegate-child-extension.ts +673 -0
- package/src/delegate-extension.ts +587 -0
- package/src/extension.ts +10 -0
- package/src/fusion-child-extension.ts +279 -2
- package/src/fusion-extension.ts +183 -26
package/TEST_PLAN.md
CHANGED
|
@@ -13,7 +13,7 @@ This package follows:
|
|
|
13
13
|
| Package | `pi-background-tasks` |
|
|
14
14
|
| Extension entrypoint | `extensions/background-tasks.ts` |
|
|
15
15
|
| Public commands | `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear`, `/bg-update`, `/fusion`, `/fusion-models` |
|
|
16
|
-
| Public tools | `bg_run`, `bg_run_pi_attested`, `bg_status`, `bg_logs`, `bg_kill`, `fusion_brainstorm` |
|
|
16
|
+
| Public tools | `bg_run`, `bg_delegate`, `bg_result`, `bg_run_pi_attested`, `bg_status`, `bg_logs`, `bg_kill`, `fusion_brainstorm`, `fusion_validate` |
|
|
17
17
|
| Extension EventBus API | `pi-background-tasks:request:v1`, `pi-background-tasks:response:v1`, `pi-background-tasks:terminal:v1`; schemas exported from `src/core/extension-api.ts` |
|
|
18
18
|
| Shortcuts | `Shift+Down`; optional fallback `Ctrl+Alt+C` |
|
|
19
19
|
| Custom UI | footer status + focused bottom dock overlay |
|
|
@@ -32,9 +32,11 @@ This package follows:
|
|
|
32
32
|
| Package | `npm run test:package` | yes | implemented |
|
|
33
33
|
| PTY/TUI | `npm run test:pty` | full gate | implemented (answers pi's Kitty keyboard-protocol negotiation; auto-skips with a loud reason on hosts that cannot deliver raw-mode Node stdin via `/usr/bin/expect`) |
|
|
34
34
|
| Scripted provider | `npm run test:agent-loop` | full gate | implemented |
|
|
35
|
+
| Hook contract | `npm run test:hook-contract` | yes | implemented; drives a real Pi agent loop and compares observed `context`/`tool_result` behaviour against committed evidence. Fails loudly if Pi's hook semantics change rather than regenerating them |
|
|
35
36
|
| Pack dry run | `npm run pack:dry-run` | release gate | implemented |
|
|
36
37
|
| Smoke | `npm run smoke` | no | implemented; isolated load-only |
|
|
37
38
|
| Large-context smoke | `npm run smoke:large-context` | release gate | implemented; rebuilds the production failure byte composition, proves the pre-fix input is rejected and the post-fix projection fits all four stages against the smallest configured route, with no inference and no child spawn |
|
|
39
|
+
| Live subscription evidence | `npx tsx scripts/delegate-live-run.ts` | release gate | implemented; one real subscription-OAuth child on the parent's current route, large seeded session, hash-verified answer, and explicit leak checks. Caught two defects no offline gate did (undelivered prompt, seed-not-prompt budgeting), both now pinned by unit and mutation tests |
|
|
38
40
|
| Compatibility | `npm run test:compat` | release gate | implemented; exact Pi `0.75.5`, `0.81.1`, `0.82.1`, `0.83.0` pack/install plus `/jobs`, `/fusion`, and `/fusion-models` surfaces, per-version bundled-TypeBox peer verification, removed-TypeBox-API scan of installed package bytes, followed by a current-host real `fusion_brainstorm` session-stat and replay witness |
|
|
39
41
|
|
|
40
42
|
## Feature coverage matrix
|
|
@@ -53,9 +55,24 @@ This package follows:
|
|
|
53
55
|
| Stop task from LLM tool | `bg_kill` | | yes | | | | | | Covers running kill and already-finished loud failure. |
|
|
54
56
|
| Fusion command direct result | `/fusion`, `fusion-result` custom message | yes | yes | yes | | yes | yes | | Core unit covers deterministic v2 conversation-projection construction, explicit image omission markers without raw image data, direct final-text child argv/stdin/compact-metadata parsing, exact multi-block answer reconstruction, artifacts, pre-abort handling, and orchestration. SDK runs real `/fusion` against a fake child `pi`, verifies exactly five child text-mode invocations, hidden prompt persistence, visible exact merged custom message, no parent assistant rewrite, renderer output, progress/status behavior, no-argument editor flow, editor cancellation, and malformed config causing zero children. RPC verifies command discovery, `/fusion <prompt>` with U+2028/U+2029 content, custom-message result emission, no parent `agent_start`, no-argument editor protocol, malformed config/child failure visibility, and child isolation flags. PTY verifies `/fusion` renders the exact merged answer directly in a real TUI. Compatibility smoke runs `/fusion` through every supported Pi version with the installed package entrypoint. |
|
|
55
57
|
| Fusion tool result | `fusion_brainstorm({prompt})` | yes | yes | | | current-host stats/replay | yes | yes | Tool is registered at load and re-added on `session_start`; no eligibility/quota/routine/justification gates are implemented. BUG-182 unit/SDK coverage pins the exact host `Usage` contract, complete cost-component aggregation, rejection of legacy `costTotal`, v2 child/result/manifest schemas, exact merged text, progress, context exclusion, and final details. The release compatibility gate drives a real current-host RPC agent through the tool, verifies persisted tool usage, invokes the same session-stat path used by the footer, then reopens the durable session and verifies identical stats. Scripted-provider coverage proves normal parent consumption and evaluator schema repair. |
|
|
56
|
-
| Fusion
|
|
57
|
-
| Fusion
|
|
58
|
+
| Fusion Anthropic child sanitization | `@ravshansbox/pi-anthropic-sps` dependency, child `--extension` argv | yes | | | | | yes | | Unit proves a Claude route receives exactly two `--extension` paths with the metadata extension first and the sanitizer second, that every non-Anthropic provider still receives exactly one and never resolves the sanitizer at all, and that the real installed package resolves to an existing file through its manifest `pi.extensions[0]`. Resolution failures are each pinned as loud errors: package unresolvable, manifest unreadable, manifest not JSON, missing `pi` section, empty `pi.extensions`, blank entry, and a declared file that does not exist. Package guards require the sanitizer to be a declared dependency and require the provider gate so non-Anthropic child argv cannot drift. |
|
|
59
|
+
| Fusion validation workflow | `fusion_validate({prompt})`, workflow profiles, `fusion-manifest.v3`, `fusion-result.v4` | yes | | | | | yes | | Unit pins the brainstorm profile to the exact pre-extraction prompt constants so the workflow seam is proven byte-neutral, binds the validate profile to its own four system prompts, and asserts the two workflows share one evaluation schema contract and one repair framing. Validate orchestration covers five children with validate framing, candidates always `inspect` while evaluator and merger stay `reason`, tool-call log paths assigned only to candidates, `workflow` plus a `v`-prefixed run id persisted in the manifest and result details, and prompt artifacts byte-identical to the bytes handed to each child. A caller capability contradicting the fixed workflow policy is rejected before the artifact store exists and launches **zero** children; the same capability stated explicitly is accepted. Golden bytes pin a separate committed validate corpus, prove canonical input and omission ledger are identical across workflows for every case, and prove budget plans move for every case so the profile provably reaches the budget engine. Package guards reject `capability` and extra keys on `fusion_validate`, pin `FUSION_VALIDATE_CAPABILITY` to `inspect`, require `fusion_validate` in both the Fusion and delegate denylists, and pin the workflow-invariant comments at exactly one occurrence per launch site. |
|
|
60
|
+
| Fusion conversation projection | canonical input `fusion-input.v4`, `context-omission-ledger.json` | yes | yes | yes | | | yes | | Unit covers a >1 MB synthetic tool-heavy session staying within budget, verbatim user/assistant text, thinking exclusion, zero tool-payload preview bytes (head/tail sentinels absent), exact and stable omission counts/byte totals/hashes, tuple round-trip preservation of roles/source ordinals/block ordinals/exact text, byte-identical repeated construction and separate-process determinism, hash change on omitted-payload mutation without exposing the payload, ledger root hash unchanged by compact encoding, contiguous omission-run collapsing into compact `["o", span, bytes, [thinking, toolCalls, toolResults]]` tuples, material canonical-input byte reduction versus the verbose object encoding, receipt-to-ledger reconciliation through `projection_map`, active-tool-call-leaf and sibling-call exclusion, marker-only user images plus ledger-only tool-result images with no base64, per-entry-point policy ids and request authority, and the exactly-one-disposition property for every retained block. SDK verifies the exact child stdin carries `conversation_projection` and never `conversation_transcript`. RPC verifies the versioned request object. A dedicated high-cardinality fixture (340 receipt runs over 1,360 omitted events with incident-matching visible-text volume) pins per-receipt cost and proves the pre-fix verbose format would overflow the merge stage while the compact tuple format fits. Package tests assert the ledger artifact ships in the documented artifact set. |
|
|
61
|
+
| Fusion stage budgets | `budget-plan.json` (v3 per-stage forecasts), typed `prompt_budget_exceeded_forecast` / `prompt_budget_exceeded_measured` | yes | | | | | | | Unit covers the per-family affine estimator, additive segment accounting, multibyte 1.0-token/byte charging, unknown-provider floor visibility, byte-capacity route selection, scope guards for small windows, input-only fatal preflight versus warning-only reservations, rejection of unknown/zero/negative/too-small context windows, boundary accept at exactly the limit and reject one byte past, the child system prompt counted as input, per-stage forecasts built from the real prompt builders against each stage's own route, reservation warnings, breach-detector artifacts, safe prompts completing all five calls, persisted route/plan snapshots including negative slack on fatal rejection, and the reproduced 1 MB failure shape now fitting the smallest configured budget. Errors carry stage, measured size, allowed size, limiting model, estimator source, and remediation in both structured detail and message text. |
|
|
62
|
+
| Fusion candidate capability profiles | `fusion_brainstorm({prompt, capability?})`, candidate child argv | yes | yes | | | | yes | | Unit covers `reason` defaulting; reason and inspect argv remain byte-identical; `inspect` argv is exactly `--no-builtin-tools --tools read,grep,find,ls --exclude-tools bash,edit,write,fusion_brainstorm,bg_delegate,bg_result,bg_run,bg_kill,bg_status,bg_logs,bg_run_pi_attested`; `research` argv is the inspect allowlist plus `fusion_web_fetch`, with the same denylist. Unit also proves capability-specific prompts, research env only for research children, candidate tool-call log paths assigned only to candidates, and evaluator plus merger calls staying `reason`/`--no-tools` under caller-selected inspect or research. SDK and package guards validate only `reason`/`inspect`/`research`, reject extra keys, pin the allow/deny lists and `fusion_web_fetch` tool name, and assert evaluator/merger launch sites cannot read the caller-selected capability. |
|
|
63
|
+
| Fusion research web fetch | `fusion_web_fetch({ url, extract? })`, `candidate-<slot>.attempt-<n>.tool-calls.jsonl` | yes | | | | | yes | live child | Unit covers the closed schema surface through the child extension, registration only when research mode is enabled, unsupported schemes and URL credentials failing before network access, blocked DNS classes, a mixed public-plus-blocked DNS answer failing loudly, a redirect to a blocked address failing loudly, redirect-loop limit failure, Content-Length and streamed-body response caps, unsupported content types, Markdown extraction preserving links/headings/tables/code blocks, script/style stripping, text/plain passthrough with hash and byte count, UTF-8-safe output truncation, and typed timeout. Unit also proves the resolver is consulted once and the connection is pinned to that vetted address, and that audit metadata records `url`, `final_url`, `http_status`, `response_bytes`, and `content_sha256` without raw page content. Package guards require `fusion_web_fetch` registration behind the research env flag. Live verification fetched `https://example.com/`, reported the page H1, read a repository constant, and refused instruction-like text from both a repo file and the fetched page. |
|
|
64
|
+
| Fusion multi-message transcript validation | `*.events.jsonl`, stop reasons `toolUse`/`stop` | yes | | | | | | | Unit accepts a multi-message tool loop, reconstructs stdout from the final assistant message only, and sums usage across all compact metadata records. Non-final records must be `toolUse`, the final record must be `stop`, and `length`, `error`, `aborted`, and `pending` fail loudly instead of being skipped. |
|
|
65
|
+
| Fusion stale-action watchdog | `FUSION_CHILD_IDLE_TIMEOUT_MS`, child process lifecycle | yes | | | | | | | Unit proves a child with no stdout or stderr activity fails as `child_timeout` with the stalled-child message and is terminated; stderr activity resets the watchdog and allows success; the 30-minute absolute timeout path remains distinct and is not reported as a stalled child. |
|
|
66
|
+
| Fusion candidate tool-call audit log | `candidate-<slot>.attempt-<n>.tool-calls.jsonl`, `pi-background-tasks.fusion-tool-call.v1` | yes | | | | | | | Unit proves completed tool calls are logged with tool name, byte counts, and hashes while raw arguments/results containing a secret never appear; a complete 3-call log summarizes count and result bytes; zero completed calls is a complete trace; a trailing partial line, ordinal gap, duplicate ordinal, wrong schema version, and inspect-child partial log all fail loudly. Artifact coverage verifies successful attempts persist the JSONL and manifest summary, while no-log attempts do not invent one. |
|
|
58
67
|
| Pi/TypeBox compatibility | `peerDependencies`, packed bytes | yes | | | | | yes | | Unit pins typebox as a `"*"` peer that is neither a runtime nor bundled dependency, requires the resolved TypeBox to be Pi 0.83's 1.3.x line, requires all four supported Pi/TUI lines in the peer range, scans all package TypeScript for the seven removed TypeBox APIs, and compiles the shipped tool schema plus nullable-array/nullable-string and optional-field shapes under TypeBox 1.3. The release compat gate repeats the removed-API scan against installed package bytes per Pi version. |
|
|
68
|
+
| Pi hook contract | `npm run test:hook-contract`, `tests/scripted-provider/pi-hook-contract-evidence.json`, `src/core/delegate/hook-contract-evidence.json` | | | | | | evidence parity | yes | Executes a real Pi agent loop and records, rather than assumes: `context` fires once before every model call in load order; returned messages reach the provider; **throwing** in `context` does NOT block dispatch (Pi catches and continues); `ctx.abort()` does not skip the call site but delivers an already-aborted signal and terminates the run; `tool_result` fires before the transcript entry, chains in load order, replacement reaches the provider while the original does not, and toolCallId/role/`isError` survive. Handler ordering across two separate probe extensions is pinned. The evidence file is compared, never silently regenerated, and a package test asserts the shipped copy is byte-identical. |
|
|
69
|
+
| Delegate context seeding | `bg_delegate` seed `pi-background-tasks.delegate-seed.v1`, `seed.json`, `context-omission-ledger.json` | yes | yes | | | | mutation guard | yes | Unit covers verbatim visible user/assistant text, complete exclusion of thinking/tool-call arguments/tool-result payloads, marker-only images with no raw bytes anywhere in the seed, exclusion of the in-flight `bg_delegate` call **and every sibling call in the same assistant message** (two delegates launched together get byte-identical projected history), the prompt preserved exactly and marked `explicit_text` authority, zero payload-preview bytes, byte-identical construction across repeated builds and across separate processes via a fixed-leaf fixture, and refusal of a blank prompt. Receive-side verification rejects a single mutated byte, a foreign task identity, a structurally malformed seed whose hash matches, a directive whose hash disagrees with its text, and an unsupported capability. SDK proves the projected parent text actually reaches the child. |
|
|
70
|
+
| Delegate route pinning | `bg_delegate` `route`, `route_attestations` | yes | yes | | | | mutation guard | yes | Unit covers defaulting to the parent current model, explicit pinning, refusal of an unavailable route with `route_unresolved` and no substitution, refusal of a route with no declared context window with `route_capacity_unknown` rather than assuming one, and refusal when neither is available. The result package rejects a mismatched route, a mismatched attestation, and a package carrying no attestation at all. The scripted-provider gate proves an observed route drift prevents a success commit entirely. The mutation guard fails if a fallback list or first-available selection appears. |
|
|
71
|
+
| Delegate budget and guard | `budget-plan.json`, typed `seed_budget_exceeded` / `provider_context_budget_exhausted` | yes | yes | | | | mutation guard | yes | Unit covers reserve subtraction, exact-boundary accept and one-byte-past reject, the child system prompt counted as input, multi-byte UTF-8 measured by bytes, refusal of unusable windows without defaulting, and a total runtime governor that cannot throw from inside a hook. Preflight rejection is proven to create **zero** children and **zero** artifacts for hook-contract, budget, and blank-prompt refusals, with the artifact directory verified absent. The scripted-provider gate proves an over-budget model call is blocked in a real agent loop and produces a typed terminal record with no committed result. |
|
|
72
|
+
| Delegate tool-result spilling | `spill/` artifacts, `pi-background-tasks.delegate-receipt.v1`, `delegate_read_artifact` | yes | yes | | | | mutation guard | yes | Unit proves a 2 MB payload is written in full, hashed, and represented by a receipt carrying `(turn_sequence, source_call_index, tool_call_id)` assigned before execution, so parallel spills completing out of order cannot be mis-associated; an aggregate-cap refusal writes nothing and emits no receipt; bounded range reads return exactly the requested bytes; a range past end-of-file, a path escaping the artifact directory, and non-positive lengths are refused rather than shortened. The scripted-provider gate proves in a real agent loop that the raw payload never enters the transcript, the transcript instead carries a receipt naming the hash and stating nothing was truncated, and an over-long range read fails loudly. |
|
|
73
|
+
| Delegate result commit and `bg_result` | `result.json` (`pi-background-tasks.delegate-result.v1`), `bg_result` | yes | yes | | | | mutation guard | yes | The single atomically-committed package is the sole answer data plane: temp-write, fsync, rename, directory fsync, with the rename as the commit point. Unit covers round-trip verification, multi-block concatenation, exact preservation of U+2028/U+2029/emoji/unnormalized sequences, refusal of lone surrogates rather than U+FFFD substitution, detection of block-hash, aggregate-hash, declared-length, and non-strict-base64 corruption, foreign identity/seed rejection, and explicitly unavailable usage that is never reported as zero. Terminal evaluation treats a zero-exit child with no committed package as typed `child_exited_without_commit`, reports the child-recorded reason when present, and classifies a killed task as cancelled. SDK covers not-ready (non-blocking), completed, corrupted, route-drift, and unknown-task paths, and proves an oversized answer degrades to an artifact reference while an explicit inline request fails with `result_too_large_for_inline` and is never truncated. |
|
|
74
|
+
| Delegate isolation boundary | child argv, `--session-id`, `--session-dir`, `--tools` | yes | yes | | | | mutation guard | yes | Unit proves the child receives its own session id and a task-owned session directory, no `--continue`/`--resume`/`--session`/`--fork`, only the inspect tool set, an explicit denylist covering `bash`/`edit`/`write`/`bg_delegate`/`fusion_brainstorm`, disabled discovery of extensions/skills/prompt templates/themes/context files, exactly one explicitly loaded package guard extension, explicit provider/model with no `--api-key`, and parent session identity stripped from the child environment. SDK verifies the same facts from the argv and environment the child actually observed. |
|
|
75
|
+
| Fusion artifact byte immutability | `canonical-input.json`, `context-omission-ledger.json`, `budget-plan.json` | yes | | | | | shared-source guard | | A 28-case differential corpus is rendered to raw bytes and compared against a committed golden file that is never auto-updated once present, and separately against `tests/oracle/fusion-context-pre-extraction.ts`, a verbatim pre-extraction copy of the projection engine used as an **independent oracle**. Equivalence covers canonical input, ledger, and budget-plan bytes, `Object.is` comparison of budget floats including `utilization`, field-for-field accounting parity, ledger root hashes, and identical error type and message for unknown blocks and blank requests. The package guard now scans the shared `context/` modules, not only the Fusion facade, and pins both the shared divisor definition and Fusion's binding to it. |
|
|
59
76
|
| Fusion model selector | `/fusion-models`, `fusion-models.json`, `FusionModelSelector` | yes | yes | yes | yes | yes | yes | | Unit covers strict config parsing, duplicates, slash-containing model IDs, stale model failures, `$current`, atomic save, inter-process lock/revision compare-and-swap, and deterministic concurrent-save conflict. Component covers all five slots, duplicate selection, searchable model list, stale display, reset/save/cancel, persistence errors, and width safety. SDK drives the real command in a synthetic TUI context without `ctx.mode` to cover old Pi compatibility and verifies duplicate `$current`/explicit model persistence; SDK also verifies headless no-UI rejection. RPC verifies non-TUI notification without hanging, PTY verifies the real selector opens in a TUI, and compatibility smoke verifies print-mode rejection for every supported Pi version. |
|
|
60
77
|
| Fusion child isolation/lifecycle | child `pi --mode text`, private compact metadata extension, shutdown cleanup, `.pi/fusion` artifacts | yes | yes | yes | | current-host stats/replay | yes | | BUG-180 coverage proves final-text transport, reasoning exclusion, strict response reconstruction, unchanged caps, isolation, process cleanup, failure artifacts, and lifecycle semantics. BUG-182 upgrades the private compact metadata and artifact manifest to v2, preserves all token and cost components, centralizes clone/add/empty operations, rejects missing/legacy/unknown/non-finite cost shapes, and proves successful plus failed/cancelled attempt costs aggregate exactly once. SDK/RPC verify public child isolation; session shutdown tracks initializing and live runs. |
|
|
61
78
|
| Extension request/response service | `pi-background-tasks:request:v1` → `pi-background-tasks:response:v1` | yes | yes | | | | yes | | Unit covers closed-frame validation, capability handshake, unknown keys, unknown operation, duplicate request IDs, missing `session_start`, shutdown refusal, strict `run.payload`, strict malformed frames, and unsubscribe. SDK loads the real extension with a shared `createEventBus()`, starts `printf api-ok`, reads bounded logs, lists status, starts and kills a real sleep task, and checks malformed/unknown/duplicate controls without model/provider calls. Package tests assert `src/core/extension-api.ts` ships. |
|
|
@@ -104,7 +121,14 @@ Lane A residual hardening is now covered by automated tests. No remaining harden
|
|
|
104
121
|
|
|
105
122
|
## Acceptance checklist
|
|
106
123
|
|
|
107
|
-
- [x] `npm run test` passes offline in isolated temp dirs.
|
|
124
|
+
- [x] `npm run test` passes offline in isolated temp dirs, including the Pi hook characterisation gate.
|
|
125
|
+
- [x] Pi hook behaviour relied on by the delegate child guard is proven by execution and committed as evidence, not inferred from type declarations.
|
|
126
|
+
- [x] Fusion's persisted artifact bytes are proven unchanged against an independent pre-extraction oracle.
|
|
127
|
+
- [x] Delegate preflight refusal is proven to create zero child processes and zero artifacts.
|
|
128
|
+
- [x] No silent truncation, silent fallback, route substitution, unbounded inline answer, synthesized zero usage, fail-open guard hook, or undelivered seed exists on the delegate path, enforced by a mutation guard.
|
|
129
|
+
- [x] A guard-hook exception fails closed: it latches terminal state and suppresses content instead of letting the original message set reach the provider.
|
|
130
|
+
- [x] An incomplete response (`length`, non-`stop` stop reason) or a whitespace-only answer is never committed as a complete result, because a valid hash proves integrity but not completeness.
|
|
131
|
+
- [x] A real subscription-only run demonstrates the whole loop end to end, including that the child used both its read-only tools and the projected conversation.
|
|
108
132
|
- [x] `npm run test:full` validates baseline real TUI/PTY behavior.
|
|
109
133
|
- [x] `npm run pack:dry-run` passes.
|
|
110
134
|
- [x] README claims and all plausible edge cases are exhaustively mapped in this test plan, including Fusion command/tool/model-selector public surfaces and the text-only image omission limitation.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '../src/delegate-child-extension.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-background-tasks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension package for background shell tasks plus five-model fusion brainstorming: bg_run tools, /bg commands, /fusion, fusion_brainstorm, model selector UI, bounded logs, kill/timeout safety, and completion wakeups.",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Pi extension package for background shell tasks plus five-model fusion brainstorming and validation: bg_run tools, bg_delegate, /bg commands, /fusion, fusion_brainstorm, fusion_validate, model selector UI, bounded logs, kill/timeout safety, and completion wakeups.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"author": "Ismail <ismailsalikhodjaev@gmail.com>",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test:rpc": "tsx --test tests/rpc/**/*.test.ts",
|
|
46
46
|
"test:component": "tsx --test tests/component/**/*.test.ts",
|
|
47
47
|
"test:package": "tsx --test tests/package/**/*.test.ts",
|
|
48
|
-
"test": "npm run typecheck && npm run test:type-safety && npm run test:unit && npm run test:sdk && npm run test:rpc && npm run test:component && npm run test:package",
|
|
48
|
+
"test": "npm run typecheck && npm run test:type-safety && npm run test:unit && npm run test:sdk && npm run test:rpc && npm run test:component && npm run test:package && npm run test:hook-contract",
|
|
49
49
|
"test:pty": "tsx --test tests/pty/**/*.test.ts",
|
|
50
50
|
"test:windows": "tsx --test tests/windows/**/*.test.ts",
|
|
51
51
|
"test:agent-loop": "tsx --test --test-concurrency=1 tests/scripted-provider/**/*.test.ts",
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"pack:dry-run": "npm pack --dry-run",
|
|
56
56
|
"lint": "cd ../.. && npm run quality:ts:lint",
|
|
57
57
|
"format:check": "cd ../.. && npm run quality:ts:format",
|
|
58
|
-
"test:compat": "tsx scripts/test-compat.ts"
|
|
58
|
+
"test:compat": "tsx scripts/test-compat.ts",
|
|
59
|
+
"test:hook-contract": "tsx --test --test-concurrency=1 tests/scripted-provider/pi-hook-contract.test.ts"
|
|
59
60
|
},
|
|
60
61
|
"pi": {
|
|
61
62
|
"extensions": [
|
|
@@ -71,12 +72,17 @@
|
|
|
71
72
|
"@earendil-works/pi-ai": "^0.83.0",
|
|
72
73
|
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
73
74
|
"@earendil-works/pi-tui": "^0.83.0",
|
|
75
|
+
"@ravshansbox/pi-anthropic-sps": "github:ravshansbox/pi-anthropic-sps",
|
|
74
76
|
"@types/node": "^24.0.0",
|
|
77
|
+
"@types/turndown": "5.0.6",
|
|
75
78
|
"tsx": "^4.19.0",
|
|
76
79
|
"typebox": "^1.3.7",
|
|
77
80
|
"typescript": "^5.9.0"
|
|
78
81
|
},
|
|
79
82
|
"engines": {
|
|
80
83
|
"node": ">=22.19.0"
|
|
84
|
+
},
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"turndown": "7.2.4"
|
|
81
87
|
}
|
|
82
88
|
}
|
package/src/core/common.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { open } from 'node:fs/promises';
|
|
|
3
3
|
import { extname, isAbsolute, join, win32 } from 'node:path';
|
|
4
4
|
import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent';
|
|
5
5
|
import type { BackgroundTaskChildProcess } from './registry.js';
|
|
6
|
+
import type { DelegateBudgetRouteSource } from './delegate/types.js';
|
|
6
7
|
|
|
7
8
|
export const TASK_STATUS_VALUES = ['running', 'completed', 'failed', 'killed'] as const;
|
|
8
9
|
export const TERMINAL_TASK_STATUS_VALUES = ['completed', 'failed', 'killed'] as const;
|
|
@@ -60,6 +61,7 @@ export interface BgTaskSnapshot {
|
|
|
60
61
|
model?: string | undefined;
|
|
61
62
|
telemetryUnavailableReason?: string | undefined;
|
|
62
63
|
attestationPath?: string | undefined;
|
|
64
|
+
delegate?: DelegateTaskFacts | undefined;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export interface AttestedPiTaskFiles {
|
|
@@ -73,6 +75,30 @@ export interface AttestedPiTaskSnapshot extends BgTaskSnapshot {
|
|
|
73
75
|
attestedPi?: AttestedPiTaskFiles | undefined;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
/** Delegate-specific task facts surfaced through snapshots and `bg_result`. */
|
|
79
|
+
export interface DelegateTaskFacts {
|
|
80
|
+
taskId: string;
|
|
81
|
+
launchNonce: string;
|
|
82
|
+
artifactDir: string;
|
|
83
|
+
artifactDirAbs: string;
|
|
84
|
+
seedSha256: string;
|
|
85
|
+
childSessionId: string;
|
|
86
|
+
route: { provider: string; model: string; qualifiedId: string };
|
|
87
|
+
budget: DelegateBudgetRouteSource;
|
|
88
|
+
autoDeliver: 'never' | 'when_small' | 'always';
|
|
89
|
+
/** Set once the run reaches a terminal state and its result has been evaluated. */
|
|
90
|
+
outcome?: DelegateTaskOutcome | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface DelegateTaskOutcome {
|
|
94
|
+
status: 'committed' | 'failed' | 'cancelled';
|
|
95
|
+
errorCode?: string | undefined;
|
|
96
|
+
answerBytes?: number | undefined;
|
|
97
|
+
answerSha256?: string | undefined;
|
|
98
|
+
turns?: number | undefined;
|
|
99
|
+
toolCalls?: number | undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
|
|
77
103
|
name: string;
|
|
78
104
|
outputAbsPath: string;
|
|
@@ -102,6 +128,7 @@ export interface BgTask extends Omit<BgTaskSnapshot, 'name'> {
|
|
|
102
128
|
telemetryUnavailableReason?: string | undefined;
|
|
103
129
|
attestationPath?: string | undefined;
|
|
104
130
|
attestedPi?: AttestedPiTaskFiles | undefined;
|
|
131
|
+
delegate?: DelegateTaskFacts | undefined;
|
|
105
132
|
metadataWriteChain?: Promise<void> | undefined;
|
|
106
133
|
waiters: Array<() => void>;
|
|
107
134
|
}
|
|
@@ -198,6 +225,19 @@ export interface StartTaskOptions {
|
|
|
198
225
|
terminalPublicationGate?: Promise<void> | undefined;
|
|
199
226
|
}
|
|
200
227
|
|
|
228
|
+
/** Prepared delegate launch handed to the registry after preflight has succeeded. */
|
|
229
|
+
export interface StartDelegateTaskOptions {
|
|
230
|
+
name: string;
|
|
231
|
+
argv: readonly string[];
|
|
232
|
+
/** Prompt bytes delivered over stdin, never as a shell or positional argument. */
|
|
233
|
+
stdinBytes: Buffer;
|
|
234
|
+
env: NodeJS.ProcessEnv;
|
|
235
|
+
facts: DelegateTaskFacts;
|
|
236
|
+
notifyOnCompletion: boolean;
|
|
237
|
+
triggerOnCompletion: boolean;
|
|
238
|
+
timeoutSeconds?: number | undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
201
241
|
export interface StartAttestedPiTaskOptions {
|
|
202
242
|
name: string;
|
|
203
243
|
provider: string;
|
|
@@ -687,6 +727,7 @@ export function snapshot(task: BgTask): BgTaskSnapshot {
|
|
|
687
727
|
model: task.model,
|
|
688
728
|
telemetryUnavailableReason: task.telemetryUnavailableReason,
|
|
689
729
|
attestationPath: task.attestationPath,
|
|
730
|
+
delegate: task.delegate,
|
|
690
731
|
};
|
|
691
732
|
}
|
|
692
733
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildSessionContext,
|
|
3
|
+
convertToLlm,
|
|
4
|
+
type SessionEntry,
|
|
5
|
+
} from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
7
|
+
import { isJsonObject, type JsonObject } from '../common.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pi session adapter shared by every consumer of the visible-conversation
|
|
11
|
+
* transform.
|
|
12
|
+
*
|
|
13
|
+
* Responsible for exactly one thing: turning the parent's live Pi session into a
|
|
14
|
+
* frozen `Message[]` snapshot, with the in-flight tool call that requested the
|
|
15
|
+
* snapshot (and therefore its sibling calls) excluded from the branch.
|
|
16
|
+
*
|
|
17
|
+
* The transform itself lives in `visible-conversation-v2.ts` and never sees a
|
|
18
|
+
* `SessionManager`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface ReadonlyParentSessionManager {
|
|
22
|
+
getLeafId(): string | null;
|
|
23
|
+
getLeafEntry(): SessionEntry | undefined;
|
|
24
|
+
getEntries(): SessionEntry[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ParentContextSource {
|
|
28
|
+
cwd: string;
|
|
29
|
+
sessionManager: ReadonlyParentSessionManager;
|
|
30
|
+
getSystemPrompt(): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ParentSnapshotOptions {
|
|
34
|
+
/** Tool call currently executing, when the snapshot is requested from a tool. */
|
|
35
|
+
toolCallId?: string | undefined;
|
|
36
|
+
/** Tool name used for leaf matching when no explicit call id is available. */
|
|
37
|
+
toolName: string;
|
|
38
|
+
/** Commands have no in-flight tool call and therefore never exclude a leaf. */
|
|
39
|
+
excludeActiveToolCallLeaf: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ParentSnapshot {
|
|
43
|
+
messages: readonly Message[];
|
|
44
|
+
leafId: string | null;
|
|
45
|
+
activeToolCallLeafExcluded: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function entriesById(entries: readonly SessionEntry[]): Map<string, SessionEntry> {
|
|
49
|
+
const byId = new Map<string, SessionEntry>();
|
|
50
|
+
for (const entry of entries) byId.set(entry.id, entry);
|
|
51
|
+
return byId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readArray(record: JsonObject, key: string): readonly unknown[] | undefined {
|
|
55
|
+
const value = record[key];
|
|
56
|
+
return Array.isArray(value) ? value : undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function recordOf(value: unknown): JsonObject | undefined {
|
|
60
|
+
if (!isJsonObject(value) || Array.isArray(value)) return undefined;
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function entryMessage(entry: SessionEntry): JsonObject | undefined {
|
|
65
|
+
if (entry.type !== 'message') return undefined;
|
|
66
|
+
return recordOf(entry.message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toolCallPartMatches(
|
|
70
|
+
part: unknown,
|
|
71
|
+
toolCallId: string | undefined,
|
|
72
|
+
toolName: string,
|
|
73
|
+
): boolean {
|
|
74
|
+
const record = recordOf(part);
|
|
75
|
+
if (record === undefined || record['type'] !== 'toolCall') return false;
|
|
76
|
+
if (toolCallId !== undefined) return record['id'] === toolCallId;
|
|
77
|
+
return record['name'] === toolName;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function messageContainsToolCall(
|
|
81
|
+
message: JsonObject,
|
|
82
|
+
toolCallId: string | undefined,
|
|
83
|
+
toolName: string,
|
|
84
|
+
): boolean {
|
|
85
|
+
if (message['role'] !== 'assistant') return false;
|
|
86
|
+
const content = readArray(message, 'content');
|
|
87
|
+
if (content === undefined) return false;
|
|
88
|
+
for (const part of content) {
|
|
89
|
+
if (toolCallPartMatches(part, toolCallId, toolName)) return true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface EffectiveLeaf {
|
|
95
|
+
leafId: string | null;
|
|
96
|
+
activeToolCallLeafExcluded: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function effectiveLeafForTool(
|
|
100
|
+
sessionManager: ReadonlyParentSessionManager,
|
|
101
|
+
toolCallId: string | undefined,
|
|
102
|
+
toolName: string,
|
|
103
|
+
): EffectiveLeaf {
|
|
104
|
+
const leaf = sessionManager.getLeafEntry();
|
|
105
|
+
if (leaf === undefined)
|
|
106
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
107
|
+
const message = entryMessage(leaf);
|
|
108
|
+
if (message !== undefined && messageContainsToolCall(message, toolCallId, toolName)) {
|
|
109
|
+
return { leafId: leaf.parentId, activeToolCallLeafExcluded: true };
|
|
110
|
+
}
|
|
111
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveEffectiveLeaf(
|
|
115
|
+
sessionManager: ReadonlyParentSessionManager,
|
|
116
|
+
options: ParentSnapshotOptions,
|
|
117
|
+
): EffectiveLeaf {
|
|
118
|
+
if (!options.excludeActiveToolCallLeaf)
|
|
119
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
120
|
+
return effectiveLeafForTool(sessionManager, options.toolCallId, options.toolName);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Freeze the parent conversation into LLM messages.
|
|
125
|
+
*
|
|
126
|
+
* Callers must complete every downstream use of the returned snapshot without
|
|
127
|
+
* re-reading the session, so the seed cannot drift while a child is being
|
|
128
|
+
* launched.
|
|
129
|
+
*/
|
|
130
|
+
export function snapshotParentConversation(
|
|
131
|
+
ctx: ParentContextSource,
|
|
132
|
+
options: ParentSnapshotOptions,
|
|
133
|
+
): ParentSnapshot {
|
|
134
|
+
const entries = ctx.sessionManager.getEntries();
|
|
135
|
+
const leaf = resolveEffectiveLeaf(ctx.sessionManager, options);
|
|
136
|
+
const sessionContext = buildSessionContext(entries, leaf.leafId, entriesById(entries));
|
|
137
|
+
return {
|
|
138
|
+
messages: convertToLlm(sessionContext.messages),
|
|
139
|
+
leafId: leaf.leafId,
|
|
140
|
+
activeToolCallLeafExcluded: leaf.activeToolCallLeafExcluded,
|
|
141
|
+
};
|
|
142
|
+
}
|