@springbrand/agent-runtime 0.3.0-alpha.20 → 0.3.0-alpha.21

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.
@@ -19,8 +19,6 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
19
19
  error TEXT,
20
20
  created_at INTEGER NOT NULL,
21
21
  completed_at INTEGER,
22
- assembly_revision TEXT NOT NULL,
23
- assembly_descriptor TEXT NOT NULL,
24
22
  assistant_message_id TEXT NOT NULL,
25
23
  abort_reason TEXT
26
24
  )`;
@@ -28,6 +26,12 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
28
26
  sql<{ name: string }>`PRAGMA table_info(pi_submissions)`
29
27
  .map((column) => column.name),
30
28
  );
29
+ if (submissionColumns.has("assembly_revision")) {
30
+ sql`ALTER TABLE pi_submissions DROP COLUMN assembly_revision`;
31
+ }
32
+ if (submissionColumns.has("assembly_descriptor")) {
33
+ sql`ALTER TABLE pi_submissions DROP COLUMN assembly_descriptor`;
34
+ }
31
35
  if (!submissionColumns.has("runtime_key")) {
32
36
  sql`ALTER TABLE pi_submissions ADD COLUMN runtime_key TEXT NOT NULL DEFAULT ''`;
33
37
  }
@@ -2,7 +2,7 @@ import { connectConfiguredMcpServers } from "../lib/mcp";
2
2
  import { sha256Hex } from "../lib/artifacts";
3
3
  import type { RuntimeCandidate, RuntimeSnapshot } from "./assembler";
4
4
  import type { HarnessRuntimeConfig } from "./harness-runtime";
5
- import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./pinning";
5
+ import type { PreparedPiRuntime, PreparePiRuntimeOptions } from "./prepared-runtime";
6
6
  import type { RuntimeGatewaySession, RuntimeSubmissionAdmissionHook } from "./ports";
7
7
  import { withRuntimeLoadTimeout } from "./runtime-load";
8
8
  import type { RuntimeLoadPhase } from "../conversation/state";
@@ -16,6 +16,16 @@ export interface ActiveRuntime {
16
16
  readonly admission?: RuntimeSubmissionAdmissionHook;
17
17
  }
18
18
 
19
+ /** Check the same candidate guards during admission and final activation. */
20
+ export async function verifyRuntimeCandidateGuards(
21
+ candidate: RuntimeCandidate,
22
+ phase: "admission" | "commit",
23
+ ): Promise<void> {
24
+ for (const [index, guard] of candidate.commitGuards.entries()) {
25
+ await withRuntimeLoadTimeout(`${phase}-guard:${index}`, guard);
26
+ }
27
+ }
28
+
19
29
  async function closeRuntimeGatewaySession(
20
30
  session: RuntimeGatewaySession | undefined,
21
31
  step: string,
@@ -33,7 +43,6 @@ interface RuntimeActivationOptions {
33
43
  readonly sessionId: string;
34
44
  readonly createExtensionHostBinding: PreparePiRuntimeOptions["createExtensionHostBinding"];
35
45
  advance(phase: RuntimeLoadPhase): void;
36
- isExecuting(): boolean;
37
46
  commit(runtime: ActiveRuntime): void;
38
47
  }
39
48
 
@@ -92,19 +101,10 @@ export async function activateRuntimeCandidate(
92
101
  throw error;
93
102
  }
94
103
  const revision = await sha256Hex(prepared.revisionDescriptor);
95
- const contested = Boolean(previous) &&
96
- revision !== previous?.revision &&
97
- options.isExecuting();
98
- if (contested) {
99
- await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
100
- throw new Error(
101
- "Cannot reload Runtime while a revision-pinned SpringBrand Turn is active",
102
- );
103
- }
104
104
  const next = Object.freeze({
105
105
  snapshot: candidateSnapshot,
106
106
  revision,
107
- runtimeKey: candidate.runtimeKey ?? revision,
107
+ runtimeKey: candidate.runtimeKey ?? "default",
108
108
  pi: prepared,
109
109
  ...(gatewaySession ? { gatewaySession } : {}),
110
110
  ...(candidate.admission
@@ -114,9 +114,7 @@ export async function activateRuntimeCandidate(
114
114
  let activated = false;
115
115
  const previousGatewaySession = previous?.gatewaySession;
116
116
  try {
117
- for (const [index, guard] of candidate.commitGuards.entries()) {
118
- await withRuntimeLoadTimeout(`commit-guard:${index}`, guard);
119
- }
117
+ await verifyRuntimeCandidateGuards(candidate, "commit");
120
118
  options.pi.activate(candidateSnapshot);
121
119
  activated = true;
122
120
  options.commit(next);
@@ -15,19 +15,17 @@ export {
15
15
  readModelStreamStallDetails,
16
16
  } from "../models";
17
17
  import {
18
- pinPiRuntime,
18
+ assembleCurrentPiContext,
19
19
  preparePiRuntime,
20
- readPinnedPiRuntime,
21
20
  readPreparedPiRuntime,
22
- type PinPiRuntimeOptions,
23
- type PinnedPiRuntime,
21
+ type CurrentPiContextOptions,
24
22
  type PreparePiRuntimeOptions,
25
23
  type PreparedPiRuntime,
26
- } from "./pinning";
24
+ } from "./prepared-runtime";
27
25
  import { refreshModelGuidance } from "./model-guidance";
28
26
 
29
27
  /**
30
- * Runtime 通过这个配置入口准备、固定并读取 Harness 的模型、提示和工具输入。
28
+ * Runtime 通过这个配置入口准备并读取当前 Harness 的模型、提示和工具输入。
31
29
  *
32
30
  * @remarks
33
31
  * Runtime、Pi、Turn、Tool Candidate 和 canonical transcript 等核心术语见包入口 `../../index.ts`。
@@ -37,9 +35,7 @@ import { refreshModelGuidance } from "./model-guidance";
37
35
  * 它包含已加载 Extension、Tool candidate 和快照,但 Runtime 只能读取 revision descriptor 与 degradation。
38
36
  * - revision descriptor 是影响执行行为的已排序 JSON 描述。
39
37
  * Runtime 会对它取哈希,用来识别当前 Prepared Runtime 的基础版本。
40
- * - Pinned Runtime 是 Submission 准入时固定的描述符。
41
- * 它在 Prepared Runtime 基础上加入动态 system context、approval mode 和 base revision。
42
- * 调用方:`AgentRuntimeKernel` 在配置加载、请求准入与打开 SessionHarness 前调用这些粗粒度能力。
38
+ * 调用方:`AgentRuntimeKernel` 在配置加载与打开 SessionHarness 前调用这些粗粒度能力。
43
39
  *
44
40
  * 执行位置仍由 `SessionHarness` 拥有,消息读写和投影由 `ConversationHistory` 拥有。
45
41
  */
@@ -86,23 +82,31 @@ export class HarnessRuntimeConfig {
86
82
  );
87
83
  }
88
84
 
89
- /** Read the immutable inputs consumed by the Session AgentHarness. */
90
- harnessInputs(
85
+ interaction(prepared: PreparedPiRuntime, name: string) {
86
+ return readPreparedPiRuntime(prepared, this.owner).candidates.find(
87
+ (candidate) => candidate.tool.name === name,
88
+ )?.interaction;
89
+ }
90
+
91
+ /** Read all executable inputs from the current Prepared Runtime. */
92
+ async harnessInputs(
91
93
  prepared: PreparedPiRuntime,
92
- pinnedDescriptor: string,
93
94
  request: {
94
95
  readonly modelSessionId?: string | (() => string);
95
96
  readonly onGeneration?: PiGenerationLifecycleObserver;
97
+ readonly includeMemory?: boolean;
98
+ readonly readExtensionContext?: CurrentPiContextOptions["readExtensionContext"];
96
99
  readonly assertRequestAllowed?: () => void;
97
100
  readonly onCompactionGeneration?: PiGenerationLifecycleObserver;
98
101
  } = {},
99
102
  ) {
100
- const { state, descriptor } = readPinnedPiRuntime(
103
+ const state = readPreparedPiRuntime(prepared, this.owner);
104
+ const context = await assembleCurrentPiContext({
101
105
  prepared,
102
- this.owner,
103
- pinnedDescriptor,
104
- );
105
- const pinnedModel = state.snapshot.pi.model;
106
+ includeMemory: request.includeMemory,
107
+ readExtensionContext: request.readExtensionContext,
108
+ }, this.owner);
109
+ const currentModel = state.snapshot.pi.model;
106
110
  let models!: MutableModels;
107
111
  const requestStream = (observer = request.onGeneration) => {
108
112
  const stream = withProviderBoundary(
@@ -121,8 +125,8 @@ export class HarnessRuntimeConfig {
121
125
  get: (target, property) => {
122
126
  if (property === "getModel") {
123
127
  return (provider: string, modelId: string) =>
124
- provider === pinnedModel.provider && modelId === pinnedModel.id
125
- ? pinnedModel
128
+ provider === currentModel.provider && modelId === currentModel.id
129
+ ? currentModel
126
130
  : target.getModel(provider, modelId);
127
131
  }
128
132
  if (property === "streamSimple") return requestStream();
@@ -147,7 +151,8 @@ export class HarnessRuntimeConfig {
147
151
  models,
148
152
  model: state.snapshot.pi.model,
149
153
  thinkingLevel: state.snapshot.pi.thinkingLevel,
150
- systemPrompt: descriptor.systemPrompt,
154
+ systemPrompt: context.systemPrompt,
155
+ degradations: context.degradations,
151
156
  candidates: state.candidates,
152
157
  workspace: state.snapshot.bindings.workspace,
153
158
  platform: state.snapshot.bindings.platform,
@@ -179,27 +184,12 @@ export class HarnessRuntimeConfig {
179
184
  }
180
185
  }
181
186
 
182
- /**
183
- * 在接受 Submission 前固定本次执行使用的动态上下文和审批模式。
184
- *
185
- * @remarks
186
- * 调用方:Runtime 的准入流程为每个新 Submission 调用,并把 descriptor 与其哈希一起持久化。
187
- *
188
- * 实现理由:memory、workspace 和 Extension context 可能随时间变化,所以把准入时的 Turn 字段保存在 descriptor 中。
189
- * 不要把 pin 推迟到 Turn 启动后,否则已持久化的 Submission 将失去可验证的执行基线。
190
- */
191
- pin(options: PinPiRuntimeOptions): Promise<PinnedPiRuntime> {
192
- return pinPiRuntime(options, this.owner);
193
- }
194
-
195
187
  }
196
188
 
197
189
  export type {
198
- PinPiRuntimeOptions,
199
- PinnedPiRuntime,
200
190
  PreparePiRuntimeOptions,
201
191
  PreparedPiRuntime,
202
- } from "./pinning";
192
+ } from "./prepared-runtime";
203
193
 
204
194
  /**
205
195
  * Input tokens above which a Turn compacts its history.
@@ -7,7 +7,7 @@ export * from "./assembly-view";
7
7
  export * from "./definition";
8
8
  export * from "./degradation";
9
9
  export * from "./harness-runtime";
10
- export * from "./pinning";
10
+ export * from "./prepared-runtime";
11
11
  export * from "./ports";
12
12
  export * from "./profile";
13
13
  export * from "./runtime-config";
@@ -1,4 +1,3 @@
1
- import { sha256Hex } from "../lib/artifacts";
2
1
  import type {
3
2
  RuntimeBindings,
4
3
  RuntimeGatewaySession,
@@ -9,7 +8,6 @@ import type {
9
8
  RuntimeExtensionPermissions,
10
9
  } from "../context/extension/contracts";
11
10
  import type { RuntimeProfile } from "./profile";
12
- import type { ExecutionLevel } from "../lib/execution-level";
13
11
  import type { RuntimeSnapshot } from "./assembler";
14
12
  import type { FinalizedPiToolSurface, PiRuntimeAssembly } from "../context";
15
13
  import {
@@ -28,29 +26,8 @@ import {
28
26
  type PiToolCandidate,
29
27
  } from "../tools";
30
28
 
31
- export interface RuntimeAssemblyPin {
32
- readonly revision: string;
33
- readonly descriptor: string;
34
- }
35
-
36
- /** A saved pin is reusable only with its original Runtime key and intact descriptor. */
37
- export async function validateRuntimeAssemblyPin(
38
- runtimeKey: string,
39
- pin: RuntimeAssemblyPin,
40
- ): Promise<boolean> {
41
- if (!runtimeKey) {
42
- throw new Error("Cannot safely recover a pinned Submission without its original Runtime key");
43
- }
44
- if (!pin.revision && !pin.descriptor) return false;
45
- if (!pin.revision || !pin.descriptor) throw new Error("Submission assembly pin is incomplete");
46
- if (pin.revision !== await sha256Hex(pin.descriptor)) {
47
- throw new Error("Pinned Runtime assembly descriptor is invalid");
48
- }
49
- return true;
50
- }
51
-
52
29
  /**
53
- * 本文件负责准备 Pi 的扩展与工具,并为每次提交固定可恢复的运行描述。
30
+ * 本文件负责准备当前 Pi 的扩展、工具与系统上下文。
54
31
  *
55
32
  * @remarks
56
33
  * 核心术语见本目录入口 `./index.ts`,这里不重复定义。
@@ -93,8 +70,8 @@ interface PreparePiAssemblyOptions {
93
70
  ) => Fetcher;
94
71
  }
95
72
 
96
- // 暴露后续准备和固定阶段会读取的扩展装配结果。
97
- // preparePiRuntime 读取工具与降级,pinPiRuntime 读取上下文贡献。
73
+ // 暴露后续准备和执行阶段会读取的扩展装配结果。
74
+ // preparePiRuntime 读取工具与降级,assembleCurrentPiContext 读取上下文贡献。
98
75
  // 访问器直接代理扩展装配,避免维护会漂移的副本。
99
76
  interface PreparedPiAssembly {
100
77
  readonly loaded: readonly PiLoadedExtension[];
@@ -133,10 +110,10 @@ export interface PreparePiRuntimeOptions {
133
110
  }
134
111
 
135
112
  /**
136
- * 表示已经准备好、但尚未固定到某次提交的 Pi 配置。
113
+ * 表示已准备好供当前执行使用的 Pi 配置。
137
114
  *
138
115
  * @remarks
139
- * Runtime 在配置完成后保存这个句柄,并在提交准入或创建 Turn 时传回。
116
+ * Runtime 在配置完成后保存这个句柄,并在创建 Turn 时传回。
140
117
  *
141
118
  * 公开形状只给出版本描述和加载降级,调用方不会直接拿到可变的装配状态。
142
119
  */
@@ -147,26 +124,25 @@ export interface PreparedPiRuntime {
147
124
  }
148
125
 
149
126
  /**
150
- * 提供把一次提交固定到当前 Runtime 所需的值。
127
+ * 当前执行切片读取系统上下文所需的值。
151
128
  *
152
129
  * @remarks
153
- * Runtime 在写入提交前的准入阶段把它交给 `HarnessRuntimeConfig.pin`。
130
+ * Runtime 在打开或恢复 Harness 前把它交给 `HarnessRuntimeConfig.harnessInputs`。
154
131
  *
155
132
  * 扩展没有可读上下文时不需要读取函数,所以该端口保持可选。
156
133
  */
157
- export interface PinPiRuntimeOptions {
134
+ export interface CurrentPiContextOptions {
158
135
  readonly prepared: PreparedPiRuntime;
159
- readonly baseRevision: string;
160
136
  /**
161
137
  * Whether this Submission may inherit automatic hot and cold Memory context.
162
138
  * Scheduled task input is intentionally isolated from those sources.
163
139
  */
164
140
  readonly includeMemory?: boolean;
165
141
  /**
166
- * 读取一个扩展要写入固定系统提示词的上下文。
142
+ * 读取一个扩展对当前系统提示词的上下文贡献。
167
143
  *
168
144
  * @remarks
169
- * pinPiRuntime 在准入阶段为已加载扩展声明的上下文贡献调用它。
145
+ * 每次装配 Harness 输入时为已加载扩展读取当前上下文。
170
146
  *
171
147
  * 本模块不持有扩展上下文存储,所以读取端口必须留在 Host。
172
148
  */
@@ -175,40 +151,6 @@ export interface PinPiRuntimeOptions {
175
151
  ) => Promise<string | null>;
176
152
  }
177
153
 
178
- /**
179
- * 返回已经固定到一次提交的描述和上下文降级。
180
- *
181
- * @remarks
182
- * Runtime 在准入时持久化描述,并在创建 Turn 时传回。
183
- *
184
- * 固定这些值可让恢复继续使用当时已经准入的配置。
185
- */
186
- export interface PinnedPiRuntime {
187
- readonly descriptor: string;
188
- readonly degradations: readonly RuntimeDegradation[];
189
- }
190
-
191
- /**
192
- * 描述一条已存提交可以恢复到哪份 Pi 配置。
193
- *
194
- * @remarks
195
- * `HarnessRuntimeConfig.harnessInputs` 在打开或恢复 operation 前读取这个 JSON 结构。
196
- *
197
- * 基础版本作为准入时元数据保留,创建 Turn 时不与当前 Runtime 比较。
198
- */
199
- export interface PinnedPiRuntimeDescriptor {
200
- readonly version: 1;
201
- readonly baseRevision: string;
202
- readonly model: string;
203
- readonly provider: string;
204
- readonly thinking: RuntimeSnapshot["pi"]["thinkingLevel"];
205
- readonly systemPrompt: string;
206
- readonly executionLevel: ExecutionLevel;
207
- readonly deny: readonly string[];
208
- readonly tools: readonly ReturnType<typeof describeTools>[number][];
209
- readonly [key: string]: unknown;
210
- }
211
-
212
154
  /**
213
155
  * 保存创建 Pi Turn 所需的内部准备结果。
214
156
  *
@@ -345,7 +287,7 @@ class PreparedRuntime implements PreparedPiRuntime {
345
287
  ) {}
346
288
 
347
289
  // 在调用方证明适配器身份后返回内部准备状态。
348
- // readPreparedPiRuntime 在固定配置或创建 Turn 前调用它。
290
+ // readPreparedPiRuntime 在创建 Turn 前调用它。
349
291
  // 公开接口没有可信的来源字段,所以这里必须比较对象身份。
350
292
  read(owner: object): PreparedPiRuntimeState {
351
293
  if (owner !== this.owner) {
@@ -361,7 +303,7 @@ class PreparedRuntime implements PreparedPiRuntime {
361
303
  * 为创建它的 HarnessRuntimeConfig 打开准备句柄。
362
304
  *
363
305
  * @remarks
364
- * 固定配置和创建 Turn 都会先调用它,再读取快照、装配结果或工具候选。
306
+ * 创建 Turn 前调用它,读取快照、装配结果或工具候选。
365
307
  *
366
308
  * 具体类检查与 owner 检查一起拒绝伪造句柄和跨适配器复用。
367
309
  */
@@ -375,42 +317,6 @@ export function readPreparedPiRuntime(
375
317
  return prepared.read(owner);
376
318
  }
377
319
 
378
- /**
379
- * 读取已存的固定描述。
380
- *
381
- * @remarks
382
- * `HarnessRuntimeConfig.harnessInputs` 在新建或恢复 Pi operation 前调用它。
383
- *
384
- * Prepared Runtime 提供当前执行能力,descriptor 仅保留 Submission 固定的 Turn 字段。
385
- */
386
- export function readPinnedPiRuntime(
387
- prepared: PreparedPiRuntime,
388
- owner: object,
389
- descriptorBody: string,
390
- ): {
391
- readonly state: PreparedPiRuntimeState;
392
- readonly descriptor: PinnedPiRuntimeDescriptor;
393
- } {
394
- const state = readPreparedPiRuntime(prepared, owner);
395
- let value: unknown;
396
- try {
397
- value = JSON.parse(descriptorBody);
398
- } catch {
399
- throw new Error("Pinned Runtime assembly descriptor is invalid");
400
- }
401
- if (!value || typeof value !== "object") {
402
- throw new Error("Pinned Runtime assembly descriptor is invalid");
403
- }
404
- const descriptor = value as PinnedPiRuntimeDescriptor;
405
- if (
406
- descriptor.version !== 1 ||
407
- typeof descriptor.systemPrompt !== "string"
408
- ) {
409
- throw new Error("Pinned Runtime assembly descriptor is invalid");
410
- }
411
- return { state, descriptor };
412
- }
413
-
414
320
  // #endregion
415
321
 
416
322
  // #region 扩展与工具装配
@@ -462,7 +368,7 @@ async function preparePiAssembly(
462
368
 
463
369
  // #endregion
464
370
 
465
- // #region Runtime 准备与提交固定
371
+ // #region Runtime 准备与当前上下文
466
372
 
467
373
  /**
468
374
  * 为一份已校验的 Runtime 快照准备扩展和工具状态。
@@ -499,17 +405,17 @@ export async function preparePiRuntime(
499
405
  }
500
406
 
501
407
  /**
502
- * 把请求时的上下文和执行档位固定到一次提交的描述中。
408
+ * 从当前 Runtime 装配模型要用的系统上下文。
503
409
  *
504
410
  * @remarks
505
- * Runtime 在持久化提交并启动 Turn 前的准入阶段调用它。
411
+ * Runtime 每次打开或恢复 Harness 前调用它。
506
412
  *
507
- * 此时重建上下文可避免把动态记忆和扩展上下文写进 Pi 消息,同时让恢复继续使用已准入的结果。
413
+ * 重新读取动态记忆和扩展上下文,不从旧 Submission 描述恢复配置。
508
414
  */
509
- export async function pinPiRuntime(
510
- options: PinPiRuntimeOptions,
415
+ export async function assembleCurrentPiContext(
416
+ options: CurrentPiContextOptions,
511
417
  owner: object,
512
- ): Promise<PinnedPiRuntime> {
418
+ ): Promise<{ systemPrompt: string; degradations: readonly RuntimeDegradation[] }> {
513
419
  const state = readPreparedPiRuntime(options.prepared, owner);
514
420
  const { snapshot, assembly } = state;
515
421
  const includeMemory = options.includeMemory !== false;
@@ -525,11 +431,7 @@ export async function pinPiRuntime(
525
431
  readExtensionContext: options.readExtensionContext,
526
432
  });
527
433
  return Object.freeze({
528
- descriptor: JSON.stringify({
529
- ...JSON.parse(options.prepared.revisionDescriptor),
530
- systemPrompt: context.systemPrompt,
531
- baseRevision: options.baseRevision,
532
- }),
434
+ systemPrompt: context.systemPrompt,
533
435
  degradations: context.degradations,
534
436
  });
535
437
  }
@@ -1,4 +1,5 @@
1
1
  export interface RuntimeConfigUpdateResult<Change> {
2
2
  readonly change: Change;
3
- readonly runtime: "unchanged" | "reloaded" | "reload-failed";
3
+ /** Deferred changes are persisted but activated at the next execution slice. */
4
+ readonly runtime: "unchanged" | "reloaded" | "deferred" | "reload-failed";
4
5
  }
@@ -17,13 +17,25 @@ export type PiCanonicalUserInput = UserMessage;
17
17
 
18
18
  export type UIChatTrigger = "submit-message" | "regenerate-message";
19
19
 
20
- /** A user-selected Runtime capability persisted with the visible UIMessage. */
20
+ /**
21
+ * A user-selected Runtime capability persisted with the visible UIMessage.
22
+ *
23
+ * - `skill`: `name` is the installed Skill name.
24
+ * - `tool`: `name` is a Tool materialized on the Runtime surface.
25
+ * - `plan`: `name` is always `"plan"`.
26
+ * - `action`: `name` is a host-owned Action capability id. The Runtime only adds
27
+ * `requested capability: action/<id>` to private model context; the host is
28
+ * responsible for telling the model how to act on it.
29
+ *
30
+ * `label` is the display name; capability ids never enter visible message text.
31
+ * `iconUrl` is a display-only `https:` icon (for example an Action supplier's
32
+ * logo); it is persisted with the message and never sent to the model.
33
+ */
21
34
  export interface RequestedCapability {
22
- readonly kind: "skill" | "tool" | "plan";
35
+ readonly kind: "skill" | "tool" | "plan" | "action";
23
36
  readonly name: string;
24
37
  readonly label: string;
25
- /** Stable Resource identity used only to group capabilities in presentation. */
26
- readonly resourceId?: string;
38
+ readonly iconUrl?: string;
27
39
  }
28
40
 
29
41
  export interface UIChatRequestBody {
@@ -2,6 +2,32 @@ import type { UserMessage } from "@earendil-works/pi-ai";
2
2
  import type { UIMessage } from "ai";
3
3
  import type { RequestedCapability } from "./contract";
4
4
 
5
+ const REQUESTED_CAPABILITY_KINDS: ReadonlySet<RequestedCapability["kind"]> = new Set([
6
+ "skill",
7
+ "tool",
8
+ "plan",
9
+ "action",
10
+ ]);
11
+
12
+ function isRequestedCapabilityKind(value: unknown): value is RequestedCapability["kind"] {
13
+ return typeof value === "string" &&
14
+ REQUESTED_CAPABILITY_KINDS.has(value as RequestedCapability["kind"]);
15
+ }
16
+
17
+ const MAX_CAPABILITY_ICON_URL_LENGTH = 2048;
18
+
19
+ /** Keep a display-only icon only when it is an `https:` URL; drop anything else silently. */
20
+ function capabilityIconUrlOf(value: unknown): string | undefined {
21
+ if (typeof value !== "string" || value.length > MAX_CAPABILITY_ICON_URL_LENGTH) {
22
+ return undefined;
23
+ }
24
+ try {
25
+ return new URL(value).protocol === "https:" ? value : undefined;
26
+ } catch {
27
+ return undefined;
28
+ }
29
+ }
30
+
5
31
  export function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[] {
6
32
  if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
7
33
  return [];
@@ -19,7 +45,7 @@ export function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[
19
45
  }
20
46
  const capability = value as Record<string, unknown>;
21
47
  if (
22
- (capability.kind !== "skill" && capability.kind !== "tool" && capability.kind !== "plan") ||
48
+ !isRequestedCapabilityKind(capability.kind) ||
23
49
  typeof capability.name !== "string" ||
24
50
  !capability.name.trim() ||
25
51
  typeof capability.label !== "string" ||
@@ -30,7 +56,15 @@ export function requestedCapabilitiesOf(metadata: unknown): RequestedCapability[
30
56
  const key = `${capability.kind}:${capability.name}`;
31
57
  if (seen.has(key)) continue;
32
58
  seen.add(key);
33
- result.push({ kind: capability.kind, name: capability.name, label: capability.label });
59
+ // Copy only the current fields: persisted messages may still carry legacy
60
+ // fields such as the retired `resourceId`, which are ignored.
61
+ const iconUrl = capabilityIconUrlOf(capability.iconUrl);
62
+ result.push({
63
+ kind: capability.kind,
64
+ name: capability.name,
65
+ label: capability.label,
66
+ ...(iconUrl ? { iconUrl } : {}),
67
+ });
34
68
  }
35
69
  return result;
36
70
  }
@@ -65,6 +99,13 @@ export function normalizeUIUserInput(
65
99
  );
66
100
  continue;
67
101
  }
102
+ if (capability.kind === "action") {
103
+ // Actions are host capabilities, not Runtime Tools: the Runtime only
104
+ // names the request and does not check installation. The host owns how
105
+ // the model acts on `action/<id>` (its tools and system guidance).
106
+ context.push(`requested capability: action/${capability.name}`);
107
+ continue;
108
+ }
68
109
  if (capability.name !== "plan") {
69
110
  throw new Error(`Unknown plan capability: ${capability.name}`);
70
111
  }
@@ -184,6 +184,8 @@ export class SessionHarness {
184
184
  runtime.requestId = unfinished[0].requestId;
185
185
  }
186
186
  await runtime.restoreGovernance(context);
187
+ // Harness.open is passive; install the current Config before anything resumes scheduling.
188
+ await runtime.applyCurrentConfiguration(context);
187
189
  return runtime;
188
190
  }
189
191
 
@@ -236,6 +238,22 @@ export class SessionHarness {
236
238
  this.conversation = (await this.harness.conversation(created.id, context))!;
237
239
  this.turnsAtOpen = (await this.entries(context)).filter((entry) => entry.kind === "pi.assistant").length;
238
240
  }
241
+ private async applyCurrentConfiguration(context: Context): Promise<void> {
242
+ const names = new Set(this.options.activeToolNames ?? this.options.tools?.map((tool) => tool.name) ?? []);
243
+ const previous = await this.harness.snapshot(AgentDoc, this.conversation.id, context);
244
+ if (Array.isArray(previous?.tools)) {
245
+ for (const name of previous.tools) if (this.options.onDemandToolNames?.includes(name)) names.add(name);
246
+ }
247
+ await this.conversation.configure({
248
+ model: { provider: this.options.model.provider, modelId: this.options.model.id },
249
+ thinkingLevel: this.options.thinkingLevel ?? "off",
250
+ tools: this.wrapUpStarted || this.governance.wrapUp
251
+ ? []
252
+ : this.options.tools?.filter((tool) => names.has(tool.name)) ?? [],
253
+ // Wrap-up instructions belong to one Submission and are supplied by the request hook.
254
+ instructions: null,
255
+ }, context);
256
+ }
239
257
  async admit(request: SessionRunRequest, context: Context): Promise<void> {
240
258
  if (this.submission && this.requestId !== request.operationId) throw new Error("A native Pi Submission is already active");
241
259
  const existing = await this.submissionFor(request.operationId, context);
@@ -244,12 +262,9 @@ export class SessionHarness {
244
262
  this.submission = await this.harness.submission(existing.id, context);
245
263
  } else {
246
264
  if (request.prompt === undefined) throw new Error("A new native Pi Submission needs input");
247
- const names = new Set(this.options.activeToolNames ?? this.options.tools?.map((tool) => tool.name) ?? []);
248
- const previous = await this.harness.snapshot(AgentDoc, this.conversation.id, context);
249
- if (Array.isArray(previous?.tools)) for (const name of previous.tools) if (this.options.onDemandToolNames?.includes(name)) names.add(name);
250
- await this.conversation.configure({ model: { provider: this.options.model.provider, modelId: this.options.model.id }, thinkingLevel: this.options.thinkingLevel ?? "off", tools: this.options.tools?.filter((tool) => names.has(tool.name)) ?? [],
251
- // Earlier runtimes stored the wrap-up instruction in the agent; it applies to one Submission only.
252
- instructions: null }, context);
265
+ this.wrapUpStarted = false;
266
+ this.governance = new ToolGovernance();
267
+ await this.applyCurrentConfiguration(context);
253
268
  this.requestId = request.operationId;
254
269
  this.submission = await this.conversation.submit({ type: "input", requestId: request.operationId, content: typeof request.prompt === "string" ? request.prompt : request.prompt.content, whenBusy: "reject" }, context);
255
270
  }