Skip to main content
Os agentes da sua empresa trabalham sobre o estado dela: uma venda, um processo, uma proposta, uma internação, um pedido, um item na prateleira. Um tipo de objeto descreve uma dessas coisas uma vez só, para que a memória e todo agente a leiam do mesmo jeito: quais campos ela tem e quão certo é cada valor, de onde cada valor vem e qual fonte vence, quão velho um valor pode ficar antes de não poder mais ser afirmado, qual é o ciclo de vida, quando os temporizadores disparam e o que quem lê pode perguntar dela. Um tipo é declarado pela sua empresa, ou derivado do esquema do seu próprio banco e confirmado por uma pessoa. Ele sempre espelha um sistema seu: a memória reflete esse sistema e aponta a deriva, e nunca é a barreira que impõe as regras da empresa. O valor oficial continua no seu sistema. Esta página cobre o registro de tipos e as leituras de estado tipado. Os objetos de negócio sem tipo declarado continuam como sempre: estado derivado dos eventos de sistema, com as_of e a linha do tempo.

Ligar

O estado tipado é a funcionalidade state do espaço. Tudo começa desligado: o documento de configuração features lista o que está ligado, e é alterado por um diff aprovado no Console ou pela API de controle, pelo papel security. Num espaço sem a funcionalidade, as rotas desta página respondem 404, como se não existissem, e uma leitura de contexto que pede o bloco state recebe o contexto sem ele. GET /v1/sdk/profile anuncia ao SDK o que está ligado. Os tipos ficam no documento object-types, do papel integration. Uma mudança de sensibilidade, acesso ou dado pessoal de um campo, da licença de uma fonte ou das finalidades de um tipo é uma decisão de privacidade: além de integration, ela pede a aprovação do papel security.

O formato de um tipo

Um tipo é um documento JSON no documento object-types do espaço. Os membros principais: O formato completo, com a validação além do esquema e a linguagem de expressões, está na especificação aberta spec/object-type.md, no repositório público niadra-spec. Um exemplo curto, um item de loja compartilhado:

Declarado ou derivado

Uma empresa que guarda o estado dela em PostgreSQL já escreveu boa parte de um tipo: as colunas, os valores que um CHECK permite, as enumerações, as chaves estrangeiras. O comando niadra types derive dos SDKs lê esse catálogo dentro da sua empresa, propõe o tipo e lista o que uma pessoa precisa olhar antes de enviar (colunas que não viraram campo, CHECKs que não são uma lista, gatilhos, chaves estrangeiras sem relação). O catálogo, a proposta e a revisão nunca saem da sua empresa pela ferramenta; só a impressão digital do esquema, um SHA-256 sobre o catálogo normalizado, e as contagens do que mudou. Veja Derivar tipos do seu banco. Um tipo derivado guarda em mirror_of.fingerprint a impressão digital do catálogo que leu. O mesmo comando, com --check, lê o catálogo de novo e diz se ele derivou: campos acrescentados, removidos ou retipados, estados e relações que mudaram, a chave que mudou. Sem --no-send, ele reporta a impressão digital e as contagens a POST /v1/types/fingerprint: num tipo com drift: alert, a Niadra abre um problema de dados do tipo drift, ou conta mais uma ocorrência no que já está aberto, e envia o webhook type.drift uma vez por problema, quando ele abre; num tipo com drift: ignore, ela responde drift: true e não abre nada. A Niadra também aponta deriva sem a ferramenta, por observação: uma transição vista nas fontes que o tipo não declara, ou um valor fora do vocabulário, abre o mesmo problema de dados, e nunca recusa o que o seu sistema fez. Essa observação cobre os objetos de cliente e o que as ferramentas mostram; um push de objeto compartilhado é decidido na cópia quente, sem essa conferência.

Quatro valores lógicos

Todo valor carrega um de quatro valores lógicos, e “não conferido” nunca vira “não”: unobserved e known_defect são desconhecidos. Uma comparação com um valor desconhecido é desconhecida, e a negação dela também. Um campo logic: "tri" declara quem pode observá-lo (machine, human ou source:<nome>), sob qual regra, por quanto tempo a observação vale e quem sobrepõe quem: a conferência de uma pessoa vale para sempre e vence a da máquina, quando o tipo diz isso. O mesmo campo declara o que um valor que ninguém observou bloqueia: model_read, derive, claim ou uma tarefa sua, como decide:close. A Niadra aplica dois desses bloqueios sozinha, claim e decide; os outros chegam em blocked na leitura, para o seu código agir.

Frescor e a finalidade da leitura

Cada campo tem uma classe de frescor e, se quiser, uma idade máxima e uma idade máxima para afirmar. Sem max_age, um campo volatile fica velho depois de 30 segundos, price depois de 60 segundos, semi depois de 1 hora e stable depois de 7 dias; um campo none nunca envelhece. O frescor nunca é guardado: cada leitura o calcula contra a declaração em vigor, então uma declaração nova vale para todos os objetos de uma vez. Toda leitura diz a finalidade: display (o padrão), claim ou decide. Uma leitura nunca recusa um valor velho: ele vem com a idade, e o que muda é o que vem junto. Um valor só é seguro para afirmar (claim_safe) quando o tipo permite afirmá-lo, o valor lógico é yes ou no, o status é fresh dentro de claim_max_age, ele veio de uma observação live ou do próprio sistema de registro (nunca de um snapshot ou de um cache), não está mascarado, o conteúdo dele está limpo, o objeto derivado não venceu por insumo e nenhum campo desconhecido bloqueia claim. As proibições chegam em toda leitura em que um valor afirmável servido não é seguro, e claim_safe diz qual.

O que uma leitura devolve

POST /v1/state/read recebe as referências dos objetos (type, namespace, id, e a variant quando a chave tem uma), os campos e as leituras nomeadas que quer, e a finalidade. Cada objeto volta com state, latches, outcome, axes, fields, values, timers, readings, prohibitions, declared_gaps, withheld, refetch, refusal e blocked; cada campo, com v, logic, status, age_s, observed_at, src, observer, valid_at, known_at, claim_safe, e was quando o tipo acompanha mudanças. O esquema é object-state.v1, público em niadra-spec. Uma leitura com o estado aquecido não consulta o banco. known_at é quando a Niadra soube do valor pela primeira vez e nunca se move, mesmo quando uma revisão muda o valor: o que é novo continua novo. valid_at é quando o valor passou a valer no mundo, no eixo de tempo que o tipo declara para ele. Um valor atrasado só move um campo quando o valid_at dele é mais novo que o do campo, e entra na história do objeto mesmo assim. Nos SDKs, o caminho mais curto é o bloco state do contexto, lido na mesma ida e volta que o pacote:
O bloco state do contexto usa a finalidade display: os objetos do cliente nos tipos declarados, os objetos compartilhados em que ele mostrou interesse, o que mudou desde que ele os viu (changes_since_seen, contra os valores que a interface mostrou) e text, a view em linhas curtas para o bloco do turno, depois de slots e nunca dentro do corpo fixado do contexto, então o prefixo em cache mantém os bytes. Veja O contexto e as views. POST /v1/state/view devolve a mesma view sozinha, para a finalidade que você pedir. O bloco state passa pelo mesmo portão de verificação do contexto que o acompanha: um objeto do cliente só entra onde a linha de sistema dele entraria, pela mesma política, e os interesses e o que mudou desde que ele os viu só entram quando o contexto não reteve nada até a identidade ser mais verificada, porque um bloco não sabe dizer de que conversa uma linha veio. Numa conversa em V0, ou numa que ainda não provou o nível que a política pede, o bloco volta sem nada do cliente. POST /v1/state/view e POST /v1/state/read são leituras de programa, com comprovante próprio e sem contexto ao lado, e não passam por esse portão. Veja Blocos por include. degraded: true diz que o estado veio de uma alternativa (o banco quando a cópia quente não respondeu, a cópia local do SDK quando a Niadra está fora), com as idades reais. Uma falha nunca vira unobserved. O mesmo código, com o worker e a conferência de uma afirmação, está em examples/object_state.py e examples/object-state.ts.

Valores computados e objetos derivados

Um prazo, um preço ou uma carência é computado pela regra da sua empresa, nomeada por referência (prazo_forense@v9): a Niadra guarda o nome da regra, nunca a lógica dela. O valor entra como um fato com versões: uma versão nova nasce quando o valor, a regra ou o hash dos insumos muda, nomeia a versão que substitui e sai como o evento object.value_revised, com a referência do objeto, o nome do valor e as versões, nunca o valor. Cada valor declara as lacunas da regra (declared_gaps, como um feriado municipal ou uma portaria de tribunal), que um agente precisa dizer quando o afirma, e para que lado elas erram (gap_effect). Uma ausência tem nome (absent_as, como sem_prazo): nunca zero, nunca nulo. Um tipo derived nomeia os insumos dele, campos de outros objetos ou parâmetros do turno (lead.city, turn.product_type). Quando um campo que é insumo de um objeto derivado muda, todo objeto que depende dele passa a expired_by_input num passo só, expired_by nomeia os insumos e o evento object.expired_by_input avisa. Um objeto vencido nunca tem valor seguro para afirmar, e uma leitura decide dele leva a recusa. Uma cotação vence no instante em que um dado dela muda, não quando o prazo passa. Uma nova leitura do objeto derivado depois do vencimento o traz de volta a current, preso aos insumos que nomeia.

Ciclo de vida e temporizadores

O estado é o state mais novo reportado no mundo, traduzido pelo vocabulário da fonte (lifecycle.outcome.vocabularies). Um estado que o tipo não declara não é servido. As travas (latches) guardam a primeira vez que o objeto chegou a um estado, mesmo quando ele sai dele: “já foi pago” e “está pago agora” são duas leituras do mesmo objeto. O desfecho é o estado quando ele é final, expired_without_outcome quando o prazo do tipo passou sem estado final, ou o estado provisório; é o que a medição de desfechos lê. Uma transição com erase_derived lacra o objeto: os campos nomeados saem quando ele entra no estado, e toda leitura leva blocked para model_read e derive. A Niadra não impõe o ciclo de vida: a declaração de um agente de uma transição que ele não pode fazer fica registrada como recusada, e uma transição de uma fonte fora da declaração é deriva, apontada e nunca recusada. Um temporizador é armado no momento que a expressão due dele dá, a partir da regra e dos valores do momento, e se move quando esse momento se move; o vencimento é recalculado em toda leitura. Ele dispara uma vez por disparo, com até 5 minutos de atraso (antes quando vence dentro da hora), e um disparo atrasado diz quanto. Um temporizador que notifica manda o webhook object.timer_due, com o objeto, o temporizador, o id do disparo, o que ele substitui, quando venceu e quando disparou. Quando um valor que armou um temporizador já disparado é revisado para outro vencimento, o temporizador dispara de novo, e o disparo novo nomeia o que substitui: um aviso por fato e por versão. Um temporizador também pode pedir a releitura de um campo (refresh(<campo>)) ou fazer uma transição que o relógio pode fazer.

Objetos compartilhados

Um tipo shared (um item, um hospital da rede, uma vara) existe na Niadra só enquanto algo o referencia. Um objeto entra no conjunto de trabalho quando um turno o apresenta, o cliente interage com ele, o observa ou se compromete com ele, conforme working_set.enter_on, e sai depois de leave_after sem referência, a menos que um watch o segure. Um watch é o pedido explícito de um cliente para ser avisado quando um objeto compartilhado atende a uma condição (uma interação watch, com o evento de consentimento dela e uma expressão sobre os campos, como available == yes); quando uma escrita move o objeto e a condição vale sobre um valor que veio ao vivo da fonte, sai object.watch_fired, com o objeto, o watch e revalidated, nunca um valor, no máximo uma vez por hora por watch. Um interesse inferido nunca gera aviso. Quando a condição vale sobre um valor que não veio ao vivo da fonte (um snapshot, um cache, a observação de uma ferramenta), a Niadra não avisa ainda: ela pede ao seu worker de resolução uma releitura do objeto, com o motivo watch_revalidation, e o push do worker com o request_id do pedido decide, mesmo quando os valores não são mais novos que os guardados: object.watch_fired sai com revalidated: true só quando a condição continua valendo sobre os valores ao vivo. Sem valor fresco (nenhum worker tomou pedidos nos últimos dez minutos, o orçamento recusou, o worker liberou o pedido ou ele foi abandonado), refetch.unconfirmed_watch do tipo decide: fire, o padrão, avisa com revalidated: false; drop não avisa. Um tipo sem refetch, ou que lista watch_revalidation em never, não pede nada, e fire vale na hora. O sistema de registro envia o estado por POST /v1/objects/push: até 1.000 itens, cada um com a referência, a versão da fonte, os campos e a proveniência (live, snapshot ou cache, com source_observed_at). Cada campo guarda, por fonte, a última observação; um campo só avança quando a versão do item é maior que a que o escreveu por último, então um pedido repetido é stale_version, e uma fonte nunca reusa uma versão para outro conteúdo. Um item de objeto compartilhado é decidido contra a cópia quente do conjunto de trabalho, sem comando no banco (applied), e gravado em até um segundo com a mesma regra; um objeto fora do conjunto é out_of_set, e um push nunca o traz para dentro. Quando a cópia quente não responde, o push responde 503 com Retry-After: nada foi guardado, e repetir é inofensivo pela regra da versão. Um item de objeto de cliente é recorded: aceito como evento de sistema num comando só. POST /v1/objects/snapshot reconcilia um tipo compartilhado inteiro, em NDJSON, com a proveniência snapshot, que pode ser mostrada e nunca afirmada. A leitura serve o valor que a union do tipo escolhe (first_authoritative_live, first_by_precedence, union, min, max, latest ou divergence, que avisa quando duas implementações de uma regra discordam). O que o resultado de uma ferramenta mostrou também é estado, quando o registro do turno traz a proveniência: a ferramenta é a fonte, e a hora em que a fonte dela observou o valor é a versão. Uma observação sem proveniência é só para exibição; uma de escopo customer ou context nunca alimenta um objeto compartilhado; um acerto de cache dentro da ferramenta não é observação nova; um objeto que faltou num resultado não é um estado, porque a ausência não prova nada.

Releitura pelo seu worker

A Niadra nunca chama um sistema seu. Quando um valor precisa ser lido de novo (uma afirmação espera por ele, um temporizador venceu, alguém observa o objeto), ela avalia os refetch.reasons do tipo, admite o pedido dentro do orçamento (um por objeto e motivo no período, o min_interval do tipo, o orçamento pago da unidade da empresa, reservado antes) e o deixa em GET /v1/state/refresh-requests. O seu worker de resolução toma os pedidos, lê cada objeto com a função sua daquele tipo e envia o que leu por POST /v1/objects/push, o que encerra o pedido. O orçamento é na unidade da sua empresa (uma chamada paga), nunca em dinheiro. Veja O worker de resolução.

Conteúdo de terceiros

Um campo de conteúdo (content.fields) guarda o texto de um terceiro: o despacho de um tribunal, um documento, um campo livre de um sistema. Nenhum conteúdo é instrução. Regras determinísticas triam o texto na chegada: um texto que fala com um modelo (pede que ele ignore o que lhe disseram, fala como o sistema ou o assistente, escreve os marcadores do envelope) fica flagged; o que passa fica clean, a menos que o tipo peça scan: required, e então fica pending até o modelo de decisão liberá-lo ou marcá-lo, num lote a cada 30 segundos. Conteúdo guardado só por ponteiro ou digest fica pending quando a triagem é exigida, porque a Niadra não consegue lê-lo. Conteúdo marcado nunca chega a uma leitura, nem o pendente sob triagem exigida: o campo sai e withheld o nomeia com scan, e um campo cujo conteúdo não está limpo nunca é seguro para afirmar. Onde um conteúdo limpo entra num texto que um modelo lê, ele vai dentro de um envelope <niadra-data n="..."> com um nonce de 16 caracteres hexadecimais, derivado por HMAC com uma chave do espaço, e escapado, então ninguém de fora do espaço prevê o marcador de fechamento. O evento content.flagged nomeia o objeto, o campo e a referência, nunca o texto, e uma pessoa do papel security libera um conteúdo por POST /v1/content/{ref}/release, com motivo e comprovante. Num espaço que guarda conteúdo por ponteiro (content.mode: pointer), o texto fica no seu armazenamento, e o SDK o recompõe dentro da sua empresa com o resolvedor de conteúdo (niadra.content em Python, ContentResolver em TypeScript). Veja Só metadado.

Acesso por campo

Cada campo pode declarar sensitivity (as categorias sensíveis fixadas pelo produto, as mesmas da política), pii e regras de access: quem lê (fontes, agentes ou public), para quais finalidades e com que efeito (allow, mask, deny). Um campo mascarado para quem lê vem com masked: true e sem v; um negado fica em withheld com o motivo access. Um campo declarado pii, com sensitivity ou com regras de access é privado: ele nunca entra nas linhas de sistema do contexto, nas linhas de evento de sistema do histórico, no vetor pelo qual um objeto é buscado nem no text do bloco state. Um agente que precisa dele o lê por POST /v1/state/read, com a finalidade da leitura, sob o acesso por campo. Numa ferramenta sua, mask_output (@Niadra.tool(mask_output=True) em Python, niadra.tool(name, fn, { maskOutput: true }) em TypeScript) tira do que chega ao modelo os campos que a chave não pode ler: deny remove, mask mascara, pelo field_access do perfil do SDK. O último perfil lido continua valendo com a Niadra fora do alcance, e on_unknown="block" (onUnknown: "block") retém a saída inteira quando nenhum perfil foi lido. Sem mask_output no código, vale capabilities.mask_output da vinculação da ferramenta servida no perfil. O exemplo está em examples/masked_tool.py e examples/masked-tool.ts. Uma fonte pode declarar licence: uso comercial permitido ou proibido e as finalidades de que um valor dela fica excluído. O registro lista em commercial_purposes as finalidades que contam como uso comercial, e um valor excluído fica em withheld com licence. Uma mudança de licença ou de commercial_purposes é uma decisão de privacidade, e pede também o papel security.

Cobertura

GET /v1/objects/coverage diz, por tipo declarado, a fração dos objetos que traz cada campo e a idade mediana da observação mais nova dele, medida sobre os objetos do tipo que mudaram por último e só pelos carimbos, nunca um valor. É o que a tela Tipos do Console mostra, com os tipos declarados e derivados, a deriva e a cobertura; pede o papel integration.

Problemas de dados

Quando um replay atribui uma falha ao dado, quando duas fontes de um valor discordam, quando um tipo derivou ou quando uma ferramenta mostra objetos de um tipo que o espaço nunca declarou, a Niadra abre um problema de dados para o dono do dado: um por classe, tipo, campo e fonte, contado cada vez que é achado de novo, com até 50 referências de objeto e nenhum valor. Os tipos são null_field, out_of_vocabulary, stale_source e invalid_value (um valor que a ferramenta de um agente mostrou), coverage_drop (um campo preenchido menos do que antes), divergence (duas fontes de um valor discordam), drift (o esquema que um tipo espelha mudou, pela conferência da ferramenta ou por observação), rule_conflict (duas regras decidem uma coisa de jeitos diferentes) e type_undeclared (ferramentas mostraram objetos de um tipo que o espaço nunca declarou, então eles não viraram estado). GET /v1/data-issues lista do mais novo para o mais antigo, com occurrences, opened_at, last_seen_at, o tipo, o campo e a fonte; POST /v1/data-issues/{issue_id}/ack reconhece, e a próxima ocorrência abre um problema novo. Os webhooks data_issue.opened e, na deriva conferida pela ferramenta, type.drift avisam quando um abre, e o feed de avisos traz os mesmos eventos para quem puxa. Os problemas de dados pedem o papel integration e a funcionalidade turns ou state.

O que fica no registro

A declaração de um tipo vale para o que já foi gravado: a qual fonte declarada uma observação pertence é decidido na leitura, então uma declaração que nomeia uma fonte depois se aplica ao que foi guardado antes dela. Um objeto de um tipo que o registro não declara continua legível pelas rotas de objeto, com a union latest. Toda leitura de estado deixa um comprovante, com a versão e as contagens, nunca um valor. Apagar um cliente apaga os objetos dele, as observações e as inferências; um objeto compartilhado é de ninguém e fica enquanto estiver no conjunto de trabalho.

Próximos passos

Derivar tipos do seu banco

niadra types derive, a revisão e a conferência de deriva.

O worker de resolução

as releituras que a Niadra pede e o seu código faz.

Afirmações

o contrato que confere o que o agente diz contra o estado.

Ler objetos tipados

a referência de POST /v1/state/read.