@sema-agent/core 7.17.7 → 7.18.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 +140 -13
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/agents/teacher.js +1 -0
- package/dist/agents/verify.js +3 -0
- package/dist/brain/route-adjudicator.d.ts +18 -0
- package/dist/brain/route-adjudicator.js +16 -0
- package/dist/core/agent-definition.d.ts +10 -0
- package/dist/core/artifact-host.d.ts +41 -4
- package/dist/core/artifact-host.js +18 -2
- package/dist/core/auto-mode.d.ts +141 -45
- package/dist/core/auto-mode.js +109 -3
- package/dist/core/checkpoint-store.d.ts +16 -2
- package/dist/core/engine-notice.d.ts +47 -1
- package/dist/core/engine-notice.js +7 -0
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +17 -2
- package/dist/core/gate-outcome.js +16 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hook-seats.d.ts +66 -0
- package/dist/core/hook-seats.js +37 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
- package/dist/core/runner/assemble-result.d.ts +2 -1
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/classifier-ladder.d.ts +89 -0
- package/dist/core/runner/classifier-ladder.js +65 -0
- package/dist/core/runner/contracts.d.ts +69 -13
- package/dist/core/runner/decide-continuation.d.ts +1 -3
- package/dist/core/runner/decide-continuation.js +1 -7
- package/dist/core/runner/deferral-census.d.ts +52 -0
- package/dist/core/runner/deferral-census.js +44 -0
- package/dist/core/runner/derived-route-fallback.d.ts +8 -4
- package/dist/core/runner/derived-route-fallback.js +8 -3
- package/dist/core/runner/gate-exit.d.ts +12 -0
- package/dist/core/runner/gate-exit.js +6 -3
- package/dist/core/runner/prepare-artifact.d.ts +10 -2
- package/dist/core/runner/prepare-artifact.js +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
- package/dist/core/runner/prepare-config-doors.d.ts +1 -1
- package/dist/core/runner/prepare-config-doors.js +5 -3
- package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
- package/dist/core/runner/prepare-defer-classify.js +8 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
- package/dist/core/runner/prepare-hands-readface.js +60 -24
- package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
- package/dist/core/runner/prepare-inherited-gate.js +10 -2
- package/dist/core/runner/prepare-lsp.d.ts +2 -2
- package/dist/core/runner/prepare-lsp.js +1 -1
- package/dist/core/runner/prepare-policy-chain.js +2 -0
- package/dist/core/runner/prepare-project-context.js +4 -2
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
- package/dist/core/runner/prepare-prompt-inputs.js +4 -1
- package/dist/core/runner/prepare-run-refs.d.ts +9 -1
- package/dist/core/runner/prepare-run-refs.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
- package/dist/core/runner/prepare-suspend-saga.js +5 -2
- package/dist/core/runner/prepare-task.js +18 -13
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
- package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
- package/dist/core/runner/prepare-turn-wiring.js +3 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/resume-checkpoint-screen.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-identity-wiring.js +6 -2
- package/dist/core/runner/run-leg.d.ts +2 -0
- package/dist/core/runner/run-leg.js +35 -41
- package/dist/core/runner/run-notification-lane.js +55 -27
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +16 -13
- package/dist/core/runner/run-turn-boundary.js +3 -1
- package/dist/core/runner/runtask.d.ts +6 -5
- package/dist/core/runner/runtask.js +26 -14
- package/dist/core/runner/skill-base-dir.d.ts +54 -0
- package/dist/core/runner/skill-base-dir.js +42 -0
- package/dist/core/runner/synthetic-tools.d.ts +16 -19
- package/dist/core/runner/synthetic-tools.js +18 -10
- package/dist/core/runner/tool-defer-gate.d.ts +22 -9
- package/dist/core/runner/tool-defer-gate.js +15 -5
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +2 -0
- package/dist/core/runner/tool-progress-lane.d.ts +42 -0
- package/dist/core/runner/tool-progress-lane.js +61 -0
- package/dist/core/runner-deps.d.ts +48 -7
- package/dist/core/swappable-deps.d.ts +9 -7
- package/dist/core/swappable-deps.js +7 -3
- package/dist/core/task-event.d.ts +96 -29
- package/dist/core/task-notification.d.ts +142 -16
- package/dist/core/task-notification.js +106 -0
- package/dist/core/task-result.d.ts +9 -1
- package/dist/core/task-spec.d.ts +20 -3
- package/dist/core/tighten-task-spec.js +7 -5
- package/dist/core/tool-model-gate.d.ts +1 -1
- package/dist/core/tool-search-setting.d.ts +78 -0
- package/dist/core/tool-search-setting.js +90 -0
- package/dist/core/tool-spec.d.ts +8 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +46 -0
- package/dist/core/wiring-manifest.js +9 -1
- package/dist/engine/execution-env/node-execution-env.js +10 -4
- package/dist/engine/harness/agent-harness.d.ts +42 -0
- package/dist/engine/harness/agent-harness.js +61 -29
- package/dist/engine/loop/agent-loop.d.ts +68 -3
- package/dist/engine/loop/agent-loop.js +109 -16
- package/dist/index.d.ts +10 -5
- package/dist/index.js +9 -5
- package/dist/internal/harness.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +6 -1
- package/dist/orchestration/workflow-governance.d.ts +7 -1
- package/dist/orchestration/workflow-governance.js +9 -5
- package/dist/orchestration/workflow-primitives.d.ts +3 -0
- package/dist/orchestration/workflow-primitives.js +6 -1
- package/dist/orchestration/workflow-types.d.ts +48 -13
- package/dist/orchestration/workflow-types.js +1 -1
- package/dist/orchestration/workflow.js +45 -17
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/section-usage.d.ts +26 -0
- package/dist/prompt-assembly/section-usage.js +10 -0
- package/dist/prompt-assembly/types.d.ts +8 -6
- package/dist/prompts/default.d.ts +13 -0
- package/dist/prompts/default.js +16 -8
- package/dist/tools/artifact/artifact-text.d.ts +10 -1
- package/dist/tools/artifact/artifact-text.js +6 -3
- package/dist/tools/artifact/artifact-tool.d.ts +12 -2
- package/dist/tools/artifact/artifact-tool.js +203 -70
- package/dist/tools/artifact/conformance.d.ts +56 -0
- package/dist/tools/artifact/conformance.js +349 -0
- package/dist/tools/artifact/local-stub.js +1 -1
- package/dist/tools/artifact/reject-breaker.d.ts +28 -0
- package/dist/tools/artifact/reject-breaker.js +42 -0
- package/dist/tools/fs/bash-lexer.d.ts +132 -0
- package/dist/tools/fs/bash-lexer.js +563 -0
- package/dist/tools/fs/bash-program-position.d.ts +142 -0
- package/dist/tools/fs/bash-program-position.js +125 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
- package/dist/tools/fs/bash-readonly-classifier.js +121 -552
- package/dist/tools/fs/containment-face.d.ts +89 -0
- package/dist/tools/fs/containment-face.js +81 -0
- package/dist/tools/fs/fs-bash.d.ts +46 -1
- package/dist/tools/fs/fs-bash.js +158 -22
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +24 -5
- package/dist/tools/fs/index.js +18 -6
- package/dist/tools/fs/repo-map.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +3 -3
- package/dist/tools/fs/safety.js +4 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +85 -9
- package/wire-changes/core@7.17.7.json +49 -0
- package/dist/tools/fs/read-face.d.ts +0 -68
- package/dist/tools/fs/read-face.js +0 -47
|
@@ -49,6 +49,7 @@ import { runAttachmentSeats } from "./run-attachment-seats.js";
|
|
|
49
49
|
import { runToolMountFacts } from "./run-tool-mount-facts.js";
|
|
50
50
|
import { runReasoningSeat } from "./run-reasoning-seat.js";
|
|
51
51
|
import { runClockAndContent } from "./run-clock-and-content.js";
|
|
52
|
+
import { createToolProgressLane } from "./tool-progress-lane.js";
|
|
52
53
|
import { runBrainSinks } from "./run-brain-sinks.js";
|
|
53
54
|
import { runCompactionMachinery } from "./run-compaction-machinery.js";
|
|
54
55
|
import { runStopAndFinalVerify } from "./run-stop-and-final-verify.js";
|
|
@@ -212,18 +213,25 @@ export class Runner {
|
|
|
212
213
|
}
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
|
-
const
|
|
216
|
-
const
|
|
216
|
+
const faceSeats = ["readFace", "writeFace"].filter((seat) => seats.includes(seat));
|
|
217
|
+
const priorFaces = {
|
|
218
|
+
readFace: this.deps.readFace,
|
|
219
|
+
writeFace: this.deps.writeFace,
|
|
220
|
+
};
|
|
217
221
|
const written = {
|
|
218
222
|
...this.deps,
|
|
219
223
|
...(expanded !== undefined ? { models: expanded } : {}),
|
|
220
224
|
...(catalogSwap && tiers !== undefined ? { tiers } : {}),
|
|
221
|
-
...(faceSwap && candidate.readFace !== undefined ? { readFace: candidate.readFace } : {}),
|
|
222
225
|
};
|
|
223
226
|
if (catalogSwap && tiers === undefined)
|
|
224
227
|
delete written.tiers;
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
const writtenFaceSeats = written;
|
|
229
|
+
for (const seat of faceSeats) {
|
|
230
|
+
if (candidate[seat] !== undefined)
|
|
231
|
+
writtenFaceSeats[seat] = candidate[seat];
|
|
232
|
+
else
|
|
233
|
+
delete writtenFaceSeats[seat];
|
|
234
|
+
}
|
|
227
235
|
this.deps = written;
|
|
228
236
|
if (catalogSwap) {
|
|
229
237
|
const modelCount = Object.keys(candidate.models).length;
|
|
@@ -245,13 +253,15 @@ export class Runner {
|
|
|
245
253
|
});
|
|
246
254
|
}
|
|
247
255
|
}
|
|
248
|
-
|
|
249
|
-
const to = candidate
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
256
|
+
for (const seat of faceSeats) {
|
|
257
|
+
const to = candidate[seat] ?? null;
|
|
258
|
+
const from = priorFaces[seat] ?? null;
|
|
259
|
+
const message = `deployment ${seat === "readFace" ? "read" : "write"} face swapped: ${from ?? "(no declaration)"} → ${to ?? "(no declaration)"}; in-flight legs keep the face they prepared under, new legs resolve against the new declaration`;
|
|
260
|
+
const detail = { from, to, occurrenceId: uuidv7() };
|
|
261
|
+
if (seat === "readFace")
|
|
262
|
+
deliverEngineNotice(this.deps.onNotice, { code: "config.read_face_swapped", message, detail });
|
|
263
|
+
else
|
|
264
|
+
deliverEngineNotice(this.deps.onNotice, { code: "config.write_face_swapped", message, detail });
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
sideQuery(spec) {
|
|
@@ -413,6 +423,8 @@ export class Runner {
|
|
|
413
423
|
const { todoToolMounted, taskToolsMounted, writeFamilyOf, toolStartAt, startedToolCallIds } = runToolMountFacts({ spec, prepared, rs });
|
|
414
424
|
const { observeReasoningWireFacts, reasoningResolution } = runReasoningSeat({ prepared, rs, taskIdRef });
|
|
415
425
|
const { effectiveTimeoutMs, walltimeMonotonicDeadline, timeout, pushContent, ownCommittedTailRef, emitCommitted } = runClockAndContent({ spec, prepared, queue, internals, rs, ident, parentToolCallId, startedToolCallIds });
|
|
426
|
+
const toolProgress = createToolProgressLane(pushContent, ident);
|
|
427
|
+
prepared.toolProgressRef.current = toolProgress;
|
|
416
428
|
const { subagentName, withBrainSinks } = runBrainSinks({ queue, internals, rs, ident, parentToolCallId, observeReasoningWireFacts });
|
|
417
429
|
const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
|
|
418
430
|
for (const orphan of prepared.wakeRecovered) {
|
|
@@ -426,7 +438,7 @@ export class Runner {
|
|
|
426
438
|
prepared, stats, rs,
|
|
427
439
|
deps: {
|
|
428
440
|
spec, queue, internals, ident, parentToolCallId, subagentName,
|
|
429
|
-
pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf,
|
|
441
|
+
pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf,
|
|
430
442
|
toolLabels, postToolBatchHook, batchArgs,
|
|
431
443
|
runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
|
|
432
444
|
},
|
|
@@ -453,7 +465,7 @@ export class Runner {
|
|
|
453
465
|
});
|
|
454
466
|
const { flushGitMirror, unsubGitRetry, unsubBoundary, unsub } = runGitLane({ prepared, queue, rs, ident, onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, onTurnBoundary, runner: this.depsSeat });
|
|
455
467
|
return runLeg({
|
|
456
|
-
spec, queue, prepared, resume, rs, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds,
|
|
468
|
+
spec, queue, prepared, resume, rs, isDelegatedChild: internals?.isDelegatedChild === true, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds,
|
|
457
469
|
announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub,
|
|
458
470
|
runner: this.depsSeat,
|
|
459
471
|
next: ({ final, threw, abortedLive, userInterruptedLive, strandedHumanAnswers }) => runTerminalAdoption({
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A skill's BASE DIRECTORY: the placeholder grammar its material may use to name its own root, the bounded
|
|
3
|
+
* substitution that resolves it, and the delivered-length measure the load gate budgets on.
|
|
4
|
+
*
|
|
5
|
+
* Split out of the skill tool because it is a different concern: the tool decides what a skill INVOKE
|
|
6
|
+
* returns, this decides how the caller's material names files on the machine the tools run on.
|
|
7
|
+
*/
|
|
8
|
+
/** The typed refusal a `SkillSpec.baseDir` outside its domain raises — one code, one sentence. */
|
|
9
|
+
export declare const SKILL_BASE_DIR_INVALID_CODE = "config.skill_base_dir_invalid";
|
|
10
|
+
/**
|
|
11
|
+
* The ONE screen of `SkillSpec.baseDir`: absent ⇒ no directory (nothing is substituted and no opening line is
|
|
12
|
+
* added); a NON-EMPTY string ⇒ itself; anything else ⇒ a loud refusal carrying {@link SKILL_BASE_DIR_INVALID_CODE}.
|
|
13
|
+
*
|
|
14
|
+
* Both rejected shapes were SILENT before, and each was silent in its own direction. `""` is not "no directory":
|
|
15
|
+
* it substitutes to nothing, so a body's `${SKILL_ROOT}/ref.md` is delivered as the filesystem-root path
|
|
16
|
+
* `/ref.md` and the opening line names no directory at all — a wrong answer where absence would have given the
|
|
17
|
+
* visible unresolved placeholder the seat's contract promises. A non-string makes the delivered LENGTH `NaN`,
|
|
18
|
+
* and `NaN <= limit` is false, so the load gate dropped the skill and reported "content is NaN chars" —
|
|
19
|
+
* a configuration typo reading as an over-limit body. Core still neither resolves nor reads the directory
|
|
20
|
+
* (the brain-outside rule); this screens the VALUE, not the path.
|
|
21
|
+
*
|
|
22
|
+
* It runs at the load gate ({@link substitutedSkillLength}, which spec assembly calls for every skill), so the
|
|
23
|
+
* substitution faces below are reached only with a value that already passed it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function skillBaseDirOf(value: unknown): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Substitute a skill's base directory for the root placeholders in ITS OWN MATERIAL.
|
|
28
|
+
*
|
|
29
|
+
* @contract skill.base_dir — with `SkillSpec.baseDir` set, a `Skill` result opens with the line
|
|
30
|
+
* `Base directory for this skill: <baseDir>` and every `${SKILL_ROOT}` / `${CLAUDE_PLUGIN_ROOT}` in the
|
|
31
|
+
* body AND in every attachment's content is replaced by that value; `files[].path` is then read relative to
|
|
32
|
+
* it. The substitution covers the CALLER'S material only — never the model-supplied `args` section, which is
|
|
33
|
+
* live input and is delivered as written. Core neither resolves nor reads the directory (the brain-outside
|
|
34
|
+
* rule that keeps `SkillSpec.files` inline); with `baseDir` absent no line is added, nothing is substituted,
|
|
35
|
+
* and the result is byte-identical to a pre-baseDir invoke.
|
|
36
|
+
*/
|
|
37
|
+
export declare function substituteSkillRoot(text: string, baseDir: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Substitute under a CEILING, and report the size the full substitution would have had.
|
|
40
|
+
*
|
|
41
|
+
* Substitution is the one step here that can make text grow without bound: a placeholder is 13 or 21 chars and a real directory can be any length, so an
|
|
42
|
+
* attachment that is nothing but placeholders expands by a factor of the path's length. Building that string in order to measure it against the attachments
|
|
43
|
+
* cap — or worse, to discover it does not FIT and drop it — spends exactly the memory the cap exists to bound before the cap is consulted, and a big enough
|
|
44
|
+
* one exceeds the runtime's own string limit. So the length is counted as the loop walks, nothing is materialized past `maxChars`, and the caller budgets on
|
|
45
|
+
* `totalChars` while rendering only what fits.
|
|
46
|
+
*/
|
|
47
|
+
/** The DELIVERED length of a body once its root placeholders resolve — counted, never materialized, so the load gate measures what the model will actually
|
|
48
|
+
* receive rather than what the caller typed. Equal to `text.length` when no directory is stated, and the one place the seat's value is SCREENED
|
|
49
|
+
* ({@link skillBaseDirOf}) — the gate that budgets on this number is the first reader of every skill on every leg. */
|
|
50
|
+
export declare function substitutedSkillLength(text: string, baseDir: unknown): number;
|
|
51
|
+
export declare function substituteSkillRootBounded(text: string, baseDir: string, maxChars: number): {
|
|
52
|
+
text: string;
|
|
53
|
+
totalChars: number;
|
|
54
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const SKILL_ROOT_PLACEHOLDERS = /\$\{(?:SKILL_ROOT|CLAUDE_PLUGIN_ROOT)\}/g;
|
|
2
|
+
export const SKILL_BASE_DIR_INVALID_CODE = "config.skill_base_dir_invalid";
|
|
3
|
+
export function skillBaseDirOf(value) {
|
|
4
|
+
if (value === undefined)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (typeof value === "string" && value !== "")
|
|
7
|
+
return value;
|
|
8
|
+
const e = new Error(`SkillSpec.baseDir must be a non-empty string naming its own directory (got ${value === "" ? '""' : typeof value}) — ` +
|
|
9
|
+
`omit the key where no directory is stated; an empty one resolves the body's \${SKILL_ROOT} placeholders to nothing.`);
|
|
10
|
+
e.code = SKILL_BASE_DIR_INVALID_CODE;
|
|
11
|
+
throw e;
|
|
12
|
+
}
|
|
13
|
+
export function substituteSkillRoot(text, baseDir) {
|
|
14
|
+
return text.replace(SKILL_ROOT_PLACEHOLDERS, () => baseDir);
|
|
15
|
+
}
|
|
16
|
+
export function substitutedSkillLength(text, baseDir) {
|
|
17
|
+
const dir = skillBaseDirOf(baseDir);
|
|
18
|
+
if (dir === undefined)
|
|
19
|
+
return text.length;
|
|
20
|
+
return substituteSkillRootBounded(text, dir, 0).totalChars;
|
|
21
|
+
}
|
|
22
|
+
export function substituteSkillRootBounded(text, baseDir, maxChars) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
let totalChars = 0;
|
|
25
|
+
let built = 0;
|
|
26
|
+
const push = (piece) => {
|
|
27
|
+
totalChars += piece.length;
|
|
28
|
+
if (built >= maxChars)
|
|
29
|
+
return;
|
|
30
|
+
const kept = piece.length > maxChars - built ? piece.slice(0, maxChars - built) : piece;
|
|
31
|
+
parts.push(kept);
|
|
32
|
+
built += kept.length;
|
|
33
|
+
};
|
|
34
|
+
let last = 0;
|
|
35
|
+
for (const m of text.matchAll(SKILL_ROOT_PLACEHOLDERS)) {
|
|
36
|
+
push(text.slice(last, m.index));
|
|
37
|
+
push(baseDir);
|
|
38
|
+
last = m.index + m[0].length;
|
|
39
|
+
}
|
|
40
|
+
push(text.slice(last));
|
|
41
|
+
return { text: parts.join(""), totalChars };
|
|
42
|
+
}
|
|
@@ -52,12 +52,10 @@ export interface ReportedFinding {
|
|
|
52
52
|
export declare function createReportFindingsTool(): AgentTool;
|
|
53
53
|
/** Reserved name of the injected skill-loading tool (present only when `spec.skills` is non-empty). */
|
|
54
54
|
export declare const SKILL_TOOL_NAME = "Skill";
|
|
55
|
-
/** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only size door
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* the disclosure block and the tool, with an operator-visible config error. The compaction-time
|
|
60
|
-
* retention caps live in the engine's compaction module — a different door, deliberately. */
|
|
55
|
+
/** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only invoke-time size door is a 1MB file-load gate — an over-limit skill is REJECTED
|
|
56
|
+
* WHOLE, never truncated, and the invoked body enters context untruncated). Sema skills are spec-supplied, so the equivalent gate sits at spec assembly:
|
|
57
|
+
* an oversized skill is excluded from the disclosure block and the tool, with an operator-visible config error. The compaction-time retention caps are a
|
|
58
|
+
* different door, deliberately. */
|
|
61
59
|
export declare const SKILL_CONTENT_MAX_CHARS: number;
|
|
62
60
|
/** Total listing-block cap in UTF-8 BYTES — [c209-C] R2 C4①: the old 8000-CHAR cap measured a
|
|
63
61
|
* different coordinate than the bundle's `ATTACHMENT_BYTE_CAP` (8192 UTF-8 bytes) and counted only
|
|
@@ -68,28 +66,24 @@ export declare const SKILL_CONTENT_MAX_CHARS: number;
|
|
|
68
66
|
* bundle cap solo and an intact commit is always reachable. MUST stay ≤ ATTACHMENT_BYTE_CAP
|
|
69
67
|
* (turn-attachments.ts; not imported here — that module imports this one — pinned by test instead). */
|
|
70
68
|
export declare const SKILLS_BLOCK_MAX_BYTES = 8000;
|
|
71
|
-
/** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face
|
|
72
|
-
*
|
|
73
|
-
* at invoke time (CC parity), while attachments are reference material riding along — bounded and
|
|
74
|
-
* truncation-disclosed. */
|
|
69
|
+
/** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face budget, distinct from the (gone) body truncation door:
|
|
70
|
+
* the body is delivered untruncated at invoke time (CC parity), while attachments ride along bounded and truncation-disclosed. */
|
|
75
71
|
export declare const SKILL_ATTACHMENTS_MAX_CHARS = 50000;
|
|
76
|
-
/** Per-attachment cap on the PATH rendered in its `###` header —
|
|
77
|
-
* eat the attachments budget on its own. Overflow is truncated with an ellipsis. */
|
|
72
|
+
/** Per-attachment cap on the PATH rendered in its `###` header — one pathological path must not eat the budget. Overflow truncates with an ellipsis. */
|
|
78
73
|
export declare const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
|
|
79
74
|
interface SkillLike {
|
|
80
75
|
name: string;
|
|
81
76
|
description: string;
|
|
82
77
|
content: string;
|
|
83
|
-
/** Optional Gate-3 runtime capability manifest — carried through so a successful
|
|
84
|
-
* `skill` load can push its scope frame. Skills without one stay unscoped (backward-compatible). */
|
|
78
|
+
/** Optional Gate-3 runtime capability manifest — carried through so a successful `skill` load can push its scope frame; skills without one stay unscoped. */
|
|
85
79
|
manifest?: SkillManifest;
|
|
86
|
-
/** Optional attachments (SkillSpec.files):
|
|
87
|
-
*
|
|
88
|
-
* (see the module note above) — core never writes them to disk (deployment's half). */
|
|
80
|
+
/** Optional attachments (SkillSpec.files): paths in the block, delivered inline on invoke under {@link SKILL_ATTACHMENTS_MAX_CHARS}. Same caller-trusted
|
|
81
|
+
* contract as `content` — core never writes them to disk (deployment's half). */
|
|
89
82
|
files?: Array<{
|
|
90
83
|
path: string;
|
|
91
84
|
content: string;
|
|
92
85
|
}>;
|
|
86
|
+
/** Optional base directory (SkillSpec.baseDir) — see `@contract skill.base_dir`. Absent ⇒ byte-identical result. */ baseDir?: string;
|
|
93
87
|
}
|
|
94
88
|
/** [c209-C] The metadata slice of a skill the LISTING faces consume (disclosure block + the
|
|
95
89
|
* skills_listing turn attachment): name, description, attachment paths — never the body. The
|
|
@@ -116,8 +110,11 @@ export interface SkillListingEntry {
|
|
|
116
110
|
* keep their descriptions"). Absent ⇒ the entry's array index is used, i.e. the prior behavior. */
|
|
117
111
|
declaredRank?: number;
|
|
118
112
|
}
|
|
119
|
-
/** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the
|
|
120
|
-
*
|
|
113
|
+
/** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the disclosure block
|
|
114
|
+
* and the tool, so what the prompt promises is exactly what the tool serves. Each field is READ ONCE, BY NAME, and
|
|
115
|
+
* both consumers see the copy: the gate screens this list while the invoke substitutes from it, so a `get baseDir()`
|
|
116
|
+
* answering those two reads differently passed a screen that measured a value the model never received — and a rest
|
|
117
|
+
* copy would silently drop an INHERITED or non-enumerable `manifest`, i.e. widen a skill's capability restriction. */
|
|
121
118
|
export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[];
|
|
122
119
|
/** [c209-C] The skills listing's lead line — single-sourced so the run loop's durable-resume
|
|
123
120
|
* transcript probe (MED-3② skills arm, runtask) matches EXACTLY what the initial frame carries
|
|
@@ -2,6 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { defineTool } from "../tools.js";
|
|
3
3
|
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
4
4
|
import { toolFace } from "../tool-catalog-entries.js";
|
|
5
|
+
import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
|
|
5
6
|
export const OUTPUT_TOOL_NAME = "StructuredOutput";
|
|
6
7
|
export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
|
|
7
8
|
export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
|
|
@@ -112,7 +113,9 @@ export function normalizeSkills(skills) {
|
|
|
112
113
|
for (const s of skills ?? []) {
|
|
113
114
|
if (!seen.has(s.name)) {
|
|
114
115
|
seen.add(s.name);
|
|
115
|
-
|
|
116
|
+
const { name, description, content, manifest, files, baseDir, ...rest } = s;
|
|
117
|
+
void rest;
|
|
118
|
+
out.push({ name, description, content, ...(manifest !== undefined ? { manifest } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
@@ -202,7 +205,8 @@ export function createSkillTool(skills, scope) {
|
|
|
202
205
|
if (scope && skill.manifest) {
|
|
203
206
|
scope.push({ kind: "manifest", manifest: skill.manifest });
|
|
204
207
|
}
|
|
205
|
-
|
|
208
|
+
const baseDir = skill.baseDir;
|
|
209
|
+
let body = baseDir !== undefined ? substituteSkillRoot(skill.content, baseDir) : skill.content;
|
|
206
210
|
if (skill.files !== undefined && skill.files.length > 0) {
|
|
207
211
|
const SEPARATOR_CHARS = 2;
|
|
208
212
|
const SECTION_HEADER = "## Attachments";
|
|
@@ -217,27 +221,30 @@ export function createSkillTool(skills, scope) {
|
|
|
217
221
|
: f.path;
|
|
218
222
|
const header = `### ${path}\n`;
|
|
219
223
|
const renderedOverhead = header.length + SEPARATOR_CHARS;
|
|
224
|
+
const expanded = baseDir !== undefined ? substituteSkillRootBounded(f.content, baseDir, Math.max(0, budget)) : { text: f.content, totalChars: f.content.length };
|
|
225
|
+
const content = expanded.text;
|
|
226
|
+
const contentChars = expanded.totalChars;
|
|
220
227
|
if (budget <= renderedOverhead) {
|
|
221
228
|
omittedCount += 1;
|
|
222
|
-
omittedChars += header.length +
|
|
229
|
+
omittedChars += header.length + contentChars;
|
|
223
230
|
continue;
|
|
224
231
|
}
|
|
225
232
|
const contentBudget = budget - renderedOverhead;
|
|
226
|
-
if (
|
|
227
|
-
const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${
|
|
233
|
+
if (contentChars > contentBudget) {
|
|
234
|
+
const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${contentChars} chars — attachments cap reached]`;
|
|
228
235
|
const shown = contentBudget - markerFor(contentBudget).length;
|
|
229
236
|
if (shown <= 0) {
|
|
230
237
|
omittedCount += 1;
|
|
231
|
-
omittedChars += header.length +
|
|
238
|
+
omittedChars += header.length + contentChars;
|
|
232
239
|
budget = 0;
|
|
233
240
|
continue;
|
|
234
241
|
}
|
|
235
|
-
sections.push(`${header}${
|
|
242
|
+
sections.push(`${header}${content.slice(0, shown)}${markerFor(shown)}`);
|
|
236
243
|
budget = 0;
|
|
237
244
|
continue;
|
|
238
245
|
}
|
|
239
|
-
sections.push(`${header}${
|
|
240
|
-
budget -= renderedOverhead +
|
|
246
|
+
sections.push(`${header}${content}`);
|
|
247
|
+
budget -= renderedOverhead + contentChars;
|
|
241
248
|
}
|
|
242
249
|
if (omittedCount > 0) {
|
|
243
250
|
sections.push(`[+${omittedCount} more attachment(s) omitted (${omittedChars} chars) — the ${SKILL_ATTACHMENTS_MAX_CHARS}-char attachments cap is exhausted]`);
|
|
@@ -245,7 +252,8 @@ export function createSkillTool(skills, scope) {
|
|
|
245
252
|
body = `${body}\n\n${sections.join("\n\n")}`;
|
|
246
253
|
}
|
|
247
254
|
const skillArgs = typeof a.args === "string" && a.args.length > 0 ? a.args.slice(0, 10_000) : undefined;
|
|
248
|
-
|
|
255
|
+
const delivered = skillArgs ? `${body}\n\n## Input\n${skillArgs}` : body;
|
|
256
|
+
return baseDir !== undefined ? `Base directory for this skill: ${baseDir}\n\n${delivered}` : delivered;
|
|
249
257
|
},
|
|
250
258
|
});
|
|
251
259
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { Model } from "../../internal/llm.js";
|
|
2
2
|
import type { ToolSpec } from "../types.js";
|
|
3
3
|
import type { ToolFingerprintInput } from "../cache-break-detector.js";
|
|
4
|
+
import { type ResolvedToolSearch, type ToolSearchSetting } from "../tool-search-setting.js";
|
|
4
5
|
/**
|
|
5
6
|
* The deferral GATE: which mounted tool names ship as placeholders on a request. Pure
|
|
6
7
|
* registry logic over names and sizes — no harness, no I/O. `tool-disclosure.ts` owns the placeholder
|
|
7
8
|
* and ToolSearch mechanics that act on the set this module decides; `prepare-defer-classify.ts` is the
|
|
8
9
|
* phase that assembles the inputs from the mounted face.
|
|
9
10
|
*/
|
|
10
|
-
/** The deferral gate's threshold: the
|
|
11
|
-
*
|
|
12
|
-
export declare const DEFER_THRESHOLD_FRACTION
|
|
11
|
+
/** The deferral gate's DEFAULT threshold: with no `toolSearch` seat, the candidate set defers when its inlined size reaches this fraction of the model's
|
|
12
|
+
* context window (that set measured, never the rest of the face). Derived from the seat's own default percent — one default, two units. */
|
|
13
|
+
export declare const DEFER_THRESHOLD_FRACTION: number;
|
|
13
14
|
/** The typed refusal a `deferMode` outside its declared domain (`"auto"` or absent) raises at the classifier —
|
|
14
15
|
* a bad value never selects an arm silently. */
|
|
15
16
|
export declare const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
|
|
@@ -24,10 +25,13 @@ export interface DeferralGate {
|
|
|
24
25
|
gated: Set<string>;
|
|
25
26
|
/** Σ inlined chars (name + description + serialized schema) over `gated` — the bytes the gate could reclaim. */
|
|
26
27
|
gatedChars: number;
|
|
27
|
-
/** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold.
|
|
28
|
+
/** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. `0` under `toolSearch: true` and `Infinity` under
|
|
29
|
+
* `false` — the two boolean arms ARE this number. */
|
|
28
30
|
thresholdChars: number;
|
|
29
31
|
/** Whether `gated` is deferred on this request: `gatedChars ≥ thresholdChars`. */
|
|
30
32
|
open: boolean;
|
|
33
|
+
/** The lowered seat this verdict was measured under ({@link ToolSearchSetting}); always present, since an absent seat resolves to the default `auto`. */
|
|
34
|
+
toolSearch: ResolvedToolSearch;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Deferral has TWO rules, and only two.
|
|
@@ -44,11 +48,12 @@ export interface DeferralGate {
|
|
|
44
48
|
* "auto"` WIDENS the candidate set to every caller spec — undeclared caller tools included — and nothing
|
|
45
49
|
* else: it is not a second gate, and engine built-ins the deployment did not name are candidates under no
|
|
46
50
|
* setting. The gate decides the set all or nothing: deferred iff `gatedChars ≥ thresholdChars`, where the
|
|
47
|
-
* threshold is
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
51
|
+
* threshold is the DEPLOYMENT'S FRACTION of the model's context window (`contextTokens ?? contextWindow`,
|
|
52
|
+
* in chars at {@link CHARS_PER_TOKEN}) — {@link ToolSearchSetting} lowered by {@link resolveToolSearch},
|
|
53
|
+
* which is {@link DEFER_THRESHOLD_FRACTION} when no seat is wired. The measure is the candidate set ONLY —
|
|
54
|
+
* the bytes the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose
|
|
55
|
+
* weight sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A
|
|
56
|
+
* window of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
|
|
52
57
|
*
|
|
53
58
|
* Invariants: `explicit ∩ gated = ∅`; `deferred = explicit ∪ (open ? gated : ∅)`; a pinned name is in no
|
|
54
59
|
* list; the same inputs always yield the same verdict (a pure function of the request).
|
|
@@ -65,6 +70,9 @@ export declare function deferralGate(opts: {
|
|
|
65
70
|
fullTools: ReadonlyArray<ToolFingerprintInput>;
|
|
66
71
|
/** `"auto"` widens the candidate set to every caller spec (see above). Any other value refuses. */
|
|
67
72
|
deferMode?: "auto";
|
|
73
|
+
/** The deployment's gate policy for rule ② ({@link ToolSearchSetting}); absent ⇒ the default `auto` at
|
|
74
|
+
* {@link TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT}, byte-for-byte the prior behavior. */
|
|
75
|
+
toolSearch?: ToolSearchSetting;
|
|
68
76
|
model: Model;
|
|
69
77
|
/** Rule ① — `TaskSpec.deferTools`: ALREADY-MOUNTED wire names (built-ins included; `ToolSpec.defer`
|
|
70
78
|
* only reaches caller specs) the caller defers on every request. Caller pre-filters to mounted names. */
|
|
@@ -84,3 +92,8 @@ export declare function deferralGate(opts: {
|
|
|
84
92
|
* gate; the `TaskSpec` / `RunnerDeps` JSDoc points here.
|
|
85
93
|
*/
|
|
86
94
|
export declare function classifyDeferred(opts: Parameters<typeof deferralGate>[0]): Set<string>;
|
|
95
|
+
/** The SAME fold, with the verdict it folded — a disclosure that reports the policy must read the one the gate measured under, never a later re-resolution. */
|
|
96
|
+
export declare function classifyDeferredWithGate(opts: Parameters<typeof deferralGate>[0]): {
|
|
97
|
+
deferred: Set<string>;
|
|
98
|
+
gate: DeferralGate;
|
|
99
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
|
|
2
|
-
|
|
2
|
+
import { TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT, describeRejectedValue, resolveToolSearch } from "../tool-search-setting.js";
|
|
3
|
+
export const DEFER_THRESHOLD_FRACTION = TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT / 100;
|
|
3
4
|
const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
|
|
4
5
|
function inlinedChars(t) {
|
|
5
6
|
let schema = "";
|
|
@@ -14,10 +15,11 @@ function inlinedChars(t) {
|
|
|
14
15
|
export const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
|
|
15
16
|
export function deferralGate(opts) {
|
|
16
17
|
if (opts.deferMode !== undefined && opts.deferMode !== "auto") {
|
|
17
|
-
const e = new Error(`RunnerDeps.deferMode: invalid value ${
|
|
18
|
+
const e = new Error(`RunnerDeps.deferMode: invalid value ${describeRejectedValue(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
|
|
18
19
|
e.code = DEFER_MODE_INVALID_CODE;
|
|
19
20
|
throw e;
|
|
20
21
|
}
|
|
22
|
+
const toolSearch = resolveToolSearch(opts.toolSearch);
|
|
21
23
|
const pinned = new Set(opts.alwaysLoadNames ?? []);
|
|
22
24
|
for (const s of opts.specs) {
|
|
23
25
|
if (s.alwaysLoad === true)
|
|
@@ -44,14 +46,22 @@ export function deferralGate(opts) {
|
|
|
44
46
|
if (gated.has(t.name))
|
|
45
47
|
gatedChars += inlinedChars(t);
|
|
46
48
|
const windowTokens = Math.max(0, opts.model.contextTokens ?? opts.model.contextWindow ?? 0);
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
+
const thresholdTokens = windowTokens * toolSearch.fraction;
|
|
50
|
+
const thresholdChars = Number.isFinite(thresholdTokens)
|
|
51
|
+
? Math.floor(thresholdTokens) * CHARS_PER_TOKEN
|
|
52
|
+
: toolSearch.fraction === 0
|
|
53
|
+
? 0
|
|
54
|
+
: Number.POSITIVE_INFINITY;
|
|
55
|
+
return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars, toolSearch };
|
|
49
56
|
}
|
|
50
57
|
export function classifyDeferred(opts) {
|
|
58
|
+
return classifyDeferredWithGate(opts).deferred;
|
|
59
|
+
}
|
|
60
|
+
export function classifyDeferredWithGate(opts) {
|
|
51
61
|
const gate = deferralGate(opts);
|
|
52
62
|
const deferred = new Set(gate.explicit);
|
|
53
63
|
if (gate.open)
|
|
54
64
|
for (const n of gate.gated)
|
|
55
65
|
deferred.add(n);
|
|
56
|
-
return deferred;
|
|
66
|
+
return { deferred, gate };
|
|
57
67
|
}
|
|
@@ -247,6 +247,11 @@ export declare function createToolSearchTool(opts: {
|
|
|
247
247
|
*/
|
|
248
248
|
isMounted?: (name: string) => boolean;
|
|
249
249
|
serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
|
|
250
|
+
/** Called at the TAIL of this lane's activation section, after the rollback window has closed — the one point at which the shared active set is committed
|
|
251
|
+
* for this call. The disclosure census rides it, because a publisher reading that set from OUTSIDE the section (an MCP refresh's rematerialize, which runs
|
|
252
|
+
* unserialized and after an await) can observe a name a racing activation staged and then rolled back, and announce an activation that never happened.
|
|
253
|
+
* Absent ⇒ nothing is published. */
|
|
254
|
+
onActivationCommitted?: () => void;
|
|
250
255
|
/** Resolve a deferred name's REAL declared
|
|
251
256
|
* parameter schema, read from the live roster at call time. Presence switches this face to the
|
|
252
257
|
* static posture: activation never swaps the placeholder in the tools block, so the result text
|
|
@@ -336,6 +336,8 @@ export function createToolSearchTool(opts) {
|
|
|
336
336
|
throw e;
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
+
if (newly.length > 0)
|
|
340
|
+
opts.onActivationCommitted?.();
|
|
339
341
|
return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
|
|
340
342
|
});
|
|
341
343
|
const { newly, ride } = await section;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run's TOOL-PROGRESS lane — the single mint of the `tool_progress` frame: its clock, its cadence,
|
|
3
|
+
* and the heartbeat that keeps a silent long-running tool call visible.
|
|
4
|
+
*
|
|
5
|
+
* TWO PRODUCERS, ONE MINT. A tool that observes its own work ticks fast and carries what it produced: the
|
|
6
|
+
* shell's execute leg watches its command's output and calls `emit` about once a second. Every OTHER call is
|
|
7
|
+
* opaque here — the runner knows only that it started and has not ended — so this lane runs a slow beat for
|
|
8
|
+
* any in-flight call silent for {@link TOOL_HEARTBEAT_MS}.
|
|
9
|
+
*
|
|
10
|
+
* THE LANE OWNS TIME, the producer owns FACTS. A producer reports what it has seen (the tail, the totals),
|
|
11
|
+
* never how long it has run: `elapsedTimeSeconds` is computed HERE from one origin per call, because the two
|
|
12
|
+
* producers do not start their clocks together — the station registers a call when it ENTERS EXECUTION, a
|
|
13
|
+
* shell command's own clock could only start at its spawn. With two origins a 30 s beat could be followed
|
|
14
|
+
* moments later by a shell tick reporting 1 s, breaking the monotonicity the frame promises.
|
|
15
|
+
*
|
|
16
|
+
* THE CADENCE HAS ONE ENFORCEMENT POINT, same reason: a producer's timer is a SAMPLING clock, not the
|
|
17
|
+
* contract. `emit` drops a frame landing inside {@link TOOL_PROGRESS_MIN_INTERVAL_MS} of that call's last.
|
|
18
|
+
*
|
|
19
|
+
* ONE RULE FOR THE BEAT: a call is beaten when nothing has been emitted FOR IT for a whole interval,
|
|
20
|
+
* counting its own start as the first such event — covering both "it only just started" and "the tool
|
|
21
|
+
* already ticks faster than this". The interval is the RUN's, so a first beat lands anywhere in
|
|
22
|
+
* [TOOL_HEARTBEAT_MS, 2 × TOOL_HEARTBEAT_MS): the price of one timer per run, stated rather than hidden.
|
|
23
|
+
*
|
|
24
|
+
* THE CLOCK IS MONOTONIC: every predicate here is a DIFFERENCE of two readings (is this call due a beat, is
|
|
25
|
+
* this offer inside the floor, how long has it run), and off the wall clock a backward step — NTP, a
|
|
26
|
+
* container's clock settling after a resume — makes each difference negative and silences BOTH producers
|
|
27
|
+
* until it catches up: the exact failure this lane exists to prevent.
|
|
28
|
+
*
|
|
29
|
+
* LIVENESS ONLY — see the `tool_progress` member's contract in `task-event.ts`. The timer is `unref`'d and
|
|
30
|
+
* every emission is fault-contained. */
|
|
31
|
+
import type { TaskEvent, TaskEventIdentity } from "../types.js";
|
|
32
|
+
import type { ToolProgressLane } from "./contracts.js";
|
|
33
|
+
/** How long a tool call may stay silent before this lane beats for it, in ms. ONE SOURCE, and CC
|
|
34
|
+
* 2.1.259's own per-tool heartbeat interval (`Cct`) — this engine keeps the cadence and folds the beat
|
|
35
|
+
* into the one progress frame instead of minting a second shape (registered divergence). */
|
|
36
|
+
export declare const TOOL_HEARTBEAT_MS = 30000;
|
|
37
|
+
/** The floor between two frames of ONE call — the `tool_progress.cadence` contract, enforced here and
|
|
38
|
+
* nowhere else so it holds across producers rather than inside each one. */
|
|
39
|
+
export declare const TOOL_PROGRESS_MIN_INTERVAL_MS = 1000;
|
|
40
|
+
/** Build the lane over the run's content pusher (`pushContent` — the ONE seat that puts a frame on this
|
|
41
|
+
* run's queue and, for a sub-agent, forwards it to the parent's sink) and its identity mint. */
|
|
42
|
+
export declare function createToolProgressLane(push: (e: TaskEvent) => void, ident: () => TaskEventIdentity): ToolProgressLane;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const TOOL_HEARTBEAT_MS = 30_000;
|
|
2
|
+
export const TOOL_PROGRESS_MIN_INTERVAL_MS = 1_000;
|
|
3
|
+
const monotonicNow = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
4
|
+
export function createToolProgressLane(push, ident) {
|
|
5
|
+
const inFlight = new Map();
|
|
6
|
+
let timer;
|
|
7
|
+
const arm = () => {
|
|
8
|
+
if (timer !== undefined)
|
|
9
|
+
return;
|
|
10
|
+
timer = setInterval(beat, TOOL_HEARTBEAT_MS);
|
|
11
|
+
timer.unref?.();
|
|
12
|
+
};
|
|
13
|
+
const emit = (body) => {
|
|
14
|
+
const now = monotonicNow();
|
|
15
|
+
let live = inFlight.get(body.toolCallId);
|
|
16
|
+
if (live === undefined) {
|
|
17
|
+
live = { toolName: body.toolName, startedAt: now, lastAt: now - TOOL_PROGRESS_MIN_INTERVAL_MS };
|
|
18
|
+
inFlight.set(body.toolCallId, live);
|
|
19
|
+
arm();
|
|
20
|
+
}
|
|
21
|
+
if (now - live.lastAt < TOOL_PROGRESS_MIN_INTERVAL_MS)
|
|
22
|
+
return;
|
|
23
|
+
live.lastAt = now;
|
|
24
|
+
try {
|
|
25
|
+
push({ type: "tool_progress", ...body, elapsedTimeSeconds: Math.floor((now - live.startedAt) / 1000), ...ident() });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function beat() {
|
|
31
|
+
const now = monotonicNow();
|
|
32
|
+
for (const [toolCallId, live] of inFlight) {
|
|
33
|
+
if (now - live.lastAt < TOOL_HEARTBEAT_MS)
|
|
34
|
+
continue;
|
|
35
|
+
emit({ toolCallId, toolName: live.toolName });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const disarm = () => {
|
|
39
|
+
if (timer === undefined)
|
|
40
|
+
return;
|
|
41
|
+
clearInterval(timer);
|
|
42
|
+
timer = undefined;
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
emit,
|
|
46
|
+
noteStart(toolCallId, toolName) {
|
|
47
|
+
const startedAt = monotonicNow();
|
|
48
|
+
inFlight.set(toolCallId, { toolName, startedAt, lastAt: startedAt });
|
|
49
|
+
arm();
|
|
50
|
+
},
|
|
51
|
+
noteEnd(toolCallId) {
|
|
52
|
+
inFlight.delete(toolCallId);
|
|
53
|
+
if (inFlight.size === 0)
|
|
54
|
+
disarm();
|
|
55
|
+
},
|
|
56
|
+
clear() {
|
|
57
|
+
inFlight.clear();
|
|
58
|
+
disarm();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|