@sema-agent/core 7.9.2 → 7.11.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 +102 -0
- package/dist/agents/child-model-seat.d.ts +71 -14
- package/dist/agents/child-model-seat.js +23 -7
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +8 -5
- package/dist/agents/teacher.js +2 -2
- package/dist/core/ask-unresolvable-notice.d.ts +52 -0
- package/dist/core/ask-unresolvable-notice.js +25 -0
- package/dist/core/auto-mode-defaults.d.ts +15 -3
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +86 -23
- package/dist/core/auto-mode.js +43 -12
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/engine-notice.d.ts +28 -7
- package/dist/core/gate-fold.js +1 -0
- package/dist/core/gate-lanes.d.ts +6 -1
- package/dist/core/gate-lanes.js +60 -18
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +37 -1
- package/dist/core/hooks.js +2 -0
- package/dist/core/permission-rule-model.d.ts +53 -16
- package/dist/core/permission-rule-model.js +59 -21
- package/dist/core/permission-rules.d.ts +6 -4
- package/dist/core/permission-rules.js +14 -14
- package/dist/core/persisted-rule-arms.js +4 -3
- package/dist/core/read-only-shell-table.d.ts +87 -0
- package/dist/core/read-only-shell-table.js +485 -0
- package/dist/core/read-only-shell.d.ts +42 -0
- package/dist/core/read-only-shell.js +316 -0
- package/dist/core/roles.d.ts +3 -2
- package/dist/core/runner/contracts.d.ts +26 -2
- package/dist/core/runner/denial-limit-arms.d.ts +14 -3
- package/dist/core/runner/denial-limit-arms.js +15 -5
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
- package/dist/core/runner/permission-rule-lanes.js +9 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +35 -14
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
- package/dist/core/runner/prepare-gate-stations.js +12 -0
- package/dist/core/runner/prepare-policy-chain.js +7 -6
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/prepare-turn-wiring.js +1 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +8 -1
- package/dist/core/runner/runtask.d.ts +33 -32
- package/dist/core/runner/runtask.js +63 -33
- package/dist/core/runner-deps.d.ts +9 -2
- package/dist/core/shell-lexer.d.ts +18 -0
- package/dist/core/shell-lexer.js +17 -10
- package/dist/core/shell-wrapper-table.js +8 -5
- package/dist/core/swappable-deps.d.ts +90 -0
- package/dist/core/swappable-deps.js +55 -0
- package/dist/core/tool-policy.d.ts +30 -1
- package/dist/core/tool-policy.js +6 -2
- package/dist/core/tools.d.ts +28 -7
- package/dist/core/tools.js +44 -4
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/wiring-manifest.d.ts +15 -1
- package/dist/core/wiring-manifest.js +10 -2
- package/dist/core/workflow-journal-store.d.ts +21 -2
- package/dist/core/workflow-journal-store.js +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/engine/harness/agent-harness.d.ts +3 -1
- package/dist/engine/harness/agent-harness.js +1 -1
- package/dist/engine/harness/types.d.ts +15 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -3
- package/dist/orchestration/run-workflow-tool.d.ts +22 -2
- package/dist/orchestration/run-workflow-tool.js +14 -1
- package/dist/orchestration/workflow-governance.d.ts +3 -2
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-observe.js +2 -0
- package/dist/orchestration/workflow-primitives.d.ts +4 -1
- package/dist/orchestration/workflow-primitives.js +1 -6
- package/dist/orchestration/workflow-types.d.ts +37 -2
- package/dist/orchestration/workflow-types.js +16 -0
- package/dist/orchestration/workflow.d.ts +53 -6
- package/dist/orchestration/workflow.js +319 -51
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +85 -5
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { deploymentSubagentTierModel, forkGovernanceDenial } from "../../agents/subagent.js";
|
|
3
|
+
import { childThinkingSeat, deploymentSubagentThinking, deploymentSubagentTierModel, forkGovernanceDenial } from "../../agents/subagent.js";
|
|
4
4
|
import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelope.js";
|
|
5
5
|
import { createRunWorkflowTool } from "../../orchestration/run-workflow-tool.js";
|
|
6
6
|
import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
|
|
7
7
|
import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
|
|
8
8
|
import { thinkingOffExpressible } from "../../brain/reasoning.js";
|
|
9
9
|
import { autoModeArmingRecipeOf } from "../auto-mode-arming.js";
|
|
10
|
-
import { AUTO_MODE_CLASSIFIER_MAX_TOKENS } from "../auto-mode-defaults.js";
|
|
10
|
+
import { AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS } from "../auto-mode-defaults.js";
|
|
11
11
|
import { buildAutoModePrompt, renderAutoModeAction, renderAutoModeWindow } from "../auto-mode-prompt.js";
|
|
12
12
|
import { createAutoModeDecider, createAutoModeDenialTracker } from "../auto-mode.js";
|
|
13
13
|
import { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
@@ -17,9 +17,20 @@ import { resolveTaskModel } from "../roles.js";
|
|
|
17
17
|
import { brainToRuntime } from "../runtime.js";
|
|
18
18
|
import { defaultTaskRegistry } from "../task-registry.js";
|
|
19
19
|
import { emitTrace } from "../trace.js";
|
|
20
|
-
import { defineTool, isDefineToolProduct } from "../tools.js";
|
|
20
|
+
import { defineTool, isDefineToolProduct, rebindDefineToolCtx } from "../tools.js";
|
|
21
21
|
import { derivedRouteFallsBack } from "./derived-route-fallback.js";
|
|
22
22
|
import { REPORT_FINDINGS_TOOL_NAME, createReportBlockedTool, createReportFindingsTool } from "./synthetic-tools.js";
|
|
23
|
+
function classifierReplyText(reply) {
|
|
24
|
+
return reply.content
|
|
25
|
+
.filter((c) => c.type === "text")
|
|
26
|
+
.map((c) => c.text)
|
|
27
|
+
.join("");
|
|
28
|
+
}
|
|
29
|
+
function classifierReplyEmptyAtCap(reply) {
|
|
30
|
+
if (classifierReplyText(reply) !== "")
|
|
31
|
+
return false;
|
|
32
|
+
return reply.stopReason === "length" || (reply.stopReason === "error" && reply.errorKind === "length_empty");
|
|
33
|
+
}
|
|
23
34
|
import { RosterBuilder, callerMountSource } from "../tool-roster.js";
|
|
24
35
|
function assembleParentCaptureState(o, i, ctl, ancestors) {
|
|
25
36
|
const build = (optedOut, indeterminate) => ({
|
|
@@ -72,9 +83,8 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
72
83
|
roster.mount(mounted, { source: callerMountSource(mounted), mountedBy: "scenario" });
|
|
73
84
|
}
|
|
74
85
|
function mountCallerTool(t) {
|
|
75
|
-
if (isDefineToolProduct(t))
|
|
76
|
-
return maybeOffload(t, t);
|
|
77
|
-
}
|
|
86
|
+
if (isDefineToolProduct(t))
|
|
87
|
+
return maybeOffload(rebindDefineToolCtx(t, enrichSpecToolCtx), t);
|
|
78
88
|
return maybeOffload(defineTool({
|
|
79
89
|
...t,
|
|
80
90
|
execute: (args, ctx) => t.execute(args, enrichSpecToolCtx(ctx)),
|
|
@@ -170,7 +180,10 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
170
180
|
autoModeDecider = createAutoModeDecider({
|
|
171
181
|
...(am.timeoutMs !== undefined ? { timeoutMs: am.timeoutMs } : {}),
|
|
172
182
|
...(am.failureThreshold !== undefined ? { failureThreshold: am.failureThreshold } : {}),
|
|
173
|
-
|
|
183
|
+
onBreakerOpen: (info) => {
|
|
184
|
+
internals?.autoModeBreakerLedger?.record(sessionId, { openedAtMs: Date.now(), lastCause: info.lastCause, failures: info.consecutiveFailures, runId });
|
|
185
|
+
am.onBreakerOpen?.(info);
|
|
186
|
+
},
|
|
174
187
|
onClassified: (info) => emitTrace(deps.tracer, () => ({
|
|
175
188
|
kind: "auto_mode.classified",
|
|
176
189
|
version: 1,
|
|
@@ -188,17 +201,18 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
188
201
|
(m.role === "user" || m.role === "assistant" || m.role === "toolResult"));
|
|
189
202
|
const userPrompt = renderAutoModeWindow(known, am.window) + renderAutoModeAction(input);
|
|
190
203
|
const classifierAuth = await spec.getApiKeyAndHeaders?.(classifierModel);
|
|
191
|
-
const
|
|
204
|
+
const request = (cap) => classifierRuntime.completeSimple(classifierModel, { systemPrompt: classifierSystemPrompt, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] }, {
|
|
192
205
|
signal,
|
|
193
206
|
reasoning: "off",
|
|
194
|
-
...
|
|
207
|
+
...cap,
|
|
195
208
|
...(classifierAuth?.apiKey !== undefined ? { apiKey: classifierAuth.apiKey } : {}),
|
|
196
209
|
...(classifierAuth?.headers !== undefined ? { headers: classifierAuth.headers } : {}),
|
|
197
210
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
211
|
+
let response = await request(classifierCap);
|
|
212
|
+
if (classifierCap.maxTokens !== undefined && classifierReplyEmptyAtCap(response)) {
|
|
213
|
+
response = await request({ maxTokens: classifierCap.maxTokens + AUTO_MODE_CLASSIFIER_THINKING_ALLOWANCE_TOKENS });
|
|
214
|
+
}
|
|
215
|
+
return classifierReplyText(response);
|
|
202
216
|
},
|
|
203
217
|
});
|
|
204
218
|
if (am.persistArming === true)
|
|
@@ -246,6 +260,13 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
246
260
|
parentModel: () => deploymentSubagentTierModel(runnerSelf.agentCatalog) ?? harnessRef.current?.getModel(),
|
|
247
261
|
...(spec.getApiKeyAndHeaders !== undefined ? { parentGetApiKeyAndHeaders: spec.getApiKeyAndHeaders } : {}),
|
|
248
262
|
...(frozenOnAsk !== undefined ? { parentOnAsk: frozenOnAsk } : {}),
|
|
263
|
+
...(spec.durableApproval !== undefined ? { parentDurableApproval: { ...spec.durableApproval } } : {}),
|
|
264
|
+
...(internals?.workflowParkedResume !== undefined
|
|
265
|
+
? { parkedResume: (resumeFromRunId) => {
|
|
266
|
+
const mine = internals.workflowParkedResume.filter((d) => d.runId === resumeFromRunId).map(({ runId: _r, ...rest }) => rest);
|
|
267
|
+
return mine.length > 0 ? mine : undefined;
|
|
268
|
+
} }
|
|
269
|
+
: {}),
|
|
249
270
|
principal: spec.principal,
|
|
250
271
|
oneShot: spec.oneShot,
|
|
251
272
|
...(resolvedInteractionPosture !== undefined ? { parentInteractionPosture: resolvedInteractionPosture } : {}),
|
|
@@ -258,7 +279,7 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
258
279
|
autoModeReview: () => (autoModeDecider !== undefined ? { decider: autoModeDecider } : undefined),
|
|
259
280
|
workflowDepth: internals?.workflowDepth,
|
|
260
281
|
parentCwd: taskRootFinal,
|
|
261
|
-
parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
|
|
282
|
+
parentThinking: () => childThinkingSeat(undefined, deploymentSubagentThinking(runnerSelf.agentCatalog?.roles), harnessRef.current?.getThinkingLevel() ?? thinking).thinking,
|
|
262
283
|
parentReadFace: () => carrierReadFace(),
|
|
263
284
|
parentReadDenyPatterns: () => {
|
|
264
285
|
const readDenyAdditionsNormalized = input.readDenyAdditionsNormalized();
|
|
@@ -49,8 +49,9 @@ export interface PrepareGateStationsInput {
|
|
|
49
49
|
* first-party tool's declared effect (never overrides a scanned one); the plan-mode deny reads it per call. */
|
|
50
50
|
toolEffects: Map<string, ToolEffect>;
|
|
51
51
|
/** borrowed-readonly — the deployment seats these stations read, as a Pick over the SAME `deps` object (receiver
|
|
52
|
-
* preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face
|
|
53
|
-
|
|
52
|
+
* preserved for `deps.onError?.()`): the write-protection table and its data root, the tracer, the error face, and
|
|
53
|
+
* the notice sink (#648: the gate's unresolvable-ask exit delivers `delegation.ask_unresolvable` through it). */
|
|
54
|
+
deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "tracer" | "onError" | "onNotice">;
|
|
54
55
|
/** borrowed-mutable — the protocol-tools phase's arming belt. Writer here: `armed` ← the registration predicate, the
|
|
55
56
|
* one write; the refresh seam reads it. */
|
|
56
57
|
toolCallGateArmedRef: {
|
|
@@ -5,6 +5,8 @@ import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
|
5
5
|
import { screenGateOutcome } from "../gate-outcome.js";
|
|
6
6
|
import { directDeny } from "./gate-exit.js";
|
|
7
7
|
import { emitTrace } from "../trace.js";
|
|
8
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
9
|
+
import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
|
|
8
10
|
import { createWriteProtectionCheck } from "../write-protect.js";
|
|
9
11
|
import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
|
|
10
12
|
import { toolCallFaceOf } from "../tool-roster.js";
|
|
@@ -120,6 +122,7 @@ export function prepareGateStations(input) {
|
|
|
120
122
|
shellGated: (e.toolName === "Bash" && shellGatedBash) || (e.toolName === "Monitor" && shellGatedMonitor),
|
|
121
123
|
...(autoModeDecider ? { autoMode: { decider: autoModeDecider, ...(autoModeDenialTracking !== undefined ? { denialTracking: autoModeDenialTracking } : {}) } } : {}),
|
|
122
124
|
onHeadlessDenialLimit: stopForDenialLimit,
|
|
125
|
+
onAskUnresolvable: (info) => deliverEngineNotice(deps.onNotice, askUnresolvableNotice({ sessionId, ...info })),
|
|
123
126
|
...(permissionRuleLane
|
|
124
127
|
? {
|
|
125
128
|
persistedRules: {
|
|
@@ -136,6 +139,15 @@ export function prepareGateStations(input) {
|
|
|
136
139
|
},
|
|
137
140
|
}
|
|
138
141
|
: {}),
|
|
142
|
+
onReadOnlyAllowed: (info) => emitTrace(deps.tracer, () => ({
|
|
143
|
+
kind: "permission.read_only_allowed",
|
|
144
|
+
version: 1,
|
|
145
|
+
taskId: hostTaskId,
|
|
146
|
+
toolName: info.toolName,
|
|
147
|
+
toolCallId: info.toolCallId,
|
|
148
|
+
command: info.command,
|
|
149
|
+
ts: Date.now(),
|
|
150
|
+
})),
|
|
139
151
|
...(permissionRuleOrgLane
|
|
140
152
|
? {
|
|
141
153
|
orgRules: {
|
|
@@ -252,6 +252,7 @@ export async function preparePolicyChain(input) {
|
|
|
252
252
|
sessionId,
|
|
253
253
|
root: taskRootFinal,
|
|
254
254
|
liveCwd: () => handsCwdRef?.current,
|
|
255
|
+
home: executionEnv.homeDir,
|
|
255
256
|
questionToolMounted,
|
|
256
257
|
questionToolName: ASK_USER_QUESTION_TOOL_NAME,
|
|
257
258
|
onRevisionDefect: (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }),
|
|
@@ -299,7 +300,7 @@ export async function preparePolicyChain(input) {
|
|
|
299
300
|
return { action: "allow", updatedInput: editArgs };
|
|
300
301
|
if (judged.kind === "deny")
|
|
301
302
|
return judged.result;
|
|
302
|
-
const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere
|
|
303
|
+
const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
|
|
303
304
|
re = editAsk;
|
|
304
305
|
const rr = await resolveAsk({
|
|
305
306
|
toolName: creq.toolName,
|
|
@@ -311,7 +312,7 @@ export async function preparePolicyChain(input) {
|
|
|
311
312
|
...riskAxesOf(creq.toolName),
|
|
312
313
|
...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
313
314
|
...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
|
|
314
|
-
...inheritedAskCarry(
|
|
315
|
+
...inheritedAskCarry(judged, isLiveApproverSeat(onAskOf), ancestorTracker),
|
|
315
316
|
ruleEvidence: inheritedAskEvidence,
|
|
316
317
|
}, onAskOf, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
317
318
|
settleDenialLimitFallback({ fallback: editFallback, mintedHere: editMintedHere, tracker: ancestorTracker, resolved: rr, headless: headlessDenyAtRecheck, stop: stopForDenialLimit, toolName: creq.toolName, toolCallId: creq.toolCallId });
|
|
@@ -372,7 +373,7 @@ export async function preparePolicyChain(input) {
|
|
|
372
373
|
return { action: "allow" };
|
|
373
374
|
if (judged.kind === "deny")
|
|
374
375
|
return judged.result;
|
|
375
|
-
const { ask: inheritedAsk, fallback, mintedHere
|
|
376
|
+
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
376
377
|
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
|
|
377
378
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
378
379
|
return { action: "allow" };
|
|
@@ -389,7 +390,7 @@ export async function preparePolicyChain(input) {
|
|
|
389
390
|
...riskAxesOf(creq.toolName),
|
|
390
391
|
...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
391
392
|
...(inheritedAsk.action === "ask" && inheritedAsk.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: inheritedAsk.persistedRuleShadowed } : {}),
|
|
392
|
-
...inheritedAskCarry(
|
|
393
|
+
...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
|
|
393
394
|
ruleEvidence: inheritedAskEvidence,
|
|
394
395
|
}, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
395
396
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
@@ -452,7 +453,7 @@ export async function preparePolicyChain(input) {
|
|
|
452
453
|
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
|
|
453
454
|
if (judged.kind === "deny")
|
|
454
455
|
return judged.result;
|
|
455
|
-
const { ask: inheritedAsk, fallback, mintedHere
|
|
456
|
+
const { ask: inheritedAsk, fallback, mintedHere } = judged;
|
|
456
457
|
if (fallback === undefined && sandboxAdmissionArmed && policyAskClassOf(pc.policy) === "sandbox_local" && !sandboxBoundaryCapable(creq.toolName)) {
|
|
457
458
|
recordAncestorSandboxAdmission(creq.toolCallId, creq.toolName);
|
|
458
459
|
return decision.updatedInput !== undefined ? { action: "allow", updatedInput: decision.updatedInput } : { action: "allow" };
|
|
@@ -468,7 +469,7 @@ export async function preparePolicyChain(input) {
|
|
|
468
469
|
...riskAxesOf(creq.toolName),
|
|
469
470
|
...(inheritedAsk.action === "ask" && inheritedAsk.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
470
471
|
...(inheritedAsk.action === "ask" && inheritedAsk.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: inheritedAsk.persistedRuleShadowed } : {}),
|
|
471
|
-
...inheritedAskCarry(
|
|
472
|
+
...inheritedAskCarry(judged, isLiveApproverSeat(pc.onAsk), pc.autoMode?.denialTracking),
|
|
472
473
|
ruleEvidence: inheritedAskEvidence,
|
|
473
474
|
}, pc.onAsk, csignal ?? abortController.signal, lateAskSettlementObserver({ toolName: creq.toolName, toolCallId: creq.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
|
|
474
475
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
@@ -634,7 +634,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
634
634
|
session,
|
|
635
635
|
tools: [...harnessTools],
|
|
636
636
|
model,
|
|
637
|
-
thinkingLevel: thinking
|
|
637
|
+
thinkingLevel: thinking,
|
|
638
638
|
systemPrompt: systemPromptSeat.current,
|
|
639
639
|
...(systemBlocks ? { systemBlocks } : {}),
|
|
640
640
|
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
@@ -26,7 +26,7 @@ export function prepareTurnWiring(input) {
|
|
|
26
26
|
layoutGroups: (systemBlocks ?? [{ groupId: "body", cacheControlBoundary: true }]).map((b) => ({ groupId: b.groupId, cacheControlBoundary: b.cacheControlBoundary })),
|
|
27
27
|
stableSystemText: systemPromptSeat.current,
|
|
28
28
|
toolWire: toolsToFingerprintInputs(harnessTools),
|
|
29
|
-
thinkingLevel:
|
|
29
|
+
thinkingLevel: thinking ?? null,
|
|
30
30
|
maxTokens: model.maxTokens,
|
|
31
31
|
hasOutputSchema: spec.outputSchema !== undefined,
|
|
32
32
|
sessionId,
|
|
@@ -46,7 +46,7 @@ export interface PrepareWiringManifestInput {
|
|
|
46
46
|
deps: RunnerDeps;
|
|
47
47
|
/** borrowed-readonly — the trusted spawn-side channel: `questionFaceStripped`, `isDelegatedChild`, `onTaskNotification`,
|
|
48
48
|
* `peerSelfRef`, `explicitAgentName` (the drain), `insideFork` / `agentName` / `parentToolCallId` (the identity). */
|
|
49
|
-
internals: Pick<RunInternals, "questionFaceStripped" | "isDelegatedChild" | "onTaskNotification" | "peerSelfRef" | "explicitAgentName" | "insideFork" | "agentName" | "parentToolCallId"> | undefined;
|
|
49
|
+
internals: Pick<RunInternals, "questionFaceStripped" | "isDelegatedChild" | "onTaskNotification" | "peerSelfRef" | "explicitAgentName" | "insideFork" | "agentName" | "parentToolCallId" | "autoModeBreakerLedger"> | undefined;
|
|
50
50
|
/** borrowed-readonly — the resume leg, or undefined (the manifest's leg derivation reads presence only). */
|
|
51
51
|
resume: Pick<PrepareResume, "seed"> | undefined;
|
|
52
52
|
/** borrowed-readonly — the acquired session id. */
|
|
@@ -199,7 +199,14 @@ export function prepareWiringManifest(input) {
|
|
|
199
199
|
memoryAdmissionWired: deps.memoryScopeAdmission !== undefined,
|
|
200
200
|
retentionPolicyWired: deps.retentionPolicy !== undefined,
|
|
201
201
|
...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
|
|
202
|
-
autoMode: {
|
|
202
|
+
autoMode: {
|
|
203
|
+
armed: autoModeArmReason === "armed",
|
|
204
|
+
reason: autoModeArmReason,
|
|
205
|
+
...(() => {
|
|
206
|
+
const trip = internals?.autoModeBreakerLedger?.lastTrip(sessionId);
|
|
207
|
+
return trip !== undefined ? { breaker: trip } : {};
|
|
208
|
+
})(),
|
|
209
|
+
},
|
|
203
210
|
mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
|
|
204
211
|
tools: toolRoster,
|
|
205
212
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type Model } from "../../internal/llm.js";
|
|
2
2
|
import { type CheckpointToken, type ResumeOutcome } from "../checkpoint-store.js";
|
|
3
3
|
import { type TaskOutcome } from "../task-outcome.js";
|
|
4
|
+
import { type SwappableDeps } from "../swappable-deps.js";
|
|
4
5
|
import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
|
|
5
6
|
import type { SessionStore } from "../session.js";
|
|
6
|
-
import type { AgentDefinition,
|
|
7
|
+
import type { AgentDefinition, ModelRoles, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
|
|
7
8
|
import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerSelfSeat } from "./contracts.js";
|
|
8
9
|
export type { ResumeTaskConfig } from "./contracts.js";
|
|
9
10
|
/**
|
|
@@ -20,6 +21,9 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
20
21
|
* authority: a durable resume's checkpoint seed wins on its leg, and a miss (cross-process,
|
|
21
22
|
* evicted) falls back to the transcript-replay rung. See {@link SessionReadFileStates}. */
|
|
22
23
|
private readonly sessionReadStates;
|
|
24
|
+
/** #616 — the per-session breaker read face's record (see {@link RunInternals.autoModeBreakerLedger}). Runner-lived:
|
|
25
|
+
* a trip in one leg is what the session's next leg reports. */
|
|
26
|
+
private readonly autoModeBreakerLedger;
|
|
23
27
|
/** task notifications born BETWEEN turns (run torn down / harness already idle), parked
|
|
24
28
|
* per session and drained into the session's next run at its first boundary. Runner-lived (outlives any
|
|
25
29
|
* single run, like the registry handles that produce into it); bounded + drop-disclosing, see
|
|
@@ -57,7 +61,7 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
57
61
|
private readonly locallyClaimedTokens;
|
|
58
62
|
private static readonly PARENT_CONSTRAINT_REGISTRY_CAP;
|
|
59
63
|
/** The lanes' LIVE view of `deps` ({@link RunnerDepsSeat}): a getter, never a captured object, because
|
|
60
|
-
* `
|
|
64
|
+
* `swapDeps` replaces the object and a rung reading after its own await must see the replacement. */
|
|
61
65
|
private readonly depsSeat;
|
|
62
66
|
constructor(deps: RunnerDeps);
|
|
63
67
|
/** design/73 §1 v1 — fire `RunnerDeps.onTaskOutcome` through the single swallow-guarded chokepoint
|
|
@@ -88,38 +92,35 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
88
92
|
/** Acquire the lock for a sessionId; returns a release fn. New (undefined) sessions need no lock. */
|
|
89
93
|
private acquireSessionLock;
|
|
90
94
|
/**
|
|
91
|
-
* Hot-swap the
|
|
92
|
-
*
|
|
93
|
-
*
|
|
95
|
+
* Hot-swap the deployment seats a running Runner may change without a restart — the ONE door for every
|
|
96
|
+
* hot-swappable seat ({@link SWAPPABLE_DEP_SEATS}: the model catalog generation `models` + its `tiers`
|
|
97
|
+
* bindings, and the deployment's read-face declaration `readFace`), and the ONLY sanctioned way to
|
|
98
|
+
* change any of them after construction. It replaces the former `swapModels` verb (retired, no alias):
|
|
99
|
+
* one verb per seat restated the same laws with a list of verbs as the only record of what is swappable.
|
|
94
100
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* -
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* the notice directory) for same-name entries whose URL moved while the Model-visible
|
|
117
|
-
* credential fingerprint did not.
|
|
101
|
+
* The laws, stated once (see the vocabulary module for the seat-by-seat meaning):
|
|
102
|
+
* - **Screened first, written once — atomic.** The candidate is screened whole
|
|
103
|
+
* ({@link screenSwappableDeps}: the key set is the closed one, at least one seat, `tiers` rides with
|
|
104
|
+
* `models`, each seat's own value gate — `readFace` through the very `assertReadFaceValue` every
|
|
105
|
+
* prepare door applies) and the catalog is tier-expanded on the candidate (an illegal binding throws
|
|
106
|
+
* exactly like the constructor's boot error) BEFORE the deps object is replaced. On any throw the
|
|
107
|
+
* current deps stay in force, byte-identical.
|
|
108
|
+
* - **In-flight legs keep their snapshot.** A running task read `this.deps` at its prepare and holds
|
|
109
|
+
* that object: its `Model`, its resolved read face, everything. The swap changes what FUTURE prepares
|
|
110
|
+
* (and auto-sourced agents' string refs — {@link agentCatalog} copies per read) resolve. Same-name
|
|
111
|
+
* redirects never re-route or re-price a task mid-run; a read-face change never re-fences a mounted
|
|
112
|
+
* band; the divergence window is exactly the in-flight legs' lifetime, by design.
|
|
113
|
+
* - **Presence is the instruction.** A key present on the candidate is replaced (an explicit
|
|
114
|
+
* `undefined` clears an optional seat: `tiers: undefined` ⇒ no bindings, `readFace: undefined` ⇒ the
|
|
115
|
+
* engine default); a key absent is kept. `tiers` omitted beside `models` ⇒ the current bindings are
|
|
116
|
+
* re-applied over the new catalog.
|
|
117
|
+
* - **Announced per seat.** `config.models_swapped` (models/tiers counts — key material only, never the
|
|
118
|
+
* catalog itself; plus the advisory `route.base_url_changed_key_unchanged` for same-name entries
|
|
119
|
+
* whose URL moved while the Model-visible credential fingerprint did not) and
|
|
120
|
+
* `config.read_face_swapped` (`{ from, to, occurrenceId }`, the seat's previous and new value, `null`
|
|
121
|
+
* = no declaration) — the table {@link SWAP_SEAT_NOTICE} binds every seat to its line.
|
|
118
122
|
*/
|
|
119
|
-
|
|
120
|
-
models: Record<string, Model>;
|
|
121
|
-
tiers?: Record<string, ModelRef>;
|
|
122
|
-
}): void;
|
|
123
|
+
swapDeps(next: SwappableDeps): void;
|
|
123
124
|
/** one-shot brain-routed utility query (see {@link runSideQuery} for the full contract):
|
|
124
125
|
* preserves system/multi-turn messages/tool DEFINITIONS, routes through the deployment's brain with
|
|
125
126
|
* the same model resolution as tasks, returns real usage/model. No session, no tool execution, no
|
|
@@ -23,6 +23,8 @@ import { resolveReasoning } from "../../brain/reasoning.js";
|
|
|
23
23
|
import { adjudicateDerivedRoute, authCarrierFingerprint, fallbackToPrimaryNotice, normalizeBaseUrl, sameRouteIdentity } from "../../brain/route-adjudicator.js";
|
|
24
24
|
import { runWithBrainTelemetry, runWithReasoningWireFacts, runWithStatusSink } from "../../brain/status-sink.js";
|
|
25
25
|
import { expandTiers, resolveModel, resolveTaskModel } from "../roles.js";
|
|
26
|
+
import { screenSwappableDeps } from "../swappable-deps.js";
|
|
27
|
+
import { AutoModeBreakerLedger } from "../auto-mode.js";
|
|
26
28
|
import { runSideQuery } from "../side-query.js";
|
|
27
29
|
import { generatePromptSuggestions } from "./prompt-suggestions.js";
|
|
28
30
|
import { PushQueue } from "../push-queue.js";
|
|
@@ -82,6 +84,7 @@ export class Runner {
|
|
|
82
84
|
sessions;
|
|
83
85
|
sessionLocks = new Map();
|
|
84
86
|
sessionReadStates = new SessionReadFileStates();
|
|
87
|
+
autoModeBreakerLedger = new AutoModeBreakerLedger();
|
|
85
88
|
pendingSessionNotifications = new PendingSessionNotifications();
|
|
86
89
|
parentConstraintRegistry = new Map();
|
|
87
90
|
suspendedEnvReaps = new Map();
|
|
@@ -213,41 +216,67 @@ export class Runner {
|
|
|
213
216
|
});
|
|
214
217
|
};
|
|
215
218
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
219
|
+
swapDeps(next) {
|
|
220
|
+
const screened = screenSwappableDeps(next);
|
|
221
|
+
const { seats, candidate } = screened;
|
|
222
|
+
const catalogSwap = seats.includes("models");
|
|
223
|
+
let expanded;
|
|
224
|
+
let tiers;
|
|
222
225
|
const movedEntries = [];
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
226
|
+
if (catalogSwap) {
|
|
227
|
+
const models = candidate.models;
|
|
228
|
+
tiers = Object.hasOwn(candidate, "tiers") ? candidate.tiers : this.deps.tiers;
|
|
229
|
+
expanded = tiers && Object.keys(tiers).length > 0 ? expandTiers({ ...models }, tiers) : { ...models };
|
|
230
|
+
for (const [name, nextModel] of Object.entries(expanded ?? {})) {
|
|
231
|
+
const prior = this.deps.models?.[name];
|
|
232
|
+
if (!prior)
|
|
233
|
+
continue;
|
|
234
|
+
const from = normalizeBaseUrl(prior.baseUrl);
|
|
235
|
+
const to = normalizeBaseUrl(nextModel.baseUrl);
|
|
236
|
+
if (from !== to && authCarrierFingerprint(prior.headers) === authCarrierFingerprint(nextModel.headers)) {
|
|
237
|
+
movedEntries.push({ modelId: name, from, to });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const faceSwap = seats.includes("readFace");
|
|
242
|
+
const priorFace = this.deps.readFace;
|
|
243
|
+
const written = {
|
|
244
|
+
...this.deps,
|
|
245
|
+
...(expanded !== undefined ? { models: expanded } : {}),
|
|
246
|
+
...(catalogSwap && tiers !== undefined ? { tiers } : {}),
|
|
247
|
+
...(faceSwap && candidate.readFace !== undefined ? { readFace: candidate.readFace } : {}),
|
|
248
|
+
};
|
|
249
|
+
if (catalogSwap && tiers === undefined)
|
|
250
|
+
delete written.tiers;
|
|
251
|
+
if (faceSwap && candidate.readFace === undefined)
|
|
252
|
+
delete written.readFace;
|
|
253
|
+
this.deps = written;
|
|
254
|
+
if (catalogSwap) {
|
|
255
|
+
const modelCount = Object.keys(candidate.models).length;
|
|
256
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
257
|
+
code: "config.models_swapped",
|
|
258
|
+
message: `model catalog swapped: ${modelCount} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`,
|
|
259
|
+
detail: { models: modelCount, tiers: tiers ? Object.keys(tiers).length : 0, occurrenceId: uuidv7() },
|
|
260
|
+
});
|
|
261
|
+
if (movedEntries.length > 0) {
|
|
262
|
+
const RENDER_CAP = 8;
|
|
263
|
+
const rendered = movedEntries.slice(0, RENDER_CAP);
|
|
264
|
+
deliverEngineNotice(this.deps.onNotice, {
|
|
265
|
+
code: "route.base_url_changed_key_unchanged",
|
|
266
|
+
message: `model catalog swap moved ${movedEntries.length} entry/entries to a new baseUrl while the Model-visible credential half stayed unchanged: ` +
|
|
267
|
+
rendered.map((e) => `"${e.modelId}" ${e.from || "(config root)"} → ${e.to || "(config root)"}`).join("; ") +
|
|
268
|
+
(movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
|
|
269
|
+
" — if the provider changed (not just its domain), update the credential reference in the same step",
|
|
270
|
+
detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (faceSwap) {
|
|
275
|
+
const to = candidate.readFace ?? null;
|
|
244
276
|
deliverEngineNotice(this.deps.onNotice, {
|
|
245
|
-
code: "
|
|
246
|
-
message: `
|
|
247
|
-
|
|
248
|
-
(movedEntries.length > rendered.length ? `; +${movedEntries.length - rendered.length} more` : "") +
|
|
249
|
-
" — if the provider changed (not just its domain), update the credential reference in the same step",
|
|
250
|
-
detail: { entries: rendered, total: movedEntries.length, occurrenceId: uuidv7() },
|
|
277
|
+
code: "config.read_face_swapped",
|
|
278
|
+
message: `deployment read face swapped: ${priorFace ?? "(no declaration)"} → ${to ?? "(no declaration)"}; in-flight legs keep the face they prepared under, new legs resolve against the new declaration`,
|
|
279
|
+
detail: { from: priorFace ?? null, to, occurrenceId: uuidv7() },
|
|
251
280
|
});
|
|
252
281
|
}
|
|
253
282
|
}
|
|
@@ -962,6 +991,7 @@ export class Runner {
|
|
|
962
991
|
peerInboundChainRef,
|
|
963
992
|
detachHub,
|
|
964
993
|
sessionReadStates: this.sessionReadStates,
|
|
994
|
+
autoModeBreakerLedger: this.autoModeBreakerLedger,
|
|
965
995
|
onTaskNotification: (notification, opts) => {
|
|
966
996
|
try {
|
|
967
997
|
upstreamTaskNotification?.(notification, opts);
|
|
@@ -400,6 +400,11 @@ export interface RunnerDeps {
|
|
|
400
400
|
* may not. Beside a read-only (verifier) mount this seat's "open" silently CLAMPS to roots (a
|
|
401
401
|
* deployment default cannot override a load-bearing containment wall) — TaskSpec.readFace's own
|
|
402
402
|
* "open" there is the genuine per-task contradiction, and that one still refuses loudly (#123).
|
|
403
|
+
*
|
|
404
|
+
* A LIVE seat: `Runner.swapDeps({ readFace })` replaces it without a restart (the same natural
|
|
405
|
+
* snapshot the model catalog has — a leg reads the seat at its prepare and keeps that reading; every
|
|
406
|
+
* later prepare reads the new declaration; announced as `config.read_face_swapped`). A host whose
|
|
407
|
+
* declaration arrives late (a governance source read after boot) swaps it in rather than restarting.
|
|
403
408
|
*/
|
|
404
409
|
readFace?: import("../tools/fs/read-face.js").ReadFace;
|
|
405
410
|
/**
|
|
@@ -686,10 +691,12 @@ export interface RunnerDeps {
|
|
|
686
691
|
timeoutMs?: number;
|
|
687
692
|
/** Consecutive-failure threshold opening the one-way session breaker (default 3). */
|
|
688
693
|
failureThreshold?: number;
|
|
689
|
-
/** Fired ONCE when the session breaker opens (the "退回非 auto" alarm — surface it to the operator).
|
|
694
|
+
/** Fired ONCE when the session breaker opens (the "退回非 auto" alarm — surface it to the operator). The engine
|
|
695
|
+
* records the trip on the session's breaker read face (`WiringManifest.autoMode.breaker`, #616) BEFORE calling
|
|
696
|
+
* this hook, so a deployment that wires nothing here still gets the read face. */
|
|
690
697
|
onBreakerOpen?: (info: {
|
|
691
698
|
consecutiveFailures: number;
|
|
692
|
-
lastCause:
|
|
699
|
+
lastCause: import("./auto-mode.js").AutoModeBreakerCause;
|
|
693
700
|
}) => void;
|
|
694
701
|
/**
|
|
695
702
|
* The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
|
|
@@ -9,10 +9,20 @@ export interface ShellWord {
|
|
|
9
9
|
* yield zero, one or several words. */
|
|
10
10
|
readonly expands: false | "one" | "many";
|
|
11
11
|
}
|
|
12
|
+
/** One redirection of a segment, as read: the operator and its operand word (a heredoc's operand is its
|
|
13
|
+
* delimiter; the IO number or `{name}` before the operator is folded into the operator's meaning and not
|
|
14
|
+
* kept). The tightening reader never reads these — a rule speaks about program runs; the read-only
|
|
15
|
+
* reader does (an output redirection is a write). */
|
|
16
|
+
export interface ShellRedirection {
|
|
17
|
+
readonly op: string;
|
|
18
|
+
readonly target: ShellWord;
|
|
19
|
+
}
|
|
12
20
|
/** One program run the lexer found, after keyword stripping. */
|
|
13
21
|
export interface ShellSegment {
|
|
14
22
|
/** The run as SPELLED (a leading keyword removed): the first candidate a rule is compared against. */
|
|
15
23
|
readonly argv: readonly ShellWord[];
|
|
24
|
+
/** The segment's redirections in source order (see {@link ShellRedirection}); empty when it has none. */
|
|
25
|
+
readonly redirections: readonly ShellRedirection[];
|
|
16
26
|
/** The deeper candidates, one per peeled layer (leading assignments, then each wrapper of the table):
|
|
17
27
|
* `sudo -u root rm -r x` ⇒ `[[rm, -r, x]]`. Empty when nothing peeled. */
|
|
18
28
|
readonly peeled: readonly (readonly ShellWord[])[];
|
|
@@ -34,6 +44,14 @@ export interface ShellCommandShape {
|
|
|
34
44
|
/** A subshell, group, control-structure keyword or dropped empty piece bounded the segments: the
|
|
35
45
|
* connector list is not one flat chain a compound rule could spell. */
|
|
36
46
|
readonly grouped: boolean;
|
|
47
|
+
/** A `&` connector anywhere — between two segments or after the last one (`ls &`): some segment runs in the
|
|
48
|
+
* background, past any approval-time reading of the line. The connector list alone cannot say so for a
|
|
49
|
+
* trailing `&` (a connector rides BETWEEN segments). */
|
|
50
|
+
readonly backgrounded: boolean;
|
|
51
|
+
/** A redirection on a piece that runs NO program (`> x` alone, `[ -f a ] > x`, a keyword's own redirection):
|
|
52
|
+
* such a piece is dropped from `segments` (the tightening reader asks about program runs, and it runs
|
|
53
|
+
* none) — but the shell still opens the target, so a reader that asks about EFFECTS must refuse the line. */
|
|
54
|
+
readonly strayRedirection: boolean;
|
|
37
55
|
}
|
|
38
56
|
export { SHELL_WRAPPER_TABLE, type ShellWrapperName } from "./shell-wrapper-table.js";
|
|
39
57
|
/** Longer than this and the command is not read at all (one unreadable segment): every pass is linear,
|