@springbrand/agent-runtime 0.1.3-alpha.4 → 0.1.3-alpha.5

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 (34) hide show
  1. package/package.json +3 -1
  2. package/src/adapter/cloudflare/index.ts +60 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +86 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1509 -0
  5. package/src/adapter/cloudflare/sandbox/id.ts +23 -0
  6. package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
  7. package/src/adapter/cloudflare/subagent/definition.ts +574 -0
  8. package/src/adapter/cloudflare/subagent/runner.ts +175 -0
  9. package/src/adapter/cloudflare/subagent/tools.ts +256 -0
  10. package/src/adapter/cloudflare/universal-agent/definition.ts +71 -0
  11. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  12. package/src/adapter/cloudflare/universal-agent/preparation.ts +273 -0
  13. package/src/adapter/cloudflare/universal-agent/tools.ts +74 -0
  14. package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
  15. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
  16. package/src/agent-tool-runtime.ts +152 -0
  17. package/src/index.ts +49 -7
  18. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  19. package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
  20. package/src/pi/message/contract.ts +7 -0
  21. package/src/pi/message/conversion.ts +9 -1
  22. package/src/pi/runtime-adapter/assembly.ts +4 -2
  23. package/src/pi/runtime-adapter/index.ts +6 -2
  24. package/src/pi/tool/base.ts +17 -0
  25. package/src/pi/tool/core.ts +13 -0
  26. package/src/pi/tool/schedule.ts +11 -0
  27. package/src/pi/tool/subagent.ts +14 -0
  28. package/src/pi/tool/workspace-sandbox.ts +15 -0
  29. package/src/runtime-agent-context.ts +112 -0
  30. package/src/runtime-agent.ts +429 -315
  31. package/src/runtime-assembler.ts +249 -99
  32. package/src/runtime-definition.ts +173 -0
  33. package/src/runtime.ts +139 -12
  34. package/src/tool-registry.ts +143 -0
package/src/runtime.ts CHANGED
@@ -49,8 +49,11 @@ import { RuntimeLoadTracker } from "./kernel/runtime-load";
49
49
  import {
50
50
  prepareRuntimeCandidate,
51
51
  type RuntimeAssemblyInput,
52
+ type RuntimeCandidate,
52
53
  type RuntimeSnapshot,
53
54
  } from "./runtime-assembler";
55
+ import type { RuntimeAgentHooks } from "./runtime-definition";
56
+ import type { RuntimeTurnEventsPort } from "./kernel/bindings";
54
57
  import { connectConfiguredMcpServers } from "./lib/mcp";
55
58
  import { installConsoleSink } from "./lib/telemetry-dev";
56
59
  import {
@@ -62,6 +65,7 @@ import {
62
65
  type PiCanonicalTranscriptSnapshot,
63
66
  type PiChatRecoveryData,
64
67
  type UIChatRequestBody,
68
+ type RequestedCapability,
65
69
  type PiDurableMutation,
66
70
  type PiRecoveryCommand,
67
71
  type PiRecoveryDecision,
@@ -221,6 +225,44 @@ function userContentKey(message: PiCanonicalUserInput): string {
221
225
  );
222
226
  }
223
227
 
228
+ function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
229
+ if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
230
+ return [];
231
+ }
232
+ const record = metadata as Record<string, unknown>;
233
+ if (!("requestedCapabilities" in record)) return [];
234
+ if (!Array.isArray(record.requestedCapabilities)) {
235
+ throw new Error("requestedCapabilities must be an array");
236
+ }
237
+
238
+ const result: RequestedCapability[] = [];
239
+ const seen = new Set<string>();
240
+ for (const value of record.requestedCapabilities) {
241
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
242
+ throw new Error("requestedCapabilities contains an invalid capability");
243
+ }
244
+ const capability = value as Record<string, unknown>;
245
+ if (
246
+ (capability.kind !== "skill" && capability.kind !== "plan") ||
247
+ typeof capability.name !== "string" ||
248
+ !capability.name.trim() ||
249
+ typeof capability.label !== "string" ||
250
+ !capability.label.trim()
251
+ ) {
252
+ throw new Error("requestedCapabilities contains an invalid capability");
253
+ }
254
+ const key = `${capability.kind}:${capability.name}`;
255
+ if (seen.has(key)) continue;
256
+ seen.add(key);
257
+ result.push({
258
+ kind: capability.kind,
259
+ name: capability.name,
260
+ label: capability.label,
261
+ });
262
+ }
263
+ return result;
264
+ }
265
+
224
266
  interface PendingTemporaryAgentApproval {
225
267
  receipt: ApprovalReceipt;
226
268
  resolve(decision: TemporaryAgentApprovalDecision): void;
@@ -257,6 +299,8 @@ export abstract class AgentRuntimeKernel<
257
299
  private runtimeRevision?: string;
258
300
  private runtimePi?: PreparedPiRuntime;
259
301
  private runtimeGatewaySession?: RuntimeGatewaySession;
302
+ private runtimeTurnEvents?: RuntimeTurnEventsPort;
303
+ private runtimeHooks?: RuntimeAgentHooks;
260
304
  private piAdapter?: PiRuntimeAdapter;
261
305
  private readonly transcript: PiRuntimeTranscript;
262
306
  private readonly submissions: SubmissionLifecycle<
@@ -462,11 +506,21 @@ export abstract class AgentRuntimeKernel<
462
506
  // 调用:生成 Agent 在首次启动或显式重载 Runtime key 时调用。
463
507
  // 原因:先完整 prepare 再替换 Snapshot,且活跃 Turn 期间禁止换 revision,可避免半装配和恢复时能力漂移。
464
508
  protected async initConfig(input: RuntimeAssemblyInput): Promise<void> {
465
- const previous = this.runtimeSnapshot;
466
- this.runtimeLoad.advance("assembly");
467
509
  const candidate = await prepareRuntimeCandidate(input);
468
- const candidateSnapshot = candidate.snapshot;
510
+ await this.initCandidate(candidate);
511
+ }
512
+
513
+ private turnEventsPort(): RuntimeTurnEventsPort | undefined {
514
+ return this.runtimeTurnEvents ?? this.runtimeSnapshot?.bindings.turnEvents;
515
+ }
469
516
 
517
+ protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
518
+ this.runtimeTurnEvents =
519
+ candidate.turnEvents ?? candidate.snapshot.bindings.turnEvents;
520
+ this.runtimeHooks = candidate.hooks;
521
+ const previous = this.runtimeSnapshot;
522
+ const candidateSnapshot = candidate.snapshot;
523
+ this.runtimeLoad.advance("assembly");
470
524
  this.runtimeLoad.advance("mcp");
471
525
  await connectConfiguredMcpServers(
472
526
  this,
@@ -601,7 +655,7 @@ export abstract class AgentRuntimeKernel<
601
655
  async (created) => {
602
656
  await onCreated?.();
603
657
  try {
604
- await this.assembly().bindings.turnEvents?.onApproval?.({
658
+ await this.turnEventsPort()?.onApproval?.({
605
659
  submissionId: submission.submissionId,
606
660
  approvalExecutionId: created.executionId,
607
661
  });
@@ -727,7 +781,7 @@ export abstract class AgentRuntimeKernel<
727
781
  }
728
782
 
729
783
  private async drainRuntimeEvents(idempotentRetry = true): Promise<void> {
730
- const turnEvents = this.runtimeSnapshot?.bindings.turnEvents;
784
+ const turnEvents = this.turnEventsPort();
731
785
  if (!turnEvents) return;
732
786
 
733
787
  let failed = false;
@@ -862,6 +916,41 @@ export abstract class AgentRuntimeKernel<
862
916
  return this.runtimeSnapshot;
863
917
  }
864
918
 
919
+ private async normalizeUIUserInput(
920
+ message: UIMessage & { role: "user" },
921
+ ): Promise<PiCanonicalUserInput> {
922
+ const capabilities = requestedCapabilitiesOf(message.metadata);
923
+ if (capabilities.length === 0) return this.pi.normalizeUserInput(message);
924
+
925
+ await this.ensureRuntimeReady();
926
+ const installedSkills = new Set(
927
+ this.assembly().bindings.skills.sources.map(({ name }) => name),
928
+ );
929
+ const context: string[] = [];
930
+ for (const capability of capabilities) {
931
+ if (capability.kind === "skill") {
932
+ if (!installedSkills.has(capability.name)) {
933
+ throw new Error(
934
+ `Requested Skill is not installed: ${capability.name}`,
935
+ );
936
+ }
937
+ context.push(
938
+ `requested capability: skill/${capability.name}`,
939
+ `required action: call activate_skill for "${capability.name}" before handling the task`,
940
+ );
941
+ continue;
942
+ }
943
+ if (capability.name !== "plan") {
944
+ throw new Error(`Unknown plan capability: ${capability.name}`);
945
+ }
946
+ context.push(
947
+ "requested capability: plan/plan",
948
+ "required action: call update_plan with the complete plan before other work, then wait for user confirmation before execution",
949
+ );
950
+ }
951
+ return this.pi.normalizeUserInput(message, context.join("\n"));
952
+ }
953
+
865
954
  // 作用:把当前已安装的 Runtime Snapshot 投影成可序列化的调试视图。
866
955
  // 调用:Session facet 的 `getRuntimeAssembly` RPC。
867
956
  // 原因:UI 需要读取真实装配结果,而不是上层配置声明。
@@ -2246,7 +2335,7 @@ export abstract class AgentRuntimeKernel<
2246
2335
  messages,
2247
2336
  }),
2248
2337
  );
2249
- const events = this.assembly().bindings.turnEvents;
2338
+ const events = this.turnEventsPort();
2250
2339
  if (events) {
2251
2340
  try {
2252
2341
  await events.onResponse(
@@ -2395,7 +2484,7 @@ export abstract class AgentRuntimeKernel<
2395
2484
  ReturnType<AgentRuntimeKernel["submitMessage"]>
2396
2485
  >;
2397
2486
  try {
2398
- const normalized = this.pi.normalizeUserInput(latest);
2487
+ const normalized = await this.normalizeUIUserInput(latest);
2399
2488
  submitted = await this.submitMessage(normalized, {
2400
2489
  requestId: event.id,
2401
2490
  idempotencyKey: event.id,
@@ -2480,6 +2569,18 @@ export abstract class AgentRuntimeKernel<
2480
2569
  return this.transcript.browserMessages();
2481
2570
  }
2482
2571
 
2572
+ /** 读取当前 canonical transcript 中最后一条助手文本。 */
2573
+ protected async latestAssistantText(): Promise<string | undefined> {
2574
+ const message = [...await this.transcript.canonicalMessages()]
2575
+ .reverse()
2576
+ .find((entry): entry is AssistantMessage => entry.role === "assistant");
2577
+ const text = message?.content
2578
+ .flatMap((part) => part.type === "text" ? [part.text] : [])
2579
+ .join("")
2580
+ .trim();
2581
+ return text || undefined;
2582
+ }
2583
+
2483
2584
  // #endregion
2484
2585
 
2485
2586
  // #region Runtime Extension Host 回环端口
@@ -2701,9 +2802,18 @@ export abstract class AgentRuntimeKernel<
2701
2802
  };
2702
2803
  }
2703
2804
 
2704
- const userMessage = this.pi.normalizeUserInput(
2705
- message as UIMessage & { role: "user" },
2706
- );
2805
+ let userMessage: PiCanonicalUserInput;
2806
+ try {
2807
+ userMessage = await this.normalizeUIUserInput(
2808
+ message as UIMessage & { role: "user" },
2809
+ );
2810
+ } catch (error) {
2811
+ return {
2812
+ kind: "rejected",
2813
+ code: "invalid_message",
2814
+ message: errorText(error),
2815
+ };
2816
+ }
2707
2817
  // 停在 interaction park 上的 Turn 一律按 steer 处理,哪怕客户端发的是 enqueue:
2708
2818
  // enqueue 要等本 Turn 结束,而本 Turn 正在等一个永远不会来的答案 —— 死锁。
2709
2819
  const parked = this.findInteractionParkedSubmission();
@@ -3194,6 +3304,24 @@ export abstract class AgentRuntimeKernel<
3194
3304
  await this.broadcastApprovals();
3195
3305
  }
3196
3306
 
3307
+ async _cfDetachedNotifyFinish(
3308
+ run: AgentToolRunInfo,
3309
+ result: AgentToolLifecycleResult,
3310
+ ): Promise<void> {
3311
+ const outcome = result.status === "completed"
3312
+ ? result.summary ?? "Completed without a result."
3313
+ : result.error ?? `Background run ${result.status}.`;
3314
+ await this.submitPrompt(
3315
+ [
3316
+ "A background sub-agent run has finished.",
3317
+ `runId: ${run.runId}`,
3318
+ `status: ${result.status}`,
3319
+ `result: ${outcome}`,
3320
+ ].join("\n"),
3321
+ { idempotencyKey: `detached-agent-tool:${run.runId}:${result.status}` },
3322
+ );
3323
+ }
3324
+
3197
3325
  // 作用:把审批、队列和 Session 活动投影到 Agent 可广播状态。
3198
3326
  // 调用:启动、准入、审批变化、interaction 变化、子运行变化和 Turn 完成时调用。
3199
3327
  // 原因:这些都是可重建的 UI 投影,相同内容不重复 `setState`,投影失败也不能阻断执行。
@@ -3306,8 +3434,7 @@ export abstract class AgentRuntimeKernel<
3306
3434
  turn,
3307
3435
  });
3308
3436
  }
3309
- const projection = this.runtimeSnapshot?.bindings.turnEvents
3310
- ?.onActivityChanged?.(nextActivity);
3437
+ const projection = this.turnEventsPort()?.onActivityChanged?.(nextActivity);
3311
3438
  if (projection) {
3312
3439
  this.ctx.waitUntil(
3313
3440
  projection.catch(() => undefined),
@@ -0,0 +1,143 @@
1
+ import type { ExecutionLevel } from "./lib/execution-level";
2
+ import type {
3
+ RuntimeMemoryPort,
4
+ WorkspacePort,
5
+ } from "./kernel/bindings";
6
+ import type { RuntimeExtensionConfig } from "./kernel/extensions";
7
+ import type { RuntimeDegradation } from "./kernel/degradation";
8
+ import type { PiToolCandidate } from "./pi/tool/compiler";
9
+
10
+ /** Tool Surface 筛选策略(不含 Manifest deny 列表)。 */
11
+ export interface ToolSurfaceSelectionPolicy {
12
+ readonly allowsTool?: (name: string) => boolean;
13
+ readonly allowsExtension?: (extension: RuntimeExtensionConfig) => boolean;
14
+ }
15
+
16
+ /** 模型调用 Tool 时传给 `execute` 的上下文。 */
17
+ export interface ToolContext {
18
+ readonly toolCallId: string;
19
+ readonly signal: AbortSignal;
20
+ }
21
+
22
+ /** Definition 作者声明的一个 Tool。 */
23
+ export interface ToolSpec {
24
+ readonly label: string;
25
+ readonly description: string;
26
+ readonly parameters: unknown;
27
+ readonly requiredExecutionLevel: ExecutionLevel;
28
+ /** Require a fresh human decision for every call, regardless of execution level. */
29
+ readonly alwaysRequiresApproval?: boolean;
30
+ readonly execute: (
31
+ input: unknown,
32
+ ctx: ToolContext,
33
+ ) => Promise<unknown>;
34
+ }
35
+
36
+ /** 一次装配声明的全部 Tool,键即模型可见的工具名。 */
37
+ export type ToolRegistry = Record<string, ToolSpec>;
38
+
39
+ export function emptyToolRegistry(): ToolRegistry {
40
+ return {};
41
+ }
42
+
43
+ export function mergeToolRegistries(...registries: ToolRegistry[]): ToolRegistry {
44
+ const merged: ToolRegistry = {};
45
+ for (const registry of registries) {
46
+ for (const [name, spec] of Object.entries(registry)) {
47
+ if (name in merged) {
48
+ throw new Error(`Duplicate Runtime Tool: ${name}`);
49
+ }
50
+ merged[name] = spec;
51
+ }
52
+ }
53
+ return merged;
54
+ }
55
+
56
+ export function toolRegistryFromPiCandidates(
57
+ candidates:
58
+ | readonly PiToolCandidate[]
59
+ | Record<string, PiToolCandidate>,
60
+ ): ToolRegistry {
61
+ const list = Array.isArray(candidates)
62
+ ? candidates
63
+ : Object.values(candidates);
64
+ const registry: ToolRegistry = {};
65
+ for (const candidate of list) {
66
+ const name = candidate.tool.name;
67
+ if (!name.trim()) {
68
+ throw new Error("Pi Tool name must not be empty");
69
+ }
70
+ registry[name] = piCandidateToToolSpec(candidate);
71
+ }
72
+ return registry;
73
+ }
74
+
75
+ export function piCandidateToToolSpec(candidate: PiToolCandidate): ToolSpec {
76
+ const name = candidate.tool.name;
77
+ return {
78
+ label: candidate.tool.label ?? name,
79
+ description: candidate.tool.description,
80
+ parameters: candidate.tool.parameters,
81
+ requiredExecutionLevel: candidate.requiredExecutionLevel,
82
+ ...(candidate.alwaysRequiresApproval
83
+ ? { alwaysRequiresApproval: true }
84
+ : {}),
85
+ execute: async (input, ctx) => {
86
+ const execute = candidate.tool.execute;
87
+ if (typeof execute !== "function") {
88
+ throw new Error(`Tool "${name}" is not executable`);
89
+ }
90
+ const result = await execute(
91
+ ctx.toolCallId,
92
+ input,
93
+ ctx.signal,
94
+ undefined,
95
+ );
96
+ return result.details ?? result;
97
+ },
98
+ };
99
+ }
100
+
101
+ /** Kernel 绑定仍需要的执行后端;不出现在 Definition 公开类型里。 */
102
+ export interface RuntimeToolBindings {
103
+ readonly workspace?: WorkspacePort;
104
+ readonly memory?: RuntimeMemoryPort;
105
+ }
106
+
107
+ /**
108
+ * Worker `tools()` 可返回纯注册表,或附带 Kernel 绑定与降级信息。
109
+ */
110
+ export interface ToolAssemblyResult {
111
+ readonly tools: ToolRegistry;
112
+ readonly bindings?: RuntimeToolBindings;
113
+ readonly memoryProfile?: import("./kernel/profile").RuntimeMemoryProfile;
114
+ readonly enabledSubagents?: readonly string[];
115
+ readonly degradations?: readonly RuntimeDegradation[];
116
+ readonly surfacePolicy?: ToolSurfaceSelectionPolicy;
117
+ readonly commitGuard?: () => Promise<void>;
118
+ /** 释放本次装配创建的外部资源;临时 Agent 终止后由 Runtime 调用。 */
119
+ readonly dispose?: () => Promise<void>;
120
+ }
121
+
122
+ export function normalizeToolAssembly(
123
+ input: ToolRegistry | ToolAssemblyResult,
124
+ ): ToolAssemblyResult {
125
+ if ("tools" in input && typeof input.tools === "object" && input.tools !== null) {
126
+ return input as ToolAssemblyResult;
127
+ }
128
+ return { tools: input as ToolRegistry };
129
+ }
130
+
131
+ /** @deprecated 使用 `ToolSpec`。 */
132
+ export type PlatformToolSpec = ToolSpec;
133
+
134
+ /** @deprecated 使用 `ToolContext`。 */
135
+ export type PlatformToolContext = ToolContext;
136
+
137
+ /** @deprecated 使用 `ToolRegistry`。 */
138
+ export type PlatformToolRegistry = ToolRegistry;
139
+
140
+ /** @deprecated 使用 `emptyToolRegistry`。 */
141
+ export function emptyPlatformToolRegistry(): ToolRegistry {
142
+ return emptyToolRegistry();
143
+ }