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.
- package/CHANGELOG.md +3659 -0
- package/client/csrTypes.ts +2 -2
- package/common/index.ts +0 -1
- package/common/routeConvention.ts +1 -9
- package/package.json +1 -1
- package/server/cachePolicy.ts +12 -2
- package/server/crawler.ts +6 -0
- package/server/head.tsx +0 -4
- package/server/robots.ts +1 -23
- package/server/routeTreeBuilder.ts +4 -2
- package/server/rscWorker.tsx +12 -24
- package/server/ssrFromRscRenderer.tsx +19 -15
- package/server/ssrTypes.ts +9 -0
- package/server/webRouter.ts +6 -2
- package/types/client/csrTypes.d.ts +2 -2
- package/types/common/index.d.ts +1 -1
- package/types/common/routeConvention.d.ts +0 -2
- package/types/server/cachePolicy.d.ts +2 -1
- package/types/server/crawler.d.ts +1 -0
- package/types/server/head.d.ts +0 -3
- package/types/server/ssrFromRscRenderer.d.ts +3 -1
- package/types/server/ssrTypes.d.ts +8 -0
- package/types/ui/recipe/badgeRecipe.d.ts +2 -2
- package/types/ui/recipe/buttonRecipe.d.ts +2 -2
- package/types/webkit/useFrameRuntime.d.ts +0 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +2 -1
- package/vendor/@akanjs/native/packages/cli/src/lib/exec.ts +11 -1
- package/vendor/use-agentic/CHANGELOG.md +663 -0
- package/webkit/CsrRouteTable.ts +0 -2
- package/webkit/useCsrValues.ts +21 -11
|
@@ -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.
|