@springbrand/agent-runtime 0.2.0-alpha.27 → 0.2.0-alpha.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/agent-runtime",
3
- "version": "0.2.0-alpha.27",
3
+ "version": "0.2.0-alpha.28",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -210,6 +210,41 @@ function projectToolResultsForModel(
210
210
  return changed ? projected : [...messages];
211
211
  }
212
212
 
213
+ function orderLateToolResultsBeforeTurnAbortMarker(
214
+ messages: readonly AgentMessage[],
215
+ ): AgentMessage[] {
216
+ const ordered: AgentMessage[] = [];
217
+ for (let index = 0; index < messages.length; index += 1) {
218
+ const message = messages[index]!;
219
+ const previous = ordered.at(-1);
220
+ if (
221
+ message.role !== "custom" ||
222
+ message.customType !== "turn-aborted" ||
223
+ previous?.role !== "assistant"
224
+ ) {
225
+ ordered.push(message);
226
+ continue;
227
+ }
228
+ const pending = new Set(
229
+ previous.content.flatMap((part) =>
230
+ part.type === "toolCall" ? [part.id] : []
231
+ ),
232
+ );
233
+ let cursor = index + 1;
234
+ while (true) {
235
+ const result = messages[cursor];
236
+ if (result?.role !== "toolResult" || !pending.has(result.toolCallId)) {
237
+ break;
238
+ }
239
+ ordered.push(result);
240
+ cursor += 1;
241
+ }
242
+ ordered.push(message);
243
+ index = cursor - 1;
244
+ }
245
+ return ordered;
246
+ }
247
+
213
248
  class PiTurnAdapter {
214
249
  private piInstance?: PiCore;
215
250
  private abortRequested = false;
@@ -304,7 +339,9 @@ class PiTurnAdapter {
304
339
  transformContext: async (messages, signal) => {
305
340
  const ctx = await this.opts.transformContext(messages, signal);
306
341
  return transformMessages(
307
- projectToolResultsForModel(ctx, selfBounded) as Message[],
342
+ orderLateToolResultsBeforeTurnAbortMarker(
343
+ projectToolResultsForModel(ctx, selfBounded),
344
+ ) as Message[],
308
345
  this.opts.pi.model,
309
346
  ) as AgentMessage[];
310
347
  },
package/src/runtime.ts CHANGED
@@ -2560,6 +2560,36 @@ export abstract class AgentRuntimeKernel<
2560
2560
  return { kind: "settled" };
2561
2561
  case "resume-turn": {
2562
2562
  const last = (await this.transcript.canonicalMessages()).at(-1);
2563
+ if (last?.role === "assistant" && last.stopReason === "toolUse") {
2564
+ // Tool input 会在真正执行前持久化。恢复状态仍为 idle 证明这些
2565
+ // canonical Tool Call 还没越过 attempt 边界,包括非幂等 Tool 也可以安全首次执行。
2566
+ const unstartedCalls = last.content.filter((part) =>
2567
+ part.type === "toolCall"
2568
+ );
2569
+ if (unstartedCalls.length > 0) {
2570
+ for (const call of unstartedCalls) {
2571
+ try {
2572
+ if (
2573
+ !await adapter.retryTool({
2574
+ toolName: call.name,
2575
+ toolCallId: call.id,
2576
+ input: call.arguments,
2577
+ })
2578
+ ) {
2579
+ return {
2580
+ kind: "unresumable",
2581
+ reason:
2582
+ `SpringBrand could not resume this turn: Tool "${call.name}" is no longer available.`,
2583
+ };
2584
+ }
2585
+ } catch {
2586
+ // The governed Tool persisted its bounded error ToolResult.
2587
+ }
2588
+ }
2589
+ decision = await this.materializeRecoveredToolResults(submission);
2590
+ continue;
2591
+ }
2592
+ }
2563
2593
  // Pi 的 agentLoopContinue 拒绝从 assistant 消息继续。悬空的尾部 assistant
2564
2594
  // 意味着这条 transcript 续不下去了 —— 判断出来就必须收尾,不能默默退场。
2565
2595
  return last?.role === "user" || last?.role === "toolResult"