@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 +166 -0
- package/dist/core.cjs +532 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +278 -0
- package/dist/core.d.ts +278 -0
- package/dist/core.mjs +482 -0
- package/dist/core.mjs.map +1 -0
- package/dist/host.cjs +113 -0
- package/dist/host.cjs.map +1 -0
- package/dist/host.d.cts +3 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.mjs +74 -0
- package/dist/host.mjs.map +1 -0
- package/dist/index-DGN6eYnp.d.cts +103 -0
- package/dist/index-DGN6eYnp.d.ts +103 -0
- package/dist/index.cjs +3145 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +863 -0
- package/dist/index.d.ts +863 -0
- package/dist/index.mjs +3068 -0
- package/dist/index.mjs.map +1 -0
- package/dist/protocol-CZJ1v9Hq.d.cts +270 -0
- package/dist/protocol-CZJ1v9Hq.d.ts +270 -0
- package/dist/services.cjs +333 -0
- package/dist/services.cjs.map +1 -0
- package/dist/services.d.cts +118 -0
- package/dist/services.d.ts +118 -0
- package/dist/services.mjs +300 -0
- package/dist/services.mjs.map +1 -0
- package/package.json +89 -0
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
|