Facio's Tool Result Economy Discipline: How AI Agents Get Just Enough Information From Every Tool Call Without Wasting Their Context Window
AI agents call tools. The tools return data. The data goes into the agent's context window. The context window is finite. The agent calls many tools. The data accumulates. The agent runs out of context. The agent loses earlier information. The agent degrades.
The naive approach returns everything from every tool call. The agent gets the full database row, the full API response, the full document. The agent has all the data. The agent also has no context left for important things.
Facio's tool result economy discipline gives AI agents structured mechanisms to manage what tools return. The tools return summaries when summaries suffice. The tools return projections when only projections are needed. The tools return pointers when the full data can be fetched later. The agent's context window is spent on what matters.
Here's how the discipline works, what tool result economy it includes, and why tool result economy discipline is what makes AI agents operate efficiently over long sessions where context is the limiting resource.
The Tool Result Economy Reality
Production AI agents face a tool result economy problem:
Problem 1: Oversized results. The agent calls a database query, gets 10,000 rows. The agent doesn't need 10,000 rows. The agent needs 5 rows. The other 9,995 rows waste context.
Problem 2: Verbose formatting. The API returns a JSON blob with 50 fields. The agent needs 3 fields. The other 47 fields flood the context.
Problem 3: Repeating data. The agent calls the same tool multiple times. Each call returns the same data. The data is duplicated in context. The waste compounds.
Problem 4: Long-lived irrelevant data. The agent called a tool early in the conversation. The data is in context. The data is no longer relevant. The data still takes up space.
Problem 5: High-cardinality enumerations. The agent gets a list of 1,000 customers. The agent only needs to know 5 are relevant. The list fills context.
Problem 6: Cumulative waste. Each tool call adds a small amount of waste. After 50 tool calls, the waste is huge. The agent runs out of context.
The naive approach — return everything, hope the agent sorts it out — fails every test. The team ends up with a system where context is the bottleneck.
The Tool Result Economy Discipline
Facio's tool result economy discipline has five pillars. Each addresses a different aspect of result economy.
Pillar 1: Result Projection (Return Only Requested Fields)
Tools support projection — returning only specific fields:
# Tool result projection
tool_projection = {
"postgres.read_customer_data": {
"default_projection": ["id", "name", "email", "status"],
"available_fields": ["id", "name", "email", "status", "phone", "address", "ssn", "payment_info", "preferences"],
"projection_strategies": {
"minimal": ["id", "name"],
"standard": ["id", "name", "email", "status"],
"full": "all_available_fields",
},
"projection_examples": {
"support_inquiry": ["id", "name", "email", "status"],
"billing_issue": ["id", "name", "email", "payment_info"],
"verification": ["id", "name", "email", "phone"],
},
},
"stripe.list_charges": {
"default_projection": ["id", "amount", "status", "created"],
"available_fields": ["id", "amount", "status", "created", "customer", "description", "metadata", "refunded", "failure_message"],
"projection_strategies": {
"minimal": ["id", "amount", "status"],
"standard": ["id", "amount", "status", "created"],
"security": ["id", "amount", "status", "customer"],
},
},
"github.get_pull_request": {
"default_projection": ["number", "title", "state", "merged"],
"available_fields": ["number", "title", "state", "merged", "body", "comments", "reviews", "commits", "files", "labels"],
"projection_strategies": {
"summary": ["number", "title", "state", "merged"],
"review": ["number", "title", "state", "reviews", "files"],
"discussion": ["number", "title", "comments", "reviews"],
},
},
}
def execute_with_projection(tool_name, arguments, projection_strategy="standard"):
tool = tool_projection[tool_name]
projection = tool["projection_strategies"][projection_strategy]
return execute_tool(tool_name, arguments, projection=projection)
The projection ensures the agent gets only what it asked for. The context window is preserved.
Pillar 2: Result Summarization (Compress Large Results)
Large results are summarized:
# Result summarization
result_summarization = {
"summarization_strategies": {
"list_summarization": {
"description": "Summarize long lists",
"method": "group_by_relevant_dimension_AND_aggregate",
"examples": [
{
"input": "1000 customers with status, region, signup_date",
"output": "Customers: 250 active in EU, 180 active in US, 320 active in APAC, 250 inactive. Total: 1000.",
},
{
"input": "500 events with type, timestamp, severity",
"output": "Events: 380 type_A, 95 type_B, 25 type_C. Severity: 12 high, 488 low. Timestamps: 2026-07-30 09:00-12:00.",
},
],
},
"tabular_summarization": {
"description": "Summarize tabular data",
"method": "extract_key_columns_AND_aggregate",
"examples": [
{
"input": "100 orders with id, customer, amount, status",
"output": "Orders: 60 completed totalling $45,000, 25 pending totalling $18,000, 15 failed. Average: $510.",
},
],
},
"narrative_summarization": {
"description": "Summarize long text",
"method": "extractive_summarization_with_key_facts",
"examples": [
{
"input": "10,000-word document",
"output": "5-sentence summary capturing key points, decisions, and action items",
},
],
},
},
"summarization_query": {
"tool_name": "stripe.list_charges",
"results_count": 1000,
"context_use": "full_data_uses_100000_tokens",
"summarized_use": "summary_uses_500_tokens",
"saved": 99500,
},
}
def summarize_if_large(tool_result, threshold_tokens=2000):
estimated_tokens = count_tokens(tool_result)
if estimated_tokens > threshold_tokens:
return summarize_result(tool_result, strategy="auto")
return {"result": tool_result, "summarized": False}
The summarization reduces the data size. The context is preserved.
Pillar 3: Result Pagination (Chunk Large Results)
Large results are paginated:
# Result pagination
result_pagination = {
"pagination_strategies": {
"page_based": {
"description": "Return pages of results",
"page_size": 50,
"use_case": "user-facing_lists",
"example": "page 1 of 20 (50 items each)",
},
"cursor_based": {
"description": "Return next cursor for results",
"page_size": 100,
"use_case": "infinite_scrolling",
"example": "next_cursor: 'abc-123', has_more: True",
},
"limit_with_count": {
"description": "Return top N results with total count",
"limit": 20,
"use_case": "preview before fetching all",
"example": "20 of 1,247 results, has_more: True",
},
},
"agent_pagination_handling": {
"fetch_initial_page": "fetch with reasonable limit",
"decide_to_fetch_more": "if agent needs more detail",
"fetch_specific_page": "if agent knows what it needs",
"summarize_instead": "if agent doesn't need full data",
},
}
def paginated_tool_call(tool_name, arguments, page_size=50):
result = execute_tool(tool_name, {**arguments, "limit": page_size})
if result["total_count"] > page_size:
return {
"current_page": result["items"],
"total_count": result["total_count"],
"has_more": True,
"next_cursor": result.get("next_cursor"),
"summary": f"Showing {page_size} of {result['total_count']} total",
}
return {"current_page": result["items"], "total_count": result["total_count"], "has_more": False}
The pagination enables the agent to start small. The agent can opt in to more.
Pillar 4: Reference-Based Results (Pointers, Not Data)
Large results are returned as references:
# Reference-based results
reference_based_results = {
"reference_patterns": {
"document_reference": {
"description": "Return document reference, not content",
"example_in_context": "Document: customer_complaint_2026_07_30.pdf (47 pages, 23000 words)",
"fetch_when": "agent needs specific section",
"fetch_method": "specify section_AND_fetch_only_that_section",
},
"database_record_reference": {
"description": "Return record reference, not full record",
"example_in_context": "Customer: cust-12345 (record in postgres.customers)",
"fetch_when": "agent needs specific fields",
"fetch_method": "fetch_with_projection",
},
"log_reference": {
"description": "Return log reference, not full logs",
"example_in_context": "Logs: 1500 entries from 2026-07-30 (request_id: req-abc-123)",
"fetch_when": "agent needs to analyze logs",
"fetch_method": "fetch_with_filter_and_projection",
},
"file_reference": {
"description": "Return file reference, not content",
"example_in_context": "File: data.csv (10,000 rows, 2.3MB)",
"fetch_when": "agent needs to analyze specific rows",
"fetch_method": "fetch_with_specific_query",
},
},
"context_savings": {
"document_reference_vs_full": "47-pages (35000 tokens) -> 1-line reference (50 tokens)",
"database_record_reference_vs_full": "full-record (500 tokens) -> reference (20 tokens)",
"log_reference_vs_full": "1500-entries (50000 tokens) -> reference (50 tokens)",
},
}
def return_as_reference(tool_result, reference_type):
return {
"type": "reference",
"reference_id": tool_result["id"],
"reference_type": reference_type,
"summary": generate_short_summary(tool_result),
"fetch_instructions": f"fetch_with_specific_query_to_get_full_data",
}
The reference-based approach keeps context light. The agent can fetch when needed.
Pillar 5: Result Eviction (Remove When No Longer Needed)
Old tool results are evicted from context:
# Result eviction
result_eviction = {
"eviction_strategies": {
"lru_eviction": {
"description": "Evict least recently used results",
"when": "context_window > 80% full",
"keep": "recently_used_AND_marked_important",
"example": "after 50 tool calls, evict oldest 20",
},
"relevance_eviction": {
"description": "Evict based on relevance scoring",
"when": "context_window > 80% full",
"scoring": "recency + relevance_to_current_task + use_count",
"example": "evict low-relevance old results",
},
"task_completion_eviction": {
"description": "Evict results that are no longer needed for the task",
"when": "task phase changes",
"example": "after_lookup_phase_evict_lookup_results",
},
"manual_eviction": {
"description": "Agent explicitly evicts results",
"when": "agent knows it doesn't need the result",
"example": "after_using_lookup_result_evict_lookup_response",
},
},
"eviction_protection": {
"protected_results": "results_marked_important_never_evicted",
"recently_used": "results_used_in_last_5_turns_not_evicted",
"in_active_use": "results_referenced_in_current_reasoning_not_evicted",
},
"eviction_logging": {
"every_eviction": "logged_with_reason_and_savings",
"savings_aggregated": "tracks_total_tokens_saved",
"re_eviction_allowed": "evicted_result_can_be_re_fetched",
},
}
def evict_results_if_needed(context_window, threshold=0.8):
if context_window.usage_ratio > threshold:
evictable = identify_evictable_results(context_window)
for result in evictable:
evict_result(context_window, result, reason="context_pressure")
log_eviction(result, "context_pressure")
The eviction prevents context overflow. The agent stays within budget.
The Tool Result Economy Patterns
Several patterns emerge from disciplined tool result economy.
Pattern 1: Result Tiering
Results are categorized by tier:
# Result tiering
result_tiers = {
"tier_1_essential": {
"description": "Must keep in context",
"examples": ["current_user_message", "system_instructions", "current_task", "active_decision_context"],
"eviction_allowed": False,
},
"tier_2_important": {
"description": "Keep unless evicted",
"examples": ["recent_tool_results", "decision_context", "key_facts"],
"eviction_allowed": "if_needed",
"eviction_strategy": "LRU",
},
"tier_3_supporting": {
"description": "Evict first under pressure",
"examples": ["lookup_results", "earlier_tool_outputs", "supporting_evidence"],
"eviction_allowed": True,
"eviction_strategy": "eager",
},
"tier_4_transient": {
"description": "Evict immediately",
"examples": ["debug_info", "verbose_logs", "formatting_metadata"],
"eviction_allowed": True,
"eviction_strategy": "immediate",
},
}
def tier_result(tool_result, context):
if is_critical_to_current_task(tool_result, context):
return "tier_1_essential"
elif is_recently_used(tool_result, context):
return "tier_2_important"
elif is_supporting_evidence(tool_result, context):
return "tier_3_supporting"
else:
return "tier_4_transient"
The tiering provides eviction priority. The important data stays.
Pattern 2: Result Compression
Results are compressed when stored:
# Result compression
result_compression = {
"compression_strategies": {
"json_compaction": {
"description": "Remove whitespace, use short keys",
"savings": "10-20%",
"use_case": "all_json_results",
},
"redundant_field_removal": {
"description": "Remove fields that are empty or null",
"savings": "5-15%",
"use_case": "verbose_api_responses",
},
"array_deduplication": {
"description": "Deduplicate repeated values",
"savings": "varies",
"use_case": "results_with_repeated_values",
},
"format_conversion": {
"description": "Convert verbose formats to compact",
"savings": "20-50%",
"use_case": "convert_xml_to_json_AND_compact",
},
"abbreviation": {
"description": "Use abbreviations for known keys",
"savings": "5-10%",
"use_case": "known_apis",
},
},
"compression_strategy_choice": {
"lossless_when": "data needed for reasoning",
"lossy_when": "data is supporting reference",
"summarization_when": "data is large but not detail-critical",
},
}
The compression reduces result size. The context is preserved.
Pattern 3: Result Caching
Cached results are reused instead of refetched:
# Result caching
result_caching = {
"cache_scope": {
"session_cache": "results cached for current session",
"user_cache": "results cached for user across sessions",
"global_cache": "results cached globally for popular queries",
},
"cache_strategies": {
"exact_match": {
"description": "Cache exact query results",
"use_case": "idempotent_queries",
"ttl_seconds": 300,
},
"parameter_match": {
"description": "Cache by key parameters",
"use_case": "mostly_stable_queries",
"ttl_seconds": 600,
},
"semantic_match": {
"description": "Cache by semantic similarity",
"use_case": "similar_queries",
"ttl_seconds": 1800,
},
},
"cache_invalidation": {
"on_data_change": "cache_invalidated_when_underlying_data_changes",
"on_policy_change": "cache_invalidated_when_security_policy_changes",
"ttl_expiry": "cache_evicted_after_ttl",
"manual_invalidation": "operator_can_invalidate_cache",
},
"cache_response": {
"cached": True,
"original_query": "...",
"cached_at": "2026-07-31T09:00:00Z",
"expires_at": "2026-07-31T09:05:00Z",
},
}
def cached_tool_call(tool_name, arguments, cache_strategy="parameter_match"):
cache_key = generate_cache_key(tool_name, arguments, cache_strategy)
cached = cache.get(cache_key)
if cached and not is_expired(cached):
return {"result": cached["result"], "cached": True, "cache_age_seconds": time.now() - cached["cached_at"]}
result = execute_tool(tool_name, arguments)
cache.set(cache_key, result, ttl=get_ttl(cache_strategy))
return {"result": result, "cached": False}
The caching prevents redundant tool calls. The context is preserved.
Pattern 4: Streaming Results
Long-running tools stream results:
# Streaming results
streaming_results = {
"streaming_patterns": {
"chunked_results": {
"description": "Return results in chunks",
"use_case": "large_queries",
"implementation": "agent_processes_chunks_as_they_arrive",
"context_savings": "only_processed_chunks_in_context",
},
"progressive_results": {
"description": "Return progressively more detailed results",
"use_case": "exploration",
"implementation": "agent_requests_more_detail_if_needed",
"context_savings": "summary_first_then_detail",
},
"filtered_stream": {
"description": "Filter streaming results by criteria",
"use_case": "relevant_data_only",
"implementation": "agent_specifies_filter",
"context_savings": "filtered_data_only",
},
},
"streaming_response": {
"chunk_1": {"items": [...], "is_first": True, "has_more": True},
"chunk_2": {"items": [...], "is_first": False, "has_more": True},
"chunk_n": {"items": [...], "is_first": False, "has_more": False, "summary": "..."},
},
}
def stream_tool_call(tool_name, arguments, chunk_size=50):
for chunk in execute_tool_streaming(tool_name, arguments, chunk_size):
yield {"chunk": chunk, "is_last": chunk["is_last"]}
The streaming reduces peak context usage. The agent processes incrementally.
Pattern 5: Tool Result Observability
Tool result economy is observable:
# Tool result observability
tool_result_metrics = {
"metrics": {
"average_result_size_tokens": 850,
"average_projection_savings": 0.62,
"average_summarization_savings": 0.85,
"average_eviction_savings": 0.40,
"average_cache_hit_rate": 0.45,
"context_window_pressure_events_per_hour": 12,
},
"per_tool_metrics": {
"postgres.read_customer_data": {
"average_size": 500,
"projection_rate": 0.80,
"summary_rate": 0.05,
},
"stripe.list_charges": {
"average_size": 1500,
"projection_rate": 0.65,
"summary_rate": 0.25,
"pagination_rate": 0.10,
},
},
"alerts": [
{"level": "warning", "message": "Tool result size growing over time"},
{"level": "info", "message": "Cache hit rate improved"},
{"level": "warning", "message": "Context window pressure events increasing"},
],
"dashboard": "https://internal-dashboard/facio/tool-results",
}
The observability surfaces result economy patterns. The team sees optimization opportunities.
The Tool Result Economy Discipline Doesn't Do
Honest limitations:
- It doesn't eliminate context limits. The context window is finite. The discipline works within it.
- It can hide important data. Aggressive summarization can miss important details. The discipline needs tuning.
- It adds complexity. Projection, summarization, eviction each add complexity. The discipline requires infrastructure.
- It can be wrong about relevance. The agent might evict the wrong result. The discipline needs re-eviction capability.
- It adds latency. Some compression/summarization takes time. The discipline trades latency for context.
The Tool Result Economy as Operational Practice
Tool result economy discipline is operational practice:
Projection tuning. The team tunes projection strategies. They balance context and detail.
Summarization review. The team reviews summarization quality. They ensure summaries capture key facts.
Eviction policy tuning. The team tunes eviction policies. They balance memory and re-fetch.
Cache strategy review. The team reviews cache strategies. They balance freshness and efficiency.
The practice is what makes the discipline sustainable. Without it, the discipline is too aggressive or too lax. With it, the discipline is calibrated.
The Compound Effect of Tool Result Economy Discipline
Tool result economy discipline compounds:
- Longer sessions. The agent runs longer without running out of context.
- Better reasoning. The agent has context for important decisions.
- Lower costs. Less context means lower token costs.
- Higher reliability. The agent doesn't truncate mid-task.
- More tools available. The agent can use more tools within budget.
The undisciplined approach has the opposite trajectory. Short sessions, poor reasoning, high costs, low reliability, few tools.
Bottom Line
AI agents call tools. The tools return data. The data consumes context. Without tool result economy discipline, the context fills up. With tool result economy discipline, the context is managed.
Facio's tool result economy discipline provides result projection, summarization, pagination, reference-based results, and eviction. The discipline makes AI agents efficient.
The agent without tool result economy is a liability that fills context quickly. The agent with it is efficient. The team trusts the efficient one. The deployment scales with the efficient one.
Because AI agents in production call tools. The question is whether the tools return blobs or managed data. The tool result economy discipline is what makes the answer managed.
See the tool result economy documentation for projection strategies, summarization options, and eviction policies.