skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
217 lines (175 loc) • 7.96 kB
Markdown
## Contents
- LangGraph: State Machine Agents
- Basic Agent with Tool Calling
- Human-in-the-Loop with interrupt() and Checkpointing
- TypeScript LangGraph
## LangGraph: State Machine Agents
LangGraph is the production-grade choice for complex agents. It gives you explicit control flow, checkpointing, and human-in-the-loop — things you need in production but that simple chains don't offer.
### Basic Agent with Tool Calling
```python
# pip install langgraph langchain-openai langgraph-checkpoint-sqlite
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
# Define state
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
# Define tools
@tool
def search_database(query: str) -> str:
"""Search the product database for items matching the query."""
# Real implementation here
return f"Found 3 products matching '{query}': Widget A ($10), Widget B ($20), Widget C ($30)"
@tool
def create_order(product_name: str, quantity: int) -> str:
"""Create an order for a product."""
order_id = f"ORD-{hash(product_name) % 10000:04d}"
return f"Order {order_id} created: {quantity}x {product_name}"
tools = [search_database, create_order]
model = ChatOpenAI(model="gpt-5.5").bind_tools(tools) # gpt-5-family models reject temperature; use reasoning effort to steer
# Define nodes
def agent(state: AgentState) -> AgentState:
response = model.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: AgentState) -> str:
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return END
# Build graph
graph = StateGraph(AgentState)
graph.add_node("agent", agent)
graph.add_node("tools", ToolNode(tools))
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
app = graph.compile()
# Run
result = app.invoke({
"messages": [{"role": "user", "content": "Find me a widget under $15 and order 2 of them"}]
})
```
### Human-in-the-Loop with `interrupt()` and Checkpointing
The modern pattern (LangGraph 0.2.x+) uses the `interrupt()` function to pause *inside* a node and `Command(resume=...)` to feed a decision back. The value passed to `Command(resume=...)` becomes the return value of `interrupt()`, so you must **actually check it** before executing the side-effecting tool — never blindly continue into the tool node. Requires a checkpointer and a stable `thread_id`.
```python
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.types import interrupt, Command
from langgraph.checkpoint.sqlite import SqliteSaver # pip install langgraph-checkpoint-sqlite
# For pure in-memory dev use: from langgraph.checkpoint.memory import InMemorySaver
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
def agent(state: AgentState) -> AgentState:
return {"messages": [model.invoke(state["messages"])]}
def route_after_agent(state: AgentState) -> str:
last = state["messages"][-1]
if not getattr(last, "tool_calls", None):
return END
# High-stakes tools go through approval; everything else runs directly.
if any(tc["name"] == "create_order" for tc in last.tool_calls):
return "approval"
return "tools"
def approval(state: AgentState) -> Command:
"""Pause and surface the pending order to a human. The resumed value is the decision."""
last = state["messages"][-1]
order_calls = [tc for tc in last.tool_calls if tc["name"] == "create_order"]
# interrupt() returns whatever the human passes via Command(resume=...)
decision = interrupt({
"action": "approve_order",
"orders": [tc["args"] for tc in order_calls],
"prompt": "Approve these orders? Reply {'approved': bool, 'reason': str}",
})
if not decision.get("approved"):
# Reject: feed a tool message back so the agent can apologize / replan.
# Do NOT fall through to the tools node.
from langchain_core.messages import ToolMessage
return Command(
goto="agent",
update={"messages": [
ToolMessage(
content=f"Order rejected by human: {decision.get('reason', 'no reason given')}",
tool_call_id=tc["id"],
) for tc in order_calls
]},
)
# Approved: now (and only now) proceed to execute the tool.
return Command(goto="tools")
graph = StateGraph(AgentState)
graph.add_node("agent", agent)
graph.add_node("tools", ToolNode(tools))
graph.add_node("approval", approval) # returns Command, so its targets are dynamic
graph.add_edge(START, "agent")
graph.add_conditional_edges("agent", route_after_agent,
{"tools": "tools", "approval": "approval", END: END})
graph.add_edge("tools", "agent")
# Compile with a checkpointer — required for interrupt/resume.
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
app = graph.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "order-123"}}
# First run stops at interrupt(); the payload appears under "__interrupt__".
result = app.invoke(
{"messages": [{"role": "user", "content": "Order 5 Widget As"}]},
config=config,
)
print(result["__interrupt__"]) # show the orders to the human / UI
# Human decides. Resume by passing the decision into interrupt() via Command(resume=...).
final = app.invoke(Command(resume={"approved": True}), config=config)
# To deny instead: app.invoke(Command(resume={"approved": False, "reason": "over budget"}), config=config)
```
> `interrupt()` replaces the old `interrupt_before=[...]` / `app.invoke(None, config)` resume idiom, which paused *before* a node but did not let you pass or inspect an approval value. Note `SqliteSaver.from_conn_string` is now a context manager; for persistence on disk use a file path instead of `":memory:"`.
### TypeScript LangGraph
```typescript
import { StateGraph, START, END, Annotation } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { BaseMessage, HumanMessage } from "@langchain/core/messages";
// State definition
const AgentState = Annotation.Root({
messages: Annotation<BaseMessage[]>({
reducer: (prev, next) => [...prev, ...next],
}),
});
// Tools
const searchTool = tool(
async ({ query }) => {
return `Results for "${query}": Product A, Product B`;
},
{
name: "search",
description: "Search the product database",
schema: z.object({ query: z.string() }),
}
);
const model = new ChatOpenAI({ model: "gpt-5.5" }).bindTools([searchTool]);
// Nodes
async function agent(state: typeof AgentState.State) {
const response = await model.invoke(state.messages);
return { messages: [response] };
}
function shouldContinue(state: typeof AgentState.State) {
const lastMsg = state.messages[state.messages.length - 1];
if ("tool_calls" in lastMsg && lastMsg.tool_calls?.length) {
return "tools";
}
return END;
}
// Graph
const graph = new StateGraph(AgentState)
.addNode("agent", agent)
.addNode("tools", new ToolNode([searchTool]))
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue, { tools: "tools", [END]: END })
.addEdge("tools", "agent");
const app = graph.compile();
const result = await app.invoke({
messages: [new HumanMessage("Find products related to widgets")],
});
```
---