AI Agent Architecture: LangGraph vs CrewAI vs a Gateway
an AI agent architecture is four layers - perception, reasoning, memory, action - plus the framework that sequences them. ai agent architecture draws 720 US searches a month and langgraph vs crewai 170; both ask about control flow. The twelve shipped symbols below cover the layer neither framework decides: registration, tool names, step wiring, annotations, provenance, tracing, transport and…
Short answer: an AI agent architecture is four layers - perception, reasoning, memory,
action - plus the framework that sequences them. ai agent architecture draws 720 US searches a
month and langgraph vs crewai 170; both ask about control flow. The
twelve shipped symbols below cover the layer neither framework decides: registration, tool
names, step wiring, annotations, provenance, tracing, transport and retries. SmartGate is the
MCP-native algorithm gateway for token control, traffic shaping and agent audit.
Key takeaways
- One registry owns module lifecycle.
ModuleRegistrykeeps the dict, the write path, the read path and the async start and stop on one class, so registered and started cannot drift apart. - Names resolve, orchestrators refuse.
resolve_pipeline_tool_namemapssmart_fetchtofetchand raises when the name is a pipeline orchestrator - the cheapest guard you will ever write against a self-calling loop. - Steps are wired by name.
resolve_pipeline_step_paramsfillsquery,urlandtextfrom run inputs first and the most recent successful prior step second, leaving the caller's dict untouched. - Contracts and provenance are derived.
tool_annotationsreads one table instead of duplicating the promise;infer_routeandinfer_transportturn a path intomcp/restandmcp_sse/rest;get_or_create_session_tracekeeps one trace id per MCP session. - Do this next: count tokens on one live agent call path this week and cap it there, then decide whether you need a graph, a crew, or neither.
The short version for whoever signs off on the agent budget
An agent architecture has two budgets, and diagrams usually show one. The framework budget answers how work gets sequenced; the gateway budget answers what each step cost, who called it, and what you can prove afterwards. LangGraph and CrewAI are strong answers to the first and silent on the second.
That silence is where the money goes. A retry loop that re-runs a fetch five times is a product bug and an invoice at once, and the billing model behind the second budget is deliberately shared-risk: pay for the platform, share only when you save. Operating those two budgets - the record, the evaluation set and the retention window behind them - is the layer LLMOps lays out.
What the AI agent architecture SERP already covers
Two searches, two questions. The AI Overview for ai agent architecture decomposes the class
into perception, an LLM reasoning engine, memory, an action layer and a feedback loop, then
splits it into single agents and multi-agent orchestration that names LangGraph and CrewAI. The
AI Overview for langgraph vs crewai frames the same fork from the other side: role-based crews
with a gentle learning curve against an explicit graph state machine with deterministic routing
and checkpointing.
Microsoft's orchestration patterns guide catalogues sequential, concurrent, handoff and group-chat patterns (Microsoft Learn); IBM covers the same layer for enterprises (IBM); Anthropic's advice is to start with the simplest thing that works (building effective agents). None of them specifies the boundary - which tool was called, in which session, at what cost, and what happens when the fourth hop fails - which is what the rest of this article shows. Those hops ride the Model Context Protocol, and Anthropic MCP naming has drifted from the wire format it describes: the acronym, the client names and the dated spec revisions do not line up one to one.
ModuleRegistry: the one place agent modules register
# backend/smartgate/core/registry.py — source lines 11–41 (ModuleRegistry)
class ModuleRegistry:
"""管理所有算法模块的注册、初始化、查询。"""
def __init__(self):
self._modules: Dict[str, SmartModule] = {}
@property
def modules(self) -> Dict[str, SmartModule]:
return self._modules
def register(self, module: SmartModule) -> None:
if module.name in self._modules:
logger.warning(f"Module '{module.name}' already registered, overwriting.")
self._modules[module.name] = module
logger.info(f"Registered module: {module.name} v{module.version}")
def get(self, name: str) -> SmartModule:
module = self._modules.get(name)
if module is None:
raise KeyError(f"Module '{name}' not found. Available: {list(self._modules.keys())}")
return module
async def initialize_all(self, resources, config) -> None:
for name, module in self._modules.items():
logger.info(f"Initializing module: {name}")
await module.initialize(resources, config)
async def shutdown_all(self) -> None:
for name, module in self._modules.items():
logger.info(f"Shutting down module: {name}")
await module.shutdown()
The class is 31 lines and owns four things: the dict, the write path, the read path and the
lifecycle. register warns instead of raising on a duplicate name and logs the module and
version it stored. get raises a KeyError whose message lists the modules that do exist, and
initialize_all and shutdown_all walk the same dict asynchronously, so a module cannot be
registered and silently never started. The docstring in the pinned slice is the original Chinese,
quoted verbatim because every block here is asserted byte-for-byte.
resolve_pipeline_tool_name: the name the model calls versus the module that runs
# backend/smartgate/core/pipeline.py — source lines 42–53 (resolve_pipeline_tool_name)
def resolve_pipeline_tool_name(tool_name: str) -> str:
"""Map MCP-facing tool names to registry module names."""
name = (tool_name or "").strip()
if not name:
return name
if name in MCP_TOOL_TO_MODULE:
return MCP_TOOL_TO_MODULE[name]
if name in PIPELINE_ORCHESTRATORS:
raise KeyError(f"'{name}' is a pipeline orchestrator, not a runnable step tool")
if name.startswith("smart_"):
return name.removeprefix("smart_")
return name
MCP clients call smart_fetch; the registry holds fetch. Twelve lines close that gap, and the
interesting line is the refusal: a name found in PIPELINE_ORCHESTRATORS raises instead of
resolving, because smart_pipe is not a runnable step. Addressing an orchestrator as a step is
exactly how a pipeline ends up calling itself. An unrecognised name falls through unchanged
rather than failing, so the error surfaces at the registry instead of here, and empty input
stays empty. This is where the slogan stops being a slogan - agent loops do not warn, they bill -
because a step that re-enters the orchestrator is billed per iteration.
resolve_pipeline_step_params: how a step gets its query, url and text
# backend/smartgate/core/pipeline.py — source lines 183–192 (resolve_pipeline_step_params)
def resolve_pipeline_step_params(
module_name: str,
params: Dict[str, Any],
pipeline_ctx: PipelineContext,
run_inputs: Optional[Dict[str, Any]] = None,
pipeline_template: str = "",
) -> Dict[str, Any]:
"""Inject query/url/text across template steps from run inputs and prior results."""
resolved = dict(params)
inputs = run_inputs or {}
# backend/smartgate/core/pipeline.py — source lines 194–220 (resolve_pipeline_step_params)
if module_name == "search" and not (resolved.get("query") or "").strip():
query = (inputs.get("query") or "").strip()
if pipeline_template == "research":
query = _research_search_query(query)
resolved["query"] = query
if module_name == "fetch" and not (resolved.get("url") or "").strip():
url = (inputs.get("url") or "").strip()
if not url:
for prev in reversed(pipeline_ctx.prior_results()):
if prev.success and prev.data:
url = _first_search_url(prev.data) or ""
if url:
break
if url:
resolved["url"] = url
if module_name == "context_gate" and not (resolved.get("text") or "").strip():
text = (inputs.get("text") or "").strip()
if not text:
for prev in reversed(pipeline_ctx.prior_results()):
if prev.success and prev.data:
text = _fetch_text(prev.data) or ""
if text:
break
if text:
resolved["text"] = _truncate_pipeline_text(text, pipeline_template)
Two windows of a 66-line function, covering the signature and the search, fetch and context-gate
branches. Every branch has the same shape: check this module's own field, take the run input if
the caller sent one, otherwise walk prior_results() in reverse until a successful result
yields something usable. Search rewrites its query for the research template, fetch pulls a URL
out of the previous search, and context_gate takes the previous fetch text through
_truncate_pipeline_text. resolved = dict(params) means the caller's dict is never mutated.
Not quoted: the dedup and memory-add branches. Those branches exist because the text travelling
between steps has to fit the next call: how a long page is compressed and deduplicated before it is
handed on is the subject of Window Management Techniques.
smart_pipe: orchestration that keeps one agent infrastructure in one call
# backend/smartgate/api/mcp.py — source lines 302–331 (smart_pipe)
@server.tool(
name="smart_pipe",
description=TOOL_DESCRIPTIONS["smart_pipe"],
annotations=ToolAnnotations(
title="Pipeline orchestrator",
readOnlyHint=False,
),
)
async def smart_pipe(
template: str = Field(
default="",
description="Built-in template: research, read, or remember. Omit when using steps.",
),
steps: list[dict[str, Any]] | None = Field(
default=None,
description="Custom steps; each step has tool and params (legacy alias: args).",
),
query: str = Field(
default="",
description="Search query for research/remember templates (wired to search step).",
),
url: str = Field(
default="",
description="URL for read/research templates (wired to fetch when search is skipped).",
),
text: str = Field(
default="",
description="Optional text seed for context_gate when not produced by a prior fetch.",
),
) -> str:
# backend/smartgate/api/mcp.py — source lines 334–361 (smart_pipe)
_, registry = _app_state()
engine = PipelineEngine(registry)
ctx = _tool_ctx()
run_inputs = _non_empty(query=query or None, url=url or None, text=text or None)
params = _non_empty(
template=template or None,
steps=steps,
**run_inputs,
)
payload = await engine.run(
ctx,
template=template,
steps=steps,
inputs=run_inputs,
)
data = payload if isinstance(payload, dict) else {"result": payload}
merged_data = {
**data,
"results": payload.get("results") or [],
}
steps_meta = payload.get("pipeline_steps") or []
pipeline_ok = bool(steps_meta) and all(s.get("success") for s in steps_meta)
failed_step = next((s for s in steps_meta if not s.get("success")), None)
failed_result = next(
(r for r in (payload.get("results") or []) if not r.get("success")),
None,
)
step_error = None
# backend/smartgate/api/mcp.py — source lines 376–387 (smart_pipe)
audit_extra = {
**params,
"trace_kind": "pipeline",
"pipeline_template": template or None,
"pipeline_steps": payload.get("pipeline_steps") or [],
}
return await _run_with_audit(
"pipe",
ctx,
_identity_result(tool_result),
audit_extra,
)
Three windows: the decorator and signature, the engine call, and the audit payload. The tool
takes a template - research, read or remember - or explicit steps. readOnlyHint=False is
honest, because a pipe can write to team memory, and _non_empty keeps empty defaults out of the
record. Success is folded over the step metadata,
bool(steps_meta) and all(s.get("success") for s in steps_meta), with the first failing step
promoted into failed_step, failed_tool and step_error. The audit receives the topology,
not the payloads.
Retrieval is the step most stacks under-specify, which is why the four-stage RAG architecture is worth reading beside this comparison — the call shape is the same whether the second stage is a vector search or a tool. The contract each step in that chain honours - one named input in, one artefact out, with timing and status recorded - is set out on Agent Pipeline.
register_mcp_tools: seven tools, one registry function
# backend/smartgate/api/mcp.py — source lines 106–126 (register_mcp_tools)
def register_mcp_tools(server: FastMCP) -> None:
"""Register all 7 smart_* tools on a FastMCP instance."""
@server.tool(
name="smart_fetch",
description=TOOL_DESCRIPTIONS["smart_fetch"],
annotations=tool_annotations("smart_fetch"),
)
async def smart_fetch(
url: str = Field(description="Full HTTP or HTTPS URL to fetch."),
timeout: int = Field(default=30, description="HTTP timeout in seconds."),
) -> str:
_, registry = _app_state()
module = registry.get("fetch")
ctx = _tool_ctx()
return await _run_with_audit(
"fetch",
ctx,
module.process(ctx, url=url, timeout=timeout),
{"url": url},
)
# backend/smartgate/api/mcp.py — source lines 203–252 (register_mcp_tools)
async def smart_budget_guard(
action: str = Field(
description="One of: check (allowance), count (estimate tokens), record (log usage).",
),
team_id: str = Field(
default="",
description="Optional team override; leave empty to use the API key tenant.",
),
text: str | None = Field(
default=None,
description="Text for count action.",
),
messages: list[dict[str, Any]] | None = Field(
default=None,
description="Chat messages for count action.",
),
model: str = Field(
default="deepseek-chat",
description="Model name for token counting.",
),
tokens: int = Field(default=0, description="Tokens to record when action=record."),
monthly_limit: int = Field(
default=0,
description="Optional monthly cap override for check.",
),
completion_tokens: int = Field(
default=0,
description="Completion tokens for record action.",
),
) -> str:
_, registry = _app_state()
module = registry.get("budget_guard")
tid = (team_id or "").strip() or bound_team_id()
ctx = _tool_ctx(tid)
params = _non_empty(
action=action,
team_id=tid or None,
text=text,
messages=messages,
model=model,
tokens=tokens or None,
monthly_limit=monthly_limit or None,
completion_tokens=completion_tokens or None,
)
return await _run_with_audit(
"budget_guard",
ctx,
module.process(ctx, **params),
params,
)
One function registers all seven smart_* tools on a FastMCP instance, and two are quoted:
smart_fetch, the smallest useful tool, and smart_budget_guard, the one that carries the
money. Look the module up by string key at call time, build the tool context, call
module.process, and wrap the whole thing in _run_with_audit(action, ctx, result, extra) - so
metering, auditing and error shaping live in one place rather than in seven handlers. smart_budget_guard adds an optional team_id that
falls back to the tenant bound to the API key. Not quoted, in the same 282-line body: the
search, context-gate, dedup, memory and pipe registrations. The memory one is the registration whose
write path needs the most care - what belongs in that store, when retrieval may run and what each
path costs is Agent Memory Architecture.
tool_annotations: read-only hints for agent tool contracts
# backend/smartgate/api/mcp_tool_docs.py — source lines 63–67 (tool_annotations)
def tool_annotations(name: str) -> ToolAnnotations:
return ToolAnnotations(
title=TOOL_TITLES.get(name),
readOnlyHint=name in READ_ONLY_TOOLS,
)
Five lines, two derived values: the display title from TOOL_TITLES and the read-only hint from
membership in READ_ONLY_TOOLS. Both are advisory metadata a client can use to decide how much
confirmation a tool deserves, with the semantics defined in the MCP tool specification
(MCP tools). Deriving
them from tables next to the tool list is the part that matters: a hand-written annotation is a
second source of truth, and those drift.
infer_route: which surface did this agent call arrive on
# backend/smartgate/core/audit_enrichment.py — source lines 35–40 (infer_route)
def infer_route(path: str, route_hdr: str) -> str:
if route_hdr:
return route_hdr
if path.startswith("/mcp"):
return "mcp"
return "rest"
Six lines: an explicit route header wins; otherwise a /mcp prefix means mcp; otherwise
rest. Deriving the value rather than demanding it is the point, because an audit trail that
depends on the client sending a field is missing exactly when a client is misbehaving. An
unknown path is reported as rest rather than guessed into a category, which is what makes the
field safe to group by in a cost dashboard.
infer_transport: MCP session transport per agent client
# backend/smartgate/core/audit_enrichment.py — source lines 29–32 (infer_transport)
def infer_transport(path: str) -> str:
if path.startswith("/mcp"):
return "mcp_sse"
return "rest"
Same shape, different vocabulary: /mcp becomes mcp_sse, everything else becomes rest. The
label is older than the transport it names - Streamable HTTP replaced the HTTP-plus-SSE pair
(MCP transports) -
and the mount further down says so in its own docstring while this field still says mcp_sse.
Read it as a path-family label and it is useful; assume it identifies the wire protocol and you
will be surprised.
get_or_create_session_trace: a stable trace id per MCP session
# backend/smartgate/core/mcp_session_trace.py — source lines 15–29 (get_or_create_session_trace)
def get_or_create_session_trace(session_id: str, *, header_trace: str = "") -> str:
"""Return stable trace_id for an MCP SSE session."""
sid = (session_id or "").strip()
hdr = (header_trace or "").strip()
if hdr and sid:
_sessions[sid] = hdr
return hdr
if hdr:
return hdr
if sid and sid in _sessions:
return _sessions[sid]
trace = f"sess_{uuid4()}"
if sid:
_sessions[sid] = trace
return trace
The precedence chain is the design: a caller-supplied header trace plus a session id is stored against that session and returned; a header alone is returned as-is; a known session id returns the cached trace; anything else mints a session id. Header-first ordering lets an upstream correlation id survive, which is what joins a tool call to the model call behind it when your agent already speaks W3C Trace Context (W3C Trace Context). Session-first keying makes the unit of aggregation the conversation rather than the request, so a ten-step pipe is one traceable run.
bind_mcp_session_trace_if_needed: attaching the trace before the tool runs
# backend/smartgate/core/request_context.py — source lines 111–124 (bind_mcp_session_trace_if_needed)
def bind_mcp_session_trace_if_needed(
*,
path: str,
headers: Mapping[str, str],
query_string: str = "",
) -> None:
"""Attach session-level trace_id for MCP tool calls when header is absent."""
if bound_trace_id():
return
if not path.startswith("/mcp"):
return
hdr_trace = resolve_trace_id(headers)
session_id = _mcp_session_id_from_query(path, query_string)
bind_trace_id(get_or_create_session_trace(session_id, header_trace=hdr_trace))
Three early returns and one bind. Already bound: stop, which makes the function safe in a
middleware chain that may run twice. Not an /mcp path: stop, because REST requests keep the
context they already have. Otherwise resolve the header trace, read the session id out of the
query string, and bind whatever get_or_create_session_trace returns. The cross-file split is
deliberate - the module that decides a trace id is separate from the request plumbing that binds
it.
mount_mcp_routes: exposing one MCP endpoint on the app
# backend/smartgate/api/mcp.py — source lines 399–406 (mount_mcp_routes)
def mount_mcp_routes(app: FastAPI) -> None:
"""Expose POST /mcp (Streamable HTTP, stateless)."""
apply_mcp_session_compat()
streamable_app = mcp.streamable_http_app()
streamable_app.router.lifespan_context = _noop_starlette_lifespan(streamable_app)
app.mount("/mcp", streamable_app)
logger.info("MCP Streamable HTTP at POST /mcp")
Eight lines to expose a stateless MCP endpoint: apply the session compatibility shim, build the
Streamable HTTP app, replace its lifespan with a no-op wrapper, mount it at /mcp, log the
fact. The lifespan swap is the subtle one - the mounted sub-app cannot run a startup sequence of
its own, which is correct here because resources are owned by the parent application and by
ModuleRegistry.initialize_all. Mounting a sub-application rather than wiring every route by
hand is documented FastAPI behaviour
(sub-applications).
download_with_retry: bootstrap that survives a flaky registry
# backend/scripts/bake_model.py — source lines 88–110 (download_with_retry)
def download_with_retry(repo_id: str, dest: Path) -> None:
dest.mkdir(parents=True, exist_ok=True)
last_exc: Exception | None = None
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
_download_repo(repo_id, dest)
if not (dest / "config.json").is_file():
raise RuntimeError(f"missing config.json under {dest}")
mark_complete(dest)
return
except Exception as exc:
last_exc = exc
print(
f"WARN: download attempt {attempt}/{MAX_ATTEMPTS} failed for {repo_id}: {exc}",
file=sys.stderr,
flush=True,
)
if attempt < MAX_ATTEMPTS:
delay = min(120, RETRY_BASE_SEC * (2 ** (attempt - 1)))
print(f" retrying in {delay}s ...", file=sys.stderr, flush=True)
time.sleep(delay)
assert last_exc is not None
raise last_exc
Backoff capped at two minutes - min(120, RETRY_BASE_SEC * (2 ** (attempt - 1))) - with two
guards that matter more than the retry: after each attempt config.json must exist or the
function raises, and a completion marker is written, so a half-fetched artifact is detectable
and a re-run skips work that finished. The last exception is re-raised unwrapped so the
traceback names the real cause. Model weights are part of the agent stack: a bootstrap that
succeeds with an incomplete artifact produces an agent that answers confidently and wrongly.
LangGraph vs CrewAI vs hand-rolled orchestration vs a gateway
Two of these rows are frameworks, one is a lifestyle, one is a layer, and one is inertia.
| Approach | What it decides well | What it still leaves to you | Cost shape |
|---|---|---|---|
| LangGraph | Explicit graph state machine: nodes, edges, checkpointing, durable execution, deterministic routing | Registry, wiring, provenance, transport, token accounting | Open source; you pay for hosting, plus managed tracing if you want it |
| CrewAI or another role-based framework | Fast prototypes: roles, goals, delegation, a gentle learning curve | The same list, plus more of the routing is delegated to the agents | Open source core; enterprise tiers add security and compliance |
| Hand-rolled orchestration | Exactly what you wrote, and nothing you did not | Everything: registry, retries, audit, metering, on-call | Cheapest licence, most expensive maintenance |
| Gateway layer (SmartGate) | The MCP tool surface, token control, traffic shaping and agent audit, including smart_pipe orchestration |
Control flow - it does not decide whether you draw a graph or hire a crew | Platform fee; a savings share only once measured savings clear the threshold |
| Do nothing | Nothing changes this sprint | Every question about cost, provenance and duplicate work | Fine until a loop repeats, and then the invoice is the alert |
Both framework answers are right, because the question is control flow. The gateway row does not compete: a LangGraph node can call a SmartGate tool, and so can a crew, which is why both ends speak MCP. What it changes is the third column, the list you no longer build yourself. Compare it against your own bill on the pricing page - the differentiator is the billing model, pay for the platform, share only when you save. The vocabulary underneath all of this - loop versus pipeline versus multi-agent, and what a stopping rule owes you - is set out in What Is an Agentic Workflow.
How to get started
- Choose control flow second. Write the smallest loop that solves the task; add a graph for checkpointing and deterministic routing, a crew for delegation. The layer below does not change either way.
- Register tools in one place. A dict keyed by module name with one write path and one read path is enough to start - the value is that what exists and what started are one object.
- Map the public name to the internal one explicitly. Strip a prefix or keep a table, but refuse orchestrators as steps; it is the cheapest runaway-loop guard you will write.
- Derive provenance instead of requesting it, and count before you cap. A limit you cannot measure is a wish. Which processes deserve this machinery in the first place - and where the judgement step belongs - is the adoption question on AI Workflow Automation.
All seven smart_* tools - smart_fetch, smart_search, smart_context_gate, smart_dedup,
smart_budget_guard, smart_memory and smart_pipe - are available on the free tier, which is
2M tokens a month and needs no card:
start free. Pro starts at $18 a month for
20M tokens, Teams at $55 for a 100M token pool, and Enterprise is a contract conversation
through sales. The tool surface is in the
docs, with
token control and
audit and compliance described
separately.
Frequently Asked Questions
Is SmartGate a replacement for LangGraph or CrewAI?
No. It is the layer under them: an MCP-native tool surface either framework can call, so keep your graph or your crew and point the tools at the gateway.
Which of LangGraph and CrewAI should I choose?
Choose on control flow - the graph for deterministic routing, checkpointing and durable execution; a role-based crew for delegation and fast prototypes. The gateway decision is orthogonal.
Why does the audit field say mcp_sse when MCP moved to Streamable HTTP?
The label predates the transport rename, and the mount serves Streamable HTTP. Treat it as a path-family label; read the request if you need the wire protocol.
Can I use these patterns without adopting SmartGate?
Yes - they are plain code you can write in an afternoon, and all of it is quoted here. What you would be rebuilding is the tool surface, the metering and the audit record behind it.
What does it actually cost?
Free is $0 with 2M tokens a month and all seven tools; Pro starts at $18 for 20M tokens and caps near $36; Teams starts at $55 for a 100M pool; Enterprise is contract-based. You pay for the platform and share only once measured savings clear the threshold.
How do I join an MCP tool call to the model call behind it?
Send a W3C trace context header. A caller-supplied trace id is returned unchanged and stored against the session, so the id your model layer already has appears in the tool audit record.
Limitations and what this does not do
- Three sections quote windows, not whole bodies -
resolve_pipeline_step_paramsas its signature plus its search, fetch and context-gate branches,smart_pipeas three ranges, andregister_mcp_toolsas its head plus two of seven registrations. The provenance table records every range. - There is no unregister. The registry warns and overwrites on a duplicate name and has no removal path, so replacing a module is a restart rather than an operation.
- Session traces are per process. The session map behind
get_or_create_session_traceis an in-memory dict, so multiple workers will not share session traces without a shared store. - The transport label has drifted from the transport actually mounted, as noted above.
- This is not a benchmark. Nothing here timed LangGraph or CrewAI, and the two layers compose: adopting one does not remove the need to instrument the other.
Sources
- Microsoft Learn — agent orchestration patterns: https://learn.microsoft.com/en-us/azure/architecture/ai-ml/guide/ai-agent-design-patterns
- IBM — agentic architecture overview: https://www.ibm.com/think/topics/ai-agent-architecture
- Anthropic — building effective agents: https://www.anthropic.com/engineering/building-effective-agents
- LangGraph — control flow and durable execution: https://docs.langchain.com/oss/python/langgraph/overview
- CrewAI — role-based crews: https://docs.crewai.com/en/introduction
- Model Context Protocol — architecture overview: https://modelcontextprotocol.io/docs/learn/architecture
- Model Context Protocol — tool definitions and annotations: https://modelcontextprotocol.io/specification/2026-07-28/server/tools
- Model Context Protocol — transports, including Streamable HTTP: https://modelcontextprotocol.io/specification/2026-07-28/basic/transports
- Model Context Protocol — Python SDK and FastMCP: https://github.com/modelcontextprotocol/python-sdk
- W3C — Trace Context, the correlation header used above: https://www.w3.org/TR/trace-context/
- FastAPI — sub-applications and mounts: https://fastapi.tiangolo.com/advanced/sub-applications/
- SmartGate — pricing, docs, sales: https://smartgate.network/pricing · https://smartgate.network/docs · https://smartgate.network/contact
Method note
The code here is not transcribed: every block was cut out of the slice body returned by the
SmartGate slice API and re-asserted byte-for-byte before publication, and the first line inside
every fence records the file and source lines. Symbols were pinned by whole-name containment
(rule A level 2) and confirmed through the service's slot-proof endpoint - 12 of 12 sections
pinned, no abstentions, no misses. Three sections quote line windows rather than whole bodies to
keep the code share inside the pipeline ceiling. Demand figures come from
work/batch6/volume-candidates2.json (Google Ads search volume, US/en, 2026-09-15):
ai agent architecture 720 a month, langgraph vs crewai 170. This
page is a same-slug rebuild of an earlier page that carried five blocks and roughly 3.6 KiB.
Slice provenance
| # | SERP keyword | Symbol | File | Source lines | How it was pinned | sha256(12) |
|---|---|---|---|---|---|---|
| 1 | ModuleRegistry module registry for the agent infrastructure stack | ModuleRegistry |
backend/smartgate/core/registry.py |
11–41 | rule A L2 → slot-proof | de7a246bcdbe |
| 2 | resolve_pipeline_tool_name resolve pipeline tool name for agent stacks | resolve_pipeline_tool_name |
backend/smartgate/core/pipeline.py |
42–53 | rule A L2 → slot-proof | cfa550729045 |
| 3 | resolve_pipeline_step_params resolve pipeline step params for agent stacks | resolve_pipeline_step_params |
backend/smartgate/core/pipeline.py |
183–192, 194–220 | rule A L2 → slot-proof | 9049b657cb1f |
| 4 | smart_pipe smart pipe orchestration for agent infrastructure | smart_pipe |
backend/smartgate/api/mcp.py |
302–331, 334–361, 376–387 | rule A L2 → slot-proof | 91ee6d5bb70b |
| 5 | register_mcp_tools register mcp tools in the agent tool registry | register_mcp_tools |
backend/smartgate/api/mcp.py |
106–126, 203–252 | rule A L2 → slot-proof | 9d4a1623b28c |
| 6 | tool_annotations tool annotations for agent tool contracts | tool_annotations |
backend/smartgate/api/mcp_tool_docs.py |
63–67 | rule A L2 → slot-proof | a822944005fe |
| 7 | infer_route infer route for agent request provenance | infer_route |
backend/smartgate/core/audit_enrichment.py |
35–40 | rule A L2 → slot-proof | 8cb200979cc6 |
| 8 | infer_transport infer transport per agent client | infer_transport |
backend/smartgate/core/audit_enrichment.py |
29–32 | rule A L2 → slot-proof | fc93297c9232 |
| 9 | get_or_create_session_trace get or create session trace for agent runs | get_or_create_session_trace |
backend/smartgate/core/mcp_session_trace.py |
15–29 | rule A L2 → slot-proof | 616865f41e75 |
| 10 | bind_mcp_session_trace_if_needed bind mcp session trace for agent observability | bind_mcp_session_trace_if_needed |
backend/smartgate/core/request_context.py |
111–124 | rule A L2 → slot-proof | 1afbebd0e086 |
| 11 | mount_mcp_routes mount mcp routes for one agent endpoint | mount_mcp_routes |
backend/smartgate/api/mcp.py |
399–406 | rule A L2 → slot-proof | e66f6b69a172 |
| 12 | download_with_retry download with retry for agent bootstrap | download_with_retry |
backend/scripts/bake_model.py |
88–110 | rule A L2 → slot-proof | 7e870f9d2ceb |
Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before publication. 12 of 12 sections pinned, 0 abstentions, 0 misses.