@hachej/boring-agent 0.1.77 → 0.1.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,531 @@
1
+ import {
2
+ AgentNotImplementedError
3
+ } from "./chunk-WSQ5QNIY.js";
4
+ import {
5
+ ErrorCode
6
+ } from "./chunk-YVON2BHN.js";
7
+
8
+ // src/core/createAgent.ts
9
+ var DEFAULT_LIVE_BUFFER_SIZE = 1e3;
10
+ function createAgent(config) {
11
+ return createAgentRuntimeBridge(config).agent;
12
+ }
13
+ function createAgentRuntimeBridge(config) {
14
+ const runtimeLoader = createRuntimeLoader(config.runtimeFactory);
15
+ const live = new AgentLiveEventBuffer(DEFAULT_LIVE_BUFFER_SIZE);
16
+ const sessionContexts = /* @__PURE__ */ new Map();
17
+ const startedSessions = /* @__PURE__ */ new Map();
18
+ const sendLocks = /* @__PURE__ */ new Map();
19
+ const producerTeardownLocks = /* @__PURE__ */ new Map();
20
+ let disposed = false;
21
+ let disposePromise;
22
+ const assertActive = () => {
23
+ if (disposed) {
24
+ throw stableAgentError(ErrorCode.enum.AGENT_BINDING_DISPOSED, "agent binding has been disposed");
25
+ }
26
+ };
27
+ const getRuntime = async () => {
28
+ assertActive();
29
+ const runtime = await runtimeLoader.get();
30
+ assertActive();
31
+ return runtime;
32
+ };
33
+ const sessions = createFacadeSessionStore(
34
+ getRuntime,
35
+ assertActive,
36
+ live,
37
+ sessionContexts,
38
+ startedSessions,
39
+ runProducerTeardown
40
+ );
41
+ const readiness = createReadiness(config, assertActive);
42
+ async function ensureSession(input, runtime) {
43
+ if (input.sessionId) {
44
+ const ctx = await authorizeSessionAccess(runtime, input.sessionId, input.ctx, sessionContexts);
45
+ return { sessionId: input.sessionId, sessionKey: sessionCacheKey(input.sessionId, ctx), ctx };
46
+ }
47
+ const service = runtime.service;
48
+ const created = await service.createSession(toPiRequestContext(input.ctx), {
49
+ title: contentToText(input.content ?? input.message).slice(0, 80) || void 0
50
+ });
51
+ rememberSessionCtx(sessionContexts, created.id, input.ctx);
52
+ return { sessionId: created.id, sessionKey: sessionCacheKey(created.id, input.ctx), ctx: input.ctx };
53
+ }
54
+ async function ensureBridge(sessionKey, sessionId, ctx, service) {
55
+ const state = live.ensure(sessionKey);
56
+ if (state.bridge || state.closed) return state;
57
+ state.bridgePromise ??= (async () => {
58
+ const subscribeFrom = state.latestIndex === 0 ? 0 : Number.MAX_SAFE_INTEGER;
59
+ let result = await service.subscribe(toPiRequestContext(ctx), sessionId, subscribeFrom, (chunk) => {
60
+ if (!state.closed) live.publish(sessionKey, sessionId, chunk);
61
+ });
62
+ if (result.type !== "ok") {
63
+ result = await service.subscribe(toPiRequestContext(ctx), sessionId, result.latestSeq, (chunk) => {
64
+ if (!state.closed) live.publish(sessionKey, sessionId, chunk);
65
+ });
66
+ }
67
+ if (result.type !== "ok") throw new AgentNotImplementedError("Historical pi-chat replay is not implemented until T1.");
68
+ if (disposed || state.closed) {
69
+ result.unsubscribe();
70
+ if (disposed) assertActive();
71
+ throw stableAgentError(ErrorCode.enum.ABORTED, "session stopped while start was pending");
72
+ }
73
+ state.bridge = result;
74
+ result.closed?.finally(() => live.close(sessionKey, state)).catch(() => live.close(sessionKey, state));
75
+ return state;
76
+ })();
77
+ try {
78
+ return await state.bridgePromise;
79
+ } finally {
80
+ state.bridgePromise = void 0;
81
+ }
82
+ }
83
+ async function start(input) {
84
+ assertActive();
85
+ const runtime = await getRuntime();
86
+ const { sessionId, sessionKey, ctx } = await ensureSession(input, runtime);
87
+ assertActive();
88
+ startedSessions.set(sessionKey, { sessionId, ctx });
89
+ await ensureBridge(sessionKey, sessionId, ctx, runtime.service);
90
+ assertActive();
91
+ const startIndex = live.latestIndex(sessionKey);
92
+ await runtime.service.prompt(toPiRequestContext(ctx), sessionId, toPromptPayload(input));
93
+ assertActive();
94
+ return { sessionId, startIndex };
95
+ }
96
+ const agent = {
97
+ sessions,
98
+ readiness,
99
+ start,
100
+ stream(sessionId, options) {
101
+ assertActive();
102
+ return authorizedStream(sessionId, options);
103
+ },
104
+ async *send(input) {
105
+ assertActive();
106
+ const release = input.sessionId ? await acquireSessionLock(sendLocks, sessionCacheKey(input.sessionId, input.ctx)) : void 0;
107
+ try {
108
+ assertActive();
109
+ const receipt = await start(input);
110
+ let turnId;
111
+ for await (const event of authorizedStream(receipt.sessionId, { startIndex: receipt.startIndex, ctx: input.ctx })) {
112
+ yield event;
113
+ if (event.chunk.type === "agent-start") turnId = event.chunk.turnId;
114
+ if (isSendTerminalEvent(event, turnId)) break;
115
+ }
116
+ } finally {
117
+ release?.();
118
+ }
119
+ },
120
+ async resolveInput(_sessionId, _requestId, _response) {
121
+ assertActive();
122
+ throw new AgentNotImplementedError("resolveInput is not implemented until T1.");
123
+ },
124
+ async interrupt(sessionId, ctx) {
125
+ assertActive();
126
+ const runtime = await getRuntime();
127
+ const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
128
+ assertActive();
129
+ return runtime.service.interrupt(toPiRequestContext(accessCtx), sessionId, {});
130
+ },
131
+ async stop(sessionId, ctx) {
132
+ assertActive();
133
+ const runtime = await getRuntime();
134
+ const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
135
+ assertActive();
136
+ const sessionKey = sessionCacheKey(sessionId, accessCtx);
137
+ return runProducerTeardown(sessionKey, async () => {
138
+ assertActive();
139
+ const receipt = await runtime.service.stop(toPiRequestContext(accessCtx), sessionId, {});
140
+ startedSessions.delete(sessionKey);
141
+ live.close(sessionKey);
142
+ return receipt;
143
+ });
144
+ },
145
+ dispose() {
146
+ disposed = true;
147
+ disposePromise ??= disposeBinding();
148
+ return disposePromise;
149
+ }
150
+ };
151
+ async function disposeBinding() {
152
+ const started = [...startedSessions.entries()];
153
+ let runtime;
154
+ let teardownError;
155
+ try {
156
+ runtime = await runtimeLoader.current();
157
+ if (runtime) {
158
+ const activeRuntime = runtime;
159
+ const results = await Promise.allSettled(started.map(
160
+ ([sessionKey, session]) => runProducerTeardown(sessionKey, async () => {
161
+ if (startedSessions.get(sessionKey) !== session) return;
162
+ await activeRuntime.service.stop(toPiRequestContext(session.ctx), session.sessionId, {});
163
+ startedSessions.delete(sessionKey);
164
+ live.close(sessionKey);
165
+ })
166
+ ));
167
+ const failed = results.find((result) => result.status === "rejected");
168
+ if (failed) throw failed.reason;
169
+ }
170
+ } catch (error) {
171
+ teardownError = error;
172
+ }
173
+ try {
174
+ await runtime?.service.dispose?.();
175
+ } catch (error) {
176
+ teardownError ??= error;
177
+ } finally {
178
+ try {
179
+ live.dispose();
180
+ } catch (error) {
181
+ teardownError ??= error;
182
+ } finally {
183
+ startedSessions.clear();
184
+ sessionContexts.clear();
185
+ sendLocks.clear();
186
+ producerTeardownLocks.clear();
187
+ }
188
+ }
189
+ if (teardownError) throw teardownError;
190
+ }
191
+ async function runProducerTeardown(sessionKey, teardown) {
192
+ const release = await acquireSessionLock(producerTeardownLocks, sessionKey);
193
+ try {
194
+ return await teardown();
195
+ } finally {
196
+ release();
197
+ }
198
+ }
199
+ return {
200
+ agent,
201
+ getRuntime,
202
+ currentRuntime() {
203
+ assertActive();
204
+ return runtimeLoader.current();
205
+ }
206
+ };
207
+ async function* authorizedStream(sessionId, options) {
208
+ assertActive();
209
+ const runtime = await getRuntime();
210
+ const accessCtx = await authorizeSessionAccess(runtime, sessionId, options.ctx, sessionContexts);
211
+ assertActive();
212
+ yield* live.stream(sessionCacheKey(sessionId, accessCtx), options.startIndex);
213
+ }
214
+ }
215
+ function createRuntimeLoader(runtimeFactory) {
216
+ let runtimePromise;
217
+ return {
218
+ get() {
219
+ runtimePromise ??= Promise.resolve().then(() => runtimeFactory());
220
+ return runtimePromise;
221
+ },
222
+ current() {
223
+ return runtimePromise;
224
+ }
225
+ };
226
+ }
227
+ function createReadiness(config, assertActive) {
228
+ const requirements = [...config.readinessRequirements ?? []];
229
+ return {
230
+ requirements,
231
+ async status() {
232
+ assertActive();
233
+ return requirements.map((key) => ({
234
+ key,
235
+ ready: false,
236
+ message: "readiness status is not available in the core facade"
237
+ }));
238
+ }
239
+ };
240
+ }
241
+ function createFacadeSessionStore(getRuntime, assertActive, live, sessionContexts, startedSessions, runProducerTeardown) {
242
+ const store = async () => (await getRuntime()).sessionStore;
243
+ return {
244
+ async list(ctx, options) {
245
+ assertActive();
246
+ const summaries = await (await store()).list(ctx, options);
247
+ assertActive();
248
+ return summaries.filter((summary) => canAccessStoredSessionCtx(summary.id, ctx, sessionContexts));
249
+ },
250
+ async create(ctx, init) {
251
+ assertActive();
252
+ const created = await (await store()).create(ctx, init);
253
+ assertActive();
254
+ rememberSessionCtx(sessionContexts, created.id, ctx);
255
+ return created;
256
+ },
257
+ async load(ctx, sessionId) {
258
+ assertActive();
259
+ const runtime = await getRuntime();
260
+ const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
261
+ assertActive();
262
+ const loaded = await (await store()).load(accessCtx ?? {}, sessionId);
263
+ assertActive();
264
+ return loaded;
265
+ },
266
+ async delete(ctx, sessionId) {
267
+ assertActive();
268
+ const runtime = await getRuntime();
269
+ const accessCtx = await authorizeSessionAccess(runtime, sessionId, ctx, sessionContexts);
270
+ assertActive();
271
+ const sessionKey = sessionCacheKey(sessionId, accessCtx);
272
+ await runProducerTeardown(sessionKey, async () => {
273
+ assertActive();
274
+ await runtime.service.deleteSession(toPiRequestContext(accessCtx), sessionId);
275
+ startedSessions.delete(sessionKey);
276
+ live.close(sessionKey);
277
+ sessionContexts.delete(sessionKey);
278
+ });
279
+ }
280
+ };
281
+ }
282
+ function toPiRequestContext(ctx) {
283
+ return {
284
+ workspaceId: ctx?.workspaceId,
285
+ authSubject: ctx?.userId,
286
+ requestId: "agent-core"
287
+ };
288
+ }
289
+ function toPromptPayload(input) {
290
+ return {
291
+ message: contentToText(input.content ?? input.message),
292
+ clientNonce: `agent:${Date.now()}:${Math.random().toString(36).slice(2)}`,
293
+ ...input.model ? { model: input.model } : {},
294
+ ...input.thinkingLevel ? { thinkingLevel: input.thinkingLevel } : {},
295
+ ...input.attachments ? { attachments: input.attachments } : {}
296
+ };
297
+ }
298
+ function contentToText(content) {
299
+ if (content === void 0) return "";
300
+ if (typeof content === "string") return content;
301
+ return content.map((part) => part.text).filter((text) => typeof text === "string" && text.length > 0).join("\n");
302
+ }
303
+ async function loadSession(store, ctx, sessionId) {
304
+ try {
305
+ await store.load(ctx, sessionId);
306
+ } catch (error) {
307
+ throw normalizeSessionLoadError(error, sessionId);
308
+ }
309
+ }
310
+ async function assertLoadedSessionVisible(store, ctx, sessionId) {
311
+ const summaries = await store.list(ctx, { includeId: sessionId });
312
+ if (!summaries.some((summary) => summary.id === sessionId)) {
313
+ throw stableAgentError(ErrorCode.enum.UNAUTHORIZED, "session context mismatch");
314
+ }
315
+ }
316
+ function normalizeSessionLoadError(error, sessionId) {
317
+ if (error?.code === ErrorCode.enum.SESSION_NOT_FOUND || isPlainSessionNotFound(error, sessionId)) {
318
+ return stableAgentError(ErrorCode.enum.SESSION_NOT_FOUND, "session not found");
319
+ }
320
+ return error;
321
+ }
322
+ async function authorizeSessionAccess(runtime, sessionId, callerCtx, sessionContexts) {
323
+ const requestedKey = sessionCacheKey(sessionId, callerCtx);
324
+ const hasStoredCtx = sessionContexts.has(requestedKey);
325
+ const storedCtx = sessionContexts.get(requestedKey);
326
+ if (hasStoredCtx && callerCtx && !sameSessionCtx(callerCtx, storedCtx)) {
327
+ throw stableAgentError(ErrorCode.enum.UNAUTHORIZED, "session context mismatch");
328
+ }
329
+ if (hasStoredCtx && !callerCtx && !isEmptySessionCtx(storedCtx)) {
330
+ throw stableAgentError(ErrorCode.enum.UNAUTHORIZED, "session context required");
331
+ }
332
+ const accessCtx = callerCtx ?? storedCtx ?? {};
333
+ await loadSession(runtime.sessionStore, accessCtx, sessionId);
334
+ if (!hasStoredCtx) {
335
+ await assertLoadedSessionVisible(runtime.sessionStore, accessCtx, sessionId);
336
+ rememberSessionCtx(sessionContexts, sessionId, accessCtx);
337
+ }
338
+ return accessCtx;
339
+ }
340
+ function rememberSessionCtx(sessionContexts, sessionId, ctx) {
341
+ sessionContexts.set(sessionCacheKey(sessionId, ctx), isEmptySessionCtx(ctx) ? void 0 : { workspaceId: ctx?.workspaceId, userId: ctx?.userId });
342
+ }
343
+ function sameSessionCtx(a, b) {
344
+ return (a?.workspaceId ?? "") === (b?.workspaceId ?? "") && (a?.userId ?? "") === (b?.userId ?? "");
345
+ }
346
+ function isEmptySessionCtx(ctx) {
347
+ return !ctx?.workspaceId && !ctx?.userId;
348
+ }
349
+ function canAccessStoredSessionCtx(sessionId, callerCtx, sessionContexts) {
350
+ const sessionKey = sessionCacheKey(sessionId, callerCtx);
351
+ if (!sessionContexts.has(sessionKey)) return true;
352
+ const storedCtx = sessionContexts.get(sessionKey);
353
+ return sameSessionCtx(callerCtx, storedCtx);
354
+ }
355
+ function sessionCacheKey(sessionId, ctx) {
356
+ return JSON.stringify([sessionId, ctx?.workspaceId ?? "", ctx?.userId ?? ""]);
357
+ }
358
+ function stableAgentError(code, message) {
359
+ return Object.assign(new Error(message), { code });
360
+ }
361
+ async function acquireSessionLock(locks, sessionId) {
362
+ const previous = locks.get(sessionId) ?? Promise.resolve();
363
+ let release;
364
+ const current = previous.catch(() => {
365
+ }).then(() => new Promise((resolve) => {
366
+ release = resolve;
367
+ }));
368
+ locks.set(sessionId, current);
369
+ await previous.catch(() => {
370
+ });
371
+ return () => {
372
+ release();
373
+ if (locks.get(sessionId) === current) locks.delete(sessionId);
374
+ };
375
+ }
376
+ function isPlainSessionNotFound(error, sessionId) {
377
+ return error instanceof Error && (error.message === "session not found" || error.message === `Session not found: ${sessionId}` || error.message === `missing session ${sessionId}`);
378
+ }
379
+ function isSendTerminalEvent(event, turnId) {
380
+ const chunk = event.chunk;
381
+ if (chunk.type === "error") return !chunk.turnId || chunk.turnId === turnId;
382
+ return chunk.type === "agent-end" && chunk.willRetry !== true && chunk.turnId === turnId;
383
+ }
384
+ var AgentLiveEventBuffer = class {
385
+ constructor(maxEvents) {
386
+ this.maxEvents = maxEvents;
387
+ }
388
+ maxEvents;
389
+ sessions = /* @__PURE__ */ new Map();
390
+ eventIndexes = /* @__PURE__ */ new Map();
391
+ disposed = false;
392
+ latestIndex(sessionKey) {
393
+ return this.eventIndexes.get(sessionKey) ?? 0;
394
+ }
395
+ ensure(sessionKey) {
396
+ let state = this.sessions.get(sessionKey);
397
+ if (!state || state.closed) {
398
+ const latestIndex = this.latestIndex(sessionKey);
399
+ state = {
400
+ events: [],
401
+ subscribers: /* @__PURE__ */ new Set(),
402
+ latestIndex,
403
+ evictedThroughIndex: latestIndex - 1,
404
+ closed: false
405
+ };
406
+ this.sessions.set(sessionKey, state);
407
+ }
408
+ return state;
409
+ }
410
+ publish(sessionKey, sessionId, chunk) {
411
+ if (this.disposed) return;
412
+ const state = this.ensure(sessionKey);
413
+ if (state.closed) return;
414
+ const eventIndex = this.latestIndex(sessionKey);
415
+ this.eventIndexes.set(sessionKey, eventIndex + 1);
416
+ const event = {
417
+ v: 1,
418
+ eventIndex,
419
+ timestamp: Date.now(),
420
+ sessionId,
421
+ chunk
422
+ };
423
+ state.latestIndex = event.eventIndex + 1;
424
+ state.events.push(event);
425
+ if (state.events.length > this.maxEvents) {
426
+ const evicted = state.events.splice(0, state.events.length - this.maxEvents);
427
+ state.evictedThroughIndex = evicted[evicted.length - 1]?.eventIndex ?? state.evictedThroughIndex;
428
+ }
429
+ for (const subscriber of state.subscribers) subscriber.push(event);
430
+ }
431
+ stream(sessionKey, startIndex) {
432
+ const state = this.ensure(sessionKey);
433
+ this.assertReplayable(state, startIndex);
434
+ return {
435
+ [Symbol.asyncIterator]: () => {
436
+ this.assertReplayable(state, startIndex);
437
+ return createLiveIterator(state, startIndex);
438
+ }
439
+ };
440
+ }
441
+ close(sessionKey, expectedState) {
442
+ const state = this.sessions.get(sessionKey);
443
+ if (!state || expectedState && state !== expectedState) return;
444
+ state.closed = true;
445
+ let unsubscribeError;
446
+ try {
447
+ state.bridge?.unsubscribe();
448
+ } catch (error) {
449
+ unsubscribeError = error;
450
+ }
451
+ for (const subscriber of state.subscribers) subscriber.close();
452
+ state.subscribers.clear();
453
+ if (unsubscribeError) throw unsubscribeError;
454
+ }
455
+ dispose() {
456
+ if (this.disposed) return;
457
+ this.disposed = true;
458
+ let disposeError;
459
+ for (const [sessionId] of this.sessions) {
460
+ try {
461
+ this.close(sessionId);
462
+ } catch (error) {
463
+ disposeError ??= error;
464
+ }
465
+ }
466
+ this.sessions.clear();
467
+ this.eventIndexes.clear();
468
+ if (disposeError) throw disposeError;
469
+ }
470
+ assertReplayable(state, startIndex) {
471
+ if (!Number.isInteger(startIndex) || startIndex < 0) {
472
+ throw cursorOutOfRangeError("startIndex must be a non-negative integer", {
473
+ startIndex,
474
+ latestIndex: state.latestIndex
475
+ });
476
+ }
477
+ if (startIndex <= state.evictedThroughIndex) {
478
+ throw new AgentNotImplementedError("Historical stream replay is not implemented until T1.");
479
+ }
480
+ if (startIndex > state.latestIndex) {
481
+ throw cursorOutOfRangeError(`startIndex ${startIndex} is ahead of next eventIndex ${state.latestIndex}`, {
482
+ startIndex,
483
+ latestIndex: state.latestIndex
484
+ });
485
+ }
486
+ }
487
+ };
488
+ function cursorOutOfRangeError(message, details) {
489
+ return Object.assign(new RangeError(message), {
490
+ code: ErrorCode.enum.CURSOR_OUT_OF_RANGE,
491
+ details
492
+ });
493
+ }
494
+ function createLiveIterator(state, startIndex) {
495
+ const queued = [];
496
+ const waiters = [];
497
+ let active = true;
498
+ const subscriber = {
499
+ push(event) {
500
+ if (!active) return;
501
+ const waiter = waiters.shift();
502
+ if (waiter) waiter({ value: event, done: false });
503
+ else queued.push(event);
504
+ },
505
+ close() {
506
+ if (!active) return;
507
+ active = false;
508
+ while (waiters.length > 0) waiters.shift()?.({ value: void 0, done: true });
509
+ }
510
+ };
511
+ state.subscribers.add(subscriber);
512
+ queued.push(...state.events.filter((event) => event.eventIndex >= startIndex));
513
+ return {
514
+ async next() {
515
+ if (queued.length > 0) return { value: queued.shift(), done: false };
516
+ if (!active || state.closed) return { value: void 0, done: true };
517
+ return new Promise((resolve) => waiters.push(resolve));
518
+ },
519
+ async return() {
520
+ active = false;
521
+ state.subscribers.delete(subscriber);
522
+ while (waiters.length > 0) waiters.shift()?.({ value: void 0, done: true });
523
+ return { value: void 0, done: true };
524
+ }
525
+ };
526
+ }
527
+
528
+ export {
529
+ createAgent,
530
+ createAgentRuntimeBridge
531
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ErrorCode
3
- } from "./chunk-6AEK34XU.js";
3
+ } from "./chunk-YVON2BHN.js";
4
4
 
5
5
  // src/shared/tool-ui.ts
6
6
  function isRecord(value) {