@rowan-agent/agent 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -387,51 +387,29 @@ import { dirname as dirname2, join, resolve } from "path";
387
387
  // src/harness/loader.ts
388
388
  import { readFile } from "fs/promises";
389
389
  import { basename, dirname, extname } from "path";
390
- function parseFrontmatter(raw) {
391
- const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
392
- if (!match) return { frontmatter: {}, body: raw };
393
- const lines = match[1].split("\n");
394
- const frontmatter = {};
395
- for (let i = 0; i < lines.length; i++) {
396
- const line = lines[i];
397
- const idx = line.indexOf(":");
398
- if (idx === -1) continue;
399
- const key = line.slice(0, idx).trim();
400
- const value = line.slice(idx + 1).trim();
401
- if (!key) continue;
402
- if (value === "" && i + 1 < lines.length) {
403
- const nested = {};
404
- while (i + 1 < lines.length) {
405
- const next = lines[i + 1];
406
- if (!next.startsWith(" ") || next.trim() === "") break;
407
- const nIdx = next.indexOf(":");
408
- if (nIdx === -1) break;
409
- const nKey = next.slice(0, nIdx).trim();
410
- const nValue = next.slice(nIdx + 1).trim();
411
- nested[nKey] = nValue;
412
- i++;
413
- }
414
- if (Object.keys(nested).length) {
415
- frontmatter[key] = nested;
416
- }
417
- continue;
418
- }
419
- frontmatter[key] = parseValue(value);
420
- }
421
- return { frontmatter, body: raw.slice(match[0].length) };
422
- }
423
- function parseValue(value) {
424
- const arrayMatch = value.match(/^\[(.*)\]$/);
425
- if (arrayMatch) {
426
- return arrayMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
390
+ import { parse as parseYaml } from "yaml";
391
+ var FrontmatterParseError = class extends Error {
392
+ constructor(message) {
393
+ super(message);
394
+ this.name = "FrontmatterParseError";
427
395
  }
428
- if (value.toLowerCase() === "true") return true;
429
- if (value.toLowerCase() === "false") return false;
430
- if (/^\d+$/.test(value)) return Number(value);
431
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
432
- return value.slice(1, -1);
396
+ };
397
+ function parseFrontmatter(raw) {
398
+ const normalized = raw.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
399
+ if (!normalized.startsWith("---")) return { frontmatter: {}, body: normalized };
400
+ const endIndex = normalized.indexOf("\n---", 3);
401
+ if (endIndex === -1) return { frontmatter: {}, body: normalized };
402
+ const yamlString = normalized.slice(4, endIndex);
403
+ try {
404
+ const frontmatter = parseYaml(yamlString) ?? {};
405
+ return {
406
+ frontmatter,
407
+ body: normalized.slice(endIndex + 4).trim()
408
+ };
409
+ } catch (error) {
410
+ const message = error instanceof Error ? error.message : String(error);
411
+ throw new FrontmatterParseError(message);
433
412
  }
434
- return value;
435
413
  }
436
414
  async function loadMarkdown(filePath) {
437
415
  const raw = await readFile(filePath, "utf8");
@@ -543,6 +521,82 @@ function buildPhaseDirectiveMessage(phase, output) {
543
521
  return parts.join("\n");
544
522
  }
545
523
 
524
+ // src/harness/resource-validation.ts
525
+ var MAX_RESOURCE_ID_LENGTH = 64;
526
+ var MAX_DESCRIPTION_LENGTH = 1024;
527
+ var ResourceMetadataError = class extends Error {
528
+ constructor(code, message) {
529
+ super(message);
530
+ this.code = code;
531
+ this.name = "ResourceMetadataError";
532
+ }
533
+ code;
534
+ };
535
+ function validateResourceId(id, label = "id") {
536
+ const errors = [];
537
+ if (id.length > MAX_RESOURCE_ID_LENGTH) {
538
+ errors.push(`${label} exceeds ${MAX_RESOURCE_ID_LENGTH} characters (${id.length})`);
539
+ }
540
+ if (!/^[a-z0-9-]+$/.test(id)) {
541
+ errors.push(`${label} contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
542
+ }
543
+ if (id.startsWith("-") || id.endsWith("-")) {
544
+ errors.push(`${label} must not start or end with a hyphen`);
545
+ }
546
+ if (id.includes("--")) {
547
+ errors.push(`${label} must not contain consecutive hyphens`);
548
+ }
549
+ return errors;
550
+ }
551
+ function validateResourceName(name, id) {
552
+ const errors = [];
553
+ if (name !== id) {
554
+ errors.push(`name "${name}" does not match parent directory "${id}"`);
555
+ errors.push(...validateResourceId(name, "name"));
556
+ }
557
+ return errors;
558
+ }
559
+ function validateDescription(value) {
560
+ if (typeof value !== "string" || value.trim() === "") {
561
+ return {
562
+ warnings: ["description is required"],
563
+ missing: true
564
+ };
565
+ }
566
+ if (value.length > MAX_DESCRIPTION_LENGTH) {
567
+ return {
568
+ description: value,
569
+ warnings: [`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${value.length})`],
570
+ missing: false
571
+ };
572
+ }
573
+ return { description: value, warnings: [], missing: false };
574
+ }
575
+ function validateSkillReferences(value) {
576
+ if (value === void 0) return [];
577
+ if (!Array.isArray(value)) return ["skills must be an array"];
578
+ const errors = [];
579
+ for (const [index, skillId] of value.entries()) {
580
+ if (typeof skillId !== "string") {
581
+ errors.push(`skills[${index}] must be a string`);
582
+ continue;
583
+ }
584
+ errors.push(...validateResourceId(skillId, `skills[${index}]`));
585
+ }
586
+ return errors;
587
+ }
588
+ function validatePhaseTarget(value) {
589
+ if (value === void 0) return [];
590
+ if (typeof value !== "string") return ["target must be a string"];
591
+ if (value === "stop") return [];
592
+ return validateResourceId(value, "target");
593
+ }
594
+ function warnResourceDiagnostics(kind, filePath, diagnostics) {
595
+ for (const diagnostic of diagnostics) {
596
+ console.warn(`Invalid ${kind} metadata at "${filePath}": ${diagnostic}`);
597
+ }
598
+ }
599
+
546
600
  // src/harness/phases/loader.ts
547
601
  var PHASE_MARKER = "PHASE.md";
548
602
  function resolvePhasePath(input) {
@@ -551,28 +605,44 @@ function resolvePhasePath(input) {
551
605
  }
552
606
  async function loadPhase(targetPath) {
553
607
  const resolved = resolvePhasePath(targetPath);
554
- const { frontmatter, body } = await loadMarkdown(resolved);
555
- const id = inferResourceName(resolved, PHASE_MARKER);
556
- if (!frontmatter.name) {
557
- throw new Error(`Phase "${id}" at "${resolved}" is missing required field "name" in PHASE.md frontmatter.`);
558
- }
559
- if (!frontmatter.description) {
560
- throw new Error(`Phase "${id}" at "${resolved}" is missing required field "description" in PHASE.md frontmatter.`);
608
+ let loaded;
609
+ try {
610
+ loaded = await loadMarkdown(resolved);
611
+ } catch (error) {
612
+ if (!(error instanceof FrontmatterParseError)) throw error;
613
+ warnResourceDiagnostics("phase", resolved, [`frontmatter could not be parsed: ${error.message}`]);
614
+ throw new ResourceMetadataError("parse_failed", error.message);
615
+ }
616
+ const directoryName = inferResourceName(resolved, PHASE_MARKER);
617
+ const { frontmatter, body } = loaded;
618
+ const metadata = frontmatter;
619
+ const frontmatterName = typeof metadata.name === "string" ? metadata.name : void 0;
620
+ const name = frontmatterName || directoryName;
621
+ const description = validateDescription(metadata.description);
622
+ const diagnostics = validateResourceId(directoryName);
623
+ if (frontmatterName && frontmatterName !== directoryName) {
624
+ diagnostics.push(...validateResourceName(frontmatterName, directoryName));
625
+ }
626
+ diagnostics.push(...description.warnings);
627
+ diagnostics.push(...validateSkillReferences(metadata.skills));
628
+ diagnostics.push(...validatePhaseTarget(metadata.target));
629
+ warnResourceDiagnostics("phase", resolved, diagnostics);
630
+ if (description.missing) {
631
+ throw new ResourceMetadataError("invalid_metadata", "description is required");
561
632
  }
562
633
  const baseDir = dirname2(resolved);
563
634
  const phase = {
564
- id,
565
- name: frontmatter.name,
566
- description: frontmatter.description,
567
- tools: frontmatter.tools,
568
- skills: frontmatter.skills,
569
- target: frontmatter.target,
570
- input: frontmatter.input,
571
- isolated: frontmatter.isolated,
635
+ name,
636
+ description: description.description,
637
+ tools: metadata.tools,
638
+ skills: metadata.skills,
639
+ target: metadata.target,
640
+ input: metadata.input,
641
+ isolated: metadata.isolated,
572
642
  filePath: resolved,
573
643
  baseDir,
574
644
  content: body,
575
- model: parseModelRef(frontmatter.model)
645
+ model: parseModelRef(metadata.model)
576
646
  };
577
647
  const codePath = await discoverPhaseCode(baseDir);
578
648
  if (codePath) {
@@ -598,13 +668,21 @@ async function loadPhases(targetPath) {
598
668
  const phases = /* @__PURE__ */ new Map();
599
669
  const phasesDir = resolve(targetPath);
600
670
  if (existsSync(phasesDir) && statSync(phasesDir).isFile()) {
601
- const phase = await loadPhase(phasesDir);
602
- phases.set(phase.id, phase);
671
+ try {
672
+ const phase = await loadPhase(phasesDir);
673
+ phases.set(phase.name, phase);
674
+ } catch (error) {
675
+ if (!(error instanceof ResourceMetadataError)) throw error;
676
+ }
603
677
  return { phases, entryPhaseId: null };
604
678
  }
605
679
  if (existsSync(join(phasesDir, PHASE_MARKER))) {
606
- const phase = await loadPhase(phasesDir);
607
- phases.set(phase.id, phase);
680
+ try {
681
+ const phase = await loadPhase(phasesDir);
682
+ phases.set(phase.name, phase);
683
+ } catch (error) {
684
+ if (!(error instanceof ResourceMetadataError)) throw error;
685
+ }
608
686
  return { phases, entryPhaseId: null };
609
687
  }
610
688
  const entries = await readdir(phasesDir, { withFileTypes: true });
@@ -614,8 +692,9 @@ async function loadPhases(targetPath) {
614
692
  if (!existsSync(phaseFile)) continue;
615
693
  try {
616
694
  const phase = await loadPhase(phaseFile);
617
- phases.set(phase.id, phase);
695
+ phases.set(phase.name, phase);
618
696
  } catch (error) {
697
+ if (error instanceof ResourceMetadataError) continue;
619
698
  console.warn(`Failed to load phase "${entry.name}":`, error);
620
699
  }
621
700
  }
@@ -653,8 +732,7 @@ async function loadPhaseCode(codePath) {
653
732
  var DEFAULT_PHASE_ID = "default";
654
733
  function createDefaultPhase() {
655
734
  return {
656
- id: DEFAULT_PHASE_ID,
657
- name: "Execution Phase",
735
+ name: DEFAULT_PHASE_ID,
658
736
  description: "Executes concrete task operations and produces artifacts.",
659
737
  filePath: "",
660
738
  baseDir: "",
@@ -678,7 +756,7 @@ function normalizeRelativePath(path) {
678
756
  import Type from "typebox";
679
757
  var PhaseRouteTool = "route";
680
758
  function buildPhaseEntry(p) {
681
- const entry = { id: p.id, description: p.description };
759
+ const entry = { name: p.name, description: p.description };
682
760
  if (p.tools && p.tools.length > 0) {
683
761
  entry.available_tools = p.tools.join(", ");
684
762
  }
@@ -693,7 +771,7 @@ function buildPhaseEntry(p) {
693
771
  function buildRouteDescription(availablePhases) {
694
772
  const phasesBlock = buildStructuredSection("phase", [
695
773
  ...availablePhases.map(buildPhaseEntry),
696
- { id: "stop", description: "Terminate the workflow and return final result to the user" }
774
+ { name: "stop", description: "Terminate the workflow and return final result to the user" }
697
775
  ]);
698
776
  return [
699
777
  "Route execution to one or more phases concurrently, or stop execution.",
@@ -714,7 +792,7 @@ function buildRouteDescription(availablePhases) {
714
792
  function createRouteTool(availablePhases) {
715
793
  const DecisionTarget = Type.Object({
716
794
  phase: Type.Union([
717
- ...availablePhases.map((p) => Type.Literal(p.id)),
795
+ ...availablePhases.map((p) => Type.Literal(p.name)),
718
796
  Type.Literal("stop")
719
797
  ]),
720
798
  reason: Type.Optional(Type.String({ description: "Brief reason for this decision" })),
@@ -776,34 +854,27 @@ var DEFAULT_MAX_READ_BYTES = 64e3;
776
854
  var DEFAULT_BASH_TIMEOUT_MS = 3e4;
777
855
  var DEFAULT_MAX_BASH_OUTPUT_BYTES = 64e3;
778
856
  var ReadArgsSchema = Type2.Object({
779
- path: Type2.String(),
780
- maxBytes: Type2.Optional(Type2.Number()),
781
- type: Type2.Optional(Type2.Union([
782
- Type2.Literal("skill"),
783
- Type2.Literal("phase"),
784
- Type2.Literal("markdown"),
785
- Type2.Literal("code"),
786
- Type2.Literal("file")
787
- ]))
857
+ path: Type2.String({ description: "Path to the file to read." }),
858
+ offset: Type2.Optional(Type2.Number({ description: "1-based line to start from." })),
859
+ limit: Type2.Optional(Type2.Number({ description: "Maximum number of lines." }))
788
860
  });
789
861
  var ReadArgsValidator = Schema.Compile(ReadArgsSchema);
790
862
  var WriteArgsSchema = Type2.Object({
791
- path: Type2.String(),
792
- content: Type2.String()
863
+ path: Type2.String({ description: "Path to the file to write." }),
864
+ content: Type2.String({ description: "Complete file contents." })
793
865
  });
794
866
  var WriteArgsValidator = Schema.Compile(WriteArgsSchema);
795
867
  var EditArgsSchema = Type2.Object({
796
- path: Type2.String(),
797
- oldText: Type2.String(),
798
- newText: Type2.String(),
799
- replaceAll: Type2.Optional(Type2.Boolean())
868
+ path: Type2.String({ description: "Path to the file to edit." }),
869
+ edits: Type2.Array(Type2.Object({
870
+ oldText: Type2.String({ description: "Exact unique text to replace." }),
871
+ newText: Type2.String({ description: "Replacement text." })
872
+ }), { description: "One or more non-overlapping replacements." })
800
873
  });
801
874
  var EditArgsValidator = Schema.Compile(EditArgsSchema);
802
875
  var BashArgsSchema = Type2.Object({
803
- command: Type2.String(),
804
- cwd: Type2.Optional(Type2.String()),
805
- timeoutMs: Type2.Optional(Type2.Number()),
806
- maxOutputBytes: Type2.Optional(Type2.Number())
876
+ command: Type2.String({ description: "Bash command to execute." }),
877
+ timeout: Type2.Optional(Type2.Number({ description: "Timeout in seconds." }))
807
878
  });
808
879
  var BashArgsValidator = Schema.Compile(BashArgsSchema);
809
880
  var validatorCache = /* @__PURE__ */ new WeakMap();
@@ -906,12 +977,7 @@ async function executeRuntimeToolCall(input) {
906
977
  }
907
978
  await input.observe?.({ type: "tool_start", tool, args });
908
979
  try {
909
- const rawResult = await tool.execute(args, input.toolContext, input.signal);
910
- let result = {
911
- ...rawResult,
912
- toolCallId: input.toolCall.id,
913
- toolName: tool.name
914
- };
980
+ let result = await tool.execute(args, input.toolContext, input.signal);
915
981
  if (input.afterToolCall) {
916
982
  await input.observe?.({
917
983
  type: "result_review_requested",
@@ -945,6 +1011,20 @@ function positiveNumber(value, name) {
945
1011
  }
946
1012
  return void 0;
947
1013
  }
1014
+ function positiveInteger(value, name) {
1015
+ if (!Number.isInteger(value) || value <= 0) {
1016
+ return `${name} must be a positive integer.`;
1017
+ }
1018
+ return void 0;
1019
+ }
1020
+ function readTextLines(text, offset, limit) {
1021
+ if (offset === void 0 && limit === void 0) {
1022
+ return text;
1023
+ }
1024
+ const start = (offset ?? 1) - 1;
1025
+ const lines = text.split(/\r\n|\n|\r/);
1026
+ return lines.slice(start, limit === void 0 ? void 0 : start + limit).join("\n");
1027
+ }
948
1028
  async function captureStream(stream, maxBytes) {
949
1029
  const reader = stream.getReader();
950
1030
  const chunks = [];
@@ -984,17 +1064,25 @@ async function captureStream(stream, maxBytes) {
984
1064
  function createReadTool(context) {
985
1065
  return {
986
1066
  name: "read",
987
- description: "Reads a text file within the workspace.",
1067
+ description: "Read a file in the workspace.",
988
1068
  parameters: ReadArgsSchema,
989
- promptSnippet: "Read a text file (max 64KB default).",
990
- promptGuidelines: [
991
- "Always read a file before editing or writing to understand its current content.",
992
- "Use maxBytes to limit output for large files."
993
- ],
1069
+ promptSnippet: "Read file contents.",
1070
+ promptGuidelines: ["Read files before editing them."],
994
1071
  async execute(args, toolContext) {
995
1072
  const parsed = ReadArgsValidator.Parse(args);
1073
+ const offsetError = parsed.offset === void 0 ? void 0 : positiveInteger(parsed.offset, "offset");
1074
+ const limitError = parsed.limit === void 0 ? void 0 : positiveInteger(parsed.limit, "limit");
1075
+ if (offsetError || limitError) {
1076
+ return toolResult({
1077
+ context: toolContext,
1078
+ toolName: "read",
1079
+ ok: false,
1080
+ content: null,
1081
+ error: offsetError ?? limitError
1082
+ });
1083
+ }
996
1084
  const resolved = resolveCoreToolPath(context, parsed.path);
997
- const maxBytes = parsed.maxBytes ?? context.maxReadBytes;
1085
+ const maxBytes = context.maxReadBytes;
998
1086
  const invalidLimit = positiveNumber(maxBytes, "maxBytes");
999
1087
  if (invalidLimit) {
1000
1088
  return toolResult({
@@ -1016,9 +1104,12 @@ function createReadTool(context) {
1016
1104
  });
1017
1105
  }
1018
1106
  const bytes = await readFile2(resolved.absolutePath);
1019
- const sliced = bytes.subarray(0, maxBytes);
1107
+ const source = new TextDecoder().decode(bytes);
1108
+ const selected = readTextLines(source, parsed.offset, parsed.limit);
1109
+ const selectedBytes = new TextEncoder().encode(selected);
1110
+ const sliced = selectedBytes.subarray(0, maxBytes);
1020
1111
  const text = new TextDecoder().decode(sliced);
1021
- const resourceType = parsed.type ?? detectResourceType(resolved.absolutePath);
1112
+ const resourceType = detectResourceType(resolved.absolutePath);
1022
1113
  let name;
1023
1114
  let baseDir;
1024
1115
  if (resourceType === "skill" || resourceType === "phase") {
@@ -1029,16 +1120,14 @@ function createReadTool(context) {
1029
1120
  } else {
1030
1121
  name = inferResourceName(resolved.absolutePath, basename2(resolved.absolutePath));
1031
1122
  }
1123
+ const formatted = resourceType === "skill" || resourceType === "phase" ? formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: text, baseDir }) : text;
1124
+ const content = selectedBytes.byteLength > maxBytes ? `${formatted}
1125
+ [truncated]` : formatted;
1032
1126
  return toolResult({
1033
1127
  context: toolContext,
1034
1128
  toolName: "read",
1035
1129
  ok: true,
1036
- content: {
1037
- path: resolved.relativePath,
1038
- content: formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: text, baseDir }),
1039
- sizeBytes: bytes.byteLength,
1040
- truncated: bytes.byteLength > maxBytes
1041
- }
1130
+ content
1042
1131
  });
1043
1132
  }
1044
1133
  };
@@ -1046,13 +1135,10 @@ function createReadTool(context) {
1046
1135
  function createWriteTool(context) {
1047
1136
  return {
1048
1137
  name: "write",
1049
- description: "Writes provided text content to a workspace file, creating parent directories as needed.",
1138
+ description: "Create or overwrite a file in the workspace.",
1050
1139
  parameters: WriteArgsSchema,
1051
- promptSnippet: "Write content to a file (creates parent dirs automatically).",
1052
- promptGuidelines: [
1053
- "Use write for new files or full file rewrites.",
1054
- "For partial edits, prefer the edit tool to avoid overwriting unchanged content."
1055
- ],
1140
+ promptSnippet: "Create or overwrite files.",
1141
+ promptGuidelines: ["Use edit for partial changes."],
1056
1142
  async execute(args, toolContext) {
1057
1143
  const parsed = WriteArgsValidator.Parse(args);
1058
1144
  const resolved = resolveCoreToolPath(context, parsed.path);
@@ -1062,69 +1148,74 @@ function createWriteTool(context) {
1062
1148
  context: toolContext,
1063
1149
  toolName: "write",
1064
1150
  ok: true,
1065
- content: {
1066
- path: resolved.relativePath,
1067
- bytesWritten: new TextEncoder().encode(parsed.content).byteLength
1068
- }
1151
+ content: `Successfully wrote ${parsed.content.length} bytes to ${resolved.relativePath}.`
1069
1152
  });
1070
1153
  }
1071
1154
  };
1072
1155
  }
1156
+ function applyEditReplacements(current, edits) {
1157
+ if (edits.length === 0) {
1158
+ throw new Error("edits must contain at least one replacement.");
1159
+ }
1160
+ const matches = [];
1161
+ for (const edit of edits) {
1162
+ if (!edit.oldText) throw new Error("oldText must not be empty.");
1163
+ const start = current.indexOf(edit.oldText);
1164
+ if (start < 0) throw new Error(`oldText not found in file.`);
1165
+ const second = current.indexOf(edit.oldText, start + edit.oldText.length);
1166
+ if (second >= 0) {
1167
+ const count = current.split(edit.oldText).length - 1;
1168
+ throw new Error(`oldText appears ${count} times; provide more context.`);
1169
+ }
1170
+ matches.push({ start, end: start + edit.oldText.length, newText: edit.newText });
1171
+ }
1172
+ matches.sort((a, b) => a.start - b.start);
1173
+ for (let index = 1; index < matches.length; index += 1) {
1174
+ const previous = matches[index - 1];
1175
+ const currentMatch = matches[index];
1176
+ if (currentMatch.start < previous.end) {
1177
+ throw new Error("edits must not overlap.");
1178
+ }
1179
+ }
1180
+ let content = current;
1181
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
1182
+ const match = matches[index];
1183
+ content = content.slice(0, match.start) + match.newText + content.slice(match.end);
1184
+ }
1185
+ return { content, replacements: matches.length };
1186
+ }
1073
1187
  function createEditTool(context) {
1074
1188
  return {
1075
1189
  name: "edit",
1076
- description: "Edits a workspace text file by replacing exact oldText with newText.",
1190
+ description: "Apply exact text replacements to a workspace file.",
1077
1191
  parameters: EditArgsSchema,
1078
- promptSnippet: "Replace exact text in a file (oldText \u2192 newText).",
1079
- promptGuidelines: [
1080
- "Read the file first to get the exact oldText to replace.",
1081
- "oldText must be an exact match including whitespace and indentation.",
1082
- "If oldText appears multiple times, set replaceAll=true or provide more surrounding context."
1083
- ],
1192
+ promptSnippet: "Apply exact text replacements.",
1193
+ promptGuidelines: ["Read the file first; each oldText must match exactly once."],
1084
1194
  async execute(args, toolContext) {
1085
1195
  const parsed = EditArgsValidator.Parse(args);
1086
- if (!parsed.oldText) {
1087
- return toolResult({
1088
- context: toolContext,
1089
- toolName: "edit",
1090
- ok: false,
1091
- content: null,
1092
- error: "oldText must not be empty."
1093
- });
1094
- }
1095
1196
  const resolved = resolveCoreToolPath(context, parsed.path);
1096
1197
  const current = await readFile2(resolved.absolutePath, "utf8");
1097
- if (!current.includes(parsed.oldText)) {
1098
- return toolResult({
1099
- context: toolContext,
1100
- toolName: "edit",
1101
- ok: false,
1102
- content: null,
1103
- error: `oldText not found in ${resolved.relativePath}.`
1104
- });
1105
- }
1106
- const matches = current.split(parsed.oldText).length - 1;
1107
- if (matches > 1 && !parsed.replaceAll) {
1198
+ let next;
1199
+ let replacements;
1200
+ try {
1201
+ const result = applyEditReplacements(current, parsed.edits);
1202
+ next = result.content;
1203
+ replacements = result.replacements;
1204
+ } catch (error) {
1108
1205
  return toolResult({
1109
1206
  context: toolContext,
1110
1207
  toolName: "edit",
1111
1208
  ok: false,
1112
1209
  content: null,
1113
- error: `oldText appears ${matches} times in ${resolved.relativePath}; set replaceAll=true or provide more context.`
1210
+ error: error instanceof Error ? error.message : String(error)
1114
1211
  });
1115
1212
  }
1116
- const replacements = parsed.replaceAll ? matches : 1;
1117
- const next = parsed.replaceAll ? current.split(parsed.oldText).join(parsed.newText) : current.replace(parsed.oldText, parsed.newText);
1118
1213
  await writeFile(resolved.absolutePath, next, "utf8");
1119
1214
  return toolResult({
1120
1215
  context: toolContext,
1121
1216
  toolName: "edit",
1122
1217
  ok: true,
1123
- content: {
1124
- path: resolved.relativePath,
1125
- replacements,
1126
- bytesWritten: new TextEncoder().encode(next).byteLength
1127
- }
1218
+ content: `Successfully replaced ${replacements} block(s) in ${resolved.relativePath}.`
1128
1219
  });
1129
1220
  }
1130
1221
  };
@@ -1132,19 +1223,15 @@ function createEditTool(context) {
1132
1223
  function createBashTool(context) {
1133
1224
  return {
1134
1225
  name: "bash",
1135
- description: "Runs a bash command within the workspace.",
1226
+ description: "Run a bash command in the workspace.",
1136
1227
  parameters: BashArgsSchema,
1137
- promptSnippet: "Execute a bash command (30s timeout, 64KB output limit).",
1138
- promptGuidelines: [
1139
- "Use bash for build commands, tests, git operations, and system tools.",
1140
- "Prefer dedicated tools (read/write/edit) for file operations.",
1141
- "Set timeoutMs for long-running commands."
1142
- ],
1228
+ promptSnippet: "Run shell commands.",
1229
+ promptGuidelines: ["Use read/write/edit for file operations."],
1143
1230
  async execute(args, toolContext, signal) {
1144
1231
  const parsed = BashArgsValidator.Parse(args);
1145
- const timeoutMs = parsed.timeoutMs ?? context.bashTimeoutMs;
1146
- const maxOutputBytes = parsed.maxOutputBytes ?? context.maxBashOutputBytes;
1147
- const invalidTimeout = positiveNumber(timeoutMs, "timeoutMs");
1232
+ const timeoutMs = parsed.timeout === void 0 ? context.bashTimeoutMs : parsed.timeout * 1e3;
1233
+ const maxOutputBytes = context.maxBashOutputBytes;
1234
+ const invalidTimeout = positiveNumber(timeoutMs, "timeout");
1148
1235
  const invalidOutputLimit = positiveNumber(maxOutputBytes, "maxOutputBytes");
1149
1236
  if (invalidTimeout || invalidOutputLimit) {
1150
1237
  return toolResult({
@@ -1155,7 +1242,7 @@ function createBashTool(context) {
1155
1242
  error: invalidTimeout ?? invalidOutputLimit
1156
1243
  });
1157
1244
  }
1158
- const cwd = resolveCoreToolPath(context, parsed.cwd ?? ".");
1245
+ const cwd = resolveCoreToolPath(context, ".");
1159
1246
  let timedOut = false;
1160
1247
  let aborted = false;
1161
1248
  const proc = Bun.spawn(["bash", "-lc", parsed.command], {
@@ -1182,19 +1269,14 @@ function createBashTool(context) {
1182
1269
  proc.exited
1183
1270
  ]);
1184
1271
  const ok = exitCode === 0 && !timedOut && !aborted;
1272
+ const output = [stdout.text, stderr.text].filter(Boolean).join(stdout.text && stderr.text ? "\n" : "");
1273
+ const content = stdout.truncated || stderr.truncated ? `${output}
1274
+ [truncated]` : output;
1185
1275
  return toolResult({
1186
1276
  context: toolContext,
1187
1277
  toolName: "bash",
1188
1278
  ok,
1189
- content: {
1190
- command: parsed.command,
1191
- cwd: cwd.relativePath,
1192
- exitCode,
1193
- stdout: stdout.text,
1194
- stderr: stderr.text,
1195
- stdoutTruncated: stdout.truncated,
1196
- stderrTruncated: stderr.truncated
1197
- },
1279
+ content,
1198
1280
  ...ok ? {} : {
1199
1281
  error: timedOut ? `Command timed out after ${timeoutMs}ms.` : aborted ? "Command aborted." : `Command exited with ${exitCode}.`
1200
1282
  }
@@ -1210,9 +1292,9 @@ function createCoreTools(input = {}) {
1210
1292
  const context = createCoreToolContext(input);
1211
1293
  return [
1212
1294
  createReadTool(context),
1213
- createWriteTool(context),
1295
+ createBashTool(context),
1214
1296
  createEditTool(context),
1215
- createBashTool(context)
1297
+ createWriteTool(context)
1216
1298
  ];
1217
1299
  }
1218
1300
 
@@ -1403,6 +1485,23 @@ function removePhaseMessage(messages, msgId) {
1403
1485
  const idx = messages.findIndex((m) => m.id === msgId);
1404
1486
  if (idx !== -1) messages.splice(idx, 1);
1405
1487
  }
1488
+ function findLatestUserInputMessage(messages) {
1489
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1490
+ const message = messages[index];
1491
+ const kind = message.metadata?.kind;
1492
+ if (message.role === "user" && kind !== "phase_prompt" && kind !== "phase_input") {
1493
+ return message;
1494
+ }
1495
+ }
1496
+ return void 0;
1497
+ }
1498
+ function moveMessageBefore(messages, messageId, targetId) {
1499
+ const messageIndex = messages.findIndex((message2) => message2.id === messageId);
1500
+ const targetIndex = messages.findIndex((message2) => message2.id === targetId);
1501
+ if (messageIndex === -1 || targetIndex === -1 || messageIndex < targetIndex) return;
1502
+ const [message] = messages.splice(messageIndex, 1);
1503
+ if (message) messages.splice(targetIndex, 0, message);
1504
+ }
1406
1505
  function normalizePayload(payload) {
1407
1506
  if (typeof payload === "string") {
1408
1507
  try {
@@ -1423,12 +1522,12 @@ function injectPhaseContent(phase, output, messages, mirror) {
1423
1522
  try {
1424
1523
  const phaseContent = phase.filePath ? readPhaseContent(phase) : phase.content ?? phase.description ?? "";
1425
1524
  const content = buildPhaseDirectiveMessage(
1426
- { name: phase.id, content: phaseContent },
1525
+ { name: phase.name, content: phaseContent },
1427
1526
  output
1428
1527
  );
1429
1528
  const message = createMessage("user", content, {
1430
1529
  kind: "phase_prompt",
1431
- phase: phase.id
1530
+ phase: phase.name
1432
1531
  });
1433
1532
  messages.push(message);
1434
1533
  if (mirror && mirror !== messages) mirror.push(message);
@@ -1539,6 +1638,15 @@ function createToolResultContent(result) {
1539
1638
  }
1540
1639
  ];
1541
1640
  }
1641
+ function createRouteToolResultContent(toolCall) {
1642
+ return [
1643
+ {
1644
+ type: "tool_result",
1645
+ toolUseId: toolCall.id,
1646
+ content: '{"ok": true}'
1647
+ }
1648
+ ];
1649
+ }
1542
1650
  async function executePhaseWithModel(ctx) {
1543
1651
  const executableToolNames = new Set(
1544
1652
  ctx.context.tools.filter((tool) => tool.name !== PhaseRouteTool).map((tool) => tool.name)
@@ -1561,6 +1669,13 @@ async function executePhaseWithModel(ctx) {
1561
1669
  phase: ctx.phase.name,
1562
1670
  toolCalls: collected.toolCalls
1563
1671
  };
1672
+ for (const toolCall of collected.toolCalls) {
1673
+ if (toolCall.name !== PhaseRouteTool) continue;
1674
+ const messageId = ctx.messageManager.start("tool", createRouteToolResultContent(toolCall), {
1675
+ phase: ctx.phase.name
1676
+ });
1677
+ await ctx.messageManager.end(messageId);
1678
+ }
1564
1679
  const executableToolCalls = collected.toolCalls.filter(
1565
1680
  (toolCall) => executableToolNames.has(toolCall.name)
1566
1681
  );
@@ -1570,7 +1685,7 @@ async function executePhaseWithModel(ctx) {
1570
1685
  for (const toolCall of executableToolCalls) {
1571
1686
  const result = await ctx.execution.executeTool(roundContext, toolCall);
1572
1687
  const messageId = ctx.messageManager.start("tool", createToolResultContent(result), {
1573
- phase: ctx.phase.id
1688
+ phase: ctx.phase.name
1574
1689
  });
1575
1690
  await ctx.messageManager.end(messageId);
1576
1691
  }
@@ -1603,17 +1718,20 @@ async function runPhaseLoop(config, state, registry) {
1603
1718
  }
1604
1719
  let previousPayload = resumingSuspendedRun ? state.continuation?.previousPayload : void 0;
1605
1720
  let previousPhaseMsgId = resumingSuspendedRun ? state.continuation?.previousPhaseMessageId : void 0;
1721
+ let previousPhaseInputMsgId;
1606
1722
  let previousResults = resumingSuspendedRun ? state.continuation?.previousResults?.map((result) => ({ ...result })) ?? [] : [];
1607
1723
  let pendingInstruction = resumingSuspendedRun ? state.continuation?.pendingInstruction : void 0;
1608
1724
  while (currentPhaseId) {
1609
1725
  const availablePhases = [];
1610
1726
  for (const [, phase2] of registry.phases) {
1611
- availablePhases.push({ id: phase2.id, name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
1727
+ availablePhases.push({ name: phase2.name, description: phase2.description, tools: phase2.tools, skills: phase2.skills, input: phase2.input, isolated: phase2.isolated });
1612
1728
  }
1613
1729
  const abortResult = LoopGuard.checkAbort(config.signal);
1614
1730
  if (abortResult.stopReason !== "none") {
1615
1731
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1732
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1616
1733
  previousPhaseMsgId = void 0;
1734
+ previousPhaseInputMsgId = void 0;
1617
1735
  return completeRun(config, state, createOutcome.aborted());
1618
1736
  }
1619
1737
  state.metrics.iterations++;
@@ -1672,13 +1790,17 @@ async function runPhaseLoop(config, state, registry) {
1672
1790
  if (extBefore.abort) {
1673
1791
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1674
1792
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1793
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1675
1794
  previousPhaseMsgId = void 0;
1795
+ previousPhaseInputMsgId = void 0;
1676
1796
  return completeRun(config, state, extBefore.abort);
1677
1797
  }
1678
1798
  if (extBefore.skip) {
1679
1799
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1680
1800
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1801
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1681
1802
  previousPhaseMsgId = void 0;
1803
+ previousPhaseInputMsgId = void 0;
1682
1804
  if (extBefore.skip.route === "stop") {
1683
1805
  return completeRun(config, state, {
1684
1806
  id: "skip",
@@ -1696,13 +1818,32 @@ async function runPhaseLoop(config, state, registry) {
1696
1818
  if (enteringNewPhase || !phaseMessageIsPresent) {
1697
1819
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1698
1820
  removePhaseMessage(phaseContext.messages, previousPhaseMsgId);
1821
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1822
+ removePhaseMessage(phaseContext.messages, previousPhaseInputMsgId);
1699
1823
  previousPhaseMsgId = void 0;
1824
+ previousPhaseInputMsgId = void 0;
1825
+ const latestUserInputMessage = findLatestUserInputMessage(config.context.messages);
1826
+ const lastMessageBeforePhase = config.context.messages.at(-1);
1700
1827
  previousPhaseMsgId = injectPhaseContent(
1701
1828
  phase,
1702
1829
  { results: previousResults, instruction: pendingInstruction },
1703
1830
  config.context.messages,
1704
1831
  phaseContext.messages
1705
1832
  );
1833
+ if (previousPhaseMsgId) {
1834
+ if (lastMessageBeforePhase?.role === "user") {
1835
+ moveMessageBefore(config.context.messages, previousPhaseMsgId, lastMessageBeforePhase.id);
1836
+ moveMessageBefore(phaseContext.messages, previousPhaseMsgId, lastMessageBeforePhase.id);
1837
+ } else if (latestUserInputMessage) {
1838
+ const phaseInputMessage = createMessage("user", latestUserInputMessage.content, {
1839
+ kind: "phase_input",
1840
+ phase: phase.name
1841
+ });
1842
+ config.context.messages.push(phaseInputMessage);
1843
+ phaseContext.messages.push(phaseInputMessage);
1844
+ previousPhaseInputMsgId = phaseInputMessage.id;
1845
+ }
1846
+ }
1706
1847
  previousResults = [];
1707
1848
  pendingInstruction = void 0;
1708
1849
  }
@@ -1720,7 +1861,9 @@ async function runPhaseLoop(config, state, registry) {
1720
1861
  if (extAfter.abort) {
1721
1862
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1722
1863
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1864
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1723
1865
  previousPhaseMsgId = void 0;
1866
+ previousPhaseInputMsgId = void 0;
1724
1867
  return completeRun(config, state, extAfter.abort);
1725
1868
  }
1726
1869
  if (extAfter.retry && (phase.run || phase.factory)) {
@@ -1740,7 +1883,9 @@ async function runPhaseLoop(config, state, registry) {
1740
1883
  if (preAbort.stopReason !== "none") {
1741
1884
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1742
1885
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1886
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1743
1887
  previousPhaseMsgId = void 0;
1888
+ previousPhaseInputMsgId = void 0;
1744
1889
  return completeRun(config, state, createOutcome.aborted());
1745
1890
  }
1746
1891
  const requestedAt = createTimestamp();
@@ -1768,7 +1913,9 @@ async function runPhaseLoop(config, state, registry) {
1768
1913
  if (abortResult2.stopReason !== "none") {
1769
1914
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1770
1915
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1916
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1771
1917
  previousPhaseMsgId = void 0;
1918
+ previousPhaseInputMsgId = void 0;
1772
1919
  return completeRun(config, state, createOutcome.aborted());
1773
1920
  }
1774
1921
  for (const message of userMessages) {
@@ -1779,7 +1926,9 @@ async function runPhaseLoop(config, state, registry) {
1779
1926
  }
1780
1927
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1781
1928
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1929
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1782
1930
  previousPhaseMsgId = void 0;
1931
+ previousPhaseInputMsgId = void 0;
1783
1932
  return completeRun(config, state, createOutcome.default(output, config.context.messages));
1784
1933
  }
1785
1934
  if (output.route === "continue") {
@@ -1822,7 +1971,9 @@ async function runPhaseLoop(config, state, registry) {
1822
1971
  const entryPhaseId = phase.target ?? registry.entryPhaseId;
1823
1972
  if (entryPhaseId === "stop") {
1824
1973
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1974
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1825
1975
  previousPhaseMsgId = void 0;
1976
+ previousPhaseInputMsgId = void 0;
1826
1977
  return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
1827
1978
  }
1828
1979
  currentPhaseId = entryPhaseId;
@@ -1839,13 +1990,17 @@ async function runPhaseLoop(config, state, registry) {
1839
1990
  }
1840
1991
  if (nextRoute === "stop") {
1841
1992
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1993
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1842
1994
  previousPhaseMsgId = void 0;
1995
+ previousPhaseInputMsgId = void 0;
1843
1996
  return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
1844
1997
  }
1845
1998
  const targetPhaseId = nextRoute;
1846
1999
  if (!registry.phases.has(targetPhaseId)) {
1847
2000
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
2001
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1848
2002
  previousPhaseMsgId = void 0;
2003
+ previousPhaseInputMsgId = void 0;
1849
2004
  return completeRun(config, state, createOutcome.phaseNotFound(output));
1850
2005
  }
1851
2006
  state.metrics.phaseTransitions.push({
@@ -1854,7 +2009,7 @@ async function runPhaseLoop(config, state, registry) {
1854
2009
  ts: createTimestamp()
1855
2010
  });
1856
2011
  previousPayload = output.payload;
1857
- previousResults = output.payload !== void 0 ? [{ name: phase.id, output: output.payload }] : [];
2012
+ previousResults = output.payload !== void 0 ? [{ name: phase.name, output: output.payload }] : [];
1858
2013
  currentPhaseId = targetPhaseId;
1859
2014
  }
1860
2015
  throw new Error("Phase machine exited without a stop or abort transition.");
@@ -1961,24 +2116,31 @@ async function executePhase(ctx) {
1961
2116
  return executePhaseWithModel(ctx);
1962
2117
  }
1963
2118
  async function executeToolCall(input) {
2119
+ let result;
1964
2120
  if (input.config.runtime?.tools) {
1965
- return input.config.runtime.tools({
2121
+ result = await input.config.runtime.tools({
1966
2122
  config: input.config,
1967
2123
  toolCall: input.toolCall
1968
2124
  });
2125
+ } else {
2126
+ const toolContext = {
2127
+ skills: input.config.context.skills,
2128
+ toolCallId: input.toolCall.id
2129
+ };
2130
+ result = await executeRuntimeToolCall({
2131
+ tools: input.tools,
2132
+ toolCall: input.toolCall,
2133
+ toolContext,
2134
+ beforeToolCall: input.config.beforeToolCall,
2135
+ afterToolCall: input.config.afterToolCall,
2136
+ signal: input.config.signal
2137
+ });
1969
2138
  }
1970
- const toolContext = {
1971
- skills: input.config.context.skills,
1972
- toolCallId: input.toolCall.id
2139
+ return {
2140
+ ...result,
2141
+ toolCallId: input.toolCall.id,
2142
+ toolName: input.toolCall.name
1973
2143
  };
1974
- return executeRuntimeToolCall({
1975
- tools: input.tools,
1976
- toolCall: input.toolCall,
1977
- toolContext,
1978
- beforeToolCall: input.config.beforeToolCall,
1979
- afterToolCall: input.config.afterToolCall,
1980
- signal: input.config.signal
1981
- });
1982
2144
  }
1983
2145
  function createPhaseExecution(config, state, allTools, phase, messageManager, toolExecutionManager, registry) {
1984
2146
  return {
@@ -2000,7 +2162,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2000
2162
  },
2001
2163
  async invokeModel(phaseContext) {
2002
2164
  if (config.beforePrompt) {
2003
- phaseContext = await config.beforePrompt(phase.id, phaseContext);
2165
+ phaseContext = await config.beforePrompt(phase.name, phaseContext);
2004
2166
  }
2005
2167
  const request = buildModelRequest({
2006
2168
  systemPrompt: phaseContext.systemPrompt,
@@ -2027,7 +2189,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2027
2189
  config,
2028
2190
  message: messageManager,
2029
2191
  request,
2030
- phaseId: phase.id
2192
+ phaseId: phase.name
2031
2193
  }),
2032
2194
  {
2033
2195
  signal: config.signal,
@@ -2049,7 +2211,7 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
2049
2211
  }
2050
2212
  )
2051
2213
  );
2052
- await config.onModelTranscript?.(result.transcript, { phase: phase.id, model: phase.model ?? config.model });
2214
+ await config.onModelTranscript?.(result.transcript, { phase: phase.name, model: phase.model ?? config.model });
2053
2215
  return result;
2054
2216
  },
2055
2217
  async executeTool(_context, toolCall) {
@@ -2082,7 +2244,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2082
2244
  sourcePhaseId
2083
2245
  },
2084
2246
  state: {
2085
- current: phase.id,
2247
+ current: phase.name,
2086
2248
  available: Array.from(registry.phases.keys()),
2087
2249
  iterations: 0,
2088
2250
  payload
@@ -2101,7 +2263,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2101
2263
  if (phaseMsgId) removePhaseMessage(messages, phaseMsgId);
2102
2264
  const decision = output.toolCalls ? extractRouteCall(output.toolCalls) : void 0;
2103
2265
  const resultPayload = decision?.decision[0]?.payload !== void 0 ? normalizePayload(decision.decision[0].payload) : output.payload;
2104
- return { instanceId, phaseId: phase.id, payload: resultPayload, content: output.message };
2266
+ return { instanceId, phaseId: phase.name, payload: resultPayload, content: output.message };
2105
2267
  }
2106
2268
  async function waitForBackgroundTasks(backgroundTasks) {
2107
2269
  const entries = Array.from(backgroundTasks.entries());
@@ -2189,23 +2351,55 @@ function readSkillContent(skill) {
2189
2351
  }
2190
2352
  async function loadSkill(path) {
2191
2353
  const resolved = resolveSkillPath(path);
2192
- const { frontmatter, body } = await loadMarkdown(resolved);
2354
+ let loaded;
2355
+ try {
2356
+ loaded = await loadMarkdown(resolved);
2357
+ } catch (error) {
2358
+ if (!(error instanceof FrontmatterParseError)) throw error;
2359
+ warnResourceDiagnostics("skill", resolved, [`frontmatter could not be parsed: ${error.message}`]);
2360
+ throw new ResourceMetadataError("parse_failed", error.message);
2361
+ }
2362
+ const { frontmatter, body } = loaded;
2363
+ const metadata = frontmatter;
2364
+ const id = inferResourceName(resolved, SKILL_MARKER);
2365
+ const frontmatterName = typeof metadata.name === "string" ? metadata.name : void 0;
2366
+ const name = frontmatterName || id;
2367
+ const description = validateDescription(metadata.description);
2368
+ const diagnostics = validateResourceId(id);
2369
+ if (frontmatterName && frontmatterName !== id) {
2370
+ diagnostics.push(...validateResourceName(frontmatterName, id));
2371
+ }
2372
+ diagnostics.push(...description.warnings);
2373
+ warnResourceDiagnostics("skill", resolved, diagnostics);
2374
+ if (description.missing) {
2375
+ throw new ResourceMetadataError("invalid_metadata", "description is required");
2376
+ }
2193
2377
  return {
2194
- name: frontmatter.name ?? inferResourceName(resolved, SKILL_MARKER),
2195
- description: frontmatter.description ?? "",
2378
+ name,
2379
+ description: description.description,
2196
2380
  filePath: resolved,
2197
2381
  baseDir: dirname4(resolved),
2198
2382
  content: body,
2199
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
2383
+ disableModelInvocation: metadata["disable-model-invocation"] === true
2200
2384
  };
2201
2385
  }
2202
2386
  async function loadSkills(targetPath) {
2203
2387
  const skillsDir = resolve4(targetPath);
2204
2388
  if (existsSync2(skillsDir) && statSync2(skillsDir).isFile()) {
2205
- return [await loadSkill(skillsDir)];
2389
+ try {
2390
+ return [await loadSkill(skillsDir)];
2391
+ } catch (error) {
2392
+ if (error instanceof ResourceMetadataError) return [];
2393
+ throw error;
2394
+ }
2206
2395
  }
2207
2396
  if (existsSync2(join2(skillsDir, SKILL_MARKER))) {
2208
- return [await loadSkill(skillsDir)];
2397
+ try {
2398
+ return [await loadSkill(skillsDir)];
2399
+ } catch (error) {
2400
+ if (error instanceof ResourceMetadataError) return [];
2401
+ throw error;
2402
+ }
2209
2403
  }
2210
2404
  const entries = await readdir2(skillsDir, { withFileTypes: true });
2211
2405
  const skills = [];
@@ -2217,6 +2411,7 @@ async function loadSkills(targetPath) {
2217
2411
  const skill = await loadSkill(skillFile);
2218
2412
  skills.push(skill);
2219
2413
  } catch (error) {
2414
+ if (error instanceof ResourceMetadataError) continue;
2220
2415
  console.warn(`Failed to load skill "${entry.name}":`, error);
2221
2416
  }
2222
2417
  }
@@ -2710,8 +2905,8 @@ var ExtensionRunner = class {
2710
2905
  // ---------------------------------------------------------------------------
2711
2906
  // Phase management
2712
2907
  // ---------------------------------------------------------------------------
2713
- getPhase(id) {
2714
- const reg = this.getRegisteredPhase(id);
2908
+ getPhase(name) {
2909
+ const reg = this.getRegisteredPhase(name);
2715
2910
  if (!reg) return void 0;
2716
2911
  return this.adaptToPhase(reg);
2717
2912
  }
@@ -2723,8 +2918,8 @@ var ExtensionRunner = class {
2723
2918
  createPhaseRegistry(input = {}) {
2724
2919
  const registered = this.collectRegisteredPhases();
2725
2920
  const phases = /* @__PURE__ */ new Map();
2726
- for (const [id, reg] of registered) {
2727
- phases.set(id, this.adaptToPhase(reg));
2921
+ for (const [name, reg] of registered) {
2922
+ phases.set(name, this.adaptToPhase(reg));
2728
2923
  }
2729
2924
  const entryPhaseId = input.entryPhaseId ?? null;
2730
2925
  return { phases, entryPhaseId };
@@ -2733,9 +2928,8 @@ var ExtensionRunner = class {
2733
2928
  adaptToPhase(reg) {
2734
2929
  const def = reg.definition;
2735
2930
  return {
2736
- id: def.id,
2737
- name: def.name ?? def.id,
2738
- description: def.description ?? "",
2931
+ name: def.name,
2932
+ description: def.description,
2739
2933
  tools: def.tools,
2740
2934
  skills: def.skills,
2741
2935
  target: def.target,
@@ -3002,22 +3196,27 @@ var ExtensionRunner = class {
3002
3196
  });
3003
3197
  }
3004
3198
  registerPhase(extension, registration) {
3005
- if (!registration.id) {
3006
- throw new Error(`Phase registration requires an "id" field.`);
3007
- }
3008
- if (!registration.name) {
3009
- throw new Error(`Phase registration "${registration.id}" requires a "name" field.`);
3199
+ if (typeof registration.name !== "string" || registration.name.length === 0) {
3200
+ throw new Error(`Phase registration requires a "name" field.`);
3201
+ }
3202
+ const name = registration.name;
3203
+ const description = validateDescription(registration.description);
3204
+ const errors = validateResourceId(name, "name");
3205
+ if (description.missing) errors.push(...description.warnings);
3206
+ errors.push(...validateSkillReferences(registration.skills));
3207
+ errors.push(...validatePhaseTarget(registration.target));
3208
+ if (errors.length > 0) {
3209
+ throw new Error(`Invalid phase registration "${name}": ${errors.join("; ")}`);
3010
3210
  }
3011
- if (!registration.description) {
3012
- throw new Error(`Phase registration "${registration.id}" requires a "description" field.`);
3211
+ if (description.warnings.length > 0) {
3212
+ warnResourceDiagnostics("phase", `extension ${extension.path}`, description.warnings);
3013
3213
  }
3014
- if (this.phases.has(registration.id)) {
3015
- throw new Error(`Duplicate phase id: ${registration.id}`);
3214
+ if (this.phases.has(name)) {
3215
+ throw new Error(`Duplicate phase name: ${name}`);
3016
3216
  }
3017
3217
  const definition = {
3018
- id: registration.id,
3019
- name: registration.name,
3020
- description: registration.description,
3218
+ name,
3219
+ description: description.description,
3021
3220
  run: registration.run,
3022
3221
  ...registration.model ? { model: registration.model } : {}
3023
3222
  };
@@ -3025,8 +3224,8 @@ var ExtensionRunner = class {
3025
3224
  definition,
3026
3225
  source: { extensionPath: extension.path }
3027
3226
  };
3028
- this.phases.set(registration.id, registered);
3029
- extension.phases.set(registration.id, registered);
3227
+ this.phases.set(name, registered);
3228
+ extension.phases.set(name, registered);
3030
3229
  this._phaseCache = null;
3031
3230
  }
3032
3231
  registerProvider(config) {
@@ -3053,8 +3252,8 @@ var ExtensionRunner = class {
3053
3252
  }
3054
3253
  this.pendingProviders.length = 0;
3055
3254
  }
3056
- getRegisteredPhase(id) {
3057
- return this.collectRegisteredPhases().get(id);
3255
+ getRegisteredPhase(name) {
3256
+ return this.collectRegisteredPhases().get(name);
3058
3257
  }
3059
3258
  collectRegisteredPhases() {
3060
3259
  if (this._phaseCache) return this._phaseCache;
@@ -4388,6 +4587,22 @@ var InMemorySessionStore = class {
4388
4587
  async open(sessionId) {
4389
4588
  return this.sessions.get(sessionId);
4390
4589
  }
4590
+ async list() {
4591
+ return Promise.all([...this.sessions.values()].map(async (session) => {
4592
+ const header = await session.getHeader();
4593
+ const entries = "listEntries" in session && typeof session.listEntries === "function" ? await session.listEntries() : [];
4594
+ return {
4595
+ id: header.id,
4596
+ title: header.title,
4597
+ createdAt: header.createdAt,
4598
+ updatedAt: header.updatedAt,
4599
+ messageCount: entries.filter((entry) => entry.type === "message").length
4600
+ };
4601
+ }));
4602
+ }
4603
+ async delete(sessionId) {
4604
+ return this.sessions.delete(sessionId);
4605
+ }
4391
4606
  };
4392
4607
  var JsonlSessionStore = class {
4393
4608
  constructor(sessionsDir) {
@@ -4403,6 +4618,9 @@ var JsonlSessionStore = class {
4403
4618
  list() {
4404
4619
  return LocalJsonlSessionManager.list(this.sessionsDir);
4405
4620
  }
4621
+ delete(sessionId) {
4622
+ return LocalJsonlSessionManager.delete(this.sessionsDir, sessionId);
4623
+ }
4406
4624
  };
4407
4625
 
4408
4626
  // src/runtime/agent-runtime.ts
@@ -4678,6 +4896,7 @@ var AgentRuntime = class _AgentRuntime {
4678
4896
  toolRuntime;
4679
4897
  bindings = /* @__PURE__ */ new Map();
4680
4898
  agents = /* @__PURE__ */ new Map();
4899
+ invalidatedAgents = /* @__PURE__ */ new Set();
4681
4900
  runHandles = /* @__PURE__ */ new Map();
4682
4901
  runWaiters = /* @__PURE__ */ new Map();
4683
4902
  pendingRuns = [];
@@ -4689,6 +4908,7 @@ var AgentRuntime = class _AgentRuntime {
4689
4908
  runningRuns = 0;
4690
4909
  pumping = false;
4691
4910
  nextWorkerId = 0;
4911
+ leaseRecoveryTimer;
4692
4912
  stopped = false;
4693
4913
  constructor(options) {
4694
4914
  this.stateStore = options.stateStore;
@@ -4723,7 +4943,17 @@ var AgentRuntime = class _AgentRuntime {
4723
4943
  const runtime = new _AgentRuntime(options);
4724
4944
  activeRuntime = runtime;
4725
4945
  try {
4946
+ if (options.sessionProvider) {
4947
+ for (const record of await options.stateStore.listAgents()) {
4948
+ if (record.lifecycleState !== "deleting") continue;
4949
+ const activeRuns = await options.stateStore.listRuns({ agentId: record.id, states: ["queued", "running", "suspended"] });
4950
+ if (activeRuns.length > 0) continue;
4951
+ await options.stateStore.deleteAgentData(record.id);
4952
+ await options.sessionProvider.delete(record.sessionId);
4953
+ }
4954
+ }
4726
4955
  await runtime.recover();
4956
+ runtime.startLeaseRecovery();
4727
4957
  return runtime;
4728
4958
  } catch (error) {
4729
4959
  await runtime.stop().catch(() => void 0);
@@ -4740,8 +4970,10 @@ var AgentRuntime = class _AgentRuntime {
4740
4970
  input: "",
4741
4971
  skills: options.context.skills
4742
4972
  });
4973
+ const header = await manager.getHeader();
4743
4974
  const record = await this.stateStore.createAgent({
4744
- sessionId: manager.getSessionId()
4975
+ sessionId: manager.getSessionId(),
4976
+ parentSessionId: header.parentSessionId
4745
4977
  });
4746
4978
  this.pausedAgents.delete(record.id);
4747
4979
  const agent = await this.attachAgent(record, manager, options);
@@ -4753,6 +4985,7 @@ var AgentRuntime = class _AgentRuntime {
4753
4985
  assertAgentOptions(options);
4754
4986
  const record = await this.stateStore.getAgent(agentId);
4755
4987
  if (!record) throw new Error(`Agent not found: ${agentId}.`);
4988
+ if (record.lifecycleState !== "active") throw new Error(`Session ${record.sessionId} is ${record.lifecycleState}.`);
4756
4989
  if (record.state === "paused") this.pausedAgents.add(record.id);
4757
4990
  else this.pausedAgents.delete(record.id);
4758
4991
  const provider = this.sessionProvider;
@@ -4766,6 +4999,10 @@ var AgentRuntime = class _AgentRuntime {
4766
4999
  async stop() {
4767
5000
  if (this.stopped) return;
4768
5001
  this.stopped = true;
5002
+ if (this.leaseRecoveryTimer) {
5003
+ clearInterval(this.leaseRecoveryTimer);
5004
+ this.leaseRecoveryTimer = void 0;
5005
+ }
4769
5006
  let suspendedAgents;
4770
5007
  try {
4771
5008
  suspendedAgents = new Set(
@@ -4857,6 +5094,82 @@ var AgentRuntime = class _AgentRuntime {
4857
5094
  async listRuns(input) {
4858
5095
  return this.stateStore.listRuns(input);
4859
5096
  }
5097
+ async listSessions(input = {}) {
5098
+ const records = await this.stateStore.listAgents();
5099
+ const items = this.sessionProvider ? await this.sessionProvider.list() : [];
5100
+ const summaries = new Map(items.map((item) => [item.id, item]));
5101
+ const activeRuns = await this.stateStore.listActiveRuns();
5102
+ return records.filter((record) => !input.state || record.lifecycleState === input.state).map((record) => {
5103
+ const item = summaries.get(record.sessionId);
5104
+ return {
5105
+ id: record.sessionId,
5106
+ state: record.lifecycleState,
5107
+ createdAt: record.createdAt,
5108
+ updatedAt: item?.updatedAt ?? record.updatedAt,
5109
+ ...record.lifecycleState === "archived" ? { archivedAt: record.updatedAt } : {},
5110
+ ...record.parentSessionId ? { parentSessionId: record.parentSessionId } : {},
5111
+ agentId: record.id,
5112
+ activeRunCount: activeRuns.filter((run) => run.agentId === record.id).length,
5113
+ ...item?.title ? { title: item.title } : {}
5114
+ };
5115
+ });
5116
+ }
5117
+ async archiveSession(sessionId) {
5118
+ const record = await this.requireSessionAgent(sessionId);
5119
+ if (record.lifecycleState === "archived") return;
5120
+ if (record.lifecycleState === "deleting") throw new Error(`Session ${sessionId} is being deleted.`);
5121
+ await this.stateStore.setAgentLifecycleState(record.id, "archived");
5122
+ this.publishEvents();
5123
+ }
5124
+ async unarchiveSession(sessionId) {
5125
+ const record = await this.requireSessionAgent(sessionId);
5126
+ if (record.lifecycleState === "active") return;
5127
+ if (record.lifecycleState === "deleting") throw new Error(`Session ${sessionId} is being deleted.`);
5128
+ await this.stateStore.setAgentLifecycleState(record.id, "active");
5129
+ await this.scheduleQueuedRuns(record.id);
5130
+ this.publishEvents();
5131
+ }
5132
+ async deleteSession(sessionId) {
5133
+ this.assertRunning();
5134
+ if (!this.sessionProvider) throw new Error("Agent Runtime requires a SessionManager provider to delete a Session.");
5135
+ if (!this.sessionProvider.delete) throw new Error("Session Manager provider does not support deletion.");
5136
+ const root = await this.requireSessionAgent(sessionId);
5137
+ const records = await this.stateStore.listAgents();
5138
+ const subtree = /* @__PURE__ */ new Set([root.id]);
5139
+ let changed = true;
5140
+ while (changed) {
5141
+ changed = false;
5142
+ for (const record of records) {
5143
+ if (record.parentSessionId && records.some((candidate) => subtree.has(candidate.id) && candidate.sessionId === record.parentSessionId) && !subtree.has(record.id)) {
5144
+ subtree.add(record.id);
5145
+ changed = true;
5146
+ }
5147
+ }
5148
+ }
5149
+ const activeRuns = await this.stateStore.listActiveRuns();
5150
+ if (activeRuns.some((run) => subtree.has(run.agentId))) {
5151
+ throw new Error(`Session ${sessionId} has active Runs and cannot be deleted.`);
5152
+ }
5153
+ const targets = records.filter((record) => subtree.has(record.id));
5154
+ for (const record of targets) await this.stateStore.setAgentLifecycleState(record.id, "deleting");
5155
+ for (const record of targets) {
5156
+ this.bindings.delete(record.id);
5157
+ this.agents.delete(record.id);
5158
+ this.invalidatedAgents.add(record.id);
5159
+ this.pausedAgents.delete(record.id);
5160
+ for (let index = this.pendingRuns.length - 1; index >= 0; index -= 1) {
5161
+ if (this.pendingRuns[index]?.agentId === record.id) this.pendingRuns.splice(index, 1);
5162
+ }
5163
+ await this.stateStore.deleteAgentData(record.id);
5164
+ await this.sessionProvider.delete(record.sessionId);
5165
+ }
5166
+ this.publishEvents();
5167
+ }
5168
+ async requireSessionAgent(sessionId) {
5169
+ const record = await this.stateStore.getAgentBySessionId(sessionId);
5170
+ if (!record) throw new Error(`Session not found: ${sessionId}.`);
5171
+ return record;
5172
+ }
4860
5173
  getAgent(agentId) {
4861
5174
  return this.agents.get(agentId);
4862
5175
  }
@@ -4943,13 +5256,26 @@ var AgentRuntime = class _AgentRuntime {
4943
5256
  return this.stateStore.listEvents(cursor);
4944
5257
  }
4945
5258
  async recover() {
4946
- await this.stateStore.recoverLeases();
5259
+ await this.recoverExpiredLeases();
4947
5260
  const agents = await this.stateStore.listAgents();
4948
5261
  for (const record of agents) {
4949
5262
  if (record.state === "paused") this.pausedAgents.add(record.id);
4950
5263
  }
4951
5264
  this.publishEvents();
4952
5265
  }
5266
+ startLeaseRecovery() {
5267
+ this.leaseRecoveryTimer = setInterval(() => {
5268
+ void this.recoverExpiredLeases().catch(() => void 0);
5269
+ }, this.leaseRenewalIntervalMs);
5270
+ }
5271
+ async recoverExpiredLeases() {
5272
+ const recovered = await this.stateStore.recoverExpiredLeases();
5273
+ for (const run of recovered) {
5274
+ this.notifyRun(run);
5275
+ if (this.bindings.has(run.agentId)) this.scheduleRun(run.agentId, run.id);
5276
+ }
5277
+ if (recovered.length > 0) this.publishEvents();
5278
+ }
4953
5279
  async scheduleQueuedRuns(agentId) {
4954
5280
  if (!this.bindings.has(agentId)) return;
4955
5281
  const runs = await this.stateStore.listRuns({ agentId, states: ["queued"] });
@@ -4976,7 +5302,11 @@ var AgentRuntime = class _AgentRuntime {
4976
5302
  }
4977
5303
  async submitAgentInput(agentId, input, persist) {
4978
5304
  this.assertRunning();
5305
+ if (this.invalidatedAgents.has(agentId)) throw new Error(`Session for Agent ${agentId} was deleted.`);
4979
5306
  if (!this.bindings.has(agentId)) throw new Error(`Agent ${agentId} has no live Agent Binding.`);
5307
+ const record = await this.stateStore.getAgent(agentId);
5308
+ if (!record) throw new Error(`Agent ${agentId} was deleted.`);
5309
+ if (record.lifecycleState !== "active") throw new Error(`Session ${record.sessionId} is ${record.lifecycleState}.`);
4980
5310
  const enqueued = await this.stateStore.enqueueAgentInput({ agentId, input });
4981
5311
  await persist();
4982
5312
  if (enqueued.resumed) this.notifyRun(enqueued.run);
@@ -5232,6 +5562,8 @@ var InMemoryRuntimeStateStore = class {
5232
5562
  id: createId("agt"),
5233
5563
  sessionId: input.sessionId,
5234
5564
  state: "active",
5565
+ lifecycleState: "active",
5566
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
5235
5567
  createdAt: timestamp,
5236
5568
  updatedAt: timestamp
5237
5569
  };
@@ -5243,6 +5575,10 @@ var InMemoryRuntimeStateStore = class {
5243
5575
  const agent = this.agents.get(agentId);
5244
5576
  return agent ? clone2(agent) : void 0;
5245
5577
  }
5578
+ async getAgentBySessionId(sessionId) {
5579
+ const agent = [...this.agents.values()].find((candidate) => candidate.sessionId === sessionId);
5580
+ return agent ? clone2(agent) : void 0;
5581
+ }
5246
5582
  async listAgents() {
5247
5583
  return clone2([...this.agents.values()]);
5248
5584
  }
@@ -5255,6 +5591,27 @@ var InMemoryRuntimeStateStore = class {
5255
5591
  this.recordEvent(state === "paused" ? "agent_paused" : "agent_resumed", { agentId });
5256
5592
  return clone2(agent);
5257
5593
  }
5594
+ async setAgentLifecycleState(agentId, state) {
5595
+ const agent = this.requireAgent(agentId);
5596
+ if (agent.lifecycleState === state) return clone2(agent);
5597
+ agent.lifecycleState = state;
5598
+ agent.updatedAt = createTimestamp();
5599
+ if (state === "archived") this.recordEvent("session_archived", { agentId });
5600
+ if (state === "active") this.recordEvent("session_unarchived", { agentId });
5601
+ return clone2(agent);
5602
+ }
5603
+ async deleteAgentData(agentId) {
5604
+ this.requireAgent(agentId);
5605
+ const runIds = new Set([...this.runs.values()].filter((run) => run.agentId === agentId).map((run) => run.id));
5606
+ for (const [id, call] of this.toolCalls) if (call.agentId === agentId || runIds.has(call.runId)) this.toolCalls.delete(id);
5607
+ for (const [id, run] of this.runs) if (run.agentId === agentId) this.runs.delete(id);
5608
+ for (const [id, message] of this.messages) if (message.agentId === agentId) this.messages.delete(id);
5609
+ for (let index = this.events.length - 1; index >= 0; index -= 1) {
5610
+ const event = this.events[index];
5611
+ if (event?.agentId === agentId || event?.runId && runIds.has(event.runId)) this.events.splice(index, 1);
5612
+ }
5613
+ this.agents.delete(agentId);
5614
+ }
5258
5615
  async enqueueAgentInput(input) {
5259
5616
  return this.enqueueAgentInputState(input);
5260
5617
  }
@@ -5684,10 +6041,15 @@ import { Database } from "bun:sqlite";
5684
6041
 
5685
6042
  // src/runtime/runtime-schema.ts
5686
6043
  var RUNTIME_SCHEMA_SQL = `
6044
+ CREATE TABLE IF NOT EXISTS runtime_schema (
6045
+ version INTEGER PRIMARY KEY NOT NULL
6046
+ );
5687
6047
  CREATE TABLE IF NOT EXISTS agents (
5688
6048
  id TEXT PRIMARY KEY NOT NULL,
5689
6049
  session_id TEXT NOT NULL,
6050
+ parent_session_id TEXT,
5690
6051
  state TEXT NOT NULL CHECK (state IN ('active', 'paused')),
6052
+ lifecycle_state TEXT NOT NULL CHECK (lifecycle_state IN ('active', 'archived', 'deleting')),
5691
6053
  created_at TEXT NOT NULL,
5692
6054
  updated_at TEXT NOT NULL
5693
6055
  );
@@ -5770,6 +6132,8 @@ var RUNTIME_SCHEMA_SQL = `
5770
6132
 
5771
6133
  CREATE INDEX IF NOT EXISTS agent_runs_runnable_idx
5772
6134
  ON agent_runs (state, created_at);
6135
+ CREATE UNIQUE INDEX IF NOT EXISTS agents_session_id_unique
6136
+ ON agents (session_id);
5773
6137
  `;
5774
6138
  function initializeRuntimeSchema(database) {
5775
6139
  const initialize = database.transaction(() => {
@@ -5786,6 +6150,7 @@ function initializeRuntimeSchema(database) {
5786
6150
  }
5787
6151
  });
5788
6152
  initialize();
6153
+ database.run("INSERT OR IGNORE INTO runtime_schema (version) VALUES (2)");
5789
6154
  }
5790
6155
 
5791
6156
  // src/runtime/sqlite-store.ts
@@ -5829,13 +6194,15 @@ var SqliteRuntimeStateStore = class {
5829
6194
  id: createId("agt"),
5830
6195
  sessionId: input.sessionId,
5831
6196
  state: "active",
6197
+ lifecycleState: "active",
6198
+ ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
5832
6199
  createdAt: timestamp,
5833
6200
  updatedAt: timestamp
5834
6201
  };
5835
6202
  const create = this.database.transaction(() => {
5836
6203
  this.database.run(
5837
- "INSERT INTO agents (id, session_id, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
5838
- [agent.id, agent.sessionId, agent.state, agent.createdAt, agent.updatedAt]
6204
+ "INSERT INTO agents (id, session_id, parent_session_id, state, lifecycle_state, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
6205
+ [agent.id, agent.sessionId, agent.parentSessionId ?? null, agent.state, agent.lifecycleState, agent.createdAt, agent.updatedAt]
5839
6206
  );
5840
6207
  this.recordEvent("agent_created", { agentId: agent.id });
5841
6208
  });
@@ -5846,6 +6213,10 @@ var SqliteRuntimeStateStore = class {
5846
6213
  const row = this.database.query("SELECT * FROM agents WHERE id = ?").get(agentId);
5847
6214
  return row ? this.agentFromRow(row) : void 0;
5848
6215
  }
6216
+ async getAgentBySessionId(sessionId) {
6217
+ const row = this.database.query("SELECT * FROM agents WHERE session_id = ?").get(sessionId);
6218
+ return row ? this.agentFromRow(row) : void 0;
6219
+ }
5849
6220
  async listAgents() {
5850
6221
  const rows = this.database.query("SELECT * FROM agents ORDER BY created_at, id").all();
5851
6222
  return rows.map((row) => this.agentFromRow(row));
@@ -5861,6 +6232,34 @@ var SqliteRuntimeStateStore = class {
5861
6232
  })();
5862
6233
  return clone3(updated);
5863
6234
  }
6235
+ async setAgentLifecycleState(agentId, state) {
6236
+ const current = this.requireAgent(agentId);
6237
+ if (current.lifecycleState === state) return clone3(current);
6238
+ const { timestamp } = timestampFor();
6239
+ const updated = this.database.transaction(() => {
6240
+ this.database.run("UPDATE agents SET lifecycle_state = ?, updated_at = ? WHERE id = ?", [state, timestamp, agentId]);
6241
+ if (state === "archived") this.recordEvent("session_archived", { agentId });
6242
+ if (state === "active" && current.lifecycleState === "archived") this.recordEvent("session_unarchived", { agentId });
6243
+ return this.requireAgent(agentId);
6244
+ })();
6245
+ return clone3(updated);
6246
+ }
6247
+ async deleteAgentData(agentId) {
6248
+ this.database.transaction(() => {
6249
+ const agent = this.requireAgent(agentId);
6250
+ const runIds = this.database.query("SELECT id FROM agent_runs WHERE agent_id = ?").all(agentId).map((row) => row.id);
6251
+ if (runIds.length > 0) {
6252
+ const marks = runIds.map(() => "?").join(",");
6253
+ this.database.run(`DELETE FROM runtime_tool_calls WHERE run_id IN (${marks})`, runIds);
6254
+ this.database.run(`DELETE FROM runtime_leases WHERE run_id IN (${marks})`, runIds);
6255
+ this.database.run(`DELETE FROM runtime_events WHERE agent_id = ? OR run_id IN (${marks})`, [agentId, ...runIds]);
6256
+ this.database.run(`DELETE FROM agent_runs WHERE id IN (${marks})`, runIds);
6257
+ }
6258
+ this.database.run("DELETE FROM runtime_messages WHERE agent_id = ?", [agentId]);
6259
+ this.database.run("DELETE FROM runtime_events WHERE agent_id = ?", [agentId]);
6260
+ this.database.run("DELETE FROM agents WHERE id = ?", [agentId]);
6261
+ })();
6262
+ }
5864
6263
  async enqueueAgentInput(input) {
5865
6264
  const enqueue = this.database.transaction(() => this.enqueueAgentInputState(input));
5866
6265
  return clone3(enqueue());
@@ -6350,6 +6749,8 @@ var SqliteRuntimeStateStore = class {
6350
6749
  id: row.id,
6351
6750
  sessionId: row.session_id,
6352
6751
  state: row.state,
6752
+ lifecycleState: row.lifecycle_state,
6753
+ ...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
6353
6754
  createdAt: row.created_at,
6354
6755
  updatedAt: row.updated_at
6355
6756
  };