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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +30 -4
  3. package/dist/frontend/components/AiChatButton.js +3 -2
  4. package/dist/frontend/components/AiChatButton.js.map +2 -2
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
  7. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
  8. package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
  9. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
  10. package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
  11. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
  12. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
  13. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
  14. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
  15. package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
  16. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  17. package/dist/modules/ai_assistant/api/settings/route.js +4 -3
  18. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  19. package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
  20. package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
  21. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
  22. package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
  23. package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
  24. package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
  25. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
  26. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  27. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
  28. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
  29. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
  30. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  31. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
  32. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
  33. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
  34. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
  35. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
  36. package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
  37. package/dist/modules/ai_assistant/cli.js +12 -0
  38. package/dist/modules/ai_assistant/cli.js.map +2 -2
  39. package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
  40. package/dist/modules/ai_assistant/data/entities.js +177 -1
  41. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  42. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
  43. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  44. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
  45. package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
  46. package/dist/modules/ai_assistant/events.js +8 -0
  47. package/dist/modules/ai_assistant/events.js.map +2 -2
  48. package/dist/modules/ai_assistant/i18n/de.json +74 -1
  49. package/dist/modules/ai_assistant/i18n/en.json +74 -1
  50. package/dist/modules/ai_assistant/i18n/es.json +75 -2
  51. package/dist/modules/ai_assistant/i18n/pl.json +74 -1
  52. package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
  53. package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
  54. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
  55. package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
  56. package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
  57. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  58. package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
  59. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  60. package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
  61. package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
  62. package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
  63. package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
  64. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
  65. package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
  66. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
  67. package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
  68. package/dist/modules/ai_assistant/setup.js +34 -0
  69. package/dist/modules/ai_assistant/setup.js.map +2 -2
  70. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
  71. package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
  72. package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
  73. package/generated/entities/ai_token_usage_daily/index.ts +16 -0
  74. package/generated/entities/ai_token_usage_event/index.ts +19 -0
  75. package/generated/entities.ids.generated.ts +2 -0
  76. package/generated/entity-fields-registry.ts +47 -1
  77. package/package.json +15 -7
  78. package/src/frontend/components/AiChatButton.tsx +3 -2
  79. package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
  80. package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
  81. package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
  82. package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
  83. package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
  84. package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
  85. package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
  86. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
  87. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
  88. package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
  89. package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
  90. package/src/modules/ai_assistant/api/settings/route.ts +5 -3
  91. package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
  92. package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
  93. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
  94. package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
  95. package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
  96. package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
  97. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
  98. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
  99. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
  100. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
  101. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
  102. package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
  103. package/src/modules/ai_assistant/cli.ts +18 -0
  104. package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
  105. package/src/modules/ai_assistant/data/entities.ts +237 -0
  106. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
  107. package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
  108. package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
  109. package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
  110. package/src/modules/ai_assistant/events.ts +8 -0
  111. package/src/modules/ai_assistant/i18n/de.json +74 -1
  112. package/src/modules/ai_assistant/i18n/en.json +74 -1
  113. package/src/modules/ai_assistant/i18n/es.json +75 -2
  114. package/src/modules/ai_assistant/i18n/pl.json +74 -1
  115. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
  116. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
  117. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
  118. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
  119. package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
  120. package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
  121. package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
  122. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
  123. package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
  124. package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
  125. package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
  126. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
  127. package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
  128. package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
  129. package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
  130. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
  131. package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
  132. package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
  133. package/src/modules/ai_assistant/setup.ts +49 -0
  134. package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
  135. package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
@@ -1,13 +1,21 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { createContainer } from "awilix";
2
3
  import {
3
4
  convertToModelMessages,
4
5
  generateObject,
6
+ hasToolCall,
5
7
  stepCountIs,
6
8
  streamObject,
7
- streamText
9
+ streamText,
10
+ Experimental_Agent as ToolLoopAgent
8
11
  } from "ai";
9
- import { createModelFactory } from "./model-factory.js";
10
- import { resolveAiAgentTools, AgentPolicyError } from "./agent-tools.js";
12
+ import { createModelFactory, resolveAllowRuntimeOverride } from "./model-factory.js";
13
+ import {
14
+ resolveAiAgentTools,
15
+ AgentPolicyError,
16
+ desanitizeToolNameForDisplay,
17
+ sanitizeToolNameForModel
18
+ } from "./agent-tools.js";
11
19
  import { resolveEffectiveMutationPolicy } from "./agent-policy.js";
12
20
  import { toolRegistry } from "./tool-registry.js";
13
21
  import {
@@ -21,10 +29,377 @@ import { AiAgentRuntimeOverrideRepository } from "../data/repositories/AiAgentRu
21
29
  import { AiTenantModelAllowlistRepository } from "../data/repositories/AiTenantModelAllowlistRepository.js";
22
30
  import { composeSystemPromptWithOverride } from "./prompt-override-merge.js";
23
31
  import { isKnownMutationPolicy } from "./agent-policy.js";
32
+ import { recordTokenUsage } from "./token-usage-recorder.js";
24
33
  import "./llm-bootstrap.js";
34
+ const WRAPPER_DEFAULT_LOOP_CHAT = { maxSteps: 10 };
35
+ const WRAPPER_DEFAULT_LOOP_OBJECT = {};
36
+ function resolveLoopBudgetPreset(preset) {
37
+ switch (preset) {
38
+ case "tight":
39
+ return { maxSteps: 3, budget: { maxToolCalls: 3, maxWallClockMs: 1e4, maxTokens: 5e4 } };
40
+ case "loose":
41
+ return { maxSteps: 20, budget: { maxToolCalls: 20, maxWallClockMs: 12e4, maxTokens: 5e5 } };
42
+ case "default":
43
+ return void 0;
44
+ }
45
+ }
46
+ const SSE_ENCODER = new TextEncoder();
47
+ function appendLoopFinishToStream(baseResponse, finalizeLoopTrace) {
48
+ const { readable, writable } = new TransformStream();
49
+ const writer = writable.getWriter();
50
+ async function pump() {
51
+ if (!baseResponse.body) {
52
+ await writer.close();
53
+ return;
54
+ }
55
+ const reader = baseResponse.body.getReader();
56
+ try {
57
+ for (; ; ) {
58
+ const { value, done } = await reader.read();
59
+ if (done) break;
60
+ await writer.write(value);
61
+ }
62
+ const trace = finalizeLoopTrace();
63
+ const eventLine = `data: ${JSON.stringify({ type: "loop-finish", trace })}
64
+
65
+ `;
66
+ await writer.write(SSE_ENCODER.encode(eventLine));
67
+ } catch {
68
+ } finally {
69
+ reader.releaseLock();
70
+ await writer.close().catch(() => void 0);
71
+ }
72
+ }
73
+ void pump();
74
+ return new Response(readable, {
75
+ status: baseResponse.status,
76
+ headers: baseResponse.headers
77
+ });
78
+ }
79
+ function assertLoopRuntimeOverrideAllowed(agent, callerLoop) {
80
+ if (!callerLoop) return;
81
+ const allowed = agent.loop?.allowRuntimeOverride ?? true;
82
+ if (!allowed) {
83
+ throw new AgentPolicyError(
84
+ "loop_runtime_override_disabled",
85
+ `Agent "${agent.id}" has disabled per-call loop overrides (loop.allowRuntimeOverride: false). Remove the loop override to proceed.`
86
+ );
87
+ }
88
+ }
89
+ function readModuleLoopEnv(moduleId) {
90
+ const prefix = moduleId.toUpperCase();
91
+ const partial = {};
92
+ const maxStepsRaw = process.env[`${prefix}_AI_LOOP_MAX_STEPS`];
93
+ if (maxStepsRaw) {
94
+ const parsed = parseInt(maxStepsRaw.trim(), 10);
95
+ if (!isNaN(parsed) && parsed > 0) partial.maxSteps = parsed;
96
+ }
97
+ const maxWallClockRaw = process.env[`${prefix}_AI_LOOP_MAX_WALL_CLOCK_MS`];
98
+ const maxTokensRaw = process.env[`${prefix}_AI_LOOP_MAX_TOKENS`];
99
+ if (maxWallClockRaw || maxTokensRaw) {
100
+ const budgetPartial = {};
101
+ if (maxWallClockRaw) {
102
+ const parsed = parseInt(maxWallClockRaw.trim(), 10);
103
+ if (!isNaN(parsed) && parsed > 0) budgetPartial.maxWallClockMs = parsed;
104
+ }
105
+ if (maxTokensRaw) {
106
+ const parsed = parseInt(maxTokensRaw.trim(), 10);
107
+ if (!isNaN(parsed) && parsed > 0) budgetPartial.maxTokens = parsed;
108
+ }
109
+ if (Object.keys(budgetPartial).length > 0) partial.budget = budgetPartial;
110
+ }
111
+ return partial;
112
+ }
113
+ function resolveEffectiveLoopConfig(agent, callerLoop, wrapperDefault) {
114
+ assertLoopRuntimeOverrideAllowed(agent, callerLoop);
115
+ const effectiveDefault = wrapperDefault ?? WRAPPER_DEFAULT_LOOP_CHAT;
116
+ const legacyMaxSteps = typeof agent.maxSteps === "number" && agent.maxSteps > 0 && !agent.loop ? { maxSteps: agent.maxSteps } : void 0;
117
+ const base = {
118
+ ...effectiveDefault,
119
+ ...legacyMaxSteps ?? {},
120
+ ...agent.loop ?? {}
121
+ };
122
+ const envOverride = readModuleLoopEnv(agent.moduleId);
123
+ const withEnv = {
124
+ ...base,
125
+ ...envOverride,
126
+ ...envOverride.budget != null ? { budget: { ...base.budget ?? {}, ...envOverride.budget } } : {}
127
+ };
128
+ const withCaller = callerLoop ? { ...withEnv, ...callerLoop } : withEnv;
129
+ if (withCaller.disabled === true) {
130
+ return { ...withCaller, maxSteps: 1 };
131
+ }
132
+ return withCaller;
133
+ }
134
+ class BudgetEnforcer {
135
+ constructor(budget, abortController) {
136
+ this.budget = budget;
137
+ this.abortController = abortController;
138
+ this.toolCallsUsed = 0;
139
+ this.tokensUsed = 0;
140
+ this.abortReason = null;
141
+ this.turnStartMs = Date.now();
142
+ }
143
+ get hasActiveBudget() {
144
+ const b = this.budget;
145
+ return b !== void 0 && (b.maxToolCalls !== void 0 || b.maxWallClockMs !== void 0 || b.maxTokens !== void 0);
146
+ }
147
+ recordStep(usage) {
148
+ if (!this.budget) return;
149
+ this.toolCallsUsed += usage.toolCalls ?? 0;
150
+ this.tokensUsed += (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
151
+ this.checkLimits();
152
+ }
153
+ checkLimits() {
154
+ const b = this.budget;
155
+ if (!b) return;
156
+ if (b.maxToolCalls !== void 0 && this.toolCallsUsed >= b.maxToolCalls) {
157
+ this.abort("budget-tool-calls");
158
+ return;
159
+ }
160
+ const elapsedMs = Date.now() - this.turnStartMs;
161
+ if (b.maxWallClockMs !== void 0 && elapsedMs >= b.maxWallClockMs) {
162
+ this.abort("budget-wall-clock");
163
+ return;
164
+ }
165
+ if (b.maxTokens !== void 0 && this.tokensUsed >= b.maxTokens) {
166
+ this.abort("budget-tokens");
167
+ }
168
+ }
169
+ abort(reason) {
170
+ if (this.abortReason !== null) return;
171
+ this.abortReason = reason;
172
+ console.info(
173
+ `[AI Agents] Budget exceeded \u2014 aborting turn. Reason: ${reason}. toolCalls=${this.toolCallsUsed}, tokens=${this.tokensUsed}, elapsedMs=${Date.now() - this.turnStartMs}.`
174
+ );
175
+ this.abortController.abort(reason);
176
+ }
177
+ wire(userOnStepFinish) {
178
+ if (!this.hasActiveBudget) return userOnStepFinish;
179
+ return async (event) => {
180
+ this.recordStep({
181
+ inputTokens: event.usage?.inputTokens,
182
+ outputTokens: event.usage?.outputTokens,
183
+ toolCalls: event.toolCalls?.length
184
+ });
185
+ if (userOnStepFinish) {
186
+ try {
187
+ await userOnStepFinish(event);
188
+ } catch (err) {
189
+ console.error("[AI Agents] User onStepFinish threw; ignoring:", err);
190
+ }
191
+ }
192
+ };
193
+ }
194
+ }
195
+ function buildLoopTraceCollector(agentId, sessionId, turnId, userOnStepFinish) {
196
+ const turnStartMs = Date.now();
197
+ const steps = [];
198
+ const onStepFinish = async (event) => {
199
+ const stepIndex = steps.length;
200
+ const toolCalls = (event.toolCalls ?? []).map((tc) => {
201
+ const raw = tc;
202
+ return {
203
+ toolName: raw.toolName ? desanitizeToolNameForDisplay(raw.toolName) : "unknown",
204
+ args: raw.args ?? {},
205
+ result: raw.result,
206
+ error: raw.experimental_toToolResultError ? {
207
+ code: String(raw.experimental_toToolResultError?.code ?? "unknown"),
208
+ message: String(raw.experimental_toToolResultError?.message ?? "")
209
+ } : void 0,
210
+ repairAttempted: raw.repairAttempted === true,
211
+ durationMs: typeof raw.startTime === "number" && typeof raw.endTime === "number" ? raw.endTime - raw.startTime : 0
212
+ };
213
+ });
214
+ const textDelta = event.text ?? "";
215
+ const finishReason = event.finishReason ?? "stop";
216
+ const modelId = event.response?.modelId ?? "unknown";
217
+ steps.push({
218
+ stepIndex,
219
+ modelId,
220
+ toolCalls,
221
+ textDelta,
222
+ usage: {
223
+ inputTokens: event.usage?.inputTokens ?? 0,
224
+ outputTokens: event.usage?.outputTokens ?? 0
225
+ },
226
+ finishReason
227
+ });
228
+ if (userOnStepFinish) {
229
+ try {
230
+ await userOnStepFinish(event);
231
+ } catch (err) {
232
+ console.error("[AI Agents] User onStepFinish in LoopTrace collector threw; ignoring:", err);
233
+ }
234
+ }
235
+ };
236
+ function finalize(abortReason) {
237
+ const totalDurationMs = Date.now() - turnStartMs;
238
+ const totalUsage = steps.reduce(
239
+ (acc, step) => ({
240
+ inputTokens: acc.inputTokens + step.usage.inputTokens,
241
+ outputTokens: acc.outputTokens + step.usage.outputTokens
242
+ }),
243
+ { inputTokens: 0, outputTokens: 0 }
244
+ );
245
+ let stopReason = "finish-reason";
246
+ if (abortReason === "budget-tool-calls") stopReason = "budget-tool-calls";
247
+ else if (abortReason === "budget-wall-clock") stopReason = "budget-wall-clock";
248
+ else if (abortReason === "budget-tokens") stopReason = "budget-tokens";
249
+ return {
250
+ agentId,
251
+ sessionId,
252
+ turnId,
253
+ steps,
254
+ stopReason,
255
+ totalDurationMs,
256
+ totalUsage
257
+ };
258
+ }
259
+ return { onStepFinish, finalize };
260
+ }
261
+ function translateStopConditions(loopConfig, mapToolName = (toolName) => toolName) {
262
+ const effectiveMaxSteps = loopConfig.maxSteps ?? 10;
263
+ const userConditions = [];
264
+ const rawStopWhen = loopConfig.stopWhen;
265
+ if (rawStopWhen) {
266
+ const items = Array.isArray(rawStopWhen) ? rawStopWhen : [rawStopWhen];
267
+ for (const item of items) {
268
+ if (item.kind === "stepCount") {
269
+ userConditions.push(stepCountIs(item.count));
270
+ } else if (item.kind === "hasToolCall") {
271
+ userConditions.push(hasToolCall(mapToolName(item.toolName)));
272
+ } else if (item.kind === "custom") {
273
+ userConditions.push(item.stop);
274
+ }
275
+ }
276
+ }
277
+ return [...userConditions, stepCountIs(effectiveMaxSteps)];
278
+ }
279
+ function normalizeAllowedToolNameForAgent(toolName, agent) {
280
+ if (agent.allowedTools.includes(toolName)) return toolName;
281
+ const dottedName = desanitizeToolNameForDisplay(toolName);
282
+ return agent.allowedTools.includes(dottedName) ? dottedName : null;
283
+ }
284
+ function mergeStepOverrides(wrapperOverride, userOverride, agent, wrappedToolRegistry) {
285
+ if (!userOverride) return wrapperOverride;
286
+ const merged = { ...wrapperOverride };
287
+ const userWithTools = userOverride;
288
+ if (userOverride.model !== void 0) {
289
+ merged.model = userOverride.model;
290
+ }
291
+ if (userOverride.toolChoice !== void 0) {
292
+ merged.toolChoice = userOverride.toolChoice;
293
+ }
294
+ if (userOverride.activeTools !== void 0) {
295
+ const filtered = userOverride.activeTools.flatMap((name) => {
296
+ const normalized = normalizeAllowedToolNameForAgent(name, agent);
297
+ const allowed = normalized !== null;
298
+ if (!allowed) {
299
+ console.warn(
300
+ `[AI Agents] loop:active_tools_filtered \u2014 tool "${name}" is not in agent "${agent.id}" allowedTools; dropping from activeTools.`
301
+ );
302
+ }
303
+ return normalized ? [normalized] : [];
304
+ });
305
+ merged.activeTools = filtered;
306
+ }
307
+ if (userWithTools.tools !== void 0) {
308
+ const userTools = userWithTools.tools;
309
+ const mergedTools = {};
310
+ for (const [toolKey, userHandler] of Object.entries(userTools)) {
311
+ const wrappedHandler = wrappedToolRegistry[toolKey];
312
+ if (!wrappedHandler) {
313
+ console.warn(
314
+ `[AI Agents] mergeStepOverrides \u2014 tool "${toolKey}" from user prepareStep is not in the wrapper tool registry; dropping.`
315
+ );
316
+ continue;
317
+ }
318
+ if (userHandler !== wrappedHandler) {
319
+ const toolDef = toolRegistry.getTool(
320
+ toolKey.replace(/__/g, ".")
321
+ );
322
+ if (toolDef?.isMutation === true) {
323
+ throw new AgentPolicyError(
324
+ "loop_violates_mutation_policy",
325
+ `User prepareStep returned a tools map with raw (unwrapped) mutation handler for "${toolKey}". This bypasses the mutation-approval gate and is rejected.`
326
+ );
327
+ }
328
+ }
329
+ mergedTools[toolKey] = wrappedHandler;
330
+ }
331
+ merged.tools = mergedTools;
332
+ }
333
+ return merged;
334
+ }
335
+ function mapPrepareStepResultForModel(result, wrappedTools) {
336
+ if (!result?.activeTools) return result;
337
+ const activeTools = result.activeTools.map((toolName) => sanitizeToolNameForModel(toolName)).filter((toolName) => wrappedTools[toolName] !== void 0);
338
+ return { ...result, activeTools };
339
+ }
340
+ function mapToolChoiceForModel(toolChoice) {
341
+ if (!toolChoice || typeof toolChoice !== "object" || toolChoice.type !== "tool") {
342
+ return toolChoice;
343
+ }
344
+ return {
345
+ ...toolChoice,
346
+ toolName: sanitizeToolNameForModel(toolChoice.toolName)
347
+ };
348
+ }
349
+ function mapActiveToolsForModel(activeTools, wrappedTools) {
350
+ if (!activeTools) return void 0;
351
+ return activeTools.map((toolName) => sanitizeToolNameForModel(toolName)).filter((toolName) => wrappedTools[toolName] !== void 0);
352
+ }
353
+ function buildWrapperPrepareStep(agent, effectiveLoop, wrappedTools) {
354
+ return async (state) => {
355
+ const wrapperOverride = {};
356
+ if (effectiveLoop.activeTools && effectiveLoop.activeTools.length > 0) {
357
+ wrapperOverride.activeTools = effectiveLoop.activeTools.flatMap((name) => {
358
+ const normalized = normalizeAllowedToolNameForAgent(name, agent);
359
+ const allowed = normalized !== null;
360
+ if (!allowed) {
361
+ console.warn(
362
+ `[AI Agents] loop:active_tools_filtered \u2014 tool "${name}" is not in agent "${agent.id}" allowedTools; dropping from activeTools.`
363
+ );
364
+ }
365
+ return normalized ? [normalized] : [];
366
+ });
367
+ }
368
+ if (effectiveLoop.prepareStep) {
369
+ let userOverride;
370
+ try {
371
+ userOverride = await effectiveLoop.prepareStep(state);
372
+ } catch (error) {
373
+ console.error(
374
+ `[AI Agents] User prepareStep threw for agent "${agent.id}"; ignoring user override:`,
375
+ error
376
+ );
377
+ return mapPrepareStepResultForModel(wrapperOverride, wrappedTools);
378
+ }
379
+ return mapPrepareStepResultForModel(
380
+ mergeStepOverrides(wrapperOverride, userOverride, agent, wrappedTools),
381
+ wrappedTools
382
+ );
383
+ }
384
+ return mapPrepareStepResultForModel(wrapperOverride, wrappedTools);
385
+ };
386
+ }
387
+ function assertLoopObjectModeCompatible(loopConfig) {
388
+ const unsupportedFields = [];
389
+ if (loopConfig.prepareStep !== void 0) unsupportedFields.push("prepareStep");
390
+ if (loopConfig.repairToolCall !== void 0) unsupportedFields.push("repairToolCall");
391
+ if (loopConfig.stopWhen !== void 0) unsupportedFields.push("stopWhen");
392
+ if (loopConfig.activeTools !== void 0) unsupportedFields.push("activeTools");
393
+ if (loopConfig.toolChoice !== void 0) unsupportedFields.push("toolChoice");
394
+ if (unsupportedFields.length > 0) {
395
+ throw new AgentPolicyError(
396
+ "loop_unsupported_in_object_mode",
397
+ `Object-mode agents do not support these loop primitives: ${unsupportedFields.join(", ")}. Use runAiAgentText for agents that require these loop controls.`
398
+ );
399
+ }
400
+ }
25
401
  function resolveAgentModel(agent, modelOverride, providerOverride, container, baseUrlOverride, tenantOverride, requestOverride, tenantAllowlist) {
26
402
  const effectiveContainer = container ?? createContainer();
27
- const allowRuntimeModelOverride = agent.allowRuntimeModelOverride !== false;
28
403
  const resolution = createModelFactory(effectiveContainer).resolveModel({
29
404
  moduleId: agent.moduleId,
30
405
  agentDefaultModel: agent.defaultModel,
@@ -33,7 +408,7 @@ function resolveAgentModel(agent, modelOverride, providerOverride, container, ba
33
408
  callerOverride: modelOverride,
34
409
  providerOverride,
35
410
  baseUrlOverride,
36
- allowRuntimeModelOverride,
411
+ allowRuntimeOverride: resolveAllowRuntimeOverride(agent),
37
412
  tenantOverride: tenantOverride ?? void 0,
38
413
  requestOverride: requestOverride ?? void 0,
39
414
  tenantAllowlist: tenantAllowlist ?? null
@@ -353,6 +728,7 @@ async function runAiAgentText(input) {
353
728
  input.authContext.organizationId
354
729
  )
355
730
  ]);
731
+ const effectiveSessionId = (input.sessionId ?? input.conversationId) || randomUUID();
356
732
  const { agent, tools } = await resolveAiAgentTools({
357
733
  agentId: input.agentId,
358
734
  authContext: input.authContext,
@@ -360,7 +736,7 @@ async function runAiAgentText(input) {
360
736
  attachmentIds: input.attachmentIds,
361
737
  mutationPolicyOverride,
362
738
  container: input.container,
363
- conversationId: input.conversationId ?? null
739
+ conversationId: effectiveSessionId
364
740
  });
365
741
  const resolvedAttachments = await resolveAttachmentPartsForAgent({
366
742
  agent,
@@ -380,7 +756,7 @@ async function runAiAgentText(input) {
380
756
  agent,
381
757
  mutationPolicyOverride
382
758
  );
383
- const { model } = resolveAgentModel(
759
+ const resolvedModel = resolveAgentModel(
384
760
  agent,
385
761
  input.modelOverride,
386
762
  input.providerOverride,
@@ -390,26 +766,162 @@ async function runAiAgentText(input) {
390
766
  input.requestOverride,
391
767
  tenantAllowlistSnapshot
392
768
  );
769
+ const { model } = resolvedModel;
393
770
  const normalizedMessages = ensureUiMessageShape(input.messages);
394
771
  const hydratedMessages = attachAttachmentsToMessages(normalizedMessages, resolvedAttachments);
395
772
  const modelMessages = await convertToModelMessages(hydratedMessages);
396
- const effectiveMaxSteps = typeof agent.maxSteps === "number" && agent.maxSteps > 0 ? agent.maxSteps : 10;
397
- const stopWhen = stepCountIs(effectiveMaxSteps);
398
- const streamArgs = {
773
+ const effectiveLoop = resolveEffectiveLoopConfig(agent, input.loop, WRAPPER_DEFAULT_LOOP_CHAT);
774
+ const stopConditions = translateStopConditions(effectiveLoop, sanitizeToolNameForModel);
775
+ const wrapperPrepareStep = buildWrapperPrepareStep(agent, effectiveLoop, tools);
776
+ const sdkActiveTools = mapActiveToolsForModel(effectiveLoop.activeTools, tools);
777
+ const sdkToolChoice = mapToolChoiceForModel(effectiveLoop.toolChoice);
778
+ const turnId = randomUUID();
779
+ const loopTraceCollector = buildLoopTraceCollector(agent.id, effectiveSessionId, turnId, effectiveLoop.onStepFinish);
780
+ const abortController = new AbortController();
781
+ const budgetEnforcer = new BudgetEnforcer(effectiveLoop.budget, abortController);
782
+ const tracedOnStepFinish = budgetEnforcer.wire(loopTraceCollector.onStepFinish);
783
+ let currentStepIndex = 0;
784
+ const wiredOnStepFinish = async (event) => {
785
+ const capturedStepIndex = currentStepIndex;
786
+ currentStepIndex += 1;
787
+ if (tracedOnStepFinish) {
788
+ await tracedOnStepFinish(event);
789
+ }
790
+ if (input.container) {
791
+ const rawEvent = event;
792
+ void recordTokenUsage(
793
+ {
794
+ authContext: input.authContext,
795
+ agentId: agent.id,
796
+ moduleId: agent.moduleId,
797
+ sessionId: effectiveSessionId,
798
+ turnId,
799
+ stepIndex: capturedStepIndex,
800
+ providerId: resolvedModel.providerId,
801
+ modelId: resolvedModel.modelId,
802
+ usage: {
803
+ inputTokens: rawEvent.usage?.inputTokens,
804
+ outputTokens: rawEvent.usage?.outputTokens,
805
+ cachedInputTokens: rawEvent.usage?.cachedInputTokens,
806
+ reasoningTokens: rawEvent.usage?.reasoningTokens
807
+ },
808
+ finishReason: rawEvent.finishReason,
809
+ loopAbortReason: budgetEnforcer.abortReason ?? void 0
810
+ },
811
+ input.container
812
+ );
813
+ }
814
+ };
815
+ let wallClockTimer;
816
+ if (effectiveLoop.budget?.maxWallClockMs) {
817
+ wallClockTimer = setTimeout(() => {
818
+ budgetEnforcer.recordStep({ toolCalls: 0 });
819
+ }, effectiveLoop.budget.maxWallClockMs);
820
+ }
821
+ let builtToolLoopAgent;
822
+ if (agent.executionEngine === "tool-loop-agent") {
823
+ const agentSettings = {
824
+ model,
825
+ tools,
826
+ stopWhen: stopConditions,
827
+ prepareStep: wrapperPrepareStep,
828
+ onStepFinish: wiredOnStepFinish,
829
+ ...effectiveLoop.repairToolCall !== void 0 ? { experimental_repairToolCall: effectiveLoop.repairToolCall } : {},
830
+ ...sdkActiveTools !== void 0 ? { activeTools: sdkActiveTools } : {},
831
+ ...sdkToolChoice !== void 0 ? { toolChoice: sdkToolChoice } : {}
832
+ };
833
+ builtToolLoopAgent = new ToolLoopAgent(agentSettings);
834
+ }
835
+ const preparedOptions = {
399
836
  model,
837
+ tools,
400
838
  system: systemPrompt,
401
839
  messages: modelMessages,
402
- tools,
403
- stopWhen
840
+ maxSteps: effectiveLoop.maxSteps ?? 10,
841
+ stopWhen: stopConditions,
842
+ prepareStep: wrapperPrepareStep,
843
+ onStepFinish: wiredOnStepFinish,
844
+ onStepStart: effectiveLoop.onStepStart,
845
+ onToolCallStart: effectiveLoop.onToolCallStart,
846
+ onToolCallFinish: effectiveLoop.onToolCallFinish,
847
+ experimental_repairToolCall: effectiveLoop.repairToolCall,
848
+ activeTools: sdkActiveTools,
849
+ toolChoice: sdkToolChoice,
850
+ abortSignal: abortController.signal,
851
+ finalizeLoopTrace: () => loopTraceCollector.finalize(budgetEnforcer.abortReason),
852
+ ...builtToolLoopAgent !== void 0 ? { toolLoopAgent: builtToolLoopAgent } : {}
404
853
  };
405
- const result = streamText(streamArgs);
406
- return result.toUIMessageStreamResponse({
854
+ if (input.generateText) {
855
+ try {
856
+ const callbackResult = await input.generateText(preparedOptions);
857
+ const baseResponse2 = callbackResult.toUIMessageStreamResponse({
858
+ sendReasoning: true,
859
+ headers: {
860
+ "Cache-Control": "no-cache, no-transform",
861
+ Connection: "keep-alive"
862
+ }
863
+ });
864
+ if (input.emitLoopTrace) {
865
+ return appendLoopFinishToStream(baseResponse2, preparedOptions.finalizeLoopTrace);
866
+ }
867
+ return baseResponse2;
868
+ } finally {
869
+ if (wallClockTimer !== void 0) clearTimeout(wallClockTimer);
870
+ }
871
+ }
872
+ if (builtToolLoopAgent !== void 0) {
873
+ const agentStreamResult = await builtToolLoopAgent.stream({
874
+ messages: modelMessages,
875
+ abortSignal: abortController.signal,
876
+ onStepFinish: wiredOnStepFinish
877
+ });
878
+ if (wallClockTimer !== void 0) {
879
+ const clearTimer = () => clearTimeout(wallClockTimer);
880
+ Promise.resolve(agentStreamResult.consumeStream()).then(clearTimer, clearTimer);
881
+ }
882
+ const baseResponse2 = agentStreamResult.toUIMessageStreamResponse({
883
+ sendReasoning: true,
884
+ headers: {
885
+ "Cache-Control": "no-cache, no-transform",
886
+ Connection: "keep-alive"
887
+ }
888
+ });
889
+ if (input.emitLoopTrace) {
890
+ return appendLoopFinishToStream(baseResponse2, preparedOptions.finalizeLoopTrace);
891
+ }
892
+ return baseResponse2;
893
+ }
894
+ const result = streamText({
895
+ model,
896
+ system: systemPrompt,
897
+ messages: modelMessages,
898
+ tools,
899
+ stopWhen: stopConditions,
900
+ prepareStep: wrapperPrepareStep,
901
+ onStepFinish: wiredOnStepFinish,
902
+ experimental_onStepStart: effectiveLoop.onStepStart,
903
+ experimental_onToolCallStart: effectiveLoop.onToolCallStart,
904
+ experimental_onToolCallFinish: effectiveLoop.onToolCallFinish,
905
+ experimental_repairToolCall: effectiveLoop.repairToolCall,
906
+ ...sdkActiveTools !== void 0 ? { activeTools: sdkActiveTools } : {},
907
+ ...sdkToolChoice !== void 0 ? { toolChoice: sdkToolChoice } : {},
908
+ abortSignal: abortController.signal
909
+ });
910
+ if (wallClockTimer !== void 0) {
911
+ const clearTimer = () => clearTimeout(wallClockTimer);
912
+ Promise.resolve(result.consumeStream()).then(clearTimer, clearTimer);
913
+ }
914
+ const baseResponse = result.toUIMessageStreamResponse({
407
915
  sendReasoning: true,
408
916
  headers: {
409
917
  "Cache-Control": "no-cache, no-transform",
410
918
  Connection: "keep-alive"
411
919
  }
412
920
  });
921
+ if (input.emitLoopTrace) {
922
+ return appendLoopFinishToStream(baseResponse, preparedOptions.finalizeLoopTrace);
923
+ }
924
+ return baseResponse;
413
925
  }
414
926
  function normalizeObjectMessages(input) {
415
927
  if (typeof input === "string") {
@@ -508,14 +1020,60 @@ async function runAiAgentObject(input) {
508
1020
  resolvedAttachments
509
1021
  );
510
1022
  const modelMessages = await convertToModelMessages(hydratedMessages);
511
- const stopWhen = typeof agent.maxSteps === "number" && agent.maxSteps > 0 ? stepCountIs(agent.maxSteps) : void 0;
1023
+ void tools;
1024
+ const effectiveObjectSessionId = input.sessionId ?? randomUUID();
1025
+ const objectTurnId = randomUUID();
1026
+ void effectiveObjectSessionId;
1027
+ void objectTurnId;
1028
+ if (input.loop) {
1029
+ assertLoopObjectModeCompatible(input.loop);
1030
+ }
1031
+ const effectiveLoop = resolveEffectiveLoopConfig(agent, input.loop, WRAPPER_DEFAULT_LOOP_OBJECT);
1032
+ const abortController = new AbortController();
1033
+ const preparedObjectOptions = {
1034
+ model,
1035
+ system: systemPrompt,
1036
+ messages: modelMessages,
1037
+ schemaName: resolvedOutput.schemaName,
1038
+ schema: resolvedOutput.schema,
1039
+ maxSteps: effectiveLoop.maxSteps,
1040
+ onStepFinish: effectiveLoop.onStepFinish,
1041
+ onStepStart: effectiveLoop.onStepStart,
1042
+ abortSignal: abortController.signal
1043
+ };
1044
+ if (input.generateObject) {
1045
+ const callbackResult = await input.generateObject(preparedObjectOptions);
1046
+ const typedResult = callbackResult;
1047
+ if ("partialObjectStream" in typedResult) {
1048
+ const streamResult = typedResult;
1049
+ return {
1050
+ mode: "stream",
1051
+ object: streamResult.object,
1052
+ partialObjectStream: streamResult.partialObjectStream,
1053
+ textStream: streamResult.textStream,
1054
+ finishReason: streamResult.finishReason,
1055
+ usage: streamResult.usage
1056
+ };
1057
+ }
1058
+ const genResult = typedResult;
1059
+ return {
1060
+ mode: "generate",
1061
+ object: genResult.object,
1062
+ finishReason: genResult.finishReason,
1063
+ usage: genResult.usage
1064
+ };
1065
+ }
512
1066
  if (resolvedOutput.mode === "stream") {
513
1067
  const streamArgs = {
514
1068
  model,
515
1069
  system: systemPrompt,
516
1070
  messages: modelMessages,
517
1071
  schema: resolvedOutput.schema,
518
- schemaName: resolvedOutput.schemaName
1072
+ schemaName: resolvedOutput.schemaName,
1073
+ ...effectiveLoop.maxSteps !== void 0 ? { maxSteps: effectiveLoop.maxSteps } : {},
1074
+ onStepFinish: effectiveLoop.onStepFinish,
1075
+ onStepStart: effectiveLoop.onStepStart,
1076
+ abortSignal: abortController.signal
519
1077
  };
520
1078
  const result2 = streamObject(streamArgs);
521
1079
  return {
@@ -532,13 +1090,12 @@ async function runAiAgentObject(input) {
532
1090
  system: systemPrompt,
533
1091
  messages: modelMessages,
534
1092
  schema: resolvedOutput.schema,
535
- schemaName: resolvedOutput.schemaName
1093
+ schemaName: resolvedOutput.schemaName,
1094
+ ...effectiveLoop.maxSteps !== void 0 ? { maxSteps: effectiveLoop.maxSteps } : {},
1095
+ onStepFinish: effectiveLoop.onStepFinish,
1096
+ onStepStart: effectiveLoop.onStepStart,
1097
+ abortSignal: abortController.signal
536
1098
  };
537
- if (stopWhen) {
538
- ;
539
- generateArgs.stopWhen = stopWhen;
540
- }
541
- void tools;
542
1099
  const result = await generateObject(generateArgs);
543
1100
  return {
544
1101
  mode: "generate",
@@ -549,8 +1106,16 @@ async function runAiAgentObject(input) {
549
1106
  }
550
1107
  export {
551
1108
  AgentPolicyError,
1109
+ BudgetEnforcer,
1110
+ assertLoopObjectModeCompatible,
1111
+ buildLoopTraceCollector,
1112
+ buildWrapperPrepareStep,
552
1113
  composeSystemPrompt,
1114
+ mergeStepOverrides,
1115
+ resolveEffectiveLoopConfig,
1116
+ resolveLoopBudgetPreset,
553
1117
  runAiAgentObject,
554
- runAiAgentText
1118
+ runAiAgentText,
1119
+ translateStopConditions
555
1120
  };
556
1121
  //# sourceMappingURL=agent-runtime.js.map