@sema-agent/core 2.13.0 → 4.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 (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -1,4 +1,4 @@
1
- import { type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Result } from "../harness/types.js";
1
+ import { type ExecResult, type ExecutionEnv, ExecutionError, type ExecutionEnvExecOptions, FileError, type FileInfo, type Result } from "../harness/types.js";
2
2
  import type { BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
3
3
  import { BackgroundShellError } from "../../core/background-shell.js";
4
4
  import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "../../core/scheduler.js";
@@ -54,29 +54,7 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
54
54
  list(ctx: SchedulerContext): Promise<Result<ScheduledTaskSummary[], SchedulerError>>;
55
55
  absolutePath(path: string): Promise<Result<string, FileError>>;
56
56
  joinPath(parts: string[]): Promise<Result<string, FileError>>;
57
- exec(command: string, options?: {
58
- cwd?: string;
59
- env?: Record<string, string>;
60
- timeout?: number;
61
- abortSignal?: AbortSignal;
62
- onStdout?: (chunk: string) => void;
63
- onStderr?: (chunk: string) => void;
64
- detachSignal?: AbortSignal;
65
- onSpawn?: (handle: {
66
- pid: number;
67
- kill: () => void;
68
- }) => void;
69
- onDetachAdopted?: () => void;
70
- autoBackgroundOnTimeout?: boolean;
71
- }): Promise<Result<{
72
- stdout: string;
73
- stderr: string;
74
- exitCode: number;
75
- detached?: {
76
- shellId: string;
77
- cause?: "timeout";
78
- };
79
- }, ExecutionError>>;
57
+ exec(command: string, options?: ExecutionEnvExecOptions): Promise<Result<ExecResult, ExecutionError>>;
80
58
  readTextFile(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
81
59
  readTextLines(path: string, options?: {
82
60
  maxLines?: number;
@@ -92,6 +92,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
92
92
  images?: ImageContent[];
93
93
  } & UserMessageProvenance): Promise<AssistantMessage>;
94
94
  private static emptyInjection;
95
+ private enqueueInjection;
95
96
  steer(text: string, options?: {
96
97
  images?: ImageContent[];
97
98
  } & UserMessageProvenance): Promise<void>;
@@ -478,14 +478,7 @@ export class AgentHarness {
478
478
  details: result.details,
479
479
  isError,
480
480
  });
481
- return patch
482
- ? {
483
- content: patch.content,
484
- details: patch.details,
485
- isError: patch.isError,
486
- terminate: patch.terminate,
487
- }
488
- : undefined;
481
+ return patch;
489
482
  },
490
483
  prepareNextTurn: async () => {
491
484
  await this.flushPendingSessionWrites();
@@ -725,42 +718,29 @@ export class AgentHarness {
725
718
  static emptyInjection(text, options) {
726
719
  return text.trim().length === 0 && (options?.images === undefined || options.images.length === 0);
727
720
  }
721
+ async enqueueInjection(queue, text, options) {
722
+ if (AgentHarness.emptyInjection(text, options))
723
+ return;
724
+ const m = createUserMessage(text, options?.images, options);
725
+ if (options?.enginePayload !== undefined)
726
+ engineNotePayloads.set(m, options.enginePayload);
727
+ queue.push(m);
728
+ await this.emitQueueUpdate();
729
+ }
728
730
  async steer(text, options) {
729
731
  if (this.phase === "idle") {
730
732
  throw new AgentHarnessError("invalid_state", "Cannot steer while idle");
731
733
  }
732
- if (AgentHarness.emptyInjection(text, options))
733
- return;
734
- {
735
- const m = createUserMessage(text, options?.images, options);
736
- if (options?.enginePayload !== undefined)
737
- engineNotePayloads.set(m, options.enginePayload);
738
- this.steerQueue.push(m);
739
- }
740
- await this.emitQueueUpdate();
734
+ await this.enqueueInjection(this.steerQueue, text, options);
741
735
  }
742
736
  async followUp(text, options) {
743
737
  if (this.phase === "idle") {
744
738
  throw new AgentHarnessError("invalid_state", "Cannot follow up while idle");
745
739
  }
746
- if (AgentHarness.emptyInjection(text, options))
747
- return;
748
- {
749
- const m = createUserMessage(text, options?.images, options);
750
- if (options?.enginePayload !== undefined)
751
- engineNotePayloads.set(m, options.enginePayload);
752
- this.followUpQueue.push(m);
753
- }
754
- await this.emitQueueUpdate();
740
+ await this.enqueueInjection(this.followUpQueue, text, options);
755
741
  }
756
742
  async nextTurn(text, options) {
757
- if (AgentHarness.emptyInjection(text, options))
758
- return;
759
- const m = createUserMessage(text, options?.images, options);
760
- if (options?.enginePayload !== undefined)
761
- engineNotePayloads.set(m, options.enginePayload);
762
- this.nextTurnQueue.push(m);
763
- await this.emitQueueUpdate();
743
+ await this.enqueueInjection(this.nextTurnQueue, text, options);
764
744
  }
765
745
  async appendMessage(message) {
766
746
  try {
@@ -1,5 +1,6 @@
1
+ import type { PromptEpochDescriptor } from "../../prompt-assembly/epoch.js";
1
2
  import type { DocumentContent, ImageContent, Model, SimpleStreamOptions, TextContent, Transport } from "../llm/index.js";
2
- import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } from "../loop/types.js";
3
+ import type { AfterToolCallResult, AgentEvent, AgentMessage, AgentTool, BeforeToolCallResult, QueueMode, ThinkingLevel } from "../loop/types.js";
3
4
  import type { AgentCoreRuntimeDeps } from "../loop/runtime-deps.js";
4
5
  export type Result<TValue, TError> = {
5
6
  ok: true;
@@ -129,16 +130,17 @@ export interface FileSystem {
129
130
  }): Promise<Result<string, FileError>>;
130
131
  cleanup(): Promise<void>;
131
132
  }
133
+ export interface ExecResult {
134
+ stdout: string;
135
+ stderr: string;
136
+ exitCode: number;
137
+ detached?: {
138
+ shellId: string;
139
+ cause?: "timeout";
140
+ };
141
+ }
132
142
  export interface Shell {
133
- exec(command: string, options?: ExecutionEnvExecOptions): Promise<Result<{
134
- stdout: string;
135
- stderr: string;
136
- exitCode: number;
137
- detached?: {
138
- shellId: string;
139
- cause?: "timeout";
140
- };
141
- }, ExecutionError>>;
143
+ exec(command: string, options?: ExecutionEnvExecOptions): Promise<Result<ExecResult, ExecutionError>>;
142
144
  cleanup(): Promise<void>;
143
145
  }
144
146
  export interface ExecutionEnv extends FileSystem, Shell {
@@ -195,15 +197,8 @@ export interface LeafEntry extends SessionTreeEntryBase {
195
197
  type: "leaf";
196
198
  targetId: string | null;
197
199
  }
198
- export interface PromptEpochEntry extends SessionTreeEntryBase {
200
+ export interface PromptEpochEntry extends SessionTreeEntryBase, PromptEpochDescriptor {
199
201
  type: "prompt_epoch";
200
- epoch: number;
201
- artifactDigest: string;
202
- packId: string;
203
- assemblyApi: number;
204
- activatedBy: "session_start" | "compaction" | "legacy_migration";
205
- centerArtifactDigest?: string;
206
- sourceRevision?: string;
207
202
  }
208
203
  export interface AnnouncedListingEntry extends SessionTreeEntryBase {
209
204
  type: "announced_listing";
@@ -211,28 +206,22 @@ export interface AnnouncedListingEntry extends SessionTreeEntryBase {
211
206
  skills?: string[];
212
207
  models?: string[];
213
208
  }
214
- export interface WorkspaceStateEntry extends SessionTreeEntryBase {
215
- type: "workspace_state";
209
+ export interface ActiveWorktreeSession {
210
+ worktreeDir: string;
211
+ originalCwd: string;
212
+ baseSha: string;
213
+ entered?: boolean;
214
+ }
215
+ export interface WorkspaceState {
216
216
  taskRoot: string;
217
217
  handsCwd?: string;
218
- activeWorktree?: {
219
- worktreeDir: string;
220
- originalCwd: string;
221
- baseSha: string;
222
- entered?: boolean;
223
- };
218
+ activeWorktree?: ActiveWorktreeSession;
219
+ }
220
+ export interface WorkspaceStateEntry extends SessionTreeEntryBase, WorkspaceState {
221
+ type: "workspace_state";
224
222
  }
225
223
  export declare const WORKSPACE_STATE_MAX_PATH_CHARS = 4096;
226
- export declare function normalizeWorkspaceState(value: unknown): {
227
- taskRoot: string;
228
- handsCwd?: string;
229
- activeWorktree?: {
230
- worktreeDir: string;
231
- originalCwd: string;
232
- baseSha: string;
233
- entered?: boolean;
234
- };
235
- } | undefined;
224
+ export declare function normalizeWorkspaceState(value: unknown): WorkspaceState | undefined;
236
225
  export declare const ANNOUNCED_LISTING_MAX_NAMES = 500;
237
226
  export declare const ANNOUNCED_LISTING_MAX_NAME_CHARS = 200;
238
227
  export declare function normalizeCompactionStateCarrier(v: unknown): {
@@ -331,26 +320,8 @@ export interface Session<TMetadata extends SessionMetadata = SessionMetadata> {
331
320
  skills?: string[];
332
321
  models?: string[];
333
322
  } | undefined>;
334
- appendWorkspaceState(state: {
335
- taskRoot: string;
336
- handsCwd?: string;
337
- activeWorktree?: {
338
- worktreeDir: string;
339
- originalCwd: string;
340
- baseSha: string;
341
- entered?: boolean;
342
- };
343
- }): Promise<string>;
344
- getWorkspaceState(): Promise<{
345
- taskRoot: string;
346
- handsCwd?: string;
347
- activeWorktree?: {
348
- worktreeDir: string;
349
- originalCwd: string;
350
- baseSha: string;
351
- entered?: boolean;
352
- };
353
- } | undefined>;
323
+ appendWorkspaceState(state: WorkspaceState): Promise<string>;
324
+ getWorkspaceState(): Promise<WorkspaceState | undefined>;
354
325
  getPromptEpoch?(): Promise<import("../../prompt-assembly/epoch.js").PromptEpochDescriptor | undefined>;
355
326
  }
356
327
  export interface SessionCreateOptions {
@@ -487,17 +458,8 @@ export interface BeforeProviderRequestResult {
487
458
  export interface BeforeProviderPayloadResult {
488
459
  payload: unknown;
489
460
  }
490
- export interface ToolCallResult {
491
- block?: boolean;
492
- reason?: string;
493
- updatedInput?: unknown;
494
- }
495
- export interface ToolResultPatch {
496
- content?: Array<TextContent | ImageContent | DocumentContent>;
497
- details?: unknown;
498
- isError?: boolean;
499
- terminate?: boolean;
500
- }
461
+ export type ToolCallResult = BeforeToolCallResult;
462
+ export type ToolResultPatch = AfterToolCallResult;
501
463
  export interface SessionBeforeCompactResult {
502
464
  cancel?: boolean;
503
465
  compaction?: CompactResult;
@@ -143,7 +143,7 @@ export interface AssistantMessage {
143
143
  usageMissing?: true;
144
144
  stopReason: StopReason;
145
145
  errorMessage?: string;
146
- errorKind?: "length_empty";
146
+ errorKind?: "length_empty" | "degenerate" | "walltime_cutoff";
147
147
  partialFinalized?: true;
148
148
  repetition?: {
149
149
  cut?: RepetitionEvent;
@@ -647,10 +647,16 @@ function partitionToolCalls(toolCalls, tools) {
647
647
  }
648
648
  return batches;
649
649
  }
650
+ function resolveToolConcurrency(configured) {
651
+ if (configured === undefined)
652
+ return 10;
653
+ return Number.isFinite(configured) ? Math.max(1, Math.floor(configured)) : 1;
654
+ }
650
655
  async function runCapped(thunks, limit) {
651
656
  const results = new Array(thunks.length);
652
657
  let next = 0;
653
- const workers = Array.from({ length: Math.max(1, Math.min(limit, thunks.length)) }, async () => {
658
+ const width = Number.isFinite(limit) ? Math.max(1, Math.min(limit, thunks.length)) : 1;
659
+ const workers = Array.from({ length: width }, async () => {
654
660
  while (next < thunks.length) {
655
661
  const i = next++;
656
662
  results[i] = await thunks[i]();
@@ -660,7 +666,7 @@ async function runCapped(thunks, limit) {
660
666
  return results;
661
667
  }
662
668
  async function executeToolCallsPartitioned(currentContext, assistantMessage, toolCalls, config, signal, emit, executor) {
663
- const cap = config.maxToolConcurrency ?? 10;
669
+ const cap = resolveToolConcurrency(config.maxToolConcurrency);
664
670
  const allFinalized = [];
665
671
  const messages = [];
666
672
  let engineCutSeen = false;
@@ -798,7 +804,7 @@ class StreamToolExecutor {
798
804
  this.signal?.aborted ||
799
805
  partialAssistant === null ||
800
806
  !isBatchSafeToolCall(findToolByName(this.context.tools, toolCall.name), toolCall) ||
801
- this.inFlight >= (this.config.maxToolConcurrency ?? 10) ||
807
+ this.inFlight >= resolveToolConcurrency(this.config.maxToolConcurrency) ||
802
808
  this.entries.has(toolCall.id)) {
803
809
  this.barrier = true;
804
810
  return;
@@ -927,7 +933,8 @@ function prepareToolCallArguments(tool, toolCall) {
927
933
  async function prepareToolCall(currentContext, assistantMessage, toolCall, config, signal) {
928
934
  const tool = findToolByName(currentContext.tools, toolCall.name);
929
935
  if (!tool) {
930
- const tombstone = TOMBSTONED_TOOLS[toolCall.name];
936
+ const tombstoneEntry = Object.hasOwn(TOMBSTONED_TOOLS, toolCall.name) ? TOMBSTONED_TOOLS[toolCall.name] : undefined;
937
+ const tombstone = typeof tombstoneEntry === "string" ? tombstoneEntry : undefined;
931
938
  if (tombstone !== undefined) {
932
939
  return {
933
940
  kind: "immediate",
@@ -74,21 +74,23 @@ export interface LoopRecoveryOptions {
74
74
  hasLiveWriteoutWindow?: () => boolean;
75
75
  };
76
76
  }
77
+ export interface ToolCutInfo {
78
+ toolCallId: string;
79
+ toolName: string;
80
+ elapsedMs: number;
81
+ settled: Promise<void>;
82
+ }
83
+ export interface LoopToolDeadline {
84
+ deadlineMs: () => number | undefined;
85
+ onCut?: (info: ToolCutInfo) => void;
86
+ }
77
87
  export interface AgentLoopConfig extends SimpleStreamOptions {
78
88
  model: Model;
79
89
  recovery?: LoopRecoveryOptions;
80
90
  maxTokensPerCall?: () => number | undefined;
81
91
  callDeadlineMsPerCall?: () => number | undefined;
82
92
  stallTimeoutsPerCall?: () => import("../llm/types.js").StallTimeouts | undefined;
83
- toolDeadline?: {
84
- deadlineMs: () => number | undefined;
85
- onCut?: (info: {
86
- toolCallId: string;
87
- toolName: string;
88
- elapsedMs: number;
89
- settled: Promise<void>;
90
- }) => void;
91
- };
93
+ toolDeadline?: LoopToolDeadline;
92
94
  maxToolConcurrency?: number;
93
95
  streamingToolExecution?: boolean;
94
96
  convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
@@ -8,8 +8,7 @@ export function createFrameDecoder(maxBytes = 50 * 1024 * 1024) {
8
8
  return function decode(chunk) {
9
9
  const frames = [];
10
10
  buf = Buffer.concat([buf, chunk]);
11
- if (buf.length > maxBytes + HEADER_SLACK)
12
- buf = Buffer.alloc(0);
11
+ let awaiting;
13
12
  for (;;) {
14
13
  const he = buf.indexOf("\r\n\r\n");
15
14
  if (he === -1)
@@ -25,11 +24,15 @@ export function createFrameDecoder(maxBytes = 50 * 1024 * 1024) {
25
24
  continue;
26
25
  }
27
26
  const start = he + 4;
28
- if (buf.length < start + len)
27
+ if (buf.length < start + len) {
28
+ awaiting = start + len;
29
29
  break;
30
+ }
30
31
  frames.push(buf.subarray(start, start + len).toString("utf8"));
31
32
  buf = buf.subarray(start + len);
32
33
  }
34
+ if (buf.length > (awaiting ?? maxBytes + HEADER_SLACK))
35
+ buf = Buffer.alloc(0);
33
36
  return frames;
34
37
  };
35
38
  }
@@ -29,6 +29,7 @@ export declare class NodeLspManager implements LspServerManager {
29
29
  private readonly cache;
30
30
  private readonly opening;
31
31
  private readonly failed;
32
+ private readonly initFailures;
32
33
  private disposed;
33
34
  constructor(opts?: NodeLspManagerOptions);
34
35
  private sweepIdle;
@@ -37,6 +38,7 @@ export declare class NodeLspManager implements LspServerManager {
37
38
  private evictOverCap;
38
39
  evict(root: string): Promise<void>;
39
40
  clearFailed(root?: string): void;
41
+ unavailableReason(filePath: string, env?: ExecutionEnv): string | undefined;
40
42
  private openSession;
41
43
  private serverCmd;
42
44
  dispose(): Promise<void>;
@@ -1,13 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { killProcessTree } from "../execution-env/kill-tree.js";
3
- import { readFile, stat } from "node:fs/promises";
3
+ import { open, readFile, stat } from "node:fs/promises";
4
4
  import { basename, dirname, extname, resolve as resolvePath } from "node:path";
5
- import { existsSync } from "node:fs";
5
+ import { constants as fsConstants, existsSync } from "node:fs";
6
6
  import { pathToUri } from "../../core/lsp-protocol.js";
7
7
  import { LspDiagnosticsRegistry } from "../../core/lsp-diagnostics.js";
8
8
  import { TransportLspSession } from "../../core/lsp-session.js";
9
9
  import { StdioLspTransport } from "./stdio-lsp-transport.js";
10
- import { MAX_LSP_FILE_BYTES } from "../../core/lsp.js";
10
+ import { MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "../../core/lsp.js";
11
11
  export const DEFAULT_LSP_SERVERS = {
12
12
  typescript: "typescript-language-server --stdio",
13
13
  javascript: "typescript-language-server --stdio",
@@ -46,6 +46,7 @@ const CLIENT_CAPABILITIES = {
46
46
  },
47
47
  workspace: { symbol: {}, workspaceFolders: false },
48
48
  };
49
+ const INIT_FAILURE_LATCH = 2;
49
50
  export class NodeLspManager {
50
51
  servers;
51
52
  spawnFn;
@@ -60,6 +61,7 @@ export class NodeLspManager {
60
61
  cache = new Map();
61
62
  opening = new Map();
62
63
  failed = new Map();
64
+ initFailures = new Map();
63
65
  disposed = false;
64
66
  constructor(opts = {}) {
65
67
  this.servers = { ...DEFAULT_LSP_SERVERS, ...opts.servers };
@@ -79,6 +81,10 @@ export class NodeLspManager {
79
81
  sweepIdle() {
80
82
  const cutoff = Date.now() - this.idleTtlMs;
81
83
  for (const [key, e] of [...this.cache]) {
84
+ if (e.session.inFlight > 0) {
85
+ e.lastUsed = Date.now();
86
+ continue;
87
+ }
82
88
  if (e.lastUsed < cutoff) {
83
89
  this.cache.delete(key);
84
90
  this.log("lsp_evict_idle", { key });
@@ -106,19 +112,20 @@ export class NodeLspManager {
106
112
  this.cache.delete(key);
107
113
  const carry = cached.session.openedFiles();
108
114
  this.log("lsp_heal", { language, root, carried: carry.length });
109
- const healed = await this.open(key, language, root, signal, env);
110
- if (healed && carry.length)
111
- await healed.warmOpen(carry, signal).catch(() => undefined);
112
- return healed;
115
+ return this.open(key, language, root, signal, env, carry);
113
116
  }
114
117
  return this.open(key, language, root, signal, env);
115
118
  }
116
- open(key, language, root, signal, env) {
119
+ open(key, language, root, signal, env, carry = []) {
117
120
  const inflight = this.opening.get(key);
118
- if (inflight)
119
- return inflight.job;
120
- const entry = { root, cancelled: false, job: undefined };
121
- entry.job = this.openSession(language, root, signal, env)
121
+ if (inflight && !inflight.scope.signal.aborted) {
122
+ inflight.scope.join(signal);
123
+ return settleOnAbort(inflight.job, signal, () => undefined);
124
+ }
125
+ const scope = new SharedAbortScope();
126
+ scope.join(signal);
127
+ const entry = { root, cancelled: false, scope, job: undefined };
128
+ entry.job = this.openSession(language, root, scope.signal, env)
122
129
  .then(async (session) => {
123
130
  if (!session)
124
131
  return undefined;
@@ -126,26 +133,41 @@ export class NodeLspManager {
126
133
  await session.close().catch(() => undefined);
127
134
  return undefined;
128
135
  }
136
+ if (carry.length) {
137
+ const carried = await session.warmOpen(carry, scope.signal).catch(() => ({ opened: 0, failed: carry.length }));
138
+ this.log("lsp_heal_carried", { language, root, ...carried });
139
+ if (this.disposed || entry.cancelled) {
140
+ await session.close().catch(() => undefined);
141
+ return undefined;
142
+ }
143
+ }
129
144
  this.evictOverCap();
130
145
  this.cache.set(key, { session, root, lastUsed: Date.now() });
131
146
  return session;
132
147
  })
133
- .finally(() => this.opening.delete(key));
148
+ .finally(() => {
149
+ if (this.opening.get(key) === entry)
150
+ this.opening.delete(key);
151
+ });
134
152
  this.opening.set(key, entry);
135
- return entry.job;
153
+ return settleOnAbort(entry.job, signal, () => undefined);
136
154
  }
137
155
  evictOverCap() {
138
156
  while (this.cache.size >= this.maxSessions) {
139
157
  let lruKey;
140
158
  let lruTime = Infinity;
141
159
  for (const [k, e] of this.cache) {
160
+ if (e.session.inFlight > 0)
161
+ continue;
142
162
  if (e.lastUsed < lruTime) {
143
163
  lruTime = e.lastUsed;
144
164
  lruKey = k;
145
165
  }
146
166
  }
147
- if (lruKey === undefined)
167
+ if (lruKey === undefined) {
168
+ this.log("lsp_evict_deferred", { size: this.cache.size, maxSessions: this.maxSessions });
148
169
  break;
170
+ }
149
171
  const evicted = this.cache.get(lruKey);
150
172
  this.cache.delete(lruKey);
151
173
  this.log("lsp_evict_lru", { key: lruKey });
@@ -154,6 +176,7 @@ export class NodeLspManager {
154
176
  }
155
177
  async evict(root) {
156
178
  const closing = [];
179
+ this.clearFailed(root);
157
180
  for (const [key, e] of [...this.cache]) {
158
181
  if (e.root === root) {
159
182
  this.cache.delete(key);
@@ -174,8 +197,27 @@ export class NodeLspManager {
174
197
  if (root !== undefined && e.root !== root)
175
198
  continue;
176
199
  this.failed.delete(key);
177
- this.log("lsp_negative_cache_cleared", { key, attempts: e.attempts });
200
+ this.initFailures.delete(key);
201
+ this.log("lsp_negative_cache_cleared", { key, attempts: e.attempts, kind: e.kind });
178
202
  }
203
+ for (const [key, _n] of [...this.initFailures]) {
204
+ if (root === undefined || key.endsWith(` ${root}`))
205
+ this.initFailures.delete(key);
206
+ }
207
+ }
208
+ unavailableReason(filePath, env) {
209
+ const language = languageFor(filePath);
210
+ if (!language)
211
+ return undefined;
212
+ const cmd = this.serverCmd(language);
213
+ if (!cmd)
214
+ return `no ${language} language server is configured in this environment`;
215
+ const e = this.failed.get(`${language} ${this.resolveRootFn(filePath, env)}`);
216
+ if (e?.kind === "missing_binary")
217
+ return `the ${language} language server ("${e.command}") is not installed on this host`;
218
+ if (e?.kind === "initialize")
219
+ return `the ${language} language server ("${e.command}") is installed but failed to initialize`;
220
+ return undefined;
179
221
  }
180
222
  async openSession(language, root, signal, _env) {
181
223
  const cmd = this.serverCmd(language);
@@ -183,10 +225,18 @@ export class NodeLspManager {
183
225
  return undefined;
184
226
  const tokens = (cmd.match(/"[^"]*"|\S+/g) ?? []).map((t) => (t.startsWith('"') && t.endsWith('"') && t.length >= 2 ? t.slice(1, -1) : t));
185
227
  const [command, ...args] = tokens.length > 0 ? tokens : [""];
186
- const child = await this.spawnFn(command, args, root, signal).catch(() => undefined);
228
+ const key = `${language} ${root}`;
229
+ let child;
230
+ try {
231
+ child = await this.spawnFn(command, args, root, signal);
232
+ }
233
+ catch (e) {
234
+ this.log("lsp_spawn_error", { language, root, command, err: e instanceof Error ? e.message : String(e) });
235
+ return undefined;
236
+ }
187
237
  if (!child) {
188
238
  if (signal?.aborted !== true)
189
- this.failed.set(`${language} ${root}`, { root, attempts: 1 });
239
+ this.failed.set(key, { root, attempts: 1, kind: "missing_binary", command, language });
190
240
  this.log("lsp_spawn_degraded", { language, root, command });
191
241
  return undefined;
192
242
  }
@@ -204,9 +254,18 @@ export class NodeLspManager {
204
254
  }
205
255
  catch (e) {
206
256
  await transport.close();
207
- this.log("lsp_init_degraded", { language, root, err: e instanceof Error ? e.message : String(e) });
257
+ const attempt = (this.initFailures.get(key) ?? 0) + 1;
258
+ const latched = attempt >= INIT_FAILURE_LATCH && signal?.aborted !== true;
259
+ if (latched) {
260
+ this.failed.set(key, { root, attempts: 1, kind: "initialize", command, language });
261
+ this.initFailures.delete(key);
262
+ }
263
+ else
264
+ this.initFailures.set(key, attempt);
265
+ this.log("lsp_init_degraded", { language, root, err: e instanceof Error ? e.message : String(e), attempt, latched });
208
266
  return undefined;
209
267
  }
268
+ this.initFailures.delete(key);
210
269
  this.log("lsp_open", { language, root });
211
270
  return new TransportLspSession(transport, language, this.readTextFn, this.log, this.warmup, this.diagnostics);
212
271
  }
@@ -249,33 +308,51 @@ export const defaultLspReadText = async (fp, signal) => {
249
308
  throw new Error(`not a regular file: ${fp}`);
250
309
  if (st.size > MAX_LSP_FILE_BYTES)
251
310
  throw new Error(`file too large for LSP analysis (${st.size} bytes exceeds ${MAX_LSP_FILE_BYTES})`);
252
- return readFile(fp, { encoding: "utf8", signal });
311
+ const flags = fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0);
312
+ const fh = await open(fp, flags);
313
+ try {
314
+ const fst = await fh.stat();
315
+ if (!fst.isFile())
316
+ throw new Error(`not a regular file: ${fp}`);
317
+ if (fst.size > MAX_LSP_FILE_BYTES)
318
+ throw new Error(`file too large for LSP analysis (${fst.size} bytes exceeds ${MAX_LSP_FILE_BYTES})`);
319
+ return await readFile(fh, { encoding: "utf8", signal });
320
+ }
321
+ finally {
322
+ await fh.close().catch(() => undefined);
323
+ }
253
324
  };
254
325
  export function lspSpawnOptions(cwd) {
255
326
  return process.platform === "win32"
256
327
  ? { cwd, stdio: ["pipe", "pipe", "ignore"], shell: true }
257
328
  : { cwd, stdio: ["pipe", "pipe", "ignore"] };
258
329
  }
259
- export const defaultLspSpawn = (command, args, cwd, signal) => new Promise((resolve) => {
330
+ export const defaultLspSpawn = (command, args, cwd, signal) => new Promise((resolve, reject) => {
260
331
  let child;
261
332
  try {
262
333
  child = spawn(command, args, lspSpawnOptions(cwd));
263
334
  }
264
- catch {
265
- return resolve(undefined);
335
+ catch (e) {
336
+ return reject(e instanceof Error ? e : new Error(String(e)));
266
337
  }
338
+ child.on("error", () => { });
267
339
  const onSpawn = () => {
268
340
  cleanup();
269
341
  resolve(child);
270
342
  };
271
- const onError = () => {
343
+ const onError = (err) => {
272
344
  cleanup();
273
- resolve(undefined);
345
+ if (err?.code === "ENOENT")
346
+ resolve(undefined);
347
+ else
348
+ reject(err instanceof Error ? err : new Error(String(err)));
274
349
  };
275
350
  const onAbort = () => {
276
351
  cleanup();
277
352
  try {
278
- if (process.platform === "win32" && child.pid)
353
+ if (child.pid === undefined)
354
+ return resolve(undefined);
355
+ if (process.platform === "win32")
279
356
  killProcessTree(child.pid, { force: true });
280
357
  else
281
358
  child.kill();