pi-background-tasks 0.7.2 → 0.7.3
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 +6 -6
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +3 -3
- package/package.json +1 -1
- package/src/core/fusion/artifacts.ts +7 -19
- package/src/core/fusion/orchestrator.ts +7 -18
- package/src/core/fusion/pi-child.ts +46 -42
- package/src/core/fusion/types.ts +49 -10
- package/src/fusion-child-extension.ts +11 -20
- package/src/fusion-extension.ts +5 -4
package/PUBLISHING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Publishing pi-background-tasks
|
|
2
2
|
|
|
3
|
-
Release checklist for npm publishing and standalone git publishing. The current release candidate is 0.7.
|
|
3
|
+
Release checklist for npm publishing and standalone git publishing. The current release candidate is 0.7.3; version 0.7.0 introduced the Fusion public surfaces (`/fusion`, `/fusion-models`, `fusion_brainstorm`) in addition to the background-task surfaces. Do not advertise the GitHub install target until the standalone repository has the exact release commit and tag.
|
|
4
4
|
|
|
5
5
|
## Preconditions
|
|
6
6
|
|
|
@@ -34,8 +34,8 @@ npm publish --access public
|
|
|
34
34
|
Pi install smoke after publish:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
PI_CODING_AGENT_DIR=$(mktemp -d) pi -e npm:pi-background-tasks@0.7.
|
|
38
|
-
pi install npm:pi-background-tasks@0.7.
|
|
37
|
+
PI_CODING_AGENT_DIR=$(mktemp -d) pi -e npm:pi-background-tasks@0.7.3 --offline --no-tools --no-session -p "/jobs"
|
|
38
|
+
pi install npm:pi-background-tasks@0.7.3
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
## Publish to git
|
|
@@ -48,15 +48,15 @@ git status --short --branch
|
|
|
48
48
|
git log --oneline -3
|
|
49
49
|
git remote -v
|
|
50
50
|
git push origin main
|
|
51
|
-
git tag v0.7.
|
|
52
|
-
git push origin v0.7.
|
|
51
|
+
git tag v0.7.3
|
|
52
|
+
git push origin v0.7.3
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Pi install smoke after git tag, using an isolated Pi agent directory so no local checkout or user `~/.pi` state is involved:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
PI_CODING_AGENT_DIR=$(mktemp -d) pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.7.
|
|
59
|
-
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.
|
|
58
|
+
PI_CODING_AGENT_DIR=$(mktemp -d) pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.7.3 --offline --no-tools --no-session -p "/jobs"
|
|
59
|
+
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.3
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## pi.dev/packages
|
package/README.md
CHANGED
|
@@ -9,19 +9,19 @@ This package adds named, tracked background shell jobs with durable output files
|
|
|
9
9
|
From npm after publish:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pi install npm:pi-background-tasks@0.7.
|
|
12
|
+
pi install npm:pi-background-tasks@0.7.3
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
From git after pushing this package to its standalone repository and tagging:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.
|
|
18
|
+
pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.3
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
For project-local install:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
pi install -l npm:pi-background-tasks@0.7.
|
|
24
|
+
pi install -l npm:pi-background-tasks@0.7.3
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Commands
|
|
@@ -95,7 +95,7 @@ The lookup runs at most once per session on `session_start`, is time-boxed, and
|
|
|
95
95
|
- `bg_status` — inspect one task or all recent tasks.
|
|
96
96
|
- `bg_logs` — read bounded task output.
|
|
97
97
|
- `bg_kill` — stop a running task.
|
|
98
|
-
- `fusion_brainstorm({prompt})` — always-active tool that runs the Fusion workflow and returns the exact merged text as the tool result for the parent agent to consume, with
|
|
98
|
+
- `fusion_brainstorm({prompt})` — always-active tool that runs the Fusion workflow and returns the exact merged text as the tool result for the parent agent to consume, with the exact Pi `Usage` shape attached when the host supports tool-result usage: token fields plus complete `cost.input`, `cost.output`, `cost.cacheRead`, `cost.cacheWrite`, and `cost.total`. Its closed public schema has exactly one required parameter, `prompt`; extra keys are rejected. It has no eligibility, quota, routine, or justification gate. Tool context capture excludes the current assistant tool-call leaf when Pi is executing that `fusion_brainstorm` call, so the nested children do not see the in-progress tool call or sibling calls.
|
|
99
99
|
|
|
100
100
|
`bg_run` requires a concise `name` for the footer dock, the shell `command`, and required `isAgent: boolean`. Set `isAgent: true` only when the background task launches an LLM/agent process (for example `pi -p ...` or `pi --mode json ...`); set `isAgent: false` for scripts, tests, dev servers, sleeps, and ordinary shell commands. It defaults both `notifyOnCompletion` and `triggerOnCompletion` to `true`. With those defaults, `bg_run` returns immediately, the agent continues only independent useful work or ends its current turn instead of sleeping or polling, and a durable `background-task-notification` for completed, failed, or killed state automatically starts a follow-up turn. The launch receipt states the effective notification/wake behavior explicitly. `bg_status` and `bg_logs` remain available for user-requested inspection, deliberately disabled completion delivery, concrete hang diagnosis, or reading output after the terminal event; they are not waiting primitives, and the terminal notification does not need status reconfirmation. Setting `triggerOnCompletion: false` keeps the notification but prevents it from starting an agent turn. Setting `notifyOnCompletion: false` suppresses both notification and wake-up even if `triggerOnCompletion` is true.
|
|
101
101
|
|
|
@@ -108,7 +108,7 @@ Tasks marked with `isAgent: true` that launch print/json child Pi agents through
|
|
|
108
108
|
|
|
109
109
|
Fusion runs direct child `pi --mode text` processes only; it never calls `pi-ai` completion APIs. Each child is launched with `--no-session`, `--no-tools`, `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, and `--no-context-files`, plus the resolved provider/model/thinking level and the package-owned private `extensions/fusion-child.ts` metadata extension. The prompt travels over stdin, not a shell or positional argument.
|
|
110
110
|
|
|
111
|
-
Pi text mode writes the final full answer exactly once instead of serializing cumulative reasoning/partial-message events on every token delta. The private child extension emits one compact, reasoning-free metadata record per finalized assistant message for provider/model, stop reason,
|
|
111
|
+
Pi text mode writes the final full answer exactly once instead of serializing cumulative reasoning/partial-message events on every token delta. The private child extension emits one compact, reasoning-free metadata record per finalized assistant message for provider/model, stop reason, the complete Pi token/cost `Usage` object, and response byte/hash validation. Fusion persists those compact records in `*.events.jsonl`; the complete answer remains in the stage response artifact. The 32 MiB child stdout cap therefore applies to one final response, not amplified JSON telemetry. Failed attempts keep the authoritative response artifact empty and, when any stdout was captured, persist it separately as an explicitly incomplete `*.response.partial.*` artifact.
|
|
112
112
|
|
|
113
113
|
Model configuration is global under the Pi agent directory:
|
|
114
114
|
|
|
@@ -162,7 +162,7 @@ Fusion writes private debugging artifacts under:
|
|
|
162
162
|
.pi/fusion/<session-id>-<pid>/<run-id>/
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
Each run contains `manifest.json`, `canonical-input.json`, candidate/evaluation/merge prompts, raw child JSONL events, stderr, responses, `blind-candidates.json`, `evaluation.json`, `merged.md`, and `error.json` for failed/cancelled runs. Artifact files are written by private temp-file/fsync/rename, and manifests persist cumulative child usage plus per-attempt observed usage/model data for successful, failed, and cancelled child attempts. These artifacts are local evidence only; they are not shown in `/jobs` or the background-task dock.
|
|
165
|
+
Each run contains `manifest.json`, `canonical-input.json`, candidate/evaluation/merge prompts, raw child JSONL events, stderr, responses, `blind-candidates.json`, `evaluation.json`, `merged.md`, and `error.json` for failed/cancelled runs. Artifact files are written by private temp-file/fsync/rename, and v2 manifests persist cumulative child usage plus per-attempt observed usage/model data for successful, failed, and cancelled child attempts. Every usage record preserves the complete Pi cost breakdown; the same exact shape is cloned into `fusion_brainstorm` tool results so newer Pi hosts can calculate and replay footer/session statistics safely. These artifacts are local evidence only; they are not shown in `/jobs` or the background-task dock.
|
|
166
166
|
|
|
167
167
|
For attested Pi tasks only, the task id is `b` plus 32 random hex characters (128 bits) and additional flat siblings are written in the same directory:
|
|
168
168
|
|
package/TESTING.md
CHANGED
|
@@ -77,7 +77,7 @@ npm run test:rpc
|
|
|
77
77
|
npm run test:agent-loop
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
The Fusion SDK/RPC/scripted-provider tests install a deterministic fake child `pi` in a temp `PATH` from `tests/helpers/fusion-fake-pi.ts`. Parent Pi remains the real SDK/RPC runtime; only direct child `pi --mode text` calls with the package-owned private compact metadata extension are intercepted. `PI_CODING_AGENT_DIR` is pointed at the temp agent directory so `fusion-models.json` is never read from the user's real global Pi directory. Fusion context coverage is intentionally text-transcript coverage: tests verify long text is not summarization-truncated and image-containing session entries are represented by `[Image omitted from fusion text transcript: <mime-type>]` markers without forwarding raw image bytes/base64 to child Pi prompts. The release-only `npm run test:compat` packs the package, installs exact supported Pi versions, runs `/jobs`, runs `/fusion` through the installed package entrypoint with the fake child Pi, verifies five child invocations, and verifies `/fusion-models` rejects non-TUI mode.
|
|
80
|
+
The Fusion SDK/RPC/scripted-provider tests install a deterministic fake child `pi` in a temp `PATH` from `tests/helpers/fusion-fake-pi.ts`. Parent Pi remains the real SDK/RPC runtime; only direct child `pi --mode text` calls with the package-owned private compact metadata extension are intercepted. `PI_CODING_AGENT_DIR` is pointed at the temp agent directory so `fusion-models.json` is never read from the user's real global Pi directory. Fusion context coverage is intentionally text-transcript coverage: tests verify long text is not summarization-truncated and image-containing session entries are represented by `[Image omitted from fusion text transcript: <mime-type>]` markers without forwarding raw image bytes/base64 to child Pi prompts. The release-only `npm run test:compat` packs the package, installs exact supported Pi versions, runs `/jobs`, runs `/fusion` through the installed package entrypoint with the fake child Pi, verifies five child invocations, and verifies `/fusion-models` rejects non-TUI mode. It then drives the current host Pi through a real RPC `fusion_brainstorm` parent-agent loop, checks the persisted tool result carries the complete Pi `Usage.cost` object, invokes `get_session_stats` (the same aggregation boundary used by the TUI footer), reopens the durable session, and verifies identical token/cost totals. All parent and child inference remains deterministic and local.
|
|
81
81
|
|
|
82
82
|
## Coverage summary
|
|
83
83
|
|
|
@@ -87,13 +87,13 @@ Implemented coverage includes:
|
|
|
87
87
|
- commands: `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear`, `/bg-update`, `/fusion`, `/fusion-models` discovery, happy paths, `/fusion` direct custom-message delivery, `/fusion` editor/cancel flow, `/fusion-models` TUI save and non-TUI rejection, `/bg --agent` parsing, finished-notice clearing, malformed `/bg`, unknown/ambiguous IDs, completed-task `/kill`, byte-limit normalization, and RPC no-hang fallback behavior
|
|
88
88
|
- update-available notice: semver parse/compare/precedence, `formatUpdateSegment`, npm/`package.json` payload narrowing, and injected-fetch success/404/throw/timeout (unit); localhost-registry footer segment (idle + appended to an active footer), `/bg-update` non-installing instructions, and opt-out/offline/already-current/registry-failure no-segment-and-no-throw paths (SDK); `/bg-update` discovery and offline instructions (RPC). The check is one-shot on `session_start`, time-boxed, offline-safe, gated by `PI_OFFLINE`/`PI_BG_DISABLE_UPDATE_CHECK`, and `PI_BG_REGISTRY_URL` overrides the registry endpoint
|
|
89
89
|
- shortcut/UI: component coverage for focused dock list/detail/key handling, detail output-tail scrolling (arrow/page scroll, follow-pause-on-scroll, `lines X–Y of N` position indicator, resume-follow-at-bottom, and no-scroll when output fits), empty/history/unread states, paging, close aliases, stop/stop-all/rerun/path actions, missing output files; SDK coverage for explicit `/bg-clear` finished-notice clearing, `/bg-clear` footer hinting, optional `Ctrl+Alt+C` fallback shortcut registration, and mixed failed/stopped/done/focused footer status; RPC coverage that `/bg-clear` works as a terminal-independent clear path; and PTY coverage for `/tasks`, `/bg-tasks`, real `Shift+Down`, arrows, page keys, detail/back/history/stop/stop-all/rerun/path/close, failed unread badges, and running/completed/failed/killed rerun paths
|
|
90
|
-
- runtime files: output and metadata files under `.pi/tasks/`, Fusion private `.pi/fusion/<session-id>-<pid>/<run-id>/` artifacts plus global `fusion-models.json`, persisted `isAgent` classification, task-owned context-window telemetry snapshots, cumulative background Pi-agent token usage, tool-use counts, agent model identifier (preferring the fully-qualified `provider/model` form), explicit `isAgent:true` telemetry wrapping for background `pi` agents, `isAgent:false` non-wrapping for scripts, attested Pi flat siblings (`.pi-events.jsonl`, `.stderr`, `.pi-telemetry-wrapper.cjs`, `.attestation.json`), real child `pi --mode json` tool-event parsing for background-agent telemetry, split/large telemetry ingestion, metadata after completion/failure, Fusion compact final-only
|
|
90
|
+
- runtime files: output and metadata files under `.pi/tasks/`, Fusion private `.pi/fusion/<session-id>-<pid>/<run-id>/` artifacts plus global `fusion-models.json`, persisted `isAgent` classification, task-owned context-window telemetry snapshots, cumulative background Pi-agent token usage, tool-use counts, agent model identifier (preferring the fully-qualified `provider/model` form), explicit `isAgent:true` telemetry wrapping for background `pi` agents, `isAgent:false` non-wrapping for scripts, attested Pi flat siblings (`.pi-events.jsonl`, `.stderr`, `.pi-telemetry-wrapper.cjs`, `.attestation.json`), real child `pi --mode json` tool-event parsing for background-agent telemetry, split/large telemetry ingestion, metadata after completion/failure, Fusion v2 compact final-only metadata and explicitly marked partial-response artifacts, and Fusion manifest token plus complete cost-component aggregates equal to the sum of successful and observed failed/cancelled attempts
|
|
91
91
|
- extension EventBus API: unit coverage for `pi-background-tasks:request:v1`/`response:v1` closed-frame validation, exact capability handshake, malformed payload rejection, unknown keys, unknown operations, duplicate request IDs, missing `session_start`, shutdown refusal, unsubscribe, strict terminal frame shape, and response-barrier ordering; registry coverage for exactly-one terminal publication after durable metadata plus loud/retriable terminal delivery failure; SDK coverage with a shared real `createEventBus()` loading the actual extension, starting `printf api-ok`, reading bounded logs, observing exactly one terminal event after the run response, and killing a real sleep task without model/provider calls
|
|
92
92
|
- attested Pi producer: unit/SDK coverage for 128-bit attested task ids, exact direct argv/cwd, ModelRegistry OAuth observation without secrets, raw Pi session/message events, separate stderr, prompt/report/source hashes, authority start/finish commit/tree/clean checks, atomic metadata serialization, completion visibility only after the sidecar is durable, malformed event rejection, and no attestation sidecar for ordinary tasks
|
|
93
93
|
- agent activity transcript: pure `parseAgentActivity`/`formatAgentActivityLine` coverage (assistant text, reasoning, tool start with arg summary, silent successful tool end, `✗ tool failed` errors, truncation, invalid/non-activity narrowing); registry-unit coverage that wrapped-agent stdout is reconstructed across split chunks into the human-readable transcript while telemetry/activity control JSON is stripped from the output file (telemetry fields still updated), stderr passes through, and the trailing partial line is flushed on finalize; SDK coverage that fake and real child `pi --mode json` runs surface `→ tool`/`✗ tool failed`/assistant text in `bg_logs` with no control JSON leaking into the visible output
|
|
94
94
|
- safety: kill, already-finished kill failure, timeout failure, spawn failure, low output-cap failure, multi-task shutdown cleanup, process-group kill fallback, Windows child-kill behavior, SIGKILL escalation, duplicate finalization/notification races, metadata/notification failure handling, and pruning
|
|
95
95
|
- agent loop: deterministic scripted-provider coverage against `extensions/background-tasks.ts` for actual event-driven `bg_run` behavior. The provider observes the effective system prompt, public tool descriptions, and real launch receipt and deliberately emits the pre-fix `bg_status` poll if any contract layer is absent; the passing path proves one launch, no sleep/status/log polling, one durable terminal notification, and exactly one follow-up turn. It also covers notification-only `triggerOnCompletion:false`, `/bg` display-only behavior, `notifyOnCompletion:false`, failed-task notification error fields, and parent-model `fusion_brainstorm` tool use followed by normal parent response
|
|
96
|
-
- package: manifest, docs, `pi.extensions`, exported `src/core/extension-api.ts`, peer dependency/import parity, packed runtime files, tarball-install smoke, direct-completion import bans, test/helper/script/artifact exclusion,
|
|
96
|
+
- package: manifest, docs, `pi.extensions`, exported `src/core/extension-api.ts`, peer dependency/import parity, packed runtime files, tarball-install smoke, direct-completion import bans, test/helper/script/artifact exclusion, isolated offline npm installation, exact-version compatibility, and current-host persisted/replayed tool-usage safety
|
|
97
97
|
|
|
98
98
|
## PTY notes
|
|
99
99
|
|
package/TEST_PLAN.md
CHANGED
|
@@ -34,7 +34,7 @@ This package follows:
|
|
|
34
34
|
| Scripted provider | `npm run test:agent-loop` | full gate | implemented |
|
|
35
35
|
| Pack dry run | `npm run pack:dry-run` | release gate | implemented |
|
|
36
36
|
| Smoke | `npm run smoke` | no | implemented; isolated load-only |
|
|
37
|
-
| Compatibility | `npm run test:compat` | release gate | implemented; exact Pi `0.75.5`, `0.81.1`, `0.82.1` pack/install plus `/jobs`, `/fusion`, and `/fusion-models` surfaces |
|
|
37
|
+
| Compatibility | `npm run test:compat` | release gate | implemented; exact Pi `0.75.5`, `0.81.1`, `0.82.1` pack/install plus `/jobs`, `/fusion`, and `/fusion-models` surfaces, followed by a current-host real `fusion_brainstorm` session-stat and replay witness |
|
|
38
38
|
|
|
39
39
|
## Feature coverage matrix
|
|
40
40
|
|
|
@@ -51,9 +51,9 @@ This package follows:
|
|
|
51
51
|
| Read task logs | `bg_logs` | yes | yes | | | | | yes | SDK verifies bounded content and the model-facing no-wait-loop contract. Scripted-provider ordinary waiting makes zero log calls. |
|
|
52
52
|
| Stop task from LLM tool | `bg_kill` | | yes | | | | | | Covers running kill and already-finished loud failure. |
|
|
53
53
|
| Fusion command direct result | `/fusion`, `fusion-result` custom message | yes | yes | yes | | yes | yes | | Core unit covers deterministic full text-transcript context 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. |
|
|
54
|
-
| Fusion tool result | `fusion_brainstorm({prompt})` | yes | yes | | |
|
|
54
|
+
| 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. |
|
|
55
55
|
| 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. |
|
|
56
|
-
| Fusion child isolation/lifecycle | child `pi --mode text`, private compact metadata extension, shutdown cleanup, `.pi/fusion` artifacts | yes | yes | yes | |
|
|
56
|
+
| 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. |
|
|
57
57
|
| 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. |
|
|
58
58
|
| Terminal EventBus publication | `pi-background-tasks:terminal:v1` | yes | yes | | | | yes | | Registry unit proves exactly-one terminal snapshot after durable metadata and loud/retriable EventBus delivery failure; extension API unit proves one strict terminal frame correlated by task id after the run response for immediate, normal, failed, timeout, and killed tasks; SDK observes one terminal event for a completed task and one for a killed task through the real extension service. |
|
|
59
59
|
| Completion notification | custom message `background-task-notification` | yes | yes | | renderer via typecheck | | | yes | BUG-181 unit/SDK coverage pins the durable-terminal guidance and truthful effective delivery receipts. Scripted provider loads the shipped extension, conditionally attempts the old `bg_status` poll when the real system prompt/descriptions/receipt are incomplete, and proves the fixed contract instead yields with exactly one `bg_run`, one terminal notification, and one default follow-up. Notification-only, notification-disabled, failed-task, and display-only `/bg` paths remain covered. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-background-tasks",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
EMPTY_FUSION_USAGE,
|
|
9
9
|
FUSION_MANIFEST_SCHEMA_VERSION,
|
|
10
10
|
FusionError,
|
|
11
|
+
cloneFusionUsage,
|
|
11
12
|
type FusionArtifactManifest,
|
|
12
13
|
type FusionArtifactRef,
|
|
13
14
|
type FusionAttemptArtifactRecord,
|
|
@@ -83,18 +84,6 @@ function makeRunId(): string {
|
|
|
83
84
|
return `f${randomBytes(16).toString('hex')}`;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function usageClone(usage: FusionUsage): FusionUsage {
|
|
87
|
-
const out: FusionUsage = {
|
|
88
|
-
input: usage.input,
|
|
89
|
-
output: usage.output,
|
|
90
|
-
cacheRead: usage.cacheRead,
|
|
91
|
-
cacheWrite: usage.cacheWrite,
|
|
92
|
-
totalTokens: usage.totalTokens,
|
|
93
|
-
};
|
|
94
|
-
if (usage.costTotal !== undefined) out.costTotal = usage.costTotal;
|
|
95
|
-
return out;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
87
|
function modelsForManifest(models: ResolvedFusionModels): MutableFusionArtifactManifest['models'] {
|
|
99
88
|
const first = models.candidates[0].qualifiedId;
|
|
100
89
|
const second = models.candidates[1].qualifiedId;
|
|
@@ -196,7 +185,7 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
|
|
|
196
185
|
cwd: manifest.cwd,
|
|
197
186
|
config: manifest.config,
|
|
198
187
|
models: manifest.models,
|
|
199
|
-
usage:
|
|
188
|
+
usage: cloneFusionUsage(manifest.usage),
|
|
200
189
|
attempts: [...manifest.attempts],
|
|
201
190
|
artifacts: { ...manifest.artifacts },
|
|
202
191
|
};
|
|
@@ -260,7 +249,7 @@ export class FusionArtifactStore {
|
|
|
260
249
|
cwd: options.cwd,
|
|
261
250
|
config: options.config,
|
|
262
251
|
models: modelsForManifest(options.models),
|
|
263
|
-
usage:
|
|
252
|
+
usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
|
|
264
253
|
attempts: [],
|
|
265
254
|
artifacts: {},
|
|
266
255
|
};
|
|
@@ -316,7 +305,7 @@ export class FusionArtifactStore {
|
|
|
316
305
|
|
|
317
306
|
async setUsage(usage: FusionUsage): Promise<void> {
|
|
318
307
|
await this.updateManifest((manifest) => {
|
|
319
|
-
manifest.usage =
|
|
308
|
+
manifest.usage = cloneFusionUsage(usage);
|
|
320
309
|
});
|
|
321
310
|
}
|
|
322
311
|
|
|
@@ -372,7 +361,7 @@ export class FusionArtifactStore {
|
|
|
372
361
|
provider: input.result.provider,
|
|
373
362
|
model: input.result.model,
|
|
374
363
|
qualifiedId: input.result.qualifiedId,
|
|
375
|
-
usage:
|
|
364
|
+
usage: cloneFusionUsage(input.result.usage),
|
|
376
365
|
};
|
|
377
366
|
if (input.result.slot !== undefined) record.slot = input.result.slot;
|
|
378
367
|
manifest.attempts.push(record);
|
|
@@ -403,12 +392,11 @@ export class FusionArtifactStore {
|
|
|
403
392
|
response_path: responseRef.path,
|
|
404
393
|
error: input.error,
|
|
405
394
|
};
|
|
406
|
-
if (partialResponseRef !== undefined)
|
|
407
|
-
record.partial_response_path = partialResponseRef.path;
|
|
395
|
+
if (partialResponseRef !== undefined) record.partial_response_path = partialResponseRef.path;
|
|
408
396
|
if (input.provider !== undefined) record.provider = input.provider;
|
|
409
397
|
if (input.model !== undefined) record.model = input.model;
|
|
410
398
|
if (input.qualifiedId !== undefined) record.qualifiedId = input.qualifiedId;
|
|
411
|
-
if (input.usage !== undefined) record.usage =
|
|
399
|
+
if (input.usage !== undefined) record.usage = cloneFusionUsage(input.usage);
|
|
412
400
|
if (input.slot !== undefined) record.slot = input.slot;
|
|
413
401
|
manifest.attempts.push(record);
|
|
414
402
|
});
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
import {
|
|
28
28
|
FUSION_RESULT_SCHEMA_VERSION,
|
|
29
29
|
FusionError,
|
|
30
|
+
addFusionUsage,
|
|
31
|
+
createEmptyFusionUsage,
|
|
30
32
|
type FusionCanonicalInputV1,
|
|
31
33
|
type FusionCandidateId,
|
|
32
34
|
type FusionChildRunResult,
|
|
@@ -80,21 +82,8 @@ interface EvaluationAttemptResult {
|
|
|
80
82
|
errors: readonly string[];
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
function emptyUsage(): FusionUsage {
|
|
84
|
-
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0 };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function addUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
88
|
-
target.input += delta.input;
|
|
89
|
-
target.output += delta.output;
|
|
90
|
-
target.cacheRead += delta.cacheRead;
|
|
91
|
-
target.cacheWrite += delta.cacheWrite;
|
|
92
|
-
target.totalTokens += delta.totalTokens;
|
|
93
|
-
if (delta.costTotal !== undefined) target.costTotal = (target.costTotal ?? 0) + delta.costTotal;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
85
|
function addFailedChildUsage(target: FusionUsage, error: unknown): void {
|
|
97
|
-
if (error instanceof FusionChildRunError)
|
|
86
|
+
if (error instanceof FusionChildRunError) addFusionUsage(target, error.usage);
|
|
98
87
|
}
|
|
99
88
|
|
|
100
89
|
function errorText(error: unknown): string {
|
|
@@ -340,7 +329,7 @@ export class FusionOrchestrator {
|
|
|
340
329
|
if (this.now !== undefined) storeOptions.now = this.now;
|
|
341
330
|
const store = await this.createArtifactStore(storeOptions);
|
|
342
331
|
input.onProgress?.({ type: 'state', state: 'initializing' });
|
|
343
|
-
const usage =
|
|
332
|
+
const usage = createEmptyFusionUsage();
|
|
344
333
|
try {
|
|
345
334
|
await store.writeCanonicalInput(input.canonicalInputSerialized);
|
|
346
335
|
await store.transition('candidates_running');
|
|
@@ -378,7 +367,7 @@ export class FusionOrchestrator {
|
|
|
378
367
|
undefined,
|
|
379
368
|
'md',
|
|
380
369
|
);
|
|
381
|
-
|
|
370
|
+
addFusionUsage(usage, merged.usage);
|
|
382
371
|
await store.recordChildAttempt({ result: merged, prompt: mergePrompt, responseKind: 'md' });
|
|
383
372
|
await store.writeMerged(merged.text);
|
|
384
373
|
await store.setUsage(usage);
|
|
@@ -465,7 +454,7 @@ export class FusionOrchestrator {
|
|
|
465
454
|
).then(async (result) => {
|
|
466
455
|
await store.recordChildAttempt({ result, prompt, responseKind: 'md' });
|
|
467
456
|
completed += 1;
|
|
468
|
-
|
|
457
|
+
addFusionUsage(usage, result.usage);
|
|
469
458
|
await store.setUsage(usage);
|
|
470
459
|
input.onProgress?.({ type: 'candidate_completed', slot, completed, total: 3 });
|
|
471
460
|
return { slot, result };
|
|
@@ -545,7 +534,7 @@ export class FusionOrchestrator {
|
|
|
545
534
|
'txt',
|
|
546
535
|
attempt,
|
|
547
536
|
);
|
|
548
|
-
|
|
537
|
+
addFusionUsage(usage, result.usage);
|
|
549
538
|
await store.recordChildAttempt({ result, prompt, responseKind: 'txt' });
|
|
550
539
|
await store.setUsage(usage);
|
|
551
540
|
const parsed = parseEvaluationAttempt(result.text);
|
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
9
9
|
type FusionChildResultMetadata,
|
|
10
10
|
} from '../../fusion-child-extension.js';
|
|
11
|
-
import type { ResolvedFusionModel } from './types.js';
|
|
12
11
|
import {
|
|
13
12
|
FusionError,
|
|
13
|
+
addFusionUsage,
|
|
14
|
+
cloneFusionUsage,
|
|
15
|
+
createEmptyFusionUsage,
|
|
14
16
|
type FusionChildRunResult,
|
|
15
17
|
type FusionErrorDetails,
|
|
16
18
|
type FusionStage,
|
|
17
19
|
type FusionUsage,
|
|
20
|
+
type ResolvedFusionModel,
|
|
18
21
|
} from './types.js';
|
|
19
22
|
import { isJsonObject, parseJsonText } from '../common.js';
|
|
20
23
|
|
|
@@ -149,7 +152,7 @@ export class FusionChildRunError extends FusionError {
|
|
|
149
152
|
this.stderr = stderr;
|
|
150
153
|
this.exitCode = close.code;
|
|
151
154
|
this.signalName = close.signal;
|
|
152
|
-
this.usage =
|
|
155
|
+
this.usage = cloneFusionUsage(observed.usage);
|
|
153
156
|
this.provider = observed.provider;
|
|
154
157
|
this.modelName = observed.model;
|
|
155
158
|
this.qualifiedId = observed.qualifiedId;
|
|
@@ -204,15 +207,6 @@ export function buildFusionPiChildArgv(
|
|
|
204
207
|
];
|
|
205
208
|
}
|
|
206
209
|
|
|
207
|
-
function addUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
208
|
-
target.input += delta.input;
|
|
209
|
-
target.output += delta.output;
|
|
210
|
-
target.cacheRead += delta.cacheRead;
|
|
211
|
-
target.cacheWrite += delta.cacheWrite;
|
|
212
|
-
target.totalTokens += delta.totalTokens;
|
|
213
|
-
if (delta.costTotal !== undefined) target.costTotal = (target.costTotal ?? 0) + delta.costTotal;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
210
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
217
211
|
const FUSION_CHILD_RESULT_PREFIX_BYTES = Buffer.from(FUSION_CHILD_RESULT_PREFIX, 'utf8');
|
|
218
212
|
|
|
@@ -265,32 +259,42 @@ function requireUsageInteger(
|
|
|
265
259
|
return value;
|
|
266
260
|
}
|
|
267
261
|
|
|
262
|
+
function requireCostNumber(
|
|
263
|
+
record: Record<PropertyKey, unknown>,
|
|
264
|
+
key: string,
|
|
265
|
+
label: string,
|
|
266
|
+
): number {
|
|
267
|
+
const value = record[key];
|
|
268
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
269
|
+
throw new Error(`${label}.${key} must be a non-negative finite number`);
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
268
273
|
function parseCompactUsage(value: unknown): FusionUsage {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const usage: FusionUsage = {
|
|
274
|
+
const record = assertClosedRecord(
|
|
275
|
+
value,
|
|
276
|
+
['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens', 'cost'],
|
|
277
|
+
'fusion child usage',
|
|
278
|
+
);
|
|
279
|
+
const cost = assertClosedRecord(
|
|
280
|
+
record['cost'],
|
|
281
|
+
['input', 'output', 'cacheRead', 'cacheWrite', 'total'],
|
|
282
|
+
'fusion child usage.cost',
|
|
283
|
+
);
|
|
284
|
+
return {
|
|
281
285
|
input: requireUsageInteger(record, 'input', 'fusion child usage'),
|
|
282
286
|
output: requireUsageInteger(record, 'output', 'fusion child usage'),
|
|
283
287
|
cacheRead: requireUsageInteger(record, 'cacheRead', 'fusion child usage'),
|
|
284
288
|
cacheWrite: requireUsageInteger(record, 'cacheWrite', 'fusion child usage'),
|
|
285
289
|
totalTokens: requireUsageInteger(record, 'totalTokens', 'fusion child usage'),
|
|
290
|
+
cost: {
|
|
291
|
+
input: requireCostNumber(cost, 'input', 'fusion child usage.cost'),
|
|
292
|
+
output: requireCostNumber(cost, 'output', 'fusion child usage.cost'),
|
|
293
|
+
cacheRead: requireCostNumber(cost, 'cacheRead', 'fusion child usage.cost'),
|
|
294
|
+
cacheWrite: requireCostNumber(cost, 'cacheWrite', 'fusion child usage.cost'),
|
|
295
|
+
total: requireCostNumber(cost, 'total', 'fusion child usage.cost'),
|
|
296
|
+
},
|
|
286
297
|
};
|
|
287
|
-
const costTotal = record['costTotal'];
|
|
288
|
-
if (costTotal !== undefined) {
|
|
289
|
-
if (typeof costTotal !== 'number' || !Number.isFinite(costTotal) || costTotal < 0)
|
|
290
|
-
throw new Error('fusion child usage.costTotal must be a non-negative finite number');
|
|
291
|
-
usage.costTotal = costTotal;
|
|
292
|
-
}
|
|
293
|
-
return usage;
|
|
294
298
|
}
|
|
295
299
|
|
|
296
300
|
function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
|
|
@@ -385,7 +389,8 @@ function reconstructFinalText(response: Buffer, record: FusionChildResultMetadat
|
|
|
385
389
|
if (sha256Buffer(joined) !== record.text_sha256)
|
|
386
390
|
throw new Error('Pi final text aggregate hash mismatch');
|
|
387
391
|
const text = joined.toString('utf8');
|
|
388
|
-
if (!Buffer.from(text, 'utf8').equals(joined))
|
|
392
|
+
if (!Buffer.from(text, 'utf8').equals(joined))
|
|
393
|
+
throw new Error('Pi final text is not valid UTF-8');
|
|
389
394
|
if (text.trim().length === 0) throw new Error('Pi assistant response is empty');
|
|
390
395
|
return text;
|
|
391
396
|
}
|
|
@@ -404,11 +409,14 @@ export class FusionPiCompactResultParser {
|
|
|
404
409
|
const parsed = parseFusionChildStderr(stderr);
|
|
405
410
|
return this.observedFromRecords(parsed.records);
|
|
406
411
|
} catch {
|
|
407
|
-
return { usage:
|
|
412
|
+
return { usage: createEmptyFusionUsage() };
|
|
408
413
|
}
|
|
409
414
|
}
|
|
410
415
|
|
|
411
|
-
finish(
|
|
416
|
+
finish(
|
|
417
|
+
response: Buffer,
|
|
418
|
+
stderr: Buffer,
|
|
419
|
+
): {
|
|
412
420
|
text: string;
|
|
413
421
|
usage: FusionUsage;
|
|
414
422
|
provider: string;
|
|
@@ -443,15 +451,11 @@ export class FusionPiCompactResultParser {
|
|
|
443
451
|
}
|
|
444
452
|
}
|
|
445
453
|
|
|
446
|
-
private observedFromRecords(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
cacheWrite: 0,
|
|
452
|
-
totalTokens: 0,
|
|
453
|
-
};
|
|
454
|
-
for (const record of records) addUsage(usage, record.usage);
|
|
454
|
+
private observedFromRecords(
|
|
455
|
+
records: readonly FusionChildResultMetadata[],
|
|
456
|
+
): ObservedChildSnapshot {
|
|
457
|
+
const usage = createEmptyFusionUsage();
|
|
458
|
+
for (const record of records) addFusionUsage(usage, record.usage);
|
|
455
459
|
const final = records.at(-1);
|
|
456
460
|
if (final === undefined) return { usage };
|
|
457
461
|
return {
|
package/src/core/fusion/types.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { Usage } from '@earendil-works/pi-ai';
|
|
2
|
+
|
|
1
3
|
export type FusionThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
2
4
|
|
|
3
5
|
export const FUSION_MODEL_CONFIG_SCHEMA_VERSION = 'pi-background-tasks.fusion-models.v1';
|
|
4
6
|
export const FUSION_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.v1';
|
|
5
7
|
export const FUSION_EVALUATION_SCHEMA_VERSION = 'pi-background-tasks.fusion-evaluation.v1';
|
|
6
|
-
export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.
|
|
7
|
-
export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.
|
|
8
|
+
export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v2';
|
|
9
|
+
export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v2';
|
|
8
10
|
|
|
9
11
|
export const FUSION_CANDIDATE_IDS = ['A', 'B', 'C'] as const;
|
|
10
12
|
export type FusionCandidateId = (typeof FUSION_CANDIDATE_IDS)[number];
|
|
@@ -124,14 +126,16 @@ export interface FusionEvaluationV1 {
|
|
|
124
126
|
synthesis_plan: FusionSynthesisPlan;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
/** Exact Pi usage contract used at the child, artifact, and host tool-result boundaries. */
|
|
130
|
+
export type FusionUsage = Usage;
|
|
131
|
+
|
|
132
|
+
const EMPTY_FUSION_COST: Usage['cost'] = Object.freeze({
|
|
133
|
+
input: 0,
|
|
134
|
+
output: 0,
|
|
135
|
+
cacheRead: 0,
|
|
136
|
+
cacheWrite: 0,
|
|
137
|
+
total: 0,
|
|
138
|
+
});
|
|
135
139
|
|
|
136
140
|
export const EMPTY_FUSION_USAGE: FusionUsage = Object.freeze({
|
|
137
141
|
input: 0,
|
|
@@ -139,8 +143,43 @@ export const EMPTY_FUSION_USAGE: FusionUsage = Object.freeze({
|
|
|
139
143
|
cacheRead: 0,
|
|
140
144
|
cacheWrite: 0,
|
|
141
145
|
totalTokens: 0,
|
|
146
|
+
cost: EMPTY_FUSION_COST,
|
|
142
147
|
});
|
|
143
148
|
|
|
149
|
+
export function createEmptyFusionUsage(): FusionUsage {
|
|
150
|
+
return cloneFusionUsage(EMPTY_FUSION_USAGE);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function cloneFusionUsage(usage: FusionUsage): FusionUsage {
|
|
154
|
+
return {
|
|
155
|
+
input: usage.input,
|
|
156
|
+
output: usage.output,
|
|
157
|
+
cacheRead: usage.cacheRead,
|
|
158
|
+
cacheWrite: usage.cacheWrite,
|
|
159
|
+
totalTokens: usage.totalTokens,
|
|
160
|
+
cost: {
|
|
161
|
+
input: usage.cost.input,
|
|
162
|
+
output: usage.cost.output,
|
|
163
|
+
cacheRead: usage.cost.cacheRead,
|
|
164
|
+
cacheWrite: usage.cost.cacheWrite,
|
|
165
|
+
total: usage.cost.total,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
171
|
+
target.input += delta.input;
|
|
172
|
+
target.output += delta.output;
|
|
173
|
+
target.cacheRead += delta.cacheRead;
|
|
174
|
+
target.cacheWrite += delta.cacheWrite;
|
|
175
|
+
target.totalTokens += delta.totalTokens;
|
|
176
|
+
target.cost.input += delta.cost.input;
|
|
177
|
+
target.cost.output += delta.cost.output;
|
|
178
|
+
target.cost.cacheRead += delta.cost.cacheRead;
|
|
179
|
+
target.cost.cacheWrite += delta.cost.cacheWrite;
|
|
180
|
+
target.cost.total += delta.cost.total;
|
|
181
|
+
}
|
|
182
|
+
|
|
144
183
|
export interface FusionResultDetails {
|
|
145
184
|
schema_version: typeof FUSION_RESULT_SCHEMA_VERSION;
|
|
146
185
|
run_id: string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { Usage } from '@earendil-works/pi-ai';
|
|
2
3
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
3
4
|
|
|
4
5
|
export const FUSION_CHILD_RESULT_SCHEMA_VERSION =
|
|
5
|
-
'pi-background-tasks.fusion-child-result.
|
|
6
|
+
'pi-background-tasks.fusion-child-result.v2' as const;
|
|
6
7
|
export const FUSION_CHILD_RESULT_PREFIX = '\u001ePI_FUSION_CHILD_RESULT ';
|
|
7
8
|
|
|
8
9
|
export interface FusionChildTextBlockMetadata {
|
|
@@ -10,14 +11,7 @@ export interface FusionChildTextBlockMetadata {
|
|
|
10
11
|
sha256: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
input: number;
|
|
15
|
-
output: number;
|
|
16
|
-
cacheRead: number;
|
|
17
|
-
cacheWrite: number;
|
|
18
|
-
totalTokens: number;
|
|
19
|
-
costTotal?: number;
|
|
20
|
-
}
|
|
14
|
+
export type FusionChildResultUsageMetadata = Usage;
|
|
21
15
|
|
|
22
16
|
export interface FusionChildResultMetadata {
|
|
23
17
|
schema_version: typeof FUSION_CHILD_RESULT_SCHEMA_VERSION;
|
|
@@ -38,14 +32,7 @@ export function buildFusionChildResultMetadata(message: {
|
|
|
38
32
|
model: string;
|
|
39
33
|
stopReason: string;
|
|
40
34
|
content: ReadonlyArray<{ type: string; text?: string }>;
|
|
41
|
-
usage:
|
|
42
|
-
input: number;
|
|
43
|
-
output: number;
|
|
44
|
-
cacheRead: number;
|
|
45
|
-
cacheWrite: number;
|
|
46
|
-
totalTokens: number;
|
|
47
|
-
cost: { total: number };
|
|
48
|
-
};
|
|
35
|
+
usage: Usage;
|
|
49
36
|
}): FusionChildResultMetadata {
|
|
50
37
|
const textBlocks = message.content.flatMap((part) =>
|
|
51
38
|
part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
|
|
@@ -56,10 +43,14 @@ export function buildFusionChildResultMetadata(message: {
|
|
|
56
43
|
cacheRead: message.usage.cacheRead,
|
|
57
44
|
cacheWrite: message.usage.cacheWrite,
|
|
58
45
|
totalTokens: message.usage.totalTokens,
|
|
46
|
+
cost: {
|
|
47
|
+
input: message.usage.cost.input,
|
|
48
|
+
output: message.usage.cost.output,
|
|
49
|
+
cacheRead: message.usage.cost.cacheRead,
|
|
50
|
+
cacheWrite: message.usage.cost.cacheWrite,
|
|
51
|
+
total: message.usage.cost.total,
|
|
52
|
+
},
|
|
59
53
|
};
|
|
60
|
-
if (Number.isFinite(message.usage.cost.total) && message.usage.cost.total >= 0) {
|
|
61
|
-
usage.costTotal = message.usage.cost.total;
|
|
62
|
-
}
|
|
63
54
|
return {
|
|
64
55
|
schema_version: FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
65
56
|
provider: message.provider,
|
package/src/fusion-extension.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Usage } from '@earendil-works/pi-ai';
|
|
1
2
|
import type {
|
|
2
3
|
AgentToolResult,
|
|
3
4
|
ExtensionAPI,
|
|
@@ -26,6 +27,7 @@ import { FusionOrchestrator } from './core/fusion/orchestrator.js';
|
|
|
26
27
|
import {
|
|
27
28
|
FUSION_RESULT_SCHEMA_VERSION,
|
|
28
29
|
FusionError,
|
|
30
|
+
cloneFusionUsage,
|
|
29
31
|
type FusionModelConfigV1,
|
|
30
32
|
type FusionModelSelection,
|
|
31
33
|
type FusionProgressEvent,
|
|
@@ -49,7 +51,7 @@ const FUSION_MODEL_COMMAND_NAME = 'fusion-models';
|
|
|
49
51
|
|
|
50
52
|
type FusionToolDetails = FusionResultDetails | FusionProgressDetails;
|
|
51
53
|
type FusionToolResultWithUsage = AgentToolResult<FusionToolDetails> & {
|
|
52
|
-
usage:
|
|
54
|
+
usage: Usage;
|
|
53
55
|
};
|
|
54
56
|
|
|
55
57
|
type CommandDialogResult =
|
|
@@ -160,8 +162,7 @@ function makeProgressDetails(event: FusionProgressEvent): FusionProgressDetails
|
|
|
160
162
|
|
|
161
163
|
function usageSummary(details: FusionResultDetails): string {
|
|
162
164
|
const tokens = details.usage.totalTokens;
|
|
163
|
-
const cost =
|
|
164
|
-
details.usage.costTotal === undefined ? '' : ` · $${details.usage.costTotal.toFixed(4)}`;
|
|
165
|
+
const cost = ` · $${details.usage.cost.total.toFixed(4)}`;
|
|
165
166
|
return `${String(tokens)} tokens${cost}`;
|
|
166
167
|
}
|
|
167
168
|
|
|
@@ -578,7 +579,7 @@ export function registerFusionExtension(pi: ExtensionAPI): void {
|
|
|
578
579
|
const toolResult: FusionToolResultWithUsage = {
|
|
579
580
|
content: textContent(result.mergedText),
|
|
580
581
|
details: result.details,
|
|
581
|
-
usage: result.details.usage,
|
|
582
|
+
usage: cloneFusionUsage(result.details.usage),
|
|
582
583
|
};
|
|
583
584
|
return toolResult;
|
|
584
585
|
},
|