@softize/opus 18.5.0 → 20.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 +275 -0
- package/bin/cli.mjs +5 -0
- package/bin/lib/check.mjs +198 -10
- package/bin/lib/copy.mjs +29 -0
- package/bin/lib/mcp.mjs +1 -1
- package/docs/adr/0023-surfaces-share-one-anatomy-and-two-layouts.md +298 -0
- package/docs/adr/0024-fill-height-propagates-by-context.md +118 -0
- package/docs/data-products.md +15 -9
- package/docs/protocol.md +3 -4
- package/docs/releasing.md +2 -2
- package/package.json +2 -2
- package/registry/skills/create-opus-action/scripts/scaffold.mjs +2 -1
- package/registry/templates/app/package.json +1 -1
- package/src/core/runtime.ts +96 -8
- package/src/core/types.ts +5 -2
- package/src/ui/components/patterns/action-form-card.tsx +11 -1
- package/src/ui/components/patterns/content-header.tsx +16 -12
- package/src/ui/components/patterns/fill-region.ts +150 -0
- package/src/ui/components/patterns/heading-level.ts +16 -0
- package/src/ui/components/patterns/list.tsx +103 -73
- package/src/ui/components/patterns/page.tsx +20 -4
- package/src/ui/components/patterns/pane.tsx +294 -0
- package/src/ui/components/patterns/presentation.tsx +81 -54
- package/src/ui/components/patterns/sidebar.tsx +13 -72
- package/src/ui/components/patterns/split.tsx +51 -38
- package/src/ui/components/patterns/surface-anatomy.ts +246 -0
- package/src/ui/components/patterns/surface-header.tsx +51 -6
- package/src/ui/components/patterns/surface-media.tsx +69 -0
- package/src/ui/components/patterns/trigger.tsx +12 -2
- package/src/ui/components/primitives/alert.tsx +9 -1
- package/src/ui/components/primitives/avatar.tsx +7 -3
- package/src/ui/components/primitives/card.tsx +91 -29
- package/src/ui/components/primitives/carousel.tsx +34 -5
- package/src/ui/components/primitives/checkbox.tsx +11 -3
- package/src/ui/components/primitives/command.tsx +14 -5
- package/src/ui/components/primitives/copyable.tsx +120 -10
- package/src/ui/components/primitives/detail.tsx +10 -13
- package/src/ui/components/primitives/dialog.tsx +179 -45
- package/src/ui/components/primitives/drawer.tsx +131 -24
- package/src/ui/components/primitives/empty.tsx +1 -1
- package/src/ui/components/primitives/icon-picker.tsx +6 -2
- package/src/ui/components/primitives/icon-tile.tsx +7 -0
- package/src/ui/components/primitives/input-group.tsx +9 -2
- package/src/ui/components/primitives/input-otp.tsx +1 -1
- package/src/ui/components/primitives/kbd.tsx +1 -1
- package/src/ui/components/primitives/label.tsx +3 -0
- package/src/ui/components/primitives/menu.tsx +5 -0
- package/src/ui/components/primitives/metric-card.tsx +98 -39
- package/src/ui/components/primitives/popover.tsx +1 -1
- package/src/ui/components/primitives/progress.tsx +9 -1
- package/src/ui/components/primitives/slider.tsx +62 -11
- package/src/ui/components/primitives/truncate.tsx +10 -1
- package/src/ui/docs/content/accordion.md +74 -27
- package/src/ui/docs/content/action-form-card.md +24 -15
- package/src/ui/docs/content/action-form-dialog.md +17 -7
- package/src/ui/docs/content/action-form.md +92 -77
- package/src/ui/docs/content/action-list-dialog.md +75 -38
- package/src/ui/docs/content/action-list.md +134 -52
- package/src/ui/docs/content/action-trigger.md +27 -10
- package/src/ui/docs/content/action-view.md +14 -14
- package/src/ui/docs/content/actions.md +161 -66
- package/src/ui/docs/content/ai.md +43 -44
- package/src/ui/docs/content/alert.md +83 -29
- package/src/ui/docs/content/ask.md +51 -26
- package/src/ui/docs/content/aspect-ratio.md +22 -24
- package/src/ui/docs/content/audit.md +23 -19
- package/src/ui/docs/content/auth.md +38 -19
- package/src/ui/docs/content/avatar.md +54 -5
- package/src/ui/docs/content/badge.md +29 -9
- package/src/ui/docs/content/breadcrumb.md +21 -8
- package/src/ui/docs/content/button.md +73 -43
- package/src/ui/docs/content/calendar.md +134 -26
- package/src/ui/docs/content/card.md +252 -35
- package/src/ui/docs/content/carousel.md +37 -6
- package/src/ui/docs/content/chat.md +118 -37
- package/src/ui/docs/content/checkbox.md +73 -7
- package/src/ui/docs/content/cli.md +94 -55
- package/src/ui/docs/content/collapsible.md +43 -19
- package/src/ui/docs/content/command.md +30 -8
- package/src/ui/docs/content/composer.md +23 -8
- package/src/ui/docs/content/content.md +37 -7
- package/src/ui/docs/content/copyable.md +39 -8
- package/src/ui/docs/content/customization.md +55 -46
- package/src/ui/docs/content/cycle.md +33 -18
- package/src/ui/docs/content/data-products.md +115 -0
- package/src/ui/docs/content/data-state.md +20 -6
- package/src/ui/docs/content/data.md +20 -19
- package/src/ui/docs/content/detail.md +12 -5
- package/src/ui/docs/content/dialog.md +219 -28
- package/src/ui/docs/content/dictionary-value.md +4 -4
- package/src/ui/docs/content/dock.md +54 -19
- package/src/ui/docs/content/dot.md +35 -5
- package/src/ui/docs/content/drawer.md +193 -22
- package/src/ui/docs/content/empty-value.md +20 -6
- package/src/ui/docs/content/empty.md +24 -3
- package/src/ui/docs/content/events.md +76 -16
- package/src/ui/docs/content/executions.md +9 -9
- package/src/ui/docs/content/field.md +21 -6
- package/src/ui/docs/content/getting-started.md +44 -34
- package/src/ui/docs/content/icon-picker.md +19 -13
- package/src/ui/docs/content/icon-tile.md +28 -5
- package/src/ui/docs/content/input-otp.md +58 -7
- package/src/ui/docs/content/input.md +22 -3
- package/src/ui/docs/content/item.md +53 -6
- package/src/ui/docs/content/kbd.md +5 -1
- package/src/ui/docs/content/label.md +13 -4
- package/src/ui/docs/content/log.md +5 -5
- package/src/ui/docs/content/markdown.md +5 -4
- package/src/ui/docs/content/mcp.md +49 -23
- package/src/ui/docs/content/menu.md +95 -10
- package/src/ui/docs/content/metric-card.md +68 -21
- package/src/ui/docs/content/observability.md +10 -9
- package/src/ui/docs/content/page.md +125 -11
- package/src/ui/docs/content/pagination.md +13 -5
- package/src/ui/docs/content/pane.md +300 -0
- package/src/ui/docs/content/popover.md +42 -5
- package/src/ui/docs/content/presentation.md +91 -47
- package/src/ui/docs/content/progress.md +27 -14
- package/src/ui/docs/content/queue.md +39 -21
- package/src/ui/docs/content/radio-group.md +26 -4
- package/src/ui/docs/content/router.md +29 -10
- package/src/ui/docs/content/runtime.md +90 -54
- package/src/ui/docs/content/scheduler.md +53 -21
- package/src/ui/docs/content/scroll-area.md +42 -35
- package/src/ui/docs/content/select.md +78 -33
- package/src/ui/docs/content/semantic-context.md +21 -2
- package/src/ui/docs/content/separator.md +15 -9
- package/src/ui/docs/content/sidebar.md +25 -45
- package/src/ui/docs/content/skeleton.md +13 -4
- package/src/ui/docs/content/slider.md +37 -9
- package/src/ui/docs/content/spinner.md +4 -3
- package/src/ui/docs/content/split.md +34 -20
- package/src/ui/docs/content/storage.md +8 -9
- package/src/ui/docs/content/switch.md +10 -7
- package/src/ui/docs/content/table.md +46 -8
- package/src/ui/docs/content/tabs.md +14 -4
- package/src/ui/docs/content/testing.md +76 -31
- package/src/ui/docs/content/textarea.md +5 -4
- package/src/ui/docs/content/toast.md +48 -7
- package/src/ui/docs/content/toggle.md +3 -3
- package/src/ui/docs/content/tokens.md +15 -26
- package/src/ui/docs/content/tooltip.md +18 -2
- package/src/ui/docs/content/truncate.md +36 -6
- package/src/ui/docs/content/ui.md +74 -22
- package/src/ui/docs/content/upgrading.md +45 -36
- package/src/ui/docs/plugin.ts +36 -2
- package/src/ui/docs/registry.tsx +14 -2
- package/src/ui/drivers/react.tsx +16 -8
- package/src/ui/meta.ts +38 -32
- package/src/ui/react.tsx +14 -0
- package/src/vite/design.ts +16 -4
|
@@ -5,18 +5,19 @@ title: Actions & contratos
|
|
|
5
5
|
# Actions & contratos
|
|
6
6
|
|
|
7
7
|
Uma action descreve uma operação que cliente e servidor precisam compreender da mesma forma.
|
|
8
|
-
Seu contrato reúne
|
|
9
|
-
o handler e a
|
|
8
|
+
Seu contrato reúne entrada, saída, documentação e regras de acesso num módulo compartilhado. O
|
|
9
|
+
servidor liga o handler a esse contrato, e a interface usa a mesma definição para executar ou
|
|
10
|
+
renderizar a operação.
|
|
10
11
|
|
|
11
12
|
## Contrato primeiro
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
O contrato fica no código que roda nos dois lados. Ele declara identidade, schemas (zod e os tipos
|
|
15
|
+
lógicos `t.*`) e metadados, sem depender de banco, segredo ou driver de servidor. `api` e `web`
|
|
16
|
+
importam o mesmo objeto.
|
|
15
17
|
|
|
16
18
|
```ts
|
|
17
|
-
// shared/ — roda
|
|
18
|
-
import { defineContract } from '@softize/opus'
|
|
19
|
-
import { t } from '@softize/opus/schema/zod'
|
|
19
|
+
// shared/ — roda no cliente e no servidor.
|
|
20
|
+
import { defineContract } from '@softize/opus/core'
|
|
20
21
|
import { z } from 'zod'
|
|
21
22
|
|
|
22
23
|
export const workspaceListContract = defineContract({
|
|
@@ -25,21 +26,24 @@ export const workspaceListContract = defineContract({
|
|
|
25
26
|
summary: 'Lista os workspaces do back-office',
|
|
26
27
|
description: 'O workspace é a entidade raiz que agrupa repositórios, apps e agentes…',
|
|
27
28
|
input: z.object({ q: z.string().optional() }),
|
|
28
|
-
output: workspaceRowSchema,
|
|
29
|
+
output: workspaceRowSchema, // em `list`, o schema de cada item
|
|
29
30
|
paginate: 'cursor',
|
|
30
31
|
})
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
O nome segue `<recurso>.<verbo>` e o `kind` é `simple`, `form`, `list` ou `view`. Cada action fica
|
|
35
|
+
em um arquivo próprio, em `src/domains/<recurso>/actions/`.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
## Ligar o handler no servidor
|
|
38
|
+
|
|
39
|
+
`bindAction` junta ao contrato a parte que só existe no servidor e produz a action executável. O
|
|
40
|
+
contrato continua importável pela web sem arrastar o acesso ao banco.
|
|
37
41
|
|
|
38
42
|
```ts
|
|
39
|
-
// api/ —
|
|
43
|
+
// api/ — somente no servidor.
|
|
40
44
|
import { randomUUID } from 'node:crypto'
|
|
41
45
|
import { bindAction } from '@softize/opus/core'
|
|
42
|
-
import { workspaceCreateContract } from '@app/shared' // contrato `form`, declarado
|
|
46
|
+
import { workspaceCreateContract } from '@app/shared' // contrato `form`, declarado como o acima
|
|
43
47
|
|
|
44
48
|
export const workspaceCreate = bindAction(workspaceCreateContract, {
|
|
45
49
|
handler: async (ctx, input) => {
|
|
@@ -50,42 +54,120 @@ export const workspaceCreate = bindAction(workspaceCreateContract, {
|
|
|
50
54
|
})
|
|
51
55
|
```
|
|
52
56
|
|
|
57
|
+
O binding aceita `handler`, `loads`, `authorize`, `background`, `emits` e `idempotency`. Exceto
|
|
58
|
+
`authorize`, esses campos não existem no contrato: `defineContract` recusa qualquer um deles pelo
|
|
59
|
+
tipo. O `authorize` pode aparecer nos dois lados; o declarado no binding substitui o do contrato. O handler recebe a entrada já validada pelo schema, com os
|
|
60
|
+
valores padrão aplicados.
|
|
61
|
+
|
|
62
|
+
[`background`](/docs/queue) e [`emits`](/docs/events) só têm efeito em actions `simple` e `form`.
|
|
63
|
+
O tipo do binding os aceita em `list` e `view`, mas ali o runtime os ignora: a action executa na
|
|
64
|
+
própria requisição e a lista de eventos não entra no manifest. Fora de produção, o runtime avisa ao
|
|
65
|
+
registrar a action (`runtime.action_field_ignored`), e o [`opus check`](/docs/cli) aponta a regra
|
|
66
|
+
`ignored-by-kind`.
|
|
67
|
+
|
|
68
|
+
## Carregar dados antes de autorizar
|
|
69
|
+
|
|
70
|
+
Quando a permissão depende do registro, como “só quem criou a tarefa pode editá-la”, declare em
|
|
71
|
+
`loads` o que precisa ser buscado. O runtime carrega esses dados antes do `authorize` e os entrega
|
|
72
|
+
também ao handler.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
export const taskUpdate = bindAction(taskUpdateContract, {
|
|
76
|
+
loads: { task: 'task(:id)' }, // busca a tarefa pelo `id` do input
|
|
77
|
+
authorize: 'task.ownerId == user.id', // expressão avaliada com user, input e o que foi carregado
|
|
78
|
+
handler: async (ctx, input, loaded) => {
|
|
79
|
+
// `loaded.task` já passou pela autorização.
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Na montagem do runtime: como resolver cada entidade citada em `loads`.
|
|
84
|
+
runtime.registerLoaders({
|
|
85
|
+
task: (args, ctx) => findTask(ctx.db, String(args.id)),
|
|
86
|
+
})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Uma entidade sem resolver registrado falha com `runtime.loader_not_registered`. Quando o resolver
|
|
90
|
+
devolve `null` ou `undefined`, a action responde `<action>.<chave>.not_found`. `loads` também aceita
|
|
91
|
+
uma função `(ctx, input) => Promise<unknown>` no lugar da expressão. A ordem completa das
|
|
92
|
+
verificações está em [Autenticação](/docs/auth).
|
|
93
|
+
|
|
94
|
+
## Registrar em um domínio
|
|
95
|
+
|
|
96
|
+
Uma action só existe para o runtime depois de registrada. `defineDomain` agrupa as actions,
|
|
97
|
+
reactions, schedules, entidades e [Produtos de Dados](/docs/data-products) de um mesmo recorte
|
|
98
|
+
funcional:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// src/domains/workspace/index.ts
|
|
102
|
+
import { defineDomain } from '@softize/opus/core'
|
|
103
|
+
import { workspaceCreate } from './actions/create.ts'
|
|
104
|
+
import { workspaceList } from './actions/list.ts'
|
|
105
|
+
|
|
106
|
+
export const workspaceDomain = defineDomain({
|
|
107
|
+
name: 'workspace',
|
|
108
|
+
description: 'Workspaces do back-office e o que eles agrupam.',
|
|
109
|
+
actions: { workspaceCreate, workspaceList },
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Registre o domínio no runtime com `runtime.register([workspaceDomain])` e liste-o em `domains` no
|
|
114
|
+
`opus.config.ts`, que é de onde `opus gen` projeta o manifest.
|
|
115
|
+
|
|
116
|
+
`defineDomain` valida a declaração no momento em que roda: o `name` segue `[a-z][a-zA-Z0-9_]*`,
|
|
117
|
+
duas actions do domínio (subdomínios incluídos) não podem ter o mesmo nome e cada Produto de Dados
|
|
118
|
+
precisa citar actions e entidades existentes. Ao registrar, o runtime avisa no console quando o
|
|
119
|
+
nome de uma action não começa pelo caminho do domínio, como `workspace.` ou `workspace.member.`.
|
|
120
|
+
`subdomains` aceita outros domínios e forma esse caminho.
|
|
121
|
+
|
|
122
|
+
`actions` aceita um mapa, uma lista ou o resultado de `import * as actions`. Um domínio também pode
|
|
123
|
+
registrar só o contrato, sem handler: no modo design, descrito abaixo, o `mockHandler` responde;
|
|
124
|
+
fora dele, a execução falha por falta de handler.
|
|
125
|
+
|
|
53
126
|
## Ordem canônica dos campos
|
|
54
127
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
128
|
+
Manter os campos na mesma ordem torna contratos previsíveis para leitura e geração. O `opus check`
|
|
129
|
+
aponta o primeiro campo fora de ordem, e a skill `$create-opus-action` já cria contrato e binding
|
|
130
|
+
nessa estrutura. Os grupos seguem esta sequência; dentro de cada grupo, a ordem é livre:
|
|
58
131
|
|
|
59
132
|
```text
|
|
60
|
-
name
|
|
61
|
-
→
|
|
62
|
-
→
|
|
63
|
-
→
|
|
64
|
-
→ handler
|
|
65
|
-
→ invalidates
|
|
133
|
+
name, kind
|
|
134
|
+
→ label, title, summary, description, icon, color, messages, tags, examples, errors
|
|
135
|
+
→ input, output
|
|
136
|
+
→ public, requires, authorize, loads
|
|
137
|
+
→ handler, mockHandler
|
|
138
|
+
→ audit, confirm, invalidates, rateLimit, idempotency
|
|
139
|
+
→ ai, automatable, internal
|
|
66
140
|
```
|
|
67
141
|
|
|
68
|
-
|
|
69
|
-
|
|
142
|
+
Campos específicos de um kind, como `fields`, `paginate`, `columns`, `filters`, `expand`,
|
|
143
|
+
`background` e `emits`, ficam fora dessa verificação.
|
|
70
144
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
145
|
+
Declare `input` e `output` com `z.object` e use os tipos lógicos do catálogo em vez de reescrever
|
|
146
|
+
validações: `z.object({ tag: t.slug().zod(), contato: t.email().zod() })`. O `.zod()` devolve o
|
|
147
|
+
schema zod de cada tipo lógico (slug, email, phone, money…), importado de `@softize/opus/schema/zod`.
|
|
74
148
|
|
|
75
|
-
|
|
149
|
+
A autorização é fechada por padrão: sem `public: true`, a action exige usuário autenticado; com
|
|
150
|
+
`authorize`, exige também a permissão declarada.
|
|
76
151
|
|
|
77
|
-
|
|
78
|
-
`OPUS_MODE=design` o runtime roteia o `execute` para o `mockHandler` (cai no `handler` real se
|
|
79
|
-
ausente). Como ele fica no contrato, e não no `bindAction`, é isomorfo — o design autora, o
|
|
80
|
-
handoff pluga o handler, e o mock nem toca `ctx.db`.
|
|
152
|
+
## Prototipar com dados simulados
|
|
81
153
|
|
|
82
|
-
|
|
154
|
+
Para desenhar a interface antes de o backend existir, declare um `mockHandler` no contrato. Com
|
|
155
|
+
`OPUS_MODE=design`, o runtime executa o `mockHandler` no lugar do `handler`; sem ele, usa o
|
|
156
|
+
`handler` real. Como o mock fica no contrato, a mesma declaração serve ao design e, depois, ao
|
|
157
|
+
handler real, e o mock não precisa acessar `ctx.db`.
|
|
158
|
+
|
|
159
|
+
`fake` e `fakeMany` geram dados determinísticos a partir do próprio schema (ver
|
|
160
|
+
[Testes de action](/docs/testing)). Em `list`, o `mockHandler` devolve a página inteira, com
|
|
161
|
+
`items`, `cursor` e `total`:
|
|
83
162
|
|
|
84
163
|
```ts
|
|
164
|
+
import { bindAction, defineContract } from '@softize/opus/core'
|
|
85
165
|
import { entityRowSchema } from '@softize/opus/schema'
|
|
86
166
|
import { fake, fakeMany } from '@softize/opus/testing'
|
|
167
|
+
import { z } from 'zod'
|
|
168
|
+
import { EventEntity } from '../entity.ts'
|
|
87
169
|
|
|
88
|
-
// `
|
|
170
|
+
// `entityRowSchema` deriva da entidade o schema de uma linha.
|
|
89
171
|
const eventRowSchema = entityRowSchema(EventEntity)
|
|
90
172
|
|
|
91
173
|
export const eventGet = defineContract({
|
|
@@ -93,19 +175,30 @@ export const eventGet = defineContract({
|
|
|
93
175
|
kind: 'view',
|
|
94
176
|
input: z.object({ id: z.string().uuid() }),
|
|
95
177
|
output: eventRowSchema,
|
|
96
|
-
mockHandler: () => fake(eventRowSchema),
|
|
178
|
+
mockHandler: () => fake(eventRowSchema),
|
|
97
179
|
})
|
|
98
|
-
// listas: `mockHandler: () => fakeMany(eventRowSchema, 20)`
|
|
99
180
|
|
|
100
|
-
|
|
101
|
-
|
|
181
|
+
export const eventList = defineContract({
|
|
182
|
+
name: 'event.list',
|
|
183
|
+
kind: 'list',
|
|
184
|
+
input: z.object({}),
|
|
185
|
+
output: eventRowSchema,
|
|
186
|
+
paginate: 'cursor',
|
|
187
|
+
mockHandler: async () => ({ items: fakeMany(eventRowSchema, 20), cursor: { next: null }, total: 20 }),
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
// Depois, o servidor liga o handler real ao mesmo contrato.
|
|
191
|
+
export const eventGetImpl = bindAction(eventGet, {
|
|
192
|
+
handler: async (ctx, input) =>
|
|
193
|
+
ctx.db.selectFrom('events').selectAll().where('id', '=', input.id).executeTakeFirstOrThrow(),
|
|
194
|
+
})
|
|
102
195
|
```
|
|
103
196
|
|
|
104
|
-
O `mockHandler` roda no
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
197
|
+
O `mockHandler` roda no servidor, não no navegador. O plugin `opusDesign()`, de
|
|
198
|
+
`@softize/opus/vite`, monta o runtime do Opus dentro do dev server e responde `/api` no próprio
|
|
199
|
+
processo. No modo design, qualquer `server.proxy` para um backend externo fica desligado: um prefixo
|
|
200
|
+
que ia para o proxy e não tem action correspondente responde 503 em envelope, sem alcançar o
|
|
201
|
+
backend. A aplicação chama `/api` como sempre e recebe os dados simulados.
|
|
109
202
|
|
|
110
203
|
```ts
|
|
111
204
|
// vite.config.ts — o plugin só carrega no modo design, por import dinâmico.
|
|
@@ -121,31 +214,32 @@ async function designPlugin(): Promise<PluginOption> {
|
|
|
121
214
|
const designPlugins: PluginOption[] = DESIGN_RUN ? [await designPlugin()] : []
|
|
122
215
|
|
|
123
216
|
export default defineConfig({ plugins: [react(), ...designPlugins] })
|
|
124
|
-
//
|
|
217
|
+
// Execute com: vite --mode design --configLoader runner (script dev:design do template)
|
|
125
218
|
```
|
|
126
219
|
|
|
127
|
-
O import
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
220
|
+
O import é dinâmico e condicionado porque o Opus publica TypeScript: o carregador padrão de config
|
|
221
|
+
do Vite externaliza um import estático, e o Node 22+ recusa type stripping dentro de `node_modules`
|
|
222
|
+
(`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). Com `--configLoader runner`, o Vite transforma o
|
|
223
|
+
TypeScript antes de avaliar o plugin. O app criado por `opus create` já traz esse arranjo.
|
|
224
|
+
|
|
225
|
+
O runtime de design atende somente requisições: reactions e schedules dos módulos carregados não
|
|
226
|
+
são registrados.
|
|
131
227
|
|
|
132
|
-
O `entry` (
|
|
133
|
-
'src/domains/*/contract.ts' })
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
228
|
+
O `entry` do plugin (padrão `./opus.config.ts`) também aceita glob, como
|
|
229
|
+
`opusDesign({ entry: 'src/domains/*/contract.ts' })`. O glob é expandido de novo a cada
|
|
230
|
+
reconstrução: um domínio novo passa a responder assim que o arquivo é salvo, sem editar o `entry`
|
|
231
|
+
nem reiniciar o dev server. Quando uma action não é encontrada, o plugin reconstrói o runtime uma
|
|
232
|
+
vez por ciclo de mudança e tenta de novo; se a action continuar ausente, a resposta indica o
|
|
233
|
+
`entry` a conferir.
|
|
138
234
|
|
|
139
|
-
Como o contrato
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
(não existe em Node). É a materialização Opus-nativa do "mock = contrato": o design não é
|
|
143
|
-
descartável — vira o contrato que o backend honra.
|
|
235
|
+
Como o contrato vai para os dois lados, o `mockHandler` também entra no bundle da web, onde nunca é
|
|
236
|
+
chamado. Não o condicione com `import.meta.env` no contrato: essa variável não existe no Node e
|
|
237
|
+
quebraria o servidor.
|
|
144
238
|
|
|
145
|
-
##
|
|
239
|
+
## Consumir o contrato na interface
|
|
146
240
|
|
|
147
|
-
|
|
148
|
-
|
|
241
|
+
A interface não precisa de fetch nem de tipos escritos à mão. Os hooks e os componentes de action,
|
|
242
|
+
como `ActionForm` e `ActionList`, executam e renderizam a operação a partir do contrato:
|
|
149
243
|
|
|
150
244
|
```tsx
|
|
151
245
|
import { ActionForm, useListAction } from '@softize/opus/ui/react'
|
|
@@ -153,10 +247,11 @@ import { ActionForm, useListAction } from '@softize/opus/ui/react'
|
|
|
153
247
|
// Lista declarativa: busca ao montar e refaz quando uma action invalida `workspace.list`.
|
|
154
248
|
const { items, total, isLoading } = useListAction<WorkspaceRow>(workspaceListContract, { q })
|
|
155
249
|
|
|
156
|
-
//
|
|
250
|
+
// Formulário a partir do contrato: os campos vêm de `fields`, declarado no próprio contrato.
|
|
157
251
|
<ActionForm action={workspaceCreateContract} onSuccess={…} />
|
|
158
252
|
```
|
|
159
253
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
254
|
+
Os hooks precisam de `OpusProvider` e `QueryClientProvider` acima da árvore; a montagem está em
|
|
255
|
+
[Como consumir](/ui/como-consumir). Para buscar sob demanda, como num autocomplete, use
|
|
256
|
+
`useLookupAction`: ele não busca ao montar, expõe `run(input)` e `reset()` e devolve `items`,
|
|
257
|
+
`cursor`, `total` e os estados da última execução.
|
|
@@ -4,14 +4,12 @@ title: IA generativa
|
|
|
4
4
|
|
|
5
5
|
# IA generativa
|
|
6
6
|
|
|
7
|
-
> **Experimental.**
|
|
8
|
-
> especulação (memória longa de conversa entra quando um caso real cobrar).
|
|
7
|
+
> **Experimental.** A superfície é mínima e cresce conforme casos reais a exijam.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
é
|
|
13
|
-
|
|
14
|
-
como o usuário, pelas mesmas actions que a UI usa.
|
|
9
|
+
Use o adapter de IA para completar texto, extrair dados estruturados ou executar um agente sobre as
|
|
10
|
+
actions do app. Em `extract`, o mesmo `Schema` usado nas actions define a resposta do modelo: a saída
|
|
11
|
+
é forçada e validada por ele. Em `run`, as actions expostas viram as ferramentas do modelo, que age
|
|
12
|
+
no app como o usuário, pelas mesmas actions usadas pela interface.
|
|
15
13
|
|
|
16
14
|
## O contrato
|
|
17
15
|
|
|
@@ -19,7 +17,7 @@ como o usuário, pelas mesmas actions que a UI usa.
|
|
|
19
17
|
interface AiAdapter {
|
|
20
18
|
complete(prompt: string, opts?: AiCompleteOptions): Promise<string>
|
|
21
19
|
extract<T>(prompt: string, schema: Schema<T>, opts?: AiCompleteOptions): Promise<T>
|
|
22
|
-
// Loop agêntico: o modelo chama tools (as actions
|
|
20
|
+
// Loop agêntico: o modelo chama tools (as actions expostas à IA) até responder em texto.
|
|
23
21
|
run?(input: string | AiMessage[], opts: AiRunOptions): Promise<AiRunResult>
|
|
24
22
|
// O mesmo loop emitindo ChatEvent conforme acontece (ver Streaming, abaixo).
|
|
25
23
|
runStream?(input: string | AiMessage[], opts: AiRunOptions): AsyncIterable<ChatEvent>
|
|
@@ -32,16 +30,15 @@ interface AiAdapter {
|
|
|
32
30
|
```ts
|
|
33
31
|
import { anthropicAi } from '@softize/opus/ai/anthropic'
|
|
34
32
|
|
|
35
|
-
//
|
|
33
|
+
// Padrão: modelo Haiku e ANTHROPIC_API_KEY do ambiente.
|
|
36
34
|
const ai = anthropicAi()
|
|
37
35
|
|
|
38
36
|
// Tudo injetável: client próprio, modelo default, teto de tokens.
|
|
39
37
|
const custom = anthropicAi({ apiKey, model: 'claude-sonnet-5', maxTokens: 2048 })
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
e nada toca a rede.
|
|
40
|
+
`@anthropic-ai/sdk` é dependência opcional, carregada só quando o driver é usado. Em teste, injete um
|
|
41
|
+
client falso (`{ messages: { create } }`) para que nada acesse a rede.
|
|
45
42
|
|
|
46
43
|
## No runtime
|
|
47
44
|
|
|
@@ -54,50 +51,52 @@ const runtime = createRuntime({
|
|
|
54
51
|
// No handler: ctx.ai (null quando não configurado).
|
|
55
52
|
handler: async (ctx, input) => {
|
|
56
53
|
const meta = await ctx.ai!.extract(
|
|
57
|
-
`Classifique o chamado e proponha um título curto: ${input.
|
|
58
|
-
z.object({
|
|
54
|
+
`Classifique o chamado e proponha um título curto: ${input.text}`,
|
|
55
|
+
z.object({ title: z.string(), priority: z.enum(['low', 'medium', 'high']) }),
|
|
59
56
|
)
|
|
60
|
-
return meta // já validado
|
|
57
|
+
return meta // já validado; uma resposta fora do schema faz o extract lançar erro
|
|
61
58
|
}
|
|
62
59
|
```
|
|
63
60
|
|
|
64
|
-
## Actions como
|
|
61
|
+
## Actions como ferramentas do agente
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
modelo
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
Exponha uma action à IA no contrato, com `ai: true` ou com um objeto `ai` que não declare
|
|
64
|
+
`enabled: false`, para que o modelo possa chamá-la. Então `ctx.ai.run(prompt)` executa o ciclo do
|
|
65
|
+
agente sobre as actions expostas: o modelo escolhe uma
|
|
66
|
+
ferramenta, o runtime executa a action como o usuário autenticado, limitado por `ctx.can`, e o
|
|
67
|
+
resultado volta ao modelo. O ciclo se repete até o modelo responder em texto.
|
|
70
68
|
|
|
71
69
|
```ts
|
|
72
70
|
// A action opta por entrar; o schema do contrato também descreve a ferramenta:
|
|
73
|
-
export const
|
|
74
|
-
name: '
|
|
71
|
+
export const searchInvoices = defineContract({
|
|
72
|
+
name: 'invoice.search',
|
|
75
73
|
kind: 'list',
|
|
76
74
|
label: 'Buscar notas',
|
|
77
|
-
input: z.object({
|
|
78
|
-
output: z.object({
|
|
75
|
+
input: z.object({ customer: z.string(), month: z.string() }),
|
|
76
|
+
output: z.object({ number: z.string(), amount: z.number() }), // em `list`, o schema de cada item
|
|
79
77
|
ai: { enabled: true, description: 'Busca notas por cliente e mês.' },
|
|
80
78
|
})
|
|
81
79
|
|
|
82
80
|
// No handler — ou fora dele, via runtime.aiFor(base), para o backend de um chat:
|
|
83
81
|
const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
|
|
84
|
-
// o modelo chamou
|
|
82
|
+
// o modelo chamou invoice.search sozinho, como o usuário logado, e respondeu em texto.
|
|
85
83
|
```
|
|
86
84
|
|
|
87
85
|
`runtime.aiTools()` projeta cada action exposta como um `AiTool`: `name` é o identificador técnico
|
|
88
86
|
usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
|
|
89
87
|
`description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
|
|
90
|
-
`metadata.dataProducts` lista os Produtos de Dados relacionados e
|
|
91
|
-
associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
|
|
88
|
+
`metadata.dataProducts` lista os [Produtos de Dados](/docs/data-products) relacionados e
|
|
89
|
+
`metadata.dataProductLabels` associa cada identificador ao seu nome legível. Esses metadados servem ao host: não entram no
|
|
92
90
|
prompt e não concedem acesso.
|
|
93
91
|
|
|
94
92
|
O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
|
|
95
93
|
para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
não rodam sem aprovação: passe `run(prompt, { confirm })` para que o chat peça a confirmação. Sem
|
|
95
|
+
`confirm`, a action é recusada e o modelo informa a recusa. O gate lê somente esses dois campos de `ai`: o
|
|
98
96
|
`confirm: { destructive: true }` da action configura a confirmação da UI e não protege a chamada
|
|
99
|
-
feita pelo agente. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto
|
|
100
|
-
o backend do chat resolve o usuário e chama `.run(
|
|
97
|
+
feita pelo agente. Fora do handler, `runtime.aiFor(base)` devolve o `ai` já ligado a um contexto:
|
|
98
|
+
o backend do chat resolve o usuário e chama `.run(history)`. Para agentes fora da aplicação, veja
|
|
99
|
+
[MCP](/docs/mcp).
|
|
101
100
|
|
|
102
101
|
Para o modelo perguntar algo à pessoa no meio do turno, passe `onAsk` em `run` ou `runStream`. O
|
|
103
102
|
driver Anthropic então oferece a tool reservada `ask_user`, entrega as perguntas (`AskQuestion[]`)
|
|
@@ -105,13 +104,13 @@ ao seu `onAsk` e devolve as respostas (`AskAnswer[]`) ao modelo. Sem `onAsk`, um
|
|
|
105
104
|
`ask_user` é recusada com a orientação de perguntar em texto. Na interface, o componente
|
|
106
105
|
[Ask](/ui/ask) apresenta essas perguntas e coleta as respostas.
|
|
107
106
|
|
|
108
|
-
## Streaming
|
|
107
|
+
## Streaming da conversa
|
|
109
108
|
|
|
110
|
-
`runStream`
|
|
111
|
-
|
|
112
|
-
em uso
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
`runStream` executa o mesmo ciclo do `run` e emite `ChatEvent` à medida que ele avança. O protocolo
|
|
110
|
+
está em `docs/chat-event-protocol.md`, dentro do pacote: `text` traz um trecho novo de texto,
|
|
111
|
+
`tool` identifica a action em uso (sem tradução para a interface, que fica com quem apresenta),
|
|
112
|
+
`artifact` entrega algo produzido na conversa e `done` encerra o turno. O `run` continua disponível;
|
|
113
|
+
uma resposta completa equivale a um `text` seguido de `done`.
|
|
115
114
|
|
|
116
115
|
```ts
|
|
117
116
|
// O backend de um chat streamando por SSE:
|
|
@@ -120,12 +119,12 @@ for await (const event of runtime.aiFor(base)!.runStream(messages)) {
|
|
|
120
119
|
}
|
|
121
120
|
```
|
|
122
121
|
|
|
123
|
-
O driver Anthropic emite
|
|
124
|
-
|
|
125
|
-
O
|
|
122
|
+
O driver Anthropic emite o texto em trechos, à medida que chega, quando o client oferece
|
|
123
|
+
`messages.stream`. Com um client que só tem `create`, como um client falso de teste, o texto de cada
|
|
124
|
+
passo sai num único trecho. O componente [Chat](/ui/chat) consome os dois modos.
|
|
126
125
|
|
|
127
|
-
## Limites
|
|
126
|
+
## Limites atuais
|
|
128
127
|
|
|
129
|
-
|
|
130
|
-
`extract` exige schema com objeto na raiz (`z.object`)
|
|
131
|
-
|
|
128
|
+
`run` e `runStream` aceitam o histórico da conversa, mas não mantêm memória longa nem resumem o
|
|
129
|
+
histórico automaticamente. `extract` exige um schema com objeto na raiz (`z.object`), uma regra da
|
|
130
|
+
API de ferramentas do provedor.
|