@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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { callable } from "agents";
2
- import { AIChatAgent as AIChatAgent$1 } from "@cloudflare/ai-chat";
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
- experimental_transform: composedTransform,
241
- stopWhen: rest.stopWhen ?? stepCountIs(20)
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/index.ts
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 `AIChatAgent.log()`. Not intended for direct use.
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 `AIChatAgent` after every turn. On first call for a given
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 `AIChatAgent` every `SUMMARY_CONTEXT_MESSAGES` messages after
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 = "deleteConversation";
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/AIChatAgent.ts
641
+ //#region src/server/agents/ChatAgent.ts
493
642
  /**
494
- * Base class for Cloudflare Agents SDK chat agents with lazy skill loading
495
- * and built-in audit logging.
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 only: DO SQLite persistence for loaded
498
- * skill state, stripping skill meta-tool messages before persistence, history
499
- * replay to newly connected clients, and writing audit events to D1.
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 communication are delegated to
502
- * `buildLLMParams` from `@economic/agents`, which you call inside `onChatMessage`.
650
+ * Skill loading, compaction, and LLM calls use `buildLLMParams` from
651
+ * `@economic/agents` inside `onChatMessage`.
503
652
  */
504
- var AIChatAgent = class extends AIChatAgent$1 {
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("[AIChatAgent] Connection rejected: no AGENT_DB bound");
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("[AIChatAgent] Connection rejected: name must be in the format userId:uniqueChatId");
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 after every turn (from `persistMessages`) and on
543
- * non-clean finish reasons (from `buildLLMParams`). Also available via
544
- * `experimental_context.log` in tool `execute` functions.
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 log(message, payload) {
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("[AIChatAgent] Failed to write audit event", 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 `log` into `experimental_context` and logs non-clean finish reasons.
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 context = {
715
+ const experimental_context = {
567
716
  ...config.options?.body,
568
- log: this.log.bind(this)
717
+ logEvent: this.logEvent.bind(this)
569
718
  };
570
719
  const messages = await convertToModelMessages(this.messages);
571
- const processedMessages = this.fastModel && this.maxMessagesBeforeCompaction !== void 0 ? await compactIfNeeded(messages, this.fastModel, this.maxMessagesBeforeCompaction) : messages;
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: 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
- * logs a turn summary, then strips all skill meta-tool messages before
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. Logs a turn summary via `log()`. Best-effort: fire-and-forget.
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
- * 4. Delegates to super.persistMessages for message storage and WS broadcast.
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 `persistMessages` after every turn.
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.fastModel);
815
+ const { title, summary } = await generateTitleAndSummary(messages, this.getFastModel());
633
816
  await recordConversation(this.env.AGENT_DB, this.name, title, summary);
634
- this.log("[AIChatAgent] Conversation summary generated");
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.fastModel);
639
- this.log("[AIChatAgent] Conversation summary updated");
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 deleteConversation() {
644
- try {
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
- * Builds the payload for a "turn completed" audit event from the final message list.
690
- *
691
- * Extracts the last user and assistant message texts (truncated to 200 chars),
692
- * all non-meta tool call names used this turn, and the current loaded skill set.
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
- const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
707
- const lastAssistantMsg = [...messages].reverse().find((m) => m.role === "assistant");
708
- return {
709
- userMessage: extractMessageText(lastUserMsg).slice(0, 200),
710
- toolCalls: toolCallNames,
711
- loadedSkills,
712
- assistantMessage: extractMessageText(lastAssistantMsg).slice(0, 200)
713
- };
714
- }
715
- function extractMessageText(msg) {
716
- if (!msg?.parts) return "";
717
- return msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(" ").trim();
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 { AIChatAgent, buildLLMParams };
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.1",
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
- "./react": "./dist/react.mjs",
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.134",
38
+ "ai": "^6.0.158",
32
39
  "bumpp": "^11.0.1",
33
- "react": "^19.0.0",
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
- "react": ">=18"
50
+ "hono": "^4.0.0",
51
+ "jose": "^6.0.0"
43
52
  },
44
53
  "peerDependenciesMeta": {
45
- "react": {
54
+ "hono": {
55
+ "optional": true
56
+ },
57
+ "jose": {
46
58
  "optional": true
47
59
  }
48
60
  }
@@ -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
- -- Full schema for @economic/agents.
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 };