Package not found. Please check the package name and try again.
@springbrand/agent-runtime 0.2.0-alpha.44 → 0.2.0-alpha.46
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/adapter/cloudflare/subagent/definition.ts +0 -3
- package/src/adapter/cloudflare/subagent/runner.ts +19 -6
- package/src/db/schema.ts +6 -1
- package/src/db/submission.repo.ts +13 -1
- package/src/index.ts +0 -1
- package/src/kernel/bindings.ts +0 -4
- package/src/kernel/submission-lifecycle.ts +25 -8
- package/src/pi/assembly/context.ts +23 -10
- package/src/pi/assembly/snapshot.ts +14 -8
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/message/projection.ts +47 -21
- package/src/pi/runtime-adapter/assembly.ts +2 -30
- package/src/pi/runtime-adapter/execution.ts +56 -46
- package/src/pi/runtime-adapter/index.ts +2 -8
- package/src/pi/runtime-adapter/models.ts +63 -26
- package/src/pi/runtime-adapter/transcript.ts +6 -3
- package/src/pi/tool/base.ts +10 -15
- package/src/pi/tool/compiler.ts +1 -1
- package/src/pi/tool/core.ts +2 -0
- package/src/pi/tool/schedule.ts +15 -11
- package/src/pi/tool/skill.ts +3 -0
- package/src/pi/tool/subagent.ts +2 -0
- package/src/pi/tool/time.ts +1 -0
- package/src/pi/tool/web-search/api.ts +8 -0
- package/src/pi/tool/web-search/web-search.ts +1 -0
- package/src/pi/tool/workspace-sandbox.ts +3 -0
- package/src/runtime-agent.ts +9 -0
- package/src/runtime-assembler.ts +17 -11
- package/src/runtime.ts +111 -39
|
@@ -15,13 +15,17 @@ import type {
|
|
|
15
15
|
ToolResultMessage,
|
|
16
16
|
UserMessage,
|
|
17
17
|
} from "@earendil-works/pi-ai";
|
|
18
|
+
import { isContextOverflow } from "@earendil-works/pi-ai";
|
|
18
19
|
import { transformMessages } from "@earendil-works/pi-ai/api/transform-messages";
|
|
19
20
|
import {
|
|
20
21
|
parkPiToolApproval,
|
|
21
22
|
parkPiToolInteraction,
|
|
22
23
|
requiresPiToolApproval,
|
|
23
24
|
} from "../turn";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
classifyPiTerminalStopReason,
|
|
27
|
+
PiChunkEncoder,
|
|
28
|
+
} from "../message";
|
|
25
29
|
import type { UIMessageChunk } from "ai";
|
|
26
30
|
import { ChatStreamStalledError } from "agents/chat";
|
|
27
31
|
import {
|
|
@@ -42,7 +46,6 @@ import {
|
|
|
42
46
|
import {
|
|
43
47
|
isRecoverableAssistantError,
|
|
44
48
|
isModelStreamStallMessage,
|
|
45
|
-
resolvePiApiKey,
|
|
46
49
|
withProviderRetry,
|
|
47
50
|
type PiGenerationLifecycleObserver,
|
|
48
51
|
} from "./models";
|
|
@@ -79,6 +82,8 @@ const MAX_WRAP_UP_TURNS = 2;
|
|
|
79
82
|
|
|
80
83
|
export class RetryableModelError extends Error {}
|
|
81
84
|
|
|
85
|
+
export class ContextOverflowError extends RetryableModelError {}
|
|
86
|
+
|
|
82
87
|
function normalizePlanToolCalls(message: AgentMessage): void {
|
|
83
88
|
if (message.role !== "assistant") return;
|
|
84
89
|
for (const part of message.content) {
|
|
@@ -88,42 +93,6 @@ function normalizePlanToolCalls(message: AgentMessage): void {
|
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
/**
|
|
92
|
-
* 把 Pi 的终止原因翻译成本仓的回合结局。
|
|
93
|
-
*
|
|
94
|
-
* 调用:终态提交(`onTerminal`)与流式记录的 `turnStatus` 共用这一处,
|
|
95
|
-
* 保证服务端只有一份判据 —— 两处各写一遍三元链正是它们悄悄分叉的原因。
|
|
96
|
-
*
|
|
97
|
-
* 为什么未知取值判失败而不是成功:`stopReason` 是上游会扩的联合体
|
|
98
|
-
* (0.83 就加了 `"pending"`)。把认不出的结局宣称成「成功」,等于让一次
|
|
99
|
-
* 异常结束伪装成正常完成 —— 这是本仓明确禁止的假完成,
|
|
100
|
-
* 也是 Pi 自己在 0.83 的选择(认不出的终止原因直接抛错而非压平成 stop)。
|
|
101
|
-
* 前端对未知取值取 `"running"`(未知即未定),最终由服务端这份权威判据校正。
|
|
102
|
-
*/
|
|
103
|
-
function classifyPiStopReason(stopReason: string | undefined): {
|
|
104
|
-
outcome: PiTurnTerminalIntent["outcome"];
|
|
105
|
-
turnStatus: "completed" | "error" | "aborted";
|
|
106
|
-
message?: string;
|
|
107
|
-
} {
|
|
108
|
-
switch (stopReason) {
|
|
109
|
-
case "stop":
|
|
110
|
-
case "length":
|
|
111
|
-
return { outcome: "succeeded", turnStatus: "completed" };
|
|
112
|
-
case "aborted":
|
|
113
|
-
return { outcome: "aborted", turnStatus: "aborted" };
|
|
114
|
-
case "error":
|
|
115
|
-
return { outcome: "failed", turnStatus: "error" };
|
|
116
|
-
default:
|
|
117
|
-
return {
|
|
118
|
-
outcome: "failed",
|
|
119
|
-
turnStatus: "error",
|
|
120
|
-
message: `SpringBrand ended the turn with an unrecognized stop reason: ${
|
|
121
|
-
stopReason ?? "(none)"
|
|
122
|
-
}`,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
96
|
function visibleAssistantContent(message: AssistantMessage) {
|
|
128
97
|
return message.content.filter((part) =>
|
|
129
98
|
part.type === "text"
|
|
@@ -140,7 +109,6 @@ interface PiTurnAdapterOptions {
|
|
|
140
109
|
readonly thinkingLevel?: ThinkingLevel;
|
|
141
110
|
};
|
|
142
111
|
readonly models: Models;
|
|
143
|
-
readonly apiKey: string;
|
|
144
112
|
readonly modelSessionId: string;
|
|
145
113
|
// 读取这次执行真正要交给 Pi 的 canonical transcript。
|
|
146
114
|
// PiTurnAdapter.run 在创建 PiCore 前调用它,宿主应返回当时最新的 transcript。
|
|
@@ -331,7 +299,6 @@ class PiTurnAdapter {
|
|
|
331
299
|
this.opts.modelSessionId,
|
|
332
300
|
this.opts.onGeneration,
|
|
333
301
|
),
|
|
334
|
-
getApiKey: () => this.opts.apiKey,
|
|
335
302
|
// transformMessages 在宿主上下文变换之后运行,确保跨 provider 的 tool call ID 格式兼容。
|
|
336
303
|
// OpenAI Responses API 生成含 `|` 的 450+ 字符 ID,Anthropic 只接受 ^[a-zA-Z0-9_-]+$(64 字符上限);
|
|
337
304
|
// 切换 provider 或消息跨 provider 回放时若不规范化,provider 会静默拒绝。
|
|
@@ -564,6 +531,21 @@ export interface CreatePreparedPiTurnOptions {
|
|
|
564
531
|
error?: unknown;
|
|
565
532
|
occurredAt: number;
|
|
566
533
|
}>) => void;
|
|
534
|
+
/** Reports model-requested Tools absent from the pinned Runtime surface. */
|
|
535
|
+
readonly onUnknownToolStarted?: (input: Readonly<{
|
|
536
|
+
toolCallId: string;
|
|
537
|
+
toolName: string;
|
|
538
|
+
input: unknown;
|
|
539
|
+
occurredAt: number;
|
|
540
|
+
}>) => void;
|
|
541
|
+
/** Reports the immediate Pi failure for a Tool absent from the pinned Runtime surface. */
|
|
542
|
+
readonly onUnknownToolFinished?: (input: Readonly<{
|
|
543
|
+
toolCallId: string;
|
|
544
|
+
toolName: string;
|
|
545
|
+
outcome: "failed";
|
|
546
|
+
output: import("@earendil-works/pi-agent-core").AgentToolResult<unknown>;
|
|
547
|
+
occurredAt: number;
|
|
548
|
+
}>) => void;
|
|
567
549
|
/** Per-Submission executors for tools whose metadata is fixed at assembly time. */
|
|
568
550
|
readonly toolExecutors?: Readonly<Record<
|
|
569
551
|
string,
|
|
@@ -659,6 +641,7 @@ export class PreparedPiTurnAdapter {
|
|
|
659
641
|
private readonly encoder: PiChunkEncoder;
|
|
660
642
|
private readonly steerMessageIds: string[] = [];
|
|
661
643
|
private terminalIntent?: PiTurnTerminalIntent;
|
|
644
|
+
private readonly contextWindow: number;
|
|
662
645
|
|
|
663
646
|
/**
|
|
664
647
|
* 把 Prepared Runtime 和 Submission 持久化端口绑定成一个可执行 Turn。
|
|
@@ -679,6 +662,7 @@ export class PreparedPiTurnAdapter {
|
|
|
679
662
|
dependencies.owner,
|
|
680
663
|
options.pinnedDescriptor,
|
|
681
664
|
);
|
|
665
|
+
this.contextWindow = state.snapshot.pi.model.contextWindow;
|
|
682
666
|
this.assistantOrdinal = options.submission.assistantOrdinal;
|
|
683
667
|
this.encoder = new PiChunkEncoder({
|
|
684
668
|
messageId: options.submission.messageId,
|
|
@@ -903,10 +887,6 @@ export class PreparedPiTurnAdapter {
|
|
|
903
887
|
thinkingLevel: state.snapshot.pi.thinkingLevel,
|
|
904
888
|
},
|
|
905
889
|
models: dependencies.models,
|
|
906
|
-
apiKey: resolvePiApiKey(
|
|
907
|
-
state.snapshot.bindings.provider,
|
|
908
|
-
state.snapshot.pi.model.id,
|
|
909
|
-
),
|
|
910
890
|
modelSessionId: options.modelSessionId,
|
|
911
891
|
...(options.onGeneration ? { onGeneration: options.onGeneration } : {}),
|
|
912
892
|
canonicalMessages: options.canonicalMessages,
|
|
@@ -1038,6 +1018,28 @@ export class PreparedPiTurnAdapter {
|
|
|
1038
1018
|
// PiTurnAdapter 会对每个 AgentEvent 调用它,PiCore 会等待该 Promise 后再越过订阅事件屏障。
|
|
1039
1019
|
// canonical message 先于流投影提交,恢复才不会依赖仅供浏览器消费的记录;调整顺序必须复核 recovery effect。
|
|
1040
1020
|
private async handleEvent(event: AgentEvent): Promise<void> {
|
|
1021
|
+
if (
|
|
1022
|
+
event.type === "tool_execution_start" &&
|
|
1023
|
+
!this.candidates.some(({ tool }) => tool.name === event.toolName)
|
|
1024
|
+
) {
|
|
1025
|
+
this.options.onUnknownToolStarted?.({
|
|
1026
|
+
toolCallId: event.toolCallId,
|
|
1027
|
+
toolName: event.toolName,
|
|
1028
|
+
input: event.args,
|
|
1029
|
+
occurredAt: Date.now(),
|
|
1030
|
+
});
|
|
1031
|
+
} else if (
|
|
1032
|
+
event.type === "tool_execution_end" &&
|
|
1033
|
+
!this.candidates.some(({ tool }) => tool.name === event.toolName)
|
|
1034
|
+
) {
|
|
1035
|
+
this.options.onUnknownToolFinished?.({
|
|
1036
|
+
toolCallId: event.toolCallId,
|
|
1037
|
+
toolName: event.toolName,
|
|
1038
|
+
outcome: "failed",
|
|
1039
|
+
output: event.result,
|
|
1040
|
+
occurredAt: Date.now(),
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1041
1043
|
if (
|
|
1042
1044
|
event.type === "message_start" ||
|
|
1043
1045
|
event.type === "message_update" ||
|
|
@@ -1048,7 +1050,10 @@ export class PreparedPiTurnAdapter {
|
|
|
1048
1050
|
if (
|
|
1049
1051
|
event.type === "message_update" &&
|
|
1050
1052
|
event.assistantMessageEvent.type === "error" &&
|
|
1051
|
-
(
|
|
1053
|
+
(isContextOverflow(
|
|
1054
|
+
event.assistantMessageEvent.error,
|
|
1055
|
+
this.contextWindow,
|
|
1056
|
+
) || isModelStreamStallMessage(
|
|
1052
1057
|
event.assistantMessageEvent.error.errorMessage,
|
|
1053
1058
|
) ||
|
|
1054
1059
|
isRecoverableAssistantError(event.assistantMessageEvent.error))
|
|
@@ -1059,6 +1064,11 @@ export class PreparedPiTurnAdapter {
|
|
|
1059
1064
|
if (event.type === "message_end") {
|
|
1060
1065
|
const message = event.message;
|
|
1061
1066
|
if (message.role === "assistant") {
|
|
1067
|
+
if (isContextOverflow(message, this.contextWindow)) {
|
|
1068
|
+
throw new ContextOverflowError(
|
|
1069
|
+
message.errorMessage ?? "Model context window overflow",
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1062
1072
|
const stalled = message.stopReason === "error" &&
|
|
1063
1073
|
isModelStreamStallMessage(message.errorMessage);
|
|
1064
1074
|
const retryable = isRecoverableAssistantError(message);
|
|
@@ -1119,7 +1129,7 @@ export class PreparedPiTurnAdapter {
|
|
|
1119
1129
|
authoritativeMessage.role === "assistant" &&
|
|
1120
1130
|
authoritativeMessage.stopReason !== "toolUse"
|
|
1121
1131
|
) {
|
|
1122
|
-
const terminal =
|
|
1132
|
+
const terminal = classifyPiTerminalStopReason(
|
|
1123
1133
|
authoritativeMessage.stopReason,
|
|
1124
1134
|
);
|
|
1125
1135
|
this.terminalIntent = {
|
|
@@ -2,13 +2,13 @@ import {
|
|
|
2
2
|
PreparedPiTurnAdapter,
|
|
3
3
|
type CreatePreparedPiTurnOptions,
|
|
4
4
|
} from "./execution";
|
|
5
|
-
export { RetryableModelError } from "./execution";
|
|
5
|
+
export { ContextOverflowError, RetryableModelError } from "./execution";
|
|
6
6
|
import {
|
|
7
7
|
uiUserMessageToPi,
|
|
8
8
|
} from "../message";
|
|
9
9
|
import type { UIMessage } from "ai";
|
|
10
10
|
import { createModels, type MutableModels } from "@earendil-works/pi-ai";
|
|
11
|
-
import { configurePiModels, createPiModels
|
|
11
|
+
import { configurePiModels, createPiModels } from "./models";
|
|
12
12
|
export {
|
|
13
13
|
MODEL_STREAM_STALL_TIMEOUT_MS,
|
|
14
14
|
modelRequestUrl,
|
|
@@ -120,12 +120,6 @@ export class PiRuntimeAdapter {
|
|
|
120
120
|
return uiUserMessageToPi(input, privateContext);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
resolveApiKey(
|
|
124
|
-
...args: Parameters<typeof resolvePiApiKey>
|
|
125
|
-
): ReturnType<typeof resolvePiApiKey> {
|
|
126
|
-
return resolvePiApiKey(...args);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
123
|
/**
|
|
130
124
|
* 加载并预检一份可供后续准入使用的 Prepared Runtime。
|
|
131
125
|
*
|
|
@@ -111,6 +111,38 @@ export type PiGenerationLifecycleObserver = (
|
|
|
111
111
|
event: PiGenerationLifecycleEvent,
|
|
112
112
|
) => void;
|
|
113
113
|
|
|
114
|
+
export function structuredOutputStream(
|
|
115
|
+
streamFn: StreamFn,
|
|
116
|
+
model: Model<Api>,
|
|
117
|
+
schema: Record<string, unknown>,
|
|
118
|
+
): StreamFn | undefined {
|
|
119
|
+
if (
|
|
120
|
+
model.api !== "anthropic-messages" ||
|
|
121
|
+
!model.provider.startsWith("model-api-openrouter-messages-")
|
|
122
|
+
) return undefined;
|
|
123
|
+
|
|
124
|
+
return (activeModel, context, options) =>
|
|
125
|
+
streamFn(activeModel, context, {
|
|
126
|
+
...options,
|
|
127
|
+
onPayload: async (payload, payloadModel) => {
|
|
128
|
+
const value = await options?.onPayload?.(payload, payloadModel) ?? payload;
|
|
129
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
130
|
+
throw new Error("Structured model payload must be an object");
|
|
131
|
+
}
|
|
132
|
+
const current = (value as Record<string, unknown>).output_config;
|
|
133
|
+
return {
|
|
134
|
+
...value,
|
|
135
|
+
output_config: {
|
|
136
|
+
...(current && typeof current === "object" && !Array.isArray(current)
|
|
137
|
+
? current
|
|
138
|
+
: {}),
|
|
139
|
+
format: { type: "json_schema", schema },
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
114
146
|
export function isModelStreamStallMessage(message?: string): message is string {
|
|
115
147
|
return message === MODEL_STREAM_STALL_MESSAGE ||
|
|
116
148
|
message?.startsWith(MODEL_STREAM_STALL_DETAILS_PREFIX) === true;
|
|
@@ -534,8 +566,8 @@ export function withProviderRetry(
|
|
|
534
566
|
}
|
|
535
567
|
|
|
536
568
|
// 找出部署把这个模型放在哪个端点,并保留该端点在配置中的位置。
|
|
537
|
-
// resolvePiModel
|
|
538
|
-
//
|
|
569
|
+
// resolvePiModel 在组装运行快照前调用它,调用方只需传部署配置和 modelId。
|
|
570
|
+
// 同一 modelId 若配置多次会取第一个,是否应改为拒绝重复配置仍待确认。
|
|
539
571
|
function endpointFor(
|
|
540
572
|
provider: RuntimeProviderPort,
|
|
541
573
|
modelId: string,
|
|
@@ -585,6 +617,20 @@ function piApiFor(protocol: RuntimeModelProtocol) {
|
|
|
585
617
|
}
|
|
586
618
|
}
|
|
587
619
|
|
|
620
|
+
function anthropicMessagesModelForOpenRouter(
|
|
621
|
+
modelId: string,
|
|
622
|
+
): Model<"anthropic-messages"> | undefined {
|
|
623
|
+
if (!modelId.startsWith("anthropic/")) return;
|
|
624
|
+
const anthropicModelId = modelId
|
|
625
|
+
.slice("anthropic/".length)
|
|
626
|
+
.replace(/:batch$/, "")
|
|
627
|
+
.replace(/-fast$/, "")
|
|
628
|
+
.replaceAll(".", "-");
|
|
629
|
+
return CATALOGS["anthropic-messages"].find(
|
|
630
|
+
(model) => model.id === anthropicModelId,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
588
634
|
// 把部署声明的模型改造成可由对应自定义端点执行的 Pi 模型。
|
|
589
635
|
// resolvePiModel 为单个运行快照调用它,configurePiModels 则在注册端点时为端点中的每个 modelId 调用它。
|
|
590
636
|
// Pi 的内置 OpenAI、Anthropic provider 提供上下文窗口、费用和推理能力等元数据;这里保留这些元数据,但以部署协议、provider 标识和 baseURL 覆盖内置路由,未知模型会在初始化阶段直接报错,而不是拖到请求阶段。
|
|
@@ -602,10 +648,10 @@ function configuredModel(
|
|
|
602
648
|
);
|
|
603
649
|
}
|
|
604
650
|
const {
|
|
605
|
-
api:
|
|
651
|
+
api: catalogApi,
|
|
606
652
|
provider: _provider,
|
|
607
653
|
baseUrl: _baseUrl,
|
|
608
|
-
compat,
|
|
654
|
+
compat: catalogCompat,
|
|
609
655
|
headers: catalogHeaders,
|
|
610
656
|
...metadata
|
|
611
657
|
} = catalogModel;
|
|
@@ -614,17 +660,24 @@ function configuredModel(
|
|
|
614
660
|
...endpoint.headers,
|
|
615
661
|
};
|
|
616
662
|
const openRouterProviderPin = endpoint.openRouterProviderPins?.[modelId];
|
|
617
|
-
const
|
|
663
|
+
const configuredApi = apiFor(endpoint.protocol);
|
|
664
|
+
// OpenRouter uses dotted versions and optional transport suffixes while the
|
|
665
|
+
// Anthropic catalog uses dashed base model ids.
|
|
666
|
+
const anthropicMessagesCompat = endpoint.protocol === "openrouter-messages"
|
|
667
|
+
? anthropicMessagesModelForOpenRouter(modelId)?.compat
|
|
668
|
+
: undefined;
|
|
618
669
|
return {
|
|
619
670
|
...metadata,
|
|
620
|
-
api:
|
|
671
|
+
api: configuredApi,
|
|
621
672
|
provider: providerId(endpoint, index),
|
|
622
673
|
baseUrl: endpoint.baseURL,
|
|
623
674
|
...(endpoint.protocol === "openrouter-messages"
|
|
624
675
|
? {
|
|
625
676
|
compat: {
|
|
677
|
+
...anthropicMessagesCompat,
|
|
626
678
|
supportsEagerToolInputStreaming: false,
|
|
627
|
-
supportsStrictTools
|
|
679
|
+
supportsStrictTools:
|
|
680
|
+
anthropicMessagesCompat?.supportsStrictTools ?? false,
|
|
628
681
|
supportsToolReferences: true,
|
|
629
682
|
openRouterRouting: {
|
|
630
683
|
require_parameters: true,
|
|
@@ -637,7 +690,9 @@ function configuredModel(
|
|
|
637
690
|
},
|
|
638
691
|
},
|
|
639
692
|
}
|
|
640
|
-
:
|
|
693
|
+
: catalogApi === configuredApi && catalogCompat
|
|
694
|
+
? { compat: catalogCompat }
|
|
695
|
+
: {}),
|
|
641
696
|
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
642
697
|
};
|
|
643
698
|
}
|
|
@@ -661,24 +716,6 @@ export function resolvePiModel(
|
|
|
661
716
|
return configuredModel(endpoint, index, modelId);
|
|
662
717
|
}
|
|
663
718
|
|
|
664
|
-
/**
|
|
665
|
-
* 取出负责指定模型的部署端点密钥。
|
|
666
|
-
*
|
|
667
|
-
* Runtime 在创建一次模型执行或压缩上下文前调用它;辅助的标题生成器也在请求 Pi 时使用它。
|
|
668
|
-
* 调用方应传入与 {@link resolvePiModel} 相同的 provider 和 modelId,并只把返回值用于当前请求。
|
|
669
|
-
*
|
|
670
|
-
* 它与模型解析共用 endpointFor,确保密钥和模型总是选择同一个端点,也让未配置的模型在密钥离开部署边界前失败。
|
|
671
|
-
* 不要另写按协议选密钥的分支,否则同协议的多个端点会拿错凭据。
|
|
672
|
-
*
|
|
673
|
-
* @throws 当部署未配置该 modelId 时抛出错误。
|
|
674
|
-
*/
|
|
675
|
-
export function resolvePiApiKey(
|
|
676
|
-
provider: RuntimeProviderPort,
|
|
677
|
-
modelId: string,
|
|
678
|
-
): string {
|
|
679
|
-
return endpointFor(provider, modelId).endpoint.apiKey;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
719
|
/**
|
|
683
720
|
* 用当前部署端点完整重建一个已有的 Pi 模型集合。
|
|
684
721
|
*
|
|
@@ -440,7 +440,7 @@ export class PiRuntimeTranscript {
|
|
|
440
440
|
/**
|
|
441
441
|
* 在 Pi 请求模型前按 Runtime 阈值压缩当前上下文,并把压缩 entry 写回 Session。
|
|
442
442
|
*
|
|
443
|
-
* `AgentRuntimeKernel.transformPiContext` 作为 Pi 的 `transformContext`
|
|
443
|
+
* `AgentRuntimeKernel.transformPiContext` 作为 Pi 的 `transformContext` 调用它,并提供本次固定的模型和取消信号。
|
|
444
444
|
*
|
|
445
445
|
* 方法先逐条核对调用参数与耐久分支;不一致或压缩不可用时保留原消息,避免把漂移的内存上下文提交成新的规范分支。
|
|
446
446
|
*/
|
|
@@ -448,8 +448,8 @@ export class PiRuntimeTranscript {
|
|
|
448
448
|
messages: AgentMessage[],
|
|
449
449
|
options: {
|
|
450
450
|
compactAfterTokens: number;
|
|
451
|
+
force?: boolean;
|
|
451
452
|
model: Model<Api>;
|
|
452
|
-
apiKey: string;
|
|
453
453
|
signal?: AbortSignal;
|
|
454
454
|
submissionId?: string;
|
|
455
455
|
onCompactionPersisted?: (event: RuntimeModelUsageEvent) => void;
|
|
@@ -474,11 +474,14 @@ export class PiRuntimeTranscript {
|
|
|
474
474
|
const result = await compactPiContext({
|
|
475
475
|
branch,
|
|
476
476
|
compactAfterTokens: options.compactAfterTokens,
|
|
477
|
+
force: options.force,
|
|
477
478
|
models: this.models,
|
|
478
479
|
model: options.model,
|
|
479
|
-
apiKey: options.apiKey,
|
|
480
480
|
signal: options.signal,
|
|
481
481
|
});
|
|
482
|
+
if (options.force && !result.didCompact) {
|
|
483
|
+
throw new Error("Forced context compaction did not reduce the model input");
|
|
484
|
+
}
|
|
482
485
|
if (result.compactionEntry) {
|
|
483
486
|
// 整对象写入:assembly 侧已经造好了完整的 compaction 条目,这里不再拆成位置
|
|
484
487
|
// 参数。旧写法拆 5 个参数喂 pi 的 Session.appendCompaction,pi 0.81 把它加到
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type {
|
|
|
2
2
|
AgentTool,
|
|
3
3
|
AgentToolResult,
|
|
4
4
|
} from "@earendil-works/pi-agent-core";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
StringEnum,
|
|
7
|
+
Type,
|
|
8
|
+
type Static,
|
|
9
|
+
type TSchema,
|
|
10
|
+
} from "@earendil-works/pi-ai";
|
|
6
11
|
import { estimateStringTokens } from "agents/experimental/memory/utils";
|
|
7
12
|
import type { RuntimeMemoryPort } from "../../kernel/bindings";
|
|
8
13
|
import type { RuntimeMemoryProfile } from "../../kernel/profile";
|
|
@@ -145,11 +150,7 @@ const updatePlanParameters = Type.Object({
|
|
|
145
150
|
text: Type.String({
|
|
146
151
|
description: "The step, phrased as a short imperative.",
|
|
147
152
|
}),
|
|
148
|
-
status:
|
|
149
|
-
Type.Literal("pending"),
|
|
150
|
-
Type.Literal("in_progress"),
|
|
151
|
-
Type.Literal("done"),
|
|
152
|
-
], {
|
|
153
|
+
status: StringEnum(["pending", "in_progress", "done"] as const, {
|
|
153
154
|
description:
|
|
154
155
|
'Current status. Use "done" for a finished step; never use "completed".',
|
|
155
156
|
}),
|
|
@@ -214,18 +215,12 @@ export function normalizeUpdatePlanArguments(
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
const setContextParameters = Type.Object({
|
|
217
|
-
label:
|
|
218
|
-
Type.Literal("memory"),
|
|
219
|
-
Type.Literal("preferences"),
|
|
220
|
-
], {
|
|
218
|
+
label: StringEnum(["memory", "preferences"] as const, {
|
|
221
219
|
description:
|
|
222
220
|
"Context block to update: memory for durable facts and active context, preferences for tone and workflow choices.",
|
|
223
221
|
}),
|
|
224
222
|
content: Type.String({ description: "Text to store in the selected context block." }),
|
|
225
|
-
action: Type.Optional(
|
|
226
|
-
Type.Literal("replace"),
|
|
227
|
-
Type.Literal("append"),
|
|
228
|
-
], {
|
|
223
|
+
action: Type.Optional(StringEnum(["replace", "append"] as const, {
|
|
229
224
|
description: 'Whether to replace the block or append to it. Defaults to "replace".',
|
|
230
225
|
})),
|
|
231
226
|
}, { additionalProperties: false });
|
|
@@ -318,7 +313,7 @@ export function basePiToolCandidates(
|
|
|
318
313
|
name: "update_plan",
|
|
319
314
|
label: "Update plan",
|
|
320
315
|
description:
|
|
321
|
-
"Maintain the user-visible plan for the current task. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
|
|
316
|
+
"Maintain the user-visible plan for the current task. Required input: steps. Call this whenever a task involves 2 or more steps, and again every time the plan or a step's status changes. Always pass the FULL plan — it replaces the previous plan entirely (idempotent overwrite), so omitted steps disappear.",
|
|
322
317
|
parameters: updatePlanParameters,
|
|
323
318
|
prepareArguments: normalizeUpdatePlanArguments,
|
|
324
319
|
async execute(_toolCallId, input) {
|
package/src/pi/tool/compiler.ts
CHANGED
|
@@ -66,7 +66,7 @@ export interface PiToolCandidate {
|
|
|
66
66
|
* 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
|
|
67
67
|
*/
|
|
68
68
|
readonly alwaysRequiresApproval?: boolean;
|
|
69
|
-
/** Recovery may replay this Tool Call only when the candidate guarantees convergence. */
|
|
69
|
+
/** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
|
|
70
70
|
readonly retry?: "idempotent" | "non-idempotent";
|
|
71
71
|
readonly summary?: string;
|
|
72
72
|
readonly source?: ApprovalReceipt["source"];
|
package/src/pi/tool/core.ts
CHANGED
|
@@ -196,6 +196,7 @@ export function listExtensionsPiToolCandidate(
|
|
|
196
196
|
return {
|
|
197
197
|
owner: "core:extensions",
|
|
198
198
|
requiredExecutionLevel: "low",
|
|
199
|
+
retry: "idempotent",
|
|
199
200
|
tool,
|
|
200
201
|
};
|
|
201
202
|
}
|
|
@@ -278,6 +279,7 @@ export function codeExecutionPiToolCandidate(
|
|
|
278
279
|
return {
|
|
279
280
|
owner: "core:codemode",
|
|
280
281
|
requiredExecutionLevel: "safe",
|
|
282
|
+
retry: "idempotent",
|
|
281
283
|
outputBudget: { kind: "structure" },
|
|
282
284
|
source: "codemode",
|
|
283
285
|
summary: "Run JavaScript with network and configured connector access",
|
package/src/pi/tool/schedule.ts
CHANGED
|
@@ -92,7 +92,7 @@ function candidate<T extends TSchema>(
|
|
|
92
92
|
options: Partial<
|
|
93
93
|
Pick<
|
|
94
94
|
PiToolCandidate,
|
|
95
|
-
"alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "summary"
|
|
95
|
+
"alwaysRequiresApproval" | "exposureMode" | "owner" | "requiredExecutionLevel" | "retry" | "summary"
|
|
96
96
|
>
|
|
97
97
|
> = {},
|
|
98
98
|
): PiToolCandidate {
|
|
@@ -106,6 +106,7 @@ function candidate<T extends TSchema>(
|
|
|
106
106
|
? { alwaysRequiresApproval: true }
|
|
107
107
|
: {}),
|
|
108
108
|
...(options.summary ? { summary: options.summary } : {}),
|
|
109
|
+
...(options.retry ? { retry: options.retry } : {}),
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
112
|
|
|
@@ -138,17 +139,20 @@ export function schedulePiToolCandidates(
|
|
|
138
139
|
summary: "Create a scheduled task",
|
|
139
140
|
},
|
|
140
141
|
),
|
|
141
|
-
candidate(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
signal
|
|
149
|
-
|
|
142
|
+
candidate(
|
|
143
|
+
{
|
|
144
|
+
name: "list_schedules",
|
|
145
|
+
label: "List schedules",
|
|
146
|
+
description:
|
|
147
|
+
"List the user's existing scheduled tasks. Call this when the user asks what they have scheduled, or before cancelling one — you need the id, and the user will refer to a task by what it does, not by its id.",
|
|
148
|
+
parameters: noParameters,
|
|
149
|
+
async execute(_toolCallId, _input, signal) {
|
|
150
|
+
signal?.throwIfAborted();
|
|
151
|
+
return result({ schedules: await schedule.list() });
|
|
152
|
+
},
|
|
150
153
|
},
|
|
151
|
-
|
|
154
|
+
{ retry: "idempotent" },
|
|
155
|
+
),
|
|
152
156
|
candidate({
|
|
153
157
|
name: "update_schedule",
|
|
154
158
|
label: "Update schedule",
|
package/src/pi/tool/skill.ts
CHANGED
|
@@ -534,6 +534,9 @@ export async function skillPiToolCandidates(
|
|
|
534
534
|
...(name === "activate_skill"
|
|
535
535
|
? { exposureMode: "direct" as const }
|
|
536
536
|
: { exposureMode: "codemode" as const }),
|
|
537
|
+
...(name === "activate_skill" || name === "read_skill_resource"
|
|
538
|
+
? { retry: "idempotent" as const }
|
|
539
|
+
: {}),
|
|
537
540
|
tool: adapted,
|
|
538
541
|
};
|
|
539
542
|
});
|
package/src/pi/tool/subagent.ts
CHANGED
|
@@ -182,6 +182,7 @@ export function subagentPiToolCandidates(
|
|
|
182
182
|
owner: `subagent:${type.name}`,
|
|
183
183
|
exposureMode: "direct",
|
|
184
184
|
requiredExecutionLevel: "safe",
|
|
185
|
+
retry: "idempotent",
|
|
185
186
|
tool,
|
|
186
187
|
};
|
|
187
188
|
});
|
|
@@ -265,6 +266,7 @@ export function subagentPiToolCandidates(
|
|
|
265
266
|
owner: "subagent:background",
|
|
266
267
|
exposureMode: "direct",
|
|
267
268
|
requiredExecutionLevel: "low",
|
|
269
|
+
retry: "idempotent",
|
|
268
270
|
summary: "Dispatch a background sub-agent",
|
|
269
271
|
source: "action",
|
|
270
272
|
tool: background,
|
package/src/pi/tool/time.ts
CHANGED
|
@@ -707,6 +707,12 @@ async function callOpenAIStream(
|
|
|
707
707
|
tools: isOpenRouter
|
|
708
708
|
? [{ type: "openrouter:web_search", parameters: { max_uses: 10 } }]
|
|
709
709
|
: [{ type: "web_search" }],
|
|
710
|
+
...(isOpenRouter
|
|
711
|
+
? {
|
|
712
|
+
max_output_tokens: Math.min(model.maxTokens, OPENROUTER_MAX_OUTPUT_TOKENS),
|
|
713
|
+
reasoning: { effort: "none" },
|
|
714
|
+
}
|
|
715
|
+
: {}),
|
|
710
716
|
...(!isOpenRouter
|
|
711
717
|
? {
|
|
712
718
|
include: isCodex
|
|
@@ -1108,6 +1114,8 @@ export function applyCitations(text: string, groundingMetadata: any): { text: st
|
|
|
1108
1114
|
|
|
1109
1115
|
const PROVIDER_ERROR_MAX_LENGTH = 2_048;
|
|
1110
1116
|
const NATIVE_TIMEOUT_MS = 60_000;
|
|
1117
|
+
const OPENROUTER_MAX_OUTPUT_TOKENS = 4_096;
|
|
1118
|
+
export const WEB_SEARCH_MODEL = "deepseek/deepseek-v4-flash";
|
|
1111
1119
|
|
|
1112
1120
|
export interface WebSearchOptions {
|
|
1113
1121
|
endpoint: RuntimeModelEndpoint;
|
|
@@ -402,6 +402,7 @@ export function workspacePiToolCandidates(
|
|
|
402
402
|
{
|
|
403
403
|
owner: "workspace",
|
|
404
404
|
requiredExecutionLevel: "safe" as const,
|
|
405
|
+
retry: "idempotent" as const,
|
|
405
406
|
// read 是大输出外置协议的**出口**:溢出结果的提示语指向它。出口自己再外置,
|
|
406
407
|
// 就会变成 read → 新 artifact → read 的死循环,模型只能靠猜逃出去。
|
|
407
408
|
// 分页上限由 `pageReadResult` 自己保证,不需要外置也不会撑爆一轮。
|
|
@@ -411,6 +412,7 @@ export function workspacePiToolCandidates(
|
|
|
411
412
|
...[write, edit, list, find, grep, remove].map((tool) => ({
|
|
412
413
|
owner: "workspace",
|
|
413
414
|
requiredExecutionLevel: "safe" as const,
|
|
415
|
+
retry: "idempotent" as const,
|
|
414
416
|
tool,
|
|
415
417
|
})),
|
|
416
418
|
];
|
|
@@ -621,6 +623,7 @@ export function sandboxPiToolCandidates(
|
|
|
621
623
|
{
|
|
622
624
|
owner: "sandbox",
|
|
623
625
|
requiredExecutionLevel: "safe",
|
|
626
|
+
retry: "idempotent",
|
|
624
627
|
tool: processLogs,
|
|
625
628
|
},
|
|
626
629
|
{
|
package/src/runtime-agent.ts
CHANGED
|
@@ -301,6 +301,7 @@ export interface RuntimeAgentControls {
|
|
|
301
301
|
requestId?: string,
|
|
302
302
|
reason?: string,
|
|
303
303
|
): Promise<{ ok: boolean }>;
|
|
304
|
+
stopAllSubmissions(reason?: string): Promise<{ ok: boolean }>;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
export interface RuntimeAgentConfigControls<Command, Change> {
|
|
@@ -525,6 +526,10 @@ export function defineRuntimeAgent<
|
|
|
525
526
|
return super.stopTurn(requestId, reason);
|
|
526
527
|
}
|
|
527
528
|
|
|
529
|
+
stopAllSubmissions(reason?: string): Promise<{ ok: boolean }> {
|
|
530
|
+
return super.stopAllSubmissions(reason);
|
|
531
|
+
}
|
|
532
|
+
|
|
528
533
|
protected override async prepareAgentToolRun(
|
|
529
534
|
input: unknown,
|
|
530
535
|
): Promise<string> {
|
|
@@ -922,6 +927,10 @@ export function defineRuntimeAgent<
|
|
|
922
927
|
callableContext,
|
|
923
928
|
);
|
|
924
929
|
callable()(GeneratedRuntimeAgent.prototype.stopTurn, callableContext);
|
|
930
|
+
callable()(
|
|
931
|
+
GeneratedRuntimeAgent.prototype.stopAllSubmissions,
|
|
932
|
+
callableContext,
|
|
933
|
+
);
|
|
925
934
|
|
|
926
935
|
return GeneratedRuntimeAgent as unknown as RuntimeAgentClass<
|
|
927
936
|
Env,
|