@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":"truncate-BSMXgiXj.mjs","names":["sw"],"sources":["../src/utils/truncate.ts"],"sourcesContent":["import { sliceAnsi, splitAnsi, stringWidth as sw } from \"@zaly/shared/ansi\"\n\nconst MAX_CHARS = 50_000\nconst MAX_LINES = 2000\nconst MAX_LINE_CHARS = 1000\n\nexport type TruncateOps = {\n strategy?: \"head\" | \"tail\" | \"head+tail\"\n /** Max chars to keep (default: 50k). */\n maxChars?: number\n /** Max lines to keep (default: 2000). */\n maxLines?: number\n /** Max chars per line (default: 1000). */\n maxLineChars?: number\n /** When `strategy` is \"head+tail\", how many lines to keep in the head.\n * When < 1, treated as a fraction of `maxLines`. Default: maxLines/2. */\n head?: number\n}\n\nexport type TruncateResult = {\n text: string\n truncated: boolean\n truncatedLines?: boolean\n truncatedChars?: boolean\n truncatedLineChars?: boolean\n opts: Required<TruncateOps>\n origLines: number\n origChars: number\n origLongestLine: number\n origBytes: number\n}\n\nconst isJsonish = (lines: string[]) => lines.length === 1 && /^[\\s]*[[{]/.test(lines[0])\n\nfunction truncateOpts(opts: TruncateOps = {}): Required<TruncateOps> {\n const maxLines = Math.max(1, opts.maxLines ?? MAX_LINES)\n let head = opts.head ?? Math.floor(maxLines / 2)\n if (head < 1) head = Math.floor(maxLines * head)\n head = Math.max(0, Math.min(head, maxLines))\n return {\n ...opts,\n head,\n maxChars: Math.max(1, opts.maxChars ?? MAX_CHARS),\n maxLineChars: Math.max(1, opts.maxLineChars ?? MAX_LINE_CHARS),\n maxLines,\n strategy: opts.strategy ?? \"head+tail\",\n }\n}\n\nexport function truncate(data: string | Buffer, opts: TruncateOps = {}) {\n const text = typeof data === \"string\" ? data : data.toString(\"utf8\")\n const o = truncateOpts(opts)\n\n let lines = splitAnsi(text)\n while (lines.at(-1) === \"\") lines.pop()\n const bytes = typeof data === \"string\" ? Buffer.byteLength(data, \"utf8\") : data.length\n\n const ret: TruncateResult = {\n opts: o,\n origBytes: bytes,\n origChars: lines.reduce((sum, l) => sum + sw(l) + 1, 0) - 1,\n origLines: lines.length,\n origLongestLine: lines.reduce((max, l) => Math.max(max, sw(l)), 0),\n text,\n truncated: false,\n }\n\n if (lines.length === 0) return ret\n\n const jsonish = isJsonish(lines)\n const truncLineChars = !jsonish && ret.origLongestLine > o.maxLineChars\n\n if (lines.length <= o.maxLines && ret.origChars <= o.maxChars && !truncLineChars) return ret\n\n ret.truncated = true\n\n if (jsonish && ret.origChars > o.maxChars) {\n const marker = ` … [truncated ${ret.origChars - o.maxChars} chars]`\n const len = Math.max(0, o.maxChars - marker.length)\n ret.text = sliceAnsi(lines[0], 0, len)\n ret.text += marker\n ret.truncatedChars = true\n return ret\n }\n\n if (truncLineChars) {\n lines = lines.map((l) => {\n const len = sw(l)\n if (len <= o.maxLineChars) return l\n ret.truncatedLineChars = true\n const trunc = len - o.maxLineChars\n return `${sliceAnsi(l, 0, o.maxLineChars)} [ … truncated ${trunc} chars]`\n })\n }\n\n const truncateLines = (ll: string[], maxLines: number, maxChars: number, reverse = false) => {\n ll = reverse ? ll.toReversed() : ll\n if (ll.length > maxLines) {\n ret.truncatedLines = true\n ll = ll.slice(0, maxLines)\n }\n let c = 0\n ll = ll.filter((l) => {\n c += sw(l) + 1 // +1 for the dropped \\n\n if (c <= maxChars) return true\n ret.truncatedChars = true\n ret.truncatedLines = true\n return false\n })\n return reverse ? ll.toReversed() : ll\n }\n\n let strategy: \"head\" | \"tail\" | \"head+tail\" = o.strategy\n if (strategy === \"head+tail\") {\n if (o.head === 0) strategy = \"tail\"\n else if (o.head >= o.maxLines) strategy = \"head\"\n }\n\n if (strategy === \"head\") {\n lines = truncateLines(lines, o.maxLines, o.maxChars)\n const truncated = ret.origLines - lines.length\n if (truncated) lines.push(` [ … truncated ${truncated} lines]`)\n } else if (strategy === \"tail\") {\n lines = truncateLines(lines, o.maxLines, o.maxChars, true)\n const truncated = ret.origLines - lines.length\n if (truncated) lines.unshift(` [ … truncated ${truncated} lines]`)\n } else {\n const headMaxChars = Math.floor((o.head / o.maxLines) * o.maxChars)\n const tailMaxChars = o.maxChars - headMaxChars\n const headLines = lines.splice(0, o.head)\n const head = truncateLines(headLines, o.head, headMaxChars)\n const tail = truncateLines(lines, o.maxLines - o.head, tailMaxChars, true)\n const truncated = ret.origLines - head.length - tail.length\n lines = truncated\n ? [...head, ` [ … truncated ${truncated} lines]`, ...tail]\n : [...head, ...tail]\n }\n\n ret.text = lines.join(\"\\n\")\n return ret\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"truncate-CChlZ113.mjs","names":["sw"],"sources":["../src/utils/truncate.ts"],"sourcesContent":["import { sliceAnsi, splitAnsi, stringWidth as sw } from \"@zaly/shared/ansi\"\n\nconst MAX_CHARS = 50_000\nconst MAX_LINES = 2000\nconst MAX_LINE_CHARS = 1000\n\nexport type TruncateOps = {\n strategy?: \"head\" | \"tail\" | \"head+tail\"\n /** Max chars to keep (default: 50k). */\n maxChars?: number\n /** Max lines to keep (default: 2000). */\n maxLines?: number\n /** Max chars per line (default: 1000). */\n maxLineChars?: number\n /** When `strategy` is \"head+tail\", how many lines to keep in the head.\n * When < 1, treated as a fraction of `maxLines`. Default: maxLines/2. */\n head?: number\n}\n\nexport type TruncateResult = {\n text: string\n truncated: boolean\n truncatedLines?: boolean\n truncatedChars?: boolean\n truncatedLineChars?: boolean\n opts: Required<TruncateOps>\n origLines: number\n origChars: number\n origLongestLine: number\n origBytes: number\n}\n\nconst isJsonish = (lines: string[]) => lines.length === 1 && /^[\\s]*[[{]/.test(lines[0])\n\nfunction truncateOpts(opts: TruncateOps = {}): Required<TruncateOps> {\n const maxLines = Math.max(1, opts.maxLines ?? MAX_LINES)\n let head = opts.head ?? Math.floor(maxLines / 2)\n if (head < 1) head = Math.floor(maxLines * head)\n head = Math.max(0, Math.min(head, maxLines))\n return {\n ...opts,\n head,\n maxChars: Math.max(1, opts.maxChars ?? MAX_CHARS),\n maxLineChars: Math.max(1, opts.maxLineChars ?? MAX_LINE_CHARS),\n maxLines,\n strategy: opts.strategy ?? \"head+tail\",\n }\n}\n\nexport function truncate(data: string | Buffer, opts: TruncateOps = {}) {\n const text = typeof data === \"string\" ? data : data.toString(\"utf8\")\n const o = truncateOpts(opts)\n\n let lines = splitAnsi(text)\n while (lines.at(-1) === \"\") lines.pop()\n const bytes = typeof data === \"string\" ? Buffer.byteLength(data, \"utf8\") : data.length\n\n const ret: TruncateResult = {\n opts: o,\n origBytes: bytes,\n origChars: lines.reduce((sum, l) => sum + sw(l) + 1, 0) - 1,\n origLines: lines.length,\n origLongestLine: lines.reduce((max, l) => Math.max(max, sw(l)), 0),\n text,\n truncated: false,\n }\n\n if (lines.length === 0) return ret\n\n const jsonish = isJsonish(lines)\n const truncLineChars = !jsonish && ret.origLongestLine > o.maxLineChars\n\n if (lines.length <= o.maxLines && ret.origChars <= o.maxChars && !truncLineChars) return ret\n\n ret.truncated = true\n\n if (jsonish && ret.origChars > o.maxChars) {\n const marker = ` … [truncated ${ret.origChars - o.maxChars} chars]`\n const len = Math.max(0, o.maxChars - marker.length)\n ret.text = sliceAnsi(lines[0], 0, len)\n ret.text += marker\n ret.truncatedChars = true\n return ret\n }\n\n if (truncLineChars) {\n lines = lines.map((l) => {\n const len = sw(l)\n if (len <= o.maxLineChars) return l\n ret.truncatedLineChars = true\n const trunc = len - o.maxLineChars\n return `${sliceAnsi(l, 0, o.maxLineChars)} [ … truncated ${trunc} chars]`\n })\n }\n\n const truncateLines = (ll: string[], maxLines: number, maxChars: number, reverse = false) => {\n ll = reverse ? ll.toReversed() : ll\n if (ll.length > maxLines) {\n ret.truncatedLines = true\n ll = ll.slice(0, maxLines)\n }\n let c = 0\n ll = ll.filter((l) => {\n c += sw(l) + 1 // +1 for the dropped \\n\n if (c <= maxChars) return true\n ret.truncatedChars = true\n ret.truncatedLines = true\n return false\n })\n return reverse ? ll.toReversed() : ll\n }\n\n let strategy: \"head\" | \"tail\" | \"head+tail\" = o.strategy\n if (strategy === \"head+tail\") {\n if (o.head === 0) strategy = \"tail\"\n else if (o.head >= o.maxLines) strategy = \"head\"\n }\n\n if (strategy === \"head\") {\n lines = truncateLines(lines, o.maxLines, o.maxChars)\n const truncated = ret.origLines - lines.length\n if (truncated) lines.push(` [ … truncated ${truncated} lines]`)\n } else if (strategy === \"tail\") {\n lines = truncateLines(lines, o.maxLines, o.maxChars, true)\n const truncated = ret.origLines - lines.length\n if (truncated) lines.unshift(` [ … truncated ${truncated} lines]`)\n } else {\n const headMaxChars = Math.floor((o.head / o.maxLines) * o.maxChars)\n const tailMaxChars = o.maxChars - headMaxChars\n const headLines = lines.splice(0, o.head)\n const head = truncateLines(headLines, o.head, headMaxChars)\n const tail = truncateLines(lines, o.maxLines - o.head, tailMaxChars, true)\n const truncated = ret.origLines - head.length - tail.length\n lines = truncated\n ? [...head, ` [ … truncated ${truncated} lines]`, ...tail]\n : [...head, ...tail]\n }\n\n ret.text = lines.join(\"\\n\")\n return ret\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- //#region src/utils/uuid.ts
1
+ // src/utils/uuid.ts
2
2
  /**
3
3
  * RFC 9562 UUIDv7 — timestamp-prefixed (48-bit ms since epoch),
4
4
  * monotonic within a millisecond via a 32-bit sequence counter,
@@ -53,7 +53,6 @@ function isUuidv7Like(s) {
53
53
  const ts = parseInt(s, 16);
54
54
  return ts > Date.parse("2026-01-01T00:00:00Z") && ts < Date.now() + 36e5;
55
55
  }
56
- //#endregion
57
56
  export { isUuidv7Like as n, uuidv7 as r, isUuidv7 as t };
58
57
 
59
- //# sourceMappingURL=uuid-CAdJg4l8.mjs.map
58
+ //# sourceMappingURL=uuid-rfbQ9ad2.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"uuid-CAdJg4l8.mjs","names":[],"sources":["../src/utils/uuid.ts"],"sourcesContent":["/**\n * RFC 9562 UUIDv7 — timestamp-prefixed (48-bit ms since epoch),\n * monotonic within a millisecond via a 32-bit sequence counter,\n * random tail otherwise. Sortable lexicographically by creation time,\n * which is handy for debugging (\"show me the latest few records by\n * uuid\") and for any future DB index.\n *\n * Hand-rolled to keep `@zaly/agent` dep-free. Same algorithm as the\n * `uuid` package's v7 implementation, just without the buffer/offset\n * ceremony — we only ever want the canonical string form.\n */\n\nlet lastMs = 0\nlet seq = 0\n\nexport function uuidv7(): string {\n let ms = Date.now()\n if (ms > lastMs) {\n // Time moved on — fresh sequence base.\n lastMs = ms\n seq = Math.floor(Math.random() * 0x1_00_00_00_00)\n } else {\n // Same (or earlier) ms — bump sequence with 32-bit rollover. On\n // rollover, advance the timestamp by one ms to preserve\n // monotonicity (allowed by RFC 9562 §6.2; self-corrects as the\n // wall clock catches up).\n seq = (seq + 1) >>> 0\n if (seq === 0) lastMs++\n ms = lastMs\n }\n\n const r = crypto.getRandomValues(new Uint8Array(6))\n const b = new Uint8Array(16)\n\n // bytes 0-5: 48-bit big-endian millisecond timestamp\n b[0] = (ms / 0x1_00_00_00_00_00) & 0xff\n b[1] = (ms / 0x1_00_00_00_00) & 0xff\n b[2] = (ms / 0x1_00_00_00) & 0xff\n b[3] = (ms / 0x1_00_00) & 0xff\n b[4] = (ms / 0x1_00) & 0xff\n b[5] = ms & 0xff\n // byte 6: version (0x70) + top 4 bits of sequence\n b[6] = 0x70 | ((seq >>> 28) & 0x0f)\n // byte 7: next 8 bits of sequence\n b[7] = (seq >>> 20) & 0xff\n // byte 8: variant (0x80) + next 6 bits of sequence\n b[8] = 0x80 | ((seq >>> 14) & 0x3f)\n // byte 9: next 8 bits of sequence\n b[9] = (seq >>> 6) & 0xff\n // byte 10: low 6 bits of sequence + 2 random bits\n b[10] = ((seq << 2) & 0xff) | (r[0] & 0x03)\n // bytes 11-15: 40 random bits\n b[11] = r[1]\n b[12] = r[2]\n b[13] = r[3]\n b[14] = r[4]\n b[15] = r[5]\n\n const hex = [...b].map((x) => x.toString(16).padStart(2, \"0\")).join(\"\")\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nexport function isUuidv7(s: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s)\n}\n\nexport function isUuidv7Like(s: string): boolean {\n if (s.length < 7) return false\n // First 12 chars of a v7 UUID is a timestamp\n s = s.replace(/-/g, \"\").slice(0, 12).padEnd(12, \"0\").toLowerCase()\n if (!/^[0-9a-f]+$/.test(s)) return false\n const ts = parseInt(s, 16)\n // Reject timestamps before 2026, and more than an hour in the future to allow for clock drift\n return ts > Date.parse(\"2026-01-01T00:00:00Z\") && ts < Date.now() + 3600 * 1000\n}\n"],"mappings":";;;;;;;;;;;;AAYA,IAAI,SAAS;AACb,IAAI,MAAM;AAEV,SAAgB,SAAiB;CAC/B,IAAI,KAAK,KAAK,IAAI;CAClB,IAAI,KAAK,QAAQ;EAEf,SAAS;EACT,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,UAAe;CAClD,OAAO;EAKL,MAAO,MAAM,MAAO;EACpB,IAAI,QAAQ,GAAG;EACf,KAAK;CACP;CAEA,MAAM,IAAI,OAAO,gCAAgB,IAAI,WAAW,CAAC,CAAC;CAClD,MAAM,oBAAI,IAAI,WAAW,EAAE;CAG3B,EAAE,KAAM,KAAK,gBAAsB;CACnC,EAAE,KAAM,KAAK,aAAmB;CAChC,EAAE,KAAM,KAAK,WAAgB;CAC7B,EAAE,KAAM,KAAK,QAAa;CAC1B,EAAE,KAAM,KAAK,MAAU;CACvB,EAAE,KAAK,KAAK;CAEZ,EAAE,KAAK,MAAS,QAAQ,KAAM;CAE9B,EAAE,KAAM,QAAQ,KAAM;CAEtB,EAAE,KAAK,MAAS,QAAQ,KAAM;CAE9B,EAAE,KAAM,QAAQ,IAAK;CAErB,EAAE,MAAQ,OAAO,IAAK,MAAS,EAAE,KAAK;CAEtC,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CAEV,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;CACtE,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE;AAC7G;AAEA,SAAgB,SAAS,GAAoB;CAC3C,OAAO,yEAAyE,KAAK,CAAC;AACxF;AAEA,SAAgB,aAAa,GAAoB;CAC/C,IAAI,EAAE,SAAS,GAAG,OAAO;CAEzB,IAAI,EAAE,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC,YAAY;CACjE,IAAI,CAAC,cAAc,KAAK,CAAC,GAAG,OAAO;CACnC,MAAM,KAAK,SAAS,GAAG,EAAE;CAEzB,OAAO,KAAK,KAAK,MAAM,sBAAsB,KAAK,KAAK,KAAK,IAAI,IAAI;AACtE"}
1
+ {"version":3,"file":"uuid-rfbQ9ad2.mjs","names":[],"sources":["../src/utils/uuid.ts"],"sourcesContent":["/**\n * RFC 9562 UUIDv7 — timestamp-prefixed (48-bit ms since epoch),\n * monotonic within a millisecond via a 32-bit sequence counter,\n * random tail otherwise. Sortable lexicographically by creation time,\n * which is handy for debugging (\"show me the latest few records by\n * uuid\") and for any future DB index.\n *\n * Hand-rolled to keep `@zaly/agent` dep-free. Same algorithm as the\n * `uuid` package's v7 implementation, just without the buffer/offset\n * ceremony — we only ever want the canonical string form.\n */\n\nlet lastMs = 0\nlet seq = 0\n\nexport function uuidv7(): string {\n let ms = Date.now()\n if (ms > lastMs) {\n // Time moved on — fresh sequence base.\n lastMs = ms\n seq = Math.floor(Math.random() * 0x1_00_00_00_00)\n } else {\n // Same (or earlier) ms — bump sequence with 32-bit rollover. On\n // rollover, advance the timestamp by one ms to preserve\n // monotonicity (allowed by RFC 9562 §6.2; self-corrects as the\n // wall clock catches up).\n seq = (seq + 1) >>> 0\n if (seq === 0) lastMs++\n ms = lastMs\n }\n\n const r = crypto.getRandomValues(new Uint8Array(6))\n const b = new Uint8Array(16)\n\n // bytes 0-5: 48-bit big-endian millisecond timestamp\n b[0] = (ms / 0x1_00_00_00_00_00) & 0xff\n b[1] = (ms / 0x1_00_00_00_00) & 0xff\n b[2] = (ms / 0x1_00_00_00) & 0xff\n b[3] = (ms / 0x1_00_00) & 0xff\n b[4] = (ms / 0x1_00) & 0xff\n b[5] = ms & 0xff\n // byte 6: version (0x70) + top 4 bits of sequence\n b[6] = 0x70 | ((seq >>> 28) & 0x0f)\n // byte 7: next 8 bits of sequence\n b[7] = (seq >>> 20) & 0xff\n // byte 8: variant (0x80) + next 6 bits of sequence\n b[8] = 0x80 | ((seq >>> 14) & 0x3f)\n // byte 9: next 8 bits of sequence\n b[9] = (seq >>> 6) & 0xff\n // byte 10: low 6 bits of sequence + 2 random bits\n b[10] = ((seq << 2) & 0xff) | (r[0] & 0x03)\n // bytes 11-15: 40 random bits\n b[11] = r[1]\n b[12] = r[2]\n b[13] = r[3]\n b[14] = r[4]\n b[15] = r[5]\n\n const hex = [...b].map((x) => x.toString(16).padStart(2, \"0\")).join(\"\")\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`\n}\n\nexport function isUuidv7(s: string): boolean {\n return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(s)\n}\n\nexport function isUuidv7Like(s: string): boolean {\n if (s.length < 7) return false\n // First 12 chars of a v7 UUID is a timestamp\n s = s.replace(/-/g, \"\").slice(0, 12).padEnd(12, \"0\").toLowerCase()\n if (!/^[0-9a-f]+$/.test(s)) return false\n const ts = parseInt(s, 16)\n // Reject timestamps before 2026, and more than an hour in the future to allow for clock drift\n return ts > Date.parse(\"2026-01-01T00:00:00Z\") && ts < Date.now() + 3600 * 1000\n}\n"],"mappings":";;;;;;;;;;;;AAYA,IAAI,SAAS;AACb,IAAI,MAAM;AAEV,SAAgB,SAAiB;CAC/B,IAAI,KAAK,KAAK,IAAI;CAClB,IAAI,KAAK,QAAQ;EAEf,SAAS;EACT,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,UAAe;CAClD,OAAO;EAKL,MAAO,MAAM,MAAO;EACpB,IAAI,QAAQ,GAAG;EACf,KAAK;CACP;CAEA,MAAM,IAAI,OAAO,gCAAgB,IAAI,WAAW,CAAC,CAAC;CAClD,MAAM,oBAAI,IAAI,WAAW,EAAE;CAG3B,EAAE,KAAM,KAAK,gBAAsB;CACnC,EAAE,KAAM,KAAK,aAAmB;CAChC,EAAE,KAAM,KAAK,WAAgB;CAC7B,EAAE,KAAM,KAAK,QAAa;CAC1B,EAAE,KAAM,KAAK,MAAU;CACvB,EAAE,KAAK,KAAK;CAEZ,EAAE,KAAK,MAAS,QAAQ,KAAM;CAE9B,EAAE,KAAM,QAAQ,KAAM;CAEtB,EAAE,KAAK,MAAS,QAAQ,KAAM;CAE9B,EAAE,KAAM,QAAQ,IAAK;CAErB,EAAE,MAAQ,OAAO,IAAK,MAAS,EAAE,KAAK;CAEtC,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CACV,EAAE,MAAM,EAAE;CAEV,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;CACtE,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE;AAC7G;AAEA,SAAgB,SAAS,GAAoB;CAC3C,OAAO,yEAAyE,KAAK,CAAC;AACxF;AAEA,SAAgB,aAAa,GAAoB;CAC/C,IAAI,EAAE,SAAS,GAAG,OAAO;CAEzB,IAAI,EAAE,QAAQ,MAAM,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC,YAAY;CACjE,IAAI,CAAC,cAAc,KAAK,CAAC,GAAG,OAAO;CACnC,MAAM,KAAK,SAAS,GAAG,EAAE;CAEzB,OAAO,KAAK,KAAK,MAAM,sBAAsB,KAAK,KAAK,KAAK,IAAI,IAAI;AACtE"}
@@ -1,6 +1,6 @@
1
1
  import { AiError, defineTool } from "@zaly/ai";
2
2
  import { Type } from "typebox";
3
- //#region src/tools/wakeup.ts
3
+ // src/tools/wakeup.ts
4
4
  const wakeupTool = defineTool({
5
5
  name: "wakeup",
6
6
  desc: "Schedule a one-shot wake-up of the agent loop. If `delayMs` elapses without anything else waking the loop (task completion, heartbeat, user message), you'll receive a system message with the optional `hint`. If something else wakes the loop first, the wakeup is cancelled — its hint still surfaces (with `status=\"cancelled\"`) so your reminder doesn't evaporate. Useful for polling external state or scheduling a return to a thought.",
@@ -23,7 +23,6 @@ const wakeupTool = defineTool({
23
23
  })} scheduled for ${new Date(Date.now() + args.delayMs).toISOString()}`;
24
24
  }
25
25
  });
26
- //#endregion
27
26
  export { wakeupTool };
28
27
 
29
- //# sourceMappingURL=wakeup-BYqTnfSp.mjs.map
28
+ //# sourceMappingURL=wakeup-CFr1nu_L.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"wakeup-BYqTnfSp.mjs","names":[],"sources":["../src/tools/wakeup.ts"],"sourcesContent":["import type { Agent } from \"../agent.ts\"\n\nimport { defineTool, AiError } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\n\n/**\n * Schedule a future wake-up of the agent loop.\n *\n * Behavior:\n * - If `delayMs` elapses without anything else waking the loop, the\n * agent receives a system message containing the wakeup id and the\n * optional `hint`. The loop ticks; the model decides what to do next.\n * - If task completion / heartbeat / user message wakes the loop first,\n * the wakeup is auto-cancelled. Any cancelled wakeups carry their\n * `hint`s forward as a system message tagged `status=\"cancelled\"` so\n * the model still sees what it scheduled — the timing context is just\n * explicit (\"I scheduled this; it didn't fire on its own\").\n *\n * Use cases:\n * - Watch an external (non-task) process: \"wake me in 60s to re-fetch\n * the build status URL.\"\n * - Poll-style fallback for long-running tasks the model cares about\n * finishing: \"wake me in 30s in case the build hasn't completed yet.\"\n * - \"Take a moment\" patterns: \"wake me in 5min to reflect on whether\n * this approach is right.\"\n *\n * No companion `wakeup_cancel` tool exists — by the time the model has\n * a turn to call cancel, the wakeup has either fired or been auto-\n * cancelled for it. The only useful state transition is \"fire,\" and that\n * the harness handles automatically.\n */\nexport interface WakeupContext {\n /** The agent that scheduled the wakeup — provided via ToolContext\n * augmentation so the tool can call `scheduleWakeup` without a\n * cross-package dep on `Agent`. */\n agent?: Agent\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const wakeupTool = defineTool({\n name: \"wakeup\",\n desc:\n \"Schedule a one-shot wake-up of the agent loop. If `delayMs` elapses \" +\n \"without anything else waking the loop (task completion, heartbeat, \" +\n \"user message), you'll receive a system message with the optional \" +\n \"`hint`. If something else wakes the loop first, the wakeup is \" +\n 'cancelled — its hint still surfaces (with `status=\"cancelled\"`) so ' +\n \"your reminder doesn't evaporate. Useful for polling external state \" +\n \"or scheduling a return to a thought.\",\n parallel: true,\n params: Type.Object({\n delayMs: Type.Integer({\n description: \"How long to wait before waking up, in milliseconds. Minimum 1.\",\n minimum: 1,\n }),\n hint: Type.Optional(\n Type.String({\n description:\n \"Short text the wakeup carries — what you wanted to remember \" +\n \"to do at this point. Surfaces in the system message whether \" +\n \"the wakeup fires on its own timer or gets cancelled by another \" +\n \"event. Keep it concise; treat it as a TODO line, not prose.\",\n })\n ),\n }),\n\n call(args, ctx) {\n if (!ctx.agent) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message: \"wakeup requires an Agent reference on the context (set up by the agent harness).\",\n })\n }\n const id = ctx.agent.scheduleWakeup({ delayMs: args.delayMs, hint: args.hint })\n const when = new Date(Date.now() + args.delayMs).toISOString()\n return `wakeup ${id} scheduled for ${when}`\n },\n})\n"],"mappings":";;;AAuCA,MAAa,aAAa,WAAW;CACnC,MAAM;CACN,MACE;CAOF,UAAU;CACV,QAAQ,KAAK,OAAO;EAClB,SAAS,KAAK,QAAQ;GACpB,aAAa;GACb,SAAS;EACX,CAAC;EACD,MAAM,KAAK,SACT,KAAK,OAAO,EACV,aACE,qPAIJ,CAAC,CACH;CACF,CAAC;CAED,KAAK,MAAM,KAAK;EACd,IAAI,CAAC,IAAI,OACP,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,SAAS;EACX,CAAC;EAIH,OAAO,UAFI,IAAI,MAAM,eAAe;GAAE,SAAS,KAAK;GAAS,MAAM,KAAK;EAAK,CAE3D,EAAE,iBADP,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,YACT;CAC1C;AACF,CAAC"}
1
+ {"version":3,"file":"wakeup-CFr1nu_L.mjs","names":[],"sources":["../src/tools/wakeup.ts"],"sourcesContent":["import type { Agent } from \"../agent.ts\"\n\nimport { defineTool, AiError } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\n\n/**\n * Schedule a future wake-up of the agent loop.\n *\n * Behavior:\n * - If `delayMs` elapses without anything else waking the loop, the\n * agent receives a system message containing the wakeup id and the\n * optional `hint`. The loop ticks; the model decides what to do next.\n * - If task completion / heartbeat / user message wakes the loop first,\n * the wakeup is auto-cancelled. Any cancelled wakeups carry their\n * `hint`s forward as a system message tagged `status=\"cancelled\"` so\n * the model still sees what it scheduled — the timing context is just\n * explicit (\"I scheduled this; it didn't fire on its own\").\n *\n * Use cases:\n * - Watch an external (non-task) process: \"wake me in 60s to re-fetch\n * the build status URL.\"\n * - Poll-style fallback for long-running tasks the model cares about\n * finishing: \"wake me in 30s in case the build hasn't completed yet.\"\n * - \"Take a moment\" patterns: \"wake me in 5min to reflect on whether\n * this approach is right.\"\n *\n * No companion `wakeup_cancel` tool exists — by the time the model has\n * a turn to call cancel, the wakeup has either fired or been auto-\n * cancelled for it. The only useful state transition is \"fire,\" and that\n * the harness handles automatically.\n */\nexport interface WakeupContext {\n /** The agent that scheduled the wakeup — provided via ToolContext\n * augmentation so the tool can call `scheduleWakeup` without a\n * cross-package dep on `Agent`. */\n agent?: Agent\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const wakeupTool = defineTool({\n name: \"wakeup\",\n desc:\n \"Schedule a one-shot wake-up of the agent loop. If `delayMs` elapses \" +\n \"without anything else waking the loop (task completion, heartbeat, \" +\n \"user message), you'll receive a system message with the optional \" +\n \"`hint`. If something else wakes the loop first, the wakeup is \" +\n 'cancelled — its hint still surfaces (with `status=\"cancelled\"`) so ' +\n \"your reminder doesn't evaporate. Useful for polling external state \" +\n \"or scheduling a return to a thought.\",\n parallel: true,\n params: Type.Object({\n delayMs: Type.Integer({\n description: \"How long to wait before waking up, in milliseconds. Minimum 1.\",\n minimum: 1,\n }),\n hint: Type.Optional(\n Type.String({\n description:\n \"Short text the wakeup carries — what you wanted to remember \" +\n \"to do at this point. Surfaces in the system message whether \" +\n \"the wakeup fires on its own timer or gets cancelled by another \" +\n \"event. Keep it concise; treat it as a TODO line, not prose.\",\n })\n ),\n }),\n\n call(args, ctx) {\n if (!ctx.agent) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message: \"wakeup requires an Agent reference on the context (set up by the agent harness).\",\n })\n }\n const id = ctx.agent.scheduleWakeup({ delayMs: args.delayMs, hint: args.hint })\n const when = new Date(Date.now() + args.delayMs).toISOString()\n return `wakeup ${id} scheduled for ${when}`\n },\n})\n"],"mappings":";;;AAuCA,MAAa,aAAa,WAAW;CACnC,MAAM;CACN,MACE;CAOF,UAAU;CACV,QAAQ,KAAK,OAAO;EAClB,SAAS,KAAK,QAAQ;GACpB,aAAa;GACb,SAAS;EACX,CAAC;EACD,MAAM,KAAK,SACT,KAAK,OAAO,EACV,aACE,qPAIJ,CAAC,CACH;CACF,CAAC;CAED,KAAK,MAAM,KAAK;EACd,IAAI,CAAC,IAAI,OACP,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,SAAS;EACX,CAAC;EAIH,OAAO,UAFI,IAAI,MAAM,eAAe;GAAE,SAAS,KAAK;GAAS,MAAM,KAAK;EAAK,CAE3D,EAAE,iBADP,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,YACT;CAC1C;AACF,CAAC"}
@@ -4,7 +4,7 @@ import { AiError, defineTool } from "@zaly/ai";
4
4
  import { mkdir, stat, writeFile } from "node:fs/promises";
5
5
  import { Type } from "typebox";
6
6
  import { detectEol, normalizeEol } from "@zaly/shared/text";
7
- //#region src/tools/write.ts
7
+ // src/tools/write.ts
8
8
  /**
9
9
  * Write a file to disk. Overwrites if the file exists; creates parent
10
10
  * directories as needed.
@@ -75,7 +75,6 @@ function countLines(content) {
75
75
  if (content.endsWith("\n")) n--;
76
76
  return n;
77
77
  }
78
- //#endregion
79
78
  export { writeTool };
80
79
 
81
- //# sourceMappingURL=write-DwGiDNeG.mjs.map
80
+ //# sourceMappingURL=write-BRQFfWAn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-BRQFfWAn.mjs","names":[],"sources":["../src/tools/write.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"./read.ts\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath, safeReadFile } from \"@zaly/shared\"\nimport { detectEol, normalizeEol } from \"@zaly/shared/text\"\nimport { mkdir, stat, writeFile } from \"node:fs/promises\"\nimport { dirname } from \"pathe\"\nimport { Type } from \"typebox\"\n\nexport type WriteTool = typeof writeTool\n\nexport type WriteToolMeta = FileMeta & {\n /** File content before the write. Undefined for new files. The\n * post-write content is recoverable from the original call's\n * `params.content`, so we don't redundantly stash it on meta. */\n original?: string\n}\n\n/**\n * Write a file to disk. Overwrites if the file exists; creates parent\n * directories as needed.\n *\n * Content is written *verbatim* in everything but line endings. The\n * model always emits LF; this tool detects the existing file's\n * line-ending style (or the platform default for new files via\n * `detectEol`) and re-applies it on write so a CRLF file stays CRLF\n * after an LF-from-the-model overwrite. No trailing-newline injection,\n * no whitespace stripping.\n *\n * Returns a small acknowledgement (`{ ok, bytes, lines }`) rather than\n * echoing the content back — the model already has it. `bytes` reflects\n * the on-disk byte count, including any extra bytes from CRLF expansion.\n */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const writeTool = defineTool({\n name: \"write\",\n desc:\n \"Write content to a file. Overwrites existing files; \" +\n \"creates parent directories as needed. Returns the byte count and line \" +\n \"count of the written file.\",\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 content: Type.String({ description: \"File contents to write, verbatim.\" }),\n }),\n\n async preflight(args, ctx: ToolContext<WriteToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n await ctx.need?.(\"write\", path)\n },\n\n async call(\n args,\n ctx: ToolContext<WriteToolMeta>\n ): Promise<{ ok: true; path: string; bytes: number; lines: number }> {\n const path = normPath(ctx.cwd, args.path)\n\n let text: string\n let original: string | undefined\n try {\n original = await safeReadFile(path)\n const eol = detectEol({ path, text: original })\n text = normalizeEol(args.content, { eol })\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, text, \"utf8\")\n } catch (error) {\n throw new AiError({\n cause: error,\n code: \"WRITE_FAILED\",\n message: `failed to write ${path}: ${error instanceof Error ? error.message : String(error)}`,\n })\n }\n\n // Record the new mtime for unchanged-read detection and mark the\n // full-file replacement for context masking.\n const fstat = await stat(path)\n ctx.meta = { full: true, kind: \"write\", mtime: fstat.mtimeMs, original, path }\n\n const bytes = Buffer.byteLength(text, \"utf8\")\n const lines = countLines(text)\n return { bytes, lines, ok: true, path }\n },\n})\n\n/** Count lines the way an editor would — non-empty trailing line counts;\n * trailing newline doesn't add a phantom blank line. */\nfunction countLines(content: string): number {\n if (content === \"\") return 0\n let n = 1\n for (const c of content) if (c === \"\\n\") n++\n // A trailing newline means the count is one too high (no real line follows).\n if (content.endsWith(\"\\n\")) n--\n return n\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,YAAY,WAAW;CAClC,MAAM;CACN,MACE;CAIF,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,OAAO,EAAE,aAAa,8CAA8C,CAAC;EAChF,SAAS,KAAK,OAAO,EAAE,aAAa,oCAAoC,CAAC;CAC3E,CAAC;CAED,MAAM,UAAU,MAAM,KAAiC;EACrD,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EACxC,MAAM,IAAI,OAAO,SAAS,IAAI;CAChC;CAEA,MAAM,KACJ,MACA,KACmE;EACnE,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EAExC,IAAI;EACJ,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,aAAa,IAAI;GAClC,MAAM,MAAM,UAAU;IAAE;IAAM,MAAM;GAAS,CAAC;GAC9C,OAAO,aAAa,KAAK,SAAS,EAAE,IAAI,CAAC;GACzC,MAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;GAC9C,MAAM,UAAU,MAAM,MAAM,MAAM;EACpC,SAAS,OAAO;GACd,MAAM,IAAI,QAAQ;IAChB,OAAO;IACP,MAAM;IACN,SAAS,mBAAmB,KAAK,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GAC5F,CAAC;EACH;EAKA,IAAI,OAAO;GAAE,MAAM;GAAM,MAAM;GAAS,QAAO,MAD3B,KAAK,IAAI,EAAA,CACwB;GAAS;GAAU;EAAK;EAI7E,OAAO;GAAE,OAFK,OAAO,WAAW,MAAM,MAEzB;GAAG,OADF,WAAW,IACL;GAAG,IAAI;GAAM;EAAK;CACxC;AACF,CAAC;;;AAID,SAAS,WAAW,SAAyB;CAC3C,IAAI,YAAY,IAAI,OAAO;CAC3B,IAAI,IAAI;CACR,KAAK,MAAM,KAAK,SAAS,IAAI,MAAM,MAAM;CAEzC,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaly/agent",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Agent runtime, sessions, tools, permissions, compaction, and tasks for zaly.",
5
5
  "keywords": [
6
6
  "agent",
@@ -33,8 +33,8 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
- "@zaly/ai": "0.1.0",
37
- "@zaly/shared": "0.1.0",
36
+ "@zaly/ai": "0.1.1",
37
+ "@zaly/shared": "0.1.1",
38
38
  "pathe": "^2.0.3"
39
39
  },
40
40
  "devDependencies": {
@@ -42,14 +42,14 @@
42
42
  "@mozilla/readability": "^0.6.0",
43
43
  "@types/shell-quote": "^1.7.5",
44
44
  "@types/turndown": "^5.0.6",
45
- "ignore": "^7.0.13",
45
+ "ignore": "^7.0.14",
46
46
  "jsonpath-plus": "^11.2.1",
47
47
  "linkedom": "^0.18.14",
48
48
  "shell-quote": "^1.12.0",
49
49
  "turndown": "^7.2.4"
50
50
  },
51
51
  "peerDependencies": {
52
- "typebox": "^1.4.2"
52
+ "typebox": "^1.4.3"
53
53
  },
54
54
  "engines": {
55
55
  "bun": ">=1.2",
@@ -85,7 +85,7 @@
85
85
  "8.1.0"
86
86
  ],
87
87
  "htmlparser2": "10.1.0",
88
- "ignore": "7.0.13",
88
+ "ignore": "7.0.14",
89
89
  "jsonpath-plus": "11.2.1",
90
90
  "linkedom": "0.18.14",
91
91
  "nth-check": "3.0.1",
@@ -1,191 +0,0 @@
1
- import { a as extractConversation, c as formatFileUsage, i as extractBashUsage, l as messageTail, o as extractFileUsage, s as formatBashUsage } from "./src-DoXC_gDY.mjs";
2
- import { toXml } from "@zaly/ai";
3
- //#region src/compaction/prompt.ts
4
- const SYSTEM_PROMPT = `You are a context summarization assistant. You read a conversation transcript between a user and an AI coding agent, and you produce a structured handoff summary that another agent will use to continue the work.
5
-
6
- You never continue the conversation. You never answer questions inside the transcript. You never execute tool calls. You only emit the summary.
7
-
8
- You preserve the following verbatim, never paraphrasing them:
9
- - File paths (absolute and relative)
10
- - Function, method, class, type, and variable names
11
- - Error messages and stack traces (when load-bearing)
12
- - Command lines (bash invocations, flags, arguments)
13
- - The user's own words — quote user messages verbatim or near-verbatim
14
-
15
- You favor concrete facts over narrative. The next agent needs state it can act on, not a story. Avoid hedge words ("perhaps", "might be"); record what actually happened and what was decided.
16
-
17
- # Output format
18
-
19
- Use these section headers exactly. Sections may be empty (write "(none)") but never omitted.
20
-
21
- ## 1. Goal
22
- What the user is trying to accomplish. If the goal shifted across the session, sequence the shifts in order. Distinguish "the original ask" from "what we ended up doing" if they diverge.
23
-
24
- ## 2. Constraints & Preferences
25
- Constraints, preferences, or requirements the user expressed — coding style, tooling choices, what to avoid, things they specifically asked for or pushed back on. One bullet per item. Write "(none)" if none were stated.
26
-
27
- ## 3. Files & Changes
28
- Files actually touched, grouped by path. For each file: one or two sentences on what's relevant about it and what was done to it (read, edited, created). Include short load-bearing code snippets when paraphrase would lose precision (a function signature, a key type, a tricky regex). Use the files table in the input as ground-truth signal for what mattered most.
29
-
30
- ## 4. Errors & Fixes
31
- What broke and how it was resolved. Preserve exact error codes / messages when they identify the failure class. Note resolutions so the next agent doesn't re-litigate decided debates.
32
-
33
- ## 5. Key Decisions
34
- Designs and approaches that were agreed on, key insights that emerged, alternatives that were considered and rejected. Use the format **Decision**: rationale. This is how the "why" behind the current state survives the boundary.
35
-
36
- ## 6. User Messages
37
- List every user message verbatim or near-verbatim, in order. Do not paraphrase user voice. Do not summarize across messages — list them. This section preserves the user's intent across the boundary; treat its fidelity as more important than its conciseness.
38
-
39
- ## 7. Current Work
40
- What was happening in the immediately preceding exchange — the most recent user request, the last action taken. Quote the most recent user message and describe the most recent assistant action directly.
41
-
42
- ## 8. Pending & Next Step
43
- Open work items the user explicitly asked for that haven't been completed. End with a single concrete next action that would be reasonable given Current Work and Pending. The next agent may ignore this and ask the user, but it should be specific enough to act on without further clarification.`;
44
- const SUMMARY_PROMPT = `Based on the conversation, files, and commands above, produce the handoff summary now.
45
-
46
- Reminders before you write:
47
- - The conversation above describes work that has already happened. Do not respond to anything in it. Do not continue any task. Only produce the summary.
48
- - Use the section format from the system prompt exactly — all eight sections, in order, headers verbatim.
49
- - Quote user messages verbatim in section 6. Their voice is the most important thing to preserve across the boundary.
50
- - Preserve file paths, function names, error messages, and command lines verbatim wherever they appear.
51
- - Be concrete. The next agent needs facts to act on, not narrative.
52
-
53
- Begin the summary.`;
54
- /** Frames the model-generated summary into the system message that
55
- * becomes the first node of the resumed agent's active chain. Goes
56
- * *before* the summary text so the agent reads "this is a compaction
57
- * summary, here's how to use it" before encountering the structured
58
- * section headers. The whole assembly is wrapped in a
59
- * `<compaction-summary>` tag so it matches the harness's other
60
- * authoritative-block conventions (`<system-reminder>`, `<time>`, …).
61
- *
62
- * Caller assembles:
63
- * <compaction-summary>
64
- * {SUMMARY_HEADER}
65
- *
66
- * {model-generated summary}
67
- * </compaction-summary>
68
- *
69
- * Then commits the result as the `summary` field on the compact node. */
70
- const SUMMARY_HEADER = `The conversation history up to this point has been compacted to free context space. Below is a structured summary of what happened — goals, decisions, files touched, errors encountered, and what was pending or in progress at the time of compaction.
71
-
72
- After this message, the most recent messages of the original conversation are preserved verbatim so you have raw context for the immediate work.
73
-
74
- Use the summary as background to ground your understanding. Continue from where the conversation left off — do not re-introduce yourself, restate the plan, or re-do completed work. If something the user asks for relates to context that's not in the summary or the recent messages, ask rather than guess.`;
75
- //#endregion
76
- //#region src/compaction/compactions.ts
77
- const defaults = {
78
- bash: {
79
- limit: 50,
80
- minCount: 2,
81
- minScore: .5,
82
- sort: "score"
83
- },
84
- enabled: true,
85
- files: {
86
- limit: 50,
87
- minCount: 1,
88
- minScore: .5,
89
- sort: "score"
90
- },
91
- keepTokens: 2e4,
92
- maxToolResultLen: 2e3,
93
- reasoning: "low",
94
- summaryTokens: 1e4,
95
- threshold: .95
96
- };
97
- var Compaction = class {
98
- #opts;
99
- #agent;
100
- constructor(agent, opts = {}) {
101
- this.#agent = agent;
102
- this.#opts = {
103
- ...defaults,
104
- ...opts
105
- };
106
- }
107
- async compact(pressure) {
108
- const { session } = this.#agent;
109
- const masker = await this.#agent.ctx.masker();
110
- const messages = masker ? await masker.mask(session.messages, pressure) : session.messages;
111
- const now = performance.now();
112
- const tail = messageTail(messages, { keepTokens: this.#opts.keepTokens });
113
- const older = tail.length > 0 ? messages.slice(0, -tail.length) : messages;
114
- if (older.length === 0) return;
115
- const conversation = extractConversation(older, { maxToolResultLen: this.#opts.maxToolResultLen });
116
- const bashUsage = formatBashUsage(extractBashUsage(messages, this.#opts.bash));
117
- const fileUsage = formatFileUsage(extractFileUsage(messages, this.#opts.files));
118
- const request = {
119
- content: [
120
- {
121
- text: conversation,
122
- type: "text"
123
- },
124
- {
125
- text: fileUsage,
126
- type: "text"
127
- },
128
- {
129
- text: bashUsage,
130
- type: "text"
131
- },
132
- {
133
- text: SUMMARY_PROMPT,
134
- type: "text"
135
- }
136
- ],
137
- role: "user"
138
- };
139
- const summary = await this.#summarize(request);
140
- const summaryMessage = {
141
- content: [
142
- {
143
- text: SUMMARY_HEADER,
144
- type: "text"
145
- },
146
- {
147
- text: toXml(summary, "compaction-summary", { indent: false }),
148
- type: "text"
149
- },
150
- {
151
- text: fileUsage,
152
- type: "text"
153
- },
154
- {
155
- text: bashUsage,
156
- type: "text"
157
- }
158
- ],
159
- meta: { kind: "compaction-summary" },
160
- role: "system"
161
- };
162
- await session.compact({
163
- durationMs: Math.round(performance.now() - now),
164
- preTokens: this.#agent.contextSize,
165
- summary: summaryMessage,
166
- tail: tail.length,
167
- trigger: this.#opts.trigger
168
- });
169
- masker?.reset();
170
- }
171
- async #summarize(message) {
172
- const model = this.#agent.model;
173
- if (!model) throw new Error("model must be loaded to compact");
174
- const m = await model.stream({
175
- messages: [message],
176
- prompt: [SYSTEM_PROMPT],
177
- sessionId: this.#agent.session.id
178
- }, {
179
- caching: false,
180
- maxTokens: this.#opts.summaryTokens,
181
- reasoning: this.#opts.reasoning ? { effort: this.#opts.reasoning } : void 0,
182
- signal: this.#opts.signal
183
- });
184
- if (typeof m.content === "string") return m.content;
185
- return m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
186
- }
187
- };
188
- //#endregion
189
- export { Compaction };
190
-
191
- //# sourceMappingURL=compactions-D9XlwQGO.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"compactions-D9XlwQGO.mjs","names":[],"sources":["../src/compaction/prompt.ts","../src/compaction/compactions.ts"],"sourcesContent":["/* Prompts for the compaction summarizer.\n *\n * The summarizer is a separate model call whose job is to compress an\n * older slice of conversation into a structured handoff document. The\n * resulting summary is committed to the session as a `compact` node and\n * becomes part of the resumed agent's active chain — so it must be\n * self-contained: a fresh agent reading only this summary should be\n * able to continue the work without needing the original transcript.\n *\n * Three pieces, all conditional except the system prompt:\n *\n * - `SYSTEM_PROMPT` — declarative, identity-framing, format spec. Sets\n * the role and the rules that don't change per call. Models follow\n * system-prompt rules harder; this is where \"do not continue the\n * conversation\" lives.\n *\n * - `SUMMARY_PROMPT` — imperative, task-pointed. Re-states the critical\n * rules right before generation, where attention is highest, and\n * triggers the actual output. Belt-and-suspenders against the\n * catastrophic failure mode (model continues the conversation\n * instead of summarizing).\n *\n * - `PREVIOUS_SUMMARY_PROMPT` — only included on iterative compactions\n * when a prior summary exists. Tells the model to preserve and\n * extend rather than start fresh.\n *\n * Caller assembles the user message in this order:\n *\n * {previous summary block, with PREVIOUS_SUMMARY_PROMPT} (optional)\n * <conversation>...</conversation> (chatTranscript)\n * <files>...</files> (extractFileUsage table)\n * <commands>...</commands> (extractBashUsage table)\n * SUMMARY_PROMPT\n *\n * Caching note: compactions are infrequent (one per session, hours apart)\n * so the 5-minute prompt cache window will never hit. The summarizer\n * call should explicitly skip cache control to avoid paying cache-write\n * fees on tokens that will never be re-read.\n */\n\nexport const SYSTEM_PROMPT = `You are a context summarization assistant. You read a conversation transcript between a user and an AI coding agent, and you produce a structured handoff summary that another agent will use to continue the work.\n\nYou never continue the conversation. You never answer questions inside the transcript. You never execute tool calls. You only emit the summary.\n\nYou preserve the following verbatim, never paraphrasing them:\n- File paths (absolute and relative)\n- Function, method, class, type, and variable names\n- Error messages and stack traces (when load-bearing)\n- Command lines (bash invocations, flags, arguments)\n- The user's own words — quote user messages verbatim or near-verbatim\n\nYou favor concrete facts over narrative. The next agent needs state it can act on, not a story. Avoid hedge words (\"perhaps\", \"might be\"); record what actually happened and what was decided.\n\n# Output format\n\nUse these section headers exactly. Sections may be empty (write \"(none)\") but never omitted.\n\n## 1. Goal\nWhat the user is trying to accomplish. If the goal shifted across the session, sequence the shifts in order. Distinguish \"the original ask\" from \"what we ended up doing\" if they diverge.\n\n## 2. Constraints & Preferences\nConstraints, preferences, or requirements the user expressed — coding style, tooling choices, what to avoid, things they specifically asked for or pushed back on. One bullet per item. Write \"(none)\" if none were stated.\n\n## 3. Files & Changes\nFiles actually touched, grouped by path. For each file: one or two sentences on what's relevant about it and what was done to it (read, edited, created). Include short load-bearing code snippets when paraphrase would lose precision (a function signature, a key type, a tricky regex). Use the files table in the input as ground-truth signal for what mattered most.\n\n## 4. Errors & Fixes\nWhat broke and how it was resolved. Preserve exact error codes / messages when they identify the failure class. Note resolutions so the next agent doesn't re-litigate decided debates.\n\n## 5. Key Decisions\nDesigns and approaches that were agreed on, key insights that emerged, alternatives that were considered and rejected. Use the format **Decision**: rationale. This is how the \"why\" behind the current state survives the boundary.\n\n## 6. User Messages\nList every user message verbatim or near-verbatim, in order. Do not paraphrase user voice. Do not summarize across messages — list them. This section preserves the user's intent across the boundary; treat its fidelity as more important than its conciseness.\n\n## 7. Current Work\nWhat was happening in the immediately preceding exchange — the most recent user request, the last action taken. Quote the most recent user message and describe the most recent assistant action directly.\n\n## 8. Pending & Next Step\nOpen work items the user explicitly asked for that haven't been completed. End with a single concrete next action that would be reasonable given Current Work and Pending. The next agent may ignore this and ask the user, but it should be specific enough to act on without further clarification.`\n\nexport const SUMMARY_PROMPT = `Based on the conversation, files, and commands above, produce the handoff summary now.\n\nReminders before you write:\n- The conversation above describes work that has already happened. Do not respond to anything in it. Do not continue any task. Only produce the summary.\n- Use the section format from the system prompt exactly — all eight sections, in order, headers verbatim.\n- Quote user messages verbatim in section 6. Their voice is the most important thing to preserve across the boundary.\n- Preserve file paths, function names, error messages, and command lines verbatim wherever they appear.\n- Be concrete. The next agent needs facts to act on, not narrative.\n\nBegin the summary.`\n\nexport const PREVIOUS_SUMMARY_PROMPT = `A prior summary exists from an earlier compaction. Preserve all information from it and extend it with what's new in the conversation above:\n- Carry forward Goal, Constraints & Preferences, Key Decisions, and User Messages — do not drop entries.\n- Move items from Pending into Current Work or out of the list when completed.\n- Update Current Work and the next-step recommendation based on the most recent activity.\n- If something previously listed is now obsolete or contradicted, drop it and note the change in Key Decisions.`\n\n/** Frames the model-generated summary into the system message that\n * becomes the first node of the resumed agent's active chain. Goes\n * *before* the summary text so the agent reads \"this is a compaction\n * summary, here's how to use it\" before encountering the structured\n * section headers. The whole assembly is wrapped in a\n * `<compaction-summary>` tag so it matches the harness's other\n * authoritative-block conventions (`<system-reminder>`, `<time>`, …).\n *\n * Caller assembles:\n * <compaction-summary>\n * {SUMMARY_HEADER}\n *\n * {model-generated summary}\n * </compaction-summary>\n *\n * Then commits the result as the `summary` field on the compact node. */\nexport const SUMMARY_HEADER = `The conversation history up to this point has been compacted to free context space. Below is a structured summary of what happened — goals, decisions, files touched, errors encountered, and what was pending or in progress at the time of compaction.\n\nAfter this message, the most recent messages of the original conversation are preserved verbatim so you have raw context for the immediate work.\n\nUse the summary as background to ground your understanding. Continue from where the conversation left off — do not re-introduce yourself, restate the plan, or re-do completed work. If something the user asks for relates to context that's not in the summary or the recent messages, ask rather than guess.`\n","import type { Message, ReasoningEffort } from \"@zaly/ai\"\nimport type { Agent } from \"../agent.ts\"\nimport type { ContextPressure } from \"../types.ts\"\nimport type { ToolStatOptions } from \"./utils.ts\"\n\nimport { toXml } from \"@zaly/ai\"\nimport { SUMMARY_HEADER, SUMMARY_PROMPT, SYSTEM_PROMPT } from \"./prompt.ts\"\nimport {\n extractBashUsage,\n extractConversation,\n extractFileUsage,\n formatBashUsage,\n formatFileUsage,\n messageTail,\n} from \"./utils.ts\"\n\nexport type CompactionOptions = {\n enabled: boolean\n threshold?: number\n /** Existing messages up to this many tokens will be preserved in the context */\n keepTokens: number\n maxToolResultLen: number\n summaryTokens: number\n bash: ToolStatOptions\n files: ToolStatOptions\n signal?: AbortSignal\n reasoning?: ReasoningEffort\n trigger?: \"manual\" | \"auto\"\n}\n\nconst defaults: CompactionOptions = {\n bash: { limit: 50, minCount: 2, minScore: 0.5, sort: \"score\" },\n enabled: true,\n files: { limit: 50, minCount: 1, minScore: 0.5, sort: \"score\" },\n keepTokens: 20_000,\n maxToolResultLen: 2000,\n reasoning: \"low\",\n summaryTokens: 10_000,\n threshold: 0.95,\n}\n\nexport class Compaction {\n #opts: CompactionOptions\n #agent: Agent\n\n constructor(agent: Agent, opts: Partial<CompactionOptions> = {}) {\n this.#agent = agent\n this.#opts = { ...defaults, ...opts }\n }\n\n async compact(pressure: ContextPressure): Promise<void> {\n const { session } = this.#agent\n\n const masker = await this.#agent.ctx.masker()\n\n const messages = masker ? await masker.mask(session.messages, pressure) : session.messages\n\n const now = performance.now()\n\n const tail = messageTail(messages, { keepTokens: this.#opts.keepTokens })\n const older = tail.length > 0 ? messages.slice(0, -tail.length) : messages\n\n if (older.length === 0) return\n\n const conversation = extractConversation(\n // Conversation summary is based on the older messages, not the tail — the tail is what we keep\n older,\n { maxToolResultLen: this.#opts.maxToolResultLen }\n )\n\n const bashUsage = formatBashUsage(extractBashUsage(messages, this.#opts.bash))\n const fileUsage = formatFileUsage(extractFileUsage(messages, this.#opts.files))\n\n const request: Message<\"user\"> = {\n content: [\n { text: conversation, type: \"text\" },\n { text: fileUsage, type: \"text\" },\n { text: bashUsage, type: \"text\" },\n { text: SUMMARY_PROMPT, type: \"text\" },\n ],\n role: \"user\",\n }\n\n const summary = await this.#summarize(request)\n // The bash + file usage tables are deterministic, ground-truth signals\n // — pricier to ask the model to reproduce than to copy verbatim. Carry\n // them through to the resumed agent so it has the same working-set\n // visibility the summarizer had, without paying for output tokens.\n const summaryMessage: Message<\"system\"> = {\n content: [\n { text: SUMMARY_HEADER, type: \"text\" },\n { text: toXml(summary, \"compaction-summary\", { indent: false }), type: \"text\" },\n { text: fileUsage, type: \"text\" },\n { text: bashUsage, type: \"text\" },\n ],\n meta: { kind: \"compaction-summary\" },\n role: \"system\",\n }\n await session.compact({\n durationMs: Math.round(performance.now() - now),\n preTokens: this.#agent.contextSize,\n summary: summaryMessage,\n tail: tail.length,\n trigger: this.#opts.trigger,\n })\n masker?.reset()\n }\n\n async #summarize(message: Message<\"user\">): Promise<string> {\n const model = this.#agent.model\n if (!model) throw new Error(\"model must be loaded to compact\")\n const m = await model.stream(\n {\n messages: [message],\n prompt: [SYSTEM_PROMPT],\n sessionId: this.#agent.session.id,\n },\n {\n caching: false,\n maxTokens: this.#opts.summaryTokens,\n reasoning: this.#opts.reasoning ? { effort: this.#opts.reasoning } : undefined,\n signal: this.#opts.signal,\n }\n )\n if (typeof m.content === \"string\") return m.content\n const parts = m.content.filter((p) => p.type === \"text\").map((p) => p.text)\n return parts.join(\"\\n\")\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"edit-D4yaBK1K.mjs","names":[],"sources":["../src/tools/edit.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"../utils/file.ts\"\n\nimport { AiError, defineTool } from \"@zaly/ai\"\nimport { normPath } from \"@zaly/shared\"\nimport { detectEol, normalizeEol } from \"@zaly/shared/text\"\nimport { readFile, stat, writeFile } from \"node:fs/promises\"\nimport { Type } from \"typebox\"\n\nexport type EditTool = typeof editTool\nexport type EditToolMeta = FileMeta & {\n /** File content before any edit was applied. */\n original: string\n /** File content after all edits were applied — exactly what's now on disk. */\n content: string\n}\n\n/**\n * Apply one or more exact-text replacements to a file.\n *\n * Semantics:\n * - All `oldText` matches are resolved against the *original* file\n * content. Edits are not applied sequentially — so edit #1's\n * `newText` cannot accidentally produce a match for edit #2's\n * `oldText`.\n * - Each `oldText` must occur exactly once in the file (uniqueness\n * forces the model to disambiguate via context rather than guessing).\n * - Edits must not overlap. Nearby changes should be combined into a\n * single edit covering the surrounding block.\n * - The file is rewritten atomically: either every edit applies\n * cleanly or none do.\n *\n * Use `read` first to inspect the file. Keep `oldText` minimal but\n * uniquely identifying. For full-file rewrites, use `write` instead.\n */\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const editTool = defineTool({\n name: \"edit\",\n desc:\n \"Apply one or more exact-text replacements to a file. All matches are resolved \" +\n \"against the original content (not sequentially), each `oldText` must be unique, \" +\n \"and edits must not overlap. Atomic: all-or-nothing.\",\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 edits: Type.Array(\n // oxlint-disable-next-line sort-keys -- semantic order: oldText then newText\n Type.Object({\n oldText: Type.String({ description: \"Exact text to find. Must occur once.\" }),\n newText: Type.String({ description: \"Replacement text.\" }),\n }),\n { description: \"Edits to apply atomically.\", minItems: 1 }\n ),\n }),\n\n async preflight(args, ctx: ToolContext<EditToolMeta>) {\n const path = normPath(ctx.cwd, args.path)\n // Edits ride the `write` scope — they're mutations.\n await ctx.need?.(\"write\", path)\n },\n\n async call(\n args,\n ctx: ToolContext<EditToolMeta>\n ): Promise<{ ok: true; path: string; bytes: number; lines: number; edits: number }> {\n const path = normPath(ctx.cwd, args.path)\n\n const original = await readFile(path, \"utf8\").catch((error: unknown) => {\n throw new AiError({\n cause: error,\n code: \"FILE_NOT_FOUND\",\n message: `cannot edit ${path}: file not found`,\n })\n })\n\n const updated = applyEdits(original, args.edits, path)\n await writeFile(path, updated, \"utf8\")\n const fstat = await stat(path)\n ctx.meta = { content: updated, kind: \"edit\", mtime: fstat.mtimeMs, original, path }\n\n const bytes = Buffer.byteLength(updated, \"utf8\")\n const lines = countLines(updated)\n return { bytes, edits: args.edits.length, lines, ok: true, path }\n },\n})\n\ninterface EditSpec {\n oldText: string\n newText: string\n}\n\n/** Resolve all edit positions against `original`, validate uniqueness +\n * non-overlap, then splice the replacements in.\n *\n * Line-ending fidelity: the model always emits LF, but the file may be\n * CRLF. We detect the file's eol once, then normalize each `oldText` /\n * `newText` to that eol before matching/splicing. The result has the\n * same line-ending style as the original file. */\nfunction applyEdits(original: string, edits: readonly EditSpec[], path: string): string {\n const eol = detectEol({ path, text: original })\n // 1. Locate every oldText. Reject not-found and non-unique up-front so\n // the model gets a precise error per failing edit rather than a\n // \"patch failed at line N\" mystery.\n const matches: { start: number; end: number; newText: string; spec: EditSpec }[] = []\n for (const [i, edit] of edits.entries()) {\n const oldText = normalizeEol(edit.oldText, { eol })\n const newText = normalizeEol(edit.newText, { eol })\n if (oldText === \"\") {\n throw new AiError({\n code: \"EMPTY_OLD_TEXT\",\n data: { editIndex: i },\n message: `edit #${i}: oldText is empty (use \\`write\\` to create or fully replace a file)`,\n })\n }\n const first = original.indexOf(oldText)\n if (first === -1) {\n throw new AiError({\n code: \"NOT_FOUND\",\n data: { editIndex: i, path, snippet: preview(oldText) },\n message: `edit #${i}: oldText not found in ${path}`,\n })\n }\n const next = original.indexOf(oldText, first + 1)\n if (next !== -1) {\n throw new AiError({\n code: \"NOT_UNIQUE\",\n data: { editIndex: i, occurrences: countOccurrences(original, oldText), path },\n message:\n `edit #${i}: oldText matches multiple locations in ${path}. ` +\n `Add surrounding context to make it unique.`,\n })\n }\n matches.push({\n end: first + oldText.length,\n newText,\n spec: edit,\n start: first,\n })\n }\n\n // 2. Sort by start; verify no overlap. Equal start positions are\n // impossible because oldText is unique.\n matches.sort((a, b) => a.start - b.start)\n for (let i = 1; i < matches.length; i++) {\n if (matches[i].start < matches[i - 1].end) {\n throw new AiError({\n code: \"OVERLAP\",\n data: { a: matches[i - 1], b: matches[i] },\n message:\n `edits overlap in ${path}. ` +\n `Combine adjacent changes into a single edit covering both.`,\n })\n }\n }\n\n // 3. Splice. Walk the original and emit untouched-prefix + replacement\n // chunks; cheaper than N independent string replacements.\n const out: string[] = []\n let cursor = 0\n for (const m of matches) {\n out.push(original.slice(cursor, m.start), m.newText)\n cursor = m.end\n }\n out.push(original.slice(cursor))\n return out.join(\"\")\n}\n\n/** Count lines the way an editor would — non-empty trailing line counts;\n * a trailing newline doesn't add a phantom blank line. */\nfunction countLines(content: string): number {\n if (content === \"\") return 0\n let n = 1\n for (const c of content) if (c === \"\\n\") n++\n if (content.endsWith(\"\\n\")) n--\n return n\n}\n\nfunction countOccurrences(haystack: string, needle: string): number {\n let n = 0\n let from = 0\n for (;;) {\n const i = haystack.indexOf(needle, from)\n if (i === -1) return n\n n++\n from = i + needle.length\n }\n}\n\nfunction preview(text: string): string {\n const trimmed = text.trim().replaceAll(/\\s+/g, \" \")\n return trimmed.length > 80 ? `${trimmed.slice(0, 79)}…` : trimmed\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,WAAW,WAAW;CACjC,MAAM;CACN,MACE;CAIF,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,OAAO,EAAE,aAAa,8CAA8C,CAAC;EAChF,OAAO,KAAK,MAEV,KAAK,OAAO;GACV,SAAS,KAAK,OAAO,EAAE,aAAa,uCAAuC,CAAC;GAC5E,SAAS,KAAK,OAAO,EAAE,aAAa,oBAAoB,CAAC;EAC3D,CAAC,GACD;GAAE,aAAa;GAA8B,UAAU;EAAE,CAC3D;CACF,CAAC;CAED,MAAM,UAAU,MAAM,KAAgC;EACpD,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EAExC,MAAM,IAAI,OAAO,SAAS,IAAI;CAChC;CAEA,MAAM,KACJ,MACA,KACkF;EAClF,MAAM,OAAO,SAAS,IAAI,KAAK,KAAK,IAAI;EAExC,MAAM,WAAW,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,OAAO,UAAmB;GACtE,MAAM,IAAI,QAAQ;IAChB,OAAO;IACP,MAAM;IACN,SAAS,eAAe,KAAK;GAC/B,CAAC;EACH,CAAC;EAED,MAAM,UAAU,WAAW,UAAU,KAAK,OAAO,IAAI;EACrD,MAAM,UAAU,MAAM,SAAS,MAAM;EAErC,IAAI,OAAO;GAAE,SAAS;GAAS,MAAM;GAAQ,QAAO,MADhC,KAAK,IAAI,EAAA,CAC6B;GAAS;GAAU;EAAK;EAElF,MAAM,QAAQ,OAAO,WAAW,SAAS,MAAM;EAC/C,MAAM,QAAQ,WAAW,OAAO;EAChC,OAAO;GAAE;GAAO,OAAO,KAAK,MAAM;GAAQ;GAAO,IAAI;GAAM;EAAK;CAClE;AACF,CAAC;;;;;;;;AAcD,SAAS,WAAW,UAAkB,OAA4B,MAAsB;CACtF,MAAM,MAAM,UAAU;EAAE;EAAM,MAAM;CAAS,CAAC;CAI9C,MAAM,UAA6E,CAAC;CACpF,KAAK,MAAM,CAAC,GAAG,SAAS,MAAM,QAAQ,GAAG;EACvC,MAAM,UAAU,aAAa,KAAK,SAAS,EAAE,IAAI,CAAC;EAClD,MAAM,UAAU,aAAa,KAAK,SAAS,EAAE,IAAI,CAAC;EAClD,IAAI,YAAY,IACd,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM,EAAE,WAAW,EAAE;GACrB,SAAS,SAAS,EAAE;EACtB,CAAC;EAEH,MAAM,QAAQ,SAAS,QAAQ,OAAO;EACtC,IAAI,UAAU,IACZ,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM;IAAE,WAAW;IAAG;IAAM,SAAS,QAAQ,OAAO;GAAE;GACtD,SAAS,SAAS,EAAE,yBAAyB;EAC/C,CAAC;EAGH,IADa,SAAS,QAAQ,SAAS,QAAQ,CACxC,MAAM,IACX,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM;IAAE,WAAW;IAAG,aAAa,iBAAiB,UAAU,OAAO;IAAG;GAAK;GAC7E,SACE,SAAS,EAAE,0CAA0C,KAAK;EAE9D,CAAC;EAEH,QAAQ,KAAK;GACX,KAAK,QAAQ,QAAQ;GACrB;GACA,MAAM;GACN,OAAO;EACT,CAAC;CACH;CAIA,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAClC,IAAI,QAAQ,EAAE,CAAC,QAAQ,QAAQ,IAAI,EAAE,CAAC,KACpC,MAAM,IAAI,QAAQ;EAChB,MAAM;EACN,MAAM;GAAE,GAAG,QAAQ,IAAI;GAAI,GAAG,QAAQ;EAAG;EACzC,SACE,oBAAoB,KAAK;CAE7B,CAAC;CAML,MAAM,MAAgB,CAAC;CACvB,IAAI,SAAS;CACb,KAAK,MAAM,KAAK,SAAS;EACvB,IAAI,KAAK,SAAS,MAAM,QAAQ,EAAE,KAAK,GAAG,EAAE,OAAO;EACnD,SAAS,EAAE;CACb;CACA,IAAI,KAAK,SAAS,MAAM,MAAM,CAAC;CAC/B,OAAO,IAAI,KAAK,EAAE;AACpB;;;AAIA,SAAS,WAAW,SAAyB;CAC3C,IAAI,YAAY,IAAI,OAAO;CAC3B,IAAI,IAAI;CACR,KAAK,MAAM,KAAK,SAAS,IAAI,MAAM,MAAM;CACzC,IAAI,QAAQ,SAAS,IAAI,GAAG;CAC5B,OAAO;AACT;AAEA,SAAS,iBAAiB,UAAkB,QAAwB;CAClE,IAAI,IAAI;CACR,IAAI,OAAO;CACX,SAAS;EACP,MAAM,IAAI,SAAS,QAAQ,QAAQ,IAAI;EACvC,IAAI,MAAM,IAAI,OAAO;EACrB;EACA,OAAO,IAAI,OAAO;CACpB;AACF;AAEA,SAAS,QAAQ,MAAsB;CACrC,MAAM,UAAU,KAAK,KAAK,CAAC,CAAC,WAAW,QAAQ,GAAG;CAClD,OAAO,QAAQ,SAAS,KAAK,GAAG,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AAC5D"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"read-B-hwcc2Q.mjs","names":[],"sources":["../src/utils/file.ts","../src/tools/read.ts"],"sourcesContent":["import type { Tool, ToolContext } from \"@zaly/ai\"\n\nimport { AiError, extractToolResults } from \"@zaly/ai\"\nimport { normPath, safeStat } from \"@zaly/shared\"\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 FileTool = Tool<{ path: string }, unknown, FileMeta>\n\nfunction isFileMeta(meta: unknown): meta is FileMeta {\n const m = meta as Partial<Record<string, unknown>> | undefined | null\n return (\n m !== undefined &&\n m !== null &&\n typeof m === \"object\" &&\n typeof m.path === \"string\" &&\n typeof m.mtime === \"number\" &&\n (m.kind === \"read\" || m.kind === \"write\" || m.kind === \"edit\")\n )\n}\n\nexport function assertFresh(path: string, ctx: ToolContext) {\n const err = checkFresh(path, ctx)\n if (err !== true) throw err\n}\n\nexport function isUnchanged(path: string, ctx: ToolContext) {\n return checkFresh(path, ctx, { full: true }) === true\n}\n\nexport function checkFresh(\n path: string,\n ctx: ToolContext,\n opts: { full?: boolean } = {}\n): AiError | true {\n path = normPath(ctx.cwd, path)\n const mtime = safeStat(path)?.mtimeMs\n if (mtime === undefined)\n return new AiError({ code: \"NOT_FOUND\", message: `${path}: file not found` })\n const messages = ctx.messages ?? []\n let ret: AiError = freshnessError(path, \"NOT_READ\")\n\n for (const { m, $p, p } of extractToolResults<FileMeta>(messages)) {\n const id = m.id\n if (!id || !isFileMeta(p.meta) || p.meta.path !== path || ctx.isMasked?.(id, $p)) continue\n if (opts.full && !p.meta.full) continue\n if (p.meta.mtime === mtime) return true // Fresh! The file's mtime matches what we saw at read time.\n ret = freshnessError(path, \"STALE\")\n }\n return ret\n}\n\n/** Build the canonical freshness error. `data.reason` distinguishes\n * never-read from changed-since-read for callers and renderers. */\nexport function freshnessError(path: string, reason: \"NOT_READ\" | \"STALE\"): AiError {\n return new AiError({\n code: \"FILE_NOT_FRESH\",\n data: { path, reason },\n message:\n reason === \"NOT_READ\"\n ? `${path}: read this file before mutating it.`\n : `${path}: file changed since last read. Re-read before mutating.`,\n })\n}\n","import type { Content, ToolContext } from \"@zaly/ai\"\nimport type { FileMeta } from \"../utils/file.ts\"\n\nimport { AiError, defineTool, 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\"\nimport { isUnchanged } from \"../utils/file.ts\"\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\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 if (isUnchanged(path, ctx)) {\n // Fresh! We've seen this file's current bytes, so we can skip\n // returning the content 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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"}