@springbrand/agent-runtime 0.3.0-alpha.2 → 0.3.0-alpha.21
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 +11 -9
- package/src/adapter/cloudflare/agent.ts +233 -179
- package/src/adapter/cloudflare/define-agent.ts +252 -104
- package/src/adapter/cloudflare/index.ts +18 -0
- package/src/adapter/cloudflare/scheduling/schedule-rule.ts +322 -0
- package/src/adapter/cloudflare/scheduling/zoned-cron.ts +11 -6
- package/src/adapter/cloudflare/schema.ts +21 -2
- package/src/adapter/cloudflare/subagent/definition.ts +25 -13
- package/src/adapter/cloudflare/subagent/runner.ts +43 -50
- package/src/adapter/cloudflare/subagent/tools.ts +4 -4
- package/src/adapter/cloudflare/universal-agent/preparation.ts +3 -3
- package/src/adapter/cloudflare/universal-agent/tools.ts +4 -6
- package/src/adapter/cloudflare/workspace/version-control.ts +1 -1
- package/src/config/activation.ts +13 -15
- package/src/config/agent-context.ts +1 -1
- package/src/config/assembler.ts +48 -31
- package/src/config/assembly-snapshot.ts +1 -1
- package/src/config/definition.ts +1 -1
- package/src/config/harness-runtime.ts +77 -49
- package/src/config/index.ts +1 -1
- package/src/config/model-guidance.ts +20 -0
- package/src/config/ports.ts +57 -7
- package/src/config/{pinning.ts → prepared-runtime.ts} +59 -123
- package/src/config/public-contracts.ts +2 -2
- package/src/config/runtime-config.ts +2 -1
- package/src/context/assembly.ts +1 -1
- package/src/context/extension/assembly.ts +2 -2
- package/src/context/memory/budget/gate.ts +19 -3
- package/src/conversation/harness-resume.ts +89 -238
- package/src/conversation/harness-stream.ts +87 -71
- package/src/conversation/history.ts +154 -152
- package/src/conversation/message/contract.ts +28 -4
- package/src/conversation/message/conversion.ts +46 -5
- package/src/conversation/message/projection.ts +61 -21
- package/src/conversation/session-entry.ts +29 -0
- package/src/conversation/state.ts +23 -2
- package/src/harness/governance.ts +206 -0
- package/src/harness/index.ts +3 -0
- package/src/harness/session-harness.ts +269 -445
- package/src/harness/submission-operation.ts +15 -40
- package/src/index.ts +18 -8
- package/src/lib/artifacts.ts +18 -0
- package/src/lib/execution-level.ts +13 -0
- package/src/lib/prompt.ts +9 -10
- package/src/lib/token-estimate.ts +8 -0
- package/src/model-protocol/openrouter.ts +41 -0
- package/src/models/index.ts +200 -35
- package/src/models/openrouter/messages.ts +13 -8
- package/src/models/openrouter/openai.ts +200 -0
- package/src/models/openrouter/protocol-response.ts +85 -0
- package/src/models/provider-streams.ts +45 -0
- package/src/session/cloudflare/migration.ts +382 -864
- package/src/session/cloudflare/storage.ts +13 -536
- package/src/session/index.ts +22 -35
- package/src/skills/springbrand-worker-website/index.ts +1 -1
- package/src/submission/admission.ts +20 -30
- package/src/submission/contracts.ts +42 -2
- package/src/submission/delivery.ts +1 -1
- package/src/submission/finalization.ts +36 -15
- package/src/submission/lifecycle.ts +28 -15
- package/src/submission/repository.ts +97 -49
- package/src/submission/steer-delivery.ts +32 -76
- package/src/submission/unattended.ts +40 -0
- package/src/telemetry/contract.ts +0 -1
- package/src/telemetry/coordinator.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +0 -4
- package/src/telemetry/subagent-lifecycle.ts +1 -1
- package/src/telemetry/turn-events.ts +35 -25
- package/src/tools/README.md +44 -0
- package/src/tools/{ai-adapter.ts → adapters/ai-to-pi.ts} +6 -9
- package/src/tools/{declared.ts → adapters/declared.ts} +6 -2
- package/src/tools/{nested-tools.ts → adapters/pi-to-ai.ts} +4 -4
- package/src/tools/assistant/ask-user.ts +319 -0
- package/src/tools/assistant/candidate.ts +15 -0
- package/src/tools/assistant/followups.ts +34 -0
- package/src/tools/assistant/index.ts +19 -0
- package/src/tools/assistant/memory.ts +73 -0
- package/src/tools/assistant/plan.ts +98 -0
- package/src/tools/{time.ts → assistant/time.ts} +1 -1
- package/src/tools/{core-host.ts → codemode/adapter.ts} +50 -53
- package/src/tools/{core.ts → codemode/index.ts} +36 -92
- package/src/tools/codemode/workspace-connector.ts +198 -0
- package/src/tools/extension/index.ts +42 -0
- package/src/tools/index.ts +29 -26
- package/src/tools/{gateway.ts → mcp/gateway.ts} +19 -19
- package/src/tools/{mcp.ts → mcp/index.ts} +5 -5
- package/src/tools/runtime/activation.ts +40 -0
- package/src/tools/{approval → runtime/approval}/repository.ts +13 -12
- package/src/tools/runtime/compiler.ts +246 -0
- package/src/tools/runtime/contracts.ts +140 -0
- package/src/tools/{decision-gate.ts → runtime/decision-gate.ts} +149 -39
- package/src/tools/{human-decisions.ts → runtime/human-decisions.ts} +4 -4
- package/src/tools/{interaction → runtime/interaction}/repository.ts +1 -7
- package/src/tools/runtime/result.ts +22 -0
- package/src/tools/runtime/unattended-error.ts +4 -0
- package/src/tools/sandbox/index.ts +228 -0
- package/src/tools/schedule/index.ts +301 -0
- package/src/tools/skill/contracts.ts +20 -0
- package/src/tools/skill/index.ts +99 -0
- package/src/tools/skill/resources.ts +322 -0
- package/src/tools/skill/scripts.ts +168 -0
- package/src/tools/{subagent-authority.ts → subagent/authority.ts} +31 -31
- package/src/tools/{subagent.ts → subagent/index.ts} +34 -37
- package/src/tools/{subagent-run-repository.ts → subagent/repository.ts} +1 -1
- package/src/tools/{subagent-runtime.ts → subagent/runtime.ts} +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/approvals.ts +4 -7
- package/src/tools/{temporary-agent → subagent/temporary}/core.ts +2 -3
- package/src/tools/{temporary-agent → subagent/temporary}/runner.ts +1 -1
- package/src/tools/{temporary-agent → subagent/temporary}/workspace.ts +1 -4
- package/src/tools/{web-fetch.ts → web/fetch.ts} +4 -13
- package/src/tools/web/index.ts +2 -0
- package/src/tools/web/search/api.ts +1322 -0
- package/src/tools/web/search/index.ts +2 -0
- package/src/tools/{web-search/web-search.ts → web/search/search.ts} +5 -7
- package/src/tools/{workspace-sandbox.ts → workspace/index.ts} +12 -272
- package/src/tools/{workspace-revision.ts → workspace/revision.ts} +3 -3
- package/src/tools/base.ts +0 -534
- package/src/tools/compiler.ts +0 -327
- package/src/tools/schedule.ts +0 -261
- package/src/tools/skill.ts +0 -565
- package/src/tools/web-search/api.ts +0 -1318
- package/src/tools/web-search/index.ts +0 -2
- package/src/tools/workspace-state-connector.ts +0 -146
- package/src/tools/{approval → runtime/approval}/index.ts +0 -0
- package/src/tools/{interaction → runtime/interaction}/index.ts +0 -0
- package/src/tools/{temporary-agent → subagent/temporary}/coordinator.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/contract.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/extract/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/fanout/index.ts +0 -0
- package/src/tools/{subagent-types → subagent/types}/registry.ts +1 -1
- /package/src/tools/{workspace-versioning.ts → workspace/versioning.ts} +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
|
|
2
|
+
import { modelAttachmentText } from "../../conversation/message/conversion";
|
|
3
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
4
|
+
import { candidate } from "./candidate";
|
|
5
|
+
|
|
6
|
+
export const ASK_USER_TOOL_NAME = "ask_user";
|
|
7
|
+
|
|
8
|
+
const askUserResponseTypes = [
|
|
9
|
+
"text",
|
|
10
|
+
"single_select",
|
|
11
|
+
"multi_select",
|
|
12
|
+
"attachment",
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const askUserQuestion = Type.Object({
|
|
16
|
+
question: Type.String({
|
|
17
|
+
minLength: 1,
|
|
18
|
+
description: "The question or file request to put to the user.",
|
|
19
|
+
}),
|
|
20
|
+
responseType: StringEnum(askUserResponseTypes, {
|
|
21
|
+
description:
|
|
22
|
+
"Required response UI: text, single_select, multi_select, or attachment.",
|
|
23
|
+
}),
|
|
24
|
+
options: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
|
|
25
|
+
minItems: 2,
|
|
26
|
+
maxItems: 6,
|
|
27
|
+
uniqueItems: true,
|
|
28
|
+
description:
|
|
29
|
+
"Required only for single_select or multi_select. Provide 2-6 distinct options; omit for text and attachment.",
|
|
30
|
+
})),
|
|
31
|
+
allowCustom: Type.Optional(Type.Boolean({
|
|
32
|
+
description:
|
|
33
|
+
"For single_select or multi_select, whether the user may also add free text. Defaults to false.",
|
|
34
|
+
})),
|
|
35
|
+
required: Type.Optional(Type.Boolean({
|
|
36
|
+
default: false,
|
|
37
|
+
description:
|
|
38
|
+
"Whether the user must answer this question to submit. Defaults to false.",
|
|
39
|
+
})),
|
|
40
|
+
}, { additionalProperties: false });
|
|
41
|
+
|
|
42
|
+
const askUserParameters = Type.Object({
|
|
43
|
+
questions: Type.Array(askUserQuestion, {
|
|
44
|
+
minItems: 1,
|
|
45
|
+
maxItems: 10,
|
|
46
|
+
description:
|
|
47
|
+
"All user decisions and files needed to continue. Every question requires responseType. At most one question may use attachment.",
|
|
48
|
+
}),
|
|
49
|
+
}, { additionalProperties: false });
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `ask_user` 的客户端响应体。
|
|
53
|
+
*
|
|
54
|
+
* `answers` 与 Tool 输入的 `questions` 按下标一一对应。
|
|
55
|
+
* 刻意用结构化数组而不是拼好的字符串 —— 选项本身可能含分隔符,拼了再拆是有损的。
|
|
56
|
+
*/
|
|
57
|
+
type AskUserInput = Static<typeof askUserParameters>;
|
|
58
|
+
|
|
59
|
+
type AskUserFile = {
|
|
60
|
+
readonly type: "file";
|
|
61
|
+
readonly url: string;
|
|
62
|
+
readonly filename: string;
|
|
63
|
+
readonly mediaType: string;
|
|
64
|
+
readonly providerMetadata?: unknown;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type AskUserResponse = {
|
|
68
|
+
readonly answers: ReadonlyArray<{
|
|
69
|
+
readonly selections: readonly string[];
|
|
70
|
+
readonly text: string;
|
|
71
|
+
readonly attachments: readonly AskUserFile[];
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function recordOf(value: unknown): Record<string, unknown> | null {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
77
|
+
? value as Record<string, unknown>
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function questionsOf(value: unknown): AskUserInput["questions"] | null {
|
|
82
|
+
const questions = recordOf(value)?.questions;
|
|
83
|
+
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 10) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const normalized: AskUserInput["questions"] = [];
|
|
87
|
+
let attachmentQuestions = 0;
|
|
88
|
+
for (const value of questions) {
|
|
89
|
+
const question = recordOf(value);
|
|
90
|
+
if (!question || typeof question.question !== "string" || !question.question) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(question).some((key) =>
|
|
94
|
+
key !== "question" && key !== "responseType" && key !== "options" &&
|
|
95
|
+
key !== "allowCustom" && key !== "required"
|
|
96
|
+
)) return null;
|
|
97
|
+
if (
|
|
98
|
+
question.required !== undefined && typeof question.required !== "boolean"
|
|
99
|
+
) return null;
|
|
100
|
+
const required = question.required === true;
|
|
101
|
+
let responseType = question.responseType;
|
|
102
|
+
if (
|
|
103
|
+
typeof responseType !== "string" ||
|
|
104
|
+
!askUserResponseTypes.includes(
|
|
105
|
+
responseType as typeof askUserResponseTypes[number],
|
|
106
|
+
)
|
|
107
|
+
) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const normalizedQuestion = {
|
|
111
|
+
question: question.question,
|
|
112
|
+
responseType: responseType as typeof askUserResponseTypes[number],
|
|
113
|
+
required,
|
|
114
|
+
};
|
|
115
|
+
if (responseType === "attachment") {
|
|
116
|
+
attachmentQuestions += 1;
|
|
117
|
+
if (
|
|
118
|
+
attachmentQuestions > 1 ||
|
|
119
|
+
question.options !== undefined ||
|
|
120
|
+
question.allowCustom !== undefined
|
|
121
|
+
) return null;
|
|
122
|
+
normalized.push(normalizedQuestion);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (responseType === "text") {
|
|
126
|
+
if (
|
|
127
|
+
question.options !== undefined ||
|
|
128
|
+
question.allowCustom !== undefined
|
|
129
|
+
) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
normalized.push(normalizedQuestion);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (
|
|
136
|
+
!Array.isArray(question.options) ||
|
|
137
|
+
question.options.length < 2 ||
|
|
138
|
+
question.options.length > 6 ||
|
|
139
|
+
!question.options.every((option) =>
|
|
140
|
+
typeof option === "string" && option.length > 0
|
|
141
|
+
) ||
|
|
142
|
+
new Set(question.options).size !== question.options.length ||
|
|
143
|
+
(question.allowCustom !== undefined &&
|
|
144
|
+
typeof question.allowCustom !== "boolean")
|
|
145
|
+
) return null;
|
|
146
|
+
normalized.push({
|
|
147
|
+
...normalizedQuestion,
|
|
148
|
+
options: question.options,
|
|
149
|
+
...(question.allowCustom === undefined
|
|
150
|
+
? {}
|
|
151
|
+
: { allowCustom: question.allowCustom }),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return normalized;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function prepareAskUserArguments(input: unknown): AskUserInput {
|
|
158
|
+
const value = recordOf(input);
|
|
159
|
+
if (!value) throw new Error("Invalid ask_user questions");
|
|
160
|
+
let questions = value.questions;
|
|
161
|
+
if (typeof questions === "string") {
|
|
162
|
+
try {
|
|
163
|
+
questions = JSON.parse(questions);
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error("Invalid ask_user questions");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const prepared = { ...value, questions };
|
|
169
|
+
if (!questionsOf(prepared)) throw new Error("Invalid ask_user questions");
|
|
170
|
+
return prepared as AskUserInput;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
|
|
174
|
+
// 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
|
|
175
|
+
function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
176
|
+
const questions = questionsOf(input);
|
|
177
|
+
const answers = recordOf(value)?.answers;
|
|
178
|
+
if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return questions.every((question, index) => {
|
|
183
|
+
const answer = recordOf(answers[index]);
|
|
184
|
+
if (
|
|
185
|
+
!answer ||
|
|
186
|
+
!Array.isArray(answer.selections) ||
|
|
187
|
+
typeof answer.text !== "string" ||
|
|
188
|
+
!Array.isArray(answer.attachments) ||
|
|
189
|
+
Object.keys(answer).some((key) =>
|
|
190
|
+
key !== "selections" && key !== "text" && key !== "attachments"
|
|
191
|
+
)
|
|
192
|
+
) return false;
|
|
193
|
+
if (!answer.selections.every((item) => typeof item === "string")) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (answer.text.length > 2_000) return false;
|
|
197
|
+
if (!answer.attachments.every((value) => {
|
|
198
|
+
const file = recordOf(value);
|
|
199
|
+
return Boolean(
|
|
200
|
+
file &&
|
|
201
|
+
file.type === "file" &&
|
|
202
|
+
typeof file.url === "string" && file.url.length > 0 &&
|
|
203
|
+
typeof file.filename === "string" && file.filename.length > 0 &&
|
|
204
|
+
typeof file.mediaType === "string" && file.mediaType.length > 0 &&
|
|
205
|
+
(file.providerMetadata === undefined || recordOf(file.providerMetadata)) &&
|
|
206
|
+
Object.keys(file).every((key) =>
|
|
207
|
+
key === "type" || key === "url" || key === "filename" ||
|
|
208
|
+
key === "mediaType" || key === "providerMetadata"
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
})) return false;
|
|
212
|
+
|
|
213
|
+
if (
|
|
214
|
+
answer.selections.length === 0 &&
|
|
215
|
+
!answer.text.trim() &&
|
|
216
|
+
answer.attachments.length === 0
|
|
217
|
+
) return question.required !== true;
|
|
218
|
+
|
|
219
|
+
if (question.responseType === "attachment") {
|
|
220
|
+
return answer.selections.length === 0 && !answer.text &&
|
|
221
|
+
answer.attachments.length >= 1 && answer.attachments.length <= 8;
|
|
222
|
+
}
|
|
223
|
+
if (answer.attachments.length > 0) return false;
|
|
224
|
+
if (question.responseType === "text") {
|
|
225
|
+
return answer.selections.length === 0 && answer.text.trim().length > 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const options = question.options ?? [];
|
|
229
|
+
if (answer.selections.some((selection) => !options.includes(selection))) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (new Set(answer.selections).size !== answer.selections.length) return false;
|
|
233
|
+
if (
|
|
234
|
+
question.responseType === "single_select" &&
|
|
235
|
+
answer.selections.length > 1
|
|
236
|
+
) return false;
|
|
237
|
+
if (!question.allowCustom && answer.text.trim()) return false;
|
|
238
|
+
return answer.selections.length > 0 || answer.text.trim().length > 0;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function askUserPiToolCandidate(): PiToolCandidate {
|
|
243
|
+
return {
|
|
244
|
+
...candidate({
|
|
245
|
+
name: ASK_USER_TOOL_NAME,
|
|
246
|
+
label: "Ask user",
|
|
247
|
+
description:
|
|
248
|
+
"Ask all 1-10 user questions needed to continue in one call. Required input for every question: question and responseType; required defaults to false, so set it to true only when progress cannot continue without an answer. Use responseType text for free text, single_select or multi_select with 2-6 distinct options for choices, and attachment for 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.",
|
|
249
|
+
parameters: askUserParameters,
|
|
250
|
+
prepareArguments: prepareAskUserArguments,
|
|
251
|
+
// 永远不会被调用:执行链在 interaction 闸就 park 住了。留一个失败关闭的
|
|
252
|
+
// 实现,是为了万一哪次改动绕过了那道闸,能立刻炸出来而不是静默返回空答案。
|
|
253
|
+
async execute() {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"ask_user is client-settled and must not execute on the server",
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
interaction: {
|
|
260
|
+
validateResponse: isAskUserResponse,
|
|
261
|
+
settle: (input, response) => {
|
|
262
|
+
const questions = questionsOf(input)!;
|
|
263
|
+
const responseAnswers = (response as AskUserResponse).answers;
|
|
264
|
+
const answers = questions.map((question, index) => {
|
|
265
|
+
const answer = responseAnswers[index]!;
|
|
266
|
+
return {
|
|
267
|
+
question: question.question,
|
|
268
|
+
selections: [...answer.selections],
|
|
269
|
+
text: answer.text.trim(),
|
|
270
|
+
attachments: answer.attachments.map((file) => {
|
|
271
|
+
const springbrand = recordOf(
|
|
272
|
+
recordOf(file.providerMetadata)?.springbrand,
|
|
273
|
+
);
|
|
274
|
+
return {
|
|
275
|
+
name: file.filename,
|
|
276
|
+
mediaType: file.mediaType,
|
|
277
|
+
size: typeof springbrand?.size === "number"
|
|
278
|
+
? springbrand.size
|
|
279
|
+
: 0,
|
|
280
|
+
cdnUrl: typeof springbrand?.cdnUrl === "string"
|
|
281
|
+
? springbrand.cdnUrl
|
|
282
|
+
: file.url,
|
|
283
|
+
workspacePath: typeof springbrand?.path === "string"
|
|
284
|
+
? springbrand.path
|
|
285
|
+
: "",
|
|
286
|
+
attachmentId: typeof springbrand?.attachmentId === "string"
|
|
287
|
+
? springbrand.attachmentId
|
|
288
|
+
: "",
|
|
289
|
+
contentVersion:
|
|
290
|
+
typeof springbrand?.contentVersion === "string"
|
|
291
|
+
? springbrand.contentVersion
|
|
292
|
+
: "",
|
|
293
|
+
};
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
content: [{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: `The user answered:\n${answers.map((answer, index) => {
|
|
301
|
+
if (answer.attachments.length > 0) {
|
|
302
|
+
const responseAnswer = responseAnswers[index]!;
|
|
303
|
+
return `${index + 1}. ${answer.question}\n${responseAnswer.attachments
|
|
304
|
+
.map((file) => ` - ${modelAttachmentText(file)}`)
|
|
305
|
+
.join("\n")}`;
|
|
306
|
+
}
|
|
307
|
+
const parts = [
|
|
308
|
+
...answer.selections,
|
|
309
|
+
...(answer.text ? [answer.text] : []),
|
|
310
|
+
];
|
|
311
|
+
return `${index + 1}. ${answer.question}\n ${parts.join(" / ")}`;
|
|
312
|
+
}).join("\n")}`,
|
|
313
|
+
}],
|
|
314
|
+
details: { answers },
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type TSchema } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
4
|
+
|
|
5
|
+
export function candidate<T extends TSchema, D = any>(tool: AgentTool<T, D>): PiToolCandidate<T, D> {
|
|
6
|
+
return {
|
|
7
|
+
owner: "runtime-base",
|
|
8
|
+
requiredExecutionLevel: "safe",
|
|
9
|
+
source: "action",
|
|
10
|
+
tool: {
|
|
11
|
+
...tool,
|
|
12
|
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
|
13
|
+
} as AgentTool<T, D>,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Type } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
3
|
+
import { result } from "../runtime/result";
|
|
4
|
+
import { candidate } from "./candidate";
|
|
5
|
+
|
|
6
|
+
const suggestFollowupsParameters = Type.Object({
|
|
7
|
+
items: Type.Array(Type.String({
|
|
8
|
+
description: "A follow-up the user could ask next, phrased as a request.",
|
|
9
|
+
}), {
|
|
10
|
+
minItems: 2,
|
|
11
|
+
maxItems: 4,
|
|
12
|
+
description: "2-4 concrete, distinct follow-up directions.",
|
|
13
|
+
}),
|
|
14
|
+
}, { additionalProperties: false });
|
|
15
|
+
|
|
16
|
+
export function suggestFollowupsPiToolCandidate(): PiToolCandidate {
|
|
17
|
+
return {
|
|
18
|
+
...candidate({
|
|
19
|
+
name: "suggest_followups",
|
|
20
|
+
label: "Suggest follow-ups",
|
|
21
|
+
description:
|
|
22
|
+
"Offer the user 2-4 optional follow-up directions for a completed substantive task. Call at most once.",
|
|
23
|
+
parameters: suggestFollowupsParameters,
|
|
24
|
+
async execute(_toolCallId, input) {
|
|
25
|
+
return result({ noted: true, count: input.items.length });
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
unattendedExecution: {
|
|
29
|
+
capability: "runtime-ui",
|
|
30
|
+
operation: "suggest-followups",
|
|
31
|
+
},
|
|
32
|
+
exposureMode: "direct",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
2
|
+
import { webSearchPiToolCandidate } from "../web/search";
|
|
3
|
+
import type { WebSearch } from "../web/search/api";
|
|
4
|
+
import { askUserPiToolCandidate } from "./ask-user";
|
|
5
|
+
import { suggestFollowupsPiToolCandidate } from "./followups";
|
|
6
|
+
import { updatePlanPiToolCandidate } from "./plan";
|
|
7
|
+
|
|
8
|
+
export { memoryPiToolCandidate } from "./memory";
|
|
9
|
+
export { normalizeUpdatePlanArguments } from "./plan";
|
|
10
|
+
export * from "./time";
|
|
11
|
+
|
|
12
|
+
export function basePiToolCandidates(webSearch?: WebSearch): PiToolCandidate[] {
|
|
13
|
+
return [
|
|
14
|
+
askUserPiToolCandidate(),
|
|
15
|
+
suggestFollowupsPiToolCandidate(),
|
|
16
|
+
updatePlanPiToolCandidate(),
|
|
17
|
+
...(webSearch ? [webSearchPiToolCandidate(webSearch)] : []),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
3
|
+
import { estimateStringTokens } from "../../lib/token-estimate";
|
|
4
|
+
import type { RuntimeMemoryPort } from "../../config/ports";
|
|
5
|
+
import type { RuntimeMemoryProfile } from "../../config/profile";
|
|
6
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
7
|
+
import { result } from "../runtime/result";
|
|
8
|
+
|
|
9
|
+
const setContextParameters = Type.Object({
|
|
10
|
+
label: StringEnum(["memory", "preferences"] as const, {
|
|
11
|
+
description:
|
|
12
|
+
"Context block to update: memory for durable facts and active context, preferences for tone and workflow choices.",
|
|
13
|
+
}),
|
|
14
|
+
content: Type.String({ description: "Text to store in the selected context block." }),
|
|
15
|
+
action: Type.Optional(StringEnum(["replace", "append"] as const, {
|
|
16
|
+
description: 'Whether to replace the block or append to it. Defaults to "replace".',
|
|
17
|
+
})),
|
|
18
|
+
}, { additionalProperties: false });
|
|
19
|
+
|
|
20
|
+
/** Writable hot-memory Tool restored from Think Session context blocks. */
|
|
21
|
+
export function memoryPiToolCandidate(
|
|
22
|
+
memory: RuntimeMemoryPort,
|
|
23
|
+
profile: Pick<
|
|
24
|
+
RuntimeMemoryProfile,
|
|
25
|
+
"memoryTokens" | "preferencesTokens"
|
|
26
|
+
>,
|
|
27
|
+
): PiToolCandidate {
|
|
28
|
+
const queues = new Map<string, Promise<void>>();
|
|
29
|
+
const tool: AgentTool<typeof setContextParameters> = {
|
|
30
|
+
name: "set_context",
|
|
31
|
+
label: "Set context",
|
|
32
|
+
description:
|
|
33
|
+
"Replace or append durable working context. Use memory for facts and active context; use preferences for tone, format, tools, and workflow preferences.",
|
|
34
|
+
parameters: setContextParameters,
|
|
35
|
+
async execute(_toolCallId, { label, content, action = "replace" }) {
|
|
36
|
+
const run = async () => {
|
|
37
|
+
const existing = action === "append"
|
|
38
|
+
? await memory.get(label) ?? ""
|
|
39
|
+
: "";
|
|
40
|
+
const separator = existing && !content.startsWith("\n") ? "\n" : "";
|
|
41
|
+
const updated = `${existing}${separator}${content}`;
|
|
42
|
+
const maxTokens = label === "memory"
|
|
43
|
+
? profile.memoryTokens
|
|
44
|
+
: profile.preferencesTokens;
|
|
45
|
+
const tokens = estimateStringTokens(updated);
|
|
46
|
+
if (tokens > maxTokens) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Block "${label}" exceeds maxTokens: ${tokens} > ${maxTokens}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
await memory.set(label, updated);
|
|
52
|
+
return result({ label, action, tokens, maxTokens });
|
|
53
|
+
};
|
|
54
|
+
const tail = queues.get(label) ?? Promise.resolve();
|
|
55
|
+
const output = tail.then(run);
|
|
56
|
+
const next = output.then(
|
|
57
|
+
() => { },
|
|
58
|
+
() => { },
|
|
59
|
+
);
|
|
60
|
+
queues.set(label, next);
|
|
61
|
+
void next.then(() => {
|
|
62
|
+
if (queues.get(label) === next) queues.delete(label);
|
|
63
|
+
});
|
|
64
|
+
return output;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
owner: "runtime-memory",
|
|
69
|
+
requiredExecutionLevel: "safe",
|
|
70
|
+
source: "action",
|
|
71
|
+
tool,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
3
|
+
import { result } from "../runtime/result";
|
|
4
|
+
import { candidate } from "./candidate";
|
|
5
|
+
|
|
6
|
+
const updatePlanParameters = Type.Object({
|
|
7
|
+
steps: Type.Array(Type.Object({
|
|
8
|
+
text: Type.String({
|
|
9
|
+
description: "The step, phrased as a short imperative.",
|
|
10
|
+
}),
|
|
11
|
+
status: StringEnum(["pending", "in_progress", "done"] as const, {
|
|
12
|
+
description:
|
|
13
|
+
'Current status. Use "done" for a finished step; never use "completed".',
|
|
14
|
+
}),
|
|
15
|
+
}, { additionalProperties: false }), {
|
|
16
|
+
description:
|
|
17
|
+
"The complete, ordered plan. Overwrites any previously reported plan.",
|
|
18
|
+
}),
|
|
19
|
+
}, { additionalProperties: false });
|
|
20
|
+
|
|
21
|
+
type UpdatePlanArguments = Static<typeof updatePlanParameters>;
|
|
22
|
+
|
|
23
|
+
function normalizeStepStatus(
|
|
24
|
+
status: unknown,
|
|
25
|
+
): UpdatePlanArguments["steps"][number]["status"] {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case "done":
|
|
28
|
+
case "completed": // Anthropic models sometimes output "completed"
|
|
29
|
+
return "done";
|
|
30
|
+
case "in_progress":
|
|
31
|
+
case "inProgress": // camelCase variant
|
|
32
|
+
return "in_progress";
|
|
33
|
+
default: // covers "pending", "todo" (Google), and any other unknown value
|
|
34
|
+
return "pending";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeUpdatePlanArguments(
|
|
39
|
+
input: unknown,
|
|
40
|
+
): UpdatePlanArguments {
|
|
41
|
+
if (input === null || typeof input !== "object") {
|
|
42
|
+
return input as UpdatePlanArguments;
|
|
43
|
+
}
|
|
44
|
+
const value = input as Record<string, unknown>;
|
|
45
|
+
let steps = value.steps;
|
|
46
|
+
if (typeof steps === "string") {
|
|
47
|
+
try {
|
|
48
|
+
steps = JSON.parse(steps);
|
|
49
|
+
} catch {
|
|
50
|
+
return input as UpdatePlanArguments;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!Array.isArray(steps)) return input as UpdatePlanArguments;
|
|
54
|
+
return {
|
|
55
|
+
...value,
|
|
56
|
+
steps: steps.map((step) => {
|
|
57
|
+
if (step === null || typeof step !== "object") return step;
|
|
58
|
+
const s = step as Record<string, unknown>;
|
|
59
|
+
const { step: stepAlias, title: titleAlias, ...rest } = s;
|
|
60
|
+
const alias = typeof stepAlias === "string" ? stepAlias : titleAlias;
|
|
61
|
+
const text = typeof rest.text === "string"
|
|
62
|
+
? rest.text
|
|
63
|
+
: typeof alias === "string"
|
|
64
|
+
? alias
|
|
65
|
+
: undefined;
|
|
66
|
+
return {
|
|
67
|
+
...rest,
|
|
68
|
+
...(text === undefined ? {} : { text }),
|
|
69
|
+
status: normalizeStepStatus(s.status),
|
|
70
|
+
};
|
|
71
|
+
}),
|
|
72
|
+
} as UpdatePlanArguments;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function updatePlanPiToolCandidate(): PiToolCandidate {
|
|
76
|
+
return {
|
|
77
|
+
...candidate({
|
|
78
|
+
name: "update_plan",
|
|
79
|
+
label: "Update plan",
|
|
80
|
+
description:
|
|
81
|
+
"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.",
|
|
82
|
+
parameters: updatePlanParameters,
|
|
83
|
+
prepareArguments: normalizeUpdatePlanArguments,
|
|
84
|
+
async execute(_toolCallId, input) {
|
|
85
|
+
return result({
|
|
86
|
+
ok: true,
|
|
87
|
+
total: input.steps.length,
|
|
88
|
+
done: input.steps.filter((step) => step.status === "done").length,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
unattendedExecution: {
|
|
93
|
+
capability: "runtime-ui",
|
|
94
|
+
operation: "update",
|
|
95
|
+
},
|
|
96
|
+
exposureMode: "direct",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import { Type } from "@earendil-works/pi-ai";
|
|
3
|
-
import type { PiToolCandidate } from "
|
|
3
|
+
import type { PiToolCandidate } from "../runtime/contracts";
|
|
4
4
|
|
|
5
5
|
export const GET_TIME_TOOL_NAME = "get_time";
|
|
6
6
|
|