PromptisePromptise
Docs
GitHub
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Learning paths> Agentic Engineering with Promptise Foundry
Lesson 33 / 71
46%

Context Engineering for AI Agents: Context Window Management

Context engineering for AI agents in Python: keep what matters in long runs, trim the rest, and cut input tokens 18x. Real token counts per step.

Intermediate26 minContext EngineeringCost ControlModel Context Protocol (MCP)
All 71 lessons↓
  1. ✓What Is an AI Agent? Definition, How It Works, Examples
  2. ✓How to Build an AI Agent in Python (2026 Guide)
  3. ✓What Is an Agent Harness? Parts and How to Build One
  4. ✓System Prompt Best Practices for AI Agents in Python
  5. ✓Best LLM for AI Agents: Test and Switch Models in Python
  6. ✓AI Agent YAML: Define and Run an Agent From a Config File
  7. ✓Streaming LLM Responses with FastAPI: Tokens and Tool Calls
  8. ✓Claude AI Agent with MCP: Use Anthropic Models in Promptise
  9. ✓Gemini AI Agent with MCP: Use Google Models in Promptise
  10. ✓Azure OpenAI Agent with MCP: Use Deployments in Promptise
  11. ✓OpenRouter AI Agent with MCP: Many Models in Promptise
  12. ✓Ollama AI Agent with MCP: Run Local Models in Promptise
  13. ✓LLM Semantic Cache and Model Fallback in Python
  14. ✓What Is MCP (Model Context Protocol)? A Developer's Guide
  15. ✓What Is Tool Calling? LLM Function Calling Explained
  16. ✓MCP Tools vs Resources vs Prompts (With Python Examples)
  17. ✓How to Connect MCP Servers to Your AI Agent in Python
  18. ✓Build a Production MCP Server in Python (Step by Step)
  19. ✓MCP Transports: stdio vs Streamable HTTP vs SSE (Python)
  20. ✓FastAPI MCP Server: Turn Your FastAPI App into MCP Tools
  21. ✓OpenAPI to MCP: Turn Any REST API into an MCP Server
  22. ✓MCP Authentication in Python: API Keys, JWT and OAuth
  23. ✓Build an Authenticated MCP Server for Your AI Agent (Python)
  24. ✓MCP Security Best Practices: A Checklist for MCP Servers
  25. ✓Testing MCP Servers in Python: pytest Without a Live Server
  26. ✓Too Many MCP Tools? LLM Tool Selection, Measured
  27. ✓MCP Server Caching, Timeouts and Circuit Breakers in Python
  28. ✓MCP vs CLI for AI Agents: Tokens, Security, When to Use Each
  29. ✓AI Agent Audit Logging: Tamper-Evident Logs in Python
  30. ✓AI Agent Human in the Loop: Approve Tool Calls in Python
  31. ✓AI Agent Prompt Injection: How to Defend Against It
  32. ✓Build a Multi-Agent System in Python (and When Not To)
  33. 33Context Engineering for AI Agents: Context Window Management
  34. 34How to Deploy an AI Agent with Docker (2026 Guide)
  35. 35LLM Tool Rate Limiting: Per-Client Limits for MCP Servers
  36. 36MCP Long-Running Tasks: Background Jobs in Python
  37. 37MCP Server Monitoring: Prometheus, OpenTelemetry and Logs
  38. 38Python AI Agent Frameworks Compared: Which to Choose (2026)
  39. 39Self-Improving AI Agent: Learn From Tool Failures in Python
  40. 40AI Agent Identity: Authenticate Agents to MCP Servers
  41. 41Code-Action Agents in Python: One Program vs 37 Tool Calls
  42. 42Multi-Tenant AI Agents: Keep Each Customer's Data Apart
  43. 43What Is Agentic RAG? How It Differs from Classic RAG
  44. 44AI Agent Memory in Python: Short-Term and Long-Term
  45. 45Agentic RAG in Python: Give Your AI Agent a Search Tool
  46. 46AI Agent Reasoning Patterns in Python: 9 Patterns Tested
  47. 47Plan-and-Execute Agents in Python: Beyond the ReAct Loop
  48. 48Self-Modifying AI Agent in Python: Open Mode & Limits
  49. 49CrewAI Alternatives: 7 Options and How to Migrate (2026)
  50. 50Migrate from AutoGen: Agent Framework, Promptise and More
  51. 51What Is an LLM Guardrail? Types, Examples and Limits
  52. 52AI Agent Guardrails in Python: Block Injection, Redact PII
  53. 53AI Agent Code Execution Sandbox: Run LLM Code Safely
  54. 54AI Agent Production Checklist: What to Check Before Launch
  55. 55Approval Fatigue: Auto-Approve Safe AI Agent Actions
  56. 56What Is Human in the Loop AI? Approvals for AI Agents
  57. 57AI Agent Notifications: Webhook and Slack Alerts in Python
  58. 58AI Agent Observability: Trace Tool Calls, Tokens and Cost
  59. 59AI Agent Stuck in a Loop? How to Detect and Stop It
  60. 60How to Run, Debug and Deploy an AI Agent in Python
  61. 61AI Agent Cost Tracking: Tokens and Cost per Run and User
  62. 62How to Evaluate AI Agents: Evals, LLM Judges and pass^k
  63. 63AI Agent Cost Control: Budgets That Stop Runaway Spend
  64. 64How to Test LLM Prompts and AI Agents in Python
  65. 65AI Agent Evals in CI: Run Them on Every Pull Request
  66. 66Autonomous AI Agent Runtime: Budgets, Crash Recovery, API
  67. 67Event-Driven AI Agents: Webhook, Cron and File Triggers
  68. 68Durable AI Agents: Crash Recovery and Safe Resume
  69. 69Deploy AI Agents in Production: Long-Running Python Agents
  70. 70What Is an AI Agent Framework? When You Need One
  71. 71LangGraph Alternatives: 7 Options and How to Migrate (2026)

Lessons in this path

  1. ✓What Is an AI Agent? Definition, How It Works, Examples
  2. ✓How to Build an AI Agent in Python (2026 Guide)
  3. ✓What Is an Agent Harness? Parts and How to Build One
  4. ✓System Prompt Best Practices for AI Agents in Python
  5. ✓Best LLM for AI Agents: Test and Switch Models in Python
  6. ✓AI Agent YAML: Define and Run an Agent From a Config File
  7. ✓Streaming LLM Responses with FastAPI: Tokens and Tool Calls
  8. ✓Claude AI Agent with MCP: Use Anthropic Models in Promptise
  9. ✓Gemini AI Agent with MCP: Use Google Models in Promptise
  10. ✓Azure OpenAI Agent with MCP: Use Deployments in Promptise
  11. ✓OpenRouter AI Agent with MCP: Many Models in Promptise
  12. ✓Ollama AI Agent with MCP: Run Local Models in Promptise
  13. ✓LLM Semantic Cache and Model Fallback in Python
  14. ✓What Is MCP (Model Context Protocol)? A Developer's Guide
  15. ✓What Is Tool Calling? LLM Function Calling Explained
  16. ✓MCP Tools vs Resources vs Prompts (With Python Examples)
  17. ✓How to Connect MCP Servers to Your AI Agent in Python
  18. ✓Build a Production MCP Server in Python (Step by Step)
  19. ✓MCP Transports: stdio vs Streamable HTTP vs SSE (Python)
  20. ✓FastAPI MCP Server: Turn Your FastAPI App into MCP Tools
  21. ✓OpenAPI to MCP: Turn Any REST API into an MCP Server
  22. ✓MCP Authentication in Python: API Keys, JWT and OAuth
  23. ✓Build an Authenticated MCP Server for Your AI Agent (Python)
  24. ✓MCP Security Best Practices: A Checklist for MCP Servers
  25. ✓Testing MCP Servers in Python: pytest Without a Live Server
  26. ✓Too Many MCP Tools? LLM Tool Selection, Measured
  27. ✓MCP Server Caching, Timeouts and Circuit Breakers in Python
  28. ✓MCP vs CLI for AI Agents: Tokens, Security, When to Use Each
  29. ✓AI Agent Audit Logging: Tamper-Evident Logs in Python
  30. ✓AI Agent Human in the Loop: Approve Tool Calls in Python
  31. ✓AI Agent Prompt Injection: How to Defend Against It
  32. ✓Build a Multi-Agent System in Python (and When Not To)
  33. 33Context Engineering for AI Agents: Context Window Management
  34. 34How to Deploy an AI Agent with Docker (2026 Guide)
  35. 35LLM Tool Rate Limiting: Per-Client Limits for MCP Servers
  36. 36MCP Long-Running Tasks: Background Jobs in Python
  37. 37MCP Server Monitoring: Prometheus, OpenTelemetry and Logs
  38. 38Python AI Agent Frameworks Compared: Which to Choose (2026)
  39. 39Self-Improving AI Agent: Learn From Tool Failures in Python
  40. 40AI Agent Identity: Authenticate Agents to MCP Servers
  41. 41Code-Action Agents in Python: One Program vs 37 Tool Calls
  42. 42Multi-Tenant AI Agents: Keep Each Customer's Data Apart
  43. 43What Is Agentic RAG? How It Differs from Classic RAG
  44. 44AI Agent Memory in Python: Short-Term and Long-Term
  45. 45Agentic RAG in Python: Give Your AI Agent a Search Tool
  46. 46AI Agent Reasoning Patterns in Python: 9 Patterns Tested
  47. 47Plan-and-Execute Agents in Python: Beyond the ReAct Loop
  48. 48Self-Modifying AI Agent in Python: Open Mode & Limits
  49. 49CrewAI Alternatives: 7 Options and How to Migrate (2026)
  50. 50Migrate from AutoGen: Agent Framework, Promptise and More
  51. 51What Is an LLM Guardrail? Types, Examples and Limits
  52. 52AI Agent Guardrails in Python: Block Injection, Redact PII
  53. 53AI Agent Code Execution Sandbox: Run LLM Code Safely
  54. 54AI Agent Production Checklist: What to Check Before Launch
  55. 55Approval Fatigue: Auto-Approve Safe AI Agent Actions
  56. 56What Is Human in the Loop AI? Approvals for AI Agents
  57. 57AI Agent Notifications: Webhook and Slack Alerts in Python
  58. 58AI Agent Observability: Trace Tool Calls, Tokens and Cost
  59. 59AI Agent Stuck in a Loop? How to Detect and Stop It
  60. 60How to Run, Debug and Deploy an AI Agent in Python
  61. 61AI Agent Cost Tracking: Tokens and Cost per Run and User
  62. 62How to Evaluate AI Agents: Evals, LLM Judges and pass^k
  63. 63AI Agent Cost Control: Budgets That Stop Runaway Spend
  64. 64How to Test LLM Prompts and AI Agents in Python
  65. 65AI Agent Evals in CI: Run Them on Every Pull Request
  66. 66Autonomous AI Agent Runtime: Budgets, Crash Recovery, API
  67. 67Event-Driven AI Agents: Webhook, Cron and File Triggers
  68. 68Durable AI Agents: Crash Recovery and Safe Resume
  69. 69Deploy AI Agents in Production: Long-Running Python Agents
  70. 70What Is an AI Agent Framework? When You Need One
  71. 71LangGraph Alternatives: 7 Options and How to Migrate (2026)
← Path overview

A long agent run fills its context window one tool result at a time. Ten log files later, every model call carries all ten, you pay for them again on every step, and the agent's grip on the original question gets looser. Context engineering for AI agents is the work of deciding what the model sees on each step: what stays, what gets trimmed or summarized, and what it fetches only when it needs it. In this guide you'll build an agent that investigates an incident across ten service logs, meter the tokens of every model call, and compare Promptise Foundry's automatic compaction, its ContextEngine and a full transcript with real numbers, then fix what goes wrong. Every snippet ran against Promptise Foundry 1.3.2 with gpt-5-mini, and every output is what it printed.

[01]

How do you manage the context window of an AI agent?

Keep three kinds of content apart. Rules and facts the agent needs on every step go in the system prompt. Bulk data stays behind tools that return only what the step needs, such as a summary instead of a whole file, with a second tool to drill into details. And the run itself needs a way to stay bounded as tool results pile up. With Promptise Foundry the first two are yours to write, and the third is built in:

Pythonfinal.py
1
2
3
4
5
6
7
8
9
10
11
    agent = await build_agent(
        model="openai:gpt-5-mini",
        servers={"logs": StdioServerSpec(command=sys.executable, args=["lean_server.py"])},
        instructions=INSTRUCTIONS,
    )
    meter = TokenMeter(watch="the number of ERROR lines per service")
    try:
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": QUESTION}]},
            config={"callbacks": [meter]},
        )

On the incident below, this setup read all ten logs and answered every part of the question correctly in three runs out of three, with 15,832 to 17,796 input tokens per run. The agent that read the raw files with nothing trimmed also got it right every time, for 313,783 tokens per run, about twenty times as many. The default setup, which compacts automatically, kept the question in view but never finished: once older logs were cut short, it kept reading them again until it ran out of steps, and the run ended with an error instead of an answer. The rest of this guide shows each of those runs and why they behaved the way they did.


[02]

How context works in a long agent run

Every step of a tool loop is one model call. Without any management, each call sends the instructions, the question, and every tool call and result so far, so the input grows with each step. Promptise Foundry has two mechanisms that change what gets sent:

Rendering diagram…

  • Compaction inside the tool loop. The default agent pattern, ReAct, runs its loop with context_scope="auto". Until there are six tool results, the model sees the full transcript. From then on it sees a compact view: your instructions and any system messages you passed, the current question, its most recent step, and a "facts already gathered" ledger with one line per earlier tool call. Results longer than 2,000 characters are cut to an excerpt that tells the model how to get the full result again. Compaction only changes what the model sees: a call it repeats runs the tool again. agent_pattern="managed" uses that compact view from the first step, and build_agent(context_compaction=...) turns it off or moves the threshold. The Context Lifecycle guide and the Context scope reference describe both.

  • The ContextEngine, before the loop. build_agent(context_engine=...) assembles the instructions, chat history and memory into a token budget, and trims the least important parts first. It assembles once per call to the agent, before any tool runs. Since 1.3.0, what its budget leaves after the instructions and tool definitions also caps the compacted tool loop. The Context Engine docs list its layers.

What survives compaction matters more than how much it saves. This is what the model receives before and after the switch, from Promptise's source and the runs below:

In the prompt

Before compaction

After compaction, in 1.2.1

After compaction, since 1.3.0

Your instructions

Yes

Yes

Yes

The question, passed as a dict

Yes

No

Yes

The question, passed as a HumanMessage

Yes

Yes, but only the first one in the input

Yes, the latest one

System messages you pass in

Yes

No

Yes

Earlier turns of a chat

Yes

No, except the first user message

A short note: the last six messages, 400 characters each

Tool results

Every call and result

The last step's results, plus every result again in the ledger

The last step's results; older ones once each in the ledger, cut to 2,000 characters

A tool call the model repeats

Runs the tool

Gets the earlier result; the tool doesn't run

Runs the tool since 1.3.2 (1.3.0 and 1.3.1 returned the earlier result); reuse is opt-in

Side by side, the full transcript the model receives with fewer than 6 tool results and the compact view it receives with 6 or more, where earlier turns become a short note and older tool results move into a facts already gathered ledger cut to 2,000 characters.
FigureWhat the model receives before and after compaction

[03]

What you need

  • Python 3.10 or newer.

  • Promptise Foundry 1.3.2 or newer from PyPI. tiktoken, which the ContextEngine uses to count tokens exactly for OpenAI models, comes with it.

  • An OpenAI API key. This guide uses gpt-5-mini; other providers work by changing the model string, as listed in Models & Providers.

>_Terminal
pip install "promptise>=1.3.2"
export OPENAI_API_KEY="sk-..."

[04]

Investigate an incident across ten logs, step by step

The job: an incident hit checkout between 14:00 and 14:30. The agent reads the logs of ten services and reports the ERROR count per service, the total, the first ERROR, and the root cause. A small script, make_logs.py, wrote the logs (140 normal lines per service plus the incident) and printed the right answer, so every run can be checked against it:

Output
files: 10 | bytes: 140724
ERROR lines per service: {'gateway': 4, 'inventory': 7, 'orders': 5, 'payments': 2}
total ERROR lines: 18
first ERROR: inventory 14:07:41.204
Step 01

Give the agent the logs as a tool

The tools live in a small MCP server. read_log returns a whole file, the way a first version of a tool usually does:

Pythonlogs_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from pathlib import Path

from promptise.mcp.server import MCPServer

server = MCPServer("logs")
LOG_DIR = Path(__file__).parent / "logs"


def services() -> list[str]:
    return sorted(p.stem for p in LOG_DIR.glob("*.log"))


@server.tool()
async def list_logs() -> list[str]:
    """List the services that have a log file for the incident window."""
    return services()


@server.tool()
async def read_log(service: str) -> str:
    """Return the full log file of one service, every line.

    Args:
        service: A service name from list_logs, for example "orders".
    """
    if service not in services():
        return f"No log for service {service!r}. Call list_logs for valid names."
    return (LOG_DIR / f"{service}.log").read_text()


if __name__ == "__main__":
    server.run()

How big is one result? The ContextEngine's token counter answers that without calling a model:

Pythoncount_tokens.py
1
2
3
4
5
6
7
8
"""How big is each tool result? Count before you send it."""
from pathlib import Path

from promptise import ContextEngine

engine = ContextEngine(model="openai:gpt-5-mini")
for path in sorted(Path("logs").glob("*.log"))[:3]:
    print(f"{path.name:<16} {engine.count_tokens(path.read_text()):>6,} tokens")
Output
auth.log          5,517 tokens
billing.log       5,526 tokens
gateway.log       5,669 tokens

About 5,500 tokens per file, 55,000 for all ten. If MCP servers are new to you, How to Connect MCP Servers to Your AI Agent in Python explains this file line by line.

Step 02

Meter every model call

You can't manage what you don't measure. This LangChain callback records, for each model call, how many messages went in, whether the compaction ledger was among them, whether a phrase you choose was still visible, and the token usage the provider reported:

Pythonmeter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""Record what the model saw and what it cost, one row per model call."""

from langchain_core.callbacks import AsyncCallbackHandler

LEDGER_MARKER = "Facts already gathered"


class TokenMeter(AsyncCallbackHandler):
    def __init__(self, watch: str | None = None):
        self.watch = watch  # a phrase to look for in what the model sees
        self.calls: list[dict] = []

    async def on_chat_model_start(self, serialized, messages, **kwargs):
        sent = messages[0]
        text = "\n".join(str(m.content) for m in sent)
        self.calls.append(
            {
                "messages": len(sent),
                "tool_results": sum(1 for m in sent if m.type == "tool"),
                "ledger": LEDGER_MARKER in text,
                "watch_seen": bool(self.watch) and self.watch in text,
            }
        )

    async def on_llm_end(self, response, **kwargs):
        message = response.generations[0][0].message
        usage = message.usage_metadata or {}
        self.calls[-1].update(
            input=usage.get("input_tokens", 0),
            cached=(usage.get("input_token_details") or {}).get("cache_read", 0),
            output=usage.get("output_tokens", 0),
            wants=", ".join(
                tc["name"] + "".join(f" {v}" for v in tc["args"].values()) for tc in message.tool_calls
            )
            or "final answer",
        )

The file also has a table() method that prints one row per call and a total, priced at OpenAI's list price for gpt-5-mini: $0.25 per million input tokens, $0.025 per million cached input tokens and $2.00 per million output tokens. You pass the meter to any run with config={"callbacks": [meter]}. For tracing in production, Promptise's own observability records tokens per call too; the meter is here because it also shows what the model was sent.

Step 03

Run the long investigation with the default agent

The script asks the question, watches for the phrase "the number of ERROR lines per service" from it, and can build the agent four ways. The instructions ask for one service per step, so the run is long enough to watch it grow:

Pythoninvestigate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Investigate an incident across ten service logs, and meter every model call.

    python investigate.py            # the default agent
    python investigate.py managed    # agent_pattern="managed"
    python investigate.py full       # no compaction: the whole transcript every call
    python investigate.py engine     # the default agent with a ContextEngine
    python investigate.py default lean_server.py   # the same agent with smaller tool results
"""

import asyncio
import sys

from promptise import ContextEngine, build_agent
from promptise.config import StdioServerSpec
from promptise.engine import GraphIterationLimitError

from meter import TokenMeter

INSTRUCTIONS = (
    "You are an SRE assistant. Answer from the logs only. "
    "Work through the logs one service at a time: call one tool for one service, "
    "look at the result, then go to the next. Never ask about two services in one step."
)
QUESTION = (
    "We had an incident on 2026-10-09 between 14:00 and 14:30. Read every service log. "
    "Then tell me: (1) the number of ERROR lines per service, (2) the total, "
    "(3) which service logged the first ERROR and its exact timestamp, "
    "and (4) the root cause in one sentence."
)
SERVER_FILE = sys.argv[2] if len(sys.argv) > 2 else "logs_server.py"
SERVER = StdioServerSpec(command=sys.executable, args=[SERVER_FILE])


async def main(mode: str) -> None:
    options = {
        "full": {"context_compaction": False},
        "engine": {"context_engine": ContextEngine(model="openai:gpt-5-mini")},
        "managed": {"agent_pattern": "managed"},
    }.get(mode, {})

    agent = await build_agent(
        model="openai:gpt-5-mini", servers={"logs": SERVER}, instructions=INSTRUCTIONS, **options
    )
    meter = TokenMeter(watch="the number of ERROR lines per service")
    try:
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": QUESTION}]},
            config={"callbacks": [meter]},
        )
        answer = result["messages"][-1].content
    except GraphIterationLimitError as exc:
        # The run hit its step limit before the model answered.
        answer = f"no answer: {exc}"
    finally:
        await agent.shutdown()
    print(meter.table())
    print("\n>>>", answer)


asyncio.run(main(sys.argv[1] if len(sys.argv) > 1 else "default"))
>_Terminal
python investigate.py
Output
Node 'reason' reached its iteration limit (15) in graph 'react' — not running it again
Stuck node 'reason' — forcing graph end
Graph 'react' at node 'reason': reached its iteration limit (15) before the model gave an answer; the run stopped after a call to read_log whose result the model never saw
call msgs tool msgs ledger   input  cached output  next
   1    2         0     no     333       0     83  list_logs  [watch: seen]
   2    4         1     no     387       0     87  read_log auth  [watch: seen]
   3    6         2     no   5,932   5,760     17  read_log billing  [watch: seen]
   4    8         3     no  11,486  11,264     17  read_log gateway  [watch: seen]
   5   10         4     no  17,183       0     17  read_log inventory  [watch: seen]
   6   12         5     no  22,969  22,016     17  read_log notifications  [watch: seen]
   7    5         1    yes   9,438   9,216    343  read_log auth  [watch: seen]
   8    5         1    yes  10,236  10,112     87  read_log orders  [watch: seen]
   9    5         1    yes  10,411       0    343  read_log billing  [watch: seen]
  10    5         1    yes  11,100       0    919  read_log auth  [watch: seen]
  11    5         1    yes  11,091   8,704    663  read_log billing  [watch: seen]
  12    5         1    yes  11,100   5,632    471  read_log auth  [watch: seen]
  13    5         1    yes  11,091   7,680    279  read_log billing  [watch: seen]
  14    5         1    yes  11,100  10,880    343  read_log payments  [watch: seen]
  15    5         1    yes  11,151       0    343  read_log gateway  [watch: seen]
total: 15 model calls, 155,008 input (91,264 cached) + 4,029 output tokens = $0.0263

>>> no answer: Graph 'react' failed at node 'reason': reached its iteration limit (15) before the model gave an answer; the run stopped after a call to read_log whose result the model never saw

Read it row by row. Each log added about 5,600 input tokens to every later call. After six tool results, list_logs and five logs, compaction switched on at call 7. The message count dropped from 12 to 5: the instructions, the question, the last tool call, its result, and the ledger. The question stayed in view, [watch: seen] on every row. The trouble is the ledger. It cuts each older result to its first 2,000 characters and ends it with a note, [shortened: … characters not shown; call read_log with the same arguments to see the full result (several in one step if you need them together)], and in these logs the ERROR lines start after 14:07, well past the first 2,000 characters.

So the model did what the note says. The ledger's own header adds that it shouldn't call a tool again for these results unless it needs one marked as not shown in full, and every log was marked. From call 7 on it went back to logs it had already read, auth and billing again and again, and each re-read pushed the one before out of full view. Each re-read ran the tool again: compaction changes what the model sees, not what a call does. The instructions made it worse, because they forbid asking for two services in one step.

At call 15 the ReAct loop hit its step limit, with a read_log result the model never got to see. Promptise logged Stuck node 'reason' — forcing graph end, and since 1.3.2 ainvoke then raises GraphIterationLimitError from promptise.engine instead of returning that log file as if it were the answer. investigate.py catches it and prints the message, so the meter's table still shows where the tokens went. Before 1.3.2 the run ended without an error, and result["messages"][-1] was the raw log.

Re-reading a file that can't change is wasted work for the server, so 1.3.2 lets you opt in to reuse: with build_agent(context_compaction=ContextCompaction(reuse_results=["read_log"])), a repeated read_log call gets the earlier result once the loop compacts, and trace_tools prints ↺ Reusing an earlier result for tool: read_log for it. In one run with it, 5 of the 14 log reads were answered that way. The model was sent the same text, though, so the run used about as many tokens, 158,937, and still ran out of steps. Reuse saves tool runs, not context. List only tools whose results can't change during a run, never a status or health check.

On Promptise Foundry 1.2.1 this run failed differently: compaction kept the question only if it was a LangChain HumanMessage, so the dict question was dropped at call 7, and the model asked what to do. 1.3.0 fixed that, and it no longer repeats the latest result in the ledger. The cut to 2,000 characters is what's left, and on whole files it decides the outcome. That's one more reason to make the tools return less, in step 5.

Step 04

Compare it with the full transcript

To see what compaction should be saving, run the same agent with compaction turned off. Since 1.3.0 that's one argument, build_agent(context_compaction=False), which investigate.py full passes:

>_Terminal
python investigate.py full
Output
call msgs tool msgs ledger   input  cached output  next
   1    2         0     no     333       0    147  list_logs  [watch: seen]
   2    4         1     no     387       0    151  read_log auth  [watch: seen]
   3    6         2     no   5,932   5,760     17  read_log billing  [watch: seen]
   …
  10   20         9     no  45,402  45,184     17  read_log shipping  [watch: seen]
  11   22        10     no  50,949  50,816     17  read_log users  [watch: seen]
  12   24        11     no  56,504  56,320  1,393  final answer  [watch: seen]
total: 12 model calls, 313,783 input (311,296 cached) + 1,844 output tokens = $0.0121

>>> Summary from the logs (2026-10-09 14:00–14:30):

1) ERROR counts per service:
- auth: 0
- billing: 0
- gateway: 4
- inventory: 7
- notifications: 0
- orders: 5
- payments: 2
- search: 0
- shipping: 0
- users: 0

2) Total ERROR lines: 18

3) First ERROR: inventory at 2026-10-09T14:07:41.204Z

4) Root cause (one sentence): The inventory service exhausted its database connection pool (db pool exhausted, max=20), producing 500 errors which cascaded to orders (inventory lookup failures), causing gateway 502s and payment cancellations.

Every log was read and every part of the answer is right, in all three runs. The price is growth: the last call sent 56,504 tokens, and the run sent 313,783 in total, because every result rides along on every later call.

One thing kept the bill low here. The cached column shows input the provider served from its prompt cache: a full transcript only ever appends, so each call starts with the previous call's exact text. At list price without the cache, the same run costs about $0.082. Cache hits vary from run to run, and they don't help the context window: with a hundred logs instead of ten, this design simply stops fitting.

Step 05

Make the tools return less

The cheapest token is the one a tool never returns. The agent doesn't need 140 lines of 200 OK to count errors; it needs the counts and the error lines. So summarize where the data lives, and give the model a second tool to drill in when it needs detail:

Pythonlean_server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from collections import Counter
from pathlib import Path

from promptise.mcp.server import MCPServer

server = MCPServer("logs")
LOG_DIR = Path(__file__).parent / "logs"


def services() -> list[str]:
    return sorted(p.stem for p in LOG_DIR.glob("*.log"))


def lines_of(service: str) -> list[str]:
    return (LOG_DIR / f"{service}.log").read_text().splitlines()


@server.tool()
async def list_logs() -> list[str]:
    """List the services that have a log file for the incident window."""
    return services()


@server.tool()
async def log_summary(service: str) -> dict:
    """Summarize one service's log: line counts per level, time range, and every ERROR line.

    Args:
        service: A service name from list_logs, for example "orders".
    """
    if service not in services():
        return {"error": f"No log for service {service!r}. Call list_logs for valid names."}
    lines = lines_of(service)
    return {
        "service": service,
        "lines": len(lines),
        "levels": dict(Counter(line.split()[1] for line in lines)),
        "from": lines[0].split()[0],
        "to": lines[-1].split()[0],
        "errors": [line for line in lines if line.split()[1] == "ERROR"],
    }


@server.tool()
async def search_log(service: str, text: str, limit: int = 20) -> list[str]:
    """Return up to `limit` lines of one service's log that contain `text`, such as a request ID.

    Args:
        service: A service name from list_logs.
        text: Text to look for, for example "r-4be19a" or "WARN".
        limit: The most lines to return.
    """
    if service not in services():
        return [f"No log for service {service!r}. Call list_logs for valid names."]
    return [line for line in lines_of(service) if text in line][:limit]


if __name__ == "__main__":
    server.run()

The counting now happens in code, which counts correctly every time, and the limit on search_log keeps a drill-down from flooding the context. Same default agent, smaller tools:

>_Terminal
python investigate.py default lean_server.py
Output
call msgs tool msgs ledger   input  cached output  next
   1    2         0     no     476       0    147  list_logs  [watch: seen]
   2    4         1     no     530       0     23  log_summary auth  [watch: seen]
   …
   6   12         5     no   1,325   1,152     17  log_summary notifications  [watch: seen]
   7    5         1    yes   1,401       0     17  log_summary orders  [watch: seen]
   8    5         1    yes   1,664       0    215  log_summary payments  [watch: seen]
   …
  11    5         1    yes   1,966       0    279  log_summary users  [watch: seen]
  12    5         1    yes   2,045       0    888  final answer  [watch: seen]
total: 12 model calls, 15,400 input (1,152 cached) + 1,805 output tokens = $0.0072

>>> - ERROR counts per service:
  - auth: 0
  - billing: 0
  - gateway: 4
  - inventory: 7
  - notifications: 0
  - orders: 5
  - payments: 2
  - search: 0
  - shipping: 0
  - users: 0

- Total ERROR lines: 18

- First ERROR logged: inventory at 2026-10-09T14:07:41.204Z

- Root cause (one sentence): The inventory service exhausted its database connection pool (db pool exhausted, max=20) beginning at 2026-10-09T14:07:41.204Z, causing inventory HTTP 500s that cascaded to orders failures, gateway 502s, and downstream payment/order cancellations.

The input fell from 313,783 to 15,400 tokens, about twenty times less. The agent got through all ten services with the question in view at every call, and the answer is right. Each summary is far below the 2,000-character cut, so the ledger carries every error line in full: small tool results are what make compaction work.

Step 06

Keep the question and the facts that must survive

On Promptise Foundry 1.2.1 this step took two changes. The question had to be a HumanMessage, the only form 1.2.1's compaction kept; 1.3.0 keeps a dict as well, so that change is gone. The other still matters: put the rules and facts the agent needs at its last step into instructions, because they are sent on every call. Here that's who the report is for and what shape it takes:

Pythonfinal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"""The same long run, set up to keep what matters."""

import asyncio
import sys

from promptise import build_agent
from promptise.config import StdioServerSpec

from meter import TokenMeter

# Rules and facts the agent needs on every step live in the instructions.
INSTRUCTIONS = """\
You are an SRE assistant. Answer from the logs only.
Work through the logs one service at a time: call one tool for one service,
look at the result, then go to the next. Never ask about two services in one step.

Facts for every report:
- Incident commander: Priya Raman. Address the report to her by name.
- Answer exactly the numbered parts the user asks for. No mitigation advice.
"""
QUESTION = (
    "We had an incident on 2026-10-09 between 14:00 and 14:30. Read every service log. "
    "Then tell me: (1) the number of ERROR lines per service, (2) the total, "
    "(3) which service logged the first ERROR and its exact timestamp, "
    "and (4) the root cause in one sentence."
)


async def main() -> None:
    agent = await build_agent(
        model="openai:gpt-5-mini",
        servers={"logs": StdioServerSpec(command=sys.executable, args=["lean_server.py"])},
        instructions=INSTRUCTIONS,
    )
    meter = TokenMeter(watch="the number of ERROR lines per service")
    try:
        result = await agent.ainvoke(
            {"messages": [{"role": "user", "content": QUESTION}]},
            config={"callbacks": [meter]},
        )
        print(meter.table())
        print("\n>>>", result["messages"][-1].content)
    finally:
        await agent.shutdown()


asyncio.run(main())
>_Terminal
python final.py
Output
call msgs tool msgs ledger   input  cached output  next
   1    2         0     no     512       0     83  list_logs  [watch: seen]
   2    4         1     no     566       0     23  log_summary auth  [watch: seen]
   3    6         2     no     660       0     87  log_summary billing  [watch: seen]
   4    8         3     no     754       0     17  log_summary gateway  [watch: seen]
   5   10         4     no     989       0     17  log_summary inventory  [watch: seen]
   6   12         5     no   1,361       0     17  log_summary notifications  [watch: seen]
   7    5         1    yes   1,437       0     17  log_summary orders  [watch: seen]
   8    5         1    yes   1,700       0    151  log_summary payments  [watch: seen]
   9    5         1    yes   1,846       0     17  log_summary search  [watch: seen]
  10    5         1    yes   1,924       0    279  log_summary shipping  [watch: seen]
  11    5         1    yes   2,002       0    343  log_summary users  [watch: seen]
  12    5         1    yes   2,081       0  1,017  final answer  [watch: seen]
total: 12 model calls, 15,832 input (0 cached) + 2,068 output tokens = $0.0081

>>> Priya Raman,

1) ERROR lines per service:
- auth: 0
- billing: 0
- gateway: 4
- inventory: 7
- notifications: 0
- orders: 5
- payments: 2
- search: 0
- shipping: 0
- users: 0

2) Total: 18

3) First ERROR and timestamp: inventory — 2026-10-09T14:07:41.204Z

4) Root cause (one sentence): The inventory service exhausted its DB connection pool (logs show "db pool exhausted (max=20, waiting=31)" at 2026-10-09T14:07:41.204Z), causing inventory HTTP 500s that cascaded into orders failures, gateway 502s, and payments cancellations.

After compaction the model gets five messages, and the question is one of them on every call. The answer matches the ground truth line by line, the report is addressed to the incident commander from the instructions, and the whole run cost under a cent. Each call stayed between 512 and 2,081 input tokens, so a run over a hundred services would still fit comfortably.


[05]

The numbers from every run

Each setup ran three times on the same logs and question, on Promptise Foundry 1.3.2. "Complete" means all four parts of the answer matched the ground truth.

Setup

Model calls

Input tokens

Logs read

Complete

Default agent, whole files

15 (step limit)

155,008 to 158,542

7 to 9

0 of 3

agent_pattern="managed", whole files

25 (step limit)

258,617 to 273,869

10

0 of 3

Default agent with a ContextEngine, whole files

15 (step limit)

151,972 to 157,701

7 to 8

0 of 3

No compaction, whole files

12

313,783

10

3 of 3

Default agent, summaries

12

15,400

10

3 of 3

Default agent with a ContextEngine, summaries

12

15,400

10

3 of 3

No compaction, summaries

12

15,743

10

3 of 3

final.py: summaries, facts in instructions

12 to 13

15,832 to 17,796

10

3 of 3

With whole files, every compacted setup kept the question but ran out of steps re-reading logs that the ledger had cut short, and all nine runs ended with GraphIterationLimitError instead of an answer. The default ReAct loop stops after 15 model calls and the managed pattern after 25; the managed runs had read all ten logs by call 15 to 19 and spent the rest going back to them, mostly auth and billing. Without compaction, whole files cost about 314,000 input tokens a run and got every part right. With summaries, every setup got every part right, compacted or not, for about 15,000 to 18,000 tokens; the one 13-call run asked for the orders summary twice, and the repeat ran the tool again. On 1.2.1 the compacted summary runs lost the question and failed; that's what 1.3.0 fixed. The tool design decided both the cost and whether compaction could work.


[06]

When the model reads all ten logs at once

Models often ask for several tools in one step. With the instruction changed to "Read each service's log with read_log, one service per call." in investigate_parallel.py, gpt-5-mini asked for all ten logs in its second call. Here's the default agent, then the full transcript:

Output
call msgs tool msgs ledger   input  cached output  next
   1    2         0     no     310       0     19  list_logs  [watch: seen]
   2    4         1     no     364       0    165  read_log auth, read_log billing, read_lo  [watch: seen]
   3   14        10    yes  56,436  56,192  1,437  final answer  [watch: seen]
total: 3 model calls, 57,110 input (56,192 cached) + 1,621 output tokens = $0.0049
Output
call msgs tool msgs ledger   input  cached output  next
   1    2         0     no     310       0     19  list_logs  [watch: seen]
   2    4         1     no     364       0    165  read_log auth, read_log billing, read_lo  [watch: seen]
   3   15        11     no  56,393       0  1,516  final answer  [watch: seen]
total: 3 model calls, 57,067 input (0 cached) + 1,700 output tokens = $0.0177

With eleven results in one step, the compacted call and the full transcript are about the same size: 56,436 tokens against 56,393. The ten results are the last step, so they appear once, the ledger holds only list_logs, and both runs answered every part correctly. On 1.2.1 the compacted call carried all ten logs twice, 116,479 tokens, and lost the question. Batching cuts the number of calls, but it puts every result into one call, so the size of each result matters even more.


[07]

Budget the starting context with the ContextEngine

The ContextEngine solves a different problem: a chat whose history keeps growing. Before each call it counts the instructions, history, memory and question with tiktoken, and when they exceed the budget it drops the least important parts first, oldest conversation turns before anything else. This example uses a tiny window so trimming shows on a short history:

Pythonengine_budget.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"""Budget the context an agent starts with: instructions, chat history and the new question."""

import asyncio

from langchain_core.callbacks import AsyncCallbackHandler
from promptise import ContextEngine, build_agent


class ShowPrompt(AsyncCallbackHandler):
    """Print every message the model receives on its first call."""

    def __init__(self):
        self.done = False

    async def on_chat_model_start(self, serialized, messages, **kwargs):
        if not self.done:
            self.done = True
            for m in messages[0]:
                print(f"  {m.type:<6} {str(m.content)[:60]}")


# Six earlier turns of a chat about the incident.
history = []
for service in ["auth", "billing", "gateway", "inventory", "notifications", "orders"]:
    history.append({"role": "user", "content": f"Summarize the {service} log."})
    history.append(
        {"role": "assistant", "content": f"The {service} log has 140 INFO lines. " + "Nothing unusual. " * 40}
    )


async def main() -> None:
    default = ContextEngine(model="openai:gpt-5-mini")
    print(f"default for gpt-5-mini: window {default.window:,}, budget {default.budget:,}")

    # A deliberately tiny window, so trimming happens on a short example.
    engine = ContextEngine(model="openai:gpt-5-mini", model_context_window=800, response_reserve=300)
    engine.add_layer("runbook", priority=7, required=True, content="Incident commander: Priya Raman.")

    agent = await build_agent(
        model="openai:gpt-5-mini",
        servers={},
        instructions="You are an SRE assistant. Be brief.",
        context_engine=engine,
    )
    try:
        print("the model sees:")
        result = await agent.ainvoke(
            {"messages": history + [{"role": "user", "content": "Which services have we covered so far?"}]},
            config={"callbacks": [ShowPrompt()]},
        )
        report = engine.last_report
        print(f"budget {report.budget}, used {report.total_tokens} ({report.utilization:.0%}), trimmed {report.trimmed_layers}")
        for layer in report.layers:
            print(f"  {layer['name']:<13} priority {layer['priority']:>2}  {layer['tokens']:>4} tokens  trimmed={layer['trimmed']}")
        print(">>>", result["messages"][-1].content)
    finally:
        await agent.shutdown()


asyncio.run(main())
Output
default for gpt-5-mini: window 400,000, budget 272,000
[promptise] No tools discovered from MCP servers; agent will run without tools.
the model sees:
  system Incident commander: Priya Raman.
  system You are an SRE assistant. Be brief.
  human  Summarize the inventory log.
  ai     The inventory log has 140 INFO lines. Nothing unusual. Nothi
  human  Summarize the notifications log.
  ai     The notifications log has 140 INFO lines. Nothing unusual. N
  human  Summarize the orders log.
  ai     The orders log has 140 INFO lines. Nothing unusual. Nothing 
  human  Which services have we covered so far?
budget 500, used 445 (89%), trimmed ['conversation']
  identity      priority 10    10 tokens  trimmed=False
  user_message  priority 10     8 tokens  trimmed=False
  runbook       priority  7     7 tokens  trimmed=False
  conversation  priority  1   420 tokens  trimmed=True
>>> Covered so far:
- inventory service (log reviewed)
- notifications service (log reviewed)
- orders service (log reviewed)

Each log had 140 INFO lines and no anomalies reported.

The budget is the window minus response_reserve, the three oldest exchanges were dropped as whole user and assistant pairs, the new question stayed, the runbook layer marked required=True reached the model, the instructions were sent once, and last_report says exactly what happened. The agent now believes only three services were covered, which is what trimming means: dropped turns are gone, not summarized. The first line shows the default for gpt-5-mini: a 400,000-token window, with the budget capped at the model's 272,000-token input limit.

A window of 800 tokens minus a response reserve of 300 leaves a budget of 500, of which 445 were used, and the report shows the identity, user_message and runbook layers kept while the priority 1 conversation layer was trimmed by dropping the three oldest exchanges.
FigureThe ContextEngine fits its layers into the budget and trims the conversation first

For chats stored through a conversation store, there's a simpler cap: build_agent(conversation_max_messages=N) keeps only the newest N messages of each session when it saves them.


[08]

Practical rules for agent context

These held up across every run in this guide:

  • Measure before you tune. Count a tool result with ContextEngine.count_tokens and meter real runs. The numbers decide where the work is; here it was entirely in the tool results.

  • Return less from tools. Summarize, filter and count where the data lives, in code. Return the error lines, not the file. Add a drill-down tool with a limit for the cases where the model needs detail.

  • System prompt for what must always hold. Output format, who the report is for, hard rules, the facts the last step depends on. It's sent on every call, twelve times in the final.py run above, so keep it short. Since 1.3.0, system messages you pass in and the current question survive compaction too, but instructions are the place for anything the last step depends on.

  • Tools for everything large or occasional. Logs, runbooks, past incidents and documentation belong behind a search or lookup tool, the way RAG turns documents into a search tool. The model fetches the slice it needs, when it needs it.

  • Summaries are yours to write. Promptise doesn't ask a model to summarize the run: the ledger cuts long results to their first 2,000 characters, earlier chat turns become a short clipped note, and the ContextEngine drops whole turns. If a summary matters, produce it in the tool, as log_summary does.

  • Keep one long job per question. In a chat, a compacted turn sees the earlier conversation only as a note of its last six messages, cut to 400 characters each. Run long investigations as their own ainvoke call, and bring the result back into the chat afterwards.


[09]

Honest limits

These are true of Promptise Foundry 1.3.2, and worth knowing before a long run goes to production:

  • Compaction cuts long results. After six tool results, older results longer than 2,000 characters are cut to an excerpt, and the model has to call the tool again to see the rest. In this guide that made the default agent read whole log files again and again until it ran out of steps. Raise the limit with context_compaction=ContextCompaction(keep_result_chars=...) from promptise.engine, turn compaction off with context_compaction=False, or make the tools return less.

  • A run that runs out of steps raises an error. Since 1.3.2, when an iteration limit stops a tool loop before the model answered, ainvoke raises GraphIterationLimitError from promptise.engine, with the limit and the pending_tools whose results the model never saw. The default ReAct loop allows 15 model calls and the managed pattern 25. Catch it wherever a long run can reach the limit, as investigate.py does.

  • Repeated calls run the tool. That's right for a status or a health check, and wasted work for a file that doesn't change during the run. ContextCompaction(reuse_results=[...]) answers repeats of the tools you list from the earlier result once the loop compacts. It saves tool runs, not tokens, so it doesn't fix a loop like the one in step 3.

  • Earlier chat turns become a short note. With chat history in the input, as agent.chat() builds it, a compacted call sees the last six earlier messages, 400 characters each, and then the current question. Facts a long run needs from earlier turns belong in instructions or behind a tool.

  • Nothing is summarized by a model. Compaction and the ContextEngine are deterministic: they cut and drop, so what's cut is gone from that call. That costs no extra model calls, and it means the quality of what's kept depends on your tools.

  • The ContextEngine's default budget is large for big models. For gpt-5-mini it's 272,000 tokens, the model's input limit, so it only bounds a tool loop that gets very long. Pass model_context_window when you want it to budget for less.


[10]

Frequently asked questions

What is context engineering for AI agents?

Deciding what goes into the model's context on each step of an agent run: instructions, the task, history, tool results and retrieved data, and in what form. Prompt engineering is about the wording of one prompt; context engineering is about the whole working set as it changes over a long run. In practice most of it is tool design: what each tool returns, and how much.

What is context compaction?

Replacing a long transcript with a shorter view before the next model call. Promptise's default agent does it automatically after six tool results, by keeping the instructions and any system messages, the current question, the latest step and a ledger with one entry per earlier tool call, with long results cut to 2,000 characters. Other systems summarize the history with a model instead. Either way, check what the compacted view still contains, because that's all the model knows.

Does a bigger context window solve the problem?

Not on its own. The full-transcript run here peaked at 56,504 tokens, well inside gpt-5-mini's window, and answered correctly, but it sent 313,783 input tokens to do work that took about 16,000 with smaller tool results. Every token in the window is paid for on every call, and the growth doesn't stop at ten files.

How do I reduce the token usage of an AI agent?

Meter each model call first, then shrink the largest input. For agents that's almost always tool results: return summaries and counts instead of raw data, cap list sizes, and add a drill-down tool. Keep the system prompt short, since it's resent every call. Prompt caching lowers the price of repeated prefixes, but not the size of the context.

Should I summarize conversation history?

When a chat outgrows its budget, something has to give: either old turns are dropped, as the ContextEngine and conversation_max_messages do, or they're summarized. Promptise doesn't summarize history with a model; inside a long tool loop, earlier turns become a short note of the last six messages. If older turns matter, keep the facts they produced in a tool or in the instructions, rather than relying on the turns themselves.


[11]

Where to go next

  • Context Engine: layers, priorities, budgets and the assembly report.

  • Context Lifecycle Management: the full, scoped and ledger modes and the managed pattern.

  • Engine nodes: Context scope: every PromptNode option, including context_scope and auto_ledger_after.

  • Reasoning Patterns: the built-in agent patterns and when each one fits.

  • Context System for prompts: context providers that inject user, environment and error context into prompts.

  • Observability: record tokens and latency for every call in production.

  • How to Connect MCP Servers to Your AI Agent in Python: the agent and server basics this guide builds on.

  • OpenAPI to MCP: Turn Any REST API into an MCP Server: generated tools are a common source of oversized results, and a good place to apply these rules.

← PreviousBuild a Multi-Agent System in Python (and When Not To)Next lesson →How to Deploy an AI Agent with Docker (2026 Guide)