@tangle-network/agent-app 0.44.18 → 0.44.21

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 (44) hide show
  1. package/.claude/skills/eval-campaign/SKILL.md +58 -95
  2. package/.claude/skills/surface-evolution/SKILL.md +29 -23
  3. package/README.md +8 -7
  4. package/dist/assistant/index.d.ts +2 -2
  5. package/dist/assistant/index.js +3 -3
  6. package/dist/{attachment-validation-Zw8eslhN.d.ts → attachment-validation-Dvc_Livy.d.ts} +1 -1
  7. package/dist/chat-routes/index.d.ts +17 -10
  8. package/dist/chat-routes/index.js +89 -12
  9. package/dist/chat-routes/index.js.map +1 -1
  10. package/dist/chat-store/index.d.ts +2 -2
  11. package/dist/chat-store/index.js +1 -1
  12. package/dist/{chunk-JXIQSGOV.js → chunk-2ZSSOYXP.js} +2 -6
  13. package/dist/chunk-2ZSSOYXP.js.map +1 -0
  14. package/dist/{chunk-VM6VLJH6.js → chunk-BI6NKSO4.js} +2 -2
  15. package/dist/{chunk-DV2FA2PW.js → chunk-C3SRFZGL.js} +18 -9
  16. package/dist/chunk-C3SRFZGL.js.map +1 -0
  17. package/dist/{chunk-34M7AUWO.js → chunk-M3UFMQ7D.js} +1 -1
  18. package/dist/chunk-M3UFMQ7D.js.map +1 -0
  19. package/dist/{chunk-7SMPAAIT.js → chunk-PRKSYTMQ.js} +3 -3
  20. package/dist/eval-campaign/index.d.ts +6 -8
  21. package/dist/eval-campaign/index.js +9 -5
  22. package/dist/eval-campaign/index.js.map +1 -1
  23. package/dist/forms/index.d.ts +333 -0
  24. package/dist/forms/index.js +343 -0
  25. package/dist/forms/index.js.map +1 -0
  26. package/dist/{parts-S1OINlRP.d.ts → parts-fyPPdDdK.d.ts} +3 -4
  27. package/dist/profile/index.d.ts +5 -6
  28. package/dist/profile/index.js +5 -3
  29. package/dist/profile/index.js.map +1 -1
  30. package/dist/sandbox/index.d.ts +3 -7
  31. package/dist/sandbox/index.js +1 -1
  32. package/dist/skills/index.d.ts +4 -3
  33. package/dist/skills/index.js +1 -1
  34. package/dist/skills-placement/index.d.ts +0 -1
  35. package/dist/skills-placement/index.js +1 -1
  36. package/dist/teams-react/index.js +3 -3
  37. package/dist/web-react/index.d.ts +3 -3
  38. package/dist/web-react/index.js +3 -3
  39. package/package.json +29 -18
  40. package/dist/chunk-34M7AUWO.js.map +0 -1
  41. package/dist/chunk-DV2FA2PW.js.map +0 -1
  42. package/dist/chunk-JXIQSGOV.js.map +0 -1
  43. /package/dist/{chunk-VM6VLJH6.js.map → chunk-BI6NKSO4.js.map} +0 -0
  44. /package/dist/{chunk-7SMPAAIT.js.map → chunk-PRKSYTMQ.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/skills/index.ts"],"sourcesContent":["/**\n * Unified skill + corpus mounter for agent products.\n *\n * Every agent product hand-rolls the same two file-mount systems and then\n * drifts on the seams between them. (1) An ALWAYS-MOUNTED markdown corpus —\n * (`skills/<slug>/SKILL.md`, `doctrine` and `knowledge` markdown trees) — discovered\n * by a Vite `?raw` glob in the Worker bundle and by Node `fs` under the eval\n * CLI, then projected into `resources.files`. (2) A TIER-GATED installable\n * registry — a hand-authored array of `SkillEntry` whose free tier mounts at\n * the harness skill-discovery path and whose paid tier is installed on demand.\n * Both ride the same `resources.files` channel but use different provenance\n * (file-backed vs inline), different mount paths (relative corpus path vs\n * `~/.claude/skills/<id>/SKILL.md`), and different selection rules. This module\n * makes both DATA: a corpus loader that accepts a Vite glob-result map (or an\n * fs fallback), a registry adapter that tier-gates, and a single\n * `composeShellResources` that projects either onto the SDK file-mount shape.\n *\n * A THIRD surface lives alongside those two: adoptable `SkillEntry`s sourced\n * from `SKILL.md` frontmatter rather than hand-authored fields.\n * `parseSkillFrontmatter` is the ONE frontmatter parser (hand-rolled, no YAML\n * dependency — fail loud on a malformed block rather than silently mis-reading\n * a field); `skillEntryFromMarkdown`/`parseCorpusSkills` turn raw markdown into\n * `SkillEntry`s. From there a skill reaches the agent by one of two DELIVERY\n * MODES: `inline` renders the skill body straight into the system prompt (every\n * harness can read it, at prompt-byte cost), or `mounted` projects it onto the\n * typed `resources.skills` channel (`AgentProfileResourceRef[]`) plus an index\n * section that just names the file, and lets the platform materializer place it\n * at the harness-native skill dir. `composeSkills` builds either shape;\n * `mergeComposedSkills` combines batches and throws (via\n * `assertSkillDeliveryDisjoint`) on a skill accidentally delivered both\n * ways. Picking WHICH harnesses can take `mounted` delivery is platform-bound\n * (see `@tangle-network/agent-app/skills-placement`) and deliberately kept out\n * of this substrate-free module.\n *\n * Substrate-free over storage, exact over the SDK boundary: the only inbound\n * seam is the glob-result map the consumer passes in (its call site keeps the\n * literal `import.meta.glob` Vite must static-analyze); the only outbound seam\n * is `@tangle-network/sandbox`'s `AgentProfileFileMount[]`/`AgentProfileResourceRef[]`,\n * the exact shapes `resources.files`/`resources.skills` consume. Node builtins\n * are resolved lazily via `process.getBuiltinModule` so a static `node:*`\n * import never reaches the Vite SSR bundle.\n */\n\nimport type { AgentProfileFileMount, AgentProfileResourceRef } from '@tangle-network/sandbox'\n\n/** Construct the inline arm of the SDK's `AgentProfileResourceRef`. Inlined here\n * so this leaf subpath stays type-only over `@tangle-network/sandbox` — it\n * carries no runtime dependency on the SDK, just its file-mount type contract. */\nfunction inlineResource(name: string, content: string): AgentProfileResourceRef {\n return { kind: 'inline', name, content }\n}\n\n/** A Vite eager `?raw` glob result: glob key -> raw file body. The consumer\n * produces this by calling `import.meta.glob('<lit>', { eager: true, query:\n * '?raw', import: 'default' })` at its own call site — the literal must stay\n * literal so Vite can static-analyze it; passing the result here keeps that\n * constraint at the edge and the loader substrate-free. */\nexport type GlobModules = Record<string, string>\n\n/** One markdown document discovered from the corpus. */\nexport interface CorpusEntry {\n /** Slug derived from the glob key (folder slug for `SKILL.md` layouts, or the\n * normalized relative path for flat `*.md` layouts). */\n id: string\n /** Glob/fs key the entry was loaded from, normalized to a stable relative\n * form (leading `./` and absolute prefixes stripped). */\n key: string\n /** Raw markdown body (including any frontmatter). */\n content: string\n}\n\n/** A hand-authored, tier-gated installable skill. Mirrors the per-product\n * registry entry (gtm/insurance `SkillEntry`); the runtime's certified `skill`\n * artifact kind is unrelated. `skillMd` is the inline body — file provenance\n * does not apply to the registry. */\nexport interface SkillEntry {\n id: string\n name: string\n description: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n /** Gate keyword. `composeShellResources`/`registrySkills` treat `free` as\n * always-mounted; everything else is install-on-demand. */\n tier: string\n skillMd: string\n}\n\n/** Harness skill-discovery path the Claude Code backend reads natively. The\n * registry mounts here; the corpus mounts at its relative path.\n *\n * @deprecated Hardcodes the claude-code path (`~/.claude/skills/<id>/SKILL.md`).\n * It is NOT a path other harnesses read — OpenCode discovers `.opencode/skills`,\n * not `~/.claude/skills` (the doc comment here previously claimed otherwise);\n * codex, kimi-code, and the rest each have their own dir or none at all. Use\n * {@link skillRefs} to put a skill on the typed `resources.skills` channel and\n * let the platform materializer place it correctly, or resolve the\n * harness-native dir directly via `@tangle-network/agent-app/skills-placement`.\n * Kept only for the pre-existing `registrySkills`/`userSkillMounts` callers;\n * do not add new call sites. */\nexport function skillMountPath(id: string): string {\n return `~/.claude/skills/${id}/SKILL.md`\n}\n\n/** Strip a glob/fs key down to a stable relative form: drop a leading `./`,\n * and for an absolute fs path keep only the tail from the last anchor segment\n * the pattern implies. We normalize on the trailing `<dir>/.../*.md` so the\n * Vite key (`./skills/x/SKILL.md`) and the fs key (`/abs/.../skills/x/SKILL.md`)\n * collapse to the same value. */\nfunction normalizeKey(key: string, anchor: string): string {\n const marker = `${anchor}/`\n const at = key.lastIndexOf(marker)\n if (at >= 0) return key.slice(at)\n return key.startsWith('./') ? key.slice(2) : key\n}\n\n/** Folder-slug for a `<anchor>/<slug>/SKILL.md` layout; falls back to the\n * normalized key (sans `<anchor>/` prefix, sans `.md`) for flat layouts. */\nfunction toCorpusId(normalizedKey: string, anchor: string): string {\n const nested = normalizedKey.match(new RegExp(`${anchor}/([^/]+)/SKILL\\\\.md$`))\n if (nested) return nested[1]!\n const flat = normalizedKey.match(new RegExp(`${anchor}/(.+)\\\\.md$`))\n if (flat) return flat[1]!\n return normalizedKey\n}\n\n/** Resolve Node builtins lazily. `process.getBuiltinModule` (Node 22+) is\n * absent in workerd, so this returns undefined there and the fs path is never\n * taken — Workers always reach the loader through the Vite glob map. A static\n * `import 'node:fs'` would break Vite SSR bundling in the consumer apps, so it\n * is deliberately avoided. */\nfunction nodeBuiltins():\n | { fs: typeof import('node:fs'); path: typeof import('node:path'); url: typeof import('node:url') }\n | undefined {\n const getBuiltin = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } })\n .process?.getBuiltinModule\n if (typeof getBuiltin !== 'function') return undefined\n return {\n fs: getBuiltin('node:fs') as typeof import('node:fs'),\n path: getBuiltin('node:path') as typeof import('node:path'),\n url: getBuiltin('node:url') as typeof import('node:url'),\n }\n}\n\n/** Options for {@link loadMarkdownCorpus}. */\nexport interface LoadCorpusOptions {\n /** The anchor folder name that appears in both glob keys and fs paths\n * (`skills`, `doctrine`, `knowledge`). Used to normalize keys + derive ids. */\n anchor: string\n /** Vite glob-result map. When present and non-empty it is authoritative and\n * the fs path is skipped. Omit it (or pass an empty map) only outside Vite. */\n globModules?: GlobModules\n /** Absolute or `import.meta.url`-relative base dir the fs fallback walks when\n * `globModules` is empty. Required for the fs path to run; without it the fs\n * fallback returns no entries (Workers never need it). */\n fsBaseDir?: string\n /** Walk strategy for the fs fallback. `nested` finds `<dir>/<slug>/SKILL.md`\n * one level deep; `flat` recurses for every `*.md`. Default: `flat`. */\n fsLayout?: 'nested' | 'flat'\n /** Drop an entry by its normalized key after load. Covers the per-product\n * skip lists (corpus index/log files, scaffold templates, allow-lists). */\n skip?: (normalizedKey: string) => boolean\n}\n\n/** Outcome of {@link loadMarkdownCorpus}: the entries plus which path produced\n * them, so a caller can fail loud when both are empty rather than silently\n * mounting nothing. */\nexport interface CorpusLoadResult {\n source: 'vite' | 'fs' | 'empty'\n entries: CorpusEntry[]\n}\n\nfunction fsWalkFlat(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n const walk = (dir: string) => {\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const full = path.join(dir, entry.name)\n if (entry.isDirectory()) walk(full)\n else if (entry.isFile() && entry.name.endsWith('.md')) out[full] = fs.readFileSync(full, 'utf8')\n }\n }\n walk(root)\n return out\n}\n\nfunction fsWalkNested(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n root: string,\n): GlobModules {\n const { fs, path } = builtins\n const out: GlobModules = {}\n if (!fs.existsSync(root)) return out\n let entries: import('node:fs').Dirent[]\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return out\n }\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n const skillFile = path.join(root, entry.name, 'SKILL.md')\n if (!fs.existsSync(skillFile)) continue\n out[skillFile] = fs.readFileSync(skillFile, 'utf8')\n }\n return out\n}\n\n/** Resolve `fsBaseDir` against `import.meta.url` when relative-looking, so a\n * consumer can pass a bare folder name (`'skills'`) and have it land beside\n * the calling module. Absolute paths pass through. */\nfunction resolveFsBase(\n builtins: NonNullable<ReturnType<typeof nodeBuiltins>>,\n fsBaseDir: string,\n importMetaUrl?: string,\n): string {\n const { path, url } = builtins\n if (path.isAbsolute(fsBaseDir)) return fsBaseDir\n const here = importMetaUrl\n ? path.dirname(url.fileURLToPath(importMetaUrl))\n : process.cwd()\n return path.join(here, fsBaseDir)\n}\n\n/**\n * Load a markdown corpus, preferring a Vite glob-result map and falling back to\n * a Node fs walk. Selection is by non-empty glob result — never an env flag.\n * Entries are normalized, optionally skip-filtered, and sorted by id for\n * determinism. The `import.meta.glob` literal stays at the CONSUMER call site\n * (passed in as `globModules`); this loader never constructs a glob.\n */\nexport function loadMarkdownCorpus(\n options: LoadCorpusOptions,\n importMetaUrl?: string,\n): CorpusLoadResult {\n const { anchor, globModules, fsBaseDir, fsLayout = 'flat', skip } = options\n\n let modules: GlobModules\n let source: CorpusLoadResult['source']\n if (globModules && Object.keys(globModules).length > 0) {\n modules = globModules\n source = 'vite'\n } else {\n const builtins = nodeBuiltins()\n if (builtins && fsBaseDir) {\n const root = resolveFsBase(builtins, fsBaseDir, importMetaUrl)\n modules = fsLayout === 'nested' ? fsWalkNested(builtins, root) : fsWalkFlat(builtins, root)\n source = Object.keys(modules).length > 0 ? 'fs' : 'empty'\n } else {\n modules = {}\n source = 'empty'\n }\n }\n\n const entries: CorpusEntry[] = []\n for (const [rawKey, content] of Object.entries(modules)) {\n if (typeof content !== 'string') continue\n const key = normalizeKey(rawKey, anchor)\n if (skip && skip(key)) continue\n entries.push({ id: toCorpusId(key, anchor), key, content })\n }\n entries.sort((a, b) => a.id.localeCompare(b.id))\n return { source, entries }\n}\n\n/** Project corpus entries onto SDK file mounts at a relative path under\n * `<anchor>/`. Always-mounted: the corpus is the agent's baseline knowledge. */\nexport function corpusSkills(corpus: CorpusEntry[], anchor: string): AgentProfileFileMount[] {\n return corpus\n .map(\n (entry) =>\n ({\n path: `${anchor}/${entry.id}.md`,\n resource: inlineResource(`${anchor}-${entry.id}`, entry.content),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Project the registry's free-tier (or `tier`-matched) entries onto SDK file\n * mounts at the harness skill-discovery path. Tier-gating is the registry's\n * only selection rule — paid skills are installed on demand, not at boot. */\nexport function registrySkills(\n registry: SkillEntry[],\n tier: string = 'free',\n): AgentProfileFileMount[] {\n return registry\n .filter((s) => s.tier === tier)\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: inlineResource(s.id, s.skillMd),\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/** Inputs to {@link composeShellResources}. Each channel is optional so a\n * product mounts only the systems it has — corpus-only, registry-only, or\n * both — without conflating them. */\nexport interface ComposeShellResourcesInput {\n /** Corpus mounts (always-mounted baseline). Pass the result of\n * {@link corpusSkills}, or a hand-built mount list. */\n skills?: AgentProfileFileMount[]\n /** Knowledge-corpus mounts (a second always-mounted corpus, e.g. a domain\n * knowledge pack distinct from the skills corpus). */\n knowledge?: AgentProfileFileMount[]\n /** Evolvable / learned-guidance mounts (single-file corpora). */\n evolvable?: AgentProfileFileMount[]\n /** Registry mounts (tier-gated). Pass the result of {@link registrySkills}. */\n registry?: AgentProfileFileMount[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n predicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/**\n * Compose every mount channel into one `resources.files`-ready array. Corpus\n * channels come first (baseline), the tier-gated registry last (so a registry\n * entry can override a corpus entry that mounts at the same path). The result\n * is exactly `AgentProfileFileMount[]` — assign it straight into\n * `profile.resources.files` with no cast.\n */\nexport function composeShellResources(input: ComposeShellResourcesInput): AgentProfileFileMount[] {\n const { skills = [], knowledge = [], evolvable = [], registry = [], predicate } = input\n const composed = [...skills, ...knowledge, ...evolvable, ...registry]\n return predicate ? composed.filter(predicate) : composed\n}\n\n// ─── Adoptable skills: one frontmatter parser, two delivery modes ─────────────\n\n/** Fields a `SKILL.md` frontmatter block may declare. All optional — absent\n * frontmatter (or an absent field within it) is legal; callers fill defaults\n * (see {@link skillEntryFromMarkdown}). */\nexport interface SkillFrontmatter {\n id?: string\n name?: string\n description?: string\n author?: { name: string; url?: string }\n source?: string\n category?: string\n tags?: string[]\n tier?: string\n}\n\n/** The result of {@link parseSkillFrontmatter}: the parsed fields, the body\n * with the frontmatter block stripped, and the original untouched text. */\nexport interface ParsedSkill {\n frontmatter: SkillFrontmatter\n body: string\n raw: string\n}\n\n/** Quoted values (`description: \"...\"`, as emitted by materialize's\n * `normalizeSkillMd`) are JSON strings — decode with `JSON.parse` so escapes\n * round-trip; anything else is a bare scalar, trimmed. */\nfunction parseFrontmatterScalar(value: string): string {\n const trimmed = value.trim()\n if (trimmed.startsWith('\"')) return JSON.parse(trimmed) as string\n return trimmed\n}\n\n/** THE one `SKILL.md` frontmatter parser — hand-rolled, no YAML dependency.\n *\n * Absent frontmatter (text does not open with a `---` delimiter line) is\n * legal: returns `{frontmatter: {}, body: raw, raw}`. An OPENED block with no\n * closing `---` is truncated input and throws. Inside the block: scalar\n * `key: value` lines (value optionally double-quoted, decoded via\n * `JSON.parse`); a nested `author:` block whose indented `name:`/`url:` lines\n * are the only children it accepts; `tags:` as an inline `[a, b]` list or as\n * an indented `- item` block. Unknown scalar keys are ignored (forward-compat)\n * — but a line that matches NONE of these shapes (no colon, an orphaned\n * indented line, a bad dash) throws naming the offending line. Silently\n * mis-parsed metadata is the bug class this parser exists to kill; an\n * unrecognized shape is never guessed at.\n */\nexport function parseSkillFrontmatter(raw: string): ParsedSkill {\n const lines = raw.split('\\n')\n if ((lines[0] ?? '').trim() !== '---') {\n return { frontmatter: {}, body: raw, raw }\n }\n\n let closeIndex = -1\n for (let i = 1; i < lines.length; i++) {\n if ((lines[i] ?? '').trim() === '---') {\n closeIndex = i\n break\n }\n }\n if (closeIndex === -1) {\n throw new Error(\n 'parseSkillFrontmatter: opening \"---\" has no closing \"---\" — truncated frontmatter block',\n )\n }\n\n const blockLines = lines.slice(1, closeIndex)\n const body = lines.slice(closeIndex + 1).join('\\n').replace(/^\\n+/, '')\n const frontmatter: SkillFrontmatter = {}\n const scalarKeys = new Set(['id', 'name', 'description', 'source', 'category', 'tier'])\n const topLineRe = /^(\\S[^:]*):\\s*(.*)$/\n\n let i = 0\n while (i < blockLines.length) {\n const line = blockLines[i] ?? ''\n if (line.trim() === '') {\n i++\n continue\n }\n const match = line.match(topLineRe)\n if (!match) {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const key = (match[1] ?? '').trim()\n const rest = match[2] ?? ''\n\n if (key === 'author') {\n if (rest.trim() !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const author: { name: string; url?: string } = { name: '' }\n let sawName = false\n i++\n while (i < blockLines.length && /^\\s+\\S/.test(blockLines[i] ?? '')) {\n const sub = (blockLines[i] ?? '').trim()\n const subMatch = sub.match(/^(name|url):\\s*(.*)$/)\n if (!subMatch) {\n throw new Error(\n `parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(blockLines[i])}`,\n )\n }\n const subKey = subMatch[1] as 'name' | 'url'\n const subValue = parseFrontmatterScalar(subMatch[2] ?? '')\n if (subKey === 'name') {\n author.name = subValue\n sawName = true\n } else {\n author.url = subValue\n }\n i++\n }\n if (sawName) frontmatter.author = author\n continue\n }\n\n if (key === 'tags') {\n const inline = rest.trim()\n if (inline.startsWith('[') && inline.endsWith(']')) {\n const inner = inline.slice(1, -1).trim()\n frontmatter.tags = inner === '' ? [] : inner.split(',').map((t) => parseFrontmatterScalar(t))\n i++\n continue\n }\n if (inline !== '') {\n throw new Error(`parseSkillFrontmatter: unrecognized frontmatter line: ${JSON.stringify(line)}`)\n }\n const tags: string[] = []\n i++\n while (i < blockLines.length && /^\\s*-\\s*/.test(blockLines[i] ?? '')) {\n tags.push(parseFrontmatterScalar((blockLines[i] ?? '').replace(/^\\s*-\\s*/, '')))\n i++\n }\n frontmatter.tags = tags\n continue\n }\n\n if (scalarKeys.has(key)) {\n ;(frontmatter as Record<string, string>)[key] = parseFrontmatterScalar(rest)\n }\n // Unknown scalar key: ignored, forward-compat.\n i++\n }\n\n return { frontmatter, body, raw }\n}\n\n/** Build a {@link SkillEntry} from a raw `SKILL.md` body. `id` comes from\n * frontmatter, falling back to `fallbackId` (typically the corpus entry's\n * slug/filename); neither present throws. `name` defaults to `id`,\n * `description` to `''`, `tier` to `'free'`. `skillMd` is always the\n * untouched `raw` input — the full file, frontmatter included, is what a\n * `mounted` delivery writes to disk and what `renderInlineSkills` strips per\n * render. */\nexport function skillEntryFromMarkdown(raw: string, fallbackId?: string): SkillEntry {\n const { frontmatter } = parseSkillFrontmatter(raw)\n const id = frontmatter.id ?? fallbackId\n if (!id) {\n throw new Error(\n 'skillEntryFromMarkdown: no \"id\" in frontmatter and no fallbackId supplied — cannot build a SkillEntry',\n )\n }\n const entry: SkillEntry = {\n id,\n name: frontmatter.name ?? id,\n description: frontmatter.description ?? '',\n tier: frontmatter.tier ?? 'free',\n skillMd: raw,\n }\n if (frontmatter.author) entry.author = frontmatter.author\n if (frontmatter.source) entry.source = frontmatter.source\n if (frontmatter.category) entry.category = frontmatter.category\n if (frontmatter.tags) entry.tags = frontmatter.tags\n return entry\n}\n\n/** Map a loaded corpus (see {@link loadMarkdownCorpus}) onto `SkillEntry`s,\n * using each entry's `id` as the fallback when its `SKILL.md` carries no\n * frontmatter `id` of its own. */\nexport function parseCorpusSkills(corpus: CorpusEntry[]): SkillEntry[] {\n return corpus.map((entry) => skillEntryFromMarkdown(entry.content, entry.id))\n}\n\n/** Project skills onto the typed `resources.skills` channel\n * (`AgentProfileResourceRef[]`), tier-filtered (same `s.tier === tier`\n * semantics as {@link registrySkills}) when `opts.tier` is given, sorted by\n * id for determinism. `ref.name` MUST be the skill id: the platform\n * materializer writes each ref to `${skillDir}/${name}/SKILL.md`. */\nexport function skillRefs(\n skills: SkillEntry[],\n opts: { tier?: string } = {},\n): AgentProfileResourceRef[] {\n const filtered = opts.tier ? skills.filter((s) => s.tier === opts.tier) : skills\n return [...filtered]\n .sort((a, b) => a.id.localeCompare(b.id))\n .map((s) => inlineResource(s.id, s.skillMd))\n}\n\n/** Strip the frontmatter block from a skill's `skillMd` and render it as a\n * prompt subsection. The default `body` renderer {@link renderInlineSkills}\n * passes to itself. */\nfunction renderInlineSkillBody(skill: SkillEntry): string {\n const { body } = parseSkillFrontmatter(skill.skillMd)\n return `### ${skill.name}\\n\\n${body.trim()}`\n}\n\n/** Inputs to {@link renderInlineSkills}. */\nexport interface RenderInlineSkillsInput {\n skills: SkillEntry[]\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n /** Per-skill body renderer. Default strips frontmatter and emits\n * `### <name>\\n\\n<body>`. */\n body?: (skill: SkillEntry) => string\n}\n\n/**\n * Render every (tier-filtered) skill's full body inline into the prompt — the\n * `inline` delivery mode. Returns `''` when no skill survives the filter, else\n * a section starting `\\n\\n<heading>\\n\\n` with each skill's body joined by\n * `\\n\\n` — the same \"already carries its own leading `\\n\\n`\" shape\n * `assembleSystemPrompt` expects from every section it concatenates.\n */\nexport function renderInlineSkills(input: RenderInlineSkillsInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const renderBody = input.body ?? renderInlineSkillBody\n return `\\n\\n${heading}\\n\\n${filtered.map(renderBody).join('\\n\\n')}`\n}\n\n/** Inputs to {@link renderSkillIndex}. */\nexport interface RenderSkillIndexInput {\n skills: SkillEntry[]\n /** cwd-relative directory the skills are mounted under (e.g.\n * `.opencode/skills`) — named in each index line so the agent knows where\n * to read the full `SKILL.md`. */\n skillDir: string\n /** Section heading. Default `'## Skills'`. */\n heading?: string\n /** Tier filter — same semantics as {@link skillRefs}. */\n tier?: string\n}\n\n/**\n * Render a one-line-per-skill INDEX (name, description, and the path to read\n * the full body) — the `mounted` delivery mode's prompt section, paired with\n * {@link skillRefs} putting the actual files on `resources.skills`. Same\n * empty/section shape as {@link renderInlineSkills}.\n */\nexport function renderSkillIndex(input: RenderSkillIndexInput): string {\n const heading = input.heading ?? '## Skills'\n const filtered = input.tier ? input.skills.filter((s) => s.tier === input.tier) : input.skills\n if (filtered.length === 0) return ''\n const lines = filtered.map(\n (s) => `- ${s.name}: ${s.description} (read ${input.skillDir}/${s.id}/SKILL.md)`,\n )\n return `\\n\\n${heading}\\n\\n${lines.join('\\n')}`\n}\n\n/** How a skill reaches the agent: `inline` renders its full body into the\n * system prompt; `mounted` puts it on the typed `resources.skills` channel\n * and renders only an index line. */\nexport type SkillDeliveryMode = 'inline' | 'mounted'\n\n/** The output of {@link composeSkills}: the refs to attach to\n * `resources.skills` (empty for `inline`) and the prompt section to fold into\n * the system prompt (already carries its own leading `\\n\\n`, or `''`).\n * `inlineIds`/`mountedIds` record which (tier-filtered) skills were delivered\n * in each mode so {@link mergeComposedSkills} can enforce the modes stay\n * disjoint when batches are combined. */\nexport interface ComposedSkills {\n refs: AgentProfileResourceRef[]\n promptSection: string\n inlineIds: string[]\n mountedIds: string[]\n}\n\n/** Inputs to {@link composeSkills}. */\nexport interface ComposeSkillsInput {\n skills: SkillEntry[]\n mode: SkillDeliveryMode\n tier?: string\n heading?: string\n /** REQUIRED (non-null) for `mode: 'mounted'` — the cwd-relative skill dir\n * the harness reads. Resolve it with `@tangle-network/agent-app/skills-placement`\n * rather than hardcoding it; omitted or `null` throws. */\n skillDir?: string | null\n}\n\n/**\n * Build the {@link ComposedSkills} for one delivery mode. `inline` never\n * touches `resources.skills` — `refs` is always `[]`. `mounted` requires a\n * non-null `skillDir` (the harness must have a native skill-discovery\n * directory); a null/absent one throws rather than silently falling back, so\n * the caller resolves the fallback deliberately (see\n * `@tangle-network/agent-app/skills-placement`'s `composeSkillsForHarness`,\n * whose own default is to throw rather than choose `inline` for you — inline\n * is a narrow, explicitly opted-into fallback, not a co-equal mode).\n */\nexport function composeSkills(input: ComposeSkillsInput): ComposedSkills {\n const { skills, mode, tier, heading } = input\n const deliveredIds = (tier ? skills.filter((s) => s.tier === tier) : skills).map((s) => s.id)\n if (mode === 'inline') {\n return {\n refs: [],\n promptSection: renderInlineSkills({ skills, tier, heading }),\n inlineIds: deliveredIds,\n mountedIds: [],\n }\n }\n if (!input.skillDir) {\n throw new Error(\n 'composeSkills: mode \"mounted\" requires a non-null skillDir — this harness cannot receive ' +\n 'skill files at a cwd path; pass mode \"inline\" instead',\n )\n }\n return {\n refs: skillRefs(skills, { tier }),\n promptSection: renderSkillIndex({ skills, skillDir: input.skillDir, tier, heading }),\n inlineIds: [],\n mountedIds: deliveredIds,\n }\n}\n\n/** Throw when the same skill id is delivered both `inline` and `mounted` —\n * the agent would see it twice (once in the prompt body, once as a mounted\n * file it's told to go read), doubling prompt bytes and inviting drift\n * between the two copies. Lists every offending id in the message. */\nexport function assertSkillDeliveryDisjoint(\n inlineIds: Iterable<string>,\n mountedIds: Iterable<string>,\n): void {\n const inline = new Set(inlineIds)\n const overlap = [...new Set(mountedIds)].filter((id) => inline.has(id))\n if (overlap.length > 0) {\n throw new Error(\n `assertSkillDeliveryDisjoint: skill id(s) delivered both inline and mounted: ${overlap.join(', ')}`,\n )\n }\n}\n\n/**\n * Combine multiple {@link ComposedSkills} batches (e.g. a built-in corpus and\n * an installed catalog) into one, concatenating refs and prompt sections in\n * batch order. This is the seam where inline and mounted deliveries can first\n * collide, so it applies {@link assertSkillDeliveryDisjoint} — a skill id\n * delivered inline by one batch and mounted by another throws instead of\n * reaching the agent twice. Merge through this rather than spreading batch\n * fields by hand.\n */\nexport function mergeComposedSkills(batches: ComposedSkills[]): ComposedSkills {\n const merged: ComposedSkills = {\n refs: batches.flatMap((b) => b.refs),\n promptSection: batches.map((b) => b.promptSection).join(''),\n inlineIds: batches.flatMap((b) => b.inlineIds),\n mountedIds: batches.flatMap((b) => b.mountedIds),\n }\n assertSkillDeliveryDisjoint(merged.inlineIds, merged.mountedIds)\n return merged\n}\n"],"mappings":"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","names":["i"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/chat-routes/wire.ts","../src/chat-store/parts.ts"],"sourcesContent":["/**\n * Wire contract between the chat client (composer + `streamChatTurn`) and the\n * assembled server vertical (`createChatTurnRoutes`). Import-free on purpose:\n * `/web-react` re-exports these types into browser bundles, so nothing here may\n * reach a Node builtin or an engine package.\n *\n * The part shape mirrors the sandbox SDK's `PromptInputPart` structurally\n * (text | image | file with filename/mediaType/url/path/content) — derived\n * here, not imported, so the client bundle never touches the SDK.\n */\n\nexport interface ChatTurnTextPartInput {\n type: 'text'\n text: string\n}\n\n/** A non-text prompt part the upload route hands back and the client echoes\n * on send. `url` carries an inline `data:` URI for small files; `path` is a\n * sandbox workspace reference for large ones (the >1 MiB gateway body cap\n * makes the two-step upload mandatory). */\nexport interface ChatTurnFilePartInput {\n type: 'image' | 'file'\n filename?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\n/** Resolve input as either a text part or a file part of a chat turn */\nexport type ChatTurnPartInput = ChatTurnTextPartInput | ChatTurnFilePartInput\n\n// ── producer stream vocabulary ───────────────────────────────────────────────\n\n/** Represent a text event produced by a source with a fixed type and associated text content */\nexport interface ProducerTextEvent {\n type: 'text'\n text: string\n}\n\n/** Define an event representing reasoning output with a fixed type and associated text */\nexport interface ProducerReasoningEvent {\n type: 'reasoning'\n text: string\n}\n\n/** Represent an event triggered by a producer tool call with its identifier, name, and arguments */\nexport interface ProducerToolCallEvent {\n type: 'tool_call'\n call: {\n toolCallId: string\n toolName: string\n args: Record<string, unknown>\n }\n}\n\n/** Describe the structure of an event representing the result of a producer tool call */\nexport interface ProducerToolResultEvent {\n type: 'tool_result'\n toolCallId: string\n toolName: string\n outcome: {\n ok: boolean\n result?: unknown\n message?: string\n }\n}\n\n/** Describe usage event with prompt and completion token counts for a producer */\nexport interface ProducerUsageEvent {\n type: 'usage'\n usage: {\n promptTokens: number\n completionTokens: number\n }\n}\n\n/** Define the structure for a producer notice event with type, id, kind, and text fields */\nexport interface ProducerNoticeEvent {\n type: 'notice'\n id: string\n /** Kept inline with `/interactions`' `NoticeKind` so this file stays import-free. */\n noticeKind: 'warning' | 'auto-declined'\n text: string\n}\n\n/** Represent an error event emitted by a producer containing message, code, and optional details */\nexport interface ProducerErrorEvent {\n type: 'error'\n data: {\n message: string\n code?: string\n details?: Record<string, unknown>\n }\n}\n\n/** Stable raw lifecycle/interaction/plan/route events forwarded unchanged. */\nexport type ProducerPassthroughEventType =\n | 'turn'\n | 'metadata'\n | 'interaction'\n | 'interaction.cancel'\n | 'plan.submitted'\n | 'done'\n | 'warning'\n | 'session.run.started'\n | 'session.run.completed'\n | 'session.run.failed'\n | 'turn_status'\n\n/** Define an event carrying passthrough data with flexible properties for producer communication */\nexport interface ProducerPassthroughEvent {\n type: ProducerPassthroughEventType\n data?: Record<string, unknown>\n /** Route markers and raw passthroughs may carry `turnId`, `status`, `seq`, etc. */\n [key: string]: unknown\n}\n\n/** Represent events emitted by a producer during its operation for processing and handling */\nexport type ProducerWireEvent =\n | ProducerTextEvent\n | ProducerReasoningEvent\n | ProducerToolCallEvent\n | ProducerToolResultEvent\n | ProducerUsageEvent\n | ProducerNoticeEvent\n | ProducerErrorEvent\n | ProducerPassthroughEvent\n\n/** The image/file split an attachment is rendered and persisted under — the\n * same discriminant as {@link ChatMentionKind}, but a distinct name because an\n * attachment carries content the product uploaded (`ChatAttachmentInput`)\n * while a mention points at a file the box already has. Defined HERE (the\n * import-free layer) so `ChatAttachmentInput` can reference it and the client\n * composer imports it without pulling the persisted-part vocabulary;\n * `/chat-store`'s parts module re-exports it alongside the attachment helpers. */\nexport type ChatAttachmentKind = 'image' | 'file'\n\n/** `POST` turn-body entry describing a file already uploaded to the product's\n * store (vault/object-store) — distinct from an inline {@link\n * ChatTurnFilePartInput} (which carries bytes) and from a {@link FileMention}\n * (a sandbox path the box already holds). The route resolves this field with\n * {@link resolveChatAttachments}: every path is re-validated and every size is\n * re-derived from the stored body, so nothing here is trusted as sent. */\nexport interface ChatAttachmentInput {\n path: string\n name: string\n size: number\n mediaType: string\n kind: ChatAttachmentKind\n}\n\n/** POST body for the turn route. `content` may be empty when `parts` carry the\n * message (an image-only send). Product routing fields (workspaceId etc.) ride\n * alongside and are read by the product's `authorize` seam. */\nexport interface ChatTurnRequestPayload {\n threadId: string\n content?: string\n /** Non-text parts from the upload route, echoed back verbatim. */\n parts?: ChatTurnFilePartInput[]\n /** `@`-picked file mentions for this turn — path references into the\n * workspace sandbox, NOT uploads, so they travel in their own field rather\n * than as `parts` entries. A product whose `parts` field is already spoken\n * for (an attachment sentinel) can still send mentions, and mentions\n * persist as their own `ChatMentionPart`s so a retry rebuilds them. The\n * route validates this field with {@link parseFileMentions} and replaces it\n * on the payload with the validated, deduped list. */\n mentions?: FileMention[]\n /** Files uploaded to the product's store ahead of the turn — path\n * references, NOT inline bytes (those ride `parts`). Validated and\n * size-re-derived by {@link resolveChatAttachments} into persistable\n * attachment parts; a product whose `parts` field is spoken for by inline\n * uploads still sends store-backed files here. */\n attachments?: ChatAttachmentInput[]\n model?: string\n effort?: 'auto' | 'low' | 'medium' | 'high'\n harness?: string\n /** Client-generated idempotency key for the logical turn (retry-safe). */\n turnId?: string\n [key: string]: unknown\n}\n\n/** `fetch` init for the turn route — the one place the client wire shape is\n * serialized, so composer glue and products never drift from the server's\n * parser. */\nexport function chatTurnRequestInit(payload: ChatTurnRequestPayload): RequestInit {\n return {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n }\n}\n\n// ── inline-part byte budget ─────────────────────────────────────────────────\n//\n// The sandbox gateway caps request bodies at 1 MiB; a turn body whose inline\n// `data:` parts exceed it dies at the gateway with an opaque 413. Enforce the\n// budget at the route boundary instead, with headroom for the JSON envelope\n// (same fail-loud-at-the-choke-point style as /sandbox's provision-payload and\n// env-size gates).\n\n/** Define the maximum byte size allowed for inline parts in data processing */\nexport const INLINE_PARTS_MAX_BYTES = 950_000\n\n// ── dispatch (parts[]) budget vocabulary ────────────────────────────────────\n//\n// The default caps `buildDispatchParts` sizes an attachment/mention dispatch\n// against — the sidecar/proxy limits an assembled `parts` array crosses, one\n// step past `INLINE_PARTS_MAX_BYTES` (which gates the raw turn BODY). Grouped\n// here, in the import-free layer, so the numbers are one overridable\n// vocabulary the client can read and a product can tune per call rather than\n// constants buried in the server module. NOTE: these model sidecar/proxy caps,\n// not a product's MIME accept-list or vault bucketing — those are DOMAIN\n// values the product supplies, never defaulted here.\n\n/** Hard cap on the whole `/prompt` request body as it crosses the sandbox\n * proxy — smaller in practice than a raw-file write cap because a dispatch\n * carries several inline parts plus the flattened history in one request. */\nexport const DISPATCH_REQUEST_MAX_BYTES = 1024 * 1024\n\n/** Bytes reserved off the top of {@link DISPATCH_REQUEST_MAX_BYTES} for the\n * JSON structure around the parts array (keys, delimiters, per-part\n * `type`/`filename`/`mediaType` fields) that {@link base64WireLen} does not\n * account for — keeps the inline budget off the exact proxy cap where one\n * stray byte trips the 413. */\nexport const DISPATCH_STRUCTURAL_RESERVE_BYTES = 64 * 1024\n\n/** Sidecar's hard cap on the `parts` array of one prompt request — a dispatch\n * must never assemble more parts than this or the whole turn 400s. */\nexport const DISPATCH_MAX_PARTS = 64\n\n/** Product-side cap on media parts per dispatch (current turn + carried\n * history), well under {@link DISPATCH_MAX_PARTS}. History trimming that keeps\n * a transcript's native media under this is a PRODUCT concern (the pointer\n * block keeps trimmed media reachable); `buildDispatchParts` enforces only the\n * total {@link DISPATCH_MAX_PARTS} cap. */\nexport const DISPATCH_MAX_MEDIA_PARTS = 24\n\n/** Size a base64-encoded string occupies on the wire given the raw\n * (pre-encoding) byte length: base64 packs 3 raw bytes into 4 output\n * characters, rounded up to the next multiple of 4. */\nexport function base64WireLen(byteLen: number): number {\n return Math.ceil(byteLen / 3) * 4\n}\n\n/**\n * Render a raw byte count as a human-readable size (`512B`, `3KB`, `12MB\n * 500KB`). Ported EXACTLY from gtm-agent's `attachment-limits.ts` — byte-\n * identical implementation, not a reinterpretation — so `resolve-attachments`'s\n * and `promote-file-part`'s error strings match gtm's wording verbatim. Lives\n * in the import-free wire layer (not `resolve-attachments.ts` alone) because\n * BOTH the aggregate-cap message here and the per-file oversize message in\n * `promote-file-part.ts` need it; a browser composer wanting the same\n * formatting for a client-side pre-check can also import it with no engine\n * pulled in.\n */\nexport function formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes}B`\n if (bytes >= 1024 * 1024) {\n const megabytes = Math.floor(bytes / (1024 * 1024))\n const remainder = bytes % (1024 * 1024)\n return remainder === 0 ? `${megabytes}MB` : `${megabytes}MB ${formatBytes(remainder)}`\n }\n return `${Math.round(bytes / 1024)}KB`\n}\n\n/** Represent errors for invalid chat turn inputs with status and code properties */\nexport class ChatTurnInputError extends Error {\n constructor(message: string, readonly status = 400, readonly code = 'INVALID_CHAT_TURN') {\n super(message)\n this.name = 'ChatTurnInputError'\n }\n}\n\nfunction partByteSize(part: ChatTurnPartInput): number {\n let bytes = 0\n if (part.type === 'text') return part.text.length\n if (part.url) bytes += part.url.length\n if (part.content) bytes += part.content.length\n if (part.path) bytes += part.path.length\n return bytes\n}\n\n/** Calculate the total byte size of an array of chat turn parts */\nexport function promptPartsByteSize(parts: ChatTurnPartInput[]): number {\n return parts.reduce((total, part) => total + partByteSize(part), 0)\n}\n\n/** Throws `ChatTurnInputError` (413) when the parts' inline payload would blow\n * the gateway cap. Path-ref parts are tiny by construction and always pass. */\nexport function assertPromptPartsWithinCap(\n parts: ChatTurnPartInput[],\n maxBytes = INLINE_PARTS_MAX_BYTES,\n): void {\n const total = promptPartsByteSize(parts)\n if (total <= maxBytes) return\n const largest = [...parts].sort((a, b) => partByteSize(b) - partByteSize(a))[0]\n const largestName = largest && largest.type !== 'text' ? largest.filename ?? largest.path ?? largest.type : 'text'\n throw new ChatTurnInputError(\n `Inline prompt parts total ${total}B, over the ${maxBytes}B budget (largest: ${largestName}, ${largest ? partByteSize(largest) : 0}B). ` +\n 'Upload large files through the upload route so they travel as sandbox path references.',\n 413,\n 'PROMPT_PARTS_TOO_LARGE',\n )\n}\n\n// ── file mentions ────────────────────────────────────────────────────────\n//\n// A file mention (`@`-picked in the composer, sandbox-ui#184) is a path\n// reference into the workspace sandbox — no byte upload. These helpers turn\n// a resolved mention list into wire parts and the prompt pointer block that\n// tells the agent where to read them from.\n\n/** A file mention resolved from the composer's `@`-picker: the\n * workspace-relative path plus enough metadata to build a prompt part and\n * pointer text. `path` is the canonical identity — the mention pill's\n * `MentionItem.id` for the file kind (`/web-react`'s `useFileMentions`). */\nexport interface FileMention {\n path: string\n name: string\n size?: number\n}\n\nconst MENTION_IMAGE_MEDIA_TYPES: ReadonlyMap<string, string> = new Map([\n ['.png', 'image/png'],\n ['.jpg', 'image/jpeg'],\n ['.jpeg', 'image/jpeg'],\n ['.gif', 'image/gif'],\n ['.webp', 'image/webp'],\n ['.svg', 'image/svg+xml'],\n ['.bmp', 'image/bmp'],\n ['.heic', 'image/heic'],\n ['.heif', 'image/heif'],\n ['.avif', 'image/avif'],\n])\n\nfunction extensionOf(path: string): string {\n const base = path.split('/').filter(Boolean).pop() ?? path\n const dot = base.lastIndexOf('.')\n return dot > 0 ? base.slice(dot).toLowerCase() : ''\n}\n\n/** The `image/*` mime for a mention path by extension, or `undefined` for\n * anything not in the known image set (dispatched as `type: 'file'`). */\nexport function mediaTypeForMentionPath(path: string): string | undefined {\n return MENTION_IMAGE_MEDIA_TYPES.get(extensionOf(path))\n}\n\n/** The image/file split a mention is rendered and persisted under — the\n * composer pill's icon, the dispatched part's `type`, and\n * `ChatMentionPart.mentionKind` are all this one value. */\nexport type ChatMentionKind = 'image' | 'file'\n\n/** `image` when the path's extension is in the known image set (the same table\n * {@link mediaTypeForMentionPath} reads), `file` otherwise. Exported so a\n * client that needs only the discriminant — a pill icon, a persisted part's\n * `mentionKind` — never re-declares the extension table; two frozen copies of\n * one mime table is how one gains a format and the other doesn't. */\nexport function mentionKindForPath(path: string): ChatMentionKind {\n return mediaTypeForMentionPath(path) ? 'image' : 'file'\n}\n\n/** Define options to resolve mention paths when converting file mentions to parts */\nexport interface FileMentionsToPartsOptions {\n /** Resolve a mention's workspace-relative path to the absolute path the\n * dispatched part should carry (e.g. a host prefixing the in-box vault\n * root). Default: identity — the path travels unchanged. */\n resolvePath?: (path: string) => string\n}\n\n/** Maps resolved file mentions to path-only `ChatTurnFilePartInput`s —\n * `image` vs `file` by extension, and always a `path`, never a `url` (the\n * url/path XOR invariant: a mention is a sandbox path reference, never\n * inline bytes). */\nexport function fileMentionsToParts(\n mentions: readonly FileMention[],\n opts: FileMentionsToPartsOptions = {},\n): ChatTurnFilePartInput[] {\n const resolvePath = opts.resolvePath ?? ((path: string) => path)\n return mentions.map((mention) => {\n const mediaType = mediaTypeForMentionPath(mention.path)\n const part: ChatTurnFilePartInput = {\n type: mediaType ? 'image' : 'file',\n filename: mention.name,\n path: resolvePath(mention.path),\n }\n if (mediaType) part.mediaType = mediaType\n return part\n })\n}\n\n/** The agent-facing pointer block appended to the dispatched prompt — never\n * persisted in message `content`. Empty array → `''` so callers can append\n * unconditionally. This is the sole producer of that text: the current\n * turn's dispatch and any history projection built from the same mention\n * list both route through here, so the two can't drift apart. */\nexport function buildMentionPromptBlock(\n mentions: readonly Pick<FileMention, 'name' | 'path'>[],\n): string {\n if (mentions.length === 0) return ''\n const lines = mentions.map((m) => `- ${m.name} (${m.path})`)\n return `\\n\\nMentioned files — read them from these paths:\\n${lines.join('\\n')}`\n}\n\n// ── mention validation ───────────────────────────────────────────────────\n//\n// This package owns BOTH ends of the mention path contract — it emits paths\n// from `createSandboxFileIndexRoute` and consumes them in `fileMentionsToParts`\n// / `buildMentionPromptBlock` — so the validation belongs here rather than in\n// each app that wires the pair up. A mention names a file that already exists\n// in the sandbox, so validation is a pure path/charset/count check with no\n// I/O: existence is proven later, when the agent reads the path and the turn\n// fails loudly if it is gone.\n\n/** Hard cap on mentions per turn. Bounds the prompt pointer block, the\n * persisted parts, and whatever media budget a dispatch draws from them. */\nexport const MENTION_MAX_COUNT = 16\n\n/** Longest mention display name accepted — bounds the pointer-block text and\n * the transcript pill label. */\nconst MAX_MENTION_NAME_LENGTH = 256\n/** Longest mention path accepted. */\nconst MAX_MENTION_PATH_LENGTH = 1024\n\n/** Represent the result of a sandbox mention path check indicating success or failure with an error message */\nexport type SandboxMentionPathCheck =\n | { succeeded: true }\n | { succeeded: false; error: string }\n\n/**\n * Validate a workspace-relative sandbox mention path. Rejects traversal (a\n * `..` path segment), absolute paths (leading `/`), backslashes, and null\n * bytes — the four ways a path picked in a client can escape the root the\n * index route scanned.\n *\n * Spaces and unicode are deliberately ALLOWED: in-box filenames are arbitrary,\n * and an ASCII-only charset would silently drop real files from a feature\n * whose whole job is naming them.\n */\nexport function validateSandboxMentionPath(path: unknown): SandboxMentionPathCheck {\n if (typeof path !== 'string' || path.length === 0) {\n return { succeeded: false, error: 'mention path must be a non-empty string' }\n }\n if (path.length > MAX_MENTION_PATH_LENGTH) {\n return { succeeded: false, error: `mention path must not exceed ${MAX_MENTION_PATH_LENGTH} characters` }\n }\n if (path.includes('\\0')) {\n return { succeeded: false, error: 'mention path must not contain null bytes' }\n }\n if (path.includes('\\\\')) {\n return { succeeded: false, error: 'mention path must not contain backslashes' }\n }\n if (path.startsWith('/')) {\n return { succeeded: false, error: 'mention path must be workspace-relative, not absolute' }\n }\n if (path.split('/').some((segment) => segment === '..')) {\n return { succeeded: false, error: 'mention path must not contain \"..\" segments' }\n }\n return { succeeded: true }\n}\n\nfunction parseFileMention(value: unknown, index: number): FileMention {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n throw new ChatTurnInputError(`mentions[${index}] must be an object`)\n }\n const record = value as Record<string, unknown>\n\n const pathCheck = validateSandboxMentionPath(record.path)\n if (!pathCheck.succeeded) throw new ChatTurnInputError(`mentions[${index}]: ${pathCheck.error}`)\n\n const name = record.name\n if (typeof name !== 'string' || !name.trim()) {\n throw new ChatTurnInputError(`mentions[${index}].name must be a non-empty string`)\n }\n if (name.length > MAX_MENTION_NAME_LENGTH) {\n throw new ChatTurnInputError(`mentions[${index}].name must not exceed ${MAX_MENTION_NAME_LENGTH} characters`)\n }\n\n const size = record.size\n if (size !== undefined) {\n if (typeof size !== 'number' || !Number.isFinite(size)) {\n throw new ChatTurnInputError(`mentions[${index}].size must be a finite number`)\n }\n if (size < 0) {\n throw new ChatTurnInputError(`mentions[${index}].size must not be negative`)\n }\n }\n\n return { path: record.path as string, name, ...(typeof size === 'number' ? { size } : {}) }\n}\n\n/**\n * Validates the untyped `mentions` array off the wire, mirroring\n * {@link parseChatTurnParts}: the typed list, or `ChatTurnInputError` (400)\n * naming the offending entry. Never sanitizes-and-continues — a traversal path\n * is a rejected request, not a trimmed one.\n *\n * A path repeated within one turn is deduped to its first occurrence rather\n * than rejected: mentioning the same file twice is plausible user input, not\n * an attack.\n */\nexport function parseFileMentions(raw: unknown): FileMention[] {\n if (raw === undefined || raw === null) return []\n if (!Array.isArray(raw)) throw new ChatTurnInputError('mentions must be an array')\n if (raw.length > MENTION_MAX_COUNT) {\n throw new ChatTurnInputError(`mentions must not exceed ${MENTION_MAX_COUNT} entries`)\n }\n\n const mentions: FileMention[] = []\n const seenPaths = new Set<string>()\n for (let index = 0; index < raw.length; index += 1) {\n const mention = parseFileMention(raw[index], index)\n if (seenPaths.has(mention.path)) continue\n seenPaths.add(mention.path)\n mentions.push(mention)\n }\n return mentions\n}\n\n/** Validates the untyped `parts` array off the wire. Returns the typed parts\n * or throws `ChatTurnInputError` (400) naming the offending entry. */\nexport function parseChatTurnParts(raw: unknown): ChatTurnFilePartInput[] {\n if (raw === undefined || raw === null) return []\n if (!Array.isArray(raw)) throw new ChatTurnInputError('parts must be an array')\n return raw.map((entry, index) => {\n const part = entry as Record<string, unknown> | null\n if (!part || typeof part !== 'object') {\n throw new ChatTurnInputError(`parts[${index}] must be an object`)\n }\n if (part.type !== 'image' && part.type !== 'file') {\n throw new ChatTurnInputError(`parts[${index}].type must be 'image' or 'file'`)\n }\n for (const key of ['filename', 'mediaType', 'url', 'path', 'content'] as const) {\n if (part[key] !== undefined && typeof part[key] !== 'string') {\n throw new ChatTurnInputError(`parts[${index}].${key} must be a string`)\n }\n }\n if (!part.url && !part.path && !part.content) {\n throw new ChatTurnInputError(`parts[${index}] needs a url, path, or content`)\n }\n return {\n type: part.type,\n ...(part.filename !== undefined ? { filename: part.filename as string } : {}),\n ...(part.mediaType !== undefined ? { mediaType: part.mediaType as string } : {}),\n ...(part.url !== undefined ? { url: part.url as string } : {}),\n ...(part.path !== undefined ? { path: part.path as string } : {}),\n ...(part.content !== undefined ? { content: part.content as string } : {}),\n }\n })\n}\n","/**\n * The stored shape of `message.parts` — one typed vocabulary for every part a\n * product persists into a chat transcript. NOT an ad-hoc union reverse-\n * engineered from product schemas; each member is matched field-for-field to\n * its canonical source:\n *\n * - `text` / `reasoning` / `tool`: the persisted projection `/stream`'s\n * `normalizePersistedPart` produces from the harness lane's\n * `message.part.updated` events (ADC sidecar\n * `apps/sidecar/src/events/session-events.ts:56` wraps the canonical part in\n * an `{id, sessionID, messageID}` envelope; the projection strips the\n * session/message ids and keeps the per-segment part id).\n * - `file` / `image` / `step-start` / `step-finish`: the sidecar's canonical\n * `MessagePartSchema` members (ADC\n * `apps/sidecar/src/schemas/agent-schemas.ts:50-154`); `step-finish` carries\n * the harness's per-step usage receipt — tokens\n * `{total, input, output, reasoning, cache{write, read}}` + `cost` — which is\n * also the shape the message-level token/cost columns mirror.\n * - `subtask`: `@tangle-network/agent-interface`'s `SubtaskPart` (a spawned\n * sub-agent task).\n * - `interaction` / `notice`: the persisted-part codecs in\n * `/web-react`'s chat-interactions contract (`interactionToPersistedPart`,\n * `noticePart`) — type-only imports, one source of truth for their statuses\n * and field shapes.\n * - `plan`: the durable-plan projection in `/plans`, derived from the sandbox\n * SDK's authoritative plan lifecycle.\n * - `work_product`: the persisted work-product anchor card in\n * `/work-product`'s contract (`workProductToPersistedPart` /\n * `persistedPartToWorkProduct`) — SYSTEM-authored on the ready transition\n * and updated on a reviewer verdict; no prompt teaches an agent to author\n * one.\n * - `mention`: an `@`-picked reference to a file that already lives in the\n * workspace sandbox (`FileMention` in `/chat-routes`'s wire contract, plus\n * the image/file discriminant). Neither transport lane produces it — the\n * turn route persists it from the request's `mentions` field — but it is a\n * part a product persists into a transcript, so it belongs in this\n * vocabulary rather than in a parallel one.\n *\n * `@tangle-network/agent-interface` exports the canonical wire `Part` union,\n * but its `PartBase` requires the `sessionID`/`messageID` stream envelope that\n * is deliberately NOT persisted, so the stored union is defined here as the\n * envelope-free projection (a type-level coverage check against the peer's\n * `Part['type']` lives in the tests). Contribute-down candidate: if\n * agent-interface grows envelope-free persisted-part types, re-export them\n * here and delete these definitions.\n *\n * Two transport lanes serialize into this SAME stored shape:\n * - harness lane: canonical `message.part.updated` parts, merged/normalized by\n * `/stream` (`mergePersistedPart`, `finalizeAssistantParts`);\n * - router/openai-compat lane: `text_delta`/`tool_call` stream events are\n * mapped INTO canonical part events first (`/runtime`'s `toLoopEvents` +\n * `/stream`'s `normalizeToolEvent`) and then persisted identically — the\n * store never sees a router-specific shape.\n */\n\nimport type { Part as HarnessWirePart } from '@tangle-network/agent-interface'\nimport type {\n ChatInteractionField,\n ChatInteractionStatus,\n InteractionAnswers,\n InteractionPersistedPart,\n NoticeKind,\n NoticePersistedPart,\n} from '../web-react/chat-interactions'\nimport { persistedPartToInteraction } from '../interactions/contract'\nimport {\n persistedPartToWorkProduct,\n type WorkProductPersistedPart,\n} from '../work-product/types'\nimport {\n persistedPartToPlan,\n planToPersistedPart,\n type ChatPlanPersistedPart,\n} from '../plans/index'\n// `./wire` is import-free by construction, so this edge costs the store\n// nothing and keeps ONE image-extension table for the whole mention layer.\nimport {\n mentionKindForPath,\n type ChatAttachmentInput,\n type ChatAttachmentKind,\n type ChatMentionKind,\n type FileMention,\n} from '../chat-routes/wire'\n\nexport type { ChatMentionKind, ChatAttachmentKind }\n\n/** Start/end wall-clock millis, as normalized by `/stream`'s `normalizeTime`. */\nexport interface ChatPartTime {\n start?: number\n end?: number\n}\n\n/** `id` is the harness's per-segment identity; absent on legacy/router parts,\n * which collapse to a single logical text stream. Never invented client-side. */\nexport interface ChatTextPart {\n type: 'text'\n text: string\n id?: string\n}\n\n/** Define a reasoning part of a chat with text content and optional metadata fields */\nexport interface ChatReasoningPart {\n type: 'reasoning'\n text: string\n id?: string\n time?: ChatPartTime\n}\n\n/** Superset of the sidecar's status enum (`pending|running|completed|failed`)\n * and agent-interface's `ToolState` statuses; `error` is the persisted\n * terminal form `/stream`'s `normalizePersistedPart` settles on. */\nexport type ChatToolStatus = 'pending' | 'running' | 'completed' | 'error' | 'failed'\n\n/** Describe the current state and data of a chat tool including status, input, output, and metadata */\nexport interface ChatToolState {\n status: ChatToolStatus\n input?: unknown\n output?: unknown\n error?: string\n title?: string\n metadata?: Record<string, unknown>\n time?: ChatPartTime\n}\n\n/** Define a chat component representing a tool with its state and optional call identifier */\nexport interface ChatToolPart {\n type: 'tool'\n id: string\n tool: string\n callID?: string\n state: ChatToolState\n}\n\n/** Union of the sidecar's legacy (path-based) and AI-SDK (url-based) file\n * shapes; response-side every field besides `type` is optional. `name` is the\n * attachment-flavored display label an uploaded/promoted file carries (see\n * {@link ChatAttachmentPart}) — absent on a harness-emitted file part, which\n * uses `filename`. */\nexport interface ChatFilePart {\n type: 'file'\n id?: string\n filename?: string\n name?: string\n mediaType?: string\n url?: string\n path?: string\n content?: string\n}\n\n/** Define properties for an image part within a chat message including optional metadata fields */\nexport interface ChatImagePart {\n type: 'image'\n filename?: string\n name?: string\n mediaType?: string\n url?: string\n path?: string\n}\n\n/** Define a subtask part of a chat with prompt, description, agent, and optional identifier */\nexport interface ChatSubtaskPart {\n type: 'subtask'\n prompt: string\n description: string\n agent: string\n id?: string\n}\n\n/** OpenCode step-boundary marker — no renderable text; preserved so mappers\n * never coerce it into a \"[object Object]\" text part. */\nexport interface ChatStepStartPart {\n type: 'step-start'\n}\n\n/** Per-step usage receipt as the harness reports it (sidecar\n * `StepFinishPartSchema`). The message-level token/cost columns are this\n * shape flattened. */\nexport interface ChatUsageTokens {\n total?: number\n input?: number\n output?: number\n reasoning?: number\n cache?: {\n write?: number\n read?: number\n }\n}\n\n/** Define a chat step finish part indicating completion with optional reason, tokens, and cost */\nexport interface ChatStepFinishPart {\n type: 'step-finish'\n reason?: string\n tokens?: ChatUsageTokens\n cost?: number\n}\n\n/** Persisted human-in-the-loop ask — byte-matches\n * `interactionToPersistedPart` in `/web-react`'s chat-interactions contract. */\nexport interface ChatInteractionPart {\n type: 'interaction'\n id: string\n kind: string\n title: string\n body?: string\n answerSpec: { fields: ChatInteractionField[] }\n status: ChatInteractionStatus\n answers?: InteractionAnswers\n cancelReason?: string\n}\n\n/** Resolve a chat plan part by aliasing it to the persisted chat plan part type */\nexport type ChatPlanPart = ChatPlanPersistedPart\n\n/** Persisted work-product anchor card — byte-matches\n * `workProductToPersistedPart` in `/work-product`'s contract. Written by the\n * PLATFORM on the ready transition (and updated on a verdict); never\n * authored by a prompt. */\nexport type ChatWorkProductPart = WorkProductPersistedPart\n\n/** Persisted one-line transcript notice — byte-matches `noticePart` in\n * `/web-react`'s chat-interactions contract. */\nexport interface ChatNoticePart {\n type: 'notice'\n id: string\n noticeKind: NoticeKind\n text: string\n}\n\n/**\n * A file the user `@`-mentioned on this turn: a workspace-relative path into\n * the sandbox, never bytes. `type: 'mention'` is its own discriminant\n * precisely so it does NOT collide with the `file`/`image` attachment parts —\n * an attachment carries content the product uploaded, a mention points at\n * something the box already has, and a transcript renders them differently\n * (an inline pill, not an attachment card).\n *\n * `path` is the identity: mentioning one file twice in a turn folds to one\n * part. `turnId` is optional and set by products that rebuild a turn's\n * mentions on retry.\n */\nexport interface ChatMentionPart {\n type: 'mention'\n mentionKind: ChatMentionKind\n path: string\n name: string\n size?: number\n turnId?: string\n}\n\n// The \"byte-matches\" claims above, enforced at compile time: the interaction\n// contract's codec output types and the stored part types must stay mutually\n// assignable, so a codec field added on one side without the other fails here.\ntype MutuallyAssignable<A extends B, B> = A\ntype _CodecEmitsStorableInteractionPart = MutuallyAssignable<InteractionPersistedPart, ChatInteractionPart>\ntype _StoredInteractionPartFeedsCodec = MutuallyAssignable<ChatInteractionPart, InteractionPersistedPart>\ntype _CodecEmitsStorableNoticePart = MutuallyAssignable<NoticePersistedPart, ChatNoticePart>\ntype _StoredNoticePartFeedsCodec = MutuallyAssignable<ChatNoticePart, NoticePersistedPart>\ntype _CodecEmitsStorablePlanPart = MutuallyAssignable<ChatPlanPersistedPart, ChatPlanPart>\ntype _StoredPlanPartFeedsCodec = MutuallyAssignable<ChatPlanPart, ChatPlanPersistedPart>\ntype _CodecEmitsStorableWorkProductPart = MutuallyAssignable<WorkProductPersistedPart, ChatWorkProductPart>\ntype _StoredWorkProductPartFeedsCodec = MutuallyAssignable<ChatWorkProductPart, WorkProductPersistedPart>\n\n/** Represent parts of a chat message including text, reasoning, tools, files, images, subtasks, steps, interactions, notices, plans, and mentions */\nexport type ChatMessagePart =\n | ChatTextPart\n | ChatReasoningPart\n | ChatToolPart\n | ChatFilePart\n | ChatImagePart\n | ChatSubtaskPart\n | ChatStepStartPart\n | ChatStepFinishPart\n | ChatInteractionPart\n | ChatNoticePart\n | ChatPlanPart\n | ChatWorkProductPart\n | ChatMentionPart\n\n/** Every canonical harness wire-part kind must be storable — compile-time\n * guarantee that a new agent-interface part kind cannot silently fall out of\n * the persisted vocabulary. */\nexport type StorableHarnessPartKind = HarnessWirePart['type'] & ChatMessagePart['type']\n\n/**\n * The typed projection at the `/stream` → `/chat-store` boundary. The stream\n * normalizers (`normalizePersistedPart`/`mergePersistedPart`/\n * `finalizeAssistantParts`) deliberately produce untyped `JsonRecord`s — they\n * normalize wire shapes and do not own the stored vocabulary. THIS module\n * owns it, so this is where rows gain the `ChatMessagePart` type: each entry\n * is validated against its kind's required fields and narrowed, junk is\n * dropped, and — enforced by the exhaustiveness check below — no storable\n * kind can silently fall out (the step-finish/interaction trap).\n */\nexport function toChatMessageParts(parts: Array<Record<string, unknown>>): ChatMessagePart[] {\n const out: ChatMessagePart[] = []\n for (const part of parts) {\n const typed = toChatMessagePart(part)\n if (typed) out.push(typed)\n }\n return out\n}\n\nconst str = (value: unknown): value is string => typeof value === 'string'\n\nfunction toChatMessagePart(part: Record<string, unknown>): ChatMessagePart | null {\n if (!part || typeof part !== 'object') return null\n const type = part.type as ChatMessagePart['type'] | undefined\n switch (type) {\n case 'text':\n case 'reasoning':\n return str(part.text) ? (part as unknown as ChatTextPart | ChatReasoningPart) : null\n case 'tool':\n return str(part.id) && str(part.tool) && part.state && typeof part.state === 'object'\n ? (part as unknown as ChatToolPart)\n : null\n case 'file':\n case 'image':\n return part as unknown as ChatFilePart | ChatImagePart\n case 'subtask':\n return str(part.prompt) && str(part.description) && str(part.agent)\n ? (part as unknown as ChatSubtaskPart)\n : null\n case 'step-start':\n return { type: 'step-start' }\n case 'step-finish':\n return part as unknown as ChatStepFinishPart\n case 'interaction':\n return persistedPartToInteraction(part) ? (part as unknown as ChatInteractionPart) : null\n case 'notice':\n return str(part.id) && str(part.noticeKind) && str(part.text)\n ? (part as unknown as ChatNoticePart)\n : null\n case 'plan': {\n const plan = persistedPartToPlan(part)\n return plan ? ({ ...part, ...planToPersistedPart(plan) } as ChatPlanPart) : null\n }\n case 'work_product':\n // Validate via the codec but keep the original row (extra fields the\n // product persisted round-trip, matching the interaction case).\n return persistedPartToWorkProduct(part) ? (part as unknown as ChatWorkProductPart) : null\n case 'mention':\n return isChatMentionPart(part) ? part : null\n case undefined:\n return null\n default: {\n // Compile-time exhaustiveness: a new ChatMessagePart kind that is not\n // handled above makes `type` non-never here and this line fails.\n const _exhaustive: never = type\n void _exhaustive\n return null\n }\n }\n}\n\n/** Resolve whether a ChatMessagePart is specifically a ChatToolPart based on its type property */\nexport function isChatToolPart(part: ChatMessagePart): part is ChatToolPart {\n return part.type === 'tool'\n}\n\n/** Resolve whether a chat message part is a text part based on its type property */\nexport function isChatTextPart(part: ChatMessagePart): part is ChatTextPart {\n return part.type === 'text'\n}\n\n/** Resolve whether a ChatMessagePart is a ChatInteractionPart based on its type property */\nexport function isChatInteractionPart(part: ChatMessagePart): part is ChatInteractionPart {\n return part.type === 'interaction'\n}\n\n/** Resolve whether a chat message part is a persisted chat plan part */\nexport function isChatPlanPart(part: ChatMessagePart): part is ChatPlanPart {\n return part.type === 'plan'\n}\n\n/** Resolve whether a chat message part is a persisted work-product anchor */\nexport function isChatWorkProductPart(part: ChatMessagePart): part is ChatWorkProductPart {\n return part.type === 'work_product'\n}\n\n/** Determine if a chat message part represents the completion of a chat step */\nexport function isChatStepFinishPart(part: ChatMessagePart): part is ChatStepFinishPart {\n return part.type === 'step-finish'\n}\n\n/** Widened to `unknown` — unlike its siblings this guard also runs over raw\n * untyped stored rows (a transcript renderer reads `message.parts` before the\n * typed projection), which is exactly what {@link mentionPartsFromMessageParts}\n * needs. `path` and `name` carry the pill; a row missing either is unrenderable.\n *\n * Mirrors the write contract exactly (`parseFileMention` in `/chat-routes`,\n * then {@link mentionInputToPart}): a blank `name` is rejected there and so is\n * rejected here, and `size` — optional, but typed `number` once present — is\n * type-checked so `'12'` or `null` cannot ride through the guard wearing a\n * type it does not have. Negative sizes are NOT re-rejected: the wire screens\n * them, `mentionInputToPart` trusts its input, and a read guard stricter than\n * what the writer can emit would drop rows it produced itself. */\nexport function isChatMentionPart(part: unknown): part is ChatMentionPart {\n if (!part || typeof part !== 'object') return false\n const record = part as Record<string, unknown>\n if (record.size !== undefined && (typeof record.size !== 'number' || !Number.isFinite(record.size))) {\n return false\n }\n return (\n record.type === 'mention' &&\n typeof record.path === 'string' &&\n record.path.length > 0 &&\n typeof record.name === 'string' &&\n record.name.trim().length > 0 &&\n (record.mentionKind === 'image' || record.mentionKind === 'file')\n )\n}\n\n/** Every mention part on one message, in stored order. The projection a\n * transcript renderer runs before deciding which mentions the message text\n * already shows inline (see `segmentMentionContent` in `/web-react`). */\nexport function mentionPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | ReadonlyArray<ChatMessagePart> | null | undefined,\n): ChatMentionPart[] {\n if (!parts) return []\n return (parts as ReadonlyArray<unknown>).filter(isChatMentionPart)\n}\n\n/** A validated wire mention (`parseFileMentions` in `/chat-routes`) as the\n * part the turn route persists. An absent/non-finite `size` is DROPPED rather\n * than stored as `undefined`, so a stored row never carries a key that means\n * nothing. */\nexport function mentionInputToPart(input: FileMention): ChatMentionPart {\n const part: ChatMentionPart = {\n type: 'mention',\n mentionKind: mentionKindForPath(input.path),\n path: input.path,\n name: input.name,\n }\n if (typeof input.size === 'number' && Number.isFinite(input.size)) part.size = input.size\n return part\n}\n\n// ── attachments ──────────────────────────────────────────────────────────\n//\n// An attachment is a file the product uploaded into its own store (vault /\n// object-store) ahead of the turn — content the box did not already have,\n// unlike a `@`-mention. It persists NOT as a new `ChatMessagePart` union\n// member but as an attachment-flavored `file`/`image` part: `type` is the\n// existing `image`/`file` discriminant, with `path` + `name` load-bearing.\n// That reuse is deliberate — a transcript already renders `file`/`image`\n// parts, and `toChatMessagePart` passes them through by cast (fields survive),\n// so an attachment slots into the same stored `parts` array without a parallel\n// vocabulary. These helpers own the input→part projection, the path-keyed\n// stream key, and the agent-facing prompt block, all byte-identical to the gtm\n// source they were lifted from (gtm-agent#618 adoption reproduces its rows and\n// dispatched prompt bytes through them).\n\n/** `image` for any `image/*` mime, `file` for everything else (including an\n * absent/empty mime) — the same split the composer and the persisted-part\n * discriminant both key on. */\nexport function attachmentKindForMime(mime?: string): ChatAttachmentKind {\n return mime?.startsWith('image/') ? 'image' : 'file'\n}\n\n/** Stream/transcript part key for an attachment — ONE declaration, owned by\n * `/stream`'s normalizer (whose `getPartKey` routes path-bearing file/image\n * parts through it). Re-exported here (not redefined) so the root barrel's\n * star-exports of both modules resolve to the same symbol instead of\n * colliding (TS2308 in the dts build). */\nexport { attachmentPartKey } from '../stream/stream-normalizer'\n\n/**\n * Persisted attachment part: structurally an attachment-flavored `ChatFilePart`\n * / `ChatImagePart` (`path` + `name` promoted to required) rather than a\n * separate union member — see the section note above. The index signature keeps\n * it a `Record<string, unknown>` so it drops into the same untyped `parts`\n * arrays every other stored part rides in, and lets the `type`-driven\n * constructors below build it without union-discriminant gymnastics.\n */\nexport interface ChatAttachmentPart {\n type: ChatAttachmentKind\n path: string\n name: string\n size?: number\n mediaType?: string\n turnId?: string\n [key: string]: unknown\n}\n\n/** True for any `image`/`file` part carrying a non-empty string `path`.\n * DELIBERATELY also matches a sidecar path-bearing file part (a mention-style\n * `file` part with only `path`) — the guard is a structural attachment shape,\n * not proof of provenance; a caller that needs to exclude those filters\n * upstream. The `path.length > 0` check diverges from gtm's original guard\n * (which only checked `typeof`), mirroring sibling {@link isChatMentionPart}:\n * read-side robustness against a stored `{type:'file', path:''}` row — which\n * would otherwise render a malformed `(vault: )` pointer line — justifies the\n * small divergence from byte-for-byte gtm parity here; no legitimate write\n * path ever produces an empty `path`. */\nexport function isChatAttachmentPart(part: unknown): part is ChatAttachmentPart {\n if (!part || typeof part !== 'object') return false\n const record = part as Record<string, unknown>\n return (\n (record.type === 'image' || record.type === 'file') &&\n typeof record.path === 'string' &&\n record.path.length > 0\n )\n}\n\n/** Drop absent/empty optional fields rather than persisting `undefined`/`''`\n * — keeps stored parts minimal. */\nexport function attachmentInputToPart(input: ChatAttachmentInput): ChatAttachmentPart {\n const part: ChatAttachmentPart = { type: input.kind, path: input.path, name: input.name }\n if (typeof input.size === 'number' && Number.isFinite(input.size)) part.size = input.size\n if (input.mediaType) part.mediaType = input.mediaType\n return part\n}\n\n/** Every attachment part on one message's stored `parts`, in stored order. */\nexport function attachmentPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | null | undefined,\n): ChatAttachmentPart[] {\n if (!parts) return []\n return parts.filter(isChatAttachmentPart)\n}\n\n/** gtm's exact default header for {@link buildAttachmentPromptBlock} — kept\n * byte-identical because it feeds dispatched prompts (gtm-agent#618\n * reproduces the prompt bytes through this module). Override only with intent. */\nexport const DEFAULT_ATTACHMENT_PROMPT_HEADER =\n 'Attached files (already saved to the workspace vault — read them from these paths):'\n\n/** Same C0-control/DEL sweep as `/chat-routes`'s `resolve-attachments.ts`\n * (kept as a separate literal rather than a shared import — this module is\n * the read side and must stay usable over PERSISTED rows the wire validator\n * never saw). Replaced with a single space, not stripped or collapsed: this\n * is defense-in-depth for legacy/corrupt stored rows that bypassed wire\n * validation, not the primary control — a name/path a `resolveChatAttachments`\n * call validated today never contains one, so this is a no-op on legitimate\n * input and gtm byte-compat holds. */\nconst PROMPT_BLOCK_CONTROL_CHARS = /[\\x00-\\x1F\\x7F]/g\n\n/**\n * The agent-facing pointer block appended to the dispatched prompt — never\n * persisted in `message.content`. Empty array → `''` so callers can append\n * unconditionally. The sole producer of this text: both the current turn's\n * dispatch and history projection ({@link historyContentWithAttachments}) build\n * it from here, so the two can never drift. `header` overrides the first line\n * ({@link DEFAULT_ATTACHMENT_PROMPT_HEADER}); the per-file line format is fixed.\n *\n * Every interpolated `name`/`path` is swept for control characters first\n * (newline/carriage-return/tab included) and each one replaced with a single\n * space — a single-line-per-file guarantee. Without it, a control character\n * riding through on a PERSISTED row (`historyContentWithAttachments` reads\n * whatever is already stored; a legacy or hand-edited row was never re-checked\n * against `resolveChatAttachments`) could inject extra lines — fabricated\n * pointer entries or instructions — into a prompt the agent trusts verbatim.\n * The wire-side validator is the primary control; this is the backstop for\n * rows it never saw.\n */\nexport function buildAttachmentPromptBlock(\n atts: ReadonlyArray<Pick<ChatAttachmentPart, 'name' | 'path'>>,\n header: string = DEFAULT_ATTACHMENT_PROMPT_HEADER,\n): string {\n if (atts.length === 0) return ''\n const clean = (value: string) => value.replace(PROMPT_BLOCK_CONTROL_CHARS, ' ')\n const lines = atts.map((a) => `- ${clean(a.name)} (vault: ${clean(a.path)})`)\n return `\\n\\n${header}\\n${lines.join('\\n')}`\n}\n\n/**\n * History projection for a persisted message: `content` unchanged when it\n * carries no attachment parts, else the block is appended once. The\n * no-double-annotation guarantee holds under the invariant that the block is\n * never persisted INTO `message.content` — it is appended only at read time,\n * by this function and the current turn's dispatch, both sourcing from\n * `attachmentPartsFromMessageParts` — never by content inspection. This\n * function does not (and cannot) detect an already-embedded block by scanning\n * `content` for its text: a message whose stored `content` happens to already\n * contain the block's bytes, alongside attachment parts, still gets the block\n * appended a second time. See the pinned test for that boundary.\n */\nexport function historyContentWithAttachments(\n message: {\n content: string\n parts?: ReadonlyArray<Record<string, unknown>> | null\n },\n header?: string,\n): string {\n const attachmentParts = attachmentPartsFromMessageParts(message.parts)\n if (attachmentParts.length === 0) return message.content\n return `${message.content}${buildAttachmentPromptBlock(attachmentParts, header)}`\n}\n"],"mappings":"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","names":[]}