@sema-agent/core 1.451.0 → 2.0.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 (191) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/observer.js +3 -0
  5. package/dist/agents/repair-loop.d.ts +1 -1
  6. package/dist/agents/repair-loop.js +1 -1
  7. package/dist/agents/retain-ledger.d.ts +60 -0
  8. package/dist/agents/retain-ledger.js +225 -0
  9. package/dist/agents/roster-store.d.ts +1 -0
  10. package/dist/agents/roster-store.js +9 -0
  11. package/dist/agents/send-message-tool.d.ts +41 -0
  12. package/dist/agents/send-message-tool.js +469 -0
  13. package/dist/agents/subagent.d.ts +33 -108
  14. package/dist/agents/subagent.js +40 -813
  15. package/dist/agents/teacher.d.ts +0 -1
  16. package/dist/agents/teacher.js +0 -9
  17. package/dist/bin/sema-tb.js +3 -3
  18. package/dist/brain/anthropic.js +15 -40
  19. package/dist/brain/openai.js +15 -40
  20. package/dist/brain/reasoning.d.ts +1 -1
  21. package/dist/brain/stream-engine.js +2 -2
  22. package/dist/brain/stream-shared.d.ts +17 -0
  23. package/dist/brain/stream-shared.js +50 -0
  24. package/dist/brain/timeout.d.ts +1 -1
  25. package/dist/brain/timeout.js +1 -1
  26. package/dist/config/catalog.js +1 -3
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.js +6 -0
  29. package/dist/core/ask-question.d.ts +1 -1
  30. package/dist/core/ask-question.js +5 -5
  31. package/dist/core/background-agent-store.d.ts +2 -0
  32. package/dist/core/background-agent-store.js +5 -1
  33. package/dist/core/background-shell.d.ts +1 -1
  34. package/dist/core/checkpoint-store.d.ts +0 -1
  35. package/dist/core/exec-gate.d.ts +1 -1
  36. package/dist/core/exec-output-tail.d.ts +19 -1
  37. package/dist/core/exec-output-tail.js +44 -5
  38. package/dist/core/file-snapshot-store.d.ts +1 -1
  39. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  40. package/dist/core/git-worktree-env.d.ts +1 -1
  41. package/dist/core/hooks.d.ts +1 -1
  42. package/dist/core/image-downsample.d.ts +44 -0
  43. package/dist/core/image-downsample.js +97 -0
  44. package/dist/core/lsp-session.d.ts +1 -0
  45. package/dist/core/lsp-session.js +24 -6
  46. package/dist/core/lsp.d.ts +11 -1
  47. package/dist/core/lsp.js +63 -6
  48. package/dist/core/mailbox-store.d.ts +3 -2
  49. package/dist/core/mailbox-store.js +19 -4
  50. package/dist/core/mcp.d.ts +3 -45
  51. package/dist/core/mcp.js +2 -97
  52. package/dist/core/media-byte-cap.d.ts +1 -1
  53. package/dist/core/memory-recall.d.ts +0 -12
  54. package/dist/core/memory-recall.js +0 -222
  55. package/dist/core/memory.d.ts +0 -6
  56. package/dist/core/memory.js +0 -88
  57. package/dist/core/message-utils.d.ts +1 -1
  58. package/dist/core/oracle-isolation.d.ts +1 -1
  59. package/dist/core/remote-env.d.ts +1 -1
  60. package/dist/core/roles.d.ts +1 -1
  61. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  62. package/dist/core/runner/assemble-result.d.ts +0 -2
  63. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  64. package/dist/core/runner/prepare-memory.d.ts +16 -0
  65. package/dist/core/runner/prepare-memory.js +191 -0
  66. package/dist/core/runner/prepare-task.d.ts +4 -28
  67. package/dist/core/runner/prepare-task.js +29 -208
  68. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  69. package/dist/core/runner/runtask.d.ts +2 -3
  70. package/dist/core/runner/runtask.js +337 -529
  71. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  73. package/dist/core/runner/synthetic-tools.js +4 -4
  74. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  75. package/dist/core/runner/tool-disclosure.js +2 -2
  76. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  77. package/dist/core/runner/tool-output-projection.js +81 -0
  78. package/dist/core/runtime.d.ts +1 -1
  79. package/dist/core/scheduler.d.ts +1 -1
  80. package/dist/core/sensitive-path-policy.d.ts +1 -1
  81. package/dist/core/session-reconcile.d.ts +1 -1
  82. package/dist/core/session-reconcile.js +5 -2
  83. package/dist/core/session-store.d.ts +2 -1
  84. package/dist/core/session-store.js +2 -1
  85. package/dist/core/session.d.ts +0 -1
  86. package/dist/core/task-notification.d.ts +1 -0
  87. package/dist/core/task-registry-agent.d.ts +115 -0
  88. package/dist/core/task-registry-agent.js +1163 -0
  89. package/dist/core/task-registry-monitor.d.ts +13 -0
  90. package/dist/core/task-registry-monitor.js +347 -0
  91. package/dist/core/task-registry-shared.d.ts +294 -0
  92. package/dist/core/task-registry-shared.js +271 -0
  93. package/dist/core/task-registry-workflow.d.ts +5 -0
  94. package/dist/core/task-registry-workflow.js +143 -0
  95. package/dist/core/task-registry.d.ts +54 -213
  96. package/dist/core/task-registry.js +190 -1766
  97. package/dist/core/task-tool-shape.d.ts +32 -0
  98. package/dist/core/task-tool-shape.js +83 -0
  99. package/dist/core/tool-policy.d.ts +1 -2
  100. package/dist/core/tool-result-budget.d.ts +1 -1
  101. package/dist/core/tool-result-store.d.ts +2 -2
  102. package/dist/core/tool-result-store.js +3 -3
  103. package/dist/core/tools.d.ts +7 -3
  104. package/dist/core/tools.js +3 -8
  105. package/dist/core/trace.d.ts +2 -2
  106. package/dist/core/types.d.ts +1 -29
  107. package/dist/core/workflow-journal-store.d.ts +2 -0
  108. package/dist/core/workflow-journal-store.js +14 -0
  109. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  110. package/dist/core/workflow-run-store-contract.js +19 -19
  111. package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
  112. package/dist/engine/execution-env/node-execution-env.js +184 -44
  113. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  114. package/dist/engine/lsp/frame-decoder.js +1 -1
  115. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
  116. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  117. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  118. package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
  119. package/dist/index.d.ts +18 -16
  120. package/dist/index.js +15 -13
  121. package/dist/internal/harness-types.d.ts +7 -0
  122. package/dist/internal/harness-types.js +1 -0
  123. package/dist/internal/harness.d.ts +1 -7
  124. package/dist/internal/harness.js +1 -0
  125. package/dist/orchestration/goal.js +2 -2
  126. package/dist/orchestration/run-workflow-tool.d.ts +2 -1
  127. package/dist/orchestration/run-workflow-tool.js +29 -6
  128. package/dist/orchestration/workflow-types.d.ts +193 -0
  129. package/dist/orchestration/workflow-types.js +54 -0
  130. package/dist/orchestration/workflow.d.ts +3 -184
  131. package/dist/orchestration/workflow.js +87 -64
  132. package/dist/prompt-assembly/assemble.d.ts +3 -3
  133. package/dist/prompt-assembly/assemble.js +0 -16
  134. package/dist/prompt-assembly/explain.d.ts +1 -1
  135. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  136. package/dist/prompts/default.d.ts +1 -6
  137. package/dist/prompts/default.js +3 -11
  138. package/dist/scenarios/env.d.ts +1 -1
  139. package/dist/scenarios/full-body.d.ts +0 -3
  140. package/dist/scenarios/full-body.js +0 -2
  141. package/dist/stores/cc/mailbox-store.js +6 -1
  142. package/dist/stores/file/background-agent-store.d.ts +2 -15
  143. package/dist/stores/file/background-agent-store.js +33 -102
  144. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  145. package/dist/stores/file/checkpoint-store.js +47 -119
  146. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  147. package/dist/stores/file/mailbox-store.d.ts +1 -1
  148. package/dist/stores/file/mailbox-store.js +9 -7
  149. package/dist/stores/file/shared-ledger.d.ts +35 -0
  150. package/dist/stores/file/shared-ledger.js +94 -0
  151. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  152. package/dist/stores/file/workflow-run-store.js +24 -93
  153. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  154. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  155. package/dist/tools/fs/encoding.d.ts +5 -0
  156. package/dist/tools/fs/encoding.js +6 -0
  157. package/dist/tools/fs/fs-bash.d.ts +31 -0
  158. package/dist/tools/fs/fs-bash.js +672 -0
  159. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  160. package/dist/tools/fs/fs-pdf.js +236 -0
  161. package/dist/tools/fs/fs-read.d.ts +8 -0
  162. package/dist/tools/fs/fs-read.js +286 -0
  163. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  164. package/dist/tools/fs/fs-search-tools.js +173 -0
  165. package/dist/tools/fs/fs-shared.d.ts +55 -0
  166. package/dist/tools/fs/fs-shared.js +144 -0
  167. package/dist/tools/fs/fs-write.d.ts +8 -0
  168. package/dist/tools/fs/fs-write.js +423 -0
  169. package/dist/tools/fs/index.d.ts +12 -65
  170. package/dist/tools/fs/index.js +26 -2026
  171. package/dist/tools/fs/notebook.d.ts +43 -0
  172. package/dist/tools/fs/notebook.js +141 -0
  173. package/dist/tools/fs/pdf.d.ts +1 -1
  174. package/dist/tools/fs/repo-map.d.ts +3 -3
  175. package/dist/tools/fs/repo-map.js +3 -3
  176. package/dist/tools/fs/safety.d.ts +1 -1
  177. package/dist/tools/fs/search.d.ts +1 -1
  178. package/dist/tools/fs/search.js +141 -12
  179. package/dist/tools/gitea-issue.js +4 -2
  180. package/dist/tools/monitor.d.ts +1 -1
  181. package/dist/tools/monitor.js +12 -8
  182. package/dist/tools/scheduler-tools.d.ts +1 -1
  183. package/dist/tools/scheduler-tools.js +16 -16
  184. package/dist/tools/task-list.js +34 -12
  185. package/dist/tools/web.d.ts +4 -2
  186. package/dist/tools/web.js +106 -20
  187. package/dist/tools/worktree.d.ts +1 -1
  188. package/dist/tools/worktree.js +14 -14
  189. package/package.json +1 -1
  190. package/dist/core/dynamic-recall.d.ts +0 -32
  191. package/dist/core/dynamic-recall.js +0 -76
@@ -1,9 +1,11 @@
1
+ import { StringDecoder } from "node:string_decoder";
1
2
  export const MAX_EXEC_OUTPUT_BYTES = 8 * 1024 * 1024;
2
3
  export class RollingTailBuffer {
3
4
  maxBytes;
4
5
  chunks = [];
5
6
  size = 0;
6
- dropped = 0;
7
+ headDropped = 0;
8
+ sourceSkipped = 0;
7
9
  constructor(maxBytes = MAX_EXEC_OUTPUT_BYTES) {
8
10
  this.maxBytes = maxBytes;
9
11
  }
@@ -18,25 +20,62 @@ export class RollingTailBuffer {
18
20
  if (over >= head.length) {
19
21
  this.chunks.shift();
20
22
  this.size -= head.length;
21
- this.dropped += head.length;
23
+ this.headDropped += head.length;
22
24
  }
23
25
  else {
24
26
  this.chunks[0] = head.subarray(over);
25
27
  this.size -= over;
26
- this.dropped += over;
28
+ this.headDropped += over;
27
29
  }
28
30
  }
29
31
  }
30
32
  recordSkippedBytes(n) {
31
33
  if (n > 0)
32
- this.dropped += n;
34
+ this.sourceSkipped += n;
33
35
  }
34
36
  result() {
35
- return { text: Buffer.concat(this.chunks).toString("utf8"), droppedBytes: this.dropped };
37
+ return { text: Buffer.concat(this.chunks).toString("utf8"), droppedBytes: this.headDropped + this.sourceSkipped };
36
38
  }
39
+ bytes() {
40
+ return Buffer.concat(this.chunks);
41
+ }
42
+ headDroppedBytes() {
43
+ return this.headDropped;
44
+ }
45
+ sourceSkippedBytes() {
46
+ return this.sourceSkipped;
47
+ }
48
+ }
49
+ export function newStreamCursorState(tail = new RollingTailBuffer()) {
50
+ return { tail, cursorBytes: 0, acceptedBytes: 0, disclosedSkippedBytes: 0, decoder: new StringDecoder("utf8") };
51
+ }
52
+ export function sliceStreamIncrement(s, terminal) {
53
+ const headDropped = s.tail.headDroppedBytes();
54
+ const sourceSkipped = s.tail.sourceSkippedBytes();
55
+ const newSkipped = Math.max(0, sourceSkipped - s.disclosedSkippedBytes);
56
+ s.disclosedSkippedBytes = sourceSkipped;
57
+ const droppedBeforeCursor = Math.max(0, headDropped - s.cursorBytes) + newSkipped;
58
+ const buf = s.tail.bytes();
59
+ const startInTail = Math.min(Math.max(s.cursorBytes, headDropped) - headDropped, buf.length);
60
+ const incBuf = startInTail <= 0 ? buf : buf.subarray(startInTail);
61
+ if (droppedBeforeCursor > 0)
62
+ s.decoder = new StringDecoder("utf8");
63
+ let inc = s.decoder.write(incBuf);
64
+ if (terminal) {
65
+ inc += s.decoder.end();
66
+ s.decoder = new StringDecoder("utf8");
67
+ }
68
+ s.cursorBytes = s.acceptedBytes;
69
+ return { inc, droppedBeforeCursor };
37
70
  }
38
71
  export function markTruncated(text, droppedBytes) {
39
72
  return droppedBytes > 0
40
73
  ? `[... ${droppedBytes} earlier byte(s) truncated; showing last ${Buffer.byteLength(text, "utf8")} ...]\n${text}`
41
74
  : text;
42
75
  }
76
+ export function markLiveSpoolRotations(text, liveRotations) {
77
+ if (liveRotations <= 0)
78
+ return text;
79
+ const note = `[spool reclaimed ${liveRotations} time(s) while the process was still writing; bytes appended during those windows may be missing beyond any counted truncation]`;
80
+ return text === "" ? note : `${text}${text.endsWith("\n") ? "" : "\n"}${note}`;
81
+ }
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv } from "../internal/harness.js";
1
+ import type { ExecutionEnv } from "../internal/harness-types.js";
2
2
  export interface FileSnapshotError {
3
3
  code: "too_large" | "enumerate_failed" | "read_failed" | "restore_failed" | "not_found" | "aborted";
4
4
  message: string;
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv } from "../internal/harness.js";
1
+ import type { ExecutionEnv } from "../internal/harness-types.js";
2
2
  import type { ToolPolicy } from "./tool-policy.js";
3
3
  export interface FsWriteGatePolicyOptions {
4
4
  env: ExecutionEnv;
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv } from "../internal/harness.js";
1
+ import type { ExecutionEnv } from "../internal/harness-types.js";
2
2
  export declare const WORKTREE_PARENT = ".sema-worktrees";
3
3
  export interface AddWorktreeOptions {
4
4
  repoRoot: string;
@@ -1,5 +1,5 @@
1
1
  import type { DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
2
- import type { SessionTreeEntry } from "../internal/harness.js";
2
+ import type { SessionTreeEntry } from "../internal/harness-types.js";
3
3
  import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "./tool-policy.js";
4
4
  export interface Hooks {
5
5
  preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise<PreToolUseResult | undefined>;
@@ -0,0 +1,44 @@
1
+ export declare const MCP_IMAGE_MAX_BASE64: number;
2
+ export declare const IMAGE_TARGET_RAW_SIZE: number;
3
+ export declare const IMAGE_MAX_WIDTH = 2000;
4
+ export declare const IMAGE_MAX_HEIGHT = 2000;
5
+ export type McpImageResizer = (base64: string, mimeType: string) => Promise<{
6
+ base64: string;
7
+ mimeType: string;
8
+ } | undefined>;
9
+ export interface ImageDimensions {
10
+ originalWidth: number;
11
+ originalHeight: number;
12
+ displayWidth: number;
13
+ displayHeight: number;
14
+ }
15
+ export interface DownsampledImage {
16
+ base64: string;
17
+ mimeType: string;
18
+ dimensions?: ImageDimensions;
19
+ }
20
+ export type ImageDownsampler = (input: Buffer, mimeType: string) => Promise<DownsampledImage | undefined>;
21
+ type SharpFactory = (input: Buffer) => SharpOps;
22
+ interface SharpOps {
23
+ metadata: () => Promise<{
24
+ width?: number;
25
+ height?: number;
26
+ format?: string;
27
+ }>;
28
+ resize: (w: number, h: number, o: {
29
+ fit: "inside";
30
+ withoutEnlargement: boolean;
31
+ }) => SharpOps;
32
+ jpeg: (o: {
33
+ quality: number;
34
+ }) => SharpOps;
35
+ png: (o: {
36
+ compressionLevel: number;
37
+ palette: boolean;
38
+ }) => SharpOps;
39
+ toBuffer: () => Promise<Buffer>;
40
+ }
41
+ export declare function createImageDownsampler(sharp: SharpFactory): ImageDownsampler;
42
+ export declare function sharpImageDownsampler(): Promise<ImageDownsampler | undefined>;
43
+ export declare function sharpImageResizer(): Promise<McpImageResizer | undefined>;
44
+ export {};
@@ -0,0 +1,97 @@
1
+ export const MCP_IMAGE_MAX_BASE64 = 5 * 1024 * 1024;
2
+ export const IMAGE_TARGET_RAW_SIZE = (MCP_IMAGE_MAX_BASE64 * 3) / 4;
3
+ export const IMAGE_MAX_WIDTH = 2000;
4
+ export const IMAGE_MAX_HEIGHT = 2000;
5
+ export function createImageDownsampler(sharp) {
6
+ const BOX = { fit: "inside", withoutEnlargement: true };
7
+ return async (input, mimeType) => {
8
+ if (input.length === 0)
9
+ return undefined;
10
+ try {
11
+ const meta = await sharp(input).metadata();
12
+ const fmt = meta.format === "jpg" ? "jpeg" : meta.format;
13
+ const passMime = fmt ? `image/${fmt}` : mimeType;
14
+ const originalSize = input.length;
15
+ if (!meta.width || !meta.height) {
16
+ if (originalSize > IMAGE_TARGET_RAW_SIZE) {
17
+ const buf = await sharp(input).jpeg({ quality: 80 }).toBuffer();
18
+ return { base64: buf.toString("base64"), mimeType: "image/jpeg" };
19
+ }
20
+ return { base64: input.toString("base64"), mimeType: passMime };
21
+ }
22
+ const originalWidth = meta.width;
23
+ const originalHeight = meta.height;
24
+ let width = originalWidth;
25
+ let height = originalHeight;
26
+ const dims = (dw, dh) => ({ originalWidth, originalHeight, displayWidth: dw, displayHeight: dh });
27
+ if (originalSize <= IMAGE_TARGET_RAW_SIZE && width <= IMAGE_MAX_WIDTH && height <= IMAGE_MAX_HEIGHT) {
28
+ return { base64: input.toString("base64"), mimeType: passMime, dimensions: dims(width, height) };
29
+ }
30
+ const needsDimensionResize = width > IMAGE_MAX_WIDTH || height > IMAGE_MAX_HEIGHT;
31
+ const isPng = fmt === "png";
32
+ if (!needsDimensionResize) {
33
+ if (isPng) {
34
+ const png = await sharp(input).png({ compressionLevel: 9, palette: true }).toBuffer();
35
+ if (png.length <= IMAGE_TARGET_RAW_SIZE)
36
+ return { base64: png.toString("base64"), mimeType: "image/png", dimensions: dims(width, height) };
37
+ }
38
+ for (const quality of [80, 60, 40, 20]) {
39
+ const buf = await sharp(input).jpeg({ quality }).toBuffer();
40
+ if (buf.length <= IMAGE_TARGET_RAW_SIZE)
41
+ return { base64: buf.toString("base64"), mimeType: "image/jpeg", dimensions: dims(width, height) };
42
+ }
43
+ }
44
+ if (width > IMAGE_MAX_WIDTH) {
45
+ height = Math.round((height * IMAGE_MAX_WIDTH) / width);
46
+ width = IMAGE_MAX_WIDTH;
47
+ }
48
+ if (height > IMAGE_MAX_HEIGHT) {
49
+ width = Math.round((width * IMAGE_MAX_HEIGHT) / height);
50
+ height = IMAGE_MAX_HEIGHT;
51
+ }
52
+ const resized = await sharp(input).resize(width, height, BOX).toBuffer();
53
+ if (resized.length <= IMAGE_TARGET_RAW_SIZE) {
54
+ return { base64: resized.toString("base64"), mimeType: passMime, dimensions: dims(width, height) };
55
+ }
56
+ if (isPng) {
57
+ const png = await sharp(input).resize(width, height, BOX).png({ compressionLevel: 9, palette: true }).toBuffer();
58
+ if (png.length <= IMAGE_TARGET_RAW_SIZE)
59
+ return { base64: png.toString("base64"), mimeType: "image/png", dimensions: dims(width, height) };
60
+ }
61
+ for (const quality of [80, 60, 40, 20]) {
62
+ const buf = await sharp(input).resize(width, height, BOX).jpeg({ quality }).toBuffer();
63
+ if (buf.length <= IMAGE_TARGET_RAW_SIZE)
64
+ return { base64: buf.toString("base64"), mimeType: "image/jpeg", dimensions: dims(width, height) };
65
+ }
66
+ const smallerWidth = Math.min(width, 1000);
67
+ const smallerHeight = Math.round((height * smallerWidth) / Math.max(width, 1));
68
+ const last = await sharp(input).resize(smallerWidth, smallerHeight, BOX).jpeg({ quality: 20 }).toBuffer();
69
+ return { base64: last.toString("base64"), mimeType: "image/jpeg", dimensions: dims(smallerWidth, smallerHeight) };
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ };
75
+ }
76
+ const SHARP_LIMIT_INPUT_PIXELS = 100_000_000;
77
+ export async function sharpImageDownsampler() {
78
+ try {
79
+ const mod = (await import("sharp"));
80
+ const real = (mod.default ?? mod);
81
+ return createImageDownsampler((input) => real(input, { limitInputPixels: SHARP_LIMIT_INPUT_PIXELS }));
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ export async function sharpImageResizer() {
88
+ const down = await sharpImageDownsampler();
89
+ if (!down)
90
+ return undefined;
91
+ return async (base64, mimeType) => {
92
+ const out = await down(Buffer.from(base64, "base64"), mimeType).catch(() => undefined);
93
+ if (!out || out.base64.length > MCP_IMAGE_MAX_BASE64)
94
+ return undefined;
95
+ return { base64: out.base64, mimeType: out.mimeType };
96
+ };
97
+ }
@@ -18,6 +18,7 @@ export declare class TransportLspSession implements LspSession {
18
18
  request(op: LspOperation, params: LspRequestParams, signal?: AbortSignal): Promise<LspResult>;
19
19
  private syncOpenedFiles;
20
20
  private syncOne;
21
+ private static classifyUnbranded;
21
22
  get closed(): boolean;
22
23
  openedFiles(): string[];
23
24
  warmOpen(filePaths: string[], signal?: AbortSignal): Promise<void>;
@@ -1,4 +1,5 @@
1
1
  import { uriToPath, buildRequest, callHierarchyMethod, parseResult, pathToUri } from "./lsp-protocol.js";
2
+ import { lspFailureOf } from "./lsp.js";
2
3
  export class TransportLspSession {
3
4
  transport;
4
5
  languageId;
@@ -37,7 +38,7 @@ export class TransportLspSession {
37
38
  async request(op, params, signal) {
38
39
  const built = buildRequest(op, params);
39
40
  if (!built)
40
- return { kind: "none" };
41
+ return { kind: "none", reason: "unsupported_operation" };
41
42
  await this.syncOpenedFiles(params.filePath, signal);
42
43
  if (!this.warmupBarrierDone && this.warmup.windowMs > 0) {
43
44
  this.warmupBarrierDone = true;
@@ -49,18 +50,23 @@ export class TransportLspSession {
49
50
  }
50
51
  for (let attempt = 0;; attempt++) {
51
52
  let raw;
53
+ let noHierarchyItem = false;
52
54
  try {
53
55
  raw = await this.transport.request(built.method, built.params, signal);
54
56
  if (op === "incomingCalls" || op === "outgoingCalls") {
55
57
  const items = Array.isArray(raw) ? raw : raw != null ? [raw] : [];
56
- raw = items.length === 0 ? null : await this.transport.request(callHierarchyMethod(op), { item: items[0] }, signal);
58
+ noHierarchyItem = items.length === 0;
59
+ raw = noHierarchyItem ? null : await this.transport.request(callHierarchyMethod(op), { item: items[0] }, signal);
57
60
  }
58
61
  }
59
62
  catch (e) {
60
- this.log("lsp_op_degraded", { op, file: params.filePath, err: e instanceof Error ? e.message : String(e) });
61
- return { kind: "none" };
63
+ const f = lspFailureOf(e) ?? TransportLspSession.classifyUnbranded(signal, this.transport);
64
+ this.log("lsp_op_degraded", { op, file: params.filePath, reason: f.reason, err: e instanceof Error ? e.message : String(e) });
65
+ return { kind: "none", reason: f.reason, ...(f.detail !== undefined ? { detail: f.detail } : {}) };
62
66
  }
63
- const result = parseResult(op, raw);
67
+ const result = noHierarchyItem
68
+ ? { kind: "none", reason: "no_call_hierarchy_item" }
69
+ : parseResult(op, raw);
64
70
  if (result.kind === "none" && this.warmup.windowMs > 0 && Date.now() - this.createdAt < this.warmup.windowMs && !signal?.aborted) {
65
71
  await new Promise((r) => setTimeout(r, this.warmup.retryMs));
66
72
  continue;
@@ -92,7 +98,12 @@ export class TransportLspSession {
92
98
  try {
93
99
  text = await this.readText(filePath, signal);
94
100
  }
95
- catch {
101
+ catch (e) {
102
+ this.log("lsp_sync_skipped", {
103
+ file: filePath,
104
+ error: e instanceof Error ? e.message : String(e),
105
+ opened: this.opened.has(filePath),
106
+ });
96
107
  return;
97
108
  }
98
109
  const cur = this.opened.get(filePath);
@@ -115,6 +126,13 @@ export class TransportLspSession {
115
126
  this.syncing.set(filePath, job);
116
127
  return job.finally(() => this.syncing.delete(filePath));
117
128
  }
129
+ static classifyUnbranded(signal, transport) {
130
+ if (signal?.aborted)
131
+ return { reason: "cancelled" };
132
+ if (transport.closed === true)
133
+ return { reason: "server_terminated" };
134
+ return { reason: "server_error" };
135
+ }
118
136
  get closed() {
119
137
  return this.transport.closed === true;
120
138
  }
@@ -1,4 +1,4 @@
1
- import type { AgentTool, ExecutionEnv } from "../internal/harness.js";
1
+ import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
2
2
  export type LspOperation = "goToDefinition" | "findReferences" | "hover" | "documentSymbol" | "workspaceSymbol" | "goToImplementation" | "prepareCallHierarchy" | "incomingCalls" | "outgoingCalls";
3
3
  export declare const LSP_OPERATIONS: readonly LspOperation[];
4
4
  export interface LspLocation {
@@ -14,6 +14,7 @@ export interface LspSymbolInfo {
14
14
  line: number;
15
15
  character: number;
16
16
  }
17
+ export type LspNoneReason = "unsupported_operation" | "server_error" | "timeout" | "cancelled" | "server_terminated" | "no_call_hierarchy_item";
17
18
  export type LspResult = {
18
19
  kind: "locations";
19
20
  locations: LspLocation[];
@@ -25,7 +26,15 @@ export type LspResult = {
25
26
  contents: string;
26
27
  } | {
27
28
  kind: "none";
29
+ reason?: LspNoneReason;
30
+ detail?: string;
28
31
  };
32
+ export declare const LSP_FAILURE_BRAND = "lspFailureReason";
33
+ export declare function brandLspFailure<E extends Error>(e: E, reason: LspNoneReason, detail?: string): E;
34
+ export declare function lspFailureOf(e: unknown): {
35
+ reason: LspNoneReason;
36
+ detail?: string;
37
+ } | undefined;
29
38
  export interface LspRequestParams {
30
39
  filePath: string;
31
40
  line?: number;
@@ -43,6 +52,7 @@ export interface LspTransport {
43
52
  readonly closed?: boolean;
44
53
  }
45
54
  export type LspReadText = (filePath: string, signal?: AbortSignal) => Promise<string>;
55
+ export declare const MAX_LSP_FILE_BYTES: number;
46
56
  export interface LspServerManager {
47
57
  sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise<LspSession | undefined>;
48
58
  readonly diagnostics?: import("./lsp-diagnostics.js").LspDiagnosticsRegistry;
package/dist/core/lsp.js CHANGED
@@ -1,11 +1,30 @@
1
1
  import { uriToPath } from "./lsp-protocol.js";
2
2
  import { Type } from "typebox";
3
- import { defineTool } from "./tools.js";
3
+ import { defineTool, errorResult } from "./tools.js";
4
4
  export const LSP_OPERATIONS = [
5
5
  "goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol",
6
6
  "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls",
7
7
  ];
8
- function fmt(result) {
8
+ export const LSP_FAILURE_BRAND = "lspFailureReason";
9
+ const LSP_NONE_REASONS = new Set([
10
+ "unsupported_operation", "server_error", "timeout", "cancelled", "server_terminated", "no_call_hierarchy_item",
11
+ ]);
12
+ export function brandLspFailure(e, reason, detail) {
13
+ const carrier = e;
14
+ carrier[LSP_FAILURE_BRAND] = reason;
15
+ if (detail !== undefined)
16
+ carrier.lspFailureDetail = detail;
17
+ return e;
18
+ }
19
+ export function lspFailureOf(e) {
20
+ const r = e?.[LSP_FAILURE_BRAND];
21
+ if (typeof r !== "string" || !LSP_NONE_REASONS.has(r))
22
+ return undefined;
23
+ const d = e.lspFailureDetail;
24
+ return { reason: r, ...(typeof d === "string" ? { detail: d } : {}) };
25
+ }
26
+ export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
27
+ function fmt(result, ctx) {
9
28
  switch (result.kind) {
10
29
  case "hover":
11
30
  return result.contents.trim() || "(no hover information)";
@@ -20,9 +39,30 @@ function fmt(result) {
20
39
  : `Found ${result.symbols.length} symbol(s):\n` +
21
40
  result.symbols.map((s) => `- ${s.name}${s.kind ? ` (${s.kind})` : ""} — ${s.uri}:${s.line}:${s.character}`).join("\n");
22
41
  case "none":
42
+ return fmtNone(result, ctx);
43
+ }
44
+ }
45
+ function fmtNone(result, ctx) {
46
+ const { operation, filePath } = ctx;
47
+ const detail = result.detail;
48
+ switch (result.reason) {
49
+ case undefined:
23
50
  return "No results.";
51
+ case "no_call_hierarchy_item":
52
+ return "No call hierarchy item found at this position";
53
+ case "unsupported_operation":
54
+ return `The language server for "${filePath}" does not support ${operation}${detail ? ` (${detail})` : ""}. Fall back to Grep/Read for this lookup.`;
55
+ case "timeout":
56
+ return `Error performing ${operation}: the language server did not respond in time. This is NOT an empty result — do not conclude the symbol has no ${operation === "findReferences" ? "references" : "results"}; retry or fall back to Grep/Read.`;
57
+ case "server_terminated":
58
+ return `Error performing ${operation}: the language server exited before answering. This is NOT an empty result — retry (a fresh server is spawned on the next call) or fall back to Grep/Read.`;
59
+ case "cancelled":
60
+ return `Error performing ${operation}: the request was cancelled before the language server answered. This is NOT an empty result.`;
61
+ case "server_error":
62
+ return `Error performing ${operation}: ${detail ?? "the language server returned an error"}. This is NOT an empty result — fall back to Grep/Read.`;
24
63
  }
25
64
  }
65
+ const LSP_ERROR_REASONS = new Set(["server_error", "timeout", "cancelled", "server_terminated"]);
26
66
  async function filterIgnored(result, isIgnored) {
27
67
  if (!isIgnored)
28
68
  return result;
@@ -80,14 +120,29 @@ export function createLspTool(manager, opts = {}) {
80
120
  execute: async (args, ctx) => {
81
121
  const { operation, filePath, line, character, query, symbol } = args;
82
122
  if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
83
- return `Error (LSP): the "${operation}" operation needs both line and character.`;
123
+ return errorResult(`Error (LSP): the "${operation}" operation needs both line and character.`);
84
124
  }
85
125
  if (POSITION_OPS.has(operation) && (!Number.isInteger(line) || line < 1 || !Number.isInteger(character) || character < 1)) {
86
- return `Error (LSP): line and character are 1-based integers (as shown in editors); got line=${line}, character=${character}.`;
126
+ return errorResult(`Error (LSP): line and character are 1-based integers (as shown in editors); got line=${line}, character=${character}.`);
87
127
  }
88
128
  const wsQuery = query ?? symbol;
89
129
  if (operation === "workspaceSymbol" && wsQuery === undefined) {
90
- return `Error (LSP): the "workspaceSymbol" operation needs a query.`;
130
+ return errorResult(`Error (LSP): the "workspaceSymbol" operation needs a query.`);
131
+ }
132
+ if (opts.env) {
133
+ const isAbsForm = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath);
134
+ const cwd = opts.env.cwd;
135
+ const probe = isAbsForm || !cwd ? filePath : cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath;
136
+ const info = await opts.env.fileInfo(probe, ctx.signal);
137
+ if (!info.ok && info.error.code === "not_found")
138
+ return errorResult(`Error (LSP): File does not exist: ${filePath}`);
139
+ if (!info.ok && info.error.code === "invalid")
140
+ return errorResult(`Error (LSP): Path is not a file: ${filePath}`);
141
+ if (info.ok && info.value.kind === "directory")
142
+ return errorResult(`Error (LSP): Path is not a file: ${filePath}`);
143
+ if (info.ok && info.value.kind === "file" && info.value.size > MAX_LSP_FILE_BYTES) {
144
+ return `File too large for LSP analysis (${Math.ceil(info.value.size / 1e6)}MB exceeds 10MB limit)`;
145
+ }
91
146
  }
92
147
  const session = await manager.sessionFor(filePath, ctx.signal, opts.env);
93
148
  if (!session) {
@@ -95,7 +150,9 @@ export function createLspTool(manager, opts = {}) {
95
150
  }
96
151
  const seamCharacter = character !== undefined ? character - 1 : undefined;
97
152
  const result = await session.request(operation, { filePath, line, character: seamCharacter, symbol: wsQuery }, ctx.signal);
98
- return fmt(toModelBase(await filterIgnored(result, opts.isPathIgnored)));
153
+ const final = toModelBase(await filterIgnored(result, opts.isPathIgnored));
154
+ const text = fmt(final, { operation, filePath });
155
+ return final.kind === "none" && final.reason !== undefined && LSP_ERROR_REASONS.has(final.reason) ? errorResult(text) : text;
99
156
  },
100
157
  });
101
158
  }
@@ -15,7 +15,7 @@ export interface MailboxStore {
15
15
  sentAt: number;
16
16
  }): Promise<number>;
17
17
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
18
- ack(scope: string, handle: string, upToSeq: number): Promise<void>;
18
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
19
19
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
20
20
  peekCount(scope: string, handle: string): Promise<number>;
21
21
  drop(scope: string, handle: string): Promise<void>;
@@ -23,6 +23,7 @@ export interface MailboxStore {
23
23
  maxAgeMs?: number;
24
24
  }): Promise<number>;
25
25
  }
26
+ export declare function newestSentAt(messages: readonly MailboxMessage[]): number | undefined;
26
27
  export declare class InMemoryMailboxStore implements MailboxStore {
27
28
  private boxes;
28
29
  private key;
@@ -33,7 +34,7 @@ export declare class InMemoryMailboxStore implements MailboxStore {
33
34
  sentAt: number;
34
35
  }): Promise<number>;
35
36
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
36
- ack(scope: string, handle: string, upToSeq: number): Promise<void>;
37
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
37
38
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
38
39
  peekCount(scope: string, handle: string): Promise<number>;
39
40
  drop(scope: string, handle: string): Promise<void>;
@@ -1,6 +1,19 @@
1
+ export function newestSentAt(messages) {
2
+ let newest;
3
+ for (const m of messages) {
4
+ if (!Number.isFinite(m.sentAt))
5
+ return undefined;
6
+ if (newest === undefined || m.sentAt > newest)
7
+ newest = m.sentAt;
8
+ }
9
+ return newest;
10
+ }
1
11
  export class InMemoryMailboxStore {
2
12
  boxes = new Map();
3
13
  key(scope, handle) {
14
+ if (handle.includes("\u0000")) {
15
+ throw new Error(`MailboxStore: unsafe handle ${JSON.stringify(handle)} — a NUL byte makes the composite key ambiguous`);
16
+ }
4
17
  return `${scope}\u0000${handle.toLowerCase()}`;
5
18
  }
6
19
  box(scope, handle) {
@@ -30,12 +43,14 @@ export class InMemoryMailboxStore {
30
43
  b.lease = { owner, expiresAt: now + ttlMs, maxSeq };
31
44
  return { messages: b.messages.map((m) => ({ ...m })), maxSeq };
32
45
  }
33
- async ack(scope, handle, upToSeq) {
46
+ async ack(scope, handle, owner, upToSeq) {
34
47
  const b = this.boxes.get(this.key(scope, handle));
35
48
  if (!b)
36
49
  return;
50
+ if (b.lease === undefined || b.lease.owner !== owner)
51
+ return;
37
52
  b.messages = b.messages.filter((m) => m.seq > upToSeq);
38
- if (b.lease !== undefined && b.lease.maxSeq <= upToSeq)
53
+ if (b.lease.maxSeq <= upToSeq)
39
54
  b.lease = undefined;
40
55
  }
41
56
  async releaseLease(scope, handle, owner) {
@@ -56,8 +71,8 @@ export class InMemoryMailboxStore {
56
71
  for (const b of this.boxes.values()) {
57
72
  if (b.scope !== scope)
58
73
  continue;
59
- const newest = b.messages[b.messages.length - 1];
60
- if (newest !== undefined && newest.sentAt < now - opts.maxAgeMs) {
74
+ const newest = newestSentAt(b.messages);
75
+ if (newest !== undefined && newest < now - opts.maxAgeMs) {
61
76
  b.messages = [];
62
77
  b.lease = undefined;
63
78
  dropped++;
@@ -1,5 +1,6 @@
1
- import type { AgentTool } from "../internal/harness.js";
1
+ import type { AgentTool } from "../internal/harness-types.js";
2
2
  import type { ImageContent, TextContent } from "../internal/llm.js";
3
+ import { type McpImageResizer } from "./image-downsample.js";
3
4
  import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
4
5
  export interface McpToolAxis {
5
6
  name: string;
@@ -69,49 +70,7 @@ export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
69
70
  export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
70
71
  export declare function normalizeMcpName(name: string): string;
71
72
  export declare function clampNameSegment(seg: string, max?: number): string;
72
- export declare const MCP_IMAGE_MAX_BASE64: number;
73
- export declare const IMAGE_TARGET_RAW_SIZE: number;
74
- export declare const IMAGE_MAX_WIDTH = 2000;
75
- export declare const IMAGE_MAX_HEIGHT = 2000;
76
- export type McpImageResizer = (base64: string, mimeType: string) => Promise<{
77
- base64: string;
78
- mimeType: string;
79
- } | undefined>;
80
- export interface ImageDimensions {
81
- originalWidth: number;
82
- originalHeight: number;
83
- displayWidth: number;
84
- displayHeight: number;
85
- }
86
- export interface DownsampledImage {
87
- base64: string;
88
- mimeType: string;
89
- dimensions?: ImageDimensions;
90
- }
91
- export type ImageDownsampler = (input: Buffer, mimeType: string) => Promise<DownsampledImage | undefined>;
92
- type SharpFactory = (input: Buffer) => SharpOps;
93
- interface SharpOps {
94
- metadata: () => Promise<{
95
- width?: number;
96
- height?: number;
97
- format?: string;
98
- }>;
99
- resize: (w: number, h: number, o: {
100
- fit: "inside";
101
- withoutEnlargement: boolean;
102
- }) => SharpOps;
103
- jpeg: (o: {
104
- quality: number;
105
- }) => SharpOps;
106
- png: (o: {
107
- compressionLevel: number;
108
- palette: boolean;
109
- }) => SharpOps;
110
- toBuffer: () => Promise<Buffer>;
111
- }
112
- export declare function makeImageDownsampler(sharp: SharpFactory): ImageDownsampler;
113
- export declare function sharpImageDownsampler(): Promise<ImageDownsampler | undefined>;
114
- export declare function sharpImageResizer(): Promise<McpImageResizer | undefined>;
73
+ export * from "./image-downsample.js";
115
74
  export declare function inferCompactSchema(value: unknown, depth?: number): string;
116
75
  export declare function resolveMcpHttpHeaders(t: {
117
76
  headers?: Record<string, string>;
@@ -147,4 +106,3 @@ export type McpSchemaNormalizeResult = {
147
106
  export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormalizeResult;
148
107
  export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
149
108
  export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer): Promise<MaterializedMcp>;
150
- export {};