Skip to content

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.

The current flow is:

provider response -> service/command payload -> FinanceCommandResult -> output renderer

The agent-oriented path adds one internal step:

provider response -> service/command payload -> normalized Record[] -> generic renderer

Adapters 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.

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 renderers
scripts/
generate_cli_docs.py # publishes result envelope, record schema, output formats
docs-site/
src/content/docs/ # human and agent-facing docs

This 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.

from dataclasses import dataclass, field
from 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 = None

Use 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.

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.

Use JSON when you need the full command contract:

Terminal window
finance market.quote AAPL --output json

Use compact formats when the agent only needs normalized facts:

Terminal window
finance market.quote AAPL --output compact --fields last_price,market_cap,currency
finance filings.statement AAPL statement=income --output schema --fields label,value,unit --max-records 20
finance news.search symbol=NVDA max_records=5 --output schema --fields title,url,domain
finance calendar.earnings AAPL --output schema --fields earnings_date,eps_estimate,reported_eps

Global record-renderer controls:

OptionApplies ToUse
--fields a,b,ccompact, schemaSelect record fields to render. Structural fields stay available.
--max-records Ncompact, schemaBound repeated rows before rendering.
--max-chars Ncompact, schemaApply an approximate final character cap.

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-K

Schema-once rows:

schema|entity|kind|period|source|revenue|net_income
row|AAPL|financial_metric|2024Q4|10-K|119.6B USD|36.3B USD

The 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.

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",
)
FormatBest ForTradeoff
JSON envelopeAudits, MCP/tool wrappers, tests, replayVerbose repeated keys and nested payloads.
Normalized JSON recordsProgrammatic adapter boundariesStill key-heavy, but schema is stable.
Compact textDense inline facts for LLM contextLess self-describing than JSON.
Schema-once rowsMany records with the same fieldsRequires the consumer to retain the header.
Markdown tableHuman scan plus LLM comparisonPrefer schema rows in CLI output; render Markdown at the presentation layer if needed.
TSVVery compact tabular dataHarder to preserve nested citations and escaping.
YAMLReadable config-like dataUsually more tokens than compact rows and easier to misparse.
MessagePackBinary transport/storageNot 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.

Records are good retrieval units because each row has stable subject, kind, time, fields, and source. A practical RAG pipeline can:

  1. Store Record.to_dict() as metadata.
  2. Embed the compact rendering as chunk text, or schema rows when the chunk contains repeated records.
  3. Build chunk IDs from entity, kind, period or timestamp, and source.
  4. Keep long document text out of default compact fields and retrieve it with document.window or command-specific JSON when needed.
  5. Re-render retrieved records into schema rows 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.