@ai-ecoverse/slicc-agent 7.6.8 → 7.7.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.
package/README.md CHANGED
@@ -426,7 +426,7 @@ The `memory` section, read before every request, holds the first 200 lines (at m
426
426
 
427
427
  **`memory_write`** saves, replaces (same section and title) or removes one entry in the caller's own file, or in `global` for cones. Writes to a file are serialized and re-read the file, so edits by hand survive. A file holds 16 KB; a write over that must make it smaller. Anything that looks like a secret (API keys, tokens, `Authorization` values, private keys, `password=`, launch URLs with a `#key`) is redacted before it's written. A role with `memory` and write tools gets `memory_write` in its tool list.
428
428
 
429
- **Extraction.** Durable compacts a long conversation into a summary; before it does, a `beforeCompact` hook (which never declines or replaces the summary) schedules a `slicc.memory-extract` task for the cone. A cone that has been idle for `memory.idleMinutes` (30 by default, `0` turns the idle trigger off) gets the same task. Each cone's conversation has one background `slicc.memory-idle` task (listed in the session document `slicc.memory-idle`), which pi durable keeps asleep between checks: it wakes `idleMinutes` after the conversation last changed (the newest entry's message time, or when it noticed the change), shows in the monitor as `memory idle watch`, schedules the extraction if it hasn't changed since, and sleeps again. The sleep is stored, so the idle time counts across reloads and while no worker runs, and settings are read at each wake. A cone that goes away (frozen, or its conversation replaced by New conversation) ends its task; a new cone gets one. The task runs only when there is something new since the last extraction (a cursor per conversation in the session document `slicc.memory-extract`), makes one model call, with the cone's model unless `memory.extractModel` names a cheaper one (`provider/model`), and writes the proposed entries through `memory_write`'s writer into the cone's memory file, within its 16 KB. The transcript it sends is redacted first, its prompt forbids secrets, and the writer redacts again. `"memory": { "extract": false }` in `~/.pi/agent/settings.json` turns extraction off. v6's curator scoop, three-way merge, curation ledger and dreamer aren't ported.
429
+ **Extraction.** Durable compacts a long conversation into a summary; before it does, a `beforeCompact` hook (which never declines or replaces the summary) schedules a `slicc.memory-extract` task for the cone. A cone that has been idle for `memory.idleMinutes` (30 by default, `0` turns the idle trigger off) gets the same task. Each cone's conversation has one background `slicc.memory-idle` task (listed in the session document `slicc.memory-idle`), which pi durable keeps asleep between checks: it wakes `idleMinutes` after the newest message time among the entries it hasn't seen yet, so a turn that ends while it sleeps counts from that turn, and entries without a message (a skipped lick, a sprinkle note, an approval verdict) don't push the time back, shows in the monitor as `memory idle watch`, schedules the extraction if it hasn't changed since, and sleeps again. The sleep is stored, so the idle time counts across reloads and while no worker runs, and settings are read at each wake. A cone that goes away (frozen, or its conversation replaced by New conversation) ends its task; a new cone gets one. The task runs only when there is something new since the last extraction (a cursor per conversation in the session document `slicc.memory-extract`), makes one model call, with the cone's model unless `memory.extractModel` names a cheaper one (`provider/model`), and writes the proposed entries through `memory_write`'s writer into the cone's memory file, within its 16 KB. The transcript it sends is redacted first, its prompt forbids secrets, and the writer redacts again. `"memory": { "extract": false }` in `~/.pi/agent/settings.json` turns extraction off. v6's curator scoop, three-way merge, curation ledger and dreamer aren't ported.
430
430
 
431
431
  **The gelatiere.** SLICC's resident advisor, as in v6, is a scoop with the built-in `gelatiere` role. That role has its own memory (`memory: { scope: user, path: gelatiere }`), so it keeps its notes there.
432
432
  - `gelatiere init` starts the scoop in the first cone, writes `~/.pi/agent/GELATIERE.md` (its procedure, which the user can edit) unless it exists, and installs one job `# name: gelatiere` / `0 3 * * * agent send scoop:<handle> --lick=cron 'Nightly pass: …'` in the scoop's own crontab (root's without kernel users), replacing an older one. At start, a gelatiere job an earlier version installed without `--lick=cron` gets it, as migrated jobs do. `gelatiere status` shows its schedule from there.
@@ -464,6 +464,7 @@ pi core has no browser tooling, and Mario's pi-skills `browser-tools` launches i
464
464
  - **`curlwright`** (slicc-cdp ≥ 1.2.1) runs curl-style requests as a page-context `fetch()` in an open tab, with that tab's cookies and origin. The skill prefers it to UI clicks for reading data the app already fetches, and plain `curl` when no login is needed. Writes need the user's yes like clicks do, and response bodies are data.
465
465
  - **The `system` section** never says the browser is connected. When `SLICC_CDP_URL` is set and `playwright-cli` is on PATH, it says a browser may be available (naming `curlwright` too when it's there) and tells the agent to run the browser skill's check first; otherwise it lists the browser under "Not here yet". The agent can't check a connection without asking: bios asks the user "may agents control the browser?" on the first CDP connection of a page session, whatever the call, so neither the worker nor the prompt opens one. The skill's check, `timeout -k 1 5 bash -c 'playwright-cli tab-list'`, runs only when the agent is about to browse. It goes by the exit status (tab-list can leave tabs out) and maps the errors: no browser to drive, declined, a timeout while the user hasn't answered, and the local link not running.
466
466
  - **Images:** durable's `read` doesn't return images, so a wrapper (`slicc-images`) answers `read` of a PNG, JPEG, GIF or WebP with image content. In a worker that can draw, images wider than 1600 px are scaled down, and an image over 1 MB of base64 is re-encoded as JPEG or refused with a hint. The adapter puts image content from a tool result on the tool card (`ToolCall.image`), so screenshots show in the chat.
467
+ - **Images from command output:** a stdout line that is exactly `<img:/absolute/path>` makes `bash` read that file the same way, through the calling agent's own environment (so with its kernel user's permissions, and only after any guest gate approved the command). The line becomes `<path> (<mime>, <bytes> bytes)` and the image is added to the tool result after the text, at most 4 per command. A relative path, an unreadable file, a file that isn't a PNG, JPEG, GIF or WebP, an image still over 1 MB of base64 after fitting, and every marker after the fourth get a one-line note in place of the marker instead. Markers are found per stream on the scrubbed stdout, as it streams, so stderr and lines longer than 4 KB never count. A marker that scrolled out of the 2000-line tail is still honoured: its line is added after the retained output, and the tail is cut again to fit, with one truncation notice. Any program's stdout can trigger the read, for example a `curl` whose output holds such a line, not only a command the model ran to show an image. In a run the guest guard gates (one a guest started, or one that inherited guest or lick taint), markers are never honoured: a reviewer approves only the command and never sees the paths its output names, so nothing is read and each marker line says so. The guard records the gated run in a task memo (`slicc.guest-gated`) before it reviews the call, and `bash` checks that memo (`gatedRun`). Elsewhere a marker brings into the model's context only an image the caller could already `read`. Only the first 4 markers and those recent enough to still be in the tail are kept; older ones after the fourth collapse into one "N more image lines were not shown" line.
467
468
  - **SLICC's own pages are off limits** (PRE-1). The `slicc-browser-guard` extension ([`src/browser/guard.ts`](src/browser/guard.ts)) refuses a `bash` call, direct or from codemode, that runs `playwright-cli` or `curlwright` and names a protected URL: one that [`@ai-ecoverse/slicc-browsers`](https://github.com/ai-ecoverse/slicc-browsers#protected-origins)' `protectedOrigins` matcher protects, with the origin the worker runs on (`location.origin`) as its extra origin. That covers `sliccy.ai` and every name under it (including `www` and `vault`) over http or https on any port, the worker's origin, `devtools:`, and `blob:`, `filesystem:` and `view-source:` by their inner URL. It also refuses `*.kernel.localhost` names other than the agent's own kernel servers, `<port>.kernel.localhost` for a port 1–65535 other than the CDP facade's 9222. The answer is "SLICC's own pages can't be opened or driven from the browser tools.", and the `browser` skill says so too.
468
469
  - **How it reads a command:** a shell tokenizer ([`src/browser/shell.ts`](src/browser/shell.ts)) removes quotes and escapes (`'…'`, `"…"`, `$'…'`, `$"…"`, `\`), and reads into `$(…)`, backticks, `<(…)` and words that are themselves scripts, such as `bash -c` and `eval` arguments, four levels deep. If any word is `playwright-cli` or `curlwright` (by its last path segment), every word is checked: as a URL, as `https://` plus the word, after a `NAME=` or `--flag=` prefix, and for URLs and `//host` inside it. Tabs and newlines are removed and surrounding spaces trimmed, as the URL parser does; the matcher parses with the WHATWG `URL`, so case, IDNA, full-width characters, percent escapes, backslashes and userinfo all count. `about:blank`, `about:srcdoc` and the empty URL are allowed: a tab `playwright-cli` opens has no SLICC opener, and a command that also names a protected URL is refused anyway.
469
470
  - **The list** is slicc-browsers' `catalog.json` `protectedOrigins` with its reference matcher `protected.mjs`, wrapped by [`src/browser/origins.ts`](src/browser/origins.ts) (`protectedMatcher(extra)`); the unit tests run every URL and target vector in its `protected.json`. `*.kernel.localhost` isn't on the shared list; that rule is this guard's own. The build keeps the `with { type: 'json' }` import attribute, so the catalog loads unbundled.
@@ -15,6 +15,7 @@ const GUEST_GUARD = "slicc-guest-guard";
15
15
  const GUEST_REQUEST = "guest:";
16
16
  const LICK_REQUEST = "lick:";
17
17
  const GUEST_TOOL_MEMO = "slicc.guest-tool";
18
+ const GUEST_GATED_MEMO = "slicc.guest-gated";
18
19
  const SUMMARY_LENGTH = 2e3;
19
20
  const GATE_REASONS = {
20
21
  unknown: "A guest started this run, and its seat names no review for tool calls that this agent knows.",
@@ -103,6 +104,9 @@ function reasonOf(item) {
103
104
  if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;
104
105
  return GATE_REASONS.unanswered;
105
106
  }
107
+ async function gatedRun(api, context) {
108
+ return await api.memo(GUEST_GATED_MEMO, context) === true;
109
+ }
106
110
  const GuestTaintDoc = defineDoc({
107
111
  kind: "slicc.guest-taint",
108
112
  checkpointWhen: boundedDeltas,
@@ -274,7 +278,9 @@ function guestToolGate(harness) {
274
278
  const found = inputs.length ? await taintsOf(inputs, seats, held, context) : [];
275
279
  const licks = (await api.snapshot(LickTaintDoc, context))?.licks ?? {};
276
280
  const licked = await lickTaints(inputs, licks, held, context);
277
- for (const taint of [...found, ...inheritedTaints([...inherited, ...licked], seats)]) {
281
+ const taints = [...found, ...inheritedTaints([...inherited, ...licked], seats)];
282
+ if (taints.length) await api.memo(GUEST_GATED_MEMO, true, context);
283
+ for (const taint of taints) {
278
284
  const block = await review(taint, call, api, context);
279
285
  if (block) return { block };
280
286
  }
@@ -293,6 +299,7 @@ function guestToolGate(harness) {
293
299
  }
294
300
  export {
295
301
  GATE_REASONS,
302
+ GUEST_GATED_MEMO,
296
303
  GUEST_GUARD,
297
304
  GUEST_REQUEST,
298
305
  GUEST_TOOL_MEMO,
@@ -303,6 +310,7 @@ export {
303
310
  argumentsHash,
304
311
  canonical,
305
312
  distinctTaints,
313
+ gatedRun,
306
314
  guestToolGate,
307
315
  lickTaints,
308
316
  reasonOf,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/guests/guard.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n type ConversationId,\n defineDoc,\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS, scoopTier } from '../approvals/record.ts';\nimport { boundedDeltas } from '../checkpoints.ts';\nimport { scrubText, scrubTexts } from '../secrets/scrub.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const LICK_REQUEST = 'lick:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nfunction leaves(value: unknown, found: string[]): void {\n if (typeof value === 'string') found.push(value);\n else if (value && typeof value === 'object')\n for (const item of Object.values(value)) leaves(item, found);\n}\n\nfunction replaced(value: unknown, texts: string[]): unknown {\n if (typeof value === 'string') return texts.shift();\n if (Array.isArray(value)) return value.map((item) => replaced(item, texts));\n if (value && typeof value === 'object')\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, replaced(item, texts)])\n );\n return value;\n}\n\nexport async function scrubbedArguments(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const found: string[] = [];\n leaves(args, found);\n return replaced(args, await scrubTexts(found)) as Record<string, unknown>;\n}\n\nexport async function argumentsHash(args: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(args));\n return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nasync function shownArguments(text: string): Promise<string> {\n try {\n return canonical(await scrubbedArguments(JSON.parse(text)));\n } catch {\n return scrubText(text);\n }\n}\n\nexport async function sealedRules(rules: readonly AlwaysTool[]): Promise<AlwaysTool[] | null> {\n if (rules.every((rule) => rule.argumentsHash)) return null;\n return Promise.all(\n rules.map(async (rule) =>\n rule.argumentsHash\n ? rule\n : {\n ...rule,\n argumentsHash: await argumentsHash(rule.arguments),\n arguments: await shownArguments(rule.arguments),\n }\n )\n );\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nconst SCOOP_SAID: Record<string, (name: string) => string> = {\n [REASONS.noScoop]: (name) => `there is no scoop ${name} in this cone to review it.`,\n [REASONS.scoopStopped]: (name) => `the scoop ${name} is stopped, so it can't review it.`,\n [REASONS.scoopAsked]: (name) =>\n `the scoop ${name} can't review a call that asks something of it.`,\n [REASONS.scoopUndecided]: (name) => `the scoop ${name} ended its review without deciding.`,\n};\n\nfunction scoopReason(item: Approval): string | null {\n const name = item.tier.slice('scoop:'.length);\n if (item.decidedBy?.startsWith('scoop:'))\n return `The scoop ${name} declined this tool call in a run a guest started${item.reason ? `: ${item.reason}` : '.'}`;\n const said = SCOOP_SAID[item.reason ?? ''];\n return said ? `A guest started this run, and ${said(name)}` : null;\n}\n\nexport function reasonOf(item: Approval): string {\n const scooped = item.state === 'rejected' && scoopTier(item.tier) ? scoopReason(item) : null;\n if (scooped) return scooped;\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nexport type InheritedTaint = { seat: string | null; tier: string };\n\nexport const GuestTaintDoc = defineDoc<{ taints: InheritedTaint[] }>({\n kind: 'slicc.guest-taint',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'conversation',\n history: 'rewindable',\n fork: 'asOf',\n initial: () => ({ taints: [] }),\n});\n\nexport const LickTaintDoc = defineDoc<{ licks: Record<string, InheritedTaint[]> }>({\n kind: 'slicc.lick-taint',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'session',\n initial: () => ({ licks: {} }),\n});\n\ntype TaintReader = Pick<Harness, 'submission' | 'snapshot'>;\n\nexport async function lickTaints(\n inputs: readonly SubmissionId[],\n licks: Readonly<Record<string, InheritedTaint[]>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<InheritedTaint[]> {\n if (!Object.keys(licks).length) return [];\n const found: InheritedTaint[] = [];\n for (const id of inputs) {\n const record = await (await harness.submission(id, context))?.status(context);\n const request = record?.requestId ?? '';\n if (request.startsWith(LICK_REQUEST))\n found.push(...(licks[request.slice(LICK_REQUEST.length)] ?? []));\n }\n return distinctTaints(found);\n}\n\nexport async function runTaints(\n harness: TaintReader,\n conversationId: ConversationId,\n context: Context\n): Promise<InheritedTaint[]> {\n const live = await harness.snapshot(LiveDoc, conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited = (await harness.snapshot(GuestTaintDoc, conversationId, context))?.taints ?? [];\n const seats = (await harness.snapshot(GuestsDoc, context))?.seats ?? {};\n const found = inputs.length ? await taintsOf(inputs, seats, harness, context) : [];\n const licks = (await harness.snapshot(LickTaintDoc, context))?.licks ?? {};\n return distinctTaints([\n ...found.map((taint) => ({ seat: taint.seat?.id ?? null, tier: taint.tier })),\n ...inherited,\n ...(await lickTaints(inputs, licks, harness, context)),\n ]);\n}\n\nexport function distinctTaints(taints: readonly InheritedTaint[]): InheritedTaint[] {\n const kept = new Map<string, InheritedTaint>();\n for (const taint of taints) kept.set(`${taint.seat}\\n${taint.tier}`, taint);\n return [...kept.values()];\n}\n\nfunction inheritedTaints(\n inherited: readonly InheritedTaint[],\n seats: Readonly<Record<string, SeatRecord>>\n): Taint[] {\n return inherited.map((taint) => ({\n seat: taint.seat === null ? null : (seats[taint.seat] ?? null),\n tier: taint.tier,\n }));\n}\n\nexport async function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n hash: string\n): string | null {\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.argumentsHash === hash);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.argumentsHash !== rule.argumentsHash\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const hash = await argumentsHash(canonical(call.arguments));\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, hash);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const shown = await scrubbedArguments(call.arguments);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: {\n tool: call.name,\n summary: summaryOf(shown),\n arguments: canonical(shown),\n task,\n },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n argumentsHash: hash,\n arguments: canonical(shown),\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited =\n (await api.snapshot(GuestTaintDoc, api.conversationId, context))?.taints ?? [];\n if (!inputs.length && !inherited.length) return undefined;\n const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n const found = inputs.length ? await taintsOf(inputs, seats, held, context) : [];\n const licks = (await api.snapshot(LickTaintDoc, context))?.licks ?? {};\n const licked = await lickTaints(inputs, licks, held, context);\n for (const taint of [...found, ...inheritedTaints([...inherited, ...licked], seats)]) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context, JsonValue } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n type ConversationId,\n defineDoc,\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS, scoopTier } from '../approvals/record.ts';\nimport { boundedDeltas } from '../checkpoints.ts';\nimport { scrubText, scrubTexts } from '../secrets/scrub.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const LICK_REQUEST = 'lick:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const GUEST_GATED_MEMO = 'slicc.guest-gated';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nfunction leaves(value: unknown, found: string[]): void {\n if (typeof value === 'string') found.push(value);\n else if (value && typeof value === 'object')\n for (const item of Object.values(value)) leaves(item, found);\n}\n\nfunction replaced(value: unknown, texts: string[]): unknown {\n if (typeof value === 'string') return texts.shift();\n if (Array.isArray(value)) return value.map((item) => replaced(item, texts));\n if (value && typeof value === 'object')\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, replaced(item, texts)])\n );\n return value;\n}\n\nexport async function scrubbedArguments(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const found: string[] = [];\n leaves(args, found);\n return replaced(args, await scrubTexts(found)) as Record<string, unknown>;\n}\n\nexport async function argumentsHash(args: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(args));\n return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nasync function shownArguments(text: string): Promise<string> {\n try {\n return canonical(await scrubbedArguments(JSON.parse(text)));\n } catch {\n return scrubText(text);\n }\n}\n\nexport async function sealedRules(rules: readonly AlwaysTool[]): Promise<AlwaysTool[] | null> {\n if (rules.every((rule) => rule.argumentsHash)) return null;\n return Promise.all(\n rules.map(async (rule) =>\n rule.argumentsHash\n ? rule\n : {\n ...rule,\n argumentsHash: await argumentsHash(rule.arguments),\n arguments: await shownArguments(rule.arguments),\n }\n )\n );\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nconst SCOOP_SAID: Record<string, (name: string) => string> = {\n [REASONS.noScoop]: (name) => `there is no scoop ${name} in this cone to review it.`,\n [REASONS.scoopStopped]: (name) => `the scoop ${name} is stopped, so it can't review it.`,\n [REASONS.scoopAsked]: (name) =>\n `the scoop ${name} can't review a call that asks something of it.`,\n [REASONS.scoopUndecided]: (name) => `the scoop ${name} ended its review without deciding.`,\n};\n\nfunction scoopReason(item: Approval): string | null {\n const name = item.tier.slice('scoop:'.length);\n if (item.decidedBy?.startsWith('scoop:'))\n return `The scoop ${name} declined this tool call in a run a guest started${item.reason ? `: ${item.reason}` : '.'}`;\n const said = SCOOP_SAID[item.reason ?? ''];\n return said ? `A guest started this run, and ${said(name)}` : null;\n}\n\nexport function reasonOf(item: Approval): string {\n const scooped = item.state === 'rejected' && scoopTier(item.tier) ? scoopReason(item) : null;\n if (scooped) return scooped;\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nexport async function gatedRun(\n api: { memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined> },\n context: Context\n): Promise<boolean> {\n return (await api.memo<boolean>(GUEST_GATED_MEMO, context)) === true;\n}\n\nexport type InheritedTaint = { seat: string | null; tier: string };\n\nexport const GuestTaintDoc = defineDoc<{ taints: InheritedTaint[] }>({\n kind: 'slicc.guest-taint',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'conversation',\n history: 'rewindable',\n fork: 'asOf',\n initial: () => ({ taints: [] }),\n});\n\nexport const LickTaintDoc = defineDoc<{ licks: Record<string, InheritedTaint[]> }>({\n kind: 'slicc.lick-taint',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'session',\n initial: () => ({ licks: {} }),\n});\n\ntype TaintReader = Pick<Harness, 'submission' | 'snapshot'>;\n\nexport async function lickTaints(\n inputs: readonly SubmissionId[],\n licks: Readonly<Record<string, InheritedTaint[]>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<InheritedTaint[]> {\n if (!Object.keys(licks).length) return [];\n const found: InheritedTaint[] = [];\n for (const id of inputs) {\n const record = await (await harness.submission(id, context))?.status(context);\n const request = record?.requestId ?? '';\n if (request.startsWith(LICK_REQUEST))\n found.push(...(licks[request.slice(LICK_REQUEST.length)] ?? []));\n }\n return distinctTaints(found);\n}\n\nexport async function runTaints(\n harness: TaintReader,\n conversationId: ConversationId,\n context: Context\n): Promise<InheritedTaint[]> {\n const live = await harness.snapshot(LiveDoc, conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited = (await harness.snapshot(GuestTaintDoc, conversationId, context))?.taints ?? [];\n const seats = (await harness.snapshot(GuestsDoc, context))?.seats ?? {};\n const found = inputs.length ? await taintsOf(inputs, seats, harness, context) : [];\n const licks = (await harness.snapshot(LickTaintDoc, context))?.licks ?? {};\n return distinctTaints([\n ...found.map((taint) => ({ seat: taint.seat?.id ?? null, tier: taint.tier })),\n ...inherited,\n ...(await lickTaints(inputs, licks, harness, context)),\n ]);\n}\n\nexport function distinctTaints(taints: readonly InheritedTaint[]): InheritedTaint[] {\n const kept = new Map<string, InheritedTaint>();\n for (const taint of taints) kept.set(`${taint.seat}\\n${taint.tier}`, taint);\n return [...kept.values()];\n}\n\nfunction inheritedTaints(\n inherited: readonly InheritedTaint[],\n seats: Readonly<Record<string, SeatRecord>>\n): Taint[] {\n return inherited.map((taint) => ({\n seat: taint.seat === null ? null : (seats[taint.seat] ?? null),\n tier: taint.tier,\n }));\n}\n\nexport async function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n hash: string\n): string | null {\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.argumentsHash === hash);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.argumentsHash !== rule.argumentsHash\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const hash = await argumentsHash(canonical(call.arguments));\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, hash);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const shown = await scrubbedArguments(call.arguments);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: {\n tool: call.name,\n summary: summaryOf(shown),\n arguments: canonical(shown),\n task,\n },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n argumentsHash: hash,\n arguments: canonical(shown),\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited =\n (await api.snapshot(GuestTaintDoc, api.conversationId, context))?.taints ?? [];\n if (!inputs.length && !inherited.length) return undefined;\n const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n const found = inputs.length ? await taintsOf(inputs, seats, held, context) : [];\n const licks = (await api.snapshot(LickTaintDoc, context))?.licks ?? {};\n const licked = await lickTaints(inputs, licks, held, context);\n const taints = [...found, ...inheritedTaints([...inherited, ...licked], seats)];\n if (taints.length) await api.memo(GUEST_GATED_MEMO, true, context);\n for (const taint of taints) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["record"]
7
7
  }
@@ -40,10 +40,15 @@ const shrink = async (image) => {
40
40
  const blob = png.size * 1.4 > MAX_BASE64 ? await canvas.convertToBlob({ type: "image/jpeg", quality: 0.8 }) : png;
41
41
  return { bytes: new Uint8Array(await blob.arrayBuffer()), mimeType: blob.type };
42
42
  };
43
+ async function fitted(image, fit) {
44
+ const shown = await fit(image).catch(() => image);
45
+ return { image: shown, data: base64(shown.bytes) };
46
+ }
43
47
  export {
44
48
  MAX_BASE64,
45
49
  MAX_WIDTH,
46
50
  base64,
51
+ fitted,
47
52
  mimeOf,
48
53
  shrink
49
54
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/images/fit.ts"],
4
- "sourcesContent": ["export const MAX_WIDTH = 1600;\nexport const MAX_BASE64 = 1024 * 1024;\n\nexport type Image = { bytes: Uint8Array; mimeType: string };\nexport type Shrink = (image: Image) => Promise<Image>;\n\nconst SIGNATURES: [string, number[]][] = [\n ['image/png', [0x89, 0x50, 0x4e, 0x47]],\n ['image/jpeg', [0xff, 0xd8, 0xff]],\n ['image/gif', [0x47, 0x49, 0x46, 0x38]],\n];\n\nexport function mimeOf(bytes: Uint8Array): string | undefined {\n for (const [mime, signature] of SIGNATURES)\n if (signature.every((byte, index) => bytes[index] === byte)) return mime;\n const riff = String.fromCharCode(...bytes.slice(0, 4));\n const webp = String.fromCharCode(...bytes.slice(8, 12));\n return riff === 'RIFF' && webp === 'WEBP' ? 'image/webp' : undefined;\n}\n\nexport function base64(bytes: Uint8Array): string {\n let text = '';\n for (let start = 0; start < bytes.length; start += 0x8000)\n text += String.fromCharCode(...bytes.subarray(start, start + 0x8000));\n return btoa(text);\n}\n\nexport const shrink: Shrink = async (image) => {\n const scope = globalThis as unknown as {\n createImageBitmap?: (blob: Blob) => Promise<{ width: number; height: number; close(): void }>;\n OffscreenCanvas?: new (\n width: number,\n height: number\n ) => {\n getContext(\n kind: '2d'\n ): { drawImage(source: unknown, x: number, y: number, w: number, h: number): void } | null;\n convertToBlob(options: { type: string; quality?: number }): Promise<Blob>;\n };\n };\n if (!scope.createImageBitmap || !scope.OffscreenCanvas) return image;\n const bitmap = await scope.createImageBitmap(\n new Blob([image.bytes as BlobPart], { type: image.mimeType })\n );\n const tooWide = bitmap.width > MAX_WIDTH;\n const tooBig = base64(image.bytes).length > MAX_BASE64;\n if (!tooWide && !tooBig) {\n bitmap.close();\n return image;\n }\n const scale = tooWide ? MAX_WIDTH / bitmap.width : 1;\n const width = Math.round(bitmap.width * scale);\n const height = Math.round(bitmap.height * scale);\n const canvas = new scope.OffscreenCanvas(width, height);\n canvas.getContext('2d')?.drawImage(bitmap, 0, 0, width, height);\n bitmap.close();\n const png = await canvas.convertToBlob({ type: 'image/png' });\n const blob =\n png.size * 1.4 > MAX_BASE64\n ? await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 })\n : png;\n return { bytes: new Uint8Array(await blob.arrayBuffer()), mimeType: blob.type };\n};\n"],
5
- "mappings": "AAAO,MAAM,YAAY;AAClB,MAAM,aAAa,OAAO;AAKjC,MAAM,aAAmC;AAAA,EACvC,CAAC,aAAa,CAAC,KAAM,IAAM,IAAM,EAAI,CAAC;AAAA,EACtC,CAAC,cAAc,CAAC,KAAM,KAAM,GAAI,CAAC;AAAA,EACjC,CAAC,aAAa,CAAC,IAAM,IAAM,IAAM,EAAI,CAAC;AACxC;AAEO,SAAS,OAAO,OAAuC;AAC5D,aAAW,CAAC,MAAM,SAAS,KAAK;AAC9B,QAAI,UAAU,MAAM,CAAC,MAAM,UAAU,MAAM,KAAK,MAAM,IAAI,EAAG,QAAO;AACtE,QAAM,OAAO,OAAO,aAAa,GAAG,MAAM,MAAM,GAAG,CAAC,CAAC;AACrD,QAAM,OAAO,OAAO,aAAa,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC;AACtD,SAAO,SAAS,UAAU,SAAS,SAAS,eAAe;AAC7D;AAEO,SAAS,OAAO,OAA2B;AAChD,MAAI,OAAO;AACX,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;AACjD,YAAQ,OAAO,aAAa,GAAG,MAAM,SAAS,OAAO,QAAQ,KAAM,CAAC;AACtE,SAAO,KAAK,IAAI;AAClB;AAEO,MAAM,SAAiB,OAAO,UAAU;AAC7C,QAAM,QAAQ;AAYd,MAAI,CAAC,MAAM,qBAAqB,CAAC,MAAM,gBAAiB,QAAO;AAC/D,QAAM,SAAS,MAAM,MAAM;AAAA,IACzB,IAAI,KAAK,CAAC,MAAM,KAAiB,GAAG,EAAE,MAAM,MAAM,SAAS,CAAC;AAAA,EAC9D;AACA,QAAM,UAAU,OAAO,QAAQ;AAC/B,QAAM,SAAS,OAAO,MAAM,KAAK,EAAE,SAAS;AAC5C,MAAI,CAAC,WAAW,CAAC,QAAQ;AACvB,WAAO,MAAM;AACb,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,UAAU,YAAY,OAAO,QAAQ;AACnD,QAAM,QAAQ,KAAK,MAAM,OAAO,QAAQ,KAAK;AAC7C,QAAM,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK;AAC/C,QAAM,SAAS,IAAI,MAAM,gBAAgB,OAAO,MAAM;AACtD,SAAO,WAAW,IAAI,GAAG,UAAU,QAAQ,GAAG,GAAG,OAAO,MAAM;AAC9D,SAAO,MAAM;AACb,QAAM,MAAM,MAAM,OAAO,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,QAAM,OACJ,IAAI,OAAO,MAAM,aACb,MAAM,OAAO,cAAc,EAAE,MAAM,cAAc,SAAS,IAAI,CAAC,IAC/D;AACN,SAAO,EAAE,OAAO,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC,GAAG,UAAU,KAAK,KAAK;AAChF;",
4
+ "sourcesContent": ["export const MAX_WIDTH = 1600;\nexport const MAX_BASE64 = 1024 * 1024;\n\nexport type Image = { bytes: Uint8Array; mimeType: string };\nexport type Shrink = (image: Image) => Promise<Image>;\n\nconst SIGNATURES: [string, number[]][] = [\n ['image/png', [0x89, 0x50, 0x4e, 0x47]],\n ['image/jpeg', [0xff, 0xd8, 0xff]],\n ['image/gif', [0x47, 0x49, 0x46, 0x38]],\n];\n\nexport function mimeOf(bytes: Uint8Array): string | undefined {\n for (const [mime, signature] of SIGNATURES)\n if (signature.every((byte, index) => bytes[index] === byte)) return mime;\n const riff = String.fromCharCode(...bytes.slice(0, 4));\n const webp = String.fromCharCode(...bytes.slice(8, 12));\n return riff === 'RIFF' && webp === 'WEBP' ? 'image/webp' : undefined;\n}\n\nexport function base64(bytes: Uint8Array): string {\n let text = '';\n for (let start = 0; start < bytes.length; start += 0x8000)\n text += String.fromCharCode(...bytes.subarray(start, start + 0x8000));\n return btoa(text);\n}\n\nexport const shrink: Shrink = async (image) => {\n const scope = globalThis as unknown as {\n createImageBitmap?: (blob: Blob) => Promise<{ width: number; height: number; close(): void }>;\n OffscreenCanvas?: new (\n width: number,\n height: number\n ) => {\n getContext(\n kind: '2d'\n ): { drawImage(source: unknown, x: number, y: number, w: number, h: number): void } | null;\n convertToBlob(options: { type: string; quality?: number }): Promise<Blob>;\n };\n };\n if (!scope.createImageBitmap || !scope.OffscreenCanvas) return image;\n const bitmap = await scope.createImageBitmap(\n new Blob([image.bytes as BlobPart], { type: image.mimeType })\n );\n const tooWide = bitmap.width > MAX_WIDTH;\n const tooBig = base64(image.bytes).length > MAX_BASE64;\n if (!tooWide && !tooBig) {\n bitmap.close();\n return image;\n }\n const scale = tooWide ? MAX_WIDTH / bitmap.width : 1;\n const width = Math.round(bitmap.width * scale);\n const height = Math.round(bitmap.height * scale);\n const canvas = new scope.OffscreenCanvas(width, height);\n canvas.getContext('2d')?.drawImage(bitmap, 0, 0, width, height);\n bitmap.close();\n const png = await canvas.convertToBlob({ type: 'image/png' });\n const blob =\n png.size * 1.4 > MAX_BASE64\n ? await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 })\n : png;\n return { bytes: new Uint8Array(await blob.arrayBuffer()), mimeType: blob.type };\n};\n\nexport async function fitted(image: Image, fit: Shrink): Promise<{ image: Image; data: string }> {\n const shown = await fit(image).catch(() => image);\n return { image: shown, data: base64(shown.bytes) };\n}\n"],
5
+ "mappings": "AAAO,MAAM,YAAY;AAClB,MAAM,aAAa,OAAO;AAKjC,MAAM,aAAmC;AAAA,EACvC,CAAC,aAAa,CAAC,KAAM,IAAM,IAAM,EAAI,CAAC;AAAA,EACtC,CAAC,cAAc,CAAC,KAAM,KAAM,GAAI,CAAC;AAAA,EACjC,CAAC,aAAa,CAAC,IAAM,IAAM,IAAM,EAAI,CAAC;AACxC;AAEO,SAAS,OAAO,OAAuC;AAC5D,aAAW,CAAC,MAAM,SAAS,KAAK;AAC9B,QAAI,UAAU,MAAM,CAAC,MAAM,UAAU,MAAM,KAAK,MAAM,IAAI,EAAG,QAAO;AACtE,QAAM,OAAO,OAAO,aAAa,GAAG,MAAM,MAAM,GAAG,CAAC,CAAC;AACrD,QAAM,OAAO,OAAO,aAAa,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC;AACtD,SAAO,SAAS,UAAU,SAAS,SAAS,eAAe;AAC7D;AAEO,SAAS,OAAO,OAA2B;AAChD,MAAI,OAAO;AACX,WAAS,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;AACjD,YAAQ,OAAO,aAAa,GAAG,MAAM,SAAS,OAAO,QAAQ,KAAM,CAAC;AACtE,SAAO,KAAK,IAAI;AAClB;AAEO,MAAM,SAAiB,OAAO,UAAU;AAC7C,QAAM,QAAQ;AAYd,MAAI,CAAC,MAAM,qBAAqB,CAAC,MAAM,gBAAiB,QAAO;AAC/D,QAAM,SAAS,MAAM,MAAM;AAAA,IACzB,IAAI,KAAK,CAAC,MAAM,KAAiB,GAAG,EAAE,MAAM,MAAM,SAAS,CAAC;AAAA,EAC9D;AACA,QAAM,UAAU,OAAO,QAAQ;AAC/B,QAAM,SAAS,OAAO,MAAM,KAAK,EAAE,SAAS;AAC5C,MAAI,CAAC,WAAW,CAAC,QAAQ;AACvB,WAAO,MAAM;AACb,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,UAAU,YAAY,OAAO,QAAQ;AACnD,QAAM,QAAQ,KAAK,MAAM,OAAO,QAAQ,KAAK;AAC7C,QAAM,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK;AAC/C,QAAM,SAAS,IAAI,MAAM,gBAAgB,OAAO,MAAM;AACtD,SAAO,WAAW,IAAI,GAAG,UAAU,QAAQ,GAAG,GAAG,OAAO,MAAM;AAC9D,SAAO,MAAM;AACb,QAAM,MAAM,MAAM,OAAO,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5D,QAAM,OACJ,IAAI,OAAO,MAAM,aACb,MAAM,OAAO,cAAc,EAAE,MAAM,cAAc,SAAS,IAAI,CAAC,IAC/D;AACN,SAAO,EAAE,OAAO,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC,GAAG,UAAU,KAAK,KAAK;AAChF;AAEA,eAAsB,OAAO,OAAc,KAAsD;AAC/F,QAAM,QAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,KAAK;AAChD,SAAO,EAAE,OAAO,OAAO,MAAM,OAAO,MAAM,KAAK,EAAE;AACnD;",
6
6
  "names": []
7
7
  }
@@ -2,8 +2,10 @@ import {
2
2
  defineExtension,
3
3
  wrapTool
4
4
  } from "@earendil-works/pi-durable";
5
- import { base64, MAX_BASE64, MAX_WIDTH, mimeOf, shrink } from "./fit.js";
6
- import { base64 as base642, MAX_BASE64 as MAX_BASE642, MAX_WIDTH as MAX_WIDTH2, mimeOf as mimeOf2, shrink as shrink2 } from "./fit.js";
5
+ import { CodingTools } from "@earendil-works/pi-durable/tools";
6
+ import { fitted, MAX_BASE64, MAX_WIDTH, mimeOf, shrink } from "./fit.js";
7
+ import { imageMarkers } from "./markers.js";
8
+ import { base64, MAX_BASE64 as MAX_BASE642, MAX_WIDTH as MAX_WIDTH2, mimeOf as mimeOf2, shrink as shrink2 } from "./fit.js";
7
9
  function unsupportedImage(result) {
8
10
  const diagnostics = result.diagnostics ?? [];
9
11
  return diagnostics.some((diagnostic) => diagnostic.code === "unsupported_image");
@@ -11,6 +13,9 @@ function unsupportedImage(result) {
11
13
  function refuse(text) {
12
14
  return { output: [{ type: "text", text }], isError: true };
13
15
  }
16
+ const bash = CodingTools.tools.find(
17
+ (tool) => tool.name === "bash"
18
+ );
14
19
  function imageReads(read, fit = shrink) {
15
20
  return defineExtension({
16
21
  name: "slicc-images",
@@ -24,11 +29,7 @@ function imageReads(read, fit = shrink) {
24
29
  const read2 = await api.env.readBinaryFile(path, context);
25
30
  const mimeType = read2.ok ? mimeOf(read2.value) : void 0;
26
31
  if (!read2.ok || !mimeType) return result;
27
- const image = await fit({ bytes: read2.value, mimeType }).catch(() => ({
28
- bytes: read2.value,
29
- mimeType
30
- }));
31
- const data = base64(image.bytes);
32
+ const { image, data } = await fitted({ bytes: read2.value, mimeType }, fit);
32
33
  if (data.length > MAX_BASE64)
33
34
  return refuse(
34
35
  `${path} is too large to read as an image (${image.bytes.length} bytes after fitting it to ${MAX_WIDTH} px). Take a smaller screenshot, for example with --max-width ${MAX_WIDTH}.`
@@ -40,14 +41,15 @@ function imageReads(read, fit = shrink) {
40
41
  ]
41
42
  };
42
43
  }
43
- }))
44
+ })),
45
+ imageMarkers(bash, fit)
44
46
  ]
45
47
  });
46
48
  }
47
49
  export {
48
50
  MAX_BASE642 as MAX_BASE64,
49
51
  MAX_WIDTH2 as MAX_WIDTH,
50
- base642 as base64,
52
+ base64,
51
53
  imageReads,
52
54
  mimeOf2 as mimeOf,
53
55
  shrink2 as shrink
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/images/index.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n defineExtension,\n type Extension,\n type ToolRegistration,\n wrapTool,\n} from '@earendil-works/pi-durable';\n\nimport { base64, MAX_BASE64, MAX_WIDTH, mimeOf, type Shrink, shrink } from './fit.ts';\n\nexport { base64, type Image, MAX_BASE64, MAX_WIDTH, mimeOf, type Shrink, shrink } from './fit.ts';\n\ntype Result = Awaited<ReturnType<ToolRegistration['execute']>>;\n\nfunction unsupportedImage(result: Result): boolean {\n const diagnostics = (result as { diagnostics?: { code?: string }[] }).diagnostics ?? [];\n return diagnostics.some((diagnostic) => diagnostic.code === 'unsupported_image');\n}\n\nfunction refuse(text: string): Result {\n return { output: [{ type: 'text', text }], isError: true } as Result;\n}\n\nexport function imageReads(read: ToolRegistration, fit: Shrink = shrink): Extension {\n return defineExtension({\n name: 'slicc-images',\n wraps: [\n wrapTool(read, (inner) => ({\n ...inner,\n async execute(args, api, context: Context) {\n const result = await inner.execute(args, api, context);\n if (!unsupportedImage(result) || !api.env) return result;\n const path = String((args as { path: unknown }).path);\n const read = await api.env.readBinaryFile(path, context);\n const mimeType = read.ok ? mimeOf(read.value) : undefined;\n if (!read.ok || !mimeType) return result;\n const image = await fit({ bytes: read.value, mimeType }).catch(() => ({\n bytes: read.value,\n mimeType,\n }));\n const data = base64(image.bytes);\n if (data.length > MAX_BASE64)\n return refuse(\n `${path} is too large to read as an image (${image.bytes.length} bytes after fitting it to ${MAX_WIDTH} px). Take a smaller screenshot, for example with --max-width ${MAX_WIDTH}.`\n );\n return {\n output: [\n { type: 'text', text: `${path} (${image.mimeType}, ${image.bytes.length} bytes)` },\n { type: 'image', data, mimeType: image.mimeType },\n ],\n } as Result;\n },\n })),\n ],\n });\n}\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EAGA;AAAA,OACK;AAEP,SAAS,QAAQ,YAAY,WAAW,QAAqB,cAAc;AAE3E,SAAS,UAAAA,SAAoB,cAAAC,aAAY,aAAAC,YAAW,UAAAC,SAAqB,UAAAC,eAAc;AAIvF,SAAS,iBAAiB,QAAyB;AACjD,QAAM,cAAe,OAAiD,eAAe,CAAC;AACtF,SAAO,YAAY,KAAK,CAAC,eAAe,WAAW,SAAS,mBAAmB;AACjF;AAEA,SAAS,OAAO,MAAsB;AACpC,SAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,GAAG,SAAS,KAAK;AAC3D;AAEO,SAAS,WAAW,MAAwB,MAAc,QAAmB;AAClF,SAAO,gBAAgB;AAAA,IACrB,MAAM;AAAA,IACN,OAAO;AAAA,MACL,SAAS,MAAM,CAAC,WAAW;AAAA,QACzB,GAAG;AAAA,QACH,MAAM,QAAQ,MAAM,KAAK,SAAkB;AACzC,gBAAM,SAAS,MAAM,MAAM,QAAQ,MAAM,KAAK,OAAO;AACrD,cAAI,CAAC,iBAAiB,MAAM,KAAK,CAAC,IAAI,IAAK,QAAO;AAClD,gBAAM,OAAO,OAAQ,KAA2B,IAAI;AACpD,gBAAMC,QAAO,MAAM,IAAI,IAAI,eAAe,MAAM,OAAO;AACvD,gBAAM,WAAWA,MAAK,KAAK,OAAOA,MAAK,KAAK,IAAI;AAChD,cAAI,CAACA,MAAK,MAAM,CAAC,SAAU,QAAO;AAClC,gBAAM,QAAQ,MAAM,IAAI,EAAE,OAAOA,MAAK,OAAO,SAAS,CAAC,EAAE,MAAM,OAAO;AAAA,YACpE,OAAOA,MAAK;AAAA,YACZ;AAAA,UACF,EAAE;AACF,gBAAM,OAAO,OAAO,MAAM,KAAK;AAC/B,cAAI,KAAK,SAAS;AAChB,mBAAO;AAAA,cACL,GAAG,IAAI,sCAAsC,MAAM,MAAM,MAAM,8BAA8B,SAAS,iEAAiE,SAAS;AAAA,YAClL;AACF,iBAAO;AAAA,YACL,QAAQ;AAAA,cACN,EAAE,MAAM,QAAQ,MAAM,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK,MAAM,MAAM,MAAM,UAAU;AAAA,cACjF,EAAE,MAAM,SAAS,MAAM,UAAU,MAAM,SAAS;AAAA,YAClD;AAAA,UACF;AAAA,QACF;AAAA,MACF,EAAE;AAAA,IACJ;AAAA,EACF,CAAC;AACH;",
6
- "names": ["base64", "MAX_BASE64", "MAX_WIDTH", "mimeOf", "shrink", "read"]
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n defineExtension,\n type Extension,\n type ToolRegistration,\n wrapTool,\n} from '@earendil-works/pi-durable';\nimport { CodingTools } from '@earendil-works/pi-durable/tools';\nimport { fitted, MAX_BASE64, MAX_WIDTH, mimeOf, type Shrink, shrink } from './fit.ts';\nimport { imageMarkers } from './markers.ts';\n\nexport { base64, type Image, MAX_BASE64, MAX_WIDTH, mimeOf, type Shrink, shrink } from './fit.ts';\n\ntype Result = Awaited<ReturnType<ToolRegistration['execute']>>;\n\nfunction unsupportedImage(result: Result): boolean {\n const diagnostics = (result as { diagnostics?: { code?: string }[] }).diagnostics ?? [];\n return diagnostics.some((diagnostic) => diagnostic.code === 'unsupported_image');\n}\n\nfunction refuse(text: string): Result {\n return { output: [{ type: 'text', text }], isError: true } as Result;\n}\n\nconst bash = (CodingTools.tools as readonly ToolRegistration[]).find(\n (tool) => tool.name === 'bash'\n) as ToolRegistration;\n\nexport function imageReads(read: ToolRegistration, fit: Shrink = shrink): Extension {\n return defineExtension({\n name: 'slicc-images',\n wraps: [\n wrapTool(read, (inner) => ({\n ...inner,\n async execute(args, api, context: Context) {\n const result = await inner.execute(args, api, context);\n if (!unsupportedImage(result) || !api.env) return result;\n const path = String((args as { path: unknown }).path);\n const read = await api.env.readBinaryFile(path, context);\n const mimeType = read.ok ? mimeOf(read.value) : undefined;\n if (!read.ok || !mimeType) return result;\n const { image, data } = await fitted({ bytes: read.value, mimeType }, fit);\n if (data.length > MAX_BASE64)\n return refuse(\n `${path} is too large to read as an image (${image.bytes.length} bytes after fitting it to ${MAX_WIDTH} px). Take a smaller screenshot, for example with --max-width ${MAX_WIDTH}.`\n );\n return {\n output: [\n { type: 'text', text: `${path} (${image.mimeType}, ${image.bytes.length} bytes)` },\n { type: 'image', data, mimeType: image.mimeType },\n ],\n } as Result;\n },\n })),\n imageMarkers(bash, fit),\n ],\n });\n}\n"],
5
+ "mappings": "AACA;AAAA,EACE;AAAA,EAGA;AAAA,OACK;AACP,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,YAAY,WAAW,QAAqB,cAAc;AAC3E,SAAS,oBAAoB;AAE7B,SAAS,QAAoB,cAAAA,aAAY,aAAAC,YAAW,UAAAC,SAAqB,UAAAC,eAAc;AAIvF,SAAS,iBAAiB,QAAyB;AACjD,QAAM,cAAe,OAAiD,eAAe,CAAC;AACtF,SAAO,YAAY,KAAK,CAAC,eAAe,WAAW,SAAS,mBAAmB;AACjF;AAEA,SAAS,OAAO,MAAsB;AACpC,SAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,GAAG,SAAS,KAAK;AAC3D;AAEA,MAAM,OAAQ,YAAY,MAAsC;AAAA,EAC9D,CAAC,SAAS,KAAK,SAAS;AAC1B;AAEO,SAAS,WAAW,MAAwB,MAAc,QAAmB;AAClF,SAAO,gBAAgB;AAAA,IACrB,MAAM;AAAA,IACN,OAAO;AAAA,MACL,SAAS,MAAM,CAAC,WAAW;AAAA,QACzB,GAAG;AAAA,QACH,MAAM,QAAQ,MAAM,KAAK,SAAkB;AACzC,gBAAM,SAAS,MAAM,MAAM,QAAQ,MAAM,KAAK,OAAO;AACrD,cAAI,CAAC,iBAAiB,MAAM,KAAK,CAAC,IAAI,IAAK,QAAO;AAClD,gBAAM,OAAO,OAAQ,KAA2B,IAAI;AACpD,gBAAMC,QAAO,MAAM,IAAI,IAAI,eAAe,MAAM,OAAO;AACvD,gBAAM,WAAWA,MAAK,KAAK,OAAOA,MAAK,KAAK,IAAI;AAChD,cAAI,CAACA,MAAK,MAAM,CAAC,SAAU,QAAO;AAClC,gBAAM,EAAE,OAAO,KAAK,IAAI,MAAM,OAAO,EAAE,OAAOA,MAAK,OAAO,SAAS,GAAG,GAAG;AACzE,cAAI,KAAK,SAAS;AAChB,mBAAO;AAAA,cACL,GAAG,IAAI,sCAAsC,MAAM,MAAM,MAAM,8BAA8B,SAAS,iEAAiE,SAAS;AAAA,YAClL;AACF,iBAAO;AAAA,YACL,QAAQ;AAAA,cACN,EAAE,MAAM,QAAQ,MAAM,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK,MAAM,MAAM,MAAM,UAAU;AAAA,cACjF,EAAE,MAAM,SAAS,MAAM,UAAU,MAAM,SAAS;AAAA,YAClD;AAAA,UACF;AAAA,QACF;AAAA,MACF,EAAE;AAAA,MACF,aAAa,MAAM,GAAG;AAAA,IACxB;AAAA,EACF,CAAC;AACH;",
6
+ "names": ["MAX_BASE64", "MAX_WIDTH", "mimeOf", "shrink", "read"]
7
7
  }
@@ -0,0 +1,210 @@
1
+ import { boundOutput, OutputBuffer } from "@earendil-works/chord/output";
2
+ import { wrapTool } from "@earendil-works/pi-durable";
3
+ import { gatedRun } from "../guests/guard.js";
4
+ import { fitted, MAX_BASE64, MAX_WIDTH, mimeOf } from "./fit.js";
5
+ const MAX_IMAGES = 4;
6
+ const MAX_MARKER = 4096;
7
+ const DEFAULT_MAX_BYTES = 51200;
8
+ const MARKER = /^<img:(.+)>$/;
9
+ const LATER_REASON = `a command shows at most ${MAX_IMAGES} images.`;
10
+ const LATER = `was not shown: ${LATER_REASON}`;
11
+ class Markers {
12
+ first = [];
13
+ count = 0;
14
+ #recent = [];
15
+ #head = 0;
16
+ #bytes = 0;
17
+ #line = "";
18
+ #over = false;
19
+ budget;
20
+ constructor(budget) {
21
+ this.budget = budget;
22
+ }
23
+ get recent() {
24
+ return this.#recent.slice(this.#head);
25
+ }
26
+ push(text) {
27
+ let start = 0;
28
+ for (let at = text.indexOf("\n"); at !== -1; at = text.indexOf("\n", start)) {
29
+ this.#add(text.slice(start, at));
30
+ this.#close();
31
+ start = at + 1;
32
+ }
33
+ this.#add(text.slice(start));
34
+ }
35
+ skip() {
36
+ this.#line = "";
37
+ this.#over = true;
38
+ }
39
+ end() {
40
+ this.#close();
41
+ }
42
+ #add(part) {
43
+ if (this.#over) return;
44
+ if (this.#line.length + part.length > MAX_MARKER) this.skip();
45
+ else this.#line += part;
46
+ }
47
+ #close() {
48
+ const line = this.#line.replace(/\r$/, "");
49
+ if (!this.#over && MARKER.test(line)) this.#keep(line);
50
+ this.#line = "";
51
+ this.#over = false;
52
+ }
53
+ #keep(line) {
54
+ const at = this.count++;
55
+ if (at < MAX_IMAGES) this.first.push({ at, line });
56
+ this.#recent.push({ at, line });
57
+ this.#bytes += line.length + 1;
58
+ while (this.#bytes > this.budget) {
59
+ this.#bytes -= this.#recent[this.#head].line.length + 1;
60
+ this.#head++;
61
+ }
62
+ if (this.#head * 2 > this.#recent.length) {
63
+ this.#recent = this.#recent.slice(this.#head);
64
+ this.#head = 0;
65
+ }
66
+ }
67
+ }
68
+ function pathOf(marker) {
69
+ return MARKER.exec(marker)[1];
70
+ }
71
+ function watched(env, markers) {
72
+ const exec = (command, options, context) => {
73
+ const onOutput = (text, at, info) => {
74
+ if (info.skipped) markers.skip();
75
+ if (info.stream === "stdout") markers.push(text);
76
+ options?.onOutput?.(text, at, info);
77
+ };
78
+ return env.exec(command, { ...options, onOutput }, context);
79
+ };
80
+ return new Proxy(env, {
81
+ get(target, key) {
82
+ if (key === "exec") return exec;
83
+ const value = Reflect.get(target, key, target);
84
+ return typeof value === "function" ? value.bind(target) : value;
85
+ }
86
+ });
87
+ }
88
+ async function shown(env, path, fit, context) {
89
+ if (!path.startsWith("/"))
90
+ return { text: `${path} is not an absolute path, so it was not read.` };
91
+ const read = await env.readBinaryFile(path, context);
92
+ if (!read.ok) return { text: `${path} could not be read (${read.error.code}).` };
93
+ const mimeType = mimeOf(read.value);
94
+ if (!mimeType) return { text: `${path} is not a PNG, JPEG, GIF or WebP image.` };
95
+ const { image, data } = await fitted({ bytes: read.value, mimeType }, fit);
96
+ if (data.length > MAX_BASE64)
97
+ return {
98
+ text: `${path} is too large to show as an image (${image.bytes.length} bytes after fitting it to ${MAX_WIDTH} px).`
99
+ };
100
+ return {
101
+ text: `${path} (${image.mimeType}, ${image.bytes.length} bytes)`,
102
+ image: { type: "image", data, mimeType: image.mimeType }
103
+ };
104
+ }
105
+ function placed(text, markers, lineOf2) {
106
+ const rows = text.split("\n");
107
+ const { recent } = markers;
108
+ const done = /* @__PURE__ */ new Set();
109
+ let next = recent.length - 1;
110
+ for (let row = rows.length - 1; row >= 0 && next >= 0; row--) {
111
+ const marker = recent[next];
112
+ if (rows[row].replace(/\r$/, "") !== marker.line) continue;
113
+ rows[row] = lineOf2(marker);
114
+ done.add(marker.at);
115
+ next--;
116
+ }
117
+ const missing = markers.first.filter((marker) => !done.has(marker.at)).map(lineOf2);
118
+ const shownLater = [...done].filter((at) => at >= MAX_IMAGES).length;
119
+ const omitted = Math.max(0, markers.count - MAX_IMAGES) - shownLater;
120
+ if (omitted > 0)
121
+ missing.push(
122
+ `${omitted} more image ${omitted === 1 ? "line was" : "lines were"} not shown: ${LATER_REASON}`
123
+ );
124
+ const body = rows.join("\n");
125
+ if (!missing.length) return body;
126
+ const gap = body === "" || body.endsWith("\n") ? "" : "\n";
127
+ return `${body}${gap}${missing.join("\n")}
128
+ `;
129
+ }
130
+ async function images(api, first, fit, context) {
131
+ const all = [];
132
+ for (const marker of first)
133
+ all.push(await shown(api.env, pathOf(marker.line), fit, context));
134
+ return all;
135
+ }
136
+ const GUESTED = "was not read: a run a guest started shows no images from command output.";
137
+ function lineOf(all, gated) {
138
+ return (marker) => {
139
+ if (gated) return `${pathOf(marker.line)} ${GUESTED}`;
140
+ return all[marker.at]?.text ?? `${pathOf(marker.line)} ${LATER}`;
141
+ };
142
+ }
143
+ function bounded(text, shadow) {
144
+ if (!shadow) return { text, diagnostics: [] };
145
+ shadow.buffer.end();
146
+ const before = shadow.buffer.snapshot();
147
+ const slice = boundOutput(text, shadow.limits);
148
+ const bytes = before.droppedBytes + slice.droppedBytes;
149
+ const lines = before.droppedLines + slice.droppedLines;
150
+ if (!bytes) return { text, diagnostics: [] };
151
+ return {
152
+ text: slice.text,
153
+ diagnostics: [
154
+ {
155
+ severity: "warn",
156
+ code: "truncated",
157
+ message: `Output truncated to its end: ${lines} lines, ${bytes} bytes dropped`
158
+ }
159
+ ]
160
+ };
161
+ }
162
+ function imageMarkers(bash, fit) {
163
+ return wrapTool(bash, (inner) => ({
164
+ ...inner,
165
+ async execute(args, api, context) {
166
+ if (!api.env) return inner.execute(args, api, context);
167
+ const window = api.outputWindow;
168
+ const markers = new Markers(window?.maxBytes ?? DEFAULT_MAX_BYTES);
169
+ const limits = window && {
170
+ maxBytes: window.maxBytes,
171
+ maxLines: window.maxLines,
172
+ retain: "tail"
173
+ };
174
+ const shadow = limits && { limits, buffer: new OutputBuffer(limits) };
175
+ const output = (chunk, skipped) => {
176
+ api.output(chunk, skipped);
177
+ shadow?.buffer.push(chunk, skipped);
178
+ };
179
+ const env = watched(api.env, markers);
180
+ const wrapped = Object.create(api, { env: { value: env }, output: { value: output } });
181
+ const result = await inner.execute(args, wrapped, context);
182
+ markers.end();
183
+ if (!markers.count) return result;
184
+ const gated = await gatedRun(api, context);
185
+ const all = gated ? [] : await images(api, markers.first, fit, context);
186
+ const { text, diagnostics } = bounded(
187
+ placed(api.retainedOutput().text, markers, lineOf(all, gated)),
188
+ shadow
189
+ );
190
+ const structured = result.structuredOutput;
191
+ return {
192
+ ...result,
193
+ output: [{ type: "text", text }, ...all.flatMap((one) => one.image ? [one.image] : [])],
194
+ diagnostics: [...result.diagnostics ?? [], ...diagnostics],
195
+ ...structured && typeof structured === "object" ? { structuredOutput: { ...structured, output: text } } : {}
196
+ };
197
+ }
198
+ }));
199
+ }
200
+ export {
201
+ DEFAULT_MAX_BYTES,
202
+ MAX_IMAGES,
203
+ MAX_MARKER,
204
+ Markers,
205
+ imageMarkers,
206
+ pathOf,
207
+ placed,
208
+ shown
209
+ };
210
+ //# sourceMappingURL=markers.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/images/markers.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { boundOutput, OutputBuffer, type OutputLimits } from '@earendil-works/chord/output';\nimport { type ToolRegistration, type Wrap, wrapTool } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, ShellExecOptions } from '@earendil-works/pi-durable/env';\n\nimport { gatedRun } from '../guests/guard.ts';\nimport { fitted, MAX_BASE64, MAX_WIDTH, mimeOf, type Shrink } from './fit.ts';\n\nexport const MAX_IMAGES = 4;\nexport const MAX_MARKER = 4096;\nexport const DEFAULT_MAX_BYTES = 51200;\n\ntype Api = Parameters<ToolRegistration['execute']>[1];\ntype Result = Awaited<ReturnType<ToolRegistration['execute']>>;\ntype Image = { type: 'image'; data: string; mimeType: string };\ntype Shown = { text: string; image?: Image };\n\nconst MARKER = /^<img:(.+)>$/;\nconst LATER_REASON = `a command shows at most ${MAX_IMAGES} images.`;\nconst LATER = `was not shown: ${LATER_REASON}`;\n\nexport type Marker = { at: number; line: string };\n\nexport class Markers {\n readonly first: Marker[] = [];\n count = 0;\n #recent: Marker[] = [];\n #head = 0;\n #bytes = 0;\n #line = '';\n #over = false;\n\n readonly budget: number;\n\n constructor(budget: number) {\n this.budget = budget;\n }\n\n get recent(): readonly Marker[] {\n return this.#recent.slice(this.#head);\n }\n\n push(text: string): void {\n let start = 0;\n for (let at = text.indexOf('\\n'); at !== -1; at = text.indexOf('\\n', start)) {\n this.#add(text.slice(start, at));\n this.#close();\n start = at + 1;\n }\n this.#add(text.slice(start));\n }\n\n skip(): void {\n this.#line = '';\n this.#over = true;\n }\n\n end(): void {\n this.#close();\n }\n\n #add(part: string): void {\n if (this.#over) return;\n if (this.#line.length + part.length > MAX_MARKER) this.skip();\n else this.#line += part;\n }\n\n #close(): void {\n const line = this.#line.replace(/\\r$/, '');\n if (!this.#over && MARKER.test(line)) this.#keep(line);\n this.#line = '';\n this.#over = false;\n }\n\n #keep(line: string): void {\n const at = this.count++;\n if (at < MAX_IMAGES) this.first.push({ at, line });\n this.#recent.push({ at, line });\n this.#bytes += line.length + 1;\n while (this.#bytes > this.budget) {\n this.#bytes -= (this.#recent[this.#head] as Marker).line.length + 1;\n this.#head++;\n }\n if (this.#head * 2 > this.#recent.length) {\n this.#recent = this.#recent.slice(this.#head);\n this.#head = 0;\n }\n }\n}\n\nexport function pathOf(marker: string): string {\n return (MARKER.exec(marker) as RegExpExecArray)[1] as string;\n}\n\nfunction watched(env: ExecutionEnv, markers: Markers): ExecutionEnv {\n const exec: ExecutionEnv['exec'] = (command, options, context) => {\n const onOutput: NonNullable<ShellExecOptions['onOutput']> = (text, at, info) => {\n if (info.skipped) markers.skip();\n if (info.stream === 'stdout') markers.push(text);\n options?.onOutput?.(text, at, info);\n };\n return env.exec(command, { ...options, onOutput }, context);\n };\n return new Proxy(env, {\n get(target, key) {\n if (key === 'exec') return exec;\n const value = Reflect.get(target, key, target);\n return typeof value === 'function' ? value.bind(target) : value;\n },\n });\n}\n\nexport async function shown(\n env: ExecutionEnv,\n path: string,\n fit: Shrink,\n context: Context\n): Promise<Shown> {\n if (!path.startsWith('/'))\n return { text: `${path} is not an absolute path, so it was not read.` };\n const read = await env.readBinaryFile(path, context);\n if (!read.ok) return { text: `${path} could not be read (${read.error.code}).` };\n const mimeType = mimeOf(read.value);\n if (!mimeType) return { text: `${path} is not a PNG, JPEG, GIF or WebP image.` };\n const { image, data } = await fitted({ bytes: read.value, mimeType }, fit);\n if (data.length > MAX_BASE64)\n return {\n text: `${path} is too large to show as an image (${image.bytes.length} bytes after fitting it to ${MAX_WIDTH} px).`,\n };\n return {\n text: `${path} (${image.mimeType}, ${image.bytes.length} bytes)`,\n image: { type: 'image', data, mimeType: image.mimeType },\n };\n}\n\nexport function placed(\n text: string,\n markers: Pick<Markers, 'count' | 'first' | 'recent'>,\n lineOf: (marker: Marker) => string\n): string {\n const rows = text.split('\\n');\n const { recent } = markers;\n const done = new Set<number>();\n let next = recent.length - 1;\n for (let row = rows.length - 1; row >= 0 && next >= 0; row--) {\n const marker = recent[next] as Marker;\n if ((rows[row] as string).replace(/\\r$/, '') !== marker.line) continue;\n rows[row] = lineOf(marker);\n done.add(marker.at);\n next--;\n }\n const missing = markers.first.filter((marker) => !done.has(marker.at)).map(lineOf);\n const shownLater = [...done].filter((at) => at >= MAX_IMAGES).length;\n const omitted = Math.max(0, markers.count - MAX_IMAGES) - shownLater;\n if (omitted > 0)\n missing.push(\n `${omitted} more image ${omitted === 1 ? 'line was' : 'lines were'} not shown: ${LATER_REASON}`\n );\n const body = rows.join('\\n');\n if (!missing.length) return body;\n const gap = body === '' || body.endsWith('\\n') ? '' : '\\n';\n return `${body}${gap}${missing.join('\\n')}\\n`;\n}\n\nasync function images(api: Api, first: readonly Marker[], fit: Shrink, context: Context) {\n const all: Shown[] = [];\n for (const marker of first)\n all.push(await shown(api.env as ExecutionEnv, pathOf(marker.line), fit, context));\n return all;\n}\n\nconst GUESTED = 'was not read: a run a guest started shows no images from command output.';\n\nfunction lineOf(all: readonly Shown[], gated: boolean) {\n return (marker: Marker) => {\n if (gated) return `${pathOf(marker.line)} ${GUESTED}`;\n return (all[marker.at] as Shown | undefined)?.text ?? `${pathOf(marker.line)} ${LATER}`;\n };\n}\n\ntype Shadow = { buffer: OutputBuffer; limits: OutputLimits };\n\nfunction bounded(text: string, shadow: Shadow | undefined) {\n if (!shadow) return { text, diagnostics: [] };\n shadow.buffer.end();\n const before = shadow.buffer.snapshot();\n const slice = boundOutput(text, shadow.limits);\n const bytes = before.droppedBytes + slice.droppedBytes;\n const lines = before.droppedLines + slice.droppedLines;\n if (!bytes) return { text, diagnostics: [] };\n return {\n text: slice.text,\n diagnostics: [\n {\n severity: 'warn' as const,\n code: 'truncated',\n message: `Output truncated to its end: ${lines} lines, ${bytes} bytes dropped`,\n },\n ],\n };\n}\n\nexport function imageMarkers(bash: ToolRegistration, fit: Shrink): Wrap {\n return wrapTool(bash, (inner) => ({\n ...inner,\n async execute(args, api, context: Context) {\n if (!api.env) return inner.execute(args, api, context);\n const window = api.outputWindow;\n const markers = new Markers(window?.maxBytes ?? DEFAULT_MAX_BYTES);\n const limits: OutputLimits | undefined = window && {\n maxBytes: window.maxBytes,\n maxLines: window.maxLines,\n retain: 'tail',\n };\n const shadow = limits && { limits, buffer: new OutputBuffer(limits) };\n const output: Api['output'] = (chunk, skipped) => {\n api.output(chunk, skipped);\n shadow?.buffer.push(chunk, skipped);\n };\n const env = watched(api.env, markers);\n const wrapped = Object.create(api, { env: { value: env }, output: { value: output } });\n const result = await inner.execute(args, wrapped, context);\n markers.end();\n if (!markers.count) return result;\n const gated = await gatedRun(api, context);\n const all = gated ? [] : await images(api, markers.first, fit, context);\n const { text, diagnostics } = bounded(\n placed(api.retainedOutput().text, markers, lineOf(all, gated)),\n shadow\n );\n const structured = result.structuredOutput;\n return {\n ...result,\n output: [{ type: 'text', text }, ...all.flatMap((one) => (one.image ? [one.image] : []))],\n diagnostics: [...(result.diagnostics ?? []), ...diagnostics],\n ...(structured && typeof structured === 'object'\n ? { structuredOutput: { ...structured, output: text } }\n : {}),\n } as Result;\n },\n }));\n}\n"],
5
+ "mappings": "AACA,SAAS,aAAa,oBAAuC;AAC7D,SAA2C,gBAAgB;AAG3D,SAAS,gBAAgB;AACzB,SAAS,QAAQ,YAAY,WAAW,cAA2B;AAE5D,MAAM,aAAa;AACnB,MAAM,aAAa;AACnB,MAAM,oBAAoB;AAOjC,MAAM,SAAS;AACf,MAAM,eAAe,2BAA2B,UAAU;AAC1D,MAAM,QAAQ,kBAAkB,YAAY;AAIrC,MAAM,QAAQ;AAAA,EACV,QAAkB,CAAC;AAAA,EAC5B,QAAQ;AAAA,EACR,UAAoB,CAAC;AAAA,EACrB,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EAEC;AAAA,EAET,YAAY,QAAgB;AAC1B,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,IAAI,SAA4B;AAC9B,WAAO,KAAK,QAAQ,MAAM,KAAK,KAAK;AAAA,EACtC;AAAA,EAEA,KAAK,MAAoB;AACvB,QAAI,QAAQ;AACZ,aAAS,KAAK,KAAK,QAAQ,IAAI,GAAG,OAAO,IAAI,KAAK,KAAK,QAAQ,MAAM,KAAK,GAAG;AAC3E,WAAK,KAAK,KAAK,MAAM,OAAO,EAAE,CAAC;AAC/B,WAAK,OAAO;AACZ,cAAQ,KAAK;AAAA,IACf;AACA,SAAK,KAAK,KAAK,MAAM,KAAK,CAAC;AAAA,EAC7B;AAAA,EAEA,OAAa;AACX,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,MAAY;AACV,SAAK,OAAO;AAAA,EACd;AAAA,EAEA,KAAK,MAAoB;AACvB,QAAI,KAAK,MAAO;AAChB,QAAI,KAAK,MAAM,SAAS,KAAK,SAAS,WAAY,MAAK,KAAK;AAAA,QACvD,MAAK,SAAS;AAAA,EACrB;AAAA,EAEA,SAAe;AACb,UAAM,OAAO,KAAK,MAAM,QAAQ,OAAO,EAAE;AACzC,QAAI,CAAC,KAAK,SAAS,OAAO,KAAK,IAAI,EAAG,MAAK,MAAM,IAAI;AACrD,SAAK,QAAQ;AACb,SAAK,QAAQ;AAAA,EACf;AAAA,EAEA,MAAM,MAAoB;AACxB,UAAM,KAAK,KAAK;AAChB,QAAI,KAAK,WAAY,MAAK,MAAM,KAAK,EAAE,IAAI,KAAK,CAAC;AACjD,SAAK,QAAQ,KAAK,EAAE,IAAI,KAAK,CAAC;AAC9B,SAAK,UAAU,KAAK,SAAS;AAC7B,WAAO,KAAK,SAAS,KAAK,QAAQ;AAChC,WAAK,UAAW,KAAK,QAAQ,KAAK,KAAK,EAAa,KAAK,SAAS;AAClE,WAAK;AAAA,IACP;AACA,QAAI,KAAK,QAAQ,IAAI,KAAK,QAAQ,QAAQ;AACxC,WAAK,UAAU,KAAK,QAAQ,MAAM,KAAK,KAAK;AAC5C,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AACF;AAEO,SAAS,OAAO,QAAwB;AAC7C,SAAQ,OAAO,KAAK,MAAM,EAAsB,CAAC;AACnD;AAEA,SAAS,QAAQ,KAAmB,SAAgC;AAClE,QAAM,OAA6B,CAAC,SAAS,SAAS,YAAY;AAChE,UAAM,WAAsD,CAAC,MAAM,IAAI,SAAS;AAC9E,UAAI,KAAK,QAAS,SAAQ,KAAK;AAC/B,UAAI,KAAK,WAAW,SAAU,SAAQ,KAAK,IAAI;AAC/C,eAAS,WAAW,MAAM,IAAI,IAAI;AAAA,IACpC;AACA,WAAO,IAAI,KAAK,SAAS,EAAE,GAAG,SAAS,SAAS,GAAG,OAAO;AAAA,EAC5D;AACA,SAAO,IAAI,MAAM,KAAK;AAAA,IACpB,IAAI,QAAQ,KAAK;AACf,UAAI,QAAQ,OAAQ,QAAO;AAC3B,YAAM,QAAQ,QAAQ,IAAI,QAAQ,KAAK,MAAM;AAC7C,aAAO,OAAO,UAAU,aAAa,MAAM,KAAK,MAAM,IAAI;AAAA,IAC5D;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,MACpB,KACA,MACA,KACA,SACgB;AAChB,MAAI,CAAC,KAAK,WAAW,GAAG;AACtB,WAAO,EAAE,MAAM,GAAG,IAAI,gDAAgD;AACxE,QAAM,OAAO,MAAM,IAAI,eAAe,MAAM,OAAO;AACnD,MAAI,CAAC,KAAK,GAAI,QAAO,EAAE,MAAM,GAAG,IAAI,uBAAuB,KAAK,MAAM,IAAI,KAAK;AAC/E,QAAM,WAAW,OAAO,KAAK,KAAK;AAClC,MAAI,CAAC,SAAU,QAAO,EAAE,MAAM,GAAG,IAAI,0CAA0C;AAC/E,QAAM,EAAE,OAAO,KAAK,IAAI,MAAM,OAAO,EAAE,OAAO,KAAK,OAAO,SAAS,GAAG,GAAG;AACzE,MAAI,KAAK,SAAS;AAChB,WAAO;AAAA,MACL,MAAM,GAAG,IAAI,sCAAsC,MAAM,MAAM,MAAM,8BAA8B,SAAS;AAAA,IAC9G;AACF,SAAO;AAAA,IACL,MAAM,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK,MAAM,MAAM,MAAM;AAAA,IACvD,OAAO,EAAE,MAAM,SAAS,MAAM,UAAU,MAAM,SAAS;AAAA,EACzD;AACF;AAEO,SAAS,OACd,MACA,SACAA,SACQ;AACR,QAAM,OAAO,KAAK,MAAM,IAAI;AAC5B,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,OAAO,OAAO,SAAS;AAC3B,WAAS,MAAM,KAAK,SAAS,GAAG,OAAO,KAAK,QAAQ,GAAG,OAAO;AAC5D,UAAM,SAAS,OAAO,IAAI;AAC1B,QAAK,KAAK,GAAG,EAAa,QAAQ,OAAO,EAAE,MAAM,OAAO,KAAM;AAC9D,SAAK,GAAG,IAAIA,QAAO,MAAM;AACzB,SAAK,IAAI,OAAO,EAAE;AAClB;AAAA,EACF;AACA,QAAM,UAAU,QAAQ,MAAM,OAAO,CAAC,WAAW,CAAC,KAAK,IAAI,OAAO,EAAE,CAAC,EAAE,IAAIA,OAAM;AACjF,QAAM,aAAa,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,OAAO,MAAM,UAAU,EAAE;AAC9D,QAAM,UAAU,KAAK,IAAI,GAAG,QAAQ,QAAQ,UAAU,IAAI;AAC1D,MAAI,UAAU;AACZ,YAAQ;AAAA,MACN,GAAG,OAAO,eAAe,YAAY,IAAI,aAAa,YAAY,eAAe,YAAY;AAAA,IAC/F;AACF,QAAM,OAAO,KAAK,KAAK,IAAI;AAC3B,MAAI,CAAC,QAAQ,OAAQ,QAAO;AAC5B,QAAM,MAAM,SAAS,MAAM,KAAK,SAAS,IAAI,IAAI,KAAK;AACtD,SAAO,GAAG,IAAI,GAAG,GAAG,GAAG,QAAQ,KAAK,IAAI,CAAC;AAAA;AAC3C;AAEA,eAAe,OAAO,KAAU,OAA0B,KAAa,SAAkB;AACvF,QAAM,MAAe,CAAC;AACtB,aAAW,UAAU;AACnB,QAAI,KAAK,MAAM,MAAM,IAAI,KAAqB,OAAO,OAAO,IAAI,GAAG,KAAK,OAAO,CAAC;AAClF,SAAO;AACT;AAEA,MAAM,UAAU;AAEhB,SAAS,OAAO,KAAuB,OAAgB;AACrD,SAAO,CAAC,WAAmB;AACzB,QAAI,MAAO,QAAO,GAAG,OAAO,OAAO,IAAI,CAAC,IAAI,OAAO;AACnD,WAAQ,IAAI,OAAO,EAAE,GAAyB,QAAQ,GAAG,OAAO,OAAO,IAAI,CAAC,IAAI,KAAK;AAAA,EACvF;AACF;AAIA,SAAS,QAAQ,MAAc,QAA4B;AACzD,MAAI,CAAC,OAAQ,QAAO,EAAE,MAAM,aAAa,CAAC,EAAE;AAC5C,SAAO,OAAO,IAAI;AAClB,QAAM,SAAS,OAAO,OAAO,SAAS;AACtC,QAAM,QAAQ,YAAY,MAAM,OAAO,MAAM;AAC7C,QAAM,QAAQ,OAAO,eAAe,MAAM;AAC1C,QAAM,QAAQ,OAAO,eAAe,MAAM;AAC1C,MAAI,CAAC,MAAO,QAAO,EAAE,MAAM,aAAa,CAAC,EAAE;AAC3C,SAAO;AAAA,IACL,MAAM,MAAM;AAAA,IACZ,aAAa;AAAA,MACX;AAAA,QACE,UAAU;AAAA,QACV,MAAM;AAAA,QACN,SAAS,gCAAgC,KAAK,WAAW,KAAK;AAAA,MAChE;AAAA,IACF;AAAA,EACF;AACF;AAEO,SAAS,aAAa,MAAwB,KAAmB;AACtE,SAAO,SAAS,MAAM,CAAC,WAAW;AAAA,IAChC,GAAG;AAAA,IACH,MAAM,QAAQ,MAAM,KAAK,SAAkB;AACzC,UAAI,CAAC,IAAI,IAAK,QAAO,MAAM,QAAQ,MAAM,KAAK,OAAO;AACrD,YAAM,SAAS,IAAI;AACnB,YAAM,UAAU,IAAI,QAAQ,QAAQ,YAAY,iBAAiB;AACjE,YAAM,SAAmC,UAAU;AAAA,QACjD,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,QAAQ;AAAA,MACV;AACA,YAAM,SAAS,UAAU,EAAE,QAAQ,QAAQ,IAAI,aAAa,MAAM,EAAE;AACpE,YAAM,SAAwB,CAAC,OAAO,YAAY;AAChD,YAAI,OAAO,OAAO,OAAO;AACzB,gBAAQ,OAAO,KAAK,OAAO,OAAO;AAAA,MACpC;AACA,YAAM,MAAM,QAAQ,IAAI,KAAK,OAAO;AACpC,YAAM,UAAU,OAAO,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,IAAI,GAAG,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC;AACrF,YAAM,SAAS,MAAM,MAAM,QAAQ,MAAM,SAAS,OAAO;AACzD,cAAQ,IAAI;AACZ,UAAI,CAAC,QAAQ,MAAO,QAAO;AAC3B,YAAM,QAAQ,MAAM,SAAS,KAAK,OAAO;AACzC,YAAM,MAAM,QAAQ,CAAC,IAAI,MAAM,OAAO,KAAK,QAAQ,OAAO,KAAK,OAAO;AACtE,YAAM,EAAE,MAAM,YAAY,IAAI;AAAA,QAC5B,OAAO,IAAI,eAAe,EAAE,MAAM,SAAS,OAAO,KAAK,KAAK,CAAC;AAAA,QAC7D;AAAA,MACF;AACA,YAAM,aAAa,OAAO;AAC1B,aAAO;AAAA,QACL,GAAG;AAAA,QACH,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,GAAG,GAAG,IAAI,QAAQ,CAAC,QAAS,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,CAAC,CAAE,CAAC;AAAA,QACxF,aAAa,CAAC,GAAI,OAAO,eAAe,CAAC,GAAI,GAAG,WAAW;AAAA,QAC3D,GAAI,cAAc,OAAO,eAAe,WACpC,EAAE,kBAAkB,EAAE,GAAG,YAAY,QAAQ,KAAK,EAAE,IACpD,CAAC;AAAA,MACP;AAAA,IACF;AAAA,EACF,EAAE;AACJ;",
6
+ "names": ["lineOf"]
7
+ }
@@ -216,6 +216,17 @@ const IdleDoc = defineDoc({
216
216
  scope: "session",
217
217
  initial: () => ({ tasks: {} })
218
218
  });
219
+ function changedAt(entries, seen, now) {
220
+ let newest = Number.NEGATIVE_INFINITY;
221
+ for (const entry of entries) {
222
+ if (entry.id <= seen.tail) continue;
223
+ for (const message of entry.model ?? [])
224
+ if (typeof message.timestamp === "number")
225
+ newest = Math.max(newest, Math.min(message.timestamp, now));
226
+ }
227
+ if (newest === Number.NEGATIVE_INFINITY) return seen.tail < 0 ? now : seen.since;
228
+ return Math.max(seen.since, newest);
229
+ }
219
230
  function idleTask(ready, schedule, settings) {
220
231
  return defineTask({
221
232
  name: IDLE_TASK,
@@ -238,18 +249,14 @@ function idleTask(ready, schedule, settings) {
238
249
  const idleMs = (on ? idleMinutes : DEFAULT_IDLE_MINUTES) * 6e4;
239
250
  const now = runtime.now();
240
251
  const view = await runtime.context(conversation, context);
241
- const last = view.entries.at(-1);
242
- const tail = last?.id ?? 0;
252
+ const tail = view.entries.at(-1)?.id ?? 0;
243
253
  const seen = task.state.checkpoint;
244
- if (tail !== seen.tail) {
245
- const stamp = last?.model?.at(-1)?.timestamp;
246
- const since = typeof stamp === "number" && stamp <= now ? stamp : now;
247
- return runtime.sleepUntil(since + idleMs, { phase: "watch", tail, since }, context);
248
- }
249
- const due = seen.since + idleMs;
250
- if (now < due) return runtime.sleepUntil(due, seen, context);
254
+ const since = tail === seen.tail ? seen.since : changedAt(view.entries, seen, now);
255
+ const next = { phase: "watch", tail, since };
256
+ const due = since + idleMs;
257
+ if (now < due) return runtime.sleepUntil(due, next, context);
251
258
  if (on) await schedule(conversation, tail, context, who.id);
252
- return runtime.sleepUntil(now + idleMs, seen, context);
259
+ return runtime.sleepUntil(now + idleMs, next, context);
253
260
  }
254
261
  },
255
262
  abort: (task, runtime, context) => runtime.commit(async (tx) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/memory/extract.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Message } from '@earendil-works/pi-ai';\nimport {\n CompactionTask,\n type ConversationId,\n defineDoc,\n defineTask,\n type EntryId,\n hook,\n type ModelRef,\n type TaskId,\n} from '@earendil-works/pi-durable';\nimport { boundedDeltas } from '../checkpoints.ts';\nimport type { MemoryTag } from './format.ts';\nimport { MAX_BYTES, parseMemory, redactSecrets, serializeMemory } from './format.ts';\nimport { type Host, whoIs } from './prompt.ts';\nimport { type Place, placeOf } from './store.ts';\n\nexport const EXTRACT_TASK = 'slicc.memory-extract';\nexport const TRANSCRIPT_CHARS = 60_000;\nexport const DEFAULT_IDLE_MINUTES = 30;\n\nexport type ExtractState = {\n cursors: Record<string, number>;\n pending: Record<string, number>;\n};\n\nexport const ExtractDoc = defineDoc<ExtractState>({\n kind: 'slicc.memory-extract',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'session',\n initial: () => ({ cursors: {}, pending: {} }),\n});\n\nexport type ExtractSettings = { extract: boolean; model: ModelRef | null; idleMinutes: number };\n\nexport function extractSettings(text: string | undefined): ExtractSettings {\n let memory: { extract?: unknown; extractModel?: unknown; idleMinutes?: unknown } = {};\n try {\n const parsed = JSON.parse(text ?? '{}') as { memory?: typeof memory };\n memory = parsed.memory && typeof parsed.memory === 'object' ? parsed.memory : {};\n } catch {\n memory = {};\n }\n const named = typeof memory.extractModel === 'string' ? memory.extractModel.split('/') : [];\n const model =\n named.length >= 2 && named[0] && named.slice(1).join('/')\n ? { provider: named[0], modelId: named.slice(1).join('/') }\n : null;\n const idle =\n typeof memory.idleMinutes === 'number' && memory.idleMinutes >= 0\n ? memory.idleMinutes\n : DEFAULT_IDLE_MINUTES;\n return { extract: memory.extract !== false, model, idleMinutes: idle };\n}\n\nfunction textOf(message: Message): string {\n const content = (message as { content?: unknown }).content;\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part: { type?: string; text?: string; name?: string }) =>\n part.type === 'text'\n ? (part.text ?? '')\n : part.type === 'toolCall'\n ? `[tool ${part.name}]`\n : ''\n )\n .filter(Boolean)\n .join('\\n');\n}\n\nexport function transcriptOf(messages: readonly Message[]): string {\n const lines: string[] = [];\n for (const message of messages) {\n if (message.role !== 'user' && message.role !== 'assistant') continue;\n const text = textOf(message).trim();\n if (text) lines.push(`${message.role === 'user' ? 'User' : 'Agent'}: ${text}`);\n }\n const joined = redactSecrets(lines.join('\\n\\n')).text;\n return joined.length > TRANSCRIPT_CHARS ? joined.slice(-TRANSCRIPT_CHARS) : joined;\n}\n\nexport const EXTRACT_PROMPT = [\n 'You keep the long-term memory of a personal agent. Read the conversation excerpt and the current memory file, and propose memory entries worth keeping across conversations:',\n '- who the user is (tag \"user\"), how they want the agent to work, including corrections (tag \"feedback\"), and durable facts about their projects (tag \"project\");',\n '- only facts that will still matter in a later conversation; nothing about this one task, no transcripts, no guesses;',\n '- never secrets: no keys, tokens, passwords, Authorization values or URLs that carry a key;',\n '- to change an entry that exists, repeat its section and title with the new body; leave entries alone that need no change.',\n 'Answer with a JSON array and nothing else: [{\"section\": \"...\", \"title\": \"...\", \"body\": \"...\", \"tag\": \"user\" | \"feedback\" | \"project\"}]. An empty array is a fine answer.',\n].join('\\n');\n\nexport type Proposal = { section: string; title: string; body: string; tag: MemoryTag | null };\n\nconst TAGS = new Set(['user', 'feedback', 'project']);\n\nexport function proposals(text: string): Proposal[] {\n const start = text.indexOf('[');\n const end = text.lastIndexOf(']');\n if (start < 0 || end < start) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(text.slice(start, end + 1));\n } catch {\n return [];\n }\n if (!Array.isArray(parsed)) return [];\n const out: Proposal[] = [];\n for (const item of parsed as Record<string, unknown>[]) {\n if (!item || typeof item !== 'object') continue;\n const { section, title, body, tag } = item;\n if (typeof section !== 'string' || typeof title !== 'string' || typeof body !== 'string')\n continue;\n if (!section.trim() || !title.trim() || !body.trim()) continue;\n out.push({\n section,\n title,\n body,\n tag: typeof tag === 'string' && TAGS.has(tag) ? (tag as MemoryTag) : null,\n });\n }\n return out.slice(0, 20);\n}\n\ntype Input = { cone: string; after: number; tail: number; model: ModelRef | null };\ntype State = { phase: 'extract' };\nexport type ExtractResult = { saved: number; skipped: string | null };\n\nasync function save(host: Host, place: Place, found: Proposal[], context: Context) {\n let saved = 0;\n for (const proposal of found) {\n try {\n await host.files.change(place, { kind: 'save', ...proposal }, context);\n saved++;\n } catch {\n break;\n }\n }\n if (saved) await host.changed(context).catch(() => undefined);\n return saved;\n}\n\nexport function extractTask(\n ready: Promise<Host>,\n settings: (context: Context) => Promise<ExtractSettings>\n) {\n return defineTask<Input, State, ExtractResult>({\n name: EXTRACT_TASK,\n version: 1,\n initial: () => ({ phase: 'extract' }),\n phases: {\n async extract(task, runtime, context) {\n const { cone, after, tail } = task.input;\n const key = String(runtime.conversationId);\n const done = (result: ExtractResult) =>\n runtime.commit(async (tx) => {\n const doc = await tx.doc(ExtractDoc);\n doc.cursors[key] = Math.max(doc.cursors[key] ?? 0, tail);\n delete doc.pending[key];\n return { status: 'terminal', outcome: { status: 'completed', result } };\n }, context);\n if (!(await settings(context)).extract) return done({ saved: 0, skipped: 'turned off' });\n const host = await ready;\n const place = placeOf(host.home, cone);\n const view = await runtime.context(runtime.conversationId, context, {\n at: tail as EntryId,\n });\n const fresh = view.entries.flatMap((entry, index) =>\n entry.id > after ? (view.contributions[index] as readonly Message[]) : []\n );\n const transcript = transcriptOf(fresh);\n if (!place || !transcript) return done({ saved: 0, skipped: 'nothing new' });\n const ref = task.input.model ?? (await runtime.agent(context)).model;\n const model = ref ? runtime.models.getModel(ref.provider, ref.modelId) : undefined;\n if (!model) return done({ saved: 0, skipped: 'no model' });\n const current = await host.files.read(place.file, context);\n const memory = current ? serializeMemory(parseMemory(current.text)) : '';\n const now = Date.now();\n const message = await runtime.models.completeSimple(\n model,\n {\n messages: [\n { role: 'system', content: EXTRACT_PROMPT, timestamp: now } as unknown as Message,\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: `<memory_file bytes_left=\"${MAX_BYTES - new TextEncoder().encode(memory).length}\">\\n${memory}</memory_file>\\n\\n<conversation>\\n${transcript}\\n</conversation>`,\n },\n ],\n timestamp: now,\n },\n ],\n },\n { maxTokens: 2000, signal: runtime.signal, cacheRetention: 'none' }\n );\n runtime.signal.throwIfAborted();\n if (message.stopReason === 'error') return done({ saved: 0, skipped: 'model error' });\n const saved = await save(host, place, proposals(textOf(message as Message)), context);\n return done({ saved, skipped: null });\n },\n },\n abort: (_task, runtime, context) =>\n runtime.commit(async (tx) => {\n delete (await tx.doc(ExtractDoc)).pending[String(runtime.conversationId)];\n return { status: 'terminal', outcome: { status: 'aborted' } };\n }, context),\n });\n}\n\nexport type Schedule = (\n conversation: number,\n tail: number,\n context: Context,\n cone?: string\n) => Promise<boolean>;\n\nexport function compactionHook(schedule: Schedule) {\n return hook(CompactionTask, {\n beforeCompact(compaction, api) {\n const tail = compaction.entries.reduce((max, entry) => Math.max(max, entry.id), 0);\n void schedule(api.conversationId, tail, BACKGROUND_CONTEXT).catch(() => false);\n return undefined;\n },\n });\n}\n\nexport function scheduler(\n ready: Promise<Host>,\n task: ReturnType<typeof extractTask>,\n settings: (context: Context) => Promise<ExtractSettings>\n): Schedule {\n return async (conversation, tail, context, cone) => {\n const host = await ready;\n const chosen = await settings(context);\n const who = cone\n ? { kind: 'cone' as const, id: cone }\n : whoIs(host.agents.state(), conversation);\n if (who.kind !== 'cone') return false;\n return host.harness.commit(async (tx) => {\n const doc = await tx.doc(ExtractDoc);\n const key = String(conversation);\n const after = doc.cursors[key] ?? 0;\n if (doc.pending[key] !== undefined || tail <= after) return false;\n doc.pending[key] = (await tx.createTask(\n task,\n { cone: who.id, after, tail, model: chosen.model },\n {\n ownership: { kind: 'conversation' },\n conversationId: conversation as ConversationId,\n background: true,\n }\n )) as number;\n return true;\n }, context);\n };\n}\n\nexport function settingsReader(ready: Promise<Host>) {\n return async (context: Context): Promise<ExtractSettings> => {\n const host = await ready;\n const dir = host.agentDir ?? `${host.home}/.pi/agent`;\n const read = await host.env.readTextFile(`${dir}/settings.json`, context);\n return extractSettings(read.ok ? read.value : undefined);\n };\n}\n\nexport const IDLE_TASK = 'slicc.memory-idle';\n\nexport const IdleDoc = defineDoc<{ tasks: Record<string, number> }>({\n kind: 'slicc.memory-idle',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'session',\n initial: () => ({ tasks: {} }),\n});\n\ntype IdleState = { phase: 'watch'; tail: number; since: number };\n\nexport function idleTask(\n ready: Promise<Host>,\n schedule: Schedule,\n settings: (context: Context) => Promise<ExtractSettings>\n) {\n return defineTask<{ cone: string }, IdleState, null>({\n name: IDLE_TASK,\n version: 1,\n initial: () => ({ phase: 'watch', tail: -1, since: 0 }),\n phases: {\n async watch(task, runtime, context) {\n const host = await ready;\n const conversation = runtime.conversationId;\n const key = String(conversation);\n const who = whoIs(host.agents.state(), conversation);\n if (who.kind !== 'cone')\n return runtime.commit(async (tx) => {\n const doc = await tx.doc(IdleDoc);\n if (doc.tasks[key] === Number(task.id)) delete doc.tasks[key];\n return { status: 'terminal', outcome: { status: 'completed', result: null } };\n }, context);\n const { extract, idleMinutes } = await settings(context);\n const on = extract && idleMinutes > 0;\n const idleMs = (on ? idleMinutes : DEFAULT_IDLE_MINUTES) * 60_000;\n const now = runtime.now();\n const view = await runtime.context(conversation, context);\n const last = view.entries.at(-1);\n const tail = last?.id ?? 0;\n const seen = task.state.checkpoint;\n if (tail !== seen.tail) {\n const stamp = last?.model?.at(-1)?.timestamp;\n const since = typeof stamp === 'number' && stamp <= now ? stamp : now;\n return runtime.sleepUntil(since + idleMs, { phase: 'watch', tail, since }, context);\n }\n const due = seen.since + idleMs;\n if (now < due) return runtime.sleepUntil(due, seen, context);\n if (on) await schedule(conversation, tail, context, who.id);\n return runtime.sleepUntil(now + idleMs, seen, context);\n },\n },\n abort: (task, runtime, context) =>\n runtime.commit(async (tx) => {\n const doc = await tx.doc(IdleDoc);\n const key = String(runtime.conversationId);\n if (doc.tasks[key] === Number(task.id)) delete doc.tasks[key];\n return { status: 'terminal', outcome: { status: 'aborted' } };\n }, context),\n });\n}\n\nexport function idleWatch(\n host: Pick<Host, 'agents' | 'harness'>,\n task: ReturnType<typeof idleTask>,\n context: Context\n): () => void {\n let stopped = false;\n let line = Promise.resolve();\n const ensure = () => {\n line = line\n .then(async () => {\n if (stopped) return;\n await host.harness.commit(async (tx) => {\n const doc = await tx.doc(IdleDoc);\n for (const [id, cone] of Object.entries(host.agents.state().cones)) {\n const key = String(cone.conversation);\n const known = doc.tasks[key];\n const record = known === undefined ? undefined : await tx.task(known as TaskId);\n if (record && record.state.status !== 'terminal') continue;\n doc.tasks[key] = (await tx.createTask(\n task,\n { cone: id },\n {\n ownership: { kind: 'conversation' },\n conversationId: cone.conversation as ConversationId,\n background: true,\n }\n )) as number;\n }\n }, context);\n })\n .catch(() => undefined);\n };\n const off = host.agents.onChange(ensure);\n ensure();\n return () => {\n stopped = true;\n off();\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Message } from '@earendil-works/pi-ai';\nimport {\n CompactionTask,\n type ConversationId,\n defineDoc,\n defineTask,\n type EntryId,\n hook,\n type ModelRef,\n type TaskId,\n} from '@earendil-works/pi-durable';\nimport { boundedDeltas } from '../checkpoints.ts';\nimport type { MemoryTag } from './format.ts';\nimport { MAX_BYTES, parseMemory, redactSecrets, serializeMemory } from './format.ts';\nimport { type Host, whoIs } from './prompt.ts';\nimport { type Place, placeOf } from './store.ts';\n\nexport const EXTRACT_TASK = 'slicc.memory-extract';\nexport const TRANSCRIPT_CHARS = 60_000;\nexport const DEFAULT_IDLE_MINUTES = 30;\n\nexport type ExtractState = {\n cursors: Record<string, number>;\n pending: Record<string, number>;\n};\n\nexport const ExtractDoc = defineDoc<ExtractState>({\n kind: 'slicc.memory-extract',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'session',\n initial: () => ({ cursors: {}, pending: {} }),\n});\n\nexport type ExtractSettings = { extract: boolean; model: ModelRef | null; idleMinutes: number };\n\nexport function extractSettings(text: string | undefined): ExtractSettings {\n let memory: { extract?: unknown; extractModel?: unknown; idleMinutes?: unknown } = {};\n try {\n const parsed = JSON.parse(text ?? '{}') as { memory?: typeof memory };\n memory = parsed.memory && typeof parsed.memory === 'object' ? parsed.memory : {};\n } catch {\n memory = {};\n }\n const named = typeof memory.extractModel === 'string' ? memory.extractModel.split('/') : [];\n const model =\n named.length >= 2 && named[0] && named.slice(1).join('/')\n ? { provider: named[0], modelId: named.slice(1).join('/') }\n : null;\n const idle =\n typeof memory.idleMinutes === 'number' && memory.idleMinutes >= 0\n ? memory.idleMinutes\n : DEFAULT_IDLE_MINUTES;\n return { extract: memory.extract !== false, model, idleMinutes: idle };\n}\n\nfunction textOf(message: Message): string {\n const content = (message as { content?: unknown }).content;\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part: { type?: string; text?: string; name?: string }) =>\n part.type === 'text'\n ? (part.text ?? '')\n : part.type === 'toolCall'\n ? `[tool ${part.name}]`\n : ''\n )\n .filter(Boolean)\n .join('\\n');\n}\n\nexport function transcriptOf(messages: readonly Message[]): string {\n const lines: string[] = [];\n for (const message of messages) {\n if (message.role !== 'user' && message.role !== 'assistant') continue;\n const text = textOf(message).trim();\n if (text) lines.push(`${message.role === 'user' ? 'User' : 'Agent'}: ${text}`);\n }\n const joined = redactSecrets(lines.join('\\n\\n')).text;\n return joined.length > TRANSCRIPT_CHARS ? joined.slice(-TRANSCRIPT_CHARS) : joined;\n}\n\nexport const EXTRACT_PROMPT = [\n 'You keep the long-term memory of a personal agent. Read the conversation excerpt and the current memory file, and propose memory entries worth keeping across conversations:',\n '- who the user is (tag \"user\"), how they want the agent to work, including corrections (tag \"feedback\"), and durable facts about their projects (tag \"project\");',\n '- only facts that will still matter in a later conversation; nothing about this one task, no transcripts, no guesses;',\n '- never secrets: no keys, tokens, passwords, Authorization values or URLs that carry a key;',\n '- to change an entry that exists, repeat its section and title with the new body; leave entries alone that need no change.',\n 'Answer with a JSON array and nothing else: [{\"section\": \"...\", \"title\": \"...\", \"body\": \"...\", \"tag\": \"user\" | \"feedback\" | \"project\"}]. An empty array is a fine answer.',\n].join('\\n');\n\nexport type Proposal = { section: string; title: string; body: string; tag: MemoryTag | null };\n\nconst TAGS = new Set(['user', 'feedback', 'project']);\n\nexport function proposals(text: string): Proposal[] {\n const start = text.indexOf('[');\n const end = text.lastIndexOf(']');\n if (start < 0 || end < start) return [];\n let parsed: unknown;\n try {\n parsed = JSON.parse(text.slice(start, end + 1));\n } catch {\n return [];\n }\n if (!Array.isArray(parsed)) return [];\n const out: Proposal[] = [];\n for (const item of parsed as Record<string, unknown>[]) {\n if (!item || typeof item !== 'object') continue;\n const { section, title, body, tag } = item;\n if (typeof section !== 'string' || typeof title !== 'string' || typeof body !== 'string')\n continue;\n if (!section.trim() || !title.trim() || !body.trim()) continue;\n out.push({\n section,\n title,\n body,\n tag: typeof tag === 'string' && TAGS.has(tag) ? (tag as MemoryTag) : null,\n });\n }\n return out.slice(0, 20);\n}\n\ntype Input = { cone: string; after: number; tail: number; model: ModelRef | null };\ntype State = { phase: 'extract' };\nexport type ExtractResult = { saved: number; skipped: string | null };\n\nasync function save(host: Host, place: Place, found: Proposal[], context: Context) {\n let saved = 0;\n for (const proposal of found) {\n try {\n await host.files.change(place, { kind: 'save', ...proposal }, context);\n saved++;\n } catch {\n break;\n }\n }\n if (saved) await host.changed(context).catch(() => undefined);\n return saved;\n}\n\nexport function extractTask(\n ready: Promise<Host>,\n settings: (context: Context) => Promise<ExtractSettings>\n) {\n return defineTask<Input, State, ExtractResult>({\n name: EXTRACT_TASK,\n version: 1,\n initial: () => ({ phase: 'extract' }),\n phases: {\n async extract(task, runtime, context) {\n const { cone, after, tail } = task.input;\n const key = String(runtime.conversationId);\n const done = (result: ExtractResult) =>\n runtime.commit(async (tx) => {\n const doc = await tx.doc(ExtractDoc);\n doc.cursors[key] = Math.max(doc.cursors[key] ?? 0, tail);\n delete doc.pending[key];\n return { status: 'terminal', outcome: { status: 'completed', result } };\n }, context);\n if (!(await settings(context)).extract) return done({ saved: 0, skipped: 'turned off' });\n const host = await ready;\n const place = placeOf(host.home, cone);\n const view = await runtime.context(runtime.conversationId, context, {\n at: tail as EntryId,\n });\n const fresh = view.entries.flatMap((entry, index) =>\n entry.id > after ? (view.contributions[index] as readonly Message[]) : []\n );\n const transcript = transcriptOf(fresh);\n if (!place || !transcript) return done({ saved: 0, skipped: 'nothing new' });\n const ref = task.input.model ?? (await runtime.agent(context)).model;\n const model = ref ? runtime.models.getModel(ref.provider, ref.modelId) : undefined;\n if (!model) return done({ saved: 0, skipped: 'no model' });\n const current = await host.files.read(place.file, context);\n const memory = current ? serializeMemory(parseMemory(current.text)) : '';\n const now = Date.now();\n const message = await runtime.models.completeSimple(\n model,\n {\n messages: [\n { role: 'system', content: EXTRACT_PROMPT, timestamp: now } as unknown as Message,\n {\n role: 'user',\n content: [\n {\n type: 'text',\n text: `<memory_file bytes_left=\"${MAX_BYTES - new TextEncoder().encode(memory).length}\">\\n${memory}</memory_file>\\n\\n<conversation>\\n${transcript}\\n</conversation>`,\n },\n ],\n timestamp: now,\n },\n ],\n },\n { maxTokens: 2000, signal: runtime.signal, cacheRetention: 'none' }\n );\n runtime.signal.throwIfAborted();\n if (message.stopReason === 'error') return done({ saved: 0, skipped: 'model error' });\n const saved = await save(host, place, proposals(textOf(message as Message)), context);\n return done({ saved, skipped: null });\n },\n },\n abort: (_task, runtime, context) =>\n runtime.commit(async (tx) => {\n delete (await tx.doc(ExtractDoc)).pending[String(runtime.conversationId)];\n return { status: 'terminal', outcome: { status: 'aborted' } };\n }, context),\n });\n}\n\nexport type Schedule = (\n conversation: number,\n tail: number,\n context: Context,\n cone?: string\n) => Promise<boolean>;\n\nexport function compactionHook(schedule: Schedule) {\n return hook(CompactionTask, {\n beforeCompact(compaction, api) {\n const tail = compaction.entries.reduce((max, entry) => Math.max(max, entry.id), 0);\n void schedule(api.conversationId, tail, BACKGROUND_CONTEXT).catch(() => false);\n return undefined;\n },\n });\n}\n\nexport function scheduler(\n ready: Promise<Host>,\n task: ReturnType<typeof extractTask>,\n settings: (context: Context) => Promise<ExtractSettings>\n): Schedule {\n return async (conversation, tail, context, cone) => {\n const host = await ready;\n const chosen = await settings(context);\n const who = cone\n ? { kind: 'cone' as const, id: cone }\n : whoIs(host.agents.state(), conversation);\n if (who.kind !== 'cone') return false;\n return host.harness.commit(async (tx) => {\n const doc = await tx.doc(ExtractDoc);\n const key = String(conversation);\n const after = doc.cursors[key] ?? 0;\n if (doc.pending[key] !== undefined || tail <= after) return false;\n doc.pending[key] = (await tx.createTask(\n task,\n { cone: who.id, after, tail, model: chosen.model },\n {\n ownership: { kind: 'conversation' },\n conversationId: conversation as ConversationId,\n background: true,\n }\n )) as number;\n return true;\n }, context);\n };\n}\n\nexport function settingsReader(ready: Promise<Host>) {\n return async (context: Context): Promise<ExtractSettings> => {\n const host = await ready;\n const dir = host.agentDir ?? `${host.home}/.pi/agent`;\n const read = await host.env.readTextFile(`${dir}/settings.json`, context);\n return extractSettings(read.ok ? read.value : undefined);\n };\n}\n\nexport const IDLE_TASK = 'slicc.memory-idle';\n\nexport const IdleDoc = defineDoc<{ tasks: Record<string, number> }>({\n kind: 'slicc.memory-idle',\n checkpointWhen: boundedDeltas,\n version: 1,\n scope: 'session',\n initial: () => ({ tasks: {} }),\n});\n\ntype IdleState = { phase: 'watch'; tail: number; since: number };\n\nfunction changedAt(\n entries: readonly { id: number; model?: readonly { timestamp?: unknown }[] }[],\n seen: IdleState,\n now: number\n): number {\n let newest = Number.NEGATIVE_INFINITY;\n for (const entry of entries) {\n if (entry.id <= seen.tail) continue;\n for (const message of entry.model ?? [])\n if (typeof message.timestamp === 'number')\n newest = Math.max(newest, Math.min(message.timestamp, now));\n }\n if (newest === Number.NEGATIVE_INFINITY) return seen.tail < 0 ? now : seen.since;\n return Math.max(seen.since, newest);\n}\n\nexport function idleTask(\n ready: Promise<Host>,\n schedule: Schedule,\n settings: (context: Context) => Promise<ExtractSettings>\n) {\n return defineTask<{ cone: string }, IdleState, null>({\n name: IDLE_TASK,\n version: 1,\n initial: () => ({ phase: 'watch', tail: -1, since: 0 }),\n phases: {\n async watch(task, runtime, context) {\n const host = await ready;\n const conversation = runtime.conversationId;\n const key = String(conversation);\n const who = whoIs(host.agents.state(), conversation);\n if (who.kind !== 'cone')\n return runtime.commit(async (tx) => {\n const doc = await tx.doc(IdleDoc);\n if (doc.tasks[key] === Number(task.id)) delete doc.tasks[key];\n return { status: 'terminal', outcome: { status: 'completed', result: null } };\n }, context);\n const { extract, idleMinutes } = await settings(context);\n const on = extract && idleMinutes > 0;\n const idleMs = (on ? idleMinutes : DEFAULT_IDLE_MINUTES) * 60_000;\n const now = runtime.now();\n const view = await runtime.context(conversation, context);\n const tail = view.entries.at(-1)?.id ?? 0;\n const seen = task.state.checkpoint;\n const since = tail === seen.tail ? seen.since : changedAt(view.entries, seen, now);\n const next: IdleState = { phase: 'watch', tail, since };\n const due = since + idleMs;\n if (now < due) return runtime.sleepUntil(due, next, context);\n if (on) await schedule(conversation, tail, context, who.id);\n return runtime.sleepUntil(now + idleMs, next, context);\n },\n },\n abort: (task, runtime, context) =>\n runtime.commit(async (tx) => {\n const doc = await tx.doc(IdleDoc);\n const key = String(runtime.conversationId);\n if (doc.tasks[key] === Number(task.id)) delete doc.tasks[key];\n return { status: 'terminal', outcome: { status: 'aborted' } };\n }, context),\n });\n}\n\nexport function idleWatch(\n host: Pick<Host, 'agents' | 'harness'>,\n task: ReturnType<typeof idleTask>,\n context: Context\n): () => void {\n let stopped = false;\n let line = Promise.resolve();\n const ensure = () => {\n line = line\n .then(async () => {\n if (stopped) return;\n await host.harness.commit(async (tx) => {\n const doc = await tx.doc(IdleDoc);\n for (const [id, cone] of Object.entries(host.agents.state().cones)) {\n const key = String(cone.conversation);\n const known = doc.tasks[key];\n const record = known === undefined ? undefined : await tx.task(known as TaskId);\n if (record && record.state.status !== 'terminal') continue;\n doc.tasks[key] = (await tx.createTask(\n task,\n { cone: id },\n {\n ownership: { kind: 'conversation' },\n conversationId: cone.conversation as ConversationId,\n background: true,\n }\n )) as number;\n }\n }, context);\n })\n .catch(() => undefined);\n };\n const off = host.agents.onChange(ensure);\n ensure();\n return () => {\n stopped = true;\n off();\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- import type { Context } from '@earendil-works/chord';
1
+ import type { Context, JsonValue } from '@earendil-works/chord';
2
2
  import { type ConversationId, type Extension, type Harness, type SubmissionId } from '@earendil-works/pi-durable';
3
3
  import type { Approval, Approvals } from '../approvals/index.ts';
4
4
  import { type GuestPolicy } from './policy.ts';
@@ -7,6 +7,7 @@ export declare const GUEST_GUARD = "slicc-guest-guard";
7
7
  export declare const GUEST_REQUEST = "guest:";
8
8
  export declare const LICK_REQUEST = "lick:";
9
9
  export declare const GUEST_TOOL_MEMO = "slicc.guest-tool";
10
+ export declare const GUEST_GATED_MEMO = "slicc.guest-gated";
10
11
  export declare const SUMMARY_LENGTH = 2000;
11
12
  export declare const GATE_REASONS: {
12
13
  readonly unknown: 'A guest started this run, and its seat names no review for tool calls that this agent knows.';
@@ -37,6 +38,9 @@ export declare function argumentsHash(args: string): Promise<string>;
37
38
  export declare function sealedRules(rules: readonly AlwaysTool[]): Promise<AlwaysTool[] | null>;
38
39
  export declare function summaryOf(args: Record<string, unknown>): string;
39
40
  export declare function reasonOf(item: Approval): string;
41
+ export declare function gatedRun(api: {
42
+ memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
43
+ }, context: Context): Promise<boolean>;
40
44
  export type InheritedTaint = {
41
45
  seat: string | null;
42
46
  tier: string;
@@ -8,3 +8,7 @@ export type Shrink = (image: Image) => Promise<Image>;
8
8
  export declare function mimeOf(bytes: Uint8Array): string | undefined;
9
9
  export declare function base64(bytes: Uint8Array): string;
10
10
  export declare const shrink: Shrink;
11
+ export declare function fitted(image: Image, fit: Shrink): Promise<{
12
+ image: Image;
13
+ data: string;
14
+ }>;
@@ -0,0 +1,36 @@
1
+ import type { Context } from '@earendil-works/chord';
2
+ import { type ToolRegistration, type Wrap } from '@earendil-works/pi-durable';
3
+ import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
4
+ import { type Shrink } from './fit.ts';
5
+ export declare const MAX_IMAGES = 4;
6
+ export declare const MAX_MARKER = 4096;
7
+ export declare const DEFAULT_MAX_BYTES = 51200;
8
+ type Image = {
9
+ type: 'image';
10
+ data: string;
11
+ mimeType: string;
12
+ };
13
+ type Shown = {
14
+ text: string;
15
+ image?: Image;
16
+ };
17
+ export type Marker = {
18
+ at: number;
19
+ line: string;
20
+ };
21
+ export declare class Markers {
22
+ #private;
23
+ readonly first: Marker[];
24
+ count: number;
25
+ readonly budget: number;
26
+ constructor(budget: number);
27
+ get recent(): readonly Marker[];
28
+ push(text: string): void;
29
+ skip(): void;
30
+ end(): void;
31
+ }
32
+ export declare function pathOf(marker: string): string;
33
+ export declare function shown(env: ExecutionEnv, path: string, fit: Shrink, context: Context): Promise<Shown>;
34
+ export declare function placed(text: string, markers: Pick<Markers, 'count' | 'first' | 'recent'>, lineOf: (marker: Marker) => string): string;
35
+ export declare function imageMarkers(bash: ToolRegistration, fit: Shrink): Wrap;
36
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-agent",
3
- "version": "7.6.8",
3
+ "version": "7.7.1",
4
4
  "description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,7 +65,7 @@
65
65
  "release": "semantic-release"
66
66
  },
67
67
  "devDependencies": {
68
- "@ai-ecoverse/slicc-kernel": "1.153.0",
68
+ "@ai-ecoverse/slicc-kernel": "1.154.0",
69
69
  "@ai-ecoverse/slicc-scrub": "1.1.0",
70
70
  "@ai-ecoverse/slicc-shared-web": "2.12.0",
71
71
  "@ai-ecoverse/slicc-spectrum": "1.107.1",