@springbrand/agent-runtime 0.2.0-alpha.32 → 0.2.0-alpha.34

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.32",
3
+ "version": "0.2.0-alpha.34",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -12,7 +12,7 @@ import type {
12
12
  PiToolInteraction,
13
13
  PiToolInteractionCancelReason,
14
14
  } from "../pi/runtime-adapter";
15
- import type { ToolResultMessage } from "@earendil-works/pi-ai";
15
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
16
16
 
17
17
  // #region 类型约定
18
18
 
@@ -43,10 +43,10 @@ export interface InteractionRecord extends PiToolInteraction {
43
43
  * `content` / `details` 由 Tool 自己的 `settle` 映射产出 —— 本流程不解释响应体语义。
44
44
  * 取消不带载荷,由纯函数层用固定文案兜底。
45
45
  */
46
- export interface InteractionSettlementPayload {
47
- readonly content: ToolResultMessage["content"];
48
- readonly details: unknown;
49
- }
46
+ export type InteractionSettlementPayload = Omit<
47
+ AgentToolResult<unknown>,
48
+ "terminate"
49
+ >;
50
50
 
51
51
  // 宿主用这些依赖把 interaction 状态机接入数据库、Pi 恢复计算和 Turn 续跑。
52
52
  interface InteractionLifecycleOptions<
@@ -31,6 +31,7 @@ export interface ToolSpec extends Partial<
31
31
  readonly requiredExecutionLevel: ExecutionLevel;
32
32
  /** Require a fresh human decision for every call, regardless of execution level. */
33
33
  readonly alwaysRequiresApproval?: boolean;
34
+ /** Preserve adapted Pi result envelopes; only details-only adapters may unwrap them. */
34
35
  readonly execute: (
35
36
  input: unknown,
36
37
  ctx: ToolContext,
@@ -15,6 +15,8 @@ import {
15
15
  type PiToolRecoveryState,
16
16
  } from "../turn";
17
17
  import type { ToolResultMessage } from "@earendil-works/pi-ai";
18
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
19
+ import { isEqual } from "lodash-es";
18
20
  import {
19
21
  aiSdkRecoveryCodec,
20
22
  shouldCreditStreamProgress,
@@ -35,10 +37,7 @@ export interface PiRecoveredToolSettlement {
35
37
  readonly toolCallId: string;
36
38
  readonly toolName: string;
37
39
  readonly args: unknown;
38
- readonly result: {
39
- readonly content: ToolResultMessage["content"];
40
- readonly details: unknown;
41
- };
40
+ readonly result: Omit<AgentToolResult<unknown>, "terminate">;
42
41
  readonly isError: boolean;
43
42
  readonly createdAt: number;
44
43
  }
@@ -75,10 +74,7 @@ export type PiRecoveryCommand =
75
74
  | {
76
75
  readonly kind: "respond";
77
76
  readonly response: unknown;
78
- readonly result: {
79
- readonly content: ToolResultMessage["content"];
80
- readonly details: unknown;
81
- };
77
+ readonly result: Omit<AgentToolResult<unknown>, "terminate">;
82
78
  }
83
79
  | {
84
80
  readonly kind: "cancel";
@@ -89,10 +85,7 @@ export type PiRecoveryCommand =
89
85
  readonly kind: "record-tool-result";
90
86
  readonly toolCallId: string;
91
87
  readonly toolName: string;
92
- readonly result: {
93
- readonly content: ToolResultMessage["content"];
94
- readonly details: unknown;
95
- };
88
+ readonly result: AgentToolResult<unknown>;
96
89
  readonly isError: boolean;
97
90
  readonly timestamp: number;
98
91
  readonly needsContinuation?: boolean;
@@ -226,6 +219,10 @@ function recoveredToolSettlements(
226
219
  result: {
227
220
  content: toolResult.content,
228
221
  details: toolResult.details,
222
+ ...(toolResult.usage ? { usage: toolResult.usage } : {}),
223
+ ...(toolResult.addedToolNames?.length
224
+ ? { addedToolNames: toolResult.addedToolNames }
225
+ : {}),
229
226
  },
230
227
  isError: toolResult.isError,
231
228
  createdAt: toolResult.timestamp,
@@ -248,21 +245,17 @@ function approvalExecutionId(continuationKey: string): string | null {
248
245
  // 记录必须已经存在 —— 拿不到 toolCallId/toolName 就无法把结果绑回原调用,这时失败关闭而不是编一个 id。
249
246
  function interactionToolResult(
250
247
  interaction: PiToolInteraction | undefined,
251
- result: {
252
- readonly content: ToolResultMessage["content"];
253
- readonly details: unknown;
254
- },
248
+ result: Omit<AgentToolResult<unknown>, "terminate">,
255
249
  timestamp: number,
256
250
  ): ToolResultMessage {
257
251
  if (!interaction) {
258
252
  throw new Error("SpringBrand Tool interaction is missing for its response");
259
253
  }
260
254
  return {
255
+ ...result,
261
256
  role: "toolResult",
262
257
  toolCallId: interaction.toolCallId,
263
258
  toolName: interaction.toolName,
264
- content: result.content,
265
- details: result.details,
266
259
  isError: false,
267
260
  timestamp,
268
261
  };
@@ -448,9 +441,9 @@ export function decidePiRecovery(
448
441
  const existing = state.toolCalls[record.toolCallId];
449
442
  if (existing) {
450
443
  if (
451
- existing.toolName !== record.toolName ||
452
- existing.retry !== record.retry ||
453
- JSON.stringify(existing.input) !== JSON.stringify(record.input)
444
+ existing.toolName !== record.toolName ||
445
+ existing.retry !== record.retry ||
446
+ !isEqual(existing.input, record.input)
454
447
  ) {
455
448
  throw new Error(
456
449
  `Conflicting SpringBrand Tool input for ${record.toolCallId}`,
@@ -614,12 +607,16 @@ export function decidePiRecovery(
614
607
  toolName: command.toolName,
615
608
  content: command.result.content,
616
609
  details: command.result.details,
610
+ ...(command.result.usage ? { usage: command.result.usage } : {}),
611
+ ...(command.result.addedToolNames?.length
612
+ ? { addedToolNames: command.result.addedToolNames }
613
+ : {}),
617
614
  isError: command.isError,
618
615
  timestamp: command.timestamp,
619
616
  };
620
617
  const existing = state.toolCalls[command.toolCallId]?.result;
621
618
  if (existing) {
622
- if (JSON.stringify(existing) !== JSON.stringify(toolResult)) {
619
+ if (!isEqual(existing, toolResult)) {
623
620
  throw new Error(
624
621
  `Conflicting SpringBrand Tool result for ${command.toolCallId}`,
625
622
  );
@@ -346,11 +346,10 @@ export class PiRuntimeTranscript {
346
346
  return this.append(
347
347
  `${submissionId}:tool:${settlement.toolCallId}`,
348
348
  {
349
+ ...settlement.result,
349
350
  role: "toolResult",
350
351
  toolCallId: settlement.toolCallId,
351
352
  toolName: settlement.toolName,
352
- content: settlement.result.content,
353
- details: settlement.result.details,
354
353
  isError: settlement.isError,
355
354
  timestamp: settlement.createdAt,
356
355
  },
@@ -39,7 +39,7 @@ export interface PiToolInteractionSpec {
39
39
  readonly settle?: (
40
40
  input: unknown,
41
41
  response: unknown,
42
- ) => AgentToolResult<unknown>;
42
+ ) => Omit<AgentToolResult<unknown>, "terminate">;
43
43
  }
44
44
 
45
45
  /** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
@@ -46,10 +46,18 @@ function codeExecutionTools(tools: ToolRegistry): ToolSet {
46
46
  aiTool({
47
47
  description: spec.description,
48
48
  inputSchema: jsonSchema(spec.parameters as never),
49
- execute: (input, options) => spec.execute(input, {
50
- toolCallId: options?.toolCallId ?? name,
51
- signal: options?.abortSignal ?? new AbortController().signal,
52
- }),
49
+ execute: async (input, options) => {
50
+ const result = await spec.execute(input, {
51
+ toolCallId: options?.toolCallId ?? name,
52
+ signal: options?.abortSignal ?? new AbortController().signal,
53
+ });
54
+ return result != null &&
55
+ typeof result === "object" &&
56
+ "content" in result &&
57
+ Array.isArray((result as { content?: unknown }).content)
58
+ ? (result as { details?: unknown }).details ?? result
59
+ : result;
60
+ },
53
61
  }),
54
62
  ]));
55
63
  }
@@ -1,4 +1,5 @@
1
1
  import type { ToolResultMessage } from "@earendil-works/pi-ai";
2
+ import { isEqual } from "lodash-es";
2
3
  import { EXECUTION_LEVELS } from "../../lib/execution-level";
3
4
  import {
4
5
  decidePiToolApproval,
@@ -444,11 +445,10 @@ function sameToolInput(
444
445
  { type: "tool-input" }
445
446
  >,
446
447
  ): boolean {
447
- // 待确认:JSON.stringify 对对象键顺序敏感;当前写入链会保留输入顺序,重建输入的调用方可能产生语义相同但顺序不同的对象。
448
448
  return (
449
449
  previous.toolName === milestone.toolName &&
450
450
  previous.retry === milestone.retry &&
451
- JSON.stringify(previous.input) === JSON.stringify(milestone.input)
451
+ isEqual(previous.input, milestone.input)
452
452
  );
453
453
  }
454
454
 
package/src/runtime.ts CHANGED
@@ -4086,10 +4086,7 @@ export abstract class AgentRuntimeKernel<
4086
4086
  const settled = spec.settle
4087
4087
  ? spec.settle(input, response)
4088
4088
  : defaultInteractionResult(response);
4089
- return this.interactions.respond(pending.interactionId, response, {
4090
- content: settled.content,
4091
- details: settled.details,
4092
- });
4089
+ return this.interactions.respond(pending.interactionId, response, settled);
4093
4090
  }
4094
4091
 
4095
4092
  /**
@@ -85,13 +85,12 @@ export function piCandidateToToolSpec(candidate: PiToolCandidate): ToolSpec {
85
85
  name,
86
86
  arguments: input as Record<string, unknown>,
87
87
  });
88
- const result = await execute(
88
+ return execute(
89
89
  ctx.toolCallId,
90
90
  validated,
91
91
  ctx.signal,
92
92
  undefined,
93
93
  );
94
- return result.details ?? result;
95
94
  },
96
95
  };
97
96
  }