@springbrand/agent-runtime 0.1.3-alpha.8 → 0.2.0-alpha.14
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 +4 -3
- package/src/adapter/cloudflare/index.ts +1 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +4 -0
- package/src/adapter/cloudflare/subagent/definition.ts +60 -5
- package/src/adapter/cloudflare/universal-agent/hooks.ts +23 -1
- package/src/adapter/cloudflare/universal-agent/preparation.ts +36 -2
- package/src/adapter/cloudflare/universal-agent/tools.ts +9 -12
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/db/index.ts +4 -0
- package/src/db/runtime-event-outbox.repo.ts +34 -1
- package/src/db/schema.ts +42 -0
- package/src/db/submission-admission.repo.ts +127 -0
- package/src/db/submission.repo.ts +54 -3
- package/src/index.ts +5 -2
- package/src/kernel/bindings.ts +52 -0
- package/src/kernel/durable-lifecycle.ts +100 -0
- package/src/kernel/public-contracts.ts +11 -0
- package/src/kernel/receipts.ts +1 -0
- package/src/kernel/recoverable-chat-agent.ts +0 -19
- package/src/kernel/subagent-runtime.ts +137 -0
- package/src/kernel/submission-authority.ts +114 -0
- package/src/kernel/submission-lifecycle.ts +35 -10
- package/src/layers/context/budget/gate.ts +99 -0
- package/src/lib/prompt.ts +7 -3
- package/src/pi/message/projection.ts +2 -2
- package/src/pi/runtime-adapter/assembly.ts +4 -3
- package/src/pi/runtime-adapter/execution.ts +55 -16
- package/src/pi/runtime-adapter/index.ts +34 -0
- package/src/pi/runtime-adapter/models.ts +10 -1
- package/src/pi/runtime-adapter/recovery.ts +17 -17
- package/src/pi/runtime-adapter/transcript.ts +2 -2
- package/src/pi/tool/base.ts +113 -27
- package/src/pi/tool/compiler.ts +66 -6
- package/src/pi/tool/core-host.ts +50 -23
- package/src/pi/tool/core.ts +2 -23
- package/src/pi/tool/index.ts +1 -0
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/skill.ts +78 -3
- package/src/pi/tool/subagent.ts +142 -16
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +4 -4
- package/src/pi/turn/tool-recovery.ts +7 -7
- package/src/runtime-agent-context.ts +1 -0
- package/src/runtime-agent.ts +8 -3
- package/src/runtime-assembler.ts +61 -9
- package/src/runtime-definition.ts +12 -0
- package/src/runtime.ts +485 -66
- package/src/tool-registry.ts +10 -1
- package/src/workspace-versioning.ts +46 -0
package/src/runtime.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { InteractionLifecycle } from "./kernel/interaction-lifecycle";
|
|
21
21
|
import {
|
|
22
22
|
SubmissionLifecycle,
|
|
23
|
+
MAX_PENDING_SUBMISSIONS,
|
|
23
24
|
SubmissionQueueFullError,
|
|
24
25
|
type SubmissionInput,
|
|
25
26
|
type SubmissionStore,
|
|
@@ -88,10 +89,21 @@ import { serializeOutput } from "./lib/artifacts";
|
|
|
88
89
|
import type {
|
|
89
90
|
RuntimeModelUsageEvent,
|
|
90
91
|
RuntimeToolSettlementEvent,
|
|
92
|
+
RuntimeLifecycleFact,
|
|
93
|
+
RuntimeSubagentUsageEvent,
|
|
91
94
|
} from "./kernel/bindings";
|
|
95
|
+
import { RuntimeSubagentLifecycleRecorder } from "./kernel/durable-lifecycle";
|
|
96
|
+
import {
|
|
97
|
+
decodeSubagentCompletion,
|
|
98
|
+
isZeroSubagentUsage,
|
|
99
|
+
runtimeSubagentRunMetadata,
|
|
100
|
+
subagentTerminalStatus,
|
|
101
|
+
type RuntimeSubagentRunMetadata,
|
|
102
|
+
} from "./kernel/subagent-runtime";
|
|
92
103
|
import {
|
|
93
104
|
isTerminalSubmissionStatus,
|
|
94
105
|
RuntimeDatabase,
|
|
106
|
+
type StoredSubmissionAdmission,
|
|
95
107
|
type SubmissionStatus,
|
|
96
108
|
} from "./db/index";
|
|
97
109
|
import {
|
|
@@ -114,6 +126,11 @@ import {
|
|
|
114
126
|
|
|
115
127
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
116
128
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
129
|
+
// 一次 Host admission 调用最多被视为「在途」多久。停在 pending 的准入行永久保留
|
|
130
|
+
// —— 它固定了同一 identity 重投时必须复用的 runId,不能靠删行来让容量回收 ——
|
|
131
|
+
// 但一次远端失败或崩溃留下的行超过这个窗口后就不再占用 pending 容量,否则
|
|
132
|
+
// Host 侧的一次抖动会把 Session 永久卡在「队列已满」。
|
|
133
|
+
const ADMISSION_ATTEMPT_WINDOW_MS = 60_000;
|
|
117
134
|
export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
118
135
|
// stall 单独收窄。理由不是「stall 更不值得救」,而是它的重试**期望值和别的错不一样**:
|
|
119
136
|
// 瞬时错(5xx / 断流)重跑一次往往就好了;stall 的重跑是拿同一份 transcript 让模型
|
|
@@ -129,7 +146,9 @@ type RuntimeEventOutboxPayload =
|
|
|
129
146
|
| {
|
|
130
147
|
readonly type: "tool-settlement";
|
|
131
148
|
readonly event: RuntimeToolSettlementEvent;
|
|
132
|
-
}
|
|
149
|
+
}
|
|
150
|
+
| { readonly type: "subagent-usage"; readonly event: RuntimeSubagentUsageEvent }
|
|
151
|
+
| { readonly type: "lifecycle"; readonly event: RuntimeLifecycleFact };
|
|
133
152
|
interface StoredSubmission extends SubmissionReceipt {
|
|
134
153
|
status: SubmissionStatus;
|
|
135
154
|
requestId: string;
|
|
@@ -144,6 +163,10 @@ interface StoredSubmission extends SubmissionReceipt {
|
|
|
144
163
|
regenerateMessageId?: string | null;
|
|
145
164
|
recoveryErrorCount: number;
|
|
146
165
|
recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
|
|
166
|
+
runId: string | null;
|
|
167
|
+
accountId: string | null;
|
|
168
|
+
rateVersion: number | null;
|
|
169
|
+
slotIdentity: string | null;
|
|
147
170
|
}
|
|
148
171
|
|
|
149
172
|
interface SubmitMessageOptions {
|
|
@@ -164,6 +187,11 @@ interface ActiveTurn {
|
|
|
164
187
|
agent: PreparedPiTurnAdapter;
|
|
165
188
|
}
|
|
166
189
|
|
|
190
|
+
interface PlannedContinuationData {
|
|
191
|
+
readonly submissionId: string;
|
|
192
|
+
readonly requestId: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
167
195
|
// 作用:把任意异常整理成可以持久化或发给客户端的文字。
|
|
168
196
|
// 调用:Turn 执行、恢复和业务投影捕获 `unknown` 异常时调用。
|
|
169
197
|
// 原因:错误边界不能假定抛出值一定是 `Error`。
|
|
@@ -178,6 +206,32 @@ function json(value: unknown): string {
|
|
|
178
206
|
return JSON.stringify(value);
|
|
179
207
|
}
|
|
180
208
|
|
|
209
|
+
function validateAdmissionResult(
|
|
210
|
+
result: import("./kernel/bindings").RuntimeSubmissionAdmissionResult,
|
|
211
|
+
): import("./kernel/bindings").RuntimeSubmissionAdmissionResult {
|
|
212
|
+
if (!result || typeof result !== "object" || typeof result.accepted !== "boolean") {
|
|
213
|
+
throw new Error("Runtime admission result is invalid");
|
|
214
|
+
}
|
|
215
|
+
if (!result.accepted) {
|
|
216
|
+
if (
|
|
217
|
+
typeof result.code !== "string" || !result.code.trim() ||
|
|
218
|
+
(result.retryable !== undefined && typeof result.retryable !== "boolean")
|
|
219
|
+
) {
|
|
220
|
+
throw new Error("Runtime admission rejection is invalid");
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
typeof result.accountId !== "string" || !result.accountId.trim() ||
|
|
226
|
+
typeof result.slotIdentity !== "string" || !result.slotIdentity.trim() ||
|
|
227
|
+
!Number.isSafeInteger(result.rateVersion) ||
|
|
228
|
+
result.rateVersion <= 0
|
|
229
|
+
) {
|
|
230
|
+
throw new Error("Runtime admission context is invalid");
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
181
235
|
async function closeRuntimeGatewaySession(
|
|
182
236
|
session: RuntimeGatewaySession | undefined,
|
|
183
237
|
step: string,
|
|
@@ -192,10 +246,17 @@ function assistantUsageEvent(
|
|
|
192
246
|
eventId: string,
|
|
193
247
|
submissionId: string,
|
|
194
248
|
message: AssistantMessage,
|
|
249
|
+
pinned?: Pick<StoredSubmission, "accountId" | "rateVersion" | "slotIdentity">,
|
|
195
250
|
): RuntimeModelUsageEvent {
|
|
196
251
|
return {
|
|
197
252
|
eventId,
|
|
198
253
|
submissionId,
|
|
254
|
+
runId: submissionId,
|
|
255
|
+
...(pinned?.accountId ? { accountId: pinned.accountId } : {}),
|
|
256
|
+
...(pinned?.rateVersion !== null && pinned?.rateVersion !== undefined
|
|
257
|
+
? { rateVersion: pinned.rateVersion }
|
|
258
|
+
: {}),
|
|
259
|
+
...(pinned?.slotIdentity ? { slotIdentity: pinned.slotIdentity } : {}),
|
|
199
260
|
kind: "assistant",
|
|
200
261
|
api: message.api,
|
|
201
262
|
provider: message.provider,
|
|
@@ -327,6 +388,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
327
388
|
private migratedSubmissionIds?: Set<string>;
|
|
328
389
|
private db!: RuntimeDatabase;
|
|
329
390
|
private readonly temporaryAgents = new TemporaryAgentCoordinator();
|
|
391
|
+
private readonly admissionsInFlight = new Map<
|
|
392
|
+
string,
|
|
393
|
+
Promise<import("./kernel/bindings").RuntimeSubmissionAdmissionResult>
|
|
394
|
+
>();
|
|
330
395
|
private readonly temporaryAgentApprovals =
|
|
331
396
|
new Map<string, PendingTemporaryAgentApproval>();
|
|
332
397
|
|
|
@@ -493,7 +558,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
493
558
|
status: "failed" as const,
|
|
494
559
|
error:
|
|
495
560
|
receipt.error ??
|
|
496
|
-
`
|
|
561
|
+
`SpringBrand recovery ended with status ${receipt.status}`,
|
|
497
562
|
};
|
|
498
563
|
} catch (error) {
|
|
499
564
|
if (
|
|
@@ -591,7 +656,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
591
656
|
if (contested && !parked) {
|
|
592
657
|
await closeRuntimeGatewaySession(gatewaySession, "gateway.close.contested");
|
|
593
658
|
throw new Error(
|
|
594
|
-
"Cannot reload Runtime while a revision-pinned
|
|
659
|
+
"Cannot reload Runtime while a revision-pinned SpringBrand Turn is active",
|
|
595
660
|
);
|
|
596
661
|
}
|
|
597
662
|
const repin = parked
|
|
@@ -663,6 +728,37 @@ export abstract class AgentRuntimeKernel<
|
|
|
663
728
|
} = {},
|
|
664
729
|
): PreparedPiTurnAdapter {
|
|
665
730
|
const startedAt = output.startedAt ?? Date.now();
|
|
731
|
+
const snapshot = this.assembly();
|
|
732
|
+
const runMetadata: RuntimeSubagentRunMetadata = {
|
|
733
|
+
submissionId: submission.submissionId,
|
|
734
|
+
parentRunId: submission.runId ?? submission.submissionId,
|
|
735
|
+
...(submission.accountId ? { accountId: submission.accountId } : {}),
|
|
736
|
+
...(submission.rateVersion !== null
|
|
737
|
+
? { rateVersion: submission.rateVersion }
|
|
738
|
+
: {}),
|
|
739
|
+
...(submission.slotIdentity
|
|
740
|
+
? { slotIdentity: submission.slotIdentity }
|
|
741
|
+
: {}),
|
|
742
|
+
};
|
|
743
|
+
const lifecycle = this.subagentLifecycle(runMetadata);
|
|
744
|
+
const toolExecutors = snapshot.bindings.subagents
|
|
745
|
+
? this.pi.subagentToolExecutors(
|
|
746
|
+
snapshot.bindings.subagents,
|
|
747
|
+
snapshot.profile.enabledSubagents,
|
|
748
|
+
{
|
|
749
|
+
...runMetadata,
|
|
750
|
+
onRegistered: async (runId) => {
|
|
751
|
+
await lifecycle.register(runId);
|
|
752
|
+
},
|
|
753
|
+
onUsage: async (event) => {
|
|
754
|
+
await lifecycle.usage(event);
|
|
755
|
+
},
|
|
756
|
+
onTerminal: async (terminal) => {
|
|
757
|
+
await lifecycle.terminal(terminal);
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
)
|
|
761
|
+
: undefined;
|
|
666
762
|
return this.pi.createTurn({
|
|
667
763
|
prepared: this.preparedPi(),
|
|
668
764
|
pinnedDescriptor: submission.assemblyDescriptor,
|
|
@@ -707,6 +803,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
707
803
|
settleTool: (call) =>
|
|
708
804
|
this.settleTool(submission.submissionId, call),
|
|
709
805
|
},
|
|
806
|
+
...(toolExecutors && Object.keys(toolExecutors).length > 0
|
|
807
|
+
? { toolExecutors }
|
|
808
|
+
: {}),
|
|
710
809
|
onToolTelemetry: (event) => {
|
|
711
810
|
this._emit("ua:tool" as never, { ...event });
|
|
712
811
|
},
|
|
@@ -732,6 +831,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
732
831
|
persisted.id,
|
|
733
832
|
submission.submissionId,
|
|
734
833
|
commit.message,
|
|
834
|
+
submission,
|
|
735
835
|
);
|
|
736
836
|
this.db.runtimeEvents.insert({
|
|
737
837
|
eventId: event.eventId,
|
|
@@ -774,6 +874,29 @@ export abstract class AgentRuntimeKernel<
|
|
|
774
874
|
});
|
|
775
875
|
}
|
|
776
876
|
|
|
877
|
+
private subagentLifecycle(
|
|
878
|
+
input: RuntimeSubagentRunMetadata,
|
|
879
|
+
): RuntimeSubagentLifecycleRecorder {
|
|
880
|
+
return new RuntimeSubagentLifecycleRecorder(input, {
|
|
881
|
+
save: async (event) => {
|
|
882
|
+
this.db.transaction(() => {
|
|
883
|
+
this.db.runtimeEvents.insert({
|
|
884
|
+
eventId: event.eventId,
|
|
885
|
+
body: json({
|
|
886
|
+
type: event.kind === "subagent" ? "subagent-usage" : "lifecycle",
|
|
887
|
+
event,
|
|
888
|
+
}),
|
|
889
|
+
createdAt: Date.now(),
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
await this.drainRuntimeEvents();
|
|
893
|
+
},
|
|
894
|
+
has: (eventId) => this.db.runtimeEvents.has(eventId),
|
|
895
|
+
hasSubagentUsage: (submissionId, subagentRunId) =>
|
|
896
|
+
this.db.runtimeEvents.hasSubagentUsage(submissionId, subagentRunId),
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
777
900
|
/**
|
|
778
901
|
* 在 Agent 实例启动或从休眠唤醒后恢复 Session 本地后台状态。
|
|
779
902
|
*
|
|
@@ -822,9 +945,23 @@ export abstract class AgentRuntimeKernel<
|
|
|
822
945
|
if (payload.type === "model-usage") {
|
|
823
946
|
if (!turnEvents.onModelUsage) continue;
|
|
824
947
|
await turnEvents.onModelUsage(payload.event);
|
|
825
|
-
} else {
|
|
948
|
+
} else if (payload.type === "tool-settlement") {
|
|
826
949
|
if (!turnEvents.onToolSettled) continue;
|
|
827
950
|
await turnEvents.onToolSettled(payload.event);
|
|
951
|
+
} else if (payload.type === "subagent-usage") {
|
|
952
|
+
if (!turnEvents.onSubagentUsage) continue;
|
|
953
|
+
const confirmation = await turnEvents.onSubagentUsage(payload.event);
|
|
954
|
+
if (confirmation?.confirmed !== true) {
|
|
955
|
+
failed = true;
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
if (!turnEvents.onLifecycleFact) continue;
|
|
960
|
+
const confirmation = await turnEvents.onLifecycleFact(payload.event);
|
|
961
|
+
if (confirmation?.confirmed !== true) {
|
|
962
|
+
failed = true;
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
828
965
|
}
|
|
829
966
|
this.db.transaction(() =>
|
|
830
967
|
this.db.runtimeEvents.markDelivered(row.eventId, Date.now())
|
|
@@ -835,6 +972,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
835
972
|
"[runtime-turn-event:degraded]",
|
|
836
973
|
json({ eventId: row.eventId, error: errorText(error) }),
|
|
837
974
|
);
|
|
975
|
+
break;
|
|
838
976
|
}
|
|
839
977
|
}
|
|
840
978
|
if (!failed) return;
|
|
@@ -1053,7 +1191,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1053
1191
|
private preparedPi(): PreparedPiRuntime {
|
|
1054
1192
|
if (!this.runtimePi) {
|
|
1055
1193
|
throw new Error(
|
|
1056
|
-
"
|
|
1194
|
+
"SpringBrand Runtime must be prepared before starting a Turn",
|
|
1057
1195
|
);
|
|
1058
1196
|
}
|
|
1059
1197
|
return this.runtimePi;
|
|
@@ -1255,7 +1393,23 @@ export abstract class AgentRuntimeKernel<
|
|
|
1255
1393
|
onCompactionPersisted: (event) =>
|
|
1256
1394
|
this.db.runtimeEvents.insert({
|
|
1257
1395
|
eventId: event.eventId,
|
|
1258
|
-
body: json({
|
|
1396
|
+
body: json({
|
|
1397
|
+
type: "model-usage",
|
|
1398
|
+
event: {
|
|
1399
|
+
...event,
|
|
1400
|
+
runId: submissionId,
|
|
1401
|
+
...(this.readSubmission(submissionId)?.accountId
|
|
1402
|
+
? { accountId: this.readSubmission(submissionId)!.accountId! }
|
|
1403
|
+
: {}),
|
|
1404
|
+
...(this.readSubmission(submissionId)?.rateVersion !== null &&
|
|
1405
|
+
this.readSubmission(submissionId)?.rateVersion !== undefined
|
|
1406
|
+
? { rateVersion: this.readSubmission(submissionId)!.rateVersion! }
|
|
1407
|
+
: {}),
|
|
1408
|
+
...(this.readSubmission(submissionId)?.slotIdentity
|
|
1409
|
+
? { slotIdentity: this.readSubmission(submissionId)!.slotIdentity! }
|
|
1410
|
+
: {}),
|
|
1411
|
+
},
|
|
1412
|
+
}),
|
|
1259
1413
|
createdAt: Date.now(),
|
|
1260
1414
|
}),
|
|
1261
1415
|
});
|
|
@@ -1303,31 +1457,147 @@ export abstract class AgentRuntimeKernel<
|
|
|
1303
1457
|
? { idempotencyKey: options.idempotencyKey }
|
|
1304
1458
|
: {}),
|
|
1305
1459
|
prepareAdmission: async () => {
|
|
1460
|
+
await this.ensureRuntimeReady();
|
|
1306
1461
|
const regenerateEntry =
|
|
1307
1462
|
options.regenerate && options.userMessageId
|
|
1308
1463
|
? await this.transcript.findUserMessage(options.userMessageId)
|
|
1309
1464
|
: undefined;
|
|
1465
|
+
if (
|
|
1466
|
+
options.regenerate &&
|
|
1467
|
+
(!regenerateEntry ||
|
|
1468
|
+
userContentKey(regenerateEntry) !== userContentKey(userMessage))
|
|
1469
|
+
) {
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
"Regenerate request must match an existing user message",
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
const hook = this.runtimeHooks?.onSubmissionAdmission;
|
|
1475
|
+
let existing: StoredSubmissionAdmission | null = null;
|
|
1476
|
+
if (hook) {
|
|
1477
|
+
const attemptedAt = Date.now();
|
|
1478
|
+
const reservedRunId = crypto.randomUUID();
|
|
1479
|
+
this.db.transaction(() => {
|
|
1480
|
+
const current = this.db.submissionAdmissions.find(
|
|
1481
|
+
options.requestId,
|
|
1482
|
+
options.idempotencyKey,
|
|
1483
|
+
);
|
|
1484
|
+
if (current) {
|
|
1485
|
+
// 同一 identity 重投:沿用已固定的 runId,只把这次尝试的时间推到现在,
|
|
1486
|
+
// 使它重新占住一个在途名额。
|
|
1487
|
+
if (current.status === "pending") {
|
|
1488
|
+
this.db.submissionAdmissions.recordAttempt(
|
|
1489
|
+
current.requestId,
|
|
1490
|
+
attemptedAt,
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
if (
|
|
1496
|
+
this.db.submissions.countPending() +
|
|
1497
|
+
this.db.submissionAdmissions.countPending(
|
|
1498
|
+
attemptedAt - ADMISSION_ATTEMPT_WINDOW_MS,
|
|
1499
|
+
) >=
|
|
1500
|
+
MAX_PENDING_SUBMISSIONS
|
|
1501
|
+
) {
|
|
1502
|
+
throw new SubmissionQueueFullError();
|
|
1503
|
+
}
|
|
1504
|
+
this.db.submissionAdmissions.begin(
|
|
1505
|
+
options.requestId,
|
|
1506
|
+
options.idempotencyKey ?? null,
|
|
1507
|
+
reservedRunId,
|
|
1508
|
+
attemptedAt,
|
|
1509
|
+
);
|
|
1510
|
+
});
|
|
1511
|
+
existing = this.db.submissionAdmissions.find(
|
|
1512
|
+
options.requestId,
|
|
1513
|
+
options.idempotencyKey,
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const runId = existing?.runId ?? crypto.randomUUID();
|
|
1517
|
+
const admissionRequestId = existing?.requestId ?? options.requestId;
|
|
1518
|
+
const admissionIdempotencyKey = existing
|
|
1519
|
+
? existing.idempotencyKey ?? undefined
|
|
1520
|
+
: options.idempotencyKey;
|
|
1521
|
+
const admissionKey = admissionIdempotencyKey
|
|
1522
|
+
? `idempotency:${admissionIdempotencyKey}`
|
|
1523
|
+
: `request:${admissionRequestId}`;
|
|
1524
|
+
const pendingAdmission = hook && existing?.status === "pending"
|
|
1525
|
+
? this.admissionsInFlight.get(admissionKey)
|
|
1526
|
+
: undefined;
|
|
1527
|
+
const admissionPromise = pendingAdmission ?? (hook && (!existing || existing.status === "pending")
|
|
1528
|
+
? hook({
|
|
1529
|
+
requestId: admissionRequestId,
|
|
1530
|
+
...(admissionIdempotencyKey
|
|
1531
|
+
? { idempotencyKey: admissionIdempotencyKey }
|
|
1532
|
+
: {}),
|
|
1533
|
+
runId,
|
|
1534
|
+
}).then(validateAdmissionResult).catch((error) => {
|
|
1535
|
+
this.admissionsInFlight.delete(admissionKey);
|
|
1536
|
+
throw error;
|
|
1537
|
+
})
|
|
1538
|
+
: undefined);
|
|
1539
|
+
if (hook && admissionPromise && !pendingAdmission) {
|
|
1540
|
+
this.admissionsInFlight.set(
|
|
1541
|
+
admissionKey,
|
|
1542
|
+
admissionPromise,
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
let admission = existing?.status === "accepted"
|
|
1546
|
+
? {
|
|
1547
|
+
accepted: true as const,
|
|
1548
|
+
accountId: existing.accountId!,
|
|
1549
|
+
rateVersion: existing.rateVersion!,
|
|
1550
|
+
slotIdentity: existing.slotIdentity!,
|
|
1551
|
+
}
|
|
1552
|
+
: existing?.status === "rejected"
|
|
1553
|
+
? {
|
|
1554
|
+
accepted: false as const,
|
|
1555
|
+
code: existing.code ?? "admission_rejected",
|
|
1556
|
+
retryable: existing.retryable ?? false,
|
|
1557
|
+
}
|
|
1558
|
+
: admissionPromise
|
|
1559
|
+
? await admissionPromise
|
|
1560
|
+
: undefined;
|
|
1310
1561
|
return () => {
|
|
1311
|
-
|
|
1562
|
+
if (admission && !admission.accepted) {
|
|
1563
|
+
this.db.submissionAdmissions.saveRejected(
|
|
1564
|
+
admissionRequestId,
|
|
1565
|
+
runId,
|
|
1566
|
+
admission.code,
|
|
1567
|
+
admission.retryable ?? false,
|
|
1568
|
+
);
|
|
1569
|
+
const rejected = this.insertSkippedSubmission({
|
|
1570
|
+
submissionId: runId,
|
|
1571
|
+
requestId: admissionRequestId,
|
|
1572
|
+
idempotencyKey: admissionIdempotencyKey ?? null,
|
|
1573
|
+
createdAt: userMessage.timestamp,
|
|
1574
|
+
code: admission.code,
|
|
1575
|
+
retryable: admission.retryable ?? false,
|
|
1576
|
+
});
|
|
1577
|
+
this.admissionsInFlight.delete(admissionKey);
|
|
1578
|
+
return {
|
|
1579
|
+
...rejected,
|
|
1580
|
+
admissionRejected: true,
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
const submissionId = runId;
|
|
1312
1584
|
const assistantMessageId = crypto.randomUUID();
|
|
1313
1585
|
const createdAt = userMessage.timestamp;
|
|
1314
1586
|
const userMessageId =
|
|
1315
1587
|
options.userMessageId ?? crypto.randomUUID();
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
);
|
|
1325
|
-
}
|
|
1588
|
+
if (admission?.accepted) {
|
|
1589
|
+
this.db.submissionAdmissions.saveAccepted(
|
|
1590
|
+
admissionRequestId,
|
|
1591
|
+
runId,
|
|
1592
|
+
admission.accountId,
|
|
1593
|
+
admission.rateVersion,
|
|
1594
|
+
admission.slotIdentity,
|
|
1595
|
+
);
|
|
1326
1596
|
}
|
|
1327
1597
|
this.db.submissions.insert({
|
|
1328
1598
|
submissionId,
|
|
1329
|
-
requestId:
|
|
1330
|
-
idempotencyKey:
|
|
1599
|
+
requestId: admissionRequestId,
|
|
1600
|
+
idempotencyKey: admissionIdempotencyKey ?? null,
|
|
1331
1601
|
createdAt,
|
|
1332
1602
|
assemblyRevision: "",
|
|
1333
1603
|
assemblyDescriptor: "",
|
|
@@ -1340,7 +1610,28 @@ export abstract class AgentRuntimeKernel<
|
|
|
1340
1610
|
regenerateMessageId: options.regenerate
|
|
1341
1611
|
? options.userMessageId ?? null
|
|
1342
1612
|
: null,
|
|
1613
|
+
runId,
|
|
1614
|
+
accountId: admission?.accepted ? admission.accountId : null,
|
|
1615
|
+
rateVersion: admission?.accepted ? admission.rateVersion : null,
|
|
1616
|
+
slotIdentity: admission?.accepted ? admission.slotIdentity : null,
|
|
1343
1617
|
});
|
|
1618
|
+
if (admission?.accepted) {
|
|
1619
|
+
const fact: RuntimeLifecycleFact = {
|
|
1620
|
+
kind: "submission-admitted",
|
|
1621
|
+
eventId: `${submissionId}:admitted`,
|
|
1622
|
+
submissionId,
|
|
1623
|
+
runId: submissionId,
|
|
1624
|
+
accountId: admission.accountId,
|
|
1625
|
+
rateVersion: admission.rateVersion,
|
|
1626
|
+
slotIdentity: admission.slotIdentity,
|
|
1627
|
+
};
|
|
1628
|
+
this.db.runtimeEvents.insert({
|
|
1629
|
+
eventId: fact.eventId,
|
|
1630
|
+
body: json({ type: "lifecycle", event: fact }),
|
|
1631
|
+
createdAt,
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (hook) this.admissionsInFlight.delete(admissionKey);
|
|
1344
1635
|
return this.readSubmission(submissionId)!;
|
|
1345
1636
|
};
|
|
1346
1637
|
},
|
|
@@ -1364,6 +1655,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1364
1655
|
const submitted = await this.submissions.submit(
|
|
1365
1656
|
this.submissionInput(userMessage, options),
|
|
1366
1657
|
);
|
|
1658
|
+
await this.drainRuntimeEvents();
|
|
1367
1659
|
await this.broadcastApprovals();
|
|
1368
1660
|
return submitted;
|
|
1369
1661
|
}
|
|
@@ -1380,7 +1672,10 @@ export abstract class AgentRuntimeKernel<
|
|
|
1380
1672
|
this.submissionInput(userMessage, options),
|
|
1381
1673
|
timeoutMs,
|
|
1382
1674
|
);
|
|
1383
|
-
if (submitted)
|
|
1675
|
+
if (submitted) {
|
|
1676
|
+
await this.drainRuntimeEvents();
|
|
1677
|
+
await this.broadcastApprovals();
|
|
1678
|
+
}
|
|
1384
1679
|
return submitted;
|
|
1385
1680
|
}
|
|
1386
1681
|
|
|
@@ -1405,11 +1700,51 @@ export abstract class AgentRuntimeKernel<
|
|
|
1405
1700
|
);
|
|
1406
1701
|
const submission = this.readSubmission(submissionId);
|
|
1407
1702
|
if (!submission) {
|
|
1408
|
-
throw new Error(`Unknown
|
|
1703
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
1409
1704
|
}
|
|
1410
1705
|
return submission;
|
|
1411
1706
|
}
|
|
1412
1707
|
|
|
1708
|
+
private insertSubmissionTerminalFact(submission: StoredSubmission): void {
|
|
1709
|
+
const terminalFact: RuntimeLifecycleFact = {
|
|
1710
|
+
kind: "submission-terminal",
|
|
1711
|
+
eventId: `${submission.submissionId}:terminal`,
|
|
1712
|
+
submissionId: submission.submissionId,
|
|
1713
|
+
runId: submission.runId ?? submission.submissionId,
|
|
1714
|
+
status: submission.status as "completed" | "aborted" | "skipped" | "error",
|
|
1715
|
+
childRegistrationClosed: true,
|
|
1716
|
+
zeroUsage: !this.db.runtimeEvents.hasNonZeroModelUsage(
|
|
1717
|
+
submission.submissionId,
|
|
1718
|
+
),
|
|
1719
|
+
...(submission.accountId ? { accountId: submission.accountId } : {}),
|
|
1720
|
+
...(submission.rateVersion !== null
|
|
1721
|
+
? { rateVersion: submission.rateVersion }
|
|
1722
|
+
: {}),
|
|
1723
|
+
...(submission.slotIdentity
|
|
1724
|
+
? { slotIdentity: submission.slotIdentity }
|
|
1725
|
+
: {}),
|
|
1726
|
+
};
|
|
1727
|
+
this.db.runtimeEvents.insert({
|
|
1728
|
+
eventId: terminalFact.eventId,
|
|
1729
|
+
body: json({ type: "lifecycle", event: terminalFact }),
|
|
1730
|
+
createdAt: submission.completedAt ?? Date.now(),
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
private insertSkippedSubmission(input: {
|
|
1735
|
+
submissionId: string;
|
|
1736
|
+
requestId: string;
|
|
1737
|
+
idempotencyKey: string | null;
|
|
1738
|
+
createdAt: number;
|
|
1739
|
+
code: string;
|
|
1740
|
+
retryable: boolean;
|
|
1741
|
+
}): StoredSubmission {
|
|
1742
|
+
this.db.submissions.insertRejected(input);
|
|
1743
|
+
const skipped = this.readSubmission(input.submissionId)!;
|
|
1744
|
+
this.insertSubmissionTerminalFact(skipped);
|
|
1745
|
+
return skipped;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1413
1748
|
// 作用:读取某个 Submission 已持久的全部 Pi 恢复里程碑。
|
|
1414
1749
|
// 调用:恢复决策器每次检查或应用命令前调用。
|
|
1415
1750
|
// 原因:决策只依赖持久事实,重启后才会得到同样结果。
|
|
@@ -1560,7 +1895,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1560
1895
|
): void {
|
|
1561
1896
|
const submission = this.readSubmission(submissionId);
|
|
1562
1897
|
if (!submission) {
|
|
1563
|
-
throw new Error(`Unknown
|
|
1898
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
1564
1899
|
}
|
|
1565
1900
|
const existing = this.db.settlements.find(submissionId, call.toolCallId);
|
|
1566
1901
|
const args = json(call.args);
|
|
@@ -1708,11 +2043,28 @@ export abstract class AgentRuntimeKernel<
|
|
|
1708
2043
|
const latest = this.readSubmission(submission.submissionId);
|
|
1709
2044
|
if (!latest) {
|
|
1710
2045
|
throw new Error(
|
|
1711
|
-
`Unknown
|
|
2046
|
+
`Unknown SpringBrand submission: ${submission.submissionId}`,
|
|
1712
2047
|
);
|
|
1713
2048
|
}
|
|
1714
2049
|
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
1715
2050
|
|
|
2051
|
+
for (const steer of this.db.steers.listForSubmission(latest.submissionId)) {
|
|
2052
|
+
const userMessage = JSON.parse(steer.canonicalJson) as PiCanonicalUserInput;
|
|
2053
|
+
const uiMessage = steer.uiMessageJson
|
|
2054
|
+
? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
|
|
2055
|
+
: undefined;
|
|
2056
|
+
const submitted = await this.submitMessage(userMessage, {
|
|
2057
|
+
requestId: steer.messageId,
|
|
2058
|
+
idempotencyKey: steer.messageId,
|
|
2059
|
+
userMessageId: steer.messageId,
|
|
2060
|
+
...(uiMessage ? { userMessage: uiMessage } : {}),
|
|
2061
|
+
});
|
|
2062
|
+
this.ctx.waitUntil(submitted.completion);
|
|
2063
|
+
this.db.transaction(() =>
|
|
2064
|
+
this.db.steers.deleteByMessageId(steer.messageId)
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1716
2068
|
const durableIntent = this.decidePiRecovery(latest).terminal;
|
|
1717
2069
|
const effectiveOutcome = latest.abortReason
|
|
1718
2070
|
? "aborted"
|
|
@@ -1742,38 +2094,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
1742
2094
|
: effectiveOutcome === "aborted"
|
|
1743
2095
|
? "aborted"
|
|
1744
2096
|
: "error";
|
|
1745
|
-
for (const steer of this.db.steers.listForSubmission(
|
|
1746
|
-
latest.submissionId,
|
|
1747
|
-
)) {
|
|
1748
|
-
if (!this.db.submissions.findByRequestId(steer.messageId)) {
|
|
1749
|
-
this.db.submissions.insert({
|
|
1750
|
-
submissionId: steer.steerId,
|
|
1751
|
-
requestId: steer.messageId,
|
|
1752
|
-
idempotencyKey: steer.messageId,
|
|
1753
|
-
createdAt: steer.createdAt,
|
|
1754
|
-
assemblyRevision: latest.assemblyRevision,
|
|
1755
|
-
assemblyDescriptor: latest.assemblyDescriptor,
|
|
1756
|
-
assistantMessageId: crypto.randomUUID(),
|
|
1757
|
-
queuedInputJson: steer.canonicalJson,
|
|
1758
|
-
queuedUiMessageJson: steer.uiMessageJson,
|
|
1759
|
-
userMessageId: steer.messageId,
|
|
1760
|
-
regenerateMessageId: null,
|
|
1761
|
-
});
|
|
1762
|
-
}
|
|
1763
|
-
this.db.steers.deleteByMessageId(steer.messageId);
|
|
1764
|
-
}
|
|
1765
2097
|
const terminal = this.updateSubmission(
|
|
1766
2098
|
latest.submissionId,
|
|
1767
2099
|
status,
|
|
1768
2100
|
effectiveMessage,
|
|
1769
2101
|
);
|
|
2102
|
+
this.insertSubmissionTerminalFact(terminal);
|
|
1770
2103
|
if (
|
|
1771
2104
|
effectiveOutcome === "aborted" &&
|
|
1772
2105
|
effectiveMessage === USER_STOP_REASON
|
|
1773
2106
|
) {
|
|
1774
2107
|
if (terminal.completedAt == null) {
|
|
1775
2108
|
throw new Error(
|
|
1776
|
-
`Aborted
|
|
2109
|
+
`Aborted SpringBrand submission has no completion time: ${latest.submissionId}`,
|
|
1777
2110
|
);
|
|
1778
2111
|
}
|
|
1779
2112
|
this.transcript.appendTurnAbortedMarker(
|
|
@@ -1896,7 +2229,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1896
2229
|
}
|
|
1897
2230
|
}
|
|
1898
2231
|
}
|
|
1899
|
-
throw new Error("
|
|
2232
|
+
throw new Error("SpringBrand recovery plan did not converge");
|
|
1900
2233
|
}
|
|
1901
2234
|
|
|
1902
2235
|
// #endregion
|
|
@@ -1925,7 +2258,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1925
2258
|
typeof userMessage.timestamp !== "number" ||
|
|
1926
2259
|
!submission.userMessageId
|
|
1927
2260
|
) {
|
|
1928
|
-
throw new Error("Queued
|
|
2261
|
+
throw new Error("Queued SpringBrand user message is invalid");
|
|
1929
2262
|
}
|
|
1930
2263
|
const uiMessage = submission.queuedUiMessageJson
|
|
1931
2264
|
? JSON.parse(submission.queuedUiMessageJson) as UIMessage & {
|
|
@@ -1954,7 +2287,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1954
2287
|
if (!latest || latest.status !== "pending") {
|
|
1955
2288
|
if (!latest) {
|
|
1956
2289
|
throw new Error(
|
|
1957
|
-
`Unknown
|
|
2290
|
+
`Unknown SpringBrand submission: ${submission.submissionId}`,
|
|
1958
2291
|
);
|
|
1959
2292
|
}
|
|
1960
2293
|
return latest;
|
|
@@ -1962,7 +2295,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1962
2295
|
const running = this.db.submissions.findRunning();
|
|
1963
2296
|
if (running) {
|
|
1964
2297
|
throw new Error(
|
|
1965
|
-
`
|
|
2298
|
+
`SpringBrand submission ${running.submissionId} is already running`,
|
|
1966
2299
|
);
|
|
1967
2300
|
}
|
|
1968
2301
|
if (submission.regenerateMessageId) {
|
|
@@ -1979,7 +2312,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1979
2312
|
);
|
|
1980
2313
|
if (!appended) {
|
|
1981
2314
|
throw new Error(
|
|
1982
|
-
`
|
|
2315
|
+
`SpringBrand Session message ${submission.userMessageId} already exists`,
|
|
1983
2316
|
);
|
|
1984
2317
|
}
|
|
1985
2318
|
}
|
|
@@ -1989,7 +2322,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
1989
2322
|
["pending"],
|
|
1990
2323
|
)) {
|
|
1991
2324
|
throw new Error(
|
|
1992
|
-
`
|
|
2325
|
+
`SpringBrand submission ${submission.submissionId} could not activate`,
|
|
1993
2326
|
);
|
|
1994
2327
|
}
|
|
1995
2328
|
this.db.submissions.clearQueuedPayload(
|
|
@@ -2014,7 +2347,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2014
2347
|
): Promise<StoredSubmission> {
|
|
2015
2348
|
const submission = this.readSubmission(submissionId);
|
|
2016
2349
|
if (!submission) {
|
|
2017
|
-
throw new Error(`Unknown
|
|
2350
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
2018
2351
|
}
|
|
2019
2352
|
if (isTerminalSubmissionStatus(submission.status)) {
|
|
2020
2353
|
return submission;
|
|
@@ -2057,7 +2390,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2057
2390
|
this.sendChatTerminal(
|
|
2058
2391
|
submission.requestId,
|
|
2059
2392
|
failed.status === "error"
|
|
2060
|
-
? failed.error ?? "
|
|
2393
|
+
? failed.error ?? "SpringBrand turn failed"
|
|
2061
2394
|
: undefined,
|
|
2062
2395
|
recovery,
|
|
2063
2396
|
);
|
|
@@ -2127,7 +2460,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2127
2460
|
|
|
2128
2461
|
const ready = this.readSubmission(submissionId);
|
|
2129
2462
|
if (!ready) {
|
|
2130
|
-
throw new Error(`Unknown
|
|
2463
|
+
throw new Error(`Unknown SpringBrand submission: ${submissionId}`);
|
|
2131
2464
|
}
|
|
2132
2465
|
if (isTerminalSubmissionStatus(ready.status)) return ready;
|
|
2133
2466
|
if (ready.abortReason) {
|
|
@@ -2189,6 +2522,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2189
2522
|
agent: adapter,
|
|
2190
2523
|
};
|
|
2191
2524
|
let streamId: string | undefined;
|
|
2525
|
+
let runResult: import("./pi/runtime-adapter").PiTurnRunResult | undefined;
|
|
2192
2526
|
|
|
2193
2527
|
const deactivate = this.submissions.activate(submission, turn);
|
|
2194
2528
|
await this.broadcastApprovals();
|
|
@@ -2208,7 +2542,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2208
2542
|
streamId = activeStreamId;
|
|
2209
2543
|
this.streamBySubmission.set(submissionId, streamId);
|
|
2210
2544
|
try {
|
|
2211
|
-
await adapter.run({});
|
|
2545
|
+
runResult = await adapter.run({});
|
|
2212
2546
|
} finally {
|
|
2213
2547
|
this.streamBySubmission.delete(submissionId);
|
|
2214
2548
|
}
|
|
@@ -2220,10 +2554,27 @@ export abstract class AgentRuntimeKernel<
|
|
|
2220
2554
|
if (streamId) this.failRecoverableStream(streamId);
|
|
2221
2555
|
return this.readSubmission(submissionId)!;
|
|
2222
2556
|
}
|
|
2557
|
+
if (runResult?.kind === "yielded") {
|
|
2558
|
+
const latest = this.readSubmission(submissionId)!;
|
|
2559
|
+
if (latest.abortReason) {
|
|
2560
|
+
return this.submissions.finish(
|
|
2561
|
+
latest,
|
|
2562
|
+
"aborted",
|
|
2563
|
+
latest.abortReason,
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
await this.schedule(
|
|
2567
|
+
0,
|
|
2568
|
+
"_piPlannedContinuation",
|
|
2569
|
+
{ submissionId, requestId: submission.requestId },
|
|
2570
|
+
{ idempotent: true },
|
|
2571
|
+
);
|
|
2572
|
+
return latest;
|
|
2573
|
+
}
|
|
2223
2574
|
const intent = terminalIntent ?? {
|
|
2224
2575
|
outcome: "failed" as const,
|
|
2225
2576
|
message:
|
|
2226
|
-
"
|
|
2577
|
+
"SpringBrand turn ended without an authoritative assistant message",
|
|
2227
2578
|
};
|
|
2228
2579
|
const terminal = await this.submissions.finish(
|
|
2229
2580
|
submission,
|
|
@@ -2385,7 +2736,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2385
2736
|
this.sendChatTerminal(
|
|
2386
2737
|
turn.requestId,
|
|
2387
2738
|
submission.status === "error"
|
|
2388
|
-
? submission.error ?? "
|
|
2739
|
+
? submission.error ?? "SpringBrand turn failed"
|
|
2389
2740
|
: undefined,
|
|
2390
2741
|
turn.continuation,
|
|
2391
2742
|
);
|
|
@@ -3048,7 +3399,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
3048
3399
|
const reason = "Moved to active Turn";
|
|
3049
3400
|
this.appendTerminalIntent(latest, "aborted", reason);
|
|
3050
3401
|
this.db.submissions.updateAbortReason(submissionId, reason);
|
|
3051
|
-
this.
|
|
3402
|
+
this.insertSubmissionTerminalFact(
|
|
3403
|
+
this.updateSubmission(submissionId, "aborted", reason),
|
|
3404
|
+
);
|
|
3052
3405
|
return true;
|
|
3053
3406
|
});
|
|
3054
3407
|
if (!moved) {
|
|
@@ -3064,6 +3417,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
3064
3417
|
active.agent.steer(message, queued.userMessageId);
|
|
3065
3418
|
}
|
|
3066
3419
|
} finally {
|
|
3420
|
+
await this.drainRuntimeEvents();
|
|
3067
3421
|
await this.broadcastApprovals();
|
|
3068
3422
|
}
|
|
3069
3423
|
return {
|
|
@@ -3101,13 +3455,22 @@ export abstract class AgentRuntimeKernel<
|
|
|
3101
3455
|
SCHEDULED_STABLE_TIMEOUT_MS,
|
|
3102
3456
|
);
|
|
3103
3457
|
if (!submitted) {
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3458
|
+
const skipped = this.db.transaction(() => {
|
|
3459
|
+
const duplicate = options?.idempotencyKey
|
|
3460
|
+
? this.db.submissions.findByIdempotencyKey(options.idempotencyKey)
|
|
3461
|
+
: this.db.submissions.findByRequestId(requestId);
|
|
3462
|
+
return duplicate ?? this.insertSkippedSubmission({
|
|
3463
|
+
submissionId: requestId,
|
|
3464
|
+
requestId,
|
|
3465
|
+
idempotencyKey: options?.idempotencyKey ?? null,
|
|
3466
|
+
createdAt: Date.now(),
|
|
3467
|
+
code: "Runtime not stable within timeout",
|
|
3468
|
+
retryable: true,
|
|
3469
|
+
});
|
|
3470
|
+
}) as StoredSubmission;
|
|
3471
|
+
await this.drainRuntimeEvents();
|
|
3472
|
+
await this.broadcastApprovals();
|
|
3473
|
+
return skipped;
|
|
3111
3474
|
}
|
|
3112
3475
|
this.ctx.waitUntil(submitted.completion.then(() => undefined));
|
|
3113
3476
|
return submitted.receipt;
|
|
@@ -3222,9 +3585,9 @@ export abstract class AgentRuntimeKernel<
|
|
|
3222
3585
|
const adapter = this.createSubmissionExecutionAdapter(submission);
|
|
3223
3586
|
const spec = adapter.interactionSpec(pending.toolName);
|
|
3224
3587
|
if (!spec) return { ok: false };
|
|
3225
|
-
if (!spec.validateResponse(response)) return { ok: false };
|
|
3226
3588
|
|
|
3227
3589
|
const input = safeParseJson(pending.inputJson);
|
|
3590
|
+
if (!spec.validateResponse(input, response)) return { ok: false };
|
|
3228
3591
|
const settled = spec.settle
|
|
3229
3592
|
? spec.settle(input, response)
|
|
3230
3593
|
: defaultInteractionResult(response);
|
|
@@ -3347,11 +3710,30 @@ export abstract class AgentRuntimeKernel<
|
|
|
3347
3710
|
}
|
|
3348
3711
|
}
|
|
3349
3712
|
|
|
3713
|
+
async _piPlannedContinuation(
|
|
3714
|
+
data?: PlannedContinuationData,
|
|
3715
|
+
): Promise<void> {
|
|
3716
|
+
if (!data?.submissionId || !data.requestId) return;
|
|
3717
|
+
const submission = this.readSubmission(data.submissionId);
|
|
3718
|
+
if (
|
|
3719
|
+
!submission ||
|
|
3720
|
+
submission.requestId !== data.requestId ||
|
|
3721
|
+
isTerminalSubmissionStatus(submission.status)
|
|
3722
|
+
) {
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
await this.submissions.recoverAfterCurrent(submission.submissionId);
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3350
3728
|
// 作用:Agent Tool 子运行开始后重算一次活动投影。
|
|
3351
3729
|
// 调用:Agents SDK 在登记子运行后调用。
|
|
3352
3730
|
// 原因:分离的子运行不进 Submission 表,不在这里重算,Host 的列表会显示成已经空闲。
|
|
3353
3731
|
override async onAgentToolStart(run: AgentToolRunInfo): Promise<void> {
|
|
3354
3732
|
await super.onAgentToolStart(run);
|
|
3733
|
+
const metadata = runtimeSubagentRunMetadata(run.inputPreview);
|
|
3734
|
+
if (metadata) {
|
|
3735
|
+
await this.subagentLifecycle(metadata).register(run.runId);
|
|
3736
|
+
}
|
|
3355
3737
|
await this.broadcastApprovals();
|
|
3356
3738
|
}
|
|
3357
3739
|
|
|
@@ -3363,6 +3745,40 @@ export abstract class AgentRuntimeKernel<
|
|
|
3363
3745
|
result: AgentToolLifecycleResult,
|
|
3364
3746
|
): Promise<void> {
|
|
3365
3747
|
await super.onAgentToolFinish(run, result);
|
|
3748
|
+
const metadata = runtimeSubagentRunMetadata(run.inputPreview);
|
|
3749
|
+
if (metadata) {
|
|
3750
|
+
const lifecycle = this.subagentLifecycle(metadata);
|
|
3751
|
+
await lifecycle.register(run.runId);
|
|
3752
|
+
if (!(result.status === "interrupted" && result.childStillRunning)) {
|
|
3753
|
+
const completion = decodeSubagentCompletion(result.summary);
|
|
3754
|
+
const hasUsage = completion && !isZeroSubagentUsage(completion.usage);
|
|
3755
|
+
const status = subagentTerminalStatus(result.status);
|
|
3756
|
+
if (completion && hasUsage) {
|
|
3757
|
+
await lifecycle.usage({
|
|
3758
|
+
eventId: `${run.runId}:usage`,
|
|
3759
|
+
submissionId: metadata.submissionId,
|
|
3760
|
+
runId: run.runId,
|
|
3761
|
+
subagentRunId: run.runId,
|
|
3762
|
+
parentRunId: metadata.parentRunId,
|
|
3763
|
+
...(metadata.accountId ? { accountId: metadata.accountId } : {}),
|
|
3764
|
+
...(metadata.rateVersion !== undefined
|
|
3765
|
+
? { rateVersion: metadata.rateVersion }
|
|
3766
|
+
: {}),
|
|
3767
|
+
...(metadata.slotIdentity
|
|
3768
|
+
? { slotIdentity: metadata.slotIdentity }
|
|
3769
|
+
: {}),
|
|
3770
|
+
kind: "subagent",
|
|
3771
|
+
status,
|
|
3772
|
+
usage: completion.usage,
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
await lifecycle.terminal({
|
|
3776
|
+
subagentRunId: run.runId,
|
|
3777
|
+
status,
|
|
3778
|
+
zeroUsage: !hasUsage,
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3366
3782
|
await this.broadcastApprovals();
|
|
3367
3783
|
}
|
|
3368
3784
|
|
|
@@ -3370,8 +3786,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
3370
3786
|
run: AgentToolRunInfo,
|
|
3371
3787
|
result: AgentToolLifecycleResult,
|
|
3372
3788
|
): Promise<void> {
|
|
3789
|
+
const completion = decodeSubagentCompletion(result.summary);
|
|
3373
3790
|
const outcome = result.status === "completed"
|
|
3374
|
-
?
|
|
3791
|
+
? completion
|
|
3792
|
+
? JSON.stringify(completion.output)
|
|
3793
|
+
: result.summary ?? "Completed without a result."
|
|
3375
3794
|
: result.error ?? `Background run ${result.status}.`;
|
|
3376
3795
|
await this.submitPrompt(
|
|
3377
3796
|
[
|