@sema-agent/core 7.3.0 → 7.3.1
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 +14 -0
- package/dist/brain/status-sink.d.ts +10 -0
- package/dist/brain/status-sink.js +13 -4
- package/dist/brain/stream-engine.d.ts +11 -0
- package/dist/brain/stream-engine.js +39 -3
- package/dist/core/arg-summary.d.ts +13 -3
- package/dist/core/arg-summary.js +138 -7
- package/dist/core/runner/prepare-task.js +15 -0
- package/dist/core/runner/runtask.js +2 -0
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +44 -5
- package/dist/core/wiring-manifest.d.ts +41 -5
- package/dist/core/wiring-manifest.js +8 -0
- package/dist/engine/harness/agent-harness.d.ts +1 -0
- package/dist/engine/harness/agent-harness.js +3 -0
- package/dist/engine/harness/types.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +7 -0
- package/dist/engine/loop/agent-loop.js +79 -0
- package/dist/engine/loop/types.d.ts +42 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/tools/fs/fs-write.js +69 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.3.1 — 2026-09-03
|
|
4
|
+
|
|
5
|
+
### Changed (narrowing, named) — #531 Edit validates BEFORE it asks
|
|
6
|
+
- **`ToolSpec.validateInput?(args, ctx)`** — a pure input pre-validation hook the loop runs BEFORE the permission gate (CC parity: `validateInput` precedes the permission prompt; a refusal is a typed tool result, not a card). Edit / Write-on-existing / NotebookEdit implement it with the same `requireRead` + path checks the execute path keeps (double-checked; TOCTOU-safe). **Narrowing**: an Edit of a file the model never Read no longer raises an approval card — the model receives the read-first refusal directly (the auto classifier is not consulted); a Read-then-Edit still asks exactly as before. A throwing validator reads as "no verdict" (the call proceeds to the gate; one `onError` hook-phase disclosure).
|
|
7
|
+
|
|
8
|
+
### Added — #530 `waiting_first_token` status phase (closed set 6 → 7)
|
|
9
|
+
- `BrainStatusPhase` +`"waiting_first_token"`: with an armed first-token watchdog (`firstTokenTimeoutMs` > 30s), a `status` frame is emitted once the first token has been awaited 30s and every 30s after (`elapsedMs`, `timeoutMs`; no `retryAtMs`); cleared by the token, the watchdog or the call's end. An announced wait closes with the terminal frame (`recovered`, detail `first token arrived`) — never a retry's wording. Constants `WAITING_FIRST_TOKEN_AFTER_MS` / `WAITING_FIRST_TOKEN_EVERY_MS` exported. **Consumers pinning the six-member phase set must widen** (cli client-core retry-status closed set; server status projection is a pass-through).
|
|
10
|
+
|
|
11
|
+
### Added — #529 `WiringManifest.autoMode { armed, reason }`
|
|
12
|
+
- Minted on every leg (effective half; fingerprinted like `modelGate`): `reason ∈ armed | no_intent | no_face | denied | resolver_fault | latch_open` (`AUTO_MODE_ARM_REASONS`), `armed ⇔ reason === "armed"`. The per-leg READ face of the #521 arming decision.
|
|
13
|
+
|
|
14
|
+
### Fixed — #532 format characters inside / around a secret (family fix of A-087 root C)
|
|
15
|
+
- The format-free scanning view sinks into `runRedactionPasses`, so BOTH entries (`scrubSecrets`, `redactSecrets`) and every consumer (`primaryActivityArg` → `TaskResult.stats.humanReview.gates[].toolArg`, previews, error text) redact a key split by U+200B / U+00AD / U+FEFF as one marker; the raw scan and one suffix scan per format slot are unioned in so a format character used as the BOUNDARY before a key (`x<ZWSP>sk-…`) is still found; past 64 format slots the input is replaced whole (over-strip, never a leak). Format characters outside a secret are preserved; format-free input is byte-identical.
|
|
16
|
+
|
|
3
17
|
## 7.3.0 — 2026-09-03
|
|
4
18
|
|
|
5
19
|
### Added — design/385 slice 2a + 2b (cross-session peer messaging: mailbox contract, file backend, agents domain; +40 exports, 0 removed)
|
|
@@ -16,6 +16,16 @@ export declare function runWithStatusSink<T>(emit: (s: BrainStatus) => void, fn:
|
|
|
16
16
|
* call site for this exact reason and states the contract ("an advisory frame must never break a settled
|
|
17
17
|
* call"); the guard belongs HERE so every call site inherits it rather than each remembering.
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* #530 — bind the ACTIVE sink now, for an emit that will happen from a timer callback later. The
|
|
21
|
+
* async-local store propagates through Node's timers, but not through every timer implementation a
|
|
22
|
+
* process may run under (fake clocks in tests replace `setTimeout` outright and run callbacks from
|
|
23
|
+
* their own stack, where `getStore()` is empty). A frame scheduled inside the sink's scope must reach
|
|
24
|
+
* that sink whichever clock fires it, so the scheduler captures the store here and the callback emits
|
|
25
|
+
* through the bound function. Same swallow guard and the same retryAtMs stamp as {@link emitBrainStatus};
|
|
26
|
+
* a no-op function when no sink is active.
|
|
27
|
+
*/
|
|
28
|
+
export declare function bindBrainStatusEmitter(): (status: BrainStatus) => void;
|
|
19
29
|
export declare function emitBrainStatus(status: BrainStatus): void;
|
|
20
30
|
/**
|
|
21
31
|
* Silent-fallback telemetry (C1/C4/C5/C6) — the brain→runner TELEMETRY channel, a
|
|
@@ -3,16 +3,25 @@ const statusSinkStore = new AsyncLocalStorage();
|
|
|
3
3
|
export function runWithStatusSink(emit, fn) {
|
|
4
4
|
return statusSinkStore.run({ emit }, fn);
|
|
5
5
|
}
|
|
6
|
-
export function
|
|
6
|
+
export function bindBrainStatusEmitter() {
|
|
7
|
+
const sink = statusSinkStore.getStore();
|
|
8
|
+
if (sink === undefined)
|
|
9
|
+
return () => undefined;
|
|
10
|
+
return (status) => deliverBrainStatus(sink, status);
|
|
11
|
+
}
|
|
12
|
+
function deliverBrainStatus(sink, status) {
|
|
7
13
|
try {
|
|
8
|
-
const sink = statusSinkStore.getStore();
|
|
9
|
-
if (sink === undefined)
|
|
10
|
-
return;
|
|
11
14
|
sink.emit(withRetryDeadline(status));
|
|
12
15
|
}
|
|
13
16
|
catch {
|
|
14
17
|
}
|
|
15
18
|
}
|
|
19
|
+
export function emitBrainStatus(status) {
|
|
20
|
+
const sink = statusSinkStore.getStore();
|
|
21
|
+
if (sink === undefined)
|
|
22
|
+
return;
|
|
23
|
+
deliverBrainStatus(sink, status);
|
|
24
|
+
}
|
|
16
25
|
function withRetryDeadline(status) {
|
|
17
26
|
if (status.retryAtMs !== undefined)
|
|
18
27
|
return status;
|
|
@@ -89,6 +89,17 @@ export declare function sameRequestModuloCap(original: SSERequest, rebuilt: SSER
|
|
|
89
89
|
* Tolerates a `doFetch` (BYOM injection point) whose Response-shaped return has no usable `headers`.
|
|
90
90
|
*/
|
|
91
91
|
export declare function shouldRetryHeaderVerdict(res: Response | undefined): boolean | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* #530 — the first-token WAIT disclosure. While a first-token watchdog is armed
|
|
94
|
+
* (`firstTokenTimeoutMs > 0`) and no content token has arrived, the engine says so on the status
|
|
95
|
+
* channel: the FIRST `waiting_first_token` frame goes out once the wait has lasted
|
|
96
|
+
* {@link WAITING_FIRST_TOKEN_AFTER_MS}, and one more every {@link WAITING_FIRST_TOKEN_EVERY_MS} after
|
|
97
|
+
* that, until the first token lands, the watchdog fires, or the attempt ends. A short wait (under the
|
|
98
|
+
* threshold) announces nothing — the frame exists for the unattended "no answer for a minute" case,
|
|
99
|
+
* not for every request. The same 30s slice the retry countdown re-announces on.
|
|
100
|
+
*/
|
|
101
|
+
export declare const WAITING_FIRST_TOKEN_AFTER_MS = 30000;
|
|
102
|
+
export declare const WAITING_FIRST_TOKEN_EVERY_MS = 30000;
|
|
92
103
|
export interface SSERequest {
|
|
93
104
|
url: string;
|
|
94
105
|
headers: Record<string, string>;
|
|
@@ -3,7 +3,7 @@ import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } fr
|
|
|
3
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
|
-
import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
6
|
+
import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
7
7
|
import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
|
|
8
8
|
const DEFAULT_MAX_RETRIES = 10;
|
|
9
9
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
@@ -189,6 +189,8 @@ export function shouldRetryHeaderVerdict(res) {
|
|
|
189
189
|
return undefined;
|
|
190
190
|
}
|
|
191
191
|
const RETRY_STATUS_SLICE_MS = 30_000;
|
|
192
|
+
export const WAITING_FIRST_TOKEN_AFTER_MS = 30_000;
|
|
193
|
+
export const WAITING_FIRST_TOKEN_EVERY_MS = 30_000;
|
|
192
194
|
const THINKING_RETRY_BUDGET = 2;
|
|
193
195
|
const EMPTY_USAGE = {
|
|
194
196
|
input: 0,
|
|
@@ -249,6 +251,8 @@ export function runStreamingBrain(args) {
|
|
|
249
251
|
const out = createAssistantMessageEventStream();
|
|
250
252
|
let cleanup;
|
|
251
253
|
let announcedRetry = false;
|
|
254
|
+
let announcedWait = false;
|
|
255
|
+
let terminalWaitDetail = "first token arrived";
|
|
252
256
|
let terminalRetryPhase = "recovered";
|
|
253
257
|
let terminalRetryDetail = "recovered after retrying";
|
|
254
258
|
let requestIdSeen;
|
|
@@ -257,6 +261,7 @@ export function runStreamingBrain(args) {
|
|
|
257
261
|
const aborted = signal?.aborted === true || isAbortError(err);
|
|
258
262
|
terminalRetryPhase = "gave_up";
|
|
259
263
|
terminalRetryDetail = aborted ? "cancelled while retrying" : "retries exhausted";
|
|
264
|
+
terminalWaitDetail = aborted ? "cancelled while waiting for the first token" : "the first-token wait ended in failure";
|
|
260
265
|
const errorMsg = emptyAssistant(model);
|
|
261
266
|
errorMsg.stopReason = aborted ? "aborted" : "error";
|
|
262
267
|
errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -278,10 +283,10 @@ export function runStreamingBrain(args) {
|
|
|
278
283
|
})
|
|
279
284
|
.finally(() => {
|
|
280
285
|
cleanup?.();
|
|
281
|
-
if (!announcedRetry)
|
|
286
|
+
if (!announcedRetry && !announcedWait)
|
|
282
287
|
return;
|
|
283
288
|
try {
|
|
284
|
-
emitBrainStatus({ phase: terminalRetryPhase, detail: terminalRetryDetail });
|
|
289
|
+
emitBrainStatus({ phase: terminalRetryPhase, detail: announcedRetry ? terminalRetryDetail : terminalWaitDetail });
|
|
285
290
|
}
|
|
286
291
|
catch {
|
|
287
292
|
}
|
|
@@ -468,11 +473,39 @@ export function runStreamingBrain(args) {
|
|
|
468
473
|
let firstTokenSeen = false;
|
|
469
474
|
let firstTokenTimedOut = false;
|
|
470
475
|
let ftTimer;
|
|
476
|
+
let waitTimer;
|
|
477
|
+
const clearWaitTimer = () => {
|
|
478
|
+
if (waitTimer) {
|
|
479
|
+
clearTimeout(waitTimer);
|
|
480
|
+
waitTimer = undefined;
|
|
481
|
+
}
|
|
482
|
+
};
|
|
471
483
|
if (firstTokenTimeoutMs && firstTokenTimeoutMs > 0) {
|
|
472
484
|
ftTimer = setTimeout(() => {
|
|
473
485
|
firstTokenTimedOut = true;
|
|
486
|
+
clearWaitTimer();
|
|
474
487
|
void reader.cancel().catch(() => undefined);
|
|
475
488
|
}, firstTokenTimeoutMs);
|
|
489
|
+
if (firstTokenTimeoutMs > WAITING_FIRST_TOKEN_AFTER_MS) {
|
|
490
|
+
const waitStartedAt = Date.now();
|
|
491
|
+
const emitWaitFrame = bindBrainStatusEmitter();
|
|
492
|
+
const scheduleWaitFrame = (delayMs) => {
|
|
493
|
+
waitTimer = setTimeout(() => {
|
|
494
|
+
waitTimer = undefined;
|
|
495
|
+
if (firstTokenSeen || firstTokenTimedOut || Date.now() - waitStartedAt >= firstTokenTimeoutMs)
|
|
496
|
+
return;
|
|
497
|
+
announcedWait = true;
|
|
498
|
+
emitWaitFrame({
|
|
499
|
+
phase: "waiting_first_token",
|
|
500
|
+
detail: "waiting for the first token",
|
|
501
|
+
elapsedMs: Date.now() - waitStartedAt,
|
|
502
|
+
timeoutMs: firstTokenTimeoutMs,
|
|
503
|
+
});
|
|
504
|
+
scheduleWaitFrame(WAITING_FIRST_TOKEN_EVERY_MS);
|
|
505
|
+
}, delayMs);
|
|
506
|
+
};
|
|
507
|
+
scheduleWaitFrame(WAITING_FIRST_TOKEN_AFTER_MS);
|
|
508
|
+
}
|
|
476
509
|
}
|
|
477
510
|
let idleTimedOut = false;
|
|
478
511
|
let idleTimer;
|
|
@@ -497,12 +530,14 @@ export function runStreamingBrain(args) {
|
|
|
497
530
|
clearTimeout(ftTimer);
|
|
498
531
|
ftTimer = undefined;
|
|
499
532
|
}
|
|
533
|
+
clearWaitTimer();
|
|
500
534
|
};
|
|
501
535
|
cleanup = () => {
|
|
502
536
|
if (ftTimer) {
|
|
503
537
|
clearTimeout(ftTimer);
|
|
504
538
|
ftTimer = undefined;
|
|
505
539
|
}
|
|
540
|
+
clearWaitTimer();
|
|
506
541
|
if (idleTimer) {
|
|
507
542
|
clearTimeout(idleTimer);
|
|
508
543
|
idleTimer = undefined;
|
|
@@ -575,6 +610,7 @@ export function runStreamingBrain(args) {
|
|
|
575
610
|
clearTimeout(ftTimer);
|
|
576
611
|
ftTimer = undefined;
|
|
577
612
|
}
|
|
613
|
+
clearWaitTimer();
|
|
578
614
|
if (idleTimer) {
|
|
579
615
|
clearTimeout(idleTimer);
|
|
580
616
|
idleTimer = undefined;
|
|
@@ -71,9 +71,19 @@ export interface RedactionPass {
|
|
|
71
71
|
/** A literal replacement (only `$1` substitution is used by these passes) or a replacer fn. */
|
|
72
72
|
replace: string | ((match: string, ...groups: string[]) => string);
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
/**
|
|
75
|
+
* Run the declared passes in order, collecting findings (original-input spans) when a report is given.
|
|
76
|
+
*
|
|
77
|
+
* FORMAT CHARACTERS (#532): the passes scan a VIEW of the input with every `\p{Cf}` removed, so a
|
|
78
|
+
* credential split by a zero-width character is one token to the scanner, not two fragments of which
|
|
79
|
+
* only the first (or neither) matches. The OUTPUT keeps every format character that sits OUTSIDE a
|
|
80
|
+
* replaced span — a joiner in an emoji sequence, a non-joiner in Persian text, a soft hyphen in a word
|
|
81
|
+
* — and drops only those INSIDE the span the marker replaces (they were part of the secret). A
|
|
82
|
+
* format-free input takes the exact historical path (byte-identical output). When the replacements
|
|
83
|
+
* cannot be mapped back exactly (a later pass matched across an earlier pass's inserted text), the
|
|
84
|
+
* output is the redacted view itself — every format character dropped: the failure direction is
|
|
85
|
+
* over-stripping, never a leak.
|
|
86
|
+
*/
|
|
77
87
|
export declare function runRedactionPasses(input: string, passes: readonly RedactionPass[], report?: RedactionReport): string;
|
|
78
88
|
/** The credential-shape passes (the pre-channel SECRET_PATTERNS, now with declared kinds/confidence).
|
|
79
89
|
* Exported for {@link runRedactionPasses} composition (untrusted-egress chains its URL passes after
|
package/dist/core/arg-summary.js
CHANGED
|
@@ -25,10 +25,10 @@ function mapBackOnePass(edits, pos) {
|
|
|
25
25
|
return pos - delta;
|
|
26
26
|
}
|
|
27
27
|
const PREEXISTING_MARKER_RE = /\[redacted(?:-[a-z]+)?\]/g;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const FORMAT_CHAR_RE = /\p{Cf}/u;
|
|
29
|
+
const MAX_FORMAT_SLOT_SCANS = 64;
|
|
30
|
+
const REDACTED_WHOLE = "[redacted]";
|
|
31
|
+
function applyPasses(input, passes, report, toOriginal) {
|
|
32
32
|
const batches = [];
|
|
33
33
|
let cur = input;
|
|
34
34
|
for (const pass of passes) {
|
|
@@ -51,18 +51,149 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
51
51
|
report.findings.push({
|
|
52
52
|
kind: pass.kind,
|
|
53
53
|
confidence: pass.confidence,
|
|
54
|
-
span: [s0, e0],
|
|
54
|
+
span: [toOriginal(s0), toOriginal(e0)],
|
|
55
55
|
marker: pass.marker,
|
|
56
56
|
source: pass.source,
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length });
|
|
59
|
+
edits.push({ at: offset, removedLen: match.length, insertedLen: inserted.length, inserted });
|
|
60
60
|
}
|
|
61
61
|
return inserted;
|
|
62
62
|
});
|
|
63
63
|
batches.push(edits);
|
|
64
64
|
}
|
|
65
|
-
return cur;
|
|
65
|
+
return { out: cur, batches };
|
|
66
|
+
}
|
|
67
|
+
function replacedSpans(batches) {
|
|
68
|
+
const spans = [];
|
|
69
|
+
for (let j = 0; j < batches.length; j++) {
|
|
70
|
+
for (const e of batches[j]) {
|
|
71
|
+
let s0 = e.at;
|
|
72
|
+
let e0 = e.at + e.removedLen;
|
|
73
|
+
for (let i = j - 1; i >= 0; i--) {
|
|
74
|
+
s0 = mapBackOnePass(batches[i], s0);
|
|
75
|
+
e0 = mapBackOnePass(batches[i], e0);
|
|
76
|
+
}
|
|
77
|
+
if (e0 - s0 !== e.removedLen)
|
|
78
|
+
return undefined;
|
|
79
|
+
spans.push({ start: s0, end: e0, inserted: e.inserted, pass: j });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
spans.sort((a, b) => a.start - b.start);
|
|
83
|
+
for (let i = 1; i < spans.length; i++)
|
|
84
|
+
if (spans[i].start < spans[i - 1].end)
|
|
85
|
+
return undefined;
|
|
86
|
+
return spans;
|
|
87
|
+
}
|
|
88
|
+
export function runRedactionPasses(input, passes, report) {
|
|
89
|
+
if (report !== undefined && report.preexistingMarkers === undefined) {
|
|
90
|
+
report.preexistingMarkers = input.match(PREEXISTING_MARKER_RE)?.length ?? 0;
|
|
91
|
+
}
|
|
92
|
+
if (!FORMAT_CHAR_RE.test(input))
|
|
93
|
+
return applyPasses(input, passes, report, (pos) => pos).out;
|
|
94
|
+
const viewChars = [];
|
|
95
|
+
const viewToOriginal = [];
|
|
96
|
+
const formatAtSlot = [];
|
|
97
|
+
const charStartOffsets = [];
|
|
98
|
+
{
|
|
99
|
+
let orig = 0;
|
|
100
|
+
let viewOff = 0;
|
|
101
|
+
let slotBuf = "";
|
|
102
|
+
for (const ch of input) {
|
|
103
|
+
if (FORMAT_CHAR_RE.test(ch)) {
|
|
104
|
+
slotBuf += ch;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
formatAtSlot.push(slotBuf);
|
|
108
|
+
slotBuf = "";
|
|
109
|
+
viewToOriginal.push(orig);
|
|
110
|
+
charStartOffsets.push(viewOff);
|
|
111
|
+
viewChars.push(ch);
|
|
112
|
+
viewOff += ch.length;
|
|
113
|
+
}
|
|
114
|
+
orig += ch.length;
|
|
115
|
+
}
|
|
116
|
+
formatAtSlot.push(slotBuf);
|
|
117
|
+
viewToOriginal.push(input.length);
|
|
118
|
+
charStartOffsets.push(viewOff);
|
|
119
|
+
}
|
|
120
|
+
const view = viewChars.join("");
|
|
121
|
+
const offsetToCharIndex = new Map();
|
|
122
|
+
charStartOffsets.forEach((off, idx) => offsetToCharIndex.set(off, idx));
|
|
123
|
+
const toOriginal = (pos) => {
|
|
124
|
+
const idx = offsetToCharIndex.get(pos);
|
|
125
|
+
return idx === undefined ? input.length : viewToOriginal[idx];
|
|
126
|
+
};
|
|
127
|
+
const { out, batches } = applyPasses(view, passes, report, toOriginal);
|
|
128
|
+
const viewSpans = replacedSpans(batches);
|
|
129
|
+
if (viewSpans === undefined)
|
|
130
|
+
return out;
|
|
131
|
+
const spans = viewSpans.map((sp) => {
|
|
132
|
+
const startIdx = offsetToCharIndex.get(sp.start) ?? viewChars.length;
|
|
133
|
+
const endIdx = offsetToCharIndex.get(sp.end) ?? viewChars.length;
|
|
134
|
+
const lastIdx = endIdx - 1;
|
|
135
|
+
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
136
|
+
return { start: viewToOriginal[startIdx], end: endOrig, inserted: sp.inserted };
|
|
137
|
+
});
|
|
138
|
+
const viewOffsetToOriginal = (viewStart, viewEnd) => {
|
|
139
|
+
const startIdx = offsetToCharIndex.get(viewStart) ?? viewChars.length;
|
|
140
|
+
const endIdx = offsetToCharIndex.get(viewEnd) ?? viewChars.length;
|
|
141
|
+
const lastIdx = endIdx - 1;
|
|
142
|
+
const endOrig = lastIdx >= startIdx && lastIdx < viewChars.length ? viewToOriginal[lastIdx] + viewChars[lastIdx].length : viewToOriginal[startIdx];
|
|
143
|
+
return { start: viewToOriginal[startIdx], end: endOrig };
|
|
144
|
+
};
|
|
145
|
+
const cutOffsets = [];
|
|
146
|
+
for (let k = 1; k < viewChars.length; k++)
|
|
147
|
+
if (formatAtSlot[k] !== "")
|
|
148
|
+
cutOffsets.push(charStartOffsets[k]);
|
|
149
|
+
if (cutOffsets.length > MAX_FORMAT_SLOT_SCANS)
|
|
150
|
+
return REDACTED_WHOLE;
|
|
151
|
+
const extra = [];
|
|
152
|
+
const rawSpans = replacedSpans(applyPasses(input, passes, undefined, (pos) => pos).batches);
|
|
153
|
+
if (rawSpans !== undefined)
|
|
154
|
+
extra.push(...rawSpans);
|
|
155
|
+
for (const cut of cutOffsets) {
|
|
156
|
+
const cutSpans = replacedSpans(applyPasses(view.slice(cut), passes, undefined, (pos) => pos).batches);
|
|
157
|
+
if (cutSpans === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
for (const c of cutSpans) {
|
|
160
|
+
const o = viewOffsetToOriginal(c.start + cut, c.end + cut);
|
|
161
|
+
extra.push({ start: o.start, end: o.end, inserted: c.inserted, pass: c.pass });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
extra.sort((a, b) => a.start - b.start);
|
|
165
|
+
for (const r of extra) {
|
|
166
|
+
const overlapping = spans.find((v) => r.start < v.end && v.start < r.end);
|
|
167
|
+
if (overlapping !== undefined) {
|
|
168
|
+
if (r.start < overlapping.start)
|
|
169
|
+
overlapping.start = r.start;
|
|
170
|
+
if (r.end > overlapping.end)
|
|
171
|
+
overlapping.end = r.end;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
spans.push({ start: r.start, end: r.end, inserted: r.inserted });
|
|
175
|
+
if (report !== undefined) {
|
|
176
|
+
const pass = passes[r.pass];
|
|
177
|
+
report.findings.push({ kind: pass.kind, confidence: pass.confidence, span: [r.start, r.end], marker: pass.marker, source: pass.source });
|
|
178
|
+
}
|
|
179
|
+
spans.sort((a, b) => a.start - b.start);
|
|
180
|
+
}
|
|
181
|
+
for (let i = 1; i < spans.length;) {
|
|
182
|
+
if (spans[i].start < spans[i - 1].end) {
|
|
183
|
+
spans[i - 1].end = Math.max(spans[i - 1].end, spans[i].end);
|
|
184
|
+
spans.splice(i, 1);
|
|
185
|
+
}
|
|
186
|
+
else
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
let result = "";
|
|
190
|
+
let cursor = 0;
|
|
191
|
+
for (const sp of spans) {
|
|
192
|
+
result += input.slice(cursor, sp.start) + sp.inserted;
|
|
193
|
+
cursor = sp.end;
|
|
194
|
+
}
|
|
195
|
+
result += input.slice(cursor);
|
|
196
|
+
return result;
|
|
66
197
|
}
|
|
67
198
|
export const SECRET_PASSES = [
|
|
68
199
|
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
|
|
@@ -1210,6 +1210,18 @@ function autoModeLatchHealthy(d) {
|
|
|
1210
1210
|
return false;
|
|
1211
1211
|
}
|
|
1212
1212
|
}
|
|
1213
|
+
function autoModeArmReasonOf(intent, facePresent, capsAutoMode, capsFaulted) {
|
|
1214
|
+
if (!intent)
|
|
1215
|
+
return "no_intent";
|
|
1216
|
+
if (!facePresent)
|
|
1217
|
+
return "no_face";
|
|
1218
|
+
if (capsAutoMode === false)
|
|
1219
|
+
return capsFaulted ? "resolver_fault" : "denied";
|
|
1220
|
+
return "armed";
|
|
1221
|
+
}
|
|
1222
|
+
function validateInputFaultNotice(onError, sessionId) {
|
|
1223
|
+
return ({ toolName, toolCallId, error }) => onError?.(new Error(`tool input pre-validation (validateInput) threw for ${toolName} (call ${toolCallId}) — read as no verdict; the call proceeded to the gate: ${error instanceof Error ? error.message : String(error)}`), { phase: "hook", sessionId });
|
|
1224
|
+
}
|
|
1213
1225
|
function refuseInvalidAutoModeSeat(spec) {
|
|
1214
1226
|
if (spec.autoModeRequested !== undefined && spec.autoModeRequested !== true) {
|
|
1215
1227
|
const e = new Error(`autoModeRequested must be the literal true when present (got ${JSON.stringify(spec.autoModeRequested)}) — omit it for a non-auto task; a non-boolean or false value is refused rather than read as either polarity.`);
|
|
@@ -1977,6 +1989,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1977
1989
|
}
|
|
1978
1990
|
agentForkDenial = forkGovernanceDenial(spec.enableFork, runtimeCaps?.allowFork);
|
|
1979
1991
|
observersActive = runtimeCaps?.allowObservers === true;
|
|
1992
|
+
const autoModeArmReason = autoModeArmReasonOf(autoModeIntent, deps.autoMode !== undefined, runtimeCaps?.autoMode, runtimeCapsFaulted);
|
|
1980
1993
|
if (autoModeIntent && deps.autoMode !== undefined && runtimeCaps?.autoMode !== false) {
|
|
1981
1994
|
const am = deps.autoMode;
|
|
1982
1995
|
let classifierModel;
|
|
@@ -3614,6 +3627,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3614
3627
|
};
|
|
3615
3628
|
const harness = new AgentHarness({
|
|
3616
3629
|
abortResultDetails: () => parkContaminationMarker({ suspendRef, reviewRef }),
|
|
3630
|
+
onToolInputValidationFault: validateInputFaultNotice(deps.onError, sessionId),
|
|
3617
3631
|
...(spec.limits?.maxOutputTokens !== undefined && spec.limits.maxOutputTokens > 0
|
|
3618
3632
|
? { maxOutputTokens: spec.limits.maxOutputTokens }
|
|
3619
3633
|
: {}),
|
|
@@ -4400,6 +4414,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4400
4414
|
memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
|
|
4401
4415
|
retentionPolicyWired: deps.retentionPolicy !== undefined,
|
|
4402
4416
|
...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
|
|
4417
|
+
autoMode: { armed: autoModeArmReason === "armed", reason: autoModeArmReason },
|
|
4403
4418
|
});
|
|
4404
4419
|
peerLaneRefs.askEffective = wiringManifest.ask.effective;
|
|
4405
4420
|
if (peerLaneActive && internals?.onTaskNotification !== undefined) {
|
|
@@ -2872,6 +2872,8 @@ export class Runner {
|
|
|
2872
2872
|
...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
|
|
2873
2873
|
...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
|
|
2874
2874
|
...(s.retryAtMs !== undefined ? { retryAtMs: s.retryAtMs } : {}),
|
|
2875
|
+
...(s.elapsedMs !== undefined ? { elapsedMs: s.elapsedMs } : {}),
|
|
2876
|
+
...(s.timeoutMs !== undefined ? { timeoutMs: s.timeoutMs } : {}),
|
|
2875
2877
|
...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
|
|
2876
2878
|
...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
|
|
2877
2879
|
...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
|
package/dist/core/tools.js
CHANGED
|
@@ -55,6 +55,7 @@ export function defineTool(spec, options) {
|
|
|
55
55
|
...(spec.alwaysLoad !== undefined ? { alwaysLoad: spec.alwaysLoad } : {}),
|
|
56
56
|
...(spec.prepareArguments ? { prepareArguments: spec.prepareArguments } : {}),
|
|
57
57
|
...(spec.approvalPreview ? { approvalPreview: spec.approvalPreview } : {}),
|
|
58
|
+
...(spec.validateInput ? { validateInput: spec.validateInput } : {}),
|
|
58
59
|
execute: async (toolCallId, rawParams, signal) => {
|
|
59
60
|
const params = spec.prepareArguments ? spec.prepareArguments(rawParams) : rawParams;
|
|
60
61
|
if (!Value.Check(spec.parameters, params)) {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
|
-
import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
|
|
2
|
+
import type { AgentTool, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict } from "../internal/harness.js";
|
|
3
3
|
import type { ActorAssertion, CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
|
|
4
4
|
import type { TaskNotificationPayload } from "./task-notification.js";
|
|
5
5
|
/** A model reference: either a fully-formed Model, or a `"provider/id"` ref resolved via a catalog. */
|
|
@@ -241,6 +241,24 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
|
241
241
|
/** approval display projection (see AgentTool.approvalPreview): pure args→display value
|
|
242
242
|
* for human approval surfaces; clamped + throw-swallowed by the gate; never adjudication input. */
|
|
243
243
|
approvalPreview?: (args: unknown) => unknown;
|
|
244
|
+
/**
|
|
245
|
+
* OPTIONAL input pre-validation — runs BEFORE the permission ask (and before every other tool-call
|
|
246
|
+
* gate station: PreToolUse hooks, policy, the auto-mode classifier), AFTER schema validation. A
|
|
247
|
+
* refusal (`{ ok: false, message }`) is returned to the model as a typed error result and NO
|
|
248
|
+
* approval card is shown for the call; `{ ok: true }` / `undefined` lets the call proceed to the
|
|
249
|
+
* gate unchanged. Upstream form (CC 2.1.250 `validateInput` → `{result:false, message, errorCode}`,
|
|
250
|
+
* consulted ahead of the permission prompt): a call the tool would refuse on its own precondition —
|
|
251
|
+
* an Edit/Write/NotebookEdit whose target file was never Read this session — used to reach the
|
|
252
|
+
* operator as a card that could only ever fail; the model then retried the same failing call under
|
|
253
|
+
* auto mode until the turn cap. A validator MUST be pure (read tracking state, path grammar; no
|
|
254
|
+
* side effects, no writes, nothing the run observes) and MUST refuse with the SAME text its
|
|
255
|
+
* execution would have produced for that input, so the model sees one refusal either way. A
|
|
256
|
+
* validator that THROWS is read as "no verdict": the call proceeds to the gate and the fault is
|
|
257
|
+
* disclosed via `RunnerDeps.onError` (phase `"hook"`) — a broken validator must not refuse tools.
|
|
258
|
+
* The execution-time check stays in place (validate and execute are two reads; a direct
|
|
259
|
+
* `AgentTool.execute` caller — durable replay — never passes this seat).
|
|
260
|
+
*/
|
|
261
|
+
validateInput?: (args: unknown, ctx: ToolInputValidationContext) => Promise<ToolInputVerdict | undefined> | ToolInputVerdict | undefined;
|
|
244
262
|
/**
|
|
245
263
|
* [c209-C] Q4 — the model catalog names (`SubagentToolOptions.models` keys), filled by
|
|
246
264
|
* `createSubagentTool` alongside {@link agentListing}. Rendered as the agent_listing INITIAL frame's
|
|
@@ -3994,11 +4012,25 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
|
|
|
3994
4012
|
* RB-420-c — TERMINAL frames closing a retry sequence. Without them a consumer that rendered a
|
|
3995
4013
|
* `rate_limited`/`retrying`/`reconnecting` frame has nothing telling it the wait is over, so the
|
|
3996
4014
|
* warning row stays pinned until the next unrelated event. Exactly one terminal frame is emitted,
|
|
3997
|
-
* and only for a call that actually waited
|
|
3998
|
-
* `
|
|
4015
|
+
* and only for a call that actually waited (a retry wait, or a `waiting_first_token` trail —
|
|
4016
|
+
* #530): `recovered` = the call was served after the wait (a later attempt, or the first token
|
|
4017
|
+
* finally arriving), `gave_up` = the retry budget ran out (or the call was cancelled) and a
|
|
4018
|
+
* terminal error follows.
|
|
3999
4019
|
* Both mean the same thing to a renderer — stop showing the retry state.
|
|
4000
4020
|
*/
|
|
4001
|
-
| "recovered" | "gave_up"
|
|
4021
|
+
| "recovered" | "gave_up"
|
|
4022
|
+
/**
|
|
4023
|
+
* #530 — the first-token WAIT, before any failure: a first-token watchdog is armed
|
|
4024
|
+
* (`firstTokenTimeoutMs > 0`) and nothing has arrived yet. Emitted once the wait has lasted
|
|
4025
|
+
* `WAITING_FIRST_TOKEN_AFTER_MS` and re-emitted every `WAITING_FIRST_TOKEN_EVERY_MS` while it goes
|
|
4026
|
+
* on, carrying {@link BrainStatus.elapsedMs} and {@link BrainStatus.timeoutMs}; never carries a
|
|
4027
|
+
* retry countdown (`retryInMs`/`retryAtMs` — no wait is being announced, a wait is being
|
|
4028
|
+
* OBSERVED). CLOSED like a retry trail: once a wait frame went out, the call's terminal frame
|
|
4029
|
+
* follows — `recovered` when the call went on to be served (the first token arrived, on this
|
|
4030
|
+
* attempt or a later one), `gave_up` when it failed for good — so a renderer that pinned the row
|
|
4031
|
+
* has its "stop" signal. No frame at all when no watchdog is armed.
|
|
4032
|
+
*/
|
|
4033
|
+
| "waiting_first_token";
|
|
4002
4034
|
/**
|
|
4003
4035
|
* WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
|
|
4004
4036
|
* {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
|
|
@@ -4053,6 +4085,13 @@ export interface BrainStatus {
|
|
|
4053
4085
|
* the authoritative RELATIVE quantity, and this seat is the absolute convenience derived from it.
|
|
4054
4086
|
*/
|
|
4055
4087
|
retryAtMs?: number;
|
|
4088
|
+
/** #530 — on a `waiting_first_token` frame: how long the first token has been awaited, ms, at the
|
|
4089
|
+
* moment the frame is emitted (the emitting process's clock; approximate across a hop). Absent on
|
|
4090
|
+
* every other phase. */
|
|
4091
|
+
elapsedMs?: number;
|
|
4092
|
+
/** #530 — on a `waiting_first_token` frame: the armed first-token watchdog, ms — the wait's ceiling,
|
|
4093
|
+
* so a consumer can render "45s of 120s". Absent on every other phase. */
|
|
4094
|
+
timeoutMs?: number;
|
|
4056
4095
|
/** RB-420-c — 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
|
|
4057
4096
|
* same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
|
|
4058
4097
|
attempt?: number;
|
|
@@ -7542,4 +7581,4 @@ export interface RunnerDeps {
|
|
|
7542
7581
|
* 3, CC parity). The Runner tracks the consecutive-reuse counter per task across both compaction sites. */
|
|
7543
7582
|
maxConsecutiveProviderReuse?: number;
|
|
7544
7583
|
}
|
|
7545
|
-
export type { AgentTool, Model, StreamFn, ThinkingLevel };
|
|
7584
|
+
export type { AgentTool, Model, StreamFn, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict };
|
|
@@ -56,6 +56,28 @@ export type QuestionChannelState = "wired" | "absent" | "stripped_bg_lane";
|
|
|
56
56
|
export type SeamProvenance = "spec" | "deps";
|
|
57
57
|
/** Machine-readable reason codes for a park lane that is not (or not provably) effective. */
|
|
58
58
|
export type ParkLaneReason = "no_checkpoint_store" | "no_durable_approval_opt_in" | "no_force_durable_gate" | "no_armed_safety_vocabulary" | "await_runtime_caps" | "await_tool_vocabulary";
|
|
59
|
+
/**
|
|
60
|
+
* #529 — the closed set of auto-mode ARMING outcomes a leg's manifest reports
|
|
61
|
+
* ({@link WiringManifest.autoMode}). Minted from the arming predicate's own three-arm read, as the
|
|
62
|
+
* FIRST failing arm in predicate order:
|
|
63
|
+
* - `no_intent` — no `TaskSpec.autoModeRequested` seat and no intent inherited on the chain /
|
|
64
|
+
* recorded on the resumed checkpoint (a non-auto run; byte-identical arming).
|
|
65
|
+
* - `no_face` — intent, but no `RunnerDeps.autoMode` deployment face (the trust gate).
|
|
66
|
+
* - `denied` — intent + face, but the organization's deny bit (`RuntimeCaps.autoMode:false`)
|
|
67
|
+
* as the resolver STATED it.
|
|
68
|
+
* - `resolver_fault` — intent + face, and the deny bit was COINED by a resolver fault (a throw, or a
|
|
69
|
+
* marshalled/non-record result the value screen refused) — an outage, not a
|
|
70
|
+
* verdict; the same fail-closed outcome as `denied`, named apart so a consumer
|
|
71
|
+
* can tell "policy said no" from "the entitlement source was unreachable".
|
|
72
|
+
* - `armed` — all three arms held; the classifier decider was minted for this leg.
|
|
73
|
+
* - `latch_open` — the session's one-way "back to non-auto" breaker has tripped. A leg-START
|
|
74
|
+
* manifest never carries it (a decider is minted fresh at prepare and its latch
|
|
75
|
+
* is closed by construction; a tripped ancestor hands no intent forward, which
|
|
76
|
+
* reads as `no_intent`); it is in the vocabulary so a consumer switching on the
|
|
77
|
+
* closed set is complete for a mid-leg re-read face, which nothing mints today.
|
|
78
|
+
*/
|
|
79
|
+
export declare const AUTO_MODE_ARM_REASONS: readonly ["armed", "no_intent", "no_face", "denied", "resolver_fault", "latch_open"];
|
|
80
|
+
export type AutoModeArmReason = (typeof AUTO_MODE_ARM_REASONS)[number];
|
|
59
81
|
/**
|
|
60
82
|
* design/173 §2.2/§8.4 — the wiring manifest. Consumers MUST ignore unknown fields
|
|
61
83
|
* (`schemaVersion` bumps only on a semantic break of an EXISTING field). The TaskEvent face is the
|
|
@@ -190,6 +212,19 @@ export interface WiringManifest {
|
|
|
190
212
|
removed: readonly string[];
|
|
191
213
|
restore: string;
|
|
192
214
|
};
|
|
215
|
+
/**
|
|
216
|
+
* #529 — EFFECTIVE half only, and ALWAYS present on an engine-minted effective manifest (unlike
|
|
217
|
+
* `modelGate`, which is present only when a trim happened: auto mode has an answer on every leg,
|
|
218
|
+
* so absence here means an older mint or an external derivation, never "not applicable"). The
|
|
219
|
+
* per-leg READ face of the auto-mode arming decision: `armed` ⇔ `reason === "armed"` (the
|
|
220
|
+
* classifier decider was minted for this leg), and `reason` names the first arm that failed
|
|
221
|
+
* otherwise — see {@link AUTO_MODE_ARM_REASONS}. A serving layer that used to infer the mode from
|
|
222
|
+
* spec shape reads it here instead.
|
|
223
|
+
*/
|
|
224
|
+
autoMode?: {
|
|
225
|
+
armed: boolean;
|
|
226
|
+
reason: AutoModeArmReason;
|
|
227
|
+
};
|
|
193
228
|
/** EFFECTIVE half only — a short, non-sensitive fingerprint (sha256 prefix over the canonical
|
|
194
229
|
* JSON of this manifest's own resolved facts; every field here is an enum/boolean/count, no
|
|
195
230
|
* secrets) so an operator can correlate legs that ran under the same resolved assembly. */
|
|
@@ -252,6 +287,12 @@ export interface WiringFacts {
|
|
|
252
287
|
removed: readonly string[];
|
|
253
288
|
restore: string;
|
|
254
289
|
};
|
|
290
|
+
/** Effective half only — see {@link WiringManifest.autoMode}; the static half has no leg to arm.
|
|
291
|
+
* `armed` must agree with `reason` (`armed ⇔ reason === "armed"`); a contradicting pair is refused. */
|
|
292
|
+
autoMode?: {
|
|
293
|
+
armed: boolean;
|
|
294
|
+
reason: AutoModeArmReason;
|
|
295
|
+
};
|
|
255
296
|
}
|
|
256
297
|
/** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
|
|
257
298
|
* single-source shapes, no parallel hand-copied interface). */
|
|
@@ -313,11 +354,6 @@ export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean,
|
|
|
313
354
|
* else the headless degrade `auto_deny`.
|
|
314
355
|
*/
|
|
315
356
|
export declare function deriveAskEffective(form: AskSeamForm, parkEffective: boolean | "unresolved"): AskEffective;
|
|
316
|
-
/**
|
|
317
|
-
* design/173 §2.1 — the SINGLE derivation from facts to manifest, shared by both halves. The static
|
|
318
|
-
* half omits `ask.effective`, `leg` and the fingerprint; the effective half carries all three. Any
|
|
319
|
-
* second construction point for {@link WiringManifest} is a defect (single-mint rule).
|
|
320
|
-
*/
|
|
321
357
|
export declare function deriveWiringManifest(facts: WiringFacts): WiringManifest;
|
|
322
358
|
/** The one ask-seat resolution (`spec` wins over `deps`), shared with the effective half so the
|
|
323
359
|
* form/provenance read cannot fork. Junk values (a JS caller passing something outside the OnAsk
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { canonicalize } from "./canonical-json.js";
|
|
3
3
|
import { resolveCheckpointStore } from "./checkpoint-store.js";
|
|
4
4
|
import { isLiveQuestionFace } from "./ask-question.js";
|
|
5
|
+
export const AUTO_MODE_ARM_REASONS = ["armed", "no_intent", "no_face", "denied", "resolver_fault", "latch_open"];
|
|
5
6
|
export function resolveDeclaredDurability(store, storeName) {
|
|
6
7
|
const declared = store?.durability;
|
|
7
8
|
if (declared === undefined)
|
|
@@ -66,6 +67,12 @@ function deriveParkLane(facts) {
|
|
|
66
67
|
reasons.push("no_armed_safety_vocabulary");
|
|
67
68
|
return { capable: true, effective: unresolved ? "unresolved" : false, reasons, ...durability };
|
|
68
69
|
}
|
|
70
|
+
function readAutoModeFact(fact) {
|
|
71
|
+
if (!AUTO_MODE_ARM_REASONS.includes(fact.reason) || fact.armed !== (fact.reason === "armed")) {
|
|
72
|
+
throw new Error(`the auto-mode wiring fact is inconsistent (armed=${String(fact.armed)}, reason=${JSON.stringify(fact.reason)}) — armed must hold exactly when reason is "armed", and reason must be one of ${AUTO_MODE_ARM_REASONS.join("|")}`);
|
|
73
|
+
}
|
|
74
|
+
return { armed: fact.armed, reason: fact.reason };
|
|
75
|
+
}
|
|
69
76
|
export function deriveWiringManifest(facts) {
|
|
70
77
|
if (facts.half === "static" && facts.leg !== undefined) {
|
|
71
78
|
throw new Error("a static wiring manifest has no leg — leg identity is an effective-half fact");
|
|
@@ -118,6 +125,7 @@ export function deriveWiringManifest(facts) {
|
|
|
118
125
|
retention: facts.retentionPolicyWired,
|
|
119
126
|
},
|
|
120
127
|
...(facts.half === "effective" && facts.modelGate !== undefined ? { modelGate: { class: facts.modelGate.class, removed: [...facts.modelGate.removed], restore: facts.modelGate.restore } } : {}),
|
|
128
|
+
...(facts.half === "effective" && facts.autoMode !== undefined ? { autoMode: readAutoModeFact(facts.autoMode) } : {}),
|
|
121
129
|
};
|
|
122
130
|
if (facts.half === "effective") {
|
|
123
131
|
const { leg: _leg, ...assembly } = manifest;
|
|
@@ -203,6 +203,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
|
|
|
203
203
|
private stallTimeoutsPerCall?;
|
|
204
204
|
private abortResultDetails?;
|
|
205
205
|
private loopTrace?;
|
|
206
|
+
private onToolInputValidationFault?;
|
|
206
207
|
private resilience?;
|
|
207
208
|
private maxToolConcurrency?;
|
|
208
209
|
private streamingToolExecution?;
|
|
@@ -252,6 +252,7 @@ export class AgentHarness {
|
|
|
252
252
|
stallTimeoutsPerCall;
|
|
253
253
|
abortResultDetails;
|
|
254
254
|
loopTrace;
|
|
255
|
+
onToolInputValidationFault;
|
|
255
256
|
resilience;
|
|
256
257
|
maxToolConcurrency;
|
|
257
258
|
streamingToolExecution;
|
|
@@ -285,6 +286,7 @@ export class AgentHarness {
|
|
|
285
286
|
this.stallTimeoutsPerCall = options.stallTimeoutsPerCall;
|
|
286
287
|
this.abortResultDetails = options.abortResultDetails;
|
|
287
288
|
this.loopTrace = options.loopTrace;
|
|
289
|
+
this.onToolInputValidationFault = options.onToolInputValidationFault;
|
|
288
290
|
this.resilience = options.resilience;
|
|
289
291
|
this.maxToolConcurrency = options.maxToolConcurrency;
|
|
290
292
|
this.streamingToolExecution = options.streamingToolExecution;
|
|
@@ -577,6 +579,7 @@ export class AgentHarness {
|
|
|
577
579
|
...(this.maxOutputTokensPerCall !== undefined ? { maxTokensPerCall: this.maxOutputTokensPerCall } : {}),
|
|
578
580
|
...(this.stallTimeoutsPerCall !== undefined ? { stallTimeoutsPerCall: this.stallTimeoutsPerCall } : {}),
|
|
579
581
|
...(this.abortResultDetails !== undefined ? { abortResultDetails: this.abortResultDetails } : {}),
|
|
582
|
+
...(this.onToolInputValidationFault !== undefined ? { onToolInputValidationFault: this.onToolInputValidationFault } : {}),
|
|
580
583
|
...(this.resilience !== undefined ? { resilience: this.resilience } : {}),
|
|
581
584
|
...(this.maxToolConcurrency !== undefined ? { maxToolConcurrency: this.maxToolConcurrency } : {}),
|
|
582
585
|
...(this.streamingToolExecution === true && (this.getHandlers("tool_call")?.size ?? 0) === 0
|
|
@@ -1367,6 +1367,9 @@ export interface AgentHarnessOptions<TSkill extends Skill = Skill, TPromptTempla
|
|
|
1367
1367
|
* recoveries were invisible outside display:false nudge messages). Must not throw; hot path.
|
|
1368
1368
|
*/
|
|
1369
1369
|
loopTrace?: (step: import("../loop/agent-loop.js").LoopStep) => void;
|
|
1370
|
+
/** Loop-config pass-through — see `AgentLoopConfig.onToolInputValidationFault` (a throwing
|
|
1371
|
+
* `AgentTool.validateInput` is read as no verdict and disclosed here). */
|
|
1372
|
+
onToolInputValidationFault?: import("../loop/types.js").AgentLoopConfig["onToolInputValidationFault"];
|
|
1370
1373
|
/**
|
|
1371
1374
|
* design/131 (拍): per-task resilience intent flags, forwarded to every provider
|
|
1372
1375
|
* call (`StreamOptions.resilience`) so the deployment's decorator stack (degrading / failover /
|
|
@@ -103,3 +103,10 @@ export declare const ROSTER_LISTING_MAX = 25;
|
|
|
103
103
|
* core/tool-errors.ts, same reason).
|
|
104
104
|
*/
|
|
105
105
|
export declare const ROSTER_SEARCH_HINT_NAME = "ToolSearch";
|
|
106
|
+
/** The structured `details.code` of a tool result minted by an input pre-validation refusal (lifted onto
|
|
107
|
+
* the wire frame's `errorCode` like every other loop-minted code, e.g. `tool.not_found`). */
|
|
108
|
+
export declare const TOOL_VALIDATION_REFUSED_CODE = "tool.validation_refused";
|
|
109
|
+
/** How long an asynchronous `validateInput` may take to answer before it is read as "no verdict" (and
|
|
110
|
+
* reported on the fault seat): a validator is a precondition read, not a network call, so a single
|
|
111
|
+
* bound serves every tool; a deployment validator that needs longer is a validator doing too much. */
|
|
112
|
+
export declare const TOOL_INPUT_VALIDATION_TIMEOUT_MS = 10000;
|
|
@@ -1122,6 +1122,76 @@ function formatRosterRecovery(availableTools) {
|
|
|
1122
1122
|
: "";
|
|
1123
1123
|
return ` Available tools: ${listing}${hint}`;
|
|
1124
1124
|
}
|
|
1125
|
+
export const TOOL_VALIDATION_REFUSED_CODE = "tool.validation_refused";
|
|
1126
|
+
export const TOOL_INPUT_VALIDATION_TIMEOUT_MS = 10_000;
|
|
1127
|
+
async function runInputValidation(tool, args, toolCallId, signal, onFault) {
|
|
1128
|
+
if (tool.validateInput === undefined)
|
|
1129
|
+
return undefined;
|
|
1130
|
+
try {
|
|
1131
|
+
const verdict = await boundedValidatorCall(() => tool.validateInput(args, signal !== undefined ? { toolCallId, signal } : { toolCallId }), signal);
|
|
1132
|
+
if (verdict !== null && typeof verdict === "object" && verdict.ok === false && typeof verdict.message === "string") {
|
|
1133
|
+
return typeof verdict.code === "string" ? { message: verdict.message, code: verdict.code } : { message: verdict.message };
|
|
1134
|
+
}
|
|
1135
|
+
return undefined;
|
|
1136
|
+
}
|
|
1137
|
+
catch (error) {
|
|
1138
|
+
if (signal?.aborted === true)
|
|
1139
|
+
return undefined;
|
|
1140
|
+
try {
|
|
1141
|
+
const ret = onFault?.({ toolName: tool.name, toolCallId, error });
|
|
1142
|
+
if (ret !== null && typeof ret === "object" && typeof ret.then === "function") {
|
|
1143
|
+
ret.then(undefined, () => undefined);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
}
|
|
1148
|
+
return undefined;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function boundedValidatorCall(call, signal) {
|
|
1152
|
+
return new Promise((resolve, reject) => {
|
|
1153
|
+
let settled = false;
|
|
1154
|
+
const finish = (fn) => {
|
|
1155
|
+
if (settled)
|
|
1156
|
+
return;
|
|
1157
|
+
settled = true;
|
|
1158
|
+
clearTimeout(timer);
|
|
1159
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1160
|
+
fn();
|
|
1161
|
+
};
|
|
1162
|
+
const onAbort = () => finish(() => reject(Object.assign(new Error("tool input pre-validation aborted"), { name: "AbortError" })));
|
|
1163
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`tool input pre-validation did not answer within ${TOOL_INPUT_VALIDATION_TIMEOUT_MS}ms`))), TOOL_INPUT_VALIDATION_TIMEOUT_MS);
|
|
1164
|
+
if (signal?.aborted === true) {
|
|
1165
|
+
onAbort();
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1169
|
+
let out;
|
|
1170
|
+
try {
|
|
1171
|
+
out = call();
|
|
1172
|
+
}
|
|
1173
|
+
catch (err) {
|
|
1174
|
+
finish(() => reject(err));
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
Promise.resolve(out).then((v) => finish(() => resolve(v)), (err) => finish(() => reject(err)));
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
function sameToolArgs(a, b) {
|
|
1181
|
+
if (a === b)
|
|
1182
|
+
return true;
|
|
1183
|
+
const canon = (v) => JSON.stringify(v, (_k, x) => (x !== null && typeof x === "object" && !Array.isArray(x) ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, x[k]])) : x));
|
|
1184
|
+
return canon(a) === canon(b);
|
|
1185
|
+
}
|
|
1186
|
+
function validationRefusedOutcome(tool, refusal) {
|
|
1187
|
+
return {
|
|
1188
|
+
kind: "immediate",
|
|
1189
|
+
result: createErrorToolResult(refusal.message, {
|
|
1190
|
+
details: { code: TOOL_VALIDATION_REFUSED_CODE, toolName: tool.name, ...(refusal.code !== undefined ? { reason: refusal.code } : {}) },
|
|
1191
|
+
}),
|
|
1192
|
+
isError: true,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1125
1195
|
async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
|
|
1126
1196
|
const tool = findToolByName(currentContext.tools, toolCall.name);
|
|
1127
1197
|
if (!tool) {
|
|
@@ -1135,6 +1205,10 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
1135
1205
|
try {
|
|
1136
1206
|
const preparedToolCall = prepareToolCallArguments(tool, toolCall);
|
|
1137
1207
|
let finalArgs = validateToolArguments(tool, preparedToolCall);
|
|
1208
|
+
const refusal = await runInputValidation(tool, finalArgs, toolCall.id, signal, config.onToolInputValidationFault);
|
|
1209
|
+
if (refusal !== undefined)
|
|
1210
|
+
return validationRefusedOutcome(tool, refusal);
|
|
1211
|
+
const validatedArgs = finalArgs;
|
|
1138
1212
|
if (config.beforeToolCall) {
|
|
1139
1213
|
const beforeResult = await config.beforeToolCall({
|
|
1140
1214
|
assistantMessage,
|
|
@@ -1161,6 +1235,11 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
1161
1235
|
...preparedToolCall,
|
|
1162
1236
|
arguments: beforeResult.updatedInput,
|
|
1163
1237
|
});
|
|
1238
|
+
if (!sameToolArgs(validatedArgs, finalArgs)) {
|
|
1239
|
+
const rewrittenRefusal = await runInputValidation(tool, finalArgs, toolCall.id, signal, config.onToolInputValidationFault);
|
|
1240
|
+
if (rewrittenRefusal !== undefined)
|
|
1241
|
+
return validationRefusedOutcome(tool, rewrittenRefusal);
|
|
1242
|
+
}
|
|
1164
1243
|
}
|
|
1165
1244
|
}
|
|
1166
1245
|
if (signal?.aborted) {
|
|
@@ -19,6 +19,26 @@ export type StreamFn = LlmStreamFn;
|
|
|
19
19
|
* while tool-result message artifacts are emitted later in assistant source order.
|
|
20
20
|
*/
|
|
21
21
|
export type ToolExecutionMode = "sequential" | "parallel";
|
|
22
|
+
/**
|
|
23
|
+
* The verdict a tool's INPUT PRE-VALIDATION returns (see {@link AgentTool.validateInput}). `ok: true`
|
|
24
|
+
* (or an `undefined` return) lets the call proceed to the gate; `ok: false` refuses it BEFORE any
|
|
25
|
+
* permission question is asked — `message` is the model-facing text (the same text the tool's own
|
|
26
|
+
* execution would have produced for the same input, so the refusal reads identically either way) and
|
|
27
|
+
* `code` an optional machine-readable reason that rides the error result's structured details.
|
|
28
|
+
*/
|
|
29
|
+
export type ToolInputVerdict = {
|
|
30
|
+
ok: true;
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
message: string;
|
|
34
|
+
code?: string;
|
|
35
|
+
};
|
|
36
|
+
/** The context an input pre-validation hook receives: the call's identity and its abort signal, and
|
|
37
|
+
* nothing else — a validator reads state, it does not act on the run. */
|
|
38
|
+
export interface ToolInputValidationContext {
|
|
39
|
+
toolCallId: string;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
22
42
|
/**
|
|
23
43
|
* Controls how many queued user messages are injected when the agent loop reaches a queue drain point.
|
|
24
44
|
*
|
|
@@ -527,6 +547,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
|
|
|
527
547
|
* signal re-check, which has always minted its abort result with no details.
|
|
528
548
|
*/
|
|
529
549
|
abortResultDetails?: () => Record<string, unknown> | undefined;
|
|
550
|
+
/**
|
|
551
|
+
* Fault seat for a THROWING {@link AgentTool.validateInput}: the loop reads the throw as "no
|
|
552
|
+
* verdict" (the call proceeds to the gate as if the tool had no validator) and reports the fault
|
|
553
|
+
* here so a host can disclose it. Must not throw; a throwing sink is swallowed.
|
|
554
|
+
*/
|
|
555
|
+
onToolInputValidationFault?: (info: {
|
|
556
|
+
toolName: string;
|
|
557
|
+
toolCallId: string;
|
|
558
|
+
error: unknown;
|
|
559
|
+
}) => void;
|
|
530
560
|
/**
|
|
531
561
|
* Called after a tool finishes executing, before `tool_execution_end` and tool-result message events are emitted.
|
|
532
562
|
*
|
|
@@ -684,6 +714,18 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unk
|
|
|
684
714
|
* (boundInputHash binds args, never the preview). NEVER adjudication input. Mechanism-neutral:
|
|
685
715
|
* any tool may declare one (run_workflow projects its script meta). */
|
|
686
716
|
approvalPreview?: (args: unknown) => unknown;
|
|
717
|
+
/**
|
|
718
|
+
* OPTIONAL input pre-validation, run by the loop AFTER schema validation and BEFORE the tool-call
|
|
719
|
+
* gate (permission hooks, policy, the human/classifier ask). A refusal is returned to the model as a
|
|
720
|
+
* typed error result and no question is ever asked for the call — the point of the seat: a call the
|
|
721
|
+
* tool would refuse on its own precondition (a file edit whose target was never read this session)
|
|
722
|
+
* must not cost the operator an approval card it can only ever fail on. Pure by contract: reads
|
|
723
|
+
* state (read-tracking, path grammar), never acts. A THROWING validator is read as "no verdict"
|
|
724
|
+
* (the call proceeds to the gate; the fault is disclosed through the loop's fault seat) — a broken
|
|
725
|
+
* validator must not refuse tools. The tool's own execution keeps validating the same precondition
|
|
726
|
+
* (the two reads are not one atomic step, and direct `execute` callers skip this seat).
|
|
727
|
+
*/
|
|
728
|
+
validateInput?: (args: Static<TParameters>, ctx: ToolInputValidationContext) => Promise<ToolInputVerdict | undefined> | ToolInputVerdict | undefined;
|
|
687
729
|
/** Execute the tool call. Throw on failure instead of encoding errors in `content`. */
|
|
688
730
|
execute: (toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;
|
|
689
731
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export { createFsWriteGatePolicy, type FsWriteGatePolicyOptions } from "./core/f
|
|
|
119
119
|
export { RETIRED_TOOL_NAMES } from "./core/tool-name-aliases.js";
|
|
120
120
|
export { DEFAULT_SUBAGENT_TOOL_NAME } from "./agents/subagent.js";
|
|
121
121
|
export { renderTaskNotificationXml, taskNotificationDedupKey, isDelegatedAgentTerminal, isTerminalTaskNotification, SystemInjectionQueue, type TaskNotificationPayload, type TaskNotificationStatus, type ExternalNotificationInput, type SystemInjection, type SystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES, isSystemInjectionPriority, AGENT_MESSAGE_TAG, renderAgentMessageFrame, type SemaProvenance, } from "./core/task-notification.js";
|
|
122
|
-
export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, type SubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, type WiringManifest, type WiringFacts, type WiringLegKind, type AskSeamForm, type AskEffective, type QuestionChannelState, type SeamProvenance, type ParkLaneReason, type ManifestDurability, type StaticWiringDeps, type StaticWiringSpec, } from "./core/wiring-manifest.js";
|
|
122
|
+
export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, type SubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, AUTO_MODE_ARM_REASONS, type AutoModeArmReason, type WiringManifest, type WiringFacts, type WiringLegKind, type AskSeamForm, type AskEffective, type QuestionChannelState, type SeamProvenance, type ParkLaneReason, type ManifestDurability, type StaticWiringDeps, type StaticWiringSpec, } from "./core/wiring-manifest.js";
|
|
123
123
|
export { probeParkRoundTrip, durableParkGapOf, durableParkGapFor, PARK_SELFCHECK_SCOPE_PREFIX, type ParkSelfCheckResult, type ParkProbeFinding, type ParkProbeFindingCode, } from "./core/park-selfcheck.js";
|
|
124
124
|
export { type StoreDurability } from "./core/checkpoint-store.js";
|
|
125
125
|
export { type StoreFidelity } from "./core/checkpoint-store.js";
|
|
@@ -277,13 +277,14 @@ export { repairTextToolCalls } from "./brain/tool-call-repair.js";
|
|
|
277
277
|
export { createCircuitBreakerBrain, type CircuitBreakerOptions, type BreakerState, type BreakerSnapshot, type BreakerPhase, CIRCUIT_OPEN_MARKER, } from "./brain/circuit-breaker.js";
|
|
278
278
|
export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type DegradingBrainOptions, type DegradeReason, type DegradationInfo, } from "./brain/degrading.js";
|
|
279
279
|
export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
|
|
280
|
+
export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
|
|
280
281
|
export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, normalizeBaseUrl, hasAuthCarrier, type RoutePairingStatus, } from "./brain/route-adjudicator.js";
|
|
281
282
|
export type { RouteAdjudication, RouteCredential, RouteCredentialSource, RoutePairingConfig, RoutePairingPosture, RouteRefusalCode, RouteRefusalDetail, } from "./internal/llm.js";
|
|
282
283
|
export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } from "./brain/route-conformance.js";
|
|
283
284
|
export { type BrainTimeoutConfig } from "./brain/timeout.js";
|
|
284
285
|
export { createAssistantMessageEventStream } from "./internal/llm.js";
|
|
285
286
|
export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
286
|
-
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
|
|
287
|
+
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolInputVerdict, ToolInputValidationContext, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
|
|
287
288
|
export { Type } from "typebox";
|
|
288
289
|
export type { TSchema, Static } from "typebox";
|
|
289
290
|
export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
|
package/dist/index.js
CHANGED
|
@@ -96,7 +96,7 @@ export { createFsWriteGatePolicy } from "./core/fs-write-gate-policy.js";
|
|
|
96
96
|
export { RETIRED_TOOL_NAMES } from "./core/tool-name-aliases.js";
|
|
97
97
|
export { DEFAULT_SUBAGENT_TOOL_NAME } from "./agents/subagent.js";
|
|
98
98
|
export { renderTaskNotificationXml, taskNotificationDedupKey, isDelegatedAgentTerminal, isTerminalTaskNotification, SystemInjectionQueue, SYSTEM_INJECTION_PRIORITIES, isSystemInjectionPriority, AGENT_MESSAGE_TAG, renderAgentMessageFrame, } from "./core/task-notification.js";
|
|
99
|
-
export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, } from "./core/wiring-manifest.js";
|
|
99
|
+
export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, AUTO_MODE_ARM_REASONS, } from "./core/wiring-manifest.js";
|
|
100
100
|
export { probeParkRoundTrip, durableParkGapOf, durableParkGapFor, PARK_SELFCHECK_SCOPE_PREFIX, } from "./core/park-selfcheck.js";
|
|
101
101
|
export {} from "./core/checkpoint-store.js";
|
|
102
102
|
export {} from "./core/checkpoint-store.js";
|
|
@@ -234,6 +234,7 @@ export { repairTextToolCalls } from "./brain/tool-call-repair.js";
|
|
|
234
234
|
export { createCircuitBreakerBrain, CIRCUIT_OPEN_MARKER, } from "./brain/circuit-breaker.js";
|
|
235
235
|
export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, } from "./brain/degrading.js";
|
|
236
236
|
export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
|
|
237
|
+
export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
|
|
237
238
|
export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, normalizeBaseUrl, hasAuthCarrier, } from "./brain/route-adjudicator.js";
|
|
238
239
|
export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS } from "./brain/route-conformance.js";
|
|
239
240
|
export {} from "./brain/timeout.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
export type { CompactionPreparation, SummarizationClampDryRun } from "../engine/compaction/compaction.js";
|
|
8
8
|
export type { InvokedSkillRetention } from "../engine/compaction/utils.js";
|
|
9
9
|
export type { AgentCoreRuntimeDeps } from "../engine/loop/runtime-deps.js";
|
|
10
|
-
export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, } from "../engine/loop/types.js";
|
|
10
|
+
export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, ToolInputValidationContext, ToolInputVerdict, } from "../engine/loop/types.js";
|
|
11
11
|
export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileError, FileErrorCode, FileInfo, Result, WriteExpectation, WriteReceipt, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
|
|
12
12
|
export type { ExecutionEnvExecOptions, ExecResult } from "../engine/harness/types.js";
|
|
13
13
|
export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/types.js";
|
|
@@ -83,6 +83,35 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
|
|
|
83
83
|
replace_all: Type.Optional(Type.Boolean({ description: "Replace all occurrences of old_string (default false)" })),
|
|
84
84
|
}),
|
|
85
85
|
effect: "write",
|
|
86
|
+
validateInput: async (args, ctx) => {
|
|
87
|
+
const a = args;
|
|
88
|
+
if (Array.isArray(a.edits) && a.edits.length > 0)
|
|
89
|
+
return { ok: true };
|
|
90
|
+
if (typeof a.old_string !== "string" || typeof a.new_string !== "string")
|
|
91
|
+
return { ok: true };
|
|
92
|
+
const path = fileArgPath(args);
|
|
93
|
+
if (path === undefined)
|
|
94
|
+
return { ok: true };
|
|
95
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
96
|
+
if (!r.ok)
|
|
97
|
+
return { ok: true };
|
|
98
|
+
if (a.old_string === a.new_string)
|
|
99
|
+
return { ok: true };
|
|
100
|
+
const exists = await env.exists(r.key, ctx.signal);
|
|
101
|
+
if (!exists.ok || !exists.value)
|
|
102
|
+
return { ok: true };
|
|
103
|
+
const editInfo = await env.fileInfo(r.key, ctx.signal);
|
|
104
|
+
if (editInfo.ok && editInfo.value.size > MAX_EDIT_BYTES)
|
|
105
|
+
return { ok: true };
|
|
106
|
+
if (a.old_string === "")
|
|
107
|
+
return { ok: true };
|
|
108
|
+
if (ipynbRedirect("Edit", path))
|
|
109
|
+
return { ok: true };
|
|
110
|
+
const notRead = requireRead(state, r.key);
|
|
111
|
+
if (notRead === undefined)
|
|
112
|
+
return { ok: true };
|
|
113
|
+
return { ok: false, code: notRead.code, message: await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT) };
|
|
114
|
+
},
|
|
86
115
|
execute: async (args, ctx) => {
|
|
87
116
|
const a = args;
|
|
88
117
|
const batch = Array.isArray(a.edits) && a.edits.length > 0;
|
|
@@ -290,6 +319,23 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
290
319
|
content: Type.String({ description: "The content to write to the file" }),
|
|
291
320
|
}),
|
|
292
321
|
effect: "write",
|
|
322
|
+
validateInput: async (args, ctx) => {
|
|
323
|
+
const path = fileArgPath(args);
|
|
324
|
+
if (path === undefined)
|
|
325
|
+
return { ok: true };
|
|
326
|
+
if (ipynbRedirect("Write", path))
|
|
327
|
+
return { ok: true };
|
|
328
|
+
const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
329
|
+
if (!r.ok)
|
|
330
|
+
return { ok: true };
|
|
331
|
+
const exists = await env.exists(r.key, ctx.signal);
|
|
332
|
+
if (!exists.ok || !exists.value)
|
|
333
|
+
return { ok: true };
|
|
334
|
+
const notRead = requireRead(state, r.key);
|
|
335
|
+
if (notRead === undefined)
|
|
336
|
+
return { ok: true };
|
|
337
|
+
return { ok: false, code: notRead.code, message: await notReadRefusalText(env, "Write", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT) };
|
|
338
|
+
},
|
|
293
339
|
execute: async (args, ctx) => {
|
|
294
340
|
const { content } = args;
|
|
295
341
|
const path = fileArgPath(args);
|
|
@@ -356,6 +402,9 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
|
|
|
356
402
|
},
|
|
357
403
|
});
|
|
358
404
|
}
|
|
405
|
+
const notebookNotIpynbMessage = (notebookPath) => `Error (NotebookEdit): "${notebookPath}" is not a .ipynb file; use Edit for other file types.`;
|
|
406
|
+
const NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE = `Error (NotebookEdit): Cell type is required when using edit_mode=insert.`;
|
|
407
|
+
const NOTEBOOK_CELL_ID_REQUIRED_MESSAGE = `Error (NotebookEdit): cell_id is required for replace/delete.`;
|
|
359
408
|
export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
|
|
360
409
|
return defineTool({
|
|
361
410
|
name: "NotebookEdit",
|
|
@@ -382,18 +431,35 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
|
|
|
382
431
|
})),
|
|
383
432
|
}),
|
|
384
433
|
effect: "write",
|
|
434
|
+
validateInput: async (args, ctx) => {
|
|
435
|
+
const a = args;
|
|
436
|
+
const mode = a.edit_mode ?? "replace";
|
|
437
|
+
if (!a.notebook_path.toLowerCase().endsWith(".ipynb"))
|
|
438
|
+
return { ok: false, code: "invalid", message: notebookNotIpynbMessage(a.notebook_path) };
|
|
439
|
+
if (mode === "insert" && !a.cell_type)
|
|
440
|
+
return { ok: false, code: "invalid", message: NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE };
|
|
441
|
+
if (mode !== "insert" && !a.cell_id)
|
|
442
|
+
return { ok: false, code: "invalid", message: NOTEBOOK_CELL_ID_REQUIRED_MESSAGE };
|
|
443
|
+
const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
444
|
+
if (!r.ok)
|
|
445
|
+
return { ok: true };
|
|
446
|
+
const notRead = requireRead(state, r.key);
|
|
447
|
+
if (notRead === undefined)
|
|
448
|
+
return { ok: true };
|
|
449
|
+
return { ok: false, code: notRead.code, message: await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT) };
|
|
450
|
+
},
|
|
385
451
|
execute: async (args, ctx) => {
|
|
386
452
|
const a = args;
|
|
387
453
|
const { notebook_path, cell_id, new_source } = a;
|
|
388
454
|
let cellType = a.cell_type;
|
|
389
455
|
const mode = a.edit_mode ?? "replace";
|
|
390
456
|
if (!notebook_path.toLowerCase().endsWith(".ipynb")) {
|
|
391
|
-
return errorResult(
|
|
457
|
+
return errorResult(notebookNotIpynbMessage(notebook_path));
|
|
392
458
|
}
|
|
393
459
|
if (mode === "insert" && !cellType)
|
|
394
|
-
return errorResult(
|
|
460
|
+
return errorResult(NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE);
|
|
395
461
|
if (mode !== "insert" && !cell_id)
|
|
396
|
-
return errorResult(
|
|
462
|
+
return errorResult(NOTEBOOK_CELL_ID_REQUIRED_MESSAGE);
|
|
397
463
|
const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
|
|
398
464
|
if (!r.ok)
|
|
399
465
|
return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
|
|
3
3
|
"_tierComment": "#435 v1 — machine-readable layering of the public surface: stable = demonstrated by README.md / src/examples; internal = an `Internal`-marked name or a runner/engine deep-subtree declaration (the model seam src/engine/llm is excluded — it is the BYOM contract, not an engine internal); advanced = a supported export the front door does not walk you through. THIS IS AN INITIAL HEURISTIC, derived mechanically and expected to be refined ticket by ticket: no human reviewed these 1700+ entries one by one, and nothing here claims otherwise. Known bias: a short or English-word export name (ok, err, Result, Usage) can match ordinary prose in README.md and land `stable` on a coincidence. Every export MUST carry a tier — a new export with no row fails the gate in export-surface.test.ts.",
|
|
4
|
-
"count":
|
|
4
|
+
"count": 1964,
|
|
5
5
|
"exports": {
|
|
6
6
|
"A2ATaskState": "type",
|
|
7
7
|
"A2ATaskStateReversal": "type",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT": "variable",
|
|
31
31
|
"AUTONOMOUS_LOOP_SENTINEL": "variable",
|
|
32
32
|
"AUTO_MODE_ARMING_RECIPE_VERSION": "variable",
|
|
33
|
+
"AUTO_MODE_ARM_REASONS": "variable",
|
|
33
34
|
"AUTO_MODE_BASE_PROMPT": "variable",
|
|
34
35
|
"AUTO_MODE_DEFAULTS_SENTINEL": "variable",
|
|
35
36
|
"AUTO_MODE_PERMISSIONS_EXTERNAL": "variable",
|
|
@@ -78,6 +79,7 @@
|
|
|
78
79
|
"AssertOracleIsolationOptions": "interface",
|
|
79
80
|
"AssistantMessage": "interface",
|
|
80
81
|
"AssistantMessageEvent": "type",
|
|
82
|
+
"AutoModeArmReason": "type",
|
|
81
83
|
"AutoModeArmingFace": "interface",
|
|
82
84
|
"AutoModeArmingFold": "type",
|
|
83
85
|
"AutoModeArmingRecipe": "interface",
|
|
@@ -1199,6 +1201,8 @@
|
|
|
1199
1201
|
"ToolEffect": "type",
|
|
1200
1202
|
"ToolExecuteContext": "interface",
|
|
1201
1203
|
"ToolFingerprintInput": "interface",
|
|
1204
|
+
"ToolInputValidationContext": "interface",
|
|
1205
|
+
"ToolInputVerdict": "type",
|
|
1202
1206
|
"ToolModelGateRule": "interface",
|
|
1203
1207
|
"ToolOrigin": "type",
|
|
1204
1208
|
"ToolPolicy": "interface",
|
|
@@ -1251,6 +1255,8 @@
|
|
|
1251
1255
|
"VerificationResult": "interface",
|
|
1252
1256
|
"VerifyConfig": "interface",
|
|
1253
1257
|
"VmLifecycleOptions": "interface",
|
|
1258
|
+
"WAITING_FIRST_TOKEN_AFTER_MS": "variable",
|
|
1259
|
+
"WAITING_FIRST_TOKEN_EVERY_MS": "variable",
|
|
1254
1260
|
"WEBFETCH_SUMMARY_GUIDELINES": "variable",
|
|
1255
1261
|
"WEBFETCH_SUMMARY_MAX_CONTENT": "variable",
|
|
1256
1262
|
"WEB_FETCH_TOOL_NAME": "variable",
|
|
@@ -1990,6 +1996,7 @@
|
|
|
1990
1996
|
"AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT": "advanced",
|
|
1991
1997
|
"AUTONOMOUS_LOOP_SENTINEL": "advanced",
|
|
1992
1998
|
"AUTO_MODE_ARMING_RECIPE_VERSION": "advanced",
|
|
1999
|
+
"AUTO_MODE_ARM_REASONS": "advanced",
|
|
1993
2000
|
"AUTO_MODE_BASE_PROMPT": "advanced",
|
|
1994
2001
|
"AUTO_MODE_DEFAULTS_SENTINEL": "advanced",
|
|
1995
2002
|
"AUTO_MODE_PERMISSIONS_EXTERNAL": "advanced",
|
|
@@ -2038,6 +2045,7 @@
|
|
|
2038
2045
|
"AssertOracleIsolationOptions": "advanced",
|
|
2039
2046
|
"AssistantMessage": "stable",
|
|
2040
2047
|
"AssistantMessageEvent": "stable",
|
|
2048
|
+
"AutoModeArmReason": "advanced",
|
|
2041
2049
|
"AutoModeArmingFace": "advanced",
|
|
2042
2050
|
"AutoModeArmingFold": "advanced",
|
|
2043
2051
|
"AutoModeArmingRecipe": "advanced",
|
|
@@ -3159,6 +3167,8 @@
|
|
|
3159
3167
|
"ToolEffect": "advanced",
|
|
3160
3168
|
"ToolExecuteContext": "advanced",
|
|
3161
3169
|
"ToolFingerprintInput": "advanced",
|
|
3170
|
+
"ToolInputValidationContext": "internal",
|
|
3171
|
+
"ToolInputVerdict": "internal",
|
|
3162
3172
|
"ToolModelGateRule": "advanced",
|
|
3163
3173
|
"ToolOrigin": "advanced",
|
|
3164
3174
|
"ToolPolicy": "stable",
|
|
@@ -3211,6 +3221,8 @@
|
|
|
3211
3221
|
"VerificationResult": "advanced",
|
|
3212
3222
|
"VerifyConfig": "advanced",
|
|
3213
3223
|
"VmLifecycleOptions": "advanced",
|
|
3224
|
+
"WAITING_FIRST_TOKEN_AFTER_MS": "advanced",
|
|
3225
|
+
"WAITING_FIRST_TOKEN_EVERY_MS": "advanced",
|
|
3214
3226
|
"WEBFETCH_SUMMARY_GUIDELINES": "advanced",
|
|
3215
3227
|
"WEBFETCH_SUMMARY_MAX_CONTENT": "advanced",
|
|
3216
3228
|
"WEB_FETCH_TOOL_NAME": "advanced",
|