@springbrand/agent-runtime 0.3.0-alpha.1 → 0.3.0-alpha.11

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 (111) hide show
  1. package/package.json +8 -8
  2. package/src/adapter/cloudflare/agent.ts +160 -14
  3. package/src/adapter/cloudflare/define-agent.ts +12 -7
  4. package/src/adapter/cloudflare/index.ts +18 -0
  5. package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
  6. package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
  7. package/src/adapter/cloudflare/schema.ts +15 -0
  8. package/src/adapter/cloudflare/subagent/definition.ts +4 -4
  9. package/src/adapter/cloudflare/subagent/runner.ts +1 -1
  10. package/src/adapter/cloudflare/subagent/tools.ts +2 -2
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
  13. package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
  14. package/src/config/agent-context.ts +1 -1
  15. package/src/config/assembler.ts +29 -31
  16. package/src/config/assembly-snapshot.ts +1 -1
  17. package/src/config/definition.ts +1 -1
  18. package/src/config/harness-runtime.ts +9 -1
  19. package/src/config/model-guidance.ts +21 -0
  20. package/src/config/pinning.ts +22 -3
  21. package/src/config/ports.ts +27 -7
  22. package/src/config/public-contracts.ts +2 -2
  23. package/src/context/assembly.ts +1 -1
  24. package/src/context/extension/assembly.ts +2 -2
  25. package/src/context/memory/budget/gate.ts +19 -3
  26. package/src/conversation/harness-stream.ts +2 -1
  27. package/src/conversation/history.ts +2 -0
  28. package/src/conversation/message/projection.ts +38 -21
  29. package/src/conversation/state.ts +23 -2
  30. package/src/harness/session-harness.ts +160 -6
  31. package/src/index.ts +8 -7
  32. package/src/lib/artifacts.ts +18 -0
  33. package/src/lib/execution-level.ts +13 -0
  34. package/src/lib/prompt.ts +9 -10
  35. package/src/lib/token-estimate.ts +8 -0
  36. package/src/models/index.ts +32 -10
  37. package/src/models/openrouter/messages.ts +13 -8
  38. package/src/models/provider-streams.ts +45 -0
  39. package/src/session/cloudflare/migration.ts +71 -11
  40. package/src/skills/springbrand-worker-website/index.ts +1 -1
  41. package/src/submission/admission.ts +12 -1
  42. package/src/submission/contracts.ts +42 -2
  43. package/src/submission/delivery.ts +1 -1
  44. package/src/submission/finalization.ts +1 -1
  45. package/src/submission/repository.ts +64 -4
  46. package/src/submission/unattended.ts +40 -0
  47. package/src/telemetry/coordinator.ts +1 -1
  48. package/src/telemetry/subagent-lifecycle.ts +1 -1
  49. package/src/telemetry/turn-events.ts +1 -1
  50. package/src/tools/README.md +44 -0
  51. package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
  52. package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
  53. package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
  54. package/src/tools/assistant/ask-user.ts +317 -0
  55. package/src/tools/assistant/candidate.ts +15 -0
  56. package/src/tools/assistant/followups.ts +34 -0
  57. package/src/tools/assistant/index.ts +19 -0
  58. package/src/tools/assistant/memory.ts +73 -0
  59. package/src/tools/assistant/plan.ts +98 -0
  60. package/src/tools/{time.ts → assistant/time.ts} +1 -1
  61. package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
  62. package/src/tools/{core.ts → codemode/index.ts} +36 -92
  63. package/src/tools/codemode/workspace-connector.ts +198 -0
  64. package/src/tools/extension/index.ts +42 -0
  65. package/src/tools/index.ts +29 -26
  66. package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
  67. package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
  68. package/src/tools/runtime/activation.ts +40 -0
  69. package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
  70. package/src/tools/{compiler.ts → runtime/compiler.ts} +84 -147
  71. package/src/tools/runtime/contracts.ts +140 -0
  72. package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +147 -37
  73. package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
  74. package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
  75. package/src/tools/runtime/result.ts +22 -0
  76. package/src/tools/runtime/unattended-error.ts +4 -0
  77. package/src/tools/sandbox/index.ts +228 -0
  78. package/src/tools/schedule/index.ts +301 -0
  79. package/src/tools/skill/contracts.ts +20 -0
  80. package/src/tools/skill/index.ts +99 -0
  81. package/src/tools/skill/resources.ts +322 -0
  82. package/src/tools/skill/scripts.ts +168 -0
  83. package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
  84. package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
  85. package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
  86. package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
  87. package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
  88. package/src/tools/{temporary-agent → subagent/temporary}/core.ts +1 -1
  89. package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
  90. package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
  91. package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
  92. package/src/tools/web/index.ts +2 -0
  93. package/src/tools/web/search/api.ts +1320 -0
  94. package/src/tools/web/search/index.ts +2 -0
  95. package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
  96. package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
  97. package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
  98. package/src/tools/base.ts +0 -534
  99. package/src/tools/schedule.ts +0 -261
  100. package/src/tools/skill.ts +0 -565
  101. package/src/tools/web-search/api.ts +0 -1318
  102. package/src/tools/web-search/index.ts +0 -2
  103. package/src/tools/workspace-state-connector.ts +0 -146
  104. package/src/tools/{approval → runtime/approval}/index.ts +0 -0
  105. package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
  106. package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
  107. package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
  108. package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
  109. package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
  110. package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
  111. /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  type AgentHarness as PiAgentHarness,
5
5
  type AgentHarnessOptions,
6
6
  type AgentLane,
7
+ type AgentToolResult,
7
8
  type DriveOutcome,
8
9
  type DriveOptions,
9
10
  type OpenOperation,
@@ -17,6 +18,7 @@ import {
17
18
  isRecoverableAssistantError,
18
19
  } from "../models";
19
20
  import { projectToolOutputForModel } from "../context/memory/budget/gate";
21
+ import type { ArtifactRef } from "../lib/artifacts";
20
22
 
21
23
  export interface SessionHarnessLimits {
22
24
  /** Complete model turns committed before one Worker instance releases. */
@@ -34,6 +36,22 @@ export interface SessionHarnessOptions<TContext extends object | undefined>
34
36
  modelToolResultExemptions?: readonly string[];
35
37
  /** Base provider Session identity, retained except after a server Tool timeout. */
36
38
  modelSessionId?: string;
39
+ /**
40
+ * Compiled tools omitted from the initial active set. A later successful
41
+ * result may add them through {@link SessionHarnessOptions.toolsToActivate}.
42
+ */
43
+ onDemandToolNames?: readonly string[];
44
+ /**
45
+ * Names to add with `setActiveTools` from `after_tool`. That hook runs
46
+ * before the next generation snapshots `activeToolNames`; Pi then writes
47
+ * the difference as transcript `toolsAdded`. Do not call `setActiveTools`
48
+ * from `tool_end`: event delivery is serialized and would deadlock.
49
+ */
50
+ toolsToActivate?: (
51
+ toolName: string,
52
+ result: AgentToolResult<unknown>,
53
+ isError: boolean,
54
+ ) => readonly string[];
37
55
  }
38
56
 
39
57
  export type SessionHarnessRunResult = DriveOutcome | {
@@ -54,6 +72,7 @@ const TOOL_FAILURE_WRAP_UP_INSTRUCTION =
54
72
  "This turn reached its tool failure limit. Stop calling tools now. Reply with a final message that explains what failed and the exact next step the user should take. Do not start new work.";
55
73
  const MAX_TOOL_RETRIES = 3;
56
74
  const TURN_FAILURE_CEILING = 8;
75
+ const ARTIFACT_MANIFEST_MAX_CHARS = 12 * 1024;
57
76
 
58
77
  function canonicalJson(value: unknown): string {
59
78
  if (Array.isArray(value)) {
@@ -73,6 +92,10 @@ function toolCallKey(toolName: string, args: unknown): string {
73
92
  return `${toolName}\0${canonicalJson(args)}`;
74
93
  }
75
94
 
95
+ function uniqueToolNames(names: readonly string[]): string[] {
96
+ return [...new Set(names)];
97
+ }
98
+
76
99
  function projectToolResultsForModel(
77
100
  messages: readonly AgentMessage[],
78
101
  exemptions: ReadonlySet<string>,
@@ -92,6 +115,66 @@ function projectToolResultsForModel(
92
115
  return changed ? projected : [...messages];
93
116
  }
94
117
 
118
+ function artifactRefFromMessage(message: AgentMessage): ArtifactRef | undefined {
119
+ if (message.role !== "toolResult" || message.details === null ||
120
+ typeof message.details !== "object" ||
121
+ (message.details as { kind?: unknown }).kind !== "artifact_ref") {
122
+ return undefined;
123
+ }
124
+ const ref = message.details as Partial<ArtifactRef>;
125
+ if (typeof ref.path !== "string" || typeof ref.hash !== "string" ||
126
+ typeof ref.bytes !== "number" || typeof ref.preview !== "string" ||
127
+ typeof ref.note !== "string") return undefined;
128
+ return {
129
+ kind: "artifact_ref",
130
+ artifactId: typeof ref.artifactId === "string"
131
+ ? ref.artifactId
132
+ : `artifact_${ref.hash.slice(0, 24)}`,
133
+ path: ref.path,
134
+ bytes: ref.bytes,
135
+ hash: ref.hash,
136
+ workspaceRevision: typeof ref.workspaceRevision === "string"
137
+ ? ref.workspaceRevision
138
+ : "unknown",
139
+ availability: ref.availability === "unavailable"
140
+ ? "unavailable"
141
+ : "available",
142
+ preview: ref.preview,
143
+ note: ref.note,
144
+ };
145
+ }
146
+
147
+ function artifactManifestMessage(
148
+ refs: ReadonlyMap<string, ArtifactRef>,
149
+ ): AgentMessage | undefined {
150
+ if (refs.size === 0) return undefined;
151
+ const lines = [
152
+ "<workspace_artifact_manifest>",
153
+ "These are lightweight references to prior Tool output. Do not treat previews as complete. Read an available artifact with the Workspace readFile/readFilePage tool only when the current request needs its evidence.",
154
+ ];
155
+ let omitted = 0;
156
+ for (const ref of refs.values()) {
157
+ const preview = ref.preview.replace(/[\r\n]+/g, " ").slice(0, 160);
158
+ const line = [
159
+ `path=${ref.path}`,
160
+ ...(ref.availability === "unavailable" ? ["unavailable"] : []),
161
+ `preview=${preview}`,
162
+ ].join(" ");
163
+ if ([...lines, line, "</workspace_artifact_manifest>"].join("\n").length > ARTIFACT_MANIFEST_MAX_CHARS) {
164
+ omitted += 1;
165
+ continue;
166
+ }
167
+ lines.push(line);
168
+ }
169
+ if (omitted > 0) lines.push(`omitted=${omitted} additional artifact references due to manifest budget`);
170
+ lines.push("</workspace_artifact_manifest>");
171
+ return {
172
+ role: "user",
173
+ content: lines.join("\n"),
174
+ timestamp: 1,
175
+ };
176
+ }
177
+
95
178
  function deferred<T>(): {
96
179
  promise: Promise<T>;
97
180
  resolve(value: T): void;
@@ -126,6 +209,15 @@ export class SessionHarness<TContext extends object | undefined> {
126
209
  private readonly modelSessionId: string | undefined;
127
210
  private readonly wrapUpInstruction: string;
128
211
  private readonly configuredToolNames: string[];
212
+ private readonly compiledToolNames: ReadonlySet<string>;
213
+ private readonly onDemandToolNames: ReadonlySet<string>;
214
+ private readonly toolsToActivate:
215
+ | ((
216
+ toolName: string,
217
+ result: AgentToolResult<unknown>,
218
+ isError: boolean,
219
+ ) => readonly string[])
220
+ | undefined;
129
221
  private readonly modelToolResultExemptions: ReadonlySet<string>;
130
222
  private readonly toolFailures = new Map<string, number>();
131
223
  private totalToolFailures = 0;
@@ -144,6 +236,7 @@ export class SessionHarness<TContext extends object | undefined> {
144
236
  private readonly releaseRequest = deferred<"approval" | "interaction">();
145
237
  private readonly activeTools = new Set<string>();
146
238
  private readonly toolsIdle = new Set<() => void>();
239
+ private readonly artifactRefs = new Map<string, ArtifactRef>();
147
240
 
148
241
  private constructor(
149
242
  readonly harness: PiAgentHarness<TContext>,
@@ -169,18 +262,22 @@ export class SessionHarness<TContext extends object | undefined> {
169
262
  DEFAULT_WRAP_UP_INSTRUCTION;
170
263
  this.configuredToolNames = options.activeToolNames ??
171
264
  (options.tools ?? []).map(({ name }) => name);
265
+ this.compiledToolNames = new Set((options.tools ?? []).map(({ name }) => name));
266
+ this.onDemandToolNames = new Set(options.onDemandToolNames ?? []);
267
+ this.toolsToActivate = options.toolsToActivate;
172
268
  this.modelToolResultExemptions = new Set(
173
269
  options.modelToolResultExemptions ?? [],
174
270
  );
175
271
  }
176
272
 
177
273
  private async operationEntries(context: Context): Promise<Entry[]> {
274
+ // Stop boundaries follow traversal order; walk back from the current tip.
178
275
  const entries = await this.lane.findEntries({
179
276
  ...(this.operationStartTip === null ? {} : { stopAtId: this.operationStartTip }),
180
277
  type: "message",
181
- order: "oldestFirst",
278
+ order: "newestFirst",
182
279
  }, context);
183
- return entries.filter(({ id }) => id !== this.operationStartTip);
280
+ return entries.filter(({ id }) => id !== this.operationStartTip).reverse();
184
281
  }
185
282
 
186
283
  private async restoreToolGovernance(context: Context): Promise<void> {
@@ -255,6 +352,8 @@ export class SessionHarness<TContext extends object | undefined> {
255
352
  limits: _limits,
256
353
  modelToolResultExemptions: _modelToolResultExemptions,
257
354
  modelSessionId: _modelSessionId,
355
+ onDemandToolNames: _onDemandToolNames,
356
+ toolsToActivate: _toolsToActivate,
258
357
  ...harnessOptions
259
358
  } = options;
260
359
  let runtime!: SessionHarness<TContext>;
@@ -288,6 +387,12 @@ export class SessionHarness<TContext extends object | undefined> {
288
387
  }
289
388
 
290
389
  private async install(context: Context): Promise<void> {
390
+ for (const entry of await this.lane.findEntries({ type: "message", order: "oldestFirst" }, context)) {
391
+ if (entry.type === "message") {
392
+ const ref = artifactRefFromMessage(entry.message);
393
+ if (ref) this.artifactRefs.set(ref.artifactId, ref);
394
+ }
395
+ }
291
396
  if (this._openOperation) {
292
397
  const watch = await this.lane.watch(context);
293
398
  this.operationStartTip = watch.snapshot.operation?.fromTipId ?? null;
@@ -316,19 +421,21 @@ export class SessionHarness<TContext extends object | undefined> {
316
421
  event.messages,
317
422
  this.modelToolResultExemptions,
318
423
  );
424
+ const manifest = artifactManifestMessage(this.artifactRefs);
319
425
  const instruction = this.toolFailureWrapUp
320
426
  ? TOOL_FAILURE_WRAP_UP_INSTRUCTION
321
427
  : this.wrappingUp
322
428
  ? this.wrapUpInstruction
323
429
  : undefined;
430
+ const contextMessages = manifest ? [...messages, manifest] : messages;
324
431
  return {
325
432
  messages: instruction
326
- ? [...messages, {
433
+ ? [...contextMessages, {
327
434
  role: "user" as const,
328
435
  content: instruction,
329
436
  timestamp: 1,
330
437
  }]
331
- : messages,
438
+ : contextMessages,
332
439
  };
333
440
  });
334
441
  this.harness.hooks.on("before_tool", (event) => {
@@ -353,7 +460,12 @@ export class SessionHarness<TContext extends object | undefined> {
353
460
  };
354
461
  });
355
462
  this.harness.events.on("entry_added", (event) => {
356
- if (event.lane === this.lane.name) this.recordCanonicalToolOutcome(event.entry);
463
+ if (event.lane !== this.lane.name) return;
464
+ this.recordCanonicalToolOutcome(event.entry);
465
+ if (event.entry.type === "message") {
466
+ const ref = artifactRefFromMessage(event.entry.message);
467
+ if (ref) this.artifactRefs.set(ref.artifactId, ref);
468
+ }
357
469
  });
358
470
  this.harness.events.on("turn_end", async (_event, eventContext) => {
359
471
  const total = await this.completedModelTurns(eventContext);
@@ -371,6 +483,17 @@ export class SessionHarness<TContext extends object | undefined> {
371
483
  }
372
484
  await this.harness.close(eventContext);
373
485
  });
486
+ this.harness.hooks.on("after_tool", async (event, eventContext) => {
487
+ await this.activateToolsFromResult({
488
+ toolName: event.toolName,
489
+ result: {
490
+ content: event.content,
491
+ details: event.details,
492
+ },
493
+ isError: event.isError,
494
+ }, eventContext);
495
+ return undefined;
496
+ });
374
497
  this.harness.events.on("tool_start", (event) => {
375
498
  this.activeTools.add(event.toolCallId);
376
499
  });
@@ -393,6 +516,34 @@ export class SessionHarness<TContext extends object | undefined> {
393
516
  await new Promise<void>((resolve) => this.toolsIdle.add(resolve));
394
517
  }
395
518
 
519
+ private async activeToolsForNewOperation(context: Context): Promise<string[]> {
520
+ const previous = await this.lane.getActiveTools(context);
521
+ const keepOnDemand = previous.filter((name) =>
522
+ this.onDemandToolNames.has(name) && this.compiledToolNames.has(name)
523
+ );
524
+ return uniqueToolNames([...this.configuredToolNames, ...keepOnDemand]);
525
+ }
526
+
527
+ private async activateToolsFromResult(
528
+ event: {
529
+ toolName: string;
530
+ result: AgentToolResult<unknown>;
531
+ isError: boolean;
532
+ },
533
+ context: Context,
534
+ ): Promise<void> {
535
+ const requested = this.toolsToActivate?.(
536
+ event.toolName,
537
+ event.result,
538
+ event.isError,
539
+ ) ?? [];
540
+ const allowed = requested.filter((name) => this.compiledToolNames.has(name));
541
+ if (allowed.length === 0) return;
542
+ const current = await this.lane.getActiveTools(context);
543
+ if (allowed.every((name) => current.includes(name))) return;
544
+ await this.lane.setActiveTools(uniqueToolNames([...current, ...allowed]), context);
545
+ }
546
+
396
547
  private async completedModelTurns(context: Context): Promise<number> {
397
548
  return (await this.operationEntries(context)).filter((entry) =>
398
549
  entry.type === "message" && entry.message.role === "assistant"
@@ -417,7 +568,10 @@ export class SessionHarness<TContext extends object | undefined> {
417
568
  this.toolFailureWrapUp = false;
418
569
  this.toolFailureWrapUpAt = undefined;
419
570
  this.releaseReason = undefined;
420
- await this.lane.setActiveTools(this.configuredToolNames, context);
571
+ await this.lane.setActiveTools(
572
+ await this.activeToolsForNewOperation(context),
573
+ context,
574
+ );
421
575
  const admission = await this.lane.accept(request, context);
422
576
  if (!admission.ok) throw admission.error;
423
577
  operationId = admission.value.operationId;
package/src/index.ts CHANGED
@@ -51,7 +51,7 @@ export type { RuntimeAssemblyView } from "./config/assembly-view";
51
51
  export * from "./adapter/cloudflare/define-agent";
52
52
  export type { ToolInteractionResponsePreparation } from "./adapter/cloudflare/agent";
53
53
  export * from "./conversation/state";
54
- export * from "./tools/workspace-versioning";
54
+ export * from "./tools/workspace/versioning";
55
55
  export * from "./lib/execution-level";
56
56
  export type {
57
57
  RuntimeAssemblyInput,
@@ -84,16 +84,17 @@ export type {
84
84
  } from "./lib/host-actions";
85
85
  export {
86
86
  AGENT_TYPES,
87
- } from "./tools/subagent-types/registry";
87
+ } from "./tools/subagent/types/registry";
88
88
  export type {
89
89
  AgentType,
90
- } from "./tools/subagent-types/contract";
90
+ } from "./tools/subagent/types/contract";
91
91
  export type {
92
92
  PiToolCandidate,
93
93
  ToolExposureMode,
94
94
  } from "./tools";
95
95
  export { basePiToolCandidates } from "./tools";
96
96
  export { createPiDeclaredToolCandidate } from "./tools";
97
+ export { harnessToolActivation } from "./tools";
97
98
  export type {
98
99
  PiDeclaredTool,
99
100
  PiDeclaredToolCallContext,
@@ -129,15 +130,15 @@ export type {
129
130
  TemporaryAgentLaunch,
130
131
  TemporaryAgentRequest,
131
132
  TemporaryAgentRunContext,
132
- } from "./tools/temporary-agent/core";
133
- export { TEMPORARY_AGENT_LAUNCH_KEY } from "./tools/temporary-agent/core";
133
+ } from "./tools/subagent/temporary/core";
134
+ export { TEMPORARY_AGENT_LAUNCH_KEY } from "./tools/subagent/temporary/core";
134
135
  export {
135
136
  temporaryAgentExtensionIsSafe,
136
137
  temporaryAgentToolAllowed,
137
- } from "./tools/temporary-agent/runner";
138
+ } from "./tools/subagent/temporary/runner";
138
139
  export {
139
140
  createTemporaryAgentWorkspace,
140
- } from "./tools/temporary-agent/workspace";
141
+ } from "./tools/subagent/temporary/workspace";
141
142
  export {
142
143
  createPiModels,
143
144
  modelRequestUrl,
@@ -4,9 +4,12 @@
4
4
  /** Stable narrow reference shared by messages, RPC responses and the UI. */
5
5
  export interface ArtifactRef {
6
6
  kind: "artifact_ref";
7
+ artifactId: string;
7
8
  path: string;
8
9
  bytes: number;
9
10
  hash: string;
11
+ workspaceRevision: string;
12
+ availability: "available" | "unavailable";
10
13
  preview: string;
11
14
  note: string;
12
15
  }
@@ -14,6 +17,15 @@ export interface ArtifactRef {
14
17
  export interface SpillWorkspace {
15
18
  /** 把溢出的 Artifact 内容写入指定 Workspace 路径。 */
16
19
  writeFile(path: string, content: string, mimeType?: string): Promise<void>;
20
+ /** 写入后验证物理文件,防止只持久化了一个不存在或被截断的引用。 */
21
+ stat?(path: string): Promise<{
22
+ type: "file" | "directory" | "symlink";
23
+ size: number;
24
+ updatedAt: number;
25
+ contentVersion?: string;
26
+ } | null>;
27
+ /** 写入后验证内容摘要;Artifact 全部是 UTF-8 文本。 */
28
+ readFileBytes?(path: string): Promise<Uint8Array | null>;
17
29
  }
18
30
 
19
31
  /**
@@ -33,6 +45,10 @@ export async function sha256Hex(text: string): Promise<string> {
33
45
  return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
34
46
  }
35
47
 
48
+ function artifactId(hash: string): string {
49
+ return `artifact_${hash.slice(0, 24)}`;
50
+ }
51
+
36
52
  /**
37
53
  * 把 Tool 结果转成可存储的文本,并选择 `.txt` 或 `.json` 扩展名。
38
54
  *
@@ -52,3 +68,5 @@ export function serializeOutput(output: unknown): { text: string; ext: "txt" | "
52
68
  return { text: String(output), ext: "txt" };
53
69
  }
54
70
  }
71
+
72
+ export { artifactId };
@@ -25,3 +25,16 @@ export function maxExecutionLevel(
25
25
  )
26
26
  ]!;
27
27
  }
28
+
29
+ /** The most permissive level shared by an Agent and a Scheduled Task ceiling. */
30
+ export function minExecutionLevel(
31
+ left: ExecutionLevel,
32
+ right: ExecutionLevel,
33
+ ): ExecutionLevel {
34
+ return EXECUTION_LEVELS[
35
+ Math.min(
36
+ EXECUTION_LEVELS.indexOf(left),
37
+ EXECUTION_LEVELS.indexOf(right),
38
+ )
39
+ ]!;
40
+ }
package/src/lib/prompt.ts CHANGED
@@ -17,9 +17,8 @@ export const PERSONA =
17
17
  // Names the stable runtime environment.
18
18
  export const RUNTIME =
19
19
  "Runtime: this agent runs on Cloudflare Workers. When execute is present, its Code Mode Dynamic Worker is your " +
20
- "instrument — code there runs with outbound network access (fetch), your " +
21
- "workspace filesystem (state.*), and the tools.* methods listed in its own description. Use it for raw or customized HTTP " +
22
- "requests, parsing a payload, hitting several known endpoints, or computing over a file. Write plain " +
20
+ "instrument — code there runs with your workspace filesystem (state.*) and the tools.* methods listed in its own description. " +
21
+ "Use it for tool composition, parsing a payload, or computing over a file. Write plain " +
23
22
  "JavaScript — the sandbox evaluates " +
24
23
  "it directly, so TypeScript type annotations (`: number`, `as Type`) are a syntax error, and there " +
25
24
  "is no Python interpreter or package manager to invoke. Use the JS/Workers equivalent of what you " +
@@ -58,8 +57,7 @@ export const TOOLS =
58
57
  "use state.* for workspace file operations, tools.* for other host capabilities, and sequential calls for repeated or durable operations. " +
59
58
  "Top-level-only Tools remain Direct even when called repeatedly. " +
60
59
  "For web tasks, use web_search for web discovery, current facts, cited research, " +
61
- "and public URL analysis. For web access specifically, use execute Code Mode only for raw or customized network requests, structured " +
62
- "API calls, or when web_search cannot retrieve the required content; do not use execute for ordinary web " +
60
+ "and public URL analysis; do not use execute for ordinary web " +
63
61
  "searches. When execute is absent, use the actually exposed Direct Tools. When sandbox_* tools are present, use that isolated Linux environment for Python/Node, " +
64
62
  "package managers, system commands, builds, tests and background processes; its filesystem is temporary, " +
65
63
  "persistent inputs are copied in automatically on first use, /userspace is read-only, and only explicitly " +
@@ -70,7 +68,10 @@ export const TOOLS =
70
68
  "prioritize verification, saving durable results, and the final response. " +
71
69
  "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; " +
72
70
  "only independent top-level-only Direct Tools should be issued together in one turn. When you reference " +
73
- "code, cite it as file_path:line_number.";
71
+ "code, cite it as file_path:line_number. " +
72
+ "When schedule tools are available, prepare a standalone Requirement (at most 1000 Unicode characters), a name (at most 50), a time rule and an explicit timezone. Ask for missing information. " +
73
+ "Use local once/daily/weekly rules for new scheduled tasks. The Host prepares the authorization scope and the user must confirm before saving; never claim a task was created before the Tool succeeds. " +
74
+ "One source Chat may create multiple tasks. Before editing, list tasks and use the exact Task ID; ask when the target is ambiguous. Never attach source history or hidden context to the Requirement.";
74
75
 
75
76
  // A real browser is the only way to observe what a page actually does, and the
76
77
  // shared failure mode is a browser tool that returns nothing and looks clean.
@@ -117,10 +118,8 @@ export const INTERACTION =
117
118
  "without that answer; required defaults to false. At most one question may request attachments. Do not add an accept field or tell the user to upload through the normal chat composer; " +
118
119
  "continue the same turn when its result arrives. " +
119
120
  "Don't use it when a sensible default lets you proceed. " +
120
- "After completing a substantive task (report, analysis, multi-step job), finish all work and other Tool calls, " +
121
- "then call suggest_followups (2-4 directions) as the last Tool call before your final answer. " +
122
- "After it returns, write your complete final answer and make no further Tool calls. Never for small talk or while a " +
123
- "task is still in progress; it is non-blocking, unlike ask_user.";
121
+ "When a substantive task (report, analysis, or multi-step job) is complete, call suggest_followups once before the final answer. " +
122
+ "Never use it for small talk, quick answers, or while a task is still in progress; it is non-blocking, unlike ask_user.";
124
123
 
125
124
  // Separates short always-on memory from indexed long-tail workspace files.
126
125
  export const MEMORY =
@@ -0,0 +1,8 @@
1
+ /** Conservative token estimate for prompt budgeting, not provider billing. */
2
+ export function estimateStringTokens(text: string): number {
3
+ if (!text) return 0;
4
+ return Math.ceil(Math.max(
5
+ text.length / 4,
6
+ text.split(/\s+/u).filter(Boolean).length * 1.3,
7
+ ));
8
+ }
@@ -2,6 +2,7 @@ import type { StreamFn } from "@earendil-works/pi-agent-core";
2
2
  import {
3
3
  createModels,
4
4
  createProvider,
5
+ getCurrentSystemPrompt,
5
6
  isRetryableAssistantError,
6
7
  lazyStream,
7
8
  type Api,
@@ -11,6 +12,8 @@ import {
11
12
  type Model,
12
13
  type MutableModels,
13
14
  type ProviderStreams,
15
+ type SimpleStreamOptions,
16
+ type TranscriptContext,
14
17
  } from "@earendil-works/pi-ai";
15
18
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
16
19
  import {
@@ -38,7 +41,7 @@ import {
38
41
  openaiCodexProvider,
39
42
  } from "@earendil-works/pi-ai/providers/openai-codex";
40
43
  import {
41
- cloudflareStreams,
44
+ resolveCloudflareModel,
42
45
  } from "@earendil-works/pi-ai/providers/cloudflare-stream";
43
46
  import {
44
47
  ChatStreamStalledError,
@@ -51,6 +54,10 @@ import type {
51
54
  } from "../config/ports";
52
55
  import { modelInputMessages } from "../conversation/message/model-input";
53
56
  import { openRouterMessagesApi } from "./openrouter/messages";
57
+ import {
58
+ overlayProviderStreams,
59
+ remapProviderStreamModels,
60
+ } from "./provider-streams";
54
61
 
55
62
  const CATALOGS = {
56
63
  "openai-chat": openaiProvider().getModels(),
@@ -130,6 +137,12 @@ export type PiGenerationLifecycleObserver = (
130
137
  event: PiGenerationLifecycleEvent,
131
138
  ) => void;
132
139
 
140
+ type ProviderBoundStream = (
141
+ model: Model<Api>,
142
+ context: Context | TranscriptContext,
143
+ options?: SimpleStreamOptions,
144
+ ) => ReturnType<StreamFn>;
145
+
133
146
  export function structuredOutputStream(
134
147
  streamFn: StreamFn,
135
148
  model: Model<Api>,
@@ -490,16 +503,19 @@ export function modelRequestUrl(model: Model<Api>): string {
490
503
  * observer 的异常被隔离,不能改变模型执行结果。
491
504
  */
492
505
  export function withProviderBoundary(
493
- streamFn: StreamFn,
506
+ streamFn: ProviderBoundStream,
494
507
  defaultSessionId?: string,
495
508
  onGeneration?: PiGenerationLifecycleObserver,
496
- ): StreamFn {
509
+ ): ProviderBoundStream {
497
510
  return (model, context, options) =>
498
511
  lazyStream(model, async () => {
499
512
  const requestId = crypto.randomUUID();
500
513
  const startedAt = Date.now();
501
514
  const sessionId = options?.sessionId ?? defaultSessionId;
502
515
  const url = modelRequestUrl(model);
516
+ const systemPrompt = "systemPrompt" in context && typeof context.systemPrompt === "string"
517
+ ? context.systemPrompt
518
+ : getCurrentSystemPrompt(context.messages);
503
519
  const notify = (event: PiGenerationLifecycleEvent) => {
504
520
  try {
505
521
  onGeneration?.(event);
@@ -516,7 +532,7 @@ export function withProviderBoundary(
516
532
  model: model.id,
517
533
  url,
518
534
  input: {
519
- ...(context.systemPrompt ? { systemPrompt: context.systemPrompt } : {}),
535
+ ...(systemPrompt ? { systemPrompt } : {}),
520
536
  messages: context.messages,
521
537
  },
522
538
  });
@@ -708,7 +724,10 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
708
724
  return openAICodexResponsesApi();
709
725
  }
710
726
  })();
711
- const contextFor = (model: Model<Api>, context: Context): Context => ({
727
+ const contextFor = (
728
+ model: Model<Api>,
729
+ context: TranscriptContext,
730
+ ): TranscriptContext => ({
712
731
  ...context,
713
732
  messages: modelInputMessages(
714
733
  context.messages,
@@ -716,12 +735,12 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
716
735
  adapterHostedInputModalities(protocol),
717
736
  ),
718
737
  });
719
- return {
738
+ return overlayProviderStreams(api, {
720
739
  stream: (model, context, options) =>
721
740
  api.stream(model, contextFor(model, context), options),
722
741
  streamSimple: (model, context, options) =>
723
742
  api.streamSimple(model, contextFor(model, context), options),
724
- };
743
+ });
725
744
  }
726
745
 
727
746
  function anthropicMessagesModelForOpenRouter(
@@ -790,6 +809,7 @@ function configuredModel(
790
809
  ? {
791
810
  compat: {
792
811
  ...anthropicMessagesCompat,
812
+ // OpenRouter rejects Anthropic's mid-conversation effort messages and block binding.
793
813
  supportsMidConvoEffort: false,
794
814
  supportsEagerToolInputStreaming: false,
795
815
  supportsStrictTools:
@@ -865,9 +885,11 @@ export function configurePiModels(
865
885
  models: endpoint.models.map((modelId) =>
866
886
  configuredModel(endpoint, index, modelId),
867
887
  ),
868
- // cloudflareStreams 在运行时把 baseURL 里的 {CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID}
869
- // 占位符替换成真实值;如果 baseURL 不含占位符则直接透传,不影响现有端点。
870
- api: cloudflareStreams(piApiFor(endpoint)),
888
+ // remapProviderStreamModels 在运行时把 baseURL 里的
889
+ // {CLOUDFLARE_ACCOUNT_ID}/{CLOUDFLARE_GATEWAY_ID} 占位符替换成真实值;
890
+ // 如果 baseURL 不含占位符则直接透传。Pi 自带的 cloudflareStreams 只保留
891
+ // stream / streamSimple,这里 spread 其余方法以免丢掉 deferred 入口。
892
+ api: remapProviderStreamModels(piApiFor(endpoint), resolveCloudflareModel),
871
893
  })
872
894
  );
873
895
  models.clearProviders();
@@ -1,12 +1,14 @@
1
1
  import type {
2
- Context,
3
2
  Model,
4
3
  ProviderStreams,
5
4
  StreamOptions,
5
+ TranscriptContext,
6
6
  } from "@earendil-works/pi-ai";
7
+ import { getCurrentTools } from "@earendil-works/pi-ai";
7
8
  import {
8
9
  anthropicMessagesApi,
9
10
  } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
11
+ import { overlayProviderStreams } from "../provider-streams";
10
12
 
11
13
  type Payload = Record<string, unknown>;
12
14
 
@@ -21,14 +23,16 @@ function record(value: unknown): Payload | undefined {
21
23
  function decoratePayload(
22
24
  payload: unknown,
23
25
  model: Model<"anthropic-messages">,
24
- context: Context,
26
+ context: TranscriptContext,
25
27
  sessionId?: string,
26
28
  ): unknown {
27
29
  const body = record(payload);
28
30
  if (!body) throw new Error("OpenRouter Messages payload must be an object");
29
31
 
30
32
  const deferredNames = new Set(
31
- context.tools?.filter((tool) => tool.deferLoading).map((tool) => tool.name),
33
+ getCurrentTools(context.messages)
34
+ .filter((tool) => tool.deferLoading)
35
+ .map((tool) => tool.name),
32
36
  );
33
37
  const tools = Array.isArray(body.tools) ? body.tools : [];
34
38
  const immediate: unknown[] = [];
@@ -80,7 +84,7 @@ function decoratePayload(
80
84
 
81
85
  function openRouterOptions(
82
86
  model: Model<"anthropic-messages">,
83
- context: Context,
87
+ context: TranscriptContext,
84
88
  options: StreamOptions | undefined,
85
89
  ): StreamOptions {
86
90
  const apiKey = options?.apiKey;
@@ -107,9 +111,10 @@ function openRouterOptions(
107
111
  };
108
112
  }
109
113
 
110
- export function openRouterMessagesApi(): ProviderStreams {
111
- const anthropic = anthropicMessagesApi();
112
- return {
114
+ export function openRouterMessagesApi(
115
+ anthropic: ProviderStreams = anthropicMessagesApi(),
116
+ ): ProviderStreams {
117
+ return overlayProviderStreams(anthropic, {
113
118
  stream: (model, context, options) =>
114
119
  anthropic.stream(
115
120
  model,
@@ -130,5 +135,5 @@ export function openRouterMessagesApi(): ProviderStreams {
130
135
  options,
131
136
  ),
132
137
  ),
133
- };
138
+ });
134
139
  }