@zihin/agent-client 0.2.0 → 0.4.1

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/dist/index.mjs CHANGED
@@ -29,6 +29,21 @@ function parseUsage(usageRaw) {
29
29
  costUsd: num(u.cost_usd)
30
30
  };
31
31
  }
32
+ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
33
+ "running",
34
+ "pending",
35
+ "queued",
36
+ "processing",
37
+ "in_progress",
38
+ "waiting",
39
+ "retrying",
40
+ "scheduled",
41
+ "awaiting_approval"
42
+ // HITL: turno pausado esperando decisão humana não terminou
43
+ ]);
44
+ function isTerminalStatus(status) {
45
+ return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
46
+ }
32
47
 
33
48
  // src/security.ts
34
49
  var PRIVATE_IP_PATTERNS = [
@@ -211,14 +226,19 @@ var SENSITIVE_PATTERNS = [
211
226
  var ERROR_CODE_MAP = {
212
227
  RATE_LIMIT_EXCEEDED: "Rate limit exceeded. Please wait before retrying.",
213
228
  UNAUTHORIZED: "Authentication failed. Please check your API credentials.",
214
- FORBIDDEN: "This API key lacks permission for this action. Use an admin/editor key (member keys cannot create triggers or read telemetry).",
229
+ FORBIDDEN: "This API key's role does not allow this action. The required role depends on the endpoint \u2014 check the key's role in Zihin \u2192 Settings \u2192 API Keys.",
215
230
  INVALID_MODEL: "The specified model is not available.",
216
231
  INTERNAL_ERROR: "An internal error occurred. Please try again later.",
217
232
  TIMEOUT: "Request timed out. Try increasing the timeout.",
218
233
  INVALID_REQUEST: "The request was malformed. Please check your input.",
219
234
  INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
220
235
  CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
221
- AGENT_NOT_FOUND: "Agent not found or not active for this tenant."
236
+ AGENT_NOT_FOUND: "Agent not found or not active for this tenant.",
237
+ // Bloqueios comerciais. As mensagens do BE vêm em português e estes shells são em inglês;
238
+ // além disso cada caso tem uma ação DIFERENTE, e o texto precisa dizer qual.
239
+ TRIAL_EXPIRED: "Your Zihin trial has ended. Upgrade the plan to keep invoking agents \u2014 the API key is fine.",
240
+ QUOTA_EXCEEDED: "Your Zihin token quota is exhausted. Upgrade the plan or wait for the next cycle \u2014 retrying will not help, and the API key is fine.",
241
+ NO_ACTIVE_SUBSCRIPTION: "This tenant has no active Zihin subscription. Activate a plan to invoke agents \u2014 the API key is fine."
222
242
  };
223
243
  function sanitizeApiError(error) {
224
244
  let message = "An unexpected error occurred";
@@ -231,8 +251,10 @@ function sanitizeApiError(error) {
231
251
  const codeRaw = nested?.code ?? (typeof err.error === "string" ? err.error : err.code);
232
252
  code = typeof codeRaw === "string" ? codeRaw : void 0;
233
253
  const rawMessage = nested?.message ?? err.message;
234
- if (code && ERROR_CODE_MAP[code]) {
235
- message = ERROR_CODE_MAP[code];
254
+ const reason = typeof err.reason === "string" ? err.reason : void 0;
255
+ const effectiveCode = code === "QUOTA_EXCEEDED" && reason === "no_active_subscription" ? "NO_ACTIVE_SUBSCRIPTION" : code;
256
+ if (effectiveCode && ERROR_CODE_MAP[effectiveCode]) {
257
+ message = ERROR_CODE_MAP[effectiveCode];
236
258
  } else if (typeof rawMessage === "string" && rawMessage) {
237
259
  message = rawMessage;
238
260
  }
@@ -281,6 +303,7 @@ var ZihinAgentError = class _ZihinAgentError extends Error {
281
303
  this.status = opts.status;
282
304
  this.code = opts.code;
283
305
  this.requestId = opts.requestId;
306
+ this.executionId = opts.executionId;
284
307
  this.attempts = opts.attempts;
285
308
  this.endpoint = opts.endpoint;
286
309
  this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
@@ -292,25 +315,65 @@ function buildMessage(base, opts) {
292
315
  if (opts.endpoint) s += ` [${opts.endpoint}]`;
293
316
  if (opts.attempts && opts.attempts > 1) s += ` (after ${opts.attempts} attempts)`;
294
317
  if (opts.requestId) s += ` (request_id: ${opts.requestId})`;
318
+ if (opts.executionId) s += ` (execution_id: ${opts.executionId})`;
295
319
  return s;
296
320
  }
321
+ var COMMERCIAL_BLOCK_CODES = /* @__PURE__ */ new Set(["TRIAL_EXPIRED", "QUOTA_EXCEEDED", "NO_ACTIVE_SUBSCRIPTION"]);
322
+ function errorCodeOf(body) {
323
+ const b = rec(body);
324
+ return str(b.error) ?? str(rec(b.error).code) ?? str(b.code);
325
+ }
326
+ function isCommercialBlock(body) {
327
+ const code = errorCodeOf(body);
328
+ return code !== void 0 && COMMERCIAL_BLOCK_CODES.has(code);
329
+ }
330
+ function commercialKind(body) {
331
+ const code = errorCodeOf(body);
332
+ if (code === "TRIAL_EXPIRED") return "trial_expired";
333
+ if (code === "NO_ACTIVE_SUBSCRIPTION") return "no_subscription";
334
+ if (code === "QUOTA_EXCEEDED") {
335
+ return str(rec(body).reason) === "no_active_subscription" ? "no_subscription" : "quota_exceeded";
336
+ }
337
+ return void 0;
338
+ }
297
339
  function extractRequestId(body) {
298
340
  const b = rec(body);
299
- return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id);
341
+ return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id) ?? str(rec(b.meta).request_id);
300
342
  }
301
343
  function errorFromResponse(status, body, meta = {}) {
302
344
  const sanitized = sanitizeApiError(body);
303
345
  const message = sanitized.message;
304
346
  const code = sanitized.code;
305
- const base = { status, code, requestId: extractRequestId(body), ...meta };
347
+ const base = {
348
+ status,
349
+ code,
350
+ requestId: extractRequestId(body),
351
+ executionId: str(rec(body).execution_id),
352
+ ...meta
353
+ };
354
+ const commercial = commercialKind(body);
355
+ if (commercial) {
356
+ return new ZihinAgentError(commercial, message || "Request blocked by your plan.", {
357
+ ...base,
358
+ retryable: false
359
+ });
360
+ }
306
361
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
307
362
  if (status === 403)
308
363
  return new ZihinAgentError(
309
364
  "auth",
310
- message || "This API key lacks permission. Use an admin/editor key (member keys cannot create triggers or read telemetry).",
365
+ // Não enumerar papéis aqui: o papel exigido varia por rota (triggers aceitam editor,
366
+ // mas delegar/cancelar uma task exige admin/owner). A frase antiga mandava o usuário
367
+ // para o papel errado em metade da API.
368
+ message || "This API key's role does not allow this action. The required role depends on the endpoint \u2014 check the key's role in Zihin \u2192 Settings \u2192 API Keys.",
311
369
  base
312
370
  );
313
371
  if (status === 404) return new ZihinAgentError("not_found", message || "Resource not found.", base);
372
+ if (status === 408)
373
+ return new ZihinAgentError("timeout", message || "The server timed out handling the request.", {
374
+ ...base,
375
+ retryable: true
376
+ });
314
377
  if (status === 429)
315
378
  return new ZihinAgentError("rate_limit", message || "Rate limit exceeded.", { ...base, retryable: true });
316
379
  if (status >= 500)
@@ -340,7 +403,25 @@ function errorFromThrow(err, timeoutMs, meta = {}) {
340
403
 
341
404
  // src/http.ts
342
405
  var DEFAULT_BASE_URL = "https://llm.zihin.ai";
343
- var DEFAULT_TIMEOUT_MS = 12e4;
406
+ var OPERATION_TIMEOUT_MS = Object.freeze({
407
+ stream: 2e5,
408
+ dispatch: 26e4,
409
+ short: 35e3
410
+ });
411
+ var DEADLINE_GRACE_MS = 1e4;
412
+ var RETRY_OVERHEAD_MS = 6e4;
413
+ var MIN_TIMEOUT_MS = 1e3;
414
+ var MAX_TIMEOUT_MS = 9e5;
415
+ function normalizeTimeoutMs(value, label = "timeoutMs") {
416
+ const n = typeof value === "number" ? value : Number(value);
417
+ if (!Number.isFinite(n) || n <= 0) {
418
+ throw new ZihinAgentError(
419
+ "config",
420
+ `${label} must be a positive number of milliseconds (got ${JSON.stringify(value)}).`
421
+ );
422
+ }
423
+ return Math.min(Math.max(n, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS);
424
+ }
344
425
  var DEFAULT_MAX_RETRIES = 2;
345
426
  var DEFAULT_MAX_EVENT_BYTES = 1 * 1024 * 1024;
346
427
  var DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
@@ -373,7 +454,7 @@ function resolveConfig(config) {
373
454
  return {
374
455
  apiKey: config.apiKey.trim(),
375
456
  baseUrl: urlCheck.sanitizedUrl || DEFAULT_BASE_URL,
376
- timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
457
+ timeoutMs: config.timeoutMs === void 0 || config.timeoutMs === null ? void 0 : normalizeTimeoutMs(config.timeoutMs, "timeoutMs"),
377
458
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
378
459
  maxStreamEventBytes: config.maxStreamEventBytes ?? DEFAULT_MAX_EVENT_BYTES,
379
460
  maxStreamTotalBytes: config.maxStreamTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
@@ -382,6 +463,9 @@ function resolveConfig(config) {
382
463
  onError: config.onError
383
464
  };
384
465
  }
466
+ function resolveTimeout(cfg, operation) {
467
+ return cfg.timeoutMs ?? OPERATION_TIMEOUT_MS[operation];
468
+ }
385
469
  function endpointLabel(method, url) {
386
470
  try {
387
471
  const u = new URL(url);
@@ -455,12 +539,27 @@ function createDeadline(timeoutMs, callerSignal) {
455
539
  };
456
540
  }
457
541
  function shouldRetryStatus(status, policy) {
458
- if (policy === "idempotent") return status >= 500 || status === 429;
542
+ if (policy === "idempotent") return status >= 500 || status === 429 || status === 408;
459
543
  return status === 429 || status === 503;
460
544
  }
545
+ function budgetTracker(args) {
546
+ const startedAt = Date.now();
547
+ const reported = args.reportedTimeoutMs ?? args.timeoutMs;
548
+ const overheadCap = Math.min(args.timeoutMs, RETRY_OVERHEAD_MS);
549
+ return {
550
+ reported,
551
+ /** Quanto ainda pode ser gasto com tentativas falhas + backoff. */
552
+ overheadLeft: () => overheadCap - (Date.now() - startedAt),
553
+ expired: (attempts) => new ZihinAgentError("timeout", `Request timed out after ${reported}ms.`, {
554
+ endpoint: args.endpoint,
555
+ attempts
556
+ })
557
+ };
558
+ }
461
559
  async function requestBuffered(args) {
462
560
  const endpoint = endpointLabel(args.method, args.url);
463
561
  const doFetch = resolveFetch(args.fetchImpl);
562
+ const budget = budgetTracker({ ...args, endpoint });
464
563
  let lastError = null;
465
564
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
466
565
  const d = createDeadline(args.timeoutMs, args.callerSignal);
@@ -475,30 +574,39 @@ async function requestBuffered(args) {
475
574
  cache: "no-store"
476
575
  // evita cache stale (ex.: polling em Next.js/edge)
477
576
  });
478
- if (shouldRetryStatus(res.status, args.policy) && attempt < args.maxRetries) {
479
- const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
480
- try {
481
- await res.body?.cancel();
482
- } catch {
577
+ let wait = shouldRetryStatus(res.status, args.policy) && attempt < args.maxRetries ? retryAfterMs(res.headers) ?? backoffMs(attempt) : null;
578
+ let peeked;
579
+ if (wait !== null && res.status === 429) {
580
+ peeked = await res.text();
581
+ if (isCommercialBlock(parseBody(peeked))) wait = null;
582
+ }
583
+ if (wait !== null && budget.overheadLeft() > wait) {
584
+ if (peeked === void 0) {
585
+ try {
586
+ await res.body?.cancel();
587
+ } catch {
588
+ }
483
589
  }
484
590
  d.dispose();
485
591
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
486
592
  await delay(wait, args.callerSignal);
487
593
  continue;
488
594
  }
489
- const text = await res.text();
595
+ const text = peeked ?? await res.text();
490
596
  d.dispose();
491
597
  return { status: res.status, ok: res.ok, text, endpoint, attempts: attempt + 1 };
492
598
  } catch (err) {
493
599
  d.dispose();
600
+ if (err instanceof ZihinAgentError) throw err;
494
601
  const meta = { endpoint, attempts: attempt + 1 };
495
- if (d.timedOut()) throw new ZihinAgentError("timeout", `Request timed out after ${args.timeoutMs}ms.`, meta);
602
+ if (d.timedOut()) throw budget.expired(attempt + 1);
496
603
  if (d.abortedByCaller())
497
604
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
498
- lastError = errorFromThrow(err, args.timeoutMs, meta);
605
+ lastError = errorFromThrow(err, budget.reported, meta);
499
606
  const canRetry = args.policy === "idempotent" && lastError.retryable && attempt < args.maxRetries;
500
607
  if (!canRetry) throw lastError;
501
608
  const wait = backoffMs(attempt);
609
+ if (budget.overheadLeft() <= wait) throw lastError;
502
610
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
503
611
  await delay(wait, args.callerSignal);
504
612
  }
@@ -508,6 +616,7 @@ async function requestBuffered(args) {
508
616
  async function openStream(args) {
509
617
  const endpoint = endpointLabel("POST", args.url);
510
618
  const doFetch = resolveFetch(args.fetchImpl);
619
+ const budget = budgetTracker({ ...args, endpoint });
511
620
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
512
621
  const d = createDeadline(args.timeoutMs, args.callerSignal);
513
622
  try {
@@ -519,8 +628,20 @@ async function openStream(args) {
519
628
  redirect: "error",
520
629
  cache: "no-store"
521
630
  });
522
- if (shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries) {
523
- const wait = retryAfterMs(response.headers) ?? backoffMs(attempt);
631
+ let wait = shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
632
+ if (response.status === 429) {
633
+ const text = await response.text().catch(() => "");
634
+ const body = parseBody(text);
635
+ if (wait === null || isCommercialBlock(body) || budget.overheadLeft() <= wait) {
636
+ d.dispose();
637
+ throw errorFromResponse(response.status, body, { endpoint, attempts: attempt + 1 });
638
+ }
639
+ d.dispose();
640
+ args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
641
+ await delay(wait, args.callerSignal);
642
+ continue;
643
+ }
644
+ if (wait !== null && budget.overheadLeft() > wait) {
524
645
  try {
525
646
  await response.body?.cancel();
526
647
  } catch {
@@ -539,15 +660,51 @@ async function openStream(args) {
539
660
  return { response, deadline: d, endpoint };
540
661
  } catch (err) {
541
662
  d.dispose();
663
+ if (err instanceof ZihinAgentError) throw err;
542
664
  const meta = { endpoint, attempts: attempt + 1 };
543
- if (d.timedOut()) throw new ZihinAgentError("timeout", `Request timed out after ${args.timeoutMs}ms.`, meta);
665
+ if (d.timedOut()) throw budget.expired(attempt + 1);
544
666
  if (d.abortedByCaller())
545
667
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
546
- throw errorFromThrow(err, args.timeoutMs, meta);
668
+ throw errorFromThrow(err, budget.reported, meta);
547
669
  }
548
670
  }
549
671
  throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
550
672
  }
673
+ function toJsonBody(value, label = "body") {
674
+ try {
675
+ return JSON.stringify(value);
676
+ } catch (err) {
677
+ throw new ZihinAgentError(
678
+ "config",
679
+ `${label} is not JSON-serializable: ${err?.message ?? "unknown reason"}.`
680
+ );
681
+ }
682
+ }
683
+ async function requestJson(cfg, args) {
684
+ const res = await requestBuffered({
685
+ method: args.method,
686
+ url: `${cfg.baseUrl}${args.path}`,
687
+ headers: buildHeaders(cfg.apiKey),
688
+ body: args.body !== void 0 ? toJsonBody(args.body) : void 0,
689
+ timeoutMs: resolveTimeout(cfg, args.operation ?? "short"),
690
+ maxRetries: cfg.maxRetries,
691
+ policy: args.policy,
692
+ callerSignal: args.signal,
693
+ fetchImpl: cfg.fetchImpl,
694
+ onRetry: cfg.onRetry
695
+ });
696
+ if (!res.ok) {
697
+ throw errorFromResponse(res.status, parseBody(res.text), {
698
+ endpoint: res.endpoint,
699
+ attempts: res.attempts
700
+ });
701
+ }
702
+ try {
703
+ return JSON.parse(res.text);
704
+ } catch {
705
+ throw new ZihinAgentError("protocol", "Expected JSON response.", { endpoint: res.endpoint });
706
+ }
707
+ }
551
708
  function parseBody(text) {
552
709
  if (!text) return {};
553
710
  try {
@@ -563,7 +720,7 @@ async function listAgents(cfg, opts = {}) {
563
720
  method: "GET",
564
721
  url: `${cfg.baseUrl}/api/v2/agents`,
565
722
  headers: buildHeaders(cfg.apiKey),
566
- timeoutMs: cfg.timeoutMs,
723
+ timeoutMs: resolveTimeout(cfg, "short"),
567
724
  maxRetries: cfg.maxRetries,
568
725
  policy: "idempotent",
569
726
  callerSignal: opts.signal,
@@ -730,6 +887,21 @@ function mapEvent(e) {
730
887
  return { type: "token", content: str(d.content) ?? "", data: e.data };
731
888
  case "warning":
732
889
  return { type: "warning", message: str(d.message), data: e.data };
890
+ case "session":
891
+ return { type: "session", sessionId: str(d.session_id), data: e.data };
892
+ case "done":
893
+ return { type: "done", data: e.data };
894
+ case "error": {
895
+ const err = rec(d.error);
896
+ return {
897
+ type: "error",
898
+ code: str(err.code),
899
+ message: str(err.message),
900
+ requestId: str(err.request_id),
901
+ timedOut: typeof err.timed_out === "boolean" ? err.timed_out : void 0,
902
+ data: e.data
903
+ };
904
+ }
733
905
  case "response":
734
906
  return {
735
907
  type: "response",
@@ -754,7 +926,7 @@ function mapEvent(e) {
754
926
  return { type: "unknown", event: e.event, data: e.data };
755
927
  }
756
928
  }
757
- function buildRequestBody(params) {
929
+ function buildRequestBody(params, normalizedTimeoutMs) {
758
930
  const body = {};
759
931
  if (params.message !== void 0) body.message = params.message;
760
932
  if (params.sessionId) body.session_id = params.sessionId;
@@ -765,7 +937,7 @@ function buildRequestBody(params) {
765
937
  const o = {};
766
938
  if (params.options.temperature !== void 0) o.temperature = params.options.temperature;
767
939
  if (params.options.maxTokens !== void 0) o.max_tokens = params.options.maxTokens;
768
- if (params.options.timeoutMs !== void 0) o.timeout_ms = params.options.timeoutMs;
940
+ if (normalizedTimeoutMs !== void 0) o.timeout_ms = normalizedTimeoutMs;
769
941
  if (params.options.jitMode !== void 0) o.jit_mode = params.options.jitMode;
770
942
  if (Object.keys(o).length) body.options = o;
771
943
  }
@@ -796,6 +968,9 @@ function validateParams(params) {
796
968
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
797
969
  }
798
970
  }
971
+ if (params.options?.timeoutMs !== void 0) {
972
+ normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
973
+ }
799
974
  for (const att of params.attachments ?? []) {
800
975
  const urlCheck = validateBaseUrl(att.url, { allowCustomDomains: true });
801
976
  if (!urlCheck.valid) {
@@ -805,13 +980,16 @@ function validateParams(params) {
805
980
  }
806
981
  async function* streamAgent(cfg, params) {
807
982
  validateParams(params);
808
- const timeoutMs = params.options?.timeoutMs ?? cfg.timeoutMs;
983
+ const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
984
+ const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
985
+ const reportedMs = callerTimeoutMs ?? timeoutMs;
809
986
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
810
987
  const { response, deadline, endpoint } = await openStream({
811
988
  url,
812
989
  headers: buildHeaders(cfg.apiKey),
813
- body: JSON.stringify(buildRequestBody(params)),
990
+ body: toJsonBody(buildRequestBody(params, callerTimeoutMs), "request body"),
814
991
  timeoutMs,
992
+ reportedTimeoutMs: reportedMs,
815
993
  maxRetries: cfg.maxRetries,
816
994
  callerSignal: params.signal,
817
995
  fetchImpl: cfg.fetchImpl,
@@ -826,7 +1004,7 @@ async function* streamAgent(cfg, params) {
826
1004
  }
827
1005
  throw errorFromResponse(response.status, parseBody(text), { endpoint });
828
1006
  }
829
- 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 });
1007
+ const onAbortError = () => deadline.timedOut() ? new ZihinAgentError("timeout", `Stream timed out after ${reportedMs}ms.`, { endpoint }) : deadline.abortedByCaller() ? new ZihinAgentError("aborted", "Stream aborted by the caller.", { endpoint }) : new ZihinAgentError("network", "Stream connection interrupted.", { endpoint });
830
1008
  for await (const e of parseSse(response, {
831
1009
  signal: params.signal,
832
1010
  onAbortError,
@@ -839,25 +1017,50 @@ async function* streamAgent(cfg, params) {
839
1017
  deadline.dispose();
840
1018
  }
841
1019
  }
1020
+ function classifyStreamErrorKind(code, timedOut) {
1021
+ if (timedOut === true) return "timeout";
1022
+ if (timedOut === false) return "server";
1023
+ return /TIMEOUT/i.test(code) ? "timeout" : "server";
1024
+ }
1025
+ function errorFromStreamChunk(chunk) {
1026
+ const code = chunk.code ?? "AGENT_ERROR";
1027
+ const kind = classifyStreamErrorKind(code, chunk.timedOut);
1028
+ return new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1029
+ code,
1030
+ requestId: chunk.requestId,
1031
+ retryable: false
1032
+ });
1033
+ }
842
1034
  async function invokeAgent(cfg, params) {
843
1035
  const raw = [];
844
1036
  const keepRaw = params.includeRaw === true;
845
1037
  let responseChunk;
846
1038
  let metricsChunk;
847
1039
  let metadataSessionId;
1040
+ let mintedSessionId;
1041
+ let cleanEnd = false;
848
1042
  let tokenBuffer = "";
849
1043
  for await (const chunk of streamAgent(cfg, params)) {
850
1044
  if (keepRaw) raw.push(chunk);
1045
+ if (chunk.type === "error") {
1046
+ throw errorFromStreamChunk(chunk);
1047
+ }
851
1048
  if (chunk.type === "response") responseChunk = chunk;
852
1049
  else if (chunk.type === "metrics") metricsChunk = chunk;
853
1050
  else if (chunk.type === "token") tokenBuffer += chunk.content;
854
1051
  else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1052
+ else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1053
+ else if (chunk.type === "done") cleanEnd = true;
855
1054
  }
856
1055
  if (!responseChunk) {
1056
+ if (cleanEnd) {
1057
+ throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
1058
+ }
857
1059
  if (tokenBuffer) {
858
1060
  return {
859
1061
  content: tokenBuffer,
860
- sessionId: metadataSessionId ?? params.sessionId,
1062
+ contentSource: "tokens",
1063
+ sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
861
1064
  usage: metricsChunk?.usage,
862
1065
  model: metricsChunk?.model,
863
1066
  iterations: metricsChunk?.iterations,
@@ -868,8 +1071,9 @@ async function invokeAgent(cfg, params) {
868
1071
  throw new ZihinAgentError("protocol", "Stream ended without a `response` event.");
869
1072
  }
870
1073
  return {
871
- content: responseChunk.content || tokenBuffer,
872
- sessionId: responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1074
+ content: responseChunk.content,
1075
+ contentSource: "response",
1076
+ sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
873
1077
  model: metricsChunk?.model,
874
1078
  usage: metricsChunk?.usage,
875
1079
  sources: responseChunk.sources,
@@ -880,29 +1084,180 @@ async function invokeAgent(cfg, params) {
880
1084
  };
881
1085
  }
882
1086
 
883
- // src/triggers.ts
884
- async function requestJson(cfg, args) {
885
- const res = await requestBuffered({
886
- method: args.method,
887
- url: `${cfg.baseUrl}${args.path}`,
888
- headers: buildHeaders(cfg.apiKey),
889
- body: args.body !== void 0 ? JSON.stringify(args.body) : void 0,
890
- timeoutMs: cfg.timeoutMs,
891
- maxRetries: cfg.maxRetries,
892
- policy: args.policy,
893
- callerSignal: args.signal,
894
- fetchImpl: cfg.fetchImpl,
895
- onRetry: cfg.onRetry
1087
+ // src/tasks.ts
1088
+ var MAX_TASK_PAYLOAD_BYTES = 64 * 1024;
1089
+ var SCHEDULE_PAST_TOLERANCE_MS = 6e4;
1090
+ function envelope(json) {
1091
+ const j = rec(json);
1092
+ return { data: rec(j.data ?? j), meta: rec(j.meta) };
1093
+ }
1094
+ function toIso(value, label) {
1095
+ if (value === void 0 || value === null || value === "") return void 0;
1096
+ const d = value instanceof Date ? value : new Date(value);
1097
+ if (Number.isNaN(d.getTime())) {
1098
+ throw new ZihinAgentError("config", `${label} must be a valid ISO 8601 date (got ${JSON.stringify(value)}).`);
1099
+ }
1100
+ return d.toISOString();
1101
+ }
1102
+ function mapTask(raw) {
1103
+ const t = rec(raw);
1104
+ const status = str(t.status);
1105
+ return {
1106
+ id: str(t.id) ?? "",
1107
+ agentId: str(t.agent_id),
1108
+ title: str(t.title),
1109
+ instruction: str(t.instruction),
1110
+ status: status ?? "unknown",
1111
+ // Mesma regra do Caminho B, e de propósito a MESMA função: status desconhecido conta como
1112
+ // terminal (o polling para em vez de girar), status ausente não (não lemos o envelope).
1113
+ terminal: status === void 0 ? false : isTerminalStatus(status),
1114
+ scheduledFor: str(t.scheduled_for),
1115
+ deadlineAt: str(t.deadline_at),
1116
+ parentTaskId: str(t.parent_task_id),
1117
+ rootTaskId: str(t.root_task_id),
1118
+ sessionId: str(t.session_id),
1119
+ createdAt: str(t.created_at),
1120
+ updatedAt: str(t.updated_at),
1121
+ raw
1122
+ };
1123
+ }
1124
+ function mapExecution(raw) {
1125
+ const e = rec(raw);
1126
+ return {
1127
+ id: str(e.id) ?? "",
1128
+ status: str(e.status) ?? "unknown",
1129
+ exitReason: str(e.exit_reason),
1130
+ modelUsed: str(e.model_used),
1131
+ totalCostUsd: num(e.total_cost_usd),
1132
+ executionTimeMs: num(e.execution_time_ms),
1133
+ iterations: num(e.iterations),
1134
+ startedAt: str(e.started_at),
1135
+ finishedAt: str(e.finished_at),
1136
+ raw
1137
+ };
1138
+ }
1139
+ async function delegateTask(cfg, params) {
1140
+ if (!params.agentId) throw new ZihinAgentError("config", "agentId is required.");
1141
+ if (!params.instruction) throw new ZihinAgentError("config", "instruction is required.");
1142
+ const scheduledFor = toIso(params.scheduledFor, "scheduledFor");
1143
+ const deadlineAt = toIso(params.deadlineAt, "deadlineAt");
1144
+ if (scheduledFor && Date.parse(scheduledFor) < Date.now() - SCHEDULE_PAST_TOLERANCE_MS) {
1145
+ throw new ZihinAgentError("config", "scheduledFor is in the past.");
1146
+ }
1147
+ if (deadlineAt && Date.parse(deadlineAt) <= Date.now() - SCHEDULE_PAST_TOLERANCE_MS) {
1148
+ throw new ZihinAgentError("config", "deadlineAt must be in the future.");
1149
+ }
1150
+ if (scheduledFor && deadlineAt && Date.parse(deadlineAt) <= Date.parse(scheduledFor)) {
1151
+ throw new ZihinAgentError("config", "deadlineAt must be after scheduledFor.");
1152
+ }
1153
+ if (params.payload !== void 0) {
1154
+ if (typeof params.payload !== "object" || params.payload === null || Array.isArray(params.payload)) {
1155
+ throw new ZihinAgentError("config", "payload must be an object.");
1156
+ }
1157
+ const bytes = toJsonBody(params.payload, "payload").length;
1158
+ if (bytes > MAX_TASK_PAYLOAD_BYTES) {
1159
+ throw new ZihinAgentError(
1160
+ "config",
1161
+ `payload exceeds ${MAX_TASK_PAYLOAD_BYTES / 1024}KB (got ${Math.ceil(bytes / 1024)}KB).`
1162
+ );
1163
+ }
1164
+ }
1165
+ const body = {
1166
+ agent_id: params.agentId,
1167
+ instruction: params.instruction
1168
+ };
1169
+ if (params.title !== void 0) body.title = params.title;
1170
+ if (params.payload !== void 0) body.payload = params.payload;
1171
+ if (scheduledFor) body.scheduled_for = scheduledFor;
1172
+ if (deadlineAt) body.deadline_at = deadlineAt;
1173
+ if (params.retryPolicy !== void 0) body.retry_policy = params.retryPolicy;
1174
+ if (params.notify !== void 0) body.notify = params.notify;
1175
+ if (params.parentTaskId) body.parent_task_id = params.parentTaskId;
1176
+ const json = await requestJson(cfg, {
1177
+ method: "POST",
1178
+ path: "/api/v1/tasks",
1179
+ body,
1180
+ // Criar uma task E dispará-la é efeito colateral: nunca repetir em erro de rede.
1181
+ policy: "side-effecting",
1182
+ signal: params.signal
896
1183
  });
897
- if (!res.ok) {
898
- throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
1184
+ const { data } = envelope(json);
1185
+ const task = mapTask(data.task ?? data);
1186
+ if (!task.id) {
1187
+ throw new ZihinAgentError("protocol", "delegateTask: response has no task id.");
899
1188
  }
900
- try {
901
- return JSON.parse(res.text);
902
- } catch {
903
- throw new ZihinAgentError("protocol", "Expected JSON response.", { endpoint: res.endpoint });
1189
+ return task;
1190
+ }
1191
+ async function listTasks(cfg, params = {}) {
1192
+ const qs = new URLSearchParams();
1193
+ if (params.live) qs.set("live", "true");
1194
+ if (params.status) qs.set("status", params.status);
1195
+ if (params.agentId) qs.set("agent_id", params.agentId);
1196
+ if (params.rootTaskId) qs.set("root_task_id", params.rootTaskId);
1197
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
1198
+ if (params.offset !== void 0) qs.set("offset", String(params.offset));
1199
+ const query = qs.toString();
1200
+ const json = await requestJson(cfg, {
1201
+ method: "GET",
1202
+ path: `/api/v1/tasks${query ? `?${query}` : ""}`,
1203
+ policy: "idempotent",
1204
+ signal: params.signal
1205
+ });
1206
+ const { data, meta } = envelope(json);
1207
+ const items = Array.isArray(data.items) ? data.items : [];
1208
+ return {
1209
+ items: items.map(mapTask),
1210
+ // ⚠️ `total` vive em `meta`, não no topo — o envelope novo aninhou os campos de paginação.
1211
+ total: num(meta.total),
1212
+ raw: json
1213
+ };
1214
+ }
1215
+ async function getTask(cfg, params) {
1216
+ if (!params.taskId) throw new ZihinAgentError("config", "taskId is required.");
1217
+ const json = await requestJson(cfg, {
1218
+ method: "GET",
1219
+ path: `/api/v1/tasks/${encodeURIComponent(params.taskId)}`,
1220
+ policy: "idempotent",
1221
+ signal: params.signal
1222
+ });
1223
+ const { data } = envelope(json);
1224
+ const taskRaw = data.task ?? (str(rec(data).id) ? data : void 0);
1225
+ if (!taskRaw || !str(rec(taskRaw).id)) {
1226
+ throw new ZihinAgentError("protocol", "getTask: response has no task.");
904
1227
  }
1228
+ const executions = Array.isArray(data.executions) ? data.executions : [];
1229
+ const children = Array.isArray(data.children) ? data.children : [];
1230
+ return {
1231
+ task: mapTask(taskRaw),
1232
+ executions: executions.map(mapExecution),
1233
+ children: children.map(mapTask),
1234
+ approvals: Array.isArray(data.approvals) ? data.approvals : [],
1235
+ raw: json
1236
+ };
905
1237
  }
1238
+ async function cancelTask(cfg, params) {
1239
+ if (!params.taskId) throw new ZihinAgentError("config", "taskId is required.");
1240
+ const json = await requestJson(cfg, {
1241
+ method: "POST",
1242
+ path: `/api/v1/tasks/${encodeURIComponent(params.taskId)}/cancel`,
1243
+ body: params.reason ? { reason: params.reason } : {},
1244
+ // Cancelar é side-effecting; repetir num socket caído bateria em 409 e confundiria o chamador.
1245
+ policy: "side-effecting",
1246
+ signal: params.signal
1247
+ });
1248
+ const { data } = envelope(json);
1249
+ const taskRaw = data.task ?? (str(rec(data).id) ? data : void 0);
1250
+ if (!taskRaw || !str(rec(taskRaw).id)) {
1251
+ throw new ZihinAgentError("protocol", "cancelTask: response has no task.");
1252
+ }
1253
+ return {
1254
+ task: mapTask(taskRaw),
1255
+ aborted: data.aborted === true,
1256
+ raw: json
1257
+ };
1258
+ }
1259
+
1260
+ // src/triggers.ts
906
1261
  function queryExtractionBody(extraction, queryField) {
907
1262
  if (!extraction) return { mode: "field", field: queryField ?? "message" };
908
1263
  switch (extraction.mode) {
@@ -935,11 +1290,18 @@ async function createTrigger(cfg, params) {
935
1290
  throw new ZihinAgentError("config", `Invalid callbackUrl: ${cb.error}`);
936
1291
  }
937
1292
  }
1293
+ if (params.responseTransform && !params.callbackUrl) {
1294
+ throw new ZihinAgentError(
1295
+ "config",
1296
+ "responseTransform requires callbackUrl: the backend only reads it from trigger_config.execution.callback.response_transform, and a callback without a url is rejected."
1297
+ );
1298
+ }
938
1299
  const execution = { mode };
939
1300
  if (params.callbackUrl) {
940
1301
  execution.callback = {
941
1302
  url: params.callbackUrl,
942
- method: params.callbackMethod ?? "POST"
1303
+ method: params.callbackMethod ?? "POST",
1304
+ ...params.responseTransform ? { response_transform: { mode: params.responseTransform } } : {}
943
1305
  };
944
1306
  }
945
1307
  const body = {
@@ -969,8 +1331,8 @@ async function dispatchAgent(cfg, params) {
969
1331
  method: "POST",
970
1332
  url: `${cfg.baseUrl}/api/triggers/webhook/${encodeURIComponent(params.triggerId)}`,
971
1333
  headers: buildHeaders(cfg.apiKey),
972
- body: JSON.stringify(params.payload ?? {}),
973
- timeoutMs: cfg.timeoutMs,
1334
+ body: toJsonBody(params.payload ?? {}, "payload"),
1335
+ timeoutMs: resolveTimeout(cfg, "dispatch"),
974
1336
  maxRetries: cfg.maxRetries,
975
1337
  policy: "side-effecting",
976
1338
  // S1: nunca duplica disparo por erro de rede
@@ -983,12 +1345,29 @@ async function dispatchAgent(cfg, params) {
983
1345
  }
984
1346
  const json = parseBody(res.text);
985
1347
  const j = rec(json);
1348
+ if (j.success === false) {
1349
+ const detail = sanitizeApiError(str(j.error) ?? "").message;
1350
+ throw new ZihinAgentError("server", detail || "The agent run failed.", {
1351
+ status: res.status,
1352
+ endpoint: res.endpoint,
1353
+ attempts: res.attempts,
1354
+ executionId: str(j.execution_id),
1355
+ // Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
1356
+ retryable: false
1357
+ });
1358
+ }
986
1359
  return {
987
1360
  accepted: true,
988
1361
  triggerId: str(j.trigger_id) ?? params.triggerId,
1362
+ agentId: str(j.agent_id),
989
1363
  executionId: str(j.execution_id),
990
1364
  agentResponse: str(j.agent_response),
991
1365
  usage: parseUsage(j.usage),
1366
+ iterations: num(j.iterations),
1367
+ latencyMs: num(j.latency_ms),
1368
+ // Qual texto o BE extraiu do payload — é o campo que denuncia um `query_extraction` mal
1369
+ // configurado (mensagem vazia, ou o payload inteiro serializado virando a pergunta).
1370
+ queryExtracted: str(j.query_extracted),
992
1371
  raw: json
993
1372
  };
994
1373
  }
@@ -1044,7 +1423,7 @@ async function updateTrigger(cfg, params) {
1044
1423
  if (params.name !== void 0) body.name = params.name;
1045
1424
  if (params.description !== void 0) body.description = params.description;
1046
1425
  if (params.enabled !== void 0) body.enabled = params.enabled;
1047
- const touchesConfig = params.queryExtraction !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.mode !== void 0;
1426
+ const touchesConfig = params.queryExtraction !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.responseTransform !== void 0 || params.mode !== void 0;
1048
1427
  if (touchesConfig) {
1049
1428
  const path = `/api/triggers/${encodeURIComponent(params.triggerId)}`;
1050
1429
  const currentJson = rec(
@@ -1062,22 +1441,34 @@ async function updateTrigger(cfg, params) {
1062
1441
  if (params.queryExtraction !== void 0) {
1063
1442
  merged.query_extraction = queryExtractionBody(params.queryExtraction, void 0);
1064
1443
  }
1065
- if (params.mode !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
1444
+ const touchesCallback = params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.responseTransform !== void 0;
1445
+ if (params.mode !== void 0 || touchesCallback) {
1066
1446
  const execution = rec(config.execution);
1067
1447
  const callback = rec(execution.callback);
1068
1448
  const newExecution = { ...execution };
1069
1449
  if (params.mode !== void 0) newExecution.mode = params.mode;
1070
- if (params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
1450
+ if (touchesCallback) {
1071
1451
  const url = params.callbackUrl ?? str(callback.url);
1072
1452
  if (!url) {
1073
1453
  throw new ZihinAgentError(
1074
1454
  "config",
1075
- "callbackUrl is required: this trigger has no callback configured yet."
1455
+ params.responseTransform ? "responseTransform requires a callback: the backend only reads it from trigger_config.execution.callback.response_transform, and a callback without a url is rejected." : "callbackUrl is required: this trigger has no callback configured yet."
1076
1456
  );
1077
1457
  }
1078
1458
  newExecution.callback = {
1459
+ // Preserva o callback atual: trocar só a URL não pode apagar auth/headers/
1460
+ // body_template/content_type/response_transform. Antes este objeto era
1461
+ // SUBSTITUÍDO por { url, method } — a mesma perda silenciosa que o merge de
1462
+ // trigger_config acima existe para evitar, um nível mais fundo.
1463
+ ...callback,
1079
1464
  url,
1080
- method: params.callbackMethod ?? str(callback.method) ?? "POST"
1465
+ method: params.callbackMethod ?? str(callback.method) ?? "POST",
1466
+ ...params.responseTransform ? {
1467
+ response_transform: {
1468
+ ...rec(callback.response_transform),
1469
+ mode: params.responseTransform
1470
+ }
1471
+ } : {}
1081
1472
  };
1082
1473
  }
1083
1474
  if (newExecution.mode === "async" && !rec(newExecution.callback).url) {
@@ -1106,7 +1497,7 @@ async function deleteTrigger(cfg, params) {
1106
1497
  method: "DELETE",
1107
1498
  url: `${cfg.baseUrl}/api/triggers/${encodeURIComponent(params.triggerId)}`,
1108
1499
  headers: buildHeaders(cfg.apiKey),
1109
- timeoutMs: cfg.timeoutMs,
1500
+ timeoutMs: resolveTimeout(cfg, "short"),
1110
1501
  maxRetries: cfg.maxRetries,
1111
1502
  policy: "idempotent",
1112
1503
  // DELETE é idempotente
@@ -1143,9 +1534,11 @@ async function getExecution(cfg, params) {
1143
1534
  const data = rec(json.data);
1144
1535
  const exec = rec(data.execution ?? json.execution);
1145
1536
  const metrics = rec(data.metrics ?? json.metrics);
1537
+ const rawStatus = str(exec.status);
1146
1538
  return {
1147
1539
  executionId: str(exec.id) ?? params.executionId,
1148
- status: str(exec.status) ?? "unknown",
1540
+ status: rawStatus ?? "unknown",
1541
+ terminal: rawStatus === void 0 ? false : isTerminalStatus(rawStatus),
1149
1542
  startedAt: str(exec.started_at),
1150
1543
  finishedAt: str(exec.finished_at),
1151
1544
  executionTimeMs: num(exec.execution_time_ms),
@@ -1226,6 +1619,26 @@ var ZihinAgentClient = class {
1226
1619
  getExecution(params) {
1227
1620
  return this.guard(getExecution(this.cfg, params));
1228
1621
  }
1622
+ // ---- Caminho C: tasks (delegação assíncrona durável) ----
1623
+ /**
1624
+ * Delega uma tarefa a um agente (requer key owner/admin). Sem `scheduledFor`, o BE dispara na
1625
+ * hora fora do request e devolve só o handle — acompanhe com `getTask`.
1626
+ */
1627
+ delegateTask(params) {
1628
+ return this.guard(delegateTask(this.cfg, params));
1629
+ }
1630
+ /** Board de tasks do tenant (requer key owner/admin/editor). */
1631
+ listTasks(params = {}) {
1632
+ return this.guard(listTasks(this.cfg, params));
1633
+ }
1634
+ /** Dossiê de uma task: task + execuções + filhos + aprovações. */
1635
+ getTask(params) {
1636
+ return this.guard(getTask(this.cfg, params));
1637
+ }
1638
+ /** Cancela uma task (requer key owner/admin); derruba o turno em voo, se houver. */
1639
+ cancelTask(params) {
1640
+ return this.guard(cancelTask(this.cfg, params));
1641
+ }
1229
1642
  // ---- Auxiliar ----
1230
1643
  /** Lista os agentes do tenant (dropdown). */
1231
1644
  listAgents(opts = {}) {
@@ -1264,10 +1677,16 @@ function signRequest(payload, secretKey, timestamp) {
1264
1677
  };
1265
1678
  }
1266
1679
  export {
1680
+ DEADLINE_GRACE_MS,
1267
1681
  DEFAULT_LIMITS,
1682
+ MAX_TASK_PAYLOAD_BYTES,
1683
+ OPERATION_TIMEOUT_MS,
1268
1684
  ZihinAgentClient,
1269
1685
  ZihinAgentError,
1686
+ classifyStreamErrorKind,
1270
1687
  createClient,
1688
+ errorFromStreamChunk,
1689
+ isCommercialBlock,
1271
1690
  sanitizeApiError,
1272
1691
  signRequest,
1273
1692
  validateApiKeyFormat,