@sema-agent/core 5.56.0 → 5.57.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/agents/send-message-tool.d.ts +11 -0
  3. package/dist/agents/send-message-tool.js +34 -12
  4. package/dist/agents/team.d.ts +10 -1
  5. package/dist/agents/team.js +1 -0
  6. package/dist/brain/anthropic.js +15 -5
  7. package/dist/brain/circuit-breaker.js +2 -1
  8. package/dist/brain/degrading.js +4 -1
  9. package/dist/brain/failover.js +16 -1
  10. package/dist/brain/open-responses.js +15 -5
  11. package/dist/brain/openai.js +16 -5
  12. package/dist/brain/request-params.d.ts +30 -27
  13. package/dist/brain/request-params.js +1 -7
  14. package/dist/brain/route-adjudicator.d.ts +190 -0
  15. package/dist/brain/route-adjudicator.js +189 -0
  16. package/dist/brain/route-conformance.d.ts +55 -0
  17. package/dist/brain/route-conformance.js +136 -0
  18. package/dist/brain/routing.js +8 -3
  19. package/dist/core/mcp.js +4 -4
  20. package/dist/core/memory-engine/engine.d.ts +15 -5
  21. package/dist/core/memory-engine/engine.js +3 -1
  22. package/dist/core/permission-rule-consent.d.ts +45 -0
  23. package/dist/core/permission-rule-consent.js +40 -11
  24. package/dist/core/permission-rule-model.d.ts +110 -75
  25. package/dist/core/permission-rule-model.js +61 -28
  26. package/dist/core/runner/prepare-task.js +33 -4
  27. package/dist/core/runner/runtask.d.ts +4 -1
  28. package/dist/core/runner/runtask.js +48 -0
  29. package/dist/core/scheduler.d.ts +5 -0
  30. package/dist/core/side-query.d.ts +12 -5
  31. package/dist/core/types.d.ts +32 -0
  32. package/dist/engine/harness/agent-harness.js +26 -1
  33. package/dist/engine/harness/types.d.ts +5 -1
  34. package/dist/engine/llm/types.d.ts +65 -0
  35. package/dist/index.d.ts +4 -1
  36. package/dist/index.js +3 -1
  37. package/dist/internal/llm.d.ts +1 -1
  38. package/dist/prompts/default.d.ts +2 -2
  39. package/dist/prompts/default.js +2 -0
  40. package/dist/scenarios/scenario-registry.d.ts +5 -1
  41. package/dist/scenarios/scenario-registry.js +4 -2
  42. package/dist/tools/fs/index.js +8 -1
  43. package/dist/tools/scheduler-tools.js +28 -6
  44. package/dist/tools/web.d.ts +15 -0
  45. package/dist/tools/web.js +8 -2
  46. package/dist/tools/worktree.js +2 -2
  47. package/package.json +1 -1
  48. package/test/export-surface.snapshot.json +19 -1
@@ -61,7 +61,7 @@ export async function runScenario(opts) {
61
61
  switch (profile.orchestrator) {
62
62
  case "solo": {
63
63
  const model = requireModel(opts.models, "default", opts.scenario);
64
- const spec = { objective: opts.objective, model, signal: opts.signal };
64
+ const spec = { objective: opts.objective, model, signal: opts.signal, ...(opts.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.getApiKeyAndHeaders } : {}) };
65
65
  return { scenario: "solo", result: await opts.runner.runTask(spec) };
66
66
  }
67
67
  case "team": {
@@ -81,6 +81,7 @@ export async function runScenario(opts) {
81
81
  ],
82
82
  synthesizer: { model: synthModel },
83
83
  signal: opts.signal,
84
+ ...(opts.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.getApiKeyAndHeaders } : {}),
84
85
  });
85
86
  return { scenario: "design-review", result };
86
87
  }
@@ -95,6 +96,7 @@ export async function runScenario(opts) {
95
96
  members: codeReviewMembers(opts.reviewerCount ?? 2, teamModel),
96
97
  synthesizer: { model: synthModel },
97
98
  signal: opts.signal,
99
+ ...(opts.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.getApiKeyAndHeaders } : {}),
98
100
  });
99
101
  return { scenario: "code-review", mode: "team", result };
100
102
  }
@@ -127,7 +129,7 @@ async function runCodeReviewVerify(opts) {
127
129
  result: opts.objective,
128
130
  stats: { turns: 0, tokens: 0 },
129
131
  };
130
- const specBase = { signal: opts.signal };
132
+ const specBase = { signal: opts.signal, ...(opts.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: opts.getApiKeyAndHeaders } : {}) };
131
133
  const result = await verifyCompleted(opts.runner, reviewed, specBase, opts.objective, {
132
134
  verifierModel,
133
135
  verifierPrompt: STATIC_VERIFICATION_PROMPT,
@@ -79,7 +79,14 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
79
79
  if (!readOnly && mountBackgroundTaskTools && hasBackgroundShell(env)) {
80
80
  const sessionAxis = opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {};
81
81
  tools.push(opts.taskRegistry
82
- ? createTaskOutputTool({ registry: opts.taskRegistry, owner: opts.taskOwner, scope: opts.taskScope, ...sessionAxis, ...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}) })
82
+ ? createTaskOutputTool({
83
+ registry: opts.taskRegistry,
84
+ owner: opts.taskOwner,
85
+ scope: opts.taskScope,
86
+ ...sessionAxis,
87
+ ...(opts.oneShot !== undefined ? { oneShot: opts.oneShot } : {}),
88
+ notificationWired: opts.taskNotification !== undefined,
89
+ })
83
90
  : createEnvTaskOutputTool(env), opts.taskRegistry
84
91
  ? createTaskStopTool({ registry: opts.taskRegistry, owner: opts.taskOwner, scope: opts.taskScope, ...sessionAxis })
85
92
  : createEnvTaskStopTool(env));
@@ -396,10 +396,19 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
396
396
  wakeupChain = next.then(() => undefined, () => undefined);
397
397
  return next;
398
398
  };
399
+ const wakeupMountNote = sched.schedulerCapabilities.supportsSessionWakeup === false
400
+ ? `
401
+
402
+ MOUNT NOTE: this host has no resident wakeup leg, so ${SCHEDULE_WAKEUP_TOOL_NAME} refuses every scheduling call here — do not plan a self-paced loop around it (\`stop: true\` still works).`
403
+ : sched.schedulerCapabilities.supportsSessionLifetime === true
404
+ ? ""
405
+ : `
406
+
407
+ MOUNT NOTE: this host does not vouch for session-scoped scheduling, so a wakeup scheduled here is PERSISTENT — contrary to the paragraph above, it can outlive this session instead of ending with it. End the loop explicitly with \`stop: true\` when the work is done; do not rely on the session ending to cancel it.`;
399
408
  const scheduleWakeup = defineTool({
400
409
  name: SCHEDULE_WAKEUP_TOOL_NAME,
401
410
  contract: { contractId: "core.schedule_wakeup@1", implementationRevision: "1" },
402
- description: SCHEDULE_WAKEUP_PROMPT,
411
+ description: `${SCHEDULE_WAKEUP_PROMPT}${wakeupMountNote}`,
403
412
  parameters: Type.Object({
404
413
  delaySeconds: Type.Optional(Type.Number({
405
414
  description: "Seconds from now to wake up. Clamped to [60, 3600] by the runtime. Required unless `stop` is true.",
@@ -433,20 +442,33 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
433
442
  if (sched.schedulerCapabilities.supportsSessionWakeup === false) {
434
443
  return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`);
435
444
  }
445
+ const sessionReapVouched = sched.schedulerCapabilities.supportsSessionLifetime === true;
436
446
  const clampedDelaySeconds = Math.min(3600, Math.max(60, Math.round(a.delaySeconds)));
437
447
  const wasClamped = clampedDelaySeconds !== a.delaySeconds;
438
448
  const pending = await listPendingWakeups();
439
- if (!("err" in pending))
440
- await cancelWakeups(pending.ids);
441
- const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, sessionCtx);
449
+ const cancelledCount = "err" in pending ? 0 : await cancelWakeups(pending.ids);
450
+ const cleanupIncomplete = "err" in pending || cancelledCount < pending.ids.length;
451
+ const r = await sched.schedule({
452
+ prompt: a.prompt,
453
+ when: { kind: "delay", delaySec: clampedDelaySeconds },
454
+ label: WAKEUP_LABEL,
455
+ mode: "session-wakeup",
456
+ ...(sessionReapVouched ? { lifetime: "session" } : {}),
457
+ }, sessionCtx);
442
458
  if (!r.ok)
443
459
  return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`);
444
460
  const scheduledFor = Date.now() + clampedDelaySeconds * 1000;
445
461
  const hhmmss = new Date(scheduledFor).toTimeString().slice(0, 8);
446
462
  const clampNote = wasClamped ? ` (clamped to ${clampedDelaySeconds}s from your requested value)` : "";
463
+ const cleanupNote = cleanupIncomplete
464
+ ? " Warning: the previously pending wakeup(s) for this session could not all be listed or cancelled, so MORE THAN ONE wakeup may now be armed (you would be re-invoked twice). Call this tool with `stop: true` and then re-schedule to get back to a single one."
465
+ : "";
466
+ const reapNote = sessionReapVouched
467
+ ? ""
468
+ : " Note: this scheduler does not vouch for session-lifetime reap, so this wakeup is scheduled as a persistent one and may outlive the session — end the loop explicitly with `stop: true` rather than relying on the session ending.";
447
469
  return {
448
- content: `Next wakeup scheduled for ${hhmmss} (in ${clampedDelaySeconds}s)${clampNote}. Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.`,
449
- details: { type: "schedule-wakeup", stopped: false, scheduledFor, clampedDelaySeconds, wasClamped },
470
+ content: `Next wakeup scheduled for ${hhmmss} (in ${clampedDelaySeconds}s)${clampNote}. Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.${reapNote}${cleanupNote}`,
471
+ details: { type: "schedule-wakeup", stopped: false, scheduledFor, clampedDelaySeconds, wasClamped, reason: a.reason },
450
472
  };
451
473
  }),
452
474
  });
@@ -160,6 +160,21 @@ export declare function resolveSummaryInputChars(model: Model, override?: number
160
160
  export interface WebFetchSummarizerOptions {
161
161
  /** Override the derived page-content budget (see {@link resolveSummaryInputChars}). */
162
162
  maxContentChars?: number;
163
+ /**
164
+ * Per-model auth — MIRRORS {@link TaskSpec.getApiKeyAndHeaders} (same signature, resolved per
165
+ * call against the summarizer's model, the side-query seat's form). Without this seat the
166
+ * summarizer called the brain with `{ signal }` only, so a summarizer model on a
167
+ * per-model-credential route fell to the brain's construction-time credential — which the
168
+ * key↔URL pairing gate refuses off the deployment root. Absent ⇒ the options object is
169
+ * byte-identical to the pre-seat shape (no keys added).
170
+ */
171
+ getApiKeyAndHeaders?: (model: Model) => Promise<{
172
+ apiKey?: string;
173
+ headers?: Record<string, string>;
174
+ } | undefined> | {
175
+ apiKey?: string;
176
+ headers?: Record<string, string>;
177
+ } | undefined;
163
178
  }
164
179
  /**
165
180
  * 黑板 [1870] L1 / [1900] — a reference {@link WebFetchConfig.summarize} implementation: reuses a
package/dist/tools/web.js CHANGED
@@ -725,9 +725,15 @@ export function createWebFetchSummarizer(brain, model, options = {}) {
725
725
  const truncated = inputTruncated ? content.slice(0, budget) + "\n\n[Content truncated due to length...]" : content;
726
726
  const userPrompt = `\nWeb page content:\n---\n${truncated}\n---\n\n${prompt}\n\n` + WEBFETCH_SUMMARY_GROUNDING_CLAUSE + "\n" + WEBFETCH_SUMMARY_GUIDELINES;
727
727
  const context = { messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] };
728
+ const auth = await options.getApiKeyAndHeaders?.(model);
729
+ const callOptions = {
730
+ signal,
731
+ ...(auth?.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
732
+ ...(auth?.headers !== undefined ? { headers: auth.headers } : {}),
733
+ };
728
734
  const msg = brain.complete
729
- ? await brain.complete(model, context, { signal })
730
- : await (await Promise.resolve(brain.stream(model, context, { signal }))).result();
735
+ ? await brain.complete(model, context, callOptions)
736
+ : await (await Promise.resolve(brain.stream(model, context, callOptions))).result();
731
737
  if (msg.stopReason === "error" || msg.stopReason === "aborted") {
732
738
  throw new Error(msg.errorMessage ?? `summarizer stopped with ${msg.stopReason}`);
733
739
  }
@@ -248,10 +248,10 @@ export function createWorktreeTools(env, opts) {
248
248
  description: "true = remove the worktree even when it has uncommitted files or new commits, permanently discarding them. Default false: a changed worktree is kept and its changes listed. Cannot be combined with `keep: true`.",
249
249
  })),
250
250
  action: Type.Optional(Type.Union([Type.Literal("keep"), Type.Literal("remove")], {
251
- description: '"keep" leaves the worktree and branch on disk; "remove" deletes both.',
251
+ description: '"keep" leaves the worktree on disk. "remove" takes the default removal path: an unchanged worktree is removed, a changed one is still KEPT unless `discard_changes: true`. The worktree is detached — no branch is created or deleted either way.',
252
252
  })),
253
253
  discard_changes: Type.Optional(Type.Boolean({
254
- description: 'Required true when action is "remove" and the worktree has uncommitted files or unmerged commits. The tool will refuse and list them otherwise.',
254
+ description: 'Set true to remove the worktree even when it has uncommitted files or unmerged commits — they are permanently discarded. Without it a changed worktree is KEPT (the exit still completes and the reply lists the changes plus this escape), never refused.',
255
255
  })),
256
256
  }),
257
257
  effect: "write",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.56.0",
3
+ "version": "5.57.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "design/87 L3 — frozen public export surface of src/index.ts (name -> kind). DO NOT edit by hand to silence a red test. A removed/changed entry = a SemVer-BREAKING change; bump MAJOR and update this fixture in the SAME commit (design/87 §4.2 / §5.2). Regenerate via REGEN in test/export-surface.test.ts.",
3
- "count": 1678,
3
+ "count": 1696,
4
4
  "exports": {
5
5
  "A2ATaskState": "type",
6
6
  "A2ATaskStateReversal": "type",
@@ -269,6 +269,7 @@
269
269
  "EXPLORE_SYSTEM_PROMPT": "variable",
270
270
  "EXPLORE_WHEN_TO_USE": "variable",
271
271
  "EXPLORE_WHEN_TO_USE_LEAN": "variable",
272
+ "EditedRuleTextPrecheck": "type",
272
273
  "EffectiveConfigField": "interface",
273
274
  "EffectiveMemoryScopes": "type",
274
275
  "EffectivePermissionRule": "interface",
@@ -729,6 +730,7 @@
729
730
  "RETRYABLE_REMOTE_ERROR_CODES": "variable",
730
731
  "ROLE_TIER_DEFAULTS": "variable",
731
732
  "ROOT_ADOPTION_FILE": "variable",
733
+ "ROUTE_ADJUDICATION_CONFORMANCE_CORPUS": "variable",
732
734
  "RULE_SYNC_DROP_CODES": "variable",
733
735
  "RUN_WORKFLOW_TOOL_NAME": "variable",
734
736
  "RawPrefixInputs": "interface",
@@ -801,6 +803,15 @@
801
803
  "RosterEntry": "interface",
802
804
  "RosterGcOptions": "interface",
803
805
  "RosterStore": "interface",
806
+ "RouteAdjudication": "type",
807
+ "RouteAdjudicationVector": "interface",
808
+ "RouteCredential": "interface",
809
+ "RouteCredentialSource": "type",
810
+ "RoutePairingConfig": "interface",
811
+ "RoutePairingPosture": "type",
812
+ "RoutePairingStatus": "type",
813
+ "RouteRefusalCode": "type",
814
+ "RouteRefusalDetail": "interface",
804
815
  "RoutingBrainOptions": "interface",
805
816
  "RuleAdd": "interface",
806
817
  "RuleAddDelta": "interface",
@@ -1142,6 +1153,7 @@
1142
1153
  "acquireCcLock": "function",
1143
1154
  "addDotsOf": "function",
1144
1155
  "addWorktree": "function",
1156
+ "adjudicateModelRoute": "function",
1145
1157
  "admitMemoryScopes": "function",
1146
1158
  "adoptFilePermissionRuleStore": "function",
1147
1159
  "adoptLocalDataRoot": "function",
@@ -1386,6 +1398,7 @@
1386
1398
  "guardedMemoryStore": "function",
1387
1399
  "harnessContext": "function",
1388
1400
  "harvestFilesToPatches": "function",
1401
+ "hasAuthCarrier": "function",
1389
1402
  "hasBackgroundShell": "function",
1390
1403
  "hasDestroy": "function",
1391
1404
  "hasScheduler": "function",
@@ -1461,6 +1474,7 @@
1461
1474
  "mysqlQuery": "function",
1462
1475
  "nextSyncBaseline": "function",
1463
1476
  "normalizeAgentName": "function",
1477
+ "normalizeBaseUrl": "function",
1464
1478
  "normalizeForExactMatch": "function",
1465
1479
  "normalizeMemorySpec": "function",
1466
1480
  "normalizePersistedRuleHit": "function",
@@ -1494,6 +1508,7 @@
1494
1508
  "peerAxisToken": "function",
1495
1509
  "permissionRuleSyncContract": "function",
1496
1510
  "pgQuery": "function",
1511
+ "precheckEditedRuleText": "function",
1497
1512
  "prepareCardApproval": "function",
1498
1513
  "prepareCcImport": "function",
1499
1514
  "prepareStarterBatch": "function",
@@ -1563,6 +1578,7 @@
1563
1578
  "resolveReadFace": "function",
1564
1579
  "resolveReasoning": "function",
1565
1580
  "resolveReasoningProfile": "function",
1581
+ "resolveRouteCredential": "function",
1566
1582
  "resolveSubagentTranscriptTier": "function",
1567
1583
  "resolveTaskLimits": "function",
1568
1584
  "resolveTaskModel": "function",
@@ -1573,6 +1589,8 @@
1573
1589
  "retiredWorkflowNameAliases": "function",
1574
1590
  "retryBackoffMs": "function",
1575
1591
  "riskSeverity": "function",
1592
+ "routePairingStatus": "function",
1593
+ "routeRefusalText": "function",
1576
1594
  "ruleAdmitsCommand": "function",
1577
1595
  "ruleSyncVector": "function",
1578
1596
  "runCascade": "function",