@botbotgo/agent-harness 0.0.39 → 0.0.41

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.
package/README.md CHANGED
@@ -111,11 +111,23 @@ import { run } from "@botbotgo/agent-harness";
111
111
  const result = await run(runtime, {
112
112
  agentId: "orchestra",
113
113
  input: "Explain the available agents in this workspace.",
114
+ context: {
115
+ requestId: "req-123",
116
+ },
117
+ state: {
118
+ visitCount: 1,
119
+ },
114
120
  });
115
121
  ```
116
122
 
117
123
  Each run creates or continues a persisted thread. The result includes `threadId`, `runId`, `state`, and `output`.
118
124
 
125
+ When the underlying LangChain or DeepAgents graph expects extra invocation data, `run(...)` can pass it through without inventing a parallel harness abstraction:
126
+
127
+ - `context`: forwarded as LangChain v1 runtime context
128
+ - `state`: merged into the agent invocation input for stateful graphs
129
+ - `files`: merged into the agent invocation input so DeepAgents state-backed skills or memories can be provided at call time
130
+
119
131
  ### Let The Runtime Route
120
132
 
121
133
  ```ts
@@ -58,7 +58,7 @@ spec:
58
58
  # regex:
59
59
  # - "\\b(create|build|implement|fix|review|debug|inspect|analy[sz]e|download|clone)\\b"
60
60
  rules:
61
- - agentId: research-lite
61
+ - agentId: orchestra
62
62
  contains: ["latest", "recent", "today", "current", "news", "最新", "最近", "今天", "当前", "新闻", "头条", "研究", "调研"]
63
63
  - agentId: orchestra
64
64
  regex:
@@ -102,3 +102,17 @@ spec:
102
102
  # sqlite:
103
103
  # sweepBatchSize: 200
104
104
  # vacuum: false
105
+
106
+ # agent-harness feature: runtime-managed recovery policy for interrupted runs.
107
+ # This keeps checkpoint resume as an internal lifecycle concern instead of a primary user-facing API concept.
108
+ #
109
+ # Current support:
110
+ # - startup recovery of runs already in `resuming` state
111
+ # - persisted approval-decision intent for cross-restart resume continuation
112
+ # - bounded retry attempts to avoid infinite restart loops
113
+ #
114
+ # Example:
115
+ # recovery:
116
+ # enabled: true
117
+ # resumeResumingRunsOnStartup: true
118
+ # maxRecoveryAttempts: 3
@@ -86,9 +86,13 @@ export type LangChainAgentParams = {
86
86
  model: CompiledModel;
87
87
  tools: CompiledTool[];
88
88
  systemPrompt?: string;
89
+ stateSchema?: unknown;
89
90
  responseFormat?: unknown;
90
91
  contextSchema?: unknown;
91
92
  middleware?: Array<Record<string, unknown>>;
93
+ includeAgentName?: "inline";
94
+ version?: "v1" | "v2";
95
+ name?: string;
92
96
  description: string;
93
97
  };
94
98
  export type CompiledSubAgent = {
@@ -119,6 +123,8 @@ export type DeepAgentParams = {
119
123
  name: string;
120
124
  memory: string[];
121
125
  skills: string[];
126
+ generalPurposeAgent?: boolean;
127
+ taskDescription?: string;
122
128
  };
123
129
  export type CompiledModel = {
124
130
  id: string;
@@ -262,6 +268,9 @@ export type RunStartOptions = {
262
268
  agentId?: string;
263
269
  input: MessageContent;
264
270
  threadId?: string;
271
+ context?: Record<string, unknown>;
272
+ state?: Record<string, unknown>;
273
+ files?: Record<string, unknown>;
265
274
  listeners?: RunListeners;
266
275
  };
267
276
  export type RunDecisionOptions = {
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.38";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.40";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.38";
1
+ export const AGENT_HARNESS_VERSION = "0.0.40";
@@ -8,6 +8,36 @@ type ThreadMeta = {
8
8
  status: RunState;
9
9
  latestRunId: string;
10
10
  };
11
+ type RunMeta = {
12
+ runId: string;
13
+ threadId: string;
14
+ agentId: string;
15
+ executionMode: string;
16
+ createdAt: string;
17
+ updatedAt: string;
18
+ };
19
+ type Lifecycle = {
20
+ state: RunState;
21
+ previousState: RunState | null;
22
+ stateEnteredAt: string;
23
+ lastTransitionAt: string;
24
+ resumable: boolean;
25
+ checkpointRef: string | null;
26
+ };
27
+ type RunIndexRecord = {
28
+ runId: string;
29
+ threadId: string;
30
+ state: RunState;
31
+ resumable: boolean;
32
+ updatedAt: string;
33
+ };
34
+ type RecoveryIntent = {
35
+ kind: "approval-decision";
36
+ savedAt: string;
37
+ checkpointRef: string | null;
38
+ resumePayload: unknown;
39
+ attempts: number;
40
+ };
11
41
  export declare class FilePersistence {
12
42
  private readonly runRoot;
13
43
  constructor(runRoot: string);
@@ -31,6 +61,7 @@ export declare class FilePersistence {
31
61
  setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
32
62
  appendEvent(event: HarnessEvent): Promise<void>;
33
63
  listSessions(): Promise<ThreadSummary[]>;
64
+ listRunIndexes(): Promise<RunIndexRecord[]>;
34
65
  getSession(threadId: string): Promise<ThreadSummary | null>;
35
66
  getThreadMeta(threadId: string): Promise<ThreadMeta | null>;
36
67
  listThreadRuns(threadId: string): Promise<ThreadRunRecord[]>;
@@ -38,6 +69,8 @@ export declare class FilePersistence {
38
69
  listApprovals(): Promise<ApprovalRecord[]>;
39
70
  getApproval(approvalId: string): Promise<ApprovalRecord | null>;
40
71
  getRunApprovals(threadId: string, runId: string): Promise<ApprovalRecord[]>;
72
+ getRunMeta(threadId: string, runId: string): Promise<RunMeta>;
73
+ getRunLifecycle(threadId: string, runId: string): Promise<Lifecycle>;
41
74
  listDelegations(): Promise<DelegationRecord[]>;
42
75
  createApproval(record: ApprovalRecord): Promise<void>;
43
76
  resolveApproval(threadId: string, runId: string, approvalId: string, status: ApprovalRecord["status"]): Promise<ApprovalRecord>;
@@ -47,5 +80,8 @@ export declare class FilePersistence {
47
80
  listArtifacts(threadId: string, runId: string): Promise<ArtifactListing>;
48
81
  appendThreadMessage(threadId: string, message: TranscriptMessage): Promise<void>;
49
82
  listThreadMessages(threadId: string, limit?: number): Promise<TranscriptMessage[]>;
83
+ saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
84
+ getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
85
+ clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
50
86
  }
51
87
  export {};
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { rm } from "node:fs/promises";
2
3
  import { readdir } from "node:fs/promises";
3
4
  import { ensureDir, fileExists, readJson, writeJson } from "../utils/fs.js";
4
5
  export class FilePersistence {
@@ -168,6 +169,14 @@ export class FilePersistence {
168
169
  }));
169
170
  return records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
170
171
  }
172
+ async listRunIndexes() {
173
+ const runIndexDir = path.join(this.runRoot, "indexes", "runs");
174
+ if (!(await fileExists(runIndexDir))) {
175
+ return [];
176
+ }
177
+ const entries = (await readdir(runIndexDir)).sort();
178
+ return Promise.all(entries.map((entry) => readJson(path.join(runIndexDir, entry))));
179
+ }
171
180
  async getSession(threadId) {
172
181
  const filePath = path.join(this.runRoot, "indexes", "threads", `${threadId}.json`);
173
182
  if (!(await fileExists(filePath))) {
@@ -249,6 +258,12 @@ export class FilePersistence {
249
258
  const entries = (await readdir(approvalsDir)).sort();
250
259
  return Promise.all(entries.map((entry) => readJson(path.join(approvalsDir, entry))));
251
260
  }
261
+ async getRunMeta(threadId, runId) {
262
+ return readJson(path.join(this.runDir(threadId, runId), "meta.json"));
263
+ }
264
+ async getRunLifecycle(threadId, runId) {
265
+ return readJson(path.join(this.runDir(threadId, runId), "lifecycle.json"));
266
+ }
252
267
  async listDelegations() {
253
268
  const delegationsDir = path.join(this.runRoot, "indexes", "delegations");
254
269
  if (!(await fileExists(delegationsDir))) {
@@ -321,4 +336,21 @@ export class FilePersistence {
321
336
  const current = await readJson(messagesPath);
322
337
  return current.items.slice(-limit);
323
338
  }
339
+ async saveRecoveryIntent(threadId, runId, intent) {
340
+ await writeJson(path.join(this.runDir(threadId, runId), "recovery-intent.json"), intent);
341
+ }
342
+ async getRecoveryIntent(threadId, runId) {
343
+ const intentPath = path.join(this.runDir(threadId, runId), "recovery-intent.json");
344
+ if (!(await fileExists(intentPath))) {
345
+ return null;
346
+ }
347
+ return readJson(intentPath);
348
+ }
349
+ async clearRecoveryIntent(threadId, runId) {
350
+ const intentPath = path.join(this.runDir(threadId, runId), "recovery-intent.json");
351
+ if (!(await fileExists(intentPath))) {
352
+ return;
353
+ }
354
+ await rm(intentPath, { force: true });
355
+ }
324
356
  }
@@ -27,6 +27,7 @@ export declare class AgentRuntimeAdapter {
27
27
  private resolveModel;
28
28
  private buildToolNameMapping;
29
29
  private buildAgentMessages;
30
+ private buildInvocationRequest;
30
31
  private buildRawModelMessages;
31
32
  private resolveTools;
32
33
  private normalizeInterruptPolicy;
@@ -40,7 +41,15 @@ export declare class AgentRuntimeAdapter {
40
41
  route(input: MessageContent, primaryBinding: CompiledAgentBinding, secondaryBinding: CompiledAgentBinding, options?: {
41
42
  systemPrompt?: string;
42
43
  }): Promise<string>;
43
- invoke(binding: CompiledAgentBinding, input: MessageContent, threadId: string, runId: string, resumePayload?: unknown, history?: TranscriptMessage[]): Promise<RunResult>;
44
- stream(binding: CompiledAgentBinding, input: MessageContent, threadId: string, history?: TranscriptMessage[]): AsyncGenerator<RuntimeStreamChunk | string>;
44
+ invoke(binding: CompiledAgentBinding, input: MessageContent, threadId: string, runId: string, resumePayload?: unknown, history?: TranscriptMessage[], options?: {
45
+ context?: Record<string, unknown>;
46
+ state?: Record<string, unknown>;
47
+ files?: Record<string, unknown>;
48
+ }): Promise<RunResult>;
49
+ stream(binding: CompiledAgentBinding, input: MessageContent, threadId: string, history?: TranscriptMessage[], options?: {
50
+ context?: Record<string, unknown>;
51
+ state?: Record<string, unknown>;
52
+ files?: Record<string, unknown>;
53
+ }): AsyncGenerator<RuntimeStreamChunk | string>;
45
54
  }
46
55
  export { AgentRuntimeAdapter as RuntimeAdapter, AGENT_INTERRUPT_SENTINEL_PREFIX, AGENT_INTERRUPT_SENTINEL_PREFIX as INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError, };
@@ -377,6 +377,13 @@ export class AgentRuntimeAdapter {
377
377
  { role: "user", content: normalizeMessageContent(input) },
378
378
  ];
379
379
  }
380
+ buildInvocationRequest(history, input, options = {}) {
381
+ return {
382
+ ...(options.state ?? {}),
383
+ ...(options.files ? { files: options.files } : {}),
384
+ messages: this.buildAgentMessages(history, input),
385
+ };
386
+ }
380
387
  buildRawModelMessages(systemPrompt, history, input) {
381
388
  const messages = [];
382
389
  if (systemPrompt) {
@@ -503,10 +510,16 @@ export class AgentRuntimeAdapter {
503
510
  model: model,
504
511
  tools: tools,
505
512
  systemPrompt: binding.langchainAgentParams.systemPrompt,
513
+ stateSchema: binding.langchainAgentParams.stateSchema,
506
514
  responseFormat: binding.langchainAgentParams.responseFormat,
507
515
  contextSchema: binding.langchainAgentParams.contextSchema,
508
516
  middleware: (await this.resolveMiddleware(binding, interruptOn)),
509
517
  checkpointer: this.resolveCheckpointer(binding),
518
+ store: this.options.storeResolver?.(binding),
519
+ includeAgentName: binding.langchainAgentParams.includeAgentName,
520
+ version: binding.langchainAgentParams.version,
521
+ name: binding.langchainAgentParams.name,
522
+ description: binding.langchainAgentParams.description,
510
523
  });
511
524
  }
512
525
  const params = binding.deepAgentParams;
@@ -545,6 +558,8 @@ export class AgentRuntimeAdapter {
545
558
  name: params.name,
546
559
  memory: params.memory,
547
560
  skills: params.skills,
561
+ generalPurposeAgent: params.generalPurposeAgent,
562
+ taskDescription: params.taskDescription,
548
563
  };
549
564
  return createDeepAgent(deepAgentConfig);
550
565
  }
@@ -577,14 +592,14 @@ export class AgentRuntimeAdapter {
577
592
  ? secondaryBinding.agent.id
578
593
  : primaryBinding.agent.id;
579
594
  }
580
- async invoke(binding, input, threadId, runId, resumePayload, history = []) {
595
+ async invoke(binding, input, threadId, runId, resumePayload, history = [], options = {}) {
581
596
  const request = resumePayload === undefined
582
- ? { messages: this.buildAgentMessages(history, input) }
597
+ ? this.buildInvocationRequest(history, input, options)
583
598
  : new Command({ resume: resumePayload });
584
599
  let result;
585
600
  try {
586
601
  const runnable = await this.create(binding);
587
- result = (await this.withTimeout(() => runnable.invoke(request, { configurable: { thread_id: threadId } }), this.resolveBindingTimeout(binding), "agent invoke", "invoke"));
602
+ result = (await this.withTimeout(() => runnable.invoke(request, { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(binding), "agent invoke", "invoke"));
588
603
  }
589
604
  catch (error) {
590
605
  if (resumePayload !== undefined || !isToolCallParseFailure(error)) {
@@ -592,7 +607,7 @@ export class AgentRuntimeAdapter {
592
607
  }
593
608
  const retriedBinding = this.applyStrictToolJsonInstruction(binding);
594
609
  const runnable = await this.create(retriedBinding);
595
- result = (await this.withTimeout(() => runnable.invoke({ messages: this.buildAgentMessages(history, input) }, { configurable: { thread_id: threadId } }), this.resolveBindingTimeout(retriedBinding), "agent invoke", "invoke"));
610
+ result = (await this.withTimeout(() => runnable.invoke(this.buildInvocationRequest(history, input, options), { configurable: { thread_id: threadId }, ...(options.context ? { context: options.context } : {}) }), this.resolveBindingTimeout(retriedBinding), "agent invoke", "invoke"));
596
611
  }
597
612
  const interruptContent = Array.isArray(result.__interrupt__) && result.__interrupt__.length > 0 ? JSON.stringify(result.__interrupt__) : undefined;
598
613
  const extractedOutput = extractVisibleOutput(result);
@@ -613,7 +628,7 @@ export class AgentRuntimeAdapter {
613
628
  output: sanitizeVisibleText(output),
614
629
  };
615
630
  }
616
- async *stream(binding, input, threadId, history = []) {
631
+ async *stream(binding, input, threadId, history = [], options = {}) {
617
632
  try {
618
633
  const invokeTimeoutMs = this.resolveBindingTimeout(binding);
619
634
  const streamIdleTimeoutMs = this.resolveStreamIdleTimeout(binding);
@@ -651,9 +666,9 @@ export class AgentRuntimeAdapter {
651
666
  }
652
667
  }
653
668
  const runnable = await this.create(binding);
654
- const request = { messages: this.buildAgentMessages(history, input) };
669
+ const request = this.buildInvocationRequest(history, input, options);
655
670
  if (typeof runnable.streamEvents === "function") {
656
- const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2" }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
671
+ const events = await this.withTimeout(() => runnable.streamEvents(request, { configurable: { thread_id: threadId }, version: "v2", ...(options.context ? { context: options.context } : {}) }), computeRemainingTimeoutMs(streamDeadlineAt, invokeTimeoutMs), "agent streamEvents start", "stream");
657
672
  const allowVisibleStreamDeltas = Boolean(binding.langchainAgentParams);
658
673
  let emittedOutput = "";
659
674
  let emittedToolError = false;
@@ -1,4 +1,4 @@
1
- import { modelCallLimitMiddleware, modelRetryMiddleware, summarizationMiddleware, todoListMiddleware, toolCallLimitMiddleware, toolRetryMiddleware, } from "langchain";
1
+ import { anthropicPromptCachingMiddleware, contextEditingMiddleware, llmToolSelectorMiddleware, modelCallLimitMiddleware, modelFallbackMiddleware, modelRetryMiddleware, openAIModerationMiddleware, summarizationMiddleware, todoListMiddleware, toolCallLimitMiddleware, toolEmulatorMiddleware, toolRetryMiddleware, } from "langchain";
2
2
  function asMiddlewareConfig(value) {
3
3
  return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : null;
4
4
  }
@@ -13,6 +13,18 @@ function omitKind(config) {
13
13
  const { kind: _kind, ...rest } = config;
14
14
  return rest;
15
15
  }
16
+ async function resolveReferencedModel(value, options) {
17
+ if (value && typeof value === "object" && "id" in value) {
18
+ return options.resolveModel(value);
19
+ }
20
+ return value;
21
+ }
22
+ async function resolveReferencedModelList(values, options) {
23
+ if (!Array.isArray(values)) {
24
+ return undefined;
25
+ }
26
+ return Promise.all(values.map((value) => resolveReferencedModel(value, options)));
27
+ }
16
28
  export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
17
29
  const resolved = [];
18
30
  for (const rawConfig of middlewareConfigs ?? []) {
@@ -24,18 +36,29 @@ export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
24
36
  const runtimeConfig = omitKind(config);
25
37
  switch (kind) {
26
38
  case "summarization": {
27
- if (runtimeConfig.model && typeof runtimeConfig.model === "object" && runtimeConfig.model !== null && "id" in runtimeConfig.model) {
28
- runtimeConfig.model = await options.resolveModel(runtimeConfig.model);
29
- }
39
+ runtimeConfig.model = await resolveReferencedModel(runtimeConfig.model, options);
30
40
  if (runtimeConfig.model === undefined) {
31
41
  throw new Error("summarization middleware requires model or modelRef");
32
42
  }
33
43
  resolved.push(summarizationMiddleware(runtimeConfig));
34
44
  break;
35
45
  }
46
+ case "llmToolSelector":
47
+ runtimeConfig.model = await resolveReferencedModel(runtimeConfig.model, options);
48
+ resolved.push(llmToolSelectorMiddleware(runtimeConfig));
49
+ break;
36
50
  case "modelRetry":
37
51
  resolved.push(modelRetryMiddleware(runtimeConfig));
38
52
  break;
53
+ case "modelFallback": {
54
+ const fallbackModels = (await resolveReferencedModelList(runtimeConfig.fallbackModels, options)) ??
55
+ (await resolveReferencedModelList(runtimeConfig.models, options));
56
+ if (!fallbackModels || fallbackModels.length === 0) {
57
+ throw new Error("modelFallback middleware requires fallbackModels or models");
58
+ }
59
+ resolved.push(modelFallbackMiddleware(...fallbackModels));
60
+ break;
61
+ }
39
62
  case "toolRetry":
40
63
  resolved.push(toolRetryMiddleware(runtimeConfig));
41
64
  break;
@@ -48,6 +71,18 @@ export async function resolveDeclaredMiddleware(middlewareConfigs, options) {
48
71
  case "todoList":
49
72
  resolved.push(todoListMiddleware(runtimeConfig));
50
73
  break;
74
+ case "contextEditing":
75
+ resolved.push(contextEditingMiddleware(runtimeConfig));
76
+ break;
77
+ case "toolEmulator":
78
+ resolved.push(toolEmulatorMiddleware(runtimeConfig));
79
+ break;
80
+ case "openAIModeration":
81
+ resolved.push(openAIModerationMiddleware(runtimeConfig));
82
+ break;
83
+ case "anthropicPromptCaching":
84
+ resolved.push(anthropicPromptCachingMiddleware(runtimeConfig));
85
+ break;
51
86
  default:
52
87
  throw new Error(`Unsupported declarative middleware kind ${kind}`);
53
88
  }
@@ -20,6 +20,7 @@ export declare class AgentHarnessRuntime {
20
20
  private readonly unregisterThreadMemorySync;
21
21
  private readonly resolvedRuntimeAdapterOptions;
22
22
  private readonly checkpointMaintenance;
23
+ private readonly recoveryConfig;
23
24
  private listHostBindings;
24
25
  private defaultRunRoot;
25
26
  private heuristicRoute;
@@ -53,8 +54,11 @@ export declare class AgentHarnessRuntime {
53
54
  private emit;
54
55
  private ensureThreadStarted;
55
56
  private loadPriorHistory;
57
+ private loadRunInput;
56
58
  private appendAssistantMessage;
57
59
  private invokeWithHistory;
60
+ private checkpointRefForState;
61
+ private finalizeContinuedRun;
58
62
  private emitOutputDeltaAndCreateItem;
59
63
  private emitRunCreated;
60
64
  private setRunStateAndEmit;
@@ -73,5 +77,6 @@ export declare class AgentHarnessRuntime {
73
77
  }>;
74
78
  close(): Promise<void>;
75
79
  stop(): Promise<void>;
80
+ private recoverStartupRuns;
76
81
  }
77
82
  export { AgentHarnessRuntime as AgentHarness };
@@ -5,7 +5,7 @@ import { AGENT_INTERRUPT_SENTINEL_PREFIX, AgentRuntimeAdapter, RuntimeOperationT
5
5
  import { createResourceBackendResolver, createResourceToolResolver } from "../resource/resource.js";
6
6
  import { EventBus } from "./event-bus.js";
7
7
  import { PolicyEngine } from "./policy-engine.js";
8
- import { getRoutingDefaultAgentId, getRoutingRules, getRoutingSystemPrompt, isModelRoutingEnabled, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
8
+ import { getRecoveryConfig, getRoutingDefaultAgentId, getRoutingRules, getRoutingSystemPrompt, isModelRoutingEnabled, matchRoutingRule, } from "../workspace/support/workspace-ref-utils.js";
9
9
  import { createHarnessEvent, createPendingApproval, heuristicRoute, inferRoutingBindings, renderRuntimeFailure, renderToolFailure, } from "./support/harness-support.js";
10
10
  import { createCheckpointerForConfig, createStoreForConfig } from "./support/runtime-factories.js";
11
11
  import { resolveCompiledEmbeddingModel, resolveCompiledEmbeddingModelRef } from "./support/embedding-models.js";
@@ -35,6 +35,7 @@ export class AgentHarnessRuntime {
35
35
  unregisterThreadMemorySync;
36
36
  resolvedRuntimeAdapterOptions;
37
37
  checkpointMaintenance;
38
+ recoveryConfig;
38
39
  listHostBindings() {
39
40
  return inferRoutingBindings(this.workspace).hostBindings;
40
41
  }
@@ -157,10 +158,12 @@ export class AgentHarnessRuntime {
157
158
  this.checkpointMaintenance = checkpointMaintenanceConfig
158
159
  ? new CheckpointMaintenanceLoop(discoverCheckpointMaintenanceTargets(workspace), checkpointMaintenanceConfig)
159
160
  : null;
161
+ this.recoveryConfig = getRecoveryConfig(workspace.refs);
160
162
  }
161
163
  async initialize() {
162
164
  await this.persistence.initialize();
163
165
  await this.checkpointMaintenance?.start();
166
+ await this.recoverStartupRuns();
164
167
  }
165
168
  subscribe(listener) {
166
169
  return this.eventBus.subscribe(listener);
@@ -338,6 +341,11 @@ export class AgentHarnessRuntime {
338
341
  const history = await this.persistence.listThreadMessages(threadId);
339
342
  return history.filter((message) => message.runId !== runId);
340
343
  }
344
+ async loadRunInput(threadId, runId) {
345
+ const history = await this.persistence.listThreadMessages(threadId, 100);
346
+ const userTurn = history.find((message) => message.runId === runId && message.role === "user");
347
+ return userTurn?.content ?? "";
348
+ }
341
349
  async appendAssistantMessage(threadId, runId, content) {
342
350
  if (!content) {
343
351
  return;
@@ -349,9 +357,31 @@ export class AgentHarnessRuntime {
349
357
  createdAt: new Date().toISOString(),
350
358
  });
351
359
  }
352
- async invokeWithHistory(binding, input, threadId, runId, resumePayload) {
360
+ async invokeWithHistory(binding, input, threadId, runId, resumePayload, options = {}) {
353
361
  const priorHistory = await this.loadPriorHistory(threadId, runId);
354
- return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory);
362
+ return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory, options);
363
+ }
364
+ checkpointRefForState(threadId, runId, state) {
365
+ return state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
366
+ }
367
+ async finalizeContinuedRun(threadId, runId, input, actual, options) {
368
+ let approval;
369
+ await this.appendAssistantMessage(threadId, runId, actual.output);
370
+ const checkpointRef = this.checkpointRefForState(threadId, runId, actual.state);
371
+ await this.setRunStateAndEmit(threadId, runId, options.stateSequence, actual.state, {
372
+ previousState: options.previousState,
373
+ checkpointRef,
374
+ });
375
+ if (actual.state === "waiting_for_approval" && options.approvalSequence) {
376
+ approval = (await this.requestApprovalAndEmit(threadId, runId, input, actual.interruptContent, checkpointRef, options.approvalSequence)).approval;
377
+ }
378
+ return {
379
+ ...actual,
380
+ threadId,
381
+ runId,
382
+ approvalId: approval?.approvalId ?? actual.approvalId,
383
+ pendingActionId: approval?.pendingActionId ?? actual.pendingActionId,
384
+ };
355
385
  }
356
386
  async emitOutputDeltaAndCreateItem(threadId, runId, agentId, content) {
357
387
  await this.emit(threadId, runId, 3, "output.delta", {
@@ -516,24 +546,19 @@ export class AgentHarnessRuntime {
516
546
  executionMode: binding.agent.executionMode,
517
547
  });
518
548
  try {
519
- const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
520
- let approval;
521
- await this.appendAssistantMessage(threadId, runId, actual.output);
522
- const checkpointRef = actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
523
- await this.setRunStateAndEmit(threadId, runId, 3, actual.state, {
549
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId, undefined, {
550
+ context: options.context,
551
+ state: options.state,
552
+ files: options.files,
553
+ });
554
+ const finalized = await this.finalizeContinuedRun(threadId, runId, options.input, actual, {
524
555
  previousState: null,
525
- checkpointRef,
556
+ stateSequence: 3,
557
+ approvalSequence: 4,
526
558
  });
527
- if (actual.state === "waiting_for_approval") {
528
- approval = (await this.requestApprovalAndEmit(threadId, runId, options.input, actual.interruptContent, checkpointRef, 4)).approval;
529
- }
530
559
  return {
531
- ...actual,
532
- threadId,
533
- runId,
560
+ ...finalized,
534
561
  agentId: selectedAgentId,
535
- approvalId: approval?.approvalId ?? actual.approvalId,
536
- pendingActionId: approval?.pendingActionId ?? actual.pendingActionId,
537
562
  };
538
563
  }
539
564
  catch (error) {
@@ -580,7 +605,11 @@ export class AgentHarnessRuntime {
580
605
  const priorHistory = await this.loadPriorHistory(threadId, runId);
581
606
  let assistantOutput = "";
582
607
  const toolErrors = [];
583
- for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory)) {
608
+ for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory, {
609
+ context: options.context,
610
+ state: options.state,
611
+ files: options.files,
612
+ })) {
584
613
  if (chunk) {
585
614
  const normalizedChunk = typeof chunk === "string"
586
615
  ? chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)
@@ -734,6 +763,15 @@ export class AgentHarnessRuntime {
734
763
  throw new Error(`Unknown agent ${thread.agentId}`);
735
764
  }
736
765
  await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
766
+ await this.persistence.saveRecoveryIntent(threadId, runId, {
767
+ kind: "approval-decision",
768
+ savedAt: new Date().toISOString(),
769
+ checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
770
+ resumePayload: options.decision === "edit" && options.editedInput
771
+ ? { decision: "edit", editedInput: options.editedInput }
772
+ : (options.decision ?? "approve"),
773
+ attempts: 0,
774
+ });
737
775
  await this.emit(threadId, runId, 5, "run.resumed", {
738
776
  resumeKind: "cross-restart",
739
777
  checkpointRef: `checkpoints/${threadId}/${runId}/cp-1`,
@@ -750,24 +788,27 @@ export class AgentHarnessRuntime {
750
788
  });
751
789
  const history = await this.persistence.listThreadMessages(threadId);
752
790
  const priorHistory = history.filter((message) => message.runId !== runId);
791
+ const runInput = await this.loadRunInput(threadId, runId);
753
792
  const resumeDecision = options.decision === "edit" && options.editedInput
754
793
  ? { decision: "edit", editedInput: options.editedInput }
755
794
  : (options.decision ?? "approve");
756
- const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
757
- await this.appendAssistantMessage(threadId, runId, actual.output);
758
- await this.persistence.setRunState(threadId, runId, actual.state, actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null);
759
- await this.emit(threadId, runId, 7, "run.state.changed", {
760
- previousState: "resuming",
761
- state: actual.state,
762
- checkpointRef: actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null,
763
- });
764
- return {
765
- ...actual,
766
- threadId,
767
- runId,
768
- approvalId: approval.approvalId,
769
- pendingActionId: approval.pendingActionId,
770
- };
795
+ try {
796
+ const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
797
+ await this.persistence.clearRecoveryIntent(threadId, runId);
798
+ const finalized = await this.finalizeContinuedRun(threadId, runId, runInput, actual, {
799
+ previousState: "resuming",
800
+ stateSequence: 7,
801
+ approvalSequence: 8,
802
+ });
803
+ return {
804
+ ...finalized,
805
+ approvalId: finalized.approvalId ?? approval.approvalId,
806
+ pendingActionId: finalized.pendingActionId ?? approval.pendingActionId,
807
+ };
808
+ }
809
+ catch (error) {
810
+ throw error;
811
+ }
771
812
  }
772
813
  async restartConversation(options) {
773
814
  const thread = await this.getSession(options.threadId);
@@ -802,5 +843,62 @@ export class AgentHarnessRuntime {
802
843
  async stop() {
803
844
  await this.close();
804
845
  }
846
+ async recoverStartupRuns() {
847
+ if (!this.recoveryConfig.enabled || !this.recoveryConfig.resumeResumingRunsOnStartup) {
848
+ return;
849
+ }
850
+ const threads = await this.persistence.listSessions();
851
+ for (const thread of threads) {
852
+ if (thread.status !== "resuming") {
853
+ continue;
854
+ }
855
+ const binding = this.workspace.bindings.get(thread.agentId);
856
+ if (!binding) {
857
+ continue;
858
+ }
859
+ const recoveryIntent = await this.persistence.getRecoveryIntent(thread.threadId, thread.latestRunId);
860
+ if (!recoveryIntent || recoveryIntent.kind !== "approval-decision") {
861
+ continue;
862
+ }
863
+ if (recoveryIntent.attempts >= this.recoveryConfig.maxRecoveryAttempts) {
864
+ await this.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
865
+ await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
866
+ continue;
867
+ }
868
+ await this.persistence.saveRecoveryIntent(thread.threadId, thread.latestRunId, {
869
+ ...recoveryIntent,
870
+ attempts: recoveryIntent.attempts + 1,
871
+ });
872
+ await this.emit(thread.threadId, thread.latestRunId, 100, "run.resumed", {
873
+ resumeKind: "startup-recovery",
874
+ checkpointRef: recoveryIntent.checkpointRef,
875
+ state: "resuming",
876
+ });
877
+ const history = await this.persistence.listThreadMessages(thread.threadId);
878
+ const priorHistory = history.filter((message) => message.runId !== thread.latestRunId);
879
+ const runInput = await this.loadRunInput(thread.threadId, thread.latestRunId);
880
+ try {
881
+ const actual = await this.runtimeAdapter.invoke(binding, "", thread.threadId, thread.latestRunId, recoveryIntent.resumePayload, priorHistory);
882
+ await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
883
+ await this.finalizeContinuedRun(thread.threadId, thread.latestRunId, runInput, actual, {
884
+ previousState: "resuming",
885
+ stateSequence: 101,
886
+ approvalSequence: 102,
887
+ });
888
+ }
889
+ catch (error) {
890
+ if (recoveryIntent.attempts + 1 >= this.recoveryConfig.maxRecoveryAttempts) {
891
+ await this.persistence.setRunState(thread.threadId, thread.latestRunId, "failed", recoveryIntent.checkpointRef);
892
+ await this.persistence.clearRecoveryIntent(thread.threadId, thread.latestRunId);
893
+ await this.emit(thread.threadId, thread.latestRunId, 101, "run.state.changed", {
894
+ previousState: "resuming",
895
+ state: "failed",
896
+ checkpointRef: recoveryIntent.checkpointRef,
897
+ error: error instanceof Error ? error.message : String(error),
898
+ });
899
+ }
900
+ }
901
+ }
902
+ }
805
903
  }
806
904
  export { AgentHarnessRuntime as AgentHarness };
@@ -43,6 +43,22 @@ function compileMiddlewareConfigs(middleware, models, ownerId) {
43
43
  compiled.model = requireModel(models, compiled.modelRef, ownerId);
44
44
  delete compiled.modelRef;
45
45
  }
46
+ if (compiled.kind === "llmToolSelector" && typeof compiled.modelRef === "string") {
47
+ compiled.model = requireModel(models, compiled.modelRef, ownerId);
48
+ delete compiled.modelRef;
49
+ }
50
+ if (compiled.kind === "modelFallback") {
51
+ const fallbackModelRefs = Array.isArray(compiled.fallbackModelRefs)
52
+ ? compiled.fallbackModelRefs
53
+ : Array.isArray(compiled.modelRefs)
54
+ ? compiled.modelRefs
55
+ : [];
56
+ if (fallbackModelRefs.length > 0) {
57
+ compiled.fallbackModels = fallbackModelRefs.map((modelRef) => requireModel(models, String(modelRef), ownerId));
58
+ delete compiled.fallbackModelRefs;
59
+ delete compiled.modelRefs;
60
+ }
61
+ }
46
62
  return compiled;
47
63
  });
48
64
  }
@@ -162,9 +178,15 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
162
178
  model: compiledAgentModel,
163
179
  tools: requireTools(tools, agent.toolRefs, agent.id),
164
180
  systemPrompt: resolveSystemPrompt(agent),
181
+ stateSchema: agent.langchainAgentConfig?.stateSchema,
165
182
  responseFormat: agent.langchainAgentConfig?.responseFormat,
166
183
  contextSchema: agent.langchainAgentConfig?.contextSchema,
167
184
  middleware: compileMiddlewareConfigs(agent.langchainAgentConfig?.middleware, models, agent.id),
185
+ includeAgentName: agent.langchainAgentConfig?.includeAgentName === "inline" ? "inline" : undefined,
186
+ version: agent.langchainAgentConfig?.version === "v1" || agent.langchainAgentConfig?.version === "v2"
187
+ ? agent.langchainAgentConfig.version
188
+ : undefined,
189
+ name: resolveAgentRuntimeName(agent),
168
190
  description: agent.description,
169
191
  };
170
192
  return {
@@ -196,6 +218,10 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
196
218
  name: resolveAgentRuntimeName(agent),
197
219
  memory: compiledAgentMemory,
198
220
  skills: compiledAgentSkills,
221
+ generalPurposeAgent: typeof agent.deepAgentConfig?.generalPurposeAgent === "boolean" ? agent.deepAgentConfig.generalPurposeAgent : undefined,
222
+ taskDescription: typeof agent.deepAgentConfig?.taskDescription === "string" && agent.deepAgentConfig.taskDescription.trim()
223
+ ? agent.deepAgentConfig.taskDescription
224
+ : undefined,
199
225
  },
200
226
  };
201
227
  }
@@ -5,7 +5,7 @@ import { validateAgent, validateTopology } from "./validate.js";
5
5
  import { compileBinding } from "./agent-binding-compiler.js";
6
6
  import { discoverSubagents, ensureDiscoverySources } from "./support/discovery.js";
7
7
  import { collectAgentDiscoverySourceRefs, collectToolSourceRefs } from "./support/source-collectors.js";
8
- import { resolveRefId } from "./support/workspace-ref-utils.js";
8
+ import { getRoutingDefaultAgentId, getRoutingRules, resolveRefId, } from "./support/workspace-ref-utils.js";
9
9
  import { hydrateAgentMcpTools, hydrateResourceAndExternalTools } from "./tool-hydration.js";
10
10
  function collectParsedResources(refs) {
11
11
  const embeddings = new Map();
@@ -70,6 +70,20 @@ function compileBindings(workspaceRoot, refs, agentsList, models, tools) {
70
70
  }
71
71
  return bindings;
72
72
  }
73
+ function validateRoutingTargets(refs, agentsList) {
74
+ const hostFacingAgentIds = new Set(agentsList
75
+ .filter((agent) => !agentsList.some((owner) => owner.subagentRefs.some((ref) => resolveRefId(ref) === agent.id)))
76
+ .map((agent) => agent.id));
77
+ const defaultAgentId = getRoutingDefaultAgentId(refs);
78
+ if (defaultAgentId && !hostFacingAgentIds.has(defaultAgentId)) {
79
+ throw new Error(`Runtime routing.defaultAgentId references unknown host-facing agent ${defaultAgentId}`);
80
+ }
81
+ for (const rule of getRoutingRules(refs)) {
82
+ if (!hostFacingAgentIds.has(rule.agentId)) {
83
+ throw new Error(`Runtime routing.rules references unknown host-facing agent ${rule.agentId}`);
84
+ }
85
+ }
86
+ }
73
87
  export async function loadWorkspace(workspaceRoot, options = {}) {
74
88
  const loaded = await loadWorkspaceObjects(workspaceRoot, options);
75
89
  loaded.agents = await discoverSubagents(loaded.agents, workspaceRoot);
@@ -84,6 +98,7 @@ export async function loadWorkspace(workspaceRoot, options = {}) {
84
98
  await ensureResourceSources(toolSourceRefs, workspaceRoot);
85
99
  await hydrateResourceAndExternalTools(tools, toolSourceRefs, workspaceRoot);
86
100
  validateWorkspaceResources(embeddings, mcpServers, models, vectorStores, tools, loaded.agents);
101
+ validateRoutingTargets(loaded.refs, loaded.agents);
87
102
  return {
88
103
  workspaceRoot,
89
104
  resourceSources: [...toolSourceRefs],
@@ -215,8 +215,11 @@ function readSharedAgentConfig(item) {
215
215
  ...(typeof item.systemPrompt === "string" ? { systemPrompt: item.systemPrompt } : {}),
216
216
  ...(typeof item.checkpointer === "object" && item.checkpointer ? { checkpointer: item.checkpointer } : {}),
217
217
  ...(typeof item.interruptOn === "object" && item.interruptOn ? { interruptOn: item.interruptOn } : {}),
218
+ ...(item.stateSchema !== undefined ? { stateSchema: item.stateSchema } : {}),
218
219
  ...(item.responseFormat !== undefined ? { responseFormat: item.responseFormat } : {}),
219
220
  ...(item.contextSchema !== undefined ? { contextSchema: item.contextSchema } : {}),
221
+ ...(item.includeAgentName === "inline" ? { includeAgentName: "inline" } : {}),
222
+ ...(item.version === "v1" || item.version === "v2" ? { version: item.version } : {}),
220
223
  ...(middleware ? { middleware } : {}),
221
224
  };
222
225
  }
@@ -228,6 +231,8 @@ function readDeepAgentConfig(item) {
228
231
  ...readSharedAgentConfig(item),
229
232
  ...(typeof item.backend === "object" && item.backend ? { backend: item.backend } : {}),
230
233
  ...(typeof item.store === "object" && item.store ? { store: item.store } : {}),
234
+ ...(typeof item.taskDescription === "string" && item.taskDescription.trim() ? { taskDescription: item.taskDescription } : {}),
235
+ ...(typeof item.generalPurposeAgent === "boolean" ? { generalPurposeAgent: item.generalPurposeAgent } : {}),
231
236
  };
232
237
  }
233
238
  export function parseAgentItem(item, sourcePath) {
@@ -12,8 +12,19 @@ export type RoutingRule = {
12
12
  hasThreadId?: boolean;
13
13
  caseSensitive?: boolean;
14
14
  };
15
+ export type RuntimeRecoveryConfig = {
16
+ enabled: boolean;
17
+ resumeOnStartup: boolean;
18
+ };
19
+ export type RecoveryConfig = {
20
+ enabled: boolean;
21
+ resumeResumingRunsOnStartup: boolean;
22
+ maxRecoveryAttempts: number;
23
+ };
15
24
  export declare function getWorkspaceObject(refs: Map<string, WorkspaceObject | ParsedAgentObject>, ref: string | undefined): WorkspaceObject | undefined;
16
25
  export declare function getRuntimeDefaults(refs: Map<string, WorkspaceObject | ParsedAgentObject>): Record<string, unknown> | undefined;
26
+ export declare function getRuntimeRecoveryConfig(refs: Map<string, WorkspaceObject | ParsedAgentObject>): RuntimeRecoveryConfig;
27
+ export declare function getRecoveryConfig(refs: Map<string, WorkspaceObject | ParsedAgentObject>): RecoveryConfig;
17
28
  export declare function getRoutingSystemPrompt(refs: Map<string, WorkspaceObject | ParsedAgentObject>): string | undefined;
18
29
  export declare function getRoutingDefaultAgentId(refs: Map<string, WorkspaceObject | ParsedAgentObject>): string | undefined;
19
30
  export declare function isModelRoutingEnabled(refs: Map<string, WorkspaceObject | ParsedAgentObject>): boolean;
@@ -25,6 +25,32 @@ export function getRuntimeDefaults(refs) {
25
25
  }
26
26
  return runtimes[0].value;
27
27
  }
28
+ export function getRuntimeRecoveryConfig(refs) {
29
+ const runtimeDefaults = getRuntimeDefaults(refs);
30
+ const recovery = typeof runtimeDefaults?.recovery === "object" && runtimeDefaults.recovery
31
+ ? runtimeDefaults.recovery
32
+ : undefined;
33
+ return {
34
+ enabled: recovery?.enabled !== false,
35
+ resumeOnStartup: recovery?.resumeOnStartup !== false,
36
+ };
37
+ }
38
+ export function getRecoveryConfig(refs) {
39
+ const runtimeDefaults = getRuntimeDefaults(refs);
40
+ const recovery = typeof runtimeDefaults?.recovery === "object" && runtimeDefaults.recovery
41
+ ? runtimeDefaults.recovery
42
+ : {};
43
+ const maxRecoveryAttempts = typeof recovery.maxRecoveryAttempts === "number" &&
44
+ Number.isFinite(recovery.maxRecoveryAttempts) &&
45
+ recovery.maxRecoveryAttempts > 0
46
+ ? Math.floor(recovery.maxRecoveryAttempts)
47
+ : 3;
48
+ return {
49
+ enabled: recovery.enabled !== false,
50
+ resumeResumingRunsOnStartup: recovery.resumeResumingRunsOnStartup !== false,
51
+ maxRecoveryAttempts,
52
+ };
53
+ }
28
54
  export function getRoutingSystemPrompt(refs) {
29
55
  const routing = getRoutingObject(refs);
30
56
  return typeof routing?.systemPrompt === "string" && routing.systemPrompt.trim() ? routing.systemPrompt : undefined;
@@ -1,12 +1,4 @@
1
1
  const allowedExecutionModes = new Set(["deepagent", "langchain-v1"]);
2
- const allowedMiddlewareKinds = new Set([
3
- "summarization",
4
- "modelRetry",
5
- "toolRetry",
6
- "toolCallLimit",
7
- "modelCallLimit",
8
- "todoList",
9
- ]);
10
2
  function hasPromptContent(value) {
11
3
  return typeof value === "string" && value.trim().length > 0;
12
4
  }
@@ -33,13 +25,16 @@ function validateMiddlewareConfig(agent) {
33
25
  throw new Error(`Agent ${agent.id} middleware entries must be objects`);
34
26
  }
35
27
  const typed = config;
36
- const kind = typeof typed.kind === "string" ? typed.kind : "";
37
- if (!allowedMiddlewareKinds.has(kind)) {
38
- throw new Error(`Agent ${agent.id} middleware kind ${kind || "(missing)"} is not supported`);
28
+ const kind = typeof typed.kind === "string" ? typed.kind.trim() : "";
29
+ if (!kind) {
30
+ throw new Error(`Agent ${agent.id} middleware kind is required`);
39
31
  }
40
32
  if (kind === "summarization" && typed.model === undefined && typeof typed.modelRef !== "string") {
41
33
  throw new Error(`Agent ${agent.id} summarization middleware requires model or modelRef`);
42
34
  }
35
+ if (kind === "modelFallback" && !Array.isArray(typed.fallbackModels) && !Array.isArray(typed.models)) {
36
+ throw new Error(`Agent ${agent.id} modelFallback middleware requires fallbackModels or models`);
37
+ }
43
38
  }
44
39
  }
45
40
  export function validateAgent(agent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
@@ -50,7 +50,7 @@
50
50
  "scripts": {
51
51
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",
52
52
  "check": "tsc -p tsconfig.json --noEmit",
53
- "test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/runtime-adapter-regressions.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts",
53
+ "test": "vitest run test/public-api.test.ts test/resource-optional-provider.test.ts test/resource-isolation.test.ts test/stock-research-app-load-harness.test.ts test/stock-research-app-run.test.ts test/release-workflow.test.ts test/release-version.test.ts test/gitignore.test.ts test/package-lock.test.ts test/readme.test.ts test/runtime-adapter-regressions.test.ts test/runtime-recovery.test.ts test/tool-extension-gaps.test.ts test/checkpoint-maintenance.test.ts test/llamaindex-dependency-compat.test.ts test/skill-standard.test.ts test/routing-config.test.ts test/workspace-compat-regressions.test.ts",
54
54
  "test:real-providers": "vitest run test/real-provider-harness.test.ts",
55
55
  "release:prepare": "npm version patch --no-git-tag-version && node ./scripts/sync-example-version.mjs",
56
56
  "release:pack": "npm pack --dry-run",