/** * Orchestrator — runtime de execução adaptativa. * * Fluxo: * Task → Classify → Plan (graph) → Route (score agents/skills/model) * → Execute nodes (serial/paralelo por batches) → Validate artifacts * → Evaluate → Heal (se falhar) → Reflect/Learn → Trace persistido. * * Este módulo é headless: a CLI (`izanagi run`) fornece o producer real * (prompt compilado) e o consumer dos artefatos; o runtime cuida do estado. */ import type { EvaluationReport, ExecutionGraph, GraphNode, HealingAction, RoutingHints, RunTrace } from './types.js'; import { TraceStore, Tracer } from './observability/tracer.js'; import type { IzanagiEvent } from './observability/events.js'; import { MemoryStore } from './memory/store.js'; import { PhaseTokenBudget } from './token/budget.js'; import { CheckpointStore } from './recovery/checkpoint.js'; import { ArtifactRegistry } from './artifacts/registry.js'; import { DecisionJournal } from './memory/decisions.js'; import { ApprovalStore } from './recovery/approvals.js'; import type { CommanderPlan, ReplanFailure, ReplanResult } from './orchestration/commander.js'; import { type AgentRole, type ExecutionMode, type TaskContract } from './contracts/task-contract.js'; import { type ResolvedContext } from './orchestration/context-resolver.js'; import { ExecutionBudget, type ExecutionBudgetLimits } from './token/execution-budget.js'; import { type SemanticJudge, type VerificationResult } from './verification/engine.js'; import { ConversationLog, type ConversationEntry } from './protocol/conversation.js'; import { ToolRegistry } from './tools/registry.js'; import type { PolicyEnvironment, TrustTier } from './security/policy.js'; import { type Critique } from './protocol/messages.js'; export interface OrchestratorOptions { baseDir: string; /** * Raiz do PROJETO do usuário. Default: o próprio `baseDir`, para que quem * já construía um `Orchestrator` continue com o comportamento exato de * antes — a CLI e o SDK declaram o valor certo, e nenhum caller existente * muda de comportamento sem pedir. * * Não é a mesma coisa que `baseDir`, e a diferença importa: `baseDir` é a * raiz do FRAMEWORK — `/.agents` num projeto inicializado, ou a * própria instalação do pacote quando não há uma — e é onde vive * `.izanagi/state`. Rodando de dentro do checkout do framework as duas * coincidem, que é exatamente por que a confusão passava despercebida: um nó * `fs.read` lia dentro de `.agents/` em vez do projeto, e um check * `file-exists` procurava o arquivo no lugar errado. * * Tudo que é do PROJETO (sandbox de tool, entrega, existência de arquivo) * resolve contra este diretório. Tudo que é do RUNTIME (trace, artefatos, * checkpoints, memória) continua em `baseDir`. */ workspaceDir?: string; /** * Onde vive o estado do run (`.izanagi/state`: trace, artefatos, memória, * checkpoints, aprovações, decisões). Default: `baseDir`, para que nenhum * caller existente mude de comportamento. * * Separado de `baseDir` porque as duas perguntas são diferentes: `baseDir` * responde "de onde leio agentes e skills?" e cai na instalação do pacote * quando o projeto não foi inicializado — o que está certo para assets e * errado para estado, que assim vazava entre projetos. */ stateDir?: string; command: string; task: string; category: string; primaryAgent: string; skillChain: string[]; /** Producer de artefato de um nó: recebe o nó e devolve conteúdo/resultado. */ /** * Executa um nó. `costUsd` é OPCIONAL e significa "custo medido pelo * executor" (hoje só o CLI de agente reporta, em `total_cost_usd`). * Presente, ele substitui a estimativa de `costOf` no orçamento: um * número medido vale mais que a mesma conta feita por tabela de preço. */ produce: (node: GraphNode, ctx: ExecuteCtx) => Promise | NodeProduction; /** Consumer de artefato validado (ex.: salvar em disco). */ consume?: (node: GraphNode, artifact: { kind: string; content: unknown; valid: boolean; }) => void; /** * Providers de LLM realmente utilizáveis no ambiente atual (ex.: com API key * configurada). Quando informado, restringe o catálogo do ModelRouter a * esses providers, então `ctx.provider`/`ctx.model` já saem prontos para uso * real — o caller não precisa rotear de novo nem aplicar fallback manual. */ availableProviders?: string[]; /** Retoma um run interrompido a partir do checkpoint salvo com este runId, em vez de planejar do zero. */ resumeRunId?: string; verbose?: boolean; /** Observador em tempo real do Event System (run.started, healing.*, quality_gate.*, ...) — ver observability/events.ts. */ onEvent?: (event: IzanagiEvent) => void; /** * Plano do Commander (modo + grafo + contratos + estimativa). Quando * presente, o Orchestrator executa ESTE grafo: nada de classificar e * planejar de novo. Ausente = caminho legado (Planner por categoria), * preservado byte-a-byte para quem já usa o Orchestrator direto. */ plan?: CommanderPlan; /** Tetos de execução (custo, tempo, tool calls, agentes, retries). */ budgetLimits?: Partial; /** * Allowlist de ids de tool para o run inteiro. Ausente (o default) significa * "sem allowlist": vale o que o contrato de cada tarefa autoriza, e nada * muda para quem já usa o Orchestrator. Presente, uma tool fora da lista * falha o nó ANTES de qualquer permissão ou política ser consultada. * * Lista VAZIA é uma allowlist vazia, não ausência: proíbe toda tool. A * diferença importa porque é a forma de declarar "este run não usa tool". */ allowedTools?: string[]; /** * Reaproveita artefato de run ANTERIOR quando a pergunta foi exatamente a * mesma (`artifacts/registry.ts`: `reuseKey` + `findReusable`). * * Opt-in, como o cache de resposta, e pelo mesmo motivo: reuso é a * otimização que, quando erra, erra em silêncio e devolve resposta velha com * cara de nova. A política de invalidação está declarada em `reuseKey()` e o * prazo em `DEFAULT_REUSE_MAX_AGE_MS`. * * Nó de tool NUNCA é reaproveitado: `fs.write` e `project.test` têm efeito * colateral, e reusar um "escreveu" significa não escrever. */ reuseArtifacts?: boolean; /** Prazo do reuso. Default: `DEFAULT_REUSE_MAX_AGE_MS` (7 dias). */ reuseMaxAgeMs?: number; /** * Cancelamento cooperativo do run. Conferido no topo de cada batch e * repassado ao `produce()` por `ExecuteCtx.signal`, que o cliente de modelo * combina com o timeout HTTP: a requisicao em voo aborta de verdade em vez * de continuar consumindo cota de um run que ninguem mais espera. * * Cancelar NAO e' falhar por bug: o checkpoint do ultimo batch concluido * segue em disco, e `izanagi resume ` retoma dali. */ signal?: AbortSignal; /** Juiz semântico opcional da Verification Engine. Sem juiz, critério semântico fica UNKNOWN. */ judge?: SemanticJudge; /** * Replanejamento pelo Commander: recebe o grafo atual e o DELTA da falha, e * devolve um Plano B (agente trocado, papel acima, tarefa quebrada). Ausente * = `Planner.replan` legado, que reabre o nó sem mudar nada da estratégia. */ replan?: (input: { graph: ExecutionGraph; failure: ReplanFailure; }) => ReplanResult | null; /** * Ambiente para a Policy Engine avaliar nós `kind: 'tool'`. Default * `development`: o mais permissivo, porque é onde o framework roda por * padrão. Quem executa em CI ou produção precisa declarar. */ environment?: PolicyEnvironment; /** * Trust tier de um agente, pela ORIGEM do arquivo dele (o * `AgentCapabilityRegistry` deriva do diretório). Sem esta função, um nó de * tool com agente declarado é tratado como `community` — o tier mais * restritivo —, porque presumir confiança não verificada é o erro caro aqui. */ trustTierOf?: (agentId: string) => TrustTier | undefined; /** * Profundidade máxima de sub-orquestração (default 2). Uma tarefa que * descobre, executando, ser maior do que o plano previu pode abrir um * subgrafo próprio — mas o teto é do runtime, não do agente: recursão * decidida por quem está dentro dela não tem fim. */ maxOrchestrationDepth?: number; /** * Onde gravar skill sintetizada de trajetória recorrente. Default: * `/skills/generated` — o projeto do usuário, que é onde a skill * precisa viver para ser encontrada depois. Explícito porque um run de teste * não pode escrever no repositório de quem roda o teste. */ generatedSkillsDir?: string; /** * Roteador por papel: devolve modelo/provider do papel de cada nó. Quando * ausente, todos os nós usam o modelo roteado uma vez para o run inteiro * (comportamento anterior). */ routeRole?: (role: AgentRole, node?: GraphNode, hints?: RoutingHints) => { model: string; provider: string; } | undefined; /** * Custo em USD de uma chamada. Sem esta função o runtime continua contando * tokens, mas o custo fica em 0 (não inventa preço de modelo desconhecido). */ costOf?: (modelId: string, inputTokens: number, outputTokens: number) => number; } /** O que um producer devolve para um nó do grafo. */ export interface NodeProduction { content: unknown; kind: string; tokens?: number; model?: string; /** Custo REAL da chamada em USD, quando o executor mediu. */ costUsd?: number; } export interface ExecuteCtx { runId: string; task: string; category: string; primaryAgent: string; skillChain: string[]; model: string; /** Provider do modelo roteado (ex.: 'anthropic') — mesma fonte de verdade do routing. */ provider: string; trace: Tracer; memory: MemoryStore; artifacts: Map; /** Token Budget 2.0 — orçamento por fase (planning/execution/evaluation/recovery). */ budget: PhaseTokenBudget; /** Índice rastreável de artefatos (quem criou, hash, dependências, versão). */ artifactRegistry: ArtifactRegistry; /** Human-in-the-loop: nós `kind: 'approval'` consultam este store. */ approvals: ApprovalStore; /** * Contrato do nó em execução (Commander). Ausente no caminho legado. * O producer usa para saber objetivo, restrições e critérios de aceite. */ contract?: TaskContract; /** Contexto mínimo já resolvido para o nó: objetivo + insumos resumidos. */ nodeContext?: ResolvedContext; /** Papel do nó em execução (commander/specialist/worker). */ nodeRole?: AgentRole; /** Budget Controller com custo, cache e escada de degradação. */ execBudget?: ExecutionBudget; /** * Cancelamento cooperativo do run. O producer deve repassá-lo ao cliente de * modelo (`CompletionOptions.signal`); ignorá-lo não quebra nada, mas deixa a * requisição em voo depois do cancelamento. */ signal?: AbortSignal; /** * Canal agente-a-agente do run. Toda mensagem carrega REFERÊNCIA de artefato, * nunca cópia de conteúdo: é o que separa uma equipe coordenada por contratos * de uma sala de reunião trocando textos longos. */ conversation: ConversationLog; /** Críticas já interpretadas, por nó crítico (saída de `parseCritique`). */ critiques: Map; /** * Profundidade de sub-orquestração da tarefa em execução. 0 é o grafo do run; * uma sub-tarefa aberta por decomposição roda em 1. */ depth: number; } export interface OrchestrationResult { trace: RunTrace; traceFile: string; graph: ExecutionGraph; evaluation?: EvaluationReport; healing: HealingAction[]; /** * `HUMAN_REQUIRED` não é um veredito de qualidade: é o run que esgotou um * teto DECLARADO (tentativas, tempo, tokens ou custo) e parou por isso. O * `evaluation.verdict` do mesmo run continua `FAIL`, porque a entrega não * fechou; o que muda é o que fazer a seguir. Diferente de `BLOCKED`, não é * retomável por `izanagi approve`: exige uma decisão sobre o teto. */ status: 'PASS' | 'PASS_WITH_WARNINGS' | 'FAIL' | 'BLOCKED' | 'HUMAN_REQUIRED' | 'UNKNOWN'; score: number; /** * Execução pausada aguardando decisão humana (`izanagi approve`/`reject`) — * quando presente, `status` é 'BLOCKED' mas não é um veredito final: o run * continua de onde parou assim que aprovado/rejeitado (via resumeRunId). */ pendingApproval?: { nodeId: string; context?: string; }; /** Modo executado (presente quando veio de um plano do Commander). */ mode?: ExecutionMode; /** Telemetria do Token Economy Engine. */ telemetry?: ReturnType; /** Verificação por nó (Verification Engine 2.0). */ verification?: Array<{ nodeId: string; result: VerificationResult; }>; /** Log do protocolo agente-a-agente (task/result/critique/correction). */ conversation?: ConversationEntry[]; } export declare class Orchestrator { private readonly opts; /** * Raiz do projeto do usuário, resolvida uma vez no construtor: o cwd do * processo pode mudar durante o run (um consumer que faz `chdir`), e a * sandbox de uma tool não pode depender de quando ela foi chamada. */ private readonly workspaceDir; /** Raiz do estado do run. Ver `OrchestratorOptions.stateDir`. */ private readonly stateDir; constructor(opts: OrchestratorOptions); /** Pontos de extensão (compatibilidade: permite injetar implementações). */ private store?; private memory?; private checkpointStore?; private artifactRegistry?; private decisionJournal?; private approvalStore?; private tokensUsed; private verifier?; private contextResolver?; private toolRegistry?; /** Verificação por nó (Verification Engine 2.0), preenchida durante a execução. */ private readonly verifications; /** * Juiz semântico desligado no meio do run por orçamento de avaliação * esgotado. A partir daí, critério semântico fica UNVERIFIED — o mesmo que * acontece quando não há juiz configurado, e pelo mesmo motivo: ausência de * julgamento não é aprovação. */ private judgeDisabled; /** Nós pulados por early stopping (objetivo já comprovado). */ private readonly earlyStopped; /** Nós pulados por pressão de orçamento (degradação drop-optional-tasks). */ private readonly budgetDropped; /** * Nós já reprovados UMA vez por crítica bloqueante. O crítico pode reprovar * um artefato e exigir correção, mas não pode reabrir o mesmo nó * indefinidamente: sem este teto, crítico e executor entram em ping-pong e o * orçamento vira o único freio. */ private readonly critiqueRounds; /** * Efeito ACUMULADO da escada de degradação. Cada passo aplicado muda um * destes campos, e o resto do executor consulta este estado. Sem isto, a * escada apenas registraria o passo sem mudar nada de fato na execução. */ private readonly degradation; setStore(store: TraceStore): void; setMemory(memory: MemoryStore): void; setCheckpointStore(store: CheckpointStore): void; setArtifactRegistry(registry: ArtifactRegistry): void; setDecisionJournal(journal: DecisionJournal): void; setApprovalStore(store: ApprovalStore): void; /** Injeta uma ToolRegistry (com PolicyEngine próprio, se for o caso). */ setToolRegistry(registry: ToolRegistry): void; /** Executa o ciclo completo e retorna trace + avaliação. */ run(): Promise; /** Monta o snapshot persistível do progresso atual — chamado a cada rodada de batches. */ private captureCheckpoint; /** * Executa os batches em ordem, respeitando paralelismo e retries. * Retorna a primeira falha não resolvida (ou null). * * `onBatchDone` persiste o progresso ao fim de CADA batch. O checkpoint era * salvo só depois de todos os batches terminarem, então um run interrompido * no meio do grafo (Ctrl-C, crash, queda de rede) descartava todos os nós já * concluídos daquela tentativa e o `izanagi resume` recomeçava a tentativa * inteira — pagando de novo chamadas de modelo que já tinham sido pagas. O * docstring de `captureCheckpoint` já dizia "chamado a cada rodada de * batches"; agora é verdade. */ private executeBatches; private executeNode; /** * Artefato de run anterior que responde exatamente esta tarefa, ou null. * * Nunca lança: reuso é otimização, e uma falha aqui não pode derrubar um nó * que teria executado normalmente. */ private tryReuse; /** Chave de reuso desta tarefa. Ver `reuseKey()` para a política. */ private reuseKeyFor; /** * Impressão do estado do projeto neste run: o checksum do artefato de * survey, quando houve um. * * Sem survey o run não declarou nada sobre o projeto, e a chave registra * isso como `sem-survey` — que é uma chave DIFERENTE. Reaproveitar entre um * run que leu o projeto e um que não leu seria assumir que o projeto não * importava para a resposta. */ private projectFingerprint; /** * Registra a trajetória do run e, quando ela já se repetiu o bastante, * sintetiza a skill procedural correspondente. * * A barra é RECORRÊNCIA, não sucesso. Sintetizar a cada run bem-sucedido * produziria uma biblioteca de skills genéricas que ninguém usa e que * competem com as boas no ranking — que é exatamente o motivo de esta ideia * ter ficado parada até existir um gatilho defensável. * * Nunca lança: aprender é efeito colateral do run, e falha aqui não pode * derrubar um resultado que já foi produzido e verificado. */ private recordTrajectory; /** * Executa o subgrafo pedido por um nó que descobriu, executando, ser maior * do que o plano previu. * * O que faz isto ser sub-orquestração e não uma colmeia: * - o pedido é validado estruturalmente antes de virar grafo; * - o orçamento de tokens é o DO PAI, dividido — decompor não libera gasto; * - a profundidade tem teto do runtime, não do agente; * - sub-tarefa não decompõe de novo (o contrato filho nasce com * `decomposable: false`); * - falha de sub-tarefa é falha do pai, e cai no mesmo healing. * * Devolve `null` quando o pedido é recusado — e nesse caso o conteúdo * original do nó segue para validação, que provavelmente vai reprovar: um * agente que respondeu com um plano em vez do artefato não entregou. */ private runSubgraph; /** * Executa um nó de tool com a política aplicada ANTES da execução. * * Este é o caminho que faltava: até aqui `Orchestrator.executeNode` sempre * chamava `opts.produce()` — uma chamada de LLM ou a simulação headless — e * NUNCA a `ToolRegistry`. As garantias de menor privilégio, trust tier e * sandbox existiam, eram testadas, e não se aplicavam a nada que o * `izanagi run` realmente executasse. * * Menor privilégio por construção: o `ToolContext` sai do CONTRATO da tarefa. * Contrato sem `permissions` executa tool nenhuma, e a `ToolRegistry` recusa * antes de a `PolicyEngine` opinar. O trust tier vem da origem do agente, não * do que ele declara sobre si. */ private executeTool; /** * Interpreta a saída de um nó crítico e transforma crítica em AÇÃO. * * Sem isto, o crítico produzia texto, o texto virava um artefato `critique`, e * ninguém o lia: a crítica adversarial custava uma chamada de modelo e não * mudava nada na execução. Aqui a crítica vira uma decisão determinística: * bloqueante reprova o artefato criticado e devolve a correção MÍNIMA (só os * problemas high/critical), sem reenviar histórico nenhum. * * Devolve a falha a propagar (do nó CRITICADO, não do crítico) ou null quando * a crítica não bloqueia. */ private interpretCritique; /** * Qual nó a crítica reprova. O crítico costuma nomear o artefato em * `issue.artifact`; quando esse nome bate com um nó do grafo ele é mais * preciso que a topologia. Sem nome utilizável, cai na dependência do * crítico — que é exatamente o que ele foi posto no grafo para revisar. */ private critiqueTarget; /** * Recomendações da avaliação final derivadas das críticas REALMENTE * interpretadas. Antes disto a checagem era `ctx.artifacts.has('critique')`, * que nunca era verdadeira: `critique` é o KIND do artefato, e a chave do mapa * é o id do nó (`critic`). A recomendação simplesmente nunca aparecia. */ private critiqueRecommendations; /** * Aplica UM degrau da escada de degradação. Cada passo muda o estado que o * executor consulta daqui para frente: contexto menor, saída menor, modelo * mais barato, menos paralelismo, tarefas opcionais cortadas, ou pausa para * decisão humana. O passo já veio marcado como consumido pelo Budget * Controller, então a escada nunca repete um degrau. */ private applyDegradation; /** Artefatos disponíveis para o Context Resolver, já com a referência do registry. */ private availableArtifacts; /** * Early stopping: pula um nó OPCIONAL quando aquilo que ele revisaria já * está comprovado. A decisão é LOCAL (olha as dependências do próprio nó), * não global: um nó obrigatório mais adiante no grafo, ainda pendente, não * é motivo para rodar uma crítica sobre algo que já passou na verificação. * * Nó sem contrato nunca é opcional, então o caminho legado executa tudo. */ private shouldSkipOptional; } /** * Um nó é de tool quando o kind diz isso E existe tool declarada. Kind sozinho * não basta: grafos antigos usavam `kind: 'tool'` como rótulo descritivo e * seguiam pelo producer normal — quebrar isso seria mudar o comportamento de * plano que já roda. */ /** * A política bloqueou a tool E disse que uma pessoa pode destravar. * * Classe própria porque o caminho de erro do nó precisa distinguir isto de uma * falha comum sem casar mensagem por regex: um "não" definitivo vai para o * healing, um bloqueio destravável vai para `izanagi approve`. */ export declare class ToolApprovalRequired extends Error { readonly toolId: string; constructor(toolId: string, message: string); } //# sourceMappingURL=orchestrator.d.ts.map