@mushinjs/plugin-agent 0.0.0-stage → 0.2.0-next.20261112165327
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/LICENSE +21 -0
- package/dist/index.d.ts +274 -0
- package/dist/index.js +942 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt.d.ts +29 -0
- package/dist/prompt.js +53 -0
- package/dist/prompt.js.map +1 -0
- package/dist/settings.d.ts +75 -0
- package/dist/settings.js +128 -0
- package/dist/settings.js.map +1 -0
- package/dist/skills.d.ts +43 -0
- package/dist/skills.js +126 -0
- package/dist/skills.js.map +1 -0
- package/package.json +55 -3
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
import { SYSTEM_PROMPT, contextNote, skillBlock } from "./prompt.js";
|
|
2
|
+
import { SCENE_KEYS, defaultPolicy, formatSkillMarkdown, parseSkillMarkdown, policyInput, profileInput, today } from "./settings.js";
|
|
3
|
+
import { BUILTIN_SKILLS, skillFields, skillInput } from "./skills.js";
|
|
4
|
+
import { MushinError, Service, field, fromToolName, randomId, segmentsToText, toToolName, z } from "@mushinjs/core";
|
|
5
|
+
// src/index.ts
|
|
6
|
+
/** Commands the dashboard runs itself, without asking the model. */
|
|
7
|
+
const BUILTIN_COMMANDS = [
|
|
8
|
+
{
|
|
9
|
+
name: "new",
|
|
10
|
+
description: "Start a new conversation",
|
|
11
|
+
"zh-CN": "开始新对话"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "history",
|
|
15
|
+
description: "Open the full conversation history",
|
|
16
|
+
"zh-CN": "打开完整历史"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "model",
|
|
20
|
+
description: "Switch the Agent's model",
|
|
21
|
+
"zh-CN": "切换模型"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "status",
|
|
25
|
+
description: "Show how much context this conversation uses",
|
|
26
|
+
"zh-CN": "查看上下文用量"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "clear",
|
|
30
|
+
description: "Clear the input and close the conversation",
|
|
31
|
+
"zh-CN": "清空当前对话视图"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "help",
|
|
35
|
+
description: "Show what you can type",
|
|
36
|
+
"zh-CN": "查看可用的指令和技能"
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
const SETTINGS_KEY = "agent/settings/main";
|
|
40
|
+
const USAGE_KEY = "agent/settings/usage";
|
|
41
|
+
const MAX_RESULT_CHARS = 16e3;
|
|
42
|
+
const Config = z.object({
|
|
43
|
+
provider: z.string().optional().meta(field("Provider", "Provider or model profile for the Agent; empty uses the default provider", { "zh-CN": {
|
|
44
|
+
title: "提供商",
|
|
45
|
+
description: "智能体使用的提供商或配置方案;留空使用默认提供商"
|
|
46
|
+
} }, { choices: "providers.choices" })),
|
|
47
|
+
model: z.string().optional().meta(field("Model", "Model for the Agent; empty uses the provider's default", { "zh-CN": {
|
|
48
|
+
title: "模型",
|
|
49
|
+
description: "智能体使用的模型;留空使用提供商默认模型"
|
|
50
|
+
} })),
|
|
51
|
+
maxSteps: z.number().int().min(1).max(50).default(20).meta(field("Max steps", "Most tool-calling rounds in one reply", { "zh-CN": {
|
|
52
|
+
title: "最大步数",
|
|
53
|
+
description: "一次回复中最多调用工具的轮数"
|
|
54
|
+
} })),
|
|
55
|
+
confirmTimeout: z.number().int().min(10).default(600).meta(field("Approval timeout", "Seconds to wait for approval of a dangerous action", { "zh-CN": {
|
|
56
|
+
title: "确认超时",
|
|
57
|
+
description: "等待用户批准危险操作的秒数"
|
|
58
|
+
} })),
|
|
59
|
+
dailyTokens: z.number().int().min(0).default(0).meta(field("Daily token budget", "Input and output tokens the Agent may use per day; 0 means no limit", { "zh-CN": {
|
|
60
|
+
title: "每日 Token 预算",
|
|
61
|
+
description: "智能体每天最多使用的输入和输出 Token;0 表示不限制"
|
|
62
|
+
} }))
|
|
63
|
+
});
|
|
64
|
+
/** The turn used every tool round it may; the dashboard offers to continue. */
|
|
65
|
+
var StepLimit = class extends Error {
|
|
66
|
+
constructor(steps) {
|
|
67
|
+
super(`Stopped after ${steps} tool rounds; send another message to continue`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** After this many failures of one tool in a turn, its errors ask the model to change course. */
|
|
71
|
+
const FAILURE_HINT_AFTER = 3;
|
|
72
|
+
/** Runs conversations in which the LLM operates Mushin.js through the action registry. */
|
|
73
|
+
var Agent = class extends Service {
|
|
74
|
+
config;
|
|
75
|
+
static inject = [
|
|
76
|
+
"actions",
|
|
77
|
+
"llm",
|
|
78
|
+
"storage"
|
|
79
|
+
];
|
|
80
|
+
static Config = Config;
|
|
81
|
+
home;
|
|
82
|
+
running = /* @__PURE__ */ new Map();
|
|
83
|
+
pluginSkills = /* @__PURE__ */ new Map();
|
|
84
|
+
settingsCache;
|
|
85
|
+
constructor(ctx, config) {
|
|
86
|
+
super(ctx, "agent");
|
|
87
|
+
this.config = config;
|
|
88
|
+
this.home = ctx;
|
|
89
|
+
}
|
|
90
|
+
*[Service.init]() {
|
|
91
|
+
yield* this.defineActions();
|
|
92
|
+
yield () => {
|
|
93
|
+
for (const id of this.running.keys()) this.cancel(id);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Tools offered to the model: every action except the Agent's own and those turned off. */
|
|
97
|
+
async tools() {
|
|
98
|
+
const settings = await this.settings();
|
|
99
|
+
return this.home.actions.list().filter((action) => action.agent && !action.name.startsWith("agent.") && this.policyOf(settings, action) !== "off").map((action) => ({
|
|
100
|
+
name: toToolName(action.name),
|
|
101
|
+
description: describeTool(action),
|
|
102
|
+
parameters: action.inputSchema
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
async settings() {
|
|
106
|
+
if (!this.settingsCache) {
|
|
107
|
+
const saved = await this.home.storage.get(SETTINGS_KEY);
|
|
108
|
+
this.settingsCache = {
|
|
109
|
+
profiles: {},
|
|
110
|
+
toolPolicy: {},
|
|
111
|
+
disabledSkills: [],
|
|
112
|
+
...saved
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return this.settingsCache;
|
|
116
|
+
}
|
|
117
|
+
async updateSettings(change) {
|
|
118
|
+
const settings = await this.settings();
|
|
119
|
+
change(settings);
|
|
120
|
+
await this.home.storage.set(SETTINGS_KEY, settings);
|
|
121
|
+
this.home.emit("agent/changed");
|
|
122
|
+
return settings;
|
|
123
|
+
}
|
|
124
|
+
policyOf(settings, action) {
|
|
125
|
+
return settings.toolPolicy[action.name] ?? defaultPolicy(action.risk);
|
|
126
|
+
}
|
|
127
|
+
/** Every action with its policy, for the tool policy table. */
|
|
128
|
+
async policies() {
|
|
129
|
+
const settings = await this.settings();
|
|
130
|
+
return this.home.actions.list().filter((action) => action.agent && !action.name.startsWith("agent.")).map((action) => ({
|
|
131
|
+
name: action.name,
|
|
132
|
+
description: action.description,
|
|
133
|
+
plugin: action.plugin,
|
|
134
|
+
risk: action.risk,
|
|
135
|
+
client: action.client,
|
|
136
|
+
policy: this.policyOf(settings, action),
|
|
137
|
+
custom: action.name in settings.toolPolicy
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
/** Sets the policy of some actions; `null` goes back to the default for the action's risk. */
|
|
141
|
+
async setPolicy(changes) {
|
|
142
|
+
await this.updateSettings((settings) => {
|
|
143
|
+
for (const [name, policy] of Object.entries(changes)) if (policy === null) delete settings.toolPolicy[name];
|
|
144
|
+
else settings.toolPolicy[name] = policy;
|
|
145
|
+
});
|
|
146
|
+
return this.policies();
|
|
147
|
+
}
|
|
148
|
+
async profiles() {
|
|
149
|
+
return (await this.settings()).profiles;
|
|
150
|
+
}
|
|
151
|
+
async setProfile(scene, profile) {
|
|
152
|
+
return (await this.updateSettings((settings) => {
|
|
153
|
+
if (profile === null) delete settings.profiles[scene];
|
|
154
|
+
else settings.profiles[scene] = profile;
|
|
155
|
+
})).profiles[scene] ?? {};
|
|
156
|
+
}
|
|
157
|
+
/** Tokens used today and the daily budget. */
|
|
158
|
+
async usage() {
|
|
159
|
+
const saved = await this.home.storage.get(USAGE_KEY);
|
|
160
|
+
return {
|
|
161
|
+
...saved?.day === today() ? saved : {
|
|
162
|
+
day: today(),
|
|
163
|
+
input: 0,
|
|
164
|
+
output: 0
|
|
165
|
+
},
|
|
166
|
+
budget: this.config.dailyTokens
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async addUsage(usage) {
|
|
170
|
+
const current = await this.usage();
|
|
171
|
+
await this.home.storage.set(USAGE_KEY, {
|
|
172
|
+
day: current.day,
|
|
173
|
+
input: current.input + usage.inputTokens,
|
|
174
|
+
output: current.output + usage.outputTokens
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
emit(event) {
|
|
178
|
+
this.home.emit("agent/event", event);
|
|
179
|
+
}
|
|
180
|
+
async allSessions() {
|
|
181
|
+
const keys = await this.home.storage.list("agent");
|
|
182
|
+
return (await Promise.all(keys.map((key) => this.home.storage.get(key)))).filter((session) => !!session && Array.isArray(session.messages)).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
183
|
+
}
|
|
184
|
+
summary({ id, title, createdAt, updatedAt, scene, tags }) {
|
|
185
|
+
return {
|
|
186
|
+
id,
|
|
187
|
+
title,
|
|
188
|
+
createdAt,
|
|
189
|
+
updatedAt,
|
|
190
|
+
scene,
|
|
191
|
+
tags: tags ?? [],
|
|
192
|
+
running: this.running.has(id)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
async sessions() {
|
|
196
|
+
return (await this.allSessions()).map((session) => this.summary(session));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Conversations matching every tag (`scene:flowEditor`, `skill:<id>`, `status:running`, …) and
|
|
200
|
+
* containing the query in the title or in what the user or the Agent wrote.
|
|
201
|
+
*/
|
|
202
|
+
async search(options = {}) {
|
|
203
|
+
const query = options.query?.trim().toLowerCase();
|
|
204
|
+
const results = [];
|
|
205
|
+
for (const session of await this.allSessions()) {
|
|
206
|
+
const summary = this.summary(session);
|
|
207
|
+
const tags = [...summary.tags, `status:${summary.running ? "running" : "idle"}`];
|
|
208
|
+
if (options.tags?.some((tag) => !tags.includes(tag))) continue;
|
|
209
|
+
let snippet;
|
|
210
|
+
if (query) {
|
|
211
|
+
if (!session.title.toLowerCase().includes(query)) {
|
|
212
|
+
const hit = session.messages.filter((message) => message.role === "user" || message.role === "assistant").map((message) => message.role === "user" ? message.display ?? message.content : message.content).find((text) => typeof text === "string" && text.toLowerCase().includes(query));
|
|
213
|
+
if (!hit) continue;
|
|
214
|
+
const at = hit.toLowerCase().indexOf(query);
|
|
215
|
+
snippet = `${at > 30 ? "…" : ""}${hit.slice(Math.max(0, at - 30), at + query.length + 50).replace(/\s+/g, " ")}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
results.push({
|
|
219
|
+
...summary,
|
|
220
|
+
...snippet ? { snippet } : {}
|
|
221
|
+
});
|
|
222
|
+
if (results.length >= (options.limit ?? 50)) break;
|
|
223
|
+
}
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
/** Adds a skill from a plugin; removed when the plugin unloads. */
|
|
227
|
+
defineSkill(definition) {
|
|
228
|
+
const ctx = this.ctx;
|
|
229
|
+
const skill = {
|
|
230
|
+
...definition,
|
|
231
|
+
source: "plugin",
|
|
232
|
+
plugin: ctx.fiber.name,
|
|
233
|
+
enabled: true
|
|
234
|
+
};
|
|
235
|
+
return ctx.effect(() => {
|
|
236
|
+
this.pluginSkills.set(skill.id, skill);
|
|
237
|
+
return () => {
|
|
238
|
+
if (this.pluginSkills.get(skill.id) === skill) this.pluginSkills.delete(skill.id);
|
|
239
|
+
};
|
|
240
|
+
}, `agent.defineSkill(${definition.id})`);
|
|
241
|
+
}
|
|
242
|
+
/** Built-in, plugin and user skills; user skills win on the same id. */
|
|
243
|
+
async skills() {
|
|
244
|
+
const disabled = new Set((await this.settings()).disabledSkills);
|
|
245
|
+
const byId = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const skill of BUILTIN_SKILLS) byId.set(skill.id, {
|
|
247
|
+
...skill,
|
|
248
|
+
source: "builtin",
|
|
249
|
+
enabled: true
|
|
250
|
+
});
|
|
251
|
+
for (const skill of this.pluginSkills.values()) byId.set(skill.id, { ...skill });
|
|
252
|
+
const keys = await this.home.storage.list("agent/skills");
|
|
253
|
+
for (const skill of await Promise.all(keys.map((key) => this.home.storage.get(key)))) if (skill?.id) byId.set(skill.id, {
|
|
254
|
+
...skill,
|
|
255
|
+
source: "user",
|
|
256
|
+
enabled: skill.enabled !== false
|
|
257
|
+
});
|
|
258
|
+
return [...byId.values()].map((skill) => skill.source !== "user" && disabled.has(skill.id) ? {
|
|
259
|
+
...skill,
|
|
260
|
+
enabled: false
|
|
261
|
+
} : skill);
|
|
262
|
+
}
|
|
263
|
+
async skill(id) {
|
|
264
|
+
const skill = (await this.skills()).find((item) => item.id === id);
|
|
265
|
+
if (!skill) throw new MushinError("not_found", `No skill "${id}"`);
|
|
266
|
+
return skill;
|
|
267
|
+
}
|
|
268
|
+
/** Saves a user skill. With the id of a built-in or plugin skill it replaces that one until deleted. */
|
|
269
|
+
async saveSkill(input) {
|
|
270
|
+
const skill = {
|
|
271
|
+
...input,
|
|
272
|
+
source: "user",
|
|
273
|
+
enabled: input.enabled !== false
|
|
274
|
+
};
|
|
275
|
+
await this.home.storage.set(`agent/skills/${skill.id}`, skill);
|
|
276
|
+
this.home.emit("agent/changed");
|
|
277
|
+
return this.skill(skill.id);
|
|
278
|
+
}
|
|
279
|
+
async createSkill(input) {
|
|
280
|
+
const taken = new Set((await this.skills()).map((skill) => skill.id));
|
|
281
|
+
let id = input.id ?? (input.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || randomId(6).toLowerCase());
|
|
282
|
+
if (taken.has(id) && !input.id) id = `${id}-${randomId(4).toLowerCase()}`;
|
|
283
|
+
else if (taken.has(id)) throw new MushinError("conflict", `A skill with id "${id}" already exists`);
|
|
284
|
+
return this.saveSkill({
|
|
285
|
+
...input,
|
|
286
|
+
id
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async updateSkill(id, patch) {
|
|
290
|
+
const { source, plugin, enabled, ...rest } = await this.skill(id);
|
|
291
|
+
return this.saveSkill({
|
|
292
|
+
...rest,
|
|
293
|
+
...patch,
|
|
294
|
+
id,
|
|
295
|
+
enabled
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/** Deletes a user skill; a built-in or plugin skill it replaced comes back. */
|
|
299
|
+
async deleteSkill(id) {
|
|
300
|
+
if ((await this.skill(id)).source !== "user") throw new MushinError("forbidden", "Built-in and plugin skills cannot be deleted; turn them off instead");
|
|
301
|
+
await this.home.storage.delete(`agent/skills/${id}`);
|
|
302
|
+
this.home.emit("agent/changed");
|
|
303
|
+
return {
|
|
304
|
+
deleted: id,
|
|
305
|
+
restored: (await this.skills()).some((skill) => skill.id === id)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async setSkillEnabled(id, enabled) {
|
|
309
|
+
const current = await this.skill(id);
|
|
310
|
+
if (current.source === "user") {
|
|
311
|
+
await this.home.storage.set(`agent/skills/${id}`, {
|
|
312
|
+
...current,
|
|
313
|
+
enabled
|
|
314
|
+
});
|
|
315
|
+
this.home.emit("agent/changed");
|
|
316
|
+
return this.skill(id);
|
|
317
|
+
}
|
|
318
|
+
await this.updateSettings((settings) => {
|
|
319
|
+
settings.disabledSkills = enabled ? settings.disabledSkills.filter((item) => item !== id) : [.../* @__PURE__ */ new Set([...settings.disabledSkills, id])];
|
|
320
|
+
});
|
|
321
|
+
return this.skill(id);
|
|
322
|
+
}
|
|
323
|
+
/** Agent commands and the commands of every bot, for the `/` picker. */
|
|
324
|
+
commands() {
|
|
325
|
+
const bots = this.home.get("bots");
|
|
326
|
+
const names = new Map((bots?.list() ?? []).map((bot) => [bot.id, bot.name]));
|
|
327
|
+
const listed = bots?.commandList().commands ?? [];
|
|
328
|
+
return {
|
|
329
|
+
builtin: BUILTIN_COMMANDS,
|
|
330
|
+
bots: listed.filter((entry) => !entry.hidden).map((entry) => ({
|
|
331
|
+
name: entry.name,
|
|
332
|
+
description: entry.description,
|
|
333
|
+
usage: entry.usage,
|
|
334
|
+
permission: entry.permission ?? entry.role,
|
|
335
|
+
bot: entry.botId,
|
|
336
|
+
botName: names.get(entry.botId),
|
|
337
|
+
source: entry.source
|
|
338
|
+
}))
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async session(id) {
|
|
342
|
+
const session = await this.home.storage.get(`agent/${id}`);
|
|
343
|
+
if (!session) throw new MushinError("not_found", `No Agent session "${id}"`);
|
|
344
|
+
return session;
|
|
345
|
+
}
|
|
346
|
+
async createSession() {
|
|
347
|
+
const now = Date.now();
|
|
348
|
+
const session = {
|
|
349
|
+
id: randomId(),
|
|
350
|
+
title: "",
|
|
351
|
+
createdAt: now,
|
|
352
|
+
updatedAt: now,
|
|
353
|
+
tools: await this.tools(),
|
|
354
|
+
messages: []
|
|
355
|
+
};
|
|
356
|
+
await this.save(session);
|
|
357
|
+
return session;
|
|
358
|
+
}
|
|
359
|
+
async deleteSession(id) {
|
|
360
|
+
this.cancel(id);
|
|
361
|
+
await this.home.storage.delete(`agent/${id}`);
|
|
362
|
+
return { deleted: id };
|
|
363
|
+
}
|
|
364
|
+
save(session) {
|
|
365
|
+
session.updatedAt = Date.now();
|
|
366
|
+
return this.home.storage.set(`agent/${session.id}`, session);
|
|
367
|
+
}
|
|
368
|
+
turnContext(page, scene) {
|
|
369
|
+
const providers = this.home.llm.list();
|
|
370
|
+
const bots = this.home.get("bots")?.list() ?? [];
|
|
371
|
+
return {
|
|
372
|
+
page,
|
|
373
|
+
scene,
|
|
374
|
+
providers: providers.length,
|
|
375
|
+
ready: this.home.llm.isReady(),
|
|
376
|
+
bots: bots.length,
|
|
377
|
+
online: bots.filter((bot) => bot.status === "online").length
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/** A bot's chat around one message, for quoting into an Agent message. */
|
|
381
|
+
async quoteChat(input) {
|
|
382
|
+
const bots = this.home.get("bots");
|
|
383
|
+
if (!bots) throw new MushinError("unavailable", "Quoting a chat needs the bots plugin");
|
|
384
|
+
const record = bots.get(input.bot);
|
|
385
|
+
const list = await bots.messages(input.bot, {
|
|
386
|
+
channel: input.channel,
|
|
387
|
+
limit: 300
|
|
388
|
+
});
|
|
389
|
+
const index = list.findIndex((item) => item.id === input.id);
|
|
390
|
+
if (index < 0) throw new MushinError("not_found", "That message is no longer in the chat's recent messages");
|
|
391
|
+
const picked = list.slice(Math.max(0, index - (input.before ?? 6)), index + 1 + (input.after ?? 2));
|
|
392
|
+
return {
|
|
393
|
+
bot: input.bot,
|
|
394
|
+
botName: record.name,
|
|
395
|
+
channel: {
|
|
396
|
+
type: list[index].channel.type,
|
|
397
|
+
id: list[index].channel.id,
|
|
398
|
+
...list[index].channel.name ? { name: list[index].channel.name } : {}
|
|
399
|
+
},
|
|
400
|
+
focus: input.id,
|
|
401
|
+
lines: picked.map((item) => ({
|
|
402
|
+
id: item.id,
|
|
403
|
+
direction: item.direction,
|
|
404
|
+
user: item.user.name ?? item.user.id,
|
|
405
|
+
userId: item.user.id,
|
|
406
|
+
text: segmentsToText(item.segments).trim().slice(0, 500),
|
|
407
|
+
at: item.timestamp
|
|
408
|
+
}))
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/** Starts a turn and returns once it finishes. Progress streams as `agent/event` events. */
|
|
412
|
+
async send(sessionId, text, options = {}) {
|
|
413
|
+
if (!this.home.llm.list().length) throw new MushinError("unavailable", "Add an LLM provider before using the Agent");
|
|
414
|
+
const usage = await this.usage();
|
|
415
|
+
if (usage.budget && usage.input + usage.output >= usage.budget) throw new MushinError("unavailable", `The Agent used its daily budget of ${usage.budget} tokens; raise it in the Agent settings or wait until tomorrow`);
|
|
416
|
+
const session = sessionId ? await this.session(sessionId) : await this.createSession();
|
|
417
|
+
if (this.running.has(session.id)) throw new MushinError("conflict", "The Agent is still working on this conversation");
|
|
418
|
+
const running = {
|
|
419
|
+
abort: new AbortController(),
|
|
420
|
+
confirms: /* @__PURE__ */ new Map(),
|
|
421
|
+
usage: {
|
|
422
|
+
inputTokens: 0,
|
|
423
|
+
outputTokens: 0
|
|
424
|
+
},
|
|
425
|
+
failures: /* @__PURE__ */ new Map()
|
|
426
|
+
};
|
|
427
|
+
this.running.set(session.id, running);
|
|
428
|
+
const skills = options.skills?.length ? await Promise.all(options.skills.map((id) => this.skill(id))) : [];
|
|
429
|
+
if (!session.title) session.title = (text || skills.map((skill) => skill.name).join(", ")).replace(/\s+/g, " ").slice(0, 48);
|
|
430
|
+
if (!session.messages.length && options.scene) session.scene = {
|
|
431
|
+
key: options.scene.key,
|
|
432
|
+
...options.scene.label ? { label: options.scene.label } : {},
|
|
433
|
+
...options.page ? { page: options.page } : {}
|
|
434
|
+
};
|
|
435
|
+
session.tags = [.../* @__PURE__ */ new Set([...session.tags ?? [], ...turnTags(text, options, skills)])];
|
|
436
|
+
const quote = options.quote ? await this.quoteChat(options.quote) : void 0;
|
|
437
|
+
session.messages.push({
|
|
438
|
+
role: "user",
|
|
439
|
+
content: [
|
|
440
|
+
contextNote(this.turnContext(options.page, options.scene)),
|
|
441
|
+
...skills.map(skillBlock),
|
|
442
|
+
quote ? quoteBlock(quote) : "",
|
|
443
|
+
text
|
|
444
|
+
].filter(Boolean).join("\n\n"),
|
|
445
|
+
display: text,
|
|
446
|
+
...skills.length ? { skills: skills.map((skill) => skill.name) } : {},
|
|
447
|
+
...quote ? { quote } : {},
|
|
448
|
+
at: Date.now()
|
|
449
|
+
});
|
|
450
|
+
await this.save(session);
|
|
451
|
+
this.emit({
|
|
452
|
+
session: session.id,
|
|
453
|
+
type: "start"
|
|
454
|
+
});
|
|
455
|
+
try {
|
|
456
|
+
const final = await this.loop(session, running, options);
|
|
457
|
+
this.emit({
|
|
458
|
+
session: session.id,
|
|
459
|
+
type: "done",
|
|
460
|
+
text: final?.text ?? "",
|
|
461
|
+
usage: running.usage
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
const message = running.abort.signal.aborted ? "Stopped" : error instanceof Error ? error.message : String(error);
|
|
465
|
+
this.emit({
|
|
466
|
+
session: session.id,
|
|
467
|
+
type: "error",
|
|
468
|
+
error: message,
|
|
469
|
+
...error instanceof StepLimit && !running.abort.signal.aborted ? { code: "max_steps" } : {}
|
|
470
|
+
});
|
|
471
|
+
} finally {
|
|
472
|
+
this.running.delete(session.id);
|
|
473
|
+
await this.save(session);
|
|
474
|
+
}
|
|
475
|
+
return { session: session.id };
|
|
476
|
+
}
|
|
477
|
+
async loop(session, running, options) {
|
|
478
|
+
for (let step = 0; step < this.config.maxSteps; step++) {
|
|
479
|
+
const history = [{
|
|
480
|
+
role: "system",
|
|
481
|
+
content: SYSTEM_PROMPT
|
|
482
|
+
}, ...session.messages.map(stripMeta)];
|
|
483
|
+
let result;
|
|
484
|
+
for await (const event of this.home.llm.stream({
|
|
485
|
+
provider: this.config.provider,
|
|
486
|
+
model: this.config.model,
|
|
487
|
+
messages: history,
|
|
488
|
+
tools: session.tools,
|
|
489
|
+
signal: running.abort.signal
|
|
490
|
+
})) if (event.type === "delta") this.emit({
|
|
491
|
+
session: session.id,
|
|
492
|
+
type: "delta",
|
|
493
|
+
text: event.text
|
|
494
|
+
});
|
|
495
|
+
else if (event.type === "result") result = event.result;
|
|
496
|
+
if (!result) throw new Error("The provider returned no result");
|
|
497
|
+
if (result.usage) {
|
|
498
|
+
running.usage.inputTokens += result.usage.inputTokens;
|
|
499
|
+
running.usage.outputTokens += result.usage.outputTokens;
|
|
500
|
+
await this.addUsage(result.usage);
|
|
501
|
+
}
|
|
502
|
+
session.messages.push({
|
|
503
|
+
role: "assistant",
|
|
504
|
+
content: result.text,
|
|
505
|
+
...result.toolCalls.length ? { toolCalls: result.toolCalls } : {},
|
|
506
|
+
...result.raw ? { raw: result.raw } : {},
|
|
507
|
+
at: Date.now()
|
|
508
|
+
});
|
|
509
|
+
if (!result.toolCalls.length) return result;
|
|
510
|
+
if (result.finishReason === "length" || result.finishReason === "refusal") {
|
|
511
|
+
const reason = result.finishReason === "length" ? "The reply hit the output limit, so this tool call may be incomplete and was not run." : "The model declined this turn, so this tool call was not run.";
|
|
512
|
+
for (const call of result.toolCalls) session.messages.push({
|
|
513
|
+
role: "tool",
|
|
514
|
+
toolCallId: call.id,
|
|
515
|
+
content: reason,
|
|
516
|
+
isError: true,
|
|
517
|
+
at: Date.now()
|
|
518
|
+
});
|
|
519
|
+
throw new Error(reason);
|
|
520
|
+
}
|
|
521
|
+
for (const call of result.toolCalls) await this.runTool(session, running, call, options);
|
|
522
|
+
if (step === this.config.maxSteps - 2) {
|
|
523
|
+
const last = session.messages.at(-1);
|
|
524
|
+
last.content += `\n\n[This was the last tool round for this message (the limit is ${this.config.maxSteps}). Do not call tools now: tell the user what is done, what is left, and that they can send another message to continue.]`;
|
|
525
|
+
}
|
|
526
|
+
await this.save(session);
|
|
527
|
+
}
|
|
528
|
+
throw new StepLimit(this.config.maxSteps);
|
|
529
|
+
}
|
|
530
|
+
async runTool(session, running, call, options) {
|
|
531
|
+
const name = fromToolName(call.name);
|
|
532
|
+
const info = this.home.actions.get(name);
|
|
533
|
+
const reply = (content, isError = false) => {
|
|
534
|
+
session.messages.push({
|
|
535
|
+
role: "tool",
|
|
536
|
+
toolCallId: call.id,
|
|
537
|
+
content,
|
|
538
|
+
...isError ? { isError } : {},
|
|
539
|
+
at: Date.now()
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
let input;
|
|
543
|
+
try {
|
|
544
|
+
input = JSON.parse(call.arguments || "{}");
|
|
545
|
+
} catch {
|
|
546
|
+
this.emit({
|
|
547
|
+
session: session.id,
|
|
548
|
+
type: "tool-result",
|
|
549
|
+
id: call.id,
|
|
550
|
+
ok: false,
|
|
551
|
+
error: "Arguments are not valid JSON"
|
|
552
|
+
});
|
|
553
|
+
return reply("The arguments were not valid JSON. Send a JSON object that matches the tool schema.", true);
|
|
554
|
+
}
|
|
555
|
+
if (!info || !info.agent) {
|
|
556
|
+
this.emit({
|
|
557
|
+
session: session.id,
|
|
558
|
+
type: "tool-result",
|
|
559
|
+
id: call.id,
|
|
560
|
+
ok: false,
|
|
561
|
+
error: `Unknown tool ${call.name}`
|
|
562
|
+
});
|
|
563
|
+
return reply(`The tool ${call.name} is not available right now.`, true);
|
|
564
|
+
}
|
|
565
|
+
const policy = this.policyOf(await this.settings(), info);
|
|
566
|
+
if (policy === "off") {
|
|
567
|
+
this.emit({
|
|
568
|
+
session: session.id,
|
|
569
|
+
type: "tool-result",
|
|
570
|
+
id: call.id,
|
|
571
|
+
ok: false,
|
|
572
|
+
error: "Turned off in the Agent settings"
|
|
573
|
+
});
|
|
574
|
+
return reply(`The user turned ${call.name} off for the Agent. Do not use it; tell them if it is needed.`, true);
|
|
575
|
+
}
|
|
576
|
+
this.emit({
|
|
577
|
+
session: session.id,
|
|
578
|
+
type: "tool-call",
|
|
579
|
+
id: call.id,
|
|
580
|
+
action: name,
|
|
581
|
+
input,
|
|
582
|
+
risk: info.risk
|
|
583
|
+
});
|
|
584
|
+
if (policy === "confirm") {
|
|
585
|
+
this.emit({
|
|
586
|
+
session: session.id,
|
|
587
|
+
type: "confirm",
|
|
588
|
+
id: call.id,
|
|
589
|
+
action: name,
|
|
590
|
+
input
|
|
591
|
+
});
|
|
592
|
+
if (!await this.waitForApproval(running, call.id)) {
|
|
593
|
+
this.emit({
|
|
594
|
+
session: session.id,
|
|
595
|
+
type: "tool-result",
|
|
596
|
+
id: call.id,
|
|
597
|
+
ok: false,
|
|
598
|
+
error: "Declined by the user"
|
|
599
|
+
});
|
|
600
|
+
return reply("The user declined this action. Do not retry it.", true);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
const result = await this.home.actions.call(name, input, {
|
|
605
|
+
kind: "agent",
|
|
606
|
+
session: session.id,
|
|
607
|
+
client: options.client
|
|
608
|
+
});
|
|
609
|
+
this.emit({
|
|
610
|
+
session: session.id,
|
|
611
|
+
type: "tool-result",
|
|
612
|
+
id: call.id,
|
|
613
|
+
ok: true,
|
|
614
|
+
result
|
|
615
|
+
});
|
|
616
|
+
reply(truncate(JSON.stringify(result ?? { ok: true })));
|
|
617
|
+
} catch (error) {
|
|
618
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
619
|
+
this.emit({
|
|
620
|
+
session: session.id,
|
|
621
|
+
type: "tool-result",
|
|
622
|
+
id: call.id,
|
|
623
|
+
ok: false,
|
|
624
|
+
error: message
|
|
625
|
+
});
|
|
626
|
+
const failures = (running.failures.get(name) ?? 0) + 1;
|
|
627
|
+
running.failures.set(name, failures);
|
|
628
|
+
reply(`Error: ${message}${failures >= FAILURE_HINT_AFTER ? `\n${call.name} has failed ${failures} times in this reply. Stop retrying it with guessed inputs: take valid values from the error above or from the tool that lists them, or tell the user what is missing.` : ""}`, true);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
waitForApproval(running, callId) {
|
|
632
|
+
return new Promise((resolve) => {
|
|
633
|
+
const timer = setTimeout(() => finish(false), this.config.confirmTimeout * 1e3);
|
|
634
|
+
const finish = (approved) => {
|
|
635
|
+
clearTimeout(timer);
|
|
636
|
+
running.confirms.delete(callId);
|
|
637
|
+
resolve(approved);
|
|
638
|
+
};
|
|
639
|
+
running.confirms.set(callId, finish);
|
|
640
|
+
running.abort.signal.addEventListener("abort", () => finish(false), { once: true });
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
confirm(sessionId, callId, approved) {
|
|
644
|
+
const resolve = this.running.get(sessionId)?.confirms.get(callId);
|
|
645
|
+
if (!resolve) throw new MushinError("not_found", "No action is waiting for approval");
|
|
646
|
+
resolve(approved);
|
|
647
|
+
return { ok: true };
|
|
648
|
+
}
|
|
649
|
+
cancel(sessionId) {
|
|
650
|
+
const running = this.running.get(sessionId);
|
|
651
|
+
if (!running) return { ok: false };
|
|
652
|
+
running.abort.abort();
|
|
653
|
+
return { ok: true };
|
|
654
|
+
}
|
|
655
|
+
status() {
|
|
656
|
+
return {
|
|
657
|
+
ready: this.home.llm.isReady(),
|
|
658
|
+
providers: this.home.llm.list().length,
|
|
659
|
+
running: [...this.running.keys()]
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
*defineActions() {
|
|
663
|
+
const actions = this.home.actions;
|
|
664
|
+
yield actions.define({
|
|
665
|
+
name: "agent.status",
|
|
666
|
+
description: "Whether the Agent can run (a provider exists and passed its test).",
|
|
667
|
+
risk: "read",
|
|
668
|
+
input: z.object({}),
|
|
669
|
+
handler: () => this.status()
|
|
670
|
+
});
|
|
671
|
+
yield actions.define({
|
|
672
|
+
name: "agent.sessions",
|
|
673
|
+
description: "List Agent conversations, newest first.",
|
|
674
|
+
risk: "read",
|
|
675
|
+
input: z.object({}),
|
|
676
|
+
handler: () => this.sessions()
|
|
677
|
+
});
|
|
678
|
+
yield actions.define({
|
|
679
|
+
name: "agent.session",
|
|
680
|
+
description: "Get one Agent conversation with its messages.",
|
|
681
|
+
risk: "read",
|
|
682
|
+
input: z.object({ id: z.string() }),
|
|
683
|
+
handler: async ({ id }) => {
|
|
684
|
+
const { tools, ...session } = await this.session(id);
|
|
685
|
+
const now = new Set((await this.tools()).map((tool) => tool.name));
|
|
686
|
+
const toolsStale = tools.length !== now.size || tools.some((tool) => !now.has(tool.name));
|
|
687
|
+
return {
|
|
688
|
+
...session,
|
|
689
|
+
messages: session.messages.map(({ raw, ...message }) => message),
|
|
690
|
+
running: this.running.has(id),
|
|
691
|
+
toolsStale
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
yield actions.define({
|
|
696
|
+
name: "agent.send",
|
|
697
|
+
description: "Send a message to the Agent. Omit session to start a new conversation. Progress streams over the WebSocket.",
|
|
698
|
+
risk: "write",
|
|
699
|
+
input: z.object({
|
|
700
|
+
session: z.string().optional(),
|
|
701
|
+
text: z.string(),
|
|
702
|
+
page: z.string().optional(),
|
|
703
|
+
scene: z.object({
|
|
704
|
+
key: z.string(),
|
|
705
|
+
label: z.string().max(200).optional(),
|
|
706
|
+
hint: z.string().max(1e3).optional(),
|
|
707
|
+
state: z.record(z.string(), z.union([
|
|
708
|
+
z.string().max(200),
|
|
709
|
+
z.number(),
|
|
710
|
+
z.boolean()
|
|
711
|
+
])).optional(),
|
|
712
|
+
resource: z.string().max(200).optional()
|
|
713
|
+
}).optional().describe("The dashboard page as the dashboard describes it"),
|
|
714
|
+
skills: z.array(z.string()).max(5).optional().describe("Skill ids to apply to this message"),
|
|
715
|
+
quote: z.object({
|
|
716
|
+
bot: z.string(),
|
|
717
|
+
channel: z.string().describe("The chat's id"),
|
|
718
|
+
id: z.string().describe("The quoted message's record id, from bots.messages"),
|
|
719
|
+
before: z.number().int().min(0).max(30).optional(),
|
|
720
|
+
after: z.number().int().min(0).max(10).optional()
|
|
721
|
+
}).optional().describe("A message of a bot's chat to quote, with a few messages around it")
|
|
722
|
+
}).refine((input) => input.text.trim() || input.skills?.length, "Send some text or pick a skill"),
|
|
723
|
+
handler: ({ session, text, ...options }, caller) => this.send(session, text, {
|
|
724
|
+
client: caller.client,
|
|
725
|
+
...options
|
|
726
|
+
})
|
|
727
|
+
});
|
|
728
|
+
yield actions.define({
|
|
729
|
+
name: "agent.search",
|
|
730
|
+
description: "Search Agent conversations by text and tags (scene:<page kind>, skill:<id>, command:<name>, resource:<kind>:<id>, status:running).",
|
|
731
|
+
risk: "read",
|
|
732
|
+
input: z.object({
|
|
733
|
+
query: z.string().optional(),
|
|
734
|
+
tags: z.array(z.string()).optional(),
|
|
735
|
+
limit: z.number().int().min(1).max(200).optional()
|
|
736
|
+
}),
|
|
737
|
+
handler: (input) => this.search(input)
|
|
738
|
+
});
|
|
739
|
+
yield actions.define({
|
|
740
|
+
name: "agent.commands",
|
|
741
|
+
description: "Commands for the Agent's \"/\" picker: its own (new, history, …) and every bot's commands.",
|
|
742
|
+
risk: "read",
|
|
743
|
+
input: z.object({}),
|
|
744
|
+
handler: () => this.commands()
|
|
745
|
+
});
|
|
746
|
+
yield actions.define({
|
|
747
|
+
name: "agent.skills.list",
|
|
748
|
+
description: "List the Agent's skills: built in, from plugins, and written by the user.",
|
|
749
|
+
risk: "read",
|
|
750
|
+
input: z.object({}),
|
|
751
|
+
handler: () => this.skills()
|
|
752
|
+
});
|
|
753
|
+
yield actions.define({
|
|
754
|
+
name: "agent.skills.get",
|
|
755
|
+
description: "Get one Agent skill with its instructions.",
|
|
756
|
+
risk: "read",
|
|
757
|
+
input: z.object({ id: z.string() }),
|
|
758
|
+
handler: ({ id }) => this.skill(id)
|
|
759
|
+
});
|
|
760
|
+
const skillId = z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).describe("Lowercase letters, digits and dashes");
|
|
761
|
+
yield actions.define({
|
|
762
|
+
name: "agent.skills.create",
|
|
763
|
+
description: "Create a user skill. Omit id to make one from the name.",
|
|
764
|
+
risk: "write",
|
|
765
|
+
input: skillInput.extend({ id: skillId.optional() }),
|
|
766
|
+
handler: (input) => this.createSkill(input)
|
|
767
|
+
});
|
|
768
|
+
yield actions.define({
|
|
769
|
+
name: "agent.skills.update",
|
|
770
|
+
description: "Change a skill. Changing a built-in or plugin skill saves your own copy that replaces it until deleted.",
|
|
771
|
+
risk: "write",
|
|
772
|
+
input: z.object({
|
|
773
|
+
id: z.string(),
|
|
774
|
+
name: skillFields.name.optional(),
|
|
775
|
+
description: z.string().trim().max(300).optional(),
|
|
776
|
+
instructions: skillFields.instructions.optional(),
|
|
777
|
+
whenToUse: skillFields.whenToUse,
|
|
778
|
+
tools: skillFields.tools,
|
|
779
|
+
scenes: skillFields.scenes
|
|
780
|
+
}),
|
|
781
|
+
handler: ({ id, ...patch }) => this.updateSkill(id, patch)
|
|
782
|
+
});
|
|
783
|
+
yield actions.define({
|
|
784
|
+
name: "agent.skills.delete",
|
|
785
|
+
description: "Delete a user skill. A built-in or plugin skill it replaced comes back.",
|
|
786
|
+
risk: "write",
|
|
787
|
+
input: z.object({ id: z.string() }),
|
|
788
|
+
handler: ({ id }) => this.deleteSkill(id)
|
|
789
|
+
});
|
|
790
|
+
yield actions.define({
|
|
791
|
+
name: "agent.skills.setEnabled",
|
|
792
|
+
description: "Turn a skill on or off; off skills are not offered with $.",
|
|
793
|
+
risk: "write",
|
|
794
|
+
input: z.object({
|
|
795
|
+
id: z.string(),
|
|
796
|
+
enabled: z.boolean()
|
|
797
|
+
}),
|
|
798
|
+
handler: ({ id, enabled }) => this.setSkillEnabled(id, enabled)
|
|
799
|
+
});
|
|
800
|
+
yield actions.define({
|
|
801
|
+
name: "agent.skills.import",
|
|
802
|
+
description: "Import a skill from a SKILL.md file (YAML frontmatter with name and description, then Markdown instructions), as used by Claude and Codex skills.",
|
|
803
|
+
risk: "write",
|
|
804
|
+
input: z.object({
|
|
805
|
+
markdown: z.string().min(1).max(4e4),
|
|
806
|
+
replace: z.boolean().optional().describe("Overwrite a skill with the same id")
|
|
807
|
+
}),
|
|
808
|
+
handler: async ({ markdown, replace }) => {
|
|
809
|
+
let parsed;
|
|
810
|
+
try {
|
|
811
|
+
parsed = parseSkillMarkdown(markdown);
|
|
812
|
+
} catch (error) {
|
|
813
|
+
throw new MushinError("invalid_input", error instanceof Error ? error.message : String(error));
|
|
814
|
+
}
|
|
815
|
+
const input = skillInput.extend({ id: skillId.optional() }).parse(parsed);
|
|
816
|
+
if (replace && input.id) return this.saveSkill({
|
|
817
|
+
...input,
|
|
818
|
+
id: input.id
|
|
819
|
+
});
|
|
820
|
+
return this.createSkill(input);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
yield actions.define({
|
|
824
|
+
name: "agent.skills.export",
|
|
825
|
+
description: "A skill as a SKILL.md file.",
|
|
826
|
+
risk: "read",
|
|
827
|
+
input: z.object({ id: z.string() }),
|
|
828
|
+
handler: async ({ id }) => ({
|
|
829
|
+
fileName: "SKILL.md",
|
|
830
|
+
markdown: formatSkillMarkdown(await this.skill(id))
|
|
831
|
+
})
|
|
832
|
+
});
|
|
833
|
+
yield actions.define({
|
|
834
|
+
name: "agent.profiles.list",
|
|
835
|
+
description: "What the Agent says and offers on each dashboard page, as the user changed it: hint, welcome line, own suggestions, hidden built-in suggestions, recommended skills.",
|
|
836
|
+
risk: "read",
|
|
837
|
+
input: z.object({}),
|
|
838
|
+
handler: async () => ({
|
|
839
|
+
scenes: SCENE_KEYS,
|
|
840
|
+
profiles: await this.profiles()
|
|
841
|
+
})
|
|
842
|
+
});
|
|
843
|
+
yield actions.define({
|
|
844
|
+
name: "agent.profiles.update",
|
|
845
|
+
description: "Change what the Agent says and offers on one dashboard page. Send the whole profile; null goes back to the defaults.",
|
|
846
|
+
risk: "write",
|
|
847
|
+
input: z.object({
|
|
848
|
+
scene: z.enum(SCENE_KEYS),
|
|
849
|
+
profile: profileInput.nullable()
|
|
850
|
+
}),
|
|
851
|
+
handler: ({ scene, profile }) => this.setProfile(scene, profile)
|
|
852
|
+
});
|
|
853
|
+
yield actions.define({
|
|
854
|
+
name: "agent.policy.list",
|
|
855
|
+
description: "Every tool the Agent can use, with its plugin, risk and policy: auto (runs), confirm (asks the user first) or off (not offered).",
|
|
856
|
+
risk: "read",
|
|
857
|
+
input: z.object({}),
|
|
858
|
+
handler: () => this.policies()
|
|
859
|
+
});
|
|
860
|
+
yield actions.define({
|
|
861
|
+
name: "agent.policy.set",
|
|
862
|
+
description: "Set the policy of tools by action name: auto, confirm, or off; null goes back to the default (dangerous actions ask, others run). Off tools leave new conversations only.",
|
|
863
|
+
risk: "dangerous",
|
|
864
|
+
input: z.object({ policies: z.record(z.string(), policyInput.nullable()) }),
|
|
865
|
+
handler: ({ policies }) => this.setPolicy(policies)
|
|
866
|
+
});
|
|
867
|
+
yield actions.define({
|
|
868
|
+
name: "agent.usage",
|
|
869
|
+
description: "Tokens the Agent used today and its daily budget (0 means no limit).",
|
|
870
|
+
risk: "read",
|
|
871
|
+
input: z.object({}),
|
|
872
|
+
handler: () => this.usage()
|
|
873
|
+
});
|
|
874
|
+
yield actions.define({
|
|
875
|
+
name: "agent.confirm",
|
|
876
|
+
description: "Approve or decline an action the Agent is waiting on.",
|
|
877
|
+
risk: "write",
|
|
878
|
+
input: z.object({
|
|
879
|
+
session: z.string(),
|
|
880
|
+
id: z.string(),
|
|
881
|
+
approved: z.boolean()
|
|
882
|
+
}),
|
|
883
|
+
handler: ({ session, id, approved }) => this.confirm(session, id, approved)
|
|
884
|
+
});
|
|
885
|
+
yield actions.define({
|
|
886
|
+
name: "agent.cancel",
|
|
887
|
+
description: "Stop the Agent's current reply in a conversation.",
|
|
888
|
+
risk: "write",
|
|
889
|
+
input: z.object({ session: z.string() }),
|
|
890
|
+
handler: ({ session }) => this.cancel(session)
|
|
891
|
+
});
|
|
892
|
+
yield actions.define({
|
|
893
|
+
name: "agent.deleteSession",
|
|
894
|
+
description: "Delete an Agent conversation.",
|
|
895
|
+
risk: "write",
|
|
896
|
+
input: z.object({ id: z.string() }),
|
|
897
|
+
handler: ({ id }) => this.deleteSession(id)
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
function describeTool(action) {
|
|
902
|
+
const notes = [];
|
|
903
|
+
if (action.risk === "dangerous") notes.push("Dangerous: the user must approve it.");
|
|
904
|
+
if (action.client) notes.push("Runs in the user's dashboard; fails if none is connected.");
|
|
905
|
+
return [action.description, ...notes].join(" ");
|
|
906
|
+
}
|
|
907
|
+
/** Search tags for one message: its scene, skills, a leading command, and the page's resource. */
|
|
908
|
+
/** A quoted chat as the model reads it; the picked message is marked, with its record id for samples. */
|
|
909
|
+
function quoteBlock(quote) {
|
|
910
|
+
const where = quote.channel.type === "group" ? `group ${quote.channel.name ? `${quote.channel.name} ` : ""}(${quote.channel.id})` : `private chat with ${quote.channel.id}`;
|
|
911
|
+
const lines = quote.lines.map((line) => {
|
|
912
|
+
const time = new Date(line.at).toISOString().slice(11, 16);
|
|
913
|
+
const who = line.direction === "out" ? `${quote.botName} (bot)` : `${line.user} (${line.userId})`;
|
|
914
|
+
return `${line.id === quote.focus ? "→ " : " "}[${time}] ${who}: ${line.text}`;
|
|
915
|
+
});
|
|
916
|
+
return [
|
|
917
|
+
`<quote bot="${quote.bot}" chat="${quote.channel.id}" message="${quote.focus}">`,
|
|
918
|
+
`Messages from ${quote.botName} in the ${where}; the user picked the one marked →. To build or check a flow for it, pass sample { bot: "${quote.bot}", id: "${quote.focus}", channel: "${quote.channel.id}" } to flows.test or flows.preview.`,
|
|
919
|
+
...lines,
|
|
920
|
+
"</quote>"
|
|
921
|
+
].join("\n");
|
|
922
|
+
}
|
|
923
|
+
function turnTags(text, options, skills) {
|
|
924
|
+
const tags = [];
|
|
925
|
+
if (options.scene) tags.push(`scene:${options.scene.key}`);
|
|
926
|
+
if (options.scene?.resource) tags.push(`resource:${options.scene.resource}`);
|
|
927
|
+
for (const skill of skills) tags.push(`skill:${skill.id}`);
|
|
928
|
+
const command = /^\s*\/([\p{L}\p{N}_-]+)/u.exec(text);
|
|
929
|
+
if (command) tags.push(`command:${command[1]}`);
|
|
930
|
+
return tags;
|
|
931
|
+
}
|
|
932
|
+
function stripMeta(message) {
|
|
933
|
+
const { display, skills, quote, at, ...rest } = message;
|
|
934
|
+
return rest;
|
|
935
|
+
}
|
|
936
|
+
function truncate(text) {
|
|
937
|
+
if (text.length <= MAX_RESULT_CHARS) return text;
|
|
938
|
+
return `${text.slice(0, MAX_RESULT_CHARS)}… [truncated ${text.length - MAX_RESULT_CHARS} characters; ask for less, e.g. with a filter or limit]`;
|
|
939
|
+
}
|
|
940
|
+
export { Agent, Agent as default, BUILTIN_COMMANDS, BUILTIN_SKILLS, Config, SCENE_KEYS, SYSTEM_PROMPT, contextNote, defaultPolicy, formatSkillMarkdown, parseSkillMarkdown, policyInput, profileInput, skillBlock, skillFields, skillInput, today };
|
|
941
|
+
|
|
942
|
+
//# sourceMappingURL=index.js.map
|