Orkas Orkas
Início Blog Arquitetura
Arquitetura

A camada que transforma um modelo num produto: Agent Harness da engenharia do Orkas

Como o Orkas transforma chamadas de modelo num ambiente de execução fiável de agentes para computador: ciclos de execução em streaming, encaminhamento de ferramentas, compactação de contexto, abstração de fornecedores, memória e sessões resistentes a falhas.

Qualquer pessoa que tenha lançado um produto de agentes conhece a sensação: pôr uma demonstração a funcionar é rápido, mas transformá-la em algo em que o utilizador confia todos os dias — algo que funciona o dia todo na sua própria máquina sem falhar — é difícil. E a parte difícil não é ligar o modelo. É toda a camada que o rodeia.

Essa camada tem vários nomes; o que usarei é Agent Harness. Fica entre o "grande modelo" e as "funcionalidades do produto" e é o verdadeiro ambiente de execução: transforma um único pedido do utilizador em sucessivas rondas de conversa com o modelo, encadeando chamadas de ferramentas entre elas, fornecendo os resultados, compactando o contexto antes de este transbordar, repetindo tentativas perante falhas de rede e recuperando a conversa mesmo após o processo bloquear. O modelo pensa; o harness faz com que esse pensamento se transforme numa sequência fiável de ações.

O Orkas é uma aplicação de agentes para computador que é executada na própria máquina do utilizador e funciona inteiramente no cliente. Este artigo explica como essa camada é construída: como se divide em camadas, como funciona o ciclo de execução, como as ferramentas e os modelos são abstraídos e como a memória e as sessões são tratadas. Os detalhes do código foram omitidos e generalizados, mas a estrutura de engenharia é real.

Em resumo O harness é a parte que realmente instala Tudo o que está aqui descrito está incluído na aplicação para computador — a mesma camada que executa os seus agentes localmente, com o código no GitHub.
Descarregar o Orkas — grátis

As camadas

Disponha um produto de agentes na horizontal e obterá aproximadamente estas camadas, empilhadas de baixo para cima:

┌───────── ────────────────────────────────┐
│ Recursos do produto (chat/habilidades/conectores/sincronização) │
├──────────────────── ─────────────────────┤
│ Agente Harness (loop de execução / ferramentas / sessão) │
├──────────────────── ─────────────────────┤
│ Abstração de provedor (unificar muitos fornecedores LLM) │
├──────────────────── ─────────────────────┤
│ Infraestrutura (tipos/erros/registro/configuração) │
└─────────────────────────── ──────────────┘

Há uma escolha importante aqui: toda a inferência do modelo acontece no cliente. A aplicação para computador não é um cliente ligeiro – ​​detém o controlo e chama o modelo diretamente. O servidor trata apenas das contas, da sincronização entre vários dispositivos e da faturação; não executa o agente. Essa decisão moldou quase tudo o que veio a seguir: as sessões são guardadas no disco local, as ferramentas operam diretamente no diretório de trabalho do utilizador e os dados confidenciais nunca saem da máquina.

O harness divide-se em algumas partes: o ciclo de execução (executor), a sessão, as ferramentas, a camada de fornecedores e a memória. Vamos analisá-las uma de cada vez.

O ciclo de execução: um gerador de streaming

O núcleo do harness é o executor. Numa frase, o que faz é: falar com o modelo repetidamente até que este diga "Terminei".

É implementado como um gerador assíncrono, e essa escolha é importante. A execução de um único agente é muito mais do que "enviar um pedido e aguardar um resultado". Muita coisa acontece entretanto - o modelo está a emitir tokens, quer chamar uma ferramenta, a ferramenta terminou, o contexto cresceu o suficiente para acionar a compactação, a rede falhou e estamos a tentar novamente. Com funções de retorno ou promessas simples, é difícil apresentar estes estados intermédios de forma clara a quem faz a chamada. Com geradores, todos eles se tornam um fluxo de eventos emitidos por yield:

type AgentRunEvent =
  | { type: "text_delta"; text: string }              // model emitting tokens
  | { type: "tool_start"; name: string; input: unknown } // a tool starts executing
  | { type: "tool_end"; name: string; result: string }   // a tool finished
  | { type: "compaction"; tokensBefore: number; tokensAfter: number } // context compacted
  | { type: "retry"; attempt: number; reason: string }   // error, retrying
  | { type: "done"; result: AgentRunResult }             // terminal

A interface de utilizador subscreve esse fluxo de eventos e apresenta a saída do modelo e a execução da ferramenta em tempo real. O ponto de entrada sem streaming consiste internamente apenas em "consumir o fluxo até ao fim, obter o done" — ambos os pontos de entrada partilham uma implementação, pelo que não existe um segundo percurso de código que possa ficar dessincronizado.

O que acontece dentro de um turno

Passo a passo, um turno funciona mais ou menos assim:

  1. Envie a mensagem do utilizador (possivelmente com imagens) para o histórico da sessão.
  2. Construa o prompt do sistema, incluindo as ferramentas atualmente disponíveis, o índice de competências e assim por diante.
  3. Analise a string do modelo e associe-a a um fornecedor concreto e a um ID de modelo.
  4. Converta todas as ferramentas em definições que o modelo compreenda e envie-as juntamente com o histórico.
  5. Consuma o fluxo de resposta do modelo, yieldtokens de texto um a um enquanto recolhe quaisquer chamadas de ferramentas feitas pelo modelo.
  6. Quando o fluxo terminar, verifique o motivo de paragem do modelo:
  • Se for tool_use, o modelo quer chamar uma ferramenta - execute as ferramentas, depois volte à etapa 5 e volte a consultar o modelo.
  • Caso contrário, o turno terminou - construa o resultado, yield done e termine.

Há uma invariante que deve manter aqui: cada chamada de ferramenta que o modelo faz deve ser imediatamente seguida no histórico por um resultado de ferramenta correspondente. A API do modelo impõe rigorosamente esse emparelhamento – viole-o e o pedido seguinte produzirá um erro ou simplesmente bloqueará. Voltaremos a isto quando falarmos da recuperação automática da sessão.

Como uma chamada de ferramenta é encaminhada de volta

O modelo não executa ferramentas por si só; diz apenas "Gostaria de chamar read_file com estes argumentos." Assim que o executor detetar essa intenção:

for (const call of toolUseBlocks) {
  yield { type: "tool_start", name: call.name, input: call.input };

  const tool = this.tools.get(call.name);
  const ctx = { workingDir, signal, state: { sandboxEnv } };
  const result = await tool.execute(call.input, ctx);

  // append the result to the session as a tool-result message
  session.addToolResult(call.id, result);

  yield { type: "tool_end", name: call.name, result: result.content };
}

As ferramentas são executadas sequencialmente, os resultados são registados no histórico pela ordem em que o modelo os declarou e, em seguida, o modelo é novamente consultado com esses resultados disponíveis. Ao ver os resultados, o modelo pode chamar outra ferramenta ou simplesmente dar a resposta final. Esse ciclo "perguntar → chamar → responder → perguntar novamente" é precisamente o que permite a um agente concluir tarefas com várias etapas.

Um detalhe merece destaque: algumas ferramentas devolvem imagens — capturas de ecrã, imagens geradas. Mas muitos modelos não aceitam imagens no canal de resultados de ferramentas. O Orkas trata esta situação separando a imagem numa mensagem do utilizador colocada após o resultado da ferramenta — o modelo lê primeiro "a ferramenta devolveu este texto" e, no turno seguinte, vê a imagem correspondente. Um pequeno compromisso que contorna as diferenças de capacidade entre os fornecedores.

O que fazer quando o contexto está prestes a transbordar

O limite que as tarefas longas atingem com maior frequência é a janela de contexto. O Orkas não espera até que esteja cheia — define um limiar de 60%: após cada ronda de ferramentas, estima quanto da janela os tokens atuais ocupam e, quando essa ocupação ultrapassa os 60%, aciona proativamente a compactação.

A própria compactação pede ao modelo que resuma a conversa anterior e, em seguida, substitui as mensagens antigas por esse resumo, mantendo apenas a parte final mais recente. Parece simples, mas há uma armadilha: após a substituição, a parte mantida não pode começar com um "resultado de ferramenta órfão" - não pode haver um "resultado sem chamada correspondente", caso contrário volta a violar-se a invariante de emparelhamento. Por isso, a lógica de compactação garante que o corte ocorre num ponto seguro.

Há uma escolha mais interessante que vale a pena analisar aqui: porquê a abordagem pouco refinada de "resumir todo o bloco aos 60%", em vez de algo mais sofisticado - atribuir uma pontuação a cada mensagem e cortar por importância, extrair de forma estruturada as saídas das ferramentas, manter uma árvore de memória em camadas? Essas abordagens parecem ótimas nos artigos, mas optámos deliberadamente por não seguir esse caminho, por três motivos.

Primeiro, armazenamento em cache. A cache de prompts do modelo funciona por prefixo: desde que o prefixo do histórico permaneça inalterado, esse segmento é obtido da cache, poupando dinheiro e reduzindo a latência. A compactação refinada reescreve constantemente o meio do histórico, o que significa invalidar repetidamente o prefixo em cache – cada edição obriga a um grande reprocessamento. A estratégia "deixar como está e depois compactar uma vez ao atingir o limiar" mantém o prefixo estável na grande maioria dos turnos, com apenas uma compactação a invalidá-lo. Muito mais favorável à cache.

Em segundo lugar, complexidade. A invariante de que “cada chamada de ferramenta deve ser emparelhada”, que continuamos a salientar – quanto mais minuciosamente se aparar o histórico, maior será a probabilidade de a violar nalgum ponto. Um resumo pouco refinado só precisa de garantir um ponto de corte seguro; há uma ordem de grandeza menos locais onde errar. Menos uma classe de casos extremos significa menos uma classe de incidentes em produção.

Terceiro, aproveitar os dividendos de modelos melhores. As janelas de contexto têm crescido continuamente nos últimos anos, e os modelos lidam cada vez melhor com contextos longos. Investir esforços hoje num algoritmo de compactação elaborado é essencialmente combater um problema que está a diminuir – é provável que acabe de o ajustar quando a geração seguinte duplicar a janela e a sua complexidade passar a ser apenas um encargo. Por outro lado, confiar o resumo ao próprio modelo produz automaticamente melhores resultados à medida que o modelo melhora: quanto melhor selecionar o que importa, maior será a qualidade do resumo, sem alterarmos uma linha. A complexidade que o modelo pode assumir por si é complexidade que não deveria ter de assumir sozinho.

A estimativa de tokens esconde um problema fácil de ignorar: o chinês. Estime o chinês com base nas intuições do inglês (aproximadamente um token por cada poucos caracteres) e obterá uma grande subestimativa. O Orkas pondera os caracteres CJK separadamente na sua estimativa; caso contrário, o limiar de uma conversa inteiramente em chinês estará incorreto e a compactação não será acionada quando deveria.

Erros e novas tentativas

Ao ser executado na máquina do utilizador e depender de um modelo externo acedido por API, o sistema encontra erros como regra, não como exceção. O executor agrupa-os em algumas classes e trata cada uma de forma diferente:

  • Repetível: limites de pedidos, tempos de espera excedidos, ligações interrompidas, 5xx. Espera exponencial com variação aleatória, limitada a 30 segundos; se for um limite de pedidos e o servidor tiver enviado retry-after, respeite-o.
  • Não é possível repetir: situações como falhas de autenticação - nenhuma quantidade de tentativas ajudará, por isso devolva o erro imediatamente.
  • Especial: contexto excedido. Tente compactar primeiro, repita a tentativa uma vez depois e só devolva um erro se continuar a falhar.

Há mais uma classe: "a própria ferramenta falhou." Isto não compromete o turno inteiro - uma falha da ferramenta é, por si só, informação para o modelo, que, ao ver "aquele comando falhou", pode perfeitamente tentar uma abordagem diferente. O harness distingue estes erros transitórios da ferramenta de falhas reais: não interrompe o fluxo nem os perde – aparecem nas estatísticas posteriores. (Estes dados alimentam posteriormente o mecanismo de autoevolução, que é o tema do próximo artigo.)

O sinal de cancelamento externo (AbortSignal) é verificado em cada ponto-chave. O utilizador clica em "parar" e o turno atual é interrompido imediatamente. Não é iniciada nenhuma nova tentativa.

Abstração de ferramentas: suficientemente simples para ser alargada

A interface da ferramenta é deliberadamente reduzida:

interface AgentTool {
  readonly name: string;
  readonly description: string;          // shown to the model
  readonly inputSchema: Record<string, unknown>;  // JSON Schema to constrain inputs
  execute(input: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
}

Uma ferramenta é apenas “um nome + uma descrição para o modelo + um esquema de entrada + uma função de execução”. Todas as funcionalidades integradas – ler um ficheiro, escrever num ficheiro, executar um comando shell, pesquisar e obter conteúdos na Web – implementam essa interface. A camada da aplicação para computador acrescenta um conjunto de ferramentas de âmbito local (pesquisa na base de conhecimento, geração de imagens, chamada de conectores externos), mas a interface é a mesma.

A vantagem de uma interface reduzida é que a origem de uma ferramenta é irrelevante para o executor: integrada, definida pelo utilizador ou carregada a partir de uma competência. Todas são do mesmo tipo, registadas num Map<string, AgentTool> e convertidas em definições legíveis pelo modelo a cada turno.

As ferramentas com efeitos secundários, como os comandos shell, passam por um executor isolado: limites de tempo, limites de comprimento da saída, uma lista de bloqueio de comandos e variáveis ​​de ambiente passadas separadamente, em vez de alterar o ambiente global do processo.

A camada de fornecedores: uniformizar vários modelos numa interface

As preferências de modelos dos utilizadores são muito variadas, e um produto não pode ficar dependente de um único fornecedor. Abaixo do harness, o Orkas estabelece uma abstração de fornecedores que unifica modelos de diferentes fornecedores numa única interface:

interface LLMProvider {
  readonly id: string;
  complete(params: CompletionParams): Promise<CompletionResult>;
  stream(params: CompletionParams): AsyncIterable<StreamEvent>;
  validateAuth(): Promise<boolean>;
}

O executor acima só comunica com esta interface; não sabe qual é o fornecedor subjacente. Um registo trata do encaminhamento pela string do modelo: um formato provider/model explícito é dividido diretamente; um nome de modelo simples é atribuído por prefixo. A autenticação (uma chave de API ou um token OAuth) também é gerida aqui, e um token OAuth expirado é renovado automaticamente.

Ao uniformizar vários modelos, a verdadeira dor de cabeça não é a conclusão do texto, mas os pontos em que a semântica dos fornecedores diverge. Dois exemplos que nos marcaram.

Um deles é preservar blocos de raciocínio entre fornecedores. Os modelos de raciocínio emitem uma série de conteúdos de “raciocínio”; alguns fornecedores cifram-nos e exigem que sejam reproduzidos literalmente, enquanto outros os representam com um conjunto diferente de campos. Se um utilizador mudar do fornecedor A para o fornecedor B a meio da conversa, a assinatura desse segmento de raciocínio no histórico deixará de corresponder. A solução é marcar cada mensagem do histórico com “qual o modelo que produziu isto”, para que a camada de transformação possa decidir se deve mantê-la literalmente: se for o mesmo modelo, mantém-na; se for um modelo diferente, converte-a para uma forma mais simples de acordo com as regras.

O outro é a cache de prompts. Nos turnos de uma sessão, o prefixo é altamente repetitivo e o armazenamento em cache permite poupanças significativas de custos e latência. A implementação passa o ID da sessão como chave de cache aos fornecedores que o suportam, tratando também dos limites de comprimento das chaves de cada fornecedor (truncando ou aplicando um hash se a chave for demasiado longa, por exemplo).

Tudo isto é trabalho pesado, mas é precisamente esta camada de trabalho pesado que permite ao executor acima fingir que "só existe um tipo de modelo".

Memória: dois mecanismos, cada um com a sua função

A "memória" no Orkas consiste, na verdade, em dois mecanismos paralelos que resolvem dois problemas completamente diferentes. Um deles é uma base de conhecimento assente na recuperação de informação, para grandes volumes de material que se “procuram quando necessário”. O outro é a memória entre sessões, para o pequeno conjunto de factos importantes que se “devem ter sempre em mente”. Muitos produtos misturam os dois; mantê-los separados torna tudo muito mais claro.

Base de conhecimento: recuperação híbrida

O primeiro mecanismo destina-se a grandes volumes de conteúdo que só ocasionalmente são relevantes – documentos do utilizador, notas anteriores, conhecimento do domínio. Trata-se de uma base de conhecimento local com recuperação vetorial, em dois backends: uma versão ligeira inteiramente em memória (para testes e utilização efémera) e uma versão persistida numa base de dados local (para produção, com indexação de texto integral e vetores).

Os dados chegam por este caminho:

documentos → pedaços de limites de linha (com sobreposição) → indexação dupla
                                          ├─ índice de texto completo (palavras-chave, sem custo de incorporação)
                                          └─ índice vetorial (se um modelo de incorporação estiver configurado)

Os blocos são cortados nos limites das linhas, com uma pequena sobreposição entre eles, para evitar dividir uma unidade completa de significado a meio. A recuperação é híbrida: uma pesquisa vetorial (proximidade semântica) e uma pesquisa por palavras-chave (correspondências literais), com os dois conjuntos de resultados combinados através de RRF (Reciprocal Rank Fusion):

score = Σ  1 / (k + rank_i)

Quanto mais elevada for a classificação de um resultado numa pesquisa, maior será a sua contribuição; ao somar as contribuições das duas pesquisas, respeita-se a relevância semântica sem perder correspondências literais exatas. Os pesos dos vetores e das palavras-chave são ajustáveis e favorecem a semântica por predefinição. Após a combinação, os resultados são desduplicados por "(documento, linha inicial)", mantendo apenas o melhor por localização e, em seguida, eliminando tudo o que fique abaixo de um limiar e devolvendo os K melhores resultados.

Porquê confiar apenas nos vetores? Porque a recuperação vetorial geralmente se baseia em nomes próprios, símbolos de código e strings literais exatas - consultas que não são semanticamente especiais, mas em que o texto literal é muito importante - enquanto as palavras-chave, por si só, não conseguem captar "o mesmo significado, com uma formulação diferente". Executar ambas as pesquisas é um compromisso muito prático entre a qualidade e o custo da recuperação.

Memória entre sessões: manter o utilizador em mente

A base de conhecimento resolve o problema de "muito material para guardar". Mas há outro tipo de informação – de pequeno volume, mas que deve estar sempre em mente: quem é este utilizador, o que prefere, o que foi acordado da última vez. Estes elementos não deveriam depender da recuperação para "ter a sorte de ser encontrados" — deveriam estar presentes em todos os turnos.

Para isso, o Orkas cria uma camada separada de memória entre sessões, dividida por conteúdo em duas partes:

  • Perfil do utilizador: factos estáveis ​​sobre a pessoa — função, preferências, estilo de comunicação, conjunto de tecnologias.
  • Notas de factos: factos duradouros ​​sobre o trabalho — decisões, marcos, convenções do projeto.

Ambos são pequenos, cada um com um limite rígido de alguns milhares de caracteres, o que os obriga a manter apenas o que é verdadeiramente útil a longo prazo. Não passam por recuperação; em vez disso, são fixados diretamente no prompt do sistema no início de cada turno – o que significa que o agente simplesmente “sabe” essas coisas, sem ter de se lembrar de as procurar. Esta é precisamente a abordagem oposta à da base de conhecimento: a base de conhecimento é “consultada apenas quando necessário e depois seguida”, enquanto a memória entre sessões está “sempre presente, sempre visível”.

As gravações passam por uma ferramenta de memória dedicada que o modelo chama quando considera, a meio da conversa, que "vale a pena lembrar isto a longo prazo", com suporte para adição, substituição de substrings e eliminação. O que guardar e o que não guardar está claramente definido na descrição da ferramenta: as correções e preferências do utilizador têm prioridade máxima; as decisões e convenções duradouras são guardadas; já o estado transitório da tarefa atual, informações pontuais de depuração e tudo o que seja facilmente redescoberto não são guardados - a memória serve para "factos duradouros ​​sobre o utilizador e o projeto", não para "onde cheguei neste momento".

Há um detalhe fácil de ignorar, mas muito importante: uma verificação de segurança é executada antes de cada gravação. Este conteúdo entra literalmente no prompt do sistema e persiste entre sessões durante muito tempo – na prática, uma superfície de injeção duradoura. Por isso, cada memória prestes a ser gravada no disco é primeiro verificada à procura de padrões suspeitos - frases clássicas de injeção de prompts ("ignorar todas as instruções anteriores" e semelhantes), comandos que tentam exfiltrar chaves, caracteres Unicode invisíveis escondidos no texto - e qualquer correspondência é rejeitada imediatamente. Com desduplicação e corte do que excede o limite, esta camada de memória permanece útil sem se tornar um risco.

Juntos, os dois mecanismos cobrem os dois extremos — "enorme, mas ocasional" e "pequeno, mas constante": a base de conhecimento trata do primeiro, e a memória entre sessões, do segundo. Acrescente-se a isso a compreensão que o agente tem de si mesmo (o tema do próximo artigo), e um agente Orkas começa com três tipos de memória ao mesmo tempo: sobre o material, sobre o utilizador e sobre si mesmo.

Sessões: preparadas para falhar, preparadas para recuperar

Uma sessão gere o histórico de mensagens. A versão básica é apenas um conjunto de mensagens em memória, com corte e compactação do histórico. Mas tudo o que é executado na máquina de um utilizador deve pressupor que pode ser encerrado a qualquer momento – o utilizador fecha a aplicação, o sistema reinicia, um limite de tempo do watchdog interrompe o processo. Por isso, em produção usa-se uma sessão persistente, gravada num ficheiro JSONL local, com uma mensagem por linha.

Existem duas estratégias de gravação: acrescentar uma nova mensagem usa uma operação atómica de adição; qualquer operação que reescreva o ficheiro inteiro (compactação, limpeza) usa "escrita num ficheiro temporário + mudança de nome atómica". Desta forma, mesmo que a energia seja cortada a meio da gravação, nunca fica metade de um registo corrompido.

A parte mais interessante é recuperar chamadas de ferramentas órfãs. Voltando à invariante de emparelhamento: o modelo faz uma chamada de ferramenta, o harness executa-a, o resultado é gravado - interrompa qualquer uma destas três etapas e fica um órfão no disco, "uma chamada sem resultado". Carregue essa sessão da próxima vez e envie-a tal como está para o modelo, e a API irá rejeitá-la ou bloquear.

A lógica de recuperação é executada sempre que uma sessão é carregada do disco e é idempotente:

  1. Verifique todas as mensagens do assistente e recolha os IDs das chamadas de ferramentas que contêm.
  2. Aguarde os resultados das ferramentas correspondentes.
  3. Para qualquer chamada sem resultado correspondente, sintetize uma chamada marcada como "interrompida".
  4. Ao longo do processo, alinhe a ordem dos resultados com a ordem de declaração das chamadas e elimine quaisquer resultados órfãos que não tenham uma chamada correspondente.

Após esta passagem, fica garantido que a sessão está num estado que cumpre os requisitos de emparelhamento da API e pode ser enviada em segurança. O mecanismo parece banal, mas é a rede de segurança que garante que "a conversa de um utilizador não fica bloqueada permanentemente apenas por causa de uma falha".

Algumas decisões que se revelaram importantes em retrospetiva

Juntando tudo isto, algumas decisões parecem especialmente valiosas em retrospetiva.

Geradores como interface principal. Os modos com e sem streaming partilham uma implementação, o estado intermédio surge naturalmente e a interface de utilizador pode apresentar tantos detalhes quantos desejar. Isto evitou toda uma classe de erros de inconsistência que teria resultado de "implementar primeiro o modo sem streaming e ativar o streaming depois".

Compactar aos 60%, não quando estiver cheio. Isto deixa espaço para a própria compactação (que também custa uma chamada de modelo) e evita improvisações de última hora.

A invariante de emparelhamento aplica-se a tudo. Do ponto de corte da compactação à gravação no disco e à recuperação durante o carregamento, todos os pontos que interagem com a sessão mantêm a mesma regra. Com uma única regra, nenhum ponto precisa de inventar a sua própria lógica de correção.

Trabalho pesado concentrado na camada de fornecedores. Todas as particularidades entre fornecedores — blocos de raciocínio, chaves de cache, diferenças de capacidade — são tratadas nesta camada, permitindo manter um executor simples acima. Se um dia acrescentar um novo fornecedor de modelos, a alteração quase não se propagará.

Para concluir

O harness do Orkas não tem nenhum algoritmo impressionante. O seu valor está em "fazer um agente funcionar de forma fiável num ambiente real" e dividir esse trabalho num conjunto de módulos com limites claros, cada um responsável por uma parte: o executor gere o ciclo e as novas tentativas, as ferramentas fornecem as capacidades, a camada de fornecedores uniformiza os vários modelos, a memória trata da recuperação de informação e a sessão trata da persistência e da recuperação de falhas. Nenhum deles é complexo por si só; só em conjunto sustentam algo que as pessoas usam todos os dias.

Se há uma lição a retirar: faça do ciclo de execução um gerador de streaming e o estado intermédio torna-se muito mais fácil de gerir; uma vez definida uma invariante principal (como "as chamadas de ferramentas devem ser emparelhadas"), mantenha-a de forma consistente durante a compactação, as gravações no disco e o carregamento - não permita exceções em nenhum ponto; concentre o trabalho pesado entre fornecedores numa camada e mantenha-o fora da lógica de negócio; e - o mais evidente de tudo - parta do princípio de que o seu processo será terminado no pior momento possível e prepare antecipadamente a recuperação para esse momento.

O próximo artigo aborda uma parte mais interessante do Orkas: como este agente aprende com a sua própria utilização, transforma a experiência em competências reutilizáveis ​​e se torna lentamente mais útil.