@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +69 -18
- package/README.md +2 -1
- package/dist/frontend/components/AiChatButton.js +3 -2
- package/dist/frontend/components/AiChatButton.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +4 -3
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +12 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
- package/dist/modules/ai_assistant/data/entities.js +177 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +8 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +74 -1
- package/dist/modules/ai_assistant/i18n/en.json +74 -1
- package/dist/modules/ai_assistant/i18n/es.json +75 -2
- package/dist/modules/ai_assistant/i18n/pl.json +74 -1
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
- package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js +0 -111
- package/dist/modules/ai_assistant/lib/api-endpoint-index.js.map +2 -2
- package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/http-server.js +0 -5
- package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js +0 -5
- package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/mcp-server.js +0 -5
- package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +34 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
- package/generated/entities/ai_token_usage_daily/index.ts +16 -0
- package/generated/entities/ai_token_usage_event/index.ts +19 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +47 -1
- package/package.json +14 -5
- package/src/frontend/components/AiChatButton.tsx +3 -2
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
- package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
- package/src/modules/ai_assistant/api/settings/route.ts +5 -3
- package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
- package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
- package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/data/entities.ts +237 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
- package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
- package/src/modules/ai_assistant/events.ts +8 -0
- package/src/modules/ai_assistant/i18n/de.json +74 -1
- package/src/modules/ai_assistant/i18n/en.json +74 -1
- package/src/modules/ai_assistant/i18n/es.json +75 -2
- package/src/modules/ai_assistant/i18n/pl.json +74 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
- package/src/modules/ai_assistant/lib/__tests__/mcp-startup-no-dead-index.test.ts +65 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
- package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
- package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
- package/src/modules/ai_assistant/lib/api-endpoint-index.ts +5 -186
- package/src/modules/ai_assistant/lib/codemode-tools.ts +0 -2
- package/src/modules/ai_assistant/lib/http-server.ts +2 -9
- package/src/modules/ai_assistant/lib/mcp-dev-server.ts +2 -8
- package/src/modules/ai_assistant/lib/mcp-server.ts +1 -10
- package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
- package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
- package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
- package/src/modules/ai_assistant/setup.ts +49 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js +0 -170
- package/dist/modules/ai_assistant/lib/api-discovery-tools.js.map +0 -7
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js +0 -177
- package/dist/modules/ai_assistant/lib/api-endpoint-index-config.js.map +0 -7
- package/dist/modules/ai_assistant/lib/entity-graph-tools.js +0 -127
- package/dist/modules/ai_assistant/lib/entity-graph-tools.js.map +0 -7
- package/src/modules/ai_assistant/lib/api-discovery-tools.ts +0 -250
- package/src/modules/ai_assistant/lib/api-endpoint-index-config.ts +0 -243
- package/src/modules/ai_assistant/lib/entity-graph-tools.ts +0 -192
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:ai-assistant] found
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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`.
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
│ │ │ ├──
|
|
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 {
|
|
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(
|
|
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 {
|
|
5
|
-
"mappings": ";
|
|
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
|