@sema-agent/core 5.49.0 → 5.50.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 +49 -0
- package/dist/agents/roster-store.js +4 -1
- package/dist/agents/subagent.d.ts +6 -0
- package/dist/agents/subagent.js +126 -1
- package/dist/agents/teacher.js +4 -1
- package/dist/brain/anthropic.js +11 -20
- package/dist/brain/open-responses.js +6 -14
- package/dist/brain/openai.js +6 -18
- package/dist/brain/reasoning.d.ts +100 -8
- package/dist/brain/reasoning.js +39 -15
- package/dist/brain/request-params.d.ts +37 -1
- package/dist/brain/request-params.js +40 -2
- package/dist/core/mcp.d.ts +7 -1
- package/dist/core/mcp.js +64 -8
- package/dist/core/memory-engine/engine.d.ts +30 -1
- package/dist/core/memory-engine/engine.js +219 -18
- package/dist/core/memory-engine/layout.d.ts +43 -0
- package/dist/core/memory-engine/layout.js +59 -0
- package/dist/core/memory-engine/memory-backend-contract.js +87 -0
- package/dist/core/memory-engine/types.d.ts +13 -1
- package/dist/core/runner/prepare-task.js +15 -1
- package/dist/core/task-registry-agent.js +6 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/orchestration/run-workflow-tool.d.ts +12 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-governance.d.ts +27 -0
- package/dist/orchestration/workflow-governance.js +13 -0
- package/dist/orchestration/workflow-primitives.d.ts +8 -1
- package/dist/orchestration/workflow-primitives.js +11 -3
- package/package.json +1 -1
|
@@ -277,6 +277,19 @@ function raceAbort(p, signal, onAbort) {
|
|
|
277
277
|
p.then(finish, () => finish(onAbort()));
|
|
278
278
|
});
|
|
279
279
|
}
|
|
280
|
+
function mcpRevocationWiring(deps) {
|
|
281
|
+
if (deps.mcpRevocations === undefined)
|
|
282
|
+
return undefined;
|
|
283
|
+
const ledger = deps.mcpRevocations;
|
|
284
|
+
return {
|
|
285
|
+
isRevoked: (name) => ledger.isRevoked(name),
|
|
286
|
+
onProbeFailure: (e) => deliverEngineNotice(deps.onNotice, {
|
|
287
|
+
code: "mcp.revocation_probe_failed",
|
|
288
|
+
message: `the mcpRevocations.isRevoked probe threw — MCP dispatch fails OPEN (no server treated as revoked) until the probe recovers: ${e instanceof Error ? e.message : String(e)}`,
|
|
289
|
+
detail: { message: e instanceof Error ? e.message : String(e) },
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
280
293
|
async function forgetQuietly(sessions, sessionId) {
|
|
281
294
|
try {
|
|
282
295
|
if (sessions.forget)
|
|
@@ -1030,6 +1043,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1030
1043
|
parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
|
|
1031
1044
|
parentReadFace: () => carrierReadFace(),
|
|
1032
1045
|
parentReadDenyPatterns: () => (readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized : undefined),
|
|
1046
|
+
...(spec.handsReadOnly === true ? { parentHandsReadOnly: true } : {}), ...(spec.interactiveTools === false ? { parentInteractiveTools: false } : {}),
|
|
1033
1047
|
onNotice: deps.onNotice,
|
|
1034
1048
|
parentCheckpointStoreDisabled: spec.checkpointStore === null,
|
|
1035
1049
|
parentCenterArtifactDigest: () => centerAdoption?.artifact.artifactDigest,
|
|
@@ -1101,7 +1115,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1101
1115
|
tools.push(createOutputTool(outputRef, spec.outputSchema, compiled.strict ? compiled.modelSchema : undefined));
|
|
1102
1116
|
}
|
|
1103
1117
|
mcp = lockedPreflight.mcp?.length
|
|
1104
|
-
? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts })
|
|
1118
|
+
? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps))
|
|
1105
1119
|
: { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
|
|
1106
1120
|
for (const w of mcp.warnings)
|
|
1107
1121
|
deps.onError?.(w, { phase: "mcp", sessionId });
|
|
@@ -233,6 +233,12 @@ export async function reapDurableAgentsLane(core, scope, deps, policy) {
|
|
|
233
233
|
continue;
|
|
234
234
|
}
|
|
235
235
|
rowsReaped++;
|
|
236
|
+
{
|
|
237
|
+
const staleInProc = core.handles.get(r.handle);
|
|
238
|
+
if (staleInProc !== undefined && staleInProc.status !== "running" && staleInProc.status !== "pending" && staleInProc.status !== "parked") {
|
|
239
|
+
core.handles.delete(r.handle);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
236
242
|
core.reapedHandles.add(r.handle);
|
|
237
243
|
if (deps.mailbox !== undefined) {
|
|
238
244
|
try {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4730,6 +4730,10 @@ export interface EngineNotice {
|
|
|
4730
4730
|
* `detail: { total, stripped: [{ key, reason }], omitted? }`, the rendered key list bounded in count
|
|
4731
4731
|
* and length because the names come from the untrusted script. One aggregated notice per governed
|
|
4732
4732
|
* child build, not de-duplicated across builds: each spec is a distinct fact.
|
|
4733
|
+
* - `"mcp.revocation_probe_failed"` (design/338) — the deployment's `mcpRevocations.isRevoked`
|
|
4734
|
+
* probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
|
|
4735
|
+
* once per run. `detail: { message }`. The refusal itself (`mcp.server_revoked`) is a tool
|
|
4736
|
+
* RESULT code, not a notice.
|
|
4733
4737
|
* - `"config.models_swapped"` — `Runner.swapModels` replaced the model catalog generation
|
|
4734
4738
|
* (zero-restart model switching). `detail: { models, tiers }` — key COUNTS only, never the
|
|
4735
4739
|
* catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
|
|
@@ -4898,6 +4902,19 @@ export interface RunnerDeps {
|
|
|
4898
4902
|
brain: Brain;
|
|
4899
4903
|
/** Catalog used to resolve string ModelRefs to Model objects. */
|
|
4900
4904
|
models?: Record<string, Model>;
|
|
4905
|
+
/**
|
|
4906
|
+
* design/338 (mid-turn MCP revocation) — the HOST's revocation ledger, probed synchronously at
|
|
4907
|
+
* every MCP dispatch (tool call + the three resource tools) BEFORE the transport. The engine
|
|
4908
|
+
* never caches the answer: the ledger's one authority lives on the host (a cached copy would be
|
|
4909
|
+
* a split-state second authority). A revoked server's calls settle as the coded refusal
|
|
4910
|
+
* `mcp.server_revoked` with known-not-executed wording; in-flight calls a revocation raced are
|
|
4911
|
+
* deliberately not chased (the threat shape is "new calls after removal"). Absent seat = the
|
|
4912
|
+
* pre-338 semantics. A THROWING probe fails open (revocation is a tightening face — a broken
|
|
4913
|
+
* probe must not brick every MCP call) with a once-per-run `mcp.revocation_probe_failed` notice.
|
|
4914
|
+
*/
|
|
4915
|
+
mcpRevocations?: {
|
|
4916
|
+
isRevoked(serverName: string): boolean;
|
|
4917
|
+
};
|
|
4901
4918
|
/**
|
|
4902
4919
|
* design/147 S1c (clay ruling 2026-07-18) — the DURABLE name→agent roster behind explicit-name
|
|
4903
4920
|
* addressing, a storage-tier seam like the checkpoint store: core bundles `MemoryRosterStore`
|
|
@@ -234,6 +234,18 @@ export interface RunWorkflowToolDeps {
|
|
|
234
234
|
parentReadFace?: () => import("../core/types.js").TaskSpec["readFace"];
|
|
235
235
|
/** Twin of the above for the deny-set additions (union, not replace). */
|
|
236
236
|
parentReadDenyPatterns?: () => import("../core/types.js").TaskSpec["readDenyPatterns"];
|
|
237
|
+
/** #345 ③ — the HOST task's write-hands clamp, the read-face clamp's sibling on this lane (the
|
|
238
|
+
* subagent delegation lane has carried it as `ToolExecuteContext.handsReadOnly` since its own
|
|
239
|
+
* tighten-only ruling; the workflow lane was the remaining gap — a read-only host's workflow
|
|
240
|
+
* children spawned with full write hands). Filled ONLY when the clamp is ON (`true`), exactly
|
|
241
|
+
* like the ctx seat, so an unclamped deployment's mount gains no key. Known at prepare-time
|
|
242
|
+
* (frozen TaskSpec snapshot — no lazy getter needed, the `parentCheckpointStoreDisabled` shape). */
|
|
243
|
+
parentHandsReadOnly?: true;
|
|
244
|
+
/** #345 ③ — the HOST task's hard-headless clamp (`interactiveTools: false`), same carriage rules
|
|
245
|
+
* as `parentHandsReadOnly` above: only the DISABLING value travels, mirroring the subagent lane's
|
|
246
|
+
* `ToolExecuteContext.interactiveTools` seat — without it a headless-clamped host's workflow
|
|
247
|
+
* children could re-mount the interactive tool the host run disabled. */
|
|
248
|
+
parentInteractiveTools?: false;
|
|
237
249
|
/** [1238](A) — call-time getter for the HOST task's RESOLVED Model object: a spawned agent whose
|
|
238
250
|
* fold chain produced no model inherits the parent's full object (baseUrl/key routing included),
|
|
239
251
|
* mirroring the subagent lane's ctx.model semantics. */
|
|
@@ -468,7 +468,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
468
468
|
}
|
|
469
469
|
: governance;
|
|
470
470
|
const scriptFn = (wfCtx) => {
|
|
471
|
-
const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns);
|
|
471
|
+
const primitives = buildWorkflowPrimitives(wfCtx, runGovernance, d.onAgentSpawn, d.parentThinking, principal, ctx.checkpointStoreDisabledForChildren === true || d.parentCheckpointStoreDisabled === true, d.parentReadFace, d.parentReadDenyPatterns, ctx.handsReadOnly === true || d.parentHandsReadOnly === true, ctx.interactiveTools === false || d.parentInteractiveTools === false);
|
|
472
472
|
return d.scriptRunner.run({ scriptSource: script, primitives, scriptArgs: effectiveArgs, signal: wfCtx.signal }).then((r) => r.result);
|
|
473
473
|
};
|
|
474
474
|
if (ctx.signal?.aborted) {
|
|
@@ -125,6 +125,33 @@ export interface ResourceClampNote {
|
|
|
125
125
|
/** The effective value the child actually runs under (min of script/baseline/caps, or a forced ceiling). */
|
|
126
126
|
applied: number;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The worktree-overlay merge — `{ ...base, ...worktreeBase }` with ONE amendment: an overlay key
|
|
130
|
+
* whose value is OWN NULLISH (`undefined` OR `null` — both plain-JS deployment shapes) never
|
|
131
|
+
* overrides `base`; it reads as the absent key its value spells ("an absent overlay field inherits
|
|
132
|
+
* from base", the documented semantics). The GENERIC successor of the per-key protections that grew
|
|
133
|
+
* one field at a time (three face arrays, then `onAsk` — the mount-side deletes, which remain for
|
|
134
|
+
* their base-slot readings) while every OTHER same-shaped key stayed destructive: a surviving own
|
|
135
|
+
* nullish overlay key erased the base value — `toolPolicy` / `shellGate` / `limits` / `hooks` /
|
|
136
|
+
* `restoreGatedTools` / `readFace` / … — for exactly the children an untrusted script can route to
|
|
137
|
+
* the overlay (`isolation: "worktree"`), own-`undefined` keys included (a spread copies them; the
|
|
138
|
+
* old `=== null` face drop missed that half). One rule at the one merge point: a new baseline key
|
|
139
|
+
* is safe with no list to extend. The single exception is {@link NULL_VALUED_BASELINE_KEYS}
|
|
140
|
+
* (`checkpointStore: null` — the deployment disarming durable suspend for isolated children is a
|
|
141
|
+
* capability removal it must be able to spell). Explicit non-nullish overlay values keep winning
|
|
142
|
+
* wholesale — the documented deployment override.
|
|
143
|
+
*
|
|
144
|
+
* ACCEPTED COST, stated (adversarial-review round 2): the rule is direction-blind — a CAPABILITY
|
|
145
|
+
* key's out-of-type overlay `null` used to WITHHOLD the base value by the same spread accident
|
|
146
|
+
* (e.g. `worktreeBase: { getApiKeyAndHeaders: null }` overwrote the base resolver AND defeated the
|
|
147
|
+
* parent-credential `=== undefined` fill in workflow.ts — silently blocking the documented
|
|
148
|
+
* inheritance, the same seam-defeat disease the onAsk r3 ruling deleted null for), and such a key
|
|
149
|
+
* now inherits per the documented contract. Deployments withhold capabilities from isolated
|
|
150
|
+
* children with IN-TYPE spellings (`tools: []`, a deny `toolPolicy`, `handsReadOnly: true`);
|
|
151
|
+
* `getApiKeyAndHeaders` currently has no in-type disable value — that expressiveness gap is a
|
|
152
|
+
* baseline-contract question, deliberately not solved by resurrecting undocumented null semantics.
|
|
153
|
+
*/
|
|
154
|
+
export declare function overlayWorktreeBaseline(base: WorkflowGovernanceBaseline["base"], worktreeBase: WorkflowGovernanceBaseline["base"]): WorkflowGovernanceBaseline["base"];
|
|
128
155
|
/** Resolve an LLM-supplied model NAME to a deploy-configured `Model`, FAIL-CLOSED against the allowlist.
|
|
129
156
|
* Throws (never falls open to the whole catalog) when no allowlist is configured or the name is not on it. */
|
|
130
157
|
export declare function resolveModelName(name: string, allowlist: string[] | undefined, models: Record<string, Model> | undefined): Model;
|
|
@@ -57,6 +57,19 @@ function emitStrippedKeysNotice(survey, onNotice) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
60
|
+
const NULL_VALUED_BASELINE_KEYS = new Set(["checkpointStore"]);
|
|
61
|
+
export function overlayWorktreeBaseline(base, worktreeBase) {
|
|
62
|
+
let overlay = worktreeBase;
|
|
63
|
+
for (const key of Object.keys(worktreeBase)) {
|
|
64
|
+
const v = worktreeBase[key];
|
|
65
|
+
if (v !== undefined && (v !== null || NULL_VALUED_BASELINE_KEYS.has(key)))
|
|
66
|
+
continue;
|
|
67
|
+
if (overlay === worktreeBase)
|
|
68
|
+
overlay = { ...worktreeBase };
|
|
69
|
+
delete overlay[key];
|
|
70
|
+
}
|
|
71
|
+
return { ...base, ...overlay };
|
|
72
|
+
}
|
|
60
73
|
export function resolveModelName(name, allowlist, models) {
|
|
61
74
|
if (!allowlist || allowlist.length === 0) {
|
|
62
75
|
throw new WorkflowModelNotAllowedError(name, "no workflowModelAllowlist is configured (fail-closed: an LLM-authored script cannot pick a model)");
|
|
@@ -44,4 +44,11 @@ parentCheckpointStoreDisabled?: boolean,
|
|
|
44
44
|
parentReadFace?: () => TaskSpec["readFace"],
|
|
45
45
|
/** Twin of the above for the deny-set additions (built-ins always apply; these are the extra
|
|
46
46
|
* entries the host's own deployment/task layers stacked on). */
|
|
47
|
-
parentReadDenyPatterns?: () => TaskSpec["readDenyPatterns"]
|
|
47
|
+
parentReadDenyPatterns?: () => TaskSpec["readDenyPatterns"],
|
|
48
|
+
/** #345 ③ — the HOST task's write-hands clamp is ON (`spec.handsReadOnly === true`). Injected
|
|
49
|
+
* tighten-only into every spawned child, like the read-face clamp above; see the injection
|
|
50
|
+
* below for why it outranks even a `worktreeBase: { handsReadOnly: false }` overlay. */
|
|
51
|
+
parentHandsReadOnly?: boolean,
|
|
52
|
+
/** #345 ③ — the HOST task's hard-headless clamp is ON (`spec.interactiveTools === false`).
|
|
53
|
+
* Same tighten-only injection; only the disabling value ever arrives here as `true`. */
|
|
54
|
+
parentInteractiveToolsOff?: boolean): WorkflowPrimitives;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertSupportedAgentIsolation } from "./workflow.js";
|
|
2
|
-
import { buildGovernedChildSpec } from "./workflow-governance.js";
|
|
2
|
+
import { buildGovernedChildSpec, overlayWorktreeBaseline } from "./workflow-governance.js";
|
|
3
3
|
function safeAgentOptions(opts) {
|
|
4
4
|
if (typeof opts !== "object" || opts === null)
|
|
5
5
|
return {};
|
|
@@ -22,12 +22,12 @@ function formatResourceClampNote(notes) {
|
|
|
22
22
|
const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
|
|
23
23
|
return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
|
|
24
24
|
}
|
|
25
|
-
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal, parentCheckpointStoreDisabled, parentReadFace, parentReadDenyPatterns) {
|
|
25
|
+
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThinking, parentPrincipal, parentCheckpointStoreDisabled, parentReadFace, parentReadDenyPatterns, parentHandsReadOnly, parentInteractiveToolsOff) {
|
|
26
26
|
const agent = (spec, opts) => {
|
|
27
27
|
if (typeof spec === "string")
|
|
28
28
|
spec = { objective: spec };
|
|
29
29
|
const agentOpts = safeAgentOptions(opts);
|
|
30
|
-
const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase
|
|
30
|
+
const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase != null ? { ...b, base: overlayWorktreeBaseline(b.base, b.worktreeBase) } : b;
|
|
31
31
|
const childSpec = governance
|
|
32
32
|
? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)), governance.onNotice)
|
|
33
33
|
: { ...spec };
|
|
@@ -53,6 +53,14 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
|
|
|
53
53
|
childSpec.readDenyPatterns = childSpec.readDenyPatterns !== undefined ? [...childSpec.readDenyPatterns, ...pd] : [...pd];
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
if (parentHandsReadOnly === true) {
|
|
57
|
+
childSpec.handsReadOnly = true;
|
|
58
|
+
}
|
|
59
|
+
if (parentInteractiveToolsOff === true) {
|
|
60
|
+
childSpec.interactiveTools = false;
|
|
61
|
+
if (childSpec.interactionPosture === "interactive")
|
|
62
|
+
delete childSpec.interactionPosture;
|
|
63
|
+
}
|
|
56
64
|
if (onAgentSpawn) {
|
|
57
65
|
return ctx.agentStream(childSpec, agentOpts).then((handle) => {
|
|
58
66
|
onAgentSpawn(handle);
|