@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.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,20 @@ 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
+ data: e.data
902
+ };
903
+ }
733
904
  case "response":
734
905
  return {
735
906
  type: "response",
@@ -754,7 +925,7 @@ function mapEvent(e) {
754
925
  return { type: "unknown", event: e.event, data: e.data };
755
926
  }
756
927
  }
757
- function buildRequestBody(params) {
928
+ function buildRequestBody(params, normalizedTimeoutMs) {
758
929
  const body = {};
759
930
  if (params.message !== void 0) body.message = params.message;
760
931
  if (params.sessionId) body.session_id = params.sessionId;
@@ -765,7 +936,7 @@ function buildRequestBody(params) {
765
936
  const o = {};
766
937
  if (params.options.temperature !== void 0) o.temperature = params.options.temperature;
767
938
  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;
939
+ if (normalizedTimeoutMs !== void 0) o.timeout_ms = normalizedTimeoutMs;
769
940
  if (params.options.jitMode !== void 0) o.jit_mode = params.options.jitMode;
770
941
  if (Object.keys(o).length) body.options = o;
771
942
  }
@@ -796,6 +967,9 @@ function validateParams(params) {
796
967
  throw new ZihinAgentError("config", sizeCheck.error || "Message too large.");
797
968
  }
798
969
  }
970
+ if (params.options?.timeoutMs !== void 0) {
971
+ normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs");
972
+ }
799
973
  for (const att of params.attachments ?? []) {
800
974
  const urlCheck = validateBaseUrl(att.url, { allowCustomDomains: true });
801
975
  if (!urlCheck.valid) {
@@ -805,13 +979,16 @@ function validateParams(params) {
805
979
  }
806
980
  async function* streamAgent(cfg, params) {
807
981
  validateParams(params);
808
- const timeoutMs = params.options?.timeoutMs ?? cfg.timeoutMs;
982
+ const callerTimeoutMs = params.options?.timeoutMs !== void 0 ? normalizeTimeoutMs(params.options.timeoutMs, "options.timeoutMs") : void 0;
983
+ const timeoutMs = callerTimeoutMs !== void 0 ? callerTimeoutMs + DEADLINE_GRACE_MS : resolveTimeout(cfg, "stream");
984
+ const reportedMs = callerTimeoutMs ?? timeoutMs;
809
985
  const url = `${cfg.baseUrl}/api/v2/agents/${encodeURIComponent(params.agentId)}/stream`;
810
986
  const { response, deadline, endpoint } = await openStream({
811
987
  url,
812
988
  headers: buildHeaders(cfg.apiKey),
813
- body: JSON.stringify(buildRequestBody(params)),
989
+ body: toJsonBody(buildRequestBody(params, callerTimeoutMs), "request body"),
814
990
  timeoutMs,
991
+ reportedTimeoutMs: reportedMs,
815
992
  maxRetries: cfg.maxRetries,
816
993
  callerSignal: params.signal,
817
994
  fetchImpl: cfg.fetchImpl,
@@ -826,7 +1003,7 @@ async function* streamAgent(cfg, params) {
826
1003
  }
827
1004
  throw errorFromResponse(response.status, parseBody(text), { endpoint });
828
1005
  }
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 });
1006
+ 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
1007
  for await (const e of parseSse(response, {
831
1008
  signal: params.signal,
832
1009
  onAbortError,
@@ -845,19 +1022,36 @@ async function invokeAgent(cfg, params) {
845
1022
  let responseChunk;
846
1023
  let metricsChunk;
847
1024
  let metadataSessionId;
1025
+ let mintedSessionId;
1026
+ let cleanEnd = false;
848
1027
  let tokenBuffer = "";
849
1028
  for await (const chunk of streamAgent(cfg, params)) {
850
1029
  if (keepRaw) raw.push(chunk);
1030
+ if (chunk.type === "error") {
1031
+ const code = chunk.code ?? "AGENT_ERROR";
1032
+ const kind = /TIMEOUT/i.test(code) ? "timeout" : "server";
1033
+ throw new ZihinAgentError(kind, sanitizeApiError(chunk.message ?? code).message || code, {
1034
+ code,
1035
+ requestId: chunk.requestId,
1036
+ retryable: false
1037
+ });
1038
+ }
851
1039
  if (chunk.type === "response") responseChunk = chunk;
852
1040
  else if (chunk.type === "metrics") metricsChunk = chunk;
853
1041
  else if (chunk.type === "token") tokenBuffer += chunk.content;
854
1042
  else if (chunk.type === "metadata" && chunk.sessionId) metadataSessionId = chunk.sessionId;
1043
+ else if (chunk.type === "session" && chunk.sessionId) mintedSessionId = chunk.sessionId;
1044
+ else if (chunk.type === "done") cleanEnd = true;
855
1045
  }
856
1046
  if (!responseChunk) {
1047
+ if (cleanEnd) {
1048
+ throw new ZihinAgentError("protocol", "Stream ended (done) without a `response` event.");
1049
+ }
857
1050
  if (tokenBuffer) {
858
1051
  return {
859
1052
  content: tokenBuffer,
860
- sessionId: metadataSessionId ?? params.sessionId,
1053
+ contentSource: "tokens",
1054
+ sessionId: mintedSessionId ?? metadataSessionId ?? params.sessionId,
861
1055
  usage: metricsChunk?.usage,
862
1056
  model: metricsChunk?.model,
863
1057
  iterations: metricsChunk?.iterations,
@@ -868,8 +1062,9 @@ async function invokeAgent(cfg, params) {
868
1062
  throw new ZihinAgentError("protocol", "Stream ended without a `response` event.");
869
1063
  }
870
1064
  return {
871
- content: responseChunk.content || tokenBuffer,
872
- sessionId: responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
1065
+ content: responseChunk.content,
1066
+ contentSource: "response",
1067
+ sessionId: mintedSessionId ?? responseChunk.sessionId ?? metadataSessionId ?? params.sessionId,
873
1068
  model: metricsChunk?.model,
874
1069
  usage: metricsChunk?.usage,
875
1070
  sources: responseChunk.sources,
@@ -880,29 +1075,180 @@ async function invokeAgent(cfg, params) {
880
1075
  };
881
1076
  }
882
1077
 
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
1078
+ // src/tasks.ts
1079
+ var MAX_TASK_PAYLOAD_BYTES = 64 * 1024;
1080
+ var SCHEDULE_PAST_TOLERANCE_MS = 6e4;
1081
+ function envelope(json) {
1082
+ const j = rec(json);
1083
+ return { data: rec(j.data ?? j), meta: rec(j.meta) };
1084
+ }
1085
+ function toIso(value, label) {
1086
+ if (value === void 0 || value === null || value === "") return void 0;
1087
+ const d = value instanceof Date ? value : new Date(value);
1088
+ if (Number.isNaN(d.getTime())) {
1089
+ throw new ZihinAgentError("config", `${label} must be a valid ISO 8601 date (got ${JSON.stringify(value)}).`);
1090
+ }
1091
+ return d.toISOString();
1092
+ }
1093
+ function mapTask(raw) {
1094
+ const t = rec(raw);
1095
+ const status = str(t.status);
1096
+ return {
1097
+ id: str(t.id) ?? "",
1098
+ agentId: str(t.agent_id),
1099
+ title: str(t.title),
1100
+ instruction: str(t.instruction),
1101
+ status: status ?? "unknown",
1102
+ // Mesma regra do Caminho B, e de propósito a MESMA função: status desconhecido conta como
1103
+ // terminal (o polling para em vez de girar), status ausente não (não lemos o envelope).
1104
+ terminal: status === void 0 ? false : isTerminalStatus(status),
1105
+ scheduledFor: str(t.scheduled_for),
1106
+ deadlineAt: str(t.deadline_at),
1107
+ parentTaskId: str(t.parent_task_id),
1108
+ rootTaskId: str(t.root_task_id),
1109
+ sessionId: str(t.session_id),
1110
+ createdAt: str(t.created_at),
1111
+ updatedAt: str(t.updated_at),
1112
+ raw
1113
+ };
1114
+ }
1115
+ function mapExecution(raw) {
1116
+ const e = rec(raw);
1117
+ return {
1118
+ id: str(e.id) ?? "",
1119
+ status: str(e.status) ?? "unknown",
1120
+ exitReason: str(e.exit_reason),
1121
+ modelUsed: str(e.model_used),
1122
+ totalCostUsd: num(e.total_cost_usd),
1123
+ executionTimeMs: num(e.execution_time_ms),
1124
+ iterations: num(e.iterations),
1125
+ startedAt: str(e.started_at),
1126
+ finishedAt: str(e.finished_at),
1127
+ raw
1128
+ };
1129
+ }
1130
+ async function delegateTask(cfg, params) {
1131
+ if (!params.agentId) throw new ZihinAgentError("config", "agentId is required.");
1132
+ if (!params.instruction) throw new ZihinAgentError("config", "instruction is required.");
1133
+ const scheduledFor = toIso(params.scheduledFor, "scheduledFor");
1134
+ const deadlineAt = toIso(params.deadlineAt, "deadlineAt");
1135
+ if (scheduledFor && Date.parse(scheduledFor) < Date.now() - SCHEDULE_PAST_TOLERANCE_MS) {
1136
+ throw new ZihinAgentError("config", "scheduledFor is in the past.");
1137
+ }
1138
+ if (deadlineAt && Date.parse(deadlineAt) <= Date.now() - SCHEDULE_PAST_TOLERANCE_MS) {
1139
+ throw new ZihinAgentError("config", "deadlineAt must be in the future.");
1140
+ }
1141
+ if (scheduledFor && deadlineAt && Date.parse(deadlineAt) <= Date.parse(scheduledFor)) {
1142
+ throw new ZihinAgentError("config", "deadlineAt must be after scheduledFor.");
1143
+ }
1144
+ if (params.payload !== void 0) {
1145
+ if (typeof params.payload !== "object" || params.payload === null || Array.isArray(params.payload)) {
1146
+ throw new ZihinAgentError("config", "payload must be an object.");
1147
+ }
1148
+ const bytes = toJsonBody(params.payload, "payload").length;
1149
+ if (bytes > MAX_TASK_PAYLOAD_BYTES) {
1150
+ throw new ZihinAgentError(
1151
+ "config",
1152
+ `payload exceeds ${MAX_TASK_PAYLOAD_BYTES / 1024}KB (got ${Math.ceil(bytes / 1024)}KB).`
1153
+ );
1154
+ }
1155
+ }
1156
+ const body = {
1157
+ agent_id: params.agentId,
1158
+ instruction: params.instruction
1159
+ };
1160
+ if (params.title !== void 0) body.title = params.title;
1161
+ if (params.payload !== void 0) body.payload = params.payload;
1162
+ if (scheduledFor) body.scheduled_for = scheduledFor;
1163
+ if (deadlineAt) body.deadline_at = deadlineAt;
1164
+ if (params.retryPolicy !== void 0) body.retry_policy = params.retryPolicy;
1165
+ if (params.notify !== void 0) body.notify = params.notify;
1166
+ if (params.parentTaskId) body.parent_task_id = params.parentTaskId;
1167
+ const json = await requestJson(cfg, {
1168
+ method: "POST",
1169
+ path: "/api/v1/tasks",
1170
+ body,
1171
+ // Criar uma task E dispará-la é efeito colateral: nunca repetir em erro de rede.
1172
+ policy: "side-effecting",
1173
+ signal: params.signal
896
1174
  });
897
- if (!res.ok) {
898
- throw errorFromResponse(res.status, parseBody(res.text), { endpoint: res.endpoint, attempts: res.attempts });
1175
+ const { data } = envelope(json);
1176
+ const task = mapTask(data.task ?? data);
1177
+ if (!task.id) {
1178
+ throw new ZihinAgentError("protocol", "delegateTask: response has no task id.");
899
1179
  }
900
- try {
901
- return JSON.parse(res.text);
902
- } catch {
903
- throw new ZihinAgentError("protocol", "Expected JSON response.", { endpoint: res.endpoint });
1180
+ return task;
1181
+ }
1182
+ async function listTasks(cfg, params = {}) {
1183
+ const qs = new URLSearchParams();
1184
+ if (params.live) qs.set("live", "true");
1185
+ if (params.status) qs.set("status", params.status);
1186
+ if (params.agentId) qs.set("agent_id", params.agentId);
1187
+ if (params.rootTaskId) qs.set("root_task_id", params.rootTaskId);
1188
+ if (params.limit !== void 0) qs.set("limit", String(params.limit));
1189
+ if (params.offset !== void 0) qs.set("offset", String(params.offset));
1190
+ const query = qs.toString();
1191
+ const json = await requestJson(cfg, {
1192
+ method: "GET",
1193
+ path: `/api/v1/tasks${query ? `?${query}` : ""}`,
1194
+ policy: "idempotent",
1195
+ signal: params.signal
1196
+ });
1197
+ const { data, meta } = envelope(json);
1198
+ const items = Array.isArray(data.items) ? data.items : [];
1199
+ return {
1200
+ items: items.map(mapTask),
1201
+ // ⚠️ `total` vive em `meta`, não no topo — o envelope novo aninhou os campos de paginação.
1202
+ total: num(meta.total),
1203
+ raw: json
1204
+ };
1205
+ }
1206
+ async function getTask(cfg, params) {
1207
+ if (!params.taskId) throw new ZihinAgentError("config", "taskId is required.");
1208
+ const json = await requestJson(cfg, {
1209
+ method: "GET",
1210
+ path: `/api/v1/tasks/${encodeURIComponent(params.taskId)}`,
1211
+ policy: "idempotent",
1212
+ signal: params.signal
1213
+ });
1214
+ const { data } = envelope(json);
1215
+ const taskRaw = data.task ?? (str(rec(data).id) ? data : void 0);
1216
+ if (!taskRaw || !str(rec(taskRaw).id)) {
1217
+ throw new ZihinAgentError("protocol", "getTask: response has no task.");
904
1218
  }
1219
+ const executions = Array.isArray(data.executions) ? data.executions : [];
1220
+ const children = Array.isArray(data.children) ? data.children : [];
1221
+ return {
1222
+ task: mapTask(taskRaw),
1223
+ executions: executions.map(mapExecution),
1224
+ children: children.map(mapTask),
1225
+ approvals: Array.isArray(data.approvals) ? data.approvals : [],
1226
+ raw: json
1227
+ };
905
1228
  }
1229
+ async function cancelTask(cfg, params) {
1230
+ if (!params.taskId) throw new ZihinAgentError("config", "taskId is required.");
1231
+ const json = await requestJson(cfg, {
1232
+ method: "POST",
1233
+ path: `/api/v1/tasks/${encodeURIComponent(params.taskId)}/cancel`,
1234
+ body: params.reason ? { reason: params.reason } : {},
1235
+ // Cancelar é side-effecting; repetir num socket caído bateria em 409 e confundiria o chamador.
1236
+ policy: "side-effecting",
1237
+ signal: params.signal
1238
+ });
1239
+ const { data } = envelope(json);
1240
+ const taskRaw = data.task ?? (str(rec(data).id) ? data : void 0);
1241
+ if (!taskRaw || !str(rec(taskRaw).id)) {
1242
+ throw new ZihinAgentError("protocol", "cancelTask: response has no task.");
1243
+ }
1244
+ return {
1245
+ task: mapTask(taskRaw),
1246
+ aborted: data.aborted === true,
1247
+ raw: json
1248
+ };
1249
+ }
1250
+
1251
+ // src/triggers.ts
906
1252
  function queryExtractionBody(extraction, queryField) {
907
1253
  if (!extraction) return { mode: "field", field: queryField ?? "message" };
908
1254
  switch (extraction.mode) {
@@ -935,11 +1281,18 @@ async function createTrigger(cfg, params) {
935
1281
  throw new ZihinAgentError("config", `Invalid callbackUrl: ${cb.error}`);
936
1282
  }
937
1283
  }
1284
+ if (params.responseTransform && !params.callbackUrl) {
1285
+ throw new ZihinAgentError(
1286
+ "config",
1287
+ "responseTransform requires callbackUrl: the backend only reads it from trigger_config.execution.callback.response_transform, and a callback without a url is rejected."
1288
+ );
1289
+ }
938
1290
  const execution = { mode };
939
1291
  if (params.callbackUrl) {
940
1292
  execution.callback = {
941
1293
  url: params.callbackUrl,
942
- method: params.callbackMethod ?? "POST"
1294
+ method: params.callbackMethod ?? "POST",
1295
+ ...params.responseTransform ? { response_transform: { mode: params.responseTransform } } : {}
943
1296
  };
944
1297
  }
945
1298
  const body = {
@@ -969,8 +1322,8 @@ async function dispatchAgent(cfg, params) {
969
1322
  method: "POST",
970
1323
  url: `${cfg.baseUrl}/api/triggers/webhook/${encodeURIComponent(params.triggerId)}`,
971
1324
  headers: buildHeaders(cfg.apiKey),
972
- body: JSON.stringify(params.payload ?? {}),
973
- timeoutMs: cfg.timeoutMs,
1325
+ body: toJsonBody(params.payload ?? {}, "payload"),
1326
+ timeoutMs: resolveTimeout(cfg, "dispatch"),
974
1327
  maxRetries: cfg.maxRetries,
975
1328
  policy: "side-effecting",
976
1329
  // S1: nunca duplica disparo por erro de rede
@@ -983,12 +1336,29 @@ async function dispatchAgent(cfg, params) {
983
1336
  }
984
1337
  const json = parseBody(res.text);
985
1338
  const j = rec(json);
1339
+ if (j.success === false) {
1340
+ const detail = sanitizeApiError(str(j.error) ?? "").message;
1341
+ throw new ZihinAgentError("server", detail || "The agent run failed.", {
1342
+ status: res.status,
1343
+ endpoint: res.endpoint,
1344
+ attempts: res.attempts,
1345
+ executionId: str(j.execution_id),
1346
+ // Não é transitório: o turno rodou e falhou. Repetir gastaria outra execução.
1347
+ retryable: false
1348
+ });
1349
+ }
986
1350
  return {
987
1351
  accepted: true,
988
1352
  triggerId: str(j.trigger_id) ?? params.triggerId,
1353
+ agentId: str(j.agent_id),
989
1354
  executionId: str(j.execution_id),
990
1355
  agentResponse: str(j.agent_response),
991
1356
  usage: parseUsage(j.usage),
1357
+ iterations: num(j.iterations),
1358
+ latencyMs: num(j.latency_ms),
1359
+ // Qual texto o BE extraiu do payload — é o campo que denuncia um `query_extraction` mal
1360
+ // configurado (mensagem vazia, ou o payload inteiro serializado virando a pergunta).
1361
+ queryExtracted: str(j.query_extracted),
992
1362
  raw: json
993
1363
  };
994
1364
  }
@@ -1044,7 +1414,7 @@ async function updateTrigger(cfg, params) {
1044
1414
  if (params.name !== void 0) body.name = params.name;
1045
1415
  if (params.description !== void 0) body.description = params.description;
1046
1416
  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;
1417
+ const touchesConfig = params.queryExtraction !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.responseTransform !== void 0 || params.mode !== void 0;
1048
1418
  if (touchesConfig) {
1049
1419
  const path = `/api/triggers/${encodeURIComponent(params.triggerId)}`;
1050
1420
  const currentJson = rec(
@@ -1062,22 +1432,34 @@ async function updateTrigger(cfg, params) {
1062
1432
  if (params.queryExtraction !== void 0) {
1063
1433
  merged.query_extraction = queryExtractionBody(params.queryExtraction, void 0);
1064
1434
  }
1065
- if (params.mode !== void 0 || params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
1435
+ const touchesCallback = params.callbackUrl !== void 0 || params.callbackMethod !== void 0 || params.responseTransform !== void 0;
1436
+ if (params.mode !== void 0 || touchesCallback) {
1066
1437
  const execution = rec(config.execution);
1067
1438
  const callback = rec(execution.callback);
1068
1439
  const newExecution = { ...execution };
1069
1440
  if (params.mode !== void 0) newExecution.mode = params.mode;
1070
- if (params.callbackUrl !== void 0 || params.callbackMethod !== void 0) {
1441
+ if (touchesCallback) {
1071
1442
  const url = params.callbackUrl ?? str(callback.url);
1072
1443
  if (!url) {
1073
1444
  throw new ZihinAgentError(
1074
1445
  "config",
1075
- "callbackUrl is required: this trigger has no callback configured yet."
1446
+ 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
1447
  );
1077
1448
  }
1078
1449
  newExecution.callback = {
1450
+ // Preserva o callback atual: trocar só a URL não pode apagar auth/headers/
1451
+ // body_template/content_type/response_transform. Antes este objeto era
1452
+ // SUBSTITUÍDO por { url, method } — a mesma perda silenciosa que o merge de
1453
+ // trigger_config acima existe para evitar, um nível mais fundo.
1454
+ ...callback,
1079
1455
  url,
1080
- method: params.callbackMethod ?? str(callback.method) ?? "POST"
1456
+ method: params.callbackMethod ?? str(callback.method) ?? "POST",
1457
+ ...params.responseTransform ? {
1458
+ response_transform: {
1459
+ ...rec(callback.response_transform),
1460
+ mode: params.responseTransform
1461
+ }
1462
+ } : {}
1081
1463
  };
1082
1464
  }
1083
1465
  if (newExecution.mode === "async" && !rec(newExecution.callback).url) {
@@ -1106,7 +1488,7 @@ async function deleteTrigger(cfg, params) {
1106
1488
  method: "DELETE",
1107
1489
  url: `${cfg.baseUrl}/api/triggers/${encodeURIComponent(params.triggerId)}`,
1108
1490
  headers: buildHeaders(cfg.apiKey),
1109
- timeoutMs: cfg.timeoutMs,
1491
+ timeoutMs: resolveTimeout(cfg, "short"),
1110
1492
  maxRetries: cfg.maxRetries,
1111
1493
  policy: "idempotent",
1112
1494
  // DELETE é idempotente
@@ -1143,9 +1525,11 @@ async function getExecution(cfg, params) {
1143
1525
  const data = rec(json.data);
1144
1526
  const exec = rec(data.execution ?? json.execution);
1145
1527
  const metrics = rec(data.metrics ?? json.metrics);
1528
+ const rawStatus = str(exec.status);
1146
1529
  return {
1147
1530
  executionId: str(exec.id) ?? params.executionId,
1148
- status: str(exec.status) ?? "unknown",
1531
+ status: rawStatus ?? "unknown",
1532
+ terminal: rawStatus === void 0 ? false : isTerminalStatus(rawStatus),
1149
1533
  startedAt: str(exec.started_at),
1150
1534
  finishedAt: str(exec.finished_at),
1151
1535
  executionTimeMs: num(exec.execution_time_ms),
@@ -1226,6 +1610,26 @@ var ZihinAgentClient = class {
1226
1610
  getExecution(params) {
1227
1611
  return this.guard(getExecution(this.cfg, params));
1228
1612
  }
1613
+ // ---- Caminho C: tasks (delegação assíncrona durável) ----
1614
+ /**
1615
+ * Delega uma tarefa a um agente (requer key owner/admin). Sem `scheduledFor`, o BE dispara na
1616
+ * hora fora do request e devolve só o handle — acompanhe com `getTask`.
1617
+ */
1618
+ delegateTask(params) {
1619
+ return this.guard(delegateTask(this.cfg, params));
1620
+ }
1621
+ /** Board de tasks do tenant (requer key owner/admin/editor). */
1622
+ listTasks(params = {}) {
1623
+ return this.guard(listTasks(this.cfg, params));
1624
+ }
1625
+ /** Dossiê de uma task: task + execuções + filhos + aprovações. */
1626
+ getTask(params) {
1627
+ return this.guard(getTask(this.cfg, params));
1628
+ }
1629
+ /** Cancela uma task (requer key owner/admin); derruba o turno em voo, se houver. */
1630
+ cancelTask(params) {
1631
+ return this.guard(cancelTask(this.cfg, params));
1632
+ }
1229
1633
  // ---- Auxiliar ----
1230
1634
  /** Lista os agentes do tenant (dropdown). */
1231
1635
  listAgents(opts = {}) {
@@ -1264,10 +1668,14 @@ function signRequest(payload, secretKey, timestamp) {
1264
1668
  };
1265
1669
  }
1266
1670
  export {
1671
+ DEADLINE_GRACE_MS,
1267
1672
  DEFAULT_LIMITS,
1673
+ MAX_TASK_PAYLOAD_BYTES,
1674
+ OPERATION_TIMEOUT_MS,
1268
1675
  ZihinAgentClient,
1269
1676
  ZihinAgentError,
1270
1677
  createClient,
1678
+ isCommercialBlock,
1271
1679
  sanitizeApiError,
1272
1680
  signRequest,
1273
1681
  validateApiKeyFormat,