@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
package/package.json
CHANGED
|
@@ -19,7 +19,6 @@ import type {
|
|
|
19
19
|
import { PiChunkEncoder } from "../../../pi/message";
|
|
20
20
|
import {
|
|
21
21
|
createPiModels,
|
|
22
|
-
resolvePiApiKey,
|
|
23
22
|
resolvePiModel,
|
|
24
23
|
withProviderRetry,
|
|
25
24
|
} from "../../../pi/runtime-adapter/models";
|
|
@@ -398,8 +397,6 @@ export abstract class CloudflareSubAgent<
|
|
|
398
397
|
input,
|
|
399
398
|
model,
|
|
400
399
|
streamFn: withProviderRetry(models.streamSimple.bind(models)),
|
|
401
|
-
getApiKey: () =>
|
|
402
|
-
resolvePiApiKey(provider, provider.defaultModel),
|
|
403
400
|
tools,
|
|
404
401
|
signal,
|
|
405
402
|
onEvent: (event) => {
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { assembleSubagentPrompt } from "../../../lib/prompt";
|
|
14
14
|
import type { AgentType } from "../../../layers/orchestration/subagents/agent-types/contract";
|
|
15
|
+
import { structuredOutputStream } from "../../../pi/runtime-adapter/models";
|
|
15
16
|
|
|
16
17
|
const RECURSIVE_TOOLS = new Set([
|
|
17
18
|
"run_agent",
|
|
@@ -28,7 +29,6 @@ export interface CloudflareSubAgentRun {
|
|
|
28
29
|
model: Model<Api>;
|
|
29
30
|
streamFn: StreamFn;
|
|
30
31
|
tools: AgentTool[];
|
|
31
|
-
getApiKey?: (provider: string) => string | undefined;
|
|
32
32
|
signal?: AbortSignal;
|
|
33
33
|
timeoutMs?: number;
|
|
34
34
|
onEvent?: (event: AgentEvent, signal: AbortSignal) => void | Promise<void>;
|
|
@@ -67,11 +67,16 @@ function toolErrorText(result: unknown): string {
|
|
|
67
67
|
return "tool execution failed";
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function taskPrompt(
|
|
70
|
+
function taskPrompt(
|
|
71
|
+
input: unknown,
|
|
72
|
+
outputSchema?: Record<string, unknown>,
|
|
73
|
+
): string {
|
|
71
74
|
return [
|
|
72
75
|
"Complete this bounded task.",
|
|
73
76
|
`Input:\n${JSON.stringify(input)}`,
|
|
74
|
-
|
|
77
|
+
outputSchema
|
|
78
|
+
? `Return only one JSON object matching this schema:\n${JSON.stringify(outputSchema)}`
|
|
79
|
+
: "Return only one JSON object matching the configured output schema.",
|
|
75
80
|
].join("\n\n");
|
|
76
81
|
}
|
|
77
82
|
|
|
@@ -90,13 +95,18 @@ export async function runCloudflareSubAgent(
|
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
if (run.signal?.aborted) throw new Error("SubAgent run aborted");
|
|
98
|
+
const { $schema: _, ...outputSchema } = z.toJSONSchema(run.type.outputSchema);
|
|
99
|
+
const constrainedStream = structuredOutputStream(
|
|
100
|
+
run.streamFn,
|
|
101
|
+
run.model,
|
|
102
|
+
outputSchema,
|
|
103
|
+
);
|
|
93
104
|
|
|
94
105
|
// 这里没有审批闸,也装不了:到这一步工具已经被 compilePiTools 编译成 AgentTool,
|
|
95
106
|
// requiredExecutionLevel 只挂在编译前的 PiToolCandidate 上,这里读不到。「子 agent 不许挂需审批的工具」
|
|
96
107
|
// 这条不变量由装配处 createCloudflareSubAgentTools 的启动期断言强制执行。
|
|
97
108
|
const agent = new Agent({
|
|
98
|
-
streamFn: run.streamFn,
|
|
99
|
-
getApiKey: run.getApiKey,
|
|
109
|
+
streamFn: constrainedStream ?? run.streamFn,
|
|
100
110
|
initialState: {
|
|
101
111
|
model: run.model,
|
|
102
112
|
systemPrompt: assembleSubagentPrompt(run.type.persona),
|
|
@@ -125,7 +135,10 @@ export async function runCloudflareSubAgent(
|
|
|
125
135
|
}, run.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
126
136
|
|
|
127
137
|
try {
|
|
128
|
-
await agent.prompt(taskPrompt(
|
|
138
|
+
await agent.prompt(taskPrompt(
|
|
139
|
+
input.data,
|
|
140
|
+
constrainedStream ? undefined : outputSchema,
|
|
141
|
+
));
|
|
129
142
|
} finally {
|
|
130
143
|
clearTimeout(timeout);
|
|
131
144
|
run.signal?.removeEventListener("abort", abort);
|
package/src/db/schema.ts
CHANGED
|
@@ -25,7 +25,8 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
|
|
|
25
25
|
assistant_message_id TEXT NOT NULL,
|
|
26
26
|
abort_reason TEXT,
|
|
27
27
|
recovery_error_count INTEGER NOT NULL DEFAULT 0,
|
|
28
|
-
recovery_reason TEXT
|
|
28
|
+
recovery_reason TEXT,
|
|
29
|
+
context_overflow_retried INTEGER NOT NULL DEFAULT 0
|
|
29
30
|
)`;
|
|
30
31
|
const submissionColumns = new Set(
|
|
31
32
|
sql<{ name: string }>`PRAGMA table_info(pi_submissions)`
|
|
@@ -77,6 +78,10 @@ export function initializeSchema(sql: SqlTaggedTemplate): void {
|
|
|
77
78
|
if (!submissionColumns.has("recovery_reason")) {
|
|
78
79
|
sql`ALTER TABLE pi_submissions ADD COLUMN recovery_reason TEXT`;
|
|
79
80
|
}
|
|
81
|
+
if (!submissionColumns.has("context_overflow_retried")) {
|
|
82
|
+
sql`ALTER TABLE pi_submissions
|
|
83
|
+
ADD COLUMN context_overflow_retried INTEGER NOT NULL DEFAULT 0`;
|
|
84
|
+
}
|
|
80
85
|
sql`CREATE UNIQUE INDEX IF NOT EXISTS pi_submissions_one_running
|
|
81
86
|
ON pi_submissions(status)
|
|
82
87
|
WHERE status = 'running'`;
|
|
@@ -11,7 +11,8 @@ export type SubmissionStatus =
|
|
|
11
11
|
| "error";
|
|
12
12
|
export type SubmissionRecoveryReason =
|
|
13
13
|
| "no_meaningful_model_progress"
|
|
14
|
-
| "transient_model_error"
|
|
14
|
+
| "transient_model_error"
|
|
15
|
+
| "context_overflow";
|
|
15
16
|
|
|
16
17
|
const TERMINAL_SUBMISSION_STATUSES: ReadonlySet<SubmissionStatus> = new Set([
|
|
17
18
|
"completed",
|
|
@@ -422,6 +423,17 @@ export class SubmissionRepository {
|
|
|
422
423
|
`[0]?.recovery_error_count ?? 0;
|
|
423
424
|
}
|
|
424
425
|
|
|
426
|
+
claimContextOverflowRecovery(id: string): boolean {
|
|
427
|
+
return Boolean(this.sql<{ claimed: number }>`
|
|
428
|
+
UPDATE pi_submissions
|
|
429
|
+
SET context_overflow_retried = 1
|
|
430
|
+
WHERE submission_id = ${id}
|
|
431
|
+
AND status IN ('pending', 'running')
|
|
432
|
+
AND context_overflow_retried = 0
|
|
433
|
+
RETURNING 1 AS claimed
|
|
434
|
+
`[0]?.claimed);
|
|
435
|
+
}
|
|
436
|
+
|
|
425
437
|
/**
|
|
426
438
|
* 记录一个让步执行片的累计模型回合数和续跑标识。
|
|
427
439
|
*
|
package/src/index.ts
CHANGED
package/src/kernel/bindings.ts
CHANGED
|
@@ -855,10 +855,6 @@ export interface RuntimeTurnEventsPort {
|
|
|
855
855
|
readonly submissionId: string;
|
|
856
856
|
readonly approvalExecutionId: string;
|
|
857
857
|
}): Promise<void>;
|
|
858
|
-
/**
|
|
859
|
-
* 至少一次投当前 Session Activity;Host 应按 revision 幂等处理重试。
|
|
860
|
-
* Runtime 恢复时会略过已过时的 revision,避免重放旧 working 状态。
|
|
861
|
-
*/
|
|
862
858
|
onActivityChanged?(
|
|
863
859
|
projection: RuntimeActivityProjection,
|
|
864
860
|
): Promise<void>;
|
|
@@ -92,14 +92,7 @@ export interface SubmissionStore<TSubmission extends SubmissionRecord> {
|
|
|
92
92
|
countPending(): number;
|
|
93
93
|
findRunning(): TSubmission | null;
|
|
94
94
|
findNextPending(): TSubmission | null;
|
|
95
|
-
|
|
96
|
-
* 列出全部等待或运行中的提交标识。
|
|
97
|
-
*
|
|
98
|
-
* @remarks
|
|
99
|
-
* `stop` 没有指定请求时调用,并逐条走统一取消路径。
|
|
100
|
-
*
|
|
101
|
-
* 只返回标识可以避免停止入口复制完整记录或状态判断。
|
|
102
|
-
*/
|
|
95
|
+
listPending(): TSubmission[];
|
|
103
96
|
/**
|
|
104
97
|
* 持久化一条取消原因。
|
|
105
98
|
*
|
|
@@ -694,5 +687,29 @@ export class SubmissionLifecycle<
|
|
|
694
687
|
return { ok: submissionIds.length > 0 };
|
|
695
688
|
}
|
|
696
689
|
|
|
690
|
+
/** Stops every running or queued Submission without pumping between them. */
|
|
691
|
+
async stopAll(reason = "Stopped"): Promise<{ ok: boolean }> {
|
|
692
|
+
const running = this.options.store.findRunning();
|
|
693
|
+
const submissions = [
|
|
694
|
+
...(running ? [running] : []),
|
|
695
|
+
...this.options.store.listPending(),
|
|
696
|
+
];
|
|
697
|
+
if (submissions.length === 0) return { ok: false };
|
|
698
|
+
|
|
699
|
+
this.options.store.transaction(() => {
|
|
700
|
+
for (const submission of submissions) {
|
|
701
|
+
this.options.store.updateAbortReason(submission.submissionId, reason);
|
|
702
|
+
this.options.appendAbortIntent(submission, reason);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
for (const submission of submissions) {
|
|
706
|
+
const active = this.activeBySubmission.get(submission.submissionId);
|
|
707
|
+
if (active) this.options.abortActive(active);
|
|
708
|
+
await this.finish(submission, "aborted", reason);
|
|
709
|
+
}
|
|
710
|
+
this.pump();
|
|
711
|
+
return { ok: true };
|
|
712
|
+
}
|
|
713
|
+
|
|
697
714
|
// #endregion
|
|
698
715
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
compact as compactPiTranscript,
|
|
4
4
|
DEFAULT_COMPACTION_SETTINGS,
|
|
5
5
|
estimateContextTokens,
|
|
6
|
+
estimateTokens,
|
|
6
7
|
prepareCompaction,
|
|
7
8
|
type AgentMessage,
|
|
8
9
|
type CompactionEntry,
|
|
@@ -86,9 +87,9 @@ function sumUsage(responses: readonly AssistantMessage[]): Usage | undefined {
|
|
|
86
87
|
export async function compactPiContext(input: Readonly<{
|
|
87
88
|
branch: readonly SessionTreeEntry[];
|
|
88
89
|
compactAfterTokens: number;
|
|
90
|
+
force?: boolean;
|
|
89
91
|
models: Models;
|
|
90
92
|
model: Model<any>;
|
|
91
|
-
apiKey?: string;
|
|
92
93
|
signal?: AbortSignal;
|
|
93
94
|
}>): Promise<{
|
|
94
95
|
messages: AgentMessage[];
|
|
@@ -116,8 +117,8 @@ export async function compactPiContext(input: Readonly<{
|
|
|
116
117
|
const settings = DEFAULT_COMPACTION_SETTINGS;
|
|
117
118
|
if (
|
|
118
119
|
!settings.enabled ||
|
|
119
|
-
estimateContextTokens(currentMessages).tokens <
|
|
120
|
-
input.compactAfterTokens
|
|
120
|
+
(!input.force && estimateContextTokens(currentMessages).tokens <
|
|
121
|
+
input.compactAfterTokens)
|
|
121
122
|
) {
|
|
122
123
|
return {
|
|
123
124
|
messages: currentMessages,
|
|
@@ -149,10 +150,7 @@ export async function compactPiContext(input: Readonly<{
|
|
|
149
150
|
context: Parameters<Models["completeSimple"]>[1],
|
|
150
151
|
options?: Parameters<Models["completeSimple"]>[2],
|
|
151
152
|
) => {
|
|
152
|
-
const response = await input.models.completeSimple(model, context,
|
|
153
|
-
...options,
|
|
154
|
-
...(input.apiKey ? { apiKey: input.apiKey } : {}),
|
|
155
|
-
});
|
|
153
|
+
const response = await input.models.completeSimple(model, context, options);
|
|
156
154
|
responses.push(response);
|
|
157
155
|
return response;
|
|
158
156
|
},
|
|
@@ -199,10 +197,25 @@ export async function compactPiContext(input: Readonly<{
|
|
|
199
197
|
? firstResponse.stopReason
|
|
200
198
|
: undefined;
|
|
201
199
|
const usage = sumUsage(responses);
|
|
200
|
+
const messages = buildSessionContext(
|
|
201
|
+
[...branch, previewEntry],
|
|
202
|
+
).messages;
|
|
203
|
+
if (
|
|
204
|
+
input.force &&
|
|
205
|
+
messages.reduce((total, message) => total + estimateTokens(message), 0) >=
|
|
206
|
+
currentMessages.reduce(
|
|
207
|
+
(total, message) => total + estimateTokens(message),
|
|
208
|
+
0,
|
|
209
|
+
)
|
|
210
|
+
) {
|
|
211
|
+
return {
|
|
212
|
+
messages: currentMessages,
|
|
213
|
+
didCompact: false,
|
|
214
|
+
degraded: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
202
217
|
return {
|
|
203
|
-
messages
|
|
204
|
-
[...branch, previewEntry],
|
|
205
|
-
).messages,
|
|
218
|
+
messages,
|
|
206
219
|
compactionEntry,
|
|
207
220
|
...(firstResponse && usage
|
|
208
221
|
? {
|
|
@@ -2,8 +2,11 @@ import type {
|
|
|
2
2
|
AgentMessage,
|
|
3
3
|
ThinkingLevel,
|
|
4
4
|
} from "@earendil-works/pi-agent-core";
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
clampThinkingLevel,
|
|
7
|
+
type Api,
|
|
8
|
+
type Model,
|
|
9
|
+
} from "@earendil-works/pi-ai";
|
|
7
10
|
import type {
|
|
8
11
|
RuntimeMcpServer,
|
|
9
12
|
RuntimeProfile,
|
|
@@ -84,15 +87,14 @@ function freezeModel(model: Model<Api>): Model<Api> {
|
|
|
84
87
|
});
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
// 作用:把 Runtime
|
|
88
|
-
// 调用:创建 Runtime
|
|
89
|
-
// 原因:`none` 映射成 `off`;xhigh/max 用 clampReasoning 夹到 API 真正支持的最高档,
|
|
90
|
-
// 避免超出范围的等级在 provider 侧静默失败。
|
|
90
|
+
// 作用:把 Runtime 的思考强度夹到当前模型真正支持的 Pi 档位。
|
|
91
|
+
// 调用:创建 Runtime 装配快照时对已解析模型和 profile `thinking` 调用。
|
|
91
92
|
function toPiThinkingLevel(
|
|
93
|
+
model: Model<Api>,
|
|
92
94
|
thinking: ThinkingEffort,
|
|
93
95
|
): ThinkingLevel {
|
|
94
96
|
if (thinking === "none") return "off";
|
|
95
|
-
return
|
|
97
|
+
return clampThinkingLevel(model, thinking);
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
// 作用:复制并冻结一个 Extension 配置的 manifest 和权限集合。
|
|
@@ -197,10 +199,14 @@ export function createPiRuntimeAssembly(
|
|
|
197
199
|
Object.freeze(messages);
|
|
198
200
|
Object.freeze(mcpServers);
|
|
199
201
|
Object.freeze(extensions);
|
|
202
|
+
const model = freezeModel(
|
|
203
|
+
resolvePiModel(input.provider, input.profile.model),
|
|
204
|
+
);
|
|
200
205
|
|
|
201
206
|
return Object.freeze({
|
|
202
|
-
model
|
|
207
|
+
model,
|
|
203
208
|
thinkingLevel: toPiThinkingLevel(
|
|
209
|
+
model,
|
|
204
210
|
input.profile.thinking,
|
|
205
211
|
),
|
|
206
212
|
systemPrompt: assembleSystemPrompt(
|
|
@@ -43,7 +43,15 @@ function userContent(
|
|
|
43
43
|
data: inlineImage[2]!,
|
|
44
44
|
});
|
|
45
45
|
} else {
|
|
46
|
-
content.push(
|
|
46
|
+
content.push(
|
|
47
|
+
{ type: "text", text: attachmentText(part) },
|
|
48
|
+
{
|
|
49
|
+
type: "file",
|
|
50
|
+
url: part.url,
|
|
51
|
+
mimeType: part.mediaType,
|
|
52
|
+
...(part.filename ? { filename: part.filename } : {}),
|
|
53
|
+
},
|
|
54
|
+
);
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
if (content.length === 0) throw new Error("User message content is required");
|
|
@@ -123,17 +123,40 @@ function toolCallAt(
|
|
|
123
123
|
return part?.type === "toolCall" ? part : undefined;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
/** The one interpretation of a non-Tool Pi assistant terminal. */
|
|
127
|
+
export function classifyPiTerminalStopReason(
|
|
128
|
+
stopReason: string | undefined,
|
|
129
|
+
): {
|
|
130
|
+
outcome: "succeeded" | "failed" | "aborted";
|
|
131
|
+
turnStatus: "completed" | "error" | "aborted";
|
|
132
|
+
finishReason?: FinishReason;
|
|
133
|
+
message?: string;
|
|
134
|
+
} {
|
|
135
|
+
switch (stopReason) {
|
|
128
136
|
case "stop":
|
|
129
137
|
case "length":
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
return {
|
|
139
|
+
outcome: "succeeded",
|
|
140
|
+
turnStatus: "completed",
|
|
141
|
+
finishReason: stopReason,
|
|
142
|
+
};
|
|
143
|
+
case "aborted":
|
|
144
|
+
return { outcome: "aborted", turnStatus: "aborted" };
|
|
133
145
|
case "error":
|
|
134
|
-
return
|
|
146
|
+
return {
|
|
147
|
+
outcome: "failed",
|
|
148
|
+
turnStatus: "error",
|
|
149
|
+
finishReason: "error",
|
|
150
|
+
};
|
|
135
151
|
default:
|
|
136
|
-
return
|
|
152
|
+
return {
|
|
153
|
+
outcome: "failed",
|
|
154
|
+
turnStatus: "error",
|
|
155
|
+
finishReason: "error",
|
|
156
|
+
message: `SpringBrand ended the turn with an unrecognized stop reason: ${
|
|
157
|
+
stopReason ?? "(none)"
|
|
158
|
+
}`,
|
|
159
|
+
};
|
|
137
160
|
}
|
|
138
161
|
}
|
|
139
162
|
|
|
@@ -345,8 +368,15 @@ export class PiChunkEncoder {
|
|
|
345
368
|
update.error.errorMessage ?? update.reason,
|
|
346
369
|
) ?? "SpringBrand turn failed",
|
|
347
370
|
}];
|
|
348
|
-
|
|
371
|
+
case "start":
|
|
372
|
+
case "done":
|
|
349
373
|
return [];
|
|
374
|
+
default:
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Unsupported SpringBrand AssistantMessageEvent: ${
|
|
377
|
+
String((update as { type?: unknown }).type)
|
|
378
|
+
}`,
|
|
379
|
+
);
|
|
350
380
|
}
|
|
351
381
|
}
|
|
352
382
|
|
|
@@ -448,43 +478,39 @@ export class PiChunkEncoder {
|
|
|
448
478
|
}
|
|
449
479
|
this.finished = true;
|
|
450
480
|
const publicError = publicAssistantError(message.errorMessage);
|
|
481
|
+
const terminal = classifyPiTerminalStopReason(message.stopReason);
|
|
451
482
|
if (this.startedAt !== undefined) {
|
|
452
|
-
const turnStatus = message.stopReason === "aborted"
|
|
453
|
-
? "aborted"
|
|
454
|
-
: message.stopReason === "error"
|
|
455
|
-
? "error"
|
|
456
|
-
: "completed";
|
|
457
483
|
chunks.push({
|
|
458
484
|
type: "message-metadata",
|
|
459
485
|
messageMetadata: {
|
|
460
486
|
createdAt: this.startedAt,
|
|
461
487
|
completedAt: message.timestamp,
|
|
462
488
|
turnDurationMs: Math.max(0, message.timestamp - this.startedAt),
|
|
463
|
-
turnStatus,
|
|
489
|
+
turnStatus: terminal.turnStatus,
|
|
464
490
|
...(this.turnId ? { turnId: this.turnId } : {}),
|
|
465
|
-
...(publicError &&
|
|
466
|
-
(
|
|
491
|
+
...((publicError ?? terminal.message) &&
|
|
492
|
+
(terminal.outcome !== "aborted" ||
|
|
467
493
|
message.errorMessage !== USER_STOP_REASON)
|
|
468
|
-
? { error: publicError }
|
|
494
|
+
? { error: publicError ?? terminal.message }
|
|
469
495
|
: {}),
|
|
470
496
|
},
|
|
471
497
|
});
|
|
472
498
|
}
|
|
473
|
-
if (
|
|
499
|
+
if (terminal.outcome === "aborted") {
|
|
474
500
|
chunks.push({ type: "abort", reason: message.errorMessage });
|
|
475
501
|
return chunks;
|
|
476
502
|
}
|
|
477
|
-
if (
|
|
503
|
+
if (terminal.outcome === "failed") {
|
|
478
504
|
chunks.push({
|
|
479
505
|
type: "error",
|
|
480
|
-
errorText: publicError ?? "SpringBrand turn failed",
|
|
506
|
+
errorText: publicError ?? terminal.message ?? "SpringBrand turn failed",
|
|
481
507
|
});
|
|
482
508
|
return chunks;
|
|
483
509
|
}
|
|
484
510
|
chunks.push({ type: "finish-step" });
|
|
485
511
|
chunks.push({
|
|
486
512
|
type: "finish",
|
|
487
|
-
finishReason: finishReason
|
|
513
|
+
finishReason: terminal.finishReason ?? "other",
|
|
488
514
|
});
|
|
489
515
|
return chunks;
|
|
490
516
|
}
|
|
@@ -200,46 +200,18 @@ export interface PreparedPiRuntimeState {
|
|
|
200
200
|
|
|
201
201
|
// #region 版本描述与准备句柄
|
|
202
202
|
|
|
203
|
-
const IDEMPOTENT_TOOL_NAMES = new Set([
|
|
204
|
-
"activate_skill",
|
|
205
|
-
"browser_extract",
|
|
206
|
-
"browser_links",
|
|
207
|
-
"browser_markdown",
|
|
208
|
-
"browser_scrape",
|
|
209
|
-
"bind_resource",
|
|
210
|
-
"delete",
|
|
211
|
-
"edit",
|
|
212
|
-
"execute",
|
|
213
|
-
"find",
|
|
214
|
-
"get_time",
|
|
215
|
-
"grep",
|
|
216
|
-
"list",
|
|
217
|
-
"list_resources",
|
|
218
|
-
"list_extensions",
|
|
219
|
-
"list_schedules",
|
|
220
|
-
"read",
|
|
221
|
-
"read_skill_resource",
|
|
222
|
-
"sandbox_process_logs",
|
|
223
|
-
"web_search",
|
|
224
|
-
"write",
|
|
225
|
-
"unbind_resource",
|
|
226
|
-
]);
|
|
227
|
-
|
|
228
203
|
/**
|
|
229
204
|
* 判断工具在执行结果不确定时能否安全重试。
|
|
230
205
|
*
|
|
231
206
|
* @remarks
|
|
232
207
|
* Turn 执行在记录第一次尝试前调用它,恢复流程随后使用这条持久化结论。
|
|
233
208
|
*
|
|
234
|
-
*
|
|
209
|
+
* 重试语义由 Tool Candidate 自己声明;未声明的工具失败关闭为不可重复执行。
|
|
235
210
|
*/
|
|
236
211
|
export function piToolRetryPolicy(
|
|
237
212
|
candidate: PiToolCandidate,
|
|
238
213
|
): "idempotent" | "non-idempotent" {
|
|
239
|
-
return candidate.retry ??
|
|
240
|
-
candidate.owner.startsWith("subagent:")
|
|
241
|
-
? "idempotent"
|
|
242
|
-
: "non-idempotent");
|
|
214
|
+
return candidate.retry ?? "non-idempotent";
|
|
243
215
|
}
|
|
244
216
|
|
|
245
217
|
// 把工具候选整理成版本描述里要保存的稳定字段。
|