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