@zihin/agent-client 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -6
- package/dist/index.d.mts +510 -0
- package/dist/index.d.ts +509 -20
- package/dist/index.js +1303 -32
- package/dist/index.mjs +1276 -0
- package/package.json +19 -4
- package/dist/agents.d.ts +0 -21
- package/dist/agents.js +0 -52
- package/dist/agents.js.map +0 -1
- package/dist/client.d.ts +0 -43
- package/dist/client.js +0 -75
- package/dist/client.js.map +0 -1
- package/dist/errors.d.ts +0 -35
- package/dist/errors.js +0 -89
- package/dist/errors.js.map +0 -1
- package/dist/http.d.ts +0 -98
- package/dist/http.js +0 -281
- package/dist/http.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts +0 -13
- package/dist/internal.js +0 -40
- package/dist/internal.js.map +0 -1
- package/dist/invoke.d.ts +0 -18
- package/dist/invoke.js +0 -233
- package/dist/invoke.js.map +0 -1
- package/dist/security.d.ts +0 -82
- package/dist/security.js +0 -251
- package/dist/security.js.map +0 -1
- package/dist/sse.d.ts +0 -27
- package/dist/sse.js +0 -138
- package/dist/sse.js.map +0 -1
- package/dist/triggers.d.ts +0 -43
- package/dist/triggers.js +0 -241
- package/dist/triggers.js.map +0 -1
- package/dist/types.d.ts +0 -268
- package/dist/types.js +0 -9
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,33 +1,1304 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_LIMITS: () => DEFAULT_LIMITS,
|
|
24
|
+
ZihinAgentClient: () => ZihinAgentClient,
|
|
25
|
+
ZihinAgentError: () => ZihinAgentError,
|
|
26
|
+
createClient: () => createClient,
|
|
27
|
+
sanitizeApiError: () => sanitizeApiError,
|
|
28
|
+
signRequest: () => signRequest,
|
|
29
|
+
validateApiKeyFormat: () => validateApiKeyFormat,
|
|
30
|
+
validateBaseUrl: () => validateBaseUrl,
|
|
31
|
+
validatePromptSize: () => validatePromptSize
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/internal.ts
|
|
36
|
+
var _utf8 = new TextEncoder();
|
|
37
|
+
function utf8Len(s) {
|
|
38
|
+
return _utf8.encode(s).length;
|
|
39
|
+
}
|
|
40
|
+
function rec(x) {
|
|
41
|
+
return x && typeof x === "object" ? x : {};
|
|
42
|
+
}
|
|
43
|
+
function str(x) {
|
|
44
|
+
return typeof x === "string" ? x : void 0;
|
|
45
|
+
}
|
|
46
|
+
function num(x) {
|
|
47
|
+
return typeof x === "number" ? x : void 0;
|
|
48
|
+
}
|
|
49
|
+
function parseUsage(usageRaw) {
|
|
50
|
+
const u = rec(usageRaw);
|
|
51
|
+
if (Object.keys(u).length === 0) return void 0;
|
|
52
|
+
return {
|
|
53
|
+
inputTokens: num(u.input_tokens) ?? num(u.prompt_tokens),
|
|
54
|
+
outputTokens: num(u.output_tokens) ?? num(u.completion_tokens),
|
|
55
|
+
totalTokens: num(u.total_tokens),
|
|
56
|
+
costUsd: num(u.cost_usd)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/security.ts
|
|
61
|
+
var PRIVATE_IP_PATTERNS = [
|
|
62
|
+
/^127\./,
|
|
63
|
+
// 127.0.0.0/8 (localhost)
|
|
64
|
+
/^10\./,
|
|
65
|
+
// 10.0.0.0/8 (private)
|
|
66
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
67
|
+
// 172.16.0.0/12 (private)
|
|
68
|
+
/^192\.168\./,
|
|
69
|
+
// 192.168.0.0/16 (private)
|
|
70
|
+
/^169\.254\./,
|
|
71
|
+
// 169.254.0.0/16 (link-local)
|
|
72
|
+
/^0\./
|
|
73
|
+
// 0.0.0.0/8
|
|
74
|
+
];
|
|
75
|
+
function parseIpv6(host) {
|
|
76
|
+
if (!host.includes(":")) return null;
|
|
77
|
+
let text = host;
|
|
78
|
+
const words = [];
|
|
79
|
+
const v4 = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(text);
|
|
80
|
+
let tail = [];
|
|
81
|
+
if (v4) {
|
|
82
|
+
const octets = v4[1].split(".").map(Number);
|
|
83
|
+
if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return null;
|
|
84
|
+
tail = [octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]];
|
|
85
|
+
text = text.slice(0, v4.index).replace(/:$/, "") || "::";
|
|
86
|
+
}
|
|
87
|
+
const halves = text.split("::");
|
|
88
|
+
if (halves.length > 2) return null;
|
|
89
|
+
const toWords = (part) => {
|
|
90
|
+
if (!part) return [];
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const group of part.split(":")) {
|
|
93
|
+
if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
|
|
94
|
+
out.push(parseInt(group, 16));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
};
|
|
98
|
+
const head = toWords(halves[0]);
|
|
99
|
+
const rest = halves.length === 2 ? toWords(halves[1]) : [];
|
|
100
|
+
if (!head || !rest) return null;
|
|
101
|
+
if (halves.length === 2) {
|
|
102
|
+
const fill = 8 - (head.length + rest.length + tail.length);
|
|
103
|
+
if (fill < 0) return null;
|
|
104
|
+
words.push(...head, ...Array(fill).fill(0), ...rest, ...tail);
|
|
105
|
+
} else {
|
|
106
|
+
words.push(...head, ...tail);
|
|
107
|
+
}
|
|
108
|
+
return words.length === 8 ? words : null;
|
|
109
|
+
}
|
|
110
|
+
function isPrivateIpv6(words) {
|
|
111
|
+
const [w0, w1, w2, w3, w4, w5] = words;
|
|
112
|
+
const allZero = words.every((w) => w === 0);
|
|
113
|
+
if (allZero) return true;
|
|
114
|
+
if (words.slice(0, 7).every((w) => w === 0) && words[7] === 1) return true;
|
|
115
|
+
if (w0 === 0 && w1 === 0 && w2 === 0 && w3 === 0 && w4 === 0 && (w5 === 0 || w5 === 65535)) {
|
|
116
|
+
const v4 = `${words[6] >> 8}.${words[6] & 255}.${words[7] >> 8}.${words[7] & 255}`;
|
|
117
|
+
return PRIVATE_IP_PATTERNS.some((p) => p.test(v4));
|
|
118
|
+
}
|
|
119
|
+
if ((w0 >> 8 & 254) === 252) return true;
|
|
120
|
+
if ((w0 & 65472) === 65152) return true;
|
|
121
|
+
if ((w0 & 65472) === 65216) return true;
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function bareHostname(hostname) {
|
|
125
|
+
const bare = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
126
|
+
return bare.length > 1 && bare.endsWith(".") ? bare.replace(/\.+$/, "") : bare;
|
|
127
|
+
}
|
|
128
|
+
var BLOCKED_HOSTNAMES = [
|
|
129
|
+
"localhost",
|
|
130
|
+
"localtest.me",
|
|
131
|
+
"metadata.google.internal",
|
|
132
|
+
// GCP metadata
|
|
133
|
+
"169.254.169.254",
|
|
134
|
+
// AWS/Azure/GCP metadata
|
|
135
|
+
"metadata.azure.com"
|
|
136
|
+
];
|
|
137
|
+
var ALLOWED_ZIHIN_DOMAINS = [
|
|
138
|
+
"llm.zihin.ai",
|
|
139
|
+
"api.zihin.ai",
|
|
140
|
+
"staging.llm.zihin.ai",
|
|
141
|
+
"dev.llm.zihin.ai"
|
|
142
|
+
];
|
|
143
|
+
function validateBaseUrl(url, options = {}) {
|
|
144
|
+
if (!url || url.trim() === "") {
|
|
145
|
+
return { valid: true, sanitizedUrl: "https://llm.zihin.ai" };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const parsed = new URL(url);
|
|
149
|
+
if (parsed.hostname.length > 1 && parsed.hostname.endsWith(".")) {
|
|
150
|
+
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
|
|
151
|
+
}
|
|
152
|
+
const hostname = bareHostname(parsed.hostname);
|
|
153
|
+
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
154
|
+
if (isLocalhost && options.allowLocalhost) {
|
|
155
|
+
const sanitizedUrl2 = parsed.origin + parsed.pathname.replace(/\/+$/, "");
|
|
156
|
+
return { valid: true, sanitizedUrl: sanitizedUrl2 };
|
|
157
|
+
}
|
|
158
|
+
if (!options.allowHttp && parsed.protocol !== "https:") {
|
|
159
|
+
return {
|
|
160
|
+
valid: false,
|
|
161
|
+
error: "Only HTTPS URLs are allowed for security reasons"
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const ipv6 = parseIpv6(hostname);
|
|
165
|
+
if (ipv6) {
|
|
166
|
+
if (isPrivateIpv6(ipv6)) {
|
|
167
|
+
return { valid: false, error: "Private IP addresses are not allowed" };
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
for (const pattern of PRIVATE_IP_PATTERNS) {
|
|
171
|
+
if (pattern.test(hostname)) {
|
|
172
|
+
return { valid: false, error: "Private IP addresses are not allowed" };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const blocked of BLOCKED_HOSTNAMES) {
|
|
177
|
+
if (blocked.startsWith("*")) {
|
|
178
|
+
const suffix = blocked.slice(1);
|
|
179
|
+
if (hostname.endsWith(suffix)) {
|
|
180
|
+
return { valid: false, error: "This hostname is not allowed" };
|
|
181
|
+
}
|
|
182
|
+
} else if (hostname === blocked || hostname.endsWith("." + blocked)) {
|
|
183
|
+
return { valid: false, error: "This hostname is not allowed" };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!options.allowCustomDomains) {
|
|
187
|
+
const isAllowed = ALLOWED_ZIHIN_DOMAINS.some(
|
|
188
|
+
(domain) => hostname === domain || hostname.endsWith("." + domain)
|
|
189
|
+
);
|
|
190
|
+
if (!isAllowed) {
|
|
191
|
+
return {
|
|
192
|
+
valid: false,
|
|
193
|
+
error: `Only Zihin domains are allowed. Use 'allowCustomBaseUrl' for custom endpoints.`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const sanitizedUrl = parsed.origin + parsed.pathname.replace(/\/+$/, "");
|
|
198
|
+
return { valid: true, sanitizedUrl };
|
|
199
|
+
} catch {
|
|
200
|
+
return { valid: false, error: "Invalid URL format" };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
var DEFAULT_LIMITS = {
|
|
204
|
+
maxPromptBytes: 100 * 1024,
|
|
205
|
+
// 100KB
|
|
206
|
+
maxMessageCount: 100,
|
|
207
|
+
maxSingleMessageBytes: 50 * 1024,
|
|
208
|
+
// 50KB
|
|
209
|
+
maxTotalPayloadBytes: 500 * 1024
|
|
210
|
+
// 500KB
|
|
211
|
+
};
|
|
212
|
+
function validatePromptSize(prompt, limits = {}) {
|
|
213
|
+
const effectiveLimits = { ...DEFAULT_LIMITS, ...limits };
|
|
214
|
+
const bytes = utf8Len(prompt);
|
|
215
|
+
if (bytes > effectiveLimits.maxPromptBytes) {
|
|
216
|
+
return {
|
|
217
|
+
valid: false,
|
|
218
|
+
error: `Prompt size (${Math.round(bytes / 1024)}KB) exceeds maximum allowed (${Math.round(
|
|
219
|
+
effectiveLimits.maxPromptBytes / 1024
|
|
220
|
+
)}KB)`,
|
|
221
|
+
details: { promptBytes: bytes }
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { valid: true, details: { promptBytes: bytes } };
|
|
225
|
+
}
|
|
226
|
+
var SENSITIVE_PATTERNS = [
|
|
227
|
+
/zhn_(?:live|test|dev)_[a-zA-Z0-9]+/g,
|
|
228
|
+
// API keys
|
|
229
|
+
/Bearer\s+[a-zA-Z0-9._-]+/gi,
|
|
230
|
+
// Bearer tokens
|
|
231
|
+
/at\s+[\w./<>]+\s*\(.*:\d+:\d+\)/g,
|
|
232
|
+
// Stack trace lines
|
|
233
|
+
/^\s+at\s+.+$/gm,
|
|
234
|
+
// Stack traces
|
|
235
|
+
/Error:\s*.*\n\s+at/g
|
|
236
|
+
// Error stack beginnings
|
|
237
|
+
];
|
|
238
|
+
var ERROR_CODE_MAP = {
|
|
239
|
+
RATE_LIMIT_EXCEEDED: "Rate limit exceeded. Please wait before retrying.",
|
|
240
|
+
UNAUTHORIZED: "Authentication failed. Please check your API credentials.",
|
|
241
|
+
FORBIDDEN: "This API key lacks permission for this action. Use an admin/editor key (member keys cannot create triggers or read telemetry).",
|
|
242
|
+
INVALID_MODEL: "The specified model is not available.",
|
|
243
|
+
INTERNAL_ERROR: "An internal error occurred. Please try again later.",
|
|
244
|
+
TIMEOUT: "Request timed out. Try increasing the timeout.",
|
|
245
|
+
INVALID_REQUEST: "The request was malformed. Please check your input.",
|
|
246
|
+
INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
|
|
247
|
+
CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
|
|
248
|
+
AGENT_NOT_FOUND: "Agent not found or not active for this tenant."
|
|
249
|
+
};
|
|
250
|
+
function sanitizeApiError(error) {
|
|
251
|
+
let message = "An unexpected error occurred";
|
|
252
|
+
let code;
|
|
253
|
+
let retryable = false;
|
|
254
|
+
let retryAfter;
|
|
255
|
+
if (typeof error === "object" && error !== null) {
|
|
256
|
+
const err = error;
|
|
257
|
+
const nested = err.error && typeof err.error === "object" ? err.error : void 0;
|
|
258
|
+
const codeRaw = nested?.code ?? (typeof err.error === "string" ? err.error : err.code);
|
|
259
|
+
code = typeof codeRaw === "string" ? codeRaw : void 0;
|
|
260
|
+
const rawMessage = nested?.message ?? err.message;
|
|
261
|
+
if (code && ERROR_CODE_MAP[code]) {
|
|
262
|
+
message = ERROR_CODE_MAP[code];
|
|
263
|
+
} else if (typeof rawMessage === "string" && rawMessage) {
|
|
264
|
+
message = rawMessage;
|
|
265
|
+
}
|
|
266
|
+
if (code === "RATE_LIMIT_EXCEEDED" || code === "TIMEOUT") {
|
|
267
|
+
retryable = true;
|
|
268
|
+
retryAfter = err.retryAfter || 60;
|
|
269
|
+
}
|
|
270
|
+
if (message.length > 2e3) message = message.slice(0, 2e3);
|
|
271
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
272
|
+
message = message.replace(pattern, "[REDACTED]");
|
|
273
|
+
}
|
|
274
|
+
} else if (typeof error === "string") {
|
|
275
|
+
message = error;
|
|
276
|
+
if (message.length > 2e3) message = message.slice(0, 2e3);
|
|
277
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
278
|
+
message = message.replace(pattern, "[REDACTED]");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (message.length > 500) {
|
|
282
|
+
message = message.substring(0, 500) + "...";
|
|
283
|
+
}
|
|
284
|
+
return { message, code, retryable, retryAfter };
|
|
285
|
+
}
|
|
286
|
+
var API_KEY_PATTERN = /^zhn_(live|test|dev)_[a-zA-Z0-9]{20,}$/;
|
|
287
|
+
function validateApiKeyFormat(apiKey) {
|
|
288
|
+
if (!apiKey || apiKey.trim() === "") {
|
|
289
|
+
return { valid: false, error: "API key is required" };
|
|
290
|
+
}
|
|
291
|
+
const trimmed = apiKey.trim();
|
|
292
|
+
if (!API_KEY_PATTERN.test(trimmed)) {
|
|
293
|
+
return {
|
|
294
|
+
valid: false,
|
|
295
|
+
error: "Invalid API key format. Expected: zhn_live_xxx, zhn_test_xxx or zhn_dev_xxx"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const environment = trimmed.startsWith("zhn_live_") ? "live" : trimmed.startsWith("zhn_test_") ? "test" : "dev";
|
|
299
|
+
return { valid: true, environment };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/errors.ts
|
|
303
|
+
var ZihinAgentError = class _ZihinAgentError extends Error {
|
|
304
|
+
constructor(kind, message, opts = {}) {
|
|
305
|
+
super(buildMessage(message, opts));
|
|
306
|
+
this.name = "ZihinAgentError";
|
|
307
|
+
this.kind = kind;
|
|
308
|
+
this.status = opts.status;
|
|
309
|
+
this.code = opts.code;
|
|
310
|
+
this.requestId = opts.requestId;
|
|
311
|
+
this.attempts = opts.attempts;
|
|
312
|
+
this.endpoint = opts.endpoint;
|
|
313
|
+
this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
|
|
314
|
+
Object.setPrototypeOf(this, _ZihinAgentError.prototype);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function buildMessage(base, opts) {
|
|
318
|
+
let s = base;
|
|
319
|
+
if (opts.endpoint) s += ` [${opts.endpoint}]`;
|
|
320
|
+
if (opts.attempts && opts.attempts > 1) s += ` (after ${opts.attempts} attempts)`;
|
|
321
|
+
if (opts.requestId) s += ` (request_id: ${opts.requestId})`;
|
|
322
|
+
return s;
|
|
323
|
+
}
|
|
324
|
+
function extractRequestId(body) {
|
|
325
|
+
const b = rec(body);
|
|
326
|
+
return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id);
|
|
327
|
+
}
|
|
328
|
+
function errorFromResponse(status, body, meta = {}) {
|
|
329
|
+
const sanitized = sanitizeApiError(body);
|
|
330
|
+
const message = sanitized.message;
|
|
331
|
+
const code = sanitized.code;
|
|
332
|
+
const base = { status, code, requestId: extractRequestId(body), ...meta };
|
|
333
|
+
if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
|
|
334
|
+
if (status === 403)
|
|
335
|
+
return new ZihinAgentError(
|
|
336
|
+
"auth",
|
|
337
|
+
message || "This API key lacks permission. Use an admin/editor key (member keys cannot create triggers or read telemetry).",
|
|
338
|
+
base
|
|
339
|
+
);
|
|
340
|
+
if (status === 404) return new ZihinAgentError("not_found", message || "Resource not found.", base);
|
|
341
|
+
if (status === 429)
|
|
342
|
+
return new ZihinAgentError("rate_limit", message || "Rate limit exceeded.", { ...base, retryable: true });
|
|
343
|
+
if (status >= 500)
|
|
344
|
+
return new ZihinAgentError("server", message || `Server error (${status}).`, { ...base, retryable: true });
|
|
345
|
+
return new ZihinAgentError("unknown", message || `Request failed (${status}).`, base);
|
|
346
|
+
}
|
|
347
|
+
var NON_RETRYABLE_NET = /^(ENOTFOUND|EAI_AGAIN|CERT_|ERR_TLS|UNABLE_TO_VERIFY|DEPTH_ZERO)/;
|
|
348
|
+
function errorFromThrow(err, timeoutMs, meta = {}) {
|
|
349
|
+
if (err instanceof ZihinAgentError) return err;
|
|
350
|
+
const name = err?.name;
|
|
351
|
+
if (name === "TimeoutError") {
|
|
352
|
+
return new ZihinAgentError("timeout", `Request timed out after ${timeoutMs ?? "?"}ms.`, {
|
|
353
|
+
...meta,
|
|
354
|
+
retryable: false
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (name === "AbortError") {
|
|
358
|
+
return new ZihinAgentError("aborted", "Request was aborted by the caller.", { ...meta, retryable: false });
|
|
359
|
+
}
|
|
360
|
+
const cause = err?.cause;
|
|
361
|
+
const causeCode = cause?.code;
|
|
362
|
+
const sanitized = sanitizeApiError(err);
|
|
363
|
+
const detail = causeCode || sanitized.message || "Network error";
|
|
364
|
+
const retryable = causeCode ? !NON_RETRYABLE_NET.test(causeCode) : true;
|
|
365
|
+
return new ZihinAgentError("network", `Network error: ${detail}`, { ...meta, retryable });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/http.ts
|
|
369
|
+
var DEFAULT_BASE_URL = "https://llm.zihin.ai";
|
|
370
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
371
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
372
|
+
var DEFAULT_MAX_EVENT_BYTES = 1 * 1024 * 1024;
|
|
373
|
+
var DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
374
|
+
function resolveFetch(injected) {
|
|
375
|
+
const f = injected ?? globalThis.fetch;
|
|
376
|
+
if (typeof f !== "function") {
|
|
377
|
+
throw new ZihinAgentError("config", "Global fetch is unavailable in this runtime.");
|
|
378
|
+
}
|
|
379
|
+
return f;
|
|
380
|
+
}
|
|
381
|
+
function resolveConfig(config) {
|
|
382
|
+
const keyCheck = validateApiKeyFormat(config.apiKey ?? "");
|
|
383
|
+
if (!keyCheck.valid) {
|
|
384
|
+
throw new ZihinAgentError("config", keyCheck.error || "Invalid API key.");
|
|
385
|
+
}
|
|
386
|
+
const urlCheck = validateBaseUrl(config.baseUrl ?? "", {
|
|
387
|
+
allowCustomDomains: config.allowCustomBaseUrl,
|
|
388
|
+
allowHttp: config.allowHttp,
|
|
389
|
+
allowLocalhost: config.allowHttp
|
|
390
|
+
});
|
|
391
|
+
if (!urlCheck.valid) {
|
|
392
|
+
throw new ZihinAgentError("config", urlCheck.error || "Invalid base URL.");
|
|
393
|
+
}
|
|
394
|
+
if (typeof (config.fetch ?? globalThis.fetch) !== "function") {
|
|
395
|
+
throw new ZihinAgentError(
|
|
396
|
+
"config",
|
|
397
|
+
"Global fetch is unavailable in this runtime. Use Node >=18 or pass a `fetch` implementation in the client config."
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
apiKey: config.apiKey.trim(),
|
|
402
|
+
baseUrl: urlCheck.sanitizedUrl || DEFAULT_BASE_URL,
|
|
403
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
404
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
405
|
+
maxStreamEventBytes: config.maxStreamEventBytes ?? DEFAULT_MAX_EVENT_BYTES,
|
|
406
|
+
maxStreamTotalBytes: config.maxStreamTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
|
|
407
|
+
fetchImpl: config.fetch,
|
|
408
|
+
onRetry: config.onRetry,
|
|
409
|
+
onError: config.onError
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function endpointLabel(method, url) {
|
|
413
|
+
try {
|
|
414
|
+
const u = new URL(url);
|
|
415
|
+
return `${method} ${u.host}${u.pathname}`;
|
|
416
|
+
} catch {
|
|
417
|
+
return method;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function retryAfterMs(headers, capMs = 3e4) {
|
|
421
|
+
const raw = headers.get("retry-after");
|
|
422
|
+
if (!raw) return void 0;
|
|
423
|
+
const secs = Number(raw);
|
|
424
|
+
if (Number.isFinite(secs)) return Math.min(Math.max(0, secs * 1e3), capMs);
|
|
425
|
+
const when = Date.parse(raw);
|
|
426
|
+
if (Number.isFinite(when)) return Math.min(Math.max(0, when - Date.now()), capMs);
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
function buildHeaders(apiKey, extra = {}) {
|
|
430
|
+
return {
|
|
431
|
+
"Content-Type": "application/json",
|
|
432
|
+
Accept: "application/json",
|
|
433
|
+
"X-Api-Key": apiKey,
|
|
434
|
+
...extra
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function delay(ms, signal) {
|
|
438
|
+
return new Promise((resolve, reject) => {
|
|
439
|
+
if (signal?.aborted) {
|
|
440
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const onAbort = () => {
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
reject(new ZihinAgentError("aborted", "Request was aborted by the caller."));
|
|
446
|
+
};
|
|
447
|
+
const timer = setTimeout(() => {
|
|
448
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
449
|
+
resolve();
|
|
450
|
+
}, ms);
|
|
451
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function backoffMs(attempt) {
|
|
455
|
+
const base = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
456
|
+
return base / 2 + Math.random() * (base / 2);
|
|
457
|
+
}
|
|
458
|
+
function createDeadline(timeoutMs, callerSignal) {
|
|
459
|
+
const controller = new AbortController();
|
|
460
|
+
let timedOut = false;
|
|
461
|
+
let abortedByCaller = false;
|
|
462
|
+
const timer = setTimeout(() => {
|
|
463
|
+
timedOut = true;
|
|
464
|
+
controller.abort();
|
|
465
|
+
}, timeoutMs);
|
|
466
|
+
const onAbort = () => {
|
|
467
|
+
abortedByCaller = true;
|
|
468
|
+
controller.abort();
|
|
469
|
+
};
|
|
470
|
+
if (callerSignal) {
|
|
471
|
+
if (callerSignal.aborted) onAbort();
|
|
472
|
+
else callerSignal.addEventListener("abort", onAbort, { once: true });
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
signal: controller.signal,
|
|
476
|
+
dispose: () => {
|
|
477
|
+
clearTimeout(timer);
|
|
478
|
+
if (callerSignal) callerSignal.removeEventListener("abort", onAbort);
|
|
479
|
+
},
|
|
480
|
+
timedOut: () => timedOut,
|
|
481
|
+
abortedByCaller: () => abortedByCaller
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function shouldRetryStatus(status, policy) {
|
|
485
|
+
if (policy === "idempotent") return status >= 500 || status === 429;
|
|
486
|
+
return status === 429 || status === 503;
|
|
487
|
+
}
|
|
488
|
+
async function requestBuffered(args) {
|
|
489
|
+
const endpoint = endpointLabel(args.method, args.url);
|
|
490
|
+
const doFetch = resolveFetch(args.fetchImpl);
|
|
491
|
+
let lastError = null;
|
|
492
|
+
for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
|
|
493
|
+
const d = createDeadline(args.timeoutMs, args.callerSignal);
|
|
494
|
+
try {
|
|
495
|
+
const res = await doFetch(args.url, {
|
|
496
|
+
method: args.method,
|
|
497
|
+
headers: args.headers,
|
|
498
|
+
body: args.body,
|
|
499
|
+
signal: d.signal,
|
|
500
|
+
redirect: "error",
|
|
501
|
+
// 3xx para outro host vazaria a X-Api-Key
|
|
502
|
+
cache: "no-store"
|
|
503
|
+
// evita cache stale (ex.: polling em Next.js/edge)
|
|
504
|
+
});
|
|
505
|
+
if (shouldRetryStatus(res.status, args.policy) && attempt < args.maxRetries) {
|
|
506
|
+
const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
|
|
507
|
+
try {
|
|
508
|
+
await res.body?.cancel();
|
|
509
|
+
} catch {
|
|
510
|
+
}
|
|
511
|
+
d.dispose();
|
|
512
|
+
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
|
|
513
|
+
await delay(wait, args.callerSignal);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const text = await res.text();
|
|
517
|
+
d.dispose();
|
|
518
|
+
return { status: res.status, ok: res.ok, text, endpoint, attempts: attempt + 1 };
|
|
519
|
+
} catch (err) {
|
|
520
|
+
d.dispose();
|
|
521
|
+
const meta = { endpoint, attempts: attempt + 1 };
|
|
522
|
+
if (d.timedOut()) throw new ZihinAgentError("timeout", `Request timed out after ${args.timeoutMs}ms.`, meta);
|
|
523
|
+
if (d.abortedByCaller())
|
|
524
|
+
throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
|
|
525
|
+
lastError = errorFromThrow(err, args.timeoutMs, meta);
|
|
526
|
+
const canRetry = args.policy === "idempotent" && lastError.retryable && attempt < args.maxRetries;
|
|
527
|
+
if (!canRetry) throw lastError;
|
|
528
|
+
const wait = backoffMs(attempt);
|
|
529
|
+
args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
|
|
530
|
+
await delay(wait, args.callerSignal);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
throw lastError ?? new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
534
|
+
}
|
|
535
|
+
async function openStream(args) {
|
|
536
|
+
const endpoint = endpointLabel("POST", args.url);
|
|
537
|
+
const doFetch = resolveFetch(args.fetchImpl);
|
|
538
|
+
for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
|
|
539
|
+
const d = createDeadline(args.timeoutMs, args.callerSignal);
|
|
540
|
+
try {
|
|
541
|
+
const response = await doFetch(args.url, {
|
|
542
|
+
method: "POST",
|
|
543
|
+
headers: { ...args.headers, Accept: "text/event-stream" },
|
|
544
|
+
body: args.body,
|
|
545
|
+
signal: d.signal,
|
|
546
|
+
redirect: "error",
|
|
547
|
+
cache: "no-store"
|
|
548
|
+
});
|
|
549
|
+
if (shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries) {
|
|
550
|
+
const wait = retryAfterMs(response.headers) ?? backoffMs(attempt);
|
|
551
|
+
try {
|
|
552
|
+
await response.body?.cancel();
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
d.dispose();
|
|
556
|
+
args.onRetry?.({
|
|
557
|
+
endpoint,
|
|
558
|
+
attempt: attempt + 1,
|
|
559
|
+
delayMs: wait,
|
|
560
|
+
reason: "status",
|
|
561
|
+
status: response.status
|
|
562
|
+
});
|
|
563
|
+
await delay(wait, args.callerSignal);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
return { response, deadline: d, endpoint };
|
|
567
|
+
} catch (err) {
|
|
568
|
+
d.dispose();
|
|
569
|
+
const meta = { endpoint, attempts: attempt + 1 };
|
|
570
|
+
if (d.timedOut()) throw new ZihinAgentError("timeout", `Request timed out after ${args.timeoutMs}ms.`, meta);
|
|
571
|
+
if (d.abortedByCaller())
|
|
572
|
+
throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
|
|
573
|
+
throw errorFromThrow(err, args.timeoutMs, meta);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
|
|
577
|
+
}
|
|
578
|
+
function parseBody(text) {
|
|
579
|
+
if (!text) return {};
|
|
580
|
+
try {
|
|
581
|
+
return JSON.parse(text);
|
|
582
|
+
} catch {
|
|
583
|
+
return { message: text };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/agents.ts
|
|
588
|
+
async function listAgents(cfg, opts = {}) {
|
|
589
|
+
const res = await requestBuffered({
|
|
590
|
+
method: "GET",
|
|
591
|
+
url: `${cfg.baseUrl}/api/v2/agents`,
|
|
592
|
+
headers: buildHeaders(cfg.apiKey),
|
|
593
|
+
timeoutMs: cfg.timeoutMs,
|
|
594
|
+
maxRetries: cfg.maxRetries,
|
|
595
|
+
policy: "idempotent",
|
|
596
|
+
callerSignal: opts.signal,
|
|
597
|
+
fetchImpl: cfg.fetchImpl,
|
|
598
|
+
onRetry: cfg.onRetry
|
|
599
|
+
});
|
|
600
|
+
if (!res.ok) {
|
|
601
|
+
throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
|
|
602
|
+
}
|
|
603
|
+
let json;
|
|
604
|
+
try {
|
|
605
|
+
json = JSON.parse(res.text);
|
|
606
|
+
} catch {
|
|
607
|
+
throw new ZihinAgentError("protocol", "Expected JSON from GET /api/v2/agents.");
|
|
608
|
+
}
|
|
609
|
+
const data = rec(json).data;
|
|
610
|
+
const list = Array.isArray(data) ? data : [];
|
|
611
|
+
return list.map((item) => {
|
|
612
|
+
const a = rec(item);
|
|
613
|
+
return {
|
|
614
|
+
id: str(a.id) ?? "",
|
|
615
|
+
name: str(a.name),
|
|
616
|
+
commercialName: str(a.commercial_name),
|
|
617
|
+
bio: str(a.bio),
|
|
618
|
+
type: str(a.type),
|
|
619
|
+
status: str(a.status),
|
|
620
|
+
tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : void 0,
|
|
621
|
+
toolsCount: typeof a.tools_count === "number" ? a.tools_count : void 0
|
|
622
|
+
};
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/sse.ts
|
|
627
|
+
var DEFAULT_MAX_EVENT_BYTES2 = 1 * 1024 * 1024;
|
|
628
|
+
var DEFAULT_MAX_TOTAL_BYTES2 = 64 * 1024 * 1024;
|
|
629
|
+
async function* parseSse(response, opts = {}) {
|
|
630
|
+
if (!response.body) {
|
|
631
|
+
throw new ZihinAgentError("protocol", "SSE response has no body.");
|
|
632
|
+
}
|
|
633
|
+
const maxEventBytes = opts.maxEventBytes ?? DEFAULT_MAX_EVENT_BYTES2;
|
|
634
|
+
const maxTotalBytes = opts.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES2;
|
|
635
|
+
const reader = response.body.getReader();
|
|
636
|
+
const decoder = new TextDecoder("utf-8");
|
|
637
|
+
let buffer = "";
|
|
638
|
+
let eventName = "";
|
|
639
|
+
let dataLines = [];
|
|
640
|
+
let dataBytes = 0;
|
|
641
|
+
let totalBytes = 0;
|
|
642
|
+
const flush = () => {
|
|
643
|
+
const had = dataLines.length > 0 || eventName !== "";
|
|
644
|
+
const rawData = dataLines.join("\n");
|
|
645
|
+
const evt = eventName || "message";
|
|
646
|
+
eventName = "";
|
|
647
|
+
dataLines = [];
|
|
648
|
+
dataBytes = 0;
|
|
649
|
+
if (!had || rawData === "") return null;
|
|
650
|
+
let data = rawData;
|
|
651
|
+
try {
|
|
652
|
+
data = JSON.parse(rawData);
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
return { event: evt, data, rawData };
|
|
656
|
+
};
|
|
657
|
+
try {
|
|
658
|
+
while (true) {
|
|
659
|
+
if (opts.signal?.aborted) {
|
|
660
|
+
throw opts.onAbortError ? opts.onAbortError() : new ZihinAgentError("aborted", "Stream aborted.");
|
|
661
|
+
}
|
|
662
|
+
let chunk;
|
|
663
|
+
try {
|
|
664
|
+
chunk = await reader.read();
|
|
665
|
+
} catch (err) {
|
|
666
|
+
throw opts.onAbortError ? opts.onAbortError() : errorFromThrow(err);
|
|
667
|
+
}
|
|
668
|
+
if (chunk.done) break;
|
|
669
|
+
totalBytes += chunk.value.byteLength;
|
|
670
|
+
if (totalBytes > maxTotalBytes) {
|
|
671
|
+
throw new ZihinAgentError("protocol", `SSE stream exceeded ${maxTotalBytes} bytes.`);
|
|
672
|
+
}
|
|
673
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
674
|
+
let nl;
|
|
675
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
676
|
+
let line = buffer.slice(0, nl);
|
|
677
|
+
buffer = buffer.slice(nl + 1);
|
|
678
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
679
|
+
if (line === "") {
|
|
680
|
+
const e = flush();
|
|
681
|
+
if (e) yield e;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (line.startsWith(":")) continue;
|
|
685
|
+
const colon = line.indexOf(":");
|
|
686
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
687
|
+
let val = colon === -1 ? "" : line.slice(colon + 1);
|
|
688
|
+
if (val.startsWith(" ")) val = val.slice(1);
|
|
689
|
+
if (field === "event") {
|
|
690
|
+
eventName = val;
|
|
691
|
+
} else if (field === "data") {
|
|
692
|
+
dataBytes += utf8Len(val) + 1;
|
|
693
|
+
if (dataBytes > maxEventBytes) {
|
|
694
|
+
throw new ZihinAgentError("protocol", `SSE event exceeded ${maxEventBytes} bytes.`);
|
|
695
|
+
}
|
|
696
|
+
dataLines.push(val);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (utf8Len(buffer) > maxEventBytes) {
|
|
700
|
+
throw new ZihinAgentError("protocol", `SSE line exceeded ${maxEventBytes} bytes.`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
buffer += decoder.decode();
|
|
704
|
+
if (buffer.length > 0) {
|
|
705
|
+
let line = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
|
|
706
|
+
if (line && !line.startsWith(":")) {
|
|
707
|
+
const colon = line.indexOf(":");
|
|
708
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
709
|
+
let val = colon === -1 ? "" : line.slice(colon + 1);
|
|
710
|
+
if (val.startsWith(" ")) val = val.slice(1);
|
|
711
|
+
if (field === "data") dataLines.push(val);
|
|
712
|
+
else if (field === "event") eventName = val;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const tail = flush();
|
|
716
|
+
if (tail) yield tail;
|
|
717
|
+
} finally {
|
|
718
|
+
try {
|
|
719
|
+
await reader.cancel();
|
|
720
|
+
} catch {
|
|
721
|
+
}
|
|
722
|
+
opts.onEnd?.();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/invoke.ts
|
|
727
|
+
function mapEvent(e) {
|
|
728
|
+
const d = rec(e.data);
|
|
729
|
+
switch (e.event) {
|
|
730
|
+
case "metadata":
|
|
731
|
+
return {
|
|
732
|
+
type: "metadata",
|
|
733
|
+
requestId: str(d.request_id),
|
|
734
|
+
agentId: str(d.agent_id),
|
|
735
|
+
sessionId: str(d.session_id),
|
|
736
|
+
data: e.data
|
|
737
|
+
};
|
|
738
|
+
case "status":
|
|
739
|
+
return { type: "status", phase: str(d.phase), data: e.data };
|
|
740
|
+
case "attachments":
|
|
741
|
+
return { type: "attachments", data: e.data };
|
|
742
|
+
case "phase":
|
|
743
|
+
return { type: "phase", phase: str(d.phase), data: e.data };
|
|
744
|
+
case "thinking":
|
|
745
|
+
return { type: "thinking", thought: str(d.thought), data: e.data };
|
|
746
|
+
case "tool":
|
|
747
|
+
return {
|
|
748
|
+
type: "tool",
|
|
749
|
+
tool: str(d.tool),
|
|
750
|
+
callId: str(d.call_id),
|
|
751
|
+
success: typeof d.success === "boolean" ? d.success : void 0,
|
|
752
|
+
data: e.data
|
|
753
|
+
};
|
|
754
|
+
case "action_proposed":
|
|
755
|
+
return { type: "action_proposed", data: e.data };
|
|
756
|
+
case "token":
|
|
757
|
+
return { type: "token", content: str(d.content) ?? "", data: e.data };
|
|
758
|
+
case "warning":
|
|
759
|
+
return { type: "warning", message: str(d.message), data: e.data };
|
|
760
|
+
case "response":
|
|
761
|
+
return {
|
|
762
|
+
type: "response",
|
|
763
|
+
content: str(d.content) ?? "",
|
|
764
|
+
sessionId: str(d.session_id),
|
|
765
|
+
sources: Array.isArray(d.sources) ? d.sources : void 0,
|
|
766
|
+
blocks: Array.isArray(d.blocks) ? d.blocks : void 0,
|
|
767
|
+
data: e.data
|
|
768
|
+
};
|
|
769
|
+
case "metrics": {
|
|
770
|
+
const modelConfig = rec(d.model_config);
|
|
771
|
+
return {
|
|
772
|
+
type: "metrics",
|
|
773
|
+
usage: parseUsage(d.usage),
|
|
774
|
+
model: str(modelConfig.model) ?? str(d.model),
|
|
775
|
+
iterations: num(d.iterations),
|
|
776
|
+
executionTimeMs: num(d.execution_time_ms),
|
|
777
|
+
data: e.data
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
default:
|
|
781
|
+
return { type: "unknown", event: e.event, data: e.data };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
function buildRequestBody(params) {
|
|
785
|
+
const body = {};
|
|
786
|
+
if (params.message !== void 0) body.message = params.message;
|
|
787
|
+
if (params.sessionId) body.session_id = params.sessionId;
|
|
788
|
+
if (params.attachments?.length) {
|
|
789
|
+
body.attachments = params.attachments.map((a) => ({ url: a.url, type: a.type }));
|
|
790
|
+
}
|
|
791
|
+
if (params.options) {
|
|
792
|
+
const o = {};
|
|
793
|
+
if (params.options.temperature !== void 0) o.temperature = params.options.temperature;
|
|
794
|
+
if (params.options.maxTokens !== void 0) o.max_tokens = params.options.maxTokens;
|
|
795
|
+
if (params.options.timeoutMs !== void 0) o.timeout_ms = params.options.timeoutMs;
|
|
796
|
+
if (params.options.jitMode !== void 0) o.jit_mode = params.options.jitMode;
|
|
797
|
+
if (Object.keys(o).length) body.options = o;
|
|
798
|
+
}
|
|
799
|
+
if (params.metadata) {
|
|
800
|
+
const m = { ...params.metadata };
|
|
801
|
+
if (params.metadata.traceId !== void 0) {
|
|
802
|
+
m.trace_id = params.metadata.traceId;
|
|
803
|
+
delete m.traceId;
|
|
804
|
+
}
|
|
805
|
+
if (params.metadata.correlationId !== void 0) {
|
|
806
|
+
m.correlation_id = params.metadata.correlationId;
|
|
807
|
+
delete m.correlationId;
|
|
808
|
+
}
|
|
809
|
+
body.metadata = m;
|
|
810
|
+
}
|
|
811
|
+
return body;
|
|
812
|
+
}
|
|
813
|
+
function validateParams(params) {
|
|
814
|
+
if (!params.agentId) {
|
|
815
|
+
throw new ZihinAgentError("config", "agentId is required.");
|
|
816
|
+
}
|
|
817
|
+
if (!params.message && !(params.attachments && params.attachments.length)) {
|
|
818
|
+
throw new ZihinAgentError("config", "Either `message` or `attachments` is required.");
|
|
819
|
+
}
|
|
820
|
+
if (params.message) {
|
|
821
|
+
const sizeCheck = validatePromptSize(params.message);
|
|
822
|
+
if (!sizeCheck.valid) {
|
|
823
|
+
throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
for (const att of params.attachments ?? []) {
|
|
827
|
+
const urlCheck = validateBaseUrl(att.url, { allowCustomDomains: true });
|
|
828
|
+
if (!urlCheck.valid) {
|
|
829
|
+
throw new ZihinAgentError("config", `Invalid attachment URL: ${urlCheck.error}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function* streamAgent(cfg, params) {
|
|
834
|
+
validateParams(params);
|
|
835
|
+
const timeoutMs = params.options?.timeoutMs ?? cfg.timeoutMs;
|
|
836
|
+
const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
|
|
837
|
+
const { response, deadline, endpoint } = await openStream({
|
|
838
|
+
url,
|
|
839
|
+
headers: buildHeaders(cfg.apiKey),
|
|
840
|
+
body: JSON.stringify(buildRequestBody(params)),
|
|
841
|
+
timeoutMs,
|
|
842
|
+
maxRetries: cfg.maxRetries,
|
|
843
|
+
callerSignal: params.signal,
|
|
844
|
+
fetchImpl: cfg.fetchImpl,
|
|
845
|
+
onRetry: cfg.onRetry
|
|
846
|
+
});
|
|
847
|
+
try {
|
|
848
|
+
if (!response.ok) {
|
|
849
|
+
let text = "";
|
|
850
|
+
try {
|
|
851
|
+
text = await response.text();
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
throw errorFromResponse(response.status, parseBody(text), { endpoint });
|
|
855
|
+
}
|
|
856
|
+
const onAbortError = () => deadline.timedOut() ? new ZihinAgentError("timeout", `Stream timed out after ${timeoutMs}ms.`, { endpoint }) : deadline.abortedByCaller() ? new ZihinAgentError("aborted", "Stream aborted by the caller.", { endpoint }) : new ZihinAgentError("network", "Stream connection interrupted.", { endpoint });
|
|
857
|
+
for await (const e of parseSse(response, {
|
|
858
|
+
signal: params.signal,
|
|
859
|
+
onAbortError,
|
|
860
|
+
maxEventBytes: cfg.maxStreamEventBytes,
|
|
861
|
+
maxTotalBytes: cfg.maxStreamTotalBytes
|
|
862
|
+
})) {
|
|
863
|
+
yield mapEvent(e);
|
|
864
|
+
}
|
|
865
|
+
} finally {
|
|
866
|
+
deadline.dispose();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function invokeAgent(cfg, params) {
|
|
870
|
+
const raw = [];
|
|
871
|
+
const keepRaw = params.includeRaw === true;
|
|
872
|
+
let responseChunk;
|
|
873
|
+
let metricsChunk;
|
|
874
|
+
let metadataSessionId;
|
|
875
|
+
let tokenBuffer = "";
|
|
876
|
+
for await (const chunk of streamAgent(cfg, params)) {
|
|
877
|
+
if (keepRaw) raw.push(chunk);
|
|
878
|
+
if (chunk.type === "response") responseChunk = chunk;
|
|
879
|
+
else if (chunk.type === "metrics") metricsChunk = chunk;
|
|
880
|
+
else if (chunk.type === "token") tokenBuffer += chunk.content;
|
|
881
|
+
else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
|
|
882
|
+
}
|
|
883
|
+
if (!responseChunk) {
|
|
884
|
+
if (tokenBuffer) {
|
|
885
|
+
return {
|
|
886
|
+
content: tokenBuffer,
|
|
887
|
+
sessionId: metadataSessionId ?? params.sessionId,
|
|
888
|
+
usage: metricsChunk?.usage,
|
|
889
|
+
model: metricsChunk?.model,
|
|
890
|
+
iterations: metricsChunk?.iterations,
|
|
891
|
+
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
892
|
+
raw
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
throw new ZihinAgentError("protocol", "Stream ended without a `response` event.");
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
content: responseChunk.content || tokenBuffer,
|
|
899
|
+
sessionId: responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
|
|
900
|
+
model: metricsChunk?.model,
|
|
901
|
+
usage: metricsChunk?.usage,
|
|
902
|
+
sources: responseChunk.sources,
|
|
903
|
+
blocks: responseChunk.blocks,
|
|
904
|
+
iterations: metricsChunk?.iterations,
|
|
905
|
+
executionTimeMs: metricsChunk?.executionTimeMs,
|
|
906
|
+
raw
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/triggers.ts
|
|
911
|
+
async function requestJson(cfg, args) {
|
|
912
|
+
const res = await requestBuffered({
|
|
913
|
+
method: args.method,
|
|
914
|
+
url: `${cfg.baseUrl}${args.path}`,
|
|
915
|
+
headers: buildHeaders(cfg.apiKey),
|
|
916
|
+
body: args.body !== void 0 ? JSON.stringify(args.body) : void 0,
|
|
917
|
+
timeoutMs: cfg.timeoutMs,
|
|
918
|
+
maxRetries: cfg.maxRetries,
|
|
919
|
+
policy: args.policy,
|
|
920
|
+
callerSignal: args.signal,
|
|
921
|
+
fetchImpl: cfg.fetchImpl,
|
|
922
|
+
onRetry: cfg.onRetry
|
|
923
|
+
});
|
|
924
|
+
if (!res.ok) {
|
|
925
|
+
throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
return JSON.parse(res.text);
|
|
929
|
+
} catch {
|
|
930
|
+
throw new ZihinAgentError("protocol", "Expected JSON response.", { endpoint: res.endpoint });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function queryExtractionBody(extraction, queryField) {
|
|
934
|
+
if (!extraction) return { mode: "field", field: queryField ?? "message" };
|
|
935
|
+
switch (extraction.mode) {
|
|
936
|
+
case "field":
|
|
937
|
+
if (!extraction.field) {
|
|
938
|
+
throw new ZihinAgentError("config", 'queryExtraction.field is required for mode "field".');
|
|
939
|
+
}
|
|
940
|
+
return { mode: "field", field: extraction.field };
|
|
941
|
+
case "template":
|
|
942
|
+
if (!extraction.template) {
|
|
943
|
+
throw new ZihinAgentError("config", 'queryExtraction.template is required for mode "template".');
|
|
944
|
+
}
|
|
945
|
+
return { mode: "template", template: extraction.template };
|
|
946
|
+
case "full_body":
|
|
947
|
+
return { mode: "full_body" };
|
|
948
|
+
default:
|
|
949
|
+
throw new ZihinAgentError("config", "Unknown queryExtraction.mode (use field|template|full_body).");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async function createTrigger(cfg, params) {
|
|
953
|
+
if (!params.agentId) throw new ZihinAgentError("config", "agentId is required.");
|
|
954
|
+
if (!params.name) throw new ZihinAgentError("config", "name is required.");
|
|
955
|
+
const mode = params.mode ?? "async";
|
|
956
|
+
if (mode === "async" && !params.callbackUrl) {
|
|
957
|
+
throw new ZihinAgentError("config", "callbackUrl is required for async triggers.");
|
|
958
|
+
}
|
|
959
|
+
if (params.callbackUrl) {
|
|
960
|
+
const cb = validateBaseUrl(params.callbackUrl, { allowCustomDomains: true });
|
|
961
|
+
if (!cb.valid) {
|
|
962
|
+
throw new ZihinAgentError("config", `Invalid callbackUrl: ${cb.error}`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const execution = { mode };
|
|
966
|
+
if (params.callbackUrl) {
|
|
967
|
+
execution.callback = {
|
|
968
|
+
url: params.callbackUrl,
|
|
969
|
+
method: params.callbackMethod ?? "POST"
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
const body = {
|
|
973
|
+
agent_id: params.agentId,
|
|
974
|
+
name: params.name,
|
|
975
|
+
trigger_type: "webhook",
|
|
976
|
+
trigger_config: {
|
|
977
|
+
query_extraction: queryExtractionBody(params.queryExtraction, params.queryField),
|
|
978
|
+
auth: { type: params.dispatchAuth ?? "api_key" },
|
|
979
|
+
execution
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
const json = rec(
|
|
983
|
+
await requestJson(cfg, { method: "POST", path: "/api/triggers", body, policy: "side-effecting", signal: params.signal })
|
|
984
|
+
);
|
|
985
|
+
const data = rec(json.data);
|
|
986
|
+
const triggerId = str(data.id) ?? str(json.id);
|
|
987
|
+
const webhookUrl = str(json.webhook_url) ?? (triggerId ? `${cfg.baseUrl}/api/triggers/webhook/${triggerId}` : void 0);
|
|
988
|
+
if (!triggerId || !webhookUrl) {
|
|
989
|
+
throw new ZihinAgentError("protocol", "createTrigger: missing trigger id / webhook url in response.");
|
|
990
|
+
}
|
|
991
|
+
return { triggerId, webhookUrl };
|
|
992
|
+
}
|
|
993
|
+
async function dispatchAgent(cfg, params) {
|
|
994
|
+
if (!params.triggerId) throw new ZihinAgentError("config", "triggerId is required.");
|
|
995
|
+
const res = await requestBuffered({
|
|
996
|
+
method: "POST",
|
|
997
|
+
url: `${cfg.baseUrl}/api/triggers/webhook/${encodeURIComponent(params.triggerId)}`,
|
|
998
|
+
headers: buildHeaders(cfg.apiKey),
|
|
999
|
+
body: JSON.stringify(params.payload ?? {}),
|
|
1000
|
+
timeoutMs: cfg.timeoutMs,
|
|
1001
|
+
maxRetries: cfg.maxRetries,
|
|
1002
|
+
policy: "side-effecting",
|
|
1003
|
+
// S1: nunca duplica disparo por erro de rede
|
|
1004
|
+
callerSignal: params.signal,
|
|
1005
|
+
fetchImpl: cfg.fetchImpl,
|
|
1006
|
+
onRetry: cfg.onRetry
|
|
1007
|
+
});
|
|
1008
|
+
if (!res.ok) {
|
|
1009
|
+
throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
|
|
1010
|
+
}
|
|
1011
|
+
const json = parseBody(res.text);
|
|
1012
|
+
const j = rec(json);
|
|
1013
|
+
return {
|
|
1014
|
+
accepted: true,
|
|
1015
|
+
triggerId: str(j.trigger_id) ?? params.triggerId,
|
|
1016
|
+
executionId: str(j.execution_id),
|
|
1017
|
+
agentResponse: str(j.agent_response),
|
|
1018
|
+
usage: parseUsage(j.usage),
|
|
1019
|
+
raw: json
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function webhookUrlFor(cfg, t) {
|
|
1023
|
+
if (str(t.webhook_url)) return str(t.webhook_url);
|
|
1024
|
+
const id = str(t.id);
|
|
1025
|
+
return t.trigger_type === "webhook" && id ? `${cfg.baseUrl}/api/triggers/webhook/${id}` : void 0;
|
|
1026
|
+
}
|
|
1027
|
+
function mapTriggerInfo(cfg, raw) {
|
|
1028
|
+
const t = rec(raw);
|
|
1029
|
+
return {
|
|
1030
|
+
id: str(t.id) ?? "",
|
|
1031
|
+
agentId: str(t.agent_id),
|
|
1032
|
+
name: str(t.name),
|
|
1033
|
+
description: str(t.description),
|
|
1034
|
+
triggerType: str(t.trigger_type),
|
|
1035
|
+
enabled: typeof t.enabled === "boolean" ? t.enabled : void 0,
|
|
1036
|
+
triggerCount: num(t.trigger_count),
|
|
1037
|
+
lastError: str(t.last_error),
|
|
1038
|
+
webhookUrl: webhookUrlFor(cfg, t)
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
async function listTriggers(cfg, params = {}) {
|
|
1042
|
+
const qs = new URLSearchParams();
|
|
1043
|
+
if (params.agentId) qs.set("agentId", params.agentId);
|
|
1044
|
+
if (params.triggerType) qs.set("triggerType", params.triggerType);
|
|
1045
|
+
if (params.enabledOnly) qs.set("enabledOnly", "true");
|
|
1046
|
+
const query = qs.toString();
|
|
1047
|
+
const path = `/api/triggers${query ? `?${query}` : ""}`;
|
|
1048
|
+
const json = await requestJson(cfg, { method: "GET", path, policy: "idempotent", signal: params.signal });
|
|
1049
|
+
const list = Array.isArray(json) ? json : Array.isArray(rec(json).data) ? rec(json).data : [];
|
|
1050
|
+
return list.map((t) => mapTriggerInfo(cfg, t));
|
|
1051
|
+
}
|
|
1052
|
+
async function getTrigger(cfg, params) {
|
|
1053
|
+
if (!params.triggerId) throw new ZihinAgentError("config", "triggerId is required.");
|
|
1054
|
+
const json = await requestJson(cfg, {
|
|
1055
|
+
method: "GET",
|
|
1056
|
+
path: `/api/triggers/${encodeURIComponent(params.triggerId)}`,
|
|
1057
|
+
policy: "idempotent",
|
|
1058
|
+
signal: params.signal
|
|
1059
|
+
});
|
|
1060
|
+
return mapTriggerInfo(cfg, rec(json).data ?? json);
|
|
1061
|
+
}
|
|
1062
|
+
async function updateTrigger(cfg, params) {
|
|
1063
|
+
if (!params.triggerId) throw new ZihinAgentError("config", "triggerId is required.");
|
|
1064
|
+
if (params.callbackUrl) {
|
|
1065
|
+
const cb = validateBaseUrl(params.callbackUrl, { allowCustomDomains: true });
|
|
1066
|
+
if (!cb.valid) {
|
|
1067
|
+
throw new ZihinAgentError("config", `Invalid callbackUrl: ${cb.error}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const body = {};
|
|
1071
|
+
if (params.name !== void 0) body.name = params.name;
|
|
1072
|
+
if (params.description !== void 0) body.description = params.description;
|
|
1073
|
+
if (params.enabled !== void 0) body.enabled = params.enabled;
|
|
1074
|
+
const touchesConfig = params.queryExtraction !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.mode !== void 0;
|
|
1075
|
+
if (touchesConfig) {
|
|
1076
|
+
const path = `/api/triggers/${encodeURIComponent(params.triggerId)}`;
|
|
1077
|
+
const currentJson = rec(
|
|
1078
|
+
await requestJson(cfg, { method: "GET", path, policy: "idempotent", signal: params.signal })
|
|
1079
|
+
);
|
|
1080
|
+
const current = rec(currentJson.data ?? currentJson);
|
|
1081
|
+
if (!current.trigger_config || typeof current.trigger_config !== "object" || Object.keys(rec(current.trigger_config)).length === 0) {
|
|
1082
|
+
throw new ZihinAgentError(
|
|
1083
|
+
"protocol",
|
|
1084
|
+
"updateTrigger: current trigger_config unavailable \u2014 refusing to overwrite it partially."
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
const config = rec(current.trigger_config);
|
|
1088
|
+
const merged = { ...config };
|
|
1089
|
+
if (params.queryExtraction !== void 0) {
|
|
1090
|
+
merged.query_extraction = queryExtractionBody(params.queryExtraction, void 0);
|
|
1091
|
+
}
|
|
1092
|
+
if (params.mode !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
|
|
1093
|
+
const execution = rec(config.execution);
|
|
1094
|
+
const callback = rec(execution.callback);
|
|
1095
|
+
const newExecution = { ...execution };
|
|
1096
|
+
if (params.mode !== void 0) newExecution.mode = params.mode;
|
|
1097
|
+
if (params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
|
|
1098
|
+
const url = params.callbackUrl ?? str(callback.url);
|
|
1099
|
+
if (!url) {
|
|
1100
|
+
throw new ZihinAgentError(
|
|
1101
|
+
"config",
|
|
1102
|
+
"callbackUrl is required: this trigger has no callback configured yet."
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
newExecution.callback = {
|
|
1106
|
+
url,
|
|
1107
|
+
method: params.callbackMethod ?? str(callback.method) ?? "POST"
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
if (newExecution.mode === "async" && !rec(newExecution.callback).url) {
|
|
1111
|
+
throw new ZihinAgentError("config", "callbackUrl is required for async triggers.");
|
|
1112
|
+
}
|
|
1113
|
+
merged.execution = newExecution;
|
|
1114
|
+
}
|
|
1115
|
+
body.trigger_config = merged;
|
|
1116
|
+
}
|
|
1117
|
+
if (Object.keys(body).length === 0) {
|
|
1118
|
+
throw new ZihinAgentError("config", "updateTrigger: no fields to update.");
|
|
1119
|
+
}
|
|
1120
|
+
const json = await requestJson(cfg, {
|
|
1121
|
+
method: "PATCH",
|
|
1122
|
+
path: `/api/triggers/${encodeURIComponent(params.triggerId)}`,
|
|
1123
|
+
body,
|
|
1124
|
+
// PATCH aqui é replay-safe: reaplicar os mesmos valores produz o mesmo estado.
|
|
1125
|
+
policy: "idempotent",
|
|
1126
|
+
signal: params.signal
|
|
1127
|
+
});
|
|
1128
|
+
return mapTriggerInfo(cfg, rec(json).data ?? json);
|
|
1129
|
+
}
|
|
1130
|
+
async function deleteTrigger(cfg, params) {
|
|
1131
|
+
if (!params.triggerId) throw new ZihinAgentError("config", "triggerId is required.");
|
|
1132
|
+
const res = await requestBuffered({
|
|
1133
|
+
method: "DELETE",
|
|
1134
|
+
url: `${cfg.baseUrl}/api/triggers/${encodeURIComponent(params.triggerId)}`,
|
|
1135
|
+
headers: buildHeaders(cfg.apiKey),
|
|
1136
|
+
timeoutMs: cfg.timeoutMs,
|
|
1137
|
+
maxRetries: cfg.maxRetries,
|
|
1138
|
+
policy: "idempotent",
|
|
1139
|
+
// DELETE é idempotente
|
|
1140
|
+
callerSignal: params.signal,
|
|
1141
|
+
fetchImpl: cfg.fetchImpl,
|
|
1142
|
+
onRetry: cfg.onRetry
|
|
1143
|
+
});
|
|
1144
|
+
if (!res.ok) {
|
|
1145
|
+
throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
|
|
1146
|
+
}
|
|
1147
|
+
const j = rec(parseBody(res.text));
|
|
1148
|
+
return { deleted: j.deleted === true || res.status === 200 };
|
|
1149
|
+
}
|
|
1150
|
+
async function findOrCreateTrigger(cfg, params) {
|
|
1151
|
+
if (!params.agentId) throw new ZihinAgentError("config", "agentId is required.");
|
|
1152
|
+
if (!params.name) throw new ZihinAgentError("config", "name is required.");
|
|
1153
|
+
const existing = await listTriggers(cfg, { agentId: params.agentId, signal: params.signal });
|
|
1154
|
+
const match = existing.find((t) => t.name === params.name);
|
|
1155
|
+
if (match && match.id) {
|
|
1156
|
+
const webhookUrl = match.webhookUrl ?? `${cfg.baseUrl}/api/triggers/webhook/${match.id}`;
|
|
1157
|
+
return { triggerId: match.id, webhookUrl, created: false };
|
|
1158
|
+
}
|
|
1159
|
+
const ref = await createTrigger(cfg, params);
|
|
1160
|
+
return { ...ref, created: true };
|
|
1161
|
+
}
|
|
1162
|
+
async function getExecution(cfg, params) {
|
|
1163
|
+
if (!params.triggerId || !params.executionId) {
|
|
1164
|
+
throw new ZihinAgentError("config", "triggerId and executionId are required.");
|
|
1165
|
+
}
|
|
1166
|
+
const path = `/api/triggers/${encodeURIComponent(params.triggerId)}/executions/${encodeURIComponent(
|
|
1167
|
+
params.executionId
|
|
1168
|
+
)}`;
|
|
1169
|
+
const json = rec(await requestJson(cfg, { method: "GET", path, policy: "idempotent", signal: params.signal }));
|
|
1170
|
+
const data = rec(json.data);
|
|
1171
|
+
const exec = rec(data.execution ?? json.execution);
|
|
1172
|
+
const metrics = rec(data.metrics ?? json.metrics);
|
|
1173
|
+
return {
|
|
1174
|
+
executionId: str(exec.id) ?? params.executionId,
|
|
1175
|
+
status: str(exec.status) ?? "unknown",
|
|
1176
|
+
startedAt: str(exec.started_at),
|
|
1177
|
+
finishedAt: str(exec.finished_at),
|
|
1178
|
+
executionTimeMs: num(exec.execution_time_ms),
|
|
1179
|
+
usage: parseUsage(metrics.usage ?? metrics) ?? parseUsage(exec.usage),
|
|
1180
|
+
result: str(exec.agent_response) ?? str(exec.result),
|
|
1181
|
+
raw: json
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// src/client.ts
|
|
1186
|
+
var ZihinAgentClient = class {
|
|
1187
|
+
constructor(config) {
|
|
1188
|
+
this.cfg = resolveConfig(config);
|
|
1189
|
+
}
|
|
1190
|
+
/** Base URL efetiva (após validação SSRF). */
|
|
1191
|
+
get baseUrl() {
|
|
1192
|
+
return this.cfg.baseUrl;
|
|
1193
|
+
}
|
|
1194
|
+
/** Notifica `config.onError` (falha definitiva); o callback nunca derruba o fluxo. */
|
|
1195
|
+
report(err) {
|
|
1196
|
+
if (this.cfg.onError && err instanceof ZihinAgentError) {
|
|
1197
|
+
try {
|
|
1198
|
+
this.cfg.onError(err);
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
guard(p) {
|
|
1204
|
+
return p.catch((err) => {
|
|
1205
|
+
this.report(err);
|
|
1206
|
+
throw err;
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
// ---- Caminho A: direto (SSE) ----
|
|
1210
|
+
/** Invoca o agente e retorna o resultado final consolidado (response + metrics). */
|
|
1211
|
+
invokeAgent(params) {
|
|
1212
|
+
return this.guard(invokeAgent(this.cfg, params));
|
|
1213
|
+
}
|
|
1214
|
+
/** Emite deltas ao vivo do agente (async iterator). */
|
|
1215
|
+
async *streamAgent(params) {
|
|
1216
|
+
try {
|
|
1217
|
+
yield* streamAgent(this.cfg, params);
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
this.report(err);
|
|
1220
|
+
throw err;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
// ---- Caminho B: triggers (async/callback) ----
|
|
1224
|
+
/** Provisiona (1x) um trigger de webhook ligado a um agente. */
|
|
1225
|
+
createTrigger(params) {
|
|
1226
|
+
return this.guard(createTrigger(this.cfg, params));
|
|
1227
|
+
}
|
|
1228
|
+
/** Reaproveita (por agente+nome) ou cria um trigger — evita duplicação. */
|
|
1229
|
+
findOrCreateTrigger(params) {
|
|
1230
|
+
return this.guard(findOrCreateTrigger(this.cfg, params));
|
|
1231
|
+
}
|
|
1232
|
+
/** Lista os triggers do tenant (filtros opcionais). */
|
|
1233
|
+
listTriggers(params = {}) {
|
|
1234
|
+
return this.guard(listTriggers(this.cfg, params));
|
|
1235
|
+
}
|
|
1236
|
+
/** Obtém um trigger pelo id (inclui webhookUrl). */
|
|
1237
|
+
getTrigger(params) {
|
|
1238
|
+
return this.guard(getTrigger(this.cfg, params));
|
|
1239
|
+
}
|
|
1240
|
+
/** Atualiza um trigger (name/description/enabled/extração/callback) via PATCH. */
|
|
1241
|
+
updateTrigger(params) {
|
|
1242
|
+
return this.guard(updateTrigger(this.cfg, params));
|
|
1243
|
+
}
|
|
1244
|
+
/** Remove um trigger (requer key admin/owner). */
|
|
1245
|
+
deleteTrigger(params) {
|
|
1246
|
+
return this.guard(deleteTrigger(this.cfg, params));
|
|
1247
|
+
}
|
|
1248
|
+
/** Dispara um trigger: 202 (async) ou JSON imediato (sync). */
|
|
1249
|
+
dispatchAgent(params) {
|
|
1250
|
+
return this.guard(dispatchAgent(this.cfg, params));
|
|
1251
|
+
}
|
|
1252
|
+
/** Consulta o resultado de uma execução disparada por trigger (polling). */
|
|
1253
|
+
getExecution(params) {
|
|
1254
|
+
return this.guard(getExecution(this.cfg, params));
|
|
1255
|
+
}
|
|
1256
|
+
// ---- Auxiliar ----
|
|
1257
|
+
/** Lista os agentes do tenant (dropdown). */
|
|
1258
|
+
listAgents(opts = {}) {
|
|
1259
|
+
return this.guard(listAgents(this.cfg, opts));
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
function createClient(config) {
|
|
1263
|
+
return new ZihinAgentClient(config);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/signing.ts
|
|
1267
|
+
function loadNodeCrypto() {
|
|
1268
|
+
const proc = globalThis.process;
|
|
1269
|
+
if (typeof proc?.getBuiltinModule === "function") {
|
|
1270
|
+
return proc.getBuiltinModule("node:crypto");
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
return require("crypto");
|
|
1274
|
+
} catch {
|
|
1275
|
+
throw new ZihinAgentError(
|
|
1276
|
+
"config",
|
|
1277
|
+
"signRequest requires Node.js crypto, unavailable in this runtime (the ESM build needs Node >=20.16)."
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
function signRequest(payload, secretKey, timestamp) {
|
|
1282
|
+
const { createHmac, randomBytes } = loadNodeCrypto();
|
|
1283
|
+
const ts = timestamp || Date.now();
|
|
1284
|
+
const nonce = randomBytes(16).toString("hex");
|
|
1285
|
+
const message = `${ts}.${nonce}.${payload}`;
|
|
1286
|
+
const signature = createHmac("sha256", secretKey).update(message).digest("hex");
|
|
1287
|
+
return {
|
|
1288
|
+
"X-Zihin-Signature": `sha256=${signature}`,
|
|
1289
|
+
"X-Zihin-Timestamp": ts.toString(),
|
|
1290
|
+
"X-Zihin-Nonce": nonce
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1294
|
+
0 && (module.exports = {
|
|
1295
|
+
DEFAULT_LIMITS,
|
|
1296
|
+
ZihinAgentClient,
|
|
1297
|
+
ZihinAgentError,
|
|
1298
|
+
createClient,
|
|
1299
|
+
sanitizeApiError,
|
|
1300
|
+
signRequest,
|
|
1301
|
+
validateApiKeyFormat,
|
|
1302
|
+
validateBaseUrl,
|
|
1303
|
+
validatePromptSize
|
|
1304
|
+
});
|