@hachej/boring-agent 0.4.2 → 0.4.4

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/NOTICE ADDED
@@ -0,0 +1,8 @@
1
+ OptChat memory includes prompt wording and algorithm adaptations from
2
+ VictorTaelin/uniichat, https://github.com/VictorTaelin/uniichat
3
+ Reference revision: 0500bf6a8ea4bf0ce047488562022b1b6532cc30
4
+ Copyright 2026 Higher Order Company
5
+ Licensed under the Apache License, Version 2.0. See licenses/Apache-2.0.txt.
6
+
7
+ Changed in Boring UI: Pi-native tasks and documents, Pi message kinds,
8
+ bounded views, host-configured retries and concurrency, and memory-only forgetting.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## One file tool set: the guard
10
10
 
11
- The agent edits files with Pi's own `read`, `write`, `edit` and `bash` over the workspace's `ExecutionEnv`; there are no document or artifact tools of Boring's. `createFileGuard({ workspace, resolveAccess?, name? })` from `@hachej/boring-agent/file-guard` returns a native extension (named `name`, default `boring.files.guard`: two agents with different workspaces in one registry need two names, or the last installed guard wraps both) that wraps those three tools with the public `wrapTool` (select it after the extension that registers them). Every wrapped call runs inside the workspace provider's mutation queue (`@hachej/boring-files/workspace`), so a viewer save cannot land between `edit`'s internal read and its write. `workspace` is the call's workspace resolved like Pi's environment, or one binding `{ files, root, access? }` (see [Workspace per call](#workspace-per-call)); `resolveAccess` defaults to the binding's `access`. A genuine `read` call records the file's revision as the conversation's last-read revision of that workspace and path (`baselineKey`) in a native conversation document (`lastReadRevisions`, kept across restarts; a fork starts empty); `edit`'s internal read records nothing. `write` and `edit` of a file that exists are refused unless the conversation has read it and its revision still equals the recorded one, and the refusal tells the model to read the file again. Creating a file is allowed only while it is absent. The guard fails closed: an unresolvable workspace, a refused provider read, a path outside the root, a link that leads outside it and a path that does not exist (so Pi's read never falls back to another spelling of it, such as an NFD name) (resolved through the env's `canonicalPath`, so also a new file under a link directory) are refused for `read` as well as `write` and `edit`; nothing falls through to Pi's native tool. After a successful `write` or `edit` the baseline is the revision just written, and the guard calls the provider's `record` (owner ruling 2: observed, never receipted): the file's history gains the written revision with source `agent` (and the bytes it replaced, read before the tool ran when that revision is not already retained), and the workspace's change feed an `agent` event with the conversation. Nothing enters the publication journal. Pi's `edit` tries an exact match first and falls back to a fuzzy one (Unicode normalisation, trailing whitespace, smart quotes, dashes and special spaces); the guard does not change that. A `bash` write is not intercepted, but it changes the revision, so the next `write` or `edit` of that file is refused until it is read; it reaches the change feed (no history) through the file system's watcher or the host's `poll()` at turn end. The guard adds no strictness option and no file API of its own.
11
+ The agent edits files with Pi's own `read`, `write`, `edit` and `bash` over the workspace's `ExecutionEnv`; Boring adds no replacement file-editing tools. The optional [document readers](#shared-document-readers) extract PDF and spreadsheet content without replacing native `read`. `createFileGuard({ workspace, resolveAccess?, name? })` from `@hachej/boring-agent/file-guard` returns a native extension (named `name`, default `boring.files.guard`: two agents with different workspaces in one registry need two names, or the last installed guard wraps both) that wraps those three tools with the public `wrapTool` (select it after the extension that registers them). Every wrapped call runs inside the workspace provider's mutation queue (`@hachej/boring-files/workspace`), so a viewer save cannot land between `edit`'s internal read and its write. `workspace` is the call's workspace resolved like Pi's environment, or one binding `{ files, root, access? }` (see [Workspace per call](#workspace-per-call)); `resolveAccess` defaults to the binding's `access`. A genuine `read` call records the file's revision as the conversation's last-read revision of that workspace and path (`baselineKey`) in a native conversation document (`lastReadRevisions`, kept across restarts; a fork starts empty); `edit`'s internal read records nothing. `write` and `edit` of a file that exists are refused unless the conversation has read it and its revision still equals the recorded one, and the refusal tells the model to read the file again. Creating a file is allowed only while it is absent. The guard fails closed: an unresolvable workspace, a refused provider read, a path outside the root, a link that leads outside it and a path that does not exist (so Pi's read never falls back to another spelling of it, such as an NFD name) (resolved through the env's `canonicalPath`, so also a new file under a link directory) are refused for `read` as well as `write` and `edit`; nothing falls through to Pi's native tool. After a successful `write` or `edit` the baseline is the revision just written, and the guard calls the provider's `record` (owner ruling 2: observed, never receipted): the file's history gains the written revision with source `agent` (and the bytes it replaced, read before the tool ran when that revision is not already retained), and the workspace's change feed an `agent` event with the conversation. Nothing enters the publication journal. Pi's `edit` tries an exact match first and falls back to a fuzzy one (Unicode normalisation, trailing whitespace, smart quotes, dashes and special spaces); the guard does not change that. A `bash` write is not intercepted, but it changes the revision, so the next `write` or `edit` of that file is refused until it is read; it reaches the change feed (no history) through the file system's watcher or the host's `poll()` at turn end. The guard adds no strictness option and no file API of its own.
12
12
 
13
13
  ## Workspace per call
14
14
 
@@ -68,6 +68,31 @@ The descriptor's `id` and `ordinal` are optional and `present` sets neither; `pa
68
68
 
69
69
  `createConvertingRead({ workspace?, resolveAccess?, convert?, cache?, maxBytes?, name? })` from `@hachej/boring-agent/file-convert` returns an extension that wraps Pi's native `read` (`wrapTool`); there is one `read`, and no second tool. Text goes to Pi's read untouched. A PDF, an office document or an image goes to the host's `convert({ name, mediaType, bytes })`, once per saved revision (cached by path and revision), and the converted text is paged the way Pi pages a text file: `offset` is the 1-based line to start at, `limit` a number of lines, a page ends at 2000 lines or 50 KB, and the note names the `offset` to continue from. What is text and what is not comes from one table, `fileKind` in `@hachej/boring-agent/file-types`, which looks at the extension and the leading bytes (Pi's read returns garbled text for a PDF, so a PDF is recognised by `%PDF-` even under another name); the mention resolver uses the same table. An archive or other binary file answers `unsupported` and is never converted; without `convert`, an image uses Pi's native behaviour and a PDF or office file answers `unsupported`. A line of converted text above 50 KB is split into 50 KB segments, each counted as one line, with a warning, so nothing is cut. The type is sniffed on the bytes the provider returns after its access check, and the converter is told the media type the file's signature names (a PDF named `.txt` arrives as `application/pdf`). The file is read through the workspace provider, so its access policy applies; a path the wrapper cannot place inside the workspace is left to the native read. Install it before `createFileGuard`, so the guard stays outermost and refuses anything outside the workspace first. `workersAiMarkdown(env.AI)` adapts a Workers AI `toMarkdown` binding to `convert` without a Cloudflare types dependency.
70
70
 
71
+ ## Shared document readers
72
+
73
+ `@hachej/boring-agent/documents` exports `documentTools({ readBytes, ocr?, images? })`, `sniff(bytes, name?)` and `readStored(path, bytes, { images })`. These are native `defineTool` registrations and a native read-result helper, not a filesystem or another `read` tool. Pi's own `read` is unchanged; `file-convert` above remains the host-conversion alternative.
74
+
75
+ ```ts
76
+ import { defineExtension } from '@earendil-works/pi-durable';
77
+ import { documentTools, readStored } from '@hachej/boring-agent/documents';
78
+
79
+ // readBytes is the host's authorized read through its one workspace provider.
80
+ const tools = documentTools({ readBytes, ocr });
81
+ registry.install(defineExtension({ name: 'app.documents', tools }));
82
+ // A host's stored-file read path can return this native ToolExecutionResult:
83
+ const result = readStored(path, await readBytes(path), { images: true });
84
+ ```
85
+
86
+ The host supplies byte access and optional OCR; this package performs no file/network I/O and holds no credentials. Bind `readBytes(path)` to the authorized workspace/view for this conversation. Read failures propagate as native tool errors. Both readers use `replay: 'safe'`; recovery may reread bytes or call OCR again, so the host owns stable revisions, current authorization and OCR cost policy.
87
+
88
+ - `read_pdf(path, pages?)` returns `{ path, totalPages, pages: [{ n, text, ocr?, truncated? }], untrusted: true }`. Page lists such as `"1-3,5"` are sorted and deduplicated, at most 30 pages per call; without a list, the first 30 pages and a `next` hint when more remain. Each page is capped at 20,000 characters. Requested pages without text go to `ocr(bytes, pageNumbers)`, which returns `{ pages?: [{ n, text? }], provider? }`. Only matching scanned pages are replaced and marked `ocr: true`; absent or failed OCR adds a note. Unlike the Factory reference's media-type argument, this callback receives the selected scanned page numbers.
89
+ - `read_spreadsheet(path, sheet?, range?)` reads xlsx, xls, ods and CSV. It returns `sheetNames` and `{ name, rows, columns, shown, csv, truncated? }` for each selected sheet, plus `untrusted: true`. The limits are 20 sheets, 200 rows per sheet by default, 1,000 with a cell range such as `A1:C1500`, and 60,000 CSV characters per sheet. Formulas use their last computed values, never execute.
90
+ - `sniff` uses magic numbers and ZIP entry names, not extensions (except CSV versus other UTF-8 text). `readStored` returns JSON text containing `{ path, content }` for text/CSV; images up to 4 MiB add native `ImageContent` when `images` is true. Other binaries, oversized images and image-incapable models return `{ path, type, size, error }`; PDF/spreadsheet refusals point to their reader. `images` affects this helper, not the two extraction tools.
91
+
92
+ Results use native `output` for the model and the same metadata in `details`. As in the reference, typed refusals are ordinary results carrying `error`, not `isError: true`. Document content is untrusted evidence, never instructions or consent. The readers set Pi's native output byte limit to 8 MiB to retain valid JSON at the extraction caps; the JSON occupies one native output line. These are returned-extraction limits, not bounds on input bytes or parser memory/CPU. Enforce input and resource budgets in the host.
93
+
94
+ The regular dependencies are pinned `unpdf@1.9.1` (MIT) and the official SheetJS `xlsx@0.20.3` HTTPS tarball (Apache-2.0), not npm's old `xlsx@0.18.5`. Both are imported lazily inside their reader only. Their notices ship in `THIRD_PARTY_NOTICES.md`. `test/packages/agent-documents.test.mjs` drives public registrations through a real Harness with generated fictional files; no live OCR provider is qualified.
95
+
71
96
  ## Approval gate
72
97
 
73
98
  `requireApproval(tool, { summarize })` from `@hachej/boring-agent/approval` wraps a native tool so each call waits for the person's Approve or Deny before `execute`. It reuses the ask-user mechanism (`askPerson` from `@hachej/boring-agent/ask-user`): a conversation document keyed by the tool call ID, answered with `answerUserQuestion` through the chat transport's `answer` operation, and shown by the `pi-chat` approval card. The check runs inside the tool's own execution in host code, so the model cannot skip it. While the call waits, the one-line `summarize` text is also published as the call's running details under `APPROVAL_DETAILS` (it stays as the result's details unless the tool returns its own). That is also how `pi-chat` recognizes a gated call: no list of tool names is needed. The card shows the summary as the headline with the raw arguments collapsed. Deny (or a stopped conversation) returns an error result starting `DENIED_PREFIX` and `execute` never runs. The wrapper is replay-safe: a pending approval survives a restart, and a retained native memo makes execution at most once (after a crash during execution the model gets an "outcome unknown" error instead of a repeat of the change). Reads are simply not wrapped.
@@ -80,7 +105,7 @@ Code mode: gated tools are native tools and must not be exposed to a code-mode s
80
105
 
81
106
  ## OptChat memory
82
107
 
83
- `@hachej/boring-agent/memory/optchat` is Victor Taelin's [OptChat](https://gist.github.com/VictorTaelin/91837951a5ce5b38f341ec1ba1df6449) (see also his [OptMem](https://github.com/VictorTaelin/OptMem)) as one native pi-durable extension: an endless conversation whose memory is the conversation itself, shown to the model as a compressed tree. Credit for the idea, the algorithm and the summarizer prompt is his; this package maps it onto Pi.
108
+ `@hachej/boring-agent/memory/optchat` is Victor Taelin's [OptChat](https://gist.github.com/VictorTaelin/91837951a5ce5b38f341ec1ba1df6449), aligned with [uniichat at 0500bf6](https://github.com/VictorTaelin/uniichat/tree/0500bf6a8ea4bf0ce047488562022b1b6532cc30) as one native pi-durable extension: an endless conversation whose memory is the conversation itself, shown to the model as a compressed tree. Credit for the idea, the algorithm and the summarizer prompt is his; this package maps it onto Pi.
84
109
 
85
110
  ```ts
86
111
  const memory = createOptChatMemory({ harness: () => harness, context, summarizer: { models, model: { provider, modelId } } });
@@ -89,15 +114,15 @@ await conversation.configure({ extensions: { add: [memory.extension] } }, contex
89
114
  await conversation.configure({ extensions: { remove: [memory.extension] } }, context); // off again
90
115
  ```
91
116
 
92
- Pi applies an extension's hooks only in conversations that select it, so memory is opt-in per conversation and there is no separate mode. A host that selects extensions by default selects it everywhere; give conversations an explicit `extensions` list to keep it opt-in. `createOptChatMemory(options)` returns `{ extension, nap(id), stats(id), zoom(id, n), date(id), forget(id), dispose() }`. Options: `harness`, `context` (long-lived, for background work), `summarizer` (required: `{ model }`, where `model` may be a function `(conversationId) => ModelRef`, sync or async, for example that conversation's own model, and `models` defaults to the Harness's; or `{ summarize(job, signal) }` returning one line), `nodeBytes` (512), `viewBytes` (128000), `jobs` (8, how many summary tasks of a conversation the host keeps live at once), `tries` (5), `cap` (30000), `retryMs` (10000), `settleTimeoutMs` (60000), `agentName`, `name`, `onError`. `dispose()` only stops this process's waits: the summary tasks belong to Pi, which keeps and recovers them, so it is harmless to skip and safe to call before `harness.close`.
117
+ Pi applies an extension's hooks only in conversations that select it, so memory is opt-in per conversation and there is no separate mode. A host that selects extensions by default selects it everywhere; give conversations an explicit `extensions` list to keep it opt-in. `createOptChatMemory(options)` returns `{ extension, nap(id), stats(id), zoom(id, leafId, n), date(id, leafId), forgetMessage(id, messageIndex), forget(id), dispose() }`. Options: `harness`, `context` (long-lived, for background work), `summarizer` (required: `{ model }`, where `model` may be a function `(conversationId) => ModelRef`, sync or async, for example that conversation's own model, and `models` defaults to the Harness's; or `{ summarize(job, signal) }` returning one line), `nodeBytes` (512), `viewBytes` (128000), `compactBytes` (32000, the compactor's separate view), `notes` (seed strings, stored once per conversation before the transcript), `workOf(message)` (host classification of subagent or job reports), `jobs` (64, how many summary tasks of a conversation the host keeps live at once), `tries` (5), `cap` (30000), `retryMs` (10000 initially, doubling after each failure to at most 60000), `settleTimeoutMs` (60000), `agentName`, `name`, `onError`. `dispose()` only stops this process's waits: the summary tasks belong to Pi, which keeps and recovers them, so it is harmless to skip and safe to call before `harness.close`.
93
118
 
94
119
  | Reference | Here |
95
120
  | --- | --- |
96
- | Append-only log of user, talk, tool and echo messages | The Pi transcript, never rewritten. Leaves are derived from the model context: a user message is `user`, assistant text `talk`, each tool call `tool`, each tool result `echo`; thinking is not logged. A leaf over `cap` characters keeps head and tail |
121
+ | Append-only log of user, talk, tool and echo messages | The Pi transcript, never rewritten. Leaves are derived from the model context: a user message is `user`, assistant text `talk`, each tool call `tool`, each tool result `echo`; thinking is not logged. A leaf over `cap` characters keeps head and tail. Seeded `note` leaves precede them; messages marked by `workOf` become one `work` leaf |
97
122
  | Summary tree: node (l, i) covers messages [i·2^l, (i+1)·2^l), at most `nodeBytes` | The same, purely binary. A short message, or two children that fit together, are their own text and cost no call |
98
123
  | The view tiles the whole chat, one `id+n\|text` line per node, fit incrementally by merging the most due pair (age / 2^(level+2)), never splitting | The same fold (`advanceFold`), kept per conversation and advanced as the log grows, so the start of the view stays the same between requests. `renderView` produces the `<chat>` block |
99
- | Each turn calls the model with [system][view][new message] | `GenerationTask.beforeRequest` replaces the messages before the current turn with the view, as the first block of the current user message. The turn in progress (the latest user message and the steps after it) stays verbatim; every positional system message is kept in order. The explanation of the view is a prompt section, so the system prompt and tools never change between calls |
100
- | Compactor: context first, no ids, scale line, resubmit with the cut at the limit, `JOBS` in parallel, retry after 10 s | The same protocol (`askModel`), run by native pi-durable tasks: each node `(level, index, fingerprint)` is one `boring.memory.optchat.node` task (shipped in the extension's `tasks`, input `{ key, level, index, of, upTo }`, one `summarize` phase) that calls the summarizer through `runtime.models` (or your `summarize`) and, in one commit, writes the node and the summarizer's usage into the extension's state. Pi schedules, runs and recovers them. See below |
124
+ | Each turn calls the model with [system][view][new message] | `GenerationTask.beforeRequest` replaces the messages before the current turn with the view, as four-line text blocks at the start of the current turn's first message. The turn in progress starts at the latest user or work message. Work reports and sibling results whose tool calls fell outside the turn become ordinary user-role input, with their full text preserved. Every positional system message is kept in order. The explanation of the view is a prompt section, so the system prompt and tools never change between calls |
125
+ | Compactor: context first, no ids, scale line, resubmit with the cut at the limit, `JOBS` in parallel, patient retry | The same protocol (`askModel`), run by native pi-durable tasks: each node `(level, index, fingerprint)` is one `boring.memory.optchat.node` task (shipped in the extension's `tasks`, input `{ key, level, index, of, upTo }`, one `summarize` phase) that calls the summarizer through `runtime.models` (or your `summarize`) and, in one commit, writes the node and the summarizer's usage into the extension's state. Pi schedules, runs and recovers them. See below |
101
126
  | A turn waits until every line of the view is a summary and never shows cut text | The request waits up to `settleTimeoutMs`; after that the missing lines show `(not summarized yet: zoom it)`, never a clipped message, and `zoom(id, 1)` returns the message whole |
102
127
  | `zoom(id, n)` and `date(id)` | Native replay-safe tools of the same names |
103
128
  | Summary files | A conversation document family (`boring.memory.optchat.nodes`, shards of 64 nodes per level, `fork: 'current'`), one fingerprint per node over the content it covers. A fork or an edit never shows a summary of different content: a stale node is unbuilt until rebuilt |
@@ -106,8 +131,8 @@ What is native, what is host-side:
106
131
 
107
132
  - **Tasks.** The host creates node tasks with `conversation.commit(tx => tx.createTask(...))` (hooks are read-only) as `background: true`, conversation-owned tasks: they never keep the conversation busy, never block or get aborted with the user's run (`conversation.abort()` leaves them; `abort(context, { background: true })` aborts them), and show in `harness.taskGraph()` and `harness.inspect()`. A pending task after a restart is resumed by Pi once the extension is installed again (the extension carries the task definition; the options must be the same).
108
133
  - **Triggers (host admission, not scheduling).** After an answer (`onYield`, once the run is idle) and when a request needs its view (`beforeRequest`). A trigger creates the tasks of the nodes whose sources exist and whose preceding view is summarized. One live task per node key, found by scanning native tasks in the same commit that creates them. `jobs` caps how many are live per conversation; Pi has no task concurrency setting, so this is the host's admission cap and Pi's scheduler decides when they run. A node's parent is created by the next trigger or by the waiting `nap` or request, once its children are built.
109
- - **Waiting.** A request waits with `waitForTask` on the live node tasks, up to `settleTimeoutMs`, then shows the placeholder. `nap` waits the same way. These waits are in-process; if the process dies the tasks are not lost, only the cascade to parents waits for the next trigger.
110
- - **Retry.** A failed node is a failed native task (`outcome.failed`). Its last failure time is kept in the conversation document `boring.memory.optchat.state` (with the summarizer call count and spend), and a trigger creates the task again once `retryMs` has passed. No timer retries: a node in its retry window is left alone, so a request that needs it shows the placeholder instead of waiting. `onError` reports the first failure of a streak.
134
+ - **Waiting.** A request waits with `waitForTask` on the live node tasks, up to `settleTimeoutMs`, then shows the placeholder. `nap` waits until all nodes are built, including retries. These waits are in-process; if the process dies the tasks are not lost, only the cascade to parents waits for the next trigger.
135
+ - **Retry.** A failed node is a failed native task (`outcome.failed`). Its failure time and streak length live in `boring.memory.optchat.state` with call count and spend. The host's idle admission loop waits from `retryMs`, doubling to at most 60 seconds, then creates another native task without a new turn. `dispose()` aborts those waits, not Pi's tasks. After a process restart, `nap` or another trigger restarts admission. Requests retain their bounded wait and can show placeholders during an outage. `onError` reports only the first failure of a streak.
111
136
  - **Usage.** In the commit that stores a node (or records its failure) the task adds the summarizer's usage, all attempts of the cut-and-resubmit loop included, to the extension's own state document (`boring.memory.optchat.state`), reported by `stats().compactor.usage`. It is not written into Pi's `pi.usage`: Pi has no public way for a task to record model usage (`recordUsage` is internal), so per BORING-PI-4 that guarantee is unavailable here rather than worked around; a public usage-recording seam is an upstream request. Hosts that budget model spend add `stats().compactor.usage` to `harness.usage()`. Task and document kinds are all in `boring.memory.optchat.*`; this package writes no `pi.*` document.
112
137
  - **Still host-side.** The trigger and admission logic, the in-memory fold and log per process (a cache; fingerprints keep it honest), and the summarizer prompt. A custom `summarize` function has no model usage to record. A model call interrupted by a crash is rerun from the start of the task, and its spend is not recorded.
113
138
 
@@ -116,10 +141,20 @@ Deviations, honestly:
116
141
  - The log is Pi's model context, not an OptChat file, so ids are positions in that context. A native compaction, reset or edit that changes earlier messages invalidates the summaries it touches (fingerprints), and the view is refolded. `beforeCompact` declines while the extension is selected: the view replaces compaction, and a native summary would cut the transcript the view is built from. That is the only effect on Pi's lifecycle.
117
142
  - The current turn is verbatim (including its tool steps) instead of being summarized to one line each as soon as it is logged; it enters the view when the next user message arrives. A user message sent mid-run starts a new "current turn" for the next step.
118
143
  - A request is rewritten once per attempt from the full context. The summaries are a cache rebuilt from the log; the summarizer's calls are not Pi generations: they run as Pi tasks and their usage is in `stats().compactor.usage`, not `pi.usage` (see above).
119
- - No cache breakpoints inside the view (provider-specific stream options are the host's), no subagent or `note` kinds, no `fsync` log or single-writer lock (Pi's storage owns durability), and the compactor prompt names only the four kinds Pi logs. Summaries are read from the documents on every request and stats call, since tasks write them.
120
- - Cost: the compactor makes about two calls per message, each carrying the view as context. Choose a cheap summarizer model; the browser-agent example uses the chat model only because it has one.
144
+ - Both views use four-line text blocks with a stable prefix. The compactor folds its own `compactBytes` view of preceding messages, independent of the chat's `viewBytes`. Admission builds preceding parents before dispatching later requests whose context would exceed this budget. Pi 1.2's `Models.complete` accepts retention preferences, but `TextContent` exposes no per-block cache marker. Blocks are unmarked; provider-specific cache-hit guarantees are unavailable. There is no separate `fsync` log or single-writer lock; Pi's storage owns durability. Summaries are read from documents on every request and stats call.
145
+ - Cost: the compactor makes about two calls per message, each carrying its small view as context. Choose a cheap summarizer model; the browser-agent example uses the chat model only because it has one.
121
146
  - The package runs in Node and in a browser (no Node-only imports). Fictional-data tests are in `test/packages/agent.test.mjs`; `examples/bot` and `examples/browser-agent` have journeys.
122
147
 
148
+ ### Forget a message
149
+
150
+ `await memory.forgetMessage(conversationId, messageIndex)` forgets one message in Pi's current model context. `messageIndex` is its zero-based position in `(await conversation.context(context)).messages`, including system-message positions. System messages cannot be forgotten. This is distinct from the leaf id used by `zoom`; seeded notes and assistant tool calls can shift leaf ids.
151
+
152
+ The operation replaces every leaf of that message with `(forgotten)` at the same positions. One native commit stores the tombstone and deletes those leaf summaries, their ancestors, later summaries and corresponding failure records. Later summaries can contain facts resolved from earlier context, so deleting only the logarithmic structural ancestors would leave copies behind. Rebuilding can therefore cost more than logarithmic work. Later leaves keep their ids and fingerprints. Native tasks rebuild the invalidated summaries. In-flight calls recheck the memory revision inside their commit and cannot restore old output. A chat request waiting on summaries also rechecks that revision before returning its rewritten messages. `nap` waits for rebuilding. Repeated forgetting is harmless. `forget(id)` only evicts the process cache and does not undo tombstones.
153
+
154
+ This changes active memory only, not requests already dispatched. Pi's session entries, native task history, underlying storage pages, provider logs, backups and copies quoted in other messages or existing forks are not erased. Indices address the current context, so a reset or history replacement requires the host to resolve positions again. Hosts must authorize the operation. Seed notes are stored once in `boring.memory.optchat.notes`; changing `options.notes` does not overwrite an existing conversation's notes.
155
+
156
+ Prompt wording and algorithm adaptations retain the upstream Apache-2.0 attribution in [NOTICE](NOTICE) and [the license copy](licenses/Apache-2.0.txt). The file headers identify the changes.
157
+
123
158
  ## Per-request prompt context
124
159
 
125
160
  `@hachej/boring-agent/prompt-context` (`createPromptContext({ context })`) is one ordinary native extension whose `GenerationHooks.beforeRequest` (registered with `hook(GenerationTask, ...)`) runs before every request attempt, retries and crash recovery included. The hook's text is used for that request only: it is recomputed each time and never appended to the transcript. The text is one user-role `<request-context>...</request-context>` message placed immediately before the round's question when the final message is the user's question, otherwise at the tail (after tool results or an assistant continuation); system messages and earlier messages are unchanged, so the stable prefix stays cacheable.
@@ -0,0 +1,240 @@
1
+ # Third-party notices
2
+
3
+ The optional `@hachej/boring-agent/documents` readers load the following dependencies
4
+ only when a document reader runs. Their upstream licenses are reproduced below.
5
+
6
+ ## unpdf 1.9.1 — MIT
7
+
8
+ Source: https://github.com/unjs/unpdf/tree/v1.9.1
9
+
10
+ ```text
11
+ MIT License
12
+
13
+ Copyright (c) 2023-PRESENT Johann Schopplich
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+ ```
33
+
34
+ ## SheetJS xlsx 0.20.3 — Apache-2.0
35
+
36
+ Distribution: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
37
+
38
+ ```text
39
+ Apache License
40
+ Version 2.0, January 2004
41
+ http://www.apache.org/licenses/
42
+
43
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
44
+
45
+ 1. Definitions.
46
+
47
+ "License" shall mean the terms and conditions for use, reproduction,
48
+ and distribution as defined by Sections 1 through 9 of this document.
49
+
50
+ "Licensor" shall mean the copyright owner or entity authorized by
51
+ the copyright owner that is granting the License.
52
+
53
+ "Legal Entity" shall mean the union of the acting entity and all
54
+ other entities that control, are controlled by, or are under common
55
+ control with that entity. For the purposes of this definition,
56
+ "control" means (i) the power, direct or indirect, to cause the
57
+ direction or management of such entity, whether by contract or
58
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
59
+ outstanding shares, or (iii) beneficial ownership of such entity.
60
+
61
+ "You" (or "Your") shall mean an individual or Legal Entity
62
+ exercising permissions granted by this License.
63
+
64
+ "Source" form shall mean the preferred form for making modifications,
65
+ including but not limited to software source code, documentation
66
+ source, and configuration files.
67
+
68
+ "Object" form shall mean any form resulting from mechanical
69
+ transformation or translation of a Source form, including but
70
+ not limited to compiled object code, generated documentation,
71
+ and conversions to other media types.
72
+
73
+ "Work" shall mean the work of authorship, whether in Source or
74
+ Object form, made available under the License, as indicated by a
75
+ copyright notice that is included in or attached to the work
76
+ (an example is provided in the Appendix below).
77
+
78
+ "Derivative Works" shall mean any work, whether in Source or Object
79
+ form, that is based on (or derived from) the Work and for which the
80
+ editorial revisions, annotations, elaborations, or other modifications
81
+ represent, as a whole, an original work of authorship. For the purposes
82
+ of this License, Derivative Works shall not include works that remain
83
+ separable from, or merely link (or bind by name) to the interfaces of,
84
+ the Work and Derivative Works thereof.
85
+
86
+ "Contribution" shall mean any work of authorship, including
87
+ the original version of the Work and any modifications or additions
88
+ to that Work or Derivative Works thereof, that is intentionally
89
+ submitted to Licensor for inclusion in the Work by the copyright owner
90
+ or by an individual or Legal Entity authorized to submit on behalf of
91
+ the copyright owner. For the purposes of this definition, "submitted"
92
+ means any form of electronic, verbal, or written communication sent
93
+ to the Licensor or its representatives, including but not limited to
94
+ communication on electronic mailing lists, source code control systems,
95
+ and issue tracking systems that are managed by, or on behalf of, the
96
+ Licensor for the purpose of discussing and improving the Work, but
97
+ excluding communication that is conspicuously marked or otherwise
98
+ designated in writing by the copyright owner as "Not a Contribution."
99
+
100
+ "Contributor" shall mean Licensor and any individual or Legal Entity
101
+ on behalf of whom a Contribution has been received by Licensor and
102
+ subsequently incorporated within the Work.
103
+
104
+ 2. Grant of Copyright License. Subject to the terms and conditions of
105
+ this License, each Contributor hereby grants to You a perpetual,
106
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
107
+ copyright license to reproduce, prepare Derivative Works of,
108
+ publicly display, publicly perform, sublicense, and distribute the
109
+ Work and such Derivative Works in Source or Object form.
110
+
111
+ 3. Grant of Patent License. Subject to the terms and conditions of
112
+ this License, each Contributor hereby grants to You a perpetual,
113
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
114
+ (except as stated in this section) patent license to make, have made,
115
+ use, offer to sell, sell, import, and otherwise transfer the Work,
116
+ where such license applies only to those patent claims licensable
117
+ by such Contributor that are necessarily infringed by their
118
+ Contribution(s) alone or by combination of their Contribution(s)
119
+ with the Work to which such Contribution(s) was submitted. If You
120
+ institute patent litigation against any entity (including a
121
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
122
+ or a Contribution incorporated within the Work constitutes direct
123
+ or contributory patent infringement, then any patent licenses
124
+ granted to You under this License for that Work shall terminate
125
+ as of the date such litigation is filed.
126
+
127
+ 4. Redistribution. You may reproduce and distribute copies of the
128
+ Work or Derivative Works thereof in any medium, with or without
129
+ modifications, and in Source or Object form, provided that You
130
+ meet the following conditions:
131
+
132
+ (a) You must give any other recipients of the Work or
133
+ Derivative Works a copy of this License; and
134
+
135
+ (b) You must cause any modified files to carry prominent notices
136
+ stating that You changed the files; and
137
+
138
+ (c) You must retain, in the Source form of any Derivative Works
139
+ that You distribute, all copyright, patent, trademark, and
140
+ attribution notices from the Source form of the Work,
141
+ excluding those notices that do not pertain to any part of
142
+ the Derivative Works; and
143
+
144
+ (d) If the Work includes a "NOTICE" text file as part of its
145
+ distribution, then any Derivative Works that You distribute must
146
+ include a readable copy of the attribution notices contained
147
+ within such NOTICE file, excluding those notices that do not
148
+ pertain to any part of the Derivative Works, in at least one
149
+ of the following places: within a NOTICE text file distributed
150
+ as part of the Derivative Works; within the Source form or
151
+ documentation, if provided along with the Derivative Works; or,
152
+ within a display generated by the Derivative Works, if and
153
+ wherever such third-party notices normally appear. The contents
154
+ of the NOTICE file are for informational purposes only and
155
+ do not modify the License. You may add Your own attribution
156
+ notices within Derivative Works that You distribute, alongside
157
+ or as an addendum to the NOTICE text from the Work, provided
158
+ that such additional attribution notices cannot be construed
159
+ as modifying the License.
160
+
161
+ You may add Your own copyright statement to Your modifications and
162
+ may provide additional or different license terms and conditions
163
+ for use, reproduction, or distribution of Your modifications, or
164
+ for any such Derivative Works as a whole, provided Your use,
165
+ reproduction, and distribution of the Work otherwise complies with
166
+ the conditions stated in this License.
167
+
168
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
169
+ any Contribution intentionally submitted for inclusion in the Work
170
+ by You to the Licensor shall be under the terms and conditions of
171
+ this License, without any additional terms or conditions.
172
+ Notwithstanding the above, nothing herein shall supersede or modify
173
+ the terms of any separate license agreement you may have executed
174
+ with Licensor regarding such Contributions.
175
+
176
+ 6. Trademarks. This License does not grant permission to use the trade
177
+ names, trademarks, service marks, or product names of the Licensor,
178
+ except as required for reasonable and customary use in describing the
179
+ origin of the Work and reproducing the content of the NOTICE file.
180
+
181
+ 7. Disclaimer of Warranty. Unless required by applicable law or
182
+ agreed to in writing, Licensor provides the Work (and each
183
+ Contributor provides its Contributions) on an "AS IS" BASIS,
184
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
185
+ implied, including, without limitation, any warranties or conditions
186
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
187
+ PARTICULAR PURPOSE. You are solely responsible for determining the
188
+ appropriateness of using or redistributing the Work and assume any
189
+ risks associated with Your exercise of permissions under this License.
190
+
191
+ 8. Limitation of Liability. In no event and under no legal theory,
192
+ whether in tort (including negligence), contract, or otherwise,
193
+ unless required by applicable law (such as deliberate and grossly
194
+ negligent acts) or agreed to in writing, shall any Contributor be
195
+ liable to You for damages, including any direct, indirect, special,
196
+ incidental, or consequential damages of any character arising as a
197
+ result of this License or out of the use or inability to use the
198
+ Work (including but not limited to damages for loss of goodwill,
199
+ work stoppage, computer failure or malfunction, or any and all
200
+ other commercial damages or losses), even if such Contributor
201
+ has been advised of the possibility of such damages.
202
+
203
+ 9. Accepting Warranty or Additional Liability. While redistributing
204
+ the Work or Derivative Works thereof, You may choose to offer,
205
+ and charge a fee for, acceptance of support, warranty, indemnity,
206
+ or other liability obligations and/or rights consistent with this
207
+ License. However, in accepting such obligations, You may act only
208
+ on Your own behalf and on Your sole responsibility, not on behalf
209
+ of any other Contributor, and only if You agree to indemnify,
210
+ defend, and hold each Contributor harmless for any liability
211
+ incurred by, or claims asserted against, such Contributor by reason
212
+ of your accepting any such warranty or additional liability.
213
+
214
+ END OF TERMS AND CONDITIONS
215
+
216
+ APPENDIX: How to apply the Apache License to your work.
217
+
218
+ To apply the Apache License to your work, attach the following
219
+ boilerplate notice, with the fields enclosed by brackets "{}"
220
+ replaced with your own identifying information. (Don't include
221
+ the brackets!) The text should be enclosed in the appropriate
222
+ comment syntax for the file format. We also recommend that a
223
+ file or class name and description of purpose be included on the
224
+ same "printed page" as the copyright notice for easier
225
+ identification within third-party archives.
226
+
227
+ Copyright (C) 2012-present SheetJS LLC
228
+
229
+ Licensed under the Apache License, Version 2.0 (the "License");
230
+ you may not use this file except in compliance with the License.
231
+ You may obtain a copy of the License at
232
+
233
+ http://www.apache.org/licenses/LICENSE-2.0
234
+
235
+ Unless required by applicable law or agreed to in writing, software
236
+ distributed under the License is distributed on an "AS IS" BASIS,
237
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
238
+ See the License for the specific language governing permissions and
239
+ limitations under the License.
240
+ ```
@@ -0,0 +1,82 @@
1
+ import type { ToolExecutionResult } from '@earendil-works/pi-durable';
2
+ import { Type } from '@earendil-works/pi-ai';
3
+ /** The media type of these bytes; the name only distinguishes CSV from other UTF-8 text. */
4
+ export declare function sniff(bytes: Uint8Array, name?: string): string;
5
+ type Refusal = {
6
+ path: string;
7
+ type: string;
8
+ size: number;
9
+ error: string;
10
+ };
11
+ type StoredDocument = {
12
+ path: string;
13
+ content: string;
14
+ } | {
15
+ path: string;
16
+ type: string;
17
+ size: number;
18
+ } | Refusal;
19
+ type PdfPage = {
20
+ n: number;
21
+ text: string;
22
+ ocr?: true;
23
+ truncated?: true;
24
+ };
25
+ type PdfResult = Refusal | {
26
+ path: string;
27
+ error: string;
28
+ totalPages: number;
29
+ } | {
30
+ path: string;
31
+ totalPages: number;
32
+ pages: PdfPage[];
33
+ note?: string;
34
+ next?: string;
35
+ untrusted: true;
36
+ };
37
+ type SheetResult = {
38
+ name: string;
39
+ rows: number;
40
+ columns: number;
41
+ shown: number;
42
+ csv: string;
43
+ truncated?: true;
44
+ };
45
+ type SpreadsheetResult = Refusal | {
46
+ path: string;
47
+ error: string;
48
+ sheetNames: string[];
49
+ } | {
50
+ path: string;
51
+ sheetNames: string[];
52
+ moreSheets?: number;
53
+ sheets: SheetResult[];
54
+ untrusted: true;
55
+ };
56
+ export type DocumentOcr = (bytes: Uint8Array, pages: readonly number[]) => Promise<{
57
+ readonly pages?: readonly {
58
+ readonly n: number;
59
+ readonly text?: string;
60
+ }[];
61
+ readonly provider?: string;
62
+ }>;
63
+ export type DocumentToolsOptions = {
64
+ readonly readBytes: (path: string) => Uint8Array | Promise<Uint8Array>;
65
+ readonly ocr?: DocumentOcr;
66
+ readonly images?: boolean;
67
+ };
68
+ /** Native result for a host's stored-file read path; this does not register or replace Pi's read. */
69
+ export declare function readStored(path: string, bytes: Uint8Array, { images }: {
70
+ readonly images: boolean;
71
+ }): ToolExecutionResult<StoredDocument>;
72
+ /** Native tool registration is the Pi seam; the host owns byte access, policy and OCR credentials. */
73
+ export declare function documentTools({ readBytes, ocr }: DocumentToolsOptions): (import("@earendil-works/pi-durable").ToolRegistration<Type.TObject<{
74
+ path: Type.TString;
75
+ pages: Type.TOptional<Type.TString>;
76
+ }>, PdfResult> | import("@earendil-works/pi-durable").ToolRegistration<Type.TObject<{
77
+ path: Type.TString;
78
+ sheet: Type.TOptional<Type.TString>;
79
+ range: Type.TOptional<Type.TString>;
80
+ }>, SpreadsheetResult>)[];
81
+ export {};
82
+ //# sourceMappingURL=documents.d.ts.map