@camelai/run 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +63 -2
- package/dist/clients/agents.d.ts +265 -0
- package/dist/clients/agents.js +283 -0
- package/dist/clients/ai-sdk.d.ts +137 -0
- package/dist/clients/ai-sdk.js +461 -0
- package/dist/clients/chat.d.ts +237 -0
- package/dist/clients/chat.js +633 -0
- package/dist/clients/handler.d.ts +116 -0
- package/dist/clients/handler.js +513 -0
- package/dist/clients/markdown.d.ts +72 -0
- package/dist/clients/markdown.js +385 -0
- package/dist/clients/mcp.d.ts +13 -0
- package/dist/clients/mcp.js +38 -0
- package/dist/clients/node.d.ts +21 -0
- package/dist/clients/node.js +76 -0
- package/dist/clients/server.d.ts +83 -0
- package/dist/clients/server.js +187 -0
- package/dist/clients/testing.d.ts +60 -0
- package/dist/clients/testing.js +53 -0
- package/dist/clients/types.d.ts +264 -0
- package/dist/clients/types.js +6 -0
- package/dist/clients/typescript.d.ts +1161 -0
- package/dist/clients/typescript.js +1035 -0
- package/dist/clients/watch.d.ts +90 -0
- package/dist/clients/watch.js +482 -0
- package/dist/shared/client-protocol.d.ts +106 -0
- package/dist/shared/client-protocol.js +9 -0
- package/package.json +76 -4
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown as an agent writes it, parsed into a small tree for any UI framework to render: paragraphs,
|
|
3
|
+
* headings, lists, quotes, code, tables and rules; bold, italic, strikethrough, code, links and
|
|
4
|
+
* images. There is no raw HTML, and links keep only http(s) and mailto URLs, so rendering the tree
|
|
5
|
+
* with a framework's elements (never as HTML) is safe with untrusted text.
|
|
6
|
+
*
|
|
7
|
+
* It is made for text that is still streaming: an unclosed code fence runs to the end, and with
|
|
8
|
+
* `streaming`, emphasis or code opened in the last paragraph shows as such before it closes, so
|
|
9
|
+
* nothing jumps when the closing marker arrives. Each block keeps its source (`raw`), so a renderer
|
|
10
|
+
* can skip blocks that did not change.
|
|
11
|
+
*/
|
|
12
|
+
export type Inline = {
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "strong" | "em" | "del";
|
|
17
|
+
children: Inline[];
|
|
18
|
+
} | {
|
|
19
|
+
type: "code";
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "link";
|
|
23
|
+
href: string;
|
|
24
|
+
children: Inline[];
|
|
25
|
+
} | {
|
|
26
|
+
type: "image";
|
|
27
|
+
src: string;
|
|
28
|
+
alt: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "break";
|
|
31
|
+
};
|
|
32
|
+
export type Block = ({
|
|
33
|
+
type: "paragraph";
|
|
34
|
+
children: Inline[];
|
|
35
|
+
} | {
|
|
36
|
+
type: "heading";
|
|
37
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
38
|
+
children: Inline[];
|
|
39
|
+
} | {
|
|
40
|
+
type: "code";
|
|
41
|
+
lang: string;
|
|
42
|
+
text: string;
|
|
43
|
+
closed: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
type: "blockquote";
|
|
46
|
+
children: Block[];
|
|
47
|
+
} | {
|
|
48
|
+
type: "list";
|
|
49
|
+
ordered: boolean;
|
|
50
|
+
start: number;
|
|
51
|
+
items: Block[][];
|
|
52
|
+
} | {
|
|
53
|
+
type: "table";
|
|
54
|
+
align: ("left" | "center" | "right" | null)[];
|
|
55
|
+
head: Inline[][];
|
|
56
|
+
rows: Inline[][][];
|
|
57
|
+
} | {
|
|
58
|
+
type: "hr";
|
|
59
|
+
}) & {
|
|
60
|
+
raw: string;
|
|
61
|
+
};
|
|
62
|
+
export interface MarkdownOptions {
|
|
63
|
+
/** The text is still being written: close what its last block leaves open. */
|
|
64
|
+
streaming?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** Only URLs a link may take: http(s) and mailto. Anything else (javascript:, data:, relative) is not a link. */
|
|
67
|
+
export declare function safeUrl(url: string): string | null;
|
|
68
|
+
export declare function parseMarkdown(text: string, options?: MarkdownOptions): Block[];
|
|
69
|
+
/** Inline markdown. `open`: close emphasis and code left open at the end (the text is still streaming). */
|
|
70
|
+
export declare function parseInline(text: string, open?: boolean, depth?: number): Inline[];
|
|
71
|
+
/** The plain text of inline nodes (for alt text, copy, and announcements). */
|
|
72
|
+
export declare function inlineText(nodes: Inline[]): string;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown as an agent writes it, parsed into a small tree for any UI framework to render: paragraphs,
|
|
3
|
+
* headings, lists, quotes, code, tables and rules; bold, italic, strikethrough, code, links and
|
|
4
|
+
* images. There is no raw HTML, and links keep only http(s) and mailto URLs, so rendering the tree
|
|
5
|
+
* with a framework's elements (never as HTML) is safe with untrusted text.
|
|
6
|
+
*
|
|
7
|
+
* It is made for text that is still streaming: an unclosed code fence runs to the end, and with
|
|
8
|
+
* `streaming`, emphasis or code opened in the last paragraph shows as such before it closes, so
|
|
9
|
+
* nothing jumps when the closing marker arrives. Each block keeps its source (`raw`), so a renderer
|
|
10
|
+
* can skip blocks that did not change.
|
|
11
|
+
*/
|
|
12
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)[^`]*$/;
|
|
13
|
+
const HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
|
|
14
|
+
const RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
15
|
+
const QUOTE = /^ {0,3}> ?/;
|
|
16
|
+
const ITEM = /^( {0,3})([-*+]|\d{1,9}[.)])([ \t]+|$)/;
|
|
17
|
+
const TABLE_DIVIDER = /^ {0,3}\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
|
|
18
|
+
/** Only URLs a link may take: http(s) and mailto. Anything else (javascript:, data:, relative) is not a link. */
|
|
19
|
+
export function safeUrl(url) {
|
|
20
|
+
const trimmed = url.trim();
|
|
21
|
+
return /^(https?:\/\/|mailto:)/i.test(trimmed) && !/[\s<>"]/.test(trimmed) ? trimmed : null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* How deep quotes, lists and emphasis nest before the rest is plain text, and how far a link's label or
|
|
25
|
+
* URL may reach: text a model streams (or a user pastes) cannot make parsing deep or quadratic.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_DEPTH = 16;
|
|
28
|
+
const MAX_LABEL = 1000;
|
|
29
|
+
const MAX_URL = 2048;
|
|
30
|
+
export function parseMarkdown(text, options = {}) {
|
|
31
|
+
return parseBlocks(text.replace(/\r\n?/g, "\n").split("\n"), options.streaming ?? false, 0);
|
|
32
|
+
}
|
|
33
|
+
function parseBlocks(lines, streaming, depth) {
|
|
34
|
+
if (depth > MAX_DEPTH)
|
|
35
|
+
return [{ type: "paragraph", children: [{ type: "text", text: lines.join("\n") }], raw: lines.join("\n") }];
|
|
36
|
+
const blocks = [];
|
|
37
|
+
let at = 0;
|
|
38
|
+
while (at < lines.length) {
|
|
39
|
+
const line = lines[at];
|
|
40
|
+
if (!line.trim()) {
|
|
41
|
+
at++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const start = at;
|
|
45
|
+
const fence = FENCE.exec(line);
|
|
46
|
+
if (fence) {
|
|
47
|
+
const marker = fence[1];
|
|
48
|
+
const indent = line.length - line.trimStart().length;
|
|
49
|
+
const body = [];
|
|
50
|
+
at++;
|
|
51
|
+
let closed = false;
|
|
52
|
+
while (at < lines.length) {
|
|
53
|
+
const close = lines[at].trim();
|
|
54
|
+
if (close.startsWith(marker[0].repeat(marker.length)) && /^[`~]+$/.test(close) && close.length >= marker.length) {
|
|
55
|
+
closed = true;
|
|
56
|
+
at++;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
body.push(indent ? lines[at].replace(new RegExp(`^ {0,${indent}}`), "") : lines[at]);
|
|
60
|
+
at++;
|
|
61
|
+
}
|
|
62
|
+
blocks.push({ type: "code", lang: fence[2] ?? "", text: body.join("\n"), closed, raw: lines.slice(start, at).join("\n") });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const heading = HEADING.exec(line);
|
|
66
|
+
if (heading) {
|
|
67
|
+
at++;
|
|
68
|
+
blocks.push({ type: "heading", level: heading[1].length, children: parseInline(heading[2] ?? "", streaming && at >= lines.length), raw: line });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (RULE.test(line)) {
|
|
72
|
+
at++;
|
|
73
|
+
blocks.push({ type: "hr", raw: line });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (QUOTE.test(line)) {
|
|
77
|
+
const inner = [];
|
|
78
|
+
while (at < lines.length && lines[at].trim() && (QUOTE.test(lines[at]) || inner.length)) {
|
|
79
|
+
if (!QUOTE.test(lines[at]) && (FENCE.test(lines[at]) || HEADING.test(lines[at]) || ITEM.test(lines[at])))
|
|
80
|
+
break;
|
|
81
|
+
inner.push(lines[at].replace(QUOTE, ""));
|
|
82
|
+
at++;
|
|
83
|
+
}
|
|
84
|
+
blocks.push({ type: "blockquote", children: parseBlocks(inner, streaming && at >= lines.length, depth + 1), raw: lines.slice(start, at).join("\n") });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const item = ITEM.exec(line);
|
|
88
|
+
if (item) {
|
|
89
|
+
const list = parseList(lines, at, streaming, depth);
|
|
90
|
+
at = list.end;
|
|
91
|
+
blocks.push({ ...list.block, raw: lines.slice(start, at).join("\n") });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (line.includes("|") && at + 1 < lines.length && TABLE_DIVIDER.test(lines[at + 1]) && lines[at + 1].includes("-")) {
|
|
95
|
+
const head = cells(line);
|
|
96
|
+
const align = cells(lines[at + 1]).map(cell => {
|
|
97
|
+
const left = cell.startsWith(":"), right = cell.endsWith(":");
|
|
98
|
+
return left && right ? "center" : right ? "right" : left ? "left" : null;
|
|
99
|
+
});
|
|
100
|
+
at += 2;
|
|
101
|
+
const rows = [];
|
|
102
|
+
while (at < lines.length && lines[at].trim() && lines[at].includes("|")) {
|
|
103
|
+
const row = cells(lines[at]);
|
|
104
|
+
rows.push(head.map((_, column) => parseInline(row[column] ?? "", false)));
|
|
105
|
+
at++;
|
|
106
|
+
}
|
|
107
|
+
blocks.push({ type: "table", align: head.map((_, column) => align[column] ?? null), head: head.map(cell => parseInline(cell, false)), rows, raw: lines.slice(start, at).join("\n") });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// A paragraph: up to a blank line or the start of another block.
|
|
111
|
+
const body = [];
|
|
112
|
+
while (at < lines.length && lines[at].trim()) {
|
|
113
|
+
const next = lines[at];
|
|
114
|
+
if (body.length && (FENCE.test(next) || HEADING.test(next) || RULE.test(next) || QUOTE.test(next) || ITEM.test(next)))
|
|
115
|
+
break;
|
|
116
|
+
body.push(next);
|
|
117
|
+
at++;
|
|
118
|
+
}
|
|
119
|
+
// Only the last block is still being written.
|
|
120
|
+
blocks.push({ type: "paragraph", children: parseInline(body.join("\n"), streaming && at >= lines.length), raw: body.join("\n") });
|
|
121
|
+
}
|
|
122
|
+
return blocks;
|
|
123
|
+
}
|
|
124
|
+
function cells(line) {
|
|
125
|
+
let row = line.trim();
|
|
126
|
+
if (row.startsWith("|"))
|
|
127
|
+
row = row.slice(1);
|
|
128
|
+
if (row.endsWith("|") && !row.endsWith("\\|"))
|
|
129
|
+
row = row.slice(0, -1);
|
|
130
|
+
const out = [];
|
|
131
|
+
let cell = "";
|
|
132
|
+
for (let at = 0; at < row.length; at++) {
|
|
133
|
+
if (row[at] === "\\" && row[at + 1] === "|") {
|
|
134
|
+
cell += "|";
|
|
135
|
+
at++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (row[at] === "|") {
|
|
139
|
+
out.push(cell.trim());
|
|
140
|
+
cell = "";
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
cell += row[at];
|
|
144
|
+
}
|
|
145
|
+
out.push(cell.trim());
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function parseList(lines, from, streaming, depth) {
|
|
149
|
+
const first = ITEM.exec(lines[from]);
|
|
150
|
+
const ordered = /\d/.test(first[2]);
|
|
151
|
+
const indent = first[1].length;
|
|
152
|
+
const items = [];
|
|
153
|
+
let at = from;
|
|
154
|
+
while (at < lines.length) {
|
|
155
|
+
const item = ITEM.exec(lines[at]);
|
|
156
|
+
if (!item || item[1].length !== indent || /\d/.test(item[2]) !== ordered)
|
|
157
|
+
break;
|
|
158
|
+
const contentIndent = item[0].length || indent + item[2].length + 1;
|
|
159
|
+
const body = [lines[at].slice(item[0].length)];
|
|
160
|
+
at++;
|
|
161
|
+
while (at < lines.length) {
|
|
162
|
+
const next = lines[at];
|
|
163
|
+
if (!next.trim()) {
|
|
164
|
+
// A blank line continues the item only if what follows is indented under it.
|
|
165
|
+
const after = lines[at + 1];
|
|
166
|
+
if (after !== undefined && after.trim() && after.length - after.trimStart().length >= contentIndent) {
|
|
167
|
+
body.push("");
|
|
168
|
+
at++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
const nested = ITEM.exec(next);
|
|
174
|
+
const nextIndent = next.length - next.trimStart().length;
|
|
175
|
+
if (nested && nested[1].length <= indent)
|
|
176
|
+
break;
|
|
177
|
+
if (!nested && nextIndent < contentIndent && (FENCE.test(next) || HEADING.test(next) || RULE.test(next) || QUOTE.test(next)))
|
|
178
|
+
break;
|
|
179
|
+
body.push(nextIndent >= contentIndent ? next.slice(contentIndent) : next.trimStart());
|
|
180
|
+
at++;
|
|
181
|
+
}
|
|
182
|
+
items.push(parseBlocks(body, streaming && at >= lines.length, depth + 1));
|
|
183
|
+
}
|
|
184
|
+
return { block: { type: "list", ordered, start: ordered ? parseInt(first[2], 10) : 1, items }, end: at };
|
|
185
|
+
}
|
|
186
|
+
const PUNCTUATION = /[!-/:-@[-`{-~\s]/;
|
|
187
|
+
/** Inline markdown. `open`: close emphasis and code left open at the end (the text is still streaming). */
|
|
188
|
+
export function parseInline(text, open = false, depth = 0) {
|
|
189
|
+
if (depth > MAX_DEPTH)
|
|
190
|
+
return text ? [{ type: "text", text }] : [];
|
|
191
|
+
const out = [];
|
|
192
|
+
/** Where each marker has no closer from, once a search found none: a later search cannot find one either. */
|
|
193
|
+
const unclosed = new Map();
|
|
194
|
+
let buffer = "";
|
|
195
|
+
const flush = () => { if (buffer) {
|
|
196
|
+
out.push({ type: "text", text: buffer });
|
|
197
|
+
buffer = "";
|
|
198
|
+
} };
|
|
199
|
+
let at = 0;
|
|
200
|
+
while (at < text.length) {
|
|
201
|
+
const char = text[at];
|
|
202
|
+
if (char === "\\" && at + 1 < text.length && PUNCTUATION.test(text[at + 1]) && text[at + 1] !== " ") {
|
|
203
|
+
buffer += text[at + 1];
|
|
204
|
+
at += 2;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (char === "\\" && text[at + 1] === "\n") {
|
|
208
|
+
flush();
|
|
209
|
+
out.push({ type: "break" });
|
|
210
|
+
at += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (char === "\n") {
|
|
214
|
+
// A newline is a line break, as people expect in a chat.
|
|
215
|
+
buffer = buffer.replace(/ +$/, "");
|
|
216
|
+
flush();
|
|
217
|
+
out.push({ type: "break" });
|
|
218
|
+
at++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (char === "`") {
|
|
222
|
+
const ticks = /^`+/.exec(text.slice(at))[0];
|
|
223
|
+
const close = at >= (unclosed.get(ticks) ?? Infinity) ? -1 : text.indexOf(ticks, at + ticks.length);
|
|
224
|
+
if (close === -1 && !unclosed.has(ticks))
|
|
225
|
+
unclosed.set(ticks, at);
|
|
226
|
+
if (close !== -1 && text[close + ticks.length] !== "`") {
|
|
227
|
+
flush();
|
|
228
|
+
let code = text.slice(at + ticks.length, close).replace(/\n/g, " ");
|
|
229
|
+
if (/^ .*[^ ].* $/.test(code))
|
|
230
|
+
code = code.slice(1, -1);
|
|
231
|
+
out.push({ type: "code", text: code });
|
|
232
|
+
at = close + ticks.length;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (open && close === -1) {
|
|
236
|
+
flush();
|
|
237
|
+
out.push({ type: "code", text: text.slice(at + ticks.length) });
|
|
238
|
+
at = text.length;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
buffer += ticks;
|
|
242
|
+
at += ticks.length;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (char === "!" && text[at + 1] === "[") {
|
|
246
|
+
const link = linkAt(text, at + 1);
|
|
247
|
+
if (link) {
|
|
248
|
+
flush();
|
|
249
|
+
const src = safeUrl(link.url);
|
|
250
|
+
out.push(src ? { type: "image", src, alt: link.label } : { type: "text", text: link.label });
|
|
251
|
+
at = link.end;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (char === "[") {
|
|
256
|
+
const link = linkAt(text, at);
|
|
257
|
+
if (link) {
|
|
258
|
+
flush();
|
|
259
|
+
const href = safeUrl(link.url);
|
|
260
|
+
const children = parseInline(link.label, false, depth + 1);
|
|
261
|
+
if (href)
|
|
262
|
+
out.push({ type: "link", href, children });
|
|
263
|
+
else
|
|
264
|
+
out.push(...children);
|
|
265
|
+
at = link.end;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (char === "<") {
|
|
270
|
+
const auto = /^<((?:https?:\/\/|mailto:)[^\s<>]+)>/i.exec(text.slice(at));
|
|
271
|
+
if (auto) {
|
|
272
|
+
flush();
|
|
273
|
+
out.push({ type: "link", href: auto[1], children: [{ type: "text", text: auto[1].replace(/^mailto:/i, "") }] });
|
|
274
|
+
at += auto[0].length;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if ((char === "h" || char === "H") && (at === 0 || /[\s(]/.test(text[at - 1]))) {
|
|
279
|
+
const bare = /^https?:\/\/[^\s<>]*[^\s<>.,:;"')\]!?*_~]/i.exec(text.slice(at));
|
|
280
|
+
if (bare) {
|
|
281
|
+
flush();
|
|
282
|
+
out.push({ type: "link", href: bare[0], children: [{ type: "text", text: bare[0] }] });
|
|
283
|
+
at += bare[0].length;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (char === "*" || char === "_" || char === "~") {
|
|
288
|
+
const run = new RegExp(`^\\${char}+`).exec(text.slice(at))[0];
|
|
289
|
+
const size = char === "~" ? (run.length >= 2 ? 2 : 0) : Math.min(run.length, 2);
|
|
290
|
+
const before = at > 0 ? text[at - 1] : " ";
|
|
291
|
+
const after = text[at + size] ?? " ";
|
|
292
|
+
// An opener is followed by a non-space; `_` inside a word (snake_case) is not emphasis.
|
|
293
|
+
const canOpen = size > 0 && !/\s/.test(after) && !(char === "_" && /[\p{L}\p{N}]/u.test(before));
|
|
294
|
+
if (canOpen) {
|
|
295
|
+
const marker = char.repeat(size);
|
|
296
|
+
const close = at + size >= (unclosed.get(marker) ?? Infinity) ? -1 : findCloser(text, at + size, marker);
|
|
297
|
+
if (close === -1 && !unclosed.has(marker))
|
|
298
|
+
unclosed.set(marker, at + size);
|
|
299
|
+
const type = char === "~" ? "del" : size === 2 ? "strong" : "em";
|
|
300
|
+
if (close !== -1) {
|
|
301
|
+
flush();
|
|
302
|
+
out.push({ type, children: parseInline(text.slice(at + size, close), false, depth + 1) });
|
|
303
|
+
at = close + size;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (open && at + size < text.length) {
|
|
307
|
+
flush();
|
|
308
|
+
out.push({ type, children: parseInline(text.slice(at + size), true, depth + 1) });
|
|
309
|
+
at = text.length;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
buffer += run;
|
|
314
|
+
at += run.length;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
buffer += char;
|
|
318
|
+
at++;
|
|
319
|
+
}
|
|
320
|
+
flush();
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
function findCloser(text, from, marker) {
|
|
324
|
+
for (let at = from; at < text.length; at++) {
|
|
325
|
+
const char = text[at];
|
|
326
|
+
if (char === "\\") {
|
|
327
|
+
at++;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (char === "`") {
|
|
331
|
+
const ticks = /^`+/.exec(text.slice(at))[0];
|
|
332
|
+
const close = text.indexOf(ticks, at + ticks.length);
|
|
333
|
+
if (close !== -1) {
|
|
334
|
+
at = close + ticks.length - 1;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (text.startsWith(marker, at) && at > from && !/\s/.test(text[at - 1])) {
|
|
339
|
+
const next = text[at + marker.length];
|
|
340
|
+
// `**` closes `**`, not the first half of `***`; `_` closes only at a word's end.
|
|
341
|
+
if (next === marker[0] && marker.length === 1 && text[at + 2] !== marker[0]) {
|
|
342
|
+
at++;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (marker[0] === "_" && next !== undefined && /[\p{L}\p{N}]/u.test(next))
|
|
346
|
+
continue;
|
|
347
|
+
return at;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return -1;
|
|
351
|
+
}
|
|
352
|
+
function linkAt(text, at) {
|
|
353
|
+
let depth = 0;
|
|
354
|
+
for (let close = at; close < Math.min(text.length, at + MAX_LABEL); close++) {
|
|
355
|
+
if (text[close] === "\\") {
|
|
356
|
+
close++;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (text[close] === "[")
|
|
360
|
+
depth++;
|
|
361
|
+
else if (text[close] === "]" && --depth === 0) {
|
|
362
|
+
if (text[close + 1] !== "(")
|
|
363
|
+
return null;
|
|
364
|
+
// The URL, with balanced parentheses (as in a Wikipedia link), then an optional "title".
|
|
365
|
+
let end = close + 2, parens = 0;
|
|
366
|
+
while (end < text.length && end < close + 2 + MAX_URL && !/\s/.test(text[end])) {
|
|
367
|
+
if (text[end] === "(")
|
|
368
|
+
parens++;
|
|
369
|
+
else if (text[end] === ")" && parens-- === 0)
|
|
370
|
+
break;
|
|
371
|
+
end++;
|
|
372
|
+
}
|
|
373
|
+
const url = text.slice(close + 2, end).replace(/^<(.*)>$/, "$1");
|
|
374
|
+
const rest = /^(?:\s+"[^"]*")?\s*\)/.exec(text.slice(end));
|
|
375
|
+
if (!rest)
|
|
376
|
+
return null;
|
|
377
|
+
return { label: text.slice(at + 1, close), url, end: end + rest[0].length };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
/** The plain text of inline nodes (for alt text, copy, and announcements). */
|
|
383
|
+
export function inlineText(nodes) {
|
|
384
|
+
return nodes.map(node => node.type === "text" || node.type === "code" ? node.text : node.type === "break" ? "\n" : node.type === "image" ? node.alt : inlineText(node.children)).join("");
|
|
385
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ToolServer } from "./typescript.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Attach an MCP SDK server (`McpServer` or low-level `Server`) to an agent: pass the result
|
|
6
|
+
* as `mcp` to `createAgent`, `connectAgent` or `configure`. The SDK talks to the server in
|
|
7
|
+
* memory, so the same server can later run remotely, as a definition's `mcpServers` entry,
|
|
8
|
+
* without changing its tools. `callId`, `toolCallId` and `origin` reach its handlers as
|
|
9
|
+
* `_meta["agent-runtime/…"]`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fromMcpServer(server: McpServer | Server): Promise<ToolServer & {
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
3
|
+
/** Tool names the runtime accepts; others are renamed, and calls map back. */
|
|
4
|
+
const safeName = (name) => (/^[A-Za-z]/.test(name) ? name : `t_${name}`).replace(/[^A-Za-z0-9_]/g, "_").slice(0, 80);
|
|
5
|
+
/**
|
|
6
|
+
* Attach an MCP SDK server (`McpServer` or low-level `Server`) to an agent: pass the result
|
|
7
|
+
* as `mcp` to `createAgent`, `connectAgent` or `configure`. The SDK talks to the server in
|
|
8
|
+
* memory, so the same server can later run remotely, as a definition's `mcpServers` entry,
|
|
9
|
+
* without changing its tools. `callId`, `toolCallId` and `origin` reach its handlers as
|
|
10
|
+
* `_meta["agent-runtime/…"]`.
|
|
11
|
+
*/
|
|
12
|
+
export async function fromMcpServer(server) {
|
|
13
|
+
const [clientSide, serverSide] = InMemoryTransport.createLinkedPair();
|
|
14
|
+
await server.connect(serverSide);
|
|
15
|
+
const client = new Client({ name: "agent-runtime-sdk", version: "1.0.0" });
|
|
16
|
+
await client.connect(clientSide);
|
|
17
|
+
const names = new Map();
|
|
18
|
+
return {
|
|
19
|
+
async listTools() {
|
|
20
|
+
const tools = [];
|
|
21
|
+
let cursor;
|
|
22
|
+
do {
|
|
23
|
+
const page = await client.listTools(cursor ? { cursor } : {});
|
|
24
|
+
for (const tool of page.tools) {
|
|
25
|
+
names.set(safeName(tool.name), tool.name);
|
|
26
|
+
tools.push({ ...tool, name: safeName(tool.name) });
|
|
27
|
+
}
|
|
28
|
+
cursor = page.nextCursor;
|
|
29
|
+
} while (cursor);
|
|
30
|
+
return tools;
|
|
31
|
+
},
|
|
32
|
+
async callTool(name, args, context) {
|
|
33
|
+
const _meta = { "agent-runtime/callId": context.callId, ...(context.toolCallId ? { "agent-runtime/toolCallId": context.toolCallId } : {}), ...(context.origin ? { "agent-runtime/origin": context.origin } : {}) };
|
|
34
|
+
return await client.callTool({ name: names.get(name) ?? name, arguments: args, _meta }, undefined, { signal: context.signal });
|
|
35
|
+
},
|
|
36
|
+
async close() { await client.close(); },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Agents as PortableAgents, AgentRuntime as PortableAgentRuntime, type AgentsOptions, type RuntimeOptions } from "./typescript.ts";
|
|
2
|
+
export * from "./typescript.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A fetch-style handler (e.g. `serveTools(...)`) as a `node:http` request listener:
|
|
5
|
+
* `createServer(nodeListener(serveTools(tools, { runtime, tenant }), { origin: "https://tools.example.com" }))`.
|
|
6
|
+
* Identity tokens are checked against the URL the runtime called. Set `origin` to your public URL; without
|
|
7
|
+
* it the URL is this server's own (its socket's scheme and the Host header). Behind a proxy that ends TLS,
|
|
8
|
+
* set `origin`, or `trustProxy: true` to read X-Forwarded-Proto and X-Forwarded-Host, but only where the
|
|
9
|
+
* proxy overwrites those headers: otherwise any client could choose the URL tokens are checked against.
|
|
10
|
+
*/
|
|
11
|
+
export declare function nodeListener(handler: (request: Request) => Promise<Response>, options?: {
|
|
12
|
+
origin?: string;
|
|
13
|
+
trustProxy?: boolean;
|
|
14
|
+
}): (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => Promise<void>;
|
|
15
|
+
export declare class AgentRuntime extends PortableAgentRuntime {
|
|
16
|
+
constructor(options?: RuntimeOptions);
|
|
17
|
+
}
|
|
18
|
+
/** `Agents` for Node: local file paths attach as files. */
|
|
19
|
+
export declare class Agents extends PortableAgents {
|
|
20
|
+
constructor(options?: AgentsOptions);
|
|
21
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** Node/Bun convenience entry. The portable SDK itself imports no Node modules. */
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { openAsBlob } from "node:fs";
|
|
4
|
+
import { Agents as PortableAgents, AgentRuntime as PortableAgentRuntime } from "./typescript.js";
|
|
5
|
+
export * from "./typescript.js";
|
|
6
|
+
/** A forwarded header's first value (a proxy chain lists one per hop, the client's first). */
|
|
7
|
+
const forwarded = (value) => (Array.isArray(value) ? value[0] : value)?.split(",")[0].trim() || undefined;
|
|
8
|
+
/**
|
|
9
|
+
* A fetch-style handler (e.g. `serveTools(...)`) as a `node:http` request listener:
|
|
10
|
+
* `createServer(nodeListener(serveTools(tools, { runtime, tenant }), { origin: "https://tools.example.com" }))`.
|
|
11
|
+
* Identity tokens are checked against the URL the runtime called. Set `origin` to your public URL; without
|
|
12
|
+
* it the URL is this server's own (its socket's scheme and the Host header). Behind a proxy that ends TLS,
|
|
13
|
+
* set `origin`, or `trustProxy: true` to read X-Forwarded-Proto and X-Forwarded-Host, but only where the
|
|
14
|
+
* proxy overwrites those headers: otherwise any client could choose the URL tokens are checked against.
|
|
15
|
+
*/
|
|
16
|
+
export function nodeListener(handler, options = {}) {
|
|
17
|
+
return async (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const proxied = (name) => options.trustProxy ? forwarded(req.headers[name]) : undefined;
|
|
20
|
+
const protocol = proxied("x-forwarded-proto") ?? (req.socket.encrypted ? "https" : "http");
|
|
21
|
+
const origin = options.origin ?? `${protocol === "https" ? "https" : "http"}://${proxied("x-forwarded-host") ?? req.headers.host ?? "localhost"}`;
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
res.on("close", () => { if (!res.writableFinished)
|
|
24
|
+
controller.abort(); });
|
|
25
|
+
const headers = new Headers();
|
|
26
|
+
for (const [name, value] of Object.entries(req.headers))
|
|
27
|
+
if (value !== undefined)
|
|
28
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
29
|
+
const chunks = [];
|
|
30
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
31
|
+
for await (const chunk of req)
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
const body = chunks.length ? new Uint8Array(Buffer.concat(chunks)) : undefined;
|
|
34
|
+
const response = await handler(new Request(new URL(req.url ?? "/", origin), { method: req.method, headers, body, signal: controller.signal }));
|
|
35
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
36
|
+
if (!response.body) {
|
|
37
|
+
res.end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Streamed as it comes, so an event stream reaches its client as it is written.
|
|
41
|
+
res.flushHeaders();
|
|
42
|
+
const reader = response.body.getReader();
|
|
43
|
+
res.on("close", () => { void reader.cancel().catch(() => { }); });
|
|
44
|
+
for (;;) {
|
|
45
|
+
const { value, done } = await reader.read();
|
|
46
|
+
if (done)
|
|
47
|
+
break;
|
|
48
|
+
if (!res.write(value))
|
|
49
|
+
await once(res, "drain");
|
|
50
|
+
}
|
|
51
|
+
res.end();
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// A body that fails partway cannot become an error answer: the connection is cut, so the client sees it failed.
|
|
55
|
+
if (res.headersSent) {
|
|
56
|
+
res.destroy();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
60
|
+
res.end(JSON.stringify({ error: String(error).slice(0, 500) }));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export class AgentRuntime extends PortableAgentRuntime {
|
|
65
|
+
constructor(options = {}) {
|
|
66
|
+
super({ ...options, url: options.url ?? process.env.AGENT_URL, openFile: options.openFile ?? (path => openAsBlob(path)),
|
|
67
|
+
apiKey: options.apiKey ?? process.env.AGENT_RUNTIME_TOKEN,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** `Agents` for Node: local file paths attach as files. */
|
|
72
|
+
export class Agents extends PortableAgents {
|
|
73
|
+
constructor(options = {}) {
|
|
74
|
+
super({ ...options, openFile: options.openFile ?? (path => openAsBlob(path)) });
|
|
75
|
+
}
|
|
76
|
+
}
|