@sema-agent/core 5.36.0 → 5.38.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 +124 -0
- package/dist/agents/subagent.d.ts +10 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +3 -0
- package/dist/agents/team.d.ts +7 -1
- package/dist/agents/team.js +11 -9
- package/dist/agents/verify.js +3 -0
- package/dist/core/auto-mode-prompt-assets.d.ts +5 -3
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +26 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +129 -2
- package/dist/core/hooks.js +20 -3
- package/dist/core/memory-engine/engine.d.ts +142 -0
- package/dist/core/memory-engine/engine.js +264 -2
- package/dist/core/memory-engine/file-backend.d.ts +490 -16
- package/dist/core/memory-engine/file-backend.js +1099 -36
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +42 -2
- package/dist/core/memory-engine/layout.js +76 -12
- package/dist/core/memory-engine/memory-backend-contract.d.ts +13 -0
- package/dist/core/memory-engine/memory-backend-contract.js +89 -0
- package/dist/core/protocol-table.d.ts +4 -4
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-config-doors.d.ts +17 -0
- package/dist/core/runner/prepare-config-doors.js +33 -2
- package/dist/core/runner/prepare-memory.d.ts +11 -1
- package/dist/core/runner/prepare-memory.js +48 -2
- package/dist/core/runner/prepare-task.d.ts +22 -2
- package/dist/core/runner/prepare-task.js +125 -42
- package/dist/core/runner/runtask.js +50 -11
- package/dist/core/tool-model-gate.d.ts +125 -0
- package/dist/core/tool-model-gate.js +303 -0
- package/dist/core/tool-policy.d.ts +1 -1
- package/dist/core/types.d.ts +284 -1
- package/dist/core/types.js +21 -0
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/orchestration/builtin-workflows.d.ts +68 -6
- package/dist/orchestration/builtin-workflows.js +26 -9
- package/dist/orchestration/run-workflow-tool.d.ts +10 -1
- package/dist/orchestration/run-workflow-tool.js +70 -27
- package/dist/orchestration/workflow-script-store.d.ts +8 -3
- package/dist/prompts/coordinator.d.ts +4 -1
- package/dist/prompts/coordinator.js +8 -0
- package/dist/prompts/default.d.ts +14 -4
- package/dist/prompts/default.js +2 -1
- package/dist/scenarios/full-body.d.ts +5 -0
- package/dist/scenarios/full-body.js +8 -4
- package/dist/tools/fs/fs-shared.d.ts +3 -2
- package/dist/tools/fs/fs-shared.js +19 -9
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +24 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
2
2
|
import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
|
|
3
|
+
import { deliverDelegationLifecycle } from "../types.js";
|
|
3
4
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
4
5
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
5
6
|
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
@@ -757,7 +758,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
757
758
|
const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : undefined;
|
|
758
759
|
if (!boundarySteered && !prepared.abortController.signal.aborted) {
|
|
759
760
|
try {
|
|
760
|
-
const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined);
|
|
761
|
+
const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity });
|
|
761
762
|
if (r?.additionalContext && injectedThisTurn === undefined) {
|
|
762
763
|
const body = sanitizeUntrustedText(r.additionalContext);
|
|
763
764
|
const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
|
|
@@ -1484,12 +1485,12 @@ export class Runner {
|
|
|
1484
1485
|
...(typeof h.preToolUse === "function" ? { preToolUse: (t, i, c) => h.preToolUse(t, i, c) } : {}),
|
|
1485
1486
|
...(typeof h.preToolUse === "function" && h.preToolUseObservational === true ? { preToolUseObservational: true } : {}),
|
|
1486
1487
|
...(typeof h.postToolUse === "function" ? { postToolUse: (t, i, o, c) => h.postToolUse(t, i, o, c) } : {}),
|
|
1487
|
-
...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p) => h.userPromptSubmit(p) } : {}),
|
|
1488
|
+
...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p, c) => h.userPromptSubmit(p, c) } : {}),
|
|
1488
1489
|
...(typeof h.stop === "function" ? { stop: (c) => h.stop(c) } : {}),
|
|
1489
1490
|
...(typeof h.postToolUseFailure === "function"
|
|
1490
1491
|
? { postToolUseFailure: (t, i, f, c) => h.postToolUseFailure(t, i, f, c) }
|
|
1491
1492
|
: {}),
|
|
1492
|
-
...(typeof h.postToolBatch === "function" ? { postToolBatch: (b) => h.postToolBatch(b) } : {}),
|
|
1493
|
+
...(typeof h.postToolBatch === "function" ? { postToolBatch: (b, m, c) => h.postToolBatch(b, m, c) } : {}),
|
|
1493
1494
|
...(typeof h.preCompact === "function" ? { preCompact: (c) => h.preCompact(c) } : {}),
|
|
1494
1495
|
...(typeof h.postCompact === "function" ? { postCompact: (c) => h.postCompact(c) } : {}),
|
|
1495
1496
|
...(typeof h.stopFailure === "function" ? { stopFailure: (c) => h.stopFailure(c) } : {}),
|
|
@@ -1659,6 +1660,7 @@ export class Runner {
|
|
|
1659
1660
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
1660
1661
|
errorCode: code,
|
|
1661
1662
|
...(remoteEnvFailure !== undefined ? { remoteEnvFailures: remoteEnvFailure } : {}),
|
|
1663
|
+
...(taskIdRef.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: taskIdRef.effectiveMemoryScopes } : {}),
|
|
1662
1664
|
...(() => {
|
|
1663
1665
|
const hinted = err.retryAfterMs;
|
|
1664
1666
|
return code === "memory.admission_required" && typeof hinted === "number" && Number.isFinite(hinted) && hinted > 0
|
|
@@ -1678,6 +1680,13 @@ export class Runner {
|
|
|
1678
1680
|
durationMs: Date.now() - runStartedAt,
|
|
1679
1681
|
ts: Date.now(),
|
|
1680
1682
|
}));
|
|
1683
|
+
const owedTerminal = taskIdRef.delegationTerminalOwed;
|
|
1684
|
+
if (owedTerminal !== undefined) {
|
|
1685
|
+
taskIdRef.delegationTerminalOwed = undefined;
|
|
1686
|
+
deliverDelegationLifecycle(this.deps.onDelegationLifecycle, { phase: "terminal", identity: owedTerminal, status: "failed", turns: 0, ...(code !== undefined ? { errorCode: code } : {}) }, createSafeNotifier({
|
|
1687
|
+
onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
1688
|
+
}), "runtask.onDelegationLifecycle");
|
|
1689
|
+
}
|
|
1681
1690
|
queue.push({ type: "done", result: resultValue });
|
|
1682
1691
|
queue.close();
|
|
1683
1692
|
});
|
|
@@ -2029,11 +2038,27 @@ export class Runner {
|
|
|
2029
2038
|
}, this);
|
|
2030
2039
|
notificationHarness = prepared.harness;
|
|
2031
2040
|
notificationSessionId = prepared.sessionId;
|
|
2041
|
+
if (taskIdRef)
|
|
2042
|
+
taskIdRef.effectiveMemoryScopes = prepared.effectiveMemoryScopes;
|
|
2032
2043
|
const runSourceTaskId = spec.taskId ?? prepared.sessionId;
|
|
2033
2044
|
const parentToolCallId = internals?.parentToolCallId;
|
|
2034
2045
|
const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
|
|
2035
2046
|
notificationIdent = ident;
|
|
2036
2047
|
queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
|
|
2048
|
+
const delegationLifecycleNotifier = createSafeNotifier({
|
|
2049
|
+
onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
|
|
2050
|
+
});
|
|
2051
|
+
const emitDelegationLifecycle = (event) => {
|
|
2052
|
+
if (!prepared.hookIdentity.isDelegatedChild)
|
|
2053
|
+
return;
|
|
2054
|
+
if (this.deps.onDelegationLifecycle === undefined)
|
|
2055
|
+
return;
|
|
2056
|
+
deliverDelegationLifecycle(this.deps.onDelegationLifecycle, event, delegationLifecycleNotifier, "runtask.onDelegationLifecycle");
|
|
2057
|
+
};
|
|
2058
|
+
emitDelegationLifecycle({ phase: "spawn", identity: prepared.hookIdentity });
|
|
2059
|
+
if (taskIdRef !== undefined && prepared.hookIdentity.isDelegatedChild && this.deps.onDelegationLifecycle !== undefined) {
|
|
2060
|
+
taskIdRef.delegationTerminalOwed = prepared.hookIdentity;
|
|
2061
|
+
}
|
|
2037
2062
|
manualCompactRef.emitMooted = (reason) => {
|
|
2038
2063
|
queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
|
|
2039
2064
|
};
|
|
@@ -2726,6 +2751,7 @@ export class Runner {
|
|
|
2726
2751
|
stopHookActive: consecutiveBlocks > 0,
|
|
2727
2752
|
consecutiveBlocks,
|
|
2728
2753
|
getBranch: () => prepared.session.getBranch(),
|
|
2754
|
+
identity: prepared.hookIdentity,
|
|
2729
2755
|
});
|
|
2730
2756
|
}
|
|
2731
2757
|
catch (err) {
|
|
@@ -2801,7 +2827,7 @@ export class Runner {
|
|
|
2801
2827
|
...this.seamCCompactionOptions(prepared),
|
|
2802
2828
|
...gitRestateOption(prepared),
|
|
2803
2829
|
...windowSafetyOptions(prepared.harness.getModel()),
|
|
2804
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "forced"),
|
|
2830
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity),
|
|
2805
2831
|
});
|
|
2806
2832
|
if (comp.compacted) {
|
|
2807
2833
|
compactionBreaker.failures = 0;
|
|
@@ -2866,7 +2892,7 @@ export class Runner {
|
|
|
2866
2892
|
runnerHooks: {
|
|
2867
2893
|
onError: this.deps.onError,
|
|
2868
2894
|
seamCCompactionOptions: (p) => this.seamCCompactionOptions(p),
|
|
2869
|
-
compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig),
|
|
2895
|
+
compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity),
|
|
2870
2896
|
recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
|
|
2871
2897
|
},
|
|
2872
2898
|
});
|
|
@@ -3096,7 +3122,7 @@ export class Runner {
|
|
|
3096
3122
|
const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
|
|
3097
3123
|
if (userPromptSubmit) {
|
|
3098
3124
|
try {
|
|
3099
|
-
const decision = await userPromptSubmit(spec.objective);
|
|
3125
|
+
const decision = await userPromptSubmit(spec.objective, { identity: prepared.hookIdentity });
|
|
3100
3126
|
if (decision?.block) {
|
|
3101
3127
|
prepared.blockedRef.reason = formatHookFeedback(decision.block);
|
|
3102
3128
|
promptBlocked = true;
|
|
@@ -3426,6 +3452,7 @@ export class Runner {
|
|
|
3426
3452
|
remoteEnvFailures: prepared.remoteEnvFailures,
|
|
3427
3453
|
effectiveReadFace: prepared.effectiveReadFace,
|
|
3428
3454
|
effectiveReadDenyPatterns: prepared.effectiveReadDenyPatterns,
|
|
3455
|
+
effectiveMemoryScopes: prepared.effectiveMemoryScopes,
|
|
3429
3456
|
retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
|
|
3430
3457
|
abortedForTimeout: timeout.fired,
|
|
3431
3458
|
abortedForTurns: rs.limits.turnsExceeded,
|
|
@@ -3499,6 +3526,7 @@ export class Runner {
|
|
|
3499
3526
|
result.errorCode !== "conflict") {
|
|
3500
3527
|
try {
|
|
3501
3528
|
await stopFailureHook({
|
|
3529
|
+
identity: prepared.hookIdentity,
|
|
3502
3530
|
error: result.errorMessage ?? "model error",
|
|
3503
3531
|
...(result.errorCode !== undefined ? { errorKind: result.errorCode } : {}),
|
|
3504
3532
|
turns: stats.turns,
|
|
@@ -3512,6 +3540,15 @@ export class Runner {
|
|
|
3512
3540
|
}
|
|
3513
3541
|
}
|
|
3514
3542
|
}
|
|
3543
|
+
emitDelegationLifecycle({
|
|
3544
|
+
phase: "terminal",
|
|
3545
|
+
identity: prepared.hookIdentity,
|
|
3546
|
+
status: result.status,
|
|
3547
|
+
turns: stats.turns,
|
|
3548
|
+
...(result.errorCode !== undefined ? { errorCode: result.errorCode } : {}),
|
|
3549
|
+
});
|
|
3550
|
+
if (taskIdRef !== undefined)
|
|
3551
|
+
taskIdRef.delegationTerminalOwed = undefined;
|
|
3515
3552
|
if (rs.degrade.degraded)
|
|
3516
3553
|
result.degraded = rs.degrade.degraded;
|
|
3517
3554
|
if (prepared.outputRef.set)
|
|
@@ -4499,15 +4536,17 @@ export class Runner {
|
|
|
4499
4536
|
consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
|
|
4500
4537
|
};
|
|
4501
4538
|
}
|
|
4502
|
-
compactionHookOptions(spec, sessionId, trigger) {
|
|
4539
|
+
compactionHookOptions(spec, sessionId, trigger, identity) {
|
|
4503
4540
|
const hooks = spec.hooks ?? this.deps.hooks;
|
|
4504
4541
|
const pre = hooks?.preCompact;
|
|
4505
4542
|
const post = hooks?.postCompact;
|
|
4543
|
+
const withIdentity = (ctx) => identity !== undefined ? { ...ctx, identity } : ctx;
|
|
4506
4544
|
return {
|
|
4507
4545
|
trigger,
|
|
4508
4546
|
...(pre
|
|
4509
4547
|
? {
|
|
4510
|
-
preCompact: async (
|
|
4548
|
+
preCompact: async (rawCtx) => {
|
|
4549
|
+
const ctx = withIdentity(rawCtx);
|
|
4511
4550
|
const report = (err) => {
|
|
4512
4551
|
try {
|
|
4513
4552
|
this.deps.onError?.(err, { phase: "hook", sessionId });
|
|
@@ -4531,9 +4570,9 @@ export class Runner {
|
|
|
4531
4570
|
: {}),
|
|
4532
4571
|
...(post
|
|
4533
4572
|
? {
|
|
4534
|
-
postCompact: async (
|
|
4573
|
+
postCompact: async (rawCtx) => {
|
|
4535
4574
|
try {
|
|
4536
|
-
await post.call(hooks,
|
|
4575
|
+
await post.call(hooks, withIdentity(rawCtx));
|
|
4537
4576
|
}
|
|
4538
4577
|
catch (err) {
|
|
4539
4578
|
try {
|
|
@@ -4577,7 +4616,7 @@ export class Runner {
|
|
|
4577
4616
|
...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
|
|
4578
4617
|
...this.seamCCompactionOptions(prepared),
|
|
4579
4618
|
...gitRestateOption(prepared),
|
|
4580
|
-
...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
|
|
4619
|
+
...this.compactionHookOptions(spec, prepared.sessionId, "auto", prepared.hookIdentity),
|
|
4581
4620
|
});
|
|
4582
4621
|
this.recordCompactionReuse(prepared, finishComp);
|
|
4583
4622
|
if (finishComp.unevaluableWindow) {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/277 — the tool-registration MODEL GATE (CC 233 counterpart, first formalized there as a
|
|
3
|
+
* family+version-floor table over the todo/task-board tool family).
|
|
4
|
+
*
|
|
5
|
+
* WHAT THE GATE IS: a DEFAULT-MOUNT trim, never a capability ban. A {@link ToolSpec.modelGate}
|
|
6
|
+
* tag declares "this entry is a default-mounted scaffold of the named class"; when the task's
|
|
7
|
+
* RESOLVED model id matches the class's rule below, that entry is dropped from the roster at
|
|
8
|
+
* prepare (true unmount — the schema never reaches `tools[]`). An EXPLICITLY composed tool is
|
|
9
|
+
* never tagged (the bundle only stamps its default arms), so "the user asked for it" is the
|
|
10
|
+
* first restore channel by construction. Restore channels beyond that:
|
|
11
|
+
* `TaskSpec.restoreGatedTools` (per task-tree), env `SEMA_TOOL_MODEL_GATE=off` (process),
|
|
12
|
+
* `RunnerDeps.toolModelGate: false` (deployment).
|
|
13
|
+
*
|
|
14
|
+
* FAIL-OPEN, BY CONTRACT (not by accident): sema is BYOM — model ids are an OPEN set, and this
|
|
15
|
+
* table encodes POSITIVE knowledge ("this family at this version and above manages multi-step
|
|
16
|
+
* work without the scaffold"). An id the table knows nothing about (deepseek-* / gpt-* / qwen-* / any
|
|
17
|
+
* custom name) is NOT gated: no knowledge ⇒ no trim ⇒ tool stays. The matcher's three pass-through
|
|
18
|
+
* edges (family absent, id shape unmatched, version below floor) all point the same way, and it is
|
|
19
|
+
* the only polarity compatible with BYOM — the reverse would trim every non-claude deployment's
|
|
20
|
+
* default face. The engine does NOT guarantee a canonical id either: a deployment-authored
|
|
21
|
+
* `Model.id` (a raw Model object, or a catalog key shadowing the tier expansion) that is not in
|
|
22
|
+
* canonical `claude-<family>-<version>` shape simply falls off the regex and stays open — the
|
|
23
|
+
* explicit-id discipline's deployment-side duty. To gate a non-claude strong model, write the
|
|
24
|
+
* exact id into a {@link ToolModelGateRule.modelIds} row (the only non-claude channel; no prefix
|
|
25
|
+
* or wildcard syntax exists).
|
|
26
|
+
*
|
|
27
|
+
* DIVERGENCE (registered, deliberate): CC judges the MAIN-LOOP canonical model once per process;
|
|
28
|
+
* sema judges the PER-TASK resolved model on every prepare — BYOM per-task models are first-class,
|
|
29
|
+
* so a delegated child on a different model re-judges under its own (strong parent / weak child ⇒
|
|
30
|
+
* the child gets the scaffold back). And the env valve's polarity is reversed (CC: enable the
|
|
31
|
+
* tools; sema: disable the GATE) because sema's class vocabulary is open — a per-family enable
|
|
32
|
+
* env would have to grow with every class.
|
|
33
|
+
*/
|
|
34
|
+
import type { ToolSpec } from "./types.js";
|
|
35
|
+
/** The shared tail extractor (BYOM ids may carry provider prefixes — "openrouter/anthropic/claude-fable-5"):
|
|
36
|
+
* boundary-aware LAST path segment, case-folded. Single source for every model-family/model-id
|
|
37
|
+
* comparison site (`isFableFamilyModelId` consumes it too — the prompt-shape axis, semantically
|
|
38
|
+
* independent but sharing the one tail-extraction posture). */
|
|
39
|
+
export declare function modelIdTail(id: string): string;
|
|
40
|
+
/** One gate class's matching rule (both axes optional; a rule with BOTH axes empty gates nothing —
|
|
41
|
+
* the per-class OFF shape a deployment reaches by explicitly clearing the axes). */
|
|
42
|
+
export interface ToolModelGateRule {
|
|
43
|
+
/** claude-syntax family+floor rows — meaningful only for ids whose TAIL SEGMENT matches
|
|
44
|
+
* `claude-<letters>-<digits(-digits)*>`. Version tuples compare positionally, missing positions
|
|
45
|
+
* read 0, tuple ≥ floor ⇒ gated (CC 233 comparator, verbatim semantics). Any other id shape
|
|
46
|
+
* falls through this axis entirely (fail-open). */
|
|
47
|
+
floors?: ReadonlyArray<readonly [family: string, floor: ReadonlyArray<number>]>;
|
|
48
|
+
/** Exact-id rows (explicit-id discipline): the resolved `Model.id`'s tail segment must equal the
|
|
49
|
+
* row case-insensitively. The ONLY channel by which a BYOM deployment gates its own non-claude
|
|
50
|
+
* strong model — deliberately no prefix/wildcard syntax (a substring guess against an open id
|
|
51
|
+
* set is how the `deepseek-chat` alias downgrade class of accident happens). */
|
|
52
|
+
modelIds?: ReadonlyArray<string>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The built-in gate table: gate-class → rule. CC 233 z_S floors, verbatim (evidence D2; the
|
|
56
|
+
* `mythos` row rides per the anchor). v1 vocabulary = ONE class, `"task-scaffold"` — the
|
|
57
|
+
* TodoWrite/TaskCreate/TaskGet/TaskUpdate/TaskList default bundle (`assembleCodeTools`). One class
|
|
58
|
+
* is evidence discipline, not mechanism limit: a family enters this table only with positive
|
|
59
|
+
* "strong models don't need it" evidence (a CC anchor or deployment measurement); classes are an
|
|
60
|
+
* open vocabulary and a deployment adds its own via `RunnerDeps.toolModelGate.classes`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const TOOL_MODEL_GATE_CLASSES: Readonly<Record<string, ToolModelGateRule>>;
|
|
63
|
+
/**
|
|
64
|
+
* Pure matcher: is `modelId` gated under `rule`? Every unmatched shape returns `false` (the
|
|
65
|
+
* fail-open contract in the module header). Exported for deployment pre-flight ("would my model
|
|
66
|
+
* lose the scaffold?") — the engine's own decision point calls this same function.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isModelGatedForClass(modelId: string, rule: ToolModelGateRule): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* #123 value screen for `TaskSpec.restoreGatedTools` (same posture as `assertReadFaceValue`: the
|
|
71
|
+
* VALUE is screened at the door, unconditionally — a garbage restore list must refuse loudly on
|
|
72
|
+
* every leg, never be silently read as "no restore" in the narrowing direction). Legal: absent,
|
|
73
|
+
* literal `true` (restore every class), or an array of strings (wire names; unknown names are
|
|
74
|
+
* inert by contract, like `excludeTools`).
|
|
75
|
+
*/
|
|
76
|
+
export declare function assertRestoreGatedToolsValue(value: unknown): asserts value is true | readonly string[] | undefined;
|
|
77
|
+
/** Doors-facing decision (built + applied inside `prepareConfigDoors`' synchronous stretch —
|
|
78
|
+
* decision and application share one atomic window, no drift gap). */
|
|
79
|
+
export interface ToolModelGateDecision {
|
|
80
|
+
/** Present ⇔ at least one entry was removed: the fresh, PRIVATE survivor array `spec.tools` is
|
|
81
|
+
* rebound to (a caller mutating its original live array afterwards can neither re-add a removed
|
|
82
|
+
* entry nor displace anything — there are no indices left to misalign). `undefined` = no
|
|
83
|
+
* removal, the spec is left untouched. */
|
|
84
|
+
survivors: ToolSpec[] | undefined;
|
|
85
|
+
/** gate-class → removed wire names (sorted, unique) — the removal-notice payload. */
|
|
86
|
+
removedByClass: ReadonlyMap<string, readonly string[]>;
|
|
87
|
+
/** Stamped classes with no row in the merged table — announce-once material (the loud half of
|
|
88
|
+
* fail-open: a tag typo must not silently become "never gated" with nobody told). */
|
|
89
|
+
unknownClasses: readonly string[];
|
|
90
|
+
/** An env value outside the closed set that was NOT in force (nothing this prepare would gate) —
|
|
91
|
+
* discarded-value announce material. In-force garbage never lands here: it throws. */
|
|
92
|
+
discardedEnvRaw: string | undefined;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The single decision point, called from `prepareConfigDoors` with the model id read ONCE into the
|
|
96
|
+
* decision (the Model object is mutable; decision and later reads must not diverge) and the frozen
|
|
97
|
+
* restore snapshot (never the live spec).
|
|
98
|
+
*
|
|
99
|
+
* Channel semantics (a UNION of loosening channels — any hit ⇒ don't trim; the order below is
|
|
100
|
+
* evaluation order, not priority):
|
|
101
|
+
* - `depsSeat === false` — deployment kill switch; nothing is scanned.
|
|
102
|
+
* - env `SEMA_TOOL_MODEL_GATE` `off|0|false` (case-folded) — process kill switch; `on|1|true` is
|
|
103
|
+
* the explicit default (no-op). A value outside the closed set follows the loud-bad-value
|
|
104
|
+
* dialect split (#123, the SEMA_TOOL_MATERIALIZE_STRATEGY precedent): where the value is IN
|
|
105
|
+
* FORCE — its reading would change this prepare's outcome, i.e. after folding the other
|
|
106
|
+
* channels there remains a stamped entry the gate would remove — the prepare REFUSES
|
|
107
|
+
* (`config.tool_model_gate_env_invalid`; a mistyped `off` must not silently select the
|
|
108
|
+
* narrowing arm). Everywhere else it is a DISCARDED value: announced once per process, never a
|
|
109
|
+
* veto of the configuration that outranks it.
|
|
110
|
+
* - `restoreGated === true` — every class exempt for this task; an array exempts the WHOLE class
|
|
111
|
+
* of any stamped tool it names (CC "opting into any tool of the family restores the family",
|
|
112
|
+
* judged against the FULL stamp set — a name `excludeTools` will later remove still selects its
|
|
113
|
+
* class here, while the exclusion itself stands downstream; unknown names inert).
|
|
114
|
+
* - a stamped class with no merged row ⇒ kept + reported in `unknownClasses` (fail-open, loud).
|
|
115
|
+
* - a merged rule with both axes empty ⇒ kept, silent (the documented per-class OFF).
|
|
116
|
+
* - the exclusion valve (`excludeTools`, applied downstream at the roster splice) ALWAYS wins in
|
|
117
|
+
* the result: no restore channel resurrects an excluded name.
|
|
118
|
+
*/
|
|
119
|
+
export declare function applyToolModelGate(input: {
|
|
120
|
+
tools: ReadonlyArray<ToolSpec> | undefined;
|
|
121
|
+
modelId: string;
|
|
122
|
+
depsSeat: unknown;
|
|
123
|
+
restoreGated: true | readonly string[] | undefined;
|
|
124
|
+
envRaw: string | undefined;
|
|
125
|
+
}): ToolModelGateDecision;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export function modelIdTail(id) {
|
|
2
|
+
return id.toLowerCase().split("/").pop() ?? "";
|
|
3
|
+
}
|
|
4
|
+
export const TOOL_MODEL_GATE_CLASSES = Object.freeze({
|
|
5
|
+
"task-scaffold": Object.freeze({
|
|
6
|
+
floors: Object.freeze([
|
|
7
|
+
Object.freeze(["opus", Object.freeze([4, 8])]),
|
|
8
|
+
Object.freeze(["sonnet", Object.freeze([5])]),
|
|
9
|
+
Object.freeze(["fable", Object.freeze([5])]),
|
|
10
|
+
Object.freeze(["mythos", Object.freeze([5])]),
|
|
11
|
+
]),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
const CLAUDE_ID_RE = /^claude-([a-z]+)-(\d+(?:-\d+)*)$/;
|
|
15
|
+
function versionTupleGte(version, floor) {
|
|
16
|
+
const n = Math.max(version.length, floor.length);
|
|
17
|
+
for (let i = 0; i < n; i++) {
|
|
18
|
+
const v = version[i] ?? 0;
|
|
19
|
+
const f = floor[i] ?? 0;
|
|
20
|
+
if (v > f)
|
|
21
|
+
return true;
|
|
22
|
+
if (v < f)
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
export function isModelGatedForClass(modelId, rule) {
|
|
28
|
+
const tail = modelIdTail(modelId);
|
|
29
|
+
if (rule.modelIds !== undefined) {
|
|
30
|
+
for (const row of rule.modelIds) {
|
|
31
|
+
if (typeof row === "string" && tail === row.toLowerCase())
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (rule.floors !== undefined) {
|
|
36
|
+
const m = CLAUDE_ID_RE.exec(tail);
|
|
37
|
+
if (m !== null) {
|
|
38
|
+
const family = m[1] ?? "";
|
|
39
|
+
const version = (m[2] ?? "").split("-").map(Number);
|
|
40
|
+
for (const row of rule.floors) {
|
|
41
|
+
if (!Array.isArray(row) || row.length !== 2)
|
|
42
|
+
continue;
|
|
43
|
+
const [fam, floor] = row;
|
|
44
|
+
if (typeof fam !== "string" || fam.toLowerCase() !== family)
|
|
45
|
+
continue;
|
|
46
|
+
if (Array.isArray(floor) && versionTupleGte(version, floor))
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function gateConfigError(message) {
|
|
54
|
+
const e = new Error(message);
|
|
55
|
+
e.code = "config.tool_model_gate_invalid";
|
|
56
|
+
return e;
|
|
57
|
+
}
|
|
58
|
+
export function assertRestoreGatedToolsValue(value) {
|
|
59
|
+
if (value === undefined || value === true)
|
|
60
|
+
return;
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
const entries = value;
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (typeof entry !== "string") {
|
|
65
|
+
throw gateConfigError(`TaskSpec.restoreGatedTools entries must be strings (got ${describeValue(entry)}) — an unevaluable selector must not silently restore nothing.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw gateConfigError(`TaskSpec.restoreGatedTools must be \`true\` or an array of tool names (got ${describeValue(value)}).`);
|
|
71
|
+
}
|
|
72
|
+
const isPlainObject = (v) => {
|
|
73
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
74
|
+
return false;
|
|
75
|
+
const proto = Object.getPrototypeOf(v);
|
|
76
|
+
return proto === Object.prototype || proto === null;
|
|
77
|
+
};
|
|
78
|
+
const describeValue = (v) => {
|
|
79
|
+
try {
|
|
80
|
+
const s = JSON.stringify(v);
|
|
81
|
+
return s === undefined ? String(v) : s;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return typeof v === "bigint" ? `${String(v)}n` : "[unserializable value]";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
function mergeToolModelGateClasses(depsSeat) {
|
|
88
|
+
const merged = new Map();
|
|
89
|
+
for (const [cls, rule] of Object.entries(TOOL_MODEL_GATE_CLASSES))
|
|
90
|
+
merged.set(cls, rule);
|
|
91
|
+
if (depsSeat === undefined)
|
|
92
|
+
return merged;
|
|
93
|
+
if (!isPlainObject(depsSeat)) {
|
|
94
|
+
throw gateConfigError(`RunnerDeps.toolModelGate must be \`false\` or a plain object — prototype included (got ${Array.isArray(depsSeat) ? "an array" : describeValue(depsSeat)}).`);
|
|
95
|
+
}
|
|
96
|
+
for (const key of Object.keys(depsSeat)) {
|
|
97
|
+
if (key !== "classes") {
|
|
98
|
+
throw gateConfigError(`RunnerDeps.toolModelGate has unknown key ${JSON.stringify(key)} — the only legal key is "classes".`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const classes = Object.hasOwn(depsSeat, "classes") ? depsSeat.classes : undefined;
|
|
102
|
+
if (classes === undefined)
|
|
103
|
+
return merged;
|
|
104
|
+
if (!isPlainObject(classes)) {
|
|
105
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes must be a plain object (got ${Array.isArray(classes) ? "an array" : describeValue(classes)}).`);
|
|
106
|
+
}
|
|
107
|
+
for (const [cls, row] of Object.entries(classes)) {
|
|
108
|
+
if (cls === "")
|
|
109
|
+
throw gateConfigError("RunnerDeps.toolModelGate.classes has an empty-string class name.");
|
|
110
|
+
if (row === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
if (!isPlainObject(row)) {
|
|
113
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}] must be a plain object (got ${Array.isArray(row) ? "an array" : describeValue(row)}).`);
|
|
114
|
+
}
|
|
115
|
+
for (const key of Object.keys(row)) {
|
|
116
|
+
if (key !== "floors" && key !== "modelIds") {
|
|
117
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}] has unknown key ${JSON.stringify(key)} — legal keys: "floors", "modelIds".`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let floors;
|
|
121
|
+
if (row.floors !== undefined) {
|
|
122
|
+
if (!Array.isArray(row.floors)) {
|
|
123
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors must be an array (got ${describeValue(row.floors)}).`);
|
|
124
|
+
}
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
const out = [];
|
|
127
|
+
const rows = row.floors;
|
|
128
|
+
for (const entry of rows) {
|
|
129
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
130
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors rows must be [family, floor] pairs (got ${describeValue(entry)}).`);
|
|
131
|
+
}
|
|
132
|
+
const famRaw = entry[0];
|
|
133
|
+
const floorRaw = entry[1];
|
|
134
|
+
if (typeof famRaw !== "string" || famRaw.length === 0) {
|
|
135
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors family must be a non-empty string (got ${describeValue(famRaw)}).`);
|
|
136
|
+
}
|
|
137
|
+
const fam = famRaw.toLowerCase();
|
|
138
|
+
if (!/^[a-z]+$/.test(fam)) {
|
|
139
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors family ${JSON.stringify(famRaw)} cannot ever match — the canonical id shape only admits letters. Refused rather than kept as a dead row.`);
|
|
140
|
+
}
|
|
141
|
+
if (seen.has(fam)) {
|
|
142
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors lists family ${JSON.stringify(fam)} twice.`);
|
|
143
|
+
}
|
|
144
|
+
seen.add(fam);
|
|
145
|
+
const floorNums = [];
|
|
146
|
+
if (Array.isArray(floorRaw)) {
|
|
147
|
+
const floorEntries = floorRaw;
|
|
148
|
+
for (const n of floorEntries) {
|
|
149
|
+
if (typeof n === "number" && Number.isSafeInteger(n) && n >= 0)
|
|
150
|
+
floorNums.push(n);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(floorRaw) || floorRaw.length === 0 || floorNums.length !== floorRaw.length) {
|
|
154
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors floor for ${JSON.stringify(fam)} must be a non-empty array of non-negative safe integers (got ${describeValue(floorRaw)}).`);
|
|
155
|
+
}
|
|
156
|
+
out.push([fam, floorNums]);
|
|
157
|
+
}
|
|
158
|
+
floors = out;
|
|
159
|
+
}
|
|
160
|
+
let modelIds;
|
|
161
|
+
if (row.modelIds !== undefined) {
|
|
162
|
+
if (!Array.isArray(row.modelIds)) {
|
|
163
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds must be an array (got ${describeValue(row.modelIds)}).`);
|
|
164
|
+
}
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
const out = [];
|
|
167
|
+
const ids = row.modelIds;
|
|
168
|
+
for (const entry of ids) {
|
|
169
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
170
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds entries must be non-empty strings (got ${describeValue(entry)}).`);
|
|
171
|
+
}
|
|
172
|
+
const folded = entry.toLowerCase();
|
|
173
|
+
if (folded.includes("/")) {
|
|
174
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds entry ${JSON.stringify(entry)} contains "/" — matching is against the id's TAIL segment, so a prefixed row can never fire. Write the bare id.`);
|
|
175
|
+
}
|
|
176
|
+
if (seen.has(folded)) {
|
|
177
|
+
throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds lists ${JSON.stringify(folded)} twice (case-folded).`);
|
|
178
|
+
}
|
|
179
|
+
seen.add(folded);
|
|
180
|
+
out.push(folded);
|
|
181
|
+
}
|
|
182
|
+
modelIds = out;
|
|
183
|
+
}
|
|
184
|
+
const base = merged.get(cls);
|
|
185
|
+
merged.set(cls, {
|
|
186
|
+
...(floors !== undefined ? { floors } : base?.floors !== undefined ? { floors: base.floors } : {}),
|
|
187
|
+
...(modelIds !== undefined ? { modelIds } : base?.modelIds !== undefined ? { modelIds: base.modelIds } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return merged;
|
|
191
|
+
}
|
|
192
|
+
const ENV_OFF = new Set(["off", "0", "false"]);
|
|
193
|
+
const ENV_ON = new Set(["on", "1", "true"]);
|
|
194
|
+
export function applyToolModelGate(input) {
|
|
195
|
+
const noop = (discardedEnvRaw) => ({
|
|
196
|
+
survivors: undefined,
|
|
197
|
+
removedByClass: new Map(),
|
|
198
|
+
unknownClasses: [],
|
|
199
|
+
discardedEnvRaw,
|
|
200
|
+
});
|
|
201
|
+
let merged;
|
|
202
|
+
try {
|
|
203
|
+
if (input.depsSeat !== false && input.depsSeat !== undefined && !isPlainObject(input.depsSeat)) {
|
|
204
|
+
throw gateConfigError(`RunnerDeps.toolModelGate must be \`false\` or a plain object — prototype included (got ${describeValue(input.depsSeat)}).`);
|
|
205
|
+
}
|
|
206
|
+
merged = input.depsSeat === false ? undefined : mergeToolModelGateClasses(input.depsSeat);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
let code;
|
|
210
|
+
try {
|
|
211
|
+
code = e?.code;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
code = undefined;
|
|
215
|
+
}
|
|
216
|
+
if (code !== undefined)
|
|
217
|
+
throw e;
|
|
218
|
+
let msg;
|
|
219
|
+
try {
|
|
220
|
+
msg = e instanceof Error ? e.message : String(e);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
msg = "[unreportable throw]";
|
|
224
|
+
}
|
|
225
|
+
throw gateConfigError(`RunnerDeps.toolModelGate could not be read (${msg}) — a config seat whose reads throw is a bad value, refused rather than passed.`);
|
|
226
|
+
}
|
|
227
|
+
const raw = input.envRaw;
|
|
228
|
+
let envVerdict;
|
|
229
|
+
if (raw === undefined)
|
|
230
|
+
envVerdict = "absent";
|
|
231
|
+
else {
|
|
232
|
+
const tok = raw.toLowerCase();
|
|
233
|
+
envVerdict = ENV_OFF.has(tok) ? "off" : ENV_ON.has(tok) ? "on" : "invalid";
|
|
234
|
+
}
|
|
235
|
+
const invalidRaw = envVerdict === "invalid" ? raw : undefined;
|
|
236
|
+
if (merged === undefined)
|
|
237
|
+
return noop(invalidRaw);
|
|
238
|
+
if (envVerdict === "off")
|
|
239
|
+
return noop();
|
|
240
|
+
const tools = input.tools ?? [];
|
|
241
|
+
const stamped = [];
|
|
242
|
+
for (const entry of tools) {
|
|
243
|
+
if (entry.modelGate !== undefined)
|
|
244
|
+
stamped.push({ entry, cls: entry.modelGate });
|
|
245
|
+
}
|
|
246
|
+
if (stamped.length === 0)
|
|
247
|
+
return noop(invalidRaw);
|
|
248
|
+
const restoredClasses = new Set();
|
|
249
|
+
if (input.restoreGated === true) {
|
|
250
|
+
for (const s of stamped)
|
|
251
|
+
restoredClasses.add(s.cls);
|
|
252
|
+
}
|
|
253
|
+
else if (input.restoreGated !== undefined) {
|
|
254
|
+
const names = new Set(input.restoreGated);
|
|
255
|
+
for (const s of stamped)
|
|
256
|
+
if (names.has(s.entry.name))
|
|
257
|
+
restoredClasses.add(s.cls);
|
|
258
|
+
}
|
|
259
|
+
const unknownClasses = [];
|
|
260
|
+
const unknownSeen = new Set();
|
|
261
|
+
const removed = new Set();
|
|
262
|
+
const removedByClass = new Map();
|
|
263
|
+
for (const { entry, cls } of stamped) {
|
|
264
|
+
const rule = merged.get(cls);
|
|
265
|
+
if (rule === undefined) {
|
|
266
|
+
if (!unknownSeen.has(cls)) {
|
|
267
|
+
unknownSeen.add(cls);
|
|
268
|
+
unknownClasses.push(cls);
|
|
269
|
+
}
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (rule.floors === undefined && rule.modelIds === undefined)
|
|
273
|
+
continue;
|
|
274
|
+
if (restoredClasses.has(cls))
|
|
275
|
+
continue;
|
|
276
|
+
if (!isModelGatedForClass(input.modelId, rule))
|
|
277
|
+
continue;
|
|
278
|
+
removed.add(entry);
|
|
279
|
+
const list = removedByClass.get(cls) ?? [];
|
|
280
|
+
if (!list.includes(entry.name))
|
|
281
|
+
list.push(entry.name);
|
|
282
|
+
removedByClass.set(cls, list);
|
|
283
|
+
}
|
|
284
|
+
if (invalidRaw !== undefined) {
|
|
285
|
+
if (removed.size > 0) {
|
|
286
|
+
const e = new Error(`SEMA_TOOL_MODEL_GATE=${JSON.stringify(invalidRaw)} is not in the closed set (on|1|true|off|0|false, case-insensitive) and IS in force on this task (the model gate would remove default-mounted tool(s)). Refused rather than guessed — "off" mistyped must not silently keep the trim armed.`);
|
|
287
|
+
e.code = "config.tool_model_gate_env_invalid";
|
|
288
|
+
throw e;
|
|
289
|
+
}
|
|
290
|
+
return { survivors: undefined, removedByClass: new Map(), unknownClasses, discardedEnvRaw: invalidRaw };
|
|
291
|
+
}
|
|
292
|
+
if (removed.size === 0) {
|
|
293
|
+
return { survivors: undefined, removedByClass: new Map(), unknownClasses, discardedEnvRaw: undefined };
|
|
294
|
+
}
|
|
295
|
+
for (const list of removedByClass.values())
|
|
296
|
+
list.sort();
|
|
297
|
+
return {
|
|
298
|
+
survivors: tools.filter((t) => !removed.has(t)),
|
|
299
|
+
removedByClass,
|
|
300
|
+
unknownClasses,
|
|
301
|
+
discardedEnvRaw: undefined,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -866,7 +866,7 @@ export interface AskRequest {
|
|
|
866
866
|
* run's model: a parent-thread run is told to stop and wait for the user (its transcript has a
|
|
867
867
|
* user turn coming), a delegated child is told to adapt or report the limitation. A fork is
|
|
868
868
|
* deliberately IN: it inherits the parent's authority (design/110 — which is why the RB-330
|
|
869
|
-
* `
|
|
869
|
+
* non-fork facet of `effectiveDelegationFacts`, serving the authority/context faces, excludes it), but
|
|
870
870
|
* its interaction contract is one-shot — "report once and stop … no waiting for the user"
|
|
871
871
|
* (FORK_DIRECTIVE_FRAME) — so a stop-and-wait refusal would instruct it to do the impossible
|
|
872
872
|
* (codex adversarial round, confirmed). NOT the same fact as {@link fromSubagent} either: that is
|