Agent implementation patterns
Framework neutrality
YAAgents governs the application-to-agent boundary — the REST surface between your clients and your agentic API services. What happens behind that boundary is entirely your choice.
Your agent handler can use:
- LangGraph — stateful multi-step agent graphs
- LangChain — chain-based pipeline execution
- Semantic Kernel — .NET or Python planner + skill compositions
- Pydantic AI — type-safe LLM agents in Python
- Direct LLM SDK — OpenAI, Anthropic, Google, or any other provider called directly
- Custom logic — deterministic rules, search algorithms, or any code that produces a typed outcome
- Any combination of the above — the handler is your code; yaagents sees only the typed response it returns
The only contract yaagents enforces is the response type: the handler returns an
AgenticResponse (or raises the appropriate outcome class), and the SDK serializes it
to the correct Profile v0.3 response. Everything before that return statement is yours.
Common patterns
Direct LLM call
The simplest pattern: the handler calls an LLM SDK directly, processes the output, and returns a typed outcome.
from yaagents_fastapi import agentic_endpoint, AgenticResponseimport anthropic
client = anthropic.Anthropic()
@agentic_endpoint("/campaigns/{id}/optimizations", method="POST")async def optimize_campaign(id: str, body: OptimizationRequest) -> AgenticResponse: message = client.messages.create( model="claude-opus-4-5", max_tokens=1024, messages=[{"role": "user", "content": f"Optimize campaign {id}: {body}"}] ) # Parse the LLM response into a typed outcome result = parse_optimization(message.content[0].text) return AgenticResponse.done(result)Framework wrapper
The handler invokes a LangGraph or LangChain pipeline and maps the pipeline’s output to a Profile v0.3 outcome.
from yaagents_fastapi import agentic_endpoint, AgenticResponsefrom my_agent import build_recommendation_graph
graph = build_recommendation_graph()
@agentic_endpoint("/products/{id}/recommendations", method="POST")async def recommend(id: str, body: RecommendationRequest) -> AgenticResponse: result = await graph.ainvoke({"product_id": id, "context": body}) if result["needs_clarification"]: return AgenticResponse.clarification_required(result["required_inputs"]) return AgenticResponse.done(result["recommendations"])Process-shell pattern
The handler shells out to an external process for reasoning, parses its output, and maps it to a typed outcome. Any CLI that accepts structured input and emits structured output can be the reasoning process.
import subprocess, jsonfrom yaagents_fastapi import agentic_endpoint, AgenticResponse
@agentic_endpoint("/tickets/{id}:triage", method="POST")async def triage_ticket(id: str, body: TriageRequest) -> AgenticResponse: prompt = json.dumps({"ticket_id": id, "content": body.content}) proc = subprocess.run( ["my-reasoning-cli", "--json"], input=prompt, capture_output=True, text=True, check=True ) outcome = json.loads(proc.stdout) if outcome["type"] == "clarification_required": return AgenticResponse.clarification_required(outcome["requiredInputs"]) return AgenticResponse.done(outcome["result"])The Claude Code CLI is one concrete instance of this pattern that the AmpathyMinds team uses internally — see §3 below.
Background worker
For long-running operations, the handler returns 202 accepted immediately with an
operation ID, and a background worker completes the computation asynchronously. The
client polls the operation endpoint for the final result.
from yaagents_fastapi import agentic_endpoint, AgenticResponseimport asyncio, uuid
@agentic_endpoint("/claims/{id}/risk-screens", method="POST")async def screen_claim(id: str, body: ClaimRequest) -> AgenticResponse: operation_id = str(uuid.uuid4()) asyncio.create_task(run_risk_screen(id, body, operation_id)) return AgenticResponse.accepted(operation_id=operation_id, status_url=f"/operations/{operation_id}")
async def run_risk_screen(id, body, op_id): # Long-running computation — stores result for client polling ...The Claude Code CLI internal pattern (illustrative)
Internally, AmpathyMinds builds some agent services by wrapping the Claude Code CLI — the CLI receives a structured prompt, returns structured output, and the wrapping service maps that into Profile v0.3 outcomes. This is one concrete example of the §2.3 process-shell pattern; it is not a yaagents binding — you can use any reasoning approach behind the boundary.
import subprocess, jsonfrom yaagents_fastapi import agentic_endpoint, AgenticResponse
PROMPT_TEMPLATE = """You are a campaign optimizer. Given the campaign data below, return a JSON objectwith keys: "action" (string), "reasoning" (string), "confidence" (float 0-1).If you need more information, return: {{"type": "clarification_required", "requiredInputs": [...]}}
Campaign data: {campaign_json}"""
@agentic_endpoint("/campaigns/{id}/optimizations", method="POST")async def optimize_with_claude_code_cli(id: str, body: OptimizationRequest) -> AgenticResponse: prompt = PROMPT_TEMPLATE.format(campaign_json=body.model_dump_json()) proc = subprocess.run( ["claude", "-p", prompt, "--output-format", "json"], capture_output=True, text=True, check=True ) result = json.loads(proc.stdout) if result.get("type") == "clarification_required": return AgenticResponse.clarification_required(result["requiredInputs"]) return AgenticResponse.done({"action": result["action"], "reasoning": result["reasoning"]})What YAAgents does NOT do
- Not an agent framework — yaagents does not provide a reasoning pipeline, a memory layer, or a prompt-management system.
- Not a chatbot framework — yaagents is for resource-oriented domain APIs, not conversational chat interfaces.
- Not an MCP replacement — MCP is a tool-server protocol for LLM tool use; yaagents is the application-facing API boundary. Both coexist in the same stack.
- Not a model abstraction layer — yaagents does not wrap or choose your LLM provider; that is your handler’s responsibility.
- Not a prompt-strategy library — yaagents does not manage few-shot examples, chain-of-thought scaffolding, or system-prompt versioning.