@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
|
@@ -82,11 +82,65 @@ class InMemoryConversationStore {
|
|
|
82
82
|
if (options?.status) {
|
|
83
83
|
results = results.filter((c) => c.status === options.status);
|
|
84
84
|
}
|
|
85
|
+
if (options?.projectId) {
|
|
86
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
87
|
+
}
|
|
88
|
+
if (options?.tags && options.tags.length > 0) {
|
|
89
|
+
const tagSet = new Set(options.tags);
|
|
90
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
91
|
+
}
|
|
85
92
|
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
86
93
|
const offset = options?.offset ?? 0;
|
|
87
94
|
const limit = options?.limit ?? 100;
|
|
88
95
|
return results.slice(offset, offset + limit);
|
|
89
96
|
}
|
|
97
|
+
async fork(conversationId, upToMessageId) {
|
|
98
|
+
const source = this.conversations.get(conversationId);
|
|
99
|
+
if (!source) {
|
|
100
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
101
|
+
}
|
|
102
|
+
let messagesToCopy = source.messages;
|
|
103
|
+
if (upToMessageId) {
|
|
104
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
105
|
+
if (idx === -1) {
|
|
106
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
107
|
+
}
|
|
108
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
109
|
+
}
|
|
110
|
+
const now = new Date;
|
|
111
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
112
|
+
...m,
|
|
113
|
+
id: generateId("msg"),
|
|
114
|
+
conversationId: "",
|
|
115
|
+
createdAt: new Date(m.createdAt),
|
|
116
|
+
updatedAt: new Date(m.updatedAt)
|
|
117
|
+
}));
|
|
118
|
+
const forked = {
|
|
119
|
+
...source,
|
|
120
|
+
id: generateId("conv"),
|
|
121
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
122
|
+
forkedFromId: source.id,
|
|
123
|
+
createdAt: now,
|
|
124
|
+
updatedAt: now,
|
|
125
|
+
messages: forkedMessages
|
|
126
|
+
};
|
|
127
|
+
for (const m of forked.messages) {
|
|
128
|
+
m.conversationId = forked.id;
|
|
129
|
+
}
|
|
130
|
+
this.conversations.set(forked.id, forked);
|
|
131
|
+
return forked;
|
|
132
|
+
}
|
|
133
|
+
async truncateAfter(conversationId, messageId) {
|
|
134
|
+
const conv = this.conversations.get(conversationId);
|
|
135
|
+
if (!conv)
|
|
136
|
+
return null;
|
|
137
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
138
|
+
if (idx === -1)
|
|
139
|
+
return null;
|
|
140
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
141
|
+
conv.updatedAt = new Date;
|
|
142
|
+
return conv;
|
|
143
|
+
}
|
|
90
144
|
async search(query, limit = 20) {
|
|
91
145
|
const lowerQuery = query.toLowerCase();
|
|
92
146
|
const results = [];
|
|
@@ -113,6 +167,510 @@ function createInMemoryConversationStore() {
|
|
|
113
167
|
}
|
|
114
168
|
// src/core/chat-service.ts
|
|
115
169
|
import { generateText, streamText } from "ai";
|
|
170
|
+
|
|
171
|
+
// src/core/thinking-levels.ts
|
|
172
|
+
var THINKING_LEVEL_LABELS = {
|
|
173
|
+
instant: "Instant",
|
|
174
|
+
thinking: "Thinking",
|
|
175
|
+
extra_thinking: "Extra Thinking",
|
|
176
|
+
max: "Max"
|
|
177
|
+
};
|
|
178
|
+
var THINKING_LEVEL_DESCRIPTIONS = {
|
|
179
|
+
instant: "Fast responses, minimal reasoning",
|
|
180
|
+
thinking: "Standard reasoning depth",
|
|
181
|
+
extra_thinking: "More thorough reasoning",
|
|
182
|
+
max: "Maximum reasoning depth"
|
|
183
|
+
};
|
|
184
|
+
function getProviderOptions(level, providerName) {
|
|
185
|
+
if (!level || level === "instant") {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
switch (providerName) {
|
|
189
|
+
case "anthropic": {
|
|
190
|
+
const budgetMap = {
|
|
191
|
+
thinking: 8000,
|
|
192
|
+
extra_thinking: 16000,
|
|
193
|
+
max: 32000
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
anthropic: {
|
|
197
|
+
thinking: { type: "enabled", budgetTokens: budgetMap[level] }
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
case "openai": {
|
|
202
|
+
const effortMap = {
|
|
203
|
+
thinking: "low",
|
|
204
|
+
extra_thinking: "medium",
|
|
205
|
+
max: "high"
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
openai: {
|
|
209
|
+
reasoningEffort: effortMap[level]
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
case "ollama":
|
|
214
|
+
case "mistral":
|
|
215
|
+
case "gemini":
|
|
216
|
+
return {};
|
|
217
|
+
default:
|
|
218
|
+
return {};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/core/workflow-tools.ts
|
|
223
|
+
import { tool } from "ai";
|
|
224
|
+
import { z } from "zod";
|
|
225
|
+
import {
|
|
226
|
+
WorkflowComposer,
|
|
227
|
+
validateExtension
|
|
228
|
+
} from "@contractspec/lib.workflow-composer";
|
|
229
|
+
var StepTypeSchema = z.enum(["human", "automation", "decision"]);
|
|
230
|
+
var StepActionSchema = z.object({
|
|
231
|
+
operation: z.object({
|
|
232
|
+
name: z.string(),
|
|
233
|
+
version: z.number()
|
|
234
|
+
}).optional(),
|
|
235
|
+
form: z.object({
|
|
236
|
+
key: z.string(),
|
|
237
|
+
version: z.number()
|
|
238
|
+
}).optional()
|
|
239
|
+
}).optional();
|
|
240
|
+
var StepSchema = z.object({
|
|
241
|
+
id: z.string(),
|
|
242
|
+
type: StepTypeSchema,
|
|
243
|
+
label: z.string(),
|
|
244
|
+
description: z.string().optional(),
|
|
245
|
+
action: StepActionSchema
|
|
246
|
+
});
|
|
247
|
+
var StepInjectionSchema = z.object({
|
|
248
|
+
after: z.string().optional(),
|
|
249
|
+
before: z.string().optional(),
|
|
250
|
+
inject: StepSchema,
|
|
251
|
+
transitionTo: z.string().optional(),
|
|
252
|
+
transitionFrom: z.string().optional(),
|
|
253
|
+
when: z.string().optional()
|
|
254
|
+
});
|
|
255
|
+
var WorkflowExtensionInputSchema = z.object({
|
|
256
|
+
workflow: z.string(),
|
|
257
|
+
tenantId: z.string().optional(),
|
|
258
|
+
role: z.string().optional(),
|
|
259
|
+
priority: z.number().optional(),
|
|
260
|
+
customSteps: z.array(StepInjectionSchema).optional(),
|
|
261
|
+
hiddenSteps: z.array(z.string()).optional()
|
|
262
|
+
});
|
|
263
|
+
function createWorkflowTools(config) {
|
|
264
|
+
const { baseWorkflows, composer } = config;
|
|
265
|
+
const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
|
|
266
|
+
const createWorkflowExtensionTool = tool({
|
|
267
|
+
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.",
|
|
268
|
+
inputSchema: WorkflowExtensionInputSchema,
|
|
269
|
+
execute: async (input) => {
|
|
270
|
+
const extension = {
|
|
271
|
+
workflow: input.workflow,
|
|
272
|
+
tenantId: input.tenantId,
|
|
273
|
+
role: input.role,
|
|
274
|
+
priority: input.priority,
|
|
275
|
+
customSteps: input.customSteps,
|
|
276
|
+
hiddenSteps: input.hiddenSteps
|
|
277
|
+
};
|
|
278
|
+
const base = baseByKey.get(input.workflow);
|
|
279
|
+
if (!base) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
283
|
+
extension
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
validateExtension(extension, base);
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
message: "Extension validated successfully",
|
|
291
|
+
extension
|
|
292
|
+
};
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
error: err instanceof Error ? err.message : String(err),
|
|
297
|
+
extension
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
const composeWorkflowInputSchema = z.object({
|
|
303
|
+
workflowKey: z.string().describe("Base workflow meta.key"),
|
|
304
|
+
tenantId: z.string().optional(),
|
|
305
|
+
role: z.string().optional(),
|
|
306
|
+
extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
|
|
307
|
+
});
|
|
308
|
+
const composeWorkflowTool = tool({
|
|
309
|
+
description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
|
|
310
|
+
inputSchema: composeWorkflowInputSchema,
|
|
311
|
+
execute: async (input) => {
|
|
312
|
+
const base = baseByKey.get(input.workflowKey);
|
|
313
|
+
if (!base) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const comp = composer ?? new WorkflowComposer;
|
|
320
|
+
if (input.extensions?.length) {
|
|
321
|
+
for (const ext of input.extensions) {
|
|
322
|
+
comp.register({
|
|
323
|
+
workflow: ext.workflow,
|
|
324
|
+
tenantId: ext.tenantId,
|
|
325
|
+
role: ext.role,
|
|
326
|
+
priority: ext.priority,
|
|
327
|
+
customSteps: ext.customSteps,
|
|
328
|
+
hiddenSteps: ext.hiddenSteps
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const composed = comp.compose({
|
|
334
|
+
base,
|
|
335
|
+
tenantId: input.tenantId,
|
|
336
|
+
role: input.role
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
success: true,
|
|
340
|
+
workflow: composed,
|
|
341
|
+
meta: composed.meta,
|
|
342
|
+
stepIds: composed.definition.steps.map((s) => s.id)
|
|
343
|
+
};
|
|
344
|
+
} catch (err) {
|
|
345
|
+
return {
|
|
346
|
+
success: false,
|
|
347
|
+
error: err instanceof Error ? err.message : String(err)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const generateWorkflowSpecCodeInputSchema = z.object({
|
|
353
|
+
workflowKey: z.string().describe("Workflow meta.key"),
|
|
354
|
+
composedSteps: z.array(z.object({
|
|
355
|
+
id: z.string(),
|
|
356
|
+
type: z.enum(["human", "automation", "decision"]),
|
|
357
|
+
label: z.string(),
|
|
358
|
+
description: z.string().optional()
|
|
359
|
+
})).optional().describe("Steps to include; if omitted, uses the base workflow")
|
|
360
|
+
});
|
|
361
|
+
const generateWorkflowSpecCodeTool = tool({
|
|
362
|
+
description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
|
|
363
|
+
inputSchema: generateWorkflowSpecCodeInputSchema,
|
|
364
|
+
execute: async (input) => {
|
|
365
|
+
const base = baseByKey.get(input.workflowKey);
|
|
366
|
+
if (!base) {
|
|
367
|
+
return {
|
|
368
|
+
success: false,
|
|
369
|
+
error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
|
|
370
|
+
code: null
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const steps = input.composedSteps ?? base.definition.steps;
|
|
374
|
+
const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
|
|
375
|
+
const stepsCode = steps.map((s) => ` {
|
|
376
|
+
id: '${s.id}',
|
|
377
|
+
type: '${s.type}',
|
|
378
|
+
label: '${escapeString(s.label)}',${s.description ? `
|
|
379
|
+
description: '${escapeString(s.description)}',` : ""}
|
|
380
|
+
}`).join(`,
|
|
381
|
+
`);
|
|
382
|
+
const meta = base.meta;
|
|
383
|
+
const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
|
|
384
|
+
const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Workflow: ${base.meta.key}
|
|
388
|
+
* Generated via AI chat workflow tools.
|
|
389
|
+
*/
|
|
390
|
+
export const ${specVarName}: WorkflowSpec = {
|
|
391
|
+
meta: {
|
|
392
|
+
key: '${base.meta.key}',
|
|
393
|
+
version: '${String(base.meta.version)}',
|
|
394
|
+
title: '${escapeString(meta.title ?? base.meta.key)}',
|
|
395
|
+
description: '${escapeString(meta.description ?? "")}',
|
|
396
|
+
},
|
|
397
|
+
definition: {
|
|
398
|
+
entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
|
|
399
|
+
steps: [
|
|
400
|
+
${stepsCode}
|
|
401
|
+
],
|
|
402
|
+
transitions: ${transitionsJson},
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
`;
|
|
406
|
+
return {
|
|
407
|
+
success: true,
|
|
408
|
+
code,
|
|
409
|
+
workflowKey: input.workflowKey
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
create_workflow_extension: createWorkflowExtensionTool,
|
|
415
|
+
compose_workflow: composeWorkflowTool,
|
|
416
|
+
generate_workflow_spec_code: generateWorkflowSpecCodeTool
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function toPascalCase(value) {
|
|
420
|
+
return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
421
|
+
}
|
|
422
|
+
function escapeString(value) {
|
|
423
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/core/contracts-context.ts
|
|
427
|
+
function buildContractsContextPrompt(config) {
|
|
428
|
+
const parts = [];
|
|
429
|
+
if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
|
|
430
|
+
return "";
|
|
431
|
+
}
|
|
432
|
+
parts.push(`
|
|
433
|
+
|
|
434
|
+
## Available resources`);
|
|
435
|
+
if (config.agentSpecs?.length) {
|
|
436
|
+
parts.push(`
|
|
437
|
+
### Agent tools`);
|
|
438
|
+
for (const agent of config.agentSpecs) {
|
|
439
|
+
const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
|
|
440
|
+
parts.push(`- **${agent.key}**: tools: ${toolNames}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (config.dataViewSpecs?.length) {
|
|
444
|
+
parts.push(`
|
|
445
|
+
### Data views`);
|
|
446
|
+
for (const dv of config.dataViewSpecs) {
|
|
447
|
+
parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (config.formSpecs?.length) {
|
|
451
|
+
parts.push(`
|
|
452
|
+
### Forms`);
|
|
453
|
+
for (const form of config.formSpecs) {
|
|
454
|
+
parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (config.presentationSpecs?.length) {
|
|
458
|
+
parts.push(`
|
|
459
|
+
### Presentations`);
|
|
460
|
+
for (const pres of config.presentationSpecs) {
|
|
461
|
+
parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (config.operationRefs?.length) {
|
|
465
|
+
parts.push(`
|
|
466
|
+
### Operations`);
|
|
467
|
+
for (const op of config.operationRefs) {
|
|
468
|
+
parts.push(`- **${op.key}@${op.version}**`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
parts.push(`
|
|
472
|
+
Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
|
|
473
|
+
return parts.join(`
|
|
474
|
+
`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/core/agent-tools-adapter.ts
|
|
478
|
+
import { tool as tool2 } from "ai";
|
|
479
|
+
import { z as z2 } from "zod";
|
|
480
|
+
function getInputSchema(_schema) {
|
|
481
|
+
return z2.object({}).passthrough();
|
|
482
|
+
}
|
|
483
|
+
function agentToolConfigsToToolSet(configs, handlers) {
|
|
484
|
+
const result = {};
|
|
485
|
+
for (const config of configs) {
|
|
486
|
+
const handler = handlers?.[config.name];
|
|
487
|
+
const inputSchema = getInputSchema(config.schema);
|
|
488
|
+
result[config.name] = tool2({
|
|
489
|
+
description: config.description ?? config.name,
|
|
490
|
+
inputSchema,
|
|
491
|
+
execute: async (input) => {
|
|
492
|
+
if (!handler) {
|
|
493
|
+
return {
|
|
494
|
+
status: "unimplemented",
|
|
495
|
+
message: "Wire handler in host",
|
|
496
|
+
toolName: config.name
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
const output = await Promise.resolve(handler(input));
|
|
501
|
+
return typeof output === "string" ? output : output;
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return {
|
|
504
|
+
status: "error",
|
|
505
|
+
error: err instanceof Error ? err.message : String(err),
|
|
506
|
+
toolName: config.name
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return result;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/core/surface-planner-tools.ts
|
|
516
|
+
import { tool as tool3 } from "ai";
|
|
517
|
+
import { z as z3 } from "zod";
|
|
518
|
+
import {
|
|
519
|
+
validatePatchProposal
|
|
520
|
+
} from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
|
|
521
|
+
import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
|
|
522
|
+
var VALID_OPS = [
|
|
523
|
+
"insert-node",
|
|
524
|
+
"replace-node",
|
|
525
|
+
"remove-node",
|
|
526
|
+
"move-node",
|
|
527
|
+
"resize-panel",
|
|
528
|
+
"set-layout",
|
|
529
|
+
"reveal-field",
|
|
530
|
+
"hide-field",
|
|
531
|
+
"promote-action",
|
|
532
|
+
"set-focus"
|
|
533
|
+
];
|
|
534
|
+
var DEFAULT_NODE_KINDS = [
|
|
535
|
+
"entity-section",
|
|
536
|
+
"entity-card",
|
|
537
|
+
"data-view",
|
|
538
|
+
"assistant-panel",
|
|
539
|
+
"chat-thread",
|
|
540
|
+
"action-bar",
|
|
541
|
+
"timeline",
|
|
542
|
+
"table",
|
|
543
|
+
"rich-doc",
|
|
544
|
+
"form",
|
|
545
|
+
"chart",
|
|
546
|
+
"custom-widget"
|
|
547
|
+
];
|
|
548
|
+
function collectSlotIdsFromRegion(node) {
|
|
549
|
+
const ids = [];
|
|
550
|
+
if (node.type === "slot") {
|
|
551
|
+
ids.push(node.slotId);
|
|
552
|
+
}
|
|
553
|
+
if (node.type === "panel-group" || node.type === "stack") {
|
|
554
|
+
for (const child of node.children) {
|
|
555
|
+
ids.push(...collectSlotIdsFromRegion(child));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (node.type === "tabs") {
|
|
559
|
+
for (const tab of node.tabs) {
|
|
560
|
+
ids.push(...collectSlotIdsFromRegion(tab.child));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (node.type === "floating") {
|
|
564
|
+
ids.push(node.anchorSlotId);
|
|
565
|
+
ids.push(...collectSlotIdsFromRegion(node.child));
|
|
566
|
+
}
|
|
567
|
+
return ids;
|
|
568
|
+
}
|
|
569
|
+
function deriveConstraints(plan) {
|
|
570
|
+
const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
|
|
571
|
+
const uniqueSlots = [...new Set(slotIds)];
|
|
572
|
+
return {
|
|
573
|
+
allowedOps: VALID_OPS,
|
|
574
|
+
allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
|
|
575
|
+
allowedNodeKinds: DEFAULT_NODE_KINDS
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
var ProposePatchInputSchema = z3.object({
|
|
579
|
+
proposalId: z3.string().describe("Unique proposal identifier"),
|
|
580
|
+
ops: z3.array(z3.object({
|
|
581
|
+
op: z3.enum([
|
|
582
|
+
"insert-node",
|
|
583
|
+
"replace-node",
|
|
584
|
+
"remove-node",
|
|
585
|
+
"move-node",
|
|
586
|
+
"resize-panel",
|
|
587
|
+
"set-layout",
|
|
588
|
+
"reveal-field",
|
|
589
|
+
"hide-field",
|
|
590
|
+
"promote-action",
|
|
591
|
+
"set-focus"
|
|
592
|
+
]),
|
|
593
|
+
slotId: z3.string().optional(),
|
|
594
|
+
nodeId: z3.string().optional(),
|
|
595
|
+
toSlotId: z3.string().optional(),
|
|
596
|
+
index: z3.number().optional(),
|
|
597
|
+
node: z3.object({
|
|
598
|
+
nodeId: z3.string(),
|
|
599
|
+
kind: z3.string(),
|
|
600
|
+
title: z3.string().optional(),
|
|
601
|
+
props: z3.record(z3.string(), z3.unknown()).optional(),
|
|
602
|
+
children: z3.array(z3.unknown()).optional()
|
|
603
|
+
}).optional(),
|
|
604
|
+
persistKey: z3.string().optional(),
|
|
605
|
+
sizes: z3.array(z3.number()).optional(),
|
|
606
|
+
layoutId: z3.string().optional(),
|
|
607
|
+
fieldId: z3.string().optional(),
|
|
608
|
+
actionId: z3.string().optional(),
|
|
609
|
+
placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
|
|
610
|
+
targetId: z3.string().optional()
|
|
611
|
+
}))
|
|
612
|
+
});
|
|
613
|
+
function createSurfacePlannerTools(config) {
|
|
614
|
+
const { plan, constraints, onPatchProposal } = config;
|
|
615
|
+
const resolvedConstraints = constraints ?? deriveConstraints(plan);
|
|
616
|
+
const proposePatchTool = tool3({
|
|
617
|
+
description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
|
|
618
|
+
inputSchema: ProposePatchInputSchema,
|
|
619
|
+
execute: async (input) => {
|
|
620
|
+
const ops = input.ops;
|
|
621
|
+
try {
|
|
622
|
+
validatePatchProposal(ops, resolvedConstraints);
|
|
623
|
+
const proposal = buildSurfacePatchProposal(input.proposalId, ops);
|
|
624
|
+
onPatchProposal?.(proposal);
|
|
625
|
+
return {
|
|
626
|
+
success: true,
|
|
627
|
+
proposalId: proposal.proposalId,
|
|
628
|
+
opsCount: proposal.ops.length,
|
|
629
|
+
message: "Patch proposal validated; awaiting user approval"
|
|
630
|
+
};
|
|
631
|
+
} catch (err) {
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
error: err instanceof Error ? err.message : String(err),
|
|
635
|
+
proposalId: input.proposalId
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
return {
|
|
641
|
+
"propose-patch": proposePatchTool
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
function buildPlannerPromptInput(plan) {
|
|
645
|
+
const constraints = deriveConstraints(plan);
|
|
646
|
+
return {
|
|
647
|
+
bundleMeta: {
|
|
648
|
+
key: plan.bundleKey,
|
|
649
|
+
version: "0.0.0",
|
|
650
|
+
title: plan.bundleKey
|
|
651
|
+
},
|
|
652
|
+
surfaceId: plan.surfaceId,
|
|
653
|
+
allowedPatchOps: constraints.allowedOps,
|
|
654
|
+
allowedSlots: [...constraints.allowedSlots],
|
|
655
|
+
allowedNodeKinds: [...constraints.allowedNodeKinds],
|
|
656
|
+
actions: plan.actions.map((a) => ({
|
|
657
|
+
actionId: a.actionId,
|
|
658
|
+
title: a.title
|
|
659
|
+
})),
|
|
660
|
+
preferences: {
|
|
661
|
+
guidance: "hints",
|
|
662
|
+
density: "standard",
|
|
663
|
+
dataDepth: "detailed",
|
|
664
|
+
control: "standard",
|
|
665
|
+
media: "text",
|
|
666
|
+
pace: "balanced",
|
|
667
|
+
narrative: "top-down"
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/core/chat-service.ts
|
|
673
|
+
import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
|
|
116
674
|
var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
|
|
117
675
|
|
|
118
676
|
Your capabilities:
|
|
@@ -127,6 +685,9 @@ Guidelines:
|
|
|
127
685
|
- Reference relevant ContractSpec concepts and patterns
|
|
128
686
|
- Ask clarifying questions when the user's intent is unclear
|
|
129
687
|
- When suggesting code changes, explain the rationale`;
|
|
688
|
+
var WORKFLOW_TOOLS_PROMPT = `
|
|
689
|
+
|
|
690
|
+
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.`;
|
|
130
691
|
|
|
131
692
|
class ChatService {
|
|
132
693
|
provider;
|
|
@@ -136,19 +697,93 @@ class ChatService {
|
|
|
136
697
|
maxHistoryMessages;
|
|
137
698
|
onUsage;
|
|
138
699
|
tools;
|
|
700
|
+
thinkingLevel;
|
|
139
701
|
sendReasoning;
|
|
140
702
|
sendSources;
|
|
703
|
+
modelSelector;
|
|
141
704
|
constructor(config) {
|
|
142
705
|
this.provider = config.provider;
|
|
143
706
|
this.context = config.context;
|
|
144
707
|
this.store = config.store ?? new InMemoryConversationStore;
|
|
145
|
-
this.systemPrompt = config
|
|
708
|
+
this.systemPrompt = this.buildSystemPrompt(config);
|
|
146
709
|
this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
|
|
147
710
|
this.onUsage = config.onUsage;
|
|
148
|
-
this.tools = config
|
|
149
|
-
this.
|
|
711
|
+
this.tools = this.mergeTools(config);
|
|
712
|
+
this.thinkingLevel = config.thinkingLevel;
|
|
713
|
+
this.modelSelector = config.modelSelector;
|
|
714
|
+
this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
|
|
150
715
|
this.sendSources = config.sendSources ?? false;
|
|
151
716
|
}
|
|
717
|
+
buildSystemPrompt(config) {
|
|
718
|
+
let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
719
|
+
if (config.workflowToolsConfig?.baseWorkflows?.length) {
|
|
720
|
+
base += WORKFLOW_TOOLS_PROMPT;
|
|
721
|
+
}
|
|
722
|
+
const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
|
|
723
|
+
if (contractsPrompt) {
|
|
724
|
+
base += contractsPrompt;
|
|
725
|
+
}
|
|
726
|
+
if (config.surfacePlanConfig?.plan) {
|
|
727
|
+
const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
|
|
728
|
+
base += `
|
|
729
|
+
|
|
730
|
+
` + compilePlannerPrompt(plannerInput);
|
|
731
|
+
}
|
|
732
|
+
return base;
|
|
733
|
+
}
|
|
734
|
+
mergeTools(config) {
|
|
735
|
+
let merged = config.tools ?? {};
|
|
736
|
+
const wfConfig = config.workflowToolsConfig;
|
|
737
|
+
if (wfConfig?.baseWorkflows?.length) {
|
|
738
|
+
const workflowTools = createWorkflowTools({
|
|
739
|
+
baseWorkflows: wfConfig.baseWorkflows,
|
|
740
|
+
composer: wfConfig.composer
|
|
741
|
+
});
|
|
742
|
+
merged = { ...merged, ...workflowTools };
|
|
743
|
+
}
|
|
744
|
+
const contractsCtx = config.contractsContext;
|
|
745
|
+
if (contractsCtx?.agentSpecs?.length) {
|
|
746
|
+
const allTools = [];
|
|
747
|
+
for (const agent of contractsCtx.agentSpecs) {
|
|
748
|
+
if (agent.tools?.length)
|
|
749
|
+
allTools.push(...agent.tools);
|
|
750
|
+
}
|
|
751
|
+
if (allTools.length > 0) {
|
|
752
|
+
const agentTools = agentToolConfigsToToolSet(allTools);
|
|
753
|
+
merged = { ...merged, ...agentTools };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const surfaceConfig = config.surfacePlanConfig;
|
|
757
|
+
if (surfaceConfig?.plan) {
|
|
758
|
+
const plannerTools = createSurfacePlannerTools({
|
|
759
|
+
plan: surfaceConfig.plan,
|
|
760
|
+
onPatchProposal: surfaceConfig.onPatchProposal
|
|
761
|
+
});
|
|
762
|
+
merged = { ...merged, ...plannerTools };
|
|
763
|
+
}
|
|
764
|
+
if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
|
|
765
|
+
merged = { ...merged, ...config.mcpTools };
|
|
766
|
+
}
|
|
767
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
768
|
+
}
|
|
769
|
+
async resolveModel() {
|
|
770
|
+
if (this.modelSelector) {
|
|
771
|
+
const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
|
|
772
|
+
const { model, selection } = await this.modelSelector.selectAndCreate({
|
|
773
|
+
taskDimension: dimension
|
|
774
|
+
});
|
|
775
|
+
return { model, providerName: selection.providerKey };
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
model: this.provider.getModel(),
|
|
779
|
+
providerName: this.provider.name
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
thinkingLevelToDimension(level) {
|
|
783
|
+
if (!level || level === "instant")
|
|
784
|
+
return "latency";
|
|
785
|
+
return "reasoning";
|
|
786
|
+
}
|
|
152
787
|
async send(options) {
|
|
153
788
|
let conversation;
|
|
154
789
|
if (options.conversationId) {
|
|
@@ -166,20 +801,25 @@ class ChatService {
|
|
|
166
801
|
workspacePath: this.context?.workspacePath
|
|
167
802
|
});
|
|
168
803
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
804
|
+
if (!options.skipUserAppend) {
|
|
805
|
+
await this.store.appendMessage(conversation.id, {
|
|
806
|
+
role: "user",
|
|
807
|
+
content: options.content,
|
|
808
|
+
status: "completed",
|
|
809
|
+
attachments: options.attachments
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
175
813
|
const messages = this.buildMessages(conversation, options);
|
|
176
|
-
const model = this.
|
|
814
|
+
const { model, providerName } = await this.resolveModel();
|
|
815
|
+
const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
177
816
|
try {
|
|
178
817
|
const result = await generateText({
|
|
179
818
|
model,
|
|
180
819
|
messages,
|
|
181
820
|
system: this.systemPrompt,
|
|
182
|
-
tools: this.tools
|
|
821
|
+
tools: this.tools,
|
|
822
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
183
823
|
});
|
|
184
824
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
185
825
|
role: "assistant",
|
|
@@ -224,23 +864,27 @@ class ChatService {
|
|
|
224
864
|
workspacePath: this.context?.workspacePath
|
|
225
865
|
});
|
|
226
866
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
867
|
+
if (!options.skipUserAppend) {
|
|
868
|
+
await this.store.appendMessage(conversation.id, {
|
|
869
|
+
role: "user",
|
|
870
|
+
content: options.content,
|
|
871
|
+
status: "completed",
|
|
872
|
+
attachments: options.attachments
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
conversation = await this.store.get(conversation.id) ?? conversation;
|
|
233
876
|
const assistantMessage = await this.store.appendMessage(conversation.id, {
|
|
234
877
|
role: "assistant",
|
|
235
878
|
content: "",
|
|
236
879
|
status: "streaming"
|
|
237
880
|
});
|
|
238
881
|
const messages = this.buildMessages(conversation, options);
|
|
239
|
-
const model = this.
|
|
882
|
+
const { model, providerName } = await this.resolveModel();
|
|
240
883
|
const systemPrompt = this.systemPrompt;
|
|
241
884
|
const tools = this.tools;
|
|
242
885
|
const store = this.store;
|
|
243
886
|
const onUsage = this.onUsage;
|
|
887
|
+
const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
|
|
244
888
|
async function* streamGenerator() {
|
|
245
889
|
let fullContent = "";
|
|
246
890
|
let fullReasoning = "";
|
|
@@ -251,7 +895,8 @@ class ChatService {
|
|
|
251
895
|
model,
|
|
252
896
|
messages,
|
|
253
897
|
system: systemPrompt,
|
|
254
|
-
tools
|
|
898
|
+
tools,
|
|
899
|
+
providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
|
|
255
900
|
});
|
|
256
901
|
for await (const part of result.fullStream) {
|
|
257
902
|
if (part.type === "text-delta") {
|
|
@@ -366,6 +1011,18 @@ class ChatService {
|
|
|
366
1011
|
...options
|
|
367
1012
|
});
|
|
368
1013
|
}
|
|
1014
|
+
async updateConversation(conversationId, updates) {
|
|
1015
|
+
return this.store.update(conversationId, updates);
|
|
1016
|
+
}
|
|
1017
|
+
async forkConversation(conversationId, upToMessageId) {
|
|
1018
|
+
return this.store.fork(conversationId, upToMessageId);
|
|
1019
|
+
}
|
|
1020
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
1021
|
+
return this.store.updateMessage(conversationId, messageId, updates);
|
|
1022
|
+
}
|
|
1023
|
+
async truncateAfter(conversationId, messageId) {
|
|
1024
|
+
return this.store.truncateAfter(conversationId, messageId);
|
|
1025
|
+
}
|
|
369
1026
|
async deleteConversation(conversationId) {
|
|
370
1027
|
return this.store.delete(conversationId);
|
|
371
1028
|
}
|
|
@@ -434,7 +1091,12 @@ import {
|
|
|
434
1091
|
} from "ai";
|
|
435
1092
|
var DEFAULT_SYSTEM_PROMPT2 = `You are a helpful AI assistant.`;
|
|
436
1093
|
function createChatRoute(options) {
|
|
437
|
-
const {
|
|
1094
|
+
const {
|
|
1095
|
+
provider,
|
|
1096
|
+
systemPrompt = DEFAULT_SYSTEM_PROMPT2,
|
|
1097
|
+
tools,
|
|
1098
|
+
thinkingLevel: defaultThinkingLevel
|
|
1099
|
+
} = options;
|
|
438
1100
|
return async (req) => {
|
|
439
1101
|
if (req.method !== "POST") {
|
|
440
1102
|
return new Response("Method not allowed", { status: 405 });
|
|
@@ -449,12 +1111,15 @@ function createChatRoute(options) {
|
|
|
449
1111
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
450
1112
|
return new Response("messages array required", { status: 400 });
|
|
451
1113
|
}
|
|
1114
|
+
const thinkingLevel = body.thinkingLevel ?? defaultThinkingLevel;
|
|
1115
|
+
const providerOptions = getProviderOptions(thinkingLevel, provider.name);
|
|
452
1116
|
const model = provider.getModel();
|
|
453
1117
|
const result = streamText2({
|
|
454
1118
|
model,
|
|
455
1119
|
messages: await convertToModelMessages(messages),
|
|
456
1120
|
system: systemPrompt,
|
|
457
|
-
tools
|
|
1121
|
+
tools,
|
|
1122
|
+
providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
|
|
458
1123
|
});
|
|
459
1124
|
return result.toUIMessageStreamResponse();
|
|
460
1125
|
};
|
|
@@ -486,11 +1151,468 @@ function createCompletionRoute(options) {
|
|
|
486
1151
|
return result.toTextStreamResponse();
|
|
487
1152
|
};
|
|
488
1153
|
}
|
|
1154
|
+
// src/core/export-formatters.ts
|
|
1155
|
+
function formatTimestamp(date) {
|
|
1156
|
+
return date.toLocaleTimeString([], {
|
|
1157
|
+
hour: "2-digit",
|
|
1158
|
+
minute: "2-digit"
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
function toIsoString(date) {
|
|
1162
|
+
return date.toISOString();
|
|
1163
|
+
}
|
|
1164
|
+
function messageToJsonSerializable(msg) {
|
|
1165
|
+
return {
|
|
1166
|
+
id: msg.id,
|
|
1167
|
+
conversationId: msg.conversationId,
|
|
1168
|
+
role: msg.role,
|
|
1169
|
+
content: msg.content,
|
|
1170
|
+
status: msg.status,
|
|
1171
|
+
createdAt: toIsoString(msg.createdAt),
|
|
1172
|
+
updatedAt: toIsoString(msg.updatedAt),
|
|
1173
|
+
...msg.attachments && { attachments: msg.attachments },
|
|
1174
|
+
...msg.codeBlocks && { codeBlocks: msg.codeBlocks },
|
|
1175
|
+
...msg.toolCalls && { toolCalls: msg.toolCalls },
|
|
1176
|
+
...msg.sources && { sources: msg.sources },
|
|
1177
|
+
...msg.reasoning && { reasoning: msg.reasoning },
|
|
1178
|
+
...msg.usage && { usage: msg.usage },
|
|
1179
|
+
...msg.error && { error: msg.error },
|
|
1180
|
+
...msg.metadata && { metadata: msg.metadata }
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
function formatSourcesMarkdown(sources) {
|
|
1184
|
+
if (sources.length === 0)
|
|
1185
|
+
return "";
|
|
1186
|
+
return `
|
|
1187
|
+
|
|
1188
|
+
**Sources:**
|
|
1189
|
+
` + sources.map((s) => `- [${s.title}](${s.url ?? "#"})`).join(`
|
|
1190
|
+
`);
|
|
1191
|
+
}
|
|
1192
|
+
function formatSourcesTxt(sources) {
|
|
1193
|
+
if (sources.length === 0)
|
|
1194
|
+
return "";
|
|
1195
|
+
return `
|
|
1196
|
+
|
|
1197
|
+
Sources:
|
|
1198
|
+
` + sources.map((s) => `- ${s.title}${s.url ? ` - ${s.url}` : ""}`).join(`
|
|
1199
|
+
`);
|
|
1200
|
+
}
|
|
1201
|
+
function formatToolCallsMarkdown(toolCalls) {
|
|
1202
|
+
if (toolCalls.length === 0)
|
|
1203
|
+
return "";
|
|
1204
|
+
return `
|
|
1205
|
+
|
|
1206
|
+
**Tool calls:**
|
|
1207
|
+
` + toolCalls.map((tc) => `**${tc.name}** (${tc.status})
|
|
1208
|
+
\`\`\`json
|
|
1209
|
+
${JSON.stringify(tc.args, null, 2)}
|
|
1210
|
+
\`\`\`` + (tc.result !== undefined ? `
|
|
1211
|
+
Output:
|
|
1212
|
+
\`\`\`json
|
|
1213
|
+
${typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)}
|
|
1214
|
+
\`\`\`` : "") + (tc.error ? `
|
|
1215
|
+
Error: ${tc.error}` : "")).join(`
|
|
1216
|
+
|
|
1217
|
+
`);
|
|
1218
|
+
}
|
|
1219
|
+
function formatToolCallsTxt(toolCalls) {
|
|
1220
|
+
if (toolCalls.length === 0)
|
|
1221
|
+
return "";
|
|
1222
|
+
return `
|
|
1223
|
+
|
|
1224
|
+
Tool calls:
|
|
1225
|
+
` + toolCalls.map((tc) => `- ${tc.name} (${tc.status}): ${JSON.stringify(tc.args)}` + (tc.result !== undefined ? ` -> ${typeof tc.result === "object" ? JSON.stringify(tc.result) : String(tc.result)}` : "") + (tc.error ? ` [Error: ${tc.error}]` : "")).join(`
|
|
1226
|
+
`);
|
|
1227
|
+
}
|
|
1228
|
+
function formatUsage(usage) {
|
|
1229
|
+
const total = usage.inputTokens + usage.outputTokens;
|
|
1230
|
+
return ` (${total} tokens)`;
|
|
1231
|
+
}
|
|
1232
|
+
function formatMessagesAsMarkdown(messages) {
|
|
1233
|
+
const parts = [];
|
|
1234
|
+
for (const msg of messages) {
|
|
1235
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
|
|
1236
|
+
const header = `## ${roleLabel}`;
|
|
1237
|
+
const timestamp = `*${formatTimestamp(msg.createdAt)}*`;
|
|
1238
|
+
const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
|
|
1239
|
+
const meta = `${timestamp}${usageSuffix}
|
|
1240
|
+
|
|
1241
|
+
`;
|
|
1242
|
+
let body = msg.content;
|
|
1243
|
+
if (msg.error) {
|
|
1244
|
+
body += `
|
|
1245
|
+
|
|
1246
|
+
**Error:** ${msg.error.code} - ${msg.error.message}`;
|
|
1247
|
+
}
|
|
1248
|
+
if (msg.reasoning) {
|
|
1249
|
+
body += `
|
|
1250
|
+
|
|
1251
|
+
> **Reasoning:**
|
|
1252
|
+
> ${msg.reasoning.replace(/\n/g, `
|
|
1253
|
+
> `)}`;
|
|
1254
|
+
}
|
|
1255
|
+
body += formatSourcesMarkdown(msg.sources ?? []);
|
|
1256
|
+
body += formatToolCallsMarkdown(msg.toolCalls ?? []);
|
|
1257
|
+
parts.push(`${header}
|
|
1258
|
+
|
|
1259
|
+
${meta}${body}`);
|
|
1260
|
+
}
|
|
1261
|
+
return parts.join(`
|
|
1262
|
+
|
|
1263
|
+
---
|
|
1264
|
+
|
|
1265
|
+
`);
|
|
1266
|
+
}
|
|
1267
|
+
function formatMessagesAsTxt(messages) {
|
|
1268
|
+
const parts = [];
|
|
1269
|
+
for (const msg of messages) {
|
|
1270
|
+
const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
|
|
1271
|
+
const timestamp = `(${formatTimestamp(msg.createdAt)})`;
|
|
1272
|
+
const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
|
|
1273
|
+
const header = `[${roleLabel}] ${timestamp}${usageSuffix}
|
|
1274
|
+
|
|
1275
|
+
`;
|
|
1276
|
+
let body = msg.content;
|
|
1277
|
+
if (msg.error) {
|
|
1278
|
+
body += `
|
|
1279
|
+
|
|
1280
|
+
Error: ${msg.error.code} - ${msg.error.message}`;
|
|
1281
|
+
}
|
|
1282
|
+
if (msg.reasoning) {
|
|
1283
|
+
body += `
|
|
1284
|
+
|
|
1285
|
+
Reasoning: ${msg.reasoning}`;
|
|
1286
|
+
}
|
|
1287
|
+
body += formatSourcesTxt(msg.sources ?? []);
|
|
1288
|
+
body += formatToolCallsTxt(msg.toolCalls ?? []);
|
|
1289
|
+
parts.push(`${header}${body}`);
|
|
1290
|
+
}
|
|
1291
|
+
return parts.join(`
|
|
1292
|
+
|
|
1293
|
+
---
|
|
1294
|
+
|
|
1295
|
+
`);
|
|
1296
|
+
}
|
|
1297
|
+
function formatMessagesAsJson(messages, conversation) {
|
|
1298
|
+
const payload = {
|
|
1299
|
+
messages: messages.map(messageToJsonSerializable)
|
|
1300
|
+
};
|
|
1301
|
+
if (conversation) {
|
|
1302
|
+
payload.conversation = {
|
|
1303
|
+
id: conversation.id,
|
|
1304
|
+
title: conversation.title,
|
|
1305
|
+
status: conversation.status,
|
|
1306
|
+
createdAt: toIsoString(conversation.createdAt),
|
|
1307
|
+
updatedAt: toIsoString(conversation.updatedAt),
|
|
1308
|
+
provider: conversation.provider,
|
|
1309
|
+
model: conversation.model,
|
|
1310
|
+
workspacePath: conversation.workspacePath,
|
|
1311
|
+
contextFiles: conversation.contextFiles,
|
|
1312
|
+
summary: conversation.summary,
|
|
1313
|
+
metadata: conversation.metadata
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
return JSON.stringify(payload, null, 2);
|
|
1317
|
+
}
|
|
1318
|
+
function getExportFilename(format, conversation) {
|
|
1319
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1320
|
+
const base = conversation?.title ? conversation.title.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 40) : "chat-export";
|
|
1321
|
+
const ext = format === "markdown" ? "md" : format === "txt" ? "txt" : "json";
|
|
1322
|
+
return `${base}-${timestamp}.${ext}`;
|
|
1323
|
+
}
|
|
1324
|
+
var MIME_TYPES = {
|
|
1325
|
+
markdown: "text/markdown",
|
|
1326
|
+
txt: "text/plain",
|
|
1327
|
+
json: "application/json"
|
|
1328
|
+
};
|
|
1329
|
+
function downloadAsFile(content, filename, mimeType) {
|
|
1330
|
+
const blob = new Blob([content], { type: mimeType });
|
|
1331
|
+
const url = URL.createObjectURL(blob);
|
|
1332
|
+
const a = document.createElement("a");
|
|
1333
|
+
a.href = url;
|
|
1334
|
+
a.download = filename;
|
|
1335
|
+
document.body.appendChild(a);
|
|
1336
|
+
a.click();
|
|
1337
|
+
document.body.removeChild(a);
|
|
1338
|
+
URL.revokeObjectURL(url);
|
|
1339
|
+
}
|
|
1340
|
+
function exportToFile(messages, format, conversation) {
|
|
1341
|
+
let content;
|
|
1342
|
+
if (format === "markdown") {
|
|
1343
|
+
content = formatMessagesAsMarkdown(messages);
|
|
1344
|
+
} else if (format === "txt") {
|
|
1345
|
+
content = formatMessagesAsTxt(messages);
|
|
1346
|
+
} else {
|
|
1347
|
+
content = formatMessagesAsJson(messages, conversation);
|
|
1348
|
+
}
|
|
1349
|
+
const filename = getExportFilename(format, conversation);
|
|
1350
|
+
const mimeType = MIME_TYPES[format];
|
|
1351
|
+
downloadAsFile(content, filename, mimeType);
|
|
1352
|
+
}
|
|
1353
|
+
// src/core/local-storage-conversation-store.ts
|
|
1354
|
+
var DEFAULT_KEY = "contractspec:ai-chat:conversations";
|
|
1355
|
+
function generateId2(prefix) {
|
|
1356
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
1357
|
+
}
|
|
1358
|
+
function toSerializable(conv) {
|
|
1359
|
+
return {
|
|
1360
|
+
...conv,
|
|
1361
|
+
createdAt: conv.createdAt.toISOString(),
|
|
1362
|
+
updatedAt: conv.updatedAt.toISOString(),
|
|
1363
|
+
messages: conv.messages.map((m) => ({
|
|
1364
|
+
...m,
|
|
1365
|
+
createdAt: m.createdAt.toISOString(),
|
|
1366
|
+
updatedAt: m.updatedAt.toISOString()
|
|
1367
|
+
}))
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function fromSerializable(raw) {
|
|
1371
|
+
const messages = raw.messages?.map((m) => ({
|
|
1372
|
+
...m,
|
|
1373
|
+
createdAt: new Date(m.createdAt),
|
|
1374
|
+
updatedAt: new Date(m.updatedAt)
|
|
1375
|
+
})) ?? [];
|
|
1376
|
+
return {
|
|
1377
|
+
...raw,
|
|
1378
|
+
createdAt: new Date(raw.createdAt),
|
|
1379
|
+
updatedAt: new Date(raw.updatedAt),
|
|
1380
|
+
messages
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function loadAll(key) {
|
|
1384
|
+
if (typeof window === "undefined")
|
|
1385
|
+
return new Map;
|
|
1386
|
+
try {
|
|
1387
|
+
const raw = window.localStorage.getItem(key);
|
|
1388
|
+
if (!raw)
|
|
1389
|
+
return new Map;
|
|
1390
|
+
const arr = JSON.parse(raw);
|
|
1391
|
+
const map = new Map;
|
|
1392
|
+
for (const item of arr) {
|
|
1393
|
+
const conv = fromSerializable(item);
|
|
1394
|
+
map.set(conv.id, conv);
|
|
1395
|
+
}
|
|
1396
|
+
return map;
|
|
1397
|
+
} catch {
|
|
1398
|
+
return new Map;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function saveAll(key, map) {
|
|
1402
|
+
if (typeof window === "undefined")
|
|
1403
|
+
return;
|
|
1404
|
+
try {
|
|
1405
|
+
const arr = Array.from(map.values()).map(toSerializable);
|
|
1406
|
+
window.localStorage.setItem(key, JSON.stringify(arr));
|
|
1407
|
+
} catch {}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
class LocalStorageConversationStore {
|
|
1411
|
+
key;
|
|
1412
|
+
cache = null;
|
|
1413
|
+
constructor(storageKey = DEFAULT_KEY) {
|
|
1414
|
+
this.key = storageKey;
|
|
1415
|
+
}
|
|
1416
|
+
getMap() {
|
|
1417
|
+
if (!this.cache) {
|
|
1418
|
+
this.cache = loadAll(this.key);
|
|
1419
|
+
}
|
|
1420
|
+
return this.cache;
|
|
1421
|
+
}
|
|
1422
|
+
persist() {
|
|
1423
|
+
saveAll(this.key, this.getMap());
|
|
1424
|
+
}
|
|
1425
|
+
async get(conversationId) {
|
|
1426
|
+
return this.getMap().get(conversationId) ?? null;
|
|
1427
|
+
}
|
|
1428
|
+
async create(conversation) {
|
|
1429
|
+
const now = new Date;
|
|
1430
|
+
const full = {
|
|
1431
|
+
...conversation,
|
|
1432
|
+
id: generateId2("conv"),
|
|
1433
|
+
createdAt: now,
|
|
1434
|
+
updatedAt: now
|
|
1435
|
+
};
|
|
1436
|
+
this.getMap().set(full.id, full);
|
|
1437
|
+
this.persist();
|
|
1438
|
+
return full;
|
|
1439
|
+
}
|
|
1440
|
+
async update(conversationId, updates) {
|
|
1441
|
+
const conv = this.getMap().get(conversationId);
|
|
1442
|
+
if (!conv)
|
|
1443
|
+
return null;
|
|
1444
|
+
const updated = {
|
|
1445
|
+
...conv,
|
|
1446
|
+
...updates,
|
|
1447
|
+
updatedAt: new Date
|
|
1448
|
+
};
|
|
1449
|
+
this.getMap().set(conversationId, updated);
|
|
1450
|
+
this.persist();
|
|
1451
|
+
return updated;
|
|
1452
|
+
}
|
|
1453
|
+
async appendMessage(conversationId, message) {
|
|
1454
|
+
const conv = this.getMap().get(conversationId);
|
|
1455
|
+
if (!conv)
|
|
1456
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
1457
|
+
const now = new Date;
|
|
1458
|
+
const fullMessage = {
|
|
1459
|
+
...message,
|
|
1460
|
+
id: generateId2("msg"),
|
|
1461
|
+
conversationId,
|
|
1462
|
+
createdAt: now,
|
|
1463
|
+
updatedAt: now
|
|
1464
|
+
};
|
|
1465
|
+
conv.messages.push(fullMessage);
|
|
1466
|
+
conv.updatedAt = now;
|
|
1467
|
+
this.persist();
|
|
1468
|
+
return fullMessage;
|
|
1469
|
+
}
|
|
1470
|
+
async updateMessage(conversationId, messageId, updates) {
|
|
1471
|
+
const conv = this.getMap().get(conversationId);
|
|
1472
|
+
if (!conv)
|
|
1473
|
+
return null;
|
|
1474
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
1475
|
+
if (idx === -1)
|
|
1476
|
+
return null;
|
|
1477
|
+
const msg = conv.messages[idx];
|
|
1478
|
+
if (!msg)
|
|
1479
|
+
return null;
|
|
1480
|
+
const updated = {
|
|
1481
|
+
...msg,
|
|
1482
|
+
...updates,
|
|
1483
|
+
updatedAt: new Date
|
|
1484
|
+
};
|
|
1485
|
+
conv.messages[idx] = updated;
|
|
1486
|
+
conv.updatedAt = new Date;
|
|
1487
|
+
this.persist();
|
|
1488
|
+
return updated;
|
|
1489
|
+
}
|
|
1490
|
+
async delete(conversationId) {
|
|
1491
|
+
const deleted = this.getMap().delete(conversationId);
|
|
1492
|
+
if (deleted)
|
|
1493
|
+
this.persist();
|
|
1494
|
+
return deleted;
|
|
1495
|
+
}
|
|
1496
|
+
async list(options) {
|
|
1497
|
+
let results = Array.from(this.getMap().values());
|
|
1498
|
+
if (options?.status) {
|
|
1499
|
+
results = results.filter((c) => c.status === options.status);
|
|
1500
|
+
}
|
|
1501
|
+
if (options?.projectId) {
|
|
1502
|
+
results = results.filter((c) => c.projectId === options.projectId);
|
|
1503
|
+
}
|
|
1504
|
+
if (options?.tags && options.tags.length > 0) {
|
|
1505
|
+
const tagSet = new Set(options.tags);
|
|
1506
|
+
results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
|
|
1507
|
+
}
|
|
1508
|
+
results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
1509
|
+
const offset = options?.offset ?? 0;
|
|
1510
|
+
const limit = options?.limit ?? 100;
|
|
1511
|
+
return results.slice(offset, offset + limit);
|
|
1512
|
+
}
|
|
1513
|
+
async fork(conversationId, upToMessageId) {
|
|
1514
|
+
const source = this.getMap().get(conversationId);
|
|
1515
|
+
if (!source)
|
|
1516
|
+
throw new Error(`Conversation ${conversationId} not found`);
|
|
1517
|
+
let messagesToCopy = source.messages;
|
|
1518
|
+
if (upToMessageId) {
|
|
1519
|
+
const idx = source.messages.findIndex((m) => m.id === upToMessageId);
|
|
1520
|
+
if (idx === -1)
|
|
1521
|
+
throw new Error(`Message ${upToMessageId} not found`);
|
|
1522
|
+
messagesToCopy = source.messages.slice(0, idx + 1);
|
|
1523
|
+
}
|
|
1524
|
+
const now = new Date;
|
|
1525
|
+
const forkedMessages = messagesToCopy.map((m) => ({
|
|
1526
|
+
...m,
|
|
1527
|
+
id: generateId2("msg"),
|
|
1528
|
+
conversationId: "",
|
|
1529
|
+
createdAt: new Date(m.createdAt),
|
|
1530
|
+
updatedAt: new Date(m.updatedAt)
|
|
1531
|
+
}));
|
|
1532
|
+
const forked = {
|
|
1533
|
+
...source,
|
|
1534
|
+
id: generateId2("conv"),
|
|
1535
|
+
title: source.title ? `${source.title} (fork)` : undefined,
|
|
1536
|
+
forkedFromId: source.id,
|
|
1537
|
+
createdAt: now,
|
|
1538
|
+
updatedAt: now,
|
|
1539
|
+
messages: forkedMessages
|
|
1540
|
+
};
|
|
1541
|
+
for (const m of forked.messages) {
|
|
1542
|
+
m.conversationId = forked.id;
|
|
1543
|
+
}
|
|
1544
|
+
this.getMap().set(forked.id, forked);
|
|
1545
|
+
this.persist();
|
|
1546
|
+
return forked;
|
|
1547
|
+
}
|
|
1548
|
+
async truncateAfter(conversationId, messageId) {
|
|
1549
|
+
const conv = this.getMap().get(conversationId);
|
|
1550
|
+
if (!conv)
|
|
1551
|
+
return null;
|
|
1552
|
+
const idx = conv.messages.findIndex((m) => m.id === messageId);
|
|
1553
|
+
if (idx === -1)
|
|
1554
|
+
return null;
|
|
1555
|
+
conv.messages = conv.messages.slice(0, idx + 1);
|
|
1556
|
+
conv.updatedAt = new Date;
|
|
1557
|
+
this.persist();
|
|
1558
|
+
return conv;
|
|
1559
|
+
}
|
|
1560
|
+
async search(query, limit = 20) {
|
|
1561
|
+
const lowerQuery = query.toLowerCase();
|
|
1562
|
+
const results = [];
|
|
1563
|
+
for (const conv of this.getMap().values()) {
|
|
1564
|
+
if (conv.title?.toLowerCase().includes(lowerQuery)) {
|
|
1565
|
+
results.push(conv);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
|
|
1569
|
+
results.push(conv);
|
|
1570
|
+
}
|
|
1571
|
+
if (results.length >= limit)
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
return results;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
function createLocalStorageConversationStore(storageKey) {
|
|
1578
|
+
return new LocalStorageConversationStore(storageKey);
|
|
1579
|
+
}
|
|
1580
|
+
// src/core/agent-adapter.ts
|
|
1581
|
+
function createChatAgentAdapter(agent) {
|
|
1582
|
+
return {
|
|
1583
|
+
async generate({ prompt, signal }) {
|
|
1584
|
+
const result = await agent.generate({ prompt, signal });
|
|
1585
|
+
return {
|
|
1586
|
+
text: result.text,
|
|
1587
|
+
toolCalls: result.toolCalls,
|
|
1588
|
+
toolResults: result.toolResults,
|
|
1589
|
+
usage: result.usage
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
489
1594
|
export {
|
|
1595
|
+
getProviderOptions,
|
|
1596
|
+
getExportFilename,
|
|
1597
|
+
formatMessagesAsTxt,
|
|
1598
|
+
formatMessagesAsMarkdown,
|
|
1599
|
+
formatMessagesAsJson,
|
|
1600
|
+
exportToFile,
|
|
1601
|
+
downloadAsFile,
|
|
1602
|
+
createWorkflowTools,
|
|
1603
|
+
createSurfacePlannerTools,
|
|
1604
|
+
createLocalStorageConversationStore,
|
|
490
1605
|
createInMemoryConversationStore,
|
|
491
1606
|
createCompletionRoute,
|
|
492
1607
|
createChatService,
|
|
493
1608
|
createChatRoute,
|
|
1609
|
+
createChatAgentAdapter,
|
|
1610
|
+
buildPlannerPromptInput,
|
|
1611
|
+
buildContractsContextPrompt,
|
|
1612
|
+
agentToolConfigsToToolSet,
|
|
1613
|
+
THINKING_LEVEL_LABELS,
|
|
1614
|
+
THINKING_LEVEL_DESCRIPTIONS,
|
|
1615
|
+
LocalStorageConversationStore,
|
|
494
1616
|
InMemoryConversationStore,
|
|
495
1617
|
ChatService
|
|
496
1618
|
};
|