@bluecopa/harness 0.0.1 → 0.1.0-snapshot.10

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
@@ -194,11 +194,45 @@ interface SandboxProvider {
194
194
 
195
195
  `HarnessTelemetry` provides OpenTelemetry-style spans and metrics for agent runs.
196
196
 
197
+ ### Arc: Orchestrator + Thread Architecture (`src/arc/`)
198
+
199
+ `ArcLoop` is an `AgentLoop` implementation where an orchestrator LLM dispatches bounded threads via a single `Thread` tool. Threads produce episodes (summary + full trace). The orchestrator only sees summaries, keeping its context small.
200
+
201
+ ```ts
202
+ import { createArcAgent } from './src/arc/create-arc-agent';
203
+ import { InMemoryEpisodeStore } from './src/arc/stores/episode-store';
204
+ import { InMemorySessionMemoStore } from './src/arc/stores/session-memo-store';
205
+ import { InMemoryLongTermStore } from './src/arc/stores/long-term-store';
206
+
207
+ const agent = createArcAgent({
208
+ toolProvider: new LocalToolProvider(process.cwd()),
209
+ episodeStore: new InMemoryEpisodeStore(),
210
+ sessionMemoStore: new InMemorySessionMemoStore(),
211
+ longTermStore: new InMemoryLongTermStore(),
212
+ taskId: 'task-1',
213
+ sessionId: 'session-1',
214
+ });
215
+
216
+ const result = await agent.run('Fix the authentication bug');
217
+ ```
218
+
219
+ Key features:
220
+ - **Parallel threads**: orchestrator calls Thread N times in one turn → all run concurrently
221
+ - **Four-tier memory**: thread context → episodes → session memos → long-term
222
+ - **Per-thread models**: Haiku for reads, Sonnet for implementation
223
+ - **Template compression**: zero-LLM-call episode summaries
224
+ - **Async consolidation**: non-blocking background distillation
225
+
226
+ Full architecture doc: [`docs/arc.md`](../docs/arc.md)
227
+
197
228
  ## Package layout
198
229
 
199
230
  ```
200
231
  src/
201
232
  ├── agent/ # createAgent, step executor, types
233
+ ├── arc/ # ArcLoop orchestrator, threads, memory hierarchy
234
+ │ ├── stores/ # RxDB + in-memory store implementations
235
+ │ └── object-store/ # Pluggable cloud sync (fs, memory)
202
236
  ├── interfaces/ # ToolProvider, SandboxProvider, AgentLoop contracts
203
237
  ├── loop/ # VercelAgentLoop, LCMToolLoop
204
238
  ├── providers/ # LocalToolProvider, E2BToolProvider, ControlPlaneE2BExecutor
@@ -214,6 +248,7 @@ src/
214
248
 
215
249
  ## Documentation
216
250
 
251
+ - **Arc architecture**: [`docs/arc.md`](../docs/arc.md)
217
252
  - Provider guide: `docs/guides/providers.md`
218
253
  - Skills guide: `docs/guides/skills.md`
219
254
  - Observability guide: `docs/guides/observability.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bluecopa/harness",
3
- "version": "0.0.1",
3
+ "version": "0.1.0-snapshot.10",
4
4
  "description": "Provider-agnostic TypeScript agent framework",
5
5
  "license": "UNLICENSED",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "dependencies": {
11
11
  "@ai-sdk/anthropic": "^3.0.48",
12
12
  "ai": "^6.0.101",
13
+ "rxdb": "^15.39.0",
13
14
  "zod": "^4.1.11"
14
15
  },
15
16
  "devDependencies": {
@@ -37,6 +37,8 @@ export interface AgentRuntime {
37
37
  /** Custom tool executor. Called for every tool action. Return null to fall through to built-in dispatch.
38
38
  * When hookRunner/permissionManager are provided on the runtime, they are automatically applied before/after this callback — no manual wiring needed. */
39
39
  executeToolAction?: (action: ToolCallAction) => Promise<ToolResult | null>;
40
+ /** Progress callback fired before/after each tool call during run(). */
41
+ onToolProgress?: (event: { type: 'tool_start'; name: string; args: Record<string, unknown> } | { type: 'tool_end'; name: string; success: boolean; durationMs: number }) => void;
40
42
  }
41
43
 
42
44
  /**
@@ -596,10 +598,14 @@ export function createAgent(runtime: AgentRuntime) {
596
598
 
597
599
  // Execute valid calls via batch (sequential sandbox ops) or parallel fallback
598
600
  if (validCalls.length > 0) {
601
+ for (const c of validCalls) runtime.onToolProgress?.({ type: 'tool_start', name: c.name, args: c.args });
602
+ const batchStart = Date.now();
599
603
  const results = await executeBatch(validCalls, runtime.toolProvider, runtime);
604
+ const batchMs = Date.now() - batchStart;
600
605
  for (let i = 0; i < validCalls.length; i++) {
601
606
  const call = validCalls[i]!;
602
607
  const r = results[i]!;
608
+ runtime.onToolProgress?.({ type: 'tool_end', name: call.name, success: r.success, durationMs: batchMs });
603
609
  if (!r.success) {
604
610
  recordAgentError(runtime.telemetry);
605
611
  }
@@ -659,6 +665,8 @@ export function createAgent(runtime: AgentRuntime) {
659
665
  } else {
660
666
  consecutiveInvalid = 0;
661
667
  }
668
+ runtime.onToolProgress?.({ type: 'tool_start', name: action.name, args: action.args });
669
+ const singleStart = Date.now();
662
670
  const result = validationError
663
671
  ? ({ success: false, output: '', error: validationError } as ToolResult)
664
672
  : await executor.run(async () => {
@@ -672,6 +680,7 @@ export function createAgent(runtime: AgentRuntime) {
672
680
  };
673
681
  }
674
682
  });
683
+ runtime.onToolProgress?.({ type: 'tool_end', name: action.name, success: result.success, durationMs: Date.now() - singleStart });
675
684
  if (!result.success) {
676
685
  recordAgentError(runtime.telemetry);
677
686
  }
@@ -11,13 +11,26 @@ export interface ToolResultInfo {
11
11
  isError?: boolean;
12
12
  }
13
13
 
14
+ export type ContentPart =
15
+ | { type: 'text'; text: string }
16
+ | { type: 'image'; image: Buffer | Uint8Array; mimeType: string };
17
+
14
18
  export interface AgentMessage {
15
19
  role: 'system' | 'user' | 'assistant' | 'tool';
16
- content: string;
20
+ content: string | ContentPart[];
17
21
  toolCalls?: ToolCallInfo[]; // assistant messages: what tools were called
18
22
  toolResults?: ToolResultInfo[]; // tool messages: results keyed by toolCallId
19
23
  }
20
24
 
25
+ /** Extract plain text from content (string or ContentPart[]). */
26
+ export function getTextContent(content: string | ContentPart[]): string {
27
+ if (typeof content === 'string') return content;
28
+ return content
29
+ .filter((p): p is Extract<ContentPart, { type: 'text' }> => p.type === 'text')
30
+ .map((p) => p.text)
31
+ .join('\n');
32
+ }
33
+
21
34
  export interface ToolCallAction {
22
35
  type: 'tool';
23
36
  name: string;
@@ -46,7 +59,7 @@ export interface AgentRunResult {
46
59
  export type AgentStreamEvent =
47
60
  | { type: 'text_delta'; text: string }
48
61
  | { type: 'tool_start'; name: string; args: Record<string, unknown>; toolCallId?: string }
49
- | { type: 'tool_end'; name: string; result: { success: boolean; output: string; error?: string } }
62
+ | { type: 'tool_end'; name: string; result: { success: boolean; output: string; error?: string; [key: string]: unknown } }
50
63
  | { type: 'step_start'; step: number }
51
64
  | { type: 'step_end'; step: number }
52
65
  | { type: 'done'; output: string; steps: number };
@@ -0,0 +1,395 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { AgentAction, AgentLoop, AgentMessage, AgentStreamEvent, ToolCallAction } from '../agent/types';
3
+ import { getTextContent } from '../agent/types';
4
+ import { VercelAgentLoop } from '../loop/vercel-agent-loop';
5
+ import type { ArcLoopConfig, ThreadRequest, ThreadResult, ModelTier } from './arc-types';
6
+ import { DEFAULT_MODEL_MAP, resolveModel } from './arc-types';
7
+ import { threadTool } from './thread-tool';
8
+ import { ThreadExecutor } from './thread-executor';
9
+ import type { Tool } from 'ai';
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ type AnyTool = Tool<any, any>;
13
+
14
+ // ── Orchestrator system prompt ──
15
+
16
+ const DEFAULT_ORCHESTRATOR_PROMPT = [
17
+ 'You are an orchestrator agent. You accomplish tasks by dispatching focused threads.',
18
+ 'Your ONLY tool is Thread — use it to delegate tactical work.',
19
+ '',
20
+ 'Strategy:',
21
+ '- Break complex tasks into focused, bounded threads.',
22
+ '- Dispatch independent threads in the SAME turn for parallel execution.',
23
+ '- Use contextEpisodeIds to pass context from completed threads to dependent ones.',
24
+ '- Read before writing: dispatch read threads first, then use their episode IDs for implementation threads.',
25
+ '- Each thread gets full tool access (Bash, Read, Write, Edit, Glob, Grep, etc.).',
26
+ '',
27
+ 'Model selection — choose the right tier for each thread:',
28
+ '- "fast" — file reads, searches, directory listing, simple checks',
29
+ '- "medium" (default) — implementation, writing code, running tests, standard tasks',
30
+ '- "strong" — complex refactoring, debugging subtle issues, architectural decisions, multi-file changes',
31
+ '',
32
+ 'Thread results appear as episode summaries. If you need detail, dispatch a new thread seeded with the episode.',
33
+ 'When the task is fully complete, respond with a text summary (no Thread call).',
34
+ ].join('\n');
35
+
36
+ // ── ArcLoop ──
37
+
38
+ export class ArcLoop implements AgentLoop {
39
+ private readonly orchestratorLoop: VercelAgentLoop;
40
+ private readonly threadExecutor: ThreadExecutor;
41
+ private readonly config: ArcLoopConfig;
42
+
43
+ constructor(config: ArcLoopConfig) {
44
+ this.config = config;
45
+
46
+ // Build orchestrator tools: Thread + any extra tools
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ const orchestratorTools: Record<string, AnyTool> = {
49
+ Thread: threadTool as any,
50
+ ...config.extraOrchestratorTools,
51
+ };
52
+
53
+ const modelMap = { ...DEFAULT_MODEL_MAP, ...config.modelMap };
54
+ const orchestratorModel = resolveModel(config.model, modelMap, modelMap.strong);
55
+
56
+ this.orchestratorLoop = new VercelAgentLoop({
57
+ model: orchestratorModel,
58
+ systemPrompt: config.systemPrompt ?? DEFAULT_ORCHESTRATOR_PROMPT,
59
+ ...(config.apiKey != null ? { apiKey: config.apiKey } : {}),
60
+ tools: orchestratorTools,
61
+ });
62
+
63
+ const defaultThreadModel = resolveModel(config.threadModel, modelMap, modelMap.medium);
64
+
65
+ this.threadExecutor = new ThreadExecutor({
66
+ maxConcurrency: config.maxConcurrency ?? 3,
67
+ threadTimeout: config.threadTimeout ?? 120_000,
68
+ threadMaxSteps: config.threadMaxSteps ?? 20,
69
+ threadModel: defaultThreadModel,
70
+ modelMap,
71
+ threadTools: config.threadTools ?? (undefined as unknown as Record<string, AnyTool>),
72
+ toolProvider: config.toolProvider,
73
+ ...(config.skillToolProvider != null ? { skillToolProvider: config.skillToolProvider } : {}),
74
+ ...(config.executor != null ? { executor: config.executor } : {}),
75
+ ...(config.localOutputDir != null ? { localOutputDir: config.localOutputDir } : {}),
76
+ episodeStore: config.episodeStore,
77
+ taskId: config.taskId,
78
+ sessionId: config.sessionId,
79
+ compressor: config.compressor ?? 'template',
80
+ // Pass through runtime extras for thread agents
81
+ ...(config.sandboxProvider != null ? { sandboxProvider: config.sandboxProvider } : {}),
82
+ ...(config.skillManager != null ? { skillManager: config.skillManager } : {}),
83
+ ...(config.skillIndexPath != null ? { skillIndexPath: config.skillIndexPath } : {}),
84
+ ...(config.askUser != null ? { askUser: config.askUser } : {}),
85
+ ...(config.tellUser != null ? { tellUser: config.tellUser } : {}),
86
+ ...(config.downloadRawFile != null ? { downloadRawFile: config.downloadRawFile } : {}),
87
+ ...(config.telemetry != null ? { telemetry: config.telemetry } : {}),
88
+ ...(config.hookRunner != null ? { hookRunner: config.hookRunner } : {}),
89
+ ...(config.permissionManager != null ? { permissionManager: config.permissionManager } : {}),
90
+ ...(config.executeToolAction != null ? { executeToolAction: config.executeToolAction } : {}),
91
+ ...(config.onThreadToolProgress != null ? { onThreadToolProgress: config.onThreadToolProgress } : {}),
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Run the full orchestration loop internally.
97
+ * The outer createAgent() sees this as a single step that returns FinalAction.
98
+ */
99
+ async nextAction(messages: AgentMessage[]): Promise<AgentAction> {
100
+ const maxTurns = this.config.maxOrchestratorTurns ?? 20;
101
+ const orchestratorMessages = await this.buildOrchestratorContext(messages);
102
+
103
+ for (let turn = 0; turn < maxTurns; turn++) {
104
+ const action = await this.orchestratorLoop.nextAction(orchestratorMessages);
105
+
106
+ // Final response — pass through to outer agent
107
+ if (action.type === 'final') {
108
+ return action;
109
+ }
110
+
111
+ // Batch of thread calls (parallel dispatch)
112
+ if (action.type === 'tool_batch') {
113
+ const { threadRequests, extraCalls } = this.partitionCalls(action.calls);
114
+
115
+ // Handle extra orchestrator tools first
116
+ if (extraCalls.length > 0 && this.config.onOrchestratorTool) {
117
+ // Extra tools produce directives — return immediately
118
+ const firstExtra = extraCalls[0]!;
119
+ return await this.config.onOrchestratorTool(firstExtra.name, firstExtra.args);
120
+ }
121
+
122
+ // Record assistant message with tool calls
123
+ this.appendAssistantMessage(orchestratorMessages, action.calls);
124
+
125
+ // Dispatch all threads in parallel
126
+ if (threadRequests.length > 0) {
127
+ const results = await this.threadExecutor.executeAll(threadRequests);
128
+ this.appendThreadResults(orchestratorMessages, action.calls, results);
129
+ }
130
+
131
+ continue;
132
+ }
133
+
134
+ // Single tool call
135
+ if (action.type === 'tool') {
136
+ if (action.name === 'Thread') {
137
+ const request = this.toThreadRequest(action.args);
138
+
139
+ // Record assistant message
140
+ this.appendAssistantMessage(orchestratorMessages, [action]);
141
+
142
+ const result = await this.threadExecutor.execute(request);
143
+ this.appendThreadResults(orchestratorMessages, [action], [result]);
144
+ continue;
145
+ }
146
+
147
+ // Extra orchestrator tool
148
+ if (this.config.onOrchestratorTool) {
149
+ return await this.config.onOrchestratorTool(action.name, action.args);
150
+ }
151
+ }
152
+ }
153
+
154
+ return { type: 'final', content: 'Orchestrator reached maximum turns.' };
155
+ }
156
+
157
+ /**
158
+ * Streaming version of the orchestration loop.
159
+ * Yields events throughout: text deltas during orchestrator reasoning,
160
+ * tool_start/tool_end for thread dispatch/completion.
161
+ */
162
+ async *streamAction(messages: AgentMessage[]): AsyncGenerator<AgentStreamEvent> {
163
+ const maxTurns = this.config.maxOrchestratorTurns ?? 20;
164
+ const orchestratorMessages = await this.buildOrchestratorContext(messages);
165
+ let totalSteps = 0;
166
+
167
+ for (let turn = 0; turn < maxTurns; turn++) {
168
+ totalSteps++;
169
+ yield { type: 'step_start', step: totalSteps };
170
+
171
+ // Stream orchestrator LLM to get text deltas + tool calls
172
+ const pendingTools: ToolCallAction[] = [];
173
+ let finalText = '';
174
+
175
+ if (this.orchestratorLoop.streamAction) {
176
+ for await (const event of this.orchestratorLoop.streamAction(orchestratorMessages)) {
177
+ if (event.type === 'text_delta') {
178
+ finalText += event.text;
179
+ yield event;
180
+ }
181
+ if (event.type === 'tool_start') {
182
+ pendingTools.push({
183
+ type: 'tool',
184
+ name: event.name,
185
+ args: event.args,
186
+ ...(event.toolCallId != null ? { toolCallId: event.toolCallId } : {}),
187
+ });
188
+ }
189
+ }
190
+ } else {
191
+ // Fallback to non-streaming
192
+ const action = await this.orchestratorLoop.nextAction(orchestratorMessages);
193
+ if (action.type === 'final') {
194
+ yield { type: 'step_end', step: totalSteps };
195
+ yield { type: 'done', output: action.content, steps: totalSteps };
196
+ return;
197
+ }
198
+ if (action.type === 'tool_batch') {
199
+ pendingTools.push(...action.calls);
200
+ } else if (action.type === 'tool') {
201
+ pendingTools.push(action);
202
+ }
203
+ }
204
+
205
+ // No tools → final response
206
+ if (pendingTools.length === 0) {
207
+ orchestratorMessages.push({ role: 'assistant', content: finalText });
208
+ yield { type: 'step_end', step: totalSteps };
209
+ yield { type: 'done', output: finalText, steps: totalSteps };
210
+ return;
211
+ }
212
+
213
+ // Partition into thread calls and extra tool calls
214
+ const threadCalls = pendingTools.filter(c => c.name === 'Thread');
215
+ const extraCalls = pendingTools.filter(c => c.name !== 'Thread');
216
+
217
+ // Handle extra orchestrator tools
218
+ if (extraCalls.length > 0 && this.config.onOrchestratorTool) {
219
+ const firstExtra = extraCalls[0]!;
220
+ yield { type: 'tool_start', name: firstExtra.name, args: firstExtra.args };
221
+ // Extra tools produce directives — end the stream
222
+ yield { type: 'step_end', step: totalSteps };
223
+ yield { type: 'done', output: `Directive: ${firstExtra.name}`, steps: totalSteps };
224
+ return;
225
+ }
226
+
227
+ // Record assistant message
228
+ this.appendAssistantMessage(orchestratorMessages, pendingTools, finalText);
229
+
230
+ // Yield tool_start for each thread
231
+ for (const call of threadCalls) {
232
+ yield {
233
+ type: 'tool_start',
234
+ name: 'Thread',
235
+ args: call.args,
236
+ ...(call.toolCallId != null ? { toolCallId: call.toolCallId } : {}),
237
+ };
238
+ }
239
+
240
+ // Execute all threads in parallel (shallow — no inner streaming)
241
+ const threadRequests = threadCalls.map(c => this.toThreadRequest(c.args));
242
+ const results = await this.threadExecutor.executeAll(threadRequests);
243
+
244
+ // Yield tool_end for each completed thread
245
+ for (let i = 0; i < results.length; i++) {
246
+ const result = results[i]!;
247
+ yield {
248
+ type: 'tool_end',
249
+ name: 'Thread',
250
+ result: {
251
+ success: result.success,
252
+ output: result.episode.summary,
253
+ episodeId: result.episode.id,
254
+ toolCalls: result.episode.toolCalls,
255
+ steps: result.episode.steps,
256
+ filesRead: result.episode.filesRead,
257
+ filesModified: result.episode.filesModified,
258
+ ...(result.durationMs != null ? { threadDurationMs: result.durationMs } : {}),
259
+ ...(result.resolvedModel != null ? { resolvedModel: result.resolvedModel } : {}),
260
+ ...(result.error != null ? { error: result.error } : {}),
261
+ },
262
+ };
263
+ }
264
+
265
+ // Append thread results to orchestrator context
266
+ this.appendThreadResults(orchestratorMessages, threadCalls, results);
267
+
268
+ yield { type: 'step_end', step: totalSteps };
269
+ }
270
+
271
+ yield { type: 'done', output: 'Orchestrator reached maximum turns.', steps: totalSteps };
272
+ }
273
+
274
+ // ── Private helpers ──
275
+
276
+ private async buildOrchestratorContext(outerMessages: AgentMessage[]): Promise<AgentMessage[]> {
277
+ const messages: AgentMessage[] = [];
278
+
279
+ // Inject long-term memories
280
+ const longTermMemories = await this.config.longTermStore.getAllMemories();
281
+ if (longTermMemories.length > 0) {
282
+ const memoryText = longTermMemories
283
+ .map(m => `[${m.category}] ${m.content}`)
284
+ .join('\n');
285
+ messages.push({
286
+ role: 'system',
287
+ content: `Long-term memories:\n${memoryText}`,
288
+ });
289
+ }
290
+
291
+ // Inject session memos
292
+ const sessionMemos = await this.config.sessionMemoStore.getMemosBySession(this.config.sessionId);
293
+ if (sessionMemos.length > 0) {
294
+ const memoText = sessionMemos.map(m => m.content).join('\n---\n');
295
+ messages.push({
296
+ role: 'system',
297
+ content: `Session memos:\n${memoText}`,
298
+ });
299
+ }
300
+
301
+ // Inject existing episode summaries for this task
302
+ const existingEpisodes = await this.config.episodeStore.getEpisodesByTask(this.config.taskId);
303
+ if (existingEpisodes.length > 0) {
304
+ const episodeText = existingEpisodes
305
+ .map(e => `Episode ${e.index} [${e.id}]:\n${e.summary}`)
306
+ .join('\n\n');
307
+ messages.push({
308
+ role: 'system',
309
+ content: `Prior episodes for this task:\n${episodeText}`,
310
+ });
311
+ }
312
+
313
+ // Include outer messages (user prompt, any history)
314
+ messages.push(...outerMessages);
315
+
316
+ return messages;
317
+ }
318
+
319
+ private partitionCalls(calls: ToolCallAction[]): {
320
+ threadRequests: ThreadRequest[];
321
+ extraCalls: ToolCallAction[];
322
+ } {
323
+ const threadRequests: ThreadRequest[] = [];
324
+ const extraCalls: ToolCallAction[] = [];
325
+
326
+ for (const call of calls) {
327
+ if (call.name === 'Thread') {
328
+ threadRequests.push(this.toThreadRequest(call.args));
329
+ } else {
330
+ extraCalls.push(call);
331
+ }
332
+ }
333
+
334
+ return { threadRequests, extraCalls };
335
+ }
336
+
337
+ private toThreadRequest(args: Record<string, unknown>): ThreadRequest {
338
+ const req: ThreadRequest = { action: String(args.action ?? '') };
339
+ if (Array.isArray(args.contextEpisodeIds)) {
340
+ req.contextEpisodeIds = args.contextEpisodeIds.map(String);
341
+ }
342
+ if (args.model != null) {
343
+ req.model = String(args.model);
344
+ }
345
+ if (typeof args.maxSteps === 'number') {
346
+ req.maxSteps = args.maxSteps;
347
+ }
348
+ return req;
349
+ }
350
+
351
+ private appendAssistantMessage(
352
+ messages: AgentMessage[],
353
+ calls: ToolCallAction[],
354
+ text?: string,
355
+ ): void {
356
+ const content = text ?? calls.map(c =>
357
+ c.name === 'Thread'
358
+ ? `Thread: ${String(c.args.action ?? '')}`
359
+ : `${c.name}: ${JSON.stringify(c.args)}`
360
+ ).join('\n');
361
+
362
+ messages.push({
363
+ role: 'assistant',
364
+ content,
365
+ toolCalls: calls.map(c => ({
366
+ toolCallId: c.toolCallId ?? randomUUID(),
367
+ toolName: c.name,
368
+ args: c.args,
369
+ })),
370
+ });
371
+ }
372
+
373
+ private appendThreadResults(
374
+ messages: AgentMessage[],
375
+ calls: ToolCallAction[],
376
+ results: ThreadResult[],
377
+ ): void {
378
+ for (let i = 0; i < results.length; i++) {
379
+ const result = results[i]!;
380
+ const call = calls[i];
381
+ const callId = call?.toolCallId ?? '';
382
+
383
+ messages.push({
384
+ role: 'tool',
385
+ content: result.episode.summary,
386
+ toolResults: [{
387
+ toolCallId: callId,
388
+ toolName: 'Thread',
389
+ result: result.episode.summary,
390
+ isError: !result.success,
391
+ }],
392
+ });
393
+ }
394
+ }
395
+ }