@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.2
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/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/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 +8 -3
- package/src/pi/runtime-adapter/models.ts +221 -29
- 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 +190 -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,9 @@ import {
|
|
|
91
96
|
|
|
92
97
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
93
98
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
99
|
+
const CHAT_RECOVERY_MAX_ATTEMPTS = 5;
|
|
100
|
+
const CHAT_RECOVERY_TERMINAL_MESSAGE =
|
|
101
|
+
"多次恢复仍未成功,本次生成已停止,当前进度已保留。请发送新消息继续。";
|
|
94
102
|
|
|
95
103
|
type RuntimeEventOutboxPayload =
|
|
96
104
|
| { readonly type: "model-usage"; readonly event: RuntimeModelUsageEvent }
|
|
@@ -110,6 +118,8 @@ interface StoredSubmission extends SubmissionReceipt {
|
|
|
110
118
|
queuedUiMessageJson?: string | null;
|
|
111
119
|
userMessageId?: string | null;
|
|
112
120
|
regenerateMessageId?: string | null;
|
|
121
|
+
recoveryErrorCount: number;
|
|
122
|
+
recoveryReason?: "no_meaningful_model_progress" | "transient_model_error";
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
interface SubmitMessageOptions {
|
|
@@ -210,6 +220,11 @@ interface PendingTemporaryAgentApproval {
|
|
|
210
220
|
export abstract class AgentRuntimeKernel<
|
|
211
221
|
Env extends Cloudflare.Env = Cloudflare.Env,
|
|
212
222
|
> extends RecoverableChatAgent<Env, RuntimeState, PiChatRecoveryData> {
|
|
223
|
+
override chatRecovery: ChatRecoveryConfig = {
|
|
224
|
+
maxAttempts: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
225
|
+
terminalMessage: CHAT_RECOVERY_TERMINAL_MESSAGE,
|
|
226
|
+
};
|
|
227
|
+
|
|
213
228
|
initialState: RuntimeState = {
|
|
214
229
|
runtimeLoad: { status: "idle", available: false },
|
|
215
230
|
};
|
|
@@ -352,15 +367,25 @@ export abstract class AgentRuntimeKernel<
|
|
|
352
367
|
};
|
|
353
368
|
},
|
|
354
369
|
retryTurn: async (submissionId) => {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
370
|
+
try {
|
|
371
|
+
const receipt = await this.submissions.recover(submissionId);
|
|
372
|
+
return receipt.status === "completed"
|
|
373
|
+
? { status: "completed" as const }
|
|
374
|
+
: {
|
|
375
|
+
status: "failed" as const,
|
|
376
|
+
error:
|
|
377
|
+
receipt.error ??
|
|
378
|
+
`Pi recovery ended with status ${receipt.status}`,
|
|
379
|
+
};
|
|
380
|
+
} catch (error) {
|
|
381
|
+
if (
|
|
382
|
+
error instanceof ChatStreamStalledError ||
|
|
383
|
+
error instanceof RetryableModelError
|
|
384
|
+
) {
|
|
385
|
+
return { status: "scheduled" as const };
|
|
386
|
+
}
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
364
389
|
},
|
|
365
390
|
failTurn: async (submissionId, message) => {
|
|
366
391
|
const submission = this.readSubmission(submissionId);
|
|
@@ -450,7 +475,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
450
475
|
const startedAt = output.startedAt ?? Date.now();
|
|
451
476
|
return this.pi.createTurn({
|
|
452
477
|
prepared: this.preparedPi(),
|
|
453
|
-
baseRevision: this.baseRevision(),
|
|
454
478
|
pinnedDescriptor: submission.assemblyDescriptor,
|
|
455
479
|
submission: {
|
|
456
480
|
id: submission.submissionId,
|
|
@@ -833,16 +857,6 @@ export abstract class AgentRuntimeKernel<
|
|
|
833
857
|
.join("");
|
|
834
858
|
}
|
|
835
859
|
|
|
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
860
|
// 作用:返回当前已准备好的 Pi Runtime。
|
|
847
861
|
// 调用:创建 Turn 适配器或准入固定装配时调用。
|
|
848
862
|
// 原因:明确区分“已有 Snapshot”和“Pi 已完成准备”,防止半初始化状态进入 Turn。
|
|
@@ -922,7 +936,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
922
936
|
await this.hash(submission.assemblyDescriptor)
|
|
923
937
|
) {
|
|
924
938
|
throw new Error(
|
|
925
|
-
"Pinned Runtime
|
|
939
|
+
"Pinned Runtime assembly descriptor is invalid",
|
|
926
940
|
);
|
|
927
941
|
}
|
|
928
942
|
}
|
|
@@ -1436,6 +1450,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
1436
1450
|
return terminal;
|
|
1437
1451
|
});
|
|
1438
1452
|
await this.drainRuntimeEvents();
|
|
1453
|
+
try {
|
|
1454
|
+
await this.settleChatRecovery(
|
|
1455
|
+
latest.requestId,
|
|
1456
|
+
effectiveOutcome === "succeeded"
|
|
1457
|
+
? "completed"
|
|
1458
|
+
: effectiveOutcome === "aborted"
|
|
1459
|
+
? "skipped"
|
|
1460
|
+
: "failed",
|
|
1461
|
+
effectiveMessage,
|
|
1462
|
+
);
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
console.error("[chat-recovery] terminal settlement failed", error);
|
|
1465
|
+
}
|
|
1439
1466
|
let inactiveActivity: RuntimeActivity = "idle";
|
|
1440
1467
|
if (terminal.status === "completed") {
|
|
1441
1468
|
try {
|
|
@@ -1479,6 +1506,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
1479
1506
|
): Promise<boolean> {
|
|
1480
1507
|
let decision = await this.materializeRecoveredToolResults(submission);
|
|
1481
1508
|
for (let step = 0; step < 32; step += 1) {
|
|
1509
|
+
const latest = this.readSubmission(submission.submissionId);
|
|
1510
|
+
if (!latest || isTerminalSubmissionStatus(latest.status)) return false;
|
|
1511
|
+
if (latest.abortReason) {
|
|
1512
|
+
await this.submissions.finish(
|
|
1513
|
+
latest,
|
|
1514
|
+
"aborted",
|
|
1515
|
+
latest.abortReason,
|
|
1516
|
+
);
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1482
1519
|
this.db.transaction(() => {
|
|
1483
1520
|
this.applyPiRecoveryMutations(
|
|
1484
1521
|
submission,
|
|
@@ -1656,13 +1693,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
1656
1693
|
submission.abortReason,
|
|
1657
1694
|
);
|
|
1658
1695
|
}
|
|
1659
|
-
if (recovery)
|
|
1696
|
+
if (recovery) {
|
|
1697
|
+
this.db.submissions.clearRecoveryReason(submissionId);
|
|
1698
|
+
await this.broadcastApprovals();
|
|
1699
|
+
await this.ensureRuntimeReady();
|
|
1700
|
+
}
|
|
1660
1701
|
try {
|
|
1661
1702
|
return await this.executeNonTerminalSubmission(
|
|
1662
1703
|
submission,
|
|
1663
1704
|
recovery,
|
|
1664
1705
|
);
|
|
1665
1706
|
} catch (error) {
|
|
1707
|
+
if (
|
|
1708
|
+
recovery &&
|
|
1709
|
+
(error instanceof ChatStreamStalledError ||
|
|
1710
|
+
error instanceof RetryableModelError)
|
|
1711
|
+
) {
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1666
1714
|
const failed = await this.submissions.finish(
|
|
1667
1715
|
submission,
|
|
1668
1716
|
"failed",
|
|
@@ -1712,11 +1760,33 @@ export abstract class AgentRuntimeKernel<
|
|
|
1712
1760
|
}
|
|
1713
1761
|
const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
|
|
1714
1762
|
if (recovery) {
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1763
|
+
const deactivatePreparation = this.submissions.activate(submission, {
|
|
1764
|
+
submissionId,
|
|
1765
|
+
requestId: submission.requestId,
|
|
1766
|
+
messageId: submission.assistantMessageId,
|
|
1767
|
+
startedAt: submission.createdAt,
|
|
1768
|
+
continuation: true,
|
|
1769
|
+
agent: recoveryAdapter,
|
|
1770
|
+
});
|
|
1771
|
+
let ready: boolean;
|
|
1772
|
+
try {
|
|
1773
|
+
ready = await this.prepareRecoveredTurn(
|
|
1774
|
+
submission,
|
|
1775
|
+
recoveryAdapter,
|
|
1776
|
+
);
|
|
1777
|
+
} finally {
|
|
1778
|
+
deactivatePreparation();
|
|
1779
|
+
}
|
|
1780
|
+
if (!ready) {
|
|
1781
|
+
const latest = this.readSubmission(submissionId)!;
|
|
1782
|
+
return latest.abortReason
|
|
1783
|
+
? this.submissions.finish(
|
|
1784
|
+
latest,
|
|
1785
|
+
"aborted",
|
|
1786
|
+
latest.abortReason,
|
|
1787
|
+
)
|
|
1788
|
+
: latest;
|
|
1789
|
+
}
|
|
1720
1790
|
} else {
|
|
1721
1791
|
await this.materializeRecoveredToolResults(submission);
|
|
1722
1792
|
}
|
|
@@ -1822,13 +1892,77 @@ export abstract class AgentRuntimeKernel<
|
|
|
1822
1892
|
await this.projectTerminal(turn, terminal);
|
|
1823
1893
|
return terminal;
|
|
1824
1894
|
} catch (error) {
|
|
1895
|
+
const stalled = error instanceof ChatStreamStalledError;
|
|
1896
|
+
const abortReason = this.readSubmission(submissionId)?.abortReason;
|
|
1897
|
+
if (!abortReason && (stalled || error instanceof RetryableModelError)) {
|
|
1898
|
+
const recoveryErrorCount = this.db.submissions
|
|
1899
|
+
.incrementRecoveryErrorCount(
|
|
1900
|
+
submissionId,
|
|
1901
|
+
stalled
|
|
1902
|
+
? "no_meaningful_model_progress"
|
|
1903
|
+
: "transient_model_error",
|
|
1904
|
+
);
|
|
1905
|
+
if (stalled) {
|
|
1906
|
+
const stallDetails = readModelStreamStallDetails(errorText(error)) ?? {
|
|
1907
|
+
lastMeaningfulActivityAt:
|
|
1908
|
+
Date.now() - MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
1909
|
+
lastMeaningfulActivityType: "model_stream_started",
|
|
1910
|
+
idleMs: MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
1911
|
+
};
|
|
1912
|
+
this._emit("chat:stream:stalled", {
|
|
1913
|
+
requestId: submission.requestId,
|
|
1914
|
+
submissionId,
|
|
1915
|
+
attempt: recoveryErrorCount,
|
|
1916
|
+
timeoutMs: MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
1917
|
+
...stallDetails,
|
|
1918
|
+
reason: "no_meaningful_model_progress",
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
await this.broadcastApprovals();
|
|
1922
|
+
const stoppedDuringRecovery = this.readSubmission(submissionId)
|
|
1923
|
+
?.abortReason;
|
|
1924
|
+
if (
|
|
1925
|
+
!stoppedDuringRecovery &&
|
|
1926
|
+
recoveryErrorCount < CHAT_RECOVERY_MAX_ATTEMPTS
|
|
1927
|
+
) {
|
|
1928
|
+
const recoveryOutcome = await this.scheduleChatRecoveryRetry(
|
|
1929
|
+
{
|
|
1930
|
+
submissionId,
|
|
1931
|
+
requestId: submission.requestId,
|
|
1932
|
+
},
|
|
1933
|
+
async () => {
|
|
1934
|
+
this.broadcast(json({
|
|
1935
|
+
type: MessageType.CF_AGENT_CHAT_MESSAGES,
|
|
1936
|
+
messages: await this.getMessages(),
|
|
1937
|
+
}));
|
|
1938
|
+
if (streamId) this.completeRecoverableStream(streamId);
|
|
1939
|
+
this.sendChatResponse(submission.requestId, "", true);
|
|
1940
|
+
},
|
|
1941
|
+
);
|
|
1942
|
+
if (
|
|
1943
|
+
recoveryOutcome !== "disabled" &&
|
|
1944
|
+
!this.readSubmission(submissionId)?.abortReason
|
|
1945
|
+
) {
|
|
1946
|
+
if (recovery && recoveryOutcome === "scheduled") throw error;
|
|
1947
|
+
return this.readSubmission(submissionId)!;
|
|
1948
|
+
}
|
|
1949
|
+
} else if (!stoppedDuringRecovery) {
|
|
1950
|
+
terminalIntent = {
|
|
1951
|
+
outcome: "failed",
|
|
1952
|
+
message: CHAT_RECOVERY_TERMINAL_MESSAGE,
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1825
1956
|
const latest = this.readSubmission(submissionId);
|
|
1826
|
-
const intent = terminalIntent ??
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1957
|
+
const intent = terminalIntent ?? (latest?.abortReason
|
|
1958
|
+
? {
|
|
1959
|
+
outcome: "aborted" as const,
|
|
1960
|
+
message: latest.abortReason,
|
|
1961
|
+
}
|
|
1962
|
+
: {
|
|
1963
|
+
outcome: "failed" as const,
|
|
1964
|
+
message: errorText(error),
|
|
1965
|
+
});
|
|
1832
1966
|
const failed = await this.submissions.finish(
|
|
1833
1967
|
submission,
|
|
1834
1968
|
intent.outcome,
|
|
@@ -1857,19 +1991,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
1857
1991
|
}
|
|
1858
1992
|
}
|
|
1859
1993
|
|
|
1860
|
-
//
|
|
1994
|
+
// 作用:持久化一条 Pi 流记录并广播给在线客户端。
|
|
1861
1995
|
// 调用:活跃 Turn 适配器每产生一条 stream record 时调用。
|
|
1862
|
-
//
|
|
1996
|
+
// 原因:先发 WebSocket 再等持久化完成,让客户端零延迟收到 token,同时
|
|
1997
|
+
// await 保证 Pi 的事件循环不越过尚未落盘的 chunk,续传完整性不受影响。
|
|
1863
1998
|
private async persistAndBroadcastRecord(
|
|
1864
1999
|
turn: ActiveTurn,
|
|
1865
2000
|
chunk: UIMessageChunk,
|
|
1866
2001
|
): Promise<void> {
|
|
1867
2002
|
const streamId = this.streamBySubmission.get(turn.submissionId);
|
|
1868
2003
|
const body = json(chunk);
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2004
|
+
const persist = streamId
|
|
2005
|
+
? this.appendRecoverableChunk(streamId, body)
|
|
2006
|
+
: Promise.resolve();
|
|
1872
2007
|
this.sendChatResponse(turn.requestId, body, false);
|
|
2008
|
+
await persist;
|
|
1873
2009
|
}
|
|
1874
2010
|
|
|
1875
2011
|
private sendChatTerminal(requestId: string, error?: string): void {
|
|
@@ -2081,7 +2217,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2081
2217
|
if (submission) {
|
|
2082
2218
|
await this.cancelSubmissionById(
|
|
2083
2219
|
submission.submissionId,
|
|
2084
|
-
|
|
2220
|
+
USER_STOP_REASON,
|
|
2085
2221
|
);
|
|
2086
2222
|
}
|
|
2087
2223
|
return;
|
|
@@ -2822,7 +2958,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
2822
2958
|
);
|
|
2823
2959
|
const turn: RuntimeTurnState = {
|
|
2824
2960
|
...(current
|
|
2825
|
-
? {
|
|
2961
|
+
? {
|
|
2962
|
+
activeSubmissionId: current.submissionId,
|
|
2963
|
+
...(current.requestId
|
|
2964
|
+
? { activeRequestId: current.requestId }
|
|
2965
|
+
: {}),
|
|
2966
|
+
...(current.recoveryErrorCount === undefined
|
|
2967
|
+
? {}
|
|
2968
|
+
: {
|
|
2969
|
+
recoveryAttempt: current.recoveryErrorCount,
|
|
2970
|
+
recoveryMax: CHAT_RECOVERY_MAX_ATTEMPTS,
|
|
2971
|
+
...(current.recoveryReason
|
|
2972
|
+
? { recoveryReason: current.recoveryReason }
|
|
2973
|
+
: {}),
|
|
2974
|
+
}),
|
|
2975
|
+
}
|
|
2826
2976
|
: {}),
|
|
2827
2977
|
steerable: Boolean(
|
|
2828
2978
|
current &&
|