@zaly/agent 0.0.3
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/README.md +50 -0
- package/dist/agent-BaD2kax9.mjs +68 -0
- package/dist/agent-BaD2kax9.mjs.map +1 -0
- package/dist/agent-GDLdDc2w.mjs +902 -0
- package/dist/agent-GDLdDc2w.mjs.map +1 -0
- package/dist/bash-QNMTQ1PD.mjs +142 -0
- package/dist/bash-QNMTQ1PD.mjs.map +1 -0
- package/dist/compactions-3AjPqjeZ.mjs +190 -0
- package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
- package/dist/edit-Qxl4w1C2.mjs +170 -0
- package/dist/edit-Qxl4w1C2.mjs.map +1 -0
- package/dist/env-UUFv5Mye.mjs +30 -0
- package/dist/env-UUFv5Mye.mjs.map +1 -0
- package/dist/fetch-D8cmmA3E.mjs +111 -0
- package/dist/fetch-D8cmmA3E.mjs.map +1 -0
- package/dist/find-Bah9SKWX.mjs +87 -0
- package/dist/find-Bah9SKWX.mjs.map +1 -0
- package/dist/grep-B2Ax7ewy.mjs +244 -0
- package/dist/grep-B2Ax7ewy.mjs.map +1 -0
- package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
- package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
- package/dist/index.d.mts +1605 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +948 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsonl-Dw_VBp46.mjs +134 -0
- package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
- package/dist/manager-D-lWBTHG.mjs +271 -0
- package/dist/manager-D-lWBTHG.mjs.map +1 -0
- package/dist/markdown-Bv4J8cg3.mjs +53 -0
- package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
- package/dist/masker-Bv9KjkDF.mjs +396 -0
- package/dist/masker-Bv9KjkDF.mjs.map +1 -0
- package/dist/model-_3ZA2xqm.mjs +27 -0
- package/dist/model-_3ZA2xqm.mjs.map +1 -0
- package/dist/notify-DpG_m9iP.mjs +96 -0
- package/dist/notify-DpG_m9iP.mjs.map +1 -0
- package/dist/read-Djsx2NZl.mjs +205 -0
- package/dist/read-Djsx2NZl.mjs.map +1 -0
- package/dist/registry-CFmR5G3J.mjs +564 -0
- package/dist/registry-CFmR5G3J.mjs.map +1 -0
- package/dist/search-CRw247ES.mjs +121 -0
- package/dist/search-CRw247ES.mjs.map +1 -0
- package/dist/session/claude.d.mts +59 -0
- package/dist/session/claude.d.mts.map +1 -0
- package/dist/session/claude.mjs +455 -0
- package/dist/session/claude.mjs.map +1 -0
- package/dist/session-CUUdWFC5.mjs +450 -0
- package/dist/session-CUUdWFC5.mjs.map +1 -0
- package/dist/session-Ds5nJz5A.d.mts +346 -0
- package/dist/session-Ds5nJz5A.d.mts.map +1 -0
- package/dist/session.d.mts +37 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +106 -0
- package/dist/session.mjs.map +1 -0
- package/dist/subagent-baqzjRMc.mjs +136 -0
- package/dist/subagent-baqzjRMc.mjs.map +1 -0
- package/dist/swarm-BJ3gQA1Z.mjs +194 -0
- package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
- package/dist/swarm-BuFx_xeY.mjs +109 -0
- package/dist/swarm-BuFx_xeY.mjs.map +1 -0
- package/dist/tasks-D8rARHZ1.mjs +65 -0
- package/dist/tasks-D8rARHZ1.mjs.map +1 -0
- package/dist/tasks-WmC270X2.mjs +615 -0
- package/dist/tasks-WmC270X2.mjs.map +1 -0
- package/dist/tools-DW1dbcCz.mjs +695 -0
- package/dist/tools-DW1dbcCz.mjs.map +1 -0
- package/dist/truncate-BSMXgiXj.mjs +102 -0
- package/dist/truncate-BSMXgiXj.mjs.map +1 -0
- package/dist/uuid-ByXP5SO5.mjs +59 -0
- package/dist/uuid-ByXP5SO5.mjs.map +1 -0
- package/dist/wakeup-BYqTnfSp.mjs +29 -0
- package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
- package/dist/write-D3L-92vj.mjs +89 -0
- package/dist/write-D3L-92vj.mjs.map +1 -0
- package/package.json +63 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import { i as __exportAll, n as parseBash, t as TOOLS } from "./tools-DW1dbcCz.mjs";
|
|
2
|
+
import { t as handlerRegistry } from "./registry-CFmR5G3J.mjs";
|
|
3
|
+
import { n as isUuidv7Like, r as uuidv7, t as isUuidv7 } from "./uuid-ByXP5SO5.mjs";
|
|
4
|
+
import { Emitter, formatDuration, isInstance, normPath, prettyPath, safeReadFile, safeStatAsync, safeStringify, toError } from "@zaly/shared";
|
|
5
|
+
import { LazyCache } from "@zaly/shared/cache";
|
|
6
|
+
import { BaseCollection } from "@zaly/shared/collection";
|
|
7
|
+
import { createRegistry } from "@zaly/shared/registry";
|
|
8
|
+
import { basename, dirname } from "pathe";
|
|
9
|
+
import { AiError, ContentTransform, attachmentToMeta, defineTool, errorToMeta, extractToolCalls, extractToolResults, metaToText, safeParseToolParams, sanitizeText, stringifyContent, toXml } from "@zaly/ai";
|
|
10
|
+
import { glob } from "@zaly/shared/glob";
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
13
|
+
import { Type } from "typebox";
|
|
14
|
+
//#region src/ctx.ts
|
|
15
|
+
var ctx_exports = /* @__PURE__ */ __exportAll({
|
|
16
|
+
AgentContext: () => AgentContext,
|
|
17
|
+
createAgent: () => createAgent,
|
|
18
|
+
createAgentContext: () => createAgentContext
|
|
19
|
+
});
|
|
20
|
+
var AgentContext = class extends Emitter {
|
|
21
|
+
#agent;
|
|
22
|
+
#opts;
|
|
23
|
+
#model;
|
|
24
|
+
#session;
|
|
25
|
+
#reasoning;
|
|
26
|
+
#cwd;
|
|
27
|
+
#cache = new LazyCache();
|
|
28
|
+
#tools;
|
|
29
|
+
#skills;
|
|
30
|
+
#prompt;
|
|
31
|
+
constructor(opts) {
|
|
32
|
+
super();
|
|
33
|
+
this.#opts = opts;
|
|
34
|
+
this.#cwd = normPath(opts.cwd);
|
|
35
|
+
this.#reasoning = opts.request?.reasoning?.effort ?? "medium";
|
|
36
|
+
this.#session = opts.session;
|
|
37
|
+
this.#model = opts.model;
|
|
38
|
+
this.#skills = opts.skills;
|
|
39
|
+
this.#prompt = opts.prompt ?? [];
|
|
40
|
+
this.#tools = opts.tools ?? [];
|
|
41
|
+
this.onEmitError = (error) => this.#opts.logger?.child("context").error(error);
|
|
42
|
+
this.on("model", async ({ model }) => {
|
|
43
|
+
if (!this.#session.started) return;
|
|
44
|
+
if (model) await this.session.update({ modelId: model.id });
|
|
45
|
+
}).on("reasoning", async ({ effort }) => {
|
|
46
|
+
if (!this.#session.started) return;
|
|
47
|
+
await this.session.update({ reasoning: effort });
|
|
48
|
+
}).on("cwd", ({ cwd }) => {
|
|
49
|
+
if (!this.#session.started) return;
|
|
50
|
+
this.#cache.forget("permissions");
|
|
51
|
+
this.session.update({ cwd });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
const [_masker, notifier] = await Promise.all([this.masker(), this.notifier()]);
|
|
56
|
+
if (!this.model) throw new Error("model is required to start agent session");
|
|
57
|
+
if (notifier) notifier.attach(this.agent);
|
|
58
|
+
await this.session.start({
|
|
59
|
+
cwd: this.cwd,
|
|
60
|
+
modelId: this.model.id,
|
|
61
|
+
reasoning: this.reasoning
|
|
62
|
+
});
|
|
63
|
+
for (const m of this.#opts.messages ?? []) await this.session.add(m);
|
|
64
|
+
}
|
|
65
|
+
attach(agent) {
|
|
66
|
+
if (this.#agent === agent) return;
|
|
67
|
+
if (this.#agent) throw new Error("agent already attached to context");
|
|
68
|
+
if (agent.started) throw new Error("cannot attach agent that has already started");
|
|
69
|
+
this.#agent = agent;
|
|
70
|
+
agent.once("start", () => this.start());
|
|
71
|
+
}
|
|
72
|
+
get messages() {
|
|
73
|
+
return this.session.messages;
|
|
74
|
+
}
|
|
75
|
+
get status() {
|
|
76
|
+
return this.#agent?.status;
|
|
77
|
+
}
|
|
78
|
+
get opts() {
|
|
79
|
+
return this.#opts;
|
|
80
|
+
}
|
|
81
|
+
get cwd() {
|
|
82
|
+
return this.#cwd;
|
|
83
|
+
}
|
|
84
|
+
set cwd(c) {
|
|
85
|
+
if (c === this.#cwd) return;
|
|
86
|
+
const prev = this.#cwd;
|
|
87
|
+
this.#cwd = normPath(c);
|
|
88
|
+
this.emit("cwd", {
|
|
89
|
+
cwd: this.#cwd,
|
|
90
|
+
prev
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
get signal() {
|
|
94
|
+
return this.#agent?.signal;
|
|
95
|
+
}
|
|
96
|
+
get agent() {
|
|
97
|
+
if (!this.#agent) throw new Error("agent not attached to context");
|
|
98
|
+
return this.#agent;
|
|
99
|
+
}
|
|
100
|
+
get reasoning() {
|
|
101
|
+
return this.#reasoning;
|
|
102
|
+
}
|
|
103
|
+
set reasoning(r) {
|
|
104
|
+
if (r === this.#reasoning) return;
|
|
105
|
+
const prev = this.#reasoning;
|
|
106
|
+
this.#reasoning = r;
|
|
107
|
+
this.emit("reasoning", {
|
|
108
|
+
effort: r,
|
|
109
|
+
prev
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
get session() {
|
|
113
|
+
return this.#session;
|
|
114
|
+
}
|
|
115
|
+
async useSession(s) {
|
|
116
|
+
const prev = this.#session;
|
|
117
|
+
this.#session = s;
|
|
118
|
+
const modelId = s.settings.modelId;
|
|
119
|
+
if (modelId && modelId !== this.model?.id) if (this.#opts.loadModel) this.model = await this.#opts.loadModel(modelId);
|
|
120
|
+
else {
|
|
121
|
+
const { loadModel } = await import("@zaly/ai");
|
|
122
|
+
this.model = await loadModel(modelId);
|
|
123
|
+
}
|
|
124
|
+
this.cwd = s.settings.cwd ?? this.cwd;
|
|
125
|
+
this.reasoning = s.settings.reasoning ?? this.reasoning;
|
|
126
|
+
await this.emit("session", {
|
|
127
|
+
prev,
|
|
128
|
+
session: s
|
|
129
|
+
});
|
|
130
|
+
if (this.#agent?.started) await s.start({
|
|
131
|
+
cwd: this.cwd,
|
|
132
|
+
modelId: this.model?.id,
|
|
133
|
+
reasoning: this.reasoning
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
get model() {
|
|
137
|
+
return this.#model;
|
|
138
|
+
}
|
|
139
|
+
set model(m) {
|
|
140
|
+
if (m === this.#model) return;
|
|
141
|
+
const prev = this.#model;
|
|
142
|
+
this.#model = m;
|
|
143
|
+
this.emit("model", {
|
|
144
|
+
model: m,
|
|
145
|
+
prev
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
set tools(tools) {
|
|
149
|
+
this.#tools = tools;
|
|
150
|
+
}
|
|
151
|
+
get tools() {
|
|
152
|
+
return [...this.#tools, ...this.skills?.tool ? [this.skills.tool] : []];
|
|
153
|
+
}
|
|
154
|
+
get prompt() {
|
|
155
|
+
return this.#prompt;
|
|
156
|
+
}
|
|
157
|
+
set prompt(p) {
|
|
158
|
+
this.#prompt = p;
|
|
159
|
+
}
|
|
160
|
+
async swarm() {
|
|
161
|
+
return this.#cache.need("swarm", async () => {
|
|
162
|
+
const { Swarm } = await import("./swarm-BJ3gQA1Z.mjs");
|
|
163
|
+
return new Swarm();
|
|
164
|
+
}, this.#opts.swarm);
|
|
165
|
+
}
|
|
166
|
+
set skills(s) {
|
|
167
|
+
if (s === this.#skills) return;
|
|
168
|
+
this.#skills = s;
|
|
169
|
+
this.emit("skills", { skills: s });
|
|
170
|
+
}
|
|
171
|
+
get skills() {
|
|
172
|
+
return this.#skills;
|
|
173
|
+
}
|
|
174
|
+
async notifier() {
|
|
175
|
+
return this.#cache.want("notifier", async (opts) => {
|
|
176
|
+
const { Notifier } = await import("./notify-DpG_m9iP.mjs");
|
|
177
|
+
return new Notifier(opts);
|
|
178
|
+
}, this.#opts.notify);
|
|
179
|
+
}
|
|
180
|
+
async masker() {
|
|
181
|
+
return this.#cache.want("masker", async (opts) => {
|
|
182
|
+
const { Masker } = await import("./masker-Bv9KjkDF.mjs");
|
|
183
|
+
return new Masker(this.agent, opts);
|
|
184
|
+
}, this.#opts.mask);
|
|
185
|
+
}
|
|
186
|
+
async permissions() {
|
|
187
|
+
return this.#cache.need("permissions", async (opts) => {
|
|
188
|
+
const { PermissionManager } = await import("./manager-D-lWBTHG.mjs");
|
|
189
|
+
return new PermissionManager({
|
|
190
|
+
...opts,
|
|
191
|
+
cwd: this.cwd
|
|
192
|
+
});
|
|
193
|
+
}, this.#opts.permissions);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
async function loadSession(spec) {
|
|
197
|
+
if (isInstance(spec)) return spec;
|
|
198
|
+
else {
|
|
199
|
+
const { Session } = await import("./session-CUUdWFC5.mjs").then((n) => n.n);
|
|
200
|
+
return await Session.load({ ...spec });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function createAgentContext(opts) {
|
|
204
|
+
const session = await loadSession(opts.session);
|
|
205
|
+
return new AgentContext({
|
|
206
|
+
...opts,
|
|
207
|
+
session
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function createAgent(opts) {
|
|
211
|
+
const [{ Agent }, ctx] = await Promise.all([import("./agent-GDLdDc2w.mjs"), createAgentContext(opts)]);
|
|
212
|
+
return new Agent(ctx);
|
|
213
|
+
}
|
|
214
|
+
const promptRegistry = createRegistry("prompt").from({
|
|
215
|
+
"AGENTS.md": (ctx) => import("./markdown-Bv4J8cg3.mjs").then((m) => m.agentsMdPrompt(ctx)),
|
|
216
|
+
"MEMORY.md": (ctx) => import("./markdown-Bv4J8cg3.mjs").then((m) => m.memoryMdPrompt(ctx)),
|
|
217
|
+
agent: () => import("./agent-BaD2kax9.mjs").then((m) => m.agentPrompt),
|
|
218
|
+
env: (ctx) => import("./env-UUFv5Mye.mjs").then((m) => m.prompt(ctx)),
|
|
219
|
+
model: (ctx) => import("./model-_3ZA2xqm.mjs").then((m) => m.prompt(ctx))
|
|
220
|
+
});
|
|
221
|
+
var PromptCollection = class extends BaseCollection {
|
|
222
|
+
list() {
|
|
223
|
+
const ret = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const k of promptRegistry.keys()) ret.set(k, {
|
|
225
|
+
name: k,
|
|
226
|
+
text: (ctx) => promptRegistry.load(k, ctx)
|
|
227
|
+
});
|
|
228
|
+
for (const r of this.registered) ret.set(r.name, r);
|
|
229
|
+
return [...ret.values()];
|
|
230
|
+
}
|
|
231
|
+
async render(ctx) {
|
|
232
|
+
const all = new Map(this.list().map((p) => [p.name, p]));
|
|
233
|
+
return (await Promise.all((ctx.prompts ?? this.active).map(async (p) => {
|
|
234
|
+
const def = all.get(p);
|
|
235
|
+
if (!def) throw new Error(`Unknown prompt: ${p}`);
|
|
236
|
+
return {
|
|
237
|
+
name: def.name,
|
|
238
|
+
text: typeof def.text === "string" ? def.text : await def.text(ctx)
|
|
239
|
+
};
|
|
240
|
+
}))).map((p) => ({
|
|
241
|
+
name: p.name,
|
|
242
|
+
text: p.text.trim()
|
|
243
|
+
})).filter((p) => p.text.length > 0);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
async function promptCollection() {
|
|
247
|
+
return new PromptCollection([
|
|
248
|
+
"agent",
|
|
249
|
+
"env",
|
|
250
|
+
"model",
|
|
251
|
+
"AGENTS.md",
|
|
252
|
+
"MEMORY.md"
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
const toolRegistry = createRegistry("tool").from({
|
|
256
|
+
agent_send: () => import("./swarm-BuFx_xeY.mjs").then((m) => m.agentSendTool),
|
|
257
|
+
agent_spawn: () => import("./swarm-BuFx_xeY.mjs").then((m) => m.agentSpawnTool),
|
|
258
|
+
bash: () => import("./bash-QNMTQ1PD.mjs").then((m) => m.bashTool),
|
|
259
|
+
edit: () => import("./edit-Qxl4w1C2.mjs").then((m) => m.editTool),
|
|
260
|
+
fetch: () => import("./fetch-D8cmmA3E.mjs").then((m) => m.fetchTool),
|
|
261
|
+
find: () => import("./find-Bah9SKWX.mjs").then((m) => m.findTool),
|
|
262
|
+
grep: () => import("./grep-B2Ax7ewy.mjs").then((m) => m.grepTool),
|
|
263
|
+
read: () => import("./read-Djsx2NZl.mjs").then((m) => m.readTool),
|
|
264
|
+
search: () => import("./search-CRw247ES.mjs").then((m) => m.searchTool),
|
|
265
|
+
subagent: () => import("./subagent-baqzjRMc.mjs").then((m) => m.subagentTool),
|
|
266
|
+
task_list: () => import("./tasks-D8rARHZ1.mjs").then((m) => m.taskListTool),
|
|
267
|
+
task_poll: () => import("./tasks-D8rARHZ1.mjs").then((m) => m.taskPollTool),
|
|
268
|
+
task_stop: () => import("./tasks-D8rARHZ1.mjs").then((m) => m.taskStopTool),
|
|
269
|
+
wakeup: () => import("./wakeup-BYqTnfSp.mjs").then((m) => m.wakeupTool),
|
|
270
|
+
write: () => import("./write-D3L-92vj.mjs").then((m) => m.writeTool)
|
|
271
|
+
});
|
|
272
|
+
var ToolCollection = class extends BaseCollection {
|
|
273
|
+
list() {
|
|
274
|
+
const ret = new Set(toolRegistry.keys());
|
|
275
|
+
for (const r of this.registered) ret.add(r.name);
|
|
276
|
+
return [...ret];
|
|
277
|
+
}
|
|
278
|
+
async load(tools) {
|
|
279
|
+
const load = async (t) => this.registered.findLast((r) => r.name === t) ?? toolRegistry.load(t);
|
|
280
|
+
return await Promise.all((tools ?? this.active).map((t) => load(t)));
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
async function toolCollection() {
|
|
284
|
+
return new ToolCollection([]);
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/commands.ts
|
|
288
|
+
const BASH_RE = /^!`(.*)`$/;
|
|
289
|
+
var Commands = class {
|
|
290
|
+
#opts;
|
|
291
|
+
catalog = /* @__PURE__ */ new Map();
|
|
292
|
+
#logger;
|
|
293
|
+
constructor(opts = {}) {
|
|
294
|
+
this.#opts = opts;
|
|
295
|
+
this.#logger = opts.logger;
|
|
296
|
+
}
|
|
297
|
+
async load() {
|
|
298
|
+
const paths = this.#opts.paths ?? [];
|
|
299
|
+
await Promise.all(paths.map(async (path) => await this.add(path)));
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
async add(path) {
|
|
303
|
+
path = normPath(path);
|
|
304
|
+
const name = basename(path, ".md");
|
|
305
|
+
const { parseFrontmatter } = await import("@zaly/shared/yaml");
|
|
306
|
+
try {
|
|
307
|
+
const content = await safeReadFile(path);
|
|
308
|
+
if (!content) throw new Error(`Failed to read command file at ${path}`);
|
|
309
|
+
const { fm, body } = await parseFrontmatter(content);
|
|
310
|
+
const args = {};
|
|
311
|
+
const fmArgs = fm.args ?? {};
|
|
312
|
+
for (const [o, opt] of Object.entries(fmArgs)) {
|
|
313
|
+
if (opt.type === "boolean") opt.default ??= false;
|
|
314
|
+
else if (opt.type === "string") opt.required ??= opt.default === void 0;
|
|
315
|
+
else {
|
|
316
|
+
this.#logger?.warn(`Unsupported argument type for \`--${o}\` in command **${name}** at \`${prettyPath(path)}\`\nOnly \`string\` and \`boolean\` are supported. Skipping this argument.`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
args[o] = opt;
|
|
320
|
+
}
|
|
321
|
+
const cmd = {
|
|
322
|
+
...fm,
|
|
323
|
+
args,
|
|
324
|
+
body,
|
|
325
|
+
name,
|
|
326
|
+
path
|
|
327
|
+
};
|
|
328
|
+
if (this.catalog.has(name)) return;
|
|
329
|
+
this.catalog.set(name, cmd);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
this.#logger?.error(`Error loading **command** from ${path}: ${error.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
get(name) {
|
|
335
|
+
return this.catalog.get(name);
|
|
336
|
+
}
|
|
337
|
+
async format(input, cmd) {
|
|
338
|
+
let args;
|
|
339
|
+
if (typeof input === "string") {
|
|
340
|
+
const { argsParse } = await import("@zaly/shared/args");
|
|
341
|
+
args = await argsParse(input, cmd.args);
|
|
342
|
+
} else args = input;
|
|
343
|
+
const { createTemplate } = await import("@zaly/shared/template");
|
|
344
|
+
const rendered = createTemplate(cmd.body, {
|
|
345
|
+
expr: this.#opts.expr,
|
|
346
|
+
format: (value) => {
|
|
347
|
+
if (value === args._) return args._.join(" ");
|
|
348
|
+
},
|
|
349
|
+
name: cmd.name
|
|
350
|
+
})({
|
|
351
|
+
...args,
|
|
352
|
+
args: args._,
|
|
353
|
+
raw: args.$
|
|
354
|
+
});
|
|
355
|
+
if (this.#opts.bash === false) return rendered;
|
|
356
|
+
const bash = Array.isArray(this.#opts.bash) ? this.#opts.bash : ["bash"];
|
|
357
|
+
const lines = rendered.split("\n");
|
|
358
|
+
const ret = [];
|
|
359
|
+
for (const token of this.#tokenize(lines, cmd)) {
|
|
360
|
+
let text;
|
|
361
|
+
if (token.type === "script") text = await exec(token.value, { bash });
|
|
362
|
+
else if (token.type === "block" && /^bash\s*!$/.test(token.lang ?? "")) text = await exec(token.body, { bash });
|
|
363
|
+
ret.push(text ?? token.value);
|
|
364
|
+
}
|
|
365
|
+
return ret.join("\n");
|
|
366
|
+
}
|
|
367
|
+
*#tokenize(lines, cmd) {
|
|
368
|
+
let block = [];
|
|
369
|
+
let marker = void 0;
|
|
370
|
+
let lang = void 0;
|
|
371
|
+
for (const line of lines) {
|
|
372
|
+
const m = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
373
|
+
const script = line.match(BASH_RE);
|
|
374
|
+
if (marker && line.match(marker)) {
|
|
375
|
+
block.push(line);
|
|
376
|
+
yield {
|
|
377
|
+
body: block.slice(1, -1).join("\n"),
|
|
378
|
+
lang,
|
|
379
|
+
type: "block",
|
|
380
|
+
value: block.join("\n")
|
|
381
|
+
};
|
|
382
|
+
marker = void 0;
|
|
383
|
+
} else if (!marker && m) {
|
|
384
|
+
marker = new RegExp(`^\\s*${m[1]}\\s*$`);
|
|
385
|
+
lang = m[2].trim() || void 0;
|
|
386
|
+
block = [line];
|
|
387
|
+
} else if (marker) block.push(line);
|
|
388
|
+
else if (script) yield {
|
|
389
|
+
type: "script",
|
|
390
|
+
value: script[1]
|
|
391
|
+
};
|
|
392
|
+
else yield {
|
|
393
|
+
type: "text",
|
|
394
|
+
value: line
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (marker) throw new Error(`Unclosed code block in command **${cmd.name}**`);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
async function exec(script, opts = {}) {
|
|
401
|
+
if (!script.trim()) return;
|
|
402
|
+
const { spawnCmd } = await import("@zaly/shared/process");
|
|
403
|
+
let result;
|
|
404
|
+
try {
|
|
405
|
+
result = (await spawnCmd(script, {
|
|
406
|
+
bash: opts.bash ?? true,
|
|
407
|
+
throw: true
|
|
408
|
+
}))?.trim() ?? "";
|
|
409
|
+
} catch (error) {
|
|
410
|
+
result = toError(error).message.trim();
|
|
411
|
+
}
|
|
412
|
+
const md = [];
|
|
413
|
+
if (script.includes("\n")) {
|
|
414
|
+
md.push("```bash");
|
|
415
|
+
md.push(script);
|
|
416
|
+
md.push("```");
|
|
417
|
+
md.push("");
|
|
418
|
+
md.push("```shellsession");
|
|
419
|
+
md.push(result);
|
|
420
|
+
md.push("```");
|
|
421
|
+
} else {
|
|
422
|
+
md.push("```shellsession");
|
|
423
|
+
md.push(`$ ${script}`);
|
|
424
|
+
md.push(result);
|
|
425
|
+
md.push("```");
|
|
426
|
+
}
|
|
427
|
+
md.push("");
|
|
428
|
+
return md.join("\n");
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/skills.ts
|
|
432
|
+
var Skills = class Skills {
|
|
433
|
+
catalog = /* @__PURE__ */ new Map();
|
|
434
|
+
#tool;
|
|
435
|
+
#opts;
|
|
436
|
+
constructor(opts = {}) {
|
|
437
|
+
this.#opts = opts;
|
|
438
|
+
}
|
|
439
|
+
static async load(opts) {
|
|
440
|
+
return new Skills(opts).reload();
|
|
441
|
+
}
|
|
442
|
+
/** (Re)scan project + user `.agent/skills/` directories. Wipes the
|
|
443
|
+
* current catalog and the cached tool, then repopulates. Safe to
|
|
444
|
+
* call mid-session — agent uses `this.tool` per-step so the next
|
|
445
|
+
* request after reload picks up the change. */
|
|
446
|
+
async reload() {
|
|
447
|
+
this.catalog.clear();
|
|
448
|
+
this.#tool = void 0;
|
|
449
|
+
const paths = this.#opts.paths ?? [];
|
|
450
|
+
const skills = await Promise.all(paths.map(async (path) => await this.#load(path)));
|
|
451
|
+
for (const skill of skills) if (skill && !this.catalog.has(skill.name)) this.catalog.set(skill.name, skill);
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
async #load(path) {
|
|
455
|
+
if (!path.endsWith("SKILL.md")) return;
|
|
456
|
+
const dir = dirname(path);
|
|
457
|
+
try {
|
|
458
|
+
const { meta, mtime, body } = await readSkill(path);
|
|
459
|
+
if (!meta.name || !meta.description || this.catalog.has(meta.name)) return;
|
|
460
|
+
return {
|
|
461
|
+
body,
|
|
462
|
+
desc: meta.description,
|
|
463
|
+
dir,
|
|
464
|
+
mtime,
|
|
465
|
+
name: meta.name,
|
|
466
|
+
path
|
|
467
|
+
};
|
|
468
|
+
} catch {}
|
|
469
|
+
}
|
|
470
|
+
async #update(entry) {
|
|
471
|
+
const updated = await this.#load(entry.path) ?? entry;
|
|
472
|
+
if (isDeepStrictEqual(updated, entry)) return entry;
|
|
473
|
+
this.catalog.set(entry.name, updated);
|
|
474
|
+
this.#tool = void 0;
|
|
475
|
+
return updated;
|
|
476
|
+
}
|
|
477
|
+
/** Skill base directories — pass each to `agent.permissions.addWorkspace`
|
|
478
|
+
* so the model can read bundled `references/` files without tripping
|
|
479
|
+
* permission asks. */
|
|
480
|
+
get dirs() {
|
|
481
|
+
return [...this.catalog.values()].map((s) => s.dir);
|
|
482
|
+
}
|
|
483
|
+
/** The activation tool. Returns `undefined` when the catalog is empty
|
|
484
|
+
* (the agent then omits skills entirely from the model's tool list,
|
|
485
|
+
* per the spec). Lazily built on first access; invalidated by
|
|
486
|
+
* `load()`. */
|
|
487
|
+
get tool() {
|
|
488
|
+
if (this.catalog.size === 0) return void 0;
|
|
489
|
+
return this.#tool ??= this.#buildTool();
|
|
490
|
+
}
|
|
491
|
+
#buildTool() {
|
|
492
|
+
const names = [...this.catalog.keys()];
|
|
493
|
+
return defineTool({
|
|
494
|
+
name: "skill",
|
|
495
|
+
desc: this.#buildCatalogDesc(),
|
|
496
|
+
parallel: true,
|
|
497
|
+
params: Type.Object({ name: Type.Union(names.map((n) => Type.Literal(n)), { description: "Name of the skill to activate. The full SKILL.md instructions are loaded as the tool result; bundled files in the skill's directory are listed but not read — fetch them via the `read` tool when the skill's instructions require them." }) }),
|
|
498
|
+
call: (args, ctx) => this.#call(args.name, ctx)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
async #call(requested, ctx) {
|
|
502
|
+
let skill = this.catalog.get(requested);
|
|
503
|
+
if (!skill) {
|
|
504
|
+
const available = [...this.catalog.keys()];
|
|
505
|
+
throw new AiError({
|
|
506
|
+
code: "UNKNOWN_SKILL",
|
|
507
|
+
data: {
|
|
508
|
+
available,
|
|
509
|
+
name: requested
|
|
510
|
+
},
|
|
511
|
+
message: `no skill named "${requested}". Available: ${available.join(", ")}`
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
skill = await this.#update(skill);
|
|
515
|
+
ctx.meta = {
|
|
516
|
+
desc: skill.desc,
|
|
517
|
+
mtime: skill.mtime,
|
|
518
|
+
name: skill.name,
|
|
519
|
+
path: skill.path
|
|
520
|
+
};
|
|
521
|
+
if (await this.isActivated(skill, ctx)) {
|
|
522
|
+
ctx.meta.unchanged = true;
|
|
523
|
+
return [{
|
|
524
|
+
content: `skill "${skill.name}" unchanged since last read: ${skill.path}`,
|
|
525
|
+
tag: "unchanged",
|
|
526
|
+
type: "meta"
|
|
527
|
+
}];
|
|
528
|
+
}
|
|
529
|
+
const references = await listReferences(skill.dir);
|
|
530
|
+
return [{
|
|
531
|
+
data: {
|
|
532
|
+
dir: skill.dir,
|
|
533
|
+
name: skill.name,
|
|
534
|
+
references
|
|
535
|
+
},
|
|
536
|
+
tag: "skill",
|
|
537
|
+
type: "meta"
|
|
538
|
+
}, {
|
|
539
|
+
text: skill.body,
|
|
540
|
+
type: "text"
|
|
541
|
+
}];
|
|
542
|
+
}
|
|
543
|
+
async isActivated(skill, ctx) {
|
|
544
|
+
const messages = ctx.messages ?? [];
|
|
545
|
+
for (const { m, $p, p } of extractToolResults(messages, ["skill"])) {
|
|
546
|
+
const id = m.id;
|
|
547
|
+
if (!id || !p.meta || ctx.isMasked?.(id, $p)) continue;
|
|
548
|
+
if (p.meta.unchanged) continue;
|
|
549
|
+
if (p.meta.name === skill.name) return p.meta.mtime === skill.mtime;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
async activate(name, agent) {
|
|
554
|
+
const ret = await agent.useTool("skill", { name }, `Skill "${name}" was activated by the user.`);
|
|
555
|
+
if (ret.result.meta?.unchanged) return;
|
|
556
|
+
return ret;
|
|
557
|
+
}
|
|
558
|
+
#buildCatalogDesc() {
|
|
559
|
+
return `Activate one of the available skills below. The skill's full SKILL.md instructions are loaded as the tool result; the model should follow them directly. Bundled files are listed in the result but not eagerly read — fetch the ones the skill's instructions reference via the \`read\` tool.\n\nAvailable skills:\n${[...this.catalog.values()].map((s) => `- ${s.name}: ${s.desc}`).join("\n")}`;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
async function readSkill(path) {
|
|
563
|
+
const s = await safeStatAsync(path);
|
|
564
|
+
if (!s?.isFile()) throw new Error(`Skill file not found at ${path}`);
|
|
565
|
+
const raw = await readFile(path, "utf8");
|
|
566
|
+
const { parseFrontmatter } = await import("@zaly/shared/yaml");
|
|
567
|
+
const { fm, body } = await parseFrontmatter(raw);
|
|
568
|
+
return {
|
|
569
|
+
body,
|
|
570
|
+
meta: fm,
|
|
571
|
+
mtime: s.mtimeMs
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
async function listReferences(dir) {
|
|
575
|
+
const out = [];
|
|
576
|
+
for await (const rel of glob("**", {
|
|
577
|
+
cwd: dir,
|
|
578
|
+
depth: 4,
|
|
579
|
+
type: "file"
|
|
580
|
+
})) {
|
|
581
|
+
if (rel === "SKILL.md" || rel.endsWith("/SKILL.md")) continue;
|
|
582
|
+
out.push(rel);
|
|
583
|
+
if (out.length >= 200) break;
|
|
584
|
+
}
|
|
585
|
+
return out.toSorted();
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/compaction/utils.ts
|
|
589
|
+
const LAMBDA = Math.LN2 / 60;
|
|
590
|
+
const KIND_WEIGHTS = {
|
|
591
|
+
edit: 2,
|
|
592
|
+
read: 1,
|
|
593
|
+
write: 3
|
|
594
|
+
};
|
|
595
|
+
const sorters = {
|
|
596
|
+
count: (a, b) => b[1].count - a[1].count,
|
|
597
|
+
key: (a, b) => a[0].localeCompare(b[0]),
|
|
598
|
+
score: (a, b) => b[1].score - a[1].score
|
|
599
|
+
};
|
|
600
|
+
function turnInfo(messages) {
|
|
601
|
+
const weights = [];
|
|
602
|
+
const turns = [];
|
|
603
|
+
let turn = 0;
|
|
604
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
605
|
+
if (messages[i].role === "assistant") turn++;
|
|
606
|
+
weights[i] = Math.exp(-LAMBDA * turn);
|
|
607
|
+
turns[i] = turn;
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
turns,
|
|
611
|
+
weights
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function toolStats(map, opts = {}) {
|
|
615
|
+
const limit = opts.limit ?? 50;
|
|
616
|
+
const sorter = sorters[opts.sort ?? "score"];
|
|
617
|
+
return [...map.entries()].toSorted(sorter).map(([_, stat]) => stat).filter((stat) => stat.count >= (opts.minCount ?? 1)).filter((stat) => stat.score >= (opts.minScore ?? 0)).slice(0, limit);
|
|
618
|
+
}
|
|
619
|
+
/** Walk assistant tool-calls for `read` / `write` / `edit` and tally
|
|
620
|
+
* per-path activity with frecency-weighted scoring. Returns the top-N
|
|
621
|
+
* by score. Each entry carries the per-kind breakdown so the
|
|
622
|
+
* summarizer can describe the *kind* of activity ("heavily edited",
|
|
623
|
+
* "read once, then written", "iteratively refined").
|
|
624
|
+
*
|
|
625
|
+
* Paths are canonicalized via the cwd recorded on each call's session
|
|
626
|
+
* node — relative paths are resolved against the cwd that was active
|
|
627
|
+
* when the call ran, and `~` is expanded. Calls under different cwds
|
|
628
|
+
* that target the same file thus collapse into one bucket. */
|
|
629
|
+
function extractFileUsage(messages, opts = {}) {
|
|
630
|
+
opts = {
|
|
631
|
+
minScore: .5,
|
|
632
|
+
...opts
|
|
633
|
+
};
|
|
634
|
+
const map = /* @__PURE__ */ new Map();
|
|
635
|
+
const { turns, weights } = turnInfo(messages);
|
|
636
|
+
for (const { m, p, $m: idx } of extractToolResults(messages, [
|
|
637
|
+
"read",
|
|
638
|
+
"write",
|
|
639
|
+
"edit"
|
|
640
|
+
])) {
|
|
641
|
+
const path = p.meta?.path;
|
|
642
|
+
if (!path) continue;
|
|
643
|
+
const entry = map.get(path) ?? {
|
|
644
|
+
count: 0,
|
|
645
|
+
edits: 0,
|
|
646
|
+
lastTs: 0,
|
|
647
|
+
lastTurn: Infinity,
|
|
648
|
+
path,
|
|
649
|
+
reads: 0,
|
|
650
|
+
score: 0,
|
|
651
|
+
writes: 0
|
|
652
|
+
};
|
|
653
|
+
if (p.name === "read") entry.reads++;
|
|
654
|
+
else if (p.name === "write") entry.writes++;
|
|
655
|
+
else entry.edits++;
|
|
656
|
+
entry.count++;
|
|
657
|
+
entry.score += weights[idx] * KIND_WEIGHTS[p.name];
|
|
658
|
+
if (turns[idx] < entry.lastTurn) entry.lastTurn = turns[idx];
|
|
659
|
+
if (m.ts && m.ts > entry.lastTs) entry.lastTs = m.ts;
|
|
660
|
+
map.set(path, entry);
|
|
661
|
+
}
|
|
662
|
+
return toolStats(map, opts);
|
|
663
|
+
}
|
|
664
|
+
/** Walk assistant tool-calls for `bash`, parse each command line, and
|
|
665
|
+
* tally per-command frecency. Plumbing commands (cat, head, grep, awk,
|
|
666
|
+
* sed, …) are filtered out — they dominate raw counts but carry no
|
|
667
|
+
* operational signal a summarizer would describe. Returns the top-N
|
|
668
|
+
* by frecency `score`. */
|
|
669
|
+
function extractBashUsage(messages, opts = {}) {
|
|
670
|
+
opts = {
|
|
671
|
+
minCount: 2,
|
|
672
|
+
minScore: .5,
|
|
673
|
+
...opts
|
|
674
|
+
};
|
|
675
|
+
const { turns, weights } = turnInfo(messages);
|
|
676
|
+
const map = /* @__PURE__ */ new Map();
|
|
677
|
+
for (const { m, p, $m: idx } of extractToolCalls(messages, ["bash"])) {
|
|
678
|
+
const cmd = safeParseToolParams(p.params)?.command;
|
|
679
|
+
if (!cmd) continue;
|
|
680
|
+
const result = parseBash(cmd);
|
|
681
|
+
if (!result.ok) continue;
|
|
682
|
+
const w = weights[idx] ?? 0;
|
|
683
|
+
const t = turns[idx] ?? Infinity;
|
|
684
|
+
for (const seg of result.segments) {
|
|
685
|
+
if (TOOLS[seg.cmd]) continue;
|
|
686
|
+
const key = `${seg.cmd} ${seg.args.join(" ")}`;
|
|
687
|
+
if (key.includes("\n")) continue;
|
|
688
|
+
const entry = map.get(key) ?? {
|
|
689
|
+
command: key,
|
|
690
|
+
count: 0,
|
|
691
|
+
lastTs: 0,
|
|
692
|
+
lastTurn: Infinity,
|
|
693
|
+
score: 0
|
|
694
|
+
};
|
|
695
|
+
entry.count++;
|
|
696
|
+
entry.score += w;
|
|
697
|
+
if (t < entry.lastTurn) entry.lastTurn = t;
|
|
698
|
+
if (m.ts && m.ts > entry.lastTs) entry.lastTs = m.ts;
|
|
699
|
+
map.set(key, entry);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return toolStats(map, opts);
|
|
703
|
+
}
|
|
704
|
+
function truncate(text, len) {
|
|
705
|
+
return text.length <= len ? text : `${text.slice(0, len)}…`;
|
|
706
|
+
}
|
|
707
|
+
/** Flatten a list of messages into a single tagged transcript. */
|
|
708
|
+
function extractConversation(messages, opts = {}) {
|
|
709
|
+
const transform = ContentTransform.create().drop("reasoning").map("tool-call", (part) => ({
|
|
710
|
+
content: [{
|
|
711
|
+
text: `${part.name}(${safeStringify(part.params)})`,
|
|
712
|
+
type: "text"
|
|
713
|
+
}],
|
|
714
|
+
tag: "tool-call",
|
|
715
|
+
type: "meta"
|
|
716
|
+
})).map("tool-result", (part) => {
|
|
717
|
+
const text = truncate(stringifyContent(part.content), opts.maxToolResultLen ?? 2e3);
|
|
718
|
+
return {
|
|
719
|
+
content: text === "" ? void 0 : [{
|
|
720
|
+
text,
|
|
721
|
+
type: "text"
|
|
722
|
+
}],
|
|
723
|
+
data: {
|
|
724
|
+
tool: part.name,
|
|
725
|
+
...part.isError ? { error: true } : {}
|
|
726
|
+
},
|
|
727
|
+
tag: "tool-result",
|
|
728
|
+
type: "meta"
|
|
729
|
+
};
|
|
730
|
+
}).pipe(attachmentToMeta("image", "pdf", "audio", "video")).pipe(errorToMeta()).pipe(metaToText()).pipe(sanitizeText());
|
|
731
|
+
const blocks = [];
|
|
732
|
+
let role = void 0;
|
|
733
|
+
for (const m of messages) {
|
|
734
|
+
const parts = typeof m.content === "string" ? [{
|
|
735
|
+
text: m.content,
|
|
736
|
+
type: "text"
|
|
737
|
+
}] : m.content;
|
|
738
|
+
const text = stringifyContent(transform.runSync(parts)).trim();
|
|
739
|
+
if (!text) continue;
|
|
740
|
+
let currentRole = m.role.charAt(0).toUpperCase() + m.role.slice(1);
|
|
741
|
+
currentRole = currentRole === "Tool" ? "Assistant" : currentRole;
|
|
742
|
+
blocks.push(role === currentRole ? text : `\n[${currentRole}]: ${text}`);
|
|
743
|
+
role = currentRole;
|
|
744
|
+
}
|
|
745
|
+
return toXml(blocks.join("\n").replace(/^\s+$/gm, "").trim(), "conversation", { indent: false });
|
|
746
|
+
}
|
|
747
|
+
function messageTail(messages, opts) {
|
|
748
|
+
const maxTokens = opts.keepTokens ?? 2e4;
|
|
749
|
+
messages = messages.toReversed();
|
|
750
|
+
const tail = [];
|
|
751
|
+
const queue = [];
|
|
752
|
+
let used = 0;
|
|
753
|
+
let last = void 0;
|
|
754
|
+
for (const m of messages) {
|
|
755
|
+
queue.unshift(m);
|
|
756
|
+
const usage = m.role === "assistant" ? m.meta?.usage : void 0;
|
|
757
|
+
if (!usage) continue;
|
|
758
|
+
const current = usage.input + usage.output + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
759
|
+
const delta = Math.max(0, (last ?? current) - current);
|
|
760
|
+
if (used + delta > maxTokens) break;
|
|
761
|
+
used += delta;
|
|
762
|
+
last = current;
|
|
763
|
+
tail.unshift(...queue.splice(0));
|
|
764
|
+
}
|
|
765
|
+
return tail;
|
|
766
|
+
}
|
|
767
|
+
const lastCol = (lastTurn, lastTs) => {
|
|
768
|
+
return `${lastTurn === Infinity ? "?" : `${lastTurn}t`} / ${lastTs > 0 ? formatDuration(lastTs) : "?"}`;
|
|
769
|
+
};
|
|
770
|
+
function formatBashUsage(commands) {
|
|
771
|
+
if (commands.length === 0) return "(no bash commands found)";
|
|
772
|
+
const lines = [];
|
|
773
|
+
lines.push("=== Top bash commands (sorted by frecency) ===");
|
|
774
|
+
lines.push(` ${"score".padStart(7)} ${"count".padStart(5)} ${"last".padEnd(14)} command`);
|
|
775
|
+
for (const c of commands) lines.push(` ${c.score.toFixed(2).padStart(7)} ${String(c.count).padStart(5)} ${lastCol(c.lastTurn, c.lastTs).padEnd(14)} ${c.command}`);
|
|
776
|
+
return toXml(lines.join("\n"), "bash-commands", { indent: false });
|
|
777
|
+
}
|
|
778
|
+
function formatFileUsage(files) {
|
|
779
|
+
if (files.length === 0) return "(no file ops found)";
|
|
780
|
+
const lines = [];
|
|
781
|
+
lines.push("=== Top files touched (sorted by frecency) ===");
|
|
782
|
+
lines.push(` ${"score".padStart(7)} ${"total".padStart(5)} r/w/e ${"last".padEnd(14)} path`);
|
|
783
|
+
for (const f of files) {
|
|
784
|
+
const rwe = `${f.reads}/${f.writes}/${f.edits}`.padEnd(12);
|
|
785
|
+
lines.push(` ${f.score.toFixed(2).padStart(7)} ${String(f.count).padStart(5)} ${rwe} ${lastCol(f.lastTurn, f.lastTs).padEnd(14)} ${f.path}`);
|
|
786
|
+
}
|
|
787
|
+
return toXml(lines.join("\n"), "files", { indent: false });
|
|
788
|
+
}
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/context/tokens.ts
|
|
791
|
+
const CHARS_PER_TOKEN = 4;
|
|
792
|
+
const ATTACHMENT_TOKENS = {
|
|
793
|
+
audio: 3e3,
|
|
794
|
+
image: 1500,
|
|
795
|
+
pdf: 8e3,
|
|
796
|
+
video: 5e3
|
|
797
|
+
};
|
|
798
|
+
function fromText(text) {
|
|
799
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
800
|
+
}
|
|
801
|
+
function sumTokens(stats) {
|
|
802
|
+
return stats.reduce((sum, s) => sum + s.tokens, 0);
|
|
803
|
+
}
|
|
804
|
+
function estimatePart(p) {
|
|
805
|
+
switch (p.type) {
|
|
806
|
+
case "reasoning":
|
|
807
|
+
case "text": return {
|
|
808
|
+
tokens: fromText(p.text),
|
|
809
|
+
type: p.type
|
|
810
|
+
};
|
|
811
|
+
case "tool-call": return {
|
|
812
|
+
kind: p.name,
|
|
813
|
+
tokens: fromText(p.name) + fromText(JSON.stringify(p.params ?? {})),
|
|
814
|
+
type: p.type
|
|
815
|
+
};
|
|
816
|
+
case "tool-result": {
|
|
817
|
+
const children = estimateContent(p.content);
|
|
818
|
+
return {
|
|
819
|
+
children,
|
|
820
|
+
kind: p.name,
|
|
821
|
+
tokens: fromText(p.name) + sumTokens(children),
|
|
822
|
+
type: p.type
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
case "image":
|
|
826
|
+
case "pdf":
|
|
827
|
+
case "audio":
|
|
828
|
+
case "video": return {
|
|
829
|
+
tokens: ATTACHMENT_TOKENS[p.type],
|
|
830
|
+
type: p.type
|
|
831
|
+
};
|
|
832
|
+
case "meta": return {
|
|
833
|
+
children: p.content ? estimateContent(p.content) : void 0,
|
|
834
|
+
kind: p.tag,
|
|
835
|
+
tokens: fromText(stringifyContent(p)),
|
|
836
|
+
type: p.type
|
|
837
|
+
};
|
|
838
|
+
case "error": return {
|
|
839
|
+
tokens: fromText(stringifyContent(p)),
|
|
840
|
+
type: p.type
|
|
841
|
+
};
|
|
842
|
+
default: throw new Error(`Unknown part type: ${p.type}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function estimateContent(content) {
|
|
846
|
+
if (typeof content === "string") return [{
|
|
847
|
+
tokens: fromText(content),
|
|
848
|
+
type: "text"
|
|
849
|
+
}];
|
|
850
|
+
let tokens = 0;
|
|
851
|
+
const children = [];
|
|
852
|
+
for (const p of content) {
|
|
853
|
+
const partStats = estimatePart(p);
|
|
854
|
+
tokens += partStats.tokens;
|
|
855
|
+
children.push(partStats);
|
|
856
|
+
}
|
|
857
|
+
return children;
|
|
858
|
+
}
|
|
859
|
+
function estimateMessage(m) {
|
|
860
|
+
const type = m.role;
|
|
861
|
+
const children = estimateContent(m.content);
|
|
862
|
+
return {
|
|
863
|
+
children,
|
|
864
|
+
kind: m.role === "system" ? m.meta?.kind : void 0,
|
|
865
|
+
tokens: sumTokens(children),
|
|
866
|
+
type
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function estimatePrompt(prompt, tools) {
|
|
870
|
+
const children = prompt.map((p) => ({
|
|
871
|
+
kind: typeof p === "string" ? void 0 : p.name,
|
|
872
|
+
tokens: fromText(typeof p === "string" ? p : p.text),
|
|
873
|
+
type: "prompt"
|
|
874
|
+
}));
|
|
875
|
+
children.push(...tools?.map((t) => ({
|
|
876
|
+
kind: t.name,
|
|
877
|
+
tokens: fromText(t.name) + fromText(JSON.stringify(t.params ?? {})),
|
|
878
|
+
type: "tool-schema"
|
|
879
|
+
})) ?? []);
|
|
880
|
+
return {
|
|
881
|
+
children,
|
|
882
|
+
tokens: sumTokens(children),
|
|
883
|
+
type: "system-prompt"
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function addCount(stat, parent, expand) {
|
|
887
|
+
const keys = stat.kind ? [stat.type, stat.kind] : [stat.type];
|
|
888
|
+
let child;
|
|
889
|
+
for (const key of keys) {
|
|
890
|
+
parent.children ??= /* @__PURE__ */ new Map();
|
|
891
|
+
child = parent.children.get(key);
|
|
892
|
+
if (!child) {
|
|
893
|
+
child = {
|
|
894
|
+
children: /* @__PURE__ */ new Map(),
|
|
895
|
+
count: 0,
|
|
896
|
+
key,
|
|
897
|
+
tokens: 0
|
|
898
|
+
};
|
|
899
|
+
parent.children.set(key, child);
|
|
900
|
+
}
|
|
901
|
+
child.tokens += stat.tokens;
|
|
902
|
+
child.count++;
|
|
903
|
+
parent = child;
|
|
904
|
+
}
|
|
905
|
+
if (stat.children && child && (!expand || expand(stat))) for (const c of stat.children) addCount(c, child, expand);
|
|
906
|
+
}
|
|
907
|
+
function tokenStats(msgs, opts = {}) {
|
|
908
|
+
const root = {
|
|
909
|
+
children: /* @__PURE__ */ new Map(),
|
|
910
|
+
count: 0,
|
|
911
|
+
key: "TOTAL",
|
|
912
|
+
tokens: 0
|
|
913
|
+
};
|
|
914
|
+
const counts = [];
|
|
915
|
+
if (opts.prompt) counts.push(estimatePrompt(opts.prompt, opts.tools));
|
|
916
|
+
counts.push(...msgs.map(estimateMessage));
|
|
917
|
+
for (const stats of counts) {
|
|
918
|
+
root.tokens += stats.tokens;
|
|
919
|
+
root.count++;
|
|
920
|
+
addCount(stats, root, opts.expand);
|
|
921
|
+
}
|
|
922
|
+
return root;
|
|
923
|
+
}
|
|
924
|
+
function fmtStats(s, indent = 0) {
|
|
925
|
+
return `${(" ".repeat(indent) + s.key).padEnd(28)} ${fmt(s.count).padStart(6)}x ${fmt(s.tokens).padStart(14)}`;
|
|
926
|
+
}
|
|
927
|
+
function formatTokenStats(s, indent = 0) {
|
|
928
|
+
const ret = [];
|
|
929
|
+
const children = s.children ? [...s.children.values()].toSorted((a, b) => b.tokens - a.tokens) : [];
|
|
930
|
+
for (const c of children) {
|
|
931
|
+
ret.push(fmtStats(c, indent));
|
|
932
|
+
if (c.children?.size) ret.push(formatTokenStats(c, indent + 1));
|
|
933
|
+
if (indent === 0) ret.push("─".repeat(51));
|
|
934
|
+
}
|
|
935
|
+
if (indent === 0) {
|
|
936
|
+
ret.unshift("─".repeat(51));
|
|
937
|
+
ret.push(fmtStats(s, indent));
|
|
938
|
+
ret.push("─".repeat(51));
|
|
939
|
+
}
|
|
940
|
+
return ret.join("\n");
|
|
941
|
+
}
|
|
942
|
+
function fmt(n) {
|
|
943
|
+
return n.toLocaleString("en-US").replace(/,/g, "_");
|
|
944
|
+
}
|
|
945
|
+
//#endregion
|
|
946
|
+
export { Commands, Skills, createAgent, estimatePart, extractBashUsage, extractConversation, extractFileUsage, formatBashUsage, formatFileUsage, formatTokenStats, handlerRegistry, isUuidv7, isUuidv7Like, messageTail, promptCollection, promptRegistry, ctx_exports as t, tokenStats, toolCollection, toolRegistry, uuidv7 };
|
|
947
|
+
|
|
948
|
+
//# sourceMappingURL=index.mjs.map
|