@codespring-app/use-agent 0.1.0 → 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/CHANGELOG.md +14 -0
- package/README.md +28 -0
- package/dist/{chunk-FAD2XMPA.js → chunk-CC365XBZ.js} +168 -2
- package/dist/{client-Bz3eVQXx.d.ts → client-DcCIhqkm.d.ts} +48 -1
- package/dist/index.d.ts +21 -3
- package/dist/index.js +3 -1
- package/dist/react.d.ts +22 -5
- package/dist/react.js +251 -52
- package/package.json +4 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0 — 2026-08-30
|
|
4
|
+
|
|
5
|
+
- Add origin-bound, single-use browser WebSocket ticket exchange.
|
|
6
|
+
- Add durable multi-page replay and the public `AgentSession.connect` API.
|
|
7
|
+
- Add contiguous cursor tracking, replay/live deduplication, gap recovery, and
|
|
8
|
+
reconnect state to the React session store.
|
|
9
|
+
- Align message and tool reducers with the canonical runtime event protocol.
|
|
10
|
+
- Add the reusable `AgentEventBuffer` and a production live-replay smoke.
|
|
11
|
+
|
|
12
|
+
## 0.1.0 — 2026-08-30
|
|
13
|
+
|
|
14
|
+
- Publish the initial public server and React SDK preview.
|
package/README.md
CHANGED
|
@@ -75,6 +75,12 @@ The `/browser` endpoint is intentional: it accepts only short-lived client
|
|
|
75
75
|
tokens and is the only runtime surface with browser CORS. Server API keys stay
|
|
76
76
|
on the endpoint root and must never be shipped to a browser.
|
|
77
77
|
|
|
78
|
+
The React session store loads durable history, then switches to a live
|
|
79
|
+
WebSocket using a 30-second, single-use ticket. It keeps one contiguous event
|
|
80
|
+
cursor, removes replay/live duplicates, repairs gaps over HTTP, and reconnects
|
|
81
|
+
with bounded jitter. `useAgentSession` exposes `connection` as `idle`,
|
|
82
|
+
`connecting`, `live`, `reconnecting`, or `closed` for custom status UI.
|
|
83
|
+
|
|
78
84
|
The default Paper experience renders assistant replies as document content on
|
|
79
85
|
an edge-to-edge canvas, user messages as quiet trailing wells, tool calls as
|
|
80
86
|
compact inspectable activity rows, and the live-edge composer without a shadow.
|
|
@@ -153,6 +159,28 @@ mixed with client-owned components.
|
|
|
153
159
|
The browser entrypoint never accepts an API key. A trusted application backend
|
|
154
160
|
must issue short-lived, origin-bound client tokens.
|
|
155
161
|
|
|
162
|
+
For a non-React browser UI, connect to the same durable stream directly:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const session = agentClient.sessions.get(sessionId);
|
|
166
|
+
const connection = await session.connect({
|
|
167
|
+
after: lastAppliedCursor,
|
|
168
|
+
onEvent(event) {
|
|
169
|
+
// Persist or reduce the event, then advance lastAppliedCursor.
|
|
170
|
+
},
|
|
171
|
+
onReplayComplete(cursor) {
|
|
172
|
+
console.log("Live at", cursor);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
connection.close();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The SDK performs the authenticated ticket exchange and automatically follows
|
|
180
|
+
multi-page WebSocket replay. `AgentEventBuffer` is available to headless
|
|
181
|
+
clients that want the same contiguous-cursor, deduplication, and gap-detection
|
|
182
|
+
rules as the React store.
|
|
183
|
+
|
|
156
184
|
## Local showcase
|
|
157
185
|
|
|
158
186
|
```sh
|
|
@@ -42,6 +42,101 @@ var Transport = class {
|
|
|
42
42
|
}
|
|
43
43
|
return payload;
|
|
44
44
|
}
|
|
45
|
+
async connectSession(sessionId, options) {
|
|
46
|
+
if (!this.options.browser) {
|
|
47
|
+
throw new TypeError("Browser WebSocket connections require createBrowserClient");
|
|
48
|
+
}
|
|
49
|
+
const after = options.after ?? 0;
|
|
50
|
+
if (!Number.isSafeInteger(after) || after < 0) {
|
|
51
|
+
throw new TypeError("after must be a non-negative safe integer");
|
|
52
|
+
}
|
|
53
|
+
const issued = await this.request(
|
|
54
|
+
`/v1/sessions/${encodeURIComponent(sessionId)}/websocket-tickets`,
|
|
55
|
+
{
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify({ after }),
|
|
58
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
const socketUrl = new URL(
|
|
62
|
+
`${this.endpoint}/v1/sessions/${encodeURIComponent(sessionId)}/connect`
|
|
63
|
+
);
|
|
64
|
+
socketUrl.protocol = socketUrl.protocol === "https:" ? "wss:" : "ws:";
|
|
65
|
+
socketUrl.searchParams.set("ticket", issued.ticket);
|
|
66
|
+
const createSocket = this.options.webSocket ?? defaultWebSocketFactory;
|
|
67
|
+
const socket = createSocket(socketUrl.toString());
|
|
68
|
+
let cursor = after;
|
|
69
|
+
let opened = false;
|
|
70
|
+
let settled = false;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const connection = {
|
|
73
|
+
get cursor() {
|
|
74
|
+
return cursor;
|
|
75
|
+
},
|
|
76
|
+
close: (code = 1e3, reason = "client closed") => socket.close(code, reason)
|
|
77
|
+
};
|
|
78
|
+
const failBeforeOpen = (error) => {
|
|
79
|
+
if (settled) return;
|
|
80
|
+
settled = true;
|
|
81
|
+
reject(error);
|
|
82
|
+
};
|
|
83
|
+
socket.addEventListener("open", () => {
|
|
84
|
+
opened = true;
|
|
85
|
+
if (settled) return;
|
|
86
|
+
settled = true;
|
|
87
|
+
resolve(connection);
|
|
88
|
+
});
|
|
89
|
+
socket.addEventListener("message", (message) => {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = parseWebSocketServerMessage(message.data);
|
|
92
|
+
if (parsed.type === "event") {
|
|
93
|
+
cursor = Math.max(cursor, parsed.event.id);
|
|
94
|
+
options.onEvent(parsed.event);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (parsed.type === "replay.completed") {
|
|
98
|
+
cursor = Math.max(cursor, parsed.cursor);
|
|
99
|
+
if (parsed.hasMore) {
|
|
100
|
+
socket.send(JSON.stringify({ type: "replay", after: cursor }));
|
|
101
|
+
} else {
|
|
102
|
+
options.onReplayComplete?.(cursor);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const error = new AgentError(parsed.message, 0, parsed.code);
|
|
107
|
+
options.onError?.(error);
|
|
108
|
+
socket.close(1008, "server rejected connection");
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
111
|
+
options.onError?.(normalized);
|
|
112
|
+
socket.close(1008, "invalid server message");
|
|
113
|
+
if (!opened) failBeforeOpen(normalized);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
socket.addEventListener("error", () => {
|
|
117
|
+
const error = new AgentError("WebSocket connection failed", 0, "websocket_failed");
|
|
118
|
+
options.onError?.(error);
|
|
119
|
+
if (!opened) failBeforeOpen(error);
|
|
120
|
+
});
|
|
121
|
+
socket.addEventListener("close", (event) => {
|
|
122
|
+
options.onClose?.(event);
|
|
123
|
+
if (!opened) {
|
|
124
|
+
failBeforeOpen(
|
|
125
|
+
new AgentError(
|
|
126
|
+
"WebSocket closed before connecting",
|
|
127
|
+
0,
|
|
128
|
+
"websocket_closed"
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (options.signal) {
|
|
134
|
+
const closeForAbort = () => socket.close(1e3, "request aborted");
|
|
135
|
+
if (options.signal.aborted) closeForAbort();
|
|
136
|
+
else options.signal.addEventListener("abort", closeForAbort, { once: true });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
45
140
|
async fetchWithToken(path, init) {
|
|
46
141
|
const headers = new Headers(init.headers);
|
|
47
142
|
headers.set("Accept", "application/json");
|
|
@@ -169,6 +264,9 @@ var AgentSession = class {
|
|
|
169
264
|
}
|
|
170
265
|
);
|
|
171
266
|
}
|
|
267
|
+
connect(options) {
|
|
268
|
+
return this.transport.connectSession(this.id, options);
|
|
269
|
+
}
|
|
172
270
|
};
|
|
173
271
|
var AgentClient = class {
|
|
174
272
|
constructor(transport) {
|
|
@@ -193,6 +291,7 @@ function createClient(options) {
|
|
|
193
291
|
new Transport({
|
|
194
292
|
endpoint: options.endpoint,
|
|
195
293
|
token: staticTokenProvider(options.apiKey),
|
|
294
|
+
browser: false,
|
|
196
295
|
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
197
296
|
})
|
|
198
297
|
);
|
|
@@ -206,15 +305,82 @@ function createBrowserClient(options) {
|
|
|
206
305
|
options.clientTokenTtlMs ?? 6e4,
|
|
207
306
|
options.refreshSkewMs ?? 3e4
|
|
208
307
|
),
|
|
209
|
-
|
|
308
|
+
browser: true,
|
|
309
|
+
...options.fetch === void 0 ? {} : { fetch: options.fetch },
|
|
310
|
+
...options.webSocket === void 0 ? {} : { webSocket: options.webSocket }
|
|
210
311
|
})
|
|
211
312
|
);
|
|
212
313
|
}
|
|
314
|
+
function defaultWebSocketFactory(url) {
|
|
315
|
+
if (typeof globalThis.WebSocket !== "function") {
|
|
316
|
+
throw new TypeError("A WebSocket implementation is required");
|
|
317
|
+
}
|
|
318
|
+
return new globalThis.WebSocket(url);
|
|
319
|
+
}
|
|
320
|
+
function parseWebSocketServerMessage(value) {
|
|
321
|
+
if (typeof value !== "string") throw new TypeError("WebSocket message must be JSON text");
|
|
322
|
+
const parsed = JSON.parse(value);
|
|
323
|
+
if (!isObject(parsed) || typeof parsed.type !== "string") {
|
|
324
|
+
throw new TypeError("WebSocket message is invalid");
|
|
325
|
+
}
|
|
326
|
+
if (parsed.type === "event" && isAgentEvent(parsed.event)) {
|
|
327
|
+
return { type: "event", event: parsed.event };
|
|
328
|
+
}
|
|
329
|
+
if (parsed.type === "replay.completed" && Number.isSafeInteger(parsed.cursor) && parsed.cursor >= 0 && typeof parsed.hasMore === "boolean") {
|
|
330
|
+
return {
|
|
331
|
+
type: "replay.completed",
|
|
332
|
+
cursor: parsed.cursor,
|
|
333
|
+
hasMore: parsed.hasMore
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (parsed.type === "error" && typeof parsed.code === "string" && typeof parsed.message === "string") {
|
|
337
|
+
return { type: "error", code: parsed.code, message: parsed.message };
|
|
338
|
+
}
|
|
339
|
+
throw new TypeError("WebSocket message is invalid");
|
|
340
|
+
}
|
|
341
|
+
function isAgentEvent(value) {
|
|
342
|
+
return isObject(value) && value.schemaVersion === 1 && Number.isSafeInteger(value.id) && value.id > 0 && typeof value.sessionId === "string" && Number.isSafeInteger(value.attempt) && value.attempt >= 0 && typeof value.type === "string" && typeof value.createdAt === "string" && (!("turnId" in value) || typeof value.turnId === "string");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/event-buffer.ts
|
|
346
|
+
var AgentEventBuffer = class {
|
|
347
|
+
ordered = [];
|
|
348
|
+
currentCursor = 0;
|
|
349
|
+
get cursor() {
|
|
350
|
+
return this.currentCursor;
|
|
351
|
+
}
|
|
352
|
+
get events() {
|
|
353
|
+
return this.ordered;
|
|
354
|
+
}
|
|
355
|
+
merge(incoming) {
|
|
356
|
+
const accepted = [];
|
|
357
|
+
const duplicateIds = [];
|
|
358
|
+
for (const event of incoming) {
|
|
359
|
+
if (event.id <= this.currentCursor) {
|
|
360
|
+
duplicateIds.push(event.id);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const expected = this.currentCursor + 1;
|
|
364
|
+
if (event.id !== expected) {
|
|
365
|
+
return { accepted, duplicateIds, gap: { expected, received: event.id } };
|
|
366
|
+
}
|
|
367
|
+
this.ordered.push(event);
|
|
368
|
+
this.currentCursor = event.id;
|
|
369
|
+
accepted.push(event);
|
|
370
|
+
}
|
|
371
|
+
return { accepted, duplicateIds };
|
|
372
|
+
}
|
|
373
|
+
reset() {
|
|
374
|
+
this.ordered = [];
|
|
375
|
+
this.currentCursor = 0;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
213
378
|
|
|
214
379
|
export {
|
|
215
380
|
AgentError,
|
|
216
381
|
AgentSession,
|
|
217
382
|
AgentClient,
|
|
218
383
|
createClient,
|
|
219
|
-
createBrowserClient
|
|
384
|
+
createBrowserClient,
|
|
385
|
+
AgentEventBuffer
|
|
220
386
|
};
|
|
@@ -40,6 +40,47 @@ interface ListEventsResponse {
|
|
|
40
40
|
cursor: number;
|
|
41
41
|
hasMore: boolean;
|
|
42
42
|
}
|
|
43
|
+
interface CreateWebSocketTicketResponse {
|
|
44
|
+
ticket: string;
|
|
45
|
+
expiresAt: string;
|
|
46
|
+
}
|
|
47
|
+
type WebSocketServerMessage = {
|
|
48
|
+
type: "event";
|
|
49
|
+
event: AgentEvent;
|
|
50
|
+
} | {
|
|
51
|
+
type: "replay.completed";
|
|
52
|
+
cursor: number;
|
|
53
|
+
hasMore: boolean;
|
|
54
|
+
} | {
|
|
55
|
+
type: "error";
|
|
56
|
+
code: string;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
interface AgentWebSocketEventMap {
|
|
60
|
+
open: Event;
|
|
61
|
+
message: MessageEvent;
|
|
62
|
+
error: Event;
|
|
63
|
+
close: CloseEvent;
|
|
64
|
+
}
|
|
65
|
+
interface AgentWebSocket {
|
|
66
|
+
readonly readyState: number;
|
|
67
|
+
addEventListener<K extends keyof AgentWebSocketEventMap>(type: K, listener: (event: AgentWebSocketEventMap[K]) => void): void;
|
|
68
|
+
send(data: string): void;
|
|
69
|
+
close(code?: number, reason?: string): void;
|
|
70
|
+
}
|
|
71
|
+
type AgentWebSocketFactory = (url: string) => AgentWebSocket;
|
|
72
|
+
interface AgentConnectionOptions {
|
|
73
|
+
after?: number;
|
|
74
|
+
signal?: AbortSignal;
|
|
75
|
+
onEvent: (event: AgentEvent) => void;
|
|
76
|
+
onReplayComplete?: (cursor: number) => void;
|
|
77
|
+
onError?: (error: Error) => void;
|
|
78
|
+
onClose?: (event: CloseEvent) => void;
|
|
79
|
+
}
|
|
80
|
+
interface AgentConnection {
|
|
81
|
+
readonly cursor: number;
|
|
82
|
+
close(code?: number, reason?: string): void;
|
|
83
|
+
}
|
|
43
84
|
/** Tenant-scoped model profile configured in the CodeSpring control plane. */
|
|
44
85
|
type ModelProfileId = string;
|
|
45
86
|
interface AgentToolReference {
|
|
@@ -95,6 +136,8 @@ interface BrowserAgentClientOptions {
|
|
|
95
136
|
/** Refresh before expiry. Defaults to 30 seconds and is bounded for short tokens. */
|
|
96
137
|
refreshSkewMs?: number;
|
|
97
138
|
fetch?: FetchLike;
|
|
139
|
+
/** Injectable for tests or non-DOM browser runtimes. Defaults to the global WebSocket constructor. */
|
|
140
|
+
webSocket?: AgentWebSocketFactory;
|
|
98
141
|
}
|
|
99
142
|
type ClientTokenResult = string | {
|
|
100
143
|
token: string;
|
|
@@ -117,6 +160,8 @@ interface TransportOptions {
|
|
|
117
160
|
endpoint: string;
|
|
118
161
|
token: TokenProvider;
|
|
119
162
|
fetch?: FetchLike;
|
|
163
|
+
webSocket?: AgentWebSocketFactory;
|
|
164
|
+
browser: boolean;
|
|
120
165
|
}
|
|
121
166
|
declare class Transport {
|
|
122
167
|
private readonly options;
|
|
@@ -124,6 +169,7 @@ declare class Transport {
|
|
|
124
169
|
readonly fetchImplementation: FetchLike;
|
|
125
170
|
constructor(options: TransportOptions);
|
|
126
171
|
request<T>(path: string, init?: RequestInit): Promise<T>;
|
|
172
|
+
connectSession(sessionId: string, options: AgentConnectionOptions): Promise<AgentConnection>;
|
|
127
173
|
private fetchWithToken;
|
|
128
174
|
}
|
|
129
175
|
declare class AgentSession {
|
|
@@ -134,6 +180,7 @@ declare class AgentSession {
|
|
|
134
180
|
submit(content: string, options?: SubmitOptions): Promise<SubmitTurnResponse>;
|
|
135
181
|
events(after?: number, limit?: number, options?: RequestOptions): Promise<ListEventsResponse>;
|
|
136
182
|
cancel(turnId: string, options?: RequestOptions): Promise<TurnStatusResponse>;
|
|
183
|
+
connect(options: AgentConnectionOptions): Promise<AgentConnection>;
|
|
137
184
|
}
|
|
138
185
|
interface TurnStatusResponse {
|
|
139
186
|
sessionId: string;
|
|
@@ -153,4 +200,4 @@ declare function createClient(options: AgentClientOptions): AgentClient;
|
|
|
153
200
|
/** Browser-safe client used by the React subpath with short-lived client tokens. */
|
|
154
201
|
declare function createBrowserClient(options: BrowserAgentClientOptions): AgentClient;
|
|
155
202
|
|
|
156
|
-
export { type AgentDefinition as A, type BrowserAgentClientOptions as B, type CreateAgentOptions as C, type FetchLike as F, type ListEventsResponse as L, type ModelProfileId as M, type RequestOptions as R, type SessionSnapshot as S, type TurnStatus as T,
|
|
203
|
+
export { type AgentDefinition as A, type BrowserAgentClientOptions as B, type CreateAgentOptions as C, type FetchLike as F, type ListEventsResponse as L, type ModelProfileId as M, type RequestOptions as R, type SessionSnapshot as S, type TurnStatus as T, type WebSocketServerMessage as W, type AgentEvent as a, AgentClient as b, type AgentClientOptions as c, type AgentConnection as d, type AgentConnectionOptions as e, AgentError as f, type AgentMcpServerReference as g, AgentSession as h, type AgentSkillReference as i, type AgentToolReference as j, type AgentWebSocket as k, type AgentWebSocketEventMap as l, type AgentWebSocketFactory as m, type ClientTokenResult as n, type CreateSessionResponse as o, type CreateWebSocketTicketResponse as p, type SubmitOptions as q, type SubmitTurnResponse as r, createBrowserClient as s, createClient as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import { C as CreateAgentOptions, A as AgentDefinition } from './client-
|
|
2
|
-
export {
|
|
1
|
+
import { C as CreateAgentOptions, A as AgentDefinition, a as AgentEvent } from './client-DcCIhqkm.js';
|
|
2
|
+
export { b as AgentClient, c as AgentClientOptions, d as AgentConnection, e as AgentConnectionOptions, f as AgentError, g as AgentMcpServerReference, h as AgentSession, i as AgentSkillReference, j as AgentToolReference, k as AgentWebSocket, l as AgentWebSocketEventMap, m as AgentWebSocketFactory, B as BrowserAgentClientOptions, n as ClientTokenResult, o as CreateSessionResponse, p as CreateWebSocketTicketResponse, F as FetchLike, L as ListEventsResponse, M as ModelProfileId, R as RequestOptions, S as SessionSnapshot, q as SubmitOptions, r as SubmitTurnResponse, T as TurnStatus, W as WebSocketServerMessage, s as createBrowserClient, t as createClient } from './client-DcCIhqkm.js';
|
|
3
3
|
|
|
4
4
|
/** Defines a portable agent revision without importing runtime implementation code. */
|
|
5
5
|
declare function createAgent(options: CreateAgentOptions): AgentDefinition;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface AgentEventMergeResult {
|
|
8
|
+
accepted: AgentEvent[];
|
|
9
|
+
duplicateIds: number[];
|
|
10
|
+
gap?: {
|
|
11
|
+
expected: number;
|
|
12
|
+
received: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Maintains the single contiguous durable cursor used by replay and live delivery. */
|
|
16
|
+
declare class AgentEventBuffer {
|
|
17
|
+
private ordered;
|
|
18
|
+
private currentCursor;
|
|
19
|
+
get cursor(): number;
|
|
20
|
+
get events(): readonly AgentEvent[];
|
|
21
|
+
merge(incoming: readonly AgentEvent[]): AgentEventMergeResult;
|
|
22
|
+
reset(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { AgentDefinition, AgentEvent, AgentEventBuffer, type AgentEventMergeResult, CreateAgentOptions, createAgent };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AgentClient,
|
|
3
3
|
AgentError,
|
|
4
|
+
AgentEventBuffer,
|
|
4
5
|
AgentSession,
|
|
5
6
|
createBrowserClient,
|
|
6
7
|
createClient
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CC365XBZ.js";
|
|
8
9
|
|
|
9
10
|
// src/agent.ts
|
|
10
11
|
var identifierPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u;
|
|
@@ -41,6 +42,7 @@ function createAgent(options) {
|
|
|
41
42
|
export {
|
|
42
43
|
AgentClient,
|
|
43
44
|
AgentError,
|
|
45
|
+
AgentEventBuffer,
|
|
44
46
|
AgentSession,
|
|
45
47
|
createAgent,
|
|
46
48
|
createBrowserClient,
|
package/dist/react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { CSSProperties, ReactNode, PropsWithChildren } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
3
|
+
import { b as AgentClient, h as AgentSession, S as SessionSnapshot, a as AgentEvent, q as SubmitOptions, r as SubmitTurnResponse, B as BrowserAgentClientOptions } from './client-DcCIhqkm.js';
|
|
4
|
+
export { s as createBrowserClient } from './client-DcCIhqkm.js';
|
|
5
5
|
|
|
6
6
|
interface AgentTheme {
|
|
7
7
|
canvas: string;
|
|
@@ -91,15 +91,17 @@ interface CreateAgentClientOptions {
|
|
|
91
91
|
credentials?: RequestCredentials;
|
|
92
92
|
clientTokenTtlMs?: number;
|
|
93
93
|
refreshSkewMs?: number;
|
|
94
|
+
webSocket?: BrowserAgentClientOptions["webSocket"];
|
|
94
95
|
}
|
|
95
96
|
/** Creates one stable browser client with an in-memory, deduplicated client-token cache. */
|
|
96
|
-
declare function createAgentClient({ endpoint, clientTokenEndpoint, fetch: fetchImplementation, credentials, clientTokenTtlMs, refreshSkewMs, }: CreateAgentClientOptions): AgentClient;
|
|
97
|
+
declare function createAgentClient({ endpoint, clientTokenEndpoint, fetch: fetchImplementation, credentials, clientTokenTtlMs, refreshSkewMs, webSocket, }: CreateAgentClientOptions): AgentClient;
|
|
97
98
|
declare function useAgentTheme(): AgentTheme;
|
|
98
99
|
declare function useAgentCopy(): AgentCopy;
|
|
99
100
|
type AgentMessageStatus = "completed" | "streaming" | "failed" | "cancelled";
|
|
100
101
|
interface AgentChatMessage {
|
|
101
102
|
id: string;
|
|
102
103
|
turnId: string;
|
|
104
|
+
attempt: number;
|
|
103
105
|
role: "user" | "assistant";
|
|
104
106
|
content: string;
|
|
105
107
|
status: AgentMessageStatus;
|
|
@@ -113,6 +115,7 @@ declare function reduceAgentMessages(events: readonly AgentEvent[]): AgentChatMe
|
|
|
113
115
|
declare function reduceAgentToolCalls(events: readonly AgentEvent[]): AgentToolCall[];
|
|
114
116
|
interface SessionState {
|
|
115
117
|
status: "idle" | "loading" | "ready" | "error";
|
|
118
|
+
connection: "idle" | "connecting" | "live" | "reconnecting" | "closed";
|
|
116
119
|
snapshot: SessionSnapshot | null;
|
|
117
120
|
events: AgentEvent[];
|
|
118
121
|
messages: AgentChatMessage[];
|
|
@@ -123,15 +126,24 @@ declare class SessionStore {
|
|
|
123
126
|
readonly session: AgentSession;
|
|
124
127
|
private state;
|
|
125
128
|
private readonly listeners;
|
|
129
|
+
private readonly eventBuffer;
|
|
126
130
|
private controller;
|
|
127
|
-
private
|
|
131
|
+
private connection;
|
|
132
|
+
private reconnectTimer;
|
|
133
|
+
private generation;
|
|
134
|
+
private reconnectAttempt;
|
|
128
135
|
constructor(session: AgentSession);
|
|
129
136
|
subscribe: (listener: () => void) => () => void;
|
|
130
137
|
getSnapshot: () => SessionState;
|
|
131
138
|
getServerSnapshot: () => SessionState;
|
|
132
139
|
private setState;
|
|
133
|
-
private scheduleRefresh;
|
|
134
140
|
refresh(): Promise<void>;
|
|
141
|
+
private replayDurableEvents;
|
|
142
|
+
private openLiveConnection;
|
|
143
|
+
private receiveLiveEvent;
|
|
144
|
+
private publishBuffer;
|
|
145
|
+
private scheduleReconnect;
|
|
146
|
+
private stop;
|
|
135
147
|
dispose(): void;
|
|
136
148
|
}
|
|
137
149
|
interface AgentSessionResult extends SessionState {
|
|
@@ -161,12 +173,17 @@ interface AgentToolCallProps extends StyledProps {
|
|
|
161
173
|
interface AgentToolCall {
|
|
162
174
|
id: string;
|
|
163
175
|
turnId: string;
|
|
176
|
+
operationId?: string;
|
|
177
|
+
callId?: string;
|
|
178
|
+
revision?: string;
|
|
179
|
+
risk?: string;
|
|
164
180
|
name: string;
|
|
165
181
|
label: string;
|
|
166
182
|
summary?: string;
|
|
167
183
|
status: AgentToolCallStatus;
|
|
168
184
|
input?: unknown;
|
|
169
185
|
output?: unknown;
|
|
186
|
+
error?: unknown;
|
|
170
187
|
createdAt: string;
|
|
171
188
|
eventId: number;
|
|
172
189
|
}
|
package/dist/react.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AgentError,
|
|
3
|
+
AgentEventBuffer,
|
|
2
4
|
createBrowserClient
|
|
3
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CC365XBZ.js";
|
|
4
6
|
|
|
5
7
|
// src/react.tsx
|
|
6
8
|
import {
|
|
@@ -182,7 +184,8 @@ function createAgentClient({
|
|
|
182
184
|
fetch: fetchImplementation,
|
|
183
185
|
credentials = "same-origin",
|
|
184
186
|
clientTokenTtlMs,
|
|
185
|
-
refreshSkewMs
|
|
187
|
+
refreshSkewMs,
|
|
188
|
+
webSocket
|
|
186
189
|
}) {
|
|
187
190
|
return createBrowserClient({
|
|
188
191
|
endpoint,
|
|
@@ -206,7 +209,8 @@ function createAgentClient({
|
|
|
206
209
|
},
|
|
207
210
|
...fetchImplementation === void 0 ? {} : { fetch: fetchImplementation },
|
|
208
211
|
...clientTokenTtlMs === void 0 ? {} : { clientTokenTtlMs },
|
|
209
|
-
...refreshSkewMs === void 0 ? {} : { refreshSkewMs }
|
|
212
|
+
...refreshSkewMs === void 0 ? {} : { refreshSkewMs },
|
|
213
|
+
...webSocket === void 0 ? {} : { webSocket }
|
|
210
214
|
});
|
|
211
215
|
}
|
|
212
216
|
function useAgentTheme() {
|
|
@@ -229,11 +233,13 @@ function reduceAgentMessages(events) {
|
|
|
229
233
|
for (const event of events) {
|
|
230
234
|
if (!event.turnId) continue;
|
|
231
235
|
const inputId = `${event.turnId}:user`;
|
|
232
|
-
const
|
|
236
|
+
const itemId = stringField(event.data, "itemId");
|
|
237
|
+
const outputId = `${event.turnId}:assistant:${itemId ?? `attempt-${event.attempt}`}`;
|
|
233
238
|
if (event.type === "message.input") {
|
|
234
239
|
messages.set(inputId, {
|
|
235
240
|
id: inputId,
|
|
236
241
|
turnId: event.turnId,
|
|
242
|
+
attempt: event.attempt,
|
|
237
243
|
role: "user",
|
|
238
244
|
content: stringField(event.data, "content") ?? "",
|
|
239
245
|
status: "completed",
|
|
@@ -246,6 +252,7 @@ function reduceAgentMessages(events) {
|
|
|
246
252
|
messages.set(outputId, {
|
|
247
253
|
id: outputId,
|
|
248
254
|
turnId: event.turnId,
|
|
255
|
+
attempt: event.attempt,
|
|
249
256
|
role: "assistant",
|
|
250
257
|
content: "",
|
|
251
258
|
status: "streaming",
|
|
@@ -270,6 +277,7 @@ function reduceAgentMessages(events) {
|
|
|
270
277
|
messages.set(outputId, {
|
|
271
278
|
id: outputId,
|
|
272
279
|
turnId: event.turnId,
|
|
280
|
+
attempt: event.attempt,
|
|
273
281
|
role: "assistant",
|
|
274
282
|
content: stringField(event.data, "content") ?? current?.content ?? "",
|
|
275
283
|
status: "completed",
|
|
@@ -279,15 +287,20 @@ function reduceAgentMessages(events) {
|
|
|
279
287
|
continue;
|
|
280
288
|
}
|
|
281
289
|
if (event.type === "message.attempt_abandoned") {
|
|
282
|
-
messages
|
|
290
|
+
for (const [id, message] of messages) {
|
|
291
|
+
if (message.turnId === event.turnId && message.role === "assistant" && message.attempt === event.attempt && message.status === "streaming") {
|
|
292
|
+
messages.delete(id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
283
295
|
continue;
|
|
284
296
|
}
|
|
285
297
|
if (event.type === "turn.failed" || event.type === "turn.cancelled") {
|
|
286
|
-
const current = [...messages.values()].filter((message) => message.turnId === event.turnId && message.role === "assistant").at(-1);
|
|
298
|
+
const current = [...messages.values()].filter((message) => message.turnId === event.turnId && message.role === "assistant" && message.attempt === event.attempt).at(-1);
|
|
287
299
|
const terminalId = current?.id ?? `${event.turnId}:assistant:${event.attempt}`;
|
|
288
300
|
messages.set(terminalId, {
|
|
289
301
|
id: terminalId,
|
|
290
302
|
turnId: event.turnId,
|
|
303
|
+
attempt: event.attempt,
|
|
291
304
|
role: "assistant",
|
|
292
305
|
content: current?.content ?? "",
|
|
293
306
|
status: event.type === "turn.failed" ? "failed" : "cancelled",
|
|
@@ -301,30 +314,49 @@ function reduceAgentMessages(events) {
|
|
|
301
314
|
function reduceAgentToolCalls(events) {
|
|
302
315
|
const calls = /* @__PURE__ */ new Map();
|
|
303
316
|
for (const event of events) {
|
|
304
|
-
if (!event.turnId
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
const
|
|
317
|
+
if (!event.turnId) continue;
|
|
318
|
+
const lifecycle = toolLifecycle(event.type);
|
|
319
|
+
if (!lifecycle) continue;
|
|
320
|
+
const operationId = stringField(event.data, "operationId");
|
|
321
|
+
const callId = stringField(event.data, "callId") ?? stringField(event.data, "toolCallId");
|
|
322
|
+
const eventName = stringField(event.data, "toolName") ?? stringField(event.data, "name");
|
|
323
|
+
const revision = stringField(event.data, "toolRevision");
|
|
324
|
+
const risk = stringField(event.data, "risk");
|
|
325
|
+
const id = operationId ?? callId ?? `${event.turnId}:${event.attempt}:${eventName ?? "tool"}`;
|
|
326
|
+
const current = calls.get(id);
|
|
308
327
|
const name = eventName ?? current?.name ?? "tool";
|
|
309
328
|
const summary = stringField(event.data, "summary");
|
|
310
|
-
const status =
|
|
311
|
-
|
|
312
|
-
|
|
329
|
+
const status = lifecycle === "completed" ? "completed" : lifecycle === "failed" ? "failed" : lifecycle === "approval_required" || lifecycle === "proposed" && stringField(event.data, "approval") === "required" ? "approval_required" : lifecycle === "started" ? "running" : "proposed";
|
|
330
|
+
const input = unknownField(event.data, "arguments") ?? unknownField(event.data, "input");
|
|
331
|
+
const output = unknownField(event.data, "output");
|
|
332
|
+
const failure = unknownField(event.data, "error");
|
|
333
|
+
calls.set(id, {
|
|
334
|
+
id,
|
|
313
335
|
turnId: event.turnId,
|
|
336
|
+
...operationId === void 0 ? current?.operationId === void 0 ? {} : { operationId: current.operationId } : { operationId },
|
|
337
|
+
...callId === void 0 ? current?.callId === void 0 ? {} : { callId: current.callId } : { callId },
|
|
338
|
+
...revision === void 0 ? current?.revision === void 0 ? {} : { revision: current.revision } : { revision },
|
|
339
|
+
...risk === void 0 ? current?.risk === void 0 ? {} : { risk: current.risk } : { risk },
|
|
314
340
|
name,
|
|
315
341
|
label: stringField(event.data, "label") ?? current?.label ?? name,
|
|
316
342
|
...summary === void 0 ? current?.summary === void 0 ? {} : { summary: current.summary } : { summary },
|
|
317
343
|
status,
|
|
318
|
-
...
|
|
319
|
-
...
|
|
344
|
+
...input === void 0 ? current?.input === void 0 ? {} : { input: current.input } : { input },
|
|
345
|
+
...output === void 0 ? current?.output === void 0 ? {} : { output: current.output } : { output },
|
|
346
|
+
...failure === void 0 ? current?.error === void 0 ? {} : { error: current.error } : { error: failure },
|
|
320
347
|
createdAt: current?.createdAt ?? event.createdAt,
|
|
321
348
|
eventId: event.id
|
|
322
349
|
});
|
|
323
350
|
}
|
|
324
351
|
return [...calls.values()].sort((left, right) => left.eventId - right.eventId);
|
|
325
352
|
}
|
|
353
|
+
function toolLifecycle(type) {
|
|
354
|
+
const canonical = /^(?:tool|tool\.call)\.(proposed|started|completed|failed|approval_required)$/u.exec(type)?.[1];
|
|
355
|
+
return canonical === "proposed" || canonical === "started" || canonical === "completed" || canonical === "failed" || canonical === "approval_required" ? canonical : null;
|
|
356
|
+
}
|
|
326
357
|
var initialSessionState = {
|
|
327
358
|
status: "idle",
|
|
359
|
+
connection: "idle",
|
|
328
360
|
snapshot: null,
|
|
329
361
|
events: [],
|
|
330
362
|
messages: [],
|
|
@@ -338,17 +370,19 @@ var SessionStore = class {
|
|
|
338
370
|
session;
|
|
339
371
|
state = initialSessionState;
|
|
340
372
|
listeners = /* @__PURE__ */ new Set();
|
|
373
|
+
eventBuffer = new AgentEventBuffer();
|
|
341
374
|
controller = null;
|
|
342
|
-
|
|
375
|
+
connection = null;
|
|
376
|
+
reconnectTimer = null;
|
|
377
|
+
generation = 0;
|
|
378
|
+
reconnectAttempt = 0;
|
|
343
379
|
subscribe = (listener) => {
|
|
344
380
|
this.listeners.add(listener);
|
|
345
381
|
if (this.listeners.size === 1) void this.refresh();
|
|
346
382
|
return () => {
|
|
347
383
|
this.listeners.delete(listener);
|
|
348
384
|
if (this.listeners.size === 0) {
|
|
349
|
-
this.
|
|
350
|
-
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
351
|
-
this.refreshTimer = null;
|
|
385
|
+
this.stop();
|
|
352
386
|
}
|
|
353
387
|
};
|
|
354
388
|
};
|
|
@@ -358,61 +392,222 @@ var SessionStore = class {
|
|
|
358
392
|
this.state = state;
|
|
359
393
|
for (const listener of this.listeners) listener();
|
|
360
394
|
}
|
|
361
|
-
scheduleRefresh() {
|
|
362
|
-
if (this.refreshTimer) clearTimeout(this.refreshTimer);
|
|
363
|
-
const isWorking = this.state.snapshot?.turns.some(
|
|
364
|
-
(turn) => turn.status === "queued" || turn.status === "running"
|
|
365
|
-
);
|
|
366
|
-
this.refreshTimer = isWorking && this.listeners.size > 0 ? setTimeout(() => void this.refresh(), 1e3) : null;
|
|
367
|
-
}
|
|
368
395
|
async refresh() {
|
|
396
|
+
const generation = ++this.generation;
|
|
369
397
|
this.controller?.abort();
|
|
398
|
+
this.connection?.close(1e3, "refreshing");
|
|
399
|
+
this.connection = null;
|
|
400
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
401
|
+
this.reconnectTimer = null;
|
|
370
402
|
this.controller = new AbortController();
|
|
371
403
|
const { signal } = this.controller;
|
|
372
404
|
this.setState({
|
|
373
405
|
...this.state,
|
|
374
406
|
status: this.state.snapshot ? "ready" : "loading",
|
|
407
|
+
connection: "connecting",
|
|
375
408
|
error: null
|
|
376
409
|
});
|
|
377
410
|
try {
|
|
378
411
|
const snapshot = await this.session.get({ signal });
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
412
|
+
if (snapshot.cursor < this.eventBuffer.cursor) this.eventBuffer.reset();
|
|
413
|
+
await this.replayDurableEvents(signal);
|
|
414
|
+
if (signal.aborted || generation !== this.generation) return;
|
|
415
|
+
this.publishBuffer(snapshot, "connecting");
|
|
416
|
+
await this.openLiveConnection(generation, signal);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (signal.aborted || generation !== this.generation) return;
|
|
419
|
+
this.scheduleReconnect(error);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async replayDurableEvents(signal) {
|
|
423
|
+
let cursor = this.eventBuffer.cursor;
|
|
424
|
+
let hasMore = true;
|
|
425
|
+
let pages = 0;
|
|
426
|
+
while (hasMore && pages < 100) {
|
|
427
|
+
const page = await this.session.events(cursor, 100, { signal });
|
|
428
|
+
const merged = this.eventBuffer.merge(page.events);
|
|
429
|
+
if (merged.gap) {
|
|
430
|
+
throw new AgentError(
|
|
431
|
+
`Durable event gap: expected ${merged.gap.expected}, received ${merged.gap.received}`,
|
|
432
|
+
0,
|
|
433
|
+
"event_gap"
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
if (page.hasMore && page.cursor <= cursor) {
|
|
437
|
+
throw new AgentError("Durable replay did not advance", 0, "replay_stalled");
|
|
438
|
+
}
|
|
439
|
+
cursor = this.eventBuffer.cursor;
|
|
440
|
+
hasMore = page.hasMore;
|
|
441
|
+
pages += 1;
|
|
442
|
+
}
|
|
443
|
+
if (hasMore) {
|
|
444
|
+
throw new AgentError(
|
|
445
|
+
"Conversation history exceeds the current 10,000-event UI limit",
|
|
446
|
+
0,
|
|
447
|
+
"history_limit_exceeded"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async openLiveConnection(generation, signal) {
|
|
452
|
+
const connection = await this.session.connect({
|
|
453
|
+
after: this.eventBuffer.cursor,
|
|
454
|
+
signal,
|
|
455
|
+
onEvent: (event) => this.receiveLiveEvent(event, generation),
|
|
456
|
+
onReplayComplete: (cursor) => {
|
|
457
|
+
if (generation !== this.generation || signal.aborted) return;
|
|
458
|
+
if (cursor > this.eventBuffer.cursor) {
|
|
459
|
+
const active = this.connection;
|
|
460
|
+
this.connection = null;
|
|
461
|
+
active?.close(1012, "repairing event gap");
|
|
462
|
+
this.scheduleReconnect(
|
|
463
|
+
new AgentError("Live replay ended ahead of the local cursor", 0, "event_gap"),
|
|
464
|
+
true
|
|
465
|
+
);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.reconnectAttempt = 0;
|
|
469
|
+
this.setState({ ...this.state, connection: "live", error: null });
|
|
470
|
+
},
|
|
471
|
+
onError: (error) => {
|
|
472
|
+
if (generation === this.generation && !signal.aborted) {
|
|
473
|
+
this.setState({ ...this.state, error });
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
onClose: () => {
|
|
477
|
+
if (generation === this.generation && !signal.aborted && this.connection !== null) {
|
|
478
|
+
this.connection = null;
|
|
479
|
+
this.scheduleReconnect(
|
|
480
|
+
new AgentError("Live connection closed", 0, "websocket_closed")
|
|
481
|
+
);
|
|
482
|
+
}
|
|
389
483
|
}
|
|
390
|
-
|
|
484
|
+
});
|
|
485
|
+
if (signal.aborted || generation !== this.generation) {
|
|
486
|
+
connection.close(1e3, "stale connection");
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
this.connection = connection;
|
|
490
|
+
}
|
|
491
|
+
receiveLiveEvent(event, generation) {
|
|
492
|
+
if (generation !== this.generation) return;
|
|
493
|
+
if (event.sessionId !== this.session.id) {
|
|
494
|
+
const active = this.connection;
|
|
495
|
+
this.connection = null;
|
|
496
|
+
active?.close(1008, "wrong session event");
|
|
391
497
|
this.setState({
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
toolCalls: reduceAgentToolCalls(events),
|
|
397
|
-
error: null
|
|
498
|
+
...this.state,
|
|
499
|
+
status: "error",
|
|
500
|
+
connection: "closed",
|
|
501
|
+
error: new AgentError("Runtime returned an event for another session", 0, "invalid_event")
|
|
398
502
|
});
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const merged = this.eventBuffer.merge([event]);
|
|
506
|
+
const snapshot = merged.accepted.reduce(
|
|
507
|
+
(current, accepted) => applyEventToSnapshot(current, accepted),
|
|
508
|
+
this.state.snapshot
|
|
509
|
+
);
|
|
510
|
+
this.publishBuffer(snapshot, this.state.connection);
|
|
511
|
+
if (merged.gap) {
|
|
512
|
+
const active = this.connection;
|
|
513
|
+
this.connection = null;
|
|
514
|
+
active?.close(1012, "repairing event gap");
|
|
515
|
+
this.scheduleReconnect(
|
|
516
|
+
new AgentError(
|
|
517
|
+
`Live event gap: expected ${merged.gap.expected}, received ${merged.gap.received}`,
|
|
518
|
+
0,
|
|
519
|
+
"event_gap"
|
|
520
|
+
),
|
|
521
|
+
true
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
publishBuffer(snapshot, connection) {
|
|
526
|
+
const events = [...this.eventBuffer.events];
|
|
527
|
+
this.setState({
|
|
528
|
+
status: snapshot ? "ready" : this.state.status,
|
|
529
|
+
connection,
|
|
530
|
+
snapshot,
|
|
531
|
+
events,
|
|
532
|
+
messages: reduceAgentMessages(events),
|
|
533
|
+
toolCalls: reduceAgentToolCalls(events),
|
|
534
|
+
error: null
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
scheduleReconnect(error, immediate = false) {
|
|
538
|
+
if (this.listeners.size === 0) return;
|
|
539
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
540
|
+
if (isPermanentConnectionError(normalized)) {
|
|
402
541
|
this.setState({
|
|
403
542
|
...this.state,
|
|
404
543
|
status: "error",
|
|
405
|
-
|
|
544
|
+
connection: "closed",
|
|
545
|
+
error: normalized
|
|
406
546
|
});
|
|
407
|
-
|
|
547
|
+
return;
|
|
408
548
|
}
|
|
549
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
550
|
+
const exponent = Math.min(this.reconnectAttempt, 5);
|
|
551
|
+
const delay = immediate ? 0 : Math.round(Math.min(1e4, 400 * 2 ** exponent) * (0.75 + Math.random() * 0.5));
|
|
552
|
+
this.reconnectAttempt += 1;
|
|
553
|
+
this.setState({
|
|
554
|
+
...this.state,
|
|
555
|
+
status: this.state.snapshot ? "ready" : "error",
|
|
556
|
+
connection: "reconnecting",
|
|
557
|
+
error: this.state.snapshot ? null : normalized
|
|
558
|
+
});
|
|
559
|
+
this.reconnectTimer = setTimeout(() => {
|
|
560
|
+
this.reconnectTimer = null;
|
|
561
|
+
void this.refresh();
|
|
562
|
+
}, delay);
|
|
409
563
|
}
|
|
410
|
-
|
|
564
|
+
stop() {
|
|
565
|
+
this.generation += 1;
|
|
411
566
|
this.controller?.abort();
|
|
412
|
-
|
|
413
|
-
this.
|
|
567
|
+
this.controller = null;
|
|
568
|
+
this.connection?.close(1e3, "no subscribers");
|
|
569
|
+
this.connection = null;
|
|
570
|
+
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
571
|
+
this.reconnectTimer = null;
|
|
572
|
+
}
|
|
573
|
+
dispose() {
|
|
574
|
+
this.stop();
|
|
414
575
|
}
|
|
415
576
|
};
|
|
577
|
+
function isPermanentConnectionError(error) {
|
|
578
|
+
return error instanceof AgentError && (error.status === 401 || error.status === 403 || error.status === 404);
|
|
579
|
+
}
|
|
580
|
+
function applyEventToSnapshot(snapshot, event) {
|
|
581
|
+
if (!snapshot) return null;
|
|
582
|
+
const status = turnStatusForEvent(event.type);
|
|
583
|
+
if (!status || !event.turnId) {
|
|
584
|
+
return event.id > snapshot.cursor ? { ...snapshot, cursor: event.id } : snapshot;
|
|
585
|
+
}
|
|
586
|
+
const existing = snapshot.turns.find((turn2) => turn2.id === event.turnId);
|
|
587
|
+
const turn = {
|
|
588
|
+
id: event.turnId,
|
|
589
|
+
status,
|
|
590
|
+
attempt: event.attempt,
|
|
591
|
+
createdAt: existing?.createdAt ?? event.createdAt,
|
|
592
|
+
updatedAt: event.createdAt
|
|
593
|
+
};
|
|
594
|
+
return {
|
|
595
|
+
...snapshot,
|
|
596
|
+
updatedAt: event.createdAt,
|
|
597
|
+
cursor: Math.max(snapshot.cursor, event.id),
|
|
598
|
+
turns: existing ? snapshot.turns.map(
|
|
599
|
+
(candidate) => candidate.id === event.turnId ? turn : candidate
|
|
600
|
+
) : [...snapshot.turns, turn]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function turnStatusForEvent(type) {
|
|
604
|
+
if (type === "turn.queued") return "queued";
|
|
605
|
+
if (type === "turn.started") return "running";
|
|
606
|
+
if (type === "turn.completed") return "completed";
|
|
607
|
+
if (type === "turn.failed") return "failed";
|
|
608
|
+
if (type === "turn.cancelled") return "cancelled";
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
416
611
|
function useAgentSession(sessionId) {
|
|
417
612
|
const context = useAgentContext();
|
|
418
613
|
const store = useMemo(() => {
|
|
@@ -712,7 +907,7 @@ function AgentToolCall({ toolCall, className, style, theme, copy }) {
|
|
|
712
907
|
const labels = { ...context.copy, ...copy };
|
|
713
908
|
const [expanded, setExpanded] = useState(false);
|
|
714
909
|
const [hovered, setHovered] = useState(false);
|
|
715
|
-
const hasDetails = toolCall.input !== void 0 || toolCall.output !== void 0;
|
|
910
|
+
const hasDetails = toolCall.input !== void 0 || toolCall.output !== void 0 || toolCall.error !== void 0;
|
|
716
911
|
const title = toolCall.label === toolCall.name ? humanizeToolName(toolCall.name) : toolCall.label;
|
|
717
912
|
const summary = toolCall.summary ?? deriveToolSummary(toolCall.input);
|
|
718
913
|
const statusLabel = toolCall.status === "completed" ? labels.toolCompleted : toolCall.status === "failed" ? labels.toolFailed : toolCall.status === "approval_required" ? labels.toolApprovalRequired : labels.toolRunning;
|
|
@@ -793,6 +988,10 @@ function AgentToolCall({ toolCall, className, style, theme, copy }) {
|
|
|
793
988
|
toolCall.output !== void 0 ? /* @__PURE__ */ jsxs("div", { children: [
|
|
794
989
|
/* @__PURE__ */ jsx("div", { style: { marginBottom: 2, color: colors.inkTertiary, fontFamily: colors.fontFamily }, children: "Output" }),
|
|
795
990
|
/* @__PURE__ */ jsx("pre", { style: { margin: 0, whiteSpace: "pre-wrap", overflowWrap: "anywhere", font: "inherit" }, children: formatToolPayload(toolCall.output) })
|
|
991
|
+
] }) : null,
|
|
992
|
+
toolCall.error !== void 0 ? /* @__PURE__ */ jsxs("div", { children: [
|
|
993
|
+
/* @__PURE__ */ jsx("div", { style: { marginBottom: 2, color: colors.statusBad, fontFamily: colors.fontFamily }, children: "Error" }),
|
|
994
|
+
/* @__PURE__ */ jsx("pre", { style: { margin: 0, whiteSpace: "pre-wrap", overflowWrap: "anywhere", font: "inherit" }, children: formatToolPayload(toolCall.error) })
|
|
796
995
|
] }) : null
|
|
797
996
|
]
|
|
798
997
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codespring-app/use-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Server and React SDKs for the CodeSpring Agents runtime",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
18
|
"docs/screenshots",
|
|
19
|
-
"README.md"
|
|
19
|
+
"README.md",
|
|
20
|
+
"CHANGELOG.md"
|
|
20
21
|
],
|
|
21
22
|
"exports": {
|
|
22
23
|
".": {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"build": "tsup src/index.ts src/react.tsx --format esm --dts --clean --out-dir dist",
|
|
34
35
|
"showcase": "vite examples/showcase --host 127.0.0.1",
|
|
35
36
|
"showcase:build": "vite build examples/showcase --outDir ../../dist-showcase --emptyOutDir",
|
|
37
|
+
"example:live": "bun run examples/live-session-replay.ts",
|
|
36
38
|
"test": "bun test",
|
|
37
39
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
38
40
|
"check": "bun run typecheck && bun run test && bun run build"
|