@kuralle-agents/cf-agent 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ import type { RuntimeLike } from '@kuralle-agents/core';
2
+ import { type ClaimResult, type Clock, type CoalesceScheduler, type ConsentStore, type ConversationKey, type InboundEvent, type InboundLedger, type InboundRuntime, type MediaResolver, type OutboundSender, type OwnershipStore, type TurnResult, type TurnRunner, type WindowState, type WindowStore } from '@kuralle-agents/messaging';
3
+ import { TurnQueue, type MessageConcurrency } from 'agents/chat';
4
+ import type { SqlExecutor } from './types.js';
5
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
6
+ [key: string]: JsonValue;
7
+ };
8
+ export declare class SqlInboundLedger implements InboundLedger {
9
+ private readonly sql;
10
+ private readonly options;
11
+ private initialized;
12
+ constructor(sql: SqlExecutor, options?: {
13
+ inProgressTtlMs?: number;
14
+ });
15
+ claim(key: ConversationKey, eventId: string): Promise<ClaimResult>;
16
+ complete(key: ConversationKey, eventId: string): Promise<void>;
17
+ append(key: ConversationKey, event: InboundEvent): Promise<{
18
+ seq: number;
19
+ }>;
20
+ readUnprocessed(key: ConversationKey): Promise<InboundEvent[]>;
21
+ commitCursor(key: ConversationKey, throughSeq: number, expect: number): Promise<boolean>;
22
+ prune(key: ConversationKey, ttlMs: number): Promise<number>;
23
+ cursor(key: ConversationKey): number;
24
+ private ensureTables;
25
+ private ensureCursor;
26
+ }
27
+ export declare class SqlWindowStore implements WindowStore {
28
+ private readonly sql;
29
+ private initialized;
30
+ constructor(sql: SqlExecutor);
31
+ get(threadId: string): Promise<WindowState>;
32
+ recordInbound(threadId: string, ts: Date): Promise<void>;
33
+ recordExpiry(threadId: string, at: Date): Promise<void>;
34
+ private ensureTable;
35
+ }
36
+ export declare class SqlConsentStore implements ConsentStore {
37
+ private readonly sql;
38
+ private initialized;
39
+ constructor(sql: SqlExecutor);
40
+ isOptedIn(customerId: string): Promise<boolean>;
41
+ optOut(customerId: string): Promise<void>;
42
+ optIn(customerId: string): Promise<void>;
43
+ private upsert;
44
+ private ensureTable;
45
+ }
46
+ export declare class SqlOwnershipStore implements OwnershipStore {
47
+ private readonly sql;
48
+ private initialized;
49
+ constructor(sql: SqlExecutor);
50
+ owner(threadId: string): Promise<'bot' | 'human'>;
51
+ claim(threadId: string, by: string): Promise<void>;
52
+ release(threadId: string): Promise<void>;
53
+ private ensureTable;
54
+ }
55
+ export interface ScheduleHost {
56
+ schedule<T = JsonValue>(when: Date | string | number, callback: string, payload?: T, options?: {
57
+ idempotent?: boolean;
58
+ }): Promise<{
59
+ id: string;
60
+ }>;
61
+ cancelSchedule(id: string): Promise<void>;
62
+ }
63
+ export declare class AgentScheduleCoalesceScheduler implements CoalesceScheduler {
64
+ private readonly sql;
65
+ private readonly host;
66
+ private readonly callback;
67
+ private initialized;
68
+ constructor(sql: SqlExecutor, host: ScheduleHost, callback: string);
69
+ arm(key: ConversationKey, atMs: number): Promise<void>;
70
+ cancel(key: ConversationKey): Promise<void>;
71
+ private ensureTable;
72
+ }
73
+ export declare class RuntimeTurnRunner implements TurnRunner {
74
+ private readonly runtime;
75
+ constructor(runtime: RuntimeLike);
76
+ runTurn(args: Parameters<TurnRunner['runTurn']>[0]): Promise<TurnResult>;
77
+ deliverSignal(args: Parameters<TurnRunner['deliverSignal']>[0]): Promise<TurnResult>;
78
+ }
79
+ export declare class QueuedTurnRunner implements TurnRunner {
80
+ private readonly inner;
81
+ private readonly queue;
82
+ private readonly messageConcurrency;
83
+ private debounceTimer;
84
+ private pendingDebounced;
85
+ constructor(inner: TurnRunner, queue?: TurnQueue, messageConcurrency?: MessageConcurrency);
86
+ runTurn(args: Parameters<TurnRunner['runTurn']>[0]): Promise<TurnResult>;
87
+ deliverSignal(args: Parameters<TurnRunner['deliverSignal']>[0]): Promise<TurnResult>;
88
+ concurrency(): MessageConcurrency;
89
+ waitForIdle(): Promise<void>;
90
+ private runDebounced;
91
+ private flushDebounced;
92
+ }
93
+ export interface DurableObjectInboundRuntimeOptions {
94
+ sql: SqlExecutor;
95
+ runtime: RuntimeLike;
96
+ media: MediaResolver;
97
+ sender: OutboundSender;
98
+ queue?: TurnQueue;
99
+ messageConcurrency?: MessageConcurrency;
100
+ scheduler?: CoalesceScheduler;
101
+ clock?: Clock;
102
+ window?: WindowStore;
103
+ consent?: ConsentStore;
104
+ ownership?: OwnershipStore;
105
+ }
106
+ export declare function createDurableObjectInboundRuntime(options: DurableObjectInboundRuntimeOptions): InboundRuntime;
107
+ export declare function eventSeqFromSql(event: InboundEvent): number | undefined;
108
+ export {};
@@ -0,0 +1,490 @@
1
+ import { mergeUserInputContents } from '@kuralle-agents/core';
2
+ import { conversationKeyToString, eventSeq, noopCoalesceScheduler, systemClock, } from '@kuralle-agents/messaging';
3
+ import { TurnQueue } from 'agents/chat';
4
+ const ledgerSeq = Symbol.for('@kuralle-agents/messaging/inbound-seq');
5
+ function nowIso() {
6
+ return new Date().toISOString();
7
+ }
8
+ function claimStatus(value) {
9
+ return value === 'in_progress' || value === 'complete' ? value : undefined;
10
+ }
11
+ function toNumber(value) {
12
+ if (typeof value === 'number')
13
+ return value;
14
+ if (typeof value === 'bigint')
15
+ return Number(value);
16
+ if (typeof value === 'string')
17
+ return Number(value);
18
+ return 0;
19
+ }
20
+ function encodeEvent(event) {
21
+ return JSON.stringify(event);
22
+ }
23
+ function decodeEvent(row) {
24
+ const event = JSON.parse(row.event_json);
25
+ if (event.kind === 'message') {
26
+ event.data.timestamp = new Date(event.data.timestamp);
27
+ }
28
+ else if (event.kind === 'status') {
29
+ event.data.timestamp = new Date(event.data.timestamp);
30
+ if (event.data.conversation?.expirationTimestamp) {
31
+ event.data.conversation.expirationTimestamp = new Date(event.data.conversation.expirationTimestamp);
32
+ }
33
+ }
34
+ const sequenced = Object.assign({}, event);
35
+ Object.defineProperty(sequenced, ledgerSeq, {
36
+ value: row.seq,
37
+ enumerable: false,
38
+ });
39
+ return sequenced;
40
+ }
41
+ function defaultSessionId(key) {
42
+ return conversationKeyToString(key);
43
+ }
44
+ async function collectParts(stream) {
45
+ const parts = [];
46
+ for await (const part of stream)
47
+ parts.push(part);
48
+ return parts;
49
+ }
50
+ function turnResult(parts) {
51
+ const paused = parts.find((part) => part.type === 'paused');
52
+ return {
53
+ parts,
54
+ suspended: paused ? { signalId: paused.waitingFor } : undefined,
55
+ handoffToHuman: parts.some((part) => part.type === 'handoff' && part.targetAgent === 'human'),
56
+ };
57
+ }
58
+ export class SqlInboundLedger {
59
+ sql;
60
+ options;
61
+ initialized = false;
62
+ constructor(sql, options = {}) {
63
+ this.sql = sql;
64
+ this.options = options;
65
+ }
66
+ async claim(key, eventId) {
67
+ this.ensureTables();
68
+ const convKey = conversationKeyToString(key);
69
+ const claimedAt = Date.now();
70
+ this.sql `
71
+ INSERT OR IGNORE INTO kuralle_inbound_claims
72
+ (conv_key, event_id, status, claimed_at)
73
+ VALUES
74
+ (${convKey}, ${eventId}, ${'in_progress'}, ${claimedAt})
75
+ `;
76
+ const inserted = this.sql `SELECT changes() AS changed`;
77
+ if (toNumber(inserted[0]?.changed) > 0)
78
+ return 'claimed';
79
+ const rows = this.sql `
80
+ SELECT status, claimed_at FROM kuralle_inbound_claims
81
+ WHERE conv_key = ${convKey} AND event_id = ${eventId}
82
+ `;
83
+ const status = claimStatus(rows[0]?.status);
84
+ if (status === 'complete')
85
+ return 'duplicate';
86
+ const staleAfterMs = this.options.inProgressTtlMs;
87
+ if (status === 'in_progress' &&
88
+ staleAfterMs !== undefined &&
89
+ claimedAt - toNumber(rows[0]?.claimed_at) >= staleAfterMs) {
90
+ this.sql `
91
+ UPDATE kuralle_inbound_claims
92
+ SET claimed_at = ${claimedAt}
93
+ WHERE conv_key = ${convKey} AND event_id = ${eventId}
94
+ `;
95
+ return 'claimed';
96
+ }
97
+ return status === 'in_progress' ? 'in_progress' : 'claimed';
98
+ }
99
+ async complete(key, eventId) {
100
+ this.ensureTables();
101
+ const convKey = conversationKeyToString(key);
102
+ this.sql `
103
+ UPDATE kuralle_inbound_claims
104
+ SET status = ${'complete'}, completed_at = ${Date.now()}
105
+ WHERE conv_key = ${convKey} AND event_id = ${eventId}
106
+ `;
107
+ }
108
+ async append(key, event) {
109
+ this.ensureTables();
110
+ const convKey = conversationKeyToString(key);
111
+ this.sql `
112
+ INSERT OR IGNORE INTO kuralle_inbound_events
113
+ (conv_key, event_id, ts, event_json, appended_at)
114
+ VALUES
115
+ (${convKey}, ${event.id}, ${event.ts}, ${encodeEvent(event)}, ${Date.now()})
116
+ `;
117
+ const rows = this.sql `
118
+ SELECT seq FROM kuralle_inbound_events
119
+ WHERE conv_key = ${convKey} AND event_id = ${event.id}
120
+ `;
121
+ return { seq: toNumber(rows[0]?.seq) };
122
+ }
123
+ async readUnprocessed(key) {
124
+ this.ensureTables();
125
+ const convKey = conversationKeyToString(key);
126
+ this.ensureCursor(convKey);
127
+ const rows = this.sql `
128
+ SELECT e.seq, e.event_json
129
+ FROM kuralle_inbound_events e
130
+ JOIN kuralle_inbound_cursors c ON c.conv_key = e.conv_key
131
+ WHERE e.conv_key = ${convKey} AND e.seq > c.cursor
132
+ ORDER BY e.ts ASC, e.seq ASC
133
+ `;
134
+ return rows.map((row) => decodeEvent({ event_json: row.event_json, seq: toNumber(row.seq) }));
135
+ }
136
+ async commitCursor(key, throughSeq, expect) {
137
+ this.ensureTables();
138
+ const convKey = conversationKeyToString(key);
139
+ this.ensureCursor(convKey);
140
+ this.sql `
141
+ UPDATE kuralle_inbound_cursors
142
+ SET cursor = ${Math.max(throughSeq, expect)}, updated_at = ${Date.now()}
143
+ WHERE conv_key = ${convKey} AND cursor = ${expect}
144
+ `;
145
+ const rows = this.sql `SELECT changes() AS changed`;
146
+ return toNumber(rows[0]?.changed) > 0;
147
+ }
148
+ async prune(key, ttlMs) {
149
+ this.ensureTables();
150
+ const convKey = conversationKeyToString(key);
151
+ this.ensureCursor(convKey);
152
+ const cutoff = Date.now() - ttlMs;
153
+ this.sql `
154
+ DELETE FROM kuralle_inbound_events
155
+ WHERE conv_key = ${convKey}
156
+ AND appended_at < ${cutoff}
157
+ AND seq <= (SELECT cursor FROM kuralle_inbound_cursors WHERE conv_key = ${convKey})
158
+ `;
159
+ const rows = this.sql `SELECT changes() AS changed`;
160
+ return toNumber(rows[0]?.changed);
161
+ }
162
+ cursor(key) {
163
+ this.ensureTables();
164
+ const convKey = conversationKeyToString(key);
165
+ this.ensureCursor(convKey);
166
+ const rows = this.sql `
167
+ SELECT cursor FROM kuralle_inbound_cursors WHERE conv_key = ${convKey}
168
+ `;
169
+ return toNumber(rows[0]?.cursor);
170
+ }
171
+ ensureTables() {
172
+ if (this.initialized)
173
+ return;
174
+ this.sql `
175
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_claims (
176
+ conv_key TEXT NOT NULL,
177
+ event_id TEXT NOT NULL,
178
+ status TEXT NOT NULL CHECK(status IN ('in_progress', 'complete')),
179
+ claimed_at INTEGER NOT NULL,
180
+ completed_at INTEGER,
181
+ PRIMARY KEY (conv_key, event_id)
182
+ )
183
+ `;
184
+ this.sql `
185
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_events (
186
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
187
+ conv_key TEXT NOT NULL,
188
+ event_id TEXT NOT NULL,
189
+ ts INTEGER NOT NULL,
190
+ event_json TEXT NOT NULL,
191
+ appended_at INTEGER NOT NULL,
192
+ UNIQUE (conv_key, event_id)
193
+ )
194
+ `;
195
+ this.sql `
196
+ CREATE INDEX IF NOT EXISTS idx_kuralle_inbound_events_read
197
+ ON kuralle_inbound_events (conv_key, ts, seq)
198
+ `;
199
+ this.sql `
200
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_cursors (
201
+ conv_key TEXT PRIMARY KEY,
202
+ cursor INTEGER NOT NULL DEFAULT 0,
203
+ updated_at INTEGER NOT NULL
204
+ )
205
+ `;
206
+ this.initialized = true;
207
+ }
208
+ ensureCursor(convKey) {
209
+ this.sql `
210
+ INSERT OR IGNORE INTO kuralle_inbound_cursors (conv_key, cursor, updated_at)
211
+ VALUES (${convKey}, ${0}, ${Date.now()})
212
+ `;
213
+ }
214
+ }
215
+ export class SqlWindowStore {
216
+ sql;
217
+ initialized = false;
218
+ constructor(sql) {
219
+ this.sql = sql;
220
+ }
221
+ async get(threadId) {
222
+ this.ensureTable();
223
+ const rows = this.sql `
224
+ SELECT expires_at FROM kuralle_inbound_windows WHERE thread_id = ${threadId}
225
+ `;
226
+ const expiresAt = rows[0]?.expires_at ? new Date(rows[0].expires_at) : null;
227
+ if (!expiresAt)
228
+ return { open: false, expiresAt: null };
229
+ return expiresAt > new Date() ? { open: true, expiresAt } : { open: false, expiresAt };
230
+ }
231
+ async recordInbound(threadId, ts) {
232
+ const expiresAt = new Date(ts.getTime() + 24 * 60 * 60 * 1000);
233
+ await this.recordExpiry(threadId, expiresAt);
234
+ }
235
+ async recordExpiry(threadId, at) {
236
+ this.ensureTable();
237
+ this.sql `
238
+ INSERT INTO kuralle_inbound_windows (thread_id, expires_at, updated_at)
239
+ VALUES (${threadId}, ${at.toISOString()}, ${nowIso()})
240
+ ON CONFLICT(thread_id) DO UPDATE SET
241
+ expires_at = excluded.expires_at,
242
+ updated_at = excluded.updated_at
243
+ `;
244
+ }
245
+ ensureTable() {
246
+ if (this.initialized)
247
+ return;
248
+ this.sql `
249
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_windows (
250
+ thread_id TEXT PRIMARY KEY,
251
+ expires_at TEXT,
252
+ updated_at TEXT NOT NULL
253
+ )
254
+ `;
255
+ this.initialized = true;
256
+ }
257
+ }
258
+ export class SqlConsentStore {
259
+ sql;
260
+ initialized = false;
261
+ constructor(sql) {
262
+ this.sql = sql;
263
+ }
264
+ async isOptedIn(customerId) {
265
+ this.ensureTable();
266
+ const rows = this.sql `
267
+ SELECT opted_in FROM kuralle_inbound_consent WHERE customer_id = ${customerId}
268
+ `;
269
+ return rows.length === 0 || toNumber(rows[0].opted_in) === 1;
270
+ }
271
+ async optOut(customerId) {
272
+ this.ensureTable();
273
+ this.upsert(customerId, false);
274
+ }
275
+ async optIn(customerId) {
276
+ this.ensureTable();
277
+ this.upsert(customerId, true);
278
+ }
279
+ upsert(customerId, optedIn) {
280
+ this.sql `
281
+ INSERT INTO kuralle_inbound_consent (customer_id, opted_in, updated_at)
282
+ VALUES (${customerId}, ${optedIn ? 1 : 0}, ${nowIso()})
283
+ ON CONFLICT(customer_id) DO UPDATE SET
284
+ opted_in = excluded.opted_in,
285
+ updated_at = excluded.updated_at
286
+ `;
287
+ }
288
+ ensureTable() {
289
+ if (this.initialized)
290
+ return;
291
+ this.sql `
292
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_consent (
293
+ customer_id TEXT PRIMARY KEY,
294
+ opted_in INTEGER NOT NULL,
295
+ updated_at TEXT NOT NULL
296
+ )
297
+ `;
298
+ this.initialized = true;
299
+ }
300
+ }
301
+ export class SqlOwnershipStore {
302
+ sql;
303
+ initialized = false;
304
+ constructor(sql) {
305
+ this.sql = sql;
306
+ }
307
+ async owner(threadId) {
308
+ this.ensureTable();
309
+ const rows = this.sql `
310
+ SELECT owner FROM kuralle_inbound_ownership WHERE thread_id = ${threadId}
311
+ `;
312
+ return rows[0]?.owner === 'human' ? 'human' : 'bot';
313
+ }
314
+ async claim(threadId, by) {
315
+ this.ensureTable();
316
+ const owner = by === 'human' ? 'human' : 'bot';
317
+ this.sql `
318
+ INSERT INTO kuralle_inbound_ownership (thread_id, owner, updated_at)
319
+ VALUES (${threadId}, ${owner}, ${nowIso()})
320
+ ON CONFLICT(thread_id) DO UPDATE SET
321
+ owner = excluded.owner,
322
+ updated_at = excluded.updated_at
323
+ `;
324
+ }
325
+ async release(threadId) {
326
+ this.ensureTable();
327
+ this.sql `DELETE FROM kuralle_inbound_ownership WHERE thread_id = ${threadId}`;
328
+ }
329
+ ensureTable() {
330
+ if (this.initialized)
331
+ return;
332
+ this.sql `
333
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_ownership (
334
+ thread_id TEXT PRIMARY KEY,
335
+ owner TEXT NOT NULL CHECK(owner IN ('bot', 'human')),
336
+ updated_at TEXT NOT NULL
337
+ )
338
+ `;
339
+ this.initialized = true;
340
+ }
341
+ }
342
+ export class AgentScheduleCoalesceScheduler {
343
+ sql;
344
+ host;
345
+ callback;
346
+ initialized = false;
347
+ constructor(sql, host, callback) {
348
+ this.sql = sql;
349
+ this.host = host;
350
+ this.callback = callback;
351
+ }
352
+ async arm(key, atMs) {
353
+ this.ensureTable();
354
+ const convKey = conversationKeyToString(key);
355
+ const existing = this.sql `
356
+ SELECT schedule_id FROM kuralle_inbound_schedule_refs WHERE conv_key = ${convKey}
357
+ `;
358
+ if (existing[0]?.schedule_id) {
359
+ await this.host.cancelSchedule(existing[0].schedule_id);
360
+ }
361
+ const schedule = await this.host.schedule(new Date(atMs), this.callback, { key }, { idempotent: true });
362
+ this.sql `
363
+ INSERT INTO kuralle_inbound_schedule_refs (conv_key, schedule_id, updated_at)
364
+ VALUES (${convKey}, ${schedule.id}, ${Date.now()})
365
+ ON CONFLICT(conv_key) DO UPDATE SET
366
+ schedule_id = excluded.schedule_id,
367
+ updated_at = excluded.updated_at
368
+ `;
369
+ }
370
+ async cancel(key) {
371
+ this.ensureTable();
372
+ const convKey = conversationKeyToString(key);
373
+ const existing = this.sql `
374
+ SELECT schedule_id FROM kuralle_inbound_schedule_refs WHERE conv_key = ${convKey}
375
+ `;
376
+ if (existing[0]?.schedule_id) {
377
+ await this.host.cancelSchedule(existing[0].schedule_id);
378
+ }
379
+ this.sql `DELETE FROM kuralle_inbound_schedule_refs WHERE conv_key = ${convKey}`;
380
+ }
381
+ ensureTable() {
382
+ if (this.initialized)
383
+ return;
384
+ this.sql `
385
+ CREATE TABLE IF NOT EXISTS kuralle_inbound_schedule_refs (
386
+ conv_key TEXT PRIMARY KEY,
387
+ schedule_id TEXT NOT NULL,
388
+ updated_at INTEGER NOT NULL
389
+ )
390
+ `;
391
+ this.initialized = true;
392
+ }
393
+ }
394
+ export class RuntimeTurnRunner {
395
+ runtime;
396
+ constructor(runtime) {
397
+ this.runtime = runtime;
398
+ }
399
+ async runTurn(args) {
400
+ const handle = this.runtime.run({
401
+ input: args.input,
402
+ selection: args.selection,
403
+ sessionId: args.sessionId ?? defaultSessionId(args.key),
404
+ userId: args.userId,
405
+ abortSignal: args.signal,
406
+ });
407
+ return turnResult(await collectParts(handle.events));
408
+ }
409
+ async deliverSignal(args) {
410
+ const handle = this.runtime.run({
411
+ sessionId: args.sessionId ?? defaultSessionId(args.key),
412
+ signalDelivery: args.signal,
413
+ abortSignal: args.signal2,
414
+ });
415
+ return turnResult(await collectParts(handle.events));
416
+ }
417
+ }
418
+ export class QueuedTurnRunner {
419
+ inner;
420
+ queue;
421
+ messageConcurrency;
422
+ debounceTimer;
423
+ pendingDebounced = [];
424
+ constructor(inner, queue = new TurnQueue(), messageConcurrency = 'queue') {
425
+ this.inner = inner;
426
+ this.queue = queue;
427
+ this.messageConcurrency = messageConcurrency;
428
+ }
429
+ async runTurn(args) {
430
+ if (typeof this.messageConcurrency === 'object' && this.messageConcurrency.strategy === 'debounce') {
431
+ return this.runDebounced(args, this.messageConcurrency.debounceMs ?? 750);
432
+ }
433
+ const result = await this.queue.enqueue(`${conversationKeyToString(args.key)}:${crypto.randomUUID()}`, async () => this.inner.runTurn(args));
434
+ return result.status === 'completed' ? result.value : { parts: [] };
435
+ }
436
+ async deliverSignal(args) {
437
+ const result = await this.queue.enqueue(`${conversationKeyToString(args.key)}:${args.signal.signalId}`, async () => this.inner.deliverSignal(args));
438
+ return result.status === 'completed' ? result.value : { parts: [] };
439
+ }
440
+ concurrency() {
441
+ return this.messageConcurrency;
442
+ }
443
+ waitForIdle() {
444
+ return this.queue.waitForIdle();
445
+ }
446
+ runDebounced(args, debounceMs) {
447
+ return new Promise((resolve, reject) => {
448
+ this.pendingDebounced.push({ args, resolve, reject });
449
+ if (this.debounceTimer)
450
+ clearTimeout(this.debounceTimer);
451
+ this.debounceTimer = setTimeout(() => {
452
+ this.debounceTimer = undefined;
453
+ void this.flushDebounced();
454
+ }, debounceMs);
455
+ });
456
+ }
457
+ async flushDebounced() {
458
+ const pending = this.pendingDebounced.splice(0);
459
+ if (pending.length === 0)
460
+ return;
461
+ const latest = pending[pending.length - 1];
462
+ const input = mergeUserInputContents(pending.map((item) => item.args.input)) ?? latest.args.input;
463
+ for (const item of pending.slice(0, -1))
464
+ item.resolve({ parts: [] });
465
+ try {
466
+ const result = await this.queue.enqueue(`${conversationKeyToString(latest.args.key)}:${crypto.randomUUID()}`, async () => this.inner.runTurn({ ...latest.args, input }));
467
+ latest.resolve(result.status === 'completed' ? result.value : { parts: [] });
468
+ }
469
+ catch (error) {
470
+ latest.reject(error);
471
+ }
472
+ }
473
+ }
474
+ export function createDurableObjectInboundRuntime(options) {
475
+ const baseRunner = new RuntimeTurnRunner(options.runtime);
476
+ return {
477
+ ledger: new SqlInboundLedger(options.sql),
478
+ window: options.window ?? new SqlWindowStore(options.sql),
479
+ consent: options.consent ?? new SqlConsentStore(options.sql),
480
+ ownership: options.ownership ?? new SqlOwnershipStore(options.sql),
481
+ media: options.media,
482
+ sender: options.sender,
483
+ runtime: new QueuedTurnRunner(baseRunner, options.queue, options.messageConcurrency),
484
+ scheduler: options.scheduler ?? noopCoalesceScheduler,
485
+ clock: options.clock ?? systemClock,
486
+ };
487
+ }
488
+ export function eventSeqFromSql(event) {
489
+ return eventSeq(event);
490
+ }
package/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export { SqlPersistentMemoryStore } from './SqlPersistentMemoryStore.js';
30
30
  export { createSqlExecutor } from './sqlExecutor.js';
31
31
  export { createSSEResponse } from './StreamAdapter.js';
32
32
  export { lastUserInputFromMessages } from './cfMessageInput.js';
33
+ export { AgentScheduleCoalesceScheduler, QueuedTurnRunner, RuntimeTurnRunner, SqlConsentStore, SqlInboundLedger, SqlOwnershipStore, SqlWindowStore, createDurableObjectInboundRuntime, eventSeqFromSql, } from './inbound-runtime.js';
33
34
  export type { StreamAdapterConfig, OrchestrationState, SqlExecutor, } from './types.js';
35
+ export type { DurableObjectInboundRuntimeOptions, ScheduleHost, } from './inbound-runtime.js';
34
36
  export { DEFAULT_STREAM_CONFIG } from './types.js';
35
37
  export type { HarnessConfig, HarnessHooks, HarnessStreamPart, Session, } from '@kuralle-agents/core';
package/dist/index.js CHANGED
@@ -30,4 +30,5 @@ export { SqlPersistentMemoryStore } from './SqlPersistentMemoryStore.js';
30
30
  export { createSqlExecutor } from './sqlExecutor.js';
31
31
  export { createSSEResponse } from './StreamAdapter.js';
32
32
  export { lastUserInputFromMessages } from './cfMessageInput.js';
33
+ export { AgentScheduleCoalesceScheduler, QueuedTurnRunner, RuntimeTurnRunner, SqlConsentStore, SqlInboundLedger, SqlOwnershipStore, SqlWindowStore, createDurableObjectInboundRuntime, eventSeqFromSql, } from './inbound-runtime.js';
33
34
  export { DEFAULT_STREAM_CONFIG } from './types.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuralle-agents/cf-agent",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Kuralle agent integration for Cloudflare Workers with AIChatAgent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,20 +21,21 @@
21
21
  "dependencies": {
22
22
  "@cloudflare/ai-chat": "^0.8.4",
23
23
  "ai": "^6.0.90",
24
- "@kuralle-agents/core": "0.10.0",
25
- "@kuralle-agents/realtime-audio": "0.10.0"
24
+ "@kuralle-agents/core": "0.11.0",
25
+ "@kuralle-agents/messaging": "0.11.0",
26
+ "@kuralle-agents/realtime-audio": "0.11.0"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "agents": ">=0.14.0 <1.0.0",
29
30
  "zod": "^4.0.0",
30
- "@kuralle-agents/voice-protocol": "0.10.0"
31
+ "@kuralle-agents/voice-protocol": "0.11.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@cloudflare/vitest-pool-workers": "^0.12.7",
34
35
  "agents": "^0.15.0",
35
36
  "typescript": "^5.7.0",
36
37
  "vitest": "^3.2.4",
37
- "@kuralle-agents/voice-protocol": "0.10.0"
38
+ "@kuralle-agents/voice-protocol": "0.11.0"
38
39
  },
39
40
  "engines": {
40
41
  "node": ">=20"