@contractspec/module.ai-chat 4.3.6 → 4.3.7

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 (46) hide show
  1. package/README.md +62 -370
  2. package/dist/adapters/ai-sdk-bundle-adapter.d.ts +1 -1
  3. package/dist/adapters/index.d.ts +1 -1
  4. package/dist/browser/context/index.js +93 -93
  5. package/dist/browser/core/index.js +308 -307
  6. package/dist/browser/index.js +3803 -3801
  7. package/dist/browser/presentation/components/index.js +2617 -2618
  8. package/dist/browser/presentation/hooks/index.js +476 -476
  9. package/dist/browser/presentation/index.js +2474 -2475
  10. package/dist/browser/providers/index.js +7 -7
  11. package/dist/context/index.d.ts +1 -1
  12. package/dist/context/index.js +93 -93
  13. package/dist/core/agent-tools-adapter.d.ts +1 -1
  14. package/dist/core/chat-service.d.ts +4 -4
  15. package/dist/core/create-chat-route.d.ts +1 -1
  16. package/dist/core/create-completion-route.d.ts +15 -0
  17. package/dist/core/export-formatters.d.ts +1 -1
  18. package/dist/core/index.d.ts +6 -6
  19. package/dist/core/index.js +308 -307
  20. package/dist/core/local-storage-conversation-store.d.ts +1 -1
  21. package/dist/core/surface-planner-tools.d.ts +2 -2
  22. package/dist/core/workflow-tools.d.ts +1 -1
  23. package/dist/index.d.ts +8 -8
  24. package/dist/index.js +3803 -3801
  25. package/dist/node/context/index.js +93 -93
  26. package/dist/node/core/index.js +308 -307
  27. package/dist/node/index.js +3803 -3801
  28. package/dist/node/presentation/components/index.js +2617 -2618
  29. package/dist/node/presentation/hooks/index.js +476 -476
  30. package/dist/node/presentation/index.js +2474 -2475
  31. package/dist/node/providers/index.js +7 -7
  32. package/dist/presentation/components/ChainOfThought.d.ts +1 -1
  33. package/dist/presentation/components/ChatExportToolbar.d.ts +1 -1
  34. package/dist/presentation/components/ChatSidebar.d.ts +1 -1
  35. package/dist/presentation/components/ChatWithExport.d.ts +1 -1
  36. package/dist/presentation/components/index.d.ts +11 -11
  37. package/dist/presentation/components/index.js +2617 -2618
  38. package/dist/presentation/hooks/index.d.ts +4 -4
  39. package/dist/presentation/hooks/index.js +476 -476
  40. package/dist/presentation/hooks/useChat.d.ts +6 -6
  41. package/dist/presentation/hooks/useConversations.d.ts +1 -1
  42. package/dist/presentation/hooks/useProviders.d.ts +1 -1
  43. package/dist/presentation/index.js +2474 -2475
  44. package/dist/providers/index.d.ts +2 -2
  45. package/dist/providers/index.js +7 -7
  46. package/package.json +10 -10
@@ -1,13 +1,109 @@
1
1
  import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
+ // src/presentation/hooks/index.ts
5
+ import { useCompletion } from "@ai-sdk/react";
6
+
4
7
  // src/presentation/hooks/useChat.tsx
5
- import * as React from "react";
8
+ import {
9
+ createProvider
10
+ } from "@contractspec/lib.ai-providers";
6
11
  import { tool as tool4 } from "ai";
12
+ import * as React from "react";
7
13
  import { z as z4 } from "zod";
8
14
 
9
15
  // src/core/chat-service.ts
10
16
  import { generateText, streamText } from "ai";
17
+ import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
18
+
19
+ // src/core/agent-tools-adapter.ts
20
+ import { tool } from "ai";
21
+ import { z } from "zod";
22
+ function getInputSchema(_schema) {
23
+ return z.object({}).passthrough();
24
+ }
25
+ function agentToolConfigsToToolSet(configs, handlers) {
26
+ const result = {};
27
+ for (const config of configs) {
28
+ const handler = handlers?.[config.name];
29
+ const inputSchema = getInputSchema(config.schema);
30
+ result[config.name] = tool({
31
+ description: config.description ?? config.name,
32
+ inputSchema,
33
+ execute: async (input) => {
34
+ if (!handler) {
35
+ return {
36
+ status: "unimplemented",
37
+ message: "Wire handler in host",
38
+ toolName: config.name
39
+ };
40
+ }
41
+ try {
42
+ const output = await Promise.resolve(handler(input));
43
+ return typeof output === "string" ? output : output;
44
+ } catch (err) {
45
+ return {
46
+ status: "error",
47
+ error: err instanceof Error ? err.message : String(err),
48
+ toolName: config.name
49
+ };
50
+ }
51
+ }
52
+ });
53
+ }
54
+ return result;
55
+ }
56
+
57
+ // src/core/contracts-context.ts
58
+ function buildContractsContextPrompt(config) {
59
+ const parts = [];
60
+ if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
61
+ return "";
62
+ }
63
+ parts.push(`
64
+
65
+ ## Available resources`);
66
+ if (config.agentSpecs?.length) {
67
+ parts.push(`
68
+ ### Agent tools`);
69
+ for (const agent of config.agentSpecs) {
70
+ const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
71
+ parts.push(`- **${agent.key}**: tools: ${toolNames}`);
72
+ }
73
+ }
74
+ if (config.dataViewSpecs?.length) {
75
+ parts.push(`
76
+ ### Data views`);
77
+ for (const dv of config.dataViewSpecs) {
78
+ parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
79
+ }
80
+ }
81
+ if (config.formSpecs?.length) {
82
+ parts.push(`
83
+ ### Forms`);
84
+ for (const form of config.formSpecs) {
85
+ parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
86
+ }
87
+ }
88
+ if (config.presentationSpecs?.length) {
89
+ parts.push(`
90
+ ### Presentations`);
91
+ for (const pres of config.presentationSpecs) {
92
+ parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
93
+ }
94
+ }
95
+ if (config.operationRefs?.length) {
96
+ parts.push(`
97
+ ### Operations`);
98
+ for (const op of config.operationRefs) {
99
+ parts.push(`- **${op.key}@${op.version}**`);
100
+ }
101
+ }
102
+ parts.push(`
103
+ Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
104
+ return parts.join(`
105
+ `);
106
+ }
11
107
 
12
108
  // src/core/conversation-store.ts
13
109
  function generateId(prefix) {
@@ -170,6 +266,163 @@ function createInMemoryConversationStore() {
170
266
  return new InMemoryConversationStore;
171
267
  }
172
268
 
269
+ // src/core/surface-planner-tools.ts
270
+ import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
271
+ import {
272
+ validatePatchProposal
273
+ } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
274
+ import { tool as tool2 } from "ai";
275
+ import { z as z2 } from "zod";
276
+ var VALID_OPS = [
277
+ "insert-node",
278
+ "replace-node",
279
+ "remove-node",
280
+ "move-node",
281
+ "resize-panel",
282
+ "set-layout",
283
+ "reveal-field",
284
+ "hide-field",
285
+ "promote-action",
286
+ "set-focus"
287
+ ];
288
+ var DEFAULT_NODE_KINDS = [
289
+ "entity-section",
290
+ "entity-card",
291
+ "data-view",
292
+ "assistant-panel",
293
+ "chat-thread",
294
+ "action-bar",
295
+ "timeline",
296
+ "table",
297
+ "rich-doc",
298
+ "form",
299
+ "chart",
300
+ "custom-widget"
301
+ ];
302
+ function collectSlotIdsFromRegion(node) {
303
+ const ids = [];
304
+ if (node.type === "slot") {
305
+ ids.push(node.slotId);
306
+ }
307
+ if (node.type === "panel-group" || node.type === "stack") {
308
+ for (const child of node.children) {
309
+ ids.push(...collectSlotIdsFromRegion(child));
310
+ }
311
+ }
312
+ if (node.type === "tabs") {
313
+ for (const tab of node.tabs) {
314
+ ids.push(...collectSlotIdsFromRegion(tab.child));
315
+ }
316
+ }
317
+ if (node.type === "floating") {
318
+ ids.push(node.anchorSlotId);
319
+ ids.push(...collectSlotIdsFromRegion(node.child));
320
+ }
321
+ return ids;
322
+ }
323
+ function deriveConstraints(plan) {
324
+ const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
325
+ const uniqueSlots = [...new Set(slotIds)];
326
+ return {
327
+ allowedOps: VALID_OPS,
328
+ allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
329
+ allowedNodeKinds: DEFAULT_NODE_KINDS
330
+ };
331
+ }
332
+ var ProposePatchInputSchema = z2.object({
333
+ proposalId: z2.string().describe("Unique proposal identifier"),
334
+ ops: z2.array(z2.object({
335
+ op: z2.enum([
336
+ "insert-node",
337
+ "replace-node",
338
+ "remove-node",
339
+ "move-node",
340
+ "resize-panel",
341
+ "set-layout",
342
+ "reveal-field",
343
+ "hide-field",
344
+ "promote-action",
345
+ "set-focus"
346
+ ]),
347
+ slotId: z2.string().optional(),
348
+ nodeId: z2.string().optional(),
349
+ toSlotId: z2.string().optional(),
350
+ index: z2.number().optional(),
351
+ node: z2.object({
352
+ nodeId: z2.string(),
353
+ kind: z2.string(),
354
+ title: z2.string().optional(),
355
+ props: z2.record(z2.string(), z2.unknown()).optional(),
356
+ children: z2.array(z2.unknown()).optional()
357
+ }).optional(),
358
+ persistKey: z2.string().optional(),
359
+ sizes: z2.array(z2.number()).optional(),
360
+ layoutId: z2.string().optional(),
361
+ fieldId: z2.string().optional(),
362
+ actionId: z2.string().optional(),
363
+ placement: z2.enum(["header", "inline", "context", "assistant"]).optional(),
364
+ targetId: z2.string().optional()
365
+ }))
366
+ });
367
+ function createSurfacePlannerTools(config) {
368
+ const { plan, constraints, onPatchProposal } = config;
369
+ const resolvedConstraints = constraints ?? deriveConstraints(plan);
370
+ const proposePatchTool = tool2({
371
+ description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
372
+ inputSchema: ProposePatchInputSchema,
373
+ execute: async (input) => {
374
+ const ops = input.ops;
375
+ try {
376
+ validatePatchProposal(ops, resolvedConstraints);
377
+ const proposal = buildSurfacePatchProposal(input.proposalId, ops);
378
+ onPatchProposal?.(proposal);
379
+ return {
380
+ success: true,
381
+ proposalId: proposal.proposalId,
382
+ opsCount: proposal.ops.length,
383
+ message: "Patch proposal validated; awaiting user approval"
384
+ };
385
+ } catch (err) {
386
+ return {
387
+ success: false,
388
+ error: err instanceof Error ? err.message : String(err),
389
+ proposalId: input.proposalId
390
+ };
391
+ }
392
+ }
393
+ });
394
+ return {
395
+ "propose-patch": proposePatchTool
396
+ };
397
+ }
398
+ function buildPlannerPromptInput(plan) {
399
+ const constraints = deriveConstraints(plan);
400
+ return {
401
+ bundleMeta: {
402
+ key: plan.bundleKey,
403
+ version: "0.0.0",
404
+ title: plan.bundleKey
405
+ },
406
+ surfaceId: plan.surfaceId,
407
+ allowedPatchOps: constraints.allowedOps,
408
+ allowedSlots: [...constraints.allowedSlots],
409
+ allowedNodeKinds: [...constraints.allowedNodeKinds],
410
+ actions: plan.actions.map((a) => ({
411
+ actionId: a.actionId,
412
+ title: a.title
413
+ })),
414
+ preferences: {
415
+ guidance: "hints",
416
+ density: "standard",
417
+ dataDepth: "detailed",
418
+ control: "standard",
419
+ media: "text",
420
+ pace: "balanced",
421
+ narrative: "top-down"
422
+ }
423
+ };
424
+ }
425
+
173
426
  // src/core/thinking-levels.ts
174
427
  var THINKING_LEVEL_LABELS = {
175
428
  instant: "Instant",
@@ -222,457 +475,210 @@ function getProviderOptions(level, providerName) {
222
475
  }
223
476
 
224
477
  // src/core/workflow-tools.ts
225
- import { tool } from "ai";
226
- import { z } from "zod";
227
478
  import {
228
- WorkflowComposer,
229
- validateExtension
479
+ validateExtension,
480
+ WorkflowComposer
230
481
  } from "@contractspec/lib.workflow-composer";
231
- var StepTypeSchema = z.enum(["human", "automation", "decision"]);
232
- var StepActionSchema = z.object({
233
- operation: z.object({
234
- name: z.string(),
235
- version: z.number()
482
+ import { tool as tool3 } from "ai";
483
+ import { z as z3 } from "zod";
484
+ var StepTypeSchema = z3.enum(["human", "automation", "decision"]);
485
+ var StepActionSchema = z3.object({
486
+ operation: z3.object({
487
+ name: z3.string(),
488
+ version: z3.number()
236
489
  }).optional(),
237
- form: z.object({
238
- key: z.string(),
239
- version: z.number()
490
+ form: z3.object({
491
+ key: z3.string(),
492
+ version: z3.number()
240
493
  }).optional()
241
494
  }).optional();
242
- var StepSchema = z.object({
243
- id: z.string(),
495
+ var StepSchema = z3.object({
496
+ id: z3.string(),
244
497
  type: StepTypeSchema,
245
- label: z.string(),
246
- description: z.string().optional(),
498
+ label: z3.string(),
499
+ description: z3.string().optional(),
247
500
  action: StepActionSchema
248
501
  });
249
- var StepInjectionSchema = z.object({
250
- after: z.string().optional(),
251
- before: z.string().optional(),
502
+ var StepInjectionSchema = z3.object({
503
+ after: z3.string().optional(),
504
+ before: z3.string().optional(),
252
505
  inject: StepSchema,
253
- transitionTo: z.string().optional(),
254
- transitionFrom: z.string().optional(),
255
- when: z.string().optional()
506
+ transitionTo: z3.string().optional(),
507
+ transitionFrom: z3.string().optional(),
508
+ when: z3.string().optional()
256
509
  });
257
- var WorkflowExtensionInputSchema = z.object({
258
- workflow: z.string(),
259
- tenantId: z.string().optional(),
260
- role: z.string().optional(),
261
- priority: z.number().optional(),
262
- customSteps: z.array(StepInjectionSchema).optional(),
263
- hiddenSteps: z.array(z.string()).optional()
510
+ var WorkflowExtensionInputSchema = z3.object({
511
+ workflow: z3.string(),
512
+ tenantId: z3.string().optional(),
513
+ role: z3.string().optional(),
514
+ priority: z3.number().optional(),
515
+ customSteps: z3.array(StepInjectionSchema).optional(),
516
+ hiddenSteps: z3.array(z3.string()).optional()
264
517
  });
265
518
  function createWorkflowTools(config) {
266
519
  const { baseWorkflows, composer } = config;
267
520
  const baseByKey = new Map(baseWorkflows.map((b) => [b.meta.key, b]));
268
- const createWorkflowExtensionTool = tool({
521
+ const createWorkflowExtensionTool = tool3({
269
522
  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.",
270
- inputSchema: WorkflowExtensionInputSchema,
271
- execute: async (input) => {
272
- const extension = {
273
- workflow: input.workflow,
274
- tenantId: input.tenantId,
275
- role: input.role,
276
- priority: input.priority,
277
- customSteps: input.customSteps,
278
- hiddenSteps: input.hiddenSteps
279
- };
280
- const base = baseByKey.get(input.workflow);
281
- if (!base) {
282
- return {
283
- success: false,
284
- error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
285
- extension
286
- };
287
- }
288
- try {
289
- validateExtension(extension, base);
290
- return {
291
- success: true,
292
- message: "Extension validated successfully",
293
- extension
294
- };
295
- } catch (err) {
296
- return {
297
- success: false,
298
- error: err instanceof Error ? err.message : String(err),
299
- extension
300
- };
301
- }
302
- }
303
- });
304
- const composeWorkflowInputSchema = z.object({
305
- workflowKey: z.string().describe("Base workflow meta.key"),
306
- tenantId: z.string().optional(),
307
- role: z.string().optional(),
308
- extensions: z.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
309
- });
310
- const composeWorkflowTool = tool({
311
- description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
312
- inputSchema: composeWorkflowInputSchema,
313
- execute: async (input) => {
314
- const base = baseByKey.get(input.workflowKey);
315
- if (!base) {
316
- return {
317
- success: false,
318
- error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
319
- };
320
- }
321
- const comp = composer ?? new WorkflowComposer;
322
- if (input.extensions?.length) {
323
- for (const ext of input.extensions) {
324
- comp.register({
325
- workflow: ext.workflow,
326
- tenantId: ext.tenantId,
327
- role: ext.role,
328
- priority: ext.priority,
329
- customSteps: ext.customSteps,
330
- hiddenSteps: ext.hiddenSteps
331
- });
332
- }
333
- }
334
- try {
335
- const composed = comp.compose({
336
- base,
337
- tenantId: input.tenantId,
338
- role: input.role
339
- });
340
- return {
341
- success: true,
342
- workflow: composed,
343
- meta: composed.meta,
344
- stepIds: composed.definition.steps.map((s) => s.id)
345
- };
346
- } catch (err) {
347
- return {
348
- success: false,
349
- error: err instanceof Error ? err.message : String(err)
350
- };
351
- }
352
- }
353
- });
354
- const generateWorkflowSpecCodeInputSchema = z.object({
355
- workflowKey: z.string().describe("Workflow meta.key"),
356
- composedSteps: z.array(z.object({
357
- id: z.string(),
358
- type: z.enum(["human", "automation", "decision"]),
359
- label: z.string(),
360
- description: z.string().optional()
361
- })).optional().describe("Steps to include; if omitted, uses the base workflow")
362
- });
363
- const generateWorkflowSpecCodeTool = tool({
364
- description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
365
- inputSchema: generateWorkflowSpecCodeInputSchema,
366
- execute: async (input) => {
367
- const base = baseByKey.get(input.workflowKey);
368
- if (!base) {
369
- return {
370
- success: false,
371
- error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
372
- code: null
373
- };
374
- }
375
- const steps = input.composedSteps ?? base.definition.steps;
376
- const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
377
- const stepsCode = steps.map((s) => ` {
378
- id: '${s.id}',
379
- type: '${s.type}',
380
- label: '${escapeString(s.label)}',${s.description ? `
381
- description: '${escapeString(s.description)}',` : ""}
382
- }`).join(`,
383
- `);
384
- const meta = base.meta;
385
- const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
386
- const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
387
-
388
- /**
389
- * Workflow: ${base.meta.key}
390
- * Generated via AI chat workflow tools.
391
- */
392
- export const ${specVarName}: WorkflowSpec = {
393
- meta: {
394
- key: '${base.meta.key}',
395
- version: '${String(base.meta.version)}',
396
- title: '${escapeString(meta.title ?? base.meta.key)}',
397
- description: '${escapeString(meta.description ?? "")}',
398
- },
399
- definition: {
400
- entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
401
- steps: [
402
- ${stepsCode}
403
- ],
404
- transitions: ${transitionsJson},
405
- },
406
- };
407
- `;
408
- return {
409
- success: true,
410
- code,
411
- workflowKey: input.workflowKey
412
- };
413
- }
414
- });
415
- return {
416
- create_workflow_extension: createWorkflowExtensionTool,
417
- compose_workflow: composeWorkflowTool,
418
- generate_workflow_spec_code: generateWorkflowSpecCodeTool
419
- };
420
- }
421
- function toPascalCase(value) {
422
- return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
423
- }
424
- function escapeString(value) {
425
- return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
426
- }
427
-
428
- // src/core/contracts-context.ts
429
- function buildContractsContextPrompt(config) {
430
- const parts = [];
431
- if (!config.agentSpecs?.length && !config.dataViewSpecs?.length && !config.formSpecs?.length && !config.presentationSpecs?.length && !config.operationRefs?.length) {
432
- return "";
433
- }
434
- parts.push(`
435
-
436
- ## Available resources`);
437
- if (config.agentSpecs?.length) {
438
- parts.push(`
439
- ### Agent tools`);
440
- for (const agent of config.agentSpecs) {
441
- const toolNames = agent.tools?.map((t) => t.name).join(", ") ?? "none";
442
- parts.push(`- **${agent.key}**: tools: ${toolNames}`);
443
- }
444
- }
445
- if (config.dataViewSpecs?.length) {
446
- parts.push(`
447
- ### Data views`);
448
- for (const dv of config.dataViewSpecs) {
449
- parts.push(`- **${dv.key}**: ${dv.meta.title ?? dv.key}`);
450
- }
451
- }
452
- if (config.formSpecs?.length) {
453
- parts.push(`
454
- ### Forms`);
455
- for (const form of config.formSpecs) {
456
- parts.push(`- **${form.key}**: ${form.meta.title ?? form.key}`);
457
- }
458
- }
459
- if (config.presentationSpecs?.length) {
460
- parts.push(`
461
- ### Presentations`);
462
- for (const pres of config.presentationSpecs) {
463
- parts.push(`- **${pres.key}**: ${pres.meta.title ?? pres.key} (targets: ${pres.targets?.join(", ") ?? "react"})`);
464
- }
465
- }
466
- if (config.operationRefs?.length) {
467
- parts.push(`
468
- ### Operations`);
469
- for (const op of config.operationRefs) {
470
- parts.push(`- **${op.key}@${op.version}**`);
471
- }
472
- }
473
- parts.push(`
474
- Use the available tools to invoke operations, query data views, or propose surface changes when appropriate.`);
475
- return parts.join(`
476
- `);
477
- }
478
-
479
- // src/core/agent-tools-adapter.ts
480
- import { tool as tool2 } from "ai";
481
- import { z as z2 } from "zod";
482
- function getInputSchema(_schema) {
483
- return z2.object({}).passthrough();
484
- }
485
- function agentToolConfigsToToolSet(configs, handlers) {
486
- const result = {};
487
- for (const config of configs) {
488
- const handler = handlers?.[config.name];
489
- const inputSchema = getInputSchema(config.schema);
490
- result[config.name] = tool2({
491
- description: config.description ?? config.name,
492
- inputSchema,
493
- execute: async (input) => {
494
- if (!handler) {
495
- return {
496
- status: "unimplemented",
497
- message: "Wire handler in host",
498
- toolName: config.name
499
- };
500
- }
501
- try {
502
- const output = await Promise.resolve(handler(input));
503
- return typeof output === "string" ? output : output;
504
- } catch (err) {
505
- return {
506
- status: "error",
507
- error: err instanceof Error ? err.message : String(err),
508
- toolName: config.name
509
- };
510
- }
511
- }
512
- });
513
- }
514
- return result;
515
- }
516
-
517
- // src/core/surface-planner-tools.ts
518
- import { tool as tool3 } from "ai";
519
- import { z as z3 } from "zod";
520
- import {
521
- validatePatchProposal
522
- } from "@contractspec/lib.surface-runtime/spec/validate-surface-patch";
523
- import { buildSurfacePatchProposal } from "@contractspec/lib.surface-runtime/runtime/planner-tools";
524
- var VALID_OPS = [
525
- "insert-node",
526
- "replace-node",
527
- "remove-node",
528
- "move-node",
529
- "resize-panel",
530
- "set-layout",
531
- "reveal-field",
532
- "hide-field",
533
- "promote-action",
534
- "set-focus"
535
- ];
536
- var DEFAULT_NODE_KINDS = [
537
- "entity-section",
538
- "entity-card",
539
- "data-view",
540
- "assistant-panel",
541
- "chat-thread",
542
- "action-bar",
543
- "timeline",
544
- "table",
545
- "rich-doc",
546
- "form",
547
- "chart",
548
- "custom-widget"
549
- ];
550
- function collectSlotIdsFromRegion(node) {
551
- const ids = [];
552
- if (node.type === "slot") {
553
- ids.push(node.slotId);
554
- }
555
- if (node.type === "panel-group" || node.type === "stack") {
556
- for (const child of node.children) {
557
- ids.push(...collectSlotIdsFromRegion(child));
558
- }
559
- }
560
- if (node.type === "tabs") {
561
- for (const tab of node.tabs) {
562
- ids.push(...collectSlotIdsFromRegion(tab.child));
563
- }
564
- }
565
- if (node.type === "floating") {
566
- ids.push(node.anchorSlotId);
567
- ids.push(...collectSlotIdsFromRegion(node.child));
568
- }
569
- return ids;
570
- }
571
- function deriveConstraints(plan) {
572
- const slotIds = collectSlotIdsFromRegion(plan.layoutRoot);
573
- const uniqueSlots = [...new Set(slotIds)];
574
- return {
575
- allowedOps: VALID_OPS,
576
- allowedSlots: uniqueSlots.length > 0 ? uniqueSlots : ["assistant", "primary"],
577
- allowedNodeKinds: DEFAULT_NODE_KINDS
578
- };
579
- }
580
- var ProposePatchInputSchema = z3.object({
581
- proposalId: z3.string().describe("Unique proposal identifier"),
582
- ops: z3.array(z3.object({
583
- op: z3.enum([
584
- "insert-node",
585
- "replace-node",
586
- "remove-node",
587
- "move-node",
588
- "resize-panel",
589
- "set-layout",
590
- "reveal-field",
591
- "hide-field",
592
- "promote-action",
593
- "set-focus"
594
- ]),
595
- slotId: z3.string().optional(),
596
- nodeId: z3.string().optional(),
597
- toSlotId: z3.string().optional(),
598
- index: z3.number().optional(),
599
- node: z3.object({
600
- nodeId: z3.string(),
601
- kind: z3.string(),
602
- title: z3.string().optional(),
603
- props: z3.record(z3.string(), z3.unknown()).optional(),
604
- children: z3.array(z3.unknown()).optional()
605
- }).optional(),
606
- persistKey: z3.string().optional(),
607
- sizes: z3.array(z3.number()).optional(),
608
- layoutId: z3.string().optional(),
609
- fieldId: z3.string().optional(),
610
- actionId: z3.string().optional(),
611
- placement: z3.enum(["header", "inline", "context", "assistant"]).optional(),
612
- targetId: z3.string().optional()
613
- }))
614
- });
615
- function createSurfacePlannerTools(config) {
616
- const { plan, constraints, onPatchProposal } = config;
617
- const resolvedConstraints = constraints ?? deriveConstraints(plan);
618
- const proposePatchTool = tool3({
619
- description: "Propose surface patches (layout changes, node insertions, etc.) for user approval. " + "Only use allowed ops, slots, and node kinds from the planner context.",
620
- inputSchema: ProposePatchInputSchema,
523
+ inputSchema: WorkflowExtensionInputSchema,
621
524
  execute: async (input) => {
622
- const ops = input.ops;
525
+ const extension = {
526
+ workflow: input.workflow,
527
+ tenantId: input.tenantId,
528
+ role: input.role,
529
+ priority: input.priority,
530
+ customSteps: input.customSteps,
531
+ hiddenSteps: input.hiddenSteps
532
+ };
533
+ const base = baseByKey.get(input.workflow);
534
+ if (!base) {
535
+ return {
536
+ success: false,
537
+ error: `Base workflow "${input.workflow}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
538
+ extension
539
+ };
540
+ }
623
541
  try {
624
- validatePatchProposal(ops, resolvedConstraints);
625
- const proposal = buildSurfacePatchProposal(input.proposalId, ops);
626
- onPatchProposal?.(proposal);
542
+ validateExtension(extension, base);
627
543
  return {
628
544
  success: true,
629
- proposalId: proposal.proposalId,
630
- opsCount: proposal.ops.length,
631
- message: "Patch proposal validated; awaiting user approval"
545
+ message: "Extension validated successfully",
546
+ extension
632
547
  };
633
548
  } catch (err) {
634
549
  return {
635
550
  success: false,
636
551
  error: err instanceof Error ? err.message : String(err),
637
- proposalId: input.proposalId
552
+ extension
553
+ };
554
+ }
555
+ }
556
+ });
557
+ const composeWorkflowInputSchema = z3.object({
558
+ workflowKey: z3.string().describe("Base workflow meta.key"),
559
+ tenantId: z3.string().optional(),
560
+ role: z3.string().optional(),
561
+ extensions: z3.array(WorkflowExtensionInputSchema).optional().describe("Extensions to register before composing")
562
+ });
563
+ const composeWorkflowTool = tool3({
564
+ description: "Compose a workflow by applying registered extensions to a base workflow. Returns the composed WorkflowSpec.",
565
+ inputSchema: composeWorkflowInputSchema,
566
+ execute: async (input) => {
567
+ const base = baseByKey.get(input.workflowKey);
568
+ if (!base) {
569
+ return {
570
+ success: false,
571
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`
572
+ };
573
+ }
574
+ const comp = composer ?? new WorkflowComposer;
575
+ if (input.extensions?.length) {
576
+ for (const ext of input.extensions) {
577
+ comp.register({
578
+ workflow: ext.workflow,
579
+ tenantId: ext.tenantId,
580
+ role: ext.role,
581
+ priority: ext.priority,
582
+ customSteps: ext.customSteps,
583
+ hiddenSteps: ext.hiddenSteps
584
+ });
585
+ }
586
+ }
587
+ try {
588
+ const composed = comp.compose({
589
+ base,
590
+ tenantId: input.tenantId,
591
+ role: input.role
592
+ });
593
+ return {
594
+ success: true,
595
+ workflow: composed,
596
+ meta: composed.meta,
597
+ stepIds: composed.definition.steps.map((s) => s.id)
598
+ };
599
+ } catch (err) {
600
+ return {
601
+ success: false,
602
+ error: err instanceof Error ? err.message : String(err)
603
+ };
604
+ }
605
+ }
606
+ });
607
+ const generateWorkflowSpecCodeInputSchema = z3.object({
608
+ workflowKey: z3.string().describe("Workflow meta.key"),
609
+ composedSteps: z3.array(z3.object({
610
+ id: z3.string(),
611
+ type: z3.enum(["human", "automation", "decision"]),
612
+ label: z3.string(),
613
+ description: z3.string().optional()
614
+ })).optional().describe("Steps to include; if omitted, uses the base workflow")
615
+ });
616
+ const generateWorkflowSpecCodeTool = tool3({
617
+ description: "Generate TypeScript code for a workflow spec. Use after composing a workflow to output the spec as code the user can save.",
618
+ inputSchema: generateWorkflowSpecCodeInputSchema,
619
+ execute: async (input) => {
620
+ const base = baseByKey.get(input.workflowKey);
621
+ if (!base) {
622
+ return {
623
+ success: false,
624
+ error: `Base workflow "${input.workflowKey}" not found. Available: ${Array.from(baseByKey.keys()).join(", ")}`,
625
+ code: null
638
626
  };
639
627
  }
628
+ const steps = input.composedSteps ?? base.definition.steps;
629
+ const specVarName = toPascalCase((base.meta.key.split(".").pop() ?? "Workflow") + "") + "Workflow";
630
+ const stepsCode = steps.map((s) => ` {
631
+ id: '${s.id}',
632
+ type: '${s.type}',
633
+ label: '${escapeString(s.label)}',${s.description ? `
634
+ description: '${escapeString(s.description)}',` : ""}
635
+ }`).join(`,
636
+ `);
637
+ const meta = base.meta;
638
+ const transitionsJson = JSON.stringify(base.definition.transitions, null, 6);
639
+ const code = `import type { WorkflowSpec } from '@contractspec/lib.contracts-spec/workflow';
640
+
641
+ /**
642
+ * Workflow: ${base.meta.key}
643
+ * Generated via AI chat workflow tools.
644
+ */
645
+ export const ${specVarName}: WorkflowSpec = {
646
+ meta: {
647
+ key: '${base.meta.key}',
648
+ version: '${String(base.meta.version)}',
649
+ title: '${escapeString(meta.title ?? base.meta.key)}',
650
+ description: '${escapeString(meta.description ?? "")}',
651
+ },
652
+ definition: {
653
+ entryStepId: '${base.definition.entryStepId ?? base.definition.steps[0]?.id ?? ""}',
654
+ steps: [
655
+ ${stepsCode}
656
+ ],
657
+ transitions: ${transitionsJson},
658
+ },
659
+ };
660
+ `;
661
+ return {
662
+ success: true,
663
+ code,
664
+ workflowKey: input.workflowKey
665
+ };
640
666
  }
641
667
  });
642
668
  return {
643
- "propose-patch": proposePatchTool
669
+ create_workflow_extension: createWorkflowExtensionTool,
670
+ compose_workflow: composeWorkflowTool,
671
+ generate_workflow_spec_code: generateWorkflowSpecCodeTool
644
672
  };
645
673
  }
646
- function buildPlannerPromptInput(plan) {
647
- const constraints = deriveConstraints(plan);
648
- return {
649
- bundleMeta: {
650
- key: plan.bundleKey,
651
- version: "0.0.0",
652
- title: plan.bundleKey
653
- },
654
- surfaceId: plan.surfaceId,
655
- allowedPatchOps: constraints.allowedOps,
656
- allowedSlots: [...constraints.allowedSlots],
657
- allowedNodeKinds: [...constraints.allowedNodeKinds],
658
- actions: plan.actions.map((a) => ({
659
- actionId: a.actionId,
660
- title: a.title
661
- })),
662
- preferences: {
663
- guidance: "hints",
664
- density: "standard",
665
- dataDepth: "detailed",
666
- control: "standard",
667
- media: "text",
668
- pace: "balanced",
669
- narrative: "top-down"
670
- }
671
- };
674
+ function toPascalCase(value) {
675
+ return value.split(/[-_.]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
676
+ }
677
+ function escapeString(value) {
678
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
672
679
  }
673
680
 
674
681
  // src/core/chat-service.ts
675
- import { compilePlannerPrompt } from "@contractspec/lib.surface-runtime/runtime/planner-prompt";
676
682
  var DEFAULT_SYSTEM_PROMPT = `You are ContractSpec AI, an expert coding assistant specialized in ContractSpec development.
677
683
 
678
684
  Your capabilities:
@@ -1088,9 +1094,6 @@ function createChatService(config) {
1088
1094
  }
1089
1095
 
1090
1096
  // src/presentation/hooks/useChat.tsx
1091
- import {
1092
- createProvider
1093
- } from "@contractspec/lib.ai-providers";
1094
1097
  "use client";
1095
1098
  function toolsToToolSet(defs) {
1096
1099
  const result = {};
@@ -1529,44 +1532,42 @@ function useChat(options = {}) {
1529
1532
  ...hasApprovalTools && { addToolApprovalResponse }
1530
1533
  };
1531
1534
  }
1532
- // src/presentation/hooks/useProviders.tsx
1535
+ // src/presentation/hooks/useConversations.ts
1533
1536
  import * as React2 from "react";
1534
- import {
1535
- getAvailableProviders,
1536
- getModelsForProvider
1537
- } from "@contractspec/lib.ai-providers";
1538
1537
  "use client";
1539
- function useProviders() {
1540
- const [providers, setProviders] = React2.useState([]);
1538
+ function useConversations(options) {
1539
+ const { store, projectId, tags, limit = 50 } = options;
1540
+ const [conversations, setConversations] = React2.useState([]);
1541
1541
  const [isLoading, setIsLoading] = React2.useState(true);
1542
- const loadProviders = React2.useCallback(async () => {
1542
+ const refresh = React2.useCallback(async () => {
1543
1543
  setIsLoading(true);
1544
1544
  try {
1545
- const available = getAvailableProviders();
1546
- const providersWithModels = available.map((p) => ({
1547
- ...p,
1548
- models: getModelsForProvider(p.provider)
1549
- }));
1550
- setProviders(providersWithModels);
1551
- } catch (error) {
1552
- console.error("Failed to load providers:", error);
1545
+ const list = await store.list({
1546
+ status: "active",
1547
+ projectId,
1548
+ tags,
1549
+ limit
1550
+ });
1551
+ setConversations(list);
1553
1552
  } finally {
1554
1553
  setIsLoading(false);
1555
1554
  }
1556
- }, []);
1555
+ }, [store, projectId, tags, limit]);
1557
1556
  React2.useEffect(() => {
1558
- loadProviders();
1559
- }, [loadProviders]);
1560
- const availableProviders = React2.useMemo(() => providers.filter((p) => p.available), [providers]);
1561
- const isAvailable = React2.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
1562
- const getModelsCallback = React2.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
1557
+ refresh();
1558
+ }, [refresh]);
1559
+ const deleteConversation = React2.useCallback(async (id) => {
1560
+ const ok = await store.delete(id);
1561
+ if (ok) {
1562
+ setConversations((prev) => prev.filter((c) => c.id !== id));
1563
+ }
1564
+ return ok;
1565
+ }, [store]);
1563
1566
  return {
1564
- providers,
1565
- availableProviders,
1566
- isAvailable,
1567
- getModels: getModelsCallback,
1567
+ conversations,
1568
1568
  isLoading,
1569
- refresh: loadProviders
1569
+ refresh,
1570
+ deleteConversation
1570
1571
  };
1571
1572
  }
1572
1573
  // src/presentation/hooks/useMessageSelection.ts
@@ -1612,47 +1613,46 @@ function useMessageSelection(messageIds) {
1612
1613
  selectedCount
1613
1614
  };
1614
1615
  }
1615
- // src/presentation/hooks/useConversations.ts
1616
+ // src/presentation/hooks/useProviders.tsx
1617
+ import {
1618
+ getAvailableProviders,
1619
+ getModelsForProvider
1620
+ } from "@contractspec/lib.ai-providers";
1616
1621
  import * as React4 from "react";
1617
1622
  "use client";
1618
- function useConversations(options) {
1619
- const { store, projectId, tags, limit = 50 } = options;
1620
- const [conversations, setConversations] = React4.useState([]);
1623
+ function useProviders() {
1624
+ const [providers, setProviders] = React4.useState([]);
1621
1625
  const [isLoading, setIsLoading] = React4.useState(true);
1622
- const refresh = React4.useCallback(async () => {
1626
+ const loadProviders = React4.useCallback(async () => {
1623
1627
  setIsLoading(true);
1624
1628
  try {
1625
- const list = await store.list({
1626
- status: "active",
1627
- projectId,
1628
- tags,
1629
- limit
1630
- });
1631
- setConversations(list);
1629
+ const available = getAvailableProviders();
1630
+ const providersWithModels = available.map((p) => ({
1631
+ ...p,
1632
+ models: getModelsForProvider(p.provider)
1633
+ }));
1634
+ setProviders(providersWithModels);
1635
+ } catch (error) {
1636
+ console.error("Failed to load providers:", error);
1632
1637
  } finally {
1633
1638
  setIsLoading(false);
1634
1639
  }
1635
- }, [store, projectId, tags, limit]);
1640
+ }, []);
1636
1641
  React4.useEffect(() => {
1637
- refresh();
1638
- }, [refresh]);
1639
- const deleteConversation = React4.useCallback(async (id) => {
1640
- const ok = await store.delete(id);
1641
- if (ok) {
1642
- setConversations((prev) => prev.filter((c) => c.id !== id));
1643
- }
1644
- return ok;
1645
- }, [store]);
1642
+ loadProviders();
1643
+ }, [loadProviders]);
1644
+ const availableProviders = React4.useMemo(() => providers.filter((p) => p.available), [providers]);
1645
+ const isAvailable = React4.useCallback((provider) => providers.some((p) => p.provider === provider && p.available), [providers]);
1646
+ const getModelsCallback = React4.useCallback((provider) => providers.find((p) => p.provider === provider)?.models ?? [], [providers]);
1646
1647
  return {
1647
- conversations,
1648
+ providers,
1649
+ availableProviders,
1650
+ isAvailable,
1651
+ getModels: getModelsCallback,
1648
1652
  isLoading,
1649
- refresh,
1650
- deleteConversation
1653
+ refresh: loadProviders
1651
1654
  };
1652
1655
  }
1653
-
1654
- // src/presentation/hooks/index.ts
1655
- import { useCompletion } from "@ai-sdk/react";
1656
1656
  export {
1657
1657
  useProviders,
1658
1658
  useMessageSelection,