@contractspec/module.ai-chat 4.3.17 → 4.3.18
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/dist/browser/context/index.js +3 -415
- package/dist/browser/core/index.js +55 -1600
- package/dist/browser/index.js +58 -5251
- package/dist/browser/presentation/components/index.js +56 -4382
- package/dist/browser/presentation/hooks/index.js +32 -1638
- package/dist/browser/presentation/index.js +56 -4430
- package/dist/browser/providers/index.js +1 -51
- package/dist/context/index.js +3 -409
- package/dist/core/index.js +55 -1594
- package/dist/index.js +58 -5245
- package/dist/node/context/index.js +3 -410
- package/dist/node/core/index.js +55 -1595
- package/dist/node/index.js +58 -5246
- package/dist/node/presentation/components/index.js +56 -4377
- package/dist/node/presentation/hooks/index.js +32 -1633
- package/dist/node/presentation/index.js +56 -4425
- package/dist/node/providers/index.js +1 -46
- package/dist/presentation/components/index.js +56 -4376
- package/dist/presentation/hooks/index.js +32 -1632
- package/dist/presentation/index.js +56 -4424
- package/dist/providers/index.js +1 -45
- package/package.json +15 -15
|
@@ -1,685 +1,41 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { tool as
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { generateText, streamText } from "ai";
|
|
17
|
-
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
18
|
-
|
|
19
|
-
// src/core/agent-tools-adapter.ts
|
|
20
|
-
import { tool } from "ai";
|
|
21
|
-
import { z } from "zod";
|
|
22
|
-
function getInputSchema(_schema) {
|
|
23
|
-
return z.object({}).passthrough();
|
|
24
|
-
}
|
|
25
|
-
function agentToolConfigsToToolSet(configs, handlers) {
|
|
26
|
-
const result = {};
|
|
27
|
-
for (const config of configs) {
|
|
28
|
-
const handler = handlers?.[config.name];
|
|
29
|
-
const inputSchema = getInputSchema(config.schema);
|
|
30
|
-
result[config.name] = tool({
|
|
31
|
-
description: config.description ?? config.name,
|
|
32
|
-
inputSchema,
|
|
33
|
-
execute: async (input) => {
|
|
34
|
-
if (!handler) {
|
|
35
|
-
return {
|
|
36
|
-
status: "unimplemented",
|
|
37
|
-
message: "Wire handler in host",
|
|
38
|
-
toolName: config.name
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
const output = await Promise.resolve(handler(input));
|
|
43
|
-
return typeof output === "string" ? output : output;
|
|
44
|
-
} catch (err) {
|
|
45
|
-
return {
|
|
46
|
-
status: "error",
|
|
47
|
-
error: err instanceof Error ? err.message : String(err),
|
|
48
|
-
toolName: config.name
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// src/core/contracts-context.ts
|
|
58
|
-
function buildContractsContextPrompt(config) {
|
|
59
|
-
const parts = [];
|
|
60
|
-
if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
|
|
61
|
-
return "";
|
|
62
|
-
}
|
|
63
|
-
parts.push(`
|
|
64
|
-
|
|
65
|
-
## Available resources`);
|
|
66
|
-
if (config.agentSpecs?.length) {
|
|
67
|
-
parts.push(`
|
|
68
|
-
### Agent tools`);
|
|
69
|
-
for (const agent of config.agentSpecs) {
|
|
70
|
-
const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
|
|
71
|
-
parts.push(`- **${agent.key}**: tools: ${toolNames}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (config.dataViewSpecs?.length) {
|
|
75
|
-
parts.push(`
|
|
76
|
-
### Data views`);
|
|
77
|
-
for (const dv of config.dataViewSpecs) {
|
|
78
|
-
parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (config.formSpecs?.length) {
|
|
82
|
-
parts.push(`
|
|
83
|
-
### Forms`);
|
|
84
|
-
for (const form of config.formSpecs) {
|
|
85
|
-
parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (config.presentationSpecs?.length) {
|
|
89
|
-
parts.push(`
|
|
90
|
-
### Presentations`);
|
|
91
|
-
for (const pres of config.presentationSpecs) {
|
|
92
|
-
parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (config.operationRefs?.length) {
|
|
96
|
-
parts.push(`
|
|
97
|
-
### Operations`);
|
|
98
|
-
for (const op of config.operationRefs) {
|
|
99
|
-
parts.push(`- **${op.key}@${op.version}**`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
parts.push(`
|
|
103
|
-
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
|
|
104
|
-
return parts.join(`
|
|
105
|
-
`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// src/core/conversation-store.ts
|
|
109
|
-
function generateId(prefix) {
|
|
110
|
-
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
class InMemoryConversationStore {
|
|
114
|
-
conversations = new Map;
|
|
115
|
-
async get(conversationId) {
|
|
116
|
-
return this.conversations.get(conversationId) ?? null;
|
|
117
|
-
}
|
|
118
|
-
async create(conversation) {
|
|
119
|
-
const now = new Date;
|
|
120
|
-
const fullConversation = {
|
|
121
|
-
...conversation,
|
|
122
|
-
id: generateId("conv"),
|
|
123
|
-
createdAt: now,
|
|
124
|
-
updatedAt: now
|
|
125
|
-
};
|
|
126
|
-
this.conversations.set(fullConversation.id, fullConversation);
|
|
127
|
-
return fullConversation;
|
|
128
|
-
}
|
|
129
|
-
async update(conversationId, updates) {
|
|
130
|
-
const conversation = this.conversations.get(conversationId);
|
|
131
|
-
if (!conversation)
|
|
132
|
-
return null;
|
|
133
|
-
const updated = {
|
|
134
|
-
...conversation,
|
|
135
|
-
...updates,
|
|
136
|
-
updatedAt: new Date
|
|
137
|
-
};
|
|
138
|
-
this.conversations.set(conversationId, updated);
|
|
139
|
-
return updated;
|
|
140
|
-
}
|
|
141
|
-
async appendMessage(conversationId, message) {
|
|
142
|
-
const conversation = this.conversations.get(conversationId);
|
|
143
|
-
if (!conversation) {
|
|
144
|
-
throw new Error(`Conversation ${conversationId} not found`);
|
|
145
|
-
}
|
|
146
|
-
const now = new Date;
|
|
147
|
-
const fullMessage = {
|
|
148
|
-
...message,
|
|
149
|
-
id: generateId("msg"),
|
|
150
|
-
conversationId,
|
|
151
|
-
createdAt: now,
|
|
152
|
-
updatedAt: now
|
|
153
|
-
};
|
|
154
|
-
conversation.messages.push(fullMessage);
|
|
155
|
-
conversation.updatedAt = now;
|
|
156
|
-
return fullMessage;
|
|
157
|
-
}
|
|
158
|
-
async updateMessage(conversationId, messageId, updates) {
|
|
159
|
-
const conversation = this.conversations.get(conversationId);
|
|
160
|
-
if (!conversation)
|
|
161
|
-
return null;
|
|
162
|
-
const messageIndex = conversation.messages.findIndex((m) => m.id === messageId);
|
|
163
|
-
if (messageIndex === -1)
|
|
164
|
-
return null;
|
|
165
|
-
const message = conversation.messages[messageIndex];
|
|
166
|
-
if (!message)
|
|
167
|
-
return null;
|
|
168
|
-
const updated = {
|
|
169
|
-
...message,
|
|
170
|
-
...updates,
|
|
171
|
-
updatedAt: new Date
|
|
172
|
-
};
|
|
173
|
-
conversation.messages[messageIndex] = updated;
|
|
174
|
-
conversation.updatedAt = new Date;
|
|
175
|
-
return updated;
|
|
176
|
-
}
|
|
177
|
-
async delete(conversationId) {
|
|
178
|
-
return this.conversations.delete(conversationId);
|
|
179
|
-
}
|
|
180
|
-
async list(options) {
|
|
181
|
-
let results = Array.from(this.conversations.values());
|
|
182
|
-
if (options?.status) {
|
|
183
|
-
results = results.filter((c) => c.status === options.status);
|
|
184
|
-
}
|
|
185
|
-
if (options?.projectId) {
|
|
186
|
-
results = results.filter((c) => c.projectId === options.projectId);
|
|
187
|
-
}
|
|
188
|
-
if (options?.tags && options.tags.length > 0) {
|
|
189
|
-
const tagSet = new Set(options.tags);
|
|
190
|
-
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
191
|
-
}
|
|
192
|
-
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
193
|
-
const offset = options?.offset ?? 0;
|
|
194
|
-
const limit = options?.limit ?? 100;
|
|
195
|
-
return results.slice(offset, offset + limit);
|
|
196
|
-
}
|
|
197
|
-
async fork(conversationId, upToMessageId) {
|
|
198
|
-
const source = this.conversations.get(conversationId);
|
|
199
|
-
if (!source) {
|
|
200
|
-
throw new Error(`Conversation ${conversationId} not found`);
|
|
201
|
-
}
|
|
202
|
-
let messagesToCopy = source.messages;
|
|
203
|
-
if (upToMessageId) {
|
|
204
|
-
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
205
|
-
if (idx === -1) {
|
|
206
|
-
throw new Error(`Message ${upToMessageId} not found`);
|
|
207
|
-
}
|
|
208
|
-
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
209
|
-
}
|
|
210
|
-
const now = new Date;
|
|
211
|
-
const forkedMessages = messagesToCopy.map((m) => ({
|
|
212
|
-
...m,
|
|
213
|
-
id: generateId("msg"),
|
|
214
|
-
conversationId: "",
|
|
215
|
-
createdAt: new Date(m.createdAt),
|
|
216
|
-
updatedAt: new Date(m.updatedAt)
|
|
217
|
-
}));
|
|
218
|
-
const forked = {
|
|
219
|
-
...source,
|
|
220
|
-
id: generateId("conv"),
|
|
221
|
-
title: source.title ? `${source.title} (fork)` : undefined,
|
|
222
|
-
forkedFromId: source.id,
|
|
223
|
-
createdAt: now,
|
|
224
|
-
updatedAt: now,
|
|
225
|
-
messages: forkedMessages
|
|
226
|
-
};
|
|
227
|
-
for (const m of forked.messages) {
|
|
228
|
-
m.conversationId = forked.id;
|
|
229
|
-
}
|
|
230
|
-
this.conversations.set(forked.id, forked);
|
|
231
|
-
return forked;
|
|
232
|
-
}
|
|
233
|
-
async truncateAfter(conversationId, messageId) {
|
|
234
|
-
const conv = this.conversations.get(conversationId);
|
|
235
|
-
if (!conv)
|
|
236
|
-
return null;
|
|
237
|
-
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
238
|
-
if (idx === -1)
|
|
239
|
-
return null;
|
|
240
|
-
conv.messages = conv.messages.slice(0, idx + 1);
|
|
241
|
-
conv.updatedAt = new Date;
|
|
242
|
-
return conv;
|
|
243
|
-
}
|
|
244
|
-
async search(query, limit = 20) {
|
|
245
|
-
const lowerQuery = query.toLowerCase();
|
|
246
|
-
const results = [];
|
|
247
|
-
for (const conversation of this.conversations.values()) {
|
|
248
|
-
if (conversation.title?.toLowerCase().includes(lowerQuery)) {
|
|
249
|
-
results.push(conversation);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const hasMatch = conversation.messages.some((m) => m.content.toLowerCase().includes(lowerQuery));
|
|
253
|
-
if (hasMatch) {
|
|
254
|
-
results.push(conversation);
|
|
255
|
-
}
|
|
256
|
-
if (results.length >= limit)
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
return results;
|
|
260
|
-
}
|
|
261
|
-
clear() {
|
|
262
|
-
this.conversations.clear();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
function createInMemoryConversationStore() {
|
|
266
|
-
return new InMemoryConversationStore;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// src/core/surface-planner-tools.ts
|
|
270
|
-
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
271
|
-
import {
|
|
272
|
-
validatePatchProposal
|
|
273
|
-
} from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
274
|
-
import { tool as tool2 } from "ai";
|
|
275
|
-
import { z as z2 } from "zod";
|
|
276
|
-
var VALID_OPS = [
|
|
277
|
-
"insert-node",
|
|
278
|
-
"replace-node",
|
|
279
|
-
"remove-node",
|
|
280
|
-
"move-node",
|
|
281
|
-
"resize-panel",
|
|
282
|
-
"set-layout",
|
|
283
|
-
"reveal-field",
|
|
284
|
-
"hide-field",
|
|
285
|
-
"promote-action",
|
|
286
|
-
"set-focus"
|
|
287
|
-
];
|
|
288
|
-
var DEFAULT_NODE_KINDS = [
|
|
289
|
-
"entity-section",
|
|
290
|
-
"entity-card",
|
|
291
|
-
"data-view",
|
|
292
|
-
"assistant-panel",
|
|
293
|
-
"chat-thread",
|
|
294
|
-
"action-bar",
|
|
295
|
-
"timeline",
|
|
296
|
-
"table",
|
|
297
|
-
"rich-doc",
|
|
298
|
-
"form",
|
|
299
|
-
"chart",
|
|
300
|
-
"custom-widget"
|
|
301
|
-
];
|
|
302
|
-
function collectSlotIdsFromRegion(node) {
|
|
303
|
-
const ids = [];
|
|
304
|
-
if (node.type === "slot") {
|
|
305
|
-
ids.push(node.slotId);
|
|
306
|
-
}
|
|
307
|
-
if (node.type === "panel-group" || node.type === "stack") {
|
|
308
|
-
for (const child of node.children) {
|
|
309
|
-
ids.push(...collectSlotIdsFromRegion(child));
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (node.type === "tabs") {
|
|
313
|
-
for (const tab of node.tabs) {
|
|
314
|
-
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (node.type === "floating") {
|
|
318
|
-
ids.push(node.anchorSlotId);
|
|
319
|
-
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
320
|
-
}
|
|
321
|
-
return ids;
|
|
322
|
-
}
|
|
323
|
-
function deriveConstraints(plan) {
|
|
324
|
-
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
325
|
-
const uniqueSlots = [...new Set(slotIds)];
|
|
326
|
-
return {
|
|
327
|
-
allowedOps: VALID_OPS,
|
|
328
|
-
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
329
|
-
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
var ProposePatchInputSchema = z2.object({
|
|
333
|
-
proposalId: z2.string().describe("Unique proposal identifier"),
|
|
334
|
-
ops: z2.array(z2.object({
|
|
335
|
-
op: z2.enum([
|
|
336
|
-
"insert-node",
|
|
337
|
-
"replace-node",
|
|
338
|
-
"remove-node",
|
|
339
|
-
"move-node",
|
|
340
|
-
"resize-panel",
|
|
341
|
-
"set-layout",
|
|
342
|
-
"reveal-field",
|
|
343
|
-
"hide-field",
|
|
344
|
-
"promote-action",
|
|
345
|
-
"set-focus"
|
|
346
|
-
]),
|
|
347
|
-
slotId: z2.string().optional(),
|
|
348
|
-
nodeId: z2.string().optional(),
|
|
349
|
-
toSlotId: z2.string().optional(),
|
|
350
|
-
index: z2.number().optional(),
|
|
351
|
-
node: z2.object({
|
|
352
|
-
nodeId: z2.string(),
|
|
353
|
-
kind: z2.string(),
|
|
354
|
-
title: z2.string().optional(),
|
|
355
|
-
props: z2.record(z2.string(), z2.unknown()).optional(),
|
|
356
|
-
children: z2.array(z2.unknown()).optional()
|
|
357
|
-
}).optional(),
|
|
358
|
-
persistKey: z2.string().optional(),
|
|
359
|
-
sizes: z2.array(z2.number()).optional(),
|
|
360
|
-
layoutId: z2.string().optional(),
|
|
361
|
-
fieldId: z2.string().optional(),
|
|
362
|
-
actionId: z2.string().optional(),
|
|
363
|
-
placement: z2.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
364
|
-
targetId: z2.string().optional()
|
|
365
|
-
}))
|
|
366
|
-
});
|
|
367
|
-
function createSurfacePlannerTools(config) {
|
|
368
|
-
const { plan, constraints, onPatchProposal } = config;
|
|
369
|
-
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
370
|
-
const proposePatchTool = tool2({
|
|
371
|
-
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
372
|
-
inputSchema: ProposePatchInputSchema,
|
|
373
|
-
execute: async (input) => {
|
|
374
|
-
const ops = input.ops;
|
|
375
|
-
try {
|
|
376
|
-
validatePatchProposal(ops, resolvedConstraints);
|
|
377
|
-
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
378
|
-
onPatchProposal?.(proposal);
|
|
379
|
-
return {
|
|
380
|
-
success: true,
|
|
381
|
-
proposalId: proposal.proposalId,
|
|
382
|
-
opsCount: proposal.ops.length,
|
|
383
|
-
message: "Patch proposal validated; awaiting user approval"
|
|
384
|
-
};
|
|
385
|
-
} catch (err) {
|
|
386
|
-
return {
|
|
387
|
-
success: false,
|
|
388
|
-
error: err instanceof Error ? err.message : String(err),
|
|
389
|
-
proposalId: input.proposalId
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
return {
|
|
395
|
-
"propose-patch": proposePatchTool
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function buildPlannerPromptInput(plan) {
|
|
399
|
-
const constraints = deriveConstraints(plan);
|
|
400
|
-
return {
|
|
401
|
-
bundleMeta: {
|
|
402
|
-
key: plan.bundleKey,
|
|
403
|
-
version: "0.0.0",
|
|
404
|
-
title: plan.bundleKey
|
|
405
|
-
},
|
|
406
|
-
surfaceId: plan.surfaceId,
|
|
407
|
-
allowedPatchOps: constraints.allowedOps,
|
|
408
|
-
allowedSlots: [...constraints.allowedSlots],
|
|
409
|
-
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
410
|
-
actions: plan.actions.map((a) => ({
|
|
411
|
-
actionId: a.actionId,
|
|
412
|
-
title: a.title
|
|
413
|
-
})),
|
|
414
|
-
preferences: {
|
|
415
|
-
guidance: "hints",
|
|
416
|
-
density: "standard",
|
|
417
|
-
dataDepth: "detailed",
|
|
418
|
-
control: "standard",
|
|
419
|
-
media: "text",
|
|
420
|
-
pace: "balanced",
|
|
421
|
-
narrative: "top-down"
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// src/core/thinking-levels.ts
|
|
427
|
-
var THINKING_LEVEL_LABELS = {
|
|
428
|
-
instant: "Instant",
|
|
429
|
-
thinking: "Thinking",
|
|
430
|
-
extra_thinking: "Extra Thinking",
|
|
431
|
-
max: "Max"
|
|
432
|
-
};
|
|
433
|
-
var THINKING_LEVEL_DESCRIPTIONS = {
|
|
434
|
-
instant: "Fast responses, minimal reasoning",
|
|
435
|
-
thinking: "Standard reasoning depth",
|
|
436
|
-
extra_thinking: "More thorough reasoning",
|
|
437
|
-
max: "Maximum reasoning depth"
|
|
438
|
-
};
|
|
439
|
-
function getProviderOptions(level, providerName) {
|
|
440
|
-
if (!level || level === "instant") {
|
|
441
|
-
return {};
|
|
442
|
-
}
|
|
443
|
-
switch (providerName) {
|
|
444
|
-
case "anthropic": {
|
|
445
|
-
const budgetMap = {
|
|
446
|
-
thinking: 8000,
|
|
447
|
-
extra_thinking: 16000,
|
|
448
|
-
max: 32000
|
|
449
|
-
};
|
|
450
|
-
return {
|
|
451
|
-
anthropic: {
|
|
452
|
-
thinking: { type: "enabled", budgetTokens: budgetMap[level] }
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
case "openai": {
|
|
457
|
-
const effortMap = {
|
|
458
|
-
thinking: "low",
|
|
459
|
-
extra_thinking: "medium",
|
|
460
|
-
max: "high"
|
|
461
|
-
};
|
|
462
|
-
return {
|
|
463
|
-
openai: {
|
|
464
|
-
reasoningEffort: effortMap[level]
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
case "ollama":
|
|
469
|
-
case "mistral":
|
|
470
|
-
case "gemini":
|
|
471
|
-
return {};
|
|
472
|
-
default:
|
|
473
|
-
return {};
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// src/core/workflow-tools.ts
|
|
478
|
-
import {
|
|
479
|
-
validateExtension,
|
|
480
|
-
WorkflowComposer
|
|
481
|
-
} from "@contractspec/lib.workflow-composer";
|
|
482
|
-
import { tool as tool3 } from "ai";
|
|
483
|
-
import { z as z3 } from "zod";
|
|
484
|
-
var StepTypeSchema = z3.enum(["human", "automation", "decision"]);
|
|
485
|
-
var StepActionSchema = z3.object({
|
|
486
|
-
operation: z3.object({
|
|
487
|
-
name: z3.string(),
|
|
488
|
-
version: z3.number()
|
|
489
|
-
}).optional(),
|
|
490
|
-
form: z3.object({
|
|
491
|
-
key: z3.string(),
|
|
492
|
-
version: z3.number()
|
|
493
|
-
}).optional()
|
|
494
|
-
}).optional();
|
|
495
|
-
var StepSchema = z3.object({
|
|
496
|
-
id: z3.string(),
|
|
497
|
-
type: StepTypeSchema,
|
|
498
|
-
label: z3.string(),
|
|
499
|
-
description: z3.string().optional(),
|
|
500
|
-
action: StepActionSchema
|
|
501
|
-
});
|
|
502
|
-
var StepInjectionSchema = z3.object({
|
|
503
|
-
after: z3.string().optional(),
|
|
504
|
-
before: z3.string().optional(),
|
|
505
|
-
inject: StepSchema,
|
|
506
|
-
transitionTo: z3.string().optional(),
|
|
507
|
-
transitionFrom: z3.string().optional(),
|
|
508
|
-
when: z3.string().optional()
|
|
509
|
-
});
|
|
510
|
-
var WorkflowExtensionInputSchema = z3.object({
|
|
511
|
-
workflow: z3.string(),
|
|
512
|
-
tenantId: z3.string().optional(),
|
|
513
|
-
role: z3.string().optional(),
|
|
514
|
-
priority: z3.number().optional(),
|
|
515
|
-
customSteps: z3.array(StepInjectionSchema).optional(),
|
|
516
|
-
hiddenSteps: z3.array(z3.string()).optional()
|
|
517
|
-
});
|
|
518
|
-
function createWorkflowTools(config) {
|
|
519
|
-
const { baseWorkflows, composer } = config;
|
|
520
|
-
const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
|
|
521
|
-
const createWorkflowExtensionTool = tool3({
|
|
522
|
-
description: "Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",
|
|
523
|
-
inputSchema: WorkflowExtensionInputSchema,
|
|
524
|
-
execute: async (input) => {
|
|
525
|
-
const extension = {
|
|
526
|
-
workflow: input.workflow,
|
|
527
|
-
tenantId: input.tenantId,
|
|
528
|
-
role: input.role,
|
|
529
|
-
priority: input.priority,
|
|
530
|
-
customSteps: input.customSteps,
|
|
531
|
-
hiddenSteps: input.hiddenSteps
|
|
532
|
-
};
|
|
533
|
-
const base = baseByKey.get(input.workflow);
|
|
534
|
-
if (!base) {
|
|
535
|
-
return {
|
|
536
|
-
success: false,
|
|
537
|
-
error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
538
|
-
extension
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
try {
|
|
542
|
-
validateExtension(extension, base);
|
|
543
|
-
return {
|
|
544
|
-
success: true,
|
|
545
|
-
message: "Extension validated successfully",
|
|
546
|
-
extension
|
|
547
|
-
};
|
|
548
|
-
} catch (err) {
|
|
549
|
-
return {
|
|
550
|
-
success: false,
|
|
551
|
-
error: err instanceof Error ? err.message : String(err),
|
|
552
|
-
extension
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
const composeWorkflowInputSchema = z3.object({
|
|
558
|
-
workflowKey: z3.string().describe("Base workflow meta.key"),
|
|
559
|
-
tenantId: z3.string().optional(),
|
|
560
|
-
role: z3.string().optional(),
|
|
561
|
-
extensions: z3.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
|
|
562
|
-
});
|
|
563
|
-
const composeWorkflowTool = tool3({
|
|
564
|
-
description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
|
|
565
|
-
inputSchema: composeWorkflowInputSchema,
|
|
566
|
-
execute: async (input) => {
|
|
567
|
-
const base = baseByKey.get(input.workflowKey);
|
|
568
|
-
if (!base) {
|
|
569
|
-
return {
|
|
570
|
-
success: false,
|
|
571
|
-
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
const comp = composer ?? new WorkflowComposer;
|
|
575
|
-
if (input.extensions?.length) {
|
|
576
|
-
for (const ext of input.extensions) {
|
|
577
|
-
comp.register({
|
|
578
|
-
workflow: ext.workflow,
|
|
579
|
-
tenantId: ext.tenantId,
|
|
580
|
-
role: ext.role,
|
|
581
|
-
priority: ext.priority,
|
|
582
|
-
customSteps: ext.customSteps,
|
|
583
|
-
hiddenSteps: ext.hiddenSteps
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
try {
|
|
588
|
-
const composed = comp.compose({
|
|
589
|
-
base,
|
|
590
|
-
tenantId: input.tenantId,
|
|
591
|
-
role: input.role
|
|
592
|
-
});
|
|
593
|
-
return {
|
|
594
|
-
success: true,
|
|
595
|
-
workflow: composed,
|
|
596
|
-
meta: composed.meta,
|
|
597
|
-
stepIds: composed.definition.steps.map((s) => s.id)
|
|
598
|
-
};
|
|
599
|
-
} catch (err) {
|
|
600
|
-
return {
|
|
601
|
-
success: false,
|
|
602
|
-
error: err instanceof Error ? err.message : String(err)
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
const generateWorkflowSpecCodeInputSchema = z3.object({
|
|
608
|
-
workflowKey: z3.string().describe("Workflow meta.key"),
|
|
609
|
-
composedSteps: z3.array(z3.object({
|
|
610
|
-
id: z3.string(),
|
|
611
|
-
type: z3.enum(["human", "automation", "decision"]),
|
|
612
|
-
label: z3.string(),
|
|
613
|
-
description: z3.string().optional()
|
|
614
|
-
})).optional().describe("Steps to include; if omitted, uses the base workflow")
|
|
615
|
-
});
|
|
616
|
-
const generateWorkflowSpecCodeTool = tool3({
|
|
617
|
-
description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
|
|
618
|
-
inputSchema: generateWorkflowSpecCodeInputSchema,
|
|
619
|
-
execute: async (input) => {
|
|
620
|
-
const base = baseByKey.get(input.workflowKey);
|
|
621
|
-
if (!base) {
|
|
622
|
-
return {
|
|
623
|
-
success: false,
|
|
624
|
-
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
625
|
-
code: null
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
const steps = input.composedSteps ?? base.definition.steps;
|
|
629
|
-
const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
|
|
630
|
-
const stepsCode = steps.map((s) => ` {
|
|
631
|
-
id: '${s.id}',
|
|
632
|
-
type: '${s.type}',
|
|
633
|
-
label: '${escapeString(s.label)}',${s.description ? `
|
|
634
|
-
description: '${escapeString(s.description)}',` : ""}
|
|
2
|
+
var PX=import.meta.require;import{useCompletion as cZ}from"@ai-sdk/react";import{createProvider as nX}from"@contractspec/lib.ai-providers";import{tool as tX}from"ai";import*as E from"react";import{z as sX}from"zod";import{generateText as pX,streamText as cX}from"ai";import{compilePlannerPrompt as iX}from"@contractspec/lib.surface-runtime/runtime/planner-prompt";import{tool as xX}from"ai";import{z as bX}from"zod";function kX(X){return bX.object({}).passthrough()}function qX(X,Z){let $={};for(let J of X){let Y=Z?.[J.name],G=kX(J.schema);$[J.name]=xX({description:J.description??J.name,inputSchema:G,execute:async(j)=>{if(!Y)return{status:"unimplemented",message:"Wire handler in host",toolName:J.name};try{let H=await Promise.resolve(Y(j));return typeof H==="string"?H:H}catch(H){return{status:"error",error:H instanceof Error?H.message:String(H),toolName:J.name}}}})}return $}function BX(X){let Z=[];if(!X.agentSpecs?.length&&!X.dataViewSpecs?.length&&!X.formSpecs?.length&&!X.presentationSpecs?.length&&!X.operationRefs?.length)return"";if(Z.push(`
|
|
3
|
+
|
|
4
|
+
## Available resources`),X.agentSpecs?.length){Z.push(`
|
|
5
|
+
### Agent tools`);for(let $ of X.agentSpecs){let J=$.tools?.map((Y)=>Y.name).join(", ")??"none";Z.push(`- **${$.key}**: tools: ${J}`)}}if(X.dataViewSpecs?.length){Z.push(`
|
|
6
|
+
### Data views`);for(let $ of X.dataViewSpecs)Z.push(`- **${$.key}**: ${$.meta.title??$.key}`)}if(X.formSpecs?.length){Z.push(`
|
|
7
|
+
### Forms`);for(let $ of X.formSpecs)Z.push(`- **${$.key}**: ${$.meta.title??$.key}`)}if(X.presentationSpecs?.length){Z.push(`
|
|
8
|
+
### Presentations`);for(let $ of X.presentationSpecs)Z.push(`- **${$.key}**: ${$.meta.title??$.key} (targets: ${$.targets?.join(", ")??"react"})`)}if(X.operationRefs?.length){Z.push(`
|
|
9
|
+
### Operations`);for(let $ of X.operationRefs)Z.push(`- **${$.key}@${$.version}**`)}return Z.push(`
|
|
10
|
+
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`),Z.join(`
|
|
11
|
+
`)}function s(X){return`${X}_${Date.now()}_${Math.random().toString(36).slice(2,11)}`}class o{conversations=new Map;async get(X){return this.conversations.get(X)??null}async create(X){let Z=new Date,$={...X,id:s("conv"),createdAt:Z,updatedAt:Z};return this.conversations.set($.id,$),$}async update(X,Z){let $=this.conversations.get(X);if(!$)return null;let J={...$,...Z,updatedAt:new Date};return this.conversations.set(X,J),J}async appendMessage(X,Z){let $=this.conversations.get(X);if(!$)throw Error(`Conversation ${X} not found`);let J=new Date,Y={...Z,id:s("msg"),conversationId:X,createdAt:J,updatedAt:J};return $.messages.push(Y),$.updatedAt=J,Y}async updateMessage(X,Z,$){let J=this.conversations.get(X);if(!J)return null;let Y=J.messages.findIndex((H)=>H.id===Z);if(Y===-1)return null;let G=J.messages[Y];if(!G)return null;let j={...G,...$,updatedAt:new Date};return J.messages[Y]=j,J.updatedAt=new Date,j}async delete(X){return this.conversations.delete(X)}async list(X){let Z=Array.from(this.conversations.values());if(X?.status)Z=Z.filter((Y)=>Y.status===X.status);if(X?.projectId)Z=Z.filter((Y)=>Y.projectId===X.projectId);if(X?.tags&&X.tags.length>0){let Y=new Set(X.tags);Z=Z.filter((G)=>G.tags&&G.tags.some((j)=>Y.has(j)))}Z.sort((Y,G)=>G.updatedAt.getTime()-Y.updatedAt.getTime());let $=X?.offset??0,J=X?.limit??100;return Z.slice($,$+J)}async fork(X,Z){let $=this.conversations.get(X);if(!$)throw Error(`Conversation ${X} not found`);let J=$.messages;if(Z){let H=$.messages.findIndex((q)=>q.id===Z);if(H===-1)throw Error(`Message ${Z} not found`);J=$.messages.slice(0,H+1)}let Y=new Date,G=J.map((H)=>({...H,id:s("msg"),conversationId:"",createdAt:new Date(H.createdAt),updatedAt:new Date(H.updatedAt)})),j={...$,id:s("conv"),title:$.title?`${$.title} (fork)`:void 0,forkedFromId:$.id,createdAt:Y,updatedAt:Y,messages:G};for(let H of j.messages)H.conversationId=j.id;return this.conversations.set(j.id,j),j}async truncateAfter(X,Z){let $=this.conversations.get(X);if(!$)return null;let J=$.messages.findIndex((Y)=>Y.id===Z);if(J===-1)return null;return $.messages=$.messages.slice(0,J+1),$.updatedAt=new Date,$}async search(X,Z=20){let $=X.toLowerCase(),J=[];for(let Y of this.conversations.values()){if(Y.title?.toLowerCase().includes($)){J.push(Y);continue}if(Y.messages.some((j)=>j.content.toLowerCase().includes($)))J.push(Y);if(J.length>=Z)break}return J}clear(){this.conversations.clear()}}function qZ(){return new o}import{buildSurfacePatchProposal as TX}from"@contractspec/lib.surface-runtime/runtime/planner-tools";import{validatePatchProposal as SX}from"@contractspec/lib.surface-runtime/spec/validate-surface-patch";import{tool as hX}from"ai";import{z as K}from"zod";var CX=["insert-node","replace-node","remove-node","move-node","resize-panel","set-layout","reveal-field","hide-field","promote-action","set-focus"],MX=["entity-section","entity-card","data-view","assistant-panel","chat-thread","action-bar","timeline","table","rich-doc","form","chart","custom-widget"];function e(X){let Z=[];if(X.type==="slot")Z.push(X.slotId);if(X.type==="panel-group"||X.type==="stack")for(let $ of X.children)Z.push(...e($));if(X.type==="tabs")for(let $ of X.tabs)Z.push(...e($.child));if(X.type==="floating")Z.push(X.anchorSlotId),Z.push(...e(X.child));return Z}function _X(X){let Z=e(X.layoutRoot),$=[...new Set(Z)];return{allowedOps:CX,allowedSlots:$.length>0?$:["assistant","primary"],allowedNodeKinds:MX}}var IX=K.object({proposalId:K.string().describe("Unique proposal identifier"),ops:K.array(K.object({op:K.enum(["insert-node","replace-node","remove-node","move-node","resize-panel","set-layout","reveal-field","hide-field","promote-action","set-focus"]),slotId:K.string().optional(),nodeId:K.string().optional(),toSlotId:K.string().optional(),index:K.number().optional(),node:K.object({nodeId:K.string(),kind:K.string(),title:K.string().optional(),props:K.record(K.string(),K.unknown()).optional(),children:K.array(K.unknown()).optional()}).optional(),persistKey:K.string().optional(),sizes:K.array(K.number()).optional(),layoutId:K.string().optional(),fieldId:K.string().optional(),actionId:K.string().optional(),placement:K.enum(["header","inline","context","assistant"]).optional(),targetId:K.string().optional()}))});function NX(X){let{plan:Z,constraints:$,onPatchProposal:J}=X,Y=$??_X(Z);return{"propose-patch":hX({description:"Propose surface patches (layout changes, node insertions, etc.) for user approval. Only use allowed ops, slots, and node kinds from the planner context.",inputSchema:IX,execute:async(j)=>{let H=j.ops;try{SX(H,Y);let q=TX(j.proposalId,H);return J?.(q),{success:!0,proposalId:q.proposalId,opsCount:q.ops.length,message:"Patch proposal validated; awaiting user approval"}}catch(q){return{success:!1,error:q instanceof Error?q.message:String(q),proposalId:j.proposalId}}}})}}function UX(X){let Z=_X(X);return{bundleMeta:{key:X.bundleKey,version:"0.0.0",title:X.bundleKey},surfaceId:X.surfaceId,allowedPatchOps:Z.allowedOps,allowedSlots:[...Z.allowedSlots],allowedNodeKinds:[...Z.allowedNodeKinds],actions:X.actions.map(($)=>({actionId:$.actionId,title:$.title})),preferences:{guidance:"hints",density:"standard",dataDepth:"detailed",control:"standard",media:"text",pace:"balanced",narrative:"top-down"}}}var FZ={instant:"Instant",thinking:"Thinking",extra_thinking:"Extra Thinking",max:"Max"},AZ={instant:"Fast responses, minimal reasoning",thinking:"Standard reasoning depth",extra_thinking:"More thorough reasoning",max:"Maximum reasoning depth"};function GX(X,Z){if(!X||X==="instant")return{};switch(Z){case"anthropic":return{anthropic:{thinking:{type:"enabled",budgetTokens:{thinking:8000,extra_thinking:16000,max:32000}[X]}}};case"openai":return{openai:{reasoningEffort:{thinking:"low",extra_thinking:"medium",max:"high"}[X]}};case"ollama":case"mistral":case"gemini":return{};default:return{}}}import{validateExtension as fX,WorkflowComposer as mX}from"@contractspec/lib.workflow-composer";import{tool as QX}from"ai";import{z as B}from"zod";var uX=B.enum(["human","automation","decision"]),vX=B.object({operation:B.object({name:B.string(),version:B.number()}).optional(),form:B.object({key:B.string(),version:B.number()}).optional()}).optional(),gX=B.object({id:B.string(),type:uX,label:B.string(),description:B.string().optional(),action:vX}),dX=B.object({after:B.string().optional(),before:B.string().optional(),inject:gX,transitionTo:B.string().optional(),transitionFrom:B.string().optional(),when:B.string().optional()}),WX=B.object({workflow:B.string(),tenantId:B.string().optional(),role:B.string().optional(),priority:B.number().optional(),customSteps:B.array(dX).optional(),hiddenSteps:B.array(B.string()).optional()});function DX(X){let{baseWorkflows:Z,composer:$}=X,J=new Map(Z.map((Q)=>[Q.meta.key,Q])),Y=QX({description:"Create or validate a workflow extension. Use when the user asks to add steps, modify a workflow, or create a tenant-specific extension. The extension targets an existing base workflow.",inputSchema:WX,execute:async(Q)=>{let _={workflow:Q.workflow,tenantId:Q.tenantId,role:Q.role,priority:Q.priority,customSteps:Q.customSteps,hiddenSteps:Q.hiddenSteps},A=J.get(Q.workflow);if(!A)return{success:!1,error:`Base workflow "${Q.workflow}" not found. Available: ${Array.from(J.keys()).join(", ")}`,extension:_};try{return fX(_,A),{success:!0,message:"Extension validated successfully",extension:_}}catch(N){return{success:!1,error:N instanceof Error?N.message:String(N),extension:_}}}}),G=B.object({workflowKey:B.string().describe("Base workflow meta.key"),tenantId:B.string().optional(),role:B.string().optional(),extensions:B.array(WX).optional().describe("Extensions to register before composing")}),j=QX({description:"Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",inputSchema:G,execute:async(Q)=>{let _=J.get(Q.workflowKey);if(!_)return{success:!1,error:`Base workflow "${Q.workflowKey}" not found. Available: ${Array.from(J.keys()).join(", ")}`};let A=$??new mX;if(Q.extensions?.length)for(let N of Q.extensions)A.register({workflow:N.workflow,tenantId:N.tenantId,role:N.role,priority:N.priority,customSteps:N.customSteps,hiddenSteps:N.hiddenSteps});try{let N=A.compose({base:_,tenantId:Q.tenantId,role:Q.role});return{success:!0,workflow:N,meta:N.meta,stepIds:N.definition.steps.map((k)=>k.id)}}catch(N){return{success:!1,error:N instanceof Error?N.message:String(N)}}}}),H=B.object({workflowKey:B.string().describe("Workflow meta.key"),composedSteps:B.array(B.object({id:B.string(),type:B.enum(["human","automation","decision"]),label:B.string(),description:B.string().optional()})).optional().describe("Steps to include; if omitted, uses the base workflow")}),q=QX({description:"Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",inputSchema:H,execute:async(Q)=>{let _=J.get(Q.workflowKey);if(!_)return{success:!1,error:`Base workflow "${Q.workflowKey}" not found. Available: ${Array.from(J.keys()).join(", ")}`,code:null};let A=Q.composedSteps??_.definition.steps,N=lX((_.meta.key.split(".").pop()??"Workflow")+"")+"Workflow",k=A.map((U)=>` {
|
|
12
|
+
id: '${U.id}',
|
|
13
|
+
type: '${U.type}',
|
|
14
|
+
label: '${XX(U.label)}',${U.description?`
|
|
15
|
+
description: '${XX(U.description)}',`:""}
|
|
635
16
|
}`).join(`,
|
|
636
|
-
`);
|
|
637
|
-
const meta = base.meta;
|
|
638
|
-
const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
|
|
639
|
-
const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow/spec';
|
|
17
|
+
`),P=_.meta,I=JSON.stringify(_.definition.transitions,null,6);return{success:!0,code:`import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow/spec';
|
|
640
18
|
|
|
641
19
|
/**
|
|
642
|
-
* Workflow: ${
|
|
20
|
+
* Workflow: ${_.meta.key}
|
|
643
21
|
* Generated via AI chat workflow tools.
|
|
644
22
|
*/
|
|
645
|
-
export const ${
|
|
23
|
+
export const ${N}: WorkflowSpec = {
|
|
646
24
|
meta: {
|
|
647
|
-
key: '${
|
|
648
|
-
version: '${String(
|
|
649
|
-
title: '${
|
|
650
|
-
description: '${
|
|
25
|
+
key: '${_.meta.key}',
|
|
26
|
+
version: '${String(_.meta.version)}',
|
|
27
|
+
title: '${XX(P.title??_.meta.key)}',
|
|
28
|
+
description: '${XX(P.description??"")}',
|
|
651
29
|
},
|
|
652
30
|
definition: {
|
|
653
|
-
entryStepId: '${
|
|
31
|
+
entryStepId: '${_.definition.entryStepId??_.definition.steps[0]?.id??""}',
|
|
654
32
|
steps: [
|
|
655
|
-
${
|
|
33
|
+
${k}
|
|
656
34
|
],
|
|
657
|
-
transitions: ${
|
|
35
|
+
transitions: ${I},
|
|
658
36
|
},
|
|
659
37
|
};
|
|
660
|
-
|
|
661
|
-
return {
|
|
662
|
-
success: true,
|
|
663
|
-
code,
|
|
664
|
-
workflowKey: input.workflowKey
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
});
|
|
668
|
-
return {
|
|
669
|
-
create_workflow_extension: createWorkflowExtensionTool,
|
|
670
|
-
compose_workflow: composeWorkflowTool,
|
|
671
|
-
generate_workflow_spec_code: generateWorkflowSpecCodeTool
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
function toPascalCase(value) {
|
|
675
|
-
return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
676
|
-
}
|
|
677
|
-
function escapeString(value) {
|
|
678
|
-
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// src/core/chat-service.ts
|
|
682
|
-
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
38
|
+
`,workflowKey:Q.workflowKey}}});return{create_workflow_extension:Y,compose_workflow:j,generate_workflow_spec_code:q}}function lX(X){return X.split(/[-_.]/).filter(Boolean).map((Z)=>Z.charAt(0).toUpperCase()+Z.slice(1)).join("")}function XX(X){return X.replace(/\\/g,"\\\\").replace(/'/g,"\\'")}var aX=`You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
683
39
|
|
|
684
40
|
Your capabilities:
|
|
685
41
|
- Help users create, modify, and understand ContractSpec specifications
|
|
@@ -692,971 +48,15 @@ Guidelines:
|
|
|
692
48
|
- Provide code examples when helpful
|
|
693
49
|
- Reference relevant ContractSpec concepts and patterns
|
|
694
50
|
- Ask clarifying questions when the user's intent is unclear
|
|
695
|
-
- When suggesting code changes, explain the rationale
|
|
696
|
-
var WORKFLOW_TOOLS_PROMPT = `
|
|
697
|
-
|
|
698
|
-
Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;
|
|
51
|
+
- When suggesting code changes, explain the rationale`,rX=`
|
|
699
52
|
|
|
700
|
-
class
|
|
701
|
-
provider;
|
|
702
|
-
context;
|
|
703
|
-
store;
|
|
704
|
-
systemPrompt;
|
|
705
|
-
maxHistoryMessages;
|
|
706
|
-
onUsage;
|
|
707
|
-
tools;
|
|
708
|
-
thinkingLevel;
|
|
709
|
-
sendReasoning;
|
|
710
|
-
sendSources;
|
|
711
|
-
modelSelector;
|
|
712
|
-
constructor(config) {
|
|
713
|
-
this.provider = config.provider;
|
|
714
|
-
this.context = config.context;
|
|
715
|
-
this.store = config.store ?? new InMemoryConversationStore;
|
|
716
|
-
this.systemPrompt = this.buildSystemPrompt(config);
|
|
717
|
-
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
718
|
-
this.onUsage = config.onUsage;
|
|
719
|
-
this.tools = this.mergeTools(config);
|
|
720
|
-
this.thinkingLevel = config.thinkingLevel;
|
|
721
|
-
this.modelSelector = config.modelSelector;
|
|
722
|
-
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
723
|
-
this.sendSources = config.sendSources ?? false;
|
|
724
|
-
}
|
|
725
|
-
buildSystemPrompt(config) {
|
|
726
|
-
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
727
|
-
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
728
|
-
base += WORKFLOW_TOOLS_PROMPT;
|
|
729
|
-
}
|
|
730
|
-
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
731
|
-
if (contractsPrompt) {
|
|
732
|
-
base += contractsPrompt;
|
|
733
|
-
}
|
|
734
|
-
if (config.surfacePlanConfig?.plan) {
|
|
735
|
-
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
736
|
-
base += `
|
|
53
|
+
Workflow creation: You can create and modify workflows. Use create_workflow_extension when the user asks to add steps, change a workflow, or create a tenant-specific extension. Use compose_workflow to apply extensions to a base workflow. Use generate_workflow_spec_code to output TypeScript for the user to save.`;class ZX{provider;context;store;systemPrompt;maxHistoryMessages;onUsage;tools;thinkingLevel;sendReasoning;sendSources;modelSelector;constructor(X){this.provider=X.provider,this.context=X.context,this.store=X.store??new o,this.systemPrompt=this.buildSystemPrompt(X),this.maxHistoryMessages=X.maxHistoryMessages??20,this.onUsage=X.onUsage,this.tools=this.mergeTools(X),this.thinkingLevel=X.thinkingLevel,this.modelSelector=X.modelSelector,this.sendReasoning=X.sendReasoning??(X.thinkingLevel!=null&&X.thinkingLevel!=="instant"),this.sendSources=X.sendSources??!1}buildSystemPrompt(X){let Z=X.systemPrompt??aX;if(X.workflowToolsConfig?.baseWorkflows?.length)Z+=rX;let $=BX(X.contractsContext??{});if($)Z+=$;if(X.surfacePlanConfig?.plan){let J=UX(X.surfacePlanConfig.plan);Z+=`
|
|
737
54
|
|
|
738
|
-
`
|
|
739
|
-
}
|
|
740
|
-
return base;
|
|
741
|
-
}
|
|
742
|
-
mergeTools(config) {
|
|
743
|
-
let merged = config.tools ?? {};
|
|
744
|
-
const wfConfig = config.workflowToolsConfig;
|
|
745
|
-
if (wfConfig?.baseWorkflows?.length) {
|
|
746
|
-
const workflowTools = createWorkflowTools({
|
|
747
|
-
baseWorkflows: wfConfig.baseWorkflows,
|
|
748
|
-
composer: wfConfig.composer
|
|
749
|
-
});
|
|
750
|
-
merged = { ...merged, ...workflowTools };
|
|
751
|
-
}
|
|
752
|
-
const contractsCtx = config.contractsContext;
|
|
753
|
-
if (contractsCtx?.agentSpecs?.length) {
|
|
754
|
-
const allTools = [];
|
|
755
|
-
for (const agent of contractsCtx.agentSpecs) {
|
|
756
|
-
if (agent.tools?.length)
|
|
757
|
-
allTools.push(...agent.tools);
|
|
758
|
-
}
|
|
759
|
-
if (allTools.length > 0) {
|
|
760
|
-
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
761
|
-
merged = { ...merged, ...agentTools };
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
const surfaceConfig = config.surfacePlanConfig;
|
|
765
|
-
if (surfaceConfig?.plan) {
|
|
766
|
-
const plannerTools = createSurfacePlannerTools({
|
|
767
|
-
plan: surfaceConfig.plan,
|
|
768
|
-
onPatchProposal: surfaceConfig.onPatchProposal
|
|
769
|
-
});
|
|
770
|
-
merged = { ...merged, ...plannerTools };
|
|
771
|
-
}
|
|
772
|
-
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
773
|
-
merged = { ...merged, ...config.mcpTools };
|
|
774
|
-
}
|
|
775
|
-
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
776
|
-
}
|
|
777
|
-
async resolveModel() {
|
|
778
|
-
if (this.modelSelector) {
|
|
779
|
-
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
780
|
-
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
781
|
-
taskDimension: dimension
|
|
782
|
-
});
|
|
783
|
-
return { model, providerName: selection.providerKey };
|
|
784
|
-
}
|
|
785
|
-
return {
|
|
786
|
-
model: this.provider.getModel(),
|
|
787
|
-
providerName: this.provider.name
|
|
788
|
-
};
|
|
789
|
-
}
|
|
790
|
-
thinkingLevelToDimension(level) {
|
|
791
|
-
if (!level || level === "instant")
|
|
792
|
-
return "latency";
|
|
793
|
-
return "reasoning";
|
|
794
|
-
}
|
|
795
|
-
async send(options) {
|
|
796
|
-
let conversation;
|
|
797
|
-
if (options.conversationId) {
|
|
798
|
-
const existing = await this.store.get(options.conversationId);
|
|
799
|
-
if (!existing) {
|
|
800
|
-
throw new Error(`Conversation ${options.conversationId} not found`);
|
|
801
|
-
}
|
|
802
|
-
conversation = existing;
|
|
803
|
-
} else {
|
|
804
|
-
conversation = await this.store.create({
|
|
805
|
-
status: "active",
|
|
806
|
-
provider: this.provider.name,
|
|
807
|
-
model: this.provider.model,
|
|
808
|
-
messages: [],
|
|
809
|
-
workspacePath: this.context?.workspacePath
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
if (!options.skipUserAppend) {
|
|
813
|
-
await this.store.appendMessage(conversation.id, {
|
|
814
|
-
role: "user",
|
|
815
|
-
content: options.content,
|
|
816
|
-
status: "completed",
|
|
817
|
-
attachments: options.attachments
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
821
|
-
const messages = this.buildMessages(conversation, options);
|
|
822
|
-
const { model, providerName } = await this.resolveModel();
|
|
823
|
-
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
824
|
-
try {
|
|
825
|
-
const result = await generateText({
|
|
826
|
-
model,
|
|
827
|
-
messages,
|
|
828
|
-
system: this.systemPrompt,
|
|
829
|
-
tools: this.tools,
|
|
830
|
-
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
831
|
-
});
|
|
832
|
-
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
833
|
-
role: "assistant",
|
|
834
|
-
content: result.text,
|
|
835
|
-
status: "completed"
|
|
836
|
-
});
|
|
837
|
-
const updatedConversation = await this.store.get(conversation.id);
|
|
838
|
-
if (!updatedConversation) {
|
|
839
|
-
throw new Error("Conversation lost after update");
|
|
840
|
-
}
|
|
841
|
-
return {
|
|
842
|
-
message: assistantMessage,
|
|
843
|
-
conversation: updatedConversation
|
|
844
|
-
};
|
|
845
|
-
} catch (error) {
|
|
846
|
-
await this.store.appendMessage(conversation.id, {
|
|
847
|
-
role: "assistant",
|
|
848
|
-
content: "",
|
|
849
|
-
status: "error",
|
|
850
|
-
error: {
|
|
851
|
-
code: "generation_failed",
|
|
852
|
-
message: error instanceof Error ? error.message : String(error)
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
throw error;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
async stream(options) {
|
|
859
|
-
let conversation;
|
|
860
|
-
if (options.conversationId) {
|
|
861
|
-
const existing = await this.store.get(options.conversationId);
|
|
862
|
-
if (!existing) {
|
|
863
|
-
throw new Error(`Conversation ${options.conversationId} not found`);
|
|
864
|
-
}
|
|
865
|
-
conversation = existing;
|
|
866
|
-
} else {
|
|
867
|
-
conversation = await this.store.create({
|
|
868
|
-
status: "active",
|
|
869
|
-
provider: this.provider.name,
|
|
870
|
-
model: this.provider.model,
|
|
871
|
-
messages: [],
|
|
872
|
-
workspacePath: this.context?.workspacePath
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
if (!options.skipUserAppend) {
|
|
876
|
-
await this.store.appendMessage(conversation.id, {
|
|
877
|
-
role: "user",
|
|
878
|
-
content: options.content,
|
|
879
|
-
status: "completed",
|
|
880
|
-
attachments: options.attachments
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
884
|
-
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
885
|
-
role: "assistant",
|
|
886
|
-
content: "",
|
|
887
|
-
status: "streaming"
|
|
888
|
-
});
|
|
889
|
-
const messages = this.buildMessages(conversation, options);
|
|
890
|
-
const { model, providerName } = await this.resolveModel();
|
|
891
|
-
const systemPrompt = this.systemPrompt;
|
|
892
|
-
const tools = this.tools;
|
|
893
|
-
const store = this.store;
|
|
894
|
-
const onUsage = this.onUsage;
|
|
895
|
-
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
896
|
-
async function* streamGenerator() {
|
|
897
|
-
let fullContent = "";
|
|
898
|
-
let fullReasoning = "";
|
|
899
|
-
const toolCallsMap = new Map;
|
|
900
|
-
const sources = [];
|
|
901
|
-
try {
|
|
902
|
-
const result = streamText({
|
|
903
|
-
model,
|
|
904
|
-
messages,
|
|
905
|
-
system: systemPrompt,
|
|
906
|
-
tools,
|
|
907
|
-
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
908
|
-
});
|
|
909
|
-
for await (const part of result.fullStream) {
|
|
910
|
-
if (part.type === "text-delta") {
|
|
911
|
-
const text = part.text ?? "";
|
|
912
|
-
if (text) {
|
|
913
|
-
fullContent += text;
|
|
914
|
-
yield { type: "text", content: text };
|
|
915
|
-
}
|
|
916
|
-
} else if (part.type === "reasoning-delta") {
|
|
917
|
-
const text = part.text ?? "";
|
|
918
|
-
if (text) {
|
|
919
|
-
fullReasoning += text;
|
|
920
|
-
yield { type: "reasoning", content: text };
|
|
921
|
-
}
|
|
922
|
-
} else if (part.type === "source") {
|
|
923
|
-
const src = part;
|
|
924
|
-
const source = {
|
|
925
|
-
id: src.id,
|
|
926
|
-
title: src.title ?? "",
|
|
927
|
-
url: src.url,
|
|
928
|
-
type: "web"
|
|
929
|
-
};
|
|
930
|
-
sources.push(source);
|
|
931
|
-
yield { type: "source", source };
|
|
932
|
-
} else if (part.type === "tool-call") {
|
|
933
|
-
const toolCall = {
|
|
934
|
-
id: part.toolCallId,
|
|
935
|
-
name: part.toolName,
|
|
936
|
-
args: part.input ?? {},
|
|
937
|
-
status: "running"
|
|
938
|
-
};
|
|
939
|
-
toolCallsMap.set(part.toolCallId, toolCall);
|
|
940
|
-
yield { type: "tool_call", toolCall };
|
|
941
|
-
} else if (part.type === "tool-result") {
|
|
942
|
-
const tc = toolCallsMap.get(part.toolCallId);
|
|
943
|
-
if (tc) {
|
|
944
|
-
tc.result = part.output;
|
|
945
|
-
tc.status = "completed";
|
|
946
|
-
}
|
|
947
|
-
yield {
|
|
948
|
-
type: "tool_result",
|
|
949
|
-
toolResult: {
|
|
950
|
-
toolCallId: part.toolCallId,
|
|
951
|
-
toolName: part.toolName,
|
|
952
|
-
result: part.output
|
|
953
|
-
}
|
|
954
|
-
};
|
|
955
|
-
} else if (part.type === "tool-error") {
|
|
956
|
-
const tc = toolCallsMap.get(part.toolCallId);
|
|
957
|
-
if (tc) {
|
|
958
|
-
tc.status = "error";
|
|
959
|
-
tc.error = part.error ?? "Tool execution failed";
|
|
960
|
-
}
|
|
961
|
-
} else if (part.type === "finish") {
|
|
962
|
-
const usage = part.usage;
|
|
963
|
-
const inputTokens = usage?.inputTokens ?? 0;
|
|
964
|
-
const outputTokens = usage?.completionTokens ?? 0;
|
|
965
|
-
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
966
|
-
content: fullContent,
|
|
967
|
-
status: "completed",
|
|
968
|
-
reasoning: fullReasoning || undefined,
|
|
969
|
-
sources: sources.length > 0 ? sources : undefined,
|
|
970
|
-
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined,
|
|
971
|
-
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
972
|
-
});
|
|
973
|
-
onUsage?.({ inputTokens, outputTokens });
|
|
974
|
-
yield {
|
|
975
|
-
type: "done",
|
|
976
|
-
usage: usage ? { inputTokens, outputTokens } : undefined
|
|
977
|
-
};
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
982
|
-
content: fullContent,
|
|
983
|
-
status: "completed",
|
|
984
|
-
reasoning: fullReasoning || undefined,
|
|
985
|
-
sources: sources.length > 0 ? sources : undefined,
|
|
986
|
-
toolCalls: toolCallsMap.size > 0 ? Array.from(toolCallsMap.values()) : undefined
|
|
987
|
-
});
|
|
988
|
-
yield { type: "done" };
|
|
989
|
-
} catch (error) {
|
|
990
|
-
await store.updateMessage(conversation.id, assistantMessage.id, {
|
|
991
|
-
content: fullContent,
|
|
992
|
-
status: "error",
|
|
993
|
-
error: {
|
|
994
|
-
code: "stream_failed",
|
|
995
|
-
message: error instanceof Error ? error.message : String(error)
|
|
996
|
-
}
|
|
997
|
-
});
|
|
998
|
-
yield {
|
|
999
|
-
type: "error",
|
|
1000
|
-
error: {
|
|
1001
|
-
code: "stream_failed",
|
|
1002
|
-
message: error instanceof Error ? error.message : String(error)
|
|
1003
|
-
}
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
return {
|
|
1008
|
-
conversationId: conversation.id,
|
|
1009
|
-
messageId: assistantMessage.id,
|
|
1010
|
-
stream: streamGenerator()
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
async getConversation(conversationId) {
|
|
1014
|
-
return this.store.get(conversationId);
|
|
1015
|
-
}
|
|
1016
|
-
async listConversations(options) {
|
|
1017
|
-
return this.store.list({
|
|
1018
|
-
status: "active",
|
|
1019
|
-
...options
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
async updateConversation(conversationId, updates) {
|
|
1023
|
-
return this.store.update(conversationId, updates);
|
|
1024
|
-
}
|
|
1025
|
-
async forkConversation(conversationId, upToMessageId) {
|
|
1026
|
-
return this.store.fork(conversationId, upToMessageId);
|
|
1027
|
-
}
|
|
1028
|
-
async updateMessage(conversationId, messageId, updates) {
|
|
1029
|
-
return this.store.updateMessage(conversationId, messageId, updates);
|
|
1030
|
-
}
|
|
1031
|
-
async truncateAfter(conversationId, messageId) {
|
|
1032
|
-
return this.store.truncateAfter(conversationId, messageId);
|
|
1033
|
-
}
|
|
1034
|
-
async deleteConversation(conversationId) {
|
|
1035
|
-
return this.store.delete(conversationId);
|
|
1036
|
-
}
|
|
1037
|
-
buildMessages(conversation, _options) {
|
|
1038
|
-
const historyStart = Math.max(0, conversation.messages.length - this.maxHistoryMessages);
|
|
1039
|
-
const messages = [];
|
|
1040
|
-
for (let i = historyStart;i < conversation.messages.length; i++) {
|
|
1041
|
-
const msg = conversation.messages[i];
|
|
1042
|
-
if (!msg)
|
|
1043
|
-
continue;
|
|
1044
|
-
if (msg.role === "user") {
|
|
1045
|
-
let content = msg.content;
|
|
1046
|
-
if (msg.attachments?.length) {
|
|
1047
|
-
const attachmentInfo = msg.attachments.map((a) => {
|
|
1048
|
-
if (a.type === "file" || a.type === "code") {
|
|
1049
|
-
return `
|
|
55
|
+
`+iX(J)}return Z}mergeTools(X){let Z=X.tools??{},$=X.workflowToolsConfig;if($?.baseWorkflows?.length){let G=DX({baseWorkflows:$.baseWorkflows,composer:$.composer});Z={...Z,...G}}let J=X.contractsContext;if(J?.agentSpecs?.length){let G=[];for(let j of J.agentSpecs)if(j.tools?.length)G.push(...j.tools);if(G.length>0){let j=qX(G);Z={...Z,...j}}}let Y=X.surfacePlanConfig;if(Y?.plan){let G=NX({plan:Y.plan,onPatchProposal:Y.onPatchProposal});Z={...Z,...G}}if(X.mcpTools&&Object.keys(X.mcpTools).length>0)Z={...Z,...X.mcpTools};return Object.keys(Z).length>0?Z:void 0}async resolveModel(){if(this.modelSelector){let X=this.thinkingLevelToDimension(this.thinkingLevel),{model:Z,selection:$}=await this.modelSelector.selectAndCreate({taskDimension:X});return{model:Z,providerName:$.providerKey}}return{model:this.provider.getModel(),providerName:this.provider.name}}thinkingLevelToDimension(X){if(!X||X==="instant")return"latency";return"reasoning"}async send(X){let Z;if(X.conversationId){let j=await this.store.get(X.conversationId);if(!j)throw Error(`Conversation ${X.conversationId} not found`);Z=j}else Z=await this.store.create({status:"active",provider:this.provider.name,model:this.provider.model,messages:[],workspacePath:this.context?.workspacePath});if(!X.skipUserAppend)await this.store.appendMessage(Z.id,{role:"user",content:X.content,status:"completed",attachments:X.attachments});Z=await this.store.get(Z.id)??Z;let $=this.buildMessages(Z,X),{model:J,providerName:Y}=await this.resolveModel(),G=GX(this.thinkingLevel,Y);try{let j=await pX({model:J,messages:$,system:this.systemPrompt,tools:this.tools,providerOptions:Object.keys(G).length>0?G:void 0}),H=await this.store.appendMessage(Z.id,{role:"assistant",content:j.text,status:"completed"}),q=await this.store.get(Z.id);if(!q)throw Error("Conversation lost after update");return{message:H,conversation:q}}catch(j){throw await this.store.appendMessage(Z.id,{role:"assistant",content:"",status:"error",error:{code:"generation_failed",message:j instanceof Error?j.message:String(j)}}),j}}async stream(X){let Z;if(X.conversationId){let N=await this.store.get(X.conversationId);if(!N)throw Error(`Conversation ${X.conversationId} not found`);Z=N}else Z=await this.store.create({status:"active",provider:this.provider.name,model:this.provider.model,messages:[],workspacePath:this.context?.workspacePath});if(!X.skipUserAppend)await this.store.appendMessage(Z.id,{role:"user",content:X.content,status:"completed",attachments:X.attachments});Z=await this.store.get(Z.id)??Z;let $=await this.store.appendMessage(Z.id,{role:"assistant",content:"",status:"streaming"}),J=this.buildMessages(Z,X),{model:Y,providerName:G}=await this.resolveModel(),j=this.systemPrompt,H=this.tools,q=this.store,Q=this.onUsage,_=GX(this.thinkingLevel,G);async function*A(){let N="",k="",P=new Map,I=[];try{let h=cX({model:Y,messages:J,system:j,tools:H,providerOptions:Object.keys(_).length>0?_:void 0});for await(let U of h.fullStream)if(U.type==="text-delta"){let W=U.text??"";if(W)N+=W,yield{type:"text",content:W}}else if(U.type==="reasoning-delta"){let W=U.text??"";if(W)k+=W,yield{type:"reasoning",content:W}}else if(U.type==="source"){let W=U,f={id:W.id,title:W.title??"",url:W.url,type:"web"};I.push(f),yield{type:"source",source:f}}else if(U.type==="tool-call"){let W={id:U.toolCallId,name:U.toolName,args:U.input??{},status:"running"};P.set(U.toolCallId,W),yield{type:"tool_call",toolCall:W}}else if(U.type==="tool-result"){let W=P.get(U.toolCallId);if(W)W.result=U.output,W.status="completed";yield{type:"tool_result",toolResult:{toolCallId:U.toolCallId,toolName:U.toolName,result:U.output}}}else if(U.type==="tool-error"){let W=P.get(U.toolCallId);if(W)W.status="error",W.error=U.error??"Tool execution failed"}else if(U.type==="finish"){let W=U.usage,f=W?.inputTokens??0,g=W?.completionTokens??0;await q.updateMessage(Z.id,$.id,{content:N,status:"completed",reasoning:k||void 0,sources:I.length>0?I:void 0,toolCalls:P.size>0?Array.from(P.values()):void 0,usage:W?{inputTokens:f,outputTokens:g}:void 0}),Q?.({inputTokens:f,outputTokens:g}),yield{type:"done",usage:W?{inputTokens:f,outputTokens:g}:void 0};return}await q.updateMessage(Z.id,$.id,{content:N,status:"completed",reasoning:k||void 0,sources:I.length>0?I:void 0,toolCalls:P.size>0?Array.from(P.values()):void 0}),yield{type:"done"}}catch(h){await q.updateMessage(Z.id,$.id,{content:N,status:"error",error:{code:"stream_failed",message:h instanceof Error?h.message:String(h)}}),yield{type:"error",error:{code:"stream_failed",message:h instanceof Error?h.message:String(h)}}}}return{conversationId:Z.id,messageId:$.id,stream:A()}}async getConversation(X){return this.store.get(X)}async listConversations(X){return this.store.list({status:"active",...X})}async updateConversation(X,Z){return this.store.update(X,Z)}async forkConversation(X,Z){return this.store.fork(X,Z)}async updateMessage(X,Z,$){return this.store.updateMessage(X,Z,$)}async truncateAfter(X,Z){return this.store.truncateAfter(X,Z)}async deleteConversation(X){return this.store.delete(X)}buildMessages(X,Z){let $=Math.max(0,X.messages.length-this.maxHistoryMessages),J=[];for(let Y=$;Y<X.messages.length;Y++){let G=X.messages[Y];if(!G)continue;if(G.role==="user"){let j=G.content;if(G.attachments?.length){let H=G.attachments.map((q)=>{if(q.type==="file"||q.type==="code")return`
|
|
1050
56
|
|
|
1051
|
-
### ${
|
|
57
|
+
### ${q.name}
|
|
1052
58
|
\`\`\`
|
|
1053
|
-
${
|
|
1054
|
-
\`\`\``;
|
|
1055
|
-
}
|
|
1056
|
-
return `
|
|
59
|
+
${q.content??""}
|
|
60
|
+
\`\`\``;return`
|
|
1057
61
|
|
|
1058
|
-
[Attachment: ${a.name}]
|
|
1059
|
-
}).join("");
|
|
1060
|
-
content += attachmentInfo;
|
|
1061
|
-
}
|
|
1062
|
-
messages.push({ role: "user", content });
|
|
1063
|
-
} else if (msg.role === "assistant") {
|
|
1064
|
-
if (msg.toolCalls?.length) {
|
|
1065
|
-
messages.push({
|
|
1066
|
-
role: "assistant",
|
|
1067
|
-
content: msg.content || "",
|
|
1068
|
-
toolCalls: msg.toolCalls.map((tc) => ({
|
|
1069
|
-
type: "tool-call",
|
|
1070
|
-
toolCallId: tc.id,
|
|
1071
|
-
toolName: tc.name,
|
|
1072
|
-
args: tc.args
|
|
1073
|
-
}))
|
|
1074
|
-
});
|
|
1075
|
-
messages.push({
|
|
1076
|
-
role: "tool",
|
|
1077
|
-
content: msg.toolCalls.map((tc) => ({
|
|
1078
|
-
type: "tool-result",
|
|
1079
|
-
toolCallId: tc.id,
|
|
1080
|
-
toolName: tc.name,
|
|
1081
|
-
output: tc.result
|
|
1082
|
-
}))
|
|
1083
|
-
});
|
|
1084
|
-
} else {
|
|
1085
|
-
messages.push({ role: "assistant", content: msg.content });
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return messages;
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
function createChatService(config) {
|
|
1093
|
-
return new ChatService(config);
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// src/presentation/hooks/useChat.tsx
|
|
1097
|
-
"use client";
|
|
1098
|
-
function toolsToToolSet(defs) {
|
|
1099
|
-
const result = {};
|
|
1100
|
-
for (const def of defs) {
|
|
1101
|
-
result[def.name] = tool4({
|
|
1102
|
-
description: def.description ?? def.name,
|
|
1103
|
-
inputSchema: z4.object({}).passthrough(),
|
|
1104
|
-
execute: async () => ({})
|
|
1105
|
-
});
|
|
1106
|
-
}
|
|
1107
|
-
return result;
|
|
1108
|
-
}
|
|
1109
|
-
function useChat(options = {}) {
|
|
1110
|
-
const {
|
|
1111
|
-
provider = "openai",
|
|
1112
|
-
mode = "byok",
|
|
1113
|
-
model,
|
|
1114
|
-
apiKey,
|
|
1115
|
-
proxyUrl,
|
|
1116
|
-
conversationId: initialConversationId,
|
|
1117
|
-
store,
|
|
1118
|
-
systemPrompt,
|
|
1119
|
-
streaming = true,
|
|
1120
|
-
onSend,
|
|
1121
|
-
onResponse,
|
|
1122
|
-
onError,
|
|
1123
|
-
onUsage,
|
|
1124
|
-
tools: toolsDefs,
|
|
1125
|
-
thinkingLevel,
|
|
1126
|
-
workflowToolsConfig,
|
|
1127
|
-
modelSelector,
|
|
1128
|
-
contractsContext,
|
|
1129
|
-
surfacePlanConfig,
|
|
1130
|
-
mcpServers,
|
|
1131
|
-
agentMode
|
|
1132
|
-
} = options;
|
|
1133
|
-
const [messages, setMessages] = React.useState([]);
|
|
1134
|
-
const [mcpTools, setMcpTools] = React.useState(null);
|
|
1135
|
-
const mcpCleanupRef = React.useRef(null);
|
|
1136
|
-
const [conversation, setConversation] = React.useState(null);
|
|
1137
|
-
const [isLoading, setIsLoading] = React.useState(false);
|
|
1138
|
-
const [error, setError] = React.useState(null);
|
|
1139
|
-
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
1140
|
-
const abortControllerRef = React.useRef(null);
|
|
1141
|
-
const chatServiceRef = React.useRef(null);
|
|
1142
|
-
React.useEffect(() => {
|
|
1143
|
-
if (!mcpServers?.length) {
|
|
1144
|
-
setMcpTools(null);
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
let cancelled = false;
|
|
1148
|
-
import("@contractspec/lib.ai-agent/tools/mcp-client.browser").then(({ createMcpToolsets }) => {
|
|
1149
|
-
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
1150
|
-
if (!cancelled) {
|
|
1151
|
-
setMcpTools(tools);
|
|
1152
|
-
mcpCleanupRef.current = cleanup;
|
|
1153
|
-
} else {
|
|
1154
|
-
cleanup().catch(() => {
|
|
1155
|
-
return;
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1158
|
-
}).catch(() => {
|
|
1159
|
-
if (!cancelled)
|
|
1160
|
-
setMcpTools(null);
|
|
1161
|
-
});
|
|
1162
|
-
});
|
|
1163
|
-
return () => {
|
|
1164
|
-
cancelled = true;
|
|
1165
|
-
const cleanup = mcpCleanupRef.current;
|
|
1166
|
-
mcpCleanupRef.current = null;
|
|
1167
|
-
if (cleanup)
|
|
1168
|
-
cleanup().catch(() => {
|
|
1169
|
-
return;
|
|
1170
|
-
});
|
|
1171
|
-
setMcpTools(null);
|
|
1172
|
-
};
|
|
1173
|
-
}, [mcpServers]);
|
|
1174
|
-
React.useEffect(() => {
|
|
1175
|
-
const chatProvider = createProvider({
|
|
1176
|
-
provider,
|
|
1177
|
-
model,
|
|
1178
|
-
apiKey,
|
|
1179
|
-
proxyUrl
|
|
1180
|
-
});
|
|
1181
|
-
chatServiceRef.current = new ChatService({
|
|
1182
|
-
provider: chatProvider,
|
|
1183
|
-
store,
|
|
1184
|
-
systemPrompt,
|
|
1185
|
-
onUsage,
|
|
1186
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
1187
|
-
thinkingLevel,
|
|
1188
|
-
workflowToolsConfig,
|
|
1189
|
-
modelSelector,
|
|
1190
|
-
contractsContext,
|
|
1191
|
-
surfacePlanConfig,
|
|
1192
|
-
mcpTools
|
|
1193
|
-
});
|
|
1194
|
-
}, [
|
|
1195
|
-
provider,
|
|
1196
|
-
mode,
|
|
1197
|
-
model,
|
|
1198
|
-
apiKey,
|
|
1199
|
-
proxyUrl,
|
|
1200
|
-
store,
|
|
1201
|
-
systemPrompt,
|
|
1202
|
-
onUsage,
|
|
1203
|
-
toolsDefs,
|
|
1204
|
-
thinkingLevel,
|
|
1205
|
-
workflowToolsConfig,
|
|
1206
|
-
modelSelector,
|
|
1207
|
-
contractsContext,
|
|
1208
|
-
surfacePlanConfig,
|
|
1209
|
-
mcpTools
|
|
1210
|
-
]);
|
|
1211
|
-
React.useEffect(() => {
|
|
1212
|
-
if (!conversationId || !chatServiceRef.current)
|
|
1213
|
-
return;
|
|
1214
|
-
const loadConversation = async () => {
|
|
1215
|
-
if (!chatServiceRef.current)
|
|
1216
|
-
return;
|
|
1217
|
-
const conv = await chatServiceRef.current.getConversation(conversationId);
|
|
1218
|
-
if (conv) {
|
|
1219
|
-
setConversation(conv);
|
|
1220
|
-
setMessages(conv.messages);
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
|
-
loadConversation().catch(console.error);
|
|
1224
|
-
}, [conversationId]);
|
|
1225
|
-
const sendMessage = React.useCallback(async (content, attachments, opts) => {
|
|
1226
|
-
if (agentMode?.agent) {
|
|
1227
|
-
setIsLoading(true);
|
|
1228
|
-
setError(null);
|
|
1229
|
-
abortControllerRef.current = new AbortController;
|
|
1230
|
-
try {
|
|
1231
|
-
if (!opts?.skipUserAppend) {
|
|
1232
|
-
const userMessage = {
|
|
1233
|
-
id: `msg_${Date.now()}`,
|
|
1234
|
-
conversationId: conversationId ?? "",
|
|
1235
|
-
role: "user",
|
|
1236
|
-
content,
|
|
1237
|
-
status: "completed",
|
|
1238
|
-
createdAt: new Date,
|
|
1239
|
-
updatedAt: new Date,
|
|
1240
|
-
attachments
|
|
1241
|
-
};
|
|
1242
|
-
setMessages((prev) => [...prev, userMessage]);
|
|
1243
|
-
onSend?.(userMessage);
|
|
1244
|
-
}
|
|
1245
|
-
const result = await agentMode.agent.generate({
|
|
1246
|
-
prompt: content,
|
|
1247
|
-
signal: abortControllerRef.current.signal
|
|
1248
|
-
});
|
|
1249
|
-
const toolCallsMap = new Map;
|
|
1250
|
-
for (const tc of result.toolCalls ?? []) {
|
|
1251
|
-
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
1252
|
-
toolCallsMap.set(tc.toolCallId, {
|
|
1253
|
-
id: tc.toolCallId,
|
|
1254
|
-
name: tc.toolName,
|
|
1255
|
-
args: tc.args ?? {},
|
|
1256
|
-
result: tr?.output,
|
|
1257
|
-
status: "completed"
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
const assistantMessage = {
|
|
1261
|
-
id: `msg_${Date.now()}_a`,
|
|
1262
|
-
conversationId: conversationId ?? "",
|
|
1263
|
-
role: "assistant",
|
|
1264
|
-
content: result.text,
|
|
1265
|
-
status: "completed",
|
|
1266
|
-
createdAt: new Date,
|
|
1267
|
-
updatedAt: new Date,
|
|
1268
|
-
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1269
|
-
usage: result.usage
|
|
1270
|
-
};
|
|
1271
|
-
setMessages((prev) => [...prev, assistantMessage]);
|
|
1272
|
-
onResponse?.(assistantMessage);
|
|
1273
|
-
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
1274
|
-
if (store && !conversationId) {
|
|
1275
|
-
const conv = await store.create({
|
|
1276
|
-
status: "active",
|
|
1277
|
-
provider: "agent",
|
|
1278
|
-
model: "agent",
|
|
1279
|
-
messages: []
|
|
1280
|
-
});
|
|
1281
|
-
if (!opts?.skipUserAppend) {
|
|
1282
|
-
await store.appendMessage(conv.id, {
|
|
1283
|
-
role: "user",
|
|
1284
|
-
content,
|
|
1285
|
-
status: "completed",
|
|
1286
|
-
attachments
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
await store.appendMessage(conv.id, {
|
|
1290
|
-
role: "assistant",
|
|
1291
|
-
content: result.text,
|
|
1292
|
-
status: "completed",
|
|
1293
|
-
toolCalls: assistantMessage.toolCalls,
|
|
1294
|
-
usage: result.usage
|
|
1295
|
-
});
|
|
1296
|
-
const updated = await store.get(conv.id);
|
|
1297
|
-
if (updated)
|
|
1298
|
-
setConversation(updated);
|
|
1299
|
-
setConversationId(conv.id);
|
|
1300
|
-
}
|
|
1301
|
-
} catch (err) {
|
|
1302
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1303
|
-
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1304
|
-
} finally {
|
|
1305
|
-
setIsLoading(false);
|
|
1306
|
-
}
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
if (!chatServiceRef.current) {
|
|
1310
|
-
throw new Error("Chat service not initialized");
|
|
1311
|
-
}
|
|
1312
|
-
setIsLoading(true);
|
|
1313
|
-
setError(null);
|
|
1314
|
-
abortControllerRef.current = new AbortController;
|
|
1315
|
-
try {
|
|
1316
|
-
if (!opts?.skipUserAppend) {
|
|
1317
|
-
const userMessage = {
|
|
1318
|
-
id: `msg_${Date.now()}`,
|
|
1319
|
-
conversationId: conversationId ?? "",
|
|
1320
|
-
role: "user",
|
|
1321
|
-
content,
|
|
1322
|
-
status: "completed",
|
|
1323
|
-
createdAt: new Date,
|
|
1324
|
-
updatedAt: new Date,
|
|
1325
|
-
attachments
|
|
1326
|
-
};
|
|
1327
|
-
setMessages((prev) => [...prev, userMessage]);
|
|
1328
|
-
onSend?.(userMessage);
|
|
1329
|
-
}
|
|
1330
|
-
if (streaming) {
|
|
1331
|
-
const result = await chatServiceRef.current.stream({
|
|
1332
|
-
conversationId: conversationId ?? undefined,
|
|
1333
|
-
content,
|
|
1334
|
-
attachments,
|
|
1335
|
-
skipUserAppend: opts?.skipUserAppend
|
|
1336
|
-
});
|
|
1337
|
-
if (!conversationId && !opts?.skipUserAppend) {
|
|
1338
|
-
setConversationId(result.conversationId);
|
|
1339
|
-
}
|
|
1340
|
-
const assistantMessage = {
|
|
1341
|
-
id: result.messageId,
|
|
1342
|
-
conversationId: result.conversationId,
|
|
1343
|
-
role: "assistant",
|
|
1344
|
-
content: "",
|
|
1345
|
-
status: "streaming",
|
|
1346
|
-
createdAt: new Date,
|
|
1347
|
-
updatedAt: new Date
|
|
1348
|
-
};
|
|
1349
|
-
setMessages((prev) => [...prev, assistantMessage]);
|
|
1350
|
-
let fullContent = "";
|
|
1351
|
-
let fullReasoning = "";
|
|
1352
|
-
const toolCallsMap = new Map;
|
|
1353
|
-
const sources = [];
|
|
1354
|
-
for await (const chunk of result.stream) {
|
|
1355
|
-
if (chunk.type === "text" && chunk.content) {
|
|
1356
|
-
fullContent += chunk.content;
|
|
1357
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1358
|
-
...m,
|
|
1359
|
-
content: fullContent,
|
|
1360
|
-
reasoning: fullReasoning || undefined,
|
|
1361
|
-
sources: sources.length ? sources : undefined,
|
|
1362
|
-
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined
|
|
1363
|
-
} : m));
|
|
1364
|
-
} else if (chunk.type === "reasoning" && chunk.content) {
|
|
1365
|
-
fullReasoning += chunk.content;
|
|
1366
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, reasoning: fullReasoning } : m));
|
|
1367
|
-
} else if (chunk.type === "source" && chunk.source) {
|
|
1368
|
-
sources.push(chunk.source);
|
|
1369
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, sources: [...sources] } : m));
|
|
1370
|
-
} else if (chunk.type === "tool_call" && chunk.toolCall) {
|
|
1371
|
-
const tc = chunk.toolCall;
|
|
1372
|
-
const chatTc = {
|
|
1373
|
-
id: tc.id,
|
|
1374
|
-
name: tc.name,
|
|
1375
|
-
args: tc.args,
|
|
1376
|
-
status: "running"
|
|
1377
|
-
};
|
|
1378
|
-
toolCallsMap.set(tc.id, chatTc);
|
|
1379
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
1380
|
-
} else if (chunk.type === "tool_result" && chunk.toolResult) {
|
|
1381
|
-
const tr = chunk.toolResult;
|
|
1382
|
-
const tc = toolCallsMap.get(tr.toolCallId);
|
|
1383
|
-
if (tc) {
|
|
1384
|
-
tc.result = tr.result;
|
|
1385
|
-
tc.status = "completed";
|
|
1386
|
-
}
|
|
1387
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? { ...m, toolCalls: Array.from(toolCallsMap.values()) } : m));
|
|
1388
|
-
} else if (chunk.type === "done") {
|
|
1389
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1390
|
-
...m,
|
|
1391
|
-
content: fullContent,
|
|
1392
|
-
reasoning: fullReasoning || undefined,
|
|
1393
|
-
sources: sources.length ? sources : undefined,
|
|
1394
|
-
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1395
|
-
status: "completed",
|
|
1396
|
-
usage: chunk.usage,
|
|
1397
|
-
updatedAt: new Date
|
|
1398
|
-
} : m));
|
|
1399
|
-
onResponse?.(messages.find((m) => m.id === result.messageId) ?? assistantMessage);
|
|
1400
|
-
} else if (chunk.type === "error") {
|
|
1401
|
-
setMessages((prev) => prev.map((m) => m.id === result.messageId ? {
|
|
1402
|
-
...m,
|
|
1403
|
-
status: "error",
|
|
1404
|
-
error: chunk.error,
|
|
1405
|
-
updatedAt: new Date
|
|
1406
|
-
} : m));
|
|
1407
|
-
if (chunk.error) {
|
|
1408
|
-
const err = new Error(chunk.error.message);
|
|
1409
|
-
setError(err);
|
|
1410
|
-
onError?.(err);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
} else {
|
|
1415
|
-
const result = await chatServiceRef.current.send({
|
|
1416
|
-
conversationId: conversationId ?? undefined,
|
|
1417
|
-
content,
|
|
1418
|
-
attachments,
|
|
1419
|
-
skipUserAppend: opts?.skipUserAppend
|
|
1420
|
-
});
|
|
1421
|
-
setConversation(result.conversation);
|
|
1422
|
-
setMessages(result.conversation.messages);
|
|
1423
|
-
if (!conversationId) {
|
|
1424
|
-
setConversationId(result.conversation.id);
|
|
1425
|
-
}
|
|
1426
|
-
onResponse?.(result.message);
|
|
1427
|
-
}
|
|
1428
|
-
} catch (err) {
|
|
1429
|
-
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
1430
|
-
setError(error2);
|
|
1431
|
-
onError?.(error2);
|
|
1432
|
-
} finally {
|
|
1433
|
-
setIsLoading(false);
|
|
1434
|
-
abortControllerRef.current = null;
|
|
1435
|
-
}
|
|
1436
|
-
}, [
|
|
1437
|
-
conversationId,
|
|
1438
|
-
streaming,
|
|
1439
|
-
onSend,
|
|
1440
|
-
onResponse,
|
|
1441
|
-
onError,
|
|
1442
|
-
onUsage,
|
|
1443
|
-
messages,
|
|
1444
|
-
agentMode,
|
|
1445
|
-
store
|
|
1446
|
-
]);
|
|
1447
|
-
const clearConversation = React.useCallback(() => {
|
|
1448
|
-
setMessages([]);
|
|
1449
|
-
setConversation(null);
|
|
1450
|
-
setConversationId(null);
|
|
1451
|
-
setError(null);
|
|
1452
|
-
}, []);
|
|
1453
|
-
const regenerate = React.useCallback(async () => {
|
|
1454
|
-
let lastUserMessageIndex = -1;
|
|
1455
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
1456
|
-
const m = messages[i];
|
|
1457
|
-
if (m?.role === "user") {
|
|
1458
|
-
lastUserMessageIndex = i;
|
|
1459
|
-
break;
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
if (lastUserMessageIndex === -1)
|
|
1463
|
-
return;
|
|
1464
|
-
const lastUserMessage = messages[lastUserMessageIndex];
|
|
1465
|
-
if (!lastUserMessage)
|
|
1466
|
-
return;
|
|
1467
|
-
setMessages((prev) => prev.slice(0, lastUserMessageIndex + 1));
|
|
1468
|
-
await sendMessage(lastUserMessage.content, lastUserMessage.attachments);
|
|
1469
|
-
}, [messages, sendMessage]);
|
|
1470
|
-
const stop = React.useCallback(() => {
|
|
1471
|
-
abortControllerRef.current?.abort();
|
|
1472
|
-
setIsLoading(false);
|
|
1473
|
-
}, []);
|
|
1474
|
-
const createNewConversation = clearConversation;
|
|
1475
|
-
const editMessage = React.useCallback(async (messageId, newContent) => {
|
|
1476
|
-
if (!chatServiceRef.current || !conversationId)
|
|
1477
|
-
return;
|
|
1478
|
-
const msg = messages.find((m) => m.id === messageId);
|
|
1479
|
-
if (!msg || msg.role !== "user")
|
|
1480
|
-
return;
|
|
1481
|
-
await chatServiceRef.current.updateMessage(conversationId, messageId, {
|
|
1482
|
-
content: newContent
|
|
1483
|
-
});
|
|
1484
|
-
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
1485
|
-
if (truncated) {
|
|
1486
|
-
setMessages(truncated.messages);
|
|
1487
|
-
}
|
|
1488
|
-
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
1489
|
-
}, [conversationId, messages, sendMessage]);
|
|
1490
|
-
const forkConversation = React.useCallback(async (upToMessageId) => {
|
|
1491
|
-
if (!chatServiceRef.current)
|
|
1492
|
-
return null;
|
|
1493
|
-
const idToFork = conversationId ?? conversation?.id;
|
|
1494
|
-
if (!idToFork)
|
|
1495
|
-
return null;
|
|
1496
|
-
try {
|
|
1497
|
-
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
1498
|
-
setConversationId(forked.id);
|
|
1499
|
-
setConversation(forked);
|
|
1500
|
-
setMessages(forked.messages);
|
|
1501
|
-
return forked.id;
|
|
1502
|
-
} catch {
|
|
1503
|
-
return null;
|
|
1504
|
-
}
|
|
1505
|
-
}, [conversationId, conversation]);
|
|
1506
|
-
const updateConversationFn = React.useCallback(async (updates) => {
|
|
1507
|
-
if (!chatServiceRef.current || !conversationId)
|
|
1508
|
-
return null;
|
|
1509
|
-
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
1510
|
-
if (updated)
|
|
1511
|
-
setConversation(updated);
|
|
1512
|
-
return updated;
|
|
1513
|
-
}, [conversationId]);
|
|
1514
|
-
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
1515
|
-
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
1516
|
-
}, []);
|
|
1517
|
-
const hasApprovalTools = toolsDefs?.some((t) => t.requireApproval) ?? false;
|
|
1518
|
-
return {
|
|
1519
|
-
messages,
|
|
1520
|
-
conversation,
|
|
1521
|
-
isLoading,
|
|
1522
|
-
error,
|
|
1523
|
-
sendMessage,
|
|
1524
|
-
clearConversation,
|
|
1525
|
-
setConversationId,
|
|
1526
|
-
regenerate,
|
|
1527
|
-
stop,
|
|
1528
|
-
createNewConversation,
|
|
1529
|
-
editMessage,
|
|
1530
|
-
forkConversation,
|
|
1531
|
-
updateConversation: updateConversationFn,
|
|
1532
|
-
...hasApprovalTools && { addToolApprovalResponse }
|
|
1533
|
-
};
|
|
1534
|
-
}
|
|
1535
|
-
// src/presentation/hooks/useConversations.ts
|
|
1536
|
-
import * as React2 from "react";
|
|
1537
|
-
"use client";
|
|
1538
|
-
function useConversations(options) {
|
|
1539
|
-
const { store, projectId, tags, limit = 50 } = options;
|
|
1540
|
-
const [conversations, setConversations] = React2.useState([]);
|
|
1541
|
-
const [isLoading, setIsLoading] = React2.useState(true);
|
|
1542
|
-
const refresh = React2.useCallback(async () => {
|
|
1543
|
-
setIsLoading(true);
|
|
1544
|
-
try {
|
|
1545
|
-
const list = await store.list({
|
|
1546
|
-
status: "active",
|
|
1547
|
-
projectId,
|
|
1548
|
-
tags,
|
|
1549
|
-
limit
|
|
1550
|
-
});
|
|
1551
|
-
setConversations(list);
|
|
1552
|
-
} finally {
|
|
1553
|
-
setIsLoading(false);
|
|
1554
|
-
}
|
|
1555
|
-
}, [store, projectId, tags, limit]);
|
|
1556
|
-
React2.useEffect(() => {
|
|
1557
|
-
refresh();
|
|
1558
|
-
}, [refresh]);
|
|
1559
|
-
const deleteConversation = React2.useCallback(async (id) => {
|
|
1560
|
-
const ok = await store.delete(id);
|
|
1561
|
-
if (ok) {
|
|
1562
|
-
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1563
|
-
}
|
|
1564
|
-
return ok;
|
|
1565
|
-
}, [store]);
|
|
1566
|
-
return {
|
|
1567
|
-
conversations,
|
|
1568
|
-
isLoading,
|
|
1569
|
-
refresh,
|
|
1570
|
-
deleteConversation
|
|
1571
|
-
};
|
|
1572
|
-
}
|
|
1573
|
-
// src/presentation/hooks/useMessageSelection.ts
|
|
1574
|
-
import * as React3 from "react";
|
|
1575
|
-
"use client";
|
|
1576
|
-
function useMessageSelection(messageIds) {
|
|
1577
|
-
const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
|
|
1578
|
-
const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1579
|
-
React3.useEffect(() => {
|
|
1580
|
-
setSelectedIds((prev) => {
|
|
1581
|
-
const next = new Set;
|
|
1582
|
-
for (const id of prev) {
|
|
1583
|
-
if (idSet.has(id))
|
|
1584
|
-
next.add(id);
|
|
1585
|
-
}
|
|
1586
|
-
return next.size === prev.size ? prev : next;
|
|
1587
|
-
});
|
|
1588
|
-
}, [idSet]);
|
|
1589
|
-
const toggle = React3.useCallback((id) => {
|
|
1590
|
-
setSelectedIds((prev) => {
|
|
1591
|
-
const next = new Set(prev);
|
|
1592
|
-
if (next.has(id))
|
|
1593
|
-
next.delete(id);
|
|
1594
|
-
else
|
|
1595
|
-
next.add(id);
|
|
1596
|
-
return next;
|
|
1597
|
-
});
|
|
1598
|
-
}, []);
|
|
1599
|
-
const selectAll = React3.useCallback(() => {
|
|
1600
|
-
setSelectedIds(new Set(messageIds));
|
|
1601
|
-
}, [messageIds.join(",")]);
|
|
1602
|
-
const clearSelection = React3.useCallback(() => {
|
|
1603
|
-
setSelectedIds(new Set);
|
|
1604
|
-
}, []);
|
|
1605
|
-
const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1606
|
-
const selectedCount = selectedIds.size;
|
|
1607
|
-
return {
|
|
1608
|
-
selectedIds,
|
|
1609
|
-
toggle,
|
|
1610
|
-
selectAll,
|
|
1611
|
-
clearSelection,
|
|
1612
|
-
isSelected,
|
|
1613
|
-
selectedCount
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
// src/presentation/hooks/useProviders.tsx
|
|
1617
|
-
import {
|
|
1618
|
-
getAvailableProviders,
|
|
1619
|
-
getModelsForProvider
|
|
1620
|
-
} from "@contractspec/lib.ai-providers";
|
|
1621
|
-
import * as React4 from "react";
|
|
1622
|
-
"use client";
|
|
1623
|
-
function useProviders() {
|
|
1624
|
-
const [providers, setProviders] = React4.useState([]);
|
|
1625
|
-
const [isLoading, setIsLoading] = React4.useState(true);
|
|
1626
|
-
const loadProviders = React4.useCallback(async () => {
|
|
1627
|
-
setIsLoading(true);
|
|
1628
|
-
try {
|
|
1629
|
-
const available = getAvailableProviders();
|
|
1630
|
-
const providersWithModels = available.map((p) => ({
|
|
1631
|
-
...p,
|
|
1632
|
-
models: getModelsForProvider(p.provider)
|
|
1633
|
-
}));
|
|
1634
|
-
setProviders(providersWithModels);
|
|
1635
|
-
} catch (error) {
|
|
1636
|
-
console.error("Failed to load providers:", error);
|
|
1637
|
-
} finally {
|
|
1638
|
-
setIsLoading(false);
|
|
1639
|
-
}
|
|
1640
|
-
}, []);
|
|
1641
|
-
React4.useEffect(() => {
|
|
1642
|
-
loadProviders();
|
|
1643
|
-
}, [loadProviders]);
|
|
1644
|
-
const availableProviders = React4.useMemo(() => providers.filter((p) => p.available), [providers]);
|
|
1645
|
-
const isAvailable = React4.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
|
|
1646
|
-
const getModelsCallback = React4.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
|
|
1647
|
-
return {
|
|
1648
|
-
providers,
|
|
1649
|
-
availableProviders,
|
|
1650
|
-
isAvailable,
|
|
1651
|
-
getModels: getModelsCallback,
|
|
1652
|
-
isLoading,
|
|
1653
|
-
refresh: loadProviders
|
|
1654
|
-
};
|
|
1655
|
-
}
|
|
1656
|
-
export {
|
|
1657
|
-
useProviders,
|
|
1658
|
-
useMessageSelection,
|
|
1659
|
-
useConversations,
|
|
1660
|
-
useCompletion,
|
|
1661
|
-
useChat
|
|
1662
|
-
};
|
|
62
|
+
[Attachment: ${q.name}]`}).join("");j+=H}J.push({role:"user",content:j})}else if(G.role==="assistant")if(G.toolCalls?.length)J.push({role:"assistant",content:G.content||"",toolCalls:G.toolCalls.map((j)=>({type:"tool-call",toolCallId:j.id,toolName:j.name,args:j.args}))}),J.push({role:"tool",content:G.toolCalls.map((j)=>({type:"tool-result",toolCallId:j.id,toolName:j.name,output:j.result}))});else J.push({role:"assistant",content:G.content})}return J}}function SZ(X){return new ZX(X)}function oX(X){let Z={};for(let $ of X)Z[$.name]=tX({description:$.description??$.name,inputSchema:sX.object({}).passthrough(),execute:async()=>({})});return Z}function eX(X={}){let{provider:Z="openai",mode:$="byok",model:J,apiKey:Y,proxyUrl:G,conversationId:j,store:H,systemPrompt:q,streaming:Q=!0,onSend:_,onResponse:A,onError:N,onUsage:k,tools:P,thinkingLevel:I,workflowToolsConfig:h,modelSelector:U,contractsContext:W,surfacePlanConfig:f,mcpServers:g,agentMode:$X}=X,[m,x]=E.useState([]),[VX,t]=E.useState(null),JX=E.useRef(null),[YX,p]=E.useState(null),[FX,a]=E.useState(!1),[AX,c]=E.useState(null),[w,i]=E.useState(j??null),r=E.useRef(null),T=E.useRef(null);E.useEffect(()=>{if(!g?.length){t(null);return}let D=!1;return import("@contractspec/lib.ai-agent/tools/mcp-client.browser").then(({createMcpToolsets:F})=>{F(g).then(({tools:L,cleanup:V})=>{if(!D)t(L),JX.current=V;else V().catch(()=>{return})}).catch(()=>{if(!D)t(null)})}),()=>{D=!0;let F=JX.current;if(JX.current=null,F)F().catch(()=>{return});t(null)}},[g]),E.useEffect(()=>{let D=nX({provider:Z,model:J,apiKey:Y,proxyUrl:G});T.current=new ZX({provider:D,store:H,systemPrompt:q,onUsage:k,tools:P?.length?oX(P):void 0,thinkingLevel:I,workflowToolsConfig:h,modelSelector:U,contractsContext:W,surfacePlanConfig:f,mcpTools:VX})},[Z,$,J,Y,G,H,q,k,P,I,h,U,W,f,VX]),E.useEffect(()=>{if(!w||!T.current)return;(async()=>{if(!T.current)return;let F=await T.current.getConversation(w);if(F)p(F),x(F.messages)})().catch(console.error)},[w]);let n=E.useCallback(async(D,F,L)=>{if($X?.agent){a(!0),c(null),r.current=new AbortController;try{if(!L?.skipUserAppend){let R={id:`msg_${Date.now()}`,conversationId:w??"",role:"user",content:D,status:"completed",createdAt:new Date,updatedAt:new Date,attachments:F};x((b)=>[...b,R]),_?.(R)}let V=await $X.agent.generate({prompt:D,signal:r.current.signal}),S=new Map;for(let R of V.toolCalls??[]){let b=V.toolResults?.find((u)=>u.toolCallId===R.toolCallId);S.set(R.toolCallId,{id:R.toolCallId,name:R.toolName,args:R.args??{},result:b?.output,status:"completed"})}let d={id:`msg_${Date.now()}_a`,conversationId:w??"",role:"assistant",content:V.text,status:"completed",createdAt:new Date,updatedAt:new Date,toolCalls:S.size?Array.from(S.values()):void 0,usage:V.usage};if(x((R)=>[...R,d]),A?.(d),k?.(V.usage??{inputTokens:0,outputTokens:0}),H&&!w){let R=await H.create({status:"active",provider:"agent",model:"agent",messages:[]});if(!L?.skipUserAppend)await H.appendMessage(R.id,{role:"user",content:D,status:"completed",attachments:F});await H.appendMessage(R.id,{role:"assistant",content:V.text,status:"completed",toolCalls:d.toolCalls,usage:V.usage});let b=await H.get(R.id);if(b)p(b);i(R.id)}}catch(V){c(V instanceof Error?V:Error(String(V))),N?.(V instanceof Error?V:Error(String(V)))}finally{a(!1)}return}if(!T.current)throw Error("Chat service not initialized");a(!0),c(null),r.current=new AbortController;try{if(!L?.skipUserAppend){let V={id:`msg_${Date.now()}`,conversationId:w??"",role:"user",content:D,status:"completed",createdAt:new Date,updatedAt:new Date,attachments:F};x((S)=>[...S,V]),_?.(V)}if(Q){let V=await T.current.stream({conversationId:w??void 0,content:D,attachments:F,skipUserAppend:L?.skipUserAppend});if(!w&&!L?.skipUserAppend)i(V.conversationId);let S={id:V.messageId,conversationId:V.conversationId,role:"assistant",content:"",status:"streaming",createdAt:new Date,updatedAt:new Date};x((y)=>[...y,S]);let d="",R="",b=new Map,u=[];for await(let y of V.stream)if(y.type==="text"&&y.content)d+=y.content,x((z)=>z.map((O)=>O.id===V.messageId?{...O,content:d,reasoning:R||void 0,sources:u.length?u:void 0,toolCalls:b.size?Array.from(b.values()):void 0}:O));else if(y.type==="reasoning"&&y.content)R+=y.content,x((z)=>z.map((O)=>O.id===V.messageId?{...O,reasoning:R}:O));else if(y.type==="source"&&y.source)u.push(y.source),x((z)=>z.map((O)=>O.id===V.messageId?{...O,sources:[...u]}:O));else if(y.type==="tool_call"&&y.toolCall){let z=y.toolCall,O={id:z.id,name:z.name,args:z.args,status:"running"};b.set(z.id,O),x((jX)=>jX.map((l)=>l.id===V.messageId?{...l,toolCalls:Array.from(b.values())}:l))}else if(y.type==="tool_result"&&y.toolResult){let z=y.toolResult,O=b.get(z.toolCallId);if(O)O.result=z.result,O.status="completed";x((jX)=>jX.map((l)=>l.id===V.messageId?{...l,toolCalls:Array.from(b.values())}:l))}else if(y.type==="done")x((z)=>z.map((O)=>O.id===V.messageId?{...O,content:d,reasoning:R||void 0,sources:u.length?u:void 0,toolCalls:b.size?Array.from(b.values()):void 0,status:"completed",usage:y.usage,updatedAt:new Date}:O)),A?.(m.find((z)=>z.id===V.messageId)??S);else if(y.type==="error"){if(x((z)=>z.map((O)=>O.id===V.messageId?{...O,status:"error",error:y.error,updatedAt:new Date}:O)),y.error){let z=Error(y.error.message);c(z),N?.(z)}}}else{let V=await T.current.send({conversationId:w??void 0,content:D,attachments:F,skipUserAppend:L?.skipUserAppend});if(p(V.conversation),x(V.conversation.messages),!w)i(V.conversation.id);A?.(V.message)}}catch(V){let S=V instanceof Error?V:Error(String(V));c(S),N?.(S)}finally{a(!1),r.current=null}},[w,Q,_,A,N,k,m,$X,H]),HX=E.useCallback(()=>{x([]),p(null),i(null),c(null)},[]),KX=E.useCallback(async()=>{let D=-1;for(let L=m.length-1;L>=0;L--)if(m[L]?.role==="user"){D=L;break}if(D===-1)return;let F=m[D];if(!F)return;x((L)=>L.slice(0,D+1)),await n(F.content,F.attachments)},[m,n]),OX=E.useCallback(()=>{r.current?.abort(),a(!1)},[]),EX=HX,LX=E.useCallback(async(D,F)=>{if(!T.current||!w)return;let L=m.find((S)=>S.id===D);if(!L||L.role!=="user")return;await T.current.updateMessage(w,D,{content:F});let V=await T.current.truncateAfter(w,D);if(V)x(V.messages);await n(F,void 0,{skipUserAppend:!0})},[w,m,n]),yX=E.useCallback(async(D)=>{if(!T.current)return null;let F=w??YX?.id;if(!F)return null;try{let L=await T.current.forkConversation(F,D);return i(L.id),p(L),x(L.messages),L.id}catch{return null}},[w,YX]),zX=E.useCallback(async(D)=>{if(!T.current||!w)return null;let F=await T.current.updateConversation(w,D);if(F)p(F);return F},[w]),wX=E.useCallback((D,F)=>{throw Error("addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.")},[]),RX=P?.some((D)=>D.requireApproval)??!1;return{messages:m,conversation:YX,isLoading:FX,error:AX,sendMessage:n,clearConversation:HX,setConversationId:i,regenerate:KX,stop:OX,createNewConversation:EX,editMessage:LX,forkConversation:yX,updateConversation:zX,...RX&&{addToolApprovalResponse:wX}}}import*as v from"react";function XZ(X){let{store:Z,projectId:$,tags:J,limit:Y=50}=X,[G,j]=v.useState([]),[H,q]=v.useState(!0),Q=v.useCallback(async()=>{q(!0);try{let A=await Z.list({status:"active",projectId:$,tags:J,limit:Y});j(A)}finally{q(!1)}},[Z,$,J,Y]);v.useEffect(()=>{Q()},[Q]);let _=v.useCallback(async(A)=>{let N=await Z.delete(A);if(N)j((k)=>k.filter((P)=>P.id!==A));return N},[Z]);return{conversations:G,isLoading:H,refresh:Q,deleteConversation:_}}import*as C from"react";function ZZ(X){let[Z,$]=C.useState(()=>new Set),J=C.useMemo(()=>new Set(X),[X.join(",")]);C.useEffect(()=>{$((Q)=>{let _=new Set;for(let A of Q)if(J.has(A))_.add(A);return _.size===Q.size?Q:_})},[J]);let Y=C.useCallback((Q)=>{$((_)=>{let A=new Set(_);if(A.has(Q))A.delete(Q);else A.add(Q);return A})},[]),G=C.useCallback(()=>{$(new Set(X))},[X.join(",")]),j=C.useCallback(()=>{$(new Set)},[]),H=C.useCallback((Q)=>Z.has(Q),[Z]),q=Z.size;return{selectedIds:Z,toggle:Y,selectAll:G,clearSelection:j,isSelected:H,selectedCount:q}}import{getAvailableProviders as $Z,getModelsForProvider as JZ}from"@contractspec/lib.ai-providers";import*as M from"react";function YZ(){let[X,Z]=M.useState([]),[$,J]=M.useState(!0),Y=M.useCallback(async()=>{J(!0);try{let Q=$Z().map((_)=>({..._,models:JZ(_.provider)}));Z(Q)}catch(q){console.error("Failed to load providers:",q)}finally{J(!1)}},[]);M.useEffect(()=>{Y()},[Y]);let G=M.useMemo(()=>X.filter((q)=>q.available),[X]),j=M.useCallback((q)=>X.some((Q)=>Q.provider===q&&Q.available),[X]),H=M.useCallback((q)=>X.find((Q)=>Q.provider===q)?.models??[],[X]);return{providers:X,availableProviders:G,isAvailable:j,getModels:H,isLoading:$,refresh:Y}}export{YZ as useProviders,ZZ as useMessageSelection,XZ as useConversations,cZ as useCompletion,eX as useChat};
|