A custom-built agent for mutual fund distributors. The orchestration loop is written in-house — not a hosted agent platform. Claude (or any LLM) is just an outbound HTTPS dependency, same as any third-party API. Everything else — memory, routing, tool dispatch, state, policies — lives in our AWS infrastructure.
North star: If the LLM is swapped tomorrow, everything else should stay the same.
Advisor / Distributor (UI / Chat)
│
▼
Agent Orchestrator (our loop, AWS)
│
├── LLM API (outbound HTTPS, inference only)
│
▼
MCP Layer (domain adapters)
├── Fund Domain MCP
├── Client Domain MCP
└── Workflow / Report MCP
│
▼
Existing Services (untouched)
├── Portfolio Analysis (gRPC) → ~10s PDF generation
├── Fund Analysis (gRPC)
└── Client Data Service (gRPC / REST)
│
▼
Data Platform
├── PostgreSQL (funds, clients, derived metrics)
├── S3 (generated PDFs, reports)
└── SQS (async job queue)
Rule: gRPC internally (service-to-service). MCP at the agent boundary. The agent never talks to gRPC or the database directly. MCP is the seam between the agent and our services — it decouples actual business logic from whatever framework or loop runs above it.
Decisions to make before writing the loop:
| Decision | Notes |
|---|---|
| Primary model | Decide per route — hard reasoning vs. routine steps don't need the same model or effort level |
| Fallback model | If primary is unavailable (rate limit, outage), fall back to a smaller/cheaper model. Define which routes tolerate degraded quality |
| Determinism | Set temperature=0 for any call that feeds a deterministic downstream step (tool selection, structured extraction). Reserve non-zero temp for response generation only |
| RPC protocol | gRPC internally between services. MCP (HTTP/SSE or stdio) at the agent boundary. The loop itself calls the LLM over HTTPS |
| Option | Trade-off |
|---|---|
| LangGraph / CrewAI / AutoGen / Semantic Kernel | Faster to start, opinionated about state and loop shape, harder to audit and control precisely |
| Custom thin loop | Full auditability, we own every decision point, more code upfront but no abstraction fighting us later |
Decision: Custom thin loop. MCP handles tool discovery and dispatch. The loop is ours. This matters in finance — every tool call, model decision, and client data access needs to be logged explicitly, not inferred from a framework's internals.
| Part | Responsibility |
|---|---|
| Intent Understanding | Translate natural language into a goal |
| Planner | Output steps as capabilities, not raw API calls |
| Execution Engine | Map capability steps to existing MCP tools/services |
| World State | Track what happened (report IDs, job status, session facts) |
| Response Generator | Format the final result for the user |
What the agent core must never do:
Not every task needs the full agentic loop. Before entering the loop, classify the request:
| Request Type | Approach |
|---|---|
| Fixed, deterministic pipeline (e.g. generate scheduled report) | Run the pipeline directly. Don't touch the LLM reasoning loop. |
| Structured query with known tool (e.g. "show fund returns for X") | Single tool call, no planning loop needed |
| Multi-step, ambiguous, or open-ended | Enter the planning loop |
Iteration cap: Every agentic loop has a hard max (e.g. 10 iterations). If the cap is hit, fail explicitly — don't silently return a partial result.
Deterministic pipelines must not touch the LLM reasoning loop. They're cheaper, faster, and fully auditable. The agent may trigger them, but the computation is entirely outside the model.
Each request flows through these stages in order. Each stage produces structured output, not free text.
1. Intent Analysis → What is the user actually trying to achieve?
2. Goal Builder → Translate intent into a concrete goal
3. Data Requirement → What data is needed? What's known? What must be fetched? What must be asked?
4. Context Builder → Load relevant session + long-term memory
5. Constraint Analyzer → Latency budget, token budget, user permissions, data freshness
6. Planner → Choose capabilities and sequence them
7. Evaluator → Verify plan is sound before executing
8. Execution Plan → Deterministic step list handed to the execution engine
Replanning triggers (loop back to planner when):
Every tool, whether domain-specific or generic, must define all five of these:
{
"name": "get_fund_returns", # snake_case, unique
"description": "...", # when to call this, not just what it does
"function": get_fund_returns, # the actual callable
"parameters": { # JSON Schema
"type": "object",
"properties": {
"scheme_code": { "type": "string", "description": "..." },
"period": { "type": "string", "enum": ["1y","3y","5y"] }
},
"required": ["scheme_code"]
}
}
Input validation happens inside the tool's execution wrapper before calling the backend — not in the model's output. Schema adherence from the model is not a guarantee. Validate, then call.
Per-call timeouts are set at the tool layer, not globally. A database query gets 2s. A PDF generation gets 30s. An async job submission gets 5s.
All wrapped behind MCP. The agent sees these as capabilities, not gRPC methods.
| Tool | What It Returns |
|---|---|
compare_funds |
Side-by-side metrics for 2–10 schemes |
get_category_stats |
Pre-aggregated stats for a fund category |
get_factsheet_history |
Historical factsheet snapshots for a scheme |
get_fund_details |
Full profile of a single scheme |
get_fund_holdings |
Current portfolio holdings of a fund |
get_fund_overview |
Summary metrics (AUM, rating, category) |
get_fund_returns |
CAGR / absolute returns over standard periods |
get_fund_risk_metrics |
Sharpe, Sortino, SD, Beta, Alpha |
get_fund_sectors |
Sector allocation breakdown |
get_holdings_history |
How a fund's holdings changed over time |
get_nav_history |
Raw NAV time series |
get_nav_history_compare |
NAV curve comparison across schemes |
get_portfolio_analysis |
Full analysis for a client's portfolio |
get_returns_history |
Rolling returns over time |
get_sector_history |
Sector allocation changes over time |
list_categories |
All available fund categories |
list_schemes |
All schemes (filterable) |
search_schemes |
Text / attribute search across schemes |
screen_clients |
Filter clients by AUM, risk, drift, etc. |
get_client_analysis_context |
Client facts, goals, current holdings |
| Tool | Notes |
|---|---|
web_search |
For live fund news, regulatory updates, market context |
python_exec |
Sandboxed. For ad-hoc calculations the deterministic services don't cover |
file_read / file_write |
Scoped to designated paths only. Never arbitrary filesystem access |
database_query |
Read-only. Parameterized queries only — no string interpolation |
calculator |
Thin wrapper to avoid asking the LLM to do arithmetic |
Used when an advisor uploads a document for the agent to read:
| Library | Use Case |
|---|---|
PyPDF2 / pdfplumber / pypdf |
PDF text extraction |
Unstructured |
Multi-format parsing (PDF, DOCX, HTML, images) |
python-docx |
Word document loading |
beautifulsoup4 |
HTML document parsing |
Document content is extracted, chunked if needed, and injected into context — never stored as raw PII.
Five distinct memory types. Never conflate them.
Just the conversation. Managed by the loop. Expires when the session ends.
What happened in this conversation that the planner needs to reference mid-task. Stored server-side in a session store, not in the prompt. Includes: which tools were called, what they returned, what the user confirmed.
Not yet fully defined — to be designed.
Durable, persists across sessions. Promoted deliberately by a memory manager, not written directly by the LLM.
Not yet fully defined — to be designed.
Append-only. Timestamped. Retrieved chronologically.
Records of significant advisor-client interactions:
This is a compliance-adjacent record. The advisor conclusion is drafted by the agent, then the advisor gives explicit confirmation, after which it is immutable. Corrections are new entries — not edits to history. Same principle as a client communication log.
Current facts about a client — the live state.
Includes: goals, goal progress, risk profile, portfolio composition preferences, agreed benchmarks. This is the "what is true right now" layer. Unlike episodic, it can be updated when facts change (new goal, updated risk profile). One owner per truth — the client data service.
General portfolio composition rules, evaluated deterministically in code.
These are rules like "equity exposure should not exceed X% for a conservative profile" or "no single fund should exceed Y% of allocation." They are not evaluated by the LLM — they run as code against a client's holdings and return pass/fail with a reason.
Fund tracking alerts are not memory. They are the output of a background job that compares live fund data against client holdings and composition rules. The agent only retrieves and explains alerts — it never evaluates the rule itself.
generate_portfolio_report(
client_ref: str,
holdings_override: list[HoldingChange] | None = None
)
HoldingChange = {
"remove": fund_id, # optional
"add": fund_id # optional
}
holdings_override is None by default — run the standard analysis. When set, run the exact same pipeline but pretend the client holds the overridden set. This enables "what if" scenarios ("run the same analysis but pretend the client holds C and D instead of A and B") without changing any computation logic.
The agent's job is to figure out the holdings_override from the conversation — the pipeline itself doesn't change.
Two things always returned together:
The agent gets back a report_url (S3) and the metrics object. It never reads the PDF content — just knows the URL.
Once generated, the holdings and data used are frozen as a snapshot. We can always look up: "this is what we told the client on date X, computed against state Y." Re-reviewing always means pulling fresh data into a new report — never mutating an existing one.
The policy engine enforces rules the agent core is not allowed to decide on its own:
The agent core says what it wants to do. The policy engine says whether it can.
Data access control is enforced server-side, not prompt-side. Telling the model "don't show client X's data to advisor Y" is not access control — it's a suggestion. Access scoping is enforced at the tool layer: each tool call carries the authenticated advisor's context, and the backend rejects requests outside their permitted client scope.
| Concern | Approach |
|---|---|
| Raw PII in memory | Never stored in any memory layer — session, long-term, or episodic. Facts about clients are stored, not verbatim chat containing PII |
| Access scoping | Enforced at tool layer per authenticated session. Not via prompt instructions |
| Encryption | All client data at rest (PostgreSQL, S3) encrypted. In transit: TLS everywhere |
| Retention policy | To be defined per data type. Episodic records (advisor conclusions) are likely long-retention compliance records. Session memory is short-retention |
| Audit trail | Every tool call, every model decision, every client data access is logged with inputs, outputs, latency, and actor. In finance this is a compliance requirement, not optional |
Every action logged:
{
"event": "tool_call",
"tool": "generate_portfolio_report",
"input": { "client_ref": "C123" },
"output": { "report_url": "s3://...", "metrics": {...} },
"duration_ms": 9820,
"status": "success",
"advisor_id": "A456",
"session_id": "sess_789",
"timestamp": "2026-07-29T10:23:11Z"
}
Also log: every model call (prompt token count, completion token count, model used), every planning decision (which capability was selected and why), every policy decision (allowed or rejected and why).
request_id passed by the orchestrator so the backend can deduplicate. This is what makes retries safe.The agent thinks in capabilities, not APIs. Each capability is an atomic action with metadata:
{
"name": "generate_portfolio_report",
"cost": "medium",
"freshness_slo": "data as of last scheduled run",
"permissions": ["advisor", "admin"],
"required_inputs": ["client_ref"],
"optional_inputs": ["holdings_override"],
"returns": "report_url + metrics_payload",
"timeout_s": 30
}
Build a capability graph (not a flat tool list) so the planner knows relationships, not just endpoints. The governance layer enforces, but the planner needs to see constraints (cost, freshness, permissions, timeout) before deciding. Capabilities are the language the agent thinks in.
Workflows compose capabilities into reusable sequences.
Example: portfolio_review = generate_report → risk_analysis → comparison → (optional) email_client
draft → observed → candidate → reviewed → approved → production
| Services | Workflows |
|---|---|
| Primitive capabilities | Compose services into sequences |
| Own how (email service sends emails) | Decide when (orchestration) |
| Stateless / deterministic | Can pause for user input, branch, retry |
| Never change per workflow | Reusable across multiple workflows |
Return references, not raw files. A portfolio PDF tool returns report_url and a metrics payload — not a base64 blob. The agent knows the report exists and can surface structured data; it doesn't read the PDF.
Async for long-running work. If a task takes minutes:
start_job(params) → job_idcheck_job_status(job_id) → { status, result_url }Never block a tool call for more than the tool's defined timeout.
Idempotent calls. Every MCP tool is safe to retry. Pass a request_id from the orchestrator.
Tool count discipline. Too many tools in context degrades capability selection. Plan for dynamic tool discovery (model asks what tools are available for a goal, rather than receiving the full list every turn) before the surface grows large.
Before the first turn, the agent is loaded with static domain context (stable, not per-user):
Dynamic context (per-user, per-task) is fetched on demand — not stuffed into the system prompt. The system prompt is frozen text; interpolating session state or timestamps into it kills prompt cache hit rate.
Define before building, not after. A fixed set of realistic scenarios:
For each scenario: define expected tool-call sequence, expected policy decisions, and acceptable response shape. Check automatically — not just "does the final text look right?"
Run the eval suite before launch.
Reasoning strategies — Different request types need different thinking modes (report generation vs. anomaly detection vs. hypothesis testing). The planner should classify reasoning type before building a plan.
Planning under constraints — The plan should adapt to latency budgets, token budgets, mobile vs. server context.
Verification step — Plans should be checked for invariants before execution, not just outputs afterwards.
Evolving user intent — Handle mid-conversation goal shifts, not just single-turn prompts.
Failure as knowledge — Operational history (which capabilities fail, which inputs are frequently missing) should feed back into planning heuristics.
Short-term and long-term memory design — Both are noted as needed but not yet defined. Need storage schema, retrieval strategy, and promotion rules.
Retention policy per data type — Episodic records, session memory, and raw logs all have different legal and operational lifetimes. Needs definition before launch.
| Principle | What It Means |
|---|---|
| Every dependency points toward stability | Infrastructure knows about business logic, not the reverse |
| Data outlives code | Design the data layer to be stable; services come and go |
| One owner per truth | One service decides what a value means; everyone else reads it |
| Stable contracts, not stable internals | Refactor freely inside a service; version the interface |
| Make illegal states unrepresentable | Use state machines for workflows, not boolean flags |
| Prefer ownership of behavior, not just data | Services expose capabilities, not tables |
| Eventual consistency with explicit bounds | Define staleness SLOs per field; expose as-of timestamps |
| Failures are normal control flow | Transient: retry with backoff + idempotency. Permanent: fail fast with context |
| Access control is infrastructure, not prompt | Enforced at the tool layer, not by telling the model what to do |
| Advisor conclusions are immutable records | Draft, confirm, freeze. Corrections are new entries |
The architecture thinking is solid; the next step is turning intent into specs with contracts, invariants, and failure semantics. That is what lets multiple engineers build without drifting.
Five documents needed before a single line of production code:
Every major decision gets a one-page Architecture Decision Record. Six months later, ADRs are the map of why we did this. Start with:
| ADR | Decision |
|---|---|
| ADR-001 | Why MCP instead of direct gRPC at the agent boundary |
| ADR-002 | Why a custom orchestrator instead of LangGraph / CrewAI |
| ADR-003 | Why immutable episodic memory |
| ADR-004 | Why reasoning packages instead of raw tool results |
| ADR-005 | Why workflow commit semantics gate memory writes |
| ADR-006 | Why planner and executor are separate processes |
| ADR-007 | Why capability graph instead of flat tool list |
Each ADR: context → decision → consequences → alternatives considered. One page.
Not just examples — a formal schema that becomes the contract between the planner and the execution engine:
{
"name": "...",
"purpose": "...",
"inputs": { "required": [], "optional": [] },
"outputs": { "schema": {}, "reasoning_package": {} },
"side_effects": [],
"idempotency": "full | idempotent-with-request-id | not-idempotent",
"cost": "low | medium | high",
"latency_slo_ms": 0,
"freshness_slo": "...",
"permissions": [],
"failure_modes": [],
"dependencies": []
}
Every capability in the system fills this schema. No exceptions.
For workflows, async jobs, memory promotion, and approvals — define every legal state and every legal transition. Illegal states are structurally impossible. Covers:
Assume everything fails. For each failure scenario, define the response:
| Failure | Behaviour |
|---|---|
| MCP server down | Which capabilities degrade? Is there a fallback? |
| LLM API timeout | Retry? Cancel? Return partial? |
| Partial workflow success | Which steps are committed? How do we resume? |
| Human approval never comes | Expiry policy, notification, cancellation |
| Tool returns stale data | Freshness check fails — replan or surface to advisor? |
| Policy rejects mid-workflow | Rollback? Hold? Escalate? |
The planner is the brain. Everything else hangs off it. See the full planner design section below.
Capabilities first. Services second. Planner third.
Start by defining domain capabilities as business surfaces — what problem does each solve, what is its contract, what are its SLOs. Then, behind each capability, design or refine the RPC services that back it. That way services conform to the agent's needs, not the other way around.
Domain sequence (each depends on the one before): Client → Portfolio → Fund → Recommendation → Monitoring → Workflow.
Static facts about a fund. Name, AMC, category, launch date, AUM, fund manager, benchmark. Read-only. Freshness: daily.
Returns (absolute, CAGR), risk metrics (Sharpe, Sortino, SD, Beta, Alpha), ratios, drawdown. The computation backend owns this. The agent receives a structured analytics object — it does not compute.
Current holdings and exposure. Sector breakdown, market-cap tilt, top holdings, overlap with other funds. Freshness: monthly (or at factsheet publish).
Side-by-side comparison of 2–10 funds across any metric set. This is one of the highest-frequency advisor tasks. Returns a structured comparison object the agent can explain.
Search and filter across the full fund universe. Inputs: category, AMC, AUM range, Sharpe minimum, rating, expense ratio cap, etc. Returns a ranked list. Deterministic — does not go through LLM reasoning.
Advisor gives a category and a set of parameters with weights. System ranks funds in that category and returns an ordered list with scores and explanations. The advisor can then open a session to argue with the ranking, adjust weights, and re-rank. The LLM explains the ranked output — it does not do the ranking computation.
Significant changes: manager change, category reclassification, SEBI rating change, exit load change, AUM spike/drop. Append-only event log. The monitoring system produces these; the agent only retrieves and explains them.
Curated summaries of static facts and derived insights — not computed live, maintained by a background job. Purpose: the agent can explain a fund without triggering a live fetch. Think of it as a pre-digested briefing per fund.
Per-fund brokerage structure and exit load schedule. Per-redemption tax implication (STCG / LTCG thresholds). This data is needed whenever the recommendation service evaluates a fund switch. It is reference data, not analytics. Freshness: updated when regulation or fund terms change.
Workflows are reusable business processes, not API sequences. Group into four buckets:
| Bucket | Examples |
|---|---|
| Advisor-initiated | Replace fund, review portfolio, prep for client meeting |
| Event-driven | Monitoring alert, portfolio drift, fund manager change |
| Scheduled | Monthly reviews, weekly summaries, data refreshes |
| Human-approval | Anywhere execution explicitly pauses for sign-off |
Every workflow follows this structure. No exceptions.
Purpose — one sentence, what business outcome does this achieve
Trigger — what starts this workflow
Steps — ordered, each mapped to a capability
Decision Points — where the workflow branches based on data or advisor choice
Approvals — which steps require explicit human confirmation before proceeding
Failures — what happens at each failure point (retry, cancel, escalate)
Audit — what is written to the audit log and when
Version — every workflow is versioned; audits record which version ran
Key design: this is construction from scratch, not a recommendation against an existing portfolio. Proposals are first-class objects — savable, versioned, retrievable.
Advisor marks specific funds for daily monitoring. A background job fetches fund data each day and compares against watch criteria (NAV drop, AUM change, rating change, manager change). Alerts are generated by the job. The agent only retrieves and explains alerts — it never evaluates the monitoring rule itself. The agent can help an advisor set up a watch, but the watch runs entirely outside the agent loop.
Services compute. Agent reasons.
Every capability response carries a reasoning package — a structured object containing the data, assumptions, and freshness metadata the agent needs to continue reasoning without re-fetching.
{
"capability": "get_portfolio_analysis",
"computed_at": "2026-07-30T09:00:00Z",
"freshness_valid_until": "2026-07-30T21:00:00Z",
"holdings_snapshot": [...],
"metrics": { ... },
"assumptions": { "benchmark": "Nifty 50", "period": "1Y" },
"summary": "Portfolio is overweight large-cap growth, underweight debt."
}
The planner checks freshness before reusing a reasoning package. If it is still fresh, it reuses — no additional capability call. If stale, it re-fetches.
Effect: the agent builds up a context of structured reasoning packages through the conversation. Follow-up questions ("why is the Sharpe low?") are answered from existing packages, not new fetches. This makes the system faster and gives it a clear identity — it reuses reasoning, it does not constantly recompute.
Purpose: Convert structured intent into the lowest-cost executable capability plan that satisfies all constraints.
| Goal | What It Means |
|---|---|
| Deterministic | Same inputs produce same plan |
| Explainable | Every step in the plan has a reason |
| Cheap | No unnecessary capability calls or loop iterations |
| Recoverable | Can replan from any point in partial execution |
The planner does:
The planner does not:
{
"intent": { "type": "...", "goal": "...", "entities": {} },
"session_context": { "conversation_history": [], "active_reasoning_packages": [] },
"world_state": { "completed_steps": [], "pending_approvals": [] },
"capability_graph": { ... },
"available_workflows": [ ... ],
"constraints": {
"latency_budget_ms": 5000,
"token_budget": 4000,
"permissions": ["advisor"],
"freshness_required": true
},
"policy_hints": [ ... ]
}
{
"plan_id": "plan_abc123",
"reasoning_strategy": "comparison",
"workflow": "replace_fund | null",
"steps": [
{
"step_id": 1,
"capability": "get_fund_details",
"inputs": { "scheme_code": "INF200K01150" },
"reason": "Need current fund profile before replacement analysis",
"reuse_from_package": null
}
],
"missing_info": [],
"estimated_cost": "medium",
"estimated_latency_ms": 1200
}
1. Intent Analysis → classify the request type
2. Goal Builder → translate intent into a structured goal
3. Context Builder → load session memory, active reasoning packages
4. Data Availability Check → which data is already fresh? what must be fetched?
5. Missing Info Resolver → what must be asked from the advisor before planning?
6. Reasoning Strategy → lookup / comparison / simulation / screening / construction
7. Workflow Selector → does a known workflow match this goal? use it.
8. Capability Planner → if no workflow, compose capability steps
9. Constraint Optimizer → minimize cost and latency while satisfying all constraints
10. Plan Verifier → check invariants, permissions, policy hints
11. Handoff → structured plan to execution engine
Key principle: minimize capability calls, maximize reuse of existing reasoning packages. The planner checks freshness at step 4 before selecting capabilities at step 8.
| Strategy | When Used |
|---|---|
| Lookup | Single entity retrieval — fund profile, client details |
| Comparison | Side-by-side analysis of 2+ entities |
| Simulation | What-if — holdings override, scenario analysis |
| Screening | Filter a large set to a ranked short-list |
| Construction | Build something new — investment proposal, portfolio |
| Explanation | Advisor wants to understand an existing result |
| Trigger | Action |
|---|---|
| Capability call fails (transient) | Retry with backoff, then replan if repeated |
| Capability call fails (permanent) | Replan without that capability, surface to advisor if no alternative |
| Missing data discovered mid-plan | Pause, ask advisor, resume |
| Policy engine rejects a step | Replan around the constraint |
| Advisor changes intent mid-conversation | Rebuild plan from current world state |
| Freshness check fails on reuse | Re-fetch and continue |
Memory is not a storage question. It is a lifecycle and ownership question.
Core principle: Nothing becomes memory by accident. Every write to semantic or episodic memory is an intentional, policy-governed act triggered by a committed workflow.
| Type | Lifecycle | Owner | Mutable |
|---|---|---|---|
| Conversation | Ephemeral — expires end of session | Session manager | Yes (trimmed) |
| Reasoning Packages | Short-lived — TTL-based cache | Execution engine | No (replaced on re-fetch) |
| Semantic Memory | Durable — current truth | Client data service | Yes (updates replace) |
| Episodic Memory | Durable — permanent history | Memory manager | No (append-only) |
| Operational Logs | Durable — infrastructure record | Observability layer | No |
When a workflow completes with advisor approval and successful execution:
Workflow reaches COMMITTED state
│
▼
Policy check: is this an event or a fact change?
│
┌────┴────┐
▼ ▼
EVENT FACT CHANGE
│ │
▼ ▼
Append to Update semantic
episodic memory (current
memory truth replaced)
(who, what, when, why)
│
▼
Log the memory write (audit record)
│
▼
Expire reasoning package (no longer needed)
Only the decision and rationale summary are stored. The full reasoning package is not persisted — it expires.
Purpose: Define when a workflow becomes business truth and what is permitted to enter long-term memory.
Only committed workflows can modify business state or write to semantic and episodic memory. Everything else is an execution attempt and is never treated as business history.
DRAFT
│
▼
VALIDATED ← input schema and policy pre-check
│
▼
PLANNING ← planner generates execution plan
│
▼
READY ← plan verified, awaiting execution
│
▼
EXECUTING ← steps running
│
├── (if human approval required)
│ ▼
│ AWAITING_APPROVAL
│ │
│ ┌────┴────┐
│ ▼ ▼
│ APPROVED REJECTED / EXPIRED
│ │
│ ▼
└── RESUMED
│
▼
EXECUTION_COMPLETE
│
▼
COMMIT_VALIDATOR ← checks: all steps done, approvals present, policy still valid, data still fresh
│
├── fail → COMMIT_FAILED (logged, surfaced to advisor)
│
▼
COMMITTED ← only this state writes to memory or business state
│
▼
COMPLETED
Failure branches from any state: VALIDATION_FAILED / PLANNING_FAILED / EXECUTION_FAILED / CANCELLED / EXPIRED
All of these happen together or none of them do:
| Memory Layer | Written By | When | Mutable |
|---|---|---|---|
| Semantic | Commit phase only | On COMMITTED | Yes — updates replace current truth |
| Episodic | Commit phase only | On COMMITTED | No — append-only |
| Reasoning Package | Execution engine | On each capability response | Replaced on re-fetch, expires by TTL |
| Operational Log | Every state transition | Always | No |
Before entering the COMMITTED state, the commit validator checks:
Target: 9 parts.
One chapter per domain. Each chapter contains: purpose, responsibilities, non-responsibilities, capability catalog, capability contracts, data ownership, freshness SLOs, permissions, failure modes, sequence diagram, state diagram, examples.
Domains: Client · Portfolio · Fund · Recommendation · Monitoring · Workflow
Planner : Intent classification, goal builder, context builder, data availability and freshness check, missing info resolver, reasoning strategy selector, workflow selector, capability planner, constraint optimizer, plan verifier, replanner.
Execution Engine : Execution model, state machine, retry semantics, idempotency, parallel execution, human approval, cancellation, resume, timeouts, context propagation, reasoning packages.
Context Builder : The agent does not go from tool result → answer. It goes from tool result → reasoning package → continued conversation. That is a major architectural idea that deserves its own chapter. Reasoning package schema, freshness management, compaction rules, handoff to planner on subsequent turns.
Memory lifecycle, promotion rules, ownership, TTL, freshness, compaction, retrieval strategy, embedding strategy, conflict resolution, memory manager service, memory APIs, memory schema per type.
Policy engine design, capability policies, human approval flows, access control, compliance rules, data retention, audit requirements, workflow commit policy, policy DSL or schema.
Every capability in the system gets its own contract using the standard schema (see Capability Specification Schema above). No undocumented capabilities in production.
AWS architecture, MCP server design, gRPC service contracts, Redis (caching layer), PostgreSQL schema principles, S3 usage patterns, SQS job queue design, event bus, observability stack, model routing, prompt cache strategy.
Benchmark scenarios, regression suite, golden conversations, tool-call sequence verification, planner accuracy metrics, latency budgets, cost benchmarks, reasoning quality rubric, memory quality checks, failure injection tests, chaos testing plan.
Every major decision. Target 30–40 ADRs by v1.0. See ADR index in Part I.