@m6d/cortex-server 2.0.1 → 2.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 (33) hide show
  1. package/README.md +10 -0
  2. package/contracts/interactive.ts +53 -0
  3. package/contracts/runtime.ts +22 -0
  4. package/contracts/wire.ts +32 -0
  5. package/dist/contracts/interactive.d.ts +39 -0
  6. package/dist/contracts/runtime.d.ts +80 -0
  7. package/dist/contracts/wire.d.ts +30 -0
  8. package/dist/src/lib/adapters/database/index.d.ts +7 -0
  9. package/dist/src/lib/adapters/database/message-content.d.ts +2 -0
  10. package/dist/src/lib/adapters/database/mssql/index.d.ts +1 -0
  11. package/dist/src/lib/adapters/database/mssql/messages.d.ts +1 -0
  12. package/dist/src/lib/adapters/database/postgres/index.d.ts +1 -0
  13. package/dist/src/lib/adapters/database/postgres/messages.d.ts +1 -0
  14. package/dist/src/lib/ai/cc-runtime.d.ts +2 -1
  15. package/dist/src/lib/ai/interactive.d.ts +62 -0
  16. package/dist/src/lib/ai/turn-tools.d.ts +14 -0
  17. package/dist/src/lib/cc/client.d.ts +24 -0
  18. package/dist/src/lib/cc/registry.d.ts +14 -1
  19. package/dist/src/lib/cc/types.d.ts +1 -1
  20. package/package.json +5 -2
  21. package/src/lib/adapters/database/index.ts +12 -0
  22. package/src/lib/adapters/database/mssql/messages.ts +28 -1
  23. package/src/lib/adapters/database/postgres/messages.ts +22 -1
  24. package/src/lib/ai/cc-runtime.ts +16 -1
  25. package/src/lib/ai/index.ts +34 -5
  26. package/src/lib/ai/interactive.ts +364 -0
  27. package/src/lib/ai/tools/search-tools.tool.ts +6 -2
  28. package/src/lib/ai/turn-tools.ts +23 -0
  29. package/src/lib/cc/client.ts +5 -1
  30. package/src/lib/cc/format.ts +5 -2
  31. package/src/lib/cc/registry.ts +16 -2
  32. package/src/lib/cc/types.ts +1 -0
  33. package/src/lib/routes/chat.ts +23 -0
package/README.md CHANGED
@@ -64,6 +64,16 @@ Only `database`, `model`, and `agents` are required for a minimal setup. `storag
64
64
 
65
65
  Each key in `agents` becomes a route prefix (e.g. `assistant` → `/agents/assistant/...`). Agents can define per-agent `systemPrompt`, `tools`, `backendFetch`, `loadSessionData`, `resolveRequestContext`, and lifecycle hooks (`onToolCall`, `onStreamFinish`).
66
66
 
67
+ ## Interactive tools
68
+
69
+ Control-Center tools published with kind "interactive" are declared to the
70
+ model as client-executed tools: the call streams to the widget, the run parks,
71
+ and `POST /chat/:chatId/tools/:toolCallId/initiate` hands the widget its embed
72
+ payload (never the model). When the widget answers, the server re-establishes
73
+ the result through the tool's verify endpoint and applies the tool's result
74
+ delivery masking before the model reads it. See
75
+ `apps/cortex-cc/docs/interactive-tools.md` for authoring them.
76
+
67
77
  ## Requirements
68
78
 
69
79
  - Bun >= 1.0.0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The embed ↔ widget handshake for interactive tools.
3
+ *
4
+ * A page embedded by the chat widget (a hosted checkout, signing page, …)
5
+ * reports its outcome with:
6
+ *
7
+ * window.parent.postMessage(
8
+ * { type: "cortex:interactive", status: "completed", reference: "session_123" },
9
+ * "*",
10
+ * );
11
+ *
12
+ * The widget accepts the message only when the event's origin matches the
13
+ * tool's configured `embedOrigin`, and treats the payload as untrusted — for
14
+ * tools with a verify endpoint the server re-checks the reference before the
15
+ * agent sees a result.
16
+ *
17
+ * Like `wire.ts`, this file is compiled into the client SDKs and must stay
18
+ * free of runtime dependencies. It is also the spec any future SDK (Flutter)
19
+ * reimplements against.
20
+ */
21
+
22
+ export const INTERACTIVE_MESSAGE_TYPE = "cortex:interactive";
23
+
24
+ export const INTERACTIVE_STATUSES = ["completed", "cancelled", "failed"] as const;
25
+
26
+ export type InteractiveStatus = (typeof INTERACTIVE_STATUSES)[number];
27
+
28
+ export type InteractiveHandshake = {
29
+ type: typeof INTERACTIVE_MESSAGE_TYPE;
30
+ status: InteractiveStatus;
31
+ /** Opaque id the tool's verify endpoint resolves (session id, envelope id, …). */
32
+ reference?: string;
33
+ };
34
+
35
+ /**
36
+ * Parse a `message` event into a handshake, or null when the origin doesn't
37
+ * match the tool's `embedOrigin` or the payload isn't a well-formed handshake.
38
+ * Call from the widget's `message` listener with `event.data` / `event.origin`.
39
+ */
40
+ export function parseInteractiveHandshake(data: unknown, origin: string, embedOrigin: string) {
41
+ if (origin !== embedOrigin) return null;
42
+ if (typeof data !== "object" || data === null) return null;
43
+ const message = data as Record<string, unknown>;
44
+ if (message["type"] !== INTERACTIVE_MESSAGE_TYPE) return null;
45
+ const status = INTERACTIVE_STATUSES.find((known) => known === message["status"]);
46
+ if (!status) return null;
47
+ const reference = message["reference"];
48
+ return {
49
+ type: INTERACTIVE_MESSAGE_TYPE,
50
+ status,
51
+ ...(typeof reference === "string" ? { reference } : {}),
52
+ } satisfies InteractiveHandshake;
53
+ }
@@ -89,6 +89,19 @@ export const knowledgeChunkSchema = z.object({
89
89
  }),
90
90
  });
91
91
 
92
+ /**
93
+ * Runtime contract §5.4: present on signatures of `interactive` tools — flows
94
+ * the end user completes in an embedded surface inside the chat widget. The
95
+ * tool's endpoint fields act as the *initiate* call (returns the embed URL);
96
+ * `hasVerify` marks a second, server-trusted call that settles the result.
97
+ */
98
+ export const toolInteractionSchema = z.object({
99
+ surface: z.enum(["inline", "modal"]),
100
+ embedOrigin: z.url(),
101
+ hasVerify: z.boolean(),
102
+ resultDelivery: z.enum(["agent", "endpoint", "both"]),
103
+ });
104
+
92
105
  export const toolSignatureSchema = z.object({
93
106
  toolId: z.uuid(),
94
107
  name: z.string().regex(TOOL_NAME_PATTERN),
@@ -98,6 +111,11 @@ export const toolSignatureSchema = z.object({
98
111
  signature: z.string(),
99
112
  score: z.number().nullable(),
100
113
  pinned: z.boolean(),
114
+ interaction: toolInteractionSchema.optional(),
115
+ /** Interactive tools only: the published input JSON Schema, verbatim.
116
+ * Client-executed declarations need a real schema — the rendered
117
+ * `signature` text alone is not enough for function calling. */
118
+ inputSchema: z.record(z.string(), z.unknown()).optional(),
101
119
  });
102
120
 
103
121
  export const serviceCardSchema = z.object({
@@ -194,6 +212,9 @@ export const searchKnowledgeResponseSchema = z.object({
194
212
 
195
213
  export const executeRequestSchema = z.object({
196
214
  input: z.record(z.string(), z.unknown()).default({}),
215
+ // Interactive tools only: absent | "initiate" targets the tool's endpoint
216
+ // fields, "verify" targets its verify endpoint config.
217
+ phase: z.enum(["initiate", "verify"]).optional(),
197
218
  context: z
198
219
  .object({
199
220
  threadId: z.string().max(128).optional(),
@@ -236,6 +257,7 @@ export const runtimeErrorSchema = z.object({
236
257
  }),
237
258
  });
238
259
 
260
+ export type ToolInteraction = z.infer<typeof toolInteractionSchema>;
239
261
  export type RuntimeAgentConfig = z.infer<typeof runtimeAgentConfigSchema>;
240
262
  export type ResolveRequest = z.input<typeof resolveRequestSchema>;
241
263
  export type ResolveResponse = z.infer<typeof resolveResponseSchema>;
package/contracts/wire.ts CHANGED
@@ -70,12 +70,29 @@ export type TokenUsage = {
70
70
  total: number;
71
71
  };
72
72
 
73
+ /**
74
+ * How to launch one interactive tool: stamped by the server, per tool name, on
75
+ * the metadata of an assistant message that may carry its pending call. Kept in
76
+ * the message so the binding survives server restarts while a run is parked.
77
+ */
78
+ export type InteractiveToolBinding = {
79
+ toolId: string;
80
+ surface: "inline" | "modal";
81
+ embedOrigin: string;
82
+ hasVerify: boolean;
83
+ resultDelivery: "agent" | "endpoint" | "both";
84
+ };
85
+
73
86
  export type MessageMetadata = {
74
87
  modelId?: string;
75
88
  providerMetadata?: unknown;
76
89
  isAborted?: boolean;
77
90
  tokenUsage?: TokenUsage;
78
91
  attachments?: AttachmentSummary[];
92
+ interactiveTools?: Record<string, InteractiveToolBinding>;
93
+ /** Per tool call: the trusted reference its initiate call returned. A
94
+ * verified completion must match it — the browser's word is never enough. */
95
+ interactiveReferences?: Record<string, string>;
79
96
  };
80
97
 
81
98
  /**
@@ -93,6 +110,21 @@ export type CortexMessage<TPart = unknown> = {
93
110
  metadata?: MessageMetadata;
94
111
  };
95
112
 
113
+ /**
114
+ * Response of `POST /chat/:chatId/tools/:toolCallId/initiate`. `interactive:
115
+ * false` means the pending call is not an interactive CC tool — the widget
116
+ * falls back to its default rendering for the call. The payload is for the
117
+ * widget only; it never reaches the LLM.
118
+ */
119
+ export type InteractiveInitiateResult =
120
+ | { interactive: false }
121
+ | {
122
+ interactive: true;
123
+ embedUrl: string;
124
+ surface: "inline" | "modal";
125
+ embedOrigin: string;
126
+ };
127
+
96
128
  export type ThreadCreatedEvent = {
97
129
  type: "thread:created";
98
130
  payload: { thread: ThreadSummary };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The embed ↔ widget handshake for interactive tools.
3
+ *
4
+ * A page embedded by the chat widget (a hosted checkout, signing page, …)
5
+ * reports its outcome with:
6
+ *
7
+ * window.parent.postMessage(
8
+ * { type: "cortex:interactive", status: "completed", reference: "session_123" },
9
+ * "*",
10
+ * );
11
+ *
12
+ * The widget accepts the message only when the event's origin matches the
13
+ * tool's configured `embedOrigin`, and treats the payload as untrusted — for
14
+ * tools with a verify endpoint the server re-checks the reference before the
15
+ * agent sees a result.
16
+ *
17
+ * Like `wire.ts`, this file is compiled into the client SDKs and must stay
18
+ * free of runtime dependencies. It is also the spec any future SDK (Flutter)
19
+ * reimplements against.
20
+ */
21
+ export declare const INTERACTIVE_MESSAGE_TYPE = "cortex:interactive";
22
+ export declare const INTERACTIVE_STATUSES: readonly ["completed", "cancelled", "failed"];
23
+ export type InteractiveStatus = (typeof INTERACTIVE_STATUSES)[number];
24
+ export type InteractiveHandshake = {
25
+ type: typeof INTERACTIVE_MESSAGE_TYPE;
26
+ status: InteractiveStatus;
27
+ /** Opaque id the tool's verify endpoint resolves (session id, envelope id, …). */
28
+ reference?: string;
29
+ };
30
+ /**
31
+ * Parse a `message` event into a handshake, or null when the origin doesn't
32
+ * match the tool's `embedOrigin` or the payload isn't a well-formed handshake.
33
+ * Call from the widget's `message` listener with `event.data` / `event.origin`.
34
+ */
35
+ export declare function parseInteractiveHandshake(data: unknown, origin: string, embedOrigin: string): {
36
+ reference?: string | undefined;
37
+ type: "cortex:interactive";
38
+ status: "failed" | "completed" | "cancelled";
39
+ } | null;
@@ -60,6 +60,25 @@ export declare const knowledgeChunkSchema: z.ZodObject<{
60
60
  service: z.ZodNullable<z.ZodString>;
61
61
  }, z.core.$strip>;
62
62
  }, z.core.$strip>;
63
+ /**
64
+ * Runtime contract §5.4: present on signatures of `interactive` tools — flows
65
+ * the end user completes in an embedded surface inside the chat widget. The
66
+ * tool's endpoint fields act as the *initiate* call (returns the embed URL);
67
+ * `hasVerify` marks a second, server-trusted call that settles the result.
68
+ */
69
+ export declare const toolInteractionSchema: z.ZodObject<{
70
+ surface: z.ZodEnum<{
71
+ inline: "inline";
72
+ modal: "modal";
73
+ }>;
74
+ embedOrigin: z.ZodURL;
75
+ hasVerify: z.ZodBoolean;
76
+ resultDelivery: z.ZodEnum<{
77
+ agent: "agent";
78
+ endpoint: "endpoint";
79
+ both: "both";
80
+ }>;
81
+ }, z.core.$strip>;
63
82
  export declare const toolSignatureSchema: z.ZodObject<{
64
83
  toolId: z.ZodUUID;
65
84
  name: z.ZodString;
@@ -69,6 +88,20 @@ export declare const toolSignatureSchema: z.ZodObject<{
69
88
  signature: z.ZodString;
70
89
  score: z.ZodNullable<z.ZodNumber>;
71
90
  pinned: z.ZodBoolean;
91
+ interaction: z.ZodOptional<z.ZodObject<{
92
+ surface: z.ZodEnum<{
93
+ inline: "inline";
94
+ modal: "modal";
95
+ }>;
96
+ embedOrigin: z.ZodURL;
97
+ hasVerify: z.ZodBoolean;
98
+ resultDelivery: z.ZodEnum<{
99
+ agent: "agent";
100
+ endpoint: "endpoint";
101
+ both: "both";
102
+ }>;
103
+ }, z.core.$strip>>;
104
+ inputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
72
105
  }, z.core.$strip>;
73
106
  export declare const serviceCardSchema: z.ZodObject<{
74
107
  serviceId: z.ZodUUID;
@@ -152,6 +185,20 @@ export declare const resolveResponseSchema: z.ZodObject<{
152
185
  signature: z.ZodString;
153
186
  score: z.ZodNullable<z.ZodNumber>;
154
187
  pinned: z.ZodBoolean;
188
+ interaction: z.ZodOptional<z.ZodObject<{
189
+ surface: z.ZodEnum<{
190
+ inline: "inline";
191
+ modal: "modal";
192
+ }>;
193
+ embedOrigin: z.ZodURL;
194
+ hasVerify: z.ZodBoolean;
195
+ resultDelivery: z.ZodEnum<{
196
+ agent: "agent";
197
+ endpoint: "endpoint";
198
+ both: "both";
199
+ }>;
200
+ }, z.core.$strip>>;
201
+ inputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
155
202
  }, z.core.$strip>>;
156
203
  services: z.ZodArray<z.ZodObject<{
157
204
  serviceId: z.ZodUUID;
@@ -191,6 +238,20 @@ export declare const searchToolsResponseSchema: z.ZodObject<{
191
238
  signature: z.ZodString;
192
239
  score: z.ZodNullable<z.ZodNumber>;
193
240
  pinned: z.ZodBoolean;
241
+ interaction: z.ZodOptional<z.ZodObject<{
242
+ surface: z.ZodEnum<{
243
+ inline: "inline";
244
+ modal: "modal";
245
+ }>;
246
+ embedOrigin: z.ZodURL;
247
+ hasVerify: z.ZodBoolean;
248
+ resultDelivery: z.ZodEnum<{
249
+ agent: "agent";
250
+ endpoint: "endpoint";
251
+ both: "both";
252
+ }>;
253
+ }, z.core.$strip>>;
254
+ inputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
194
255
  }, z.core.$strip>>;
195
256
  sharedShapes: z.ZodArray<z.ZodObject<{
196
257
  ref: z.ZodString;
@@ -215,6 +276,20 @@ export declare const searchServicesResponseSchema: z.ZodObject<{
215
276
  signature: z.ZodString;
216
277
  score: z.ZodNullable<z.ZodNumber>;
217
278
  pinned: z.ZodBoolean;
279
+ interaction: z.ZodOptional<z.ZodObject<{
280
+ surface: z.ZodEnum<{
281
+ inline: "inline";
282
+ modal: "modal";
283
+ }>;
284
+ embedOrigin: z.ZodURL;
285
+ hasVerify: z.ZodBoolean;
286
+ resultDelivery: z.ZodEnum<{
287
+ agent: "agent";
288
+ endpoint: "endpoint";
289
+ both: "both";
290
+ }>;
291
+ }, z.core.$strip>>;
292
+ inputSchema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
218
293
  }, z.core.$strip>>;
219
294
  sharedShapes: z.ZodArray<z.ZodObject<{
220
295
  ref: z.ZodString;
@@ -239,6 +314,10 @@ export declare const searchKnowledgeResponseSchema: z.ZodObject<{
239
314
  }, z.core.$strip>;
240
315
  export declare const executeRequestSchema: z.ZodObject<{
241
316
  input: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
317
+ phase: z.ZodOptional<z.ZodEnum<{
318
+ verify: "verify";
319
+ initiate: "initiate";
320
+ }>>;
242
321
  context: z.ZodOptional<z.ZodObject<{
243
322
  threadId: z.ZodOptional<z.ZodString>;
244
323
  userId: z.ZodOptional<z.ZodString>;
@@ -302,6 +381,7 @@ export declare const runtimeErrorSchema: z.ZodObject<{
302
381
  upstreamStatus: z.ZodOptional<z.ZodNumber>;
303
382
  }, z.core.$strip>;
304
383
  }, z.core.$strip>;
384
+ export type ToolInteraction = z.infer<typeof toolInteractionSchema>;
305
385
  export type RuntimeAgentConfig = z.infer<typeof runtimeAgentConfigSchema>;
306
386
  export type ResolveRequest = z.input<typeof resolveRequestSchema>;
307
387
  export type ResolveResponse = z.infer<typeof resolveResponseSchema>;
@@ -50,12 +50,28 @@ export type TokenUsage = {
50
50
  };
51
51
  total: number;
52
52
  };
53
+ /**
54
+ * How to launch one interactive tool: stamped by the server, per tool name, on
55
+ * the metadata of an assistant message that may carry its pending call. Kept in
56
+ * the message so the binding survives server restarts while a run is parked.
57
+ */
58
+ export type InteractiveToolBinding = {
59
+ toolId: string;
60
+ surface: "inline" | "modal";
61
+ embedOrigin: string;
62
+ hasVerify: boolean;
63
+ resultDelivery: "agent" | "endpoint" | "both";
64
+ };
53
65
  export type MessageMetadata = {
54
66
  modelId?: string;
55
67
  providerMetadata?: unknown;
56
68
  isAborted?: boolean;
57
69
  tokenUsage?: TokenUsage;
58
70
  attachments?: AttachmentSummary[];
71
+ interactiveTools?: Record<string, InteractiveToolBinding>;
72
+ /** Per tool call: the trusted reference its initiate call returned. A
73
+ * verified completion must match it — the browser's word is never enough. */
74
+ interactiveReferences?: Record<string, string>;
59
75
  };
60
76
  /**
61
77
  * A stored message as it is persisted and served. TanStack AI's `UIMessage`
@@ -71,6 +87,20 @@ export type CortexMessage<TPart = unknown> = {
71
87
  parts: TPart[];
72
88
  metadata?: MessageMetadata;
73
89
  };
90
+ /**
91
+ * Response of `POST /chat/:chatId/tools/:toolCallId/initiate`. `interactive:
92
+ * false` means the pending call is not an interactive CC tool — the widget
93
+ * falls back to its default rendering for the call. The payload is for the
94
+ * widget only; it never reaches the LLM.
95
+ */
96
+ export type InteractiveInitiateResult = {
97
+ interactive: false;
98
+ } | {
99
+ interactive: true;
100
+ embedUrl: string;
101
+ surface: "inline" | "modal";
102
+ embedOrigin: string;
103
+ };
74
104
  export type ThreadCreatedEvent = {
75
105
  type: "thread:created";
76
106
  payload: {
@@ -34,6 +34,13 @@ export type DatabaseAdapter = {
34
34
  upsert(threadId: string, messages: ChatMessage[], options?: {
35
35
  replaceAttachments?: boolean;
36
36
  }): Promise<void>;
37
+ /**
38
+ * Atomically merges one anchored interactive reference into the
39
+ * message's metadata as a single-statement JSON merge — safe under
40
+ * concurrent writers across replicas, unlike a read-modify-write of
41
+ * the whole message.
42
+ */
43
+ mergeInteractiveReference(threadId: string, messageId: string, toolCallId: string, reference: string): Promise<void>;
37
44
  };
38
45
  llmRequests: {
39
46
  insert(requests: {
@@ -16,6 +16,8 @@ export declare function withOwnedAttachments(messages: ChatMessage[], replaceAtt
16
16
  providerMetadata?: unknown;
17
17
  isAborted?: boolean;
18
18
  tokenUsage?: import("../../types").TokenUsage;
19
+ interactiveTools?: Record<string, import("../..").InteractiveToolBinding>;
20
+ interactiveReferences?: Record<string, string>;
19
21
  };
20
22
  id: string;
21
23
  role: "system" | "user" | "assistant";
@@ -76,6 +76,7 @@ export declare function createMssqlAdapter(connectionString: string, storage?: S
76
76
  upsert(threadId: string, messagesToUpsert: import("../../../types").ChatMessage[], options?: {
77
77
  replaceAttachments?: boolean;
78
78
  }): Promise<void>;
79
+ mergeInteractiveReference(threadId: string, messageId: string, toolCallId: string, reference: string): Promise<void>;
79
80
  };
80
81
  llmRequests: {
81
82
  insert(requests: {
@@ -34,4 +34,5 @@ export declare function createMessagesRepository(db: MssqlDb): {
34
34
  upsert(threadId: string, messagesToUpsert: ChatMessage[], options?: {
35
35
  replaceAttachments?: boolean;
36
36
  }): Promise<void>;
37
+ mergeInteractiveReference(threadId: string, messageId: string, toolCallId: string, reference: string): Promise<void>;
37
38
  };
@@ -76,6 +76,7 @@ export declare function createPostgresAdapter(connectionString: string, storage?
76
76
  upsert(threadId: string, messagesToUpsert: import("../../../types").ChatMessage[], options?: {
77
77
  replaceAttachments?: boolean;
78
78
  }): Promise<void>;
79
+ mergeInteractiveReference(threadId: string, messageId: string, toolCallId: string, reference: string): Promise<void>;
79
80
  };
80
81
  llmRequests: {
81
82
  insert(requests: {
@@ -34,4 +34,5 @@ export declare function createMessagesRepository(db: PostgresDb): {
34
34
  upsert(threadId: string, messagesToUpsert: ChatMessage[], options?: {
35
35
  replaceAttachments?: boolean;
36
36
  }): Promise<void>;
37
+ mergeInteractiveReference(threadId: string, messageId: string, toolCallId: string, reference: string): Promise<void>;
37
38
  };
@@ -1,7 +1,7 @@
1
1
  import type { ResolvedCortexAgentConfig } from "../config";
2
2
  import type { Thread } from "../types";
3
3
  import type { ControlCenterClient } from "../cc/client";
4
- import type { CcToolRegistry } from "../cc/registry";
4
+ import type { CcInteractiveTool, CcToolRegistry } from "../cc/registry";
5
5
  import type { ResolveResponse, RuntimeAgentConfig } from "../cc/types";
6
6
  type CcRuntimeOptions = {
7
7
  ccClient: ControlCenterClient | null;
@@ -48,6 +48,7 @@ export declare function createCcRuntime(options: CcRuntimeOptions): {
48
48
  publishedAt: string;
49
49
  };
50
50
  registry: CcToolRegistry;
51
+ interactiveTools: Map<string, CcInteractiveTool>;
51
52
  threadId: string;
52
53
  turnKey: string;
53
54
  userId: string;
@@ -0,0 +1,62 @@
1
+ import type { ResolvedCortexAgentConfig } from "../config";
2
+ import type { ChatMessage, Thread } from "../types";
3
+ import { ControlCenterClient } from "../cc/client";
4
+ import type { CcRuntime } from "../cc/registry";
5
+ /**
6
+ * The wire-shape bindings stamped on the turn's final assistant message, so a
7
+ * parked interactive call can be initiated and verified after any restart.
8
+ * Only names that actually won declaration merging are stamped — a call to a
9
+ * colliding consumer-owned tool must never be treated as interactive. Undefined
10
+ * when nothing qualifies, keeping metadata lean.
11
+ */
12
+ export declare function interactiveToolBindings(cc: CcRuntime | undefined, takenNames: ReadonlySet<string>): {
13
+ [k: string]: {
14
+ surface: "inline" | "modal";
15
+ embedOrigin: string;
16
+ hasVerify: boolean;
17
+ resultDelivery: "agent" | "endpoint" | "both";
18
+ toolId: string;
19
+ };
20
+ } | undefined;
21
+ /** Null when the agent has no Control Center — every caller treats that as "skip". */
22
+ export declare function createCcClient(config: ResolvedCortexAgentConfig): ControlCenterClient | null;
23
+ /**
24
+ * `POST /chat/:chatId/tools/:toolCallId/initiate` — runs the interactive
25
+ * tool's initiate call (the tool's own endpoint) and hands the widget its
26
+ * embed payload. The model never sees this payload. The idempotency key is
27
+ * pinned to the tool call, so a reload mid-flow reuses the created session
28
+ * instead of opening a second one.
29
+ */
30
+ export declare function initiateInteractiveTool(options: {
31
+ config: ResolvedCortexAgentConfig;
32
+ thread: Thread;
33
+ userId: string;
34
+ token: string;
35
+ toolCallId: string;
36
+ }): Promise<{
37
+ interactive: false;
38
+ embedUrl?: undefined;
39
+ surface?: undefined;
40
+ embedOrigin?: undefined;
41
+ } | {
42
+ interactive: true;
43
+ embedUrl: string;
44
+ surface: "inline" | "modal";
45
+ embedOrigin: string;
46
+ }>;
47
+ /**
48
+ * The trust boundary for interactive results: whatever the client submitted
49
+ * for a bound tool call is replaced wholesale — cancelled/failed pass through
50
+ * normalized, completed is re-established by the tool's verify call when it
51
+ * has one, and result delivery masking is applied before the model reads it.
52
+ * Runs before the answering turn is persisted, so both the stored transcript
53
+ * and the model context carry only settled results.
54
+ */
55
+ export declare function settleInteractiveToolResults(options: {
56
+ messages: ChatMessage[];
57
+ thread: Thread;
58
+ userId: string;
59
+ token: string;
60
+ config: ResolvedCortexAgentConfig;
61
+ ccClient: ControlCenterClient | null;
62
+ }): Promise<void>;
@@ -35,5 +35,19 @@ export declare function createToolInstrumentation(options: TurnToolsOptions & {
35
35
  onBeforeToolCall(_ctx: import("@tanstack/ai").ChatMiddlewareContext<unknown>, { toolName, toolCallId, args }: import("@tanstack/ai").ToolCallHookContext): void;
36
36
  onAfterToolCall(_ctx: import("@tanstack/ai").ChatMiddlewareContext<unknown>, { toolName, toolCallId }: import("@tanstack/ai").AfterToolCallInfo): void;
37
37
  } | undefined;
38
+ /**
39
+ * Interactive CC tools are declared like the request's client tools: no
40
+ * executor, so the call streams to the widget as a tool-call part and the run
41
+ * parks until the widget answers. The declaration carries the tool's published
42
+ * input schema — function-calling models ignore schemas described only in
43
+ * prose — with a permissive object as the fallback for older Control Centers.
44
+ * Names the consumer already claimed are skipped: those calls belong to the
45
+ * consumer's tool, and must neither be declared nor stamped as interactive.
46
+ */
47
+ export declare function interactiveToolDeclarations(cc: CcRuntime | undefined, takenNames: ReadonlySet<string>): {
48
+ name: string;
49
+ description: string;
50
+ parameters: Record<string, unknown>;
51
+ }[];
38
52
  export declare function hasDefaultAttachmentInterceptor(config: ResolvedCortexAgentConfig): boolean;
39
53
  export {};
@@ -8,6 +8,9 @@ export type ExecuteOptions = {
8
8
  turnKey: string;
9
9
  stepIndex: number;
10
10
  callIndex: number;
11
+ /** Overrides the composed key — used where stability must outlive the turn
12
+ * counters, e.g. one interactive initiate per tool call across reloads. */
13
+ idempotencyKey?: string;
11
14
  timeoutMs?: number;
12
15
  abortSignal?: AbortSignal;
13
16
  };
@@ -74,6 +77,13 @@ export declare class ControlCenterClient {
74
77
  signature: string;
75
78
  score: number | null;
76
79
  pinned: boolean;
80
+ interaction?: {
81
+ surface: "inline" | "modal";
82
+ embedOrigin: string;
83
+ hasVerify: boolean;
84
+ resultDelivery: "agent" | "endpoint" | "both";
85
+ } | undefined;
86
+ inputSchema?: Record<string, unknown> | undefined;
77
87
  }[];
78
88
  services: {
79
89
  serviceId: string;
@@ -103,6 +113,13 @@ export declare class ControlCenterClient {
103
113
  signature: string;
104
114
  score: number | null;
105
115
  pinned: boolean;
116
+ interaction?: {
117
+ surface: "inline" | "modal";
118
+ embedOrigin: string;
119
+ hasVerify: boolean;
120
+ resultDelivery: "agent" | "endpoint" | "both";
121
+ } | undefined;
122
+ inputSchema?: Record<string, unknown> | undefined;
106
123
  }[];
107
124
  sharedShapes: {
108
125
  ref: string;
@@ -127,6 +144,13 @@ export declare class ControlCenterClient {
127
144
  signature: string;
128
145
  score: number | null;
129
146
  pinned: boolean;
147
+ interaction?: {
148
+ surface: "inline" | "modal";
149
+ embedOrigin: string;
150
+ hasVerify: boolean;
151
+ resultDelivery: "agent" | "endpoint" | "both";
152
+ } | undefined;
153
+ inputSchema?: Record<string, unknown> | undefined;
130
154
  }[];
131
155
  sharedShapes: {
132
156
  ref: string;
@@ -1,9 +1,20 @@
1
1
  import type { ControlCenterClient } from "./client";
2
- import type { RuntimeAgentConfig } from "./types";
2
+ import type { RuntimeAgentConfig, ToolInteraction } from "./types";
3
3
  export type CcToolBinding = {
4
4
  toolId: string;
5
5
  readOnly: boolean;
6
6
  };
7
+ /**
8
+ * An interactive tool resolved for this turn. Declared to the model as a real
9
+ * client-executed tool — never a sandbox binding — so its call streams to the
10
+ * widget as a tool-call part and parks the run until the flow settles.
11
+ */
12
+ export type CcInteractiveTool = {
13
+ toolId: string;
14
+ signature: string;
15
+ interaction: ToolInteraction;
16
+ inputSchema?: Record<string, unknown>;
17
+ };
7
18
  /**
8
19
  * Turn-scoped and mutable: seeded from /resolve, extended by searchTools
9
20
  * mid-turn so a tool discovered at step 3 is callable at step 4.
@@ -13,6 +24,7 @@ export declare function registerCcTools(registry: CcToolRegistry, tools: {
13
24
  name: string;
14
25
  toolId: string;
15
26
  readOnly: boolean;
27
+ interaction?: ToolInteraction;
16
28
  }[]): void;
17
29
  export declare function recordRecentCcTool(threadId: string, name: string): void;
18
30
  export declare function getRecentCcTools(threadId: string): string[];
@@ -22,6 +34,7 @@ export type CcRuntime = {
22
34
  agentId: string;
23
35
  config: RuntimeAgentConfig;
24
36
  registry: CcToolRegistry;
37
+ interactiveTools: Map<string, CcInteractiveTool>;
25
38
  threadId: string;
26
39
  turnKey: string;
27
40
  userId: string;
@@ -1,5 +1,5 @@
1
1
  import z from "zod";
2
- export { AGENT_SLUG_PATTERN, executeResponseSchema, resolveResponseSchema, runtimeAgentConfigSchema, searchKnowledgeResponseSchema, searchServicesResponseSchema, searchToolsResponseSchema, type ExecuteRequest, type ResolveRequest, type ResolveResponse, type RuntimeAgentConfig, type SearchRequest, } from "../../../contracts/runtime";
2
+ export { AGENT_SLUG_PATTERN, executeResponseSchema, resolveResponseSchema, runtimeAgentConfigSchema, searchKnowledgeResponseSchema, searchServicesResponseSchema, searchToolsResponseSchema, type ExecuteRequest, type ResolveRequest, type ResolveResponse, type RuntimeAgentConfig, type SearchRequest, type ToolInteraction, } from "../../../contracts/runtime";
3
3
  /**
4
4
  * The contract's error envelope, hardened for this consumer: a newer runtime may
5
5
  * answer with an error kind this build has never heard of, which must degrade to a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-server",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Reusable AI agent chat server library for Hono + Bun",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,5 +74,8 @@
74
74
  },
75
75
  "publishConfig": {
76
76
  "access": "public"
77
- }
77
+ },
78
+ "releaseWatchPaths": [
79
+ "internal/contracts"
80
+ ]
78
81
  }