@m6d/cortex-cli 1.0.0 → 1.2.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.
- package/package.json +5 -2
- package/src/contracts/interactive.ts +53 -0
- package/src/contracts/rich-text.ts +207 -0
- package/src/contracts/runtime.ts +58 -1
- package/src/contracts/wire.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m6d/cortex-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Scaffold and operate Cortex servers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,5 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"releaseWatchPaths": [
|
|
38
|
+
"internal/contracts"
|
|
39
|
+
]
|
|
37
40
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The grammar and rendering pipeline for user-authored rich text that reaches
|
|
3
|
+
* the LLM (agent prompts, service descriptions). Both sides of the wire share
|
|
4
|
+
* it: the console edits and validates these strings, the console's runtime API
|
|
5
|
+
* pre-renders them (mentions + heading demotion), and `@m6d/cortex-server`
|
|
6
|
+
* applies per-request variable values. Two token grammars live here:
|
|
7
|
+
*
|
|
8
|
+
* - Tool mentions `@[name](uuid)` — the uuid is authoritative; the inline name
|
|
9
|
+
* is only a display cache refreshed on save ({@link normalizeMentions}) and
|
|
10
|
+
* resolved live when text is rendered for the LLM ({@link renderMentions}).
|
|
11
|
+
* - Variables `{{name}}` — validated against a known list at authoring time
|
|
12
|
+
* ({@link analyzeVariables}) and substituted per request on the server
|
|
13
|
+
* ({@link substituteVariables}).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** One mention token, unanchored and flagless — the editor's tokenizer anchors it, the helpers below add `g`. */
|
|
17
|
+
export const MENTION_PATTERN_SOURCE = String.raw`@\[([^\]\n]*)\]\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)`;
|
|
18
|
+
|
|
19
|
+
const MENTION_PATTERN = new RegExp(MENTION_PATTERN_SOURCE, "g");
|
|
20
|
+
|
|
21
|
+
const VARIABLE_PATTERN = /{{\s*([^{}]*?)\s*}}/g;
|
|
22
|
+
|
|
23
|
+
export function extractMentionedToolIds(text: string) {
|
|
24
|
+
const ids: string[] = [];
|
|
25
|
+
for (const match of text.matchAll(MENTION_PATTERN)) {
|
|
26
|
+
const id = match[2];
|
|
27
|
+
if (id && !ids.includes(id)) ids.push(id);
|
|
28
|
+
}
|
|
29
|
+
return ids;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Replaces each mention token with the tool's current name (falling back to the cached inline name). */
|
|
33
|
+
export function renderMentions(text: string, toolNameById: ReadonlyMap<string, string>) {
|
|
34
|
+
return text.replace(MENTION_PATTERN, (_, cachedName: string, toolId: string) => {
|
|
35
|
+
return toolNameById.get(toolId) ?? cachedName;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Refreshes stale display names inside stored text, keeping the tokens intact. */
|
|
40
|
+
export function normalizeMentions(text: string, toolNameById: ReadonlyMap<string, string>) {
|
|
41
|
+
return text.replace(MENTION_PATTERN, (token, _cachedName: string, toolId: string) => {
|
|
42
|
+
const name = toolNameById.get(toolId);
|
|
43
|
+
return name === undefined ? token : `@[${name}](${toolId})`;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Splits the `{{variables}}` referenced in a text into known and unknown ones. */
|
|
48
|
+
export function analyzeVariables<Variable extends string>(
|
|
49
|
+
text: string,
|
|
50
|
+
known: ReadonlyArray<Variable>,
|
|
51
|
+
) {
|
|
52
|
+
const referenced = new Set(
|
|
53
|
+
Array.from(text.matchAll(VARIABLE_PATTERN), ([, name]) => name?.trim()),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
variables: known.filter((variable) => referenced.has(variable)),
|
|
58
|
+
unknownVariables: [...referenced].filter(
|
|
59
|
+
(variable): variable is string =>
|
|
60
|
+
variable !== undefined &&
|
|
61
|
+
!known.some((knownVariable) => knownVariable === variable),
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Fills `{{variables}}` with per-request values; unknown names stay verbatim. */
|
|
67
|
+
export function substituteVariables(text: string, values: Record<string, string>) {
|
|
68
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, name: string) => values[name] ?? match);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Line-by-line fence state, following CommonMark's fence rules: a fence line
|
|
73
|
+
* is indented at most three spaces; an opening backtick fence's info string
|
|
74
|
+
* may not contain backticks; and a fence closes only on the same marker
|
|
75
|
+
* character, with at least the opening run's length, followed by nothing but
|
|
76
|
+
* whitespace — so a `~~~` line, a shorter same-marker run, or a ```` ```js ````
|
|
77
|
+
* info line inside a backtick fence all stay code content.
|
|
78
|
+
*/
|
|
79
|
+
function fenceTracker() {
|
|
80
|
+
let fence: { marker: string; length: number } | null = null;
|
|
81
|
+
return (line: string) => {
|
|
82
|
+
const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
|
|
83
|
+
const delimiter = match?.[1];
|
|
84
|
+
const rest = match?.[2] ?? "";
|
|
85
|
+
if (delimiter) {
|
|
86
|
+
const marker = delimiter.charAt(0);
|
|
87
|
+
if (fence) {
|
|
88
|
+
if (
|
|
89
|
+
marker === fence.marker &&
|
|
90
|
+
delimiter.length >= fence.length &&
|
|
91
|
+
rest.trim() === ""
|
|
92
|
+
) {
|
|
93
|
+
fence = null;
|
|
94
|
+
return "close";
|
|
95
|
+
}
|
|
96
|
+
return "inside";
|
|
97
|
+
}
|
|
98
|
+
if (marker === "`" && rest.includes("`")) return "outside";
|
|
99
|
+
fence = { marker, length: delimiter.length };
|
|
100
|
+
return "open";
|
|
101
|
+
}
|
|
102
|
+
return fence ? "inside" : "outside";
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** ATX heading, CommonMark-style: up to three leading spaces, `#` run closed by space or line end. */
|
|
107
|
+
const ATX_HEADING = /^( {0,3})(#{1,6})(?=[ \t]|$)/;
|
|
108
|
+
|
|
109
|
+
/** Setext underline: a run of `=` or `-` alone on its line, up to three leading spaces. */
|
|
110
|
+
const SETEXT_UNDERLINE = /^ {0,3}(=+|-+)[ \t]*$/;
|
|
111
|
+
|
|
112
|
+
/** The skeleton owns `##` and `###`, so user headings start three levels below it. */
|
|
113
|
+
function demotedHashes(level: number) {
|
|
114
|
+
return "#".repeat(Math.min(level + 3, 6));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether a line can be the text of a setext heading — i.e. it is an ordinary
|
|
119
|
+
* paragraph line, not a blank, a fence, another heading, or the start of a
|
|
120
|
+
* different block (quote/list), in which case the underline below it is a
|
|
121
|
+
* thematic break or list content rather than a heading marker.
|
|
122
|
+
*/
|
|
123
|
+
function isSetextText(line: string) {
|
|
124
|
+
return (
|
|
125
|
+
line.trim() !== "" &&
|
|
126
|
+
!ATX_HEADING.test(line) &&
|
|
127
|
+
!SETEXT_UNDERLINE.test(line) &&
|
|
128
|
+
!/^ {0,3}(`{3,}|~{3,})/.test(line) &&
|
|
129
|
+
!/^ {0,3}(>|[-*+][ \t]|\d+[.)][ \t])/.test(line)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Shifts user-authored headings down so they can never collide with the prompt
|
|
135
|
+
* skeleton, which owns `##` (sections) and `###` (card titles): `#` becomes
|
|
136
|
+
* `####`, deeper levels cap at `######`. Setext headings are rewritten as the
|
|
137
|
+
* equivalent demoted ATX heading, since two-level setext cannot express the
|
|
138
|
+
* shift. Fenced code is left alone.
|
|
139
|
+
*/
|
|
140
|
+
export function demoteHeadings(text: string) {
|
|
141
|
+
const lineState = fenceTracker();
|
|
142
|
+
const output: string[] = [];
|
|
143
|
+
for (const line of text.split("\n")) {
|
|
144
|
+
if (lineState(line) !== "outside") {
|
|
145
|
+
output.push(line);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const underline = SETEXT_UNDERLINE.exec(line)?.[1];
|
|
150
|
+
const previous = output[output.length - 1];
|
|
151
|
+
if (underline !== undefined && previous !== undefined && isSetextText(previous)) {
|
|
152
|
+
const level = underline.startsWith("=") ? 1 : 2;
|
|
153
|
+
output[output.length - 1] = `${demotedHashes(level)} ${previous.trim()}`;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
output.push(
|
|
158
|
+
line.replace(ATX_HEADING, (_, indent: string, hashes: string) => {
|
|
159
|
+
return `${indent}${demotedHashes(hashes.length)}`;
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return output.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The one render step for LLM-bound rich text: resolve mentions, then demote headings. */
|
|
167
|
+
export function renderRichText(text: string, toolNameById: ReadonlyMap<string, string>) {
|
|
168
|
+
return demoteHeadings(renderMentions(text, toolNameById));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reduces rich text to plain prose for embeddings: mentions become tool names
|
|
173
|
+
* (cached inline names when no map is given) and markdown syntax is dropped.
|
|
174
|
+
* ponytail: a regex stripper, not a markdown parser — nested emphasis or exotic
|
|
175
|
+
* constructs may leave residue; swap in a real parser if retrieval quality
|
|
176
|
+
* ever shows it.
|
|
177
|
+
*/
|
|
178
|
+
export function stripToPlaintext(
|
|
179
|
+
text: string,
|
|
180
|
+
toolNameById: ReadonlyMap<string, string> = new Map(),
|
|
181
|
+
) {
|
|
182
|
+
const lineState = fenceTracker();
|
|
183
|
+
return renderMentions(text, toolNameById)
|
|
184
|
+
.split("\n")
|
|
185
|
+
.filter((line) => {
|
|
186
|
+
const state = lineState(line);
|
|
187
|
+
return state !== "open" && state !== "close";
|
|
188
|
+
})
|
|
189
|
+
.map((line) =>
|
|
190
|
+
line
|
|
191
|
+
.replace(/^ {0,3}#{1,6}[ \t]+/, "")
|
|
192
|
+
.replace(SETEXT_UNDERLINE, "")
|
|
193
|
+
.replace(/^\s{0,3}(>\s?)+/, "")
|
|
194
|
+
.replace(/^(\s*)([-*+]|\d+[.)])\s+/, "$1")
|
|
195
|
+
.replace(/^\s*([-*_]\s*){3,}$/, ""),
|
|
196
|
+
)
|
|
197
|
+
.join("\n")
|
|
198
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
199
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
200
|
+
.replace(/(\*\*|__)([^*_]+)\1/g, "$2")
|
|
201
|
+
.replace(/(^|\W)[*_]([^*_]+)[*_](?=\W|$)/gm, "$1$2")
|
|
202
|
+
.replace(/~~([^~]+)~~/g, "$1")
|
|
203
|
+
.replace(/`([^`]*)`/g, "$1")
|
|
204
|
+
.replace(/\\([\\*_#[\]()>!`~+.-])/g, "$1")
|
|
205
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
206
|
+
.trim();
|
|
207
|
+
}
|
package/src/contracts/runtime.ts
CHANGED
|
@@ -13,7 +13,42 @@ export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
|
13
13
|
/** Runtime contract §5: tool names become `tools.<name>()` sandbox bindings. */
|
|
14
14
|
export const TOOL_NAME_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
15
15
|
|
|
16
|
-
export const PROMPT_VARIABLES = ["userName", "channel", "locale"] as const;
|
|
16
|
+
export const PROMPT_VARIABLES = ["userName", "channel", "locale", "utcTime", "timezone"] as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One entry per prompt variable, driving editor autocomplete, validation, and
|
|
20
|
+
* runtime substitution alike. Adding a variable = extending the tuple above
|
|
21
|
+
* plus one entry here (the compiler enforces the pair); the consumer embedding
|
|
22
|
+
* the agent supplies the value in its session/request context under the same
|
|
23
|
+
* key. Descriptions are plain English on purpose: variables are code-like
|
|
24
|
+
* identifiers shown in an LTR suggestion list, not localized UI copy.
|
|
25
|
+
*/
|
|
26
|
+
export const PROMPT_VARIABLE_DEFINITIONS = {
|
|
27
|
+
userName: {
|
|
28
|
+
description: "Display name of the signed-in user",
|
|
29
|
+
example: "Sara",
|
|
30
|
+
},
|
|
31
|
+
channel: {
|
|
32
|
+
description: "Channel the conversation arrived on",
|
|
33
|
+
example: "web",
|
|
34
|
+
},
|
|
35
|
+
locale: {
|
|
36
|
+
description: "Resolved locale of the session",
|
|
37
|
+
example: "ar",
|
|
38
|
+
},
|
|
39
|
+
// The server computes utcTime per request (a consumer-supplied value
|
|
40
|
+
// still wins); timezone has no server default — only the consumer knows
|
|
41
|
+
// the session's zone, so it must arrive via session/request context or
|
|
42
|
+
// the placeholder stays literal.
|
|
43
|
+
utcTime: {
|
|
44
|
+
description: "Current time in UTC (ISO 8601)",
|
|
45
|
+
example: "2026-08-16T09:30:00Z",
|
|
46
|
+
},
|
|
47
|
+
timezone: {
|
|
48
|
+
description: "IANA time zone of the session",
|
|
49
|
+
example: "Asia/Dubai",
|
|
50
|
+
},
|
|
51
|
+
} satisfies Record<(typeof PROMPT_VARIABLES)[number], { description: string; example: string }>;
|
|
17
52
|
|
|
18
53
|
export const RUNTIME_ERROR_KINDS = [
|
|
19
54
|
"timeout",
|
|
@@ -54,6 +89,19 @@ export const knowledgeChunkSchema = z.object({
|
|
|
54
89
|
}),
|
|
55
90
|
});
|
|
56
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
|
+
|
|
57
105
|
export const toolSignatureSchema = z.object({
|
|
58
106
|
toolId: z.uuid(),
|
|
59
107
|
name: z.string().regex(TOOL_NAME_PATTERN),
|
|
@@ -63,6 +111,11 @@ export const toolSignatureSchema = z.object({
|
|
|
63
111
|
signature: z.string(),
|
|
64
112
|
score: z.number().nullable(),
|
|
65
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(),
|
|
66
119
|
});
|
|
67
120
|
|
|
68
121
|
export const serviceCardSchema = z.object({
|
|
@@ -159,6 +212,9 @@ export const searchKnowledgeResponseSchema = z.object({
|
|
|
159
212
|
|
|
160
213
|
export const executeRequestSchema = z.object({
|
|
161
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(),
|
|
162
218
|
context: z
|
|
163
219
|
.object({
|
|
164
220
|
threadId: z.string().max(128).optional(),
|
|
@@ -201,6 +257,7 @@ export const runtimeErrorSchema = z.object({
|
|
|
201
257
|
}),
|
|
202
258
|
});
|
|
203
259
|
|
|
260
|
+
export type ToolInteraction = z.infer<typeof toolInteractionSchema>;
|
|
204
261
|
export type RuntimeAgentConfig = z.infer<typeof runtimeAgentConfigSchema>;
|
|
205
262
|
export type ResolveRequest = z.input<typeof resolveRequestSchema>;
|
|
206
263
|
export type ResolveResponse = z.infer<typeof resolveResponseSchema>;
|
package/src/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 };
|