@springbrand/agent-runtime 0.1.3-alpha.0 → 0.1.3-alpha.10

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 (79) hide show
  1. package/package.json +12 -3
  2. package/src/adapter/cloudflare/index.ts +56 -0
  3. package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
  4. package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -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 +254 -0
  10. package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
  11. package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
  12. package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
  13. package/src/adapter/cloudflare/workspace/git-fs.ts +178 -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/adapter/cloudflare/workspace/version-control.ts +374 -0
  17. package/src/agent-tool-runtime.ts +152 -0
  18. package/src/db/agent-tool.repo.ts +27 -0
  19. package/src/db/index.ts +33 -0
  20. package/src/db/interaction.repo.ts +185 -0
  21. package/src/db/schema.ts +25 -1
  22. package/src/db/submission.repo.ts +63 -1
  23. package/src/index.ts +61 -27
  24. package/src/kernel/approval-lifecycle.ts +41 -6
  25. package/src/kernel/bindings.ts +99 -12
  26. package/src/kernel/extensions.ts +1 -1
  27. package/src/kernel/interaction-lifecycle.ts +395 -0
  28. package/src/kernel/profile.ts +3 -4
  29. package/src/kernel/public-contracts.ts +2 -0
  30. package/src/kernel/recoverable-chat-agent.ts +104 -6
  31. package/src/kernel/runtime-assembly-view.ts +37 -0
  32. package/src/kernel/runtime-assembly.ts +41 -0
  33. package/src/kernel/runtime-config.ts +4 -0
  34. package/src/kernel/runtime-load.ts +191 -0
  35. package/src/kernel/state.ts +12 -1
  36. package/src/kernel/submission-lifecycle.ts +33 -2
  37. package/src/layers/orchestration/temporary-agent/core.ts +12 -1
  38. package/src/layers/orchestration/temporary-agent/runner.ts +1 -67
  39. package/src/lib/mcp.ts +7 -3
  40. package/src/lib/prompt.ts +1 -1
  41. package/src/lib/telemetry-dev.ts +7 -4
  42. package/src/pi/assembly/context.ts +4 -6
  43. package/src/pi/assembly/extensions.ts +11 -22
  44. package/src/pi/assembly/snapshot.ts +17 -9
  45. package/src/pi/message/contract.ts +7 -0
  46. package/src/pi/message/conversion.ts +9 -1
  47. package/src/pi/runtime-adapter/assembly.ts +42 -97
  48. package/src/pi/runtime-adapter/execution.ts +216 -21
  49. package/src/pi/runtime-adapter/index.ts +24 -8
  50. package/src/pi/runtime-adapter/models.ts +382 -35
  51. package/src/pi/runtime-adapter/recovery.ts +188 -1
  52. package/src/pi/runtime-adapter/transcript.ts +61 -3
  53. package/src/pi/tool/ai-adapter.ts +58 -1
  54. package/src/pi/tool/base.ts +190 -12
  55. package/src/pi/tool/compiler.ts +39 -33
  56. package/src/pi/tool/core-host.ts +28 -30
  57. package/src/pi/tool/core.ts +37 -124
  58. package/src/pi/tool/gateway.ts +54 -0
  59. package/src/pi/tool/index.ts +2 -0
  60. package/src/pi/tool/mcp.ts +98 -70
  61. package/src/pi/tool/schedule.ts +86 -20
  62. package/src/pi/tool/skill.ts +126 -420
  63. package/src/pi/tool/subagent.ts +14 -2
  64. package/src/pi/tool/web-fetch.ts +281 -0
  65. package/src/pi/tool/web-search/api.ts +34 -18
  66. package/src/pi/tool/web-search/web-search.ts +0 -1
  67. package/src/pi/tool/workspace-revision.ts +64 -0
  68. package/src/pi/tool/workspace-sandbox.ts +105 -263
  69. package/src/pi/turn/index.ts +20 -0
  70. package/src/pi/turn/interaction.ts +181 -0
  71. package/src/pi/turn/tool-recovery.ts +244 -1
  72. package/src/runtime-agent-context.ts +112 -0
  73. package/src/runtime-agent.ts +568 -321
  74. package/src/runtime-assembler.ts +797 -0
  75. package/src/runtime-definition.ts +175 -0
  76. package/src/runtime.ts +840 -208
  77. package/src/tool-registry.ts +143 -0
  78. package/src/workspace-versioning.ts +46 -0
  79. package/src/plugins.ts +0 -1033
@@ -73,15 +73,15 @@ export type RuntimeRecoveryDecision<Data> =
73
73
  | { readonly kind: "ignore"; readonly reason: string };
74
74
 
75
75
  /**
76
- * 记录一次已执行恢复的最终结果。
76
+ * 记录一次已执行恢复的结果,或表明该恢复已接力给下一次持久调度。
77
77
  *
78
78
  * @remarks
79
- * `_chatRecoveryRetry` 在调用端口的 `retry` 后读取该结果,并用它关闭对应的恢复事故。
79
+ * `_chatRecoveryRetry` 在调用端口的 `retry` 后读取该结果;终态关闭事故,`scheduled` 保持事故活跃。
80
80
  *
81
81
  * 只有失败时才需要 `error`;结果不承载流内容,避免在调度数据与流缓冲区之间复制状态。
82
82
  */
83
83
  export interface RuntimeRecoveryResult {
84
- readonly status: "completed" | "failed";
84
+ readonly status: "completed" | "scheduled" | "failed";
85
85
  readonly error?: string;
86
86
  }
87
87
 
@@ -103,7 +103,7 @@ export interface RuntimeRecoveryPort<Data> {
103
103
  /**
104
104
  * 使用已分类的业务数据重放一次对话。
105
105
  *
106
- * @remarks 持久调度回调会调用它;实现应返回权威终态,而不是仅表示重试已启动。
106
+ * @remarks 持久调度回调会调用它;实现应返回权威终态,或明确说明下一次恢复已经持久调度。
107
107
  */
108
108
  retry(data: Data): Promise<RuntimeRecoveryResult>;
109
109
  /**
@@ -353,7 +353,13 @@ export abstract class RecoverableChatAgent<
353
353
  * 清理延后执行,为断线重连的客户端保留终态流片段。
354
354
  */
355
355
  protected completeRecoverableStream(streamId: string): void {
356
+ const completedRequestId = this.resumableStream.activeRequestId;
356
357
  this.resumableStream.complete(streamId);
358
+ this.pendingResumeConnections.clear();
359
+ if (completedRequestId === this.continuation.activeRequestId) {
360
+ this.continuation.activeRequestId = null;
361
+ this.continuation.activeConnectionId = null;
362
+ }
357
363
  void this.ensureStreamCleanupScheduled();
358
364
  }
359
365
 
@@ -366,7 +372,13 @@ export abstract class RecoverableChatAgent<
366
372
  * 流错误标志与业务失败分开,因为续传协议和提交生命周期的责任不同。
367
373
  */
368
374
  protected failRecoverableStream(streamId: string): void {
375
+ const erroredRequestId = this.resumableStream.activeRequestId;
369
376
  this.resumableStream.markError(streamId);
377
+ this.pendingResumeConnections.clear();
378
+ if (erroredRequestId === this.continuation.activeRequestId) {
379
+ this.continuation.activeRequestId = null;
380
+ this.continuation.activeConnectionId = null;
381
+ }
370
382
  void this.ensureStreamCleanupScheduled();
371
383
  }
372
384
 
@@ -465,13 +477,20 @@ export abstract class RecoverableChatAgent<
465
477
  * @remarks
466
478
  * 子类在发送增量记录、成功终态或失败终态时调用;`done` 表示轮次终态,`error` 只在终态失败时设置。
467
479
  *
480
+ * `continuation` 必须与本轮 `runRecoverableChatFiber` 的取值一致。重放帧由
481
+ * `ResumableStream` 从 `is_continuation` 列自动补上这个标记,实时帧只能由调用方给出:
482
+ * 少了它,客户端会为续跑另建一个空的累加器,把已经渲染出来的那半条回答整条换掉。
483
+ *
468
484
  * 广播显式排除正在续传握手的连接,避免它们在历史流重放完成前同时收到实时帧。
469
485
  */
470
486
  protected sendChatResponse(
471
487
  requestId: string,
472
488
  body: string,
473
489
  done: boolean,
474
- error = false,
490
+ options: {
491
+ readonly error?: boolean;
492
+ readonly continuation?: boolean;
493
+ } = {},
475
494
  ): void {
476
495
  this.broadcast(
477
496
  json({
@@ -479,7 +498,8 @@ export abstract class RecoverableChatAgent<
479
498
  id: requestId,
480
499
  body,
481
500
  done,
482
- ...(error ? { error: true } : {}),
501
+ ...(options.error ? { error: true } : {}),
502
+ ...(options.continuation ? { continuation: true } : {}),
483
503
  }),
484
504
  [...this.pendingResumeConnections],
485
505
  );
@@ -544,6 +564,7 @@ export abstract class RecoverableChatAgent<
544
564
  this.activeRecoveryRootRequestId = data.requestId;
545
565
  try {
546
566
  const result = await this.recoveryPort().retry(data);
567
+ if (result.status === "scheduled") return;
547
568
  await this.engine().updateIncident(
548
569
  data.incidentId,
549
570
  result.status === "completed" ? "completed" : "failed",
@@ -563,6 +584,83 @@ export abstract class RecoverableChatAgent<
563
584
  }
564
585
  }
565
586
 
587
+ /**
588
+ * 把活跃进程内检测到的模型流停滞交给同一套持久恢复预算。
589
+ *
590
+ * @remarks
591
+ * 子类在确认当前 Turn 可以从 durable checkpoint 重放后调用;`beforeSchedule`
592
+ * 必须先结束旧流并恢复权威消息快照,随后引擎才广播 recovering 状态。
593
+ */
594
+ protected async scheduleChatRecoveryRetry(
595
+ data: RecoveryData,
596
+ beforeSchedule: () => void | Promise<void>,
597
+ ): Promise<"disabled" | "scheduled" | "exhausted"> {
598
+ if (!resolveChatRecoveryConfig(this.chatRecovery).enabled) {
599
+ return "disabled";
600
+ }
601
+ const recoveryRootRequestId =
602
+ this.activeRecoveryRootRequestId ?? data.requestId;
603
+ const { incident, exhausted } = await this.engine().beginIncident({
604
+ requestId: data.requestId,
605
+ recoveryRootRequestId,
606
+ recoveryKind: "retry",
607
+ });
608
+ await beforeSchedule();
609
+ if (exhausted) {
610
+ await this.engine().exhaustRecoveryGiveUp({
611
+ callback: "_chatRecoveryRetry",
612
+ data: {
613
+ incidentId: incident.incidentId,
614
+ originalRequestId: recoveryRootRequestId,
615
+ },
616
+ reason: incident.reason ?? "max_attempts_exceeded",
617
+ });
618
+ return "exhausted";
619
+ }
620
+ await this.engine().scheduleRecovery({
621
+ incident,
622
+ recoveryKind: "retry",
623
+ callback: "_chatRecoveryRetry",
624
+ data: {
625
+ ...data,
626
+ incidentId: incident.incidentId,
627
+ },
628
+ reason: this.activeRecoveryRootRequestId
629
+ ? "stable_timeout_retry"
630
+ : "initial",
631
+ });
632
+ return "scheduled";
633
+ }
634
+
635
+ /** 终态化一个 Turn 时取消同请求的恢复调度,并关闭恢复事故与客户端状态。 */
636
+ protected async settleChatRecovery(
637
+ requestId: string,
638
+ status: "completed" | "skipped" | "failed",
639
+ reason?: string,
640
+ ): Promise<void> {
641
+ const incidentIds = new Set<string>();
642
+ try {
643
+ for (const schedule of await this.listSchedules()) {
644
+ if (schedule.callback !== "_chatRecoveryRetry") continue;
645
+ const payload = schedule.payload;
646
+ if (!payload || typeof payload !== "object") continue;
647
+ const recovery = payload as Partial<
648
+ RecoveryScheduleData<RecoveryData>
649
+ >;
650
+ if (recovery.requestId !== requestId) continue;
651
+ if (typeof recovery.incidentId === "string") {
652
+ incidentIds.add(recovery.incidentId);
653
+ }
654
+ await this.cancelSchedule(schedule.id);
655
+ }
656
+ for (const incidentId of incidentIds) {
657
+ await this.engine().updateIncident(incidentId, status, reason);
658
+ }
659
+ } finally {
660
+ await this.setRecovering(false, requestId);
661
+ }
662
+ }
663
+
566
664
  /**
567
665
  * 把 Agents SDK 发现的孤立 Fiber 交给聊天恢复引擎。
568
666
  *
@@ -0,0 +1,37 @@
1
+ import type { RuntimeDegradation } from "./degradation";
2
+ import type {
3
+ RuntimeMemoryProfile,
4
+ RuntimeMcpServer,
5
+ ThinkingEffort,
6
+ } from "./profile";
7
+ import type { ExecutionLevel } from "../lib/execution-level";
8
+
9
+ export interface RuntimeAssemblySkillBinding {
10
+ readonly name: string;
11
+ readonly description: string;
12
+ readonly script: {
13
+ readonly network: "none" | "full";
14
+ readonly workspace: "none" | "read" | "read-write";
15
+ readonly tools: readonly string[];
16
+ };
17
+ }
18
+
19
+ export interface RuntimeAssemblyExtension {
20
+ readonly name: string;
21
+ readonly version: string;
22
+ }
23
+
24
+ export interface RuntimeAssemblyView {
25
+ readonly revision: string | null;
26
+ readonly model: string;
27
+ readonly thinking: ThinkingEffort;
28
+ readonly executionLevel: ExecutionLevel;
29
+ readonly systemPrompt?: string;
30
+ readonly memory: RuntimeMemoryProfile;
31
+ readonly toolDeny: readonly string[];
32
+ readonly skills: readonly RuntimeAssemblySkillBinding[];
33
+ readonly extensions: readonly RuntimeAssemblyExtension[];
34
+ readonly subagents: readonly string[];
35
+ readonly mcpServers: readonly RuntimeMcpServer[];
36
+ readonly degradations: readonly RuntimeDegradation[];
37
+ }
@@ -0,0 +1,41 @@
1
+ import type { RuntimeSnapshot } from "../runtime-assembler";
2
+ import type { PreparedPiRuntime } from "../pi/runtime-adapter";
3
+ import type { RuntimeAssemblyView } from "./runtime-assembly-view";
4
+ export type * from "./runtime-assembly-view";
5
+
6
+ /**
7
+ * Serializable view of the Runtime Snapshot currently installed on a Session facet.
8
+ *
9
+ * @remarks
10
+ * UI debug panels and Host tooling call `getRuntimeAssembly` to read what the
11
+ * Agent actually assembled, not what upstream configuration merely declared.
12
+ */
13
+ export function projectRuntimeAssembly(
14
+ snapshot: RuntimeSnapshot,
15
+ revision: string | null,
16
+ prepared: Pick<PreparedPiRuntime, "degradations" | "extensions">,
17
+ ): RuntimeAssemblyView {
18
+ const { profile, bindings } = snapshot;
19
+ return {
20
+ revision,
21
+ model: profile.model,
22
+ thinking: profile.thinking,
23
+ executionLevel: profile.executionLevel,
24
+ ...(profile.systemPrompt ? { systemPrompt: profile.systemPrompt } : {}),
25
+ memory: { ...profile.memory },
26
+ toolDeny: [...(profile.denyPolicy?.deny ?? [])],
27
+ skills: bindings.skills.sources.map((binding) => ({
28
+ name: binding.name,
29
+ description: binding.description,
30
+ script: {
31
+ network: binding.script.network,
32
+ workspace: binding.script.workspace,
33
+ tools: [...binding.script.tools],
34
+ },
35
+ })),
36
+ extensions: prepared.extensions.map((extension) => ({ ...extension })),
37
+ subagents: [...profile.enabledSubagents],
38
+ mcpServers: profile.mcpServers.map((server) => ({ ...server })),
39
+ degradations: prepared.degradations.map((degradation) => ({ ...degradation })),
40
+ };
41
+ }
@@ -0,0 +1,4 @@
1
+ export interface RuntimeConfigUpdateResult<Change> {
2
+ readonly change: Change;
3
+ readonly runtime: "unchanged" | "reloaded" | "reload-failed";
4
+ }
@@ -0,0 +1,191 @@
1
+ import type { RuntimeLoadPhase, RuntimeLoadState } from "./state";
2
+
3
+ /** Hard ceiling for one Host-owned Runtime definition phase. */
4
+ export const RUNTIME_LOAD_TIMEOUT_MS = 180_000;
5
+
6
+ /** Hard ceiling for one optional external capability load. */
7
+ export const RUNTIME_CAPABILITY_LOAD_TIMEOUT_MS = 60_000;
8
+
9
+ const reportedFailures = new WeakSet<object>();
10
+
11
+ function errorText(error: unknown): string {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+
15
+ /** Emit one stable structured record for a Runtime load failure. */
16
+ export function logRuntimeLoadFailure(
17
+ step: string,
18
+ error: unknown,
19
+ details: {
20
+ reason?: "error" | "timeout";
21
+ timeoutMs?: number;
22
+ durationMs?: number;
23
+ } = {},
24
+ ): void {
25
+ if (typeof error === "object" && error !== null) {
26
+ if (reportedFailures.has(error)) return;
27
+ reportedFailures.add(error);
28
+ }
29
+ console.error(
30
+ "[runtime-load:failed]",
31
+ JSON.stringify({
32
+ step,
33
+ reason: details.reason ?? "error",
34
+ ...(details.timeoutMs !== undefined
35
+ ? { timeoutMs: details.timeoutMs }
36
+ : {}),
37
+ ...(details.durationMs !== undefined
38
+ ? { durationMs: details.durationMs }
39
+ : {}),
40
+ error: errorText(error),
41
+ }),
42
+ );
43
+ }
44
+
45
+ /** Stop waiting for one Runtime load boundary and report any failure once. */
46
+ export async function withRuntimeLoadTimeout<T>(
47
+ step: string,
48
+ load: () => T | PromiseLike<T>,
49
+ options: {
50
+ timeoutMs?: number;
51
+ onLateResult?: (value: T) => void | Promise<void>;
52
+ } = {},
53
+ ): Promise<T> {
54
+ const timeoutMs = options.timeoutMs ?? RUNTIME_CAPABILITY_LOAD_TIMEOUT_MS;
55
+ const startedAt = Date.now();
56
+ let timeout: ReturnType<typeof setTimeout> | undefined;
57
+ let timedOut = false;
58
+ const operation = Promise.resolve().then(load);
59
+
60
+ if (options.onLateResult) {
61
+ void operation.then(async (value) => {
62
+ if (timedOut) await options.onLateResult!(value);
63
+ }, () => undefined).catch((error) => {
64
+ logRuntimeLoadFailure(`${step}:late-cleanup`, error);
65
+ });
66
+ }
67
+
68
+ try {
69
+ return await Promise.race([
70
+ operation,
71
+ new Promise<never>((_, reject) => {
72
+ timeout = setTimeout(() => {
73
+ timedOut = true;
74
+ reject(new Error(
75
+ `Runtime load step "${step}" timed out after ${timeoutMs}ms`,
76
+ ));
77
+ }, timeoutMs);
78
+ }),
79
+ ]);
80
+ } catch (error) {
81
+ logRuntimeLoadFailure(step, error, {
82
+ reason: timedOut ? "timeout" : "error",
83
+ timeoutMs,
84
+ durationMs: Date.now() - startedAt,
85
+ });
86
+ throw error;
87
+ } finally {
88
+ if (timeout !== undefined) clearTimeout(timeout);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * 装载状态机与宿主之间的全部接触面。
94
+ *
95
+ * @remarks
96
+ * 只有这三项:读上一次发布的状态、发布新状态、回答旧 Runtime 是否仍可服务。
97
+ * 追踪器碰不到 Kernel 的 Snapshot、数据库或 Submission,因此它的行为可以脱离
98
+ * Durable Object 单独验证。
99
+ */
100
+ export interface RuntimeLoadPort {
101
+ read(): RuntimeLoadState | undefined;
102
+ publish(next: RuntimeLoadState): void;
103
+ /** 旧 Runtime 是否仍在服务;强制重载失败时它可能仍为 true。 */
104
+ isAvailable(): boolean;
105
+ }
106
+
107
+ /**
108
+ * 维护一次 Runtime 装载尝试的公开状态机。
109
+ *
110
+ * @remarks
111
+ * 宿主在装载的各个边界调用 `begin` / `advance` / `complete` / `fail`,
112
+ * 由本类统一发布 `RuntimeLoadState` 的形状与时间戳。
113
+ *
114
+ * `startedAt` 表示一次尝试的开始时刻:`begin` 重置它,其余转移沿用它,
115
+ * 使前端可以按同一基准显示耗时。
116
+ */
117
+ export class RuntimeLoadTracker {
118
+ constructor(
119
+ private readonly port: RuntimeLoadPort,
120
+ private readonly now: () => number = Date.now,
121
+ ) {}
122
+
123
+ // 作用:把状态清回未装载。
124
+ // 调用:Durable Object 启动时调用。
125
+ // 原因:实例刚被唤醒时没有任何在途尝试,旧记录里的 ready 不能继续对外承诺。
126
+ reset(): void {
127
+ this.port.publish({ status: "idle", available: false });
128
+ }
129
+
130
+ // 作用:开始一轮新的装载尝试。
131
+ // 调用:宿主在调用 Definition config.read 前调用。
132
+ // 原因:config 阶段可能包含身份、D1 和 Resource 解析,必须在首个慢请求前可见。
133
+ begin(): void {
134
+ const now = this.now();
135
+ this.port.publish({
136
+ status: "loading",
137
+ phase: "config",
138
+ available: this.port.isAvailable(),
139
+ startedAt: now,
140
+ updatedAt: now,
141
+ });
142
+ }
143
+
144
+ // 作用:推进当前装载尝试的阶段。
145
+ // 调用:宿主在进入 Assembly、MCP 和 Pi 边界时调用。
146
+ // 原因:保持一个稳定的粗粒度协议,不向前端泄漏具体能力准备和并发细节。
147
+ advance(phase: RuntimeLoadPhase): void {
148
+ const now = this.now();
149
+ this.port.publish({
150
+ status: "loading",
151
+ phase,
152
+ available: this.port.isAvailable(),
153
+ startedAt: this.startedAt(now),
154
+ updatedAt: now,
155
+ });
156
+ }
157
+
158
+ // 作用:把成功提交的装载尝试标记为可用。
159
+ // 调用:宿主在配置初始化和 Runtime key 提交完成后调用。
160
+ // 原因:只有完整原子提交后才能向客户端承诺 ready。
161
+ complete(): void {
162
+ const now = this.now();
163
+ this.port.publish({
164
+ status: "ready",
165
+ available: true,
166
+ startedAt: this.startedAt(now),
167
+ completedAt: now,
168
+ });
169
+ }
170
+
171
+ // 作用:记录装载失败,同时保留旧 Runtime 是否仍可用的信息。
172
+ // 调用:宿主收口 Config 读取或 Runtime 装配异常时调用。
173
+ // 原因:前端需要状态但不应接收可能包含存储细节的底层错误文本。
174
+ fail(): void {
175
+ const now = this.now();
176
+ const current = this.port.read();
177
+ this.port.publish({
178
+ status: "error",
179
+ phase: current?.status === "loading" ? current.phase : "config",
180
+ available: this.port.isAvailable(),
181
+ startedAt: this.startedAt(now),
182
+ failedAt: now,
183
+ });
184
+ }
185
+
186
+ // 同一轮尝试内沿用原开始时刻;不在装载中说明这是一次孤立转移,只能以当下为准。
187
+ private startedAt(now: number): number {
188
+ const current = this.port.read();
189
+ return current?.status === "loading" ? current.startedAt : now;
190
+ }
191
+ }
@@ -1,11 +1,18 @@
1
1
  import type { ApprovalReceipt } from "./receipts";
2
2
 
3
- export type RuntimeLoadPhase = "config" | "plugins" | "mcp" | "pi";
3
+ export type RuntimeLoadPhase = "config" | "assembly" | "mcp" | "pi";
4
4
 
5
5
  export type RuntimeActivity = "idle" | "working" | "needs-input";
6
6
 
7
7
  export interface RuntimeActivityProjection {
8
8
  activity: RuntimeActivity;
9
+ /**
10
+ * 是否仍有 Agent Tool 子运行在执行。
11
+ *
12
+ * `activity` 三态互斥,表达不了「等人回应的同时后台还在跑」。Host 的列表需要
13
+ * 这两件事各自成立,所以它是一个独立维度,而不是第四种活动值。
14
+ */
15
+ backgroundWork: boolean;
9
16
  revision: number;
10
17
  }
11
18
 
@@ -51,6 +58,10 @@ export interface RuntimeQueuedSubmission {
51
58
 
52
59
  export interface RuntimeTurnState {
53
60
  activeSubmissionId?: string;
61
+ activeRequestId?: string;
62
+ recoveryAttempt?: number;
63
+ recoveryMax?: number;
64
+ recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
54
65
  steerable: boolean;
55
66
  hasPendingSteer: boolean;
56
67
  queued: RuntimeQueuedSubmission[];
@@ -202,6 +202,13 @@ interface SubmissionLifecycleOptions<
202
202
  // 调用:`cancel` 命中活动执行器,或 `activate` 发现早到取消时调用。
203
203
  // 原因:生命周期不依赖具体执行器类型,只通过这个回调中断。
204
204
  abortActive(active: TActive): void;
205
+ /**
206
+ * 中断活动执行器,但让这条提交留给持久续跑接手。
207
+ *
208
+ * `interrupt` 调用。与 `abortActive` 的差别在结局而不在动作:中断不写终态,
209
+ * 也不接受这条执行器退场路上产生的任何工具结算。
210
+ */
211
+ interruptActive(active: TActive): void;
205
212
  }
206
213
 
207
214
  // #endregion
@@ -528,6 +535,29 @@ export class SubmissionLifecycle<
528
535
  };
529
536
  }
530
537
 
538
+ /**
539
+ * 中断一条提交的活动执行器,但不写取消原因,也不写终态。
540
+ *
541
+ * @remarks
542
+ * 宿主在 park 期间换掉装配后调用:内存里那条执行器的系统提示和工具表锁在它
543
+ * 自己被创建的那一刻,续跑不可能带上新能力,必须让位给持久续跑路径重建一条。
544
+ *
545
+ * 返回的 Promise 在旧执行器真正退场后才完成,调用方必须等它 —— 否则决策会
546
+ * 看到还没散场的内存等待器,又把这一轮唤回旧装配上。没有活动执行器时是空操作。
547
+ */
548
+ async interrupt(
549
+ submissionId: string,
550
+ afterAbort?: () => void,
551
+ ): Promise<void> {
552
+ const active = this.activeBySubmission.get(submissionId);
553
+ if (!active) return;
554
+ // 先中断再拆等待器:反过来的话,park 住的工具会先醒来、发现自己没有结算,
555
+ // 然后真的把工具跑一遍 —— 而这一轮本来就该让位。
556
+ this.options.interruptActive(active);
557
+ afterAbort?.();
558
+ await this.executions.get(submissionId)?.catch(() => undefined);
559
+ }
560
+
531
561
  /**
532
562
  * 返回当前唯一活动的执行器。
533
563
  *
@@ -587,13 +617,14 @@ export class SubmissionLifecycle<
587
617
  return submission ? [submission.submissionId] : [];
588
618
  })()
589
619
  : (() => {
590
- const submission = this.options.store.findRunning();
620
+ const submission = this.options.store.findRunning() ??
621
+ this.options.store.findNextPending();
591
622
  return submission ? [submission.submissionId] : [];
592
623
  })();
593
624
  for (const submissionId of submissionIds) {
594
625
  await this.cancel(submissionId, reason);
595
626
  }
596
- return { ok: true };
627
+ return { ok: submissionIds.length > 0 };
597
628
  }
598
629
 
599
630
  // #endregion
@@ -1,9 +1,21 @@
1
+ import type { ExecutionLevel } from "../../../lib/execution-level";
2
+
3
+ export const TEMPORARY_AGENT_LAUNCH_KEY =
4
+ "universal-agent:temporary-agent-launch";
5
+
1
6
  export interface TemporaryAgentRequest {
2
7
  subagentName: string;
3
8
  instructions: string;
4
9
  task: string;
5
10
  }
6
11
 
12
+ export interface TemporaryAgentLaunch<Config = unknown>
13
+ extends TemporaryAgentRequest {
14
+ runtimeKey: string;
15
+ config: Config;
16
+ executionLevel: ExecutionLevel;
17
+ }
18
+
7
19
  export interface TemporaryAgentRunContext {
8
20
  signal: AbortSignal;
9
21
  requestId: string;
@@ -149,4 +161,3 @@ export class TemporaryAgentCoordinator {
149
161
  });
150
162
  }
151
163
  }
152
- import type { ExecutionLevel } from "../../../lib/execution-level";
@@ -1,9 +1,4 @@
1
1
  import type { RuntimeExtensionConfig } from "../../../kernel/extensions";
2
- import {
3
- requiresExecutionApproval,
4
- type ExecutionLevel,
5
- } from "../../../lib/execution-level";
6
- import type { PiToolCandidate } from "../../../pi/tool/compiler";
7
2
 
8
3
  const BLOCKED_TOOLS = new Set([
9
4
  "dispatch_background",
@@ -15,7 +10,6 @@ const BLOCKED_TOOLS = new Set([
15
10
  "resume_schedule",
16
11
  "change_schedule_agent",
17
12
  "cancel_schedule",
18
- "execute",
19
13
  "set_context",
20
14
  "load_context",
21
15
  "search_context",
@@ -30,7 +24,7 @@ const BLOCKED_TOOLS = new Set([
30
24
  * @remarks
31
25
  * 宿主在装配临时 Agent 的平台、Workspace、Sandbox 和其他 Tool 时调用;调用方可再加名字或前缀黑名单。
32
26
  *
33
- * 固定黑名单排除再次委派、调度、主 Session 上下文和绕过隔离边界的 execute,以保持单层临时 Agent 和已确认的继承范围。
27
+ * 固定黑名单排除再次委派、调度和主 Session 上下文,以保持单层临时 Agent 和已确认的继承范围。
34
28
  *
35
29
  * Agent、Session、Workspace 和 Tool 的术语见 `src/index.ts`。
36
30
  */
@@ -46,66 +40,6 @@ export function temporaryAgentToolAllowed(
46
40
  );
47
41
  }
48
42
 
49
- /**
50
- * 给临时 Agent 里需要审批的 Tool 包上父 Session 审批。
51
- *
52
- * @remarks
53
- * 宿主在筛完 Tool 候选项后调用;返回的候选项可直接交给临时 Agent 的 Pi Tool 编译流程。
54
- *
55
- * 包装层先按父 Agent 的执行档位请求批准,然后才调原 `execute`;审批完成后把包装候选降为 `safe`,避免临时 Agent 重复审批。
56
- *
57
- * Runtime、Session 和 Tool 的术语见 `src/index.ts`。
58
- */
59
- export function bridgeTemporaryAgentToolApprovals(
60
- candidates: readonly PiToolCandidate[],
61
- executionLevel: ExecutionLevel,
62
- requestApproval: (request: {
63
- toolName: string;
64
- executionLevel: ExecutionLevel;
65
- requiredExecutionLevel: ExecutionLevel;
66
- input: unknown;
67
- toolCallId: string;
68
- signal: AbortSignal;
69
- }) => Promise<void>,
70
- ): PiToolCandidate[] {
71
- return candidates.map((candidate) => {
72
- if (!requiresExecutionApproval(
73
- executionLevel,
74
- candidate.requiredExecutionLevel,
75
- )) {
76
- return candidate;
77
- }
78
- const execute = candidate.tool.execute;
79
- return {
80
- ...candidate,
81
- requiredExecutionLevel: "safe",
82
- tool: {
83
- ...candidate.tool,
84
- // 作用:在执行原 Tool 前向父 Session 请求审批。
85
- // 调用:Pi Tool 执行器调用,参数和进度回调原样传给原 Tool。
86
- // 原因:某些调用方不传 signal,但审批契约需要一个 `AbortSignal`,因此只在缺失时创建一个不会自行取消的信号。
87
- async execute(toolCallId, input, signal, onUpdate) {
88
- const activeSignal = signal ?? new AbortController().signal;
89
- await requestApproval({
90
- toolName: candidate.tool.name,
91
- executionLevel,
92
- requiredExecutionLevel: candidate.requiredExecutionLevel,
93
- input,
94
- toolCallId,
95
- signal: activeSignal,
96
- });
97
- return execute(
98
- toolCallId,
99
- input,
100
- activeSignal,
101
- onUpdate,
102
- );
103
- },
104
- },
105
- };
106
- });
107
- }
108
-
109
43
  /**
110
44
  * 判断一个 Extension 是否安全到可以交给临时 Agent。
111
45
  *
package/src/lib/mcp.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RetryOptions } from "agents";
2
+ import { withRuntimeLoadTimeout } from "../kernel/runtime-load";
2
3
 
3
4
  const MCP_RETRY: RetryOptions = {
4
5
  maxAttempts: 3,
@@ -63,9 +64,12 @@ export async function connectConfiguredMcpServers(
63
64
  const servers = [...pending.values()];
64
65
  const settled = await Promise.allSettled(
65
66
  servers.map((server) =>
66
- host.addMcpServer(server.name, server.url, {
67
- retry: MCP_RETRY,
68
- }),
67
+ withRuntimeLoadTimeout(
68
+ `mcp:${server.name}:connect`,
69
+ () => host.addMcpServer(server.name, server.url, {
70
+ retry: MCP_RETRY,
71
+ }),
72
+ ),
69
73
  ),
70
74
  );
71
75
  const failed = settled.flatMap((result, index) =>