@dataverse-kit/agent-kit 0.2.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/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @dataverse-kit/agent-kit
2
+
3
+ Fluent UI **v9** conversational and agent surfaces for Dynamics 365 — styled as Dynamics CE,
4
+ hostable in a custom page, a static web app, or a PCF control.
5
+
6
+ > The kit is feature-complete for the four surface families — **grounding**, **action/approval
7
+ > cards**, the **Teams-like shell** and **agent ops** (availability, agent picker, run history,
8
+ > traces, usage) — over the base **conversation surface**. What remains is host proof and
9
+ > retiring one of the existing chat implementations.
10
+
11
+ ```bash
12
+ npm install @dataverse-kit/agent-kit @fluentui/react-components @fluentui/react-icons
13
+ ```
14
+
15
+ ```tsx
16
+ import {
17
+ AgentKitHostProvider, MessageList, Composer, SystemNotice,
18
+ useConversation, MockAgentService,
19
+ } from '@dataverse-kit/agent-kit';
20
+
21
+ function Chat() {
22
+ const service = React.useMemo(() => new MockAgentService({ scenario: 'grounded' }), []);
23
+ const { state, announcement, busy, send, cancel } = useConversation({ service });
24
+ const [draft, setDraft] = React.useState('');
25
+
26
+ return (
27
+ <AgentKitHostProvider>
28
+ <MessageList
29
+ messages={state.messages}
30
+ announcement={announcement}
31
+ busy={busy}
32
+ onCopy={() => {}}
33
+ />
34
+ <SystemNotice failure={state.failure} />
35
+ <Composer
36
+ value={draft}
37
+ onChange={setDraft}
38
+ onSend={(text) => { setDraft(''); void send(text); }}
39
+ onStop={cancel}
40
+ busy={busy}
41
+ />
42
+ </AgentKitHostProvider>
43
+ );
44
+ }
45
+ ```
46
+
47
+ Swap `MockAgentService` for your own `IAgentService` and nothing else changes.
48
+
49
+ ## Entry points
50
+
51
+ | Subpath | Contents | React? |
52
+ |---|---|---|
53
+ | `.` | everything | yes |
54
+ | `./core` | grouping, day dividers, the stream reducer, transport normalizers, citation renumbering, scroll rules, the run-status ladder | **no** |
55
+ | `./services` | `IAgentService`, `MockAgentService`, `NullAgentService`, the provider factory | **no** |
56
+ | `./host` | `AgentKitHostProvider` — `IdPrefixProvider` ▸ `FluentProvider` ▸ `targetDocument` | yes |
57
+
58
+ `./core` and `./services` are framework-agnostic *by gate*, not by convention, so a transport
59
+ adapter can share them with an Azure Function, a web worker or a Node broker.
60
+
61
+ ## The seam
62
+
63
+ The kit ships **no real transport**. It defines one interface and one fake:
64
+
65
+ ```ts
66
+ interface IAgentService {
67
+ readonly name: string;
68
+ isAvailable(): Promise<AgentAvailability>; // probed, never assumed; never throws
69
+ send(turn: AgentTurn): AsyncIterable<AgentEvent>; // ordered; exactly one turn_end
70
+ }
71
+ ```
72
+
73
+ Four properties are deliberate and gated:
74
+
75
+ - **No write method.** `send()` yields events and nothing else — the kit renders a tool *request*
76
+ and raises a callback; the host runs the tool.
77
+ - **No `apiKey` field anywhere.** A browser control cannot keep a secret, so providers take an
78
+ endpoint and a server holds the credential.
79
+ - **Failures are data.** Neither method throws; a transport failure arrives as a `turn_end`.
80
+ - **Six terminal states, not two.** `succeeded | blocked | refused | error | notProvisioned |
81
+ cancelled`. `503 → notProvisioned`, because with on-demand Azure an absent service is a normal
82
+ state and reporting it as a failure makes the app look broken.
83
+
84
+ `MockAgentService` can reach every one of them (plus mid-stream tool calls, citations, abort and a
85
+ throwing transport), so Storybook can show each state and tests can assert it.
86
+
87
+ ## Hosting
88
+
89
+ The kit renders no provider, so a host that already has one is not given a second. For hosts that
90
+ do not, `AgentKitHostProvider` supplies the chain — and in a **PCF control** it is also where the
91
+ tabster fix goes:
92
+
93
+ ```tsx
94
+ <AgentKitHostProvider
95
+ hostTheme={context.fluentDesignLanguage?.tokenTheme}
96
+ targetDocument={createIsolatedTargetDocument(document)}
97
+ />
98
+ ```
99
+
100
+ Fluent v9's tabster is a per-window singleton; a Dynamics form's copy predates the `attrHandlers`
101
+ API, so a bundled control that reaches it throws and unmounts. Passing an isolated
102
+ `targetDocument` gives the control its own tabster core, and every child inherits it. See
103
+ `CLAUDE.md` for the measured hook-level detail.
104
+
105
+ ## Agent ops
106
+
107
+ `AgentOpsPanel` composes availability, the agent picker and run history. The run ladder has six
108
+ states, each with its own words — a content filter, the model declining, an unconfigured
109
+ environment and a user-cancelled run need different reactions, and `AgentStatusBadge` never
110
+ collapses them into one label or into a colour. `UsageMeter` distinguishes "not reported" from
111
+ zero. `RunHistoryPanel` takes a `grid` slot so a host that already has `grid-kit/v9` passes it in
112
+ rather than getting a second, worse grid.
113
+
114
+ ## The shell
115
+
116
+ `AgentAppShell` is the Teams-like frame — rail │ conversation list │ thread │ detail pane —
117
+ composed from `AppRail`, `ConversationList`, `ConversationHeader` and `RightPane`. It measures its
118
+ **container**, not the viewport, because a PCF control or custom page occupies a pane; pass
119
+ `containerWidth` (a PCF's `context.allocatedWidth`) to skip measurement entirely. Breakpoints come
120
+ from `@dataverse-kit/surface-kit/v9` rather than being redeclared.
121
+
122
+ The conversation list is a **listbox**: one tab stop, Up/Down/Home/End, `aria-selected`, and unread
123
+ count and presence carried in each row's accessible name rather than only in a badge and a colour.
124
+
125
+ ## Grounding, and why the citation list is short
126
+
127
+ `CitationList` shows only the sources the answer actually cites — a retriever commonly returns a
128
+ dozen chunks for a two-citation answer, and listing all twelve implies provenance the answer does
129
+ not have. `SourceCard` distinguishes "relevance not reported" from a score of zero.
130
+ `ProviderAttribution` changes its visible wording when a provider **withheld** its answer, so a
131
+ substituted safe response is never attributed to the model as if it were the model's words.
132
+ `GroundingPanel` renders the same object the turn carried, so what the model was told and what the
133
+ screen shows cannot drift.
134
+
135
+ ## Action cards — the kit never runs the tool
136
+
137
+ `ApprovalCard` renders a tool *request* and raises `onApprove` / `onDeny`; the host executes and
138
+ feeds the outcome back as a `tool_result` event. The card cannot double-submit, and `ConfirmInline`
139
+ — the in-tree replacement for Fluent's `Dialog` — traps focus, returns it to the trigger on close
140
+ and declares `aria-modal`, three things the gallery precedent it is modelled on does not do.
141
+
142
+ ## Accessibility
143
+
144
+ The conversation surface is built to a stated contract, each rule with a test behind it:
145
+
146
+ - The transcript is **not** a live region. One polite region announces the settled answer, on a
147
+ state transition — never per token, which would make a screen reader re-read the growing
148
+ string and never finish. Failures go to an assertive region.
149
+ - Every message names its author in the accessible tree, **including grouped continuations**
150
+ where the name is visually suppressed.
151
+ - Hover actions are revealed on hover **or focus**, as a `role="toolbar"` with a roving
152
+ tabindex — one tab stop per message, not one per action.
153
+ - Focus is restored when a focused node is removed, and a streaming answer keeps a stable
154
+ element identity so focus survives it.
155
+
156
+ `axe` runs over every component, but it is a floor: measured, it has no rule for an unlabelled
157
+ toolbar and does not report a dangling `aria-controls`. The hand-rolled ARIA is pinned by explicit
158
+ structural and keyboard tests instead. See `CLAUDE.md`.
159
+
160
+ ## Theming
161
+
162
+ `createAgentTheme()` builds on the shared `@dataverse-kit/fluent-theme` ramp (the same Dynamics
163
+ blue grid-kit/v9 and surface-kit/v9 use) and adds the CE font stack and corner radius.
164
+ `resolveHostTheme()` is the precedence ladder: explicit theme → host theme → Dynamics light/dark.
165
+ The fallback is the **Dynamics** theme, not stock Fluent — a standalone SWA has no host theme, and
166
+ stock Fluent does not read as CE.
package/dist/core.cjs ADDED
@@ -0,0 +1,532 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core/index.ts
21
+ var core_exports = {};
22
+ __export(core_exports, {
23
+ ANNOUNCE_FULL_TEXT_MAX_CHARS: () => ANNOUNCE_FULL_TEXT_MAX_CHARS,
24
+ DEFAULT_GROUP_WINDOW_MS: () => DEFAULT_GROUP_WINDOW_MS,
25
+ NO_ANNOUNCEMENT: () => NO_ANNOUNCEMENT,
26
+ STICK_TO_BOTTOM_THRESHOLD_PX: () => STICK_TO_BOTTOM_THRESHOLD_PX,
27
+ appendUserMessage: () => appendUserMessage,
28
+ applyAgentEvent: () => applyAgentEvent,
29
+ bucketByDay: () => bucketByDay,
30
+ classifyTransportFailure: () => classifyTransportFailure,
31
+ createIdPrefixCounter: () => createIdPrefixCounter,
32
+ dayKeyOf: () => dayKeyOf,
33
+ deriveAnnouncement: () => deriveAnnouncement,
34
+ extractCitationMarkers: () => extractCitationMarkers,
35
+ fromClaudeStreamEvent: () => fromClaudeStreamEvent,
36
+ fromFoundryChatResult: () => fromFoundryChatResult,
37
+ fromOpenAiChatChunk: () => fromOpenAiChatChunk,
38
+ fromSseLine: () => fromSseLine,
39
+ groupMessages: () => groupMessages,
40
+ initialConversation: () => initialConversation,
41
+ isSameAuthorRun: () => isSameAuthorRun,
42
+ isTerminalStatus: () => isTerminalStatus,
43
+ renumberCitations: () => renumberCitations,
44
+ runStatusSeverity: () => runStatusSeverity,
45
+ shouldStickToBottom: () => shouldStickToBottom,
46
+ splitTextByCitations: () => splitTextByCitations
47
+ });
48
+ module.exports = __toCommonJS(core_exports);
49
+
50
+ // src/core/runStatus.ts
51
+ var TERMINAL = /* @__PURE__ */ new Set([
52
+ "succeeded",
53
+ "blocked",
54
+ "refused",
55
+ "error",
56
+ "notProvisioned",
57
+ "cancelled"
58
+ ]);
59
+ function isTerminalStatus(status) {
60
+ return TERMINAL.has(status);
61
+ }
62
+ function runStatusSeverity(status) {
63
+ switch (status) {
64
+ case "succeeded":
65
+ return "success";
66
+ case "blocked":
67
+ case "refused":
68
+ return "warning";
69
+ case "error":
70
+ return "error";
71
+ case "notProvisioned":
72
+ case "cancelled":
73
+ return "info";
74
+ }
75
+ }
76
+ function classifyTransportFailure(input) {
77
+ const { httpStatus, code, retryAfterMs } = input;
78
+ const message = input.message ?? "The agent could not be reached.";
79
+ if (httpStatus === 503) {
80
+ return {
81
+ status: "notProvisioned",
82
+ message,
83
+ code: code ?? "not_provisioned",
84
+ recoverable: false,
85
+ correlationId: void 0
86
+ };
87
+ }
88
+ if (httpStatus === 429) {
89
+ return { status: "error", message, code: code ?? "rate_limited", recoverable: true, retryAfterMs };
90
+ }
91
+ if (code === "content_filter") {
92
+ return { status: "blocked", message, code, recoverable: false };
93
+ }
94
+ return { status: "error", message, code, recoverable: httpStatus !== void 0 && httpStatus >= 500 };
95
+ }
96
+
97
+ // src/core/grouping.ts
98
+ var DEFAULT_GROUP_WINDOW_MS = 5 * 60 * 1e3;
99
+ function dayKey(epochMs, timeZone) {
100
+ return new Intl.DateTimeFormat("en-CA", {
101
+ timeZone,
102
+ year: "numeric",
103
+ month: "2-digit",
104
+ day: "2-digit"
105
+ }).format(new Date(epochMs));
106
+ }
107
+ function isSameAuthorRun(a, b, windowMs = DEFAULT_GROUP_WINDOW_MS, timeZone) {
108
+ if (a.authorId !== b.authorId) return false;
109
+ if (a.role !== b.role) return false;
110
+ if (a.role === "system" || b.role === "system") return false;
111
+ if (b.createdAt < a.createdAt) return false;
112
+ if (b.createdAt - a.createdAt > windowMs) return false;
113
+ return dayKey(a.createdAt, timeZone) === dayKey(b.createdAt, timeZone);
114
+ }
115
+ function groupMessages(messages, options = {}) {
116
+ const windowMs = options.windowMs ?? DEFAULT_GROUP_WINDOW_MS;
117
+ const groups = [];
118
+ for (const message of messages) {
119
+ const current = groups[groups.length - 1];
120
+ const previousId = current?.messageIds[current.messageIds.length - 1];
121
+ const previous = previousId ? messages.find((m) => m.id === previousId) : void 0;
122
+ if (current && previous && isSameAuthorRun(previous, message, windowMs, options.timeZone)) {
123
+ current.messageIds.push(message.id);
124
+ continue;
125
+ }
126
+ groups.push({
127
+ key: `${message.authorId}:${message.id}`,
128
+ authorId: message.authorId,
129
+ role: message.role,
130
+ startedAt: message.createdAt,
131
+ messageIds: [message.id]
132
+ });
133
+ }
134
+ return groups;
135
+ }
136
+
137
+ // src/core/dayDividers.ts
138
+ function dayKeyOf(epochMs, timeZone) {
139
+ return new Intl.DateTimeFormat("en-CA", {
140
+ timeZone,
141
+ year: "numeric",
142
+ month: "2-digit",
143
+ day: "2-digit"
144
+ }).format(new Date(epochMs));
145
+ }
146
+ var DAY_MS = 24 * 60 * 60 * 1e3;
147
+ function labelFor(dayKey2, nowMs, timeZone) {
148
+ const todayKey = dayKeyOf(nowMs, timeZone);
149
+ if (dayKey2 === todayKey) return { kind: "today" };
150
+ if (dayKey2 === dayKeyOf(nowMs - DAY_MS, timeZone)) return { kind: "yesterday" };
151
+ const asDate = /* @__PURE__ */ new Date(`${dayKey2}T00:00:00Z`);
152
+ for (let back = 2; back <= 6; back++) {
153
+ if (dayKey2 === dayKeyOf(nowMs - back * DAY_MS, timeZone)) {
154
+ return { kind: "weekday", weekday: asDate.getUTCDay(), iso: dayKey2 };
155
+ }
156
+ }
157
+ return { kind: "date", iso: dayKey2 };
158
+ }
159
+ function bucketByDay(messages, options = {}) {
160
+ const nowMs = options.now ?? Date.now();
161
+ const buckets = [];
162
+ for (const message of messages) {
163
+ const key = dayKeyOf(message.createdAt, options.timeZone);
164
+ const current = buckets[buckets.length - 1];
165
+ if (current && current.dayKey === key) {
166
+ current.messageIds.push(message.id);
167
+ continue;
168
+ }
169
+ buckets.push({
170
+ dayKey: key,
171
+ label: labelFor(key, nowMs, options.timeZone),
172
+ messageIds: [message.id]
173
+ });
174
+ }
175
+ return buckets;
176
+ }
177
+
178
+ // src/core/citations.ts
179
+ var BRACKET_MARKER = /\[(\d{1,3})\]/g;
180
+ var AOAI_MARKER = /【(\d{1,3}):\d+†[^】]*】/g;
181
+ function extractCitationMarkers(text) {
182
+ const found = [];
183
+ for (const re of [BRACKET_MARKER, AOAI_MARKER]) {
184
+ re.lastIndex = 0;
185
+ let m;
186
+ while ((m = re.exec(text)) !== null) {
187
+ found.push({ marker: m[1], index: m.index, length: m[0].length });
188
+ }
189
+ }
190
+ return found.sort((a, b) => a.index - b.index);
191
+ }
192
+ function renumberCitations(text, sources) {
193
+ const markers = extractCitationMarkers(text);
194
+ const order = [];
195
+ for (const { marker } of markers) if (!order.includes(marker)) order.push(marker);
196
+ const byOriginal = /* @__PURE__ */ new Map();
197
+ order.forEach((marker, i) => byOriginal.set(marker, i + 1));
198
+ const used = [];
199
+ const citations = [];
200
+ for (const marker of order) {
201
+ const source = sources[Number(marker) - 1];
202
+ if (!source) continue;
203
+ used.push(source);
204
+ citations.push({ marker: String(byOriginal.get(marker)), sourceId: source.id });
205
+ }
206
+ let out = "";
207
+ let cursor = 0;
208
+ for (const { marker, index, length } of markers) {
209
+ out += text.slice(cursor, index);
210
+ const renumbered = byOriginal.get(marker);
211
+ out += renumbered === void 0 ? "" : `[${renumbered}]`;
212
+ cursor = index + length;
213
+ }
214
+ out += text.slice(cursor);
215
+ return { text: out, used, citations };
216
+ }
217
+ function splitTextByCitations(text, citations) {
218
+ const markers = extractCitationMarkers(text);
219
+ if (markers.length === 0) return text ? [{ kind: "text", text }] : [];
220
+ const segments = [];
221
+ let cursor = 0;
222
+ for (const { marker, index, length } of markers) {
223
+ if (index > cursor) segments.push({ kind: "text", text: text.slice(cursor, index) });
224
+ const citation = citations.find((c) => c.marker === marker);
225
+ segments.push({
226
+ kind: "citation",
227
+ text: `[${marker}]`,
228
+ marker,
229
+ sourceId: citation?.sourceId
230
+ });
231
+ cursor = index + length;
232
+ }
233
+ if (cursor < text.length) segments.push({ kind: "text", text: text.slice(cursor) });
234
+ return segments;
235
+ }
236
+
237
+ // src/core/conversationReducer.ts
238
+ function initialConversation(seed = []) {
239
+ return { messages: [...seed], streamingMessageId: null, status: "idle" };
240
+ }
241
+ function patch(state, messageId, update) {
242
+ const i = state.messages.findIndex((m) => m.id === messageId);
243
+ if (i === -1) return state.messages;
244
+ const next = state.messages.slice();
245
+ next[i] = update(next[i]);
246
+ return next;
247
+ }
248
+ function appendUserMessage(state, input) {
249
+ const message = {
250
+ id: input.id,
251
+ role: "user",
252
+ authorId: input.authorId ?? "user",
253
+ text: input.text,
254
+ createdAt: input.now ?? Date.now(),
255
+ attachments: input.attachments
256
+ };
257
+ return { ...state, messages: [...state.messages, message], status: "connecting", failure: void 0 };
258
+ }
259
+ function applyAgentEvent(state, event) {
260
+ switch (event.type) {
261
+ case "turn_start": {
262
+ const message = {
263
+ id: event.messageId,
264
+ role: "assistant",
265
+ authorId: event.provider ?? "agent",
266
+ text: "",
267
+ createdAt: Date.now(),
268
+ streaming: true,
269
+ provider: event.provider
270
+ };
271
+ return {
272
+ ...state,
273
+ messages: [...state.messages, message],
274
+ streamingMessageId: event.messageId,
275
+ status: "streaming",
276
+ statusLabel: void 0,
277
+ failure: void 0,
278
+ suggestions: void 0
279
+ };
280
+ }
281
+ case "text_delta":
282
+ return {
283
+ ...state,
284
+ messages: patch(state, event.messageId, (m) => ({ ...m, text: m.text + event.text }))
285
+ };
286
+ case "thinking_delta":
287
+ return {
288
+ ...state,
289
+ messages: patch(state, event.messageId, (m) => ({
290
+ ...m,
291
+ thinking: (m.thinking ?? "") + event.text
292
+ }))
293
+ };
294
+ case "tool_call":
295
+ return {
296
+ ...state,
297
+ messages: patch(state, event.messageId, (m) => ({
298
+ ...m,
299
+ toolCalls: [...m.toolCalls ?? [], event.call]
300
+ }))
301
+ };
302
+ case "tool_result":
303
+ return {
304
+ ...state,
305
+ messages: patch(state, event.messageId, (m) => ({
306
+ ...m,
307
+ toolCalls: (m.toolCalls ?? []).map(
308
+ (c) => c.id === event.callId ? { ...c, status: event.status, resultText: event.resultText, failure: event.failure } : c
309
+ )
310
+ }))
311
+ };
312
+ case "sources":
313
+ return {
314
+ ...state,
315
+ messages: patch(state, event.messageId, (m) => ({ ...m, sources: event.sources }))
316
+ };
317
+ case "citation":
318
+ return {
319
+ ...state,
320
+ messages: patch(state, event.messageId, (m) => ({
321
+ ...m,
322
+ citations: [...m.citations ?? [], event.citation]
323
+ }))
324
+ };
325
+ case "suggestions":
326
+ return { ...state, suggestions: event.prompts };
327
+ case "status":
328
+ return { ...state, statusLabel: event.label };
329
+ case "turn_end": {
330
+ const { result } = event;
331
+ return {
332
+ ...state,
333
+ messages: patch(state, event.messageId, (m) => ({
334
+ ...m,
335
+ // A non-streaming provider sends no deltas, so the assembled text arrives here.
336
+ text: m.text.length > 0 ? m.text : result.text,
337
+ streaming: false,
338
+ guarded: result.guarded,
339
+ provider: result.provider,
340
+ usage: result.usage,
341
+ failure: result.failure
342
+ })),
343
+ streamingMessageId: null,
344
+ status: result.status,
345
+ statusLabel: void 0,
346
+ failure: result.failure,
347
+ usage: result.usage
348
+ };
349
+ }
350
+ }
351
+ }
352
+
353
+ // src/core/normalize.ts
354
+ function fromSseLine(line) {
355
+ const trimmed = line.trim();
356
+ if (trimmed === "" || trimmed.startsWith(":")) return void 0;
357
+ if (!trimmed.startsWith("data:")) return void 0;
358
+ const payload = trimmed.slice("data:".length).trim();
359
+ if (payload === "") return void 0;
360
+ if (payload === "[DONE]") return "[DONE]";
361
+ try {
362
+ return JSON.parse(payload);
363
+ } catch {
364
+ return void 0;
365
+ }
366
+ }
367
+ function fromOpenAiChatChunk(chunk, ctx) {
368
+ const events = [];
369
+ const delta = chunk?.choices?.[0]?.delta;
370
+ if (!delta) return events;
371
+ if (typeof delta.content === "string" && delta.content.length > 0) {
372
+ events.push({ type: "text_delta", messageId: ctx.messageId, text: delta.content });
373
+ }
374
+ for (const call of delta.tool_calls ?? []) {
375
+ if (!call.id) continue;
376
+ const toolCall = {
377
+ id: call.id,
378
+ name: call.function?.name ?? "",
379
+ argumentsJson: call.function?.arguments ?? "",
380
+ status: "proposed"
381
+ };
382
+ events.push({ type: "tool_call", messageId: ctx.messageId, call: toolCall });
383
+ }
384
+ return events;
385
+ }
386
+ function fromClaudeStreamEvent(event, ctx) {
387
+ switch (event.type) {
388
+ case "init":
389
+ return [
390
+ {
391
+ type: "turn_start",
392
+ turnId: ctx.turnId,
393
+ messageId: ctx.messageId,
394
+ model: event.model,
395
+ provider: "claude-bridge",
396
+ sessionId: event.sessionId
397
+ }
398
+ ];
399
+ case "text_delta":
400
+ return event.text ? [{ type: "text_delta", messageId: ctx.messageId, text: event.text }] : [];
401
+ case "thinking_delta":
402
+ return event.text ? [{ type: "thinking_delta", messageId: ctx.messageId, text: event.text }] : [];
403
+ case "done":
404
+ return [
405
+ {
406
+ type: "turn_end",
407
+ turnId: ctx.turnId,
408
+ messageId: ctx.messageId,
409
+ result: {
410
+ status: "succeeded",
411
+ text: event.fullText ?? "",
412
+ provider: "claude-bridge",
413
+ model: event.model,
414
+ usage: {
415
+ inputTokens: event.inputTokens,
416
+ outputTokens: event.outputTokens,
417
+ cachedTokens: event.cacheReadTokens,
418
+ costUsd: event.costUsd,
419
+ durationMs: event.durationMs,
420
+ model: event.model
421
+ }
422
+ }
423
+ }
424
+ ];
425
+ case "error":
426
+ return [
427
+ {
428
+ type: "turn_end",
429
+ turnId: ctx.turnId,
430
+ messageId: ctx.messageId,
431
+ result: {
432
+ status: "error",
433
+ text: "",
434
+ provider: "claude-bridge",
435
+ failure: {
436
+ status: "error",
437
+ message: event.message ?? "The agent failed.",
438
+ recoverable: event.recoverable ?? false
439
+ }
440
+ }
441
+ }
442
+ ];
443
+ }
444
+ }
445
+ function fromFoundryChatResult(result, ctx, provider = "foundry") {
446
+ return [
447
+ { type: "turn_start", turnId: ctx.turnId, messageId: ctx.messageId, model: result.model, provider },
448
+ {
449
+ type: "turn_end",
450
+ turnId: ctx.turnId,
451
+ messageId: ctx.messageId,
452
+ result: {
453
+ status: "succeeded",
454
+ text: result.text,
455
+ provider,
456
+ model: result.model,
457
+ usage: {
458
+ inputTokens: result.promptTokens,
459
+ outputTokens: result.completionTokens,
460
+ model: result.model
461
+ }
462
+ }
463
+ }
464
+ ];
465
+ }
466
+
467
+ // src/core/announce.ts
468
+ var ANNOUNCE_FULL_TEXT_MAX_CHARS = 240;
469
+ var NO_ANNOUNCEMENT = { polite: "", assertive: "" };
470
+ var TERMINAL2 = /* @__PURE__ */ new Set(["succeeded", "blocked", "refused", "error", "notProvisioned", "cancelled"]);
471
+ function deriveAnnouncement(state, previous) {
472
+ const wasTerminal = previous ? TERMINAL2.has(previous.status) : false;
473
+ const isTerminal = TERMINAL2.has(state.status);
474
+ if (!isTerminal || wasTerminal) return NO_ANNOUNCEMENT;
475
+ if (state.failure) {
476
+ return { polite: "", assertive: state.failure.message };
477
+ }
478
+ const last = state.messages[state.messages.length - 1];
479
+ const text = last?.text ?? "";
480
+ if (!text) return NO_ANNOUNCEMENT;
481
+ const guarded = last?.guarded ? "The response was withheld. " : "";
482
+ if (text.length <= ANNOUNCE_FULL_TEXT_MAX_CHARS) {
483
+ return { polite: `${guarded}${text}`, assertive: "" };
484
+ }
485
+ const words = text.trim().split(/\s+/).length;
486
+ return {
487
+ polite: `${guarded}Response complete, ${words} words.`,
488
+ assertive: ""
489
+ };
490
+ }
491
+
492
+ // src/core/scroll.ts
493
+ var STICK_TO_BOTTOM_THRESHOLD_PX = 32;
494
+ function shouldStickToBottom(geometry, thresholdPx = STICK_TO_BOTTOM_THRESHOLD_PX) {
495
+ const { scrollTop, scrollHeight, clientHeight } = geometry;
496
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
497
+ return distanceFromBottom <= thresholdPx;
498
+ }
499
+
500
+ // src/core/ids.ts
501
+ function createIdPrefixCounter(tag) {
502
+ let n = 0;
503
+ return () => `${tag}${++n}-`;
504
+ }
505
+ // Annotate the CommonJS export names for ESM import in node:
506
+ 0 && (module.exports = {
507
+ ANNOUNCE_FULL_TEXT_MAX_CHARS,
508
+ DEFAULT_GROUP_WINDOW_MS,
509
+ NO_ANNOUNCEMENT,
510
+ STICK_TO_BOTTOM_THRESHOLD_PX,
511
+ appendUserMessage,
512
+ applyAgentEvent,
513
+ bucketByDay,
514
+ classifyTransportFailure,
515
+ createIdPrefixCounter,
516
+ dayKeyOf,
517
+ deriveAnnouncement,
518
+ extractCitationMarkers,
519
+ fromClaudeStreamEvent,
520
+ fromFoundryChatResult,
521
+ fromOpenAiChatChunk,
522
+ fromSseLine,
523
+ groupMessages,
524
+ initialConversation,
525
+ isSameAuthorRun,
526
+ isTerminalStatus,
527
+ renumberCitations,
528
+ runStatusSeverity,
529
+ shouldStickToBottom,
530
+ splitTextByCitations
531
+ });
532
+ //# sourceMappingURL=core.cjs.map