@springbrand/agent-runtime 0.1.3-alpha.0 → 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/index.ts +9 -11
- package/src/kernel/bindings.ts +27 -6
- package/src/kernel/extensions.ts +1 -1
- package/src/kernel/profile.ts +3 -4
- 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/layers/orchestration/temporary-agent/runner.ts +0 -65
- package/src/lib/prompt.ts +1 -1
- package/src/pi/assembly/context.ts +1 -3
- package/src/pi/assembly/snapshot.ts +17 -9
- package/src/pi/runtime-adapter/assembly.ts +25 -86
- package/src/pi/runtime-adapter/execution.ts +107 -12
- 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/compiler.ts +5 -32
- package/src/pi/tool/core-host.ts +28 -30
- package/src/pi/tool/core.ts +25 -122
- package/src/pi/tool/mcp.ts +2 -2
- package/src/pi/tool/schedule.ts +46 -2
- 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 +358 -531
- package/src/runtime.ts +195 -44
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,23 +936,24 @@ 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
|
}
|
|
929
943
|
|
|
930
|
-
//
|
|
944
|
+
// 作用:按当前模型输入预算压缩 Pi 上下文。
|
|
931
945
|
// 调用:Pi Turn 适配器在发模型请求前通过 `transformContext` 回调。
|
|
932
|
-
//
|
|
946
|
+
// 原因:模型窗口必须预留最大输出,压缩也必须使用本 Snapshot 的模型和密钥。
|
|
933
947
|
private async transformPiContext(
|
|
934
948
|
submissionId: string,
|
|
935
949
|
messages: Parameters<PiRuntimeTranscript["compactContext"]>[0],
|
|
936
950
|
signal?: AbortSignal,
|
|
937
951
|
): ReturnType<PiRuntimeTranscript["compactContext"]> {
|
|
938
952
|
const snapshot = this.assembly();
|
|
953
|
+
const inputBudget = snapshot.pi.model.contextWindow -
|
|
954
|
+
snapshot.pi.model.maxTokens;
|
|
939
955
|
const compacted = await this.transcript.compactContext(messages, {
|
|
940
|
-
compactAfterTokens:
|
|
941
|
-
snapshot.profile.memory.compactAfterTokens,
|
|
956
|
+
compactAfterTokens: inputBudget * 0.85,
|
|
942
957
|
model: snapshot.pi.model,
|
|
943
958
|
apiKey: this.pi.resolveApiKey(
|
|
944
959
|
snapshot.bindings.provider,
|
|
@@ -1435,6 +1450,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
1435
1450
|
return terminal;
|
|
1436
1451
|
});
|
|
1437
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
|
+
}
|
|
1438
1466
|
let inactiveActivity: RuntimeActivity = "idle";
|
|
1439
1467
|
if (terminal.status === "completed") {
|
|
1440
1468
|
try {
|
|
@@ -1478,6 +1506,16 @@ export abstract class AgentRuntimeKernel<
|
|
|
1478
1506
|
): Promise<boolean> {
|
|
1479
1507
|
let decision = await this.materializeRecoveredToolResults(submission);
|
|
1480
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
|
+
}
|
|
1481
1519
|
this.db.transaction(() => {
|
|
1482
1520
|
this.applyPiRecoveryMutations(
|
|
1483
1521
|
submission,
|
|
@@ -1655,13 +1693,24 @@ export abstract class AgentRuntimeKernel<
|
|
|
1655
1693
|
submission.abortReason,
|
|
1656
1694
|
);
|
|
1657
1695
|
}
|
|
1658
|
-
if (recovery)
|
|
1696
|
+
if (recovery) {
|
|
1697
|
+
this.db.submissions.clearRecoveryReason(submissionId);
|
|
1698
|
+
await this.broadcastApprovals();
|
|
1699
|
+
await this.ensureRuntimeReady();
|
|
1700
|
+
}
|
|
1659
1701
|
try {
|
|
1660
1702
|
return await this.executeNonTerminalSubmission(
|
|
1661
1703
|
submission,
|
|
1662
1704
|
recovery,
|
|
1663
1705
|
);
|
|
1664
1706
|
} catch (error) {
|
|
1707
|
+
if (
|
|
1708
|
+
recovery &&
|
|
1709
|
+
(error instanceof ChatStreamStalledError ||
|
|
1710
|
+
error instanceof RetryableModelError)
|
|
1711
|
+
) {
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1665
1714
|
const failed = await this.submissions.finish(
|
|
1666
1715
|
submission,
|
|
1667
1716
|
"failed",
|
|
@@ -1711,11 +1760,33 @@ export abstract class AgentRuntimeKernel<
|
|
|
1711
1760
|
}
|
|
1712
1761
|
const recoveryAdapter = this.createSubmissionExecutionAdapter(submission);
|
|
1713
1762
|
if (recovery) {
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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
|
+
}
|
|
1719
1790
|
} else {
|
|
1720
1791
|
await this.materializeRecoveredToolResults(submission);
|
|
1721
1792
|
}
|
|
@@ -1821,13 +1892,77 @@ export abstract class AgentRuntimeKernel<
|
|
|
1821
1892
|
await this.projectTerminal(turn, terminal);
|
|
1822
1893
|
return terminal;
|
|
1823
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
|
+
}
|
|
1824
1956
|
const latest = this.readSubmission(submissionId);
|
|
1825
|
-
const intent = terminalIntent ??
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
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
|
+
});
|
|
1831
1966
|
const failed = await this.submissions.finish(
|
|
1832
1967
|
submission,
|
|
1833
1968
|
intent.outcome,
|
|
@@ -1856,19 +1991,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
1856
1991
|
}
|
|
1857
1992
|
}
|
|
1858
1993
|
|
|
1859
|
-
//
|
|
1994
|
+
// 作用:持久化一条 Pi 流记录并广播给在线客户端。
|
|
1860
1995
|
// 调用:活跃 Turn 适配器每产生一条 stream record 时调用。
|
|
1861
|
-
//
|
|
1996
|
+
// 原因:先发 WebSocket 再等持久化完成,让客户端零延迟收到 token,同时
|
|
1997
|
+
// await 保证 Pi 的事件循环不越过尚未落盘的 chunk,续传完整性不受影响。
|
|
1862
1998
|
private async persistAndBroadcastRecord(
|
|
1863
1999
|
turn: ActiveTurn,
|
|
1864
2000
|
chunk: UIMessageChunk,
|
|
1865
2001
|
): Promise<void> {
|
|
1866
2002
|
const streamId = this.streamBySubmission.get(turn.submissionId);
|
|
1867
2003
|
const body = json(chunk);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2004
|
+
const persist = streamId
|
|
2005
|
+
? this.appendRecoverableChunk(streamId, body)
|
|
2006
|
+
: Promise.resolve();
|
|
1871
2007
|
this.sendChatResponse(turn.requestId, body, false);
|
|
2008
|
+
await persist;
|
|
1872
2009
|
}
|
|
1873
2010
|
|
|
1874
2011
|
private sendChatTerminal(requestId: string, error?: string): void {
|
|
@@ -2080,7 +2217,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
2080
2217
|
if (submission) {
|
|
2081
2218
|
await this.cancelSubmissionById(
|
|
2082
2219
|
submission.submissionId,
|
|
2083
|
-
|
|
2220
|
+
USER_STOP_REASON,
|
|
2084
2221
|
);
|
|
2085
2222
|
}
|
|
2086
2223
|
return;
|
|
@@ -2821,7 +2958,21 @@ export abstract class AgentRuntimeKernel<
|
|
|
2821
2958
|
);
|
|
2822
2959
|
const turn: RuntimeTurnState = {
|
|
2823
2960
|
...(current
|
|
2824
|
-
? {
|
|
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
|
+
}
|
|
2825
2976
|
: {}),
|
|
2826
2977
|
steerable: Boolean(
|
|
2827
2978
|
current &&
|