@sema-agent/core 7.0.2 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/agents/repair-loop.d.ts +8 -7
- package/dist/agents/roster-store.d.ts +7 -2
- package/dist/brain/errors.d.ts +18 -0
- package/dist/brain/errors.js +3 -0
- package/dist/brain/stream-engine.js +6 -4
- package/dist/core/context-edit.d.ts +3 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +26 -6
- package/dist/core/hooks.js +8 -5
- package/dist/core/image-downsample.d.ts +4 -3
- package/dist/core/roles.d.ts +30 -8
- package/dist/core/roles.js +12 -8
- package/dist/core/runner/prepare-task.d.ts +2 -1
- package/dist/core/runner/prepare-task.js +75 -39
- package/dist/core/runner/runtask.d.ts +12 -3
- package/dist/core/runner/runtask.js +32 -4
- package/dist/core/safety-axis-vocab.d.ts +1 -1
- package/dist/core/strategy-store.d.ts +4 -1
- package/dist/core/task-registry-shared.d.ts +7 -3
- package/dist/core/tool-errors.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +40 -7
- package/dist/core/tool-policy.js +63 -9
- package/dist/core/types.d.ts +87 -9
- package/dist/engine/compaction/compaction.js +6 -2
- package/dist/engine/harness/agent-harness.d.ts +28 -6
- package/dist/engine/harness/agent-harness.js +34 -2
- package/dist/engine/harness/messages.js +4 -0
- package/dist/engine/harness/types.d.ts +37 -0
- package/dist/engine/harness/types.js +5 -0
- package/dist/engine/session/session.js +3 -2
- package/dist/internal/harness.d.ts +1 -0
- package/dist/internal/harness.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +17 -9
- package/dist/orchestration/run-workflow-tool.js +7 -2
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.1.0 — 2026-09-01
|
|
4
|
+
|
|
5
|
+
### BREAKING (timing) — design/384 slice 1 (#513): turn interrupts reach the ask waiting side
|
|
6
|
+
- **`resolveAsk` no longer waits for the approver to settle once the abort signal fires.** The onAsk await is raced against the composed `AbortSignal.any([run, call])`; the wait settles as the abort-family deny on its own. The prior guarantee "no return before onAsk settles" is REMOVED. Detached discipline: the still-pending approver promise is swallow-guarded; a late APPROVE becomes the `task.late_approval` engine notice; a late REJECT reaches `onError(phase:"hook")` — disclosed, never silent. This also closes the pre-existing run-abort deaf-approver exposure (a walltime/cancel end can no longer be held open by an approver ignoring its signal).
|
|
7
|
+
- **Signal thread (additive seats)**: the loop's turn-scoped signal rides the `tool_call` hook event as an optional `signal` seat; `ToolGateInput.callSignal` (optional) hands it to the three synchronous wait seats (`adjudicate`/`resolveAsk`/`resolveContentAsk`) as a new optional trailing argument — same-arity guarantee when absent (conditional invocation, no explicit trailing `undefined`).
|
|
8
|
+
- **`ENGINE_NOTICE_CODES` closed-set +2** (downstream full-set pins go red): `task.halt_unconsumed` (halt verb answered while the run's ending was already owned by its own abort — nothing cut, `haltedByUser` unsigned, this notice is the halt's only trace) and `task.late_approval` (the detached approver's late yes ran nothing; NON-CONSUMPTION semantics — the engine claims neither who decided nor which came first). Both audience `user`.
|
|
9
|
+
- **`task_aborted` widened by DEFINITION, zero closed-set additions**: the deny resolution now names the run's OR turn's abort signal; the deny message widens to `approval aborted for X (run or turn interrupted)` at all four family sites (probes pinning the old `(task ended)` bytes re-pinned).
|
|
10
|
+
- **Ledger narrowing (named)**: the humanReview gates ledger stops booking abort-family denies (`task_aborted` joins the `approverUnavailable` skip — interrupts are not denials; also covers the pre-existing run-abort booking distortion).
|
|
11
|
+
- **steer `priority:"now"` revokes a pending card and the run continues** (CC-aligned; same seat as `interruptTurn`, pinned e2e).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Terminal API failure now persists in the transcript** (#506㋐, CC 2.1.250 re-anchor): the harness mints a SEPARATE assistant record (reserved `<synthetic>` model sentinel + `isApiErrorMessage` + one bounded `API Error:` text block ≤2000 chars, `apiErrorStatus`/`requestId` twins, honest `"error"` stop + `usageMissing` — deliberately NOT copying CC's lying `stop_sequence`/zero-usage fields). `convertToLlm` drops every such record from every request view (the invalid-sequence class stays closed), with the exclusion carried at four further seats: auto-mode classifier window, prompt-suggestions grounding tail, compaction cut points, session derived-model tracking. ABORT arm untouched.
|
|
15
|
+
- **`BrainStatus.errorStatus`** (#506㋑): the HTTP status that named a retry's failure now rides the status channel in `api_retry` context (single `namesTheFailure` predicate governs both this seat and `apiErrorStatus`; 0-sentinel and 2xx never publish). WIRE EXTENSION — server/cli consumption named in the ship post.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **#512 model-vocabulary batch** (six items): run_workflow card stops teaching a tier-word claim false on tiers-configured deployments (real two-part gate = allowlist ∩ catalog); `parseModelMention` JSDoc recipe corrected to the EXPANDED table; four prototype-chain reads own-gated (live leak: `resolveModelDisplayLabel` returned Object.prototype functions for "toString" onto `agent_start.model`; `__proto__` writes now `Object.defineProperty` own data properties — a codex-caught setter regression); `suggestNextPrompts` bare catch now announces through `onError(phase:"suggestions")` on an explicit unresolvable `role`; roster/durable-registry `model` re-documented as the RECORD KEY; `agent_end.modelResolved` added to the WorkflowEvent union (additive — emitter always wrote it).
|
|
19
|
+
- **`scripts/verify-debts.mjs` two-evidence gate told the truth** (ledger-audit A-03): CLOSED rows now require two DISTINCT addressable coordinates; the old gate passed on a single commit OR post number.
|
|
20
|
+
|
|
21
|
+
### Docs
|
|
22
|
+
- Self-scan doc-rot batch (33 findings triaged, 13 confirmed + extensions): every rotted `file.ts:NNN` cross-reference in src re-spelled by symbol (line-anchor ratchet banks the cleanup); six orphaned JSDoc blocks reattached to their symbols (incl. the public `Runner` class and `expandTiers`); the stale `agent-harness` abort-clears-queues contract sentence corrected to the #389 truth; release-docs gate register drift fixed in seven places; REVIEW-BACKLOG stale status cells refreshed against the tree.
|
|
23
|
+
|
|
24
|
+
|
|
3
25
|
## 7.0.2 — 2026-08-30
|
|
4
26
|
|
|
5
27
|
### Added
|
|
@@ -13,7 +13,7 @@ import { type OracleIsolationVerdict } from "../core/oracle-isolation.js";
|
|
|
13
13
|
* `oracleIsolation` boundary that this slice deliberately does not build — SAFE-tier escalates to a human
|
|
14
14
|
* (`needs_human_oracle`) or surfaces a candidate, but never clears its own work.
|
|
15
15
|
*
|
|
16
|
-
* Like {@link verifyCompleted}, this is a **thin composition** over `runner.runTask` (verify.ts
|
|
16
|
+
* Like {@link verifyCompleted}, this is a **thin composition** over `runner.runTask` (verify.ts's DEPLOYMENT POSTURE section)
|
|
17
17
|
* — it adds no Runner-core surface, touches no vendored code, and is called by a leader/profile (the sibling
|
|
18
18
|
* of {@link runWithVerification}). The oracle internals (which gate/judge/property-harness to compose) live
|
|
19
19
|
* in the PROFILE-injected {@link RepairOracle} closure; core only fixes the {@link OracleResult} shape and the
|
|
@@ -75,12 +75,13 @@ export interface OracleResult {
|
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
77
|
* The PROFILE-injected oracle (§1 裁决①). The profile composes the actual grading inside this closure —
|
|
78
|
-
* `runExecGate(graderEnv, steps)` (exec-gate.ts
|
|
79
|
-
* grader) → `verifyCompleted` (verify.ts
|
|
80
|
-
* (property-harness.ts
|
|
78
|
+
* `runExecGate(graderEnv, steps)` (core/exec-gate.ts, env is the FIRST param = L2 provenance bound to the
|
|
79
|
+
* grader) → `verifyCompleted` (agents/verify.ts, the read-only L3 judge) → `checkInvariants`
|
|
80
|
+
* (core/property-harness.ts, explore-only) — and maps the composite to an {@link OracleResult}. Core never
|
|
81
81
|
* composes these internals (so the oracle type can't grow a long discriminated union); it only fixes the
|
|
82
82
|
* `OracleResult` shape and the read-only/identity contract. `graderEnv` is the isolated grader env; `evidence`
|
|
83
|
-
* is the diff/results to judge (recompute it from the post-resume working tree on a resume —
|
|
83
|
+
* is the diff/results to judge (recompute it from the post-resume working tree on a resume — threat BUG5, the
|
|
84
|
+
* same freshness duty verify.ts states on its own `evidence` config field).
|
|
84
85
|
*/
|
|
85
86
|
export type RepairOracle = (graderEnv: ExecutionEnv, evidence: string | undefined) => Promise<OracleResult>;
|
|
86
87
|
/**
|
|
@@ -166,12 +167,12 @@ export interface RepairLoopConfig {
|
|
|
166
167
|
*/
|
|
167
168
|
resumeBundle?: RepairBundle;
|
|
168
169
|
/**
|
|
169
|
-
* Stop the loop once cumulative cost reaches this (verify.ts
|
|
170
|
+
* Stop the loop once cumulative cost reaches this (verify.ts's `costCeilingMicroUsd` backstop). HRD-AGO-6 — "cumulative" is
|
|
170
171
|
* every leg the loop pays for: each attempt's own + nested `runTask` cost PLUS each verdict's
|
|
171
172
|
* {@link OracleResult.costMicroUsd} (an LLM-judge oracle is charged per attempt too).
|
|
172
173
|
*/
|
|
173
174
|
costCeilingMicroUsd?: number;
|
|
174
|
-
/** Overall ACTIVE wall-clock ceiling for the whole loop (verify.ts
|
|
175
|
+
/** Overall ACTIVE wall-clock ceiling for the whole loop (verify.ts's `totalTimeoutMs` backstop). F1: both ceilings
|
|
175
176
|
* are CROSS-RESUME — the carried {@link RepairBundle.spentMicroUsd}/{@link RepairBundle.activeElapsedMs}
|
|
176
177
|
* re-seed the accounts, so a durable resume continues the budget rather than restarting it. */
|
|
177
178
|
totalTimeoutMs?: number;
|
|
@@ -12,8 +12,13 @@ export interface RosterEntry {
|
|
|
12
12
|
* key; equals the spawner's session at depth 1). Stored verbatim; no predicate arm consumes it
|
|
13
13
|
* yet (enumeration/recovery is the reader). */
|
|
14
14
|
rootSessionId?: string;
|
|
15
|
-
/** design/151 S3b — the
|
|
16
|
-
*
|
|
15
|
+
/** design/151 S3b — the model RECORD KEY at spawn (revival lookup key; display/model-routing hints
|
|
16
|
+
* only, never a serialized spec). NOT the resolved model id: a spawn that named a model in WORDS
|
|
17
|
+
* records the caller's spelling VERBATIM (a catalog key, tier word or CC alias), because a revival
|
|
18
|
+
* re-resolves this value against the catalog in force at WAKE time and a resolved id would only
|
|
19
|
+
* ever resolve again on deployments whose catalog keys happen to equal model ids. A spawn that
|
|
20
|
+
* carried a Model OBJECT (definition/tool-level/inherited ref) records that object's id, which on
|
|
21
|
+
* such a deployment is not a catalog key and degrades softly on the revival lane. */
|
|
17
22
|
model?: string;
|
|
18
23
|
/** Ruled 2026-08-05: the spawn requested a model word that did not bind — the row runs on its
|
|
19
24
|
* inherited default. Closed set, single member today; absent = bound normally (or no word). */
|
package/dist/brain/errors.d.ts
CHANGED
|
@@ -82,6 +82,24 @@ export declare function readProviderRequestId(res: {
|
|
|
82
82
|
get?: (name: string) => string | null;
|
|
83
83
|
};
|
|
84
84
|
} | undefined): string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Does this HTTP status NAME the failure it is being attached to? The one predicate governing every
|
|
87
|
+
* seat on which this engine publishes a provider status — the terminal assistant frame's
|
|
88
|
+
* `apiErrorStatus` and the retry-wait status frame's `errorStatus` (#506 ㋑). One helper on purpose:
|
|
89
|
+
* both seats promise the same three-state discipline, and two copies of the rule would drift.
|
|
90
|
+
*
|
|
91
|
+
* Two distinct shapes are refused, and both are reachable:
|
|
92
|
+
* · The engine's "no status" SENTINEL, 0 — the terminal HTTP throw carries `r?.status ?? 0` when a
|
|
93
|
+
* transport hands back neither a response nor a throw. Publishing it would be absence rendered as a
|
|
94
|
+
* number a consumer may format, which is worse than silence.
|
|
95
|
+
* · A SUCCESS status. The connect loop only streams a response that is `ok` AND has a body, so an
|
|
96
|
+
* `ok` response with a NULL body (an empty 200 from a proxy, a 204) reaches the terminal throw
|
|
97
|
+
* carrying 200 — and a provider's own `x-should-retry` verdict can send that same 2xx into a RETRY
|
|
98
|
+
* wait. The boundary really did fail, but the status is not what failed, and a consumer routing or
|
|
99
|
+
* rendering by a 2xx is exactly the misreading these seats exist to prevent.
|
|
100
|
+
* Stalls, connect failures and in-band error frames inside a 200 leave the seat absent by the same rule.
|
|
101
|
+
*/
|
|
102
|
+
export declare function namesTheFailure(status: number | undefined): status is number;
|
|
85
103
|
/** Map an HTTP status to an error class. 401/403 = auth (don't retry); 429 = rate limit; 5xx = server. */
|
|
86
104
|
export declare function classifyHttp(status: number): BrainErrorCode;
|
|
87
105
|
/** Lift the machine-readable code back out of a `[code] …` prefixed `errorMessage` (the single shared
|
package/dist/brain/errors.js
CHANGED
|
@@ -45,6 +45,9 @@ export function readProviderRequestId(res) {
|
|
|
45
45
|
}
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
|
+
export function namesTheFailure(status) {
|
|
49
|
+
return status !== undefined && status >= 100 && !(status >= 200 && status < 300);
|
|
50
|
+
}
|
|
48
51
|
export function classifyHttp(status) {
|
|
49
52
|
if (status === 401 || status === 403)
|
|
50
53
|
return "auth";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
|
|
3
|
-
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, readProviderRequestId } from "./errors.js";
|
|
3
|
+
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
4
4
|
import { classifyInputTooLong } from "./input-too-long.js";
|
|
5
5
|
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
6
6
|
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
@@ -267,9 +267,8 @@ export function runStreamingBrain(args) {
|
|
|
267
267
|
errorMsg.inputTooLongRuledOut = true;
|
|
268
268
|
if (err.apiError === true) {
|
|
269
269
|
errorMsg.isApiErrorMessage = true;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
errorMsg.apiErrorStatus = st;
|
|
270
|
+
if (namesTheFailure(err.status))
|
|
271
|
+
errorMsg.apiErrorStatus = err.status;
|
|
273
272
|
if (requestIdSeen !== undefined)
|
|
274
273
|
errorMsg.requestId = requestIdSeen;
|
|
275
274
|
}
|
|
@@ -376,6 +375,7 @@ export function runStreamingBrain(args) {
|
|
|
376
375
|
attempt: attempt + 1,
|
|
377
376
|
maxRetries,
|
|
378
377
|
errClass: "output_cap",
|
|
378
|
+
...(namesTheFailure(r?.status) ? { errorStatus: r.status } : {}),
|
|
379
379
|
});
|
|
380
380
|
continue;
|
|
381
381
|
}
|
|
@@ -428,6 +428,7 @@ export function runStreamingBrain(args) {
|
|
|
428
428
|
discardResponseBody(r);
|
|
429
429
|
cc.abort();
|
|
430
430
|
cc.dispose();
|
|
431
|
+
const retryStatus = namesTheFailure(r?.status) ? r.status : undefined;
|
|
431
432
|
await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
|
|
432
433
|
phase: statusPhase,
|
|
433
434
|
detail: statusPhase === "rate_limited"
|
|
@@ -440,6 +441,7 @@ export function runStreamingBrain(args) {
|
|
|
440
441
|
attempt: attempt + 1,
|
|
441
442
|
maxRetries: laneMaxRetries,
|
|
442
443
|
errClass,
|
|
444
|
+
...(retryStatus !== undefined ? { errorStatus: retryStatus } : {}),
|
|
443
445
|
}));
|
|
444
446
|
continue;
|
|
445
447
|
}
|
|
@@ -257,6 +257,9 @@ export interface ContextEditOptions {
|
|
|
257
257
|
* append-only sessions cannot be rewritten. Dropping the junk from the REQUEST VIEW restores a legal
|
|
258
258
|
* transcript (TR3 rejoins its batch). Deterministic ⇒ byte-stable per request ⇒ prefix-cache safe.
|
|
259
259
|
* New sessions never contain these (the loop exits cleanly on abort; the harness skips persisting them).
|
|
260
|
+
* #506 ㋐ did NOT re-open this class: the API-failure record the harness now writes carries `API Error: …`
|
|
261
|
+
* TEXT, so it is not an empty failure assistant and this heal correctly leaves it alone. That record is
|
|
262
|
+
* excluded one layer later, at `convertToLlm`, by its own three-key identity (`isSyntheticApiErrorMessage`).
|
|
260
263
|
*/
|
|
261
264
|
export declare function dropEmptyFailureAssistants(messages: AgentMessage[]): AgentMessage[];
|
|
262
265
|
/**
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -124,6 +124,8 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
124
124
|
"task.user_followup_undrained",
|
|
125
125
|
"steering.parked_input_blocked",
|
|
126
126
|
"task.turn_interrupted",
|
|
127
|
+
"task.halt_unconsumed",
|
|
128
|
+
"task.late_approval",
|
|
127
129
|
"memory.capture_opted_out",
|
|
128
130
|
"memory.capture_optout_unpersisted",
|
|
129
131
|
"tool_result.offload_put_failed",
|
|
@@ -139,6 +141,8 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
139
141
|
"task.user_followup_undrained": "user",
|
|
140
142
|
"task.turn_interrupted": "user",
|
|
141
143
|
"steering.parked_input_blocked": "user",
|
|
144
|
+
"task.halt_unconsumed": "user",
|
|
145
|
+
"task.late_approval": "user",
|
|
142
146
|
"memory.capture_opted_out": "user",
|
|
143
147
|
"memory.capture_optout_unpersisted": "user",
|
|
144
148
|
"memory.consolidation_withheld": "user",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1169,7 +1169,10 @@ export declare function normalizePersistedRuleHit(hit: PersistedRuleAnswer): {
|
|
|
1169
1169
|
unreadable?: true;
|
|
1170
1170
|
coverage?: readonly import("./permission-rule-model.js").SegmentCoverage[];
|
|
1171
1171
|
};
|
|
1172
|
-
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
|
|
1172
|
+
/** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort
|
|
1173
|
+
* signal; when the caller also supplies {@link ToolGateInput.callSignal}, the Runner's closures
|
|
1174
|
+
* additionally bind their waits to that per-call signal (`AbortSignal.any` of the two), so a turn
|
|
1175
|
+
* interrupt releases a pending ask instead of leaving it hanging on the run's lifetime alone. */
|
|
1173
1176
|
export interface ToolGateInput {
|
|
1174
1177
|
/** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
|
|
1175
1178
|
* isolation scope (onHookError itself, the deny observer, the durable-park observer). The runner
|
|
@@ -1214,13 +1217,28 @@ export interface ToolGateInput {
|
|
|
1214
1217
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
1215
1218
|
* passed through unchanged; omitted when the deployment wired no execution environment. */
|
|
1216
1219
|
hookEnv?: HookEnvCapabilities;
|
|
1217
|
-
/**
|
|
1218
|
-
|
|
1220
|
+
/**
|
|
1221
|
+
* design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
|
|
1222
|
+
* own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
|
|
1223
|
+
* gate itself never awaits on it — it hands the signal verbatim to the three synchronous-wait
|
|
1224
|
+
* seats below (`adjudicate`, `resolveAsk`, `resolveContentAsk`) as their optional trailing
|
|
1225
|
+
* argument, and the Runner's closures compose it with the run signal so a turn interrupt (bare
|
|
1226
|
+
* halt / steer-now) releases the wait. Absent (a host driving the gate directly, or an unthreaded
|
|
1227
|
+
* emit) ⇒ every seat is invoked exactly as before — same arity, no trailing `undefined` (the
|
|
1228
|
+
* threading is a conditional invocation) — and waits stay bound to the run signal alone: the
|
|
1229
|
+
* historical shape, byte-identical.
|
|
1230
|
+
*/
|
|
1231
|
+
callSignal?: AbortSignal;
|
|
1232
|
+
/** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
|
|
1233
|
+
* The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
|
|
1234
|
+
* through verbatim; an implementation that ignores it keeps the run-level binding. */
|
|
1235
|
+
adjudicate?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<PermissionResult>;
|
|
1219
1236
|
/** Resolve an `ask` to allow/deny via `onAsk` (abort-bound). Required iff a decision can be `ask`.
|
|
1220
1237
|
* G1 three-value: the result may carry `approverUnavailable` — the live approver affirmed no
|
|
1221
1238
|
* operator is reachable for THIS ask; the gate then re-routes onto the durable park leg (below),
|
|
1222
|
-
* keeping the carried fail-closed deny when no park is armed.
|
|
1223
|
-
|
|
1239
|
+
* keeping the carried fail-closed deny when no park is armed. The optional trailing `callSignal`
|
|
1240
|
+
* (design/384) is {@link ToolGateInput.callSignal}, passed through verbatim. */
|
|
1241
|
+
resolveAsk: (decision: PermissionResult, req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ResolvedAsk>;
|
|
1224
1242
|
/**
|
|
1225
1243
|
* design/45: route a policy `ask` to a **durable suspension** instead of the synchronous `resolveAsk`
|
|
1226
1244
|
* (onAsk) path. Called in the `ask` branch with the FINAL post-hook args; if it returns a suspend
|
|
@@ -1309,8 +1327,10 @@ export interface ToolGateInput {
|
|
|
1309
1327
|
* from "nobody home" — the two have opposite re-routing rules). Passed UNCONDITIONALLY by the runner,
|
|
1310
1328
|
* including on runs with no live face: it then answers `unavailable` without contacting anything, which
|
|
1311
1329
|
* is exactly the pre-174 park/refusal behavior. Absent entirely ⇒ the pre-174 path verbatim.
|
|
1330
|
+
* The optional trailing `callSignal` (design/384) is {@link ToolGateInput.callSignal}, passed
|
|
1331
|
+
* through verbatim; an implementation that ignores it keeps the run-level binding.
|
|
1312
1332
|
*/
|
|
1313
|
-
resolveContentAsk?: (req: ToolCallRequest) => Promise<ContentAskOutcome>;
|
|
1333
|
+
resolveContentAsk?: (req: ToolCallRequest, callSignal?: AbortSignal) => Promise<ContentAskOutcome>;
|
|
1314
1334
|
/**
|
|
1315
1335
|
* design/70: the called tool is egress-marked (`ToolSpec.egress` — an external write: open a PR,
|
|
1316
1336
|
* push, send). The gate tightens a surviving `allow` to `ask` so an egress tool is NEVER
|
package/dist/core/hooks.js
CHANGED
|
@@ -426,6 +426,7 @@ export function persistedRuleMandateOf(marks) {
|
|
|
426
426
|
export async function runToolGate(input) {
|
|
427
427
|
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
428
428
|
const { toolCallId, toolName } = event;
|
|
429
|
+
const callSignal = input.callSignal;
|
|
429
430
|
const hookCtx = (seatSignal) => ({
|
|
430
431
|
toolCallId,
|
|
431
432
|
toolName,
|
|
@@ -506,7 +507,7 @@ export async function runToolGate(input) {
|
|
|
506
507
|
}
|
|
507
508
|
}
|
|
508
509
|
const req = { toolName, args: currentInput, toolCallId };
|
|
509
|
-
let decision = adjudicate ? await adjudicate(req) : { action: "allow" };
|
|
510
|
+
let decision = adjudicate ? await (callSignal !== undefined ? adjudicate(req, callSignal) : adjudicate(req)) : { action: "allow" };
|
|
510
511
|
let denySource = "policy";
|
|
511
512
|
let policyRewrite;
|
|
512
513
|
if ((decision.action === "allow" || decision.action === "ask") && decision.updatedInput !== undefined) {
|
|
@@ -867,7 +868,7 @@ export async function runToolGate(input) {
|
|
|
867
868
|
}
|
|
868
869
|
}
|
|
869
870
|
if (decision.action === "ask" && req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
870
|
-
const outcome = input.resolveContentAsk !== undefined ? await input.resolveContentAsk(req) : { kind: "unavailable", parkDeclined: false };
|
|
871
|
+
const outcome = input.resolveContentAsk !== undefined ? await (callSignal !== undefined ? input.resolveContentAsk(req, callSignal) : input.resolveContentAsk(req)) : { kind: "unavailable", parkDeclined: false };
|
|
871
872
|
if (outcome.kind === "answered") {
|
|
872
873
|
decision = { action: "allow", updatedInput: outcome.presentedInput };
|
|
873
874
|
}
|
|
@@ -902,7 +903,7 @@ export async function runToolGate(input) {
|
|
|
902
903
|
}
|
|
903
904
|
if (decision.action === "ask") {
|
|
904
905
|
const askBeforeResolve = decision;
|
|
905
|
-
const resolved = await resolveAsk(decision, req);
|
|
906
|
+
const resolved = await (callSignal !== undefined ? resolveAsk(decision, req, callSignal) : resolveAsk(decision, req));
|
|
906
907
|
if (resolved.action !== "ask" && resolved.approver !== undefined)
|
|
907
908
|
resolvedApprover = resolved.approver;
|
|
908
909
|
if (resolved.action === "deny" && isAskDenyResolution(resolved.resolution))
|
|
@@ -978,7 +979,7 @@ export async function runToolGate(input) {
|
|
|
978
979
|
let recheck = { action: "allow" };
|
|
979
980
|
if (adjudicate) {
|
|
980
981
|
try {
|
|
981
|
-
recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
|
|
982
|
+
recheck = await (callSignal !== undefined ? adjudicate({ toolName, args: editArgs, toolCallId }, callSignal) : adjudicate({ toolName, args: editArgs, toolCallId }));
|
|
982
983
|
}
|
|
983
984
|
catch (err) {
|
|
984
985
|
recheck = {
|
|
@@ -1014,7 +1015,9 @@ export async function runToolGate(input) {
|
|
|
1014
1015
|
editArgs = recheck.updatedInput;
|
|
1015
1016
|
editRewrittenSinceHuman = true;
|
|
1016
1017
|
}
|
|
1017
|
-
const
|
|
1018
|
+
const editAskDecision = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
1019
|
+
const editAskReq = { toolName, args: editArgs, toolCallId };
|
|
1020
|
+
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
1018
1021
|
resolvedApprover = rr.action !== "ask" ? rr.approver : undefined;
|
|
1019
1022
|
if (rr.action !== "allow") {
|
|
1020
1023
|
if (rr.action === "deny" && isAskDenyResolution(rr.resolution))
|
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
export declare const MCP_IMAGE_MAX_BASE64: number;
|
|
16
16
|
/**
|
|
17
17
|
* 批③ image-pipeline — CC 2.1.x `constants/apiLimits.ts` values, shared by the MCP inline-image bound
|
|
18
|
-
* AND the Read tool's image branch (tools/fs — internal consistency: ONE yardstick per limit).
|
|
19
|
-
* `tools/fs/
|
|
20
|
-
*
|
|
18
|
+
* AND the Read tool's image branch (tools/fs — internal consistency: ONE yardstick per limit). That other
|
|
19
|
+
* consumers are the file tools (`tools/fs/fs-read.ts`'s image branch, plus `fs-pdf`/`fs-bash`), which IMPORT
|
|
20
|
+
* {@link MCP_IMAGE_MAX_BASE64} / {@link IMAGE_TARGET_RAW_SIZE} from here (through core/mcp.js's re-export)
|
|
21
|
+
* rather than re-deriving them — one limit, every consumer reading it, no drift.
|
|
21
22
|
* - `IMAGE_TARGET_RAW_SIZE` (apiLimits.ts:29): raw-byte target that guarantees the base64 encoding stays
|
|
22
23
|
* under {@link MCP_IMAGE_MAX_BASE64} (raw × 4/3 = base64 → 3.75MB raw = 5MB base64).
|
|
23
24
|
* - `IMAGE_MAX_WIDTH/HEIGHT` (apiLimits.ts:42-43): client-side resize box. The API internally resizes
|
package/dist/core/roles.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export declare function resolveModel(ref: ModelRef, models?: Record<string, Mode
|
|
|
9
9
|
export declare const DEFAULT_TIER_ORDER: readonly string[];
|
|
10
10
|
/** CC 档位词 → sema 档位(内置固定,兼容层):fable/mythos→max · opus→pro · sonnet→flash · haiku→lite。 */
|
|
11
11
|
export declare const CC_MODEL_TIER_ALIASES: Readonly<Record<string, string>>;
|
|
12
|
+
/** the DISPLAY face of a string model ref: a CC tier ALIAS (haiku/sonnet/…) resolves to its
|
|
13
|
+
* sema tier name (lite/flash/…) for labels — the alias verbatim reads as a strong claim about a
|
|
14
|
+
* specific vendor's model while the actual routing may land anywhere ("haiku" showing over a
|
|
15
|
+
* DeepSeek run, an operator field report). A non-alias string is the deployment's own model key — its
|
|
16
|
+
* honest name — and passes through unchanged. Display only; routing (`expandTiers`) is untouched. */
|
|
17
|
+
export declare function resolveModelDisplayLabel(ref: string): string;
|
|
12
18
|
/**
|
|
13
19
|
* Expand a deployment's tier bindings into the model catalog: every tier name (and every CC alias whose
|
|
14
20
|
* tier resolves) becomes a catalog key pointing at the bound Model, so the ENTIRE resolve surface
|
|
@@ -29,12 +35,6 @@ export declare const CC_MODEL_TIER_ALIASES: Readonly<Record<string, string>>;
|
|
|
29
35
|
* `best` wins); with no bound known-order tier (or no tiers at all) the key stays absent and
|
|
30
36
|
* `resolveModel("best")` refuses like any unknown ref.
|
|
31
37
|
*/
|
|
32
|
-
/** the DISPLAY face of a string model ref: a CC tier ALIAS (haiku/sonnet/…) resolves to its
|
|
33
|
-
* sema tier name (lite/flash/…) for labels — the alias verbatim reads as a strong claim about a
|
|
34
|
-
* specific vendor's model while the actual routing may land anywhere ("haiku" showing over a
|
|
35
|
-
* DeepSeek run, an operator field report). A non-alias string is the deployment's own model key — its
|
|
36
|
-
* honest name — and passes through unchanged. Display only; routing (`expandTiers`) is untouched. */
|
|
37
|
-
export declare function resolveModelDisplayLabel(ref: string): string;
|
|
38
38
|
export declare function expandTiers(models: Record<string, Model> | undefined, tiers: Record<string, ModelRef> | undefined): Record<string, Model> | undefined;
|
|
39
39
|
export interface ModelMention {
|
|
40
40
|
/** The matched model name — always one of `allowedNames` — or undefined if none was found. */
|
|
@@ -49,8 +49,27 @@ export interface ModelMention {
|
|
|
49
49
|
* pick *which configured model* to use, but can never inject an arbitrary `baseUrl`/`apiKey`/Model
|
|
50
50
|
* (see design/11). Matches the first allow-listed mention; the rest of the text is returned cleaned.
|
|
51
51
|
*
|
|
52
|
-
*
|
|
52
|
+
* **Case-INSENSITIVE, deliberately** — and deliberately UNLIKE the catalog, whose lookups
|
|
53
|
+
* ({@link resolveModel}) are exact. The two answer different questions and the asymmetry is the
|
|
54
|
+
* design, not an oversight: a chat mention is a HUMAN-typed UX surface where `@Opus` and `@opus` are
|
|
55
|
+
* the same intent, so intake is wide; a catalog key is a machine identifier a deployment declared, so
|
|
56
|
+
* lookup is exact. The join is safe because the wide half never widens the narrow one: this returns
|
|
57
|
+
* the allowlist's OWN canonical spelling (never the user's casing), so the value handed to
|
|
58
|
+
* `resolveModel` is always a key the deployment wrote. The only visible consequence is that a catalog
|
|
59
|
+
* carrying two keys differing ONLY in case is addressed by whichever the allowlist iterates last —
|
|
60
|
+
* `@x` cannot name both, and there is no spelling that disambiguates them.
|
|
61
|
+
*
|
|
62
|
+
* Typical use: `const { model, cleanedText } = parseModelMention(userMsg, Object.keys(runner.agentCatalog.models ?? {}));`
|
|
53
63
|
* then `runTask({ objective: cleanedText, model })` (omit `model` to fall back to the default role).
|
|
64
|
+
*
|
|
65
|
+
* **Feed it the EXPANDED table.** The allowlist this takes is the host's own, and on a deployment that
|
|
66
|
+
* configured `RunnerDeps.tiers` the host's `deps.models` is NOT what the Runner resolves against: the
|
|
67
|
+
* constructor runs {@link expandTiers} once and keeps the expansion PRIVATE, so tier words and CC
|
|
68
|
+
* aliases (`pro`/`flash`/`opus`/`sonnet`/`best`/…) are catalog keys inside the Runner and absent from
|
|
69
|
+
* the host's table. Passing `Object.keys(deps.models)` therefore makes a user's `@opus` silently not
|
|
70
|
+
* match — the mention falls through as ordinary text and the task quietly runs on the default role.
|
|
71
|
+
* `runner.agentCatalog.models` is the expanded table (a per-read copy); a caller that has no Runner in
|
|
72
|
+
* hand can pre-expand its own with the exported {@link expandTiers}.
|
|
54
73
|
*/
|
|
55
74
|
export declare function parseModelMention(text: string, allowedNames: Iterable<string>): ModelMention;
|
|
56
75
|
/**
|
|
@@ -60,7 +79,10 @@ export declare function parseModelMention(text: string, allowedNames: Iterable<s
|
|
|
60
79
|
* - `summarize` → `flash`(压缩本该便宜;flash 未绑沿降档链到 lite);
|
|
61
80
|
* - `advisor` → 第一档(`ultra` 起,未绑降档到 max/pro——teacher 用最强的);
|
|
62
81
|
* - `verifier` → `pro`;
|
|
63
|
-
* - `
|
|
82
|
+
* - `classifier` → `flash`(design/143 批2b:auto-mode 权限分类腿是便宜档;flash 未绑沿降档链到 lite);
|
|
83
|
+
* - `subagent`/`team`/`synthesize` 无档位默认 = 走 FALLBACK 继承主模型(LLM 可经 model 参数自选档);
|
|
84
|
+
* - `consolidate` 同样无档位默认,但它的 FALLBACK 刻意只有 `summarize` 一站(design/376,见
|
|
85
|
+
* {@link FALLBACK} 的注):未配即响亮拒,不静默骑主模型。
|
|
64
86
|
* 未配 tiers 的部署:catalog 无 TIERS_ACTIVE 标记 → 表整体 INERT,行为与从前逐字节一致
|
|
65
87
|
* (即使 catalog 恰有裸模型名 `pro`/`flash` 也不入档位路由——INERT 靠配置成立,不靠键名运气)。
|
|
66
88
|
*/
|
package/dist/core/roles.js
CHANGED
|
@@ -18,7 +18,7 @@ export const CC_MODEL_TIER_ALIASES = {
|
|
|
18
18
|
haiku: "lite",
|
|
19
19
|
};
|
|
20
20
|
export function resolveModelDisplayLabel(ref) {
|
|
21
|
-
return CC_MODEL_TIER_ALIASES[ref]
|
|
21
|
+
return Object.hasOwn(CC_MODEL_TIER_ALIASES, ref) ? CC_MODEL_TIER_ALIASES[ref] : ref;
|
|
22
22
|
}
|
|
23
23
|
const TIERS_ACTIVE = Symbol.for("sema.tiersActive");
|
|
24
24
|
export function expandTiers(models, tiers) {
|
|
@@ -27,7 +27,7 @@ export function expandTiers(models, tiers) {
|
|
|
27
27
|
const out = { ...models };
|
|
28
28
|
out[TIERS_ACTIVE] = true;
|
|
29
29
|
const bound = (name) => {
|
|
30
|
-
const b = tiers[name];
|
|
30
|
+
const b = Object.hasOwn(tiers, name) ? tiers[name] : undefined;
|
|
31
31
|
return b === undefined ? undefined : resolveModel(b, models);
|
|
32
32
|
};
|
|
33
33
|
const resolveTier = (name) => {
|
|
@@ -44,26 +44,30 @@ export function expandTiers(models, tiers) {
|
|
|
44
44
|
}
|
|
45
45
|
return undefined;
|
|
46
46
|
};
|
|
47
|
+
const shadowed = (key) => Object.hasOwn(out, key) && Boolean(out[key]);
|
|
48
|
+
const put = (key, m) => {
|
|
49
|
+
Object.defineProperty(out, key, { value: m, writable: true, enumerable: true, configurable: true });
|
|
50
|
+
};
|
|
47
51
|
const tierNames = new Set([...DEFAULT_TIER_ORDER, ...Object.keys(tiers)]);
|
|
48
52
|
for (const name of tierNames) {
|
|
49
|
-
if (
|
|
53
|
+
if (shadowed(name))
|
|
50
54
|
continue;
|
|
51
55
|
const m = resolveTier(name);
|
|
52
56
|
if (m)
|
|
53
|
-
|
|
57
|
+
put(name, m);
|
|
54
58
|
}
|
|
55
59
|
for (const [alias, tier] of Object.entries(CC_MODEL_TIER_ALIASES)) {
|
|
56
|
-
if (
|
|
60
|
+
if (shadowed(alias))
|
|
57
61
|
continue;
|
|
58
62
|
const m = resolveTier(tier);
|
|
59
63
|
if (m)
|
|
60
|
-
|
|
64
|
+
put(alias, m);
|
|
61
65
|
}
|
|
62
|
-
if (!
|
|
66
|
+
if (!shadowed("best")) {
|
|
63
67
|
for (const name of DEFAULT_TIER_ORDER) {
|
|
64
68
|
const m = bound(name);
|
|
65
69
|
if (m) {
|
|
66
|
-
|
|
70
|
+
put("best", m);
|
|
67
71
|
break;
|
|
68
72
|
}
|
|
69
73
|
}
|
|
@@ -1121,7 +1121,8 @@ export interface PrepareResume {
|
|
|
1121
1121
|
* to restore — prepare rebuilds the per-task env via `deps.executionEnvFactory` then `resumeVM(snapshotId)`
|
|
1122
1122
|
* + `postResumeInit()` (instead of running on a fresh, empty env). Threaded HERE (not via `ResumeTaskConfig`)
|
|
1123
1123
|
* so the factory stays a deployment-level `RunnerDeps` capability and never pollutes `TaskSpec` — preserving
|
|
1124
|
-
* the "untrusted caller can't inject an env" red line (remote-env.
|
|
1124
|
+
* the "untrusted caller can't inject an env" red line ({@link import("../remote-env.js").ExecutionEnvFactory}'s
|
|
1125
|
+
* own contract states it: "Lives on `RunnerDeps` (deployment-level) — NOT on `TaskSpec`"; code-ready council round-2). */
|
|
1125
1126
|
workspaceHandle?: import("../remote-env.js").WorkspaceHandle;
|
|
1126
1127
|
/**
|
|
1127
1128
|
* design/174 — the call id of the CONTENT ask whose answer an operator's approval was spent on, when
|