@tangle-network/agent-app 0.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.
- package/README.md +59 -0
- package/dist/billing/index.d.ts +108 -0
- package/dist/billing/index.js +7 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/chunk-45MYQ3GD.js +62 -0
- package/dist/chunk-45MYQ3GD.js.map +1 -0
- package/dist/chunk-4NXVI7PW.js +32 -0
- package/dist/chunk-4NXVI7PW.js.map +1 -0
- package/dist/chunk-7P6VIHI4.js +33 -0
- package/dist/chunk-7P6VIHI4.js.map +1 -0
- package/dist/chunk-C5CREGT2.js +45 -0
- package/dist/chunk-C5CREGT2.js.map +1 -0
- package/dist/chunk-CN75FIPT.js +61 -0
- package/dist/chunk-CN75FIPT.js.map +1 -0
- package/dist/chunk-EDIQ6F55.js +274 -0
- package/dist/chunk-EDIQ6F55.js.map +1 -0
- package/dist/chunk-FS5OUVRB.js +208 -0
- package/dist/chunk-FS5OUVRB.js.map +1 -0
- package/dist/chunk-GMFPCCQZ.js +245 -0
- package/dist/chunk-GMFPCCQZ.js.map +1 -0
- package/dist/chunk-L2TG5DBW.js +74 -0
- package/dist/chunk-L2TG5DBW.js.map +1 -0
- package/dist/chunk-SIDR6BH3.js +57 -0
- package/dist/chunk-SIDR6BH3.js.map +1 -0
- package/dist/chunk-YGUNTIT5.js +48 -0
- package/dist/chunk-YGUNTIT5.js.map +1 -0
- package/dist/crypto/index.d.ts +27 -0
- package/dist/crypto/index.js +13 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/delegation/index.d.ts +50 -0
- package/dist/delegation/index.js +11 -0
- package/dist/delegation/index.js.map +1 -0
- package/dist/eval/index.d.ts +50 -0
- package/dist/eval/index.js +17 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/index.d.ts +84 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/redact/index.d.ts +22 -0
- package/dist/redact/index.js +7 -0
- package/dist/redact/index.js.map +1 -0
- package/dist/runtime/index.d.ts +219 -0
- package/dist/runtime/index.js +17 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/stream/index.d.ts +39 -0
- package/dist/stream/index.js +35 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/tangle/index.d.ts +93 -0
- package/dist/tangle/index.js +9 -0
- package/dist/tangle/index.js.map +1 -0
- package/dist/tools/index.d.ts +213 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types-CeWor4bQ.d.ts +120 -0
- package/dist/web/index.d.ts +51 -0
- package/dist/web/index.js +15 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/tools/errors.ts
|
|
2
|
+
var ToolInputError = class extends Error {
|
|
3
|
+
constructor(code, message, status = 400) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "ToolInputError";
|
|
8
|
+
}
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/tools/capability.ts
|
|
14
|
+
async function createCapabilityToken(userId, opts) {
|
|
15
|
+
const secret = opts.secret?.trim();
|
|
16
|
+
if (!secret) return void 0;
|
|
17
|
+
const prefix = opts.prefix ?? "cap_";
|
|
18
|
+
return `${prefix}${await sign(userId, secret)}`;
|
|
19
|
+
}
|
|
20
|
+
async function verifyCapabilityToken(userId, token, opts) {
|
|
21
|
+
const secret = opts.secret?.trim();
|
|
22
|
+
const prefix = opts.prefix ?? "cap_";
|
|
23
|
+
if (!secret || !token.startsWith(prefix)) return false;
|
|
24
|
+
const expected = `${prefix}${await sign(userId, secret)}`;
|
|
25
|
+
return timingSafeEqual(token, expected);
|
|
26
|
+
}
|
|
27
|
+
async function sign(userId, secret) {
|
|
28
|
+
const enc = new TextEncoder();
|
|
29
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
30
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(`user:${userId}`));
|
|
31
|
+
return base64url(new Uint8Array(sig));
|
|
32
|
+
}
|
|
33
|
+
function base64url(bytes) {
|
|
34
|
+
let s = "";
|
|
35
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
36
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
37
|
+
}
|
|
38
|
+
function timingSafeEqual(a, b) {
|
|
39
|
+
if (a.length !== b.length) return false;
|
|
40
|
+
let diff = 0;
|
|
41
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
42
|
+
return diff === 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/tools/auth.ts
|
|
46
|
+
var DEFAULT_HEADER_NAMES = {
|
|
47
|
+
userId: "X-Agent-App-User-Id",
|
|
48
|
+
workspaceId: "X-Agent-App-Workspace-Id",
|
|
49
|
+
threadId: "X-Agent-App-Thread-Id"
|
|
50
|
+
};
|
|
51
|
+
async function authenticateToolRequest(request, opts) {
|
|
52
|
+
const h = opts.headerNames ?? DEFAULT_HEADER_NAMES;
|
|
53
|
+
const userId = request.headers.get(h.userId)?.trim();
|
|
54
|
+
const workspaceId = request.headers.get(h.workspaceId)?.trim();
|
|
55
|
+
const threadId = request.headers.get(h.threadId)?.trim() || null;
|
|
56
|
+
const bearer = request.headers.get("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
57
|
+
if (!userId || !bearer) {
|
|
58
|
+
return { ok: false, response: Response.json({ error: "Missing capability credentials" }, { status: 401 }) };
|
|
59
|
+
}
|
|
60
|
+
if (!await opts.verifyToken(userId, bearer)) {
|
|
61
|
+
return { ok: false, response: Response.json({ error: "Invalid capability token" }, { status: 401 }) };
|
|
62
|
+
}
|
|
63
|
+
if (!workspaceId) {
|
|
64
|
+
return { ok: false, response: Response.json({ error: "Missing workspace context" }, { status: 400 }) };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, ctx: { userId, workspaceId, threadId } };
|
|
67
|
+
}
|
|
68
|
+
async function readToolArgs(request) {
|
|
69
|
+
let body;
|
|
70
|
+
try {
|
|
71
|
+
body = await request.json();
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return body.args ?? body.arguments ?? body;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/tools/openai.ts
|
|
79
|
+
var APP_TOOL_NAMES = ["submit_proposal", "schedule_followup", "render_ui", "add_citation"];
|
|
80
|
+
var NAME_SET = new Set(APP_TOOL_NAMES);
|
|
81
|
+
function isAppToolName(name) {
|
|
82
|
+
return NAME_SET.has(name);
|
|
83
|
+
}
|
|
84
|
+
function buildAppToolOpenAITools(taxonomy) {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
type: "function",
|
|
88
|
+
function: {
|
|
89
|
+
name: "submit_proposal",
|
|
90
|
+
description: "Route a regulated or state-changing action to a human for approval (a recommendation, contacting/soliciting a contact, outreach, a record/account change, scheduling). Queues it for a named certified human to approve before it executes.",
|
|
91
|
+
parameters: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
type: { type: "string", enum: [...taxonomy.proposalTypes] },
|
|
95
|
+
title: { type: "string", description: "Short label for the approval queue." },
|
|
96
|
+
description: { type: "string", description: "The full drafted message/recommendation, with sources." }
|
|
97
|
+
},
|
|
98
|
+
required: ["type", "title"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "function",
|
|
104
|
+
function: {
|
|
105
|
+
name: "schedule_followup",
|
|
106
|
+
description: "Register a dated cadence step (a reminder, chase, or check-in) on the follow-up calendar. Executes immediately.",
|
|
107
|
+
parameters: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
title: { type: "string" },
|
|
111
|
+
dueDate: { type: "string", description: "ISO date YYYY-MM-DD." },
|
|
112
|
+
priority: { type: "string", enum: ["low", "medium", "high"] }
|
|
113
|
+
},
|
|
114
|
+
required: ["title", "dueDate"]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: "function",
|
|
120
|
+
function: {
|
|
121
|
+
name: "render_ui",
|
|
122
|
+
description: "Show a generated view live in the workspace. Validates the OpenUI JSON and persists the artifact. Executes immediately.",
|
|
123
|
+
parameters: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
title: { type: "string" },
|
|
127
|
+
schema: { type: "object", description: "The OpenUI JSON object." }
|
|
128
|
+
},
|
|
129
|
+
required: ["title", "schema"]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
type: "function",
|
|
135
|
+
function: {
|
|
136
|
+
name: "add_citation",
|
|
137
|
+
description: "Anchor a grounding reference: the exact quote from a file backing a figure or claim. Verifies the quote appears in the file. Executes immediately.",
|
|
138
|
+
parameters: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
path: { type: "string", description: "The vault file path." },
|
|
142
|
+
quote: { type: "string", description: "The exact text from it." }
|
|
143
|
+
},
|
|
144
|
+
required: ["path", "quote"]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/tools/dispatch.ts
|
|
152
|
+
async function dispatchAppTool(toolName, rawArgs, ctx, opts) {
|
|
153
|
+
try {
|
|
154
|
+
if (!isAppToolName(toolName)) {
|
|
155
|
+
return { ok: false, code: "unknown_tool", message: `${toolName} is not an app tool.` };
|
|
156
|
+
}
|
|
157
|
+
if (toolName === "submit_proposal") {
|
|
158
|
+
const type = String(rawArgs.type ?? "").trim();
|
|
159
|
+
const title = String(rawArgs.title ?? "").trim();
|
|
160
|
+
if (!type || !opts.taxonomy.proposalTypes.includes(type)) {
|
|
161
|
+
return { ok: false, code: "invalid_type", message: `type must be one of: ${opts.taxonomy.proposalTypes.join(", ")}.` };
|
|
162
|
+
}
|
|
163
|
+
if (!title) return { ok: false, code: "missing_title", message: "title is required." };
|
|
164
|
+
const description = rawArgs.description == null ? null : String(rawArgs.description);
|
|
165
|
+
const r2 = await opts.handlers.submitProposal({ type, title, description }, ctx);
|
|
166
|
+
const regulated = opts.taxonomy.regulatedTypes.includes(type);
|
|
167
|
+
opts.onProduced?.({ type: "proposal_created", proposalId: r2.proposalId, title, status: "pending" });
|
|
168
|
+
return { ok: true, result: { status: "queued_for_approval", proposalId: r2.proposalId, deduped: r2.deduped, regulated } };
|
|
169
|
+
}
|
|
170
|
+
if (toolName === "schedule_followup") {
|
|
171
|
+
const r2 = await opts.handlers.scheduleFollowup(
|
|
172
|
+
{ title: String(rawArgs.title ?? ""), dueDate: String(rawArgs.dueDate ?? ""), priority: rawArgs.priority },
|
|
173
|
+
ctx
|
|
174
|
+
);
|
|
175
|
+
return { ok: true, result: { followupId: r2.id, dueDate: r2.dueDate, deduped: r2.deduped } };
|
|
176
|
+
}
|
|
177
|
+
if (toolName === "render_ui") {
|
|
178
|
+
const r2 = await opts.handlers.renderUi({ title: String(rawArgs.title ?? ""), schema: rawArgs.schema }, ctx);
|
|
179
|
+
opts.onProduced?.({ type: "artifact", path: r2.path, content: r2.content });
|
|
180
|
+
return { ok: true, result: { path: r2.path } };
|
|
181
|
+
}
|
|
182
|
+
const r = await opts.handlers.addCitation(
|
|
183
|
+
{ path: String(rawArgs.path ?? ""), quote: String(rawArgs.quote ?? ""), label: rawArgs.label },
|
|
184
|
+
ctx
|
|
185
|
+
);
|
|
186
|
+
return { ok: true, result: { citationId: r.citationId, path: r.path } };
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof ToolInputError) return { ok: false, code: err.code, message: err.message, status: err.status };
|
|
189
|
+
return { ok: false, code: "app_tool_error", message: err instanceof Error ? err.message : String(err), status: 500 };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function outcomeStatus(outcome) {
|
|
193
|
+
return outcome.status ?? 400;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/tools/runtime.ts
|
|
197
|
+
function createAppToolRuntimeExecutor(opts) {
|
|
198
|
+
return ({ toolName, args }) => dispatchAppTool(toolName, args, opts.ctx, opts);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/tools/http.ts
|
|
202
|
+
async function handleAppToolRequest(request, opts) {
|
|
203
|
+
if (request.method !== "POST") return Response.json({ error: "Method not allowed" }, { status: 405 });
|
|
204
|
+
const auth = await authenticateToolRequest(request, { verifyToken: opts.verifyToken, headerNames: opts.headerNames });
|
|
205
|
+
if (!auth.ok) return auth.response;
|
|
206
|
+
let body;
|
|
207
|
+
try {
|
|
208
|
+
body = await request.json();
|
|
209
|
+
} catch {
|
|
210
|
+
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
211
|
+
}
|
|
212
|
+
const args = body.args ?? body.arguments ?? body;
|
|
213
|
+
const outcome = await dispatchAppTool(opts.tool, args, auth.ctx, opts);
|
|
214
|
+
if (!outcome.ok) {
|
|
215
|
+
return Response.json({ error: outcome.code, message: outcome.message }, { status: outcomeStatus(outcome) });
|
|
216
|
+
}
|
|
217
|
+
const payload = outcome.result;
|
|
218
|
+
return Response.json({ ok: true, ...payload, ...opts.message ? { message: opts.message(outcome.result) } : {} });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/tools/mcp.ts
|
|
222
|
+
var DEFAULT_APP_TOOL_PATHS = {
|
|
223
|
+
submit_proposal: "/api/tools/propose",
|
|
224
|
+
schedule_followup: "/api/tools/followup",
|
|
225
|
+
render_ui: "/api/tools/render-ui",
|
|
226
|
+
add_citation: "/api/tools/citation"
|
|
227
|
+
};
|
|
228
|
+
function buildHttpMcpServer(opts) {
|
|
229
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
230
|
+
const h = opts.headerNames ?? DEFAULT_HEADER_NAMES;
|
|
231
|
+
return {
|
|
232
|
+
transport: "http",
|
|
233
|
+
url: `${base}${opts.path}`,
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${opts.token}`,
|
|
236
|
+
[h.userId]: opts.ctx.userId,
|
|
237
|
+
...opts.ctx.workspaceId ? { [h.workspaceId]: opts.ctx.workspaceId } : {},
|
|
238
|
+
...opts.ctx.threadId ? { [h.threadId]: opts.ctx.threadId } : {},
|
|
239
|
+
"Content-Type": "application/json"
|
|
240
|
+
},
|
|
241
|
+
enabled: true,
|
|
242
|
+
metadata: { description: opts.description }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function buildAppToolMcpServer(opts) {
|
|
246
|
+
return buildHttpMcpServer({
|
|
247
|
+
path: opts.paths?.[opts.tool] ?? DEFAULT_APP_TOOL_PATHS[opts.tool],
|
|
248
|
+
baseUrl: opts.baseUrl,
|
|
249
|
+
token: opts.token,
|
|
250
|
+
ctx: opts.ctx,
|
|
251
|
+
description: opts.description,
|
|
252
|
+
headerNames: opts.headerNames
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export {
|
|
257
|
+
ToolInputError,
|
|
258
|
+
createCapabilityToken,
|
|
259
|
+
verifyCapabilityToken,
|
|
260
|
+
DEFAULT_HEADER_NAMES,
|
|
261
|
+
authenticateToolRequest,
|
|
262
|
+
readToolArgs,
|
|
263
|
+
APP_TOOL_NAMES,
|
|
264
|
+
isAppToolName,
|
|
265
|
+
buildAppToolOpenAITools,
|
|
266
|
+
dispatchAppTool,
|
|
267
|
+
outcomeStatus,
|
|
268
|
+
createAppToolRuntimeExecutor,
|
|
269
|
+
handleAppToolRequest,
|
|
270
|
+
DEFAULT_APP_TOOL_PATHS,
|
|
271
|
+
buildHttpMcpServer,
|
|
272
|
+
buildAppToolMcpServer
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=chunk-EDIQ6F55.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/errors.ts","../src/tools/capability.ts","../src/tools/auth.ts","../src/tools/openai.ts","../src/tools/dispatch.ts","../src/tools/runtime.ts","../src/tools/http.ts","../src/tools/mcp.ts"],"sourcesContent":["/** A correctable bad-input error a tool handler throws; the HTTP layer maps it\n * to a 4xx with the code, the runtime layer to a failed tool_result. So the\n * agent learns the call failed and can correct, instead of a silent success. */\nexport class ToolInputError extends Error {\n constructor(\n public code: string,\n message: string,\n public status = 400,\n ) {\n super(message)\n this.name = 'ToolInputError'\n }\n}\n","/**\n * Per-user capability token — the sandbox→app auth primitive behind the\n * `verifyToken` seam in {@link authenticateToolRequest}.\n *\n * An app-agent runs inside the sandbox and reaches the host app back over HTTP\n * (the app tools, the integration-invoke bridge). The route must act AS the\n * connecting user without trusting any model-supplied identity, so the turn\n * mints a short HMAC token bound to the user id and bakes it into the per-turn\n * MCP server header; the route verifies it to recover the user.\n *\n * `HMAC-SHA256(secret, \"user:<userId>\")`, base64url, with an app-chosen prefix.\n * The token encodes no scopes — the hub's policy engine authorizes per action.\n * Fail-closed: with no secret, no token is minted (the caller MUST omit the MCP\n * server rather than fake an authorized call). WebCrypto only — runs on\n * Workers, Node, and the browser with no Node `crypto` dependency.\n */\n\nexport interface CapabilityTokenOptions {\n /** Shared HMAC secret. When absent, mint returns undefined / verify returns false. */\n secret?: string\n /** Token prefix (namespaces the credential; lets verify reject foreign tokens\n * cheaply). Default `cap_`. */\n prefix?: string\n}\n\n/** Mint a capability token for `userId`, or `undefined` when no secret is\n * configured (fail-closed — the caller omits the MCP server rather than fake it). */\nexport async function createCapabilityToken(userId: string, opts: CapabilityTokenOptions): Promise<string | undefined> {\n const secret = opts.secret?.trim()\n if (!secret) return undefined\n const prefix = opts.prefix ?? 'cap_'\n return `${prefix}${await sign(userId, secret)}`\n}\n\n/** Verify a capability token against `userId`. Returns false (never throws) for\n * an unconfigured secret, a wrong prefix, a malformed token, or a mismatch. */\nexport async function verifyCapabilityToken(userId: string, token: string, opts: CapabilityTokenOptions): Promise<boolean> {\n const secret = opts.secret?.trim()\n const prefix = opts.prefix ?? 'cap_'\n if (!secret || !token.startsWith(prefix)) return false\n const expected = `${prefix}${await sign(userId, secret)}`\n return timingSafeEqual(token, expected)\n}\n\nasync function sign(userId: string, secret: string): Promise<string> {\n const enc = new TextEncoder()\n const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const sig = await crypto.subtle.sign('HMAC', key, enc.encode(`user:${userId}`))\n return base64url(new Uint8Array(sig))\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let s = ''\n for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]!)\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/** Length-independent-leak-free compare for two same-charset strings. */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n","import type { AppToolContext } from './types'\n\n/**\n * Header names carrying the server-set per-turn context + the capability token.\n * Defaults are product-neutral (`X-Agent-App-*`); a product that already ships\n * a header convention (e.g. `X-Insurance-User-Id`) passes its own.\n */\nexport interface ToolHeaderNames {\n userId: string\n workspaceId: string\n threadId: string\n}\n\nexport const DEFAULT_HEADER_NAMES: ToolHeaderNames = {\n userId: 'X-Agent-App-User-Id',\n workspaceId: 'X-Agent-App-Workspace-Id',\n threadId: 'X-Agent-App-Thread-Id',\n}\n\nexport interface AuthenticateOptions {\n /** Verify the bearer capability token belongs to `userId`. The product's\n * HMAC/JWT impl — the seam that keeps token crypto out of this package. */\n verifyToken: (userId: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n}\n\nexport type ToolAuthResult =\n | { ok: true; ctx: AppToolContext }\n | { ok: false; response: Response }\n\n/**\n * Recover + verify the trusted context for a tool request. The user comes from\n * a server-set header and the bearer token MUST verify against THAT user; the\n * workspace comes from a header too — never from tool args — so the model can\n * neither forge identity nor target another workspace. Fail-closed: any missing\n * credential or a token minted for another user yields a 401/400 Response.\n */\nexport async function authenticateToolRequest(request: Request, opts: AuthenticateOptions): Promise<ToolAuthResult> {\n const h = opts.headerNames ?? DEFAULT_HEADER_NAMES\n const userId = request.headers.get(h.userId)?.trim()\n const workspaceId = request.headers.get(h.workspaceId)?.trim()\n const threadId = request.headers.get(h.threadId)?.trim() || null\n const bearer = request.headers.get('authorization')?.match(/^Bearer\\s+(.+)$/i)?.[1]\n\n if (!userId || !bearer) {\n return { ok: false, response: Response.json({ error: 'Missing capability credentials' }, { status: 401 }) }\n }\n if (!(await opts.verifyToken(userId, bearer))) {\n return { ok: false, response: Response.json({ error: 'Invalid capability token' }, { status: 401 }) }\n }\n if (!workspaceId) {\n return { ok: false, response: Response.json({ error: 'Missing workspace context' }, { status: 400 }) }\n }\n return { ok: true, ctx: { userId, workspaceId, threadId } }\n}\n\n/** Read a tool's argument object from the request body, tolerant of MCP host\n * aliases (`args` / `arguments`) or a bare body. Returns null on non-JSON. */\nexport async function readToolArgs<T>(request: Request): Promise<T | null> {\n let body: { args?: T; arguments?: T }\n try {\n body = (await request.json()) as typeof body\n } catch {\n return null\n }\n return (body.args ?? body.arguments ?? (body as T)) as T\n}\n","import type { AppToolTaxonomy } from './types'\n\n/** The four canonical app-tool names. Stable identifiers the model calls in\n * both the sandbox (MCP server name) and runtime (function-tool name) paths. */\nexport const APP_TOOL_NAMES = ['submit_proposal', 'schedule_followup', 'render_ui', 'add_citation'] as const\nexport type AppToolName = (typeof APP_TOOL_NAMES)[number]\n\nconst NAME_SET = new Set<string>(APP_TOOL_NAMES)\nexport function isAppToolName(name: string): name is AppToolName {\n return NAME_SET.has(name)\n}\n\n/** A minimal OpenAI Chat Completions function-tool shape — structurally\n * compatible with `@tangle-network/agent-runtime`'s `OpenAIChatTool` without\n * importing it (keeps this package runtime-free). */\nexport interface OpenAIFunctionTool {\n type: 'function'\n function: {\n name: string\n description: string\n parameters: Record<string, unknown>\n }\n}\n\n/**\n * Build the four app tools in OpenAI function-tool shape. `submit_proposal`'s\n * `type` enum is the product's {@link AppToolTaxonomy.proposalTypes}; the other\n * three are fixed. Pass the result to the agent-runtime backend's `tools`.\n */\nexport function buildAppToolOpenAITools(taxonomy: AppToolTaxonomy): OpenAIFunctionTool[] {\n return [\n {\n type: 'function',\n function: {\n name: 'submit_proposal',\n description:\n 'Route a regulated or state-changing action to a human for approval (a recommendation, contacting/soliciting a contact, outreach, a record/account change, scheduling). Queues it for a named certified human to approve before it executes.',\n parameters: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: [...taxonomy.proposalTypes] },\n title: { type: 'string', description: 'Short label for the approval queue.' },\n description: { type: 'string', description: 'The full drafted message/recommendation, with sources.' },\n },\n required: ['type', 'title'],\n },\n },\n },\n {\n type: 'function',\n function: {\n name: 'schedule_followup',\n description: 'Register a dated cadence step (a reminder, chase, or check-in) on the follow-up calendar. Executes immediately.',\n parameters: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n dueDate: { type: 'string', description: 'ISO date YYYY-MM-DD.' },\n priority: { type: 'string', enum: ['low', 'medium', 'high'] },\n },\n required: ['title', 'dueDate'],\n },\n },\n },\n {\n type: 'function',\n function: {\n name: 'render_ui',\n description: 'Show a generated view live in the workspace. Validates the OpenUI JSON and persists the artifact. Executes immediately.',\n parameters: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n schema: { type: 'object', description: 'The OpenUI JSON object.' },\n },\n required: ['title', 'schema'],\n },\n },\n },\n {\n type: 'function',\n function: {\n name: 'add_citation',\n description: 'Anchor a grounding reference: the exact quote from a file backing a figure or claim. Verifies the quote appears in the file. Executes immediately.',\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'The vault file path.' },\n quote: { type: 'string', description: 'The exact text from it.' },\n },\n required: ['path', 'quote'],\n },\n },\n },\n ]\n}\n","import { ToolInputError } from './errors'\nimport { isAppToolName } from './openai'\nimport type {\n AppToolContext,\n AppToolHandlers,\n AppToolOutcome,\n AppToolProducedEvent,\n AppToolTaxonomy,\n} from './types'\n\nexport interface DispatchOptions {\n handlers: AppToolHandlers\n taxonomy: AppToolTaxonomy\n /** Called at the real side-effect site for proposals (proposal_created) and\n * generated views (artifact) so a consumer's completion oracle credits\n * persisted state. Omit when produced state isn't tracked. */\n onProduced?: (event: AppToolProducedEvent) => void\n}\n\n/**\n * The ONE place an app-tool call is validated, dispatched to the product's\n * handler, and turned into an {@link AppToolOutcome} + produced events. Shared\n * by the HTTP route layer and the agent-runtime executor so both paths apply\n * identical validation and identical side effects. A {@link ToolInputError}\n * (bad input the agent can correct) and any other throw both become\n * `{ ok: false }` — a tool call never silently \"succeeds\" without its effect.\n */\nexport async function dispatchAppTool(\n toolName: string,\n rawArgs: Record<string, unknown>,\n ctx: AppToolContext,\n opts: DispatchOptions,\n): Promise<AppToolOutcome> {\n try {\n if (!isAppToolName(toolName)) {\n return { ok: false, code: 'unknown_tool', message: `${toolName} is not an app tool.` }\n }\n\n if (toolName === 'submit_proposal') {\n const type = String(rawArgs.type ?? '').trim()\n const title = String(rawArgs.title ?? '').trim()\n if (!type || !opts.taxonomy.proposalTypes.includes(type)) {\n return { ok: false, code: 'invalid_type', message: `type must be one of: ${opts.taxonomy.proposalTypes.join(', ')}.` }\n }\n if (!title) return { ok: false, code: 'missing_title', message: 'title is required.' }\n const description = rawArgs.description == null ? null : String(rawArgs.description)\n const r = await opts.handlers.submitProposal({ type, title, description }, ctx)\n const regulated = opts.taxonomy.regulatedTypes.includes(type)\n opts.onProduced?.({ type: 'proposal_created', proposalId: r.proposalId, title, status: 'pending' })\n return { ok: true, result: { status: 'queued_for_approval', proposalId: r.proposalId, deduped: r.deduped, regulated } }\n }\n\n if (toolName === 'schedule_followup') {\n const r = await opts.handlers.scheduleFollowup(\n { title: String(rawArgs.title ?? ''), dueDate: String(rawArgs.dueDate ?? ''), priority: rawArgs.priority as string | undefined },\n ctx,\n )\n return { ok: true, result: { followupId: r.id, dueDate: r.dueDate, deduped: r.deduped } }\n }\n\n if (toolName === 'render_ui') {\n const r = await opts.handlers.renderUi({ title: String(rawArgs.title ?? ''), schema: rawArgs.schema }, ctx)\n opts.onProduced?.({ type: 'artifact', path: r.path, content: r.content })\n return { ok: true, result: { path: r.path } }\n }\n\n // add_citation\n const r = await opts.handlers.addCitation(\n { path: String(rawArgs.path ?? ''), quote: String(rawArgs.quote ?? ''), label: rawArgs.label as string | undefined },\n ctx,\n )\n return { ok: true, result: { citationId: r.citationId, path: r.path } }\n } catch (err) {\n if (err instanceof ToolInputError) return { ok: false, code: err.code, message: err.message, status: err.status }\n return { ok: false, code: 'app_tool_error', message: err instanceof Error ? err.message : String(err), status: 500 }\n }\n}\n\n/** HTTP status for a failed outcome — the handler's `ToolInputError.status`\n * when present, else 400 for a validation reject. */\nexport function outcomeStatus(outcome: Extract<AppToolOutcome, { ok: false }>): number {\n return outcome.status ?? 400\n}\n","import { dispatchAppTool, type DispatchOptions } from './dispatch'\nimport type { AppToolContext, AppToolOutcome } from './types'\n\n/** Executes an app-tool call the model emits on the agent-runtime chat path.\n * Plug into `runChatThroughRuntime({ appToolExecutor })` (or any loop that\n * dispatches function tool_calls). */\nexport type AppToolRuntimeExecutor = (call: {\n toolName: string\n args: Record<string, unknown>\n}) => Promise<AppToolOutcome>\n\nexport interface RuntimeExecutorOptions extends DispatchOptions {\n /** The trusted per-turn context — supplied directly (not from headers), since\n * the runtime path has no HTTP request. */\n ctx: AppToolContext\n}\n\n/**\n * Build the runtime executor for one turn. The agent-runtime backend must also\n * advertise the tools (`buildAppToolOpenAITools(taxonomy)` on the backend's\n * `tools`) for the model to call them; this executor fulfils each call against\n * the product's handlers and emits produced events via `opts.onProduced`.\n */\nexport function createAppToolRuntimeExecutor(opts: RuntimeExecutorOptions): AppToolRuntimeExecutor {\n return ({ toolName, args }) => dispatchAppTool(toolName, args, opts.ctx, opts)\n}\n","import { authenticateToolRequest, type ToolHeaderNames } from './auth'\nimport { dispatchAppTool, outcomeStatus, type DispatchOptions } from './dispatch'\nimport type { AppToolName } from './openai'\n\nexport interface HandleToolRequestOptions extends DispatchOptions {\n /** Which app tool this route serves. */\n tool: AppToolName\n /** Verify the bearer capability token belongs to the header user. */\n verifyToken: (userId: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n /** Optional success-message builder for a friendlier tool result. */\n message?: (result: unknown) => string\n}\n\n/**\n * Handle one app-tool HTTP request end to end — the sandbox MCP path. The\n * agent's per-turn HTTP MCP server POSTs here; this authenticates (header user\n * + capability token), reads the args (MCP-alias tolerant), dispatches to the\n * product handler, and returns a JSON Response. A product's route file becomes\n * a one-liner: `export const action = ({ request }) => handleAppToolRequest(request, cfg)`.\n */\nexport async function handleAppToolRequest(request: Request, opts: HandleToolRequestOptions): Promise<Response> {\n if (request.method !== 'POST') return Response.json({ error: 'Method not allowed' }, { status: 405 })\n\n const auth = await authenticateToolRequest(request, { verifyToken: opts.verifyToken, headerNames: opts.headerNames })\n if (!auth.ok) return auth.response\n\n let body: { args?: Record<string, unknown>; arguments?: Record<string, unknown> } & Record<string, unknown>\n try {\n body = (await request.json()) as typeof body\n } catch {\n return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n const args = (body.args ?? body.arguments ?? body) as Record<string, unknown>\n\n const outcome = await dispatchAppTool(opts.tool, args, auth.ctx, opts)\n if (!outcome.ok) {\n return Response.json({ error: outcome.code, message: outcome.message }, { status: outcomeStatus(outcome) })\n }\n const payload = outcome.result as Record<string, unknown>\n return Response.json({ ok: true, ...payload, ...(opts.message ? { message: opts.message(outcome.result) } : {}) })\n}\n","import type { AppToolContext } from './types'\nimport type { AppToolName } from './openai'\nimport type { ToolHeaderNames } from './auth'\nimport { DEFAULT_HEADER_NAMES } from './auth'\n\n/** Default route path each app tool is served at. A product mounts its routes\n * at these paths (or supplies its own via {@link BuildMcpServerOptions.paths}). */\nexport const DEFAULT_APP_TOOL_PATHS: Record<AppToolName, string> = {\n submit_proposal: '/api/tools/propose',\n schedule_followup: '/api/tools/followup',\n render_ui: '/api/tools/render-ui',\n add_citation: '/api/tools/citation',\n}\n\n/** The portable MCP server entry the sandbox SDK accepts (transport + url +\n * headers). Matches `AgentProfileMcpServer` structurally without importing the\n * sandbox SDK — products spread it into their profile's `mcp` map. */\nexport interface AppToolMcpServer {\n transport: 'http'\n url: string\n headers: Record<string, string>\n enabled: true\n metadata: { description: string }\n}\n\nexport interface BuildHttpMcpServerOptions {\n /** Route path on the app the sandbox POSTs to (e.g. `/api/tools/propose`). */\n path: string\n /** App base URL the sandbox reaches back to (no trailing slash required). */\n baseUrl: string\n /** Per-user capability token, baked into the Authorization header. */\n token: string\n ctx: AppToolContext\n /** Tool description the model sees. */\n description: string\n headerNames?: ToolHeaderNames\n}\n\n/**\n * Build ONE HTTP MCP server entry — the generic agent→app bridge. The\n * capability token + the user/workspace/thread ids ride in server-set headers\n * (never tool args), so the model can't forge identity or target another\n * workspace. Workspace/thread headers are omitted when their `ctx` value is\n * empty/null (e.g. an integration-invoke bridge that's user-scoped only). Used\n * directly for non-app-tool bridges (integration_invoke) and via\n * {@link buildAppToolMcpServer} for the four app tools.\n */\nexport function buildHttpMcpServer(opts: BuildHttpMcpServerOptions): AppToolMcpServer {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const h = opts.headerNames ?? DEFAULT_HEADER_NAMES\n return {\n transport: 'http',\n url: `${base}${opts.path}`,\n headers: {\n Authorization: `Bearer ${opts.token}`,\n [h.userId]: opts.ctx.userId,\n ...(opts.ctx.workspaceId ? { [h.workspaceId]: opts.ctx.workspaceId } : {}),\n ...(opts.ctx.threadId ? { [h.threadId]: opts.ctx.threadId } : {}),\n 'Content-Type': 'application/json',\n },\n enabled: true,\n metadata: { description: opts.description },\n }\n}\n\nexport interface BuildMcpServerOptions {\n tool: AppToolName\n baseUrl: string\n token: string\n ctx: AppToolContext\n description: string\n headerNames?: ToolHeaderNames\n paths?: Partial<Record<AppToolName, string>>\n}\n\n/** Build one of the four app-tool MCP servers — a thin wrapper over\n * {@link buildHttpMcpServer} that maps the tool name to its route path. */\nexport function buildAppToolMcpServer(opts: BuildMcpServerOptions): AppToolMcpServer {\n return buildHttpMcpServer({\n path: opts.paths?.[opts.tool] ?? DEFAULT_APP_TOOL_PATHS[opts.tool],\n baseUrl: opts.baseUrl,\n token: opts.token,\n ctx: opts.ctx,\n description: opts.description,\n headerNames: opts.headerNames,\n })\n}\n"],"mappings":"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","names":["r"]}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// src/runtime/model.ts
|
|
2
|
+
var DEFAULT_TANGLE_ROUTER_BASE_URL = "https://router.tangle.tools/v1";
|
|
3
|
+
function requireEnv(env, name) {
|
|
4
|
+
const value = env[name]?.trim();
|
|
5
|
+
if (!value) throw new Error(`${name} is required`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function resolveTangleModelConfig(opts = {}) {
|
|
9
|
+
const env = opts.env ?? process.env;
|
|
10
|
+
const provider = env.MODEL_PROVIDER?.trim() || "openai-compat";
|
|
11
|
+
const model = requireEnv(env, "MODEL_NAME");
|
|
12
|
+
if (provider === "openai-compat" || provider === "tangle-router" || provider === "tcloud") {
|
|
13
|
+
return {
|
|
14
|
+
provider: "openai-compat",
|
|
15
|
+
model,
|
|
16
|
+
apiKey: requireEnv(env, "TANGLE_API_KEY"),
|
|
17
|
+
baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\/+$/, "")
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (provider === "anthropic") {
|
|
21
|
+
return {
|
|
22
|
+
provider,
|
|
23
|
+
model,
|
|
24
|
+
apiKey: requireEnv(env, "ANTHROPIC_API_KEY"),
|
|
25
|
+
baseUrl: requireEnv(env, "ANTHROPIC_BASE_URL")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/runtime/openai-stream.ts
|
|
32
|
+
async function* toLoopEvents(chunks) {
|
|
33
|
+
const calls = /* @__PURE__ */ new Map();
|
|
34
|
+
for await (const chunk of chunks) {
|
|
35
|
+
const choice = chunk.choices?.[0];
|
|
36
|
+
if (!choice) continue;
|
|
37
|
+
const content = choice.delta?.content;
|
|
38
|
+
if (content) yield { type: "text", text: content };
|
|
39
|
+
for (const tc of choice.delta?.tool_calls ?? []) {
|
|
40
|
+
const cur = calls.get(tc.index) ?? { name: "", args: "" };
|
|
41
|
+
if (tc.id) cur.id = tc.id;
|
|
42
|
+
if (tc.function?.name) cur.name += tc.function.name;
|
|
43
|
+
if (tc.function?.arguments) cur.args += tc.function.arguments;
|
|
44
|
+
calls.set(tc.index, cur);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {
|
|
48
|
+
if (!c.name) continue;
|
|
49
|
+
yield { type: "tool_call", call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function safeParse(s) {
|
|
53
|
+
if (!s.trim()) return {};
|
|
54
|
+
try {
|
|
55
|
+
const v = JSON.parse(s);
|
|
56
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function createOpenAICompatStreamTurn(opts) {
|
|
62
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
63
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
64
|
+
return (messages) => toLoopEvents(
|
|
65
|
+
streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {
|
|
66
|
+
model: opts.model,
|
|
67
|
+
messages,
|
|
68
|
+
stream: true,
|
|
69
|
+
...opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {},
|
|
70
|
+
...opts.temperature != null ? { temperature: opts.temperature } : {},
|
|
71
|
+
...opts.extraBody
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
async function* streamChatCompletions(doFetch, url, apiKey, body) {
|
|
76
|
+
const res = await doFetch(url, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
79
|
+
body: JSON.stringify(body)
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok || !res.body) {
|
|
82
|
+
const text = res.body ? await res.text().catch(() => "") : "";
|
|
83
|
+
throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ""}`);
|
|
84
|
+
}
|
|
85
|
+
const reader = res.body.getReader();
|
|
86
|
+
const decoder = new TextDecoder();
|
|
87
|
+
let buffer = "";
|
|
88
|
+
for (; ; ) {
|
|
89
|
+
const { done, value } = await reader.read();
|
|
90
|
+
if (done) break;
|
|
91
|
+
buffer += decoder.decode(value, { stream: true });
|
|
92
|
+
const lines = buffer.split("\n");
|
|
93
|
+
buffer = lines.pop() ?? "";
|
|
94
|
+
for (const line of lines) {
|
|
95
|
+
const trimmed = line.trim();
|
|
96
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
97
|
+
const data = trimmed.slice(5).trim();
|
|
98
|
+
if (data === "[DONE]") return;
|
|
99
|
+
try {
|
|
100
|
+
yield JSON.parse(data);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/runtime/index.ts
|
|
108
|
+
var DEFAULT_MAX_TOOL_TURNS = 8;
|
|
109
|
+
function defaultRender(label, outcome) {
|
|
110
|
+
if (outcome.ok) return `- ${label} \u2192 ok: ${JSON.stringify(outcome.result)}`;
|
|
111
|
+
return `- ${label} \u2192 failed (${outcome.code}): ${outcome.message}`;
|
|
112
|
+
}
|
|
113
|
+
async function runAppToolLoop(opts) {
|
|
114
|
+
const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
|
|
115
|
+
const render = opts.renderResult ?? defaultRender;
|
|
116
|
+
const labelFor = opts.labelFor ?? ((c) => c.toolName);
|
|
117
|
+
const messages = [
|
|
118
|
+
{ role: "system", content: opts.systemPrompt },
|
|
119
|
+
...opts.priorMessages ?? [],
|
|
120
|
+
{ role: "user", content: opts.userMessage }
|
|
121
|
+
];
|
|
122
|
+
const toolResults = [];
|
|
123
|
+
let finalText = "";
|
|
124
|
+
let turns = 0;
|
|
125
|
+
for (let toolTurn = 0; ; toolTurn++) {
|
|
126
|
+
turns++;
|
|
127
|
+
let turnText = "";
|
|
128
|
+
const pending = [];
|
|
129
|
+
for await (const ev of opts.streamTurn([...messages])) {
|
|
130
|
+
if (ev.type === "text") {
|
|
131
|
+
turnText += ev.text;
|
|
132
|
+
finalText += ev.text;
|
|
133
|
+
} else if (ev.type === "tool_call" && opts.isExecutableTool(ev.call.toolName)) {
|
|
134
|
+
pending.push(ev.call);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (pending.length === 0) break;
|
|
138
|
+
if (toolTurn >= maxTurns) {
|
|
139
|
+
return { finalText, toolResults, turns, cappedOut: true };
|
|
140
|
+
}
|
|
141
|
+
if (turnText.trim()) messages.push({ role: "assistant", content: turnText });
|
|
142
|
+
const lines = [];
|
|
143
|
+
for (const call of pending) {
|
|
144
|
+
let outcome;
|
|
145
|
+
try {
|
|
146
|
+
outcome = await opts.executeToolCall(call);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
outcome = { ok: false, code: "executor_error", message: err instanceof Error ? err.message : String(err) };
|
|
149
|
+
}
|
|
150
|
+
const label = labelFor(call);
|
|
151
|
+
toolResults.push({ call, label, outcome });
|
|
152
|
+
lines.push(render(label, outcome));
|
|
153
|
+
}
|
|
154
|
+
messages.push({ role: "user", content: `Tool results:
|
|
155
|
+
${lines.join("\n")}` });
|
|
156
|
+
}
|
|
157
|
+
return { finalText, toolResults, turns, cappedOut: false };
|
|
158
|
+
}
|
|
159
|
+
async function* streamAppToolLoop(opts) {
|
|
160
|
+
const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
|
|
161
|
+
const render = opts.renderResult ?? defaultRender;
|
|
162
|
+
const labelFor = opts.labelFor ?? ((c) => c.toolName);
|
|
163
|
+
const messages = [
|
|
164
|
+
{ role: "system", content: opts.systemPrompt },
|
|
165
|
+
...opts.priorMessages ?? [],
|
|
166
|
+
{ role: "user", content: opts.userMessage }
|
|
167
|
+
];
|
|
168
|
+
for (let toolTurn = 0; ; toolTurn++) {
|
|
169
|
+
let turnText = "";
|
|
170
|
+
const pending = [];
|
|
171
|
+
for await (const event of opts.streamTurn([...messages])) {
|
|
172
|
+
yield { kind: "event", event };
|
|
173
|
+
turnText += opts.extractText(event);
|
|
174
|
+
const call = opts.extractToolCall(event);
|
|
175
|
+
if (call && opts.isExecutableTool(call.toolName)) pending.push(call);
|
|
176
|
+
}
|
|
177
|
+
if (pending.length === 0) return;
|
|
178
|
+
if (toolTurn >= maxTurns) {
|
|
179
|
+
yield { kind: "capped", pending: pending.length };
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (turnText.trim()) messages.push({ role: "assistant", content: turnText });
|
|
183
|
+
const lines = [];
|
|
184
|
+
for (const call of pending) {
|
|
185
|
+
let outcome;
|
|
186
|
+
try {
|
|
187
|
+
outcome = await opts.executeToolCall(call);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
outcome = { ok: false, code: "executor_error", message: err instanceof Error ? err.message : String(err) };
|
|
190
|
+
}
|
|
191
|
+
const label = labelFor(call);
|
|
192
|
+
yield { kind: "tool_result", toolName: call.toolName, toolCallId: call.toolCallId, label, outcome };
|
|
193
|
+
lines.push(render(label, outcome));
|
|
194
|
+
}
|
|
195
|
+
messages.push({ role: "user", content: `Tool results:
|
|
196
|
+
${lines.join("\n")}` });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export {
|
|
201
|
+
DEFAULT_TANGLE_ROUTER_BASE_URL,
|
|
202
|
+
resolveTangleModelConfig,
|
|
203
|
+
toLoopEvents,
|
|
204
|
+
createOpenAICompatStreamTurn,
|
|
205
|
+
runAppToolLoop,
|
|
206
|
+
streamAppToolLoop
|
|
207
|
+
};
|
|
208
|
+
//# sourceMappingURL=chunk-FS5OUVRB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runtime/model.ts","../src/runtime/openai-stream.ts","../src/runtime/index.ts"],"sourcesContent":["/**\n * Resolve the model config a Tangle agent's sandbox/runtime runs on.\n *\n * Every Tangle agent product resolves the SAME thing from env: the Tangle Router\n * (OpenAI-compatible, metered at the platform markup against a single\n * `TANGLE_API_KEY`) by default, with a direct-Anthropic BYOK escape hatch. The\n * shape feeds the sandbox SDK's `backend.model`. Lifted here so no product\n * hand-rolls the env parsing + the router default.\n */\n\nexport interface TangleModelConfig {\n /** The Tangle Router is OpenAI-compatible → driven via `openai-compat`.\n * `anthropic` is the BYOK escape hatch. */\n provider: 'openai-compat' | 'anthropic'\n model: string\n apiKey: string\n baseUrl: string\n}\n\nexport interface ResolveModelOptions {\n /** Env to read (defaults to process.env). */\n env?: Record<string, string | undefined>\n /** Router base URL default when `TANGLE_ROUTER_BASE_URL` is unset. */\n defaultRouterBaseUrl?: string\n}\n\nexport const DEFAULT_TANGLE_ROUTER_BASE_URL = 'https://router.tangle.tools/v1'\n\nfunction requireEnv(env: Record<string, string | undefined>, name: string): string {\n const value = env[name]?.trim()\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\n/**\n * Resolve the model config from env. DEFAULT path (`MODEL_PROVIDER` unset or\n * `openai-compat`/`tangle-router`/`tcloud`): the Tangle Router, authenticated\n * with `TANGLE_API_KEY`, model from `MODEL_NAME`. BYOK path\n * (`MODEL_PROVIDER=anthropic`): direct Anthropic with `ANTHROPIC_API_KEY` +\n * `ANTHROPIC_BASE_URL`. Throws (fail-loud) on a missing required var so a\n * misconfigured deploy fails at boot, not mid-turn.\n */\nexport function resolveTangleModelConfig(opts: ResolveModelOptions = {}): TangleModelConfig {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const provider = env.MODEL_PROVIDER?.trim() || 'openai-compat'\n const model = requireEnv(env, 'MODEL_NAME')\n\n if (provider === 'openai-compat' || provider === 'tangle-router' || provider === 'tcloud') {\n return {\n provider: 'openai-compat',\n model,\n apiKey: requireEnv(env, 'TANGLE_API_KEY'),\n baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\\/+$/, ''),\n }\n }\n\n if (provider === 'anthropic') {\n return {\n provider,\n model,\n apiKey: requireEnv(env, 'ANTHROPIC_API_KEY'),\n baseUrl: requireEnv(env, 'ANTHROPIC_BASE_URL'),\n }\n }\n\n throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`)\n}\n","/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopToolCall } from './index'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n choices?: Array<{\n delta?: {\n content?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '' }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: Array<{ role: string; content: string }>) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }),\n )\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n try {\n yield JSON.parse(data) as OpenAIStreamChunk\n } catch {\n /* skip a partial/garbled SSE frame */\n }\n }\n }\n}\n","export * from './model'\nexport * from './openai-stream'\n/**\n * The bounded agent tool-loop — the mechanism every app's chat runtime\n * hand-rolls on top of `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop: stream a turn, collect the executable tool\n * calls, stop if there are none / no executor / the turn cap is hit, otherwise\n * execute each, fold the results back as a message, and re-run so the model\n * reads them. Bounded by `maxToolTurns` so a model looping on a failing action\n * can't run forever.\n *\n * Substrate-free by design: the app supplies `streamTurn` (wrapping whatever\n * backend / `runAgentTaskStream` it uses) and `executeToolCall` (routing to its\n * integration + app-tool executors). This package owns the LOOP; the app owns\n * the model and the executors.\n *\n * LAYERING NOTE: this turn-level tool-dispatch loop is a generic RUNTIME\n * capability. It has been CONTRIBUTED DOWN and MERGED into\n * `@tangle-network/agent-runtime` as `runToolLoop` / `streamToolLoop` (PR #137),\n * but is not yet PUBLISHED (agent-runtime main is ahead of its last npm release;\n * cutting that release is the agent-runtime maintainer's call). TERMINAL STATE:\n * the moment agent-runtime publishes a version carrying #137, bump the\n * `@tangle-network/agent-runtime` peer-dep here and replace the bodies below with\n * a thin re-export — `streamAppToolLoop = streamToolLoop`, `runAppToolLoop =\n * runToolLoop` (types alias 1:1; `AppToolOutcome` ≡ `ToolCallOutcome`). Kept\n * substrate-free + shipping until then so consumers aren't blocked on the release.\n */\nimport type { AppToolOutcome } from '../tools/types'\n\nexport interface LoopToolCall {\n toolCallId?: string\n toolName: string\n args: Record<string, unknown>\n}\n\n/** Events a turn stream yields. `text` accumulates into the final answer;\n * `tool_call` is collected for dispatch. Extra event types pass through\n * untouched (the caller re-emits them to its own UI stream). */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'tool_call'; call: LoopToolCall }\n | { type: 'other'; event: unknown }\n\nexport interface ToolLoopResult {\n /** The model's final text across the loop. */\n finalText: string\n /** Every tool call executed, with its outcome, in order. */\n toolResults: Array<{ call: LoopToolCall; label: string; outcome: AppToolOutcome }>\n /** Number of model turns run (1 + tool-driven re-runs). */\n turns: number\n /** True when the loop stopped because it hit `maxToolTurns` with calls still pending. */\n cappedOut: boolean\n}\n\nexport interface AppToolLoopOptions {\n systemPrompt: string\n userMessage: string\n priorMessages?: Array<{ role: string; content: string }>\n /** Stream one model turn over the running message list. The app wraps its\n * backend here. */\n streamTurn: (messages: Array<{ role: string; content: string }>) => AsyncIterable<LoopEvent>\n /** Execute one tool call. The app routes to its integration executor / app-tool\n * executor and returns the outcome. */\n executeToolCall: (call: LoopToolCall) => Promise<AppToolOutcome>\n /** Which emitted tool names are executable (others are ignored — e.g. a UI-only\n * tool the app renders but doesn't run here). */\n isExecutableTool: (toolName: string) => boolean\n /** Max tool-driven re-runs. Default 8. */\n maxToolTurns?: number\n /** Render one tool outcome as a line the next turn's message carries. Default\n * is a compact `- <label> → ok/failed: …`. */\n renderResult?: (label: string, outcome: AppToolOutcome) => string\n /** Map a tool call to the label its result is keyed under (default: toolName). */\n labelFor?: (call: LoopToolCall) => string\n}\n\nconst DEFAULT_MAX_TOOL_TURNS = 8\n\nfunction defaultRender(label: string, outcome: AppToolOutcome): string {\n if (outcome.ok) return `- ${label} → ok: ${JSON.stringify(outcome.result)}`\n return `- ${label} → failed (${outcome.code}): ${outcome.message}`\n}\n\n/**\n * Run the bounded tool loop and return the final text + every executed tool\n * outcome. Yields nothing — it's an awaitable driver; callers that need to\n * re-emit events to a UI stream should do so inside `streamTurn`. (A streaming\n * variant can wrap this later; keeping the core awaitable makes it trivially\n * testable.)\n */\nexport async function runAppToolLoop(opts: AppToolLoopOptions): Promise<ToolLoopResult> {\n const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: LoopToolCall) => c.toolName)\n\n const messages: Array<{ role: string; content: string }> = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n\n const toolResults: ToolLoopResult['toolResults'] = []\n let finalText = ''\n let turns = 0\n\n for (let toolTurn = 0; ; toolTurn++) {\n turns++\n let turnText = ''\n const pending: LoopToolCall[] = []\n\n for await (const ev of opts.streamTurn([...messages])) {\n if (ev.type === 'text') {\n turnText += ev.text\n finalText += ev.text\n } else if (ev.type === 'tool_call' && opts.isExecutableTool(ev.call.toolName)) {\n pending.push(ev.call)\n }\n }\n\n if (pending.length === 0) break\n if (toolTurn >= maxTurns) {\n return { finalText, toolResults, turns, cappedOut: true }\n }\n\n // Record the assistant's tool-calling turn so the next turn has its context.\n if (turnText.trim()) messages.push({ role: 'assistant', content: turnText })\n\n const lines: string[] = []\n for (const call of pending) {\n let outcome: AppToolOutcome\n try {\n outcome = await opts.executeToolCall(call)\n } catch (err) {\n outcome = { ok: false, code: 'executor_error', message: err instanceof Error ? err.message : String(err) }\n }\n const label = labelFor(call)\n toolResults.push({ call, label, outcome })\n lines.push(render(label, outcome))\n }\n // Fold every outcome back as one user-role message so the model reads them.\n messages.push({ role: 'user', content: `Tool results:\\n${lines.join('\\n')}` })\n }\n\n return { finalText, toolResults, turns, cappedOut: false }\n}\n\n// ── Streaming variant ──────────────────────────────────────────────────────\n//\n// `runAppToolLoop` is awaitable — perfect for tests and drain-only callers. A\n// real chat runtime instead needs to STREAM each model event to the client (SSE)\n// AND record telemetry per event as it happens. `streamAppToolLoop` is the same\n// bounded loop as an async generator: it yields every raw turn event (the app\n// maps + telemetries + re-emits it) and every executed tool result (same), while\n// owning the loop control flow (collect → stop/dispatch → fold → re-run, capped).\n// `Raw` is the app's own runtime-event type — this package stays substrate-free.\n\nexport type StreamLoopYield<Raw> =\n | { kind: 'event'; event: Raw }\n | { kind: 'tool_result'; toolName: string; toolCallId?: string; label: string; outcome: AppToolOutcome }\n | { kind: 'capped'; pending: number }\n\nexport interface StreamAppToolLoopOptions<Raw> {\n systemPrompt: string\n userMessage: string\n priorMessages?: Array<{ role: string; content: string }>\n /** Stream one model turn (the app wraps its backend / runAgentTaskStream). */\n streamTurn: (messages: Array<{ role: string; content: string }>) => AsyncIterable<Raw>\n /** Text contribution of a raw event, '' if none — used to record the\n * assistant's turn so the next turn has its context. */\n extractText: (event: Raw) => string\n /** The tool call a raw event represents, or null. */\n extractToolCall: (event: Raw) => LoopToolCall | null\n /** Which tool names are executable here (others pass through, unexecuted). */\n isExecutableTool: (toolName: string) => boolean\n /** Execute one call — the app routes to its integration / app-tool executor. */\n executeToolCall: (call: LoopToolCall) => Promise<AppToolOutcome>\n maxToolTurns?: number\n renderResult?: (label: string, outcome: AppToolOutcome) => string\n labelFor?: (call: LoopToolCall) => string\n}\n\n/**\n * The streaming bounded tool loop. Yields `event` for each raw turn event and\n * `tool_result` for each executed tool; emits a single `capped` when it stops at\n * the turn limit with calls still pending. The app drives telemetry + UI\n * emission off the yielded items.\n */\nexport async function* streamAppToolLoop<Raw>(opts: StreamAppToolLoopOptions<Raw>): AsyncGenerator<StreamLoopYield<Raw>, void, unknown> {\n const maxTurns = opts.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS\n const render = opts.renderResult ?? defaultRender\n const labelFor = opts.labelFor ?? ((c: LoopToolCall) => c.toolName)\n\n const messages: Array<{ role: string; content: string }> = [\n { role: 'system', content: opts.systemPrompt },\n ...(opts.priorMessages ?? []),\n { role: 'user', content: opts.userMessage },\n ]\n\n for (let toolTurn = 0; ; toolTurn++) {\n let turnText = ''\n const pending: LoopToolCall[] = []\n\n for await (const event of opts.streamTurn([...messages])) {\n yield { kind: 'event', event }\n turnText += opts.extractText(event)\n const call = opts.extractToolCall(event)\n if (call && opts.isExecutableTool(call.toolName)) pending.push(call)\n }\n\n if (pending.length === 0) return\n if (toolTurn >= maxTurns) {\n yield { kind: 'capped', pending: pending.length }\n return\n }\n\n if (turnText.trim()) messages.push({ role: 'assistant', content: turnText })\n\n const lines: string[] = []\n for (const call of pending) {\n let outcome: AppToolOutcome\n try {\n outcome = await opts.executeToolCall(call)\n } catch (err) {\n outcome = { ok: false, code: 'executor_error', message: err instanceof Error ? err.message : String(err) }\n }\n const label = labelFor(call)\n yield { kind: 'tool_result', toolName: call.toolName, toolCallId: call.toolCallId, label, outcome }\n lines.push(render(label, outcome))\n }\n messages.push({ role: 'user', content: `Tool results:\\n${lines.join('\\n')}` })\n }\n}\n"],"mappings":";AA0BO,IAAM,iCAAiC;AAE9C,SAAS,WAAW,KAAyC,MAAsB;AACjF,QAAM,QAAQ,IAAI,IAAI,GAAG,KAAK;AAC9B,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,GAAG,IAAI,cAAc;AACjD,SAAO;AACT;AAUO,SAAS,yBAAyB,OAA4B,CAAC,GAAsB;AAC1F,QAAM,MAAM,KAAK,OAAQ,QAAQ;AACjC,QAAM,WAAW,IAAI,gBAAgB,KAAK,KAAK;AAC/C,QAAM,QAAQ,WAAW,KAAK,YAAY;AAE1C,MAAI,aAAa,mBAAmB,aAAa,mBAAmB,aAAa,UAAU;AACzF,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,QAAQ,WAAW,KAAK,gBAAgB;AAAA,MACxC,UAAU,IAAI,wBAAwB,KAAK,KAAK,KAAK,wBAAwB,gCAAgC,QAAQ,QAAQ,EAAE;AAAA,IACjI;AAAA,EACF;AAEA,MAAI,aAAa,aAAa;AAC5B,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ,WAAW,KAAK,mBAAmB;AAAA,MAC3C,SAAS,WAAW,KAAK,oBAAoB;AAAA,IAC/C;AAAA,EACF;AAEA,QAAM,IAAI,MAAM,+BAA+B,QAAQ,mEAAmE;AAC5H;;;ACpBA,gBAAuB,aAAa,QAAoE;AACtG,QAAM,QAAQ,oBAAI,IAA6B;AAC/C,mBAAiB,SAAS,QAAQ;AAChC,UAAM,SAAS,MAAM,UAAU,CAAC;AAChC,QAAI,CAAC,OAAQ;AACb,UAAM,UAAU,OAAO,OAAO;AAC9B,QAAI,QAAS,OAAM,EAAE,MAAM,QAAQ,MAAM,QAAQ;AACjD,eAAW,MAAM,OAAO,OAAO,cAAc,CAAC,GAAG;AAC/C,YAAM,MAAM,MAAM,IAAI,GAAG,KAAK,KAAK,EAAE,MAAM,IAAI,MAAM,GAAG;AACxD,UAAI,GAAG,GAAI,KAAI,KAAK,GAAG;AACvB,UAAI,GAAG,UAAU,KAAM,KAAI,QAAQ,GAAG,SAAS;AAC/C,UAAI,GAAG,UAAU,UAAW,KAAI,QAAQ,GAAG,SAAS;AACpD,YAAM,IAAI,GAAG,OAAO,GAAG;AAAA,IACzB;AAAA,EACF;AACA,aAAW,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,MAAM,QAAQ,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG;AACpE,QAAI,CAAC,EAAE,KAAM;AACb,UAAM,EAAE,MAAM,aAAa,MAAM,EAAE,YAAY,EAAE,IAAI,UAAU,EAAE,MAAM,MAAM,UAAU,EAAE,IAAI,EAAE,EAAyB;AAAA,EAC1H;AACF;AAEA,SAAS,UAAU,GAAoC;AACrD,MAAI,CAAC,EAAE,KAAK,EAAG,QAAO,CAAC;AACvB,MAAI;AACF,UAAM,IAAI,KAAK,MAAM,CAAC;AACtB,WAAO,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC,IAAK,IAAgC,CAAC;AAAA,EAC7F,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAyBO,SAAS,6BACd,MACkF;AAClF,QAAM,OAAO,KAAK,QAAQ,QAAQ,QAAQ,EAAE;AAC5C,QAAM,UAAU,KAAK,aAAa;AAClC,SAAO,CAAC,aACN;AAAA,IACE,sBAAsB,SAAS,GAAG,IAAI,qBAAqB,KAAK,QAAQ;AAAA,MACtE,OAAO,KAAK;AAAA,MACZ;AAAA,MACA,QAAQ;AAAA,MACR,GAAI,KAAK,SAAS,KAAK,MAAM,SAAS,IAAI,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;AAAA,MACnE,GAAI,KAAK,eAAe,OAAO,EAAE,aAAa,KAAK,YAAY,IAAI,CAAC;AAAA,MACpE,GAAG,KAAK;AAAA,IACV,CAAC;AAAA,EACH;AACJ;AAIA,gBAAgB,sBACd,SACA,KACA,QACA,MACkC;AAClC,QAAM,MAAM,MAAM,QAAQ,KAAK;AAAA,IAC7B,QAAQ;AAAA,IACR,SAAS,EAAE,eAAe,UAAU,MAAM,IAAI,gBAAgB,oBAAoB,QAAQ,oBAAoB;AAAA,IAC9G,MAAM,KAAK,UAAU,IAAI;AAAA,EAC3B,CAAC;AACD,MAAI,CAAC,IAAI,MAAM,CAAC,IAAI,MAAM;AACxB,UAAM,OAAO,IAAI,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,MAAM,EAAE,IAAI;AAC3D,UAAM,IAAI,MAAM,qCAAqC,IAAI,MAAM,IAAI,OAAO,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC,KAAK,EAAE,EAAE;AAAA,EAC5G;AACA,QAAM,SAAS,IAAI,KAAK,UAAU;AAClC,QAAM,UAAU,IAAI,YAAY;AAChC,MAAI,SAAS;AACb,aAAS;AACP,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAC1C,QAAI,KAAM;AACV,cAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AAChD,UAAM,QAAQ,OAAO,MAAM,IAAI;AAC/B,aAAS,MAAM,IAAI,KAAK;AACxB,eAAW,QAAQ,OAAO;AACxB,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,CAAC,QAAQ,WAAW,OAAO,EAAG;AAClC,YAAM,OAAO,QAAQ,MAAM,CAAC,EAAE,KAAK;AACnC,UAAI,SAAS,SAAU;AACvB,UAAI;AACF,cAAM,KAAK,MAAM,IAAI;AAAA,MACvB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;AC9EA,IAAM,yBAAyB;AAE/B,SAAS,cAAc,OAAe,SAAiC;AACrE,MAAI,QAAQ,GAAI,QAAO,KAAK,KAAK,eAAU,KAAK,UAAU,QAAQ,MAAM,CAAC;AACzE,SAAO,KAAK,KAAK,mBAAc,QAAQ,IAAI,MAAM,QAAQ,OAAO;AAClE;AASA,eAAsB,eAAe,MAAmD;AACtF,QAAM,WAAW,KAAK,gBAAgB;AACtC,QAAM,SAAS,KAAK,gBAAgB;AACpC,QAAM,WAAW,KAAK,aAAa,CAAC,MAAoB,EAAE;AAE1D,QAAM,WAAqD;AAAA,IACzD,EAAE,MAAM,UAAU,SAAS,KAAK,aAAa;AAAA,IAC7C,GAAI,KAAK,iBAAiB,CAAC;AAAA,IAC3B,EAAE,MAAM,QAAQ,SAAS,KAAK,YAAY;AAAA,EAC5C;AAEA,QAAM,cAA6C,CAAC;AACpD,MAAI,YAAY;AAChB,MAAI,QAAQ;AAEZ,WAAS,WAAW,KAAK,YAAY;AACnC;AACA,QAAI,WAAW;AACf,UAAM,UAA0B,CAAC;AAEjC,qBAAiB,MAAM,KAAK,WAAW,CAAC,GAAG,QAAQ,CAAC,GAAG;AACrD,UAAI,GAAG,SAAS,QAAQ;AACtB,oBAAY,GAAG;AACf,qBAAa,GAAG;AAAA,MAClB,WAAW,GAAG,SAAS,eAAe,KAAK,iBAAiB,GAAG,KAAK,QAAQ,GAAG;AAC7E,gBAAQ,KAAK,GAAG,IAAI;AAAA,MACtB;AAAA,IACF;AAEA,QAAI,QAAQ,WAAW,EAAG;AAC1B,QAAI,YAAY,UAAU;AACxB,aAAO,EAAE,WAAW,aAAa,OAAO,WAAW,KAAK;AAAA,IAC1D;AAGA,QAAI,SAAS,KAAK,EAAG,UAAS,KAAK,EAAE,MAAM,aAAa,SAAS,SAAS,CAAC;AAE3E,UAAM,QAAkB,CAAC;AACzB,eAAW,QAAQ,SAAS;AAC1B,UAAI;AACJ,UAAI;AACF,kBAAU,MAAM,KAAK,gBAAgB,IAAI;AAAA,MAC3C,SAAS,KAAK;AACZ,kBAAU,EAAE,IAAI,OAAO,MAAM,kBAAkB,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,MAC3G;AACA,YAAM,QAAQ,SAAS,IAAI;AAC3B,kBAAY,KAAK,EAAE,MAAM,OAAO,QAAQ,CAAC;AACzC,YAAM,KAAK,OAAO,OAAO,OAAO,CAAC;AAAA,IACnC;AAEA,aAAS,KAAK,EAAE,MAAM,QAAQ,SAAS;AAAA,EAAkB,MAAM,KAAK,IAAI,CAAC,GAAG,CAAC;AAAA,EAC/E;AAEA,SAAO,EAAE,WAAW,aAAa,OAAO,WAAW,MAAM;AAC3D;AA2CA,gBAAuB,kBAAuB,MAA0F;AACtI,QAAM,WAAW,KAAK,gBAAgB;AACtC,QAAM,SAAS,KAAK,gBAAgB;AACpC,QAAM,WAAW,KAAK,aAAa,CAAC,MAAoB,EAAE;AAE1D,QAAM,WAAqD;AAAA,IACzD,EAAE,MAAM,UAAU,SAAS,KAAK,aAAa;AAAA,IAC7C,GAAI,KAAK,iBAAiB,CAAC;AAAA,IAC3B,EAAE,MAAM,QAAQ,SAAS,KAAK,YAAY;AAAA,EAC5C;AAEA,WAAS,WAAW,KAAK,YAAY;AACnC,QAAI,WAAW;AACf,UAAM,UAA0B,CAAC;AAEjC,qBAAiB,SAAS,KAAK,WAAW,CAAC,GAAG,QAAQ,CAAC,GAAG;AACxD,YAAM,EAAE,MAAM,SAAS,MAAM;AAC7B,kBAAY,KAAK,YAAY,KAAK;AAClC,YAAM,OAAO,KAAK,gBAAgB,KAAK;AACvC,UAAI,QAAQ,KAAK,iBAAiB,KAAK,QAAQ,EAAG,SAAQ,KAAK,IAAI;AAAA,IACrE;AAEA,QAAI,QAAQ,WAAW,EAAG;AAC1B,QAAI,YAAY,UAAU;AACxB,YAAM,EAAE,MAAM,UAAU,SAAS,QAAQ,OAAO;AAChD;AAAA,IACF;AAEA,QAAI,SAAS,KAAK,EAAG,UAAS,KAAK,EAAE,MAAM,aAAa,SAAS,SAAS,CAAC;AAE3E,UAAM,QAAkB,CAAC;AACzB,eAAW,QAAQ,SAAS;AAC1B,UAAI;AACJ,UAAI;AACF,kBAAU,MAAM,KAAK,gBAAgB,IAAI;AAAA,MAC3C,SAAS,KAAK;AACZ,kBAAU,EAAE,IAAI,OAAO,MAAM,kBAAkB,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE;AAAA,MAC3G;AACA,YAAM,QAAQ,SAAS,IAAI;AAC3B,YAAM,EAAE,MAAM,eAAe,UAAU,KAAK,UAAU,YAAY,KAAK,YAAY,OAAO,QAAQ;AAClG,YAAM,KAAK,OAAO,OAAO,OAAO,CAAC;AAAA,IACnC;AACA,aAAS,KAAK,EAAE,MAAM,QAAQ,SAAS;AAAA,EAAkB,MAAM,KAAK,IAAI,CAAC,GAAG,CAAC;AAAA,EAC/E;AACF;","names":[]}
|