@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
|
@@ -8,8 +8,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/presentation/hooks/useChat.tsx
|
|
10
10
|
import * as React from "react";
|
|
11
|
-
import { tool } from "ai";
|
|
12
|
-
import { z } from "zod";
|
|
11
|
+
import { tool as tool4 } from "ai";
|
|
12
|
+
import { z as z4 } from "zod";
|
|
13
13
|
|
|
14
14
|
// src/core/chat-service.ts
|
|
15
15
|
import { generateText, streamText } from "ai";
|
|
@@ -91,11 +91,65 @@ class InMemoryConversationStore {
|
|
|
91
91
|
if (options?.status) {
|
|
92
92
|
results = results.filter((c) => c.status === options.status);
|
|
93
93
|
}
|
|
94
|
+
if (options?.projectId) {
|
|
95
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
96
|
+
}
|
|
97
|
+
if (options?.tags && options.tags.length > 0) {
|
|
98
|
+
const tagSet = new Set(options.tags);
|
|
99
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
100
|
+
}
|
|
94
101
|
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
95
102
|
const offset = options?.offset ?? 0;
|
|
96
103
|
const limit = options?.limit ?? 100;
|
|
97
104
|
return results.slice(offset, offset + limit);
|
|
98
105
|
}
|
|
106
|
+
async fork(conversationId, upToMessageId) {
|
|
107
|
+
const source = this.conversations.get(conversationId);
|
|
108
|
+
if (!source) {
|
|
109
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
110
|
+
}
|
|
111
|
+
let messagesToCopy = source.messages;
|
|
112
|
+
if (upToMessageId) {
|
|
113
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
114
|
+
if (idx === -1) {
|
|
115
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
116
|
+
}
|
|
117
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
118
|
+
}
|
|
119
|
+
const now = new Date;
|
|
120
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
121
|
+
...m,
|
|
122
|
+
id: generateId("msg"),
|
|
123
|
+
conversationId: "",
|
|
124
|
+
createdAt: new Date(m.createdAt),
|
|
125
|
+
updatedAt: new Date(m.updatedAt)
|
|
126
|
+
}));
|
|
127
|
+
const forked = {
|
|
128
|
+
...source,
|
|
129
|
+
id: generateId("conv"),
|
|
130
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
131
|
+
forkedFromId: source.id,
|
|
132
|
+
createdAt: now,
|
|
133
|
+
updatedAt: now,
|
|
134
|
+
messages: forkedMessages
|
|
135
|
+
};
|
|
136
|
+
for (const m of forked.messages) {
|
|
137
|
+
m.conversationId = forked.id;
|
|
138
|
+
}
|
|
139
|
+
this.conversations.set(forked.id, forked);
|
|
140
|
+
return forked;
|
|
141
|
+
}
|
|
142
|
+
async truncateAfter(conversationId, messageId) {
|
|
143
|
+
const conv = this.conversations.get(conversationId);
|
|
144
|
+
if (!conv)
|
|
145
|
+
return null;
|
|
146
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
147
|
+
if (idx === -1)
|
|
148
|
+
return null;
|
|
149
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
150
|
+
conv.updatedAt = new Date;
|
|
151
|
+
return conv;
|
|
152
|
+
}
|
|
99
153
|
async search(query, limit = 20) {
|
|
100
154
|
const lowerQuery = query.toLowerCase();
|
|
101
155
|
const results = [];
|
|
@@ -121,7 +175,504 @@ function createInMemoryConversationStore() {
|
|
|
121
175
|
return new InMemoryConversationStore;
|
|
122
176
|
}
|
|
123
177
|
|
|
178
|
+
// src/core/thinking-levels.ts
|
|
179
|
+
var THINKING_LEVEL_LABELS = {
|
|
180
|
+
instant: "Instant",
|
|
181
|
+
thinking: "Thinking",
|
|
182
|
+
extra_thinking: "Extra Thinking",
|
|
183
|
+
max: "Max"
|
|
184
|
+
};
|
|
185
|
+
var THINKING_LEVEL_DESCRIPTIONS = {
|
|
186
|
+
instant: "Fast responses, minimal reasoning",
|
|
187
|
+
thinking: "Standard reasoning depth",
|
|
188
|
+
extra_thinking: "More thorough reasoning",
|
|
189
|
+
max: "Maximum reasoning depth"
|
|
190
|
+
};
|
|
191
|
+
function getProviderOptions(level, providerName) {
|
|
192
|
+
if (!level || level === "instant") {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
switch (providerName) {
|
|
196
|
+
case "anthropic": {
|
|
197
|
+
const budgetMap = {
|
|
198
|
+
thinking: 8000,
|
|
199
|
+
extra_thinking: 16000,
|
|
200
|
+
max: 32000
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
anthropic: {
|
|
204
|
+
thinking: { type: "enabled", budgetTokens: budgetMap[level] }
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
case "openai": {
|
|
209
|
+
const effortMap = {
|
|
210
|
+
thinking: "low",
|
|
211
|
+
extra_thinking: "medium",
|
|
212
|
+
max: "high"
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
openai: {
|
|
216
|
+
reasoningEffort: effortMap[level]
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
case "ollama":
|
|
221
|
+
case "mistral":
|
|
222
|
+
case "gemini":
|
|
223
|
+
return {};
|
|
224
|
+
default:
|
|
225
|
+
return {};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/core/workflow-tools.ts
|
|
230
|
+
import { tool } from "ai";
|
|
231
|
+
import { z } from "zod";
|
|
232
|
+
import {
|
|
233
|
+
WorkflowComposer,
|
|
234
|
+
validateExtension
|
|
235
|
+
} from "@contractspec/lib.workflow-composer";
|
|
236
|
+
var StepTypeSchema = z.enum(["human", "automation", "decision"]);
|
|
237
|
+
var StepActionSchema = z.object({
|
|
238
|
+
operation: z.object({
|
|
239
|
+
name: z.string(),
|
|
240
|
+
version: z.number()
|
|
241
|
+
}).optional(),
|
|
242
|
+
form: z.object({
|
|
243
|
+
key: z.string(),
|
|
244
|
+
version: z.number()
|
|
245
|
+
}).optional()
|
|
246
|
+
}).optional();
|
|
247
|
+
var StepSchema = z.object({
|
|
248
|
+
id: z.string(),
|
|
249
|
+
type: StepTypeSchema,
|
|
250
|
+
label: z.string(),
|
|
251
|
+
description: z.string().optional(),
|
|
252
|
+
action: StepActionSchema
|
|
253
|
+
});
|
|
254
|
+
var StepInjectionSchema = z.object({
|
|
255
|
+
after: z.string().optional(),
|
|
256
|
+
before: z.string().optional(),
|
|
257
|
+
inject: StepSchema,
|
|
258
|
+
transitionTo: z.string().optional(),
|
|
259
|
+
transitionFrom: z.string().optional(),
|
|
260
|
+
when: z.string().optional()
|
|
261
|
+
});
|
|
262
|
+
var WorkflowExtensionInputSchema = z.object({
|
|
263
|
+
workflow: z.string(),
|
|
264
|
+
tenantId: z.string().optional(),
|
|
265
|
+
role: z.string().optional(),
|
|
266
|
+
priority: z.number().optional(),
|
|
267
|
+
customSteps: z.array(StepInjectionSchema).optional(),
|
|
268
|
+
hiddenSteps: z.array(z.string()).optional()
|
|
269
|
+
});
|
|
270
|
+
function createWorkflowTools(config) {
|
|
271
|
+
const { baseWorkflows, composer } = config;
|
|
272
|
+
const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
|
|
273
|
+
const createWorkflowExtensionTool = tool({
|
|
274
|
+
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.",
|
|
275
|
+
inputSchema: WorkflowExtensionInputSchema,
|
|
276
|
+
execute: async (input) => {
|
|
277
|
+
const extension = {
|
|
278
|
+
workflow: input.workflow,
|
|
279
|
+
tenantId: input.tenantId,
|
|
280
|
+
role: input.role,
|
|
281
|
+
priority: input.priority,
|
|
282
|
+
customSteps: input.customSteps,
|
|
283
|
+
hiddenSteps: input.hiddenSteps
|
|
284
|
+
};
|
|
285
|
+
const base = baseByKey.get(input.workflow);
|
|
286
|
+
if (!base) {
|
|
287
|
+
return {
|
|
288
|
+
success: false,
|
|
289
|
+
error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
290
|
+
extension
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
validateExtension(extension, base);
|
|
295
|
+
return {
|
|
296
|
+
success: true,
|
|
297
|
+
message: "Extension validated successfully",
|
|
298
|
+
extension
|
|
299
|
+
};
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: err instanceof Error ? err.message : String(err),
|
|
304
|
+
extension
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
const composeWorkflowInputSchema = z.object({
|
|
310
|
+
workflowKey: z.string().describe("Base workflow meta.key"),
|
|
311
|
+
tenantId: z.string().optional(),
|
|
312
|
+
role: z.string().optional(),
|
|
313
|
+
extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
|
|
314
|
+
});
|
|
315
|
+
const composeWorkflowTool = tool({
|
|
316
|
+
description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
|
|
317
|
+
inputSchema: composeWorkflowInputSchema,
|
|
318
|
+
execute: async (input) => {
|
|
319
|
+
const base = baseByKey.get(input.workflowKey);
|
|
320
|
+
if (!base) {
|
|
321
|
+
return {
|
|
322
|
+
success: false,
|
|
323
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const comp = composer ?? new WorkflowComposer;
|
|
327
|
+
if (input.extensions?.length) {
|
|
328
|
+
for (const ext of input.extensions) {
|
|
329
|
+
comp.register({
|
|
330
|
+
workflow: ext.workflow,
|
|
331
|
+
tenantId: ext.tenantId,
|
|
332
|
+
role: ext.role,
|
|
333
|
+
priority: ext.priority,
|
|
334
|
+
customSteps: ext.customSteps,
|
|
335
|
+
hiddenSteps: ext.hiddenSteps
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const composed = comp.compose({
|
|
341
|
+
base,
|
|
342
|
+
tenantId: input.tenantId,
|
|
343
|
+
role: input.role
|
|
344
|
+
});
|
|
345
|
+
return {
|
|
346
|
+
success: true,
|
|
347
|
+
workflow: composed,
|
|
348
|
+
meta: composed.meta,
|
|
349
|
+
stepIds: composed.definition.steps.map((s) => s.id)
|
|
350
|
+
};
|
|
351
|
+
} catch (err) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: err instanceof Error ? err.message : String(err)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const generateWorkflowSpecCodeInputSchema = z.object({
|
|
360
|
+
workflowKey: z.string().describe("Workflow meta.key"),
|
|
361
|
+
composedSteps: z.array(z.object({
|
|
362
|
+
id: z.string(),
|
|
363
|
+
type: z.enum(["human", "automation", "decision"]),
|
|
364
|
+
label: z.string(),
|
|
365
|
+
description: z.string().optional()
|
|
366
|
+
})).optional().describe("Steps to include; if omitted, uses the base workflow")
|
|
367
|
+
});
|
|
368
|
+
const generateWorkflowSpecCodeTool = tool({
|
|
369
|
+
description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
|
|
370
|
+
inputSchema: generateWorkflowSpecCodeInputSchema,
|
|
371
|
+
execute: async (input) => {
|
|
372
|
+
const base = baseByKey.get(input.workflowKey);
|
|
373
|
+
if (!base) {
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
377
|
+
code: null
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const steps = input.composedSteps ?? base.definition.steps;
|
|
381
|
+
const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
|
|
382
|
+
const stepsCode = steps.map((s) => ` {
|
|
383
|
+
id: '${s.id}',
|
|
384
|
+
type: '${s.type}',
|
|
385
|
+
label: '${escapeString(s.label)}',${s.description ? `
|
|
386
|
+
description: '${escapeString(s.description)}',` : ""}
|
|
387
|
+
}`).join(`,
|
|
388
|
+
`);
|
|
389
|
+
const meta = base.meta;
|
|
390
|
+
const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
|
|
391
|
+
const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Workflow: ${base.meta.key}
|
|
395
|
+
* Generated via AI chat workflow tools.
|
|
396
|
+
*/
|
|
397
|
+
export const ${specVarName}: WorkflowSpec = {
|
|
398
|
+
meta: {
|
|
399
|
+
key: '${base.meta.key}',
|
|
400
|
+
version: '${String(base.meta.version)}',
|
|
401
|
+
title: '${escapeString(meta.title ?? base.meta.key)}',
|
|
402
|
+
description: '${escapeString(meta.description ?? "")}',
|
|
403
|
+
},
|
|
404
|
+
definition: {
|
|
405
|
+
entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
|
|
406
|
+
steps: [
|
|
407
|
+
${stepsCode}
|
|
408
|
+
],
|
|
409
|
+
transitions: ${transitionsJson},
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
`;
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
code,
|
|
416
|
+
workflowKey: input.workflowKey
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
create_workflow_extension: createWorkflowExtensionTool,
|
|
422
|
+
compose_workflow: composeWorkflowTool,
|
|
423
|
+
generate_workflow_spec_code: generateWorkflowSpecCodeTool
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function toPascalCase(value) {
|
|
427
|
+
return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
428
|
+
}
|
|
429
|
+
function escapeString(value) {
|
|
430
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/core/contracts-context.ts
|
|
434
|
+
function buildContractsContextPrompt(config) {
|
|
435
|
+
const parts = [];
|
|
436
|
+
if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
|
|
437
|
+
return "";
|
|
438
|
+
}
|
|
439
|
+
parts.push(`
|
|
440
|
+
|
|
441
|
+
## Available resources`);
|
|
442
|
+
if (config.agentSpecs?.length) {
|
|
443
|
+
parts.push(`
|
|
444
|
+
### Agent tools`);
|
|
445
|
+
for (const agent of config.agentSpecs) {
|
|
446
|
+
const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
|
|
447
|
+
parts.push(`- **${agent.key}**: tools: ${toolNames}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (config.dataViewSpecs?.length) {
|
|
451
|
+
parts.push(`
|
|
452
|
+
### Data views`);
|
|
453
|
+
for (const dv of config.dataViewSpecs) {
|
|
454
|
+
parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (config.formSpecs?.length) {
|
|
458
|
+
parts.push(`
|
|
459
|
+
### Forms`);
|
|
460
|
+
for (const form of config.formSpecs) {
|
|
461
|
+
parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (config.presentationSpecs?.length) {
|
|
465
|
+
parts.push(`
|
|
466
|
+
### Presentations`);
|
|
467
|
+
for (const pres of config.presentationSpecs) {
|
|
468
|
+
parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (config.operationRefs?.length) {
|
|
472
|
+
parts.push(`
|
|
473
|
+
### Operations`);
|
|
474
|
+
for (const op of config.operationRefs) {
|
|
475
|
+
parts.push(`- **${op.key}@${op.version}**`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
parts.push(`
|
|
479
|
+
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
|
|
480
|
+
return parts.join(`
|
|
481
|
+
`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/core/agent-tools-adapter.ts
|
|
485
|
+
import { tool as tool2 } from "ai";
|
|
486
|
+
import { z as z2 } from "zod";
|
|
487
|
+
function getInputSchema(_schema) {
|
|
488
|
+
return z2.object({}).passthrough();
|
|
489
|
+
}
|
|
490
|
+
function agentToolConfigsToToolSet(configs, handlers) {
|
|
491
|
+
const result = {};
|
|
492
|
+
for (const config of configs) {
|
|
493
|
+
const handler = handlers?.[config.name];
|
|
494
|
+
const inputSchema = getInputSchema(config.schema);
|
|
495
|
+
result[config.name] = tool2({
|
|
496
|
+
description: config.description ?? config.name,
|
|
497
|
+
inputSchema,
|
|
498
|
+
execute: async (input) => {
|
|
499
|
+
if (!handler) {
|
|
500
|
+
return {
|
|
501
|
+
status: "unimplemented",
|
|
502
|
+
message: "Wire handler in host",
|
|
503
|
+
toolName: config.name
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const output = await Promise.resolve(handler(input));
|
|
508
|
+
return typeof output === "string" ? output : output;
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return {
|
|
511
|
+
status: "error",
|
|
512
|
+
error: err instanceof Error ? err.message : String(err),
|
|
513
|
+
toolName: config.name
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/core/surface-planner-tools.ts
|
|
523
|
+
import { tool as tool3 } from "ai";
|
|
524
|
+
import { z as z3 } from "zod";
|
|
525
|
+
import { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
526
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
527
|
+
var VALID_OPS = [
|
|
528
|
+
"insert-node",
|
|
529
|
+
"replace-node",
|
|
530
|
+
"remove-node",
|
|
531
|
+
"move-node",
|
|
532
|
+
"resize-panel",
|
|
533
|
+
"set-layout",
|
|
534
|
+
"reveal-field",
|
|
535
|
+
"hide-field",
|
|
536
|
+
"promote-action",
|
|
537
|
+
"set-focus"
|
|
538
|
+
];
|
|
539
|
+
var DEFAULT_NODE_KINDS = [
|
|
540
|
+
"entity-section",
|
|
541
|
+
"entity-card",
|
|
542
|
+
"data-view",
|
|
543
|
+
"assistant-panel",
|
|
544
|
+
"chat-thread",
|
|
545
|
+
"action-bar",
|
|
546
|
+
"timeline",
|
|
547
|
+
"table",
|
|
548
|
+
"rich-doc",
|
|
549
|
+
"form",
|
|
550
|
+
"chart",
|
|
551
|
+
"custom-widget"
|
|
552
|
+
];
|
|
553
|
+
function collectSlotIdsFromRegion(node) {
|
|
554
|
+
const ids = [];
|
|
555
|
+
if (node.type === "slot") {
|
|
556
|
+
ids.push(node.slotId);
|
|
557
|
+
}
|
|
558
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
559
|
+
for (const child of node.children) {
|
|
560
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (node.type === "tabs") {
|
|
564
|
+
for (const tab of node.tabs) {
|
|
565
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (node.type === "floating") {
|
|
569
|
+
ids.push(node.anchorSlotId);
|
|
570
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
571
|
+
}
|
|
572
|
+
return ids;
|
|
573
|
+
}
|
|
574
|
+
function deriveConstraints(plan) {
|
|
575
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
576
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
577
|
+
return {
|
|
578
|
+
allowedOps: VALID_OPS,
|
|
579
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
580
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
var ProposePatchInputSchema = z3.object({
|
|
584
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
585
|
+
ops: z3.array(z3.object({
|
|
586
|
+
op: z3.enum([
|
|
587
|
+
"insert-node",
|
|
588
|
+
"replace-node",
|
|
589
|
+
"remove-node",
|
|
590
|
+
"move-node",
|
|
591
|
+
"resize-panel",
|
|
592
|
+
"set-layout",
|
|
593
|
+
"reveal-field",
|
|
594
|
+
"hide-field",
|
|
595
|
+
"promote-action",
|
|
596
|
+
"set-focus"
|
|
597
|
+
]),
|
|
598
|
+
slotId: z3.string().optional(),
|
|
599
|
+
nodeId: z3.string().optional(),
|
|
600
|
+
toSlotId: z3.string().optional(),
|
|
601
|
+
index: z3.number().optional(),
|
|
602
|
+
node: z3.object({
|
|
603
|
+
nodeId: z3.string(),
|
|
604
|
+
kind: z3.string(),
|
|
605
|
+
title: z3.string().optional(),
|
|
606
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
607
|
+
children: z3.array(z3.unknown()).optional()
|
|
608
|
+
}).optional(),
|
|
609
|
+
persistKey: z3.string().optional(),
|
|
610
|
+
sizes: z3.array(z3.number()).optional(),
|
|
611
|
+
layoutId: z3.string().optional(),
|
|
612
|
+
fieldId: z3.string().optional(),
|
|
613
|
+
actionId: z3.string().optional(),
|
|
614
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
615
|
+
targetId: z3.string().optional()
|
|
616
|
+
}))
|
|
617
|
+
});
|
|
618
|
+
function createSurfacePlannerTools(config) {
|
|
619
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
620
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
621
|
+
const proposePatchTool = tool3({
|
|
622
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
623
|
+
inputSchema: ProposePatchInputSchema,
|
|
624
|
+
execute: async (input) => {
|
|
625
|
+
const ops = input.ops;
|
|
626
|
+
try {
|
|
627
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
628
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
629
|
+
onPatchProposal?.(proposal);
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
proposalId: proposal.proposalId,
|
|
633
|
+
opsCount: proposal.ops.length,
|
|
634
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
635
|
+
};
|
|
636
|
+
} catch (err) {
|
|
637
|
+
return {
|
|
638
|
+
success: false,
|
|
639
|
+
error: err instanceof Error ? err.message : String(err),
|
|
640
|
+
proposalId: input.proposalId
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
return {
|
|
646
|
+
"propose-patch": proposePatchTool
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function buildPlannerPromptInput(plan) {
|
|
650
|
+
const constraints = deriveConstraints(plan);
|
|
651
|
+
return {
|
|
652
|
+
bundleMeta: {
|
|
653
|
+
key: plan.bundleKey,
|
|
654
|
+
version: "0.0.0",
|
|
655
|
+
title: plan.bundleKey
|
|
656
|
+
},
|
|
657
|
+
surfaceId: plan.surfaceId,
|
|
658
|
+
allowedPatchOps: constraints.allowedOps,
|
|
659
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
660
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
661
|
+
actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
|
|
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
|
+
|
|
124
674
|
// src/core/chat-service.ts
|
|
675
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
125
676
|
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
126
677
|
|
|
127
678
|
Your capabilities:
|
|
@@ -136,6 +687,9 @@ Guidelines:
|
|
|
136
687
|
- Reference relevant ContractSpec concepts and patterns
|
|
137
688
|
- Ask clarifying questions when the user's intent is unclear
|
|
138
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.`;
|
|
139
693
|
|
|
140
694
|
class ChatService {
|
|
141
695
|
provider;
|
|
@@ -145,19 +699,93 @@ class ChatService {
|
|
|
145
699
|
maxHistoryMessages;
|
|
146
700
|
onUsage;
|
|
147
701
|
tools;
|
|
702
|
+
thinkingLevel;
|
|
148
703
|
sendReasoning;
|
|
149
704
|
sendSources;
|
|
705
|
+
modelSelector;
|
|
150
706
|
constructor(config) {
|
|
151
707
|
this.provider = config.provider;
|
|
152
708
|
this.context = config.context;
|
|
153
709
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
154
|
-
this.systemPrompt = config
|
|
710
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
155
711
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
156
712
|
this.onUsage = config.onUsage;
|
|
157
|
-
this.tools = config
|
|
158
|
-
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");
|
|
159
717
|
this.sendSources = config.sendSources ?? false;
|
|
160
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
|
+
}
|
|
161
789
|
async send(options) {
|
|
162
790
|
let conversation;
|
|
163
791
|
if (options.conversationId) {
|
|
@@ -175,20 +803,25 @@ class ChatService {
|
|
|
175
803
|
workspacePath: this.context?.workspacePath
|
|
176
804
|
});
|
|
177
805
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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;
|
|
184
815
|
const messages = this.buildMessages(conversation, options);
|
|
185
|
-
const model = this.
|
|
816
|
+
const { model, providerName } = await this.resolveModel();
|
|
817
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
186
818
|
try {
|
|
187
819
|
const result = await generateText({
|
|
188
820
|
model,
|
|
189
821
|
messages,
|
|
190
822
|
system: this.systemPrompt,
|
|
191
|
-
tools: this.tools
|
|
823
|
+
tools: this.tools,
|
|
824
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
192
825
|
});
|
|
193
826
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
194
827
|
role: "assistant",
|
|
@@ -233,23 +866,27 @@ class ChatService {
|
|
|
233
866
|
workspacePath: this.context?.workspacePath
|
|
234
867
|
});
|
|
235
868
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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;
|
|
242
878
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
243
879
|
role: "assistant",
|
|
244
880
|
content: "",
|
|
245
881
|
status: "streaming"
|
|
246
882
|
});
|
|
247
883
|
const messages = this.buildMessages(conversation, options);
|
|
248
|
-
const model = this.
|
|
884
|
+
const { model, providerName } = await this.resolveModel();
|
|
249
885
|
const systemPrompt = this.systemPrompt;
|
|
250
886
|
const tools = this.tools;
|
|
251
887
|
const store = this.store;
|
|
252
888
|
const onUsage = this.onUsage;
|
|
889
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
253
890
|
async function* streamGenerator() {
|
|
254
891
|
let fullContent = "";
|
|
255
892
|
let fullReasoning = "";
|
|
@@ -260,7 +897,8 @@ class ChatService {
|
|
|
260
897
|
model,
|
|
261
898
|
messages,
|
|
262
899
|
system: systemPrompt,
|
|
263
|
-
tools
|
|
900
|
+
tools,
|
|
901
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
264
902
|
});
|
|
265
903
|
for await (const part of result.fullStream) {
|
|
266
904
|
if (part.type === "text-delta") {
|
|
@@ -375,6 +1013,18 @@ class ChatService {
|
|
|
375
1013
|
...options
|
|
376
1014
|
});
|
|
377
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
|
+
}
|
|
378
1028
|
async deleteConversation(conversationId) {
|
|
379
1029
|
return this.store.delete(conversationId);
|
|
380
1030
|
}
|
|
@@ -445,9 +1095,9 @@ import {
|
|
|
445
1095
|
function toolsToToolSet(defs) {
|
|
446
1096
|
const result = {};
|
|
447
1097
|
for (const def of defs) {
|
|
448
|
-
result[def.name] =
|
|
1098
|
+
result[def.name] = tool4({
|
|
449
1099
|
description: def.description ?? def.name,
|
|
450
|
-
inputSchema:
|
|
1100
|
+
inputSchema: z4.object({}).passthrough(),
|
|
451
1101
|
execute: async () => ({})
|
|
452
1102
|
});
|
|
453
1103
|
}
|
|
@@ -461,21 +1111,63 @@ function useChat(options = {}) {
|
|
|
461
1111
|
apiKey,
|
|
462
1112
|
proxyUrl,
|
|
463
1113
|
conversationId: initialConversationId,
|
|
1114
|
+
store,
|
|
464
1115
|
systemPrompt,
|
|
465
1116
|
streaming = true,
|
|
466
1117
|
onSend,
|
|
467
1118
|
onResponse,
|
|
468
1119
|
onError,
|
|
469
1120
|
onUsage,
|
|
470
|
-
tools: toolsDefs
|
|
1121
|
+
tools: toolsDefs,
|
|
1122
|
+
thinkingLevel,
|
|
1123
|
+
workflowToolsConfig,
|
|
1124
|
+
modelSelector,
|
|
1125
|
+
contractsContext,
|
|
1126
|
+
surfacePlanConfig,
|
|
1127
|
+
mcpServers,
|
|
1128
|
+
agentMode
|
|
471
1129
|
} = options;
|
|
472
1130
|
const [messages, setMessages] = React.useState([]);
|
|
1131
|
+
const [mcpTools, setMcpTools] = React.useState(null);
|
|
1132
|
+
const mcpCleanupRef = React.useRef(null);
|
|
473
1133
|
const [conversation, setConversation] = React.useState(null);
|
|
474
1134
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
475
1135
|
const [error, setError] = React.useState(null);
|
|
476
1136
|
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
477
1137
|
const abortControllerRef = React.useRef(null);
|
|
478
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]);
|
|
479
1171
|
React.useEffect(() => {
|
|
480
1172
|
const chatProvider = createProvider({
|
|
481
1173
|
provider,
|
|
@@ -485,9 +1177,16 @@ function useChat(options = {}) {
|
|
|
485
1177
|
});
|
|
486
1178
|
chatServiceRef.current = new ChatService({
|
|
487
1179
|
provider: chatProvider,
|
|
1180
|
+
store,
|
|
488
1181
|
systemPrompt,
|
|
489
1182
|
onUsage,
|
|
490
|
-
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
|
|
491
1190
|
});
|
|
492
1191
|
}, [
|
|
493
1192
|
provider,
|
|
@@ -495,9 +1194,16 @@ function useChat(options = {}) {
|
|
|
495
1194
|
model,
|
|
496
1195
|
apiKey,
|
|
497
1196
|
proxyUrl,
|
|
1197
|
+
store,
|
|
498
1198
|
systemPrompt,
|
|
499
1199
|
onUsage,
|
|
500
|
-
toolsDefs
|
|
1200
|
+
toolsDefs,
|
|
1201
|
+
thinkingLevel,
|
|
1202
|
+
workflowToolsConfig,
|
|
1203
|
+
modelSelector,
|
|
1204
|
+
contractsContext,
|
|
1205
|
+
surfacePlanConfig,
|
|
1206
|
+
mcpTools
|
|
501
1207
|
]);
|
|
502
1208
|
React.useEffect(() => {
|
|
503
1209
|
if (!conversationId || !chatServiceRef.current)
|
|
@@ -513,7 +1219,90 @@ function useChat(options = {}) {
|
|
|
513
1219
|
};
|
|
514
1220
|
loadConversation().catch(console.error);
|
|
515
1221
|
}, [conversationId]);
|
|
516
|
-
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
|
+
}
|
|
517
1306
|
if (!chatServiceRef.current) {
|
|
518
1307
|
throw new Error("Chat service not initialized");
|
|
519
1308
|
}
|
|
@@ -521,25 +1310,28 @@ function useChat(options = {}) {
|
|
|
521
1310
|
setError(null);
|
|
522
1311
|
abortControllerRef.current = new AbortController;
|
|
523
1312
|
try {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
+
}
|
|
536
1327
|
if (streaming) {
|
|
537
1328
|
const result = await chatServiceRef.current.stream({
|
|
538
1329
|
conversationId: conversationId ?? undefined,
|
|
539
1330
|
content,
|
|
540
|
-
attachments
|
|
1331
|
+
attachments,
|
|
1332
|
+
skipUserAppend: opts?.skipUserAppend
|
|
541
1333
|
});
|
|
542
|
-
if (!conversationId) {
|
|
1334
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
543
1335
|
setConversationId(result.conversationId);
|
|
544
1336
|
}
|
|
545
1337
|
const assistantMessage = {
|
|
@@ -620,7 +1412,8 @@ function useChat(options = {}) {
|
|
|
620
1412
|
const result = await chatServiceRef.current.send({
|
|
621
1413
|
conversationId: conversationId ?? undefined,
|
|
622
1414
|
content,
|
|
623
|
-
attachments
|
|
1415
|
+
attachments,
|
|
1416
|
+
skipUserAppend: opts?.skipUserAppend
|
|
624
1417
|
});
|
|
625
1418
|
setConversation(result.conversation);
|
|
626
1419
|
setMessages(result.conversation.messages);
|
|
@@ -637,7 +1430,7 @@ function useChat(options = {}) {
|
|
|
637
1430
|
setIsLoading(false);
|
|
638
1431
|
abortControllerRef.current = null;
|
|
639
1432
|
}
|
|
640
|
-
}, [conversationId, streaming, onSend, onResponse, onError, messages]);
|
|
1433
|
+
}, [conversationId, streaming, onSend, onResponse, onError, onUsage, messages, agentMode, store]);
|
|
641
1434
|
const clearConversation = React.useCallback(() => {
|
|
642
1435
|
setMessages([]);
|
|
643
1436
|
setConversation(null);
|
|
@@ -658,6 +1451,44 @@ function useChat(options = {}) {
|
|
|
658
1451
|
abortControllerRef.current?.abort();
|
|
659
1452
|
setIsLoading(false);
|
|
660
1453
|
}, []);
|
|
1454
|
+
const createNewConversation = clearConversation;
|
|
1455
|
+
const editMessage = React.useCallback(async (messageId, newContent) => {
|
|
1456
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1457
|
+
return;
|
|
1458
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
1459
|
+
if (!msg || msg.role !== "user")
|
|
1460
|
+
return;
|
|
1461
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, { content: newContent });
|
|
1462
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
1463
|
+
if (truncated) {
|
|
1464
|
+
setMessages(truncated.messages);
|
|
1465
|
+
}
|
|
1466
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
1467
|
+
}, [conversationId, messages, sendMessage]);
|
|
1468
|
+
const forkConversation = React.useCallback(async (upToMessageId) => {
|
|
1469
|
+
if (!chatServiceRef.current)
|
|
1470
|
+
return null;
|
|
1471
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
1472
|
+
if (!idToFork)
|
|
1473
|
+
return null;
|
|
1474
|
+
try {
|
|
1475
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
1476
|
+
setConversationId(forked.id);
|
|
1477
|
+
setConversation(forked);
|
|
1478
|
+
setMessages(forked.messages);
|
|
1479
|
+
return forked.id;
|
|
1480
|
+
} catch {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
}, [conversationId, conversation]);
|
|
1484
|
+
const updateConversationFn = React.useCallback(async (updates) => {
|
|
1485
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1486
|
+
return null;
|
|
1487
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
1488
|
+
if (updated)
|
|
1489
|
+
setConversation(updated);
|
|
1490
|
+
return updated;
|
|
1491
|
+
}, [conversationId]);
|
|
661
1492
|
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
662
1493
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
663
1494
|
}, []);
|
|
@@ -672,6 +1503,10 @@ function useChat(options = {}) {
|
|
|
672
1503
|
setConversationId,
|
|
673
1504
|
regenerate,
|
|
674
1505
|
stop,
|
|
1506
|
+
createNewConversation,
|
|
1507
|
+
editMessage,
|
|
1508
|
+
forkConversation,
|
|
1509
|
+
updateConversation: updateConversationFn,
|
|
675
1510
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
676
1511
|
};
|
|
677
1512
|
}
|
|
@@ -715,11 +1550,94 @@ function useProviders() {
|
|
|
715
1550
|
refresh: loadProviders
|
|
716
1551
|
};
|
|
717
1552
|
}
|
|
1553
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1554
|
+
import * as React3 from "react";
|
|
1555
|
+
"use client";
|
|
1556
|
+
function useMessageSelection(messageIds) {
|
|
1557
|
+
const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
|
|
1558
|
+
const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1559
|
+
React3.useEffect(() => {
|
|
1560
|
+
setSelectedIds((prev) => {
|
|
1561
|
+
const next = new Set;
|
|
1562
|
+
for (const id of prev) {
|
|
1563
|
+
if (idSet.has(id))
|
|
1564
|
+
next.add(id);
|
|
1565
|
+
}
|
|
1566
|
+
return next.size === prev.size ? prev : next;
|
|
1567
|
+
});
|
|
1568
|
+
}, [idSet]);
|
|
1569
|
+
const toggle = React3.useCallback((id) => {
|
|
1570
|
+
setSelectedIds((prev) => {
|
|
1571
|
+
const next = new Set(prev);
|
|
1572
|
+
if (next.has(id))
|
|
1573
|
+
next.delete(id);
|
|
1574
|
+
else
|
|
1575
|
+
next.add(id);
|
|
1576
|
+
return next;
|
|
1577
|
+
});
|
|
1578
|
+
}, []);
|
|
1579
|
+
const selectAll = React3.useCallback(() => {
|
|
1580
|
+
setSelectedIds(new Set(messageIds));
|
|
1581
|
+
}, [messageIds.join(",")]);
|
|
1582
|
+
const clearSelection = React3.useCallback(() => {
|
|
1583
|
+
setSelectedIds(new Set);
|
|
1584
|
+
}, []);
|
|
1585
|
+
const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1586
|
+
const selectedCount = selectedIds.size;
|
|
1587
|
+
return {
|
|
1588
|
+
selectedIds,
|
|
1589
|
+
toggle,
|
|
1590
|
+
selectAll,
|
|
1591
|
+
clearSelection,
|
|
1592
|
+
isSelected,
|
|
1593
|
+
selectedCount
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
// src/presentation/hooks/useConversations.ts
|
|
1597
|
+
import * as React4 from "react";
|
|
1598
|
+
"use client";
|
|
1599
|
+
function useConversations(options) {
|
|
1600
|
+
const { store, projectId, tags, limit = 50 } = options;
|
|
1601
|
+
const [conversations, setConversations] = React4.useState([]);
|
|
1602
|
+
const [isLoading, setIsLoading] = React4.useState(true);
|
|
1603
|
+
const refresh = React4.useCallback(async () => {
|
|
1604
|
+
setIsLoading(true);
|
|
1605
|
+
try {
|
|
1606
|
+
const list = await store.list({
|
|
1607
|
+
status: "active",
|
|
1608
|
+
projectId,
|
|
1609
|
+
tags,
|
|
1610
|
+
limit
|
|
1611
|
+
});
|
|
1612
|
+
setConversations(list);
|
|
1613
|
+
} finally {
|
|
1614
|
+
setIsLoading(false);
|
|
1615
|
+
}
|
|
1616
|
+
}, [store, projectId, tags, limit]);
|
|
1617
|
+
React4.useEffect(() => {
|
|
1618
|
+
refresh();
|
|
1619
|
+
}, [refresh]);
|
|
1620
|
+
const deleteConversation = React4.useCallback(async (id) => {
|
|
1621
|
+
const ok = await store.delete(id);
|
|
1622
|
+
if (ok) {
|
|
1623
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1624
|
+
}
|
|
1625
|
+
return ok;
|
|
1626
|
+
}, [store]);
|
|
1627
|
+
return {
|
|
1628
|
+
conversations,
|
|
1629
|
+
isLoading,
|
|
1630
|
+
refresh,
|
|
1631
|
+
deleteConversation
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
718
1634
|
|
|
719
1635
|
// src/presentation/hooks/index.ts
|
|
720
1636
|
import { useCompletion } from "@ai-sdk/react";
|
|
721
1637
|
export {
|
|
722
1638
|
useProviders,
|
|
1639
|
+
useMessageSelection,
|
|
1640
|
+
useConversations,
|
|
723
1641
|
useCompletion,
|
|
724
1642
|
useChat
|
|
725
1643
|
};
|