@springbrand/agent-runtime 0.2.0-alpha.59 → 0.2.0-alpha.60
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/lib/prompt.ts +4 -2
- package/src/pi/tool/base.ts +168 -134
package/package.json
CHANGED
package/src/lib/prompt.ts
CHANGED
|
@@ -109,8 +109,10 @@ export const FILES =
|
|
|
109
109
|
// User-interaction actions are called only when their UI semantics are useful.
|
|
110
110
|
export const INTERACTION =
|
|
111
111
|
"Interaction: When progress requires user decisions, put all 1-10 necessary questions in one ask_user call " +
|
|
112
|
-
"instead of asking each question separately or writing 'please choose A/B/C' as text.
|
|
113
|
-
"
|
|
112
|
+
"instead of asking each question separately or writing 'please choose A/B/C' as text. Every question must set exactly one responseType. " +
|
|
113
|
+
"Use responseType: \"single_select\" or responseType: \"multi_select\" with 2-6 options for choices; use responseType: \"text\" " +
|
|
114
|
+
"for required free text; use responseType: \"attachment\" when the user must upload one or more files. At most one question may " +
|
|
115
|
+
"request attachments. Do not add an accept field or tell the user to upload through the normal chat composer; " +
|
|
114
116
|
"continue the same turn when its result arrives. " +
|
|
115
117
|
"Don't use it when a sensible default lets you proceed. " +
|
|
116
118
|
"After completing a substantive task (report, analysis, multi-step job), finish all work and other Tool calls, " +
|
package/src/pi/tool/base.ts
CHANGED
|
@@ -17,53 +17,41 @@ import type { PiToolCandidate } from "./compiler";
|
|
|
17
17
|
import { webSearchPiToolCandidate } from "./web-search";
|
|
18
18
|
import type { WebSearch } from "./web-search/api";
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const askUserResponseTypes = [
|
|
21
|
+
"text",
|
|
22
|
+
"single_select",
|
|
23
|
+
"multi_select",
|
|
24
|
+
"attachment",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const askUserQuestion = Type.Object({
|
|
21
28
|
question: Type.String({
|
|
22
29
|
minLength: 1,
|
|
23
|
-
description: "The question to put to the user.",
|
|
30
|
+
description: "The question or file request to put to the user.",
|
|
31
|
+
}),
|
|
32
|
+
responseType: StringEnum(askUserResponseTypes, {
|
|
33
|
+
description:
|
|
34
|
+
"Required response UI: text, single_select, multi_select, or attachment.",
|
|
24
35
|
}),
|
|
25
36
|
options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
|
26
37
|
minItems: 2,
|
|
27
38
|
maxItems: 6,
|
|
28
39
|
uniqueItems: true,
|
|
29
40
|
description:
|
|
30
|
-
"
|
|
31
|
-
})),
|
|
32
|
-
multiSelect: Type.Optional(Type.Boolean({
|
|
33
|
-
description:
|
|
34
|
-
"Whether the user may select more than one option. Defaults to false.",
|
|
41
|
+
"Required only for single_select or multi_select. Provide 2-6 distinct options; omit for text and attachment.",
|
|
35
42
|
})),
|
|
36
43
|
allowCustom: Type.Optional(Type.Boolean({
|
|
37
44
|
description:
|
|
38
|
-
"
|
|
45
|
+
"For single_select or multi_select, whether the user may also add free text. Defaults to false.",
|
|
39
46
|
})),
|
|
40
47
|
}, { additionalProperties: false });
|
|
41
48
|
|
|
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
|
-
|
|
58
49
|
const askUserParameters = Type.Object({
|
|
59
50
|
questions: Type.Array(askUserQuestion, {
|
|
60
51
|
minItems: 1,
|
|
61
52
|
maxItems: 10,
|
|
62
|
-
contains: askUserAttachmentQuestion,
|
|
63
|
-
minContains: 0,
|
|
64
|
-
maxContains: 1,
|
|
65
53
|
description:
|
|
66
|
-
"All user decisions and files needed to continue.
|
|
54
|
+
"All user decisions and files needed to continue. Every question requires responseType. At most one question may use attachment.",
|
|
67
55
|
}),
|
|
68
56
|
}, { additionalProperties: false });
|
|
69
57
|
|
|
@@ -82,10 +70,11 @@ type AskUserFile = {
|
|
|
82
70
|
readonly providerMetadata?: unknown;
|
|
83
71
|
};
|
|
84
72
|
type AskUserResponse = {
|
|
85
|
-
readonly answers: ReadonlyArray<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
73
|
+
readonly answers: ReadonlyArray<{
|
|
74
|
+
readonly selections: readonly string[];
|
|
75
|
+
readonly text: string;
|
|
76
|
+
readonly attachments: readonly AskUserFile[];
|
|
77
|
+
}>;
|
|
89
78
|
};
|
|
90
79
|
|
|
91
80
|
function recordOf(value: unknown): Record<string, unknown> | null {
|
|
@@ -94,55 +83,110 @@ function recordOf(value: unknown): Record<string, unknown> | null {
|
|
|
94
83
|
: null;
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
function questionsOf(
|
|
86
|
+
function questionsOf(
|
|
87
|
+
value: unknown,
|
|
88
|
+
allowLegacy = false,
|
|
89
|
+
): AskUserInput["questions"] | null {
|
|
98
90
|
const questions = recordOf(value)?.questions;
|
|
99
91
|
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 10) {
|
|
100
92
|
return null;
|
|
101
93
|
}
|
|
94
|
+
const normalized: AskUserInput["questions"] = [];
|
|
102
95
|
let attachmentQuestions = 0;
|
|
103
96
|
for (const value of questions) {
|
|
104
97
|
const question = recordOf(value);
|
|
105
98
|
if (!question || typeof question.question !== "string" || !question.question) {
|
|
106
99
|
return null;
|
|
107
100
|
}
|
|
108
|
-
|
|
101
|
+
let responseType = question.responseType;
|
|
102
|
+
if (
|
|
103
|
+
typeof responseType !== "string" ||
|
|
104
|
+
!askUserResponseTypes.includes(
|
|
105
|
+
responseType as typeof askUserResponseTypes[number],
|
|
106
|
+
)
|
|
107
|
+
) {
|
|
108
|
+
if (!allowLegacy || question.responseType !== undefined) return null;
|
|
109
|
+
if (question.kind === "attachment") {
|
|
110
|
+
responseType = "attachment";
|
|
111
|
+
} else if (question.kind === undefined) {
|
|
112
|
+
responseType = Array.isArray(question.options)
|
|
113
|
+
? question.multiSelect === true
|
|
114
|
+
? "multi_select"
|
|
115
|
+
: "single_select"
|
|
116
|
+
: "text";
|
|
117
|
+
} else {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (responseType === "attachment") {
|
|
109
122
|
attachmentQuestions += 1;
|
|
110
123
|
if (
|
|
111
124
|
attachmentQuestions > 1 ||
|
|
112
125
|
question.options !== undefined ||
|
|
113
|
-
question.
|
|
114
|
-
question.
|
|
126
|
+
question.allowCustom !== undefined ||
|
|
127
|
+
(!allowLegacy && question.multiSelect !== undefined)
|
|
115
128
|
) return null;
|
|
129
|
+
normalized.push({ question: question.question, responseType });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (responseType === "text") {
|
|
133
|
+
if (
|
|
134
|
+
question.options !== undefined ||
|
|
135
|
+
(!allowLegacy &&
|
|
136
|
+
(question.allowCustom !== undefined || question.multiSelect !== undefined))
|
|
137
|
+
) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
normalized.push({ question: question.question, responseType });
|
|
116
141
|
continue;
|
|
117
142
|
}
|
|
118
|
-
if (question.kind !== undefined) return null;
|
|
119
143
|
if (
|
|
120
|
-
question.
|
|
121
|
-
|
|
144
|
+
!Array.isArray(question.options) ||
|
|
145
|
+
question.options.length < 2 ||
|
|
146
|
+
question.options.length > 6 ||
|
|
147
|
+
!question.options.every((option) =>
|
|
148
|
+
typeof option === "string" && option.length > 0
|
|
149
|
+
) ||
|
|
150
|
+
new Set(question.options).size !== question.options.length ||
|
|
151
|
+
(question.allowCustom !== undefined &&
|
|
152
|
+
typeof question.allowCustom !== "boolean")
|
|
122
153
|
) return null;
|
|
123
154
|
if (
|
|
124
|
-
|
|
125
|
-
|
|
155
|
+
!allowLegacy &&
|
|
156
|
+
(question.kind !== undefined || question.multiSelect !== undefined)
|
|
126
157
|
) return null;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
normalized.push({
|
|
159
|
+
question: question.question,
|
|
160
|
+
responseType: responseType as typeof askUserResponseTypes[number],
|
|
161
|
+
options: question.options,
|
|
162
|
+
...(question.allowCustom === undefined
|
|
163
|
+
? {}
|
|
164
|
+
: { allowCustom: question.allowCustom }),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function prepareAskUserArguments(input: unknown): AskUserInput {
|
|
171
|
+
const value = recordOf(input);
|
|
172
|
+
if (!value) throw new Error("Invalid ask_user questions");
|
|
173
|
+
let questions = value.questions;
|
|
174
|
+
if (typeof questions === "string") {
|
|
175
|
+
try {
|
|
176
|
+
questions = JSON.parse(questions);
|
|
177
|
+
} catch {
|
|
178
|
+
throw new Error("Invalid ask_user questions");
|
|
137
179
|
}
|
|
138
180
|
}
|
|
139
|
-
|
|
181
|
+
const prepared = { ...value, questions };
|
|
182
|
+
if (!questionsOf(prepared)) throw new Error("Invalid ask_user questions");
|
|
183
|
+
return prepared as AskUserInput;
|
|
140
184
|
}
|
|
141
185
|
|
|
142
186
|
// 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
|
|
143
187
|
// 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
|
|
144
188
|
function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
145
|
-
const questions = questionsOf(input);
|
|
189
|
+
const questions = questionsOf(input, true);
|
|
146
190
|
const answers = recordOf(value)?.answers;
|
|
147
191
|
if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
|
|
148
192
|
return false;
|
|
@@ -150,54 +194,55 @@ function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
|
150
194
|
|
|
151
195
|
return questions.every((question, index) => {
|
|
152
196
|
const answer = recordOf(answers[index]);
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
}
|
|
178
|
-
if (!answer || !Array.isArray(answer.selections)) return false;
|
|
179
|
-
if (Object.keys(answer).some((key) => key !== "selections" && key !== "text")) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
197
|
+
if (
|
|
198
|
+
!answer ||
|
|
199
|
+
!Array.isArray(answer.selections) ||
|
|
200
|
+
typeof answer.text !== "string" ||
|
|
201
|
+
!Array.isArray(answer.attachments) ||
|
|
202
|
+
Object.keys(answer).some((key) =>
|
|
203
|
+
key !== "selections" && key !== "text" && key !== "attachments"
|
|
204
|
+
)
|
|
205
|
+
) return false;
|
|
182
206
|
if (!answer.selections.every((item) => typeof item === "string")) {
|
|
183
207
|
return false;
|
|
184
208
|
}
|
|
185
|
-
if (answer.text
|
|
186
|
-
|
|
209
|
+
if (answer.text.length > 2_000) return false;
|
|
210
|
+
if (!answer.attachments.every((value) => {
|
|
211
|
+
const file = recordOf(value);
|
|
212
|
+
return Boolean(
|
|
213
|
+
file &&
|
|
214
|
+
file.type === "file" &&
|
|
215
|
+
typeof file.url === "string" && file.url.length > 0 &&
|
|
216
|
+
typeof file.filename === "string" && file.filename.length > 0 &&
|
|
217
|
+
typeof file.mediaType === "string" && file.mediaType.length > 0 &&
|
|
218
|
+
(file.providerMetadata === undefined || recordOf(file.providerMetadata)) &&
|
|
219
|
+
Object.keys(file).every((key) =>
|
|
220
|
+
key === "type" || key === "url" || key === "filename" ||
|
|
221
|
+
key === "mediaType" || key === "providerMetadata"
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
})) return false;
|
|
225
|
+
|
|
226
|
+
if (question.responseType === "attachment") {
|
|
227
|
+
return answer.selections.length === 0 && !answer.text &&
|
|
228
|
+
answer.attachments.length >= 1 && answer.attachments.length <= 8;
|
|
229
|
+
}
|
|
230
|
+
if (answer.attachments.length > 0) return false;
|
|
231
|
+
if (question.responseType === "text") {
|
|
232
|
+
return answer.selections.length === 0 && answer.text.trim().length > 0;
|
|
187
233
|
}
|
|
188
234
|
|
|
189
|
-
const
|
|
190
|
-
const options = ordinary.options ?? [];
|
|
191
|
-
const text = typeof answer.text === "string" ? answer.text : "";
|
|
192
|
-
if (text.length > 2_000) return false;
|
|
235
|
+
const options = question.options ?? [];
|
|
193
236
|
if (answer.selections.some((selection) => !options.includes(selection))) {
|
|
194
237
|
return false;
|
|
195
238
|
}
|
|
196
239
|
if (new Set(answer.selections).size !== answer.selections.length) return false;
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
240
|
+
if (
|
|
241
|
+
question.responseType === "single_select" &&
|
|
242
|
+
answer.selections.length > 1
|
|
243
|
+
) return false;
|
|
244
|
+
if (!question.allowCustom && answer.text.trim()) return false;
|
|
245
|
+
return answer.selections.length > 0 || answer.text.trim().length > 0;
|
|
201
246
|
});
|
|
202
247
|
}
|
|
203
248
|
|
|
@@ -322,8 +367,9 @@ export function basePiToolCandidates(
|
|
|
322
367
|
name: "ask_user",
|
|
323
368
|
label: "Ask user",
|
|
324
369
|
description:
|
|
325
|
-
"Ask all 1-10 user questions needed to continue in one call.
|
|
370
|
+
"Ask all 1-10 user questions needed to continue in one call. Required input for every question: question and responseType. Use responseType text for free text, single_select or multi_select with 2-6 distinct options for choices, and attachment when the user must upload files. At most one attachment question is allowed and it accepts multiple files. Do not add file-specific fields such as accept or tell the user to use the normal chat composer. The answers return as this tool's result, so continue the same turn after calling it.",
|
|
326
371
|
parameters: askUserParameters,
|
|
372
|
+
prepareArguments: prepareAskUserArguments,
|
|
327
373
|
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
|
328
374
|
// 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
|
|
329
375
|
async execute() {
|
|
@@ -335,59 +381,47 @@ export function basePiToolCandidates(
|
|
|
335
381
|
interaction: {
|
|
336
382
|
validateResponse: isAskUserResponse,
|
|
337
383
|
settle: (input, response) => {
|
|
338
|
-
const questions = (input
|
|
384
|
+
const questions = questionsOf(input, true)!;
|
|
339
385
|
const responseAnswers = (response as AskUserResponse).answers;
|
|
340
386
|
const answers = questions.map((question, index) => {
|
|
341
387
|
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
|
-
}
|
|
378
388
|
return {
|
|
379
389
|
question: question.question,
|
|
380
|
-
selections: answer.selections,
|
|
381
|
-
|
|
390
|
+
selections: [...answer.selections],
|
|
391
|
+
text: answer.text.trim(),
|
|
392
|
+
attachments: answer.attachments.map((file) => {
|
|
393
|
+
const springbrand = recordOf(
|
|
394
|
+
recordOf(file.providerMetadata)?.springbrand,
|
|
395
|
+
);
|
|
396
|
+
return {
|
|
397
|
+
name: file.filename,
|
|
398
|
+
mediaType: file.mediaType,
|
|
399
|
+
size: typeof springbrand?.size === "number"
|
|
400
|
+
? springbrand.size
|
|
401
|
+
: 0,
|
|
402
|
+
cdnUrl: typeof springbrand?.cdnUrl === "string"
|
|
403
|
+
? springbrand.cdnUrl
|
|
404
|
+
: file.url,
|
|
405
|
+
workspacePath: typeof springbrand?.path === "string"
|
|
406
|
+
? springbrand.path
|
|
407
|
+
: "",
|
|
408
|
+
attachmentId: typeof springbrand?.attachmentId === "string"
|
|
409
|
+
? springbrand.attachmentId
|
|
410
|
+
: "",
|
|
411
|
+
contentVersion:
|
|
412
|
+
typeof springbrand?.contentVersion === "string"
|
|
413
|
+
? springbrand.contentVersion
|
|
414
|
+
: "",
|
|
415
|
+
};
|
|
416
|
+
}),
|
|
382
417
|
};
|
|
383
418
|
});
|
|
384
419
|
return {
|
|
385
420
|
content: [{
|
|
386
421
|
type: "text",
|
|
387
422
|
text: `The user answered:\n${answers.map((answer, index) => {
|
|
388
|
-
if (
|
|
423
|
+
if (answer.attachments.length > 0) {
|
|
389
424
|
const responseAnswer = responseAnswers[index]!;
|
|
390
|
-
if (!("attachments" in responseAnswer)) return "";
|
|
391
425
|
return `${index + 1}. ${answer.question}\n${responseAnswer.attachments
|
|
392
426
|
.map((file) => ` - ${modelAttachmentText(file)}`)
|
|
393
427
|
.join("\n")}`;
|