hobots 2.4.0 → 3.2.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.
@@ -6,116 +6,70 @@
6
6
  pnpm add hobots
7
7
  ```
8
8
 
9
- Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global). A telemetria também funciona no browser para captura de erros.
9
+ Sem dependências de runtime. Requer **Node 18+** (usa o `fetch` global).
10
10
 
11
11
  ## Conceitos em 2 minutos
12
12
 
13
- Os termos que aparecem nesta página, em uma frase cada. Explicação completa de todos em [Conceitos](./concepts.md).
14
-
15
13
  | Conceito | Em uma frase |
16
14
  | --- | --- |
17
- | [Evento](./concepts.md#evento) | A foto de um erro ou de uma mensagem que você registrou |
18
- | [Tag](./concepts.md#tag) | Rótulo curto e indexável, para filtrar depois (`cliente`, `regiao`) |
19
- | [Heartbeat](./concepts.md#heartbeat) | Batimento "estou vivo" — o servidor acusa offline quando ele para |
20
- | [instanceId](./concepts.md#instanceid) | A identidade da instância do robô; a mesma nos dois canais |
21
- | [Run](./concepts.md#run) | Uma execução solicitada no app, que um agente pega e executa |
22
- | [Fila, flush e close](./concepts.md#fila-flush-e-close) | Nada é enviado na hora — vai para uma fila que precisa ser esvaziada antes do processo morrer |
23
-
24
- ## O Client Secret
25
-
26
- Cada agente tem **um único Client Secret** — a mesma credencial autentica a telemetria **e** as tasks:
27
-
28
- ```
29
- hb_<64 hex>
30
- ```
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 |
17
+ | [Item](./concepts.md#item) | Uma unidade de trabalho entregue pela execução (nota baixada, boleto emitido) |
18
+ | [Heartbeat](./concepts.md#heartbeat) | Batimento "estou vivo" — sempre ligado; o servidor acusa offline quando para |
19
+ | [instanceId](./concepts.md#instanceid) | A identidade da instância do robô no painel |
20
+ | [Issue](./concepts.md#evento-e-issue) | Erros agrupados — `hobots.captureException` e erros não tratados |
31
21
 
32
- A credencial é **secreta** e viaja sempre no header `Authorization: Bearer` — nunca na URL. Copie o Client Secret no app, em **Configurações → Agentes** — os snippets de integração já vêm prontos.
22
+ ## Credenciais
33
23
 
34
- O SDK fala com a API oficial `https://api2.hobots.app` automaticamente — não há configuração de host no `init()`. Em desenvolvimento, aponte para outro host com a env `HOBOTS_API_URL`:
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:
35
25
 
36
26
  ```bash
37
- HOBOTS_API_URL=http://localhost:3001 node meu-robo.js
27
+ export HOBOTS_CLIENT_SECRET=hb_<64 hex>
28
+ export HOBOTS_INSTANCE_ID=vm-cliente-01 # opcional — sem ela, o hostname
38
29
  ```
39
30
 
40
- Veja [Protocolo & transporte](./protocol.md) para os endpoints derivados da URL base.
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`.
41
32
 
42
- ## Telemetria e tasks — qual usar
33
+ ## Primeira task sob demanda (`register`)
43
34
 
44
- As duas capabilities vivem no mesmo `init()` e você usa **uma, outra ou as duas**:
45
-
46
- - Quer só saber se o robô quebrou, quanto demora e se está vivo? → `init({ clientSecret, heartbeat: true })`.
47
- - Quer disparar o robô sob demanda a partir de um formulário no app? → `init({ clientSecret, tasks: true })` + `register()` + `start()`.
48
- - Ambos? Passe `heartbeat` e `tasks` no **mesmo `init()`**. É o cenário do
49
- [`full.ts`](../../hobots-sdk-demos/src/full.ts) nos demos do SDK.
50
-
51
- ## O `init` único
35
+ 1. No app, crie um formulário em **Configurações → Formulários** com o `task_slug` `processar-nfe`.
36
+ 2. No robô:
52
37
 
53
38
  ```js
54
- import * as Hobots from 'hobots';
55
-
56
- Hobots.init({
57
- clientSecret: 'hb_<64 hex>',
58
- environment: 'production',
59
- release: 'robo-nfe@1.4.2',
60
- instanceId: 'vm-cliente-01', // identidade única: heartbeat + agente de tasks
61
- heartbeat: true, // "estou vivo" a cada 30s
39
+ import hobots from 'hobots';
40
+
41
+ hobots.register('processar-nfe', async params => {
42
+ hobots.log.info(`lote ${params.lote}`);
43
+ for (const nota of params.notas) {
44
+ await baixar(nota);
45
+ hobots.items.succeeded({ numero_nota: nota }, { id: nota });
46
+ }
47
+ hobots.log.success('concluído');
62
48
  });
63
49
 
64
- // Erros não tratados são capturados automaticamente.
65
- // Captura manual:
66
- try {
67
- await processarNota(nota);
68
- } catch (error) {
69
- Hobots.captureException(error, { tags: { cliente: 'acme' } });
70
- throw error;
71
- }
50
+ process.on('SIGINT', () => void hobots.close(15_000).then(() => process.exit(0)));
72
51
  ```
73
52
 
74
- Chame `Hobots.init` **uma vez**, o mais cedo possível no processo. Detalhes em [Telemetria](./telemetry.md).
75
-
76
- ## Primeiro agente (tasks)
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.
77
54
 
78
- Habilite a execução sob demanda com o campo `tasks` no mesmo `init()`:
55
+ ## Primeira task sempre ligada (`start`)
79
56
 
80
57
  ```js
81
- import * as Hobots from 'hobots';
82
-
83
- Hobots.init({
84
- clientSecret: 'hb_<64 hex>',
85
- instanceId: 'vm-cliente-01',
86
- tasks: true, // ou { pollIntervalMs, concurrency, ... }
87
- });
88
-
89
- Hobots.register('processar-nfe', async (params, ctx) => {
90
- ctx.log.info(`processando lote ${params.lote}…`);
91
- // Hobots.captureMessage('portal após o login', 'info', { attachment: await page.screenshot() }); // print junto do log
92
- ctx.log.info('12 notas processadas'); // o retorno do handler é ignorado — reporte por log
58
+ import hobots from 'hobots';
59
+
60
+ hobots.start('monitor-pasta', async () => {
61
+ hobots.log.info('monitor iniciado');
62
+ while (true) {
63
+ await hobots.sleep(60_000); // cancelável — para se a execução for cancelada no app
64
+ for (const arquivo of await novosArquivos()) hobots.items.succeeded({ arquivo });
65
+ }
93
66
  });
94
- Hobots.start(); // mantém o processo vivo até stop()/close()
95
67
  ```
96
68
 
97
- > O `instanceId` é a identidade dos **dois** canais (heartbeat e poll de tasks) — o robô aparece como **um agente só** no módulo Agentes do app, sem configuração extra. Com `tasks: true`, o heartbeat é opcional: o próprio poll já marca a presença do agente.
98
-
99
- As execuções aparecem no módulo **Solicitações** do app; o `task` do `register()` é o `task_slug` do formulário em **Configurações → Formulários**. Detalhes (cancelamento, concorrência, idempotência) em [Tasks](./tasks.md).
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()`.
100
70
 
101
71
  ## Encerramento
102
72
 
103
- `close()` encerra tudo na ordem certa: para o polling de tasks (aguardando a run atual), o heartbeat e faz o flush final do transport.
104
-
105
- Sem `start()` chamado, o SDK não segura o processo (timers `unref`'d), mas faça o flush final para não perder eventos em buffer:
106
-
107
- ```js
108
- await Hobots.close(2000);
109
- ```
110
-
111
- Com as tasks, `start()` segura o processo por design. Encerre de forma graciosa aguardando a run atual:
112
-
113
- ```js
114
- process.on('SIGINT', () => {
115
- void Hobots.close(15_000).then(() => process.exit(0));
116
- });
117
- ```
118
-
119
- > **Atenção:** o `timeoutMs` do `close()` é gasto **duas vezes, em sequência** — uma esperando as runs de tasks, outra no flush final. O `close(15_000)` acima pode levar até ~30s no pior caso. Se o seu SIGTERM tem prazo curto (Kubernetes, systemd), dimensione por aí. Os defaults, quando você omite o argumento, estão em [Tempos e intervalos](./api-reference.md#tempos-e-intervalos).
73
+ `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.
120
74
 
121
- Para parar de aceitar execuções **sem** derrubar a telemetria, use `Hobots.stop(timeoutMs?)`. Conceito das duas funções em [Fila, flush e close](./concepts.md#fila-flush-e-close).
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.
package/docs/protocol.md CHANGED
@@ -18,7 +18,7 @@ hb_<64 hex>
18
18
  | Tasks (execução) | `<baseUrl>/v1/tasks/*` | `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 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
+ 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.
22
22
 
23
23
  ## Telemetria — ingestão
24
24
 
@@ -38,11 +38,11 @@ Endpoint fixo `POST /v1/ingest` recebe **envelopes** JSON autenticados por Beare
38
38
 
39
39
  Tipos de item: `event`, `transaction`, `heartbeat` (definidos em `hobots/protocol`).
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
+ Todo `transaction` carrega `run_id`: é assim que a [transaction da execução](./concepts.md#transaction) (`hobots.transaction`) é correlacionada, e o que permite o app montar o waterfall de performance dentro da solicitação.
42
42
 
43
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
44
 
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).
45
+ **Flush & retries:** o transport flusha a cada 2 s ou ao acumular 10 itens. 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).
46
46
 
47
47
 
48
48
  ### MachineSnapshot
@@ -52,7 +52,7 @@ Recursos da máquina onde a instância roda. Vai em `HeartbeatPayload.machine`,
52
52
  | Campo | Tipo | Descrição |
53
53
  | --- | --- | --- |
54
54
  | `sampled_at` | ISO 8601 | momento da coleta (relógio do cliente) — chave de dedupe da série |
55
- | `instance_id` | string | o `instanceId` do `init()` — liga o snapshot de um evento à série da instância |
55
+ | `instance_id` | string | o `instanceId` da instância — liga o snapshot de um evento à série da instância |
56
56
  | `hostname`, `os`, `arch` | string | identificação da máquina (`process.platform` / `sys.platform`) |
57
57
  | `cpu_count` | int | cores lógicos |
58
58
  | `cpu_percent` | number \| null | uso de CPU do sistema — média entre duas coletas; `null` até haver 2 amostras |
@@ -66,9 +66,9 @@ 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 e reporte
69
+ ## Tasks — polling, criação e reporte
70
70
 
71
- Dois endpoints sob `<baseUrl>/v1/tasks`, ambos com `Authorization: Bearer <clientSecret>` (a credencial única do agente):
71
+ Endpoints sob `<baseUrl>/v1/tasks`, todos com `Authorization: Bearer <clientSecret>` (a credencial única do agente):
72
72
 
73
73
  ### `POST /v1/tasks/poll`
74
74
 
@@ -79,22 +79,34 @@ Corpo `PollRequest`:
79
79
  "instance": {
80
80
  "instance_id": "vm-01", // identidade canônica da instância (obrigatória)
81
81
  "name": "VM 01",
82
- "version": "2.0.0",
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"]
85
+ "tasks": ["processar-nfe", "monitor-pasta"], // TODAS as tasks (register + start) — o painel mostra o que o agente sabe fazer
86
+ "poll_interval_ms": 2000,
87
+ // colunas de itens declaradas no código (opção `items`) — só as tasks que declararam;
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$)" }] } }
86
90
  },
87
- "max": 1, // quantas reivindicar (1..5)
88
- "tasks": ["processar-nfe"] // só runs destas tasks registradas
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
89
93
  }
90
94
  ```
91
95
 
92
96
  Resposta `PollResponse` = `{ runs: RunDescriptor[] }`, cada `{ id, task, params, form, source, created_at }`.
93
97
 
94
- 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)`. Com o `init()` único, os dois derivam do `instanceId` — o robô aparece como um agente só no painel automaticamente. O próprio poll marca a presença da instância (`last_poll_at`), por isso o heartbeat é opcional quando as tasks estão habilitadas.
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.
95
99
 
96
100
  **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]`.
97
101
 
102
+ ### `POST /v1/tasks/runs`
103
+
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'`.
105
+
106
+ - 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`.
108
+ - 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
+
98
110
  ### `PATCH /v1/tasks/runs/{id}`
99
111
 
100
112
  Corpo `RunUpdateRequest`:
@@ -108,23 +120,23 @@ Corpo `RunUpdateRequest`:
108
120
  "attachments": ["<32 hex>"] } // opcional; ids de anexo (máx. 4)
109
121
  ],
110
122
  "error": { "type": "Error", "message": "…", "stack": "…" }, // só com 'failed'
111
- "progress": { "current": 10, "total": 100 } // opcional (SDK ≥ 2.1): snapshot de ctx.progress
123
+ "progress": { "current": 10, "total": 100 } // opcional: snapshot de hobots.progress
112
124
  }
113
125
  ```
114
126
 
115
- - `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).
116
- - `progress` é **opcional** e aditivo (SDK ≥ 2.1): snapshot absoluto de `ctx.progress` (`current` 0..total, `total` ≥ 1) — o servidor **sobrescreve** `progress_current`/`progress_total` e grava `progress_started_at` no primeiro PATCH que o traga (relógio do servidor; âncora da ETA no app). `current > total` é clampado; progresso enviado a uma run já terminal é descartado. O SDK anexa o snapshot mais recente a **qualquer** PATCH que saia (logs, heartbeat, terminal) e só dispara um PATCH próprio, no máximo 1/s, quando nada mais está a caminho. Uma API anterior ao recurso descarta o campo (objeto não-strict).
127
+ - `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). O aviso de cada tentativa do retry do handler é uma dessas linhas.
128
+ - `progress` é **opcional** e aditivo: snapshot absoluto de `hobots.progress` (`current` 0..total, `total` ≥ 1) — o servidor **sobrescreve** `progress_current`/`progress_total` e grava `progress_started_at` no primeiro PATCH que o traga (relógio do servidor; âncora da ETA no app). `current > total` é clampado; progresso enviado a uma run já terminal é descartado. O SDK anexa o snapshot mais recente a **qualquer** PATCH que saia (logs, heartbeat, terminal) e só dispara um PATCH próprio, no máximo 1/s, quando nada mais está a caminho. Uma API anterior ao recurso descarta o campo (objeto não-strict).
117
129
  - `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.
118
130
  - Ordem garantida `running → logs → terminal` (PATCHes serializados numa cadeia de promise).
119
- - Retry `[1s, 4s, 16s]`; 4xx exceto 429 é terminal (**409** = run cancelada / transição inválida → marca a run como morta e dispara `ctx.signal`).
120
- - 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.
131
+ - Retry `[1s, 4s, 16s]`; 4xx exceto 429 é terminal (**409** = run cancelada / transição inválida → marca a run como morta e dispara `hobots.signal`).
132
+ - 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á **120s**, 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.
121
133
  - O protocolo **não transporta resultado** de execução: o retorno do handler é ignorado pelo SDK e o servidor descarta qualquer campo `result` (dados de negócio podem ser sensíveis e não são coletados).
122
134
 
123
- Ciclo de vida completo da run no servidor (`RunStatus`): `pending → claimed → running → succeeded | failed | canceled`. O agente só transiciona para `running`/`succeeded`/`failed` (`RunUpdateStatus`).
135
+ 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`).
124
136
 
125
137
  ### `POST /v1/tasks/runs/{id}/items`
126
138
 
127
- Registra em lote os **itens de execução** (a entrega do bot — `ctx.items` no SDK). Insert-only: cada item chega com o status final; um `id` repetido na mesma run é ignorado (retry de rede idempotente).
139
+ 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).
128
140
 
129
141
  Corpo `RunItemsCreateRequest`:
130
142
 
@@ -213,4 +225,4 @@ O `POST /v1/tasks/poll` no app chama `claimPendingRuns()` em
213
225
 
214
226
  ## Identidade do SDK
215
227
 
216
- `SDK_NAME = 'hobots'` e `SDK_VERSION` ficam em `src/utils.ts` e vão em todo envelope (campo `sdk`) e no `agent.version` do poll. A `SDK_VERSION` é **injetada pelo build** (o `define` do tsup lê a `version` do `package.json`) — não há sincronização manual ao publicar.
228
+ `SDK_NAME = 'hobots'` e `SDK_VERSION` ficam em `src/utils.ts` e vão em todo envelope (campo `sdk`) e no `instance.version` do poll e da criação de run. A `SDK_VERSION` é **injetada pelo build** (o `define` do tsup lê a `version` do `package.json`) — não há sincronização manual ao publicar.