@bluecopa/harness 0.1.0-snapshot.101 → 0.1.0-snapshot.103

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,4 +1,4 @@
1
- import { A as ArcEvent, S as StepUsage } from '../types-g-3DvSSE.js';
1
+ import { A as ArcEvent, S as StepUsage } from '../types-BV-E1pvS.js';
2
2
  import 'zod';
3
3
  import 'ai';
4
4
 
@@ -1,4 +1,4 @@
1
- import { a as ArcLoopConfig, b as AgentMessage, A as ArcEvent, c as ArcRunResult, T as TraceEvent } from '../types-g-3DvSSE.js';
1
+ import { a as ArcLoopConfig, b as AgentMessage, A as ArcEvent, c as ArcRunResult, T as TraceEvent } from '../types-BV-E1pvS.js';
2
2
  import 'zod';
3
3
  import 'ai';
4
4
 
@@ -231,13 +231,24 @@ var Remember = tool({
231
231
  })
232
232
  });
233
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.",
234
+ description: 'Retrieve prior episode context by ID. Default output is summary-first and includes structured output plus artifact handles when available. Set detail to "trace" or "artifacts" for richer detail.',
235
235
  inputSchema: z.object({
236
236
  id: z.string().describe("Episode ID to read"),
237
+ detail: z.enum(["summary", "trace", "artifacts"]).optional().describe("Level of detail to retrieve. Default: summary."),
238
+ artifactKey: z.string().optional().describe('Optional artifact handle to focus on when detail is "artifacts".'),
237
239
  maxTokens: z.number().optional().describe("Max tokens to return (truncates from the end). Default: no limit.")
238
240
  })
239
241
  });
240
- var orchestratorTools = { Thread, Check, Cancel, Remember, ReadEpisode };
242
+ var ReadRollup = tool({
243
+ description: "Retrieve a rollup summary by ref or ID. Use to inspect compressed older history and discover which episode refs to hydrate next.",
244
+ inputSchema: z.object({
245
+ id: z.string().describe("Rollup ref or ID to read (for example: EE1)"),
246
+ detail: z.enum(["summary", "children"]).optional().describe("Whether to return just the rollup summary or include child refs/summaries. Default: summary."),
247
+ maxTokens: z.number().optional().describe("Max tokens to return (truncates from the end). Default: no limit.")
248
+ })
249
+ });
250
+ var orchestratorTools = { Thread, Check, Cancel, Remember, ReadEpisode, ReadRollup };
251
+ var processTools = { ...builtinTools, ReadEpisode };
241
252
  tool({
242
253
  description: "Ask the user a question and wait for their response.",
243
254
  inputSchema: z.object({
@@ -320,6 +331,7 @@ var ContextWindow = class {
320
331
  config;
321
332
  turns = [];
322
333
  episodes = [];
334
+ rollups = [];
323
335
  cachedContextSize = null;
324
336
  constructor(config) {
325
337
  this.config = config;
@@ -351,6 +363,7 @@ var ContextWindow = class {
351
363
  const contextWindowSize = await this.getContextWindowSize();
352
364
  const totalBudget = contextWindowSize - this.config.outputReserve;
353
365
  this.episodes = await this.config.episodeStore.getEpisodesByTask(this.config.taskId);
366
+ this.rollups = this.config.rollupStore ? await this.config.rollupStore.getRollupsByTask(this.config.taskId) : [];
354
367
  const filesBeingTouched = this.extractFilesBeingTouched();
355
368
  const systemTokens = estimateTokens(this.config.systemPrompt);
356
369
  const memoryBudget = Math.min(Math.floor(totalBudget * 0.15), 4e3);
@@ -367,7 +380,7 @@ var ContextWindow = class {
367
380
  totalUsed = systemTokens + memoryTokens + episodeTokens + conversationTokens;
368
381
  }
369
382
  const episodeBudget = Math.floor(totalBudget * 0.2);
370
- if (episodeTokens > episodeBudget && this.episodes.length > 10) {
383
+ if (episodeTokens > episodeBudget && this.episodes.length > 10 && this.rollups.length === 0) {
371
384
  const grouped = this.groupEpisodeSummaries();
372
385
  episodeTokens = estimateMessagesTokens(grouped);
373
386
  episodeMessages.length = 0;
@@ -410,14 +423,13 @@ var ContextWindow = class {
410
423
  const contextWindowSize = await this.getContextWindowSize();
411
424
  const totalBudget = contextWindowSize - this.config.outputReserve;
412
425
  const systemTokens = estimateTokens(this.config.systemPrompt);
426
+ this.episodes = await this.config.episodeStore.getEpisodesByTask(this.config.taskId);
427
+ this.rollups = this.config.rollupStore ? await this.config.rollupStore.getRollupsByTask(this.config.taskId) : [];
413
428
  let conversationTokens = 0;
414
429
  for (const turn of this.turns) {
415
430
  conversationTokens += turn.tokenEstimate;
416
431
  }
417
- let episodeTokens = 0;
418
- for (const ep of this.episodes) {
419
- episodeTokens += estimateTokens(ep.summary);
420
- }
432
+ const episodeTokens = estimateMessagesTokens(this.buildEpisodeSummaries());
421
433
  return {
422
434
  limit: totalBudget,
423
435
  used: systemTokens + conversationTokens + episodeTokens,
@@ -451,14 +463,40 @@ var ContextWindow = class {
451
463
  }
452
464
  buildEpisodeSummaries() {
453
465
  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");
466
+ const recentEpisodes = this.episodes.slice(-5);
467
+ const recentStartIndex = recentEpisodes[0]?.index ?? Number.MAX_SAFE_INTEGER;
468
+ const rollupLines = this.buildRollupFrontier(recentStartIndex).map((rollup) => `${rollup.ref} [E${rollup.rangeStartIndex}-E${rollup.rangeEndIndex}]:
469
+ ${rollup.summary}`);
470
+ const coveredEpisodeIds = new Set(
471
+ this.buildRollupFrontier(recentStartIndex).flatMap((rollup) => rollup.childEpisodeIds)
472
+ );
473
+ const olderLeafLines = this.episodes.filter((episode) => episode.index < recentStartIndex && !coveredEpisodeIds.has(episode.id)).map((episode) => `Episode E${episode.index} [${episode.id}] (${episode.success ? "ok" : "failed"}):
474
+ ${episode.summary}`);
475
+ const recentLines = recentEpisodes.map((episode) => `Episode E${episode.index} [${episode.id}] (${episode.success ? "ok" : "failed"}):
476
+ ${episode.summary}`);
477
+ const episodeText = [...rollupLines, ...olderLeafLines, ...recentLines].join("\n\n");
456
478
  return [{
457
479
  role: "system",
458
480
  content: `Prior episodes for this task:
459
481
  ${episodeText}`
460
482
  }];
461
483
  }
484
+ buildRollupFrontier(beforeEpisodeIndex) {
485
+ if (this.rollups.length === 0) {
486
+ return [];
487
+ }
488
+ const eligible = this.rollups.filter((rollup) => rollup.rangeEndIndex < beforeEpisodeIndex).sort((a, b) => b.level - a.level || a.rangeStartIndex - b.rangeStartIndex);
489
+ const selected = [];
490
+ for (const rollup of eligible) {
491
+ const overlaps = selected.some(
492
+ (existing) => !(rollup.rangeEndIndex < existing.rangeStartIndex || rollup.rangeStartIndex > existing.rangeEndIndex)
493
+ );
494
+ if (!overlaps) {
495
+ selected.push(rollup);
496
+ }
497
+ }
498
+ return selected.sort((a, b) => a.rangeStartIndex - b.rangeStartIndex);
499
+ }
462
500
  buildTurnMessages() {
463
501
  const messages = [];
464
502
  for (const turn of this.turns) {
@@ -732,6 +770,96 @@ ${repeatedErrors.map(([action, n]) => ` "${action}" failed ${n}x`).join("\n")}`
732
770
  return { removed, merged };
733
771
  }
734
772
  };
773
+ var ROLLUP_GROUP_SIZE = 6;
774
+ function nextRollupRef(level, existing) {
775
+ const prefix = "E".repeat(level + 1);
776
+ const maxIndex = existing.filter((rollup) => rollup.level === level).map((rollup) => Number(rollup.ref.slice(prefix.length))).filter((value) => Number.isFinite(value)).reduce((max, value) => Math.max(max, value), 0);
777
+ return `${prefix}${maxIndex + 1}`;
778
+ }
779
+ function summarizeEpisodeBatch(episodes) {
780
+ const files = [...new Set(episodes.flatMap((episode) => [...episode.filesRead, ...episode.filesModified]))];
781
+ const childRefs = episodes.map((episode) => `E${episode.index}`);
782
+ const actions = episodes.map((episode) => `- E${episode.index}: ${episode.threadAction} (${episode.success ? "ok" : "failed"})`);
783
+ return [
784
+ `Summary of [${childRefs[0]}-${childRefs[childRefs.length - 1]}]`,
785
+ ...actions,
786
+ ...files.length > 0 ? [`Key files: ${files.join(", ")}`] : []
787
+ ].join("\n");
788
+ }
789
+ function summarizeRollupBatch(rollups) {
790
+ const childRefs = rollups.map((rollup) => rollup.ref);
791
+ const files = [...new Set(rollups.flatMap((rollup) => rollup.keyFiles))];
792
+ const summaries = rollups.map((rollup) => `- ${rollup.ref}: ${rollup.summary.split("\n")[0] ?? rollup.summary}`);
793
+ return [
794
+ `Summary of [${childRefs[0]}-${childRefs[childRefs.length - 1]}]`,
795
+ ...summaries,
796
+ ...files.length > 0 ? [`Key files: ${files.join(", ")}`] : []
797
+ ].join("\n");
798
+ }
799
+ async function buildEpisodeRollups(episodes, sessionId, rollupStore) {
800
+ const sortedEpisodes = [...episodes].sort((a, b) => a.index - b.index);
801
+ let rollups = await rollupStore.getRollupsBySession(sessionId);
802
+ const coveredEpisodeIds = new Set(rollups.flatMap((rollup) => rollup.childEpisodeIds));
803
+ const uncoveredEpisodes = sortedEpisodes.filter((episode) => !coveredEpisodeIds.has(episode.id));
804
+ const level1Chunks = [];
805
+ for (let index = 0; index + ROLLUP_GROUP_SIZE <= uncoveredEpisodes.length; index += ROLLUP_GROUP_SIZE) {
806
+ level1Chunks.push(uncoveredEpisodes.slice(index, index + ROLLUP_GROUP_SIZE));
807
+ }
808
+ for (const chunk of level1Chunks) {
809
+ const rollup = {
810
+ id: randomUUID(),
811
+ ref: nextRollupRef(1, rollups),
812
+ taskId: chunk[0].taskId,
813
+ sessionId,
814
+ level: 1,
815
+ summary: summarizeEpisodeBatch(chunk),
816
+ childEpisodeIds: chunk.map((episode) => episode.id),
817
+ childRollupIds: [],
818
+ rangeStartIndex: chunk[0].index,
819
+ rangeEndIndex: chunk[chunk.length - 1].index,
820
+ keyFiles: [...new Set(chunk.flatMap((episode) => [...episode.filesRead, ...episode.filesModified]))],
821
+ createdAt: Date.now()
822
+ };
823
+ await rollupStore.addRollup(rollup);
824
+ rollups = [...rollups, rollup];
825
+ }
826
+ let level = 1;
827
+ while (true) {
828
+ const allRollups = await rollupStore.getRollupsBySession(sessionId);
829
+ const parentedChildIds = new Set(
830
+ allRollups.flatMap((rollup) => rollup.childRollupIds)
831
+ );
832
+ const levelCandidates = allRollups.filter((rollup) => rollup.level === level && !parentedChildIds.has(rollup.id)).sort((a, b) => a.rangeStartIndex - b.rangeStartIndex);
833
+ if (levelCandidates.length < ROLLUP_GROUP_SIZE) {
834
+ break;
835
+ }
836
+ const batches = [];
837
+ for (let index = 0; index + ROLLUP_GROUP_SIZE <= levelCandidates.length; index += ROLLUP_GROUP_SIZE) {
838
+ batches.push(levelCandidates.slice(index, index + ROLLUP_GROUP_SIZE));
839
+ }
840
+ if (batches.length === 0) {
841
+ break;
842
+ }
843
+ for (const batch of batches) {
844
+ const rollup = {
845
+ id: randomUUID(),
846
+ ref: nextRollupRef(level + 1, allRollups),
847
+ taskId: batch[0].taskId,
848
+ sessionId,
849
+ level: level + 1,
850
+ summary: summarizeRollupBatch(batch),
851
+ childEpisodeIds: [],
852
+ childRollupIds: batch.map((child) => child.id),
853
+ rangeStartIndex: batch[0].rangeStartIndex,
854
+ rangeEndIndex: batch[batch.length - 1].rangeEndIndex,
855
+ keyFiles: [...new Set(batch.flatMap((rollupChild) => rollupChild.keyFiles))],
856
+ createdAt: Date.now()
857
+ };
858
+ await rollupStore.addRollup(rollup);
859
+ }
860
+ level += 1;
861
+ }
862
+ }
735
863
  async function consolidateEpisodes(episodes, sessionId, sessionMemoStore) {
736
864
  if (episodes.length === 0) {
737
865
  throw new Error("Cannot consolidate zero episodes");
@@ -770,9 +898,12 @@ async function consolidateEpisodes(episodes, sessionId, sessionMemoStore) {
770
898
  await sessionMemoStore.addMemo(memo);
771
899
  return memo;
772
900
  }
773
- async function runConsolidation(_taskId, sessionId, episodeStore, sessionMemoStore, _longTermStore) {
901
+ async function runConsolidation(_taskId, sessionId, episodeStore, sessionMemoStore, _longTermStore, rollupStore) {
774
902
  const episodes = await episodeStore.getEpisodesBySession(sessionId);
775
903
  if (episodes.length > 0) {
904
+ if (rollupStore) {
905
+ await buildEpisodeRollups(episodes, sessionId, rollupStore);
906
+ }
776
907
  await consolidateEpisodes(episodes, sessionId, sessionMemoStore);
777
908
  }
778
909
  }
@@ -1064,6 +1195,7 @@ var EpisodeCompressor = class {
1064
1195
  compress(input) {
1065
1196
  const now = Date.now();
1066
1197
  const id = randomUUID();
1198
+ const artifacts = extractArtifacts(input.messages);
1067
1199
  const episode = {
1068
1200
  id,
1069
1201
  taskId: input.taskId,
@@ -1078,14 +1210,14 @@ var EpisodeCompressor = class {
1078
1210
  steps: countSteps(input.messages),
1079
1211
  success: input.success,
1080
1212
  createdAt: now,
1081
- parentEpisodeIds: input.parentEpisodeIds
1213
+ parentEpisodeIds: input.parentEpisodeIds,
1214
+ artifactKeys: artifacts.map((artifact) => artifact.key)
1082
1215
  };
1083
1216
  const trace = {
1084
1217
  episodeId: id,
1085
1218
  messages: input.messages,
1086
1219
  createdAt: now
1087
1220
  };
1088
- const artifacts = extractArtifacts(input.messages);
1089
1221
  return { episode, trace, artifacts };
1090
1222
  }
1091
1223
  async compressLLM(input, signal) {
@@ -1844,32 +1976,29 @@ async function buildSeedMessages(episodeIds, episodeStore) {
1844
1976
  if (episodeIds.length === 0) return [];
1845
1977
  const messages = [];
1846
1978
  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");
1979
+ const episode = await episodeStore.getEpisode(id);
1980
+ if (!episode) continue;
1981
+ const blocks = [
1982
+ `Action: ${episode.threadAction}`,
1983
+ `Status: ${episode.success ? "success" : "failed"}`,
1984
+ `Summary:
1985
+ ${episode.summary}`
1986
+ ];
1987
+ if (episode.structuredOutput && Object.keys(episode.structuredOutput).length > 0) {
1988
+ const structuredOutput = JSON.stringify(episode.structuredOutput, null, 2);
1989
+ const truncated = structuredOutput.length > 2e3 ? structuredOutput.slice(0, 2e3) + "\n... [truncated]" : structuredOutput;
1990
+ blocks.push(`Structured output:
1991
+ ${truncated}`);
1992
+ }
1993
+ if (episode.artifactKeys && episode.artifactKeys.length > 0) {
1994
+ blocks.push(`Artifact handles:
1995
+ ${episode.artifactKeys.join(", ")}`);
1996
+ }
1997
+ blocks.push('Use ReadEpisode with detail: "trace" or "artifacts" only if you need more than this summary.');
1869
1998
  messages.push({
1870
1999
  role: "system",
1871
2000
  content: `Context from prior episode (${id}):
1872
- ${traceText}`
2001
+ ${blocks.join("\n\n")}`
1873
2002
  });
1874
2003
  }
1875
2004
  return messages;
@@ -1897,6 +2026,114 @@ async function* drainNonBlocking(iterable) {
1897
2026
  yield next.value;
1898
2027
  }
1899
2028
  }
2029
+
2030
+ // src/arc/episode-reader.ts
2031
+ function coerceDetail(value, fallback) {
2032
+ if (value === "summary" || value === "trace" || value === "artifacts") {
2033
+ return value;
2034
+ }
2035
+ return fallback;
2036
+ }
2037
+ function renderTraceText(messages) {
2038
+ return messages.map((message) => {
2039
+ const textContent = getTextContent(message.content);
2040
+ if (message.role === "assistant" && message.toolCalls?.length) {
2041
+ const calls = message.toolCalls.map((toolCall) => ` ${toolCall.toolName}(${JSON.stringify(toolCall.args)})`).join("\n");
2042
+ return `[assistant]
2043
+ ${textContent}
2044
+ [tool calls]
2045
+ ${calls}`;
2046
+ }
2047
+ if (message.role === "tool" && message.toolResults?.length) {
2048
+ const results = message.toolResults.map((toolResult) => {
2049
+ const output = toolResult.result.length > 500 ? toolResult.result.slice(0, 500) + "..." : toolResult.result;
2050
+ return ` ${toolResult.toolName}: ${toolResult.isError ? "ERROR: " : ""}${output}`;
2051
+ }).join("\n");
2052
+ return `[tool results]
2053
+ ${results}`;
2054
+ }
2055
+ return `[${message.role}]
2056
+ ${textContent}`;
2057
+ }).join("\n\n");
2058
+ }
2059
+ function formatSummaryBlock(episode) {
2060
+ const lines = [
2061
+ `Action: ${episode.threadAction}`,
2062
+ `Status: ${episode.success ? "success" : "failed"}`,
2063
+ `Summary:
2064
+ ${episode.summary}`
2065
+ ];
2066
+ if (episode.structuredOutput && Object.keys(episode.structuredOutput).length > 0) {
2067
+ const structuredOutput = JSON.stringify(episode.structuredOutput, null, 2);
2068
+ const truncated = structuredOutput.length > 2e3 ? structuredOutput.slice(0, 2e3) + "\n... [truncated]" : structuredOutput;
2069
+ lines.push(`Structured output:
2070
+ ${truncated}`);
2071
+ }
2072
+ if (episode.artifactKeys && episode.artifactKeys.length > 0) {
2073
+ lines.push(`Artifact handles:
2074
+ ${episode.artifactKeys.join(", ")}`);
2075
+ }
2076
+ lines.push(`Use ReadEpisode with detail: "trace" or "artifacts" for more detail from this episode.`);
2077
+ return lines.join("\n\n");
2078
+ }
2079
+ function formatArtifactsBlock(artifacts, artifactKey) {
2080
+ const filtered = artifactKey ? artifacts.filter((artifact) => artifact.key === artifactKey) : artifacts;
2081
+ if (filtered.length === 0) {
2082
+ return artifactKey ? `Artifact not found: ${artifactKey}` : "No artifact content is currently available for this episode.";
2083
+ }
2084
+ return filtered.map((artifact) => `[${artifact.key}]
2085
+ ${artifact.content}`).join("\n\n");
2086
+ }
2087
+ function applyMaxTokens(text, maxTokens) {
2088
+ if (typeof maxTokens !== "number" || maxTokens <= 0) {
2089
+ return text;
2090
+ }
2091
+ const maxChars = maxTokens * 4;
2092
+ if (text.length <= maxChars) {
2093
+ return text;
2094
+ }
2095
+ return text.slice(0, maxChars) + "\n... [truncated]";
2096
+ }
2097
+ async function renderEpisodeReadResult(options) {
2098
+ const episodeId = String(options.args.id ?? "");
2099
+ const detail = coerceDetail(options.args.detail, options.defaultDetail ?? "summary");
2100
+ const artifactKey = options.args.artifactKey != null ? String(options.args.artifactKey) : void 0;
2101
+ const episode = await options.episodeStore.getEpisode(episodeId);
2102
+ if (!episode) {
2103
+ return `Episode not found: ${episodeId}`;
2104
+ }
2105
+ const proc = options.processes ? [...options.processes].find((process2) => process2.result?.episode.id === episodeId) : void 0;
2106
+ const liveArtifacts = proc?.result?.artifacts ?? [];
2107
+ let resultText = `Episode ${episodeId}
2108
+
2109
+ ${formatSummaryBlock(episode)}`;
2110
+ if (detail === "artifacts") {
2111
+ resultText = `Episode ${episodeId}
2112
+
2113
+ ${formatArtifactsBlock(liveArtifacts, artifactKey)}`;
2114
+ } else if (detail === "trace") {
2115
+ const trace = await options.episodeStore.getTrace(episodeId);
2116
+ if (!trace) {
2117
+ resultText += "\n\nTrace not found.";
2118
+ } else {
2119
+ resultText += `
2120
+
2121
+ --- Trace ---
2122
+ ${renderTraceText(trace.messages)}`;
2123
+ }
2124
+ if (liveArtifacts.length > 0) {
2125
+ resultText += `
2126
+
2127
+ --- Artifacts ---
2128
+ ${formatArtifactsBlock(liveArtifacts, artifactKey)}`;
2129
+ }
2130
+ } else if (artifactKey) {
2131
+ resultText += `
2132
+
2133
+ Requested artifact handle: ${artifactKey}`;
2134
+ }
2135
+ return applyMaxTokens(resultText, options.args.maxTokens);
2136
+ }
1900
2137
  var FIELD_RE = /^(\w+)(?::(\w+)(\[\])?)(\?)?(?:\s*\(([^)]+)\))?$/;
1901
2138
  function parseField(raw) {
1902
2139
  const trimmed = raw.trim();
@@ -2054,7 +2291,7 @@ var ProcessManager = class {
2054
2291
  dispatch(request, parentSignal) {
2055
2292
  const { loopConfig } = this.config;
2056
2293
  const profileConfig = request.profile ? loopConfig.processProfiles?.[request.profile] : void 0;
2057
- const globalTools = loopConfig.processTools ?? builtinTools;
2294
+ const globalTools = loopConfig.processTools ?? processTools;
2058
2295
  const profile = profileConfig ? resolveProfile(profileConfig, globalTools) : void 0;
2059
2296
  const defaultModel = this.config.modelMap[profile?.model ?? "medium"] ?? this.config.modelMap.medium;
2060
2297
  const profileSkills = profileConfig && isProfileDeclaration(profileConfig) ? profileConfig.skills : void 0;
@@ -2103,9 +2340,32 @@ var ProcessManager = class {
2103
2340
  ...pickDefined(loopConfig, [
2104
2341
  "hookRunner",
2105
2342
  "permissionManager",
2106
- "telemetry",
2107
- "executeToolAction"
2108
- ])
2343
+ "telemetry"
2344
+ ]),
2345
+ executeToolAction: async (action) => {
2346
+ if (action.name === "ReadEpisode") {
2347
+ const allowedEpisodeIds = new Set(request.contextEpisodeIds ?? []);
2348
+ const requestedEpisodeId = String(action.args.id ?? "");
2349
+ if (!allowedEpisodeIds.has(requestedEpisodeId)) {
2350
+ return {
2351
+ success: false,
2352
+ output: "",
2353
+ error: `ReadEpisode is limited to contextEpisodeIds for this thread. Allowed episode IDs: ${[...allowedEpisodeIds].join(", ") || "(none)"}`
2354
+ };
2355
+ }
2356
+ const output = await renderEpisodeReadResult({
2357
+ episodeStore: loopConfig.episodeStore,
2358
+ args: action.args,
2359
+ defaultDetail: "summary",
2360
+ processes: this.processes.values()
2361
+ });
2362
+ return { success: true, output };
2363
+ }
2364
+ if (loopConfig.executeToolAction) {
2365
+ return loopConfig.executeToolAction(action);
2366
+ }
2367
+ return null;
2368
+ }
2109
2369
  });
2110
2370
  this.processes.set(proc.id, proc);
2111
2371
  this.actionIndex.set(normalizeAction(request.action), proc.id);
@@ -2250,6 +2510,64 @@ var ProcessManager = class {
2250
2510
  }
2251
2511
  }
2252
2512
  };
2513
+
2514
+ // src/arc/rollup-reader.ts
2515
+ function applyMaxTokens2(text, maxTokens) {
2516
+ if (typeof maxTokens !== "number" || maxTokens <= 0) {
2517
+ return text;
2518
+ }
2519
+ const maxChars = maxTokens * 4;
2520
+ if (text.length <= maxChars) {
2521
+ return text;
2522
+ }
2523
+ return text.slice(0, maxChars) + "\n... [truncated]";
2524
+ }
2525
+ function formatRollupSummary(rollup) {
2526
+ return [
2527
+ `Rollup ${rollup.ref}`,
2528
+ `Level: ${rollup.level}`,
2529
+ `Covers: E${rollup.rangeStartIndex}-E${rollup.rangeEndIndex}`,
2530
+ `Child episodes: ${rollup.childEpisodeIds.length}`,
2531
+ `Child rollups: ${rollup.childRollupIds.length}`,
2532
+ ...rollup.keyFiles.length > 0 ? [`Key files: ${rollup.keyFiles.join(", ")}`] : [],
2533
+ "",
2534
+ rollup.summary
2535
+ ].join("\n");
2536
+ }
2537
+ async function renderRollupReadResult(options) {
2538
+ const rollupId = String(options.args.id ?? "");
2539
+ const rollup = await options.rollupStore.getRollup(rollupId);
2540
+ if (!rollup) {
2541
+ return `Rollup not found: ${rollupId}`;
2542
+ }
2543
+ const detail = options.args.detail === "children" ? "children" : "summary";
2544
+ let resultText = formatRollupSummary(rollup);
2545
+ if (detail === "children") {
2546
+ const childSections = [];
2547
+ for (const childRollupId of rollup.childRollupIds) {
2548
+ const childRollup = await options.rollupStore.getRollup(childRollupId);
2549
+ if (childRollup) {
2550
+ childSections.push(`--- Child rollup ${childRollup.ref} ---
2551
+ ${childRollup.summary}`);
2552
+ }
2553
+ }
2554
+ for (const childEpisodeId of rollup.childEpisodeIds) {
2555
+ const episode = await options.episodeStore.getEpisode(childEpisodeId);
2556
+ if (episode) {
2557
+ childSections.push(`--- Episode E${episode.index} [${episode.id}] ---
2558
+ ${episode.summary}`);
2559
+ }
2560
+ }
2561
+ if (childSections.length > 0) {
2562
+ resultText += `
2563
+
2564
+ ${childSections.join("\n\n")}`;
2565
+ }
2566
+ }
2567
+ return applyMaxTokens2(resultText, options.args.maxTokens);
2568
+ }
2569
+
2570
+ // src/arc/orchestrator-turn-runner.ts
2253
2571
  var OrchestratorTurnRunner = class {
2254
2572
  config;
2255
2573
  constructor(config) {
@@ -2382,13 +2700,6 @@ var OrchestratorTurnRunner = class {
2382
2700
  yield { type: "thread_rejected", action: request.action, reason: "duplicate (completed)" };
2383
2701
  continue;
2384
2702
  }
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
2703
  const proc = this.config.processManager.dispatch(request, signal);
2393
2704
  const resultText = `Process ${proc.id} dispatched: "${request.action}" (model: ${request.model ?? "medium"})`;
2394
2705
  toolResultMessages.push({
@@ -2435,45 +2746,12 @@ ${proc.result.episode.summary}`;
2435
2746
  continue;
2436
2747
  }
2437
2748
  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
- }
2749
+ const resultText = await renderEpisodeReadResult({
2750
+ episodeStore: this.config.config.episodeStore,
2751
+ args: call.args,
2752
+ defaultDetail: "summary",
2753
+ processes: this.config.processManager.values()
2754
+ });
2477
2755
  toolResultMessages.push({
2478
2756
  role: "tool",
2479
2757
  content: resultText,
@@ -2481,6 +2759,20 @@ ${artifact.content}`).join("\n\n");
2481
2759
  });
2482
2760
  continue;
2483
2761
  }
2762
+ if (call.toolName === "ReadRollup") {
2763
+ const rollupStore = this.config.config.rollupStore;
2764
+ const resultText = rollupStore ? await renderRollupReadResult({
2765
+ rollupStore,
2766
+ episodeStore: this.config.config.episodeStore,
2767
+ args: call.args
2768
+ }) : "Rollup store is not configured.";
2769
+ toolResultMessages.push({
2770
+ role: "tool",
2771
+ content: resultText,
2772
+ toolResults: [{ toolCallId, toolName: "ReadRollup", result: resultText }]
2773
+ });
2774
+ continue;
2775
+ }
2484
2776
  if (call.toolName === "Remember") {
2485
2777
  const memOpts = {};
2486
2778
  if (call.args.category != null) memOpts.category = String(call.args.category);
@@ -2630,6 +2922,7 @@ var DEFAULT_ORCHESTRATOR_PROMPT = `You are an orchestrator agent. You accomplish
2630
2922
  - **Cancel**: Cancel a running process.
2631
2923
  - **Remember**: Save information to persistent memory.
2632
2924
  - **ReadEpisode**: Retrieve the full trace of a completed episode (detailed tool outputs, file contents, errors).
2925
+ - **ReadRollup**: Retrieve an indexed rollup of older episode history and discover which episode refs to hydrate next.
2633
2926
 
2634
2927
  ## When to dispatch threads
2635
2928
 
@@ -2712,6 +3005,7 @@ var ArcLoop = class {
2712
3005
  outputReserve: config.outputReserve ?? 2e4,
2713
3006
  systemPrompt: this.systemPrompt,
2714
3007
  episodeStore: config.episodeStore,
3008
+ ...config.rollupStore ? { rollupStore: config.rollupStore } : {},
2715
3009
  memory: this.memory,
2716
3010
  taskId: config.taskId,
2717
3011
  createModel: this.createModel,
@@ -2868,7 +3162,8 @@ function createArcAgent(config) {
2868
3162
  config.sessionId,
2869
3163
  config.episodeStore,
2870
3164
  config.sessionMemoStore,
2871
- config.longTermStore
3165
+ config.longTermStore,
3166
+ config.rollupStore
2872
3167
  );
2873
3168
  } catch {
2874
3169
  }
@@ -2899,7 +3194,8 @@ function createArcAgent(config) {
2899
3194
  config.sessionId,
2900
3195
  config.episodeStore,
2901
3196
  config.sessionMemoStore,
2902
- config.longTermStore
3197
+ config.longTermStore,
3198
+ config.rollupStore
2903
3199
  );
2904
3200
  } catch {
2905
3201
  }