@springbrand/agent-runtime 0.2.0-alpha.42 → 0.2.0-alpha.44

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.42",
3
+ "version": "0.2.0-alpha.44",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -29,9 +29,6 @@ const READONLY_WORKSPACE_TOOLS = new Set([
29
29
  "grep",
30
30
  ]);
31
31
  const DENIED_TOOLS = [
32
- // SubAgents have no approval channel; keep the restored high-risk Workspace
33
- // shell on the main Agent surface, matching the pre-Pi behavior.
34
- "bash",
35
32
  "run_agent",
36
33
  "subagents",
37
34
  "fanout",
@@ -46,11 +43,11 @@ const executeParameters = Type.Object({
46
43
  description:
47
44
  "JavaScript to run. Return the final value. The sandbox provides fetch and state.*.",
48
45
  }),
49
- });
46
+ }, { additionalProperties: false });
50
47
 
51
48
  const fetchParameters = Type.Object({
52
49
  url: Type.String({ minLength: 1, maxLength: 8_192 }),
53
- });
50
+ }, { additionalProperties: false });
54
51
 
55
52
  function result(details: unknown): AgentToolResult<unknown> {
56
53
  let text: string;
@@ -55,14 +55,6 @@ const DEFAULT_MEMORY: RuntimeMemoryProfile = Object.freeze({
55
55
  });
56
56
 
57
57
  const WORKSPACE_TOOL_NAMES = [
58
- "read",
59
- "write",
60
- "edit",
61
- "list",
62
- "find",
63
- "grep",
64
- "delete",
65
- "bash",
66
58
  "execute",
67
59
  ] as const;
68
60
 
@@ -157,6 +149,7 @@ export async function prepareWorkspace<Env extends Cloudflare.Env>(
157
149
  loader: platform.loader,
158
150
  outbound: platform.outbound(),
159
151
  workspace: workspace.value,
152
+ workspaceAccessMode: "write",
160
153
  }),
161
154
  degradations: workspace.degradations,
162
155
  };
@@ -12,7 +12,6 @@ import { schedulePiToolCandidates } from "../../../pi/tool/schedule";
12
12
  import { workspaceRevisionPiToolCandidate } from "../../../pi/tool/workspace-revision";
13
13
  import {
14
14
  sandboxPiToolCandidates,
15
- workspacePiToolCandidates,
16
15
  } from "../../../pi/tool/workspace-sandbox";
17
16
  import type { RuntimeCodeExecutionFactory } from "../../../kernel/bindings";
18
17
  import type { ToolAssemblyResult } from "../../../runtime-definition";
@@ -34,7 +33,6 @@ export function assembleUniversalAgentTools(options: {
34
33
  }): ToolAssemblyResult {
35
34
  const tools = [
36
35
  ...(options.hostTools ?? []),
37
- ...(options.workspace ? workspacePiToolCandidates(options.workspace) : []),
38
36
  ...(options.workspaceRevisions
39
37
  ? [workspaceRevisionPiToolCandidate(options.workspaceRevisions)]
40
38
  : []),
@@ -2,6 +2,7 @@ import type {
2
2
  WorkspaceAdminPort,
3
3
  WorkspaceFileInfo,
4
4
  WorkspacePort,
5
+ WorkspaceStreamWriteOptions,
5
6
  WorkspaceQuota,
6
7
  WorkspaceUsage,
7
8
  } from "../../../kernel/bindings";
@@ -216,6 +217,18 @@ export class ScopedWorkspace implements WorkspacePort, WorkspaceAdminPort {
216
217
  );
217
218
  }
218
219
 
220
+ async writeFileStream(
221
+ path: string,
222
+ content: ReadableStream<Uint8Array>,
223
+ options?: WorkspaceStreamWriteOptions,
224
+ ) {
225
+ return this.parent.writeFileStream(
226
+ await this.guardedPhysical(path, true),
227
+ content,
228
+ options,
229
+ );
230
+ }
231
+
219
232
  async writeFileBytesIfUnchanged(
220
233
  path: string,
221
234
  data: Uint8Array,
@@ -363,6 +376,8 @@ export function createWorkspacePortFacade(
363
376
  workspace.writeFile(path, content, mimeType),
364
377
  writeFileBytes: (path, data, mimeType) =>
365
378
  workspace.writeFileBytes(path, data, mimeType),
379
+ writeFileStream: (path, content, options) =>
380
+ workspace.writeFileStream(path, content, options),
366
381
  appendFile: (path, content, mimeType) =>
367
382
  workspace.appendFile(path, content, mimeType),
368
383
  exists: (path) => workspace.exists(path),
package/src/index.ts CHANGED
@@ -93,6 +93,7 @@ export type {
93
93
  CompilePiToolsOptions,
94
94
  PiToolCandidate,
95
95
  SettledPiToolCall,
96
+ ToolExposureMode,
96
97
  } from "./pi/tool";
97
98
  export { basePiToolCandidates } from "./pi/tool";
98
99
  export { createPiDeclaredToolCandidate } from "./pi/tool";
@@ -92,6 +92,11 @@ export interface WorkspacePort {
92
92
  data: Uint8Array | ArrayBuffer,
93
93
  mimeType?: string,
94
94
  ): Promise<void>;
95
+ writeFileStream(
96
+ path: string,
97
+ content: ReadableStream<Uint8Array>,
98
+ options?: WorkspaceStreamWriteOptions,
99
+ ): Promise<{ path: string; bytes: number }>;
95
100
  /**
96
101
  * 把文本追加到文件末尾。
97
102
  *
@@ -205,6 +210,11 @@ export interface WorkspacePort {
205
210
  glob(pattern: string): Promise<WorkspaceFileInfo[]>;
206
211
  }
207
212
 
213
+ export interface WorkspaceStreamWriteOptions {
214
+ contentLength?: number;
215
+ mediaType?: string;
216
+ }
217
+
208
218
  /**
209
219
  * 向 Runtime 提供已组装的 Workspace Code Mode 执行能力。
210
220
  *
@@ -845,6 +855,10 @@ export interface RuntimeTurnEventsPort {
845
855
  readonly submissionId: string;
846
856
  readonly approvalExecutionId: string;
847
857
  }): Promise<void>;
858
+ /**
859
+ * 至少一次投当前 Session Activity;Host 应按 revision 幂等处理重试。
860
+ * Runtime 恢复时会略过已过时的 revision,避免重放旧 working 状态。
861
+ */
848
862
  onActivityChanged?(
849
863
  projection: RuntimeActivityProjection,
850
864
  ): Promise<void>;
@@ -173,13 +173,13 @@ export async function spillDurableToolOutput(
173
173
  // 出口指令必须是可执行的,而且必须只承诺模型真的拿得到的东西:Provider 只把
174
174
  // Tool 结果的 content 发给模型,所以这里不能引用只存在于 details 的字段。
175
175
  // 溢出产物是 JSON,一个大字符串叶子会整块挤在一行上,而按行分页追不回被行宽
176
- // 截断的内容 —— 那种情况要走 grep / bash,不能让模型以为 read 一定够用。
176
+ // 截断的内容 —— 那种情况要走 state.searchText,不能让模型以为分页读取一定够用。
177
177
  note:
178
178
  "Output was large and has been saved to the Workspace file above. " +
179
- "Read it in pages with read(path, offset, limit) — do not read it whole; " +
179
+ "Read it in pages inside execute with state.readFile — do not read it whole; " +
180
180
  "each page ends with a footer telling you the line range and the next offset. " +
181
181
  "This file is JSON, so a single large value can sit on one very long line: " +
182
- "if a page reports that lines were cut short, use grep or bash on the path instead.",
182
+ "if a page reports that lines were cut short, use state.searchText on the path instead.",
183
183
  };
184
184
  } catch {
185
185
  return null;
@@ -83,6 +83,8 @@ export function createTemporaryAgentWorkspace(
83
83
  // 原因:二进制写入必须与文本写入使用同一 Memory 禁止,不能留下第二条写入路径。
84
84
  writeFileBytes: async (path, data, mimeType) =>
85
85
  workspace.writeFileBytes(allowed(path), data, mimeType),
86
+ writeFileStream: async (path, content, options) =>
87
+ workspace.writeFileStream(allowed(path), content, options),
86
88
  // 作用:在一个允许的文件末尾追加文本。
87
89
  // 调用:临时 Agent 需要增量写入时通过 Workspace Tool 调用。
88
90
  // 原因:追加也是写操作,必须在委托前拦截 Memory 路径。
package/src/lib/prompt.ts CHANGED
@@ -55,7 +55,7 @@ export const TOOLS =
55
55
  TOOL_ROUTING +
56
56
  " Relatedness, repetition, or multiple calls never overrides this availability rule. " +
57
57
  "When every required Tool is available inside execute, use one execute for repeated or related calls, branching, or repetition. Inside execute, " +
58
- "use state.* for workspace file operations, tools.* for other host capabilities, a loop for repeated calls, and Promise.all for independent calls. " +
58
+ "use state.* for workspace file operations, tools.* for other host capabilities, and sequential calls for repeated or durable operations. " +
59
59
  "Top-level-only Tools remain Direct even when called repeatedly. " +
60
60
  "For web tasks, use web_search for web discovery, current facts, cited research, " +
61
61
  "and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
@@ -65,11 +65,7 @@ export const TOOLS =
65
65
  "persistent inputs are copied in automatically on first use, /userspace is read-only, and only explicitly " +
66
66
  "published /workspace outputs survive. Use Code Mode for computation, multi-step data work, and the " +
67
67
  "raw or customized network cases described above; every response and failure it sees is visible to you. " +
68
- "bash is a shell over the workspace filesystem only " +
69
- "(no network, no system utilities) and is approval-gated — don't reach for it to read files or fetch. " +
70
- "When execute is present, make related file changes in one execute with state.*. " +
71
- "Use read for one existing Workspace file, write to create or replace one file, and edit for one localized change. " +
72
- "Use bash only when a single shell workflow must coordinate multiple Workspace files; do not use it for a single-file read, write, or edit. " +
68
+ "Every Workspace filesystem operation goes through execute and state.*; no Workspace file methods exist under tools.*. " +
73
69
  "Do not re-plan or explain between consecutive tool calls. When a run is within the last five model turns, stop expanding scope and " +
74
70
  "prioritize verification, saving durable results, and the final response. " +
75
71
  "When related Tool calls can run independently, all are available inside execute, and execute is present, run them inside that execute rather than as parallel top-level calls; " +
@@ -104,8 +100,7 @@ export const BROWSER =
104
100
  // Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
105
101
  export const FILES =
106
102
  "Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
107
- "'<file>.md' (structured markdown). Policy: to summarize/quote/search one file, read or grep the .md; when execute is present, " +
108
- "read or search multiple files through state.* in one execute instead of repeated top-level file Tool calls. " +
103
+ "'<file>.md' (structured markdown). Policy: to summarize, quote, or search files, use state.* inside execute. " +
109
104
  "To compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
110
105
  "the ORIGINAL file — converted markdown tables are not for computation, and large spreadsheets may " +
111
106
  "have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
@@ -523,6 +523,8 @@ export interface PiTurnDurability {
523
523
  export interface CreatePreparedPiTurnOptions {
524
524
  readonly prepared: PreparedPiRuntime;
525
525
  readonly pinnedDescriptor: string;
526
+ /** 同一 Session 的所有 Submission 共用的稳定 Provider 路由身份。 */
527
+ readonly modelSessionId: string;
526
528
  readonly submission: {
527
529
  readonly id: string;
528
530
  readonly requestId: string;
@@ -653,7 +655,6 @@ export class PreparedPiTurnAdapter {
653
655
  private readonly turn: PiTurnAdapter;
654
656
  private readonly abortController = new AbortController();
655
657
  private readonly candidates: readonly PiToolCandidate[];
656
- private nestedToolOrdinal = 0;
657
658
  private assistantOrdinal: number;
658
659
  private readonly encoder: PiChunkEncoder;
659
660
  private readonly steerMessageIds: string[] = [];
@@ -829,7 +830,7 @@ export class PreparedPiTurnAdapter {
829
830
  return execute(toolCallId, input, signal, onUpdate);
830
831
  }
831
832
  const startedAt = Date.now();
832
- const telemetryToolCallId = `${toolCallId}:${++this.nestedToolOrdinal}`;
833
+ const telemetryToolCallId = toolCallId;
833
834
  options.onNestedToolStarted?.({
834
835
  parentToolCallId: toolCallIdPrefix,
835
836
  toolCallId: telemetryToolCallId,
@@ -906,7 +907,7 @@ export class PreparedPiTurnAdapter {
906
907
  state.snapshot.bindings.provider,
907
908
  state.snapshot.pi.model.id,
908
909
  ),
909
- modelSessionId: options.submission.requestId,
910
+ modelSessionId: options.modelSessionId,
910
911
  ...(options.onGeneration ? { onGeneration: options.onGeneration } : {}),
911
912
  canonicalMessages: options.canonicalMessages,
912
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
 
@@ -197,7 +197,8 @@ export function normalizeUpdatePlanArguments(
197
197
  steps: steps.map((step) => {
198
198
  if (step === null || typeof step !== "object") return step;
199
199
  const s = step as Record<string, unknown>;
200
- const { step: alias, ...rest } = s;
200
+ const { step: stepAlias, title: titleAlias, ...rest } = s;
201
+ const alias = typeof stepAlias === "string" ? stepAlias : titleAlias;
201
202
  const text = typeof rest.text === "string"
202
203
  ? rest.text
203
204
  : typeof alias === "string"
@@ -227,7 +228,7 @@ const setContextParameters = Type.Object({
227
228
  ], {
228
229
  description: 'Whether to replace the block or append to it. Defaults to "replace".',
229
230
  })),
230
- });
231
+ }, { additionalProperties: false });
231
232
 
232
233
  function result<T>(details: T): AgentToolResult<T> {
233
234
  return {
@@ -241,7 +242,10 @@ function candidate<T extends TSchema>(tool: AgentTool<T>): PiToolCandidate {
241
242
  owner: "runtime-base",
242
243
  requiredExecutionLevel: "safe",
243
244
  source: "action",
244
- tool,
245
+ tool: {
246
+ ...tool,
247
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
248
+ } as AgentTool<T>,
245
249
  };
246
250
  }
247
251
 
@@ -307,7 +311,7 @@ export function basePiToolCandidates(
307
311
  return result({ noted: true, count: input.items.length });
308
312
  },
309
313
  }),
310
- direct: true,
314
+ exposureMode: "direct",
311
315
  },
312
316
  {
313
317
  ...candidate({
@@ -325,7 +329,7 @@ export function basePiToolCandidates(
325
329
  });
326
330
  },
327
331
  }),
328
- direct: true,
332
+ exposureMode: "direct",
329
333
  },
330
334
  ...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
331
335
  ];
@@ -43,15 +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;
51
- /** Keep this Tool Direct-only instead of also offering it through Code Mode. */
52
- readonly direct?: true;
53
- /** Offer this Tool only through Code Mode, never as a top-level Tool. */
54
- readonly codeExecutionOnly?: true;
53
+ /** Omitted means visible both directly and through Code Mode. */
54
+ readonly exposureMode?: ToolExposureMode;
55
55
  /** Conservative maximum used in the stable Runtime descriptor. */
56
56
  readonly requiredExecutionLevel: ExecutionLevel;
57
57
  /** Trusted parameter-level policy, evaluated before approval or dispatch. */
@@ -310,12 +310,21 @@ function governedTool(
310
310
  candidate: PiToolCandidate,
311
311
  options: CompilePiToolsOptions,
312
312
  ): AgentTool<any, any> {
313
- 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;
314
323
  const state = options.governance?.[governanceState];
315
324
  const settle = options.settle;
316
325
 
317
326
  return {
318
- ...candidate.tool,
327
+ ...tool,
319
328
  ...(candidate.deferLoading ? { deferLoading: true as const } : {}),
320
329
  // 执行一次完整的受治理 Pi 工具调用。
321
330
  // Pi 工具循环选中编译后的工具时调用,调用方应传入稳定 toolCall id 供结算去重。