@springbrand/agent-runtime 0.2.0-alpha.41 → 0.2.0-alpha.43

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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/adapter/cloudflare/resources/r2-skill-source.ts +215 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +1 -18
  4. package/src/adapter/cloudflare/subagent/tools.ts +2 -5
  5. package/src/adapter/cloudflare/universal-agent/preparation.ts +21 -9
  6. package/src/adapter/cloudflare/universal-agent/tools.ts +3 -2
  7. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +15 -0
  8. package/src/index.ts +3 -0
  9. package/src/kernel/bindings.ts +38 -6
  10. package/src/layers/context/budget/gate.ts +3 -3
  11. package/src/layers/orchestration/temporary-agent/workspace.ts +2 -0
  12. package/src/lib/prompt.ts +30 -14
  13. package/src/pi/assembly/snapshot.ts +7 -1
  14. package/src/pi/runtime-adapter/assembly.ts +8 -3
  15. package/src/pi/runtime-adapter/execution.ts +108 -13
  16. package/src/pi/runtime-adapter/models.ts +9 -6
  17. package/src/pi/runtime-adapter/openrouter-messages.ts +10 -3
  18. package/src/pi/tool/base.ts +42 -14
  19. package/src/pi/tool/compiler.ts +15 -2
  20. package/src/pi/tool/core-host.ts +228 -1
  21. package/src/pi/tool/core.ts +37 -5
  22. package/src/pi/tool/declared.ts +3 -0
  23. package/src/pi/tool/nested-tools.ts +5 -1
  24. package/src/pi/tool/schedule.ts +12 -10
  25. package/src/pi/tool/skill.ts +240 -86
  26. package/src/pi/tool/subagent.ts +2 -0
  27. package/src/pi/tool/time.ts +1 -1
  28. package/src/pi/tool/web-fetch.ts +1 -1
  29. package/src/pi/tool/web-search/web-search.ts +2 -1
  30. package/src/pi/tool/workspace-revision.ts +2 -1
  31. package/src/pi/tool/workspace-sandbox.ts +10 -21
  32. package/src/runtime-agent.ts +3 -0
  33. package/src/runtime-assembler.ts +105 -7
  34. package/src/runtime-definition.ts +1 -0
  35. package/src/runtime.ts +50 -0
@@ -10,7 +10,7 @@ import type {
10
10
  import type { RuntimeProfile } from "../../kernel/profile";
11
11
  import type { ExecutionLevel } from "../../lib/execution-level";
12
12
  import type { RuntimeSnapshot } from "../../runtime-assembler";
13
- import type { PiRuntimeAssembly } from "../assembly";
13
+ import type { FinalizedPiToolSurface, PiRuntimeAssembly } from "../assembly";
14
14
  import {
15
15
  assemblePiSystemContext,
16
16
  assemblePiExtensions,
@@ -77,7 +77,7 @@ interface PreparedPiAssembly {
77
77
  readonly loaded: readonly PiLoadedExtension[];
78
78
  readonly context: readonly PiExtensionContextContribution[];
79
79
  readonly degradations: PiExtensionAssembly["degradations"];
80
- readonly toolSurface: readonly PiToolCandidate[];
80
+ readonly toolSurface: FinalizedPiToolSurface;
81
81
  }
82
82
 
83
83
  /**
@@ -193,6 +193,7 @@ export interface PreparedPiRuntimeState {
193
193
  readonly snapshot: RuntimeSnapshot;
194
194
  readonly assembly: PreparedPiAssembly;
195
195
  readonly candidates: readonly PiToolCandidate[];
196
+ readonly codeExecutionCandidates: readonly PiToolCandidate[];
196
197
  }
197
198
 
198
199
  // #endregion
@@ -208,6 +209,7 @@ const IDEMPOTENT_TOOL_NAMES = new Set([
208
209
  "bind_resource",
209
210
  "delete",
210
211
  "edit",
212
+ "execute",
211
213
  "find",
212
214
  "get_time",
213
215
  "grep",
@@ -446,7 +448,7 @@ export async function preparePiRuntime(
446
448
  owner: object,
447
449
  ): Promise<PreparedPiRuntime> {
448
450
  const assembly = await preparePiAssembly(options);
449
- const candidates = assembly.toolSurface;
451
+ const candidates = assembly.toolSurface.candidates;
450
452
  return Object.freeze(new PreparedRuntime(
451
453
  describeRuntime(options.snapshot, candidates, assembly.loaded),
452
454
  Object.freeze([
@@ -460,6 +462,9 @@ export async function preparePiRuntime(
460
462
  snapshot: options.snapshot,
461
463
  assembly,
462
464
  candidates: Object.freeze([...candidates]),
465
+ codeExecutionCandidates: Object.freeze([
466
+ ...assembly.toolSurface.codeExecutionCandidates,
467
+ ]),
463
468
  }),
464
469
  ));
465
470
  }
@@ -25,6 +25,7 @@ import { PiChunkEncoder } from "../message";
25
25
  import type { UIMessageChunk } from "ai";
26
26
  import { ChatStreamStalledError } from "agents/chat";
27
27
  import {
28
+ codeExecutionPiToolCandidate,
28
29
  compilePiTools,
29
30
  createPiToolGovernance,
30
31
  normalizeUpdatePlanArguments,
@@ -522,6 +523,8 @@ export interface PiTurnDurability {
522
523
  export interface CreatePreparedPiTurnOptions {
523
524
  readonly prepared: PreparedPiRuntime;
524
525
  readonly pinnedDescriptor: string;
526
+ /** 同一 Session 的所有 Submission 共用的稳定 Provider 路由身份。 */
527
+ readonly modelSessionId: string;
525
528
  readonly submission: {
526
529
  readonly id: string;
527
530
  readonly requestId: string;
@@ -542,6 +545,25 @@ export interface CreatePreparedPiTurnOptions {
542
545
  readonly durability: PiTurnDurability;
543
546
  /** Reports paired model-generation lifecycle facts to the Runtime owner. */
544
547
  readonly onGeneration?: PiGenerationLifecycleObserver;
548
+ /** Reports Code Mode child Tool starts without adding Pi recovery state. */
549
+ readonly onNestedToolStarted?: (input: Readonly<{
550
+ parentToolCallId: string;
551
+ toolCallId: string;
552
+ toolName: string;
553
+ input: unknown;
554
+ occurredAt: number;
555
+ }>) => void;
556
+ /** Reports Code Mode child Tool outcomes without adding Pi settlements. */
557
+ readonly onNestedToolFinished?: (input: Readonly<{
558
+ parentToolCallId: string;
559
+ toolCallId: string;
560
+ toolName: string;
561
+ outcome: "completed" | "failed" | "cancelled";
562
+ durationMs: number;
563
+ output?: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
564
+ error?: unknown;
565
+ occurredAt: number;
566
+ }>) => void;
545
567
  /** Per-Submission executors for tools whose metadata is fixed at assembly time. */
546
568
  readonly toolExecutors?: Readonly<Record<
547
569
  string,
@@ -665,6 +687,8 @@ export class PreparedPiTurnAdapter {
665
687
  });
666
688
  const bindCandidate = (
667
689
  candidate: PiToolCandidate,
690
+ toolCallIdPrefix?: string,
691
+ recordRecoveryAttempt = true,
668
692
  ): PiToolCandidate => ({
669
693
  ...candidate,
670
694
  tool: {
@@ -673,6 +697,9 @@ export class PreparedPiTurnAdapter {
673
697
  // 实时运行由 PiCore 调用它,恢复或审批续跑则由 retryTool 进入同一路径。
674
698
  // 顺序不能随便调整:门禁先于结果复用,审批可能生成结果,不确定的非幂等工作不能重放。
675
699
  execute: async (toolCallId, input, signal, onUpdate) => {
700
+ if (toolCallIdPrefix) {
701
+ toolCallId = `${toolCallIdPrefix}:${toolCallId}`;
702
+ }
676
703
  const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
677
704
  ? await candidate.requiredExecutionLevelForInput(input)
678
705
  : candidate.requiredExecutionLevel;
@@ -783,25 +810,93 @@ export class PreparedPiTurnAdapter {
783
810
  }
784
811
  return responded.result;
785
812
  }
786
- const retry = piToolRetryPolicy(candidate);
787
- const firstAttempt = options.durability.appendToolInput({
788
- toolCallId,
813
+ if (recordRecoveryAttempt) {
814
+ const retry = piToolRetryPolicy(candidate);
815
+ const firstAttempt = options.durability.appendToolInput({
816
+ toolCallId,
817
+ toolName: candidate.tool.name,
818
+ input,
819
+ retry,
820
+ });
821
+ if (!firstAttempt && retry === "non-idempotent") {
822
+ throw new Error(
823
+ `Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
824
+ );
825
+ }
826
+ }
827
+ const execute = options.toolExecutors?.[candidate.tool.name] ??
828
+ candidate.tool.execute;
829
+ if (recordRecoveryAttempt || !toolCallIdPrefix) {
830
+ return execute(toolCallId, input, signal, onUpdate);
831
+ }
832
+ const startedAt = Date.now();
833
+ const telemetryToolCallId = toolCallId;
834
+ options.onNestedToolStarted?.({
835
+ parentToolCallId: toolCallIdPrefix,
836
+ toolCallId: telemetryToolCallId,
789
837
  toolName: candidate.tool.name,
790
838
  input,
791
- retry,
839
+ occurredAt: startedAt,
792
840
  });
793
- if (!firstAttempt && retry === "non-idempotent") {
794
- throw new Error(
795
- `Non-idempotent Tool outcome is uncertain after recovery: ${candidate.tool.name}`,
796
- );
841
+ try {
842
+ const output = await execute(toolCallId, input, signal, onUpdate);
843
+ const occurredAt = Date.now();
844
+ options.onNestedToolFinished?.({
845
+ parentToolCallId: toolCallIdPrefix,
846
+ toolCallId: telemetryToolCallId,
847
+ toolName: candidate.tool.name,
848
+ outcome: "completed",
849
+ durationMs: occurredAt - startedAt,
850
+ output,
851
+ occurredAt,
852
+ });
853
+ return output;
854
+ } catch (error) {
855
+ const occurredAt = Date.now();
856
+ options.onNestedToolFinished?.({
857
+ parentToolCallId: toolCallIdPrefix,
858
+ toolCallId: telemetryToolCallId,
859
+ toolName: candidate.tool.name,
860
+ outcome: signal?.aborted ? "cancelled" : "failed",
861
+ durationMs: occurredAt - startedAt,
862
+ error,
863
+ occurredAt,
864
+ });
865
+ throw error;
797
866
  }
798
- const execute = options.toolExecutors?.[candidate.tool.name] ??
799
- candidate.tool.execute;
800
- return execute(toolCallId, input, signal, onUpdate);
801
867
  },
802
868
  },
803
869
  });
804
- this.candidates = state.candidates.map(bindCandidate);
870
+ this.candidates = state.candidates.map((candidate) => {
871
+ if (candidate.tool.name !== "execute") return bindCandidate(candidate);
872
+ const factory = state.snapshot.bindings.codeExecution;
873
+ if (!factory) {
874
+ throw new Error("Prepared Code Mode Tool requires a Runtime factory");
875
+ }
876
+ return bindCandidate({
877
+ ...candidate,
878
+ tool: {
879
+ ...candidate.tool,
880
+ execute: (toolCallId, input, signal, onUpdate) => {
881
+ const runtimeCandidate = codeExecutionPiToolCandidate(
882
+ factory.create(state.codeExecutionCandidates.map((inner) =>
883
+ // Code Mode owns replay of its connector calls. Pi persists
884
+ // only the parent execute attempt/result; recording an inner
885
+ // input without a Pi settlement creates an orphan recovery
886
+ // action that cannot be found on the direct Tool surface.
887
+ bindCandidate(inner, toolCallId, false)
888
+ )),
889
+ );
890
+ return runtimeCandidate.tool.execute(
891
+ toolCallId,
892
+ input,
893
+ signal,
894
+ onUpdate,
895
+ );
896
+ },
897
+ },
898
+ });
899
+ });
805
900
  this.turn = new PiTurnAdapter({
806
901
  pi: {
807
902
  model: state.snapshot.pi.model,
@@ -812,7 +907,7 @@ export class PreparedPiTurnAdapter {
812
907
  state.snapshot.bindings.provider,
813
908
  state.snapshot.pi.model.id,
814
909
  ),
815
- modelSessionId: options.submission.requestId,
910
+ modelSessionId: options.modelSessionId,
816
911
  ...(options.onGeneration ? { onGeneration: options.onGeneration } : {}),
817
912
  canonicalMessages: options.canonicalMessages,
818
913
  // 被中断的 Turn 不写结算。它退场路上还会吐出「工具被中止」,而工具其实
@@ -614,6 +614,7 @@ function configuredModel(
614
614
  ...endpoint.headers,
615
615
  };
616
616
  const openRouterProviderPin = endpoint.openRouterProviderPins?.[modelId];
617
+ const supportsStrictTools = modelId.startsWith("anthropic/claude-");
617
618
  return {
618
619
  ...metadata,
619
620
  api: apiFor(endpoint.protocol),
@@ -623,15 +624,17 @@ function configuredModel(
623
624
  ? {
624
625
  compat: {
625
626
  supportsEagerToolInputStreaming: false,
627
+ supportsStrictTools,
626
628
  supportsToolReferences: true,
627
- ...(openRouterProviderPin
628
- ? {
629
- openRouterRouting: {
629
+ openRouterRouting: {
630
+ require_parameters: true,
631
+ ...(openRouterProviderPin
632
+ ? {
630
633
  order: [openRouterProviderPin],
631
634
  allow_fallbacks: false,
632
- },
633
- }
634
- : {}),
635
+ }
636
+ : {}),
637
+ },
635
638
  },
636
639
  }
637
640
  : {}),
@@ -10,6 +10,8 @@ import {
10
10
 
11
11
  type Payload = Record<string, unknown>;
12
12
 
13
+ const STRUCTURED_OUTPUTS_BETA = "structured-outputs-2025-11-13";
14
+
13
15
  function record(value: unknown): Payload | undefined {
14
16
  return value !== null && typeof value === "object"
15
17
  ? value as Payload
@@ -56,9 +58,11 @@ function decoratePayload(
56
58
  return {
57
59
  ...body,
58
60
  ...(sessionId ? { session_id: sessionId } : {}),
59
- ...(model.compat?.openRouterRouting
60
- ? { provider: model.compat.openRouterRouting }
61
- : {}),
61
+ provider: {
62
+ ...record(body.provider),
63
+ ...model.compat?.openRouterRouting,
64
+ require_parameters: true,
65
+ },
62
66
  ...(deferred.length > 0
63
67
  ? {
64
68
  tools: [
@@ -90,6 +94,9 @@ function openRouterOptions(
90
94
  apiKey: undefined,
91
95
  headers: {
92
96
  ...headers,
97
+ ...(model.compat?.supportsStrictTools === true
98
+ ? { "x-anthropic-beta": STRUCTURED_OUTPUTS_BETA }
99
+ : {}),
93
100
  ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
94
101
  "x-api-key": null,
95
102
  },
@@ -31,7 +31,7 @@ const askUserQuestion = Type.Object({
31
31
  description:
32
32
  "Whether the user may add free text. Defaults to false with options and true without options.",
33
33
  })),
34
- });
34
+ }, { additionalProperties: false });
35
35
 
36
36
  const askUserParameters = Type.Object({
37
37
  questions: Type.Array(askUserQuestion, {
@@ -40,7 +40,7 @@ const askUserParameters = Type.Object({
40
40
  description:
41
41
  "All user decisions needed to continue. Ask them together in one call.",
42
42
  }),
43
- });
43
+ }, { additionalProperties: false });
44
44
 
45
45
  /**
46
46
  * `ask_user` 的客户端响应体。
@@ -52,8 +52,8 @@ const askUserResponse = Type.Object({
52
52
  answers: Type.Array(Type.Object({
53
53
  selections: Type.Array(Type.String()),
54
54
  text: Type.Optional(Type.String()),
55
- })),
56
- });
55
+ }, { additionalProperties: false })),
56
+ }, { additionalProperties: false });
57
57
 
58
58
  type AskUserInput = Static<typeof askUserParameters>;
59
59
  type AskUserResponse = Static<typeof askUserResponse>;
@@ -138,7 +138,7 @@ const suggestFollowupsParameters = Type.Object({
138
138
  maxItems: 4,
139
139
  description: "2-4 concrete, distinct follow-up directions.",
140
140
  }),
141
- });
141
+ }, { additionalProperties: false });
142
142
 
143
143
  const updatePlanParameters = Type.Object({
144
144
  steps: Type.Array(Type.Object({
@@ -153,11 +153,11 @@ const updatePlanParameters = Type.Object({
153
153
  description:
154
154
  'Current status. Use "done" for a finished step; never use "completed".',
155
155
  }),
156
- }), {
156
+ }, { additionalProperties: false }), {
157
157
  description:
158
158
  "The complete, ordered plan. Overwrites any previously reported plan.",
159
159
  }),
160
- });
160
+ }, { additionalProperties: false });
161
161
 
162
162
  type UpdatePlanArguments = Static<typeof updatePlanParameters>;
163
163
 
@@ -183,13 +183,32 @@ export function normalizeUpdatePlanArguments(
183
183
  return input as UpdatePlanArguments;
184
184
  }
185
185
  const value = input as Record<string, unknown>;
186
- if (!Array.isArray(value.steps)) return input as UpdatePlanArguments;
186
+ let steps = value.steps;
187
+ if (typeof steps === "string") {
188
+ try {
189
+ steps = JSON.parse(steps);
190
+ } catch {
191
+ return input as UpdatePlanArguments;
192
+ }
193
+ }
194
+ if (!Array.isArray(steps)) return input as UpdatePlanArguments;
187
195
  return {
188
196
  ...value,
189
- steps: value.steps.map((step) => {
197
+ steps: steps.map((step) => {
190
198
  if (step === null || typeof step !== "object") return step;
191
199
  const s = step as Record<string, unknown>;
192
- return { ...s, status: normalizeStepStatus(s.status) };
200
+ const { step: stepAlias, title: titleAlias, ...rest } = s;
201
+ const alias = typeof stepAlias === "string" ? stepAlias : titleAlias;
202
+ const text = typeof rest.text === "string"
203
+ ? rest.text
204
+ : typeof alias === "string"
205
+ ? alias
206
+ : undefined;
207
+ return {
208
+ ...rest,
209
+ ...(text === undefined ? {} : { text }),
210
+ status: normalizeStepStatus(s.status),
211
+ };
193
212
  }),
194
213
  } as UpdatePlanArguments;
195
214
  }
@@ -209,7 +228,7 @@ const setContextParameters = Type.Object({
209
228
  ], {
210
229
  description: 'Whether to replace the block or append to it. Defaults to "replace".',
211
230
  })),
212
- });
231
+ }, { additionalProperties: false });
213
232
 
214
233
  function result<T>(details: T): AgentToolResult<T> {
215
234
  return {
@@ -223,7 +242,10 @@ function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
223
242
  owner: "runtime-base",
224
243
  requiredExecutionLevel: "safe",
225
244
  source: "action",
226
- tool,
245
+ tool: {
246
+ ...tool,
247
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
248
+ } as AgentTool<T>,
227
249
  };
228
250
  }
229
251
 
@@ -278,7 +300,8 @@ export function basePiToolCandidates(
278
300
  },
279
301
  },
280
302
  },
281
- candidate({
303
+ {
304
+ ...candidate({
282
305
  name: "suggest_followups",
283
306
  label: "Suggest follow-ups",
284
307
  description:
@@ -288,7 +311,10 @@ export function basePiToolCandidates(
288
311
  return result({ noted: true, count: input.items.length });
289
312
  },
290
313
  }),
291
- candidate({
314
+ exposureMode: "direct",
315
+ },
316
+ {
317
+ ...candidate({
292
318
  name: "update_plan",
293
319
  label: "Update plan",
294
320
  description:
@@ -303,6 +329,8 @@ export function basePiToolCandidates(
303
329
  });
304
330
  },
305
331
  }),
332
+ exposureMode: "direct",
333
+ },
306
334
  ...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
307
335
  ];
308
336
  }
@@ -43,11 +43,15 @@ export interface PiToolInteractionSpec {
43
43
  }
44
44
 
45
45
  /** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
46
+ export type ToolExposureMode = "direct" | "codemode" | "both";
47
+
46
48
  export interface PiToolCandidate {
47
49
  readonly owner: string;
48
50
  readonly tool: AgentTool<any, any>;
49
51
  /** @internal Send the complete schema but hide it until provider Tool Search finds it. */
50
52
  readonly deferLoading?: true;
53
+ /** Omitted means visible both directly and through Code Mode. */
54
+ readonly exposureMode?: ToolExposureMode;
51
55
  /** Conservative maximum used in the stable Runtime descriptor. */
52
56
  readonly requiredExecutionLevel: ExecutionLevel;
53
57
  /** Trusted parameter-level policy, evaluated before approval or dispatch. */
@@ -306,12 +310,21 @@ function governedTool(
306
310
  candidate: PiToolCandidate,
307
311
  options: CompilePiToolsOptions,
308
312
  ): AgentTool<any, any> {
309
- const execute = candidate.tool.execute;
313
+ const tool = candidate.tool.constrainedSampling === false
314
+ ? candidate.tool
315
+ : {
316
+ ...candidate.tool,
317
+ constrainedSampling: candidate.tool.constrainedSampling ?? {
318
+ type: "json_schema" as const,
319
+ strict: "prefer" as const,
320
+ },
321
+ };
322
+ const execute = tool.execute;
310
323
  const state = options.governance?.[governanceState];
311
324
  const settle = options.settle;
312
325
 
313
326
  return {
314
- ...candidate.tool,
327
+ ...tool,
315
328
  ...(candidate.deferLoading ? { deferLoading: true as const } : {}),
316
329
  // 执行一次完整的受治理 Pi 工具调用。
317
330
  // Pi 工具循环选中编译后的工具时调用,调用方应传入稳定 toolCall id 供结算去重。