@rowan-agent/agent 0.6.1 → 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 CHANGED
@@ -136,9 +136,9 @@ dead-lettered.
136
136
 
137
137
  ### Process Recovery
138
138
 
139
- Runtime startup recovers abandoned Leases into durable queued work without
140
- constructing Agent Bindings. The host supplies its current executable resources
141
- when it reconstructs an Agent:
139
+ Runtime startup and periodic recovery return expired Leases to durable queued
140
+ work without disturbing unexpired Leases owned by another process. The host
141
+ supplies its current executable resources when it reconstructs an Agent:
142
142
 
143
143
  ```ts
144
144
  const runtime = await AgentRuntime.start({ stateStore, sessionProvider });
@@ -337,17 +337,17 @@ const tools = createCoreTools({
337
337
  bashTimeoutMs?, // default: 30s
338
338
  maxBashOutputBytes?, // default: 64KB
339
339
  });
340
- // Returns: read, write, edit, bash
340
+ // Returns: read, bash, edit, write
341
341
  ```
342
342
 
343
343
  ### Built-in Tools
344
344
 
345
345
  | Tool | Description | Parameters |
346
346
  |------|-------------|------------|
347
- | `read` | Reads a text file within the workspace | `path` (required), `maxBytes?`, `type?` ("skill" \| "phase" \| "markdown" \| "code" \| "file") |
348
- | `write` | Writes content to a file, creating parent directories as needed | `path` (required), `content` (required) |
349
- | `edit` | Replaces exact `oldText` with `newText` in a file | `path` (required), `oldText` (required), `newText` (required), `replaceAll?` |
350
- | `bash` | Runs a bash command within the workspace | `command` (required), `cwd?`, `timeoutMs?`, `maxOutputBytes?` |
347
+ | `read` | Reads a file, optionally by line range | `path` (required), `offset?`, `limit?` |
348
+ | `bash` | Runs a bash command in the workspace | `command` (required), `timeout?` (seconds) |
349
+ | `edit` | Applies exact replacements, including multiple disjoint edits in one call | `path` (required), `edits[]` (each with `oldText` and `newText`) |
350
+ | `write` | Creates or overwrites a file | `path` (required), `content` (required) |
351
351
 
352
352
  ### Custom Tools
353
353
 
package/dist/index.d.ts CHANGED
@@ -1420,6 +1420,7 @@ declare class AgentRuntime {
1420
1420
  private runningRuns;
1421
1421
  private pumping;
1422
1422
  private nextWorkerId;
1423
+ private leaseRecoveryTimer;
1423
1424
  private stopped;
1424
1425
  private constructor();
1425
1426
  static start(options: AgentRuntimeOptions): Promise<AgentRuntime>;
@@ -1443,6 +1444,8 @@ declare class AgentRuntime {
1443
1444
  private registerEventConsumer;
1444
1445
  listEvents(cursor?: RuntimeEventCursor): Promise<RuntimeEvent[]>;
1445
1446
  private recover;
1447
+ private startLeaseRecovery;
1448
+ private recoverExpiredLeases;
1446
1449
  private scheduleQueuedRuns;
1447
1450
  private attachAgent;
1448
1451
  private submitAgentInput;
package/dist/index.js CHANGED
@@ -776,34 +776,27 @@ var DEFAULT_MAX_READ_BYTES = 64e3;
776
776
  var DEFAULT_BASH_TIMEOUT_MS = 3e4;
777
777
  var DEFAULT_MAX_BASH_OUTPUT_BYTES = 64e3;
778
778
  var ReadArgsSchema = Type2.Object({
779
- path: Type2.String(),
780
- maxBytes: Type2.Optional(Type2.Number()),
781
- type: Type2.Optional(Type2.Union([
782
- Type2.Literal("skill"),
783
- Type2.Literal("phase"),
784
- Type2.Literal("markdown"),
785
- Type2.Literal("code"),
786
- Type2.Literal("file")
787
- ]))
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." }))
788
782
  });
789
783
  var ReadArgsValidator = Schema.Compile(ReadArgsSchema);
790
784
  var WriteArgsSchema = Type2.Object({
791
- path: Type2.String(),
792
- content: Type2.String()
785
+ path: Type2.String({ description: "Path to the file to write." }),
786
+ content: Type2.String({ description: "Complete file contents." })
793
787
  });
794
788
  var WriteArgsValidator = Schema.Compile(WriteArgsSchema);
795
789
  var EditArgsSchema = Type2.Object({
796
- path: Type2.String(),
797
- oldText: Type2.String(),
798
- newText: Type2.String(),
799
- replaceAll: Type2.Optional(Type2.Boolean())
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." })
800
795
  });
801
796
  var EditArgsValidator = Schema.Compile(EditArgsSchema);
802
797
  var BashArgsSchema = Type2.Object({
803
- command: Type2.String(),
804
- cwd: Type2.Optional(Type2.String()),
805
- timeoutMs: Type2.Optional(Type2.Number()),
806
- maxOutputBytes: Type2.Optional(Type2.Number())
798
+ command: Type2.String({ description: "Bash command to execute." }),
799
+ timeout: Type2.Optional(Type2.Number({ description: "Timeout in seconds." }))
807
800
  });
808
801
  var BashArgsValidator = Schema.Compile(BashArgsSchema);
809
802
  var validatorCache = /* @__PURE__ */ new WeakMap();
@@ -906,12 +899,7 @@ async function executeRuntimeToolCall(input) {
906
899
  }
907
900
  await input.observe?.({ type: "tool_start", tool, args });
908
901
  try {
909
- const rawResult = await tool.execute(args, input.toolContext, input.signal);
910
- let result = {
911
- ...rawResult,
912
- toolCallId: input.toolCall.id,
913
- toolName: tool.name
914
- };
902
+ let result = await tool.execute(args, input.toolContext, input.signal);
915
903
  if (input.afterToolCall) {
916
904
  await input.observe?.({
917
905
  type: "result_review_requested",
@@ -945,6 +933,20 @@ function positiveNumber(value, name) {
945
933
  }
946
934
  return void 0;
947
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
+ }
948
950
  async function captureStream(stream, maxBytes) {
949
951
  const reader = stream.getReader();
950
952
  const chunks = [];
@@ -984,17 +986,25 @@ async function captureStream(stream, maxBytes) {
984
986
  function createReadTool(context) {
985
987
  return {
986
988
  name: "read",
987
- description: "Reads a text file within the workspace.",
989
+ description: "Read a file in the workspace.",
988
990
  parameters: ReadArgsSchema,
989
- promptSnippet: "Read a text file (max 64KB default).",
990
- promptGuidelines: [
991
- "Always read a file before editing or writing to understand its current content.",
992
- "Use maxBytes to limit output for large files."
993
- ],
991
+ promptSnippet: "Read file contents.",
992
+ promptGuidelines: ["Read files before editing them."],
994
993
  async execute(args, toolContext) {
995
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
+ }
996
1006
  const resolved = resolveCoreToolPath(context, parsed.path);
997
- const maxBytes = parsed.maxBytes ?? context.maxReadBytes;
1007
+ const maxBytes = context.maxReadBytes;
998
1008
  const invalidLimit = positiveNumber(maxBytes, "maxBytes");
999
1009
  if (invalidLimit) {
1000
1010
  return toolResult({
@@ -1016,9 +1026,12 @@ function createReadTool(context) {
1016
1026
  });
1017
1027
  }
1018
1028
  const bytes = await readFile2(resolved.absolutePath);
1019
- const sliced = bytes.subarray(0, maxBytes);
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);
1020
1033
  const text = new TextDecoder().decode(sliced);
1021
- const resourceType = parsed.type ?? detectResourceType(resolved.absolutePath);
1034
+ const resourceType = detectResourceType(resolved.absolutePath);
1022
1035
  let name;
1023
1036
  let baseDir;
1024
1037
  if (resourceType === "skill" || resourceType === "phase") {
@@ -1029,16 +1042,14 @@ function createReadTool(context) {
1029
1042
  } else {
1030
1043
  name = inferResourceName(resolved.absolutePath, basename2(resolved.absolutePath));
1031
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;
1032
1048
  return toolResult({
1033
1049
  context: toolContext,
1034
1050
  toolName: "read",
1035
1051
  ok: true,
1036
- content: {
1037
- path: resolved.relativePath,
1038
- content: formatResourceOutput({ type: resourceType, name, location: resolved.absolutePath, content: text, baseDir }),
1039
- sizeBytes: bytes.byteLength,
1040
- truncated: bytes.byteLength > maxBytes
1041
- }
1052
+ content
1042
1053
  });
1043
1054
  }
1044
1055
  };
@@ -1046,13 +1057,10 @@ function createReadTool(context) {
1046
1057
  function createWriteTool(context) {
1047
1058
  return {
1048
1059
  name: "write",
1049
- description: "Writes provided text content to a workspace file, creating parent directories as needed.",
1060
+ description: "Create or overwrite a file in the workspace.",
1050
1061
  parameters: WriteArgsSchema,
1051
- promptSnippet: "Write content to a file (creates parent dirs automatically).",
1052
- promptGuidelines: [
1053
- "Use write for new files or full file rewrites.",
1054
- "For partial edits, prefer the edit tool to avoid overwriting unchanged content."
1055
- ],
1062
+ promptSnippet: "Create or overwrite files.",
1063
+ promptGuidelines: ["Use edit for partial changes."],
1056
1064
  async execute(args, toolContext) {
1057
1065
  const parsed = WriteArgsValidator.Parse(args);
1058
1066
  const resolved = resolveCoreToolPath(context, parsed.path);
@@ -1062,69 +1070,74 @@ function createWriteTool(context) {
1062
1070
  context: toolContext,
1063
1071
  toolName: "write",
1064
1072
  ok: true,
1065
- content: {
1066
- path: resolved.relativePath,
1067
- bytesWritten: new TextEncoder().encode(parsed.content).byteLength
1068
- }
1073
+ content: `Successfully wrote ${parsed.content.length} bytes to ${resolved.relativePath}.`
1069
1074
  });
1070
1075
  }
1071
1076
  };
1072
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
+ }
1073
1109
  function createEditTool(context) {
1074
1110
  return {
1075
1111
  name: "edit",
1076
- description: "Edits a workspace text file by replacing exact oldText with newText.",
1112
+ description: "Apply exact text replacements to a workspace file.",
1077
1113
  parameters: EditArgsSchema,
1078
- promptSnippet: "Replace exact text in a file (oldText \u2192 newText).",
1079
- promptGuidelines: [
1080
- "Read the file first to get the exact oldText to replace.",
1081
- "oldText must be an exact match including whitespace and indentation.",
1082
- "If oldText appears multiple times, set replaceAll=true or provide more surrounding context."
1083
- ],
1114
+ promptSnippet: "Apply exact text replacements.",
1115
+ promptGuidelines: ["Read the file first; each oldText must match exactly once."],
1084
1116
  async execute(args, toolContext) {
1085
1117
  const parsed = EditArgsValidator.Parse(args);
1086
- if (!parsed.oldText) {
1087
- return toolResult({
1088
- context: toolContext,
1089
- toolName: "edit",
1090
- ok: false,
1091
- content: null,
1092
- error: "oldText must not be empty."
1093
- });
1094
- }
1095
1118
  const resolved = resolveCoreToolPath(context, parsed.path);
1096
1119
  const current = await readFile2(resolved.absolutePath, "utf8");
1097
- if (!current.includes(parsed.oldText)) {
1098
- return toolResult({
1099
- context: toolContext,
1100
- toolName: "edit",
1101
- ok: false,
1102
- content: null,
1103
- error: `oldText not found in ${resolved.relativePath}.`
1104
- });
1105
- }
1106
- const matches = current.split(parsed.oldText).length - 1;
1107
- if (matches > 1 && !parsed.replaceAll) {
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) {
1108
1127
  return toolResult({
1109
1128
  context: toolContext,
1110
1129
  toolName: "edit",
1111
1130
  ok: false,
1112
1131
  content: null,
1113
- error: `oldText appears ${matches} times in ${resolved.relativePath}; set replaceAll=true or provide more context.`
1132
+ error: error instanceof Error ? error.message : String(error)
1114
1133
  });
1115
1134
  }
1116
- const replacements = parsed.replaceAll ? matches : 1;
1117
- const next = parsed.replaceAll ? current.split(parsed.oldText).join(parsed.newText) : current.replace(parsed.oldText, parsed.newText);
1118
1135
  await writeFile(resolved.absolutePath, next, "utf8");
1119
1136
  return toolResult({
1120
1137
  context: toolContext,
1121
1138
  toolName: "edit",
1122
1139
  ok: true,
1123
- content: {
1124
- path: resolved.relativePath,
1125
- replacements,
1126
- bytesWritten: new TextEncoder().encode(next).byteLength
1127
- }
1140
+ content: `Successfully replaced ${replacements} block(s) in ${resolved.relativePath}.`
1128
1141
  });
1129
1142
  }
1130
1143
  };
@@ -1132,19 +1145,15 @@ function createEditTool(context) {
1132
1145
  function createBashTool(context) {
1133
1146
  return {
1134
1147
  name: "bash",
1135
- description: "Runs a bash command within the workspace.",
1148
+ description: "Run a bash command in the workspace.",
1136
1149
  parameters: BashArgsSchema,
1137
- promptSnippet: "Execute a bash command (30s timeout, 64KB output limit).",
1138
- promptGuidelines: [
1139
- "Use bash for build commands, tests, git operations, and system tools.",
1140
- "Prefer dedicated tools (read/write/edit) for file operations.",
1141
- "Set timeoutMs for long-running commands."
1142
- ],
1150
+ promptSnippet: "Run shell commands.",
1151
+ promptGuidelines: ["Use read/write/edit for file operations."],
1143
1152
  async execute(args, toolContext, signal) {
1144
1153
  const parsed = BashArgsValidator.Parse(args);
1145
- const timeoutMs = parsed.timeoutMs ?? context.bashTimeoutMs;
1146
- const maxOutputBytes = parsed.maxOutputBytes ?? context.maxBashOutputBytes;
1147
- const invalidTimeout = positiveNumber(timeoutMs, "timeoutMs");
1154
+ const timeoutMs = parsed.timeout === void 0 ? context.bashTimeoutMs : parsed.timeout * 1e3;
1155
+ const maxOutputBytes = context.maxBashOutputBytes;
1156
+ const invalidTimeout = positiveNumber(timeoutMs, "timeout");
1148
1157
  const invalidOutputLimit = positiveNumber(maxOutputBytes, "maxOutputBytes");
1149
1158
  if (invalidTimeout || invalidOutputLimit) {
1150
1159
  return toolResult({
@@ -1155,7 +1164,7 @@ function createBashTool(context) {
1155
1164
  error: invalidTimeout ?? invalidOutputLimit
1156
1165
  });
1157
1166
  }
1158
- const cwd = resolveCoreToolPath(context, parsed.cwd ?? ".");
1167
+ const cwd = resolveCoreToolPath(context, ".");
1159
1168
  let timedOut = false;
1160
1169
  let aborted = false;
1161
1170
  const proc = Bun.spawn(["bash", "-lc", parsed.command], {
@@ -1182,19 +1191,14 @@ function createBashTool(context) {
1182
1191
  proc.exited
1183
1192
  ]);
1184
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;
1185
1197
  return toolResult({
1186
1198
  context: toolContext,
1187
1199
  toolName: "bash",
1188
1200
  ok,
1189
- content: {
1190
- command: parsed.command,
1191
- cwd: cwd.relativePath,
1192
- exitCode,
1193
- stdout: stdout.text,
1194
- stderr: stderr.text,
1195
- stdoutTruncated: stdout.truncated,
1196
- stderrTruncated: stderr.truncated
1197
- },
1201
+ content,
1198
1202
  ...ok ? {} : {
1199
1203
  error: timedOut ? `Command timed out after ${timeoutMs}ms.` : aborted ? "Command aborted." : `Command exited with ${exitCode}.`
1200
1204
  }
@@ -1210,9 +1214,9 @@ function createCoreTools(input = {}) {
1210
1214
  const context = createCoreToolContext(input);
1211
1215
  return [
1212
1216
  createReadTool(context),
1213
- createWriteTool(context),
1217
+ createBashTool(context),
1214
1218
  createEditTool(context),
1215
- createBashTool(context)
1219
+ createWriteTool(context)
1216
1220
  ];
1217
1221
  }
1218
1222
 
@@ -1403,6 +1407,23 @@ function removePhaseMessage(messages, msgId) {
1403
1407
  const idx = messages.findIndex((m) => m.id === msgId);
1404
1408
  if (idx !== -1) messages.splice(idx, 1);
1405
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
+ }
1406
1427
  function normalizePayload(payload) {
1407
1428
  if (typeof payload === "string") {
1408
1429
  try {
@@ -1539,6 +1560,15 @@ function createToolResultContent(result) {
1539
1560
  }
1540
1561
  ];
1541
1562
  }
1563
+ function createRouteToolResultContent(toolCall) {
1564
+ return [
1565
+ {
1566
+ type: "tool_result",
1567
+ toolUseId: toolCall.id,
1568
+ content: '{"ok": true}'
1569
+ }
1570
+ ];
1571
+ }
1542
1572
  async function executePhaseWithModel(ctx) {
1543
1573
  const executableToolNames = new Set(
1544
1574
  ctx.context.tools.filter((tool) => tool.name !== PhaseRouteTool).map((tool) => tool.name)
@@ -1561,6 +1591,13 @@ async function executePhaseWithModel(ctx) {
1561
1591
  phase: ctx.phase.name,
1562
1592
  toolCalls: collected.toolCalls
1563
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
+ }
1564
1601
  const executableToolCalls = collected.toolCalls.filter(
1565
1602
  (toolCall) => executableToolNames.has(toolCall.name)
1566
1603
  );
@@ -1603,6 +1640,7 @@ async function runPhaseLoop(config, state, registry) {
1603
1640
  }
1604
1641
  let previousPayload = resumingSuspendedRun ? state.continuation?.previousPayload : void 0;
1605
1642
  let previousPhaseMsgId = resumingSuspendedRun ? state.continuation?.previousPhaseMessageId : void 0;
1643
+ let previousPhaseInputMsgId;
1606
1644
  let previousResults = resumingSuspendedRun ? state.continuation?.previousResults?.map((result) => ({ ...result })) ?? [] : [];
1607
1645
  let pendingInstruction = resumingSuspendedRun ? state.continuation?.pendingInstruction : void 0;
1608
1646
  while (currentPhaseId) {
@@ -1613,7 +1651,9 @@ async function runPhaseLoop(config, state, registry) {
1613
1651
  const abortResult = LoopGuard.checkAbort(config.signal);
1614
1652
  if (abortResult.stopReason !== "none") {
1615
1653
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1654
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1616
1655
  previousPhaseMsgId = void 0;
1656
+ previousPhaseInputMsgId = void 0;
1617
1657
  return completeRun(config, state, createOutcome.aborted());
1618
1658
  }
1619
1659
  state.metrics.iterations++;
@@ -1672,13 +1712,17 @@ async function runPhaseLoop(config, state, registry) {
1672
1712
  if (extBefore.abort) {
1673
1713
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1674
1714
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1715
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1675
1716
  previousPhaseMsgId = void 0;
1717
+ previousPhaseInputMsgId = void 0;
1676
1718
  return completeRun(config, state, extBefore.abort);
1677
1719
  }
1678
1720
  if (extBefore.skip) {
1679
1721
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1680
1722
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1723
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1681
1724
  previousPhaseMsgId = void 0;
1725
+ previousPhaseInputMsgId = void 0;
1682
1726
  if (extBefore.skip.route === "stop") {
1683
1727
  return completeRun(config, state, {
1684
1728
  id: "skip",
@@ -1696,13 +1740,32 @@ async function runPhaseLoop(config, state, registry) {
1696
1740
  if (enteringNewPhase || !phaseMessageIsPresent) {
1697
1741
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1698
1742
  removePhaseMessage(phaseContext.messages, previousPhaseMsgId);
1743
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1744
+ removePhaseMessage(phaseContext.messages, previousPhaseInputMsgId);
1699
1745
  previousPhaseMsgId = void 0;
1746
+ previousPhaseInputMsgId = void 0;
1747
+ const latestUserInputMessage = findLatestUserInputMessage(config.context.messages);
1748
+ const lastMessageBeforePhase = config.context.messages.at(-1);
1700
1749
  previousPhaseMsgId = injectPhaseContent(
1701
1750
  phase,
1702
1751
  { results: previousResults, instruction: pendingInstruction },
1703
1752
  config.context.messages,
1704
1753
  phaseContext.messages
1705
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
+ }
1706
1769
  previousResults = [];
1707
1770
  pendingInstruction = void 0;
1708
1771
  }
@@ -1720,7 +1783,9 @@ async function runPhaseLoop(config, state, registry) {
1720
1783
  if (extAfter.abort) {
1721
1784
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1722
1785
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1786
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1723
1787
  previousPhaseMsgId = void 0;
1788
+ previousPhaseInputMsgId = void 0;
1724
1789
  return completeRun(config, state, extAfter.abort);
1725
1790
  }
1726
1791
  if (extAfter.retry && (phase.run || phase.factory)) {
@@ -1740,7 +1805,9 @@ async function runPhaseLoop(config, state, registry) {
1740
1805
  if (preAbort.stopReason !== "none") {
1741
1806
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1742
1807
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1808
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1743
1809
  previousPhaseMsgId = void 0;
1810
+ previousPhaseInputMsgId = void 0;
1744
1811
  return completeRun(config, state, createOutcome.aborted());
1745
1812
  }
1746
1813
  const requestedAt = createTimestamp();
@@ -1768,7 +1835,9 @@ async function runPhaseLoop(config, state, registry) {
1768
1835
  if (abortResult2.stopReason !== "none") {
1769
1836
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1770
1837
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1838
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1771
1839
  previousPhaseMsgId = void 0;
1840
+ previousPhaseInputMsgId = void 0;
1772
1841
  return completeRun(config, state, createOutcome.aborted());
1773
1842
  }
1774
1843
  for (const message of userMessages) {
@@ -1779,7 +1848,9 @@ async function runPhaseLoop(config, state, registry) {
1779
1848
  }
1780
1849
  config.emit?.({ type: "phase_end", phase: currentPhaseId, ts: createTimestamp() });
1781
1850
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1851
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1782
1852
  previousPhaseMsgId = void 0;
1853
+ previousPhaseInputMsgId = void 0;
1783
1854
  return completeRun(config, state, createOutcome.default(output, config.context.messages));
1784
1855
  }
1785
1856
  if (output.route === "continue") {
@@ -1822,7 +1893,9 @@ async function runPhaseLoop(config, state, registry) {
1822
1893
  const entryPhaseId = phase.target ?? registry.entryPhaseId;
1823
1894
  if (entryPhaseId === "stop") {
1824
1895
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1896
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1825
1897
  previousPhaseMsgId = void 0;
1898
+ previousPhaseInputMsgId = void 0;
1826
1899
  return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
1827
1900
  }
1828
1901
  currentPhaseId = entryPhaseId;
@@ -1839,13 +1912,17 @@ async function runPhaseLoop(config, state, registry) {
1839
1912
  }
1840
1913
  if (nextRoute === "stop") {
1841
1914
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1915
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1842
1916
  previousPhaseMsgId = void 0;
1917
+ previousPhaseInputMsgId = void 0;
1843
1918
  return completeRun(config, state, createTerminalOutcome(phase, output, config.context.messages));
1844
1919
  }
1845
1920
  const targetPhaseId = nextRoute;
1846
1921
  if (!registry.phases.has(targetPhaseId)) {
1847
1922
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
1923
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
1848
1924
  previousPhaseMsgId = void 0;
1925
+ previousPhaseInputMsgId = void 0;
1849
1926
  return completeRun(config, state, createOutcome.phaseNotFound(output));
1850
1927
  }
1851
1928
  state.metrics.phaseTransitions.push({
@@ -1961,24 +2038,31 @@ async function executePhase(ctx) {
1961
2038
  return executePhaseWithModel(ctx);
1962
2039
  }
1963
2040
  async function executeToolCall(input) {
2041
+ let result;
1964
2042
  if (input.config.runtime?.tools) {
1965
- return input.config.runtime.tools({
2043
+ result = await input.config.runtime.tools({
1966
2044
  config: input.config,
1967
2045
  toolCall: input.toolCall
1968
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
+ });
1969
2060
  }
1970
- const toolContext = {
1971
- skills: input.config.context.skills,
1972
- toolCallId: input.toolCall.id
2061
+ return {
2062
+ ...result,
2063
+ toolCallId: input.toolCall.id,
2064
+ toolName: input.toolCall.name
1973
2065
  };
1974
- return executeRuntimeToolCall({
1975
- tools: input.tools,
1976
- toolCall: input.toolCall,
1977
- toolContext,
1978
- beforeToolCall: input.config.beforeToolCall,
1979
- afterToolCall: input.config.afterToolCall,
1980
- signal: input.config.signal
1981
- });
1982
2066
  }
1983
2067
  function createPhaseExecution(config, state, allTools, phase, messageManager, toolExecutionManager, registry) {
1984
2068
  return {
@@ -4689,6 +4773,7 @@ var AgentRuntime = class _AgentRuntime {
4689
4773
  runningRuns = 0;
4690
4774
  pumping = false;
4691
4775
  nextWorkerId = 0;
4776
+ leaseRecoveryTimer;
4692
4777
  stopped = false;
4693
4778
  constructor(options) {
4694
4779
  this.stateStore = options.stateStore;
@@ -4724,6 +4809,7 @@ var AgentRuntime = class _AgentRuntime {
4724
4809
  activeRuntime = runtime;
4725
4810
  try {
4726
4811
  await runtime.recover();
4812
+ runtime.startLeaseRecovery();
4727
4813
  return runtime;
4728
4814
  } catch (error) {
4729
4815
  await runtime.stop().catch(() => void 0);
@@ -4766,6 +4852,10 @@ var AgentRuntime = class _AgentRuntime {
4766
4852
  async stop() {
4767
4853
  if (this.stopped) return;
4768
4854
  this.stopped = true;
4855
+ if (this.leaseRecoveryTimer) {
4856
+ clearInterval(this.leaseRecoveryTimer);
4857
+ this.leaseRecoveryTimer = void 0;
4858
+ }
4769
4859
  let suspendedAgents;
4770
4860
  try {
4771
4861
  suspendedAgents = new Set(
@@ -4943,13 +5033,26 @@ var AgentRuntime = class _AgentRuntime {
4943
5033
  return this.stateStore.listEvents(cursor);
4944
5034
  }
4945
5035
  async recover() {
4946
- await this.stateStore.recoverLeases();
5036
+ await this.recoverExpiredLeases();
4947
5037
  const agents = await this.stateStore.listAgents();
4948
5038
  for (const record of agents) {
4949
5039
  if (record.state === "paused") this.pausedAgents.add(record.id);
4950
5040
  }
4951
5041
  this.publishEvents();
4952
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
+ }
4953
5056
  async scheduleQueuedRuns(agentId) {
4954
5057
  if (!this.bindings.has(agentId)) return;
4955
5058
  const runs = await this.stateStore.listRuns({ agentId, states: ["queued"] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowan-agent/agent",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,7 +23,7 @@
23
23
  "build": "tsup && bun scripts/check-public-interface.ts"
24
24
  },
25
25
  "dependencies": {
26
- "@rowan-agent/models": "0.6.1",
26
+ "@rowan-agent/models": "0.6.2",
27
27
  "jiti": "2.7.0",
28
28
  "typebox": "^1.0.0"
29
29
  },