@contractspec/module.ai-chat 4.0.3 → 4.1.2
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/README.md +130 -10
- package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/browser/core/index.js +1143 -21
- package/dist/browser/index.js +2813 -631
- package/dist/browser/presentation/components/index.js +3160 -358
- package/dist/browser/presentation/hooks/index.js +978 -43
- package/dist/browser/presentation/index.js +2801 -666
- package/dist/core/agent-adapter.d.ts +53 -0
- package/dist/core/agent-tools-adapter.d.ts +12 -0
- package/dist/core/chat-service.d.ts +49 -1
- package/dist/core/contracts-context.d.ts +46 -0
- package/dist/core/contracts-context.test.d.ts +1 -0
- package/dist/core/conversation-store.d.ts +16 -2
- package/dist/core/create-chat-route.d.ts +3 -0
- package/dist/core/export-formatters.d.ts +29 -0
- package/dist/core/export-formatters.test.d.ts +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +1143 -21
- package/dist/core/local-storage-conversation-store.d.ts +33 -0
- package/dist/core/message-types.d.ts +6 -0
- package/dist/core/surface-planner-tools.d.ts +23 -0
- package/dist/core/surface-planner-tools.test.d.ts +1 -0
- package/dist/core/thinking-levels.d.ts +38 -0
- package/dist/core/thinking-levels.test.d.ts +1 -0
- package/dist/core/workflow-tools.d.ts +18 -0
- package/dist/core/workflow-tools.test.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2813 -631
- package/dist/node/core/index.js +1143 -21
- package/dist/node/index.js +2813 -631
- package/dist/node/presentation/components/index.js +3160 -358
- package/dist/node/presentation/hooks/index.js +978 -43
- package/dist/node/presentation/index.js +2804 -669
- package/dist/presentation/components/ChatContainer.d.ts +3 -1
- package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
- package/dist/presentation/components/ChatMessage.d.ts +16 -1
- package/dist/presentation/components/ChatSidebar.d.ts +26 -0
- package/dist/presentation/components/ChatWithExport.d.ts +34 -0
- package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
- package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
- package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
- package/dist/presentation/components/index.d.ts +6 -0
- package/dist/presentation/components/index.js +3160 -358
- package/dist/presentation/hooks/index.d.ts +2 -0
- package/dist/presentation/hooks/index.js +978 -43
- package/dist/presentation/hooks/useChat.d.ts +44 -2
- package/dist/presentation/hooks/useConversations.d.ts +18 -0
- package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
- package/dist/presentation/index.js +2804 -669
- package/package.json +14 -18
|
@@ -3,8 +3,8 @@ var __require = import.meta.require;
|
|
|
3
3
|
|
|
4
4
|
// src/presentation/hooks/useChat.tsx
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import { tool } from "ai";
|
|
7
|
-
import { z } from "zod";
|
|
6
|
+
import { tool as tool4 } from "ai";
|
|
7
|
+
import { z as z4 } from "zod";
|
|
8
8
|
|
|
9
9
|
// src/core/chat-service.ts
|
|
10
10
|
import { generateText, streamText } from "ai";
|
|
@@ -86,11 +86,65 @@ class InMemoryConversationStore {
|
|
|
86
86
|
if (options?.status) {
|
|
87
87
|
results = results.filter((c) => c.status === options.status);
|
|
88
88
|
}
|
|
89
|
+
if (options?.projectId) {
|
|
90
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
91
|
+
}
|
|
92
|
+
if (options?.tags && options.tags.length > 0) {
|
|
93
|
+
const tagSet = new Set(options.tags);
|
|
94
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
95
|
+
}
|
|
89
96
|
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
90
97
|
const offset = options?.offset ?? 0;
|
|
91
98
|
const limit = options?.limit ?? 100;
|
|
92
99
|
return results.slice(offset, offset + limit);
|
|
93
100
|
}
|
|
101
|
+
async fork(conversationId, upToMessageId) {
|
|
102
|
+
const source = this.conversations.get(conversationId);
|
|
103
|
+
if (!source) {
|
|
104
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
105
|
+
}
|
|
106
|
+
let messagesToCopy = source.messages;
|
|
107
|
+
if (upToMessageId) {
|
|
108
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
109
|
+
if (idx === -1) {
|
|
110
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
111
|
+
}
|
|
112
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
113
|
+
}
|
|
114
|
+
const now = new Date;
|
|
115
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
116
|
+
...m,
|
|
117
|
+
id: generateId("msg"),
|
|
118
|
+
conversationId: "",
|
|
119
|
+
createdAt: new Date(m.createdAt),
|
|
120
|
+
updatedAt: new Date(m.updatedAt)
|
|
121
|
+
}));
|
|
122
|
+
const forked = {
|
|
123
|
+
...source,
|
|
124
|
+
id: generateId("conv"),
|
|
125
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
126
|
+
forkedFromId: source.id,
|
|
127
|
+
createdAt: now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
messages: forkedMessages
|
|
130
|
+
};
|
|
131
|
+
for (const m of forked.messages) {
|
|
132
|
+
m.conversationId = forked.id;
|
|
133
|
+
}
|
|
134
|
+
this.conversations.set(forked.id, forked);
|
|
135
|
+
return forked;
|
|
136
|
+
}
|
|
137
|
+
async truncateAfter(conversationId, messageId) {
|
|
138
|
+
const conv = this.conversations.get(conversationId);
|
|
139
|
+
if (!conv)
|
|
140
|
+
return null;
|
|
141
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
142
|
+
if (idx === -1)
|
|
143
|
+
return null;
|
|
144
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
145
|
+
conv.updatedAt = new Date;
|
|
146
|
+
return conv;
|
|
147
|
+
}
|
|
94
148
|
async search(query, limit = 20) {
|
|
95
149
|
const lowerQuery = query.toLowerCase();
|
|
96
150
|
const results = [];
|
|
@@ -116,7 +170,509 @@ function createInMemoryConversationStore() {
|
|
|
116
170
|
return new InMemoryConversationStore;
|
|
117
171
|
}
|
|
118
172
|
|
|
173
|
+
// src/core/thinking-levels.ts
|
|
174
|
+
var THINKING_LEVEL_LABELS = {
|
|
175
|
+
instant: "Instant",
|
|
176
|
+
thinking: "Thinking",
|
|
177
|
+
extra_thinking: "Extra Thinking",
|
|
178
|
+
max: "Max"
|
|
179
|
+
};
|
|
180
|
+
var THINKING_LEVEL_DESCRIPTIONS = {
|
|
181
|
+
instant: "Fast responses, minimal reasoning",
|
|
182
|
+
thinking: "Standard reasoning depth",
|
|
183
|
+
extra_thinking: "More thorough reasoning",
|
|
184
|
+
max: "Maximum reasoning depth"
|
|
185
|
+
};
|
|
186
|
+
function getProviderOptions(level, providerName) {
|
|
187
|
+
if (!level || level === "instant") {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
switch (providerName) {
|
|
191
|
+
case "anthropic": {
|
|
192
|
+
const budgetMap = {
|
|
193
|
+
thinking: 8000,
|
|
194
|
+
extra_thinking: 16000,
|
|
195
|
+
max: 32000
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
anthropic: {
|
|
199
|
+
thinking: { type: "enabled", budgetTokens: budgetMap[level] }
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
case "openai": {
|
|
204
|
+
const effortMap = {
|
|
205
|
+
thinking: "low",
|
|
206
|
+
extra_thinking: "medium",
|
|
207
|
+
max: "high"
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
openai: {
|
|
211
|
+
reasoningEffort: effortMap[level]
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
case "ollama":
|
|
216
|
+
case "mistral":
|
|
217
|
+
case "gemini":
|
|
218
|
+
return {};
|
|
219
|
+
default:
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/core/workflow-tools.ts
|
|
225
|
+
import { tool } from "ai";
|
|
226
|
+
import { z } from "zod";
|
|
227
|
+
import {
|
|
228
|
+
WorkflowComposer,
|
|
229
|
+
validateExtension
|
|
230
|
+
} from "@contractspec/lib.workflow-composer";
|
|
231
|
+
var StepTypeSchema = z.enum(["human", "automation", "decision"]);
|
|
232
|
+
var StepActionSchema = z.object({
|
|
233
|
+
operation: z.object({
|
|
234
|
+
name: z.string(),
|
|
235
|
+
version: z.number()
|
|
236
|
+
}).optional(),
|
|
237
|
+
form: z.object({
|
|
238
|
+
key: z.string(),
|
|
239
|
+
version: z.number()
|
|
240
|
+
}).optional()
|
|
241
|
+
}).optional();
|
|
242
|
+
var StepSchema = z.object({
|
|
243
|
+
id: z.string(),
|
|
244
|
+
type: StepTypeSchema,
|
|
245
|
+
label: z.string(),
|
|
246
|
+
description: z.string().optional(),
|
|
247
|
+
action: StepActionSchema
|
|
248
|
+
});
|
|
249
|
+
var StepInjectionSchema = z.object({
|
|
250
|
+
after: z.string().optional(),
|
|
251
|
+
before: z.string().optional(),
|
|
252
|
+
inject: StepSchema,
|
|
253
|
+
transitionTo: z.string().optional(),
|
|
254
|
+
transitionFrom: z.string().optional(),
|
|
255
|
+
when: z.string().optional()
|
|
256
|
+
});
|
|
257
|
+
var WorkflowExtensionInputSchema = z.object({
|
|
258
|
+
workflow: z.string(),
|
|
259
|
+
tenantId: z.string().optional(),
|
|
260
|
+
role: z.string().optional(),
|
|
261
|
+
priority: z.number().optional(),
|
|
262
|
+
customSteps: z.array(StepInjectionSchema).optional(),
|
|
263
|
+
hiddenSteps: z.array(z.string()).optional()
|
|
264
|
+
});
|
|
265
|
+
function createWorkflowTools(config) {
|
|
266
|
+
const { baseWorkflows, composer } = config;
|
|
267
|
+
const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
|
|
268
|
+
const createWorkflowExtensionTool = tool({
|
|
269
|
+
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.",
|
|
270
|
+
inputSchema: WorkflowExtensionInputSchema,
|
|
271
|
+
execute: async (input) => {
|
|
272
|
+
const extension = {
|
|
273
|
+
workflow: input.workflow,
|
|
274
|
+
tenantId: input.tenantId,
|
|
275
|
+
role: input.role,
|
|
276
|
+
priority: input.priority,
|
|
277
|
+
customSteps: input.customSteps,
|
|
278
|
+
hiddenSteps: input.hiddenSteps
|
|
279
|
+
};
|
|
280
|
+
const base = baseByKey.get(input.workflow);
|
|
281
|
+
if (!base) {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
285
|
+
extension
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
validateExtension(extension, base);
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
message: "Extension validated successfully",
|
|
293
|
+
extension
|
|
294
|
+
};
|
|
295
|
+
} catch (err) {
|
|
296
|
+
return {
|
|
297
|
+
success: false,
|
|
298
|
+
error: err instanceof Error ? err.message : String(err),
|
|
299
|
+
extension
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
const composeWorkflowInputSchema = z.object({
|
|
305
|
+
workflowKey: z.string().describe("Base workflow meta.key"),
|
|
306
|
+
tenantId: z.string().optional(),
|
|
307
|
+
role: z.string().optional(),
|
|
308
|
+
extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
|
|
309
|
+
});
|
|
310
|
+
const composeWorkflowTool = tool({
|
|
311
|
+
description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
|
|
312
|
+
inputSchema: composeWorkflowInputSchema,
|
|
313
|
+
execute: async (input) => {
|
|
314
|
+
const base = baseByKey.get(input.workflowKey);
|
|
315
|
+
if (!base) {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const comp = composer ?? new WorkflowComposer;
|
|
322
|
+
if (input.extensions?.length) {
|
|
323
|
+
for (const ext of input.extensions) {
|
|
324
|
+
comp.register({
|
|
325
|
+
workflow: ext.workflow,
|
|
326
|
+
tenantId: ext.tenantId,
|
|
327
|
+
role: ext.role,
|
|
328
|
+
priority: ext.priority,
|
|
329
|
+
customSteps: ext.customSteps,
|
|
330
|
+
hiddenSteps: ext.hiddenSteps
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const composed = comp.compose({
|
|
336
|
+
base,
|
|
337
|
+
tenantId: input.tenantId,
|
|
338
|
+
role: input.role
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
workflow: composed,
|
|
343
|
+
meta: composed.meta,
|
|
344
|
+
stepIds: composed.definition.steps.map((s) => s.id)
|
|
345
|
+
};
|
|
346
|
+
} catch (err) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
error: err instanceof Error ? err.message : String(err)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
const generateWorkflowSpecCodeInputSchema = z.object({
|
|
355
|
+
workflowKey: z.string().describe("Workflow meta.key"),
|
|
356
|
+
composedSteps: z.array(z.object({
|
|
357
|
+
id: z.string(),
|
|
358
|
+
type: z.enum(["human", "automation", "decision"]),
|
|
359
|
+
label: z.string(),
|
|
360
|
+
description: z.string().optional()
|
|
361
|
+
})).optional().describe("Steps to include; if omitted, uses the base workflow")
|
|
362
|
+
});
|
|
363
|
+
const generateWorkflowSpecCodeTool = tool({
|
|
364
|
+
description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
|
|
365
|
+
inputSchema: generateWorkflowSpecCodeInputSchema,
|
|
366
|
+
execute: async (input) => {
|
|
367
|
+
const base = baseByKey.get(input.workflowKey);
|
|
368
|
+
if (!base) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
372
|
+
code: null
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const steps = input.composedSteps ?? base.definition.steps;
|
|
376
|
+
const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
|
|
377
|
+
const stepsCode = steps.map((s) => ` {
|
|
378
|
+
id: '${s.id}',
|
|
379
|
+
type: '${s.type}',
|
|
380
|
+
label: '${escapeString(s.label)}',${s.description ? `
|
|
381
|
+
description: '${escapeString(s.description)}',` : ""}
|
|
382
|
+
}`).join(`,
|
|
383
|
+
`);
|
|
384
|
+
const meta = base.meta;
|
|
385
|
+
const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
|
|
386
|
+
const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Workflow: ${base.meta.key}
|
|
390
|
+
* Generated via AI chat workflow tools.
|
|
391
|
+
*/
|
|
392
|
+
export const ${specVarName}: WorkflowSpec = {
|
|
393
|
+
meta: {
|
|
394
|
+
key: '${base.meta.key}',
|
|
395
|
+
version: '${String(base.meta.version)}',
|
|
396
|
+
title: '${escapeString(meta.title ?? base.meta.key)}',
|
|
397
|
+
description: '${escapeString(meta.description ?? "")}',
|
|
398
|
+
},
|
|
399
|
+
definition: {
|
|
400
|
+
entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
|
|
401
|
+
steps: [
|
|
402
|
+
${stepsCode}
|
|
403
|
+
],
|
|
404
|
+
transitions: ${transitionsJson},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
`;
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
code,
|
|
411
|
+
workflowKey: input.workflowKey
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
create_workflow_extension: createWorkflowExtensionTool,
|
|
417
|
+
compose_workflow: composeWorkflowTool,
|
|
418
|
+
generate_workflow_spec_code: generateWorkflowSpecCodeTool
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function toPascalCase(value) {
|
|
422
|
+
return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
423
|
+
}
|
|
424
|
+
function escapeString(value) {
|
|
425
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/core/contracts-context.ts
|
|
429
|
+
function buildContractsContextPrompt(config) {
|
|
430
|
+
const parts = [];
|
|
431
|
+
if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
|
|
432
|
+
return "";
|
|
433
|
+
}
|
|
434
|
+
parts.push(`
|
|
435
|
+
|
|
436
|
+
## Available resources`);
|
|
437
|
+
if (config.agentSpecs?.length) {
|
|
438
|
+
parts.push(`
|
|
439
|
+
### Agent tools`);
|
|
440
|
+
for (const agent of config.agentSpecs) {
|
|
441
|
+
const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
|
|
442
|
+
parts.push(`- **${agent.key}**: tools: ${toolNames}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (config.dataViewSpecs?.length) {
|
|
446
|
+
parts.push(`
|
|
447
|
+
### Data views`);
|
|
448
|
+
for (const dv of config.dataViewSpecs) {
|
|
449
|
+
parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (config.formSpecs?.length) {
|
|
453
|
+
parts.push(`
|
|
454
|
+
### Forms`);
|
|
455
|
+
for (const form of config.formSpecs) {
|
|
456
|
+
parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (config.presentationSpecs?.length) {
|
|
460
|
+
parts.push(`
|
|
461
|
+
### Presentations`);
|
|
462
|
+
for (const pres of config.presentationSpecs) {
|
|
463
|
+
parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (config.operationRefs?.length) {
|
|
467
|
+
parts.push(`
|
|
468
|
+
### Operations`);
|
|
469
|
+
for (const op of config.operationRefs) {
|
|
470
|
+
parts.push(`- **${op.key}@${op.version}**`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
parts.push(`
|
|
474
|
+
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
|
|
475
|
+
return parts.join(`
|
|
476
|
+
`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/core/agent-tools-adapter.ts
|
|
480
|
+
import { tool as tool2 } from "ai";
|
|
481
|
+
import { z as z2 } from "zod";
|
|
482
|
+
function getInputSchema(_schema) {
|
|
483
|
+
return z2.object({}).passthrough();
|
|
484
|
+
}
|
|
485
|
+
function agentToolConfigsToToolSet(configs, handlers) {
|
|
486
|
+
const result = {};
|
|
487
|
+
for (const config of configs) {
|
|
488
|
+
const handler = handlers?.[config.name];
|
|
489
|
+
const inputSchema = getInputSchema(config.schema);
|
|
490
|
+
result[config.name] = tool2({
|
|
491
|
+
description: config.description ?? config.name,
|
|
492
|
+
inputSchema,
|
|
493
|
+
execute: async (input) => {
|
|
494
|
+
if (!handler) {
|
|
495
|
+
return {
|
|
496
|
+
status: "unimplemented",
|
|
497
|
+
message: "Wire handler in host",
|
|
498
|
+
toolName: config.name
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const output = await Promise.resolve(handler(input));
|
|
503
|
+
return typeof output === "string" ? output : output;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
return {
|
|
506
|
+
status: "error",
|
|
507
|
+
error: err instanceof Error ? err.message : String(err),
|
|
508
|
+
toolName: config.name
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/core/surface-planner-tools.ts
|
|
518
|
+
import { tool as tool3 } from "ai";
|
|
519
|
+
import { z as z3 } from "zod";
|
|
520
|
+
import {
|
|
521
|
+
validatePatchProposal
|
|
522
|
+
} from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
523
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
524
|
+
var VALID_OPS = [
|
|
525
|
+
"insert-node",
|
|
526
|
+
"replace-node",
|
|
527
|
+
"remove-node",
|
|
528
|
+
"move-node",
|
|
529
|
+
"resize-panel",
|
|
530
|
+
"set-layout",
|
|
531
|
+
"reveal-field",
|
|
532
|
+
"hide-field",
|
|
533
|
+
"promote-action",
|
|
534
|
+
"set-focus"
|
|
535
|
+
];
|
|
536
|
+
var DEFAULT_NODE_KINDS = [
|
|
537
|
+
"entity-section",
|
|
538
|
+
"entity-card",
|
|
539
|
+
"data-view",
|
|
540
|
+
"assistant-panel",
|
|
541
|
+
"chat-thread",
|
|
542
|
+
"action-bar",
|
|
543
|
+
"timeline",
|
|
544
|
+
"table",
|
|
545
|
+
"rich-doc",
|
|
546
|
+
"form",
|
|
547
|
+
"chart",
|
|
548
|
+
"custom-widget"
|
|
549
|
+
];
|
|
550
|
+
function collectSlotIdsFromRegion(node) {
|
|
551
|
+
const ids = [];
|
|
552
|
+
if (node.type === "slot") {
|
|
553
|
+
ids.push(node.slotId);
|
|
554
|
+
}
|
|
555
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
556
|
+
for (const child of node.children) {
|
|
557
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (node.type === "tabs") {
|
|
561
|
+
for (const tab of node.tabs) {
|
|
562
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (node.type === "floating") {
|
|
566
|
+
ids.push(node.anchorSlotId);
|
|
567
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
568
|
+
}
|
|
569
|
+
return ids;
|
|
570
|
+
}
|
|
571
|
+
function deriveConstraints(plan) {
|
|
572
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
573
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
574
|
+
return {
|
|
575
|
+
allowedOps: VALID_OPS,
|
|
576
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
577
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
var ProposePatchInputSchema = z3.object({
|
|
581
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
582
|
+
ops: z3.array(z3.object({
|
|
583
|
+
op: z3.enum([
|
|
584
|
+
"insert-node",
|
|
585
|
+
"replace-node",
|
|
586
|
+
"remove-node",
|
|
587
|
+
"move-node",
|
|
588
|
+
"resize-panel",
|
|
589
|
+
"set-layout",
|
|
590
|
+
"reveal-field",
|
|
591
|
+
"hide-field",
|
|
592
|
+
"promote-action",
|
|
593
|
+
"set-focus"
|
|
594
|
+
]),
|
|
595
|
+
slotId: z3.string().optional(),
|
|
596
|
+
nodeId: z3.string().optional(),
|
|
597
|
+
toSlotId: z3.string().optional(),
|
|
598
|
+
index: z3.number().optional(),
|
|
599
|
+
node: z3.object({
|
|
600
|
+
nodeId: z3.string(),
|
|
601
|
+
kind: z3.string(),
|
|
602
|
+
title: z3.string().optional(),
|
|
603
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
604
|
+
children: z3.array(z3.unknown()).optional()
|
|
605
|
+
}).optional(),
|
|
606
|
+
persistKey: z3.string().optional(),
|
|
607
|
+
sizes: z3.array(z3.number()).optional(),
|
|
608
|
+
layoutId: z3.string().optional(),
|
|
609
|
+
fieldId: z3.string().optional(),
|
|
610
|
+
actionId: z3.string().optional(),
|
|
611
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
612
|
+
targetId: z3.string().optional()
|
|
613
|
+
}))
|
|
614
|
+
});
|
|
615
|
+
function createSurfacePlannerTools(config) {
|
|
616
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
617
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
618
|
+
const proposePatchTool = tool3({
|
|
619
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
620
|
+
inputSchema: ProposePatchInputSchema,
|
|
621
|
+
execute: async (input) => {
|
|
622
|
+
const ops = input.ops;
|
|
623
|
+
try {
|
|
624
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
625
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
626
|
+
onPatchProposal?.(proposal);
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
proposalId: proposal.proposalId,
|
|
630
|
+
opsCount: proposal.ops.length,
|
|
631
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
632
|
+
};
|
|
633
|
+
} catch (err) {
|
|
634
|
+
return {
|
|
635
|
+
success: false,
|
|
636
|
+
error: err instanceof Error ? err.message : String(err),
|
|
637
|
+
proposalId: input.proposalId
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
return {
|
|
643
|
+
"propose-patch": proposePatchTool
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function buildPlannerPromptInput(plan) {
|
|
647
|
+
const constraints = deriveConstraints(plan);
|
|
648
|
+
return {
|
|
649
|
+
bundleMeta: {
|
|
650
|
+
key: plan.bundleKey,
|
|
651
|
+
version: "0.0.0",
|
|
652
|
+
title: plan.bundleKey
|
|
653
|
+
},
|
|
654
|
+
surfaceId: plan.surfaceId,
|
|
655
|
+
allowedPatchOps: constraints.allowedOps,
|
|
656
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
657
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
658
|
+
actions: plan.actions.map((a) => ({
|
|
659
|
+
actionId: a.actionId,
|
|
660
|
+
title: a.title
|
|
661
|
+
})),
|
|
662
|
+
preferences: {
|
|
663
|
+
guidance: "hints",
|
|
664
|
+
density: "standard",
|
|
665
|
+
dataDepth: "detailed",
|
|
666
|
+
control: "standard",
|
|
667
|
+
media: "text",
|
|
668
|
+
pace: "balanced",
|
|
669
|
+
narrative: "top-down"
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
119
674
|
// src/core/chat-service.ts
|
|
675
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
120
676
|
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
121
677
|
|
|
122
678
|
Your capabilities:
|
|
@@ -131,6 +687,9 @@ Guidelines:
|
|
|
131
687
|
- Reference relevant ContractSpec concepts and patterns
|
|
132
688
|
- Ask clarifying questions when the user's intent is unclear
|
|
133
689
|
- When suggesting code changes, explain the rationale`;
|
|
690
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
691
|
+
|
|
692
|
+
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.`;
|
|
134
693
|
|
|
135
694
|
class ChatService {
|
|
136
695
|
provider;
|
|
@@ -140,19 +699,93 @@ class ChatService {
|
|
|
140
699
|
maxHistoryMessages;
|
|
141
700
|
onUsage;
|
|
142
701
|
tools;
|
|
702
|
+
thinkingLevel;
|
|
143
703
|
sendReasoning;
|
|
144
704
|
sendSources;
|
|
705
|
+
modelSelector;
|
|
145
706
|
constructor(config) {
|
|
146
707
|
this.provider = config.provider;
|
|
147
708
|
this.context = config.context;
|
|
148
709
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
149
|
-
this.systemPrompt = config
|
|
710
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
150
711
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
151
712
|
this.onUsage = config.onUsage;
|
|
152
|
-
this.tools = config
|
|
153
|
-
this.
|
|
713
|
+
this.tools = this.mergeTools(config);
|
|
714
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
715
|
+
this.modelSelector = config.modelSelector;
|
|
716
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
154
717
|
this.sendSources = config.sendSources ?? false;
|
|
155
718
|
}
|
|
719
|
+
buildSystemPrompt(config) {
|
|
720
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
721
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
722
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
723
|
+
}
|
|
724
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
725
|
+
if (contractsPrompt) {
|
|
726
|
+
base += contractsPrompt;
|
|
727
|
+
}
|
|
728
|
+
if (config.surfacePlanConfig?.plan) {
|
|
729
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
730
|
+
base += `
|
|
731
|
+
|
|
732
|
+
` + compilePlannerPrompt(plannerInput);
|
|
733
|
+
}
|
|
734
|
+
return base;
|
|
735
|
+
}
|
|
736
|
+
mergeTools(config) {
|
|
737
|
+
let merged = config.tools ?? {};
|
|
738
|
+
const wfConfig = config.workflowToolsConfig;
|
|
739
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
740
|
+
const workflowTools = createWorkflowTools({
|
|
741
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
742
|
+
composer: wfConfig.composer
|
|
743
|
+
});
|
|
744
|
+
merged = { ...merged, ...workflowTools };
|
|
745
|
+
}
|
|
746
|
+
const contractsCtx = config.contractsContext;
|
|
747
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
748
|
+
const allTools = [];
|
|
749
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
750
|
+
if (agent.tools?.length)
|
|
751
|
+
allTools.push(...agent.tools);
|
|
752
|
+
}
|
|
753
|
+
if (allTools.length > 0) {
|
|
754
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
755
|
+
merged = { ...merged, ...agentTools };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
759
|
+
if (surfaceConfig?.plan) {
|
|
760
|
+
const plannerTools = createSurfacePlannerTools({
|
|
761
|
+
plan: surfaceConfig.plan,
|
|
762
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
763
|
+
});
|
|
764
|
+
merged = { ...merged, ...plannerTools };
|
|
765
|
+
}
|
|
766
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
767
|
+
merged = { ...merged, ...config.mcpTools };
|
|
768
|
+
}
|
|
769
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
770
|
+
}
|
|
771
|
+
async resolveModel() {
|
|
772
|
+
if (this.modelSelector) {
|
|
773
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
774
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
775
|
+
taskDimension: dimension
|
|
776
|
+
});
|
|
777
|
+
return { model, providerName: selection.providerKey };
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
model: this.provider.getModel(),
|
|
781
|
+
providerName: this.provider.name
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
thinkingLevelToDimension(level) {
|
|
785
|
+
if (!level || level === "instant")
|
|
786
|
+
return "latency";
|
|
787
|
+
return "reasoning";
|
|
788
|
+
}
|
|
156
789
|
async send(options) {
|
|
157
790
|
let conversation;
|
|
158
791
|
if (options.conversationId) {
|
|
@@ -170,20 +803,25 @@ class ChatService {
|
|
|
170
803
|
workspacePath: this.context?.workspacePath
|
|
171
804
|
});
|
|
172
805
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
806
|
+
if (!options.skipUserAppend) {
|
|
807
|
+
await this.store.appendMessage(conversation.id, {
|
|
808
|
+
role: "user",
|
|
809
|
+
content: options.content,
|
|
810
|
+
status: "completed",
|
|
811
|
+
attachments: options.attachments
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
179
815
|
const messages = this.buildMessages(conversation, options);
|
|
180
|
-
const model = this.
|
|
816
|
+
const { model, providerName } = await this.resolveModel();
|
|
817
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
181
818
|
try {
|
|
182
819
|
const result = await generateText({
|
|
183
820
|
model,
|
|
184
821
|
messages,
|
|
185
822
|
system: this.systemPrompt,
|
|
186
|
-
tools: this.tools
|
|
823
|
+
tools: this.tools,
|
|
824
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
187
825
|
});
|
|
188
826
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
189
827
|
role: "assistant",
|
|
@@ -228,23 +866,27 @@ class ChatService {
|
|
|
228
866
|
workspacePath: this.context?.workspacePath
|
|
229
867
|
});
|
|
230
868
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
869
|
+
if (!options.skipUserAppend) {
|
|
870
|
+
await this.store.appendMessage(conversation.id, {
|
|
871
|
+
role: "user",
|
|
872
|
+
content: options.content,
|
|
873
|
+
status: "completed",
|
|
874
|
+
attachments: options.attachments
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
237
878
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
238
879
|
role: "assistant",
|
|
239
880
|
content: "",
|
|
240
881
|
status: "streaming"
|
|
241
882
|
});
|
|
242
883
|
const messages = this.buildMessages(conversation, options);
|
|
243
|
-
const model = this.
|
|
884
|
+
const { model, providerName } = await this.resolveModel();
|
|
244
885
|
const systemPrompt = this.systemPrompt;
|
|
245
886
|
const tools = this.tools;
|
|
246
887
|
const store = this.store;
|
|
247
888
|
const onUsage = this.onUsage;
|
|
889
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
248
890
|
async function* streamGenerator() {
|
|
249
891
|
let fullContent = "";
|
|
250
892
|
let fullReasoning = "";
|
|
@@ -255,7 +897,8 @@ class ChatService {
|
|
|
255
897
|
model,
|
|
256
898
|
messages,
|
|
257
899
|
system: systemPrompt,
|
|
258
|
-
tools
|
|
900
|
+
tools,
|
|
901
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
259
902
|
});
|
|
260
903
|
for await (const part of result.fullStream) {
|
|
261
904
|
if (part.type === "text-delta") {
|
|
@@ -370,6 +1013,18 @@ class ChatService {
|
|
|
370
1013
|
...options
|
|
371
1014
|
});
|
|
372
1015
|
}
|
|
1016
|
+
async updateConversation(conversationId, updates) {
|
|
1017
|
+
return this.store.update(conversationId, updates);
|
|
1018
|
+
}
|
|
1019
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
1020
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
1021
|
+
}
|
|
1022
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
1023
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
1024
|
+
}
|
|
1025
|
+
async truncateAfter(conversationId, messageId) {
|
|
1026
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
1027
|
+
}
|
|
373
1028
|
async deleteConversation(conversationId) {
|
|
374
1029
|
return this.store.delete(conversationId);
|
|
375
1030
|
}
|
|
@@ -440,9 +1095,9 @@ import {
|
|
|
440
1095
|
function toolsToToolSet(defs) {
|
|
441
1096
|
const result = {};
|
|
442
1097
|
for (const def of defs) {
|
|
443
|
-
result[def.name] =
|
|
1098
|
+
result[def.name] = tool4({
|
|
444
1099
|
description: def.description ?? def.name,
|
|
445
|
-
inputSchema:
|
|
1100
|
+
inputSchema: z4.object({}).passthrough(),
|
|
446
1101
|
execute: async () => ({})
|
|
447
1102
|
});
|
|
448
1103
|
}
|
|
@@ -456,21 +1111,63 @@ function useChat(options = {}) {
|
|
|
456
1111
|
apiKey,
|
|
457
1112
|
proxyUrl,
|
|
458
1113
|
conversationId: initialConversationId,
|
|
1114
|
+
store,
|
|
459
1115
|
systemPrompt,
|
|
460
1116
|
streaming = true,
|
|
461
1117
|
onSend,
|
|
462
1118
|
onResponse,
|
|
463
1119
|
onError,
|
|
464
1120
|
onUsage,
|
|
465
|
-
tools: toolsDefs
|
|
1121
|
+
tools: toolsDefs,
|
|
1122
|
+
thinkingLevel,
|
|
1123
|
+
workflowToolsConfig,
|
|
1124
|
+
modelSelector,
|
|
1125
|
+
contractsContext,
|
|
1126
|
+
surfacePlanConfig,
|
|
1127
|
+
mcpServers,
|
|
1128
|
+
agentMode
|
|
466
1129
|
} = options;
|
|
467
1130
|
const [messages, setMessages] = React.useState([]);
|
|
1131
|
+
const [mcpTools, setMcpTools] = React.useState(null);
|
|
1132
|
+
const mcpCleanupRef = React.useRef(null);
|
|
468
1133
|
const [conversation, setConversation] = React.useState(null);
|
|
469
1134
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
470
1135
|
const [error, setError] = React.useState(null);
|
|
471
1136
|
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
472
1137
|
const abortControllerRef = React.useRef(null);
|
|
473
1138
|
const chatServiceRef = React.useRef(null);
|
|
1139
|
+
React.useEffect(() => {
|
|
1140
|
+
if (!mcpServers?.length) {
|
|
1141
|
+
setMcpTools(null);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
let cancelled = false;
|
|
1145
|
+
import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
|
|
1146
|
+
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
1147
|
+
if (!cancelled) {
|
|
1148
|
+
setMcpTools(tools);
|
|
1149
|
+
mcpCleanupRef.current = cleanup;
|
|
1150
|
+
} else {
|
|
1151
|
+
cleanup().catch(() => {
|
|
1152
|
+
return;
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
}).catch(() => {
|
|
1156
|
+
if (!cancelled)
|
|
1157
|
+
setMcpTools(null);
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
return () => {
|
|
1161
|
+
cancelled = true;
|
|
1162
|
+
const cleanup = mcpCleanupRef.current;
|
|
1163
|
+
mcpCleanupRef.current = null;
|
|
1164
|
+
if (cleanup)
|
|
1165
|
+
cleanup().catch(() => {
|
|
1166
|
+
return;
|
|
1167
|
+
});
|
|
1168
|
+
setMcpTools(null);
|
|
1169
|
+
};
|
|
1170
|
+
}, [mcpServers]);
|
|
474
1171
|
React.useEffect(() => {
|
|
475
1172
|
const chatProvider = createProvider({
|
|
476
1173
|
provider,
|
|
@@ -480,9 +1177,16 @@ function useChat(options = {}) {
|
|
|
480
1177
|
});
|
|
481
1178
|
chatServiceRef.current = new ChatService({
|
|
482
1179
|
provider: chatProvider,
|
|
1180
|
+
store,
|
|
483
1181
|
systemPrompt,
|
|
484
1182
|
onUsage,
|
|
485
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
1183
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
1184
|
+
thinkingLevel,
|
|
1185
|
+
workflowToolsConfig,
|
|
1186
|
+
modelSelector,
|
|
1187
|
+
contractsContext,
|
|
1188
|
+
surfacePlanConfig,
|
|
1189
|
+
mcpTools
|
|
486
1190
|
});
|
|
487
1191
|
}, [
|
|
488
1192
|
provider,
|
|
@@ -490,9 +1194,16 @@ function useChat(options = {}) {
|
|
|
490
1194
|
model,
|
|
491
1195
|
apiKey,
|
|
492
1196
|
proxyUrl,
|
|
1197
|
+
store,
|
|
493
1198
|
systemPrompt,
|
|
494
1199
|
onUsage,
|
|
495
|
-
toolsDefs
|
|
1200
|
+
toolsDefs,
|
|
1201
|
+
thinkingLevel,
|
|
1202
|
+
workflowToolsConfig,
|
|
1203
|
+
modelSelector,
|
|
1204
|
+
contractsContext,
|
|
1205
|
+
surfacePlanConfig,
|
|
1206
|
+
mcpTools
|
|
496
1207
|
]);
|
|
497
1208
|
React.useEffect(() => {
|
|
498
1209
|
if (!conversationId || !chatServiceRef.current)
|
|
@@ -508,7 +1219,90 @@ function useChat(options = {}) {
|
|
|
508
1219
|
};
|
|
509
1220
|
loadConversation().catch(console.error);
|
|
510
1221
|
}, [conversationId]);
|
|
511
|
-
const sendMessage = React.useCallback(async (content, attachments) => {
|
|
1222
|
+
const sendMessage = React.useCallback(async (content, attachments, opts) => {
|
|
1223
|
+
if (agentMode?.agent) {
|
|
1224
|
+
setIsLoading(true);
|
|
1225
|
+
setError(null);
|
|
1226
|
+
abortControllerRef.current = new AbortController;
|
|
1227
|
+
try {
|
|
1228
|
+
if (!opts?.skipUserAppend) {
|
|
1229
|
+
const userMessage = {
|
|
1230
|
+
id: `msg_${Date.now()}`,
|
|
1231
|
+
conversationId: conversationId ?? "",
|
|
1232
|
+
role: "user",
|
|
1233
|
+
content,
|
|
1234
|
+
status: "completed",
|
|
1235
|
+
createdAt: new Date,
|
|
1236
|
+
updatedAt: new Date,
|
|
1237
|
+
attachments
|
|
1238
|
+
};
|
|
1239
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1240
|
+
onSend?.(userMessage);
|
|
1241
|
+
}
|
|
1242
|
+
const result = await agentMode.agent.generate({
|
|
1243
|
+
prompt: content,
|
|
1244
|
+
signal: abortControllerRef.current.signal
|
|
1245
|
+
});
|
|
1246
|
+
const toolCallsMap = new Map;
|
|
1247
|
+
for (const tc of result.toolCalls ?? []) {
|
|
1248
|
+
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
1249
|
+
toolCallsMap.set(tc.toolCallId, {
|
|
1250
|
+
id: tc.toolCallId,
|
|
1251
|
+
name: tc.toolName,
|
|
1252
|
+
args: tc.args ?? {},
|
|
1253
|
+
result: tr?.output,
|
|
1254
|
+
status: "completed"
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
const assistantMessage = {
|
|
1258
|
+
id: `msg_${Date.now()}_a`,
|
|
1259
|
+
conversationId: conversationId ?? "",
|
|
1260
|
+
role: "assistant",
|
|
1261
|
+
content: result.text,
|
|
1262
|
+
status: "completed",
|
|
1263
|
+
createdAt: new Date,
|
|
1264
|
+
updatedAt: new Date,
|
|
1265
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1266
|
+
usage: result.usage
|
|
1267
|
+
};
|
|
1268
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
1269
|
+
onResponse?.(assistantMessage);
|
|
1270
|
+
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
1271
|
+
if (store && !conversationId) {
|
|
1272
|
+
const conv = await store.create({
|
|
1273
|
+
status: "active",
|
|
1274
|
+
provider: "agent",
|
|
1275
|
+
model: "agent",
|
|
1276
|
+
messages: []
|
|
1277
|
+
});
|
|
1278
|
+
if (!opts?.skipUserAppend) {
|
|
1279
|
+
await store.appendMessage(conv.id, {
|
|
1280
|
+
role: "user",
|
|
1281
|
+
content,
|
|
1282
|
+
status: "completed",
|
|
1283
|
+
attachments
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
await store.appendMessage(conv.id, {
|
|
1287
|
+
role: "assistant",
|
|
1288
|
+
content: result.text,
|
|
1289
|
+
status: "completed",
|
|
1290
|
+
toolCalls: assistantMessage.toolCalls,
|
|
1291
|
+
usage: result.usage
|
|
1292
|
+
});
|
|
1293
|
+
const updated = await store.get(conv.id);
|
|
1294
|
+
if (updated)
|
|
1295
|
+
setConversation(updated);
|
|
1296
|
+
setConversationId(conv.id);
|
|
1297
|
+
}
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1300
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1301
|
+
} finally {
|
|
1302
|
+
setIsLoading(false);
|
|
1303
|
+
}
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
512
1306
|
if (!chatServiceRef.current) {
|
|
513
1307
|
throw new Error("Chat service not initialized");
|
|
514
1308
|
}
|
|
@@ -516,25 +1310,28 @@ function useChat(options = {}) {
|
|
|
516
1310
|
setError(null);
|
|
517
1311
|
abortControllerRef.current = new AbortController;
|
|
518
1312
|
try {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1313
|
+
if (!opts?.skipUserAppend) {
|
|
1314
|
+
const userMessage = {
|
|
1315
|
+
id: `msg_${Date.now()}`,
|
|
1316
|
+
conversationId: conversationId ?? "",
|
|
1317
|
+
role: "user",
|
|
1318
|
+
content,
|
|
1319
|
+
status: "completed",
|
|
1320
|
+
createdAt: new Date,
|
|
1321
|
+
updatedAt: new Date,
|
|
1322
|
+
attachments
|
|
1323
|
+
};
|
|
1324
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1325
|
+
onSend?.(userMessage);
|
|
1326
|
+
}
|
|
531
1327
|
if (streaming) {
|
|
532
1328
|
const result = await chatServiceRef.current.stream({
|
|
533
1329
|
conversationId: conversationId ?? undefined,
|
|
534
1330
|
content,
|
|
535
|
-
attachments
|
|
1331
|
+
attachments,
|
|
1332
|
+
skipUserAppend: opts?.skipUserAppend
|
|
536
1333
|
});
|
|
537
|
-
if (!conversationId) {
|
|
1334
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
538
1335
|
setConversationId(result.conversationId);
|
|
539
1336
|
}
|
|
540
1337
|
const assistantMessage = {
|
|
@@ -615,7 +1412,8 @@ function useChat(options = {}) {
|
|
|
615
1412
|
const result = await chatServiceRef.current.send({
|
|
616
1413
|
conversationId: conversationId ?? undefined,
|
|
617
1414
|
content,
|
|
618
|
-
attachments
|
|
1415
|
+
attachments,
|
|
1416
|
+
skipUserAppend: opts?.skipUserAppend
|
|
619
1417
|
});
|
|
620
1418
|
setConversation(result.conversation);
|
|
621
1419
|
setMessages(result.conversation.messages);
|
|
@@ -632,7 +1430,17 @@ function useChat(options = {}) {
|
|
|
632
1430
|
setIsLoading(false);
|
|
633
1431
|
abortControllerRef.current = null;
|
|
634
1432
|
}
|
|
635
|
-
}, [
|
|
1433
|
+
}, [
|
|
1434
|
+
conversationId,
|
|
1435
|
+
streaming,
|
|
1436
|
+
onSend,
|
|
1437
|
+
onResponse,
|
|
1438
|
+
onError,
|
|
1439
|
+
onUsage,
|
|
1440
|
+
messages,
|
|
1441
|
+
agentMode,
|
|
1442
|
+
store
|
|
1443
|
+
]);
|
|
636
1444
|
const clearConversation = React.useCallback(() => {
|
|
637
1445
|
setMessages([]);
|
|
638
1446
|
setConversation(null);
|
|
@@ -653,6 +1461,46 @@ function useChat(options = {}) {
|
|
|
653
1461
|
abortControllerRef.current?.abort();
|
|
654
1462
|
setIsLoading(false);
|
|
655
1463
|
}, []);
|
|
1464
|
+
const createNewConversation = clearConversation;
|
|
1465
|
+
const editMessage = React.useCallback(async (messageId, newContent) => {
|
|
1466
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1467
|
+
return;
|
|
1468
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
1469
|
+
if (!msg || msg.role !== "user")
|
|
1470
|
+
return;
|
|
1471
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, {
|
|
1472
|
+
content: newContent
|
|
1473
|
+
});
|
|
1474
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
1475
|
+
if (truncated) {
|
|
1476
|
+
setMessages(truncated.messages);
|
|
1477
|
+
}
|
|
1478
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
1479
|
+
}, [conversationId, messages, sendMessage]);
|
|
1480
|
+
const forkConversation = React.useCallback(async (upToMessageId) => {
|
|
1481
|
+
if (!chatServiceRef.current)
|
|
1482
|
+
return null;
|
|
1483
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
1484
|
+
if (!idToFork)
|
|
1485
|
+
return null;
|
|
1486
|
+
try {
|
|
1487
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
1488
|
+
setConversationId(forked.id);
|
|
1489
|
+
setConversation(forked);
|
|
1490
|
+
setMessages(forked.messages);
|
|
1491
|
+
return forked.id;
|
|
1492
|
+
} catch {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
}, [conversationId, conversation]);
|
|
1496
|
+
const updateConversationFn = React.useCallback(async (updates) => {
|
|
1497
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1498
|
+
return null;
|
|
1499
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
1500
|
+
if (updated)
|
|
1501
|
+
setConversation(updated);
|
|
1502
|
+
return updated;
|
|
1503
|
+
}, [conversationId]);
|
|
656
1504
|
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
657
1505
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
658
1506
|
}, []);
|
|
@@ -667,6 +1515,10 @@ function useChat(options = {}) {
|
|
|
667
1515
|
setConversationId,
|
|
668
1516
|
regenerate,
|
|
669
1517
|
stop,
|
|
1518
|
+
createNewConversation,
|
|
1519
|
+
editMessage,
|
|
1520
|
+
forkConversation,
|
|
1521
|
+
updateConversation: updateConversationFn,
|
|
670
1522
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
671
1523
|
};
|
|
672
1524
|
}
|
|
@@ -710,11 +1562,94 @@ function useProviders() {
|
|
|
710
1562
|
refresh: loadProviders
|
|
711
1563
|
};
|
|
712
1564
|
}
|
|
1565
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1566
|
+
import * as React3 from "react";
|
|
1567
|
+
"use client";
|
|
1568
|
+
function useMessageSelection(messageIds) {
|
|
1569
|
+
const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
|
|
1570
|
+
const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1571
|
+
React3.useEffect(() => {
|
|
1572
|
+
setSelectedIds((prev) => {
|
|
1573
|
+
const next = new Set;
|
|
1574
|
+
for (const id of prev) {
|
|
1575
|
+
if (idSet.has(id))
|
|
1576
|
+
next.add(id);
|
|
1577
|
+
}
|
|
1578
|
+
return next.size === prev.size ? prev : next;
|
|
1579
|
+
});
|
|
1580
|
+
}, [idSet]);
|
|
1581
|
+
const toggle = React3.useCallback((id) => {
|
|
1582
|
+
setSelectedIds((prev) => {
|
|
1583
|
+
const next = new Set(prev);
|
|
1584
|
+
if (next.has(id))
|
|
1585
|
+
next.delete(id);
|
|
1586
|
+
else
|
|
1587
|
+
next.add(id);
|
|
1588
|
+
return next;
|
|
1589
|
+
});
|
|
1590
|
+
}, []);
|
|
1591
|
+
const selectAll = React3.useCallback(() => {
|
|
1592
|
+
setSelectedIds(new Set(messageIds));
|
|
1593
|
+
}, [messageIds.join(",")]);
|
|
1594
|
+
const clearSelection = React3.useCallback(() => {
|
|
1595
|
+
setSelectedIds(new Set);
|
|
1596
|
+
}, []);
|
|
1597
|
+
const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1598
|
+
const selectedCount = selectedIds.size;
|
|
1599
|
+
return {
|
|
1600
|
+
selectedIds,
|
|
1601
|
+
toggle,
|
|
1602
|
+
selectAll,
|
|
1603
|
+
clearSelection,
|
|
1604
|
+
isSelected,
|
|
1605
|
+
selectedCount
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
// src/presentation/hooks/useConversations.ts
|
|
1609
|
+
import * as React4 from "react";
|
|
1610
|
+
"use client";
|
|
1611
|
+
function useConversations(options) {
|
|
1612
|
+
const { store, projectId, tags, limit = 50 } = options;
|
|
1613
|
+
const [conversations, setConversations] = React4.useState([]);
|
|
1614
|
+
const [isLoading, setIsLoading] = React4.useState(true);
|
|
1615
|
+
const refresh = React4.useCallback(async () => {
|
|
1616
|
+
setIsLoading(true);
|
|
1617
|
+
try {
|
|
1618
|
+
const list = await store.list({
|
|
1619
|
+
status: "active",
|
|
1620
|
+
projectId,
|
|
1621
|
+
tags,
|
|
1622
|
+
limit
|
|
1623
|
+
});
|
|
1624
|
+
setConversations(list);
|
|
1625
|
+
} finally {
|
|
1626
|
+
setIsLoading(false);
|
|
1627
|
+
}
|
|
1628
|
+
}, [store, projectId, tags, limit]);
|
|
1629
|
+
React4.useEffect(() => {
|
|
1630
|
+
refresh();
|
|
1631
|
+
}, [refresh]);
|
|
1632
|
+
const deleteConversation = React4.useCallback(async (id) => {
|
|
1633
|
+
const ok = await store.delete(id);
|
|
1634
|
+
if (ok) {
|
|
1635
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1636
|
+
}
|
|
1637
|
+
return ok;
|
|
1638
|
+
}, [store]);
|
|
1639
|
+
return {
|
|
1640
|
+
conversations,
|
|
1641
|
+
isLoading,
|
|
1642
|
+
refresh,
|
|
1643
|
+
deleteConversation
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
713
1646
|
|
|
714
1647
|
// src/presentation/hooks/index.ts
|
|
715
1648
|
import { useCompletion } from "@ai-sdk/react";
|
|
716
1649
|
export {
|
|
717
1650
|
useProviders,
|
|
1651
|
+
useMessageSelection,
|
|
1652
|
+
useConversations,
|
|
718
1653
|
useCompletion,
|
|
719
1654
|
useChat
|
|
720
1655
|
};
|