@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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/agent-BaD2kax9.mjs +68 -0
  4. package/dist/agent-BaD2kax9.mjs.map +1 -0
  5. package/dist/agent-GDLdDc2w.mjs +902 -0
  6. package/dist/agent-GDLdDc2w.mjs.map +1 -0
  7. package/dist/bash-QNMTQ1PD.mjs +142 -0
  8. package/dist/bash-QNMTQ1PD.mjs.map +1 -0
  9. package/dist/compactions-3AjPqjeZ.mjs +190 -0
  10. package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
  11. package/dist/edit-Qxl4w1C2.mjs +170 -0
  12. package/dist/edit-Qxl4w1C2.mjs.map +1 -0
  13. package/dist/env-UUFv5Mye.mjs +30 -0
  14. package/dist/env-UUFv5Mye.mjs.map +1 -0
  15. package/dist/fetch-D8cmmA3E.mjs +111 -0
  16. package/dist/fetch-D8cmmA3E.mjs.map +1 -0
  17. package/dist/find-Bah9SKWX.mjs +87 -0
  18. package/dist/find-Bah9SKWX.mjs.map +1 -0
  19. package/dist/grep-B2Ax7ewy.mjs +244 -0
  20. package/dist/grep-B2Ax7ewy.mjs.map +1 -0
  21. package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
  22. package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
  23. package/dist/index.d.mts +1605 -0
  24. package/dist/index.d.mts.map +1 -0
  25. package/dist/index.mjs +948 -0
  26. package/dist/index.mjs.map +1 -0
  27. package/dist/jsonl-Dw_VBp46.mjs +134 -0
  28. package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
  29. package/dist/manager-D-lWBTHG.mjs +271 -0
  30. package/dist/manager-D-lWBTHG.mjs.map +1 -0
  31. package/dist/markdown-Bv4J8cg3.mjs +53 -0
  32. package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
  33. package/dist/masker-Bv9KjkDF.mjs +396 -0
  34. package/dist/masker-Bv9KjkDF.mjs.map +1 -0
  35. package/dist/model-_3ZA2xqm.mjs +27 -0
  36. package/dist/model-_3ZA2xqm.mjs.map +1 -0
  37. package/dist/notify-DpG_m9iP.mjs +96 -0
  38. package/dist/notify-DpG_m9iP.mjs.map +1 -0
  39. package/dist/read-Djsx2NZl.mjs +205 -0
  40. package/dist/read-Djsx2NZl.mjs.map +1 -0
  41. package/dist/registry-CFmR5G3J.mjs +564 -0
  42. package/dist/registry-CFmR5G3J.mjs.map +1 -0
  43. package/dist/search-CRw247ES.mjs +121 -0
  44. package/dist/search-CRw247ES.mjs.map +1 -0
  45. package/dist/session/claude.d.mts +59 -0
  46. package/dist/session/claude.d.mts.map +1 -0
  47. package/dist/session/claude.mjs +455 -0
  48. package/dist/session/claude.mjs.map +1 -0
  49. package/dist/session-CUUdWFC5.mjs +450 -0
  50. package/dist/session-CUUdWFC5.mjs.map +1 -0
  51. package/dist/session-Ds5nJz5A.d.mts +346 -0
  52. package/dist/session-Ds5nJz5A.d.mts.map +1 -0
  53. package/dist/session.d.mts +37 -0
  54. package/dist/session.d.mts.map +1 -0
  55. package/dist/session.mjs +106 -0
  56. package/dist/session.mjs.map +1 -0
  57. package/dist/subagent-baqzjRMc.mjs +136 -0
  58. package/dist/subagent-baqzjRMc.mjs.map +1 -0
  59. package/dist/swarm-BJ3gQA1Z.mjs +194 -0
  60. package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
  61. package/dist/swarm-BuFx_xeY.mjs +109 -0
  62. package/dist/swarm-BuFx_xeY.mjs.map +1 -0
  63. package/dist/tasks-D8rARHZ1.mjs +65 -0
  64. package/dist/tasks-D8rARHZ1.mjs.map +1 -0
  65. package/dist/tasks-WmC270X2.mjs +615 -0
  66. package/dist/tasks-WmC270X2.mjs.map +1 -0
  67. package/dist/tools-DW1dbcCz.mjs +695 -0
  68. package/dist/tools-DW1dbcCz.mjs.map +1 -0
  69. package/dist/truncate-BSMXgiXj.mjs +102 -0
  70. package/dist/truncate-BSMXgiXj.mjs.map +1 -0
  71. package/dist/uuid-ByXP5SO5.mjs +59 -0
  72. package/dist/uuid-ByXP5SO5.mjs.map +1 -0
  73. package/dist/wakeup-BYqTnfSp.mjs +29 -0
  74. package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
  75. package/dist/write-D3L-92vj.mjs +89 -0
  76. package/dist/write-D3L-92vj.mjs.map +1 -0
  77. package/package.json +63 -0
@@ -0,0 +1,53 @@
1
+ import { findUp, normPath, safeReadFile, safeStat } from "@zaly/shared";
2
+ import { join } from "pathe";
3
+ import { zalyPaths } from "@zaly/shared/paths";
4
+ //#region src/prompt/markdown.ts
5
+ async function markdownPrompt(ctx, opts) {
6
+ const resources = [];
7
+ const project = zalyPaths.project(ctx.cwd);
8
+ if (opts.dotUser) {
9
+ const checks = opts.dotUser.flatMap((name) => [join(zalyPaths.config, name), `~/.agents/${name}`]);
10
+ for (const check of checks) {
11
+ const file = normPath(check);
12
+ if (safeStat(file)?.isFile()) {
13
+ resources.push(file);
14
+ break;
15
+ }
16
+ }
17
+ }
18
+ if (opts.project) {
19
+ const file = findUp(ctx.cwd, opts.project, {
20
+ stop: project.stop,
21
+ type: "file"
22
+ });
23
+ if (file) resources.push(file);
24
+ }
25
+ if (resources.length === 0) return "";
26
+ const contents = await Promise.all(resources.map((r) => safeReadFile(r)));
27
+ const ret = [];
28
+ for (let i = 0; i < resources.length; i++) {
29
+ const content = contents[i];
30
+ if (!content) continue;
31
+ const r = resources[i];
32
+ ret.push(`## ${opts.name} (${r})\n\n${content.trim()}`);
33
+ }
34
+ return ret.join("\n\n");
35
+ }
36
+ async function agentsMdPrompt(ctx) {
37
+ return markdownPrompt(ctx, {
38
+ dotUser: ["AGENTS.md"],
39
+ name: "AGENTS.md",
40
+ project: ["AGENTS.md", "CLAUDE.md"]
41
+ });
42
+ }
43
+ async function memoryMdPrompt(ctx) {
44
+ return markdownPrompt(ctx, {
45
+ dotUser: ["MEMORY.md"],
46
+ name: "MEMORY.md",
47
+ project: [".agents/MEMORY.md"]
48
+ });
49
+ }
50
+ //#endregion
51
+ export { agentsMdPrompt, memoryMdPrompt };
52
+
53
+ //# sourceMappingURL=markdown-Bv4J8cg3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-Bv4J8cg3.mjs","names":[],"sources":["../src/prompt/markdown.ts"],"sourcesContent":["import type { PromptCtx } from \"./registry.ts\"\n\nimport { findUp, normPath, safeReadFile, safeStat } from \"@zaly/shared\"\nimport { zalyPaths } from \"@zaly/shared/paths\"\nimport { join } from \"pathe\"\n\nexport type MarkdownPromptOptions = {\n name: string\n project?: string[]\n dotUser?: string[] // dotZaly or dotAgents\n}\n\nexport function createMarkdownPrompt(opts: MarkdownPromptOptions) {\n return (ctx: PromptCtx) => markdownPrompt(ctx, opts)\n}\n\nexport async function markdownPrompt(ctx: PromptCtx, opts: MarkdownPromptOptions): Promise<string> {\n const resources: string[] = []\n const project = zalyPaths.project(ctx.cwd)\n\n if (opts.dotUser) {\n const checks = opts.dotUser.flatMap((name) => [\n join(zalyPaths.config, name),\n `~/.agents/${name}`,\n ])\n for (const check of checks) {\n const file = normPath(check)\n if (safeStat(file)?.isFile()) {\n resources.push(file)\n break\n }\n }\n }\n\n if (opts.project) {\n const file = findUp(ctx.cwd, opts.project, { stop: project.stop, type: \"file\" })\n if (file) resources.push(file)\n }\n\n if (resources.length === 0) return \"\"\n\n const contents = await Promise.all(resources.map((r) => safeReadFile(r)))\n const ret: string[] = []\n for (let i = 0; i < resources.length; i++) {\n const content = contents[i]\n if (!content) continue\n const r = resources[i]\n ret.push(`## ${opts.name} (${r})\\n\\n${content.trim()}`)\n }\n return ret.join(\"\\n\\n\")\n}\n\nexport async function agentsMdPrompt(ctx: PromptCtx) {\n return markdownPrompt(ctx, {\n dotUser: [\"AGENTS.md\"],\n name: \"AGENTS.md\",\n project: [\"AGENTS.md\", \"CLAUDE.md\"],\n })\n}\n\nexport async function memoryMdPrompt(ctx: PromptCtx) {\n return markdownPrompt(ctx, {\n dotUser: [\"MEMORY.md\"],\n name: \"MEMORY.md\",\n project: [\".agents/MEMORY.md\"],\n })\n}\n"],"mappings":";;;;AAgBA,eAAsB,eAAe,KAAgB,MAA8C;CACjG,MAAM,YAAsB,CAAC;CAC7B,MAAM,UAAU,UAAU,QAAQ,IAAI,GAAG;CAEzC,IAAI,KAAK,SAAS;EAChB,MAAM,SAAS,KAAK,QAAQ,SAAS,SAAS,CAC5C,KAAK,UAAU,QAAQ,IAAI,GAC3B,aAAa,MACf,CAAC;EACD,KAAK,MAAM,SAAS,QAAQ;GAC1B,MAAM,OAAO,SAAS,KAAK;GAC3B,IAAI,SAAS,IAAI,CAAC,EAAE,OAAO,GAAG;IAC5B,UAAU,KAAK,IAAI;IACnB;GACF;EACF;CACF;CAEA,IAAI,KAAK,SAAS;EAChB,MAAM,OAAO,OAAO,IAAI,KAAK,KAAK,SAAS;GAAE,MAAM,QAAQ;GAAM,MAAM;EAAO,CAAC;EAC/E,IAAI,MAAM,UAAU,KAAK,IAAI;CAC/B;CAEA,IAAI,UAAU,WAAW,GAAG,OAAO;CAEnC,MAAM,WAAW,MAAM,QAAQ,IAAI,UAAU,KAAK,MAAM,aAAa,CAAC,CAAC,CAAC;CACxE,MAAM,MAAgB,CAAC;CACvB,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;EACzC,MAAM,UAAU,SAAS;EACzB,IAAI,CAAC,SAAS;EACd,MAAM,IAAI,UAAU;EACpB,IAAI,KAAK,MAAM,KAAK,KAAK,IAAI,EAAE,OAAO,QAAQ,KAAK,GAAG;CACxD;CACA,OAAO,IAAI,KAAK,MAAM;AACxB;AAEA,eAAsB,eAAe,KAAgB;CACnD,OAAO,eAAe,KAAK;EACzB,SAAS,CAAC,WAAW;EACrB,MAAM;EACN,SAAS,CAAC,aAAa,WAAW;CACpC,CAAC;AACH;AAEA,eAAsB,eAAe,KAAgB;CACnD,OAAO,eAAe,KAAK;EACzB,SAAS,CAAC,WAAW;EACrB,MAAM;EACN,SAAS,CAAC,mBAAmB;CAC/B,CAAC;AACH"}
@@ -0,0 +1,396 @@
1
+ import { estimatePart, tokenStats } from "./index.mjs";
2
+ import { safeStringify, toValue } from "@zaly/shared";
3
+ import { isAttachment, safeParseToolParams } from "@zaly/ai";
4
+ //#region src/context/scoring.ts
5
+ const DEFAULT_HALF_LIFE = 40;
6
+ const DEFAULT_WEIGHT = 1;
7
+ const DEFAULT_GAMMA = .5;
8
+ const maskedMeta = (result = "result", action = "call") => ({
9
+ content: `Masked ${result}. Re-${action} to refresh`,
10
+ tag: "masked",
11
+ type: "meta"
12
+ });
13
+ var MaskPolicy = class {
14
+ #rule;
15
+ constructor(rule) {
16
+ this.#rule = rule;
17
+ }
18
+ update(group, part) {
19
+ group.parts.push(part);
20
+ this.#rule.update?.(group, part);
21
+ }
22
+ mask(part, group) {
23
+ return this.#rule.mask(part, group);
24
+ }
25
+ filter(part) {
26
+ return this.#rule.filter?.(part) ?? true;
27
+ }
28
+ id(part) {
29
+ return this.#rule.id?.(part) ?? `${part.$m}:${part.$p}`;
30
+ }
31
+ halfLife(group) {
32
+ const halfLife = this.#rule.halfLife ?? DEFAULT_HALF_LIFE;
33
+ return typeof halfLife === "number" ? halfLife : halfLife(group) ?? DEFAULT_HALF_LIFE;
34
+ }
35
+ lambda(group) {
36
+ return Math.LN2 / this.halfLife(group);
37
+ }
38
+ weight(group) {
39
+ const weight = this.#rule.weight ?? DEFAULT_WEIGHT;
40
+ return typeof weight === "number" ? weight : weight(group) ?? DEFAULT_WEIGHT;
41
+ }
42
+ gamma(group) {
43
+ const gamma = this.#rule.gamma ?? DEFAULT_GAMMA;
44
+ return typeof gamma === "number" ? gamma : gamma(group) ?? DEFAULT_GAMMA;
45
+ }
46
+ key(group) {
47
+ const key = this.#rule.key ?? "id";
48
+ if (key === "id") return group.id;
49
+ else if (typeof key === "function") return key(group) ?? group.id;
50
+ return group.id;
51
+ }
52
+ };
53
+ var ToolPolicy = class extends MaskPolicy {
54
+ constructor(name, rule) {
55
+ super({
56
+ filter: (part) => (part.part.type === "tool-call" || part.part.type === "tool-result") && (part.part.name === name || name === "*"),
57
+ id: (part) => part.part.id,
58
+ mask: (part, group) => {
59
+ if (part.part.type === "tool-call") return {
60
+ ...part.part,
61
+ params: { masked: truncate(safeStringify(group.params), 100) }
62
+ };
63
+ return {
64
+ ...part.part,
65
+ content: [maskedMeta()]
66
+ };
67
+ },
68
+ update: (group, part) => {
69
+ group.name = name;
70
+ if (part.part.type === "tool-call") {
71
+ group.call = part.part;
72
+ group.params = safeParseToolParams(part.part.params);
73
+ }
74
+ if (part.part.type === "tool-result") group.result = part.part;
75
+ },
76
+ ...rule,
77
+ key: (group) => {
78
+ const key = rule.key ?? "params";
79
+ if (key === "name") return group.name;
80
+ if (key === "params" && group.params !== void 0) return safeStringify(group.params);
81
+ if (key === "id") return group.id;
82
+ if (typeof key === "function") return key(group);
83
+ return group.id;
84
+ }
85
+ });
86
+ }
87
+ };
88
+ function truncate(text, len) {
89
+ return text.length <= len ? text : `${text.slice(0, len)}…`;
90
+ }
91
+ const fileScore = {
92
+ gamma: (t) => {
93
+ if (t.name === "write") return .45;
94
+ if (t.name === "edit") return .65;
95
+ if (t.result?.meta?.full) return .75;
96
+ return .95;
97
+ },
98
+ halfLife: (t) => {
99
+ if (t.name === "write") return 90;
100
+ if (t.name === "edit") return 75;
101
+ if (t.result?.meta?.full) return 60;
102
+ return 50;
103
+ },
104
+ key: (t) => `file:${t.result?.meta?.path ?? t.id}`,
105
+ mask: (part) => {
106
+ if (part.part.type === "tool-call") return {
107
+ ...part.part,
108
+ params: {
109
+ masked: true,
110
+ path: part.part.params.path
111
+ }
112
+ };
113
+ return part.part.isError ? part.part : {
114
+ ...part.part,
115
+ content: [maskedMeta()]
116
+ };
117
+ },
118
+ weight: (t) => {
119
+ if (t.name === "write") return 1.25;
120
+ if (t.name === "edit") return 1.2;
121
+ if (t.result?.meta?.full) return 1.1;
122
+ return 1;
123
+ }
124
+ };
125
+ const defaults$1 = {
126
+ parts: [{
127
+ filter: (part) => isAttachment(part.part),
128
+ mask: (part) => maskedMeta(part.part.type, "attach")
129
+ }, {
130
+ filter: (part) => part.message.role === "system" && part.message.meta?.kind === "task",
131
+ halfLife: 25,
132
+ mask: (part) => {
133
+ if (part.part.type === "meta") return part.part;
134
+ return maskedMeta("task");
135
+ }
136
+ }],
137
+ tools: {
138
+ bash: {
139
+ gamma: .35,
140
+ halfLife: 25,
141
+ key: (t) => `bash:${t.params?.command?.replace(/\s+/g, " ") ?? t.id}`
142
+ },
143
+ find: {
144
+ gamma: .7,
145
+ halfLife: 35
146
+ },
147
+ grep: {
148
+ gamma: .7,
149
+ halfLife: 35
150
+ },
151
+ search: {
152
+ gamma: .5,
153
+ halfLife: 40
154
+ },
155
+ read: fileScore,
156
+ edit: fileScore,
157
+ write: fileScore,
158
+ "*": {
159
+ gamma: .5,
160
+ halfLife: 40
161
+ }
162
+ }
163
+ };
164
+ var ContextScoring = class {
165
+ #groups = /* @__PURE__ */ new Map();
166
+ #messages = [];
167
+ #policies = [];
168
+ constructor(opts = {}) {
169
+ const tools = {
170
+ ...defaults$1.tools,
171
+ ...opts.tools
172
+ };
173
+ for (const [name, tool] of Object.entries(tools)) this.#policies.push(new ToolPolicy(name, tool));
174
+ for (const rule of [...defaults$1.parts ?? [], ...opts.parts ?? []]) this.#policies.push(new MaskPolicy(rule));
175
+ }
176
+ #reset() {
177
+ this.#groups.clear();
178
+ }
179
+ #index() {
180
+ let turn = 0;
181
+ for (let $m = this.#messages.length - 1; $m >= 0; $m--) {
182
+ const m = this.#messages[$m];
183
+ if (m.role === "assistant") turn++;
184
+ if (!Array.isArray(m.content)) continue;
185
+ for (let $p = m.content.length - 1; $p >= 0; $p--) {
186
+ const part = {
187
+ $m,
188
+ $p,
189
+ message: m,
190
+ part: m.content[$p],
191
+ score: 0,
192
+ turn
193
+ };
194
+ const policy = this.#policy(part);
195
+ if (!policy) continue;
196
+ const id = policy.id(part);
197
+ const group = this.#groups.get(id) ?? {
198
+ id,
199
+ key: "",
200
+ parts: [],
201
+ policy
202
+ };
203
+ policy.update(group, part);
204
+ this.#groups.set(id, group);
205
+ }
206
+ }
207
+ }
208
+ #policy(part) {
209
+ return this.#policies.find((policy) => policy.filter(part));
210
+ }
211
+ score(messages) {
212
+ this.#reset();
213
+ this.#messages = messages;
214
+ this.#index();
215
+ const groups = [...this.#groups.values()];
216
+ const gammas = /* @__PURE__ */ new Map();
217
+ for (const group of groups) {
218
+ const key = group.policy.key(group);
219
+ group.key = key;
220
+ const weight = group.policy.weight(group);
221
+ const shadow = gammas.get(key) ?? 1;
222
+ const lambda = group.policy.lambda(group);
223
+ for (const part of group.parts) part.score = Math.exp(-lambda * part.turn) * weight * shadow;
224
+ gammas.set(key, shadow * group.policy.gamma(group));
225
+ }
226
+ return groups;
227
+ }
228
+ };
229
+ //#endregion
230
+ //#region src/masker.ts
231
+ const defaults = {
232
+ delta: .25,
233
+ enabled: true,
234
+ keepTurns: 40,
235
+ minTokens: 50,
236
+ target: .5
237
+ };
238
+ /** In-place mask projection for the request stream.
239
+ *
240
+ * The masker replaces low-value parts with stable stubs to keep the
241
+ * projected request within budget. A scoring pass ranks maskable parts
242
+ * by recency, repeated-use shadowing, and per-policy weights; the budget
243
+ * pass masks from lowest score upward until the target is reached or no
244
+ * useful candidates remain.
245
+ *
246
+ * Masking intentionally happens rarely because it changes historical
247
+ * bytes and busts prefix cache. The first pass after startup/session
248
+ * reset always rebuilds the projection. Later passes use a hysteresis
249
+ * threshold: mask toward `target`, then trigger again at roughly
250
+ * `actual + delta`. If the target cannot be reached, the next threshold
251
+ * rises automatically to avoid repeated no-op cache busts.
252
+ */
253
+ var Masker = class {
254
+ #o;
255
+ #stats = /* @__PURE__ */ new Map();
256
+ #masked = /* @__PURE__ */ new Map();
257
+ #threshold;
258
+ #agent;
259
+ constructor(agent, opts = {}) {
260
+ this.#o = opts;
261
+ this.#agent = agent;
262
+ agent.on("context", async (ctx) => ctx.messages = await this.mask(ctx.messages));
263
+ agent.ctx.on("session", () => this.reset());
264
+ }
265
+ get #opts() {
266
+ return {
267
+ ...defaults,
268
+ ...toValue(this.#o)
269
+ };
270
+ }
271
+ get enabled() {
272
+ return this.#opts.enabled;
273
+ }
274
+ get #session() {
275
+ return this.#agent.session;
276
+ }
277
+ #maskOpts(opts) {
278
+ const pressure = this.#agent.pressure;
279
+ return {
280
+ force: false,
281
+ limit: pressure.limit,
282
+ prompt: this.#agent.prompt,
283
+ ratio: pressure.ratio,
284
+ tools: this.#agent.tools,
285
+ ...opts
286
+ };
287
+ }
288
+ reset() {
289
+ this.#masked.clear();
290
+ this.#stats.clear();
291
+ this.#threshold = void 0;
292
+ }
293
+ get stats() {
294
+ return this.#stats;
295
+ }
296
+ /** Number of messages with at least one masked part. */
297
+ get masked() {
298
+ return this.#masked.size;
299
+ }
300
+ isMasked(msgId, partIdx) {
301
+ if (!this.enabled) return false;
302
+ const parts = this.#masked.get(msgId);
303
+ if (parts === void 0) return false;
304
+ return partIdx === void 0 ? parts.size > 0 : parts.has(partIdx);
305
+ }
306
+ /** Apply masks and return the projected message array.
307
+ *
308
+ * Always rebuild once after startup/session reset: `pressure.ratio`
309
+ * may describe a previously masked projection, while this instance has
310
+ * no mask decisions yet. After that, rebuild only when projected
311
+ * pressure crosses the hysteresis threshold. */
312
+ async mask(messages, opts) {
313
+ if (!this.enabled || messages.length === 0) return [...messages];
314
+ const resolved = this.#maskOpts(opts);
315
+ if (this.#threshold === void 0) {
316
+ const cp = this.#session.maskCheckpoint;
317
+ if (!cp) this.#threshold = this.#opts.target + this.#opts.delta;
318
+ else {
319
+ this.#threshold = cp.threshold;
320
+ const idx = messages.findIndex((m) => m.id === cp.messageId);
321
+ this.#update(idx === -1 ? messages : messages.slice(0, idx + 1), resolved);
322
+ }
323
+ }
324
+ if (resolved.ratio >= this.#threshold || resolved.force) {
325
+ const messageId = messages.at(-1)?.id;
326
+ if (!messageId) throw new Error("Message in masker without ID. Should never happen.");
327
+ await this.#session.addMaskCheckpoint({
328
+ messageId,
329
+ threshold: this.#threshold
330
+ });
331
+ this.#update(messages, resolved);
332
+ }
333
+ return this.#mask(messages);
334
+ }
335
+ #stat(role, key, n = 1) {
336
+ const r = this.#stats.get(role) ?? {};
337
+ r[key] = (r[key] ?? 0) + n;
338
+ this.#stats.set(role, r);
339
+ }
340
+ /** Recompute mask decisions for the current message history. */
341
+ #update(messages, opts) {
342
+ this.#masked.clear();
343
+ this.#stats.clear();
344
+ this.#threshold = this.#opts.target + this.#opts.delta;
345
+ const usage = tokenStats(messages, {
346
+ prompt: opts.prompt,
347
+ tools: opts.tools
348
+ });
349
+ const targetRatio = Math.max(this.#opts.target, this.#threshold - this.#opts.delta);
350
+ const target = opts.limit * targetRatio;
351
+ if (usage.tokens <= target) return;
352
+ let mask = usage.tokens - target;
353
+ if (mask === 0) return;
354
+ const scores = new ContextScoring().score(messages);
355
+ const parts = [];
356
+ for (const s of scores) for (const p of s.parts) parts.push({
357
+ ...p,
358
+ mask: () => s.policy.mask(p, s)
359
+ });
360
+ parts.sort((a, b) => a.score - b.score);
361
+ let masked = 0;
362
+ for (const p of parts) {
363
+ if (mask <= 0) break;
364
+ const id = p.message.id;
365
+ if (!id) continue;
366
+ if (p.turn <= this.#opts.keepTurns) continue;
367
+ const tokens = estimatePart(p.part).tokens;
368
+ if (tokens < this.#opts.minTokens) continue;
369
+ const part = p.mask();
370
+ if (part === p.part) continue;
371
+ const msgParts = this.#masked.get(id) ?? /* @__PURE__ */ new Map();
372
+ msgParts.set(p.$p, part);
373
+ this.#stat(p.message.role, `${p.part.type}${p.part.type === "tool-result" || p.part.type === "tool-call" ? `:${p.part.name}` : ""}`);
374
+ this.#masked.set(id, msgParts);
375
+ const delta = tokens - estimatePart(part).tokens;
376
+ masked += delta;
377
+ mask -= delta;
378
+ }
379
+ this.#threshold = Math.max((usage.tokens - masked) / opts.limit + this.#opts.delta, this.#opts.target + this.#opts.delta);
380
+ }
381
+ /** Render the current projection using the last computed mask decisions. */
382
+ #mask(messages) {
383
+ return messages.map((m) => {
384
+ if (m.id === void 0 || typeof m.content === "string") return m;
385
+ const masked = m.id ? this.#masked.get(m.id) : void 0;
386
+ return masked && masked.size > 0 ? {
387
+ ...m,
388
+ content: m.content.map((p, i) => masked.get(i) ?? p)
389
+ } : m;
390
+ });
391
+ }
392
+ };
393
+ //#endregion
394
+ export { Masker };
395
+
396
+ //# sourceMappingURL=masker-Bv9KjkDF.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"masker-Bv9KjkDF.mjs","names":["#rule","defaults","#policies","#groups","#messages","#policy","#reset","#index","#o","#agent","#opts","#session","#masked","#stats","#threshold","#maskOpts","#update","#mask","#stat"],"sources":["../src/context/scoring.ts","../src/masker.ts"],"sourcesContent":["// ── Frecency tuning ───────────────────────────────────────────────────\n// Half-life measured in assistant turns, not wall time — what matters for\n// compaction context is what's been hot in the agent's *recent\n// experience*, regardless of how long the user spent between turns.\n// 60 turns is a middle ground: actively-iterated work stays at the top\n// while recurring workflow patterns (git stash, test-runner flags) still\n// clear the minScore filter instead of getting buried under one-shot\n\nimport type {\n AnyPart,\n Message,\n MetaPart,\n ParamsOf,\n Role,\n SafeParamsOf,\n Tool,\n ToolCallPart,\n ToolResultPart,\n} from \"@zaly/ai\"\nimport type { BashTool } from \"../tools/bash.ts\"\nimport type { EditTool, EditToolMeta } from \"../tools/edit.ts\"\nimport type { ReadTool, ReadToolMeta } from \"../tools/read.ts\"\nimport type { AnyTool } from \"../tools/registry.ts\"\nimport type { WriteTool, WriteToolMeta } from \"../tools/write.ts\"\n\nimport { isAttachment, safeParseToolParams } from \"@zaly/ai\"\nimport { safeStringify } from \"@zaly/shared\"\n\nconst DEFAULT_HALF_LIFE = 40\nconst DEFAULT_WEIGHT = 1\nconst DEFAULT_GAMMA = 0.5\n\nconst maskedMeta = (result = \"result\", action = \"call\"): MetaPart => ({\n content: `Masked ${result}. Re-${action} to refresh`,\n tag: \"masked\",\n type: \"meta\",\n})\n\nexport type MsgPart<R extends Role = Role, P extends AnyPart = AnyPart> = {\n message: Message<R>\n part: P\n score: number\n turn: number\n $m: number\n $p: number\n}\n\nexport type MaskGroup<P extends MsgPart = MsgPart> = {\n id: string\n key: string\n policy: MaskPolicy<P, MaskGroup<P>>\n parts: P[]\n}\n\nexport type MaskRule<P extends MsgPart = MsgPart, G extends MaskGroup<P> = MaskGroup<P>> = {\n filter?: (part: MsgPart) => boolean\n id?: (part: P) => string | undefined\n update?: (group: G, part: P) => void\n gamma?: number | ((group: G) => number | undefined)\n halfLife?: number | ((group: G) => number | undefined)\n key?: \"id\" | ((group: G) => string | undefined)\n weight?: number | ((group: G) => number | undefined)\n mask: (part: P, group: G) => AnyPart\n}\n\nexport type ToolPart<T extends Tool = Tool, M extends object = object> = MsgPart<\n \"assistant\" | \"tool\" | \"system\",\n ToolCallPart<T[\"name\"], ParamsOf<T>> | ToolResultPart<T[\"name\"], M>\n>\n\nexport type ToolGroup<T extends Tool = Tool, M extends object = object> = MaskGroup<\n ToolPart<T, M>\n> & {\n name: T[\"name\"]\n params: SafeParamsOf<T>\n call?: ToolCallPart<T[\"name\"], ParamsOf<T>>\n result?: ToolResultPart<T[\"name\"], M>\n}\n\nexport type ToolRule<T extends Tool = Tool, M extends object = object> =\n | MaskRule<ToolPart<T, M>, ToolGroup<T, M>>\n | {\n key?: \"name\" | \"params\" | \"id\" | ((tool: ToolGroup<T, M>) => string | undefined)\n }\n\nclass MaskPolicy<\n P extends MsgPart = MsgPart,\n G extends MaskGroup<P> = MaskGroup<P>,\n> implements Required<MaskRule<P, G>> {\n #rule: MaskRule<P, G>\n\n constructor(rule: MaskRule<P, G>) {\n this.#rule = rule\n }\n\n update(group: G, part: P): void {\n group.parts.push(part)\n this.#rule.update?.(group, part)\n }\n\n mask(part: P, group: G): AnyPart {\n return this.#rule.mask(part, group)\n }\n\n filter(part: MsgPart): boolean {\n return this.#rule.filter?.(part) ?? true\n }\n\n id(part: P): string {\n return this.#rule.id?.(part) ?? `${part.$m}:${part.$p}`\n }\n\n halfLife(group: G): number {\n const halfLife = this.#rule.halfLife ?? DEFAULT_HALF_LIFE\n return typeof halfLife === \"number\" ? halfLife : (halfLife(group) ?? DEFAULT_HALF_LIFE)\n }\n\n lambda(group: G): number {\n return Math.LN2 / this.halfLife(group)\n }\n\n weight(group: G): number {\n const weight = this.#rule.weight ?? DEFAULT_WEIGHT\n return typeof weight === \"number\" ? weight : (weight(group) ?? DEFAULT_WEIGHT)\n }\n\n gamma(group: G): number {\n const gamma = this.#rule.gamma ?? DEFAULT_GAMMA\n return typeof gamma === \"number\" ? gamma : (gamma(group) ?? DEFAULT_GAMMA)\n }\n\n key(group: G): string {\n const key = this.#rule.key ?? \"id\"\n if (key === \"id\") return group.id\n else if (typeof key === \"function\") return key(group) ?? group.id\n return group.id\n }\n}\n\nclass ToolPolicy<T extends Tool = Tool, M extends object = object> extends MaskPolicy<\n ToolPart<T, M>,\n ToolGroup<T, M>\n> {\n constructor(name: string, rule: ToolRule<T, M>) {\n super({\n filter: (part) =>\n (part.part.type === \"tool-call\" || part.part.type === \"tool-result\") &&\n (part.part.name === name || name === \"*\"),\n id: (part) => part.part.id,\n mask: (part, group) => {\n if (part.part.type === \"tool-call\")\n return {\n ...part.part,\n params: { masked: truncate(safeStringify(group.params), 100) },\n }\n return { ...part.part, content: [maskedMeta()] }\n },\n update: (group, part) => {\n group.name = name\n if (part.part.type === \"tool-call\") {\n group.call = part.part\n group.params = safeParseToolParams(part.part.params)\n }\n if (part.part.type === \"tool-result\") group.result = part.part\n },\n ...rule,\n key: (group) => {\n const key = rule.key ?? \"params\"\n if (key === \"name\") return group.name\n if (key === \"params\" && group.params !== undefined) return safeStringify(group.params)\n if (key === \"id\") return group.id\n if (typeof key === \"function\") return key(group)\n return group.id\n },\n })\n }\n}\n\nfunction truncate(text: string, len: number): string {\n return text.length <= len ? text : `${text.slice(0, len)}…`\n}\n\nconst fileScore: ToolRule<\n ReadTool | WriteTool | EditTool,\n ReadToolMeta | WriteToolMeta | EditToolMeta\n> = {\n gamma: (t) => {\n if (t.name === \"write\") return 0.45\n if (t.name === \"edit\") return 0.65\n if (t.result?.meta?.full) return 0.75\n return 0.95\n },\n halfLife: (t) => {\n if (t.name === \"write\") return 90\n if (t.name === \"edit\") return 75\n if (t.result?.meta?.full) return 60\n return 50 // partial read\n },\n key: (t) => `file:${t.result?.meta?.path ?? t.id}`,\n mask: (part) => {\n if (part.part.type === \"tool-call\")\n return {\n ...part.part,\n params: { masked: true, path: part.part.params.path },\n }\n return part.part.isError ? part.part : { ...part.part, content: [maskedMeta()] }\n },\n weight: (t) => {\n if (t.name === \"write\") return 1.25\n if (t.name === \"edit\") return 1.2\n if (t.result?.meta?.full) return 1.1\n return 1\n },\n}\n\nexport type ContextScoringOptions = {\n tools?: Record<AnyTool | \"*\", ToolRule>\n parts?: MaskRule[]\n}\n\nconst defaults: ContextScoringOptions = {\n parts: [\n {\n filter: (part) => isAttachment(part.part),\n mask: (part) => maskedMeta(part.part.type, \"attach\"),\n },\n {\n filter: (part) => part.message.role === \"system\" && part.message.meta?.kind === \"task\",\n halfLife: 25,\n mask: (part) => {\n if (part.part.type === \"meta\") return part.part\n return maskedMeta(\"task\")\n },\n },\n ],\n // oxlint-disable-next-line sort-keys\n tools: {\n bash: {\n gamma: 0.35,\n halfLife: 25,\n key: (t: ToolGroup<BashTool>) => `bash:${t.params?.command?.replace(/\\s+/g, \" \") ?? t.id}`,\n },\n find: { gamma: 0.7, halfLife: 35 },\n grep: { gamma: 0.7, halfLife: 35 },\n search: { gamma: 0.5, halfLife: 40 },\n\n read: fileScore,\n edit: fileScore,\n write: fileScore,\n\n \"*\": { gamma: 0.5, halfLife: 40 },\n } as Record<AnyTool | \"*\", ToolRule>,\n}\n\nexport class ContextScoring {\n #groups = new Map<string, MaskGroup>()\n #messages: readonly Message[] = []\n #policies: MaskPolicy[] = []\n\n constructor(opts: ContextScoringOptions = {}) {\n const tools = { ...defaults.tools, ...opts.tools }\n for (const [name, tool] of Object.entries(tools)) {\n this.#policies.push(new ToolPolicy(name, tool) as MaskPolicy)\n }\n for (const rule of [...(defaults.parts ?? []), ...(opts.parts ?? [])]) {\n this.#policies.push(new MaskPolicy(rule))\n }\n }\n\n #reset() {\n this.#groups.clear()\n }\n\n #index() {\n let turn = 0\n for (let $m = this.#messages.length - 1; $m >= 0; $m--) {\n const m = this.#messages[$m]\n if (m.role === \"assistant\") turn++\n\n if (!Array.isArray(m.content)) continue\n\n for (let $p = m.content.length - 1; $p >= 0; $p--) {\n const part: MsgPart = { $m, $p, message: m, part: m.content[$p], score: 0, turn }\n const policy = this.#policy(part)\n if (!policy) continue\n const id = policy.id(part)\n const group = this.#groups.get(id) ?? { id, key: \"\", parts: [], policy }\n policy.update(group, part)\n this.#groups.set(id, group)\n }\n }\n }\n\n #policy(part: MsgPart): MaskPolicy | undefined {\n return this.#policies.find((policy) => policy.filter(part))\n }\n\n score(messages: readonly Message[]): MaskGroup[] {\n this.#reset()\n this.#messages = messages\n this.#index()\n // tools sorted by recency\n const groups = [...this.#groups.values()]\n const gammas = new Map<string, number>()\n for (const group of groups) {\n const key = group.policy.key(group)\n group.key = key\n const weight = group.policy.weight(group)\n const shadow = gammas.get(key) ?? 1\n const lambda = group.policy.lambda(group)\n for (const part of group.parts) {\n const decay = Math.exp(-lambda * part.turn)\n part.score = decay * weight * shadow\n }\n gammas.set(key, shadow * group.policy.gamma(group))\n }\n return groups\n }\n}\n","import type { AnyPart, Message, Role, Tool } from \"@zaly/ai\"\nimport type { MaybeGetter } from \"@zaly/shared\"\nimport type { Agent } from \"./agent.ts\"\nimport type { MsgPart } from \"./context/scoring.ts\"\nimport type { Session } from \"./session/session.ts\"\n\nimport { toValue } from \"@zaly/shared\"\nimport { ContextScoring } from \"./context/scoring.ts\"\nimport { estimatePart, tokenStats } from \"./context/tokens.ts\"\n\n/** Top-level masking config. */\nexport type MaskerOptions = {\n enabled?: boolean\n /** Don't mask tool-result parts whose original content is shorter\n * than this (estimated tokens). Skips tiny \"ok\"-style success\n * messages where the stub would be larger than the original.\n * Doesn't apply to attachments (always worth masking). */\n minTokens?: number\n /** How many turns to keep in the tail of the conversation, regardless\n * of score. Defaults to 20. */\n keepTurns?: number\n /** How far above the target ratio to trigger a new masking pass.\n * Defaults to 0.25 (25%). */\n delta?: number\n /** Target ratio of used/limit tokens to reach by masking. Defaults to 0.5 (50%). */\n target?: number\n}\n\nexport type MaskOpts = {\n force?: boolean\n limit?: number\n ratio?: number\n tools?: Tool[]\n prompt?: string[]\n}\n\nconst defaults = {\n delta: 0.25,\n enabled: true,\n keepTurns: 40,\n minTokens: 50,\n target: 0.5,\n} as const satisfies Required<MaskerOptions>\n\n/** In-place mask projection for the request stream.\n *\n * The masker replaces low-value parts with stable stubs to keep the\n * projected request within budget. A scoring pass ranks maskable parts\n * by recency, repeated-use shadowing, and per-policy weights; the budget\n * pass masks from lowest score upward until the target is reached or no\n * useful candidates remain.\n *\n * Masking intentionally happens rarely because it changes historical\n * bytes and busts prefix cache. The first pass after startup/session\n * reset always rebuilds the projection. Later passes use a hysteresis\n * threshold: mask toward `target`, then trigger again at roughly\n * `actual + delta`. If the target cannot be reached, the next threshold\n * rises automatically to avoid repeated no-op cache busts.\n */\nexport class Masker {\n #o: MaybeGetter<MaskerOptions>\n #stats = new Map<Role, Record<string, number>>()\n #masked = new Map<string, Map<number, AnyPart>>()\n #threshold?: number\n #agent: Agent\n\n constructor(agent: Agent, opts: MaybeGetter<MaskerOptions> = {}) {\n this.#o = opts\n this.#agent = agent\n agent.on(\"context\", async (ctx) => (ctx.messages = await this.mask(ctx.messages)))\n agent.ctx.on(\"session\", () => this.reset())\n }\n\n get #opts(): Required<MaskerOptions> {\n return { ...defaults, ...toValue(this.#o) }\n }\n\n get enabled(): boolean {\n return this.#opts.enabled\n }\n\n get #session(): Session {\n return this.#agent.session\n }\n\n #maskOpts(opts?: MaskOpts): Required<MaskOpts> {\n const pressure = this.#agent.pressure\n return {\n force: false,\n limit: pressure.limit,\n prompt: this.#agent.prompt,\n ratio: pressure.ratio,\n tools: this.#agent.tools,\n ...opts,\n }\n }\n\n reset(): void {\n this.#masked.clear()\n this.#stats.clear()\n this.#threshold = undefined\n }\n\n get stats(): Map<Role, Record<string, number>> {\n return this.#stats\n }\n\n /** Number of messages with at least one masked part. */\n get masked(): number {\n return this.#masked.size\n }\n\n isMasked(msgId: string, partIdx?: number): boolean {\n if (!this.enabled) return false\n const parts = this.#masked.get(msgId)\n if (parts === undefined) return false\n return partIdx === undefined ? parts.size > 0 : parts.has(partIdx)\n }\n\n /** Apply masks and return the projected message array.\n *\n * Always rebuild once after startup/session reset: `pressure.ratio`\n * may describe a previously masked projection, while this instance has\n * no mask decisions yet. After that, rebuild only when projected\n * pressure crosses the hysteresis threshold. */\n async mask(messages: readonly Message[], opts?: MaskOpts): Promise<Message[]> {\n if (!this.enabled || messages.length === 0) return [...messages]\n\n const resolved = this.#maskOpts(opts)\n\n // Masker has not yet been initialized for this session.\n // Use the session's mask checkpoint to restore the last threshold\n // and rebuild the mask decisions up to that point.\n if (this.#threshold === undefined) {\n const cp = this.#session.maskCheckpoint\n if (!cp) {\n // No checkpoint means the session has never been masked before, so\n // just set to the default threshold for the first pass, so that the next pass will\n // trigger only if the pressure exceeds the target + delta.\n this.#threshold = this.#opts.target + this.#opts.delta\n } else {\n this.#threshold = cp.threshold\n const idx = messages.findIndex((m) => m.id === cp.messageId)\n // NOTE: the checkpoint message should exists. If not mask all, just in case\n // Rebuild the mask decisions\n this.#update(idx === -1 ? messages : messages.slice(0, idx + 1), resolved)\n }\n }\n\n // If the current pressure ratio exceeds the threshold, rebuild the mask decisions.\n if (resolved.ratio >= this.#threshold || resolved.force) {\n const messageId = messages.at(-1)?.id\n if (!messageId) throw new Error(\"Message in masker without ID. Should never happen.\")\n // Create a checkpoint first, with the CURRENT threshold, not the new one.\n await this.#session.addMaskCheckpoint({\n messageId,\n threshold: this.#threshold,\n })\n this.#update(messages, resolved)\n }\n return this.#mask(messages)\n }\n\n #stat(role: Role, key: string, n = 1): void {\n const r = this.#stats.get(role) ?? {}\n r[key] = (r[key] ?? 0) + n\n this.#stats.set(role, r)\n }\n\n /** Recompute mask decisions for the current message history. */\n #update(messages: readonly Message[], opts: Required<MaskOpts>): void {\n this.#masked.clear()\n this.#stats.clear()\n this.#threshold = this.#opts.target + this.#opts.delta\n const usage = tokenStats(messages, { prompt: opts.prompt, tools: opts.tools })\n // `pressure.ratio` is based on the current projected/masked request,\n // not the raw session. Raw history may exceed model context; masking\n // only needs to keep the projected request in a safe band.\n const targetRatio = Math.max(this.#opts.target, this.#threshold - this.#opts.delta)\n const target = opts.limit * targetRatio\n\n // Return when already under target.\n if (usage.tokens <= target) return\n\n let mask = usage.tokens - target\n\n if (mask === 0) return\n\n const scorer = new ContextScoring()\n const scores = scorer.score(messages)\n const parts: (MsgPart & { mask: () => AnyPart })[] = []\n for (const s of scores) {\n for (const p of s.parts) {\n parts.push({\n ...p,\n mask: () => s.policy.mask(p, s),\n })\n }\n }\n parts.sort((a, b) => a.score - b.score)\n\n let masked = 0\n for (const p of parts) {\n if (mask <= 0) break\n const id = p.message.id\n if (!id) continue\n\n if (p.turn <= this.#opts.keepTurns) continue\n\n const tokens = estimatePart(p.part).tokens\n if (tokens < this.#opts.minTokens) continue\n\n const part = p.mask()\n if (part === p.part) continue\n\n const msgParts = this.#masked.get(id) ?? new Map()\n msgParts.set(p.$p, part)\n this.#stat(\n p.message.role,\n `${p.part.type}${p.part.type === \"tool-result\" || p.part.type === \"tool-call\" ? `:${p.part.name}` : \"\"}`\n )\n this.#masked.set(id, msgParts)\n const delta = tokens - estimatePart(part).tokens\n masked += delta\n mask -= delta\n }\n\n this.#threshold = Math.max(\n (usage.tokens - masked) / opts.limit + this.#opts.delta,\n this.#opts.target + this.#opts.delta\n )\n }\n\n /** Render the current projection using the last computed mask decisions. */\n #mask(messages: readonly Message[]): Message[] {\n return messages.map((m) => {\n if (m.id === undefined || typeof m.content === \"string\") return m\n const masked = m.id ? this.#masked.get(m.id) : undefined\n return masked && masked.size > 0\n ? ({\n ...m,\n content: m.content.map((p, i) => masked.get(i) ?? p),\n } as Message)\n : m\n })\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ //#region src/prompt/model.ts
2
+ async function prompt(ctx) {
3
+ const model = ctx.model.spec;
4
+ const provider = model.provider.id;
5
+ const modelId = model.model;
6
+ const modsIn = model.input;
7
+ const modsOut = model.output ?? [];
8
+ return [
9
+ "## Model",
10
+ `- Id: ${modelId}`,
11
+ `- Name: ${model.name}`,
12
+ `- Provider: ${provider}`,
13
+ `- Max tokens: ${model.maxTokens}`,
14
+ model.knowledge ? `- Knowledge cutoff: ${model.knowledge}` : void 0,
15
+ model.release_date ? `- Release date: ${model.release_date}` : void 0,
16
+ model.last_updated ? `- Last updated: ${model.last_updated}` : void 0,
17
+ modsIn.length > 0 || modsOut.length > 0 ? [
18
+ "\n### Modalities",
19
+ modsIn.length ? `- Input modalities: ${modsIn.join(", ")}` : void 0,
20
+ modsOut.length ? `- Output modalities: ${modsOut.join(", ")}` : void 0
21
+ ] : void 0
22
+ ].flat().filter(Boolean).join("\n");
23
+ }
24
+ //#endregion
25
+ export { prompt };
26
+
27
+ //# sourceMappingURL=model-_3ZA2xqm.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-_3ZA2xqm.mjs","names":[],"sources":["../src/prompt/model.ts"],"sourcesContent":["import type { PromptCtx } from \"./registry.ts\"\n\nexport async function prompt(ctx: PromptCtx): Promise<string> {\n const model = ctx.model.spec\n const provider = model.provider.id\n const modelId = model.model\n const modsIn = model.input\n const modsOut = model.output ?? []\n\n const lines = [\n \"## Model\",\n `- Id: ${modelId}`,\n `- Name: ${model.name}`,\n `- Provider: ${provider}`,\n `- Max tokens: ${model.maxTokens}`,\n model.knowledge ? `- Knowledge cutoff: ${model.knowledge}` : undefined,\n model.release_date ? `- Release date: ${model.release_date}` : undefined,\n model.last_updated ? `- Last updated: ${model.last_updated}` : undefined,\n modsIn.length > 0 || modsOut.length > 0\n ? [\n \"\\n### Modalities\",\n modsIn.length ? `- Input modalities: ${modsIn.join(\", \")}` : undefined,\n modsOut.length ? `- Output modalities: ${modsOut.join(\", \")}` : undefined,\n ]\n : undefined,\n ]\n\n return lines.flat().filter(Boolean).join(\"\\n\")\n}\n"],"mappings":";AAEA,eAAsB,OAAO,KAAiC;CAC5D,MAAM,QAAQ,IAAI,MAAM;CACxB,MAAM,WAAW,MAAM,SAAS;CAChC,MAAM,UAAU,MAAM;CACtB,MAAM,SAAS,MAAM;CACrB,MAAM,UAAU,MAAM,UAAU,CAAC;CAoBjC,OAAO;EAjBL;EACA,SAAS;EACT,WAAW,MAAM;EACjB,eAAe;EACf,iBAAiB,MAAM;EACvB,MAAM,YAAY,uBAAuB,MAAM,cAAc,KAAA;EAC7D,MAAM,eAAe,mBAAmB,MAAM,iBAAiB,KAAA;EAC/D,MAAM,eAAe,mBAAmB,MAAM,iBAAiB,KAAA;EAC/D,OAAO,SAAS,KAAK,QAAQ,SAAS,IAClC;GACE;GACA,OAAO,SAAS,uBAAuB,OAAO,KAAK,IAAI,MAAM,KAAA;GAC7D,QAAQ,SAAS,wBAAwB,QAAQ,KAAK,IAAI,MAAM,KAAA;EAClE,IACA,KAAA;CAGK,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,IAAI;AAC/C"}
@@ -0,0 +1,96 @@
1
+ import { formatDuration } from "@zaly/shared";
2
+ //#region src/notify.ts
3
+ var Notifier = class {
4
+ #opts;
5
+ #lastTime;
6
+ #lastStep;
7
+ /** Highest pressure threshold (from `#pressureLevels`) we've already
8
+ * notified for in this session. Reset to 0 when pressure drops back
9
+ * below the lowest threshold (e.g. after compaction), so a later
10
+ * refill can fire the same level again. */
11
+ #pressureLevel = 0;
12
+ #ac;
13
+ constructor(opts = {}) {
14
+ this.#opts = {
15
+ idle: 1800,
16
+ periodic: 3600,
17
+ ...opts
18
+ };
19
+ }
20
+ attach(agent) {
21
+ agent.on("step-start", () => this.check(agent));
22
+ agent.ctx.on("session", () => this.attachSession(agent));
23
+ this.attachSession(agent);
24
+ }
25
+ attachSession(agent) {
26
+ this.#ac?.abort();
27
+ this.#ac = new AbortController();
28
+ const opts = { signal: this.#ac.signal };
29
+ agent.session.on("compact", ({ node }) => {
30
+ this.#pressureLevel = 0;
31
+ agent.notify("compacted", {
32
+ ...this.time(),
33
+ messages_preserved: node.tail,
34
+ trigger: node.trigger
35
+ });
36
+ }, opts).on("session-resume", () => {
37
+ agent.notify("session-resume", this.time());
38
+ }, opts).on("session-start", () => {
39
+ agent.notify("session-start", this.time());
40
+ }, opts).on("cwd", ({ cwd }) => {
41
+ agent.notify("cwd-changed", { cwd });
42
+ }, opts).on("model", ({ model, prev }) => {
43
+ agent.notify("model-changed", {
44
+ current: model,
45
+ prev
46
+ });
47
+ }, opts);
48
+ }
49
+ time(now = Date.now()) {
50
+ this.#lastTime = now;
51
+ return timeInfo(now);
52
+ }
53
+ check(agent) {
54
+ const now = Date.now();
55
+ const lastInfo = this.#lastTime ? timeInfo(this.#lastTime) : void 0;
56
+ this.#lastStep ??= now;
57
+ this.#lastTime ??= now;
58
+ if (lastInfo && timeInfo(now).date !== lastInfo.date) agent.notify("new-day", this.time(now));
59
+ else if (now - this.#lastStep > this.#opts.idle * 1e3) agent.notify("user-returned", {
60
+ idle: formatDuration(this.#lastStep, { to: now }),
61
+ ...this.time(now)
62
+ });
63
+ else if (now - this.#lastTime > this.#opts.periodic * 1e3) agent.notify("time", this.time(now));
64
+ this.#lastStep = now;
65
+ const pressure = agent.pressure;
66
+ if (pressure.level > this.#pressureLevel) {
67
+ agent.notify("context-pressure", {
68
+ limit: pressure.limit,
69
+ pct: Math.round(pressure.ratio * 100),
70
+ used: pressure.used
71
+ });
72
+ this.#pressureLevel = pressure.level;
73
+ } else if (pressure.level === 0) this.#pressureLevel = 0;
74
+ }
75
+ };
76
+ function timeInfo(t = Date.now()) {
77
+ const now = new Date(t);
78
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
79
+ return {
80
+ date: now.toLocaleDateString("sv-SE", { timeZone: tz }),
81
+ day: now.toLocaleDateString("en-US", {
82
+ timeZone: tz,
83
+ weekday: "long"
84
+ }),
85
+ time: now.toLocaleTimeString("sv-SE", {
86
+ hour: "2-digit",
87
+ minute: "2-digit",
88
+ timeZone: tz
89
+ }),
90
+ tz
91
+ };
92
+ }
93
+ //#endregion
94
+ export { Notifier };
95
+
96
+ //# sourceMappingURL=notify-DpG_m9iP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify-DpG_m9iP.mjs","names":["#opts","#ac","#pressureLevel","#lastTime","#lastStep"],"sources":["../src/notify.ts"],"sourcesContent":["import type { Agent } from \"./agent.ts\"\n\nimport { formatDuration } from \"@zaly/shared\"\n\nexport type NotifyContext = {\n agent: Agent\n}\n\nexport type NotifyOptions = {\n idle?: number // seconds of idle time (1800 = 30m)\n periodic?: number // seconds of periodic time notifications (3600 = 1h)\n}\n\nexport class Notifier {\n #opts: Required<NotifyOptions>\n #lastTime?: number\n #lastStep?: number\n /** Highest pressure threshold (from `#pressureLevels`) we've already\n * notified for in this session. Reset to 0 when pressure drops back\n * below the lowest threshold (e.g. after compaction), so a later\n * refill can fire the same level again. */\n #pressureLevel = 0\n #ac?: AbortController\n\n constructor(opts: NotifyOptions = {}) {\n this.#opts = { idle: 30 * 60, periodic: 60 * 60, ...opts }\n }\n\n attach(agent: Agent) {\n agent.on(\"step-start\", () => this.check(agent))\n agent.ctx.on(\"session\", () => this.attachSession(agent))\n this.attachSession(agent)\n }\n\n attachSession(agent: Agent) {\n this.#ac?.abort()\n this.#ac = new AbortController()\n const opts = { signal: this.#ac.signal }\n agent.session\n .on(\n \"compact\",\n ({ node }) => {\n this.#pressureLevel = 0\n agent.notify(\"compacted\", {\n ...this.time(),\n messages_preserved: node.tail,\n trigger: node.trigger,\n })\n },\n opts\n )\n .on(\n \"session-resume\",\n () => {\n agent.notify(\"session-resume\", this.time())\n },\n opts\n )\n .on(\n \"session-start\",\n () => {\n agent.notify(\"session-start\", this.time())\n },\n opts\n )\n .on(\n \"cwd\",\n ({ cwd }) => {\n agent.notify(\"cwd-changed\", { cwd })\n },\n opts\n )\n .on(\n \"model\",\n ({ model, prev }) => {\n agent.notify(\"model-changed\", { current: model, prev })\n },\n opts\n )\n }\n\n time(now = Date.now()) {\n this.#lastTime = now\n return timeInfo(now)\n }\n\n check(agent: Agent) {\n const now = Date.now()\n const lastInfo = this.#lastTime ? timeInfo(this.#lastTime) : undefined\n\n this.#lastStep ??= now\n this.#lastTime ??= now\n\n if (lastInfo && timeInfo(now).date !== lastInfo.date) {\n agent.notify(\"new-day\", this.time(now))\n } else if (now - this.#lastStep > this.#opts.idle * 1000) {\n agent.notify(\"user-returned\", {\n idle: formatDuration(this.#lastStep, { to: now }),\n ...this.time(now),\n })\n } else if (now - this.#lastTime > this.#opts.periodic * 1000) {\n agent.notify(\"time\", this.time(now))\n }\n this.#lastStep = now\n\n // Context-window pressure — denominator is `limit.context` (full\n // window), NOT `maxTokens` (per-request output cap). Notification\n // fires once per discrete level crossing (75% / 85% / 95%) so the\n // model gets at most a few escalating signals per session, not one\n // per step. Resets if pressure drops below the lowest level (e.g.\n // after compaction) so the cycle can fire again later.\n const pressure = agent.pressure\n if (pressure.level > this.#pressureLevel) {\n agent.notify(\"context-pressure\", {\n limit: pressure.limit,\n pct: Math.round(pressure.ratio * 100),\n used: pressure.used,\n })\n this.#pressureLevel = pressure.level\n } else if (pressure.level === 0) this.#pressureLevel = 0\n }\n}\n\nfunction timeInfo(t = Date.now()): {\n day: string\n date: string\n time: string\n tz: string\n} {\n const now = new Date(t)\n const tz = Intl.DateTimeFormat().resolvedOptions().timeZone\n return {\n date: now.toLocaleDateString(\"sv-SE\", { timeZone: tz }),\n day: now.toLocaleDateString(\"en-US\", { timeZone: tz, weekday: \"long\" }),\n time: now.toLocaleTimeString(\"sv-SE\", {\n hour: \"2-digit\",\n minute: \"2-digit\",\n timeZone: tz,\n }),\n tz,\n }\n}\n"],"mappings":";;AAaA,IAAa,WAAb,MAAsB;CACpB;CACA;CACA;;;;;CAKA,iBAAiB;CACjB;CAEA,YAAY,OAAsB,CAAC,GAAG;EACpC,KAAKA,QAAQ;GAAE,MAAM;GAAS,UAAU;GAAS,GAAG;EAAK;CAC3D;CAEA,OAAO,OAAc;EACnB,MAAM,GAAG,oBAAoB,KAAK,MAAM,KAAK,CAAC;EAC9C,MAAM,IAAI,GAAG,iBAAiB,KAAK,cAAc,KAAK,CAAC;EACvD,KAAK,cAAc,KAAK;CAC1B;CAEA,cAAc,OAAc;EAC1B,KAAKC,KAAK,MAAM;EAChB,KAAKA,MAAM,IAAI,gBAAgB;EAC/B,MAAM,OAAO,EAAE,QAAQ,KAAKA,IAAI,OAAO;EACvC,MAAM,QACH,GACC,YACC,EAAE,WAAW;GACZ,KAAKC,iBAAiB;GACtB,MAAM,OAAO,aAAa;IACxB,GAAG,KAAK,KAAK;IACb,oBAAoB,KAAK;IACzB,SAAS,KAAK;GAChB,CAAC;EACH,GACA,IACF,CAAC,CACA,GACC,wBACM;GACJ,MAAM,OAAO,kBAAkB,KAAK,KAAK,CAAC;EAC5C,GACA,IACF,CAAC,CACA,GACC,uBACM;GACJ,MAAM,OAAO,iBAAiB,KAAK,KAAK,CAAC;EAC3C,GACA,IACF,CAAC,CACA,GACC,QACC,EAAE,UAAU;GACX,MAAM,OAAO,eAAe,EAAE,IAAI,CAAC;EACrC,GACA,IACF,CAAC,CACA,GACC,UACC,EAAE,OAAO,WAAW;GACnB,MAAM,OAAO,iBAAiB;IAAE,SAAS;IAAO;GAAK,CAAC;EACxD,GACA,IACF;CACJ;CAEA,KAAK,MAAM,KAAK,IAAI,GAAG;EACrB,KAAKC,YAAY;EACjB,OAAO,SAAS,GAAG;CACrB;CAEA,MAAM,OAAc;EAClB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,KAAKA,YAAY,SAAS,KAAKA,SAAS,IAAI,KAAA;EAE7D,KAAKC,cAAc;EACnB,KAAKD,cAAc;EAEnB,IAAI,YAAY,SAAS,GAAG,CAAC,CAAC,SAAS,SAAS,MAC9C,MAAM,OAAO,WAAW,KAAK,KAAK,GAAG,CAAC;OACjC,IAAI,MAAM,KAAKC,YAAY,KAAKJ,MAAM,OAAO,KAClD,MAAM,OAAO,iBAAiB;GAC5B,MAAM,eAAe,KAAKI,WAAW,EAAE,IAAI,IAAI,CAAC;GAChD,GAAG,KAAK,KAAK,GAAG;EAClB,CAAC;OACI,IAAI,MAAM,KAAKD,YAAY,KAAKH,MAAM,WAAW,KACtD,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAG,CAAC;EAErC,KAAKI,YAAY;EAQjB,MAAM,WAAW,MAAM;EACvB,IAAI,SAAS,QAAQ,KAAKF,gBAAgB;GACxC,MAAM,OAAO,oBAAoB;IAC/B,OAAO,SAAS;IAChB,KAAK,KAAK,MAAM,SAAS,QAAQ,GAAG;IACpC,MAAM,SAAS;GACjB,CAAC;GACD,KAAKA,iBAAiB,SAAS;EACjC,OAAO,IAAI,SAAS,UAAU,GAAG,KAAKA,iBAAiB;CACzD;AACF;AAEA,SAAS,SAAS,IAAI,KAAK,IAAI,GAK7B;CACA,MAAM,MAAM,IAAI,KAAK,CAAC;CACtB,MAAM,KAAK,KAAK,eAAe,CAAC,CAAC,gBAAgB,CAAC,CAAC;CACnD,OAAO;EACL,MAAM,IAAI,mBAAmB,SAAS,EAAE,UAAU,GAAG,CAAC;EACtD,KAAK,IAAI,mBAAmB,SAAS;GAAE,UAAU;GAAI,SAAS;EAAO,CAAC;EACtE,MAAM,IAAI,mBAAmB,SAAS;GACpC,MAAM;GACN,QAAQ;GACR,UAAU;EACZ,CAAC;EACD;CACF;AACF"}