@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 +35 -0
- package/package.json +2 -1
- package/src/agent/create-agent.ts +9 -0
- package/src/agent/types.ts +15 -2
- package/src/arc/arc-loop.ts +395 -0
- package/src/arc/arc-types.ts +215 -0
- package/src/arc/bridge-tools.ts +170 -0
- package/src/arc/bridged-tool-provider.ts +80 -0
- package/src/arc/consolidation.ts +118 -0
- package/src/arc/create-arc-agent.ts +80 -0
- package/src/arc/debug.ts +62 -0
- package/src/arc/episode-compressor.ts +151 -0
- package/src/arc/object-store/fs-object-store.ts +60 -0
- package/src/arc/object-store/memory-object-store.ts +41 -0
- package/src/arc/object-store/object-store.ts +12 -0
- package/src/arc/stores/episode-store.ts +120 -0
- package/src/arc/stores/long-term-store.ts +86 -0
- package/src/arc/stores/rxdb-setup.ts +112 -0
- package/src/arc/stores/session-memo-store.ts +58 -0
- package/src/arc/thread-executor.ts +365 -0
- package/src/arc/thread-tool.ts +29 -0
- package/src/loop/context-store.ts +12 -9
- package/src/loop/vercel-agent-loop.ts +12 -6
- package/tests/integration/agent-skill-default-from-sandbox.spec.ts +3 -2
- package/tests/unit/structured-messages.spec.ts +1 -1
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.
|
|
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
|
}
|
package/src/agent/types.ts
CHANGED
|
@@ -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
|
+
}
|