@zaly/agent 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-D3wVIibN.mjs → agent-Dp4LZDSo.mjs} +16 -16
- package/dist/agent-Dp4LZDSo.mjs.map +1 -0
- package/dist/{compactions-B8VdZJoU.mjs → compactions-CvQs64fS.mjs} +4 -3
- package/dist/{compactions-B8VdZJoU.mjs.map → compactions-CvQs64fS.mjs.map} +1 -1
- package/dist/index.d.mts +35 -19
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{masker-DRFztUwc.mjs → masker-ZoP381jN.mjs} +2 -2
- package/dist/{masker-DRFztUwc.mjs.map → masker-ZoP381jN.mjs.map} +1 -1
- package/dist/{read-CSL_8nAk.mjs → read-B-hwcc2Q.mjs} +7 -4
- package/dist/read-B-hwcc2Q.mjs.map +1 -0
- package/dist/{src-fK6DL2bg.mjs → src-C852Q6gy.mjs} +4 -4
- package/dist/{src-fK6DL2bg.mjs.map → src-C852Q6gy.mjs.map} +1 -1
- package/package.json +3 -3
- package/dist/agent-D3wVIibN.mjs.map +0 -1
- package/dist/read-CSL_8nAk.mjs.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"read-CSL_8nAk.mjs","names":[],"sources":["../src/utils/file.ts","../src/tools/read.ts"],"sourcesContent":["import type { 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\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) || 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 for (let i = start; i < end; i++) {\n let line = lines[i]\n if (line.length > MAX_LINE_LENGTH) {\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 const full = start === 0 && end === lines.length\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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"}
|