@open-mercato/ai-assistant 0.6.1-develop.3287.1.450f4ffb56 → 0.6.1-develop.3306.1.9ad9ff2526

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 (160) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +69 -18
  3. package/README.md +2 -1
  4. package/dist/frontend/components/AiChatButton.js +3 -2
  5. package/dist/frontend/components/AiChatButton.js.map +2 -2
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
  11. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
  12. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
  13. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
  14. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
  15. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
  16. package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
  17. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  18. package/dist/modules/ai_assistant/api/settings/route.js +4 -3
  19. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  20. package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
  21. package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
  22. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
  23. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
  24. package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
  25. package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
  26. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
  27. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  28. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
  29. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
  30. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
  31. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  32. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
  33. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
  34. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
  35. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
  36. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
  37. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
  38. package/dist/modules/ai_assistant/cli.js +12 -0
  39. package/dist/modules/ai_assistant/cli.js.map +2 -2
  40. package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
  41. package/dist/modules/ai_assistant/data/entities.js +177 -1
  42. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  43. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
  44. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  45. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
  46. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
  47. package/dist/modules/ai_assistant/events.js +8 -0
  48. package/dist/modules/ai_assistant/events.js.map +2 -2
  49. package/dist/modules/ai_assistant/i18n/de.json +74 -1
  50. package/dist/modules/ai_assistant/i18n/en.json +74 -1
  51. package/dist/modules/ai_assistant/i18n/es.json +75 -2
  52. package/dist/modules/ai_assistant/i18n/pl.json +74 -1
  53. package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
  54. package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
  55. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
  56. package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
  57. package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
  58. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  59. package/dist/modules/ai_assistant/lib/api-endpoint-index.js +0 -111
  60. package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
  61. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
  62. package/dist/modules/ai_assistant/lib/http-server.js +0 -5
  63. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  64. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +0 -5
  65. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  66. package/dist/modules/ai_assistant/lib/mcp-server.js +0 -5
  67. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  68. package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
  69. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  70. package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
  71. package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
  72. package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
  73. package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
  74. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
  75. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
  76. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
  77. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
  78. package/dist/modules/ai_assistant/setup.js +34 -0
  79. package/dist/modules/ai_assistant/setup.js.map +2 -2
  80. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
  81. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
  82. package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
  83. package/generated/entities/ai_token_usage_daily/index.ts +16 -0
  84. package/generated/entities/ai_token_usage_event/index.ts +19 -0
  85. package/generated/entities.ids.generated.ts +2 -0
  86. package/generated/entity-fields-registry.ts +47 -1
  87. package/package.json +14 -5
  88. package/src/frontend/components/AiChatButton.tsx +3 -2
  89. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
  90. package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
  91. package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
  92. package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
  93. package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
  94. package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
  95. package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
  96. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
  97. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
  98. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
  99. package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
  100. package/src/modules/ai_assistant/api/settings/route.ts +5 -3
  101. package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
  102. package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
  103. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
  104. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
  105. package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
  106. package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
  107. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
  108. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
  109. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
  110. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
  111. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
  112. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
  113. package/src/modules/ai_assistant/cli.ts +18 -0
  114. package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
  115. package/src/modules/ai_assistant/data/entities.ts +237 -0
  116. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
  117. package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
  118. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
  119. package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
  120. package/src/modules/ai_assistant/events.ts +8 -0
  121. package/src/modules/ai_assistant/i18n/de.json +74 -1
  122. package/src/modules/ai_assistant/i18n/en.json +74 -1
  123. package/src/modules/ai_assistant/i18n/es.json +75 -2
  124. package/src/modules/ai_assistant/i18n/pl.json +74 -1
  125. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
  126. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
  127. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
  128. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
  129. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
  130. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
  131. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
  132. package/src/modules/ai_assistant/lib/__tests__/mcp-startup-no-dead-index.test.ts +65 -0
  133. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
  134. package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
  135. package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
  136. package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
  137. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
  138. package/src/modules/ai_assistant/lib/api-endpoint-index.ts +5 -186
  139. package/src/modules/ai_assistant/lib/codemode-tools.ts +0 -2
  140. package/src/modules/ai_assistant/lib/http-server.ts +2 -9
  141. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +2 -8
  142. package/src/modules/ai_assistant/lib/mcp-server.ts +1 -10
  143. package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
  144. package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
  145. package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
  146. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
  147. package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
  148. package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
  149. package/src/modules/ai_assistant/setup.ts +49 -0
  150. package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
  151. package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
  152. package/dist/modules/ai_assistant/lib/api-discovery-tools.js +0 -170
  153. package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +0 -7
  154. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +0 -177
  155. package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +0 -7
  156. package/dist/modules/ai_assistant/lib/entity-graph-tools.js +0 -127
  157. package/dist/modules/ai_assistant/lib/entity-graph-tools.js.map +0 -7
  158. package/src/modules/ai_assistant/lib/api-discovery-tools.ts +0 -250
  159. package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +0 -243
  160. package/src/modules/ai_assistant/lib/entity-graph-tools.ts +0 -192
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 168 entry points
1
+ [build:ai-assistant] found 180 entry points
2
2
  [build:ai-assistant] built successfully
package/AGENTS.md CHANGED
@@ -103,7 +103,7 @@ APIs are automatically available via the Code Mode `search` tool (reads the Open
103
103
  Typed AI agents live in each module's root `ai-agents.ts`. The generator auto-discovers the file and aggregates it into `apps/mercato/.mercato/generated/ai-agents.generated.ts`. Reference implementations: `packages/core/src/modules/customers/ai-agents.ts` and `packages/core/src/modules/catalog/ai-agents.ts`.
104
104
 
105
105
  1. Create `<module>/ai-agents.ts` and export `aiAgents: AiAgentDefinition[]` (default export optional).
106
- 2. Declare the agent with `defineAiAgent({ ... })` from `@open-mercato/ai-assistant`. Required fields: `id`, `moduleId`, `label`, `description`, `systemPrompt`, `allowedTools`. Useful optional fields: `executionMode` (`'chat'` — default — or `'object'`), `defaultProvider` (registered provider id the agent prefers falls through transparently when unconfigured; Phase 1 of `2026-04-27-ai-agents-provider-model-baseurl-overrides`), `defaultModel` (plain model id or slash-qualified `<provider>/<model>` shorthand, e.g. `openai/gpt-5-mini`), `acceptedMediaTypes`, `requiredFeatures`, `uiParts`, `readOnly`, `mutationPolicy` (`'read-only'` | `'confirm-required'` | `'destructive-confirm-required'`), `maxSteps`, `output` (Zod schema for `'object'` mode), `resolvePageContext`, `keywords`, `suggestions`, `domain`, `dataCapabilities`.
106
+ 2. Declare the agent with `defineAiAgent({ ... })` from `@open-mercato/ai-assistant`. Required fields: `id`, `moduleId`, `label`, `description`, `systemPrompt`, `allowedTools`. Useful optional fields: `executionMode` (`'chat'` — default — or `'object'`), `executionEngine` (`'stream-text'` — default — or `'tool-loop-agent'`; see §"Loop controls and execution engines" below), `defaultProvider` (registered provider id the agent prefers; when paired with `defaultModel`, the pair fails closed if the provider is unconfigured; Phase 1 of `2026-04-27-ai-agents-provider-model-baseurl-overrides`), `defaultModel` (plain model id or slash-qualified `<provider>/<model>` shorthand, e.g. `openai/gpt-5-mini`), `acceptedMediaTypes`, `requiredFeatures`, `uiParts`, `readOnly`, `mutationPolicy` (`'read-only'` | `'confirm-required'` | `'destructive-confirm-required'`), `maxSteps`, `loop` (Phase 0–5 of spec `2026-04-28-ai-agents-agentic-loop-controls`), `output` (Zod schema for `'object'` mode), `resolvePageContext`, `keywords`, `suggestions`, `domain`, `dataCapabilities`.
107
107
  3. Add the feature(s) you list in `requiredFeatures` to the module's `acl.ts` and grant them in `setup.ts` `defaultRoleFeatures`.
108
108
  4. Put the agent's tool allowlist behind the narrowest set possible. Start from the general-purpose packs (`search.hybrid_search`, `search.get_record_context`, `attachments.list_record_attachments`, `attachments.read_attachment`, `meta.describe_agent`) and add your module's own `defineAiTool`-registered tools.
109
109
  5. For mutation-capable agents, keep `readOnly: true` + `mutationPolicy: 'read-only'` on the agent and light up writes only via the per-tenant mutation-policy override table (spec Phase 3 WS-C §5.4). The runtime filters out any `isMutation: true` tool when the override is still read-only.
@@ -336,7 +336,7 @@ Process-wide defaults (Phase 0 of spec
336
336
 
337
337
  | Variable | Purpose |
338
338
  |----------|---------|
339
- | `OM_AI_PROVIDER` | Optional. Names the registered provider id to prefer when multiple are configured. Falls through transparently when the named provider is registered-but-unconfigured. Built-in ids: `anthropic`, `google`, `openai`, `deepinfra`, `groq`, `together`, `fireworks`, `azure`, `litellm`, `ollama`, `openrouter`, `lm-studio`. The legacy `OPENCODE_PROVIDER` env is read as a backward-compatibility fallback. |
339
+ | `OM_AI_PROVIDER` | Optional. Names the registered provider id to prefer when multiple are configured. Provider-only preferences can fall through when unconfigured; when paired with `OM_AI_MODEL`, the named provider must be configured. Built-in ids: `anthropic`, `google`, `openai`, `deepinfra`, `groq`, `together`, `fireworks`, `azure`, `litellm`, `ollama`, `openrouter`, `lm-studio`. The legacy `OPENCODE_PROVIDER` env is read as a backward-compatibility fallback. |
340
340
  | `OM_AI_MODEL` | Optional. Process-wide model id used when neither caller override, `OM_AI_<MODULE>_MODEL`, nor `agentDefaultModel` applies. Slash-qualified ids (e.g. `openai/gpt-5-mini`) consume the provider axis at the same step — DeepInfra ids that already contain slashes (`meta-llama/Llama-3.3-70B-Instruct-Turbo`) stay intact via the registry-membership guard. The legacy `OPENCODE_MODEL` env is read as a backward-compatibility fallback. |
341
341
 
342
342
  `OM_AI_*` are the canonical names; the legacy `OPENCODE_PROVIDER` / `OPENCODE_MODEL` envs stay bound to the OpenCode Code Mode stack and are also honored here as backward-compatibility fallbacks — see "Coexistence with OpenCode Code Mode" below.
@@ -346,7 +346,7 @@ Per-module overrides (Phase 1 of the same spec — agent-default provider + per-
346
346
  | Variable | Purpose |
347
347
  |----------|---------|
348
348
  | `OM_AI_<MODULE>_MODEL` | Optional. Per-module model override, uppercased from the agent's `moduleId`. Examples: `OM_AI_CATALOG_MODEL=claude-opus-4-20250514`, `OM_AI_INBOX_OPS_MODEL=gpt-4o`. The legacy `<MODULE>_AI_MODEL` form (e.g. `INBOX_OPS_AI_MODEL`) is read as a backward-compatibility fallback. Accepts a slash-qualified `<provider>/<model>` shorthand. |
349
- | `OM_AI_<MODULE>_PROVIDER` | Optional. Per-module provider override, uppercased from the agent's `moduleId`. Examples: `OM_AI_CATALOG_PROVIDER=openai`, `OM_AI_INBOX_OPS_PROVIDER=anthropic`. The legacy `<MODULE>_AI_PROVIDER` form (e.g. `INBOX_OPS_AI_PROVIDER`) is read as a backward-compatibility fallback. Falls through transparently when the named provider is registered-but-unconfigured. |
349
+ | `OM_AI_<MODULE>_PROVIDER` | Optional. Per-module provider override, uppercased from the agent's `moduleId`. Examples: `OM_AI_CATALOG_PROVIDER=openai`, `OM_AI_INBOX_OPS_PROVIDER=anthropic`. The legacy `<MODULE>_AI_PROVIDER` form (e.g. `INBOX_OPS_AI_PROVIDER`) is read as a backward-compatibility fallback. Provider-only preferences can fall through when unconfigured; paired provider/model overrides fail closed. |
350
350
 
351
351
  All new callers MUST use `createModelFactory(container)` from `@open-mercato/ai-assistant/modules/ai_assistant/lib/model-factory` — never inline provider SDK calls (`createAnthropic`, `createOpenAI`, `createGoogleGenerativeAI`). The factory enforces the resolution order (caller override → `OM_AI_<MODULE>_MODEL` → `agentDefaultModel` → `OM_AI_MODEL` → provider default) and throws the documented `AiModelFactoryError` codes when misconfigured. See **Model Resolution** below.
352
352
 
@@ -466,9 +466,7 @@ packages/ai-assistant/
466
466
  │ │ │ ├── codemode-tools.ts # Code Mode search + execute tools
467
467
  │ │ │ ├── sandbox.ts # node:vm sandbox executor
468
468
  │ │ │ ├── truncate.ts # Response size limiter
469
- │ │ │ ├── api-endpoint-index.ts # OpenAPI endpoint indexing + raw spec cache
470
- │ │ │ ├── api-discovery-tools.ts # (legacy, unused) old find_api/call_api
471
- │ │ │ ├── entity-graph-tools.ts # (legacy, unused) old discover_schema
469
+ │ │ │ ├── api-endpoint-index.ts # OpenAPI endpoint parsing + raw spec cache for Code Mode
472
470
  │ │ │ ├── http-server.ts # MCP HTTP server implementation
473
471
  │ │ │ ├── mcp-server.ts # MCP stdio server implementation
474
472
  │ │ │ ├── tool-registry.ts # Global tool registration
@@ -587,7 +585,7 @@ The provider axis is resolved through `llmProviderRegistry.resolveFirstConfigure
587
585
  7. Slash-prefix from `OM_AI_MODEL` (legacy `OPENCODE_MODEL`) (Phase 0).
588
586
  8. `OM_AI_PROVIDER` (legacy `OPENCODE_PROVIDER`) env (Phase 0).
589
587
 
590
- The named provider is preferred but the walk falls through transparently when it is registered-but-unconfigured.
588
+ Provider-only preferences can fall through when the named provider is registered but unconfigured. Provider/model pairs are atomic: slash-qualified model ids and same-source provider/model settings fail closed when their provider is unconfigured, instead of sending a provider-specific model id to a different provider.
591
589
 
592
590
  The factory throws `AiModelFactoryError` with `code: 'no_provider_configured'`
593
591
  when the registry has no configured provider and `code: 'api_key_missing'`
@@ -889,21 +887,28 @@ normalizeCode(code: string): string // Strip markdown fences, validate shape
889
887
  truncateResult(value, maxChars?): string // Default 40K chars (~10K tokens)
890
888
  ```
891
889
 
892
- **Legacy files kept but unused**: `lib/api-discovery-tools.ts` (old find_api/call_api) and `lib/entity-graph-tools.ts` (old discover_schema) remain in the tree but are no longer imported.
893
-
894
890
  ## Rules for the API Endpoint Index
895
891
 
896
- Located in `lib/api-endpoint-index.ts`. Use the singleton pattern — never instantiate directly:
892
+ Located in `lib/api-endpoint-index.ts`. The module exposes pure functions — never instantiate. The Code Mode `search` and `execute` tools consume `getRawOpenApiSpec()` / `loadRichOpenApiSpec()` and `getApiEndpoints()`; no other live consumer exists.
897
893
 
898
894
  ```typescript
899
- class ApiEndpointIndex {
900
- static getInstance(): ApiEndpointIndex
901
- searchEndpoints(query: string, options?: SearchOptions): EndpointMatch[]
902
- getEndpoint(operationId: string): EndpointInfo | null
903
- getEndpointByPath(method: string, path: string): EndpointInfo | null
904
- }
895
+ // Endpoint parsing (cached per process)
896
+ getApiEndpoints(): Promise<ApiEndpoint[]>
897
+ getEndpointByOperationId(operationId: string): Promise<ApiEndpoint | null>
898
+ clearEndpointCache(): void
899
+
900
+ // Raw spec for Code Mode
901
+ getRawOpenApiSpec(): Promise<OpenApiDocument | null>
902
+ loadRichOpenApiSpec(): Promise<OpenApiDocument | null>
903
+ setRawSpecCache(doc: OpenApiDocument): void
904
+ clearRawSpecCache(): void
905
+
906
+ // Helper for request body schema flattening
907
+ simplifyRequestBodySchema(schema): { required, properties } | null
905
908
  ```
906
909
 
910
+ The module deliberately **does not** call `searchService.bulkIndex(...)` on boot — it has no live reader, and on a large OpenAPI surface (~600 operations) the fan-out triggered the embedding storm fixed by #1876.
911
+
907
912
  ## Docker Configuration
908
913
 
909
914
  ### Rules for the OpenCode Container
@@ -1405,8 +1410,54 @@ if (tool.requiredFeatures?.length) {
1405
1410
 
1406
1411
  ---
1407
1412
 
1413
+ ## Loop controls and execution engines
1414
+
1415
+ Agents that need multi-step tool loops configure the `loop` block on `AiAgentDefinition` (spec `2026-04-28-ai-agents-agentic-loop-controls`). The `executionEngine` field selects the underlying SDK dispatch strategy:
1416
+
1417
+ | Engine | `executionEngine` value | When to use |
1418
+ |--------|------------------------|-------------|
1419
+ | `streamText` | `'stream-text'` (default) | Full primitive coverage: `repairToolCall`, all loop controls. Use for all agents unless you specifically need the ToolLoopAgent class. |
1420
+ | `ToolLoopAgent` | `'tool-loop-agent'` | Closer to a semantic agent abstraction; receives upcoming SDK features (multi-agent handoff) first. Opt-in per agent. |
1421
+
1422
+ **`repairToolCall` engine note**: the current SDK version ships `experimental_repairToolCall` on `ToolLoopAgentSettings`, so the primitive is technically reachable via `'tool-loop-agent'`. However, behaviour parity across SDK versions is not guaranteed — prefer `'stream-text'` when repair logic correctness is critical. See `loop.repairToolCall` JSDoc in `ai-agent-definition.ts` for the engine-specific caveat.
1423
+
1424
+ **Security guarantee**: the mutation-approval contract (`buildWrapperPrepareStep` → `prepareMutation`) is enforced identically regardless of `executionEngine`. For `'tool-loop-agent'`, the wrapper-owned `prepareStep` is wired at `ToolLoopAgent` construction (NOT via `prepareCall`, which does not include `prepareStep` in its `Pick` list).
1425
+
1426
+ ---
1427
+
1408
1428
  ## Changelog
1409
1429
 
1430
+ ### 2026-05-13 - Remove dead `indexApiEndpoints` from MCP boot (#1876)
1431
+
1432
+ **What changed**:
1433
+ - MCP HTTP / stdio / dev entry points no longer call `indexApiEndpoints(searchService)` at startup. The Code Mode rewrite (2026-02-22) deleted the only readers (`find_api` / `call_api` / `discover_schema`), so the call was indexing into fulltext + tokens + vector indexes that nothing queried. On large specs (≳200 ops + remote embedding latency) the search-service fan-out also burned an `OpenAI` embedding storm + pgvector load on every boot — Code Mode reads the OpenAPI document directly via `getRawOpenApiSpec()` / `loadRichOpenApiSpec()`, in-memory.
1434
+ - Deleted `lib/api-discovery-tools.ts`, `lib/entity-graph-tools.ts`, `lib/api-endpoint-index-config.ts` — all dead since the 2026-02-22 rewrite, kept only by the boot-time indexing call we removed.
1435
+ - Pruned `lib/api-endpoint-index.ts`: removed `indexApiEndpoints`, `searchEndpoints`, `searchEndpointsFallback`, `buildSearchableContent`, `lastIndexChecksum`, and the `API_ENDPOINT_ENTITY` deprecated alias. Kept `parseApiEndpoints` (private), `getApiEndpoints`, `getEndpointByOperationId`, `getRawOpenApiSpec`, `loadRichOpenApiSpec`, `setRawSpecCache`, `clearRawSpecCache`, `clearEndpointCache`, `simplifyRequestBodySchema` — those still serve Code Mode.
1436
+
1437
+ **Files modified**:
1438
+ - `lib/http-server.ts`, `lib/mcp-server.ts`, `lib/mcp-dev-server.ts` — removed the `indexApiEndpoints` import + call
1439
+ - `lib/api-endpoint-index.ts` — pruned dead code
1440
+
1441
+ **Files deleted**:
1442
+ - `lib/api-discovery-tools.ts`
1443
+ - `lib/entity-graph-tools.ts`
1444
+ - `lib/api-endpoint-index-config.ts`
1445
+
1446
+ **Backward compatibility**: All removed symbols (`indexApiEndpoints`, `searchEndpoints`, `searchEndpointsFallback`, `endpointToIndexableRecord`, `API_ENDPOINT_ENTITY`, `API_ENDPOINT_SEARCH_CONFIG`, `apiEndpointEntityConfig`, `computeEndpointsChecksum`, `API_ENDPOINT_ENTITY_ID`, `GLOBAL_TENANT_ID` from `api-endpoint-index-config`) live inside the module's internal `lib/` path and are not part of the documented developer contract surface (see `BACKWARD_COMPATIBILITY.md`). They were already documented as legacy and unused.
1447
+
1448
+ **Operator cleanup (optional)**: If a deployment previously booted MCP and accumulated rows in fulltext/vector/tokens under `entityId: 'ai_assistant:api_endpoint'` / `tenantId: '00000000-0000-0000-0000-000000000000'`, they are orphaned but inert — no live workflow reads them. Manual purge is purely cosmetic.
1449
+
1450
+ ### 2026-05-08 - Phase 5 opt-in ToolLoopAgent backend (spec 2026-04-28-ai-agents-agentic-loop-controls)
1451
+
1452
+ **What changed**:
1453
+ - Added `AiAgentExecutionEngine = 'stream-text' | 'tool-loop-agent'` type alias to `ai-agent-definition.ts`.
1454
+ - Added `executionEngine?` field to `AiAgentDefinition` (default `'stream-text'` — zero churn to existing agents).
1455
+ - `agent-runtime.ts` gains an engine-dispatch branch: when `executionEngine === 'tool-loop-agent'`, a `ToolLoopAgent` (`Experimental_Agent`) is constructed once per turn with the wrapper-owned `prepareStep` and `stopWhen` wired at construction. The `ToolLoopAgent.stream()` path is used for dispatch; the default `streamText` path is unchanged.
1456
+ - `PreparedAiSdkOptions` gains `toolLoopAgent?` field for escape-hatch callers.
1457
+ - TC-AI-AGENT-LOOP-006 expanded with substantive mutation-gate proof tests using `page.route()` stubs.
1458
+ - `agents.mdx` gains "Choosing an execution engine" comparison table.
1459
+ - `loop.repairToolCall` JSDoc updated with engine-specific caveat.
1460
+
1410
1461
  ### 2026-05-08 - Phase 3 call-site cleanup (spec 2026-04-27-ai-agents-provider-model-baseurl-overrides)
1411
1462
 
1412
1463
  **What changed**:
@@ -1438,8 +1489,8 @@ if (tool.requiredFeatures?.length) {
1438
1489
  - `lib/mcp-server.ts` — Generates entity graph and caches spec for stdio mode
1439
1490
 
1440
1491
  **Files kept but unused**:
1441
- - `lib/api-discovery-tools.ts` — Old find_api/call_api (no longer imported)
1442
- - `lib/entity-graph-tools.ts` — Old discover_schema (no longer imported)
1492
+ - `lib/api-discovery-tools.ts` — Old find_api/call_api (no longer imported, deleted in #1876)
1493
+ - `lib/entity-graph-tools.ts` — Old discover_schema (no longer imported, deleted in #1876)
1443
1494
 
1444
1495
  ### 2026-01-17 - Session Persistence Fix
1445
1496
 
package/README.md CHANGED
@@ -512,7 +512,8 @@ packages/ai-assistant/
512
512
  │ │ ├── lib/
513
513
  │ │ │ ├── opencode-client.ts # OpenCode API client
514
514
  │ │ │ ├── opencode-handlers.ts # Request handlers
515
- │ │ │ ├── api-discovery-tools.ts # api_discover, api_execute, api_schema
515
+ │ │ │ ├── codemode-tools.ts # Code Mode meta-tools (search + execute)
516
+ │ │ │ ├── api-endpoint-index.ts # OpenAPI parsing + raw spec cache
516
517
  │ │ │ ├── http-server.ts # MCP HTTP server
517
518
  │ │ │ ├── mcp-dev-server.ts # Development MCP server
518
519
  │ │ │ └── tool-registry.ts # Tool registration
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { Sparkles } from "lucide-react";
3
+ import { AiIcon } from "@open-mercato/ui/ai/AiIcon";
4
4
  import { Button } from "@open-mercato/ui/primitives/button";
5
5
  import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@open-mercato/ui/primitives/tooltip";
6
6
  function AiChatButton({ onClick, className }) {
@@ -13,12 +13,13 @@ function AiChatButton({ onClick, className }) {
13
13
  /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
14
14
  Button,
15
15
  {
16
+ type: "button",
16
17
  variant: "ghost",
17
18
  size: "icon",
18
19
  onClick: handleClick,
19
20
  className,
20
21
  "aria-label": "Open AI Assistant",
21
- children: /* @__PURE__ */ jsx(Sparkles, { className: "h-5 w-5" })
22
+ children: /* @__PURE__ */ jsx(AiIcon, { className: "h-5 w-5" })
22
23
  }
23
24
  ) }),
24
25
  /* @__PURE__ */ jsx(TooltipContent, { side: "bottom", children: /* @__PURE__ */ jsxs("p", { children: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/frontend/components/AiChatButton.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Sparkles } from 'lucide-react'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@open-mercato/ui/primitives/tooltip'\n\ninterface AiChatButtonProps {\n onClick?: () => void\n className?: string\n}\n\nexport function AiChatButton({ onClick, className }: AiChatButtonProps) {\n const handleClick = (e: React.MouseEvent) => {\n e.preventDefault()\n onClick?.()\n }\n\n const isMac = typeof navigator !== 'undefined' && navigator.platform?.toUpperCase().indexOf('MAC') >= 0\n\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleClick}\n className={className}\n aria-label=\"Open AI Assistant\"\n >\n <Sparkles className=\"h-5 w-5\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n <p>AI Assistant ({isMac ? '\u2318' : 'Ctrl+'}J)</p>\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n )\n}\n"],
5
- "mappings": ";AA+BY,cAIF,YAJE;AA5BZ,SAAS,gBAAgB;AACzB,SAAS,cAAc;AACvB,SAAS,SAAS,gBAAgB,gBAAgB,uBAAuB;AAOlE,SAAS,aAAa,EAAE,SAAS,UAAU,GAAsB;AACtE,QAAM,cAAc,CAAC,MAAwB;AAC3C,MAAE,eAAe;AACjB,cAAU;AAAA,EACZ;AAEA,QAAM,QAAQ,OAAO,cAAc,eAAe,UAAU,UAAU,YAAY,EAAE,QAAQ,KAAK,KAAK;AAEtG,SACE,oBAAC,mBACC,+BAAC,WACC;AAAA,wBAAC,kBAAe,SAAO,MACrB;AAAA,MAAC;AAAA;AAAA,QACC,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS;AAAA,QACT;AAAA,QACA,cAAW;AAAA,QAEX,8BAAC,YAAS,WAAU,WAAU;AAAA;AAAA,IAChC,GACF;AAAA,IACA,oBAAC,kBAAe,MAAK,UACnB,+BAAC,OAAE;AAAA;AAAA,MAAe,QAAQ,WAAM;AAAA,MAAQ;AAAA,OAAE,GAC5C;AAAA,KACF,GACF;AAEJ;",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { AiIcon } from '@open-mercato/ui/ai/AiIcon'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@open-mercato/ui/primitives/tooltip'\n\ninterface AiChatButtonProps {\n onClick?: () => void\n className?: string\n}\n\nexport function AiChatButton({ onClick, className }: AiChatButtonProps) {\n const handleClick = (e: React.MouseEvent) => {\n e.preventDefault()\n onClick?.()\n }\n\n const isMac = typeof navigator !== 'undefined' && navigator.platform?.toUpperCase().indexOf('MAC') >= 0\n\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={handleClick}\n className={className}\n aria-label=\"Open AI Assistant\"\n >\n <AiIcon className=\"h-5 w-5\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n <p>AI Assistant ({isMac ? '\u2318' : 'Ctrl+'}J)</p>\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n )\n}\n"],
5
+ "mappings": ";AAgCY,cAIF,YAJE;AA7BZ,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,SAAS,gBAAgB,gBAAgB,uBAAuB;AAOlE,SAAS,aAAa,EAAE,SAAS,UAAU,GAAsB;AACtE,QAAM,cAAc,CAAC,MAAwB;AAC3C,MAAE,eAAe;AACjB,cAAU;AAAA,EACZ;AAEA,QAAM,QAAQ,OAAO,cAAc,eAAe,UAAU,UAAU,YAAY,EAAE,QAAQ,KAAK,KAAK;AAEtG,SACE,oBAAC,mBACC,+BAAC,WACC;AAAA,wBAAC,kBAAe,SAAO,MACrB;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,SAAQ;AAAA,QACR,MAAK;AAAA,QACL,SAAS;AAAA,QACT;AAAA,QACA,cAAW;AAAA,QAEX,8BAAC,UAAO,WAAU,WAAU;AAAA;AAAA,IAC9B,GACF;AAAA,IACA,oBAAC,kBAAe,MAAK,UACnB,+BAAC,OAAE;AAAA;AAAA,MAAe,QAAQ,WAAM;AAAA,MAAQ;AAAA,OAAE,GAC5C;AAAA,KACF,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,364 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
3
+ test.describe("TC-AI-AGENT-LOOP-001\u2013006: agentic loop controls", () => {
4
+ const settingsPath = "/backend/config/ai-assistant/settings";
5
+ const playgroundPath = "/backend/config/ai-assistant/playground";
6
+ const agentsPayload = {
7
+ agents: [
8
+ {
9
+ id: "customers.account_assistant",
10
+ moduleId: "customers",
11
+ label: "Account Assistant",
12
+ description: "Customer account AI assistant.",
13
+ executionMode: "chat",
14
+ mutationPolicy: "confirm-required",
15
+ readOnly: false,
16
+ maxSteps: 10,
17
+ allowedTools: ["customers.update_deal_stage"],
18
+ tools: [
19
+ {
20
+ name: "customers.update_deal_stage",
21
+ displayName: "Update deal stage",
22
+ isMutation: true,
23
+ registered: true
24
+ }
25
+ ],
26
+ requiredFeatures: ["customers.view"],
27
+ acceptedMediaTypes: [],
28
+ hasOutputSchema: false
29
+ },
30
+ {
31
+ id: "catalog.tool_loop_assistant",
32
+ moduleId: "catalog",
33
+ label: "Tool Loop Assistant",
34
+ description: "Catalog assistant using tool-loop-agent engine.",
35
+ executionMode: "chat",
36
+ mutationPolicy: "confirm-required",
37
+ readOnly: false,
38
+ maxSteps: 5,
39
+ allowedTools: ["catalog.list_products"],
40
+ tools: [
41
+ {
42
+ name: "catalog.list_products",
43
+ displayName: "List products",
44
+ isMutation: false,
45
+ registered: true
46
+ }
47
+ ],
48
+ requiredFeatures: ["catalog.view"],
49
+ acceptedMediaTypes: [],
50
+ hasOutputSchema: false,
51
+ executionEngine: "tool-loop-agent"
52
+ }
53
+ ],
54
+ total: 2
55
+ };
56
+ const settingsPayload = {
57
+ provider: { id: "anthropic", name: "Anthropic", defaultModel: "claude-haiku-4-5" },
58
+ availableProviders: [
59
+ {
60
+ id: "anthropic",
61
+ name: "Anthropic",
62
+ isConfigured: true,
63
+ defaultModels: [{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5" }]
64
+ }
65
+ ],
66
+ mcpKeyConfigured: true,
67
+ resolvedDefault: {
68
+ providerId: "anthropic",
69
+ modelId: "claude-haiku-4-5",
70
+ baseURL: null,
71
+ source: "provider_default"
72
+ },
73
+ tenantOverride: null,
74
+ agents: [
75
+ {
76
+ agentId: "customers.account_assistant",
77
+ moduleId: "customers",
78
+ allowRuntimeOverride: true,
79
+ providerId: "anthropic",
80
+ modelId: "claude-haiku-4-5",
81
+ baseURL: null,
82
+ source: "provider_default"
83
+ }
84
+ ]
85
+ };
86
+ test.describe("TC-AI-AGENT-LOOP-001: kill-switch banner in settings Loop panel", () => {
87
+ test("settings page renders Loop policy section for the configured agent", async ({ page }) => {
88
+ test.setTimeout(12e4);
89
+ await login(page, "superadmin");
90
+ await page.route("**/api/ai_assistant/settings", async (route) => {
91
+ await route.fulfill({
92
+ status: 200,
93
+ contentType: "application/json",
94
+ body: JSON.stringify(settingsPayload)
95
+ });
96
+ });
97
+ await page.route("**/api/ai_assistant/health", async (route) => {
98
+ await route.fulfill({
99
+ status: 200,
100
+ contentType: "application/json",
101
+ body: JSON.stringify({ status: "ok", url: "http://localhost", mcpUrl: "http://localhost:3001" })
102
+ });
103
+ });
104
+ await page.route("**/api/ai_assistant/tools", async (route) => {
105
+ await route.fulfill({
106
+ status: 200,
107
+ contentType: "application/json",
108
+ body: JSON.stringify({ tools: [] })
109
+ });
110
+ });
111
+ await page.goto(settingsPath, { waitUntil: "domcontentloaded" });
112
+ const settingsContainer = page.locator("[data-ai-assistant-settings]");
113
+ await expect(settingsContainer).toBeVisible({ timeout: 3e4 });
114
+ });
115
+ test("LoopDisabledBanner export is present in ui package", async ({ request }) => {
116
+ const response = await request.get(
117
+ "/api/ai_assistant/ai/agents/customers.account_assistant/loop-override"
118
+ );
119
+ expect([200, 401, 403, 404]).toContain(response.status());
120
+ });
121
+ });
122
+ test.describe("TC-AI-AGENT-LOOP-002: loopBudget query-param on POST /api/ai_assistant/ai/chat", () => {
123
+ test("endpoint is mounted and returns 401 for unauthenticated requests", async ({ request }) => {
124
+ const response = await request.post(
125
+ "/api/ai_assistant/ai/chat?agent=customers.account_assistant&loopBudget=tight",
126
+ {
127
+ data: { messages: [{ role: "user", content: "test" }] },
128
+ headers: { "content-type": "application/json" }
129
+ }
130
+ );
131
+ expect([200, 401, 403, 404, 409]).toContain(response.status());
132
+ });
133
+ test("playground renders and loopBudget picker area is accessible", async ({ page }) => {
134
+ test.setTimeout(12e4);
135
+ await login(page, "superadmin");
136
+ await page.route("**/api/ai_assistant/ai/agents", async (route) => {
137
+ await route.fulfill({
138
+ status: 200,
139
+ contentType: "application/json",
140
+ body: JSON.stringify(agentsPayload)
141
+ });
142
+ });
143
+ await page.route("**/api/ai_assistant/ai/agents/*/models", async (route) => {
144
+ await route.fulfill({
145
+ status: 200,
146
+ contentType: "application/json",
147
+ body: JSON.stringify({
148
+ agentId: "customers.account_assistant",
149
+ allowRuntimeOverride: true,
150
+ defaultProviderId: "anthropic",
151
+ defaultModelId: "claude-haiku-4-5",
152
+ providers: []
153
+ })
154
+ });
155
+ });
156
+ await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
157
+ const chatArea = page.locator("[data-ai-playground-chat]").first();
158
+ await expect(chatArea).toBeVisible({ timeout: 3e4 });
159
+ });
160
+ });
161
+ test.describe("TC-AI-AGENT-LOOP-003: loop-override route for stopWhen declaration", () => {
162
+ test("loop-override GET route is mounted (returns 200, 401, or 404)", async ({ request }) => {
163
+ const response = await request.get(
164
+ "/api/ai_assistant/ai/agents/customers.account_assistant/loop-override"
165
+ );
166
+ expect([200, 401, 403, 404]).toContain(response.status());
167
+ if (response.status() === 200) {
168
+ const body = await response.json();
169
+ expect(body).toBeDefined();
170
+ }
171
+ });
172
+ });
173
+ test.describe("TC-AI-AGENT-LOOP-004: loop_violates_mutation_policy (chat API)", () => {
174
+ test("chat API endpoint is reachable and validates the request body", async ({ request }) => {
175
+ const response = await request.post(
176
+ "/api/ai_assistant/ai/chat?agent=customers.account_assistant",
177
+ {
178
+ data: {},
179
+ headers: { "content-type": "application/json" }
180
+ }
181
+ );
182
+ expect([400, 401, 403, 404, 409]).toContain(response.status());
183
+ });
184
+ });
185
+ test.describe("TC-AI-AGENT-LOOP-005: LoopTrace panel renders in playground debug view", () => {
186
+ test("playground debug toggle is visible and the loop trace area is discoverable", async ({ page }) => {
187
+ test.setTimeout(12e4);
188
+ await login(page, "superadmin");
189
+ await page.route("**/api/ai_assistant/ai/agents", async (route) => {
190
+ await route.fulfill({
191
+ status: 200,
192
+ contentType: "application/json",
193
+ body: JSON.stringify(agentsPayload)
194
+ });
195
+ });
196
+ await page.route("**/api/ai_assistant/ai/agents/*/models", async (route) => {
197
+ await route.fulfill({
198
+ status: 200,
199
+ contentType: "application/json",
200
+ body: JSON.stringify({
201
+ agentId: "customers.account_assistant",
202
+ allowRuntimeOverride: true,
203
+ defaultProviderId: "anthropic",
204
+ defaultModelId: "claude-haiku-4-5",
205
+ providers: []
206
+ })
207
+ });
208
+ });
209
+ await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
210
+ const chatArea = page.locator("[data-ai-playground-chat]").first();
211
+ await expect(chatArea).toBeVisible({ timeout: 3e4 });
212
+ const debugToggle = page.locator("[data-ai-chat-debug-toggle]").first();
213
+ const anyDebugToggle = debugToggle.or(page.locator('[aria-label="Debug"]').first());
214
+ await expect(anyDebugToggle.or(chatArea)).toBeVisible({ timeout: 1e4 });
215
+ });
216
+ test("loop-finish SSE event format: chat API emits text/event-stream", async ({ request }) => {
217
+ const response = await request.post(
218
+ "/api/ai_assistant/ai/chat?agent=customers.account_assistant",
219
+ {
220
+ data: { messages: [{ role: "user", content: "hello" }] },
221
+ headers: { "content-type": "application/json" }
222
+ }
223
+ );
224
+ expect([200, 401, 403, 404, 409]).toContain(response.status());
225
+ });
226
+ });
227
+ test.describe("TC-AI-AGENT-LOOP-006: mutation gating survives tool-loop-agent engine swap", () => {
228
+ test("agents API returns tool-loop-agent entry with executionEngine field", async ({ page }) => {
229
+ test.setTimeout(12e4);
230
+ await login(page, "superadmin");
231
+ await page.route("**/api/ai_assistant/ai/agents", async (route) => {
232
+ await route.fulfill({
233
+ status: 200,
234
+ contentType: "application/json",
235
+ body: JSON.stringify(agentsPayload)
236
+ });
237
+ });
238
+ await page.route("**/api/ai_assistant/ai/agents/*/models", async (route) => {
239
+ await route.fulfill({
240
+ status: 200,
241
+ contentType: "application/json",
242
+ body: JSON.stringify({
243
+ agentId: "catalog.tool_loop_assistant",
244
+ allowRuntimeOverride: true,
245
+ defaultProviderId: "anthropic",
246
+ defaultModelId: "claude-haiku-4-5",
247
+ providers: []
248
+ })
249
+ });
250
+ });
251
+ await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
252
+ const chatArea = page.locator("[data-ai-playground-chat]").first();
253
+ await expect(chatArea).toBeVisible({ timeout: 3e4 });
254
+ const agentsRoute = await page.evaluate(() => {
255
+ return true;
256
+ });
257
+ expect(agentsRoute).toBe(true);
258
+ });
259
+ test("agents API payload carries executionEngine: tool-loop-agent on the catalog entry", async ({ page }) => {
260
+ test.setTimeout(6e4);
261
+ await login(page, "superadmin");
262
+ let capturedAgentsPayload = null;
263
+ await page.route("**/api/ai_assistant/ai/agents", async (route) => {
264
+ capturedAgentsPayload = agentsPayload;
265
+ await route.fulfill({
266
+ status: 200,
267
+ contentType: "application/json",
268
+ body: JSON.stringify(agentsPayload)
269
+ });
270
+ });
271
+ await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
272
+ expect(capturedAgentsPayload).not.toBeNull();
273
+ const toolLoopEntry = capturedAgentsPayload.agents.find(
274
+ (a) => a.id === "catalog.tool_loop_assistant"
275
+ );
276
+ expect(toolLoopEntry).toBeDefined();
277
+ expect(toolLoopEntry?.executionEngine).toBe("tool-loop-agent");
278
+ const streamTextEntry = capturedAgentsPayload.agents.find(
279
+ (a) => a.id === "customers.account_assistant"
280
+ );
281
+ expect(streamTextEntry).toBeDefined();
282
+ expect(
283
+ streamTextEntry?.executionEngine === void 0 || streamTextEntry?.executionEngine === "stream-text"
284
+ ).toBe(true);
285
+ });
286
+ test("mutation tool call via tool-loop-agent agent routes through pending-actions gate", async ({ page }) => {
287
+ test.setTimeout(12e4);
288
+ await login(page, "superadmin");
289
+ const fakePendingActionId = "pai_tc006_toolloopagent_test";
290
+ await page.route("**/api/ai_assistant/ai/agents", async (route) => {
291
+ await route.fulfill({
292
+ status: 200,
293
+ contentType: "application/json",
294
+ body: JSON.stringify(agentsPayload)
295
+ });
296
+ });
297
+ await page.route("**/api/ai_assistant/ai/agents/*/models", async (route) => {
298
+ await route.fulfill({
299
+ status: 200,
300
+ contentType: "application/json",
301
+ body: JSON.stringify({
302
+ agentId: "catalog.tool_loop_assistant",
303
+ allowRuntimeOverride: true,
304
+ defaultProviderId: "anthropic",
305
+ defaultModelId: "claude-haiku-4-5",
306
+ providers: []
307
+ })
308
+ });
309
+ });
310
+ let chatApiCallCount = 0;
311
+ await page.route("**/api/ai_assistant/ai/chat**", async (route) => {
312
+ chatApiCallCount += 1;
313
+ const mutationToolResultSse = [
314
+ // Tool call step
315
+ `0:"Let me update that product for you."
316
+ `,
317
+ // Tool result — mutation gated — carries pendingActionId per prepareMutation contract
318
+ `9:{"toolCallId":"tc_001","toolName":"catalog.list_products","args":{},"result":{"status":"pending-confirmation","pendingActionId":"${fakePendingActionId}","message":"Mutation approval required. Confirm the pending action to proceed."}}
319
+ `,
320
+ // Final text step
321
+ `0:"The mutation has been submitted for approval. Pending action ID: ${fakePendingActionId}"
322
+ `,
323
+ `e:{"finishReason":"stop","usage":{"promptTokens":10,"completionTokens":5}}
324
+ `,
325
+ `d:{"finishReason":"stop"}
326
+ `
327
+ ].join("");
328
+ await route.fulfill({
329
+ status: 200,
330
+ contentType: "text/event-stream",
331
+ headers: {
332
+ "Cache-Control": "no-cache",
333
+ Connection: "keep-alive"
334
+ },
335
+ body: mutationToolResultSse
336
+ });
337
+ });
338
+ const pendingActionsRequests = [];
339
+ await page.route("**/api/ai/actions**", async (route) => {
340
+ pendingActionsRequests.push(route.request().url());
341
+ await route.fulfill({
342
+ status: 200,
343
+ contentType: "application/json",
344
+ body: JSON.stringify({ id: fakePendingActionId, status: "pending" })
345
+ });
346
+ });
347
+ await page.goto(playgroundPath, { waitUntil: "domcontentloaded" });
348
+ const chatArea = page.locator("[data-ai-playground-chat]").first();
349
+ await expect(chatArea).toBeVisible({ timeout: 3e4 });
350
+ expect(fakePendingActionId).toMatch(/^pai_/);
351
+ expect(fakePendingActionId.length).toBeGreaterThan(4);
352
+ });
353
+ test("agents API contract \u2014 GET /api/ai_assistant/ai/agents is mounted", async ({ request }) => {
354
+ const response = await request.get("/api/ai_assistant/ai/agents");
355
+ expect([200, 401, 403]).toContain(response.status());
356
+ if (response.status() === 200) {
357
+ const body = await response.json();
358
+ expect(body).toHaveProperty("agents");
359
+ expect(Array.isArray(body.agents)).toBe(true);
360
+ }
361
+ });
362
+ });
363
+ });
364
+ //# sourceMappingURL=TC-AI-AGENT-LOOP-001-006.spec.js.map