@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.
- package/package.json +1 -1
- package/src/adapter/cloudflare/agent.ts +79 -80
- package/src/adapter/cloudflare/define-agent.ts +220 -93
- package/src/adapter/cloudflare/schema.ts +6 -2
- package/src/config/activation.ts +13 -15
- package/src/config/harness-runtime.ts +26 -36
- package/src/config/index.ts +1 -1
- package/src/config/{pinning.ts → prepared-runtime.ts} +20 -118
- package/src/config/runtime-config.ts +2 -1
- package/src/conversation/message/contract.ts +16 -4
- package/src/conversation/message/conversion.ts +43 -2
- package/src/harness/session-harness.ts +21 -6
- package/src/submission/admission.ts +8 -29
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +39 -30
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/tools/assistant/ask-user.ts +3 -1
- package/src/tools/extension/index.ts +2 -2
- package/src/tools/web/search/api.ts +1 -1
|
@@ -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
|
}
|
package/src/config/activation.ts
CHANGED
|
@@ -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 "./
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
assembleCurrentPiContext,
|
|
19
19
|
preparePiRuntime,
|
|
20
|
-
readPinnedPiRuntime,
|
|
21
20
|
readPreparedPiRuntime,
|
|
22
|
-
type
|
|
23
|
-
type PinnedPiRuntime,
|
|
21
|
+
type CurrentPiContextOptions,
|
|
24
22
|
type PreparePiRuntimeOptions,
|
|
25
23
|
type PreparedPiRuntime,
|
|
26
|
-
} from "./
|
|
24
|
+
} from "./prepared-runtime";
|
|
27
25
|
import { refreshModelGuidance } from "./model-guidance";
|
|
28
26
|
|
|
29
27
|
/**
|
|
30
|
-
* Runtime
|
|
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
|
-
*
|
|
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
|
-
|
|
90
|
-
|
|
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
|
|
103
|
+
const state = readPreparedPiRuntime(prepared, this.owner);
|
|
104
|
+
const context = await assembleCurrentPiContext({
|
|
101
105
|
prepared,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
const
|
|
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 ===
|
|
125
|
-
?
|
|
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:
|
|
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 "./
|
|
192
|
+
} from "./prepared-runtime";
|
|
203
193
|
|
|
204
194
|
/**
|
|
205
195
|
* Input tokens above which a Turn compacts its history.
|
package/src/config/index.ts
CHANGED
|
@@ -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 "./
|
|
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
|
-
*
|
|
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 读取工具与降级,
|
|
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
|
-
*
|
|
113
|
+
* 表示已准备好供当前执行使用的 Pi 配置。
|
|
137
114
|
*
|
|
138
115
|
* @remarks
|
|
139
|
-
* Runtime
|
|
116
|
+
* Runtime 在配置完成后保存这个句柄,并在创建 Turn 时传回。
|
|
140
117
|
*
|
|
141
118
|
* 公开形状只给出版本描述和加载降级,调用方不会直接拿到可变的装配状态。
|
|
142
119
|
*/
|
|
@@ -147,26 +124,25 @@ export interface PreparedPiRuntime {
|
|
|
147
124
|
}
|
|
148
125
|
|
|
149
126
|
/**
|
|
150
|
-
*
|
|
127
|
+
* 当前执行切片读取系统上下文所需的值。
|
|
151
128
|
*
|
|
152
129
|
* @remarks
|
|
153
|
-
* Runtime
|
|
130
|
+
* Runtime 在打开或恢复 Harness 前把它交给 `HarnessRuntimeConfig.harnessInputs`。
|
|
154
131
|
*
|
|
155
132
|
* 扩展没有可读上下文时不需要读取函数,所以该端口保持可选。
|
|
156
133
|
*/
|
|
157
|
-
export interface
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
411
|
+
* Runtime 每次打开或恢复 Harness 前调用它。
|
|
506
412
|
*
|
|
507
|
-
*
|
|
413
|
+
* 重新读取动态记忆和扩展上下文,不从旧 Submission 描述恢复配置。
|
|
508
414
|
*/
|
|
509
|
-
export async function
|
|
510
|
-
options:
|
|
415
|
+
export async function assembleCurrentPiContext(
|
|
416
|
+
options: CurrentPiContextOptions,
|
|
511
417
|
owner: object,
|
|
512
|
-
): Promise<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
}
|