@sema-agent/core 1.428.0 → 1.430.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.
@@ -18,6 +18,8 @@ export declare class SubagentStepRecorder {
18
18
  private readonly pending;
19
19
  private readonly edits;
20
20
  private action;
21
+ private actionTool;
22
+ private actionTarget;
21
23
  constructor(parentToolCallId?: string);
22
24
  lockTo(taskId: string): void;
23
25
  private resetRecorded;
@@ -26,5 +28,9 @@ export declare class SubagentStepRecorder {
26
28
  recentSteps(): SubagentStep[] | undefined;
27
29
  editedFiles(): SubagentEditedFile[] | undefined;
28
30
  currentAction(): string | undefined;
31
+ currentActionStructured(): {
32
+ toolName: string;
33
+ target?: string;
34
+ } | undefined;
29
35
  }
30
36
  export declare function stepsFromMessages(messages: readonly unknown[], lastN: number): SubagentStep[];
@@ -58,6 +58,8 @@ export class SubagentStepRecorder {
58
58
  pending = new Map();
59
59
  edits = new Map();
60
60
  action;
61
+ actionTool;
62
+ actionTarget;
61
63
  constructor(parentToolCallId) {
62
64
  this.parentToolCallId = parentToolCallId;
63
65
  }
@@ -72,6 +74,8 @@ export class SubagentStepRecorder {
72
74
  this.pending.clear();
73
75
  this.edits.clear();
74
76
  this.action = undefined;
77
+ this.actionTool = undefined;
78
+ this.actionTarget = undefined;
75
79
  }
76
80
  inScope(e) {
77
81
  const { sourceTaskId: src, parentToolCallId: ptc } = e;
@@ -94,6 +98,8 @@ export class SubagentStepRecorder {
94
98
  const target = extractTarget(e.args);
95
99
  this.pending.set(e.toolCallId, { tool, target, ...(editTargetPath(e.toolName, e.args) ? { edit: editTargetPath(e.toolName, e.args) } : {}) });
96
100
  this.action = target ? `${tool} ${target}`.slice(0, FIELD_MAX + tool.length + 1) : tool;
101
+ this.actionTool = tool;
102
+ this.actionTarget = target || undefined;
97
103
  }
98
104
  else if (e.type === "tool_end") {
99
105
  if (!this.inScope(e))
@@ -128,6 +134,9 @@ export class SubagentStepRecorder {
128
134
  currentAction() {
129
135
  return this.action;
130
136
  }
137
+ currentActionStructured() {
138
+ return this.actionTool === undefined ? undefined : { toolName: this.actionTool, ...(this.actionTarget !== undefined ? { target: this.actionTarget } : {}) };
139
+ }
131
140
  }
132
141
  export function stepsFromMessages(messages, lastN) {
133
142
  const results = new Map();
@@ -1569,6 +1569,36 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1569
1569
  timer.unref?.();
1570
1570
  return true;
1571
1571
  };
1572
+ if (ctx.autoModeReview) {
1573
+ const spawnToolNames = childTools.map((t) => t.name);
1574
+ const spawnToolsNote = spawnToolNames.length > 0
1575
+ ? spawnToolNames.join(", ")
1576
+ : "(none explicitly listed — but if this deployment gave the child a real execution environment, it likely also has the standard file/shell tools: Read/Edit/Write/Bash/Grep/Glob)";
1577
+ const spawnVerdict = await ctx.autoModeReview.decider
1578
+ .decide({
1579
+ req: {
1580
+ toolName: wantsFork ? "Agent(fork)" : "Agent",
1581
+ args: { objective: prompt.slice(0, 2_000), tools: spawnToolNames, systemPrompt: childSystemPrompt?.slice(0, 2_000) },
1582
+ toolCallId: ctx.toolCallId,
1583
+ },
1584
+ askMessage: `Reviewing a sub-agent about to be spawned. Objective: ${prompt.slice(0, 2000)}${prompt.length > 2000 ? "…" : ""}\nTools available to it: ${spawnToolsNote}`,
1585
+ }, ctx.signal)
1586
+ .catch(() => ({ kind: "unavailable", cause: "error" }));
1587
+ if (ctx.signal?.aborted) {
1588
+ await cancelObserver();
1589
+ const wt = await finishWorktree();
1590
+ return { isError: true, content: `Sub-agent not started: the delegating call was aborted.${wt ? `\n${wt}` : ""}`, details: { error: "aborted" } };
1591
+ }
1592
+ if (spawnVerdict.kind === "block") {
1593
+ await cancelObserver();
1594
+ const wt = await finishWorktree();
1595
+ return {
1596
+ isError: true,
1597
+ content: `Sub-agent not started: blocked by the auto-mode classifier${spawnVerdict.reason ? `: ${inlineUntrusted(spawnVerdict.reason)}` : ""}.${wt ? `\n${wt}` : ""}`,
1598
+ details: { error: "autoMode.spawn_blocked" },
1599
+ };
1600
+ }
1601
+ }
1572
1602
  if (wantsFork) {
1573
1603
  let forkedId;
1574
1604
  let releaseForked;
@@ -1686,6 +1716,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1686
1716
  }
1687
1717
  if (e.type === "task_progress") {
1688
1718
  const currentAction = stepRecorder.currentAction();
1719
+ const currentTool = stepRecorder.currentActionStructured();
1689
1720
  sinkEmit({
1690
1721
  kind: "tick",
1691
1722
  taskId,
@@ -1696,6 +1727,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1696
1727
  agentType: spawnAgentType,
1697
1728
  ...(e.name !== undefined ? { name: e.name } : {}),
1698
1729
  ...(currentAction !== undefined ? { currentAction } : {}),
1730
+ ...(currentTool !== undefined ? { currentTool } : {}),
1699
1731
  usage: e.usage,
1700
1732
  });
1701
1733
  }
@@ -2078,6 +2110,7 @@ task_id: ${taskId}
2078
2110
  }
2079
2111
  if (e.type === "task_progress") {
2080
2112
  const currentAction = stepRecorder.currentAction();
2113
+ const currentTool = stepRecorder.currentActionStructured();
2081
2114
  sinkEmit({
2082
2115
  kind: "tick",
2083
2116
  taskId,
@@ -2088,6 +2121,7 @@ task_id: ${taskId}
2088
2121
  agentType: spawnAgentType,
2089
2122
  ...(e.name !== undefined ? { name: e.name } : {}),
2090
2123
  ...(currentAction !== undefined ? { currentAction } : {}),
2124
+ ...(currentTool !== undefined ? { currentTool } : {}),
2091
2125
  usage: e.usage,
2092
2126
  });
2093
2127
  }
@@ -2507,6 +2541,26 @@ task_id: ${taskId}
2507
2541
  child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
2508
2542
  }
2509
2543
  await settleObserver(child.status);
2544
+ let handbackWarning;
2545
+ if (ctx.autoModeReview) {
2546
+ const handbackSteps = stepRecorder.recentSteps();
2547
+ const handbackEdits = stepRecorder.editedFiles();
2548
+ if (child.result || handbackSteps || handbackEdits) {
2549
+ const handbackVerdict = await ctx.autoModeReview.decider
2550
+ .decide({
2551
+ req: {
2552
+ toolName: "Agent(handback)",
2553
+ args: { result: child.result?.slice(0, 2_000), toolSteps: handbackSteps, editedFiles: handbackEdits },
2554
+ toolCallId: ctx.toolCallId,
2555
+ },
2556
+ askMessage: "Subagent has finished and is handing back control to the main agent. Review the subagent's work and flag if any action may violate security policy.",
2557
+ }, ctx.signal)
2558
+ .catch(() => ({ kind: "unavailable", cause: "error" }));
2559
+ if (handbackVerdict.kind === "block") {
2560
+ handbackWarning = `SECURITY WARNING: This subagent performed actions that may violate security policy. Reason: ${inlineUntrusted(handbackVerdict.reason ?? "(no reason given)")}. Review the subagent's actions carefully before acting on its output.`;
2561
+ }
2562
+ }
2563
+ }
2510
2564
  const ns = child.stats.nested;
2511
2565
  ctx.reportUsage?.({
2512
2566
  tokens: child.stats.tokens + (ns?.tokens ?? 0),
@@ -2541,6 +2595,7 @@ task_id: ${taskId}
2541
2595
  const worktreeLine = await finishWorktree();
2542
2596
  const errClass = classifySubagentError(child);
2543
2597
  const lines = [
2598
+ ...(handbackWarning ? [handbackWarning, ""] : []),
2544
2599
  `[Sub-agent report${def ? ` · ${def.name}` : ""}${a.description ? ` · ${String(a.description)}` : ""}]`,
2545
2600
  `status: ${child.status}`,
2546
2601
  modelNote ?? "",
@@ -757,6 +757,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
757
757
  ...(centerAdoption?.sourceRevision !== undefined ? { centerSourceRevision: centerAdoption.sourceRevision } : {}),
758
758
  activeSkillScope: () => skillScope.active(),
759
759
  inheritedGateForChildren,
760
+ ...(autoModeDecider ? { autoModeReview: { decider: autoModeDecider } } : {}),
760
761
  ...(spec.durableApproval !== undefined ? { durableApprovalForChildren: { ...spec.durableApproval } } : {}),
761
762
  taskId: hostTaskId,
762
763
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
@@ -101,6 +101,9 @@ export interface ToolExecuteContext {
101
101
  getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
102
102
  activeSkillScope?: () => readonly unknown[];
103
103
  inheritedGateForChildren?: () => import("./runner/prepare-task.js").InheritedGate;
104
+ autoModeReview?: {
105
+ decider: import("./auto-mode.js").AutoModeDecider;
106
+ };
104
107
  durableApprovalForChildren?: {
105
108
  scope: string;
106
109
  ttlMs?: number;
@@ -646,6 +649,10 @@ export interface BackgroundChildEvent {
646
649
  progressParentTaskId?: string;
647
650
  name?: string;
648
651
  currentAction?: string;
652
+ currentTool?: {
653
+ toolName: string;
654
+ target?: string;
655
+ };
649
656
  sessionId?: string;
650
657
  transcriptId?: string;
651
658
  recentSteps?: import("../agents/subagent-steps.js").SubagentStep[];
@@ -6,6 +6,7 @@ import { type ImageDownsampler } from "../../core/mcp.js";
6
6
  import { type PdfModelCapabilities } from "./pdf.js";
7
7
  export { pdfModelCapabilitiesOf, type PdfModelCapabilities } from "./pdf.js";
8
8
  export declare const SLICED_READ_MAX_BYTES: number;
9
+ export declare const MAX_EDIT_BYTES: number;
9
10
  export type ReadImageDownsamplerOption = ImageDownsampler | false | undefined;
10
11
  export declare function msTimeoutToSec(timeoutMs: number | undefined): number;
11
12
  export declare function seededFileUnchangedReminder(filePath: string): string;
@@ -14,6 +14,26 @@ import { PDF_FALLBACK_RENDER_PAGES, PDF_INLINE_PAGE_THRESHOLD, PDF_MAX_EXTRACT_S
14
14
  export { pdfModelCapabilitiesOf } from "./pdf.js";
15
15
  const MAX_READ_BYTES = 256 * 1024;
16
16
  export const SLICED_READ_MAX_BYTES = 64 * 1024 * 1024;
17
+ export const MAX_EDIT_BYTES = 1024 * 1024 * 1024;
18
+ function formatByteSize(n) {
19
+ const kb = n / 1024;
20
+ if (kb < 1)
21
+ return `${n} bytes`;
22
+ if (kb < 1024)
23
+ return `${kb.toFixed(1).replace(/\.0$/, "")}KB`;
24
+ const mb = kb / 1024;
25
+ if (mb < 1024)
26
+ return `${mb.toFixed(1).replace(/\.0$/, "")}MB`;
27
+ return `${(mb / 1024).toFixed(1).replace(/\.0$/, "")}GB`;
28
+ }
29
+ function decodeEditBytes(bytes, path) {
30
+ try {
31
+ return { ok: true, value: decodeTextBytes(bytes) };
32
+ }
33
+ catch {
34
+ return { ok: false, message: `Error (Edit): "${path}" is too large to edit (${formatByteSize(bytes.byteLength)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.` };
35
+ }
36
+ }
17
37
  async function notReadRefusalText(env, toolName, key, v, signal) {
18
38
  const base = `Error (${toolName}): ${v.message}`;
19
39
  const info = await env.fileInfo(key, signal);
@@ -656,11 +676,21 @@ export function makeEditFileTool(env, state, rootCanonical, cwdRef, additionalRo
656
676
  }
657
677
  return `Error (Edit): ${await enoentMessage(env, r.key, cwdRef?.current ?? rootCanonical, ctx.signal)}`;
658
678
  }
679
+ const editInfo = await env.fileInfo(r.key, ctx.signal);
680
+ if (editInfo.ok && editInfo.value.size > MAX_EDIT_BYTES) {
681
+ return `Error (Edit): "${path}" is too large to edit (${formatByteSize(editInfo.value.size)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.`;
682
+ }
659
683
  if (singleOld === "") {
660
684
  const preBin = await env.readBinaryFile(r.key, ctx.signal);
661
685
  if (!preBin.ok)
662
686
  return `Error (Edit): cannot read "${path}": ${preBin.error.message}`;
663
- const preDec = decodeTextBytes(preBin.value);
687
+ if (preBin.value.byteLength > MAX_EDIT_BYTES) {
688
+ return `Error (Edit): "${path}" is too large to edit (${formatByteSize(preBin.value.byteLength)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.`;
689
+ }
690
+ const preDecResult = decodeEditBytes(preBin.value, path);
691
+ if (!preDecResult.ok)
692
+ return preDecResult.message;
693
+ const preDec = preDecResult.value;
664
694
  if (preDec.malformed)
665
695
  return `Error (Edit): "${path}" has a truncated UTF-16 body; repair/convert it with bash first.`;
666
696
  if (preDec.text.trim() !== "")
@@ -686,7 +716,13 @@ export function makeEditFileTool(env, state, rootCanonical, cwdRef, additionalRo
686
716
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
687
717
  if (!readBin.ok)
688
718
  return `Error (Edit): cannot read "${path}": ${readBin.error.message}`;
689
- const decoded = decodeTextBytes(readBin.value);
719
+ if (readBin.value.byteLength > MAX_EDIT_BYTES) {
720
+ return `Error (Edit): "${path}" is too large to edit (${formatByteSize(readBin.value.byteLength)}). Maximum editable file size is ${formatByteSize(MAX_EDIT_BYTES)}.`;
721
+ }
722
+ const decodedResult = decodeEditBytes(readBin.value, path);
723
+ if (!decodedResult.ok)
724
+ return decodedResult.message;
725
+ const decoded = decodedResult.value;
690
726
  if (decoded.malformed)
691
727
  return `Error (Edit): "${path}" has a truncated UTF-16 body; repair/convert it with bash first.`;
692
728
  const original = decoded.text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.428.0",
3
+ "version": "1.430.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",