Agent Output Formats
Finance CLI already has command-level schemas in tools.json, an OpenAPI-style adapter contract in openapi.json, and a stable JSON result envelope:
{"ok": true, "data": {}, "error": null, "warnings": []}Those remain the canonical audit and integration format. The record formats are an additive rendering layer for cases where an LLM needs dense structured facts rather than the full provider payload.
Architecture
Section titled “Architecture”The current flow is:
provider response -> service/command payload -> FinanceCommandResult -> output rendererThe agent-oriented path adds one internal step:
provider response -> service/command payload -> normalized Record[] -> generic rendererAdapters should only normalize provider-specific shapes into Record objects. Renderers should not know whether a row came from SEC, Yahoo, GDELT, FMP, transcripts, or a future provider.
Folder Shape
Section titled “Folder Shape”finance_cli/ schemas.py # FinanceCommandResult and normalized Record dataclass records.py # RecordAdapter protocol, dict normalizer, generic renderers cli/ main.py # --output and renderer controls formatting.py # bridges FinanceCommandResult to record renderersscripts/ generate_cli_docs.py # publishes result envelope, record schema, output formatsdocs-site/ src/content/docs/ # human and agent-facing docsThis keeps the existing command registry and generated schema files as the source of truth. Future APIs should add or adapt service payloads, not write one-off compact formatters.
Record Schema
Section titled “Record Schema”from dataclasses import dataclass, fieldfrom typing import Any
@dataclass(frozen=True)class Record: entity: str kind: str period: str | None = None timestamp: str | None = None fields: dict[str, Any] = field(default_factory=dict) source: str | None = None metadata: dict[str, Any] | None = NoneUse entity for the subject, such as AAPL, NVDA, US, or a query string. Use kind for the semantic row type, such as filings_statement_row, market_quote, news_article, or earning. Put source-specific facts in fields, not in renderer code.
Adapter Interface
Section titled “Adapter Interface”from typing import Any, Protocol
class RecordAdapter(Protocol): def to_records(self, payload: Any, *, command: str | None = None) -> list[Record]: """Normalize a provider or command payload into records."""The built-in dict adapter handles existing Finance CLI payloads by looking for common row containers such as rows, filings, articles, events, estimates, transcripts, and grouped symbols. For a new provider with an unusual shape, write a small adapter that returns Record[]; do not add a provider-specific renderer.
Renderer Controls
Section titled “Renderer Controls”Use JSON when you need the full command contract:
finance market.quote AAPL --output jsonUse compact formats when the agent only needs normalized facts:
finance market.quote AAPL --output compact --fields last_price,market_cap,currencyfinance filings.statement AAPL statement=income --output schema --fields label,value,unit --max-records 20finance news.search symbol=NVDA max_records=5 --output schema --fields title,url,domainfinance calendar.earnings AAPL --output schema --fields earnings_date,eps_estimate,reported_epsGlobal record-renderer controls:
| Option | Applies To | Use |
|---|---|---|
--fields a,b,c | compact, schema | Select record fields to render. Structural fields stay available. |
--max-records N | compact, schema | Bound repeated rows before rendering. |
--max-chars N | compact, schema | Apply an approximate final character cap. |
Output Examples
Section titled “Output Examples”Normalized record:
Record( entity="AAPL", kind="financial_metric", period="2024Q4", fields={"revenue": "119.6B USD", "net_income": "36.3B USD"}, source="10-K",)Compact:
AAPL|financial_metric|2024Q4|revenue=119.6B USD|net_income=36.3B USD|src=10-KSchema-once rows:
schema|entity|kind|period|source|revenue|net_incomerow|AAPL|financial_metric|2024Q4|10-K|119.6B USD|36.3B USDThe schema renderer derives its header from the present Record structural fields plus the selected or discovered field names. It does not use command-specific column templates.
Domain Examples
Section titled “Domain Examples”SEC filing statement row:
Record( entity="AAPL", kind="filings_statement_row", period="2024", fields={ "statement": "income", "label": "Net sales", "value": 391035000000, "unit": "USD", "accession_no": "0000320193-24-000123", }, source="sec_edgar",)Stock quote:
Record( entity="AAPL", kind="market_quote", timestamp="2026-05-20T14:30:00Z", fields={"last_price": 190.12, "market_cap": 2960000000000, "currency": "USD"}, source="yfinance",)News article:
Record( entity="NVDA", kind="news_article", timestamp="2026-05-20T12:15:00Z", fields={"title": "NVIDIA supplier shares rise", "domain": "example.com", "url": "https://example.com/a"}, source="gdelt",)Earnings date:
Record( entity="AAPL", kind="earning", timestamp="2026-07-30", fields={"eps_estimate": 1.42, "reported_eps": None, "surprise": None}, source="yfinance",)Serialization Tradeoffs
Section titled “Serialization Tradeoffs”| Format | Best For | Tradeoff |
|---|---|---|
| JSON envelope | Audits, MCP/tool wrappers, tests, replay | Verbose repeated keys and nested payloads. |
| Normalized JSON records | Programmatic adapter boundaries | Still key-heavy, but schema is stable. |
| Compact text | Dense inline facts for LLM context | Less self-describing than JSON. |
| Schema-once rows | Many records with the same fields | Requires the consumer to retain the header. |
| Markdown table | Human scan plus LLM comparison | Prefer schema rows in CLI output; render Markdown at the presentation layer if needed. |
| TSV | Very compact tabular data | Harder to preserve nested citations and escaping. |
| YAML | Readable config-like data | Usually more tokens than compact rows and easier to misparse. |
| MessagePack | Binary transport/storage | Not useful inside LLM context because models see text tokens. |
Default recommendation: keep --output json for source-of-truth capture, then render selected records as compact or schema for agent context. compact is best for a few records; schema is best when many rows share fields.
RAG And Chunking
Section titled “RAG And Chunking”Records are good retrieval units because each row has stable subject, kind, time, fields, and source. A practical RAG pipeline can:
- Store
Record.to_dict()as metadata. - Embed the
compactrendering as chunk text, orschemarows when the chunk contains repeated records. - Build chunk IDs from
entity,kind,periodortimestamp, andsource. - Keep long document text out of default compact fields and retrieve it with
document.windowor command-specific JSON when needed. - Re-render retrieved records into
schemarows before passing many similar rows back to an LLM.
This avoids one formatter per API while preserving enough structure for citation, ranking, and deterministic replay.