@sema-agent/core 5.60.1 → 5.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/dist/agents/subagent.d.ts +4 -2
- package/dist/agents/subagent.js +9 -9
- package/dist/brain/open-responses.js +8 -3
- package/dist/brain/openai.js +4 -4
- package/dist/brain/stream-engine.d.ts +13 -2
- package/dist/brain/stream-engine.js +3 -3
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +36 -4
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -2
- package/dist/core/hooks.d.ts +83 -4
- package/dist/core/hooks.js +3 -3
- package/dist/core/memory-engine/consolidation-driver.d.ts +19 -1
- package/dist/core/memory-engine/consolidation-driver.js +75 -3
- package/dist/core/memory-engine/consolidation.d.ts +52 -5
- package/dist/core/memory-engine/consolidation.js +3 -1
- package/dist/core/memory-engine/distiller.d.ts +89 -1
- package/dist/core/memory-engine/distiller.js +94 -5
- package/dist/core/memory-engine/engine.d.ts +8 -0
- package/dist/core/memory-engine/engine.js +51 -8
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/pricing.d.ts +24 -0
- package/dist/core/pricing.js +18 -0
- package/dist/core/runner/prepare-config-doors.d.ts +34 -0
- package/dist/core/runner/prepare-config-doors.js +55 -0
- package/dist/core/runner/prepare-task.d.ts +52 -10
- package/dist/core/runner/prepare-task.js +77 -42
- package/dist/core/runner/runtask.d.ts +7 -0
- package/dist/core/runner/runtask.js +254 -38
- package/dist/core/runner/turn-attachments.d.ts +137 -5
- package/dist/core/runner/turn-attachments.js +25 -2
- package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
- package/dist/core/task-notification.d.ts +50 -23
- package/dist/core/task-notification.js +20 -4
- package/dist/core/tool-errors.d.ts +2 -1
- package/dist/core/tool-policy.d.ts +27 -0
- package/dist/core/types.d.ts +214 -31
- package/dist/core/untrusted-text.d.ts +5 -4
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/usage-window-store.d.ts +109 -8
- package/dist/core/usage-window-store.js +79 -12
- package/dist/engine/harness/agent-harness.d.ts +58 -2
- package/dist/engine/harness/agent-harness.js +115 -5
- package/dist/engine/loop/agent-loop.js +153 -15
- package/dist/engine/loop/types.d.ts +32 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +9 -4
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow.d.ts +2 -2
- package/dist/prompt-assembly/event-registry.js +2 -0
- package/dist/server/http.d.ts +1 -1
- package/dist/stores/file/usage-window-store.d.ts +1 -1
- package/dist/stores/file/usage-window-store.js +27 -6
- package/dist/tools/loop-tick.js +1 -1
- package/dist/tools/monitor.d.ts +3 -3
- package/dist/tools/monitor.js +1 -1
- package/dist/tools/scheduler-tools.js +9 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,130 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.62.0 — 2026-08-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Usage-governance cost arm (#462, ruled form 1)**: `UsageWindow.maxCostUsd` joins `maxTokens` as an
|
|
7
|
+
independent second axis — either filling first exhausts the window; `freesAt`/`retryAfterMs` shared.
|
|
8
|
+
The charge seam (`UsageWindowStore.charge` / `chargeUsageRecord`) gains a trailing
|
|
9
|
+
`costMicroUsd?: number | null` three-state (number / `null` = spent-but-unpriceable / `undefined` =
|
|
10
|
+
store refuses on a $-governed window: `usage_window.store_cost_unanswered`); ledger rows and readings
|
|
11
|
+
carry `costMicroUsd`/`costUnknown` (a reading with unknown spend declares its money figure a LOWER
|
|
12
|
+
bound and the engine announces the gap once per run). Unpriced runs under a $-window refuse loudly:
|
|
13
|
+
`config.usage_window_unpriced` at the config door (the run's own model) and by name at the accounting
|
|
14
|
+
point (a mid-run degrade target); malformed pricing tables (NaN / negative / missing base rates)
|
|
15
|
+
refuse the same way. Token-arm behavior and ledger bytes are unchanged for every existing deployment.
|
|
16
|
+
- **Prose segment-end wire signal (#447)**: additive TaskEvent `text_end { content }` — emitted at the
|
|
17
|
+
brain's block-close boundary (CC's own segmentation point; CC ships each completed content block as
|
|
18
|
+
its own unit and has no idle heuristic). `content` is the segment's authoritative full text; absence
|
|
19
|
+
is honest (a brain that reports no block close ≠ the segment did not end). Subagent viewing panes
|
|
20
|
+
forward it opt-in. Downstream idle-flush heuristics can retire.
|
|
21
|
+
- **design/373 S3 — the userPromptSubmit entrance screens**: `UserPromptSubmitContext` gains
|
|
22
|
+
`source` (closed five-value union: objective / steer / resume_message / parked_redelivery, plus
|
|
23
|
+
`followUp` reserved), `inputId`, and `actor` (detached snapshot). The deployment hook now SCREENS
|
|
24
|
+
three entrances: live `TaskStream.steer` (chain position: domain → liveness → replay short-circuit →
|
|
25
|
+
screen → accept; replay identity minted from PRE-screen bytes), resume-message (pre-CAS at the verb —
|
|
26
|
+
a blocked message burns nothing, the row stays pending, the same token redeems once clear), and
|
|
27
|
+
parked-steer wake redelivery (blocks withhold THAT frame only, disclosed twice: the new notice
|
|
28
|
+
`steering.parked_input_blocked` + the entry's `human_input` account value `"blocked"`; siblings
|
|
29
|
+
deliver, a wake never wedges). Blocks refuse typed `steering.blocked_by_hook` (steer-verb error +
|
|
30
|
+
CheckpointError, one vocabulary, fail-closed on timeout/cancel/crash). Hook-less deployments are
|
|
31
|
+
byte-unchanged.
|
|
32
|
+
- **Preview-withheld determinant (#457②, CC 245 anchor)**: `AskRequest.previewWithheld`
|
|
33
|
+
(`"oversize"` / `"unavailable"`) distinguishes "the tool declares no preview" from "the projection
|
|
34
|
+
failed / was withheld" on the ask face, the park row, and `CheckpointSummary` — the CC-245 form where
|
|
35
|
+
an unreviewable edit narrows approval to one-time. `matchedAskRule` now joins the rule-offer
|
|
36
|
+
exclusion set (sync + park faces).
|
|
37
|
+
- **ScheduleWakeup `noop` seat (#455, CC 245 graduation)**: the schema and prompt carry CC's noop
|
|
38
|
+
contract verbatim; `noop` is REQUIRED unless `stop: true` (a loud one-round refusal, self-correcting —
|
|
39
|
+
the CC-245 form); the flag rides `tool_end.structured.noop` for hosts to fold streaks (core carries,
|
|
40
|
+
never counts). Dynamic loop-tick prompts grow the noop clause; cron templates do not.
|
|
41
|
+
- **Token-countdown lane (RB-318 ruled B)**: `TaskSpec.attachments.totalTokensReminder` (opt-in flag) +
|
|
42
|
+
`totalTokensReminderMode` (CC's five-mode closed set, default `padded-countdown`) render CC's
|
|
43
|
+
byte-exact `<total_tokens>N tokens left</total_tokens>` from CALLER-fed coordinates (the same
|
|
44
|
+
used/ceiling pair the budget gate enforces — core forwards, never estimates; no ceiling ⇒ countdown
|
|
45
|
+
arms stay silent). The padded arm carries CC's monotonic smoothing floor (the number never jumps
|
|
46
|
+
backwards). Bad modes refuse `config.attachment_invalid` at the config door.
|
|
47
|
+
|
|
48
|
+
### Narrowings (named)
|
|
49
|
+
- `<total_tokens>` joins the fenced + disclosed envelope tag tables: untrusted content carrying that
|
|
50
|
+
spelling is now zero-width-broken (fenced lane) and verbatim external data containing it triggers the
|
|
51
|
+
reminder disclosure footer — this applies to EVERY deployment, opted into the countdown lane or not
|
|
52
|
+
(the lane's own byte-identity claim is scoped to the attachment lane; the sanitize/disclosure tables
|
|
53
|
+
are unconditional defense). `attachmentEnvelopeTags` becomes a two-owner table (skills_listing owns
|
|
54
|
+
`skills`, total_tokens_reminder owns `total_tokens`) — pins asserting a single owner will red.
|
|
55
|
+
- `text_end` is honest about truncation: the openai / open-responses brains no longer mint a
|
|
56
|
+
`text_end` for a prose segment cut mid-stream (stall promotion, disconnect, in-band overload
|
|
57
|
+
degrade) — a truncated segment is not COMPLETE, so the frame is absent (the anthropic form); a
|
|
58
|
+
stream whose `finish_reason` already arrived still closes its segment. The frame's `content` now
|
|
59
|
+
carries the same trimmed text the final assistant message holds (one source).
|
|
60
|
+
- Pre-existing behavior restored on one seam: a fresh steer landing in the post-loop teardown window
|
|
61
|
+
now refuses `steering.not_running` (as before 373 S3) instead of `steering.blocked_by_hook`, and
|
|
62
|
+
the deployment hook is no longer invoked for an undeliverable input.
|
|
63
|
+
- Scheduler calls omitting `noop` (without `stop`) now refuse loudly (previously accepted silently).
|
|
64
|
+
- Closed-set adds, named: steering error family +`steering.blocked_by_hook` (also a CheckpointError
|
|
65
|
+
code); `ENGINE_NOTICE_CODES` +`steering.parked_input_blocked` (audience `user`);
|
|
66
|
+
`HumanInputDelivery` +`"blocked"` (3→4); `FROZEN_TASK_EVENT_TYPES` +`text_end`;
|
|
67
|
+
`steering_injected.source` +`"total_tokens_reminder"`. Downstream exhaustive switches and closed-set
|
|
68
|
+
pins over any of these will red — re-pin per the new members.
|
|
69
|
+
|
|
70
|
+
## 5.61.0 — 2026-08-25
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- **Injection-priority ladder is LIVE (design/373 S1+S2)**: `now`/`next`/`later` now carry three
|
|
74
|
+
distinct delivery semantics (CC 2.1.223 form). `next` = the running turn's next boundary (the
|
|
75
|
+
previous behavior of every frame — byte-identical for `next`); `later` = the run's
|
|
76
|
+
would-otherwise-stop seat (its own closing turn; non-natural endings re-pend per session, the next
|
|
77
|
+
run's turn-open delivers); `now` = `next`'s guarantee plus a manufactured boundary. A
|
|
78
|
+
caller-provenance `TaskStream.steer(text, { priority: "now" })` CUTS the in-flight turn at a
|
|
79
|
+
reconciled boundary (finished tools keep real results; never-started ones settle as paired
|
|
80
|
+
`[INTERRUPTED]` results with `details.errorKind: "interrupted_never_started"`; the CC-verbatim
|
|
81
|
+
interruption marker lands) and the run CONTINUES; every real cut emits the new
|
|
82
|
+
`task.turn_interrupted` notice (audience `user`) and counts toward `maxTurns`. Interrupt authority
|
|
83
|
+
is the steer face's alone.
|
|
84
|
+
- `TaskStream.steer` gains `options.priority` (default `"next"`); the replay identity (`inputId`)
|
|
85
|
+
gains the normalized tier as an axis (same id+text at a different tier ⇒ `steering.duplicate_input_id`).
|
|
86
|
+
- `task_notification` wire events gain an additive `priority` field (live + turn-open lanes; absent =
|
|
87
|
+
pre-373 producer or tier-unknown park — never read absence as `later`).
|
|
88
|
+
- `isTerminalTaskNotification` package-root export; engine-note backlog cap gains terminal preference
|
|
89
|
+
(#445): at a full cap an INTERNAL-lane terminal frame displaces the oldest event batch (which parks
|
|
90
|
+
losslessly) instead of parking behind it — external frames of every status stay ordinary
|
|
91
|
+
displaceable notes (trust cut at the marking site).
|
|
92
|
+
- Engine-layer seams: per-turn AbortController + `interruptTurn(frame)` (consumption guard; run
|
|
93
|
+
signal wins), pre-request immediate re-check, final commit point (a frame arriving during the
|
|
94
|
+
stop-gate await window is served by the same run), `AgentHarness.steer` returns the minted frame.
|
|
95
|
+
- **Product-grade origin folding behind a host-code attestation gate (design/376-C1)**:
|
|
96
|
+
`freezeConsolidationPlan` grows the attested arm — `proposal.mintExposure: "partitioned"` (closed
|
|
97
|
+
single-member set; non-members refuse structurally at the commit door) switches the fold from the
|
|
98
|
+
run-level single value to a per-product committed-exposure re-read over declared inputs (any marked
|
|
99
|
+
input ⇒ external/derived, all-clean ⇒ no origin); every UN-attested plan — every pre-C1 caller and
|
|
100
|
+
every foreign minter — keeps the run-level blanket byte-identical. `mintExposurePartitionedPlan`
|
|
101
|
+
+ `LlmConsolidationPlanArm` exported: the exposure-partitioned mint bisects candidates on the
|
|
102
|
+
engine-minted marked flag (clean arm byte-identical to the historical chain; marked arm mints only
|
|
103
|
+
at markedCount ≥ 2; a lone marked row stands as its handle row, withheld from every call); the
|
|
104
|
+
attestation threads verbatim distiller → driver → commit. D-1b two-layer world-movement guard:
|
|
105
|
+
the engine re-checks every served candidate on four axes (rev moved / left listing / challenge-latch
|
|
106
|
+
/ turned marked) and refuses the whole plan `_stale_snapshot`; the driver's cross-cycle roster guard
|
|
107
|
+
discards a stale mint cache and settles under the NEW stop reason `mint_invalidated`
|
|
108
|
+
(closed-set add; committed cycles stand add-only). Plan rows gain `markedProducts` (additive);
|
|
109
|
+
snapshot gate rows record `markedIds` (additive); the instruction-form gate grows the D-5 three-arm
|
|
110
|
+
form (attested all-clean products follow C-3's clean arm — default refusal + the
|
|
111
|
+
`allowInstructionProducts` hatch, a named WIDENING vs the pre-C1 marked-set blanket; a marked-input
|
|
112
|
+
product keeps the unconditional hard arm).
|
|
113
|
+
|
|
114
|
+
### BREAKING
|
|
115
|
+
- **B1** — `notify()` frames with `priority: "later"` (INCLUDING the parameterless default) moved
|
|
116
|
+
from next-boundary delivery to the run's closing seat / the next run's turn-open. Honestly noted:
|
|
117
|
+
in the flat era `later` was silently inert (no runtime signal said so) — the engine version is the
|
|
118
|
+
only discriminator. Callers needing boundary delivery must pass `priority: "next"` explicitly.
|
|
119
|
+
- **B2** — `notify(…, { priority: "now" })` is now a typed refusal (`notify.invalid_priority`);
|
|
120
|
+
previously it delivered with a one-per-run disclosure notice.
|
|
121
|
+
- **B3** — the `task.injection_priority_unimplemented` notice code is RETIRED (catalog + audience row
|
|
122
|
+
+ word table). Consumers must judge ladder support by version, never by the code's absence.
|
|
123
|
+
- Closed-set adds, named: `ENGINE_NOTICE_CODES` +`task.turn_interrupted`; notify error family
|
|
124
|
+
+`notify.invalid_priority`; consolidation run stop reasons +`mint_invalidated`. Internal producer
|
|
125
|
+
tiers re-seated: subagent terminal frames ×9 and monitor (both shapes) `later`→`next` (#445
|
|
126
|
+
ALIGNED), workflow terminals now explicit `next`.
|
|
127
|
+
|
|
3
128
|
## 5.60.1 — 2026-08-25
|
|
4
129
|
|
|
5
130
|
### Fixed
|
|
@@ -511,8 +511,10 @@ export interface SubagentToolOptions {
|
|
|
511
511
|
/**
|
|
512
512
|
* design/115 P3 — background sub-agents (CC `run_in_background`). When set, the tool exposes the
|
|
513
513
|
* `run_in_background` parameter: the call returns an `a*` task_id immediately, the child runs
|
|
514
|
-
* asynchronously, and completion fires ONE task-notification ("
|
|
515
|
-
*
|
|
514
|
+
* asynchronously, and completion fires ONE task-notification ("next" priority — design/373 §3.7 census
|
|
515
|
+
* re-seat, upstream-anchored: every background-task terminal is explicitly "next" there; the earlier
|
|
516
|
+
* "later doesn't derail active work" reading was the flat era's — a completion must reach a busy model
|
|
517
|
+
* at the boundary, not at the run's closing seat). Poll/stop via TaskOutput/TaskStop. Absent ⇒ synchronous only.
|
|
516
518
|
*/
|
|
517
519
|
background?: {
|
|
518
520
|
registry: import("../core/task-registry.js").TaskRegistry;
|
package/dist/agents/subagent.js
CHANGED
|
@@ -775,7 +775,7 @@ export function createSubagentResume(deps) {
|
|
|
775
775
|
}
|
|
776
776
|
else if (ownsTerminalFacesRevive) {
|
|
777
777
|
try {
|
|
778
|
-
deps.notify?.(resumeFrame, { priority: "
|
|
778
|
+
deps.notify?.(resumeFrame, { priority: "next" });
|
|
779
779
|
}
|
|
780
780
|
catch {
|
|
781
781
|
}
|
|
@@ -829,7 +829,7 @@ export function createSubagentResume(deps) {
|
|
|
829
829
|
...(abort.signal.aborted ? {} : { error: msgRevive.slice(0, REPORT_FIELD_MAX) }),
|
|
830
830
|
...(!abort.signal.aborted && errCodeReviveReject !== undefined ? { errorCode: errCodeReviveReject } : {}),
|
|
831
831
|
...(completionIdRejectRevive !== undefined ? { completionId: completionIdRejectRevive } : {}),
|
|
832
|
-
}, { priority: "
|
|
832
|
+
}, { priority: "next" });
|
|
833
833
|
}
|
|
834
834
|
catch {
|
|
835
835
|
}
|
|
@@ -862,7 +862,7 @@ function parkCompletionNotify(deps) {
|
|
|
862
862
|
...payload,
|
|
863
863
|
summary: `${payload.summary} (delivered after ${pendingCount} background ${pendingCount === 1 ? "child" : "children"} settled)`,
|
|
864
864
|
}
|
|
865
|
-
: payload, { priority: "
|
|
865
|
+
: payload, { priority: "next" });
|
|
866
866
|
}
|
|
867
867
|
catch {
|
|
868
868
|
}
|
|
@@ -2387,7 +2387,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2387
2387
|
sessionId: forkedId,
|
|
2388
2388
|
resumable: false,
|
|
2389
2389
|
...(completionIdReap !== undefined ? { completionId: completionIdReap } : {}),
|
|
2390
|
-
}, { priority: "
|
|
2390
|
+
}, { priority: "next" }), "subagent.reapTerminalNotify");
|
|
2391
2391
|
});
|
|
2392
2392
|
if (agentName !== undefined) {
|
|
2393
2393
|
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
@@ -2588,7 +2588,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2588
2588
|
resumable: resumableFork,
|
|
2589
2589
|
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
2590
2590
|
...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
|
|
2591
|
-
}, { priority: "
|
|
2591
|
+
}, { priority: "next" });
|
|
2592
2592
|
}
|
|
2593
2593
|
catch {
|
|
2594
2594
|
}
|
|
@@ -2639,7 +2639,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2639
2639
|
...(settledBg === "failed" ? { error: msgFork.slice(0, REPORT_FIELD_MAX) } : {}),
|
|
2640
2640
|
...(settledBg === "failed" && errCodeForkReject !== undefined ? { errorCode: errCodeForkReject } : {}),
|
|
2641
2641
|
...(completionIdForkReject !== undefined ? { completionId: completionIdForkReject } : {}),
|
|
2642
|
-
}, { priority: "
|
|
2642
|
+
}, { priority: "next" });
|
|
2643
2643
|
}
|
|
2644
2644
|
catch {
|
|
2645
2645
|
}
|
|
@@ -2967,7 +2967,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2967
2967
|
sessionId: bgChildSessionId,
|
|
2968
2968
|
resumable: false,
|
|
2969
2969
|
...(completionIdReap !== undefined ? { completionId: completionIdReap } : {}),
|
|
2970
|
-
}, { priority: "
|
|
2970
|
+
}, { priority: "next" }), "subagent.reapTerminalNotify");
|
|
2971
2971
|
});
|
|
2972
2972
|
const bgDurableProbe = bg.registry.durableAgentRowProbe(taskId);
|
|
2973
2973
|
const bgRowConfirmed = async () => (bgDurableProbe ? await bgDurableProbe().catch(() => false) : false);
|
|
@@ -3438,7 +3438,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3438
3438
|
}
|
|
3439
3439
|
else if (ownsTerminalFaces) {
|
|
3440
3440
|
try {
|
|
3441
|
-
notify?.(completionFrame, { priority: "
|
|
3441
|
+
notify?.(completionFrame, { priority: "next" });
|
|
3442
3442
|
}
|
|
3443
3443
|
catch {
|
|
3444
3444
|
}
|
|
@@ -3508,7 +3508,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3508
3508
|
...(settled === "failed" ? { error: msg.slice(0, REPORT_FIELD_MAX) } : {}),
|
|
3509
3509
|
...(settled === "failed" && errCodeBgReject !== undefined ? { errorCode: errCodeBgReject } : {}),
|
|
3510
3510
|
...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
|
|
3511
|
-
}, { priority: "
|
|
3511
|
+
}, { priority: "next" });
|
|
3512
3512
|
}
|
|
3513
3513
|
catch {
|
|
3514
3514
|
}
|
|
@@ -389,6 +389,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
};
|
|
392
|
+
const messageFace = (acc) => acc === degenItem && degenFace === "text" && rep.cut !== undefined ? trimDegenerateTail(acc.text, rep.cut) : acc.text;
|
|
392
393
|
const closeItem = (index, acc) => {
|
|
393
394
|
if (acc.closed)
|
|
394
395
|
return;
|
|
@@ -399,7 +400,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
399
400
|
}
|
|
400
401
|
else if (acc.kind === "message") {
|
|
401
402
|
if (acc.block)
|
|
402
|
-
out.push({ type: "text_end", contentIndex: index, content: acc
|
|
403
|
+
out.push({ type: "text_end", contentIndex: index, content: messageFace(acc), partial: { ...partial } });
|
|
403
404
|
}
|
|
404
405
|
else {
|
|
405
406
|
const tc = closeFunctionCall(acc);
|
|
@@ -598,7 +599,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
598
599
|
}
|
|
599
600
|
handleFrame(frame);
|
|
600
601
|
},
|
|
601
|
-
finalize() {
|
|
602
|
+
finalize(opts) {
|
|
602
603
|
for (const [index, acc] of [...items.entries()].sort((a, b) => a[0] - b[0])) {
|
|
603
604
|
if (acc.closed)
|
|
604
605
|
continue;
|
|
@@ -606,6 +607,10 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
606
607
|
acc.closed = true;
|
|
607
608
|
continue;
|
|
608
609
|
}
|
|
610
|
+
if (acc.kind === "message" && opts?.truncated === true) {
|
|
611
|
+
acc.closed = true;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
609
614
|
closeItem(index, acc);
|
|
610
615
|
}
|
|
611
616
|
const finalContent = [];
|
|
@@ -626,7 +631,7 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
626
631
|
});
|
|
627
632
|
}
|
|
628
633
|
else if (acc.kind === "message") {
|
|
629
|
-
const text =
|
|
634
|
+
const text = messageFace(acc);
|
|
630
635
|
if (text !== "") {
|
|
631
636
|
finalContent.push({ type: "text", text });
|
|
632
637
|
if (text.trim() !== "")
|
package/dist/brain/openai.js
CHANGED
|
@@ -481,15 +481,15 @@ export function createOpenAIBrain(config = {}) {
|
|
|
481
481
|
}
|
|
482
482
|
}
|
|
483
483
|
},
|
|
484
|
-
finalize() {
|
|
484
|
+
finalize(opts) {
|
|
485
485
|
closeThinking();
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
const textFace = degenerate && degenFace === "text" && rep.cut !== undefined ? trimDegenerateTail(accumText, rep.cut) : accumText;
|
|
487
|
+
if (emittedTextStart && (opts?.truncated !== true || finishReason != null)) {
|
|
488
|
+
out.push({ type: "text_end", contentIndex: textIndex, content: textFace, partial: { ...partial } });
|
|
488
489
|
}
|
|
489
490
|
const finalContent = [];
|
|
490
491
|
if (accumReasoning)
|
|
491
492
|
finalContent.push({ type: "thinking", thinking: accumReasoning });
|
|
492
|
-
const textFace = degenerate && degenFace === "text" && rep.cut !== undefined ? trimDegenerateTail(accumText, rep.cut) : accumText;
|
|
493
493
|
if (textFace)
|
|
494
494
|
finalContent.push({ type: "text", text: textFace });
|
|
495
495
|
const toolCalls = [];
|
|
@@ -159,8 +159,19 @@ export interface StreamControls {
|
|
|
159
159
|
export interface StreamParser {
|
|
160
160
|
/** Handle one raw SSE line (`data: {...}`); parse, emit events, call `ctrl.sawContentToken()` on content. */
|
|
161
161
|
onLine(line: string): void;
|
|
162
|
-
/**
|
|
163
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Build + emit the final `done`/`error` message after the stream ends.
|
|
164
|
+
*
|
|
165
|
+
* `truncated: true` = this finalize is a design/124 tier-A PROMOTION of a mid-stream failure
|
|
166
|
+
* (stall / connection cut / in-band overload downgrade): the provider never terminated the
|
|
167
|
+
* stream, so any still-open prose segment was CUT, not closed. A parser that mints segment-end
|
|
168
|
+
* frames (`text_end`) must withhold them on this arm — the TaskEvent contract promises the frame
|
|
169
|
+
* means "the model closed the block" (HONEST ABSENCE otherwise) — while still building the same
|
|
170
|
+
* final message (the facade stamps `partialFinalized` + the warning on the terminal event).
|
|
171
|
+
*/
|
|
172
|
+
finalize(opts?: {
|
|
173
|
+
truncated?: boolean;
|
|
174
|
+
}): void;
|
|
164
175
|
/**
|
|
165
176
|
* design/124 §0.5-2: introspection for the mid-stream failure tiering. Read directly off the
|
|
166
177
|
* parser's accumulated state (cheap, no side effects):
|
|
@@ -551,7 +551,7 @@ export function runStreamingBrain(args) {
|
|
|
551
551
|
start = nl + 1;
|
|
552
552
|
if (onLineOverloadTierA(line)) {
|
|
553
553
|
midstreamWarning = INBAND_OVERLOAD_WARNING;
|
|
554
|
-
parser.finalize();
|
|
554
|
+
parser.finalize({ truncated: true });
|
|
555
555
|
return;
|
|
556
556
|
}
|
|
557
557
|
}
|
|
@@ -576,7 +576,7 @@ export function runStreamingBrain(args) {
|
|
|
576
576
|
buffer += decoder.decode();
|
|
577
577
|
if (buffer.trim() && onLineOverloadTierA(buffer))
|
|
578
578
|
midstreamWarning = INBAND_OVERLOAD_WARNING;
|
|
579
|
-
parser.finalize();
|
|
579
|
+
parser.finalize(midstreamWarning !== undefined ? { truncated: true } : undefined);
|
|
580
580
|
return;
|
|
581
581
|
}
|
|
582
582
|
const snap = parser.snapshot();
|
|
@@ -585,7 +585,7 @@ export function runStreamingBrain(args) {
|
|
|
585
585
|
failure.kind === "stalled"
|
|
586
586
|
? "Response stalled mid-stream. The response above may be incomplete."
|
|
587
587
|
: "Connection closed mid-response. The response above may be incomplete.";
|
|
588
|
-
parser.finalize();
|
|
588
|
+
parser.finalize({ truncated: true });
|
|
589
589
|
return;
|
|
590
590
|
}
|
|
591
591
|
if (snap.hasCompletedToolCall)
|