@contractspec/module.ai-chat 4.0.3 → 4.1.0

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