{"slug": "script-for-creating-agents-which-can-plug-anywhere", "title": "Script for creating agents which can plug anywhere", "summary": "A developer has released a bash script that scaffolds a portable, embeddable LangGraph agent codebase. The script, named create-agent.sh, generates a generic project structure with state, config, and graph modules, and runs the agent immediately to provide proof-of-life output. It is designed to work for any agent and supports injection of LLM clients, checkpointers, and tools.", "body_md": "| #!/usr/bin/env bash | |\n| # create-agent.sh — scaffold a portable, embeddable LangGraph agent codebase. | |\n| # Fully generic: no domain-specific nodes, works for ANY agent you build. | |\n| # | |\n| # Usage: | |\n| # ./create-agent.sh my_agent | |\n| # | |\n| # After scaffolding, it installs deps and RUNS the agent immediately so | |\n| # you get proof-of-life output, not just a pile of files. | |\n| set -euo pipefail | |\n| if [ -z \"${1:-}\" ]; then | |\n| echo \"Usage: $0 <agent_name_snake_case>\" | |\n| exit 1 | |\n| fi | |\n| NAME=\"$1\" | |\n| PKG=\"src/${NAME}\" | |\n| mkdir -p \"${PKG}\"/{nodes,tools,adapters} | |\n| mkdir -p examples tests | |\n| # ---------- state.py ---------- | |\n| cat > \"${PKG}/state.py\" << 'EOF' | |\n| \"\"\" | |\n| Public contract for this agent. Treat like a versioned API schema: | |\n| additive changes only, since hosts embedding this agent depend on these shapes. | |\n| \"\"\" | |\n| from typing import TypedDict, Optional | |\n| class InputState(TypedDict): | |\n| \"\"\"What a caller must provide. Add/rename fields for your agent.\"\"\" | |\n| query: str | |\n| context: Optional[dict] | |\n| class OutputState(TypedDict): | |\n| \"\"\"What a caller gets back. Nothing else leaks out of the graph.\"\"\" | |\n| result: str | |\n| metadata: dict | |\n| class InternalState(InputState, OutputState): | |\n| \"\"\"Full scratchpad state used only inside this graph's nodes. | |\n| Add whatever fields your nodes need to pass data between steps.\"\"\" | |\n| iterations: int | |\n| scratch: dict | |\n| EOF | |\n| # ---------- config.py ---------- | |\n| cat > \"${PKG}/config.py\" << 'EOF' | |\n| \"\"\" | |\n| Everything that varies by host (LLM client, checkpointer, tools, callbacks) | |\n| is injected here, never imported directly inside node files. | |\n| \"\"\" | |\n| from dataclasses import dataclass, field | |\n| from typing import Any, Callable, Optional | |\n| @dataclass | |\n| class AgentConfig: | |\n| llm: Any = None # inject a real client for production use | |\n| checkpointer: Optional[Any] = None # host may pass Postgres/Redis saver; None = in-memory | |\n| tools: list = field(default_factory=list) | |\n| max_iterations: int = 5 | |\n| on_event: Optional[Callable[[str, dict], None]] = None # optional observability hook | |\n| class MockLLM: | |\n| \"\"\"Zero-setup stand-in so the scaffold runs immediately with no API keys. | |\n| Swap AgentConfig.llm for a real client (ChatAnthropic, ChatOpenAI, etc.).\"\"\" | |\n| def invoke(self, prompt: str) -> str: | |\n| return f\"[mock output for: {str(prompt)[:60]}]\" | |\n| def default_config() -> AgentConfig: | |\n| \"\"\"Sensible standalone defaults — used by examples/ and the self-test below.\"\"\" | |\n| return AgentConfig(llm=MockLLM()) | |\n| EOF | |\n| # ---------- graph.py ---------- | |\n| cat > \"${PKG}/graph.py\" << EOF | |\n| \"\"\" | |\n| The single product of this package: build_graph(config) -> CompiledStateGraph. | |\n| Every adapter (subgraph / tool / MCP / REST / CLI) wraps THIS and only this. | |\n| The example flow below (process -> loop -> finish) is a placeholder. | |\n| Replace the nodes in nodes/ with your real logic; the graph wiring here | |\n| is deliberately generic so it fits any agent shape you build. | |\n| \"\"\" | |\n| from langgraph.graph import StateGraph, START, END | |\n| from langgraph.checkpoint.memory import MemorySaver | |\n| from .state import InternalState, InputState, OutputState | |\n| from .config import AgentConfig | |\n| from .nodes.process import make_process_node | |\n| from .nodes.finish import make_finish_node | |\n| def should_continue(state: InternalState) -> str: | |\n| if state.get(\"iterations\", 0) >= state.get(\"_max_iterations\", 5): | |\n| return \"finish\" | |\n| return \"finish\" if state.get(\"scratch\", {}).get(\"done\", True) else \"process\" | |\n| def build_graph(config: AgentConfig): | |\n| builder = StateGraph(InternalState, input_schema=InputState, output_schema=OutputState) | |\n| builder.add_node(\"process\", make_process_node(config)) | |\n| builder.add_node(\"finish\", make_finish_node(config)) | |\n| builder.add_edge(START, \"process\") | |\n| builder.add_conditional_edges(\"process\", should_continue, { | |\n| \"process\": \"process\", | |\n| \"finish\": \"finish\", | |\n| }) | |\n| builder.add_edge(\"finish\", END) | |\n| checkpointer = config.checkpointer or MemorySaver() | |\n| return builder.compile(checkpointer=checkpointer) | |\n| EOF | |\n| # ---------- nodes/process.py (generic — replace with your logic) ---------- | |\n| cat > \"${PKG}/nodes/process.py\" << 'EOF' | |\n| \"\"\"Generic processing node. Factory pattern: takes AgentConfig, returns a | |\n| node function. Replace the body with your actual step logic — this is | |\n| just a working placeholder so the graph runs out of the box.\"\"\" | |\n| from ..config import AgentConfig | |\n| from ..state import InternalState | |\n| def make_process_node(config: AgentConfig): | |\n| def _node(state: InternalState) -> dict: | |\n| iterations = state.get(\"iterations\", 0) + 1 | |\n| if config.on_event: | |\n| config.on_event(\"process\", {\"iteration\": iterations}) | |\n| # TODO: real logic here, e.g. call config.llm.invoke(...) or config.tools | |\n| return { | |\n| \"iterations\": iterations, | |\n| \"scratch\": {\"done\": True}, # flip logic here to loop more than once | |\n| } | |\n| return _node | |\n| EOF | |\n| # ---------- nodes/finish.py (generic — replace with your logic) ---------- | |\n| cat > \"${PKG}/nodes/finish.py\" << 'EOF' | |\n| \"\"\"Generic finishing node. Produces the OutputState. Replace with your | |\n| actual synthesis/response-formatting logic.\"\"\" | |\n| from ..config import AgentConfig | |\n| from ..state import InternalState | |\n| def make_finish_node(config: AgentConfig): | |\n| def _node(state: InternalState) -> dict: | |\n| query = state.get(\"query\", \"\") | |\n| output = config.llm.invoke(query) if config.llm else f\"processed: {query}\" | |\n| return { | |\n| \"result\": output, | |\n| \"metadata\": {\"iterations\": state.get(\"iterations\", 0)}, | |\n| } | |\n| return _node | |\n| EOF | |\n| # ---------- tools/registry.py ---------- | |\n| cat > \"${PKG}/tools/registry.py\" << 'EOF' | |\n| \"\"\"Central place to define/collect tools. Hosts can override via | |\n| AgentConfig.tools instead of editing this file, so the agent stays embeddable.\"\"\" | |\n| from langchain_core.tools import tool | |\n| @tool | |\n| def example_tool(query: str) -> str: | |\n| \"\"\"Placeholder tool. Replace or extend, or inject alternatives via config.\"\"\" | |\n| return f\"result for: {query}\" | |\n| DEFAULT_TOOLS = [example_tool] | |\n| EOF | |\n| # ---------- adapters/as_subgraph.py ---------- | |\n| cat > \"${PKG}/adapters/as_subgraph.py\" << EOF | |\n| \"\"\" | |\n| Embed this agent as a single node inside a HOST LangGraph graph. | |\n| from ${NAME}.adapters.as_subgraph import get_subgraph_node | |\n| parent_builder.add_node(\"my_agent\", get_subgraph_node(config)) | |\n| \"\"\" | |\n| from ..graph import build_graph | |\n| from ..config import AgentConfig, default_config | |\n| def get_subgraph_node(config: AgentConfig = None): | |\n| \"\"\"Returns the compiled graph directly — LangGraph treats a compiled | |\n| StateGraph as a valid node. Map field names in the host graph if the | |\n| parent's state keys differ from InputState/OutputState.\"\"\" | |\n| return build_graph(config or default_config()) | |\n| EOF | |\n| # ---------- adapters/as_tool.py ---------- | |\n| cat > \"${PKG}/adapters/as_tool.py\" << EOF | |\n| \"\"\" | |\n| Expose this agent as a single LangChain tool for a host ReAct-style agent | |\n| that wants to call it as a leaf action rather than embed it as a subgraph. | |\n| \"\"\" | |\n| from langchain_core.tools import tool | |\n| from ..graph import build_graph | |\n| from ..config import AgentConfig, default_config | |\n| def get_agent_tool(config: AgentConfig = None): | |\n| graph = build_graph(config or default_config()) | |\n| @tool | |\n| def ${NAME}(query: str) -> str: | |\n| \"\"\"Run the ${NAME} agent on a query and return its result.\"\"\" | |\n| out = graph.invoke({\"query\": query, \"context\": None}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}}) | |\n| return out[\"result\"] | |\n| return ${NAME} | |\n| EOF | |\n| # ---------- adapters/as_mcp_server.py ---------- | |\n| cat > \"${PKG}/adapters/as_mcp_server.py\" << EOF | |\n| \"\"\" | |\n| Expose this agent as an MCP server so any MCP-capable host (agentic IDE, | |\n| Claude Code, Cursor, etc.) can call it as a single tool without custom | |\n| integration code. Run: python -m ${NAME}.adapters.as_mcp_server | |\n| \"\"\" | |\n| from mcp.server.fastmcp import FastMCP | |\n| from ..graph import build_graph | |\n| from ..config import default_config | |\n| mcp = FastMCP(\"${NAME}\") | |\n| @mcp.tool() | |\n| def run_agent(query: str, context: dict | None = None) -> dict: | |\n| \"\"\"Run the agent and return its OutputState.\"\"\" | |\n| graph = build_graph(default_config()) # swap in real config/llm as needed | |\n| return graph.invoke({\"query\": query, \"context\": context}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}}) | |\n| if __name__ == \"__main__\": | |\n| mcp.run() | |\n| EOF | |\n| # ---------- adapters/as_rest_api.py ---------- | |\n| cat > \"${PKG}/adapters/as_rest_api.py\" << EOF | |\n| \"\"\" | |\n| Expose this agent over HTTP for language-agnostic hosts. | |\n| Run: uvicorn ${NAME}.adapters.as_rest_api:app --reload | |\n| \"\"\" | |\n| from fastapi import FastAPI | |\n| from pydantic import BaseModel | |\n| from ..graph import build_graph | |\n| from ..config import default_config | |\n| app = FastAPI(title=\"${NAME}\") | |\n| _graph = build_graph(default_config()) # wire real config at startup | |\n| class Request(BaseModel): | |\n| query: str | |\n| context: dict | None = None | |\n| @app.post(\"/invoke\") | |\n| def invoke(req: Request): | |\n| return _graph.invoke({\"query\": req.query, \"context\": req.context}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}}) | |\n| EOF | |\n| # ---------- adapters/as_cli.py ---------- | |\n| cat > \"${PKG}/adapters/as_cli.py\" << EOF | |\n| \"\"\"Local dev/debug entrypoint. Run: python -m ${NAME}.adapters.as_cli \"your query\" \"\"\" | |\n| import sys | |\n| from ..graph import build_graph | |\n| from ..config import default_config | |\n| def main(): | |\n| query = \" \".join(sys.argv[1:]) or \"example query\" | |\n| graph = build_graph(default_config()) | |\n| result = graph.invoke({\"query\": query, \"context\": None}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}}) | |\n| print(result) | |\n| if __name__ == \"__main__\": | |\n| main() | |\n| EOF | |\n| # ---------- __init__.py files ---------- | |\n| touch \"${PKG}/__init__.py\" \"${PKG}/nodes/__init__.py\" \"${PKG}/tools/__init__.py\" \"${PKG}/adapters/__init__.py\" | |\n| # ---------- examples ---------- | |\n| cat > \"examples/standalone_run.py\" << EOF | |\n| from ${NAME}.graph import build_graph | |\n| from ${NAME}.config import default_config | |\n| if __name__ == \"__main__\": | |\n| graph = build_graph(default_config()) | |\n| result = graph.invoke({\"query\": \"example question\", \"context\": None}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}}) | |\n| print(result) | |\n| EOF | |\n| cat > \"examples/embed_in_parent_graph.py\" << EOF | |\n| \"\"\"Shows how a HOST system with its own LangGraph would embed this agent as one node.\"\"\" | |\n| from langgraph.graph import StateGraph, START, END | |\n| from typing import TypedDict | |\n| from ${NAME}.adapters.as_subgraph import get_subgraph_node | |\n| class HostState(TypedDict): | |\n| query: str | |\n| context: dict | None | |\n| result: str | |\n| metadata: dict | |\n| host_builder = StateGraph(HostState) | |\n| host_builder.add_node(\"${NAME}\", get_subgraph_node()) | |\n| host_builder.add_edge(START, \"${NAME}\") | |\n| host_builder.add_edge(\"${NAME}\", END) | |\n| host_graph = host_builder.compile() | |\n| if __name__ == \"__main__\": | |\n| print(host_graph.invoke({\"query\": \"example question\", \"context\": None}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}})) | |\n| EOF | |\n| # ---------- tests ---------- | |\n| cat > \"tests/test_graph.py\" << EOF | |\n| from ${NAME}.graph import build_graph | |\n| from ${NAME}.config import default_config | |\n| def test_graph_compiles(): | |\n| graph = build_graph(default_config()) | |\n| assert graph is not None | |\n| def test_graph_runs_end_to_end(): | |\n| graph = build_graph(default_config()) | |\n| out = graph.invoke({\"query\": \"hello\", \"context\": None}, | |\n| config={\"configurable\": {\"thread_id\": \"1\"}}) | |\n| assert \"result\" in out | |\n| EOF | |\n| # ---------- pyproject.toml ---------- | |\n| cat > \"pyproject.toml\" << EOF | |\n| [project] | |\n| name = \"${NAME}\" | |\n| version = \"0.1.0\" | |\n| requires-python = \">=3.11\" | |\n| dependencies = [ | |\n| \"langgraph>=0.2\", | |\n| \"langchain-core\", | |\n| \"fastapi\", | |\n| \"uvicorn\", | |\n| \"mcp\", | |\n| ] | |\n| [tool.setuptools.packages.find] | |\n| where = [\"src\"] | |\n| EOF | |\n| # ---------- README.md ---------- | |\n| cat > \"README.md\" << EOF | |\n| # ${NAME} | |\n| Single source of truth: \\`src/${NAME}/graph.py\\` — \\`build_graph(config) -> CompiledStateGraph\\`. | |\n| Everything else is an adapter around it. Replace the placeholder nodes in | |\n| \\`nodes/\\` with your real logic; do NOT add invocation-layer imports | |\n| (FastAPI, MCP SDK, etc.) inside \\`nodes/\\`, \\` graph.py\\`, or \\` state.py\\` — | |\n| that belongs only in \\`adapters/\\`. | |\n| ## Structure | |\n| - \\`state.py\\` — public contract (InputState/OutputState) + internal scratchpad. | |\n| - \\`config.py\\` — injected LLM/checkpointer/tools/callbacks. MockLLM by default so it runs with zero setup. | |\n| - \\`nodes/\\` — one file per graph node, factory pattern \\`make_x_node(config)\\`. Replace with real logic. | |\n| - \\`tools/registry.py\\` — default tools; hosts can override via \\`AgentConfig.tools\\`. | |\n| - \\`adapters/\\` — every way this agent can be consumed: | |\n| - \\`as_subgraph.py\\` — embed as a node in another LangGraph graph | |\n| - \\`as_tool.py\\` — expose as a single LangChain tool for a ReAct-style host | |\n| - \\`as_mcp_server.py\\` — expose via MCP for agentic IDEs / Claude Code-like hosts | |\n| - \\`as_rest_api.py\\` — expose over HTTP for non-Python hosts | |\n| - \\`as_cli.py\\` — local dev/debug entrypoint | |\n| ## Quickstart | |\n| \\`\\`\\` bash | |\n| pip install -e . | |\n| python examples/standalone_run.py | |\n| python -m ${NAME}.adapters.as_cli \"your query\" | |\n| \\`\\`\\` | |\n| EOF | |\n| echo \"✅ Scaffolded '${NAME}'.\" | |\n| echo \"\" | |\n| # Only install what's missing — don't touch existing system packages. | |\n| echo \"Checking dependencies...\" | |\n| python3 -c \"import langgraph, langchain_core\" 2>/dev/null || \\ | |\n| pip install --break-system-packages -q langgraph langchain-core | |\n| echo \"\" | |\n| echo \"=== Self-test: running the agent right now (no install needed) ===\" | |\n| PYTHONPATH=\"src:${PYTHONPATH:-}\" python3 examples/standalone_run.py | |\n| echo \"=== If you see a result dict above, the scaffold works. ===\" | |\n| echo \"\" | |\n| echo \"To use it as an installable package later: pip install -e . (in a venv)\" |", "url": "https://wpnews.pro/news/script-for-creating-agents-which-can-plug-anywhere", "canonical_source": "https://gist.github.com/kevinrawal/5b7b93213bbe7d50a2c3ac295b3fd5d4", "published_at": "2026-08-03 06:58:54+00:00", "updated_at": "2026-08-03 07:02:16.129838+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["LangGraph"], "alternates": {"html": "https://wpnews.pro/news/script-for-creating-agents-which-can-plug-anywhere", "markdown": "https://wpnews.pro/news/script-for-creating-agents-which-can-plug-anywhere.md", "text": "https://wpnews.pro/news/script-for-creating-agents-which-can-plug-anywhere.txt", "jsonld": "https://wpnews.pro/news/script-for-creating-agents-which-can-plug-anywhere.jsonld"}}