@flowy-agent-store/client 0.1.0-beta.2
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/LICENSE +201 -0
- package/README.md +5 -0
- package/dist/index.cjs +1216 -0
- package/dist/index.d.cts +501 -0
- package/dist/index.d.mts +501 -0
- package/dist/index.mjs +1197 -0
- package/package.json +32 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _flowy_agent_store_protocol = require("@flowy-agent-store/protocol");
|
|
3
|
+
//#region src/transport.ts
|
|
4
|
+
/**
|
|
5
|
+
* JSON-RPC 2.0 transport over WebSocket for the App Server protocol.
|
|
6
|
+
*
|
|
7
|
+
* Works in browsers and in Node 22+/Bun (global `WebSocket`). The browser
|
|
8
|
+
* WebSocket API cannot set custom headers, so an optional bearer token is
|
|
9
|
+
* appended as a query parameter (`?token=...`) when provided; HTTP calls in
|
|
10
|
+
* the SDK use the `Authorization` header instead.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* True for loopback-only URLs (`127.0.0.0/8`, `::1`, `localhost`).
|
|
14
|
+
* SDK spawn contract (docs/agent-store/12-sdk-packaging.md P0-4): an SDK
|
|
15
|
+
* must only speak the local-trusted mode over loopback; anything else is
|
|
16
|
+
* rejected before connecting.
|
|
17
|
+
*/
|
|
18
|
+
function isLoopbackUrl(raw) {
|
|
19
|
+
try {
|
|
20
|
+
const host = new URL(raw).hostname.toLowerCase();
|
|
21
|
+
if (host === "localhost" || host === "::1") return true;
|
|
22
|
+
if (host.startsWith("[") && host.endsWith("]")) return host === "[::1]";
|
|
23
|
+
const parts = host.split(".").map(Number);
|
|
24
|
+
return parts.length === 4 && parts[0] === 127 && parts.every((n) => Number.isInteger(n) && n >= 0 && n <= 255);
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var WebSocketTransport = class {
|
|
30
|
+
socket = null;
|
|
31
|
+
nextId = 1;
|
|
32
|
+
pending = /* @__PURE__ */ new Map();
|
|
33
|
+
listeners = /* @__PURE__ */ new Set();
|
|
34
|
+
connectResolvers = [];
|
|
35
|
+
connectRejecters = [];
|
|
36
|
+
closedByCaller = false;
|
|
37
|
+
requestTimeoutMs;
|
|
38
|
+
url;
|
|
39
|
+
token;
|
|
40
|
+
constructor(url, options = {}) {
|
|
41
|
+
this.url = url;
|
|
42
|
+
this.token = options.token;
|
|
43
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e4;
|
|
44
|
+
}
|
|
45
|
+
get connected() {
|
|
46
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
47
|
+
}
|
|
48
|
+
connect() {
|
|
49
|
+
if (this.connected) return Promise.resolve();
|
|
50
|
+
this.closedByCaller = false;
|
|
51
|
+
const target = this.token ? `${this.url}${this.url.includes("?") ? "&" : "?"}token=${encodeURIComponent(this.token)}` : this.url;
|
|
52
|
+
const socket = new WebSocket(target);
|
|
53
|
+
this.socket = socket;
|
|
54
|
+
socket.onopen = () => {
|
|
55
|
+
for (const resolve of this.connectResolvers.splice(0)) resolve();
|
|
56
|
+
};
|
|
57
|
+
socket.onmessage = (event) => {
|
|
58
|
+
this.handleMessage(event.data);
|
|
59
|
+
};
|
|
60
|
+
socket.onclose = (event) => {
|
|
61
|
+
this.failAllPending(new _flowy_agent_store_protocol.TransportError("close", `app-server connection closed (code ${event.code})`, { retryable: !this.closedByCaller }));
|
|
62
|
+
this.socket = null;
|
|
63
|
+
for (const reject of this.connectRejecters.splice(0)) reject(new _flowy_agent_store_protocol.TransportError("connect", "app-server connection closed before opening"));
|
|
64
|
+
};
|
|
65
|
+
socket.onerror = () => {
|
|
66
|
+
this.socket?.close();
|
|
67
|
+
};
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
this.connectResolvers.push(resolve);
|
|
70
|
+
this.connectRejecters.push(reject);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
request(method, params) {
|
|
74
|
+
const socket = this.requireSocket();
|
|
75
|
+
const id = this.nextId++;
|
|
76
|
+
const payload = {
|
|
77
|
+
jsonrpc: "2.0",
|
|
78
|
+
id,
|
|
79
|
+
method,
|
|
80
|
+
params
|
|
81
|
+
};
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timer = setTimeout(() => {
|
|
84
|
+
this.pending.delete(id);
|
|
85
|
+
reject(new _flowy_agent_store_protocol.RequestTimeoutError(method, this.requestTimeoutMs));
|
|
86
|
+
}, this.requestTimeoutMs);
|
|
87
|
+
this.pending.set(id, {
|
|
88
|
+
resolve,
|
|
89
|
+
reject,
|
|
90
|
+
timer,
|
|
91
|
+
method
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
socket.send(JSON.stringify(payload));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.pending.delete(id);
|
|
98
|
+
reject(new _flowy_agent_store_protocol.TransportError("send", `failed to send ${method}`, { cause: error }));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
notify(method, params) {
|
|
103
|
+
const socket = this.requireSocket();
|
|
104
|
+
const payload = {
|
|
105
|
+
jsonrpc: "2.0",
|
|
106
|
+
method,
|
|
107
|
+
params
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
socket.send(JSON.stringify(payload));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new _flowy_agent_store_protocol.TransportError("send", `failed to send notification ${method}`, { cause: error });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
onNotification(listener) {
|
|
116
|
+
this.listeners.add(listener);
|
|
117
|
+
return () => {
|
|
118
|
+
this.listeners.delete(listener);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
close() {
|
|
122
|
+
this.closedByCaller = true;
|
|
123
|
+
this.socket?.close();
|
|
124
|
+
this.socket = null;
|
|
125
|
+
for (const [, pending] of this.pending) {
|
|
126
|
+
clearTimeout(pending.timer);
|
|
127
|
+
pending.reject(new _flowy_agent_store_protocol.TransportError("close", "app-server transport closed by caller"));
|
|
128
|
+
}
|
|
129
|
+
this.pending.clear();
|
|
130
|
+
}
|
|
131
|
+
requireSocket() {
|
|
132
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) throw new _flowy_agent_store_protocol.TransportError("send", "app-server transport is not connected", { retryable: true });
|
|
133
|
+
return this.socket;
|
|
134
|
+
}
|
|
135
|
+
handleMessage(raw) {
|
|
136
|
+
let message;
|
|
137
|
+
try {
|
|
138
|
+
message = JSON.parse(String(raw));
|
|
139
|
+
} catch {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if ("id" in message && message.id !== void 0) {
|
|
143
|
+
const response = message;
|
|
144
|
+
const pending = response.id === null ? void 0 : this.pending.get(Number(response.id));
|
|
145
|
+
if (!pending) return;
|
|
146
|
+
clearTimeout(pending.timer);
|
|
147
|
+
this.pending.delete(Number(response.id));
|
|
148
|
+
if (response.error) pending.reject(new _flowy_agent_store_protocol.AppServerError(response.error));
|
|
149
|
+
else pending.resolve(response.result);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if ("method" in message) {
|
|
153
|
+
const notification = message;
|
|
154
|
+
for (const listener of [...this.listeners]) try {
|
|
155
|
+
listener(notification);
|
|
156
|
+
} catch {}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
failAllPending(error) {
|
|
161
|
+
for (const [, pending] of this.pending) {
|
|
162
|
+
clearTimeout(pending.timer);
|
|
163
|
+
pending.reject(error);
|
|
164
|
+
}
|
|
165
|
+
this.pending.clear();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/agents.ts
|
|
170
|
+
var AgentClient = class {
|
|
171
|
+
transport;
|
|
172
|
+
constructor(transport) {
|
|
173
|
+
this.transport = transport;
|
|
174
|
+
}
|
|
175
|
+
/** List Agent Store AgentDefinitions (public summaries only). */
|
|
176
|
+
list() {
|
|
177
|
+
return this.transport.request("agent/list", {});
|
|
178
|
+
}
|
|
179
|
+
/** Fetch one AgentDefinition; raw prompts never cross the wire. */
|
|
180
|
+
get(agentId) {
|
|
181
|
+
return this.transport.request("agent/get", { agent_id: agentId });
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/conversations.ts
|
|
186
|
+
var ConversationClient = class {
|
|
187
|
+
transport;
|
|
188
|
+
constructor(transport) {
|
|
189
|
+
this.transport = transport;
|
|
190
|
+
}
|
|
191
|
+
create(input) {
|
|
192
|
+
const model = input.model ? {
|
|
193
|
+
provider_id: input.model.provider_id,
|
|
194
|
+
model: input.model.model
|
|
195
|
+
} : void 0;
|
|
196
|
+
return this.transport.request("conversation/create", {
|
|
197
|
+
name: input.name,
|
|
198
|
+
model,
|
|
199
|
+
workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
|
|
200
|
+
reasoning_effort: input.reasoningEffort || void 0
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
update(conversationId, input) {
|
|
204
|
+
return this.transport.request("conversation/update", {
|
|
205
|
+
conversation_id: conversationId,
|
|
206
|
+
name: input.name,
|
|
207
|
+
model: input.model ? {
|
|
208
|
+
provider_id: input.model.provider_id,
|
|
209
|
+
model: input.model.model
|
|
210
|
+
} : void 0,
|
|
211
|
+
reasoning_effort: input.reasoningEffort || void 0
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
modelOptions() {
|
|
215
|
+
return this.transport.request("conversation/model-options", {});
|
|
216
|
+
}
|
|
217
|
+
list(limit = 100) {
|
|
218
|
+
return this.transport.request("conversation/list", { limit });
|
|
219
|
+
}
|
|
220
|
+
get(conversationId) {
|
|
221
|
+
return this.transport.request("conversation/get", { conversation_id: conversationId });
|
|
222
|
+
}
|
|
223
|
+
messages(query) {
|
|
224
|
+
return this.transport.request("conversation/messages", {
|
|
225
|
+
conversation_id: query.conversationId,
|
|
226
|
+
page: query.page,
|
|
227
|
+
page_size: query.pageSize,
|
|
228
|
+
cursor: query.cursor
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
send(conversationId, content, idempotencyKey) {
|
|
232
|
+
return this.transport.request("conversation/send", {
|
|
233
|
+
conversation_id: conversationId,
|
|
234
|
+
content,
|
|
235
|
+
idempotency_key: idempotencyKey
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
cancel(conversationId) {
|
|
239
|
+
return this.transport.request("conversation/cancel", { conversation_id: conversationId });
|
|
240
|
+
}
|
|
241
|
+
delete(conversationId) {
|
|
242
|
+
return this.transport.request("conversation/delete", { conversation_id: conversationId });
|
|
243
|
+
}
|
|
244
|
+
async follow(conversationId) {
|
|
245
|
+
await this.transport.request("conversation/subscribe", { conversation_id: conversationId });
|
|
246
|
+
return new ConversationSubscription(this.transport, conversationId);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var ConversationSubscription = class {
|
|
250
|
+
transport;
|
|
251
|
+
conversationId;
|
|
252
|
+
events = /* @__PURE__ */ new Set();
|
|
253
|
+
resyncs = /* @__PURE__ */ new Set();
|
|
254
|
+
closed = false;
|
|
255
|
+
lastSeenSequence = 0;
|
|
256
|
+
removeListener;
|
|
257
|
+
constructor(transport, conversationId) {
|
|
258
|
+
this.transport = transport;
|
|
259
|
+
this.conversationId = conversationId;
|
|
260
|
+
this.removeListener = transport.onNotification((notification) => this.dispatch(notification));
|
|
261
|
+
}
|
|
262
|
+
get lastSequence() {
|
|
263
|
+
return this.lastSeenSequence;
|
|
264
|
+
}
|
|
265
|
+
onEvent(listener) {
|
|
266
|
+
this.events.add(listener);
|
|
267
|
+
return () => this.events.delete(listener);
|
|
268
|
+
}
|
|
269
|
+
onResync(listener) {
|
|
270
|
+
this.resyncs.add(listener);
|
|
271
|
+
return () => this.resyncs.delete(listener);
|
|
272
|
+
}
|
|
273
|
+
async close() {
|
|
274
|
+
if (this.closed) return;
|
|
275
|
+
this.closed = true;
|
|
276
|
+
this.removeListener?.();
|
|
277
|
+
this.removeListener = null;
|
|
278
|
+
this.events.clear();
|
|
279
|
+
this.resyncs.clear();
|
|
280
|
+
try {
|
|
281
|
+
await this.transport.request("conversation/unsubscribe", { conversation_id: this.conversationId });
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
dispatch(notification) {
|
|
285
|
+
if (notification.method === "conversation/event") {
|
|
286
|
+
const event = notification.params;
|
|
287
|
+
if (event.conversation_id !== this.conversationId || event.sequence <= this.lastSeenSequence) return;
|
|
288
|
+
this.lastSeenSequence = event.sequence;
|
|
289
|
+
for (const listener of [...this.events]) try {
|
|
290
|
+
listener(event);
|
|
291
|
+
} catch {}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (notification.method === "conversation/resync-required") {
|
|
295
|
+
if (!notification.params.conversation_ids.includes(this.conversationId)) return;
|
|
296
|
+
for (const listener of [...this.resyncs]) try {
|
|
297
|
+
listener(notification.params.reason);
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/connectors.ts
|
|
304
|
+
var ConnectorClient = class {
|
|
305
|
+
transport;
|
|
306
|
+
constructor(transport) {
|
|
307
|
+
this.transport = transport;
|
|
308
|
+
}
|
|
309
|
+
/** List the Agent Store Connector catalog (public summaries only). */
|
|
310
|
+
list() {
|
|
311
|
+
return this.transport.request("connector/list", {});
|
|
312
|
+
}
|
|
313
|
+
/** Fetch one Connector detail: namespaced tools + auth state. */
|
|
314
|
+
get(connectorId) {
|
|
315
|
+
return this.transport.request("connector/get", { connector_id: connectorId });
|
|
316
|
+
}
|
|
317
|
+
/** Combined auth × probe status. `connected` only when auth is ready AND the last probe succeeded. */
|
|
318
|
+
status(connectorId) {
|
|
319
|
+
return this.transport.request("connector/status", { connector_id: connectorId });
|
|
320
|
+
}
|
|
321
|
+
/** Run a connection probe; the server persists the result. */
|
|
322
|
+
test(connectorId) {
|
|
323
|
+
return this.transport.request("connector/test", { connector_id: connectorId });
|
|
324
|
+
}
|
|
325
|
+
/** Check the current OAuth state for a connector. */
|
|
326
|
+
authStatus(connectorId) {
|
|
327
|
+
return this.transport.request("connector/auth/status", { connector_id: connectorId });
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Kick off the OAuth browser flow on the trusted host. Returns the start
|
|
331
|
+
* acknowledgement immediately; poll `authStatus` until `authenticated`.
|
|
332
|
+
*/
|
|
333
|
+
authStart(connectorId) {
|
|
334
|
+
return this.transport.request("connector/auth/start", { connector_id: connectorId });
|
|
335
|
+
}
|
|
336
|
+
/** Revoke the OAuth token for a connector. */
|
|
337
|
+
logout(connectorId) {
|
|
338
|
+
return this.transport.request("connector/auth/logout", { connector_id: connectorId }).then(() => void 0);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/runs.ts
|
|
343
|
+
/**
|
|
344
|
+
* Run client: agent/run, run/get, run/result, run/events, run/cancel plus the
|
|
345
|
+
* realtime `follow` subscription over the WebSocket transport.
|
|
346
|
+
*/
|
|
347
|
+
var RunClient = class {
|
|
348
|
+
transport;
|
|
349
|
+
constructor(transport) {
|
|
350
|
+
this.transport = transport;
|
|
351
|
+
}
|
|
352
|
+
/** Start an Agent run and return its asynchronous receipt. */
|
|
353
|
+
agent(input) {
|
|
354
|
+
return this.transport.request("agent/run", toWire(input));
|
|
355
|
+
}
|
|
356
|
+
/** Read the authoritative run state. */
|
|
357
|
+
get(runId) {
|
|
358
|
+
return this.transport.request("run/get", { run_id: runId });
|
|
359
|
+
}
|
|
360
|
+
/** Read the terminal result (fails until the run reaches a terminal state). */
|
|
361
|
+
result(runId) {
|
|
362
|
+
return this.transport.request("run/result", { run_id: runId });
|
|
363
|
+
}
|
|
364
|
+
/** Replay persisted events after a cursor. */
|
|
365
|
+
events(query) {
|
|
366
|
+
return this.transport.request("run/events", {
|
|
367
|
+
run_id: query.runId,
|
|
368
|
+
after_sequence: query.afterSequence,
|
|
369
|
+
limit: query.limit
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/** Request cancellation; the terminal state is confirmed by events/run/get. */
|
|
373
|
+
cancel(input) {
|
|
374
|
+
return this.transport.request("run/cancel", {
|
|
375
|
+
run_id: input.runId,
|
|
376
|
+
expected_version: input.expectedVersion,
|
|
377
|
+
command_id: input.commandId,
|
|
378
|
+
idempotency_key: input.idempotencyKey
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Inject steering text into the currently active step of a running Agent
|
|
383
|
+
* (runtime durable conversation effect; server resolves the opaque step).
|
|
384
|
+
*/
|
|
385
|
+
steer(input) {
|
|
386
|
+
return this.transport.request("run/steer", {
|
|
387
|
+
run_id: input.runId,
|
|
388
|
+
text: input.text,
|
|
389
|
+
expected_version: input.expectedVersion,
|
|
390
|
+
command_id: input.commandId,
|
|
391
|
+
idempotency_key: input.idempotencyKey
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
/** Subscribe to best-effort realtime events for one run. */
|
|
395
|
+
async follow(runId, options) {
|
|
396
|
+
const params = { run_id: runId };
|
|
397
|
+
await this.transport.request("run/subscribe", params);
|
|
398
|
+
const subscription = new EventSubscription(this.transport, runId, (afterSequence) => this.events({
|
|
399
|
+
runId,
|
|
400
|
+
afterSequence
|
|
401
|
+
}), options);
|
|
402
|
+
if (options?.catchUp !== false) await subscription.catchUp().catch(() => void 0);
|
|
403
|
+
return subscription;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
/** Upper bound for the silent catch-up page (cursor baseline, not delivery). */
|
|
407
|
+
const CATCH_UP_LIMIT = 500;
|
|
408
|
+
/** Prune the dedup set past this size (sequences are per-run monotonic). */
|
|
409
|
+
const SEEN_SET_PRUNE_AT = 2e3;
|
|
410
|
+
const SEEN_SET_KEEP_BELOW_MAX = 500;
|
|
411
|
+
var EventSubscription = class {
|
|
412
|
+
transport;
|
|
413
|
+
runId;
|
|
414
|
+
fetchAfter;
|
|
415
|
+
eventListeners = /* @__PURE__ */ new Set();
|
|
416
|
+
resyncListeners = /* @__PURE__ */ new Set();
|
|
417
|
+
errorListeners = /* @__PURE__ */ new Set();
|
|
418
|
+
closed = false;
|
|
419
|
+
unsubscribeNotification = null;
|
|
420
|
+
lastSeenSequence = 0;
|
|
421
|
+
seenSequences = /* @__PURE__ */ new Set();
|
|
422
|
+
resyncInFlight = null;
|
|
423
|
+
autoResync;
|
|
424
|
+
constructor(transport, runId, fetchAfter, options) {
|
|
425
|
+
this.transport = transport;
|
|
426
|
+
this.runId = runId;
|
|
427
|
+
this.fetchAfter = fetchAfter;
|
|
428
|
+
this.autoResync = options?.autoResync !== false;
|
|
429
|
+
this.unsubscribeNotification = transport.onNotification((notification) => {
|
|
430
|
+
this.dispatch(notification);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
onEvent(listener) {
|
|
434
|
+
this.eventListeners.add(listener);
|
|
435
|
+
return () => {
|
|
436
|
+
this.eventListeners.delete(listener);
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
onResync(listener) {
|
|
440
|
+
this.resyncListeners.add(listener);
|
|
441
|
+
return () => {
|
|
442
|
+
this.resyncListeners.delete(listener);
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
onError(listener) {
|
|
446
|
+
this.errorListeners.add(listener);
|
|
447
|
+
return () => {
|
|
448
|
+
this.errorListeners.delete(listener);
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
get lastSequence() {
|
|
452
|
+
return this.lastSeenSequence;
|
|
453
|
+
}
|
|
454
|
+
async close() {
|
|
455
|
+
if (this.closed) return;
|
|
456
|
+
this.closed = true;
|
|
457
|
+
this.unsubscribeNotification?.();
|
|
458
|
+
this.eventListeners.clear();
|
|
459
|
+
this.resyncListeners.clear();
|
|
460
|
+
this.errorListeners.clear();
|
|
461
|
+
try {
|
|
462
|
+
await this.transport.request("run/unsubscribe", { run_id: this.runId });
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
dispatch(notification) {
|
|
466
|
+
if (this.closed) return;
|
|
467
|
+
if (notification.method === "event") {
|
|
468
|
+
const event = notification.params;
|
|
469
|
+
if (event.run_id !== this.runId) return;
|
|
470
|
+
if (!Number.isFinite(event.sequence) || this.seenSequences.has(event.sequence)) return;
|
|
471
|
+
this.markSeen(event.sequence);
|
|
472
|
+
this.emitEvent(event);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (notification.method === "run/resync-required") {
|
|
476
|
+
const params = notification.params;
|
|
477
|
+
if (params.run_ids.length > 0 && !params.run_ids.includes(this.runId)) return;
|
|
478
|
+
for (const listener of [...this.resyncListeners]) try {
|
|
479
|
+
listener(params);
|
|
480
|
+
} catch {}
|
|
481
|
+
if (this.autoResync && this.fetchAfter) this.resync().catch((error) => this.emitError(error));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Silent cursor baseline: mark already-persisted events seen WITHOUT
|
|
487
|
+
* dispatching, so the first resync never replays history as "new".
|
|
488
|
+
* For explicit history use `RunClient.events({ afterSequence: 0 })`.
|
|
489
|
+
*/
|
|
490
|
+
async catchUp() {
|
|
491
|
+
if (this.closed || !this.fetchAfter) return;
|
|
492
|
+
const history = await this.fetchAfter(0);
|
|
493
|
+
const page = Array.isArray(history) ? history.slice(0, CATCH_UP_LIMIT) : [];
|
|
494
|
+
for (const event of page) if (event.run_id === this.runId && Number.isFinite(event.sequence)) this.markSeen(event.sequence);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
*追平 missed events via `run/events` after the cursor. Returned (and
|
|
498
|
+
* dispatched) in sequence order, deduplicated against live delivery.
|
|
499
|
+
* Concurrent calls share one flight.
|
|
500
|
+
*/
|
|
501
|
+
async resync() {
|
|
502
|
+
if (this.closed) return [];
|
|
503
|
+
if (this.resyncInFlight) return this.resyncInFlight;
|
|
504
|
+
if (!this.fetchAfter) return [];
|
|
505
|
+
const fetchAfter = this.fetchAfter;
|
|
506
|
+
const task = (async () => {
|
|
507
|
+
const missed = await fetchAfter(this.lastSeenSequence);
|
|
508
|
+
const fresh = (Array.isArray(missed) ? [...missed] : []).filter((event) => event.run_id === this.runId && Number.isFinite(event.sequence)).sort((left, right) => left.sequence - right.sequence).filter((event) => {
|
|
509
|
+
if (this.seenSequences.has(event.sequence)) return false;
|
|
510
|
+
this.markSeen(event.sequence);
|
|
511
|
+
return true;
|
|
512
|
+
});
|
|
513
|
+
for (const event of fresh) this.emitEvent(event);
|
|
514
|
+
return fresh;
|
|
515
|
+
})();
|
|
516
|
+
this.resyncInFlight = task;
|
|
517
|
+
try {
|
|
518
|
+
return await task;
|
|
519
|
+
} finally {
|
|
520
|
+
if (this.resyncInFlight === task) this.resyncInFlight = null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
markSeen(sequence) {
|
|
524
|
+
this.seenSequences.add(sequence);
|
|
525
|
+
if (sequence > this.lastSeenSequence) this.lastSeenSequence = sequence;
|
|
526
|
+
if (this.seenSequences.size > SEEN_SET_PRUNE_AT) {
|
|
527
|
+
const floor = this.lastSeenSequence - SEEN_SET_KEEP_BELOW_MAX;
|
|
528
|
+
for (const seen of this.seenSequences) if (seen < floor) this.seenSequences.delete(seen);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
emitEvent(event) {
|
|
532
|
+
for (const listener of [...this.eventListeners]) try {
|
|
533
|
+
listener(event);
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
emitError(error) {
|
|
537
|
+
for (const listener of [...this.errorListeners]) try {
|
|
538
|
+
listener(error);
|
|
539
|
+
} catch {}
|
|
540
|
+
}
|
|
541
|
+
/** Forward transport-level errors into subscription error listeners. */
|
|
542
|
+
deliverError(error) {
|
|
543
|
+
if (error instanceof _flowy_agent_store_protocol.TransportError) for (const listener of [...this.errorListeners]) try {
|
|
544
|
+
listener(error);
|
|
545
|
+
} catch {}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
function toWire(input) {
|
|
549
|
+
return {
|
|
550
|
+
agent_id: input.agentId,
|
|
551
|
+
agent_version: input.agentVersion,
|
|
552
|
+
goal: input.goal,
|
|
553
|
+
input: input.input,
|
|
554
|
+
workspace: input.workspaceId ? { id: input.workspaceId } : void 0,
|
|
555
|
+
steps: input.steps,
|
|
556
|
+
command_id: input.commandId,
|
|
557
|
+
idempotency_key: input.idempotencyKey,
|
|
558
|
+
mentions: input.mentions
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/skills.ts
|
|
563
|
+
var SkillClient = class {
|
|
564
|
+
transport;
|
|
565
|
+
constructor(transport) {
|
|
566
|
+
this.transport = transport;
|
|
567
|
+
}
|
|
568
|
+
/** List the Agent Store Skill catalog (public summaries only). */
|
|
569
|
+
list() {
|
|
570
|
+
return this.transport.request("skill/list", {});
|
|
571
|
+
}
|
|
572
|
+
/** Fetch one Skill detail; instructions are a bounded public summary. */
|
|
573
|
+
get(skillId) {
|
|
574
|
+
return this.transport.request("skill/get", { skill_id: skillId });
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/teams.ts
|
|
579
|
+
var TeamClient = class {
|
|
580
|
+
transport;
|
|
581
|
+
constructor(transport) {
|
|
582
|
+
this.transport = transport;
|
|
583
|
+
}
|
|
584
|
+
/** List AgentTeamDefinitions (fixed-member rosters). */
|
|
585
|
+
list() {
|
|
586
|
+
return this.transport.request("team/list", {});
|
|
587
|
+
}
|
|
588
|
+
/** Fetch one TeamDefinition; planning-context bodies are never returned. */
|
|
589
|
+
get(teamId) {
|
|
590
|
+
return this.transport.request("team/get", { team_id: teamId });
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/models.ts
|
|
595
|
+
var ModelClient = class {
|
|
596
|
+
transport;
|
|
597
|
+
constructor(transport) {
|
|
598
|
+
this.transport = transport;
|
|
599
|
+
}
|
|
600
|
+
/** Enabled providers' models; `is_default` marks the `agent/run` fallback. */
|
|
601
|
+
async list() {
|
|
602
|
+
return (await this.transport.request("models/list", {})).items;
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/workspaces.ts
|
|
607
|
+
var WorkspaceClient = class {
|
|
608
|
+
transport;
|
|
609
|
+
constructor(transport) {
|
|
610
|
+
this.transport = transport;
|
|
611
|
+
}
|
|
612
|
+
list() {
|
|
613
|
+
return this.transport.request("workspace/list", {});
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Validate + register (idempotently per canonical root) an absolute local
|
|
617
|
+
* directory chosen by the owner. The server canonicalizes the path and
|
|
618
|
+
* rejects links/reparse points; the returned view only carries the
|
|
619
|
+
* canonical path back to this same authenticated connection.
|
|
620
|
+
*/
|
|
621
|
+
create(path) {
|
|
622
|
+
return this.transport.request("workspace/create", { path });
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Revoke (soft-delete) an owner's active workspace. Its conversations are
|
|
626
|
+
* kept and their `workspace_id` reference is preserved, so re-registering
|
|
627
|
+
* the same path re-activates the same workspace and those conversations
|
|
628
|
+
* become visible again.
|
|
629
|
+
*/
|
|
630
|
+
revoke(workspaceId) {
|
|
631
|
+
return this.transport.request("workspace/revoke", { workspace_id: workspaceId });
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/client.ts
|
|
636
|
+
/**
|
|
637
|
+
* Typed App Server client (`@flowy-agent-store/client`).
|
|
638
|
+
*
|
|
639
|
+
* Transport-agnostic: all business methods go through the injected
|
|
640
|
+
* `Transport` (07 §2.3). Web-only helpers (asset URLs, `/api/fs/browse`,
|
|
641
|
+
* one-shot HTTP registration) live in the host app, not here.
|
|
642
|
+
*
|
|
643
|
+
* Connection lifecycle: transport connect → `initialize` → protocol version
|
|
644
|
+
* check → `initialized` notification → ready. Business methods require a
|
|
645
|
+
* ready client; the server rejects them with `not_initialized` otherwise.
|
|
646
|
+
*/
|
|
647
|
+
var AppServerClient = class {
|
|
648
|
+
transport;
|
|
649
|
+
/** Legacy preset/execution workflow client. */
|
|
650
|
+
runs;
|
|
651
|
+
/** Persistent presetless Nomi chat client. */
|
|
652
|
+
conversations;
|
|
653
|
+
/** Owner-scoped workspace registry client (user-chosen paths). */
|
|
654
|
+
workspaces;
|
|
655
|
+
/** Agent Store Skill catalog client (`skill/list`, `skill/get`). */
|
|
656
|
+
skills;
|
|
657
|
+
/** Agent Store Connector catalog/status/probe/OAuth client. */
|
|
658
|
+
connectors;
|
|
659
|
+
/** Agent Store AgentDefinition catalog client (`agent/list`, `agent/get`). */
|
|
660
|
+
agents;
|
|
661
|
+
/** Agent Store Team catalog client (`team/list`, `team/get`). */
|
|
662
|
+
teams;
|
|
663
|
+
/** Public model directory (`models/list`, REQ-PAR-05b). */
|
|
664
|
+
models;
|
|
665
|
+
clientInfo;
|
|
666
|
+
capabilities;
|
|
667
|
+
initializeResult = null;
|
|
668
|
+
notificationListeners = /* @__PURE__ */ new Set();
|
|
669
|
+
constructor(options) {
|
|
670
|
+
this.clientInfo = options.client;
|
|
671
|
+
this.capabilities = options.capabilities;
|
|
672
|
+
this.transport = options.transport;
|
|
673
|
+
this.runs = new RunClient(this.transport);
|
|
674
|
+
this.conversations = new ConversationClient(this.transport);
|
|
675
|
+
this.workspaces = new WorkspaceClient(this.transport);
|
|
676
|
+
this.skills = new SkillClient(this.transport);
|
|
677
|
+
this.connectors = new ConnectorClient(this.transport);
|
|
678
|
+
this.agents = new AgentClient(this.transport);
|
|
679
|
+
this.teams = new TeamClient(this.transport);
|
|
680
|
+
this.models = new ModelClient(this.transport);
|
|
681
|
+
this.transport.onNotification((notification) => {
|
|
682
|
+
for (const listener of [...this.notificationListeners]) try {
|
|
683
|
+
listener(notification);
|
|
684
|
+
} catch {}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
get ready() {
|
|
688
|
+
return this.initializeResult !== null;
|
|
689
|
+
}
|
|
690
|
+
get initializeInfo() {
|
|
691
|
+
return this.initializeResult;
|
|
692
|
+
}
|
|
693
|
+
/** Connect and perform the initialize/initialized handshake. */
|
|
694
|
+
async connect() {
|
|
695
|
+
await this.transport.connect();
|
|
696
|
+
const request = {
|
|
697
|
+
protocol_version: _flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION,
|
|
698
|
+
client: this.clientInfo,
|
|
699
|
+
capabilities: this.capabilities
|
|
700
|
+
};
|
|
701
|
+
const result = await this.transport.request("initialize", request);
|
|
702
|
+
if (result.protocol_version !== _flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION) throw new _flowy_agent_store_protocol.ProtocolError("version_mismatch", `server protocol version ${result.protocol_version} is not supported (client ${_flowy_agent_store_protocol.APP_SERVER_PROTOCOL_VERSION})`);
|
|
703
|
+
this.transport.notify("initialized", {});
|
|
704
|
+
this.initializeResult = result;
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
onNotification(listener) {
|
|
708
|
+
this.notificationListeners.add(listener);
|
|
709
|
+
return () => {
|
|
710
|
+
this.notificationListeners.delete(listener);
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/** Close the transport; the server revokes the connection immediately. */
|
|
714
|
+
close() {
|
|
715
|
+
this.transport.close();
|
|
716
|
+
this.initializeResult = null;
|
|
717
|
+
this.notificationListeners.clear();
|
|
718
|
+
}
|
|
719
|
+
/** Import a local CodeBuddy/WorkBuddy source directory (roadmap Phase 1). */
|
|
720
|
+
async runImport(input) {
|
|
721
|
+
return this.transport.request("import/run", input);
|
|
722
|
+
}
|
|
723
|
+
async listImports() {
|
|
724
|
+
return this.transport.request("import/list", {});
|
|
725
|
+
}
|
|
726
|
+
/** One immutable snapshot with its standardized components. */
|
|
727
|
+
async getImport(snapshotId) {
|
|
728
|
+
return this.transport.request("import/get", { snapshot_id: snapshotId });
|
|
729
|
+
}
|
|
730
|
+
/** Install a snapshot into the runtime (roadmap Phase 2). */
|
|
731
|
+
async runInstall(input) {
|
|
732
|
+
return this.transport.request("install/run", input);
|
|
733
|
+
}
|
|
734
|
+
/** Per-component installation state of a snapshot. */
|
|
735
|
+
async getInstallStatus(snapshotId) {
|
|
736
|
+
return this.transport.request("install/status", { snapshot_id: snapshotId });
|
|
737
|
+
}
|
|
738
|
+
/** Disable installed components (runtime artifacts stay). */
|
|
739
|
+
async disableInstall(snapshotId, componentIds) {
|
|
740
|
+
return this.transport.request("install/disable", {
|
|
741
|
+
snapshot_id: snapshotId,
|
|
742
|
+
component_ids: componentIds
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
/** Re-enable previously disabled components. */
|
|
746
|
+
async enableInstall(snapshotId, componentIds) {
|
|
747
|
+
return this.transport.request("install/enable", {
|
|
748
|
+
snapshot_id: snapshotId,
|
|
749
|
+
component_ids: componentIds
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
/** Uninstall: remove runtime artifacts and clear the state (snapshot kept). */
|
|
753
|
+
async uninstallInstall(snapshotId, componentIds) {
|
|
754
|
+
return this.transport.request("install/uninstall", {
|
|
755
|
+
snapshot_id: snapshotId,
|
|
756
|
+
component_ids: componentIds
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
/** Register a marketplace source on the trusted host. */
|
|
760
|
+
async addMarketplace(input) {
|
|
761
|
+
return this.transport.request("market/add", input);
|
|
762
|
+
}
|
|
763
|
+
/** All active marketplaces. */
|
|
764
|
+
async listMarketplaces() {
|
|
765
|
+
return this.transport.request("market/list", {});
|
|
766
|
+
}
|
|
767
|
+
/** One marketplace with its discovered entries. */
|
|
768
|
+
async getMarketplace(marketplaceId) {
|
|
769
|
+
return this.transport.request("market/get", { marketplace_id: marketplaceId });
|
|
770
|
+
}
|
|
771
|
+
/** Remove a marketplace (`cascade` uninstalls snapshots installed from it). */
|
|
772
|
+
async removeMarketplace(marketplaceId, cascade) {
|
|
773
|
+
return this.transport.request("market/remove", {
|
|
774
|
+
marketplace_id: marketplaceId,
|
|
775
|
+
cascade
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
/** Toggle auto-update for a marketplace. */
|
|
779
|
+
async setMarketplaceAutoUpdate(marketplaceId, enabled) {
|
|
780
|
+
return this.transport.request("market/auto-update", {
|
|
781
|
+
marketplace_id: marketplaceId,
|
|
782
|
+
enabled
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
/** Re-fetch a marketplace source and rebuild entries when the revision changed. */
|
|
786
|
+
async refreshMarketplace(marketplaceId) {
|
|
787
|
+
return this.transport.request("market/refresh", { marketplace_id: marketplaceId });
|
|
788
|
+
}
|
|
789
|
+
/** Import one discovered marketplace entry (provenance linked). */
|
|
790
|
+
async importMarketplaceEntry(marketplaceId, entryName) {
|
|
791
|
+
return this.transport.request("market/entry-import", {
|
|
792
|
+
marketplace_id: marketplaceId,
|
|
793
|
+
entry_name: entryName
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
/** All store items across enabled marketplaces (display + install state). */
|
|
797
|
+
async listStore() {
|
|
798
|
+
return this.transport.request("store/list", {});
|
|
799
|
+
}
|
|
800
|
+
/** One-click install: import (when missing) + register in one call. */
|
|
801
|
+
async installStoreEntry(marketplaceId, entryName) {
|
|
802
|
+
return this.transport.request("store/install-entry", {
|
|
803
|
+
marketplace_id: marketplaceId,
|
|
804
|
+
entry_name: entryName
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
//#endregion
|
|
809
|
+
//#region src/conversation-handle.ts
|
|
810
|
+
/** How long `send` waits for a terminal event before returning a still-open turn. */
|
|
811
|
+
const TURN_TIMEOUT_MS = 6e5;
|
|
812
|
+
function stringValue(value) {
|
|
813
|
+
return typeof value === "string" ? value : void 0;
|
|
814
|
+
}
|
|
815
|
+
/** Aggregate assistant text from `message.delta` events (fallback path). */
|
|
816
|
+
function aggregateAssistantText(events) {
|
|
817
|
+
const byId = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const event of events) {
|
|
819
|
+
if (event.event_type !== "message.delta") continue;
|
|
820
|
+
if (typeof event.payload["content"] !== "string") continue;
|
|
821
|
+
const id = stringValue(event.payload["message_id"]) ?? "";
|
|
822
|
+
if (!id) continue;
|
|
823
|
+
byId.set(id, (byId.get(id) ?? "") + event.payload["content"]);
|
|
824
|
+
}
|
|
825
|
+
let last = null;
|
|
826
|
+
for (const value of byId.values()) if (value) last = value;
|
|
827
|
+
return last;
|
|
828
|
+
}
|
|
829
|
+
function lastUsage(events) {
|
|
830
|
+
for (let index = events.length - 1; index >= 0; index -= 1) if (events[index].event_type === "context.usage") {
|
|
831
|
+
const value = events[index].payload["context_usage"];
|
|
832
|
+
return value && typeof value === "object" ? value : null;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
var ConversationHandle = class ConversationHandle {
|
|
836
|
+
conversationId;
|
|
837
|
+
conversations;
|
|
838
|
+
subscription;
|
|
839
|
+
constructor(conversations, conversationId, subscription) {
|
|
840
|
+
this.conversations = conversations;
|
|
841
|
+
this.conversationId = conversationId;
|
|
842
|
+
this.subscription = subscription;
|
|
843
|
+
}
|
|
844
|
+
/** Create a new conversation and subscribe to its live event stream. */
|
|
845
|
+
static async open(conversations, input) {
|
|
846
|
+
const view = await conversations.create(input ?? {});
|
|
847
|
+
const subscription = await conversations.follow(view.conversation_id);
|
|
848
|
+
return new ConversationHandle(conversations, view.conversation_id, subscription);
|
|
849
|
+
}
|
|
850
|
+
/** Attach to an existing conversation (by id) and subscribe. */
|
|
851
|
+
static async attach(conversations, conversationId) {
|
|
852
|
+
const subscription = await conversations.follow(conversationId);
|
|
853
|
+
return new ConversationHandle(conversations, conversationId, subscription);
|
|
854
|
+
}
|
|
855
|
+
/** Register a listener for every conversation event (returns an unsubscribe). */
|
|
856
|
+
onEvent(listener) {
|
|
857
|
+
return this.subscription.onEvent(listener);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Send a message (a "turn") and await its terminal state. The event collector
|
|
861
|
+
* is installed before the send so streaming deltas / usage are captured even
|
|
862
|
+
* when they arrive before the send acknowledgment is parsed.
|
|
863
|
+
*/
|
|
864
|
+
async send(content, options = {}) {
|
|
865
|
+
const collected = [];
|
|
866
|
+
let terminalResolve;
|
|
867
|
+
let terminal = false;
|
|
868
|
+
const terminalPromise = new Promise((resolve) => {
|
|
869
|
+
terminalResolve = resolve;
|
|
870
|
+
});
|
|
871
|
+
const markTerminal = () => {
|
|
872
|
+
if (terminal) return;
|
|
873
|
+
terminal = true;
|
|
874
|
+
terminalResolve();
|
|
875
|
+
};
|
|
876
|
+
const off = this.subscription.onEvent((event) => {
|
|
877
|
+
collected.push(event);
|
|
878
|
+
if (event.event_type === "turn.status" && stringValue(event.payload["status"]) === "completed") markTerminal();
|
|
879
|
+
else if (event.event_type === "message.error") markTerminal();
|
|
880
|
+
});
|
|
881
|
+
let receipt;
|
|
882
|
+
try {
|
|
883
|
+
receipt = await this.conversations.send(this.conversationId, content, options.idempotencyKey ?? crypto.randomUUID());
|
|
884
|
+
if (!receipt.completed && !terminal) await Promise.race([terminalPromise, new Promise((resolve) => setTimeout(resolve, TURN_TIMEOUT_MS))]);
|
|
885
|
+
} finally {
|
|
886
|
+
off();
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
conversation_id: this.conversationId,
|
|
890
|
+
content,
|
|
891
|
+
message_id: receipt.message_id,
|
|
892
|
+
turn_id: receipt.turn_id,
|
|
893
|
+
accepted: receipt.accepted,
|
|
894
|
+
completed: receipt.completed || terminal,
|
|
895
|
+
events: collected,
|
|
896
|
+
assistant_text: receipt.result_text ?? aggregateAssistantText(collected),
|
|
897
|
+
isError: Boolean(receipt.result_error) || collected.some((event) => event.event_type === "message.error"),
|
|
898
|
+
result_error: receipt.result_error ?? null,
|
|
899
|
+
result_error_code: receipt.result_error_code ?? null,
|
|
900
|
+
usage: lastUsage(collected)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/** Cancel the in-flight turn (idempotent; erases the current transcript row). */
|
|
904
|
+
cancel() {
|
|
905
|
+
return this.conversations.cancel(this.conversationId).then(() => void 0);
|
|
906
|
+
}
|
|
907
|
+
/** Fetch the transcript (paginated). */
|
|
908
|
+
messages(query) {
|
|
909
|
+
return this.conversations.messages({
|
|
910
|
+
conversationId: this.conversationId,
|
|
911
|
+
...query
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
/** Unsubscribe and release the handle. */
|
|
915
|
+
close() {
|
|
916
|
+
return this.subscription.close();
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
/** Convenience: simple transcript snapshot (all roles, newest last). */
|
|
920
|
+
function conversationMessages(page) {
|
|
921
|
+
return page.items;
|
|
922
|
+
}
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region src/turn-result.ts
|
|
925
|
+
const KIND_BY_EVENT = {
|
|
926
|
+
"run.plan_changed": "plan",
|
|
927
|
+
"task.updated": "task",
|
|
928
|
+
"attempt.updated": "attempt",
|
|
929
|
+
"run.status_changed": "status",
|
|
930
|
+
"approval.requested": "approval",
|
|
931
|
+
"approval.responded": "approval"
|
|
932
|
+
};
|
|
933
|
+
function asRecord(payload) {
|
|
934
|
+
return payload && typeof payload === "object" ? payload : {};
|
|
935
|
+
}
|
|
936
|
+
function pickString(source, key) {
|
|
937
|
+
const value = source[key];
|
|
938
|
+
return typeof value === "string" ? value : void 0;
|
|
939
|
+
}
|
|
940
|
+
function toItem(event) {
|
|
941
|
+
const payload = asRecord(event.payload);
|
|
942
|
+
return {
|
|
943
|
+
sequence: event.sequence,
|
|
944
|
+
event_type: event.event_type,
|
|
945
|
+
kind: KIND_BY_EVENT[event.event_type] ?? "other",
|
|
946
|
+
ref_id: pickString(payload, "attempt_id") ?? pickString(payload, "step_id"),
|
|
947
|
+
status: pickString(payload, "status"),
|
|
948
|
+
reason: pickString(payload, "reason") ?? pickString(payload, "change")
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function extractUsage(events) {
|
|
952
|
+
for (const event of events) {
|
|
953
|
+
const payload = asRecord(event.payload);
|
|
954
|
+
const candidate = asRecord(payload["usage"] ?? payload["context_usage"]);
|
|
955
|
+
const input = candidate["input_tokens"] ?? candidate["inputTokens"];
|
|
956
|
+
const output = candidate["output_tokens"] ?? candidate["outputTokens"];
|
|
957
|
+
const total = candidate["total_tokens"] ?? candidate["totalTokens"];
|
|
958
|
+
if (typeof input === "number" || typeof output === "number" || typeof total === "number") return {
|
|
959
|
+
...typeof input === "number" ? { input_tokens: input } : {},
|
|
960
|
+
...typeof output === "number" ? { output_tokens: output } : {},
|
|
961
|
+
...typeof total === "number" ? { total_tokens: total } : {}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/** Build the terminal turn result from the authoritative view + event stream. */
|
|
966
|
+
function aggregateTurnResult(view, events) {
|
|
967
|
+
const ordered = [...events].sort((left, right) => left.sequence - right.sequence);
|
|
968
|
+
const usage = extractUsage(ordered);
|
|
969
|
+
return {
|
|
970
|
+
run_id: view.run_id,
|
|
971
|
+
status: view.status,
|
|
972
|
+
version: view.version,
|
|
973
|
+
final_response: view.summary ?? null,
|
|
974
|
+
output_files: view.output_files ?? [],
|
|
975
|
+
preset_revision: view.preset_revision ?? null,
|
|
976
|
+
content_digest: view.content_digest ?? null,
|
|
977
|
+
events: ordered,
|
|
978
|
+
items: ordered.map(toItem),
|
|
979
|
+
...usage ? { usage } : {}
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/run-handle.ts
|
|
984
|
+
/** Poll cadence while waiting for a terminal state (authoritative backstop). */
|
|
985
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
986
|
+
/** Absolute cap so a wedged run cannot hang `finished` forever. */
|
|
987
|
+
const FINISHED_TIMEOUT_MS = 18e5;
|
|
988
|
+
/** Live events kept for the terminal aggregation (bounded). */
|
|
989
|
+
const MAX_BUFFERED_EVENTS = 2e3;
|
|
990
|
+
const TERMINAL = [
|
|
991
|
+
"completed",
|
|
992
|
+
"failed",
|
|
993
|
+
"cancelled"
|
|
994
|
+
];
|
|
995
|
+
/**
|
|
996
|
+
* A launched Agent run. Async-iterable over live events; await `.finished`
|
|
997
|
+
* for the blocking Codex `thread.run()` shape.
|
|
998
|
+
*/
|
|
999
|
+
var AgentRunHandle = class {
|
|
1000
|
+
runId;
|
|
1001
|
+
receipt;
|
|
1002
|
+
runClient;
|
|
1003
|
+
poll;
|
|
1004
|
+
subscription;
|
|
1005
|
+
terminalView = null;
|
|
1006
|
+
terminalResult = null;
|
|
1007
|
+
finishWaiters = [];
|
|
1008
|
+
iteratorQueue = [];
|
|
1009
|
+
iteratorWaiters = [];
|
|
1010
|
+
iteratorClosed = false;
|
|
1011
|
+
bufferedEvents = [];
|
|
1012
|
+
bufferedSequences = /* @__PURE__ */ new Set();
|
|
1013
|
+
constructor(runClient, receipt, subscription, poll) {
|
|
1014
|
+
this.runId = receipt.run_id;
|
|
1015
|
+
this.receipt = receipt;
|
|
1016
|
+
this.runClient = runClient;
|
|
1017
|
+
this.subscription = subscription;
|
|
1018
|
+
this.poll = poll;
|
|
1019
|
+
subscription?.onEvent((event) => this.pushEvent(event));
|
|
1020
|
+
subscription?.onError(() => void 0);
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Resolves with the aggregated terminal turn result (≈ `thread.run()`):
|
|
1024
|
+
* authoritative status/version plus `final_response`, output files, the
|
|
1025
|
+
* persisted event stream and derived items.
|
|
1026
|
+
*/
|
|
1027
|
+
get finished() {
|
|
1028
|
+
if (this.terminalResult) return Promise.resolve(this.terminalResult);
|
|
1029
|
+
return new Promise((resolve, reject) => {
|
|
1030
|
+
this.finishWaiters.push((error, result) => {
|
|
1031
|
+
if (error) {
|
|
1032
|
+
reject(error);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
resolve(result);
|
|
1036
|
+
});
|
|
1037
|
+
this.ensureTerminalWatch();
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
/** Latest live cursor (proxied from the subscription when attached). */
|
|
1041
|
+
get lastSequence() {
|
|
1042
|
+
return this.subscription?.lastSequence ?? 0;
|
|
1043
|
+
}
|
|
1044
|
+
/** Ask the server to cancel; terminal confirmation still arrives via events/get. */
|
|
1045
|
+
async cancel() {
|
|
1046
|
+
await this.runClient.cancel({
|
|
1047
|
+
runId: this.runId,
|
|
1048
|
+
expectedVersion: this.receipt.version
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Inject steering text into the currently active step (≈ Codex turn steer).
|
|
1053
|
+
* Throws `conflict` when the run already changed or has no active step —
|
|
1054
|
+
* refresh `receipt`-level state via `runClient.get()` and retry.
|
|
1055
|
+
*/
|
|
1056
|
+
async steer(text) {
|
|
1057
|
+
return this.runClient.steer({
|
|
1058
|
+
runId: this.runId,
|
|
1059
|
+
text,
|
|
1060
|
+
expectedVersion: this.receipt.version
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
/** Detach the live subscription (events may still be polled via run/events). */
|
|
1064
|
+
async close() {
|
|
1065
|
+
this.iteratorClosed = true;
|
|
1066
|
+
const waiters = this.iteratorWaiters;
|
|
1067
|
+
this.iteratorWaiters = [];
|
|
1068
|
+
for (const waiter of waiters) waiter({
|
|
1069
|
+
value: void 0,
|
|
1070
|
+
done: true
|
|
1071
|
+
});
|
|
1072
|
+
const subscription = this.subscription;
|
|
1073
|
+
this.subscription = null;
|
|
1074
|
+
await subscription?.close();
|
|
1075
|
+
}
|
|
1076
|
+
/** Async iterator over live events (≈ `thread.runStreamed()`). */
|
|
1077
|
+
[Symbol.asyncIterator]() {
|
|
1078
|
+
return { next: () => {
|
|
1079
|
+
if (this.iteratorQueue.length > 0) {
|
|
1080
|
+
const event = this.iteratorQueue.shift();
|
|
1081
|
+
return Promise.resolve({
|
|
1082
|
+
value: event,
|
|
1083
|
+
done: false
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
if (this.iteratorClosed) return Promise.resolve({
|
|
1087
|
+
value: void 0,
|
|
1088
|
+
done: true
|
|
1089
|
+
});
|
|
1090
|
+
return new Promise((resolve) => {
|
|
1091
|
+
this.iteratorWaiters.push(resolve);
|
|
1092
|
+
});
|
|
1093
|
+
} };
|
|
1094
|
+
}
|
|
1095
|
+
pushEvent(event) {
|
|
1096
|
+
if (!this.bufferedSequences.has(event.sequence)) {
|
|
1097
|
+
this.bufferedSequences.add(event.sequence);
|
|
1098
|
+
this.bufferedEvents.push(event);
|
|
1099
|
+
if (this.bufferedEvents.length > MAX_BUFFERED_EVENTS) {
|
|
1100
|
+
const dropped = this.bufferedEvents.shift();
|
|
1101
|
+
if (dropped) this.bufferedSequences.delete(dropped.sequence);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const waiter = this.iteratorWaiters.shift();
|
|
1105
|
+
if (waiter) {
|
|
1106
|
+
waiter({
|
|
1107
|
+
value: event,
|
|
1108
|
+
done: false
|
|
1109
|
+
});
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
this.iteratorQueue.push(event);
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Authoritative backfill for `finished`: `run/result` (terminal summary)
|
|
1116
|
+
* plus the persisted `run/events` stream, falling back to the live buffer
|
|
1117
|
+
* when the query fails.
|
|
1118
|
+
*/
|
|
1119
|
+
async aggregate(view) {
|
|
1120
|
+
const result = await this.runClient.result(this.runId).catch(() => null);
|
|
1121
|
+
const events = await this.runClient.events({
|
|
1122
|
+
runId: this.runId,
|
|
1123
|
+
afterSequence: 0,
|
|
1124
|
+
limit: MAX_BUFFERED_EVENTS
|
|
1125
|
+
}).catch(() => this.bufferedEvents);
|
|
1126
|
+
const source = events.length > 0 ? events : this.bufferedEvents;
|
|
1127
|
+
return aggregateTurnResult(result ?? view, source);
|
|
1128
|
+
}
|
|
1129
|
+
/** Poll run/get until terminal, then resolve all `finished` waiters once. */
|
|
1130
|
+
ensureTerminalWatch() {
|
|
1131
|
+
(async () => {
|
|
1132
|
+
const deadline = Date.now() + FINISHED_TIMEOUT_MS;
|
|
1133
|
+
try {
|
|
1134
|
+
while (!this.terminalView && Date.now() < deadline) {
|
|
1135
|
+
const view = await this.poll(this.runId);
|
|
1136
|
+
if (TERMINAL.includes(view.status)) this.terminalView = view;
|
|
1137
|
+
else await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
1138
|
+
}
|
|
1139
|
+
const waiters = this.finishWaiters;
|
|
1140
|
+
this.finishWaiters = [];
|
|
1141
|
+
if (!this.terminalView) {
|
|
1142
|
+
const timeout = /* @__PURE__ */ new Error(`run ${this.runId} did not reach a terminal state in time`);
|
|
1143
|
+
for (const waiter of waiters) waiter(timeout);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
this.terminalResult = await this.aggregate(this.terminalView);
|
|
1147
|
+
for (const waiter of waiters) waiter(void 0, this.terminalResult);
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
const waiters = this.finishWaiters;
|
|
1150
|
+
this.finishWaiters = [];
|
|
1151
|
+
for (const waiter of waiters) waiter(error);
|
|
1152
|
+
}
|
|
1153
|
+
})();
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
/** Codex-parity launch: start the run and return a live handle immediately. */
|
|
1157
|
+
async function launchRun(runClient, input, options = {}) {
|
|
1158
|
+
const receipt = await runClient.agent(input);
|
|
1159
|
+
return new AgentRunHandle(runClient, receipt, options.follow === false ? null : await runClient.follow(receipt.run_id), (runId) => runClient.get(runId));
|
|
1160
|
+
}
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/retry.ts
|
|
1163
|
+
/**
|
|
1164
|
+
* Retry helper (REQ-PAR-05e, ≈ Codex `retry_on_overload`): exponential backoff
|
|
1165
|
+
* with jitter, driven by the protocol's stable `retryable` hint.
|
|
1166
|
+
*
|
|
1167
|
+
* Only errors the server/transport marks retryable are retried; protocol
|
|
1168
|
+
* errors that carry `retryable: false` (validation, not_found, ...) surface
|
|
1169
|
+
* immediately. `command_id` / `idempotency_key` callers stay safe because the
|
|
1170
|
+
* App Server deduplicates replayed mutations.
|
|
1171
|
+
*/
|
|
1172
|
+
/** Run `operation`, retrying retryable failures with exponential backoff. */
|
|
1173
|
+
async function withRetry(operation, options = {}) {
|
|
1174
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
1175
|
+
const baseDelayMs = Math.max(0, options.baseDelayMs ?? 500);
|
|
1176
|
+
const maxDelayMs = Math.max(baseDelayMs, options.maxDelayMs ?? 8e3);
|
|
1177
|
+
const jitter = Math.min(1, Math.max(0, options.jitter ?? .25));
|
|
1178
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1179
|
+
const shouldRetry = options.shouldRetry ?? ((error) => (0, _flowy_agent_store_protocol.isRetryableError)(error));
|
|
1180
|
+
let lastError;
|
|
1181
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
1182
|
+
return await operation();
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
lastError = error;
|
|
1185
|
+
if (attempt >= maxAttempts || !shouldRetry(error, attempt)) throw error;
|
|
1186
|
+
const ceiling = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
1187
|
+
const delayMs = Math.round(ceiling * (1 - jitter) + Math.random() * ceiling * jitter);
|
|
1188
|
+
options.onRetry?.({
|
|
1189
|
+
attempt,
|
|
1190
|
+
delayMs,
|
|
1191
|
+
error
|
|
1192
|
+
});
|
|
1193
|
+
await sleep(delayMs);
|
|
1194
|
+
}
|
|
1195
|
+
throw lastError;
|
|
1196
|
+
}
|
|
1197
|
+
//#endregion
|
|
1198
|
+
exports.AgentClient = AgentClient;
|
|
1199
|
+
exports.AgentRunHandle = AgentRunHandle;
|
|
1200
|
+
exports.AppServerClient = AppServerClient;
|
|
1201
|
+
exports.ConnectorClient = ConnectorClient;
|
|
1202
|
+
exports.ConversationClient = ConversationClient;
|
|
1203
|
+
exports.ConversationHandle = ConversationHandle;
|
|
1204
|
+
exports.ConversationSubscription = ConversationSubscription;
|
|
1205
|
+
exports.EventSubscription = EventSubscription;
|
|
1206
|
+
exports.ModelClient = ModelClient;
|
|
1207
|
+
exports.RunClient = RunClient;
|
|
1208
|
+
exports.SkillClient = SkillClient;
|
|
1209
|
+
exports.TeamClient = TeamClient;
|
|
1210
|
+
exports.WebSocketTransport = WebSocketTransport;
|
|
1211
|
+
exports.WorkspaceClient = WorkspaceClient;
|
|
1212
|
+
exports.aggregateTurnResult = aggregateTurnResult;
|
|
1213
|
+
exports.conversationMessages = conversationMessages;
|
|
1214
|
+
exports.isLoopbackUrl = isLoopbackUrl;
|
|
1215
|
+
exports.launchRun = launchRun;
|
|
1216
|
+
exports.withRetry = withRetry;
|