Passo 01 / 28

A Action e o pipeline

Tudo no AOA gira em torno de uma única figura — a Action. É uma operação de negócio expressa como uma classe: uma entrada tipada (Params), uma saída (Result) e, entre elas, uma cadeia direta de passos que se lê de cima para baixo, como uma página. Cada seção abaixo corresponde a um arquivo real em examples/step_01_Action_and_pipeline/ — pressione Executar para ver sua saída real capturada, ou abra-o no Colab para executá-lo você mesmo.

Olá, mundo!

Vamos começar com uma Action que não faz nada de útil — ela imprime uma linha e retorna um stub vazio. Seu valor está em outro lugar: mostra o mínimo de declarações que o AOA exige antes de rodar qualquer coisa.

01_hello_world.py
class GreetingDomain(BaseDomain):    name = "greeting"    description = "Greetings domain"@meta(description="Say hello to the world", domain=GreetingDomain)@check_roles(GuestRole)class SayHelloAction(BaseAction[ParamsStub, ResultStub]):    @summary_aspect("Print greeting and return stub")    async def output_summary(self, params, state, box, connections):        print("Hello, world!")        return ResultStub()
$ uv run python examples/step_01_Action_and_pipeline/01_hello_world.py
saída

São bastantes linhas para uma simples saudação — mas nenhuma delas é um ritual: cada uma declara algo sem o qual uma Action no AOA não é considerada completa. Tudo começa com um domain. @meta é o passaporte da Action; @check_roles(GuestRole) declara o acesso de forma explícita, não por padrão. E @summary_aspect é o único ponto de saída — pule qualquer um dos três e a máquina se recusa a rodar a Action, antes da primeira chamada.

Params, Result e box

Stubs servem para introduções; uma Action real trabalha com dados. Vamos dar a ela uma entrada e uma saída — e trocar print pelo instrumento que as Actions no AOA usam para falar com o mundo exterior.

02_params_result_and_box.py
class GreetParams(BaseParams):    name: str = Field(description="Name of the person to greet")class GreetResult(BaseResult):    message: str = Field(description="Assembled greeting message")@meta(description="Greet a person by name", domain=GreetingDomain)@check_roles(GuestRole)class GreetPersonAction(BaseAction[GreetParams, GreetResult]):    @summary_aspect("Build greeting and return result")    async def greet_summary(self, params, state, box, connections):        await box.info(            Channel.business,            "Greeting: Hello, {%var.name|cyan}!",            name=params.name,        )        return GreetResult(message=f"Hello, {params.name}!")
$ uv run python examples/step_01_Action_and_pipeline/02_params_result_and_box.py
saída

box é mais interessante que print — é um logger estruturado ligado ao passo atual, que carrega canal, nível, domain, action e o nome do aspect, então se presta a filtragem, roteamento e exportação. Para onde o evento vai não é decisão da Action — isso é assunto da máquina, conectado a partir de fora.

Múltiplos aspects

Um único passo raramente é suficiente. O AOA não deixa liberdade para que os dados intermediários se escondam em variáveis locais ou campos de objeto — eles fluem pelo pipeline em um state explícito, enquanto a própria Action permanece vazia entre chamadas.

03_multiple_aspects.py
@meta(description="Process input string through multiple steps", domain=ProcessingDomain)@check_roles(GuestRole)class ProcessInputAction(BaseAction[ProcessParams, ProcessResult]):    @regular_aspect("Step 1: Strip whitespace and lowercase")    @result_string("cleaned", required=True)    async def validate_aspect(self, params, state, box, connections):        cleaned = params.raw_input.strip().lower()        return {"cleaned": cleaned}    @regular_aspect("Step 2: Enrich data")    @result_string("cleaned", required=True)    @result_string("enriched", required=True)    async def enrich_aspect(self, params, state, box, connections):        enriched = f"enriched::{state['cleaned']}"        return {"cleaned": state["cleaned"], "enriched": enriched}    @summary_aspect("Step 3: Assemble final result")    async def assemble_summary(self, params, state, box, connections):        return ProcessResult(            cleaned=state["cleaned"],            enriched=state["enriched"],            final=f"{state['cleaned']} → {state['enriched']}",        )
$ uv run python examples/step_01_Action_and_pipeline/03_multiple_aspects.py
saída

O dicionário retornado substitui state por completo — não o estende. @result_string("cleaned", required=True) é um verificador: um contrato verificável sobre a saída de um aspect. Quebre a promessa e a máquina para o aspect na hora, sem nunca deixar um state corrompido seguir adiante.

Herança

As Actions herdam como as classes comuns — com uma exceção deliberada: os aspects não são herdados no pipeline. A máquina constrói o pipeline apenas a partir do que é declarado na própria classe.

04_inheritance.py
# Parent: two aspects in the pipelineclass BaseOrderAction(BaseAction[OrderParams, OrderResult]):    @regular_aspect("Validate order")    async def validate_aspect(self, ...): ...    @summary_aspect("Base result")    async def base_summary(self, ...): ...# Child: declares only its own summary — validate_aspect will NOT runclass ChildOrderAction(BaseOrderAction):    @summary_aspect("Child result")    async def child_summary(self, ...): ...# The right way: declare the aspect explicitly and call super()class ExtendedOrderAction(BaseOrderAction):    @regular_aspect("Validate order")    @result_instance("steps", list, required=True)    async def validate_aspect(self, params, state, box, connections):        result = await super().validate_aspect(params, state, box, connections)        return {**result, "extended": True}    @summary_aspect("Extended result")    async def extended_summary(self, ...): ...
$ uv run python examples/step_01_Action_and_pipeline/04_inheritance.py
saída

ChildOrderAction roda sem problemas, mas seu pipeline contém apenas child_summary — o validate_aspect do pai nunca executa. ExtendedOrderAction faz certo: redeclara o aspect e chama super() para construir sobre a lógica do ancestral.


Experimentos

Este capítulo acumulou um bom número de regras — mas todas compartilham uma propriedade: quase todas são verificadas na declaração da classe, na importação do módulo, não na chamada da Action. O código aqui é uma especificação executável; uma violação de contrato aparece antes da primeira execução, não em uma noite qualquer em produção. Experimente — escolha uma forma de quebrar SayHelloAction do primeiro exemplo e pressione Executar.

01_hello_world.py
class GreetingDomain(BaseDomain):    name = "greeting"    description = "Greetings domain"@meta(description="Say hello to the world", domain=GreetingDomain)@check_roles(GuestRole)class SayHelloAction(BaseAction[ParamsStub, ResultStub]):    @summary_aspect("Print greeting and return stub")    async def output_summary(self, params, state, box, connections):        print("Hello, world!")        return ResultStub()
$ uv run python examples/step_01_Action_and_pipeline/01_hello_world.py
saída

Resumo

O núcleo do modelo já está em mãos: uma operação é uma Action com uma fronteira tipada (Params e Result), um pipeline linear de aspects e contratos que são, em sua maioria, verificados na inicialização. O comportamento é expresso na estrutura do código, não em documentação que fica ao lado dele.


Perguntas de revisão

  1. Qual invariante garante a legibilidade "a partir de uma única classe" de uma Action, e em que momento ela é verificada?
  2. Por que a ausência de @check_roles é um erro e não um silencioso "aberto a todos"?
  3. O pipeline do AOA é linear — sem ramificações, sem saídas laterais. O que essa restrição traz, e a que custo?
  4. Por que os aspects não são herdados automaticamente no pipeline? Compare com a herança comum de métodos na POO.
  5. O que significa "o código é uma especificação executável", e por que a maioria dos contratos é verificada na inicialização em vez de em tempo de execução?
passo 01 — A Action e o pipeline · aoa.run