akanjs 3.0.0 → 3.0.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.
@@ -0,0 +1,663 @@
1
+ # use-agentic
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8c31d54: The in-page chat guards the model's window, and answers a refusal for length by compacting once
8
+
9
+ Compaction used to watch one number: the transcript, estimated at four characters a token, against `compact.at`
10
+ (24k). It did not know the window, did not count the tools, the screen context or the instructions that ride every
11
+ turn, and read Hangul — nearer one character a token than four — as a fraction of what it costs. Measured on a
12
+ single live DeepSeek turn: the transcript estimate said 23 tokens where the provider counted 458. And when the
13
+ provider refused a prompt too long for its window, the turn simply failed.
14
+
15
+ **The relay now reports what it knows.** Every turn's `done` carries the provider's own count (`usage: { input,
16
+ output }`, `input` the whole prompt) and what the adaptor knows about its model (`limits: { window, output }`).
17
+ The window is declared with `option.setLlm({ contextWindow })` — a table of models would be a claim about models
18
+ that ship after it. `LlmAdaptor` gains an optional `limits`; `AnthropicLlm` reports the answer ceiling it always
19
+ sends, `OpenaiLlm` sends none and so reports none.
20
+
21
+ **The session compacts on whichever trigger comes first.** `compact.at` stays, with its 24k default, as a ceiling
22
+ on what each turn costs — the relay resends the whole transcript every turn and the app pays for each one. Beside
23
+ it, once the window is known, a guard compacts when the prompt passes `window − answer ceiling (8,192 when
24
+ unreported) − compact.buffer (13,000)`, measured by the provider's count of the last turn plus the estimate for
25
+ what arrived since. `{ at: Infinity }` leaves only the guard; `{ at: 0 }` still turns all of it off. The count is
26
+ kept on the assistant message (`ChatMessage.usage`), never sent, and dropped from what a compaction keeps.
27
+
28
+ **A refusal for length is answered once.** Adaptors build refusals through `LlmOverflow.refusal(host, status,
29
+ reason)`, which reads each provider's own "prompt too long" sentence as `agent.error.contextOverflow` with the
30
+ `limit` it named. The relay flags it `overflow` on the wire; the session drops the empty draft, compacts, sends the
31
+ same turn again and remembers the window. A second refusal in the same send fails with the translated message.
32
+ An app's own adaptor that throws through `LlmOverflow.refusal` recovers the same way.
33
+
34
+ The chat header now reads `~31k / 107k tokens` — the estimate against whichever trigger is nearer — with the point
35
+ it compacts at in its tooltip.
36
+
37
+ ## Upgrading
38
+
39
+ Nothing is required. To turn the guard on, declare the window your model has:
40
+
41
+ ```ts
42
+ option.setLlm({
43
+ apiKey,
44
+ model: "deepseek-flash",
45
+ host: "https://api.deepseek.com",
46
+ contextWindow: 1_000_000,
47
+ });
48
+ ```
49
+
50
+ A backend of your own that speaks the turn wire (use-agentic `WIRE.md`) opts in by adding `usage` and `limits` to
51
+ its `done` event and `overflow: { limit? }` to a refusal for length; one that sends neither keeps working as before.
52
+ The usage, limits and overflow fields ride the streaming answer — which is what `httpRunner` negotiates — and are
53
+ not added to the relay's single-JSON `AgentTurn` answer.
54
+
55
+ - 5f53462: A message carries the data the user pointed at
56
+
57
+ An in-page agent could be handed files but not data. What was on the screen reached the model only as the turn's
58
+ `ContextBlock`s, which are reassembled from the screen every turn — so a record named three turns ago is not in the
59
+ conversation any more, and there was nowhere to say _which_ record, or which field of it, somebody meant.
60
+
61
+ `ChatMessage.references` is that place, beside `attachments` and for the same reason: what a person referred to
62
+ while asking is part of the asking. A `MessageReference` names the host's own `refName`, the id, a label, and
63
+ optionally a dotted `path` into the document, and carries the `value` those resolved to.
64
+
65
+ Three properties are load-bearing and deliberate:
66
+
67
+ - **The value is a snapshot.** It is what the data was when the message was sent, never re-read later. Re-reading
68
+ would rewrite what the person was looking at when they spoke — and the common case is an agent that then edits
69
+ the very field it was pointed at, which would leave the reference showing the result with no record of what was
70
+ being changed from. `refName`/`refId`/`path` travel so a tool can read the current value when the answer needs it.
71
+ - **The value arrives masked, and nothing downstream can mask it again.** Masking needs the model class, which no
72
+ wire carries, so whichever model the host names when it stages a reference is the whole of the decision about
73
+ what leaves the browser.
74
+ - **It is bounded at 20,000 characters per reference, at both ends.** A reference is bulkier than a tool result and
75
+ outlives one: it rides every turn from the moment it is sent and is the last thing compaction folds, because
76
+ folding what the user pointed at is folding the question. `Reference.clipped` bounds it as it is staged, and
77
+ `AgentService.referenceLimit` bounds it again where nothing can route around it.
78
+
79
+ `AgentSession` holds the staging slot — `stage` / `staged` / `unstage(key)` / `clearStaged()` — rather than the
80
+ composer, which is where staged files live. The difference is not an inconsistency: a file only ever arrives from
81
+ the composer's own picker or drop zone, while a reference arrives from whichever component drew the data, reaching
82
+ the session it is already inside. Unstaging is keyed on `refName/refId#path`, never on an index, because what
83
+ orders the references of a message is its text.
84
+
85
+ The server folds them into the message text in one place (`AgentService.referenced`), under a heading that says the
86
+ values are a snapshot. Text is the one field every provider mapping already reads, so Anthropic, the OpenAI dialect
87
+ and the text-only default all carry references with no change between them and none of them can drop one quietly.
88
+ A string value prints as itself rather than as escaped JSON, which is the common case and the readable one.
89
+
90
+ Persistence keeps the pointer and drops the value, with a note saying so — a restored conversation re-reads what
91
+ was pointed at with a tool instead of guessing from the label, which is a better answer than a restored attachment
92
+ can give.
93
+
94
+ - 5f53462: The composer can point at data, from the menu or from the screen
95
+
96
+ Builds the user-facing half of message references on top of the carrier. There are two entry points, and which one
97
+ applies depends on who knows the value.
98
+
99
+ `<Agent.Chat reference={[…]} />` takes a `ReferenceSource` per kind of document — `refName`, a `type` in
100
+ `st.expose`'s vocabulary, the app's own `search`, and a `resolve` — and the composer's `@` menu offers whole
101
+ documents from them. Which documents somebody may point at is the app's answer, so the source brings the query; the
102
+ framework brings the token, the masking and the snapshot. The token is written the moment a row is picked and the
103
+ value is staged when `resolve` lands, so a slow fetch never freezes the menu and one that fails leaves a pointer
104
+ rather than a chip that means nothing.
105
+
106
+ `useAgentReference()` is the other half, for a field _inside_ a document. The component drawing it already holds
107
+ the value, so it hands that over with no round trip — and it is the only thing that knows a rich-text field stored
108
+ as `field(Any)` reads as a paragraph rather than as the editor document it is stored as. It no-ops with a warning
109
+ outside a session, the call `AgentValue.publishable` already makes: a card carrying a reference button must not
110
+ cost a route its render.
111
+
112
+ **The token in the draft is what carries a reference; the chip only draws it.** Deleting the token by hand drops
113
+ the reference exactly as removing the chip does, because removing the chip removes the token. Nothing re-derives a
114
+ value from edited text, so a token pasted out of an earlier message travels as a pointer with a note — the same
115
+ shape a restored conversation produces.
116
+
117
+ Staging lives on the session rather than the composer, because `useAgentReference` is called from components the
118
+ composer cannot see. Writing the token is still the composer's, so the session holds the unwritten ones as state
119
+ and a chat acknowledges each by key: two chats on one session — a responsive app rendering a desktop and a mobile
120
+ composer — each hold their own draft and each need the token, which a queue somebody drains would have given to
121
+ whichever rendered first. `refer` warns instead of staging silently when no chat is mounted on that session, which
122
+ is the case where a card sits outside the `<Agent.Zone>` its chat is inside.
123
+
124
+ References also ride the parking queue, so one pointed at while a turn is running survives to the message it
125
+ opens; taking a parked message back restores its values, and anything pointed at since wins, because the parked
126
+ value is the older read of the same field.
127
+
128
+ - ccf2ae2: feat(csr): only the current page publishes to the in-page agent, and a page can bind work to being looked at
129
+
130
+ A CSR page kept under the current one for a swipe back is mounted and live, so its `st.tool`s, resources, guides and
131
+ `st.use` keys used to sit in the agent's surface beside the current page's. The agent was offered a lever the user
132
+ could not see, and two pages declaring one name clashed.
133
+
134
+ - **`<AgentActivity active>`** (`use-agentic`) publishes what is registered below it only while `active`.
135
+ Registrations stay in place, so turning it back on re-publishes without a remount. A parked registration shadows
136
+ nothing and never clashes with a published one. Nested activities close with their parent. A registry kept outside
137
+ the surface reads the same gate through `useAgentGate()`. Under it are `AgenticSurface.gate(active, parent?)` and
138
+ `AgenticSurface.gated(gate)`, and the registration methods take the gate as an optional trailing argument.
139
+ - Every CSR page container renders one, open only for the current page. The store's live keys follow it, so
140
+ `readState` answers for the screen the user sees.
141
+ - The agent's screen targeting (`highlight`, a zone's `readScreen`, the cursor) skips anything under an `inert`,
142
+ `aria-hidden` or `hidden` ancestor, not only elements that carry one themselves.
143
+ - **`usePageFocusEffect(effect, deps)`** (`akanjs/webkit`) runs `effect` while the user is on the page and cleans it up
144
+ when they leave: after the entrance settles, and as soon as the page starts to go. It is for a camera, a poll or a
145
+ key binding that a live page under the current one must not keep running. **`usePageActivity()`** answers
146
+ `"current" | "prev" | "pending" | "hidden"`, and `current` outside a CSR stack.
147
+
148
+ ### Patch Changes
149
+
150
+ - 69f7178: feat: the in-page agent can ask the user instead of guessing
151
+
152
+ `askUser` is a fourth built-in on every turn, and the **session** owns it rather than the surface: the answer comes
153
+ from the conversation, not the screen, so it needs no declaration from the page and a zone agent asks inside its own
154
+ transcript. `AgentSession` parks the loop on `pendingQuestion` exactly as it parks on `pendingApproval`, and
155
+ `Agent.Chat` renders the question card above the composer — which is closed anyway while a turn runs, so the card is
156
+ the only way in.
157
+
158
+ `choices` offers a pick (`multiple` for several) and omitting them asks for free text. The card keeps a free-text row
159
+ either way, because the model wrote the options and only the user knows whether the right answer is among them.
160
+ Dismissing is the tool's **error** result rather than a silent empty answer, so a model that asked cannot read a
161
+ skipped question as consent; an aborted turn settles the same way. A question with no text is refused without ever
162
+ reaching the screen, and choices are trimmed and deduped because the answer is the option's own text — two identical
163
+ options cannot be told apart.
164
+
165
+ A settled ask renders as the exchange it was, question then answer, instead of as a `askUser` tool row: while the
166
+ question is pending only the card holds it, so the text is never on screen twice. A hook tool named `askUser` shadows
167
+ the built-in like any other, and the relay needed no change — the tool rides the same wire the surface's own do.
168
+
169
+ - 5f53462: An attachment can carry the host's own handle on the file, and the composer's ceilings are the app's
170
+
171
+ `MessageAttachment.ref` is opaque to the framework, which only moves it — a file id, a storage key, whatever turns
172
+ the attachment back into something a tool can be handed. Without one, a host that stores its uploads keeps a map
173
+ beside the transcript keyed on name and size, which is the guess `Attachment.same` has to make and the one that is
174
+ wrong for two crops of one export; when both sides carry a `ref`, that is now the answer instead.
175
+
176
+ The per-file ceiling is measured on what `attach` produced rather than on the file that was picked, so a reader
177
+ that uploads and answers a `url` is no longer refused for a cost it does not incur. All three ceilings become
178
+ defaults behind `<Agent.Chat attachLimits={{ perFileBytes, perMessageBytes, perMessageCount }} />`, since what one
179
+ request can carry belongs to the configured provider.
180
+
181
+ A `url` the reader answers is handed to the provider as the address it will fetch, which is now documented on
182
+ `attach`: the default storage backend serves a path only the app can resolve, and a model given one answers about
183
+ a picture it never saw with nothing anywhere reporting a failure. Answer `data` when the provider cannot reach it.
184
+
185
+ - 5f53462: fix: a batch of tool calls reports each changed resource once, as it finally stands
186
+
187
+ Every call takes its own before-and-after of the surface, so eight approvals in one turn each attached the whole
188
+ task list they approved from — eight copies in one `tool` message, each bounded at 20,000 characters on its own,
189
+ riding every later turn until compaction.
190
+
191
+ Size is the smaller half of it. The model reads the batch as one message, and by then only the last copy is still
192
+ true: the first seven are stale snapshots of a value printed directly below them. `ToolOutput.deduped` keeps the
193
+ last report of each resource and drops the superseded ones, so the batch says what changed once and says it
194
+ correctly. Dropped rather than marked, because a change entry with no value is a reason to go and read the screen
195
+ again — the round trip the change report exists to save. What each call itself did is still its own `result`.
196
+
197
+ The framework asks the model to batch independent calls, so this is now the common shape rather than the rare one.
198
+
199
+ - 5f53462: feat: the in-page agent issues independent tool calls in one turn instead of one per turn
200
+
201
+ A turn has always been able to carry several tool calls — the session collects every `toolCall` event, runs them in
202
+ order and posts them back as one `tool` message — but nothing asked the model to use it. A screen that needed ten
203
+ calls got ten turns, each paying a full model round trip and a resend of the whole transcript, and the turn cap
204
+ then parked the run on a "keep going?" card halfway through.
205
+
206
+ - `AgentService.preamble` is the framework's own half of the system prompt, ahead of whatever the app declared. It
207
+ asks for the batch — every call that does not need another call's result goes in the same turn, and a tool that
208
+ does the whole job in one call (a form's fill tool) beats one call per field — and it asks the model not to read
209
+ the screen back to confirm work it has just done, since the change report it was handed already says so. Both
210
+ sentences were measured against the provider: "approve these eight" went from 1.5 turns (with three runs in eight
211
+ doing nothing at all) to one turn in every run, and "read the screen, then approve what is pending" from 3.0
212
+ turns to its floor of 2.0 in every run. The read a fresh route needs after `navigate` is acquisition rather than
213
+ confirmation, and survived in every run of that scenario. It is composed in the service rather than in an adaptor
214
+ for the reason `explained` is — an adaptor that has to remember it is one that forgets.
215
+ - The turn cap defaults to 12 assistant turns instead of 8. A chain of ten calls could not finish under the old
216
+ default without the user answering a question first.
217
+ - `readState` and `highlight` declare `settle: false`, as `readScreen` already did. A settle is 120ms of DOM quiet
218
+ at the very least, and neither of them changes anything a resource holds, so ten reads paid a second and a bit
219
+ for a change report that is empty by construction.
220
+
221
+ - 5f53462: feat: a tool the user answers, rendered in the chat by the app that declared it
222
+
223
+ `st.tool("collectContact").desc("…").card(({ submit, cancel }) => <Contact.Form …/>)` is a second way a tool chain
224
+ ends. Where `.exec()` runs a function, a card parks the call in the chat and renders the app's own component there;
225
+ what the form submits is the call's result, and cancelling is the error the model reads instead. A name and a phone
226
+ number, a date somebody has to look up, a signature — those are answers a model must not invent, and until now the
227
+ only surfaces a chat could park on were the approval gate and `askUser`'s question, neither of which carries a
228
+ shape.
229
+
230
+ `AgentSession` holds it as `pendingCard` beside `pendingApproval` and `pendingQuestion`, and `Agent.Chat` renders it
231
+ in the same place; `AgentToolCard` is an `_overrides.tsx` slot like the other two. The frame draws its own way out
232
+ even when the app's component does not, so a turn can never park on something the user cannot dismiss.
233
+
234
+ The call waits **outside** the tool queue, for the reason an approval does: a form parked in front of somebody is
235
+ not work, and holding the execution lock across it would freeze every other agent on the page behind one unanswered
236
+ card. The declared arguments are checked before the card is parked rather than while it renders — a throw inside the
237
+ host's tree would take the chat down, where a verdict reaches the model as something it can correct — and `confirm`
238
+ is not read for a card at all, because the card is already the asking. The screen is still snapshotted around the
239
+ wait, so a card that writes what it collected into a store reports what moved like any other call.
240
+
241
+ - 69f7178: feat: the agent chat answers slash commands of its own, and the composer remembers what was sent
242
+
243
+ **Five commands join the `/` menu the app's `prompt()` endpoints already appear in**: `/new` (`/clear`) starts a
244
+ new conversation, `/retry` sends the last message again, `/copy` puts the transcript on the clipboard, `/help`
245
+ lists the commands, and `/tools` lists what this screen published. An app writes none of them and cannot add one —
246
+ the extension point for a product's own command is a `prompt()` endpoint, which is guarded and server-side.
247
+
248
+ **A built-in wins a name collision with a prompt of the same name**, and a shadowed prompt is dropped from the menu
249
+ rather than listed twice. That is the mirror image of the tool rule, deliberately: a component's `st.tool` shadows a
250
+ built-in it means to replace, but no library's prompt may take `/new` away from the user who typed it. `/new` and
251
+ `/copy` are also dispatched ahead of the is-a-turn-running check, because mid-turn is exactly when they are reached
252
+ for — so `AgentSession.reset` now ends the turn it is clearing and waits for it to wind down. It used to return
253
+ silently while one was running, and clearing before the abort lands leaves the dying turn appending onto an empty
254
+ transcript.
255
+
256
+ **A command's output is a `local` message: rendered in the transcript, withheld from the wire.** The transcript
257
+ _is_ the model's history, so `/help` text appended plainly would come back on the next turn as something the
258
+ assistant believes it said. `session.note(text)` writes one, `session.report(error)` stays what a host-side failure
259
+ lands in, and `local` messages are left out of a `/copy` export — they are the chat talking to itself.
260
+
261
+ **`/copy` exists because nothing else keeps the transcript.** The relay is stateless and the conversation lives only
262
+ in that browser, so an export is the one path a wrong answer has to whoever could fix it; it carries the route and
263
+ the timestamp for that reason. **`session.retry()`** replays only the trailing user message and leaves everything
264
+ before it in place, so a prompt's own preamble is not sent twice.
265
+
266
+ **↑ and ↓ in the composer walk what was sent.** A single-line input has nothing of its own on the vertical arrows,
267
+ and the half-written draft they were walked away from comes back at the bottom of the walk — the other half of why
268
+ a turn that fails for a reason unrelated to the ask no longer means retyping it.
269
+
270
+ - 8c31d54: fix: an in-page conversation no longer compacts away a screenshot, or most of its own earlier summary
271
+
272
+ Two faults in how the transcript summarizes itself, both reproduced against the shipped defaults.
273
+
274
+ **A picture was counted by its bytes.** The estimate is four characters to a token over the JSON a turn posts, and an
275
+ inlined image's base64 is part of that JSON — so a 300KB screenshot read as about 100k tokens, four times the 24k
276
+ threshold. The conversation compacted as soon as it passed six messages, which on a task that runs tools is the third
277
+ call, and the screenshot the user attached to ask about went into the summary as a filename. A provider bills a
278
+ picture by its pixels after its own downscale, so an inlined image now counts as `Compaction.imageTokens` (1,600)
279
+ instead. An image attached by `url`, and a text attachment, are counted as before.
280
+
281
+ **A previous summary was clipped to 1,200 characters.** The digest clipped every message's text, and the summary is a
282
+ message, so the second compaction carried only the first 1,200 characters of the first one forward — and each one
283
+ after lost more. It was also labelled `user:`, so the summarizer read the notes as something the user had said. A
284
+ previous summary now rides the digest whole, under a `previous summary:` label and outside the digest's budget, and
285
+ the summarizing instruction asks for it to be carried forward.
286
+
287
+ - f882bbc: feat!: `st.tool`, `st.expose` and `st.useState` become one chain, and a declaration's type is its mask
288
+
289
+ **Breaking.** The three declarations an app writes for the in-page agent had three different shapes and two
290
+ options that did not carry their weight. They are now one shape — `<entry>(name, …) → .desc(text) → <terminal>`,
291
+ where the terminal call is the hook — and every option left is one the runtime reads. Migration guide:
292
+ `local/tool-migration.md`.
293
+
294
+ ```ts
295
+ st.tool("removeTask", { confirm: true })
296
+ .desc("Remove one task.")
297
+ .arg("taskId", ID)
298
+ .opt("force", Boolean)
299
+ .exec(fn);
300
+ st.expose("openNote", cnst.LightNote).desc("The note on screen.").value(note);
301
+ const [tab, setTab] = st
302
+ .useState("tab", String, { set: true })
303
+ .desc("Which tab.")
304
+ .init("all");
305
+ ```
306
+
307
+ **`.desc()` is required, and it is now load-bearing twice.** A model picks a tool by that sentence and by nothing
308
+ else, so a tool without one was a tool an agent could only guess at. It also replaces `shared: true`: a row
309
+ component registering fifty `removeTask` is fifty copies of one name _and one description_, which the surface now
310
+ reads as one declaration, while a second description arriving under a name already taken is two components that
311
+ collided and still warns. `shared` was an unverified claim whose only effect was suppressing that warning — a flag
312
+ that said "trust me" where the description already says the same thing, checkably.
313
+
314
+ **`effect` becomes `settle`, because only one of its three values ever did anything.** `"query"` skipped the wait
315
+ for the screen to settle before the call's effect was reported; `"state"` and `"mutation"` were indistinguishable
316
+ at runtime, were dropped before reaching the model, and coloured a badge. What is left is the fact the session
317
+ actually needs: `{ settle: false }` is a read that returns what is already there, and everything else is waited
318
+ out because a write may still be landing when `exec` resolves. `effect` also leaves `PublishedTool`, the relay
319
+ wire (`AgentWireTool`, `WIRE.md`) and the `/tools` dock badge.
320
+
321
+ **`.arg()` is required and `.opt()` is optional**, matching the filter builder's vocabulary, so an optional
322
+ argument is a different call rather than an options bag — and the `exec` parameter widens to `| null` only for
323
+ `.opt`.
324
+
325
+ **A readable value declares its type, and the type is the mask.** `st.expose(name, Type)` and
326
+ `st.useState(name, Type)` take a scalar, an enum, a model class, a one-level array of those, or `Any`. The type
327
+ typechecks what the component hands over — a model resolves to its state object, so a hydrated document and a
328
+ plain copy of one are equally accepted — and it decides how the value reads: a model strips its own `hidden`,
329
+ `secret` and `visual` fields by the model that was _named_ rather than by whatever class the value still carries,
330
+ and a `Date` leaves as an ISO string. That subsumes `mask:` and `serialize:`, both of which are gone; `Any` is the
331
+ escape hatch and passes the value untouched. A type nothing can read is reported on the console and left
332
+ unpublished rather than thrown, the same degradation an undescribable `.arg` already had. `.value()` also takes a
333
+ thunk, read when the agent reads, for a value assembled out of a ref the children fill in after the render.
334
+ `st.useState`'s `set` is now a boolean, since the write schema comes from the same type.
335
+
336
+ - 5f53462: A stopped turn leaves no bubble that is still writing
337
+
338
+ `AgentSession` opens an assistant draft before the first token arrives, and Stop caught before that token left the
339
+ draft in the transcript forever. `Transcript` already drops an empty assistant message, so the wire and the
340
+ persisted history were both correct and the rendered array was the one place it survived — which is exactly the
341
+ array a bubble reads to decide it is still being written, so the pulsing dot never stopped. The turn's own settle
342
+ now drops it, using the predicate `Transcript` already applies. A turn a provider ends without saying anything
343
+ leaves nothing behind either, for the same reason.
344
+
345
+ - 5f53462: An attached image shows up, and survives a reload with its handle
346
+
347
+ The attachment chip only drew a thumbnail for an inlined `data` attachment, so an image attached by `url` — the
348
+ shape the per-file ceiling exists to encourage, and the only shape a deployed app produces — rendered as a bare
349
+ name in the composer and in every bubble. It draws either carrier now.
350
+
351
+ `sessionHistory` dropped `ref` on the way to storage. The reason content is stripped is that web storage is a few
352
+ megabytes and a failed save is silent; an id is not content, and it is the one thing a restored conversation has
353
+ left to find the file again — without it a host is back to guessing from name and size, which is the guess `ref`
354
+ was added to retire.
355
+
356
+ `Chips` is exported as `AgentAttachments`, so a replaced `AgentBubble` draws the attachment row with the framework's
357
+ rule rather than a copy of it. And a compacted attachment now says its content is gone instead of only naming
358
+ itself, on the same principle as every other note here: a model told only `[attached photo.png]` answers about the
359
+ picture from its filename.
360
+
361
+ - 69f7178: feat: akanjs/ui drives itself for an in-page agent
362
+
363
+ The framework's own components now publish the controls they draw, so an app gets a working agent surface for
364
+ list management, tabs, paging, forms, dialogs, and the app shell without writing a line. Names are the store
365
+ action's wherever one exists (`setSortOfTaskInOrg`, `removeTask`), so the tool an agent calls and the action the
366
+ button dispatches read the same.
367
+
368
+ - **`Data.ListContainer`**, and so every `Model.AdminPanel`: view mode, sort, page size, refresh, create, the two
369
+ exports, and the row and modal verbs — `edit`/`view`/`remove` by id, plus `submit`, `cancelEditOf`,
370
+ `closeViewOf`. It also opens a `<slice>.items` resource, which is where the id for a row verb comes from.
371
+ - **`Tab`**, **`Dialog`**, **`ScreenNavigator`** take a `namespace` prop and publish under it. Without one they
372
+ publish nothing: two tabs on one screen would otherwise answer to a single name, and the first to mount would
373
+ lose. `Tab`'s menu registry became a `Map` of menu to disabled, which also stopped the disabled-tab fallback
374
+ from landing on another disabled tab.
375
+ - **Paging** is one shared `usePageTool`, spoken by all three components that draw a pager.
376
+ - **`Layout.Sider`**, **`System.SelectLanguage`**, **`Link.Back`** publish the shell controls.
377
+
378
+ **Forms fill themselves, from two sides.** An app writes no `st.tool` for a form.
379
+
380
+ A form control handed its setter **by reference** — `onChange={st.do.setTitleOnTask}` — publishes
381
+ `setTitleOnTask` while it is on screen. That is the same reference that already earned `data-akan-action`, so the
382
+ agent's tool and the person's control are one function, and an inline arrow still publishes nothing. Scoping
383
+ publication to the control rather than to the model is what keeps an agent out of a field the template draws
384
+ nothing for.
385
+
386
+ `st.use.taskForm()` adds one more: `fillTaskForm(patch)`. It takes several fields in a single call, and it is the
387
+ only way to reach a list, a map, or an embedded object — their rows are written through
388
+ `writeOnTask("payments.3.name", value)`, an inline call that can carry no annotation. It is a patch, so a field
389
+ left out keeps what the person typed. Its schema is every writable field, because a declaration is mount-static;
390
+ the **guard** is where the screen gets its say, refusing a plain field whose control is not on screen and naming
391
+ the ones that are. A composite is let through, because nothing can see whether its rows rendered — the one place
392
+ left where an agent reaches a field the screen may not draw, and server guards still apply.
393
+
394
+ Neither side touches a relation (picked or uploaded, never typed), a base document field, or a `hidden`/`secret`
395
+ one **at any depth**: a read of those is masked, so publishing a writer would open the door its reader is barred
396
+ from. A rejected third shape is worth naming — a single `writeOnTask(path, value)` tool — because its `value`
397
+ could only be `Any`, which this framework's own MCP layer refuses for telling a model nothing, and a mistyped
398
+ `path` writes a new key into the form that ships on the next submit.
399
+
400
+ Nothing publishes a lever the screen does not have. A model with fewer than two sort keys draws no sort control
401
+ and has no `setSortOf<Model>`; a panel with no template draws no create button and has no `new<Model>`; a list
402
+ that fits on one page has no pager and no `setPageOf<Model>`. Callables go to the controls by reference, so
403
+ `data-akan-action` lands on them and `readScreen` names each control with the word its tool has.
404
+
405
+ Three gaps in the surface had to close first, all of them consequences of the surface being declaration-only.
406
+
407
+ **A conditional surface had no legal shape.** `.exec()` is a hook, so a component can never skip the declaration —
408
+ which left no way to publish a tool only when the screen renders its control. A falsy name now declares the tool
409
+ and publishes nothing: the callable still drives the click a person makes, and the agent never learns it exists.
410
+ `st.useState` and `st.expose` take a falsy name the same way. An unpublished callable carries no
411
+ `data-akan-action`, because that attribute names a tool an agent can reach and this one cannot. This is what lets
412
+ a list toolbar publish `setSortOfTask` only when it actually draws the sort control, instead of paying for the
413
+ tool in every turn's prompt on every screen.
414
+
415
+ **A value set only the render knows had no way in.** An `enumOf` class was already a complete argument type —
416
+ `.arg("mode", TaskStatus)` publishes the values, refuses anything off them, and narrows the `.exec` parameter to
417
+ the union — but a component cannot build one, because `enumOf` registers globally. A slice's sort keys or the
418
+ options a prop carried could only be described in prose and hoped for. `.arg(name, type, { oneOf })` takes the
419
+ list the render has and publishes and enforces it the same way. Neither reaches a set that fills in _after_ the
420
+ first render, since a declaration is mount-static; that belongs in the tool's `guard`, which is re-read per call
421
+ and can name the current values in its refusal — which is what `Tab` and the pagers do.
422
+
423
+ **A list inside an `Agent.Zone` was invisible to that zone's own agent.** `useScreenScope` opened its scope at the
424
+ root rather than under the scope it is mounted in, so `Load.Units` inside a zone registered `<slice>.items` at the
425
+ top. A zone view only sees keys in its own subtree, so the root agent could read the list and the zone agent
426
+ looking straight at it could not.
427
+
428
+ - 5f53462: A turn the provider cut off says so, instead of passing as a finished one
429
+
430
+ `LlmTurnAnswer.stop` gains `"length"` — `finish_reason: "length"`, `stop_reason: "max_tokens"` — and it rides the
431
+ wire and the `AgentStop` enum to the browser. It is not one provider's quirk: every adaptor here reports it,
432
+ including the default, and only the shape of the union was hiding that.
433
+
434
+ Without it a truncated answer arrived as `stop: "end"` and the user read half a sentence as the whole reply. Worse
435
+ on an agent surface: a turn cut off mid tool call carries no complete call, so it ended the loop looking exactly
436
+ like a model that had decided it was done — a hang the transcript explained as a choice. The session now records
437
+ the turn as incomplete on the assistant message the user is reading, and closes any call the turn did make rather
438
+ than running it, since a batch the model was interrupted inside is half an intention.
439
+
440
+ The ceiling wins over the calls that did arrive, in both adaptors and on both the streamed and whole paths.
441
+
442
+ - 69f7178: feat: the in-page agent waits for the screen, aims its reads, points at things, and says what it is doing
443
+
444
+ **A tool that changes the screen now waits for the screen before it answers.** `router.push` returns while the RSC
445
+ payload for the new route is still in flight, so `navigate` used to report success onto a page that had not
446
+ rendered yet: the `readScreen` in the same turn read the page the user had just left, and the new route's tools were
447
+ not registered. `ScreenSettle.wait()` waits for DOM quiescence — bounded, and quiescence rather than a framework
448
+ signal because the client router hands its promise to nobody and a change may land in the store, in a refetch, or
449
+ in a streamed Suspense boundary. `navigate` awaits it, and `AgentSession` awaits it after every non-`query` tool
450
+ before taking the change report, so an optimistic action that commits a tick later is reported as what it did
451
+ rather than as the moment before it. Tools and state from a fresh route are still only listed from the next turn —
452
+ the catalogue is snapshotted when a turn starts — and the navigate result says so.
453
+
454
+ **`goBack` joins `navigate` as a global built-in.** It was briefly declared by `Link.Back`, which made it exist
455
+ only where a back link happened to be rendered — but history is not a control a page owns: every route has a
456
+ previous page, the browser's own gesture is always there, and a page that draws no back link is not a page you may
457
+ not leave. It refuses at call time when nothing is behind the current page instead of walking out of the app.
458
+
459
+ **`readScreen` takes a `section` and there is a new `highlight(target)`.** Both resolve a name the agent has
460
+ already seen rather than a selector it invented: a `data-akan-action` / `data-akan-state` annotation (the one
461
+ `readScreen` prints beside a control), an `Agent.Zone` or `useScreenScope` container, an element id, or a heading
462
+ by its own text — matched on letters and digits, so the slug an agent writes for a heading it read resolves. That
463
+ tolerance stops at headings: a heading is a landmark and scrolling to the wrong one costs nothing, while two
464
+ buttons reading "Save" are not the same control. Nothing hidden resolves at all, because a ring nobody can see
465
+ reads as a broken tool rather than as a miss. A name that resolves to nothing is the caller's mistake, with a
466
+ refusal that lists the sections actually on screen.
467
+
468
+ **A screen is only aimable if its names are printed**, which is what the first version got wrong: a page of twenty
469
+ `Scroll.Slide` sections answered "nothing on this screen carries a name", because `readScreen` printed the heading
470
+ text without its anchor and the truncation note stopped at a character count. Headings now carry `(#anchor)` when
471
+ they open an id'd or scoped container, and a truncated read ends with the headings below the cut — otherwise
472
+ everything past the 8000-character limit is unreachable, since nothing names it. `highlight` flashes **after the
473
+ scroll lands**: a smooth scroll across a long page outlasts a flash begun at the top, which is the same bug wearing
474
+ a different hat. `useScreenScope` now hands
475
+ back its scope path and `Load.Units` / `Load.View` / `Data.ListContainer` put it on the container they render, so
476
+ every list and detail view on an akan screen is addressable with no app code. `highlight` scrolls its target into
477
+ view and flashes it on the app's own primary token: it is the one built-in that exists for the _user's_ benefit,
478
+ because showing where a control is beats writing directions to it.
479
+
480
+ **A slow tool can say what it is doing.** `AgentProgress.report(message, { done, total })` is the browser twin of
481
+ `McpProgress.report` — reached through a module slot rather than a parameter, so work several frames down (a store
482
+ action, an upload loop, an adapter) reports without every signature growing a channel argument, and a no-op when
483
+ nobody is rendering it. The chat shows the report on that call's row until the row resolves. A session runs tool
484
+ calls one at a time, which is why the browser needs no `AsyncLocalStorage` to do this.
485
+
486
+ **The turn cap is a question instead of a dead end.** At `maxTurns` the session asks whether to keep going through
487
+ the same card `askUser` uses, and the answer rides as the user's own turn — so a steer typed instead of the
488
+ keep-going choice reaches the model as guidance rather than being swallowed. A host that renders no
489
+ `pendingQuestion` passes no `continueAsk` and keeps the old failure, because asking with nobody listening would
490
+ hang.
491
+
492
+ - 69f7178: feat: the in-page agent waits out long work instead of polling it, and Stop reaches a tool that is still running
493
+
494
+ **An agent that started something slow had one way to learn it finished: ask again.** Every look is a full model
495
+ round trip, so a two-minute video generation burned the default eight-turn budget in about twenty seconds and read
496
+ to the user as check → wait → check → wait, forever. Two things change.
497
+
498
+ **A tool that awaits its own work now holds the turn, with no model round trip in between.** This always worked —
499
+ the session awaits `#execute` and `AgenticSurface.call` awaits `entry.run` — but nothing said so and nothing made
500
+ it safe, so apps wrote fire-and-forget tools and left the agent to poll. The shape is one `st.tool` whose `.exec`
501
+ awaits the store action that finishes the job; the change report that follows carries whatever landed while it
502
+ waited, so the model needs no second call to read the result.
503
+
504
+ **Stop now ends a turn parked inside a tool.** `AgentSession` passed its abort signal to the approval and question
505
+ cards and nowhere else, so a call that took two minutes held the loop for two minutes after the user pressed Stop,
506
+ with the chat still showing a turn in flight. Latent until now, and a certainty the moment a tool is allowed to
507
+ wait. The session races every call against the signal, and hands the signal to the tool through `AgentAbort` — the
508
+ module slot `AgentProgress` already is, for the same reason: work several frames down reads it without every
509
+ signature between here and there growing an argument, and a session runs tool calls one at a time. Honouring it is
510
+ optional, since the race lands whatever the tool does; what it buys is the tool's own cleanup, such as a timer that
511
+ would otherwise tick out its whole timeout with nobody left to answer. A tool that ignores it is left running
512
+ rather than cancelled — the work is usually a job a server is already doing, and throwing away a result that is
513
+ about to land helps nobody.
514
+
515
+ **`AgentAbort` and `AgentProgress` are re-exported from `akanjs/store`.** `AgentProgress` had no export path an app
516
+ could legally use, `use-agentic` being a third-party import from `apps/**` and `libs/**`, so the documented advice
517
+ to report progress from a slow tool was not followable. A tool that can neither report progress nor honour Stop is
518
+ exactly the tool this release exists to make writable.
519
+
520
+ - 69f7178: feat: component-level in-page agent — a chat that reads the rendered screen and drives it
521
+
522
+ The agent surface work so far described what an app _can_ do (the store catalogue, the MCP catalogue); this ships
523
+ the half that knows what the _screen_ is doing and lets a user hand it the wheel. One mount —
524
+ `<Agent.Chat />` in a layout — is the whole integration.
525
+
526
+ **`use-agentic`** is the new framework-independent core: a mount-lifetime surface
527
+ (`registerTool`/`registerResource`/`openScope`/`registerGuide`, name stacks where the newest registration wins and
528
+ unregistering restores what it shadowed), hooks (`useAgentState`/`useAgentTool`/`useAgentResource`/`useAgentGuide`,
529
+ declarations mount-static and behavior always-latest), and `AgentSession` — the client-side conversation loop:
530
+ send → model turn → tool calls → approval gate → execute → report resource diffs → next turn. The loop lives in the
531
+ browser because the tools do; the server is one stateless turn relay that never executes anything. Failures land in
532
+ the transcript rather than being thrown past it. The wire is documented in `WIRE.md` so any backend can serve it.
533
+
534
+ **The screen context is derived, not declared — and it follows the rendered screen, not the bundle.**
535
+ `Load.Units`/`Load.View` register scopes and curated item lists as they mount (labels come from the `text: "title"`
536
+ search role), `StoreInstance` counts which state keys the mounted components are reading — `st.use`, `st.sel`, and
537
+ `st.ref` all count, the selector-based pair by running the selector once over a recording proxy — and a store's
538
+ catalogued actions and state are published only while one of its keys is live. `AgentContext` assembles route +
539
+ screen + live-state blocks per turn — primitives inline, everything else one masked `readState(key)` call away.
540
+ `remove*` names default to a confirm gate, and the framework adds two built-ins beside `readState`: `navigate`
541
+ (internal paths only, the same router `Link` rides) and `readScreen`, which serializes the rendered DOM into
542
+ compact text — headings, links, control values with their `data-akan-*` annotations — so "what does this page
543
+ say?" is answerable; the chat's own UI is skipped via `data-agent-ui` and a password value is never read.
544
+
545
+ **Exposure is the store author's to trim.** `static agent = false` keeps a whole store off the surface — the
546
+ framework's base store declares it, which also stops `readState` from reaching the `tryJwt` credential — and
547
+ `static agent = { exclude: [...] }` withholds named actions and state keys; `st.use.x({ agent: false })`
548
+ subscribes without counting toward liveness. Generated `set<Key>` conveniences are no longer published at all:
549
+ they carried no schema, so they listed as zero-argument levers that wrote `undefined`.
550
+
551
+ **`Agent.Chat`** is the user-facing half: launcher, transcript, composer, and the inline approval card, behind an
552
+ `ssr: false` lazy boundary so none of it touches the server HTML, and re-skinnable through the `AgentChat`
553
+ `_overrides.tsx` slot. Its default runner posts the wire to the app's own `runAgentTurn` route through
554
+ `httpRunner`, which negotiates streaming via `accept`: the relay answers `text/event-stream` — one `RunnerEvent`
555
+ per SSE `data:` line, assistant text arriving as it is generated — and a server that does not stream answers the
556
+ same JSON turn. The endpoint reads the request with `.with(Req)` and returns a raw `Response` for the SSE half;
557
+ `LlmAdaptor.chat` gained an optional `onDelta`, and an adapter that ignores it still answers whole. An app that
558
+ mounts no relay degrades to a transcript message. `ThemeToggle` now publishes the current `theme` and a
559
+ `setTheme` tool, so "switch to dark mode" works out of the box wherever it is mounted.
560
+ A tool call is **one row** that resolves in place — pending, then `✓`/`✕` with the result's error or change count
561
+ — rather than a badge on the assistant message and a second row for its result: the model needs both wire
562
+ messages, but the user watched one thing happen, and the name appearing twice read as a doubled call. The row
563
+ carries the call's arguments, because two searches are otherwise the same row twice.
564
+ `<Agent.Guide instructions="..." />` layers route-scoped guidance by render position: nested Guides concatenate,
565
+ navigation withdraws them. `prompt()` endpoints double as the chat's slash commands with no listing endpoint —
566
+ the client reads its own serialized signals, and the prompt's GET enforces its guards at call time.
567
+
568
+ **The relay is framework-embedded.** `runAgentTurn`, the `agentTurn` scalar, `AgentTurnStream`, and the
569
+ `AgentRelayAccess` guard ship with `akanjs` itself, registered the way the `base` module is — every app serves the
570
+ relay with no lib to mount, `AKAN_AGENT=false` takes it off, and a lib that still carries its own `agent` module
571
+ wins the refName so older workspaces keep working. `OpenaiLlm` (chat-completions REST, zero SDK dependencies)
572
+ is the predefined default behind a new `LlmAdaptorRole`; an app swaps providers in its `option.ts` with
573
+ `applyAdaptor(LlmAdaptorRole, OwnLlm)` — the same builder family as `applyMiddleware`, and the override mechanism
574
+ works for every predefined adaptor role. The endpoint stays outside MCP — its `Any` bodies are refused from the
575
+ catalogue — and `AgentRelayAccess` defaults to allow but now **warns at boot while no policy is registered**; a
576
+ policy that throws fails closed.
577
+
578
+ **An app configures its server in `lib/option.ts`, not through the environment and not in `main.ts`.** `AkanOption`
579
+ gained three setters, each read from every lib in mount order with the app's own last, so an app tightens what a
580
+ library declared without restating it. `setLlm({ apiKey, model, host })` — or `setLlm((options) => …)` to take the
581
+ key out of the app's own gitignored env object — reaches whichever adaptor holds `LlmAdaptorRole` as the
582
+ `llmOption` use, replacing the per-provider environment names; the settings belong to the role
583
+ rather than to one provider, so they survive a swap. `setAgentAccess(SignedIn)` names the guards
584
+ `AgentRelayAccess` forwards to, which `AgentRelayAccess.use` takes at boot. `setMcp({ … })` carries the MCP server
585
+ settings that `new AkanApp("./server", { mcp })` used to spell as child environment variables — the gateway there
586
+ only spawns children, while `option.ts` is already handed to the process that mounts `/mcp`, so `AkanAppOptions.mcp`
587
+ is gone. Every `AKAN_MCP_*` env spelling still works for a deployment configuring what the source does not, and an
588
+ option written in code still wins over the env of the same name.
589
+
590
+ **`<Agent.Zone id="comments">` runs a second agent over one section, in parallel with the root.** Zones are views
591
+ of the same surface, never walls: everything mounted inside — `st.use` subscriptions (liveness is now tagged with
592
+ the ambient scope), hook tools, guides — belongs to the zone's own conversation _and_ stays visible to the root
593
+ agent, so wrapping a section costs the root nothing. An `Agent.Chat` inside binds to the zone session
594
+ automatically, a zone's `readScreen` reads only its own `data-agent-zone` container, and guides follow the layout
595
+ cascade — ancestors and own, never a sibling's. The core grew `surface.view(path)` (`SurfaceView`), and
596
+ `AgentSession` now runs over any view.
597
+
598
+ **`persist` keeps the transcript across reloads.** Off by default; `<Agent.Chat persist />` stores settled
599
+ messages in sessionStorage (per-tab, gone when the tab closes), `{ storage: "local" }` outlives it, and each
600
+ `Agent.Zone persist` keys its own entry by scope path. Restores drop the assistant draft a reload cut short,
601
+ saves are debounced against streaming deltas, a versioned envelope discards stale wire shapes, only the newest 50
602
+ messages are kept, and a full or blocked storage never breaks the chat. The chat header gained a clear button
603
+ that empties both the transcript and the stored copy.
604
+
605
+ - 5f53462: feat: the agent's pointer lives for the turn, and waits out the model's own thinking
606
+
607
+ The pointer's life was measured in the gap between **calls** — 1.4 seconds of quiet and it faded. But a model's
608
+ calls arrive with its own writing between them, which takes several seconds, so a turn of four calls was four
609
+ pointers: each one appeared, pressed, and vanished before the next arrived. The unit was wrong.
610
+
611
+ `AgentSession` now reports the boundary that matters. `onTurn(running)` fires once when a turn starts and once
612
+ when it settles, and only the conversation loop reports there: `compact` runs under the same internal flag and
613
+ drives nothing on screen, so a host drawing the agent at work would otherwise draw it for a summary nobody asked
614
+ to watch. Between the calls of one turn the pointer stays where it last landed and turns into a spinner, because a
615
+ pointer sitting perfectly still for four seconds reads as stuck rather than as waiting.
616
+
617
+ It still appears only at the first control it presses, and **a turn that drove no control draws no pointer at
618
+ all** — parking one in a corner for a turn that only answered a question would be the screen claiming something
619
+ that did not happen.
620
+
621
+ While a reveal scrolls the page to the next control, the pointer holds still the way a person's does and carries a
622
+ chevron pointing the way the view is travelling — stillness over a sliding page otherwise reads as a pointer that
623
+ has come loose rather than as the one doing the scrolling. The scroll budget is now spent per turn as well, instead
624
+ of per quiet second, which is what it was always guessing at.
625
+
626
+ **`navigate` draws again, but only on an element.** The link a navigation is going to, when exactly one visible
627
+ link goes there, is pressed before the route moves — which needs the call to wait, so `ToolRunner` now awaits the
628
+ `start` activity and `AgentSession.onActivity` may answer a promise. `AgentVisual` returns one for that call alone
629
+ and caps it at 600ms; everything else still draws while the call is already running. An off-screen link is left
630
+ alone, because scrolling to a link and then leaving the page it is on is two motions for one act. **Link presence
631
+ decides what is drawn, never what is allowed**: the agent may still navigate anywhere the user could type, and a
632
+ zone that must stay on one screen drops `navigate` through `builtins` as before.
633
+
634
+ `Router.routeOf(pathname)` is what compares the two addresses — the locale and base-path segments live on the
635
+ `<a href>` and never on the tool argument. It is `getPath` without the browser guard, which `getPath` only needs
636
+ for its own default argument.
637
+
638
+ **The pointer clears the control before it waits.** A spinner parked on the button it just pressed reads as one
639
+ stuck to it, and it covers the very change the press caused — a person clicks and takes the hand away. It drifts
640
+ just past the control's box, down and to the right unless the viewport edge is there, and spins from that spot.
641
+
642
+ **A control the screen is not actually showing is not pointed at, and the pointer goes rather than travelling to
643
+ it.** `checkVisibility` answers about an element alone, so one under a modal's backdrop, inside a drawer that has
644
+ slid off, or faded to nothing all pass it while being invisible to the user — and a pointer sent there lands on a
645
+ blank patch of overlay, which reads as the effect being broken rather than as the agent acting. The test is
646
+ `elementFromPoint` at the control's own centre, taken after the reveal has settled, with an ancestor counting as a
647
+ hit so a `<label>` wrapping its input is not mistaken for an occlusion.
648
+
649
+ **`agentAttrs(handler, key)` tells namesakes apart.** One tool serves a whole tab strip or a whole list, so every
650
+ control carrying it was interchangeable in the DOM and the no-guessing rule turned that into drawing nothing.
651
+ A key — in the same vocabulary the call's argument uses — lets the pointer pick the one the call named; short of
652
+ exactly one answer nothing is still drawn. `Tab.Menu` now passes its own `menu` key, so switching a tab is drawn
653
+ on the menu that was switched to.
654
+
655
+ `reveal` and `cursor` are now independent, as their names always claimed: `visual={{ reveal: false }}` keeps the
656
+ pointer and drops the ring, where before it dropped both.
657
+
658
+ - 2d8de1c: fix: the `use-agentic` entry is no longer marked `"use client"`
659
+
660
+ The entry re-exports every module with `export *`, and a client boundary cannot re-export that way — its export names
661
+ must be visible to the bundler. Every module that needs the client already carries its own `"use client"`, while
662
+ `AgentSession`, `httpRunner` and `Reference` are server-safe and are used on the server, so the directive on the entry
663
+ only claimed a boundary it did not draw. Nothing an importer receives changes.