@sema-agent/core 1.450.0 → 1.452.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 (57) hide show
  1. package/dist/agents/observer.js +3 -0
  2. package/dist/agents/subagent.js +51 -12
  3. package/dist/core/ask-question.js +5 -5
  4. package/dist/core/background-agent-store.d.ts +2 -0
  5. package/dist/core/background-agent-store.js +5 -1
  6. package/dist/core/exec-output-tail.d.ts +18 -1
  7. package/dist/core/exec-output-tail.js +38 -5
  8. package/dist/core/lsp-session.d.ts +1 -0
  9. package/dist/core/lsp-session.js +24 -6
  10. package/dist/core/lsp.d.ts +10 -0
  11. package/dist/core/lsp.js +63 -6
  12. package/dist/core/mailbox-store.d.ts +3 -2
  13. package/dist/core/mailbox-store.js +19 -4
  14. package/dist/core/memory.js +1 -1
  15. package/dist/core/runner/prepare-task.js +11 -1
  16. package/dist/core/runner/runtask.js +1 -1
  17. package/dist/core/session-reconcile.js +5 -2
  18. package/dist/core/task-notification.d.ts +1 -0
  19. package/dist/core/task-registry.d.ts +15 -9
  20. package/dist/core/task-registry.js +290 -53
  21. package/dist/core/tool-result-store.js +2 -2
  22. package/dist/core/tools.d.ts +5 -0
  23. package/dist/core/tools.js +3 -0
  24. package/dist/core/types.d.ts +2 -0
  25. package/dist/core/workflow-journal-store.d.ts +2 -0
  26. package/dist/core/workflow-journal-store.js +14 -0
  27. package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
  28. package/dist/engine/execution-env/node-execution-env.js +130 -20
  29. package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
  30. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  31. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  32. package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -2
  35. package/dist/orchestration/run-workflow-tool.d.ts +2 -0
  36. package/dist/orchestration/run-workflow-tool.js +35 -6
  37. package/dist/orchestration/workflow.d.ts +9 -0
  38. package/dist/orchestration/workflow.js +80 -5
  39. package/dist/stores/cc/mailbox-store.js +6 -1
  40. package/dist/stores/file/background-agent-store.js +3 -2
  41. package/dist/stores/file/mailbox-store.d.ts +1 -1
  42. package/dist/stores/file/mailbox-store.js +9 -7
  43. package/dist/tools/fs/encoding.d.ts +5 -0
  44. package/dist/tools/fs/encoding.js +6 -0
  45. package/dist/tools/fs/index.js +184 -120
  46. package/dist/tools/fs/notebook.d.ts +43 -0
  47. package/dist/tools/fs/notebook.js +141 -0
  48. package/dist/tools/fs/repo-map.js +2 -2
  49. package/dist/tools/fs/search.js +141 -12
  50. package/dist/tools/gitea-issue.js +4 -2
  51. package/dist/tools/monitor.js +12 -8
  52. package/dist/tools/scheduler-tools.js +16 -16
  53. package/dist/tools/task-list.js +34 -12
  54. package/dist/tools/web.d.ts +2 -0
  55. package/dist/tools/web.js +105 -19
  56. package/dist/tools/worktree.js +14 -14
  57. package/package.json +1 -1
@@ -326,22 +326,41 @@ export function createTaskListTools(store) {
326
326
  t.metadata[k] = v;
327
327
  }
328
328
  }
329
+ const ignoredDeps = [];
330
+ let linkedBlocks = false;
331
+ let linkedBlockedBy = false;
329
332
  for (const b of a.addBlocks ?? []) {
330
- if (!t.blocks.includes(b))
331
- t.blocks.push(b);
332
333
  const other = b === id ? t : await tx.get(b);
333
- if (other && !other.blockedBy.includes(id)) {
334
+ if (!other) {
335
+ if (!ignoredDeps.includes(b))
336
+ ignoredDeps.push(b);
337
+ continue;
338
+ }
339
+ if (!t.blocks.includes(b)) {
340
+ t.blocks.push(b);
341
+ linkedBlocks = true;
342
+ }
343
+ if (!other.blockedBy.includes(id)) {
334
344
  other.blockedBy.push(id);
345
+ linkedBlocks = true;
335
346
  if (other !== t)
336
347
  await tx.set(b, other);
337
348
  }
338
349
  }
339
350
  for (const b of a.addBlockedBy ?? []) {
340
- if (!t.blockedBy.includes(b))
341
- t.blockedBy.push(b);
342
351
  const other = b === id ? t : await tx.get(b);
343
- if (other && !other.blocks.includes(id)) {
352
+ if (!other) {
353
+ if (!ignoredDeps.includes(b))
354
+ ignoredDeps.push(b);
355
+ continue;
356
+ }
357
+ if (!t.blockedBy.includes(b)) {
358
+ t.blockedBy.push(b);
359
+ linkedBlockedBy = true;
360
+ }
361
+ if (!other.blocks.includes(id)) {
344
362
  other.blocks.push(id);
363
+ linkedBlockedBy = true;
345
364
  if (other !== t)
346
365
  await tx.set(b, other);
347
366
  }
@@ -354,10 +373,13 @@ export function createTaskListTools(store) {
354
373
  a.activeForm !== undefined ? "activeForm" : "",
355
374
  a.owner !== undefined ? "owner" : "",
356
375
  a.metadata ? "metadata" : "",
357
- a.addBlocks?.length ? "blocks" : "",
358
- a.addBlockedBy?.length ? "blockedBy" : "",
376
+ linkedBlocks ? "blocks" : "",
377
+ linkedBlockedBy ? "blockedBy" : "",
359
378
  ].filter(Boolean);
360
- return { content: `Updated task #${id}${changed.length ? ` ${changed.join(", ")}` : ""}`, details: { type: "task", task: snapTask(t) } };
379
+ const ignoredNote = ignoredDeps.length
380
+ ? ` (ignored: no such task ${ignoredDeps.map((x) => `#${x}`).join(", ")})`
381
+ : "";
382
+ return { content: `Updated task #${id}${changed.length ? ` ${changed.join(", ")}` : ""}${ignoredNote}`, details: { type: "task", task: snapTask(t) } };
361
383
  }),
362
384
  };
363
385
  const taskList = {
@@ -393,12 +415,12 @@ export function createTaskListTools(store) {
393
415
  if (all.length === 0) {
394
416
  return { content: "The task list is empty.", details: { type: "task-list", tasks: [] } };
395
417
  }
396
- const completed = new Set(all.filter((t) => t.status === "completed").map((t) => t.id));
418
+ const openIds = new Set(all.filter((x) => x.status !== "completed").map((x) => x.id));
397
419
  const lines = all.map((t) => {
398
- const liveBlockers = t.blockedBy.filter((b) => !completed.has(b));
420
+ const liveBlockers = t.blockedBy.filter((b) => openIds.has(b));
399
421
  return `#${t.id} [${t.status}] ${t.subject}${t.owner ? ` (owner: ${t.owner})` : ""}${liveBlockers.length ? ` (blocked by: ${liveBlockers.join(", ")})` : ""}`;
400
422
  });
401
- const open = all.filter((t) => t.status !== "completed").length;
423
+ const open = openIds.size;
402
424
  return {
403
425
  content: `${lines.join("\n")}\n(${all.length} total, ${open} open)`,
404
426
  details: { type: "task-list", tasks: all.map(snapTask) },
@@ -25,5 +25,7 @@ export interface WebSearchConfig {
25
25
  snippet: string;
26
26
  }>>;
27
27
  maxResults?: number;
28
+ timeoutMs?: number;
28
29
  }
30
+ export declare function clipCodePoints(s: string, max: number): string;
29
31
  export declare function createWebSearchTool(config: WebSearchConfig): ToolSpec;
package/dist/tools/web.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "../core/tools.js";
2
+ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
4
+ import { redactSecrets } from "../core/untrusted-egress.js";
4
5
  import { isPrivateHost } from "../core/runner/image.js";
5
6
  const MAX_REDIRECTS = 10;
6
7
  const MAX_URL_LENGTH = 2048;
@@ -229,10 +230,7 @@ export function webFetchToolSpec(config = {}) {
229
230
  const { url, prompt } = args;
230
231
  const startedAt = Date.now();
231
232
  let urlForDetails = url.length > MAX_URL_LENGTH ? url.slice(0, MAX_URL_LENGTH) : url;
232
- const failure = (message, extra = {}) => ({
233
- content: message,
234
- details: { type: "web-fetch", url: urlForDetails, bytes: 0, durationMs: Date.now() - startedAt, ...extra },
235
- });
233
+ const failure = (message, extra = {}) => errorResult(message, { type: "web-fetch", url: urlForDetails, bytes: 0, durationMs: Date.now() - startedAt, ...extra });
236
234
  if (url.length > MAX_URL_LENGTH) {
237
235
  return failure(`Error (WebFetch): URL is ${url.length} characters — the limit is ${MAX_URL_LENGTH}.`);
238
236
  }
@@ -381,6 +379,7 @@ export function webFetchToolSpec(config = {}) {
381
379
  return {
382
380
  content: `Error (WebFetch): the request was interrupted (aborted) before completion${finalResponse ? " while reading the response body" : ""}. No content was retrieved.`,
383
381
  details: { ...errDetails, aborted: true },
382
+ isError: true,
384
383
  };
385
384
  }
386
385
  if (timedOut) {
@@ -391,11 +390,13 @@ export function webFetchToolSpec(config = {}) {
391
390
  ? `Error (WebFetch): the server responded (HTTP ${connectedMeta.status}) to a redirect hop, but the request stalled while following the redirect — timed out after ${timeoutMs}ms. The host IS reachable; retry only if the stall was likely transient, or fetch the redirect target directly.`
392
391
  : `Error (WebFetch): timed out after ${timeoutMs}ms while connecting — the host did not respond or the connection stalled before any response arrived. Do not retry the same URL; continue without it or try a different source.`,
393
392
  details: { ...errDetails, timedOut: true },
393
+ isError: true,
394
394
  };
395
395
  }
396
396
  return {
397
397
  content: `Error (WebFetch): request failed: ${e instanceof Error ? e.message : String(e)}`,
398
398
  details: errDetails,
399
+ isError: true,
399
400
  };
400
401
  }
401
402
  finally {
@@ -447,6 +448,7 @@ export function webFetchToolSpec(config = {}) {
447
448
  durationMs: Date.now() - startedAt,
448
449
  ...transferStateDetails,
449
450
  },
451
+ isError: true,
450
452
  };
451
453
  }
452
454
  if (bodyBytes) {
@@ -474,6 +476,7 @@ export function webFetchToolSpec(config = {}) {
474
476
  durationMs: Date.now() - startedAt,
475
477
  ...transferStateDetails,
476
478
  },
479
+ isError: true,
477
480
  };
478
481
  }
479
482
  }
@@ -571,20 +574,36 @@ export function makeWebFetchSummarizer(brain, model) {
571
574
  return text;
572
575
  };
573
576
  }
577
+ const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
578
+ const SEARCH_ERROR_EXCERPT_CHARS = 2048;
579
+ const SEARCH_TITLE_MAX_CHARS = 300;
580
+ const SEARCH_SNIPPET_MAX_CHARS = 1_000;
581
+ const SEARCH_BODY_MAX_CHARS = 100_000;
582
+ export function clipCodePoints(s, max) {
583
+ const cps = [...s];
584
+ return cps.length > max ? cps.slice(0, max).join("") + "…" : s;
585
+ }
574
586
  function hostMatchesDomain(host, domain) {
575
- const d = domain.toLowerCase().replace(/^\*\./, "").replace(/^\.+|\.+$/g, "");
576
- if (!d)
587
+ const norm = (s) => s.toLowerCase().replace(/^\*\./, "").replace(/^\.+|\.+$/g, "");
588
+ const h = norm(host);
589
+ const d = norm(domain);
590
+ if (!d || !h)
577
591
  return false;
578
- return host === d || host.endsWith("." + d);
592
+ return h === d || h.endsWith("." + d);
579
593
  }
580
594
  function webSearchResultAllowed(url, allowed, blocked) {
581
595
  let host;
582
596
  try {
583
- host = new URL(url).hostname.toLowerCase();
597
+ const u = new URL(url);
598
+ if (u.protocol !== "http:" && u.protocol !== "https:")
599
+ return false;
600
+ host = u.hostname.toLowerCase();
584
601
  }
585
602
  catch {
586
- return !(allowed && allowed.length);
603
+ return false;
587
604
  }
605
+ if (!host)
606
+ return false;
588
607
  if (blocked && blocked.some((d) => hostMatchesDomain(host, d)))
589
608
  return false;
590
609
  if (allowed && allowed.length && !allowed.some((d) => hostMatchesDomain(host, d)))
@@ -612,27 +631,94 @@ export function createWebSearchTool(config) {
612
631
  effect: "read",
613
632
  execute: async (args, ctx) => {
614
633
  const { query, allowed_domains, blocked_domains } = args;
634
+ const startedAt = Date.now();
635
+ const failCard = (extra = {}) => ({
636
+ type: "web-search",
637
+ query,
638
+ results: [],
639
+ durationSeconds: (Date.now() - startedAt) / 1000,
640
+ searchCount: 0,
641
+ ...extra,
642
+ });
615
643
  if (allowed_domains?.length && blocked_domains?.length) {
616
- return "Error: Cannot specify both allowed_domains and blocked_domains in the same request";
644
+ return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard());
617
645
  }
618
- const startedAt = Date.now();
646
+ const timeoutMs = config.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
647
+ const ac = new AbortController();
648
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
649
+ const onOuterAbort = () => ac.abort();
650
+ if (ctx.signal?.aborted)
651
+ ac.abort();
652
+ else
653
+ ctx.signal?.addEventListener("abort", onOuterAbort, { once: true });
654
+ const TIMED_OUT = Symbol("websearch-timeout");
655
+ const abortedFrame = () => errorResult("Error (WebSearch): the search was interrupted (aborted) before completion. No results were retrieved.", failCard({ aborted: true }));
656
+ const timedOutFrame = () => errorResult(`Error (WebSearch): the search backend did not respond within ${timeoutMs}ms. The tool stopped waiting; the backend may still be executing the request.`, failCard({ timedOut: true }));
619
657
  let results;
620
658
  try {
621
- results = await config.search(query, ctx.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains });
659
+ const work = Promise.resolve(config.search(query, ac.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains }));
660
+ work.catch(() => { });
661
+ let fireDeadline;
662
+ const deadline = new Promise((res) => {
663
+ fireDeadline = () => res(TIMED_OUT);
664
+ });
665
+ const deadlineTimer = setTimeout(() => fireDeadline(), timeoutMs);
666
+ deadlineTimer.unref?.();
667
+ try {
668
+ const raced = await Promise.race([work, deadline]);
669
+ if (raced === TIMED_OUT) {
670
+ return ctx.signal?.aborted ? abortedFrame() : timedOutFrame();
671
+ }
672
+ results = raced;
673
+ }
674
+ finally {
675
+ clearTimeout(deadlineTimer);
676
+ }
622
677
  }
623
678
  catch (e) {
624
- return `Error (WebSearch): ${e instanceof Error ? e.message : String(e)}`;
679
+ if (ctx.signal?.aborted)
680
+ return abortedFrame();
681
+ if (ac.signal.aborted)
682
+ return timedOutFrame();
683
+ const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
684
+ const headline = "Error (WebSearch): the search backend failed.";
685
+ return errorResult(msg
686
+ ? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}`
687
+ : headline, failCard());
625
688
  }
626
- results = results.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains)).slice(0, max);
627
- const body = results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join("\n");
628
- const fenced = delimitUntrusted(`WebSearch ${JSON.stringify(query)}`, body || "(no results)");
629
- const modelText = `${fenced}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
689
+ finally {
690
+ clearTimeout(timer);
691
+ ctx.signal?.removeEventListener("abort", onOuterAbort);
692
+ }
693
+ const usable = results.filter((r) => webSearchResultAllowed(r.url));
694
+ const droppedUnusable = results.length - usable.length;
695
+ results = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains)).slice(0, max);
696
+ let clipped = false;
697
+ const shown = results.map((r) => {
698
+ const title = clipCodePoints(r.title, SEARCH_TITLE_MAX_CHARS);
699
+ const url = clipCodePoints(r.url, MAX_URL_LENGTH);
700
+ const snippet = clipCodePoints(r.snippet, SEARCH_SNIPPET_MAX_CHARS);
701
+ if (title !== r.title || url !== r.url || snippet !== r.snippet)
702
+ clipped = true;
703
+ return { title, url, snippet };
704
+ });
705
+ const body = shown.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join("\n");
706
+ if (body.length > SEARCH_BODY_MAX_CHARS)
707
+ clipped = true;
708
+ const fenced = delimitUntrusted(`WebSearch ${JSON.stringify(query)}`, body || "(no results)", SEARCH_BODY_MAX_CHARS);
709
+ const dropNote = droppedUnusable > 0
710
+ ? `\n\n[WebSearch: ${droppedUnusable} result(s) returned by the backend were dropped — their URL was not a usable http(s) link]`
711
+ : "";
712
+ const clipNote = clipped
713
+ ? "\n\n[WebSearch: one or more result fields exceeded the display limits and were truncated — treat the text above as an excerpt, not the backend's full result.]"
714
+ : "";
715
+ const modelText = `${fenced}${dropNote}${clipNote}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
630
716
  return {
631
717
  content: modelText,
632
718
  details: {
633
719
  type: "web-search",
634
720
  query,
635
- results: [{ tool_use_id: ctx.toolCallId, content: results.map((r) => ({ title: r.title, url: r.url })) }],
721
+ results: [{ tool_use_id: ctx.toolCallId, content: shown.map((r) => ({ title: r.title, url: r.url })) }],
636
722
  durationSeconds: (Date.now() - startedAt) / 1000,
637
723
  searchCount: 1,
638
724
  },
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { Type } from "typebox";
3
- import { defineTool } from "../core/tools.js";
3
+ import { defineTool, errorResult } from "../core/tools.js";
4
4
  import { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "../core/git-worktree-env.js";
5
5
  const ENTER_DESCRIPTION = "Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by " +
6
6
  "project instructions. This tool creates an isolated git worktree and switches the current session into it.\n" +
@@ -113,20 +113,20 @@ export function createWorktreeTools(env, opts) {
113
113
  execute: async (args) => {
114
114
  const { name, path, baseRef } = args;
115
115
  if (path !== undefined && name !== undefined) {
116
- return "Error (EnterWorktree): `name` and `path` are mutually exclusive — `path` enters an existing worktree, `name` creates a new one.";
116
+ return errorResult("Error (EnterWorktree): `name` and `path` are mutually exclusive — `path` enters an existing worktree, `name` creates a new one.");
117
117
  }
118
118
  if (path !== undefined && baseRef !== undefined) {
119
- return "Error (EnterWorktree): `baseRef` only applies when creating a new worktree — an existing worktree entered via `path` keeps whatever state it has.";
119
+ return errorResult("Error (EnterWorktree): `baseRef` only applies when creating a new worktree — an existing worktree entered via `path` keeps whatever state it has.");
120
120
  }
121
121
  if (session.current && path === undefined) {
122
- return (`Error (EnterWorktree): already in a worktree session (${session.current.worktreeDir}). ` +
122
+ return errorResult(`Error (EnterWorktree): already in a worktree session (${session.current.worktreeDir}). ` +
123
123
  "Use ExitWorktree to leave it before creating a new worktree, or pass `path` to switch into an " +
124
124
  "existing worktree of this repository.");
125
125
  }
126
126
  const probe = await env.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
127
127
  if (!probe.ok || probe.value.exitCode !== 0) {
128
128
  const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : String(probe.error);
129
- return (`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
129
+ return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
130
130
  `${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
131
131
  "so worktree isolation is unavailable here.");
132
132
  }
@@ -134,31 +134,31 @@ export function createWorktreeTools(env, opts) {
134
134
  const candidate = resolve(cwdRef.current, path);
135
135
  const top = await env.exec("git rev-parse --show-toplevel", { cwd: candidate });
136
136
  if (!top.ok || top.value.exitCode !== 0) {
137
- return `Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory).`;
137
+ return errorResult(`Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory).`);
138
138
  }
139
139
  const target = top.value.stdout.trim();
140
140
  const list = await env.exec("git worktree list --porcelain", { cwd: repoRoot });
141
141
  if (!list.ok || list.value.exitCode !== 0) {
142
- return "Error (EnterWorktree): could not read `git worktree list` for this repository.";
142
+ return errorResult("Error (EnterWorktree): could not read `git worktree list` for this repository.");
143
143
  }
144
144
  const registered = list.value.stdout
145
145
  .split("\n")
146
146
  .filter((l) => l.startsWith("worktree "))
147
147
  .map((l) => l.slice("worktree ".length).trim());
148
148
  if (!registered.includes(target)) {
149
- return (`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
149
+ return errorResult(`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
150
150
  "(it does not appear in `git worktree list`).");
151
151
  }
152
152
  if (target === registered[0]) {
153
- return `Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter.`;
153
+ return errorResult(`Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter.`);
154
154
  }
155
155
  if (target !== repoRoot && !target.startsWith(repoRoot.endsWith("/") ? repoRoot : `${repoRoot}/`)) {
156
- return (`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
156
+ return errorResult(`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
157
157
  "file containment stays anchored at the task root, so it cannot be entered from this session.");
158
158
  }
159
159
  const head = await env.exec("git rev-parse HEAD", { cwd: target });
160
160
  if (!head.ok || head.value.exitCode !== 0) {
161
- return `Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`;
161
+ return errorResult(`Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`);
162
162
  }
163
163
  const previous = session.current;
164
164
  const originalCwd = previous ? previous.originalCwd : cwdRef.current;
@@ -172,7 +172,7 @@ export function createWorktreeTools(env, opts) {
172
172
  }
173
173
  const base = await resolveBaseCommit(env, repoRoot, baseRef ?? "fresh");
174
174
  if ("error" in base)
175
- return `Error (EnterWorktree): ${base.error}.`;
175
+ return errorResult(`Error (EnterWorktree): ${base.error}.`);
176
176
  let createdDir;
177
177
  try {
178
178
  const { worktreeDir } = await addWorktree(env, {
@@ -187,7 +187,7 @@ export function createWorktreeTools(env, opts) {
187
187
  createdDir = worktreeDir;
188
188
  }
189
189
  catch (e) {
190
- return `Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`;
190
+ return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
191
191
  }
192
192
  session.current = {
193
193
  worktreeDir: createdDir,
@@ -227,7 +227,7 @@ export function createWorktreeTools(env, opts) {
227
227
  const discardChanges = raw.discardChanges ?? raw.discard_changes;
228
228
  const wantsRemove = raw.action === "remove";
229
229
  if (keep === true && discardChanges === true) {
230
- return "Error (ExitWorktree): `keep: true` and `discardChanges: true` contradict each other — pick one.";
230
+ return errorResult("Error (ExitWorktree): `keep: true` and `discardChanges: true` contradict each other — pick one.");
231
231
  }
232
232
  if (!session.current) {
233
233
  return ("No-op: there is no active EnterWorktree session to exit. This tool only operates on the " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.450.0",
3
+ "version": "1.452.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",