@springbrand/agent-runtime 0.3.0-alpha.2 → 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 +11 -9
- package/src/adapter/cloudflare/agent.ts +233 -179
- package/src/adapter/cloudflare/define-agent.ts +252 -104
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +21 -2
- package/src/adapter/cloudflare/subagent/definition.ts +25 -13
- package/src/adapter/cloudflare/subagent/runner.ts +43 -50
- package/src/adapter/cloudflare/subagent/tools.ts +4 -4
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/activation.ts +13 -15
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +48 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +77 -49
- package/src/config/index.ts +1 -1
- package/src/config/model-guidance.ts +20 -0
- package/src/config/ports.ts +57 -7
- package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
- package/src/config/public-contracts.ts +2 -2
- package/src/config/runtime-config.ts +2 -1
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-resume.ts +89 -238
- package/src/conversation/harness-stream.ts +87 -71
- package/src/conversation/history.ts +154 -152
- package/src/conversation/message/contract.ts +28 -4
- package/src/conversation/message/conversion.ts +46 -5
- package/src/conversation/message/projection.ts +61 -21
- package/src/conversation/session-entry.ts +29 -0
- package/src/conversation/state.ts +23 -2
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +269 -445
- package/src/harness/submission-operation.ts +15 -40
- package/src/index.ts +18 -8
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/model-protocol/openrouter.ts +41 -0
- package/src/models/index.ts +200 -35
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/openrouter/openai.ts +200 -0
- package/src/models/openrouter/protocol-response.ts +85 -0
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +382 -864
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +20 -30
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +97 -49
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +35 -25
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +319 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/runtime/compiler.ts +246 -0
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1322 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/compiler.ts +0 -327
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import { validateRuntimeAssemblyPin, type RuntimeAssemblyPin } from "../config/pinning";
|
|
3
2
|
import type {
|
|
4
3
|
RuntimeSubmissionAdmissionHook,
|
|
5
4
|
RuntimeSubmissionAdmissionResult,
|
|
@@ -12,10 +11,12 @@ import {
|
|
|
12
11
|
SubmissionQueueFullError,
|
|
13
12
|
type SubmissionInput,
|
|
14
13
|
} from "./lifecycle";
|
|
15
|
-
import { isTerminalSubmissionStatus, submissionAbortReason, type StoredSubmission, type SubmissionRepository } from "./repository";
|
|
14
|
+
import { isTerminalSubmissionStatus, submissionAbortReason, type StoredSubmission, type SubmissionOrigin, type SubmissionRepository } from "./repository";
|
|
15
|
+
import type { RuntimeUnattendedExecutionGrant } from "./contracts";
|
|
16
|
+
import type { ExecutionLevel } from "../lib/execution-level";
|
|
16
17
|
|
|
17
18
|
// Pending records retain identity indefinitely; only recent attempts reserve capacity.
|
|
18
|
-
const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
19
|
+
export const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
19
20
|
|
|
20
21
|
export interface SubmitMessageOptions {
|
|
21
22
|
requestId: string;
|
|
@@ -23,6 +24,12 @@ export interface SubmitMessageOptions {
|
|
|
23
24
|
userMessageId?: string;
|
|
24
25
|
userMessage?: UIMessage & { role: "user" };
|
|
25
26
|
regenerate?: boolean;
|
|
27
|
+
/** `scheduled` marks a Run injection that locks ordinary input until it ends. */
|
|
28
|
+
origin?: SubmissionOrigin;
|
|
29
|
+
/** Maximum execution level allowed by the Schedule Definition. */
|
|
30
|
+
scheduledMaxExecutionLevel?: ExecutionLevel;
|
|
31
|
+
/** Trusted Host grant; accepted only for a scheduled Submission. */
|
|
32
|
+
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
26
33
|
/** Internal transfer of a durable accepted steer; never a public message option. */
|
|
27
34
|
steerHandoff?: { submissionId: string; steerId: string };
|
|
28
35
|
}
|
|
@@ -169,8 +176,6 @@ export class SubmissionAdmission {
|
|
|
169
176
|
requestId,
|
|
170
177
|
idempotencyKey: idempotencyKey ?? null,
|
|
171
178
|
createdAt: userMessage.timestamp,
|
|
172
|
-
assemblyRevision: "",
|
|
173
|
-
assemblyDescriptor: "",
|
|
174
179
|
runtimeKey: runtime.runtimeKey,
|
|
175
180
|
assistantMessageId: crypto.randomUUID(),
|
|
176
181
|
queuedInputJson: JSON.stringify(userMessage),
|
|
@@ -182,6 +187,9 @@ export class SubmissionAdmission {
|
|
|
182
187
|
? options.userMessageId ?? null
|
|
183
188
|
: null,
|
|
184
189
|
runId,
|
|
190
|
+
origin: options.origin,
|
|
191
|
+
scheduledMaxExecutionLevel: options.scheduledMaxExecutionLevel,
|
|
192
|
+
unattendedExecutionGrant: options.unattendedExecutionGrant,
|
|
185
193
|
});
|
|
186
194
|
const admitted = this.options.submissions.find(runId)!;
|
|
187
195
|
this.options.onAdmitted(admitted, admission?.accepted === true);
|
|
@@ -199,38 +207,20 @@ export class SubmissionAdmission {
|
|
|
199
207
|
};
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
/**
|
|
203
|
-
async
|
|
210
|
+
/** Resolve the saved Agent identity against its current Config before every execution slice. */
|
|
211
|
+
async prepareCurrentRuntime(
|
|
204
212
|
submission: StoredSubmission,
|
|
205
|
-
|
|
206
|
-
restoreRuntime?: (runtimeKey: string) => Promise<void>,
|
|
213
|
+
restoreRuntime: (runtimeKey: string) => Promise<void>,
|
|
207
214
|
): Promise<StoredSubmission> {
|
|
208
215
|
const current = this.options.submissions.find(submission.submissionId);
|
|
209
216
|
if (!current) throw new Error(`Unknown SpringBrand submission: ${submission.submissionId}`);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
descriptor: current.assemblyDescriptor,
|
|
213
|
-
});
|
|
214
|
-
await restoreRuntime?.(current.runtimeKey);
|
|
217
|
+
if (!current.runtimeKey) throw new Error("Cannot resume a Submission without its Agent Config ID");
|
|
218
|
+
await restoreRuntime(current.runtimeKey);
|
|
215
219
|
const restored = this.options.submissions.find(current.submissionId);
|
|
216
220
|
if (!restored || isTerminalSubmissionStatus(restored.status)) {
|
|
217
|
-
throw new Error("Submission ended
|
|
218
|
-
}
|
|
219
|
-
if (hasPin) return restored;
|
|
220
|
-
const pin = await preparePin();
|
|
221
|
-
if (!await validateRuntimeAssemblyPin(current.runtimeKey, pin)) {
|
|
222
|
-
throw new Error("Submission assembly pin is incomplete");
|
|
221
|
+
throw new Error("Submission ended while loading current Agent Config");
|
|
223
222
|
}
|
|
224
|
-
|
|
225
|
-
const pinned = this.options.submissions.find(current.submissionId);
|
|
226
|
-
if (!pinned?.assemblyRevision || !pinned.assemblyDescriptor || isTerminalSubmissionStatus(pinned.status)) {
|
|
227
|
-
throw new Error("Submission ended before Runtime assembly was pinned");
|
|
228
|
-
}
|
|
229
|
-
await validateRuntimeAssemblyPin(pinned.runtimeKey, {
|
|
230
|
-
revision: pinned.assemblyRevision,
|
|
231
|
-
descriptor: pinned.assemblyDescriptor,
|
|
232
|
-
});
|
|
233
|
-
return pinned;
|
|
223
|
+
return restored;
|
|
234
224
|
}
|
|
235
225
|
|
|
236
226
|
/** The caller shares this synchronous write with its admission/timeout transaction. */
|
|
@@ -13,6 +13,8 @@ export const APPROVAL_DECISIONS = [
|
|
|
13
13
|
"allow_level",
|
|
14
14
|
] as const satisfies readonly ApprovalDecision["decision"][];
|
|
15
15
|
|
|
16
|
+
export type { RuntimeUnattendedExecutionGrant } from "./unattended";
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* 记录一次待审批 Tool 执行的身份、摘要和原始输入。
|
|
18
20
|
*
|
|
@@ -58,12 +60,35 @@ export type ScheduleJob =
|
|
|
58
60
|
*
|
|
59
61
|
* 触发条件与工作放在同一份规格里,使更新时可以原地替换两者,不用改变任务标识。
|
|
60
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* 一条按任务时区解释的当地时间规则。
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* `once` 在单个本地日历日触发;`daily` 每个本地日触发;`weekly` 在选中的每个本地星期触发
|
|
68
|
+
* (0 = 周日 … 6 = 周六)。所有星期共用同一个本地钟点。
|
|
69
|
+
*
|
|
70
|
+
* 规则只描述当地墙上时间;缺失/重复钟点、起止日期和夏令时语义由
|
|
71
|
+
* `adapter/cloudflare/scheduling` 的公共计算边界处理。
|
|
72
|
+
*/
|
|
73
|
+
export type LocalScheduleRule =
|
|
74
|
+
| { kind: "once"; date: string; time: string }
|
|
75
|
+
| { kind: "daily"; time: string }
|
|
76
|
+
| { kind: "weekly"; weekdays: number[]; time: string };
|
|
77
|
+
|
|
61
78
|
export interface ScheduleSpec {
|
|
62
79
|
trigger:
|
|
63
80
|
| { kind: "once-at"; at: string }
|
|
64
81
|
| { kind: "once-in"; seconds: number }
|
|
65
82
|
| { kind: "cron"; cron: string }
|
|
66
|
-
| { kind: "interval"; seconds: number }
|
|
83
|
+
| { kind: "interval"; seconds: number }
|
|
84
|
+
| {
|
|
85
|
+
kind: "local";
|
|
86
|
+
rule: LocalScheduleRule;
|
|
87
|
+
/** 含当天的最早生效本地日期 `YYYY-MM-DD`。 */
|
|
88
|
+
startDate?: string;
|
|
89
|
+
/** 含当天的最后生效本地日期 `YYYY-MM-DD`。 */
|
|
90
|
+
endDate?: string;
|
|
91
|
+
};
|
|
67
92
|
job: ScheduleJob;
|
|
68
93
|
label?: string;
|
|
69
94
|
/**
|
|
@@ -115,7 +140,9 @@ export type MessageDelivery = "steer" | "enqueue";
|
|
|
115
140
|
export type MessageDispatchErrorCode =
|
|
116
141
|
| "queue_full"
|
|
117
142
|
| "not_steerable"
|
|
118
|
-
| "invalid_message"
|
|
143
|
+
| "invalid_message"
|
|
144
|
+
/** A scheduled Run owns this Session's input until it reaches a terminal state. */
|
|
145
|
+
| "input_locked";
|
|
119
146
|
|
|
120
147
|
export type MessageDispatchReceipt =
|
|
121
148
|
| {
|
|
@@ -139,3 +166,16 @@ export type MessageDispatchReceipt =
|
|
|
139
166
|
code: MessageDispatchErrorCode;
|
|
140
167
|
message: string;
|
|
141
168
|
};
|
|
169
|
+
|
|
170
|
+
/** Complete, credential-free task version prepared by a trusted Host for human confirmation. */
|
|
171
|
+
export interface RuntimeSchedulePreparation {
|
|
172
|
+
candidateId: string;
|
|
173
|
+
candidateVersion: string;
|
|
174
|
+
id?: string;
|
|
175
|
+
prompt: string;
|
|
176
|
+
label: string;
|
|
177
|
+
trigger: ScheduleSpec["trigger"];
|
|
178
|
+
timezone: string;
|
|
179
|
+
nextRunAt?: number;
|
|
180
|
+
authorization: { version: string; missing: string[]; scope: string };
|
|
181
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { UIMessage } from "ai";
|
|
|
2
2
|
import type { PiCanonicalUserInput } from "../conversation/message/contract";
|
|
3
3
|
import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
|
|
4
4
|
import type { RuntimeTurnEvents } from "../telemetry/turn-events";
|
|
5
|
-
import type { HarnessHumanDecisions } from "../tools/human-decisions";
|
|
5
|
+
import type { HarnessHumanDecisions } from "../tools/runtime/human-decisions";
|
|
6
6
|
import type { SubmitMessageOptions } from "./admission";
|
|
7
7
|
import {
|
|
8
8
|
projectSubmissionReceipt,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { CancelQueuedResult } from "
|
|
2
|
+
import type { CancelQueuedResult } from "./steer-delivery";
|
|
3
3
|
import type { ConversationHistory } from "../conversation/history";
|
|
4
4
|
import type { MessageUiRepository } from "../conversation/repository";
|
|
5
5
|
import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
|
|
6
6
|
import type { RuntimeTurnEvents } from "../telemetry/turn-events";
|
|
7
|
-
import type { HarnessHumanDecisions } from "../tools/human-decisions";
|
|
7
|
+
import type { HarnessHumanDecisions } from "../tools/runtime/human-decisions";
|
|
8
8
|
import { USER_STOP_REASON } from "./contracts";
|
|
9
9
|
import type { SubmissionOutcome } from "./lifecycle";
|
|
10
10
|
import { isTerminalSubmissionStatus, type StoredSubmission, type SubmissionRepository } from "./repository";
|
|
@@ -13,6 +13,8 @@ import type { SteerRepository } from "./steer-repository";
|
|
|
13
13
|
|
|
14
14
|
/** Commits the Submission outcome and human/Host facts after accepted steering is preserved. */
|
|
15
15
|
export class SubmissionFinalizer {
|
|
16
|
+
/** User-stop markers appended after their terminal commit; every `finish()` of that Turn awaits it. */
|
|
17
|
+
private readonly stopMarkers = new Map<string, Promise<unknown>>();
|
|
16
18
|
constructor(private readonly options: {
|
|
17
19
|
submissions: SubmissionRepository;
|
|
18
20
|
steers: SteerRepository;
|
|
@@ -34,7 +36,12 @@ export class SubmissionFinalizer {
|
|
|
34
36
|
message?: string,
|
|
35
37
|
): Promise<StoredSubmission> {
|
|
36
38
|
const initial = this.read(submissionId);
|
|
37
|
-
if (isTerminalSubmissionStatus(initial.status))
|
|
39
|
+
if (isTerminalSubmissionStatus(initial.status)) {
|
|
40
|
+
// A stop RPC and the ending harness finish the same Turn; whichever loses
|
|
41
|
+
// still waits for the stop marker, so its history broadcast carries it.
|
|
42
|
+
await this.stopMarkers.get(submissionId);
|
|
43
|
+
return initial;
|
|
44
|
+
}
|
|
38
45
|
const aborted = Boolean(initial.abortReason) || outcome === "aborted";
|
|
39
46
|
if (aborted) await this.options.releaseAbortedHarness();
|
|
40
47
|
await finishPendingSteers({
|
|
@@ -53,7 +60,13 @@ export class SubmissionFinalizer {
|
|
|
53
60
|
const output = (await this.options.conversation.storedMessages())
|
|
54
61
|
.filter((entry) => entry.submissionId === submissionId && entry.message.role === "assistant")
|
|
55
62
|
.at(-1)?.message;
|
|
56
|
-
|
|
63
|
+
const completed = this.commit(submissionId, outcome, message, output?.role === "assistant" ? output : undefined);
|
|
64
|
+
const marker = this.stopMarkers.get(submissionId);
|
|
65
|
+
if (marker) {
|
|
66
|
+
await marker;
|
|
67
|
+
this.stopMarkers.delete(submissionId);
|
|
68
|
+
}
|
|
69
|
+
return completed;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
/** Shares atomic terminal facts with an admitted handoff cancelled before execution. */
|
|
@@ -63,7 +76,8 @@ export class SubmissionFinalizer {
|
|
|
63
76
|
message?: string,
|
|
64
77
|
output?: AssistantMessage,
|
|
65
78
|
): StoredSubmission {
|
|
66
|
-
|
|
79
|
+
let stoppedByUser = false;
|
|
80
|
+
const completed = this.options.transaction(() => {
|
|
67
81
|
// Host admission and history reads can await while an explicit stop wins.
|
|
68
82
|
const latest = this.read(submissionId);
|
|
69
83
|
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
@@ -79,16 +93,9 @@ export class SubmissionFinalizer {
|
|
|
79
93
|
const status = effectiveOutcome === "succeeded" ? "completed" : effectiveOutcome === "aborted" ? "aborted" : "error";
|
|
80
94
|
this.options.submissions.updateTerminal(submissionId, status, reason, Date.now());
|
|
81
95
|
const completed = this.read(submissionId);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
`Aborted SpringBrand submission has no completion time: ${submissionId}`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
this.options.conversation.appendTurnAbortedMarker(
|
|
89
|
-
submissionId,
|
|
90
|
-
completed.completedAt,
|
|
91
|
-
);
|
|
96
|
+
stoppedByUser = effectiveOutcome === "aborted" && reason === USER_STOP_REASON;
|
|
97
|
+
if (stoppedByUser && completed.completedAt === null) {
|
|
98
|
+
throw new Error(`Aborted SpringBrand submission has no completion time: ${submissionId}`);
|
|
92
99
|
}
|
|
93
100
|
this.options.events.submissionTerminal(completed);
|
|
94
101
|
this.options.telemetry.capture("turnFinished", {
|
|
@@ -101,6 +108,20 @@ export class SubmissionFinalizer {
|
|
|
101
108
|
});
|
|
102
109
|
return completed;
|
|
103
110
|
});
|
|
111
|
+
if (stoppedByUser) this.appendStopMarker(submissionId, completed.completedAt!);
|
|
112
|
+
return completed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The browser projects `interruptedByUser` from this marker and the next model request sees it as user context.
|
|
117
|
+
* Pi history writes are async, so the marker follows the synchronous terminal commit.
|
|
118
|
+
*/
|
|
119
|
+
private appendStopMarker(submissionId: string, completedAt: number): void {
|
|
120
|
+
const marker = this.options.conversation.appendTurnAbortedMarker(submissionId, completedAt).catch((error: unknown) => {
|
|
121
|
+
console.warn("[runtime-stop-marker:failed]", submissionId, error instanceof Error ? error.message : String(error));
|
|
122
|
+
});
|
|
123
|
+
this.stopMarkers.set(submissionId, marker);
|
|
124
|
+
this.options.waitUntil(marker);
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
private read(submissionId: string): StoredSubmission {
|
|
@@ -130,9 +130,9 @@ export type SubmissionOutcome = "succeeded" | "failed" | "aborted";
|
|
|
130
130
|
* @remarks
|
|
131
131
|
* `SubmissionLifecycle` 把它交给 `execute`,Runtime 据此选择执行前置动作。
|
|
132
132
|
*
|
|
133
|
-
* `recovery` 和 `continuation`
|
|
134
|
-
*
|
|
135
|
-
*
|
|
133
|
+
* `recovery` 和 `continuation` 都以持久化的 Agent 身份重新读取当前配置,
|
|
134
|
+
* 然后继续同一个持久 operation;前者处理进程恢复,后者处理本轮主动让出执行片。
|
|
135
|
+
* 两者都不沿用旧执行片的固定配置。
|
|
136
136
|
*/
|
|
137
137
|
export type SubmissionExecutionMode =
|
|
138
138
|
/** 首次执行一条新接收的提交。 */
|
|
@@ -244,6 +244,7 @@ export class SubmissionLifecycle<
|
|
|
244
244
|
private readonly executions = new Map<string, Promise<TSubmission>>();
|
|
245
245
|
private readonly activeBySubmission = new Map<string, TActive>();
|
|
246
246
|
private readonly terminalCommits = new Map<string, Promise<TSubmission>>();
|
|
247
|
+
private preparingAdmissions = 0;
|
|
247
248
|
|
|
248
249
|
// #region 提交与恢复
|
|
249
250
|
|
|
@@ -262,6 +263,13 @@ export class SubmissionLifecycle<
|
|
|
262
263
|
>,
|
|
263
264
|
) {}
|
|
264
265
|
|
|
266
|
+
/** Serialize an explicit Runtime change with this Chat's execution queue. */
|
|
267
|
+
async runConfigControl<T>(run: () => Promise<T>): Promise<T> {
|
|
268
|
+
const result = await this.queue.enqueue(`runtime-config:${crypto.randomUUID()}`, run);
|
|
269
|
+
if (result.status === "stale") throw new Error("Runtime configuration control was reset");
|
|
270
|
+
return result.value;
|
|
271
|
+
}
|
|
272
|
+
|
|
265
273
|
/**
|
|
266
274
|
* 接收新提交,或让重复请求加入已有提交。
|
|
267
275
|
*
|
|
@@ -275,17 +283,22 @@ export class SubmissionLifecycle<
|
|
|
275
283
|
): Promise<SubmissionHandle<TSubmission>> {
|
|
276
284
|
const duplicate = this.findDuplicate(input);
|
|
277
285
|
if (duplicate) return this.join(duplicate);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
await
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
:
|
|
288
|
-
|
|
286
|
+
this.preparingAdmissions += 1;
|
|
287
|
+
try {
|
|
288
|
+
const admitted = await this.admit(
|
|
289
|
+
input,
|
|
290
|
+
await input.prepareAdmission(),
|
|
291
|
+
);
|
|
292
|
+
if (admitted.admitted) this.pump();
|
|
293
|
+
return {
|
|
294
|
+
receipt: admitted.submission,
|
|
295
|
+
completion: admitted.rejected
|
|
296
|
+
? Promise.resolve(admitted.submission)
|
|
297
|
+
: this.waitForTerminal(admitted.submission.submissionId),
|
|
298
|
+
};
|
|
299
|
+
} finally {
|
|
300
|
+
this.preparingAdmissions -= 1;
|
|
301
|
+
}
|
|
289
302
|
}
|
|
290
303
|
|
|
291
304
|
/**
|
|
@@ -551,7 +564,7 @@ export class SubmissionLifecycle<
|
|
|
551
564
|
* 读取持久层而不是只看 `executions`,才能覆盖重启后尚未恢复的提交。
|
|
552
565
|
*/
|
|
553
566
|
isBusy(): boolean {
|
|
554
|
-
return this.options.store.countUnfinished() > 0;
|
|
567
|
+
return this.preparingAdmissions > 0 || this.options.store.countUnfinished() > 0;
|
|
555
568
|
}
|
|
556
569
|
|
|
557
570
|
// 作用:在有限时间内轮询 Runtime 是否已无活动提交。
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { SqlTaggedTemplate } from "agents/chat";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import type { ExecutionLevel } from "../lib/execution-level";
|
|
4
|
+
import type { RuntimeUnattendedExecutionGrant } from "./unattended";
|
|
3
5
|
|
|
4
6
|
// Submission 的统一定义见 ./index.ts。
|
|
5
7
|
// #region 状态与行类型
|
|
@@ -11,6 +13,14 @@ export type SubmissionStatus =
|
|
|
11
13
|
| "skipped"
|
|
12
14
|
| "error";
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 谁发起了这条 Submission。
|
|
18
|
+
*
|
|
19
|
+
* `scheduled` 是定时 Run 的注入:它在本 Session 未结束期间独占输入,
|
|
20
|
+
* 普通 `user` Submission 必须被拒绝,而不是排队等它结束。
|
|
21
|
+
*/
|
|
22
|
+
export type SubmissionOrigin = "user" | "scheduled";
|
|
23
|
+
|
|
14
24
|
const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
15
25
|
"completed",
|
|
16
26
|
"aborted",
|
|
@@ -36,8 +46,6 @@ export interface StoredSubmission {
|
|
|
36
46
|
error: string | null;
|
|
37
47
|
createdAt: number;
|
|
38
48
|
completedAt: number | null;
|
|
39
|
-
assemblyRevision: string;
|
|
40
|
-
assemblyDescriptor: string;
|
|
41
49
|
runtimeKey: string;
|
|
42
50
|
assistantMessageId: string;
|
|
43
51
|
abortReason: string | null;
|
|
@@ -46,9 +54,24 @@ export interface StoredSubmission {
|
|
|
46
54
|
userMessageId: string | null;
|
|
47
55
|
regenerateMessageId: string | null;
|
|
48
56
|
runId: string | null;
|
|
57
|
+
origin: SubmissionOrigin;
|
|
58
|
+
scheduledMaxExecutionLevel: ExecutionLevel | null;
|
|
59
|
+
unattendedExecutionGrant: RuntimeUnattendedExecutionGrant | null;
|
|
60
|
+
unattendedFailure: string | null;
|
|
49
61
|
retryable?: boolean;
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
/** Saved key and whether its Submission can still execute. */
|
|
65
|
+
export interface StoredRuntimeKey {
|
|
66
|
+
readonly runtimeKey: string;
|
|
67
|
+
readonly unfinished: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RuntimeKeyRewrite {
|
|
71
|
+
readonly from: string;
|
|
72
|
+
readonly to: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
/** Reuse durable stop intent when transferring work out of the source Submission. */
|
|
53
76
|
export function submissionAbortReason(submission: StoredSubmission | null): string | undefined {
|
|
54
77
|
return submission?.abortReason ||
|
|
@@ -60,8 +83,6 @@ export interface NewSubmission {
|
|
|
60
83
|
requestId: string;
|
|
61
84
|
idempotencyKey: string | null;
|
|
62
85
|
createdAt: number;
|
|
63
|
-
assemblyRevision: string;
|
|
64
|
-
assemblyDescriptor: string;
|
|
65
86
|
runtimeKey?: string;
|
|
66
87
|
assistantMessageId: string;
|
|
67
88
|
queuedInputJson?: string | null;
|
|
@@ -69,6 +90,9 @@ export interface NewSubmission {
|
|
|
69
90
|
userMessageId?: string | null;
|
|
70
91
|
regenerateMessageId?: string | null;
|
|
71
92
|
runId?: string | null;
|
|
93
|
+
origin?: SubmissionOrigin;
|
|
94
|
+
scheduledMaxExecutionLevel?: ExecutionLevel;
|
|
95
|
+
unattendedExecutionGrant?: RuntimeUnattendedExecutionGrant;
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
type SubmissionRow = {
|
|
@@ -80,8 +104,6 @@ type SubmissionRow = {
|
|
|
80
104
|
error: string | null;
|
|
81
105
|
created_at: number;
|
|
82
106
|
completed_at: number | null;
|
|
83
|
-
assembly_revision: string;
|
|
84
|
-
assembly_descriptor: string;
|
|
85
107
|
runtime_key: string;
|
|
86
108
|
assistant_message_id: string;
|
|
87
109
|
abort_reason: string | null;
|
|
@@ -90,7 +112,11 @@ type SubmissionRow = {
|
|
|
90
112
|
user_message_id: string | null;
|
|
91
113
|
regenerate_message_id: string | null;
|
|
92
114
|
run_id: string | null;
|
|
115
|
+
origin: string | null;
|
|
116
|
+
scheduled_max_execution_level: string | null;
|
|
93
117
|
admission_retryable: number | null;
|
|
118
|
+
unattended_execution_grant_json: string | null;
|
|
119
|
+
unattended_failure: string | null;
|
|
94
120
|
};
|
|
95
121
|
|
|
96
122
|
// #endregion
|
|
@@ -108,8 +134,6 @@ function mapRow(row: SubmissionRow): StoredSubmission {
|
|
|
108
134
|
error: row.error,
|
|
109
135
|
createdAt: row.created_at,
|
|
110
136
|
completedAt: row.completed_at,
|
|
111
|
-
assemblyRevision: row.assembly_revision,
|
|
112
|
-
assemblyDescriptor: row.assembly_descriptor,
|
|
113
137
|
runtimeKey: row.runtime_key,
|
|
114
138
|
assistantMessageId: row.assistant_message_id,
|
|
115
139
|
abortReason: row.abort_reason,
|
|
@@ -118,6 +142,12 @@ function mapRow(row: SubmissionRow): StoredSubmission {
|
|
|
118
142
|
userMessageId: row.user_message_id,
|
|
119
143
|
regenerateMessageId: row.regenerate_message_id,
|
|
120
144
|
runId: row.run_id,
|
|
145
|
+
origin: row.origin === "scheduled" ? "scheduled" : "user",
|
|
146
|
+
scheduledMaxExecutionLevel: row.scheduled_max_execution_level as ExecutionLevel | null,
|
|
147
|
+
unattendedExecutionGrant: row.unattended_execution_grant_json
|
|
148
|
+
? JSON.parse(row.unattended_execution_grant_json) as RuntimeUnattendedExecutionGrant
|
|
149
|
+
: null,
|
|
150
|
+
unattendedFailure: row.unattended_failure,
|
|
121
151
|
retryable: row.admission_retryable === null
|
|
122
152
|
? undefined
|
|
123
153
|
: row.admission_retryable === 1,
|
|
@@ -139,18 +169,44 @@ export class SubmissionRepository {
|
|
|
139
169
|
return this.transact(run);
|
|
140
170
|
}
|
|
141
171
|
|
|
172
|
+
/** Return saved identities for a one-time Host-owned key migration. */
|
|
173
|
+
listRuntimeKeys(): StoredRuntimeKey[] {
|
|
174
|
+
return this.sql<{ runtime_key: string; unfinished: number }>`
|
|
175
|
+
SELECT runtime_key,
|
|
176
|
+
MAX(CASE WHEN status IN ('pending', 'running') THEN 1 ELSE 0 END) AS unfinished
|
|
177
|
+
FROM pi_submissions
|
|
178
|
+
WHERE runtime_key <> ''
|
|
179
|
+
GROUP BY runtime_key
|
|
180
|
+
ORDER BY runtime_key
|
|
181
|
+
`.map((row) => ({ runtimeKey: row.runtime_key, unfinished: row.unfinished === 1 }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Rewrite each saved identity atomically after the Host validates active work. */
|
|
185
|
+
rewriteRuntimeKeys(rewrites: readonly RuntimeKeyRewrite[]): void {
|
|
186
|
+
this.transact(() => {
|
|
187
|
+
for (const { from, to } of rewrites) {
|
|
188
|
+
if (from === to) continue;
|
|
189
|
+
this.sql`UPDATE pi_submissions SET runtime_key = ${to} WHERE runtime_key = ${from}`;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
142
194
|
// 按 submissionId 读取一条完整 Submission,不存在时返回 null。
|
|
143
195
|
// Runtime 和 SubmissionLifecycle 在准入、执行、取消、结束与恢复时调用,调用方必须处理 null。
|
|
144
196
|
// 所有列在这里一次投影并统一映射,不能让不同调用方各自拼出不完整的 Submission。
|
|
145
197
|
find(id: string): StoredSubmission | null {
|
|
146
198
|
const row = this.sql<SubmissionRow>`
|
|
147
199
|
SELECT submission_id, request_id, idempotency_key, status, accepted,
|
|
148
|
-
error, created_at, completed_at,
|
|
149
|
-
|
|
200
|
+
error, created_at, completed_at,
|
|
201
|
+
runtime_key, assistant_message_id, abort_reason,
|
|
150
202
|
queued_input_json, queued_ui_message_json, user_message_id,
|
|
151
203
|
regenerate_message_id,
|
|
152
204
|
run_id,
|
|
153
|
-
|
|
205
|
+
origin,
|
|
206
|
+
scheduled_max_execution_level,
|
|
207
|
+
admission_retryable,
|
|
208
|
+
unattended_execution_grant_json,
|
|
209
|
+
unattended_failure
|
|
154
210
|
FROM pi_submissions
|
|
155
211
|
WHERE submission_id = ${id}
|
|
156
212
|
`[0];
|
|
@@ -248,10 +304,10 @@ export class SubmissionRepository {
|
|
|
248
304
|
this.sql`
|
|
249
305
|
INSERT INTO pi_submissions (
|
|
250
306
|
submission_id, request_id, idempotency_key, status, accepted,
|
|
251
|
-
created_at,
|
|
307
|
+
created_at, runtime_key,
|
|
252
308
|
assistant_message_id, queued_input_json, queued_ui_message_json,
|
|
253
309
|
user_message_id, regenerate_message_id,
|
|
254
|
-
run_id
|
|
310
|
+
run_id, origin, scheduled_max_execution_level, unattended_execution_grant_json
|
|
255
311
|
) VALUES (
|
|
256
312
|
${s.submissionId},
|
|
257
313
|
${s.requestId},
|
|
@@ -259,19 +315,37 @@ export class SubmissionRepository {
|
|
|
259
315
|
'pending',
|
|
260
316
|
1,
|
|
261
317
|
${s.createdAt},
|
|
262
|
-
${s.assemblyRevision},
|
|
263
|
-
${s.assemblyDescriptor},
|
|
264
318
|
${s.runtimeKey ?? ""},
|
|
265
319
|
${s.assistantMessageId},
|
|
266
320
|
${s.queuedInputJson ?? null},
|
|
267
321
|
${s.queuedUiMessageJson ?? null},
|
|
268
322
|
${s.userMessageId ?? null},
|
|
269
323
|
${s.regenerateMessageId ?? null},
|
|
270
|
-
${s.runId ?? s.submissionId}
|
|
324
|
+
${s.runId ?? s.submissionId},
|
|
325
|
+
${s.origin ?? "user"},
|
|
326
|
+
${s.scheduledMaxExecutionLevel ?? null},
|
|
327
|
+
${s.unattendedExecutionGrant
|
|
328
|
+
? JSON.stringify(s.unattendedExecutionGrant)
|
|
329
|
+
: null}
|
|
271
330
|
)
|
|
272
331
|
`;
|
|
273
332
|
}
|
|
274
333
|
|
|
334
|
+
/**
|
|
335
|
+
* 是否存在一条未结束的定时 Submission。
|
|
336
|
+
*
|
|
337
|
+
* 定时 Run 未结束时它独占本 Session 的输入:普通消息在准入前被拒绝,
|
|
338
|
+
* 不排队等待。Run 进入终态后查询自然为空,普通追问恢复。
|
|
339
|
+
*/
|
|
340
|
+
hasUnfinishedScheduled(): boolean {
|
|
341
|
+
return (
|
|
342
|
+
this.sql<{ count: number }>`
|
|
343
|
+
SELECT COUNT(*) AS count FROM pi_submissions
|
|
344
|
+
WHERE status IN ('pending', 'running') AND origin = 'scheduled'
|
|
345
|
+
`[0]?.count ?? 0
|
|
346
|
+
) > 0;
|
|
347
|
+
}
|
|
348
|
+
|
|
275
349
|
insertRejected(s: {
|
|
276
350
|
submissionId: string;
|
|
277
351
|
requestId: string;
|
|
@@ -285,29 +359,24 @@ export class SubmissionRepository {
|
|
|
285
359
|
this.sql`
|
|
286
360
|
INSERT INTO pi_submissions (
|
|
287
361
|
submission_id, request_id, idempotency_key, status, accepted,
|
|
288
|
-
error, created_at, completed_at,
|
|
289
|
-
|
|
362
|
+
error, created_at, completed_at,
|
|
363
|
+
runtime_key, assistant_message_id, abort_reason,
|
|
290
364
|
run_id, admission_retryable, user_message_id
|
|
291
365
|
) VALUES (
|
|
292
366
|
${s.submissionId}, ${s.requestId}, ${s.idempotencyKey}, 'skipped', 0,
|
|
293
|
-
${s.code}, ${s.createdAt}, ${s.createdAt}, '',
|
|
367
|
+
${s.code}, ${s.createdAt}, ${s.createdAt}, '', ${s.assistantMessageId ?? s.submissionId}, NULL,
|
|
294
368
|
${s.submissionId}, ${s.retryable ? 1 : 0}, ${s.userMessageId ?? null}
|
|
295
369
|
)
|
|
296
370
|
`;
|
|
297
371
|
}
|
|
298
372
|
|
|
299
|
-
|
|
300
|
-
|
|
373
|
+
recordUnattendedFailure(id: string, reason: string): void {
|
|
374
|
+
this.sql`
|
|
301
375
|
UPDATE pi_submissions
|
|
302
|
-
SET
|
|
303
|
-
assembly_descriptor = ${descriptor}
|
|
376
|
+
SET unattended_failure = COALESCE(unattended_failure, ${reason})
|
|
304
377
|
WHERE submission_id = ${id}
|
|
305
|
-
AND assembly_revision = ''
|
|
306
|
-
AND assembly_descriptor = ''
|
|
307
378
|
AND status IN ('pending', 'running')
|
|
308
|
-
RETURNING submission_id
|
|
309
379
|
`;
|
|
310
|
-
return updated.length === 1;
|
|
311
380
|
}
|
|
312
381
|
|
|
313
382
|
// 只在当前状态属于 from 时把 Submission 切换到 to,否则返回 false。
|
|
@@ -386,27 +455,6 @@ export class SubmissionRepository {
|
|
|
386
455
|
`;
|
|
387
456
|
}
|
|
388
457
|
|
|
389
|
-
/** Recover ownership from the matching Pi operation, using only its active branch. */
|
|
390
|
-
associateSessionEntries(
|
|
391
|
-
submissionId: string,
|
|
392
|
-
snapshot: Pick<LaneSnapshot, "operation" | "lastResult" | "tipId" | "transcript">,
|
|
393
|
-
): void {
|
|
394
|
-
const boundary = snapshot.operation?.id === submissionId
|
|
395
|
-
? { fromTipId: snapshot.operation.fromTipId, tipId: snapshot.tipId }
|
|
396
|
-
: snapshot.lastResult?.operationId === submissionId
|
|
397
|
-
? snapshot.lastResult
|
|
398
|
-
: undefined;
|
|
399
|
-
if (!boundary) return;
|
|
400
|
-
const start = boundary.fromTipId === null
|
|
401
|
-
? 0
|
|
402
|
-
: snapshot.transcript.findIndex(({ id }) => id === boundary.fromTipId) + 1;
|
|
403
|
-
const end = boundary.tipId === null
|
|
404
|
-
? snapshot.transcript.length
|
|
405
|
-
: snapshot.transcript.findIndex(({ id }) => id === boundary.tipId) + 1;
|
|
406
|
-
for (const entry of snapshot.transcript.slice(start, end || undefined)) {
|
|
407
|
-
this.linkSessionEntry(entry.id, submissionId);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
458
|
|
|
411
459
|
listSessionEntrySubmissionLinks(): Map<string, string> {
|
|
412
460
|
const rows = this.sql<{
|