@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.1
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 +3 -3
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1539 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/{db → adapter/cloudflare}/schema.ts +16 -83
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +182 -0
- package/src/config/index.ts +14 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
- package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +98 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +76 -10
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +175 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +481 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/mcp.ts +1 -1
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/submission/admission.ts +275 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +61 -118
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
- package/src/tools/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
- package/src/{pi/tool → tools}/base.ts +14 -30
- package/src/tools/compiler.ts +327 -0
- package/src/{pi/tool → tools}/core-host.ts +3 -3
- package/src/{pi/tool → tools}/core.ts +3 -3
- package/src/tools/decision-gate.ts +227 -0
- package/src/{pi/tool → tools}/gateway.ts +1 -1
- package/src/tools/human-decisions.ts +127 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/interaction/index.ts +1 -0
- package/src/{pi/tool → tools}/mcp.ts +1 -1
- package/src/{pi/tool → tools}/schedule.ts +3 -3
- package/src/{pi/tool → tools}/skill.ts +4 -4
- package/src/{kernel → tools}/subagent-runtime.ts +1 -1
- package/src/{pi/tool → tools}/subagent.ts +4 -4
- package/src/tools/temporary-agent/approvals.ts +126 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
- package/src/tools/temporary-agent/core.ts +53 -0
- package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
- package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
- package/src/{pi/tool → tools}/web-search/api.ts +1 -1
- package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
- package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
- package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- /package/src/{kernel → config}/degradation.ts +0 -0
- /package/src/{kernel → config}/runtime-config.ts +0 -0
- /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- /package/src/{pi/tool → tools}/declared.ts +0 -0
- /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
- /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
- /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
- /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
- /package/src/{pi/tool → tools}/time.ts +0 -0
- /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
- /package/src/{workspace-versioning.ts → tools/workspace-versioning.ts} +0 -0
|
@@ -0,0 +1,1539 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RuntimeTelemetryCoordinator,
|
|
3
|
+
RuntimeTurnEvents,
|
|
4
|
+
} from "../../telemetry";
|
|
5
|
+
import {
|
|
6
|
+
Agent,
|
|
7
|
+
type AgentToolLifecycleResult,
|
|
8
|
+
type AgentToolRunInfo,
|
|
9
|
+
type Connection,
|
|
10
|
+
} from "agents";
|
|
11
|
+
import {
|
|
12
|
+
MessageType,
|
|
13
|
+
clearChatTerminal,
|
|
14
|
+
parseProtocolMessage,
|
|
15
|
+
recordChatTerminal,
|
|
16
|
+
sendIfOpen,
|
|
17
|
+
} from "agents/chat";
|
|
18
|
+
import {
|
|
19
|
+
isTerminalSubmissionStatus,
|
|
20
|
+
SubmissionLifecycle,
|
|
21
|
+
SubmissionAdmission,
|
|
22
|
+
SubmissionDelivery,
|
|
23
|
+
SubmissionFinalizer,
|
|
24
|
+
projectSubmissionReceipt,
|
|
25
|
+
PendingSteerDelivery,
|
|
26
|
+
type StoredSubmission,
|
|
27
|
+
type SubmitMessageOptions,
|
|
28
|
+
USER_STOP_REASON,
|
|
29
|
+
type ApprovalDecision,
|
|
30
|
+
type ApprovalReceipt,
|
|
31
|
+
type MessageDelivery,
|
|
32
|
+
type MessageDispatchReceipt,
|
|
33
|
+
type SubmissionReceipt,
|
|
34
|
+
type SubmissionExecutionMode,
|
|
35
|
+
type SubmissionHandle,
|
|
36
|
+
} from "../../submission";
|
|
37
|
+
import type { ExecutionLevel } from "../../lib/execution-level";
|
|
38
|
+
import {
|
|
39
|
+
HarnessRuntimeConfig,
|
|
40
|
+
activateRuntimeCandidate,
|
|
41
|
+
type ActiveRuntime,
|
|
42
|
+
RuntimeLoadTracker,
|
|
43
|
+
prepareRuntimeCandidate,
|
|
44
|
+
projectRuntimeAssembly,
|
|
45
|
+
type PreparedPiRuntime,
|
|
46
|
+
type RuntimeAssemblyInput,
|
|
47
|
+
type RuntimeAssemblyView,
|
|
48
|
+
type RuntimeCandidate,
|
|
49
|
+
} from "../../config";
|
|
50
|
+
import type {
|
|
51
|
+
RuntimeExtensionPermissions,
|
|
52
|
+
} from "../../context";
|
|
53
|
+
import {
|
|
54
|
+
ConversationHistory,
|
|
55
|
+
HarnessConversationBridge,
|
|
56
|
+
bindHarnessStream,
|
|
57
|
+
projectSubmissionOutcome,
|
|
58
|
+
normalizeUIUserInput,
|
|
59
|
+
requestedCapabilitiesOf,
|
|
60
|
+
projectRuntimeState,
|
|
61
|
+
type PiCanonicalTranscriptSnapshot,
|
|
62
|
+
type PiCanonicalUserInput,
|
|
63
|
+
type RuntimeState,
|
|
64
|
+
type UIChatRequestBody,
|
|
65
|
+
} from "../../conversation";
|
|
66
|
+
import type { UIMessage, UIMessageChunk } from "ai";
|
|
67
|
+
import type {
|
|
68
|
+
AssistantMessage,
|
|
69
|
+
} from "@earendil-works/pi-ai";
|
|
70
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core/harness/context";
|
|
71
|
+
import { openCloudflareSession } from "../../session";
|
|
72
|
+
import { openSessionHarness, prepareSubmissionOperation } from "../../harness";
|
|
73
|
+
import type { SessionHarness } from "../../harness";
|
|
74
|
+
import {
|
|
75
|
+
compileHarnessTools,
|
|
76
|
+
decodeSubagentCompletion,
|
|
77
|
+
HarnessDecisionGate,
|
|
78
|
+
HarnessHumanDecisions,
|
|
79
|
+
runtimeSubagentRunMetadata,
|
|
80
|
+
TemporaryAgentCoordinator,
|
|
81
|
+
TemporaryAgentApprovals,
|
|
82
|
+
type TemporaryAgentApprovalDecision,
|
|
83
|
+
type TemporaryAgentApprovalRequest,
|
|
84
|
+
type TemporaryAgentExecutor,
|
|
85
|
+
type TemporaryAgentRequest,
|
|
86
|
+
type TemporaryAgentRunContext,
|
|
87
|
+
} from "../../tools";
|
|
88
|
+
import { sha256Hex } from "../../lib/artifacts";
|
|
89
|
+
import { RuntimeDatabase } from "./database";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 本文件负责把 Cloudflare Agent 的通信与持久化能力接到 Pi Turn。
|
|
93
|
+
*
|
|
94
|
+
* @remarks
|
|
95
|
+
* 核心术语见包入口 `index.ts`。
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
// #region Runtime 内部数据与基础工具
|
|
99
|
+
|
|
100
|
+
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
101
|
+
interface ActiveTurn {
|
|
102
|
+
submissionId: string;
|
|
103
|
+
requestId: string;
|
|
104
|
+
messageId: string;
|
|
105
|
+
startedAt: number;
|
|
106
|
+
continuation: boolean;
|
|
107
|
+
/** Execution adapter — use abort()/steer() instead of touching Pi directly. */
|
|
108
|
+
agent: {
|
|
109
|
+
abort(): Promise<void>;
|
|
110
|
+
interrupt(): void;
|
|
111
|
+
steer(message: PiCanonicalUserInput, messageId: string): Promise<void>;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function errorText(error: unknown): string {
|
|
116
|
+
return error instanceof Error ? error.message : String(error);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function json(value: unknown): string {
|
|
120
|
+
return JSON.stringify(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface ToolInteractionResponsePreparation {
|
|
124
|
+
readonly toolCallId: string;
|
|
125
|
+
readonly toolName: string;
|
|
126
|
+
readonly toolInput: unknown;
|
|
127
|
+
readonly response: unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// #endregion
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 这是 UniversalAgent 唯一的生产 Runtime Kernel。
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* Cloudflare Agent 负责 Durable Object 生命周期、SQLite、facet、
|
|
137
|
+
* WebSocket、定时任务和 fiber。
|
|
138
|
+
*
|
|
139
|
+
* Pi 负责模型与 Tool 循环和 canonical transcript。
|
|
140
|
+
*
|
|
141
|
+
* 生产 Agent 由 `defineRuntimeAgent` 生成子类,应用不直接实例化本类。
|
|
142
|
+
*/
|
|
143
|
+
export abstract class AgentRuntimeKernel<
|
|
144
|
+
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
145
|
+
> extends Agent<Env, RuntimeState> {
|
|
146
|
+
|
|
147
|
+
initialState: RuntimeState = {
|
|
148
|
+
runtimeLoad: { status: "idle", available: false },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
protected abstract ensureRuntimeReady(): Promise<void>;
|
|
152
|
+
/** Runs only before the first pin of a newly activated Submission. */
|
|
153
|
+
protected prepareRuntimeForSubmission(_submissionId: string): Promise<void> {
|
|
154
|
+
return this.ensureRuntimeReady();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
protected ensureRuntimeVersion(_runtimeKey: string): Promise<void> {
|
|
158
|
+
return this.ensureRuntimeReady();
|
|
159
|
+
}
|
|
160
|
+
protected get submissionContext() {
|
|
161
|
+
return {
|
|
162
|
+
id: this.submissions.currentActive()?.submissionId ?? this.db.submissions.findRunning()?.submissionId,
|
|
163
|
+
unfinishedIds: this.db.submissions.listUnfinishedIds(),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Let the Host prepare a validated client response before Tool settlement. */
|
|
168
|
+
protected prepareToolInteractionResponse(
|
|
169
|
+
input: ToolInteractionResponsePreparation,
|
|
170
|
+
): Promise<unknown | null> {
|
|
171
|
+
return Promise.resolve(input.response);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private activeRuntime?: ActiveRuntime;
|
|
175
|
+
private telemetryCoordinator?: RuntimeTelemetryCoordinator;
|
|
176
|
+
private activeHarness?: SessionHarness<object | undefined>;
|
|
177
|
+
private piAdapter?: HarnessRuntimeConfig;
|
|
178
|
+
private readonly transcript: ConversationHistory;
|
|
179
|
+
private conversationBridge?: HarnessConversationBridge;
|
|
180
|
+
private readonly submissions: SubmissionLifecycle<
|
|
181
|
+
StoredSubmission,
|
|
182
|
+
ActiveTurn
|
|
183
|
+
>;
|
|
184
|
+
private runtimeLoadTracker?: RuntimeLoadTracker;
|
|
185
|
+
private turnEventCoordinator?: RuntimeTurnEvents;
|
|
186
|
+
private db!: RuntimeDatabase;
|
|
187
|
+
private readonly temporaryAgents = new TemporaryAgentCoordinator();
|
|
188
|
+
private submissionAdmission?: SubmissionAdmission;
|
|
189
|
+
private submissionDelivery?: SubmissionDelivery;
|
|
190
|
+
private submissionFinalizer?: SubmissionFinalizer;
|
|
191
|
+
private humanDecisionCoordinator?: HarnessHumanDecisions;
|
|
192
|
+
private readonly temporaryAgentApprovals = new TemporaryAgentApprovals({
|
|
193
|
+
capture: (method, input) => this.telemetry.capture(method, input),
|
|
194
|
+
onChanged: () => this.broadcastApprovals(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// #region 构造、装配与启动
|
|
198
|
+
|
|
199
|
+
private get pi(): HarnessRuntimeConfig {
|
|
200
|
+
return this.piAdapter ??= new HarnessRuntimeConfig();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Runtime 装载进度的状态机。
|
|
205
|
+
*
|
|
206
|
+
* @remarks
|
|
207
|
+
* 本类和 `defineRuntimeAgent` 生成的宿主共同驱动它:宿主管一轮尝试的起止,
|
|
208
|
+
* `initConfig` 管中途的阶段推进,因此它是 protected 而非 private。
|
|
209
|
+
*
|
|
210
|
+
* 写成惰性访问器而不是字段:本类有测试用 `Object.create(prototype)` 造替身,
|
|
211
|
+
* 那条路径既不跑构造函数也不跑字段初始化器。
|
|
212
|
+
*/
|
|
213
|
+
protected get runtimeLoad(): RuntimeLoadTracker {
|
|
214
|
+
return this.runtimeLoadTracker ??= new RuntimeLoadTracker({
|
|
215
|
+
read: () => this.state.runtimeLoad,
|
|
216
|
+
publish: (runtimeLoad) => this.setState({ ...this.state, runtimeLoad }),
|
|
217
|
+
isAvailable: () => Boolean(this.activeRuntime),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private get telemetry(): RuntimeTelemetryCoordinator {
|
|
222
|
+
return this.telemetryCoordinator ??= new RuntimeTelemetryCoordinator({
|
|
223
|
+
outbox: () => this.db?.telemetry,
|
|
224
|
+
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
225
|
+
scheduleRetry: async (delaySeconds, idempotent) => {
|
|
226
|
+
await this.schedule(
|
|
227
|
+
delaySeconds,
|
|
228
|
+
"_drainTelemetry",
|
|
229
|
+
undefined,
|
|
230
|
+
{ idempotent },
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
onError: (error) => {
|
|
234
|
+
console.warn(
|
|
235
|
+
"[runtime-telemetry:degraded]",
|
|
236
|
+
json({ error: errorText(error) }),
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 为一个 Cloudflare Durable Object 实例建立持久化与执行协作器。
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
246
|
+
* Cloudflare Runtime 创建由 `defineRuntimeAgent` 生成的类时,间接调用。
|
|
247
|
+
*
|
|
248
|
+
* 数据库 schema、Submission、Transcript 和 Approval 在同一构造路径绑定,
|
|
249
|
+
* 避免它们引用不同的事务边界或 Pi 适配器。
|
|
250
|
+
*/
|
|
251
|
+
constructor(
|
|
252
|
+
ctx: DurableObjectState,
|
|
253
|
+
env: Env,
|
|
254
|
+
pi: HarnessRuntimeConfig = new HarnessRuntimeConfig(),
|
|
255
|
+
) {
|
|
256
|
+
super(ctx, env);
|
|
257
|
+
this.piAdapter = pi;
|
|
258
|
+
this.db = new RuntimeDatabase(
|
|
259
|
+
this.sql.bind(this),
|
|
260
|
+
(fn) => this.ctx.storage.transactionSync(fn),
|
|
261
|
+
);
|
|
262
|
+
this.db.initializeSchema();
|
|
263
|
+
this.submissions = new SubmissionLifecycle({
|
|
264
|
+
store: this.db.submissions,
|
|
265
|
+
clearTerminal: () => clearChatTerminal(this.ctx.storage),
|
|
266
|
+
execute: (submissionId, mode) =>
|
|
267
|
+
this.executeSubmission(submissionId, mode),
|
|
268
|
+
commitTerminal: (submission, outcome, message) =>
|
|
269
|
+
this.commitTerminalOutcome(submission, outcome, message),
|
|
270
|
+
abortActive: (turn) => turn.agent.abort(),
|
|
271
|
+
abortDormant: (submission) =>
|
|
272
|
+
this.executeSubmission(submission.submissionId, "continuation")
|
|
273
|
+
.then(() => undefined),
|
|
274
|
+
interruptActive: (turn) => turn.agent.interrupt(),
|
|
275
|
+
});
|
|
276
|
+
this.transcript = new ConversationHistory(
|
|
277
|
+
this.ctx.storage,
|
|
278
|
+
{
|
|
279
|
+
transaction: (run) => this.db.transaction(run),
|
|
280
|
+
upsertUserSidecar: (id, body) =>
|
|
281
|
+
this.db.messageUi.upsert(id, body),
|
|
282
|
+
readUserSidecar: (id) => this.db.messageUi.get(id),
|
|
283
|
+
clearUserSidecars: () => this.db.messageUi.clear(),
|
|
284
|
+
listSessionEntrySubmissionLinks: () =>
|
|
285
|
+
this.db.submissions.listSessionEntrySubmissionLinks(),
|
|
286
|
+
findSubmissionProjection: (submissionId) =>
|
|
287
|
+
this.db.submissions.find(submissionId),
|
|
288
|
+
listApprovalViews: (submissionId) =>
|
|
289
|
+
this.db.approvals.listApprovalViews(submissionId),
|
|
290
|
+
},
|
|
291
|
+
() => this.submissions.isBusy(),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
/** Prepare fully before replacing the active Runtime snapshot. */
|
|
295
|
+
protected async initConfig(input: RuntimeAssemblyInput): Promise<void> {
|
|
296
|
+
const candidate = await prepareRuntimeCandidate(input);
|
|
297
|
+
await this.initCandidate(candidate);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
|
|
301
|
+
await activateRuntimeCandidate(candidate, {
|
|
302
|
+
previous: this.activeRuntime,
|
|
303
|
+
pi: this.pi,
|
|
304
|
+
mcpHost: this,
|
|
305
|
+
sessionId: this.ctx.id.toString(),
|
|
306
|
+
createExtensionHostBinding: (permissions, labels) =>
|
|
307
|
+
this.createExtensionHostBinding(permissions, labels),
|
|
308
|
+
advance: (phase) => this.runtimeLoad.advance(phase),
|
|
309
|
+
isExecuting: () => Boolean(this.submissions.currentActive()),
|
|
310
|
+
commit: (runtime) => {
|
|
311
|
+
this.telemetry.configure(candidate.telemetry);
|
|
312
|
+
this.activeRuntime = runtime;
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async _drainTelemetry(): Promise<void> {
|
|
318
|
+
await this.ensureRuntimeReady();
|
|
319
|
+
await this.telemetry.drainScheduled();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private get conversation(): HarnessConversationBridge {
|
|
323
|
+
return this.conversationBridge ??= new HarnessConversationBridge({
|
|
324
|
+
history: this.transcript,
|
|
325
|
+
send: (connection, body) => { sendIfOpen(connection, body); },
|
|
326
|
+
broadcast: (body, excluded) => this.broadcast(body, excluded),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private get humanDecisions(): HarnessHumanDecisions {
|
|
331
|
+
return this.humanDecisionCoordinator ??= new HarnessHumanDecisions({
|
|
332
|
+
approvals: this.db.approvals,
|
|
333
|
+
interactions: this.db.interactions,
|
|
334
|
+
transaction: (run) => this.db.transaction(run),
|
|
335
|
+
isSubmissionActive: (id) => {
|
|
336
|
+
const submission = this.db.submissions.find(id);
|
|
337
|
+
return Boolean(submission && !submission.abortReason && !isTerminalSubmissionStatus(submission.status));
|
|
338
|
+
},
|
|
339
|
+
telemetry: this.telemetry,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private get runtimeTurnEvents(): RuntimeTurnEvents {
|
|
344
|
+
return this.turnEventCoordinator ??= new RuntimeTurnEvents({
|
|
345
|
+
outbox: this.db.runtimeEvents,
|
|
346
|
+
transaction: (run) => this.db.transaction(run),
|
|
347
|
+
port: () => this.activeRuntime?.snapshot.bindings.turnEvents,
|
|
348
|
+
telemetry: this.telemetry,
|
|
349
|
+
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
350
|
+
scheduleRetry: async (seconds, idempotent) => {
|
|
351
|
+
await this.schedule(seconds, "_drainRuntimeTurnEvents", undefined, { idempotent });
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Recover entry ownership from Pi's durable operation boundary. */
|
|
357
|
+
private async associateHarnessEntries(
|
|
358
|
+
runtime: Awaited<ReturnType<typeof openSessionHarness>>,
|
|
359
|
+
submissionId: string,
|
|
360
|
+
): Promise<void> {
|
|
361
|
+
const watch = await runtime.lane.watch(BACKGROUND_CONTEXT);
|
|
362
|
+
try {
|
|
363
|
+
this.db.transaction(() =>
|
|
364
|
+
this.db.submissions.associateSessionEntries(submissionId, watch.snapshot)
|
|
365
|
+
);
|
|
366
|
+
} finally {
|
|
367
|
+
watch.unsubscribe();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 在 Agent 实例启动或从休眠唤醒后恢复 Session 本地后台状态。
|
|
373
|
+
*
|
|
374
|
+
* @remarks
|
|
375
|
+
* Cloudflare Agents SDK 在任何 HTTP、WebSocket 或 RPC 调用前调用它。
|
|
376
|
+
*
|
|
377
|
+
* 先恢复审批续跑,再分类未完 Submission,
|
|
378
|
+
* 可避免把正在等待人的 Turn 当成普通中断直接重试。
|
|
379
|
+
*
|
|
380
|
+
* 待确认:Cloudflare 当前文档说 `DurableObjectState.waitUntil`
|
|
381
|
+
* 不会延长 Durable Object 生命;本文件仍在三处用它启动后台任务。
|
|
382
|
+
*/
|
|
383
|
+
async onStart(): Promise<void> {
|
|
384
|
+
this.runtimeLoad.reset();
|
|
385
|
+
this.telemetry.recover(() => this.ensureRuntimeReady());
|
|
386
|
+
if (this.db.runtimeEvents.hasPending()) {
|
|
387
|
+
this.ctx.waitUntil(
|
|
388
|
+
this.ensureRuntimeReady().then(() => this.runtimeTurnEvents.drain()),
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
await this.broadcastApprovals();
|
|
392
|
+
const running = this.db.submissions.findRunning() as StoredSubmission | null;
|
|
393
|
+
if (running) {
|
|
394
|
+
if (
|
|
395
|
+
this.db.approvals.listPendingForSubmission(running.submissionId).length > 0 ||
|
|
396
|
+
this.db.interactions.listPendingForSubmission(running.submissionId).length > 0
|
|
397
|
+
) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this.ctx.waitUntil(
|
|
402
|
+
this.submissions.recoverHead().then(() => undefined),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async _drainRuntimeTurnEvents(): Promise<void> {
|
|
407
|
+
await this.ensureRuntimeReady();
|
|
408
|
+
await this.runtimeTurnEvents.drain(false);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
protected runTemporaryAgent(
|
|
412
|
+
request: TemporaryAgentRequest,
|
|
413
|
+
context: TemporaryAgentRunContext,
|
|
414
|
+
execute: TemporaryAgentExecutor,
|
|
415
|
+
): Promise<string> {
|
|
416
|
+
const submissionId = this.db.submissions.findByRequestId(context.requestId)?.submissionId;
|
|
417
|
+
return this.temporaryAgents.run(request, execute, context.signal, submissionId
|
|
418
|
+
? (event) => this.telemetry.temporaryAgent(submissionId, context.toolCallId, event)
|
|
419
|
+
: undefined);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 登记一条临时 Agent 的 Tool 审批并等待用户决定。
|
|
424
|
+
*
|
|
425
|
+
* @remarks
|
|
426
|
+
* Temporary Agent Host 在子执行需要人工授权时调用。
|
|
427
|
+
*
|
|
428
|
+
* 它与持久 Pi Tool 审批共用同一客户端投影,但只保存本次执行的 Promise,
|
|
429
|
+
* 因为临时 Agent 本身不参与 Session Turn 恢复。
|
|
430
|
+
*/
|
|
431
|
+
async requestTemporaryAgentApproval(
|
|
432
|
+
request: TemporaryAgentApprovalRequest,
|
|
433
|
+
): Promise<TemporaryAgentApprovalDecision> {
|
|
434
|
+
return this.temporaryAgentApprovals.request(
|
|
435
|
+
request,
|
|
436
|
+
this.db.submissions.findByRequestId(request.requestId)?.submissionId,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 取消一条仍在等待的临时 Agent 审批。
|
|
442
|
+
*
|
|
443
|
+
* @remarks
|
|
444
|
+
* Temporary Agent Host 在子执行停止或超时时调用。
|
|
445
|
+
*
|
|
446
|
+
* 先从内存表删除再解决 Promise,避免并发的批准请求再次命中已取消项。
|
|
447
|
+
*/
|
|
448
|
+
async cancelTemporaryAgentApproval(
|
|
449
|
+
executionId: string,
|
|
450
|
+
reason = "temporary agent was cancelled",
|
|
451
|
+
): Promise<{ ok: boolean }> {
|
|
452
|
+
return this.temporaryAgentApprovals.cancel(executionId, reason);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private requireRuntime(): ActiveRuntime {
|
|
456
|
+
if (!this.activeRuntime) throw new Error("Runtime must be assembled before use");
|
|
457
|
+
return this.activeRuntime;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async normalizeUIUserInput(
|
|
461
|
+
message: UIMessage & { role: "user" },
|
|
462
|
+
): Promise<PiCanonicalUserInput> {
|
|
463
|
+
if (requestedCapabilitiesOf(message.metadata).length > 0) await this.ensureRuntimeReady();
|
|
464
|
+
return normalizeUIUserInput(message, {
|
|
465
|
+
hasSkill: (name) => this.requireRuntime().snapshot.bindings.skills.sources
|
|
466
|
+
.some((skill) => skill.name === name),
|
|
467
|
+
hasTool: (name) => this.pi.hasTool(this.requireRuntime().pi, name),
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
protected readRuntimeAssembly(): RuntimeAssemblyView {
|
|
472
|
+
const runtime = this.requireRuntime();
|
|
473
|
+
return projectRuntimeAssembly(
|
|
474
|
+
runtime.snapshot,
|
|
475
|
+
runtime.revision,
|
|
476
|
+
runtime.pi,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Give each Extension only its authorized Host capabilities. */
|
|
481
|
+
private createExtensionHostBinding(
|
|
482
|
+
permissions: RuntimeExtensionPermissions,
|
|
483
|
+
ownContextLabels: readonly string[],
|
|
484
|
+
): Fetcher {
|
|
485
|
+
const exports = this.ctx.exports as unknown as {
|
|
486
|
+
HostBridgeLoopback(input: {
|
|
487
|
+
props: {
|
|
488
|
+
agentClassName: string;
|
|
489
|
+
agentId: string;
|
|
490
|
+
permissions: RuntimeExtensionPermissions;
|
|
491
|
+
ownContextLabels: string[];
|
|
492
|
+
};
|
|
493
|
+
}): Fetcher;
|
|
494
|
+
};
|
|
495
|
+
return exports.HostBridgeLoopback({
|
|
496
|
+
props: {
|
|
497
|
+
agentClassName: this.constructor.name,
|
|
498
|
+
agentId: this.ctx.id.toString(),
|
|
499
|
+
permissions,
|
|
500
|
+
ownContextLabels: [...ownContextLabels],
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// #endregion
|
|
506
|
+
|
|
507
|
+
// #region Submission 准入与配置固定
|
|
508
|
+
|
|
509
|
+
/** Pin dynamic Context and execution policy before admission. */
|
|
510
|
+
private async admissionPin(fresh: boolean, submissionId: string): Promise<{
|
|
511
|
+
revision: string;
|
|
512
|
+
descriptor: string;
|
|
513
|
+
}> {
|
|
514
|
+
if (fresh) await this.prepareRuntimeForSubmission(submissionId);
|
|
515
|
+
else await this.ensureRuntimeReady();
|
|
516
|
+
return this.currentAssemblyPin();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// initConfig can call this inside its serialized load; re-entering ensureRuntimeReady would deadlock.
|
|
520
|
+
private async currentAssemblyPin(): Promise<{
|
|
521
|
+
revision: string;
|
|
522
|
+
descriptor: string;
|
|
523
|
+
}> {
|
|
524
|
+
await this.runtimeTurnEvents.drain();
|
|
525
|
+
const runtime = this.activeRuntime;
|
|
526
|
+
if (!runtime) {
|
|
527
|
+
throw new Error("Runtime revision is not initialized");
|
|
528
|
+
}
|
|
529
|
+
return this.pinAssembly(runtime.pi, runtime.revision);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private async pinAssembly(
|
|
533
|
+
prepared: PreparedPiRuntime,
|
|
534
|
+
baseRevision: string,
|
|
535
|
+
): Promise<{ revision: string; descriptor: string }> {
|
|
536
|
+
const pinned = await this.pi.pin({
|
|
537
|
+
prepared,
|
|
538
|
+
baseRevision,
|
|
539
|
+
readExtensionContext: ({ label }) =>
|
|
540
|
+
this._hostGetContext(label),
|
|
541
|
+
});
|
|
542
|
+
if (pinned.degradations.length > 0) {
|
|
543
|
+
console.warn(
|
|
544
|
+
"[runtime-context:degraded]",
|
|
545
|
+
json({ degradations: pinned.degradations }),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
revision: await sha256Hex(pinned.descriptor),
|
|
550
|
+
descriptor: pinned.descriptor,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private get admission(): SubmissionAdmission {
|
|
555
|
+
return this.submissionAdmission ??= new SubmissionAdmission({
|
|
556
|
+
admissions: this.db.submissionAdmissions,
|
|
557
|
+
steers: this.db.steers,
|
|
558
|
+
submissions: this.db.submissions,
|
|
559
|
+
transaction: (run) => this.db.transaction(run),
|
|
560
|
+
prepareRuntime: async () => {
|
|
561
|
+
await this.ensureRuntimeReady();
|
|
562
|
+
return { runtimeKey: this.activeRuntime!.runtimeKey, admission: this.activeRuntime?.admission };
|
|
563
|
+
},
|
|
564
|
+
findUserMessage: (id) => this.transcript.findUserMessage(id),
|
|
565
|
+
onAdmitted: (submission, hostAccepted) =>
|
|
566
|
+
this.runtimeTurnEvents.submissionAdmitted(submission, hostAccepted),
|
|
567
|
+
onRejected: (submission) => this.runtimeTurnEvents.submissionTerminal(submission),
|
|
568
|
+
cancelHandoff: (id, reason) => this.finalizer.commit(id, "aborted", reason),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private async submitMessage(
|
|
573
|
+
userMessage: PiCanonicalUserInput,
|
|
574
|
+
options: SubmitMessageOptions,
|
|
575
|
+
): Promise<SubmissionHandle<StoredSubmission>> {
|
|
576
|
+
const submitted = await this.submissions.submit(
|
|
577
|
+
this.admission.input(userMessage, options),
|
|
578
|
+
);
|
|
579
|
+
this.ctx.waitUntil(this.runtimeTurnEvents.drain());
|
|
580
|
+
await this.broadcastApprovals();
|
|
581
|
+
return submitted;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** Scheduled work waits for a bounded stable window instead of interrupting a Turn. */
|
|
585
|
+
private async submitMessageWhenStable(
|
|
586
|
+
userMessage: PiCanonicalUserInput,
|
|
587
|
+
options: SubmitMessageOptions,
|
|
588
|
+
timeoutMs: number,
|
|
589
|
+
): Promise<SubmissionHandle<StoredSubmission> | null> {
|
|
590
|
+
const submitted = await this.submissions.submitWhenStable(
|
|
591
|
+
this.admission.input(userMessage, options),
|
|
592
|
+
timeoutMs,
|
|
593
|
+
);
|
|
594
|
+
if (submitted) {
|
|
595
|
+
await this.runtimeTurnEvents.drain();
|
|
596
|
+
await this.broadcastApprovals();
|
|
597
|
+
}
|
|
598
|
+
return submitted;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// #endregion
|
|
602
|
+
|
|
603
|
+
// #region 持久化、恢复与终态提交
|
|
604
|
+
|
|
605
|
+
private get finalizer(): SubmissionFinalizer {
|
|
606
|
+
return this.submissionFinalizer ??= new SubmissionFinalizer({
|
|
607
|
+
submissions: this.db.submissions,
|
|
608
|
+
steers: this.db.steers,
|
|
609
|
+
messageUi: this.db.messageUi,
|
|
610
|
+
transaction: (run) => this.db.transaction(run),
|
|
611
|
+
conversation: this.transcript,
|
|
612
|
+
decisions: this.humanDecisions,
|
|
613
|
+
events: this.runtimeTurnEvents,
|
|
614
|
+
telemetry: this.telemetry,
|
|
615
|
+
releaseAbortedHarness: async () => {
|
|
616
|
+
const runtime = this.activeHarness;
|
|
617
|
+
if (!runtime) return;
|
|
618
|
+
await runtime.lane.waitForIdle(BACKGROUND_CONTEXT);
|
|
619
|
+
if (this.activeHarness === runtime) this.activeHarness = undefined;
|
|
620
|
+
await runtime.close(BACKGROUND_CONTEXT);
|
|
621
|
+
},
|
|
622
|
+
cancelQueued: async (id) => this.activeHarness?.lane.cancelQueued(id, BACKGROUND_CONTEXT),
|
|
623
|
+
submit: (message, options) => this.submitMessage(message, options),
|
|
624
|
+
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Commit Host terminal facts only after Pi has settled the operation. */
|
|
629
|
+
private async commitTerminalOutcome(
|
|
630
|
+
submission: StoredSubmission,
|
|
631
|
+
outcome: "succeeded" | "failed" | "aborted",
|
|
632
|
+
message?: string,
|
|
633
|
+
): Promise<StoredSubmission> {
|
|
634
|
+
const terminal = await this.finalizer.finish(submission.submissionId, outcome, message);
|
|
635
|
+
await this.runtimeTurnEvents.drain();
|
|
636
|
+
await this.broadcastApprovals();
|
|
637
|
+
try {
|
|
638
|
+
if (
|
|
639
|
+
terminal.status === "error" ||
|
|
640
|
+
(terminal.status === "aborted" && terminal.error !== USER_STOP_REASON)
|
|
641
|
+
) {
|
|
642
|
+
await recordChatTerminal(
|
|
643
|
+
this.ctx.storage,
|
|
644
|
+
terminal.requestId,
|
|
645
|
+
terminal.error ?? "",
|
|
646
|
+
);
|
|
647
|
+
} else {
|
|
648
|
+
await clearChatTerminal(this.ctx.storage);
|
|
649
|
+
}
|
|
650
|
+
} catch (error) {
|
|
651
|
+
console.error("[pi-terminal] resume marker failed", error);
|
|
652
|
+
}
|
|
653
|
+
return terminal;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// #region Turn 执行、流式记录与完成投影
|
|
657
|
+
|
|
658
|
+
/** Keep failure frames and stream cleanup around the entire Harness execution. */
|
|
659
|
+
private async executeSubmission(
|
|
660
|
+
submissionId: string,
|
|
661
|
+
mode: SubmissionExecutionMode = "fresh",
|
|
662
|
+
): Promise<StoredSubmission> {
|
|
663
|
+
const submission = this.db.submissions.find(submissionId);
|
|
664
|
+
if (!submission) {
|
|
665
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
666
|
+
}
|
|
667
|
+
if (isTerminalSubmissionStatus(submission.status)) {
|
|
668
|
+
return submission;
|
|
669
|
+
}
|
|
670
|
+
if (submission.abortReason && submission.status === "pending") {
|
|
671
|
+
return this.submissions.finish(
|
|
672
|
+
submission,
|
|
673
|
+
"aborted",
|
|
674
|
+
submission.abortReason,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (mode === "recovery") {
|
|
678
|
+
this.telemetry.capture("turnRecovering", {
|
|
679
|
+
submissionId,
|
|
680
|
+
reason: "runtime_restart",
|
|
681
|
+
attempt: 1,
|
|
682
|
+
});
|
|
683
|
+
await this.broadcastApprovals();
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
return await this.executeHarnessSubmission(submission, mode);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
const failed = await this.submissions.finish(
|
|
689
|
+
submission,
|
|
690
|
+
"failed",
|
|
691
|
+
errorText(error),
|
|
692
|
+
);
|
|
693
|
+
this.sendChatTerminal(
|
|
694
|
+
submission.requestId,
|
|
695
|
+
failed.status === "error"
|
|
696
|
+
? failed.error ?? "SpringBrand turn failed"
|
|
697
|
+
: undefined,
|
|
698
|
+
mode !== "fresh",
|
|
699
|
+
);
|
|
700
|
+
return failed;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Run or recover the Submission through Pi's single durable AgentHarness. */
|
|
705
|
+
private async executeHarnessSubmission(
|
|
706
|
+
submission: StoredSubmission,
|
|
707
|
+
mode: SubmissionExecutionMode,
|
|
708
|
+
): Promise<StoredSubmission> {
|
|
709
|
+
const continuation = mode !== "fresh";
|
|
710
|
+
const submissionId = submission.submissionId;
|
|
711
|
+
submission = await this.admission.pin(
|
|
712
|
+
submission,
|
|
713
|
+
() => this.admissionPin(mode === "fresh", submissionId),
|
|
714
|
+
mode === "fresh" ? undefined : (key) => this.ensureRuntimeVersion(key),
|
|
715
|
+
);
|
|
716
|
+
const activeRuntime = this.requireRuntime();
|
|
717
|
+
const harnessInput = this.pi.harnessInputs(
|
|
718
|
+
activeRuntime.pi,
|
|
719
|
+
submission.assemblyDescriptor,
|
|
720
|
+
{
|
|
721
|
+
modelSessionId: submissionId,
|
|
722
|
+
onGeneration: (event) => this.telemetry.generation(submissionId, event),
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
let runtime!: Awaited<ReturnType<typeof openSessionHarness>>;
|
|
726
|
+
let publishApprovalRequest = async (
|
|
727
|
+
_toolCallId: string,
|
|
728
|
+
_approvalId: string,
|
|
729
|
+
): Promise<void> => {};
|
|
730
|
+
const gate = new HarnessDecisionGate({
|
|
731
|
+
submission: {
|
|
732
|
+
submissionId,
|
|
733
|
+
requestId: submission.requestId,
|
|
734
|
+
executionLevel: activeRuntime.snapshot.profile.executionLevel,
|
|
735
|
+
},
|
|
736
|
+
candidates: harnessInput.candidates,
|
|
737
|
+
telemetry: this.telemetry,
|
|
738
|
+
approvals: this.db.approvals,
|
|
739
|
+
interactions: this.db.interactions,
|
|
740
|
+
transaction: (run) => this.db.transaction(run),
|
|
741
|
+
release: (reason) => runtime.requestHumanRelease(reason),
|
|
742
|
+
onChanged: async (kind, id) => {
|
|
743
|
+
await this.broadcastApprovals();
|
|
744
|
+
if (kind === "approval") {
|
|
745
|
+
const approval = this.db.approvals.find(id);
|
|
746
|
+
if (approval) {
|
|
747
|
+
await publishApprovalRequest(approval.toolCallId, id);
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
await activeRuntime.snapshot.bindings.turnEvents?.onApproval?.({
|
|
751
|
+
submissionId,
|
|
752
|
+
approvalExecutionId: id,
|
|
753
|
+
});
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.warn("[runtime-approval-projection:degraded]", json({
|
|
756
|
+
error: errorText(error),
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
gateTool: harnessInput.platform.gateTool,
|
|
762
|
+
});
|
|
763
|
+
runtime = await openSessionHarness({
|
|
764
|
+
session: openCloudflareSession(this.ctx.storage),
|
|
765
|
+
models: harnessInput.models,
|
|
766
|
+
modelSessionId: submissionId,
|
|
767
|
+
model: harnessInput.model,
|
|
768
|
+
thinkingLevel: harnessInput.thinkingLevel,
|
|
769
|
+
systemPrompt: harnessInput.systemPrompt,
|
|
770
|
+
tools: compileHarnessTools(harnessInput.candidates, {
|
|
771
|
+
workspace: harnessInput.workspace,
|
|
772
|
+
authorize: gate.authorize.bind(gate),
|
|
773
|
+
resolveReplay: gate.resolveReplay.bind(gate),
|
|
774
|
+
}),
|
|
775
|
+
modelToolResultExemptions: harnessInput.candidates
|
|
776
|
+
.filter(({ outputBudget }) => outputBudget?.kind === "structure")
|
|
777
|
+
.map(({ tool }) => tool.name),
|
|
778
|
+
streamOptions: { maxRetries: 0 },
|
|
779
|
+
compaction: harnessInput.compaction,
|
|
780
|
+
toolExecution: "parallel",
|
|
781
|
+
}, BACKGROUND_CONTEXT);
|
|
782
|
+
this.activeHarness = runtime;
|
|
783
|
+
let stream: ReturnType<typeof bindHarnessStream> | undefined;
|
|
784
|
+
let deactivate = () => {};
|
|
785
|
+
try {
|
|
786
|
+
runtime.harness.hooks.on("before_tool", gate.beforeTool);
|
|
787
|
+
runtime.harness.hooks.on("before_drive", async (event, context) => {
|
|
788
|
+
if (this.db.submissions.find(submissionId)?.abortReason) {
|
|
789
|
+
await runtime.requestAbort(event.runId, context);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
if (submission.abortReason && runtime.openOperation) {
|
|
794
|
+
await runtime.requestAbort(
|
|
795
|
+
runtime.openOperation.operationId,
|
|
796
|
+
BACKGROUND_CONTEXT,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
await this.associateHarnessEntries(runtime, submissionId);
|
|
801
|
+
this.runtimeTurnEvents.backfill(submissionId, harnessInput.model, await this.transcript.storedEntries());
|
|
802
|
+
|
|
803
|
+
const preparation = await prepareSubmissionOperation(
|
|
804
|
+
runtime, submission,
|
|
805
|
+
(id) => this.transcript.regenerationTarget(id),
|
|
806
|
+
BACKGROUND_CONTEXT,
|
|
807
|
+
);
|
|
808
|
+
if (preparation.kind === "settled") {
|
|
809
|
+
const record = preparation.outcome;
|
|
810
|
+
const terminal = await this.submissions.finish(
|
|
811
|
+
submission,
|
|
812
|
+
record.status === "completed" || record.status === "declined"
|
|
813
|
+
? "succeeded" : record.status === "aborted" ? "aborted" : "failed",
|
|
814
|
+
record.error?.message,
|
|
815
|
+
);
|
|
816
|
+
this.broadcast(json({
|
|
817
|
+
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
818
|
+
messages: await this.getMessages(),
|
|
819
|
+
}));
|
|
820
|
+
return terminal;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (submission.status === "pending") {
|
|
824
|
+
this.db.submissions.transition(submissionId, "running", ["pending"]);
|
|
825
|
+
this.telemetry.capture("turnStarted", {
|
|
826
|
+
submissionId,
|
|
827
|
+
assemblyRevision: submission.assemblyRevision,
|
|
828
|
+
});
|
|
829
|
+
submission = this.db.submissions.find(submissionId)!;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const steerDelivery = new PendingSteerDelivery({
|
|
833
|
+
steers: this.db.steers,
|
|
834
|
+
messageUi: this.db.messageUi,
|
|
835
|
+
transaction: (run) => this.db.transaction(run),
|
|
836
|
+
enqueue: async (message) => {
|
|
837
|
+
const queued = await runtime.lane.steer(message, undefined, BACKGROUND_CONTEXT);
|
|
838
|
+
if (!queued.ok) throw queued.error;
|
|
839
|
+
return queued.value.entryId;
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
const uiMessageBody = submission.queuedUiMessageJson;
|
|
843
|
+
let initialUserSidecarPersisted = (await this.transcript.storedMessages())
|
|
844
|
+
.some((entry) =>
|
|
845
|
+
entry.submissionId === submissionId && entry.message.role === "user"
|
|
846
|
+
);
|
|
847
|
+
runtime.harness.events.on("entry_added", (event) => {
|
|
848
|
+
const entry = event.entry;
|
|
849
|
+
this.db.transaction(() => {
|
|
850
|
+
this.db.submissions.linkSessionEntry(entry.id, submissionId);
|
|
851
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
852
|
+
if (!steerDelivery.settle(entry.id) && !initialUserSidecarPersisted && uiMessageBody) {
|
|
853
|
+
this.db.messageUi.upsert(entry.id, uiMessageBody);
|
|
854
|
+
initialUserSidecarPersisted = true;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
});
|
|
860
|
+
const pendingWatch = await runtime.lane.watch(BACKGROUND_CONTEXT);
|
|
861
|
+
try {
|
|
862
|
+
await steerDelivery.reconcile(submissionId, pendingWatch.snapshot);
|
|
863
|
+
} finally {
|
|
864
|
+
pendingWatch.unsubscribe();
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
this.runtimeTurnEvents.observe(runtime.harness, submissionId, harnessInput.model);
|
|
868
|
+
stream = bindHarnessStream(runtime.harness.events, {
|
|
869
|
+
messageId: submission.assistantMessageId,
|
|
870
|
+
userMessageId: submission.userMessageId ?? undefined,
|
|
871
|
+
startedAt: submission.createdAt,
|
|
872
|
+
turnId: submissionId,
|
|
873
|
+
publish: async (chunks) => {
|
|
874
|
+
for (const chunk of chunks) {
|
|
875
|
+
this.conversation.broadcastResponse({
|
|
876
|
+
id: submission.requestId,
|
|
877
|
+
body: json(chunk),
|
|
878
|
+
done: false,
|
|
879
|
+
continuation,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
publishApprovalRequest = stream.publishApprovalRequest;
|
|
885
|
+
this.conversation.activate(runtime.lane, submission);
|
|
886
|
+
|
|
887
|
+
const control = {
|
|
888
|
+
abort: async () => {
|
|
889
|
+
if (runtime.openOperation) {
|
|
890
|
+
await runtime.requestAbort(
|
|
891
|
+
runtime.openOperation.operationId,
|
|
892
|
+
BACKGROUND_CONTEXT,
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
interrupt: () => void runtime.close(BACKGROUND_CONTEXT),
|
|
897
|
+
steer: async (_message: PiCanonicalUserInput, messageId: string) => {
|
|
898
|
+
const steer = this.db.steers.findByMessageId(messageId);
|
|
899
|
+
if (steer) await steerDelivery.queue(steer);
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
const turn: ActiveTurn = {
|
|
903
|
+
submissionId,
|
|
904
|
+
requestId: submission.requestId,
|
|
905
|
+
messageId: submission.assistantMessageId,
|
|
906
|
+
startedAt: submission.createdAt,
|
|
907
|
+
continuation,
|
|
908
|
+
agent: control,
|
|
909
|
+
};
|
|
910
|
+
deactivate = this.submissions.activate(submission, turn);
|
|
911
|
+
const outcome = await runtime.run(preparation.request, BACKGROUND_CONTEXT, {
|
|
912
|
+
pollDeferred: !preparation.request,
|
|
913
|
+
});
|
|
914
|
+
this.db.submissions.clearQueuedPayload(submissionId);
|
|
915
|
+
if (outcome.kind === "released") {
|
|
916
|
+
if (
|
|
917
|
+
outcome.reason === "slice" ||
|
|
918
|
+
outcome.reason === "budget" ||
|
|
919
|
+
outcome.reason === "tool-failure"
|
|
920
|
+
) {
|
|
921
|
+
await this.schedule(0, "_piHarnessContinuation", { submissionId }, {
|
|
922
|
+
idempotent: true,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return this.db.submissions.find(submissionId)!;
|
|
926
|
+
}
|
|
927
|
+
if (outcome.kind === "waiting") {
|
|
928
|
+
await this.schedule(
|
|
929
|
+
outcome.reason === "retry"
|
|
930
|
+
? Math.max(0, Math.ceil((outcome.notBefore - Date.now()) / 1000))
|
|
931
|
+
: Math.max(0, Math.ceil((outcome.deferred.pollAfterMs ?? 1_000) / 1000)),
|
|
932
|
+
"_piHarnessContinuation",
|
|
933
|
+
{ submissionId },
|
|
934
|
+
{ idempotent: true },
|
|
935
|
+
);
|
|
936
|
+
return this.db.submissions.find(submissionId)!;
|
|
937
|
+
}
|
|
938
|
+
const record = outcome.outcome;
|
|
939
|
+
const status = record.status === "completed" || record.status === "declined"
|
|
940
|
+
? "succeeded"
|
|
941
|
+
: record.status === "aborted"
|
|
942
|
+
? "aborted"
|
|
943
|
+
: "failed";
|
|
944
|
+
this.runtimeTurnEvents.backfill(submissionId, harnessInput.model, await this.transcript.storedEntries());
|
|
945
|
+
const terminal = await this.submissions.finish(
|
|
946
|
+
submission,
|
|
947
|
+
status,
|
|
948
|
+
record.error?.message,
|
|
949
|
+
);
|
|
950
|
+
const projectedOutcome = projectSubmissionOutcome(record, terminal);
|
|
951
|
+
await stream.flushTerminal(projectedOutcome);
|
|
952
|
+
await this.conversation.settle(projectedOutcome);
|
|
953
|
+
this.broadcast(json({
|
|
954
|
+
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
955
|
+
messages: await this.getMessages(),
|
|
956
|
+
}));
|
|
957
|
+
return terminal;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
console.warn("[runtime-harness:failed]", json({
|
|
960
|
+
submissionId,
|
|
961
|
+
error: errorText(error),
|
|
962
|
+
}));
|
|
963
|
+
throw error;
|
|
964
|
+
} finally {
|
|
965
|
+
await this.conversation.release(runtime.lane).catch((error) => {
|
|
966
|
+
console.warn("[runtime-stream:degraded]", errorText(error));
|
|
967
|
+
});
|
|
968
|
+
stream?.unsubscribe();
|
|
969
|
+
deactivate();
|
|
970
|
+
if (this.activeHarness === runtime) this.activeHarness = undefined;
|
|
971
|
+
await runtime.close(BACKGROUND_CONTEXT).catch(() => undefined);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async _piHarnessContinuation(
|
|
976
|
+
data?: { readonly submissionId?: string },
|
|
977
|
+
): Promise<void> {
|
|
978
|
+
if (!data?.submissionId) return;
|
|
979
|
+
await this.ensureRuntimeReady();
|
|
980
|
+
await this.submissions.continueAfterCurrent(data.submissionId);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private sendChatTerminal(
|
|
984
|
+
requestId: string,
|
|
985
|
+
error?: string,
|
|
986
|
+
continuation = false,
|
|
987
|
+
): void {
|
|
988
|
+
this.conversation.broadcastResponse({
|
|
989
|
+
id: requestId,
|
|
990
|
+
body: error
|
|
991
|
+
? json({ type: "error", errorText: error } satisfies UIMessageChunk)
|
|
992
|
+
: "",
|
|
993
|
+
done: true,
|
|
994
|
+
continuation,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// #endregion
|
|
999
|
+
|
|
1000
|
+
// #region Cloudflare Agent 通信生命周期
|
|
1001
|
+
|
|
1002
|
+
/** Start the native resume handshake from Pi's current lane snapshot. */
|
|
1003
|
+
async onConnect(connection: Connection): Promise<void> {
|
|
1004
|
+
this.conversation.disconnect(connection);
|
|
1005
|
+
await this.conversation.resume(connection);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async onClose(connection: Connection, code: number, reason: string, wasClean: boolean): Promise<void> {
|
|
1009
|
+
this.conversation.disconnect(connection);
|
|
1010
|
+
await super.onClose(connection, code, reason, wasClean);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/** Serve chat hydration and delegate every other HTTP route to Agents SDK. */
|
|
1014
|
+
async onRequest(request: Request): Promise<Response> {
|
|
1015
|
+
const url = new URL(request.url);
|
|
1016
|
+
if (url.pathname.endsWith("/get-messages")) {
|
|
1017
|
+
return Response.json(url.searchParams.get("view") === "hydration"
|
|
1018
|
+
? await this.conversation.messages("hydration")
|
|
1019
|
+
: await this.getMessages());
|
|
1020
|
+
}
|
|
1021
|
+
return super.onRequest(request);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/** Validate chat protocol input before routing it to capability modules. */
|
|
1025
|
+
async onMessage(
|
|
1026
|
+
connection: Connection,
|
|
1027
|
+
raw: string | ArrayBuffer,
|
|
1028
|
+
): Promise<void> {
|
|
1029
|
+
if (typeof raw !== "string") return;
|
|
1030
|
+
const event = parseProtocolMessage(raw);
|
|
1031
|
+
if (!event) return;
|
|
1032
|
+
|
|
1033
|
+
if (event.type === "chat-request") {
|
|
1034
|
+
let body: UIChatRequestBody;
|
|
1035
|
+
try {
|
|
1036
|
+
body = JSON.parse(
|
|
1037
|
+
String(event.init.body ?? "{}"),
|
|
1038
|
+
) as UIChatRequestBody;
|
|
1039
|
+
} catch {
|
|
1040
|
+
this.sendChatTerminal(event.id, "Chat request body is invalid JSON");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const latest = Array.isArray(body.messages)
|
|
1044
|
+
? [...body.messages].reverse().find(
|
|
1045
|
+
(message): message is UIMessage & { role: "user" } =>
|
|
1046
|
+
message?.role === "user",
|
|
1047
|
+
)
|
|
1048
|
+
: undefined;
|
|
1049
|
+
if (
|
|
1050
|
+
!latest ||
|
|
1051
|
+
latest.role !== "user" ||
|
|
1052
|
+
typeof latest.id !== "string" ||
|
|
1053
|
+
!Array.isArray(latest.parts) ||
|
|
1054
|
+
!latest.parts.every((part) => {
|
|
1055
|
+
if (part === null || typeof part !== "object") return false;
|
|
1056
|
+
return part.type === "text"
|
|
1057
|
+
? typeof part.text === "string"
|
|
1058
|
+
: part.type === "file" &&
|
|
1059
|
+
typeof part.mediaType === "string" &&
|
|
1060
|
+
typeof part.url === "string" &&
|
|
1061
|
+
(part.filename === undefined || typeof part.filename === "string");
|
|
1062
|
+
}) ||
|
|
1063
|
+
(body.trigger !== "submit-message" &&
|
|
1064
|
+
body.trigger !== "regenerate-message")
|
|
1065
|
+
) {
|
|
1066
|
+
this.sendChatTerminal(event.id, "Chat request user message is invalid");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
let submitted: Awaited<
|
|
1070
|
+
ReturnType<AgentRuntimeKernel["submitMessage"]>
|
|
1071
|
+
>;
|
|
1072
|
+
try {
|
|
1073
|
+
const normalized = await this.normalizeUIUserInput(latest);
|
|
1074
|
+
submitted = await this.submitMessage(normalized, {
|
|
1075
|
+
requestId: event.id,
|
|
1076
|
+
idempotencyKey: event.id,
|
|
1077
|
+
userMessageId: latest.id,
|
|
1078
|
+
userMessage: latest,
|
|
1079
|
+
regenerate: body.trigger === "regenerate-message",
|
|
1080
|
+
});
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
this.sendChatTerminal(event.id, errorText(error));
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
await submitted.completion;
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (event.type === "cancel") {
|
|
1090
|
+
const submission = this.db.submissions.findByRequestId(event.id);
|
|
1091
|
+
if (submission) {
|
|
1092
|
+
await this.cancelSubmissionById(
|
|
1093
|
+
submission.submissionId,
|
|
1094
|
+
USER_STOP_REASON,
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (event.type === "clear") {
|
|
1101
|
+
if (this.submissions.isBusy()) return;
|
|
1102
|
+
this.db.transaction(() => {
|
|
1103
|
+
this.transcript.clear();
|
|
1104
|
+
this.db.clearAll();
|
|
1105
|
+
});
|
|
1106
|
+
await this.broadcastApprovals();
|
|
1107
|
+
this.broadcast(json({ type: MessageType.CF_AGENT_CHAT_CLEAR }));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (event.type === "stream-resume-request") {
|
|
1112
|
+
// The mounted client receives the same Pi snapshot used by initial hydration,
|
|
1113
|
+
// including any assistant text and Tools that have not settled yet.
|
|
1114
|
+
await this.conversation.resume(connection, event.probeId);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (event.type === "stream-resume-ack") {
|
|
1119
|
+
await this.conversation.acknowledge(connection, event.id);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (event.type === "tool-approval") {
|
|
1124
|
+
const executionId = this.db.approvals.findPendingByToolCallId(
|
|
1125
|
+
event.toolCallId,
|
|
1126
|
+
);
|
|
1127
|
+
if (!executionId) return;
|
|
1128
|
+
await this.decideApproval(
|
|
1129
|
+
executionId,
|
|
1130
|
+
event.approved
|
|
1131
|
+
? { decision: "allow_once" }
|
|
1132
|
+
: { decision: "deny", reason: "User denied the Tool" },
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Return the complete canonical browser projection, including an active reply. */
|
|
1138
|
+
async getMessages(): Promise<UIMessage[]> {
|
|
1139
|
+
return this.conversation.messages("full");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/** 读取当前 canonical transcript 中最后一条助手文本。 */
|
|
1143
|
+
protected async latestAssistantText(): Promise<string | undefined> {
|
|
1144
|
+
const message = [...await this.transcript.canonicalMessages()]
|
|
1145
|
+
.reverse()
|
|
1146
|
+
.find((entry): entry is AssistantMessage => entry.role === "assistant");
|
|
1147
|
+
const text = message?.content
|
|
1148
|
+
.flatMap((part) => part.type === "text" ? [part.text] : [])
|
|
1149
|
+
.join("")
|
|
1150
|
+
.trim();
|
|
1151
|
+
return text || undefined;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// #endregion
|
|
1155
|
+
|
|
1156
|
+
// #region Runtime Extension Host 回环端口
|
|
1157
|
+
|
|
1158
|
+
private async runtimeWorkspace() {
|
|
1159
|
+
await this.ensureRuntimeReady();
|
|
1160
|
+
const workspace = this.requireRuntime().snapshot.bindings.workspace;
|
|
1161
|
+
if (!workspace) throw new Error("Runtime Workspace is unavailable");
|
|
1162
|
+
return workspace;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async _hostReadFile(path: string): Promise<string | null> {
|
|
1166
|
+
return (await this.runtimeWorkspace()).readFile(path);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async _hostWriteFile(
|
|
1170
|
+
path: string,
|
|
1171
|
+
content: string,
|
|
1172
|
+
): Promise<void> {
|
|
1173
|
+
await (await this.runtimeWorkspace()).writeFile(path, content);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
async _hostDeleteFile(path: string): Promise<boolean> {
|
|
1177
|
+
const workspace = await this.runtimeWorkspace();
|
|
1178
|
+
try {
|
|
1179
|
+
await workspace.rm(path);
|
|
1180
|
+
return true;
|
|
1181
|
+
} catch {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
async _hostListFiles(
|
|
1187
|
+
dir: string,
|
|
1188
|
+
): Promise<
|
|
1189
|
+
Array<{ name: string; type: string; size: number; path: string }>
|
|
1190
|
+
> {
|
|
1191
|
+
const workspace = await this.runtimeWorkspace();
|
|
1192
|
+
return (await workspace.readDir(dir)).map((entry) => ({
|
|
1193
|
+
name: entry.name,
|
|
1194
|
+
type: entry.type,
|
|
1195
|
+
size: entry.size,
|
|
1196
|
+
path: entry.path,
|
|
1197
|
+
}));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async _hostGetContext(label: string): Promise<string | null> {
|
|
1201
|
+
return this.db.extContext.get(label) ?? null;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
async _hostSetContext(
|
|
1205
|
+
label: string,
|
|
1206
|
+
content: string,
|
|
1207
|
+
): Promise<void> {
|
|
1208
|
+
this.db.extContext.set(label, content);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async _hostGetMessages(
|
|
1212
|
+
limit?: number,
|
|
1213
|
+
): Promise<
|
|
1214
|
+
Array<{ id: string; role: string; content: string }>
|
|
1215
|
+
> {
|
|
1216
|
+
return this.transcript.hostMessages(limit);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** Steer an active Turn; append directly only while idle. */
|
|
1220
|
+
async _hostSendMessage(content: string): Promise<void> {
|
|
1221
|
+
if (typeof content !== "string" || content.trim().length === 0) {
|
|
1222
|
+
throw new Error("Extension message content is required");
|
|
1223
|
+
}
|
|
1224
|
+
const message: PiCanonicalUserInput = {
|
|
1225
|
+
role: "user",
|
|
1226
|
+
content: [{ type: "text", text: content }],
|
|
1227
|
+
timestamp: Date.now(),
|
|
1228
|
+
};
|
|
1229
|
+
const active = this.submissions.currentActive();
|
|
1230
|
+
if (active) {
|
|
1231
|
+
const messageId = crypto.randomUUID();
|
|
1232
|
+
this.db.transaction(() => {
|
|
1233
|
+
this.db.steers.insert({
|
|
1234
|
+
steerId: crypto.randomUUID(),
|
|
1235
|
+
submissionId: active.submissionId,
|
|
1236
|
+
messageId,
|
|
1237
|
+
harnessEntryId: null,
|
|
1238
|
+
canonicalJson: json(message),
|
|
1239
|
+
uiMessageJson: null,
|
|
1240
|
+
createdAt: message.timestamp,
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
await active.agent.steer(message, messageId).catch(() => undefined);
|
|
1244
|
+
await this.broadcastApprovals();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
this.transcript.append(crypto.randomUUID(), message);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
async _hostGetSessionInfo(): Promise<{ messageCount: number }> {
|
|
1251
|
+
return {
|
|
1252
|
+
messageCount: (await this._hostGetMessages()).length,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// #endregion
|
|
1257
|
+
|
|
1258
|
+
// #region Session 消息与 Submission 公开控制面
|
|
1259
|
+
|
|
1260
|
+
/** Export canonical history through one message for fork/import. */
|
|
1261
|
+
async getCanonicalSnapshot(
|
|
1262
|
+
throughMessageId: string,
|
|
1263
|
+
): Promise<PiCanonicalTranscriptSnapshot> {
|
|
1264
|
+
return this.transcript.exportSnapshot(throughMessageId);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
async addCanonicalSnapshot(
|
|
1268
|
+
snapshot: PiCanonicalTranscriptSnapshot,
|
|
1269
|
+
): Promise<void> {
|
|
1270
|
+
this.transcript.importSnapshot(snapshot);
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/** Admit text and return immediately; completion continues in the Durable Object. */
|
|
1274
|
+
async submitPrompt(
|
|
1275
|
+
text: string,
|
|
1276
|
+
options?: { idempotencyKey?: string },
|
|
1277
|
+
): Promise<SubmissionReceipt> {
|
|
1278
|
+
const submitted = await this.submitMessage({
|
|
1279
|
+
role: "user",
|
|
1280
|
+
content: [{ type: "text", text }],
|
|
1281
|
+
timestamp: Date.now(),
|
|
1282
|
+
}, {
|
|
1283
|
+
requestId: crypto.randomUUID(),
|
|
1284
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1285
|
+
});
|
|
1286
|
+
this.ctx.waitUntil(submitted.completion);
|
|
1287
|
+
return projectSubmissionReceipt(submitted.receipt);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
private get delivery(): SubmissionDelivery {
|
|
1291
|
+
return this.submissionDelivery ??= new SubmissionDelivery({
|
|
1292
|
+
submissions: this.db.submissions,
|
|
1293
|
+
steers: this.db.steers,
|
|
1294
|
+
transaction: (run) => this.db.transaction(run),
|
|
1295
|
+
lifecycle: this.submissions,
|
|
1296
|
+
decisions: this.humanDecisions,
|
|
1297
|
+
telemetry: this.telemetry,
|
|
1298
|
+
events: this.runtimeTurnEvents,
|
|
1299
|
+
normalize: (message) => this.normalizeUIUserInput(message),
|
|
1300
|
+
submit: (message, options) => this.submitMessage(message, options),
|
|
1301
|
+
onChanged: () => this.broadcastApprovals(),
|
|
1302
|
+
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
async dispatchMessage(message: UIMessage, delivery: MessageDelivery): Promise<MessageDispatchReceipt> {
|
|
1307
|
+
return this.delivery.dispatch(message, delivery);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async steerQueuedSubmission(submissionId: string): Promise<MessageDispatchReceipt> {
|
|
1311
|
+
return this.delivery.steerQueued(submissionId);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/** Admit scheduled work after a bounded stable wait, otherwise return skipped. */
|
|
1315
|
+
async submitScheduledPrompt(
|
|
1316
|
+
prompt: string,
|
|
1317
|
+
options?: { idempotencyKey?: string },
|
|
1318
|
+
): Promise<SubmissionReceipt> {
|
|
1319
|
+
const requestId = crypto.randomUUID();
|
|
1320
|
+
const submitted = await this.submitMessageWhenStable(
|
|
1321
|
+
{
|
|
1322
|
+
role: "user",
|
|
1323
|
+
content: [{ type: "text", text: prompt }],
|
|
1324
|
+
timestamp: Date.now(),
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
requestId,
|
|
1328
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1329
|
+
},
|
|
1330
|
+
SCHEDULED_STABLE_TIMEOUT_MS,
|
|
1331
|
+
);
|
|
1332
|
+
if (!submitted) {
|
|
1333
|
+
const skipped = this.db.transaction(() => {
|
|
1334
|
+
const duplicate = options?.idempotencyKey
|
|
1335
|
+
? this.db.submissions.findByIdempotencyKey(options.idempotencyKey)
|
|
1336
|
+
: this.db.submissions.findByRequestId(requestId);
|
|
1337
|
+
return duplicate ?? this.admission.skip({
|
|
1338
|
+
submissionId: requestId,
|
|
1339
|
+
requestId,
|
|
1340
|
+
idempotencyKey: options?.idempotencyKey ?? null,
|
|
1341
|
+
createdAt: Date.now(),
|
|
1342
|
+
code: "Runtime not stable within timeout",
|
|
1343
|
+
retryable: true,
|
|
1344
|
+
});
|
|
1345
|
+
}) as StoredSubmission;
|
|
1346
|
+
await this.runtimeTurnEvents.drain();
|
|
1347
|
+
await this.broadcastApprovals();
|
|
1348
|
+
return projectSubmissionReceipt(skipped);
|
|
1349
|
+
}
|
|
1350
|
+
this.ctx.waitUntil(submitted.completion.then(() => undefined));
|
|
1351
|
+
return projectSubmissionReceipt(submitted.receipt);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/** Read the durable receipt rather than inferring status from process state. */
|
|
1355
|
+
async getSubmission(
|
|
1356
|
+
submissionId: string,
|
|
1357
|
+
): Promise<SubmissionReceipt | null> {
|
|
1358
|
+
const submission = this.submissions.get(submissionId);
|
|
1359
|
+
return submission ? projectSubmissionReceipt(submission) : null;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** Persist abort intent before interrupting the active execution. */
|
|
1363
|
+
async cancelSubmissionById(
|
|
1364
|
+
submissionId: string,
|
|
1365
|
+
reason?: string,
|
|
1366
|
+
): Promise<{ ok: boolean }> {
|
|
1367
|
+
const result = await this.submissions.cancel(
|
|
1368
|
+
submissionId,
|
|
1369
|
+
reason ?? "Cancelled",
|
|
1370
|
+
);
|
|
1371
|
+
await this.broadcastApprovals();
|
|
1372
|
+
return result;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Stop one request or the current unfinished queue through SubmissionLifecycle. */
|
|
1376
|
+
async stopTurn(
|
|
1377
|
+
requestId?: string,
|
|
1378
|
+
reason?: string,
|
|
1379
|
+
): Promise<{ ok: boolean }> {
|
|
1380
|
+
const result = await this.submissions.stop(
|
|
1381
|
+
requestId,
|
|
1382
|
+
reason ?? "Stopped",
|
|
1383
|
+
);
|
|
1384
|
+
await this.broadcastApprovals();
|
|
1385
|
+
return result;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/** Stops every running, queued, or steered user request in this Session. */
|
|
1389
|
+
async stopAllSubmissions(reason?: string): Promise<{ ok: boolean }> {
|
|
1390
|
+
const result = await this.submissions.stopAll(reason ?? "Stopped");
|
|
1391
|
+
await this.broadcastApprovals();
|
|
1392
|
+
return result;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** 返回当前已装配 User Agent 的执行档位,不提供 Session 覆盖。 */
|
|
1396
|
+
executionLevel(): ExecutionLevel {
|
|
1397
|
+
return this.requireRuntime().snapshot.profile.executionLevel;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** Read trusted approval requirements before the Host applies allow_level. */
|
|
1401
|
+
getPendingApproval(executionId: string): ApprovalReceipt | null {
|
|
1402
|
+
const temporary = this.temporaryAgentApprovals.find(executionId);
|
|
1403
|
+
if (temporary) return temporary;
|
|
1404
|
+
const approval = this.db.approvals.find(executionId);
|
|
1405
|
+
if (!approval || approval.status !== "pending") return null;
|
|
1406
|
+
return {
|
|
1407
|
+
executionId: approval.executionId,
|
|
1408
|
+
source: approval.source as ApprovalReceipt["source"],
|
|
1409
|
+
action: approval.toolName,
|
|
1410
|
+
summary: approval.summary,
|
|
1411
|
+
executionLevel: approval.executionLevel,
|
|
1412
|
+
requiredExecutionLevel: approval.requiredExecutionLevel,
|
|
1413
|
+
inputJson: approval.inputJson,
|
|
1414
|
+
requestId: approval.requestId,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** Validate a client-provided Tool result and resume its durable Submission. */
|
|
1419
|
+
async respondToolInteraction(
|
|
1420
|
+
toolCallId: string,
|
|
1421
|
+
response: unknown,
|
|
1422
|
+
): Promise<{ ok: boolean }> {
|
|
1423
|
+
const pending = this.db.interactions.findPendingByToolCallId(toolCallId);
|
|
1424
|
+
if (!pending) return { ok: false };
|
|
1425
|
+
const submission = this.db.submissions.find(pending.submissionId);
|
|
1426
|
+
if (!submission || isTerminalSubmissionStatus(submission.status)) {
|
|
1427
|
+
return { ok: false };
|
|
1428
|
+
}
|
|
1429
|
+
await this.ensureRuntimeReady();
|
|
1430
|
+
const spec = this.pi.harnessInputs(
|
|
1431
|
+
this.requireRuntime().pi,
|
|
1432
|
+
submission.assemblyDescriptor,
|
|
1433
|
+
).candidates.find(({ tool }) => tool.name === pending.toolName)?.interaction;
|
|
1434
|
+
if (!spec) return { ok: false };
|
|
1435
|
+
|
|
1436
|
+
const accepted = await this.humanDecisions.respond(
|
|
1437
|
+
pending, spec, response,
|
|
1438
|
+
(input) => this.prepareToolInteractionResponse(input),
|
|
1439
|
+
);
|
|
1440
|
+
if (!accepted) return { ok: false };
|
|
1441
|
+
await this.broadcastApprovals();
|
|
1442
|
+
this.ctx.waitUntil(
|
|
1443
|
+
this.submissions.continueAfterCurrent(submission.submissionId)
|
|
1444
|
+
.then(() => undefined),
|
|
1445
|
+
);
|
|
1446
|
+
return { ok: true };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/** Apply one decision; the Host must persist allow_level before resuming. */
|
|
1450
|
+
async decideApproval(
|
|
1451
|
+
executionId: string,
|
|
1452
|
+
decision: ApprovalDecision,
|
|
1453
|
+
): Promise<{ ok: boolean }> {
|
|
1454
|
+
if (decision.decision === "allow_level") {
|
|
1455
|
+
throw new Error(
|
|
1456
|
+
"allow_level must be applied by the User Agent Host before resuming the Tool",
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
const temporary = await this.temporaryAgentApprovals.decide(executionId, decision);
|
|
1460
|
+
if (temporary) return temporary;
|
|
1461
|
+
const result = this.humanDecisions.approve(executionId, decision);
|
|
1462
|
+
if (result.submissionId) {
|
|
1463
|
+
await this.broadcastApprovals();
|
|
1464
|
+
this.ctx.waitUntil(
|
|
1465
|
+
this.submissions.continueAfterCurrent(result.submissionId).then(() => undefined),
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
return { ok: result.ok };
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
/** Project Agents SDK child-run startup into canonical telemetry and state. */
|
|
1472
|
+
override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
|
|
1473
|
+
await super.onAgentToolStart(run);
|
|
1474
|
+
const metadata = runtimeSubagentRunMetadata(run.inputPreview);
|
|
1475
|
+
if (metadata) await this.runtimeTurnEvents.subagentStarted(metadata, run);
|
|
1476
|
+
await this.broadcastApprovals();
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
/** Rebuild activity after an Agent Tool reaches or approaches terminal state. */
|
|
1480
|
+
override async onAgentToolFinish(
|
|
1481
|
+
run: AgentToolRunInfo,
|
|
1482
|
+
result: AgentToolLifecycleResult,
|
|
1483
|
+
): Promise<void> {
|
|
1484
|
+
await super.onAgentToolFinish(run, result);
|
|
1485
|
+
const metadata = runtimeSubagentRunMetadata(run.inputPreview);
|
|
1486
|
+
if (metadata) await this.runtimeTurnEvents.subagentFinished(metadata, run, result);
|
|
1487
|
+
await this.broadcastApprovals();
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
async _cfDetachedNotifyFinish(
|
|
1491
|
+
run: AgentToolRunInfo,
|
|
1492
|
+
result: AgentToolLifecycleResult,
|
|
1493
|
+
): Promise<void> {
|
|
1494
|
+
const completion = decodeSubagentCompletion(result.summary);
|
|
1495
|
+
const outcome = result.status === "completed"
|
|
1496
|
+
? completion
|
|
1497
|
+
? JSON.stringify(completion.output)
|
|
1498
|
+
: result.summary ?? "Completed without a result."
|
|
1499
|
+
: result.error ?? `Background run ${result.status}.`;
|
|
1500
|
+
await this.submitPrompt(
|
|
1501
|
+
[
|
|
1502
|
+
"A background sub-agent run has finished.",
|
|
1503
|
+
`runId: ${run.runId}`,
|
|
1504
|
+
`status: ${result.status}`,
|
|
1505
|
+
`result: ${outcome}`,
|
|
1506
|
+
].join("\n"),
|
|
1507
|
+
{ idempotencyKey: `detached-agent-tool:${run.runId}:${result.status}` },
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/** Rebuild best-effort UI state from durable facts. */
|
|
1512
|
+
protected async broadcastApprovals(): Promise<void> {
|
|
1513
|
+
try {
|
|
1514
|
+
const { state, changed } = projectRuntimeState({
|
|
1515
|
+
approvals: this.db.approvals.listPendingActive(),
|
|
1516
|
+
temporaryApprovals: this.temporaryAgentApprovals.list(),
|
|
1517
|
+
running: this.db.submissions.findRunning(),
|
|
1518
|
+
pending: this.db.submissions.listPending(),
|
|
1519
|
+
hasPendingApproval: (id) => this.humanDecisions.isWaitingForApproval(id),
|
|
1520
|
+
hasPendingInteraction: (id) => this.humanDecisions.isWaitingForInteraction(id),
|
|
1521
|
+
hasPendingSteer: (id) => this.db.steers.listForSubmission(id).length > 0,
|
|
1522
|
+
hasRunningAgentTools: this.db.agentTools.hasRunning(),
|
|
1523
|
+
previousState: this.state,
|
|
1524
|
+
});
|
|
1525
|
+
if (changed) this.setState(state);
|
|
1526
|
+
const projection = this.activeRuntime?.snapshot.bindings.turnEvents?.onActivityChanged?.(state.activity!);
|
|
1527
|
+
if (projection) {
|
|
1528
|
+
this.ctx.waitUntil(
|
|
1529
|
+
projection.catch(() => undefined),
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
} catch {
|
|
1533
|
+
// Approval projection is best effort and must not block execution.
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// #endregion
|
|
1538
|
+
|
|
1539
|
+
}
|