@contractspec/module.ai-chat 4.0.3 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -10
- package/dist/adapters/ai-sdk-bundle-adapter.d.ts +18 -0
- package/dist/adapters/index.d.ts +4 -0
- package/dist/browser/core/index.js +1143 -21
- package/dist/browser/index.js +2813 -631
- package/dist/browser/presentation/components/index.js +3160 -358
- package/dist/browser/presentation/hooks/index.js +978 -43
- package/dist/browser/presentation/index.js +2801 -666
- package/dist/core/agent-adapter.d.ts +53 -0
- package/dist/core/agent-tools-adapter.d.ts +12 -0
- package/dist/core/chat-service.d.ts +49 -1
- package/dist/core/contracts-context.d.ts +46 -0
- package/dist/core/contracts-context.test.d.ts +1 -0
- package/dist/core/conversation-store.d.ts +16 -2
- package/dist/core/create-chat-route.d.ts +3 -0
- package/dist/core/export-formatters.d.ts +29 -0
- package/dist/core/export-formatters.test.d.ts +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +1143 -21
- package/dist/core/local-storage-conversation-store.d.ts +33 -0
- package/dist/core/message-types.d.ts +6 -0
- package/dist/core/surface-planner-tools.d.ts +23 -0
- package/dist/core/surface-planner-tools.test.d.ts +1 -0
- package/dist/core/thinking-levels.d.ts +38 -0
- package/dist/core/thinking-levels.test.d.ts +1 -0
- package/dist/core/workflow-tools.d.ts +18 -0
- package/dist/core/workflow-tools.test.d.ts +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2813 -631
- package/dist/node/core/index.js +1143 -21
- package/dist/node/index.js +2813 -631
- package/dist/node/presentation/components/index.js +3160 -358
- package/dist/node/presentation/hooks/index.js +978 -43
- package/dist/node/presentation/index.js +2804 -669
- package/dist/presentation/components/ChatContainer.d.ts +3 -1
- package/dist/presentation/components/ChatExportToolbar.d.ts +25 -0
- package/dist/presentation/components/ChatMessage.d.ts +16 -1
- package/dist/presentation/components/ChatSidebar.d.ts +26 -0
- package/dist/presentation/components/ChatWithExport.d.ts +34 -0
- package/dist/presentation/components/ChatWithSidebar.d.ts +19 -0
- package/dist/presentation/components/ThinkingLevelPicker.d.ts +16 -0
- package/dist/presentation/components/ToolResultRenderer.d.ts +33 -0
- package/dist/presentation/components/index.d.ts +6 -0
- package/dist/presentation/components/index.js +3160 -358
- package/dist/presentation/hooks/index.d.ts +2 -0
- package/dist/presentation/hooks/index.js +978 -43
- package/dist/presentation/hooks/useChat.d.ts +44 -2
- package/dist/presentation/hooks/useConversations.d.ts +18 -0
- package/dist/presentation/hooks/useMessageSelection.d.ts +13 -0
- package/dist/presentation/index.js +2804 -669
- package/package.json +14 -18
|
@@ -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,509 @@ 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 {
|
|
526
|
+
validatePatchProposal
|
|
527
|
+
} from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
528
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
529
|
+
var VALID_OPS = [
|
|
530
|
+
"insert-node",
|
|
531
|
+
"replace-node",
|
|
532
|
+
"remove-node",
|
|
533
|
+
"move-node",
|
|
534
|
+
"resize-panel",
|
|
535
|
+
"set-layout",
|
|
536
|
+
"reveal-field",
|
|
537
|
+
"hide-field",
|
|
538
|
+
"promote-action",
|
|
539
|
+
"set-focus"
|
|
540
|
+
];
|
|
541
|
+
var DEFAULT_NODE_KINDS = [
|
|
542
|
+
"entity-section",
|
|
543
|
+
"entity-card",
|
|
544
|
+
"data-view",
|
|
545
|
+
"assistant-panel",
|
|
546
|
+
"chat-thread",
|
|
547
|
+
"action-bar",
|
|
548
|
+
"timeline",
|
|
549
|
+
"table",
|
|
550
|
+
"rich-doc",
|
|
551
|
+
"form",
|
|
552
|
+
"chart",
|
|
553
|
+
"custom-widget"
|
|
554
|
+
];
|
|
555
|
+
function collectSlotIdsFromRegion(node) {
|
|
556
|
+
const ids = [];
|
|
557
|
+
if (node.type === "slot") {
|
|
558
|
+
ids.push(node.slotId);
|
|
559
|
+
}
|
|
560
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
561
|
+
for (const child of node.children) {
|
|
562
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (node.type === "tabs") {
|
|
566
|
+
for (const tab of node.tabs) {
|
|
567
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (node.type === "floating") {
|
|
571
|
+
ids.push(node.anchorSlotId);
|
|
572
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
573
|
+
}
|
|
574
|
+
return ids;
|
|
575
|
+
}
|
|
576
|
+
function deriveConstraints(plan) {
|
|
577
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
578
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
579
|
+
return {
|
|
580
|
+
allowedOps: VALID_OPS,
|
|
581
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
582
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
var ProposePatchInputSchema = z3.object({
|
|
586
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
587
|
+
ops: z3.array(z3.object({
|
|
588
|
+
op: z3.enum([
|
|
589
|
+
"insert-node",
|
|
590
|
+
"replace-node",
|
|
591
|
+
"remove-node",
|
|
592
|
+
"move-node",
|
|
593
|
+
"resize-panel",
|
|
594
|
+
"set-layout",
|
|
595
|
+
"reveal-field",
|
|
596
|
+
"hide-field",
|
|
597
|
+
"promote-action",
|
|
598
|
+
"set-focus"
|
|
599
|
+
]),
|
|
600
|
+
slotId: z3.string().optional(),
|
|
601
|
+
nodeId: z3.string().optional(),
|
|
602
|
+
toSlotId: z3.string().optional(),
|
|
603
|
+
index: z3.number().optional(),
|
|
604
|
+
node: z3.object({
|
|
605
|
+
nodeId: z3.string(),
|
|
606
|
+
kind: z3.string(),
|
|
607
|
+
title: z3.string().optional(),
|
|
608
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
609
|
+
children: z3.array(z3.unknown()).optional()
|
|
610
|
+
}).optional(),
|
|
611
|
+
persistKey: z3.string().optional(),
|
|
612
|
+
sizes: z3.array(z3.number()).optional(),
|
|
613
|
+
layoutId: z3.string().optional(),
|
|
614
|
+
fieldId: z3.string().optional(),
|
|
615
|
+
actionId: z3.string().optional(),
|
|
616
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
617
|
+
targetId: z3.string().optional()
|
|
618
|
+
}))
|
|
619
|
+
});
|
|
620
|
+
function createSurfacePlannerTools(config) {
|
|
621
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
622
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
623
|
+
const proposePatchTool = tool3({
|
|
624
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
625
|
+
inputSchema: ProposePatchInputSchema,
|
|
626
|
+
execute: async (input) => {
|
|
627
|
+
const ops = input.ops;
|
|
628
|
+
try {
|
|
629
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
630
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
631
|
+
onPatchProposal?.(proposal);
|
|
632
|
+
return {
|
|
633
|
+
success: true,
|
|
634
|
+
proposalId: proposal.proposalId,
|
|
635
|
+
opsCount: proposal.ops.length,
|
|
636
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
637
|
+
};
|
|
638
|
+
} catch (err) {
|
|
639
|
+
return {
|
|
640
|
+
success: false,
|
|
641
|
+
error: err instanceof Error ? err.message : String(err),
|
|
642
|
+
proposalId: input.proposalId
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
return {
|
|
648
|
+
"propose-patch": proposePatchTool
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function buildPlannerPromptInput(plan) {
|
|
652
|
+
const constraints = deriveConstraints(plan);
|
|
653
|
+
return {
|
|
654
|
+
bundleMeta: {
|
|
655
|
+
key: plan.bundleKey,
|
|
656
|
+
version: "0.0.0",
|
|
657
|
+
title: plan.bundleKey
|
|
658
|
+
},
|
|
659
|
+
surfaceId: plan.surfaceId,
|
|
660
|
+
allowedPatchOps: constraints.allowedOps,
|
|
661
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
662
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
663
|
+
actions: plan.actions.map((a) => ({
|
|
664
|
+
actionId: a.actionId,
|
|
665
|
+
title: a.title
|
|
666
|
+
})),
|
|
667
|
+
preferences: {
|
|
668
|
+
guidance: "hints",
|
|
669
|
+
density: "standard",
|
|
670
|
+
dataDepth: "detailed",
|
|
671
|
+
control: "standard",
|
|
672
|
+
media: "text",
|
|
673
|
+
pace: "balanced",
|
|
674
|
+
narrative: "top-down"
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
124
679
|
// src/core/chat-service.ts
|
|
680
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
125
681
|
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
126
682
|
|
|
127
683
|
Your capabilities:
|
|
@@ -136,6 +692,9 @@ Guidelines:
|
|
|
136
692
|
- Reference relevant ContractSpec concepts and patterns
|
|
137
693
|
- Ask clarifying questions when the user's intent is unclear
|
|
138
694
|
- When suggesting code changes, explain the rationale`;
|
|
695
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
696
|
+
|
|
697
|
+
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
698
|
|
|
140
699
|
class ChatService {
|
|
141
700
|
provider;
|
|
@@ -145,19 +704,93 @@ class ChatService {
|
|
|
145
704
|
maxHistoryMessages;
|
|
146
705
|
onUsage;
|
|
147
706
|
tools;
|
|
707
|
+
thinkingLevel;
|
|
148
708
|
sendReasoning;
|
|
149
709
|
sendSources;
|
|
710
|
+
modelSelector;
|
|
150
711
|
constructor(config) {
|
|
151
712
|
this.provider = config.provider;
|
|
152
713
|
this.context = config.context;
|
|
153
714
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
154
|
-
this.systemPrompt = config
|
|
715
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
155
716
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
156
717
|
this.onUsage = config.onUsage;
|
|
157
|
-
this.tools = config
|
|
158
|
-
this.
|
|
718
|
+
this.tools = this.mergeTools(config);
|
|
719
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
720
|
+
this.modelSelector = config.modelSelector;
|
|
721
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
159
722
|
this.sendSources = config.sendSources ?? false;
|
|
160
723
|
}
|
|
724
|
+
buildSystemPrompt(config) {
|
|
725
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
726
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
727
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
728
|
+
}
|
|
729
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
730
|
+
if (contractsPrompt) {
|
|
731
|
+
base += contractsPrompt;
|
|
732
|
+
}
|
|
733
|
+
if (config.surfacePlanConfig?.plan) {
|
|
734
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
735
|
+
base += `
|
|
736
|
+
|
|
737
|
+
` + compilePlannerPrompt(plannerInput);
|
|
738
|
+
}
|
|
739
|
+
return base;
|
|
740
|
+
}
|
|
741
|
+
mergeTools(config) {
|
|
742
|
+
let merged = config.tools ?? {};
|
|
743
|
+
const wfConfig = config.workflowToolsConfig;
|
|
744
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
745
|
+
const workflowTools = createWorkflowTools({
|
|
746
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
747
|
+
composer: wfConfig.composer
|
|
748
|
+
});
|
|
749
|
+
merged = { ...merged, ...workflowTools };
|
|
750
|
+
}
|
|
751
|
+
const contractsCtx = config.contractsContext;
|
|
752
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
753
|
+
const allTools = [];
|
|
754
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
755
|
+
if (agent.tools?.length)
|
|
756
|
+
allTools.push(...agent.tools);
|
|
757
|
+
}
|
|
758
|
+
if (allTools.length > 0) {
|
|
759
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
760
|
+
merged = { ...merged, ...agentTools };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
764
|
+
if (surfaceConfig?.plan) {
|
|
765
|
+
const plannerTools = createSurfacePlannerTools({
|
|
766
|
+
plan: surfaceConfig.plan,
|
|
767
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
768
|
+
});
|
|
769
|
+
merged = { ...merged, ...plannerTools };
|
|
770
|
+
}
|
|
771
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
772
|
+
merged = { ...merged, ...config.mcpTools };
|
|
773
|
+
}
|
|
774
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
775
|
+
}
|
|
776
|
+
async resolveModel() {
|
|
777
|
+
if (this.modelSelector) {
|
|
778
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
779
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
780
|
+
taskDimension: dimension
|
|
781
|
+
});
|
|
782
|
+
return { model, providerName: selection.providerKey };
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
model: this.provider.getModel(),
|
|
786
|
+
providerName: this.provider.name
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
thinkingLevelToDimension(level) {
|
|
790
|
+
if (!level || level === "instant")
|
|
791
|
+
return "latency";
|
|
792
|
+
return "reasoning";
|
|
793
|
+
}
|
|
161
794
|
async send(options) {
|
|
162
795
|
let conversation;
|
|
163
796
|
if (options.conversationId) {
|
|
@@ -175,20 +808,25 @@ class ChatService {
|
|
|
175
808
|
workspacePath: this.context?.workspacePath
|
|
176
809
|
});
|
|
177
810
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
811
|
+
if (!options.skipUserAppend) {
|
|
812
|
+
await this.store.appendMessage(conversation.id, {
|
|
813
|
+
role: "user",
|
|
814
|
+
content: options.content,
|
|
815
|
+
status: "completed",
|
|
816
|
+
attachments: options.attachments
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
184
820
|
const messages = this.buildMessages(conversation, options);
|
|
185
|
-
const model = this.
|
|
821
|
+
const { model, providerName } = await this.resolveModel();
|
|
822
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
186
823
|
try {
|
|
187
824
|
const result = await generateText({
|
|
188
825
|
model,
|
|
189
826
|
messages,
|
|
190
827
|
system: this.systemPrompt,
|
|
191
|
-
tools: this.tools
|
|
828
|
+
tools: this.tools,
|
|
829
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
192
830
|
});
|
|
193
831
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
194
832
|
role: "assistant",
|
|
@@ -233,23 +871,27 @@ class ChatService {
|
|
|
233
871
|
workspacePath: this.context?.workspacePath
|
|
234
872
|
});
|
|
235
873
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
874
|
+
if (!options.skipUserAppend) {
|
|
875
|
+
await this.store.appendMessage(conversation.id, {
|
|
876
|
+
role: "user",
|
|
877
|
+
content: options.content,
|
|
878
|
+
status: "completed",
|
|
879
|
+
attachments: options.attachments
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
242
883
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
243
884
|
role: "assistant",
|
|
244
885
|
content: "",
|
|
245
886
|
status: "streaming"
|
|
246
887
|
});
|
|
247
888
|
const messages = this.buildMessages(conversation, options);
|
|
248
|
-
const model = this.
|
|
889
|
+
const { model, providerName } = await this.resolveModel();
|
|
249
890
|
const systemPrompt = this.systemPrompt;
|
|
250
891
|
const tools = this.tools;
|
|
251
892
|
const store = this.store;
|
|
252
893
|
const onUsage = this.onUsage;
|
|
894
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
253
895
|
async function* streamGenerator() {
|
|
254
896
|
let fullContent = "";
|
|
255
897
|
let fullReasoning = "";
|
|
@@ -260,7 +902,8 @@ class ChatService {
|
|
|
260
902
|
model,
|
|
261
903
|
messages,
|
|
262
904
|
system: systemPrompt,
|
|
263
|
-
tools
|
|
905
|
+
tools,
|
|
906
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
264
907
|
});
|
|
265
908
|
for await (const part of result.fullStream) {
|
|
266
909
|
if (part.type === "text-delta") {
|
|
@@ -375,6 +1018,18 @@ class ChatService {
|
|
|
375
1018
|
...options
|
|
376
1019
|
});
|
|
377
1020
|
}
|
|
1021
|
+
async updateConversation(conversationId, updates) {
|
|
1022
|
+
return this.store.update(conversationId, updates);
|
|
1023
|
+
}
|
|
1024
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
1025
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
1026
|
+
}
|
|
1027
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
1028
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
1029
|
+
}
|
|
1030
|
+
async truncateAfter(conversationId, messageId) {
|
|
1031
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
1032
|
+
}
|
|
378
1033
|
async deleteConversation(conversationId) {
|
|
379
1034
|
return this.store.delete(conversationId);
|
|
380
1035
|
}
|
|
@@ -445,9 +1100,9 @@ import {
|
|
|
445
1100
|
function toolsToToolSet(defs) {
|
|
446
1101
|
const result = {};
|
|
447
1102
|
for (const def of defs) {
|
|
448
|
-
result[def.name] =
|
|
1103
|
+
result[def.name] = tool4({
|
|
449
1104
|
description: def.description ?? def.name,
|
|
450
|
-
inputSchema:
|
|
1105
|
+
inputSchema: z4.object({}).passthrough(),
|
|
451
1106
|
execute: async () => ({})
|
|
452
1107
|
});
|
|
453
1108
|
}
|
|
@@ -461,21 +1116,63 @@ function useChat(options = {}) {
|
|
|
461
1116
|
apiKey,
|
|
462
1117
|
proxyUrl,
|
|
463
1118
|
conversationId: initialConversationId,
|
|
1119
|
+
store,
|
|
464
1120
|
systemPrompt,
|
|
465
1121
|
streaming = true,
|
|
466
1122
|
onSend,
|
|
467
1123
|
onResponse,
|
|
468
1124
|
onError,
|
|
469
1125
|
onUsage,
|
|
470
|
-
tools: toolsDefs
|
|
1126
|
+
tools: toolsDefs,
|
|
1127
|
+
thinkingLevel,
|
|
1128
|
+
workflowToolsConfig,
|
|
1129
|
+
modelSelector,
|
|
1130
|
+
contractsContext,
|
|
1131
|
+
surfacePlanConfig,
|
|
1132
|
+
mcpServers,
|
|
1133
|
+
agentMode
|
|
471
1134
|
} = options;
|
|
472
1135
|
const [messages, setMessages] = React.useState([]);
|
|
1136
|
+
const [mcpTools, setMcpTools] = React.useState(null);
|
|
1137
|
+
const mcpCleanupRef = React.useRef(null);
|
|
473
1138
|
const [conversation, setConversation] = React.useState(null);
|
|
474
1139
|
const [isLoading, setIsLoading] = React.useState(false);
|
|
475
1140
|
const [error, setError] = React.useState(null);
|
|
476
1141
|
const [conversationId, setConversationId] = React.useState(initialConversationId ?? null);
|
|
477
1142
|
const abortControllerRef = React.useRef(null);
|
|
478
1143
|
const chatServiceRef = React.useRef(null);
|
|
1144
|
+
React.useEffect(() => {
|
|
1145
|
+
if (!mcpServers?.length) {
|
|
1146
|
+
setMcpTools(null);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
let cancelled = false;
|
|
1150
|
+
import("@contractspec/lib.ai-agent/tools/mcp-client").then(({ createMcpToolsets }) => {
|
|
1151
|
+
createMcpToolsets(mcpServers).then(({ tools, cleanup }) => {
|
|
1152
|
+
if (!cancelled) {
|
|
1153
|
+
setMcpTools(tools);
|
|
1154
|
+
mcpCleanupRef.current = cleanup;
|
|
1155
|
+
} else {
|
|
1156
|
+
cleanup().catch(() => {
|
|
1157
|
+
return;
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}).catch(() => {
|
|
1161
|
+
if (!cancelled)
|
|
1162
|
+
setMcpTools(null);
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
return () => {
|
|
1166
|
+
cancelled = true;
|
|
1167
|
+
const cleanup = mcpCleanupRef.current;
|
|
1168
|
+
mcpCleanupRef.current = null;
|
|
1169
|
+
if (cleanup)
|
|
1170
|
+
cleanup().catch(() => {
|
|
1171
|
+
return;
|
|
1172
|
+
});
|
|
1173
|
+
setMcpTools(null);
|
|
1174
|
+
};
|
|
1175
|
+
}, [mcpServers]);
|
|
479
1176
|
React.useEffect(() => {
|
|
480
1177
|
const chatProvider = createProvider({
|
|
481
1178
|
provider,
|
|
@@ -485,9 +1182,16 @@ function useChat(options = {}) {
|
|
|
485
1182
|
});
|
|
486
1183
|
chatServiceRef.current = new ChatService({
|
|
487
1184
|
provider: chatProvider,
|
|
1185
|
+
store,
|
|
488
1186
|
systemPrompt,
|
|
489
1187
|
onUsage,
|
|
490
|
-
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined
|
|
1188
|
+
tools: toolsDefs?.length ? toolsToToolSet(toolsDefs) : undefined,
|
|
1189
|
+
thinkingLevel,
|
|
1190
|
+
workflowToolsConfig,
|
|
1191
|
+
modelSelector,
|
|
1192
|
+
contractsContext,
|
|
1193
|
+
surfacePlanConfig,
|
|
1194
|
+
mcpTools
|
|
491
1195
|
});
|
|
492
1196
|
}, [
|
|
493
1197
|
provider,
|
|
@@ -495,9 +1199,16 @@ function useChat(options = {}) {
|
|
|
495
1199
|
model,
|
|
496
1200
|
apiKey,
|
|
497
1201
|
proxyUrl,
|
|
1202
|
+
store,
|
|
498
1203
|
systemPrompt,
|
|
499
1204
|
onUsage,
|
|
500
|
-
toolsDefs
|
|
1205
|
+
toolsDefs,
|
|
1206
|
+
thinkingLevel,
|
|
1207
|
+
workflowToolsConfig,
|
|
1208
|
+
modelSelector,
|
|
1209
|
+
contractsContext,
|
|
1210
|
+
surfacePlanConfig,
|
|
1211
|
+
mcpTools
|
|
501
1212
|
]);
|
|
502
1213
|
React.useEffect(() => {
|
|
503
1214
|
if (!conversationId || !chatServiceRef.current)
|
|
@@ -513,7 +1224,90 @@ function useChat(options = {}) {
|
|
|
513
1224
|
};
|
|
514
1225
|
loadConversation().catch(console.error);
|
|
515
1226
|
}, [conversationId]);
|
|
516
|
-
const sendMessage = React.useCallback(async (content, attachments) => {
|
|
1227
|
+
const sendMessage = React.useCallback(async (content, attachments, opts) => {
|
|
1228
|
+
if (agentMode?.agent) {
|
|
1229
|
+
setIsLoading(true);
|
|
1230
|
+
setError(null);
|
|
1231
|
+
abortControllerRef.current = new AbortController;
|
|
1232
|
+
try {
|
|
1233
|
+
if (!opts?.skipUserAppend) {
|
|
1234
|
+
const userMessage = {
|
|
1235
|
+
id: `msg_${Date.now()}`,
|
|
1236
|
+
conversationId: conversationId ?? "",
|
|
1237
|
+
role: "user",
|
|
1238
|
+
content,
|
|
1239
|
+
status: "completed",
|
|
1240
|
+
createdAt: new Date,
|
|
1241
|
+
updatedAt: new Date,
|
|
1242
|
+
attachments
|
|
1243
|
+
};
|
|
1244
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1245
|
+
onSend?.(userMessage);
|
|
1246
|
+
}
|
|
1247
|
+
const result = await agentMode.agent.generate({
|
|
1248
|
+
prompt: content,
|
|
1249
|
+
signal: abortControllerRef.current.signal
|
|
1250
|
+
});
|
|
1251
|
+
const toolCallsMap = new Map;
|
|
1252
|
+
for (const tc of result.toolCalls ?? []) {
|
|
1253
|
+
const tr = result.toolResults?.find((r) => r.toolCallId === tc.toolCallId);
|
|
1254
|
+
toolCallsMap.set(tc.toolCallId, {
|
|
1255
|
+
id: tc.toolCallId,
|
|
1256
|
+
name: tc.toolName,
|
|
1257
|
+
args: tc.args ?? {},
|
|
1258
|
+
result: tr?.output,
|
|
1259
|
+
status: "completed"
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
const assistantMessage = {
|
|
1263
|
+
id: `msg_${Date.now()}_a`,
|
|
1264
|
+
conversationId: conversationId ?? "",
|
|
1265
|
+
role: "assistant",
|
|
1266
|
+
content: result.text,
|
|
1267
|
+
status: "completed",
|
|
1268
|
+
createdAt: new Date,
|
|
1269
|
+
updatedAt: new Date,
|
|
1270
|
+
toolCalls: toolCallsMap.size ? Array.from(toolCallsMap.values()) : undefined,
|
|
1271
|
+
usage: result.usage
|
|
1272
|
+
};
|
|
1273
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
1274
|
+
onResponse?.(assistantMessage);
|
|
1275
|
+
onUsage?.(result.usage ?? { inputTokens: 0, outputTokens: 0 });
|
|
1276
|
+
if (store && !conversationId) {
|
|
1277
|
+
const conv = await store.create({
|
|
1278
|
+
status: "active",
|
|
1279
|
+
provider: "agent",
|
|
1280
|
+
model: "agent",
|
|
1281
|
+
messages: []
|
|
1282
|
+
});
|
|
1283
|
+
if (!opts?.skipUserAppend) {
|
|
1284
|
+
await store.appendMessage(conv.id, {
|
|
1285
|
+
role: "user",
|
|
1286
|
+
content,
|
|
1287
|
+
status: "completed",
|
|
1288
|
+
attachments
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
await store.appendMessage(conv.id, {
|
|
1292
|
+
role: "assistant",
|
|
1293
|
+
content: result.text,
|
|
1294
|
+
status: "completed",
|
|
1295
|
+
toolCalls: assistantMessage.toolCalls,
|
|
1296
|
+
usage: result.usage
|
|
1297
|
+
});
|
|
1298
|
+
const updated = await store.get(conv.id);
|
|
1299
|
+
if (updated)
|
|
1300
|
+
setConversation(updated);
|
|
1301
|
+
setConversationId(conv.id);
|
|
1302
|
+
}
|
|
1303
|
+
} catch (err) {
|
|
1304
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1305
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
1306
|
+
} finally {
|
|
1307
|
+
setIsLoading(false);
|
|
1308
|
+
}
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
517
1311
|
if (!chatServiceRef.current) {
|
|
518
1312
|
throw new Error("Chat service not initialized");
|
|
519
1313
|
}
|
|
@@ -521,25 +1315,28 @@ function useChat(options = {}) {
|
|
|
521
1315
|
setError(null);
|
|
522
1316
|
abortControllerRef.current = new AbortController;
|
|
523
1317
|
try {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1318
|
+
if (!opts?.skipUserAppend) {
|
|
1319
|
+
const userMessage = {
|
|
1320
|
+
id: `msg_${Date.now()}`,
|
|
1321
|
+
conversationId: conversationId ?? "",
|
|
1322
|
+
role: "user",
|
|
1323
|
+
content,
|
|
1324
|
+
status: "completed",
|
|
1325
|
+
createdAt: new Date,
|
|
1326
|
+
updatedAt: new Date,
|
|
1327
|
+
attachments
|
|
1328
|
+
};
|
|
1329
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
1330
|
+
onSend?.(userMessage);
|
|
1331
|
+
}
|
|
536
1332
|
if (streaming) {
|
|
537
1333
|
const result = await chatServiceRef.current.stream({
|
|
538
1334
|
conversationId: conversationId ?? undefined,
|
|
539
1335
|
content,
|
|
540
|
-
attachments
|
|
1336
|
+
attachments,
|
|
1337
|
+
skipUserAppend: opts?.skipUserAppend
|
|
541
1338
|
});
|
|
542
|
-
if (!conversationId) {
|
|
1339
|
+
if (!conversationId && !opts?.skipUserAppend) {
|
|
543
1340
|
setConversationId(result.conversationId);
|
|
544
1341
|
}
|
|
545
1342
|
const assistantMessage = {
|
|
@@ -620,7 +1417,8 @@ function useChat(options = {}) {
|
|
|
620
1417
|
const result = await chatServiceRef.current.send({
|
|
621
1418
|
conversationId: conversationId ?? undefined,
|
|
622
1419
|
content,
|
|
623
|
-
attachments
|
|
1420
|
+
attachments,
|
|
1421
|
+
skipUserAppend: opts?.skipUserAppend
|
|
624
1422
|
});
|
|
625
1423
|
setConversation(result.conversation);
|
|
626
1424
|
setMessages(result.conversation.messages);
|
|
@@ -637,7 +1435,17 @@ function useChat(options = {}) {
|
|
|
637
1435
|
setIsLoading(false);
|
|
638
1436
|
abortControllerRef.current = null;
|
|
639
1437
|
}
|
|
640
|
-
}, [
|
|
1438
|
+
}, [
|
|
1439
|
+
conversationId,
|
|
1440
|
+
streaming,
|
|
1441
|
+
onSend,
|
|
1442
|
+
onResponse,
|
|
1443
|
+
onError,
|
|
1444
|
+
onUsage,
|
|
1445
|
+
messages,
|
|
1446
|
+
agentMode,
|
|
1447
|
+
store
|
|
1448
|
+
]);
|
|
641
1449
|
const clearConversation = React.useCallback(() => {
|
|
642
1450
|
setMessages([]);
|
|
643
1451
|
setConversation(null);
|
|
@@ -658,6 +1466,46 @@ function useChat(options = {}) {
|
|
|
658
1466
|
abortControllerRef.current?.abort();
|
|
659
1467
|
setIsLoading(false);
|
|
660
1468
|
}, []);
|
|
1469
|
+
const createNewConversation = clearConversation;
|
|
1470
|
+
const editMessage = React.useCallback(async (messageId, newContent) => {
|
|
1471
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1472
|
+
return;
|
|
1473
|
+
const msg = messages.find((m) => m.id === messageId);
|
|
1474
|
+
if (!msg || msg.role !== "user")
|
|
1475
|
+
return;
|
|
1476
|
+
await chatServiceRef.current.updateMessage(conversationId, messageId, {
|
|
1477
|
+
content: newContent
|
|
1478
|
+
});
|
|
1479
|
+
const truncated = await chatServiceRef.current.truncateAfter(conversationId, messageId);
|
|
1480
|
+
if (truncated) {
|
|
1481
|
+
setMessages(truncated.messages);
|
|
1482
|
+
}
|
|
1483
|
+
await sendMessage(newContent, undefined, { skipUserAppend: true });
|
|
1484
|
+
}, [conversationId, messages, sendMessage]);
|
|
1485
|
+
const forkConversation = React.useCallback(async (upToMessageId) => {
|
|
1486
|
+
if (!chatServiceRef.current)
|
|
1487
|
+
return null;
|
|
1488
|
+
const idToFork = conversationId ?? conversation?.id;
|
|
1489
|
+
if (!idToFork)
|
|
1490
|
+
return null;
|
|
1491
|
+
try {
|
|
1492
|
+
const forked = await chatServiceRef.current.forkConversation(idToFork, upToMessageId);
|
|
1493
|
+
setConversationId(forked.id);
|
|
1494
|
+
setConversation(forked);
|
|
1495
|
+
setMessages(forked.messages);
|
|
1496
|
+
return forked.id;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
}, [conversationId, conversation]);
|
|
1501
|
+
const updateConversationFn = React.useCallback(async (updates) => {
|
|
1502
|
+
if (!chatServiceRef.current || !conversationId)
|
|
1503
|
+
return null;
|
|
1504
|
+
const updated = await chatServiceRef.current.updateConversation(conversationId, updates);
|
|
1505
|
+
if (updated)
|
|
1506
|
+
setConversation(updated);
|
|
1507
|
+
return updated;
|
|
1508
|
+
}, [conversationId]);
|
|
661
1509
|
const addToolApprovalResponse = React.useCallback((_toolCallId, _result) => {
|
|
662
1510
|
throw new Error(`addToolApprovalResponse: Tool approval requires server route with toUIMessageStreamResponse. ` + `Use createChatRoute and @ai-sdk/react useChat for tools with requireApproval.`);
|
|
663
1511
|
}, []);
|
|
@@ -672,6 +1520,10 @@ function useChat(options = {}) {
|
|
|
672
1520
|
setConversationId,
|
|
673
1521
|
regenerate,
|
|
674
1522
|
stop,
|
|
1523
|
+
createNewConversation,
|
|
1524
|
+
editMessage,
|
|
1525
|
+
forkConversation,
|
|
1526
|
+
updateConversation: updateConversationFn,
|
|
675
1527
|
...hasApprovalTools && { addToolApprovalResponse }
|
|
676
1528
|
};
|
|
677
1529
|
}
|
|
@@ -715,11 +1567,94 @@ function useProviders() {
|
|
|
715
1567
|
refresh: loadProviders
|
|
716
1568
|
};
|
|
717
1569
|
}
|
|
1570
|
+
// src/presentation/hooks/useMessageSelection.ts
|
|
1571
|
+
import * as React3 from "react";
|
|
1572
|
+
"use client";
|
|
1573
|
+
function useMessageSelection(messageIds) {
|
|
1574
|
+
const [selectedIds, setSelectedIds] = React3.useState(() => new Set);
|
|
1575
|
+
const idSet = React3.useMemo(() => new Set(messageIds), [messageIds.join(",")]);
|
|
1576
|
+
React3.useEffect(() => {
|
|
1577
|
+
setSelectedIds((prev) => {
|
|
1578
|
+
const next = new Set;
|
|
1579
|
+
for (const id of prev) {
|
|
1580
|
+
if (idSet.has(id))
|
|
1581
|
+
next.add(id);
|
|
1582
|
+
}
|
|
1583
|
+
return next.size === prev.size ? prev : next;
|
|
1584
|
+
});
|
|
1585
|
+
}, [idSet]);
|
|
1586
|
+
const toggle = React3.useCallback((id) => {
|
|
1587
|
+
setSelectedIds((prev) => {
|
|
1588
|
+
const next = new Set(prev);
|
|
1589
|
+
if (next.has(id))
|
|
1590
|
+
next.delete(id);
|
|
1591
|
+
else
|
|
1592
|
+
next.add(id);
|
|
1593
|
+
return next;
|
|
1594
|
+
});
|
|
1595
|
+
}, []);
|
|
1596
|
+
const selectAll = React3.useCallback(() => {
|
|
1597
|
+
setSelectedIds(new Set(messageIds));
|
|
1598
|
+
}, [messageIds.join(",")]);
|
|
1599
|
+
const clearSelection = React3.useCallback(() => {
|
|
1600
|
+
setSelectedIds(new Set);
|
|
1601
|
+
}, []);
|
|
1602
|
+
const isSelected = React3.useCallback((id) => selectedIds.has(id), [selectedIds]);
|
|
1603
|
+
const selectedCount = selectedIds.size;
|
|
1604
|
+
return {
|
|
1605
|
+
selectedIds,
|
|
1606
|
+
toggle,
|
|
1607
|
+
selectAll,
|
|
1608
|
+
clearSelection,
|
|
1609
|
+
isSelected,
|
|
1610
|
+
selectedCount
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
// src/presentation/hooks/useConversations.ts
|
|
1614
|
+
import * as React4 from "react";
|
|
1615
|
+
"use client";
|
|
1616
|
+
function useConversations(options) {
|
|
1617
|
+
const { store, projectId, tags, limit = 50 } = options;
|
|
1618
|
+
const [conversations, setConversations] = React4.useState([]);
|
|
1619
|
+
const [isLoading, setIsLoading] = React4.useState(true);
|
|
1620
|
+
const refresh = React4.useCallback(async () => {
|
|
1621
|
+
setIsLoading(true);
|
|
1622
|
+
try {
|
|
1623
|
+
const list = await store.list({
|
|
1624
|
+
status: "active",
|
|
1625
|
+
projectId,
|
|
1626
|
+
tags,
|
|
1627
|
+
limit
|
|
1628
|
+
});
|
|
1629
|
+
setConversations(list);
|
|
1630
|
+
} finally {
|
|
1631
|
+
setIsLoading(false);
|
|
1632
|
+
}
|
|
1633
|
+
}, [store, projectId, tags, limit]);
|
|
1634
|
+
React4.useEffect(() => {
|
|
1635
|
+
refresh();
|
|
1636
|
+
}, [refresh]);
|
|
1637
|
+
const deleteConversation = React4.useCallback(async (id) => {
|
|
1638
|
+
const ok = await store.delete(id);
|
|
1639
|
+
if (ok) {
|
|
1640
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
1641
|
+
}
|
|
1642
|
+
return ok;
|
|
1643
|
+
}, [store]);
|
|
1644
|
+
return {
|
|
1645
|
+
conversations,
|
|
1646
|
+
isLoading,
|
|
1647
|
+
refresh,
|
|
1648
|
+
deleteConversation
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
718
1651
|
|
|
719
1652
|
// src/presentation/hooks/index.ts
|
|
720
1653
|
import { useCompletion } from "@ai-sdk/react";
|
|
721
1654
|
export {
|
|
722
1655
|
useProviders,
|
|
1656
|
+
useMessageSelection,
|
|
1657
|
+
useConversations,
|
|
723
1658
|
useCompletion,
|
|
724
1659
|
useChat
|
|
725
1660
|
};
|