@springbrand/agent-runtime 0.2.0-alpha.65 → 0.2.0-alpha.67

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.65",
3
+ "version": "0.2.0-alpha.67",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -197,6 +197,14 @@ export class ScopedWorkspace implements WorkspacePort, WorkspaceAdminPort {
197
197
  return this.parent.readFileBytes(await this.guardedPhysical(path));
198
198
  }
199
199
 
200
+ async readFilePage(
201
+ path: string,
202
+ offset?: number,
203
+ ) {
204
+ if (!this.parent.readFilePage) return null;
205
+ return this.parent.readFilePage(await this.guardedPhysical(path), offset);
206
+ }
207
+
200
208
  async writeFile(path: string, content: string, mimeType?: string) {
201
209
  return this.parent.writeFile(
202
210
  await this.guardedPhysical(path, true),
@@ -373,6 +381,9 @@ export function createWorkspacePortFacade(
373
381
  const facade: WorkspacePort = {
374
382
  readFile: (path) => workspace.readFile(path),
375
383
  readFileBytes: (path) => workspace.readFileBytes(path),
384
+ ...(workspace.readFilePage
385
+ ? { readFilePage: (path, offset) => workspace.readFilePage!(path, offset) }
386
+ : {}),
376
387
  writeFile: (path, content, mimeType) =>
377
388
  workspace.writeFile(path, content, mimeType),
378
389
  writeFileBytes: (path, data, mimeType) =>
package/src/index.ts CHANGED
@@ -68,6 +68,7 @@ export type {
68
68
  ResolvedResources,
69
69
  RuntimeAgentContext,
70
70
  RuntimeSettings,
71
+ RuntimeSkillBodyTransform,
71
72
  RuntimeToolBindings,
72
73
  ToolAssemblyResult,
73
74
  } from "./runtime-definition";
@@ -59,6 +59,19 @@ export type WorkspaceFileInspection = WorkspaceFileInfo & {
59
59
  evidence?: WorkspaceFileEvidence;
60
60
  };
61
61
 
62
+ type WorkspaceFilePageBase = {
63
+ size: number;
64
+ nextOffset: number | null;
65
+ };
66
+
67
+ export type WorkspaceFilePage =
68
+ | (WorkspaceFilePageBase & { kind: "text"; content: string })
69
+ | (WorkspaceFilePageBase & {
70
+ kind: "csv";
71
+ columns: string[] | null;
72
+ rows: string[][];
73
+ });
74
+
62
75
  /**
63
76
  * 向 Runtime 提供已限定作用域的文件操作。
64
77
  *
@@ -86,6 +99,14 @@ export interface WorkspacePort {
86
99
  * 它与 `readFile` 分开,避免 Runtime 猜测字符编码。
87
100
  */
88
101
  readFileBytes(path: string): Promise<Uint8Array | null>;
102
+ /**
103
+ * Reads one bounded page without buffering the complete file.
104
+ * CSV pages preserve complete records and return parsed columns and rows.
105
+ */
106
+ readFilePage?(
107
+ path: string,
108
+ offset?: number,
109
+ ): Promise<WorkspaceFilePage | null>;
89
110
  /**
90
111
  * 覆盖写入一个文本文件。
91
112
  *
package/src/lib/prompt.ts CHANGED
@@ -99,11 +99,13 @@ export const BROWSER =
99
99
 
100
100
  // Uploaded-file routing prevents lossy markdown conversions from becoming data sources.
101
101
  export const FILES =
102
- "Uploaded files live under /uploads/ in your workspace; convertible formats have a companion " +
102
+ "Uploaded files live under /inputs/ in your workspace; convertible formats have a companion " +
103
103
  "'<file>.md' (structured markdown). Policy: to summarize, quote, or search files, use state.* inside execute. " +
104
+ "For every CSV analysis in Code Mode, start with state.readFilePage({ path }); CSV pages expose columns and rows, never data. Rows are arrays aligned to columns, not objects. Never call state.readFile for a CSV. " +
104
105
  "To compute/aggregate/transform (especially csv/xlsx), write code in execute Code Mode or the Linux Sandbox that reads " +
105
- "the ORIGINAL file — converted markdown tables are not for computation, and large spreadsheets may " +
106
- "have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
106
+ "the ORIGINAL file — converted markdown tables are not for computation. " +
107
+ "For other large text files in Code Mode, use state.readFilePage({ path }) and continue until nextOffset is null. " +
108
+ "Large spreadsheets may have no .md at all. For formats without a companion .md (e.g. pptx: unzip and read ppt/slides/*.xml; " +
107
109
  "zip archives; unknown types), parse the original in the sandbox with JS.";
108
110
 
109
111
  // User-interaction actions are called only when their UI semantics are useful.
@@ -18,6 +18,7 @@ import type {
18
18
  RuntimeCodeExecutionPort,
19
19
  WorkspacePort,
20
20
  } from "../../kernel/bindings";
21
+ import { WorkspaceStateConnector } from "./workspace-state-connector";
21
22
  import type { RuntimeCodeExecutionFactory } from "../../kernel/bindings";
22
23
  import { serializeOutput } from "../../lib/artifacts";
23
24
  import type { PiToolCandidate } from "./compiler";
@@ -88,7 +89,7 @@ state.writeFileBytes is not available. Use state.writeFile or state.appendFile f
88
89
 
89
90
  Bundled Skill resources are not Workspace files. Read text with tools.read_skill_resource, run bundled scripts with tools.run_skill_script, and copy templates or assets with tools.materialize_skill_resource.
90
91
 
91
- User Attachments are already Workspace files below /inputs. Inspect their exact path and immutable cdnUrl before using them.
92
+ User Attachments are already Workspace files below /inputs. Inspect their exact path before using them. Never raw-fetch an attachment already present below /inputs or materialize its cdnUrl; read it with state.*.
92
93
 
93
94
  When an asset already exists in Workspace, use state.cp to copy it into the current Website directory. Use actual task paths and reference the destination with a relative URL.
94
95
 
@@ -336,6 +337,15 @@ export function createWorkspaceCodeExecutionFactory(options: {
336
337
  options.workspace,
337
338
  options.workspaceAccessMode,
338
339
  );
340
+ const workspaceState = state && options.workspace.readFilePage
341
+ ? new WorkspaceStateConnector(
342
+ options.ctx,
343
+ state,
344
+ options.workspace as WorkspacePort & {
345
+ readFilePage: NonNullable<WorkspacePort["readFilePage"]>;
346
+ },
347
+ )
348
+ : undefined;
339
349
  const createPort = (innerResults: AgentToolResult<unknown>[]) => {
340
350
  let nestedCallOrdinal = 0;
341
351
  const { tool } = createExecuteRuntime({
@@ -348,7 +358,7 @@ export function createWorkspaceCodeExecutionFactory(options: {
348
358
  onResult: (value) => innerResults.push(value),
349
359
  }),
350
360
  timeout: CODEMODE_SANDBOX_TIMEOUT_MS,
351
- ...(state ? { state } : {}),
361
+ ...(workspaceState ? { connectors: [workspaceState] } : state ? { state } : {}),
352
362
  name: "execute",
353
363
  });
354
364
  return toCodeExecutionPort(
@@ -13,6 +13,7 @@ import type {
13
13
  RuntimeSkillScriptPolicy,
14
14
  WorkspacePort,
15
15
  } from "../../kernel/bindings";
16
+ import type { RuntimeSkillBodyTransform } from "../../runtime-definition";
16
17
  import { STORAGE_LEAF_MAX_CHARS } from "../../layers/context/budget/gate";
17
18
  import { serializeOutput } from "../../lib/artifacts";
18
19
  import { aiToolToPi } from "./ai-adapter";
@@ -33,6 +34,7 @@ export interface SkillPiToolOptions {
33
34
  readonly loader?: WorkerLoader;
34
35
  readonly workspace?: WorkspacePort;
35
36
  readonly tools?: readonly PiToolCandidate[];
37
+ readonly transformSkillBody?: RuntimeSkillBodyTransform;
36
38
  }
37
39
 
38
40
  /**
@@ -116,7 +118,11 @@ function budgetSkillResources(skill: LoadedSkill): LoadedSkill {
116
118
  return { ...skill, resources: kept, body: `${skill.body}\n\n${lines.join("\n")}` };
117
119
  }
118
120
 
119
- function catalogSkillSource(binding: PiSkillBinding): SkillSource {
121
+ function catalogSkillSource(
122
+ binding: PiSkillBinding,
123
+ transformSkillBody: RuntimeSkillBodyTransform | undefined,
124
+ availableToolNames: readonly string[],
125
+ ): SkillSource {
120
126
  const source = binding.source;
121
127
  return {
122
128
  id: source.id,
@@ -132,7 +138,17 @@ function catalogSkillSource(binding: PiSkillBinding): SkillSource {
132
138
  load: async (name) => {
133
139
  const skill = await source.load(name);
134
140
  if (!skill) return null;
135
- const loaded = budgetSkillResources(skill);
141
+ const prepared = transformSkillBody
142
+ ? {
143
+ ...skill,
144
+ body: transformSkillBody({
145
+ name: skill.name,
146
+ body: skill.body,
147
+ availableToolNames,
148
+ }),
149
+ }
150
+ : skill;
151
+ const loaded = budgetSkillResources(prepared);
136
152
  const hasOversizedResource = skill.resources?.some((resource) =>
137
153
  typeof resource.size === "number" &&
138
154
  resource.size > SKILL_RESOURCE_BUDGET_BYTES
@@ -420,8 +436,14 @@ export async function skillPiToolCandidates(
420
436
  ): Promise<PiToolCandidate[]> {
421
437
  if (bindings.length === 0) return [];
422
438
 
439
+ const availableToolNames = (options.tools ?? []).map(({ tool }) => tool.name);
440
+
423
441
  const registry = new SkillRegistry(
424
- bindings.map(catalogSkillSource),
442
+ bindings.map((binding) => catalogSkillSource(
443
+ binding,
444
+ options.transformSkillBody,
445
+ availableToolNames,
446
+ )),
425
447
  scriptRunner(bindings, options),
426
448
  );
427
449
  await registry.load();
@@ -0,0 +1,143 @@
1
+ import type { StateBackend } from "@cloudflare/shell/workers";
2
+ import { StateConnector } from "@cloudflare/shell/workers";
3
+ import type { ConnectorTools } from "@cloudflare/codemode";
4
+ import type { WorkspacePort } from "../../kernel/bindings";
5
+
6
+ export const WORKSPACE_DIRECT_TEXT_READ_MAX_BYTES = 1024 * 1024;
7
+
8
+ const READ_FILE_PAGE_TYPES = `
9
+ type WorkspaceFilePage =
10
+ | { kind: "text"; size: number; nextOffset: number | null; content: string }
11
+ | { kind: "csv"; size: number; nextOffset: number | null; columns: string[] | null; rows: string[][] };
12
+ `;
13
+
14
+ type PagedWorkspacePort = WorkspacePort & {
15
+ readFilePage: NonNullable<WorkspacePort["readFilePage"]>;
16
+ };
17
+
18
+ export class WorkspaceStateConnector extends StateConnector {
19
+ constructor(
20
+ ctx: DurableObjectState | ExecutionContext,
21
+ backend: StateBackend,
22
+ private readonly workspace: PagedWorkspacePort,
23
+ ) {
24
+ super(ctx, backend);
25
+ }
26
+
27
+ protected override instructions(): string {
28
+ const base = super.instructions();
29
+ return `${base}
30
+
31
+ For CSV files, do not make a separate probe read or return page contents. Inspect columns and aggregate rows in the same execution with this loop shape:
32
+
33
+ let page = await state.readFilePage({ path });
34
+ if (!page || page.kind !== "csv") throw new Error("CSV not found");
35
+ const columns = page.columns;
36
+ for (;;) {
37
+ // Aggregate page.rows here. Do not return raw rows.
38
+ if (page.nextOffset === null) break;
39
+ page = await state.readFilePage({ path, offset: page.nextOffset });
40
+ }
41
+ return summary;
42
+
43
+ CSV pages use page.columns and page.rows; never page.data or page.content. Only the first page has columns. Do not use console.log to inspect a page; explicitly return the final summary.`;
44
+ }
45
+
46
+ protected override tools(): ConnectorTools {
47
+ const tools = super.tools();
48
+ const readFile = tools.readFile!;
49
+ return {
50
+ ...tools,
51
+ readFile: {
52
+ ...readFile,
53
+ description:
54
+ `Read a small non-CSV file as text. CSV and files over ${WORKSPACE_DIRECT_TEXT_READ_MAX_BYTES} bytes must use readFilePage.`,
55
+ execute: async (args, context) => {
56
+ const path = requiredPath(args, "readFile");
57
+ const file = await this.workspace.stat(path);
58
+ if (file?.type === "file" && (
59
+ file.mimeType === "text/csv" ||
60
+ file.size > WORKSPACE_DIRECT_TEXT_READ_MAX_BYTES
61
+ )) {
62
+ throw new Error(
63
+ `${path} requires state.readFilePage({ path: ${JSON.stringify(path)} }); continue with nextOffset until it is null`,
64
+ );
65
+ }
66
+ return readFile.execute(args, context);
67
+ },
68
+ },
69
+ readFilePage: {
70
+ description:
71
+ "Read up to 512 KiB or 2,000 CSV rows from a Workspace file. " +
72
+ "A CSV page returns columns and rows and has no data field; rows are string arrays aligned to columns. " +
73
+ "A text page returns content. Omit offset on the first call, then pass page.nextOffset until it is null. " +
74
+ "Prefer this over readFile for CSV and large files.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ path: { type: "string", minLength: 1, description: "Absolute Workspace file path." },
79
+ offset: {
80
+ type: "integer",
81
+ minimum: 0,
82
+ description: "Previous page.nextOffset. Omit on the first call.",
83
+ },
84
+ },
85
+ required: ["path"],
86
+ additionalProperties: false,
87
+ },
88
+ replay: "reexecute",
89
+ execute: (args) => {
90
+ const input = readFilePageInput(args);
91
+ return this.workspace.readFilePage(input.path, input.offset);
92
+ },
93
+ },
94
+ };
95
+ }
96
+
97
+ override async getTypeScriptTypes(): Promise<string> {
98
+ const types = await super.getTypeScriptTypes();
99
+ const addition = [
100
+ " /** CSV pages have columns and rows; never data. Omit offset first, then pass nextOffset until null. */",
101
+ " readFilePage(args: { path: string; offset?: number }): Promise<WorkspaceFilePage | null>;",
102
+ ].join("\n");
103
+ return `${READ_FILE_PAGE_TYPES}\n${types.replace(/\n\};$/u, `\n${addition}\n};`)}`;
104
+ }
105
+ }
106
+
107
+ function requiredPath(value: unknown, method: string): string {
108
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
109
+ typeof (value as { path?: unknown }).path !== "string" ||
110
+ !(value as { path: string }).path) {
111
+ throw new Error(
112
+ `state.${method} takes one object argument: state.${method}({ path: "/path/to/file" })`,
113
+ );
114
+ }
115
+ return (value as { path: string }).path;
116
+ }
117
+
118
+ function readFilePageInput(value: unknown): {
119
+ path: string;
120
+ offset?: number;
121
+ } {
122
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
123
+ throw new Error("state.readFilePage requires an object argument");
124
+ }
125
+ const input = value as Record<string, unknown>;
126
+ const keys = Object.keys(input);
127
+ if (keys.some((key) => !["path", "offset"].includes(key))) {
128
+ throw new Error("state.readFilePage accepts only path and offset");
129
+ }
130
+ if (typeof input.path !== "string" || input.path.length === 0) {
131
+ throw new Error("state.readFilePage requires path");
132
+ }
133
+ const offset = input.offset;
134
+ if (offset !== undefined && (!Number.isInteger(offset) || (offset as number) < 0)) {
135
+ throw new Error(
136
+ "state.readFilePage offset must be a previous page.nextOffset; omit it on the first call",
137
+ );
138
+ }
139
+ return {
140
+ path: input.path,
141
+ ...(offset === undefined ? {} : { offset: offset as number }),
142
+ };
143
+ }
@@ -44,6 +44,7 @@ import type {
44
44
  RuntimeSettings,
45
45
  RuntimeExtensionContribution,
46
46
  RuntimeProfileContribution,
47
+ RuntimeSkillBodyTransform,
47
48
  RuntimeSkillContribution,
48
49
  RuntimeToolSurfacePolicy,
49
50
  } from "./runtime-definition";
@@ -90,6 +91,7 @@ export interface RuntimeAssemblyInput {
90
91
  readonly sandbox?: RuntimeSandboxPort;
91
92
  readonly memory?: RuntimeMemoryPort;
92
93
  readonly hostTools: readonly PiToolCandidate[];
94
+ readonly transformSkillBody?: RuntimeSkillBodyTransform;
93
95
  readonly skills: readonly RuntimeSkillContribution[];
94
96
  readonly connectors: readonly RuntimeMcpServer[];
95
97
  readonly gateway?: RuntimeGatewayPort;
@@ -168,6 +170,7 @@ interface ToolSurfaceInput {
168
170
  readonly profile: RuntimeMemoryProfile;
169
171
  };
170
172
  readonly hostTools: readonly PiToolCandidate[];
173
+ readonly transformSkillBody?: RuntimeSkillBodyTransform;
171
174
  readonly skills: readonly RuntimeSkillSourceBinding[];
172
175
  readonly enabledSubagents: readonly string[];
173
176
  readonly webSearch?: Parameters<typeof basePiToolCandidates>[0];
@@ -236,6 +239,9 @@ async function createToolSurface(
236
239
  loader: input.platform.loader,
237
240
  ...(input.workspace ? { workspace: input.workspace } : {}),
238
241
  tools: scriptCandidates,
242
+ ...(input.transformSkillBody
243
+ ? { transformSkillBody: input.transformSkillBody }
244
+ : {}),
239
245
  })),
240
246
  ...baseCandidates,
241
247
  ...memoryCandidates,
@@ -369,6 +375,7 @@ class RuntimeBuilder {
369
375
  private memoryProfile: RuntimeMemoryProfile = DISABLED_MEMORY;
370
376
  private memory?: RuntimeMemoryPort;
371
377
  private readonly hostTools: PiToolCandidate[] = [];
378
+ private transformSkillBody?: RuntimeSkillBodyTransform;
372
379
  private toolPolicy?: RuntimeToolSurfacePolicy;
373
380
  private readonly skillSources = new Map<
374
381
  string,
@@ -404,6 +411,7 @@ class RuntimeBuilder {
404
411
  this.memoryProfile = input.memoryProfile;
405
412
  this.memory = input.memory;
406
413
  this.hostTools.push(...input.hostTools);
414
+ this.transformSkillBody = input.transformSkillBody;
407
415
  this.toolPolicy = input.toolPolicy;
408
416
  for (const skill of input.skills) this.addSkill(skill);
409
417
  for (const connector of input.connectors) this.addConnector(connector);
@@ -619,6 +627,9 @@ class RuntimeBuilder {
619
627
  [...this.enabledSubagents],
620
628
  ),
621
629
  ],
630
+ ...(this.transformSkillBody
631
+ ? { transformSkillBody: this.transformSkillBody }
632
+ : {}),
622
633
  skills: skillSources,
623
634
  enabledSubagents: [...this.enabledSubagents],
624
635
  ...(webSearch ? { webSearch } : {}),
@@ -786,6 +797,9 @@ export async function assembleRuntimeSnapshot<
786
797
  ? { subagents: toolAssembly.bindings.subagents }
787
798
  : {}),
788
799
  hostTools,
800
+ ...(resources.transformSkillBody
801
+ ? { transformSkillBody: resources.transformSkillBody }
802
+ : {}),
789
803
  skills:
790
804
  input.ctx.role === "primary" && toolAssembly.bindings?.workspace
791
805
  ? [...BUILT_IN_RUNTIME_SKILLS, ...resources.skills]
@@ -82,12 +82,19 @@ export interface ResolvedConnectors {
82
82
  readonly degradations: readonly RuntimeDegradation[];
83
83
  }
84
84
 
85
+ export type RuntimeSkillBodyTransform = (input: {
86
+ readonly name: string;
87
+ readonly body: string;
88
+ readonly availableToolNames: readonly string[];
89
+ }) => string;
90
+
85
91
  export interface ResolvedResources {
86
92
  readonly settings: RuntimeSettings;
87
93
  readonly provider: RuntimeProviderPort;
88
94
  readonly platform: RuntimePlatformPort;
89
95
  readonly turnEvents?: RuntimeTurnEventsPort;
90
96
  readonly admission?: RuntimeSubmissionAdmissionHook;
97
+ readonly transformSkillBody?: RuntimeSkillBodyTransform;
91
98
  readonly skills: readonly RuntimeSkillContribution[];
92
99
  readonly extensions: readonly RuntimeExtensionContribution[];
93
100
  readonly connectors: ResolvedConnectors;