@rowan-agent/agent 0.6.0 → 0.6.2
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 +137 -60
- package/dist/index.d.ts +212 -145
- package/dist/index.js +678 -295
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -521,10 +521,10 @@ ${indent}</${tag}>`;
|
|
|
521
521
|
return `${indent}<${tag}>${escapeXml(String(val))}</${tag}>`;
|
|
522
522
|
}).join("\n");
|
|
523
523
|
}
|
|
524
|
-
function buildPhaseDirectiveMessage(phase, output
|
|
524
|
+
function buildPhaseDirectiveMessage(phase, output) {
|
|
525
525
|
const parts = [];
|
|
526
|
-
parts.push(`<
|
|
527
|
-
parts.push(
|
|
526
|
+
parts.push(`<phase_content name="${escapeXml(phase.name)}">`);
|
|
527
|
+
parts.push(phase.content);
|
|
528
528
|
if (output.results && output.results.length > 0) {
|
|
529
529
|
parts.push(` <prev_phase_outputs>`);
|
|
530
530
|
if (output.instruction) {
|
|
@@ -539,13 +539,8 @@ function buildPhaseDirectiveMessage(phase, output, toolUseId) {
|
|
|
539
539
|
}
|
|
540
540
|
parts.push(` </prev_phase_outputs>`);
|
|
541
541
|
}
|
|
542
|
-
parts.push(`</
|
|
543
|
-
return
|
|
544
|
-
type: "tool_result",
|
|
545
|
-
toolUseId,
|
|
546
|
-
content: parts.join("\n"),
|
|
547
|
-
isError: false
|
|
548
|
-
}];
|
|
542
|
+
parts.push(`</phase_content>`);
|
|
543
|
+
return parts.join("\n");
|
|
549
544
|
}
|
|
550
545
|
|
|
551
546
|
// src/harness/phases/loader.ts
|
|
@@ -781,34 +776,27 @@ var DEFAULT_MAX_READ_BYTES = 64e3;
|
|
|
781
776
|
var DEFAULT_BASH_TIMEOUT_MS = 3e4;
|
|
782
777
|
var DEFAULT_MAX_BASH_OUTPUT_BYTES = 64e3;
|
|
783
778
|
var ReadArgsSchema = Type2.Object({
|
|
784
|
-
path: Type2.String(),
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
Type2.Literal("skill"),
|
|
788
|
-
Type2.Literal("phase"),
|
|
789
|
-
Type2.Literal("markdown"),
|
|
790
|
-
Type2.Literal("code"),
|
|
791
|
-
Type2.Literal("file")
|
|
792
|
-
]))
|
|
779
|
+
path: Type2.String({ description: "Path to the file to read." }),
|
|
780
|
+
offset: Type2.Optional(Type2.Number({ description: "1-based line to start from." })),
|
|
781
|
+
limit: Type2.Optional(Type2.Number({ description: "Maximum number of lines." }))
|
|
793
782
|
});
|
|
794
783
|
var ReadArgsValidator = Schema.Compile(ReadArgsSchema);
|
|
795
784
|
var WriteArgsSchema = Type2.Object({
|
|
796
|
-
path: Type2.String(),
|
|
797
|
-
content: Type2.String()
|
|
785
|
+
path: Type2.String({ description: "Path to the file to write." }),
|
|
786
|
+
content: Type2.String({ description: "Complete file contents." })
|
|
798
787
|
});
|
|
799
788
|
var WriteArgsValidator = Schema.Compile(WriteArgsSchema);
|
|
800
789
|
var EditArgsSchema = Type2.Object({
|
|
801
|
-
path: Type2.String(),
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
790
|
+
path: Type2.String({ description: "Path to the file to edit." }),
|
|
791
|
+
edits: Type2.Array(Type2.Object({
|
|
792
|
+
oldText: Type2.String({ description: "Exact unique text to replace." }),
|
|
793
|
+
newText: Type2.String({ description: "Replacement text." })
|
|
794
|
+
}), { description: "One or more non-overlapping replacements." })
|
|
805
795
|
});
|
|
806
796
|
var EditArgsValidator = Schema.Compile(EditArgsSchema);
|
|
807
797
|
var BashArgsSchema = Type2.Object({
|
|
808
|
-
command: Type2.String(),
|
|
809
|
-
|
|
810
|
-
timeoutMs: Type2.Optional(Type2.Number()),
|
|
811
|
-
maxOutputBytes: Type2.Optional(Type2.Number())
|
|
798
|
+
command: Type2.String({ description: "Bash command to execute." }),
|
|
799
|
+
timeout: Type2.Optional(Type2.Number({ description: "Timeout in seconds." }))
|
|
812
800
|
});
|
|
813
801
|
var BashArgsValidator = Schema.Compile(BashArgsSchema);
|
|
814
802
|
var validatorCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -911,12 +899,7 @@ async function executeRuntimeToolCall(input) {
|
|
|
911
899
|
}
|
|
912
900
|
await input.observe?.({ type: "tool_start", tool, args });
|
|
913
901
|
try {
|
|
914
|
-
|
|
915
|
-
let result = {
|
|
916
|
-
...rawResult,
|
|
917
|
-
toolCallId: input.toolCall.id,
|
|
918
|
-
toolName: tool.name
|
|
919
|
-
};
|
|
902
|
+
let result = await tool.execute(args, input.toolContext, input.signal);
|
|
920
903
|
if (input.afterToolCall) {
|
|
921
904
|
await input.observe?.({
|
|
922
905
|
type: "result_review_requested",
|
|
@@ -950,6 +933,20 @@ function positiveNumber(value, name) {
|
|
|
950
933
|
}
|
|
951
934
|
return void 0;
|
|
952
935
|
}
|
|
936
|
+
function positiveInteger(value, name) {
|
|
937
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
938
|
+
return `${name} must be a positive integer.`;
|
|
939
|
+
}
|
|
940
|
+
return void 0;
|
|
941
|
+
}
|
|
942
|
+
function readTextLines(text, offset, limit) {
|
|
943
|
+
if (offset === void 0 && limit === void 0) {
|
|
944
|
+
return text;
|
|
945
|
+
}
|
|
946
|
+
const start = (offset ?? 1) - 1;
|
|
947
|
+
const lines = text.split(/\r\n|\n|\r/);
|
|
948
|
+
return lines.slice(start, limit === void 0 ? void 0 : start + limit).join("\n");
|
|
949
|
+
}
|
|
953
950
|
async function captureStream(stream, maxBytes) {
|
|
954
951
|
const reader = stream.getReader();
|
|
955
952
|
const chunks = [];
|
|
@@ -989,17 +986,25 @@ async function captureStream(stream, maxBytes) {
|
|
|
989
986
|
function createReadTool(context) {
|
|
990
987
|
return {
|
|
991
988
|
name: "read",
|
|
992
|
-
description: "
|
|
989
|
+
description: "Read a file in the workspace.",
|
|
993
990
|
parameters: ReadArgsSchema,
|
|
994
|
-
promptSnippet: "Read
|
|
995
|
-
promptGuidelines: [
|
|
996
|
-
"Always read a file before editing or writing to understand its current content.",
|
|
997
|
-
"Use maxBytes to limit output for large files."
|
|
998
|
-
],
|
|
991
|
+
promptSnippet: "Read file contents.",
|
|
992
|
+
promptGuidelines: ["Read files before editing them."],
|
|
999
993
|
async execute(args, toolContext) {
|
|
1000
994
|
const parsed = ReadArgsValidator.Parse(args);
|
|
995
|
+
const offsetError = parsed.offset === void 0 ? void 0 : positiveInteger(parsed.offset, "offset");
|
|
996
|
+
const limitError = parsed.limit === void 0 ? void 0 : positiveInteger(parsed.limit, "limit");
|
|
997
|
+
if (offsetError || limitError) {
|
|
998
|
+
return toolResult({
|
|
999
|
+
context: toolContext,
|
|
1000
|
+
toolName: "read",
|
|
1001
|
+
ok: false,
|
|
1002
|
+
content: null,
|
|
1003
|
+
error: offsetError ?? limitError
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1001
1006
|
const resolved = resolveCoreToolPath(context, parsed.path);
|
|
1002
|
-
const maxBytes =
|
|
1007
|
+
const maxBytes = context.maxReadBytes;
|
|
1003
1008
|
const invalidLimit = positiveNumber(maxBytes, "maxBytes");
|
|
1004
1009
|
if (invalidLimit) {
|
|
1005
1010
|
return toolResult({
|
|
@@ -1021,9 +1026,12 @@ function createReadTool(context) {
|
|
|
1021
1026
|
});
|
|
1022
1027
|
}
|
|
1023
1028
|
const bytes = await readFile2(resolved.absolutePath);
|
|
1024
|
-
const
|
|
1029
|
+
const source = new TextDecoder().decode(bytes);
|
|
1030
|
+
const selected = readTextLines(source, parsed.offset, parsed.limit);
|
|
1031
|
+
const selectedBytes = new TextEncoder().encode(selected);
|
|
1032
|
+
const sliced = selectedBytes.subarray(0, maxBytes);
|
|
1025
1033
|
const text = new TextDecoder().decode(sliced);
|
|
1026
|
-
const resourceType =
|
|
1034
|
+
const resourceType = detectResourceType(resolved.absolutePath);
|
|
1027
1035
|
let name;
|
|
1028
1036
|
let baseDir;
|
|
1029
1037
|
if (resourceType === "skill" || resourceType === "phase") {
|
|
@@ -1034,16 +1042,14 @@ function createReadTool(context) {
|
|
|
1034
1042
|
} else {
|
|
1035
1043
|
name = inferResourceName(resolved.absolutePath, basename2(resolved.absolutePath));
|
|
1036
1044
|
}
|
|
1045
|
+
const formatted = resourceType === "skill" || resourceType === "phase" ? formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: text, baseDir }) : text;
|
|
1046
|
+
const content = selectedBytes.byteLength > maxBytes ? `${formatted}
|
|
1047
|
+
[truncated]` : formatted;
|
|
1037
1048
|
return toolResult({
|
|
1038
1049
|
context: toolContext,
|
|
1039
1050
|
toolName: "read",
|
|
1040
1051
|
ok: true,
|
|
1041
|
-
content
|
|
1042
|
-
path: resolved.relativePath,
|
|
1043
|
-
content: formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: text, baseDir }),
|
|
1044
|
-
sizeBytes: bytes.byteLength,
|
|
1045
|
-
truncated: bytes.byteLength > maxBytes
|
|
1046
|
-
}
|
|
1052
|
+
content
|
|
1047
1053
|
});
|
|
1048
1054
|
}
|
|
1049
1055
|
};
|
|
@@ -1051,13 +1057,10 @@ function createReadTool(context) {
|
|
|
1051
1057
|
function createWriteTool(context) {
|
|
1052
1058
|
return {
|
|
1053
1059
|
name: "write",
|
|
1054
|
-
description: "
|
|
1060
|
+
description: "Create or overwrite a file in the workspace.",
|
|
1055
1061
|
parameters: WriteArgsSchema,
|
|
1056
|
-
promptSnippet: "
|
|
1057
|
-
promptGuidelines: [
|
|
1058
|
-
"Use write for new files or full file rewrites.",
|
|
1059
|
-
"For partial edits, prefer the edit tool to avoid overwriting unchanged content."
|
|
1060
|
-
],
|
|
1062
|
+
promptSnippet: "Create or overwrite files.",
|
|
1063
|
+
promptGuidelines: ["Use edit for partial changes."],
|
|
1061
1064
|
async execute(args, toolContext) {
|
|
1062
1065
|
const parsed = WriteArgsValidator.Parse(args);
|
|
1063
1066
|
const resolved = resolveCoreToolPath(context, parsed.path);
|
|
@@ -1067,69 +1070,74 @@ function createWriteTool(context) {
|
|
|
1067
1070
|
context: toolContext,
|
|
1068
1071
|
toolName: "write",
|
|
1069
1072
|
ok: true,
|
|
1070
|
-
content: {
|
|
1071
|
-
path: resolved.relativePath,
|
|
1072
|
-
bytesWritten: new TextEncoder().encode(parsed.content).byteLength
|
|
1073
|
-
}
|
|
1073
|
+
content: `Successfully wrote ${parsed.content.length} bytes to ${resolved.relativePath}.`
|
|
1074
1074
|
});
|
|
1075
1075
|
}
|
|
1076
1076
|
};
|
|
1077
1077
|
}
|
|
1078
|
+
function applyEditReplacements(current, edits) {
|
|
1079
|
+
if (edits.length === 0) {
|
|
1080
|
+
throw new Error("edits must contain at least one replacement.");
|
|
1081
|
+
}
|
|
1082
|
+
const matches = [];
|
|
1083
|
+
for (const edit of edits) {
|
|
1084
|
+
if (!edit.oldText) throw new Error("oldText must not be empty.");
|
|
1085
|
+
const start = current.indexOf(edit.oldText);
|
|
1086
|
+
if (start < 0) throw new Error(`oldText not found in file.`);
|
|
1087
|
+
const second = current.indexOf(edit.oldText, start + edit.oldText.length);
|
|
1088
|
+
if (second >= 0) {
|
|
1089
|
+
const count = current.split(edit.oldText).length - 1;
|
|
1090
|
+
throw new Error(`oldText appears ${count} times; provide more context.`);
|
|
1091
|
+
}
|
|
1092
|
+
matches.push({ start, end: start + edit.oldText.length, newText: edit.newText });
|
|
1093
|
+
}
|
|
1094
|
+
matches.sort((a, b) => a.start - b.start);
|
|
1095
|
+
for (let index = 1; index < matches.length; index += 1) {
|
|
1096
|
+
const previous = matches[index - 1];
|
|
1097
|
+
const currentMatch = matches[index];
|
|
1098
|
+
if (currentMatch.start < previous.end) {
|
|
1099
|
+
throw new Error("edits must not overlap.");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
let content = current;
|
|
1103
|
+
for (let index = matches.length - 1; index >= 0; index -= 1) {
|
|
1104
|
+
const match = matches[index];
|
|
1105
|
+
content = content.slice(0, match.start) + match.newText + content.slice(match.end);
|
|
1106
|
+
}
|
|
1107
|
+
return { content, replacements: matches.length };
|
|
1108
|
+
}
|
|
1078
1109
|
function createEditTool(context) {
|
|
1079
1110
|
return {
|
|
1080
1111
|
name: "edit",
|
|
1081
|
-
description: "
|
|
1112
|
+
description: "Apply exact text replacements to a workspace file.",
|
|
1082
1113
|
parameters: EditArgsSchema,
|
|
1083
|
-
promptSnippet: "
|
|
1084
|
-
promptGuidelines: [
|
|
1085
|
-
"Read the file first to get the exact oldText to replace.",
|
|
1086
|
-
"oldText must be an exact match including whitespace and indentation.",
|
|
1087
|
-
"If oldText appears multiple times, set replaceAll=true or provide more surrounding context."
|
|
1088
|
-
],
|
|
1114
|
+
promptSnippet: "Apply exact text replacements.",
|
|
1115
|
+
promptGuidelines: ["Read the file first; each oldText must match exactly once."],
|
|
1089
1116
|
async execute(args, toolContext) {
|
|
1090
1117
|
const parsed = EditArgsValidator.Parse(args);
|
|
1091
|
-
if (!parsed.oldText) {
|
|
1092
|
-
return toolResult({
|
|
1093
|
-
context: toolContext,
|
|
1094
|
-
toolName: "edit",
|
|
1095
|
-
ok: false,
|
|
1096
|
-
content: null,
|
|
1097
|
-
error: "oldText must not be empty."
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
1118
|
const resolved = resolveCoreToolPath(context, parsed.path);
|
|
1101
1119
|
const current = await readFile2(resolved.absolutePath, "utf8");
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
const matches = current.split(parsed.oldText).length - 1;
|
|
1112
|
-
if (matches > 1 && !parsed.replaceAll) {
|
|
1120
|
+
let next;
|
|
1121
|
+
let replacements;
|
|
1122
|
+
try {
|
|
1123
|
+
const result = applyEditReplacements(current, parsed.edits);
|
|
1124
|
+
next = result.content;
|
|
1125
|
+
replacements = result.replacements;
|
|
1126
|
+
} catch (error) {
|
|
1113
1127
|
return toolResult({
|
|
1114
1128
|
context: toolContext,
|
|
1115
1129
|
toolName: "edit",
|
|
1116
1130
|
ok: false,
|
|
1117
1131
|
content: null,
|
|
1118
|
-
error:
|
|
1132
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1119
1133
|
});
|
|
1120
1134
|
}
|
|
1121
|
-
const replacements = parsed.replaceAll ? matches : 1;
|
|
1122
|
-
const next = parsed.replaceAll ? current.split(parsed.oldText).join(parsed.newText) : current.replace(parsed.oldText, parsed.newText);
|
|
1123
1135
|
await writeFile(resolved.absolutePath, next, "utf8");
|
|
1124
1136
|
return toolResult({
|
|
1125
1137
|
context: toolContext,
|
|
1126
1138
|
toolName: "edit",
|
|
1127
1139
|
ok: true,
|
|
1128
|
-
content: {
|
|
1129
|
-
path: resolved.relativePath,
|
|
1130
|
-
replacements,
|
|
1131
|
-
bytesWritten: new TextEncoder().encode(next).byteLength
|
|
1132
|
-
}
|
|
1140
|
+
content: `Successfully replaced ${replacements} block(s) in ${resolved.relativePath}.`
|
|
1133
1141
|
});
|
|
1134
1142
|
}
|
|
1135
1143
|
};
|
|
@@ -1137,19 +1145,15 @@ function createEditTool(context) {
|
|
|
1137
1145
|
function createBashTool(context) {
|
|
1138
1146
|
return {
|
|
1139
1147
|
name: "bash",
|
|
1140
|
-
description: "
|
|
1148
|
+
description: "Run a bash command in the workspace.",
|
|
1141
1149
|
parameters: BashArgsSchema,
|
|
1142
|
-
promptSnippet: "
|
|
1143
|
-
promptGuidelines: [
|
|
1144
|
-
"Use bash for build commands, tests, git operations, and system tools.",
|
|
1145
|
-
"Prefer dedicated tools (read/write/edit) for file operations.",
|
|
1146
|
-
"Set timeoutMs for long-running commands."
|
|
1147
|
-
],
|
|
1150
|
+
promptSnippet: "Run shell commands.",
|
|
1151
|
+
promptGuidelines: ["Use read/write/edit for file operations."],
|
|
1148
1152
|
async execute(args, toolContext, signal) {
|
|
1149
1153
|
const parsed = BashArgsValidator.Parse(args);
|
|
1150
|
-
const timeoutMs = parsed.
|
|
1151
|
-
const maxOutputBytes =
|
|
1152
|
-
const invalidTimeout = positiveNumber(timeoutMs, "
|
|
1154
|
+
const timeoutMs = parsed.timeout === void 0 ? context.bashTimeoutMs : parsed.timeout * 1e3;
|
|
1155
|
+
const maxOutputBytes = context.maxBashOutputBytes;
|
|
1156
|
+
const invalidTimeout = positiveNumber(timeoutMs, "timeout");
|
|
1153
1157
|
const invalidOutputLimit = positiveNumber(maxOutputBytes, "maxOutputBytes");
|
|
1154
1158
|
if (invalidTimeout || invalidOutputLimit) {
|
|
1155
1159
|
return toolResult({
|
|
@@ -1160,7 +1164,7 @@ function createBashTool(context) {
|
|
|
1160
1164
|
error: invalidTimeout ?? invalidOutputLimit
|
|
1161
1165
|
});
|
|
1162
1166
|
}
|
|
1163
|
-
const cwd = resolveCoreToolPath(context,
|
|
1167
|
+
const cwd = resolveCoreToolPath(context, ".");
|
|
1164
1168
|
let timedOut = false;
|
|
1165
1169
|
let aborted = false;
|
|
1166
1170
|
const proc = Bun.spawn(["bash", "-lc", parsed.command], {
|
|
@@ -1187,19 +1191,14 @@ function createBashTool(context) {
|
|
|
1187
1191
|
proc.exited
|
|
1188
1192
|
]);
|
|
1189
1193
|
const ok = exitCode === 0 && !timedOut && !aborted;
|
|
1194
|
+
const output = [stdout.text, stderr.text].filter(Boolean).join(stdout.text && stderr.text ? "\n" : "");
|
|
1195
|
+
const content = stdout.truncated || stderr.truncated ? `${output}
|
|
1196
|
+
[truncated]` : output;
|
|
1190
1197
|
return toolResult({
|
|
1191
1198
|
context: toolContext,
|
|
1192
1199
|
toolName: "bash",
|
|
1193
1200
|
ok,
|
|
1194
|
-
content
|
|
1195
|
-
command: parsed.command,
|
|
1196
|
-
cwd: cwd.relativePath,
|
|
1197
|
-
exitCode,
|
|
1198
|
-
stdout: stdout.text,
|
|
1199
|
-
stderr: stderr.text,
|
|
1200
|
-
stdoutTruncated: stdout.truncated,
|
|
1201
|
-
stderrTruncated: stderr.truncated
|
|
1202
|
-
},
|
|
1201
|
+
content,
|
|
1203
1202
|
...ok ? {} : {
|
|
1204
1203
|
error: timedOut ? `Command timed out after ${timeoutMs}ms.` : aborted ? "Command aborted." : `Command exited with ${exitCode}.`
|
|
1205
1204
|
}
|
|
@@ -1215,9 +1214,9 @@ function createCoreTools(input = {}) {
|
|
|
1215
1214
|
const context = createCoreToolContext(input);
|
|
1216
1215
|
return [
|
|
1217
1216
|
createReadTool(context),
|
|
1218
|
-
|
|
1217
|
+
createBashTool(context),
|
|
1219
1218
|
createEditTool(context),
|
|
1220
|
-
|
|
1219
|
+
createWriteTool(context)
|
|
1221
1220
|
];
|
|
1222
1221
|
}
|
|
1223
1222
|
|
|
@@ -1366,11 +1365,17 @@ function compactMessages(messages, options = {}) {
|
|
|
1366
1365
|
);
|
|
1367
1366
|
const recentStart = Math.max(messages.length - keepRecent, firstUserIdx + 1);
|
|
1368
1367
|
const oldMessages = messages.slice(firstUserIdx >= 0 ? firstUserIdx + 1 : 0, recentStart);
|
|
1369
|
-
|
|
1368
|
+
const protectedPhaseMessages = oldMessages.filter(
|
|
1369
|
+
(message) => message.metadata?.kind === "phase_prompt"
|
|
1370
|
+
);
|
|
1371
|
+
const summarizableMessages = oldMessages.filter(
|
|
1372
|
+
(message) => message.metadata?.kind !== "phase_prompt"
|
|
1373
|
+
);
|
|
1374
|
+
if (summarizableMessages.length < minCompact) {
|
|
1370
1375
|
return { compacted: false, messages };
|
|
1371
1376
|
}
|
|
1372
1377
|
const recentMessages = messages.slice(recentStart);
|
|
1373
|
-
const summary = buildSummary(
|
|
1378
|
+
const summary = buildSummary(summarizableMessages);
|
|
1374
1379
|
const result = [];
|
|
1375
1380
|
if (firstUserIdx >= 0) {
|
|
1376
1381
|
result.push(messages[firstUserIdx]);
|
|
@@ -1380,14 +1385,15 @@ function compactMessages(messages, options = {}) {
|
|
|
1380
1385
|
|
|
1381
1386
|
${summary}`, {
|
|
1382
1387
|
type: "compaction_summary",
|
|
1383
|
-
compactedCount:
|
|
1388
|
+
compactedCount: summarizableMessages.length
|
|
1384
1389
|
})
|
|
1385
1390
|
);
|
|
1391
|
+
result.push(...protectedPhaseMessages);
|
|
1386
1392
|
result.push(...recentMessages);
|
|
1387
1393
|
return {
|
|
1388
1394
|
compacted: true,
|
|
1389
1395
|
messages: result,
|
|
1390
|
-
summarizedCount:
|
|
1396
|
+
summarizedCount: summarizableMessages.length,
|
|
1391
1397
|
summary
|
|
1392
1398
|
};
|
|
1393
1399
|
}
|
|
@@ -1401,6 +1407,23 @@ function removePhaseMessage(messages, msgId) {
|
|
|
1401
1407
|
const idx = messages.findIndex((m) => m.id === msgId);
|
|
1402
1408
|
if (idx !== -1) messages.splice(idx, 1);
|
|
1403
1409
|
}
|
|
1410
|
+
function findLatestUserInputMessage(messages) {
|
|
1411
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1412
|
+
const message = messages[index];
|
|
1413
|
+
const kind = message.metadata?.kind;
|
|
1414
|
+
if (message.role === "user" && kind !== "phase_prompt" && kind !== "phase_input") {
|
|
1415
|
+
return message;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
return void 0;
|
|
1419
|
+
}
|
|
1420
|
+
function moveMessageBefore(messages, messageId, targetId) {
|
|
1421
|
+
const messageIndex = messages.findIndex((message2) => message2.id === messageId);
|
|
1422
|
+
const targetIndex = messages.findIndex((message2) => message2.id === targetId);
|
|
1423
|
+
if (messageIndex === -1 || targetIndex === -1 || messageIndex < targetIndex) return;
|
|
1424
|
+
const [message] = messages.splice(messageIndex, 1);
|
|
1425
|
+
if (message) messages.splice(targetIndex, 0, message);
|
|
1426
|
+
}
|
|
1404
1427
|
function normalizePayload(payload) {
|
|
1405
1428
|
if (typeof payload === "string") {
|
|
1406
1429
|
try {
|
|
@@ -1417,17 +1440,20 @@ function applyFirstDecision(route, output) {
|
|
|
1417
1440
|
if (first.reason) output.routeReason = first.reason;
|
|
1418
1441
|
if (first.payload !== void 0) output.payload = normalizePayload(first.payload);
|
|
1419
1442
|
}
|
|
1420
|
-
function injectPhaseContent(phase, output,
|
|
1443
|
+
function injectPhaseContent(phase, output, messages, mirror) {
|
|
1421
1444
|
try {
|
|
1422
1445
|
const phaseContent = phase.filePath ? readPhaseContent(phase) : phase.content ?? phase.description ?? "";
|
|
1423
1446
|
const content = buildPhaseDirectiveMessage(
|
|
1424
1447
|
{ name: phase.id, content: phaseContent },
|
|
1425
|
-
output
|
|
1426
|
-
`phase_${phase.id}`
|
|
1448
|
+
output
|
|
1427
1449
|
);
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1450
|
+
const message = createMessage("user", content, {
|
|
1451
|
+
kind: "phase_prompt",
|
|
1452
|
+
phase: phase.id
|
|
1453
|
+
});
|
|
1454
|
+
messages.push(message);
|
|
1455
|
+
if (mirror && mirror !== messages) mirror.push(message);
|
|
1456
|
+
return message.id;
|
|
1431
1457
|
} catch {
|
|
1432
1458
|
return void 0;
|
|
1433
1459
|
}
|
|
@@ -1534,6 +1560,15 @@ function createToolResultContent(result) {
|
|
|
1534
1560
|
}
|
|
1535
1561
|
];
|
|
1536
1562
|
}
|
|
1563
|
+
function createRouteToolResultContent(toolCall) {
|
|
1564
|
+
return [
|
|
1565
|
+
{
|
|
1566
|
+
type: "tool_result",
|
|
1567
|
+
toolUseId: toolCall.id,
|
|
1568
|
+
content: '{"ok": true}'
|
|
1569
|
+
}
|
|
1570
|
+
];
|
|
1571
|
+
}
|
|
1537
1572
|
async function executePhaseWithModel(ctx) {
|
|
1538
1573
|
const executableToolNames = new Set(
|
|
1539
1574
|
ctx.context.tools.filter((tool) => tool.name !== PhaseRouteTool).map((tool) => tool.name)
|
|
@@ -1556,6 +1591,13 @@ async function executePhaseWithModel(ctx) {
|
|
|
1556
1591
|
phase: ctx.phase.name,
|
|
1557
1592
|
toolCalls: collected.toolCalls
|
|
1558
1593
|
};
|
|
1594
|
+
for (const toolCall of collected.toolCalls) {
|
|
1595
|
+
if (toolCall.name !== PhaseRouteTool) continue;
|
|
1596
|
+
const messageId = ctx.messageManager.start("tool", createRouteToolResultContent(toolCall), {
|
|
1597
|
+
phase: ctx.phase.id
|
|
1598
|
+
});
|
|
1599
|
+
await ctx.messageManager.end(messageId);
|
|
1600
|
+
}
|
|
1559
1601
|
const executableToolCalls = collected.toolCalls.filter(
|
|
1560
1602
|
(toolCall) => executableToolNames.has(toolCall.name)
|
|
1561
1603
|
);
|
|
@@ -1590,12 +1632,17 @@ async function startPhaseLoop(config, state) {
|
|
|
1590
1632
|
return runPhaseLoop(config, state, registry);
|
|
1591
1633
|
}
|
|
1592
1634
|
async function runPhaseLoop(config, state, registry) {
|
|
1593
|
-
|
|
1594
|
-
let
|
|
1595
|
-
let
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1635
|
+
const resumingSuspendedRun = state.status === "suspended" && Boolean(state.currentPhase);
|
|
1636
|
+
let currentPhaseId = resumingSuspendedRun ? state.currentPhase : registry.entryPhaseId;
|
|
1637
|
+
let isContinuing = resumingSuspendedRun ? state.continuation?.isContinuing ?? true : false;
|
|
1638
|
+
if (resumingSuspendedRun) {
|
|
1639
|
+
state.status = "running";
|
|
1640
|
+
}
|
|
1641
|
+
let previousPayload = resumingSuspendedRun ? state.continuation?.previousPayload : void 0;
|
|
1642
|
+
let previousPhaseMsgId = resumingSuspendedRun ? state.continuation?.previousPhaseMessageId : void 0;
|
|
1643
|
+
let previousPhaseInputMsgId;
|
|
1644
|
+
let previousResults = resumingSuspendedRun ? state.continuation?.previousResults?.map((result) => ({ ...result })) ?? [] : [];
|
|
1645
|
+
let pendingInstruction = resumingSuspendedRun ? state.continuation?.pendingInstruction : void 0;
|
|
1599
1646
|
while (currentPhaseId) {
|
|
1600
1647
|
const availablePhases = [];
|
|
1601
1648
|
for (const [, phase2] of registry.phases) {
|
|
@@ -1603,6 +1650,10 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1603
1650
|
}
|
|
1604
1651
|
const abortResult = LoopGuard.checkAbort(config.signal);
|
|
1605
1652
|
if (abortResult.stopReason !== "none") {
|
|
1653
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1654
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1655
|
+
previousPhaseMsgId = void 0;
|
|
1656
|
+
previousPhaseInputMsgId = void 0;
|
|
1606
1657
|
return completeRun(config, state, createOutcome.aborted());
|
|
1607
1658
|
}
|
|
1608
1659
|
state.metrics.iterations++;
|
|
@@ -1640,6 +1691,10 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1640
1691
|
messages: messageManager.visible(),
|
|
1641
1692
|
tools: phaseTools,
|
|
1642
1693
|
skills: phaseSkills,
|
|
1694
|
+
invocation: {
|
|
1695
|
+
mode: "serial",
|
|
1696
|
+
instanceId: currentPhaseId
|
|
1697
|
+
},
|
|
1643
1698
|
state: {
|
|
1644
1699
|
current: currentPhaseId,
|
|
1645
1700
|
available: Array.from(registry.phases.keys()),
|
|
@@ -1656,10 +1711,18 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1656
1711
|
const extBefore = await config.beforePhase(currentPhaseId, phaseContext);
|
|
1657
1712
|
if (extBefore.abort) {
|
|
1658
1713
|
config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
|
|
1714
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1715
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1716
|
+
previousPhaseMsgId = void 0;
|
|
1717
|
+
previousPhaseInputMsgId = void 0;
|
|
1659
1718
|
return completeRun(config, state, extBefore.abort);
|
|
1660
1719
|
}
|
|
1661
1720
|
if (extBefore.skip) {
|
|
1662
1721
|
config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
|
|
1722
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1723
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1724
|
+
previousPhaseMsgId = void 0;
|
|
1725
|
+
previousPhaseInputMsgId = void 0;
|
|
1663
1726
|
if (extBefore.skip.route === "stop") {
|
|
1664
1727
|
return completeRun(config, state, {
|
|
1665
1728
|
id: "skip",
|
|
@@ -1673,21 +1736,45 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1673
1736
|
phaseContext = extBefore.input;
|
|
1674
1737
|
}
|
|
1675
1738
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1739
|
+
const phaseMessageIsPresent = previousPhaseMsgId !== void 0 && config.context.messages.some((message) => message.id === previousPhaseMsgId);
|
|
1740
|
+
if (enteringNewPhase || !phaseMessageIsPresent) {
|
|
1741
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1742
|
+
removePhaseMessage(phaseContext.messages, previousPhaseMsgId);
|
|
1743
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1744
|
+
removePhaseMessage(phaseContext.messages, previousPhaseInputMsgId);
|
|
1745
|
+
previousPhaseMsgId = void 0;
|
|
1746
|
+
previousPhaseInputMsgId = void 0;
|
|
1747
|
+
const latestUserInputMessage = findLatestUserInputMessage(config.context.messages);
|
|
1748
|
+
const lastMessageBeforePhase = config.context.messages.at(-1);
|
|
1749
|
+
previousPhaseMsgId = injectPhaseContent(
|
|
1750
|
+
phase,
|
|
1751
|
+
{ results: previousResults, instruction: pendingInstruction },
|
|
1752
|
+
config.context.messages,
|
|
1753
|
+
phaseContext.messages
|
|
1754
|
+
);
|
|
1755
|
+
if (previousPhaseMsgId) {
|
|
1756
|
+
if (lastMessageBeforePhase?.role === "user") {
|
|
1757
|
+
moveMessageBefore(config.context.messages, previousPhaseMsgId, lastMessageBeforePhase.id);
|
|
1758
|
+
moveMessageBefore(phaseContext.messages, previousPhaseMsgId, lastMessageBeforePhase.id);
|
|
1759
|
+
} else if (latestUserInputMessage) {
|
|
1760
|
+
const phaseInputMessage = createMessage("user", latestUserInputMessage.content, {
|
|
1761
|
+
kind: "phase_input",
|
|
1762
|
+
phase: phase.id
|
|
1763
|
+
});
|
|
1764
|
+
config.context.messages.push(phaseInputMessage);
|
|
1765
|
+
phaseContext.messages.push(phaseInputMessage);
|
|
1766
|
+
previousPhaseInputMsgId = phaseInputMessage.id;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1680
1769
|
previousResults = [];
|
|
1681
1770
|
pendingInstruction = void 0;
|
|
1682
1771
|
}
|
|
1683
1772
|
const runtime = { phase, config, state, execution, messageManager, registry, context: phaseContext };
|
|
1684
1773
|
let output = await executePhase(runtime);
|
|
1685
|
-
let routeToolCalled = false;
|
|
1686
1774
|
let routeDecision;
|
|
1687
1775
|
if (output.toolCalls && output.toolCalls.length > 0) {
|
|
1688
1776
|
routeDecision = extractRouteCall(output.toolCalls);
|
|
1689
1777
|
if (routeDecision) {
|
|
1690
|
-
routeToolCalled = true;
|
|
1691
1778
|
applyFirstDecision(routeDecision, output);
|
|
1692
1779
|
}
|
|
1693
1780
|
}
|
|
@@ -1695,6 +1782,10 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1695
1782
|
const extAfter = await config.afterPhase(currentPhaseId, output);
|
|
1696
1783
|
if (extAfter.abort) {
|
|
1697
1784
|
config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
|
|
1785
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1786
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1787
|
+
previousPhaseMsgId = void 0;
|
|
1788
|
+
previousPhaseInputMsgId = void 0;
|
|
1698
1789
|
return completeRun(config, state, extAfter.abort);
|
|
1699
1790
|
}
|
|
1700
1791
|
if (extAfter.retry && (phase.run || phase.factory)) {
|
|
@@ -1714,28 +1805,52 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1714
1805
|
if (preAbort.stopReason !== "none") {
|
|
1715
1806
|
config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
|
|
1716
1807
|
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1808
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1717
1809
|
previousPhaseMsgId = void 0;
|
|
1810
|
+
previousPhaseInputMsgId = void 0;
|
|
1718
1811
|
return completeRun(config, state, createOutcome.aborted());
|
|
1719
1812
|
}
|
|
1720
|
-
|
|
1721
|
-
const
|
|
1813
|
+
const requestedAt = createTimestamp();
|
|
1814
|
+
const inputRequest = {
|
|
1815
|
+
phase: currentPhaseId,
|
|
1816
|
+
prompt: output.message,
|
|
1817
|
+
requestedAt
|
|
1818
|
+
};
|
|
1819
|
+
config.emit?.({
|
|
1820
|
+
type: "user_prompt_requested",
|
|
1821
|
+
phase: currentPhaseId,
|
|
1822
|
+
ts: requestedAt
|
|
1823
|
+
});
|
|
1824
|
+
state.status = "suspended";
|
|
1825
|
+
state.continuation = {
|
|
1826
|
+
isContinuing,
|
|
1827
|
+
previousPayload,
|
|
1828
|
+
previousResults: previousResults.map((result) => ({ ...result })),
|
|
1829
|
+
pendingInstruction,
|
|
1830
|
+
previousPhaseMessageId: previousPhaseMsgId
|
|
1831
|
+
};
|
|
1832
|
+
const userMessages = await config.waitForInput(state, inputRequest);
|
|
1833
|
+
state.status = "running";
|
|
1722
1834
|
const abortResult2 = LoopGuard.checkAbort(config.signal);
|
|
1723
1835
|
if (abortResult2.stopReason !== "none") {
|
|
1724
1836
|
config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
|
|
1725
1837
|
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1838
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1726
1839
|
previousPhaseMsgId = void 0;
|
|
1840
|
+
previousPhaseInputMsgId = void 0;
|
|
1727
1841
|
return completeRun(config, state, createOutcome.aborted());
|
|
1728
1842
|
}
|
|
1729
1843
|
for (const message of userMessages) {
|
|
1730
1844
|
config.context.messages.push(message);
|
|
1731
1845
|
}
|
|
1732
|
-
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1733
|
-
previousPhaseMsgId = void 0;
|
|
1734
1846
|
isContinuing = true;
|
|
1735
1847
|
continue;
|
|
1736
1848
|
}
|
|
1737
1849
|
config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
|
|
1738
1850
|
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1851
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1852
|
+
previousPhaseMsgId = void 0;
|
|
1853
|
+
previousPhaseInputMsgId = void 0;
|
|
1739
1854
|
return completeRun(config, state, createOutcome.default(output, config.context.messages));
|
|
1740
1855
|
}
|
|
1741
1856
|
if (output.route === "continue") {
|
|
@@ -1747,6 +1862,8 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1747
1862
|
const contextSnapshot = snapshotMessages(config.context.messages);
|
|
1748
1863
|
const parallelTasks = /* @__PURE__ */ new Map();
|
|
1749
1864
|
const instanceIds = buildInstanceIds(routeDecision.decision.map((t) => t.phase));
|
|
1865
|
+
const groupId = createId("phase-group");
|
|
1866
|
+
const count = routeDecision.decision.length;
|
|
1750
1867
|
for (let i = 0; i < routeDecision.decision.length; i++) {
|
|
1751
1868
|
const target = routeDecision.decision[i];
|
|
1752
1869
|
const pt = registry.phases.get(target.phase);
|
|
@@ -1754,7 +1871,20 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1754
1871
|
const instanceId = instanceIds[i];
|
|
1755
1872
|
const context = pt.isolated ? [] : contextSnapshot;
|
|
1756
1873
|
const payload = target.payload !== void 0 ? normalizePayload(target.payload) : void 0;
|
|
1757
|
-
const promise = executeParallelPhase(
|
|
1874
|
+
const promise = executeParallelPhase(
|
|
1875
|
+
config,
|
|
1876
|
+
state,
|
|
1877
|
+
registry,
|
|
1878
|
+
pt,
|
|
1879
|
+
payload,
|
|
1880
|
+
context,
|
|
1881
|
+
availablePhases,
|
|
1882
|
+
instanceId,
|
|
1883
|
+
groupId,
|
|
1884
|
+
i,
|
|
1885
|
+
count,
|
|
1886
|
+
currentPhaseId
|
|
1887
|
+
);
|
|
1758
1888
|
parallelTasks.set(instanceId, { promise, phaseId: target.phase });
|
|
1759
1889
|
}
|
|
1760
1890
|
const successfulResults = await waitForBackgroundTasks(parallelTasks);
|
|
@@ -1762,6 +1892,10 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1762
1892
|
pendingInstruction = routeDecision.instruction;
|
|
1763
1893
|
const entryPhaseId = phase.target ?? registry.entryPhaseId;
|
|
1764
1894
|
if (entryPhaseId === "stop") {
|
|
1895
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1896
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1897
|
+
previousPhaseMsgId = void 0;
|
|
1898
|
+
previousPhaseInputMsgId = void 0;
|
|
1765
1899
|
return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
|
|
1766
1900
|
}
|
|
1767
1901
|
currentPhaseId = entryPhaseId;
|
|
@@ -1777,12 +1911,18 @@ async function runPhaseLoop(config, state, registry) {
|
|
|
1777
1911
|
nextRoute = "stop";
|
|
1778
1912
|
}
|
|
1779
1913
|
if (nextRoute === "stop") {
|
|
1780
|
-
|
|
1914
|
+
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1915
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1916
|
+
previousPhaseMsgId = void 0;
|
|
1917
|
+
previousPhaseInputMsgId = void 0;
|
|
1781
1918
|
return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
|
|
1782
1919
|
}
|
|
1783
1920
|
const targetPhaseId = nextRoute;
|
|
1784
1921
|
if (!registry.phases.has(targetPhaseId)) {
|
|
1785
1922
|
removePhaseMessage(config.context.messages, previousPhaseMsgId);
|
|
1923
|
+
removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
|
|
1924
|
+
previousPhaseMsgId = void 0;
|
|
1925
|
+
previousPhaseInputMsgId = void 0;
|
|
1786
1926
|
return completeRun(config, state, createOutcome.phaseNotFound(output));
|
|
1787
1927
|
}
|
|
1788
1928
|
state.metrics.phaseTransitions.push({
|
|
@@ -1898,24 +2038,31 @@ async function executePhase(ctx) {
|
|
|
1898
2038
|
return executePhaseWithModel(ctx);
|
|
1899
2039
|
}
|
|
1900
2040
|
async function executeToolCall(input) {
|
|
2041
|
+
let result;
|
|
1901
2042
|
if (input.config.runtime?.tools) {
|
|
1902
|
-
|
|
2043
|
+
result = await input.config.runtime.tools({
|
|
1903
2044
|
config: input.config,
|
|
1904
2045
|
toolCall: input.toolCall
|
|
1905
2046
|
});
|
|
2047
|
+
} else {
|
|
2048
|
+
const toolContext = {
|
|
2049
|
+
skills: input.config.context.skills,
|
|
2050
|
+
toolCallId: input.toolCall.id
|
|
2051
|
+
};
|
|
2052
|
+
result = await executeRuntimeToolCall({
|
|
2053
|
+
tools: input.tools,
|
|
2054
|
+
toolCall: input.toolCall,
|
|
2055
|
+
toolContext,
|
|
2056
|
+
beforeToolCall: input.config.beforeToolCall,
|
|
2057
|
+
afterToolCall: input.config.afterToolCall,
|
|
2058
|
+
signal: input.config.signal
|
|
2059
|
+
});
|
|
1906
2060
|
}
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
toolCallId: input.toolCall.id
|
|
2061
|
+
return {
|
|
2062
|
+
...result,
|
|
2063
|
+
toolCallId: input.toolCall.id,
|
|
2064
|
+
toolName: input.toolCall.name
|
|
1910
2065
|
};
|
|
1911
|
-
return executeRuntimeToolCall({
|
|
1912
|
-
tools: input.tools,
|
|
1913
|
-
toolCall: input.toolCall,
|
|
1914
|
-
toolContext,
|
|
1915
|
-
beforeToolCall: input.config.beforeToolCall,
|
|
1916
|
-
afterToolCall: input.config.afterToolCall,
|
|
1917
|
-
signal: input.config.signal
|
|
1918
|
-
});
|
|
1919
2066
|
}
|
|
1920
2067
|
function createPhaseExecution(config, state, allTools, phase, messageManager, toolExecutionManager, registry) {
|
|
1921
2068
|
return {
|
|
@@ -1999,17 +2146,25 @@ function createPhaseExecution(config, state, allTools, phase, messageManager, to
|
|
|
1999
2146
|
}
|
|
2000
2147
|
};
|
|
2001
2148
|
}
|
|
2002
|
-
async function executeParallelPhase(config, state, registry, phase, payload, context, availablePhases,
|
|
2149
|
+
async function executeParallelPhase(config, state, registry, phase, payload, context, availablePhases, instanceId, groupId, index, count, sourcePhaseId) {
|
|
2003
2150
|
const messages = [...context];
|
|
2004
2151
|
const allTools = buildToolsWithRouting(config, availablePhases);
|
|
2005
2152
|
const phaseTools = phase.tools ? allTools.filter((t) => t.name === PhaseRouteTool || phase.tools.includes(t.name)) : allTools;
|
|
2006
2153
|
const phaseSkills = phase.skills ? config.context.skills.filter((s) => phase.skills.includes(s.name)) : config.context.skills;
|
|
2007
|
-
const systemPrompt =
|
|
2154
|
+
const systemPrompt = config.context.systemPrompt;
|
|
2008
2155
|
const phaseContext = {
|
|
2009
2156
|
systemPrompt,
|
|
2010
2157
|
messages,
|
|
2011
2158
|
tools: phaseTools,
|
|
2012
2159
|
skills: phaseSkills,
|
|
2160
|
+
invocation: {
|
|
2161
|
+
mode: "parallel",
|
|
2162
|
+
instanceId,
|
|
2163
|
+
groupId,
|
|
2164
|
+
index,
|
|
2165
|
+
count,
|
|
2166
|
+
sourcePhaseId
|
|
2167
|
+
},
|
|
2013
2168
|
state: {
|
|
2014
2169
|
current: phase.id,
|
|
2015
2170
|
available: Array.from(registry.phases.keys()),
|
|
@@ -2018,10 +2173,10 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
|
|
|
2018
2173
|
}
|
|
2019
2174
|
};
|
|
2020
2175
|
const messageManager = createMessageManager({ messages }, config.emit, config.onMessage);
|
|
2021
|
-
const phaseMsgId =
|
|
2176
|
+
const phaseMsgId = injectPhaseContent(
|
|
2022
2177
|
phase,
|
|
2023
|
-
{ results: payload !== void 0 ? [{ name:
|
|
2024
|
-
|
|
2178
|
+
{ results: payload !== void 0 ? [{ name: sourcePhaseId, output: payload }] : [] },
|
|
2179
|
+
messages
|
|
2025
2180
|
);
|
|
2026
2181
|
const toolExecutionManager = createToolExecutionManager(config.emit);
|
|
2027
2182
|
const execution = createPhaseExecution(config, state, phaseTools, phase, messageManager, toolExecutionManager, registry);
|
|
@@ -2030,7 +2185,7 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
|
|
|
2030
2185
|
if (phaseMsgId) removePhaseMessage(messages, phaseMsgId);
|
|
2031
2186
|
const decision = output.toolCalls ? extractRouteCall(output.toolCalls) : void 0;
|
|
2032
2187
|
const resultPayload = decision?.decision[0]?.payload !== void 0 ? normalizePayload(decision.decision[0].payload) : output.payload;
|
|
2033
|
-
return { instanceId
|
|
2188
|
+
return { instanceId, phaseId: phase.id, payload: resultPayload, content: output.message };
|
|
2034
2189
|
}
|
|
2035
2190
|
async function waitForBackgroundTasks(backgroundTasks) {
|
|
2036
2191
|
const entries = Array.from(backgroundTasks.entries());
|
|
@@ -3224,19 +3379,29 @@ var AgentExecution = class {
|
|
|
3224
3379
|
runWithMessage(message, options, internalHooks = {}) {
|
|
3225
3380
|
const activeRun = this.activeRun;
|
|
3226
3381
|
if (activeRun?.resume) {
|
|
3382
|
+
if (internalHooks.onSuspend) activeRun.onSuspend = internalHooks.onSuspend;
|
|
3227
3383
|
activeRun.resume([message]);
|
|
3228
3384
|
return activeRun.promise;
|
|
3229
3385
|
}
|
|
3230
3386
|
if (activeRun) {
|
|
3231
3387
|
return Promise.reject(new Error("Agent is already running."));
|
|
3232
3388
|
}
|
|
3233
|
-
this.
|
|
3389
|
+
if (!this.options.context.messages.some((candidate) => candidate.id === message.id)) {
|
|
3390
|
+
this.appendMessage(message);
|
|
3391
|
+
}
|
|
3234
3392
|
return this.run(options, internalHooks);
|
|
3235
3393
|
}
|
|
3236
3394
|
/** Internal Runtime seam: executes one leased Agent Input. */
|
|
3237
|
-
async executeAgentInput(input, runId, control) {
|
|
3395
|
+
async executeAgentInput(input, runId, control, executionState) {
|
|
3238
3396
|
this.runtimeRunId = runId;
|
|
3239
|
-
|
|
3397
|
+
this.options.sessionState = executionState ? restoreSessionState(executionState) : void 0;
|
|
3398
|
+
const internal = {
|
|
3399
|
+
onSuspend: (reason, state, inputRequest) => control.suspend(
|
|
3400
|
+
reason,
|
|
3401
|
+
state ? snapshotExecutionState(state) : void 0,
|
|
3402
|
+
inputRequest
|
|
3403
|
+
)
|
|
3404
|
+
};
|
|
3240
3405
|
try {
|
|
3241
3406
|
return (await this.runWithMessage(input, void 0, internal)).outcome;
|
|
3242
3407
|
} finally {
|
|
@@ -3473,18 +3638,20 @@ var AgentExecution = class {
|
|
|
3473
3638
|
}
|
|
3474
3639
|
const abortController = new AbortController();
|
|
3475
3640
|
let suspensionRequested = false;
|
|
3476
|
-
const waitForInput = () => {
|
|
3477
|
-
|
|
3478
|
-
suspensionRequested = true;
|
|
3479
|
-
void hooks.onSuspend?.("Agent requested input.");
|
|
3480
|
-
}
|
|
3481
|
-
return new Promise((resolve7) => {
|
|
3641
|
+
const waitForInput = (state, inputRequest) => {
|
|
3642
|
+
const waiting = new Promise((resolve7) => {
|
|
3482
3643
|
const activeRun = this.activeRun;
|
|
3483
3644
|
activeRun.resume = (messages) => {
|
|
3484
3645
|
activeRun.resume = void 0;
|
|
3646
|
+
suspensionRequested = false;
|
|
3485
3647
|
resolve7(messages);
|
|
3486
3648
|
};
|
|
3487
3649
|
});
|
|
3650
|
+
if (!suspensionRequested) {
|
|
3651
|
+
suspensionRequested = true;
|
|
3652
|
+
void this.activeRun?.onSuspend?.("Agent requested input.", state, inputRequest);
|
|
3653
|
+
}
|
|
3654
|
+
return waiting;
|
|
3488
3655
|
};
|
|
3489
3656
|
const promise = Promise.resolve().then(() => executor({ signal: abortController.signal, waitForInput })).catch((error) => {
|
|
3490
3657
|
this.handleRunFailure(error, abortController.signal.aborted);
|
|
@@ -3492,7 +3659,7 @@ var AgentExecution = class {
|
|
|
3492
3659
|
}).finally(() => {
|
|
3493
3660
|
this.finishRun();
|
|
3494
3661
|
});
|
|
3495
|
-
this.activeRun = { promise, abortController };
|
|
3662
|
+
this.activeRun = { promise, abortController, onSuspend: hooks.onSuspend };
|
|
3496
3663
|
this.state.running = true;
|
|
3497
3664
|
return promise;
|
|
3498
3665
|
}
|
|
@@ -3525,7 +3692,8 @@ var AgentExecution = class {
|
|
|
3525
3692
|
const extensions = resolved.extensions ?? [];
|
|
3526
3693
|
await this.loadExtensions(extensions, resolved.cwd);
|
|
3527
3694
|
const runContext = prepareAgentContext(resolved.context, this.extensionRunner);
|
|
3528
|
-
const
|
|
3695
|
+
const resumingSuspendedRun = resolved.sessionState?.status === "suspended";
|
|
3696
|
+
const entryPhaseId = resumingSuspendedRun ? resolved.sessionState?.currentPhase || runContext.phases?.entryPhaseId || DEFAULT_PHASE_ID : this.state.initialized ? DEFAULT_PHASE_ID : runContext.phases?.entryPhaseId ?? DEFAULT_PHASE_ID;
|
|
3529
3697
|
const loopContext = {
|
|
3530
3698
|
...runContext,
|
|
3531
3699
|
phases: {
|
|
@@ -3570,6 +3738,7 @@ var AgentExecution = class {
|
|
|
3570
3738
|
model: resolved.model,
|
|
3571
3739
|
stream: resolved.stream,
|
|
3572
3740
|
maxAttempts: resolved.maxAttempts,
|
|
3741
|
+
sessionState: resolved.sessionState,
|
|
3573
3742
|
signal,
|
|
3574
3743
|
beforeToolCall,
|
|
3575
3744
|
afterToolCall,
|
|
@@ -3594,7 +3763,8 @@ var AgentExecution = class {
|
|
|
3594
3763
|
this.options = {
|
|
3595
3764
|
...resolved,
|
|
3596
3765
|
sessionId: result.sessionId,
|
|
3597
|
-
context: nextContext
|
|
3766
|
+
context: nextContext,
|
|
3767
|
+
sessionState: void 0
|
|
3598
3768
|
};
|
|
3599
3769
|
return result;
|
|
3600
3770
|
}, internalHooks);
|
|
@@ -3602,6 +3772,9 @@ var AgentExecution = class {
|
|
|
3602
3772
|
abort(reason = "Aborted by caller.") {
|
|
3603
3773
|
this.abortLocal(reason);
|
|
3604
3774
|
}
|
|
3775
|
+
isSuspended() {
|
|
3776
|
+
return Boolean(this.activeRun?.resume);
|
|
3777
|
+
}
|
|
3605
3778
|
abortLocal(reason) {
|
|
3606
3779
|
const activeRun = this.activeRun;
|
|
3607
3780
|
activeRun?.resume?.([]);
|
|
@@ -3676,6 +3849,50 @@ ${additionalInstructions}` : content;
|
|
|
3676
3849
|
};
|
|
3677
3850
|
}
|
|
3678
3851
|
};
|
|
3852
|
+
function restoreSessionState(state) {
|
|
3853
|
+
const persisted = parsePersistedExecutionState(state);
|
|
3854
|
+
return {
|
|
3855
|
+
...persisted,
|
|
3856
|
+
status: "suspended",
|
|
3857
|
+
metrics: {
|
|
3858
|
+
...persisted.metrics,
|
|
3859
|
+
phaseTransitions: persisted.metrics.phaseTransitions.map((transition) => ({ ...transition }))
|
|
3860
|
+
},
|
|
3861
|
+
...persisted.continuation ? {
|
|
3862
|
+
continuation: {
|
|
3863
|
+
...persisted.continuation,
|
|
3864
|
+
previousResults: persisted.continuation.previousResults.map((result) => ({ ...result }))
|
|
3865
|
+
}
|
|
3866
|
+
} : {}
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
function snapshotExecutionState(state) {
|
|
3870
|
+
return {
|
|
3871
|
+
version: 1,
|
|
3872
|
+
currentPhase: state.currentPhase,
|
|
3873
|
+
attempt: state.attempt,
|
|
3874
|
+
metrics: {
|
|
3875
|
+
...state.metrics,
|
|
3876
|
+
phaseTransitions: state.metrics.phaseTransitions.map((transition) => ({ ...transition }))
|
|
3877
|
+
},
|
|
3878
|
+
...state.continuation ? {
|
|
3879
|
+
continuation: {
|
|
3880
|
+
...state.continuation,
|
|
3881
|
+
previousResults: state.continuation.previousResults.map((result) => ({ ...result }))
|
|
3882
|
+
}
|
|
3883
|
+
} : {}
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
function parsePersistedExecutionState(state) {
|
|
3887
|
+
if (!state || state.version !== 1 || typeof state.currentPhase !== "string" || typeof state.attempt !== "number") {
|
|
3888
|
+
throw new Error("Invalid Agent Run execution checkpoint.");
|
|
3889
|
+
}
|
|
3890
|
+
const metrics = state.metrics;
|
|
3891
|
+
if (!metrics || !Array.isArray(metrics.phaseTransitions)) {
|
|
3892
|
+
throw new Error("Invalid Agent Run execution checkpoint metrics.");
|
|
3893
|
+
}
|
|
3894
|
+
return state;
|
|
3895
|
+
}
|
|
3679
3896
|
function clonePhaseRegistry(registry) {
|
|
3680
3897
|
return {
|
|
3681
3898
|
phases: new Map(registry.phases),
|
|
@@ -3755,6 +3972,8 @@ async function attachAgent(input) {
|
|
|
3755
3972
|
phases: input.options.context.phases
|
|
3756
3973
|
};
|
|
3757
3974
|
const persistInput = async (message) => {
|
|
3975
|
+
const context2 = await input.manager.buildAgentContext();
|
|
3976
|
+
if (context2.messages.some((candidate) => candidate.id === message.id)) return;
|
|
3758
3977
|
await input.manager.appendMessage(message);
|
|
3759
3978
|
};
|
|
3760
3979
|
let execution;
|
|
@@ -3813,7 +4032,9 @@ async function attachAgent(input) {
|
|
|
3813
4032
|
agent,
|
|
3814
4033
|
binding: {
|
|
3815
4034
|
abort: (reason) => execution.abortFromRuntime(reason),
|
|
3816
|
-
|
|
4035
|
+
isSuspended: () => execution.isSuspended(),
|
|
4036
|
+
persistInput,
|
|
4037
|
+
execute: (message, runId, control, executionState) => execution.executeAgentInput(message, runId, control, executionState)
|
|
3817
4038
|
}
|
|
3818
4039
|
};
|
|
3819
4040
|
}
|
|
@@ -4240,8 +4461,8 @@ var LocalJsonlSessionManager = class _LocalJsonlSessionManager {
|
|
|
4240
4461
|
}
|
|
4241
4462
|
};
|
|
4242
4463
|
|
|
4243
|
-
// src/harness/session/
|
|
4244
|
-
var
|
|
4464
|
+
// src/harness/session/store.ts
|
|
4465
|
+
var InMemorySessionStore = class {
|
|
4245
4466
|
sessions = /* @__PURE__ */ new Map();
|
|
4246
4467
|
async create(input) {
|
|
4247
4468
|
const session = InMemorySessionManager.create(input);
|
|
@@ -4252,7 +4473,7 @@ var InMemorySessionProvider = class {
|
|
|
4252
4473
|
return this.sessions.get(sessionId);
|
|
4253
4474
|
}
|
|
4254
4475
|
};
|
|
4255
|
-
var
|
|
4476
|
+
var JsonlSessionStore = class {
|
|
4256
4477
|
constructor(sessionsDir) {
|
|
4257
4478
|
this.sessionsDir = sessionsDir;
|
|
4258
4479
|
}
|
|
@@ -4268,6 +4489,9 @@ var LocalJsonlSessionProvider = class {
|
|
|
4268
4489
|
}
|
|
4269
4490
|
};
|
|
4270
4491
|
|
|
4492
|
+
// src/runtime/agent-runtime.ts
|
|
4493
|
+
import { createModelStream } from "@rowan-agent/models";
|
|
4494
|
+
|
|
4271
4495
|
// src/runtime/tool-runtime.ts
|
|
4272
4496
|
function failed(toolCall, message) {
|
|
4273
4497
|
return {
|
|
@@ -4469,6 +4693,9 @@ var AgentRun = class _AgentRun {
|
|
|
4469
4693
|
get state() {
|
|
4470
4694
|
return this.current.state;
|
|
4471
4695
|
}
|
|
4696
|
+
get inputRequest() {
|
|
4697
|
+
return this.current.inputRequest ? structuredClone(this.current.inputRequest) : void 0;
|
|
4698
|
+
}
|
|
4472
4699
|
async getStatus() {
|
|
4473
4700
|
const run = await this.host.getRun(this.id);
|
|
4474
4701
|
if (run) this.notify(run);
|
|
@@ -4533,8 +4760,8 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4533
4760
|
leaseDurationMs;
|
|
4534
4761
|
leaseRenewalIntervalMs;
|
|
4535
4762
|
toolRuntime;
|
|
4536
|
-
factories;
|
|
4537
4763
|
bindings = /* @__PURE__ */ new Map();
|
|
4764
|
+
agents = /* @__PURE__ */ new Map();
|
|
4538
4765
|
runHandles = /* @__PURE__ */ new Map();
|
|
4539
4766
|
runWaiters = /* @__PURE__ */ new Map();
|
|
4540
4767
|
pendingRuns = [];
|
|
@@ -4546,6 +4773,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4546
4773
|
runningRuns = 0;
|
|
4547
4774
|
pumping = false;
|
|
4548
4775
|
nextWorkerId = 0;
|
|
4776
|
+
leaseRecoveryTimer;
|
|
4549
4777
|
stopped = false;
|
|
4550
4778
|
constructor(options) {
|
|
4551
4779
|
this.stateStore = options.stateStore;
|
|
@@ -4555,10 +4783,6 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4555
4783
|
this.leaseDurationMs = options.leaseDurationMs ?? 6e4;
|
|
4556
4784
|
this.leaseRenewalIntervalMs = options.leaseRenewalIntervalMs ?? Math.max(1, Math.floor(this.leaseDurationMs / 2));
|
|
4557
4785
|
this.toolRuntime = new ToolRuntime(options.stateStore, options.toolPolicy, () => this.publishEvents());
|
|
4558
|
-
this.factories = options.factories instanceof Map ? new Map([...options.factories.entries()].map(([id, factory]) => [String(id), factory])) : new Map(Object.entries(options.factories ?? {}));
|
|
4559
|
-
for (const id of this.factories.keys()) {
|
|
4560
|
-
if (id.trim().length === 0) throw new Error("Factory ID must not be empty.");
|
|
4561
|
-
}
|
|
4562
4786
|
}
|
|
4563
4787
|
static async start(options) {
|
|
4564
4788
|
if (activeRuntime) {
|
|
@@ -4585,6 +4809,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4585
4809
|
activeRuntime = runtime;
|
|
4586
4810
|
try {
|
|
4587
4811
|
await runtime.recover();
|
|
4812
|
+
runtime.startLeaseRecovery();
|
|
4588
4813
|
return runtime;
|
|
4589
4814
|
} catch (error) {
|
|
4590
4815
|
await runtime.stop().catch(() => void 0);
|
|
@@ -4593,20 +4818,16 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4593
4818
|
}
|
|
4594
4819
|
async createAgent(options) {
|
|
4595
4820
|
this.assertRunning();
|
|
4596
|
-
|
|
4597
|
-
throw new Error("Factory ID must not be empty.");
|
|
4598
|
-
}
|
|
4821
|
+
assertAgentOptions(options);
|
|
4599
4822
|
const provider = this.sessionProvider;
|
|
4600
4823
|
if (!provider) throw new Error("Agent Runtime requires a SessionManager provider to create an Agent.");
|
|
4601
4824
|
const manager = await provider.create({
|
|
4602
4825
|
systemPrompt: options.context.systemPrompt,
|
|
4603
|
-
input:
|
|
4826
|
+
input: "",
|
|
4604
4827
|
skills: options.context.skills
|
|
4605
4828
|
});
|
|
4606
|
-
if (options.input) await manager.appendMessage(createMessage("user", options.input));
|
|
4607
4829
|
const record = await this.stateStore.createAgent({
|
|
4608
|
-
sessionId: manager.getSessionId()
|
|
4609
|
-
...options.factoryId ? { factoryId: options.factoryId } : {}
|
|
4830
|
+
sessionId: manager.getSessionId()
|
|
4610
4831
|
});
|
|
4611
4832
|
this.pausedAgents.delete(record.id);
|
|
4612
4833
|
const agent = await this.attachAgent(record, manager, options);
|
|
@@ -4615,6 +4836,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4615
4836
|
}
|
|
4616
4837
|
async reconstructAgent(agentId, options) {
|
|
4617
4838
|
this.assertRunning();
|
|
4839
|
+
assertAgentOptions(options);
|
|
4618
4840
|
const record = await this.stateStore.getAgent(agentId);
|
|
4619
4841
|
if (!record) throw new Error(`Agent not found: ${agentId}.`);
|
|
4620
4842
|
if (record.state === "paused") this.pausedAgents.add(record.id);
|
|
@@ -4630,10 +4852,23 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4630
4852
|
async stop() {
|
|
4631
4853
|
if (this.stopped) return;
|
|
4632
4854
|
this.stopped = true;
|
|
4633
|
-
|
|
4634
|
-
|
|
4855
|
+
if (this.leaseRecoveryTimer) {
|
|
4856
|
+
clearInterval(this.leaseRecoveryTimer);
|
|
4857
|
+
this.leaseRecoveryTimer = void 0;
|
|
4858
|
+
}
|
|
4859
|
+
let suspendedAgents;
|
|
4860
|
+
try {
|
|
4861
|
+
suspendedAgents = new Set(
|
|
4862
|
+
(await this.stateStore.listRuns({ states: ["suspended"] })).map((run) => run.agentId)
|
|
4863
|
+
);
|
|
4864
|
+
} catch {
|
|
4865
|
+
suspendedAgents = /* @__PURE__ */ new Set();
|
|
4866
|
+
}
|
|
4867
|
+
for (const [agentId, binding] of this.bindings) {
|
|
4868
|
+
if (!suspendedAgents.has(agentId) && !binding.isSuspended()) binding.abort("Agent Runtime stopped.");
|
|
4635
4869
|
}
|
|
4636
4870
|
this.bindings.clear();
|
|
4871
|
+
this.agents.clear();
|
|
4637
4872
|
this.pendingRuns.length = 0;
|
|
4638
4873
|
this.scheduledRunIds.clear();
|
|
4639
4874
|
this.pausedAgents.clear();
|
|
@@ -4653,6 +4888,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4653
4888
|
throw new Error(`Agent ${agentId} is already bound to a live Agent.`);
|
|
4654
4889
|
}
|
|
4655
4890
|
this.bindings.set(agentId, agent);
|
|
4891
|
+
void this.pump();
|
|
4656
4892
|
if (!this.pausedAgents.has(agentId)) void this.scheduleQueuedRuns(agentId);
|
|
4657
4893
|
}
|
|
4658
4894
|
registerRunHandle(runId, handle) {
|
|
@@ -4702,6 +4938,24 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4702
4938
|
void this.pump();
|
|
4703
4939
|
this.publishEvents();
|
|
4704
4940
|
}
|
|
4941
|
+
async getRun(runId) {
|
|
4942
|
+
return this.stateStore.getRun(runId);
|
|
4943
|
+
}
|
|
4944
|
+
async listActiveRuns() {
|
|
4945
|
+
return this.stateStore.listActiveRuns();
|
|
4946
|
+
}
|
|
4947
|
+
async listRuns(input) {
|
|
4948
|
+
return this.stateStore.listRuns(input);
|
|
4949
|
+
}
|
|
4950
|
+
getAgent(agentId) {
|
|
4951
|
+
return this.agents.get(agentId);
|
|
4952
|
+
}
|
|
4953
|
+
async getMessage(messageId) {
|
|
4954
|
+
return this.stateStore.getMessage(messageId);
|
|
4955
|
+
}
|
|
4956
|
+
async getToolCall(toolCallId) {
|
|
4957
|
+
return this.stateStore.getToolCall(toolCallId);
|
|
4958
|
+
}
|
|
4705
4959
|
async abortRun(runId, reason = "Agent Run aborted by caller.") {
|
|
4706
4960
|
const run = await this.stateStore.getRun(runId);
|
|
4707
4961
|
if (!run || ["completed", "failed", "cancelled"].includes(run.state)) return;
|
|
@@ -4722,6 +4976,11 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4722
4976
|
this.publishEvents();
|
|
4723
4977
|
}
|
|
4724
4978
|
consumeEvents(consumerId, listener) {
|
|
4979
|
+
const subscription = this.registerEventConsumer(consumerId, listener);
|
|
4980
|
+
void subscription.caughtUp.catch(() => void 0);
|
|
4981
|
+
return subscription;
|
|
4982
|
+
}
|
|
4983
|
+
registerEventConsumer(consumerId, listener) {
|
|
4725
4984
|
this.assertRunning();
|
|
4726
4985
|
if (consumerId.trim().length === 0) {
|
|
4727
4986
|
throw new Error("Runtime Event Consumer ID must not be empty.");
|
|
@@ -4736,58 +4995,78 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4736
4995
|
redeliver: false
|
|
4737
4996
|
};
|
|
4738
4997
|
this.eventSubscriptions.set(consumerId, subscription);
|
|
4739
|
-
|
|
4740
|
-
|
|
4998
|
+
let caughtUpSettled = false;
|
|
4999
|
+
let resolveCaughtUp;
|
|
5000
|
+
let rejectCaughtUp;
|
|
5001
|
+
const caughtUp = new Promise((resolve7, reject) => {
|
|
5002
|
+
resolveCaughtUp = resolve7;
|
|
5003
|
+
rejectCaughtUp = reject;
|
|
5004
|
+
});
|
|
5005
|
+
const stop = () => {
|
|
4741
5006
|
if (this.eventSubscriptions.get(consumerId) === subscription) {
|
|
4742
5007
|
this.eventSubscriptions.delete(consumerId);
|
|
4743
5008
|
}
|
|
5009
|
+
if (!caughtUpSettled) {
|
|
5010
|
+
caughtUpSettled = true;
|
|
5011
|
+
const error = new Error("Runtime Event Consumer stopped before catch-up completed.");
|
|
5012
|
+
error.name = "AbortError";
|
|
5013
|
+
rejectCaughtUp(error);
|
|
5014
|
+
}
|
|
4744
5015
|
};
|
|
5016
|
+
void this.deliverEvents(subscription).then(
|
|
5017
|
+
() => {
|
|
5018
|
+
if (!caughtUpSettled) {
|
|
5019
|
+
caughtUpSettled = true;
|
|
5020
|
+
resolveCaughtUp();
|
|
5021
|
+
}
|
|
5022
|
+
},
|
|
5023
|
+
(error) => {
|
|
5024
|
+
if (!caughtUpSettled) {
|
|
5025
|
+
caughtUpSettled = true;
|
|
5026
|
+
rejectCaughtUp(error);
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
);
|
|
5030
|
+
return { caughtUp, stop };
|
|
4745
5031
|
}
|
|
4746
5032
|
async listEvents(cursor) {
|
|
4747
5033
|
return this.stateStore.listEvents(cursor);
|
|
4748
5034
|
}
|
|
4749
5035
|
async recover() {
|
|
4750
|
-
await this.
|
|
5036
|
+
await this.recoverExpiredLeases();
|
|
4751
5037
|
const agents = await this.stateStore.listAgents();
|
|
4752
5038
|
for (const record of agents) {
|
|
4753
5039
|
if (record.state === "paused") this.pausedAgents.add(record.id);
|
|
4754
5040
|
}
|
|
4755
|
-
for (const record of agents) {
|
|
4756
|
-
if (record.state !== "active" || !record.factoryId) continue;
|
|
4757
|
-
const factory = this.factories.get(String(record.factoryId));
|
|
4758
|
-
if (!factory) {
|
|
4759
|
-
await this.stateStore.recordEvent({ kind: "factory_missing", related: { agentId: record.id }, payload: { factoryId: record.factoryId } });
|
|
4760
|
-
continue;
|
|
4761
|
-
}
|
|
4762
|
-
try {
|
|
4763
|
-
const options = await factory({ agentId: record.id, sessionId: record.sessionId, factoryId: record.factoryId });
|
|
4764
|
-
if (!options) {
|
|
4765
|
-
await this.stateStore.recordEvent({
|
|
4766
|
-
kind: "agent_recovery_failed",
|
|
4767
|
-
related: { agentId: record.id },
|
|
4768
|
-
payload: { factoryId: record.factoryId, error: "Factory returned no reconstruction options." }
|
|
4769
|
-
});
|
|
4770
|
-
continue;
|
|
4771
|
-
}
|
|
4772
|
-
await this.reconstructAgent(record.id, options);
|
|
4773
|
-
await this.stateStore.recordEvent({ kind: "agent_recovered", related: { agentId: record.id }, payload: { factoryId: record.factoryId } });
|
|
4774
|
-
} catch (error) {
|
|
4775
|
-
await this.stateStore.recordEvent({ kind: "agent_recovery_failed", related: { agentId: record.id }, payload: { factoryId: record.factoryId, error: error instanceof Error ? error.message : String(error) } });
|
|
4776
|
-
}
|
|
4777
|
-
}
|
|
4778
|
-
for (const record of await this.stateStore.listAgents()) {
|
|
4779
|
-
if (this.bindings.has(record.id)) await this.scheduleQueuedRuns(record.id);
|
|
4780
|
-
}
|
|
4781
5041
|
this.publishEvents();
|
|
4782
5042
|
}
|
|
5043
|
+
startLeaseRecovery() {
|
|
5044
|
+
this.leaseRecoveryTimer = setInterval(() => {
|
|
5045
|
+
void this.recoverExpiredLeases().catch(() => void 0);
|
|
5046
|
+
}, this.leaseRenewalIntervalMs);
|
|
5047
|
+
}
|
|
5048
|
+
async recoverExpiredLeases() {
|
|
5049
|
+
const recovered = await this.stateStore.recoverExpiredLeases();
|
|
5050
|
+
for (const run of recovered) {
|
|
5051
|
+
this.notifyRun(run);
|
|
5052
|
+
if (this.bindings.has(run.agentId)) this.scheduleRun(run.agentId, run.id);
|
|
5053
|
+
}
|
|
5054
|
+
if (recovered.length > 0) this.publishEvents();
|
|
5055
|
+
}
|
|
4783
5056
|
async scheduleQueuedRuns(agentId) {
|
|
4784
5057
|
if (!this.bindings.has(agentId)) return;
|
|
4785
5058
|
const runs = await this.stateStore.listRuns({ agentId, states: ["queued"] });
|
|
4786
5059
|
for (const run of runs) this.scheduleRun(agentId, run.id);
|
|
4787
5060
|
}
|
|
4788
5061
|
async attachAgent(record, manager, options) {
|
|
5062
|
+
const model = { provider: options.model.provider, id: options.model.id };
|
|
5063
|
+
const resolvedOptions = {
|
|
5064
|
+
...options,
|
|
5065
|
+
model,
|
|
5066
|
+
stream: options.stream ?? createModelStream(options.model)
|
|
5067
|
+
};
|
|
4789
5068
|
const attached = await attachAgent({
|
|
4790
|
-
options,
|
|
5069
|
+
options: resolvedOptions,
|
|
4791
5070
|
agentId: record.id,
|
|
4792
5071
|
sessionId: record.sessionId,
|
|
4793
5072
|
manager,
|
|
@@ -4795,6 +5074,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4795
5074
|
executeTool: (input) => this.toolRuntime.execute(input)
|
|
4796
5075
|
});
|
|
4797
5076
|
this.bindAgent(record.id, attached.binding);
|
|
5077
|
+
this.agents.set(record.id, attached.agent);
|
|
4798
5078
|
return attached.agent;
|
|
4799
5079
|
}
|
|
4800
5080
|
async submitAgentInput(agentId, input, persist) {
|
|
@@ -4802,6 +5082,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4802
5082
|
if (!this.bindings.has(agentId)) throw new Error(`Agent ${agentId} has no live Agent Binding.`);
|
|
4803
5083
|
const enqueued = await this.stateStore.enqueueAgentInput({ agentId, input });
|
|
4804
5084
|
await persist();
|
|
5085
|
+
if (enqueued.resumed) this.notifyRun(enqueued.run);
|
|
4805
5086
|
const run = createAgentRun({
|
|
4806
5087
|
register: (handle) => this.registerRunHandle(enqueued.run.id, handle),
|
|
4807
5088
|
getRun: (runId) => this.stateStore.getRun(runId),
|
|
@@ -4849,10 +5130,10 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4849
5130
|
suspendedResolve = resolve7;
|
|
4850
5131
|
});
|
|
4851
5132
|
const control = {
|
|
4852
|
-
suspend: async (reason) => {
|
|
5133
|
+
suspend: async (reason, executionState, inputRequest) => {
|
|
4853
5134
|
if (suspended) return;
|
|
4854
5135
|
suspended = true;
|
|
4855
|
-
const current = await this.stateStore.suspendRun({ runId: job.runId, reason });
|
|
5136
|
+
const current = await this.stateStore.suspendRun({ runId: job.runId, reason, executionState, inputRequest });
|
|
4856
5137
|
this.notifyRun(current);
|
|
4857
5138
|
this.publishEvents();
|
|
4858
5139
|
suspendedResolve?.();
|
|
@@ -4871,6 +5152,11 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4871
5152
|
leaseDurationMs: this.leaseDurationMs
|
|
4872
5153
|
});
|
|
4873
5154
|
this.notifyRun(leased.run);
|
|
5155
|
+
try {
|
|
5156
|
+
await binding.persistInput(leased.message.input);
|
|
5157
|
+
} catch (error) {
|
|
5158
|
+
throw new InfrastructureFailureError("Agent Input persistence failed.", { cause: error });
|
|
5159
|
+
}
|
|
4874
5160
|
renewalTimer = setInterval(() => {
|
|
4875
5161
|
void this.stateStore.renewLease({
|
|
4876
5162
|
runId: job.runId,
|
|
@@ -4884,7 +5170,7 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4884
5170
|
failRenewal(failure);
|
|
4885
5171
|
});
|
|
4886
5172
|
}, this.leaseRenewalIntervalMs);
|
|
4887
|
-
const execution = binding.execute(leased.message.input, job.runId, control);
|
|
5173
|
+
const execution = binding.execute(leased.message.input, job.runId, control, leased.run.executionState);
|
|
4888
5174
|
const outcome = await Promise.race([
|
|
4889
5175
|
execution.then((value) => ({ kind: "terminal", value })).catch((error) => ({ kind: "error", error })),
|
|
4890
5176
|
suspendedSignal.then(() => ({ kind: "suspended" })),
|
|
@@ -4976,8 +5262,21 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4976
5262
|
);
|
|
4977
5263
|
for (const event of events) {
|
|
4978
5264
|
if (this.eventSubscriptions.get(subscription.consumerId) !== subscription) return;
|
|
4979
|
-
await subscription.listener(structuredClone(event));
|
|
4980
|
-
|
|
5265
|
+
const disposition = await subscription.listener(structuredClone(event));
|
|
5266
|
+
if (disposition?.type === "enqueue") {
|
|
5267
|
+
const delivered = await this.stateStore.acknowledgeEventAndEnqueueAgentInput({
|
|
5268
|
+
consumerId: subscription.consumerId,
|
|
5269
|
+
eventId: event.id,
|
|
5270
|
+
agentId: disposition.agentId,
|
|
5271
|
+
input: disposition.input
|
|
5272
|
+
});
|
|
5273
|
+
if (delivered.enqueued) {
|
|
5274
|
+
this.scheduleRun(delivered.enqueued.run.agentId, delivered.enqueued.run.id);
|
|
5275
|
+
this.publishEvents();
|
|
5276
|
+
}
|
|
5277
|
+
} else {
|
|
5278
|
+
await this.stateStore.acknowledgeEvent(subscription.consumerId, event.id);
|
|
5279
|
+
}
|
|
4981
5280
|
}
|
|
4982
5281
|
} while (subscription.redeliver);
|
|
4983
5282
|
} finally {
|
|
@@ -4988,11 +5287,25 @@ var AgentRuntime = class _AgentRuntime {
|
|
|
4988
5287
|
if (this.stopped || activeRuntime !== this) throw new Error("Agent Runtime is stopped.");
|
|
4989
5288
|
}
|
|
4990
5289
|
};
|
|
5290
|
+
function isModelConfig(value) {
|
|
5291
|
+
if (typeof value !== "object" || value === null) return false;
|
|
5292
|
+
return "protocol" in value && "baseUrl" in value && "apiKey" in value;
|
|
5293
|
+
}
|
|
5294
|
+
function assertAgentOptions(options) {
|
|
5295
|
+
if (options.stream && isModelConfig(options.model)) {
|
|
5296
|
+
throw new Error("AgentOptions accepts either a complete model config or a custom stream, not both.");
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
4991
5299
|
function isRetryableInfrastructureError(error) {
|
|
4992
5300
|
if (error instanceof InfrastructureFailureError) return true;
|
|
4993
5301
|
return typeof error === "object" && error !== null && "retryable" in error && error.retryable === true;
|
|
4994
5302
|
}
|
|
4995
5303
|
|
|
5304
|
+
// src/runtime/domain.ts
|
|
5305
|
+
function runEventPayload(metadata, payload = {}) {
|
|
5306
|
+
return metadata ? { ...payload, metadata: structuredClone(metadata) } : payload;
|
|
5307
|
+
}
|
|
5308
|
+
|
|
4996
5309
|
// src/runtime/memory-store.ts
|
|
4997
5310
|
function clone2(value) {
|
|
4998
5311
|
return structuredClone(value);
|
|
@@ -5021,7 +5334,6 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5021
5334
|
const agent = {
|
|
5022
5335
|
id: createId("agt"),
|
|
5023
5336
|
sessionId: input.sessionId,
|
|
5024
|
-
...input.factoryId ? { factoryId: input.factoryId } : {},
|
|
5025
5337
|
state: "active",
|
|
5026
5338
|
createdAt: timestamp,
|
|
5027
5339
|
updatedAt: timestamp
|
|
@@ -5047,6 +5359,9 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5047
5359
|
return clone2(agent);
|
|
5048
5360
|
}
|
|
5049
5361
|
async enqueueAgentInput(input) {
|
|
5362
|
+
return this.enqueueAgentInputState(input);
|
|
5363
|
+
}
|
|
5364
|
+
enqueueAgentInputState(input) {
|
|
5050
5365
|
this.requireAgent(input.agentId);
|
|
5051
5366
|
const suspended = [...this.runs.values()].filter((run2) => run2.agentId === input.agentId && run2.state === "suspended").sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
5052
5367
|
const { timestamp } = now();
|
|
@@ -5067,6 +5382,7 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5067
5382
|
suspended.state = "queued";
|
|
5068
5383
|
suspended.messageId = messageId;
|
|
5069
5384
|
delete suspended.suspensionReason;
|
|
5385
|
+
delete suspended.inputRequest;
|
|
5070
5386
|
suspended.updatedAt = timestamp;
|
|
5071
5387
|
message.runId = suspended.id;
|
|
5072
5388
|
}
|
|
@@ -5076,13 +5392,14 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5076
5392
|
messageId,
|
|
5077
5393
|
state: "queued",
|
|
5078
5394
|
attempt: 0,
|
|
5395
|
+
...suspended?.metadata ? { metadata: clone2(suspended.metadata) } : input.input.metadata ? { metadata: clone2(input.input.metadata) } : {},
|
|
5079
5396
|
createdAt: timestamp,
|
|
5080
5397
|
updatedAt: timestamp
|
|
5081
5398
|
};
|
|
5082
5399
|
this.messages.set(message.id, message);
|
|
5083
5400
|
this.runs.set(run.id, suspended ?? run);
|
|
5084
5401
|
this.recordEvent("message_enqueued", { agentId: input.agentId, messageId });
|
|
5085
|
-
this.recordEvent("run_enqueued", { agentId: input.agentId, messageId, runId });
|
|
5402
|
+
this.recordEvent("run_enqueued", { agentId: input.agentId, messageId, runId }, runEventPayload(suspended?.metadata ?? input.input.metadata));
|
|
5086
5403
|
return { message: clone2(message), run: clone2(suspended ?? run), resumed: Boolean(suspended) };
|
|
5087
5404
|
}
|
|
5088
5405
|
async getMessage(messageId) {
|
|
@@ -5094,7 +5411,10 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5094
5411
|
return run ? clone2(run) : void 0;
|
|
5095
5412
|
}
|
|
5096
5413
|
async listRuns(input = {}) {
|
|
5097
|
-
return clone2([...this.runs.values()].filter((run) => input.agentId === void 0 || run.agentId === input.agentId).filter((run) => !input.states || input.states.includes(run.state)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)));
|
|
5414
|
+
return clone2([...this.runs.values()].filter((run) => input.agentId === void 0 || run.agentId === input.agentId).filter((run) => !input.states || input.states.includes(run.state)).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || String(a.id).localeCompare(String(b.id))));
|
|
5415
|
+
}
|
|
5416
|
+
async listActiveRuns() {
|
|
5417
|
+
return this.listRuns({ states: ["queued", "running", "suspended"] });
|
|
5098
5418
|
}
|
|
5099
5419
|
async leaseRun(input) {
|
|
5100
5420
|
const run = this.requireRun(input.runId);
|
|
@@ -5159,15 +5479,17 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5159
5479
|
const { timestamp } = now();
|
|
5160
5480
|
run.state = "suspended";
|
|
5161
5481
|
run.suspensionReason = input.reason;
|
|
5482
|
+
if (input.inputRequest) run.inputRequest = clone2(input.inputRequest);
|
|
5483
|
+
if (input.executionState) run.executionState = clone2(input.executionState);
|
|
5162
5484
|
delete run.leaseId;
|
|
5163
5485
|
run.updatedAt = timestamp;
|
|
5164
5486
|
requireTransition("Runtime Message", message.id, message.state, "acknowledge", ["leased"]);
|
|
5165
5487
|
message.state = "acknowledged";
|
|
5166
5488
|
delete message.lease;
|
|
5167
5489
|
message.updatedAt = timestamp;
|
|
5168
|
-
this.recordEvent("run_suspended", { agentId: run.agentId, messageId: message.id, runId: run.id }, {
|
|
5490
|
+
this.recordEvent("run_suspended", { agentId: run.agentId, messageId: message.id, runId: run.id }, runEventPayload(run.metadata, {
|
|
5169
5491
|
reason: input.reason
|
|
5170
|
-
});
|
|
5492
|
+
}));
|
|
5171
5493
|
return clone2(run);
|
|
5172
5494
|
}
|
|
5173
5495
|
async completeRun(input) {
|
|
@@ -5179,15 +5501,17 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5179
5501
|
const state = input.state ?? "completed";
|
|
5180
5502
|
run.state = state;
|
|
5181
5503
|
run.outcome = clone2(input.outcome);
|
|
5504
|
+
delete run.inputRequest;
|
|
5505
|
+
delete run.executionState;
|
|
5182
5506
|
delete run.leaseId;
|
|
5183
5507
|
run.updatedAt = timestamp;
|
|
5184
5508
|
message.state = "acknowledged";
|
|
5185
5509
|
delete message.lease;
|
|
5186
5510
|
message.updatedAt = timestamp;
|
|
5187
|
-
this.recordEvent("run_completed", { agentId: run.agentId, messageId: run.messageId, runId: run.id }, {
|
|
5511
|
+
this.recordEvent("run_completed", { agentId: run.agentId, messageId: run.messageId, runId: run.id }, runEventPayload(run.metadata, {
|
|
5188
5512
|
state,
|
|
5189
5513
|
outcome: input.outcome
|
|
5190
|
-
});
|
|
5514
|
+
}));
|
|
5191
5515
|
this.recordEvent("message_acknowledged", { agentId: message.agentId, messageId: message.id });
|
|
5192
5516
|
return clone2(run);
|
|
5193
5517
|
}
|
|
@@ -5218,16 +5542,18 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5218
5542
|
const { timestamp } = now();
|
|
5219
5543
|
run.state = "failed";
|
|
5220
5544
|
run.outcome = clone2(input.outcome);
|
|
5545
|
+
delete run.inputRequest;
|
|
5546
|
+
delete run.executionState;
|
|
5221
5547
|
delete run.leaseId;
|
|
5222
5548
|
run.updatedAt = timestamp;
|
|
5223
5549
|
message.state = "dead_lettered";
|
|
5224
5550
|
message.deadLetterReason = input.reason;
|
|
5225
5551
|
delete message.lease;
|
|
5226
5552
|
message.updatedAt = timestamp;
|
|
5227
|
-
this.recordEvent("run_completed", { agentId: run.agentId, messageId: message.id, runId: run.id }, {
|
|
5553
|
+
this.recordEvent("run_completed", { agentId: run.agentId, messageId: message.id, runId: run.id }, runEventPayload(run.metadata, {
|
|
5228
5554
|
state: "failed",
|
|
5229
5555
|
outcome: input.outcome
|
|
5230
|
-
});
|
|
5556
|
+
}));
|
|
5231
5557
|
this.recordEvent("message_dead_lettered", { agentId: run.agentId, messageId: message.id }, {
|
|
5232
5558
|
reason: input.reason
|
|
5233
5559
|
});
|
|
@@ -5240,6 +5566,8 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5240
5566
|
const { timestamp } = now();
|
|
5241
5567
|
run.state = "cancelled";
|
|
5242
5568
|
run.outcome = clone2(input.outcome);
|
|
5569
|
+
delete run.inputRequest;
|
|
5570
|
+
delete run.executionState;
|
|
5243
5571
|
delete run.leaseId;
|
|
5244
5572
|
run.updatedAt = timestamp;
|
|
5245
5573
|
if (message.state === "queued" || message.state === "leased") {
|
|
@@ -5247,9 +5575,10 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5247
5575
|
delete message.lease;
|
|
5248
5576
|
message.updatedAt = timestamp;
|
|
5249
5577
|
}
|
|
5250
|
-
this.recordEvent("run_aborted", { agentId: run.agentId, messageId: message.id, runId: run.id }, {
|
|
5578
|
+
this.recordEvent("run_aborted", { agentId: run.agentId, messageId: message.id, runId: run.id }, runEventPayload(run.metadata, {
|
|
5579
|
+
state: "cancelled",
|
|
5251
5580
|
outcome: input.outcome
|
|
5252
|
-
});
|
|
5581
|
+
}));
|
|
5253
5582
|
return clone2(run);
|
|
5254
5583
|
}
|
|
5255
5584
|
async recoverExpiredLeases(input) {
|
|
@@ -5390,6 +5719,29 @@ var InMemoryRuntimeStateStore = class {
|
|
|
5390
5719
|
this.eventCheckpoints.set(consumerId, checkpoint);
|
|
5391
5720
|
return clone2(checkpoint);
|
|
5392
5721
|
}
|
|
5722
|
+
async acknowledgeEventAndEnqueueAgentInput(input) {
|
|
5723
|
+
const event = this.events.find((candidate) => candidate.id === input.eventId);
|
|
5724
|
+
if (!event) throw new Error(`Runtime Event not found: ${input.eventId}.`);
|
|
5725
|
+
const current = this.eventCheckpoints.get(input.consumerId) ?? {
|
|
5726
|
+
consumerId: input.consumerId,
|
|
5727
|
+
sequence: 0,
|
|
5728
|
+
updatedAt: createTimestamp(/* @__PURE__ */ new Date(0))
|
|
5729
|
+
};
|
|
5730
|
+
if (event.sequence <= current.sequence) return { checkpoint: clone2(current) };
|
|
5731
|
+
if (event.sequence !== current.sequence + 1) {
|
|
5732
|
+
throw new Error(`Runtime Event Consumer Checkpoint cannot advance from sequence ${current.sequence} to ${event.sequence}.`);
|
|
5733
|
+
}
|
|
5734
|
+
this.requireAgent(input.agentId);
|
|
5735
|
+
const enqueued = this.enqueueAgentInputState(input);
|
|
5736
|
+
const checkpoint = {
|
|
5737
|
+
consumerId: input.consumerId,
|
|
5738
|
+
sequence: event.sequence,
|
|
5739
|
+
eventId: event.id,
|
|
5740
|
+
updatedAt: createTimestamp()
|
|
5741
|
+
};
|
|
5742
|
+
this.eventCheckpoints.set(input.consumerId, checkpoint);
|
|
5743
|
+
return { checkpoint: clone2(checkpoint), enqueued };
|
|
5744
|
+
}
|
|
5393
5745
|
requireAgent(agentId) {
|
|
5394
5746
|
const agent = this.agents.get(agentId);
|
|
5395
5747
|
if (!agent) {
|
|
@@ -5438,7 +5790,6 @@ var RUNTIME_SCHEMA_SQL = `
|
|
|
5438
5790
|
CREATE TABLE IF NOT EXISTS agents (
|
|
5439
5791
|
id TEXT PRIMARY KEY NOT NULL,
|
|
5440
5792
|
session_id TEXT NOT NULL,
|
|
5441
|
-
factory_id TEXT,
|
|
5442
5793
|
state TEXT NOT NULL CHECK (state IN ('active', 'paused')),
|
|
5443
5794
|
created_at TEXT NOT NULL,
|
|
5444
5795
|
updated_at TEXT NOT NULL
|
|
@@ -5466,6 +5817,9 @@ var RUNTIME_SCHEMA_SQL = `
|
|
|
5466
5817
|
lease_id TEXT,
|
|
5467
5818
|
outcome_json TEXT,
|
|
5468
5819
|
suspension_reason TEXT,
|
|
5820
|
+
input_request_json TEXT,
|
|
5821
|
+
execution_state_json TEXT,
|
|
5822
|
+
metadata_json TEXT,
|
|
5469
5823
|
created_at TEXT NOT NULL,
|
|
5470
5824
|
updated_at TEXT NOT NULL
|
|
5471
5825
|
);
|
|
@@ -5528,6 +5882,11 @@ function initializeRuntimeSchema(database) {
|
|
|
5528
5882
|
database.run(sql);
|
|
5529
5883
|
}
|
|
5530
5884
|
}
|
|
5885
|
+
try {
|
|
5886
|
+
database.run("ALTER TABLE agent_runs ADD COLUMN metadata_json TEXT");
|
|
5887
|
+
} catch (error) {
|
|
5888
|
+
if (!(error instanceof Error) || !/duplicate column name/i.test(error.message)) throw error;
|
|
5889
|
+
}
|
|
5531
5890
|
});
|
|
5532
5891
|
initialize();
|
|
5533
5892
|
}
|
|
@@ -5572,15 +5931,14 @@ var SqliteRuntimeStateStore = class {
|
|
|
5572
5931
|
const agent = {
|
|
5573
5932
|
id: createId("agt"),
|
|
5574
5933
|
sessionId: input.sessionId,
|
|
5575
|
-
...input.factoryId ? { factoryId: input.factoryId } : {},
|
|
5576
5934
|
state: "active",
|
|
5577
5935
|
createdAt: timestamp,
|
|
5578
5936
|
updatedAt: timestamp
|
|
5579
5937
|
};
|
|
5580
5938
|
const create = this.database.transaction(() => {
|
|
5581
5939
|
this.database.run(
|
|
5582
|
-
"INSERT INTO agents (id, session_id,
|
|
5583
|
-
[agent.id, agent.sessionId, agent.
|
|
5940
|
+
"INSERT INTO agents (id, session_id, state, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
5941
|
+
[agent.id, agent.sessionId, agent.state, agent.createdAt, agent.updatedAt]
|
|
5584
5942
|
);
|
|
5585
5943
|
this.recordEvent("agent_created", { agentId: agent.id });
|
|
5586
5944
|
});
|
|
@@ -5607,6 +5965,10 @@ var SqliteRuntimeStateStore = class {
|
|
|
5607
5965
|
return clone3(updated);
|
|
5608
5966
|
}
|
|
5609
5967
|
async enqueueAgentInput(input) {
|
|
5968
|
+
const enqueue = this.database.transaction(() => this.enqueueAgentInputState(input));
|
|
5969
|
+
return clone3(enqueue());
|
|
5970
|
+
}
|
|
5971
|
+
enqueueAgentInputState(input) {
|
|
5610
5972
|
this.requireAgent(input.agentId);
|
|
5611
5973
|
const { timestamp } = timestampFor();
|
|
5612
5974
|
const messageId = createId("rmsg");
|
|
@@ -5631,30 +5993,28 @@ var SqliteRuntimeStateStore = class {
|
|
|
5631
5993
|
messageId,
|
|
5632
5994
|
state: "queued",
|
|
5633
5995
|
attempt: 0,
|
|
5996
|
+
...suspendedRow?.metadata_json ? { metadata: parse(suspendedRow.metadata_json) } : input.input.metadata ? { metadata: clone3(input.input.metadata) } : {},
|
|
5634
5997
|
createdAt: timestamp,
|
|
5635
5998
|
updatedAt: timestamp
|
|
5636
5999
|
};
|
|
5637
6000
|
if (suspendedRow) {
|
|
5638
6001
|
message.runId = runId;
|
|
5639
6002
|
}
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
this.recordEvent("message_enqueued", { agentId: input.agentId, messageId });
|
|
5654
|
-
this.recordEvent("run_enqueued", { agentId: input.agentId, messageId, runId });
|
|
5655
|
-
});
|
|
5656
|
-
enqueue();
|
|
6003
|
+
this.insertMessage(message);
|
|
6004
|
+
if (suspendedRow) {
|
|
6005
|
+
this.database.run(
|
|
6006
|
+
"UPDATE agent_runs SET message_id = ?, state = 'queued', lease_id = NULL, suspension_reason = NULL, input_request_json = NULL, updated_at = ? WHERE id = ?",
|
|
6007
|
+
[messageId, timestamp, runId]
|
|
6008
|
+
);
|
|
6009
|
+
} else {
|
|
6010
|
+
this.database.run(
|
|
6011
|
+
"INSERT INTO agent_runs (id, agent_id, message_id, state, attempt, lease_id, outcome_json, suspension_reason, input_request_json, execution_state_json, metadata_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
6012
|
+
[run.id, run.agentId, run.messageId, run.state, run.attempt, null, null, null, null, null, run.metadata ? json(run.metadata) : null, run.createdAt, run.updatedAt]
|
|
6013
|
+
);
|
|
6014
|
+
}
|
|
6015
|
+
this.recordEventInternal("message_enqueued", { agentId: input.agentId, messageId });
|
|
5657
6016
|
const storedRun = this.requireRun(runId);
|
|
6017
|
+
this.recordEventInternal("run_enqueued", { agentId: input.agentId, messageId, runId }, runEventPayload(storedRun.metadata));
|
|
5658
6018
|
return { message: clone3(message), run: clone3(storedRun), resumed: Boolean(suspendedRow) };
|
|
5659
6019
|
}
|
|
5660
6020
|
async getMessage(messageId) {
|
|
@@ -5680,6 +6040,12 @@ var SqliteRuntimeStateStore = class {
|
|
|
5680
6040
|
const rows = this.database.query(`SELECT * FROM agent_runs ${where} ORDER BY created_at, id`).all(...params);
|
|
5681
6041
|
return rows.map((row) => this.runFromRow(row));
|
|
5682
6042
|
}
|
|
6043
|
+
async listActiveRuns() {
|
|
6044
|
+
const rows = this.database.query(
|
|
6045
|
+
"SELECT * FROM agent_runs WHERE state IN ('queued', 'running', 'suspended') ORDER BY created_at, id"
|
|
6046
|
+
).all();
|
|
6047
|
+
return rows.map((row) => this.runFromRow(row));
|
|
6048
|
+
}
|
|
5683
6049
|
async leaseRun(input) {
|
|
5684
6050
|
const lease = this.database.transaction(() => {
|
|
5685
6051
|
const run2 = this.requireRun(input.runId);
|
|
@@ -5754,17 +6120,17 @@ var SqliteRuntimeStateStore = class {
|
|
|
5754
6120
|
requireTransition2("Runtime Message", message.id, message.state, "acknowledge", ["leased"]);
|
|
5755
6121
|
const { timestamp } = timestampFor();
|
|
5756
6122
|
this.database.run(
|
|
5757
|
-
"UPDATE agent_runs SET state = ?, lease_id = NULL, suspension_reason = ?, updated_at = ? WHERE id = ?",
|
|
5758
|
-
["suspended", input.reason ?? null, timestamp, current.id]
|
|
6123
|
+
"UPDATE agent_runs SET state = ?, lease_id = NULL, suspension_reason = ?, input_request_json = ?, execution_state_json = ?, updated_at = ? WHERE id = ?",
|
|
6124
|
+
["suspended", input.reason ?? null, input.inputRequest ? json(input.inputRequest) : null, input.executionState ? json(input.executionState) : null, timestamp, current.id]
|
|
5759
6125
|
);
|
|
5760
6126
|
this.database.run(
|
|
5761
6127
|
"UPDATE runtime_messages SET state = ?, updated_at = ? WHERE id = ?",
|
|
5762
6128
|
["acknowledged", timestamp, message.id]
|
|
5763
6129
|
);
|
|
5764
6130
|
this.database.run("DELETE FROM runtime_leases WHERE run_id = ?", [current.id]);
|
|
5765
|
-
this.recordEvent("run_suspended", { agentId: current.agentId, messageId: message.id, runId: current.id }, {
|
|
6131
|
+
this.recordEvent("run_suspended", { agentId: current.agentId, messageId: message.id, runId: current.id }, runEventPayload(current.metadata, {
|
|
5766
6132
|
reason: input.reason
|
|
5767
|
-
});
|
|
6133
|
+
}));
|
|
5768
6134
|
return this.requireRun(current.id);
|
|
5769
6135
|
})();
|
|
5770
6136
|
return clone3(run);
|
|
@@ -5778,7 +6144,7 @@ var SqliteRuntimeStateStore = class {
|
|
|
5778
6144
|
const { timestamp } = timestampFor();
|
|
5779
6145
|
const state = input.state ?? "completed";
|
|
5780
6146
|
this.database.run(
|
|
5781
|
-
"UPDATE agent_runs SET state = ?, lease_id = NULL, outcome_json = ?, updated_at = ? WHERE id = ?",
|
|
6147
|
+
"UPDATE agent_runs SET state = ?, lease_id = NULL, outcome_json = ?, input_request_json = NULL, execution_state_json = NULL, updated_at = ? WHERE id = ?",
|
|
5782
6148
|
[state, json(input.outcome), timestamp, current.id]
|
|
5783
6149
|
);
|
|
5784
6150
|
this.database.run(
|
|
@@ -5786,10 +6152,10 @@ var SqliteRuntimeStateStore = class {
|
|
|
5786
6152
|
[timestamp, message.id]
|
|
5787
6153
|
);
|
|
5788
6154
|
this.database.run("DELETE FROM runtime_leases WHERE run_id = ?", [current.id]);
|
|
5789
|
-
this.recordEvent("run_completed", { agentId: current.agentId, messageId: current.messageId, runId: current.id }, {
|
|
6155
|
+
this.recordEvent("run_completed", { agentId: current.agentId, messageId: current.messageId, runId: current.id }, runEventPayload(current.metadata, {
|
|
5790
6156
|
state,
|
|
5791
6157
|
outcome: input.outcome
|
|
5792
|
-
});
|
|
6158
|
+
}));
|
|
5793
6159
|
this.recordEvent("message_acknowledged", { agentId: message.agentId, messageId: message.id });
|
|
5794
6160
|
return this.requireRun(current.id);
|
|
5795
6161
|
})();
|
|
@@ -5828,7 +6194,7 @@ var SqliteRuntimeStateStore = class {
|
|
|
5828
6194
|
requireTransition2("Runtime Message", message.id, message.state, "dead-letter", ["leased"]);
|
|
5829
6195
|
const { timestamp } = timestampFor();
|
|
5830
6196
|
this.database.run(
|
|
5831
|
-
"UPDATE agent_runs SET state = 'failed', lease_id = NULL, outcome_json = ?, updated_at = ? WHERE id = ?",
|
|
6197
|
+
"UPDATE agent_runs SET state = 'failed', lease_id = NULL, outcome_json = ?, input_request_json = NULL, execution_state_json = NULL, updated_at = ? WHERE id = ?",
|
|
5832
6198
|
[json(input.outcome), timestamp, current.id]
|
|
5833
6199
|
);
|
|
5834
6200
|
this.database.run(
|
|
@@ -5836,10 +6202,10 @@ var SqliteRuntimeStateStore = class {
|
|
|
5836
6202
|
[input.reason, timestamp, message.id]
|
|
5837
6203
|
);
|
|
5838
6204
|
this.database.run("DELETE FROM runtime_leases WHERE run_id = ?", [current.id]);
|
|
5839
|
-
this.recordEvent("run_completed", { agentId: current.agentId, messageId: message.id, runId: current.id }, {
|
|
6205
|
+
this.recordEvent("run_completed", { agentId: current.agentId, messageId: message.id, runId: current.id }, runEventPayload(current.metadata, {
|
|
5840
6206
|
state: "failed",
|
|
5841
6207
|
outcome: input.outcome
|
|
5842
|
-
});
|
|
6208
|
+
}));
|
|
5843
6209
|
this.recordEvent("message_dead_lettered", { agentId: current.agentId, messageId: message.id }, {
|
|
5844
6210
|
reason: input.reason
|
|
5845
6211
|
});
|
|
@@ -5854,7 +6220,7 @@ var SqliteRuntimeStateStore = class {
|
|
|
5854
6220
|
const message = this.requireMessage(current.messageId);
|
|
5855
6221
|
const { timestamp } = timestampFor();
|
|
5856
6222
|
this.database.run(
|
|
5857
|
-
"UPDATE agent_runs SET state = 'cancelled', lease_id = NULL, outcome_json = ?, updated_at = ? WHERE id = ?",
|
|
6223
|
+
"UPDATE agent_runs SET state = 'cancelled', lease_id = NULL, outcome_json = ?, input_request_json = NULL, execution_state_json = NULL, updated_at = ? WHERE id = ?",
|
|
5858
6224
|
[json(input.outcome), timestamp, current.id]
|
|
5859
6225
|
);
|
|
5860
6226
|
if (message.state === "queued" || message.state === "leased") {
|
|
@@ -5864,9 +6230,10 @@ var SqliteRuntimeStateStore = class {
|
|
|
5864
6230
|
]);
|
|
5865
6231
|
}
|
|
5866
6232
|
this.database.run("DELETE FROM runtime_leases WHERE run_id = ?", [current.id]);
|
|
5867
|
-
this.recordEvent("run_aborted", { agentId: current.agentId, messageId: message.id, runId: current.id }, {
|
|
6233
|
+
this.recordEvent("run_aborted", { agentId: current.agentId, messageId: message.id, runId: current.id }, runEventPayload(current.metadata, {
|
|
6234
|
+
state: "cancelled",
|
|
5868
6235
|
outcome: input.outcome
|
|
5869
|
-
});
|
|
6236
|
+
}));
|
|
5870
6237
|
return this.requireRun(current.id);
|
|
5871
6238
|
})();
|
|
5872
6239
|
return clone3(run);
|
|
@@ -6040,27 +6407,41 @@ var SqliteRuntimeStateStore = class {
|
|
|
6040
6407
|
return clone3(this.eventCheckpoint(consumerId));
|
|
6041
6408
|
}
|
|
6042
6409
|
async acknowledgeEvent(consumerId, eventId) {
|
|
6043
|
-
const
|
|
6044
|
-
|
|
6045
|
-
const current = this.eventCheckpoint(consumerId);
|
|
6046
|
-
if (event.sequence <= current.sequence) return current;
|
|
6047
|
-
if (event.sequence !== current.sequence + 1) {
|
|
6048
|
-
throw new Error(`Runtime Event Consumer Checkpoint cannot advance from sequence ${current.sequence} to ${event.sequence}.`);
|
|
6049
|
-
}
|
|
6050
|
-
const updatedAt = createTimestamp();
|
|
6051
|
-
this.database.run(
|
|
6052
|
-
`INSERT INTO runtime_event_consumers (consumer_id, sequence, event_id, updated_at)
|
|
6053
|
-
VALUES (?, ?, ?, ?)
|
|
6054
|
-
ON CONFLICT(consumer_id) DO UPDATE SET
|
|
6055
|
-
sequence = excluded.sequence,
|
|
6056
|
-
event_id = excluded.event_id,
|
|
6057
|
-
updated_at = excluded.updated_at`,
|
|
6058
|
-
[consumerId, event.sequence, event.id, updatedAt]
|
|
6059
|
-
);
|
|
6060
|
-
return this.eventCheckpoint(consumerId);
|
|
6061
|
-
})();
|
|
6410
|
+
const acknowledge = this.database.transaction(() => this.acknowledgeEventState(consumerId, eventId));
|
|
6411
|
+
const checkpoint = acknowledge();
|
|
6062
6412
|
return clone3(checkpoint);
|
|
6063
6413
|
}
|
|
6414
|
+
async acknowledgeEventAndEnqueueAgentInput(input) {
|
|
6415
|
+
const deliver = this.database.transaction(() => {
|
|
6416
|
+
const event = this.requireEvent(input.eventId);
|
|
6417
|
+
const current = this.eventCheckpoint(input.consumerId);
|
|
6418
|
+
if (event.sequence <= current.sequence) return { checkpoint: current };
|
|
6419
|
+
this.requireAgent(input.agentId);
|
|
6420
|
+
const enqueued = this.enqueueAgentInputState(input);
|
|
6421
|
+
const checkpoint = this.acknowledgeEventState(input.consumerId, input.eventId);
|
|
6422
|
+
return { checkpoint, enqueued };
|
|
6423
|
+
});
|
|
6424
|
+
return clone3(deliver());
|
|
6425
|
+
}
|
|
6426
|
+
acknowledgeEventState(consumerId, eventId) {
|
|
6427
|
+
const event = this.requireEvent(eventId);
|
|
6428
|
+
const current = this.eventCheckpoint(consumerId);
|
|
6429
|
+
if (event.sequence <= current.sequence) return current;
|
|
6430
|
+
if (event.sequence !== current.sequence + 1) {
|
|
6431
|
+
throw new Error(`Runtime Event Consumer Checkpoint cannot advance from sequence ${current.sequence} to ${event.sequence}.`);
|
|
6432
|
+
}
|
|
6433
|
+
const updatedAt = createTimestamp();
|
|
6434
|
+
this.database.run(
|
|
6435
|
+
`INSERT INTO runtime_event_consumers (consumer_id, sequence, event_id, updated_at)
|
|
6436
|
+
VALUES (?, ?, ?, ?)
|
|
6437
|
+
ON CONFLICT(consumer_id) DO UPDATE SET
|
|
6438
|
+
sequence = excluded.sequence,
|
|
6439
|
+
event_id = excluded.event_id,
|
|
6440
|
+
updated_at = excluded.updated_at`,
|
|
6441
|
+
[consumerId, event.sequence, event.id, updatedAt]
|
|
6442
|
+
);
|
|
6443
|
+
return this.eventCheckpoint(consumerId);
|
|
6444
|
+
}
|
|
6064
6445
|
insertMessage(message) {
|
|
6065
6446
|
this.database.run(
|
|
6066
6447
|
"INSERT INTO runtime_messages (id, agent_id, kind, payload_json, state, attempts, run_id, dead_letter_reason, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
@@ -6071,7 +6452,6 @@ var SqliteRuntimeStateStore = class {
|
|
|
6071
6452
|
return {
|
|
6072
6453
|
id: row.id,
|
|
6073
6454
|
sessionId: row.session_id,
|
|
6074
|
-
...row.factory_id ? { factoryId: row.factory_id } : {},
|
|
6075
6455
|
state: row.state,
|
|
6076
6456
|
createdAt: row.created_at,
|
|
6077
6457
|
updatedAt: row.updated_at
|
|
@@ -6111,6 +6491,9 @@ var SqliteRuntimeStateStore = class {
|
|
|
6111
6491
|
...row.lease_id ? { leaseId: row.lease_id } : {},
|
|
6112
6492
|
...row.outcome_json ? { outcome: parse(row.outcome_json) } : {},
|
|
6113
6493
|
...row.suspension_reason ? { suspensionReason: row.suspension_reason } : {},
|
|
6494
|
+
...row.input_request_json ? { inputRequest: parse(row.input_request_json) } : {},
|
|
6495
|
+
...row.execution_state_json ? { executionState: parse(row.execution_state_json) } : {},
|
|
6496
|
+
...row.metadata_json ? { metadata: parse(row.metadata_json) } : {},
|
|
6114
6497
|
createdAt: row.created_at,
|
|
6115
6498
|
updatedAt: row.updated_at
|
|
6116
6499
|
};
|
|
@@ -6235,8 +6618,8 @@ export {
|
|
|
6235
6618
|
AgentRun,
|
|
6236
6619
|
AgentRuntime,
|
|
6237
6620
|
InMemoryRuntimeStateStore,
|
|
6238
|
-
|
|
6239
|
-
|
|
6621
|
+
InMemorySessionStore,
|
|
6622
|
+
JsonlSessionStore,
|
|
6240
6623
|
SqliteRuntimeStateStore,
|
|
6241
6624
|
createCoreTools,
|
|
6242
6625
|
createMessage
|