@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/README.md +15 -17
- package/dist/index.d.ts +75 -19
- package/dist/index.js +632 -231
- package/package.json +4 -3
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
throw new
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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(
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
607
|
-
|
|
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.
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
{
|
|
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.
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
1067
|
+
description: "Read a file in the workspace.",
|
|
988
1068
|
parameters: ReadArgsSchema,
|
|
989
|
-
promptSnippet: "Read
|
|
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 =
|
|
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
|
|
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 =
|
|
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: "
|
|
1138
|
+
description: "Create or overwrite a file in the workspace.",
|
|
1050
1139
|
parameters: WriteArgsSchema,
|
|
1051
|
-
promptSnippet: "
|
|
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: "
|
|
1190
|
+
description: "Apply exact text replacements to a workspace file.",
|
|
1077
1191
|
parameters: EditArgsSchema,
|
|
1078
|
-
promptSnippet: "
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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:
|
|
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: "
|
|
1226
|
+
description: "Run a bash command in the workspace.",
|
|
1136
1227
|
parameters: BashArgsSchema,
|
|
1137
|
-
promptSnippet: "
|
|
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.
|
|
1146
|
-
const maxOutputBytes =
|
|
1147
|
-
const invalidTimeout = positiveNumber(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,
|
|
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
|
-
|
|
1295
|
+
createBashTool(context),
|
|
1214
1296
|
createEditTool(context),
|
|
1215
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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({
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1971
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
2195
|
-
description:
|
|
2378
|
+
name,
|
|
2379
|
+
description: description.description,
|
|
2196
2380
|
filePath: resolved,
|
|
2197
2381
|
baseDir: dirname4(resolved),
|
|
2198
2382
|
content: body,
|
|
2199
|
-
disableModelInvocation:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2714
|
-
const reg = this.getRegisteredPhase(
|
|
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 [
|
|
2727
|
-
phases.set(
|
|
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
|
-
|
|
2737
|
-
|
|
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 (
|
|
3006
|
-
throw new Error(`Phase registration requires
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3009
|
-
|
|
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 (
|
|
3012
|
-
|
|
3211
|
+
if (description.warnings.length > 0) {
|
|
3212
|
+
warnResourceDiagnostics("phase", `extension ${extension.path}`, description.warnings);
|
|
3013
3213
|
}
|
|
3014
|
-
if (this.phases.has(
|
|
3015
|
-
throw new Error(`Duplicate phase
|
|
3214
|
+
if (this.phases.has(name)) {
|
|
3215
|
+
throw new Error(`Duplicate phase name: ${name}`);
|
|
3016
3216
|
}
|
|
3017
3217
|
const definition = {
|
|
3018
|
-
|
|
3019
|
-
|
|
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(
|
|
3029
|
-
extension.phases.set(
|
|
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(
|
|
3057
|
-
return this.collectRegisteredPhases().get(
|
|
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.
|
|
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
|
};
|