@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
@@ -1,12 +1,10 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { useQuery, useQueryClient } from "@tanstack/react-query";
5
5
  import {
6
- AlertCircle,
7
6
  Bot,
8
7
  BookOpen,
9
- CheckCircle2,
10
8
  History,
11
9
  Image as ImageIcon,
12
10
  FileText,
@@ -16,6 +14,7 @@ import {
16
14
  RefreshCcw,
17
15
  Save,
18
16
  ShieldAlert,
17
+ ShieldOff,
19
18
  Trash2,
20
19
  Wand2,
21
20
  Wrench
@@ -26,6 +25,7 @@ import { Badge } from "@open-mercato/ui/primitives/badge";
26
25
  import { Button } from "@open-mercato/ui/primitives/button";
27
26
  import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
28
27
  import { IconButton } from "@open-mercato/ui/primitives/icon-button";
28
+ import { Input } from "@open-mercato/ui/primitives/input";
29
29
  import { Label } from "@open-mercato/ui/primitives/label";
30
30
  import { Radio, RadioGroup } from "@open-mercato/ui/primitives/radio";
31
31
  import {
@@ -496,17 +496,24 @@ function MutationPolicySection({ agent }) {
496
496
  ) })
497
497
  ] })
498
498
  ] }),
499
- /* @__PURE__ */ jsxs(Alert, { variant: "info", "data-ai-agent-mutation-policy-notice": true, children: [
500
- /* @__PURE__ */ jsx(ShieldAlert, { className: "size-4", "aria-hidden": true }),
501
- /* @__PURE__ */ jsx(AlertTitle, { children: t(
502
- "ai_assistant.agents.mutation_policy.noticeTitle",
503
- "Downgrade only \u2014 escalation is a code change"
504
- ) }),
505
- /* @__PURE__ */ jsx(AlertDescription, { children: t(
506
- "ai_assistant.agents.mutation_policy.noticeBody",
507
- "Overrides can only make the policy more restrictive. Options more permissive than the code-declared policy are disabled and rejected server-side."
508
- ) })
509
- ] }),
499
+ /* @__PURE__ */ jsxs(
500
+ Alert,
501
+ {
502
+ variant: "info",
503
+ icon: /* @__PURE__ */ jsx(ShieldAlert, { "aria-hidden": "true" }),
504
+ "data-ai-agent-mutation-policy-notice": true,
505
+ children: [
506
+ /* @__PURE__ */ jsx(AlertTitle, { children: t(
507
+ "ai_assistant.agents.mutation_policy.noticeTitle",
508
+ "Downgrade only \u2014 escalation is a code change"
509
+ ) }),
510
+ /* @__PURE__ */ jsx(AlertDescription, { children: t(
511
+ "ai_assistant.agents.mutation_policy.noticeBody",
512
+ "Overrides can only make the policy more restrictive. Options more permissive than the code-declared policy are disabled and rejected server-side."
513
+ ) })
514
+ ]
515
+ }
516
+ ),
510
517
  query.isLoading ? /* @__PURE__ */ jsx(
511
518
  SettingsLoading,
512
519
  {
@@ -516,7 +523,6 @@ function MutationPolicySection({ agent }) {
516
523
  )
517
524
  }
518
525
  ) : query.isError ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-mutation-policy-load-error": true, children: [
519
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
520
526
  /* @__PURE__ */ jsx(AlertTitle, { children: t(
521
527
  "ai_assistant.agents.mutation_policy.loadErrorTitle",
522
528
  "Failed to load mutation policy"
@@ -589,12 +595,10 @@ function MutationPolicySection({ agent }) {
589
595
  }
590
596
  ),
591
597
  state.kind === "success" ? /* @__PURE__ */ jsxs(Alert, { variant: "success", "data-ai-agent-mutation-policy-state": "success", children: [
592
- /* @__PURE__ */ jsx(CheckCircle2, { className: "size-4", "aria-hidden": true }),
593
598
  /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.mutation_policy.savedTitle", "Mutation policy updated") }),
594
599
  /* @__PURE__ */ jsx(AlertDescription, { children: state.message })
595
600
  ] }) : null,
596
601
  state.kind === "error" ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-mutation-policy-state": "error", children: [
597
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
598
602
  /* @__PURE__ */ jsx(AlertTitle, { children: t(
599
603
  "ai_assistant.agents.mutation_policy.errorTitle",
600
604
  "Failed to update mutation policy"
@@ -918,7 +922,6 @@ function AgentModelOverrideSection({ agent }) {
918
922
  )
919
923
  }
920
924
  ) : settingsQuery.isError ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-model-override-load-error": true, children: [
921
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
922
925
  /* @__PURE__ */ jsx(AlertTitle, { children: t(
923
926
  "ai_assistant.agents.model_override.loadErrorTitle",
924
927
  "Failed to load provider catalog"
@@ -1096,14 +1099,8 @@ function AgentModelOverrideSection({ agent }) {
1096
1099
  )
1097
1100
  ] })
1098
1101
  ] }) : null,
1099
- state.kind === "success" ? /* @__PURE__ */ jsxs(Alert, { variant: "success", "data-ai-agent-model-override-state": "success", children: [
1100
- /* @__PURE__ */ jsx(CheckCircle2, { className: "size-4", "aria-hidden": true }),
1101
- /* @__PURE__ */ jsx(AlertDescription, { children: state.message })
1102
- ] }) : null,
1103
- state.kind === "error" ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-model-override-state": "error", children: [
1104
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
1105
- /* @__PURE__ */ jsx(AlertDescription, { children: state.message })
1106
- ] }) : null,
1102
+ state.kind === "success" ? /* @__PURE__ */ jsx(Alert, { variant: "success", "data-ai-agent-model-override-state": "success", children: /* @__PURE__ */ jsx(AlertDescription, { children: state.message }) }) : null,
1103
+ state.kind === "error" ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", "data-ai-agent-model-override-state": "error", children: /* @__PURE__ */ jsx(AlertDescription, { children: state.message }) }) : null,
1107
1104
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-end gap-2", children: [
1108
1105
  /* @__PURE__ */ jsxs(
1109
1106
  Button,
@@ -1140,6 +1137,302 @@ function AgentModelOverrideSection({ agent }) {
1140
1137
  }
1141
1138
  );
1142
1139
  }
1140
+ async function fetchLoopOverride(agentId) {
1141
+ const { result, status } = await apiCallOrThrow(
1142
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agentId)}/loop-override`,
1143
+ { method: "GET", credentials: "include" },
1144
+ { errorMessage: "Failed to load loop override" }
1145
+ );
1146
+ if (!result) throw new Error(`Failed to load loop override (${status})`);
1147
+ return result;
1148
+ }
1149
+ function LoopPolicySection({ agent }) {
1150
+ const t = useT();
1151
+ const queryClient = useQueryClient();
1152
+ const query = useQuery({
1153
+ queryKey: ["ai_assistant", "agent_settings", "loop_override", agent.id],
1154
+ queryFn: () => fetchLoopOverride(agent.id),
1155
+ retry: false
1156
+ });
1157
+ const currentOverride = query.data?.override ?? null;
1158
+ const [loopDisabled, setLoopDisabled] = React.useState(false);
1159
+ const [maxSteps, setMaxSteps] = React.useState("");
1160
+ const [maxToolCalls, setMaxToolCalls] = React.useState("");
1161
+ const [maxWallClockMs, setMaxWallClockMs] = React.useState("");
1162
+ const [maxTokens, setMaxTokens] = React.useState("");
1163
+ const [isSaving, setIsSaving] = React.useState(false);
1164
+ const [isClearing, setIsClearing] = React.useState(false);
1165
+ const [state, setState] = React.useState({ kind: "idle" });
1166
+ React.useEffect(() => {
1167
+ setLoopDisabled(currentOverride?.loopDisabled ?? false);
1168
+ setMaxSteps(currentOverride?.loopMaxSteps != null ? String(currentOverride.loopMaxSteps) : "");
1169
+ setMaxToolCalls(
1170
+ currentOverride?.loopMaxToolCalls != null ? String(currentOverride.loopMaxToolCalls) : ""
1171
+ );
1172
+ setMaxWallClockMs(
1173
+ currentOverride?.loopMaxWallClockMs != null ? String(currentOverride.loopMaxWallClockMs) : ""
1174
+ );
1175
+ setMaxTokens(
1176
+ currentOverride?.loopMaxTokens != null ? String(currentOverride.loopMaxTokens) : ""
1177
+ );
1178
+ setState({ kind: "idle" });
1179
+ }, [
1180
+ agent.id,
1181
+ currentOverride?.loopDisabled,
1182
+ currentOverride?.loopMaxSteps,
1183
+ currentOverride?.loopMaxToolCalls,
1184
+ currentOverride?.loopMaxWallClockMs,
1185
+ currentOverride?.loopMaxTokens
1186
+ ]);
1187
+ const toNullableInt = (value) => {
1188
+ const trimmed = value.trim();
1189
+ if (trimmed === "") return null;
1190
+ const parsed = parseInt(trimmed, 10);
1191
+ return isNaN(parsed) ? null : parsed;
1192
+ };
1193
+ const save = React.useCallback(async () => {
1194
+ if (isSaving) return;
1195
+ setIsSaving(true);
1196
+ setState({ kind: "idle" });
1197
+ try {
1198
+ const { ok, status, result } = await apiCall(
1199
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/loop-override`,
1200
+ {
1201
+ method: "PUT",
1202
+ headers: { "content-type": "application/json" },
1203
+ credentials: "include",
1204
+ body: JSON.stringify({
1205
+ loopDisabled: loopDisabled || null,
1206
+ loopMaxSteps: toNullableInt(maxSteps),
1207
+ loopMaxToolCalls: toNullableInt(maxToolCalls),
1208
+ loopMaxWallClockMs: toNullableInt(maxWallClockMs),
1209
+ loopMaxTokens: toNullableInt(maxTokens)
1210
+ })
1211
+ }
1212
+ );
1213
+ const payload = result ?? {};
1214
+ if (!ok) {
1215
+ setState({
1216
+ kind: "error",
1217
+ message: payload.error ?? `Failed to save loop policy (${status}).`
1218
+ });
1219
+ return;
1220
+ }
1221
+ setState({
1222
+ kind: "success",
1223
+ message: t("ai_assistant.agents.loop_policy.savedMessage", "Loop policy override saved.")
1224
+ });
1225
+ await queryClient.invalidateQueries({
1226
+ queryKey: ["ai_assistant", "agent_settings", "loop_override", agent.id]
1227
+ });
1228
+ } catch (err) {
1229
+ setState({
1230
+ kind: "error",
1231
+ message: err instanceof Error ? err.message : String(err)
1232
+ });
1233
+ } finally {
1234
+ setIsSaving(false);
1235
+ }
1236
+ }, [agent.id, isSaving, loopDisabled, maxSteps, maxToolCalls, maxWallClockMs, maxTokens, queryClient, t]);
1237
+ const clear = React.useCallback(async () => {
1238
+ if (isClearing) return;
1239
+ setIsClearing(true);
1240
+ setState({ kind: "idle" });
1241
+ try {
1242
+ const { ok, status, result } = await apiCall(
1243
+ `/api/ai_assistant/ai/agents/${encodeURIComponent(agent.id)}/loop-override`,
1244
+ { method: "DELETE", credentials: "include" }
1245
+ );
1246
+ const payload = result ?? {};
1247
+ if (!ok) {
1248
+ setState({
1249
+ kind: "error",
1250
+ message: payload.error ?? `Failed to clear loop override (${status}).`
1251
+ });
1252
+ return;
1253
+ }
1254
+ setState({
1255
+ kind: "success",
1256
+ message: t(
1257
+ "ai_assistant.agents.loop_policy.clearedMessage",
1258
+ "Loop policy override cleared; agent is using its declared defaults."
1259
+ )
1260
+ });
1261
+ await queryClient.invalidateQueries({
1262
+ queryKey: ["ai_assistant", "agent_settings", "loop_override", agent.id]
1263
+ });
1264
+ } catch (err) {
1265
+ setState({
1266
+ kind: "error",
1267
+ message: err instanceof Error ? err.message : String(err)
1268
+ });
1269
+ } finally {
1270
+ setIsClearing(false);
1271
+ }
1272
+ }, [agent.id, isClearing, queryClient, t]);
1273
+ return /* @__PURE__ */ jsxs(
1274
+ "section",
1275
+ {
1276
+ className: "rounded-lg border border-border bg-background p-4",
1277
+ "data-ai-agent-loop-policy": agent.id,
1278
+ children: [
1279
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between gap-3 border-b border-border pb-3", children: [
1280
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1281
+ /* @__PURE__ */ jsx(ShieldOff, { className: "size-4 text-muted-foreground", "aria-hidden": true }),
1282
+ /* @__PURE__ */ jsxs("div", { children: [
1283
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("ai_assistant.agents.loop_policy.title", "Loop policy") }),
1284
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1285
+ "ai_assistant.agents.loop_policy.subtitle",
1286
+ "Set per-tenant budget limits or disable the agentic loop for this agent."
1287
+ ) })
1288
+ ] })
1289
+ ] }),
1290
+ currentOverride?.loopDisabled ? /* @__PURE__ */ jsx(Badge, { variant: "destructive", "data-ai-agent-loop-disabled-badge": true, children: t("ai_assistant.agents.loop_policy.disabledBadge", "Loop disabled") }) : null
1291
+ ] }),
1292
+ /* @__PURE__ */ jsx("div", { className: "mt-3 flex flex-col gap-4", children: query.isLoading ? /* @__PURE__ */ jsx(
1293
+ SettingsLoading,
1294
+ {
1295
+ message: t("ai_assistant.agents.loop_policy.loading", "Loading loop policy...")
1296
+ }
1297
+ ) : query.isError ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-loop-policy-load-error": true, children: [
1298
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.loop_policy.loadErrorTitle", "Failed to load loop policy") }),
1299
+ /* @__PURE__ */ jsx(AlertDescription, { children: query.error instanceof Error ? query.error.message : String(query.error) })
1300
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1301
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2", children: [
1302
+ /* @__PURE__ */ jsxs("div", { children: [
1303
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: t("ai_assistant.agents.loop_policy.killSwitchLabel", "Kill switch") }),
1304
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
1305
+ "ai_assistant.agents.loop_policy.killSwitchDescription",
1306
+ "When enabled, the agent runs as a single model call with no tool loop."
1307
+ ) })
1308
+ ] }),
1309
+ /* @__PURE__ */ jsx(
1310
+ Switch,
1311
+ {
1312
+ checked: loopDisabled,
1313
+ onCheckedChange: (next) => setLoopDisabled(next),
1314
+ "aria-label": t("ai_assistant.agents.loop_policy.killSwitchLabel", "Kill switch"),
1315
+ "data-ai-agent-loop-kill-switch": true
1316
+ }
1317
+ )
1318
+ ] }),
1319
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2", children: [
1320
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1321
+ /* @__PURE__ */ jsx(Label, { htmlFor: `loop-max-steps-${agent.id}`, className: "text-xs", children: t("ai_assistant.agents.loop_policy.maxStepsLabel", "Max steps") }),
1322
+ /* @__PURE__ */ jsx(
1323
+ Input,
1324
+ {
1325
+ id: `loop-max-steps-${agent.id}`,
1326
+ type: "number",
1327
+ min: 1,
1328
+ max: 1e3,
1329
+ value: maxSteps,
1330
+ onChange: (event) => setMaxSteps(event.target.value),
1331
+ placeholder: t("ai_assistant.agents.loop_policy.noOverridePlaceholder", "No override"),
1332
+ className: "h-8 text-sm",
1333
+ "data-ai-agent-loop-max-steps": true
1334
+ }
1335
+ )
1336
+ ] }),
1337
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1338
+ /* @__PURE__ */ jsx(Label, { htmlFor: `loop-max-tool-calls-${agent.id}`, className: "text-xs", children: t("ai_assistant.agents.loop_policy.maxToolCallsLabel", "Max tool calls") }),
1339
+ /* @__PURE__ */ jsx(
1340
+ Input,
1341
+ {
1342
+ id: `loop-max-tool-calls-${agent.id}`,
1343
+ type: "number",
1344
+ min: 1,
1345
+ max: 1e4,
1346
+ value: maxToolCalls,
1347
+ onChange: (event) => setMaxToolCalls(event.target.value),
1348
+ placeholder: t("ai_assistant.agents.loop_policy.noOverridePlaceholder", "No override"),
1349
+ className: "h-8 text-sm",
1350
+ "data-ai-agent-loop-max-tool-calls": true
1351
+ }
1352
+ )
1353
+ ] }),
1354
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1355
+ /* @__PURE__ */ jsx(Label, { htmlFor: `loop-max-wall-clock-${agent.id}`, className: "text-xs", children: t("ai_assistant.agents.loop_policy.maxWallClockMsLabel", "Max wall-clock (ms)") }),
1356
+ /* @__PURE__ */ jsx(
1357
+ Input,
1358
+ {
1359
+ id: `loop-max-wall-clock-${agent.id}`,
1360
+ type: "number",
1361
+ min: 100,
1362
+ max: 36e5,
1363
+ value: maxWallClockMs,
1364
+ onChange: (event) => setMaxWallClockMs(event.target.value),
1365
+ placeholder: t("ai_assistant.agents.loop_policy.noOverridePlaceholder", "No override"),
1366
+ className: "h-8 text-sm",
1367
+ "data-ai-agent-loop-max-wall-clock-ms": true
1368
+ }
1369
+ )
1370
+ ] }),
1371
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
1372
+ /* @__PURE__ */ jsx(Label, { htmlFor: `loop-max-tokens-${agent.id}`, className: "text-xs", children: t("ai_assistant.agents.loop_policy.maxTokensLabel", "Max tokens") }),
1373
+ /* @__PURE__ */ jsx(
1374
+ Input,
1375
+ {
1376
+ id: `loop-max-tokens-${agent.id}`,
1377
+ type: "number",
1378
+ min: 1,
1379
+ max: 1e7,
1380
+ value: maxTokens,
1381
+ onChange: (event) => setMaxTokens(event.target.value),
1382
+ placeholder: t("ai_assistant.agents.loop_policy.noOverridePlaceholder", "No override"),
1383
+ className: "h-8 text-sm",
1384
+ "data-ai-agent-loop-max-tokens": true
1385
+ }
1386
+ )
1387
+ ] })
1388
+ ] }),
1389
+ state.kind === "success" ? /* @__PURE__ */ jsxs(Alert, { variant: "success", "data-ai-agent-loop-policy-state": "success", children: [
1390
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.loop_policy.savedTitle", "Loop policy updated") }),
1391
+ /* @__PURE__ */ jsx(AlertDescription, { children: state.message })
1392
+ ] }) : null,
1393
+ state.kind === "error" ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-loop-policy-state": "error", children: [
1394
+ /* @__PURE__ */ jsx(AlertTitle, { children: t(
1395
+ "ai_assistant.agents.loop_policy.errorTitle",
1396
+ "Failed to update loop policy"
1397
+ ) }),
1398
+ /* @__PURE__ */ jsx(AlertDescription, { children: state.message })
1399
+ ] }) : null,
1400
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2", children: [
1401
+ /* @__PURE__ */ jsxs(
1402
+ Button,
1403
+ {
1404
+ type: "button",
1405
+ size: "sm",
1406
+ variant: "outline",
1407
+ onClick: () => void clear(),
1408
+ disabled: isClearing || isSaving || !currentOverride,
1409
+ "data-ai-agent-loop-policy-clear": true,
1410
+ children: [
1411
+ isClearing ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Trash2, { className: "size-4", "aria-hidden": true }),
1412
+ /* @__PURE__ */ jsx("span", { children: t("ai_assistant.agents.loop_policy.clear", "Clear override") })
1413
+ ]
1414
+ }
1415
+ ),
1416
+ /* @__PURE__ */ jsxs(
1417
+ Button,
1418
+ {
1419
+ type: "button",
1420
+ size: "sm",
1421
+ onClick: () => void save(),
1422
+ disabled: isSaving || isClearing,
1423
+ "data-ai-agent-loop-policy-save": true,
1424
+ children: [
1425
+ isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": true }) : /* @__PURE__ */ jsx(Save, { className: "size-4", "aria-hidden": true }),
1426
+ /* @__PURE__ */ jsx("span", { children: t("ai_assistant.agents.loop_policy.save", "Save override") })
1427
+ ]
1428
+ }
1429
+ )
1430
+ ] })
1431
+ ] }) })
1432
+ ]
1433
+ }
1434
+ );
1435
+ }
1143
1436
  function AgentDetailPanel({ agent }) {
1144
1437
  const t = useT();
1145
1438
  const queryClient = useQueryClient();
@@ -1347,6 +1640,7 @@ function AgentDetailPanel({ agent }) {
1347
1640
  ] }),
1348
1641
  /* @__PURE__ */ jsx(AgentModelOverrideSection, { agent }),
1349
1642
  /* @__PURE__ */ jsx(MutationPolicySection, { agent }),
1643
+ /* @__PURE__ */ jsx(LoopPolicySection, { agent }),
1350
1644
  /* @__PURE__ */ jsxs(
1351
1645
  "section",
1352
1646
  {
@@ -1376,21 +1670,26 @@ function AgentDetailPanel({ agent }) {
1376
1670
  }
1377
1671
  )
1378
1672
  ] }),
1379
- /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs(Alert, { variant: "info", "data-ai-agent-prompt-notice": true, children: [
1380
- /* @__PURE__ */ jsx(Wand2, { className: "size-4", "aria-hidden": true }),
1381
- /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.override.noticeTitle", "Prompt overrides are additive") }),
1382
- /* @__PURE__ */ jsx(AlertDescription, { children: t(
1383
- "ai_assistant.agents.override.noticeBody",
1384
- "Overrides append to the built-in sections \u2014 they never remove or replace shipped text. Saved versions are tenant-scoped and auditable from the history panel below."
1385
- ) })
1386
- ] }) }),
1673
+ /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsxs(
1674
+ Alert,
1675
+ {
1676
+ variant: "info",
1677
+ icon: /* @__PURE__ */ jsx(Wand2, { "aria-hidden": "true" }),
1678
+ "data-ai-agent-prompt-notice": true,
1679
+ children: [
1680
+ /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.override.noticeTitle", "Prompt overrides are additive") }),
1681
+ /* @__PURE__ */ jsx(AlertDescription, { children: t(
1682
+ "ai_assistant.agents.override.noticeBody",
1683
+ "Overrides append to the built-in sections \u2014 they never remove or replace shipped text. Saved versions are tenant-scoped and auditable from the history panel below."
1684
+ ) })
1685
+ ]
1686
+ }
1687
+ ) }),
1387
1688
  saveState.kind === "success" ? /* @__PURE__ */ jsxs(Alert, { variant: "success", className: "mt-3", "data-ai-agent-prompt-state": "success", children: [
1388
- /* @__PURE__ */ jsx(CheckCircle2, { className: "size-4", "aria-hidden": true }),
1389
1689
  /* @__PURE__ */ jsx(AlertTitle, { children: saveState.version > 0 ? t("ai_assistant.agents.override.savedTitle", "Prompt override saved") : t("ai_assistant.agents.prompt.pendingTitle", "Overrides accepted") }),
1390
1690
  /* @__PURE__ */ jsx(AlertDescription, { children: saveState.version > 0 ? `${saveState.message} ${t("ai_assistant.agents.override.versionLabel", "Version")} ${saveState.version}.` : saveState.message })
1391
1691
  ] }) : null,
1392
1692
  saveState.kind === "error" ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", className: "mt-3", "data-ai-agent-prompt-state": "error", children: [
1393
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
1394
1693
  /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.override.errorTitle", "Failed to save prompt override") }),
1395
1694
  /* @__PURE__ */ jsx(AlertDescription, { children: saveState.message })
1396
1695
  ] }) : null,
@@ -1479,7 +1778,6 @@ function AgentDetailPanel({ agent }) {
1479
1778
  )
1480
1779
  }
1481
1780
  ) : overrideQuery.isError ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-override-history-error": true, children: [
1482
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
1483
1781
  /* @__PURE__ */ jsx(AlertTitle, { children: t(
1484
1782
  "ai_assistant.agents.override.history.errorTitle",
1485
1783
  "Failed to load override history"
@@ -1571,7 +1869,6 @@ function AiAgentSettingsPageClient() {
1571
1869
  }
1572
1870
  if (isError) {
1573
1871
  return /* @__PURE__ */ jsxs(Alert, { variant: "destructive", "data-ai-agent-settings-error": true, children: [
1574
- /* @__PURE__ */ jsx(AlertCircle, { className: "size-4", "aria-hidden": true }),
1575
1872
  /* @__PURE__ */ jsx(AlertTitle, { children: t("ai_assistant.agents.loadErrorTitle", "Failed to load AI agents") }),
1576
1873
  /* @__PURE__ */ jsxs(AlertDescription, { children: [
1577
1874
  /* @__PURE__ */ jsx("span", { children: error instanceof Error ? error.message : String(error) }),