@softize/opus 25.6.0 → 26.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/CHANGELOG.md +108 -1
- package/docs/adr/0017-ui-assumes-a-fixed-desktop-layout.md +19 -0
- package/docs/adr/0057-doc-browser-permite-busca-no-chrome-do-consumidor.md +152 -0
- package/docs/adr/0058-mensagem-da-ia-aceita-imagem-e-o-chat-recebe-anexos.md +201 -0
- package/docs/adr/README.md +12 -0
- package/package.json +1 -1
- package/src/ai/drivers/anthropic.ts +49 -9
- package/src/ai/index.ts +5 -2
- package/src/core/ai-message.ts +20 -0
- package/src/core/index.ts +5 -0
- package/src/core/types.ts +25 -2
- package/src/ui/components/patterns/sidebar.tsx +32 -11
- package/src/ui/components/primitives/chat.tsx +179 -22
- package/src/ui/components/primitives/composer.tsx +397 -7
- package/src/ui/docs/DocBrowser.tsx +404 -69
- package/src/ui/docs/DocCatalog.tsx +255 -0
- package/src/ui/docs/DocHeader.tsx +74 -0
- package/src/ui/docs/browser.tsx +10 -1
- package/src/ui/docs/changelog.tsx +16 -41
- package/src/ui/docs/content/actions.md +3 -3
- package/src/ui/docs/content/ai.md +46 -0
- package/src/ui/docs/content/chat.md +97 -2
- package/src/ui/docs/content/composer.md +144 -1
- package/src/ui/docs/content/customization.md +5 -1
- package/src/ui/docs/content/documentation-portal.md +190 -0
- package/src/ui/docs/content/getting-started.md +129 -87
- package/src/ui/docs/content/installation.md +94 -0
- package/src/ui/docs/content/mcp.md +2 -2
- package/src/ui/docs/content/overview.md +58 -0
- package/src/ui/docs/content/sidebar.md +4 -0
- package/src/ui/docs/index.ts +8 -1
- package/src/ui/docs/markdown.tsx +5 -3
- package/src/ui/docs/records.ts +144 -0
- package/src/ui/docs/registry.tsx +149 -59
- package/src/ui/docs/types.ts +2 -0
- package/src/ui/lib/prepare-image.ts +386 -0
- package/src/ui/meta.ts +2 -2
- package/src/ui/react.tsx +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Changelog
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
3
|
O que muda em cada versão — e, quando quebra, **o que fazer**. Regras de leitura:
|
|
4
4
|
minor = novidade compatível; major = breaking (a seção **Breaking** diz a migração).
|
|
@@ -7,6 +7,113 @@ Depois de qualquer bump, rode os gates (`typecheck` · `test` · `opus check` ·
|
|
|
7
7
|
`opus copy --check` · `base copy check` · `manifest:check`) — eles apontam o que a
|
|
8
8
|
mudança cobra do seu código.
|
|
9
9
|
|
|
10
|
+
## 26.0.0 — 2026-10-09
|
|
11
|
+
|
|
12
|
+
A conversa com a IA pode levar imagens (ADR 0058). Uma mensagem passa a aceitar texto e imagem
|
|
13
|
+
juntos, o driver Anthropic envia as imagens ao modelo, e o `Composer` e o `Chat` recebem imagens
|
|
14
|
+
coladas, soltas ou escolhidas pelo botão **Anexar imagem**. O pacote não guarda imagem: enviar,
|
|
15
|
+
guardar, expirar e mostrar de novo são do aplicativo, que mantém o estado dos anexos.
|
|
16
|
+
|
|
17
|
+
- **Mensagem com imagem.** `AiMessage.content` aceita `string | AiContentPart[]`, em que cada parte é
|
|
18
|
+
`{ type: 'text', text }` ou `{ type: 'image', mediaType, data }`, com PNG, JPEG ou WebP em base64.
|
|
19
|
+
Os tipos `AiContentPart` e `AiImageMediaType` saem de `@softize/opus` e de `@softize/opus/ai`.
|
|
20
|
+
- **`aiMessageText(message)`.** Devolve o texto de uma mensagem: o próprio `content` quando é texto,
|
|
21
|
+
ou as partes de texto unidas por uma linha em branco, sem as imagens. Sai de `@softize/opus` e de
|
|
22
|
+
`@softize/opus/ai`.
|
|
23
|
+
- **Driver Anthropic.** `run` e `runStream` convertem cada parte no bloco da API (`text`, ou `image`
|
|
24
|
+
com `source.type: 'base64'`). Uma parte desconhecida, uma lista de partes vazia, uma parte de texto
|
|
25
|
+
vazia, uma imagem fora dos três formatos ou uma imagem numa mensagem `assistant` fazem a chamada
|
|
26
|
+
lançar erro antes de chegar ao provedor, dizendo qual mensagem e qual parte.
|
|
27
|
+
`complete` e `extract` continuam recebendo texto.
|
|
28
|
+
- **`Composer` com imagens.** A propriedade `attachments` liga o botão **Anexar imagem**, a faixa
|
|
29
|
+
acima do campo (miniatura, nome, estado `uploading`, `ready` ou `failed` e remover), colar e
|
|
30
|
+
soltar. A triagem aceita PNG, JPEG e WebP, até 4 por mensagem (`max` muda o limite, e `accept`,
|
|
31
|
+
tipado como `readonly AiImageMediaType[]`, escolhe um subconjunto dos três, sem curinga), e
|
|
32
|
+
explica a recusa acima do campo, ou a entrega a `onReject`. O envio passa com texto ou com uma
|
|
33
|
+
imagem pronta; espera enquanto alguma imagem está em envio e fica bloqueado com uma imagem em
|
|
34
|
+
falha, até ela ser removida ou reenviada. Uma região viva anuncia a imagem que fica pronta ou
|
|
35
|
+
falha, e o foco volta ao campo quando o botão de anexar fica desabilitado pelo limite. Colar
|
|
36
|
+
texto continua colando texto; a imagem colada sem texto recebe "Imagem colada 1.png", com um
|
|
37
|
+
número que não se repete no mesmo composer. O soltar que o composer consome não sobe para os
|
|
38
|
+
contêineres do aplicativo.
|
|
39
|
+
- **`Chat` com imagens.** No modo controlado, `composerAttachments` liga as imagens no composer e o
|
|
40
|
+
soltar sobre a conversa inteira; `onSend(text, attachments)` recebe as imagens prontas, e a
|
|
41
|
+
mensagem pode ter só imagem. Sem `composerAttachments`, `onSend` continua sendo chamado só com o
|
|
42
|
+
texto. O item `user` do transcript aceita `attachments` (`ChatAttachment`,
|
|
43
|
+
com `id`, `name`, `mediaType`, dimensões, `status: 'available' | 'expired'` e o `url` dado pelo
|
|
44
|
+
aplicativo): a fala mostra as miniaturas acima do texto, com o nome como texto alternativo, e a
|
|
45
|
+
imagem expirada como um cartão com o nome e "Imagem expirada". O modo autogerenciado (`send`) não
|
|
46
|
+
aceita imagem. Com `composerAttachments`, o arraste de arquivos sobre o Chat é consumido nele
|
|
47
|
+
(`stopPropagation` em `dragenter`, `dragover`, `dragleave` e `drop`) e não chega a um contêiner do
|
|
48
|
+
aplicativo com o próprio soltar; sem ele, o arraste segue como antes.
|
|
49
|
+
- **`prepareImage(file, options)`**, de `@softize/opus/ui/react`, prepara a imagem no navegador antes
|
|
50
|
+
do envio: lê só o cabeçalho para conferir o formato pelos bytes e as dimensões, recusa GIF, SVG,
|
|
51
|
+
imagem animada e, antes de decodificar, a imagem acima de `maxPixels` (padrão 40 megapixels);
|
|
52
|
+
reduz o lado maior a `maxEdge` (padrão 1568) respeitando a orientação EXIF
|
|
53
|
+
(`imageOrientation: 'from-image'`) e codifica de novo, descartando metadados. PNG continua PNG quando cabe em
|
|
54
|
+
`maxBytes` (padrão 3 MB); o resto vira JPEG com qualidade 0,85. Devolve o arquivo com tipo,
|
|
55
|
+
dimensões e tamanho, ou a recusa com o motivo e a mensagem em pt-BR.
|
|
56
|
+
|
|
57
|
+
Sem as propriedades novas, o `Composer` e o `Chat` se comportam como antes.
|
|
58
|
+
|
|
59
|
+
### Breaking
|
|
60
|
+
|
|
61
|
+
**`AiMessage.content` passa a ser `string | AiContentPart[]`.** Código que lê `content` como texto
|
|
62
|
+
deixa de compilar: `message.content.length`, `message.content.trim()`, uma função que recebe
|
|
63
|
+
`string`, um driver de IA próprio que repassa `content` ao provedor. Migração: leia o texto com
|
|
64
|
+
`aiMessageText(message)` e, num driver próprio, converta cada parte para o formato do provedor e
|
|
65
|
+
recuse com erro a parte que ele não conhece.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { aiMessageText } from '@softize/opus/ai'
|
|
69
|
+
|
|
70
|
+
const title = aiMessageText(message).slice(0, 80) // antes: message.content.slice(0, 80)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
O compilador não aponta o texto montado por interpolação (`${message.content}`) nem o que passa por
|
|
74
|
+
`as`: com uma mensagem com imagem, esses pontos produzem "[object Object]" sem erro. Procure-os à
|
|
75
|
+
parte (`grep -rn '\${[^}]*content' src`) e troque por `aiMessageText`. Um `ChatMessage` do `Chat`
|
|
76
|
+
autogerenciado continua com `content: string` e segue atribuível a `AiMessage`.
|
|
77
|
+
|
|
78
|
+
**`Chat.onSend` ganha o segundo parâmetro opcional `attachments?: ComposerAttachment[]`.** Em tempo de
|
|
79
|
+
execução nada muda sem `composerAttachments`: a chamada continua `onSend(text)`. Um handler que já
|
|
80
|
+
declarava um segundo parâmetro opcional de outro tipo, como `(text, options?) => …`, deixa de
|
|
81
|
+
compilar ao ser passado direto. Migração: envolva-o (`onSend={(text) => runTurn(text)}`) ou, ao ligar
|
|
82
|
+
as imagens, receba `attachments` explicitamente.
|
|
83
|
+
|
|
84
|
+
## 25.7.0 — 2026-10-07
|
|
85
|
+
|
|
86
|
+
Portais de documentação podem manter o mesmo mapa lateral em guias, ADRs e changelog. A superfície
|
|
87
|
+
leve `@softize/opus/ui/docs/browser` passa a oferecer `DocHeader`, `DocCatalog`, `DocRecordDetail`
|
|
88
|
+
e parsers de decisões e versões, sem carregar o catálogo do SDK nem o compilador de exemplos.
|
|
89
|
+
O host continua responsável por fontes, rotas e autorização.
|
|
90
|
+
|
|
91
|
+
`DocBrowser` aceita busca externa controlada, largura inicial da lateral, grupos inicialmente
|
|
92
|
+
recolhidos, destinos globais, páginas fora da árvore e conteúdo do host no lugar de um guia. O modo
|
|
93
|
+
editorial acrescenta contexto, sumário dos títulos renderizados e continuidade entre páginas.
|
|
94
|
+
A busca revela resultados sem perder a escolha manual dos ramos abertos. As novas propriedades
|
|
95
|
+
são opcionais: os consumidores existentes preservam a composição e os defaults anteriores.
|
|
96
|
+
|
|
97
|
+
O cabeçalho compartilhado mantém a busca documental compacta e centralizada. Os catálogos têm
|
|
98
|
+
busca textual própria dentro da página, independente do topo. ADRs apresentam contexto curto;
|
|
99
|
+
changelog mantém o texto das versões e destaca cuidados de atualização. Não há tabela, filtros,
|
|
100
|
+
controles de ordenação ou paginação nesses índices estáticos.
|
|
101
|
+
|
|
102
|
+
A adaptação à largura da tela é restrita ao cabeçalho, à leitura editorial e aos catálogos do
|
|
103
|
+
portal documental. A UI das aplicações e os componentes demonstrados nos exemplos vivos continuam
|
|
104
|
+
com o contrato desktop a partir de `64rem` da ADR 0017.
|
|
105
|
+
|
|
106
|
+
O portal do Opus passa a abrir pela documentação, com Visão geral, Instalação e Primeiros passos
|
|
107
|
+
antes das referências. ADRs e Changelog entram na lateral; os endereços anteriores dos guias e do
|
|
108
|
+
catálogo de UI são preservados. Não há migração obrigatória para quem não adotar essas composições.
|
|
109
|
+
|
|
110
|
+
Links de `SidebarItem` podem declarar `href`, com o tratamento de foco dos botões e abertura em
|
|
111
|
+
outra aba; sem esse campo, o item continua sendo um botão. A consulta do topo preserva a posição
|
|
112
|
+
de leitura dos registros. O changelog limita os cuidados de atualização à seção `Breaking`, sem
|
|
113
|
+
incluir novidades ou correções seguintes; resumos e detalhes preservam a ordem original das seções.
|
|
114
|
+
O changelog embutido usa **Breaking: cuidados ao atualizar** no lugar de **Breaking — o que fazer**.
|
|
115
|
+
O catálogo usa esse mesmo rótulo.
|
|
116
|
+
|
|
10
117
|
## 25.6.0 — 2026-10-07
|
|
11
118
|
|
|
12
119
|
Aplicações que apresentam Markdown do próprio produto podem usar
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- **Status:** aceita.
|
|
4
4
|
- **Data:** 2026-09-15.
|
|
5
|
+
- **Emenda:** 2026-10-07; portal documental delimitado pela ADR 0057.
|
|
5
6
|
|
|
6
7
|
## Contexto
|
|
7
8
|
|
|
@@ -28,6 +29,22 @@ Footers de formulário modal dimensionam as ações pelo conteúdo e usam `Cance
|
|
|
28
29
|
`distribution="equal"` permanece uma escolha explícita para decisões de peso equivalente e,
|
|
29
30
|
nesse caso, o cancelamento usa `outline`.
|
|
30
31
|
|
|
32
|
+
### Exceção: portal de documentação
|
|
33
|
+
|
|
34
|
+
O portal documental tem uma jornada de leitura distinta da UI operacional das aplicações. Conforme
|
|
35
|
+
a ADR 0057, `DocHeader`, a leitura editorial de `DocBrowser` e `DocCatalog`/`DocRecordDetail`
|
|
36
|
+
podem adaptar cabeçalho, espaçamento e sumário à largura da tela. A exceção foi aprovada para
|
|
37
|
+
preservar a composição validada nos portais do Opus e da Grand Brasil.
|
|
38
|
+
|
|
39
|
+
A exceção não torna responsivos `Split`, `Sidebar`, formulários, tabelas ou outros componentes da
|
|
40
|
+
UI das aplicações. O host documental continua responsável pela composição de sua moldura em tela
|
|
41
|
+
estreita. Exemplos vivos usam o mesmo contrato desktop dos componentes demonstrados; o portal não
|
|
42
|
+
promete que todos os exemplos sejam adequados a telas menores que `64rem`.
|
|
43
|
+
|
|
44
|
+
O gate permite variantes de viewport somente nos três arquivos documentais identificados na ADR
|
|
45
|
+
0057. Arquivos novos, mesmo dentro de `docs/`, continuam sujeitos à regra até uma decisão explícita.
|
|
46
|
+
Container queries não entram na exceção.
|
|
47
|
+
|
|
31
48
|
## Consequências
|
|
32
49
|
|
|
33
50
|
- O mesmo componente preserva a composição a partir de `64rem`, inclusive dentro de panes,
|
|
@@ -50,5 +67,7 @@ nesse caso, o cancelamento usa `outline`.
|
|
|
50
67
|
## Verificação
|
|
51
68
|
|
|
52
69
|
- Um teste de inventário reprova variantes de largura e container queries na UI viva.
|
|
70
|
+
- O mesmo teste delimita a exceção documental por arquivo e comprova que ela não alcança componentes
|
|
71
|
+
da aplicação, outros arquivos documentais ou container queries.
|
|
53
72
|
- Testes estruturais fixam layouts, larguras de modal e distribuição dos footers.
|
|
54
73
|
- Documentação pública declara a largura mínima, o default por conteúdo e a exceção `equal`.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# ADR 0057 — Portal documental compartilha navegação, busca e leitura
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita para implementação local; publicação pendente.
|
|
4
|
+
- **Data:** 2026-10-07.
|
|
5
|
+
|
|
6
|
+
## Contexto
|
|
7
|
+
|
|
8
|
+
Portais de documentação precisam apresentar guias, decisões e entregas sem trocar o mapa de
|
|
9
|
+
navegação a cada destino. O `DocBrowser` já organiza páginas, busca e árvore, mas sua composição
|
|
10
|
+
original mantém o campo na lateral e não permite usar o cabeçalho ou substituir a leitura por um
|
|
11
|
+
catálogo. Os portais do Opus e da Grand Brasil precisam dessas variações.
|
|
12
|
+
|
|
13
|
+
Copiar a árvore, a filtragem e o grid em cada aplicação cria comportamentos concorrentes e repete
|
|
14
|
+
defeitos de layout. Incorporar marca, rotas e autorização ao navegador, por outro lado, faria o SDK
|
|
15
|
+
assumir decisões do produto hospedeiro. A extensão precisa compartilhar a composição sem transferir
|
|
16
|
+
essas responsabilidades.
|
|
17
|
+
|
|
18
|
+
## Decisão vigente
|
|
19
|
+
|
|
20
|
+
A superfície leve `@softize/opus/ui/docs/browser` oferece navegação, cabeçalho e leitura para fontes
|
|
21
|
+
fornecidas pelo host, sem importar o catálogo do Opus nem o compilador de exemplos vivos. A entrada
|
|
22
|
+
histórica `@softize/opus/ui/docs` mantém o catálogo completo e reexporta a mesma composição.
|
|
23
|
+
|
|
24
|
+
### Cabeçalho e busca documental
|
|
25
|
+
|
|
26
|
+
`DocHeader` recebe marca, busca, ações e navegação opcional. Centraliza a geometria do topo, com
|
|
27
|
+
busca compacta de até `22rem`, colunas simétricas nas extremidades e uma linha própria para o campo
|
|
28
|
+
em telas menores que `64rem`. Não conhece tema, repositório, rotas ou estado de pesquisa.
|
|
29
|
+
|
|
30
|
+
`DocBrowser.search` mantém duas composições:
|
|
31
|
+
|
|
32
|
+
- `placement: "sidebar"` preserva o campo e o estado local existentes.
|
|
33
|
+
- `placement: "external"` recebe o valor controlado pelo host, filtra a mesma árvore e não
|
|
34
|
+
renderiza um segundo campo.
|
|
35
|
+
|
|
36
|
+
Nos dois portais, a busca documental mantém rótulo, apresentação, valor e função em guias,
|
|
37
|
+
catálogos e registros. Pesquisa os guias na lateral; não se transforma silenciosamente numa busca
|
|
38
|
+
de ADRs ou versões.
|
|
39
|
+
|
|
40
|
+
### Hierarquia e leitura
|
|
41
|
+
|
|
42
|
+
`sidebarSize` configura a largura inicial usando o `PaneSize` do `Split`. A omissão preserva
|
|
43
|
+
`14rem`. `sidebarInitiallyCollapsed` permite começar com seções e grupos nomeados recolhidos,
|
|
44
|
+
inclusive quando a pessoa abre uma página por URL direta. A busca revela os resultados
|
|
45
|
+
temporariamente; limpá-la restaura os ramos abertos manualmente.
|
|
46
|
+
|
|
47
|
+
`sidebarLinks` apresenta destinos globais após os capítulos, no mesmo nível visual e fora da
|
|
48
|
+
sequência anterior/próxima. `active` identifica o catálogo atual. `navigationTitle` oferece um
|
|
49
|
+
rótulo curto para uma página sem alterar seu título completo ou os termos pesquisáveis.
|
|
50
|
+
|
|
51
|
+
Esses destinos usam `SidebarItem.href`, não uma cópia do estilo da linha. A extensão preserva o
|
|
52
|
+
botão quando `href` é omitido, o foco nos dois modos e a abertura nativa em outra aba. Quando há
|
|
53
|
+
`onClick`, somente a navegação simples é interceptada; o host mantém a decisão de roteamento.
|
|
54
|
+
|
|
55
|
+
O modo opcional `editorial` acrescenta contexto, sumário dos headings realmente renderizados e
|
|
56
|
+
links de continuidade segundo a ordem declarada. `additionalPages` preserva URLs de páginas fora
|
|
57
|
+
da árvore e da sequência. O renderer de exemplos aceita `codeCollapsible={false}` quando ocultar
|
|
58
|
+
o código interromperia uma explicação didática.
|
|
59
|
+
|
|
60
|
+
### Catálogos e registros
|
|
61
|
+
|
|
62
|
+
`content` substitui a área de leitura sem desmontar a árvore. Quando esse slot é fornecido, o
|
|
63
|
+
host resolve rota e título; o navegador não escolhe uma página dos guias nem aplica seus redirects.
|
|
64
|
+
`null` representa uma leitura intencionalmente vazia. `sidebar={false}` permite atender
|
|
65
|
+
consumidores que precisem de uma página isolada, mas os dois portais mantêm a lateral em
|
|
66
|
+
guias, catálogos e registros.
|
|
67
|
+
|
|
68
|
+
`DocCatalog` oferece leitura editorial de registros estáticos, sem tabela, filtros, ordenação
|
|
69
|
+
configurável ou paginação. ADRs apresentam número, título, metadata declarada e o primeiro
|
|
70
|
+
parágrafo do contexto. Changelog apresenta texto integral por versão, com data e cuidados de
|
|
71
|
+
atualização em destaque. Os registros mais recentes aparecem primeiro.
|
|
72
|
+
|
|
73
|
+
Cada catálogo tem um campo textual na própria página. `query` e `onQueryChange` são controlados
|
|
74
|
+
pelo host, independentemente da busca documental. Os portais preservam a consulta local ao abrir
|
|
75
|
+
um registro e voltar, e a limpam ao trocar de catálogo; não limpam a consulta do topo pela navegação.
|
|
76
|
+
|
|
77
|
+
`DocRecordDetail` apresenta somente o nome do catálogo antes do título, sem breadcrumb ou retorno
|
|
78
|
+
duplicado. A navegação permanece na lateral. Para preservar os ramos abertos, o host mantém a mesma
|
|
79
|
+
instância de `DocBrowser` entre guia, catálogo e registro.
|
|
80
|
+
|
|
81
|
+
Os parsers projetam as mesmas fontes Markdown, sem cadastro editorial paralelo. Não inferem data
|
|
82
|
+
ou status ausentes, resumo sem seção de contexto ou releases anteriores ao versionamento.
|
|
83
|
+
Ressalvas do status permanecem no texto integral da ADR.
|
|
84
|
+
|
|
85
|
+
Este índice estático é uma exceção deliberada ao uso de `ActionList`: publica arquivos versionados
|
|
86
|
+
e funciona sem API da aplicação. Não substitui coleções de recursos servidas por actions.
|
|
87
|
+
|
|
88
|
+
### Relação com o contrato desktop da UI
|
|
89
|
+
|
|
90
|
+
A composição documental é uma exceção explícita à ADR 0017: `DocHeader.tsx`, `DocBrowser.tsx`
|
|
91
|
+
(na leitura editorial) e `DocCatalog.tsx` (incluindo `DocRecordDetail`) podem usar variantes de
|
|
92
|
+
viewport para organizar cabeçalho, sumário e espaçamento. A exceção fica restrita a esses arquivos;
|
|
93
|
+
não libera toda a pasta `docs/` nem container queries. O gate verifica as funções de composição
|
|
94
|
+
permitidas, sem liberar outras funções ou a montagem de `Split` no mesmo arquivo.
|
|
95
|
+
|
|
96
|
+
Essa adaptação foi aprovada para o portal de leitura, não para a UI das aplicações. O host organiza
|
|
97
|
+
sua moldura em telas estreitas; `Split`, `Sidebar` e os exemplos vivos não ganham um contrato
|
|
98
|
+
responsivo por estarem dentro da documentação. Manter o portal inteiramente desktop descartaria
|
|
99
|
+
a composição já validada; ampliar a exceção para toda a UI alteraria uma decisão fora deste escopo.
|
|
100
|
+
|
|
101
|
+
## Alternativas consideradas
|
|
102
|
+
|
|
103
|
+
### Manter campo, largura e leitura fixos
|
|
104
|
+
|
|
105
|
+
Preservaria uma API menor, mas obrigaria portais autônomos a copiar a árvore ou adotar a composição
|
|
106
|
+
do catálogo embutido. As novas propriedades são opcionais e mantêm os defaults existentes.
|
|
107
|
+
|
|
108
|
+
### Compor todo o portal dentro de DocBrowser
|
|
109
|
+
|
|
110
|
+
Centralizaria a implementação, ao custo de misturar identidade visual, navegação global,
|
|
111
|
+
autorização e rotas do host com a leitura documental. `DocHeader` compartilha somente a geometria;
|
|
112
|
+
`content` compartilha o mapa sem decidir os destinos.
|
|
113
|
+
|
|
114
|
+
### Copiar grids e filtragem nos consumidores
|
|
115
|
+
|
|
116
|
+
Evitaria a extensão do pacote, mas os primeiros consumidores já repetiram um desalinhamento do
|
|
117
|
+
cabeçalho. `PageHeader` e `PageShell` organizam páginas de recursos, não o topo de um portal
|
|
118
|
+
documental. Manter essas cópias deixaria o mesmo defeito com duas correções locais.
|
|
119
|
+
|
|
120
|
+
### Tabela para decisões e entregas
|
|
121
|
+
|
|
122
|
+
Foi implementada no protótipo com filtro, ordenação e paginação, mas rejeitada na avaliação visual.
|
|
123
|
+
ADRs precisam de contexto para escolher a leitura; changelog precisa manter a continuidade do texto.
|
|
124
|
+
A busca textual atende a consulta sem acrescentar controles de coleção.
|
|
125
|
+
|
|
126
|
+
### Ocultar a lateral e trocar o alcance da busca no catálogo
|
|
127
|
+
|
|
128
|
+
Também foi experimentado e substituído após avaliação. Ocultar a lateral interrompia o mapa;
|
|
129
|
+
trocar o alcance do campo superior tornava a pesquisa ambígua. A decisão vigente mantém a lateral
|
|
130
|
+
e separa a consulta documental da consulta local.
|
|
131
|
+
|
|
132
|
+
## Consequências
|
|
133
|
+
|
|
134
|
+
- Consumidores existentes preservam campo lateral, largura de `14rem` e composição não editorial.
|
|
135
|
+
- A biblioteca mantém uma árvore e uma regra de busca; o host mantém conteúdo, rotas e autorização.
|
|
136
|
+
- Os dois portais compartilham cabeçalho e catálogos sem compartilhar sua taxonomia específica.
|
|
137
|
+
- Guias, índices e fontes acessíveis por URL direta precisam ser filtrados antes de chegar ao
|
|
138
|
+
navegador quando houver restrição de acesso. Esta composição não é uma fronteira de segurança.
|
|
139
|
+
- A consolidação é um candidato local. A publicação e a adoção pela Grand Brasil são etapas
|
|
140
|
+
distintas, posteriores à revisão independente.
|
|
141
|
+
|
|
142
|
+
## Verificação
|
|
143
|
+
|
|
144
|
+
- Testes de `DocBrowser`: busca local e externa, vazio, largura configurável, lateral opcional,
|
|
145
|
+
persistência de grupos, expansão temporária, conteúdo do host, redirects e leitura editorial.
|
|
146
|
+
- Testes de `DocHeader`: slots, atributos nativos e composição com ou sem busca e navegação.
|
|
147
|
+
- Testes de `DocCatalog`: busca local controlada, independência do topo, ausência de controles de
|
|
148
|
+
coleção, texto integral das versões, incompatibilidades e abertura nativa em outra aba.
|
|
149
|
+
- Testes dos parsers: metadata ausente, resumo canônico, acentos e termos combinados, texto integral
|
|
150
|
+
e ausência de versões retroativas fabricadas.
|
|
151
|
+
- Inspeção dos dois portais: largura estreita, alinhamento do topo, lateral persistente, busca,
|
|
152
|
+
URLs diretas, ida ao registro e retorno ao catálogo.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# ADR 0058 — A mensagem da IA aceita imagem, e o Chat recebe anexos que o app guarda
|
|
2
|
+
|
|
3
|
+
- **Status:** aceita.
|
|
4
|
+
- **Data:** 2026-10-09.
|
|
5
|
+
- **Complementa:** a regra de crescimento por reincidência do `AiAdapter` (`core/types.ts`, nota do
|
|
6
|
+
contrato) e da entrada `@softize/opus/ai`; ADR 0050 (o `Markdown` não carrega imagem externa).
|
|
7
|
+
- **Origem:** ADR 0122 da plataforma Grand Brasil, "A imagem anexada à conversa com a IA fica
|
|
8
|
+
guardada com a conversa", que pede ao pacote o corte 1 do plano dela.
|
|
9
|
+
|
|
10
|
+
## Contexto
|
|
11
|
+
|
|
12
|
+
A plataforma Grand Brasil decidiu que a pessoa pode colar, soltar ou escolher uma imagem na conversa
|
|
13
|
+
com a IA, e que a IA a vê no mesmo turno. O caso que motivou a decisão é pedir ao assistente de
|
|
14
|
+
Relatórios que reproduza um relatório a partir de um print. É o primeiro caso real de entrada que não
|
|
15
|
+
é texto, e a superfície de IA do pacote cresce por reincidência: este é o caso que a cobra.
|
|
16
|
+
|
|
17
|
+
Hoje o pacote só conhece texto em três pontos:
|
|
18
|
+
|
|
19
|
+
- **O tipo da mensagem.** `AiMessage` é `{ role, content: string }`. `run` e `runStream` recebem
|
|
20
|
+
`string | AiMessage[]`.
|
|
21
|
+
- **O driver Anthropic.** Ele monta as mensagens com `content: m.content`, sem conferir a forma. Uma
|
|
22
|
+
lista de blocos no formato da Anthropic passaria por acaso, forçada com um `as`, e qualquer outro
|
|
23
|
+
provedor do consumidor receberia uma forma que não conhece.
|
|
24
|
+
- **A interface.** O `Composer` só habilita o envio com texto, e o `Chat` guarda `content: string` em
|
|
25
|
+
cada item. O modo controlado recebe o texto em `onSend(text)`.
|
|
26
|
+
|
|
27
|
+
A guarda da imagem não é do pacote. Na Grand Brasil, o motor de IA guarda os bytes junto com a
|
|
28
|
+
conversa, com retenção, governança e uma rota própria de envio. Outro consumidor pode guardar em
|
|
29
|
+
outro lugar, ou não guardar. O que se repete em qualquer consumidor é a forma de levar a imagem ao
|
|
30
|
+
modelo, a caixa de escrever com anexos e a preparação da imagem no navegador.
|
|
31
|
+
|
|
32
|
+
## Decisão
|
|
33
|
+
|
|
34
|
+
### 1. Conteúdo neutro na mensagem
|
|
35
|
+
|
|
36
|
+
O núcleo ganha um tipo de conteúdo que não tem a forma de nenhum provedor:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
type AiImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp'
|
|
40
|
+
|
|
41
|
+
type AiContentPart =
|
|
42
|
+
| { type: 'text'; text: string }
|
|
43
|
+
| { type: 'image'; mediaType: AiImageMediaType; data: string } // base64, sem o prefixo data:
|
|
44
|
+
|
|
45
|
+
interface AiMessage {
|
|
46
|
+
role: 'user' | 'assistant'
|
|
47
|
+
content: string | AiContentPart[]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- Os formatos são PNG, JPEG e WebP, os três que os provedores conhecidos aceitam. GIF fica de fora.
|
|
52
|
+
- A parte de imagem não leva nome. O rótulo que a pessoa e o modelo usam ("Imagem 1: print.png") é
|
|
53
|
+
uma parte de texto que o consumidor monta antes da imagem, como a Grand Brasil faz.
|
|
54
|
+
- `content: string` continua válido. Quem só manda texto não muda nada na escrita.
|
|
55
|
+
|
|
56
|
+
### 2. `aiMessageText` lê o texto de qualquer mensagem
|
|
57
|
+
|
|
58
|
+
`aiMessageText(message)` devolve o texto de uma mensagem: o próprio `content` quando ele é texto, ou
|
|
59
|
+
as partes de texto unidas por uma linha em branco. As imagens não entram. Serve para quem precisa só
|
|
60
|
+
do texto, como título de conversa, contagem de caracteres ou um bloco de contexto montado por
|
|
61
|
+
interpolação. Sai de `@softize/opus` e de `@softize/opus/ai`.
|
|
62
|
+
|
|
63
|
+
### 3. O driver Anthropic converte cada parte
|
|
64
|
+
|
|
65
|
+
- A parte `text` vira o bloco `text`, e a parte `image` vira o bloco `image` com
|
|
66
|
+
`source: { type: 'base64', media_type, data }`, em `run` e em `runStream`.
|
|
67
|
+
- Uma parte de tipo desconhecido, uma lista de partes vazia, uma parte de texto vazia, uma imagem num
|
|
68
|
+
formato fora da lista ou uma imagem numa mensagem `assistant` fazem o driver lançar erro antes de
|
|
69
|
+
qualquer chamada, dizendo qual mensagem e qual parte. Nunca há descarte silencioso.
|
|
70
|
+
- `complete` e `extract` continuam recebendo texto.
|
|
71
|
+
|
|
72
|
+
### 4. A guarda é do consumidor
|
|
73
|
+
|
|
74
|
+
O pacote não guarda imagem, não envia bytes a um servidor e não gera endereço. O `StorageAdapter`
|
|
75
|
+
existente não entra nesta decisão. O consumidor escolhe o transporte (a Grand Brasil manda a imagem
|
|
76
|
+
por uma rota própria, antes do turno) e entrega ao `Chat` o estado dos anexos e o endereço de cada
|
|
77
|
+
imagem. Assim, retenção, expiração, governança e dado pessoal ficam com quem guarda.
|
|
78
|
+
|
|
79
|
+
### 5. Composer e Chat recebem anexos controlados pelo app
|
|
80
|
+
|
|
81
|
+
- O `Composer` ganha a propriedade `attachments`, e o `Chat`, a `composerAttachments`, com a mesma
|
|
82
|
+
forma: os anexos pendentes (`id`, nome, estado `uploading`, `ready` ou `failed`, miniatura e motivo
|
|
83
|
+
da falha), `onAddFiles`, `onRemove`, o limite por mensagem (padrão 4), os formatos aceitos
|
|
84
|
+
(`readonly AiImageMediaType[]`, padrão PNG, JPEG e WebP, sem curinga: um formato que a mensagem
|
|
85
|
+
não leva não pode ser aceito na faixa) e `onReject` opcional. Sem essa propriedade, não há faixa, botão, colar nem soltar
|
|
86
|
+
de arquivos: o composer é o de sempre.
|
|
87
|
+
- Com a propriedade, o composer mostra a faixa de anexos acima do texto, com miniatura, nome, estado
|
|
88
|
+
e o botão de remover, e o botão **Anexar imagem** na barra. Colar uma imagem no campo, soltá-la
|
|
89
|
+
sobre a conversa ou escolhê-la pelo botão entregam os arquivos aceitos a `onAddFiles`. A imagem
|
|
90
|
+
colada recebe um nome gerado ("Imagem colada 1.png"), com um contador da instância que não volta
|
|
91
|
+
atrás, para que dois anexos da mesma conversa não tenham o mesmo nome. Colar texto continua colando texto, mesmo
|
|
92
|
+
quando a área de transferência também traz uma imagem, como acontece ao copiar células de uma
|
|
93
|
+
planilha.
|
|
94
|
+
- A triagem recusa o que não está na lista de tipos e o que passaria do limite. Sem `onReject`, o
|
|
95
|
+
composer explica a recusa logo acima do campo; com `onReject`, a explicação é do app.
|
|
96
|
+
- Uma região viva anuncia a imagem que fica pronta ou falha, porque a mudança na faixa é só visual.
|
|
97
|
+
Quando o botão de anexar fica desabilitado por atingir o limite, o foco volta ao campo.
|
|
98
|
+
- O arraste de arquivos que o `Chat` (ou o `Composer` sozinho) consome não sobe para os contêineres
|
|
99
|
+
do app, que podem ter o próprio soltar, como o envio de arquivos a um acervo.
|
|
100
|
+
- O envio passa com texto ou com pelo menos um anexo pronto. Ele espera os anexos em envio e não
|
|
101
|
+
passa com um anexo em falha: a pessoa remove o anexo, ou o app tenta de novo, para que nada do que
|
|
102
|
+
está na faixa fique de fora sem aviso.
|
|
103
|
+
- O modo controlado recebe `onSend(text, attachments)`, com os anexos prontos, quando há
|
|
104
|
+
`composerAttachments`. Sem eles, a chamada continua `onSend(text)`: um handler que já tinha um
|
|
105
|
+
segundo parâmetro opcional, com outro significado, não recebe uma lista inesperada. Limpar a faixa
|
|
106
|
+
depois do envio é do app, que é o dono do estado. Retornar `false` devolve o texto ao campo, e os
|
|
107
|
+
anexos continuam na faixa.
|
|
108
|
+
- O item `user` do transcript ganha `attachments`, com `id`, nome, tipo, dimensões, estado
|
|
109
|
+
(`available` ou `expired`) e o endereço da imagem fornecido pelo app. A fala da pessoa mostra as
|
|
110
|
+
miniaturas acima do texto, com o nome como texto alternativo. O anexo expirado aparece como um
|
|
111
|
+
cartão com o nome e "Imagem expirada". Uma fala só com imagem não mostra balão de texto vazio.
|
|
112
|
+
- As miniaturas são um componente próprio, e não `![]()` no Markdown da mensagem: a fala da pessoa
|
|
113
|
+
continua sendo o que ela escreveu. O endereço vem do app, que deve servi-lo pela própria origem ou
|
|
114
|
+
como `data:`, como a ADR 0050 já recomenda.
|
|
115
|
+
- O modo autogerenciado do `Chat` (`send(messages)`) não ganha anexo. Ele não tem transporte para a
|
|
116
|
+
imagem, e `ChatMessage` continua só com texto. Se um consumidor precisar, uma nova decisão define
|
|
117
|
+
como.
|
|
118
|
+
|
|
119
|
+
### 6. Preparação da imagem no navegador
|
|
120
|
+
|
|
121
|
+
`prepareImage(file, options)` prepara a imagem antes do envio, com os tetos que o app passa:
|
|
122
|
+
|
|
123
|
+
- lê só o cabeçalho do arquivo, sem carregá-lo inteiro, para conferir o formato pelos primeiros
|
|
124
|
+
bytes, não pela extensão nem pelo tipo declarado, e recusa o que não está em `accept` (padrão PNG,
|
|
125
|
+
JPEG e WebP);
|
|
126
|
+
- recusa PNG e WebP animados, que o modelo leria só pelo primeiro quadro;
|
|
127
|
+
- lê largura e altura do cabeçalho e recusa, antes de decodificar, a imagem acima de `maxPixels`
|
|
128
|
+
(padrão 40 megapixels): decodificá-la ocuparia centenas de megabytes de memória do navegador;
|
|
129
|
+
- reduz para que o lado maior não passe de `maxEdge` (padrão 1568 px), mantendo a proporção e a
|
|
130
|
+
orientação gravada pela câmera (EXIF), e codifica de novo sempre, o que descarta metadados como a
|
|
131
|
+
localização;
|
|
132
|
+
- mantém PNG como PNG quando cabe em `maxBytes` (padrão 3 MB) e converte o resto para JPEG com
|
|
133
|
+
qualidade 0,85, sobre fundo branco; o que continua acima do teto é recusado;
|
|
134
|
+
- devolve o arquivo preparado com tipo, dimensões e tamanho, ou a recusa com o motivo e uma mensagem
|
|
135
|
+
em pt-BR para a pessoa.
|
|
136
|
+
|
|
137
|
+
Os padrões repetem os tetos da ADR 0122 da Grand Brasil, mas o consumidor deve passar os seus: o
|
|
138
|
+
utilitário não decide teto de produto.
|
|
139
|
+
|
|
140
|
+
## Por que é major
|
|
141
|
+
|
|
142
|
+
`AiMessage.content` passa de `string` para `string | AiContentPart[]`. Quem lê `content` como texto
|
|
143
|
+
deixa de compilar, e é de propósito: o compilador aponta os consumidores e os drivers que usam o
|
|
144
|
+
conteúdo como texto, em vez de deixar a imagem sumir em silêncio. A migração é ler o texto com
|
|
145
|
+
`aiMessageText(message)`.
|
|
146
|
+
|
|
147
|
+
O compilador não aponta o texto montado por interpolação (`${message.content}`) nem o que passa por um
|
|
148
|
+
`as`. Com uma lista de partes, esses pontos produziriam "[object Object]" sem erro. Dentro do pacote
|
|
149
|
+
não há nenhum; nos consumidores, a migração precisa procurá-los à parte e cobri-los com teste.
|
|
150
|
+
|
|
151
|
+
As mudanças de `Composer` e `Chat` preservam o comportamento de quem não usa anexo: sem as
|
|
152
|
+
propriedades novas, a interface é a mesma, e `onSend` continua sendo chamado só com o texto. O tipo de
|
|
153
|
+
`onSend` ganha o segundo parâmetro opcional; um handler que já declarava um segundo parâmetro
|
|
154
|
+
opcional de outro tipo deixa de compilar e precisa ser envolvido, como `(text) => run(text)`.
|
|
155
|
+
|
|
156
|
+
## Consequências
|
|
157
|
+
|
|
158
|
+
- Um driver de IA do consumidor que repassava `content` sem conferir passa a receber partes neutras
|
|
159
|
+
quando o consumidor manda imagem. Cada driver converte as partes, ou recusa com erro claro.
|
|
160
|
+
- O pacote cresce com a faixa de anexos e a preparação da imagem, usadas por um consumidor só por
|
|
161
|
+
enquanto. Ficam no pacote porque o `Chat` e o `Composer` são dele, e cada consumidor reconstruiria
|
|
162
|
+
a sua por cima de `composerActions`.
|
|
163
|
+
- A redução no navegador limita tamanho e custo, mas não garante que o provedor não reduza de novo:
|
|
164
|
+
no nível padrão do Claude, uma imagem retangular de 1568 px ainda passa por redução na API.
|
|
165
|
+
- O tipo e o estado de cada anexo continuam como texto no pacote. A plataforma que adota declara o
|
|
166
|
+
próprio vocabulário (na Grand Brasil, um `t.dict` no protocolo da IA).
|
|
167
|
+
|
|
168
|
+
## Alternativas descartadas
|
|
169
|
+
|
|
170
|
+
- **Campo opcional só para imagens em `AiMessage`.** Não quebraria ninguém, mas um driver que não o
|
|
171
|
+
conhece descartaria a imagem sem aviso, e o compilador não apontaria nada.
|
|
172
|
+
- **Partes no formato da Anthropic.** Funcionaria só com esse driver. Os demais provedores, que a
|
|
173
|
+
Grand Brasil também usa, receberiam uma forma que não conhecem.
|
|
174
|
+
- **Nome na parte de imagem.** A API da Anthropic não tem onde pôr o nome, e o rótulo em texto antes
|
|
175
|
+
da imagem já permite que a pessoa e o modelo falem de "imagem 2".
|
|
176
|
+
- **O `Composer` envia e guarda a imagem.** Prenderia o pacote a um transporte e a uma guarda que
|
|
177
|
+
são do consumidor, com retenção e governança próprias.
|
|
178
|
+
- **Anexo no modo autogerenciado.** Exigiria levar os bytes no histórico enviado a `send`, que é o
|
|
179
|
+
que a Grand Brasil recusou para o corpo do chat. Fica para quando um consumidor trouxer o caso.
|
|
180
|
+
- **Aceitar a imagem animada e mandar só o primeiro quadro.** A pessoa acharia que o modelo viu a
|
|
181
|
+
animação; recusar com o motivo é mais honesto.
|
|
182
|
+
|
|
183
|
+
## Verificação
|
|
184
|
+
|
|
185
|
+
- Teste do driver: uma mensagem com texto e imagem chega ao client falso como bloco `text` e bloco
|
|
186
|
+
`image` com `source.type: 'base64'`, no `run` e no `runStream`; uma parte desconhecida, uma lista
|
|
187
|
+
vazia, uma parte de texto vazia, uma imagem num formato fora da lista e uma imagem em mensagem
|
|
188
|
+
`assistant` lançam erro sem chamar o client.
|
|
189
|
+
- Teste de `aiMessageText` com texto puro e com partes.
|
|
190
|
+
- Testes do `Composer` e do `Chat`: colar, soltar e escolher entregam o arquivo a `onAddFiles`;
|
|
191
|
+
colar texto continua colando; o envio passa só com imagem pronta e não passa com imagem em envio
|
|
192
|
+
ou em falha; tipo e limite são recusados com o motivo; o `Chat` mostra as miniaturas e o cartão de
|
|
193
|
+
imagem expirada, entrega `onSend(text, attachments)` e, sem `composerAttachments` ou no modo
|
|
194
|
+
autogerenciado, não mostra a faixa; o nome da imagem colada não se repete depois de remover uma;
|
|
195
|
+
a região viva anuncia pronta e falha; o foco volta ao campo no limite; o soltar consumido não
|
|
196
|
+
chega ao contêiner do app.
|
|
197
|
+
- Testes de `prepareImage` com canvas simulado e em navegador real: uma imagem de 4000 px sai com
|
|
198
|
+
lado maior de 1568 px, o PNG que cabe continua PNG, o que passa vira JPEG, e GIF, SVG e imagem
|
|
199
|
+
animada são recusados com o motivo; as dimensões vêm do cabeçalho de PNG, JPEG e WebP (com e sem
|
|
200
|
+
perdas e estendido), sem ler o arquivo inteiro, e a imagem acima do teto de pixels é recusada sem
|
|
201
|
+
decodificar; um JPEG com EXIF de rotação sai em pé.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Decisões arquiteturais
|
|
3
|
+
order: -1
|
|
4
|
+
---
|
|
5
|
+
# Decisões arquiteturais
|
|
6
|
+
|
|
7
|
+
As ADRs registram o contexto, as alternativas e as consequências das decisões estruturais do
|
|
8
|
+
Opus. Use a Documentação para compreender o contrato vigente; consulte esta área quando precisar
|
|
9
|
+
entender por que uma restrição existe ou como ela evoluiu.
|
|
10
|
+
|
|
11
|
+
Uma ADR preserva seu contexto histórico mesmo quando é substituída. O comportamento vigente deve
|
|
12
|
+
estar documentado na superfície correspondente, sem exigir a reconstrução dessa sequência.
|
package/package.json
CHANGED
|
@@ -17,6 +17,7 @@ import { declaredJsonSchema } from '../../core/index.ts'
|
|
|
17
17
|
import type {
|
|
18
18
|
AiAdapter,
|
|
19
19
|
AiCompleteOptions,
|
|
20
|
+
AiContentPart,
|
|
20
21
|
AiMessage,
|
|
21
22
|
AiRunOptions,
|
|
22
23
|
AiRunResult,
|
|
@@ -101,6 +102,50 @@ function defaultToJsonSchema(schema: unknown): unknown {
|
|
|
101
102
|
return zodToJsonSchema(schema as Parameters<typeof zodToJsonSchema>[0], { $refStrategy: 'none' })
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
const IMAGE_MEDIA_TYPES: ReadonlySet<string> = new Set(['image/png', 'image/jpeg', 'image/webp'])
|
|
106
|
+
|
|
107
|
+
/** Converte o conteúdo neutro da mensagem (ADR 0058) para os blocos da API da Anthropic. Uma
|
|
108
|
+
* parte desconhecida, uma parte de texto vazia, uma lista vazia, uma imagem fora dos formatos
|
|
109
|
+
* aceitos ou uma imagem na fala do assistente lançam erro: nada é descartado em silêncio, e nada
|
|
110
|
+
* chega a ser enviado. */
|
|
111
|
+
function toAnthropicContent(message: AiMessage, index: number): unknown {
|
|
112
|
+
const { content } = message
|
|
113
|
+
if (typeof content === 'string') return content
|
|
114
|
+
// A API recusa lista vazia e bloco de texto sem texto; o erro aqui diz qual mensagem é.
|
|
115
|
+
if (content.length === 0) {
|
|
116
|
+
throw new Error(`ai/anthropic: mensagem ${index + 1} sem conteúdo (lista de partes vazia).`)
|
|
117
|
+
}
|
|
118
|
+
return content.map((part: AiContentPart, partIndex) => {
|
|
119
|
+
const where = `mensagem ${index + 1}, parte ${partIndex + 1}`
|
|
120
|
+
if (part.type === 'text') {
|
|
121
|
+
if (part.text.trim() === '') {
|
|
122
|
+
throw new Error(`ai/anthropic: parte de texto vazia (${where}).`)
|
|
123
|
+
}
|
|
124
|
+
return { type: 'text', text: part.text }
|
|
125
|
+
}
|
|
126
|
+
if (part.type === 'image') {
|
|
127
|
+
if (message.role !== 'user') {
|
|
128
|
+
throw new Error(`ai/anthropic: imagem só pode ir numa mensagem 'user' (${where}).`)
|
|
129
|
+
}
|
|
130
|
+
if (!IMAGE_MEDIA_TYPES.has(part.mediaType)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`ai/anthropic: formato de imagem não aceito: "${String(part.mediaType)}" (${where}). ` +
|
|
133
|
+
'Use image/png, image/jpeg ou image/webp.',
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
return { type: 'image', source: { type: 'base64', media_type: part.mediaType, data: part.data } }
|
|
137
|
+
}
|
|
138
|
+
const unknownType = (part as { type?: unknown }).type
|
|
139
|
+
throw new Error(`ai/anthropic: parte de conteúdo desconhecida: "${String(unknownType)}" (${where}).`)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** O histórico de entrada no formato da API, convertido antes da primeira chamada. */
|
|
144
|
+
function toAnthropicMessages(input: string | AiMessage[]): Array<{ role: string; content: unknown }> {
|
|
145
|
+
if (typeof input === 'string') return [{ role: 'user', content: input }]
|
|
146
|
+
return input.map((m, i) => ({ role: m.role, content: toAnthropicContent(m, i) }))
|
|
147
|
+
}
|
|
148
|
+
|
|
104
149
|
interface ContentBlock {
|
|
105
150
|
type: string
|
|
106
151
|
text?: string
|
|
@@ -183,6 +228,9 @@ export function anthropicAi(options: AnthropicAiOptions = {}): AiAdapter {
|
|
|
183
228
|
},
|
|
184
229
|
|
|
185
230
|
async run(input: string | AiMessage[], opts: AiRunOptions): Promise<AiRunResult> {
|
|
231
|
+
// Mensagens no formato Anthropic — convertidas antes de tudo (uma parte inválida falha sem
|
|
232
|
+
// chamar a API) e crescem com tool_use/tool_result a cada passo.
|
|
233
|
+
const messages = toAnthropicMessages(input)
|
|
186
234
|
const c = await client()
|
|
187
235
|
const tools = withAskTool(
|
|
188
236
|
opts.tools.map((t) => ({
|
|
@@ -192,11 +240,6 @@ export function anthropicAi(options: AnthropicAiOptions = {}): AiAdapter {
|
|
|
192
240
|
})),
|
|
193
241
|
opts,
|
|
194
242
|
)
|
|
195
|
-
// Mensagens no formato Anthropic — crescem com tool_use/tool_result a cada passo.
|
|
196
|
-
const messages: Array<{ role: string; content: unknown }> =
|
|
197
|
-
typeof input === 'string'
|
|
198
|
-
? [{ role: 'user', content: input }]
|
|
199
|
-
: input.map((m) => ({ role: m.role, content: m.content }))
|
|
200
243
|
|
|
201
244
|
const maxSteps = opts.maxSteps ?? 8
|
|
202
245
|
const calls: Array<{ name: string; input: unknown }> = []
|
|
@@ -242,6 +285,7 @@ export function anthropicAi(options: AnthropicAiOptions = {}): AiAdapter {
|
|
|
242
285
|
},
|
|
243
286
|
|
|
244
287
|
async *runStream(input: string | AiMessage[], opts: AiRunOptions): AsyncIterable<ChatEvent> {
|
|
288
|
+
const messages = toAnthropicMessages(input)
|
|
245
289
|
const c = await client()
|
|
246
290
|
const tools = withAskTool(
|
|
247
291
|
opts.tools.map((t) => ({
|
|
@@ -251,10 +295,6 @@ export function anthropicAi(options: AnthropicAiOptions = {}): AiAdapter {
|
|
|
251
295
|
})),
|
|
252
296
|
opts,
|
|
253
297
|
)
|
|
254
|
-
const messages: Array<{ role: string; content: unknown }> =
|
|
255
|
-
typeof input === 'string'
|
|
256
|
-
? [{ role: 'user', content: input }]
|
|
257
|
-
: input.map((m) => ({ role: m.role, content: m.content }))
|
|
258
298
|
|
|
259
299
|
const maxSteps = opts.maxSteps ?? 8
|
|
260
300
|
|
package/src/ai/index.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* (texto → objeto VALIDADO por um `Schema` do protocolo — o mesmo tipo de schema
|
|
6
6
|
* das actions vira o contrato da resposta do modelo). Driver em `ai/anthropic`.
|
|
7
7
|
* A superfície é a mínima de backoffice e cresce por reincidência — não por
|
|
8
|
-
* especulação
|
|
9
|
-
* real
|
|
8
|
+
* especulação. Streaming, chat multi-turn e mensagem com imagem (ADR 0058) já
|
|
9
|
+
* entraram assim; o resto espera um caso real.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export type { AiAdapter, AiCompleteOptions } from '../core/index.ts'
|
|
13
|
+
// Mensagem com texto e imagem (ADR 0058): o tipo neutro de conteúdo e a leitura só do texto.
|
|
14
|
+
export type { AiContentPart, AiImageMediaType, AiMessage } from '../core/index.ts'
|
|
15
|
+
export { aiMessageText } from '../core/index.ts'
|
|
13
16
|
// Elicitação — perguntar à pessoa no meio do turno (round-trip via `AiRunOptions.onAsk`).
|
|
14
17
|
// O contrato (tipos) mora no core; aqui saem o nome reservado, o schema/descrição da tool e
|
|
15
18
|
// o formatador. É o ponto de consistência pra qualquer consumidor (chat, copilot, MCP externo).
|