@open-mercato/ai-assistant 0.6.2-develop.3406.1.2b403f40da → 0.6.2-develop.3446.1.bd060c6017
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 +8 -1
- package/build.mjs +1 -0
- package/dist/frontend/components/AiChatButton.js +1 -1
- package/dist/frontend/components/AiChatButton.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +16 -5
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +2 -2
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js +58 -1
- package/dist/modules/ai_assistant/ai-tools/meta-pack.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/route.js +2 -1
- package/dist/modules/ai_assistant/api/ai/agents/route.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +7 -1
- package/dist/modules/ai_assistant/i18n/en.json +7 -1
- package/dist/modules/ai_assistant/i18n/es.json +7 -1
- package/dist/modules/ai_assistant/i18n/pl.json +7 -1
- package/dist/modules/ai_assistant/lib/agent-registry.js +26 -6
- package/dist/modules/ai_assistant/lib/agent-registry.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +21 -8
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/pending-action-types.js.map +2 -2
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +16 -6
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
- package/dist/modules/ai_assistant/lib/task-plan-labels.js +95 -0
- package/dist/modules/ai_assistant/lib/task-plan-labels.js.map +7 -0
- package/dist/modules/ai_assistant/lib/task-plan-stream.js +349 -0
- package/dist/modules/ai_assistant/lib/task-plan-stream.js.map +7 -0
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js +3 -0
- package/dist/modules/ai_assistant/lib/tool-test-fixtures.js.map +2 -2
- package/package.json +6 -6
- package/src/frontend/components/AiChatButton.tsx +1 -1
- package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +20 -8
- package/src/modules/ai_assistant/ai-tools/__tests__/meta-pack.test.ts +60 -4
- package/src/modules/ai_assistant/ai-tools/meta-pack.ts +79 -2
- package/src/modules/ai_assistant/api/ai/agents/route.ts +2 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +1 -0
- package/src/modules/ai_assistant/i18n/de.json +7 -1
- package/src/modules/ai_assistant/i18n/en.json +7 -1
- package/src/modules/ai_assistant/i18n/es.json +7 -1
- package/src/modules/ai_assistant/i18n/pl.json +7 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-registry.test.ts +60 -0
- package/src/modules/ai_assistant/lib/__tests__/ai-agent-definition.test.ts +4 -0
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +43 -0
- package/src/modules/ai_assistant/lib/__tests__/task-plan-stream.test.ts +375 -0
- package/src/modules/ai_assistant/lib/agent-registry.ts +36 -5
- package/src/modules/ai_assistant/lib/agent-runtime.ts +26 -8
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +14 -0
- package/src/modules/ai_assistant/lib/pending-action-types.ts +4 -1
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +17 -5
- package/src/modules/ai_assistant/lib/task-plan-labels.ts +112 -0
- package/src/modules/ai_assistant/lib/task-plan-stream.ts +463 -0
- package/src/modules/ai_assistant/lib/tool-test-fixtures.ts +3 -0
- package/src/modules/ai_assistant/lib/types.ts +16 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TASK_PLAN_LABEL_MAX_CHARS,
|
|
3
|
+
isTaskPlanToolName,
|
|
4
|
+
normalizeTaskPlanToolName,
|
|
5
|
+
sanitizeAgentTaskPlanInput
|
|
6
|
+
} from "./task-plan-labels.js";
|
|
7
|
+
const SSE_ENCODER = new TextEncoder();
|
|
8
|
+
const SSE_DECODER = new TextDecoder();
|
|
9
|
+
const TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
10
|
+
"done",
|
|
11
|
+
"failed",
|
|
12
|
+
"skipped"
|
|
13
|
+
]);
|
|
14
|
+
const TASK_LABEL_MAX_CHARS = TASK_PLAN_LABEL_MAX_CHARS;
|
|
15
|
+
function deriveTaskLabel(toolName) {
|
|
16
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
17
|
+
return "Tool call";
|
|
18
|
+
}
|
|
19
|
+
const display = toolName.replace(/__/g, ".");
|
|
20
|
+
const segments = display.split(".");
|
|
21
|
+
const lastSegment = segments[segments.length - 1] ?? display;
|
|
22
|
+
const humanized = lastSegment.replace(/_/g, " ").trim();
|
|
23
|
+
if (humanized.length === 0) return display.slice(0, TASK_LABEL_MAX_CHARS);
|
|
24
|
+
const titled = humanized.charAt(0).toUpperCase() + humanized.slice(1);
|
|
25
|
+
if (segments.length <= 1) {
|
|
26
|
+
return titled.slice(0, TASK_LABEL_MAX_CHARS);
|
|
27
|
+
}
|
|
28
|
+
const moduleSegment = segments[0];
|
|
29
|
+
const moduleLabel = moduleSegment.charAt(0).toUpperCase() + moduleSegment.slice(1).replace(/_/g, " ");
|
|
30
|
+
const combined = `${moduleLabel} \xB7 ${titled}`;
|
|
31
|
+
return combined.slice(0, TASK_LABEL_MAX_CHARS);
|
|
32
|
+
}
|
|
33
|
+
class TaskPlanAccumulator {
|
|
34
|
+
constructor(planId) {
|
|
35
|
+
this.planId = planId;
|
|
36
|
+
this.tasks = /* @__PURE__ */ new Map();
|
|
37
|
+
this.toolCallToTaskId = /* @__PURE__ */ new Map();
|
|
38
|
+
this.taskToolNames = /* @__PURE__ */ new Map();
|
|
39
|
+
this.internalToolCallIds = /* @__PURE__ */ new Set();
|
|
40
|
+
this.snapshotEmitted = false;
|
|
41
|
+
this.hasAgentAuthoredPlan = false;
|
|
42
|
+
}
|
|
43
|
+
upsert(id, patch) {
|
|
44
|
+
const existing = this.tasks.get(id);
|
|
45
|
+
if (!existing) {
|
|
46
|
+
const created = {
|
|
47
|
+
id,
|
|
48
|
+
label: patch.label ?? "Tool call",
|
|
49
|
+
state: patch.state ?? "running",
|
|
50
|
+
source: patch.source ?? "runtime",
|
|
51
|
+
detail: patch.detail,
|
|
52
|
+
toolCallId: patch.toolCallId
|
|
53
|
+
};
|
|
54
|
+
this.tasks.set(id, { snapshot: created, emitted: false });
|
|
55
|
+
return created;
|
|
56
|
+
}
|
|
57
|
+
const current = existing.snapshot;
|
|
58
|
+
const nextState = TERMINAL_STATES.has(current.state) ? current.state : patch.state ?? current.state;
|
|
59
|
+
const merged = {
|
|
60
|
+
id: current.id,
|
|
61
|
+
label: patch.label ?? current.label,
|
|
62
|
+
state: nextState,
|
|
63
|
+
source: patch.source ?? current.source,
|
|
64
|
+
detail: patch.detail ?? current.detail,
|
|
65
|
+
toolCallId: patch.toolCallId ?? current.toolCallId
|
|
66
|
+
};
|
|
67
|
+
this.tasks.set(id, { snapshot: merged, emitted: existing.emitted });
|
|
68
|
+
return merged;
|
|
69
|
+
}
|
|
70
|
+
makeUniqueTaskId(baseId) {
|
|
71
|
+
let candidate = baseId;
|
|
72
|
+
let suffix = 2;
|
|
73
|
+
while (this.tasks.has(candidate)) {
|
|
74
|
+
candidate = `${baseId}-${suffix}`;
|
|
75
|
+
suffix += 1;
|
|
76
|
+
}
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
79
|
+
emitFullSnapshot() {
|
|
80
|
+
if (this.tasks.size === 0) return [];
|
|
81
|
+
this.snapshotEmitted = true;
|
|
82
|
+
const initialTasks = Array.from(this.tasks.values()).map((e) => e.snapshot);
|
|
83
|
+
for (const e of this.tasks.values()) e.emitted = true;
|
|
84
|
+
return [
|
|
85
|
+
formatSseEvent({
|
|
86
|
+
type: "data-agent-task-plan",
|
|
87
|
+
planId: this.planId,
|
|
88
|
+
tasks: initialTasks
|
|
89
|
+
})
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
handleAgentAuthoredPlan(input) {
|
|
93
|
+
const plan = sanitizeAgentTaskPlanInput(input);
|
|
94
|
+
if (plan.tasks.length === 0) return [];
|
|
95
|
+
this.tasks.clear();
|
|
96
|
+
this.toolCallToTaskId.clear();
|
|
97
|
+
this.taskToolNames.clear();
|
|
98
|
+
this.snapshotEmitted = false;
|
|
99
|
+
this.hasAgentAuthoredPlan = true;
|
|
100
|
+
plan.tasks.forEach((task, index) => {
|
|
101
|
+
const id = this.makeUniqueTaskId(task.id ?? `agent-plan-${index + 1}`);
|
|
102
|
+
const snapshot = {
|
|
103
|
+
id,
|
|
104
|
+
label: task.label,
|
|
105
|
+
state: "pending",
|
|
106
|
+
source: "agent",
|
|
107
|
+
detail: task.detail
|
|
108
|
+
};
|
|
109
|
+
this.tasks.set(id, { snapshot, emitted: false });
|
|
110
|
+
if (task.toolName) {
|
|
111
|
+
this.taskToolNames.set(id, task.toolName);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return this.emitFullSnapshot();
|
|
115
|
+
}
|
|
116
|
+
resolveTaskIdForToolCall(toolCallId, toolName) {
|
|
117
|
+
const existing = this.toolCallToTaskId.get(toolCallId);
|
|
118
|
+
if (existing) return existing;
|
|
119
|
+
const plannedTaskId = this.findPlannedTaskId(toolName);
|
|
120
|
+
if (plannedTaskId) {
|
|
121
|
+
this.toolCallToTaskId.set(toolCallId, plannedTaskId);
|
|
122
|
+
return plannedTaskId;
|
|
123
|
+
}
|
|
124
|
+
this.toolCallToTaskId.set(toolCallId, toolCallId);
|
|
125
|
+
return toolCallId;
|
|
126
|
+
}
|
|
127
|
+
findPlannedTaskId(toolName) {
|
|
128
|
+
if (!this.hasAgentAuthoredPlan) return null;
|
|
129
|
+
const entries = Array.from(this.tasks.entries());
|
|
130
|
+
const isAvailable = (entry) => !TERMINAL_STATES.has(entry.snapshot.state);
|
|
131
|
+
if (toolName) {
|
|
132
|
+
const exactPending = entries.find(([id, entry]) => {
|
|
133
|
+
return entry.snapshot.state === "pending" && this.taskToolNames.get(id) === toolName;
|
|
134
|
+
});
|
|
135
|
+
if (exactPending) return exactPending[0];
|
|
136
|
+
const exactAvailable = entries.find(([id, entry]) => {
|
|
137
|
+
return isAvailable(entry) && this.taskToolNames.get(id) === toolName;
|
|
138
|
+
});
|
|
139
|
+
if (exactAvailable) return exactAvailable[0];
|
|
140
|
+
}
|
|
141
|
+
const genericPending = entries.find(([id, entry]) => {
|
|
142
|
+
return entry.snapshot.state === "pending" && !this.taskToolNames.has(id);
|
|
143
|
+
});
|
|
144
|
+
if (genericPending) return genericPending[0];
|
|
145
|
+
const genericAvailable = entries.find(([id, entry]) => {
|
|
146
|
+
return isAvailable(entry) && !this.taskToolNames.has(id);
|
|
147
|
+
});
|
|
148
|
+
return genericAvailable?.[0] ?? null;
|
|
149
|
+
}
|
|
150
|
+
existingSnapshot(taskId) {
|
|
151
|
+
return this.tasks.get(taskId)?.snapshot;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Apply a tool lifecycle chunk. Returns the SSE event lines (already
|
|
155
|
+
* `data: ...\n\n`-formatted) that should be injected ahead of forwarding
|
|
156
|
+
* the original chunk to the client.
|
|
157
|
+
*/
|
|
158
|
+
handleToolChunk(chunk) {
|
|
159
|
+
if (!chunk || typeof chunk.type !== "string") return [];
|
|
160
|
+
const toolCallId = typeof chunk.toolCallId === "string" ? chunk.toolCallId : null;
|
|
161
|
+
const toolName = normalizeTaskPlanToolName(chunk.toolName);
|
|
162
|
+
if (isTaskPlanToolName(toolName)) {
|
|
163
|
+
if (toolCallId) this.internalToolCallIds.add(toolCallId);
|
|
164
|
+
if (chunk.type === "tool-input-available") {
|
|
165
|
+
return this.handleAgentAuthoredPlan(chunk.input);
|
|
166
|
+
}
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
if (!toolCallId) return [];
|
|
170
|
+
if (this.internalToolCallIds.has(toolCallId)) return [];
|
|
171
|
+
const taskId = this.resolveTaskIdForToolCall(toolCallId, toolName);
|
|
172
|
+
const existing = this.existingSnapshot(taskId);
|
|
173
|
+
const source = existing?.source ?? "runtime";
|
|
174
|
+
const runtimeLabel = deriveTaskLabel(toolName);
|
|
175
|
+
const runtimeDetail = toolName;
|
|
176
|
+
let nextSnapshot = null;
|
|
177
|
+
switch (chunk.type) {
|
|
178
|
+
case "tool-input-start":
|
|
179
|
+
nextSnapshot = this.upsert(taskId, {
|
|
180
|
+
label: source === "agent" ? existing?.label : runtimeLabel,
|
|
181
|
+
state: "running",
|
|
182
|
+
source,
|
|
183
|
+
toolCallId,
|
|
184
|
+
detail: source === "agent" ? existing?.detail : runtimeDetail
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
case "tool-input-available":
|
|
188
|
+
nextSnapshot = this.upsert(taskId, {
|
|
189
|
+
label: source === "agent" ? existing?.label : runtimeLabel,
|
|
190
|
+
state: "running",
|
|
191
|
+
source,
|
|
192
|
+
toolCallId,
|
|
193
|
+
detail: source === "agent" ? existing?.detail : runtimeDetail
|
|
194
|
+
});
|
|
195
|
+
break;
|
|
196
|
+
case "tool-output-available":
|
|
197
|
+
nextSnapshot = this.upsert(taskId, {
|
|
198
|
+
state: "done",
|
|
199
|
+
source,
|
|
200
|
+
toolCallId
|
|
201
|
+
});
|
|
202
|
+
break;
|
|
203
|
+
case "tool-output-error":
|
|
204
|
+
case "tool-input-error":
|
|
205
|
+
nextSnapshot = this.upsert(taskId, {
|
|
206
|
+
state: "failed",
|
|
207
|
+
source,
|
|
208
|
+
toolCallId
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
if (!nextSnapshot) return [];
|
|
215
|
+
return this.emitForSnapshot(taskId, nextSnapshot);
|
|
216
|
+
}
|
|
217
|
+
emitForSnapshot(id, snapshot) {
|
|
218
|
+
const entry = this.tasks.get(id);
|
|
219
|
+
if (!entry) return [];
|
|
220
|
+
const lines = [];
|
|
221
|
+
if (!this.snapshotEmitted) {
|
|
222
|
+
return this.emitFullSnapshot();
|
|
223
|
+
}
|
|
224
|
+
if (!entry.emitted) {
|
|
225
|
+
lines.push(
|
|
226
|
+
formatSseEvent({
|
|
227
|
+
type: "data-agent-task-update",
|
|
228
|
+
planId: this.planId,
|
|
229
|
+
task: snapshot
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
entry.emitted = true;
|
|
233
|
+
return lines;
|
|
234
|
+
}
|
|
235
|
+
lines.push(
|
|
236
|
+
formatSseEvent({
|
|
237
|
+
type: "data-agent-task-update",
|
|
238
|
+
planId: this.planId,
|
|
239
|
+
task: snapshot
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function formatSseEvent(payload) {
|
|
246
|
+
return `data: ${JSON.stringify(payload)}
|
|
247
|
+
|
|
248
|
+
`;
|
|
249
|
+
}
|
|
250
|
+
function injectTaskPlanIntoStream(baseResponse, planId) {
|
|
251
|
+
const { readable, writable } = new TransformStream();
|
|
252
|
+
const writer = writable.getWriter();
|
|
253
|
+
const accumulator = new TaskPlanAccumulator(planId);
|
|
254
|
+
async function pump() {
|
|
255
|
+
if (!baseResponse.body) {
|
|
256
|
+
await writer.close();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const reader = baseResponse.body.getReader();
|
|
260
|
+
let textBuffer = "";
|
|
261
|
+
try {
|
|
262
|
+
for (; ; ) {
|
|
263
|
+
const { value, done } = await reader.read();
|
|
264
|
+
if (done) break;
|
|
265
|
+
if (!value) continue;
|
|
266
|
+
textBuffer += SSE_DECODER.decode(value, { stream: true });
|
|
267
|
+
textBuffer = await flushBuffer(textBuffer, accumulator, writer);
|
|
268
|
+
}
|
|
269
|
+
const tail = SSE_DECODER.decode();
|
|
270
|
+
if (tail) {
|
|
271
|
+
textBuffer += tail;
|
|
272
|
+
}
|
|
273
|
+
if (textBuffer.length > 0) {
|
|
274
|
+
await writer.write(SSE_ENCODER.encode(textBuffer));
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
} finally {
|
|
278
|
+
reader.releaseLock();
|
|
279
|
+
await writer.close().catch(() => void 0);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
void pump();
|
|
283
|
+
return new Response(readable, {
|
|
284
|
+
status: baseResponse.status,
|
|
285
|
+
headers: baseResponse.headers
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async function flushBuffer(buffer, accumulator, writer) {
|
|
289
|
+
let rest = buffer;
|
|
290
|
+
for (; ; ) {
|
|
291
|
+
const boundary = rest.indexOf("\n\n");
|
|
292
|
+
if (boundary === -1) break;
|
|
293
|
+
const eventBlock = rest.slice(0, boundary + 2);
|
|
294
|
+
rest = rest.slice(boundary + 2);
|
|
295
|
+
const injected = inspectEventBlock(eventBlock, accumulator);
|
|
296
|
+
for (const line of injected.before) {
|
|
297
|
+
await writer.write(SSE_ENCODER.encode(line));
|
|
298
|
+
}
|
|
299
|
+
await writer.write(SSE_ENCODER.encode(eventBlock));
|
|
300
|
+
for (const line of injected.after) {
|
|
301
|
+
await writer.write(SSE_ENCODER.encode(line));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return rest;
|
|
305
|
+
}
|
|
306
|
+
function inspectEventBlock(eventBlock, accumulator) {
|
|
307
|
+
const dataPayload = extractDataPayload(eventBlock);
|
|
308
|
+
if (!dataPayload || dataPayload === "[DONE]") {
|
|
309
|
+
return { before: [], after: [] };
|
|
310
|
+
}
|
|
311
|
+
let parsed = null;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(dataPayload);
|
|
314
|
+
} catch {
|
|
315
|
+
return { before: [], after: [] };
|
|
316
|
+
}
|
|
317
|
+
if (!parsed || typeof parsed.type !== "string") {
|
|
318
|
+
return { before: [], after: [] };
|
|
319
|
+
}
|
|
320
|
+
const type = parsed.type;
|
|
321
|
+
const injected = accumulator.handleToolChunk(parsed);
|
|
322
|
+
if (injected.length === 0) {
|
|
323
|
+
return { before: [], after: [] };
|
|
324
|
+
}
|
|
325
|
+
if (type === "tool-input-start" || type === "tool-input-available") {
|
|
326
|
+
return { before: injected, after: [] };
|
|
327
|
+
}
|
|
328
|
+
return { before: [], after: injected };
|
|
329
|
+
}
|
|
330
|
+
function extractDataPayload(eventBlock) {
|
|
331
|
+
const lines = eventBlock.split("\n");
|
|
332
|
+
const dataLines = [];
|
|
333
|
+
for (const line of lines) {
|
|
334
|
+
if (line.startsWith("data: ")) {
|
|
335
|
+
dataLines.push(line.slice(6));
|
|
336
|
+
} else if (line.startsWith("data:")) {
|
|
337
|
+
dataLines.push(line.slice(5));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (dataLines.length === 0) return null;
|
|
341
|
+
return dataLines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
export {
|
|
344
|
+
TaskPlanAccumulator,
|
|
345
|
+
deriveTaskLabel,
|
|
346
|
+
formatSseEvent,
|
|
347
|
+
injectTaskPlanIntoStream
|
|
348
|
+
};
|
|
349
|
+
//# sourceMappingURL=task-plan-stream.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/ai_assistant/lib/task-plan-stream.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Visible AI chat agent task plan \u2014 server-side SSE injector.\n *\n * Spec: `.ai/specs/2026-05-13-ai-chat-visible-task-plan.md`.\n *\n * Wraps a streaming `Response` produced by `streamText().toUIMessageStreamResponse()`\n * (or the equivalent `ToolLoopAgent.stream(...).toUIMessageStreamResponse()`)\n * and interleaves additive `data-agent-task-plan` / `data-agent-task-update`\n * SSE chunks alongside the AI SDK tool lifecycle chunks. The original chunks\n * are passed through unchanged so existing clients that ignore unknown chunk\n * types continue to work.\n *\n * The injector derives task labels and states from the SDK tool lifecycle:\n * - `tool-input-start` \u2192 create/update task with state `running`\n * - `tool-input-available` \u2192 keep task `running` (label may be refined)\n * - `tool-output-available` \u2192 mark task `done`\n * - `tool-output-error` \u2192 mark task `failed`\n * - `tool-input-error` \u2192 mark task `failed`\n *\n * Agent-authored labels flow through the reserved non-mutation\n * `meta.update_task_plan` tool. Its input is sanitized before the plan reaches\n * the client; the raw meta-tool call is still passed through for older clients\n * but the visible plan uses only the safe labels.\n */\n\nimport {\n TASK_PLAN_LABEL_MAX_CHARS,\n isTaskPlanToolName,\n normalizeTaskPlanToolName,\n sanitizeAgentTaskPlanInput,\n} from './task-plan-labels'\n\nconst SSE_ENCODER = new TextEncoder()\nconst SSE_DECODER = new TextDecoder()\n\n/**\n * Mirrors the client-side `AiAgentTaskSnapshot` so server and client agree on\n * the wire format. Kept locally in this module to avoid pulling a UI package\n * dependency into the runtime \u2014 the shape is small and only ever serialized\n * to JSON for the SSE chunks below.\n */\nexport interface ServerTaskSnapshot {\n id: string\n label: string\n state: 'pending' | 'running' | 'done' | 'failed' | 'skipped'\n detail?: string\n source: 'runtime' | 'agent'\n toolCallId?: string\n}\n\nconst TERMINAL_STATES: ReadonlySet<ServerTaskSnapshot['state']> = new Set([\n 'done',\n 'failed',\n 'skipped',\n])\n\nconst TASK_LABEL_MAX_CHARS = TASK_PLAN_LABEL_MAX_CHARS\n\n/**\n * Convert a raw model-sanitized tool name (e.g. `customers__list_people`) to a\n * compact operator-facing label (e.g. `Customers list people`). The trailing\n * segment is title-cased so the plan reads like a checklist instead of a code\n * trace.\n */\nexport function deriveTaskLabel(toolName: string | undefined): string {\n if (typeof toolName !== 'string' || toolName.length === 0) {\n return 'Tool call'\n }\n const display = toolName.replace(/__/g, '.')\n const segments = display.split('.')\n const lastSegment = segments[segments.length - 1] ?? display\n const humanized = lastSegment.replace(/_/g, ' ').trim()\n if (humanized.length === 0) return display.slice(0, TASK_LABEL_MAX_CHARS)\n const titled = humanized.charAt(0).toUpperCase() + humanized.slice(1)\n if (segments.length <= 1) {\n return titled.slice(0, TASK_LABEL_MAX_CHARS)\n }\n const moduleSegment = segments[0]\n const moduleLabel = moduleSegment.charAt(0).toUpperCase() + moduleSegment.slice(1).replace(/_/g, ' ')\n const combined = `${moduleLabel} \u00B7 ${titled}`\n return combined.slice(0, TASK_LABEL_MAX_CHARS)\n}\n\ntype AccumulatorEntry = {\n snapshot: ServerTaskSnapshot\n emitted: boolean\n}\n\ntype ToolChunk = {\n type?: unknown\n toolCallId?: unknown\n toolName?: unknown\n input?: unknown\n}\n\n/**\n * Encapsulates the per-turn task-plan state. Exposed for unit tests so the\n * derivation logic can be exercised without standing up a full SSE pipeline.\n */\nexport class TaskPlanAccumulator {\n private readonly tasks = new Map<string, AccumulatorEntry>()\n private readonly toolCallToTaskId = new Map<string, string>()\n private readonly taskToolNames = new Map<string, string>()\n private readonly internalToolCallIds = new Set<string>()\n private snapshotEmitted = false\n private hasAgentAuthoredPlan = false\n\n constructor(public readonly planId: string) {}\n\n private upsert(\n id: string,\n patch: Partial<ServerTaskSnapshot> & { label?: string; toolCallId?: string },\n ): ServerTaskSnapshot {\n const existing = this.tasks.get(id)\n if (!existing) {\n const created: ServerTaskSnapshot = {\n id,\n label: patch.label ?? 'Tool call',\n state: patch.state ?? 'running',\n source: patch.source ?? 'runtime',\n detail: patch.detail,\n toolCallId: patch.toolCallId,\n }\n this.tasks.set(id, { snapshot: created, emitted: false })\n return created\n }\n const current = existing.snapshot\n const nextState = TERMINAL_STATES.has(current.state) ? current.state : patch.state ?? current.state\n const merged: ServerTaskSnapshot = {\n id: current.id,\n label: patch.label ?? current.label,\n state: nextState,\n source: patch.source ?? current.source,\n detail: patch.detail ?? current.detail,\n toolCallId: patch.toolCallId ?? current.toolCallId,\n }\n this.tasks.set(id, { snapshot: merged, emitted: existing.emitted })\n return merged\n }\n\n private makeUniqueTaskId(baseId: string): string {\n let candidate = baseId\n let suffix = 2\n while (this.tasks.has(candidate)) {\n candidate = `${baseId}-${suffix}`\n suffix += 1\n }\n return candidate\n }\n\n private emitFullSnapshot(): string[] {\n if (this.tasks.size === 0) return []\n this.snapshotEmitted = true\n const initialTasks = Array.from(this.tasks.values()).map((e) => e.snapshot)\n for (const e of this.tasks.values()) e.emitted = true\n return [\n formatSseEvent({\n type: 'data-agent-task-plan',\n planId: this.planId,\n tasks: initialTasks,\n }),\n ]\n }\n\n private handleAgentAuthoredPlan(input: unknown): string[] {\n const plan = sanitizeAgentTaskPlanInput(input)\n if (plan.tasks.length === 0) return []\n\n this.tasks.clear()\n this.toolCallToTaskId.clear()\n this.taskToolNames.clear()\n this.snapshotEmitted = false\n this.hasAgentAuthoredPlan = true\n\n plan.tasks.forEach((task, index) => {\n const id = this.makeUniqueTaskId(task.id ?? `agent-plan-${index + 1}`)\n const snapshot: ServerTaskSnapshot = {\n id,\n label: task.label,\n state: 'pending',\n source: 'agent',\n detail: task.detail,\n }\n this.tasks.set(id, { snapshot, emitted: false })\n if (task.toolName) {\n this.taskToolNames.set(id, task.toolName)\n }\n })\n\n return this.emitFullSnapshot()\n }\n\n private resolveTaskIdForToolCall(toolCallId: string, toolName: string | undefined): string {\n const existing = this.toolCallToTaskId.get(toolCallId)\n if (existing) return existing\n const plannedTaskId = this.findPlannedTaskId(toolName)\n if (plannedTaskId) {\n this.toolCallToTaskId.set(toolCallId, plannedTaskId)\n return plannedTaskId\n }\n this.toolCallToTaskId.set(toolCallId, toolCallId)\n return toolCallId\n }\n\n private findPlannedTaskId(toolName: string | undefined): string | null {\n if (!this.hasAgentAuthoredPlan) return null\n const entries = Array.from(this.tasks.entries())\n const isAvailable = (entry: AccumulatorEntry) => !TERMINAL_STATES.has(entry.snapshot.state)\n if (toolName) {\n const exactPending = entries.find(([id, entry]) => {\n return entry.snapshot.state === 'pending' && this.taskToolNames.get(id) === toolName\n })\n if (exactPending) return exactPending[0]\n const exactAvailable = entries.find(([id, entry]) => {\n return isAvailable(entry) && this.taskToolNames.get(id) === toolName\n })\n if (exactAvailable) return exactAvailable[0]\n }\n const genericPending = entries.find(([id, entry]) => {\n return entry.snapshot.state === 'pending' && !this.taskToolNames.has(id)\n })\n if (genericPending) return genericPending[0]\n const genericAvailable = entries.find(([id, entry]) => {\n return isAvailable(entry) && !this.taskToolNames.has(id)\n })\n return genericAvailable?.[0] ?? null\n }\n\n private existingSnapshot(taskId: string): ServerTaskSnapshot | undefined {\n return this.tasks.get(taskId)?.snapshot\n }\n\n /**\n * Apply a tool lifecycle chunk. Returns the SSE event lines (already\n * `data: ...\\n\\n`-formatted) that should be injected ahead of forwarding\n * the original chunk to the client.\n */\n handleToolChunk(chunk: ToolChunk): string[] {\n if (!chunk || typeof chunk.type !== 'string') return []\n const toolCallId = typeof chunk.toolCallId === 'string' ? chunk.toolCallId : null\n const toolName = normalizeTaskPlanToolName(chunk.toolName)\n if (isTaskPlanToolName(toolName)) {\n if (toolCallId) this.internalToolCallIds.add(toolCallId)\n if (chunk.type === 'tool-input-available') {\n return this.handleAgentAuthoredPlan(chunk.input)\n }\n return []\n }\n if (!toolCallId) return []\n if (this.internalToolCallIds.has(toolCallId)) return []\n const taskId = this.resolveTaskIdForToolCall(toolCallId, toolName)\n const existing = this.existingSnapshot(taskId)\n const source = existing?.source ?? 'runtime'\n const runtimeLabel = deriveTaskLabel(toolName)\n const runtimeDetail = toolName\n let nextSnapshot: ServerTaskSnapshot | null = null\n switch (chunk.type) {\n case 'tool-input-start':\n nextSnapshot = this.upsert(taskId, {\n label: source === 'agent' ? existing?.label : runtimeLabel,\n state: 'running',\n source,\n toolCallId,\n detail: source === 'agent' ? existing?.detail : runtimeDetail,\n })\n break\n case 'tool-input-available':\n // Runtime-derived tasks can refine the label when the SDK includes a\n // richer toolName on input-available. Agent-authored tasks keep the\n // safe label the model supplied through `meta.update_task_plan`.\n nextSnapshot = this.upsert(taskId, {\n label: source === 'agent' ? existing?.label : runtimeLabel,\n state: 'running',\n source,\n toolCallId,\n detail: source === 'agent' ? existing?.detail : runtimeDetail,\n })\n break\n case 'tool-output-available':\n nextSnapshot = this.upsert(taskId, {\n state: 'done',\n source,\n toolCallId,\n })\n break\n case 'tool-output-error':\n case 'tool-input-error':\n nextSnapshot = this.upsert(taskId, {\n state: 'failed',\n source,\n toolCallId,\n })\n break\n default:\n return []\n }\n if (!nextSnapshot) return []\n return this.emitForSnapshot(taskId, nextSnapshot)\n }\n\n private emitForSnapshot(id: string, snapshot: ServerTaskSnapshot): string[] {\n const entry = this.tasks.get(id)\n if (!entry) return []\n const lines: string[] = []\n if (!this.snapshotEmitted) {\n return this.emitFullSnapshot()\n }\n if (!entry.emitted) {\n // First time we surface this task: it must be part of the next snapshot\n // refresh, but to keep the protocol minimal we emit a single\n // `data-agent-task-update` carrying the new task \u2014 clients merge by id.\n lines.push(\n formatSseEvent({\n type: 'data-agent-task-update',\n planId: this.planId,\n task: snapshot,\n }),\n )\n entry.emitted = true\n return lines\n }\n lines.push(\n formatSseEvent({\n type: 'data-agent-task-update',\n planId: this.planId,\n task: snapshot,\n }),\n )\n return lines\n }\n}\n\nexport function formatSseEvent(payload: Record<string, unknown>): string {\n return `data: ${JSON.stringify(payload)}\\n\\n`\n}\n\n/**\n * Wrap a streaming `Response` and interleave `data-agent-task-plan` /\n * `data-agent-task-update` SSE chunks. The wrapper does not consume the\n * stream \u2014 it pipes bytes through and only parses event boundaries to know\n * when to inject extra chunks.\n */\nexport function injectTaskPlanIntoStream(\n baseResponse: Response,\n planId: string,\n): Response {\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n const writer = writable.getWriter()\n const accumulator = new TaskPlanAccumulator(planId)\n\n async function pump(): Promise<void> {\n if (!baseResponse.body) {\n await writer.close()\n return\n }\n const reader = baseResponse.body.getReader()\n let textBuffer = ''\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n textBuffer += SSE_DECODER.decode(value, { stream: true })\n textBuffer = await flushBuffer(textBuffer, accumulator, writer)\n }\n const tail = SSE_DECODER.decode()\n if (tail) {\n textBuffer += tail\n }\n if (textBuffer.length > 0) {\n // Best-effort flush of any trailing bytes (the AI SDK always\n // terminates events with `\\n\\n` so this path is rare).\n await writer.write(SSE_ENCODER.encode(textBuffer))\n }\n } catch {\n // Surface upstream aborts to the downstream consumer by closing the\n // writer \u2014 propagating the error would corrupt the SSE stream.\n } finally {\n reader.releaseLock()\n await writer.close().catch(() => undefined)\n }\n }\n\n void pump()\n return new Response(readable, {\n status: baseResponse.status,\n headers: baseResponse.headers,\n })\n}\n\nasync function flushBuffer(\n buffer: string,\n accumulator: TaskPlanAccumulator,\n writer: WritableStreamDefaultWriter<Uint8Array>,\n): Promise<string> {\n let rest = buffer\n for (;;) {\n const boundary = rest.indexOf('\\n\\n')\n if (boundary === -1) break\n const eventBlock = rest.slice(0, boundary + 2)\n rest = rest.slice(boundary + 2)\n const injected = inspectEventBlock(eventBlock, accumulator)\n for (const line of injected.before) {\n await writer.write(SSE_ENCODER.encode(line))\n }\n await writer.write(SSE_ENCODER.encode(eventBlock))\n for (const line of injected.after) {\n await writer.write(SSE_ENCODER.encode(line))\n }\n }\n return rest\n}\n\ninterface InjectedLines {\n before: string[]\n after: string[]\n}\n\nfunction inspectEventBlock(\n eventBlock: string,\n accumulator: TaskPlanAccumulator,\n): InjectedLines {\n const dataPayload = extractDataPayload(eventBlock)\n if (!dataPayload || dataPayload === '[DONE]') {\n return { before: [], after: [] }\n }\n let parsed: ToolChunk | null = null\n try {\n parsed = JSON.parse(dataPayload)\n } catch {\n return { before: [], after: [] }\n }\n if (!parsed || typeof parsed.type !== 'string') {\n return { before: [], after: [] }\n }\n const type = parsed.type\n const injected = accumulator.handleToolChunk(parsed)\n if (injected.length === 0) {\n return { before: [], after: [] }\n }\n // Tool-input-start gets the plan event BEFORE the original (so the row\n // appears at the same time as the tool starts). Output / error events\n // get the plan event AFTER so the row updates only once the tool result\n // is visible in the existing tool-call detail row.\n if (type === 'tool-input-start' || type === 'tool-input-available') {\n return { before: injected, after: [] }\n }\n return { before: [], after: injected }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const lines = eventBlock.split('\\n')\n const dataLines: string[] = []\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6))\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5))\n }\n }\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,6 +4,9 @@ const toolFixtures = {
|
|
|
4
4
|
"meta.describe_agent": f({
|
|
5
5
|
input: { agentId: "customers.account_assistant" }
|
|
6
6
|
}),
|
|
7
|
+
"meta.update_task_plan": f({
|
|
8
|
+
input: { tasks: [{ label: "Search records", toolName: "customers.list_people" }] }
|
|
9
|
+
}),
|
|
7
10
|
"attachments.list_record_attachments": f({
|
|
8
11
|
input: { entityType: "customers:person", recordId: "00000000-0000-0000-0000-000000000000", limit: 5 },
|
|
9
12
|
note: "Empty result is a valid response; we only assert shape."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/ai_assistant/lib/tool-test-fixtures.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Per-tool sample inputs used by `tool-test-runner.ts` (see also the\n * `test-tools` CLI subcommand and `TC-INT-AI-TOOLS.spec.ts`).\n *\n * Inputs are deliberately conservative \u2014 every entry is the smallest valid\n * shape that exercises the handler against demo-seeded data. ID-shaped tools\n * declare `idFrom` so the runner first runs the sibling list tool and threads\n * the discovered id through; this avoids hand-rolling fixture UUIDs that go\n * stale every time the demo seed regenerates.\n *\n * Tools without a fixture are skipped with reason `'no fixture'` rather than\n * failing \u2014 keeps the test green for newly added tools until a maintainer\n * adds an entry. Mutation tools share this map and are exercised through\n * `prepareMutation`; the runner asserts they return a pending-action envelope\n * and never confirms the action.\n */\n\nexport type ToolFixtureValueRef = { idFrom: string; field?: string }\n\nexport type ToolFixtureInput = Record<string, unknown> | ToolFixtureValueRef\n\nexport interface ToolFixture {\n /** When `true` the runner skips this tool with reason `'skip-by-fixture'`. */\n skip?: boolean\n /** Static input passed to the handler. Overrides `idFrom`-based inputs. */\n input?: Record<string, unknown>\n /**\n * When set, the runner calls the named list tool first, picks the first\n * record, reads `field` (default `id`) from it, and merges it into the input\n * under the key declared by `bindAs` (default `id`).\n */\n idFrom?: string\n bindAs?: string\n /** Optional extra static fields merged on top of the chained input. */\n extra?: Record<string, unknown>\n /** Free-form note explaining the fixture choice. */\n note?: string\n}\n\nconst f = (fixture: ToolFixture): ToolFixture => fixture\n\nexport const toolFixtures: Record<string, ToolFixture> = {\n // -------------------- ai_assistant module --------------------\n 'meta.describe_agent': f({\n input: { agentId: 'customers.account_assistant' },\n }),\n 'attachments.list_record_attachments': f({\n input: { entityType: 'customers:person', recordId: '00000000-0000-0000-0000-000000000000', limit: 5 },\n note: 'Empty result is a valid response; we only assert shape.',\n }),\n 'attachments.read_attachment': f({\n skip: true,\n note: 'Requires a real attachment id \u2014 covered by attachment-bridge integration tests.',\n }),\n 'search.hybrid_search': f({\n input: { q: 'demo', limit: 5 },\n }),\n 'search.get_record_context': f({\n skip: true,\n note: 'Requires a known record id from the search index; covered by hybrid_search tests.',\n }),\n\n // -------------------- search module --------------------\n search_query: f({ input: { q: 'demo', limit: 5 } }),\n search_status: f({ input: {} }),\n search_get: f({ skip: true, note: 'Requires a known indexed record id.' }),\n search_schema: f({ input: {} }),\n search_aggregate: f({ skip: true, note: 'Requires field config; covered by search module unit tests.' }),\n search_reindex: f({ skip: true, note: 'Mutation tool \u2014 would trigger a real reindex job.' }),\n\n // -------------------- customers module --------------------\n 'customers.list_people': f({ input: { limit: 5 } }),\n 'customers.get_person': f({\n skip: true,\n note: 'Underlying route packages/core/.../customers/api/people/[id]/route.js imports `next/server` which Node ESM cannot resolve from package dist. Bug \u2014 re-enable once the route is ESM-clean or routed through the operation runner with a different loader.',\n }),\n 'customers.list_companies': f({ input: { limit: 5 } }),\n 'customers.get_company': f({\n skip: true,\n note: 'Same next/server ESM-resolution issue as customers.get_person.',\n }),\n 'customers.list_deals': f({ input: { limit: 5 } }),\n 'customers.get_deal': f({\n skip: true,\n note: 'Same next/server ESM-resolution issue as customers.get_person.',\n }),\n 'customers.list_activities': f({ input: { limit: 5 } }),\n 'customers.list_tasks': f({ input: { limit: 5 } }),\n 'customers.list_addresses': f({\n idFrom: 'customers.list_people',\n bindAs: 'entityId',\n extra: { entityType: 'person' },\n note: 'Schema requires { entityType: \"person\" | \"company\", entityId }.',\n }),\n 'customers.list_tags': f({ input: { limit: 5 } }),\n 'customers.get_settings': f({ input: {} }),\n 'customers.update_deal_stage': f({\n idFrom: 'customers.list_deals',\n bindAs: 'dealId',\n extra: { toStage: 'open' },\n note: 'Mutation \u2014 schema requires exactly one of { toPipelineStageId, toStage }.',\n }),\n\n // -------------------- catalog module --------------------\n 'catalog.list_products': f({ input: { limit: 5 } }),\n 'catalog.search_products': f({ input: { query: 'demo', limit: 5 } }),\n 'catalog.get_product': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.get_product_bundle': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.list_categories': f({ input: { limit: 10 } }),\n 'catalog.list_variants': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.list_offers': f({\n skip: true,\n note: 'Tool requiredFeatures do not cover the underlying GET /catalog/offers route requiredFeatures \u2014 real ACL config drift. Re-enable after fixing catalog.list_offers requiredFeatures to include the route gates.',\n }),\n 'catalog.list_media': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.list_tags': f({ input: { limit: 10 } }),\n 'catalog.get_configuration': f({ input: {} }),\n // Authoring/structured-output tools \u2014 they call the LLM, skip in test mode\n // so TC-INT-AI-TOOLS still passes in CI without provider API keys.\n 'catalog.draft_description_from_attributes': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.extract_attributes_from_description': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.draft_description_from_media': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.suggest_title_variants': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.suggest_price_adjustment': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n // Mutation tools \u2014 exercised via prepareMutation only.\n 'catalog.update_product': f({\n idFrom: 'catalog.list_products',\n bindAs: 'productId',\n extra: { subtitle: 'tool-test-runner-noop' },\n note: 'Single optional field tweak; prepareMutation produces a preview card.',\n }),\n 'catalog.bulk_update_products': f({\n skip: true,\n note: 'Bulk mutation; needs records[] with versions \u2014 covered by catalog merchandising integration test.',\n }),\n 'catalog.apply_attribute_extraction': f({\n skip: true,\n note: 'Bulk mutation tied to extract_attributes output.',\n }),\n 'catalog.update_product_media_descriptions': f({\n skip: true,\n note: 'Bulk mutation tied to draft_media_descriptions output.',\n }),\n\n // -------------------- inbox_ops module --------------------\n inbox_ops_list_proposals: f({ input: { limit: 5 } }),\n inbox_ops_get_proposal: f({ idFrom: 'inbox_ops_list_proposals' }),\n inbox_ops_categorize_email: f({\n skip: true,\n note: 'Requires a queued email payload; LLM-backed.',\n }),\n inbox_ops_accept_action: f({\n skip: true,\n note: 'Mutation that finalizes a proposal; covered by inbox_ops integration tests.',\n }),\n}\n\nexport function getFixture(toolName: string): ToolFixture | undefined {\n return toolFixtures[toolName]\n}\n"],
|
|
5
|
-
"mappings": "AAuCA,MAAM,IAAI,CAAC,YAAsC;AAE1C,MAAM,eAA4C;AAAA;AAAA,EAEvD,uBAAuB,EAAE;AAAA,IACvB,OAAO,EAAE,SAAS,8BAA8B;AAAA,EAClD,CAAC;AAAA,EACD,uCAAuC,EAAE;AAAA,IACvC,OAAO,EAAE,YAAY,oBAAoB,UAAU,wCAAwC,OAAO,EAAE;AAAA,IACpG,MAAM;AAAA,EACR,CAAC;AAAA,EACD,+BAA+B,EAAE;AAAA,IAC/B,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,wBAAwB,EAAE;AAAA,IACxB,OAAO,EAAE,GAAG,QAAQ,OAAO,EAAE;AAAA,EAC/B,CAAC;AAAA,EACD,6BAA6B,EAAE;AAAA,IAC7B,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA;AAAA,EAGD,cAAc,EAAE,EAAE,OAAO,EAAE,GAAG,QAAQ,OAAO,EAAE,EAAE,CAAC;AAAA,EAClD,eAAe,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,EAC9B,YAAY,EAAE,EAAE,MAAM,MAAM,MAAM,sCAAsC,CAAC;AAAA,EACzE,eAAe,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,EAC9B,kBAAkB,EAAE,EAAE,MAAM,MAAM,MAAM,8DAA8D,CAAC;AAAA,EACvG,gBAAgB,EAAE,EAAE,MAAM,MAAM,MAAM,yDAAoD,CAAC;AAAA;AAAA,EAG3F,yBAAyB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EAClD,wBAAwB,EAAE;AAAA,IACxB,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,4BAA4B,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EACrD,yBAAyB,EAAE;AAAA,IACzB,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,wBAAwB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EACjD,sBAAsB,EAAE;AAAA,IACtB,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,6BAA6B,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EACtD,wBAAwB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EACjD,4BAA4B,EAAE;AAAA,IAC5B,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO,EAAE,YAAY,SAAS;AAAA,IAC9B,MAAM;AAAA,EACR,CAAC;AAAA,EACD,uBAAuB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EAChD,0BAA0B,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,EACzC,+BAA+B,EAAE;AAAA,IAC/B,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO,EAAE,SAAS,OAAO;AAAA,IACzB,MAAM;AAAA,EACR,CAAC;AAAA;AAAA,EAGD,yBAAyB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EAClD,2BAA2B,EAAE,EAAE,OAAO,EAAE,OAAO,QAAQ,OAAO,EAAE,EAAE,CAAC;AAAA,EACnE,uBAAuB,EAAE,EAAE,QAAQ,yBAAyB,QAAQ,YAAY,CAAC;AAAA,EACjF,8BAA8B,EAAE,EAAE,QAAQ,yBAAyB,QAAQ,YAAY,CAAC;AAAA,EACxF,2BAA2B,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,CAAC;AAAA,EACrD,yBAAyB,EAAE,EAAE,QAAQ,yBAAyB,QAAQ,YAAY,CAAC;AAAA,EACnF,uBAAuB,EAAE;AAAA,IACvB,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,sBAAsB,EAAE,EAAE,QAAQ,yBAAyB,QAAQ,YAAY,CAAC;AAAA,EAChF,qBAAqB,EAAE,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,CAAC;AAAA,EAC/C,6BAA6B,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA;AAAA;AAAA,EAG5C,6CAA6C,EAAE,EAAE,MAAM,MAAM,MAAM,6BAA6B,CAAC;AAAA,EACjG,+CAA+C,EAAE,EAAE,MAAM,MAAM,MAAM,6BAA6B,CAAC;AAAA,EACnG,wCAAwC,EAAE,EAAE,MAAM,MAAM,MAAM,6BAA6B,CAAC;AAAA,EAC5F,kCAAkC,EAAE,EAAE,MAAM,MAAM,MAAM,6BAA6B,CAAC;AAAA,EACtF,oCAAoC,EAAE,EAAE,MAAM,MAAM,MAAM,6BAA6B,CAAC;AAAA;AAAA,EAExF,0BAA0B,EAAE;AAAA,IAC1B,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO,EAAE,UAAU,wBAAwB;AAAA,IAC3C,MAAM;AAAA,EACR,CAAC;AAAA,EACD,gCAAgC,EAAE;AAAA,IAChC,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,sCAAsC,EAAE;AAAA,IACtC,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,6CAA6C,EAAE;AAAA,IAC7C,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA;AAAA,EAGD,0BAA0B,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,CAAC;AAAA,EACnD,wBAAwB,EAAE,EAAE,QAAQ,2BAA2B,CAAC;AAAA,EAChE,4BAA4B,EAAE;AAAA,IAC5B,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AAAA,EACD,yBAAyB,EAAE;AAAA,IACzB,MAAM;AAAA,IACN,MAAM;AAAA,EACR,CAAC;AACH;AAEO,SAAS,WAAW,UAA2C;AACpE,SAAO,aAAa,QAAQ;AAC9B;",
|
|
4
|
+
"sourcesContent": ["/**\n * Per-tool sample inputs used by `tool-test-runner.ts` (see also the\n * `test-tools` CLI subcommand and `TC-INT-AI-TOOLS.spec.ts`).\n *\n * Inputs are deliberately conservative \u2014 every entry is the smallest valid\n * shape that exercises the handler against demo-seeded data. ID-shaped tools\n * declare `idFrom` so the runner first runs the sibling list tool and threads\n * the discovered id through; this avoids hand-rolling fixture UUIDs that go\n * stale every time the demo seed regenerates.\n *\n * Tools without a fixture are skipped with reason `'no fixture'` rather than\n * failing \u2014 keeps the test green for newly added tools until a maintainer\n * adds an entry. Mutation tools share this map and are exercised through\n * `prepareMutation`; the runner asserts they return a pending-action envelope\n * and never confirms the action.\n */\n\nexport type ToolFixtureValueRef = { idFrom: string; field?: string }\n\nexport type ToolFixtureInput = Record<string, unknown> | ToolFixtureValueRef\n\nexport interface ToolFixture {\n /** When `true` the runner skips this tool with reason `'skip-by-fixture'`. */\n skip?: boolean\n /** Static input passed to the handler. Overrides `idFrom`-based inputs. */\n input?: Record<string, unknown>\n /**\n * When set, the runner calls the named list tool first, picks the first\n * record, reads `field` (default `id`) from it, and merges it into the input\n * under the key declared by `bindAs` (default `id`).\n */\n idFrom?: string\n bindAs?: string\n /** Optional extra static fields merged on top of the chained input. */\n extra?: Record<string, unknown>\n /** Free-form note explaining the fixture choice. */\n note?: string\n}\n\nconst f = (fixture: ToolFixture): ToolFixture => fixture\n\nexport const toolFixtures: Record<string, ToolFixture> = {\n // -------------------- ai_assistant module --------------------\n 'meta.describe_agent': f({\n input: { agentId: 'customers.account_assistant' },\n }),\n 'meta.update_task_plan': f({\n input: { tasks: [{ label: 'Search records', toolName: 'customers.list_people' }] },\n }),\n 'attachments.list_record_attachments': f({\n input: { entityType: 'customers:person', recordId: '00000000-0000-0000-0000-000000000000', limit: 5 },\n note: 'Empty result is a valid response; we only assert shape.',\n }),\n 'attachments.read_attachment': f({\n skip: true,\n note: 'Requires a real attachment id \u2014 covered by attachment-bridge integration tests.',\n }),\n 'search.hybrid_search': f({\n input: { q: 'demo', limit: 5 },\n }),\n 'search.get_record_context': f({\n skip: true,\n note: 'Requires a known record id from the search index; covered by hybrid_search tests.',\n }),\n\n // -------------------- search module --------------------\n search_query: f({ input: { q: 'demo', limit: 5 } }),\n search_status: f({ input: {} }),\n search_get: f({ skip: true, note: 'Requires a known indexed record id.' }),\n search_schema: f({ input: {} }),\n search_aggregate: f({ skip: true, note: 'Requires field config; covered by search module unit tests.' }),\n search_reindex: f({ skip: true, note: 'Mutation tool \u2014 would trigger a real reindex job.' }),\n\n // -------------------- customers module --------------------\n 'customers.list_people': f({ input: { limit: 5 } }),\n 'customers.get_person': f({\n skip: true,\n note: 'Underlying route packages/core/.../customers/api/people/[id]/route.js imports `next/server` which Node ESM cannot resolve from package dist. Bug \u2014 re-enable once the route is ESM-clean or routed through the operation runner with a different loader.',\n }),\n 'customers.list_companies': f({ input: { limit: 5 } }),\n 'customers.get_company': f({\n skip: true,\n note: 'Same next/server ESM-resolution issue as customers.get_person.',\n }),\n 'customers.list_deals': f({ input: { limit: 5 } }),\n 'customers.get_deal': f({\n skip: true,\n note: 'Same next/server ESM-resolution issue as customers.get_person.',\n }),\n 'customers.list_activities': f({ input: { limit: 5 } }),\n 'customers.list_tasks': f({ input: { limit: 5 } }),\n 'customers.list_addresses': f({\n idFrom: 'customers.list_people',\n bindAs: 'entityId',\n extra: { entityType: 'person' },\n note: 'Schema requires { entityType: \"person\" | \"company\", entityId }.',\n }),\n 'customers.list_tags': f({ input: { limit: 5 } }),\n 'customers.get_settings': f({ input: {} }),\n 'customers.update_deal_stage': f({\n idFrom: 'customers.list_deals',\n bindAs: 'dealId',\n extra: { toStage: 'open' },\n note: 'Mutation \u2014 schema requires exactly one of { toPipelineStageId, toStage }.',\n }),\n\n // -------------------- catalog module --------------------\n 'catalog.list_products': f({ input: { limit: 5 } }),\n 'catalog.search_products': f({ input: { query: 'demo', limit: 5 } }),\n 'catalog.get_product': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.get_product_bundle': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.list_categories': f({ input: { limit: 10 } }),\n 'catalog.list_variants': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.list_offers': f({\n skip: true,\n note: 'Tool requiredFeatures do not cover the underlying GET /catalog/offers route requiredFeatures \u2014 real ACL config drift. Re-enable after fixing catalog.list_offers requiredFeatures to include the route gates.',\n }),\n 'catalog.list_media': f({ idFrom: 'catalog.list_products', bindAs: 'productId' }),\n 'catalog.list_tags': f({ input: { limit: 10 } }),\n 'catalog.get_configuration': f({ input: {} }),\n // Authoring/structured-output tools \u2014 they call the LLM, skip in test mode\n // so TC-INT-AI-TOOLS still passes in CI without provider API keys.\n 'catalog.draft_description_from_attributes': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.extract_attributes_from_description': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.draft_description_from_media': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.suggest_title_variants': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n 'catalog.suggest_price_adjustment': f({ skip: true, note: 'LLM-backed authoring tool.' }),\n // Mutation tools \u2014 exercised via prepareMutation only.\n 'catalog.update_product': f({\n idFrom: 'catalog.list_products',\n bindAs: 'productId',\n extra: { subtitle: 'tool-test-runner-noop' },\n note: 'Single optional field tweak; prepareMutation produces a preview card.',\n }),\n 'catalog.bulk_update_products': f({\n skip: true,\n note: 'Bulk mutation; needs records[] with versions \u2014 covered by catalog merchandising integration test.',\n }),\n 'catalog.apply_attribute_extraction': f({\n skip: true,\n note: 'Bulk mutation tied to extract_attributes output.',\n }),\n 'catalog.update_product_media_descriptions': f({\n skip: true,\n note: 'Bulk mutation tied to draft_media_descriptions output.',\n }),\n\n // -------------------- inbox_ops module --------------------\n inbox_ops_list_proposals: f({ input: { limit: 5 } }),\n inbox_ops_get_proposal: f({ idFrom: 'inbox_ops_list_proposals' }),\n inbox_ops_categorize_email: f({\n skip: true,\n note: 'Requires a queued email payload; LLM-backed.',\n }),\n inbox_ops_accept_action: f({\n skip: true,\n note: 'Mutation that finalizes a proposal; covered by inbox_ops integration tests.',\n }),\n}\n\nexport function getFixture(toolName: string): ToolFixture | undefined {\n return toolFixtures[toolName]\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/ai-assistant",
|
|
3
|
-
"version": "0.6.2-develop.
|
|
3
|
+
"version": "0.6.2-develop.3446.1.bd060c6017",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -98,16 +98,16 @@
|
|
|
98
98
|
"zod-to-json-schema": "^3.25.2"
|
|
99
99
|
},
|
|
100
100
|
"peerDependencies": {
|
|
101
|
-
"@open-mercato/shared": "0.6.2-develop.
|
|
102
|
-
"@open-mercato/ui": "0.6.2-develop.
|
|
101
|
+
"@open-mercato/shared": "0.6.2-develop.3446.1.bd060c6017",
|
|
102
|
+
"@open-mercato/ui": "0.6.2-develop.3446.1.bd060c6017",
|
|
103
103
|
"react": "^19.0.0",
|
|
104
104
|
"react-dom": "^19.0.0",
|
|
105
105
|
"zod": ">=3.23.0"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"@open-mercato/cli": "0.6.2-develop.
|
|
109
|
-
"@open-mercato/shared": "0.6.2-develop.
|
|
110
|
-
"@open-mercato/ui": "0.6.2-develop.
|
|
108
|
+
"@open-mercato/cli": "0.6.2-develop.3446.1.bd060c6017",
|
|
109
|
+
"@open-mercato/shared": "0.6.2-develop.3446.1.bd060c6017",
|
|
110
|
+
"@open-mercato/ui": "0.6.2-develop.3446.1.bd060c6017",
|
|
111
111
|
"@types/react": "^19.2.14",
|
|
112
112
|
"@types/react-dom": "^19.2.3",
|
|
113
113
|
"react": "19.2.6",
|
|
@@ -30,7 +30,7 @@ export function AiChatButton({ onClick, className }: AiChatButtonProps) {
|
|
|
30
30
|
className={className}
|
|
31
31
|
aria-label="Open AI Assistant"
|
|
32
32
|
>
|
|
33
|
-
<AiIcon className="h-5 w-5" />
|
|
33
|
+
<AiIcon className="h-5 w-5 text-foreground" />
|
|
34
34
|
</Button>
|
|
35
35
|
</TooltipTrigger>
|
|
36
36
|
<TooltipContent side="bottom">
|
|
@@ -106,10 +106,7 @@ test.describe('TC-AI-TOKEN-USAGE-001–005: token usage stats page', () => {
|
|
|
106
106
|
test('TC-AI-TOKEN-USAGE-002: apply filter triggers re-fetch with new date params', async ({ page }) => {
|
|
107
107
|
await login(page, 'superadmin');
|
|
108
108
|
|
|
109
|
-
const fetchedUrls: string[] = [];
|
|
110
|
-
|
|
111
109
|
await page.route('**/api/ai_assistant/usage/daily**', async (route) => {
|
|
112
|
-
fetchedUrls.push(route.request().url());
|
|
113
110
|
await route.fulfill({
|
|
114
111
|
status: 200,
|
|
115
112
|
contentType: 'application/json',
|
|
@@ -125,7 +122,16 @@ test.describe('TC-AI-TOKEN-USAGE-001–005: token usage stats page', () => {
|
|
|
125
122
|
});
|
|
126
123
|
});
|
|
127
124
|
|
|
125
|
+
const initialDailyRequest = page.waitForResponse(
|
|
126
|
+
(response) => response.url().includes('/api/ai_assistant/usage/daily') && response.status() === 200,
|
|
127
|
+
{ timeout: 15_000 },
|
|
128
|
+
);
|
|
129
|
+
const initialSessionsRequest = page.waitForResponse(
|
|
130
|
+
(response) => response.url().includes('/api/ai_assistant/usage/sessions') && response.status() === 200,
|
|
131
|
+
{ timeout: 15_000 },
|
|
132
|
+
);
|
|
128
133
|
await page.goto(USAGE_PAGE, { waitUntil: 'domcontentloaded' });
|
|
134
|
+
await Promise.all([initialDailyRequest, initialSessionsRequest]);
|
|
129
135
|
|
|
130
136
|
const fromInput = page.locator('#usage-from');
|
|
131
137
|
const toInput = page.locator('#usage-to');
|
|
@@ -135,12 +141,18 @@ test.describe('TC-AI-TOKEN-USAGE-001–005: token usage stats page', () => {
|
|
|
135
141
|
|
|
136
142
|
await fromInput.fill('2026-04-01');
|
|
137
143
|
await toInput.fill('2026-04-30');
|
|
144
|
+
await expect(fromInput).toHaveValue('2026-04-01');
|
|
145
|
+
await expect(toInput).toHaveValue('2026-04-30');
|
|
146
|
+
const updatedDailyRequest = page.waitForResponse(
|
|
147
|
+
(response) =>
|
|
148
|
+
response.url().includes('/api/ai_assistant/usage/daily') &&
|
|
149
|
+
response.url().includes('from=2026-04-01') &&
|
|
150
|
+
response.url().includes('to=2026-04-30') &&
|
|
151
|
+
response.status() === 200,
|
|
152
|
+
{ timeout: 10_000 },
|
|
153
|
+
);
|
|
138
154
|
await applyButton.click();
|
|
139
|
-
|
|
140
|
-
await page.waitForTimeout(500);
|
|
141
|
-
|
|
142
|
-
const hasNewDates = fetchedUrls.some((url) => url.includes('from=2026-04-01'));
|
|
143
|
-
expect(hasNewDates).toBe(true);
|
|
155
|
+
await updatedDailyRequest;
|
|
144
156
|
});
|
|
145
157
|
|
|
146
158
|
test('TC-AI-TOKEN-USAGE-003: sessions list renders rows when API returns sessions', async ({ page }) => {
|