hobots 1.8.1 → 1.9.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Tipos do protocolo do Hobots (SDK → servidor).\n *\n * Este arquivo é a fonte de verdade do wire format e só pode reexportar tipos:\n * o app valida os payloads com zod e trava o drift via `satisfies z.ZodType<...>`\n * contra estes tipos (importados de `hobots/protocol`).\n */\n\n// Wire format de tasks (agente ↔ servidor) — reexportado aqui para o app\n// consumir tudo de `hobots/protocol`.\nexport type {\n RunStatus,\n RunUpdateStatus,\n RunLogLevel,\n RunLogEntry,\n AgentInfo,\n PollRequest,\n RunDescriptor,\n PollResponse,\n RunError,\n RunUpdateRequest,\n RunUpdateResponse,\n} from './tasks/protocol';\n\nexport type Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'success';\n\n/** Frames em ordem innermost-first (mesma ordem impressa pelo V8). */\nexport interface StackFrame {\n function?: string;\n filename?: string;\n lineno?: number;\n colno?: number;\n in_app: boolean;\n}\n\nexport interface Breadcrumb {\n /** ISO 8601 */\n timestamp: string;\n type?: string;\n category?: string;\n message?: string;\n level?: Severity;\n data?: Record<string, unknown>;\n}\n\nexport interface ExceptionData {\n type: string;\n value: string;\n stacktrace?: { frames: StackFrame[] };\n}\n\nexport interface EventUser {\n id?: string;\n username?: string;\n email?: string;\n}\n\nexport interface EventPayload {\n /** uuid v4 sem hífens (32 hex), gerado pelo SDK */\n event_id: string;\n /** ISO 8601 */\n timestamp: string;\n level: Severity;\n message?: string;\n exception?: ExceptionData;\n /** Override de agrupamento; quando ausente o servidor calcula pelo stack trace */\n fingerprint?: string[];\n breadcrumbs?: Breadcrumb[];\n tags?: Record<string, string>;\n user?: EventUser;\n contexts?: Record<string, Record<string, unknown>>;\n extra?: Record<string, unknown>;\n release?: string;\n environment?: string;\n}\n\n/** Status de uma step (etapa). `warning` = terminou, mas merece atenção (não é falha). */\nexport type StepStatus = 'ok' | 'warning' | 'error' | 'cancelled';\n\nexport interface StepData {\n /** 16 hex */\n step_id: string;\n parent_step_id?: string;\n op?: string;\n description?: string;\n status?: StepStatus;\n /** ISO 8601 */\n start_timestamp: string;\n /** ISO 8601 (fim da step) */\n timestamp: string;\n data?: Record<string, unknown>;\n}\n\nexport interface TransactionPayload {\n event_id: string;\n name: string;\n op?: string;\n status: string;\n start_timestamp: string;\n timestamp: string;\n steps: StepData[];\n tags?: Record<string, string>;\n release?: string;\n environment?: string;\n /** Correlaciona esta transaction a uma execução de task (ctx.tx dentro de um task handler) */\n run_id?: string;\n}\n\nexport interface HeartbeatPayload {\n instance_id: string;\n name?: string;\n interval_ms: number;\n /** ISO 8601 */\n timestamp: string;\n metadata?: Record<string, unknown>;\n release?: string;\n}\n\nexport type EnvelopeItem =\n | { type: 'event'; payload: EventPayload }\n | { type: 'transaction'; payload: TransactionPayload }\n | { type: 'heartbeat'; payload: HeartbeatPayload };\n\nexport interface Envelope {\n sdk: { name: string; version: string };\n /** ISO 8601 */\n sent_at: string;\n items: EnvelopeItem[];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/protocol.ts"],"sourcesContent":["/**\n * Tipos do protocolo do Hobots (SDK → servidor).\n *\n * Este arquivo é a fonte de verdade do wire format e só pode reexportar tipos:\n * o app valida os payloads com zod e trava o drift via `satisfies z.ZodType<...>`\n * contra estes tipos (importados de `hobots/protocol`).\n */\n\n// Wire format de tasks (agente ↔ servidor) — reexportado aqui para o app\n// consumir tudo de `hobots/protocol`.\nexport type {\n RunStatus,\n RunUpdateStatus,\n RunLogLevel,\n RunLogEntry,\n AgentInfo,\n PollRequest,\n RunDescriptor,\n PollResponse,\n RunError,\n RunUpdateRequest,\n RunUpdateResponse,\n RunAttachmentContentType,\n RunAttachmentCreateRequest,\n RunAttachmentCreateResponse,\n RunAttachmentConfirmRequest,\n RunAttachmentConfirmResponse,\n} from './tasks/protocol';\n\nexport type Severity = 'fatal' | 'error' | 'warning' | 'info' | 'debug' | 'success';\n\n/** Frames em ordem innermost-first (mesma ordem impressa pelo V8). */\nexport interface StackFrame {\n function?: string;\n filename?: string;\n lineno?: number;\n colno?: number;\n in_app: boolean;\n}\n\nexport interface Breadcrumb {\n /** ISO 8601 */\n timestamp: string;\n type?: string;\n category?: string;\n message?: string;\n level?: Severity;\n data?: Record<string, unknown>;\n}\n\nexport interface ExceptionData {\n type: string;\n value: string;\n stacktrace?: { frames: StackFrame[] };\n}\n\nexport interface EventUser {\n id?: string;\n username?: string;\n email?: string;\n}\n\nexport interface EventPayload {\n /** uuid v4 sem hífens (32 hex), gerado pelo SDK */\n event_id: string;\n /** ISO 8601 */\n timestamp: string;\n level: Severity;\n message?: string;\n exception?: ExceptionData;\n /** Override de agrupamento; quando ausente o servidor calcula pelo stack trace */\n fingerprint?: string[];\n breadcrumbs?: Breadcrumb[];\n tags?: Record<string, string>;\n user?: EventUser;\n contexts?: Record<string, Record<string, unknown>>;\n extra?: Record<string, unknown>;\n release?: string;\n environment?: string;\n}\n\n/** Status de uma step (etapa). `warning` = terminou, mas merece atenção (não é falha). */\nexport type StepStatus = 'ok' | 'warning' | 'error' | 'cancelled';\n\nexport interface StepData {\n /** 16 hex */\n step_id: string;\n parent_step_id?: string;\n op?: string;\n description?: string;\n status?: StepStatus;\n /** ISO 8601 */\n start_timestamp: string;\n /** ISO 8601 (fim da step) */\n timestamp: string;\n data?: Record<string, unknown>;\n}\n\nexport interface TransactionPayload {\n event_id: string;\n name: string;\n op?: string;\n status: string;\n start_timestamp: string;\n timestamp: string;\n steps: StepData[];\n tags?: Record<string, string>;\n release?: string;\n environment?: string;\n /** Correlaciona esta transaction a uma execução de task (ctx.tx dentro de um task handler) */\n run_id?: string;\n}\n\nexport interface HeartbeatPayload {\n instance_id: string;\n name?: string;\n interval_ms: number;\n /** ISO 8601 */\n timestamp: string;\n metadata?: Record<string, unknown>;\n release?: string;\n}\n\nexport type EnvelopeItem =\n | { type: 'event'; payload: EventPayload }\n | { type: 'transaction'; payload: TransactionPayload }\n | { type: 'heartbeat'; payload: HeartbeatPayload };\n\nexport interface Envelope {\n sdk: { name: string; version: string };\n /** ISO 8601 */\n sent_at: string;\n items: EnvelopeItem[];\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
@@ -15,6 +15,12 @@ interface RunLogEntry {
15
15
  ts: string;
16
16
  level: RunLogLevel;
17
17
  message: string;
18
+ /**
19
+ * Ids dos anexos (imagens) desta linha — gerados pelo agente (32 hex) e criados via
20
+ * `POST /v1/tasks/runs/{id}/attachments`. O upload roda em segundo plano: a linha pode
21
+ * chegar ao servidor antes do binário estar no S3.
22
+ */
23
+ attachments?: string[];
18
24
  }
19
25
  interface AgentInfo {
20
26
  /** Identidade canônica do agente — correlaciona com o heartbeat de telemetria */
@@ -68,6 +74,40 @@ interface RunUpdateRequest {
68
74
  interface RunUpdateResponse {
69
75
  ok: true;
70
76
  }
77
+ /** Formatos aceitos para anexo. O S3 valida o content-type contra a assinatura. */
78
+ type RunAttachmentContentType = 'image/png' | 'image/jpeg' | 'image/webp';
79
+ /** POST /v1/tasks/runs/{id}/attachments */
80
+ interface RunAttachmentCreateRequest {
81
+ /** Id do anexo gerado pelo agente (32 hex) — já referenciado na linha de log */
82
+ id: string;
83
+ content_type: RunAttachmentContentType;
84
+ /** Tamanho exato do binário; o PUT presignado é assinado com este Content-Length */
85
+ size_bytes: number;
86
+ caption?: string;
87
+ /** ISO 8601 — ts da linha de log que referencia o anexo */
88
+ log_ts?: string;
89
+ width?: number;
90
+ height?: number;
91
+ }
92
+ interface RunAttachmentCreateResponse {
93
+ id: string;
94
+ /** URL presignada de PUT direto no S3 (não envie Authorization nesta requisição) */
95
+ upload_url: string;
96
+ /** Headers obrigatórios no PUT — fazem parte da assinatura */
97
+ headers: Record<string, string>;
98
+ /** Validade da URL, em segundos */
99
+ expires_in: number;
100
+ }
101
+ /** PATCH /v1/tasks/runs/{id}/attachments/{attachmentId} */
102
+ interface RunAttachmentConfirmRequest {
103
+ status: 'uploaded' | 'failed';
104
+ size_bytes?: number;
105
+ width?: number;
106
+ height?: number;
107
+ }
108
+ interface RunAttachmentConfirmResponse {
109
+ ok: true;
110
+ }
71
111
 
72
112
  /**
73
113
  * Tipos do protocolo do Hobots (SDK → servidor).
@@ -183,4 +223,4 @@ interface Envelope {
183
223
  items: EnvelopeItem[];
184
224
  }
185
225
 
186
- export type { AgentInfo, Breadcrumb, Envelope, EnvelopeItem, EventPayload, EventUser, ExceptionData, HeartbeatPayload, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunLogLevel, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, Severity, StackFrame, StepData, StepStatus, TransactionPayload };
226
+ export type { AgentInfo, Breadcrumb, Envelope, EnvelopeItem, EventPayload, EventUser, ExceptionData, HeartbeatPayload, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentContentType, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunLogLevel, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, Severity, StackFrame, StepData, StepStatus, TransactionPayload };
@@ -15,6 +15,12 @@ interface RunLogEntry {
15
15
  ts: string;
16
16
  level: RunLogLevel;
17
17
  message: string;
18
+ /**
19
+ * Ids dos anexos (imagens) desta linha — gerados pelo agente (32 hex) e criados via
20
+ * `POST /v1/tasks/runs/{id}/attachments`. O upload roda em segundo plano: a linha pode
21
+ * chegar ao servidor antes do binário estar no S3.
22
+ */
23
+ attachments?: string[];
18
24
  }
19
25
  interface AgentInfo {
20
26
  /** Identidade canônica do agente — correlaciona com o heartbeat de telemetria */
@@ -68,6 +74,40 @@ interface RunUpdateRequest {
68
74
  interface RunUpdateResponse {
69
75
  ok: true;
70
76
  }
77
+ /** Formatos aceitos para anexo. O S3 valida o content-type contra a assinatura. */
78
+ type RunAttachmentContentType = 'image/png' | 'image/jpeg' | 'image/webp';
79
+ /** POST /v1/tasks/runs/{id}/attachments */
80
+ interface RunAttachmentCreateRequest {
81
+ /** Id do anexo gerado pelo agente (32 hex) — já referenciado na linha de log */
82
+ id: string;
83
+ content_type: RunAttachmentContentType;
84
+ /** Tamanho exato do binário; o PUT presignado é assinado com este Content-Length */
85
+ size_bytes: number;
86
+ caption?: string;
87
+ /** ISO 8601 — ts da linha de log que referencia o anexo */
88
+ log_ts?: string;
89
+ width?: number;
90
+ height?: number;
91
+ }
92
+ interface RunAttachmentCreateResponse {
93
+ id: string;
94
+ /** URL presignada de PUT direto no S3 (não envie Authorization nesta requisição) */
95
+ upload_url: string;
96
+ /** Headers obrigatórios no PUT — fazem parte da assinatura */
97
+ headers: Record<string, string>;
98
+ /** Validade da URL, em segundos */
99
+ expires_in: number;
100
+ }
101
+ /** PATCH /v1/tasks/runs/{id}/attachments/{attachmentId} */
102
+ interface RunAttachmentConfirmRequest {
103
+ status: 'uploaded' | 'failed';
104
+ size_bytes?: number;
105
+ width?: number;
106
+ height?: number;
107
+ }
108
+ interface RunAttachmentConfirmResponse {
109
+ ok: true;
110
+ }
71
111
 
72
112
  /**
73
113
  * Tipos do protocolo do Hobots (SDK → servidor).
@@ -183,4 +223,4 @@ interface Envelope {
183
223
  items: EnvelopeItem[];
184
224
  }
185
225
 
186
- export type { AgentInfo, Breadcrumb, Envelope, EnvelopeItem, EventPayload, EventUser, ExceptionData, HeartbeatPayload, PollRequest, PollResponse, RunDescriptor, RunError, RunLogEntry, RunLogLevel, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, Severity, StackFrame, StepData, StepStatus, TransactionPayload };
226
+ export type { AgentInfo, Breadcrumb, Envelope, EnvelopeItem, EventPayload, EventUser, ExceptionData, HeartbeatPayload, PollRequest, PollResponse, RunAttachmentConfirmRequest, RunAttachmentConfirmResponse, RunAttachmentContentType, RunAttachmentCreateRequest, RunAttachmentCreateResponse, RunDescriptor, RunError, RunLogEntry, RunLogLevel, RunStatus, RunUpdateRequest, RunUpdateResponse, RunUpdateStatus, Severity, StackFrame, StepData, StepStatus, TransactionPayload };
@@ -159,6 +159,7 @@ interface RunContext {
159
159
  signal: AbortSignal;
160
160
  throwIfCanceled(): void; // lança RunCanceled se o signal já disparou
161
161
  sleep(ms: number): Promise<void>; // espera cancelável — rejeita com RunCanceled no abort
162
+ attach(image: Uint8Array | ArrayBuffer | string, options?: AttachOptions): Promise<AttachResult>;
162
163
  }
163
164
  ```
164
165
 
@@ -168,6 +169,26 @@ interface RunContext {
168
169
  - **`signal`** — `AbortSignal` que dispara em cancelamento no app, timeout ou shutdown.
169
170
  - **`throwIfCanceled()`** — o ponto de parada de uma linha: lança `RunCanceled` se o `signal` já disparou.
170
171
  - **`sleep(ms)`** — espera que respeita o `signal` (rejeita com `RunCanceled` em vez de dormir até o fim). Use no lugar do seu `sleep` em esperas longas.
172
+ - **`attach(image, options?)`** — anexa um print à linha de log. Aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo (só em Node). A linha de log entra na hora; o upload é em segundo plano e **nunca lança**. Guia: [Anexos](./tasks.md#anexos-screenshots).
173
+
174
+ ### AttachOptions
175
+
176
+ | Campo | Tipo | Default | O que faz |
177
+ | --- | --- | --- | --- |
178
+ | `caption` | `string` | — | legenda exibida sob a imagem ampliada; truncada em 200 caracteres e passada pelo scrub de segredos |
179
+ | `level` | `RunLogLevel` | `'info'` | nível da linha de log que carrega o anexo |
180
+ | `message` | `string` | `caption` ou `'captura de tela'` | texto da linha de log |
181
+ | `contentType` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | detectado pelos *magic bytes* | só usado quando a detecção falha; a detecção sempre tem precedência |
182
+
183
+ ### AttachResult
184
+
185
+ | Campo | Tipo | O que é |
186
+ | --- | --- | --- |
187
+ | `id` | `string` | id do anexo — o mesmo referenciado na linha de log |
188
+ | `ok` | `boolean` | `true` quando o binário chegou ao S3 |
189
+ | `reason` | `AttachFailureReason?` | `'too-large' \| 'quota' \| 'unsupported-format' \| 'read-failed' \| 'unsupported' \| 'upload-failed' \| 'canceled'` |
190
+
191
+ Limites (todos aplicados também no servidor): PNG/JPEG/WEBP, **5 MB** por imagem, **20** anexos por execução, **4** por linha de log.
171
192
 
172
193
  ---
173
194
 
@@ -180,7 +201,8 @@ Todos os prazos do SDK num lugar só. A coluna **Onde configura** diz se o valor
180
201
  | Tempo | Default | Onde configura | O que acontece |
181
202
  | --- | --- | --- | --- |
182
203
  | flush automático | `2000` ms | `init({ flushIntervalMs })` | de quanto em quanto o transport esvazia a fila; menor = mais requests, telemetria mais fresca |
183
- | lote por envelope | `10` itens | `init({ maxBatchSize })` | ao atingir, envia **na hora** sem esperar o intervalo |
204
+ | gatilho de flush por lote | `10` itens | `init({ maxBatchSize })` | ao atingir, envia **na hora** sem esperar o intervalo; é só o gatilho, não o tamanho do envelope |
205
+ | teto de itens por envelope | `50` itens | — (servidor) | limite aceito pelo `/v1/ingest`; a fila é fatiada em quantos envelopes forem necessários, um POST por lote |
184
206
  | teto da fila | `200` itens | — (fixo) | fila cheia descarta o item **mais antigo**, com aviso no console |
185
207
  | `flush(timeoutMs)` | `5000` ms | argumento | quanto espera os envios em voo antes de devolver `false` |
186
208
  | `close(timeoutMs)` | `5000` ms | argumento | idem, no flush final (veja a nota 1 abaixo) |
@@ -193,6 +215,10 @@ Todos os prazos do SDK num lugar só. A coluna **Onde configura** diz se o valor
193
215
  | --- | --- | --- | --- |
194
216
  | poll sem trabalho | `5000` ms (mín. `1000`) | `tasks: { pollIntervalMs }` | é só o **teto da espera quando não há nada**: com trabalho na fila o próximo poll é imediato ([drain mode](./concepts.md#drain-mode)) |
195
217
  | flush de logs da run | `2000` ms | `tasks: { logFlushIntervalMs }` | ou na hora, ao acumular 20 linhas |
218
+ | validade da URL de upload de anexo | `900` s | — (servidor) | tempo para o PUT no S3 depois que a API presigna |
219
+ | espera pelos anexos no fim da run | `15000` ms | — (fixo) | antes do PATCH terminal, aguarda os uploads pendentes; no estouro a run termina e o app mostra "imagem não enviada" |
220
+ | uploads de anexo simultâneos | `3` | — (fixo) | por execução; os demais ficam na fila |
221
+ | retry do upload de anexo | `1s, 4s` | — (fixo) | ladder curta: o binário é grande e a URL presignada tem prazo |
196
222
  | heartbeat de cancelamento | `5000` ms (clamp `1000`..`15000`) | `tasks: { cancelCheckIntervalMs }` | detecta cancelamento feito no app **e** é o sinal de vida da run (veja a nota 2) |
197
223
  | timeout por run | sem timeout | `tasks: { taskTimeoutMs }` | no estouro dispara `ctx.signal`, libera o slot de concorrência e reporta `TaskTimeout` — **não mata** o handler |
198
224
  | `stop(timeoutMs)` | `30000` ms | argumento | espera as runs ativas; no estouro elas são reportadas como `AgentTerminated` |
package/docs/concepts.md CHANGED
@@ -365,6 +365,23 @@ O agente só transiciona para `running`, `succeeded` e `failed`. `pending` e `cl
365
365
 
366
366
  Cada run carrega os **`params`** (o JSON validado do formulário) e chega ao seu código como o primeiro argumento do handler, junto de um [`RunContext`](./api-reference.md#taskhandler--runcontext) com logs, sinal de cancelamento e a [transaction](#transaction-de-run-ctxtx).
367
367
 
368
+ ## Anexo
369
+
370
+ Um **anexo** é uma imagem — na prática, um print da tela — vinculada a uma linha de log de uma [run](#run). Serve para responder "como estava a tela quando isso aconteceu?", pergunta que texto de log sozinho não responde. No app, aparece como miniatura logo abaixo da linha de log correspondente, na aba "logs do agente" da execução.
371
+
372
+ Quem captura é o seu código; o SDK só recebe a imagem pronta via [`ctx.attach()`](./tasks.md#anexos-screenshots).
373
+
374
+ **O binário não passa pela API do Hobots.** O agente pede uma URL presignada, sobe direto no S3 e depois confirma. Isso mantém a API livre de tráfego pesado e permite imagens bem maiores do que o limite de corpo de um PATCH. O ciclo de vida de um anexo é:
375
+
376
+ ```
377
+ pending → uploaded
378
+ ↘ failed
379
+ ```
380
+
381
+ O `pending` existe porque a linha de log é registrada **antes** do upload terminar — é isso que impede o print de atrasar o log ou bagunçar a ordem. Enquanto o anexo está `pending`, o app mostra a miniatura como "enviando imagem…".
382
+
383
+ Anexos herdam o mesmo gate de visibilidade dos logs: solicitantes (papel `requester`) só os veem quando o projeto está com "mostrar logs para solicitantes" ligado.
384
+
368
385
  ## Claim
369
386
 
370
387
  O **claim** é o ato de um agente reivindicar uma run para si. Importa porque resolve a pergunta óbvia de qualquer sistema de fila: **o que impede dois agentes de pegarem a mesma run?**
@@ -88,6 +88,7 @@ Hobots.init({
88
88
 
89
89
  Hobots.register('processar-nfe', async (params, ctx) => {
90
90
  ctx.log.info(`processando lote ${params.lote}…`);
91
+ // await ctx.attach(await page.screenshot(), { caption: 'portal após o login' }); // print junto do log
91
92
  ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
92
93
  });
93
94
  Hobots.start(); // mantém o processo vivo até stop()/close()
package/docs/protocol.md CHANGED
@@ -16,6 +16,7 @@ hb_<64 hex>
16
16
  | --- | --- | --- |
17
17
  | Telemetria (ingestão) | `POST <baseUrl>/v1/ingest` | `Authorization: Bearer <clientSecret>` |
18
18
  | Tasks (execução) | `<baseUrl>/v1/tasks/*` | `Authorization: Bearer <clientSecret>` |
19
+ | Anexos (binário) | URL presignada do S3 (PUT) | assinatura na própria URL — **sem** header `Authorization` |
19
20
 
20
21
  A resolução da credencial nos endpoints é interna ao `init()` (em `src/config.ts`, com a constante exportada `DEFAULT_BASE_URL = 'https://api2.hobots.app'`); o resultado fica acessível em `getClient()?.config` → `{ key, baseUrl, ingestEndpoint, tasksBaseUrl }`. Em desenvolvimento a env `HOBOTS_API_URL` sobrescreve a URL base — apenas `http(s)`, e `http` para host fora de loopback gera aviso porque o secret trafega sem TLS. A credencial é **secreta** (formato `hb_` + 64 hex — chaves de outro formato são rejeitadas no `init()`) e viaja **só no header** `Authorization: Bearer` — nunca na URL da requisição.
21
22
 
@@ -39,6 +40,8 @@ Tipos de item: `event`, `transaction`, `heartbeat` (definidos em `hobots/protoco
39
40
 
40
41
  Um `transaction` pode carregar `run_id`: é assim que a [transaction de uma run](./concepts.md#transaction-de-run-ctxtx) (`ctx.tx`) é correlacionada à execução, e o que permite o app montar o waterfall de performance dentro da solicitação. Transactions abertas com `startTransaction()` fora de uma run não têm esse campo.
41
42
 
43
+ `items` aceita de 1 a **50** itens por envelope — acima disso a API responde `422`. O `maxBatchSize` é só o gatilho do flush: quando a fila tem mais que esse teto (burst de captures, ou fila acumulada durante uma janela de rede ruim), o transport a fatia em lotes de 50 e faz um POST por lote.
44
+
42
45
  **Flush & retries:** o transport flusha por `flushIntervalMs` ou ao atingir `maxBatchSize`. Ladder de retry de 3 tentativas; respostas 4xx (exceto 429) são descartadas sem retry; a fila tem teto fixo e descarta os itens **mais antigos** ao encher. Todos os valores em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
43
46
 
44
47
  ## Tasks — polling e reporte
@@ -76,12 +79,17 @@ Corpo `RunUpdateRequest`:
76
79
  ```jsonc
77
80
  {
78
81
  "status": "running", // 'running' | 'succeeded' | 'failed'
79
- "logs": [{ "ts": "…", "level": "info", "message": "…" }], // delta; o servidor concatena
80
- "error": { "type": "Error", "message": "…", "stack": "…" } // só com 'failed'
82
+ "logs": [
83
+ { "ts": "…", "level": "info", "message": "…" }, // delta; o servidor concatena
84
+ { "ts": "…", "level": "info", "message": "portal após o login",
85
+ "attachments": ["<32 hex>"] } // opcional; ids de anexo (máx. 4)
86
+ ],
87
+ "error": { "type": "Error", "message": "…", "stack": "…" } // só com 'failed'
81
88
  }
82
89
  ```
83
90
 
84
- - `status: 'running'` pode repetir para enviar **logs incrementais** (throttle por `logFlushIntervalMs`, flush ao acumular 20, chunk de 100, teto de 500 logs/run, mensagem cap 10k).
91
+ - `status: 'running'` pode repetir para enviar **logs incrementais** (throttle por `logFlushIntervalMs`, flush ao acumular 20, chunk de 100, teto de 1000 logs/run, mensagem cap 10k).
92
+ - `attachments` é **opcional** e aditivo: SDKs anteriores à 1.9 simplesmente não enviam o campo, e uma API anterior ao recurso descarta o campo na validação (objeto não-strict) sem rejeitar o PATCH. Os ids são gerados pelo agente **antes** do upload, o que permite enviar a linha de log sem esperar o binário subir.
85
93
  - Ordem garantida `running → logs → terminal` (PATCHes serializados numa cadeia de promise).
86
94
  - Retry `[1s, 4s, 16s]`; 4xx exceto 429 é terminal (**409** = run cancelada / transição inválida → marca a run como morta e dispara `ctx.signal`).
87
95
  - Todo PATCH atualiza `last_report_at` no servidor (relógio do servidor) — é o **sinal de vida da run**. O heartbeat de cancelamento (PATCH `running` vazio a cada `cancelCheckIntervalMs`, máx 15s) garante reporte contínuo; um sweep server-side marca como `failed` com erro `AgentTerminated` ("execução encerrada inesperadamente") toda run `running` sem reporte há **60s**, sem re-entrega. No exit path (sinal/erro fatal com runs ativas), o SDK envia um único PATCH `failed`/`AgentTerminated` com timeout curto (~2s), sem retry — o sweep é o backstop.
@@ -89,6 +97,55 @@ Corpo `RunUpdateRequest`:
89
97
 
90
98
  Ciclo de vida completo da run no servidor (`RunStatus`): `pending → claimed → running → succeeded | failed | canceled`. O agente só transiciona para `running`/`succeeded`/`failed` (`RunUpdateStatus`).
91
99
 
100
+ ### `POST /v1/tasks/runs/{id}/attachments`
101
+
102
+ Registra um anexo de imagem e devolve uma **URL presignada de PUT**. O binário nunca passa pela API: o agente sobe direto no S3.
103
+
104
+ Corpo `RunAttachmentCreateRequest`:
105
+
106
+ ```jsonc
107
+ {
108
+ "id": "<32 hex>", // gerado pelo agente, já referenciado na linha de log
109
+ "content_type": "image/png", // 'image/png' | 'image/jpeg' | 'image/webp'
110
+ "size_bytes": 184320, // exato — entra na assinatura da URL
111
+ "caption": "portal após o login", // opcional, máx. 500
112
+ "log_ts": "…", // opcional, ISO 8601 — ts da linha de log
113
+ "width": 1920, "height": 1080 // opcionais
114
+ }
115
+ ```
116
+
117
+ Resposta `200` `RunAttachmentCreateResponse`:
118
+
119
+ ```jsonc
120
+ {
121
+ "id": "<32 hex>",
122
+ "upload_url": "https://…", // PUT direto no S3, SEM Authorization
123
+ "headers": { "content-type": "image/png", "content-length": "184320" },
124
+ "expires_in": 900
125
+ }
126
+ ```
127
+
128
+ O PUT no S3 precisa mandar exatamente esses headers — eles fazem parte da assinatura. `content-length` é assinado de propósito: sem isso a URL aceitaria um upload de qualquer tamanho.
129
+
130
+ | Status | Quando |
131
+ | --- | --- |
132
+ | `401` | chave do agente inválida ou inativa |
133
+ | `404` | execução inexistente ou de outro projeto |
134
+ | `409` | id de anexo já registrado |
135
+ | `413` | corpo acima de 8 KB (é só metadado) |
136
+ | `422` | payload inválido |
137
+ | `429` | limite de 20 anexos por execução atingido |
138
+ | `503` | anexos não habilitados nesta instalação (sem bucket S3 configurado) |
139
+ | `502` | falha ao presignar — o registro é desfeito |
140
+
141
+ ### `PATCH /v1/tasks/runs/{id}/attachments/{attachmentId}`
142
+
143
+ Confirma o upload. Corpo `RunAttachmentConfirmRequest`: `{ "status": "uploaded" | "failed", "size_bytes"?, "width"?, "height"? }`; resposta `{ "ok": true }`, ou `404` se o anexo não existe no projeto da chave.
144
+
145
+ É **conforto de UX**, não correção: enquanto não confirmado, o app mostra a miniatura como "enviando imagem…". O agente engole falhas desta chamada.
146
+
147
+ Limites de anexo (aplicados no SDK **e** no servidor): PNG/JPEG/WEBP, 5 MB por imagem, 20 por execução, 4 por linha de log.
148
+
92
149
  ## Onde vive o claim atômico
93
150
 
94
151
  O `POST /v1/tasks/poll` no app chama `claimPendingRuns()` em
package/docs/tasks.md CHANGED
@@ -52,6 +52,7 @@ interface RunContext {
52
52
  signal: AbortSignal;
53
53
  throwIfCanceled(): void;
54
54
  sleep(ms: number): Promise<void>;
55
+ attach(image, options?): Promise<AttachResult>; // anexa um print a uma linha de log
55
56
  }
56
57
  ```
57
58
 
@@ -61,6 +62,7 @@ interface RunContext {
61
62
  - **`ctx.signal`** — `AbortSignal` que dispara quando a run é **cancelada no app**, em **timeout** ou no **shutdown**. Cancelamento é cooperativo (veja abaixo).
62
63
  - **`ctx.throwIfCanceled()`** — lança `RunCanceled` se o `signal` já disparou. O ponto de parada de 1 linha para chamar entre as etapas do trabalho.
63
64
  - **`ctx.sleep(ms)`** — sleep que respeita o `signal`: rejeita com `RunCanceled` no cancelamento em vez de esperar até o fim. Use no lugar do seu `sleep` em esperas longas.
65
+ - **`ctx.attach(image, options?)`** — anexa um print da tela a uma linha de log (veja [Anexos](#anexos-screenshots) abaixo).
64
66
  - **Retorno** — **ignorado**. O SDK não captura nem envia o valor retornado pelo handler ao servidor — resultados de execução podem conter dados sensíveis e não são coletados.
65
67
  - **Erro lançado** — a run é reportada como `failed` com tipo/mensagem/stack (mensagem cap 10k, stack 20k).
66
68
 
@@ -68,6 +70,73 @@ Você pode tipar os params: `Hobots.register<{ lote: number }>(...)`.
68
70
 
69
71
  > **Dados sensíveis:** o que você escreve em `ctx.log` e as mensagens/stack de erros lançados **são enviados ao servidor** e ficam visíveis na execução no app. Não inclua senhas, tokens, documentos, dados pessoais ou qualquer informação sensível em logs e mensagens de erro.
70
72
 
73
+ ## Anexos (screenshots)
74
+
75
+ Texto de log conta o que o robô fez; um print conta **como estava a tela**. `ctx.attach()` anexa uma imagem a uma linha de log, e ela aparece como miniatura logo abaixo dela na aba "logs do agente" da execução — clicável para ampliar.
76
+
77
+ ```js
78
+ Hobots.register('processar-nfe', async (params, ctx) => {
79
+ await entrar();
80
+ await ctx.attach(await page.screenshot(), { caption: 'portal após o login' });
81
+
82
+ try {
83
+ await emitir(params);
84
+ } catch (erro) {
85
+ // numa falha é justamente quando o print importa
86
+ await ctx.attach(await page.screenshot(), { caption: 'tela no momento do erro', level: 'error' });
87
+ throw erro;
88
+ }
89
+ });
90
+ ```
91
+
92
+ **O SDK não tira o print.** Ele recebe a imagem pronta — de onde você quiser: `page.screenshot()` do Playwright/Puppeteer, uma biblioteca de captura de desktop, um arquivo que o próprio sistema gerou. Isso mantém o SDK sem dependências nativas e funcionando em qualquer ambiente.
93
+
94
+ ### O que aceita
95
+
96
+ | Entrada | Exemplo |
97
+ | --- | --- |
98
+ | bytes | `Uint8Array`, `ArrayBuffer` ou `Buffer` — `await page.screenshot()` |
99
+ | caminho de arquivo | `'C:\\prints\\tela.png'` (só em Node) |
100
+
101
+ ### Opções
102
+
103
+ | Opção | Tipo | Default | O que faz |
104
+ | --- | --- | --- | --- |
105
+ | `caption` | `string` | — | legenda curta exibida sob a imagem ampliada (máx. 200 caracteres) |
106
+ | `level` | `RunLogLevel` | `'info'` | nível da linha de log que carrega o anexo |
107
+ | `message` | `string` | a legenda, ou `'captura de tela'` | texto da linha de log |
108
+ | `contentType` | `'image/png' \| 'image/jpeg' \| 'image/webp'` | detectado | só é usado quando os *magic bytes* não bastam |
109
+
110
+ ### Limites
111
+
112
+ | Limite | Valor |
113
+ | --- | --- |
114
+ | formatos | PNG, JPEG e WEBP |
115
+ | tamanho por imagem | 5 MB |
116
+ | anexos por execução | 20 |
117
+ | anexos por linha de log | 4 |
118
+ | uploads simultâneos | 3 (o resto fica na fila) |
119
+
120
+ ### Como funciona (e por que não trava a run)
121
+
122
+ A linha de log é registrada **na hora**, com o id do anexo; o upload roda em segundo plano. O binário vai direto para o S3 por uma URL presignada — não passa pela API do Hobots. Enquanto o upload não termina, o app mostra a miniatura como "enviando imagem…".
123
+
124
+ `attach()` **nunca lança**. Todo insucesso vira `{ ok: false, reason }` e um aviso no log da run:
125
+
126
+ | `reason` | Quando |
127
+ | --- | --- |
128
+ | `too-large` | acima de 5 MB |
129
+ | `quota` | passou de 20 anexos na execução |
130
+ | `unsupported-format` | não é PNG/JPEG/WEBP |
131
+ | `read-failed` | o caminho de arquivo não pôde ser lido |
132
+ | `unsupported` | a instalação do Hobots não tem anexos habilitados |
133
+ | `upload-failed` | falha de rede depois dos retries |
134
+ | `canceled` | a run foi cancelada ou encerrada durante o upload |
135
+
136
+ Você pode `await` para saber o desfecho, ou disparar sem esperar (`void ctx.attach(...)`) — antes de reportar o desfecho da run, o SDK aguarda os uploads pendentes por até 15 s, para a execução não terminar com miniaturas em "enviando…".
137
+
138
+ > **Dados sensíveis:** o print é enviado ao servidor **exatamente como está** — o SDK não borra nem recorta nada. Uma tela de portal costuma conter CPF, nome, saldo, número de documento. Capture apenas o necessário e evite prints de telas com dados de terceiros. Solicitantes (papel `requester`) só veem os anexos quando o projeto está com "mostrar logs para solicitantes" ligado, o mesmo gate dos logs.
139
+
71
140
  ## Performance da run (`ctx.tx`)
72
141
 
73
142
  O SDK já abre uma [transaction](./concepts.md#transaction-de-run-ctxtx) para cada run e entrega em `ctx.tx`. Ela é correlacionada à execução, recebe o status conforme o desfecho (`ok`, `error` ou `cancelled`) e é **finalizada automaticamente** quando o handler termina. Você não cria e não finaliza — só marca as etapas:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hobots",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "SDK JavaScript da Hobots — telemetria (erros, transactions e heartbeats) e execução remota de tarefas sob demanda (tasks) para RPAs e integrações. Um único import e um único init().",
5
5
  "type": "module",
6
6
  "sideEffects": false,