@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
@@ -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,505 @@ 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 { validatePatchProposal } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
519
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
520
+ var VALID_OPS = [
521
+ "insert-node",
522
+ "replace-node",
523
+ "remove-node",
524
+ "move-node",
525
+ "resize-panel",
526
+ "set-layout",
527
+ "reveal-field",
528
+ "hide-field",
529
+ "promote-action",
530
+ "set-focus"
531
+ ];
532
+ var DEFAULT_NODE_KINDS = [
533
+ "entity-section",
534
+ "entity-card",
535
+ "data-view",
536
+ "assistant-panel",
537
+ "chat-thread",
538
+ "action-bar",
539
+ "timeline",
540
+ "table",
541
+ "rich-doc",
542
+ "form",
543
+ "chart",
544
+ "custom-widget"
545
+ ];
546
+ function collectSlotIdsFromRegion(node) {
547
+ const ids = [];
548
+ if (node.type === "slot") {
549
+ ids.push(node.slotId);
550
+ }
551
+ if (node.type === "panel-group" || node.type === "stack") {
552
+ for (const child of node.children) {
553
+ ids.push(...collectSlotIdsFromRegion(child));
554
+ }
555
+ }
556
+ if (node.type === "tabs") {
557
+ for (const tab of node.tabs) {
558
+ ids.push(...collectSlotIdsFromRegion(tab.child));
559
+ }
560
+ }
561
+ if (node.type === "floating") {
562
+ ids.push(node.anchorSlotId);
563
+ ids.push(...collectSlotIdsFromRegion(node.child));
564
+ }
565
+ return ids;
566
+ }
567
+ function deriveConstraints(plan) {
568
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
569
+ const uniqueSlots = [...new Set(slotIds)];
570
+ return {
571
+ allowedOps: VALID_OPS,
572
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
573
+ allowedNodeKinds: DEFAULT_NODE_KINDS
574
+ };
575
+ }
576
+ var ProposePatchInputSchema = z3.object({
577
+ proposalId: z3.string().describe("Unique proposal identifier"),
578
+ ops: z3.array(z3.object({
579
+ op: z3.enum([
580
+ "insert-node",
581
+ "replace-node",
582
+ "remove-node",
583
+ "move-node",
584
+ "resize-panel",
585
+ "set-layout",
586
+ "reveal-field",
587
+ "hide-field",
588
+ "promote-action",
589
+ "set-focus"
590
+ ]),
591
+ slotId: z3.string().optional(),
592
+ nodeId: z3.string().optional(),
593
+ toSlotId: z3.string().optional(),
594
+ index: z3.number().optional(),
595
+ node: z3.object({
596
+ nodeId: z3.string(),
597
+ kind: z3.string(),
598
+ title: z3.string().optional(),
599
+ props: z3.record(z3.string(), z3.unknown()).optional(),
600
+ children: z3.array(z3.unknown()).optional()
601
+ }).optional(),
602
+ persistKey: z3.string().optional(),
603
+ sizes: z3.array(z3.number()).optional(),
604
+ layoutId: z3.string().optional(),
605
+ fieldId: z3.string().optional(),
606
+ actionId: z3.string().optional(),
607
+ placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
608
+ targetId: z3.string().optional()
609
+ }))
610
+ });
611
+ function createSurfacePlannerTools(config) {
612
+ const { plan, constraints, onPatchProposal } = config;
613
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
614
+ const proposePatchTool = tool3({
615
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
616
+ inputSchema: ProposePatchInputSchema,
617
+ execute: async (input) => {
618
+ const ops = input.ops;
619
+ try {
620
+ validatePatchProposal(ops, resolvedConstraints);
621
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
622
+ onPatchProposal?.(proposal);
623
+ return {
624
+ success: true,
625
+ proposalId: proposal.proposalId,
626
+ opsCount: proposal.ops.length,
627
+ message: "Patch proposal validated; awaiting user approval"
628
+ };
629
+ } catch (err) {
630
+ return {
631
+ success: false,
632
+ error: err instanceof Error ? err.message : String(err),
633
+ proposalId: input.proposalId
634
+ };
635
+ }
636
+ }
637
+ });
638
+ return {
639
+ "propose-patch": proposePatchTool
640
+ };
641
+ }
642
+ function buildPlannerPromptInput(plan) {
643
+ const constraints = deriveConstraints(plan);
644
+ return {
645
+ bundleMeta: {
646
+ key: plan.bundleKey,
647
+ version: "0.0.0",
648
+ title: plan.bundleKey
649
+ },
650
+ surfaceId: plan.surfaceId,
651
+ allowedPatchOps: constraints.allowedOps,
652
+ allowedSlots: [...constraints.allowedSlots],
653
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
654
+ actions: plan.actions.map((a) => ({ actionId: a.actionId, title: a.title })),
655
+ preferences: {
656
+ guidance: "hints",
657
+ density: "standard",
658
+ dataDepth: "detailed",
659
+ control: "standard",
660
+ media: "text",
661
+ pace: "balanced",
662
+ narrative: "top-down"
663
+ }
664
+ };
665
+ }
666
+
667
+ // src/core/chat-service.ts
668
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
116
669
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
117
670
 
118
671
  Your capabilities:
@@ -127,6 +680,9 @@ Guidelines:
127
680
  - Reference relevant ContractSpec concepts and patterns
128
681
  - Ask clarifying questions when the user's intent is unclear
129
682
  - When suggesting code changes, explain the rationale`;
683
+ var WORKFLOW_TOOLS_PROMPT = `
684
+
685
+ 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
686
 
131
687
  class ChatService {
132
688
  provider;
@@ -136,19 +692,93 @@ class ChatService {
136
692
  maxHistoryMessages;
137
693
  onUsage;
138
694
  tools;
695
+ thinkingLevel;
139
696
  sendReasoning;
140
697
  sendSources;
698
+ modelSelector;
141
699
  constructor(config) {
142
700
  this.provider = config.provider;
143
701
  this.context = config.context;
144
702
  this.store = config.store ?? new InMemoryConversationStore;
145
- this.systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
703
+ this.systemPrompt = this.buildSystemPrompt(config);
146
704
  this.maxHistoryMessages = config.maxHistoryMessages ?? 20;
147
705
  this.onUsage = config.onUsage;
148
- this.tools = config.tools;
149
- this.sendReasoning = config.sendReasoning ?? false;
706
+ this.tools = this.mergeTools(config);
707
+ this.thinkingLevel = config.thinkingLevel;
708
+ this.modelSelector = config.modelSelector;
709
+ this.sendReasoning = config.sendReasoning ?? (config.thinkingLevel != null && config.thinkingLevel !== "instant");
150
710
  this.sendSources = config.sendSources ?? false;
151
711
  }
712
+ buildSystemPrompt(config) {
713
+ let base = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
714
+ if (config.workflowToolsConfig?.baseWorkflows?.length) {
715
+ base += WORKFLOW_TOOLS_PROMPT;
716
+ }
717
+ const contractsPrompt = buildContractsContextPrompt(config.contractsContext ?? {});
718
+ if (contractsPrompt) {
719
+ base += contractsPrompt;
720
+ }
721
+ if (config.surfacePlanConfig?.plan) {
722
+ const plannerInput = buildPlannerPromptInput(config.surfacePlanConfig.plan);
723
+ base += `
724
+
725
+ ` + compilePlannerPrompt(plannerInput);
726
+ }
727
+ return base;
728
+ }
729
+ mergeTools(config) {
730
+ let merged = config.tools ?? {};
731
+ const wfConfig = config.workflowToolsConfig;
732
+ if (wfConfig?.baseWorkflows?.length) {
733
+ const workflowTools = createWorkflowTools({
734
+ baseWorkflows: wfConfig.baseWorkflows,
735
+ composer: wfConfig.composer
736
+ });
737
+ merged = { ...merged, ...workflowTools };
738
+ }
739
+ const contractsCtx = config.contractsContext;
740
+ if (contractsCtx?.agentSpecs?.length) {
741
+ const allTools = [];
742
+ for (const agent of contractsCtx.agentSpecs) {
743
+ if (agent.tools?.length)
744
+ allTools.push(...agent.tools);
745
+ }
746
+ if (allTools.length > 0) {
747
+ const agentTools = agentToolConfigsToToolSet(allTools);
748
+ merged = { ...merged, ...agentTools };
749
+ }
750
+ }
751
+ const surfaceConfig = config.surfacePlanConfig;
752
+ if (surfaceConfig?.plan) {
753
+ const plannerTools = createSurfacePlannerTools({
754
+ plan: surfaceConfig.plan,
755
+ onPatchProposal: surfaceConfig.onPatchProposal
756
+ });
757
+ merged = { ...merged, ...plannerTools };
758
+ }
759
+ if (config.mcpTools && Object.keys(config.mcpTools).length > 0) {
760
+ merged = { ...merged, ...config.mcpTools };
761
+ }
762
+ return Object.keys(merged).length > 0 ? merged : undefined;
763
+ }
764
+ async resolveModel() {
765
+ if (this.modelSelector) {
766
+ const dimension = this.thinkingLevelToDimension(this.thinkingLevel);
767
+ const { model, selection } = await this.modelSelector.selectAndCreate({
768
+ taskDimension: dimension
769
+ });
770
+ return { model, providerName: selection.providerKey };
771
+ }
772
+ return {
773
+ model: this.provider.getModel(),
774
+ providerName: this.provider.name
775
+ };
776
+ }
777
+ thinkingLevelToDimension(level) {
778
+ if (!level || level === "instant")
779
+ return "latency";
780
+ return "reasoning";
781
+ }
152
782
  async send(options) {
153
783
  let conversation;
154
784
  if (options.conversationId) {
@@ -166,20 +796,25 @@ class ChatService {
166
796
  workspacePath: this.context?.workspacePath
167
797
  });
168
798
  }
169
- await this.store.appendMessage(conversation.id, {
170
- role: "user",
171
- content: options.content,
172
- status: "completed",
173
- attachments: options.attachments
174
- });
799
+ if (!options.skipUserAppend) {
800
+ await this.store.appendMessage(conversation.id, {
801
+ role: "user",
802
+ content: options.content,
803
+ status: "completed",
804
+ attachments: options.attachments
805
+ });
806
+ }
807
+ conversation = await this.store.get(conversation.id) ?? conversation;
175
808
  const messages = this.buildMessages(conversation, options);
176
- const model = this.provider.getModel();
809
+ const { model, providerName } = await this.resolveModel();
810
+ const providerOptions = getProviderOptions(this.thinkingLevel, providerName);
177
811
  try {
178
812
  const result = await generateText({
179
813
  model,
180
814
  messages,
181
815
  system: this.systemPrompt,
182
- tools: this.tools
816
+ tools: this.tools,
817
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
183
818
  });
184
819
  const assistantMessage = await this.store.appendMessage(conversation.id, {
185
820
  role: "assistant",
@@ -224,23 +859,27 @@ class ChatService {
224
859
  workspacePath: this.context?.workspacePath
225
860
  });
226
861
  }
227
- await this.store.appendMessage(conversation.id, {
228
- role: "user",
229
- content: options.content,
230
- status: "completed",
231
- attachments: options.attachments
232
- });
862
+ if (!options.skipUserAppend) {
863
+ await this.store.appendMessage(conversation.id, {
864
+ role: "user",
865
+ content: options.content,
866
+ status: "completed",
867
+ attachments: options.attachments
868
+ });
869
+ }
870
+ conversation = await this.store.get(conversation.id) ?? conversation;
233
871
  const assistantMessage = await this.store.appendMessage(conversation.id, {
234
872
  role: "assistant",
235
873
  content: "",
236
874
  status: "streaming"
237
875
  });
238
876
  const messages = this.buildMessages(conversation, options);
239
- const model = this.provider.getModel();
877
+ const { model, providerName } = await this.resolveModel();
240
878
  const systemPrompt = this.systemPrompt;
241
879
  const tools = this.tools;
242
880
  const store = this.store;
243
881
  const onUsage = this.onUsage;
882
+ const streamProviderOptions = getProviderOptions(this.thinkingLevel, providerName);
244
883
  async function* streamGenerator() {
245
884
  let fullContent = "";
246
885
  let fullReasoning = "";
@@ -251,7 +890,8 @@ class ChatService {
251
890
  model,
252
891
  messages,
253
892
  system: systemPrompt,
254
- tools
893
+ tools,
894
+ providerOptions: Object.keys(streamProviderOptions).length > 0 ? streamProviderOptions : undefined
255
895
  });
256
896
  for await (const part of result.fullStream) {
257
897
  if (part.type === "text-delta") {
@@ -366,6 +1006,18 @@ class ChatService {
366
1006
  ...options
367
1007
  });
368
1008
  }
1009
+ async updateConversation(conversationId, updates) {
1010
+ return this.store.update(conversationId, updates);
1011
+ }
1012
+ async forkConversation(conversationId, upToMessageId) {
1013
+ return this.store.fork(conversationId, upToMessageId);
1014
+ }
1015
+ async updateMessage(conversationId, messageId, updates) {
1016
+ return this.store.updateMessage(conversationId, messageId, updates);
1017
+ }
1018
+ async truncateAfter(conversationId, messageId) {
1019
+ return this.store.truncateAfter(conversationId, messageId);
1020
+ }
369
1021
  async deleteConversation(conversationId) {
370
1022
  return this.store.delete(conversationId);
371
1023
  }
@@ -434,7 +1086,12 @@ import {
434
1086
  } from "ai";
435
1087
  var DEFAULT_SYSTEM_PROMPT2 = `You are a helpful AI assistant.`;
436
1088
  function createChatRoute(options) {
437
- const { provider, systemPrompt = DEFAULT_SYSTEM_PROMPT2, tools } = options;
1089
+ const {
1090
+ provider,
1091
+ systemPrompt = DEFAULT_SYSTEM_PROMPT2,
1092
+ tools,
1093
+ thinkingLevel: defaultThinkingLevel
1094
+ } = options;
438
1095
  return async (req) => {
439
1096
  if (req.method !== "POST") {
440
1097
  return new Response("Method not allowed", { status: 405 });
@@ -449,12 +1106,15 @@ function createChatRoute(options) {
449
1106
  if (!Array.isArray(messages) || messages.length === 0) {
450
1107
  return new Response("messages array required", { status: 400 });
451
1108
  }
1109
+ const thinkingLevel = body.thinkingLevel ?? defaultThinkingLevel;
1110
+ const providerOptions = getProviderOptions(thinkingLevel, provider.name);
452
1111
  const model = provider.getModel();
453
1112
  const result = streamText2({
454
1113
  model,
455
1114
  messages: await convertToModelMessages(messages),
456
1115
  system: systemPrompt,
457
- tools
1116
+ tools,
1117
+ providerOptions: Object.keys(providerOptions).length > 0 ? providerOptions : undefined
458
1118
  });
459
1119
  return result.toUIMessageStreamResponse();
460
1120
  };
@@ -486,11 +1146,468 @@ function createCompletionRoute(options) {
486
1146
  return result.toTextStreamResponse();
487
1147
  };
488
1148
  }
1149
+ // src/core/export-formatters.ts
1150
+ function formatTimestamp(date) {
1151
+ return date.toLocaleTimeString([], {
1152
+ hour: "2-digit",
1153
+ minute: "2-digit"
1154
+ });
1155
+ }
1156
+ function toIsoString(date) {
1157
+ return date.toISOString();
1158
+ }
1159
+ function messageToJsonSerializable(msg) {
1160
+ return {
1161
+ id: msg.id,
1162
+ conversationId: msg.conversationId,
1163
+ role: msg.role,
1164
+ content: msg.content,
1165
+ status: msg.status,
1166
+ createdAt: toIsoString(msg.createdAt),
1167
+ updatedAt: toIsoString(msg.updatedAt),
1168
+ ...msg.attachments && { attachments: msg.attachments },
1169
+ ...msg.codeBlocks && { codeBlocks: msg.codeBlocks },
1170
+ ...msg.toolCalls && { toolCalls: msg.toolCalls },
1171
+ ...msg.sources && { sources: msg.sources },
1172
+ ...msg.reasoning && { reasoning: msg.reasoning },
1173
+ ...msg.usage && { usage: msg.usage },
1174
+ ...msg.error && { error: msg.error },
1175
+ ...msg.metadata && { metadata: msg.metadata }
1176
+ };
1177
+ }
1178
+ function formatSourcesMarkdown(sources) {
1179
+ if (sources.length === 0)
1180
+ return "";
1181
+ return `
1182
+
1183
+ **Sources:**
1184
+ ` + sources.map((s) => `- [${s.title}](${s.url ?? "#"})`).join(`
1185
+ `);
1186
+ }
1187
+ function formatSourcesTxt(sources) {
1188
+ if (sources.length === 0)
1189
+ return "";
1190
+ return `
1191
+
1192
+ Sources:
1193
+ ` + sources.map((s) => `- ${s.title}${s.url ? ` - ${s.url}` : ""}`).join(`
1194
+ `);
1195
+ }
1196
+ function formatToolCallsMarkdown(toolCalls) {
1197
+ if (toolCalls.length === 0)
1198
+ return "";
1199
+ return `
1200
+
1201
+ **Tool calls:**
1202
+ ` + toolCalls.map((tc) => `**${tc.name}** (${tc.status})
1203
+ \`\`\`json
1204
+ ${JSON.stringify(tc.args, null, 2)}
1205
+ \`\`\`` + (tc.result !== undefined ? `
1206
+ Output:
1207
+ \`\`\`json
1208
+ ${typeof tc.result === "object" ? JSON.stringify(tc.result, null, 2) : String(tc.result)}
1209
+ \`\`\`` : "") + (tc.error ? `
1210
+ Error: ${tc.error}` : "")).join(`
1211
+
1212
+ `);
1213
+ }
1214
+ function formatToolCallsTxt(toolCalls) {
1215
+ if (toolCalls.length === 0)
1216
+ return "";
1217
+ return `
1218
+
1219
+ Tool calls:
1220
+ ` + 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(`
1221
+ `);
1222
+ }
1223
+ function formatUsage(usage) {
1224
+ const total = usage.inputTokens + usage.outputTokens;
1225
+ return ` (${total} tokens)`;
1226
+ }
1227
+ function formatMessagesAsMarkdown(messages) {
1228
+ const parts = [];
1229
+ for (const msg of messages) {
1230
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
1231
+ const header = `## ${roleLabel}`;
1232
+ const timestamp = `*${formatTimestamp(msg.createdAt)}*`;
1233
+ const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
1234
+ const meta = `${timestamp}${usageSuffix}
1235
+
1236
+ `;
1237
+ let body = msg.content;
1238
+ if (msg.error) {
1239
+ body += `
1240
+
1241
+ **Error:** ${msg.error.code} - ${msg.error.message}`;
1242
+ }
1243
+ if (msg.reasoning) {
1244
+ body += `
1245
+
1246
+ > **Reasoning:**
1247
+ > ${msg.reasoning.replace(/\n/g, `
1248
+ > `)}`;
1249
+ }
1250
+ body += formatSourcesMarkdown(msg.sources ?? []);
1251
+ body += formatToolCallsMarkdown(msg.toolCalls ?? []);
1252
+ parts.push(`${header}
1253
+
1254
+ ${meta}${body}`);
1255
+ }
1256
+ return parts.join(`
1257
+
1258
+ ---
1259
+
1260
+ `);
1261
+ }
1262
+ function formatMessagesAsTxt(messages) {
1263
+ const parts = [];
1264
+ for (const msg of messages) {
1265
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
1266
+ const timestamp = `(${formatTimestamp(msg.createdAt)})`;
1267
+ const usageSuffix = msg.usage ? formatUsage(msg.usage) : "";
1268
+ const header = `[${roleLabel}] ${timestamp}${usageSuffix}
1269
+
1270
+ `;
1271
+ let body = msg.content;
1272
+ if (msg.error) {
1273
+ body += `
1274
+
1275
+ Error: ${msg.error.code} - ${msg.error.message}`;
1276
+ }
1277
+ if (msg.reasoning) {
1278
+ body += `
1279
+
1280
+ Reasoning: ${msg.reasoning}`;
1281
+ }
1282
+ body += formatSourcesTxt(msg.sources ?? []);
1283
+ body += formatToolCallsTxt(msg.toolCalls ?? []);
1284
+ parts.push(`${header}${body}`);
1285
+ }
1286
+ return parts.join(`
1287
+
1288
+ ---
1289
+
1290
+ `);
1291
+ }
1292
+ function formatMessagesAsJson(messages, conversation) {
1293
+ const payload = {
1294
+ messages: messages.map(messageToJsonSerializable)
1295
+ };
1296
+ if (conversation) {
1297
+ payload.conversation = {
1298
+ id: conversation.id,
1299
+ title: conversation.title,
1300
+ status: conversation.status,
1301
+ createdAt: toIsoString(conversation.createdAt),
1302
+ updatedAt: toIsoString(conversation.updatedAt),
1303
+ provider: conversation.provider,
1304
+ model: conversation.model,
1305
+ workspacePath: conversation.workspacePath,
1306
+ contextFiles: conversation.contextFiles,
1307
+ summary: conversation.summary,
1308
+ metadata: conversation.metadata
1309
+ };
1310
+ }
1311
+ return JSON.stringify(payload, null, 2);
1312
+ }
1313
+ function getExportFilename(format, conversation) {
1314
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1315
+ const base = conversation?.title ? conversation.title.replace(/[^a-zA-Z0-9-_]/g, "_").slice(0, 40) : "chat-export";
1316
+ const ext = format === "markdown" ? "md" : format === "txt" ? "txt" : "json";
1317
+ return `${base}-${timestamp}.${ext}`;
1318
+ }
1319
+ var MIME_TYPES = {
1320
+ markdown: "text/markdown",
1321
+ txt: "text/plain",
1322
+ json: "application/json"
1323
+ };
1324
+ function downloadAsFile(content, filename, mimeType) {
1325
+ const blob = new Blob([content], { type: mimeType });
1326
+ const url = URL.createObjectURL(blob);
1327
+ const a = document.createElement("a");
1328
+ a.href = url;
1329
+ a.download = filename;
1330
+ document.body.appendChild(a);
1331
+ a.click();
1332
+ document.body.removeChild(a);
1333
+ URL.revokeObjectURL(url);
1334
+ }
1335
+ function exportToFile(messages, format, conversation) {
1336
+ let content;
1337
+ if (format === "markdown") {
1338
+ content = formatMessagesAsMarkdown(messages);
1339
+ } else if (format === "txt") {
1340
+ content = formatMessagesAsTxt(messages);
1341
+ } else {
1342
+ content = formatMessagesAsJson(messages, conversation);
1343
+ }
1344
+ const filename = getExportFilename(format, conversation);
1345
+ const mimeType = MIME_TYPES[format];
1346
+ downloadAsFile(content, filename, mimeType);
1347
+ }
1348
+ // src/core/local-storage-conversation-store.ts
1349
+ var DEFAULT_KEY = "contractspec:ai-chat:conversations";
1350
+ function generateId2(prefix) {
1351
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
1352
+ }
1353
+ function toSerializable(conv) {
1354
+ return {
1355
+ ...conv,
1356
+ createdAt: conv.createdAt.toISOString(),
1357
+ updatedAt: conv.updatedAt.toISOString(),
1358
+ messages: conv.messages.map((m) => ({
1359
+ ...m,
1360
+ createdAt: m.createdAt.toISOString(),
1361
+ updatedAt: m.updatedAt.toISOString()
1362
+ }))
1363
+ };
1364
+ }
1365
+ function fromSerializable(raw) {
1366
+ const messages = raw.messages?.map((m) => ({
1367
+ ...m,
1368
+ createdAt: new Date(m.createdAt),
1369
+ updatedAt: new Date(m.updatedAt)
1370
+ })) ?? [];
1371
+ return {
1372
+ ...raw,
1373
+ createdAt: new Date(raw.createdAt),
1374
+ updatedAt: new Date(raw.updatedAt),
1375
+ messages
1376
+ };
1377
+ }
1378
+ function loadAll(key) {
1379
+ if (typeof window === "undefined")
1380
+ return new Map;
1381
+ try {
1382
+ const raw = window.localStorage.getItem(key);
1383
+ if (!raw)
1384
+ return new Map;
1385
+ const arr = JSON.parse(raw);
1386
+ const map = new Map;
1387
+ for (const item of arr) {
1388
+ const conv = fromSerializable(item);
1389
+ map.set(conv.id, conv);
1390
+ }
1391
+ return map;
1392
+ } catch {
1393
+ return new Map;
1394
+ }
1395
+ }
1396
+ function saveAll(key, map) {
1397
+ if (typeof window === "undefined")
1398
+ return;
1399
+ try {
1400
+ const arr = Array.from(map.values()).map(toSerializable);
1401
+ window.localStorage.setItem(key, JSON.stringify(arr));
1402
+ } catch {}
1403
+ }
1404
+
1405
+ class LocalStorageConversationStore {
1406
+ key;
1407
+ cache = null;
1408
+ constructor(storageKey = DEFAULT_KEY) {
1409
+ this.key = storageKey;
1410
+ }
1411
+ getMap() {
1412
+ if (!this.cache) {
1413
+ this.cache = loadAll(this.key);
1414
+ }
1415
+ return this.cache;
1416
+ }
1417
+ persist() {
1418
+ saveAll(this.key, this.getMap());
1419
+ }
1420
+ async get(conversationId) {
1421
+ return this.getMap().get(conversationId) ?? null;
1422
+ }
1423
+ async create(conversation) {
1424
+ const now = new Date;
1425
+ const full = {
1426
+ ...conversation,
1427
+ id: generateId2("conv"),
1428
+ createdAt: now,
1429
+ updatedAt: now
1430
+ };
1431
+ this.getMap().set(full.id, full);
1432
+ this.persist();
1433
+ return full;
1434
+ }
1435
+ async update(conversationId, updates) {
1436
+ const conv = this.getMap().get(conversationId);
1437
+ if (!conv)
1438
+ return null;
1439
+ const updated = {
1440
+ ...conv,
1441
+ ...updates,
1442
+ updatedAt: new Date
1443
+ };
1444
+ this.getMap().set(conversationId, updated);
1445
+ this.persist();
1446
+ return updated;
1447
+ }
1448
+ async appendMessage(conversationId, message) {
1449
+ const conv = this.getMap().get(conversationId);
1450
+ if (!conv)
1451
+ throw new Error(`Conversation ${conversationId} not found`);
1452
+ const now = new Date;
1453
+ const fullMessage = {
1454
+ ...message,
1455
+ id: generateId2("msg"),
1456
+ conversationId,
1457
+ createdAt: now,
1458
+ updatedAt: now
1459
+ };
1460
+ conv.messages.push(fullMessage);
1461
+ conv.updatedAt = now;
1462
+ this.persist();
1463
+ return fullMessage;
1464
+ }
1465
+ async updateMessage(conversationId, messageId, updates) {
1466
+ const conv = this.getMap().get(conversationId);
1467
+ if (!conv)
1468
+ return null;
1469
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
1470
+ if (idx === -1)
1471
+ return null;
1472
+ const msg = conv.messages[idx];
1473
+ if (!msg)
1474
+ return null;
1475
+ const updated = {
1476
+ ...msg,
1477
+ ...updates,
1478
+ updatedAt: new Date
1479
+ };
1480
+ conv.messages[idx] = updated;
1481
+ conv.updatedAt = new Date;
1482
+ this.persist();
1483
+ return updated;
1484
+ }
1485
+ async delete(conversationId) {
1486
+ const deleted = this.getMap().delete(conversationId);
1487
+ if (deleted)
1488
+ this.persist();
1489
+ return deleted;
1490
+ }
1491
+ async list(options) {
1492
+ let results = Array.from(this.getMap().values());
1493
+ if (options?.status) {
1494
+ results = results.filter((c) => c.status === options.status);
1495
+ }
1496
+ if (options?.projectId) {
1497
+ results = results.filter((c) => c.projectId === options.projectId);
1498
+ }
1499
+ if (options?.tags && options.tags.length > 0) {
1500
+ const tagSet = new Set(options.tags);
1501
+ results = results.filter((c) => c.tags && c.tags.some((t) => tagSet.has(t)));
1502
+ }
1503
+ results.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
1504
+ const offset = options?.offset ?? 0;
1505
+ const limit = options?.limit ?? 100;
1506
+ return results.slice(offset, offset + limit);
1507
+ }
1508
+ async fork(conversationId, upToMessageId) {
1509
+ const source = this.getMap().get(conversationId);
1510
+ if (!source)
1511
+ throw new Error(`Conversation ${conversationId} not found`);
1512
+ let messagesToCopy = source.messages;
1513
+ if (upToMessageId) {
1514
+ const idx = source.messages.findIndex((m) => m.id === upToMessageId);
1515
+ if (idx === -1)
1516
+ throw new Error(`Message ${upToMessageId} not found`);
1517
+ messagesToCopy = source.messages.slice(0, idx + 1);
1518
+ }
1519
+ const now = new Date;
1520
+ const forkedMessages = messagesToCopy.map((m) => ({
1521
+ ...m,
1522
+ id: generateId2("msg"),
1523
+ conversationId: "",
1524
+ createdAt: new Date(m.createdAt),
1525
+ updatedAt: new Date(m.updatedAt)
1526
+ }));
1527
+ const forked = {
1528
+ ...source,
1529
+ id: generateId2("conv"),
1530
+ title: source.title ? `${source.title} (fork)` : undefined,
1531
+ forkedFromId: source.id,
1532
+ createdAt: now,
1533
+ updatedAt: now,
1534
+ messages: forkedMessages
1535
+ };
1536
+ for (const m of forked.messages) {
1537
+ m.conversationId = forked.id;
1538
+ }
1539
+ this.getMap().set(forked.id, forked);
1540
+ this.persist();
1541
+ return forked;
1542
+ }
1543
+ async truncateAfter(conversationId, messageId) {
1544
+ const conv = this.getMap().get(conversationId);
1545
+ if (!conv)
1546
+ return null;
1547
+ const idx = conv.messages.findIndex((m) => m.id === messageId);
1548
+ if (idx === -1)
1549
+ return null;
1550
+ conv.messages = conv.messages.slice(0, idx + 1);
1551
+ conv.updatedAt = new Date;
1552
+ this.persist();
1553
+ return conv;
1554
+ }
1555
+ async search(query, limit = 20) {
1556
+ const lowerQuery = query.toLowerCase();
1557
+ const results = [];
1558
+ for (const conv of this.getMap().values()) {
1559
+ if (conv.title?.toLowerCase().includes(lowerQuery)) {
1560
+ results.push(conv);
1561
+ continue;
1562
+ }
1563
+ if (conv.messages.some((m) => m.content.toLowerCase().includes(lowerQuery))) {
1564
+ results.push(conv);
1565
+ }
1566
+ if (results.length >= limit)
1567
+ break;
1568
+ }
1569
+ return results;
1570
+ }
1571
+ }
1572
+ function createLocalStorageConversationStore(storageKey) {
1573
+ return new LocalStorageConversationStore(storageKey);
1574
+ }
1575
+ // src/core/agent-adapter.ts
1576
+ function createChatAgentAdapter(agent) {
1577
+ return {
1578
+ async generate({ prompt, signal }) {
1579
+ const result = await agent.generate({ prompt, signal });
1580
+ return {
1581
+ text: result.text,
1582
+ toolCalls: result.toolCalls,
1583
+ toolResults: result.toolResults,
1584
+ usage: result.usage
1585
+ };
1586
+ }
1587
+ };
1588
+ }
489
1589
  export {
1590
+ getProviderOptions,
1591
+ getExportFilename,
1592
+ formatMessagesAsTxt,
1593
+ formatMessagesAsMarkdown,
1594
+ formatMessagesAsJson,
1595
+ exportToFile,
1596
+ downloadAsFile,
1597
+ createWorkflowTools,
1598
+ createSurfacePlannerTools,
1599
+ createLocalStorageConversationStore,
490
1600
  createInMemoryConversationStore,
491
1601
  createCompletionRoute,
492
1602
  createChatService,
493
1603
  createChatRoute,
1604
+ createChatAgentAdapter,
1605
+ buildPlannerPromptInput,
1606
+ buildContractsContextPrompt,
1607
+ agentToolConfigsToToolSet,
1608
+ THINKING_LEVEL_LABELS,
1609
+ THINKING_LEVEL_DESCRIPTIONS,
1610
+ LocalStorageConversationStore,
494
1611
  InMemoryConversationStore,
495
1612
  ChatService
496
1613
  };