SmartGateSmartGate

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. ModuleRegistry keeps 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_name maps smart_fetch to fetch and 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_params fills query, url and text from run inputs first and the most recent successful prior step second, leaving the caller's dict untouched.
  • Contracts and provenance are derived. tool_annotations reads one table instead of duplicating the promise; infer_route and infer_transport turn a path into mcp/rest and mcp_sse/rest; get_or_create_session_trace keeps 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

  1. 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.
  2. 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.
  3. 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.
  4. 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_params as its signature plus its search, fetch and context-gate branches, smart_pipe as three ranges, and register_mcp_tools as 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_trace is 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

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.