@sema-agent/core 1.429.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.
@@ -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;
@@ -2511,6 +2541,26 @@ task_id: ${taskId}
2511
2541
  child = await opts.runner.runTask(buildChildSpec(ctx.signal), childInternals);
2512
2542
  }
2513
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
+ }
2514
2564
  const ns = child.stats.nested;
2515
2565
  ctx.reportUsage?.({
2516
2566
  tokens: child.stats.tokens + (ns?.tokens ?? 0),
@@ -2545,6 +2595,7 @@ task_id: ${taskId}
2545
2595
  const worktreeLine = await finishWorktree();
2546
2596
  const errClass = classifySubagentError(child);
2547
2597
  const lines = [
2598
+ ...(handbackWarning ? [handbackWarning, ""] : []),
2548
2599
  `[Sub-agent report${def ? ` · ${def.name}` : ""}${a.description ? ` · ${String(a.description)}` : ""}]`,
2549
2600
  `status: ${child.status}`,
2550
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;
@@ -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.429.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",