@springbrand/agent-runtime 0.2.0-alpha.56 → 0.2.0-alpha.58
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/index.ts +1 -0
- package/src/pi/message/conversion.ts +8 -3
- package/src/pi/runtime-adapter/execution.ts +6 -1
- package/src/pi/runtime-adapter/models.ts +27 -0
- package/src/pi/tool/base.ts +123 -14
- package/src/runtime.ts +46 -5
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ export * from "./kernel/runtime-config";
|
|
|
50
50
|
export * from "./telemetry";
|
|
51
51
|
export type { RuntimeAssemblyView } from "./kernel/runtime-assembly-view";
|
|
52
52
|
export * from "./runtime-agent";
|
|
53
|
+
export type { ToolInteractionResponsePreparation } from "./runtime";
|
|
53
54
|
export * from "./kernel/state";
|
|
54
55
|
export * from "./workspace-versioning";
|
|
55
56
|
export * from "./lib/execution-level";
|
|
@@ -17,8 +17,13 @@ function timestampOf(message: UIMessage): number {
|
|
|
17
17
|
return typeof createdAt === "number" ? createdAt : Date.now();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
21
|
-
part:
|
|
20
|
+
export function modelAttachmentText(
|
|
21
|
+
part: {
|
|
22
|
+
readonly url: string;
|
|
23
|
+
readonly filename?: string;
|
|
24
|
+
readonly mediaType: string;
|
|
25
|
+
readonly providerMetadata?: unknown;
|
|
26
|
+
},
|
|
22
27
|
): string {
|
|
23
28
|
const name = part.filename?.trim() || "attachment";
|
|
24
29
|
const springbrand = recordOf(recordOf(part.providerMetadata).springbrand);
|
|
@@ -66,7 +71,7 @@ function userContent(
|
|
|
66
71
|
});
|
|
67
72
|
} else {
|
|
68
73
|
content.push(
|
|
69
|
-
{ type: "text", text:
|
|
74
|
+
{ type: "text", text: modelAttachmentText(part) },
|
|
70
75
|
{
|
|
71
76
|
type: "file",
|
|
72
77
|
url: part.url,
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "./assembly";
|
|
46
46
|
import {
|
|
47
47
|
isRecoverableAssistantError,
|
|
48
|
+
isOpenRouterServerToolTimeout,
|
|
48
49
|
isModelStreamStallMessage,
|
|
49
50
|
withProviderRetry,
|
|
50
51
|
type PiGenerationLifecycleObserver,
|
|
@@ -490,7 +491,7 @@ export interface PiTurnDurability {
|
|
|
490
491
|
export interface CreatePreparedPiTurnOptions {
|
|
491
492
|
readonly prepared: PreparedPiRuntime;
|
|
492
493
|
readonly pinnedDescriptor: string;
|
|
493
|
-
/**
|
|
494
|
+
/** Session 当前的稳定 Provider 路由身份;终态 Provider 故障可在下一 Submission 前旋转它。 */
|
|
494
495
|
readonly modelSessionId: string;
|
|
495
496
|
readonly submission: {
|
|
496
497
|
readonly id: string;
|
|
@@ -609,6 +610,7 @@ export type PiCanonicalMessageCommit =
|
|
|
609
610
|
export interface PiTurnTerminalIntent {
|
|
610
611
|
readonly outcome: "succeeded" | "failed" | "aborted";
|
|
611
612
|
readonly message?: string;
|
|
613
|
+
readonly resetModelSession?: boolean;
|
|
612
614
|
}
|
|
613
615
|
|
|
614
616
|
export interface PiPreparedTurnRunOptions {
|
|
@@ -1134,6 +1136,9 @@ export class PreparedPiTurnAdapter {
|
|
|
1134
1136
|
);
|
|
1135
1137
|
this.terminalIntent = {
|
|
1136
1138
|
outcome: terminal.outcome,
|
|
1139
|
+
...(isOpenRouterServerToolTimeout(authoritativeMessage)
|
|
1140
|
+
? { resetModelSession: true }
|
|
1141
|
+
: {}),
|
|
1137
1142
|
...(authoritativeMessage.errorMessage
|
|
1138
1143
|
? { message: authoritativeMessage.errorMessage }
|
|
1139
1144
|
: terminal.message
|
|
@@ -182,6 +182,32 @@ export function readModelStreamStallDetails(
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
export function isOpenRouterServerToolTimeout(
|
|
186
|
+
message: AssistantMessage,
|
|
187
|
+
): boolean {
|
|
188
|
+
if (
|
|
189
|
+
message.stopReason !== "error" ||
|
|
190
|
+
!message.provider.startsWith("model-api-openrouter-messages-") ||
|
|
191
|
+
!message.errorMessage
|
|
192
|
+
) return false;
|
|
193
|
+
try {
|
|
194
|
+
const payload = JSON.parse(message.errorMessage) as {
|
|
195
|
+
type?: unknown;
|
|
196
|
+
error?: {
|
|
197
|
+
type?: unknown;
|
|
198
|
+
message?: unknown;
|
|
199
|
+
error_type?: unknown;
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
return payload.type === "error" &&
|
|
203
|
+
payload.error?.type === "timeout_error" &&
|
|
204
|
+
payload.error.message === "Server tool request failed" &&
|
|
205
|
+
payload.error.error_type === "timeout";
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
185
211
|
export function isRecoverableAssistantError(
|
|
186
212
|
message: AssistantMessage,
|
|
187
213
|
): boolean {
|
|
@@ -189,6 +215,7 @@ export function isRecoverableAssistantError(
|
|
|
189
215
|
message.stopReason === "error" &&
|
|
190
216
|
/thought[_ ]signature/i.test(message.errorMessage ?? "")
|
|
191
217
|
) return false;
|
|
218
|
+
if (isOpenRouterServerToolTimeout(message)) return false;
|
|
192
219
|
return isRetryableAssistantError(message) ||
|
|
193
220
|
(message.stopReason === "error" &&
|
|
194
221
|
/upstream http\/2 stream failed/i.test(message.errorMessage ?? ""));
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -12,11 +12,12 @@ import { estimateStringTokens } from "agents/experimental/memory/utils";
|
|
|
12
12
|
import type { RuntimeMemoryPort } from "../../kernel/bindings";
|
|
13
13
|
import type { RuntimeMemoryProfile } from "../../kernel/profile";
|
|
14
14
|
import { serializeOutput } from "../../lib/artifacts";
|
|
15
|
+
import { modelAttachmentText } from "../message/conversion";
|
|
15
16
|
import type { PiToolCandidate } from "./compiler";
|
|
16
17
|
import { webSearchPiToolCandidate } from "./web-search";
|
|
17
18
|
import type { WebSearch } from "./web-search/api";
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const askUserOrdinaryQuestion = Type.Object({
|
|
20
21
|
question: Type.String({
|
|
21
22
|
minLength: 1,
|
|
22
23
|
description: "The question to put to the user.",
|
|
@@ -38,12 +39,31 @@ const askUserQuestion = Type.Object({
|
|
|
38
39
|
})),
|
|
39
40
|
}, { additionalProperties: false });
|
|
40
41
|
|
|
42
|
+
const askUserAttachmentQuestion = Type.Object({
|
|
43
|
+
question: Type.String({
|
|
44
|
+
minLength: 1,
|
|
45
|
+
description: "The request for one or more user files.",
|
|
46
|
+
}),
|
|
47
|
+
kind: Type.Literal("attachment", {
|
|
48
|
+
description:
|
|
49
|
+
"Use attachment when the user must upload one or more files to continue.",
|
|
50
|
+
}),
|
|
51
|
+
}, { additionalProperties: false });
|
|
52
|
+
|
|
53
|
+
const askUserQuestion = Type.Union([
|
|
54
|
+
askUserOrdinaryQuestion,
|
|
55
|
+
askUserAttachmentQuestion,
|
|
56
|
+
]);
|
|
57
|
+
|
|
41
58
|
const askUserParameters = Type.Object({
|
|
42
59
|
questions: Type.Array(askUserQuestion, {
|
|
43
60
|
minItems: 1,
|
|
44
61
|
maxItems: 10,
|
|
62
|
+
contains: askUserAttachmentQuestion,
|
|
63
|
+
minContains: 0,
|
|
64
|
+
maxContains: 1,
|
|
45
65
|
description:
|
|
46
|
-
"All user decisions needed to continue. Ask them together in one call.",
|
|
66
|
+
"All user decisions and files needed to continue. Ask them together in one call. At most one question may have kind attachment.",
|
|
47
67
|
}),
|
|
48
68
|
}, { additionalProperties: false });
|
|
49
69
|
|
|
@@ -53,15 +73,20 @@ const askUserParameters = Type.Object({
|
|
|
53
73
|
* `answers` 与 Tool 输入的 `questions` 按下标一一对应。
|
|
54
74
|
* 刻意用结构化数组而不是拼好的字符串 —— 选项本身可能含分隔符,拼了再拆是有损的。
|
|
55
75
|
*/
|
|
56
|
-
const askUserResponse = Type.Object({
|
|
57
|
-
answers: Type.Array(Type.Object({
|
|
58
|
-
selections: Type.Array(Type.String()),
|
|
59
|
-
text: Type.Optional(Type.String()),
|
|
60
|
-
}, { additionalProperties: false })),
|
|
61
|
-
}, { additionalProperties: false });
|
|
62
|
-
|
|
63
76
|
type AskUserInput = Static<typeof askUserParameters>;
|
|
64
|
-
type
|
|
77
|
+
type AskUserFile = {
|
|
78
|
+
readonly type: "file";
|
|
79
|
+
readonly url: string;
|
|
80
|
+
readonly filename: string;
|
|
81
|
+
readonly mediaType: string;
|
|
82
|
+
readonly providerMetadata?: unknown;
|
|
83
|
+
};
|
|
84
|
+
type AskUserResponse = {
|
|
85
|
+
readonly answers: ReadonlyArray<
|
|
86
|
+
| { readonly selections: readonly string[]; readonly text?: string }
|
|
87
|
+
| { readonly attachments: readonly AskUserFile[] }
|
|
88
|
+
>;
|
|
89
|
+
};
|
|
65
90
|
|
|
66
91
|
function recordOf(value: unknown): Record<string, unknown> | null {
|
|
67
92
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
@@ -74,11 +99,23 @@ function questionsOf(value: unknown): AskUserInput["questions"] | null {
|
|
|
74
99
|
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 10) {
|
|
75
100
|
return null;
|
|
76
101
|
}
|
|
102
|
+
let attachmentQuestions = 0;
|
|
77
103
|
for (const value of questions) {
|
|
78
104
|
const question = recordOf(value);
|
|
79
105
|
if (!question || typeof question.question !== "string" || !question.question) {
|
|
80
106
|
return null;
|
|
81
107
|
}
|
|
108
|
+
if ("kind" in question && question.kind === "attachment") {
|
|
109
|
+
attachmentQuestions += 1;
|
|
110
|
+
if (
|
|
111
|
+
attachmentQuestions > 1 ||
|
|
112
|
+
question.options !== undefined ||
|
|
113
|
+
question.multiSelect !== undefined ||
|
|
114
|
+
question.allowCustom !== undefined
|
|
115
|
+
) return null;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (question.kind !== undefined) return null;
|
|
82
119
|
if (
|
|
83
120
|
question.multiSelect !== undefined &&
|
|
84
121
|
typeof question.multiSelect !== "boolean"
|
|
@@ -113,7 +150,35 @@ function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
|
113
150
|
|
|
114
151
|
return questions.every((question, index) => {
|
|
115
152
|
const answer = recordOf(answers[index]);
|
|
153
|
+
if ("kind" in question && question.kind === "attachment") {
|
|
154
|
+
const attachments = answer?.attachments;
|
|
155
|
+
if (
|
|
156
|
+
!answer ||
|
|
157
|
+
!Array.isArray(attachments) ||
|
|
158
|
+
attachments.length < 1 ||
|
|
159
|
+
attachments.length > 8 ||
|
|
160
|
+
Object.keys(answer).some((key) => key !== "attachments")
|
|
161
|
+
) return false;
|
|
162
|
+
return attachments.every((value) => {
|
|
163
|
+
const file = recordOf(value);
|
|
164
|
+
return Boolean(
|
|
165
|
+
file &&
|
|
166
|
+
file.type === "file" &&
|
|
167
|
+
typeof file.url === "string" && file.url.length > 0 &&
|
|
168
|
+
typeof file.filename === "string" && file.filename.length > 0 &&
|
|
169
|
+
typeof file.mediaType === "string" && file.mediaType.length > 0 &&
|
|
170
|
+
(file.providerMetadata === undefined || recordOf(file.providerMetadata)) &&
|
|
171
|
+
Object.keys(file).every((key) =>
|
|
172
|
+
key === "type" || key === "url" || key === "filename" ||
|
|
173
|
+
key === "mediaType" || key === "providerMetadata"
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
116
178
|
if (!answer || !Array.isArray(answer.selections)) return false;
|
|
179
|
+
if (Object.keys(answer).some((key) => key !== "selections" && key !== "text")) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
117
182
|
if (!answer.selections.every((item) => typeof item === "string")) {
|
|
118
183
|
return false;
|
|
119
184
|
}
|
|
@@ -121,15 +186,16 @@ function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
|
121
186
|
return false;
|
|
122
187
|
}
|
|
123
188
|
|
|
124
|
-
const
|
|
189
|
+
const ordinary = question as Static<typeof askUserOrdinaryQuestion>;
|
|
190
|
+
const options = ordinary.options ?? [];
|
|
125
191
|
const text = typeof answer.text === "string" ? answer.text : "";
|
|
126
192
|
if (text.length > 2_000) return false;
|
|
127
193
|
if (answer.selections.some((selection) => !options.includes(selection))) {
|
|
128
194
|
return false;
|
|
129
195
|
}
|
|
130
196
|
if (new Set(answer.selections).size !== answer.selections.length) return false;
|
|
131
|
-
if (!
|
|
132
|
-
const allowCustom = options.length === 0 ||
|
|
197
|
+
if (!ordinary.multiSelect && answer.selections.length > 1) return false;
|
|
198
|
+
const allowCustom = options.length === 0 || ordinary.allowCustom === true;
|
|
133
199
|
if (!allowCustom && text.trim()) return false;
|
|
134
200
|
return answer.selections.length > 0 || text.trim().length > 0;
|
|
135
201
|
});
|
|
@@ -256,7 +322,7 @@ export function basePiToolCandidates(
|
|
|
256
322
|
name: "ask_user",
|
|
257
323
|
label: "Ask user",
|
|
258
324
|
description:
|
|
259
|
-
"Ask all 1-10 user questions needed to continue in one call. For a bounded choice, provide 2-6 distinct options. For required free text, omit the options field entirely; never pass an empty, single-item, or more-than-6-item options array.
|
|
325
|
+
"Ask all 1-10 user questions needed to continue in one call. For files, use one question with kind attachment; it accepts multiple files, so do not ask for each file separately or tell the user to use the normal chat composer. For a bounded choice, provide 2-6 distinct options. For required free text, omit the options field entirely; never pass an empty, single-item, or more-than-6-item options array. The answers return as this tool's result, so continue the same turn after calling it.",
|
|
260
326
|
parameters: askUserParameters,
|
|
261
327
|
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
|
262
328
|
// 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
|
|
@@ -273,6 +339,42 @@ export function basePiToolCandidates(
|
|
|
273
339
|
const responseAnswers = (response as AskUserResponse).answers;
|
|
274
340
|
const answers = questions.map((question, index) => {
|
|
275
341
|
const answer = responseAnswers[index]!;
|
|
342
|
+
if (
|
|
343
|
+
"kind" in question && question.kind === "attachment" &&
|
|
344
|
+
"attachments" in answer
|
|
345
|
+
) {
|
|
346
|
+
return {
|
|
347
|
+
question: question.question,
|
|
348
|
+
attachments: answer.attachments.map((file) => {
|
|
349
|
+
const springbrand = recordOf(
|
|
350
|
+
recordOf(file.providerMetadata)?.springbrand,
|
|
351
|
+
);
|
|
352
|
+
return {
|
|
353
|
+
name: file.filename,
|
|
354
|
+
mediaType: file.mediaType,
|
|
355
|
+
size: typeof springbrand?.size === "number"
|
|
356
|
+
? springbrand.size
|
|
357
|
+
: 0,
|
|
358
|
+
cdnUrl: typeof springbrand?.cdnUrl === "string"
|
|
359
|
+
? springbrand.cdnUrl
|
|
360
|
+
: file.url,
|
|
361
|
+
workspacePath: typeof springbrand?.path === "string"
|
|
362
|
+
? springbrand.path
|
|
363
|
+
: "",
|
|
364
|
+
attachmentId: typeof springbrand?.attachmentId === "string"
|
|
365
|
+
? springbrand.attachmentId
|
|
366
|
+
: "",
|
|
367
|
+
contentVersion:
|
|
368
|
+
typeof springbrand?.contentVersion === "string"
|
|
369
|
+
? springbrand.contentVersion
|
|
370
|
+
: "",
|
|
371
|
+
};
|
|
372
|
+
}),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (!("selections" in answer)) {
|
|
376
|
+
throw new Error("Invalid ask_user answer");
|
|
377
|
+
}
|
|
276
378
|
return {
|
|
277
379
|
question: question.question,
|
|
278
380
|
selections: answer.selections,
|
|
@@ -283,6 +385,13 @@ export function basePiToolCandidates(
|
|
|
283
385
|
content: [{
|
|
284
386
|
type: "text",
|
|
285
387
|
text: `The user answered:\n${answers.map((answer, index) => {
|
|
388
|
+
if ("attachments" in answer) {
|
|
389
|
+
const responseAnswer = responseAnswers[index]!;
|
|
390
|
+
if (!("attachments" in responseAnswer)) return "";
|
|
391
|
+
return `${index + 1}. ${answer.question}\n${responseAnswer.attachments
|
|
392
|
+
.map((file) => ` - ${modelAttachmentText(file)}`)
|
|
393
|
+
.join("\n")}`;
|
|
394
|
+
}
|
|
286
395
|
const parts = [
|
|
287
396
|
...answer.selections,
|
|
288
397
|
...(answer.text ? [answer.text] : []),
|
package/src/runtime.ts
CHANGED
|
@@ -134,6 +134,7 @@ import {
|
|
|
134
134
|
const SCHEDULED_STABLE_TIMEOUT_MS = 30_000;
|
|
135
135
|
const TURN_EVENT_RETRY_SECONDS = 10;
|
|
136
136
|
const RUNTIME_EVENT_MAX_ATTEMPTS = 5;
|
|
137
|
+
const MODEL_SESSION_ID_STORAGE_KEY = "universal-agent:model-session-id";
|
|
137
138
|
|
|
138
139
|
// 计划续跑丢失多久后由看门狗接手。必须显著长于一个正常执行片的时长,
|
|
139
140
|
// 否则会在健康的长执行片上误判并派发出重复的一片。
|
|
@@ -404,6 +405,13 @@ interface PendingTemporaryAgentApproval {
|
|
|
404
405
|
resolve(decision: TemporaryAgentApprovalDecision): void;
|
|
405
406
|
}
|
|
406
407
|
|
|
408
|
+
export interface ToolInteractionResponsePreparation {
|
|
409
|
+
readonly toolCallId: string;
|
|
410
|
+
readonly toolName: string;
|
|
411
|
+
readonly toolInput: unknown;
|
|
412
|
+
readonly response: unknown;
|
|
413
|
+
}
|
|
414
|
+
|
|
407
415
|
// #endregion
|
|
408
416
|
|
|
409
417
|
/**
|
|
@@ -431,6 +439,13 @@ export abstract class AgentRuntimeKernel<
|
|
|
431
439
|
|
|
432
440
|
protected abstract ensureRuntimeReady(): Promise<void>;
|
|
433
441
|
|
|
442
|
+
/** Let the Host prepare a validated client response before Tool settlement. */
|
|
443
|
+
protected prepareToolInteractionResponse(
|
|
444
|
+
input: ToolInteractionResponsePreparation,
|
|
445
|
+
): Promise<unknown | null> {
|
|
446
|
+
return Promise.resolve(input.response);
|
|
447
|
+
}
|
|
448
|
+
|
|
434
449
|
private activeRuntime?: ActiveRuntime;
|
|
435
450
|
private telemetryCoordinator?: RuntimeTelemetryCoordinator;
|
|
436
451
|
private piAdapter?: PiRuntimeAdapter;
|
|
@@ -442,6 +457,8 @@ export abstract class AgentRuntimeKernel<
|
|
|
442
457
|
private readonly approvals: ApprovalLifecycle<StoredSubmission>;
|
|
443
458
|
private readonly interactions: InteractionLifecycle<StoredSubmission>;
|
|
444
459
|
private runtimeLoadTracker?: RuntimeLoadTracker;
|
|
460
|
+
private modelSessionId?: string;
|
|
461
|
+
private readonly modelSessionIdReady?: Promise<void>;
|
|
445
462
|
private runtimeEventDrainTail?: Promise<void>;
|
|
446
463
|
private readonly streamBySubmission = new Map<string, string>();
|
|
447
464
|
private migratedSubmissionIds?: Set<string>;
|
|
@@ -521,6 +538,11 @@ export abstract class AgentRuntimeKernel<
|
|
|
521
538
|
snapshotKey: "__cfPiFiberSnapshot",
|
|
522
539
|
snapshotKind: "pi-turn",
|
|
523
540
|
});
|
|
541
|
+
this.modelSessionIdReady = ctx.storage.get<string>(
|
|
542
|
+
MODEL_SESSION_ID_STORAGE_KEY,
|
|
543
|
+
).then((stored) => {
|
|
544
|
+
this.modelSessionId = stored;
|
|
545
|
+
});
|
|
524
546
|
this.piAdapter = pi;
|
|
525
547
|
this.db = new RuntimeDatabase(
|
|
526
548
|
this.sql.bind(this),
|
|
@@ -734,6 +756,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
734
756
|
}
|
|
735
757
|
|
|
736
758
|
protected async initCandidate(candidate: RuntimeCandidate): Promise<void> {
|
|
759
|
+
await this.modelSessionIdReady;
|
|
737
760
|
const previous = this.activeRuntime;
|
|
738
761
|
const candidateSnapshot = candidate.snapshot;
|
|
739
762
|
this.runtimeLoad.advance("assembly");
|
|
@@ -870,6 +893,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
870
893
|
terminal: {
|
|
871
894
|
outcome: "succeeded" | "failed" | "aborted";
|
|
872
895
|
message?: string;
|
|
896
|
+
resetModelSession?: boolean;
|
|
873
897
|
},
|
|
874
898
|
) => void | Promise<void>;
|
|
875
899
|
} = {},
|
|
@@ -909,7 +933,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
909
933
|
return this.pi.createTurn({
|
|
910
934
|
prepared: this.preparedPi(),
|
|
911
935
|
pinnedDescriptor: submission.assemblyDescriptor,
|
|
912
|
-
modelSessionId: this.name,
|
|
936
|
+
modelSessionId: this.modelSessionId ?? this.name,
|
|
913
937
|
submission: {
|
|
914
938
|
id: submission.submissionId,
|
|
915
939
|
requestId: submission.requestId,
|
|
@@ -3038,6 +3062,7 @@ export abstract class AgentRuntimeKernel<
|
|
|
3038
3062
|
| {
|
|
3039
3063
|
outcome: "succeeded" | "failed" | "aborted";
|
|
3040
3064
|
message?: string;
|
|
3065
|
+
resetModelSession?: boolean;
|
|
3041
3066
|
}
|
|
3042
3067
|
| undefined;
|
|
3043
3068
|
const startedAt = ready.createdAt;
|
|
@@ -3050,13 +3075,20 @@ export abstract class AgentRuntimeKernel<
|
|
|
3050
3075
|
this.migratedSubmissions.has(submissionId)
|
|
3051
3076
|
? undefined
|
|
3052
3077
|
: this.persistAndBroadcastRecord(turn, record),
|
|
3053
|
-
onTerminal: (intent) => {
|
|
3078
|
+
onTerminal: async (intent) => {
|
|
3054
3079
|
terminalIntent = intent;
|
|
3055
3080
|
this.appendTerminalIntent(
|
|
3056
3081
|
submission,
|
|
3057
3082
|
intent.outcome,
|
|
3058
3083
|
intent.message,
|
|
3059
3084
|
);
|
|
3085
|
+
if (intent.resetModelSession) {
|
|
3086
|
+
this.modelSessionId = crypto.randomUUID();
|
|
3087
|
+
await this.ctx.storage.put(
|
|
3088
|
+
MODEL_SESSION_ID_STORAGE_KEY,
|
|
3089
|
+
this.modelSessionId,
|
|
3090
|
+
);
|
|
3091
|
+
}
|
|
3060
3092
|
},
|
|
3061
3093
|
});
|
|
3062
3094
|
for (const pending of this.db.steers.listForSubmission(submissionId)) {
|
|
@@ -4265,10 +4297,19 @@ export abstract class AgentRuntimeKernel<
|
|
|
4265
4297
|
|
|
4266
4298
|
const input = safeParseJson(pending.inputJson);
|
|
4267
4299
|
if (!spec.validateResponse(input, response)) return { ok: false };
|
|
4300
|
+
const prepared = await this.prepareToolInteractionResponse({
|
|
4301
|
+
toolCallId,
|
|
4302
|
+
toolName: pending.toolName,
|
|
4303
|
+
toolInput: input,
|
|
4304
|
+
response,
|
|
4305
|
+
});
|
|
4306
|
+
if (prepared === null || !spec.validateResponse(input, prepared)) {
|
|
4307
|
+
return { ok: false };
|
|
4308
|
+
}
|
|
4268
4309
|
const settled = spec.settle
|
|
4269
|
-
? spec.settle(input,
|
|
4270
|
-
: defaultInteractionResult(
|
|
4271
|
-
return this.interactions.respond(pending.interactionId,
|
|
4310
|
+
? spec.settle(input, prepared)
|
|
4311
|
+
: defaultInteractionResult(prepared);
|
|
4312
|
+
return this.interactions.respond(pending.interactionId, prepared, settled);
|
|
4272
4313
|
}
|
|
4273
4314
|
|
|
4274
4315
|
/**
|