Ao fim desta lição você sabe o que É um AI Employee no Alembic (composição, não mágica), o que cada um dos 6 subcomandos faz, por que o explain nunca fabrica, e onde exatamente o motor grava o seu primeiro registro honesto de memória.
Esta lição destila employee.ts, connectors.ts, introspect.ts, run.ts e writeback.ts — a camada que transforma capabilities soltas num "funcionário" executável e auditável.
explain--online e onde entra o A3b write-backUm AI Employee do Alembic não é um novo runtime: é uma camada de composição sobre coisas que o motor já tinha. O schema diz tudo — employeeDefinitionSchema (packages/hermes/src/employee/employee.ts:95) compõe: um soul (identidade + modelPreferences), uma lista de skills (nomes resolvidos no SkillStore), um memory binding opcional (quais dos 5 substores este funcionário lê), e DECLARAÇÕES de connectors e schedule — que outros seams (A3, A4) fiam depois. O comentário do próprio arquivo: "it adds no behaviour to any of them".
O acervo real de hoje: 43 AI Employees em ~/.alembic/employees e 148 skills no SkillStore ~/.hermes/skills.
Pense como… uma ficha de contratação: nome e valores (soul), cursos que a pessoa fez (skills), o caderno que ela pode consultar (memory binding), os crachás de acesso que ela PEDIU mas ainda não recebeu (connectors) e a agenda combinada (schedule). A analogia quebra num ponto: o funcionário de verdade lembra sozinho — aqui, só entra na cabeça dele o substore explicitamente bindado.
employeeMemoryStores (employee.ts:49) fixa o vocabulário dos substores: episodic | semantic | procedural | decision | transcript — o MESMO da memória multi-store, então o binding é type-checked contra os stores reais. loadEmployee (employee.ts:147) valida um <id>.json fail-closed (leitura, JSON e schema, cada falha vira err de uma linha); listEmployees (employee.ts:180) pula arquivo ruim sem derrubar a listagem e ordena por nome para saída determinística.
Tudo que o employee É converge para UMA string: o system prompt do turno. renderEmployeePrompt (employee.ts:208) lidera com o soul e lista skills/connectors; assembleSystemPrompt (run.ts:249) apensa ## Skill Playbooks (os CORPOS das skills) e ## Memory (só substores bindados) — playbooks antes da memória: conhecimento operacional primeiro, recall por turno depois.
Toda a superfície do domínio no CLI é alembic employee <ação>. Seis ações, todas offline/$0 por default — só run --online chama modelo.
| subcomando | o que faz | onde no código |
|---|---|---|
| list | 1 linha por employee (id/nome/papel/#skills/#connectors) | commands.ts · runEmployeeList |
| show | definição completa + o renderEmployeePrompt renderizado | commands.ts · runEmployeeShow |
| connectors | resolve declarados → wired vs unwired (nunca dropa em silêncio) | connectors.ts:87 · resolveEmployeeConnectors |
| explain | mapa dos 10 passos com tags observed/inferred/unknown | introspect.ts:476 · explainEmployeeExecution |
| run | 1 turno: preview $0 default; --online chama o modelo | run.ts:151 · runEmployeeTurn |
| schedule | schedule[] → manifests automation.toml PAUSED | apps/cli/src/employee-schedule.ts:111 |
offlineConnectorProvider (connectors.ts:111) — ele resolve NADA de propósito. Todo connector declarado aparece como unwired (no adapter): o seam A3 existe, os adapters reais (OAuth por serviço) são founder-gated e vivem fora dele.# os 6 subcomandos (todos $0; run só gasta com --online)
alembic employee list
alembic employee show failure-historian
alembic employee connectors failure-historian --json
alembic employee explain failure-historian --goal "audite os runs parados"
alembic employee run failure-historian --goal "audite os runs parados"
alembic employee schedule failure-historian --json
explain responde "como ESTE employee transformaria ESTE goal em ação verificável" — em 10 passos canônicos (executionStepKindSchema, introspect.ts:36): message-to-task → context-assembly → planning → tool-use → verification → output → memory-update → report → next-loop → failure-recovery. O truque não é o mapa: é que cada claim carrega uma tag de confiança (introspect.ts:55).
observed lido da config DESTE employee · inferred comportamento default real do motor · unknown um interno verdadeiro não-inspecionável (ex.: o CONTEÚDO de um store).
Pense como… um laudo pericial: o perito separa "eu vi" (observed), "pela mecânica, deve ter sido assim" (inferred) e "não dá para saber daqui" (unknown). Quebra onde? O perito pode errar a tag; aqui a tag é imposta pelo schema — um claim sem tag nem compila.
O relatório embute o GROUND_RULE (introspect.ts:135) como parte do ARTEFATO. E os caveats caem por prova, não por otimismo: ExplainOptions (introspect.ts:94-129) só remove o caveat "A3 not built" para os ids em wiredConnectors, o "A4 runner not built" quando scheduleWired: true, e o "write off-by-default" quando memoryWriteEnabled: true — e mesmo assim o claim continua inferred (a execução futura não é fato observado). explainEmployeeExecution e renderExecutionChainReport são PUROS/determinísticos: mesmo employee + goal ⇒ relatório byte-idêntico.
alembic employee run <id> --goal "…" tem DOIS modos, e o default nunca gasta. Sem flag: runEmployeeRunOffline (apps/cli/src/commands.ts:5163) monta via buildEmployeeRunInput (run.ts:112, PURO/determinístico — requestId = employee-<id>) e IMPRIME o preview spend-safe: o prompt que SERIA enviado + o modelId resolvido (primary ?? fallback ?? local-default; sem nenhum dos três ⇒ err, run.ts:119). NENHUM modelo é chamado.
Com --online (founder-gated): runEmployeeTurnOnline (commands.ts:5219) constrói o adapter real do gateway com preflight — gateway/token ausente ⇒ falha fechada, SEM gasto — e o turno vira runEmployeeTurn (run.ts:151): compõe memória (best-effort, memória nunca quebra o turno), compõe playbooks, chama adapter.run(input) e valida o resultado.
composeSkillsText (run.ts:281) carrega o CORPO de cada skill bindada via SkillStore e injeta como ## Skill Playbooks. Best-effort: skill que falha ao carregar é PULADA em silêncio — guarde isso; vira a lição dos bindings-fantasma na Lição 10.selectBoundStores (run.ts:228) troca cada substore não-bindado pelo EMPTY_READER (run.ts:240) — o composer exige as 5 chaves, então a neutralização é estrutural, não um "if" esquecível.# dry-run (default): imprime system prompt + goal + modelId; NÃO chama modelo alembic employee run failure-historian --goal "resuma os piores ofensores da semana" # online (founder-gated): preflight do gateway; falha fechada sem token alembic employee run failure-historian --goal "resuma os piores ofensores da semana" --online # online SEM write-back episódico (A3b off por flag) alembic employee run failure-historian --goal "…" --online --no-memory-write
Depois de um turno --online bem-sucedido, recordEmployeeTurn (writeback.ts:74) apensa UM registro episódico honesto: episode = o goal REAL verbatim (writeback.ts:92), context = excerto REAL da resposta do modelo, truncado em WRITEBACK_CONTEXT_MAX = 500 chars (writeback.ts:50). NUNCA um "learning" fabricado. Opt-in: sem binding episodic ⇒ no-op ok(false) (writeback.ts:81). Determinístico: at vem do clock injetado do CLI; id = writeback-<employeeId>-<at> (writeback.ts:89). E uma falha de escrita NUNCA quebra o turno (writeback.ts:102-107).
Isso não é teoria: o primeiro registro episódico da história do motor foi gravado em 06-07, pelo employee failure-historian. Ele está em ~/.alembic/memory/episodic.jsonl — exatamente 1 linha, com o formato de id do writeback.ts:
{"id":"writeback-failure-historian-1783331936178",
"agent":"failure-historian","at":1783331936178,
"episode":"Primeiro digest real do motor. DADOS VERIFICADOS (auditoria 06-07): …
(4) classe recorrente ABERTA: drift-de-contagem … contagens escritas a mao
e nunca re-derivadas da fonte. …",
"context":"# Repeat-Offender Digest\n\n**Estado do Catálogo:** Inicializad…"}
episode é o goal que o operador realmente passou (um digest com dados VERIFICADOS no prompt) e context é o começo da resposta real do modelo. Nada resumido por uma segunda IA, nada inventado. É a materialização da regra do writeback.ts: "It records the REAL turn, never an invented learning".episode = goal verbatim, context = excerto ≤500 chars da resposta real (writeback.ts:88-95). Só no substore episodic, só se bindado, e uma falha de escrita nunca quebra o turno.A4: alembic employee schedule <id> mapeia cada schedule[i] = {task, cron} para um manifest @alembic/automation via employeeToAutomations (apps/cli/src/employee-schedule.ts:111 — PURO, fail-closed). Id determinístico emp-<id>-<i>, o cron viaja como rrule opaco, e o status nasce PAUSED (employee-schedule.ts:42) — registrável mas spend-safe: nunca auto-roda. Default = IMPRIMIR; --out <dir> escreve <dir>/<id>/automation.toml, o exato formato que alembic automation list --dir relê.
A4c (PR #159): alembic automation run <id> EXECUTA um manifest — dry-run default; --online roda o turno e apensa 1 registro honesto ao journal memory.md da automation. O journal já é um tipo de primeira classe: JOURNAL_FILENAME = 'memory.md' + AutomationJournal append-only (packages/automation/src/journal.ts:35) — o análogo tipado do memory.md que as automations codex acumulam por run. O 1º registro real desse journal também é de 06-07. Não é daemon: PAUSED só roda à mão.
Pense como… pendurar o crachá do funcionário no relógio de ponto (A4) com o ponteiro TRAVADO em pausa — e o A4c é você girar a manivela UMA vez, à mão, e o relógio carimbar a ficha (journal). A analogia quebra: relógio de verdade anda sozinho; aqui, de propósito, não existe daemon.
# A4: preview dos manifests (nada é escrito) alembic employee schedule failure-historian # A4: materializa em disco no formato que automation list relê alembic employee schedule failure-historian --out ~/.alembic/automations alembic automation list --dir ~/.alembic/automations alembic automation show emp-failure-historian-0 --dir ~/.alembic/automations # A4c (PR #159): executa 1 manifest — dry-run default; --online registra no journal memory.md alembic automation run emp-failure-historian-0 --dir ~/.alembic/automations alembic automation run emp-failure-historian-0 --dir ~/.alembic/automations --online
Honestidade final, provada na fronteira: o turno do employee NÃO tem tool-loop. O modelo recebe prompt e devolve texto — ele não pode LER arquivo, rodar comando, varrer diretório. Se o goal pede "varra X e liste o que achar", a resposta vem com uma varredura FABRICADA (isso foi testado e provado; está no spec do curso como caveat oficial).
A regra operacional até a perna tool-loop existir: dados verificados vão NO PROMPT. É exatamente o que o goal do 1º registro episódico fez — repare no texto real: "DADOS VERIFICADOS (auditoria 06-07): (1)… (2)… (3)…". O operador auditou fora, colou os fatos, e pediu ao modelo só o que modelo faz bem: sintetizar e propor.
--goal "varra ~/.alembic/runs e liste as tasks t4-parked"O modelo não tem como varrer; ele INVENTA uma lista plausível.
--goal "DADOS VERIFICADOS: 2 tasks t4-parked (run-617ca163, run-1d9e285b). Proponha UMA mudança de sistema."A verdade entra pronta; o modelo sintetiza em cima de fato.