@zihin/agent-client 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -20,10 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DEADLINE_GRACE_MS: () => DEADLINE_GRACE_MS,
23
24
  DEFAULT_LIMITS: () => DEFAULT_LIMITS,
25
+ MAX_TASK_PAYLOAD_BYTES: () => MAX_TASK_PAYLOAD_BYTES,
26
+ OPERATION_TIMEOUT_MS: () => OPERATION_TIMEOUT_MS,
24
27
  ZihinAgentClient: () => ZihinAgentClient,
25
28
  ZihinAgentError: () => ZihinAgentError,
26
29
  createClient: () => createClient,
30
+ isCommercialBlock: () => isCommercialBlock,
27
31
  sanitizeApiError: () => sanitizeApiError,
28
32
  signRequest: () => signRequest,
29
33
  validateApiKeyFormat: () => validateApiKeyFormat,
@@ -56,6 +60,21 @@ function parseUsage(usageRaw) {
56
60
  costUsd: num(u.cost_usd)
57
61
  };
58
62
  }
63
+ var RUNNING_STATUSES = /* @__PURE__ */ new Set([
64
+ "running",
65
+ "pending",
66
+ "queued",
67
+ "processing",
68
+ "in_progress",
69
+ "waiting",
70
+ "retrying",
71
+ "scheduled",
72
+ "awaiting_approval"
73
+ // HITL: turno pausado esperando decisão humana não terminou
74
+ ]);
75
+ function isTerminalStatus(status) {
76
+ return !RUNNING_STATUSES.has(status.toLowerCase().replace(/-/g, "_"));
77
+ }
59
78
 
60
79
  // src/security.ts
61
80
  var PRIVATE_IP_PATTERNS = [
@@ -238,14 +257,19 @@ var SENSITIVE_PATTERNS = [
238
257
  var ERROR_CODE_MAP = {
239
258
  RATE_LIMIT_EXCEEDED: "Rate limit exceeded. Please wait before retrying.",
240
259
  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).",
260
+ 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.",
242
261
  INVALID_MODEL: "The specified model is not available.",
243
262
  INTERNAL_ERROR: "An internal error occurred. Please try again later.",
244
263
  TIMEOUT: "Request timed out. Try increasing the timeout.",
245
264
  INVALID_REQUEST: "The request was malformed. Please check your input.",
246
265
  INSUFFICIENT_CREDITS: "Insufficient credits. Please check your account balance.",
247
266
  CONTEXT_LENGTH_EXCEEDED: "The input exceeds the model context length limit.",
248
- AGENT_NOT_FOUND: "Agent not found or not active for this tenant."
267
+ AGENT_NOT_FOUND: "Agent not found or not active for this tenant.",
268
+ // Bloqueios comerciais. As mensagens do BE vêm em português e estes shells são em inglês;
269
+ // além disso cada caso tem uma ação DIFERENTE, e o texto precisa dizer qual.
270
+ TRIAL_EXPIRED: "Your Zihin trial has ended. Upgrade the plan to keep invoking agents \u2014 the API key is fine.",
271
+ 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.",
272
+ NO_ACTIVE_SUBSCRIPTION: "This tenant has no active Zihin subscription. Activate a plan to invoke agents \u2014 the API key is fine."
249
273
  };
250
274
  function sanitizeApiError(error) {
251
275
  let message = "An unexpected error occurred";
@@ -258,8 +282,10 @@ function sanitizeApiError(error) {
258
282
  const codeRaw = nested?.code ?? (typeof err.error === "string" ? err.error : err.code);
259
283
  code = typeof codeRaw === "string" ? codeRaw : void 0;
260
284
  const rawMessage = nested?.message ?? err.message;
261
- if (code && ERROR_CODE_MAP[code]) {
262
- message = ERROR_CODE_MAP[code];
285
+ const reason = typeof err.reason === "string" ? err.reason : void 0;
286
+ const effectiveCode = code === "QUOTA_EXCEEDED" && reason === "no_active_subscription" ? "NO_ACTIVE_SUBSCRIPTION" : code;
287
+ if (effectiveCode && ERROR_CODE_MAP[effectiveCode]) {
288
+ message = ERROR_CODE_MAP[effectiveCode];
263
289
  } else if (typeof rawMessage === "string" && rawMessage) {
264
290
  message = rawMessage;
265
291
  }
@@ -308,6 +334,7 @@ var ZihinAgentError = class _ZihinAgentError extends Error {
308
334
  this.status = opts.status;
309
335
  this.code = opts.code;
310
336
  this.requestId = opts.requestId;
337
+ this.executionId = opts.executionId;
311
338
  this.attempts = opts.attempts;
312
339
  this.endpoint = opts.endpoint;
313
340
  this.retryable = opts.retryable ?? (kind === "rate_limit" || kind === "server" || kind === "network");
@@ -319,25 +346,65 @@ function buildMessage(base, opts) {
319
346
  if (opts.endpoint) s += ` [${opts.endpoint}]`;
320
347
  if (opts.attempts && opts.attempts > 1) s += ` (after ${opts.attempts} attempts)`;
321
348
  if (opts.requestId) s += ` (request_id: ${opts.requestId})`;
349
+ if (opts.executionId) s += ` (execution_id: ${opts.executionId})`;
322
350
  return s;
323
351
  }
352
+ var COMMERCIAL_BLOCK_CODES = /* @__PURE__ */ new Set(["TRIAL_EXPIRED", "QUOTA_EXCEEDED", "NO_ACTIVE_SUBSCRIPTION"]);
353
+ function errorCodeOf(body) {
354
+ const b = rec(body);
355
+ return str(b.error) ?? str(rec(b.error).code) ?? str(b.code);
356
+ }
357
+ function isCommercialBlock(body) {
358
+ const code = errorCodeOf(body);
359
+ return code !== void 0 && COMMERCIAL_BLOCK_CODES.has(code);
360
+ }
361
+ function commercialKind(body) {
362
+ const code = errorCodeOf(body);
363
+ if (code === "TRIAL_EXPIRED") return "trial_expired";
364
+ if (code === "NO_ACTIVE_SUBSCRIPTION") return "no_subscription";
365
+ if (code === "QUOTA_EXCEEDED") {
366
+ return str(rec(body).reason) === "no_active_subscription" ? "no_subscription" : "quota_exceeded";
367
+ }
368
+ return void 0;
369
+ }
324
370
  function extractRequestId(body) {
325
371
  const b = rec(body);
326
- return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id);
372
+ return str(b.request_id) ?? str(b.requestId) ?? str(rec(b.error).request_id) ?? str(rec(b.meta).request_id);
327
373
  }
328
374
  function errorFromResponse(status, body, meta = {}) {
329
375
  const sanitized = sanitizeApiError(body);
330
376
  const message = sanitized.message;
331
377
  const code = sanitized.code;
332
- const base = { status, code, requestId: extractRequestId(body), ...meta };
378
+ const base = {
379
+ status,
380
+ code,
381
+ requestId: extractRequestId(body),
382
+ executionId: str(rec(body).execution_id),
383
+ ...meta
384
+ };
385
+ const commercial = commercialKind(body);
386
+ if (commercial) {
387
+ return new ZihinAgentError(commercial, message || "Request blocked by your plan.", {
388
+ ...base,
389
+ retryable: false
390
+ });
391
+ }
333
392
  if (status === 401) return new ZihinAgentError("auth", message || "Authentication failed.", base);
334
393
  if (status === 403)
335
394
  return new ZihinAgentError(
336
395
  "auth",
337
- message || "This API key lacks permission. Use an admin/editor key (member keys cannot create triggers or read telemetry).",
396
+ // Não enumerar papéis aqui: o papel exigido varia por rota (triggers aceitam editor,
397
+ // mas delegar/cancelar uma task exige admin/owner). A frase antiga mandava o usuário
398
+ // para o papel errado em metade da API.
399
+ 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.",
338
400
  base
339
401
  );
340
402
  if (status === 404) return new ZihinAgentError("not_found", message || "Resource not found.", base);
403
+ if (status === 408)
404
+ return new ZihinAgentError("timeout", message || "The server timed out handling the request.", {
405
+ ...base,
406
+ retryable: true
407
+ });
341
408
  if (status === 429)
342
409
  return new ZihinAgentError("rate_limit", message || "Rate limit exceeded.", { ...base, retryable: true });
343
410
  if (status >= 500)
@@ -367,7 +434,25 @@ function errorFromThrow(err, timeoutMs, meta = {}) {
367
434
 
368
435
  // src/http.ts
369
436
  var DEFAULT_BASE_URL = "https://llm.zihin.ai";
370
- var DEFAULT_TIMEOUT_MS = 12e4;
437
+ var OPERATION_TIMEOUT_MS = Object.freeze({
438
+ stream: 2e5,
439
+ dispatch: 26e4,
440
+ short: 35e3
441
+ });
442
+ var DEADLINE_GRACE_MS = 1e4;
443
+ var RETRY_OVERHEAD_MS = 6e4;
444
+ var MIN_TIMEOUT_MS = 1e3;
445
+ var MAX_TIMEOUT_MS = 9e5;
446
+ function normalizeTimeoutMs(value, label = "timeoutMs") {
447
+ const n = typeof value === "number" ? value : Number(value);
448
+ if (!Number.isFinite(n) || n <= 0) {
449
+ throw new ZihinAgentError(
450
+ "config",
451
+ `${label} must be a positive number of milliseconds (got ${JSON.stringify(value)}).`
452
+ );
453
+ }
454
+ return Math.min(Math.max(n, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS);
455
+ }
371
456
  var DEFAULT_MAX_RETRIES = 2;
372
457
  var DEFAULT_MAX_EVENT_BYTES = 1 * 1024 * 1024;
373
458
  var DEFAULT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
@@ -400,7 +485,7 @@ function resolveConfig(config) {
400
485
  return {
401
486
  apiKey: config.apiKey.trim(),
402
487
  baseUrl: urlCheck.sanitizedUrl || DEFAULT_BASE_URL,
403
- timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
488
+ timeoutMs: config.timeoutMs === void 0 || config.timeoutMs === null ? void 0 : normalizeTimeoutMs(config.timeoutMs, "timeoutMs"),
404
489
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
405
490
  maxStreamEventBytes: config.maxStreamEventBytes ?? DEFAULT_MAX_EVENT_BYTES,
406
491
  maxStreamTotalBytes: config.maxStreamTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
@@ -409,6 +494,9 @@ function resolveConfig(config) {
409
494
  onError: config.onError
410
495
  };
411
496
  }
497
+ function resolveTimeout(cfg, operation) {
498
+ return cfg.timeoutMs ?? OPERATION_TIMEOUT_MS[operation];
499
+ }
412
500
  function endpointLabel(method, url) {
413
501
  try {
414
502
  const u = new URL(url);
@@ -482,12 +570,27 @@ function createDeadline(timeoutMs, callerSignal) {
482
570
  };
483
571
  }
484
572
  function shouldRetryStatus(status, policy) {
485
- if (policy === "idempotent") return status >= 500 || status === 429;
573
+ if (policy === "idempotent") return status >= 500 || status === 429 || status === 408;
486
574
  return status === 429 || status === 503;
487
575
  }
576
+ function budgetTracker(args) {
577
+ const startedAt = Date.now();
578
+ const reported = args.reportedTimeoutMs ?? args.timeoutMs;
579
+ const overheadCap = Math.min(args.timeoutMs, RETRY_OVERHEAD_MS);
580
+ return {
581
+ reported,
582
+ /** Quanto ainda pode ser gasto com tentativas falhas + backoff. */
583
+ overheadLeft: () => overheadCap - (Date.now() - startedAt),
584
+ expired: (attempts) => new ZihinAgentError("timeout", `Request timed out after ${reported}ms.`, {
585
+ endpoint: args.endpoint,
586
+ attempts
587
+ })
588
+ };
589
+ }
488
590
  async function requestBuffered(args) {
489
591
  const endpoint = endpointLabel(args.method, args.url);
490
592
  const doFetch = resolveFetch(args.fetchImpl);
593
+ const budget = budgetTracker({ ...args, endpoint });
491
594
  let lastError = null;
492
595
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
493
596
  const d = createDeadline(args.timeoutMs, args.callerSignal);
@@ -502,30 +605,39 @@ async function requestBuffered(args) {
502
605
  cache: "no-store"
503
606
  // evita cache stale (ex.: polling em Next.js/edge)
504
607
  });
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 {
608
+ let wait = shouldRetryStatus(res.status, args.policy) && attempt < args.maxRetries ? retryAfterMs(res.headers) ?? backoffMs(attempt) : null;
609
+ let peeked;
610
+ if (wait !== null && res.status === 429) {
611
+ peeked = await res.text();
612
+ if (isCommercialBlock(parseBody(peeked))) wait = null;
613
+ }
614
+ if (wait !== null && budget.overheadLeft() > wait) {
615
+ if (peeked === void 0) {
616
+ try {
617
+ await res.body?.cancel();
618
+ } catch {
619
+ }
510
620
  }
511
621
  d.dispose();
512
622
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: res.status });
513
623
  await delay(wait, args.callerSignal);
514
624
  continue;
515
625
  }
516
- const text = await res.text();
626
+ const text = peeked ?? await res.text();
517
627
  d.dispose();
518
628
  return { status: res.status, ok: res.ok, text, endpoint, attempts: attempt + 1 };
519
629
  } catch (err) {
520
630
  d.dispose();
631
+ if (err instanceof ZihinAgentError) throw err;
521
632
  const meta = { endpoint, attempts: attempt + 1 };
522
- if (d.timedOut()) throw new ZihinAgentError("timeout", `Request timed out after ${args.timeoutMs}ms.`, meta);
633
+ if (d.timedOut()) throw budget.expired(attempt + 1);
523
634
  if (d.abortedByCaller())
524
635
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
525
- lastError = errorFromThrow(err, args.timeoutMs, meta);
636
+ lastError = errorFromThrow(err, budget.reported, meta);
526
637
  const canRetry = args.policy === "idempotent" && lastError.retryable && attempt < args.maxRetries;
527
638
  if (!canRetry) throw lastError;
528
639
  const wait = backoffMs(attempt);
640
+ if (budget.overheadLeft() <= wait) throw lastError;
529
641
  args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "network" });
530
642
  await delay(wait, args.callerSignal);
531
643
  }
@@ -535,6 +647,7 @@ async function requestBuffered(args) {
535
647
  async function openStream(args) {
536
648
  const endpoint = endpointLabel("POST", args.url);
537
649
  const doFetch = resolveFetch(args.fetchImpl);
650
+ const budget = budgetTracker({ ...args, endpoint });
538
651
  for (let attempt = 0; attempt <= args.maxRetries; attempt++) {
539
652
  const d = createDeadline(args.timeoutMs, args.callerSignal);
540
653
  try {
@@ -546,8 +659,20 @@ async function openStream(args) {
546
659
  redirect: "error",
547
660
  cache: "no-store"
548
661
  });
549
- if (shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries) {
550
- const wait = retryAfterMs(response.headers) ?? backoffMs(attempt);
662
+ let wait = shouldRetryStatus(response.status, "side-effecting") && attempt < args.maxRetries ? retryAfterMs(response.headers) ?? backoffMs(attempt) : null;
663
+ if (response.status === 429) {
664
+ const text = await response.text().catch(() => "");
665
+ const body = parseBody(text);
666
+ if (wait === null || isCommercialBlock(body) || budget.overheadLeft() <= wait) {
667
+ d.dispose();
668
+ throw errorFromResponse(response.status, body, { endpoint, attempts: attempt + 1 });
669
+ }
670
+ d.dispose();
671
+ args.onRetry?.({ endpoint, attempt: attempt + 1, delayMs: wait, reason: "status", status: 429 });
672
+ await delay(wait, args.callerSignal);
673
+ continue;
674
+ }
675
+ if (wait !== null && budget.overheadLeft() > wait) {
551
676
  try {
552
677
  await response.body?.cancel();
553
678
  } catch {
@@ -566,15 +691,51 @@ async function openStream(args) {
566
691
  return { response, deadline: d, endpoint };
567
692
  } catch (err) {
568
693
  d.dispose();
694
+ if (err instanceof ZihinAgentError) throw err;
569
695
  const meta = { endpoint, attempts: attempt + 1 };
570
- if (d.timedOut()) throw new ZihinAgentError("timeout", `Request timed out after ${args.timeoutMs}ms.`, meta);
696
+ if (d.timedOut()) throw budget.expired(attempt + 1);
571
697
  if (d.abortedByCaller())
572
698
  throw new ZihinAgentError("aborted", "Request was aborted by the caller.", meta);
573
- throw errorFromThrow(err, args.timeoutMs, meta);
699
+ throw errorFromThrow(err, budget.reported, meta);
574
700
  }
575
701
  }
576
702
  throw new ZihinAgentError("unknown", "Max retries exceeded.", { endpoint });
577
703
  }
704
+ function toJsonBody(value, label = "body") {
705
+ try {
706
+ return JSON.stringify(value);
707
+ } catch (err) {
708
+ throw new ZihinAgentError(
709
+ "config",
710
+ `${label} is not JSON-serializable: ${err?.message ?? "unknown reason"}.`
711
+ );
712
+ }
713
+ }
714
+ async function requestJson(cfg, args) {
715
+ const res = await requestBuffered({
716
+ method: args.method,
717
+ url: `${cfg.baseUrl}${args.path}`,
718
+ headers: buildHeaders(cfg.apiKey),
719
+ body: args.body !== void 0 ? toJsonBody(args.body) : void 0,
720
+ timeoutMs: resolveTimeout(cfg, args.operation ?? "short"),
721
+ maxRetries: cfg.maxRetries,
722
+ policy: args.policy,
723
+ callerSignal: args.signal,
724
+ fetchImpl: cfg.fetchImpl,
725
+ onRetry: cfg.onRetry
726
+ });
727
+ if (!res.ok) {
728
+ throw errorFromResponse(res.status, parseBody(res.text), {
729
+ endpoint: res.endpoint,
730
+ attempts: res.attempts
731
+ });
732
+ }
733
+ try {
734
+ return JSON.parse(res.text);
735
+ } catch {
736
+ throw new ZihinAgentError("protocol", "Expected JSON response.", { endpoint: res.endpoint });
737
+ }
738
+ }
578
739
  function parseBody(text) {
579
740
  if (!text) return {};
580
741
  try {
@@ -590,7 +751,7 @@ async function listAgents(cfg, opts = {}) {
590
751
  method: "GET",
591
752
  url: `${cfg.baseUrl}/api/v2/agents`,
592
753
  headers: buildHeaders(cfg.apiKey),
593
- timeoutMs: cfg.timeoutMs,
754
+ timeoutMs: resolveTimeout(cfg, "short"),
594
755
  maxRetries: cfg.maxRetries,
595
756
  policy: "idempotent",
596
757
  callerSignal: opts.signal,
@@ -757,6 +918,20 @@ function mapEvent(e) {
757
918
  return { type: "token", content: str(d.content) ?? "", data: e.data };
758
919
  case "warning":
759
920
  return { type: "warning", message: str(d.message), data: e.data };
921
+ case "session":
922
+ return { type: "session", sessionId: str(d.session_id), data: e.data };
923
+ case "done":
924
+ return { type: "done", data: e.data };
925
+ case "error": {
926
+ const err = rec(d.error);
927
+ return {
928
+ type: "error",
929
+ code: str(err.code),
930
+ message: str(err.message),
931
+ requestId: str(err.request_id),
932
+ data: e.data
933
+ };
934
+ }
760
935
  case "response":
761
936
  return {
762
937
  type: "response",
@@ -781,7 +956,7 @@ function mapEvent(e) {
781
956
  return { type: "unknown", event: e.event, data: e.data };
782
957
  }
783
958
  }
784
- function buildRequestBody(params) {
959
+ function buildRequestBody(params, normalizedTimeoutMs) {
785
960
  const body = {};
786
961
  if (params.message !== void 0) body.message = params.message;
787
962
  if (params.sessionId) body.session_id = params.sessionId;
@@ -792,7 +967,7 @@ function buildRequestBody(params) {
792
967
  const o = {};
793
968
  if (params.options.temperature !== void 0) o.temperature = params.options.temperature;
794
969
  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;
970
+ if (normalizedTimeoutMs !== void 0) o.timeout_ms = normalizedTimeoutMs;
796
971
  if (params.options.jitMode !== void 0) o.jit_mode = params.options.jitMode;
797
972
  if (Object.keys(o).length) body.options = o;
798
973
  }
@@ -823,6 +998,9 @@ function validateParams(params) {
823
998
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
824
999
  }
825
1000
  }
1001
+ if (params.options?.timeoutMs !== void 0) {
1002
+ normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
1003
+ }
826
1004
  for (const att of params.attachments ?? []) {
827
1005
  const urlCheck = validateBaseUrl(att.url, { allowCustomDomains: true });
828
1006
  if (!urlCheck.valid) {
@@ -832,13 +1010,16 @@ function validateParams(params) {
832
1010
  }
833
1011
  async function* streamAgent(cfg, params) {
834
1012
  validateParams(params);
835
- const timeoutMs = params.options?.timeoutMs ?? cfg.timeoutMs;
1013
+ const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
1014
+ const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
1015
+ const reportedMs = callerTimeoutMs ?? timeoutMs;
836
1016
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
837
1017
  const { response, deadline, endpoint } = await openStream({
838
1018
  url,
839
1019
  headers: buildHeaders(cfg.apiKey),
840
- body: JSON.stringify(buildRequestBody(params)),
1020
+ body: toJsonBody(buildRequestBody(params, callerTimeoutMs), "request body"),
841
1021
  timeoutMs,
1022
+ reportedTimeoutMs: reportedMs,
842
1023
  maxRetries: cfg.maxRetries,
843
1024
  callerSignal: params.signal,
844
1025
  fetchImpl: cfg.fetchImpl,
@@ -853,7 +1034,7 @@ async function* streamAgent(cfg, params) {
853
1034
  }
854
1035
  throw errorFromResponse(response.status, parseBody(text), { endpoint });
855
1036
  }
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 });
1037
+ 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 });
857
1038
  for await (const e of parseSse(response, {
858
1039
  signal: params.signal,
859
1040
  onAbortError,
@@ -872,19 +1053,36 @@ async function invokeAgent(cfg, params) {
872
1053
  let responseChunk;
873
1054
  let metricsChunk;
874
1055
  let metadataSessionId;
1056
+ let mintedSessionId;
1057
+ let cleanEnd = false;
875
1058
  let tokenBuffer = "";
876
1059
  for await (const chunk of streamAgent(cfg, params)) {
877
1060
  if (keepRaw) raw.push(chunk);
1061
+ if (chunk.type === "error") {
1062
+ const code = chunk.code ?? "AGENT_ERROR";
1063
+ const kind = /TIMEOUT/i.test(code) ? "timeout" : "server";
1064
+ throw new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1065
+ code,
1066
+ requestId: chunk.requestId,
1067
+ retryable: false
1068
+ });
1069
+ }
878
1070
  if (chunk.type === "response") responseChunk = chunk;
879
1071
  else if (chunk.type === "metrics") metricsChunk = chunk;
880
1072
  else if (chunk.type === "token") tokenBuffer += chunk.content;
881
1073
  else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1074
+ else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1075
+ else if (chunk.type === "done") cleanEnd = true;
882
1076
  }
883
1077
  if (!responseChunk) {
1078
+ if (cleanEnd) {
1079
+ throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
1080
+ }
884
1081
  if (tokenBuffer) {
885
1082
  return {
886
1083
  content: tokenBuffer,
887
- sessionId: metadataSessionId ?? params.sessionId,
1084
+ contentSource: "tokens",
1085
+ sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
888
1086
  usage: metricsChunk?.usage,
889
1087
  model: metricsChunk?.model,
890
1088
  iterations: metricsChunk?.iterations,
@@ -895,8 +1093,9 @@ async function invokeAgent(cfg, params) {
895
1093
  throw new ZihinAgentError("protocol", "Stream ended without a `response` event.");
896
1094
  }
897
1095
  return {
898
- content: responseChunk.content || tokenBuffer,
899
- sessionId: responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1096
+ content: responseChunk.content,
1097
+ contentSource: "response",
1098
+ sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
900
1099
  model: metricsChunk?.model,
901
1100
  usage: metricsChunk?.usage,
902
1101
  sources: responseChunk.sources,
@@ -907,29 +1106,180 @@ async function invokeAgent(cfg, params) {
907
1106
  };
908
1107
  }
909
1108
 
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
1109
+ // src/tasks.ts
1110
+ var MAX_TASK_PAYLOAD_BYTES = 64 * 1024;
1111
+ var SCHEDULE_PAST_TOLERANCE_MS = 6e4;
1112
+ function envelope(json) {
1113
+ const j = rec(json);
1114
+ return { data: rec(j.data ?? j), meta: rec(j.meta) };
1115
+ }
1116
+ function toIso(value, label) {
1117
+ if (value === void 0 || value === null || value === "") return void 0;
1118
+ const d = value instanceof Date ? value : new Date(value);
1119
+ if (Number.isNaN(d.getTime())) {
1120
+ throw new ZihinAgentError("config", `${label} must be a valid ISO 8601 date (got ${JSON.stringify(value)}).`);
1121
+ }
1122
+ return d.toISOString();
1123
+ }
1124
+ function mapTask(raw) {
1125
+ const t = rec(raw);
1126
+ const status = str(t.status);
1127
+ return {
1128
+ id: str(t.id) ?? "",
1129
+ agentId: str(t.agent_id),
1130
+ title: str(t.title),
1131
+ instruction: str(t.instruction),
1132
+ status: status ?? "unknown",
1133
+ // Mesma regra do Caminho B, e de propósito a MESMA função: status desconhecido conta como
1134
+ // terminal (o polling para em vez de girar), status ausente não (não lemos o envelope).
1135
+ terminal: status === void 0 ? false : isTerminalStatus(status),
1136
+ scheduledFor: str(t.scheduled_for),
1137
+ deadlineAt: str(t.deadline_at),
1138
+ parentTaskId: str(t.parent_task_id),
1139
+ rootTaskId: str(t.root_task_id),
1140
+ sessionId: str(t.session_id),
1141
+ createdAt: str(t.created_at),
1142
+ updatedAt: str(t.updated_at),
1143
+ raw
1144
+ };
1145
+ }
1146
+ function mapExecution(raw) {
1147
+ const e = rec(raw);
1148
+ return {
1149
+ id: str(e.id) ?? "",
1150
+ status: str(e.status) ?? "unknown",
1151
+ exitReason: str(e.exit_reason),
1152
+ modelUsed: str(e.model_used),
1153
+ totalCostUsd: num(e.total_cost_usd),
1154
+ executionTimeMs: num(e.execution_time_ms),
1155
+ iterations: num(e.iterations),
1156
+ startedAt: str(e.started_at),
1157
+ finishedAt: str(e.finished_at),
1158
+ raw
1159
+ };
1160
+ }
1161
+ async function delegateTask(cfg, params) {
1162
+ if (!params.agentId) throw new ZihinAgentError("config", "agentId is required.");
1163
+ if (!params.instruction) throw new ZihinAgentError("config", "instruction is required.");
1164
+ const scheduledFor = toIso(params.scheduledFor, "scheduledFor");
1165
+ const deadlineAt = toIso(params.deadlineAt, "deadlineAt");
1166
+ if (scheduledFor && Date.parse(scheduledFor) < Date.now() - SCHEDULE_PAST_TOLERANCE_MS) {
1167
+ throw new ZihinAgentError("config", "scheduledFor is in the past.");
1168
+ }
1169
+ if (deadlineAt && Date.parse(deadlineAt) <= Date.now() - SCHEDULE_PAST_TOLERANCE_MS) {
1170
+ throw new ZihinAgentError("config", "deadlineAt must be in the future.");
1171
+ }
1172
+ if (scheduledFor && deadlineAt && Date.parse(deadlineAt) <= Date.parse(scheduledFor)) {
1173
+ throw new ZihinAgentError("config", "deadlineAt must be after scheduledFor.");
1174
+ }
1175
+ if (params.payload !== void 0) {
1176
+ if (typeof params.payload !== "object" || params.payload === null || Array.isArray(params.payload)) {
1177
+ throw new ZihinAgentError("config", "payload must be an object.");
1178
+ }
1179
+ const bytes = toJsonBody(params.payload, "payload").length;
1180
+ if (bytes > MAX_TASK_PAYLOAD_BYTES) {
1181
+ throw new ZihinAgentError(
1182
+ "config",
1183
+ `payload exceeds ${MAX_TASK_PAYLOAD_BYTES / 1024}KB (got ${Math.ceil(bytes / 1024)}KB).`
1184
+ );
1185
+ }
1186
+ }
1187
+ const body = {
1188
+ agent_id: params.agentId,
1189
+ instruction: params.instruction
1190
+ };
1191
+ if (params.title !== void 0) body.title = params.title;
1192
+ if (params.payload !== void 0) body.payload = params.payload;
1193
+ if (scheduledFor) body.scheduled_for = scheduledFor;
1194
+ if (deadlineAt) body.deadline_at = deadlineAt;
1195
+ if (params.retryPolicy !== void 0) body.retry_policy = params.retryPolicy;
1196
+ if (params.notify !== void 0) body.notify = params.notify;
1197
+ if (params.parentTaskId) body.parent_task_id = params.parentTaskId;
1198
+ const json = await requestJson(cfg, {
1199
+ method: "POST",
1200
+ path: "/api/v1/tasks",
1201
+ body,
1202
+ // Criar uma task E dispará-la é efeito colateral: nunca repetir em erro de rede.
1203
+ policy: "side-effecting",
1204
+ signal: params.signal
923
1205
  });
924
- if (!res.ok) {
925
- throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
1206
+ const { data } = envelope(json);
1207
+ const task = mapTask(data.task ?? data);
1208
+ if (!task.id) {
1209
+ throw new ZihinAgentError("protocol", "delegateTask: response has no task id.");
926
1210
  }
927
- try {
928
- return JSON.parse(res.text);
929
- } catch {
930
- throw new ZihinAgentError("protocol", "Expected JSON response.", { endpoint: res.endpoint });
1211
+ return task;
1212
+ }
1213
+ async function listTasks(cfg, params = {}) {
1214
+ const qs = new URLSearchParams();
1215
+ if (params.live) qs.set("live", "true");
1216
+ if (params.status) qs.set("status", params.status);
1217
+ if (params.agentId) qs.set("agent_id", params.agentId);
1218
+ if (params.rootTaskId) qs.set("root_task_id", params.rootTaskId);
1219
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
1220
+ if (params.offset !== void 0) qs.set("offset", String(params.offset));
1221
+ const query = qs.toString();
1222
+ const json = await requestJson(cfg, {
1223
+ method: "GET",
1224
+ path: `/api/v1/tasks${query ? `?${query}` : ""}`,
1225
+ policy: "idempotent",
1226
+ signal: params.signal
1227
+ });
1228
+ const { data, meta } = envelope(json);
1229
+ const items = Array.isArray(data.items) ? data.items : [];
1230
+ return {
1231
+ items: items.map(mapTask),
1232
+ // ⚠️ `total` vive em `meta`, não no topo — o envelope novo aninhou os campos de paginação.
1233
+ total: num(meta.total),
1234
+ raw: json
1235
+ };
1236
+ }
1237
+ async function getTask(cfg, params) {
1238
+ if (!params.taskId) throw new ZihinAgentError("config", "taskId is required.");
1239
+ const json = await requestJson(cfg, {
1240
+ method: "GET",
1241
+ path: `/api/v1/tasks/${encodeURIComponent(params.taskId)}`,
1242
+ policy: "idempotent",
1243
+ signal: params.signal
1244
+ });
1245
+ const { data } = envelope(json);
1246
+ const taskRaw = data.task ?? (str(rec(data).id) ? data : void 0);
1247
+ if (!taskRaw || !str(rec(taskRaw).id)) {
1248
+ throw new ZihinAgentError("protocol", "getTask: response has no task.");
931
1249
  }
1250
+ const executions = Array.isArray(data.executions) ? data.executions : [];
1251
+ const children = Array.isArray(data.children) ? data.children : [];
1252
+ return {
1253
+ task: mapTask(taskRaw),
1254
+ executions: executions.map(mapExecution),
1255
+ children: children.map(mapTask),
1256
+ approvals: Array.isArray(data.approvals) ? data.approvals : [],
1257
+ raw: json
1258
+ };
932
1259
  }
1260
+ async function cancelTask(cfg, params) {
1261
+ if (!params.taskId) throw new ZihinAgentError("config", "taskId is required.");
1262
+ const json = await requestJson(cfg, {
1263
+ method: "POST",
1264
+ path: `/api/v1/tasks/${encodeURIComponent(params.taskId)}/cancel`,
1265
+ body: params.reason ? { reason: params.reason } : {},
1266
+ // Cancelar é side-effecting; repetir num socket caído bateria em 409 e confundiria o chamador.
1267
+ policy: "side-effecting",
1268
+ signal: params.signal
1269
+ });
1270
+ const { data } = envelope(json);
1271
+ const taskRaw = data.task ?? (str(rec(data).id) ? data : void 0);
1272
+ if (!taskRaw || !str(rec(taskRaw).id)) {
1273
+ throw new ZihinAgentError("protocol", "cancelTask: response has no task.");
1274
+ }
1275
+ return {
1276
+ task: mapTask(taskRaw),
1277
+ aborted: data.aborted === true,
1278
+ raw: json
1279
+ };
1280
+ }
1281
+
1282
+ // src/triggers.ts
933
1283
  function queryExtractionBody(extraction, queryField) {
934
1284
  if (!extraction) return { mode: "field", field: queryField ?? "message" };
935
1285
  switch (extraction.mode) {
@@ -962,11 +1312,18 @@ async function createTrigger(cfg, params) {
962
1312
  throw new ZihinAgentError("config", `Invalid callbackUrl: ${cb.error}`);
963
1313
  }
964
1314
  }
1315
+ if (params.responseTransform && !params.callbackUrl) {
1316
+ throw new ZihinAgentError(
1317
+ "config",
1318
+ "responseTransform requires callbackUrl: the backend only reads it from trigger_config.execution.callback.response_transform, and a callback without a url is rejected."
1319
+ );
1320
+ }
965
1321
  const execution = { mode };
966
1322
  if (params.callbackUrl) {
967
1323
  execution.callback = {
968
1324
  url: params.callbackUrl,
969
- method: params.callbackMethod ?? "POST"
1325
+ method: params.callbackMethod ?? "POST",
1326
+ ...params.responseTransform ? { response_transform: { mode: params.responseTransform } } : {}
970
1327
  };
971
1328
  }
972
1329
  const body = {
@@ -996,8 +1353,8 @@ async function dispatchAgent(cfg, params) {
996
1353
  method: "POST",
997
1354
  url: `${cfg.baseUrl}/api/triggers/webhook/${encodeURIComponent(params.triggerId)}`,
998
1355
  headers: buildHeaders(cfg.apiKey),
999
- body: JSON.stringify(params.payload ?? {}),
1000
- timeoutMs: cfg.timeoutMs,
1356
+ body: toJsonBody(params.payload ?? {}, "payload"),
1357
+ timeoutMs: resolveTimeout(cfg, "dispatch"),
1001
1358
  maxRetries: cfg.maxRetries,
1002
1359
  policy: "side-effecting",
1003
1360
  // S1: nunca duplica disparo por erro de rede
@@ -1010,12 +1367,29 @@ async function dispatchAgent(cfg, params) {
1010
1367
  }
1011
1368
  const json = parseBody(res.text);
1012
1369
  const j = rec(json);
1370
+ if (j.success === false) {
1371
+ const detail = sanitizeApiError(str(j.error) ?? "").message;
1372
+ throw new ZihinAgentError("server", detail || "The agent run failed.", {
1373
+ status: res.status,
1374
+ endpoint: res.endpoint,
1375
+ attempts: res.attempts,
1376
+ executionId: str(j.execution_id),
1377
+ // Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
1378
+ retryable: false
1379
+ });
1380
+ }
1013
1381
  return {
1014
1382
  accepted: true,
1015
1383
  triggerId: str(j.trigger_id) ?? params.triggerId,
1384
+ agentId: str(j.agent_id),
1016
1385
  executionId: str(j.execution_id),
1017
1386
  agentResponse: str(j.agent_response),
1018
1387
  usage: parseUsage(j.usage),
1388
+ iterations: num(j.iterations),
1389
+ latencyMs: num(j.latency_ms),
1390
+ // Qual texto o BE extraiu do payload — é o campo que denuncia um `query_extraction` mal
1391
+ // configurado (mensagem vazia, ou o payload inteiro serializado virando a pergunta).
1392
+ queryExtracted: str(j.query_extracted),
1019
1393
  raw: json
1020
1394
  };
1021
1395
  }
@@ -1071,7 +1445,7 @@ async function updateTrigger(cfg, params) {
1071
1445
  if (params.name !== void 0) body.name = params.name;
1072
1446
  if (params.description !== void 0) body.description = params.description;
1073
1447
  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;
1448
+ const touchesConfig = params.queryExtraction !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.responseTransform !== void 0 || params.mode !== void 0;
1075
1449
  if (touchesConfig) {
1076
1450
  const path = `/api/triggers/${encodeURIComponent(params.triggerId)}`;
1077
1451
  const currentJson = rec(
@@ -1089,22 +1463,34 @@ async function updateTrigger(cfg, params) {
1089
1463
  if (params.queryExtraction !== void 0) {
1090
1464
  merged.query_extraction = queryExtractionBody(params.queryExtraction, void 0);
1091
1465
  }
1092
- if (params.mode !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
1466
+ const touchesCallback = params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.responseTransform !== void 0;
1467
+ if (params.mode !== void 0 || touchesCallback) {
1093
1468
  const execution = rec(config.execution);
1094
1469
  const callback = rec(execution.callback);
1095
1470
  const newExecution = { ...execution };
1096
1471
  if (params.mode !== void 0) newExecution.mode = params.mode;
1097
- if (params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
1472
+ if (touchesCallback) {
1098
1473
  const url = params.callbackUrl ?? str(callback.url);
1099
1474
  if (!url) {
1100
1475
  throw new ZihinAgentError(
1101
1476
  "config",
1102
- "callbackUrl is required: this trigger has no callback configured yet."
1477
+ 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."
1103
1478
  );
1104
1479
  }
1105
1480
  newExecution.callback = {
1481
+ // Preserva o callback atual: trocar só a URL não pode apagar auth/headers/
1482
+ // body_template/content_type/response_transform. Antes este objeto era
1483
+ // SUBSTITUÍDO por { url, method } — a mesma perda silenciosa que o merge de
1484
+ // trigger_config acima existe para evitar, um nível mais fundo.
1485
+ ...callback,
1106
1486
  url,
1107
- method: params.callbackMethod ?? str(callback.method) ?? "POST"
1487
+ method: params.callbackMethod ?? str(callback.method) ?? "POST",
1488
+ ...params.responseTransform ? {
1489
+ response_transform: {
1490
+ ...rec(callback.response_transform),
1491
+ mode: params.responseTransform
1492
+ }
1493
+ } : {}
1108
1494
  };
1109
1495
  }
1110
1496
  if (newExecution.mode === "async" && !rec(newExecution.callback).url) {
@@ -1133,7 +1519,7 @@ async function deleteTrigger(cfg, params) {
1133
1519
  method: "DELETE",
1134
1520
  url: `${cfg.baseUrl}/api/triggers/${encodeURIComponent(params.triggerId)}`,
1135
1521
  headers: buildHeaders(cfg.apiKey),
1136
- timeoutMs: cfg.timeoutMs,
1522
+ timeoutMs: resolveTimeout(cfg, "short"),
1137
1523
  maxRetries: cfg.maxRetries,
1138
1524
  policy: "idempotent",
1139
1525
  // DELETE é idempotente
@@ -1170,9 +1556,11 @@ async function getExecution(cfg, params) {
1170
1556
  const data = rec(json.data);
1171
1557
  const exec = rec(data.execution ?? json.execution);
1172
1558
  const metrics = rec(data.metrics ?? json.metrics);
1559
+ const rawStatus = str(exec.status);
1173
1560
  return {
1174
1561
  executionId: str(exec.id) ?? params.executionId,
1175
- status: str(exec.status) ?? "unknown",
1562
+ status: rawStatus ?? "unknown",
1563
+ terminal: rawStatus === void 0 ? false : isTerminalStatus(rawStatus),
1176
1564
  startedAt: str(exec.started_at),
1177
1565
  finishedAt: str(exec.finished_at),
1178
1566
  executionTimeMs: num(exec.execution_time_ms),
@@ -1253,6 +1641,26 @@ var ZihinAgentClient = class {
1253
1641
  getExecution(params) {
1254
1642
  return this.guard(getExecution(this.cfg, params));
1255
1643
  }
1644
+ // ---- Caminho C: tasks (delegação assíncrona durável) ----
1645
+ /**
1646
+ * Delega uma tarefa a um agente (requer key owner/admin). Sem `scheduledFor`, o BE dispara na
1647
+ * hora fora do request e devolve só o handle — acompanhe com `getTask`.
1648
+ */
1649
+ delegateTask(params) {
1650
+ return this.guard(delegateTask(this.cfg, params));
1651
+ }
1652
+ /** Board de tasks do tenant (requer key owner/admin/editor). */
1653
+ listTasks(params = {}) {
1654
+ return this.guard(listTasks(this.cfg, params));
1655
+ }
1656
+ /** Dossiê de uma task: task + execuções + filhos + aprovações. */
1657
+ getTask(params) {
1658
+ return this.guard(getTask(this.cfg, params));
1659
+ }
1660
+ /** Cancela uma task (requer key owner/admin); derruba o turno em voo, se houver. */
1661
+ cancelTask(params) {
1662
+ return this.guard(cancelTask(this.cfg, params));
1663
+ }
1256
1664
  // ---- Auxiliar ----
1257
1665
  /** Lista os agentes do tenant (dropdown). */
1258
1666
  listAgents(opts = {}) {
@@ -1292,10 +1700,14 @@ function signRequest(payload, secretKey, timestamp) {
1292
1700
  }
1293
1701
  // Annotate the CommonJS export names for ESM import in node:
1294
1702
  0 && (module.exports = {
1703
+ DEADLINE_GRACE_MS,
1295
1704
  DEFAULT_LIMITS,
1705
+ MAX_TASK_PAYLOAD_BYTES,
1706
+ OPERATION_TIMEOUT_MS,
1296
1707
  ZihinAgentClient,
1297
1708
  ZihinAgentError,
1298
1709
  createClient,
1710
+ isCommercialBlock,
1299
1711
  sanitizeApiError,
1300
1712
  signRequest,
1301
1713
  validateApiKeyFormat,