@zaly/agent 0.1.0 → 0.1.1

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 (85) hide show
  1. package/dist/{agent-D5NrjvG5.mjs → agent-BRENvuvx.mjs} +8 -11
  2. package/dist/{agent-D5NrjvG5.mjs.map → agent-BRENvuvx.mjs.map} +1 -1
  3. package/dist/{agent-B7cQOuMZ.mjs → agent-sHtwQDAk.mjs} +2 -3
  4. package/dist/{agent-B7cQOuMZ.mjs.map → agent-sHtwQDAk.mjs.map} +1 -1
  5. package/dist/{bash-9x8PPg0A.mjs → bash-DSy03sAj.mjs} +3 -4
  6. package/dist/{bash-9x8PPg0A.mjs.map → bash-DSy03sAj.mjs.map} +1 -1
  7. package/dist/compactions-HFjEgrv3.mjs +150 -0
  8. package/dist/compactions-HFjEgrv3.mjs.map +1 -0
  9. package/dist/{edit-D4yaBK1K.mjs → edit-DT672zIy.mjs} +2 -3
  10. package/dist/edit-DT672zIy.mjs.map +1 -0
  11. package/dist/{env-UUFv5Mye.mjs → env-C8p8i1E0.mjs} +2 -3
  12. package/dist/{env-UUFv5Mye.mjs.map → env-C8p8i1E0.mjs.map} +1 -1
  13. package/dist/{fetch-D0IeDN6H.mjs → fetch-BrtIqrid.mjs} +5 -6
  14. package/dist/{fetch-D0IeDN6H.mjs.map → fetch-BrtIqrid.mjs.map} +1 -1
  15. package/dist/{find-D1Z2ac_b.mjs → find-B-vvdlHu.mjs} +3 -4
  16. package/dist/{find-D1Z2ac_b.mjs.map → find-B-vvdlHu.mjs.map} +1 -1
  17. package/dist/{grep-By4wLRDk.mjs → grep-rvZ5S319.mjs} +3 -4
  18. package/dist/{grep-By4wLRDk.mjs.map → grep-rvZ5S319.mjs.map} +1 -1
  19. package/dist/{html-J629LNIm.mjs → html-VrAsoGBK.mjs} +277 -552
  20. package/dist/{html-J629LNIm.mjs.map → html-VrAsoGBK.mjs.map} +1 -1
  21. package/dist/{index-node-esm-CJsN8G51.mjs → index-node-esm-BwaOzhXB.mjs} +2 -3
  22. package/dist/{index-node-esm-CJsN8G51.mjs.map → index-node-esm-BwaOzhXB.mjs.map} +1 -1
  23. package/dist/index.d.mts +33 -101
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3 -3
  26. package/dist/{jsonl-CvgEOakL.mjs → jsonl-BGt7eqPK.mjs} +2 -3
  27. package/dist/{jsonl-CvgEOakL.mjs.map → jsonl-BGt7eqPK.mjs.map} +1 -1
  28. package/dist/{manager-BJLjayBz.mjs → manager-1zEE5WiA.mjs} +4 -6
  29. package/dist/{manager-BJLjayBz.mjs.map → manager-1zEE5WiA.mjs.map} +1 -1
  30. package/dist/{markdown-Bv4J8cg3.mjs → markdown-8ICz5aFw.mjs} +2 -3
  31. package/dist/{markdown-Bv4J8cg3.mjs.map → markdown-8ICz5aFw.mjs.map} +1 -1
  32. package/dist/{masker-jgSUXzjY.mjs → masker-DEgR_ntk.mjs} +4 -6
  33. package/dist/{masker-jgSUXzjY.mjs.map → masker-DEgR_ntk.mjs.map} +1 -1
  34. package/dist/{model-_3ZA2xqm.mjs → model-DqGqxfAc.mjs} +2 -3
  35. package/dist/{model-_3ZA2xqm.mjs.map → model-DqGqxfAc.mjs.map} +1 -1
  36. package/dist/{notify-DpG_m9iP.mjs → notify-Ds0-4BeI.mjs} +2 -3
  37. package/dist/{notify-DpG_m9iP.mjs.map → notify-Ds0-4BeI.mjs.map} +1 -1
  38. package/dist/{read-B-hwcc2Q.mjs → read-C5keKntU.mjs} +4 -44
  39. package/dist/read-C5keKntU.mjs.map +1 -0
  40. package/dist/{registry-Zo0QmKdR.mjs → registry-CntkeXlo.mjs} +34 -32
  41. package/dist/registry-CntkeXlo.mjs.map +1 -0
  42. package/dist/{rolldown-runtime-DktXgydw.mjs → rolldown-runtime-DYf_mQCB.mjs} +1 -2
  43. package/dist/{search-CH0D5CkV.mjs → search-B-89W3SM.mjs} +2 -3
  44. package/dist/{search-CH0D5CkV.mjs.map → search-B-89W3SM.mjs.map} +1 -1
  45. package/dist/session/claude.d.mts +0 -2
  46. package/dist/session/claude.d.mts.map +1 -1
  47. package/dist/session/claude.mjs +4 -6
  48. package/dist/session/claude.mjs.map +1 -1
  49. package/dist/{session-CACkHyDR.mjs → session-C8zqqbPE.mjs} +6 -8
  50. package/dist/{session-CACkHyDR.mjs.map → session-C8zqqbPE.mjs.map} +1 -1
  51. package/dist/{session-Ds5nJz5A.d.mts → session-Cq52cam5.d.mts} +1 -11
  52. package/dist/session-Cq52cam5.d.mts.map +1 -0
  53. package/dist/session.d.mts +1 -3
  54. package/dist/session.d.mts.map +1 -1
  55. package/dist/session.mjs +4 -5
  56. package/dist/{src-DoXC_gDY.mjs → src-C04Qk9KE.mjs} +35 -40
  57. package/dist/{src-DoXC_gDY.mjs.map → src-C04Qk9KE.mjs.map} +1 -1
  58. package/dist/{subagent-BMQMbANL.mjs → subagent-BbcxLzgC.mjs} +4 -5
  59. package/dist/{subagent-BMQMbANL.mjs.map → subagent-BbcxLzgC.mjs.map} +1 -1
  60. package/dist/{swarm-BJ3gQA1Z.mjs → swarm-B_s9Qpbt.mjs} +2 -3
  61. package/dist/{swarm-BJ3gQA1Z.mjs.map → swarm-B_s9Qpbt.mjs.map} +1 -1
  62. package/dist/{swarm-BuFx_xeY.mjs → swarm-D823Yumk.mjs} +2 -3
  63. package/dist/{swarm-BuFx_xeY.mjs.map → swarm-D823Yumk.mjs.map} +1 -1
  64. package/dist/{tasks-WdRAHsOL.mjs → tasks-D76WXWr2.mjs} +3 -4
  65. package/dist/{tasks-WdRAHsOL.mjs.map → tasks-D76WXWr2.mjs.map} +1 -1
  66. package/dist/{tasks-BA-PCeGa.mjs → tasks-Dgsiuh0Q.mjs} +3 -4
  67. package/dist/{tasks-BA-PCeGa.mjs.map → tasks-Dgsiuh0Q.mjs.map} +1 -1
  68. package/dist/{tools-CJ2x45EZ.mjs → tools-DmoUYORG.mjs} +6 -10
  69. package/dist/{tools-CJ2x45EZ.mjs.map → tools-DmoUYORG.mjs.map} +1 -1
  70. package/dist/{truncate-BSMXgiXj.mjs → truncate-CChlZ113.mjs} +2 -3
  71. package/dist/{truncate-BSMXgiXj.mjs.map → truncate-CChlZ113.mjs.map} +1 -1
  72. package/dist/{uuid-CAdJg4l8.mjs → uuid-rfbQ9ad2.mjs} +2 -3
  73. package/dist/{uuid-CAdJg4l8.mjs.map → uuid-rfbQ9ad2.mjs.map} +1 -1
  74. package/dist/{wakeup-BYqTnfSp.mjs → wakeup-CFr1nu_L.mjs} +2 -3
  75. package/dist/{wakeup-BYqTnfSp.mjs.map → wakeup-CFr1nu_L.mjs.map} +1 -1
  76. package/dist/{write-DwGiDNeG.mjs → write-BRQFfWAn.mjs} +2 -3
  77. package/dist/write-BRQFfWAn.mjs.map +1 -0
  78. package/package.json +6 -6
  79. package/dist/compactions-D9XlwQGO.mjs +0 -191
  80. package/dist/compactions-D9XlwQGO.mjs.map +0 -1
  81. package/dist/edit-D4yaBK1K.mjs.map +0 -1
  82. package/dist/read-B-hwcc2Q.mjs.map +0 -1
  83. package/dist/registry-Zo0QmKdR.mjs.map +0 -1
  84. package/dist/session-Ds5nJz5A.d.mts.map +0 -1
  85. package/dist/write-DwGiDNeG.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"masker-jgSUXzjY.mjs","names":["defaults"],"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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"}
1
+ {"version":3,"file":"masker-DEgR_ntk.mjs","names":["defaults"],"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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"}
@@ -1,4 +1,4 @@
1
- //#region src/prompt/model.ts
1
+ // src/prompt/model.ts
2
2
  async function prompt(ctx) {
3
3
  const model = ctx.model.spec;
4
4
  const provider = model.provider.id;
@@ -21,7 +21,6 @@ async function prompt(ctx) {
21
21
  ] : void 0
22
22
  ].flat().filter(Boolean).join("\n");
23
23
  }
24
- //#endregion
25
24
  export { prompt };
26
25
 
27
- //# sourceMappingURL=model-_3ZA2xqm.mjs.map
26
+ //# sourceMappingURL=model-DqGqxfAc.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"model-DqGqxfAc.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"}
@@ -1,5 +1,5 @@
1
1
  import { formatDuration } from "@zaly/shared";
2
- //#region src/notify.ts
2
+ // src/notify.ts
3
3
  var Notifier = class {
4
4
  #opts;
5
5
  #lastTime;
@@ -90,7 +90,6 @@ function timeInfo(t = Date.now()) {
90
90
  tz
91
91
  };
92
92
  }
93
- //#endregion
94
93
  export { Notifier };
95
94
 
96
- //# sourceMappingURL=notify-DpG_m9iP.mjs.map
95
+ //# sourceMappingURL=notify-Ds0-4BeI.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"notify-DpG_m9iP.mjs","names":[],"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,KAAK,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,KAAK,KAAK,MAAM;EAChB,KAAK,MAAM,IAAI,gBAAgB;EAC/B,MAAM,OAAO,EAAE,QAAQ,KAAK,IAAI,OAAO;EACvC,MAAM,QACH,GACC,YACC,EAAE,WAAW;GACZ,KAAK,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,KAAK,YAAY;EACjB,OAAO,SAAS,GAAG;CACrB;CAEA,MAAM,OAAc;EAClB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,KAAK,YAAY,SAAS,KAAK,SAAS,IAAI,KAAA;EAE7D,KAAK,cAAc;EACnB,KAAK,cAAc;EAEnB,IAAI,YAAY,SAAS,GAAG,CAAC,CAAC,SAAS,SAAS,MAC9C,MAAM,OAAO,WAAW,KAAK,KAAK,GAAG,CAAC;OACjC,IAAI,MAAM,KAAK,YAAY,KAAK,MAAM,OAAO,KAClD,MAAM,OAAO,iBAAiB;GAC5B,MAAM,eAAe,KAAK,WAAW,EAAE,IAAI,IAAI,CAAC;GAChD,GAAG,KAAK,KAAK,GAAG;EAClB,CAAC;OACI,IAAI,MAAM,KAAK,YAAY,KAAK,MAAM,WAAW,KACtD,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAG,CAAC;EAErC,KAAK,YAAY;EAQjB,MAAM,WAAW,MAAM;EACvB,IAAI,SAAS,QAAQ,KAAK,gBAAgB;GACxC,MAAM,OAAO,oBAAoB;IAC/B,OAAO,SAAS;IAChB,KAAK,KAAK,MAAM,SAAS,QAAQ,GAAG;IACpC,MAAM,SAAS;GACjB,CAAC;GACD,KAAK,iBAAiB,SAAS;EACjC,OAAO,IAAI,SAAS,UAAU,GAAG,KAAK,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"}
1
+ {"version":3,"file":"notify-Ds0-4BeI.mjs","names":[],"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,KAAK,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,KAAK,KAAK,MAAM;EAChB,KAAK,MAAM,IAAI,gBAAgB;EAC/B,MAAM,OAAO,EAAE,QAAQ,KAAK,IAAI,OAAO;EACvC,MAAM,QACH,GACC,YACC,EAAE,WAAW;GACZ,KAAK,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,KAAK,YAAY;EACjB,OAAO,SAAS,GAAG;CACrB;CAEA,MAAM,OAAc;EAClB,MAAM,MAAM,KAAK,IAAI;EACrB,MAAM,WAAW,KAAK,YAAY,SAAS,KAAK,SAAS,IAAI,KAAA;EAE7D,KAAK,cAAc;EACnB,KAAK,cAAc;EAEnB,IAAI,YAAY,SAAS,GAAG,CAAC,CAAC,SAAS,SAAS,MAC9C,MAAM,OAAO,WAAW,KAAK,KAAK,GAAG,CAAC;OACjC,IAAI,MAAM,KAAK,YAAY,KAAK,MAAM,OAAO,KAClD,MAAM,OAAO,iBAAiB;GAC5B,MAAM,eAAe,KAAK,WAAW,EAAE,IAAI,IAAI,CAAC;GAChD,GAAG,KAAK,KAAK,GAAG;EAClB,CAAC;OACI,IAAI,MAAM,KAAK,YAAY,KAAK,MAAM,WAAW,KACtD,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAG,CAAC;EAErC,KAAK,YAAY;EAQjB,MAAM,WAAW,MAAM;EACvB,IAAI,SAAS,QAAQ,KAAK,gBAAgB;GACxC,MAAM,OAAO,oBAAoB;IAC/B,OAAO,SAAS;IAChB,KAAK,KAAK,MAAM,SAAS,QAAQ,GAAG;IACpC,MAAM,SAAS;GACjB,CAAC;GACD,KAAK,iBAAiB,SAAS;EACjC,OAAO,IAAI,SAAS,UAAU,GAAG,KAAK,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"}
@@ -1,49 +1,10 @@
1
- import { normPath, safeStat } from "@zaly/shared";
1
+ import { normPath } from "@zaly/shared";
2
2
  import { AiError, defineTool, extractToolResults, toAttachment } from "@zaly/ai";
3
3
  import { stat } from "node:fs/promises";
4
4
  import { Type } from "typebox";
5
5
  import { normalizeEol } from "@zaly/shared/text";
6
6
  import { fileDetect } from "@zaly/shared/detect";
7
- //#region src/utils/file.ts
8
- function isFileMeta(meta) {
9
- const m = meta;
10
- return m !== void 0 && m !== null && typeof m === "object" && typeof m.path === "string" && typeof m.mtime === "number" && (m.kind === "read" || m.kind === "write" || m.kind === "edit");
11
- }
12
- function isUnchanged(path, ctx) {
13
- return checkFresh(path, ctx, { full: true }) === true;
14
- }
15
- function checkFresh(path, ctx, opts = {}) {
16
- path = normPath(ctx.cwd, path);
17
- const mtime = safeStat(path)?.mtimeMs;
18
- if (mtime === void 0) return new AiError({
19
- code: "NOT_FOUND",
20
- message: `${path}: file not found`
21
- });
22
- const messages = ctx.messages ?? [];
23
- let ret = freshnessError(path, "NOT_READ");
24
- for (const { m, $p, p } of extractToolResults(messages)) {
25
- const id = m.id;
26
- if (!id || !isFileMeta(p.meta) || p.meta.path !== path || ctx.isMasked?.(id, $p)) continue;
27
- if (opts.full && !p.meta.full) continue;
28
- if (p.meta.mtime === mtime) return true;
29
- ret = freshnessError(path, "STALE");
30
- }
31
- return ret;
32
- }
33
- /** Build the canonical freshness error. `data.reason` distinguishes
34
- * never-read from changed-since-read for callers and renderers. */
35
- function freshnessError(path, reason) {
36
- return new AiError({
37
- code: "FILE_NOT_FRESH",
38
- data: {
39
- path,
40
- reason
41
- },
42
- message: reason === "NOT_READ" ? `${path}: read this file before mutating it.` : `${path}: file changed since last read. Re-read before mutating.`
43
- });
44
- }
45
- //#endregion
46
- //#region src/tools/read.ts
7
+ // src/tools/read.ts
47
8
  /**
48
9
  * Read a file from disk.
49
10
  *
@@ -99,7 +60,7 @@ const readTool = defineTool({
99
60
  code: "NOT_A_FILE",
100
61
  message: `${path} is not a regular file`
101
62
  });
102
- if (isUnchanged(path, ctx)) {
63
+ if (extractToolResults(ctx.messages ?? [], ["read"]).find(({ m, $p, p }) => m.id && p.meta?.path === path && p.meta.full && p.meta.mtime === fileStat.mtimeMs && !ctx.isMasked?.(m.id, $p))) {
103
64
  ctx.meta = {
104
65
  full: false,
105
66
  kind: "read",
@@ -186,7 +147,6 @@ function formatTextSlice(content, { offset, limit }) {
186
147
  offset: start + 1
187
148
  };
188
149
  }
189
- //#endregion
190
150
  export { readTool };
191
151
 
192
- //# sourceMappingURL=read-B-hwcc2Q.mjs.map
152
+ //# sourceMappingURL=read-C5keKntU.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-C5keKntU.mjs","names":[],"sources":["../src/tools/read.ts"],"sourcesContent":["import type { Content, Tool, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, defineTool, extractToolResults, toAttachment } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { fileDetect } from \"@zaly/shared/detect\"\nimport { normalizeEol } from \"@zaly/shared/text\"\nimport { stat } from \"node:fs/promises\"\nimport { Type } from \"typebox\"\n\n/**\n * Read a file from disk.\n *\n * Output shape:\n * - Text files → plain text plus a `<slice>` metadata part describing\n * the returned line range, including when reading the whole file.\n * - Image files (png, jpeg, webp, gif, avif, …) → `ImagePart` attachment.\n * The model sees the image natively if its modality supports it.\n * - Other binary files → `BINARY_FILE` error with a hint.\n *\n * Path resolution: relative paths resolve against the agent's session\n * cwd via `process.cwd()` for now. When the permission system gates this\n * via a tool-side hook, the cwd from `PermissionContext` will be used\n * instead so resolution stays consistent across the loop.\n */\nconst DEFAULT_LIMIT = 2000\nconst MAX_LINE_LENGTH = 2000\n\nexport type ReadTool = typeof readTool\nexport type FileTool = Tool<{ path: string }, unknown, FileMeta>\n\nexport type FileMeta = {\n path: string\n mtime: number\n kind: \"read\" | \"write\" | \"edit\"\n /** True when the result reflects the whole-file content — set by\n * un-sliced `read` and by `write` (which always replaces the\n * full file). Used by the masker to know whether this result\n * subsumes earlier reads of the same path. `edit` never sets it\n * because the result is patch-relative. */\n full?: boolean\n unchanged?: boolean\n}\n\nexport type ReadToolMeta = FileMeta & {\n offset: number\n limit: number\n}\n\n/** Build the `read` tool with model-aware schema description. The\n * attachment-shape blurb is included only when the loaded model\n * actually accepts the relevant modality — text-only models see a\n * description that promises only text output, so they don't reach\n * for the tool expecting images they can't process. */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const readTool = defineTool({\n name: \"read\",\n desc: `Read a file. Text files return plain text with line-range metadata. Image & PDF files are sent as attachments. Use \\`offset\\`/\\`limit\\` to slice large files.`,\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n path: Type.String({ description: \"Path to the file. Absolute or cwd-relative.\" }),\n offset: Type.Optional(\n Type.Integer({\n default: 1,\n description:\n \"1-based line number to start at. Negative values count from \" +\n \"the end: -50 starts 50 lines before EOF (so `offset: -50` \" +\n \"with the default limit returns the last 50 lines, like \" +\n \"`tail -n 50`). `offset: -50, limit: 20` reads 20 lines \" +\n \"starting 50 from the end.\",\n })\n ),\n limit: Type.Optional(\n Type.Integer({\n default: DEFAULT_LIMIT,\n description: \"Maximum number of lines to return.\",\n minimum: 1,\n })\n ),\n }),\n\n async preflight(args, ctx: ToolContext<ReadToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n await ctx.need?.(\"read\", path)\n },\n\n async call(args, ctx: ToolContext<ReadToolMeta>): Promise<Content> {\n const path = normPath(ctx.cwd, args.path)\n\n const fileStat = await stat(path).catch((error: unknown) => {\n throw new AiError({\n cause: error,\n code: \"NOT_FOUND\",\n message: `cannot read ${path}: file not found`,\n })\n })\n if (!fileStat.isFile()) {\n throw new AiError({ code: \"NOT_A_FILE\", message: `${path} is not a regular file` })\n }\n\n // Check whether we've already read this file's current bytes. If so, we can skip\n // returning the content again. This is a context optimization for large files\n // that are read multiple times without changes.\n const existing = extractToolResults<Partial<ReadToolMeta>>(ctx.messages ?? [], [\"read\"]).find(\n ({ m, $p, p }) =>\n m.id &&\n p.meta?.path === path &&\n p.meta.full &&\n p.meta.mtime === fileStat.mtimeMs &&\n !ctx.isMasked?.(m.id, $p)\n )\n\n if (existing) {\n // The exact file is still fully read in context, so we can skip returning it again.\n ctx.meta = {\n full: false,\n kind: \"read\",\n limit: 0,\n mtime: fileStat.mtimeMs,\n offset: 0,\n path,\n unchanged: true,\n }\n return [\n { content: `file unchanged since last read: ${path}`, tag: \"unchanged\", type: \"meta\" },\n ]\n }\n\n const file = await fileDetect(path)\n if (!file) {\n throw new AiError({\n code: \"READ_ERROR\",\n message: `${path}: could not read file`,\n })\n }\n\n // Threshold-based binary check — files with sporadic control bytes\n // (logs with ANSI styling, source with form feeds) still read as\n // text; only when binary content dominates the sample do we bail.\n if (file.type === \"binary\") {\n throw new AiError({\n code: \"BINARY_FILE\",\n data: { bytes: file.data.length, path },\n message: `${path}: binary file (${file.data.length} bytes); not displayable as text`,\n })\n }\n\n const att = await toAttachment(file)\n if (att) return [att]\n\n if (file.type !== \"text\") {\n throw new AiError({\n code: \"UNSUPPORTED_FILE\",\n data: { path, type: file.type },\n message: `${path}: unsupported file type ${file.type}`,\n })\n }\n\n const text = new TextDecoder().decode(file.data)\n const slice = formatTextSlice(text, {\n limit: args.limit ?? DEFAULT_LIMIT,\n offset: args.offset ?? 1,\n })\n\n // Record the observed mtime for unchanged-read detection. The masker\n // uses `full` to know whether this read subsumes earlier reads.\n ctx.meta = {\n full: slice.full,\n kind: \"read\",\n limit: slice.limit,\n mtime: fileStat.mtimeMs,\n offset: slice.offset,\n path,\n }\n\n return slice.content\n },\n})\n\n/** Return plain text and line-range metadata for both full and sliced reads. */\nfunction formatTextSlice(\n content: string,\n { offset, limit }: { offset: number; limit: number }\n): { content: Content; full?: boolean; offset: number; limit: number } {\n // Normalize line endings to LF for display. The model always sees LF;\n // edit/write detect the file's actual style and re-apply it on disk.\n content = normalizeEol(content)\n const lines = content.split(\"\\n\")\n // `split` on a final newline produces a trailing empty string — drop\n // it so a 3-line file with trailing LF reads as 3 lines, not 4.\n if (lines.at(-1) === \"\") lines.pop()\n\n // Negative offset = \"from the end\" — Python-slice / `tail -n` semantic.\n // `-1000` on a 30-line file clamps to 0 (read whole file) rather than\n // erroring; `0` is treated as `1` (head, off-by-one tolerance).\n let start = offset < 0 ? Math.max(0, lines.length + offset) : Math.max(0, offset - 1)\n // Overshoot clamps to `lines.length` so the slice loop emits nothing.\n // The agent gets an empty text + a `<slice>` meta that reports the\n // real file size, then can re-issue with a sensible offset.\n if (start >= lines.length) start = lines.length\n const end = Math.min(lines.length, start + limit)\n\n const out: string[] = []\n let full = start === 0 && end === lines.length\n for (let i = start; i < end; i++) {\n let line = lines[i]\n if (line.length > MAX_LINE_LENGTH) {\n full = false\n line = `${line.slice(0, MAX_LINE_LENGTH)}… [line truncated, ${line.length} chars]`\n }\n out.push(line)\n }\n\n const text = out.join(\"\\n\")\n\n // Empty slice (overshot offset): tell the agent the offset they asked\n // for and the real file size, rather than emitting a nonsensical\n // \"showing 6-5 of 5\"-style range.\n const sliceMsg =\n start === end && lines.length > 1\n ? `offset ${offset} past end of file (${lines.length} lines)`\n : `showing ${start + 1}-${end} of ${lines.length}`\n\n return {\n content: [\n { content: sliceMsg, tag: \"slice\", type: \"meta\" },\n { text, type: \"text\" },\n ],\n full,\n limit: end - start,\n offset: start + 1,\n }\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
- import { i as __toESM, t as __commonJSMin } from "./rolldown-runtime-DktXgydw.mjs";
2
- import { n as parseBash, t as TOOLS } from "./tools-CJ2x45EZ.mjs";
1
+ import { i as __toESM, t as __commonJSMin } from "./rolldown-runtime-DYf_mQCB.mjs";
2
+ import { n as parseBash, t as TOOLS } from "./tools-DmoUYORG.mjs";
3
3
  import { normPath, prettyPath } from "@zaly/shared";
4
4
  import { createRegistry } from "@zaly/shared/registry";
5
5
  import { dirname, isAbsolute, relative } from "pathe";
6
- //#region src/permissions/handlers/bash.ts
6
+ // src/permissions/handlers/bash.ts
7
7
  /**
8
8
  * Handler for `Rule<"bash">` — evaluates a bash command line against
9
9
  * pattern rules, then delegates each segment's inferred and explicit
@@ -154,8 +154,7 @@ function matchRule(rule, seg) {
154
154
  function resolveRules(rules, seg) {
155
155
  for (const r of rules) if (matchRule(r, seg)) return r.policy;
156
156
  }
157
- //#endregion
158
- //#region src/permissions/handlers/files.ts
157
+ // src/permissions/handlers/files.ts
159
158
  var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
160
159
  function makeArray(subject) {
161
160
  return Array.isArray(subject) ? subject : [subject];
@@ -167,6 +166,7 @@ var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
167
166
  const REGEX_LITERAL_SPECIAL = /[.*+?()[\]{}^$|\\/]/;
168
167
  const BOM = "";
169
168
  const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
169
+ const REGEX_ANCHORED_GLOBSTAR = /^\/\*{2,}$/;
170
170
  const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
171
171
  const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
172
172
  const REGEX_SPLITALL_CRLF = /\r?\n/g;
@@ -308,6 +308,11 @@ var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
308
308
  (_, run, space) => space ? run.slice(0, run.length - run.length % 2) + SPACE : run,
309
309
  "\\ "
310
310
  ],
311
+ [
312
+ /(^|\/)\*{3,}(?=\/|$)/g,
313
+ (_, start) => `${start}**`,
314
+ "*"
315
+ ],
311
316
  [/[\\$.|*+(){^]/g, (match) => `\\${match}`],
312
317
  [
313
318
  /(?!\\)\?/g,
@@ -454,6 +459,7 @@ var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
454
459
  return out;
455
460
  };
456
461
  const makeRegexPrefix = (pattern) => {
462
+ if (REGEX_ANCHORED_GLOBSTAR.test(pattern)) pattern = "**";
457
463
  const { source, sources } = extractBrackets(pattern);
458
464
  const replaced = REPLACERS.reduce((prev, [matcher, replacer, required]) => {
459
465
  if (matcher === DIRECT) return replacer(prev, pattern);
@@ -592,29 +598,27 @@ var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
592
598
  if (matchedRule) ret.rule = matchedRule;
593
599
  return ret;
594
600
  }
595
- test(path, checkUnignored, mode) {
596
- let ignored = false;
597
- let unignored = false;
598
- let matchedRule;
601
+ test(path, mode) {
599
602
  const rules = this._rules;
600
603
  const { length } = rules;
601
604
  const shortcut = this._basenameCount * 2 >= length;
602
605
  const basename = shortcut ? basenameOf(path) : path;
603
- for (let index = 0; index < length; index++) {
606
+ for (let index = length - 1; index >= 0; index--) {
604
607
  const rule = rules[index];
605
- const { negative } = rule;
606
- if (!(unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) && rule[mode].test(shortcut && rule._basenameOnly ? basename : path)) {
607
- ignored = !negative;
608
- unignored = negative;
609
- matchedRule = negative ? UNDEFINED : rule;
608
+ if (rule[mode].test(shortcut && rule._basenameOnly ? basename : path)) {
609
+ const { negative } = rule;
610
+ const ret = {
611
+ ignored: !negative,
612
+ unignored: negative
613
+ };
614
+ if (!negative) ret.rule = rule;
615
+ return ret;
610
616
  }
611
617
  }
612
- const ret = {
613
- ignored,
614
- unignored
618
+ return {
619
+ ignored: false,
620
+ unignored: false
615
621
  };
616
- if (matchedRule) ret.rule = matchedRule;
617
- return ret;
618
622
  }
619
623
  };
620
624
  const throwError = (message, Ctor) => {
@@ -657,26 +661,26 @@ var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
657
661
  addPattern(pattern) {
658
662
  return this.add(pattern);
659
663
  }
660
- _test(originalPath, cache, checkUnignored) {
664
+ _test(originalPath, cache) {
661
665
  const path = originalPath && checkPath.convert(originalPath);
662
666
  checkPath(path, originalPath, this._strictPathCheck ? throwError : RETURN_FALSE);
663
- return this._t(path, cache, checkUnignored);
667
+ return this._t(path, cache);
664
668
  }
665
669
  checkIgnore(path) {
666
670
  if (path.charCodeAt(path.length - 1) !== SLASH_CODE) return this.test(path);
667
- const dir = this._t(path, this._testCache, true);
671
+ const dir = this._t(path, this._testCache);
668
672
  if (dir.ignored) return dir;
669
673
  const literal = this._rules.testLiteral(path);
670
674
  return literal.ignored || literal.unignored ? literal : dir;
671
675
  }
672
- _t(path, cache, checkUnignored) {
676
+ _t(path, cache) {
673
677
  if (path in cache) return cache[path];
674
678
  const parentPath = parentOf(path);
675
- const parent = parentPath ? this._t(parentPath, cache, checkUnignored) : UNDEFINED;
676
- return cache[path] = parent && parent.ignored ? parent : this._rules.test(path, checkUnignored, MODE_IGNORE);
679
+ const parent = parentPath ? this._t(parentPath, cache) : UNDEFINED;
680
+ return cache[path] = parent && parent.ignored ? parent : this._rules.test(path, MODE_IGNORE);
677
681
  }
678
682
  ignores(path) {
679
- return this._test(path, this._ignoreCache, false).ignored;
683
+ return this._test(path, this._ignoreCache).ignored;
680
684
  }
681
685
  createFilter() {
682
686
  return (path) => !this.ignores(path);
@@ -685,7 +689,7 @@ var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
685
689
  return makeArray(paths).filter(this.createFilter());
686
690
  }
687
691
  test(path) {
688
- return this._test(path, this._testCache, true);
692
+ return this._test(path, this._testCache);
689
693
  }
690
694
  };
691
695
  const factory = (options) => new Ignore(options);
@@ -834,8 +838,7 @@ function relativizeToWorkspace(absPattern, workspace) {
834
838
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return void 0;
835
839
  return `/${rel}`;
836
840
  }
837
- //#endregion
838
- //#region src/permissions/handlers/tool.ts
841
+ // src/permissions/handlers/tool.ts
839
842
  /**
840
843
  * Generic tool-scope handler.
841
844
  *
@@ -882,7 +885,6 @@ const handlerRegistry = createRegistry("scope").from({
882
885
  tool: () => toolHandler,
883
886
  write: () => fileHandler
884
887
  });
885
- //#endregion
886
888
  export { handlerRegistry as t };
887
889
 
888
- //# sourceMappingURL=registry-Zo0QmKdR.mjs.map
890
+ //# sourceMappingURL=registry-CntkeXlo.mjs.map