@botbotgo/agent-harness 0.0.22 → 0.0.24

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
@@ -9,7 +9,9 @@ It helps developers:
9
9
  - define agents in YAML
10
10
  - keep workspace behavior in `config/workspace.yaml`
11
11
  - keep shared bootstrap context in `config/agent-context.md`
12
- - discover tools and skills from `resources/`
12
+ - discover tools and SKILL packages from `resources/`
13
+ - bridge remote MCP servers into agent toolsets
14
+ - expose harness-managed tools as an MCP server
13
15
  - run through either LangChain v1 or DeepAgents with one app-facing API
14
16
 
15
17
  Why it works:
@@ -54,6 +56,8 @@ your-workspace/
54
56
  skills/
55
57
  ```
56
58
 
59
+ Use the standard layout only. Agent entry files must live under `config/agents/`; root-level files such as `agent.yaml`, `orchestra.yaml`, and `direct.yaml` are not loaded.
60
+
57
61
  Minimal usage:
58
62
 
59
63
  ```ts
@@ -78,7 +82,9 @@ try {
78
82
  - One workspace for agents, models, tools, and runtime behavior
79
83
  - One API for both LangChain v1 and DeepAgents
80
84
  - Built-in routing for direct and orchestration flows
81
- - Auto-discovered local tools and skills from `resources/tools/` and `resources/skills/`
85
+ - Auto-discovered local tools and SKILL packages from `resources/tools/` and `resources/skills/`
86
+ - MCP bridge support: agent YAML can mount remote or local MCP servers as agent tools
87
+ - MCP server support: expose harness-managed tools over stdio or an in-memory MCP server
82
88
  - Built-in thread state, approvals, resumable runs, and long-term memory
83
89
  - MCP server helpers plus background checkpoint maintenance
84
90
 
@@ -146,6 +152,51 @@ const result = await run(harness, {
146
152
  });
147
153
  ```
148
154
 
155
+ ### Use Skills And Local Tools
156
+
157
+ `agent-harness` treats `resources/skills/` as SKILL packages and `resources/tools/` as executable local tools.
158
+
159
+ Point agent YAML at both:
160
+
161
+ ```yaml
162
+ spec:
163
+ skills:
164
+ - path: resources/skills/reviewer
165
+ tools:
166
+ - ref: tool/local-toolset
167
+ ```
168
+
169
+ ### Bridge MCP Servers Into Agents
170
+
171
+ Use `mcpServers:` inside agent YAML to bridge MCP servers into the agent's tool list:
172
+
173
+ ```yaml
174
+ spec:
175
+ mcpServers:
176
+ - name: browser
177
+ command: node
178
+ args: ["./mcp-browser-server.mjs"]
179
+ - name: docs
180
+ transport: http
181
+ url: https://example.com/mcp
182
+ token: ${DOCS_MCP_TOKEN}
183
+ ```
184
+
185
+ The harness discovers MCP tools, filters them through the agent config, and exposes them to the runtime like any other tool.
186
+
187
+ ### Expose Harness Tools As An MCP Server
188
+
189
+ Use the MCP server helpers when another client should connect to the harness as an MCP server:
190
+
191
+ ```ts
192
+ import { createToolMcpServer, serveToolsOverStdio } from "@botbotgo/agent-harness";
193
+
194
+ const server = await createToolMcpServer(harness, { agentId: "orchestra" });
195
+ await serveToolsOverStdio(harness, { agentId: "orchestra" });
196
+ ```
197
+
198
+ If you omit `serverInfo`, the harness uses `agent-harness-<agentId>` as the MCP server name and the current package version as the server version.
199
+
149
200
  ### Read Back Thread State
150
201
 
151
202
  ```ts
@@ -221,6 +272,12 @@ Use `config/agents/*.yaml` to configure agents. Common fields include:
221
272
  Use `resources/` for executable extensions:
222
273
 
223
274
  - `resources/tools/` for local tool modules
224
- - `resources/skills/` for skill packages
275
+ - `resources/skills/` for SKILL packages
225
276
 
226
277
  Each resource package should include its own `package.json`.
278
+
279
+ ### Skills And MCP
280
+
281
+ - Use `resources/skills/` for SKILL packages that carry reusable instructions, templates, scripts, and metadata
282
+ - Use `mcpServers:` in `config/agents/*.yaml` when you want the harness to bridge external MCP tools into an agent
283
+ - Use `createToolMcpServer(...)` or `serveToolsOverStdio(...)` when you want the harness itself to act as an MCP server for another client
@@ -0,0 +1,29 @@
1
+ # agent-harness feature: schema version for this declarative config object.
2
+ apiVersion: agent-harness/v1alpha1
3
+ # agent-harness feature: object type for named model presets.
4
+ kind: Models
5
+ spec:
6
+ - name: default
7
+ # ====================
8
+ # LangChain v1 Features
9
+ # ====================
10
+ # LangChain aligned feature: provider family or integration namespace.
11
+ # Common options in this harness today include:
12
+ # - `ollama`
13
+ # - `openai`
14
+ # - `openai-compatible`
15
+ # - `anthropic`
16
+ # - `google` / `google-genai` / `gemini`
17
+ # The runtime adapter uses this to select the concrete LangChain chat model implementation.
18
+ provider: ollama
19
+ # LangChain aligned feature: concrete model identifier passed to the selected provider integration.
20
+ # Example values depend on `provider`, such as `gpt-oss:latest` for `ollama`.
21
+ model: gpt-oss:latest
22
+ init:
23
+ # LangChain aligned feature: provider-specific initialization options.
24
+ # Available keys are provider-specific; common examples include `baseUrl`, `temperature`, and auth/client settings.
25
+ # `baseUrl` configures the Ollama-compatible endpoint used by the model client.
26
+ # For `openai-compatible`, `baseUrl` is normalized into the ChatOpenAI `configuration.baseURL` field.
27
+ baseUrl: https://ollama-rtx-4070.easynet.world/
28
+ # LangChain aligned feature: provider/model initialization option controlling sampling temperature.
29
+ temperature: 0.2
package/dist/mcp.js CHANGED
@@ -2,6 +2,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { z } from "zod";
5
+ import { AGENT_HARNESS_VERSION } from "./package-version.js";
5
6
  import { loadToolModuleDefinition } from "./tool-modules.js";
6
7
  function asResolvedTool(value) {
7
8
  return typeof value === "object" && value !== null ? value : null;
@@ -86,7 +87,7 @@ function jsonSchemaToZod(schema) {
86
87
  export async function createToolMcpServerFromHarness(harness, options) {
87
88
  const server = new McpServer({
88
89
  name: options.serverInfo?.name ?? `agent-harness-${options.agentId}`,
89
- version: options.serverInfo?.version ?? "0.0.16",
90
+ version: options.serverInfo?.version ?? AGENT_HARNESS_VERSION,
90
91
  });
91
92
  const allowedNames = options.includeToolNames ? new Set(options.includeToolNames) : null;
92
93
  for (const { compiledTool, resolvedTool } of harness.resolveAgentTools(options.agentId)) {
@@ -0,0 +1 @@
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.23";
@@ -0,0 +1 @@
1
+ export const AGENT_HARNESS_VERSION = "0.0.23";
@@ -9,6 +9,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
9
9
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
10
10
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
11
11
  import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/websocket.js";
12
+ import { AGENT_HARNESS_VERSION } from "../package-version.js";
12
13
  import { isSupportedToolModulePath, loadToolModuleDefinition } from "../tool-modules.js";
13
14
  import { resolveIsolatedResourceModulePath } from "./isolation.js";
14
15
  import { ensureExternalResourceSource, ensureExternalSource, isExternalSourceLocator, parseExternalSourceLocator } from "./sources.js";
@@ -176,7 +177,7 @@ export async function getOrCreateMcpClient(config) {
176
177
  const loading = (async () => {
177
178
  const client = new Client({
178
179
  name: "agent-harness",
179
- version: "0.0.16",
180
+ version: AGENT_HARNESS_VERSION,
180
181
  });
181
182
  const headers = {
182
183
  ...(config.headers ?? {}),
@@ -44,6 +44,15 @@ export declare class AgentHarness {
44
44
  threadId?: string;
45
45
  }): Promise<string>;
46
46
  private emit;
47
+ private ensureThreadStarted;
48
+ private loadPriorHistory;
49
+ private appendAssistantMessage;
50
+ private invokeWithHistory;
51
+ private emitOutputDeltaAndCreateItem;
52
+ private emitRunCreated;
53
+ private setRunStateAndEmit;
54
+ private requestApprovalAndEmit;
55
+ private emitSyntheticFallback;
47
56
  private persistApproval;
48
57
  private resolveApprovalRecord;
49
58
  private isDecisionRun;
@@ -269,6 +269,95 @@ export class AgentHarness {
269
269
  this.eventBus.publish(event);
270
270
  return event;
271
271
  }
272
+ async ensureThreadStarted(selectedAgentId, binding, input, existingThreadId) {
273
+ const threadId = existingThreadId ?? createPersistentId();
274
+ const runId = createPersistentId();
275
+ const createdAt = new Date().toISOString();
276
+ if (!existingThreadId) {
277
+ await this.persistence.createThread({
278
+ threadId,
279
+ agentId: selectedAgentId,
280
+ runId,
281
+ status: "running",
282
+ createdAt,
283
+ });
284
+ }
285
+ await this.persistence.appendThreadMessage(threadId, {
286
+ role: "user",
287
+ content: input,
288
+ runId,
289
+ createdAt,
290
+ });
291
+ await this.persistence.createRun({
292
+ threadId,
293
+ runId,
294
+ agentId: binding.agent.id,
295
+ executionMode: binding.agent.executionMode,
296
+ createdAt,
297
+ });
298
+ return { threadId, runId, createdAt };
299
+ }
300
+ async loadPriorHistory(threadId, runId) {
301
+ const history = await this.persistence.listThreadMessages(threadId);
302
+ return history.filter((message) => message.runId !== runId);
303
+ }
304
+ async appendAssistantMessage(threadId, runId, content) {
305
+ if (!content) {
306
+ return;
307
+ }
308
+ await this.persistence.appendThreadMessage(threadId, {
309
+ role: "assistant",
310
+ content,
311
+ runId,
312
+ createdAt: new Date().toISOString(),
313
+ });
314
+ }
315
+ async invokeWithHistory(binding, input, threadId, runId, resumePayload) {
316
+ const priorHistory = await this.loadPriorHistory(threadId, runId);
317
+ return this.runtimeAdapter.invoke(binding, input, threadId, runId, resumePayload, priorHistory);
318
+ }
319
+ async emitOutputDeltaAndCreateItem(threadId, runId, agentId, content) {
320
+ await this.emit(threadId, runId, 3, "output.delta", {
321
+ content,
322
+ });
323
+ return {
324
+ type: "content",
325
+ threadId,
326
+ runId,
327
+ agentId,
328
+ content,
329
+ };
330
+ }
331
+ async emitRunCreated(threadId, runId, payload) {
332
+ return this.emit(threadId, runId, 1, "run.created", payload);
333
+ }
334
+ async setRunStateAndEmit(threadId, runId, sequence, state, options) {
335
+ await this.persistence.setRunState(threadId, runId, state, options.checkpointRef ?? null);
336
+ return this.emit(threadId, runId, sequence, "run.state.changed", {
337
+ previousState: options.previousState,
338
+ state,
339
+ checkpointRef: options.checkpointRef ?? null,
340
+ ...(options.error ? { error: options.error } : {}),
341
+ });
342
+ }
343
+ async requestApprovalAndEmit(threadId, runId, input, interruptContent, checkpointRef, sequence) {
344
+ const approval = await this.persistApproval(threadId, runId, checkpointRef, input, interruptContent);
345
+ const event = await this.emit(threadId, runId, sequence, "approval.requested", {
346
+ approvalId: approval.approvalId,
347
+ pendingActionId: approval.pendingActionId,
348
+ toolName: approval.toolName,
349
+ toolCallId: approval.toolCallId,
350
+ allowedDecisions: approval.allowedDecisions,
351
+ checkpointRef,
352
+ });
353
+ return { approval, event };
354
+ }
355
+ async emitSyntheticFallback(threadId, runId, selectedAgentId, error, sequence = 3) {
356
+ await this.emit(threadId, runId, sequence, "runtime.synthetic_fallback", {
357
+ reason: error instanceof Error ? error.message : String(error),
358
+ selectedAgentId,
359
+ });
360
+ }
272
361
  async persistApproval(threadId, runId, checkpointRef, input, interruptContent) {
273
362
  const approval = createPendingApproval(threadId, runId, checkpointRef, input, interruptContent);
274
363
  await this.persistence.createApproval(approval);
@@ -381,68 +470,25 @@ export class AgentHarness {
381
470
  if (!policyDecision.allowed) {
382
471
  throw new Error(`Policy evaluation blocked agent ${selectedAgentId}: ${policyDecision.reasons.join(", ")}`);
383
472
  }
384
- const threadId = options.threadId ?? createPersistentId();
385
- const runId = createPersistentId();
386
- const createdAt = new Date().toISOString();
387
- if (!options.threadId) {
388
- await this.persistence.createThread({
389
- threadId,
390
- agentId: selectedAgentId,
391
- runId,
392
- status: "running",
393
- createdAt,
394
- });
395
- }
396
- await this.persistence.appendThreadMessage(threadId, {
397
- role: "user",
398
- content: options.input,
399
- runId,
400
- createdAt,
401
- });
402
- await this.persistence.createRun({
403
- threadId,
404
- runId,
405
- agentId: binding.agent.id,
406
- executionMode: binding.agent.executionMode,
407
- createdAt,
408
- });
409
- await this.emit(threadId, runId, 1, "run.created", {
473
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
474
+ await this.emitRunCreated(threadId, runId, {
410
475
  agentId: binding.agent.id,
411
476
  requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
412
477
  selectedAgentId,
413
478
  executionMode: binding.agent.executionMode,
414
479
  });
415
480
  try {
416
- const history = await this.persistence.listThreadMessages(threadId);
417
- const priorHistory = history.filter((message) => message.runId !== runId);
418
- const actual = await this.runtimeAdapter.invoke(binding, options.input, threadId, runId, undefined, priorHistory);
481
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
419
482
  let approval;
420
- if (actual.output) {
421
- await this.persistence.appendThreadMessage(threadId, {
422
- role: "assistant",
423
- content: actual.output,
424
- runId,
425
- createdAt: new Date().toISOString(),
426
- });
427
- }
428
- await this.persistence.setRunState(threadId, runId, actual.state, actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null);
429
- if (actual.state === "waiting_for_approval") {
430
- const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
431
- approval = await this.persistApproval(threadId, runId, checkpointRef, options.input, actual.interruptContent);
432
- await this.emit(threadId, runId, 4, "approval.requested", {
433
- approvalId: approval.approvalId,
434
- pendingActionId: approval.pendingActionId,
435
- toolName: approval.toolName,
436
- toolCallId: approval.toolCallId,
437
- allowedDecisions: approval.allowedDecisions,
438
- checkpointRef,
439
- });
440
- }
441
- await this.emit(threadId, runId, 3, "run.state.changed", {
483
+ await this.appendAssistantMessage(threadId, runId, actual.output);
484
+ const checkpointRef = actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null;
485
+ await this.setRunStateAndEmit(threadId, runId, 3, actual.state, {
442
486
  previousState: null,
443
- state: actual.state,
444
- checkpointRef: actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null,
487
+ checkpointRef,
445
488
  });
489
+ if (actual.state === "waiting_for_approval") {
490
+ approval = (await this.requestApprovalAndEmit(threadId, runId, options.input, actual.interruptContent, checkpointRef, 4)).approval;
491
+ }
446
492
  return {
447
493
  ...actual,
448
494
  threadId,
@@ -453,15 +499,9 @@ export class AgentHarness {
453
499
  };
454
500
  }
455
501
  catch (error) {
456
- await this.emit(threadId, runId, 3, "runtime.synthetic_fallback", {
457
- reason: error instanceof Error ? error.message : String(error),
458
- selectedAgentId,
459
- });
460
- await this.persistence.setRunState(threadId, runId, "failed");
461
- await this.emit(threadId, runId, 4, "run.state.changed", {
502
+ await this.emitSyntheticFallback(threadId, runId, selectedAgentId, error);
503
+ await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
462
504
  previousState: null,
463
- state: "failed",
464
- checkpointRef: null,
465
505
  error: error instanceof Error ? error.message : String(error),
466
506
  });
467
507
  return {
@@ -490,32 +530,8 @@ export class AgentHarness {
490
530
  return;
491
531
  }
492
532
  let emitted = false;
493
- const threadId = options.threadId ?? createPersistentId();
494
- const runId = createPersistentId();
495
- const createdAt = new Date().toISOString();
496
- if (!options.threadId) {
497
- await this.persistence.createThread({
498
- threadId,
499
- agentId: selectedAgentId,
500
- runId,
501
- status: "running",
502
- createdAt,
503
- });
504
- }
505
- await this.persistence.appendThreadMessage(threadId, {
506
- role: "user",
507
- content: options.input,
508
- runId,
509
- createdAt,
510
- });
511
- await this.persistence.createRun({
512
- threadId,
513
- runId,
514
- agentId: selectedAgentId,
515
- executionMode: binding.agent.executionMode,
516
- createdAt,
517
- });
518
- yield { type: "event", event: await this.emit(threadId, runId, 1, "run.created", {
533
+ const { threadId, runId } = await this.ensureThreadStarted(selectedAgentId, binding, options.input, options.threadId);
534
+ yield { type: "event", event: await this.emitRunCreated(threadId, runId, {
519
535
  agentId: selectedAgentId,
520
536
  requestedAgentId: options.agentId ?? AUTO_AGENT_ID,
521
537
  selectedAgentId,
@@ -523,8 +539,7 @@ export class AgentHarness {
523
539
  state: "running",
524
540
  }) };
525
541
  try {
526
- const history = await this.persistence.listThreadMessages(threadId);
527
- const priorHistory = history.filter((message) => message.runId !== runId);
542
+ const priorHistory = await this.loadPriorHistory(threadId, runId);
528
543
  let assistantOutput = "";
529
544
  for await (const chunk of this.runtimeAdapter.stream(binding, options.input, threadId, priorHistory)) {
530
545
  if (chunk) {
@@ -535,26 +550,18 @@ export class AgentHarness {
535
550
  : chunk;
536
551
  if (normalizedChunk.kind === "interrupt") {
537
552
  const checkpointRef = `checkpoints/${threadId}/${runId}/cp-1`;
538
- await this.persistence.setRunState(threadId, runId, "waiting_for_approval", checkpointRef);
539
- const approval = await this.persistApproval(threadId, runId, checkpointRef, options.input, normalizedChunk.content);
553
+ const waitingEvent = await this.setRunStateAndEmit(threadId, runId, 4, "waiting_for_approval", {
554
+ previousState: null,
555
+ checkpointRef,
556
+ });
557
+ const approvalRequest = await this.requestApprovalAndEmit(threadId, runId, options.input, normalizedChunk.content, checkpointRef, 5);
540
558
  yield {
541
559
  type: "event",
542
- event: await this.emit(threadId, runId, 4, "run.state.changed", {
543
- previousState: null,
544
- state: "waiting_for_approval",
545
- checkpointRef,
546
- }),
560
+ event: waitingEvent,
547
561
  };
548
562
  yield {
549
563
  type: "event",
550
- event: await this.emit(threadId, runId, 5, "approval.requested", {
551
- approvalId: approval.approvalId,
552
- pendingActionId: approval.pendingActionId,
553
- toolName: approval.toolName,
554
- toolCallId: approval.toolCallId,
555
- allowedDecisions: approval.allowedDecisions,
556
- checkpointRef,
557
- }),
564
+ event: approvalRequest.event,
558
565
  };
559
566
  return;
560
567
  }
@@ -594,98 +601,46 @@ export class AgentHarness {
594
601
  }
595
602
  emitted = true;
596
603
  assistantOutput += normalizedChunk.content;
597
- await this.emit(threadId, runId, 3, "output.delta", {
598
- content: normalizedChunk.content,
599
- });
600
- yield {
601
- type: "content",
602
- threadId,
603
- runId,
604
- agentId: selectedAgentId,
605
- content: normalizedChunk.content,
606
- };
604
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, normalizedChunk.content);
607
605
  }
608
606
  }
609
607
  if (!assistantOutput) {
610
- const actual = await this.runtimeAdapter.invoke(binding, options.input, threadId, runId, undefined, priorHistory);
608
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
611
609
  if (actual.output) {
612
610
  assistantOutput = actual.output;
613
611
  emitted = true;
614
- await this.emit(threadId, runId, 3, "output.delta", {
615
- content: actual.output,
616
- });
617
- yield {
618
- type: "content",
619
- threadId,
620
- runId,
621
- agentId: selectedAgentId,
622
- content: actual.output,
623
- };
612
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
624
613
  }
625
614
  }
626
- if (assistantOutput) {
627
- await this.persistence.appendThreadMessage(threadId, {
628
- role: "assistant",
629
- content: assistantOutput,
630
- runId,
631
- createdAt: new Date().toISOString(),
632
- });
633
- }
634
- await this.persistence.setRunState(threadId, runId, "completed");
635
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
615
+ await this.appendAssistantMessage(threadId, runId, assistantOutput);
616
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "completed", {
636
617
  previousState: null,
637
- state: "completed",
638
- checkpointRef: null,
639
618
  }) };
640
619
  return;
641
620
  }
642
621
  catch (error) {
643
622
  if (emitted) {
644
- await this.persistence.setRunState(threadId, runId, "failed");
645
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
623
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
646
624
  previousState: null,
647
- state: "failed",
648
625
  error: error instanceof Error ? error.message : String(error),
649
626
  }) };
650
627
  return;
651
628
  }
652
629
  try {
653
- const history = await this.persistence.listThreadMessages(threadId);
654
- const priorHistory = history.filter((message) => message.runId !== runId);
655
- const actual = await this.runtimeAdapter.invoke(binding, options.input, threadId, runId, undefined, priorHistory);
630
+ const actual = await this.invokeWithHistory(binding, options.input, threadId, runId);
631
+ await this.appendAssistantMessage(threadId, runId, actual.output);
656
632
  if (actual.output) {
657
- await this.persistence.appendThreadMessage(threadId, {
658
- role: "assistant",
659
- content: actual.output,
660
- runId,
661
- createdAt: new Date().toISOString(),
662
- });
663
- yield {
664
- type: "content",
665
- threadId,
666
- runId,
667
- agentId: selectedAgentId,
668
- content: actual.output,
669
- };
633
+ yield await this.emitOutputDeltaAndCreateItem(threadId, runId, selectedAgentId, actual.output);
670
634
  }
671
- await this.persistence.setRunState(threadId, runId, actual.state);
672
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
635
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, actual.state, {
673
636
  previousState: null,
674
- state: actual.state,
675
- checkpointRef: null,
676
637
  }) };
677
638
  return;
678
639
  }
679
640
  catch (invokeError) {
680
- await this.emit(threadId, runId, 3, "runtime.synthetic_fallback", {
681
- reason: invokeError instanceof Error ? invokeError.message : String(invokeError),
682
- selectedAgentId,
683
- });
684
- await this.persistence.setRunState(threadId, runId, "failed");
685
- yield { type: "event", event: await this.emit(threadId, runId, 4, "run.state.changed", {
641
+ await this.emitSyntheticFallback(threadId, runId, selectedAgentId, invokeError);
642
+ yield { type: "event", event: await this.setRunStateAndEmit(threadId, runId, 4, "failed", {
686
643
  previousState: null,
687
- state: "failed",
688
- checkpointRef: null,
689
644
  error: invokeError instanceof Error ? invokeError.message : String(invokeError),
690
645
  }) };
691
646
  yield {
@@ -737,14 +692,7 @@ export class AgentHarness {
737
692
  ? { decision: "edit", editedInput: options.editedInput }
738
693
  : (options.decision ?? "approve");
739
694
  const actual = await this.runtimeAdapter.invoke(binding, "", threadId, runId, resumeDecision, priorHistory);
740
- if (actual.output) {
741
- await this.persistence.appendThreadMessage(threadId, {
742
- role: "assistant",
743
- content: actual.output,
744
- runId,
745
- createdAt: new Date().toISOString(),
746
- });
747
- }
695
+ await this.appendAssistantMessage(threadId, runId, actual.output);
748
696
  await this.persistence.setRunState(threadId, runId, actual.state, actual.state === "waiting_for_approval" ? `checkpoints/${threadId}/${runId}/cp-1` : null);
749
697
  await this.emit(threadId, runId, 7, "run.state.changed", {
750
698
  previousState: "resuming",