@open-mercato/ai-assistant 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1

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.
Files changed (135) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +30 -4
  3. package/dist/frontend/components/AiChatButton.js +3 -2
  4. package/dist/frontend/components/AiChatButton.js.map +2 -2
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
  11. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
  12. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
  13. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
  14. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
  15. package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
  16. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  17. package/dist/modules/ai_assistant/api/settings/route.js +4 -3
  18. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  19. package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
  20. package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
  21. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
  22. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
  23. package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
  24. package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
  25. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
  26. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  27. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
  28. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
  29. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
  30. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  31. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
  32. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
  33. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
  34. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
  35. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
  36. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
  37. package/dist/modules/ai_assistant/cli.js +12 -0
  38. package/dist/modules/ai_assistant/cli.js.map +2 -2
  39. package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
  40. package/dist/modules/ai_assistant/data/entities.js +177 -1
  41. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  42. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
  43. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  44. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
  45. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
  46. package/dist/modules/ai_assistant/events.js +8 -0
  47. package/dist/modules/ai_assistant/events.js.map +2 -2
  48. package/dist/modules/ai_assistant/i18n/de.json +74 -1
  49. package/dist/modules/ai_assistant/i18n/en.json +74 -1
  50. package/dist/modules/ai_assistant/i18n/es.json +75 -2
  51. package/dist/modules/ai_assistant/i18n/pl.json +74 -1
  52. package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
  53. package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
  54. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
  55. package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
  56. package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
  57. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  58. package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
  59. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  60. package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
  61. package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
  62. package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
  63. package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
  64. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
  65. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
  66. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
  67. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
  68. package/dist/modules/ai_assistant/setup.js +34 -0
  69. package/dist/modules/ai_assistant/setup.js.map +2 -2
  70. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
  71. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
  72. package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
  73. package/generated/entities/ai_token_usage_daily/index.ts +16 -0
  74. package/generated/entities/ai_token_usage_event/index.ts +19 -0
  75. package/generated/entities.ids.generated.ts +2 -0
  76. package/generated/entity-fields-registry.ts +47 -1
  77. package/package.json +15 -7
  78. package/src/frontend/components/AiChatButton.tsx +3 -2
  79. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
  80. package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
  81. package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
  82. package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
  83. package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
  84. package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
  85. package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
  86. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
  87. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
  88. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
  89. package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
  90. package/src/modules/ai_assistant/api/settings/route.ts +5 -3
  91. package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
  92. package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
  93. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
  94. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
  95. package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
  96. package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
  97. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
  98. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
  99. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
  100. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
  101. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
  102. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
  103. package/src/modules/ai_assistant/cli.ts +18 -0
  104. package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
  105. package/src/modules/ai_assistant/data/entities.ts +237 -0
  106. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
  107. package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
  108. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
  109. package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
  110. package/src/modules/ai_assistant/events.ts +8 -0
  111. package/src/modules/ai_assistant/i18n/de.json +74 -1
  112. package/src/modules/ai_assistant/i18n/en.json +74 -1
  113. package/src/modules/ai_assistant/i18n/es.json +75 -2
  114. package/src/modules/ai_assistant/i18n/pl.json +74 -1
  115. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
  116. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
  117. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
  118. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
  119. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
  120. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
  121. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
  122. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
  123. package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
  124. package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
  125. package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
  126. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
  127. package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
  128. package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
  129. package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
  130. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
  131. package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
  132. package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
  133. package/src/modules/ai_assistant/setup.ts +49 -0
  134. package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
  135. package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
@@ -0,0 +1,88 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260508170000_ai_token_usage extends Migration {
3
+ async up() {
4
+ this.addSql(`
5
+ create table "ai_token_usage_events" (
6
+ "id" uuid not null default gen_random_uuid(),
7
+ "tenant_id" uuid not null,
8
+ "organization_id" uuid null,
9
+ "user_id" uuid not null,
10
+ "agent_id" text not null,
11
+ "module_id" text not null,
12
+ "session_id" uuid not null,
13
+ "turn_id" uuid not null,
14
+ "step_index" int not null,
15
+ "provider_id" text not null,
16
+ "model_id" text not null,
17
+ "input_tokens" int not null,
18
+ "output_tokens" int not null,
19
+ "cached_input_tokens" int null,
20
+ "reasoning_tokens" int null,
21
+ "finish_reason" text null,
22
+ "loop_abort_reason" text null,
23
+ "created_at" timestamptz not null,
24
+ "updated_at" timestamptz not null,
25
+ constraint "ai_token_usage_events_pkey" primary key ("id")
26
+ );
27
+ `);
28
+ this.addSql(`
29
+ create index "ai_token_usage_events_tenant_created_idx"
30
+ on "ai_token_usage_events" ("tenant_id", "created_at" desc);
31
+ `);
32
+ this.addSql(`
33
+ create index "ai_token_usage_events_tenant_agent_created_idx"
34
+ on "ai_token_usage_events" ("tenant_id", "agent_id", "created_at" desc);
35
+ `);
36
+ this.addSql(`
37
+ create index "ai_token_usage_events_tenant_model_created_idx"
38
+ on "ai_token_usage_events" ("tenant_id", "model_id", "created_at" desc);
39
+ `);
40
+ this.addSql(`
41
+ create index "ai_token_usage_events_tenant_session_turn_step_idx"
42
+ on "ai_token_usage_events" ("tenant_id", "session_id", "turn_id", "step_index");
43
+ `);
44
+ this.addSql(`
45
+ create table "ai_token_usage_daily" (
46
+ "id" uuid not null default gen_random_uuid(),
47
+ "tenant_id" uuid not null,
48
+ "organization_id" uuid null,
49
+ "day" date not null,
50
+ "agent_id" text not null,
51
+ "model_id" text not null,
52
+ "provider_id" text not null,
53
+ "input_tokens" bigint not null default 0,
54
+ "output_tokens" bigint not null default 0,
55
+ "cached_input_tokens" bigint not null default 0,
56
+ "reasoning_tokens" bigint not null default 0,
57
+ "step_count" bigint not null default 0,
58
+ "turn_count" bigint not null default 0,
59
+ "session_count" bigint not null default 0,
60
+ "created_at" timestamptz not null,
61
+ "updated_at" timestamptz not null,
62
+ constraint "ai_token_usage_daily_pkey" primary key ("id")
63
+ );
64
+ `);
65
+ this.addSql(`
66
+ create unique index "ai_token_usage_daily_tenant_day_agent_model_org_uq"
67
+ on "ai_token_usage_daily" ("tenant_id", "day", "agent_id", "model_id", "organization_id")
68
+ where "organization_id" is not null;
69
+ `);
70
+ this.addSql(`
71
+ create unique index "ai_token_usage_daily_tenant_day_agent_model_null_org_uq"
72
+ on "ai_token_usage_daily" ("tenant_id", "day", "agent_id", "model_id")
73
+ where "organization_id" is null;
74
+ `);
75
+ this.addSql(`
76
+ create index "ai_token_usage_daily_tenant_day_idx"
77
+ on "ai_token_usage_daily" ("tenant_id", "day");
78
+ `);
79
+ }
80
+ async down() {
81
+ this.addSql(`drop table if exists "ai_token_usage_events" cascade;`);
82
+ this.addSql(`drop table if exists "ai_token_usage_daily" cascade;`);
83
+ }
84
+ }
85
+ export {
86
+ Migration20260508170000_ai_token_usage
87
+ };
88
+ //# sourceMappingURL=Migration20260508170000_ai_token_usage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\nexport class Migration20260508170000_ai_token_usage extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`\n create table \"ai_token_usage_events\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"tenant_id\" uuid not null,\n \"organization_id\" uuid null,\n \"user_id\" uuid not null,\n \"agent_id\" text not null,\n \"module_id\" text not null,\n \"session_id\" uuid not null,\n \"turn_id\" uuid not null,\n \"step_index\" int not null,\n \"provider_id\" text not null,\n \"model_id\" text not null,\n \"input_tokens\" int not null,\n \"output_tokens\" int not null,\n \"cached_input_tokens\" int null,\n \"reasoning_tokens\" int null,\n \"finish_reason\" text null,\n \"loop_abort_reason\" text null,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n constraint \"ai_token_usage_events_pkey\" primary key (\"id\")\n );\n `)\n this.addSql(`\n create index \"ai_token_usage_events_tenant_created_idx\"\n on \"ai_token_usage_events\" (\"tenant_id\", \"created_at\" desc);\n `)\n this.addSql(`\n create index \"ai_token_usage_events_tenant_agent_created_idx\"\n on \"ai_token_usage_events\" (\"tenant_id\", \"agent_id\", \"created_at\" desc);\n `)\n this.addSql(`\n create index \"ai_token_usage_events_tenant_model_created_idx\"\n on \"ai_token_usage_events\" (\"tenant_id\", \"model_id\", \"created_at\" desc);\n `)\n this.addSql(`\n create index \"ai_token_usage_events_tenant_session_turn_step_idx\"\n on \"ai_token_usage_events\" (\"tenant_id\", \"session_id\", \"turn_id\", \"step_index\");\n `)\n\n this.addSql(`\n create table \"ai_token_usage_daily\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"tenant_id\" uuid not null,\n \"organization_id\" uuid null,\n \"day\" date not null,\n \"agent_id\" text not null,\n \"model_id\" text not null,\n \"provider_id\" text not null,\n \"input_tokens\" bigint not null default 0,\n \"output_tokens\" bigint not null default 0,\n \"cached_input_tokens\" bigint not null default 0,\n \"reasoning_tokens\" bigint not null default 0,\n \"step_count\" bigint not null default 0,\n \"turn_count\" bigint not null default 0,\n \"session_count\" bigint not null default 0,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n constraint \"ai_token_usage_daily_pkey\" primary key (\"id\")\n );\n `)\n this.addSql(`\n create unique index \"ai_token_usage_daily_tenant_day_agent_model_org_uq\"\n on \"ai_token_usage_daily\" (\"tenant_id\", \"day\", \"agent_id\", \"model_id\", \"organization_id\")\n where \"organization_id\" is not null;\n `)\n this.addSql(`\n create unique index \"ai_token_usage_daily_tenant_day_agent_model_null_org_uq\"\n on \"ai_token_usage_daily\" (\"tenant_id\", \"day\", \"agent_id\", \"model_id\")\n where \"organization_id\" is null;\n `)\n this.addSql(`\n create index \"ai_token_usage_daily_tenant_day_idx\"\n on \"ai_token_usage_daily\" (\"tenant_id\", \"day\");\n `)\n }\n\n override async down(): Promise<void> {\n this.addSql(`drop table if exists \"ai_token_usage_events\" cascade;`)\n this.addSql(`drop table if exists \"ai_token_usage_daily\" cascade;`)\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,+CAA+C,UAAU;AAAA,EAEpE,MAAe,KAAoB;AACjC,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAuBX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AAED,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAoBX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA,KAIX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA,KAIX;AACD,SAAK,OAAO;AAAA;AAAA;AAAA,KAGX;AAAA,EACH;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,uDAAuD;AACnE,SAAK,OAAO,sDAAsD;AAAA,EACpE;AAEF;",
6
+ "names": []
7
+ }
@@ -1,4 +1,5 @@
1
1
  const PENDING_ACTION_CLEANUP_SCHEDULE_ID = "ai_assistant:pending-action-cleanup";
2
+ const TOKEN_USAGE_PRUNE_SCHEDULE_ID = "ai_assistant:token-usage-prune";
2
3
  async function ensurePendingActionCleanupSchedule(container) {
3
4
  if (!container) return;
4
5
  let schedulerService;
@@ -31,6 +32,38 @@ async function ensurePendingActionCleanupSchedule(container) {
31
32
  );
32
33
  }
33
34
  }
35
+ async function ensureTokenUsagePruneSchedule(container) {
36
+ if (!container) return;
37
+ let schedulerService;
38
+ try {
39
+ schedulerService = container.resolve("schedulerService");
40
+ } catch {
41
+ schedulerService = void 0;
42
+ }
43
+ if (!schedulerService) return;
44
+ try {
45
+ await schedulerService.register({
46
+ id: TOKEN_USAGE_PRUNE_SCHEDULE_ID,
47
+ name: "AI token-usage prune",
48
+ description: "Delete ai_token_usage_events rows older than AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS (default 90) and reconcile session_count on the daily rollup.",
49
+ scopeType: "system",
50
+ scheduleType: "interval",
51
+ scheduleValue: "24h",
52
+ timezone: "UTC",
53
+ targetType: "queue",
54
+ targetQueue: "ai-token-usage-prune",
55
+ targetPayload: {},
56
+ sourceType: "module",
57
+ sourceModule: "ai_assistant",
58
+ isEnabled: true
59
+ });
60
+ } catch (error) {
61
+ console.warn(
62
+ "[ai_assistant] Failed to register token-usage prune schedule:",
63
+ error instanceof Error ? error.message : error
64
+ );
65
+ }
66
+ }
34
67
  const setup = {
35
68
  defaultRoleFeatures: {
36
69
  admin: [
@@ -45,6 +78,7 @@ const setup = {
45
78
  },
46
79
  async seedDefaults({ container }) {
47
80
  await ensurePendingActionCleanupSchedule(container);
81
+ await ensureTokenUsagePruneSchedule(container);
48
82
  }
49
83
  };
50
84
  var setup_default = setup;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/ai_assistant/setup.ts"],
4
- "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nconst PENDING_ACTION_CLEANUP_SCHEDULE_ID = 'ai_assistant:pending-action-cleanup'\n\n/**\n * System-scoped recurring schedule: every 5 minutes, enqueue a job to the\n * `ai-pending-action-cleanup` queue so the worker can sweep rows whose TTL\n * elapsed without any confirm/cancel activity (Step 5.12). The schedule id\n * is stable and `scheduler.register()` is an upsert, so calling this from\n * every tenant bootstrap stays idempotent.\n */\nasync function ensurePendingActionCleanupSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: PENDING_ACTION_CLEANUP_SCHEDULE_ID,\n name: 'AI pending-action cleanup',\n description:\n 'Sweep pending AI mutation approvals whose TTL elapsed without confirm/cancel and flip them to expired.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '5m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-pending-action-cleanup',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register pending-action cleanup schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'ai_assistant.view',\n 'ai_assistant.settings.manage',\n 'ai_assistant.mcp.serve',\n 'ai_assistant.tools.list',\n 'ai_assistant.mcp_servers.view',\n 'ai_assistant.mcp_servers.manage',\n ],\n employee: ['ai_assistant.view'],\n },\n\n async seedDefaults({ container }) {\n await ensurePendingActionCleanupSchedule(container)\n },\n}\n\nexport default setup\n"],
5
- "mappings": "AAEA,MAAM,qCAAqC;AAS3C,eAAe,mCACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU,CAAC,mBAAmB;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,EAAE,UAAU,GAAG;AAChC,UAAM,mCAAmC,SAAS;AAAA,EACpD;AACF;AAEA,IAAO,gBAAQ;",
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nconst PENDING_ACTION_CLEANUP_SCHEDULE_ID = 'ai_assistant:pending-action-cleanup'\nconst TOKEN_USAGE_PRUNE_SCHEDULE_ID = 'ai_assistant:token-usage-prune'\n\n/**\n * System-scoped recurring schedule: every 5 minutes, enqueue a job to the\n * `ai-pending-action-cleanup` queue so the worker can sweep rows whose TTL\n * elapsed without any confirm/cancel activity (Step 5.12). The schedule id\n * is stable and `scheduler.register()` is an upsert, so calling this from\n * every tenant bootstrap stays idempotent.\n */\nasync function ensurePendingActionCleanupSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: PENDING_ACTION_CLEANUP_SCHEDULE_ID,\n name: 'AI pending-action cleanup',\n description:\n 'Sweep pending AI mutation approvals whose TTL elapsed without confirm/cancel and flip them to expired.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '5m',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-pending-action-cleanup',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register pending-action cleanup schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\n/**\n * System-scoped daily schedule: enqueue a job to the `ai-token-usage-prune`\n * queue to prune events older than the retention window and reconcile the\n * daily rollup session counts.\n *\n * Phase 6.4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nasync function ensureTokenUsagePruneSchedule(\n container: import('awilix').AwilixContainer | undefined,\n): Promise<void> {\n if (!container) return\n let schedulerService:\n | {\n register: (registration: Record<string, unknown>) => Promise<void>\n }\n | undefined\n try {\n schedulerService = container.resolve('schedulerService')\n } catch {\n schedulerService = undefined\n }\n if (!schedulerService) return\n try {\n await schedulerService.register({\n id: TOKEN_USAGE_PRUNE_SCHEDULE_ID,\n name: 'AI token-usage prune',\n description:\n 'Delete ai_token_usage_events rows older than AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS (default 90) and reconcile session_count on the daily rollup.',\n scopeType: 'system',\n scheduleType: 'interval',\n scheduleValue: '24h',\n timezone: 'UTC',\n targetType: 'queue',\n targetQueue: 'ai-token-usage-prune',\n targetPayload: {},\n sourceType: 'module',\n sourceModule: 'ai_assistant',\n isEnabled: true,\n })\n } catch (error) {\n console.warn(\n '[ai_assistant] Failed to register token-usage prune schedule:',\n error instanceof Error ? error.message : error,\n )\n }\n}\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: [\n 'ai_assistant.view',\n 'ai_assistant.settings.manage',\n 'ai_assistant.mcp.serve',\n 'ai_assistant.tools.list',\n 'ai_assistant.mcp_servers.view',\n 'ai_assistant.mcp_servers.manage',\n ],\n employee: ['ai_assistant.view'],\n },\n\n async seedDefaults({ container }) {\n await ensurePendingActionCleanupSchedule(container)\n await ensureTokenUsagePruneSchedule(container)\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEA,MAAM,qCAAqC;AAC3C,MAAM,gCAAgC;AAStC,eAAe,mCACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AASA,eAAe,8BACb,WACe;AACf,MAAI,CAAC,UAAW;AAChB,MAAI;AAKJ,MAAI;AACF,uBAAmB,UAAU,QAAQ,kBAAkB;AAAA,EACzD,QAAQ;AACN,uBAAmB;AAAA,EACrB;AACA,MAAI,CAAC,iBAAkB;AACvB,MAAI;AACF,UAAM,iBAAiB,SAAS;AAAA,MAC9B,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,eAAe,CAAC;AAAA,MAChB,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AACF;AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,UAAU,CAAC,mBAAmB;AAAA,EAChC;AAAA,EAEA,MAAM,aAAa,EAAE,UAAU,GAAG;AAChC,UAAM,mCAAmC,SAAS;AAClD,UAAM,8BAA8B,SAAS;AAAA,EAC/C;AACF;AAEA,IAAO,gBAAQ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,114 @@
1
+ const DEFAULT_RETENTION_DAYS = 90;
2
+ const DELETE_BATCH_SIZE = 5e3;
3
+ const RECONCILE_TRAILING_DAYS = 7;
4
+ const metadata = {
5
+ queue: "ai-token-usage-prune",
6
+ id: "ai_assistant:token-usage-prune",
7
+ concurrency: 1
8
+ };
9
+ function resolveRetentionDays() {
10
+ const raw = process.env.AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS;
11
+ if (!raw) return DEFAULT_RETENTION_DAYS;
12
+ const parsed = parseInt(raw.trim(), 10);
13
+ return !isNaN(parsed) && parsed > 0 ? parsed : DEFAULT_RETENTION_DAYS;
14
+ }
15
+ async function pruneOldEvents(connection, cutoff, batchSize) {
16
+ let totalDeleted = 0;
17
+ for (; ; ) {
18
+ const result = await connection.execute(
19
+ `
20
+ delete from ai_token_usage_events
21
+ where id in (
22
+ select id from ai_token_usage_events
23
+ where created_at < ?
24
+ limit ?
25
+ )
26
+ `,
27
+ [cutoff, batchSize],
28
+ "run"
29
+ );
30
+ const deleted = result?.affectedRows ?? result?.rowCount ?? 0;
31
+ totalDeleted += deleted;
32
+ if (deleted < batchSize) break;
33
+ }
34
+ return totalDeleted;
35
+ }
36
+ async function reconcileSessionCounts(connection, now, trailingDays) {
37
+ const trailingStart = new Date(now);
38
+ trailingStart.setUTCDate(trailingStart.getUTCDate() - trailingDays);
39
+ const from = trailingStart.toISOString().slice(0, 10);
40
+ const rows = await connection.execute(
41
+ `
42
+ select
43
+ d.id,
44
+ count(distinct e.session_id)::bigint as computed_session_count
45
+ from ai_token_usage_daily d
46
+ left join ai_token_usage_events e
47
+ on e.tenant_id = d.tenant_id
48
+ and e.agent_id = d.agent_id
49
+ and e.model_id = d.model_id
50
+ and date_trunc('day', e.created_at)::date = d.day
51
+ and (
52
+ (d.organization_id is null and e.organization_id is null)
53
+ or (d.organization_id is not null and e.organization_id = d.organization_id)
54
+ )
55
+ where d.day >= ?::date
56
+ group by d.id
57
+ `,
58
+ [from],
59
+ "all"
60
+ );
61
+ if (!Array.isArray(rows) || rows.length === 0) return 0;
62
+ let reconciled = 0;
63
+ for (const row of rows) {
64
+ const rowId = row.id;
65
+ const computed = typeof row.computed_session_count === "string" ? parseInt(row.computed_session_count, 10) : row.computed_session_count ?? 0;
66
+ await connection.execute(
67
+ `update ai_token_usage_daily set session_count = ?, updated_at = now() where id = ?`,
68
+ [computed, rowId],
69
+ "run"
70
+ );
71
+ reconciled += 1;
72
+ }
73
+ return reconciled;
74
+ }
75
+ async function runTokenUsagePrune(options) {
76
+ const now = options.now ?? /* @__PURE__ */ new Date();
77
+ const retentionDays = options.retentionDays ?? resolveRetentionDays();
78
+ const batchSize = options.batchSize ?? DELETE_BATCH_SIZE;
79
+ const cutoff = new Date(now);
80
+ cutoff.setUTCDate(cutoff.getUTCDate() - retentionDays);
81
+ const connection = options.em.getConnection();
82
+ let eventsDeleted = 0;
83
+ try {
84
+ eventsDeleted = await pruneOldEvents(connection, cutoff, batchSize);
85
+ } catch (error) {
86
+ console.error(
87
+ "[ai-token-usage-prune] Failed to prune old events:",
88
+ error instanceof Error ? error.message : error
89
+ );
90
+ }
91
+ let dailyRowsReconciled = 0;
92
+ try {
93
+ dailyRowsReconciled = await reconcileSessionCounts(connection, now, RECONCILE_TRAILING_DAYS);
94
+ } catch (error) {
95
+ console.error(
96
+ "[ai-token-usage-prune] Failed to reconcile session counts:",
97
+ error instanceof Error ? error.message : error
98
+ );
99
+ }
100
+ console.info(
101
+ `[ai-token-usage-prune] Done. eventsDeleted=${eventsDeleted}, dailyRowsReconciled=${dailyRowsReconciled}, retentionDays=${retentionDays}.`
102
+ );
103
+ return { eventsDeleted, dailyRowsReconciled };
104
+ }
105
+ async function handle(_job, ctx) {
106
+ const em = ctx.resolve("em");
107
+ await runTokenUsagePrune({ em });
108
+ }
109
+ export {
110
+ handle as default,
111
+ metadata,
112
+ runTokenUsagePrune
113
+ };
114
+ //# sourceMappingURL=ai-token-usage-prune.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/workers/ai-token-usage-prune.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\n\n/** Default retention window for the ai_token_usage_events table (days). */\nconst DEFAULT_RETENTION_DAYS = 90\n/** Batch size for delete operations to avoid long locks. */\nconst DELETE_BATCH_SIZE = 5_000\n/** Number of trailing days to reconcile session_count against events table. */\nconst RECONCILE_TRAILING_DAYS = 7\n\nexport const metadata: WorkerMeta = {\n queue: 'ai-token-usage-prune',\n id: 'ai_assistant:token-usage-prune',\n concurrency: 1,\n}\n\nexport interface TokenUsagePruneRunOptions {\n em: EntityManager\n /** Override for deterministic tests. Defaults to `new Date()`. */\n now?: Date\n /** Override retention days (default from env). */\n retentionDays?: number\n /** Override batch size (default 5000). */\n batchSize?: number\n}\n\nexport interface TokenUsagePruneSummary {\n eventsDeleted: number\n dailyRowsReconciled: number\n}\n\n/**\n * Reads `AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS` env var (default 90).\n */\nfunction resolveRetentionDays(): number {\n const raw = process.env.AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS\n if (!raw) return DEFAULT_RETENTION_DAYS\n const parsed = parseInt(raw.trim(), 10)\n return !isNaN(parsed) && parsed > 0 ? parsed : DEFAULT_RETENTION_DAYS\n}\n\n/**\n * Deletes events older than the retention cutoff in batches of `batchSize` to\n * avoid long table locks. Returns total rows deleted.\n */\nasync function pruneOldEvents(\n connection: ReturnType<EntityManager['getConnection']>,\n cutoff: Date,\n batchSize: number,\n): Promise<number> {\n let totalDeleted = 0\n for (;;) {\n const result = await connection.execute(\n `\n delete from ai_token_usage_events\n where id in (\n select id from ai_token_usage_events\n where created_at < ?\n limit ?\n )\n `,\n [cutoff, batchSize],\n 'run',\n ) as { affectedRows?: number; rowCount?: number } | undefined\n const deleted = result?.affectedRows ?? result?.rowCount ?? 0\n totalDeleted += deleted\n if (deleted < batchSize) break\n }\n return totalDeleted\n}\n\n/**\n * Reconciles `session_count` on daily rollup rows for the trailing N days by\n * recomputing it directly from the events table. This corrects any drift caused\n * by out-of-order event delivery, retention pruning, or failed incremental\n * writes.\n */\nasync function reconcileSessionCounts(\n connection: ReturnType<EntityManager['getConnection']>,\n now: Date,\n trailingDays: number,\n): Promise<number> {\n const trailingStart = new Date(now)\n trailingStart.setUTCDate(trailingStart.getUTCDate() - trailingDays)\n const from = trailingStart.toISOString().slice(0, 10)\n\n // Recompute session_count for each (tenant_id, day, agent_id, model_id, org)\n // combination by counting distinct session_ids from the events table.\n const rows = await connection.execute(\n `\n select\n d.id,\n count(distinct e.session_id)::bigint as computed_session_count\n from ai_token_usage_daily d\n left join ai_token_usage_events e\n on e.tenant_id = d.tenant_id\n and e.agent_id = d.agent_id\n and e.model_id = d.model_id\n and date_trunc('day', e.created_at)::date = d.day\n and (\n (d.organization_id is null and e.organization_id is null)\n or (d.organization_id is not null and e.organization_id = d.organization_id)\n )\n where d.day >= ?::date\n group by d.id\n `,\n [from],\n 'all',\n )\n\n if (!Array.isArray(rows) || rows.length === 0) return 0\n\n let reconciled = 0\n for (const row of rows as Array<Record<string, unknown>>) {\n const rowId = row.id as string\n const computed = typeof row.computed_session_count === 'string'\n ? parseInt(row.computed_session_count, 10)\n : (row.computed_session_count as number) ?? 0\n await connection.execute(\n `update ai_token_usage_daily set session_count = ?, updated_at = now() where id = ?`,\n [computed, rowId],\n 'run',\n )\n reconciled += 1\n }\n\n return reconciled\n}\n\n/**\n * Core logic for the token-usage prune worker. Exported for unit testing.\n *\n * 1. Resolves the retention cutoff from `AI_TOKEN_USAGE_EVENTS_RETENTION_DAYS`.\n * 2. Deletes events older than the cutoff in batches of 5_000.\n * 3. Reconciles `session_count` on the daily rollup for trailing 7 days.\n *\n * Phase 6.4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\nexport async function runTokenUsagePrune(\n options: TokenUsagePruneRunOptions,\n): Promise<TokenUsagePruneSummary> {\n const now = options.now ?? new Date()\n const retentionDays = options.retentionDays ?? resolveRetentionDays()\n const batchSize = options.batchSize ?? DELETE_BATCH_SIZE\n\n const cutoff = new Date(now)\n cutoff.setUTCDate(cutoff.getUTCDate() - retentionDays)\n\n const connection = options.em.getConnection()\n\n let eventsDeleted = 0\n try {\n eventsDeleted = await pruneOldEvents(connection, cutoff, batchSize)\n } catch (error) {\n console.error(\n '[ai-token-usage-prune] Failed to prune old events:',\n error instanceof Error ? error.message : error,\n )\n }\n\n let dailyRowsReconciled = 0\n try {\n dailyRowsReconciled = await reconcileSessionCounts(connection, now, RECONCILE_TRAILING_DAYS)\n } catch (error) {\n console.error(\n '[ai-token-usage-prune] Failed to reconcile session counts:',\n error instanceof Error ? error.message : error,\n )\n }\n\n console.info(\n `[ai-token-usage-prune] Done. eventsDeleted=${eventsDeleted}, dailyRowsReconciled=${dailyRowsReconciled}, retentionDays=${retentionDays}.`,\n )\n\n return { eventsDeleted, dailyRowsReconciled }\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nexport default async function handle(\n _job: QueuedJob,\n ctx: HandlerContext,\n): Promise<void> {\n const em = ctx.resolve<EntityManager>('em')\n await runTokenUsagePrune({ em })\n}\n"],
5
+ "mappings": "AAIA,MAAM,yBAAyB;AAE/B,MAAM,oBAAoB;AAE1B,MAAM,0BAA0B;AAEzB,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAoBA,SAAS,uBAA+B;AACtC,QAAM,MAAM,QAAQ,IAAI;AACxB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,SAAS,SAAS,IAAI,KAAK,GAAG,EAAE;AACtC,SAAO,CAAC,MAAM,MAAM,KAAK,SAAS,IAAI,SAAS;AACjD;AAMA,eAAe,eACb,YACA,QACA,WACiB;AACjB,MAAI,eAAe;AACnB,aAAS;AACP,UAAM,SAAS,MAAM,WAAW;AAAA,MAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,CAAC,QAAQ,SAAS;AAAA,MAClB;AAAA,IACF;AACA,UAAM,UAAU,QAAQ,gBAAgB,QAAQ,YAAY;AAC5D,oBAAgB;AAChB,QAAI,UAAU,UAAW;AAAA,EAC3B;AACA,SAAO;AACT;AAQA,eAAe,uBACb,YACA,KACA,cACiB;AACjB,QAAM,gBAAgB,IAAI,KAAK,GAAG;AAClC,gBAAc,WAAW,cAAc,WAAW,IAAI,YAAY;AAClE,QAAM,OAAO,cAAc,YAAY,EAAE,MAAM,GAAG,EAAE;AAIpD,QAAM,OAAO,MAAM,WAAW;AAAA,IAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAiBA,CAAC,IAAI;AAAA,IACL;AAAA,EACF;AAEA,MAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,KAAK,WAAW,EAAG,QAAO;AAEtD,MAAI,aAAa;AACjB,aAAW,OAAO,MAAwC;AACxD,UAAM,QAAQ,IAAI;AAClB,UAAM,WAAW,OAAO,IAAI,2BAA2B,WACnD,SAAS,IAAI,wBAAwB,EAAE,IACtC,IAAI,0BAAqC;AAC9C,UAAM,WAAW;AAAA,MACf;AAAA,MACA,CAAC,UAAU,KAAK;AAAA,MAChB;AAAA,IACF;AACA,kBAAc;AAAA,EAChB;AAEA,SAAO;AACT;AAWA,eAAsB,mBACpB,SACiC;AACjC,QAAM,MAAM,QAAQ,OAAO,oBAAI,KAAK;AACpC,QAAM,gBAAgB,QAAQ,iBAAiB,qBAAqB;AACpE,QAAM,YAAY,QAAQ,aAAa;AAEvC,QAAM,SAAS,IAAI,KAAK,GAAG;AAC3B,SAAO,WAAW,OAAO,WAAW,IAAI,aAAa;AAErD,QAAM,aAAa,QAAQ,GAAG,cAAc;AAE5C,MAAI,gBAAgB;AACpB,MAAI;AACF,oBAAgB,MAAM,eAAe,YAAY,QAAQ,SAAS;AAAA,EACpE,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI,sBAAsB;AAC1B,MAAI;AACF,0BAAsB,MAAM,uBAAuB,YAAY,KAAK,uBAAuB;AAAA,EAC7F,SAAS,OAAO;AACd,YAAQ;AAAA,MACN;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAC3C;AAAA,EACF;AAEA,UAAQ;AAAA,IACN,8CAA8C,aAAa,yBAAyB,mBAAmB,mBAAmB,aAAa;AAAA,EACzI;AAEA,SAAO,EAAE,eAAe,oBAAoB;AAC9C;AAMA,eAAO,OACL,MACA,KACe;AACf,QAAM,KAAK,IAAI,QAAuB,IAAI;AAC1C,QAAM,mBAAmB,EAAE,GAAG,CAAC;AACjC;",
6
+ "names": []
7
+ }
@@ -11,3 +11,10 @@ export const updated_by_user_id = "updated_by_user_id";
11
11
  export const created_at = "created_at";
12
12
  export const updated_at = "updated_at";
13
13
  export const deleted_at = "deleted_at";
14
+ export const loop_disabled = "loop_disabled";
15
+ export const loop_max_steps = "loop_max_steps";
16
+ export const loop_max_tool_calls = "loop_max_tool_calls";
17
+ export const loop_max_wall_clock_ms = "loop_max_wall_clock_ms";
18
+ export const loop_max_tokens = "loop_max_tokens";
19
+ export const loop_stop_when_json = "loop_stop_when_json";
20
+ export const loop_active_tools_json = "loop_active_tools_json";
@@ -0,0 +1,16 @@
1
+ export const id = "id";
2
+ export const tenant_id = "tenant_id";
3
+ export const organization_id = "organization_id";
4
+ export const day = "day";
5
+ export const agent_id = "agent_id";
6
+ export const model_id = "model_id";
7
+ export const provider_id = "provider_id";
8
+ export const input_tokens = "input_tokens";
9
+ export const output_tokens = "output_tokens";
10
+ export const cached_input_tokens = "cached_input_tokens";
11
+ export const reasoning_tokens = "reasoning_tokens";
12
+ export const step_count = "step_count";
13
+ export const turn_count = "turn_count";
14
+ export const session_count = "session_count";
15
+ export const created_at = "created_at";
16
+ export const updated_at = "updated_at";
@@ -0,0 +1,19 @@
1
+ export const id = "id";
2
+ export const tenant_id = "tenant_id";
3
+ export const organization_id = "organization_id";
4
+ export const user_id = "user_id";
5
+ export const agent_id = "agent_id";
6
+ export const module_id = "module_id";
7
+ export const session_id = "session_id";
8
+ export const turn_id = "turn_id";
9
+ export const step_index = "step_index";
10
+ export const provider_id = "provider_id";
11
+ export const model_id = "model_id";
12
+ export const input_tokens = "input_tokens";
13
+ export const output_tokens = "output_tokens";
14
+ export const cached_input_tokens = "cached_input_tokens";
15
+ export const reasoning_tokens = "reasoning_tokens";
16
+ export const finish_reason = "finish_reason";
17
+ export const loop_abort_reason = "loop_abort_reason";
18
+ export const created_at = "created_at";
19
+ export const updated_at = "updated_at";
@@ -6,6 +6,8 @@ export const M = {
6
6
  "ai_agent_prompt_override": "ai_assistant:ai_agent_prompt_override",
7
7
  "ai_pending_action": "ai_assistant:ai_pending_action",
8
8
  "ai_agent_runtime_override": "ai_assistant:ai_agent_runtime_override",
9
+ "ai_token_usage_event": "ai_assistant:ai_token_usage_event",
10
+ "ai_token_usage_daily": "ai_assistant:ai_token_usage_daily",
9
11
  "ai_tenant_model_allowlist": "ai_assistant:ai_tenant_model_allowlist",
10
12
  "ai_agent_mutation_policy_override": "ai_assistant:ai_agent_mutation_policy_override"
11
13
  }
@@ -37,7 +37,14 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
37
37
  "updated_by_user_id": "updated_by_user_id",
38
38
  "created_at": "created_at",
39
39
  "updated_at": "updated_at",
40
- "deleted_at": "deleted_at"
40
+ "deleted_at": "deleted_at",
41
+ "loop_disabled": "loop_disabled",
42
+ "loop_max_steps": "loop_max_steps",
43
+ "loop_max_tool_calls": "loop_max_tool_calls",
44
+ "loop_max_wall_clock_ms": "loop_max_wall_clock_ms",
45
+ "loop_max_tokens": "loop_max_tokens",
46
+ "loop_stop_when_json": "loop_stop_when_json",
47
+ "loop_active_tools_json": "loop_active_tools_json"
41
48
  },
42
49
  "ai_pending_action": {
43
50
  "id": "id",
@@ -75,6 +82,45 @@ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
75
82
  "created_at": "created_at",
76
83
  "updated_at": "updated_at",
77
84
  "deleted_at": "deleted_at"
85
+ },
86
+ "ai_token_usage_daily": {
87
+ "id": "id",
88
+ "tenant_id": "tenant_id",
89
+ "organization_id": "organization_id",
90
+ "day": "day",
91
+ "agent_id": "agent_id",
92
+ "model_id": "model_id",
93
+ "provider_id": "provider_id",
94
+ "input_tokens": "input_tokens",
95
+ "output_tokens": "output_tokens",
96
+ "cached_input_tokens": "cached_input_tokens",
97
+ "reasoning_tokens": "reasoning_tokens",
98
+ "step_count": "step_count",
99
+ "turn_count": "turn_count",
100
+ "session_count": "session_count",
101
+ "created_at": "created_at",
102
+ "updated_at": "updated_at"
103
+ },
104
+ "ai_token_usage_event": {
105
+ "id": "id",
106
+ "tenant_id": "tenant_id",
107
+ "organization_id": "organization_id",
108
+ "user_id": "user_id",
109
+ "agent_id": "agent_id",
110
+ "module_id": "module_id",
111
+ "session_id": "session_id",
112
+ "turn_id": "turn_id",
113
+ "step_index": "step_index",
114
+ "provider_id": "provider_id",
115
+ "model_id": "model_id",
116
+ "input_tokens": "input_tokens",
117
+ "output_tokens": "output_tokens",
118
+ "cached_input_tokens": "cached_input_tokens",
119
+ "reasoning_tokens": "reasoning_tokens",
120
+ "finish_reason": "finish_reason",
121
+ "loop_abort_reason": "loop_abort_reason",
122
+ "created_at": "created_at",
123
+ "updated_at": "updated_at"
78
124
  }
79
125
  };
80
126
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ai-assistant",
3
- "version": "0.6.1-develop.3291.1.6fad645fd0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -98,13 +98,22 @@
98
98
  "zod-to-json-schema": "^3.25.2"
99
99
  },
100
100
  "peerDependencies": {
101
- "@open-mercato/shared": "0.6.1-develop.3291.1.6fad645fd0",
102
- "@open-mercato/ui": "0.6.1-develop.3291.1.6fad645fd0",
101
+ "@open-mercato/shared": "0.6.1",
102
+ "@open-mercato/ui": "0.6.1",
103
+ "react": "^19.0.0",
104
+ "react-dom": "^19.0.0",
103
105
  "zod": ">=3.23.0"
104
106
  },
105
107
  "devDependencies": {
106
- "@open-mercato/cli": "0.6.1-develop.3291.1.6fad645fd0",
107
- "tsx": "^4.21.0"
108
+ "@open-mercato/cli": "0.6.1",
109
+ "@open-mercato/shared": "0.6.1",
110
+ "@open-mercato/ui": "0.6.1",
111
+ "@types/react": "^19.2.14",
112
+ "@types/react-dom": "^19.2.3",
113
+ "react": "19.2.6",
114
+ "react-dom": "19.2.6",
115
+ "tsx": "^4.21.0",
116
+ "zod": "^4.3.6"
108
117
  },
109
118
  "publishConfig": {
110
119
  "access": "public"
@@ -113,6 +122,5 @@
113
122
  "type": "git",
114
123
  "url": "https://github.com/open-mercato/open-mercato",
115
124
  "directory": "packages/ai-assistant"
116
- },
117
- "stableVersion": "0.6.0"
125
+ }
118
126
  }
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { Sparkles } from 'lucide-react'
4
+ import { AiIcon } from '@open-mercato/ui/ai/AiIcon'
5
5
  import { Button } from '@open-mercato/ui/primitives/button'
6
6
  import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@open-mercato/ui/primitives/tooltip'
7
7
 
@@ -23,13 +23,14 @@ export function AiChatButton({ onClick, className }: AiChatButtonProps) {
23
23
  <Tooltip>
24
24
  <TooltipTrigger asChild>
25
25
  <Button
26
+ type="button"
26
27
  variant="ghost"
27
28
  size="icon"
28
29
  onClick={handleClick}
29
30
  className={className}
30
31
  aria-label="Open AI Assistant"
31
32
  >
32
- <Sparkles className="h-5 w-5" />
33
+ <AiIcon className="h-5 w-5" />
33
34
  </Button>
34
35
  </TooltipTrigger>
35
36
  <TooltipContent side="bottom">