@bluecopa/harness 0.1.0-snapshot.99 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2926 +0,0 @@
1
- import { anthropic, createAnthropic } from '@ai-sdk/anthropic';
2
- import { tool, generateText, generateObject, streamText, stepCountIs } from 'ai';
3
- import { z } from 'zod';
4
- import { randomUUID } from 'crypto';
5
- import { readFile } from 'fs/promises';
6
- import { dirname, resolve } from 'path';
7
-
8
- // src/arc/arc-loop.ts
9
-
10
- // src/agent/types.ts
11
- function getTextContent(content) {
12
- if (typeof content === "string") return content;
13
- return content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
14
- }
15
-
16
- // src/arc/arc-types.ts
17
- var DEFAULT_MODEL_MAP = {
18
- fast: "claude-haiku-4-5",
19
- medium: "claude-sonnet-4-6",
20
- strong: "claude-opus-4-6"
21
- };
22
- function resolveModel(modelOrTier, modelMap, fallback) {
23
- if (!modelOrTier) return fallback;
24
- if (modelOrTier in modelMap) return modelMap[modelOrTier];
25
- return modelOrTier;
26
- }
27
-
28
- // src/arc/message-convert.ts
29
- function toModelMessages(messages) {
30
- const out = [];
31
- let seenNonSystem = false;
32
- for (const msg of messages) {
33
- if (msg.role === "system") {
34
- const text = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
35
- if (seenNonSystem) {
36
- out.push({ role: "user", content: `[System] ${text}` });
37
- } else {
38
- out.push({ role: "system", content: text });
39
- }
40
- continue;
41
- }
42
- seenNonSystem = true;
43
- if (msg.role === "user") {
44
- out.push({ role: "user", content: msg.content });
45
- continue;
46
- }
47
- if (msg.role === "assistant") {
48
- const textContent = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
49
- if (msg.toolCalls && msg.toolCalls.length > 0) {
50
- const parts = [];
51
- if (textContent) {
52
- parts.push({ type: "text", text: textContent });
53
- }
54
- for (const tc of msg.toolCalls) {
55
- const part = {
56
- type: "tool-call",
57
- toolCallId: tc.toolCallId,
58
- toolName: tc.toolName,
59
- input: tc.args
60
- };
61
- if (tc.providerMetadata) {
62
- part.providerOptions = tc.providerMetadata;
63
- }
64
- parts.push(part);
65
- }
66
- const assistantMsg = { role: "assistant", content: parts };
67
- if (msg.providerMetadata) {
68
- assistantMsg.providerOptions = msg.providerMetadata;
69
- }
70
- out.push(assistantMsg);
71
- } else {
72
- out.push({ role: "assistant", content: textContent });
73
- }
74
- continue;
75
- }
76
- if (msg.role === "tool") {
77
- if (msg.toolResults && msg.toolResults.length > 0) {
78
- const parts = msg.toolResults.map((tr) => ({
79
- type: "tool-result",
80
- toolCallId: tr.toolCallId,
81
- toolName: tr.toolName,
82
- output: tr.isError ? { type: "error-text", value: tr.result } : { type: "text", value: tr.result }
83
- }));
84
- out.push({ role: "tool", content: parts });
85
- } else {
86
- const textContent = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
87
- out.push({ role: "user", content: `[Tool result]: ${textContent}` });
88
- }
89
- continue;
90
- }
91
- }
92
- return out;
93
- }
94
- function estimateTokens(text) {
95
- return Math.ceil(text.length / 4);
96
- }
97
- function estimateMessagesTokens(messages) {
98
- let total = 0;
99
- for (const msg of messages) {
100
- const text = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
101
- total += estimateTokens(text);
102
- if (msg.toolCalls) {
103
- for (const tc of msg.toolCalls) {
104
- total += estimateTokens(JSON.stringify(tc.args));
105
- }
106
- }
107
- if (msg.toolResults) {
108
- for (const tr of msg.toolResults) {
109
- total += estimateTokens(tr.result);
110
- }
111
- }
112
- }
113
- return total;
114
- }
115
-
116
- // src/arc/types.ts
117
- function resolveToolChoice(config, turn) {
118
- if (!config) return "auto";
119
- return typeof config === "function" ? config(turn) : config;
120
- }
121
- function isProfileDeclaration(p) {
122
- return "signature" in p && typeof p.signature === "string";
123
- }
124
- var builtinTools = {
125
- Bash: tool({
126
- description: "Run a shell command",
127
- inputSchema: z.object({
128
- command: z.string().describe("The shell command to run"),
129
- cwd: z.string().optional().describe("Working directory"),
130
- timeout: z.number().optional().describe("Timeout in milliseconds")
131
- })
132
- }),
133
- Read: tool({
134
- description: "Read a file from the filesystem",
135
- inputSchema: z.object({
136
- path: z.string().describe("Absolute path to the file")
137
- })
138
- }),
139
- Write: tool({
140
- description: "Write content to a file (creates or overwrites)",
141
- inputSchema: z.object({
142
- path: z.string().describe("Absolute path to the file"),
143
- content: z.string().describe("Content to write")
144
- })
145
- }),
146
- Edit: tool({
147
- description: "Replace text in a file (exact match)",
148
- inputSchema: z.object({
149
- path: z.string().describe("Absolute path to the file"),
150
- old_text: z.string().describe("Exact text to find"),
151
- new_text: z.string().describe("Replacement text")
152
- })
153
- }),
154
- Glob: tool({
155
- description: "Find files matching a glob pattern",
156
- inputSchema: z.object({
157
- pattern: z.string().describe("Glob pattern (e.g. **/*.ts)")
158
- })
159
- }),
160
- Grep: tool({
161
- description: "Search file contents with a regex pattern",
162
- inputSchema: z.object({
163
- pattern: z.string().describe("Regex pattern to search for"),
164
- path: z.string().optional().describe("Directory or file to search in")
165
- })
166
- }),
167
- WebFetch: tool({
168
- description: "Fetch content from a URL",
169
- inputSchema: z.object({
170
- url: z.string().describe("URL to fetch"),
171
- selector: z.string().optional().describe("CSS selector to extract"),
172
- maxContentLength: z.number().optional().describe("Max content length")
173
- })
174
- }),
175
- WebSearch: tool({
176
- description: "Search the web",
177
- inputSchema: z.object({
178
- query: z.string().describe("Search query")
179
- })
180
- }),
181
- AskUser: tool({
182
- description: "Ask the user a question and wait for their response",
183
- inputSchema: z.object({
184
- question: z.string().describe("The question to ask"),
185
- options: z.array(z.string()).optional().describe("Optional choices")
186
- })
187
- }),
188
- TellUser: tool({
189
- description: "Display a message to the user (no response expected)",
190
- inputSchema: z.object({
191
- message: z.string().describe("The message to display")
192
- })
193
- }),
194
- DownloadRawFile: tool({
195
- description: "Download a file from the sandbox to the local machine",
196
- inputSchema: z.object({
197
- path: z.string().describe("Sandbox path to download")
198
- })
199
- })
200
- };
201
-
202
- // src/arc/tools.ts
203
- var Thread = tool({
204
- description: "Dispatch a process. Returns immediately. Multiple per turn = parallel.",
205
- inputSchema: z.object({
206
- action: z.string().describe("What the process should accomplish"),
207
- contextEpisodeIds: z.array(z.string()).optional().describe("Episode IDs to seed as context"),
208
- model: z.enum(["fast", "medium", "strong"]).optional().describe("Model tier (default: medium)"),
209
- maxSteps: z.number().optional().describe("Max tool-call steps"),
210
- label: z.string().optional().describe("Human-readable label"),
211
- profile: z.string().optional().describe('Named process profile (e.g. "readonly", "generalist")')
212
- })
213
- });
214
- var Check = tool({
215
- description: "Check status or retrieve results of a process by ID.",
216
- inputSchema: z.object({
217
- id: z.string().describe("Process ID to check")
218
- })
219
- });
220
- var Cancel = tool({
221
- description: "Cancel a running process.",
222
- inputSchema: z.object({
223
- id: z.string().describe("Process ID to cancel")
224
- })
225
- });
226
- var Remember = tool({
227
- description: "Save information to persistent memory.",
228
- inputSchema: z.object({
229
- content: z.string().describe("What to remember"),
230
- category: z.enum(["instruction", "preference", "fact", "pattern"]).optional().describe("Memory category")
231
- })
232
- });
233
- var ReadEpisode = tool({
234
- description: "Retrieve the full trace of a completed episode by ID. Use to inspect detailed tool outputs, file contents, or error messages that the episode summary omits.",
235
- inputSchema: z.object({
236
- id: z.string().describe("Episode ID to read"),
237
- maxTokens: z.number().optional().describe("Max tokens to return (truncates from the end). Default: no limit.")
238
- })
239
- });
240
- var orchestratorTools = { Thread, Check, Cancel, Remember, ReadEpisode };
241
- tool({
242
- description: "Ask the user a question and wait for their response.",
243
- inputSchema: z.object({
244
- question: z.string().describe("The question to ask"),
245
- options: z.array(z.string()).optional().describe("Optional choices")
246
- })
247
- });
248
- tool({
249
- description: "Display a message to the user (no response expected).",
250
- inputSchema: z.object({
251
- message: z.string().describe("The message to display")
252
- })
253
- });
254
-
255
- // src/arc/openrouter-models.ts
256
- var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/models";
257
- var CACHE_TTL_MS = 5 * 60 * 1e3;
258
- var DEFAULT_CONTEXT_LIMIT = 128e3;
259
- var modelCache = null;
260
- var fetchPromise = null;
261
- async function fetchOpenRouterModels() {
262
- if (modelCache && Date.now() - modelCache.fetchedAt < CACHE_TTL_MS) {
263
- return modelCache.models;
264
- }
265
- if (fetchPromise) {
266
- return fetchPromise;
267
- }
268
- fetchPromise = (async () => {
269
- try {
270
- const response = await fetch(OPENROUTER_API_URL, {
271
- method: "GET",
272
- headers: {
273
- "Content-Type": "application/json"
274
- }
275
- });
276
- if (!response.ok) {
277
- throw new Error(`OpenRouter API error: ${response.status} ${response.statusText}`);
278
- }
279
- const data = await response.json();
280
- const models = /* @__PURE__ */ new Map();
281
- for (const model of data.data) {
282
- if (model.context_length) {
283
- models.set(model.id, model.context_length);
284
- }
285
- }
286
- modelCache = {
287
- models,
288
- fetchedAt: Date.now()
289
- };
290
- return models;
291
- } catch (error) {
292
- console.warn("[OpenRouter] Failed to fetch models, using fallback:", error);
293
- const emptyCache = /* @__PURE__ */ new Map();
294
- modelCache = {
295
- models: emptyCache,
296
- fetchedAt: Date.now()
297
- };
298
- return emptyCache;
299
- } finally {
300
- fetchPromise = null;
301
- }
302
- })();
303
- return fetchPromise;
304
- }
305
- async function getModelContextLimit(modelId, fallback = DEFAULT_CONTEXT_LIMIT) {
306
- const models = await fetchOpenRouterModels();
307
- if (models.has(modelId)) {
308
- return models.get(modelId);
309
- }
310
- for (const [id, limit] of models.entries()) {
311
- if (id.endsWith(modelId) || modelId.endsWith(id)) {
312
- return limit;
313
- }
314
- }
315
- return fallback;
316
- }
317
-
318
- // src/arc/context-window.ts
319
- var ContextWindow = class {
320
- config;
321
- turns = [];
322
- episodes = [];
323
- cachedContextSize = null;
324
- constructor(config) {
325
- this.config = config;
326
- }
327
- /**
328
- * Get the effective context window size.
329
- * If dynamicContextWindow is enabled, fetches from OpenRouter API.
330
- * Otherwise uses the configured contextWindowSize.
331
- */
332
- async getContextWindowSize() {
333
- if (this.cachedContextSize !== null) {
334
- return this.cachedContextSize;
335
- }
336
- if (this.config.dynamicContextWindow && this.config.modelId) {
337
- try {
338
- const dynamicSize = await getModelContextLimit(this.config.modelId, 128e3);
339
- this.cachedContextSize = dynamicSize;
340
- return dynamicSize;
341
- } catch (error) {
342
- console.warn("[ContextWindow] Failed to fetch dynamic context limit, using fallback:", error);
343
- this.cachedContextSize = 128e3;
344
- return 128e3;
345
- }
346
- }
347
- this.cachedContextSize = this.config.contextWindowSize;
348
- return this.config.contextWindowSize;
349
- }
350
- async prepare(signal) {
351
- const contextWindowSize = await this.getContextWindowSize();
352
- const totalBudget = contextWindowSize - this.config.outputReserve;
353
- this.episodes = await this.config.episodeStore.getEpisodesByTask(this.config.taskId);
354
- const filesBeingTouched = this.extractFilesBeingTouched();
355
- const systemTokens = estimateTokens(this.config.systemPrompt);
356
- const memoryBudget = Math.min(Math.floor(totalBudget * 0.15), 4e3);
357
- const memoryMessages = await this.config.memory.retrieve(memoryBudget, { filesBeingTouched });
358
- const memoryTokens = estimateMessagesTokens(memoryMessages);
359
- const episodeMessages = this.buildEpisodeSummaries();
360
- let episodeTokens = estimateMessagesTokens(episodeMessages);
361
- let turnMessages = this.buildTurnMessages();
362
- let conversationTokens = estimateMessagesTokens(turnMessages);
363
- let totalUsed = systemTokens + memoryTokens + episodeTokens + conversationTokens;
364
- if (totalUsed > totalBudget * 0.6) {
365
- turnMessages = this.compressTemplate(turnMessages);
366
- conversationTokens = estimateMessagesTokens(turnMessages);
367
- totalUsed = systemTokens + memoryTokens + episodeTokens + conversationTokens;
368
- }
369
- const episodeBudget = Math.floor(totalBudget * 0.2);
370
- if (episodeTokens > episodeBudget && this.episodes.length > 10) {
371
- const grouped = this.groupEpisodeSummaries();
372
- episodeTokens = estimateMessagesTokens(grouped);
373
- episodeMessages.length = 0;
374
- episodeMessages.push(...grouped);
375
- totalUsed = systemTokens + memoryTokens + episodeTokens + conversationTokens;
376
- }
377
- if (totalUsed > totalBudget * 0.8 && signal) {
378
- try {
379
- turnMessages = await this.compressLLM(turnMessages, signal);
380
- conversationTokens = estimateMessagesTokens(turnMessages);
381
- totalUsed = systemTokens + memoryTokens + episodeTokens + conversationTokens;
382
- } catch {
383
- }
384
- }
385
- const finalMemoryMessages = await this.config.memory.retrieve(memoryBudget, { filesBeingTouched });
386
- const finalMemoryTokens = estimateMessagesTokens(finalMemoryMessages);
387
- const messages = [
388
- ...finalMemoryMessages,
389
- ...episodeMessages,
390
- ...turnMessages
391
- ];
392
- const budget = {
393
- limit: totalBudget,
394
- used: systemTokens + finalMemoryTokens + episodeTokens + conversationTokens,
395
- layers: {
396
- system: systemTokens,
397
- memories: finalMemoryTokens,
398
- episodes: episodeTokens,
399
- conversation: conversationTokens
400
- }
401
- };
402
- const statusText = `[Context: ${Math.round(budget.used / 1e3)}K/${Math.round(budget.limit / 1e3)}K tokens | system: ${Math.round(budget.layers.system / 1e3)}K | memories: ${Math.round(budget.layers.memories / 1e3)}K | episodes: ${Math.round(budget.layers.episodes / 1e3)}K | conversation: ${Math.round(budget.layers.conversation / 1e3)}K]`;
403
- messages.push({ role: "system", content: statusText });
404
- return { messages, budget };
405
- }
406
- recordTurn(turn) {
407
- this.turns.push(turn);
408
- }
409
- async budget() {
410
- const contextWindowSize = await this.getContextWindowSize();
411
- const totalBudget = contextWindowSize - this.config.outputReserve;
412
- const systemTokens = estimateTokens(this.config.systemPrompt);
413
- let conversationTokens = 0;
414
- for (const turn of this.turns) {
415
- conversationTokens += turn.tokenEstimate;
416
- }
417
- let episodeTokens = 0;
418
- for (const ep of this.episodes) {
419
- episodeTokens += estimateTokens(ep.summary);
420
- }
421
- return {
422
- limit: totalBudget,
423
- used: systemTokens + conversationTokens + episodeTokens,
424
- layers: {
425
- system: systemTokens,
426
- memories: 0,
427
- episodes: episodeTokens,
428
- conversation: conversationTokens
429
- }
430
- };
431
- }
432
- addEpisode(episode) {
433
- this.episodes.push(episode);
434
- }
435
- extractFilesBeingTouched() {
436
- const files = /* @__PURE__ */ new Set();
437
- for (const ep of this.episodes) {
438
- for (const f of ep.filesModified) files.add(f);
439
- for (const f of ep.filesRead) files.add(f);
440
- }
441
- for (const turn of this.turns.slice(-5)) {
442
- for (const msg of turn.messages) {
443
- const text = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
444
- const pathMatches = text.match(/(?:\/[\w.-]+)+\.\w+/g);
445
- if (pathMatches) {
446
- for (const p of pathMatches) files.add(p);
447
- }
448
- }
449
- }
450
- return [...files];
451
- }
452
- buildEpisodeSummaries() {
453
- if (this.episodes.length === 0) return [];
454
- const episodeText = this.episodes.map((e) => `Episode ${e.index} [${e.id}] (${e.success ? "ok" : "failed"}):
455
- ${e.summary}`).join("\n\n");
456
- return [{
457
- role: "system",
458
- content: `Prior episodes for this task:
459
- ${episodeText}`
460
- }];
461
- }
462
- buildTurnMessages() {
463
- const messages = [];
464
- for (const turn of this.turns) {
465
- messages.push(...turn.messages);
466
- }
467
- return messages;
468
- }
469
- compressTemplate(messages) {
470
- if (messages.length <= 6) return messages;
471
- const result = [];
472
- const recentCount = Math.min(6, Math.ceil(messages.length * 0.3));
473
- const oldMessages = messages.slice(0, -recentCount);
474
- const recentMessages = messages.slice(-recentCount);
475
- for (const msg of oldMessages) {
476
- if (msg.role === "tool") {
477
- const text = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
478
- const truncated = text.length > 500 ? text.slice(0, 500) + "..." : text;
479
- const compressed = { ...msg, content: truncated };
480
- if (msg.toolResults) {
481
- compressed.toolResults = msg.toolResults.map((tr) => ({
482
- ...tr,
483
- result: tr.result.length > 500 ? tr.result.slice(0, 500) + "..." : tr.result
484
- }));
485
- }
486
- result.push(compressed);
487
- } else if (msg.role === "assistant") {
488
- const text = typeof msg.content === "string" ? msg.content : getTextContent(msg.content);
489
- const oneLiner = text.length > 200 ? text.slice(0, 200) + "..." : text;
490
- result.push({
491
- ...msg,
492
- content: oneLiner
493
- });
494
- } else {
495
- result.push(msg);
496
- }
497
- }
498
- result.push(...recentMessages);
499
- return result;
500
- }
501
- groupEpisodeSummaries() {
502
- if (this.episodes.length <= 10) return this.buildEpisodeSummaries();
503
- const oldEpisodes = this.episodes.slice(0, -5);
504
- const recentEpisodes = this.episodes.slice(-5);
505
- const groupSummary = [
506
- `Grouped summary of ${oldEpisodes.length} earlier episodes:`,
507
- ` Successful: ${oldEpisodes.filter((e) => e.success).length}`,
508
- ` Failed: ${oldEpisodes.filter((e) => !e.success).length}`,
509
- ` Files modified: ${[...new Set(oldEpisodes.flatMap((e) => e.filesModified))].join(", ")}`,
510
- ` Actions: ${oldEpisodes.map((e) => e.threadAction.slice(0, 60)).join("; ")}`
511
- ].join("\n");
512
- const recentText = recentEpisodes.map((e) => `Episode ${e.index} [${e.id}] (${e.success ? "ok" : "failed"}):
513
- ${e.summary}`).join("\n\n");
514
- return [{
515
- role: "system",
516
- content: `${groupSummary}
517
-
518
- Recent episodes:
519
- ${recentText}`
520
- }];
521
- }
522
- async compressLLM(messages, signal) {
523
- if (messages.length <= 10) return messages;
524
- const splitPoint = Math.floor(messages.length / 2);
525
- const toSummarize = messages.slice(0, splitPoint);
526
- const toKeep = messages.slice(splitPoint);
527
- const textToSummarize = toSummarize.map((m) => {
528
- const text = typeof m.content === "string" ? m.content : getTextContent(m.content);
529
- return `[${m.role}] ${text.slice(0, 300)}`;
530
- }).join("\n");
531
- const fastModel = resolveModel("fast", DEFAULT_MODEL_MAP, DEFAULT_MODEL_MAP.fast);
532
- const result = await generateText({
533
- model: (this.config.createModel ?? anthropic)(fastModel),
534
- system: "Summarize this conversation history into a concise context summary. Preserve key decisions, outcomes, and any information needed to continue the conversation. Keep under 500 words.",
535
- messages: [{ role: "user", content: textToSummarize.slice(0, 8e3) }],
536
- abortSignal: signal
537
- });
538
- const summaryMessage = {
539
- role: "system",
540
- content: `[Compressed conversation history]
541
- ${result.text}`
542
- };
543
- return [summaryMessage, ...toKeep];
544
- }
545
- };
546
- var TOPIC_MAP = [
547
- [/\bapi\b/i, "api-rules"],
548
- [/\bauth\b/i, "auth"],
549
- [/\btest/i, "testing"],
550
- [/\bbuild\b|\bci\b|\bpipeline\b/i, "build"],
551
- [/\bdeploy/i, "deployment"],
552
- [/\bdb\b|\bmigrat/i, "database"],
553
- [/\bstyle|\.css|\.scss/i, "styling"],
554
- [/\bconfig/i, "config"]
555
- ];
556
- function matchTopics(filesBeingTouched) {
557
- const topics = /* @__PURE__ */ new Set();
558
- for (const file of filesBeingTouched) {
559
- for (const [pattern, topic] of TOPIC_MAP) {
560
- if (pattern.test(file)) {
561
- topics.add(topic);
562
- }
563
- }
564
- }
565
- return topics;
566
- }
567
- var MemoryManager = class {
568
- memoStore;
569
- ltStore;
570
- sessionId;
571
- constructor(config) {
572
- this.memoStore = config.sessionMemoStore;
573
- this.ltStore = config.longTermStore;
574
- this.sessionId = config.sessionId;
575
- }
576
- async store(content, opts) {
577
- const scope = opts?.scope ?? "session";
578
- if (scope === "persistent") {
579
- const id2 = randomUUID();
580
- const now = Date.now();
581
- const memory = {
582
- id: id2,
583
- content,
584
- category: opts?.category ?? "fact",
585
- sourceSessionMemoIds: [],
586
- createdAt: now,
587
- updatedAt: now
588
- };
589
- await this.ltStore.addMemory(memory);
590
- return id2;
591
- }
592
- const existing = this.memoStore.getMemoBySession ? await this.memoStore.getMemoBySession(this.sessionId) : (await this.memoStore.getMemosBySession(this.sessionId))[0] ?? null;
593
- const createdAt = existing?.createdAt ?? Date.now();
594
- const updatedAt = Date.now();
595
- const combinedContent = existing?.content ? `${existing.content}
596
- ${content}` : content;
597
- const id = this.sessionId;
598
- const memo = {
599
- id,
600
- sessionId: this.sessionId,
601
- content: combinedContent,
602
- sourceEpisodeIds: [],
603
- createdAt,
604
- updatedAt
605
- };
606
- await this.memoStore.addMemo(memo);
607
- return id;
608
- }
609
- async retrieve(budget, context) {
610
- const messages = [];
611
- let used = 0;
612
- const allMemories = await this.ltStore.getAllMemories();
613
- const indexMemories = allMemories.filter((m) => m.category === "index");
614
- const indexBudget = Math.min(budget * 0.15, 2e3);
615
- if (indexMemories.length > 0) {
616
- const indexText = indexMemories.map((m) => m.content).join("\n");
617
- const tokens = estimateTokens(indexText);
618
- if (tokens <= indexBudget) {
619
- messages.push({ role: "system", content: `[Memory Index]
620
- ${indexText}` });
621
- used += tokens;
622
- }
623
- }
624
- if (context.filesBeingTouched && context.filesBeingTouched.length > 0) {
625
- const topics = matchTopics(context.filesBeingTouched);
626
- const topicBudget = Math.min((budget - used) * 0.3, 4e3);
627
- let topicUsed = 0;
628
- for (const topic of topics) {
629
- if (topicUsed >= topicBudget) break;
630
- const topicMemories = allMemories.filter((m) => m.category === topic);
631
- for (const mem of topicMemories) {
632
- const tokens = estimateTokens(mem.content);
633
- if (topicUsed + tokens > topicBudget) break;
634
- messages.push({ role: "system", content: `[Memory: ${topic}]
635
- ${mem.content}` });
636
- topicUsed += tokens;
637
- used += tokens;
638
- }
639
- }
640
- }
641
- const remaining = budget - used;
642
- if (remaining > 0) {
643
- const sid = context.sessionId ?? this.sessionId;
644
- const memo = this.memoStore.getMemoBySession ? await this.memoStore.getMemoBySession(sid) : (await this.memoStore.getMemosBySession(sid))[0] ?? null;
645
- if (memo) {
646
- const memoBudget = remaining * 0.5;
647
- const tokens = estimateTokens(memo.content);
648
- if (tokens <= memoBudget) {
649
- messages.push({ role: "system", content: `[Session Memo]
650
- ${memo.content}` });
651
- used += tokens;
652
- }
653
- }
654
- const alreadyCategories = /* @__PURE__ */ new Set([
655
- "index",
656
- ...context.filesBeingTouched ? matchTopics(context.filesBeingTouched) : []
657
- ]);
658
- const ltByRecency = allMemories.filter((m) => !alreadyCategories.has(m.category)).sort((a, b) => b.updatedAt - a.updatedAt);
659
- for (const mem of ltByRecency) {
660
- const tokens = estimateTokens(mem.content);
661
- if (used + tokens > budget) break;
662
- messages.push({ role: "system", content: `[Memory: ${mem.category}]
663
- ${mem.content}` });
664
- used += tokens;
665
- }
666
- }
667
- return messages;
668
- }
669
- async detectAndPromote(episodes) {
670
- if (episodes.length < 2) return;
671
- const fileFreq = /* @__PURE__ */ new Map();
672
- const errorPatterns = /* @__PURE__ */ new Map();
673
- for (const ep of episodes) {
674
- for (const f of ep.filesModified) {
675
- fileFreq.set(f, (fileFreq.get(f) ?? 0) + 1);
676
- }
677
- if (!ep.success) {
678
- const normalized = ep.threadAction.toLowerCase().slice(0, 100);
679
- errorPatterns.set(normalized, (errorPatterns.get(normalized) ?? 0) + 1);
680
- }
681
- }
682
- const frequentFiles = [...fileFreq.entries()].filter(([, count]) => count >= 3);
683
- if (frequentFiles.length > 0) {
684
- const content = `Frequently modified files:
685
- ${frequentFiles.map(([f, n]) => ` ${f} (${n}x)`).join("\n")}`;
686
- await this.store(content, { category: "pattern", scope: "session" });
687
- }
688
- const repeatedErrors = [...errorPatterns.entries()].filter(([, count]) => count >= 2);
689
- if (repeatedErrors.length > 0) {
690
- const content = `Common failure patterns:
691
- ${repeatedErrors.map(([action, n]) => ` "${action}" failed ${n}x`).join("\n")}`;
692
- await this.store(content, { category: "pattern", scope: "session" });
693
- }
694
- }
695
- async compact() {
696
- const allMemories = await this.ltStore.getAllMemories();
697
- if (allMemories.length < 2) return { removed: 0, merged: 0 };
698
- let removed = 0;
699
- let merged = 0;
700
- const keywordSets = allMemories.map((m) => {
701
- const words = m.content.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
702
- return new Set(words);
703
- });
704
- const toRemove = /* @__PURE__ */ new Set();
705
- for (let i = 0; i < allMemories.length; i++) {
706
- if (toRemove.has(allMemories[i].id)) continue;
707
- for (let j = i + 1; j < allMemories.length; j++) {
708
- if (toRemove.has(allMemories[j].id)) continue;
709
- const setA = keywordSets[i];
710
- const setB = keywordSets[j];
711
- const intersection = [...setA].filter((w) => setB.has(w)).length;
712
- const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
713
- const overlap = union > 0 ? intersection / union : 0;
714
- if (overlap > 0.7) {
715
- const keepIdx = allMemories[i].updatedAt >= allMemories[j].updatedAt ? i : j;
716
- const removeIdx = keepIdx === i ? j : i;
717
- toRemove.add(allMemories[removeIdx].id);
718
- merged++;
719
- }
720
- }
721
- }
722
- const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1e3;
723
- for (const mem of allMemories) {
724
- if (mem.category === "session-summary" && mem.updatedAt < thirtyDaysAgo) {
725
- toRemove.add(mem.id);
726
- }
727
- }
728
- for (const id of toRemove) {
729
- await this.ltStore.deleteMemory(id);
730
- removed++;
731
- }
732
- return { removed, merged };
733
- }
734
- };
735
- async function consolidateEpisodes(episodes, sessionId, sessionMemoStore) {
736
- if (episodes.length === 0) {
737
- throw new Error("Cannot consolidate zero episodes");
738
- }
739
- const filesModified = /* @__PURE__ */ new Set();
740
- const toolsUsed = /* @__PURE__ */ new Set();
741
- const actions = [];
742
- let successCount = 0;
743
- for (const ep of episodes) {
744
- for (const f of ep.filesModified) filesModified.add(f);
745
- for (const t of ep.toolCalls) toolsUsed.add(t);
746
- actions.push(`${ep.index}. ${ep.threadAction} (${ep.success ? "ok" : "failed"})`);
747
- if (ep.success) successCount++;
748
- }
749
- const parts = [
750
- `Task summary (${episodes.length} threads, ${successCount} succeeded):`,
751
- ...actions
752
- ];
753
- if (filesModified.size > 0) {
754
- parts.push(`Files modified: ${[...filesModified].join(", ")}`);
755
- }
756
- if (toolsUsed.size > 0) {
757
- parts.push(`Tools used: ${[...toolsUsed].join(", ")}`);
758
- }
759
- const existing = sessionMemoStore.getMemoBySession ? await sessionMemoStore.getMemoBySession(sessionId) : (await sessionMemoStore.getMemosBySession(sessionId))[0] ?? null;
760
- const createdAt = existing?.createdAt ?? Date.now();
761
- const updatedAt = Date.now();
762
- const memo = {
763
- id: sessionId,
764
- sessionId,
765
- content: parts.join("\n"),
766
- sourceEpisodeIds: episodes.map((e) => e.id),
767
- createdAt,
768
- updatedAt
769
- };
770
- await sessionMemoStore.addMemo(memo);
771
- return memo;
772
- }
773
- async function runConsolidation(_taskId, sessionId, episodeStore, sessionMemoStore, _longTermStore) {
774
- const episodes = await episodeStore.getEpisodesBySession(sessionId);
775
- if (episodes.length > 0) {
776
- await consolidateEpisodes(episodes, sessionId, sessionMemoStore);
777
- }
778
- }
779
- var routeSchema = z.object({
780
- skillName: z.string().nullable(),
781
- confidence: z.number().min(0).max(1),
782
- rationale: z.string()
783
- });
784
- var DEFAULT_ALIASES = {
785
- xlsx: ["excel", "spreadsheet", "workbook", "csv"],
786
- docx: ["word", "document", "doc"],
787
- pptx: ["powerpoint", "slides", "presentation"],
788
- "visual-explainer": ["visual explainer", "visual explanation", "interactive visual", "visualize", "diagram d3", "data visualization", "d3"],
789
- "excalidraw-diagram": ["excalidraw", "architecture diagram", "flowchart", "sequence diagram"],
790
- "json-render": ["jsonrender", "interactive ui", "action buttons"]
791
- };
792
- var SkillRouter = class {
793
- model;
794
- createModel;
795
- minConfidence;
796
- aliases;
797
- constructor(config = {}) {
798
- this.model = config.model ?? process.env.HARNESS_SKILL_ROUTER_MODEL ?? "claude-3-5-haiku-latest";
799
- this.createModel = config.createModel ?? anthropic;
800
- this.minConfidence = config.minConfidence ?? Number(process.env.HARNESS_SKILL_ROUTER_THRESHOLD ?? "0.55");
801
- this.aliases = {
802
- ...DEFAULT_ALIASES,
803
- ...config.aliases ?? {}
804
- };
805
- }
806
- async selectSkill(prompt, summaries) {
807
- if (summaries.length === 0) return null;
808
- const lower = prompt.toLowerCase();
809
- const direct = summaries.find((skill) => this.containsToken(lower, skill.name.toLowerCase()));
810
- if (direct) return direct;
811
- for (const summary of summaries) {
812
- const aliasList = this.aliases[summary.name.toLowerCase()] ?? [];
813
- if (aliasList.some((alias) => this.containsToken(lower, alias.toLowerCase()))) {
814
- return summary;
815
- }
816
- }
817
- try {
818
- const skillList = summaries.map((s) => `- ${s.name}: ${s.description}`).join("\n");
819
- const { object } = await generateObject({
820
- model: this.createModel(this.model),
821
- schema: routeSchema,
822
- system: [
823
- "You are a skill router.",
824
- "Pick at most one skill name from the provided list.",
825
- "Only choose a skill when it clearly helps with the user request.",
826
- "If nothing clearly matches, return null skillName and low confidence."
827
- ].join(" "),
828
- prompt: [
829
- "User request:",
830
- prompt,
831
- "",
832
- "Available skills:",
833
- skillList,
834
- "",
835
- "Return one object with skillName, confidence, rationale."
836
- ].join("\n")
837
- });
838
- if (!object.skillName || object.confidence < this.minConfidence) {
839
- return null;
840
- }
841
- return summaries.find((s) => s.name === object.skillName) ?? null;
842
- } catch {
843
- return null;
844
- }
845
- }
846
- containsToken(haystack, needle) {
847
- if (!needle) return false;
848
- const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
849
- const regex = new RegExp(`\\b${escaped}\\b`, "i");
850
- return regex.test(haystack);
851
- }
852
- };
853
- function parseFrontmatter(raw) {
854
- if (!raw.startsWith("---\n")) {
855
- return { meta: {}, body: raw };
856
- }
857
- const end = raw.indexOf("\n---\n", 4);
858
- if (end === -1) {
859
- return { meta: {}, body: raw };
860
- }
861
- const header = raw.slice(4, end).split("\n");
862
- const meta = {};
863
- for (const line of header) {
864
- const idx = line.indexOf(":");
865
- if (idx <= 0) continue;
866
- const key = line.slice(0, idx).trim();
867
- const value = line.slice(idx + 1).trim().replace(/^"|"$/g, "");
868
- meta[key] = value;
869
- }
870
- return {
871
- meta,
872
- body: raw.slice(end + 5).trim()
873
- };
874
- }
875
- async function loadSkillFromFile(path) {
876
- const raw = await readFile(path, "utf8");
877
- const parsed = parseFrontmatter(raw);
878
- const pythonDeps = parsed.meta.python_deps ? parsed.meta.python_deps.split(",").map((item) => item.trim()).filter(Boolean) : [];
879
- const npmDeps = parsed.meta.npm_deps ? parsed.meta.npm_deps.split(",").map((item) => item.trim()).filter(Boolean) : [];
880
- return {
881
- name: parsed.meta.name ?? path.split("/").slice(-2, -1)[0] ?? "unknown-skill",
882
- description: parsed.meta.description ?? "No description provided",
883
- path,
884
- contextMode: parsed.meta.context === "fork" ? "fork" : "inline",
885
- dependencies: {
886
- python: pythonDeps,
887
- npm: npmDeps
888
- },
889
- instructions: parsed.body
890
- };
891
- }
892
-
893
- // src/arc/skill-resolver.ts
894
- var DefaultSkillResolver = class {
895
- router;
896
- summariesPromise;
897
- summaries = null;
898
- constructor(config) {
899
- this.router = new SkillRouter(config.routerConfig);
900
- this.summariesPromise = import('fs/promises').then((fs) => fs.readFile(config.skillIndexPath, "utf-8")).then((raw) => JSON.parse(raw)).catch(() => []);
901
- }
902
- async resolve(action, profileSkills) {
903
- if (!this.summaries) {
904
- this.summaries = await this.summariesPromise;
905
- }
906
- if (this.summaries.length === 0) return null;
907
- const candidates = profileSkills ? this.summaries.filter((s) => profileSkills.includes(s.name)) : this.summaries;
908
- if (candidates.length === 0) return null;
909
- const matched = await this.router.selectSkill(action, candidates);
910
- if (matched) {
911
- console.log(`[skill-resolver] Matched skill "${matched.name}" for action: "${action.slice(0, 80)}..."`);
912
- } else {
913
- console.log(`[skill-resolver] No match for action: "${action.slice(0, 80)}..."`);
914
- }
915
- if (!matched) return null;
916
- try {
917
- const skill = await loadSkillFromFile(matched.path);
918
- console.log(`[skill-resolver] Loaded skill "${matched.name}" (${skill.instructions?.length ?? 0} chars) path: ${matched.path}`);
919
- if (!skill.instructions) return null;
920
- const subGuides = {};
921
- const linkPattern = /\[([^\]]+)\]\(([^)]+\.md)\)/g;
922
- let m;
923
- const dir = dirname(resolve(matched.path));
924
- while ((m = linkPattern.exec(skill.instructions)) !== null) {
925
- const file = m[2];
926
- if (subGuides[file]) continue;
927
- try {
928
- const content = await readFile(resolve(dir, file), "utf8");
929
- subGuides[file] = content;
930
- console.log(`[skill-resolver] Pre-read sub-guide "${file}" (${content.length} chars)`);
931
- } catch {
932
- }
933
- }
934
- return {
935
- name: matched.name,
936
- systemPrompt: skill.instructions,
937
- path: matched.path,
938
- ...Object.keys(subGuides).length > 0 ? { subGuides } : {}
939
- };
940
- } catch (err) {
941
- console.error(`[skill-resolver] Failed to load skill "${matched.name}":`, err);
942
- return null;
943
- }
944
- }
945
- };
946
- function extractToolNames(messages) {
947
- const names = /* @__PURE__ */ new Set();
948
- for (const msg of messages) {
949
- if (msg.role === "assistant" && msg.toolCalls) {
950
- for (const tc of msg.toolCalls) {
951
- names.add(tc.toolName);
952
- }
953
- }
954
- }
955
- return [...names];
956
- }
957
- function extractFilesRead(messages) {
958
- const paths = /* @__PURE__ */ new Set();
959
- for (const msg of messages) {
960
- if (msg.role === "assistant" && msg.toolCalls) {
961
- for (const tc of msg.toolCalls) {
962
- if (tc.toolName === "Read" && tc.args.path) {
963
- paths.add(String(tc.args.path));
964
- }
965
- }
966
- }
967
- }
968
- return [...paths];
969
- }
970
- function extractFilesModified(messages) {
971
- const paths = /* @__PURE__ */ new Set();
972
- for (const msg of messages) {
973
- if (msg.role === "assistant" && msg.toolCalls) {
974
- for (const tc of msg.toolCalls) {
975
- if ((tc.toolName === "Write" || tc.toolName === "Edit") && tc.args.path) {
976
- paths.add(String(tc.args.path));
977
- }
978
- }
979
- }
980
- }
981
- return [...paths];
982
- }
983
- function countSteps(messages) {
984
- return messages.filter((m) => m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0).length;
985
- }
986
- function extractFinalOutput(messages) {
987
- for (let i = messages.length - 1; i >= 0; i--) {
988
- const msg = messages[i];
989
- if (msg.role === "assistant" && (!msg.toolCalls || msg.toolCalls.length === 0)) {
990
- return getTextContent(msg.content).slice(0, 200);
991
- }
992
- }
993
- return "";
994
- }
995
- function templateSummary(threadAction, messages, success) {
996
- const toolNames = extractToolNames(messages);
997
- const filesRead = extractFilesRead(messages);
998
- const filesModified = extractFilesModified(messages);
999
- const finalOutput = extractFinalOutput(messages);
1000
- const steps = countSteps(messages);
1001
- const parts = [
1002
- `Action: ${threadAction}`,
1003
- `Status: ${success ? "success" : "failed"}`,
1004
- `Steps: ${steps}`
1005
- ];
1006
- if (toolNames.length > 0) {
1007
- parts.push(`Tools used: ${toolNames.join(", ")}`);
1008
- }
1009
- if (filesRead.length > 0) {
1010
- parts.push(`Files read: ${filesRead.slice(0, 10).join(", ")}${filesRead.length > 10 ? ` (+${filesRead.length - 10} more)` : ""}`);
1011
- }
1012
- if (filesModified.length > 0) {
1013
- parts.push(`Files modified: ${filesModified.join(", ")}`);
1014
- }
1015
- if (finalOutput) {
1016
- parts.push(`Result: ${finalOutput}`);
1017
- }
1018
- return parts.join("\n");
1019
- }
1020
- function extractArtifacts(messages) {
1021
- const artifacts = [];
1022
- for (const msg of messages) {
1023
- if (msg.role === "tool" && msg.toolResults) {
1024
- for (const tr of msg.toolResults) {
1025
- if (tr.isError) {
1026
- artifacts.push({
1027
- key: `error_${tr.toolName}`,
1028
- content: tr.result.slice(0, 2e3)
1029
- });
1030
- }
1031
- }
1032
- }
1033
- }
1034
- for (const msg of messages) {
1035
- if (msg.role === "assistant" && msg.toolCalls) {
1036
- for (const tc of msg.toolCalls) {
1037
- if ((tc.toolName === "Write" || tc.toolName === "Edit") && tc.args.path) {
1038
- const filename = String(tc.args.path).split("/").pop() ?? String(tc.args.path);
1039
- const preview = tc.toolName === "Edit" ? `old: ${String(tc.args.old_text ?? "").slice(0, 200)}
1040
- new: ${String(tc.args.new_text ?? "").slice(0, 200)}` : String(tc.args.content ?? "").slice(0, 400);
1041
- artifacts.push({
1042
- key: `edit_${filename}`,
1043
- content: `${tc.args.path}
1044
- ${preview}`
1045
- });
1046
- }
1047
- }
1048
- }
1049
- }
1050
- const finalOutput = extractFinalOutput(messages);
1051
- if (finalOutput) {
1052
- artifacts.push({
1053
- key: "final_output",
1054
- content: finalOutput
1055
- });
1056
- }
1057
- return artifacts;
1058
- }
1059
- var EpisodeCompressor = class {
1060
- createModel;
1061
- constructor(createModel) {
1062
- this.createModel = createModel ?? anthropic;
1063
- }
1064
- compress(input) {
1065
- const now = Date.now();
1066
- const id = randomUUID();
1067
- const episode = {
1068
- id,
1069
- taskId: input.taskId,
1070
- sessionId: input.sessionId,
1071
- index: input.index,
1072
- threadAction: input.threadAction,
1073
- summary: templateSummary(input.threadAction, input.messages, input.success),
1074
- toolCalls: extractToolNames(input.messages),
1075
- filesRead: extractFilesRead(input.messages),
1076
- filesModified: extractFilesModified(input.messages),
1077
- model: input.model,
1078
- steps: countSteps(input.messages),
1079
- success: input.success,
1080
- createdAt: now,
1081
- parentEpisodeIds: input.parentEpisodeIds
1082
- };
1083
- const trace = {
1084
- episodeId: id,
1085
- messages: input.messages,
1086
- createdAt: now
1087
- };
1088
- const artifacts = extractArtifacts(input.messages);
1089
- return { episode, trace, artifacts };
1090
- }
1091
- async compressLLM(input, signal) {
1092
- const result = this.compress(input);
1093
- if (countSteps(input.messages) <= 10) {
1094
- return result;
1095
- }
1096
- try {
1097
- const conversationText = input.messages.map((m) => {
1098
- const text = typeof m.content === "string" ? m.content : getTextContent(m.content);
1099
- if (m.role === "assistant" && m.toolCalls?.length) {
1100
- const calls = m.toolCalls.map((tc) => `${tc.toolName}(${JSON.stringify(tc.args).slice(0, 100)})`).join(", ");
1101
- return `[assistant] ${text}
1102
- tools: ${calls}`;
1103
- }
1104
- if (m.role === "tool" && m.toolResults?.length) {
1105
- const results = m.toolResults.map((tr) => `${tr.toolName}: ${tr.result.slice(0, 200)}`).join("; ");
1106
- return `[tool] ${results}`;
1107
- }
1108
- return `[${m.role}] ${text.slice(0, 300)}`;
1109
- }).join("\n");
1110
- const fastModel = resolveModel("fast", DEFAULT_MODEL_MAP, DEFAULT_MODEL_MAP.fast);
1111
- const llmResult = await generateText({
1112
- model: this.createModel(fastModel),
1113
- system: "Summarize this agent conversation into a concise episode summary. Focus on: what was attempted, what tools were used, what files were changed, and the outcome. Keep it under 300 words.",
1114
- messages: [{ role: "user", content: conversationText.slice(0, 8e3) }],
1115
- abortSignal: signal
1116
- });
1117
- if (llmResult.text) {
1118
- result.episode.summary = llmResult.text;
1119
- }
1120
- } catch {
1121
- }
1122
- return result;
1123
- }
1124
- };
1125
-
1126
- // src/arc/utils.ts
1127
- function normalizeTools(tools) {
1128
- const out = {};
1129
- for (const [name, t] of Object.entries(tools)) {
1130
- if (t.parameters && !t.inputSchema) {
1131
- out[name] = { ...t, inputSchema: t.parameters };
1132
- } else {
1133
- out[name] = t;
1134
- }
1135
- }
1136
- return out;
1137
- }
1138
- function pickDefined(obj, keys) {
1139
- const result = {};
1140
- for (const key of keys) {
1141
- if (obj[key] != null) {
1142
- result[key] = obj[key];
1143
- }
1144
- }
1145
- return result;
1146
- }
1147
-
1148
- // src/arc/agent-runner.ts
1149
- function createChannel() {
1150
- const buffer = [];
1151
- let closed = false;
1152
- let resolve = null;
1153
- return {
1154
- push(value) {
1155
- if (closed) return;
1156
- buffer.push(value);
1157
- if (resolve) {
1158
- const r = resolve;
1159
- resolve = null;
1160
- r();
1161
- }
1162
- },
1163
- close() {
1164
- closed = true;
1165
- if (resolve) {
1166
- const r = resolve;
1167
- resolve = null;
1168
- r();
1169
- }
1170
- },
1171
- async *[Symbol.asyncIterator]() {
1172
- let index = 0;
1173
- while (true) {
1174
- while (index < buffer.length) {
1175
- yield buffer[index];
1176
- index++;
1177
- }
1178
- if (closed) return;
1179
- await new Promise((r) => {
1180
- resolve = r;
1181
- });
1182
- }
1183
- }
1184
- };
1185
- }
1186
- var DEFAULT_PAGE_THRESHOLD = 4e3;
1187
- var READ_FULL_RESULT_HARD_CAP = 32e3;
1188
- function extractStepUsage(usage) {
1189
- if (!usage) return void 0;
1190
- const u = {};
1191
- if (usage.inputTokens != null) u.inputTokens = usage.inputTokens;
1192
- if (usage.outputTokens != null) u.outputTokens = usage.outputTokens;
1193
- const inputDetails = usage.inputTokenDetails ?? usage;
1194
- const outputDetails = usage.outputTokenDetails ?? usage;
1195
- if (inputDetails.cacheReadTokens != null) u.cacheReadTokens = inputDetails.cacheReadTokens;
1196
- if (inputDetails.cacheWriteTokens != null) u.cacheWriteTokens = inputDetails.cacheWriteTokens;
1197
- if (outputDetails.reasoningTokens != null) u.reasoningTokens = outputDetails.reasoningTokens;
1198
- return Object.keys(u).length > 0 ? u : void 0;
1199
- }
1200
- var PROCESS_SYSTEM_PROMPT = [
1201
- "You are a focused execution thread within a larger agent system.",
1202
- "Complete the assigned task using the available tools.",
1203
- "Be efficient \u2014 accomplish the objective with minimal steps.",
1204
- "If your context includes the user's original message or attachment metadata, use that information directly.",
1205
- "When done, provide a brief summary of what you accomplished."
1206
- ].join(" ");
1207
- async function executeBuiltinTool(provider, action, runtime) {
1208
- switch (action.name) {
1209
- case "Bash":
1210
- return provider.bash(String(action.args.command ?? ""), {
1211
- cwd: action.args.cwd,
1212
- timeout: action.args.timeout
1213
- });
1214
- case "Read":
1215
- return provider.readFile(String(action.args.path ?? ""));
1216
- case "Write":
1217
- return provider.writeFile(String(action.args.path ?? ""), String(action.args.content ?? ""));
1218
- case "Edit":
1219
- return provider.editFile(
1220
- String(action.args.path ?? ""),
1221
- String(action.args.old_text ?? ""),
1222
- String(action.args.new_text ?? "")
1223
- );
1224
- case "Glob":
1225
- return provider.glob(String(action.args.pattern ?? ""));
1226
- case "Grep":
1227
- return provider.grep(String(action.args.pattern ?? ""), action.args.path);
1228
- case "WebFetch":
1229
- if (!provider.webFetch) return { success: false, output: "", error: "WebFetch unavailable" };
1230
- return provider.webFetch({
1231
- url: String(action.args.url ?? ""),
1232
- selector: action.args.selector,
1233
- maxContentLength: action.args.maxContentLength
1234
- });
1235
- case "WebSearch":
1236
- if (!provider.webSearch) return { success: false, output: "", error: "WebSearch unavailable" };
1237
- return provider.webSearch(String(action.args.query ?? ""));
1238
- case "AskUser":
1239
- if (!runtime.askUser) return { success: false, output: "", error: "AskUser unavailable" };
1240
- return { success: true, output: await runtime.askUser(String(action.args.question ?? ""), Array.isArray(action.args.options) ? action.args.options.map(String) : void 0) };
1241
- case "TellUser":
1242
- if (!runtime.tellUser) return { success: false, output: "", error: "TellUser unavailable" };
1243
- await runtime.tellUser(String(action.args.message ?? ""));
1244
- return { success: true, output: "ok" };
1245
- case "DownloadRawFile":
1246
- if (!runtime.downloadRawFile) return { success: false, output: "", error: "DownloadRawFile unavailable" };
1247
- return { success: true, output: await runtime.downloadRawFile(String(action.args.path ?? "")) };
1248
- default:
1249
- return { success: false, output: "", error: `Unknown tool: ${action.name}` };
1250
- }
1251
- }
1252
- async function executeTool(action, provider, runtime) {
1253
- if (runtime.hookRunner) {
1254
- const pre = await runtime.hookRunner.run({ event: "PreToolUse", toolName: action.name, input: action.args });
1255
- if (!pre.allow) {
1256
- return { success: false, output: "", error: pre.reason ?? "blocked by pre-hook" };
1257
- }
1258
- }
1259
- if (runtime.permissionManager) {
1260
- const permission = await runtime.permissionManager.check({ toolName: action.name, input: action.args });
1261
- if (!permission.allow) {
1262
- return { success: false, output: "", error: permission.reason ?? "blocked by permission manager" };
1263
- }
1264
- }
1265
- if (runtime.executeToolAction) {
1266
- const result2 = await runtime.executeToolAction(action);
1267
- if (result2) {
1268
- if (runtime.hookRunner) {
1269
- await runtime.hookRunner.run({ event: "PostToolUse", toolName: action.name, input: action.args, output: result2 });
1270
- }
1271
- return result2;
1272
- }
1273
- }
1274
- const result = await executeBuiltinTool(provider, action, runtime);
1275
- if (runtime.hookRunner) {
1276
- await runtime.hookRunner.run({ event: "PostToolUse", toolName: action.name, input: action.args, output: result });
1277
- }
1278
- return result;
1279
- }
1280
- var AgentRunner = class {
1281
- async run(config) {
1282
- const stream = this.stream(config);
1283
- while (true) {
1284
- const next = await stream.next();
1285
- if (next.done) return next.value;
1286
- }
1287
- }
1288
- async *stream(config) {
1289
- const messages = [
1290
- ...config.seed ?? [],
1291
- { role: "user", content: config.prompt }
1292
- ];
1293
- const systemContent = config.contextFacts?.length ? config.systemPrompt + "\n\n## Known Facts\n" + config.contextFacts.map((f) => `- ${f}`).join("\n") : config.systemPrompt;
1294
- const cachedSystem = [{
1295
- role: "system",
1296
- content: systemContent
1297
- }];
1298
- const pageThreshold = config.resultPageThreshold ?? DEFAULT_PAGE_THRESHOLD;
1299
- const pagingExcludeSet = new Set(config.pagingExclude ?? []);
1300
- const effectiveTools = config.resultPager ? {
1301
- ...config.tools,
1302
- ReadFullResult: tool({
1303
- description: "Retrieve the full content of a paged tool result. Use when the summary is insufficient and you need the complete data.",
1304
- inputSchema: z.object({
1305
- ref: z.string().describe("The paged result reference from a previous tool output"),
1306
- lineRange: z.object({
1307
- start: z.number().int().min(1).describe("Start line (1-indexed, inclusive)"),
1308
- end: z.number().int().min(1).describe("End line (1-indexed, inclusive)")
1309
- }).optional().describe("Optional line range to retrieve. Omit for full content.")
1310
- })
1311
- })
1312
- } : config.tools;
1313
- for (let step = 0; step < config.maxSteps; step++) {
1314
- config.signal.throwIfAborted();
1315
- yield { type: "step_start", step: step + 1 };
1316
- if (config.inbox) {
1317
- for await (const msg of drainNonBlocking(config.inbox)) {
1318
- messages.push(msg);
1319
- }
1320
- }
1321
- if (config.maxContextTokens && step > 0) {
1322
- trimContext(messages, config.maxContextTokens);
1323
- }
1324
- const result = streamText({
1325
- model: (config.createModel ?? anthropic)(config.model),
1326
- tools: normalizeTools(effectiveTools),
1327
- toolChoice: resolveToolChoice(config.toolChoice, step),
1328
- messages: toModelMessages(messages),
1329
- system: cachedSystem,
1330
- abortSignal: config.signal,
1331
- stopWhen: stepCountIs(1)
1332
- });
1333
- const toolCalls = [];
1334
- let finalText = "";
1335
- for await (const part of result.fullStream) {
1336
- if (part.type === "text-delta") {
1337
- finalText += part.text;
1338
- yield { type: "text_delta", text: part.text };
1339
- }
1340
- if (part.type === "tool-call") {
1341
- const raw = part;
1342
- toolCalls.push({
1343
- toolName: raw.toolName,
1344
- input: raw.args ?? raw.input ?? {},
1345
- ...raw.toolCallId != null ? { toolCallId: raw.toolCallId } : {},
1346
- ...raw.providerMetadata || raw.experimental_providerMetadata ? { providerMetadata: raw.providerMetadata ?? raw.experimental_providerMetadata } : {}
1347
- });
1348
- }
1349
- }
1350
- let usage;
1351
- try {
1352
- usage = extractStepUsage(await result.usage);
1353
- } catch {
1354
- usage = void 0;
1355
- }
1356
- if (toolCalls.length === 0) {
1357
- const rawText = finalText.trim();
1358
- if (!rawText && step === 0) {
1359
- const text2 = "ERROR: LLM returned empty response with no tool calls on first step. This may indicate an API billing issue, authentication error, or rate limit.";
1360
- messages.push({ role: "assistant", content: text2 });
1361
- yield usage ? { type: "step_end", step: step + 1, usage } : { type: "step_end", step: step + 1 };
1362
- return { messages, output: text2, steps: step + 1 };
1363
- }
1364
- const text = rawText || "Done.";
1365
- messages.push({ role: "assistant", content: text });
1366
- if (config.hookRunner) {
1367
- const decision = await config.hookRunner.run({
1368
- event: "RunComplete",
1369
- metadata: {
1370
- messages,
1371
- steps: step + 1,
1372
- output: text
1373
- }
1374
- });
1375
- if (!decision.allow) {
1376
- messages.push({
1377
- role: "user",
1378
- content: decision.reason ?? "Continue \u2014 a required post-completion step was not performed."
1379
- });
1380
- yield usage ? { type: "step_end", step: step + 1, usage } : { type: "step_end", step: step + 1 };
1381
- continue;
1382
- }
1383
- }
1384
- if (config.outputSchema) {
1385
- try {
1386
- const extractionMessages = [
1387
- ...messages,
1388
- { role: "user", content: "Extract the structured output from your response above." }
1389
- ];
1390
- const structured = await generateObject({
1391
- model: (config.createModel ?? anthropic)(config.model),
1392
- schema: config.outputSchema,
1393
- messages: toModelMessages(extractionMessages),
1394
- system: config.systemPrompt,
1395
- abortSignal: config.signal
1396
- });
1397
- yield usage ? { type: "step_end", step: step + 1, usage } : { type: "step_end", step: step + 1 };
1398
- return { messages, output: text, steps: step + 1, structuredOutput: structured.object };
1399
- } catch {
1400
- }
1401
- }
1402
- yield usage ? { type: "step_end", step: step + 1, usage } : { type: "step_end", step: step + 1 };
1403
- return { messages, output: text, steps: step + 1 };
1404
- }
1405
- const toolCallInfos = toolCalls.map((tc) => {
1406
- const info = {
1407
- toolCallId: tc.toolCallId ?? randomUUID(),
1408
- toolName: tc.toolName,
1409
- args: tc.input ?? {}
1410
- };
1411
- if (tc.providerMetadata) {
1412
- info.providerMetadata = tc.providerMetadata;
1413
- }
1414
- return info;
1415
- });
1416
- messages.push({
1417
- role: "assistant",
1418
- content: finalText || toolCalls.map((tc) => `${tc.toolName}(${JSON.stringify(tc.input ?? {}).slice(0, 100)})`).join(", "),
1419
- toolCalls: toolCallInfos
1420
- });
1421
- for (let index = 0; index < toolCallInfos.length; index += 1) {
1422
- const tc = toolCallInfos[index];
1423
- const action = {
1424
- type: "tool",
1425
- name: tc.toolName,
1426
- args: tc.args,
1427
- toolCallId: tc.toolCallId
1428
- };
1429
- config.onActivity?.({
1430
- type: "tool_start",
1431
- name: tc.toolName,
1432
- args: tc.args,
1433
- ts: Date.now()
1434
- });
1435
- yield { type: "tool_start", name: tc.toolName, args: tc.args, ...tc.toolCallId ? { toolCallId: tc.toolCallId } : {} };
1436
- if (tc.toolName === "ReadFullResult" && config.resultPager) {
1437
- const ref = String(tc.args.ref ?? "");
1438
- const content = await config.resultPager.retrieve(ref);
1439
- if (!content) {
1440
- const errorText = "ERROR: Content expired or not found. Use the summary above.";
1441
- messages.push({
1442
- role: "tool",
1443
- content: errorText,
1444
- toolResults: [{ toolCallId: tc.toolCallId, toolName: tc.toolName, result: errorText, isError: true }]
1445
- });
1446
- config.onActivity?.({
1447
- type: "tool_end",
1448
- name: tc.toolName,
1449
- ok: false,
1450
- ms: 0,
1451
- preview: "",
1452
- ts: Date.now()
1453
- });
1454
- yield { type: "tool_end", name: tc.toolName, result: { success: false, output: "", error: errorText } };
1455
- continue;
1456
- }
1457
- let output = content;
1458
- const lr = tc.args.lineRange;
1459
- if (lr && typeof lr === "object" && "start" in lr && "end" in lr) {
1460
- const start2 = Number(lr.start);
1461
- const end = Number(lr.end);
1462
- if (Number.isFinite(start2) && Number.isFinite(end) && start2 >= 1 && end >= start2) {
1463
- const lines = content.split("\n");
1464
- output = lines.slice(start2 - 1, end).join("\n");
1465
- }
1466
- }
1467
- if (output.length > READ_FULL_RESULT_HARD_CAP) {
1468
- output = output.slice(0, READ_FULL_RESULT_HARD_CAP) + `
1469
-
1470
- [Showing first ${READ_FULL_RESULT_HARD_CAP} of ${output.length} chars. Use lineRange for specific sections.]`;
1471
- }
1472
- messages.push({
1473
- role: "tool",
1474
- content: output,
1475
- toolResults: [{ toolCallId: tc.toolCallId, toolName: tc.toolName, result: output, isError: false }]
1476
- });
1477
- config.onActivity?.({
1478
- type: "tool_end",
1479
- name: tc.toolName,
1480
- ok: true,
1481
- ms: 0,
1482
- preview: output.slice(0, 200),
1483
- ts: Date.now()
1484
- });
1485
- yield { type: "tool_end", name: tc.toolName, result: { success: true, output } };
1486
- continue;
1487
- }
1488
- if (config.allowedToolNames && !config.allowedToolNames.includes(tc.toolName)) {
1489
- const resultText2 = `ERROR: Tool "${tc.toolName}" is not available in this profile.`;
1490
- messages.push({
1491
- role: "tool",
1492
- content: resultText2,
1493
- toolResults: [{
1494
- toolCallId: tc.toolCallId,
1495
- toolName: tc.toolName,
1496
- result: resultText2,
1497
- isError: true
1498
- }]
1499
- });
1500
- config.onActivity?.({
1501
- type: "tool_end",
1502
- name: tc.toolName,
1503
- ok: false,
1504
- ms: 0,
1505
- preview: "",
1506
- ts: Date.now()
1507
- });
1508
- yield { type: "tool_end", name: tc.toolName, result: { success: false, output: "", error: resultText2 } };
1509
- continue;
1510
- }
1511
- const start = Date.now();
1512
- let toolResult;
1513
- try {
1514
- toolResult = await executeTool(action, config.toolProvider, {
1515
- ...config.executeToolAction != null ? { executeToolAction: config.executeToolAction } : {},
1516
- ...config.hookRunner != null ? { hookRunner: config.hookRunner } : {},
1517
- ...config.permissionManager != null ? { permissionManager: config.permissionManager } : {},
1518
- ...config.askUser != null ? { askUser: config.askUser } : {},
1519
- ...config.tellUser != null ? { tellUser: config.tellUser } : {},
1520
- ...config.downloadRawFile != null ? { downloadRawFile: config.downloadRawFile } : {}
1521
- });
1522
- } catch (error) {
1523
- const errorMsg = error instanceof Error ? error.message : String(error);
1524
- toolResult = {
1525
- success: false,
1526
- output: "",
1527
- error: errorMsg.length > 500 ? errorMsg.slice(0, 500) + "..." : errorMsg
1528
- };
1529
- }
1530
- const durationMs = Date.now() - start;
1531
- let resultText = toolResult.success ? toolResult.output : `ERROR: ${toolResult.error ?? "unknown failure"}`;
1532
- if (config.resultPager && toolResult.success && resultText.length > pageThreshold && !pagingExcludeSet.has(tc.toolName) && tc.toolName !== "ReadFullResult") {
1533
- try {
1534
- const paged = await config.resultPager.page(resultText, {
1535
- toolName: tc.toolName,
1536
- toolCallId: tc.toolCallId
1537
- });
1538
- resultText = [
1539
- paged.summary,
1540
- "",
1541
- `[Full result: ${paged.originalLength} chars \u2014 call ReadFullResult("${paged.ref}") to retrieve]`
1542
- ].join("\n");
1543
- } catch {
1544
- resultText = resultText.slice(0, pageThreshold) + `
1545
-
1546
- [Truncated \u2014 ${resultText.length} chars total. Storage unavailable.]`;
1547
- }
1548
- }
1549
- if (config.maxToolResultLength && resultText.length > config.maxToolResultLength) {
1550
- const originalLength = resultText.length;
1551
- resultText = resultText.slice(0, config.maxToolResultLength) + `
1552
-
1553
- [Truncated \u2014 ${originalLength} chars total, showing first ${config.maxToolResultLength}.]`;
1554
- }
1555
- messages.push({
1556
- role: "tool",
1557
- content: resultText,
1558
- toolResults: [{
1559
- toolCallId: tc.toolCallId,
1560
- toolName: tc.toolName,
1561
- result: resultText,
1562
- isError: !toolResult.success
1563
- }]
1564
- });
1565
- config.onActivity?.({
1566
- type: "tool_end",
1567
- name: tc.toolName,
1568
- ok: toolResult.success,
1569
- ms: durationMs,
1570
- preview: toolResult.output.slice(0, 200),
1571
- ts: Date.now()
1572
- });
1573
- yield {
1574
- type: "tool_end",
1575
- name: tc.toolName,
1576
- result: {
1577
- success: toolResult.success,
1578
- output: toolResult.output,
1579
- ...toolResult.error != null ? { error: toolResult.error } : {}
1580
- }
1581
- };
1582
- }
1583
- yield usage ? { type: "step_end", step: step + 1, usage } : { type: "step_end", step: step + 1 };
1584
- }
1585
- return { messages, output: "max steps reached", steps: config.maxSteps };
1586
- }
1587
- };
1588
- function createProcess(request, config) {
1589
- const id = randomUUID();
1590
- const model = resolveModel(request.model, config.modelMap, config.defaultModel);
1591
- const maxSteps = request.model === "fast" ? 1 : request.maxSteps ?? config.processMaxSteps;
1592
- const inboxChannel = createChannel();
1593
- const ac = new AbortController();
1594
- const onParentAbort = () => ac.abort();
1595
- config.parentSignal.addEventListener("abort", onParentAbort, { once: true });
1596
- const outbox = createChannel();
1597
- const compressor = new EpisodeCompressor(config.createModel);
1598
- const runner = new AgentRunner();
1599
- const process2 = {
1600
- id,
1601
- action: request.action,
1602
- model,
1603
- status: "pending",
1604
- outbox,
1605
- inbox: {
1606
- send(message) {
1607
- inboxChannel.push(message);
1608
- },
1609
- cancel() {
1610
- ac.abort();
1611
- inboxChannel.close();
1612
- }
1613
- }
1614
- };
1615
- const seedPromise = buildSeedMessages(request.contextEpisodeIds ?? [], config.episodeStore);
1616
- const startTime = Date.now();
1617
- void (async () => {
1618
- try {
1619
- process2.status = "running";
1620
- const seed = [
1621
- ...config.demoMessages ?? [],
1622
- ...await seedPromise,
1623
- ...normalizeSeedContext(config.processSeedContext)
1624
- ];
1625
- let systemPrompt = config.processSystemPrompt ?? PROCESS_SYSTEM_PROMPT;
1626
- const skillRef = config.skillRefPromise ? await config.skillRefPromise : null;
1627
- if (skillRef) {
1628
- const subGuideBlocks = skillRef.subGuides ? Object.entries(skillRef.subGuides).map(([file, content]) => `
1629
- ### Sub-guide: ${file}
1630
-
1631
- ${content}`).join("\n") : "";
1632
- systemPrompt += `
1633
-
1634
- <skill_system>
1635
- **Skill: ${skillRef.name}**
1636
-
1637
- Follow these skill instructions precisely. Do NOT use alternative tools or libraries.
1638
-
1639
- ${skillRef.content ?? ""}
1640
- ${subGuideBlocks}
1641
- </skill_system>`;
1642
- } else if (config.skillPromptPromise) {
1643
- const skillInstructions = await config.skillPromptPromise;
1644
- if (skillInstructions) {
1645
- systemPrompt += "\n\n<skill_instructions>\nIMPORTANT: Follow the skill instructions below precisely. They contain tested, working patterns.\n\n" + skillInstructions + "\n</skill_instructions>";
1646
- }
1647
- }
1648
- const result = await Promise.race([
1649
- (async () => {
1650
- const stream = runner.stream({
1651
- model,
1652
- prompt: request.action,
1653
- tools: config.processTools,
1654
- systemPrompt,
1655
- toolProvider: config.toolProvider,
1656
- maxSteps,
1657
- signal: ac.signal,
1658
- seed,
1659
- inbox: inboxChannel,
1660
- ...pickDefined(config, [
1661
- "createModel",
1662
- "hookRunner",
1663
- "permissionManager",
1664
- "telemetry",
1665
- "executeToolAction",
1666
- "askUser",
1667
- "tellUser",
1668
- "downloadRawFile",
1669
- "allowedToolNames",
1670
- "outputSchema",
1671
- "toolChoice",
1672
- "resultPager",
1673
- "resultPageThreshold",
1674
- "pagingExclude",
1675
- "maxToolResultLength",
1676
- "contextFacts",
1677
- "maxContextTokens"
1678
- ])
1679
- });
1680
- while (true) {
1681
- const next = await stream.next();
1682
- if (next.done) {
1683
- return next.value;
1684
- }
1685
- const event = next.value;
1686
- if (event.type === "text_delta") {
1687
- outbox.push({ type: "text_delta", text: event.text });
1688
- continue;
1689
- }
1690
- if (event.type === "step_start") {
1691
- outbox.push({ type: "step_start", step: event.step });
1692
- continue;
1693
- }
1694
- if (event.type === "step_end") {
1695
- outbox.push({
1696
- type: "step_end",
1697
- step: event.step,
1698
- ...event.usage ? { usage: event.usage } : {}
1699
- });
1700
- continue;
1701
- }
1702
- if (event.type === "tool_start") {
1703
- outbox.push({
1704
- type: "activity",
1705
- activity: {
1706
- type: "tool_start",
1707
- name: event.name,
1708
- args: event.args,
1709
- ts: Date.now()
1710
- }
1711
- });
1712
- continue;
1713
- }
1714
- if (event.type === "tool_end") {
1715
- outbox.push({
1716
- type: "activity",
1717
- activity: {
1718
- type: "tool_end",
1719
- name: event.name,
1720
- ok: event.result.success,
1721
- ms: 0,
1722
- preview: String(event.result.output ?? "").slice(0, 200),
1723
- ts: Date.now()
1724
- }
1725
- });
1726
- }
1727
- }
1728
- })(),
1729
- timeoutPromise(config.processTimeout)
1730
- ]);
1731
- const durationMs = Date.now() - startTime;
1732
- const nextIndex = await getNextEpisodeIndex(config.episodeStore, config.taskId);
1733
- const compressInput = {
1734
- taskId: config.taskId,
1735
- sessionId: config.sessionId,
1736
- index: nextIndex,
1737
- threadAction: request.action,
1738
- messages: result.messages,
1739
- model,
1740
- parentEpisodeIds: request.contextEpisodeIds ?? [],
1741
- success: true
1742
- };
1743
- const { episode, trace, artifacts } = compressor.compress(compressInput);
1744
- if (result.structuredOutput) {
1745
- episode.structuredOutput = result.structuredOutput;
1746
- }
1747
- await config.episodeStore.addEpisode(episode);
1748
- await config.episodeStore.addTrace(trace);
1749
- const processResult = {
1750
- episode,
1751
- trace,
1752
- artifacts,
1753
- success: true,
1754
- durationMs,
1755
- resolvedModel: model
1756
- };
1757
- process2.result = processResult;
1758
- process2.status = "completed";
1759
- outbox.push({ type: "episode", episode });
1760
- outbox.push({ type: "done", result: processResult });
1761
- if (episode.steps > 10) {
1762
- const upgradeAc = new AbortController();
1763
- void compressor.compressLLM(compressInput, upgradeAc.signal).then(async (upgraded) => {
1764
- episode.summary = upgraded.episode.summary;
1765
- await config.episodeStore.addEpisode(episode);
1766
- }).catch(() => {
1767
- });
1768
- }
1769
- } catch (error) {
1770
- const durationMs = Date.now() - startTime;
1771
- const errorMessage = error instanceof Error ? error.message : String(error);
1772
- if (ac.signal.aborted) {
1773
- process2.status = "cancelled";
1774
- outbox.push({ type: "failed", error: "cancelled" });
1775
- outbox.close();
1776
- return;
1777
- }
1778
- const nextIndex = await getNextEpisodeIndex(config.episodeStore, config.taskId).catch(() => 0);
1779
- const { episode, trace, artifacts } = compressor.compress({
1780
- taskId: config.taskId,
1781
- sessionId: config.sessionId,
1782
- index: nextIndex,
1783
- threadAction: request.action,
1784
- messages: [
1785
- { role: "user", content: request.action },
1786
- { role: "assistant", content: `Failed: ${errorMessage}` }
1787
- ],
1788
- model,
1789
- parentEpisodeIds: request.contextEpisodeIds ?? [],
1790
- success: false
1791
- });
1792
- await config.episodeStore.addEpisode(episode).catch(() => {
1793
- });
1794
- await config.episodeStore.addTrace(trace).catch(() => {
1795
- });
1796
- const processResult = {
1797
- episode,
1798
- trace,
1799
- artifacts,
1800
- success: false,
1801
- durationMs,
1802
- resolvedModel: model,
1803
- error: errorMessage
1804
- };
1805
- process2.result = processResult;
1806
- process2.status = "failed";
1807
- outbox.push({ type: "failed", error: errorMessage });
1808
- } finally {
1809
- config.parentSignal.removeEventListener("abort", onParentAbort);
1810
- outbox.close();
1811
- inboxChannel.close();
1812
- }
1813
- })();
1814
- return process2;
1815
- }
1816
- var STUB_THRESHOLD = 500;
1817
- function trimContext(messages, maxTokens) {
1818
- let totalTokens = 0;
1819
- for (const m of messages) {
1820
- const text = typeof m.content === "string" ? m.content : "";
1821
- totalTokens += estimateTokens(text);
1822
- }
1823
- if (totalTokens <= maxTokens) return;
1824
- const hotBoundary = Math.floor(messages.length * 0.6);
1825
- for (let i = 0; i < hotBoundary; i++) {
1826
- const m = messages[i];
1827
- if (m.role === "tool" && typeof m.content === "string" && m.content.length > STUB_THRESHOLD) {
1828
- const toolName = m.toolResults?.[0]?.toolName ?? "tool";
1829
- const stubbed = `[${toolName}: output stubbed, ${m.content.length} chars]`;
1830
- m.content = stubbed;
1831
- if (m.toolResults) {
1832
- for (const tr of m.toolResults) {
1833
- tr.result = stubbed;
1834
- }
1835
- }
1836
- }
1837
- }
1838
- }
1839
- async function getNextEpisodeIndex(store, taskId) {
1840
- const episodes = await store.getEpisodesByTask(taskId);
1841
- return episodes.length;
1842
- }
1843
- async function buildSeedMessages(episodeIds, episodeStore) {
1844
- if (episodeIds.length === 0) return [];
1845
- const messages = [];
1846
- for (const id of episodeIds) {
1847
- const trace = await episodeStore.getTrace(id);
1848
- if (!trace) continue;
1849
- const traceText = trace.messages.map((m) => {
1850
- const text = getTextContent(m.content);
1851
- if (m.role === "assistant" && m.toolCalls?.length) {
1852
- const calls = m.toolCalls.map((tc) => ` ${tc.toolName}(${JSON.stringify(tc.args)})`).join("\n");
1853
- return `[assistant]
1854
- ${text}
1855
- [tool calls]
1856
- ${calls}`;
1857
- }
1858
- if (m.role === "tool" && m.toolResults?.length) {
1859
- const results = m.toolResults.map((tr) => {
1860
- const output = tr.result.length > 500 ? tr.result.slice(0, 500) + "..." : tr.result;
1861
- return ` ${tr.toolName}: ${tr.isError ? "ERROR: " : ""}${output}`;
1862
- }).join("\n");
1863
- return `[tool results]
1864
- ${results}`;
1865
- }
1866
- return `[${m.role}]
1867
- ${text}`;
1868
- }).join("\n\n");
1869
- messages.push({
1870
- role: "system",
1871
- content: `Context from prior episode (${id}):
1872
- ${traceText}`
1873
- });
1874
- }
1875
- return messages;
1876
- }
1877
- function normalizeSeedContext(ctx) {
1878
- if (!ctx) return [];
1879
- if (typeof ctx === "string") {
1880
- return [{ role: "system", content: ctx }];
1881
- }
1882
- return ctx;
1883
- }
1884
- function timeoutPromise(ms) {
1885
- return new Promise(
1886
- (_, reject) => setTimeout(() => reject(new Error(`Process timed out after ${ms}ms`)), ms)
1887
- );
1888
- }
1889
- async function* drainNonBlocking(iterable) {
1890
- const iterator = iterable[Symbol.asyncIterator]();
1891
- while (true) {
1892
- const next = await Promise.race([
1893
- iterator.next(),
1894
- Promise.resolve({ done: true, value: void 0 })
1895
- ]);
1896
- if (next.done) return;
1897
- yield next.value;
1898
- }
1899
- }
1900
- var FIELD_RE = /^(\w+)(?::(\w+)(\[\])?)(\?)?(?:\s*\(([^)]+)\))?$/;
1901
- function parseField(raw) {
1902
- const trimmed = raw.trim();
1903
- const match = trimmed.match(FIELD_RE);
1904
- if (!match) {
1905
- const name2 = trimmed.replace(/\?$/, "");
1906
- return {
1907
- name: name2,
1908
- type: "string",
1909
- isArray: false,
1910
- isOptional: trimmed.endsWith("?")
1911
- };
1912
- }
1913
- const [, name, typeStr, arrayMark, optionalMark, desc] = match;
1914
- const type = ["string", "number", "boolean"].includes(typeStr) ? typeStr : "string";
1915
- const field = {
1916
- name,
1917
- type,
1918
- isArray: arrayMark === "[]",
1919
- isOptional: optionalMark === "?"
1920
- };
1921
- if (desc) field.description = desc.trim();
1922
- return field;
1923
- }
1924
- function parseSignature(sig) {
1925
- const arrowIdx = sig.indexOf("->");
1926
- if (arrowIdx < 0) {
1927
- throw new Error(`Invalid signature: missing "->". Got: "${sig}"`);
1928
- }
1929
- const inputStr = sig.slice(0, arrowIdx).trim();
1930
- const outputStr = sig.slice(arrowIdx + 2).trim();
1931
- const inputs = inputStr.split(",").map((s) => s.trim()).filter(Boolean).map(parseField);
1932
- const outputs = outputStr.split(",").map((s) => s.trim()).filter(Boolean).map(parseField);
1933
- return { inputs, outputs };
1934
- }
1935
- function signatureToSchema(sig) {
1936
- const shape = {};
1937
- for (const field of sig.outputs) {
1938
- let base;
1939
- switch (field.type) {
1940
- case "number":
1941
- base = z.number();
1942
- break;
1943
- case "boolean":
1944
- base = z.boolean();
1945
- break;
1946
- default:
1947
- base = z.string();
1948
- break;
1949
- }
1950
- if (field.isArray) base = z.array(base);
1951
- if (field.description) base = base.describe(field.description);
1952
- if (field.isOptional) base = base.optional();
1953
- shape[field.name] = base;
1954
- }
1955
- return z.object(shape);
1956
- }
1957
- function isTypedSignature(sig) {
1958
- return /\b\w+:\w+/.test(sig);
1959
- }
1960
-
1961
- // src/arc/profile-builder.ts
1962
- function buildProfilePrompt(decl) {
1963
- const [input, output] = decl.signature.split("->").map((s) => s.trim());
1964
- const lines = [
1965
- `You are a ${decl.name} thread.`,
1966
- `Your task: take ${input} and produce ${output}.`,
1967
- "",
1968
- `## Available tools`,
1969
- `You have access to: ${decl.tools.join(", ")}`,
1970
- "Use ONLY these tools. You cannot use any other tools.",
1971
- "If you have Skill Instructions below, follow them for delivery method and style."
1972
- ];
1973
- if (decl.background) {
1974
- lines.push("", "## Background", decl.background);
1975
- }
1976
- return lines.join("\n");
1977
- }
1978
- function resolveProfileTools(toolNames, globalTools) {
1979
- const resolved = {};
1980
- for (const name of toolNames) {
1981
- if (globalTools[name]) {
1982
- resolved[name] = globalTools[name];
1983
- }
1984
- }
1985
- return resolved;
1986
- }
1987
- function buildProcessProfile(decl, globalTools) {
1988
- const profile = {
1989
- systemPrompt: buildProfilePrompt(decl),
1990
- tools: resolveProfileTools(decl.tools, globalTools),
1991
- allowedToolNames: decl.tools
1992
- };
1993
- if (decl.model) profile.model = decl.model;
1994
- if (decl.maxSteps) profile.maxSteps = decl.maxSteps;
1995
- if (isTypedSignature(decl.signature)) {
1996
- const parsed = parseSignature(decl.signature);
1997
- if (parsed.outputs.length > 0) {
1998
- profile.outputSchema = signatureToSchema(parsed);
1999
- }
2000
- }
2001
- if (decl.demos && decl.demos.length > 0) {
2002
- profile.demoMessages = decl.demos.flatMap((demo) => [
2003
- { role: "user", content: Object.entries(demo.input).map(([k, v]) => `${k}: ${v}`).join("\n") },
2004
- { role: "assistant", content: JSON.stringify(demo.output, null, 2) }
2005
- ]);
2006
- }
2007
- return profile;
2008
- }
2009
- function resolveProfile(config, globalTools) {
2010
- if (isProfileDeclaration(config)) {
2011
- return buildProcessProfile(config, globalTools);
2012
- }
2013
- if (config.tools && !config.allowedToolNames) {
2014
- return { ...config, allowedToolNames: Object.keys(config.tools) };
2015
- }
2016
- return config;
2017
- }
2018
-
2019
- // src/arc/process-manager.ts
2020
- function normalizeAction(action) {
2021
- return action.trim().replace(/\s+/g, " ");
2022
- }
2023
- var ProcessManager = class {
2024
- config;
2025
- processes = /* @__PURE__ */ new Map();
2026
- actionIndex = /* @__PURE__ */ new Map();
2027
- processListeners = [];
2028
- pendingEventResolvers = /* @__PURE__ */ new Set();
2029
- tracedRunning = /* @__PURE__ */ new Set();
2030
- pendingEvents = [];
2031
- cachedSessionMemoFacts = [];
2032
- constructor(config) {
2033
- this.config = config;
2034
- }
2035
- setSessionMemoFacts(facts) {
2036
- this.cachedSessionMemoFacts = facts ?? [];
2037
- }
2038
- get size() {
2039
- return this.processes.size;
2040
- }
2041
- get(id) {
2042
- return this.processes.get(id);
2043
- }
2044
- values() {
2045
- return this.processes.values();
2046
- }
2047
- entries() {
2048
- return this.processes.entries();
2049
- }
2050
- findByAction(action) {
2051
- const existingId = this.actionIndex.get(normalizeAction(action));
2052
- return existingId ? this.processes.get(existingId) : void 0;
2053
- }
2054
- dispatch(request, parentSignal) {
2055
- const { loopConfig } = this.config;
2056
- const profileConfig = request.profile ? loopConfig.processProfiles?.[request.profile] : void 0;
2057
- const globalTools = loopConfig.processTools ?? builtinTools;
2058
- const profile = profileConfig ? resolveProfile(profileConfig, globalTools) : void 0;
2059
- const defaultModel = this.config.modelMap[profile?.model ?? "medium"] ?? this.config.modelMap.medium;
2060
- const profileSkills = profileConfig && isProfileDeclaration(profileConfig) ? profileConfig.skills : void 0;
2061
- const skillRefPromise = this.config.skillResolver ? this.config.skillResolver.resolve(request.action, profileSkills).then((result) => {
2062
- if (!result) return null;
2063
- result.name;
2064
- result.path;
2065
- return {
2066
- name: result.name,
2067
- path: result.path,
2068
- ...result.systemPrompt ? { content: result.systemPrompt } : {},
2069
- ...result.subGuides ? { subGuides: result.subGuides } : {}
2070
- };
2071
- }) : void 0;
2072
- const skillPromptPromise = skillRefPromise?.then(() => null);
2073
- const threadContextFacts = [
2074
- ...loopConfig.contextFacts ?? [],
2075
- ...this.cachedSessionMemoFacts
2076
- ];
2077
- const proc = createProcess(request, {
2078
- toolProvider: loopConfig.toolProvider,
2079
- episodeStore: loopConfig.episodeStore,
2080
- taskId: loopConfig.taskId,
2081
- sessionId: loopConfig.sessionId,
2082
- createModel: this.config.createModel,
2083
- modelMap: this.config.modelMap,
2084
- defaultModel,
2085
- processMaxSteps: profile?.maxSteps ?? loopConfig.processMaxSteps ?? 20,
2086
- processTimeout: loopConfig.processTimeout ?? 12e4,
2087
- processTools: profile?.tools ?? globalTools,
2088
- ...profile?.systemPrompt ?? loopConfig.processSystemPrompt ? { processSystemPrompt: profile?.systemPrompt ?? loopConfig.processSystemPrompt } : {},
2089
- ...profile?.allowedToolNames ? { allowedToolNames: profile.allowedToolNames } : {},
2090
- ...profile?.outputSchema ? { outputSchema: profile.outputSchema } : {},
2091
- ...profile?.demoMessages ? { demoMessages: profile.demoMessages } : {},
2092
- ...loopConfig.processToolChoice ? { toolChoice: loopConfig.processToolChoice } : {},
2093
- ...loopConfig.resultPager ? { resultPager: loopConfig.resultPager } : {},
2094
- ...loopConfig.resultPageThreshold != null ? { resultPageThreshold: loopConfig.resultPageThreshold } : {},
2095
- ...loopConfig.pagingExclude ? { pagingExclude: loopConfig.pagingExclude } : {},
2096
- ...loopConfig.maxToolResultLength != null ? { maxToolResultLength: loopConfig.maxToolResultLength } : {},
2097
- contextFacts: threadContextFacts,
2098
- ...loopConfig.maxContextTokens != null ? { maxContextTokens: loopConfig.maxContextTokens } : {},
2099
- ...loopConfig.processSeedContext ? { processSeedContext: loopConfig.processSeedContext } : {},
2100
- ...skillPromptPromise ? { skillPromptPromise } : {},
2101
- ...skillRefPromise ? { skillRefPromise } : {},
2102
- parentSignal,
2103
- ...pickDefined(loopConfig, [
2104
- "hookRunner",
2105
- "permissionManager",
2106
- "telemetry",
2107
- "executeToolAction"
2108
- ])
2109
- });
2110
- this.processes.set(proc.id, proc);
2111
- this.actionIndex.set(normalizeAction(request.action), proc.id);
2112
- this.processListeners.push(this.listenToProcess(proc));
2113
- this.trace({ type: "process_created", id: proc.id, status: "pending" });
2114
- if (skillRefPromise) {
2115
- skillRefPromise.then((ref) => {
2116
- if (ref) {
2117
- this.enqueuePendingEvent({
2118
- type: "skill_resolved",
2119
- processId: proc.id,
2120
- skillName: ref.name,
2121
- skillPath: ref.path
2122
- });
2123
- }
2124
- }).catch(() => {
2125
- });
2126
- }
2127
- return proc;
2128
- }
2129
- cancel(id) {
2130
- const proc = this.processes.get(id);
2131
- if (!proc) return void 0;
2132
- proc.inbox.cancel();
2133
- this.trace({ type: "process_transition", id, from: proc.status, to: "cancelled" });
2134
- this.enqueuePendingEvent({ type: "process_cancelled", id });
2135
- return proc;
2136
- }
2137
- hasRunningProcesses() {
2138
- for (const proc of this.processes.values()) {
2139
- if (proc.status === "running" || proc.status === "pending") {
2140
- return true;
2141
- }
2142
- }
2143
- return false;
2144
- }
2145
- *drainPendingEvents() {
2146
- while (this.pendingEvents.length > 0) {
2147
- yield this.pendingEvents.shift();
2148
- }
2149
- }
2150
- async waitForPendingEvent(signal, timeoutMs) {
2151
- if (this.pendingEvents.length > 0) {
2152
- return;
2153
- }
2154
- await new Promise((resolve) => {
2155
- const onAbort = () => {
2156
- cleanup();
2157
- resolve();
2158
- };
2159
- const onEvent = () => {
2160
- cleanup();
2161
- resolve();
2162
- };
2163
- const timer = setTimeout(() => {
2164
- cleanup();
2165
- resolve();
2166
- }, timeoutMs);
2167
- const cleanup = () => {
2168
- clearTimeout(timer);
2169
- signal.removeEventListener("abort", onAbort);
2170
- this.pendingEventResolvers.delete(onEvent);
2171
- };
2172
- signal.addEventListener("abort", onAbort, { once: true });
2173
- this.pendingEventResolvers.add(onEvent);
2174
- });
2175
- }
2176
- async waitForProcessListeners() {
2177
- await Promise.allSettled(this.processListeners);
2178
- }
2179
- trace(kind) {
2180
- this.config.trace?.(kind);
2181
- }
2182
- traceProcessRunning(procId) {
2183
- if (!this.tracedRunning.has(procId)) {
2184
- this.tracedRunning.add(procId);
2185
- this.trace({ type: "process_transition", id: procId, from: "pending", to: "running" });
2186
- }
2187
- }
2188
- enqueuePendingEvent(event) {
2189
- this.pendingEvents.push(event);
2190
- for (const resolve of this.pendingEventResolvers) {
2191
- resolve();
2192
- }
2193
- this.pendingEventResolvers.clear();
2194
- }
2195
- async listenToProcess(proc) {
2196
- try {
2197
- for await (const event of proc.outbox) {
2198
- if (event.type === "activity") {
2199
- this.traceProcessRunning(proc.id);
2200
- this.enqueuePendingEvent({
2201
- type: "process_activity",
2202
- id: proc.id,
2203
- activity: event.activity
2204
- });
2205
- } else if (event.type === "text_delta") {
2206
- this.traceProcessRunning(proc.id);
2207
- this.enqueuePendingEvent({
2208
- type: "process_text_delta",
2209
- id: proc.id,
2210
- text: event.text
2211
- });
2212
- } else if (event.type === "step_start") {
2213
- this.traceProcessRunning(proc.id);
2214
- this.enqueuePendingEvent({
2215
- type: "process_step_start",
2216
- id: proc.id,
2217
- step: event.step
2218
- });
2219
- } else if (event.type === "step_end") {
2220
- this.traceProcessRunning(proc.id);
2221
- this.enqueuePendingEvent({
2222
- type: "process_step_end",
2223
- id: proc.id,
2224
- step: event.step,
2225
- ...event.usage ? { usage: event.usage } : {}
2226
- });
2227
- } else if (event.type === "done") {
2228
- this.traceProcessRunning(proc.id);
2229
- this.trace({ type: "process_transition", id: proc.id, from: "running", to: "completed" });
2230
- this.config.onEpisodeCompleted?.(event.result.episode);
2231
- this.enqueuePendingEvent({
2232
- type: "process_completed",
2233
- id: proc.id,
2234
- episodeId: event.result.episode.id,
2235
- summary: event.result.episode.summary,
2236
- durationMs: event.result.durationMs
2237
- });
2238
- } else if (event.type === "failed") {
2239
- this.traceProcessRunning(proc.id);
2240
- const toStatus = event.error === "cancelled" ? "cancelled" : "failed";
2241
- this.trace({ type: "process_transition", id: proc.id, from: "running", to: toStatus });
2242
- this.enqueuePendingEvent({
2243
- type: "process_failed",
2244
- id: proc.id,
2245
- error: event.error
2246
- });
2247
- }
2248
- }
2249
- } catch {
2250
- }
2251
- }
2252
- };
2253
- var OrchestratorTurnRunner = class {
2254
- config;
2255
- constructor(config) {
2256
- this.config = config;
2257
- }
2258
- async *runTurn(params) {
2259
- const { turn, maxTurns, prepared, signal, forcedFinalResponseRetry } = params;
2260
- this.config.trace({ type: "llm_call_start" });
2261
- const callLLM = async (effectiveSignal) => (
2262
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2263
- streamText({
2264
- model: this.config.createModel(this.config.orchestratorModel),
2265
- tools: normalizeTools(this.config.tools),
2266
- toolChoice: resolveToolChoice(this.config.orchestratorToolChoice, turn),
2267
- messages: toModelMessages(prepared.messages),
2268
- system: this.config.cachedSystem,
2269
- abortSignal: effectiveSignal
2270
- })
2271
- );
2272
- let stream;
2273
- if (this.config.resilience) {
2274
- const resilienceCtx = {
2275
- attempt: 0,
2276
- totalAttempts: 1,
2277
- startTime: Date.now(),
2278
- signal
2279
- };
2280
- stream = await this.config.resilience.execute(
2281
- (ctx) => callLLM(ctx.signal),
2282
- resilienceCtx
2283
- );
2284
- } else {
2285
- stream = await callLLM(signal);
2286
- }
2287
- const toolCalls = [];
2288
- let text = "";
2289
- for await (const part of stream.fullStream) {
2290
- if (part.type === "text-delta") {
2291
- text += part.text;
2292
- yield { type: "text_delta", text: part.text };
2293
- }
2294
- if (part.type === "tool-call") {
2295
- const p = part;
2296
- toolCalls.push({
2297
- toolName: p.toolName,
2298
- args: p.args ?? p.input ?? {},
2299
- toolCallId: p.toolCallId
2300
- });
2301
- }
2302
- }
2303
- const responseType = toolCalls.length === 0 ? "text_only" : toolCalls.length === 1 ? "single_tool" : "multiple_tools";
2304
- this.config.trace({ type: "llm_call_end", response_type: responseType });
2305
- if (toolCalls.length === 0) {
2306
- if (!text.trim()) {
2307
- if (!forcedFinalResponseRetry && turn < maxTurns - 1) {
2308
- const retryInstruction = "Respond to the user now using the completed thread results. Do not call tools or dispatch threads unless absolutely necessary. Give a direct final answer.";
2309
- this.config.ctx.recordTurn({
2310
- role: "user",
2311
- messages: [{ role: "user", content: retryInstruction }],
2312
- tokenEstimate: estimateTokens(retryInstruction),
2313
- timestamp: Date.now()
2314
- });
2315
- return { type: "retry_empty" };
2316
- }
2317
- text = "No final synthesis was produced.";
2318
- }
2319
- this.config.ctx.recordTurn({
2320
- role: "orchestrator",
2321
- messages: [{ role: "assistant", content: text }],
2322
- tokenEstimate: estimateTokens(text),
2323
- timestamp: Date.now()
2324
- });
2325
- return { type: "done", output: text };
2326
- }
2327
- const assistantMessages = this.buildAssistantMessages(text, toolCalls);
2328
- this.config.ctx.recordTurn({
2329
- role: "orchestrator",
2330
- messages: assistantMessages,
2331
- tokenEstimate: estimateTokens(text) + toolCalls.reduce((sum, call) => sum + estimateTokens(JSON.stringify(call.args)), 0),
2332
- timestamp: Date.now()
2333
- });
2334
- const toolResultMessages = [];
2335
- for (const call of toolCalls) {
2336
- const toolCallId = call.toolCallId ?? randomUUID();
2337
- this.config.trace({ type: "tool_call", tool: call.toolName });
2338
- if (call.toolName === "Thread") {
2339
- const request = this.toProcessRequest(call.args);
2340
- const hasProfiles = this.config.config.processProfiles && Object.keys(this.config.config.processProfiles).length > 0;
2341
- if (hasProfiles && !request.profile) {
2342
- const names = Object.keys(this.config.config.processProfiles).map((name) => `"${name}"`).join(", ");
2343
- const resultText2 = `ERROR: profile parameter is required. Available profiles: ${names}. Re-call Thread with a profile.`;
2344
- toolResultMessages.push({
2345
- role: "tool",
2346
- content: resultText2,
2347
- toolResults: [{ toolCallId, toolName: "Thread", result: resultText2 }]
2348
- });
2349
- yield { type: "thread_rejected", action: request.action, reason: "missing profile" };
2350
- continue;
2351
- }
2352
- if (hasProfiles && request.profile && !this.config.config.processProfiles[request.profile]) {
2353
- const names = Object.keys(this.config.config.processProfiles).map((name) => `"${name}"`).join(", ");
2354
- const resultText2 = `ERROR: unknown profile "${request.profile}". Available profiles: ${names}. Re-call Thread with a valid profile.`;
2355
- toolResultMessages.push({
2356
- role: "tool",
2357
- content: resultText2,
2358
- toolResults: [{ toolCallId, toolName: "Thread", result: resultText2 }]
2359
- });
2360
- yield { type: "thread_rejected", action: request.action, reason: `unknown profile "${request.profile}"` };
2361
- continue;
2362
- }
2363
- const existing = this.config.processManager.findByAction(request.action);
2364
- if (existing && (existing.status === "running" || existing.status === "pending")) {
2365
- const resultText2 = `DUPLICATE \u2014 thread already running for this action (process ${existing.id}). Wait for it to complete.`;
2366
- toolResultMessages.push({
2367
- role: "tool",
2368
- content: resultText2,
2369
- toolResults: [{ toolCallId, toolName: "Thread", result: resultText2 }]
2370
- });
2371
- yield { type: "thread_rejected", action: request.action, reason: "duplicate (running)" };
2372
- continue;
2373
- }
2374
- if (existing && existing.status === "completed" && existing.result) {
2375
- const ep = existing.result.episode;
2376
- const resultText2 = `DUPLICATE \u2014 this action already completed (process ${existing.id}, episodeId: ${ep.id}). Use contextEpisodeIds: ["${ep.id}"] to reference it.`;
2377
- toolResultMessages.push({
2378
- role: "tool",
2379
- content: resultText2,
2380
- toolResults: [{ toolCallId, toolName: "Thread", result: resultText2 }]
2381
- });
2382
- yield { type: "thread_rejected", action: request.action, reason: "duplicate (completed)" };
2383
- continue;
2384
- }
2385
- const completedEpisodeIds = [...this.config.processManager.values()].filter((proc2) => proc2.status === "completed" && proc2.result?.episode).map((proc2) => proc2.result.episode.id);
2386
- if (completedEpisodeIds.length > 0) {
2387
- request.contextEpisodeIds = [
2388
- ...request.contextEpisodeIds ?? [],
2389
- ...completedEpisodeIds.filter((id) => !request.contextEpisodeIds?.includes(id))
2390
- ];
2391
- }
2392
- const proc = this.config.processManager.dispatch(request, signal);
2393
- const resultText = `Process ${proc.id} dispatched: "${request.action}" (model: ${request.model ?? "medium"})`;
2394
- toolResultMessages.push({
2395
- role: "tool",
2396
- content: resultText,
2397
- toolResults: [{ toolCallId, toolName: "Thread", result: resultText }]
2398
- });
2399
- yield {
2400
- type: "process_dispatched",
2401
- id: proc.id,
2402
- action: request.action,
2403
- model: proc.model,
2404
- ...request.label ? { label: request.label } : {},
2405
- ...request.profile ? { profile: request.profile } : {}
2406
- };
2407
- continue;
2408
- }
2409
- if (call.toolName === "Check") {
2410
- const proc = this.config.processManager.get(String(call.args.id));
2411
- let resultText;
2412
- if (!proc) {
2413
- resultText = `Process not found: ${call.args.id}`;
2414
- } else if (proc.result) {
2415
- resultText = `Process ${proc.id} [${proc.status}] (episodeId: ${proc.result.episode.id}):
2416
- ${proc.result.episode.summary}`;
2417
- } else {
2418
- resultText = `Process ${proc.id}: status=${proc.status}`;
2419
- }
2420
- toolResultMessages.push({
2421
- role: "tool",
2422
- content: resultText,
2423
- toolResults: [{ toolCallId, toolName: "Check", result: resultText }]
2424
- });
2425
- continue;
2426
- }
2427
- if (call.toolName === "Cancel") {
2428
- const proc = this.config.processManager.cancel(String(call.args.id));
2429
- const resultText = proc ? `Process ${call.args.id} cancelled` : `Process not found: ${call.args.id}`;
2430
- toolResultMessages.push({
2431
- role: "tool",
2432
- content: resultText,
2433
- toolResults: [{ toolCallId, toolName: "Cancel", result: resultText }]
2434
- });
2435
- continue;
2436
- }
2437
- if (call.toolName === "ReadEpisode") {
2438
- const episodeId = String(call.args.id);
2439
- const trace = await this.config.config.episodeStore.getTrace(episodeId);
2440
- let resultText;
2441
- if (!trace) {
2442
- resultText = `Trace not found for episode: ${episodeId}`;
2443
- } else {
2444
- resultText = trace.messages.map((message) => {
2445
- const textContent = getTextContent(message.content);
2446
- if (message.role === "assistant" && message.toolCalls?.length) {
2447
- const calls = message.toolCalls.map((toolCall) => ` ${toolCall.toolName}(${JSON.stringify(toolCall.args)})`).join("\n");
2448
- return `[assistant]
2449
- ${textContent}
2450
- [tool calls]
2451
- ${calls}`;
2452
- }
2453
- if (message.role === "tool" && message.toolResults?.length) {
2454
- const results = message.toolResults.map((toolResult) => {
2455
- const output = toolResult.result.length > 500 ? toolResult.result.slice(0, 500) + "..." : toolResult.result;
2456
- return ` ${toolResult.toolName}: ${toolResult.isError ? "ERROR: " : ""}${output}`;
2457
- }).join("\n");
2458
- return `[tool results]
2459
- ${results}`;
2460
- }
2461
- return `[${message.role}]
2462
- ${textContent}`;
2463
- }).join("\n\n");
2464
- const proc = [...this.config.processManager.values()].find((process2) => process2.result?.episode.id === episodeId);
2465
- if (proc?.result && proc.result.artifacts.length > 0) {
2466
- resultText += "\n\n--- Artifacts ---\n";
2467
- resultText += proc.result.artifacts.map((artifact) => `[${artifact.key}]
2468
- ${artifact.content}`).join("\n\n");
2469
- }
2470
- if (typeof call.args.maxTokens === "number" && call.args.maxTokens > 0) {
2471
- const maxChars = call.args.maxTokens * 4;
2472
- if (resultText.length > maxChars) {
2473
- resultText = resultText.slice(0, maxChars) + "\n... [truncated]";
2474
- }
2475
- }
2476
- }
2477
- toolResultMessages.push({
2478
- role: "tool",
2479
- content: resultText,
2480
- toolResults: [{ toolCallId, toolName: "ReadEpisode", result: resultText }]
2481
- });
2482
- continue;
2483
- }
2484
- if (call.toolName === "Remember") {
2485
- const memOpts = {};
2486
- if (call.args.category != null) memOpts.category = String(call.args.category);
2487
- const memId = await this.config.memory.store(String(call.args.content), memOpts);
2488
- yield { type: "memory_stored", id: memId, content: String(call.args.content) };
2489
- const resultText = `Stored to memory (id: ${memId})`;
2490
- toolResultMessages.push({
2491
- role: "tool",
2492
- content: resultText,
2493
- toolResults: [{ toolCallId, toolName: "Remember", result: resultText }]
2494
- });
2495
- continue;
2496
- }
2497
- if (this.config.config.onOrchestratorTool) {
2498
- const action = await this.config.config.onOrchestratorTool(call.toolName, call.args);
2499
- const resultText = this.toToolContent(action, call.toolName);
2500
- toolResultMessages.push({
2501
- role: "tool",
2502
- content: resultText,
2503
- toolResults: [{ toolCallId, toolName: call.toolName, result: resultText }]
2504
- });
2505
- }
2506
- }
2507
- if (toolResultMessages.length > 0) {
2508
- this.config.ctx.recordTurn({
2509
- role: "process_result",
2510
- messages: toolResultMessages,
2511
- tokenEstimate: toolResultMessages.reduce((sum, message) => {
2512
- const textContent = typeof message.content === "string" ? message.content : getTextContent(message.content);
2513
- return sum + estimateTokens(textContent);
2514
- }, 0),
2515
- timestamp: Date.now()
2516
- });
2517
- }
2518
- return { type: "continue" };
2519
- }
2520
- toProcessRequest(args) {
2521
- const req = { action: String(args.action ?? "") };
2522
- if (Array.isArray(args.contextEpisodeIds)) {
2523
- req.contextEpisodeIds = args.contextEpisodeIds.map(String);
2524
- }
2525
- if (args.model != null) {
2526
- req.model = String(args.model);
2527
- }
2528
- if (typeof args.maxSteps === "number") {
2529
- req.maxSteps = args.maxSteps;
2530
- }
2531
- if (typeof args.label === "string") {
2532
- req.label = args.label;
2533
- }
2534
- if (typeof args.profile === "string") {
2535
- req.profile = args.profile;
2536
- }
2537
- return req;
2538
- }
2539
- buildAssistantMessages(text, toolCalls) {
2540
- const content = text || toolCalls.map((call) => `${call.toolName}: ${JSON.stringify(call.args).slice(0, 100)}`).join("\n");
2541
- return [{
2542
- role: "assistant",
2543
- content,
2544
- toolCalls: toolCalls.map((call) => ({
2545
- toolCallId: call.toolCallId ?? randomUUID(),
2546
- toolName: call.toolName,
2547
- args: call.args
2548
- }))
2549
- }];
2550
- }
2551
- toToolContent(action, toolName) {
2552
- return action && "content" in action && typeof action.content === "string" ? action.content : `${toolName}: handled`;
2553
- }
2554
- };
2555
-
2556
- // src/arc/completion-policy.ts
2557
- var CompletionPolicy = class {
2558
- config;
2559
- reportedCompletions = /* @__PURE__ */ new Set();
2560
- constructor(config) {
2561
- this.config = config;
2562
- }
2563
- async *settleTurn(signal) {
2564
- yield* this.waitForRunningProcesses(signal);
2565
- this.recordCompletionMessages();
2566
- }
2567
- async *waitForRunningProcesses(signal) {
2568
- const active = [...this.config.processManager.values()].filter(
2569
- (process2) => process2.status === "running" || process2.status === "pending"
2570
- );
2571
- if (active.length === 0) return;
2572
- const deadline = Date.now() + (this.config.processTimeout ?? 12e4);
2573
- while (active.some((process2) => process2.status === "running" || process2.status === "pending") && Date.now() <= deadline) {
2574
- await this.config.processManager.waitForPendingEvent(signal, 400);
2575
- yield* this.config.processManager.drainPendingEvents();
2576
- }
2577
- }
2578
- recordCompletionMessages() {
2579
- const completionMessages = [];
2580
- for (const [id, process2] of this.config.processManager.entries()) {
2581
- if (!process2.result || this.reportedCompletions.has(id)) continue;
2582
- this.reportedCompletions.add(id);
2583
- const episode = process2.result.episode;
2584
- const status = process2.result.success ? "success" : `failed: ${process2.result.error ?? "unknown"}`;
2585
- const deliveredFile = process2.result.trace?.messages?.some(
2586
- (message) => message.role === "tool" && message.toolResults?.some(
2587
- (toolResult) => toolResult.toolName === "DownloadRawFile" && !toolResult.isError
2588
- )
2589
- );
2590
- const hints = [
2591
- `To give a dependent thread the full data from this thread, pass contextEpisodeIds: ["${episode.id}"]`
2592
- ];
2593
- if (deliveredFile) {
2594
- hints.push("File was delivered to the user. Do NOT recreate or redo this work \u2014 synthesize the result and respond.");
2595
- }
2596
- completionMessages.push({
2597
- role: "user",
2598
- content: `[Thread ${id} completed] (episodeId: ${episode.id}) \u2014 ${status}
2599
- ${episode.summary}
2600
-
2601
- ${hints.join("\n")}`
2602
- });
2603
- }
2604
- if (completionMessages.length === 0) return;
2605
- this.config.ctx.recordTurn({
2606
- role: "process_result",
2607
- messages: completionMessages,
2608
- tokenEstimate: completionMessages.reduce((sum, message) => {
2609
- const text = typeof message.content === "string" ? message.content : getTextContent(message.content);
2610
- return sum + estimateTokens(text);
2611
- }, 0),
2612
- timestamp: Date.now()
2613
- });
2614
- }
2615
- };
2616
-
2617
- // src/arc/arc-loop.ts
2618
- var DEFAULT_ORCHESTRATOR_PROMPT = `You are an orchestrator agent. You accomplish tasks by decomposing them into focused processes, delegating each to a Thread, and synthesizing the results.
2619
-
2620
- ## Workflow: Decompose \u2192 Delegate \u2192 Synthesize
2621
-
2622
- 1. **Decompose**: Break the user's request into independent, focused sub-tasks.
2623
- 2. **Delegate**: Dispatch each sub-task as a Thread call. Independent sub-tasks go in the SAME turn (parallel).
2624
- 3. **Synthesize**: Once threads complete, combine their results into a coherent response.
2625
-
2626
- ## Tools
2627
-
2628
- - **Thread**: Dispatch a background process. Returns immediately. Multiple per turn = parallel.
2629
- - **Check**: Check status or results of a process by ID.
2630
- - **Cancel**: Cancel a running process.
2631
- - **Remember**: Save information to persistent memory.
2632
- - **ReadEpisode**: Retrieve the full trace of a completed episode (detailed tool outputs, file contents, errors).
2633
-
2634
- ## When to dispatch threads
2635
-
2636
- Dispatch when the task requires tool use (search, code, file I/O, web access). Examples:
2637
- - "Research X and Y" \u2192 2 parallel research threads
2638
- - "Read file A, then modify it" \u2192 1 read thread, then 1 write thread with contextEpisodeIds
2639
- - "Create a report with data from 3 sources" \u2192 3 parallel data threads, then 1 synthesis thread
2640
-
2641
- ## When NOT to dispatch
2642
-
2643
- Do NOT dispatch threads for tasks you can answer directly:
2644
- - Simple factual questions from your knowledge
2645
- - Clarifying the user's request (ask directly instead)
2646
- - Summarizing results already in your context
2647
-
2648
- ## Clarification
2649
-
2650
- If the request is ambiguous, ask the user before dispatching. It is better to ask one clarifying question than to dispatch threads that do the wrong thing.
2651
-
2652
- ## Model selection
2653
-
2654
- - **"fast"** \u2014 1 step only: a single search, read, or check. Use parallel fast threads for multiple lookups.
2655
- - **"medium"** (default) \u2014 implementation, writing code, running tests, standard tasks.
2656
- - **"strong"** \u2014 complex refactoring, debugging subtle issues, architectural decisions, multi-file changes.
2657
-
2658
- ## Concurrency
2659
-
2660
- Dispatch at most 3 threads per turn. If more are needed, dispatch the first batch and wait for results before the next.
2661
-
2662
- ## Context passing
2663
-
2664
- Threads automatically receive the user's current message and any attachment metadata as seed context. You do NOT need to copy-paste URLs, file contents, or pasted data into the action text \u2014 threads can read this directly from their context.
2665
-
2666
- Use contextEpisodeIds to chain dependent threads: research threads first (parallel), then implementation threads with their episodeIds.
2667
-
2668
- ## Completion
2669
-
2670
- When the task is fully complete, respond with a text summary (no Thread call). Include key findings, actions taken, and any follow-up recommendations.`;
2671
- var ArcLoop = class {
2672
- config;
2673
- ctx;
2674
- memory;
2675
- modelMap;
2676
- orchestratorModel;
2677
- systemPrompt;
2678
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2679
- cachedSystem;
2680
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2681
- tools;
2682
- resilience;
2683
- traceWriter;
2684
- skillResolver;
2685
- createModel;
2686
- processManager;
2687
- turnRunner;
2688
- completionPolicy;
2689
- /** Cached session memo facts — loaded once at stream start, injected into all threads. */
2690
- cachedSessionMemoFacts;
2691
- constructor(config) {
2692
- this.config = config;
2693
- this.createModel = config.createModel ?? (config.apiKey ? createAnthropic({ apiKey: config.apiKey }) : anthropic);
2694
- this.modelMap = { ...DEFAULT_MODEL_MAP, ...config.modelMap };
2695
- this.orchestratorModel = resolveModel(config.model, this.modelMap, this.modelMap.strong);
2696
- this.systemPrompt = config.systemPrompt ?? DEFAULT_ORCHESTRATOR_PROMPT;
2697
- this.cachedSystem = [{
2698
- role: "system",
2699
- content: this.systemPrompt
2700
- }];
2701
- this.tools = {
2702
- ...orchestratorTools,
2703
- ...config.extraOrchestratorTools
2704
- };
2705
- this.memory = new MemoryManager({
2706
- sessionMemoStore: config.sessionMemoStore,
2707
- longTermStore: config.longTermStore,
2708
- sessionId: config.sessionId
2709
- });
2710
- this.ctx = new ContextWindow({
2711
- contextWindowSize: config.contextWindowSize ?? 2e5,
2712
- outputReserve: config.outputReserve ?? 2e4,
2713
- systemPrompt: this.systemPrompt,
2714
- episodeStore: config.episodeStore,
2715
- memory: this.memory,
2716
- taskId: config.taskId,
2717
- createModel: this.createModel,
2718
- modelId: this.orchestratorModel,
2719
- dynamicContextWindow: config.dynamicContextWindow ?? false
2720
- });
2721
- this.resilience = config.resilience;
2722
- this.traceWriter = config.traceWriter;
2723
- this.skillResolver = config.skillResolver ?? (config.skillIndexPath ? new DefaultSkillResolver({
2724
- skillIndexPath: config.skillIndexPath,
2725
- routerConfig: {
2726
- createModel: this.createModel,
2727
- model: this.modelMap.fast
2728
- }
2729
- }) : void 0);
2730
- this.processManager = new ProcessManager({
2731
- loopConfig: config,
2732
- createModel: this.createModel,
2733
- modelMap: this.modelMap,
2734
- ...this.skillResolver ? { skillResolver: this.skillResolver } : {},
2735
- onEpisodeCompleted: (episode) => {
2736
- this.ctx.addEpisode(episode);
2737
- },
2738
- trace: (kind) => {
2739
- this.trace(kind);
2740
- }
2741
- });
2742
- this.turnRunner = new OrchestratorTurnRunner({
2743
- createModel: this.createModel,
2744
- orchestratorModel: this.orchestratorModel,
2745
- tools: this.tools,
2746
- cachedSystem: this.cachedSystem,
2747
- ...this.config.orchestratorToolChoice ? { orchestratorToolChoice: this.config.orchestratorToolChoice } : {},
2748
- ...this.resilience ? { resilience: this.resilience } : {},
2749
- config: this.config,
2750
- ctx: this.ctx,
2751
- memory: this.memory,
2752
- processManager: this.processManager,
2753
- trace: (kind) => {
2754
- this.trace(kind);
2755
- }
2756
- });
2757
- this.completionPolicy = new CompletionPolicy({
2758
- ctx: this.ctx,
2759
- processManager: this.processManager,
2760
- ...this.config.processTimeout != null ? { processTimeout: this.config.processTimeout } : {}
2761
- });
2762
- }
2763
- trace(kind) {
2764
- this.traceWriter?.({ ts: Date.now(), kind });
2765
- }
2766
- async *stream(userMessages, signal) {
2767
- try {
2768
- const memo = this.config.sessionMemoStore.getMemoBySession ? await this.config.sessionMemoStore.getMemoBySession(this.config.sessionId) : (await this.config.sessionMemoStore.getMemosBySession(this.config.sessionId))[0] ?? null;
2769
- this.cachedSessionMemoFacts = memo ? [memo.content] : [];
2770
- } catch {
2771
- this.cachedSessionMemoFacts = [];
2772
- }
2773
- this.processManager.setSessionMemoFacts(this.cachedSessionMemoFacts);
2774
- const startTime = Date.now();
2775
- const maxTurns = this.config.maxTurns ?? 30;
2776
- let forcedFinalResponseRetry = false;
2777
- this.ctx.recordTurn({
2778
- role: "user",
2779
- messages: userMessages,
2780
- tokenEstimate: userMessages.reduce((sum, m) => {
2781
- const text = typeof m.content === "string" ? m.content : getTextContent(m.content);
2782
- return sum + estimateTokens(text);
2783
- }, 0),
2784
- timestamp: Date.now()
2785
- });
2786
- for (let turn = 0; turn < maxTurns; turn++) {
2787
- signal.throwIfAborted();
2788
- this.trace({ type: "turn_start", turn });
2789
- yield { type: "turn_start", turn };
2790
- yield* this.processManager.drainPendingEvents();
2791
- if (this.processManager.hasRunningProcesses()) {
2792
- await this.processManager.waitForPendingEvent(signal, 400);
2793
- const pending = [...this.processManager.drainPendingEvents()];
2794
- if (pending.length > 0) {
2795
- yield* pending;
2796
- continue;
2797
- }
2798
- }
2799
- const prepared = await this.ctx.prepare(signal);
2800
- this.trace({
2801
- type: "context_prepare",
2802
- used: prepared.budget.used,
2803
- limit: prepared.budget.limit,
2804
- compression_tier: "none"
2805
- // TODO: propagate actual tier from ContextWindow
2806
- });
2807
- const turnResult = yield* this.turnRunner.runTurn({
2808
- turn,
2809
- maxTurns,
2810
- prepared,
2811
- signal,
2812
- forcedFinalResponseRetry
2813
- });
2814
- if (turnResult.type === "retry_empty") {
2815
- forcedFinalResponseRetry = true;
2816
- continue;
2817
- }
2818
- if (turnResult.type === "done") {
2819
- this.trace({ type: "turn_end", turn });
2820
- yield { type: "turn_end", turn };
2821
- await this.processManager.waitForProcessListeners();
2822
- yield* this.processManager.drainPendingEvents();
2823
- this.trace({ type: "done" });
2824
- yield {
2825
- type: "done",
2826
- output: turnResult.output,
2827
- stats: { turns: turn + 1, processes: this.processManager.size, durationMs: Date.now() - startTime }
2828
- };
2829
- return;
2830
- }
2831
- this.trace({ type: "turn_end", turn });
2832
- yield { type: "turn_end", turn };
2833
- yield* this.completionPolicy.settleTurn(signal);
2834
- }
2835
- await this.processManager.waitForProcessListeners();
2836
- yield* this.processManager.drainPendingEvents();
2837
- this.trace({ type: "done" });
2838
- yield {
2839
- type: "done",
2840
- output: "Orchestrator reached maximum turns.",
2841
- stats: { turns: maxTurns, processes: this.processManager.size, durationMs: Date.now() - startTime }
2842
- };
2843
- }
2844
- async run(messages, signal) {
2845
- let output = "";
2846
- const events = [];
2847
- for await (const e of this.stream(messages, signal)) {
2848
- events.push(e);
2849
- if (e.type === "done") output = e.output;
2850
- }
2851
- return { output, events };
2852
- }
2853
- };
2854
-
2855
- // src/arc/create-arc-agent.ts
2856
- function createArcAgent(config) {
2857
- const loop = new ArcLoop(config);
2858
- return {
2859
- loop,
2860
- async run(messages, signal) {
2861
- const ac = new AbortController();
2862
- const effectiveSignal = signal ?? ac.signal;
2863
- const result = await loop.run(messages, effectiveSignal);
2864
- if (config.consolidate !== false) {
2865
- try {
2866
- await runConsolidation(
2867
- config.taskId,
2868
- config.sessionId,
2869
- config.episodeStore,
2870
- config.sessionMemoStore,
2871
- config.longTermStore
2872
- );
2873
- } catch {
2874
- }
2875
- if (config.autoMemory !== false) {
2876
- const memory = new MemoryManager({
2877
- sessionMemoStore: config.sessionMemoStore,
2878
- longTermStore: config.longTermStore,
2879
- sessionId: config.sessionId
2880
- });
2881
- try {
2882
- const episodes = await config.episodeStore.getEpisodesByTask(config.taskId);
2883
- await memory.detectAndPromote(episodes);
2884
- } catch {
2885
- }
2886
- }
2887
- }
2888
- return result;
2889
- },
2890
- async *stream(messages, signal) {
2891
- const ac = new AbortController();
2892
- const effectiveSignal = signal ?? ac.signal;
2893
- yield* loop.stream(messages, effectiveSignal);
2894
- if (config.consolidate !== false) {
2895
- void (async () => {
2896
- try {
2897
- await runConsolidation(
2898
- config.taskId,
2899
- config.sessionId,
2900
- config.episodeStore,
2901
- config.sessionMemoStore,
2902
- config.longTermStore
2903
- );
2904
- } catch {
2905
- }
2906
- if (config.autoMemory !== false) {
2907
- const memory = new MemoryManager({
2908
- sessionMemoStore: config.sessionMemoStore,
2909
- longTermStore: config.longTermStore,
2910
- sessionId: config.sessionId
2911
- });
2912
- try {
2913
- const episodes = await config.episodeStore.getEpisodesByTask(config.taskId);
2914
- await memory.detectAndPromote(episodes);
2915
- } catch {
2916
- }
2917
- }
2918
- })();
2919
- }
2920
- }
2921
- };
2922
- }
2923
-
2924
- export { createArcAgent };
2925
- //# sourceMappingURL=create-arc-agent.js.map
2926
- //# sourceMappingURL=create-arc-agent.js.map