hobots 3.3.0 → 4.0.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/docs/concepts.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Conceitos
2
2
 
3
- Esta página explica **o que cada coisa é** — o vocabulário que as outras páginas usam: task, execução, item, evento, issue, transaction, step, heartbeat, instância, claim. As outras páginas mostram **como usar**; aqui está o **por que existe**.
3
+ Esta página explica **o que cada coisa é** — o vocabulário que as outras páginas usam: agente, execução, item, evento, issue, transaction, step, heartbeat, instância, claim. As outras páginas mostram **como usar**; aqui está o **por que existe**.
4
4
 
5
5
  Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos) e nas [tabelas de opções](./api-reference.md#tabelas-de-opções).
6
6
 
@@ -8,14 +8,14 @@ Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-i
8
8
 
9
9
  | Conceito | Em uma frase | Quando você usa |
10
10
  | --- | --- | --- |
11
- | [Task](#task) | O que o robô sabe fazer, por slug | `hobots.register` / `hobots.start` |
11
+ | [Agente](#agente) | O seu robô: um processo, uma coisa que ele faz | `hobots.register` / `hobots.start` |
12
12
  | [Modo sob demanda](#modo-sob-demanda-register) | Uma execução por solicitação do app | O trabalho é disparado por alguém (formulário, agendamento) |
13
13
  | [Modo sempre ligado](#modo-sempre-ligado-start) | O robô abre a própria execução e roda o seu loop | O robô vive rodando e você só quer ver o que ele faz |
14
- | [Execução (run)](#execução-run) | Uma rodada de uma task, com logs, itens e resultado | A unidade de tudo em Solicitações |
14
+ | [Execução (run)](#execução-run) | Uma rodada do agente, com logs, itens e resultado | A unidade de tudo em Trabalhos |
15
15
  | [Item](#item) | Uma unidade de trabalho entregue | Cada nota, boleto, arquivo processado |
16
16
  | [Progresso](#progresso) | Quantos de quantos | Barra e tempo restante no app |
17
17
  | [Log da execução](#log-da-execução) | Narração do que a execução fez | `hobots.log` — nunca lança |
18
- | [Anexo](#anexo) | Um print pendurado numa linha de log | "Como estava a tela?" |
18
+ | [Anexo](#anexo) | Um print pendurado num log, item ou step | "Como estava a tela?" |
19
19
  | [Cancelamento cooperativo](#cancelamento-cooperativo) | O app pede, o handler para no próximo ponto de parada | Automático em items/progress/steps/sleep/retry |
20
20
  | [Retry](#retry) | Tentar de novo na mesma execução | Instabilidade passageira |
21
21
  | [Evento e issue](#evento-e-issue) | A foto de um erro, agrupada em issue | `hobots.captureException` e erros não tratados |
@@ -31,39 +31,39 @@ Defaults numéricos vivem em [Tempos e intervalos](./api-reference.md#tempos-e-i
31
31
 
32
32
  | No SDK | No app |
33
33
  | --- | --- |
34
- | **Agente** — dono do Client Secret (`hb_…`) que autentica tudo | **Configurações → Agentes** |
35
- | **Instância** — o par `(agente, instanceId)`; heartbeat e tasks convergem nela | Módulo **Instâncias** (uptime, recursos da máquina, performance) |
36
- | **Task / execução** — `register('slug', …)` casa com o `task_slug` do formulário; `start('slug', …)` abre a própria execução | Módulo **Solicitações** (formulários em **Configurações → Formulários**) |
34
+ | **Agente** — dono do Client Secret (`hb_…`) que autentica tudo; o que ele faz é o handler de `register()`/`start()` | **Configurações → Agentes** (o formulário do agente fica aqui) |
35
+ | **Instância** — o par `(agente, instanceId)`; heartbeat e execuções convergem nela, com o modo (sob demanda / sempre ligado) | Módulo **Instâncias** (uptime, recursos da máquina, performance) |
36
+ | **Execução** — cada solicitação atendida por `register()`, ou a execução que `start()` abre sozinho | Módulo **Trabalhos** |
37
37
  | **Evento** agrupado em **issue** — `captureException` e erros não tratados | Módulo **Issues** |
38
38
  | **Transaction / steps** — `hobots.transaction` | Aba de performance da execução |
39
39
 
40
40
  ---
41
41
 
42
- ## Task
42
+ ## Agente
43
43
 
44
- Uma **task** é uma capacidade nomeada do robô — "processar NF-e", "monitorar a pasta de entrada" — identificada por um **slug** estável. Registrar uma task é a única forma de configurar o SDK: não há `init()`. A primeira chamada a `register()`/`start()` lê as credenciais do ambiente, liga o heartbeat e a captura de erros, e define as opções do processo (`environment`, `release`, `tags`…). Um slug só pode ser registrado uma vez.
44
+ O **agente** é o seu robô: um programa que faz **uma coisa** — "processar NF-e", "monitorar a pasta de entrada". Na plataforma ele é identificado pelo Client Secret, não por um nome no código: o processo que sobe com aquele secret *é* o agente, e o handler que você passa a `register()`/`start()` *é* a tarefa. Essa chamada é a única forma de configurar o SDK: não há `init()`. Ela lê as credenciais do ambiente, liga o heartbeat e a captura de erros, define as opções (`environment`, `release`, `tags`, `items`…) e diz como o robô trabalha. **Uma chamada por processo** — a segunda lança. Um robô que faz duas coisas são dois agentes, cada um com o seu processo e o seu Client Secret.
45
45
 
46
46
  ## Modo sob demanda (`register`)
47
47
 
48
- O robô fica **online aguardando**: faz polling na plataforma e, quando alguém cria uma solicitação para aquele slug (pelo formulário ou por um agendamento), executa o handler com os `params` do formulário. É o modo para trabalho que **alguém pede**. Várias solicitações podem rodar em paralelo (`concurrency`).
48
+ O robô fica **online aguardando**: faz polling na plataforma e, quando alguém cria uma solicitação para o agente (pelo formulário do agente ou por um agendamento), executa o handler com os `params` do formulário. É o modo para trabalho que **alguém pede**. Várias solicitações podem rodar em paralelo (`concurrency`). No painel a instância aparece como "sob demanda".
49
49
 
50
50
  ## Modo sempre ligado (`start`)
51
51
 
52
- O robô **não espera ninguém**: ao iniciar, abre a própria execução na plataforma (`source: agent`, sem formulário nem solicitante) e roda o handler dentro dela. O handler é o seu loop — um `while (true)` que vigia uma pasta, uma fila, um portal. A plataforma recebe logs, itens e progresso enquanto ele roda. Quando o handler retorna, a execução fecha; o processo continua vivo pelo heartbeat, então a instância segue online até `hobots.close()`. É **uma execução por processo**: para recomeçar, reinicie o robô. Como o servidor guarda no máximo 1000 linhas de log por execução, o trabalho feito vai em itens, não em log.
52
+ O robô **não espera ninguém**: ao iniciar, abre a própria execução na plataforma (`source: agent`, sem formulário nem solicitante) e roda o handler dentro dela. O handler é o seu loop — um `while (true)` que vigia uma pasta, uma fila, um portal. A plataforma recebe logs, itens e progresso enquanto ele roda. Quando o handler retorna, a execução fecha; o processo continua vivo pelo heartbeat, então a instância segue online até `hobots.close()`. É **uma execução por processo**: para recomeçar, reinicie o robô. Como o servidor guarda no máximo 1000 linhas de log por execução, o trabalho feito vai em itens, não em log. No painel a instância aparece como "sempre ligado".
53
53
 
54
54
  ## Execução (run)
55
55
 
56
- Uma **execução** é uma rodada concreta de uma task — no app, uma solicitação. Ciclo de vida no servidor:
56
+ Uma **execução** é uma rodada concreta do agente — no app, um trabalho. Ciclo de vida no servidor:
57
57
 
58
58
  ```
59
59
  pending → claimed → running → succeeded | failed | canceled
60
60
  ```
61
61
 
62
- No modo sob demanda a execução nasce `pending` (alguém pediu), vira `claimed` quando um agente a [reivindica](#claim) e `running` quando o handler começa. No modo sempre ligado ela já nasce `running`, criada pelo próprio robô. O agente só transiciona para `running`, `succeeded` e `failed`; `canceled` vem de alguém cancelando no app. Cada execução tem a sua transaction de performance, seus logs, itens e progresso — e o objeto `hobots` aponta para a execução **atual**, em qualquer módulo, graças ao contexto async do Node.
62
+ No modo sob demanda a execução nasce `pending` (alguém pediu), vira `claimed` quando um agente a [reivindica](#claim) e `running` quando o handler começa. No modo sempre ligado ela já nasce `running`, criada pelo próprio robô. O agente só transiciona para `running`, `succeeded` e `failed`; `canceled` vem de alguém cancelando no app. Cada execução tem a sua transaction de performance (`run`), seus logs, itens e progresso — e o objeto `hobots` aponta para a execução **atual**, em qualquer módulo, graças ao contexto async do Node.
63
63
 
64
64
  ## Item
65
65
 
66
- Um **item** é uma unidade de trabalho que a execução entregou: uma nota baixada, um boleto emitido, um arquivo movido. Tem status (`succeeded`, `failed` = falha técnica, `occurrence` = ocorrência de negócio, como "nota não encontrada") e um `payload` de atributos livres que viram colunas na aba de itens da solicitação. O `id` é a identidade de negócio do item na execução — repetido, é ignorado: é isso que torna retries idempotentes. Diferente do log, o item é **dado estruturado**: dá para contar, filtrar e conferir.
66
+ Um **item** é uma unidade de trabalho que a execução entregou: uma nota baixada, um boleto emitido, um arquivo movido. Tem status (`succeeded`, `failed` = falha técnica, `occurrence` = ocorrência de negócio, como "nota não encontrada") e um `payload` de atributos livres que viram colunas na aba de itens da execução — as colunas são declaradas no código, uma vez, pela opção `items` do agente. O `id` é a identidade de negócio do item na execução — repetido, o servidor atualiza em vez de duplicar: é isso que torna retries idempotentes. Diferente do log, o item é **dado estruturado**: dá para contar, filtrar e conferir.
67
67
 
68
68
  ## Progresso
69
69
 
@@ -71,11 +71,11 @@ O **progresso** é "quantos de quantos" (`current/total`). Declare o total antes
71
71
 
72
72
  ## Log da execução
73
73
 
74
- O **log da execução** narra o que aconteceu, em linhas com nível (`debug`, `info`, `warning`, `error`, `success`), enviadas incrementalmente e visíveis no app quase em tempo real. Ele é o único membro de `hobots` que **nunca lança**, justamente para poder ir num `finally` de limpeza. O servidor guarda até 1000 linhas por execução.
74
+ O **log da execução** narra o que aconteceu, em linhas com nível (`debug`, `info`, `warning`, `error`, `success`), enviadas incrementalmente e visíveis no app quase em tempo real (e ecoadas no terminal como `[hobots] INFO …`). Ele é o único membro de `hobots` que **nunca lança**, justamente para poder ir num `finally` de limpeza. O servidor guarda até 1000 linhas por execução.
75
75
 
76
76
  ## Anexo
77
77
 
78
- Um **anexo** é uma imagem — na prática, um print — vinculada a uma linha de log. Responde "como estava a tela?" quando texto não basta. Quem captura é o seu código (Playwright, Puppeteer, um arquivo); o SDK sobe direto para o S3 por URL presignada, em segundo plano, sem atrasar o log. Enquanto sobe, o app mostra "enviando imagem…".
78
+ Um **anexo** é uma imagem — na prática, um print — vinculada ao evento que ela ilustra: uma linha de log, um item ou uma step. Responde "como estava a tela?" quando texto não basta. Quem captura é o seu código (Playwright, Puppeteer, um arquivo); o SDK sobe direto para o S3 por URL presignada, em segundo plano, sem atrasar o log. Enquanto sobe, o app mostra "enviando imagem…".
79
79
 
80
80
  ## Cancelamento cooperativo
81
81
 
@@ -91,7 +91,7 @@ Um **evento** é a foto de um erro: tipo, mensagem, stack trace, tags, ambiente,
91
91
 
92
92
  ## Transaction
93
93
 
94
- Uma **transaction** é uma unidade de trabalho **medida**: começa, termina, e o que fica é quanto tempo levou. Cada execução tem exatamente uma, aberta e finalizada pelo SDK (`hobots.transaction`), correlacionada à execução pelo `run_id`. Você não cria transactions — só adiciona etapas.
94
+ Uma **transaction** é uma unidade de trabalho **medida**: começa, termina, e o que fica é quanto tempo levou. Cada execução tem exatamente uma, chamada `run` (op `hobots.run`), aberta e finalizada pelo SDK (`hobots.transaction`), correlacionada à execução pelo `run_id`. Você não cria transactions — só adiciona etapas.
95
95
 
96
96
  ## Step
97
97
 
@@ -103,7 +103,7 @@ O **heartbeat** é o batimento "estou vivo", a cada 30 s. O servidor conhece a c
103
103
 
104
104
  ## instanceId
105
105
 
106
- A identidade da **instância** do robô: qual VM, qual container. Vem da env `HOBOTS_INSTANCE_ID` (ou da opção; sem os dois, o hostname) e deve ser **estável entre reinícios** — um valor novo a cada boot vira uma frota de robôs fantasmas. É a mesma identidade no heartbeat, no poll e na criação de execuções: um agente só no painel.
106
+ A identidade da **instância** do robô: qual VM, qual container. Vem da env `HOBOTS_INSTANCE_ID` (ou da opção; sem os dois, o hostname) e deve ser **estável entre reinícios** — um valor novo a cada boot vira uma frota de robôs fantasmas. É a mesma identidade no heartbeat, no poll e na criação de execuções: um agente só no painel. A presença da instância também declara o **modo** (`on_demand` / `always_on`) e as colunas de itens do agente.
107
107
 
108
108
  ## Release e environment
109
109
 
@@ -111,7 +111,7 @@ A identidade da **instância** do robô: qual VM, qual container. Vem da env `HO
111
111
 
112
112
  ## Claim
113
113
 
114
- O **claim** é o ato de um agente reivindicar uma execução pendente. É **server-side e atômico** (`FOR UPDATE SKIP LOCKED`): dois agentes que fazem poll no mesmo instante recebem conjuntos disjuntos — nenhum lock vive no seu processo. O agente pede só o que consegue rodar (tasks com slot livre), então nunca reivindica trabalho para deixar parado.
114
+ O **claim** é o ato de um agente reivindicar uma execução pendente. É **server-side e atômico** (`FOR UPDATE SKIP LOCKED`): dois agentes que fazem poll no mesmo instante recebem conjuntos disjuntos — nenhum lock vive no seu processo. O agente pede só o que consegue rodar (`max` = slots livres de `concurrency`), então nunca reivindica trabalho para deixar parado.
115
115
 
116
116
  ## Drain mode
117
117
 
@@ -12,8 +12,8 @@ Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
12
12
 
13
13
  | Conceito | Em uma frase |
14
14
  | --- | --- |
15
- | [Task](./concepts.md#task) | O que o robô sabe fazer, identificado por um slug — registrado com `register` (sob demanda) ou `start` (sempre ligado) |
16
- | [Execução (run)](./concepts.md#execução-run) | Uma rodada de uma task, com logs, itens, progresso e resultado — uma solicitação no app |
15
+ | [Agente](./concepts.md#agente) | O seu robô — um processo, uma coisa que ele faz, iniciado com `register` (sob demanda) ou `start` (sempre ligado) |
16
+ | [Execução (run)](./concepts.md#execução-run) | Uma rodada do agente, com logs, itens, progresso e resultado — um trabalho no app |
17
17
  | [Item](./concepts.md#item) | Uma unidade de trabalho entregue pela execução (nota baixada, boleto emitido) |
18
18
  | [Heartbeat](./concepts.md#heartbeat) | Batimento "estou vivo" — sempre ligado; o servidor acusa offline quando para |
19
19
  | [instanceId](./concepts.md#instanceid) | A identidade da instância do robô no painel |
@@ -21,24 +21,24 @@ Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
21
21
 
22
22
  ## Credenciais
23
23
 
24
- Cada agente tem **um Client Secret** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. O SDK lê do ambiente, como as libs da AWS:
24
+ Cada agente tem **um Client Secret** (`hb_<64 hex>`), copiado em **Configurações → Agentes**. O SDK lê do ambiente:
25
25
 
26
26
  ```bash
27
27
  export HOBOTS_CLIENT_SECRET=hb_<64 hex>
28
28
  export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — sem ela, o hostname
29
29
  ```
30
30
 
31
- Também dá para passar `clientSecret`/`instanceId` nas opções da task (o snippet do app já vem assim). Em desenvolvimento, `HOBOTS_API_URL=http://localhost:3001` aponta para outra API; em produção é sempre `https://api2.hobots.app`.
31
+ Também dá para passar `clientSecret`/`instanceId` nas opções de `register()`/`start()` (o snippet do app já vem assim). Em desenvolvimento, `HOBOTS_API_URL=http://localhost:3001` aponta para outra API; em produção é sempre `https://api2.hobots.app`.
32
32
 
33
- ## Primeira task sob demanda (`register`)
33
+ ## Primeiro agente sob demanda (`register`)
34
34
 
35
- 1. No app, crie um formulário em **Configurações → Formulários** com o `task_slug` `processar-nfe`.
35
+ 1. No app, abra o agente em **Configurações → Agentes** e monte o **formulário do agente** (os campos que cada solicitação vai carregar).
36
36
  2. No robô:
37
37
 
38
38
  ```js
39
39
  import hobots from 'hobots';
40
40
 
41
- hobots.register('processar-nfe', async params => {
41
+ hobots.register(async params => {
42
42
  hobots.log.info(`lote ${params.lote}`);
43
43
  for (const nota of params.notas) {
44
44
  await baixar(nota);
@@ -50,14 +50,14 @@ hobots.register('processar-nfe', async params => {
50
50
  process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
51
51
  ```
52
52
 
53
- 3. Rode o robô: ele aparece online em **Instâncias** e fica aguardando. Crie uma solicitação pelo formulário — a execução aparece em **Solicitações** com logs em tempo real.
53
+ 3. Rode o robô: ele aparece online em **Instâncias** (modo "sob demanda") e fica aguardando. Crie uma solicitação pelo formulário — a execução aparece em **Trabalhos** com logs em tempo real.
54
54
 
55
- ## Primeira task sempre ligada (`start`)
55
+ ## Primeiro agente sempre ligado (`start`)
56
56
 
57
57
  ```js
58
58
  import hobots from 'hobots';
59
59
 
60
- hobots.start('monitor-pasta', async () => {
60
+ hobots.start(async () => {
61
61
  hobots.log.info('monitor iniciado');
62
62
  while (true) {
63
63
  await hobots.sleep(60_000); // cancelável — para se a execução for cancelada no app
@@ -66,10 +66,12 @@ hobots.start('monitor-pasta', async () => {
66
66
  });
67
67
  ```
68
68
 
69
- O robô abre a própria execução ao iniciar e ela aparece em **Solicitações** com origem "agente". Quando o handler retorna (ou é cancelado), a execução fecha; o processo continua vivo pelo heartbeat até `hobots.close()`.
69
+ O robô abre a própria execução ao iniciar e ela aparece em **Trabalhos** com origem "agente". Quando o handler retorna (ou é cancelado), a execução fecha; o processo continua vivo pelo heartbeat até `hobots.close()`.
70
+
71
+ > **Uma chamada por processo.** `register()` ou `start()`, nunca os dois — a segunda chamada lança. Um robô que faz duas coisas são dois agentes (dois processos, dois Client Secrets).
70
72
 
71
73
  ## Encerramento
72
74
 
73
75
  `hobots.close(timeoutMs?)` para o polling, espera as execuções em voo, para o heartbeat e faz o flush final. Sem handler próprio de `SIGINT`/`SIGTERM`, o SDK reporta as execuções ativas como `AgentTerminated` antes do processo morrer.
74
76
 
75
- Próximos passos: [Tasks](./tasks.md) para itens, progresso, anexos, cancelamento e retry; [Referência de API](./api-reference.md) para todas as opções.
77
+ Próximos passos: [Execuções](./runs.md) para itens, progresso, anexos, cancelamento e retry; [Referência de API](./api-reference.md) para todas as opções.
package/docs/protocol.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  O SDK é um **cliente HTTP puro**, sem dependências de runtime: ele só fala com o app da Hobots por HTTP. Não abre conexão com banco e **não** roda `SKIP LOCKED` — a reivindicação atômica de runs é server-side (veja o fim desta página).
4
4
 
5
- Os arquivos `src/protocol.ts` e `src/tasks/protocol.ts` são a **fonte de verdade do wire format**: o app valida os payloads com zod travados nesses tipos (`satisfies z.ZodType<...>`), então não podem sofrer drift. Todos os tipos são exportados por `hobots/protocol` (e reexportados pelo import raiz `hobots`).
5
+ Os arquivos `src/protocol.ts` e `src/tasks/protocol.ts` são a **fonte de verdade do wire format**: o app valida os payloads com zod travados nesses tipos (`satisfies z.ZodType<...>`), então não podem sofrer drift. Todos os tipos são exportados por `hobots/protocol` (inclusive `InstanceMode` e `ItemsDeclaration`) (e reexportados pelo import raiz `hobots`).
6
6
 
7
7
  ## O Client Secret e os endpoints derivados
8
8
 
9
- Cada agente tem **um único Client Secret**, o mesmo para as duas capabilities:
9
+ Cada agente tem **um único Client Secret**, o mesmo para as duas capabilities. O agente é a própria tarefa: não há slug de task no wire — o que o agente faz é identificado pelo secret; a instância só declara **como** trabalha (`mode`).
10
10
 
11
11
  ```
12
12
  hb_<64 hex>
@@ -15,10 +15,10 @@ hb_<64 hex>
15
15
  | Canal | Endpoint | Autenticação |
16
16
  | --- | --- | --- |
17
17
  | Telemetria (ingestão) | `POST <baseUrl>/v1/ingest` | `Authorization: Bearer <clientSecret>` |
18
- | Tasks (execução) | `<baseUrl>/v1/tasks/*` | `Authorization: Bearer <clientSecret>` |
18
+ | Execuções (poll, runs, itens, anexos) | `<baseUrl>/v1/runs/*` | `Authorization: Bearer <clientSecret>` |
19
19
  | Anexos (binário) | URL presignada do S3 (PUT) | assinatura na própria URL — **sem** header `Authorization` |
20
20
 
21
- A credencial vem da opção `clientSecret` ou da env `HOBOTS_CLIENT_SECRET` (resolvida em `src/config.ts` no primeiro `register()`/`start()`, com a URL base `https://api2.hobots.app`). 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 — outro formato é rejeitado na primeira task, sem ecoar o valor) e viaja **só no header** `Authorization: Bearer` — nunca na URL da requisição.
21
+ A credencial vem da opção `clientSecret` ou da env `HOBOTS_CLIENT_SECRET` (resolvida em `src/config.ts` no primeiro `register()`/`start()`, com a URL base `https://api2.hobots.app`). 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 — outro formato é rejeitado no `register()`/`start()`, sem ecoar o valor) e viaja **só no header** `Authorization: Bearer` — nunca na URL da requisição.
22
22
 
23
23
  ## Telemetria — ingestão
24
24
 
@@ -47,7 +47,7 @@ Todo `transaction` carrega `run_id`: é assim que a [transaction da execução](
47
47
 
48
48
  ### MachineSnapshot
49
49
 
50
- Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`, em `instance.machine` do `POST /v1/tasks/poll` e em `contexts.machine` dos eventos. Bytes em bytes, percentuais 0..100; campo ausente = não mensurável nesta plataforma. Teto: **4 KiB** serializado (heartbeat/poll → 422; `contexts.machine` idem). Chaves desconhecidas são descartadas pelo servidor.
50
+ Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`, em `instance.machine` do `POST /v1/runs/poll` e em `contexts.machine` dos eventos. Bytes em bytes, percentuais 0..100; campo ausente = não mensurável nesta plataforma. Teto: **4 KiB** serializado (heartbeat/poll → 422; `contexts.machine` idem). Chaves desconhecidas são descartadas pelo servidor.
51
51
 
52
52
  | Campo | Tipo | Descrição |
53
53
  | --- | --- | --- |
@@ -66,11 +66,11 @@ Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`,
66
66
 
67
67
  O servidor guarda o último snapshot em `instances.machine` e um ponto por heartbeat/poll (máx. 1 a cada 30s por instância) em `instance_metrics`, expurgado em 14 dias.
68
68
 
69
- ## Tasks — polling, criação e reporte
69
+ ## Execuções — polling, criação e reporte
70
70
 
71
- Endpoints sob `<baseUrl>/v1/tasks`, todos com `Authorization: Bearer <clientSecret>` (a credencial única do agente):
71
+ Endpoints sob `<baseUrl>/v1/runs`, todos com `Authorization: Bearer <clientSecret>` (a credencial única do agente):
72
72
 
73
- ### `POST /v1/tasks/poll`
73
+ ### `POST /v1/runs/poll`
74
74
 
75
75
  Corpo `PollRequest`:
76
76
 
@@ -82,32 +82,31 @@ Corpo `PollRequest`:
82
82
  "version": "3.0.0",
83
83
  "host": "vm-01",
84
84
  "environment": "production", // ambiente da instância: development | testing | staging | production
85
- "tasks": ["processar-nfe", "monitor-pasta"], // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
85
+ "mode": "on_demand", // como a instância trabalha: on_demand (register) | always_on (start)
86
86
  "poll_interval_ms": 2000,
87
- // colunas de itens declaradas no código (opção `items`) — só as tasks que declararam;
87
+ // colunas de itens declaradas no código (opção `items`) — uma declaração, do agente;
88
88
  // ausente = nada declarado (o servidor nunca apaga a declaração anterior por ausência)
89
- "task_items": { "processar-nfe": { "attributes": [{ "key": "numero_nota" }, { "key": "valor", "label": "valor (R$)" }] } }
89
+ "items": { "attributes": [{ "key": "numero_nota" }, { "key": "valor", "label": "valor (R$)" }] }
90
90
  },
91
- "max": 2, // soma dos slots livres das tasks remotas (1..5)
92
- "tasks": ["processar-nfe"] // só tasks de register() com slot livre — a concorrência é por task
91
+ "max": 2 // slots livres de `concurrency` (1..5)
93
92
  }
94
93
  ```
95
94
 
96
- Resposta `PollResponse` = `{ runs: RunDescriptor[] }`, cada `{ id, task, params, form, source, created_at }`.
95
+ Resposta `PollResponse` = `{ runs: RunDescriptor[] }`, cada `{ id, params, form, source, created_at }`.
97
96
 
98
- O `instance.instance_id` é a **mesma identidade** do heartbeat de telemetria (`instance_id`): o servidor correlaciona os dois canais numa tabela `instances` por `(agent_id, instance_id)`. Os dois derivam do mesmo `instanceId` — o robô aparece como um agente só no painel. O próprio poll também marca a presença da instância (`last_poll_at`). Um processo só com tasks de `start()` nunca faz poll.
97
+ O `instance.instance_id` é a **mesma identidade** do heartbeat de telemetria (`instance_id`): o servidor correlaciona os dois canais numa tabela `instances` por `(agent_id, instance_id)`. Os dois derivam do mesmo `instanceId` — o robô aparece como um agente só no painel. O próprio poll também marca a presença da instância (`last_poll_at`). Um processo no modo `start()` nunca faz poll.
99
98
 
100
99
  **Backoff:** um request por poll; o loop decide o ritmo. HTTP 401 (key rejeitada) → retry lento a 30s com log throttled; outros erros → ladder `[1s, 4s, 16s]`.
101
100
 
102
- ### `POST /v1/tasks/runs`
101
+ ### `POST /v1/runs`
103
102
 
104
- Modo `start`: a instância abre a **própria run**. Corpo `RunCreateRequest` — `{ "task": "monitor-pasta", "instance": { /* InstanceInfo, obrigatório */ } }`. Resposta `201` `RunCreateResponse` = o mesmo `RunDescriptor` do poll, com `params: {}`, `form: null` e `source: 'agent'`.
103
+ Modo `start`: a instância abre a **própria run**. Corpo `RunCreateRequest` — `{ "instance": { /* InstanceInfo (com `mode: "always_on"`), obrigatório */ } }`. Resposta `201` `RunCreateResponse` = o mesmo `RunDescriptor` do poll, com `params: {}`, `form: null` e `source: 'agent'`.
105
104
 
106
105
  - A run nasce **`running`** (`started_at`, `claimed_at` e `last_report_at` = agora, `claim_count` 1), sem formulário nem solicitante, vinculada à instância informada. Dali em diante é reportada pelas rotas abaixo exatamente como uma run reivindicada no poll.
107
- - `touchInstance` roda antes do insert (presença); se falhar, a API responde `503` e o SDK insiste com o ladder do poll (o handler só começa quando a run existe). Corpo vazio, sem `instance.instance_id` ou com `task` fora de 1..255 → `422`.
106
+ - `touchInstance` roda antes do insert (presença); se falhar, a API responde `503` e o SDK insiste com o ladder do poll (o handler só começa quando a run existe). Corpo vazio ou sem `instance.instance_id` → `422`.
108
107
  - Sem dedupe: um processo reiniciado em menos de 120 s deixa a run anterior `running` até o sweep marcá-la `AgentTerminated`. O app não oferece "reexecutar" para runs `agent` — ninguém faria poll por elas.
109
108
 
110
- ### `PATCH /v1/tasks/runs/{id}`
109
+ ### `PATCH /v1/runs/{id}`
111
110
 
112
111
  Corpo `RunUpdateRequest`:
113
112
 
@@ -117,7 +116,7 @@ Corpo `RunUpdateRequest`:
117
116
  "logs": [
118
117
  { "ts": "…", "level": "info", "message": "…" }, // delta; o servidor concatena
119
118
  { "ts": "…", "level": "info", "message": "portal após o login",
120
- "attachments": ["<32 hex>"] } // opcional; ids de anexo (máx. 4)
119
+ "attachments": ["<32 hex>"] } // opcional; ids de anexo (máx. 4) — hobots.image() no log
121
120
  ],
122
121
  "error": { "type": "Error", "message": "…", "stack": "…" }, // só com 'failed'
123
122
  "progress": { "current": 10, "total": 100 } // opcional: snapshot de hobots.progress
@@ -134,7 +133,7 @@ Corpo `RunUpdateRequest`:
134
133
 
135
134
  Ciclo de vida completo da run no servidor (`RunStatus`): `pending → claimed → running → succeeded | failed | canceled` (runs `agent` nascem em `running`). O agente só transiciona para `running`/`succeeded`/`failed` (`RunUpdateStatus`).
136
135
 
137
- ### `POST /v1/tasks/runs/{id}/items`
136
+ ### `POST /v1/runs/{id}/items`
138
137
 
139
138
  Registra em lote os **itens de execução** (a entrega do bot — `hobots.items` no SDK). Insert-only: cada item chega com o status final; um `id` repetido na mesma run é ignorado (retry de rede idempotente).
140
139
 
@@ -149,13 +148,16 @@ Corpo `RunItemsCreateRequest`:
149
148
  "status": "succeeded", // 'succeeded' | 'failed' | 'occurrence'
150
149
  "message": "…", // opcional — motivo (failed/occurrence), máx. 2100
151
150
  "payload": { "numero_nota": "12345", "cnpj_prestador": "…" }, // opcional, máx. 16 KB
152
- "ts": "2026-08-10T12:00:00.000Z" // opcional — relógio do agente, só exibição
151
+ "ts": "2026-08-10T12:00:00.000Z", // opcional — relógio do agente, só exibição
152
+ "attachments": ["<32 hex>"] // opcional — ids de anexo (máx. 4): print do item
153
153
  }
154
154
  ]
155
155
  }
156
156
  ```
157
157
 
158
- Resposta `200` `RunItemsCreateResponse`: `{ "accepted": 1, "duplicates": 0 }` — `duplicates` conta os ignorados por `(run, id)` repetido.
158
+ Resposta `200` `RunItemsCreateResponse`: `{ "accepted": 1, "duplicates": 0 }` — `duplicates` conta os colapsados por `(run, id)` repetido no mesmo lote (vale o último reporte; no banco é upsert).
159
+
160
+ - `attachments` é **opcional** e aditivo (uma API anterior ao recurso descarta o campo). As steps carregam o print do mesmo jeito, em `data.attachments` do `StepData` que vai no `POST /v1/ingest` da transaction — sem campo novo no wire.
159
161
 
160
162
  - **Run cancelada/terminal aceita itens** (200): item é fato de trabalho já feito — mesmo racional do log tardio. Esta rota não tem 409.
161
163
  - O insert e os **contadores materializados** da run (`items_total/succeeded/failed/occurrence`, que alimentam os totalizadores no app) saem num único statement atômico no servidor.
@@ -169,7 +171,7 @@ Resposta `200` `RunItemsCreateResponse`: `{ "accepted": 1, "duplicates": 0 }`
169
171
  | `422` | payload inválido |
170
172
  | `429` | limite de 10.000 itens por execução atingido (lote inteiro rejeitado) |
171
173
 
172
- ### `POST /v1/tasks/runs/{id}/attachments`
174
+ ### `POST /v1/runs/{id}/attachments`
173
175
 
174
176
  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.
175
177
 
@@ -177,11 +179,11 @@ Corpo `RunAttachmentCreateRequest`:
177
179
 
178
180
  ```jsonc
179
181
  {
180
- "id": "<32 hex>", // gerado pelo agente, já referenciado na linha de log
182
+ "id": "<32 hex>", // gerado pelo agente, já referenciado no log/item/step
181
183
  "content_type": "image/png", // 'image/png' | 'image/jpeg' | 'image/webp'
182
184
  "size_bytes": 184320, // exato — entra na assinatura da URL
183
185
  "caption": "portal após o login", // opcional, máx. 500
184
- "log_ts": "…", // opcional, ISO 8601 — ts da linha de log
186
+ "log_ts": "…", // opcional, ISO 8601 — ts do evento (log/item/step)
185
187
  "width": 1920, "height": 1080 // opcionais
186
188
  }
187
189
  ```
@@ -210,17 +212,17 @@ O PUT no S3 precisa mandar exatamente esses headers — eles fazem parte da assi
210
212
  | `503` | anexos não habilitados nesta instalação (sem bucket S3 configurado) |
211
213
  | `502` | falha ao presignar — o registro é desfeito |
212
214
 
213
- ### `PATCH /v1/tasks/runs/{id}/attachments/{attachmentId}`
215
+ ### `PATCH /v1/runs/{id}/attachments/{attachmentId}`
214
216
 
215
217
  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.
216
218
 
217
219
  É **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.
218
220
 
219
- 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.
221
+ Limites de anexo (aplicados no SDK **e** no servidor): PNG/JPEG/WEBP, 5 MB por imagem, 20 por execução, 4 por evento (linha de log, item ou step).
220
222
 
221
223
  ## Onde vive o claim atômico
222
224
 
223
- O `POST /v1/tasks/poll` no app chama `claimPendingRuns()` em
225
+ O `POST /v1/runs/poll` no app chama `claimPendingRuns()` em
224
226
  [`apps/api/lib/tasks/claim.ts`](../../../apps/api/lib/tasks/claim.ts): um CTE que seleciona runs `pending` vencidas com `FOR UPDATE SKIP LOCKED` e as promove a `claimed` num único `UPDATE ... RETURNING`. Isso garante que dois agentes concorrentes nunca peguem a mesma run. **Nada disso está no SDK** — ele só faz o HTTP.
225
227
 
226
228
  ## Identidade do SDK
@@ -1,27 +1,27 @@
1
- # Tasks
1
+ # Execuções
2
2
 
3
- Uma **task** é o que o robô sabe fazer, identificada por um slug. Cada rodada é uma **execução** (run) na plataforma, com logs, itens, progresso, anexos e resultado. Há dois modos de registrar uma task; tudo o mais (o objeto `hobots`, cancelamento, retry) é igual nos dois.
3
+ O **agente** é o seu robô, e o handler que você passa a `register()`/`start()` é o que ele faz. Cada rodada é uma **execução** (run) na plataforma, com logs, itens, progresso, anexos e resultado. Há dois modos de iniciar o agente; tudo o mais (o objeto `hobots`, cancelamento, retry) é igual nos dois.
4
4
 
5
5
  ## Os dois modos
6
6
 
7
7
  ```js
8
- // sob demanda: uma execução por solicitação do app (formulário com este task_slug ou agendamento)
9
- hobots.register('processar-nfe', async params => { /* … */ }, { concurrency: 2 });
8
+ // sob demanda: uma execução por solicitação do app (formulário do agente ou agendamento)
9
+ hobots.register(async params => { /* … */ }, { concurrency: 2 });
10
10
 
11
11
  // sempre ligado: o robô abre a própria execução e roda o handler (o seu loop) uma vez
12
- hobots.start('monitor-pasta', async () => { /* while (true) { … } */ });
12
+ hobots.start(async () => { /* while (true) { … } */ });
13
13
  ```
14
14
 
15
- - `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário. Registre antes de haver solicitações; o polling começa sozinho.
16
- - `start` — ao iniciar, o SDK cria a execução (`POST /v1/tasks/runs`) e roda o handler com `params = {}`. Enquanto a plataforma não responde, insiste com backoff — o handler só começa quando a execução existe. Quando o handler retorna, a execução fecha como `succeeded` (ou `failed` após os retries) e o processo segue vivo pelo heartbeat. **Uma execução por processo** — para "recomeçar", reinicie o robô.
17
- - Os dois coexistem no mesmo processo. Um slug só pode ser registrado uma vez (lança).
15
+ - `register` — o robô faz polling na plataforma; cada solicitação vira uma execução com os `params` do formulário. O polling começa sozinho.
16
+ - `start` — ao iniciar, o SDK cria a execução (`POST /v1/runs`) e roda o handler com `params = {}`. Enquanto a plataforma não responde, insiste com backoff — o handler só começa quando a execução existe. Quando o handler retorna, a execução fecha como `succeeded` (ou `failed` após os retries) e o processo segue vivo pelo heartbeat. **Uma execução por processo** — para "recomeçar", reinicie o robô.
17
+ - **Uma chamada por processo**: `register` ou `start`. A segunda lança (`o agente já foi iniciado`). Dois trabalhos diferentes são dois agentes.
18
18
 
19
19
  > No modo start, lembre do teto de **1000 linhas de log por execução** no servidor: logue novidades e use `hobots.items` para o trabalho feito (até 10.000 itens).
20
20
 
21
21
  ## O handler e o objeto `hobots`
22
22
 
23
23
  ```ts
24
- type TaskHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
24
+ type RunHandler<P = Record<string, unknown>> = (params: P, ctx: RunContext) => void | Promise<void>;
25
25
  ```
26
26
 
27
27
  Dentro do handler — e em **qualquer módulo** chamado por ele — o objeto global `hobots` resolve a execução atual:
@@ -40,15 +40,15 @@ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto
40
40
 
41
41
  | Membro | Notas |
42
42
  | --- | --- |
43
- | `hobots.log.<debug\|info\|warn\|error\|success>(msg)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal. Nunca lança. |
43
+ | `hobots.log.<debug\|info\|warn\|error\|success>(msg, ...rest)` | Enviado incrementalmente (PATCHes com throttle) e ecoado no terminal (`[hobots] INFO …`). Nunca lança. `hobots.image(...)` no último argumento = print da linha. |
44
44
  | `hobots.items` | Ver [Itens](#itens-de-execução-hobotsitems) |
45
45
  | `hobots.progress` | Ver [Progresso](#progresso-hobotsprogress) |
46
- | `hobots.transaction` | Transaction da execução, já correlacionada e finalizada pelo SDK. `startChild(op, descrição)` = uma step no waterfall. |
47
- | `hobots.attach(imagem, opts?)` | Ver [Anexos](#anexos-screenshots) |
46
+ | `hobots.transaction` | Transaction da execução (`run`), já correlacionada e finalizada pelo SDK. `startChild(op, descrição)` = uma step no waterfall. |
47
+ | `hobots.image(imagem, opts?)` | Ver [Imagens](#imagens-screenshots) |
48
48
  | `hobots.captureException(erro, hint?)` | Issue no módulo Issues; `hint.attachment` anexa um print à linha de log |
49
49
  | `hobots.retry(fn, opts?)` | Ver [Retry](#retry) |
50
50
  | `hobots.sleep(ms)` · `hobots.signal` · `hobots.throwIfCanceled()` | Ver [Cancelamento](#cancelamento-cooperativo) |
51
- | `hobots.runId` · `hobots.task` · `hobots.params` | Identidade da execução |
51
+ | `hobots.runId` · `hobots.params` | Identidade da execução |
52
52
 
53
53
  - **Retorno** do handler: **ignorado** (resultados podem conter dados sensíveis). Reporte por logs e itens.
54
54
  - **Erro lançado**: nova tentativa (ver Retry) e, esgotadas, `failed` com tipo, mensagem (cap 10k) e stack (cap 20k), passando pelo scrub de segredos.
@@ -57,7 +57,7 @@ Isso funciona com `concurrency > 1` porque cada execução roda no seu contexto
57
57
 
58
58
  ## Itens de execução (`hobots.items`)
59
59
 
60
- A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas na aba de itens da execução no app), totalizadores na solicitação.
60
+ A **entrega do robô**: cada item processado, com status e atributos livres (viram colunas na aba de itens da execução no app), totalizadores no trabalho.
61
61
 
62
62
  ```js
63
63
  hobots.items.succeeded({ numero_nota: nota.numero, cnpj: nota.cnpj }, { id: nota.numero });
@@ -96,20 +96,20 @@ const resultado = await hobots.items.each(
96
96
  - Só **cancelamento, timeout e shutdown** interrompem o loop: o `RunCanceled` sobe, o item em curso não é registrado e o progresso não avança.
97
97
  - Aceita `Iterable` ou `AsyncIterable` (sem `length`, nenhum total é declarado).
98
98
 
99
- > Se o handler da task termina com o progresso parado antes do total declarado (ex.: 45/76), o SDK deixa um aviso no log da execução — e a Central sinaliza o desencontro entre processados e itens reportados.
99
+ > Se o handler termina com o progresso parado antes do total declarado (ex.: 45/76), o SDK deixa um aviso no log da execução — e a Central sinaliza o desencontro entre processados e itens reportados.
100
100
 
101
101
  ### Colunas da aba de itens
102
102
 
103
- As colunas vêm do código, não do formulário — valem para `register()` (com ou sem formulário) e `start()`:
103
+ As colunas vêm do código, não do formulário — é **uma declaração só, do agente**, válida para `register()` (com ou sem formulário) e `start()`:
104
104
 
105
105
  ```js
106
- register('processar-nfe', handler, {
106
+ hobots.register(handler, {
107
107
  items: { columns: ['numero_nota', { key: 'cnpj', label: 'CNPJ' }, { key: 'valor', label: 'valor (R$)' }] },
108
108
  });
109
109
  ```
110
110
 
111
111
  - Ordem da lista = ordem das colunas; até 12; chave com letras, números, `.`, `-`, `_` (inválidas são descartadas com aviso em `debug`).
112
- - A declaração vai na presença da instância (poll / abertura de run). No app, **Configurações → agente → itens da execução** pode personalizar as colunas (e restaurar as do código).
112
+ - A declaração vai na presença da instância (`instance.items` no poll / abertura de run). No app, **Configurações → agente → itens da execução** pode personalizar as colunas (e restaurar as do código).
113
113
  - Sem declaração nem personalização, o app mostra as chaves encontradas nos próprios payloads.
114
114
 
115
115
  ## Progresso (`hobots.progress`)
@@ -124,30 +124,49 @@ for (const nota of notas) {
124
124
 
125
125
  Opcional e independente de `items`. Sem `total()` nada é enviado. Snapshot absoluto que pega carona nos PATCHes de log (no pior caso 1/s); o PATCH terminal leva o valor final. `set(atual, total?)`, `current` e `snapshot` completam a API. `total/advance/set` são pontos de parada cooperativos; `current`/`snapshot` nunca lançam.
126
126
 
127
- ## Anexos (screenshots)
127
+ ## Imagens (screenshots)
128
+
129
+ Um print vai **junto do evento** que ele ilustra — a linha de log, o item ou a step — para o
130
+ usuário ver o estado do robô no momento em que aquilo aconteceu (botão de imagem no app):
128
131
 
129
132
  ```js
130
- await hobots.attach(await page.screenshot(), { caption: 'portal após o login' });
133
+ // na linha de log: hobots.image(...) como ÚLTIMO argumento
134
+ hobots.log.info('portal após o login', hobots.image(await page.screenshot()));
135
+ hobots.log.error('login falhou', hobots.image(await page.screenshot(), { caption: 'tela do erro' }));
136
+
137
+ // no item
138
+ hobots.items.failed('nota rejeitada', { numero }, { image: await page.screenshot() });
139
+ await hobots.items.each(notas, async (nota, item) => {
140
+ …
141
+ item.occurrence('sem fatura no mês', undefined, { image: await page.screenshot() });
142
+ });
131
143
 
144
+ // na step
145
+ const step = hobots.transaction.startChild('emitir', numero);
132
146
  try {
133
- await emitir(params);
147
+ await emitir(numero);
134
148
  } catch (erro) {
135
- hobots.captureException(erro, { attachment: { image: await page.screenshot(), caption: 'tela no momento do erro' } });
149
+ step.setError(erro, undefined, { image: await page.screenshot() });
136
150
  throw erro;
151
+ } finally {
152
+ step.finish();
137
153
  }
154
+
155
+ // numa issue (módulo Issues) — o print vai na linha de log que espelha o erro
156
+ hobots.captureException(erro, { attachment: hobots.image(await page.screenshot(), { caption: 'tela no momento do erro' }) });
138
157
  ```
139
158
 
140
- O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo. A linha de log entra na hora; o upload sobe em segundo plano direto para o S3 (URL presignada) e **nunca lança nem bloqueia** — falhas viram um aviso no log da execução. Limites: PNG/JPEG/WEBP, 5 MB, 20 por execução, 4 por linha de log. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
159
+ O SDK não tira o print: aceita bytes (`Uint8Array`/`ArrayBuffer`/`Buffer`) ou um caminho de arquivo, crus ou embrulhados por `hobots.image(bytes, { caption?, contentType? })` (só o embrulho leva legenda; em `log.*` ele é obrigatório para distinguir a imagem de um argumento comum). O evento entra na hora com o id do anexo; o upload sobe em segundo plano direto para o S3 (URL presignada) e **nunca lança nem bloqueia** — falhas viram um aviso no log da execução. Limites: PNG/JPEG/WEBP, 5 MB, 20 por execução, 4 por evento. Fora de uma execução não há onde pendurar a imagem (descartada com aviso).
141
160
 
142
161
  ## Cancelamento cooperativo
143
162
 
144
- Cancelar a solicitação no app **não mata** o handler: o SDK detecta (o PATCH periódico recebe 409), dispara `hobots.signal` e o próximo **ponto de parada** lança `RunCanceled`:
163
+ Cancelar o trabalho no app **não mata** o handler: o SDK detecta (o PATCH periódico recebe 409), dispara `hobots.signal` e o próximo **ponto de parada** lança `RunCanceled`:
145
164
 
146
165
  - `hobots.items.*`, `hobots.progress.total/advance/set`, `hobots.transaction.startChild()`, `hobots.sleep()` e cada tentativa de `hobots.retry()`.
147
166
  - **Nunca `hobots.log.*`** — log em `finally` de limpeza não pode explodir.
148
167
 
149
168
  ```js
150
- hobots.register('processar-nfe', async params => {
169
+ hobots.register(async params => {
151
170
  const browser = await abrir();
152
171
  try {
153
172
  for (const nota of params.notas) {
@@ -181,7 +200,7 @@ const dados = await hobots.retry(() => portal.buscar(nota), { retries: 3, delayM
181
200
 
182
201
  ## Concorrência (`register`)
183
202
 
184
- `concurrency` (1..5, default 1) por task. O agente só pede à plataforma o que consegue rodar (tasks com slot livre, `max` = soma dos slots, teto 5) e entra em **drain mode** — re-pesquisa imediatamente enquanto vem trabalho; sem trabalho, espera `pollIntervalMs`.
203
+ `concurrency` (1..5, default 1) é **do agente**: quantas solicitações ele atende ao mesmo tempo. O agente só pede à plataforma o que consegue rodar (`max` = slots livres, teto 5) e entra em **drain mode** — re-pesquisa imediatamente enquanto vem trabalho; sem trabalho, espera `pollIntervalMs`.
185
204
 
186
205
  ## Timeout
187
206
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hobots",
3
- "version": "3.3.0",
4
- "description": "SDK JavaScript da Hobots \u2014 tudo \u00e9 uma task: register() atende solicita\u00e7\u00f5es da plataforma, start() roda sempre e reporta logs, itens e progresso. Credenciais por env, heartbeat sempre ligado, objeto global `hobots`.",
3
+ "version": "4.0.0",
4
+ "description": "SDK JavaScript da Hobots — o agente é a própria tarefa: register() atende solicitações da plataforma (sob demanda), start() roda sempre ligado e reporta logs, itens e progresso. Credenciais por env, heartbeat sempre ligado, objeto global `hobots`.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "license": "MIT",
@@ -33,11 +33,13 @@
33
33
  "dev": "tsup --watch",
34
34
  "lint": "eslint . --max-warnings 0",
35
35
  "test": "vitest run",
36
+ "test:coverage": "vitest run --coverage",
36
37
  "test:watch": "vitest",
37
38
  "typecheck": "tsc --noEmit"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^20",
42
+ "@vitest/coverage-v8": "^4.1.10",
41
43
  "@workspace/eslint-config": "workspace:*",
42
44
  "@workspace/typescript-config": "workspace:*",
43
45
  "@workspace/utils": "workspace:*",