@economic/agents 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +323 -429
- package/bin/cli.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +561 -0
- package/dist/hono.d.mts +21 -0
- package/dist/hono.mjs +71 -0
- package/dist/index.d.mts +124 -46
- package/dist/index.mjs +280 -117
- package/package.json +20 -8
- package/schema/agent.sql +12 -0
- package/schema/{schema.sql → chat.sql} +1 -5
- package/dist/react.d.mts +0 -25
- package/dist/react.mjs +0 -38
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { callable } from "agents";
|
|
2
|
-
import { AIChatAgent
|
|
3
|
-
import { Output, convertToModelMessages, generateText, jsonSchema, stepCountIs, tool } from "ai";
|
|
1
|
+
import { Agent as Agent$1, callable } from "agents";
|
|
2
|
+
import { AIChatAgent } from "@cloudflare/ai-chat";
|
|
3
|
+
import { Output, convertToModelMessages, generateText, jsonSchema, stepCountIs, streamText, tool } from "ai";
|
|
4
4
|
//#region src/server/features/skills/index.ts
|
|
5
5
|
const TOOL_NAME_ACTIVATE_SKILL = "activate_skill";
|
|
6
6
|
const TOOL_NAME_LIST_CAPABILITIES = "list_capabilities";
|
|
@@ -237,8 +237,8 @@ function buildLLMParams(config) {
|
|
|
237
237
|
...rest,
|
|
238
238
|
system,
|
|
239
239
|
tools,
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
stopWhen: rest.stopWhen ?? stepCountIs(20),
|
|
241
|
+
experimental_transform: composedTransform
|
|
242
242
|
};
|
|
243
243
|
if (!skills?.length) return baseParams;
|
|
244
244
|
const skillsCtx = createSkills({
|
|
@@ -262,16 +262,168 @@ function buildLLMParams(config) {
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
//#endregion
|
|
265
|
-
//#region src/server/features/audit/
|
|
265
|
+
//#region src/server/features/audit/audit.ts
|
|
266
266
|
/**
|
|
267
267
|
* Inserts a single audit event row into the shared `audit_events` D1 table.
|
|
268
268
|
*
|
|
269
|
-
* Called by `
|
|
269
|
+
* Called by `ChatAgentHarness.logEvent()` (and `AgentHarness.logEvent()`). Not intended for direct use.
|
|
270
270
|
*/
|
|
271
|
+
const SENSITIVE_KEYS = /^(password|token|secret|api_key|apikey|authorization|credentials)$/i;
|
|
272
|
+
const REDACTED = "[REDACTED]";
|
|
273
|
+
/** Deep-clone and redact values for keys that look like secrets (for audit logging). */
|
|
274
|
+
function sanitizePayload(value) {
|
|
275
|
+
if (value === null || value === void 0) return value;
|
|
276
|
+
if (Array.isArray(value)) return value.map(sanitizePayload);
|
|
277
|
+
if (typeof value === "object") {
|
|
278
|
+
const result = {};
|
|
279
|
+
for (const [key, val] of Object.entries(value)) result[key] = SENSITIVE_KEYS.test(key) ? REDACTED : sanitizePayload(val);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
271
284
|
async function insertAuditEvent(db, durableObjectName, message, payload) {
|
|
272
285
|
await db.prepare(`INSERT INTO audit_events (id, durable_object_name, message, payload, created_at)
|
|
273
|
-
VALUES (?, ?, ?, ?, ?)`).bind(crypto.randomUUID(), durableObjectName, message, payload ? JSON.stringify(payload) : null, (/* @__PURE__ */ new Date()).toISOString()).run();
|
|
286
|
+
VALUES (?, ?, ?, ?, ?)`).bind(crypto.randomUUID(), durableObjectName, message, payload ? JSON.stringify(sanitizePayload(payload)) : null, (/* @__PURE__ */ new Date()).toISOString()).run();
|
|
287
|
+
}
|
|
288
|
+
function stringifyForSkillScan(output) {
|
|
289
|
+
if (typeof output === "string") return output;
|
|
290
|
+
if (output === null || output === void 0) return "";
|
|
291
|
+
try {
|
|
292
|
+
return JSON.stringify(output);
|
|
293
|
+
} catch {
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function forEachStep(event, fn) {
|
|
298
|
+
if (event.steps.length > 0) for (const step of event.steps) fn(step);
|
|
299
|
+
else fn(event);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* User text from provider request body (`contents` is e.g. Gemini format).
|
|
303
|
+
*/
|
|
304
|
+
function extractUserInputFromRequestBody(body) {
|
|
305
|
+
const firstContent = body?.contents?.[0];
|
|
306
|
+
if (firstContent?.role !== "user" || !firstContent.parts?.length) return "";
|
|
307
|
+
return firstContent.parts.map((p) => p.text).filter((t) => typeof t === "string").join(" ").trim();
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Builds the payload for a "Turn completed" audit event from the AI SDK
|
|
311
|
+
* `OnFinishEvent`.
|
|
312
|
+
*
|
|
313
|
+
* Returns:
|
|
314
|
+
* - `input`: user message text from `request.body.contents[0]` (Gemini-style)
|
|
315
|
+
* - `output`: assistant response text (truncated to 200 chars)
|
|
316
|
+
* - `toolCalls`: array of { toolName, toolInput, toolOutput }
|
|
317
|
+
* - `loadedSkills`: skill names extracted from activate_skill results
|
|
318
|
+
*/
|
|
319
|
+
function buildTurnLogPayload(event) {
|
|
320
|
+
const toolCalls = [];
|
|
321
|
+
let latestSkills;
|
|
322
|
+
const toolOutputs = /* @__PURE__ */ new Map();
|
|
323
|
+
forEachStep(event, (step) => {
|
|
324
|
+
for (const tr of step.toolResults) toolOutputs.set(tr.toolCallId, tr.output);
|
|
325
|
+
});
|
|
326
|
+
forEachStep(event, (step) => {
|
|
327
|
+
for (const tc of step.toolCalls) toolCalls.push({
|
|
328
|
+
name: tc.toolName,
|
|
329
|
+
input: tc.input,
|
|
330
|
+
output: toolOutputs.get(tc.toolCallId)
|
|
331
|
+
});
|
|
332
|
+
const considerToolResultForSkills = (toolName, output) => {
|
|
333
|
+
if (toolName !== "activate_skill") return;
|
|
334
|
+
const s = stringifyForSkillScan(output);
|
|
335
|
+
const sentinelIdx = s.indexOf(SKILL_STATE_SENTINEL);
|
|
336
|
+
if (sentinelIdx === -1) return;
|
|
337
|
+
try {
|
|
338
|
+
const stateJson = s.slice(sentinelIdx + 18);
|
|
339
|
+
latestSkills = JSON.parse(stateJson);
|
|
340
|
+
} catch {}
|
|
341
|
+
};
|
|
342
|
+
for (const tr of step.toolResults) considerToolResultForSkills(tr.toolName, tr.output);
|
|
343
|
+
});
|
|
344
|
+
const input = extractUserInputFromRequestBody(event.request?.body);
|
|
345
|
+
return {
|
|
346
|
+
detail: {
|
|
347
|
+
model: event.model.modelId,
|
|
348
|
+
tokens: event.usage?.totalTokens
|
|
349
|
+
},
|
|
350
|
+
loadedSkills: latestSkills ?? [],
|
|
351
|
+
toolCalls,
|
|
352
|
+
input: input.slice(0, 200),
|
|
353
|
+
output: (event.text ?? "").slice(0, 200)
|
|
354
|
+
};
|
|
274
355
|
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/server/agents/Agent.ts
|
|
358
|
+
/**
|
|
359
|
+
* Base agent for Cloudflare Agents SDK Durable Objects with lazy skill loading,
|
|
360
|
+
* audit logging, and `buildLLMParams` wiring.
|
|
361
|
+
*
|
|
362
|
+
* Handles CF infrastructure concerns: DO SQLite persistence for loaded skill state
|
|
363
|
+
* and writing audit events to D1.
|
|
364
|
+
*
|
|
365
|
+
* For chat agents with message history, compaction, and conversation recording,
|
|
366
|
+
* extend {@link ChatAgent} instead.
|
|
367
|
+
*/
|
|
368
|
+
var Agent = class extends Agent$1 {
|
|
369
|
+
/**
|
|
370
|
+
* Returns the user ID from the durable object name.
|
|
371
|
+
*/
|
|
372
|
+
getUserId() {
|
|
373
|
+
return this.name.split(":")[0];
|
|
374
|
+
}
|
|
375
|
+
async onConnect(connection, ctx) {
|
|
376
|
+
if (!this.env.AGENT_DB) {
|
|
377
|
+
console.error("[Agent] Connection rejected: no AGENT_DB bound");
|
|
378
|
+
connection.close(3e3, "Could not connect to agent, database not found");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!this.getUserId()) {
|
|
382
|
+
console.error("[Agent] Connection rejected: name must be in the format userId:uniqueChatId");
|
|
383
|
+
connection.close(3e3, "Could not connect to agent, name is not in correct format");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
return super.onConnect(connection, ctx);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
|
|
390
|
+
* otherwise silently does nothing.
|
|
391
|
+
*
|
|
392
|
+
* Called automatically at the end of each LLM turn (from `onFinish` in
|
|
393
|
+
* `buildLLMParams`). Also available via `experimental_context.logEvent` in tool
|
|
394
|
+
* `execute` functions.
|
|
395
|
+
*/
|
|
396
|
+
async logEvent(message, payload) {
|
|
397
|
+
try {
|
|
398
|
+
await insertAuditEvent(this.env.AGENT_DB, this.name, message, payload);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error("[Agent] Failed to write audit event", error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Builds the parameter object for a `streamText` or `generateText` call,
|
|
405
|
+
* pre-filling `activeSkills` from this agent instance.
|
|
406
|
+
* Injects `logEvent` into `experimental_context` and wires `onFinish` for
|
|
407
|
+
* turn-completed audit events.
|
|
408
|
+
*/
|
|
409
|
+
async buildLLMParams(config) {
|
|
410
|
+
const activeSkills = await getStoredSkills(this.sql.bind(this));
|
|
411
|
+
const experimental_context = {
|
|
412
|
+
...config.options?.body,
|
|
413
|
+
logEvent: this.logEvent.bind(this)
|
|
414
|
+
};
|
|
415
|
+
const onFinish = async (event) => {
|
|
416
|
+
this.logEvent("Turn completed", buildTurnLogPayload(event));
|
|
417
|
+
await config.onFinish?.(event);
|
|
418
|
+
};
|
|
419
|
+
return buildLLMParams({
|
|
420
|
+
...config,
|
|
421
|
+
activeSkills,
|
|
422
|
+
experimental_context,
|
|
423
|
+
onFinish
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
};
|
|
275
427
|
const TOOL_RESULT_PREVIEW_CHARS = 200;
|
|
276
428
|
const SUMMARY_MAX_TOKENS = 4e3;
|
|
277
429
|
/**
|
|
@@ -380,7 +532,7 @@ async function compactIfNeeded(messages, model, tailSize) {
|
|
|
380
532
|
/**
|
|
381
533
|
* Records a conversation row in the `conversations` D1 table.
|
|
382
534
|
*
|
|
383
|
-
* Called by `
|
|
535
|
+
* Called by `ChatAgentHarness` after every turn. On first call for a given
|
|
384
536
|
* `durableObjectName` the row is inserted with `created_at` set to now,
|
|
385
537
|
* and with the provided `title` and `summary` if supplied.
|
|
386
538
|
* On subsequent calls only `updated_at` is refreshed —
|
|
@@ -462,16 +614,20 @@ async function generateTitleAndSummary(messages, model, existingSummary) {
|
|
|
462
614
|
* Only the last `SUMMARY_CONTEXT_MESSAGES` messages are passed to keep the
|
|
463
615
|
* prompt bounded regardless of total conversation length.
|
|
464
616
|
*
|
|
465
|
-
* Called by `
|
|
617
|
+
* Called by `ChatAgentHarness` every `SUMMARY_CONTEXT_MESSAGES` messages after
|
|
466
618
|
* the first turn.
|
|
467
619
|
*/
|
|
468
620
|
async function generateConversationSummary(db, durableObjectName, messages, model) {
|
|
469
621
|
const { title, summary } = await generateTitleAndSummary(messages, model, (await getConversationSummary(db, durableObjectName))?.summary ?? void 0);
|
|
470
622
|
await updateConversationSummary(db, durableObjectName, title, summary);
|
|
623
|
+
return {
|
|
624
|
+
title,
|
|
625
|
+
summary
|
|
626
|
+
};
|
|
471
627
|
}
|
|
472
628
|
//#endregion
|
|
473
629
|
//#region src/server/features/conversations/retention.ts
|
|
474
|
-
const DELETE_CONVERSATION_CALLBACK = "
|
|
630
|
+
const DELETE_CONVERSATION_CALLBACK = "deleteConversationCallback";
|
|
475
631
|
const CONVERSATION_EXPIRED_CLOSE_CODE = 3001;
|
|
476
632
|
const CONVERSATION_EXPIRED_CLOSE_REASON = "Conversation expired due to inactivity.";
|
|
477
633
|
function getConversationRetentionMs(days) {
|
|
@@ -481,27 +637,20 @@ function getConversationRetentionMs(days) {
|
|
|
481
637
|
function getDeleteConversationScheduleIds(schedules) {
|
|
482
638
|
return schedules.filter((schedule) => schedule.callback === DELETE_CONVERSATION_CALLBACK).map((schedule) => schedule.id);
|
|
483
639
|
}
|
|
484
|
-
function clearConversationRuntimeState(state) {
|
|
485
|
-
for (const controller of state.chatMessageAbortControllers?.values() ?? []) controller.abort();
|
|
486
|
-
state.messages.length = 0;
|
|
487
|
-
state.clearResumableStream();
|
|
488
|
-
state.chatMessageAbortControllers?.clear();
|
|
489
|
-
state.pendingResumeConnections?.clear();
|
|
490
|
-
}
|
|
491
640
|
//#endregion
|
|
492
|
-
//#region src/server/agents/
|
|
641
|
+
//#region src/server/agents/ChatAgent.ts
|
|
493
642
|
/**
|
|
494
|
-
*
|
|
495
|
-
* and
|
|
643
|
+
* Chat agent for Cloudflare Agents SDK: lazy skill loading, audit logging,
|
|
644
|
+
* message persistence, compaction, and conversation metadata in D1.
|
|
496
645
|
*
|
|
497
|
-
* Handles CF infrastructure concerns
|
|
498
|
-
*
|
|
499
|
-
*
|
|
646
|
+
* Handles CF infrastructure concerns: DO SQLite for loaded skill state,
|
|
647
|
+
* stripping skill meta-tool messages before persistence, history replay to
|
|
648
|
+
* newly connected clients, and audit events to D1.
|
|
500
649
|
*
|
|
501
|
-
* Skill loading, compaction, and LLM
|
|
502
|
-
*
|
|
650
|
+
* Skill loading, compaction, and LLM calls use `buildLLMParams` from
|
|
651
|
+
* `@economic/agents` inside `onChatMessage`.
|
|
503
652
|
*/
|
|
504
|
-
var
|
|
653
|
+
var ChatAgent = class extends AIChatAgent {
|
|
505
654
|
/**
|
|
506
655
|
* Number of days of inactivity before the full conversation is deleted.
|
|
507
656
|
*
|
|
@@ -524,12 +673,12 @@ var AIChatAgent = class extends AIChatAgent$1 {
|
|
|
524
673
|
}
|
|
525
674
|
async onConnect(connection, ctx) {
|
|
526
675
|
if (!this.env.AGENT_DB) {
|
|
527
|
-
console.error("[
|
|
676
|
+
console.error("[ChatAgent] Connection rejected: no AGENT_DB bound");
|
|
528
677
|
connection.close(3e3, "Could not connect to agent, database not found");
|
|
529
678
|
return;
|
|
530
679
|
}
|
|
531
680
|
if (!this.getUserId()) {
|
|
532
|
-
console.error("[
|
|
681
|
+
console.error("[ChatAgent] Connection rejected: name must be in the format userId:uniqueChatId");
|
|
533
682
|
connection.close(3e3, "Could not connect to agent, name is not in correct format");
|
|
534
683
|
return;
|
|
535
684
|
}
|
|
@@ -539,56 +688,60 @@ var AIChatAgent = class extends AIChatAgent$1 {
|
|
|
539
688
|
* Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
|
|
540
689
|
* otherwise silently does nothing.
|
|
541
690
|
*
|
|
542
|
-
* Called automatically
|
|
543
|
-
*
|
|
544
|
-
* `
|
|
691
|
+
* Called automatically at the end of each LLM turn (from `onFinish` in
|
|
692
|
+
* `buildLLMParams`). Also available via `experimental_context.logEvent` in tool
|
|
693
|
+
* `execute` functions.
|
|
545
694
|
*/
|
|
546
|
-
async
|
|
695
|
+
async logEvent(message, payload) {
|
|
547
696
|
try {
|
|
548
697
|
await insertAuditEvent(this.env.AGENT_DB, this.name, message, payload);
|
|
549
698
|
} catch (error) {
|
|
550
|
-
console.error("[
|
|
699
|
+
console.error("[ChatAgent] Failed to write audit event", error);
|
|
551
700
|
}
|
|
552
701
|
}
|
|
553
702
|
/**
|
|
554
703
|
* Builds the parameter object for a `streamText` or `generateText` call,
|
|
555
704
|
* pre-filling `messages`, `activeSkills`, and `fastModel` from this agent instance.
|
|
556
|
-
* Injects `
|
|
705
|
+
* Injects `logEvent` into `experimental_context` and wires `onFinish` for
|
|
706
|
+
* turn-completed audit events and conversation recording.
|
|
557
707
|
*
|
|
558
708
|
* **Compaction** runs automatically when `fastModel` is set on the class, using
|
|
559
709
|
* `DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION` (30) as the threshold. Override the
|
|
560
710
|
* threshold by setting `maxMessagesBeforeCompaction` on the class. Disable compaction
|
|
561
711
|
* entirely by setting `maxMessagesBeforeCompaction = undefined` explicitly.
|
|
562
|
-
* ```
|
|
563
712
|
*/
|
|
564
713
|
async buildLLMParams(config) {
|
|
565
714
|
const activeSkills = await getStoredSkills(this.sql.bind(this));
|
|
566
|
-
const
|
|
715
|
+
const experimental_context = {
|
|
567
716
|
...config.options?.body,
|
|
568
|
-
|
|
717
|
+
logEvent: this.logEvent.bind(this)
|
|
569
718
|
};
|
|
570
719
|
const messages = await convertToModelMessages(this.messages);
|
|
571
|
-
const
|
|
720
|
+
const fastModel = this.getFastModel();
|
|
721
|
+
const processedMessages = fastModel && this.maxMessagesBeforeCompaction !== void 0 ? await compactIfNeeded(messages, fastModel, this.maxMessagesBeforeCompaction) : messages;
|
|
722
|
+
const onFinish = async (event) => {
|
|
723
|
+
this.logEvent("Turn completed", buildTurnLogPayload(event));
|
|
724
|
+
this.recordConversation(filterEphemeralMessages([...this.messages]));
|
|
725
|
+
await config.onFinish?.(event);
|
|
726
|
+
};
|
|
572
727
|
return buildLLMParams({
|
|
573
728
|
...config,
|
|
574
729
|
activeSkills,
|
|
575
730
|
messages: processedMessages,
|
|
576
|
-
experimental_context
|
|
731
|
+
experimental_context,
|
|
732
|
+
onFinish
|
|
577
733
|
});
|
|
578
734
|
}
|
|
579
735
|
/**
|
|
580
736
|
* Extracts skill state from activate_skill results, persists to DO SQLite,
|
|
581
|
-
*
|
|
582
|
-
* delegating to super.
|
|
737
|
+
* then strips all skill meta-tool messages before delegating to super.
|
|
583
738
|
*
|
|
584
739
|
* 1. Scans activate_skill tool results for SKILL_STATE_SENTINEL. When found,
|
|
585
740
|
* the embedded JSON array of loaded skill names is written to DO SQLite.
|
|
586
741
|
*
|
|
587
|
-
* 2.
|
|
588
|
-
*
|
|
589
|
-
* 3. Strips all activate_skill and list_capabilities messages from history.
|
|
742
|
+
* 2. Strips all activate_skill and list_capabilities messages from history.
|
|
590
743
|
*
|
|
591
|
-
*
|
|
744
|
+
* 3. Delegates to super.persistMessages for message storage and WS broadcast.
|
|
592
745
|
*/
|
|
593
746
|
async persistMessages(messages, excludeBroadcastIds = [], options) {
|
|
594
747
|
let latestSkillState;
|
|
@@ -608,18 +761,48 @@ var AIChatAgent = class extends AIChatAgent$1 {
|
|
|
608
761
|
if (latestSkillState !== void 0) saveStoredSkills(this.sql.bind(this), latestSkillState);
|
|
609
762
|
const filtered = filterEphemeralMessages(messages);
|
|
610
763
|
const result = await super.persistMessages(filtered, excludeBroadcastIds, options);
|
|
611
|
-
this.recordConversation(filtered);
|
|
612
764
|
this.scheduleConversationForDeletion();
|
|
613
|
-
this.log("turn completed", buildTurnSummaryForLog(messages, latestSkillState ?? []));
|
|
614
765
|
return result;
|
|
615
766
|
}
|
|
616
767
|
@callable({ description: "Returns all conversations for the current user" }) async getConversations() {
|
|
617
768
|
return getConversations(this.env.AGENT_DB, this.getUserId());
|
|
618
769
|
}
|
|
770
|
+
@callable({ description: "Delete a conversation by its id" }) async deleteConversation(id) {
|
|
771
|
+
if (!id.startsWith(`${this.getUserId()}:`)) {
|
|
772
|
+
console.error("[ChatAgent] Failed to delete conversation: Not owned by current user", {
|
|
773
|
+
conversationName: id,
|
|
774
|
+
userId: this.getUserId()
|
|
775
|
+
});
|
|
776
|
+
this.logEvent("Failed to delete conversation: Not owned by current user", {
|
|
777
|
+
conversationName: id,
|
|
778
|
+
userId: this.getUserId()
|
|
779
|
+
});
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
await deleteConversationRow(this.env.AGENT_DB, id);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
console.error("[ChatAgent] Failed to delete conversation row", {
|
|
786
|
+
conversationName: id,
|
|
787
|
+
error
|
|
788
|
+
});
|
|
789
|
+
return false;
|
|
790
|
+
}
|
|
791
|
+
await (id === this.name ? this : this.binding.getByName(id)).destroy();
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
async destroy() {
|
|
795
|
+
for (const connection of this.getConnections()) try {
|
|
796
|
+
connection.close(CONVERSATION_EXPIRED_CLOSE_CODE, CONVERSATION_EXPIRED_CLOSE_REASON);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
console.error("[ChatAgent] Failed to close expired conversation connection", error);
|
|
799
|
+
}
|
|
800
|
+
return super.destroy();
|
|
801
|
+
}
|
|
619
802
|
/**
|
|
620
803
|
* Records this conversation in the `conversations` D1 table and triggers
|
|
621
804
|
* LLM-based title/summary generation when appropriate. Called automatically
|
|
622
|
-
* from `
|
|
805
|
+
* from `onFinish` in `buildLLMParams` after each completed LLM turn.
|
|
623
806
|
*
|
|
624
807
|
* On the first turn (no existing row), awaits `generateTitleAndSummary` and
|
|
625
808
|
* inserts the row with title and summary already populated. On subsequent
|
|
@@ -629,35 +812,25 @@ var AIChatAgent = class extends AIChatAgent$1 {
|
|
|
629
812
|
*/
|
|
630
813
|
async recordConversation(messages) {
|
|
631
814
|
if (!await getConversationSummary(this.env.AGENT_DB, this.name)) {
|
|
632
|
-
const { title, summary } = await generateTitleAndSummary(messages, this.
|
|
815
|
+
const { title, summary } = await generateTitleAndSummary(messages, this.getFastModel());
|
|
633
816
|
await recordConversation(this.env.AGENT_DB, this.name, title, summary);
|
|
634
|
-
this.
|
|
817
|
+
this.logEvent("Conversation summary generated", {
|
|
818
|
+
title,
|
|
819
|
+
summary
|
|
820
|
+
});
|
|
635
821
|
} else {
|
|
636
822
|
await recordConversation(this.env.AGENT_DB, this.name);
|
|
637
823
|
if (messages.length % 30 === 0) {
|
|
638
|
-
generateConversationSummary(this.env.AGENT_DB, this.name, messages, this.
|
|
639
|
-
this.
|
|
824
|
+
const { title, summary } = await generateConversationSummary(this.env.AGENT_DB, this.name, messages, this.getFastModel());
|
|
825
|
+
this.logEvent("Conversation summary updated", {
|
|
826
|
+
title,
|
|
827
|
+
summary
|
|
828
|
+
});
|
|
640
829
|
}
|
|
641
830
|
}
|
|
642
831
|
}
|
|
643
|
-
async
|
|
644
|
-
|
|
645
|
-
await deleteConversationRow(this.env.AGENT_DB, this.name);
|
|
646
|
-
} catch (error) {
|
|
647
|
-
console.error("[AIChatAgent] Failed to delete conversation row", {
|
|
648
|
-
conversationName: this.name,
|
|
649
|
-
error
|
|
650
|
-
});
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
for (const connection of this.getConnections()) try {
|
|
654
|
-
connection.close(CONVERSATION_EXPIRED_CLOSE_CODE, CONVERSATION_EXPIRED_CLOSE_REASON);
|
|
655
|
-
} catch (error) {
|
|
656
|
-
console.error("[AIChatAgent] Failed to close expired conversation connection", error);
|
|
657
|
-
}
|
|
658
|
-
this.clearConversationMemoryState();
|
|
659
|
-
await this.ctx.storage.deleteAll();
|
|
660
|
-
this.log("[AiChatAgent] Conversation deleted due to inactivity", {
|
|
832
|
+
async deleteConversationCallback() {
|
|
833
|
+
if (await this.deleteConversation(this.name)) this.logEvent("Conversation deleted due to inactivity", {
|
|
661
834
|
conversationName: this.name,
|
|
662
835
|
retentionDays: this.conversationRetentionDays ?? null
|
|
663
836
|
});
|
|
@@ -669,52 +842,42 @@ var AIChatAgent = class extends AIChatAgent$1 {
|
|
|
669
842
|
await Promise.all(scheduleIds.map((scheduleId) => this.cancelSchedule(scheduleId)));
|
|
670
843
|
await this.schedule(new Date(Date.now() + retentionMs), DELETE_CONVERSATION_CALLBACK);
|
|
671
844
|
}
|
|
672
|
-
clearConversationMemoryState() {
|
|
673
|
-
const mutableState = this;
|
|
674
|
-
clearConversationRuntimeState({
|
|
675
|
-
chatMessageAbortControllers: mutableState._chatMessageAbortControllers,
|
|
676
|
-
clearResumableStream: () => this._resumableStream.clearAll(),
|
|
677
|
-
messages: mutableState.messages,
|
|
678
|
-
pendingResumeConnections: mutableState._pendingResumeConnections
|
|
679
|
-
});
|
|
680
|
-
mutableState._approvalPersistedMessageId = null;
|
|
681
|
-
mutableState._lastBody = void 0;
|
|
682
|
-
mutableState._lastClientTools = void 0;
|
|
683
|
-
mutableState._streamCompletionPromise = null;
|
|
684
|
-
mutableState._streamCompletionResolve = null;
|
|
685
|
-
mutableState._streamingMessage = null;
|
|
686
|
-
}
|
|
687
845
|
};
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
function buildTurnSummaryForLog(messages, loadedSkills) {
|
|
695
|
-
const toolCallNames = [];
|
|
696
|
-
for (const msg of messages) {
|
|
697
|
-
if (msg.role !== "assistant" || !msg.parts) continue;
|
|
698
|
-
for (const part of msg.parts) {
|
|
699
|
-
if (!("toolCallId" in part)) continue;
|
|
700
|
-
const { type } = part;
|
|
701
|
-
if (!type.startsWith("tool-")) continue;
|
|
702
|
-
const name = type.slice(5);
|
|
703
|
-
if (name !== "activate_skill" && name !== "list_capabilities" && !toolCallNames.includes(name)) toolCallNames.push(name);
|
|
704
|
-
}
|
|
846
|
+
//#endregion
|
|
847
|
+
//#region src/server/harnesses/ChatAgentHarness.ts
|
|
848
|
+
var ChatAgentHarness = class extends ChatAgent {
|
|
849
|
+
get binding() {
|
|
850
|
+
const className = this.constructor.name;
|
|
851
|
+
return this.env[className];
|
|
705
852
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
853
|
+
conversationRetentionDays = 90;
|
|
854
|
+
/**
|
|
855
|
+
* Returns the tools for the agent.
|
|
856
|
+
* @param ctx - The context object for the agent built from the request body.
|
|
857
|
+
* @returns The tools for the agent.
|
|
858
|
+
*/
|
|
859
|
+
getTools(_ctx) {
|
|
860
|
+
return {};
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Returns the skills for the agent.
|
|
864
|
+
* @param ctx - The context object for the agent built from the request body.
|
|
865
|
+
* @returns The skills for the agent.
|
|
866
|
+
*/
|
|
867
|
+
getSkills(_ctx) {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
async onChatMessage(onFinish, options) {
|
|
871
|
+
const ctx = options?.body;
|
|
872
|
+
return streamText(await this.buildLLMParams({
|
|
873
|
+
options,
|
|
874
|
+
onFinish,
|
|
875
|
+
model: this.getModel(ctx),
|
|
876
|
+
system: this.getSystemPrompt(ctx),
|
|
877
|
+
skills: this.getSkills(ctx),
|
|
878
|
+
tools: this.getTools(ctx)
|
|
879
|
+
})).toUIMessageStreamResponse();
|
|
880
|
+
}
|
|
881
|
+
};
|
|
719
882
|
//#endregion
|
|
720
|
-
export {
|
|
883
|
+
export { Agent, ChatAgent, ChatAgentHarness, buildLLMParams };
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@economic/agents",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "A starter for creating a TypeScript package.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"economic-agents": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
6
9
|
"files": [
|
|
7
10
|
"dist",
|
|
8
|
-
"schema"
|
|
11
|
+
"schema",
|
|
12
|
+
"bin"
|
|
9
13
|
],
|
|
10
14
|
"type": "module",
|
|
11
15
|
"types": "./dist/index.d.mts",
|
|
12
16
|
"exports": {
|
|
13
17
|
".": "./dist/index.mjs",
|
|
14
|
-
"./
|
|
18
|
+
"./cli": "./dist/cli.mjs",
|
|
19
|
+
"./hono": "./dist/hono.mjs",
|
|
15
20
|
"./package.json": "./package.json"
|
|
16
21
|
},
|
|
17
22
|
"scripts": {
|
|
@@ -22,15 +27,18 @@
|
|
|
22
27
|
"release": "bumpp",
|
|
23
28
|
"prepublishOnly": "npm run build"
|
|
24
29
|
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@clack/prompts": "^0.9.1"
|
|
32
|
+
},
|
|
25
33
|
"devDependencies": {
|
|
26
34
|
"@cloudflare/ai-chat": "^0.1.9",
|
|
27
35
|
"@cloudflare/workers-types": "^4.20260317.1",
|
|
28
36
|
"@types/node": "^22.0.0",
|
|
29
|
-
"@types/react": "^19.0.0",
|
|
30
37
|
"@typescript/native-preview": "7.0.0-dev.20260316.1",
|
|
31
|
-
"ai": "^6.0.
|
|
38
|
+
"ai": "^6.0.158",
|
|
32
39
|
"bumpp": "^11.0.1",
|
|
33
|
-
"
|
|
40
|
+
"hono": "^4.12.12",
|
|
41
|
+
"jose": "^6.0.0",
|
|
34
42
|
"tsdown": "^0.21.4",
|
|
35
43
|
"typescript": "^5.9.3",
|
|
36
44
|
"vitest": "^4.1.0"
|
|
@@ -39,10 +47,14 @@
|
|
|
39
47
|
"@cloudflare/ai-chat": "^0.1.0",
|
|
40
48
|
"agents": "^0.7.6",
|
|
41
49
|
"ai": "^6.0.0",
|
|
42
|
-
"
|
|
50
|
+
"hono": "^4.0.0",
|
|
51
|
+
"jose": "^6.0.0"
|
|
43
52
|
},
|
|
44
53
|
"peerDependenciesMeta": {
|
|
45
|
-
"
|
|
54
|
+
"hono": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"jose": {
|
|
46
58
|
"optional": true
|
|
47
59
|
}
|
|
48
60
|
}
|
package/schema/agent.sql
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- ─── Audit logging ────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
durable_object_name TEXT NOT NULL,
|
|
6
|
+
message TEXT NOT NULL,
|
|
7
|
+
payload TEXT,
|
|
8
|
+
created_at TEXT NOT NULL
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE INDEX IF NOT EXISTS audit_events_do ON audit_events(durable_object_name);
|
|
12
|
+
CREATE INDEX IF NOT EXISTS audit_events_ts ON audit_events(created_at);
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
--
|
|
2
|
-
-- One database per agent worker — create it once in the Cloudflare D1 portal.
|
|
3
|
-
-- Safe to re-run: all statements use IF NOT EXISTS.
|
|
4
|
-
|
|
5
|
-
-- ─── Audit events ─────────────────────────────────────────────────────────────
|
|
1
|
+
-- ─── Audit logging ────────────────────────────────────────────────────────────
|
|
6
2
|
|
|
7
3
|
CREATE TABLE IF NOT EXISTS audit_events (
|
|
8
4
|
id TEXT PRIMARY KEY,
|
package/dist/react.d.mts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { UIMessage } from "ai";
|
|
2
|
-
import { useAgentChat } from "@cloudflare/ai-chat/react";
|
|
3
|
-
import { useAgent } from "agents/react";
|
|
4
|
-
|
|
5
|
-
//#region src/client/index.d.ts
|
|
6
|
-
type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized";
|
|
7
|
-
interface UseAIChatAgentOptions {
|
|
8
|
-
agent: string;
|
|
9
|
-
host: string;
|
|
10
|
-
basePath?: string;
|
|
11
|
-
chatId: string;
|
|
12
|
-
toolContext?: Record<string, unknown>;
|
|
13
|
-
connectionParams?: Record<string, string>;
|
|
14
|
-
onConnectionStatusChange?: (status: AgentConnectionStatus) => void;
|
|
15
|
-
onOpen?: (event: Event) => void;
|
|
16
|
-
onClose?: (event: CloseEvent) => void;
|
|
17
|
-
onError?: (event: ErrorEvent) => void;
|
|
18
|
-
}
|
|
19
|
-
type UseAIChatAgentResult = {
|
|
20
|
-
agent: ReturnType<typeof useAgent>;
|
|
21
|
-
chat: ReturnType<typeof useAgentChat<unknown, UIMessage>>;
|
|
22
|
-
};
|
|
23
|
-
declare function useAIChatAgent(options: UseAIChatAgentOptions): UseAIChatAgentResult;
|
|
24
|
-
//#endregion
|
|
25
|
-
export { AgentConnectionStatus, UseAIChatAgentOptions, useAIChatAgent };
|