@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/db/schema.ts +10 -1
- package/src/db/submission.repo.ts +34 -1
- package/src/index.ts +1 -0
- package/src/kernel/bindings.ts +1 -3
- package/src/kernel/recoverable-chat-agent.ts +82 -4
- package/src/kernel/state.ts +4 -0
- package/src/kernel/submission-lifecycle.ts +3 -2
- package/src/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/snapshot.ts +5 -2
- package/src/pi/runtime-adapter/assembly.ts +9 -20
- package/src/pi/runtime-adapter/execution.ts +89 -6
- package/src/pi/runtime-adapter/index.ts +9 -3
- package/src/pi/runtime-adapter/models.ts +373 -35
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +112 -2
- package/src/pi/tool/core-host.ts +19 -24
- package/src/pi/tool/core.ts +18 -118
- package/src/pi/tool/skill.ts +112 -420
- package/src/pi/tool/web-fetch.ts +282 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/workspace-sandbox.ts +92 -258
- package/src/plugins.ts +79 -42
- package/src/runtime.ts +201 -40
package/src/runtime.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Connection } from "agents";
|
|
2
2
|
import {
|
|
3
|
+
ChatStreamStalledError,
|
|
3
4
|
MessageType,
|
|
4
5
|
clearChatTerminal,
|
|
5
6
|
parseProtocolMessage,
|
|
6
7
|
recordChatTerminal,
|
|
7
8
|
sendIfOpen,
|
|
9
|
+
type ChatRecoveryConfig,
|
|
8
10
|
} from "agents/chat";
|
|
9
11
|
import { RecoverableChatAgent } from "./kernel/recoverable-chat-agent";
|
|
10
12
|
import {
|
|
@@ -46,6 +48,9 @@ import { connectConfiguredMcpServers } from "./lib/mcp";
|
|
|
46
48
|
import { installConsoleSink } from "./lib/telemetry-dev";
|
|
47
49
|
import {
|
|
48
50
|
PiRuntimeAdapter,
|
|
51
|
+
MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
52
|
+
readModelStreamStallDetails,
|
|
53
|
+
RetryableModelError,
|
|
49
54
|
type PiCanonicalUserInput,
|
|
50
55
|
type PiCanonicalTranscriptSnapshot,
|
|
51
56
|
type PiChatRecoveryData,
|
|
@@ -91,6 +96,15 @@ import {
|
|
|
91
96
|
|
|
92
97
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
93
98
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
99
|
+
export const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
100
|
+
// stall 单独收窄。理由不是「stall 更不值得救」,而是它的重试**期望值和别的错不一样**:
|
|
101
|
+
// 瞬时错(5xx / 断流)重跑一次往往就好了;stall 的重跑是拿同一份 transcript 让模型
|
|
102
|
+
// 重新想同样久,如果它本来就超预算,再跑几次也一样超。把看门狗放宽到 240s 之后,
|
|
103
|
+
// 真该救的那一类已经在第一次就跑完了,剩下还在 stall 的基本是连接真死——
|
|
104
|
+
// 那种情况下 5 次 × 240s ≈ 20 分钟的空转纯属折磨用户。3 次约 12 分钟封顶。
|
|
105
|
+
export const CHAT_STALL_MAX_ATTEMPTS = 3;
|
|
106
|
+
const CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
107
|
+
"多次恢复仍未成功,本次生成已停止,当前进度已保留。请发送新消息继续。";
|
|
94
108
|
|
|
95
109
|
type RuntimeEventOutboxPayload =
|
|
96
110
|
| { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
|
|
@@ -110,6 +124,8 @@ interface StoredSubmission extends SubmissionReceipt {
|
|
|
110
124
|
queuedUiMessageJson?: string | null;
|
|
111
125
|
userMessageId?: string | null;
|
|
112
126
|
regenerateMessageId?: string | null;
|
|
127
|
+
recoveryErrorCount: number;
|
|
128
|
+
recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
|
|
113
129
|
}
|
|
114
130
|
|
|
115
131
|
interface SubmitMessageOptions {
|
|
@@ -210,6 +226,11 @@ interface PendingTemporaryAgentApproval {
|
|
|
210
226
|
export abstract class AgentRuntimeKernel<
|
|
211
227
|
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
212
228
|
> extends RecoverableChatAgent<Env, RuntimeState, PiChatRecoveryData> {
|
|
229
|
+
override chatRecovery: ChatRecoveryConfig = {
|
|
230
|
+
maxAttempts: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
231
|
+
terminalMessage: CHAT_RECOVERY_TERMINAL_MESSAGE,
|
|
232
|
+
};
|
|
233
|
+
|
|
213
234
|
initialState: RuntimeState = {
|
|
214
235
|
runtimeLoad: { status: "idle", available: false },
|
|
215
236
|
};
|
|
@@ -352,15 +373,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
352
373
|
};
|
|
353
374
|
},
|
|
354
375
|
retryTurn: async (submissionId) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
376
|
+
try {
|
|
377
|
+
const receipt = await this.submissions.recover(submissionId);
|
|
378
|
+
return receipt.status === "completed"
|
|
379
|
+
? { status: "completed" as const }
|
|
380
|
+
: {
|
|
381
|
+
status: "failed" as const,
|
|
382
|
+
error:
|
|
383
|
+
receipt.error ??
|
|
384
|
+
`Pi recovery ended with status ${receipt.status}`,
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (
|
|
388
|
+
error instanceof ChatStreamStalledError ||
|
|
389
|
+
error instanceof RetryableModelError
|
|
390
|
+
) {
|
|
391
|
+
return { status: "scheduled" as const };
|
|
392
|
+
}
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
364
395
|
},
|
|
365
396
|
failTurn: async (submissionId, message) => {
|
|
366
397
|
const submission = this.readSubmission(submissionId);
|
|
@@ -450,7 +481,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
450
481
|
const startedAt = output.startedAt ?? Date.now();
|
|
451
482
|
return this.pi.createTurn({
|
|
452
483
|
prepared: this.preparedPi(),
|
|
453
|
-
baseRevision: this.baseRevision(),
|
|
454
484
|
pinnedDescriptor: submission.assemblyDescriptor,
|
|
455
485
|
submission: {
|
|
456
486
|
id: submission.submissionId,
|
|
@@ -833,16 +863,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
833
863
|
.join("");
|
|
834
864
|
}
|
|
835
865
|
|
|
836
|
-
// 作用:返回当前已激活配置的 revision。
|
|
837
|
-
// 调用:创建 Pi Turn 适配器时调用。
|
|
838
|
-
// 原因:把初始化不变式收口在一处,避免执行层带着空 revision 继续。
|
|
839
|
-
private baseRevision(): string {
|
|
840
|
-
if (!this.runtimeRevision) {
|
|
841
|
-
throw new Error("Runtime revision is not initialized");
|
|
842
|
-
}
|
|
843
|
-
return this.runtimeRevision;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
866
|
// 作用:返回当前已准备好的 Pi Runtime。
|
|
847
867
|
// 调用:创建 Turn 适配器或准入固定装配时调用。
|
|
848
868
|
// 原因:明确区分“已有 Snapshot”和“Pi 已完成准备”,防止半初始化状态进入 Turn。
|
|
@@ -922,7 +942,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
922
942
|
await this.hash(submission.assemblyDescriptor)
|
|
923
943
|
) {
|
|
924
944
|
throw new Error(
|
|
925
|
-
"Pinned Runtime
|
|
945
|
+
"Pinned Runtime assembly descriptor is invalid",
|
|
926
946
|
);
|
|
927
947
|
}
|
|
928
948
|
}
|
|
@@ -1436,6 +1456,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
1436
1456
|
return terminal;
|
|
1437
1457
|
});
|
|
1438
1458
|
await this.drainRuntimeEvents();
|
|
1459
|
+
try {
|
|
1460
|
+
await this.settleChatRecovery(
|
|
1461
|
+
latest.requestId,
|
|
1462
|
+
effectiveOutcome === "succeeded"
|
|
1463
|
+
? "completed"
|
|
1464
|
+
: effectiveOutcome === "aborted"
|
|
1465
|
+
? "skipped"
|
|
1466
|
+
: "failed",
|
|
1467
|
+
effectiveMessage,
|
|
1468
|
+
);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
console.error("[chat-recovery] terminal settlement failed", error);
|
|
1471
|
+
}
|
|
1439
1472
|
let inactiveActivity: RuntimeActivity = "idle";
|
|
1440
1473
|
if (terminal.status === "completed") {
|
|
1441
1474
|
try {
|
|
@@ -1479,6 +1512,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
1479
1512
|
): Promise<boolean> {
|
|
1480
1513
|
let decision = await this.materializeRecoveredToolResults(submission);
|
|
1481
1514
|
for (let step = 0; step < 32; step += 1) {
|
|
1515
|
+
const latest = this.readSubmission(submission.submissionId);
|
|
1516
|
+
if (!latest || isTerminalSubmissionStatus(latest.status)) return false;
|
|
1517
|
+
if (latest.abortReason) {
|
|
1518
|
+
await this.submissions.finish(
|
|
1519
|
+
latest,
|
|
1520
|
+
"aborted",
|
|
1521
|
+
latest.abortReason,
|
|
1522
|
+
);
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1482
1525
|
this.db.transaction(() => {
|
|
1483
1526
|
this.applyPiRecoveryMutations(
|
|
1484
1527
|
submission,
|
|
@@ -1656,13 +1699,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
1656
1699
|
submission.abortReason,
|
|
1657
1700
|
);
|
|
1658
1701
|
}
|
|
1659
|
-
if (recovery)
|
|
1702
|
+
if (recovery) {
|
|
1703
|
+
this.db.submissions.clearRecoveryReason(submissionId);
|
|
1704
|
+
await this.broadcastApprovals();
|
|
1705
|
+
await this.ensureRuntimeReady();
|
|
1706
|
+
}
|
|
1660
1707
|
try {
|
|
1661
1708
|
return await this.executeNonTerminalSubmission(
|
|
1662
1709
|
submission,
|
|
1663
1710
|
recovery,
|
|
1664
1711
|
);
|
|
1665
1712
|
} catch (error) {
|
|
1713
|
+
if (
|
|
1714
|
+
recovery &&
|
|
1715
|
+
(error instanceof ChatStreamStalledError ||
|
|
1716
|
+
error instanceof RetryableModelError)
|
|
1717
|
+
) {
|
|
1718
|
+
throw error;
|
|
1719
|
+
}
|
|
1666
1720
|
const failed = await this.submissions.finish(
|
|
1667
1721
|
submission,
|
|
1668
1722
|
"failed",
|
|
@@ -1712,11 +1766,33 @@ export abstract class AgentRuntimeKernel<
|
|
|
1712
1766
|
}
|
|
1713
1767
|
const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
|
|
1714
1768
|
if (recovery) {
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1769
|
+
const deactivatePreparation = this.submissions.activate(submission, {
|
|
1770
|
+
submissionId,
|
|
1771
|
+
requestId: submission.requestId,
|
|
1772
|
+
messageId: submission.assistantMessageId,
|
|
1773
|
+
startedAt: submission.createdAt,
|
|
1774
|
+
continuation: true,
|
|
1775
|
+
agent: recoveryAdapter,
|
|
1776
|
+
});
|
|
1777
|
+
let ready: boolean;
|
|
1778
|
+
try {
|
|
1779
|
+
ready = await this.prepareRecoveredTurn(
|
|
1780
|
+
submission,
|
|
1781
|
+
recoveryAdapter,
|
|
1782
|
+
);
|
|
1783
|
+
} finally {
|
|
1784
|
+
deactivatePreparation();
|
|
1785
|
+
}
|
|
1786
|
+
if (!ready) {
|
|
1787
|
+
const latest = this.readSubmission(submissionId)!;
|
|
1788
|
+
return latest.abortReason
|
|
1789
|
+
? this.submissions.finish(
|
|
1790
|
+
latest,
|
|
1791
|
+
"aborted",
|
|
1792
|
+
latest.abortReason,
|
|
1793
|
+
)
|
|
1794
|
+
: latest;
|
|
1795
|
+
}
|
|
1720
1796
|
} else {
|
|
1721
1797
|
await this.materializeRecoveredToolResults(submission);
|
|
1722
1798
|
}
|
|
@@ -1822,13 +1898,78 @@ export abstract class AgentRuntimeKernel<
|
|
|
1822
1898
|
await this.projectTerminal(turn, terminal);
|
|
1823
1899
|
return terminal;
|
|
1824
1900
|
} catch (error) {
|
|
1901
|
+
const stalled = error instanceof ChatStreamStalledError;
|
|
1902
|
+
const abortReason = this.readSubmission(submissionId)?.abortReason;
|
|
1903
|
+
if (!abortReason && (stalled || error instanceof RetryableModelError)) {
|
|
1904
|
+
const recoveryErrorCount = this.db.submissions
|
|
1905
|
+
.incrementRecoveryErrorCount(
|
|
1906
|
+
submissionId,
|
|
1907
|
+
stalled
|
|
1908
|
+
? "no_meaningful_model_progress"
|
|
1909
|
+
: "transient_model_error",
|
|
1910
|
+
);
|
|
1911
|
+
if (stalled) {
|
|
1912
|
+
const stallDetails = readModelStreamStallDetails(errorText(error)) ?? {
|
|
1913
|
+
lastMeaningfulActivityAt:
|
|
1914
|
+
Date.now() - MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
1915
|
+
lastMeaningfulActivityType: "model_stream_started",
|
|
1916
|
+
idleMs: MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
1917
|
+
};
|
|
1918
|
+
this._emit("chat:stream:stalled", {
|
|
1919
|
+
requestId: submission.requestId,
|
|
1920
|
+
submissionId,
|
|
1921
|
+
attempt: recoveryErrorCount,
|
|
1922
|
+
timeoutMs: MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
1923
|
+
...stallDetails,
|
|
1924
|
+
reason: "no_meaningful_model_progress",
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
await this.broadcastApprovals();
|
|
1928
|
+
const stoppedDuringRecovery = this.readSubmission(submissionId)
|
|
1929
|
+
?.abortReason;
|
|
1930
|
+
if (
|
|
1931
|
+
!stoppedDuringRecovery &&
|
|
1932
|
+
recoveryErrorCount <
|
|
1933
|
+
(stalled ? CHAT_STALL_MAX_ATTEMPTS : CHAT_RECOVERY_MAX_ATTEMPTS)
|
|
1934
|
+
) {
|
|
1935
|
+
const recoveryOutcome = await this.scheduleChatRecoveryRetry(
|
|
1936
|
+
{
|
|
1937
|
+
submissionId,
|
|
1938
|
+
requestId: submission.requestId,
|
|
1939
|
+
},
|
|
1940
|
+
async () => {
|
|
1941
|
+
this.broadcast(json({
|
|
1942
|
+
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
1943
|
+
messages: await this.getMessages(),
|
|
1944
|
+
}));
|
|
1945
|
+
if (streamId) this.completeRecoverableStream(streamId);
|
|
1946
|
+
this.sendChatResponse(submission.requestId, "", true);
|
|
1947
|
+
},
|
|
1948
|
+
);
|
|
1949
|
+
if (
|
|
1950
|
+
recoveryOutcome !== "disabled" &&
|
|
1951
|
+
!this.readSubmission(submissionId)?.abortReason
|
|
1952
|
+
) {
|
|
1953
|
+
if (recovery && recoveryOutcome === "scheduled") throw error;
|
|
1954
|
+
return this.readSubmission(submissionId)!;
|
|
1955
|
+
}
|
|
1956
|
+
} else if (!stoppedDuringRecovery) {
|
|
1957
|
+
terminalIntent = {
|
|
1958
|
+
outcome: "failed",
|
|
1959
|
+
message: CHAT_RECOVERY_TERMINAL_MESSAGE,
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1825
1963
|
const latest = this.readSubmission(submissionId);
|
|
1826
|
-
const intent = terminalIntent ??
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1964
|
+
const intent = terminalIntent ?? (latest?.abortReason
|
|
1965
|
+
? {
|
|
1966
|
+
outcome: "aborted" as const,
|
|
1967
|
+
message: latest.abortReason,
|
|
1968
|
+
}
|
|
1969
|
+
: {
|
|
1970
|
+
outcome: "failed" as const,
|
|
1971
|
+
message: errorText(error),
|
|
1972
|
+
});
|
|
1832
1973
|
const failed = await this.submissions.finish(
|
|
1833
1974
|
submission,
|
|
1834
1975
|
intent.outcome,
|
|
@@ -1857,19 +1998,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
1857
1998
|
}
|
|
1858
1999
|
}
|
|
1859
2000
|
|
|
1860
|
-
//
|
|
2001
|
+
// 作用:持久化一条 Pi 流记录并广播给在线客户端。
|
|
1861
2002
|
// 调用:活跃 Turn 适配器每产生一条 stream record 时调用。
|
|
1862
|
-
//
|
|
2003
|
+
// 原因:先发 WebSocket 再等持久化完成,让客户端零延迟收到 token,同时
|
|
2004
|
+
// await 保证 Pi 的事件循环不越过尚未落盘的 chunk,续传完整性不受影响。
|
|
1863
2005
|
private async persistAndBroadcastRecord(
|
|
1864
2006
|
turn: ActiveTurn,
|
|
1865
2007
|
chunk: UIMessageChunk,
|
|
1866
2008
|
): Promise<void> {
|
|
1867
2009
|
const streamId = this.streamBySubmission.get(turn.submissionId);
|
|
1868
2010
|
const body = json(chunk);
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2011
|
+
const persist = streamId
|
|
2012
|
+
? this.appendRecoverableChunk(streamId, body)
|
|
2013
|
+
: Promise.resolve();
|
|
1872
2014
|
this.sendChatResponse(turn.requestId, body, false);
|
|
2015
|
+
await persist;
|
|
1873
2016
|
}
|
|
1874
2017
|
|
|
1875
2018
|
private sendChatTerminal(requestId: string, error?: string): void {
|
|
@@ -2081,7 +2224,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2081
2224
|
if (submission) {
|
|
2082
2225
|
await this.cancelSubmissionById(
|
|
2083
2226
|
submission.submissionId,
|
|
2084
|
-
|
|
2227
|
+
USER_STOP_REASON,
|
|
2085
2228
|
);
|
|
2086
2229
|
}
|
|
2087
2230
|
return;
|
|
@@ -2822,7 +2965,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
2822
2965
|
);
|
|
2823
2966
|
const turn: RuntimeTurnState = {
|
|
2824
2967
|
...(current
|
|
2825
|
-
? {
|
|
2968
|
+
? {
|
|
2969
|
+
activeSubmissionId: current.submissionId,
|
|
2970
|
+
...(current.requestId
|
|
2971
|
+
? { activeRequestId: current.requestId }
|
|
2972
|
+
: {}),
|
|
2973
|
+
...(current.recoveryErrorCount === undefined
|
|
2974
|
+
? {}
|
|
2975
|
+
: {
|
|
2976
|
+
recoveryAttempt: current.recoveryErrorCount,
|
|
2977
|
+
// 上限按当前恢复原因取,否则 stall 会显示 "2/5" 却在第 3 次就终止。
|
|
2978
|
+
recoveryMax:
|
|
2979
|
+
current.recoveryReason === "no_meaningful_model_progress"
|
|
2980
|
+
? CHAT_STALL_MAX_ATTEMPTS
|
|
2981
|
+
: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
2982
|
+
...(current.recoveryReason
|
|
2983
|
+
? { recoveryReason: current.recoveryReason }
|
|
2984
|
+
: {}),
|
|
2985
|
+
}),
|
|
2986
|
+
}
|
|
2826
2987
|
: {}),
|
|
2827
2988
|
steerable: Boolean(
|
|
2828
2989
|
current &&
|