cortena-ui 1.5.0 → 1.7.0
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 +183 -0
- package/README.md +122 -2
- package/dist/agent-chat/bridge.js +8 -4
- package/dist/agent-chat/bridge.js.map +1 -1
- package/dist/agent-chat/session.d.ts +22 -1
- package/dist/agent-chat/session.js +164 -8
- package/dist/agent-chat/session.js.map +1 -1
- package/dist/agent-chat/step-label.d.ts +99 -0
- package/dist/agent-chat/step-label.js +116 -0
- package/dist/agent-chat/step-label.js.map +1 -0
- package/dist/agent-chat/store.d.ts +90 -4
- package/dist/agent-chat/store.js +421 -28
- package/dist/agent-chat/store.js.map +1 -1
- package/dist/agent-chat/types.d.ts +101 -0
- package/dist/agent-chat/types.js.map +1 -1
- package/dist/agent-chat.d.ts +6 -5
- package/dist/agent-chat.js +5 -4
- package/dist/components/agent-chat-popup.d.ts +2 -2
- package/dist/components/agent-chat-popup.js.map +1 -1
- package/dist/components/agent-chat.d.ts +191 -15
- package/dist/components/agent-chat.js +300 -32
- package/dist/components/agent-chat.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/toast.d.ts +1 -1
- package/package.json +3 -2
- package/src/agent-chat/bridge.ts +17 -5
- package/src/agent-chat/session.ts +193 -6
- package/src/agent-chat/step-label.ts +177 -0
- package/src/agent-chat/store.ts +692 -24
- package/src/agent-chat/types.ts +106 -0
- package/src/components/agent-chat-popup.tsx +3 -3
- package/src/components/agent-chat.tsx +737 -147
- package/src/entries/agent-chat.ts +38 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# cortena-ui
|
|
2
|
+
|
|
3
|
+
Notable changes per release. Versions before 1.6.0 are recorded in the git log
|
|
4
|
+
and in `../../CONSUMING.md`; this file starts where the changelog does.
|
|
5
|
+
|
|
6
|
+
## 1.7.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`AgentChat` is a host-able surface** (PLATFORM-64). cortenaweb adopts the
|
|
11
|
+
shared component instead of running a second chat, and the pieces its chat
|
|
12
|
+
has that an extension's does not — a Shiki/KaTeX bubble with feedback
|
|
13
|
+
controls, a composer with attachments and agent/model selectors, a reading
|
|
14
|
+
column, the MCP App host — go in through props rather than a fork:
|
|
15
|
+
- `chat: UseAgentChatResult` — the host holds the state and the surface
|
|
16
|
+
draws it. A sidebar, a shortcut or a page that routes on the session key
|
|
17
|
+
can then read the same state the surface does. `AgentChatProps` is now
|
|
18
|
+
`AgentChatOwnedProps | AgentChatHostedProps`: with `chat` there is no
|
|
19
|
+
`client` and no `storageScope`, because the host owns both. The result
|
|
20
|
+
may come from `useAgentChat` or from a store of the host's projected onto
|
|
21
|
+
the same shape.
|
|
22
|
+
- `components?: Partial<AgentChatComponents>` — `Message`, `Streaming`,
|
|
23
|
+
`Empty`, `LoadOlder`, `Tools`, `Composer`; each replaces its region whole.
|
|
24
|
+
`Tools` receives the host's `renderToolCall` as passed, so a strip of the
|
|
25
|
+
host's own still mounts an MCP App through the one seam.
|
|
26
|
+
- `classNames?: AgentChatClassNames` for the wrappers (`header`, `scroll`,
|
|
27
|
+
`transcript`, `sentinel`, `tools`, `composer`); `transcript` replaces the
|
|
28
|
+
default spacing rather than merging with it. `hideHeader` for a host with
|
|
29
|
+
its own session list.
|
|
30
|
+
- `useAgentChat` gains `sendMcpAppAction`, `clearSession` (leave without
|
|
31
|
+
minting; the next send mints), `regenerate(messageId)`, `send(text,
|
|
32
|
+
attachments, { model, thinking })`, and options `mintSessionKey`,
|
|
33
|
+
`restoreSession` and `listSessions`. `AgentChatMessage.attachmentItems`
|
|
34
|
+
(`unknown[]`, the host's own shape) is set on the user bubble a send adds;
|
|
35
|
+
`attachments` keeps its `string[]` shape.
|
|
36
|
+
- `autoLoadOlder`: the transcript pages older history from a sentinel at the
|
|
37
|
+
top as the reader reaches it, as well as from the button. Off by default —
|
|
38
|
+
`AgentChatPopup` pages on a click — and armed only once the scroller
|
|
39
|
+
overflows, so a short transcript does not pull every page the server has.
|
|
40
|
+
Whatever asked for the page, the reader's place is held across the prepend
|
|
41
|
+
(`overflow-anchor: none` on the scroller; the surface owns the rule in every
|
|
42
|
+
browser), and a new message is followed smoothly when the reader was at the
|
|
43
|
+
bottom.
|
|
44
|
+
- `useAgentChat().loadOlder` returns `true` when a load was started and
|
|
45
|
+
`false` when it declined (no session, one in flight, no more), so a caller
|
|
46
|
+
saving scroll position knows whether a prepend is coming.
|
|
47
|
+
|
|
48
|
+
### Changed (type-level)
|
|
49
|
+
|
|
50
|
+
- `AgentChatProps` is now the union `AgentChatOwnedProps | AgentChatHostedProps`.
|
|
51
|
+
A wrapper that did `interface MyProps extends AgentChatProps` no longer
|
|
52
|
+
compiles — an interface cannot extend a union. Extend `AgentChatOwnedProps`
|
|
53
|
+
(you pass `client` and `storageScope`) or `AgentChatHostedProps` (you pass
|
|
54
|
+
`chat`) instead, as `AgentChatPopupProps` now does.
|
|
55
|
+
- `UseAgentChatResult.loadOlder` is `() => boolean`, was `() => void`. A store
|
|
56
|
+
projected onto the shape returns whether it started a load.
|
|
57
|
+
- The transcript wrapper (`role="log"`) gains `relative`, and a new first child
|
|
58
|
+
`data-slot="agent-chat-sentinel"` (`absolute inset-x-0 top-0 h-px` by
|
|
59
|
+
default, or `classNames.sentinel`). A selector on the transcript's first
|
|
60
|
+
child, or a test counting its children, sees one more element.
|
|
61
|
+
- `AgentChatEmptySlotProps` is unchanged, but a host's `Empty` no longer holds
|
|
62
|
+
the region while `chat.error` is set: the transcript mounts with the error
|
|
63
|
+
banner in it instead.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- **`regenerate` asks the question once.** The user turn being asked again is
|
|
68
|
+
moved onto the new run rather than drawn a second time, so the transcript
|
|
69
|
+
reads Q then A2, not Q, Q, A2.
|
|
70
|
+
- **A stale older page cannot land under another session.** `history/older`
|
|
71
|
+
is dropped for a key that is no longer the current one, the same rule
|
|
72
|
+
`history/loaded` follows.
|
|
73
|
+
- **A reopened chat brings back the surfaces the agent drew.**
|
|
74
|
+
`parseHistoryMessages` reads `a2ui` content parts, folds them across a turn,
|
|
75
|
+
and strips the injected `[Dow YYYY-MM-DD HH:MM TZ]` stamp from a user turn —
|
|
76
|
+
the same three rules `cortena-shared` applies, ported.
|
|
77
|
+
- **A stale history reply cannot change the session.** `history/loaded` for a
|
|
78
|
+
key that is no longer the current one — including `null`, after
|
|
79
|
+
`clearSession` — is dropped whole. Two quick resumes used to land the first
|
|
80
|
+
transcript under the second key.
|
|
81
|
+
|
|
82
|
+
## 1.6.0
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- **`cortena-ui/agent-chat`: one plain progress line per tool call** (SKILLS-9).
|
|
87
|
+
Each AG-UI tool call becomes a sentence under the assistant turn — a spinner
|
|
88
|
+
while it runs, a tick when it lands, a cross and the error envelope's message
|
|
89
|
+
when it fails, and the elapsed time on anything still running after eight
|
|
90
|
+
seconds. The list is labelled `Progress` and is deliberately NOT a live
|
|
91
|
+
region of its own: it is drawn inside the transcript, which is already
|
|
92
|
+
`role="log" aria-live="polite"`.
|
|
93
|
+
- `agent-chat/step-label.ts`: `stepLabel(toolName, args, phase)`,
|
|
94
|
+
`confirmationRequiredLabel(args)` and `humaniseId(id)` — the wording map for
|
|
95
|
+
`engram_search`, `engram_describe` and `broker_invoke`, pure and with no
|
|
96
|
+
dependency on the result.
|
|
97
|
+
- `useAgentChat` gains `steps: AgentChatStep[]`, paired by AG-UI
|
|
98
|
+
`toolCallId`. A `409 confirmation_required` result relabels the step rather
|
|
99
|
+
than failing it; an aborted or errored run ends every step still running.
|
|
100
|
+
- Steps are kept in `sessionStorage` beside the session key, so a collapsed
|
|
101
|
+
pop-up, a reload or a remount mid-run does not lose them —
|
|
102
|
+
`chat.history` cannot bring them back, because the server never sees them.
|
|
103
|
+
- New exports: `AgentSteps`, `AgentStepsProps`, `AgentChatStep`,
|
|
104
|
+
`AgentStepPhase`, `stepLabel`, `confirmationRequiredLabel`, `humaniseId`,
|
|
105
|
+
`genericLabel`, `isGenericLabel`, `truncate`, `readResultMetadata`,
|
|
106
|
+
`AgentToolResultMetadata`, `AgentToolResultMeta`, `readStoredSteps`,
|
|
107
|
+
`writeStoredSteps`, `serialiseSteps`, `stepsStorageKey`, `STEP_LIMIT`.
|
|
108
|
+
|
|
109
|
+
- **The outcome of a tool call is read from `TOOL_CALL_RESULT.metadata`**
|
|
110
|
+
(SKILLS-9 review). `AgentChatBridge` forwards the metadata into
|
|
111
|
+
`AgentToolEvent.data.metadata`, and the store decides from
|
|
112
|
+
`{ toolName, isError, meta?: { status, code, message } }` — an error with
|
|
113
|
+
`meta.code === "confirmation_required"` is the broker asking a question, any
|
|
114
|
+
other error is worded from `meta.message` or the first 200 characters of the
|
|
115
|
+
result text, and everything else is done. `content` is never parsed for an
|
|
116
|
+
outcome: a Cortena tool result is `{ content: [...], details }`, so the
|
|
117
|
+
`ok: false` the client used to look for was never at the top level and every
|
|
118
|
+
failed call was drawn as a success.
|
|
119
|
+
|
|
120
|
+
- **A step always stops spinning, and never claims more than it knows.** A
|
|
121
|
+
plain `RUN_FINISHED` marks every open step done with its label untouched (the
|
|
122
|
+
run succeeded, so the call did); an abort marks them `stopped` — a new,
|
|
123
|
+
neutral status, because the user asked for it and a red cross reads as a
|
|
124
|
+
fault they caused; and a step restored from storage as `running` or `paused`
|
|
125
|
+
comes back `stopped` with no message, because the page went away, not the
|
|
126
|
+
tool call. A `tool.start` for a step that has already ended is a reconnect
|
|
127
|
+
replay and is ignored.
|
|
128
|
+
|
|
129
|
+
- **An interrupt is not a success** (SKILLS-9 review 2). `RUN_FINISHED` with
|
|
130
|
+
`outcome.type === "interrupt"` is cortenacore pausing the run for an approval
|
|
131
|
+
or a frontend tool with the tool call STILL OPEN, and ticking it told the user
|
|
132
|
+
a write had gone through while the card asking their permission for it was on
|
|
133
|
+
the screen. `AgentChatEvent` carries `interrupted`, and such a step becomes
|
|
134
|
+
`paused` — a second neutral status, "Waiting for your approval", no spinner
|
|
135
|
+
and no elapsed counter. The result on the continuation resolves it; a `Deny`
|
|
136
|
+
marks it `stopped`. A stored `status` is validated against the known set on
|
|
137
|
+
the way back in, and anything else is `stopped`.
|
|
138
|
+
|
|
139
|
+
- **A late result cannot resurrect a dropped step** (SKILLS-9 review 2). A
|
|
140
|
+
runaway turn pushes its first calls past `STEP_LIMIT`, and their results
|
|
141
|
+
arrive afterwards; the store keeps the last 100 dropped ids and ignores them
|
|
142
|
+
rather than appending a `Working: tool` line for the oldest call of the turn
|
|
143
|
+
at the bottom of the strip.
|
|
144
|
+
|
|
145
|
+
- **`isError` is believed from either witness** (SKILLS-9 review 2). A
|
|
146
|
+
`tool.error` whose metadata arrived without the field was read as a success.
|
|
147
|
+
|
|
148
|
+
- **The id and the tool name are bounded** (SKILLS-9 review 2) — 80 characters,
|
|
149
|
+
in the stored projection and on `data-tool`. Neither is written by this
|
|
150
|
+
package, and 9 kB of either filled the 64 kB slot on its own.
|
|
151
|
+
|
|
152
|
+
- **Bounded and sanitised labels.** Every interpolated string — an operation id,
|
|
153
|
+
a tool name, a server message — goes through `truncate`, which strips
|
|
154
|
+
`\p{Cf}` and `\p{Cc}` before the length check, so a bidi override cannot
|
|
155
|
+
reverse a line and 8 kB of catalogue id cannot fill the strip.
|
|
156
|
+
|
|
157
|
+
- **Only a projection is persisted.** `id`, `toolName`, `label`, `status`,
|
|
158
|
+
`startedAt`, `endedAt`, `errorMessage` — never `args`, which are the user's
|
|
159
|
+
data. The write is debounced 250 ms and flushed on unmount, the list is capped
|
|
160
|
+
at `STEP_LIMIT` in the reducer, and the slot is capped at 64 kB, oldest first.
|
|
161
|
+
|
|
162
|
+
- **Accessibility.** The elapsed counter is `aria-hidden` (it changed once a
|
|
163
|
+
second inside a live region), a running step is held out of the announcement
|
|
164
|
+
until its arguments have produced a real sentence rather than reading out
|
|
165
|
+
"Working: broker_invoke" and correcting itself, and the label span is keyed on
|
|
166
|
+
the words, so a re-word is a node replacement rather than a mutated text node
|
|
167
|
+
some screen readers never announce.
|
|
168
|
+
|
|
169
|
+
### Unchanged
|
|
170
|
+
|
|
171
|
+
- No change to AG-UI protocol handling beyond forwarding
|
|
172
|
+
`TOOL_CALL_RESULT.metadata`, which the bridge already parsed and dropped.
|
|
173
|
+
Streamed reasoning keeps the fix that stops a later event wiping it, now with
|
|
174
|
+
the re-attach rewind covered by a test.
|
|
175
|
+
- `agent-chat` remains the only entry that can reach `@ag-ui/client`; every
|
|
176
|
+
other bundle probe is unchanged.
|
|
177
|
+
|
|
178
|
+
### Bundle
|
|
179
|
+
|
|
180
|
+
- `agent-chat` grows **+6.8 kB** total (2,738.6 → 2,745.4 kB) and **+5.9 kB**
|
|
181
|
+
eager (843.4 → 849.3 kB): the wording map, the step reducer and the strip.
|
|
182
|
+
Not "unmoved" — it is a new surface, and it costs what it costs. Every other
|
|
183
|
+
entry is byte-identical, and `CONSUMING.md`'s table carries the numbers.
|
package/README.md
CHANGED
|
@@ -186,6 +186,90 @@ two windows on one session may both send — cortenacore serialises the runs. A
|
|
|
186
186
|
new key is minted client-side as `agent:<id>:new-<Date.now()>-<random36>` and
|
|
187
187
|
used immediately, so an in-flight first message is never lost.
|
|
188
188
|
|
|
189
|
+
### Progress steps
|
|
190
|
+
|
|
191
|
+
Every AG-UI tool call becomes **one plain sentence** under the assistant turn,
|
|
192
|
+
streamed as it happens: a spinner while it runs, a tick and muted text when it
|
|
193
|
+
lands, a cross and the failure's `message` when it fails, and a neutral
|
|
194
|
+
"Stopped" when the user pressed Stop. A step still running after eight seconds
|
|
195
|
+
appends the time it has taken. The list is an `aria-live="polite"` region, so a
|
|
196
|
+
screen reader hears the run rather than silence — the seconds are `aria-hidden`
|
|
197
|
+
(they change every second) and a step is held out of the announcement until its
|
|
198
|
+
arguments have produced a real sentence.
|
|
199
|
+
|
|
200
|
+
This is not the tool strip at the foot of the panel. The strip is the record —
|
|
201
|
+
tool name, arguments, output, the `ui://` resource `renderToolCall` mounts an
|
|
202
|
+
MCP App from — and reading it means knowing what `broker_invoke` is. The steps
|
|
203
|
+
are for the person waiting.
|
|
204
|
+
|
|
205
|
+
The wording is `stepLabel(toolName, args, phase)` in `agent-chat/step-label.ts`,
|
|
206
|
+
a pure function with no network and no result:
|
|
207
|
+
|
|
208
|
+
| the call | the line |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| `engram_search {intent}` | Looking for a way to \<intent\>, truncated at 80 characters |
|
|
211
|
+
| `engram_describe {id: "skill.…"}` | Reading skill: \<the call's `title`, or the id's last segment\> |
|
|
212
|
+
| `engram_describe {id}` | Checking how to \<id as words: `tasks.task.list` → "tasks task list"\> |
|
|
213
|
+
| `broker_invoke {operation, confirmed: true}` | Doing: \<operation\>, then Done: \<operation\> |
|
|
214
|
+
| `broker_invoke {operation}` | Fetching \<operation\> |
|
|
215
|
+
| anything else, or arguments not yet arrived | Working: \<tool name\> |
|
|
216
|
+
|
|
217
|
+
A step that finished on that last line is re-worded if its arguments arrive
|
|
218
|
+
late; a step that already has a real sentence keeps it.
|
|
219
|
+
|
|
220
|
+
Two of those need saying out loud. **An unconfirmed invoke is worded as a
|
|
221
|
+
read** — "Fetching …" — because the client cannot know read from write before
|
|
222
|
+
the reply comes back; only the catalogue knows, and the model only sets
|
|
223
|
+
`confirmed` once the user has said yes. And a result whose code is
|
|
224
|
+
`confirmation_required` is **not** drawn as a failure: the step keeps its tick
|
|
225
|
+
and is relabelled "Needs your confirmation: \<operation\>", which is what the
|
|
226
|
+
broker is actually saying.
|
|
227
|
+
|
|
228
|
+
Every string interpolated into a label — an intent, a title, an operation id, a
|
|
229
|
+
tool name, a server's message — is bounded at 80 characters (200 for a failure
|
|
230
|
+
message) and stripped of control and format characters first, so a catalogue
|
|
231
|
+
entry cannot fill the strip and a U+202E cannot reverse it.
|
|
232
|
+
|
|
233
|
+
#### How a step knows it failed
|
|
234
|
+
|
|
235
|
+
**`TOOL_CALL_RESULT.metadata`, and nothing else.** The contract cortenacore
|
|
236
|
+
emits, and the only thing this client reads:
|
|
237
|
+
|
|
238
|
+
```jsonc
|
|
239
|
+
metadata: {
|
|
240
|
+
toolName: "broker_invoke",
|
|
241
|
+
isError: true,
|
|
242
|
+
meta: { status: 409, code: "confirmation_required", message: "Set confirmed=true." }
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`meta` is optional; where it is absent, only `isError` can be trusted. An error
|
|
247
|
+
with `meta.code === "confirmation_required"` is the question above; any other
|
|
248
|
+
error is worded from `meta.message`, or from the first 200 characters of the
|
|
249
|
+
result text when there is none.
|
|
250
|
+
|
|
251
|
+
`content` is **never** parsed for an outcome. It is the result as the model sees
|
|
252
|
+
it — for a Cortena tool, `{ content: [{ type: "text", text }], details }` — and
|
|
253
|
+
looking for an `ok: false` in it is guessing at a shape nobody promised.
|
|
254
|
+
|
|
255
|
+
Steps live in `useAgentChat`'s `steps`, are cleared when the next run starts,
|
|
256
|
+
and are kept in `sessionStorage` beside the session key. That last part is not
|
|
257
|
+
decoration: `chat.history` has no steps in it — the server never sees them — so
|
|
258
|
+
a collapsed pop-up, a reload or a host remount mid-run would otherwise lose the
|
|
259
|
+
whole strip. `AgentSteps` is exported for a host that wants to draw them
|
|
260
|
+
somewhere else.
|
|
261
|
+
|
|
262
|
+
#### What is kept, and where
|
|
263
|
+
|
|
264
|
+
Steps are held in memory with their `args`, and **only a projection is
|
|
265
|
+
persisted**: `id`, `toolName`, `label`, `status`, `startedAt`, `endedAt`,
|
|
266
|
+
`errorMessage`. A call's arguments are the user's data and do not belong in a
|
|
267
|
+
store every script on the origin can read; the label is already derived from
|
|
268
|
+
them. The write is debounced by 250 ms and flushed on unmount, the list is
|
|
269
|
+
capped at 50 steps, and the slot is capped at 64 kB with the oldest dropped
|
|
270
|
+
first. A step restored with `status: "running"` comes back as "Interrupted." —
|
|
271
|
+
the run that owned it is gone, and no result will ever arrive.
|
|
272
|
+
|
|
189
273
|
### What the extension must proxy
|
|
190
274
|
|
|
191
275
|
The pop-up talks to one origin, `baseUrl`, and every call carries the user's
|
|
@@ -234,6 +318,42 @@ returns replaces the default card.
|
|
|
234
318
|
/>
|
|
235
319
|
```
|
|
236
320
|
|
|
321
|
+
### Hosting the surface
|
|
322
|
+
|
|
323
|
+
`AgentChatPopup` owns its state. A host that already has state of its own —
|
|
324
|
+
cortenaweb's shell reads the current session from a store the sidebar, the
|
|
325
|
+
keyboard shortcuts and the Canvas panel all share — mounts `AgentChat` with
|
|
326
|
+
`chat` instead of `client`, and no `storageScope`:
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
<AgentChat
|
|
330
|
+
chat={chat} // a UseAgentChatResult: from useAgentChat, or a store projected onto it
|
|
331
|
+
agentName="Cortena"
|
|
332
|
+
hideHeader // the host has its own session list
|
|
333
|
+
components={{ Message, Streaming, Empty, LoadOlder, Tools, Composer }}
|
|
334
|
+
classNames={{ scroll: "chat-scroll", transcript: "chat-column gap-6 py-6" }}
|
|
335
|
+
renderToolCall={renderMcpApp} // the host's MCP App frame, through the one seam
|
|
336
|
+
/>
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
What stays shared is the surface: the scroller that keeps to the bottom, the
|
|
340
|
+
pager that holds the reader's place across a prepend (and, with
|
|
341
|
+
`autoLoadOlder`, asks for the next page from a sentinel at the top once the
|
|
342
|
+
transcript overflows), the order of the regions, the progress steps and the
|
|
343
|
+
tool-card seam. What each
|
|
344
|
+
region looks like is the host's to replace, one component per region; a
|
|
345
|
+
component given in `components` replaces its region whole, wrapper included.
|
|
346
|
+
`Tools` is handed `renderToolCall` as it was passed, so a strip of the host's
|
|
347
|
+
own still mounts an MCP App through the seam. `Streaming` is handed `steps`
|
|
348
|
+
because the default draws them inside the live bubble; a host's bubble draws
|
|
349
|
+
them itself. `classNames.transcript` replaces the default gutter rather than
|
|
350
|
+
merging with it, so a reading column owns its own spacing.
|
|
351
|
+
|
|
352
|
+
The hook has the matching options: `restoreSession: false` for a host whose URL
|
|
353
|
+
names the session (the key is still written, so one window stays on one
|
|
354
|
+
session), `listSessions: false` for a host with its own session list, and
|
|
355
|
+
`mintSessionKey` for a host that binds sessions some other way.
|
|
356
|
+
|
|
237
357
|
## Charts in tests
|
|
238
358
|
|
|
239
359
|
Both chart engines size themselves from the parent box, and under jsdom every
|
|
@@ -323,7 +443,7 @@ from `@a2ui/*` is installed, so `vitest.config.ts` has no entry for it.
|
|
|
323
443
|
pnpm check # types
|
|
324
444
|
pnpm lint:design # no literals where a token exists; no dangling var()
|
|
325
445
|
pnpm build # dist/ via tsdown (ESM + d.ts); also runs on prepack
|
|
326
|
-
pnpm test # the
|
|
446
|
+
pnpm test # changed-only: the suites of this package your diff affects
|
|
327
447
|
pnpm test:bundle # bundle budgets alone (builds first, then real Vite apps)
|
|
328
448
|
pnpm bundle:probe # the size table, printed, without asserting anything
|
|
329
449
|
pnpm guide # three-theme guide on a local Vite server
|
|
@@ -353,7 +473,7 @@ guide/ Vite app: ?theme=light|dark|system frames, or all three
|
|
|
353
473
|
## Releasing
|
|
354
474
|
|
|
355
475
|
```bash
|
|
356
|
-
pnpm ui:check && pnpm
|
|
476
|
+
pnpm ui:check && FORCE_FULL=1 pnpm test:all # types, token lint, every suite
|
|
357
477
|
cd packages/ui && npm version minor && npm publish # prepack builds dist/
|
|
358
478
|
git push --follow-tags
|
|
359
479
|
```
|
|
@@ -120,13 +120,14 @@ var AgentChatBridge = class {
|
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
emitFinal() {
|
|
123
|
+
emitFinal(interrupted = false) {
|
|
124
124
|
const content = this.blocks();
|
|
125
125
|
this.chat({
|
|
126
126
|
runId: this.runId,
|
|
127
127
|
sessionKey: this.sessionKey,
|
|
128
128
|
seq: ++this.seq,
|
|
129
129
|
state: "final",
|
|
130
|
+
...interrupted ? { interrupted: true } : {},
|
|
130
131
|
...content.length > 0 ? { message: {
|
|
131
132
|
role: "assistant",
|
|
132
133
|
content,
|
|
@@ -198,6 +199,7 @@ var AgentChatBridge = class {
|
|
|
198
199
|
data: {
|
|
199
200
|
...event.toolCallId === void 0 ? {} : { id: event.toolCallId },
|
|
200
201
|
output: event.content,
|
|
202
|
+
...event.metadata ? { metadata: event.metadata } : {},
|
|
201
203
|
completedAt: this.now()
|
|
202
204
|
}
|
|
203
205
|
});
|
|
@@ -205,11 +207,13 @@ var AgentChatBridge = class {
|
|
|
205
207
|
case "CUSTOM":
|
|
206
208
|
this.handleCustom(event);
|
|
207
209
|
break;
|
|
208
|
-
case "RUN_FINISHED":
|
|
210
|
+
case "RUN_FINISHED": {
|
|
209
211
|
this.terminal = true;
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
+
const interrupted = event.outcome?.type === "interrupt";
|
|
213
|
+
if (interrupted) this.pendingInterrupts.push(...event.outcome?.interrupts ?? []);
|
|
214
|
+
this.emitFinal(interrupted);
|
|
212
215
|
break;
|
|
216
|
+
}
|
|
213
217
|
case "RUN_ERROR":
|
|
214
218
|
this.terminal = true;
|
|
215
219
|
this.chat({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","names":[],"sources":["../../src/agent-chat/bridge.ts"],"sourcesContent":["/**\n * AG-UI events -> the three `AgentChatClient` sinks.\n *\n * Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37\n * step 1). `cortena-shared` is not published, so the translation lives here;\n * keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which\n * is why the tests can drive it with a literal array of events.\n *\n * One change against the original, and it is the reason this file is not a\n * copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits\n * `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`\n * at tool-start time, with the whole argument object stringified into one\n * `delta`. The original bridge had no case for it, so every tool card in the\n * AG-UI path showed a name and a spinner and never showed what the tool was\n * called with. The deltas are accumulated per call id and parsed when they\n * form valid JSON, which is also correct for a transport that streams the\n * arguments a fragment at a time.\n */\n\nimport { type A2UIChatBlock, foldA2UIBlocks, readA2UIBlock } from \"./a2ui-block\";\nimport type {\n AgentApprovalEvent,\n AgentChatEvent,\n AgentToolEvent,\n AgentApprovalDecision,\n} from \"./types\";\n\n/** The subset of an AG-UI event this bridge reads. */\nexport interface AguiIncomingEvent {\n type: string;\n messageId?: string;\n delta?: string;\n toolCallId?: string;\n toolCallName?: string;\n content?: string;\n message?: string;\n name?: string;\n value?: unknown;\n metadata?: Record<string, unknown>;\n snapshot?: unknown;\n outcome?: { type?: string; interrupts?: Array<Record<string, unknown>> };\n}\n\n/** `CUSTOM` event names cortenacore emits that this bridge acts on. */\nexport const AGUI_CUSTOM_NAMES = {\n CHAT_ABORTED: \"cortena.chat.aborted\",\n TOOL_PROGRESS: \"cortena.tool.progress\",\n APPROVAL_REQUEST: \"cortena.approval.request\",\n APPROVAL_RESOLVED: \"cortena.approval.resolved\",\n A2UI: \"cortena.a2ui\",\n} as const;\n\nexport interface AgentChatBridgeOptions {\n runId: string;\n sessionKey: string;\n chat: (event: AgentChatEvent) => void;\n tool?: (event: AgentToolEvent) => void;\n approval?: (event: AgentApprovalEvent) => void;\n now?: () => number;\n}\n\ntype ContentBlock = {\n type: \"thinking\" | \"text\" | \"a2ui\";\n thinking?: string;\n text?: string;\n a2ui?: A2UIChatBlock;\n};\n\nconst DECISIONS: readonly string[] = [\"allow-once\", \"allow-always\", \"deny\"];\n\nexport class AgentChatBridge {\n readonly runId: string;\n readonly sessionKey: string;\n\n private readonly chat: (event: AgentChatEvent) => void;\n private readonly tool: ((event: AgentToolEvent) => void) | undefined;\n private readonly approval: ((event: AgentApprovalEvent) => void) | undefined;\n private readonly now: () => number;\n\n private seq = 0;\n /** Text of the assistant message currently streaming. */\n private text = \"\";\n private textMessageId: string | null = null;\n private reasoning = \"\";\n /**\n * The reasoning accumulated before each assistant message began, by message\n * id, and the whole of the re-attach defence for the chain of thought.\n *\n * A re-attach re-streams the run from the top, so every REASONING delta\n * arrives a second time. Restoring this snapshot when a message id starts\n * again rewinds the buffer to exactly the point the first pass reached,\n * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.\n */\n private readonly reasoningAtStart = new Map<string, string>();\n /**\n * A2UI blocks seen this run. The chat treats the content array as a\n * snapshot, so every emission carries all of them — the same rule the text\n * follows, and what makes a re-attach land in the same place.\n */\n private a2ui: A2UIChatBlock[] = [];\n /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */\n private readonly args = new Map<string, string>();\n /**\n * A2UI messages already folded onto each surface, by their serialised form.\n *\n * A re-attach re-streams the run from the beginning, so every\n * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this\n * the surface's message list doubled on the first reconnection and trebled\n * on the second — a table drawn once, then drawn again underneath itself.\n */\n private readonly seenA2ui = new Map<string, Set<string>>();\n private terminal = false;\n private readonly pendingInterrupts: Array<Record<string, unknown>> = [];\n\n constructor(opts: AgentChatBridgeOptions) {\n this.runId = opts.runId;\n this.sessionKey = opts.sessionKey;\n this.chat = opts.chat;\n this.tool = opts.tool;\n this.approval = opts.approval;\n this.now = opts.now ?? (() => Date.now());\n }\n\n /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */\n get finished(): boolean {\n return this.terminal;\n }\n\n /** Interrupts carried by the terminal RUN_FINISHED, if any. */\n get interrupts(): ReadonlyArray<Record<string, unknown>> {\n return this.pendingInterrupts;\n }\n\n private blocks(): ContentBlock[] {\n const content: ContentBlock[] = [];\n if (this.reasoning) {\n content.push({ type: \"thinking\", thinking: this.reasoning });\n }\n if (this.text) {\n content.push({ type: \"text\", text: this.text });\n }\n for (const block of this.a2ui) {\n content.push({ type: \"a2ui\", a2ui: block });\n }\n return content;\n }\n\n private emitDelta(): void {\n const content = this.blocks();\n if (content.length === 0) {\n return;\n }\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"delta\",\n message: { role: \"assistant\", content, timestamp: this.now() },\n });\n }\n\n private emitFinal(): void {\n const content = this.blocks();\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"final\",\n ...(content.length > 0\n ? { message: { role: \"assistant\", content, timestamp: this.now() } }\n : {}),\n });\n }\n\n /** Surface a transport failure the way a run error is surfaced. */\n fail(message: string): void {\n if (this.terminal) {\n return;\n }\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: message,\n });\n }\n\n handleEvent(event: AguiIncomingEvent): void {\n switch (event.type) {\n case \"RUN_STARTED\":\n // The run begins here, so the streaming buffers do too. This is the\n // only place the chain of thought is cleared outright.\n this.textMessageId = null;\n this.text = \"\";\n this.reasoning = \"\";\n break;\n case \"TEXT_MESSAGE_START\": {\n // A new assistant message replaces the text buffer, and *only* the\n // text buffer. cortenacore emits the REASONING deltas before the\n // message they explain, so clearing the reasoning here threw away the\n // chain of thought a moment after it arrived — the thinking block\n // never rendered at all. Re-attach doubling is handled instead by\n // rewinding to the snapshot taken when this message id first started.\n const id = event.messageId ?? null;\n this.textMessageId = id;\n this.text = \"\";\n if (id !== null) {\n const before = this.reasoningAtStart.get(id);\n if (before === undefined) {\n this.reasoningAtStart.set(id, this.reasoning);\n } else {\n this.reasoning = before;\n }\n }\n break;\n }\n case \"TEXT_MESSAGE_CONTENT\":\n if (event.messageId && event.messageId !== this.textMessageId) {\n this.textMessageId = event.messageId;\n this.text = \"\";\n }\n this.text += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"REASONING_MESSAGE_CONTENT\":\n this.reasoning += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"TOOL_CALL_START\":\n this.tool?.({\n event: \"tool.start\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n ...(event.toolCallName === undefined ? {} : { name: event.toolCallName }),\n startedAt: this.now(),\n },\n });\n break;\n case \"TOOL_CALL_ARGS\":\n this.accumulateArgs(event);\n break;\n case \"TOOL_CALL_END\":\n // Nothing more will arrive for this call's arguments; emit whatever\n // accumulated, even when it never parsed, so the card shows something.\n this.flushArgs(event.toolCallId);\n break;\n case \"TOOL_CALL_RESULT\":\n this.tool?.({\n event: event.metadata?.isError === true ? \"tool.error\" : \"tool.complete\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n output: event.content,\n completedAt: this.now(),\n },\n });\n break;\n case \"CUSTOM\":\n this.handleCustom(event);\n break;\n case \"RUN_FINISHED\":\n this.terminal = true;\n if (event.outcome?.type === \"interrupt\") {\n this.pendingInterrupts.push(...(event.outcome.interrupts ?? []));\n }\n this.emitFinal();\n break;\n case \"RUN_ERROR\":\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: event.message ?? \"An error occurred\",\n });\n break;\n default:\n break;\n }\n }\n\n /**\n * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.\n *\n * Published on every delta rather than only at `TOOL_CALL_END`, because a\n * run that is interrupted between the two — an approval, an abort, a dropped\n * stream — would otherwise show a tool card with no input at all, which is\n * the state the card exists to avoid.\n */\n private accumulateArgs(event: AguiIncomingEvent): void {\n const id = event.toolCallId;\n if (!id) {\n return;\n }\n const next = (this.args.get(id) ?? \"\") + (event.delta ?? \"\");\n this.args.set(id, next);\n this.publishArgs(id, next);\n }\n\n private flushArgs(id?: string): void {\n if (!id) {\n return;\n }\n const raw = this.args.get(id);\n if (raw === undefined) {\n return;\n }\n this.args.delete(id);\n this.publishArgs(id, raw);\n }\n\n private publishArgs(id: string, raw: string): void {\n if (!raw) {\n return;\n }\n this.tool?.({ event: \"tool.args\", data: { id, input: parseArgs(raw) } });\n }\n\n /**\n * The block with any message this surface has already been sent removed;\n * `null` when nothing in it is new.\n */\n private freshA2UI(block: A2UIChatBlock | null): A2UIChatBlock | null {\n if (!block) {\n return null;\n }\n let seen = this.seenA2ui.get(block.surfaceId);\n if (!seen) {\n seen = new Set();\n this.seenA2ui.set(block.surfaceId, seen);\n }\n const messages: unknown[] = [];\n for (const message of block.messages) {\n const key = stableKey(message);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n messages.push(message);\n }\n // An error still deserves a card even when its messages are all repeats.\n if (messages.length === 0 && !block.error) {\n return null;\n }\n return { ...block, messages };\n }\n\n private handleCustom(event: AguiIncomingEvent): void {\n const value = (event.value ?? {}) as Record<string, unknown>;\n const id = typeof value.id === \"string\" ? value.id : \"\";\n switch (event.name) {\n case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:\n this.tool?.({\n event: \"tool.progress\",\n data: {\n ...(typeof value.toolCallId === \"string\" ? { id: value.toolCallId } : {}),\n output: value.partialResult,\n },\n });\n break;\n case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {\n const request = (value.request ?? {}) as Record<string, unknown>;\n this.approval?.({\n type: \"request\",\n request: {\n id,\n toolName: typeof request.command === \"string\" ? request.command : \"exec\",\n args: request,\n timestamp: typeof value.createdAtMs === \"number\" ? value.createdAtMs : this.now(),\n // Captured here, where the run and the session are facts about the\n // event rather than about whatever is in flight when the user\n // eventually presses a button.\n runId: this.runId,\n sessionKey: this.sessionKey,\n },\n });\n break;\n }\n case AGUI_CUSTOM_NAMES.A2UI: {\n // One event per block; folding here means two pushes to one surface\n // reach the renderer as a single message list, which is how a surface\n // the agent streams in pieces ends up drawn once.\n const block = this.freshA2UI(readA2UIBlock(value));\n if (block) {\n this.a2ui = foldA2UIBlocks([...this.a2ui, block]);\n this.emitDelta();\n }\n break;\n }\n case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED:\n this.approval?.({\n type: \"resolved\",\n id,\n decision:\n typeof value.decision === \"string\" && DECISIONS.includes(value.decision)\n ? (value.decision as AgentApprovalDecision)\n : null,\n });\n break;\n default:\n // cortena.chat.aborted needs no change here: abort() has already reset\n // the local streaming state before the request went out.\n break;\n }\n }\n}\n\n/**\n * A message's identity for de-duplication.\n *\n * `JSON.stringify` is enough: these are payloads the transport just parsed out\n * of JSON, so key order is the order the server wrote, and the same message\n * arriving twice on a re-attach serialises identically both times.\n */\nfunction stableKey(message: unknown): string {\n try {\n return JSON.stringify(message) ?? \"undefined\";\n } catch {\n // Circular, which a JSON transport cannot produce. Never equal to anything.\n return `${Math.random()}`;\n }\n}\n\n/** Arguments as an object when the accumulation parses, as the raw text otherwise. */\nfunction parseArgs(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bridge.js","names":[],"sources":["../../src/agent-chat/bridge.ts"],"sourcesContent":["/**\n * AG-UI events -> the three `AgentChatClient` sinks.\n *\n * Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37\n * step 1). `cortena-shared` is not published, so the translation lives here;\n * keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which\n * is why the tests can drive it with a literal array of events.\n *\n * One change against the original, and it is the reason this file is not a\n * copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits\n * `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`\n * at tool-start time, with the whole argument object stringified into one\n * `delta`. The original bridge had no case for it, so every tool card in the\n * AG-UI path showed a name and a spinner and never showed what the tool was\n * called with. The deltas are accumulated per call id and parsed when they\n * form valid JSON, which is also correct for a transport that streams the\n * arguments a fragment at a time.\n */\n\nimport { type A2UIChatBlock, foldA2UIBlocks, readA2UIBlock } from \"./a2ui-block\";\nimport type {\n AgentApprovalEvent,\n AgentChatEvent,\n AgentToolEvent,\n AgentApprovalDecision,\n} from \"./types\";\n\n/** The subset of an AG-UI event this bridge reads. */\nexport interface AguiIncomingEvent {\n type: string;\n messageId?: string;\n delta?: string;\n toolCallId?: string;\n toolCallName?: string;\n content?: string;\n message?: string;\n name?: string;\n value?: unknown;\n metadata?: Record<string, unknown>;\n snapshot?: unknown;\n outcome?: { type?: string; interrupts?: Array<Record<string, unknown>> };\n}\n\n/** `CUSTOM` event names cortenacore emits that this bridge acts on. */\nexport const AGUI_CUSTOM_NAMES = {\n CHAT_ABORTED: \"cortena.chat.aborted\",\n TOOL_PROGRESS: \"cortena.tool.progress\",\n APPROVAL_REQUEST: \"cortena.approval.request\",\n APPROVAL_RESOLVED: \"cortena.approval.resolved\",\n A2UI: \"cortena.a2ui\",\n} as const;\n\nexport interface AgentChatBridgeOptions {\n runId: string;\n sessionKey: string;\n chat: (event: AgentChatEvent) => void;\n tool?: (event: AgentToolEvent) => void;\n approval?: (event: AgentApprovalEvent) => void;\n now?: () => number;\n}\n\ntype ContentBlock = {\n type: \"thinking\" | \"text\" | \"a2ui\";\n thinking?: string;\n text?: string;\n a2ui?: A2UIChatBlock;\n};\n\nconst DECISIONS: readonly string[] = [\"allow-once\", \"allow-always\", \"deny\"];\n\nexport class AgentChatBridge {\n readonly runId: string;\n readonly sessionKey: string;\n\n private readonly chat: (event: AgentChatEvent) => void;\n private readonly tool: ((event: AgentToolEvent) => void) | undefined;\n private readonly approval: ((event: AgentApprovalEvent) => void) | undefined;\n private readonly now: () => number;\n\n private seq = 0;\n /** Text of the assistant message currently streaming. */\n private text = \"\";\n private textMessageId: string | null = null;\n private reasoning = \"\";\n /**\n * The reasoning accumulated before each assistant message began, by message\n * id, and the whole of the re-attach defence for the chain of thought.\n *\n * A re-attach re-streams the run from the top, so every REASONING delta\n * arrives a second time. Restoring this snapshot when a message id starts\n * again rewinds the buffer to exactly the point the first pass reached,\n * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.\n */\n private readonly reasoningAtStart = new Map<string, string>();\n /**\n * A2UI blocks seen this run. The chat treats the content array as a\n * snapshot, so every emission carries all of them — the same rule the text\n * follows, and what makes a re-attach land in the same place.\n */\n private a2ui: A2UIChatBlock[] = [];\n /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */\n private readonly args = new Map<string, string>();\n /**\n * A2UI messages already folded onto each surface, by their serialised form.\n *\n * A re-attach re-streams the run from the beginning, so every\n * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this\n * the surface's message list doubled on the first reconnection and trebled\n * on the second — a table drawn once, then drawn again underneath itself.\n */\n private readonly seenA2ui = new Map<string, Set<string>>();\n private terminal = false;\n private readonly pendingInterrupts: Array<Record<string, unknown>> = [];\n\n constructor(opts: AgentChatBridgeOptions) {\n this.runId = opts.runId;\n this.sessionKey = opts.sessionKey;\n this.chat = opts.chat;\n this.tool = opts.tool;\n this.approval = opts.approval;\n this.now = opts.now ?? (() => Date.now());\n }\n\n /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */\n get finished(): boolean {\n return this.terminal;\n }\n\n /** Interrupts carried by the terminal RUN_FINISHED, if any. */\n get interrupts(): ReadonlyArray<Record<string, unknown>> {\n return this.pendingInterrupts;\n }\n\n private blocks(): ContentBlock[] {\n const content: ContentBlock[] = [];\n if (this.reasoning) {\n content.push({ type: \"thinking\", thinking: this.reasoning });\n }\n if (this.text) {\n content.push({ type: \"text\", text: this.text });\n }\n for (const block of this.a2ui) {\n content.push({ type: \"a2ui\", a2ui: block });\n }\n return content;\n }\n\n private emitDelta(): void {\n const content = this.blocks();\n if (content.length === 0) {\n return;\n }\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"delta\",\n message: { role: \"assistant\", content, timestamp: this.now() },\n });\n }\n\n private emitFinal(interrupted = false): void {\n const content = this.blocks();\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"final\",\n // Carried through so the store can tell \"the run succeeded\" from \"the\n // run is waiting for you\". Both arrive as RUN_FINISHED.\n ...(interrupted ? { interrupted: true } : {}),\n ...(content.length > 0\n ? { message: { role: \"assistant\", content, timestamp: this.now() } }\n : {}),\n });\n }\n\n /** Surface a transport failure the way a run error is surfaced. */\n fail(message: string): void {\n if (this.terminal) {\n return;\n }\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: message,\n });\n }\n\n handleEvent(event: AguiIncomingEvent): void {\n switch (event.type) {\n case \"RUN_STARTED\":\n // The run begins here, so the streaming buffers do too. This is the\n // only place the chain of thought is cleared outright.\n this.textMessageId = null;\n this.text = \"\";\n this.reasoning = \"\";\n break;\n case \"TEXT_MESSAGE_START\": {\n // A new assistant message replaces the text buffer, and *only* the\n // text buffer. cortenacore emits the REASONING deltas before the\n // message they explain, so clearing the reasoning here threw away the\n // chain of thought a moment after it arrived — the thinking block\n // never rendered at all. Re-attach doubling is handled instead by\n // rewinding to the snapshot taken when this message id first started.\n const id = event.messageId ?? null;\n this.textMessageId = id;\n this.text = \"\";\n if (id !== null) {\n const before = this.reasoningAtStart.get(id);\n if (before === undefined) {\n this.reasoningAtStart.set(id, this.reasoning);\n } else {\n this.reasoning = before;\n }\n }\n break;\n }\n case \"TEXT_MESSAGE_CONTENT\":\n if (event.messageId && event.messageId !== this.textMessageId) {\n this.textMessageId = event.messageId;\n this.text = \"\";\n }\n this.text += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"REASONING_MESSAGE_CONTENT\":\n this.reasoning += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"TOOL_CALL_START\":\n this.tool?.({\n event: \"tool.start\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n ...(event.toolCallName === undefined ? {} : { name: event.toolCallName }),\n startedAt: this.now(),\n },\n });\n break;\n case \"TOOL_CALL_ARGS\":\n this.accumulateArgs(event);\n break;\n case \"TOOL_CALL_END\":\n // Nothing more will arrive for this call's arguments; emit whatever\n // accumulated, even when it never parsed, so the card shows something.\n this.flushArgs(event.toolCallId);\n break;\n case \"TOOL_CALL_RESULT\":\n // The metadata travels WITH the event. It carries `isError`, and the\n // `meta` a Cortena tool attaches to a failure — the status, the code\n // and the message — and it is the only trustworthy account of how the\n // call went. Dropping it here is what forced the store to guess at an\n // envelope inside `content`, and the guess was wrong for every real\n // result cortenacore emits.\n this.tool?.({\n event: event.metadata?.isError === true ? \"tool.error\" : \"tool.complete\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n output: event.content,\n ...(event.metadata ? { metadata: event.metadata } : {}),\n completedAt: this.now(),\n },\n });\n break;\n case \"CUSTOM\":\n this.handleCustom(event);\n break;\n case \"RUN_FINISHED\": {\n this.terminal = true;\n const interrupted = event.outcome?.type === \"interrupt\";\n if (interrupted) {\n this.pendingInterrupts.push(...(event.outcome?.interrupts ?? []));\n }\n this.emitFinal(interrupted);\n break;\n }\n case \"RUN_ERROR\":\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: event.message ?? \"An error occurred\",\n });\n break;\n default:\n break;\n }\n }\n\n /**\n * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.\n *\n * Published on every delta rather than only at `TOOL_CALL_END`, because a\n * run that is interrupted between the two — an approval, an abort, a dropped\n * stream — would otherwise show a tool card with no input at all, which is\n * the state the card exists to avoid.\n */\n private accumulateArgs(event: AguiIncomingEvent): void {\n const id = event.toolCallId;\n if (!id) {\n return;\n }\n const next = (this.args.get(id) ?? \"\") + (event.delta ?? \"\");\n this.args.set(id, next);\n this.publishArgs(id, next);\n }\n\n private flushArgs(id?: string): void {\n if (!id) {\n return;\n }\n const raw = this.args.get(id);\n if (raw === undefined) {\n return;\n }\n this.args.delete(id);\n this.publishArgs(id, raw);\n }\n\n private publishArgs(id: string, raw: string): void {\n if (!raw) {\n return;\n }\n this.tool?.({ event: \"tool.args\", data: { id, input: parseArgs(raw) } });\n }\n\n /**\n * The block with any message this surface has already been sent removed;\n * `null` when nothing in it is new.\n */\n private freshA2UI(block: A2UIChatBlock | null): A2UIChatBlock | null {\n if (!block) {\n return null;\n }\n let seen = this.seenA2ui.get(block.surfaceId);\n if (!seen) {\n seen = new Set();\n this.seenA2ui.set(block.surfaceId, seen);\n }\n const messages: unknown[] = [];\n for (const message of block.messages) {\n const key = stableKey(message);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n messages.push(message);\n }\n // An error still deserves a card even when its messages are all repeats.\n if (messages.length === 0 && !block.error) {\n return null;\n }\n return { ...block, messages };\n }\n\n private handleCustom(event: AguiIncomingEvent): void {\n const value = (event.value ?? {}) as Record<string, unknown>;\n const id = typeof value.id === \"string\" ? value.id : \"\";\n switch (event.name) {\n case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:\n this.tool?.({\n event: \"tool.progress\",\n data: {\n ...(typeof value.toolCallId === \"string\" ? { id: value.toolCallId } : {}),\n output: value.partialResult,\n },\n });\n break;\n case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {\n const request = (value.request ?? {}) as Record<string, unknown>;\n this.approval?.({\n type: \"request\",\n request: {\n id,\n toolName: typeof request.command === \"string\" ? request.command : \"exec\",\n args: request,\n timestamp: typeof value.createdAtMs === \"number\" ? value.createdAtMs : this.now(),\n // Captured here, where the run and the session are facts about the\n // event rather than about whatever is in flight when the user\n // eventually presses a button.\n runId: this.runId,\n sessionKey: this.sessionKey,\n },\n });\n break;\n }\n case AGUI_CUSTOM_NAMES.A2UI: {\n // One event per block; folding here means two pushes to one surface\n // reach the renderer as a single message list, which is how a surface\n // the agent streams in pieces ends up drawn once.\n const block = this.freshA2UI(readA2UIBlock(value));\n if (block) {\n this.a2ui = foldA2UIBlocks([...this.a2ui, block]);\n this.emitDelta();\n }\n break;\n }\n case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED:\n this.approval?.({\n type: \"resolved\",\n id,\n decision:\n typeof value.decision === \"string\" && DECISIONS.includes(value.decision)\n ? (value.decision as AgentApprovalDecision)\n : null,\n });\n break;\n default:\n // cortena.chat.aborted needs no change here: abort() has already reset\n // the local streaming state before the request went out.\n break;\n }\n }\n}\n\n/**\n * A message's identity for de-duplication.\n *\n * `JSON.stringify` is enough: these are payloads the transport just parsed out\n * of JSON, so key order is the order the server wrote, and the same message\n * arriving twice on a re-attach serialises identically both times.\n */\nfunction stableKey(message: unknown): string {\n try {\n return JSON.stringify(message) ?? \"undefined\";\n } catch {\n // Circular, which a JSON transport cannot produce. Never equal to anything.\n return `${Math.random()}`;\n }\n}\n\n/** Arguments as an object when the accumulation parses, as the raw text otherwise. */\nfunction parseArgs(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { AgentChatMessage } from "./types.js";
|
|
2
|
+
import { AgentChatMessage, AgentChatStep } from "./types.js";
|
|
3
3
|
//#region src/agent-chat/session.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* A new session key, minted client-side.
|
|
@@ -18,8 +18,29 @@ export declare function isSessionOfAgent(key: string, agentId: string): boolean;
|
|
|
18
18
|
export declare function sessionStorageKey(scope: string): string;
|
|
19
19
|
/** The `sessionStorage` name the open/collapsed state is kept under. */
|
|
20
20
|
export declare function viewStorageKey(scope: string): string;
|
|
21
|
+
/** The `sessionStorage` name this window's progress steps are kept under. */
|
|
22
|
+
export declare function stepsStorageKey(scope: string): string;
|
|
21
23
|
export declare function readStoredSessionKey(scope: string): string | null;
|
|
22
24
|
export declare function writeStoredSessionKey(scope: string, key: string | null): void;
|
|
25
|
+
/** Most steps kept, in memory and in `sessionStorage`. A turn that long is a runaway loop. */
|
|
26
|
+
export declare const STEP_LIMIT = 50;
|
|
27
|
+
/**
|
|
28
|
+
* The steps of the current turn, and the session they belong to.
|
|
29
|
+
*
|
|
30
|
+
* They go in the per-window store beside the session key, for the same reason
|
|
31
|
+
* the key does: a tab is reloaded mid-run, or a host remounts the surface on a
|
|
32
|
+
* route change, and both threw the whole strip away. (Collapsing the pop-up is
|
|
33
|
+
* NOT one of them — the panel is `hidden`, not unmounted, and the run streams
|
|
34
|
+
* into it either way.) Messages come back from `chat.history`; steps do not
|
|
35
|
+
* exist on the server, so this is the only place they can come back from.
|
|
36
|
+
*
|
|
37
|
+
* Scoped to a session key, and checked on read: resuming a different session
|
|
38
|
+
* must not inherit the last one's steps.
|
|
39
|
+
*/
|
|
40
|
+
export declare function readStoredSteps(scope: string, sessionKey: string): AgentChatStep[];
|
|
41
|
+
/** The slot's contents, projected and trimmed to fit the byte cap. */
|
|
42
|
+
export declare function serialiseSteps(sessionKey: string, steps: readonly AgentChatStep[]): string;
|
|
43
|
+
export declare function writeStoredSteps(scope: string, sessionKey: string | null, steps: readonly AgentChatStep[]): void;
|
|
23
44
|
/** Raw transcript records to the messages the list renders. */
|
|
24
45
|
export declare function parseHistoryMessages(raw: readonly unknown[]): AgentChatMessage[];
|
|
25
46
|
/**
|