@iowarp/clio-coder 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -2
- package/CONTRIBUTING.md +6 -6
- package/README.md +2 -2
- package/dist/{acp-S5R4RR5B.js → acp-2BEHC4DL.js} +4 -4
- package/dist/{agents-P6DMMVZY.js → agents-LNNFTM53.js} +13 -11
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-2XCZLPKS.js → auth-KXXFI2VS.js} +6 -6
- package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
- package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
- package/dist/chunk-3BPUFZDL.js +37 -0
- package/dist/{chunk-WPQLXFOZ.js → chunk-43AOLP7E.js} +2 -2
- package/dist/{chunk-N4CZJQRK.js → chunk-5JGRAMKL.js} +4 -4
- package/dist/{chunk-BRXQQJFP.js → chunk-6US73PDB.js} +568 -47
- package/dist/{chunk-K6WL7QZT.js → chunk-6XXKFVSN.js} +2 -2
- package/dist/{chunk-QQK64KLB.js → chunk-CJUB2JJ2.js} +138 -20
- package/dist/{chunk-HV5X7OR2.js → chunk-CKXWIANG.js} +12 -12
- package/dist/{chunk-UZHIZC5S.js → chunk-CYQKWTG3.js} +61 -76
- package/dist/{chunk-QWU7ZBO7.js → chunk-DJVECN66.js} +204 -45
- package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
- package/dist/{chunk-7RXG6QRZ.js → chunk-EKY57CSP.js} +2 -75
- package/dist/{chunk-EDRHSCIE.js → chunk-EYPA3EGJ.js} +10 -2
- package/dist/{chunk-TTNYS3EA.js → chunk-G7MUEIGA.js} +1 -1
- package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
- package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
- package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
- package/dist/chunk-HWUFFB6L.js +83 -0
- package/dist/{chunk-35MKKU5R.js → chunk-K7T3E2SR.js} +15 -8
- package/dist/{chunk-VAWWTKDP.js → chunk-KHSFENX2.js} +2 -2
- package/dist/chunk-LCGCVYZ4.js +57 -0
- package/dist/{chunk-X6COSD2O.js → chunk-LYF7OHWH.js} +41 -14
- package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
- package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
- package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
- package/dist/{chunk-3JLKSKD7.js → chunk-OH3TOQTB.js} +5 -1
- package/dist/{chunk-MEQ45TQ4.js → chunk-PBTHKCPN.js} +18 -4
- package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
- package/dist/{chunk-QQL5RT5M.js → chunk-QM3F2GKX.js} +94 -36
- package/dist/{chunk-A2GZF7DC.js → chunk-QNQHSOLF.js} +4 -4
- package/dist/{chunk-KRPY7NTG.js → chunk-R46L2BIR.js} +3 -3
- package/dist/{chunk-BP4OYD6A.js → chunk-RY3LY4J5.js} +20 -2
- package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
- package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
- package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
- package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
- package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
- package/dist/{chunk-HXG4IURW.js → chunk-XE2VEJHX.js} +2 -2
- package/dist/{chunk-3HZ5RWN2.js → chunk-XF5N4U5A.js} +7 -6
- package/dist/{chunk-ZYKPLLNQ.js → chunk-XXQNGV4M.js} +590 -32
- package/dist/{chunk-4JUF2NNX.js → chunk-XYDYPRZI.js} +4 -4
- package/dist/{chunk-VMNQ6OZA.js → chunk-ZRGEBJ4T.js} +971 -794
- package/dist/{chunk-2LZI5CAG.js → chunk-ZXF4XRKW.js} +75 -33
- package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
- package/dist/cli/index.js +31 -31
- package/dist/{clio-J5JIOIDS.js → clio-M2KGYUFZ.js} +2 -2
- package/dist/{code-nav-AXCXSBHX.js → code-nav-GQNL7XA6.js} +5 -5
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
- package/dist/{config-OEBMIN2U.js → config-XUUYQIWO.js} +27 -25
- package/dist/{configure-PUQOSIXQ.js → configure-IHJ7YOMV.js} +7 -7
- package/dist/{context-URSXPBCK.js → context-74JLXAWD.js} +12 -12
- package/dist/{context-MGSE4Z2T.js → context-75MIWW3U.js} +24 -22
- package/dist/{context-EKDCKUUZ.js → context-ZQ7SIFJV.js} +8 -7
- package/dist/{context-clear-KDAJRNUK.js → context-clear-GYKWNUML.js} +24 -22
- package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
- package/dist/{context-working-set-SBKMPPI2.js → context-working-set-UX5KEP4J.js} +11 -10
- package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-GIJBHNFL.js} +21 -20
- package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
- package/dist/{doctor-7BSE27PJ.js → doctor-SVJ5BZCW.js} +4 -4
- package/dist/{eval-IZGDOO4H.js → eval-CG6LLBLD.js} +47 -232
- package/dist/{evidence-SR7WXB5B.js → evidence-ZYFIEN42.js} +19 -18
- package/dist/{evolve-K7VE2CBX.js → evolve-QGEXEMDW.js} +19 -18
- package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
- package/dist/{fleet-7XMJNQNF.js → fleet-S5R4ZOQY.js} +49 -30
- package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
- package/dist/{init-JGNPAYXT.js → init-5DRU55YR.js} +31 -29
- package/dist/memory-7YKKR6UC.js +467 -0
- package/dist/{models-ZMMLFJNN.js → models-ZPOLRU2C.js} +10 -10
- package/dist/{monitor-2F3T5KHP.js → monitor-US5F5YGZ.js} +33 -18
- package/dist/{orchestrator-ORHT43JB.js → orchestrator-E2AL4T5N.js} +1092 -659
- package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
- package/dist/{reset-NXGTYNUO.js → reset-KZ652EK6.js} +3 -3
- package/dist/{run-RF4WJGMT.js → run-SRNBKDWD.js} +52 -40
- package/dist/{share-UT3W6E4M.js → share-CGZE33UP.js} +3 -3
- package/dist/{skills-PSACKC5Q.js → skills-S2X4DLY5.js} +4 -4
- package/dist/{skills-eval-WJSI55RZ.js → skills-eval-W2GGIC4R.js} +19 -18
- package/dist/{targets-PIIRAOYS.js → targets-54SWINWB.js} +14 -12
- package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-SAIF2OGY.js} +5 -4
- package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
- package/dist/{upgrade-346TZ6AV.js → upgrade-JKAR27XC.js} +8 -8
- package/dist/{usage-6KKXR32N.js → usage-MSAWCLX4.js} +60 -27
- package/dist/{verifiers-4UUM6TEE.js → verifiers-NCBTHHN2.js} +60 -54
- package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-GUSOQ6ZP.js} +30 -28
- package/dist/worker/entry.js +69 -58
- package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
- package/docs/README.md +3 -3
- package/docs/acp.md +1 -1
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/artifact-placement.md +1 -2
- package/docs/artifact-versions.md +1 -1
- package/docs/built-in-agents.md +1 -1
- package/docs/capacity-and-scheduling.md +1 -1
- package/docs/commands-and-modes.md +9 -7
- package/docs/configuration-and-targets.md +12 -1
- package/docs/context-engine.md +4 -2
- package/docs/context-working-set.md +4 -4
- package/docs/development-pipeline.md +1 -1
- package/docs/documentation-coverage.md +3 -3
- package/docs/documentation-guide.md +2 -2
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +4 -45
- package/docs/evidence-and-memory.md +67 -7
- package/docs/evolution.md +1 -1
- package/docs/exit-codes-and-output.md +1 -1
- package/docs/extensions-and-sharing.md +2 -2
- package/docs/fleet-dispatch.md +28 -2
- package/docs/installation-and-lifecycle.md +2 -2
- package/docs/middleware-and-components.md +19 -2
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +3 -3
- package/docs/proactive-memory.md +26 -16
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +38 -35
- package/docs/safety-model.md +29 -7
- package/docs/scientific-validation.md +3 -3
- package/docs/session-lifecycle.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +2 -2
- package/docs/trace-store.md +1 -1
- package/docs/troubleshooting.md +1 -1
- package/docs/tui-design.md +38 -4
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +7 -4
- package/src/cli/agents.ts +2 -3
- package/src/cli/argv.ts +14 -1
- package/src/cli/fleet.ts +15 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/memory.ts +272 -10
- package/src/cli/modes/json-stream.ts +2 -2
- package/src/cli/modes/print.ts +12 -1
- package/src/cli/run.ts +22 -2
- package/src/cli/targets.ts +12 -3
- package/src/cli/usage.ts +55 -7
- package/src/core/bus-events.ts +3 -0
- package/src/core/response-model-id.ts +134 -0
- package/src/core/toml.ts +62 -0
- package/src/core/workspace-files.ts +0 -1
- package/src/domains/agents/builtins/architect.md +1 -1
- package/src/domains/agents/catalog.ts +5 -4
- package/src/domains/agents/recipe.ts +54 -14
- package/src/domains/agents/result-contract.ts +7 -4
- package/src/domains/context/bootstrap.ts +36 -27
- package/src/domains/context/project-metadata.ts +19 -63
- package/src/domains/context/prompt-context.ts +8 -0
- package/src/domains/context/working-set/policies/index.ts +3 -4
- package/src/domains/dispatch/budget-envelope.ts +396 -0
- package/src/domains/dispatch/contract.ts +2 -0
- package/src/domains/dispatch/extension.ts +81 -27
- package/src/domains/dispatch/orphan-recovery.ts +1 -0
- package/src/domains/dispatch/receipt-integrity.ts +4 -0
- package/src/domains/dispatch/state.ts +1 -0
- package/src/domains/dispatch/types.ts +10 -3
- package/src/domains/dispatch/validation.ts +14 -0
- package/src/domains/dispatch/worker-spawn.ts +14 -3
- package/src/domains/eval/metrics/evidence.ts +0 -116
- package/src/domains/eval/metrics/invariants.ts +1 -1
- package/src/domains/eval/runners/clio-run.ts +1 -10
- package/src/domains/eval/runners/external-command.ts +2 -29
- package/src/domains/eval/schema/suite.ts +0 -7
- package/src/domains/eval/suites/run.ts +1 -7
- package/src/domains/memory/index.ts +22 -0
- package/src/domains/memory/operations.ts +58 -1
- package/src/domains/memory/promotion.ts +281 -0
- package/src/domains/memory/prompt-section.ts +25 -5
- package/src/domains/memory/proposal.ts +51 -7
- package/src/domains/memory/task-bank.ts +3 -2
- package/src/domains/memory/task-memory-handoff.ts +181 -24
- package/src/domains/memory/task-memory-policy.ts +3 -1
- package/src/domains/memory/types.ts +37 -0
- package/src/domains/memory/validate.ts +178 -0
- package/src/domains/middleware/memory-intervention.ts +35 -25
- package/src/domains/middleware/runtime.ts +6 -0
- package/src/domains/middleware/skills-reminder.ts +19 -4
- package/src/domains/middleware/stalled-turn.ts +43 -1
- package/src/domains/middleware/types.ts +10 -0
- package/src/domains/observability/contract.ts +6 -1
- package/src/domains/observability/cost.ts +20 -4
- package/src/domains/observability/extension.ts +2 -2
- package/src/domains/providers/index.ts +3 -0
- package/src/domains/providers/model-discovery.ts +9 -0
- package/src/domains/providers/runtime-resolution.ts +38 -1
- package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
- package/src/domains/providers/types/context-window-slots.ts +18 -0
- package/src/domains/providers/types/runtime-descriptor.ts +3 -1
- package/src/domains/safety/call-target.ts +211 -14
- package/src/domains/safety/decision-presentation.ts +268 -0
- package/src/domains/safety/redaction.ts +73 -0
- package/src/domains/session/context-ledger.ts +10 -1
- package/src/domains/session/decision-board.ts +4 -0
- package/src/domains/session/entries.ts +3 -0
- package/src/domains/session/history.ts +68 -19
- package/src/domains/session/usage.ts +24 -7
- package/src/engine/acp/event-mapper.ts +7 -0
- package/src/engine/acp/server.ts +29 -2
- package/src/engine/apis/lmstudio.ts +25 -4
- package/src/engine/apis/openai-completions.ts +147 -22
- package/src/engine/claude/sdk-runtime.ts +8 -2
- package/src/engine/claude/tool-safety.ts +13 -0
- package/src/engine/loop-guard.ts +27 -3
- package/src/engine/worker-events.ts +4 -3
- package/src/engine/worker-runtime.ts +59 -54
- package/src/entry/orchestrator.ts +18 -1
- package/src/interactive/chat-loop-messages.ts +22 -0
- package/src/interactive/chat-loop.ts +13 -0
- package/src/interactive/chat-renderer.ts +19 -3
- package/src/interactive/clio-editor.ts +44 -7
- package/src/interactive/context-overlay.ts +43 -5
- package/src/interactive/cost-overlay.ts +39 -8
- package/src/interactive/dispatch-board.ts +212 -35
- package/src/interactive/footer/widgets.ts +13 -0
- package/src/interactive/interactive-application.ts +6 -1
- package/src/interactive/interactive-input-runtime.ts +11 -1
- package/src/interactive/interactive-presentation.ts +11 -1
- package/src/interactive/memory-overlay.ts +89 -4
- package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
- package/src/interactive/overlay-frame.ts +5 -2
- package/src/interactive/overlay-general-openers.ts +40 -1
- package/src/interactive/overlay-key-routing.ts +41 -1
- package/src/interactive/overlay-lifecycle.ts +11 -4
- package/src/interactive/overlay-permission-lifecycle.ts +23 -8
- package/src/interactive/overlay-transitions.ts +11 -0
- package/src/interactive/overlays/ask-user.ts +74 -30
- package/src/interactive/overlays/decisions.ts +3 -1
- package/src/interactive/permission-hint.ts +35 -0
- package/src/interactive/permission-overlay.ts +95 -45
- package/src/interactive/renderers/tool-execution.ts +19 -49
- package/src/interactive/session-last-turn.ts +8 -1
- package/src/interactive/session-usage-reseed.ts +36 -10
- package/src/interactive/slash-commands.ts +2 -2
- package/src/interactive/status/summary.ts +5 -0
- package/src/interactive/status/types.ts +5 -0
- package/src/interactive/terminal-lease.ts +1 -0
- package/src/interactive/turn-context.ts +96 -23
- package/src/interactive/turn-middleware.ts +1 -0
- package/src/interactive/turn-runtime.ts +37 -8
- package/src/interactive/turn-state.ts +3 -0
- package/src/interactive/worker-progress.ts +440 -0
- package/src/interactive/worker-stream.ts +51 -110
- package/src/tools/agent-tools.ts +28 -3
- package/src/tools/ask-user.ts +21 -1
- package/src/tools/context/index.ts +2 -2
- package/src/tools/dispatch-arguments.ts +8 -0
- package/src/tools/dispatch-event-text.ts +19 -0
- package/src/tools/dispatch.ts +24 -1
- package/src/tools/monitor.ts +15 -0
- package/src/tools/registry.ts +15 -5
- package/src/tools/result-disposition.ts +156 -0
- package/src/tools/result-shaping.ts +59 -1
- package/src/tools/verify/authoring.ts +55 -54
- package/src/tools/worker-evidence.ts +19 -0
- package/src/worker/spec-contract.ts +43 -3
- package/dist/chunk-EFADSJET.js +0 -18
- package/dist/memory-4ALKDJ4Q.js +0 -246
- package/src/domains/eval/metrics/chaos-stream.ts +0 -93
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one projection of a worker's live progress.
|
|
3
|
+
*
|
|
4
|
+
* A dispatched run publishes its events on a single stream, and two surfaces
|
|
5
|
+
* read them: the transcript block a `/run` or `/delegate` opens, and the Fleet
|
|
6
|
+
* Runs board. Both used to interpret that stream themselves, so the same event
|
|
7
|
+
* could mean two things and the board could only manage a richer spinner while
|
|
8
|
+
* the transcript already held the answer. This module is the fold both now go
|
|
9
|
+
* through, so what an operator reads about a worker is one story told twice
|
|
10
|
+
* rather than two stories.
|
|
11
|
+
*
|
|
12
|
+
* Three rules it enforces rather than documents:
|
|
13
|
+
*
|
|
14
|
+
* - Arguments never arrive here. A tool's name comes from the `clio_*`
|
|
15
|
+
* telemetry events, and what the call is doing comes from the bounded
|
|
16
|
+
* {@link CallActionDescriptor} composed at the tool-admission seam. The
|
|
17
|
+
* `tool_execution_*` events, whose `args` carry the literal call arguments,
|
|
18
|
+
* are not read at all.
|
|
19
|
+
* - Reasoning content is never retained. A `thinking_delta` moves the phase
|
|
20
|
+
* and nothing else: the operator learns that the worker is thinking, never
|
|
21
|
+
* what it is thinking.
|
|
22
|
+
* - Everything is bounded before it is kept. Lines, bytes, the action trail,
|
|
23
|
+
* the distinct tool names, and the rate at which delta bytes are accepted
|
|
24
|
+
* all have explicit caps, so neither a long run nor a hostile one can grow
|
|
25
|
+
* what a render has to walk.
|
|
26
|
+
*
|
|
27
|
+
* Pure: no I/O, no bus, no clock of its own. `observe` takes the instant it
|
|
28
|
+
* should measure the rate window against, so every sequence below is testable.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { durableAssistantTextFromEvent, WORKER_OUTPUT_MAX_BYTES } from "../domains/dispatch/event-pump.js";
|
|
32
|
+
import type { CallActionDescriptor } from "../domains/safety/call-target.js";
|
|
33
|
+
import { truncateUtf8 } from "../tools/truncate-utf8.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lines of worker prose kept while a run is live. The tail is what a streaming
|
|
37
|
+
* answer is about, so the head is what gets dropped; once the run settles the
|
|
38
|
+
* body is replaced by the receipt-sealed answer and anchors at the head, which
|
|
39
|
+
* is the same rule the thinking block already follows.
|
|
40
|
+
*/
|
|
41
|
+
export const WORKER_LIVE_TAIL_LINES = 40;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bytes of live tail retained. The line bound alone leaves a worker that emits
|
|
45
|
+
* one enormous line unbounded, so the byte bound is the backstop: whole lines
|
|
46
|
+
* leave the head until the tail fits, and a single line larger than the bound
|
|
47
|
+
* is cut at its head.
|
|
48
|
+
*/
|
|
49
|
+
export const WORKER_LIVE_TAIL_MAX_BYTES = 4096;
|
|
50
|
+
|
|
51
|
+
/** Distinct tool names carried on an entry. The receipt's call count carries the total. */
|
|
52
|
+
export const WORKER_TOOL_NAME_LIMIT = 8;
|
|
53
|
+
|
|
54
|
+
/** Finished actions kept as the recent trail, newest first. */
|
|
55
|
+
export const WORKER_ACTION_TRAIL_LIMIT = 4;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The rate window for accepted delta bytes. A worker streaming faster than an
|
|
59
|
+
* operator can read is not producing more information, only more work for the
|
|
60
|
+
* fold, so bytes past the budget in one window are counted and dropped rather
|
|
61
|
+
* than concatenated into a tail that would immediately discard them anyway.
|
|
62
|
+
*/
|
|
63
|
+
export const WORKER_PROGRESS_WINDOW_MS = 250;
|
|
64
|
+
|
|
65
|
+
/** Delta bytes accepted per {@link WORKER_PROGRESS_WINDOW_MS}. */
|
|
66
|
+
export const WORKER_PROGRESS_WINDOW_BYTES = 16_384;
|
|
67
|
+
|
|
68
|
+
const TAIL_TRUNCATION_MARKER = "";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Where a worker is in its turn. Derived from the event stream and never from
|
|
72
|
+
* reasoning content: `thinking` says only that a reasoning block is open.
|
|
73
|
+
*/
|
|
74
|
+
export type WorkerProgressPhase = "starting" | "thinking" | "writing" | "tool" | "waiting" | "settled";
|
|
75
|
+
|
|
76
|
+
/** One tool call as an operator surface may show it: the name plus its redacted descriptor. */
|
|
77
|
+
export interface WorkerAction {
|
|
78
|
+
tool: string;
|
|
79
|
+
/** Correlation identity carried by new streams. Absent on legacy events. */
|
|
80
|
+
toolCallId?: string;
|
|
81
|
+
/** Absent when the runtime emitted no descriptor, so the name stands alone. */
|
|
82
|
+
descriptor?: CallActionDescriptor;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Everything both surfaces read about a live worker, bounded. Snapshots are
|
|
87
|
+
* immutable and `revision` changes only when a visible field does, so a
|
|
88
|
+
* renderer can skip a repaint by identity rather than by diffing text.
|
|
89
|
+
*/
|
|
90
|
+
export interface WorkerProgressSnapshot {
|
|
91
|
+
revision: number;
|
|
92
|
+
phase: WorkerProgressPhase;
|
|
93
|
+
/** Bounded worker prose: the live tail while running, the sealed answer once settled. */
|
|
94
|
+
tailText: string;
|
|
95
|
+
/** Lines the bounds dropped from `tailText`. */
|
|
96
|
+
droppedLines: number;
|
|
97
|
+
/** Bytes refused by the rate window or cut from an oversized line. */
|
|
98
|
+
droppedBytes: number;
|
|
99
|
+
/** The most recently started call still executing; null between calls and once settled. */
|
|
100
|
+
currentAction: WorkerAction | null;
|
|
101
|
+
/** Finished calls, newest first, bounded by {@link WORKER_ACTION_TRAIL_LIMIT}. */
|
|
102
|
+
recentActions: ReadonlyArray<WorkerAction>;
|
|
103
|
+
/** Distinct tool names in first-use order, bounded by {@link WORKER_TOOL_NAME_LIMIT}. */
|
|
104
|
+
toolNames: ReadonlyArray<string>;
|
|
105
|
+
settled: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WorkerProgressFold {
|
|
109
|
+
/**
|
|
110
|
+
* Fold one dispatch progress event. Returns true when the snapshot changed,
|
|
111
|
+
* so a caller can decide whether the surface needs anything at all.
|
|
112
|
+
*/
|
|
113
|
+
observe(event: unknown, nowMs?: number): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Seal the projection. `text` is the receipt's answer where one could be
|
|
116
|
+
* read; without it the live tail stands as the run's last honest word.
|
|
117
|
+
*/
|
|
118
|
+
settle(text?: string): boolean;
|
|
119
|
+
/**
|
|
120
|
+
* A later attempt of the same work takes over. The tail and the trail are
|
|
121
|
+
* history the operator is already reading and stay; the current call, the
|
|
122
|
+
* settled mark, and the previous attempt's durable answer go, because an
|
|
123
|
+
* answer belongs to the attempt that produced it.
|
|
124
|
+
*/
|
|
125
|
+
restart(): void;
|
|
126
|
+
snapshot(): WorkerProgressSnapshot;
|
|
127
|
+
/** The last durable assistant answer this run streamed, unbounded by the tail. */
|
|
128
|
+
durableText(): string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
132
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
136
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Incremental assistant prose carried by one worker event.
|
|
141
|
+
*
|
|
142
|
+
* Native workers slim `message_update` down to `assistantMessageEvent.delta`
|
|
143
|
+
* before it crosses the NDJSON seam (src/worker/event-projection.ts); the ACP
|
|
144
|
+
* mapper emits a top-level `text_delta` carrying `text`. Both spellings are
|
|
145
|
+
* read here so ACP peers need no separate UI path.
|
|
146
|
+
*/
|
|
147
|
+
function workerTextDelta(event: unknown): string {
|
|
148
|
+
if (!isRecord(event)) return "";
|
|
149
|
+
if (event.type === "message_update") {
|
|
150
|
+
const assistantEvent = isRecord(event.assistantMessageEvent) ? event.assistantMessageEvent : null;
|
|
151
|
+
if (assistantEvent?.type !== "text_delta") return "";
|
|
152
|
+
return typeof assistantEvent.delta === "string" ? assistantEvent.delta : "";
|
|
153
|
+
}
|
|
154
|
+
if (event.type !== "text_delta") return "";
|
|
155
|
+
if (typeof event.delta === "string") return event.delta;
|
|
156
|
+
return typeof event.text === "string" ? event.text : "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Whether this event opens or continues a reasoning block. Its content is never read. */
|
|
160
|
+
function isThinkingEvent(event: Record<string, unknown>): boolean {
|
|
161
|
+
if (event.type === "thinking_delta") return true;
|
|
162
|
+
if (event.type !== "message_update") return false;
|
|
163
|
+
const assistantEvent = isRecord(event.assistantMessageEvent) ? event.assistantMessageEvent : null;
|
|
164
|
+
return assistantEvent?.type === "thinking_delta";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The action a `clio_tool_start`/`clio_tool_finish` event names, or null when it is neither. */
|
|
168
|
+
function toolEventAction(event: Record<string, unknown>): WorkerAction | null {
|
|
169
|
+
if (event.type !== "clio_tool_start" && event.type !== "clio_tool_finish") return null;
|
|
170
|
+
const payload = isRecord(event.payload) ? event.payload : null;
|
|
171
|
+
const tool = nonEmptyString(payload?.tool);
|
|
172
|
+
if (tool === undefined) return null;
|
|
173
|
+
const toolCallId = nonEmptyString(payload?.toolCallId);
|
|
174
|
+
const raw = isRecord(payload?.action) ? (payload.action as Partial<CallActionDescriptor>) : undefined;
|
|
175
|
+
const verb = nonEmptyString(raw?.verb);
|
|
176
|
+
if (verb === undefined) return { tool, ...(toolCallId !== undefined ? { toolCallId } : {}) };
|
|
177
|
+
const object = nonEmptyString(raw?.object);
|
|
178
|
+
return {
|
|
179
|
+
tool,
|
|
180
|
+
...(toolCallId !== undefined ? { toolCallId } : {}),
|
|
181
|
+
descriptor: {
|
|
182
|
+
verb,
|
|
183
|
+
...(object !== undefined ? { object } : {}),
|
|
184
|
+
...(raw?.truncated === true ? { truncated: true } : {}),
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function lineCount(text: string): number {
|
|
190
|
+
if (text.length === 0) return 0;
|
|
191
|
+
let lines = 1;
|
|
192
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
193
|
+
if (text[index] === "\n") lines += 1;
|
|
194
|
+
}
|
|
195
|
+
return lines;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Keep the newest bytes of one oversized line, cutting on a UTF-8 boundary. */
|
|
199
|
+
function keepTailBytes(text: string, maxBytes: number): string {
|
|
200
|
+
const buffer = Buffer.from(text, "utf8");
|
|
201
|
+
if (buffer.byteLength <= maxBytes) return text;
|
|
202
|
+
let cut = buffer.byteLength - maxBytes;
|
|
203
|
+
while (cut < buffer.byteLength) {
|
|
204
|
+
const byte = buffer[cut];
|
|
205
|
+
if (byte === undefined || (byte & 0xc0) !== 0x80) break;
|
|
206
|
+
cut += 1;
|
|
207
|
+
}
|
|
208
|
+
return buffer.subarray(cut).toString("utf8");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Bound the live tail: newest lines first, then whole lines from the head until
|
|
213
|
+
* the byte cap is met, and finally a head cut on a single line larger than the
|
|
214
|
+
* cap on its own.
|
|
215
|
+
*/
|
|
216
|
+
function boundLiveTail(text: string): { text: string; droppedLines: number; droppedBytes: number } {
|
|
217
|
+
const lines = text.split("\n");
|
|
218
|
+
let droppedLines = 0;
|
|
219
|
+
if (lines.length > WORKER_LIVE_TAIL_LINES) {
|
|
220
|
+
droppedLines = lines.length - WORKER_LIVE_TAIL_LINES;
|
|
221
|
+
lines.splice(0, droppedLines);
|
|
222
|
+
}
|
|
223
|
+
while (lines.length > 1 && Buffer.byteLength(lines.join("\n"), "utf8") > WORKER_LIVE_TAIL_MAX_BYTES) {
|
|
224
|
+
lines.shift();
|
|
225
|
+
droppedLines += 1;
|
|
226
|
+
}
|
|
227
|
+
const joined = lines.join("\n");
|
|
228
|
+
const bytes = Buffer.byteLength(joined, "utf8");
|
|
229
|
+
if (bytes <= WORKER_LIVE_TAIL_MAX_BYTES) return { text: joined, droppedLines, droppedBytes: 0 };
|
|
230
|
+
const kept = keepTailBytes(joined, WORKER_LIVE_TAIL_MAX_BYTES);
|
|
231
|
+
return { text: kept, droppedLines, droppedBytes: bytes - Buffer.byteLength(kept, "utf8") };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Head-anchored byte bound for a settled answer, matching the receipt's own bound. */
|
|
235
|
+
export function boundSettledText(text: string): { text: string; dropped: number } {
|
|
236
|
+
const bounded = truncateUtf8(text, WORKER_OUTPUT_MAX_BYTES, TAIL_TRUNCATION_MARKER);
|
|
237
|
+
if (bounded === text) return { text, dropped: 0 };
|
|
238
|
+
return { text: bounded, dropped: Math.max(0, lineCount(text) - lineCount(bounded)) };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const EMPTY_SNAPSHOT: WorkerProgressSnapshot = {
|
|
242
|
+
revision: 0,
|
|
243
|
+
phase: "starting",
|
|
244
|
+
tailText: "",
|
|
245
|
+
droppedLines: 0,
|
|
246
|
+
droppedBytes: 0,
|
|
247
|
+
currentAction: null,
|
|
248
|
+
recentActions: [],
|
|
249
|
+
toolNames: [],
|
|
250
|
+
settled: false,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export function createWorkerProgressFold(): WorkerProgressFold {
|
|
254
|
+
let revision = 0;
|
|
255
|
+
let phase: WorkerProgressPhase = "starting";
|
|
256
|
+
let tailText = "";
|
|
257
|
+
let droppedLines = 0;
|
|
258
|
+
let droppedBytes = 0;
|
|
259
|
+
let currentAction: WorkerAction | null = null;
|
|
260
|
+
const recentActions: WorkerAction[] = [];
|
|
261
|
+
const toolNames: string[] = [];
|
|
262
|
+
let settled = false;
|
|
263
|
+
let durable = "";
|
|
264
|
+
/** Starts still awaiting a finish, oldest first. */
|
|
265
|
+
const pendingActions: WorkerAction[] = [];
|
|
266
|
+
/** New streams resolve exact starts through this index. */
|
|
267
|
+
const pendingActionsById = new Map<string, WorkerAction>();
|
|
268
|
+
let windowStartMs = 0;
|
|
269
|
+
let windowBytes = 0;
|
|
270
|
+
let cached: WorkerProgressSnapshot | null = EMPTY_SNAPSHOT;
|
|
271
|
+
|
|
272
|
+
const touch = (): true => {
|
|
273
|
+
revision += 1;
|
|
274
|
+
cached = null;
|
|
275
|
+
return true;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const setPhase = (next: WorkerProgressPhase): boolean => {
|
|
279
|
+
if (phase === next) return false;
|
|
280
|
+
phase = next;
|
|
281
|
+
return touch();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Admit delta bytes against the rate window. Returns the text to append,
|
|
286
|
+
* which is empty once the window's budget is spent; the refused bytes are
|
|
287
|
+
* counted so the surface can say the stream outran it rather than quietly
|
|
288
|
+
* showing less.
|
|
289
|
+
*/
|
|
290
|
+
const admitDelta = (delta: string, nowMs: number): string => {
|
|
291
|
+
if (nowMs - windowStartMs >= WORKER_PROGRESS_WINDOW_MS) {
|
|
292
|
+
windowStartMs = nowMs;
|
|
293
|
+
windowBytes = 0;
|
|
294
|
+
}
|
|
295
|
+
const bytes = Buffer.byteLength(delta, "utf8");
|
|
296
|
+
if (windowBytes + bytes <= WORKER_PROGRESS_WINDOW_BYTES) {
|
|
297
|
+
windowBytes += bytes;
|
|
298
|
+
return delta;
|
|
299
|
+
}
|
|
300
|
+
windowBytes = WORKER_PROGRESS_WINDOW_BYTES;
|
|
301
|
+
droppedBytes += bytes;
|
|
302
|
+
return "";
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const appendTail = (text: string): boolean => {
|
|
306
|
+
if (text.length === 0) return false;
|
|
307
|
+
const bounded = boundLiveTail(tailText + text);
|
|
308
|
+
tailText = bounded.text;
|
|
309
|
+
droppedLines += bounded.droppedLines;
|
|
310
|
+
droppedBytes += bounded.droppedBytes;
|
|
311
|
+
return touch();
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const noteToolName = (tool: string): boolean => {
|
|
315
|
+
if (toolNames.includes(tool) || toolNames.length >= WORKER_TOOL_NAME_LIMIT) return false;
|
|
316
|
+
toolNames.push(tool);
|
|
317
|
+
return touch();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const removePendingAction = (action: WorkerAction): void => {
|
|
321
|
+
const index = pendingActions.lastIndexOf(action);
|
|
322
|
+
if (index >= 0) pendingActions.splice(index, 1);
|
|
323
|
+
if (action.toolCallId !== undefined && pendingActionsById.get(action.toolCallId) === action) {
|
|
324
|
+
pendingActionsById.delete(action.toolCallId);
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const lastPendingActionForTool = (tool: string): WorkerAction | undefined => {
|
|
329
|
+
for (let index = pendingActions.length - 1; index >= 0; index -= 1) {
|
|
330
|
+
const candidate = pendingActions[index];
|
|
331
|
+
if (candidate?.tool === tool) return candidate;
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
observe(event: unknown, nowMs = Date.now()): boolean {
|
|
338
|
+
if (!isRecord(event) || settled) return false;
|
|
339
|
+
let changed = false;
|
|
340
|
+
|
|
341
|
+
const delta = workerTextDelta(event);
|
|
342
|
+
if (delta.length > 0) {
|
|
343
|
+
changed = appendTail(admitDelta(delta, nowMs)) || changed;
|
|
344
|
+
changed = setPhase("writing") || changed;
|
|
345
|
+
} else if (isThinkingEvent(event)) {
|
|
346
|
+
// The phase moves and nothing else: reasoning content is never kept.
|
|
347
|
+
changed = setPhase("thinking") || changed;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// A durable message_end is the answer as the receipt will seal it. An
|
|
351
|
+
// ACP peer that streamed no deltas reaches a surface only here.
|
|
352
|
+
const durableText = durableAssistantTextFromEvent(event);
|
|
353
|
+
if (durableText.trim().length > 0) {
|
|
354
|
+
durable = durableText;
|
|
355
|
+
if (delta.length === 0 && tailText.trim().length === 0) {
|
|
356
|
+
changed = appendTail(durableText) || changed;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const action = toolEventAction(event);
|
|
361
|
+
if (action !== null && event.type === "clio_tool_start") {
|
|
362
|
+
if (action.toolCallId !== undefined) {
|
|
363
|
+
const duplicate = pendingActionsById.get(action.toolCallId);
|
|
364
|
+
if (duplicate !== undefined) removePendingAction(duplicate);
|
|
365
|
+
pendingActionsById.set(action.toolCallId, action);
|
|
366
|
+
}
|
|
367
|
+
pendingActions.push(action);
|
|
368
|
+
currentAction = action;
|
|
369
|
+
noteToolName(action.tool);
|
|
370
|
+
touch();
|
|
371
|
+
setPhase("tool");
|
|
372
|
+
changed = true;
|
|
373
|
+
} else if (action !== null && event.type === "clio_tool_finish") {
|
|
374
|
+
// An id is authoritative when both sides carry it. If either side came
|
|
375
|
+
// from an older or incomplete producer, retain the historical name match.
|
|
376
|
+
const finished =
|
|
377
|
+
(action.toolCallId !== undefined ? pendingActionsById.get(action.toolCallId) : undefined) ??
|
|
378
|
+
lastPendingActionForTool(action.tool) ??
|
|
379
|
+
action;
|
|
380
|
+
removePendingAction(finished);
|
|
381
|
+
currentAction = pendingActions[pendingActions.length - 1] ?? null;
|
|
382
|
+
recentActions.unshift(finished);
|
|
383
|
+
if (recentActions.length > WORKER_ACTION_TRAIL_LIMIT) recentActions.length = WORKER_ACTION_TRAIL_LIMIT;
|
|
384
|
+
noteToolName(finished.tool);
|
|
385
|
+
touch();
|
|
386
|
+
setPhase(currentAction === null ? "waiting" : "tool");
|
|
387
|
+
changed = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (event.type === "agent_start") changed = setPhase("starting") || changed;
|
|
391
|
+
return changed;
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
settle(text?: string): boolean {
|
|
395
|
+
const sealed = nonEmptyString(text) ?? nonEmptyString(durable);
|
|
396
|
+
if (sealed !== undefined) {
|
|
397
|
+
const bounded = boundSettledText(sealed);
|
|
398
|
+
tailText = bounded.text;
|
|
399
|
+
droppedLines = bounded.dropped;
|
|
400
|
+
}
|
|
401
|
+
currentAction = null;
|
|
402
|
+
pendingActions.length = 0;
|
|
403
|
+
pendingActionsById.clear();
|
|
404
|
+
settled = true;
|
|
405
|
+
phase = "settled";
|
|
406
|
+
touch();
|
|
407
|
+
return true;
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
restart(): void {
|
|
411
|
+
currentAction = null;
|
|
412
|
+
pendingActions.length = 0;
|
|
413
|
+
pendingActionsById.clear();
|
|
414
|
+
durable = "";
|
|
415
|
+
settled = false;
|
|
416
|
+
phase = "starting";
|
|
417
|
+
touch();
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
snapshot(): WorkerProgressSnapshot {
|
|
421
|
+
if (cached !== null) return cached;
|
|
422
|
+
cached = {
|
|
423
|
+
revision,
|
|
424
|
+
phase,
|
|
425
|
+
tailText,
|
|
426
|
+
droppedLines,
|
|
427
|
+
droppedBytes,
|
|
428
|
+
currentAction: currentAction === null ? null : { ...currentAction },
|
|
429
|
+
recentActions: recentActions.map((entry) => ({ ...entry })),
|
|
430
|
+
toolNames: [...toolNames],
|
|
431
|
+
settled,
|
|
432
|
+
};
|
|
433
|
+
return cached;
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
durableText(): string {
|
|
437
|
+
return durable;
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* to a bus, or renders; the chat panel owns presentation and the wiring owns
|
|
9
9
|
* I/O, so every sequence below is testable as a pure fold.
|
|
10
10
|
*
|
|
11
|
+
* What a worker is saying and doing is not decided here. That is the canonical
|
|
12
|
+
* {@link WorkerProgressSnapshot} projection in `worker-progress.ts`, which the
|
|
13
|
+
* Fleet Runs board reads from the same events, so the two surfaces cannot drift
|
|
14
|
+
* on the answer tail, the tool names, or what a call is touching. This module
|
|
15
|
+
* owns the half the board has no use for: which assignment an event belongs to,
|
|
16
|
+
* which attempt is current, and what the receipt sealed.
|
|
17
|
+
*
|
|
11
18
|
* Three rules the fold enforces rather than documents:
|
|
12
19
|
*
|
|
13
20
|
* - Tool *names* only. Names come exclusively from the `clio_*` telemetry
|
|
@@ -34,10 +41,16 @@ import type {
|
|
|
34
41
|
DispatchStartedPayload,
|
|
35
42
|
RunAbortedPayload,
|
|
36
43
|
} from "../core/bus-events.js";
|
|
37
|
-
import { durableAssistantTextFromEvent, WORKER_OUTPUT_MAX_BYTES } from "../domains/dispatch/event-pump.js";
|
|
38
44
|
import type { RunKind } from "../domains/dispatch/types.js";
|
|
39
45
|
import type { WorkerRunOrigin, WorkerRunRuntime, WorkerRunRuntimeKind } from "../domains/session/index.js";
|
|
40
|
-
import {
|
|
46
|
+
import { createWorkerProgressFold, type WorkerProgressFold, type WorkerProgressSnapshot } from "./worker-progress.js";
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
boundSettledText,
|
|
50
|
+
WORKER_LIVE_TAIL_LINES,
|
|
51
|
+
WORKER_TOOL_NAME_LIMIT,
|
|
52
|
+
} from "./worker-progress.js";
|
|
53
|
+
export type { WorkerProgressSnapshot };
|
|
41
54
|
|
|
42
55
|
export interface WorkerAttempt {
|
|
43
56
|
runId: string;
|
|
@@ -95,6 +108,8 @@ export interface WorkerEntryState {
|
|
|
95
108
|
droppedLines: number;
|
|
96
109
|
/** Distinct tool names in first-use order, bounded. */
|
|
97
110
|
tools: string[];
|
|
111
|
+
/** Canonical live projection. Absent only on receipt-based session replay. */
|
|
112
|
+
progress?: WorkerProgressSnapshot;
|
|
98
113
|
/** Every attempt of this assignment, oldest first. */
|
|
99
114
|
attempts: WorkerAttempt[];
|
|
100
115
|
pending: boolean;
|
|
@@ -143,23 +158,6 @@ export interface WorkerStream {
|
|
|
143
158
|
reset(): void;
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
/**
|
|
147
|
-
* Lines of worker prose kept while a run is live. The tail is what a streaming
|
|
148
|
-
* answer is about, so the head is what gets dropped; once the run settles the
|
|
149
|
-
* body is replaced by the receipt-sealed answer and anchors at the head, which
|
|
150
|
-
* is the same rule the thinking block already follows.
|
|
151
|
-
*/
|
|
152
|
-
export const WORKER_LIVE_TAIL_LINES = 40;
|
|
153
|
-
|
|
154
|
-
/** Distinct tool names carried on an entry. The receipt's call count carries the total. */
|
|
155
|
-
export const WORKER_TOOL_NAME_LIMIT = 8;
|
|
156
|
-
|
|
157
|
-
const WORKER_TEXT_TRUNCATION_MARKER = "";
|
|
158
|
-
|
|
159
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
160
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
161
|
function nonEmptyString(value: unknown): string | undefined {
|
|
164
162
|
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
165
163
|
}
|
|
@@ -202,58 +200,6 @@ export function workerTargetLabel(runtime: WorkerRunRuntime): string {
|
|
|
202
200
|
return runtime.targetId ?? runtime.wireModelId ?? "unknown";
|
|
203
201
|
}
|
|
204
202
|
|
|
205
|
-
/**
|
|
206
|
-
* Incremental assistant prose carried by one worker event.
|
|
207
|
-
*
|
|
208
|
-
* Native workers slim `message_update` down to `assistantMessageEvent.delta`
|
|
209
|
-
* before it crosses the NDJSON seam (src/worker/event-projection.ts); the ACP
|
|
210
|
-
* mapper emits a top-level `text_delta` carrying `text`. Both spellings are
|
|
211
|
-
* read here so ACP peers need no separate UI path.
|
|
212
|
-
*/
|
|
213
|
-
function textDelta(event: unknown): string {
|
|
214
|
-
if (!isRecord(event)) return "";
|
|
215
|
-
if (event.type === "message_update") {
|
|
216
|
-
const assistantEvent = isRecord(event.assistantMessageEvent) ? event.assistantMessageEvent : null;
|
|
217
|
-
if (assistantEvent?.type !== "text_delta") return "";
|
|
218
|
-
return typeof assistantEvent.delta === "string" ? assistantEvent.delta : "";
|
|
219
|
-
}
|
|
220
|
-
if (event.type !== "text_delta") return "";
|
|
221
|
-
if (typeof event.delta === "string") return event.delta;
|
|
222
|
-
return typeof event.text === "string" ? event.text : "";
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Tool name from Clio's own telemetry. Never from `tool_execution_*`, whose args are the call's arguments. */
|
|
226
|
-
function toolName(event: unknown): string | undefined {
|
|
227
|
-
if (!isRecord(event)) return undefined;
|
|
228
|
-
if (event.type !== "clio_tool_start" && event.type !== "clio_tool_finish") return undefined;
|
|
229
|
-
const payload = isRecord(event.payload) ? event.payload : null;
|
|
230
|
-
return nonEmptyString(payload?.tool);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function lineCount(text: string): number {
|
|
234
|
-
if (text.length === 0) return 0;
|
|
235
|
-
let lines = 1;
|
|
236
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
237
|
-
if (text[index] === "\n") lines += 1;
|
|
238
|
-
}
|
|
239
|
-
return lines;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** Keep the newest `WORKER_LIVE_TAIL_LINES` lines; report what that cost. */
|
|
243
|
-
function boundLiveText(text: string): { text: string; dropped: number } {
|
|
244
|
-
const lines = text.split("\n");
|
|
245
|
-
if (lines.length <= WORKER_LIVE_TAIL_LINES) return { text, dropped: 0 };
|
|
246
|
-
const dropped = lines.length - WORKER_LIVE_TAIL_LINES;
|
|
247
|
-
return { text: lines.slice(dropped).join("\n"), dropped };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/** Head-anchored byte bound for a settled answer, matching the receipt's own bound. */
|
|
251
|
-
export function boundSettledText(text: string): { text: string; dropped: number } {
|
|
252
|
-
const bounded = truncateUtf8(text, WORKER_OUTPUT_MAX_BYTES, WORKER_TEXT_TRUNCATION_MARKER);
|
|
253
|
-
if (bounded === text) return { text, dropped: 0 };
|
|
254
|
-
return { text: bounded, dropped: Math.max(0, lineCount(text) - lineCount(bounded)) };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
203
|
/**
|
|
258
204
|
* Terminal facts a lifecycle payload can supply on its own. Used only when the
|
|
259
205
|
* sealed receipt is unreadable, so the block still reports an honest outcome
|
|
@@ -294,8 +240,21 @@ export function createWorkerStream(options: WorkerStreamOptions = {}): WorkerStr
|
|
|
294
240
|
const attemptByAssignment = new Map<string, number>();
|
|
295
241
|
/** Every attempt run id to its assignment, so progress and terminal events find their entry. */
|
|
296
242
|
const assignmentByRun = new Map<string, string>();
|
|
297
|
-
/**
|
|
298
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The canonical progress projection per assignment. One fold spans every
|
|
245
|
+
* attempt of an assignment, because a failover keeps streaming into the block
|
|
246
|
+
* the operator is already reading.
|
|
247
|
+
*/
|
|
248
|
+
const progressByAssignment = new Map<string, WorkerProgressFold>();
|
|
249
|
+
|
|
250
|
+
/** Copy the canonical projection onto the entry fields the block renders. */
|
|
251
|
+
const applyProgress = (entry: WorkerEntryState, fold: WorkerProgressFold): void => {
|
|
252
|
+
const snapshot = fold.snapshot();
|
|
253
|
+
entry.text = snapshot.tailText;
|
|
254
|
+
entry.droppedLines = snapshot.droppedLines;
|
|
255
|
+
entry.tools = [...snapshot.toolNames];
|
|
256
|
+
entry.progress = snapshot;
|
|
257
|
+
};
|
|
299
258
|
|
|
300
259
|
/** The entry a run id addresses, and only while that run is the entry's current attempt. */
|
|
301
260
|
const currentEntryForRun = (runId: unknown): WorkerEntryState | undefined => {
|
|
@@ -315,11 +274,10 @@ export function createWorkerStream(options: WorkerStreamOptions = {}): WorkerStr
|
|
|
315
274
|
// Terminal truth is the sealed answer. A run whose receipt could not be
|
|
316
275
|
// read keeps whatever durable text its own stream produced, and a run that
|
|
317
276
|
// produced none keeps the live tail rather than blanking the block.
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
entry
|
|
322
|
-
entry.droppedLines = bounded.dropped;
|
|
277
|
+
const fold = progressByAssignment.get(entry.assignmentId);
|
|
278
|
+
if (fold !== undefined) {
|
|
279
|
+
fold.settle(nonEmptyString(facts.text));
|
|
280
|
+
applyProgress(entry, fold);
|
|
323
281
|
}
|
|
324
282
|
return { kind: "updated", entry };
|
|
325
283
|
};
|
|
@@ -343,10 +301,16 @@ export function createWorkerStream(options: WorkerStreamOptions = {}): WorkerStr
|
|
|
343
301
|
delete existing.receipt;
|
|
344
302
|
existing.attempts.push({ runId, targetLabel: workerTargetLabel(runtime) });
|
|
345
303
|
assignmentByRun.set(runId, assignmentId);
|
|
304
|
+
const progress = progressByAssignment.get(assignmentId);
|
|
305
|
+
if (progress !== undefined) {
|
|
306
|
+
progress.restart();
|
|
307
|
+
applyProgress(existing, progress);
|
|
308
|
+
}
|
|
346
309
|
return { kind: "updated", entry: existing };
|
|
347
310
|
}
|
|
348
311
|
if (payload.requestOrigin !== "user" && payload.requestOrigin !== "agent") return null;
|
|
349
312
|
attemptByAssignment.set(assignmentId, payload.attempt);
|
|
313
|
+
const progress = createWorkerProgressFold();
|
|
350
314
|
const entry: WorkerEntryState = {
|
|
351
315
|
assignmentId,
|
|
352
316
|
runId,
|
|
@@ -356,49 +320,26 @@ export function createWorkerStream(options: WorkerStreamOptions = {}): WorkerStr
|
|
|
356
320
|
text: "",
|
|
357
321
|
droppedLines: 0,
|
|
358
322
|
tools: [],
|
|
323
|
+
progress: progress.snapshot(),
|
|
359
324
|
attempts: [{ runId, targetLabel: workerTargetLabel(runtime) }],
|
|
360
325
|
pending: true,
|
|
361
326
|
...(payload.parentToolCallId !== undefined ? { parentToolCallId: payload.parentToolCallId } : {}),
|
|
362
327
|
};
|
|
363
328
|
byAssignment.set(assignmentId, entry);
|
|
364
329
|
assignmentByRun.set(runId, assignmentId);
|
|
330
|
+
progressByAssignment.set(assignmentId, progress);
|
|
365
331
|
return { kind: "created", entry };
|
|
366
332
|
},
|
|
367
333
|
|
|
368
334
|
progress(payload): WorkerStreamChange | null {
|
|
369
335
|
const entry = currentEntryForRun(payload.runId);
|
|
370
336
|
if (entry === undefined || !entry.pending) return null;
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
entry.text = bounded.text;
|
|
378
|
-
entry.droppedLines += bounded.dropped;
|
|
379
|
-
changed = true;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// A durable message_end is the answer as the receipt will seal it. An
|
|
383
|
-
// ACP peer that streamed no deltas reaches the transcript only here.
|
|
384
|
-
const durable = durableAssistantTextFromEvent(event);
|
|
385
|
-
if (durable.trim().length > 0) {
|
|
386
|
-
durableTextByRun.set(entry.runId, durable);
|
|
387
|
-
if (delta.length === 0 && entry.text.trim().length === 0) {
|
|
388
|
-
const bounded = boundLiveText(durable);
|
|
389
|
-
entry.text = bounded.text;
|
|
390
|
-
entry.droppedLines += bounded.dropped;
|
|
391
|
-
}
|
|
392
|
-
changed = true;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const tool = toolName(event);
|
|
396
|
-
if (tool !== undefined && !entry.tools.includes(tool) && entry.tools.length < WORKER_TOOL_NAME_LIMIT) {
|
|
397
|
-
entry.tools.push(tool);
|
|
398
|
-
changed = true;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return changed ? { kind: "updated", entry } : null;
|
|
337
|
+
const fold = progressByAssignment.get(entry.assignmentId);
|
|
338
|
+
// Everything the block shows about the stream comes from the canonical
|
|
339
|
+
// projection, so an event that moved nothing visible repaints nothing.
|
|
340
|
+
if (fold === undefined || !fold.observe(payload.event)) return null;
|
|
341
|
+
applyProgress(entry, fold);
|
|
342
|
+
return { kind: "updated", entry };
|
|
402
343
|
},
|
|
403
344
|
|
|
404
345
|
completed(payload): WorkerStreamChange | null {
|
|
@@ -440,7 +381,7 @@ export function createWorkerStream(options: WorkerStreamOptions = {}): WorkerStr
|
|
|
440
381
|
byAssignment.clear();
|
|
441
382
|
attemptByAssignment.clear();
|
|
442
383
|
assignmentByRun.clear();
|
|
443
|
-
|
|
384
|
+
progressByAssignment.clear();
|
|
444
385
|
},
|
|
445
386
|
};
|
|
446
387
|
}
|