Ao fim desta lição você sabe o que um dos 43 employees em ~/.alembic/employees É (composição, não mágica), como um turno vira UMA chamada de modelo auditável, e onde o engine se recusa — estruturalmente — a fabricar.
buildEmployeeRunInput é puro e por que isso é spend-safememory.md)explain lendo as tags observed / inferred / unknownUm AI Employee não é um subsistema novo — é uma camada fina de COMPOSIÇÃO sobre peças que o Alembic já tinha: a identidade (soul), as skills do SkillStore, o binding para os 5 substores de memória da lição 06, e DECLARAÇÕES de connectors e schedule que outras unidades (A3, A4) tornam executáveis. O cabeçalho do arquivo diz literalmente: "This file invents NONE of those: it composes them" (packages/hermes/src/employee/employee.ts:12-19).
Pense como… um dossiê de contratação: uma ficha com quem a pessoa é, o que sabe fazer, a que arquivos tem acesso e que crachás pediu. Contratar é preencher a ficha — os crachás (connectors) só abrem portas quando alguém os LIGA. A analogia quebra: aqui a ficha é um schema Zod que recusa qualquer campo malformado na fronteira.
A1 = a definição + renderEmployeePrompt (composição pura). A2 = a leitura de memória no turno (composer da lição 06, só substores BINDADOS). A3 = o seam de connectors (connectors.ts) — tipado, offline honesto. A3b = o write-back episódico. A4 = schedule → manifests de automation; A4c = o runner automation run (PR #159). A4b = importar sessões/skills de fora (lição 06). A5 fecha o domínio com a superfície CLI completa: alembic employee list|show|connectors|explain|run|schedule (apps/cli/src/args.ts:520).
Seis campos, todos validados por employeeDefinitionSchema (employee.ts:95-108). O soul vai EMBUTIDO verbatim; o resto são ids opacos resolvidos pelos subsistemas donos.
Dois puros em sequência: renderEmployeePrompt monta o lead-in (soul → ## Skills → ## Connectors, seções vazias somem — employee.ts:208-219); buildEmployeeRunInput monta e VALIDA o ModelRunInput que SERIA enviado — sem clock, sem RNG, sem IO. É por isso que alembic employee run é um dry-run $0 por padrão: imprimir o preview não custa nada e é byte-idêntico a cada execução.
// trecho real, não editado const prefs = employee.soul.modelPreferences; const modelId = prefs?.primary ?? prefs?.fallback ?? opts?.fallbackModelId; if (modelId === undefined || modelId.length === 0) { return err(new Error(`employee ${employee.id} declares no model and no fallback was given`)); } const systemPrompt = assembleSystemPrompt(employee, opts?.memoryContext, opts?.skillsContext); const candidate = { requestId: `employee-${employee.id}`, // derivado, nunca aleatório modelId, systemPrompt, userPrompt: goal, ...(prefs?.maxTokens === undefined ? {} : { maxOutputTokens: prefs.maxTokens }), ...(prefs?.temperature === undefined ? {} : { temperature: prefs.temperature }), };
soul.modelPreferences.primary ?? fallback ?? fallbackModelId (na CLI, local-default). Sem nenhum dos três ⇒ err — um turno não roda sem modelo. Gotcha operacional: employee run não tem flag --model; o modelo que funciona TEM que estar no soul.alembic employee list # os 43, um por linha alembic employee show failure-historian # definição + prompt renderizado alembic employee run failure-historian --goal "resuma o dia" # DRY-RUN $0: imprime o preview alembic employee run failure-historian --goal "resuma o dia" --online # founder-gated; falha fechado sem gateway
runEmployeeTurn executa exatamente uma chamada ao ModelAdapter injetado, em 4 passos: compor memória (best-effort), compor playbooks das skills bindadas, construir+validar o input, chamar adapter.run (run.ts:151-189). Uma falha ao compor memória degrada para turno SEM memória — nunca derruba o turno. E os corpos das skills entram INTEIROS como ## Skill Playbooks, para o modelo SEGUIR as regras, não só ver os nomes.
O turno não tem tool-loop. Pedir "varra o ledger X" a um employee --online produziu uma varredura FABRICADA plausível — o modelo não tem como ler arquivo nenhum (prova registrada no fechamento do A4c, commit 8d9ed2a). Mitigação documentada: dados VERIFICADOS vão dentro do prompt até a perna tool-loop existir. O engine não esconde a limitação — ele a documenta e a etiqueta no explain.
Depois de um turno --online bem-sucedido, o engine anexa UM registro episódico: o episode É o goal real; o context É um excerto de até 500 caracteres da resposta real. Nunca um "learning" inventado. O primeiro registro episódico real da história do engine nasceu assim, na prova de fronteira de 06–07/07.
// trecho real, não editado const binding = resolveMemoryBinding(employee); // Opt-in: write back ONLY when the employee explicitly binds the episodic store. if (binding === undefined || !binding.stores.includes('episodic')) return ok(false); const at = Date.parse(opts.now); // clock INJETADO da CLI, nunca Date.now() // … const candidate: EpisodicRecord = { id: `writeback-${employee.id}-${at}`, // derivado, nunca randomUUID() agent: binding.agent, at, episode: opts.goal, // o goal REAL context: excerpt(turn.text, WRITEBACK_CONTEXT_MAX), // excerto REAL (500) salience: DEFAULT_EPISODIC_SALIENCE, };
episodic completa um turno --online com sucesso. O que o write-back A3b faz?writeback.ts:79-81 — sem binding episodic, ok(false) e nada é escrito. E mesmo quando escreve, uma falha de append vira err logado que NUNCA quebra o chamador (o turno já venceu). --no-memory-write desliga; o dry-run offline nunca escreve.resolveEmployeeConnectors projeta os ids declarados através de um ConnectorProvider: id resolvido vira port wired; id não resolvido vai para unwired — nunca é descartado em silêncio. Ordem preservada, duplicatas colapsam, e wired + unwired = ids distintos declarados (connectors.ts:87-102). O provider offline é honesto por definição: resolve TUDO para undefined, então alembic employee connectors <id> reporta cada connector declarado como unwired (no adapter). Adapters reais (OAuth por serviço) são founder-gated e vivem FORA do seam.
O port é a cintura estreita de 4 verbos do modelo Supercomputer: read, list, write, post (connectors.ts:36) — e invoke nunca lança: falha de transporte/auth é err, como no ModelAdapter.
A4: employeeToAutomations (camada CLI, para não criar aresta hermes→automation) mapeia cada schedule[i] para um manifest registrável: id determinístico emp-<id>-<i>, o cron carregado como rrule OPACO, status PAUSED (registrável mas nunca auto-roda), created_at/updated_at fixos em 0 para o mapa ser puro (apps/cli/src/employee-schedule.ts:39-48). A4c (PR #159) adicionou a perna que faltava: automation run <id> EXECUTA um manifest — sem virar daemon.
alembic employee schedule failure-historian # A4: imprime os manifests (não escreve) alembic employee schedule failure-historian --out ~/.codex/automations # escreve automation.toml alembic automation list --dir ~/.codex/automations # lê de volta o que o A4 escreveu alembic automation run emp-failure-historian-0 --dir ~/.codex/automations # dry-run $0 alembic automation run emp-failure-historian-0 --dir ~/.codex/automations --online # roda + journal
explainEmployeeExecution responde "como ESTE employee transformaria ESTE goal em ação verificável" nos 10 passos canônicos: message-to-task → context-assembly → planning → tool-use → verification → output → memory-update → report → next-loop → failure-recovery (introspect.ts:36-47). A regra de ouro é ESTRUTURAL: cada claim carrega uma tag — observed (lido da config deste employee), inferred (default real do engine) ou unknown (um interno de verdade não inspecionável). O mapa nunca afirma que A3/A4 "está operacional" sem estar: esses caveats vão verbatim para os ledgers inferred/unknown.
--online você pede: "varra o ledger X e liste os erros". O que a prova de fronteira mostrou?8d9ed2a): sem tool-loop, o modelo inventou a varredura. Por isso o caveat operacional: dados verificados entram NO prompt até a perna tool-loop nascer — e o explain mantém tool-use como inferred/unknown, nunca observed.Quatro cartas de retrieval antes do próximo módulo.