@softize/opus 25.0.0 → 25.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/bin/cli.mjs +5 -0
  3. package/bin/lib/assistant-schema.d.mts +55 -0
  4. package/bin/lib/assistant-schema.mjs +286 -0
  5. package/bin/lib/check.mjs +173 -0
  6. package/bin/lib/copy.mjs +2 -0
  7. package/bin/lib/gen-manifest.mjs +3 -0
  8. package/bin/lib/gen-runner.mjs +29 -1
  9. package/bin/lib/mcp.mjs +1 -1
  10. package/docs/adr/0052-floating-layer-coexists-with-modal-surfaces.md +668 -0
  11. package/docs/adr/0053-read-action-declares-the-resource-assistant.md +443 -0
  12. package/docs/protocol.md +54 -0
  13. package/docs/releasing.md +8 -2
  14. package/docs/stacking-scale.md +53 -0
  15. package/package.json +10 -1
  16. package/registry/skills/create-opus-action/SKILL.md +8 -3
  17. package/registry/skills/create-opus-action/references/contract-and-binding.md +24 -1
  18. package/registry/skills/create-opus-action/references/evaluations.md +3 -0
  19. package/registry/skills/test-opus-action/SKILL.md +4 -1
  20. package/registry/skills/test-opus-action/references/evaluations.md +2 -0
  21. package/registry/skills/test-opus-action/references/harness.md +14 -0
  22. package/src/ai/index.ts +3 -0
  23. package/src/ai/resource-context.ts +113 -0
  24. package/src/core/assistant.ts +545 -0
  25. package/src/core/index.ts +23 -0
  26. package/src/core/runtime.ts +136 -0
  27. package/src/core/types.ts +34 -0
  28. package/src/mcp/index.ts +3 -0
  29. package/src/ui/components/primitives/dialog.tsx +285 -26
  30. package/src/ui/components/primitives/drawer.tsx +104 -13
  31. package/src/ui/components/primitives/floating-layer.tsx +79 -0
  32. package/src/ui/components/primitives/menu.tsx +17 -2
  33. package/src/ui/components/primitives/popover.tsx +6 -1
  34. package/src/ui/components/primitives/tooltip.tsx +6 -1
  35. package/src/ui/docs/content/actions.md +97 -1
  36. package/src/ui/docs/content/ai.md +4 -2
  37. package/src/ui/docs/content/cli.md +2 -1
  38. package/src/ui/docs/content/dialog.md +3 -0
  39. package/src/ui/docs/content/drawer.md +3 -0
  40. package/src/ui/docs/content/floating-layer.md +178 -0
  41. package/src/ui/docs/content/mcp.md +3 -1
  42. package/src/ui/docs/registry.tsx +6 -0
  43. package/src/ui/lib/dismiss-on-iframe.ts +4 -1
  44. package/src/ui/lib/modal-layers.ts +547 -0
  45. package/src/ui/meta.ts +6 -0
  46. package/src/ui/react.tsx +4 -0
@@ -18,7 +18,19 @@ import {
18
18
  type StructuralMediaProps,
19
19
  } from "../patterns/structural-media.tsx";
20
20
  import { CloseButton, type CloseButtonSize } from "./close-button.tsx";
21
- import { overlayClasses, preventDismissWhen } from "./dialog.tsx";
21
+ import {
22
+ composeModalDismissHandlers,
23
+ overlayClasses,
24
+ useComposedRef,
25
+ useModalOpenState,
26
+ } from "./dialog.tsx";
27
+ import {
28
+ LAYER_MODAL_ATTRIBUTE,
29
+ LayerPortalProvider,
30
+ handleModalTabKey,
31
+ useLayerPortal,
32
+ useModalLayerEntry,
33
+ } from "../../lib/modal-layers.ts";
22
34
 
23
35
  interface DrawerContentContextValue {
24
36
  showCloseButton: boolean;
@@ -34,10 +46,36 @@ const DrawerContentContext = React.createContext<DrawerContentContextValue>({
34
46
 
35
47
  const DrawerIntroContext = React.createContext(false);
36
48
 
49
+ /** Estado de aberto que a pilha de modais da camada flutuante acompanha (ADR 0052). */
50
+ const DrawerStateContext = React.createContext({ open: false, modal: true });
51
+
37
52
  function Drawer({
53
+ open: openProp,
54
+ defaultOpen,
55
+ onOpenChange,
56
+ modal,
38
57
  ...props
39
58
  }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
40
- return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
59
+ const [open, setOpen] = useModalOpenState({
60
+ open: openProp,
61
+ defaultOpen,
62
+ onOpenChange,
63
+ });
64
+ const state = React.useMemo(
65
+ () => ({ open, modal: modal !== false }),
66
+ [open, modal],
67
+ );
68
+ return (
69
+ <DrawerStateContext.Provider value={state}>
70
+ <DrawerPrimitive.Root
71
+ data-slot="drawer"
72
+ modal={modal}
73
+ open={open}
74
+ onOpenChange={setOpen}
75
+ {...props}
76
+ />
77
+ </DrawerStateContext.Provider>
78
+ );
41
79
  }
42
80
 
43
81
  function DrawerTrigger({
@@ -53,19 +91,30 @@ function DrawerClose({
53
91
  }
54
92
 
55
93
  function DrawerPortal({
94
+ container,
56
95
  ...props
57
96
  }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
58
- return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
97
+ // Aberto de dentro de uma camada flutuante, o Drawer vai para o hospedeiro dela (ADR 0052).
98
+ const layer = useLayerPortal();
99
+ return (
100
+ <DrawerPrimitive.Portal
101
+ data-slot="drawer-portal"
102
+ container={container ?? layer.container}
103
+ {...props}
104
+ />
105
+ );
59
106
  }
60
107
 
61
108
  function DrawerOverlay({
62
109
  className,
63
110
  ...props
64
111
  }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
112
+ const layer = useLayerPortal();
65
113
  return (
66
114
  <DrawerPrimitive.Overlay
67
115
  data-slot="drawer-overlay"
68
- className={cn(overlayClasses, className)}
116
+ {...{ [LAYER_MODAL_ATTRIBUTE]: layer.container !== undefined || undefined }}
117
+ className={cn(overlayClasses, layer.zIndexClass, className)}
69
118
  {...props}
70
119
  />
71
120
  );
@@ -95,7 +144,11 @@ function DrawerContent({
95
144
  layout = "sectioned",
96
145
  "aria-describedby": ariaDescribedBy,
97
146
  onEscapeKeyDown,
147
+ onPointerDownOutside,
98
148
  onInteractOutside,
149
+ onCloseAutoFocus,
150
+ onKeyDown,
151
+ ref,
99
152
  ...props
100
153
  }: React.ComponentProps<typeof DrawerPrimitive.Content> & {
101
154
  side?: "top" | "right" | "bottom" | "left";
@@ -109,12 +162,44 @@ function DrawerContent({
109
162
  /** Anatomia da superfície: `sectioned` (default) ou `continuous` — ADR 0023. */
110
163
  layout?: SurfaceLayout;
111
164
  }) {
165
+ const { open, modal } = React.useContext(DrawerStateContext);
166
+ const layer = useLayerPortal();
167
+ const internal = layer.container !== undefined;
168
+ // Seção 6 da ADR 0052: portado para uma camada, o Drawer é interno a ela; fora, é de trabalho.
169
+ const mark = internal ? "internal" : "work";
170
+ const entryRef = useModalLayerEntry({
171
+ open,
172
+ modal,
173
+ mark,
174
+ surface: "drawer",
175
+ side,
176
+ });
177
+ const composedRef = useComposedRef(ref, entryRef);
178
+ const dismiss = composeModalDismissHandlers({
179
+ mark,
180
+ closeDisabled,
181
+ onEscapeKeyDown,
182
+ onPointerDownOutside,
183
+ onInteractOutside,
184
+ onCloseAutoFocus,
185
+ });
186
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
187
+ onKeyDown?.(event);
188
+ handleModalTabKey(event);
189
+ };
190
+ // Os portais abertos daqui herdam a marca de modal quando este Drawer mora numa camada.
191
+ const nestedPortal = React.useMemo(
192
+ () => ({ ...layer, modalSubtree: layer.modalSubtree || internal }),
193
+ [layer, internal],
194
+ );
112
195
  return (
113
196
  <DrawerPortal>
114
197
  <DrawerOverlay />
115
198
  <DrawerPrimitive.Content
199
+ ref={composedRef}
116
200
  data-slot="drawer-content"
117
201
  data-layout={layout}
202
+ {...{ [LAYER_MODAL_ATTRIBUTE]: internal || undefined }}
118
203
  className={cn(
119
204
  "fixed z-50 flex flex-col overflow-hidden rounded-xl border bg-background shadow-lg transition ease-in-out [--surface-background:var(--background)] data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
120
205
  side === "right" &&
@@ -125,20 +210,26 @@ function DrawerContent({
125
210
  "inset-x-1 top-1 h-auto data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
126
211
  side === "bottom" &&
127
212
  "inset-x-1 bottom-1 h-auto data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
213
+ layer.zIndexClass,
128
214
  className,
129
215
  )}
130
216
  aria-describedby={ariaDescribedBy}
131
- onEscapeKeyDown={preventDismissWhen(closeDisabled, onEscapeKeyDown)}
132
- onInteractOutside={preventDismissWhen(closeDisabled, onInteractOutside)}
217
+ onEscapeKeyDown={dismiss.onEscapeKeyDown}
218
+ onPointerDownOutside={dismiss.onPointerDownOutside}
219
+ onInteractOutside={dismiss.onInteractOutside}
220
+ onCloseAutoFocus={dismiss.onCloseAutoFocus}
221
+ onKeyDown={handleKeyDown}
133
222
  {...props}
134
223
  >
135
- <SurfaceLayoutProvider value={layout}>
136
- <DrawerContentContext.Provider
137
- value={{ showCloseButton, closeDisabled, closeSize }}
138
- >
139
- {children}
140
- </DrawerContentContext.Provider>
141
- </SurfaceLayoutProvider>
224
+ <LayerPortalProvider value={nestedPortal}>
225
+ <SurfaceLayoutProvider value={layout}>
226
+ <DrawerContentContext.Provider
227
+ value={{ showCloseButton, closeDisabled, closeSize }}
228
+ >
229
+ {children}
230
+ </DrawerContentContext.Provider>
231
+ </SurfaceLayoutProvider>
232
+ </LayerPortalProvider>
142
233
  </DrawerPrimitive.Content>
143
234
  </DrawerPortal>
144
235
  );
@@ -0,0 +1,79 @@
1
+ import * as React from "react";
2
+ import { createPortal } from "react-dom";
3
+
4
+ import { cn } from "../../lib/cn.ts";
5
+ import {
6
+ LayerPortalProvider,
7
+ registerFloatingLayer,
8
+ } from "../../lib/modal-layers.ts";
9
+
10
+ export interface FloatingLayerProps {
11
+ children: React.ReactNode;
12
+ /** Classes do hospedeiro. A posição de cada filho continua sendo do consumidor. */
13
+ className?: string;
14
+ /**
15
+ * Esc enquanto a pessoa está na camada, com ou sem modal aberto. É chamado uma vez por tecla.
16
+ * Nenhum modal fecha nesse caso, e o Opus não fecha a camada sozinho, porque o estado de aberto
17
+ * pertence ao consumidor.
18
+ */
19
+ onEscapeKeyDown?: (event: KeyboardEvent) => void;
20
+ }
21
+
22
+ /**
23
+ * Hospedeiro fixo, na tela inteira e acima dos modais de trabalho. Ele mesmo não recebe clique;
24
+ * os filhos diretos recebem, mesmo com o `body` bloqueado por um modal.
25
+ */
26
+ const hostClasses =
27
+ "pointer-events-none fixed inset-0 z-60 *:pointer-events-auto";
28
+
29
+ /**
30
+ * Região da aplicação que fica acima do conteúdo e dos modais de trabalho (Dialog e Drawer) e
31
+ * continua utilizável enquanto eles estão abertos, como o botão e o painel de um assistente
32
+ * (ADR 0052).
33
+ *
34
+ * Os filhos são portados para `div[data-slot="floating-layer"]` no `body`. O hospedeiro publica o
35
+ * modal de trabalho do topo em `data-modal` (`none`, `dialog` ou `drawer`) e o lado do Drawer em
36
+ * `data-modal-side`, para o consumidor reposicionar o que precisar. Popover, Menu, Tooltip, Dialog
37
+ * e Drawer abertos de dentro da camada vão para o mesmo hospedeiro.
38
+ */
39
+ export function FloatingLayer({
40
+ children,
41
+ className,
42
+ onEscapeKeyDown,
43
+ }: FloatingLayerProps) {
44
+ const [host, setHost] = React.useState<HTMLElement | null>(null);
45
+ const escapeRef = React.useRef(onEscapeKeyDown);
46
+ escapeRef.current = onEscapeKeyDown;
47
+
48
+ React.useLayoutEffect(() => {
49
+ const element = document.createElement("div");
50
+ element.setAttribute("data-slot", "floating-layer");
51
+ document.body.appendChild(element);
52
+ const unregister = registerFloatingLayer(element, (event) =>
53
+ escapeRef.current?.(event),
54
+ );
55
+ setHost(element);
56
+ // Desfaz o registro, retira o hospedeiro e, se o foco estava nele com um modal aberto,
57
+ // devolve o foco ao modal.
58
+ return unregister;
59
+ }, []);
60
+
61
+ React.useLayoutEffect(() => {
62
+ if (host !== null) host.className = cn(hostClasses, className);
63
+ }, [host, className]);
64
+
65
+ const portal = React.useMemo(
66
+ () => ({
67
+ container: host ?? undefined,
68
+ zIndexClass: undefined,
69
+ modalSubtree: false,
70
+ }),
71
+ [host],
72
+ );
73
+
74
+ if (host === null) return null;
75
+ return createPortal(
76
+ <LayerPortalProvider value={portal}>{children}</LayerPortalProvider>,
77
+ host,
78
+ );
79
+ }
@@ -5,6 +5,7 @@ import { DropdownMenu as MenuPrimitive } from "radix-ui";
5
5
 
6
6
  import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts";
7
7
  import { cn } from "../../lib/cn.ts";
8
+ import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts";
8
9
 
9
10
  function Menu({ ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {
10
11
  return <MenuPrimitive.Root data-slot="menu" {...props} />;
@@ -16,9 +17,17 @@ function Menu({ ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {
16
17
  * próxima versão major.
17
18
  */
18
19
  function MenuPortal({
20
+ container,
19
21
  ...props
20
22
  }: React.ComponentProps<typeof MenuPrimitive.Portal>) {
21
- return <MenuPrimitive.Portal data-slot="menu-portal" {...props} />;
23
+ const layer = useLayerPortal();
24
+ return (
25
+ <MenuPrimitive.Portal
26
+ data-slot="menu-portal"
27
+ container={container ?? layer.container}
28
+ {...props}
29
+ />
30
+ );
22
31
  }
23
32
 
24
33
  function MenuTrigger({
@@ -35,14 +44,18 @@ function MenuContent({
35
44
  // Mesma razão do Popover: clique dentro de iframe não chega aqui fora.
36
45
  const ref = React.useRef<HTMLDivElement>(null);
37
46
  useDismissOnIframeFocus(ref);
47
+ // Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
48
+ const layer = useLayerPortal();
38
49
  return (
39
- <MenuPrimitive.Portal>
50
+ <MenuPrimitive.Portal container={layer.container}>
40
51
  <MenuPrimitive.Content
41
52
  ref={ref}
42
53
  data-slot="menu-content"
54
+ {...layerModalMarker(layer)}
43
55
  sideOffset={sideOffset}
44
56
  className={cn(
45
57
  "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
58
+ layer.zIndexClass,
46
59
  className,
47
60
  )}
48
61
  {...props}
@@ -215,11 +228,13 @@ function MenuSubContent({
215
228
  className,
216
229
  ...props
217
230
  }: React.ComponentProps<typeof MenuPrimitive.SubContent>) {
231
+ const layer = useLayerPortal();
218
232
  return (
219
233
  <MenuPrimitive.SubContent
220
234
  data-slot="menu-sub-content"
221
235
  className={cn(
222
236
  "z-50 min-w-[10rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md will-change-[transform,opacity] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
237
+ layer.zIndexClass,
223
238
  className,
224
239
  )}
225
240
  {...props}
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { Popover as PopoverPrimitive } from "radix-ui"
3
3
 
4
4
  import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
5
+ import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts"
5
6
  import { cn } from '../../lib/cn.ts'
6
7
 
7
8
  function Popover({
@@ -26,15 +27,19 @@ function PopoverContent({
26
27
  // pendurado sobre o conteúdo. Na cabine, o preview É um iframe.
27
28
  const ref = React.useRef<HTMLDivElement>(null)
28
29
  useDismissOnIframeFocus(ref)
30
+ // Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
31
+ const layer = useLayerPortal()
29
32
  return (
30
- <PopoverPrimitive.Portal>
33
+ <PopoverPrimitive.Portal container={layer.container}>
31
34
  <PopoverPrimitive.Content
32
35
  ref={ref}
33
36
  data-slot="popover-content"
37
+ {...layerModalMarker(layer)}
34
38
  align={align}
35
39
  sideOffset={sideOffset}
36
40
  className={cn(
37
41
  "z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
42
+ layer.zIndexClass,
38
43
  className
39
44
  )}
40
45
  {...props}
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { Tooltip as TooltipPrimitive } from "radix-ui"
3
3
 
4
4
  import { cn } from '../../lib/cn.ts'
5
+ import { layerModalMarker, useLayerPortal } from "../../lib/modal-layers.ts"
5
6
 
6
7
  function TooltipProvider({
7
8
  delayDuration = 0,
@@ -34,13 +35,17 @@ function TooltipContent({
34
35
  children,
35
36
  ...props
36
37
  }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
38
+ // Aberto de dentro de uma camada flutuante, vai para o hospedeiro dela (ADR 0052).
39
+ const layer = useLayerPortal()
37
40
  return (
38
- <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Portal container={layer.container}>
39
42
  <TooltipPrimitive.Content
40
43
  data-slot="tooltip-content"
44
+ {...layerModalMarker(layer)}
41
45
  sideOffset={sideOffset}
42
46
  className={cn(
43
47
  "z-50 w-fit max-w-sm origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-pretty text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
48
+ layer.zIndexClass,
44
49
  className
45
50
  )}
46
51
  {...props}
@@ -136,7 +136,7 @@ name, kind
136
136
  → public, requires, authorize, loads
137
137
  → handler
138
138
  → audit, confirm, invalidates, rateLimit, idempotency
139
- → ai, automatable, internal
139
+ → ai, assistant, automatable, internal
140
140
  ```
141
141
 
142
142
  Campos específicos de um kind, como `fields`, `paginate`, `columns`, `filters`, `expand`,
@@ -179,6 +179,102 @@ A autorização é fechada por padrão: sem `public: true`, a action exige usuá
179
179
  `authorize`, exige também a permissão declarada. `automatable: true` permite que um
180
180
  [schedule](/docs/scheduler) execute a action como o sistema, sem abri-la a chamadores anônimos.
181
181
 
182
+ ## Declarar o assistente do recurso
183
+
184
+ Um produto pode oferecer, além do chat geral, uma conversa sobre um recurso específico: um cliente,
185
+ um relatório, um pedido. Essa conversa precisa ler o recurso com a permissão de quem conversa, seguir
186
+ o procedimento próprio daquele tipo de recurso e entregar ao modelo só os dados necessários. Os três
187
+ ficam declarados num lugar só: a action `view` que lê o recurso declara `assistant` e passa a ser a
188
+ **action dona** daquele tipo de recurso.
189
+
190
+ ```ts
191
+ export const customerViewContract = defineContract({
192
+ name: 'customer.view',
193
+ kind: 'view',
194
+ label: 'Ver cliente',
195
+ input: z.object({ id: z.string().uuid() }),
196
+ output: customerViewSchema,
197
+ personalData: true,
198
+ requires: 'customer.read',
199
+ authorize: (ctx) => ctx.can('customer.read'),
200
+ ai: true,
201
+ assistant: {
202
+ resource: 'customer',
203
+ skill: 'review-customer',
204
+ label: 'Assistente do cliente',
205
+ title: 'name',
206
+ context: ['kind', 'stage', 'sources', 'leads.totalCount', 'leads.latest.status'],
207
+ },
208
+ })
209
+ ```
210
+
211
+ | Campo | O que declara |
212
+ | --- | --- |
213
+ | `resource` | O tipo de recurso de que a action é dona, em camelCase. Cada tipo tem uma única action dona. |
214
+ | `skill` | A chave do procedimento que a IA segue ao tratar do recurso, em minúsculas com hífen. O Opus não conhece o procedimento; o produto o mantém e confere que a chave existe. |
215
+ | `label` | O nome do assistente para pessoas. Entra no inventário do `opus copy`. |
216
+ | `title` | O caminho, na saída, do campo que nomeia o recurso. |
217
+ | `context` | Os caminhos, na saída, dos campos que o host pode entregar ao modelo como dado do recurso. |
218
+ | `maxContextChars` | Opcional. O teto do dado entregue, em caracteres de JSON: 4.000 por padrão, de 1.000 a 16.000. |
219
+
220
+ A action dona precisa ser `view`, estar publicada para a IA (`ai`), declarar `personalData` e
221
+ receber só `{ id }`: o `id`, do tipo texto, é o único campo obrigatório da entrada, e outros campos,
222
+ se houver, têm valor padrão ou são opcionais. Os caminhos usam ponto para descer em objetos; um
223
+ caminho que passa por uma lista vale para cada item, e um campo intermediário `null` aparece como
224
+ `null`. Declare em `context` só o que orienta a conversa: o exemplo deixa fora contato, documento e
225
+ endereço, e o nome já vai como `title`. Um caminho que termina num objeto ou numa lista leva tudo o
226
+ que está abaixo dele, inclusive campos acrescentados depois à saída; para conferir num teste o que
227
+ cada caminho alcança, use `assistantContextLeaves`, de `@softize/opus/assistant-schema`, sobre o
228
+ schema de saída do manifest. O `title` precisa apontar para texto ou número.
229
+
230
+ O `opus check` cobra a forma da declaração (regra `assistant-declaration`), o `opus gen` recusa
231
+ `title`, caminho de `context` ou entrada que não conferem com os schemas, e o registro recusa duas
232
+ actions donas do mesmo recurso. O manifest, `runtime.aiTools()` e o MCP (`com.softize.opus/assistant`
233
+ em `_meta`) levam a declaração, e o manifest passa a mostrar também a publicação para a IA.
234
+
235
+ ### Ler o recurso pela action dona
236
+
237
+ O recurso de um assistente é lido, e o acesso a ele é conferido, somente pela action dona.
238
+ `runtime.readResource` executa a action com `{ id }` pelo mesmo `execute` de qualquer chamada; a
239
+ autorização, a auditoria e a marca de dado pessoal são as da action:
240
+
241
+ ```ts
242
+ const read = await runtime.readResource('customer', id, base, { context: true })
243
+ if (!read.ok) return notFound() // o erro da action, com a categoria dela
244
+ read.resource // { kind: 'customer', id, title: 'Maria Souza', label: 'Assistente do cliente', skill: 'review-customer' }
245
+ read.context // { data, truncated, omitted, personalData: true }
246
+ ```
247
+
248
+ Sem `{ context: true }`, a leitura devolve só o descritor, que basta para conferir o acesso e
249
+ mostrar o nome. Um tipo sem action dona devolve `runtime.assistant_resource_unknown` sem executar
250
+ nada. `runtime.assistants()` lista as declarações com a action dona de cada uma, para o produto
251
+ montar rotas e telas sem lista paralela e para conferir, num teste, que cada `skill` existe no
252
+ registro de procedimentos dele. O Opus não enxerga as rotas HTTP que o produto registra por conta
253
+ própria: um teste do produto deve garantir que nenhuma rota confere acesso a recurso sem passar por
254
+ uma action.
255
+
256
+ ### O dado que vai ao modelo
257
+
258
+ `context.data` traz só os caminhos declarados, com a estrutura da saída. Quando o JSON passa do
259
+ teto, o corte acontece por etapas, até caber, e fica registrado em `omitted`:
260
+
261
+ 1. sai o último item da maior lista, repetidamente;
262
+ 2. com as listas vazias, o texto mais longo é encurtado pela metade, até 200 caracteres;
263
+ 3. por fim, saem campos inteiros, do último declarado para o primeiro.
264
+
265
+ Com qualquer corte, `truncated` vale `true`. O título fica fora do teto e é encurtado a 200
266
+ caracteres. Um caminho que falta na saída em execução é omitido com o motivo `missing` e gera um
267
+ aviso no log, sem falhar a leitura. Quem recebe a saída por outro canal, como o MCP, aplica a mesma
268
+ projeção com `projectAssistantContext(assistant, output)`.
269
+
270
+ Entregue o dado ao modelo como bloco delimitado, numa mensagem de dado, nunca no prompt de sistema.
271
+ `resourceDataBlock`, de `@softize/opus/ai`, monta o bloco com o tipo, o id, o título, os dados e,
272
+ quando houve corte ou ausência, `truncated` e `omitted`, e escapa dentro dos valores o que poderia
273
+ fechar o delimitador. Quando o modelo chama a action dona como ferramenta, recebe a saída completa
274
+ do contrato, como em qualquer chamada: a projeção governa só o que o host entrega por iniciativa
275
+ própria. Cada execução de uma action com dado pessoal é uma exposição registrada, então reaproveite
276
+ a leitura que já conferiu o acesso em vez de executar a action de novo só para obter o contexto.
277
+
182
278
  ## Prototipar com dados simulados
183
279
 
184
280
  Para desenhar a interface antes de o backend existir, rode o app em modo design. Nesse modo, toda
@@ -86,8 +86,10 @@ const { text } = await ctx.ai!.run('Quais notas a Empresa X emitiu em junho?')
86
86
  usado na execução e na auditoria; `title` traz a `label` legível da action, quando existe;
87
87
  `description` vem de `ai.description` (ou da `description` da action) e `inputSchema`, do input.
88
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
90
- prompt e não concedem acesso.
89
+ `metadata.dataProductLabels` associa cada identificador ao seu nome legível. Na action dona de um
90
+ tipo de recurso, `metadata.assistant` traz o [assistente declarado](/docs/actions): o recurso, a
91
+ chave do procedimento, o nome, o campo do título, os campos do dado ao modelo e o teto. Esses
92
+ metadados servem ao host: não entram no prompt e não concedem acesso.
91
93
 
92
94
  O agente só executa nomes que `aiTools()` anunciou; uma chamada a outra action volta como erro
93
95
  para o modelo. Actions marcadas com `ai: { destructive: true }` ou `ai: { requiresConfirmation: true }`
@@ -21,7 +21,8 @@ opus seed check # bindings, dependências, ciclos e scripts paralelos de s
21
21
  ```
22
22
 
23
23
  `opus check` lê o código-fonte sem executá-lo e aplica regras em quatro grupos: actions
24
- (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind`), UI
24
+ (`action-name`, `kind`, `field-order`, `export`, `requires-sem-authorize`, `ignored-by-kind` e
25
+ `assistant-declaration`, que cobra a forma do [assistente do recurso](/docs/actions)), UI
25
26
  (`ui-structure`, `ui-structure-split`, `ui-semantic-api`, `removed-ui-token`, `unpaired-ui-surface`),
26
27
  [Produtos de Dados](/docs/data-products) (`data-product-export`, `-id`, `-duplicate`, `-version`,
27
28
  `-interfaces`, `-interface`, `-entities` e `-entity`, todas com o prefixo `data-product`) e
@@ -441,6 +441,9 @@ botão. O texto informa o efeito real, como `Excluir`, `Revogar acesso` ou `Arqu
441
441
  - O comportamento selecionado por `mode` determina o papel de acessibilidade; `role` não configura
442
442
  o comportamento.
443
443
  - O foco permanece dentro do diálogo aberto e retorna ao gatilho após o fechamento.
444
+ - No modo padrão, uma [`FloatingLayer`](/ui/floating-layer), como o painel de um assistente,
445
+ continua utilizável com o diálogo aberto: o Tab passa por ela, o foco não é puxado de volta e o Esc
446
+ age onde a pessoa está. O modo `alert` cobre a camada até a resposta.
444
447
  - `DialogHost` apresenta uma solicitação por vez para que cada resposta tenha um alvo inequívoco.
445
448
 
446
449
  ## Propriedades de Dialog
@@ -170,6 +170,9 @@ de continuar, use [`Dialog mode="alert"`](/ui/dialog), que não fecha pelo cliqu
170
170
  - Com `modal` ativo, o padrão, o foco permanece dentro do painel e o restante da página fica
171
171
  inacessível até o fechamento.
172
172
  - Ao fechar, o foco retorna ao gatilho.
173
+ - Uma [`FloatingLayer`](/ui/floating-layer), como o painel de um assistente, continua utilizável
174
+ com o painel lateral aberto: o Tab passa por ela, o foco não é puxado de volta e o Esc age onde a
175
+ pessoa está. Ela recebe o lado do Drawer em `data-modal-side` para sair da frente dele.
173
176
  - O botão de fechar tem o nome acessível `Fechar`. Botões só com ícone no cabeçalho precisam de
174
177
  `aria-label`.
175
178