@rowan-agent/agent 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/harness/tools/index.ts
2
- import { mkdir, readFile as readFile2, stat, writeFile } from "fs/promises";
2
+ import { chmod, mkdir, readFile as readFile2, stat, writeFile } from "fs/promises";
3
3
  import { basename as basename2, dirname as dirname2, relative as relative2, resolve as resolve2 } from "path";
4
+ import { tmpdir } from "os";
4
5
  import Type2 from "typebox";
5
6
  import Schema from "typebox/schema";
6
7
 
@@ -20,7 +21,7 @@ function buildStructuredSection(tag, items) {
20
21
  return lines.join("\n");
21
22
  }
22
23
  function buildSkillsDescription(skills) {
23
- const visibleSkills = skills.filter((skill) => !skill.disableModelInvocation);
24
+ const visibleSkills = skills.filter((skill) => !(skill.disableAutoInvocation ?? skill.disableModelInvocation));
24
25
  if (visibleSkills.length === 0) return "";
25
26
  const lines = ["<available_skills>"];
26
27
  for (const skill of visibleSkills) {
@@ -170,11 +171,12 @@ function buildPhaseEntry(p) {
170
171
  return entry;
171
172
  }
172
173
  function buildRouteDescription(availablePhases) {
174
+ const routablePhases = availablePhases.filter(({ name }) => name !== "stop");
173
175
  const phasesBlock = buildStructuredSection("phase", [
174
- ...availablePhases.map(buildPhaseEntry),
176
+ ...routablePhases.map(buildPhaseEntry),
175
177
  {
176
178
  name: "stop",
177
- description: "Explicitly finish the current user request or task when no further user input is needed"
179
+ description: "Complete the task; when the current reply has no final user-facing conclusion, the built-in Stop Phase provides one"
178
180
  }
179
181
  ]);
180
182
  return [
@@ -183,7 +185,7 @@ function buildRouteDescription(availablePhases) {
183
185
  "Rules:",
184
186
  "- Call route only to continue execution immediately in one or more phases, or to explicitly stop.",
185
187
  "- `decision` lists phase executions; a single target equal to the current phase starts another iteration of that phase.",
186
- "- `stop` means the current user request or task is complete and no further user input is needed; `stop` must be the only target.",
188
+ "- `stop` completes the task; when the current reply has no final user-facing conclusion, the built-in Stop Phase provides one. Use it only when the task is complete and no further user input is needed, and make it the only target.",
187
189
  "- Each target may include `phase`, `reason`, `payload`.",
188
190
  "- A phase may appear multiple times as independent execution instances.",
189
191
  "- `payload` MUST match the phase's `payload_schema`",
@@ -197,9 +199,10 @@ function buildRouteDescription(availablePhases) {
197
199
  ].join("\n");
198
200
  }
199
201
  function createRouteTool(availablePhases) {
202
+ const routablePhases = availablePhases.filter(({ name }) => name !== "stop");
200
203
  const DecisionTarget = Type.Object({
201
204
  phase: Type.Union([
202
- ...availablePhases.map((p) => Type.Literal(p.name)),
205
+ ...routablePhases.map((p) => Type.Literal(p.name)),
203
206
  Type.Literal("stop")
204
207
  ]),
205
208
  reason: Type.Optional(Type.String({ description: "Brief reason for this decision" })),
@@ -208,10 +211,10 @@ function createRouteTool(availablePhases) {
208
211
  return {
209
212
  name: PhaseRouteTool,
210
213
  description: buildRouteDescription(availablePhases),
211
- promptSnippet: "Route is optional: omit it to remain in the current phase and wait for user input; use it for immediate phase execution or an explicit stop.",
214
+ promptSnippet: "Route is optional: omit it to remain in the current phase and wait for user input; use it for immediate phase execution, or use stop to complete the task and obtain a final user-facing conclusion when needed.",
212
215
  promptGuidelines: [
213
216
  "Do not call route together with ordinary tools.",
214
- "Use route(stop) only when the current user request or task is complete and no further user input is needed."
217
+ "Use route(stop) only when the current user request or task is complete; the Stop Phase will provide the final user-facing conclusion."
215
218
  ],
216
219
  parameters: Type.Object({
217
220
  decision: Type.Array(DecisionTarget, { description: "Phase executions to start", minItems: 1 }),
@@ -264,7 +267,8 @@ function extractRouteCall(toolCalls) {
264
267
  }
265
268
 
266
269
  // src/harness/tools/index.ts
267
- var DEFAULT_MAX_READ_BYTES = 64e3;
270
+ var DEFAULT_MAX_READ_BYTES = 50 * 1024;
271
+ var DEFAULT_MAX_READ_LINES = 2e3;
268
272
  var DEFAULT_BASH_TIMEOUT_MS = 3e4;
269
273
  var DEFAULT_MAX_BASH_OUTPUT_BYTES = 64e3;
270
274
  var ReadArgsSchema = Type2.Object({
@@ -311,7 +315,9 @@ function normalizeCoreToolInputPath(path = ".") {
311
315
  function createCoreToolContext(input = {}) {
312
316
  return {
313
317
  root: resolve2(input.root ?? process.cwd()),
318
+ archiveDir: resolve2(input.archiveDir ?? `${tmpdir()}/rowan-tool-results`),
314
319
  maxReadBytes: input.maxReadBytes ?? DEFAULT_MAX_READ_BYTES,
320
+ maxReadLines: input.maxReadLines ?? DEFAULT_MAX_READ_LINES,
315
321
  bashTimeoutMs: input.bashTimeoutMs ?? DEFAULT_BASH_TIMEOUT_MS,
316
322
  maxBashOutputBytes: input.maxBashOutputBytes ?? DEFAULT_MAX_BASH_OUTPUT_BYTES
317
323
  };
@@ -449,17 +455,9 @@ async function captureStream(stream, maxBytes) {
449
455
  if (!value) {
450
456
  continue;
451
457
  }
452
- const remainingBytes = maxBytes - totalBytes;
453
- if (remainingBytes > 0) {
454
- const kept = value.subarray(0, remainingBytes);
455
- chunks.push(kept);
456
- totalBytes += kept.byteLength;
457
- }
458
- if (value.byteLength > remainingBytes || totalBytes >= maxBytes) {
459
- truncated = true;
460
- await reader.cancel().catch(() => void 0);
461
- break;
462
- }
458
+ chunks.push(value);
459
+ totalBytes += value.byteLength;
460
+ if (totalBytes > maxBytes) truncated = true;
463
461
  }
464
462
  const bytes = new Uint8Array(totalBytes);
465
463
  let offset = 0;
@@ -472,6 +470,19 @@ async function captureStream(stream, maxBytes) {
472
470
  truncated
473
471
  };
474
472
  }
473
+ async function spillResult(context, toolName, content) {
474
+ await mkdir(context.archiveDir, { recursive: true, mode: 448 });
475
+ await chmod(context.archiveDir, 448).catch(() => void 0);
476
+ const path = `${context.archiveDir}/rowan-${toolName}-${Date.now()}-${crypto.randomUUID()}.log`;
477
+ await writeFile(path, content, { encoding: "utf8", mode: 384 });
478
+ await chmod(path, 384).catch(() => void 0);
479
+ return path;
480
+ }
481
+ function boundedPreview(text, maxBytes) {
482
+ const bytes = new TextEncoder().encode(text);
483
+ if (bytes.byteLength <= maxBytes) return { text, truncated: false };
484
+ return { text: new TextDecoder().decode(bytes.subarray(0, maxBytes)), truncated: true };
485
+ }
475
486
  function createReadTool(context) {
476
487
  return {
477
488
  name: "read",
@@ -516,15 +527,19 @@ function createReadTool(context) {
516
527
  }
517
528
  const bytes = await readFile2(resolved.absolutePath);
518
529
  const source = new TextDecoder().decode(bytes);
519
- const selected = readTextLines(source, parsed.offset, parsed.limit);
520
- const selectedBytes = new TextEncoder().encode(selected);
521
- const sliced = selectedBytes.subarray(0, maxBytes);
522
- const text = new TextDecoder().decode(sliced);
530
+ const sourceLines = source.split(/\r\n|\n|\r/);
531
+ const requestedOffset = parsed.offset ?? 1;
532
+ const requestedLimit = parsed.limit ?? context.maxReadLines;
533
+ const selected = readTextLines(source, requestedOffset, requestedLimit);
534
+ const preview = boundedPreview(selected, maxBytes);
535
+ const text = preview.text;
536
+ const shownLines = text.split(/\r\n|\n|\r/).length;
537
+ const selectedLineEnd = Math.min(sourceLines.length, requestedOffset + shownLines - 1);
523
538
  const resourceType = detectResourceType(resolved.absolutePath);
524
539
  let name;
525
540
  let baseDir;
526
541
  if (resourceType === "skill" || resourceType === "phase") {
527
- const { frontmatter } = parseFrontmatter(text);
542
+ const { frontmatter } = parseFrontmatter(source);
528
543
  const marker = resourceType === "skill" ? "SKILL.md" : "PHASE.md";
529
544
  name = frontmatter.name ?? inferResourceName(resolved.absolutePath, marker);
530
545
  baseDir = dirname2(resolved.absolutePath);
@@ -532,8 +547,19 @@ function createReadTool(context) {
532
547
  name = inferResourceName(resolved.absolutePath, basename2(resolved.absolutePath));
533
548
  }
534
549
  const formatted = resourceType === "skill" || resourceType === "phase" ? formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: text, baseDir }) : text;
535
- const content = selectedBytes.byteLength > maxBytes ? `${formatted}
550
+ const fullFormatted = resourceType === "skill" || resourceType === "phase" ? formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: source, baseDir }) : source;
551
+ const lineTruncated = parsed.limit === void 0 && requestedOffset + requestedLimit - 1 < sourceLines.length;
552
+ const truncated = preview.truncated || lineTruncated;
553
+ let content = truncated ? `${formatted}
536
554
  [truncated]` : formatted;
555
+ const archivePath = await spillResult(context, "read", fullFormatted);
556
+ content += `
557
+ Full result: ${archivePath}
558
+ Offset: 0`;
559
+ if (lineTruncated) {
560
+ content += `
561
+ Showing lines ${requestedOffset}-${selectedLineEnd} of ${sourceLines.length}. Use offset=${selectedLineEnd + 1} to continue.`;
562
+ }
537
563
  return toolResult({
538
564
  context: toolContext,
539
565
  toolName: "read",
@@ -681,8 +707,13 @@ function createBashTool(context) {
681
707
  ]);
682
708
  const ok = exitCode === 0 && !timedOut && !aborted;
683
709
  const output = [stdout.text, stderr.text].filter(Boolean).join(stdout.text && stderr.text ? "\n" : "");
684
- const content = stdout.truncated || stderr.truncated ? `${output}
710
+ const preview = boundedPreview(output, maxOutputBytes);
711
+ let content = preview.truncated ? `${preview.text}
685
712
  [truncated]` : output;
713
+ const archivePath = await spillResult(context, "bash", output);
714
+ content += `
715
+ Full result: ${archivePath}
716
+ Offset: 0`;
686
717
  return toolResult({
687
718
  context: toolContext,
688
719
  toolName: "bash",
@@ -709,7 +740,24 @@ function createCoreTools(input = {}) {
709
740
  ];
710
741
  }
711
742
 
743
+ // src/runtime/core-tools.ts
744
+ function createRuntimeCoreTools(input = {}) {
745
+ return createCoreTools(input).map((tool) => ({
746
+ name: tool.name,
747
+ core: true,
748
+ description: tool.description,
749
+ parameters: tool.parameters,
750
+ execute: async (args, context, signal) => {
751
+ const result = await tool.execute(args, { skills: [], toolCallId: context.toolCallId }, signal);
752
+ return result.ok ? { ok: true, content: result.content } : { ok: false, content: result.content, error: result.error ?? "Tool failed." };
753
+ }
754
+ }));
755
+ }
756
+
712
757
  // src/runtime/durable-runtime.ts
758
+ import { mkdir as mkdir2, chmod as chmod2, writeFile as writeFile2 } from "fs/promises";
759
+ import { randomUUID } from "crypto";
760
+ import { join as join5 } from "path";
713
761
  import { createModelStream } from "@rowan-agent/models";
714
762
 
715
763
  // src/utils.ts
@@ -748,19 +796,6 @@ function createMessage(role, content, metadata) {
748
796
  ...metadata ? { metadata } : {}
749
797
  };
750
798
  }
751
- function messageContentText(content) {
752
- if (typeof content === "string") {
753
- return content;
754
- }
755
- return content.map((part) => {
756
- if (part.type === "text") return part.text;
757
- if (part.type === "thinking") return part.thinking;
758
- if (part.type === "tool_result") return part.content;
759
- if (part.type === "tool_use") return JSON.stringify(part.input);
760
- if (part.type === "image") return `[image:${part.mimeType}]`;
761
- return "";
762
- }).filter(Boolean).join("\n");
763
- }
764
799
  function contentBlocksToMessageContent(blocks) {
765
800
  return blocks.map((block) => {
766
801
  if (block.type === "text") {
@@ -822,7 +857,8 @@ async function invokeModel(input) {
822
857
  config: input.config,
823
858
  message: input.message,
824
859
  events,
825
- metadataPhase: input.phaseId
860
+ metadataPhase: input.phaseId,
861
+ output: input.output
826
862
  });
827
863
  const response = result.doneResponse ?? {
828
864
  content: result.text,
@@ -845,6 +881,7 @@ async function invokeModel(input) {
845
881
  };
846
882
  }
847
883
  async function collectStreamResult(input) {
884
+ const persistAssistant = input.output !== "internal";
848
885
  let activeMessageId;
849
886
  let lastPartial;
850
887
  let stopReason;
@@ -867,7 +904,7 @@ async function collectStreamResult(input) {
867
904
  }
868
905
  const abortResult2 = LoopGuard.checkAbort(input.config.signal);
869
906
  if (abortResult2.stopReason !== "none") {
870
- if (activeMessageId) {
907
+ if (persistAssistant && activeMessageId) {
871
908
  await input.message.end(activeMessageId);
872
909
  activeMessageId = void 0;
873
910
  }
@@ -878,23 +915,27 @@ async function collectStreamResult(input) {
878
915
  }
879
916
  if (event.type === "start") {
880
917
  lastPartial = event.partial;
881
- activeMessageId ??= input.message.reserve("assistant", {
882
- phase: input.metadataPhase
883
- });
918
+ if (persistAssistant) {
919
+ activeMessageId ??= input.message.reserve("assistant", {
920
+ phase: input.metadataPhase
921
+ });
922
+ }
884
923
  }
885
924
  if (event.type === "text_delta") {
886
925
  lastPartial = event.partial;
887
- if (!activeMessageId) {
926
+ if (persistAssistant && !activeMessageId) {
888
927
  activeMessageId = input.message.reserve("assistant", {
889
928
  phase: input.metadataPhase
890
929
  });
891
930
  }
892
- await input.message.update(activeMessageId, event.text);
931
+ if (persistAssistant && activeMessageId) await input.message.update(activeMessageId, event.text);
893
932
  }
894
933
  if (event.type === "tool_call_start" || event.type === "tool_call_delta" || event.type === "tool_call_end") {
895
- activeMessageId ??= input.message.reserve("assistant", {
896
- phase: input.metadataPhase
897
- });
934
+ if (persistAssistant) {
935
+ activeMessageId ??= input.message.reserve("assistant", {
936
+ phase: input.metadataPhase
937
+ });
938
+ }
898
939
  lastPartial = event.partial;
899
940
  }
900
941
  if (event.type === "thinking_delta") {
@@ -906,10 +947,10 @@ async function collectStreamResult(input) {
906
947
  const contentBlocks2 = lastPartial?.contentBlocks ?? [];
907
948
  const hasContent2 = contentBlocks2.length > 0 || (event.response?.content?.length ?? 0) > 0 || (event.response?.toolCalls?.length ?? 0) > 0;
908
949
  const shouldStoreContentParts = contentBlocks2.some((block) => block.type !== "text");
909
- if (activeMessageId && hasContent2 && shouldStoreContentParts) {
950
+ if (persistAssistant && activeMessageId && hasContent2 && shouldStoreContentParts) {
910
951
  input.message.replaceContent(activeMessageId, contentBlocksToMessageContent(contentBlocks2));
911
952
  }
912
- if (activeMessageId) {
953
+ if (persistAssistant && activeMessageId) {
913
954
  if (hasContent2) await input.message.end(activeMessageId);
914
955
  else input.message.discard(activeMessageId);
915
956
  activeMessageId = void 0;
@@ -918,13 +959,13 @@ async function collectStreamResult(input) {
918
959
  }
919
960
  const abortResult = LoopGuard.checkAbort(input.config.signal);
920
961
  if (abortResult.stopReason !== "none") {
921
- if (activeMessageId) {
962
+ if (persistAssistant && activeMessageId) {
922
963
  await input.message.end(activeMessageId);
923
964
  activeMessageId = void 0;
924
965
  }
925
966
  return { text: abortResult.message, contentBlocks: [], toolCalls: [], stopReason: "aborted" };
926
967
  }
927
- if (activeMessageId) {
968
+ if (persistAssistant && activeMessageId) {
928
969
  await input.message.end(activeMessageId);
929
970
  }
930
971
  const hasContent = (lastPartial?.contentBlocks?.length ?? 0) > 0 || (doneResponse?.content?.length ?? 0) > 0 || (doneResponse?.toolCalls?.length ?? 0) > 0;
@@ -1052,6 +1093,178 @@ function createExtensionAPI(hooks, options, runtime, eventBus) {
1052
1093
  };
1053
1094
  }
1054
1095
 
1096
+ // src/runtime/json.ts
1097
+ function isPlainObject(value) {
1098
+ const prototype = Object.getPrototypeOf(value);
1099
+ return prototype === Object.prototype || prototype === null;
1100
+ }
1101
+ function hasOnlyDataProperties(value) {
1102
+ return Reflect.ownKeys(value).every((key) => {
1103
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1104
+ return Boolean(descriptor && "value" in descriptor);
1105
+ });
1106
+ }
1107
+ function visit(value, seen) {
1108
+ if (value === null || typeof value === "boolean" || typeof value === "string") return true;
1109
+ if (typeof value === "number") return Number.isFinite(value);
1110
+ if (typeof value !== "object" || seen.has(value)) return false;
1111
+ seen.add(value);
1112
+ try {
1113
+ if (Array.isArray(value)) {
1114
+ if (Object.getPrototypeOf(value) !== Array.prototype || !hasOnlyDataProperties(value)) return false;
1115
+ if (Reflect.ownKeys(value).some((key) => key !== "length" && typeof key !== "string")) return false;
1116
+ for (let index = 0; index < value.length; index += 1) {
1117
+ if (!Object.prototype.hasOwnProperty.call(value, index) || !visit(value[index], seen)) return false;
1118
+ }
1119
+ return true;
1120
+ }
1121
+ if (!isPlainObject(value) || !hasOnlyDataProperties(value)) return false;
1122
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string")) return false;
1123
+ if (Object.prototype.hasOwnProperty.call(value, "toJSON")) return false;
1124
+ return Object.keys(value).every((key) => visit(value[key], seen));
1125
+ } finally {
1126
+ seen.delete(value);
1127
+ }
1128
+ }
1129
+ function isJsonValue(value) {
1130
+ return visit(value, /* @__PURE__ */ new Set());
1131
+ }
1132
+ function assertJsonValue(value, argument = "value") {
1133
+ if (!isJsonValue(value)) throw new TypeError(`${argument} must be JSON-safe`);
1134
+ }
1135
+ function canonicalJson(value) {
1136
+ assertJsonValue(value);
1137
+ const encode = (candidate) => {
1138
+ if (candidate === null || typeof candidate !== "object") return JSON.stringify(candidate);
1139
+ if (Array.isArray(candidate)) return `[${candidate.map(encode).join(",")}]`;
1140
+ const object = candidate;
1141
+ return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${encode(object[key])}`).join(",")}}`;
1142
+ };
1143
+ return encode(value);
1144
+ }
1145
+ function utf8ByteLength(value) {
1146
+ return new TextEncoder().encode(value).byteLength;
1147
+ }
1148
+ function assertUtf8ByteLimit(value, limit, argument = "value") {
1149
+ if (utf8ByteLength(value) > limit) throw new RangeError(`${argument} exceeds ${limit} UTF-8 bytes`);
1150
+ }
1151
+
1152
+ // src/harness/phases/interactions.ts
1153
+ var PhaseInteractionCancelledError = class extends Error {
1154
+ code = "phase_interaction_cancelled";
1155
+ constructor() {
1156
+ super("Phase interaction work was cancelled.");
1157
+ this.name = "PhaseInteractionCancelledError";
1158
+ }
1159
+ };
1160
+ var PhaseInteractionBoundary = class extends Error {
1161
+ constructor(state, interactions) {
1162
+ super("Phase interactions require host input.");
1163
+ this.state = state;
1164
+ this.interactions = interactions;
1165
+ this.name = "PhaseInteractionBoundary";
1166
+ }
1167
+ state;
1168
+ interactions;
1169
+ };
1170
+ function createPhaseInteractionDriver(state, phase, signal) {
1171
+ const stored = state.phaseInteractions;
1172
+ const requests = new Map(
1173
+ stored?.requests.map((request) => [request.id, { ...request }]) ?? []
1174
+ );
1175
+ const answers = new Map(Object.entries(stored?.answers ?? {}));
1176
+ const usedRequestIds = /* @__PURE__ */ new Set();
1177
+ let checkpoint = stored?.checkpoint;
1178
+ const snapshot = () => ({
1179
+ requests: [...requests.values()].map((request) => ({
1180
+ ...request,
1181
+ status: answers.has(request.id) ? "answered" : request.status
1182
+ })),
1183
+ answers: Object.fromEntries(answers),
1184
+ ...checkpoint === void 0 ? {} : { checkpoint }
1185
+ });
1186
+ const sync = () => {
1187
+ state.phaseInteractions = snapshot();
1188
+ };
1189
+ const assertActive = () => {
1190
+ if (signal?.aborted) throw new PhaseInteractionCancelledError();
1191
+ };
1192
+ const matchingStoredRequest = (input) => {
1193
+ const fingerprint = interactionFingerprint(input);
1194
+ return [...requests.values()].find((request) => !usedRequestIds.has(request.id) && interactionFingerprint(request) === fingerprint);
1195
+ };
1196
+ return {
1197
+ signal: signal ?? new AbortController().signal,
1198
+ request(input) {
1199
+ assertActive();
1200
+ if (input.id !== void 0 && input.id.trim().length === 0) {
1201
+ throw new TypeError("Phase interaction id must be non-empty.");
1202
+ }
1203
+ if (input.prompt.trim().length === 0) {
1204
+ throw new TypeError("Phase interaction prompt must be non-empty.");
1205
+ }
1206
+ if (!PHASE_INTERACTION_KINDS.includes(input.kind)) {
1207
+ throw new TypeError(`Unsupported Phase interaction kind: ${String(input.kind)}.`);
1208
+ }
1209
+ if (input.payload !== void 0) assertJsonValue(input.payload, "Phase interaction payload");
1210
+ const matched = input.id === void 0 ? matchingStoredRequest(input) : void 0;
1211
+ const id = input.id ?? matched?.id ?? createId("interaction");
1212
+ const existing = requests.get(id);
1213
+ if (existing) {
1214
+ if (interactionFingerprint(existing) !== interactionFingerprint(input)) {
1215
+ throw new Error(`Phase interaction ${id} was requested with different metadata.`);
1216
+ }
1217
+ usedRequestIds.add(id);
1218
+ return {
1219
+ ...existing,
1220
+ status: answers.has(id) ? "answered" : existing.status
1221
+ };
1222
+ }
1223
+ const request = {
1224
+ id,
1225
+ phase,
1226
+ kind: input.kind,
1227
+ prompt: input.prompt,
1228
+ ...input.payload === void 0 ? {} : { payload: input.payload },
1229
+ createdAt: createTimestamp(),
1230
+ status: "pending"
1231
+ };
1232
+ requests.set(id, request);
1233
+ usedRequestIds.add(id);
1234
+ sync();
1235
+ return request;
1236
+ },
1237
+ pending() {
1238
+ return [...requests.values()].filter((request) => request.status === "pending" && !answers.has(request.id)).map((request) => ({ ...request, status: "pending" }));
1239
+ },
1240
+ answers() {
1241
+ return new Map(answers);
1242
+ },
1243
+ suspend(input = {}) {
1244
+ assertActive();
1245
+ const pending = this.pending();
1246
+ if (pending.length === 0) throw new Error("Cannot suspend without pending Phase interactions.");
1247
+ if (input.checkpoint !== void 0) checkpoint = input.checkpoint;
1248
+ state.status = "suspended";
1249
+ sync();
1250
+ throw new PhaseInteractionBoundary(state, pending);
1251
+ }
1252
+ };
1253
+ }
1254
+ var PHASE_INTERACTION_KINDS = [
1255
+ "user_input",
1256
+ "permission",
1257
+ "elicitation",
1258
+ "confirmation"
1259
+ ];
1260
+ function interactionFingerprint(value) {
1261
+ return canonicalJson({
1262
+ kind: value.kind,
1263
+ prompt: value.prompt,
1264
+ payload: value.payload ?? null
1265
+ });
1266
+ }
1267
+
1055
1268
  // src/harness/phases/loader.ts
1056
1269
  import { existsSync as existsSync2, statSync as statSync2 } from "fs";
1057
1270
  import { readdir as readdir2 } from "fs/promises";
@@ -1145,7 +1358,7 @@ function optionalBundledSkills(value, field) {
1145
1358
  if (!Array.isArray(value)) throw new TypeError(`${field} must be an array of Skills`);
1146
1359
  const names = /* @__PURE__ */ new Set();
1147
1360
  for (const [index, item] of value.entries()) {
1148
- if (!isRecord(item) || typeof item.name !== "string" || item.name.trim() === "" || typeof item.description !== "string" || typeof item.filePath !== "string" || typeof item.baseDir !== "string" || typeof item.content !== "string" || typeof item.disableModelInvocation !== "boolean") {
1361
+ if (!isRecord(item) || typeof item.name !== "string" || item.name.trim() === "" || typeof item.description !== "string" || typeof item.filePath !== "string" || typeof item.baseDir !== "string" || typeof item.content !== "string" || item.disableAutoInvocation !== void 0 && typeof item.disableAutoInvocation !== "boolean" || item.disableImplicitInvocation !== void 0 && typeof item.disableImplicitInvocation !== "boolean") {
1149
1362
  throw new TypeError(`${field}[${index}] is invalid`);
1150
1363
  }
1151
1364
  if (names.has(item.name)) throw new TypeError(`Duplicate bundled Skill "${item.name}".`);
@@ -1284,7 +1497,9 @@ async function loadSkill(path) {
1284
1497
  filePath: resolved,
1285
1498
  baseDir: dirname3(resolved),
1286
1499
  content: body,
1287
- disableModelInvocation: metadata["disable-model-invocation"] === true
1500
+ disableAutoInvocation: metadata["disable-auto-invocation"] === true || metadata["disable-model-invocation"] === true,
1501
+ disableImplicitInvocation: metadata["disable-implicit-invocation"] === true,
1502
+ ...metadata["disable-model-invocation"] === true ? { disableModelInvocation: true } : {}
1288
1503
  };
1289
1504
  }
1290
1505
  async function loadSkills(targetPath) {
@@ -1376,7 +1591,9 @@ async function loadPhase(targetPath) {
1376
1591
  filePath: resolved,
1377
1592
  baseDir,
1378
1593
  content: definition.prompt,
1379
- model: definition.model
1594
+ model: definition.model,
1595
+ disableAutoInvocation: metadata["disable-auto-invocation"] === true,
1596
+ disableImplicitInvocation: metadata["disable-implicit-invocation"] === true
1380
1597
  };
1381
1598
  const codePath = await discoverPhaseCode(baseDir);
1382
1599
  if (codePath) {
@@ -1512,16 +1729,126 @@ async function loadPhaseCode(codePath) {
1512
1729
 
1513
1730
  // src/harness/phases/default.ts
1514
1731
  var DEFAULT_PHASE_ID = "default";
1732
+ var STOP_PHASE_ID = "stop";
1733
+ var COMPACT_PHASE_ID = "compact";
1515
1734
  function createDefaultPhase() {
1516
1735
  return {
1517
1736
  name: DEFAULT_PHASE_ID,
1518
- description: "Executes concrete task operations and produces artifacts.",
1737
+ core: true,
1738
+ description: "Execute the current user request using the current context.",
1519
1739
  filePath: "",
1520
1740
  baseDir: "",
1521
1741
  skills: [],
1522
- content: "Execute tasks using current context.\nNo planning. No evaluation.\nOmit route when more user input is needed. Use route for immediate phase execution, or route(stop) only when the current user request or task is complete and no further user input is needed."
1742
+ content: [
1743
+ "Execute the current user request using the current context.",
1744
+ "Must not plan or evaluate unless requested; use available tools only when needed.",
1745
+ "If more user input is needed, omit route. If the request is complete, route(stop) as the only target."
1746
+ ].join("\n"),
1747
+ disableAutoInvocation: false,
1748
+ disableImplicitInvocation: true
1523
1749
  };
1524
1750
  }
1751
+ function createStopPhase() {
1752
+ return {
1753
+ name: STOP_PHASE_ID,
1754
+ core: true,
1755
+ description: "Provide the final user-facing conclusion for the completed task.",
1756
+ filePath: "",
1757
+ baseDir: "",
1758
+ skills: [],
1759
+ content: [
1760
+ "Return only a brief normal-exit explanation in the user's language (one or two sentences).",
1761
+ "Must use only facts from the conversation and completed work, and state that the current run ended normally.",
1762
+ "If no actionable work occurred, say so briefly. Never invent Backlog, Task, Context, blockers, or next steps.",
1763
+ "Do not greet, ask questions, mention routing, call tools, or route to another Phase."
1764
+ ].join("\n"),
1765
+ disableAutoInvocation: false,
1766
+ disableImplicitInvocation: false
1767
+ };
1768
+ }
1769
+ function createCompactPhase() {
1770
+ return {
1771
+ name: COMPACT_PHASE_ID,
1772
+ core: true,
1773
+ description: "Compact the complete conversation context into a durable summary.",
1774
+ filePath: "",
1775
+ baseDir: "",
1776
+ skills: [],
1777
+ content: "Summarize all historical conversation roles and tool activity for future context. Preserve durable archive references and important decisions.",
1778
+ disableAutoInvocation: true,
1779
+ disableImplicitInvocation: false,
1780
+ run: async (context, execution) => {
1781
+ await execution.reportStatus({
1782
+ state: "running",
1783
+ kind: "compacting",
1784
+ message: "Compacting conversation context."
1785
+ });
1786
+ const tools = context.tools.filter((tool) => tool.core && (tool.name === "read" || tool.name === "bash"));
1787
+ const working = { ...context, tools, skills: [...context.skills], messages: [...context.messages] };
1788
+ let result = await execution.invokeModel(working, { output: "internal" });
1789
+ for (let attempt = 0; attempt < 8 && result.toolCalls.length > 0; attempt += 1) {
1790
+ const calls = result.toolCalls.filter((call) => tools.some((tool) => tool.name === call.name));
1791
+ if (calls.length === 0) break;
1792
+ const normalizedCalls = calls.map((call) => ({
1793
+ ...call,
1794
+ args: typeof call.args === "string" ? (() => {
1795
+ try {
1796
+ return JSON.parse(call.args);
1797
+ } catch {
1798
+ return call.args;
1799
+ }
1800
+ })() : call.args
1801
+ }));
1802
+ const assistantContent = result.contentBlocks.length > 0 ? contentBlocksToMessageContent(result.contentBlocks) : normalizedCalls.map((call) => ({ type: "tool_use", id: call.id, name: call.name, input: call.args }));
1803
+ working.messages.push(createMessage("assistant", assistantContent, {
1804
+ kind: "phase_tool",
1805
+ phase: COMPACT_PHASE_ID
1806
+ }));
1807
+ for (const call of normalizedCalls) {
1808
+ const tool = tools.find((candidate) => candidate.name === call.name);
1809
+ let toolResult2;
1810
+ try {
1811
+ toolResult2 = await tool.execute(call.args, {
1812
+ skills: working.skills,
1813
+ toolCallId: call.id
1814
+ });
1815
+ } catch (error) {
1816
+ toolResult2 = {
1817
+ toolCallId: call.id,
1818
+ toolName: call.name,
1819
+ ok: false,
1820
+ content: null,
1821
+ error: error instanceof Error ? error.message : String(error)
1822
+ };
1823
+ }
1824
+ working.messages.push(createMessage("tool", [{
1825
+ type: "tool_result",
1826
+ toolUseId: call.id,
1827
+ content: JSON.stringify(toolResult2),
1828
+ isError: !toolResult2.ok
1829
+ }], {
1830
+ kind: "phase_tool",
1831
+ phase: COMPACT_PHASE_ID
1832
+ }));
1833
+ }
1834
+ result = await execution.invokeModel(working, { output: "internal" });
1835
+ }
1836
+ return {
1837
+ route: "stop",
1838
+ phase: COMPACT_PHASE_ID,
1839
+ payload: { kind: "context_compaction", summary: result.text },
1840
+ status: {
1841
+ state: "completed",
1842
+ kind: "compacted",
1843
+ message: "Conversation context compacted."
1844
+ }
1845
+ };
1846
+ }
1847
+ };
1848
+ }
1849
+ function createCorePhases() {
1850
+ return [createDefaultPhase(), createStopPhase(), createCompactPhase()];
1851
+ }
1525
1852
 
1526
1853
  // src/harness/resource-selection.ts
1527
1854
  function selectNamedResources(candidates, names, kind) {
@@ -1573,7 +1900,7 @@ function buildSystemPrompt(options) {
1573
1900
  addGuideline(g);
1574
1901
  }
1575
1902
  }
1576
- const visibleSkills = (skills ?? []).filter((s) => !s.disableModelInvocation);
1903
+ const visibleSkills = (skills ?? []).filter((s) => !(s.disableAutoInvocation ?? s.disableModelInvocation));
1577
1904
  const skillsBlock = visibleSkills.length > 0 ? buildSkillsDescription(visibleSkills) : "";
1578
1905
  for (const g of promptGuidelines ?? []) {
1579
1906
  addGuideline(g);
@@ -1609,6 +1936,7 @@ function serializeSkills(skills) {
1609
1936
  name: skill.name,
1610
1937
  description: skill.description,
1611
1938
  filePath: skill.filePath,
1939
+ disableAutoInvocation: skill.disableAutoInvocation,
1612
1940
  disableModelInvocation: skill.disableModelInvocation
1613
1941
  }));
1614
1942
  }
@@ -1731,80 +2059,9 @@ var createOutcome = {
1731
2059
  }
1732
2060
  };
1733
2061
 
1734
- // src/harness/context/compaction.ts
1735
- function needsCompaction(messages, options = {}) {
1736
- const maxMessages = options.maxMessages ?? 50;
1737
- return messages.length > maxMessages;
1738
- }
1739
- function buildSummary(messages) {
1740
- const parts = [];
1741
- let currentRole;
1742
- let currentContent = [];
1743
- function flush() {
1744
- if (currentRole && currentContent.length > 0) {
1745
- const combined = currentContent.join("\n");
1746
- const truncated = combined.length > 500 ? combined.slice(0, 500) + "..." : combined;
1747
- parts.push(`[${currentRole}]: ${truncated}`);
1748
- }
1749
- currentContent = [];
1750
- }
1751
- for (const msg of messages) {
1752
- if (msg.role !== currentRole) {
1753
- flush();
1754
- currentRole = msg.role;
1755
- }
1756
- currentContent.push(messageContentText(msg.content));
1757
- }
1758
- flush();
1759
- return parts.join("\n\n");
1760
- }
1761
- function compactMessages(messages, options = {}) {
1762
- if (!needsCompaction(messages, options)) {
1763
- return { compacted: false, messages };
1764
- }
1765
- const keepRecent = options.keepRecent ?? 10;
1766
- const minCompact = options.minCompact ?? 20;
1767
- const firstUserIdx = messages.findIndex(
1768
- (m) => m.role === "user"
1769
- );
1770
- const recentStart = Math.max(messages.length - keepRecent, firstUserIdx + 1);
1771
- const oldMessages = messages.slice(firstUserIdx >= 0 ? firstUserIdx + 1 : 0, recentStart);
1772
- const protectedPhaseMessages = oldMessages.filter(
1773
- (message) => message.metadata?.kind === "phase_prompt"
1774
- );
1775
- const summarizableMessages = oldMessages.filter(
1776
- (message) => message.metadata?.kind !== "phase_prompt"
1777
- );
1778
- if (summarizableMessages.length < minCompact) {
1779
- return { compacted: false, messages };
1780
- }
1781
- const recentMessages = messages.slice(recentStart);
1782
- const summary = buildSummary(summarizableMessages);
1783
- const result = [];
1784
- if (firstUserIdx >= 0) {
1785
- result.push(messages[firstUserIdx]);
1786
- }
1787
- result.push(
1788
- createMessage("assistant", `[Context compaction summary]
1789
-
1790
- ${summary}`, {
1791
- type: "compaction_summary",
1792
- compactedCount: summarizableMessages.length
1793
- })
1794
- );
1795
- result.push(...protectedPhaseMessages);
1796
- result.push(...recentMessages);
1797
- return {
1798
- compacted: true,
1799
- messages: result,
1800
- summarizedCount: summarizableMessages.length,
1801
- summary
1802
- };
1803
- }
1804
-
1805
2062
  // src/loop/runners.ts
1806
2063
  function resolvePhaseOutput(result) {
1807
- return result ? { ...result } : { message: "Phase completed.", route: "stop" };
2064
+ return result ?? { message: "Phase completed.", route: "stop" };
1808
2065
  }
1809
2066
  function removePhaseMessage(messages, msgId) {
1810
2067
  if (!msgId) return;
@@ -1920,12 +2177,18 @@ async function completeRun(config, state, outcome) {
1920
2177
  return createRunResult(config, state, outcome);
1921
2178
  }
1922
2179
  function createTerminalOutcome(phase, output, transcript) {
1923
- return phase.run || phase.factory ? createOutcome.phase(output, transcript) : createOutcome.default(output, transcript);
2180
+ const outcome = phase.run || phase.factory ? createOutcome.phase(output, transcript) : createOutcome.default(output, transcript);
2181
+ return phase.name === "compact" ? { ...outcome, display: false } : outcome;
2182
+ }
2183
+ function shouldRunStopPhase(phase, output) {
2184
+ if (phase.run || phase.factory) return false;
2185
+ return typeof output.message !== "string" || output.message.trim().length === 0;
1924
2186
  }
1925
2187
  function buildToolsWithRouting(config, availablePhases) {
1926
2188
  const tools = config.context.tools.filter((tool) => tool.name !== PhaseRouteTool);
1927
- if (availablePhases.length > 0) {
1928
- tools.push(createRouteTool(availablePhases));
2189
+ const autoInvocablePhases = availablePhases.filter((phase) => phase.disableAutoInvocation !== true);
2190
+ if (autoInvocablePhases.length > 0) {
2191
+ tools.push(createRouteTool(autoInvocablePhases));
1929
2192
  }
1930
2193
  return tools;
1931
2194
  }
@@ -2138,7 +2401,15 @@ async function runPhaseLoop(config, state, registry) {
2138
2401
  while (currentPhaseId) {
2139
2402
  const availablePhases = [];
2140
2403
  for (const [, phase2] of registry.phases) {
2141
- availablePhases.push({ name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
2404
+ availablePhases.push({
2405
+ name: phase2.name,
2406
+ description: phase2.description,
2407
+ tools: phase2.tools,
2408
+ skills: phase2.skills,
2409
+ input: phase2.input,
2410
+ isolated: phase2.isolated,
2411
+ disableAutoInvocation: phase2.disableAutoInvocation
2412
+ });
2142
2413
  }
2143
2414
  const abortResult = LoopGuard.checkAbort(config.signal);
2144
2415
  if (abortResult.stopReason !== "none") {
@@ -2162,13 +2433,6 @@ async function runPhaseLoop(config, state, registry) {
2162
2433
  }
2163
2434
  state.attempt++;
2164
2435
  state.metrics.iterations++;
2165
- if (needsCompaction(config.context.messages)) {
2166
- const compacted = compactMessages(config.context.messages);
2167
- if (compacted.compacted) {
2168
- config.context.messages = compacted.messages;
2169
- state.metrics.compactionCount++;
2170
- }
2171
- }
2172
2436
  const phase = registry.phases.get(currentPhaseId);
2173
2437
  if (!phase) {
2174
2438
  throw new Error(`Phase "${currentPhaseId}" not found`);
@@ -2293,10 +2557,20 @@ async function runPhaseLoop(config, state, registry) {
2293
2557
  }
2294
2558
  }
2295
2559
  }
2560
+ if (output.status) {
2561
+ await execution.reportStatus(output.status);
2562
+ }
2563
+ if (phase.name === STOP_PHASE_ID) {
2564
+ removePhaseMessage(config.context.messages, previousPhaseMsgId);
2565
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2566
+ previousPhaseMsgId = void 0;
2567
+ previousPhaseInputMsgId = void 0;
2568
+ return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
2569
+ }
2296
2570
  if (phase.run || phase.factory) {
2297
2571
  if (output.route === void 0 && !routeDecision) output = { ...output, route: "stop" };
2298
2572
  } else if (!phase.target && output.route === void 0 && !routeDecision) {
2299
- const input = await awaitInput(output.message, true);
2573
+ const input = await awaitInput(output.message ?? `${output.phase ?? "Phase"} completed.`, true);
2300
2574
  if (input?.type === "completed") return input.result;
2301
2575
  if (input?.type === "continued") {
2302
2576
  isContinuing = true;
@@ -2347,11 +2621,16 @@ async function runPhaseLoop(config, state, registry) {
2347
2621
  pendingInstruction = void 0;
2348
2622
  const entryPhaseId = phase.target ?? registry.entryPhaseId;
2349
2623
  if (entryPhaseId === "stop") {
2350
- removePhaseMessage(config.context.messages, previousPhaseMsgId);
2351
- removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2352
- previousPhaseMsgId = void 0;
2353
- previousPhaseInputMsgId = void 0;
2354
- return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
2624
+ if (!shouldRunStopPhase(phase, output)) {
2625
+ removePhaseMessage(config.context.messages, previousPhaseMsgId);
2626
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2627
+ previousPhaseMsgId = void 0;
2628
+ previousPhaseInputMsgId = void 0;
2629
+ return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
2630
+ }
2631
+ currentPhaseId = STOP_PHASE_ID;
2632
+ previousPayload = void 0;
2633
+ continue;
2355
2634
  }
2356
2635
  currentPhaseId = entryPhaseId;
2357
2636
  previousPayload = void 0;
@@ -2366,11 +2645,18 @@ async function runPhaseLoop(config, state, registry) {
2366
2645
  nextRoute = "stop";
2367
2646
  }
2368
2647
  if (nextRoute === "stop") {
2369
- removePhaseMessage(config.context.messages, previousPhaseMsgId);
2370
- removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2371
- previousPhaseMsgId = void 0;
2372
- previousPhaseInputMsgId = void 0;
2373
- return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
2648
+ if (!shouldRunStopPhase(phase, output)) {
2649
+ removePhaseMessage(config.context.messages, previousPhaseMsgId);
2650
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2651
+ previousPhaseMsgId = void 0;
2652
+ previousPhaseInputMsgId = void 0;
2653
+ return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
2654
+ }
2655
+ currentPhaseId = STOP_PHASE_ID;
2656
+ previousPayload = output.payload;
2657
+ previousResults = output.payload !== void 0 ? [{ name: phase.name, output: output.payload }] : [];
2658
+ pendingInstruction = phase.target ? void 0 : routeDecision?.instruction;
2659
+ continue;
2374
2660
  }
2375
2661
  const targetPhaseId = nextRoute;
2376
2662
  if (!registry.phases.has(targetPhaseId)) {
@@ -2434,7 +2720,7 @@ async function withRetry(fn, options = {}) {
2434
2720
  }
2435
2721
  const delayMs = getRetryDelay(attempt, baseDelayMs, maxDelayMs);
2436
2722
  options.onRetry?.(attempt, error, delayMs);
2437
- await new Promise((resolve8) => setTimeout(resolve8, delayMs));
2723
+ await new Promise((resolve9) => setTimeout(resolve9, delayMs));
2438
2724
  if (options.signal?.aborted) {
2439
2725
  throw lastError;
2440
2726
  }
@@ -2539,7 +2825,9 @@ async function executeToolCalls(input) {
2539
2825
  return results;
2540
2826
  }
2541
2827
  function createPhaseExecution(config, state, phase, messageManager, toolExecutionManager, registry) {
2828
+ const interaction = createPhaseInteractionDriver(state, phase.name, config.signal);
2542
2829
  return {
2830
+ interaction,
2543
2831
  snapshot() {
2544
2832
  return {
2545
2833
  systemPrompt: config.context.systemPrompt,
@@ -2556,7 +2844,7 @@ function createPhaseExecution(config, state, phase, messageManager, toolExecutio
2556
2844
  state.currentPhase = snapshot.currentPhase;
2557
2845
  state.metrics.iterations = snapshot.turnNumber;
2558
2846
  },
2559
- async invokeModel(phaseContext) {
2847
+ async invokeModel(phaseContext, options = {}) {
2560
2848
  if (config.beforePrompt) {
2561
2849
  phaseContext = await config.beforePrompt(phase.name, phaseContext);
2562
2850
  }
@@ -2586,7 +2874,8 @@ function createPhaseExecution(config, state, phase, messageManager, toolExecutio
2586
2874
  config,
2587
2875
  message: messageManager,
2588
2876
  request,
2589
- phaseId: phase.name
2877
+ phaseId: phase.name,
2878
+ output: options.output
2590
2879
  }),
2591
2880
  {
2592
2881
  signal: config.signal,
@@ -2599,6 +2888,9 @@ function createPhaseExecution(config, state, phase, messageManager, toolExecutio
2599
2888
  await config.onModelTranscript?.(result.transcript, { phase: phase.name, model: phase.model ?? config.model });
2600
2889
  return result;
2601
2890
  },
2891
+ async reportStatus(status) {
2892
+ await config.onPhaseStatus?.(phase.name, status);
2893
+ },
2602
2894
  async executeTool(phaseContext, toolCall) {
2603
2895
  return runTurn(async () => {
2604
2896
  await toolExecutionManager.start(toolCall.id, toolCall.name, toolCall.args);
@@ -2688,12 +2980,13 @@ async function executeParallelPhase(config, state, registry, phase, payload, ins
2688
2980
  if (phaseMsgId) removePhaseMessage(messages, phaseMsgId);
2689
2981
  const decision = output.toolCalls ? extractRouteCall(output.toolCalls) : void 0;
2690
2982
  const resultPayload = decision?.decision[0]?.payload !== void 0 ? normalizePayload(decision.decision[0].payload) : output.payload;
2691
- return { instanceId, phaseId: phase.name, payload: resultPayload, content: output.message };
2983
+ return { instanceId, phaseId: phase.name, payload: resultPayload, content: output.message ?? `${phase.name} completed.` };
2692
2984
  }
2693
2985
  function selectPhaseTools(candidates, names, includeRouting = true) {
2694
2986
  const routing = includeRouting ? candidates.filter(({ name }) => name === PhaseRouteTool) : [];
2695
- const ordinary = candidates.filter(({ name }) => name !== PhaseRouteTool);
2696
- return [...selectNamedResources(ordinary, names, "Tool"), ...routing];
2987
+ const core = candidates.filter(({ core: core2 }) => core2);
2988
+ const ordinary = candidates.filter(({ name, core: core2 }) => name !== PhaseRouteTool && !core2);
2989
+ return [...core, ...selectNamedResources(ordinary, names, "Tool"), ...routing];
2697
2990
  }
2698
2991
  async function waitForBackgroundTasks(backgroundTasks) {
2699
2992
  const entries = Array.from(backgroundTasks.entries());
@@ -2709,62 +3002,6 @@ async function waitForBackgroundTasks(backgroundTasks) {
2709
3002
  return successful;
2710
3003
  }
2711
3004
 
2712
- // src/runtime/json.ts
2713
- function isPlainObject(value) {
2714
- const prototype = Object.getPrototypeOf(value);
2715
- return prototype === Object.prototype || prototype === null;
2716
- }
2717
- function hasOnlyDataProperties(value) {
2718
- return Reflect.ownKeys(value).every((key) => {
2719
- const descriptor = Object.getOwnPropertyDescriptor(value, key);
2720
- return Boolean(descriptor && "value" in descriptor);
2721
- });
2722
- }
2723
- function visit(value, seen) {
2724
- if (value === null || typeof value === "boolean" || typeof value === "string") return true;
2725
- if (typeof value === "number") return Number.isFinite(value);
2726
- if (typeof value !== "object" || seen.has(value)) return false;
2727
- seen.add(value);
2728
- try {
2729
- if (Array.isArray(value)) {
2730
- if (Object.getPrototypeOf(value) !== Array.prototype || !hasOnlyDataProperties(value)) return false;
2731
- if (Reflect.ownKeys(value).some((key) => key !== "length" && typeof key !== "string")) return false;
2732
- for (let index = 0; index < value.length; index += 1) {
2733
- if (!Object.prototype.hasOwnProperty.call(value, index) || !visit(value[index], seen)) return false;
2734
- }
2735
- return true;
2736
- }
2737
- if (!isPlainObject(value) || !hasOnlyDataProperties(value)) return false;
2738
- if (Reflect.ownKeys(value).some((key) => typeof key !== "string")) return false;
2739
- if (Object.prototype.hasOwnProperty.call(value, "toJSON")) return false;
2740
- return Object.keys(value).every((key) => visit(value[key], seen));
2741
- } finally {
2742
- seen.delete(value);
2743
- }
2744
- }
2745
- function isJsonValue(value) {
2746
- return visit(value, /* @__PURE__ */ new Set());
2747
- }
2748
- function assertJsonValue(value, argument = "value") {
2749
- if (!isJsonValue(value)) throw new TypeError(`${argument} must be JSON-safe`);
2750
- }
2751
- function canonicalJson(value) {
2752
- assertJsonValue(value);
2753
- const encode = (candidate) => {
2754
- if (candidate === null || typeof candidate !== "object") return JSON.stringify(candidate);
2755
- if (Array.isArray(candidate)) return `[${candidate.map(encode).join(",")}]`;
2756
- const object = candidate;
2757
- return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${encode(object[key])}`).join(",")}}`;
2758
- };
2759
- return encode(value);
2760
- }
2761
- function utf8ByteLength(value) {
2762
- return new TextEncoder().encode(value).byteLength;
2763
- }
2764
- function assertUtf8ByteLimit(value, limit, argument = "value") {
2765
- if (utf8ByteLength(value) > limit) throw new RangeError(`${argument} exceeds ${limit} UTF-8 bytes`);
2766
- }
2767
-
2768
3005
  // src/runtime/execution.ts
2769
3006
  var EXECUTION_CHECKPOINT_CODEC = "rowan.agent.execution";
2770
3007
  var EXECUTION_CHECKPOINT_VERSION = 1;
@@ -2813,7 +3050,8 @@ function encodeExecutionCheckpoint(state) {
2813
3050
  currentPhase: state.currentPhase,
2814
3051
  attempt: state.attempt,
2815
3052
  metrics,
2816
- ...continuation ? { continuation } : {}
3053
+ ...continuation ? { continuation } : {},
3054
+ ...state.phaseInteractions === void 0 ? {} : { phaseInteractions: state.phaseInteractions }
2817
3055
  };
2818
3056
  assertJsonValue(data, "execution checkpoint");
2819
3057
  return {
@@ -2844,6 +3082,13 @@ function decodeExecutionCheckpoint(checkpoint) {
2844
3082
  ...checkpoint.data.continuation,
2845
3083
  previousResults: checkpoint.data.continuation.previousResults.map((result) => ({ ...result }))
2846
3084
  }
3085
+ } : {},
3086
+ ...checkpoint.data.phaseInteractions ? {
3087
+ phaseInteractions: {
3088
+ requests: checkpoint.data.phaseInteractions.requests.map((request) => ({ ...request })),
3089
+ answers: { ...checkpoint.data.phaseInteractions.answers },
3090
+ ...checkpoint.data.phaseInteractions.checkpoint === void 0 ? {} : { checkpoint: checkpoint.data.phaseInteractions.checkpoint }
3091
+ }
2847
3092
  } : {}
2848
3093
  };
2849
3094
  }
@@ -2869,6 +3114,16 @@ async function executeOnce(input) {
2869
3114
  status: "running",
2870
3115
  metrics: createMetrics()
2871
3116
  };
3117
+ if (input.interactionAnswers) {
3118
+ state.phaseInteractions = {
3119
+ requests: state.phaseInteractions?.requests ?? [],
3120
+ answers: {
3121
+ ...state.phaseInteractions?.answers ?? {},
3122
+ ...input.interactionAnswers
3123
+ },
3124
+ ...state.phaseInteractions?.checkpoint === void 0 ? {} : { checkpoint: state.phaseInteractions.checkpoint }
3125
+ };
3126
+ }
2872
3127
  const config = {
2873
3128
  context,
2874
3129
  execution: input.execution,
@@ -2886,6 +3141,7 @@ async function executeOnce(input) {
2886
3141
  onMessageDelta: input.onMessageDelta,
2887
3142
  onOutcome: input.onOutcome,
2888
3143
  onModelTranscript: input.onModelTranscript,
3144
+ onPhaseStatus: input.onPhaseStatus,
2889
3145
  runtime: input.runtime,
2890
3146
  waitForInput: async (nextState, request) => {
2891
3147
  if (!nextState || !request) throw new Error("Input boundary did not include execution state.");
@@ -2904,6 +3160,20 @@ async function executeOnce(input) {
2904
3160
  return {
2905
3161
  type: "input_required",
2906
3162
  request: error.request,
3163
+ interactions: [],
3164
+ checkpoint: encodeExecutionCheckpoint(error.state),
3165
+ messages: snapshotMessages(context.messages)
3166
+ };
3167
+ }
3168
+ if (error instanceof PhaseInteractionBoundary) {
3169
+ return {
3170
+ type: "input_required",
3171
+ request: {
3172
+ phase: error.interactions[0]?.phase ?? state.currentPhase,
3173
+ prompt: error.interactions.map((interaction) => interaction.prompt).join("\n"),
3174
+ requestedAt: error.interactions[0]?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
3175
+ },
3176
+ interactions: error.interactions,
2907
3177
  checkpoint: encodeExecutionCheckpoint(error.state),
2908
3178
  messages: snapshotMessages(context.messages)
2909
3179
  };
@@ -2933,8 +3203,15 @@ function isCheckpointData(value) {
2933
3203
  if (!isRecord2(value) || typeof value.currentPhase !== "string" || !Number.isInteger(value.attempt)) return false;
2934
3204
  if (!isMetrics(value.metrics)) return false;
2935
3205
  if (value.continuation !== void 0 && !isContinuation(value.continuation)) return false;
3206
+ if (value.phaseInteractions !== void 0 && !isPhaseInteractionState(value.phaseInteractions)) return false;
2936
3207
  return true;
2937
3208
  }
3209
+ function isPhaseInteractionState(value) {
3210
+ if (!isRecord2(value) || !Array.isArray(value.requests) || !isRecord2(value.answers)) return false;
3211
+ if (!Object.values(value.answers).every((answer) => isJsonValue(answer))) return false;
3212
+ if (value.checkpoint !== void 0 && !isJsonValue(value.checkpoint)) return false;
3213
+ return value.requests.every((request) => isRecord2(request) && typeof request.id === "string" && typeof request.phase === "string" && ["user_input", "permission", "elicitation", "confirmation"].includes(request.kind) && typeof request.prompt === "string" && typeof request.createdAt === "string" && ["pending", "answered", "denied", "cancelled", "expired"].includes(request.status) && (request.payload === void 0 || isJsonValue(request.payload)));
3214
+ }
2938
3215
  function isMetrics(value) {
2939
3216
  if (!isRecord2(value)) return false;
2940
3217
  return Number.isInteger(value.iterations) && Array.isArray(value.phaseTransitions) && value.phaseTransitions.every((transition) => isRecord2(transition) && typeof transition.from === "string" && typeof transition.to === "string" && typeof transition.ts === "string") && Number.isInteger(value.compactionCount) && Number.isInteger(value.retryCount) && typeof value.startedAt === "string" && Number.isFinite(value.startedAtMs) && (value.endedAt === void 0 || typeof value.endedAt === "string") && (value.durationMs === void 0 || Number.isFinite(value.durationMs));
@@ -3605,6 +3882,7 @@ function projectToolContent(content) {
3605
3882
  function projectTool(tool, agentId, runId) {
3606
3883
  return {
3607
3884
  name: tool.name,
3885
+ ...tool.core ? { core: true } : {},
3608
3886
  description: tool.description,
3609
3887
  parameters: tool.parameters,
3610
3888
  execute: async (args, context, signal) => {
@@ -3623,9 +3901,17 @@ function jsonText(value) {
3623
3901
  }
3624
3902
 
3625
3903
  // src/runtime/extensions.ts
3626
- function assembleRegisteredExtensions(config, runner) {
3904
+ function assembleRegisteredExtensions(config, runner, options = {}) {
3627
3905
  const extensionTools = runner.getAllRegisteredTools().map(adaptExtensionTool);
3628
- const tools = [...config.resources.tools];
3906
+ const coreTools = createRuntimeCoreTools({
3907
+ root: config.cwd,
3908
+ ...options.toolArchiveDir ? { archiveDir: options.toolArchiveDir } : {}
3909
+ });
3910
+ const coreNames = new Set(coreTools.map((tool) => tool.name));
3911
+ const tools = [
3912
+ ...config.resources.tools.filter((tool) => !coreNames.has(tool.name)),
3913
+ ...coreTools
3914
+ ];
3629
3915
  const names = new Set(tools.map((tool) => tool.name));
3630
3916
  for (const tool of extensionTools) {
3631
3917
  if (names.has(tool.name)) throw new TypeError(`Extension Tool collides with Context Tool "${tool.name}"`);
@@ -3656,11 +3942,13 @@ function assembleRegisteredExtensions(config, runner) {
3656
3942
  };
3657
3943
  }
3658
3944
  function resolveDefinitionContext(config, assembled = {}) {
3659
- const tools = selectNamedResources(
3660
- assembled.tools ?? config.resources.tools,
3661
- config.definition.tools,
3662
- "Tool"
3663
- );
3945
+ const candidateTools = assembled.tools ?? config.resources.tools;
3946
+ const coreTools = candidateTools.filter((tool) => tool.core);
3947
+ const authoredTools = candidateTools.filter((tool) => !tool.core);
3948
+ const tools = [
3949
+ ...selectNamedResources(authoredTools, config.definition.tools, "Tool"),
3950
+ ...coreTools
3951
+ ];
3664
3952
  const skills = mergeSkills(
3665
3953
  selectNamedResources(config.resources.skills, config.definition.skills, "Skill"),
3666
3954
  config.definition.bundledSkills
@@ -3671,11 +3959,16 @@ function resolveDefinitionContext(config, assembled = {}) {
3671
3959
  "Context"
3672
3960
  );
3673
3961
  const candidateRegistry = assembled.phases ?? config.resources.phases;
3674
- const selectedPhases = selectNamedResources(
3675
- [...candidateRegistry?.phases.values() ?? []],
3676
- config.definition.phases?.phaseIds,
3677
- "Phase"
3678
- );
3962
+ const phaseCandidates = [...candidateRegistry?.phases.values() ?? []];
3963
+ const coreNames = /* @__PURE__ */ new Set([DEFAULT_PHASE_ID, STOP_PHASE_ID, COMPACT_PHASE_ID]);
3964
+ const selectedPhases = [
3965
+ ...phaseCandidates.filter((phase) => phase.core || coreNames.has(phase.name)),
3966
+ ...selectNamedResources(
3967
+ phaseCandidates.filter((phase) => !phase.core && !coreNames.has(phase.name)),
3968
+ config.definition.phases?.phaseIds,
3969
+ "Phase"
3970
+ )
3971
+ ];
3679
3972
  const phases = new Map(selectedPhases.map((phase) => [phase.name, phase]));
3680
3973
  const requestedEntry = config.definition.phases ? config.definition.phases.entryPhaseId : candidateRegistry?.entryPhaseId ?? null;
3681
3974
  const entryPhaseId = requestedEntry === DEFAULT_PHASE_ID ? DEFAULT_PHASE_ID : requestedEntry && phases.has(requestedEntry) ? requestedEntry : null;
@@ -3690,15 +3983,23 @@ function resolveDefinitionContext(config, assembled = {}) {
3690
3983
  };
3691
3984
  }
3692
3985
  function mergePhases(base, extension) {
3693
- if (!base && extension.phases.size === 0) return void 0;
3694
- const phases = new Map(base?.phases ?? []);
3986
+ const core = createCorePhases();
3987
+ const coreNames = new Set(core.map(({ name }) => name));
3988
+ const phases = new Map(core.map((phase) => [phase.name, phase]));
3989
+ for (const [name, phase] of base?.phases ?? []) {
3990
+ if (coreNames.has(name)) {
3991
+ if (!phase.core) throw new TypeError(`Configured Phase collides with Rowan built-in Phase "${name}".`);
3992
+ continue;
3993
+ }
3994
+ phases.set(name, phase);
3995
+ }
3695
3996
  for (const [name, phase] of extension.phases) {
3696
3997
  if (phases.has(name)) throw new TypeError(`Extension Phase collides with Context Phase "${name}"`);
3697
3998
  phases.set(name, phase);
3698
3999
  }
3699
4000
  return {
3700
4001
  phases,
3701
- entryPhaseId: base?.entryPhaseId ?? extension.entryPhaseId
4002
+ entryPhaseId: base?.entryPhaseId ?? extension.entryPhaseId ?? DEFAULT_PHASE_ID
3702
4003
  };
3703
4004
  }
3704
4005
  function adaptExtensionTool(input) {
@@ -3731,11 +4032,11 @@ function fromLoopResult(result) {
3731
4032
  var MAX_BUFFERED_EVENTS = 128;
3732
4033
  var MAX_BUFFERED_TEXT = 64 * 1024;
3733
4034
  function deferred() {
3734
- let resolve8;
4035
+ let resolve9;
3735
4036
  const promise = new Promise((done) => {
3736
- resolve8 = done;
4037
+ resolve9 = done;
3737
4038
  });
3738
- return { promise, resolve: resolve8 };
4039
+ return { promise, resolve: resolve9 };
3739
4040
  }
3740
4041
  var TransientRunEventSubscription = class {
3741
4042
  queue = [];
@@ -3760,11 +4061,14 @@ var TransientRunEventSubscription = class {
3760
4061
  shift() {
3761
4062
  return this.queue.shift();
3762
4063
  }
3763
- clear(executionId) {
4064
+ clear(executionId, preserveCompletedPhaseStatus = false) {
3764
4065
  if (executionId === void 0) this.queue.length = 0;
3765
4066
  else {
3766
4067
  for (let index = this.queue.length - 1; index >= 0; index -= 1) {
3767
- if (this.queue[index]?.executionId === executionId) this.queue.splice(index, 1);
4068
+ const event = this.queue[index];
4069
+ if (event?.executionId === executionId && !(preserveCompletedPhaseStatus && event.kind === "phase_status" && event.status.state === "completed")) {
4070
+ this.queue.splice(index, 1);
4071
+ }
3768
4072
  }
3769
4073
  }
3770
4074
  this.notify();
@@ -3846,9 +4150,9 @@ var TransientRunEventHub = class {
3846
4150
  subscription.push(event);
3847
4151
  }
3848
4152
  }
3849
- clear(runId, executionId) {
4153
+ clear(runId, executionId, preserveCompletedPhaseStatus = false) {
3850
4154
  for (const subscription of this.subscriptions.get(runId) ?? []) {
3851
- subscription.clear(executionId);
4155
+ subscription.clear(executionId, preserveCompletedPhaseStatus);
3852
4156
  }
3853
4157
  }
3854
4158
  clearMessage(runId, messageId) {
@@ -3996,7 +4300,7 @@ function createEventBus() {
3996
4300
 
3997
4301
  // src/extensions/runner.ts
3998
4302
  async function execCommand(command, args, cwd, options) {
3999
- return new Promise((resolve8, reject) => {
4303
+ return new Promise((resolve9, reject) => {
4000
4304
  const child = execFile(
4001
4305
  command,
4002
4306
  args,
@@ -4011,7 +4315,7 @@ async function execCommand(command, args, cwd, options) {
4011
4315
  reject(new Error("Command was aborted"));
4012
4316
  return;
4013
4317
  }
4014
- resolve8({
4318
+ resolve9({
4015
4319
  exitCode: typeof error?.code === "number" ? error.code : error ? 1 : 0,
4016
4320
  stdout: stdout ?? "",
4017
4321
  stderr: stderr ?? ""
@@ -4869,6 +5173,8 @@ function snapshotResourceValue(kind, value) {
4869
5173
  const phase = value;
4870
5174
  const snapshot = {
4871
5175
  ...phase,
5176
+ disableAutoInvocation: phase.disableAutoInvocation ?? false,
5177
+ disableImplicitInvocation: phase.disableImplicitInvocation ?? false,
4872
5178
  ...phase.tools ? { tools: Object.freeze([...phase.tools]) } : {},
4873
5179
  skills: Object.freeze((phase.skills ?? []).map((skill) => Object.freeze({ ...skill }))),
4874
5180
  ...phase.input ? { input: Object.freeze({ ...phase.input }) } : {},
@@ -4894,7 +5200,7 @@ function capitalize(value) {
4894
5200
  return value.charAt(0).toUpperCase() + value.slice(1);
4895
5201
  }
4896
5202
  function isImplicitCoreName(kind, name) {
4897
- return kind === "tool" && name === "route" || kind === "phase" && name === "default";
5203
+ return kind === "tool" && name === "route" || kind === "phase" && (name === "default" || name === "stop" || name === "compact");
4898
5204
  }
4899
5205
  async function loadDirectoryValues(kind, directory, sourceId) {
4900
5206
  const root = resolve7(directory);
@@ -5078,7 +5384,7 @@ var RuntimeBootstrapRegistry = class extends ResourceRegistry {
5078
5384
  /** Install Rowan-owned implicit resources before any host source is read. */
5079
5385
  async ensureCoreResources() {
5080
5386
  await this.replaceImplicit("tool", "rowan.core", [createRouteTool([])]);
5081
- await this.replaceImplicit("phase", "rowan.core", [createDefaultPhase()]);
5387
+ await this.replaceImplicit("phase", "rowan.core", createCorePhases());
5082
5388
  }
5083
5389
  async loadExtensions(input) {
5084
5390
  const loaded = Array.isArray(input) ? { extensions: [...input], errors: [] } : (() => {
@@ -5222,7 +5528,10 @@ function intersectPhaseSelection(parent, layer) {
5222
5528
  };
5223
5529
  }
5224
5530
  function resolvePhases(candidates, selection) {
5225
- const selected = selectNamedResources(candidates, selection?.phaseIds, "Phase");
5531
+ const coreNames = /* @__PURE__ */ new Set([DEFAULT_PHASE_ID, STOP_PHASE_ID, COMPACT_PHASE_ID]);
5532
+ const core = candidates.filter((phase) => phase.core || coreNames.has(phase.name));
5533
+ const authored = candidates.filter((phase) => !coreNames.has(phase.name) && !phase.core);
5534
+ const selected = [...core, ...selectNamedResources(authored, selection?.phaseIds, "Phase")];
5226
5535
  const entryPhaseId = selection?.entryPhaseId ?? null;
5227
5536
  if (entryPhaseId !== null && entryPhaseId !== DEFAULT_PHASE_ID && !selected.some(({ name }) => name === entryPhaseId)) {
5228
5537
  console.warn(`Phase entry "${entryPhaseId}" is not available and will be skipped.`);
@@ -5244,6 +5553,31 @@ var DEFAULT_POLL_MS = 25;
5244
5553
  var MAX_CONSUMER_IDLE_POLL_MS = 250;
5245
5554
  var OWNER_LEASE_MS = 3e4;
5246
5555
  var OWNER_RENEWAL_MS = 1e4;
5556
+ var MAX_INLINE_TOOL_RESULT_BYTES = 16 * 1024;
5557
+ async function spillLargeToolResult(result, archiveDir, toolName) {
5558
+ if (!archiveDir) return result;
5559
+ const serialized = typeof result.content === "string" ? result.content : JSON.stringify(result.content, null, 2);
5560
+ if (serialized === void 0) return result;
5561
+ const bytes = new TextEncoder().encode(serialized);
5562
+ if (bytes.byteLength <= MAX_INLINE_TOOL_RESULT_BYTES || /\nFull result:\s+\S+/.test(serialized)) {
5563
+ return result;
5564
+ }
5565
+ const toolDir = archiveDir;
5566
+ await mkdir2(toolDir, { recursive: true, mode: 448 });
5567
+ await chmod2(toolDir, 448).catch(() => void 0);
5568
+ const safeToolName = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_");
5569
+ const path = join5(toolDir, `rowan-${safeToolName}-${Date.now()}-${randomUUID()}.log`);
5570
+ await writeFile2(path, serialized, { encoding: "utf8", mode: 384 });
5571
+ await chmod2(path, 384).catch(() => void 0);
5572
+ const preview = new TextDecoder().decode(bytes.subarray(0, MAX_INLINE_TOOL_RESULT_BYTES));
5573
+ return {
5574
+ ...result,
5575
+ content: `${preview}
5576
+ [truncated]
5577
+ Full result: ${path}
5578
+ Offset: 0`
5579
+ };
5580
+ }
5247
5581
  var AgentRuntime = class _AgentRuntime {
5248
5582
  concurrency;
5249
5583
  owned;
@@ -5254,6 +5588,7 @@ var AgentRuntime = class _AgentRuntime {
5254
5588
  executionDone = /* @__PURE__ */ new Map();
5255
5589
  cancellationRequested = /* @__PURE__ */ new Set();
5256
5590
  cancellationReasons = /* @__PURE__ */ new Map();
5591
+ autoCompactionRuns = /* @__PURE__ */ new Set();
5257
5592
  consumers = /* @__PURE__ */ new Map();
5258
5593
  transientEvents = new TransientRunEventHub();
5259
5594
  resources;
@@ -5346,6 +5681,36 @@ var AgentRuntime = class _AgentRuntime {
5346
5681
  this.assertOpen();
5347
5682
  return this.owned.compact(input);
5348
5683
  }
5684
+ async contextStatus(agentId, options = {}) {
5685
+ this.assertOpen();
5686
+ const agent = await this.requireAgent(agentId);
5687
+ const contextWindow = options.contextWindow ?? await this.resolveContextWindow(agent);
5688
+ return this.owned.contextStatus(agentId, contextWindow);
5689
+ }
5690
+ async compactContext(agentId, options = {}) {
5691
+ this.assertOpen();
5692
+ const agent = await this.requireAgent(agentId);
5693
+ if (!agent.activatedAt || !agent.currentConfigToken) throw new RuntimeError("agent_not_found", { agentId });
5694
+ const active = (await this.owned.listRuns({ agentId })).filter((run2) => ["queued", "running", "input_required"].includes(run2.state));
5695
+ const existing = active.find((run2) => controlRunKind(run2) === "compact");
5696
+ if (existing) return new DurableRun(this, existing.id);
5697
+ const blocking = active.find((run2) => run2.state === "queued" || run2.state === "running");
5698
+ if (blocking) throw new RuntimeError("context_busy", { agentId, runId: blocking.id, actual: blocking.state });
5699
+ const metadata = {
5700
+ rowan: {
5701
+ kind: "compact",
5702
+ ...options.instructions === void 0 ? {} : { instructions: options.instructions }
5703
+ }
5704
+ };
5705
+ const run = await this.owned.createRun({
5706
+ agentId,
5707
+ input: "",
5708
+ metadata,
5709
+ idempotencyKey: options.idempotencyKey ?? createId("compact")
5710
+ });
5711
+ void this.pump();
5712
+ return new DurableRun(this, run.id);
5713
+ }
5349
5714
  async start(agentId, input, options) {
5350
5715
  this.assertOpen();
5351
5716
  const agent = await this.requireAgent(agentId);
@@ -5365,6 +5730,47 @@ var AgentRuntime = class _AgentRuntime {
5365
5730
  this.assertOpen();
5366
5731
  return pageRuns(await this.owned.listRuns(input), { ...input, storeIncarnation: this.storeIncarnation });
5367
5732
  }
5733
+ async listInvocations(agentId, options) {
5734
+ this.assertOpen();
5735
+ if (!options || !["auto", "implicit", "external"].includes(options.source)) {
5736
+ throw new TypeError("options.source must be auto, implicit, or external");
5737
+ }
5738
+ const agent = await this.requireAgent(agentId);
5739
+ if (!agent.currentConfigToken) throw new RuntimeError("agent_not_found", { agentId });
5740
+ const resolution = await this.commands.resolve({ agent, token: agent.currentConfigToken });
5741
+ if (resolution.kind !== "available") {
5742
+ throw new RuntimeError("configuration_unavailable", {
5743
+ agentId,
5744
+ retryable: resolution.kind === "deferred",
5745
+ reason: resolution.kind === "deferred" ? "Configuration is deferred." : resolution.reason
5746
+ });
5747
+ }
5748
+ const config = isAgentConfiguration(resolution.config) ? this.materializeConfig(resolution.config) : resolution.config;
5749
+ const assembly = assembleRegisteredExtensions(config, this.resources.extensionRunner, {
5750
+ toolArchiveDir: this.archiveDirFor(agentId)
5751
+ });
5752
+ const phases = [...assembly.context.phases?.phases.values() ?? []].filter((phase) => options.source === "external" || (options.source === "auto" ? !phase.disableAutoInvocation : !phase.disableImplicitInvocation)).filter((phase) => options.source !== "implicit" || phase.name !== DEFAULT_PHASE_ID);
5753
+ const skills = assembly.context.skills.filter((skill) => options.source === "external" || (options.source === "auto" ? !(skill.disableAutoInvocation ?? skill.disableModelInvocation) : !skill.disableImplicitInvocation));
5754
+ return [
5755
+ ...phases.map((phase) => ({
5756
+ kind: "phase",
5757
+ name: phase.name,
5758
+ description: phase.description,
5759
+ ...phase.core ? { core: true } : {},
5760
+ disableAutoInvocation: phase.disableAutoInvocation ?? false,
5761
+ disableImplicitInvocation: phase.disableImplicitInvocation ?? false,
5762
+ filePath: phase.filePath
5763
+ })),
5764
+ ...skills.map((skill) => ({
5765
+ kind: "skill",
5766
+ name: skill.name,
5767
+ description: skill.description,
5768
+ disableAutoInvocation: skill.disableAutoInvocation ?? skill.disableModelInvocation ?? false,
5769
+ disableImplicitInvocation: skill.disableImplicitInvocation ?? false,
5770
+ filePath: skill.filePath
5771
+ }))
5772
+ ];
5773
+ }
5368
5774
  async consume(input) {
5369
5775
  this.assertOpen();
5370
5776
  if (input.consumerId.trim().length === 0) throw new TypeError("consumerId must be non-empty");
@@ -5412,6 +5818,15 @@ var AgentRuntime = class _AgentRuntime {
5412
5818
  await this.owned.answerInput({ runId, requestId: input.requestId, expectedRevision: snapshot.revision, input: input.input });
5413
5819
  void this.pump();
5414
5820
  }
5821
+ async respondInteraction(runId, input) {
5822
+ this.assertOpen();
5823
+ const snapshot = await this.owned.snapshotRun(runId);
5824
+ if (snapshot.state !== "input_required" || !snapshot.interactions.some((interaction) => interaction.id === input.interactionId)) {
5825
+ throw new RuntimeError("input_request_conflict", { runId, requestId: input.interactionId, reason: "not_found" });
5826
+ }
5827
+ await this.owned.answerInteraction({ runId, interactionId: input.interactionId, expectedRevision: snapshot.revision, input: input.input });
5828
+ void this.pump();
5829
+ }
5415
5830
  async cancel(runId, reason) {
5416
5831
  this.assertOpen();
5417
5832
  const done = this.executionDone.get(runId);
@@ -5435,16 +5850,19 @@ var AgentRuntime = class _AgentRuntime {
5435
5850
  try {
5436
5851
  while (!this.closed && this.executions.size < this.concurrency) {
5437
5852
  const queued = await this.owned.listRuns({ states: ["queued"] });
5438
- const next = queued.find((run) => !this.activeAgents.has(run.agentId));
5853
+ const next = queued.filter((run) => !this.activeAgents.has(run.agentId)).sort((left, right) => Number(controlRunKind(right) === "compact") - Number(controlRunKind(left) === "compact"))[0];
5439
5854
  if (!next) return;
5440
5855
  this.activeAgents.add(next.agentId);
5441
5856
  const task = this.execute(next).finally(() => {
5442
5857
  const execution = this.executions.get(next.id);
5443
- if (execution) this.transientEvents.clear(next.id, execution.executionId);
5858
+ if (execution) this.transientEvents.clear(next.id, execution.executionId, true);
5444
5859
  this.activeAgents.delete(next.agentId);
5445
5860
  this.executions.delete(next.id);
5446
5861
  this.cancellationRequested.delete(next.id);
5447
5862
  this.cancellationReasons.delete(next.id);
5863
+ void this.owned.snapshotRun(next.id).then((snapshot) => {
5864
+ if (snapshot.state !== "queued") this.autoCompactionRuns.delete(next.id);
5865
+ }).catch(() => void 0);
5448
5866
  this.executionDone.delete(next.id);
5449
5867
  void this.pump();
5450
5868
  });
@@ -5501,34 +5919,81 @@ var AgentRuntime = class _AgentRuntime {
5501
5919
  return;
5502
5920
  }
5503
5921
  }
5922
+ const config = resolvedConfig;
5923
+ const controlKind = controlRunKind(run);
5924
+ if (!controlKind && !this.autoCompactionRuns.has(run.id)) {
5925
+ const contextWindow = await resolveContextWindowForConfig(config);
5926
+ const status = await this.owned.contextStatus(run.agentId, contextWindow);
5927
+ const inputTokens = estimateRunInputTokens(run.input);
5928
+ if (status.thresholdTokens > 0 && status.tokens + inputTokens >= status.thresholdTokens) {
5929
+ this.autoCompactionRuns.add(run.id);
5930
+ await this.owned.createRun({
5931
+ agentId: run.agentId,
5932
+ input: "",
5933
+ metadata: {
5934
+ rowan: {
5935
+ kind: "compact",
5936
+ trigger: "auto",
5937
+ sourceRunId: String(run.id)
5938
+ }
5939
+ },
5940
+ idempotencyKey: `auto-compact:${run.id}`
5941
+ });
5942
+ return;
5943
+ }
5944
+ }
5504
5945
  const executionId = createId("exec");
5505
5946
  claim = await this.owned.claimRun({ runId: run.id, expectedRevision: run.revision, executionId, configToken: token });
5506
5947
  executionRevision = claim.run.revision;
5507
5948
  const controller = new AbortController();
5508
5949
  this.executions.set(run.id, { controller, executionId });
5509
5950
  if (this.cancellationRequested.has(run.id)) controller.abort();
5510
- const config = resolvedConfig;
5511
- const assembly = assembleRegisteredExtensions(config, this.resources.extensionRunner);
5951
+ const assembly = assembleRegisteredExtensions(config, this.resources.extensionRunner, {
5952
+ toolArchiveDir: this.archiveDirFor(run.agentId)
5953
+ });
5512
5954
  let toolQueue = Promise.resolve();
5513
5955
  const model = "stream" in config && config.stream ? config.model : { provider: config.model.provider, id: config.model.id };
5514
5956
  const stream = "stream" in config && config.stream ? config.stream : createModelStream(config.model);
5515
- const executionContext = projectModelContext({
5516
- context: {
5517
- ...assembly.context,
5518
- phases: normalizePhaseRegistry(assembly.context.phases)
5519
- },
5520
- messages: claim.history,
5521
- agentId: run.agentId,
5522
- runId: run.id
5523
- });
5957
+ const phaseRegistry = normalizePhaseRegistry(assembly.context.phases);
5958
+ let modelMessages = await this.owned.contextMessages(run.agentId);
5959
+ const buildExecutionContext = (messages, phaseId) => {
5960
+ const executionContext2 = projectModelContext({
5961
+ context: {
5962
+ ...assembly.context,
5963
+ phases: phaseRegistry
5964
+ },
5965
+ messages,
5966
+ agentId: run.agentId,
5967
+ runId: run.id
5968
+ });
5969
+ if (phaseId === COMPACT_PHASE_ID || controlKind === "compact") {
5970
+ executionContext2.phases = {
5971
+ ...executionContext2.phases,
5972
+ entryPhaseId: COMPACT_PHASE_ID
5973
+ };
5974
+ const instructions = controlKind === "compact" ? compactInstructions(run) : void 0;
5975
+ if (instructions) {
5976
+ executionContext2.messages.push({
5977
+ id: createId("msg"),
5978
+ role: "user",
5979
+ content: `Additional compaction instructions:
5980
+ ${instructions}`,
5981
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5982
+ metadata: { kind: "phase_input", phase: COMPACT_PHASE_ID }
5983
+ });
5984
+ }
5985
+ }
5986
+ return executionContext2;
5987
+ };
5988
+ let executionContext = buildExecutionContext(modelMessages);
5524
5989
  const executionTools = {
5525
5990
  tools: assembly.context.tools,
5526
5991
  beforeToolCall: assembly.beforeToolCall ?? config.beforeToolCall,
5527
5992
  afterToolCall: assembly.afterToolCall ?? config.afterToolCall
5528
5993
  };
5529
- const result = await executeOnce({
5530
- canonicalMessages: executionContext.messages,
5531
- context: executionContext,
5994
+ const executeModel = (context) => executeOnce({
5995
+ canonicalMessages: context.messages,
5996
+ context,
5532
5997
  execution: {
5533
5998
  agentId: run.agentId,
5534
5999
  runId: run.id,
@@ -5537,14 +6002,27 @@ var AgentRuntime = class _AgentRuntime {
5537
6002
  ...run.metadata === void 0 ? {} : { runMetadata: run.metadata }
5538
6003
  },
5539
6004
  model,
5540
- thinkingLevel: thinkingLevelFromMessages(executionContext.messages),
6005
+ thinkingLevel: thinkingLevelFromMessages(context.messages),
5541
6006
  stream,
5542
6007
  maxAttempts: config.maxAttempts,
5543
6008
  checkpoint: claim.run.checkpoint,
6009
+ interactionAnswers: claim.run.interactionAnswers,
5544
6010
  signal: controller.signal,
5545
6011
  beforePhase: assembly.beforePhase,
5546
6012
  afterPhase: assembly.afterPhase,
5547
6013
  beforePrompt: assembly.beforePrompt,
6014
+ onPhaseStatus: (phaseId, status) => {
6015
+ const active = this.executions.get(run.id);
6016
+ if (active?.executionId !== claim.execution.executionId) return;
6017
+ this.transientEvents.publish({
6018
+ kind: "phase_status",
6019
+ durability: "transient",
6020
+ runId: run.id,
6021
+ executionId: claim.execution.executionId,
6022
+ phaseId,
6023
+ status
6024
+ });
6025
+ },
5548
6026
  onMessageDelta: (event) => {
5549
6027
  const active = this.executions.get(run.id);
5550
6028
  if (this.closed || controller.signal.aborted || active?.executionId !== claim.execution.executionId) return;
@@ -5596,8 +6074,27 @@ var AgentRuntime = class _AgentRuntime {
5596
6074
  }
5597
6075
  }
5598
6076
  });
6077
+ let result = await executeModel(executionContext);
6078
+ if (!controlKind && result.type === "failed" && isContextOverflowError(result.error)) {
6079
+ const compactContext = buildExecutionContext(await this.owned.contextMessages(run.agentId), COMPACT_PHASE_ID);
6080
+ const compactResult = await executeModel(compactContext);
6081
+ const summary = compactResult.type === "completed" ? compactSummary(compactResult.outcome.payload) : void 0;
6082
+ if (summary) {
6083
+ const covered = claim.history.at(-1);
6084
+ await this.owned.commitContextCompaction({
6085
+ id: createId("compact"),
6086
+ agentId: run.agentId,
6087
+ summary,
6088
+ ...covered ? { coveredThrough: { messageId: covered.id, sequence: covered.sequenceWithinRun } } : {},
6089
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6090
+ });
6091
+ modelMessages = await this.owned.contextMessages(run.agentId);
6092
+ executionContext = buildExecutionContext(modelMessages);
6093
+ result = await executeModel(executionContext);
6094
+ }
6095
+ }
5599
6096
  if (this.cancellationRequested.has(run.id) || controller.signal.aborted) {
5600
- const output = latestAssistant(run, result.messages, claim.history.length, true);
6097
+ const output = latestAssistant(run, result.messages, modelMessages.length, true);
5601
6098
  const reason = this.cancellationReasons.get(run.id) ?? "Agent run stopped.";
5602
6099
  await this.owned.cancelRun({
5603
6100
  runId: run.id,
@@ -5610,14 +6107,39 @@ var AgentRuntime = class _AgentRuntime {
5610
6107
  }
5611
6108
  if (result.type === "input_required") {
5612
6109
  const prompt = promptMessage(run, result.request.prompt, result.messages.length);
5613
- await this.owned.commitInputRequired({ runId: run.id, execution: claim.execution, expectedRevision: executionRevision, requestId: createId("input"), phase: result.request.phase, prompt, checkpoint: result.checkpoint });
6110
+ await this.owned.commitInputRequired({
6111
+ runId: run.id,
6112
+ execution: claim.execution,
6113
+ expectedRevision: executionRevision,
6114
+ requestId: createId("input"),
6115
+ phase: result.request.phase,
6116
+ prompt,
6117
+ checkpoint: result.checkpoint,
6118
+ interactions: result.interactions,
6119
+ interactionAnswers: claim.run.interactionAnswers
6120
+ });
5614
6121
  this.transientEvents.clear(run.id, claim.execution.executionId);
5615
6122
  return;
5616
6123
  }
5617
6124
  if (result.type === "completed") {
5618
- const output = latestAssistant(run, result.messages, claim.history.length);
6125
+ const output = latestAssistant(run, result.messages.slice(modelMessages.length), modelMessages.length);
6126
+ if (controlKind === "compact" || isCompactionOutcome(result.outcome.payload)) {
6127
+ const summary = compactSummary(result.outcome.payload);
6128
+ if (summary) {
6129
+ const covered = claim.history.at(-1);
6130
+ const record = {
6131
+ id: createId("compact"),
6132
+ agentId: run.agentId,
6133
+ summary,
6134
+ ...covered ? { coveredThrough: { messageId: covered.id, sequence: covered.sequenceWithinRun } } : {},
6135
+ ...compactInstructions(run) ? { instructions: compactInstructions(run) } : {},
6136
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
6137
+ };
6138
+ await this.owned.commitContextCompaction(record);
6139
+ }
6140
+ }
5619
6141
  await this.owned.commitOutcome({ runId: run.id, execution: claim.execution, expectedRevision: executionRevision, outcome: durableOutcome(result.outcome), ...output ? { output } : {} });
5620
- this.transientEvents.clear(run.id, claim.execution.executionId);
6142
+ this.transientEvents.clear(run.id, claim.execution.executionId, true);
5621
6143
  return;
5622
6144
  }
5623
6145
  const failure = { code: "execution_failed", message: result.error instanceof Error ? result.error.message : "Execution failed." };
@@ -5650,6 +6172,13 @@ var AgentRuntime = class _AgentRuntime {
5650
6172
  if (!(error instanceof RuntimeError) || !["run_state_conflict", "runtime_ownership_lost", "run_not_found"].includes(error.code)) throw error;
5651
6173
  });
5652
6174
  }
6175
+ async resolveContextWindow(agent) {
6176
+ if (!agent.currentConfigToken) return 128e3;
6177
+ const resolution = await this.commands.resolve({ agent, token: agent.currentConfigToken });
6178
+ if (resolution.kind !== "available") return 128e3;
6179
+ const config = isAgentConfiguration(resolution.config) ? this.materializeConfig(resolution.config) : resolution.config;
6180
+ return resolveContextWindowForConfig(config);
6181
+ }
5653
6182
  async executeTool(input) {
5654
6183
  const batch = await this.executeToolBatch({ ...input, toolCalls: [input.toolCall] });
5655
6184
  return { result: batch.results[0], revision: batch.revision };
@@ -5740,6 +6269,11 @@ var AgentRuntime = class _AgentRuntime {
5740
6269
  assertToolExecutionResult(result);
5741
6270
  if (input.toolConfig.afterToolCall) result = await input.toolConfig.afterToolCall({ tool: durableTool, result, context, signal: input.signal });
5742
6271
  assertToolExecutionResult(result);
6272
+ result = await spillLargeToolResult(
6273
+ result,
6274
+ this.archiveDirFor(input.run.agentId),
6275
+ input.toolCall.name
6276
+ );
5743
6277
  const committed = await this.owned.commitToolResult({ runId: input.run.id, execution: input.execution, expectedRevision: revision, toolCallId, state: result.ok ? "completed" : "failed", result });
5744
6278
  return {
5745
6279
  result: { toolCallId: providerToolCallId, toolName: input.toolCall.name, ...result },
@@ -5759,6 +6293,11 @@ var AgentRuntime = class _AgentRuntime {
5759
6293
  if (!agent) throw new RuntimeError("agent_not_found", { agentId });
5760
6294
  return agent;
5761
6295
  }
6296
+ archiveDirFor(agentId) {
6297
+ const store = this.owned;
6298
+ const archiveDir = store.contextArchiveDir?.(agentId);
6299
+ return archiveDir ? join5(archiveDir, "tool-results") : void 0;
6300
+ }
5762
6301
  async runConsumer(subscription) {
5763
6302
  const registration = await this.owned.openConsumer(subscription.consumerId);
5764
6303
  let cursor = registration.cursor;
@@ -5800,10 +6339,11 @@ var AgentRuntime = class _AgentRuntime {
5800
6339
  const subscription = this.transientEvents.subscribe(runId);
5801
6340
  try {
5802
6341
  while (true) {
5803
- if (options.signal?.aborted) throw abortError();
5804
6342
  const observationVersion = subscription.checkpoint();
5805
6343
  const snapshot = await this.owned.snapshotRun(runId);
5806
- if (["completed", "failed", "cancelled"].includes(snapshot.state)) {
6344
+ const terminal = ["completed", "failed", "cancelled"].includes(snapshot.state);
6345
+ if (options.signal?.aborted && !terminal) throw abortError();
6346
+ if (terminal) {
5807
6347
  const pending = await this.owned.listEvents(cursor ? { after: cursor } : {});
5808
6348
  if (!pending.some((event) => event.runId === runId)) return;
5809
6349
  }
@@ -5816,8 +6356,16 @@ var AgentRuntime = class _AgentRuntime {
5816
6356
  } else if (event.kind === "tool_state_changed" && ["completed", "failed", "indeterminate"].includes(event.transition.to)) {
5817
6357
  subscription.clearTool(event.toolCall.id);
5818
6358
  }
6359
+ if (event.kind === "run_state_changed" && ["completed", "failed", "cancelled"].includes(event.to)) {
6360
+ while (true) {
6361
+ const transient2 = subscription.shift();
6362
+ if (!transient2) break;
6363
+ yield transient2;
6364
+ }
6365
+ yield event;
6366
+ return;
6367
+ }
5819
6368
  yield event;
5820
- if (event.kind === "run_state_changed" && ["completed", "failed", "cancelled"].includes(event.to)) return;
5821
6369
  }
5822
6370
  const transient = subscription.shift();
5823
6371
  if (transient) {
@@ -5873,6 +6421,9 @@ var DurableRun = class {
5873
6421
  respond(input) {
5874
6422
  return this.runtime.respond(this.id, input);
5875
6423
  }
6424
+ respondInteraction(input) {
6425
+ return this.runtime.respondInteraction(this.id, input);
6426
+ }
5876
6427
  cancel(reason) {
5877
6428
  return this.runtime.cancel(this.id, reason);
5878
6429
  }
@@ -5899,7 +6450,14 @@ function durableOutcome(outcome) {
5899
6450
  function boundaryFromSnapshot(snapshot) {
5900
6451
  switch (snapshot.state) {
5901
6452
  case "input_required":
5902
- return { type: "input_required", requestId: snapshot.request.id, phase: snapshot.request.phase, prompt: snapshot.request.prompt };
6453
+ return {
6454
+ type: "input_required",
6455
+ requestId: snapshot.request.id,
6456
+ phase: snapshot.request.phase,
6457
+ prompt: snapshot.request.prompt,
6458
+ interactions: snapshot.interactions,
6459
+ answers: snapshot.answers
6460
+ };
5903
6461
  case "completed":
5904
6462
  return { type: "completed", outcome: snapshot.outcome, ...snapshot.output ? { output: snapshot.output } : {} };
5905
6463
  case "failed":
@@ -5911,21 +6469,67 @@ function boundaryFromSnapshot(snapshot) {
5911
6469
  }
5912
6470
  }
5913
6471
  function delay(ms) {
5914
- return new Promise((resolve8) => setTimeout(resolve8, ms));
6472
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
6473
+ }
6474
+ function controlRunKind(run) {
6475
+ const rowan = run.metadata?.rowan;
6476
+ if (typeof rowan !== "object" || rowan === null || !("kind" in rowan)) return void 0;
6477
+ const kind = rowan.kind;
6478
+ return typeof kind === "string" ? kind : void 0;
6479
+ }
6480
+ function compactInstructions(run) {
6481
+ const rowan = run.metadata?.rowan;
6482
+ if (typeof rowan !== "object" || rowan === null || !("instructions" in rowan)) return void 0;
6483
+ const instructions = rowan.instructions;
6484
+ return typeof instructions === "string" && instructions.trim().length > 0 ? instructions : void 0;
6485
+ }
6486
+ function compactSummary(payload) {
6487
+ if (typeof payload !== "object" || payload === null || !("summary" in payload)) return void 0;
6488
+ const summary = payload.summary;
6489
+ return typeof summary === "string" && summary.trim().length > 0 ? summary : void 0;
6490
+ }
6491
+ function isCompactionOutcome(payload) {
6492
+ return typeof payload === "object" && payload !== null && "kind" in payload && payload.kind === "context_compaction";
6493
+ }
6494
+ async function resolveContextWindowForConfig(config) {
6495
+ if ("contextWindow" in config.model && typeof config.model.contextWindow === "number") {
6496
+ return config.model.contextWindow;
6497
+ }
6498
+ return 128e3;
6499
+ }
6500
+ function estimateRunInputTokens(input) {
6501
+ const content = typeof input === "string" ? input : input.content;
6502
+ const text = typeof content === "string" ? content : content.map((part) => part.type === "text" ? part.text : part.data).join("\n");
6503
+ return Math.ceil(text.length / 4) + 20;
6504
+ }
6505
+ function isContextOverflowError(error) {
6506
+ const message = error instanceof Error ? `${error.name} ${error.message}` : typeof error === "string" ? error : (() => {
6507
+ try {
6508
+ return JSON.stringify(error);
6509
+ } catch {
6510
+ return String(error);
6511
+ }
6512
+ })();
6513
+ return /context(?:\s|_|-)?(?:length|window|limit|overflow|exceed)|(?:maximum|max).{0,24}tokens|too many tokens|prompt.{0,24}(?:too large|exceed|limit)/i.test(message);
5915
6514
  }
5916
6515
  function normalizePhaseRegistry(registry) {
5917
- if (registry?.phases.has(DEFAULT_PHASE_ID) && registry.phases.get(DEFAULT_PHASE_ID)?.filePath !== "") {
5918
- throw new TypeError(`Configured Phase collides with Rowan built-in Phase "${DEFAULT_PHASE_ID}".`);
6516
+ const core = createCorePhases();
6517
+ const coreNames = new Set(core.map(({ name }) => name));
6518
+ for (const [name, phase] of registry?.phases ?? []) {
6519
+ if (coreNames.has(name) && !phase.core) {
6520
+ throw new TypeError(`Configured Phase collides with Rowan built-in Phase "${name}".`);
6521
+ }
5919
6522
  }
5920
6523
  for (const [name] of registry?.phases ?? []) {
5921
- if (name === "stop" || name === "continue") {
6524
+ if (name === "continue") {
5922
6525
  throw new TypeError(`Configured Phase name "${name}" is reserved by Rowan routing controls.`);
5923
6526
  }
5924
6527
  }
6528
+ const authored = [...registry?.phases ?? []].filter(([name, phase]) => !coreNames.has(name) || phase.core === false);
5925
6529
  return {
5926
6530
  phases: new Map([
5927
- [DEFAULT_PHASE_ID, createDefaultPhase()],
5928
- ...registry?.phases ?? []
6531
+ ...core.map((phase) => [phase.name, phase]),
6532
+ ...authored
5929
6533
  ]),
5930
6534
  entryPhaseId: registry?.entryPhaseId ?? DEFAULT_PHASE_ID
5931
6535
  };
@@ -5943,13 +6547,13 @@ function toJsonValue(value) {
5943
6547
  return value;
5944
6548
  }
5945
6549
  function deferred2() {
5946
- let resolve8;
6550
+ let resolve9;
5947
6551
  let reject;
5948
6552
  const promise = new Promise((res, rej) => {
5949
- resolve8 = (value) => res(value);
6553
+ resolve9 = (value) => res(value);
5950
6554
  reject = rej;
5951
6555
  });
5952
- return { promise, resolve: resolve8, reject };
6556
+ return { promise, resolve: resolve9, reject };
5953
6557
  }
5954
6558
 
5955
6559
  // src/runtime/durable-store.ts
@@ -5971,6 +6575,7 @@ var InMemoryStore = class _InMemoryStore {
5971
6575
  ownerEpoch = 0;
5972
6576
  eventSequence = 0;
5973
6577
  retentionFloor = 1;
6578
+ contextCompactions = /* @__PURE__ */ new Map();
5974
6579
  constructor(options = {}) {
5975
6580
  this.incarnation = options.incarnation ?? createId("store");
5976
6581
  }
@@ -5989,6 +6594,7 @@ var InMemoryStore = class _InMemoryStore {
5989
6594
  for (const [agentId, sequence] of state.nextReadySequence) store.nextReadySequence.set(agentId, sequence);
5990
6595
  store.eventSequence = state.eventSequence;
5991
6596
  store.retentionFloor = Math.max(1, state.retentionFloor ?? 1);
6597
+ for (const [agentId, record] of state.contextCompactions ?? []) store.contextCompactions.set(agentId, clone(record));
5992
6598
  store.ownerEpoch = Math.max(
5993
6599
  0,
5994
6600
  ...state.runs.map((run) => run.execution?.ownerEpoch ?? 0)
@@ -6010,7 +6616,8 @@ var InMemoryStore = class _InMemoryStore {
6010
6616
  retentionFloor: this.retentionFloor,
6011
6617
  nextAgentSequence: [...this.nextAgentSequence.entries()],
6012
6618
  nextReadySequence: [...this.nextReadySequence.entries()],
6013
- eventSequence: this.eventSequence
6619
+ eventSequence: this.eventSequence,
6620
+ contextCompactions: [...this.contextCompactions.entries()]
6014
6621
  });
6015
6622
  }
6016
6623
  attachOwner(lease) {
@@ -6189,6 +6796,18 @@ var InMemoryStore = class _InMemoryStore {
6189
6796
  actual: -1
6190
6797
  });
6191
6798
  }
6799
+ const coveredThrough = this.contextCompactions.get(agent.id)?.coveredThrough;
6800
+ if (coveredThrough) {
6801
+ const history = this.activeHistory(agent.id, Number.MAX_SAFE_INTEGER);
6802
+ const coveredIndex = history.findIndex((message) => message.id === coveredThrough.messageId);
6803
+ const originalIndex = history.findIndex((message) => message.id === original.id);
6804
+ if (coveredIndex >= 0 && originalIndex >= 0 && originalIndex <= coveredIndex) {
6805
+ throw new RuntimeError("message_history_compacted", {
6806
+ agentId: agent.id,
6807
+ messageId: original.id
6808
+ });
6809
+ }
6810
+ }
6192
6811
  const actualRevision = original.messageRevision ?? 0;
6193
6812
  if (actualRevision !== input.expectedMessageRevision) {
6194
6813
  throw new RuntimeError("message_revision_conflict", {
@@ -6332,10 +6951,10 @@ var InMemoryStore = class _InMemoryStore {
6332
6951
  const run = this.requireRun(input.runId);
6333
6952
  this.assertRevision(run, input.expectedRevision);
6334
6953
  this.assertState(run, ["queued"]);
6335
- if ([...this.runs.values()].some((candidate) => candidate.agentId === run.agentId && candidate.id !== run.id && candidate.agentSequence < run.agentSequence && !["completed", "failed", "cancelled"].includes(candidate.state))) {
6954
+ if (!isControlRun(run) && [...this.runs.values()].some((candidate) => candidate.agentId === run.agentId && candidate.id !== run.id && candidate.agentSequence < run.agentSequence && !["completed", "failed", "cancelled"].includes(candidate.state))) {
6336
6955
  throw new RuntimeError("run_state_conflict", { runId: run.id, expected: ["queued"], actual: run.state });
6337
6956
  }
6338
- if ([...this.runs.values()].some((candidate) => candidate.agentId === run.agentId && candidate.id !== run.id && ["running", "input_required"].includes(candidate.state))) {
6957
+ if ([...this.runs.values()].some((candidate) => candidate.agentId === run.agentId && candidate.id !== run.id && (candidate.state === "running" || !isControlRun(run) && candidate.state === "input_required"))) {
6339
6958
  throw new RuntimeError("run_state_conflict", { runId: run.id, expected: ["queued"], actual: run.state });
6340
6959
  }
6341
6960
  if (run.pinnedConfigToken && input.configToken && run.pinnedConfigToken !== input.configToken) {
@@ -6343,7 +6962,7 @@ var InMemoryStore = class _InMemoryStore {
6343
6962
  }
6344
6963
  if (!run.pinnedConfigToken && input.configToken) run.pinnedConfigToken = input.configToken;
6345
6964
  let message;
6346
- if (!run.checkpoint && !run.initialMessageId) {
6965
+ if (!run.checkpoint && !run.initialMessageId && !isControlRun(run)) {
6347
6966
  const userInput = normalizeUserInput(run.input);
6348
6967
  message = {
6349
6968
  id: input.messageId ?? createId("msg"),
@@ -6395,7 +7014,8 @@ var InMemoryStore = class _InMemoryStore {
6395
7014
  if (typeof input.phase !== "string" || input.phase.length === 0) throw new TypeError("phase must be non-empty");
6396
7015
  const requestId = input.requestId ?? createId("input");
6397
7016
  const operationKey = `input_required:${requestId}`;
6398
- const operationPayload = canonicalJson([input.runId, input.expectedRevision, input.phase, input.prompt.id, input.checkpoint]);
7017
+ const interactions = input.interactions ?? [];
7018
+ const operationPayload = canonicalJson([input.runId, input.expectedRevision, input.phase, input.prompt.id, input.checkpoint, interactions, input.interactionAnswers ?? null]);
6399
7019
  const replay = this.replayOperation(operationKey, operationPayload);
6400
7020
  if (replay) return clone(replay);
6401
7021
  const run = this.requireRun(input.runId);
@@ -6416,12 +7036,19 @@ var InMemoryStore = class _InMemoryStore {
6416
7036
  run.state = "input_required";
6417
7037
  run.checkpoint = clone(input.checkpoint);
6418
7038
  run.openInputRequest = request;
7039
+ if (interactions.length > 0) {
7040
+ run.openInteractions = clone(interactions);
7041
+ run.interactionAnswers = clone(input.interactionAnswers ?? run.interactionAnswers ?? {});
7042
+ } else {
7043
+ delete run.openInteractions;
7044
+ delete run.interactionAnswers;
7045
+ }
6419
7046
  delete run.execution;
6420
7047
  run.revision += 1;
6421
7048
  run.updatedAt = createTimestamp();
6422
7049
  this.appendMessage(run, prompt);
6423
- this.appendTransition(run, "running", "input_required", request, prompt);
6424
- const result = { run: clone(run), prompt: clone(prompt), request: clone(request) };
7050
+ this.appendTransition(run, "running", "input_required", request, prompt, void 0, void 0, void 0, interactions, run.interactionAnswers ?? {});
7051
+ const result = { run: clone(run), prompt: clone(prompt), request: clone(request), interactions: clone(interactions) };
6425
7052
  this.writeOperationReceipt(operationKey, operationPayload, result);
6426
7053
  return result;
6427
7054
  }
@@ -6438,6 +7065,9 @@ var InMemoryStore = class _InMemoryStore {
6438
7065
  if (!run.openInputRequest || run.openInputRequest.id !== input.requestId) {
6439
7066
  throw new RuntimeError("input_request_conflict", { runId: run.id, requestId: input.requestId, reason: "not_found" });
6440
7067
  }
7068
+ if (run.openInteractions && run.openInteractions.length > 0) {
7069
+ throw new RuntimeError("input_request_conflict", { runId: run.id, requestId: input.requestId, reason: "not_found" });
7070
+ }
6441
7071
  const message = {
6442
7072
  id: input.messageId ?? createId("msg"),
6443
7073
  agentId: run.agentId,
@@ -6460,6 +7090,50 @@ var InMemoryStore = class _InMemoryStore {
6460
7090
  this.writeOperationReceipt(operationKey, operationPayload, result);
6461
7091
  return result;
6462
7092
  }
7093
+ answerInteraction(lease, input) {
7094
+ this.assertOwner(lease);
7095
+ if (input.interactionId.trim().length === 0) throw new TypeError("interactionId must be non-empty");
7096
+ const operationKey = `interaction_answer:${input.runId}:${input.interactionId}`;
7097
+ const operationPayload = canonicalJson(input.input);
7098
+ const replay = this.replayOperation(operationKey, operationPayload);
7099
+ if (replay) return clone(replay);
7100
+ const run = this.requireRun(input.runId);
7101
+ this.assertRevision(run, input.expectedRevision);
7102
+ this.assertState(run, ["input_required"]);
7103
+ const openInteractions = run.openInteractions ?? [];
7104
+ if (!openInteractions.some((interaction) => interaction.id === input.interactionId)) {
7105
+ throw new RuntimeError("input_request_conflict", { runId: run.id, requestId: input.interactionId, reason: "not_found" });
7106
+ }
7107
+ const message = {
7108
+ id: createId("msg"),
7109
+ agentId: run.agentId,
7110
+ runId: run.id,
7111
+ role: "user",
7112
+ content: typeof input.input === "string" ? input.input : JSON.stringify(input.input),
7113
+ metadata: { kind: "phase_interaction", interactionId: input.interactionId },
7114
+ sequenceWithinRun: this.nextMessageSequence(run.id),
7115
+ createdAt: createTimestamp()
7116
+ };
7117
+ this.messages.set(message.id, message);
7118
+ run.openInteractions = openInteractions.filter((interaction) => interaction.id !== input.interactionId);
7119
+ run.interactionAnswers = {
7120
+ ...run.interactionAnswers ?? {},
7121
+ [input.interactionId]: clone(input.input)
7122
+ };
7123
+ run.revision += 1;
7124
+ run.updatedAt = message.createdAt;
7125
+ this.appendMessage(run, message);
7126
+ if (run.openInteractions.length === 0) {
7127
+ delete run.openInteractions;
7128
+ delete run.openInputRequest;
7129
+ run.readySequence = this.nextReady(run.agentId);
7130
+ run.state = "queued";
7131
+ this.appendTransition(run, "input_required", "queued");
7132
+ }
7133
+ const result = clone(run);
7134
+ this.writeOperationReceipt(operationKey, operationPayload, result);
7135
+ return result;
7136
+ }
6463
7137
  reserveToolCall(lease, input) {
6464
7138
  const providerToolCallId = input.providerToolCallId ?? (input.toolCallId ? String(input.toolCallId) : createId("provider"));
6465
7139
  const result = this.reserveToolCalls(lease, {
@@ -6654,6 +7328,8 @@ var InMemoryStore = class _InMemoryStore {
6654
7328
  if (input.outcome) run.outcome = clone(input.outcome);
6655
7329
  if (input.failure) run.failure = clone(input.failure);
6656
7330
  delete run.execution;
7331
+ delete run.openInteractions;
7332
+ delete run.interactionAnswers;
6657
7333
  run.revision += 1;
6658
7334
  run.updatedAt = createTimestamp();
6659
7335
  this.appendTransition(run, "running", nextState, void 0, output, input.outcome, input.failure);
@@ -6699,6 +7375,8 @@ var InMemoryStore = class _InMemoryStore {
6699
7375
  delete run.cancellationReason;
6700
7376
  delete run.execution;
6701
7377
  delete run.openInputRequest;
7378
+ delete run.openInteractions;
7379
+ delete run.interactionAnswers;
6702
7380
  run.revision += 1;
6703
7381
  run.updatedAt = createTimestamp();
6704
7382
  this.appendTransition(run, from, "failed", void 0, void 0, void 0, failure);
@@ -6710,6 +7388,8 @@ var InMemoryStore = class _InMemoryStore {
6710
7388
  run.cancellationReason = input.reason;
6711
7389
  delete run.execution;
6712
7390
  delete run.openInputRequest;
7391
+ delete run.openInteractions;
7392
+ delete run.interactionAnswers;
6713
7393
  run.revision += 1;
6714
7394
  run.updatedAt = createTimestamp();
6715
7395
  this.appendTransition(run, from, "cancelled", void 0, void 0, void 0, void 0, input.reason);
@@ -6741,7 +7421,13 @@ var InMemoryStore = class _InMemoryStore {
6741
7421
  const request = run.openInputRequest;
6742
7422
  const prompt = request && this.messages.get(request.messageId);
6743
7423
  if (!prompt || prompt.role !== "assistant") throw new Error(`Run ${run.id} has an invalid input prompt.`);
6744
- return { ...base, state: "input_required", request: { id: request.id, phase: request.phase, prompt: clone(prompt) } };
7424
+ return {
7425
+ ...base,
7426
+ state: "input_required",
7427
+ request: { id: request.id, phase: request.phase, prompt: clone(prompt) },
7428
+ interactions: clone(run.openInteractions ?? []),
7429
+ answers: clone(run.interactionAnswers ?? {})
7430
+ };
6745
7431
  }
6746
7432
  case "completed": {
6747
7433
  const output = this.messagesForRun(run.id).filter((message) => message.role === "assistant").at(-1);
@@ -6951,6 +7637,72 @@ var InMemoryStore = class _InMemoryStore {
6951
7637
  this.requireAgent(agentId);
6952
7638
  return this.activeHistory(agentId, Number.MAX_SAFE_INTEGER);
6953
7639
  }
7640
+ contextStatus(lease, agentId, contextWindow) {
7641
+ this.assertOwner(lease);
7642
+ this.requireAgent(agentId);
7643
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0) {
7644
+ throw new TypeError("contextWindow must be a positive finite number");
7645
+ }
7646
+ const messages = this.contextMessages(lease, agentId);
7647
+ const tokens = estimateMessageTokens(messages);
7648
+ const record = this.contextCompactions.get(agentId);
7649
+ const thresholdTokens = Math.max(0, Math.floor(contextWindow) - 16384);
7650
+ return {
7651
+ tokens,
7652
+ contextWindow: Math.floor(contextWindow),
7653
+ percent: Math.round(tokens / contextWindow * 100),
7654
+ thresholdTokens,
7655
+ estimated: true,
7656
+ ...record?.coveredThrough ? { coveredThrough: clone(record.coveredThrough) } : {}
7657
+ };
7658
+ }
7659
+ contextMessages(lease, agentId, recentTokenBudget = 2e4) {
7660
+ this.assertOwner(lease);
7661
+ this.requireAgent(agentId);
7662
+ if (!Number.isFinite(recentTokenBudget) || recentTokenBudget <= 0) {
7663
+ throw new TypeError("recentTokenBudget must be a positive finite number");
7664
+ }
7665
+ const history = this.activeHistory(agentId, Number.MAX_SAFE_INTEGER);
7666
+ const record = this.contextCompactions.get(agentId);
7667
+ if (!record) return history;
7668
+ const coveredIndex = record.coveredThrough ? history.findIndex((message) => message.id === record.coveredThrough.messageId) : -1;
7669
+ const suffix = coveredIndex >= 0 ? history.slice(coveredIndex + 1) : history;
7670
+ const recent = [];
7671
+ let tokens = 0;
7672
+ for (let index = suffix.length - 1; index >= 0; index -= 1) {
7673
+ const message = suffix[index];
7674
+ const messageTokens = estimateMessageTokens([message]);
7675
+ if (recent.length > 0 && tokens + messageTokens > recentTokenBudget) break;
7676
+ recent.unshift(message);
7677
+ tokens += messageTokens;
7678
+ }
7679
+ const covered = record.coveredThrough ? history.find((message) => message.id === record.coveredThrough.messageId) : void 0;
7680
+ const summary = {
7681
+ id: `context-summary:${record.id}`,
7682
+ agentId,
7683
+ runId: covered?.runId ?? history.at(-1)?.runId ?? `context:${agentId}`,
7684
+ role: "assistant",
7685
+ content: `[Context summary]
7686
+
7687
+ ${record.summary}`,
7688
+ sequenceWithinRun: covered?.sequenceWithinRun ?? 0,
7689
+ createdAt: record.createdAt,
7690
+ metadata: { kind: "context_summary", compactionId: record.id }
7691
+ };
7692
+ return [summary, ...recent];
7693
+ }
7694
+ commitContextCompaction(lease, record) {
7695
+ this.assertOwner(lease);
7696
+ this.requireAgent(record.agentId);
7697
+ if (record.id.trim().length === 0 || record.summary.trim().length === 0) {
7698
+ throw new TypeError("context compaction id and summary must be non-empty");
7699
+ }
7700
+ const existing = this.contextCompactions.get(record.agentId);
7701
+ if (existing?.id === record.id) return clone(existing);
7702
+ const next = clone(record);
7703
+ this.contextCompactions.set(record.agentId, next);
7704
+ return clone(next);
7705
+ }
6954
7706
  activeHistory(agentId, beforeSequence) {
6955
7707
  return clone([
6956
7708
  ...this.historySeeds.get(agentId) ?? [],
@@ -6963,13 +7715,14 @@ var InMemoryStore = class _InMemoryStore {
6963
7715
  message
6964
7716
  }));
6965
7717
  }
6966
- appendTransition(run, from, to, request, prompt, outcome, failure, reason) {
7718
+ appendTransition(run, from, to, request, prompt, outcome, failure, reason, interactions, answers) {
6967
7719
  const transition = {
6968
7720
  ...this.baseEvent(run),
6969
7721
  kind: "run_state_changed",
6970
7722
  from,
6971
7723
  to,
6972
7724
  ...to === "input_required" && request && prompt ? { request: { id: request.id, phase: request.phase, prompt } } : {},
7725
+ ...to === "input_required" ? { interactions: clone(interactions ?? []), answers: clone(answers ?? {}) } : {},
6973
7726
  ...to === "completed" && outcome ? { outcome } : {},
6974
7727
  ...to === "failed" && failure ? { failure } : {},
6975
7728
  ...to === "cancelled" && reason ? { reason } : {}
@@ -7032,6 +7785,15 @@ var MemoryOwnedStore = class {
7032
7785
  async compact(input) {
7033
7786
  return this.store.compact(this.lease, input);
7034
7787
  }
7788
+ async contextStatus(agentId, contextWindow) {
7789
+ return this.store.contextStatus(this.lease, agentId, contextWindow);
7790
+ }
7791
+ async contextMessages(agentId, recentTokenBudget) {
7792
+ return this.store.contextMessages(this.lease, agentId, recentTokenBudget);
7793
+ }
7794
+ async commitContextCompaction(record) {
7795
+ return this.store.commitContextCompaction(this.lease, record);
7796
+ }
7035
7797
  async createRun(input) {
7036
7798
  return this.store.createRun(this.lease, input);
7037
7799
  }
@@ -7047,6 +7809,9 @@ var MemoryOwnedStore = class {
7047
7809
  async answerInput(input) {
7048
7810
  return this.store.answerInput(this.lease, input);
7049
7811
  }
7812
+ async answerInteraction(input) {
7813
+ return this.store.answerInteraction(this.lease, input);
7814
+ }
7050
7815
  async commitOutcome(input) {
7051
7816
  return this.store.commitOutcome(this.lease, input);
7052
7817
  }
@@ -7118,6 +7883,29 @@ function userInputContent(input) {
7118
7883
  function userInputMetadata(input) {
7119
7884
  return typeof input === "string" ? void 0 : input.metadata;
7120
7885
  }
7886
+ function isControlRun(run) {
7887
+ const rowan = run.metadata?.rowan;
7888
+ return typeof rowan === "object" && rowan !== null && "kind" in rowan && rowan.kind === "compact";
7889
+ }
7890
+ function estimateMessageTokens(messages) {
7891
+ let characters = 0;
7892
+ for (const message of messages) {
7893
+ characters += messageContentText2(message.content).length;
7894
+ characters += 20;
7895
+ }
7896
+ return Math.ceil(characters / 4);
7897
+ }
7898
+ function messageContentText2(content) {
7899
+ if (typeof content === "string") return content;
7900
+ return content.map((part) => {
7901
+ if (part.type === "text") return part.text;
7902
+ if (part.type === "thinking") return part.thinking;
7903
+ if (part.type === "tool_use") return JSON.stringify(part.input);
7904
+ if (part.type === "tool_result") return JSON.stringify(part.result);
7905
+ if (part.type === "image") return `[image:${part.mimeType}]`;
7906
+ return "";
7907
+ }).join("\n");
7908
+ }
7121
7909
  function targetInput(input, metadata) {
7122
7910
  const content = typeof input === "string" ? input : input.content;
7123
7911
  return metadata === void 0 ? { content } : { content, metadata: clone(metadata) };
@@ -7159,6 +7947,9 @@ function assertToolValue(value, argument) {
7159
7947
 
7160
7948
  // src/runtime/sqlite-durable-store.ts
7161
7949
  import { Database } from "bun:sqlite";
7950
+ import { appendFileSync, chmodSync, mkdirSync, readFileSync as readFileSync2, rmSync } from "fs";
7951
+ import { dirname as dirname7, join as join6, resolve as resolve8 } from "path";
7952
+ import { tmpdir as tmpdir2 } from "os";
7162
7953
  var SCHEMA_ID = "rowan.agent.runtime";
7163
7954
  var SCHEMA_VERSION = "2";
7164
7955
  var SCHEMA_VALUE = `${SCHEMA_ID}:${SCHEMA_VERSION}`;
@@ -7263,10 +8054,15 @@ var CURRENT_SCHEMA_SQL = `
7263
8054
  `;
7264
8055
  var SqliteStore = class {
7265
8056
  database;
8057
+ archiveRoot;
8058
+ mirroredEventIds = /* @__PURE__ */ new Map();
8059
+ mirroredCompactionIds = /* @__PURE__ */ new Map();
8060
+ mirroredAgents = /* @__PURE__ */ new Set();
7266
8061
  closed = false;
7267
8062
  configured = false;
7268
8063
  constructor(filename = ":memory:") {
7269
8064
  this.database = new Database(filename, { create: true, readwrite: true, strict: true });
8065
+ this.archiveRoot = filename === ":memory:" ? join6(tmpdir2(), `rowan-context-archives-${crypto.randomUUID()}`) : join6(dirname7(resolve8(filename)), "context-archives");
7270
8066
  }
7271
8067
  async openOwner(input) {
7272
8068
  this.assertOpen();
@@ -7318,6 +8114,10 @@ var SqliteStore = class {
7318
8114
  this.closed = true;
7319
8115
  this.database.close();
7320
8116
  }
8117
+ /** Filesystem location owned by this Store for one Agent's readable archive. */
8118
+ contextArchiveDir(agentId) {
8119
+ return join6(this.archiveRoot, String(agentId));
8120
+ }
7321
8121
  async reserveAgent(lease, input) {
7322
8122
  return this.invoke(lease, (store, current) => store.reserveAgent(current, input));
7323
8123
  }
@@ -7333,6 +8133,15 @@ var SqliteStore = class {
7333
8133
  async reviseMessage(lease, input) {
7334
8134
  return this.invoke(lease, (store, current) => store.reviseMessage(current, input));
7335
8135
  }
8136
+ async contextStatus(lease, agentId, contextWindow) {
8137
+ return this.invoke(lease, (store, current) => store.contextStatus(current, agentId, contextWindow), false);
8138
+ }
8139
+ async contextMessages(lease, agentId, recentTokenBudget) {
8140
+ return this.invoke(lease, (store, current) => store.contextMessages(current, agentId, recentTokenBudget), false);
8141
+ }
8142
+ async commitContextCompaction(lease, record) {
8143
+ return this.invoke(lease, (store, current) => store.commitContextCompaction(current, record));
8144
+ }
7336
8145
  async createRun(lease, input) {
7337
8146
  return this.invoke(lease, (store, current) => store.createRun(current, input));
7338
8147
  }
@@ -7348,6 +8157,9 @@ var SqliteStore = class {
7348
8157
  async answerInput(lease, input) {
7349
8158
  return this.invoke(lease, (store, current) => store.answerInput(current, input));
7350
8159
  }
8160
+ async answerInteraction(lease, input) {
8161
+ return this.invoke(lease, (store, current) => store.answerInteraction(current, input));
8162
+ }
7351
8163
  async commitOutcome(lease, input) {
7352
8164
  return this.invoke(lease, (store, current) => store.commitOutcome(current, input));
7353
8165
  }
@@ -7577,6 +8389,64 @@ var SqliteStore = class {
7577
8389
  [consumerId, Number(String(cursor).split(":").at(-1)), null, (/* @__PURE__ */ new Date()).toISOString()]
7578
8390
  );
7579
8391
  }
8392
+ this.mirrorState(state);
8393
+ }
8394
+ /** Append the semantic durable event mirror and remove deleted Agent sidecars. */
8395
+ mirrorState(state) {
8396
+ mkdirSync(this.archiveRoot, { recursive: true, mode: 448 });
8397
+ chmodSync(this.archiveRoot, 448);
8398
+ const currentAgents = new Set(state.agents.map((agent) => String(agent.id)));
8399
+ for (const agentId of this.mirroredAgents) {
8400
+ if (currentAgents.has(agentId)) continue;
8401
+ rmSync(join6(this.archiveRoot, agentId), { recursive: true, force: true });
8402
+ this.mirroredEventIds.delete(agentId);
8403
+ this.mirroredCompactionIds.delete(agentId);
8404
+ }
8405
+ for (const agent of state.agents) {
8406
+ const agentId = String(agent.id);
8407
+ const archiveDir = join6(this.archiveRoot, agentId);
8408
+ const toolDir = join6(archiveDir, "tool-results");
8409
+ mkdirSync(toolDir, { recursive: true, mode: 448 });
8410
+ chmodSync(archiveDir, 448);
8411
+ chmodSync(toolDir, 448);
8412
+ const sessionPath = join6(archiveDir, "session.jsonl");
8413
+ try {
8414
+ chmodSync(sessionPath, 384);
8415
+ } catch {
8416
+ }
8417
+ const known = this.mirroredEventIds.get(agentId) ?? /* @__PURE__ */ new Set();
8418
+ const knownCompactions = this.mirroredCompactionIds.get(agentId) ?? /* @__PURE__ */ new Set();
8419
+ if (known.size === 0 || knownCompactions.size === 0) {
8420
+ try {
8421
+ for (const line of readFileSync2(sessionPath, "utf8").split("\n")) {
8422
+ if (!line.trim()) continue;
8423
+ const parsed = JSON.parse(line);
8424
+ if (parsed.event?.id) known.add(parsed.event.id);
8425
+ if (parsed.record?.id) knownCompactions.add(parsed.record.id);
8426
+ }
8427
+ } catch {
8428
+ }
8429
+ }
8430
+ const lines = state.events.filter((event) => String(event.agentId) === agentId && !known.has(String(event.id))).map((event) => JSON.stringify({ schemaVersion: 1, kind: "rowan_event", event }) + "\n");
8431
+ if (lines.length > 0) {
8432
+ appendFileSync(sessionPath, lines.join(""), { encoding: "utf8", mode: 384 });
8433
+ chmodSync(sessionPath, 384);
8434
+ for (const event of state.events) {
8435
+ if (String(event.agentId) === agentId) known.add(String(event.id));
8436
+ }
8437
+ }
8438
+ const compactionLines = (state.contextCompactions ?? []).filter(([candidateId, record]) => String(candidateId) === agentId && !knownCompactions.has(record.id)).map(([, record]) => JSON.stringify({ schemaVersion: 1, kind: "context_compacted", record }) + "\n");
8439
+ if (compactionLines.length > 0) {
8440
+ appendFileSync(sessionPath, compactionLines.join(""), { encoding: "utf8", mode: 384 });
8441
+ chmodSync(sessionPath, 384);
8442
+ for (const [, record] of state.contextCompactions ?? []) {
8443
+ if (String(record.agentId) === agentId) knownCompactions.add(record.id);
8444
+ }
8445
+ }
8446
+ this.mirroredEventIds.set(agentId, known);
8447
+ this.mirroredCompactionIds.set(agentId, knownCompactions);
8448
+ }
8449
+ this.mirroredAgents = currentAgents;
7580
8450
  }
7581
8451
  requireMatchingOwner(lease, requireLive) {
7582
8452
  const row = this.readOwner();
@@ -7632,6 +8502,9 @@ var SqliteOwnedStore = class {
7632
8502
  }
7633
8503
  store;
7634
8504
  lease;
8505
+ contextArchiveDir(agentId) {
8506
+ return this.store.contextArchiveDir(agentId);
8507
+ }
7635
8508
  reserveAgent(input) {
7636
8509
  return this.store.reserveAgent(this.lease, input);
7637
8510
  }
@@ -7650,6 +8523,15 @@ var SqliteOwnedStore = class {
7650
8523
  compact(input) {
7651
8524
  return this.store.compact(this.lease, input);
7652
8525
  }
8526
+ contextStatus(agentId, contextWindow) {
8527
+ return this.store.contextStatus(this.lease, agentId, contextWindow);
8528
+ }
8529
+ contextMessages(agentId, recentTokenBudget) {
8530
+ return Promise.resolve(this.store.contextMessages(this.lease, agentId, recentTokenBudget));
8531
+ }
8532
+ commitContextCompaction(record) {
8533
+ return this.store.commitContextCompaction(this.lease, record);
8534
+ }
7653
8535
  createRun(input) {
7654
8536
  return this.store.createRun(this.lease, input);
7655
8537
  }
@@ -7665,6 +8547,9 @@ var SqliteOwnedStore = class {
7665
8547
  answerInput(input) {
7666
8548
  return this.store.answerInput(this.lease, input);
7667
8549
  }
8550
+ answerInteraction(input) {
8551
+ return this.store.answerInteraction(this.lease, input);
8552
+ }
7668
8553
  commitOutcome(input) {
7669
8554
  return this.store.commitOutcome(this.lease, input);
7670
8555
  }
@@ -7752,29 +8637,30 @@ function assertOwnerInput(ownerId, leaseMs) {
7752
8637
 
7753
8638
  // src/index.ts
7754
8639
  function createCoreTools2(input = {}) {
7755
- return createCoreTools(input).map((tool) => ({
7756
- name: tool.name,
7757
- description: tool.description,
7758
- parameters: tool.parameters,
7759
- execute: async (args, context, signal) => {
7760
- const result = await tool.execute(args, { skills: [], toolCallId: context.toolCallId }, signal);
7761
- return result.ok ? { ok: true, content: result.content } : { ok: false, content: result.content, error: result.error ?? "Tool failed." };
7762
- }
7763
- }));
8640
+ return createRuntimeCoreTools(input);
7764
8641
  }
7765
8642
  export {
7766
8643
  AgentRuntime,
8644
+ COMPACT_PHASE_ID,
8645
+ DEFAULT_PHASE_ID,
7767
8646
  ExtensionLifetimeError,
7768
8647
  InMemoryConfigProvider,
7769
8648
  InMemoryStore,
8649
+ PhaseInteractionBoundary,
8650
+ PhaseInteractionCancelledError,
7770
8651
  ResourceRegistry,
7771
8652
  ResourceRegistryError,
7772
8653
  RuntimeBootstrapRegistry,
7773
8654
  RuntimeError,
7774
8655
  RuntimeExtensionLifetime,
8656
+ STOP_PHASE_ID,
7775
8657
  SqliteStore,
7776
8658
  brandConfigToken,
8659
+ createCompactPhase,
8660
+ createCorePhases,
7777
8661
  createCoreTools2 as createCoreTools,
8662
+ createDefaultPhase,
8663
+ createStopPhase,
7778
8664
  isRuntimeError,
7779
8665
  loadExtensionsFromPath as loadExtensions,
7780
8666
  loadPhase,