@couplet/agent-ui 1.0.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.
@@ -0,0 +1,135 @@
1
+ import {
2
+ consumeAgentSseStream,
3
+ createAgentMessage,
4
+ createAgentSession,
5
+ extractClarifyFromChain
6
+ } from "./chunk-MJFUPOSB.js";
7
+
8
+ // src/client/endpoints.ts
9
+ var DEFAULT_AGENT_ENDPOINTS = {
10
+ createSession: "/api/chat/session",
11
+ sessions: "/api/chat/sessions",
12
+ completions: "/api/chat/completions"
13
+ };
14
+ function resolveAgentEndpoints(overrides) {
15
+ return {
16
+ ...DEFAULT_AGENT_ENDPOINTS,
17
+ ...overrides
18
+ };
19
+ }
20
+ function getSessionDetailEndpoint(endpoints, sessionId) {
21
+ return `${endpoints.sessions}/${encodeURIComponent(sessionId)}`;
22
+ }
23
+ function getContextUsageEndpoint(endpoints, sessionId) {
24
+ return `${getSessionDetailEndpoint(endpoints, sessionId)}/context-usage`;
25
+ }
26
+
27
+ // src/client/create-client.ts
28
+ function readTimestamp(value, fallback) {
29
+ if (!value) return fallback;
30
+ const timestamp = new Date(value).getTime();
31
+ return Number.isNaN(timestamp) ? fallback : timestamp;
32
+ }
33
+ function toAgentMessage(msg) {
34
+ const chain = msg.chain ?? void 0;
35
+ const clarify = msg.type === "clarify" && chain ? extractClarifyFromChain(chain) : void 0;
36
+ return createAgentMessage(msg.role, msg.content ?? "", {
37
+ id: msg.id ?? void 0,
38
+ type: msg.type || void 0,
39
+ chain,
40
+ clarify,
41
+ createdAt: readTimestamp(msg.created_at, Date.now())
42
+ });
43
+ }
44
+ function toAgentSession(data) {
45
+ const now = Date.now();
46
+ const createdAt = readTimestamp(data.created_at, now);
47
+ const baseSession = createAgentSession(data.session_id, createdAt);
48
+ return {
49
+ ...baseSession,
50
+ title: data.title?.trim() || baseSession.title,
51
+ updatedAt: readTimestamp(data.updated_at, now),
52
+ messages: data.messages.map(toAgentMessage)
53
+ };
54
+ }
55
+ function normalizeRequestBody(body) {
56
+ if (body == null) return body;
57
+ if (typeof body === "string" || body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof FormData || body instanceof URLSearchParams || body instanceof ReadableStream) {
58
+ return body;
59
+ }
60
+ return JSON.stringify(body);
61
+ }
62
+ async function requestStream(transport, url, init = {}) {
63
+ const body = normalizeRequestBody(init.body);
64
+ const headers = new Headers(init.headers);
65
+ if (!headers.has("Content-Type") && typeof body === "string") {
66
+ headers.set("Content-Type", "application/json");
67
+ }
68
+ if (!headers.has("Accept")) {
69
+ headers.set("Accept", "text/event-stream");
70
+ }
71
+ return transport.requestStream(url, { ...init, headers, body });
72
+ }
73
+ function createAgentClient(options) {
74
+ const { transport } = options;
75
+ const endpoints = resolveAgentEndpoints(options.endpoints);
76
+ return {
77
+ endpoints,
78
+ async createSession() {
79
+ const data = await transport.request(endpoints.createSession, {
80
+ method: "POST"
81
+ });
82
+ return data.session_id;
83
+ },
84
+ async listSessions() {
85
+ const data = await transport.request(endpoints.sessions);
86
+ return data.sessions;
87
+ },
88
+ async loadLatestSession() {
89
+ const sessions = await this.listSessions();
90
+ if (sessions.length === 0) {
91
+ const sessionId = await this.createSession();
92
+ return { sessionId, messages: [] };
93
+ }
94
+ const detail = await this.getSessionDetail(sessions[0].id);
95
+ return { sessionId: detail.id, messages: [...detail.messages] };
96
+ },
97
+ async getSessionDetail(sessionId) {
98
+ const data = await transport.request(
99
+ getSessionDetailEndpoint(endpoints, sessionId)
100
+ );
101
+ return toAgentSession(data);
102
+ },
103
+ async fetchContextUsage(sessionId) {
104
+ const data = await transport.request(
105
+ getContextUsageEndpoint(endpoints, sessionId)
106
+ );
107
+ return {
108
+ budget_tokens: data.budget_tokens,
109
+ used_tokens: data.used_tokens,
110
+ used_percent: data.used_percent,
111
+ categories: data.categories,
112
+ source: data.source,
113
+ estimated_tokens: data.estimated_tokens,
114
+ prompt_tokens: data.prompt_tokens
115
+ };
116
+ },
117
+ async sendMessageStream({ request, signal, onEvent }) {
118
+ const response = await requestStream(transport, endpoints.completions, {
119
+ method: "POST",
120
+ body: JSON.stringify(request),
121
+ signal
122
+ });
123
+ if (!response.body) throw new Error("Agent response body is empty");
124
+ await consumeAgentSseStream(response.body, onEvent);
125
+ }
126
+ };
127
+ }
128
+
129
+ export {
130
+ DEFAULT_AGENT_ENDPOINTS,
131
+ resolveAgentEndpoints,
132
+ getSessionDetailEndpoint,
133
+ getContextUsageEndpoint,
134
+ createAgentClient
135
+ };
@@ -0,0 +1,432 @@
1
+ // src/core/session-helpers.ts
2
+ var DEFAULT_PROJECT_NAME = "agent";
3
+ var DEFAULT_SESSION_TITLE = "New conversation";
4
+ var TITLE_MAX_LENGTH = 56;
5
+ function createId(prefix) {
6
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
7
+ return `${prefix}-${crypto.randomUUID()}`;
8
+ }
9
+ return `${prefix}-${Date.now().toString(36)}`;
10
+ }
11
+ function createAgentSession(id, now = Date.now()) {
12
+ return {
13
+ id: id ?? createId("session"),
14
+ title: DEFAULT_SESSION_TITLE,
15
+ project: DEFAULT_PROJECT_NAME,
16
+ createdAt: now,
17
+ updatedAt: now,
18
+ messages: []
19
+ };
20
+ }
21
+ function createAgentMessage(role, content, extras = {}) {
22
+ return {
23
+ id: createId("message"),
24
+ role,
25
+ content,
26
+ createdAt: Date.now(),
27
+ ...extras
28
+ };
29
+ }
30
+ function createClarifyMessage(question, options) {
31
+ return {
32
+ id: createId("message"),
33
+ role: "assistant",
34
+ content: question,
35
+ type: "clarify",
36
+ clarify: { question, options },
37
+ createdAt: Date.now()
38
+ };
39
+ }
40
+ function findLastClarifyCall(chain) {
41
+ return [...chain].reverse().find((node) => node.type === "tool_call" && node.tool_name === "clarify");
42
+ }
43
+ function isClarifyCallAnswered(chain, clarifyCall) {
44
+ return chain.some((node) => node.type === "user_reply" && node.parent_id === clarifyCall.id);
45
+ }
46
+ function extractClarifyFromChain(chain) {
47
+ const lastClarifyCall = findLastClarifyCall(chain);
48
+ if (!lastClarifyCall) return void 0;
49
+ const input = lastClarifyCall.tool_input ?? {};
50
+ const question = typeof input.question === "string" ? input.question : "";
51
+ const options = Array.isArray(input.options) ? input.options.map((option) => String(option)) : void 0;
52
+ if (!question) return void 0;
53
+ return { question, options };
54
+ }
55
+ function getPendingClarify(messages) {
56
+ const message = [...messages].reverse().find((item) => item.role === "assistant" && Boolean(findLastClarifyCall(item.chain ?? [])));
57
+ const chain = message?.chain ?? [];
58
+ const lastClarifyCall = findLastClarifyCall(chain);
59
+ if (!lastClarifyCall || isClarifyCallAnswered(chain, lastClarifyCall)) return void 0;
60
+ const clarify = message?.clarify ?? extractClarifyFromChain(chain);
61
+ if (!clarify?.question || !clarify.options?.length) return void 0;
62
+ return {
63
+ question: clarify.question,
64
+ options: clarify.options,
65
+ chainReplyTo: lastClarifyCall.id
66
+ };
67
+ }
68
+ function updateMessageType(session, messageId, type) {
69
+ return {
70
+ ...session,
71
+ updatedAt: Date.now(),
72
+ messages: session.messages.map(
73
+ (message) => message.id === messageId ? { ...message, type } : message
74
+ )
75
+ };
76
+ }
77
+ function updateClarifyMessage(session, messageId, clarify) {
78
+ return {
79
+ ...session,
80
+ updatedAt: Date.now(),
81
+ messages: session.messages.map(
82
+ (message) => message.id === messageId ? { ...message, content: "", type: "clarify", clarify } : message
83
+ )
84
+ };
85
+ }
86
+ function getSessionTitle(input) {
87
+ const normalized = input.trim().replace(/\s+/g, " ");
88
+ if (!normalized) return DEFAULT_SESSION_TITLE;
89
+ if (normalized.length <= TITLE_MAX_LENGTH) return normalized;
90
+ return `${normalized.slice(0, TITLE_MAX_LENGTH - 1)}...`;
91
+ }
92
+ function appendSessionMessages(session, messages) {
93
+ return {
94
+ ...session,
95
+ title: session.messages.length ? session.title : getSessionTitle(messages[0]?.content ?? ""),
96
+ updatedAt: Date.now(),
97
+ messages: [...session.messages, ...messages]
98
+ };
99
+ }
100
+ function updateMessageContent(session, messageId, content, error = false) {
101
+ return {
102
+ ...session,
103
+ updatedAt: Date.now(),
104
+ messages: session.messages.map(
105
+ (message) => message.id === messageId ? { ...message, content, error } : message
106
+ )
107
+ };
108
+ }
109
+ function updateMessageReasoning(session, messageId, reasoning) {
110
+ return {
111
+ ...session,
112
+ updatedAt: Date.now(),
113
+ messages: session.messages.map(
114
+ (message) => message.id === messageId ? { ...message, reasoning } : message
115
+ )
116
+ };
117
+ }
118
+ function appendUserReplyChainNode(session, parentNodeId, content) {
119
+ const parentMessage = session.messages.find(
120
+ (message) => message.chain?.some((node2) => node2.id === parentNodeId)
121
+ );
122
+ if (!parentMessage) return session;
123
+ const node = {
124
+ id: createId("reply"),
125
+ type: "user_reply",
126
+ title: void 0,
127
+ content,
128
+ parent_id: parentNodeId
129
+ };
130
+ return appendMessageChainNode(session, parentMessage.id, node);
131
+ }
132
+ function appendMessageChainNode(session, messageId, node) {
133
+ return {
134
+ ...session,
135
+ updatedAt: Date.now(),
136
+ messages: session.messages.map((message) => {
137
+ if (message.id !== messageId) return message;
138
+ const existing = message.chain ?? [];
139
+ const index = existing.findIndex((n) => n.id === node.id);
140
+ const nextChain = index >= 0 ? existing.map((n, i) => i === index ? node : n) : [...existing, node];
141
+ return { ...message, chain: nextChain };
142
+ })
143
+ };
144
+ }
145
+ function mergeToolCalls(existing, deltas) {
146
+ const map = /* @__PURE__ */ new Map();
147
+ for (const item of existing ?? []) {
148
+ map.set(item.index, item);
149
+ }
150
+ for (const delta of deltas) {
151
+ const current = map.get(delta.index);
152
+ if (current) {
153
+ map.set(delta.index, {
154
+ index: delta.index,
155
+ id: delta.id || current.id,
156
+ type: delta.type || current.type,
157
+ function: {
158
+ name: delta.function.name || current.function.name,
159
+ arguments: current.function.arguments + delta.function.arguments
160
+ }
161
+ });
162
+ } else {
163
+ map.set(delta.index, delta);
164
+ }
165
+ }
166
+ return Array.from(map.entries()).sort(([a], [b]) => a - b).map(([, item]) => item);
167
+ }
168
+ function appendMessageToolCalls(session, messageId, deltas) {
169
+ return {
170
+ ...session,
171
+ updatedAt: Date.now(),
172
+ messages: session.messages.map(
173
+ (message) => message.id === messageId ? { ...message, toolCalls: mergeToolCalls(message.toolCalls, deltas) } : message
174
+ )
175
+ };
176
+ }
177
+ function removeSessionMessage(session, messageId) {
178
+ return {
179
+ ...session,
180
+ updatedAt: Date.now(),
181
+ messages: session.messages.filter((message) => message.id !== messageId)
182
+ };
183
+ }
184
+
185
+ // src/core/stream.ts
186
+ var STREAM_DONE_MARKER = "[DONE]";
187
+ var SSE_DATA_PREFIX = "data:";
188
+ function assertJsonObject(payload) {
189
+ if (typeof payload !== "object" || payload === null) {
190
+ throw new Error("Agent stream payload must be a JSON object");
191
+ }
192
+ return payload;
193
+ }
194
+ function readJsonObject(value) {
195
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
196
+ return value;
197
+ }
198
+ return null;
199
+ }
200
+ function readString(value) {
201
+ return typeof value === "string" ? value : null;
202
+ }
203
+ function readStringArray(value) {
204
+ if (!Array.isArray(value)) return [];
205
+ return value.map((item) => String(item));
206
+ }
207
+ function readContextUsageSource(value) {
208
+ return value === "estimated" || value === "provider" ? value : void 0;
209
+ }
210
+ function parseContextUsageCategories(value) {
211
+ if (!Array.isArray(value)) return [];
212
+ return value.map((item) => {
213
+ const row = readJsonObject(item);
214
+ if (!row) return null;
215
+ const id = readString(row.id);
216
+ const label = readString(row.label);
217
+ const tokens = typeof row.tokens === "number" ? row.tokens : 0;
218
+ if (!id || !label) return null;
219
+ return { id, label, tokens };
220
+ }).filter((item) => item !== null);
221
+ }
222
+ function parseStreamEvent(eventPayload) {
223
+ const kind = readString(eventPayload.kind);
224
+ if (!kind) return null;
225
+ switch (kind) {
226
+ case "MessageChunk":
227
+ return { kind, text: readString(eventPayload.text) ?? "" };
228
+ case "MessageStop":
229
+ return { kind, final: Boolean(eventPayload.final) };
230
+ case "ThinkingChunk":
231
+ return {
232
+ kind,
233
+ content: readString(eventPayload.content) ?? "",
234
+ node_id: readString(eventPayload.node_id)
235
+ };
236
+ case "ToolCallFinished":
237
+ return {
238
+ kind,
239
+ tool_name: readString(eventPayload.tool_name) ?? "",
240
+ duration: typeof eventPayload.duration === "number" ? eventPayload.duration : 0,
241
+ ok: Boolean(eventPayload.ok),
242
+ index: typeof eventPayload.index === "number" ? eventPayload.index : 0
243
+ };
244
+ case "ToolCallEvent":
245
+ return {
246
+ kind,
247
+ node_id: readString(eventPayload.node_id) ?? "",
248
+ tool_name: readString(eventPayload.tool_name) ?? "",
249
+ tool_input: readJsonObject(eventPayload.tool_input),
250
+ title: readString(eventPayload.title)
251
+ };
252
+ case "ToolResultEvent":
253
+ return {
254
+ kind,
255
+ node_id: readString(eventPayload.node_id) ?? "",
256
+ tool_name: readString(eventPayload.tool_name) ?? "",
257
+ tool_output: readJsonObject(eventPayload.tool_output),
258
+ title: readString(eventPayload.title)
259
+ };
260
+ case "UserReplyEvent":
261
+ return {
262
+ kind,
263
+ node_id: readString(eventPayload.node_id) ?? "",
264
+ content: readString(eventPayload.content) ?? "",
265
+ parent_id: readString(eventPayload.parent_id)
266
+ };
267
+ case "ClarifyEvent":
268
+ return {
269
+ kind,
270
+ node_id: readString(eventPayload.node_id) ?? "",
271
+ question: readString(eventPayload.question) ?? "",
272
+ options: readStringArray(eventPayload.options)
273
+ };
274
+ case "FinalEvent":
275
+ return {
276
+ kind,
277
+ node_id: readString(eventPayload.node_id) ?? "",
278
+ content: readString(eventPayload.content) ?? ""
279
+ };
280
+ case "Retrying":
281
+ return {
282
+ kind,
283
+ attempt: typeof eventPayload.attempt === "number" ? eventPayload.attempt : 0,
284
+ max_retries: typeof eventPayload.max_retries === "number" ? eventPayload.max_retries : 0,
285
+ delay: typeof eventPayload.delay === "number" ? eventPayload.delay : 0,
286
+ reason: readString(eventPayload.reason) ?? ""
287
+ };
288
+ case "ContextCompressed":
289
+ return { kind };
290
+ case "ContextUsageEvent":
291
+ return {
292
+ kind,
293
+ budget_tokens: typeof eventPayload.budget_tokens === "number" ? eventPayload.budget_tokens : 0,
294
+ used_tokens: typeof eventPayload.used_tokens === "number" ? eventPayload.used_tokens : 0,
295
+ used_percent: typeof eventPayload.used_percent === "number" ? eventPayload.used_percent : 0,
296
+ categories: parseContextUsageCategories(eventPayload.categories),
297
+ source: readContextUsageSource(eventPayload.source),
298
+ estimated_tokens: typeof eventPayload.estimated_tokens === "number" ? eventPayload.estimated_tokens : void 0,
299
+ prompt_tokens: typeof eventPayload.prompt_tokens === "number" ? eventPayload.prompt_tokens : void 0
300
+ };
301
+ case "LongToolHint":
302
+ return {
303
+ kind,
304
+ tool_name: readString(eventPayload.tool_name) ?? "",
305
+ duration: typeof eventPayload.duration === "number" ? eventPayload.duration : 0
306
+ };
307
+ case "FatalError":
308
+ return { kind, message: readString(eventPayload.message) ?? "" };
309
+ case "OpenPageEvent":
310
+ return {
311
+ kind,
312
+ url: readString(eventPayload.url) ?? "",
313
+ title: readString(eventPayload.title)
314
+ };
315
+ case "OpenExternalLinkEvent":
316
+ return {
317
+ kind,
318
+ url: readString(eventPayload.url) ?? "",
319
+ title: readString(eventPayload.title)
320
+ };
321
+ default:
322
+ return null;
323
+ }
324
+ }
325
+ function streamEventToChainNode(event) {
326
+ switch (event.kind) {
327
+ case "ThinkingChunk":
328
+ return {
329
+ id: event.node_id ?? `thinking-${Date.now()}`,
330
+ type: "thinking",
331
+ content: event.content
332
+ };
333
+ case "ToolCallEvent":
334
+ return {
335
+ id: event.node_id,
336
+ type: "tool_call",
337
+ title: event.title ?? void 0,
338
+ tool_name: event.tool_name,
339
+ tool_input: event.tool_input ?? void 0
340
+ };
341
+ case "ToolResultEvent":
342
+ return {
343
+ id: event.node_id,
344
+ type: "tool_result",
345
+ title: event.title ?? void 0,
346
+ tool_name: event.tool_name,
347
+ tool_output: event.tool_output ?? void 0
348
+ };
349
+ case "UserReplyEvent":
350
+ return {
351
+ id: event.node_id,
352
+ type: "user_reply",
353
+ content: event.content,
354
+ parent_id: event.parent_id ?? void 0
355
+ };
356
+ case "FinalEvent":
357
+ return {
358
+ id: event.node_id,
359
+ type: "final",
360
+ content: event.content
361
+ };
362
+ default:
363
+ return null;
364
+ }
365
+ }
366
+ function parseSseEnvelope(rawEvent) {
367
+ const dataLines = [];
368
+ for (const line of rawEvent.split(/\r?\n/)) {
369
+ if (!line || line.startsWith(":")) continue;
370
+ if (line.startsWith(SSE_DATA_PREFIX)) {
371
+ dataLines.push(line.slice(SSE_DATA_PREFIX.length).trimStart());
372
+ }
373
+ }
374
+ if (!dataLines.length) return null;
375
+ const payload = dataLines.join("\n").trim();
376
+ if (payload === STREAM_DONE_MARKER) return null;
377
+ const parsed = assertJsonObject(JSON.parse(payload));
378
+ if (parsed.type !== "event") {
379
+ throw new Error(`Unexpected agent stream payload type: ${String(parsed.type)}`);
380
+ }
381
+ const eventPayload = readJsonObject(parsed.event);
382
+ if (!eventPayload) return null;
383
+ return parseStreamEvent(eventPayload);
384
+ }
385
+ function processBufferedEvents(buffered, onEvent) {
386
+ const events = buffered.split(/\r?\n\r?\n/);
387
+ const rest = events.pop() ?? "";
388
+ for (const raw of events) {
389
+ const streamEvent = parseSseEnvelope(raw);
390
+ if (streamEvent) onEvent(streamEvent);
391
+ }
392
+ return rest;
393
+ }
394
+ async function consumeAgentSseStream(body, onEvent) {
395
+ const reader = body.getReader();
396
+ const decoder = new TextDecoder();
397
+ let buffered = "";
398
+ while (true) {
399
+ const { value, done } = await reader.read();
400
+ if (done) break;
401
+ buffered = processBufferedEvents(buffered + decoder.decode(value, { stream: true }), onEvent);
402
+ }
403
+ buffered = processBufferedEvents(buffered + decoder.decode(), onEvent);
404
+ if (buffered.trim()) {
405
+ const streamEvent = parseSseEnvelope(buffered);
406
+ if (streamEvent) onEvent(streamEvent);
407
+ }
408
+ }
409
+
410
+ export {
411
+ createAgentSession,
412
+ createAgentMessage,
413
+ createClarifyMessage,
414
+ findLastClarifyCall,
415
+ isClarifyCallAnswered,
416
+ extractClarifyFromChain,
417
+ getPendingClarify,
418
+ updateMessageType,
419
+ updateClarifyMessage,
420
+ getSessionTitle,
421
+ appendSessionMessages,
422
+ updateMessageContent,
423
+ updateMessageReasoning,
424
+ appendUserReplyChainNode,
425
+ appendMessageChainNode,
426
+ appendMessageToolCalls,
427
+ removeSessionMessage,
428
+ parseStreamEvent,
429
+ streamEventToChainNode,
430
+ parseSseEnvelope,
431
+ consumeAgentSseStream
432
+ };