@rowan-agent/agent 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _rowan_agent_models from '@rowan-agent/models';
2
2
  import { LlmContentPart, ContentBlock, ModelRef, ModelConfig, StreamFn } from '@rowan-agent/models';
3
- export { ModelConfig, ModelRef, StreamFn } from '@rowan-agent/models';
3
+ export { ModelConfig, ModelRef, StreamFn, ThinkingLevel } from '@rowan-agent/models';
4
4
  import Type from 'typebox';
5
5
 
6
6
  type AgentMessage = {
@@ -455,11 +455,11 @@ type ExtensionDisposer = () => void | Promise<void>;
455
455
  type ExtensionFactoryResult = void | ExtensionDisposer;
456
456
  type ExtensionFactory = (api: ExtensionAPI) => ExtensionFactoryResult | Promise<ExtensionFactoryResult>;
457
457
 
458
- /** Unified phase output — model decides routing via route. */
458
+ /** Unified phase output — model-driven routing is optional; programmatic phases may normalize omission to stop. */
459
459
  type PhaseOutput = {
460
460
  message: string;
461
- /** Route to next phase, or "continue" to re-execute current phase, or "stop" to end */
462
- route: string;
461
+ /** Route to next phase, or "continue" to re-execute current phase, or "stop" to end. Undefined means no model route. */
462
+ route?: string;
463
463
  /** Phase name that produced this output */
464
464
  phase?: string;
465
465
  /** Tool calls from the model invocation (used by framework for route extraction) */
package/dist/index.js CHANGED
@@ -91,12 +91,12 @@ function buildPhaseDirectiveMessage(phase, output) {
91
91
  const parts = [];
92
92
  parts.push(`<phase_content name="${escapeXml(phase.name)}">`);
93
93
  parts.push(phase.content);
94
- if (output.results && output.results.length > 0) {
94
+ if (output.results && output.results.length > 0 || output.instruction) {
95
95
  parts.push(` <prev_phase_outputs>`);
96
96
  if (output.instruction) {
97
97
  parts.push(` <instruction>${escapeXml(output.instruction)}</instruction>`);
98
98
  }
99
- for (const r of output.results) {
99
+ for (const r of output.results ?? []) {
100
100
  parts.push(` <phase name="${escapeXml(r.name)}">`);
101
101
  if (r.output !== void 0) {
102
102
  parts.push(jsonToXml(r.output, 3));
@@ -172,18 +172,24 @@ function buildPhaseEntry(p) {
172
172
  function buildRouteDescription(availablePhases) {
173
173
  const phasesBlock = buildStructuredSection("phase", [
174
174
  ...availablePhases.map(buildPhaseEntry),
175
- { name: "stop", description: "Terminate the workflow and return final result to the user" }
175
+ {
176
+ name: "stop",
177
+ description: "Explicitly finish the current user request or task when no further user input is needed"
178
+ }
176
179
  ]);
177
180
  return [
178
- "Route execution to one or more phases concurrently, or stop execution.",
181
+ "Route is optional. Omit it when the current reply should wait for more user input and remain in the current phase.",
179
182
  "",
180
183
  "Rules:",
181
- "- `decision` lists phase executions; use phase 'stop' to terminate.",
184
+ "- Call route only to continue execution immediately in one or more phases, or to explicitly stop.",
185
+ "- `decision` lists phase executions; a single target equal to the current phase starts another iteration of that phase.",
186
+ "- `stop` means the current user request or task is complete and no further user input is needed; `stop` must be the only target.",
182
187
  "- Each target may include `phase`, `reason`, `payload`.",
183
188
  "- A phase may appear multiple times as independent execution instances.",
184
189
  "- `payload` MUST match the phase's `payload_schema`",
185
190
  "- `instruction` is optional shared guidance for all phases.",
186
191
  "- Executions are independent and concurrent; order is irrelevant.",
192
+ "- Do not call route in the same response as an ordinary tool; finish ordinary tools first.",
187
193
  "",
188
194
  "<available_phases>",
189
195
  phasesBlock,
@@ -202,12 +208,13 @@ function createRouteTool(availablePhases) {
202
208
  return {
203
209
  name: PhaseRouteTool,
204
210
  description: buildRouteDescription(availablePhases),
205
- promptSnippet: "Route to next phase on completion.",
211
+ promptSnippet: "Route is optional: omit it to remain in the current phase and wait for user input; use it for immediate phase execution or an explicit stop.",
206
212
  promptGuidelines: [
207
- "Call route immediately when the phase is complete."
213
+ "Do not call route together with ordinary tools.",
214
+ "Use route(stop) only when the current user request or task is complete and no further user input is needed."
208
215
  ],
209
216
  parameters: Type.Object({
210
- decision: Type.Array(DecisionTarget, { description: "Phase executions to start" }),
217
+ decision: Type.Array(DecisionTarget, { description: "Phase executions to start", minItems: 1 }),
211
218
  instruction: Type.Optional(Type.String({ description: "Overall instruction, passed as context" }))
212
219
  }),
213
220
  // No-op: this tool is intercepted by phases, never executed via tool execution
@@ -220,29 +227,35 @@ function createRouteTool(availablePhases) {
220
227
  };
221
228
  }
222
229
  function extractRouteCall(toolCalls) {
223
- const routeCall = toolCalls.find((t) => t.name === PhaseRouteTool);
224
- if (!routeCall) return void 0;
230
+ const routeCalls = toolCalls.filter((t) => t.name === PhaseRouteTool);
231
+ if (routeCalls.length !== 1) return void 0;
232
+ const routeCall = routeCalls[0];
225
233
  let args;
226
234
  if (typeof routeCall.args === "string") {
227
235
  try {
228
- args = JSON.parse(routeCall.args);
236
+ const parsed = JSON.parse(routeCall.args);
237
+ args = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
229
238
  } catch {
230
- return void 0;
239
+ return { decision: [{ phase: "" }] };
231
240
  }
232
241
  } else {
233
- args = routeCall.args;
242
+ args = routeCall.args !== null && typeof routeCall.args === "object" ? routeCall.args : {};
234
243
  }
235
244
  const decisionRaw = args.decision;
236
245
  let decision = [];
237
246
  if (Array.isArray(decisionRaw)) {
238
247
  decision = decisionRaw.map((d) => {
239
- const obj = d;
248
+ const obj = d !== null && typeof d === "object" ? d : {};
240
249
  return {
241
- phase: typeof obj?.phase === "string" ? obj.phase : "stop",
250
+ // Empty phase is an invalid target sentinel. Never coerce malformed
251
+ // input to stop: only an explicit, valid stop target may terminate.
252
+ phase: typeof obj.phase === "string" ? obj.phase : "",
242
253
  reason: typeof obj?.reason === "string" ? obj.reason : void 0,
243
254
  payload: obj?.payload
244
255
  };
245
256
  });
257
+ } else if (decisionRaw !== void 0) {
258
+ decision = [{ phase: "" }];
246
259
  }
247
260
  return {
248
261
  decision,
@@ -765,6 +778,15 @@ function contentBlocksToMessageContent(blocks) {
765
778
  });
766
779
  }
767
780
 
781
+ // src/loop/types.ts
782
+ var DEFAULT_MAX_ATTEMPTS = 16;
783
+ function validateMaxAttempts(value) {
784
+ if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) {
785
+ throw new TypeError("maxAttempts must be a positive integer.");
786
+ }
787
+ return value;
788
+ }
789
+
768
790
  // src/loop/errors.ts
769
791
  var EmptyResponseError = class extends Error {
770
792
  code = "empty_response";
@@ -882,7 +904,7 @@ async function collectStreamResult(input) {
882
904
  doneResponse = event.response;
883
905
  stopReason = event.response?.stopReason;
884
906
  const contentBlocks2 = lastPartial?.contentBlocks ?? [];
885
- const hasContent2 = contentBlocks2.length > 0 || (event.response?.content?.length ?? 0) > 0;
907
+ const hasContent2 = contentBlocks2.length > 0 || (event.response?.content?.length ?? 0) > 0 || (event.response?.toolCalls?.length ?? 0) > 0;
886
908
  const shouldStoreContentParts = contentBlocks2.some((block) => block.type !== "text");
887
909
  if (activeMessageId && hasContent2 && shouldStoreContentParts) {
888
910
  input.message.replaceContent(activeMessageId, contentBlocksToMessageContent(contentBlocks2));
@@ -905,7 +927,7 @@ async function collectStreamResult(input) {
905
927
  if (activeMessageId) {
906
928
  await input.message.end(activeMessageId);
907
929
  }
908
- const hasContent = (lastPartial?.contentBlocks?.length ?? 0) > 0 || (doneResponse?.content?.length ?? 0) > 0;
930
+ const hasContent = (lastPartial?.contentBlocks?.length ?? 0) > 0 || (doneResponse?.content?.length ?? 0) > 0 || (doneResponse?.toolCalls?.length ?? 0) > 0;
909
931
  if (!hasContent && stopReason !== "error" && stopReason !== "aborted") {
910
932
  throw new EmptyResponseError();
911
933
  }
@@ -1497,7 +1519,7 @@ function createDefaultPhase() {
1497
1519
  filePath: "",
1498
1520
  baseDir: "",
1499
1521
  skills: [],
1500
- content: "Execute tasks using current context.\nNo planning. No evaluation.\nRoute to next phase or stop when done."
1522
+ content: "Execute tasks using current context.\nNo planning. No evaluation.\nOmit route when more user input is needed. Use route for immediate phase execution, or route(stop) only when the current user request or task is complete and no further user input is needed."
1501
1523
  };
1502
1524
  }
1503
1525
 
@@ -1821,6 +1843,31 @@ function applyFirstDecision(route, output) {
1821
1843
  if (first.reason) output.routeReason = first.reason;
1822
1844
  if (first.payload !== void 0) output.payload = normalizePayload(first.payload);
1823
1845
  }
1846
+ function resolveRouteDecision(route, registry) {
1847
+ if (!route || route.decision.length === 0) return void 0;
1848
+ const requestedCount = route.decision.length;
1849
+ const hasStop = route.decision.some(({ phase }) => phase === "stop");
1850
+ if (hasStop) {
1851
+ if (requestedCount !== 1) return void 0;
1852
+ return { ...route, requestedCount };
1853
+ }
1854
+ const decision = route.decision.filter(({ phase }) => registry.phases.has(phase));
1855
+ if (decision.length === 0) return void 0;
1856
+ return { ...route, decision, requestedCount };
1857
+ }
1858
+ function resolveModelRouteDecision(toolCalls, registry) {
1859
+ return resolveRouteDecision(toolCalls ? extractRouteCall(toolCalls) : void 0, registry);
1860
+ }
1861
+ function resolveHookRouteDecision(output, registry) {
1862
+ if (output.route === void 0 || output.route === "continue") return void 0;
1863
+ return resolveRouteDecision({
1864
+ decision: [{
1865
+ phase: output.route,
1866
+ ...output.routeReason ? { reason: output.routeReason } : {},
1867
+ ...output.payload === void 0 ? {} : { payload: output.payload }
1868
+ }]
1869
+ }, registry);
1870
+ }
1824
1871
  function injectPhaseContent(phase, output, messages, mirror) {
1825
1872
  try {
1826
1873
  const phaseContent = phase.filePath ? readPhaseContent(phase) : phase.content ?? phase.description ?? "";
@@ -1952,12 +1999,13 @@ function createToolResultContent(result) {
1952
1999
  }
1953
2000
  ];
1954
2001
  }
1955
- function createRouteToolResultContent(toolCall) {
2002
+ function createRouteToolResultContent(toolCall, error) {
1956
2003
  return [
1957
2004
  {
1958
2005
  type: "tool_result",
1959
2006
  toolUseId: toolCall.id,
1960
- content: '{"ok": true}'
2007
+ content: error ? JSON.stringify({ ok: false, error }) : '{"ok": true}',
2008
+ ...error ? { isError: true } : {}
1961
2009
  }
1962
2010
  ];
1963
2011
  }
@@ -1967,7 +2015,6 @@ async function executePhaseWithModel(ctx) {
1967
2015
  );
1968
2016
  let output = {
1969
2017
  message: "",
1970
- route: "stop",
1971
2018
  phase: ctx.phase.name,
1972
2019
  toolCalls: []
1973
2020
  };
@@ -1979,15 +2026,24 @@ async function executePhaseWithModel(ctx) {
1979
2026
  const collected = await ctx.execution.invokeModel(roundContext);
1980
2027
  output = {
1981
2028
  message: collected.text,
1982
- route: "stop",
1983
2029
  phase: ctx.phase.name,
1984
- toolCalls: collected.toolCalls
2030
+ // A route decision sharing a model response with an ordinary Tool is
2031
+ // rejected for this round. Keep ordinary calls visible so they can run,
2032
+ // but do not let the route escape the error/re-decision boundary.
2033
+ toolCalls: collected.toolCalls.some((candidate) => candidate.name === PhaseRouteTool) && collected.toolCalls.some((candidate) => candidate.name !== PhaseRouteTool) ? collected.toolCalls.filter((candidate) => candidate.name !== PhaseRouteTool) : collected.toolCalls
1985
2034
  };
1986
2035
  for (const toolCall of collected.toolCalls) {
1987
2036
  if (toolCall.name !== PhaseRouteTool) continue;
1988
- const messageId = ctx.messageManager.start("tool", createRouteToolResultContent(toolCall), {
1989
- phase: ctx.phase.name
1990
- });
2037
+ const messageId = ctx.messageManager.start(
2038
+ "tool",
2039
+ createRouteToolResultContent(
2040
+ toolCall,
2041
+ collected.toolCalls.some((candidate) => candidate.name !== PhaseRouteTool) ? "Route must be called separately after ordinary tools finish; this route decision was ignored." : void 0
2042
+ ),
2043
+ {
2044
+ phase: ctx.phase.name
2045
+ }
2046
+ );
1991
2047
  await ctx.messageManager.end(messageId);
1992
2048
  }
1993
2049
  const executableToolCalls = collected.toolCalls.filter(
@@ -2019,6 +2075,7 @@ async function startPhaseLoop(config, state) {
2019
2075
  return runPhaseLoop(config, state, registry);
2020
2076
  }
2021
2077
  async function runPhaseLoop(config, state, registry) {
2078
+ const maxAttempts = validateMaxAttempts(config.maxAttempts) ?? DEFAULT_MAX_ATTEMPTS;
2022
2079
  const resumingSuspendedRun = state.status === "suspended" && Boolean(state.currentPhase);
2023
2080
  let currentPhaseId = resumingSuspendedRun ? state.currentPhase : registry.entryPhaseId;
2024
2081
  let isContinuing = resumingSuspendedRun ? state.continuation?.isContinuing ?? true : false;
@@ -2030,6 +2087,53 @@ async function runPhaseLoop(config, state, registry) {
2030
2087
  let previousPhaseInputMsgId;
2031
2088
  let previousResults = resumingSuspendedRun ? state.continuation?.previousResults?.map((result) => ({ ...result })) ?? [] : [];
2032
2089
  let pendingInstruction = resumingSuspendedRun ? state.continuation?.pendingInstruction : void 0;
2090
+ const awaitInput = async (prompt, resumeAsContinuing) => {
2091
+ if (!config.waitForInput) return void 0;
2092
+ const preAbort = LoopGuard.checkAbort(config.signal);
2093
+ if (preAbort.stopReason !== "none") {
2094
+ removePhaseMessage(config.context.messages, previousPhaseMsgId);
2095
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2096
+ previousPhaseMsgId = void 0;
2097
+ previousPhaseInputMsgId = void 0;
2098
+ return {
2099
+ type: "completed",
2100
+ result: await completeRun(config, state, createOutcome.aborted())
2101
+ };
2102
+ }
2103
+ const requestedAt = createTimestamp();
2104
+ const inputRequest = {
2105
+ phase: currentPhaseId,
2106
+ prompt,
2107
+ requestedAt
2108
+ };
2109
+ state.currentPhase = currentPhaseId;
2110
+ state.status = "suspended";
2111
+ state.attempt = 0;
2112
+ state.continuation = {
2113
+ isContinuing: resumeAsContinuing,
2114
+ previousPayload,
2115
+ previousResults: previousResults.map((result) => ({ ...result })),
2116
+ pendingInstruction,
2117
+ previousPhaseMessageId: previousPhaseMsgId
2118
+ };
2119
+ const userMessages = await config.waitForInput(state, inputRequest);
2120
+ state.status = "running";
2121
+ const abortResult = LoopGuard.checkAbort(config.signal);
2122
+ if (abortResult.stopReason !== "none") {
2123
+ removePhaseMessage(config.context.messages, previousPhaseMsgId);
2124
+ removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2125
+ previousPhaseMsgId = void 0;
2126
+ previousPhaseInputMsgId = void 0;
2127
+ return {
2128
+ type: "completed",
2129
+ result: await completeRun(config, state, createOutcome.aborted())
2130
+ };
2131
+ }
2132
+ for (const message of userMessages) {
2133
+ config.context.messages.push(message);
2134
+ }
2135
+ return { type: "continued" };
2136
+ };
2033
2137
  while (currentPhaseId) {
2034
2138
  const availablePhases = [];
2035
2139
  for (const [, phase2] of registry.phases) {
@@ -2043,6 +2147,19 @@ async function runPhaseLoop(config, state, registry) {
2043
2147
  previousPhaseInputMsgId = void 0;
2044
2148
  return completeRun(config, state, createOutcome.aborted());
2045
2149
  }
2150
+ if (state.attempt >= maxAttempts) {
2151
+ const input = await awaitInput(
2152
+ `The autonomous Phase execution limit (${maxAttempts}) was reached. Provide input to continue.`,
2153
+ false
2154
+ );
2155
+ if (input?.type === "completed") return input.result;
2156
+ if (input?.type === "continued") {
2157
+ isContinuing = false;
2158
+ continue;
2159
+ }
2160
+ throw new Error("Phase execution reached maxAttempts but waitForInput is unavailable.");
2161
+ }
2162
+ state.attempt++;
2046
2163
  state.metrics.iterations++;
2047
2164
  if (needsCompaction(config.context.messages)) {
2048
2165
  const compacted = compactMessages(config.context.messages);
@@ -2143,13 +2260,9 @@ async function runPhaseLoop(config, state, registry) {
2143
2260
  }
2144
2261
  const runtime = { phase, config, state, execution, messageManager, registry, context: phaseContext };
2145
2262
  let output = await executePhase(runtime);
2146
- let routeDecision;
2147
- if (output.toolCalls && output.toolCalls.length > 0) {
2148
- routeDecision = extractRouteCall(output.toolCalls);
2149
- if (routeDecision) {
2150
- applyFirstDecision(routeDecision, output);
2151
- }
2152
- }
2263
+ let modelRouteDecision = resolveModelRouteDecision(output.toolCalls, registry);
2264
+ let routeDecision = modelRouteDecision;
2265
+ if (routeDecision) applyFirstDecision(routeDecision, output);
2153
2266
  if (config.afterPhase) {
2154
2267
  const extAfter = await config.afterPhase(currentPhaseId, output);
2155
2268
  if (extAfter.abort) {
@@ -2161,52 +2274,30 @@ async function runPhaseLoop(config, state, registry) {
2161
2274
  }
2162
2275
  if (extAfter.retry && (phase.run || phase.factory)) {
2163
2276
  output = await executePhase(runtime);
2164
- routeDecision = output.toolCalls ? extractRouteCall(output.toolCalls) : void 0;
2277
+ modelRouteDecision = resolveModelRouteDecision(output.toolCalls, registry);
2278
+ routeDecision = modelRouteDecision;
2165
2279
  if (routeDecision) applyFirstDecision(routeDecision, output);
2166
2280
  }
2167
2281
  if (extAfter.output) {
2282
+ const hookHasRoute = extAfter.output.route !== void 0;
2168
2283
  output = extAfter.output;
2284
+ if (hookHasRoute) {
2285
+ routeDecision = resolveHookRouteDecision(output, registry);
2286
+ } else if (phase.run || phase.factory) {
2287
+ output = { ...output, route: "stop" };
2288
+ routeDecision = void 0;
2289
+ } else {
2290
+ routeDecision = modelRouteDecision;
2291
+ if (routeDecision) applyFirstDecision(routeDecision, output);
2292
+ }
2169
2293
  }
2170
2294
  }
2171
- const routeToolAvailable = phaseContext.tools.some((tool) => tool.name === PhaseRouteTool);
2172
- const routeRequired = !phase.target && !phase.run && !phase.factory && routeToolAvailable && registry.phases.size > 1;
2173
- if (routeRequired && !routeDecision) {
2174
- if (config.waitForInput) {
2175
- const preAbort = LoopGuard.checkAbort(config.signal);
2176
- if (preAbort.stopReason !== "none") {
2177
- removePhaseMessage(config.context.messages, previousPhaseMsgId);
2178
- removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2179
- previousPhaseMsgId = void 0;
2180
- previousPhaseInputMsgId = void 0;
2181
- return completeRun(config, state, createOutcome.aborted());
2182
- }
2183
- const requestedAt = createTimestamp();
2184
- const inputRequest = {
2185
- phase: currentPhaseId,
2186
- prompt: output.message,
2187
- requestedAt
2188
- };
2189
- state.status = "suspended";
2190
- state.continuation = {
2191
- isContinuing,
2192
- previousPayload,
2193
- previousResults: previousResults.map((result) => ({ ...result })),
2194
- pendingInstruction,
2195
- previousPhaseMessageId: previousPhaseMsgId
2196
- };
2197
- const userMessages = await config.waitForInput(state, inputRequest);
2198
- state.status = "running";
2199
- const abortResult2 = LoopGuard.checkAbort(config.signal);
2200
- if (abortResult2.stopReason !== "none") {
2201
- removePhaseMessage(config.context.messages, previousPhaseMsgId);
2202
- removePhaseMessage(config.context.messages, previousPhaseInputMsgId);
2203
- previousPhaseMsgId = void 0;
2204
- previousPhaseInputMsgId = void 0;
2205
- return completeRun(config, state, createOutcome.aborted());
2206
- }
2207
- for (const message of userMessages) {
2208
- config.context.messages.push(message);
2209
- }
2295
+ if (phase.run || phase.factory) {
2296
+ if (output.route === void 0 && !routeDecision) output = { ...output, route: "stop" };
2297
+ } else if (!phase.target && output.route === void 0 && !routeDecision) {
2298
+ const input = await awaitInput(output.message, true);
2299
+ if (input?.type === "completed") return input.result;
2300
+ if (input?.type === "continued") {
2210
2301
  isContinuing = true;
2211
2302
  continue;
2212
2303
  }
@@ -2220,7 +2311,7 @@ async function runPhaseLoop(config, state, registry) {
2220
2311
  isContinuing = true;
2221
2312
  continue;
2222
2313
  }
2223
- if (routeDecision && routeDecision.decision.length > 1) {
2314
+ if (routeDecision && routeDecision.requestedCount > 1) {
2224
2315
  const contextSnapshot = snapshotMessages(config.context.messages);
2225
2316
  const parallelTasks = /* @__PURE__ */ new Map();
2226
2317
  const instanceIds = buildInstanceIds(routeDecision.decision.map((t) => t.phase));
@@ -2239,6 +2330,7 @@ async function runPhaseLoop(config, state, registry) {
2239
2330
  registry,
2240
2331
  pt,
2241
2332
  payload,
2333
+ routeDecision.instruction,
2242
2334
  context,
2243
2335
  availablePhases,
2244
2336
  instanceId,
@@ -2251,7 +2343,7 @@ async function runPhaseLoop(config, state, registry) {
2251
2343
  }
2252
2344
  const successfulResults = await waitForBackgroundTasks(parallelTasks);
2253
2345
  previousResults = successfulResults.map((r) => ({ name: r.instanceId, output: r.payload }));
2254
- pendingInstruction = routeDecision.instruction;
2346
+ pendingInstruction = void 0;
2255
2347
  const entryPhaseId = phase.target ?? registry.entryPhaseId;
2256
2348
  if (entryPhaseId === "stop") {
2257
2349
  removePhaseMessage(config.context.messages, previousPhaseMsgId);
@@ -2294,6 +2386,7 @@ async function runPhaseLoop(config, state, registry) {
2294
2386
  });
2295
2387
  previousPayload = output.payload;
2296
2388
  previousResults = output.payload !== void 0 ? [{ name: phase.name, output: output.payload }] : [];
2389
+ pendingInstruction = phase.target ? void 0 : routeDecision?.instruction;
2297
2390
  currentPhaseId = targetPhaseId;
2298
2391
  }
2299
2392
  throw new Error("Phase machine exited without a stop or abort transition.");
@@ -2548,10 +2641,10 @@ function createPhaseExecution(config, state, phase, messageManager, toolExecutio
2548
2641
  }
2549
2642
  };
2550
2643
  }
2551
- async function executeParallelPhase(config, state, registry, phase, payload, context, availablePhases, instanceId, groupId, index, count, sourcePhaseId) {
2644
+ async function executeParallelPhase(config, state, registry, phase, payload, instruction, context, availablePhases, instanceId, groupId, index, count, sourcePhaseId) {
2552
2645
  const messages = [...context];
2553
2646
  const allTools = buildToolsWithRouting(config, availablePhases);
2554
- const phaseTools = selectPhaseTools(allTools, phase.tools);
2647
+ const phaseTools = selectPhaseTools(allTools, phase.tools, false);
2555
2648
  const phaseSkills = mergeSkills(config.context.skills, phase.skills);
2556
2649
  const systemPrompt = config.context.systemPrompt;
2557
2650
  const phaseContext = {
@@ -2578,7 +2671,10 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2578
2671
  const messageManager = createMessageManager({ messages }, config.onMessage, config.onMessageDelta);
2579
2672
  const phaseMsgId = injectPhaseContent(
2580
2673
  phase,
2581
- { results: payload !== void 0 ? [{ name: sourcePhaseId, output: payload }] : [] },
2674
+ {
2675
+ instruction,
2676
+ results: payload !== void 0 ? [{ name: sourcePhaseId, output: payload }] : []
2677
+ },
2582
2678
  messages
2583
2679
  );
2584
2680
  const toolExecutionManager = createToolExecutionManager();
@@ -2590,8 +2686,8 @@ async function executeParallelPhase(config, state, registry, phase, payload, con
2590
2686
  const resultPayload = decision?.decision[0]?.payload !== void 0 ? normalizePayload(decision.decision[0].payload) : output.payload;
2591
2687
  return { instanceId, phaseId: phase.name, payload: resultPayload, content: output.message };
2592
2688
  }
2593
- function selectPhaseTools(candidates, names) {
2594
- const routing = candidates.filter(({ name }) => name === PhaseRouteTool);
2689
+ function selectPhaseTools(candidates, names, includeRouting = true) {
2690
+ const routing = includeRouting ? candidates.filter(({ name }) => name === PhaseRouteTool) : [];
2595
2691
  const ordinary = candidates.filter(({ name }) => name !== PhaseRouteTool);
2596
2692
  return [...selectNamedResources(ordinary, names, "Tool"), ...routing];
2597
2693
  }
@@ -3040,6 +3136,7 @@ function validateConfigResolution(agentId, value) {
3040
3136
  throw new Error(`Config Provider returned an invalid resolution for Agent ${agentId}.`);
3041
3137
  }
3042
3138
  function snapshotConfig(config) {
3139
+ validateMaxAttempts(config.maxAttempts);
3043
3140
  if (isAgentConfiguration(config)) return snapshotConfiguration(config);
3044
3141
  const definition = Object.freeze({
3045
3142
  ...config.definition,
@@ -3913,11 +4010,12 @@ function applyProviderRegistration(config) {
3913
4010
  baseUrl: config.baseUrl,
3914
4011
  apiKey: config.apiKey,
3915
4012
  reasoning: modelConfig.reasoning,
4013
+ ...modelConfig.thinkingLevel !== void 0 ? { thinkingLevel: modelConfig.thinkingLevel } : {},
3916
4014
  input: modelConfig.input,
3917
4015
  cost: modelConfig.cost,
3918
4016
  contextWindow: modelConfig.contextWindow,
3919
4017
  maxTokens: modelConfig.maxTokens,
3920
- ...config.headers ? { headers: config.headers } : {},
4018
+ ...config.headers || modelConfig.headers ? { headers: { ...config.headers, ...modelConfig.headers } } : {},
3921
4019
  ...config.timeoutMs !== void 0 ? { timeoutMs: config.timeoutMs } : {},
3922
4020
  ...config.maxRetries !== void 0 ? { maxRetries: config.maxRetries } : {},
3923
4021
  ...config.retryDelayMs !== void 0 ? { retryDelayMs: config.retryDelayMs } : {}
@@ -5000,6 +5098,7 @@ function adaptExtensionTool2(input) {
5000
5098
 
5001
5099
  // src/runtime/configuration-snapshot.ts
5002
5100
  function resolveConfigurationSnapshot(registry, input) {
5101
+ validateMaxAttempts(input.maxAttempts);
5003
5102
  const resolved = registry.resolveView(input.resourceView);
5004
5103
  const base = resolved.agents.find(({ name }) => name === input.definition.name);
5005
5104
  if (!base) {
@@ -5785,6 +5884,11 @@ function normalizePhaseRegistry(registry) {
5785
5884
  if (registry?.phases.has(DEFAULT_PHASE_ID) && registry.phases.get(DEFAULT_PHASE_ID)?.filePath !== "") {
5786
5885
  throw new TypeError(`Configured Phase collides with Rowan built-in Phase "${DEFAULT_PHASE_ID}".`);
5787
5886
  }
5887
+ for (const [name] of registry?.phases ?? []) {
5888
+ if (name === "stop" || name === "continue") {
5889
+ throw new TypeError(`Configured Phase name "${name}" is reserved by Rowan routing controls.`);
5890
+ }
5891
+ }
5788
5892
  return {
5789
5893
  phases: new Map([
5790
5894
  [DEFAULT_PHASE_ID, createDefaultPhase()],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowan-agent/agent",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
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.4",
26
+ "@rowan-agent/models": "0.6.5",
27
27
  "jiti": "2.7.0",
28
28
  "typebox": "^1.0.0",
29
29
  "yaml": "^2.7.0"