@contractspec/module.ai-chat 4.0.2 → 4.1.0
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 +1138 -21
- package/dist/browser/index.js +2816 -651
- package/dist/browser/presentation/components/index.js +3143 -358
- package/dist/browser/presentation/hooks/index.js +961 -43
- package/dist/browser/presentation/index.js +2784 -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 +1138 -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 +2816 -651
- package/dist/node/core/index.js +1138 -21
- package/dist/node/index.js +2816 -651
- package/dist/node/presentation/components/index.js +3143 -358
- package/dist/node/presentation/hooks/index.js +961 -43
- package/dist/node/presentation/index.js +2787 -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 +3143 -358
- package/dist/presentation/hooks/index.d.ts +2 -0
- package/dist/presentation/hooks/index.js +961 -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 +2787 -669
- package/package.json +14 -18
|
@@ -3,8 +3,8 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
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,504 @@ 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 { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
521
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
522
|
+
var VALID_OPS = [
|
|
523
|
+
"insert-node",
|
|
524
|
+
"replace-node",
|
|
525
|
+
"remove-node",
|
|
526
|
+
"move-node",
|
|
527
|
+
"resize-panel",
|
|
528
|
+
"set-layout",
|
|
529
|
+
"reveal-field",
|
|
530
|
+
"hide-field",
|
|
531
|
+
"promote-action",
|
|
532
|
+
"set-focus"
|
|
533
|
+
];
|
|
534
|
+
var DEFAULT_NODE_KINDS = [
|
|
535
|
+
"entity-section",
|
|
536
|
+
"entity-card",
|
|
537
|
+
"data-view",
|
|
538
|
+
"assistant-panel",
|
|
539
|
+
"chat-thread",
|
|
540
|
+
"action-bar",
|
|
541
|
+
"timeline",
|
|
542
|
+
"table",
|
|
543
|
+
"rich-doc",
|
|
544
|
+
"form",
|
|
545
|
+
"chart",
|
|
546
|
+
"custom-widget"
|
|
547
|
+
];
|
|
548
|
+
function collectSlotIdsFromRegion(node) {
|
|
549
|
+
const ids = [];
|
|
550
|
+
if (node.type === "slot") {
|
|
551
|
+
ids.push(node.slotId);
|
|
552
|
+
}
|
|
553
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
554
|
+
for (const child of node.children) {
|
|
555
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (node.type === "tabs") {
|
|
559
|
+
for (const tab of node.tabs) {
|
|
560
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (node.type === "floating") {
|
|
564
|
+
ids.push(node.anchorSlotId);
|
|
565
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
566
|
+
}
|
|
567
|
+
return ids;
|
|
568
|
+
}
|
|
569
|
+
function deriveConstraints(plan) {
|
|
570
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
571
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
572
|
+
return {
|
|
573
|
+
allowedOps: VALID_OPS,
|
|
574
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
575
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
var ProposePatchInputSchema = z3.object({
|
|
579
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
580
|
+
ops: z3.array(z3.object({
|
|
581
|
+
op: z3.enum([
|
|
582
|
+
"insert-node",
|
|
583
|
+
"replace-node",
|
|
584
|
+
"remove-node",
|
|
585
|
+
"move-node",
|
|
586
|
+
"resize-panel",
|
|
587
|
+
"set-layout",
|
|
588
|
+
"reveal-field",
|
|
589
|
+
"hide-field",
|
|
590
|
+
"promote-action",
|
|
591
|
+
"set-focus"
|
|
592
|
+
]),
|
|
593
|
+
slotId: z3.string().optional(),
|
|
594
|
+
nodeId: z3.string().optional(),
|
|
595
|
+
toSlotId: z3.string().optional(),
|
|
596
|
+
index: z3.number().optional(),
|
|
597
|
+
node: z3.object({
|
|
598
|
+
nodeId: z3.string(),
|
|
599
|
+
kind: z3.string(),
|
|
600
|
+
title: z3.string().optional(),
|
|
601
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
602
|
+
children: z3.array(z3.unknown()).optional()
|
|
603
|
+
}).optional(),
|
|
604
|
+
persistKey: z3.string().optional(),
|
|
605
|
+
sizes: z3.array(z3.number()).optional(),
|
|
606
|
+
layoutId: z3.string().optional(),
|
|
607
|
+
fieldId: z3.string().optional(),
|
|
608
|
+
actionId: z3.string().optional(),
|
|
609
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
610
|
+
targetId: z3.string().optional()
|
|
611
|
+
}))
|
|
612
|
+
});
|
|
613
|
+
function createSurfacePlannerTools(config) {
|
|
614
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
615
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
616
|
+
const proposePatchTool = tool3({
|
|
617
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
618
|
+
inputSchema: ProposePatchInputSchema,
|
|
619
|
+
execute: async (input) => {
|
|
620
|
+
const ops = input.ops;
|
|
621
|
+
try {
|
|
622
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
623
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
624
|
+
onPatchProposal?.(proposal);
|
|
625
|
+
return {
|
|
626
|
+
success: true,
|
|
627
|
+
proposalId: proposal.proposalId,
|
|
628
|
+
opsCount: proposal.ops.length,
|
|
629
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
630
|
+
};
|
|
631
|
+
} catch (err) {
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
error: err instanceof Error ? err.message : String(err),
|
|
635
|
+
proposalId: input.proposalId
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
"propose-patch": proposePatchTool
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function buildPlannerPromptInput(plan) {
|
|
645
|
+
const constraints = deriveConstraints(plan);
|
|
646
|
+
return {
|
|
647
|
+
bundleMeta: {
|
|
648
|
+
key: plan.bundleKey,
|
|
649
|
+
version: "0.0.0",
|
|
650
|
+
title: plan.bundleKey
|
|
651
|
+
},
|
|
652
|
+
surfaceId: plan.surfaceId,
|
|
653
|
+
allowedPatchOps: constraints.allowedOps,
|
|
654
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
655
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
656
|
+
actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
|
|
657
|
+
preferences: {
|
|
658
|
+
guidance: "hints",
|
|
659
|
+
density: "standard",
|
|
660
|
+
dataDepth: "detailed",
|
|
661
|
+
control: "standard",
|
|
662
|
+
media: "text",
|
|
663
|
+
pace: "balanced",
|
|
664
|
+
narrative: "top-down"
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
119
669
|
// src/core/chat-service.ts
|
|
670
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
120
671
|
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
121
672
|
|
|
122
673
|
Your capabilities:
|
|
@@ -131,6 +682,9 @@ Guidelines:
|
|
|
131
682
|
- Reference relevant ContractSpec concepts and patterns
|
|
132
683
|
- Ask clarifying questions when the user's intent is unclear
|
|
133
684
|
- When suggesting code changes, explain the rationale`;
|
|
685
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
686
|
+
|
|
687
|
+
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
688
|
|
|
135
689
|
class ChatService {
|
|
136
690
|
provider;
|
|
@@ -140,19 +694,93 @@ class ChatService {
|
|
|
140
694
|
maxHistoryMessages;
|
|
141
695
|
onUsage;
|
|
142
696
|
tools;
|
|
697
|
+
thinkingLevel;
|
|
143
698
|
sendReasoning;
|
|
144
699
|
sendSources;
|
|
700
|
+
modelSelector;
|
|
145
701
|
constructor(config) {
|
|
146
702
|
this.provider = config.provider;
|
|
147
703
|
this.context = config.context;
|
|
148
704
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
149
|
-
this.systemPrompt = config
|
|
705
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
150
706
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
151
707
|
this.onUsage = config.onUsage;
|
|
152
|
-
this.tools = config
|
|
153
|
-
this.
|
|
708
|
+
this.tools = this.mergeTools(config);
|
|
709
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
710
|
+
this.modelSelector = config.modelSelector;
|
|
711
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
154
712
|
this.sendSources = config.sendSources ?? false;
|
|
155
713
|
}
|
|
714
|
+
buildSystemPrompt(config) {
|
|
715
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
716
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
717
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
718
|
+
}
|
|
719
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
720
|
+
if (contractsPrompt) {
|
|
721
|
+
base += contractsPrompt;
|
|
722
|
+
}
|
|
723
|
+
if (config.surfacePlanConfig?.plan) {
|
|
724
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
725
|
+
base += `
|
|
726
|
+
|
|
727
|
+
` + compilePlannerPrompt(plannerInput);
|
|
728
|
+
}
|
|
729
|
+
return base;
|
|
730
|
+
}
|
|
731
|
+
mergeTools(config) {
|
|
732
|
+
let merged = config.tools ?? {};
|
|
733
|
+
const wfConfig = config.workflowToolsConfig;
|
|
734
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
735
|
+
const workflowTools = createWorkflowTools({
|
|
736
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
737
|
+
composer: wfConfig.composer
|
|
738
|
+
});
|
|
739
|
+
merged = { ...merged, ...workflowTools };
|
|
740
|
+
}
|
|
741
|
+
const contractsCtx = config.contractsContext;
|
|
742
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
743
|
+
const allTools = [];
|
|
744
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
745
|
+
if (agent.tools?.length)
|
|
746
|
+
allTools.push(...agent.tools);
|
|
747
|
+
}
|
|
748
|
+
if (allTools.length > 0) {
|
|
749
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
750
|
+
merged = { ...merged, ...agentTools };
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
754
|
+
if (surfaceConfig?.plan) {
|
|
755
|
+
const plannerTools = createSurfacePlannerTools({
|
|
756
|
+
plan: surfaceConfig.plan,
|
|
757
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
758
|
+
});
|
|
759
|
+
merged = { ...merged, ...plannerTools };
|
|
760
|
+
}
|
|
761
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
762
|
+
merged = { ...merged, ...config.mcpTools };
|
|
763
|
+
}
|
|
764
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
765
|
+
}
|
|
766
|
+
async resolveModel() {
|
|
767
|
+
if (this.modelSelector) {
|
|
768
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
769
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
770
|
+
taskDimension: dimension
|
|
771
|
+
});
|
|
772
|
+
return { model, providerName: selection.providerKey };
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
model: this.provider.getModel(),
|
|
776
|
+
providerName: this.provider.name
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
thinkingLevelToDimension(level) {
|
|
780
|
+
if (!level || level === "instant")
|
|
781
|
+
return "latency";
|
|
782
|
+
return "reasoning";
|
|
783
|
+
}
|
|
156
784
|
async send(options) {
|
|
157
785
|
let conversation;
|
|
158
786
|
if (options.conversationId) {
|
|
@@ -170,20 +798,25 @@ class ChatService {
|
|
|
170
798
|
workspacePath: this.context?.workspacePath
|
|
171
799
|
});
|
|
172
800
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
801
|
+
if (!options.skipUserAppend) {
|
|
802
|
+
await this.store.appendMessage(conversation.id, {
|
|
803
|
+
role: "user",
|
|
804
|
+
content: options.content,
|
|
805
|
+
status: "completed",
|
|
806
|
+
attachments: options.attachments
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
179
810
|
const messages = this.buildMessages(conversation, options);
|
|
180
|
-
const model = this.
|
|
811
|
+
const { model, providerName } = await this.resolveModel();
|
|
812
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
181
813
|
try {
|
|
182
814
|
const result = await generateText({
|
|
183
815
|
model,
|
|
184
816
|
messages,
|
|
185
817
|
system: this.systemPrompt,
|
|
186
|
-
tools: this.tools
|
|
818
|
+
tools: this.tools,
|
|
819
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
187
820
|
});
|
|
188
821
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
189
822
|
role: "assistant",
|
|
@@ -228,23 +861,27 @@ class ChatService {
|
|
|
228
861
|
workspacePath: this.context?.workspacePath
|
|
229
862
|
});
|
|
230
863
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
864
|
+
if (!options.skipUserAppend) {
|
|
865
|
+
await this.store.appendMessage(conversation.id, {
|
|
866
|
+
role: "user",
|
|
867
|
+
content: options.content,
|
|
868
|
+
status: "completed",
|
|
869
|
+
attachments: options.attachments
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
237
873
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
238
874
|
role: "assistant",
|
|
239
875
|
content: "",
|
|
240
876
|
status: "streaming"
|
|
241
877
|
});
|
|
242
878
|
const messages = this.buildMessages(conversation, options);
|
|
243
|
-
const model = this.
|
|
879
|
+
const { model, providerName } = await this.resolveModel();
|
|
244
880
|
const systemPrompt = this.systemPrompt;
|
|
245
881
|
const tools = this.tools;
|
|
246
882
|
const store = this.store;
|
|
247
883
|
const onUsage = this.onUsage;
|
|
884
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
248
885
|
async function* streamGenerator() {
|
|
249
886
|
let fullContent = "";
|
|
250
887
|
let fullReasoning = "";
|
|
@@ -255,7 +892,8 @@ class ChatService {
|
|
|
255
892
|
model,
|
|
256
893
|
messages,
|
|
257
894
|
system: systemPrompt,
|
|
258
|
-
tools
|
|
895
|
+
tools,
|
|
896
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
259
897
|
});
|
|
260
898
|
for await (const part of result.fullStream) {
|
|
261
899
|
if (part.type === "text-delta") {
|
|
@@ -370,6 +1008,18 @@ class ChatService {
|
|
|
370
1008
|
...options
|
|
371
1009
|
});
|
|
372
1010
|
}
|
|
1011
|
+
async updateConversation(conversationId, updates) {
|
|
1012
|
+
return this.store.update(conversationId, updates);
|
|
1013
|
+
}
|
|
1014
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
1015
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
1016
|
+
}
|
|
1017
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
1018
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
1019
|
+
}
|
|
1020
|
+
async truncateAfter(conversationId, messageId) {
|
|
1021
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
1022
|
+
}
|
|
373
1023
|
async deleteConversation(conversationId) {
|
|
374
1024
|
return this.store.delete(conversationId);
|
|
375
1025
|
}
|
|
@@ -440,9 +1090,9 @@ import {
|
|
|
440
1090
|
function toolsToToolSet(defs) {
|
|
441
1091
|
const result = {};
|
|
442
1092
|
for (const def of defs) {
|
|
443
|
-
result[def.name] =
|
|
1093
|
+
result[def.name] = tool4({
|
|
444
1094
|
description: def.description ?? def.name,
|
|
445
|
-
inputSchema:
|
|
1095
|
+
inputSchema: z4.object({}).passthrough(),
|
|
446
1096
|
execute: async () => ({})
|
|
447
1097
|
});
|
|
448
1098
|
}
|
|
@@ -456,21 +1106,63 @@ function useChat(options = {}) {
|
|
|
456
1106
|
apiKey,
|
|
457
1107
|
proxyUrl,
|
|
458
1108
|
conversationId: initialConversationId,
|
|
1109
|
+
store,
|
|
459
1110
|
systemPrompt,
|
|
460
1111
|
streaming = true,
|
|
461
1112
|
onSend,
|
|
462
1113
|
onResponse,
|
|
463
1114
|
onError,
|
|
464
1115
|
onUsage,
|
|
465
|
-
tools: toolsDefs
|
|
1116
|
+
tools: toolsDefs,
|
|
1117
|
+
thinkingLevel,
|
|
1118
|
+
workflowToolsConfig,
|
|
1119
|
+
modelSelector,
|
|
1120
|
+
contractsContext,
|
|
1121
|
+
surfacePlanConfig,
|
|
1122
|
+
mcpServers,
|
|
1123
|
+
agentMode
|
|
466
1124
|
} = options;
|
|
467
1125
|
const [messages, setMessages] = React.useState([]);
|
|
1126
|
+
const [mcpTools, setMcpTools] = React.useState(null);
|
|
1127
|
+
const mcpCleanupRef = React.useRef(null);
|
|
468
1128
|
const [conversation, setConversation] = React.useState(null);
|
|
469
1129
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
470
1130
|
const [error, setError] = React.useState(null);
|
|
471
1131
|
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
472
1132
|
const abortControllerRef = React.useRef(null);
|
|
473
1133
|
const chatServiceRef = React.useRef(null);
|
|
1134
|
+
React.useEffect(() => {
|
|
1135
|
+
if (!mcpServers?.length) {
|
|
1136
|
+
setMcpTools(null);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
let cancelled = false;
|
|
1140
|
+
import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
|
|
1141
|
+
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
1142
|
+
if (!cancelled) {
|
|
1143
|
+
setMcpTools(tools);
|
|
1144
|
+
mcpCleanupRef.current = cleanup;
|
|
1145
|
+
} else {
|
|
1146
|
+
cleanup().catch(() => {
|
|
1147
|
+
return;
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}).catch(() => {
|
|
1151
|
+
if (!cancelled)
|
|
1152
|
+
setMcpTools(null);
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
return () => {
|
|
1156
|
+
cancelled = true;
|
|
1157
|
+
const cleanup = mcpCleanupRef.current;
|
|
1158
|
+
mcpCleanupRef.current = null;
|
|
1159
|
+
if (cleanup)
|
|
1160
|
+
cleanup().catch(() => {
|
|
1161
|
+
return;
|
|
1162
|
+
});
|
|
1163
|
+
setMcpTools(null);
|
|
1164
|
+
};
|
|
1165
|
+
}, [mcpServers]);
|
|
474
1166
|
React.useEffect(() => {
|
|
475
1167
|
const chatProvider = createProvider({
|
|
476
1168
|
provider,
|
|
@@ -480,9 +1172,16 @@ function useChat(options = {}) {
|
|
|
480
1172
|
});
|
|
481
1173
|
chatServiceRef.current = new ChatService({
|
|
482
1174
|
provider: chatProvider,
|
|
1175
|
+
store,
|
|
483
1176
|
systemPrompt,
|
|
484
1177
|
onUsage,
|
|
485
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
1178
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
1179
|
+
thinkingLevel,
|
|
1180
|
+
workflowToolsConfig,
|
|
1181
|
+
modelSelector,
|
|
1182
|
+
contractsContext,
|
|
1183
|
+
surfacePlanConfig,
|
|
1184
|
+
mcpTools
|
|
486
1185
|
});
|
|
487
1186
|
}, [
|
|
488
1187
|
provider,
|
|
@@ -490,9 +1189,16 @@ function useChat(options = {}) {
|
|
|
490
1189
|
model,
|
|
491
1190
|
apiKey,
|
|
492
1191
|
proxyUrl,
|
|
1192
|
+
store,
|
|
493
1193
|
systemPrompt,
|
|
494
1194
|
onUsage,
|
|
495
|
-
toolsDefs
|
|
1195
|
+
toolsDefs,
|
|
1196
|
+
thinkingLevel,
|
|
1197
|
+
workflowToolsConfig,
|
|
1198
|
+
modelSelector,
|
|
1199
|
+
contractsContext,
|
|
1200
|
+
surfacePlanConfig,
|
|
1201
|
+
mcpTools
|
|
496
1202
|
]);
|
|
497
1203
|
React.useEffect(() => {
|
|
498
1204
|
if (!conversationId || !chatServiceRef.current)
|
|
@@ -508,7 +1214,90 @@ function useChat(options = {}) {
|
|
|
508
1214
|
};
|
|
509
1215
|
loadConversation().catch(console.error);
|
|
510
1216
|
}, [conversationId]);
|
|
511
|
-
const sendMessage = React.useCallback(async (content, attachments) => {
|
|
1217
|
+
const sendMessage = React.useCallback(async (content, attachments, opts) => {
|
|
1218
|
+
if (agentMode?.agent) {
|
|
1219
|
+
setIsLoading(true);
|
|
1220
|
+
setError(null);
|
|
1221
|
+
abortControllerRef.current = new AbortController;
|
|
1222
|
+
try {
|
|
1223
|
+
if (!opts?.skipUserAppend) {
|
|
1224
|
+
const userMessage = {
|
|
1225
|
+
id: `msg_${Date.now()}`,
|
|
1226
|
+
conversationId: conversationId ?? "",
|
|
1227
|
+
role: "user",
|
|
1228
|
+
content,
|
|
1229
|
+
status: "completed",
|
|
1230
|
+
createdAt: new Date,
|
|
1231
|
+
updatedAt: new Date,
|
|
1232
|
+
attachments
|
|
1233
|
+
};
|
|
1234
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1235
|
+
onSend?.(userMessage);
|
|
1236
|
+
}
|
|
1237
|
+
const result = await agentMode.agent.generate({
|
|
1238
|
+
prompt: content,
|
|
1239
|
+
signal: abortControllerRef.current.signal
|
|
1240
|
+
});
|
|
1241
|
+
const toolCallsMap = new Map;
|
|
1242
|
+
for (const tc of result.toolCalls ?? []) {
|
|
1243
|
+
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
1244
|
+
toolCallsMap.set(tc.toolCallId, {
|
|
1245
|
+
id: tc.toolCallId,
|
|
1246
|
+
name: tc.toolName,
|
|
1247
|
+
args: tc.args ?? {},
|
|
1248
|
+
result: tr?.output,
|
|
1249
|
+
status: "completed"
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
const assistantMessage = {
|
|
1253
|
+
id: `msg_${Date.now()}_a`,
|
|
1254
|
+
conversationId: conversationId ?? "",
|
|
1255
|
+
role: "assistant",
|
|
1256
|
+
content: result.text,
|
|
1257
|
+
status: "completed",
|
|
1258
|
+
createdAt: new Date,
|
|
1259
|
+
updatedAt: new Date,
|
|
1260
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1261
|
+
usage: result.usage
|
|
1262
|
+
};
|
|
1263
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
1264
|
+
onResponse?.(assistantMessage);
|
|
1265
|
+
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
1266
|
+
if (store && !conversationId) {
|
|
1267
|
+
const conv = await store.create({
|
|
1268
|
+
status: "active",
|
|
1269
|
+
provider: "agent",
|
|
1270
|
+
model: "agent",
|
|
1271
|
+
messages: []
|
|
1272
|
+
});
|
|
1273
|
+
if (!opts?.skipUserAppend) {
|
|
1274
|
+
await store.appendMessage(conv.id, {
|
|
1275
|
+
role: "user",
|
|
1276
|
+
content,
|
|
1277
|
+
status: "completed",
|
|
1278
|
+
attachments
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
await store.appendMessage(conv.id, {
|
|
1282
|
+
role: "assistant",
|
|
1283
|
+
content: result.text,
|
|
1284
|
+
status: "completed",
|
|
1285
|
+
toolCalls: assistantMessage.toolCalls,
|
|
1286
|
+
usage: result.usage
|
|
1287
|
+
});
|
|
1288
|
+
const updated = await store.get(conv.id);
|
|
1289
|
+
if (updated)
|
|
1290
|
+
setConversation(updated);
|
|
1291
|
+
setConversationId(conv.id);
|
|
1292
|
+
}
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1295
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1296
|
+
} finally {
|
|
1297
|
+
setIsLoading(false);
|
|
1298
|
+
}
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
512
1301
|
if (!chatServiceRef.current) {
|
|
513
1302
|
throw new Error("Chat service not initialized");
|
|
514
1303
|
}
|
|
@@ -516,25 +1305,28 @@ function useChat(options = {}) {
|
|
|
516
1305
|
setError(null);
|
|
517
1306
|
abortControllerRef.current = new AbortController;
|
|
518
1307
|
try {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1308
|
+
if (!opts?.skipUserAppend) {
|
|
1309
|
+
const userMessage = {
|
|
1310
|
+
id: `msg_${Date.now()}`,
|
|
1311
|
+
conversationId: conversationId ?? "",
|
|
1312
|
+
role: "user",
|
|
1313
|
+
content,
|
|
1314
|
+
status: "completed",
|
|
1315
|
+
createdAt: new Date,
|
|
1316
|
+
updatedAt: new Date,
|
|
1317
|
+
attachments
|
|
1318
|
+
};
|
|
1319
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1320
|
+
onSend?.(userMessage);
|
|
1321
|
+
}
|
|
531
1322
|
if (streaming) {
|
|
532
1323
|
const result = await chatServiceRef.current.stream({
|
|
533
1324
|
conversationId: conversationId ?? undefined,
|
|
534
1325
|
content,
|
|
535
|
-
attachments
|
|
1326
|
+
attachments,
|
|
1327
|
+
skipUserAppend: opts?.skipUserAppend
|
|
536
1328
|
});
|
|
537
|
-
if (!conversationId) {
|
|
1329
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
538
1330
|
setConversationId(result.conversationId);
|
|
539
1331
|
}
|
|
540
1332
|
const assistantMessage = {
|
|
@@ -615,7 +1407,8 @@ function useChat(options = {}) {
|
|
|
615
1407
|
const result = await chatServiceRef.current.send({
|
|
616
1408
|
conversationId: conversationId ?? undefined,
|
|
617
1409
|
content,
|
|
618
|
-
attachments
|
|
1410
|
+
attachments,
|
|
1411
|
+
skipUserAppend: opts?.skipUserAppend
|
|
619
1412
|
});
|
|
620
1413
|
setConversation(result.conversation);
|
|
621
1414
|
setMessages(result.conversation.messages);
|
|
@@ -632,7 +1425,7 @@ function useChat(options = {}) {
|
|
|
632
1425
|
setIsLoading(false);
|
|
633
1426
|
abortControllerRef.current = null;
|
|
634
1427
|
}
|
|
635
|
-
}, [conversationId, streaming, onSend, onResponse, onError, messages]);
|
|
1428
|
+
}, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
|
|
636
1429
|
const clearConversation = React.useCallback(() => {
|
|
637
1430
|
setMessages([]);
|
|
638
1431
|
setConversation(null);
|
|
@@ -653,6 +1446,44 @@ function useChat(options = {}) {
|
|
|
653
1446
|
abortControllerRef.current?.abort();
|
|
654
1447
|
setIsLoading(false);
|
|
655
1448
|
}, []);
|
|
1449
|
+
const createNewConversation = clearConversation;
|
|
1450
|
+
const editMessage = React.useCallback(async (messageId, newContent) => {
|
|
1451
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1452
|
+
return;
|
|
1453
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
1454
|
+
if (!msg || msg.role !== "user")
|
|
1455
|
+
return;
|
|
1456
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
|
|
1457
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
1458
|
+
if (truncated) {
|
|
1459
|
+
setMessages(truncated.messages);
|
|
1460
|
+
}
|
|
1461
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
1462
|
+
}, [conversationId, messages, sendMessage]);
|
|
1463
|
+
const forkConversation = React.useCallback(async (upToMessageId) => {
|
|
1464
|
+
if (!chatServiceRef.current)
|
|
1465
|
+
return null;
|
|
1466
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
1467
|
+
if (!idToFork)
|
|
1468
|
+
return null;
|
|
1469
|
+
try {
|
|
1470
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
1471
|
+
setConversationId(forked.id);
|
|
1472
|
+
setConversation(forked);
|
|
1473
|
+
setMessages(forked.messages);
|
|
1474
|
+
return forked.id;
|
|
1475
|
+
} catch {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
}, [conversationId, conversation]);
|
|
1479
|
+
const updateConversationFn = React.useCallback(async (updates) => {
|
|
1480
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1481
|
+
return null;
|
|
1482
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
1483
|
+
if (updated)
|
|
1484
|
+
setConversation(updated);
|
|
1485
|
+
return updated;
|
|
1486
|
+
}, [conversationId]);
|
|
656
1487
|
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
657
1488
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
658
1489
|
}, []);
|
|
@@ -667,6 +1498,10 @@ function useChat(options = {}) {
|
|
|
667
1498
|
setConversationId,
|
|
668
1499
|
regenerate,
|
|
669
1500
|
stop,
|
|
1501
|
+
createNewConversation,
|
|
1502
|
+
editMessage,
|
|
1503
|
+
forkConversation,
|
|
1504
|
+
updateConversation: updateConversationFn,
|
|
670
1505
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
671
1506
|
};
|
|
672
1507
|
}
|
|
@@ -710,11 +1545,94 @@ function useProviders() {
|
|
|
710
1545
|
refresh: loadProviders
|
|
711
1546
|
};
|
|
712
1547
|
}
|
|
1548
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1549
|
+
import * as React3 from "react";
|
|
1550
|
+
"use client";
|
|
1551
|
+
function useMessageSelection(messageIds) {
|
|
1552
|
+
const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
|
|
1553
|
+
const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1554
|
+
React3.useEffect(() => {
|
|
1555
|
+
setSelectedIds((prev) => {
|
|
1556
|
+
const next = new Set;
|
|
1557
|
+
for (const id of prev) {
|
|
1558
|
+
if (idSet.has(id))
|
|
1559
|
+
next.add(id);
|
|
1560
|
+
}
|
|
1561
|
+
return next.size === prev.size ? prev : next;
|
|
1562
|
+
});
|
|
1563
|
+
}, [idSet]);
|
|
1564
|
+
const toggle = React3.useCallback((id) => {
|
|
1565
|
+
setSelectedIds((prev) => {
|
|
1566
|
+
const next = new Set(prev);
|
|
1567
|
+
if (next.has(id))
|
|
1568
|
+
next.delete(id);
|
|
1569
|
+
else
|
|
1570
|
+
next.add(id);
|
|
1571
|
+
return next;
|
|
1572
|
+
});
|
|
1573
|
+
}, []);
|
|
1574
|
+
const selectAll = React3.useCallback(() => {
|
|
1575
|
+
setSelectedIds(new Set(messageIds));
|
|
1576
|
+
}, [messageIds.join(",")]);
|
|
1577
|
+
const clearSelection = React3.useCallback(() => {
|
|
1578
|
+
setSelectedIds(new Set);
|
|
1579
|
+
}, []);
|
|
1580
|
+
const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1581
|
+
const selectedCount = selectedIds.size;
|
|
1582
|
+
return {
|
|
1583
|
+
selectedIds,
|
|
1584
|
+
toggle,
|
|
1585
|
+
selectAll,
|
|
1586
|
+
clearSelection,
|
|
1587
|
+
isSelected,
|
|
1588
|
+
selectedCount
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
// src/presentation/hooks/useConversations.ts
|
|
1592
|
+
import * as React4 from "react";
|
|
1593
|
+
"use client";
|
|
1594
|
+
function useConversations(options) {
|
|
1595
|
+
const { store, projectId, tags, limit = 50 } = options;
|
|
1596
|
+
const [conversations, setConversations] = React4.useState([]);
|
|
1597
|
+
const [isLoading, setIsLoading] = React4.useState(true);
|
|
1598
|
+
const refresh = React4.useCallback(async () => {
|
|
1599
|
+
setIsLoading(true);
|
|
1600
|
+
try {
|
|
1601
|
+
const list = await store.list({
|
|
1602
|
+
status: "active",
|
|
1603
|
+
projectId,
|
|
1604
|
+
tags,
|
|
1605
|
+
limit
|
|
1606
|
+
});
|
|
1607
|
+
setConversations(list);
|
|
1608
|
+
} finally {
|
|
1609
|
+
setIsLoading(false);
|
|
1610
|
+
}
|
|
1611
|
+
}, [store, projectId, tags, limit]);
|
|
1612
|
+
React4.useEffect(() => {
|
|
1613
|
+
refresh();
|
|
1614
|
+
}, [refresh]);
|
|
1615
|
+
const deleteConversation = React4.useCallback(async (id) => {
|
|
1616
|
+
const ok = await store.delete(id);
|
|
1617
|
+
if (ok) {
|
|
1618
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1619
|
+
}
|
|
1620
|
+
return ok;
|
|
1621
|
+
}, [store]);
|
|
1622
|
+
return {
|
|
1623
|
+
conversations,
|
|
1624
|
+
isLoading,
|
|
1625
|
+
refresh,
|
|
1626
|
+
deleteConversation
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
713
1629
|
|
|
714
1630
|
// src/presentation/hooks/index.ts
|
|
715
1631
|
import { useCompletion } from "@ai-sdk/react";
|
|
716
1632
|
export {
|
|
717
1633
|
useProviders,
|
|
1634
|
+
useMessageSelection,
|
|
1635
|
+
useConversations,
|
|
718
1636
|
useCompletion,
|
|
719
1637
|
useChat
|
|
720
1638
|
};
|