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

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.1.3-alpha.0",
3
+ "version": "0.1.3-alpha.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
package/src/db/schema.ts CHANGED
@@ -21,7 +21,9 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
21
21
  assembly_revision TEXT NOT NULL,
22
22
  assembly_descriptor TEXT NOT NULL,
23
23
  assistant_message_id TEXT NOT NULL,
24
- abort_reason TEXT
24
+ abort_reason TEXT,
25
+ recovery_error_count INTEGER NOT NULL DEFAULT 0,
26
+ recovery_reason TEXT
25
27
  )`;
26
28
  const submissionColumns = new Set(
27
29
  sql<{ name: string }>`PRAGMA table_info(pi_submissions)`
@@ -39,6 +41,13 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
39
41
  if (!submissionColumns.has("regenerate_message_id")) {
40
42
  sql`ALTER TABLE pi_submissions ADD COLUMN regenerate_message_id TEXT`;
41
43
  }
44
+ if (!submissionColumns.has("recovery_error_count")) {
45
+ sql`ALTER TABLE pi_submissions
46
+ ADD COLUMN recovery_error_count INTEGER NOT NULL DEFAULT 0`;
47
+ }
48
+ if (!submissionColumns.has("recovery_reason")) {
49
+ sql`ALTER TABLE pi_submissions ADD COLUMN recovery_reason TEXT`;
50
+ }
42
51
  sql`CREATE UNIQUE INDEX IF NOT EXISTS pi_submissions_one_running
43
52
  ON pi_submissions(status)
44
53
  WHERE status = 'running'`;
@@ -9,6 +9,9 @@ export type SubmissionStatus =
9
9
  | "aborted"
10
10
  | "skipped"
11
11
  | "error";
12
+ export type SubmissionRecoveryReason =
13
+ | "no_meaningful_model_progress"
14
+ | "transient_model_error";
12
15
 
13
16
  const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
14
17
  "completed",
@@ -43,6 +46,8 @@ export interface StoredSubmission {
43
46
  queuedUiMessageJson: string | null;
44
47
  userMessageId: string | null;
45
48
  regenerateMessageId: string | null;
49
+ recoveryErrorCount: number;
50
+ recoveryReason: SubmissionRecoveryReason | null;
46
51
  }
47
52
 
48
53
  export interface NewSubmission {
@@ -76,6 +81,8 @@ type SubmissionRow = {
76
81
  queued_ui_message_json: string | null;
77
82
  user_message_id: string | null;
78
83
  regenerate_message_id: string | null;
84
+ recovery_error_count: number;
85
+ recovery_reason: string | null;
79
86
  };
80
87
 
81
88
  // #endregion
@@ -101,6 +108,8 @@ function mapRow(row: SubmissionRow): StoredSubmission {
101
108
  queuedUiMessageJson: row.queued_ui_message_json,
102
109
  userMessageId: row.user_message_id,
103
110
  regenerateMessageId: row.regenerate_message_id,
111
+ recoveryErrorCount: row.recovery_error_count,
112
+ recoveryReason: row.recovery_reason as SubmissionRecoveryReason | null,
104
113
  };
105
114
  }
106
115
 
@@ -121,7 +130,7 @@ export class SubmissionRepository {
121
130
  error, created_at, completed_at, assembly_revision,
122
131
  assembly_descriptor, assistant_message_id, abort_reason,
123
132
  queued_input_json, queued_ui_message_json, user_message_id,
124
- regenerate_message_id
133
+ regenerate_message_id, recovery_error_count, recovery_reason
125
134
  FROM pi_submissions
126
135
  WHERE submission_id = ${id}
127
136
  `[0];
@@ -309,6 +318,30 @@ export class SubmissionRepository {
309
318
  `;
310
319
  }
311
320
 
321
+ // chatRecovery 遇到 partial 会重置 attempt;Submission 计数保证同一 Turn 的可恢复模型错误仍绝对封顶。
322
+ incrementRecoveryErrorCount(
323
+ id: string,
324
+ reason: SubmissionRecoveryReason,
325
+ ): number {
326
+ return this.sql<{ recovery_error_count: number }>`
327
+ UPDATE pi_submissions
328
+ SET recovery_error_count = recovery_error_count + 1,
329
+ recovery_reason = ${reason}
330
+ WHERE submission_id = ${id}
331
+ AND status IN ('pending', 'running')
332
+ RETURNING recovery_error_count
333
+ `[0]?.recovery_error_count ?? 0;
334
+ }
335
+
336
+ clearRecoveryReason(id: string): void {
337
+ this.sql`
338
+ UPDATE pi_submissions
339
+ SET recovery_reason = NULL
340
+ WHERE submission_id = ${id}
341
+ AND status IN ('pending', 'running')
342
+ `;
343
+ }
344
+
312
345
  // 返回所有标准会话消息到 Submission 的关联映射。
313
346
  // Transcript.storedMessages 读取 Pi Session 分支时调用,它用 Map 为每条消息补回 submissionId。
314
347
  // 关联表由 Pi Session 存储拥有,本方法只读不写;不能在 RuntimeDatabase.clearAll 中单独删它而破坏 Transcript 的所有权。
package/src/index.ts CHANGED
@@ -23,11 +23,9 @@
23
23
  * - `canonical transcript` 是 Pi 持久化和恢复 Turn 时使用的权威消息历史。
24
24
  * - `admission` 是提示进入 Turn 前的接纳、去重和状态登记流程。
25
25
  * - `AgentConfig` 是应用交给 Runtime 的一次完整装配说明。
26
- * - `AgentPlugin` 是一种能力的加载与贡献单元。
27
- * - `PluginKind` Plugin 的固定类别,也是贡献时的稳定排序依据。
28
- * - `loader` 读取业务数据或外部绑定,并返回本次装配要使用的值。
29
- * - `prepare` 执行 `loader`,但不会改动正在运行的 Runtime。
30
- * - `RuntimeContributionContext` 是 Plugin 写入候选配置的受限接口。
26
+ * - `AgentPlugin` 是一种能力的声明式准备单元。
27
+ * - `PluginKind` 同时收窄 Plugin 可以准备的字段,并作为内部稳定合并顺序。
28
+ * - `prepare` 读取业务数据或外部绑定,但不会改动正在运行的 Runtime。
31
29
  * - `Port` 是 Runtime 调用外部能力时依赖的最小接口。
32
30
  * - `RuntimeBindings` 是本次装配选中的 Port 和可执行对象集合。
33
31
  * - `RuntimeBuilder` 是包内实现,用来校验贡献并生成候选结果。
@@ -40,8 +38,7 @@
40
38
  *
41
39
  * Snapshot 不允许替换已选集合,但不承诺把每个 Port 的内部对象深冻结。
42
40
  *
43
- * 正常调用顺序是 `loaderpreparecontributevalidate → guard → commit`。
44
- * 待确认:当前实现先校验 Plugin kind,再在 `prepare` 中调用 `loader`;这里的阶段简写是否仍应作为对外文档保留,需要与实际命名统一。
41
+ * 正常调用顺序是 `preparemergevalidatefreeze → guard → commit`。
45
42
  *
46
43
  * 应用只创建 `AgentConfig` 和 Plugin。
47
44
  *
@@ -62,11 +59,13 @@ export type {
62
59
  AgentPlugin,
63
60
  AgentPluginSpec,
64
61
  PluginKind,
65
- PluginLoadResult,
62
+ PluginPreparation,
66
63
  PreparedPlugin,
67
- RuntimeContributionContext,
68
64
  RuntimeDegradation,
65
+ RuntimeExtensionContribution,
69
66
  RuntimeProfileContribution,
67
+ RuntimeSkillContribution,
68
+ RuntimeToolSurfacePolicy,
70
69
  } from "./plugins";
71
70
  export type { ModelOption } from "./lib/model-catalog";
72
71
  export {
@@ -105,7 +104,7 @@ export {
105
104
  browserQuickActionPiToolCandidates,
106
105
  } from "./pi/tool";
107
106
  export {
108
- workspaceCodeExecutionPiToolCandidate,
107
+ createWorkspaceCodeExecutionPort,
109
108
  } from "./pi/tool";
110
109
  export {
111
110
  assemblePiExtensions,
@@ -119,7 +118,6 @@ export type {
119
118
  TemporaryAgentRunContext,
120
119
  } from "./layers/orchestration/temporary-agent/core";
121
120
  export {
122
- bridgeTemporaryAgentToolApprovals,
123
121
  temporaryAgentExtensionIsSafe,
124
122
  temporaryAgentToolAllowed,
125
123
  } from "./layers/orchestration/temporary-agent/runner";
@@ -186,6 +186,17 @@ export interface WorkspacePort {
186
186
  glob(pattern: string): Promise<WorkspaceFileInfo[]>;
187
187
  }
188
188
 
189
+ /**
190
+ * 向 Runtime 提供已组装的 Workspace Code Mode 执行能力。
191
+ *
192
+ * @remarks
193
+ * Host Adapter 负责绑定 Durable Object、Worker Loader、网络出口和
194
+ * Workspace;Tool Surface 只把这个已授权 Port 转成 `execute` Tool。
195
+ */
196
+ export interface RuntimeCodeExecutionPort {
197
+ execute(input: { code: string }): Promise<unknown>;
198
+ }
199
+
189
200
  /**
190
201
  * 汇总一个已限定作用域的 Workspace 用量。
191
202
  *
@@ -283,9 +294,7 @@ export interface RuntimeMemoryPort {
283
294
  * 按标签覆盖一块热记忆。
284
295
  *
285
296
  * @remarks
286
- * Host 实现提供该能力,但当前 `packages/agent-runtime/src` 没有调用点。
287
- *
288
- * TODO(待确认): 确认后续写入方是否仍需通过这个公开 Port 提交热记忆。
297
+ * `set_context` 在校验标签和 token 预算后调用,Host 负责按当前 Agent 与会话持久化。
289
298
  */
290
299
  set(label: string, content: string): Promise<void>;
291
300
  }
@@ -540,6 +549,14 @@ export interface RuntimeScheduleUpdate {
540
549
  tz?: string;
541
550
  }
542
551
 
552
+ /** 定时任务可切换到的已授权 User Agent 摘要。 */
553
+ export interface RuntimeScheduleAgentSummary {
554
+ id: string;
555
+ name: string;
556
+ description: string | null;
557
+ isDefault: boolean;
558
+ }
559
+
543
560
  /**
544
561
  * 让 Runtime 通过 Host 管理当前用户的定时任务。
545
562
  *
@@ -573,6 +590,10 @@ export interface RuntimeSchedulePort {
573
590
  ): Promise<{ ok: boolean }>;
574
591
  pause(id: string): Promise<{ ok: boolean }>;
575
592
  resume(id: string): Promise<{ ok: boolean }>;
593
+ /** 列出当前用户可用于执行定时任务的 User Agent。 */
594
+ listAgents(): Promise<RuntimeScheduleAgentSummary[]>;
595
+ /** 更换一条定时任务的执行 Agent。 */
596
+ changeAgent(id: string, userAgentId: string): Promise<{ ok: boolean }>;
576
597
  /**
577
598
  * 按 ID 取消一条定时任务。
578
599
  *
@@ -786,7 +807,7 @@ export interface RuntimeModelEndpoint {
786
807
  * 向 Runtime 提供本次装配可用的模型端点。
787
808
  *
788
809
  * @remarks
789
- * Provider Plugin 贡献它,Builder 在提交前校验,Pi Adapter 在激活与每次解析模型时读取。
810
+ * Provider Plugin 准备它,Builder 在提交前校验,Pi Adapter 在激活与每次解析模型时读取。
790
811
  *
791
812
  * Runtime 只依赖已解析结果,不知道环境变量或业务模型配置的来源。
792
813
  */
@@ -800,7 +821,7 @@ export interface RuntimeProviderPort {
800
821
  * 向 Runtime 提供 Cloudflare 执行平台上的可授权能力。
801
822
  *
802
823
  * @remarks
803
- * Platform Plugin 贡献它,Workspace Codemode、Browser 工具、遥测和工具门卫按需使用。
824
+ * Platform Plugin 准备它,Workspace Codemode、Browser 工具、遥测和工具门卫按需使用。
804
825
  *
805
826
  * Worker Loader 与网络出口由 Host 选择,使 Dynamic Worker 只获得已授权绑定;术语见 `../index.ts`。
806
827
  */
@@ -853,7 +874,7 @@ export interface RuntimeSkillBindings {
853
874
  * 限定一个 Skill 脚本可使用的网络、Workspace 和工具。
854
875
  *
855
876
  * @remarks
856
- * Skill Plugin 在贡献来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
877
+ * Skill Plugin 在准备来源时设置,Pi Skill 工具在决定是否注入脚本执行能力时读取。
857
878
  *
858
879
  * 这是已授权结果,执行期不应为某个 Skill 自动放宽。
859
880
  */
@@ -43,7 +43,7 @@ export interface RuntimeExtensionContextDefinition {
43
43
  * 把 Extension 的 manifest 与待加载源码放在同一份配置里。
44
44
  *
45
45
  * @remarks
46
- * extension Plugin 在贡献阶段提供它,Pi Extension 适配器在候选 Runtime 装配时加载。
46
+ * extension Plugin 在准备结果中提供它,Pi Extension 适配器在候选 Runtime 装配时加载。
47
47
  *
48
48
  * manifest 与源码一起传递,可避免授权信息与实际执行代码在不同查找步骤中错位。
49
49
  *
@@ -22,7 +22,7 @@ export type ThinkingEffort =
22
22
  * 保存一次装配选定的内存开关与上下文预算。
23
23
  *
24
24
  * @remarks
25
- * memory Plugin 在贡献阶段提供它,Runtime 在构建上下文和触发压缩时读取。
25
+ * memory Plugin 在准备结果中提供它,Runtime 在构建上下文时读取。
26
26
  *
27
27
  * 预算与开关放在同一个 Profile 中,确保一次 Snapshot 只使用一套内存配置。
28
28
  *
@@ -32,14 +32,13 @@ export interface RuntimeMemoryProfile {
32
32
  enabled: boolean;
33
33
  memoryTokens: number;
34
34
  preferencesTokens: number;
35
- compactAfterTokens: number;
36
35
  }
37
36
 
38
37
  /**
39
38
  * 指明本次装配允许连接的一个 MCP 服务。
40
39
  *
41
40
  * @remarks
42
- * connector Plugin 在贡献阶段加入它,Pi 工具装配只选择 Host 已就绪且 URL 匹配的连接。
41
+ * connector Plugin 在准备结果中加入它,Pi 工具装配只选择 Host 已就绪且 URL 匹配的连接。
43
42
  *
44
43
  * 这里只保留名称和 URL;连接生命周期、凭据与重试仍由 Host 管理。
45
44
  *
@@ -71,7 +70,7 @@ export interface RuntimeDenyPolicy {
71
70
  * 保存一次装配中真正会影响执行的参数。
72
71
  *
73
72
  * @remarks
74
- * `RuntimeBuilder` 在所有 Plugin 贡献完成后生成它,Kernel 只通过已提交的 Snapshot 读取。
73
+ * `RuntimeBuilder` 在所有 Plugin 声明合并完成后生成它,Kernel 只通过已提交的 Snapshot 读取。
75
74
  *
76
75
  * 它不包含业务身份、归属键或存储地址,因为这些选择属于 Host,不应被冻结成执行参数。
77
76
  *
@@ -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
  /**
@@ -544,6 +544,7 @@ export abstract class RecoverableChatAgent<
544
544
  this.activeRecoveryRootRequestId = data.requestId;
545
545
  try {
546
546
  const result = await this.recoveryPort().retry(data);
547
+ if (result.status === "scheduled") return;
547
548
  await this.engine().updateIncident(
548
549
  data.incidentId,
549
550
  result.status === "completed" ? "completed" : "failed",
@@ -563,6 +564,83 @@ export abstract class RecoverableChatAgent<
563
564
  }
564
565
  }
565
566
 
567
+ /**
568
+ * 把活跃进程内检测到的模型流停滞交给同一套持久恢复预算。
569
+ *
570
+ * @remarks
571
+ * 子类在确认当前 Turn 可以从 durable checkpoint 重放后调用;`beforeSchedule`
572
+ * 必须先结束旧流并恢复权威消息快照,随后引擎才广播 recovering 状态。
573
+ */
574
+ protected async scheduleChatRecoveryRetry(
575
+ data: RecoveryData,
576
+ beforeSchedule: () => void | Promise<void>,
577
+ ): Promise<"disabled" | "scheduled" | "exhausted"> {
578
+ if (!resolveChatRecoveryConfig(this.chatRecovery).enabled) {
579
+ return "disabled";
580
+ }
581
+ const recoveryRootRequestId =
582
+ this.activeRecoveryRootRequestId ?? data.requestId;
583
+ const { incident, exhausted } = await this.engine().beginIncident({
584
+ requestId: data.requestId,
585
+ recoveryRootRequestId,
586
+ recoveryKind: "retry",
587
+ });
588
+ await beforeSchedule();
589
+ if (exhausted) {
590
+ await this.engine().exhaustRecoveryGiveUp({
591
+ callback: "_chatRecoveryRetry",
592
+ data: {
593
+ incidentId: incident.incidentId,
594
+ originalRequestId: recoveryRootRequestId,
595
+ },
596
+ reason: incident.reason ?? "max_attempts_exceeded",
597
+ });
598
+ return "exhausted";
599
+ }
600
+ await this.engine().scheduleRecovery({
601
+ incident,
602
+ recoveryKind: "retry",
603
+ callback: "_chatRecoveryRetry",
604
+ data: {
605
+ ...data,
606
+ incidentId: incident.incidentId,
607
+ },
608
+ reason: this.activeRecoveryRootRequestId
609
+ ? "stable_timeout_retry"
610
+ : "initial",
611
+ });
612
+ return "scheduled";
613
+ }
614
+
615
+ /** 终态化一个 Turn 时取消同请求的恢复调度,并关闭恢复事故与客户端状态。 */
616
+ protected async settleChatRecovery(
617
+ requestId: string,
618
+ status: "completed" | "skipped" | "failed",
619
+ reason?: string,
620
+ ): Promise<void> {
621
+ const incidentIds = new Set<string>();
622
+ try {
623
+ for (const schedule of await this.listSchedules()) {
624
+ if (schedule.callback !== "_chatRecoveryRetry") continue;
625
+ const payload = schedule.payload;
626
+ if (!payload || typeof payload !== "object") continue;
627
+ const recovery = payload as Partial<
628
+ RecoveryScheduleData<RecoveryData>
629
+ >;
630
+ if (recovery.requestId !== requestId) continue;
631
+ if (typeof recovery.incidentId === "string") {
632
+ incidentIds.add(recovery.incidentId);
633
+ }
634
+ await this.cancelSchedule(schedule.id);
635
+ }
636
+ for (const incidentId of incidentIds) {
637
+ await this.engine().updateIncident(incidentId, status, reason);
638
+ }
639
+ } finally {
640
+ await this.setRecovering(false, requestId);
641
+ }
642
+ }
643
+
566
644
  /**
567
645
  * 把 Agents SDK 发现的孤立 Fiber 交给聊天恢复引擎。
568
646
  *
@@ -51,6 +51,10 @@ export interface RuntimeQueuedSubmission {
51
51
 
52
52
  export interface RuntimeTurnState {
53
53
  activeSubmissionId?: string;
54
+ activeRequestId?: string;
55
+ recoveryAttempt?: number;
56
+ recoveryMax?: number;
57
+ recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
54
58
  steerable: boolean;
55
59
  hasPendingSteer: boolean;
56
60
  queued: RuntimeQueuedSubmission[];
@@ -587,13 +587,14 @@ export class SubmissionLifecycle<
587
587
  return submission ? [submission.submissionId] : [];
588
588
  })()
589
589
  : (() => {
590
- const submission = this.options.store.findRunning();
590
+ const submission = this.options.store.findRunning() ??
591
+ this.options.store.findNextPending();
591
592
  return submission ? [submission.submissionId] : [];
592
593
  })();
593
594
  for (const submissionId of submissionIds) {
594
595
  await this.cancel(submissionId, reason);
595
596
  }
596
- return { ok: true };
597
+ return { ok: submissionIds.length > 0 };
597
598
  }
598
599
 
599
600
  // #endregion
@@ -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",
@@ -46,66 +41,6 @@ export function temporaryAgentToolAllowed(
46
41
  );
47
42
  }
48
43
 
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
44
  /**
110
45
  * 判断一个 Extension 是否安全到可以交给临时 Agent。
111
46
  *
package/src/lib/prompt.ts CHANGED
@@ -46,7 +46,7 @@ export const BEHAVIOR =
46
46
  export const PLANNING =
47
47
  "Planning: For work with 3 or more distinct steps, or any non-trivial / multi-file change, call " +
48
48
  "update_plan with the complete plan (it fully replaces the previous one) so the user sees live " +
49
- "progress. Mark a step in_progress before starting it and completed the moment it's done; keep only " +
49
+ "progress. Mark a step in_progress before starting it and done the moment it's done; keep only " +
50
50
  "one step in_progress at a time and don't batch completions. Do not make a plan for a single trivial " +
51
51
  "step or a purely conversational reply — just do it.";
52
52
 
@@ -116,7 +116,7 @@ export async function compactPiContext(input: Readonly<{
116
116
  const settings = DEFAULT_COMPACTION_SETTINGS;
117
117
  if (
118
118
  !settings.enabled ||
119
- estimateContextTokens(currentMessages).tokens <=
119
+ estimateContextTokens(currentMessages).tokens <
120
120
  input.compactAfterTokens
121
121
  ) {
122
122
  return {
@@ -479,7 +479,6 @@ export async function assemblePiSystemContext(input: Readonly<{
479
479
  ) => Promise<string | null>;
480
480
  }>): Promise<Readonly<{
481
481
  systemPrompt: string;
482
- compactAfterTokens: number;
483
482
  degradations: readonly RuntimeDegradation[];
484
483
  }>> {
485
484
  const [memory, extensions] = await Promise.all([
@@ -499,7 +498,6 @@ export async function assemblePiSystemContext(input: Readonly<{
499
498
 
500
499
  return Object.freeze({
501
500
  systemPrompt: parts.join("\n\n"),
502
- compactAfterTokens: input.memory.compactAfterTokens,
503
501
  degradations: Object.freeze([
504
502
  ...memory.degradations,
505
503
  ...extensions.degradations,
@@ -3,6 +3,7 @@ import type {
3
3
  ThinkingLevel,
4
4
  } from "@earendil-works/pi-agent-core";
5
5
  import type { Api, Model } from "@earendil-works/pi-ai";
6
+ import { clampReasoning } from "@earendil-works/pi-ai/api/simple-options";
6
7
  import type {
7
8
  RuntimeMcpServer,
8
9
  RuntimeProfile,
@@ -16,13 +17,22 @@ import { assembleSystemPrompt } from "../../lib/prompt";
16
17
  import { resolvePiModel } from "../runtime-adapter/models";
17
18
  import type { PiToolCandidate } from "../tool/compiler";
18
19
 
20
+ /**
21
+ * 收集完动态 Extension/MCP 候选后,一次性生成最终 Tool Surface。
22
+ */
23
+ export interface PiToolSurface {
24
+ finalize(
25
+ authorizedCandidates: readonly PiToolCandidate[],
26
+ ): readonly PiToolCandidate[];
27
+ }
28
+
19
29
  /** Immutable inputs consumed directly by Pi Agent Core. */
20
30
  export interface PiRuntimeAssembly {
21
31
  readonly model: Model<Api>;
22
32
  readonly thinkingLevel: ThinkingLevel;
23
33
  readonly systemPrompt: string;
24
34
  readonly messages: readonly AgentMessage[];
25
- readonly toolCandidates: readonly PiToolCandidate[];
35
+ readonly toolSurface: PiToolSurface;
26
36
  readonly mcpServers: readonly RuntimeMcpServer[];
27
37
  readonly extensions: readonly RuntimeExtensionConfig[];
28
38
  }
@@ -34,7 +44,7 @@ interface PiRuntimeAssemblyInput {
34
44
  >;
35
45
  readonly provider: RuntimeProviderPort;
36
46
  readonly messages?: readonly AgentMessage[];
37
- readonly toolCandidates: readonly PiToolCandidate[];
47
+ readonly toolSurface: PiToolSurface;
38
48
  readonly mcpServers: readonly RuntimeMcpServer[];
39
49
  readonly extensions: readonly RuntimeExtensionConfig[];
40
50
  }
@@ -70,11 +80,13 @@ function freezeModel(model: Model<Api>): Model<Api> {
70
80
 
71
81
  // 作用:把 Runtime 的思考强度名称转成 Pi Agent Core 接受的名称。
72
82
  // 调用:创建 Runtime 装配快照时对 profile 中的 `thinking` 调用。
73
- // 原因:只有 `none` Pi 的 `off` 命名不同,其他级别应保持原值。
83
+ // 原因:`none` 映射成 `off`;xhigh/max 用 clampReasoning 夹到 API 真正支持的最高档,
84
+ // 避免超出范围的等级在 provider 侧静默失败。
74
85
  function toPiThinkingLevel(
75
86
  thinking: ThinkingEffort,
76
87
  ): ThinkingLevel {
77
- return thinking === "none" ? "off" : thinking;
88
+ if (thinking === "none") return "off";
89
+ return clampReasoning(thinking) ?? "off";
78
90
  }
79
91
 
80
92
  // 作用:复制并冻结一个 Extension 配置的 manifest 和权限集合。
@@ -172,15 +184,11 @@ export function createPiRuntimeAssembly(
172
184
  input: PiRuntimeAssemblyInput,
173
185
  ): PiRuntimeAssembly {
174
186
  const messages = [...(input.messages ?? [])];
175
- const toolCandidates = input.toolCandidates.map(
176
- (candidate) => Object.freeze({ ...candidate }),
177
- );
178
187
  const mcpServers = input.mcpServers.map(
179
188
  (server) => Object.freeze({ ...server }),
180
189
  );
181
190
  const extensions = input.extensions.map(freezeExtension);
182
191
  Object.freeze(messages);
183
- Object.freeze(toolCandidates);
184
192
  Object.freeze(mcpServers);
185
193
  Object.freeze(extensions);
186
194
 
@@ -193,7 +201,7 @@ export function createPiRuntimeAssembly(
193
201
  input.profile.systemPrompt,
194
202
  ),
195
203
  messages,
196
- toolCandidates,
204
+ toolSurface: input.toolSurface,
197
205
  mcpServers,
198
206
  extensions,
199
207
  });