noos-langchain
LangChain + LangGraph callback adapter for Noos — the pre-delivery reliability regulator for LLM agents.
Drop NoosCallbackHandler into any agent built on langchain-core and get tool-loop halts, cost circuit breaks, scope-drift warnings, and procedural correction memory from the agent's existing event stream. No instrumentation inside your chains.
Overhead: one on_event dispatch is 20–250 ns; a decide() check is ~2 µs. Source: Noos criterion benchmarks, Session 33. An LLM call at 200 tokens/sec runs ~500 ms per turn — the regulator sits six orders of magnitude below that floor.
Install
pip install noos noos-langchain
Requires Python ≥3.9, noos>=0.1.0, langchain-core>=0.3.0. Works with LangChain, LangGraph, CrewAI, and anything else using the langchain-core callback interface.
Quick start — LangChain
from noos import Regulator
from noos_langchain import NoosCallbackHandler, CircuitBreakError
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate
# 1. Construct a regulator per user (or per session). Persist across
# processes via export_json / from_json.
regulator = Regulator.for_user("alice").with_cost_cap(10_000)
# 2. Wire the handler via the standard callbacks list.
handler = NoosCallbackHandler(
regulator,
raise_on_circuit_break=True, # abort agent on halt decisions
)
agent = create_openai_tools_agent(
ChatOpenAI(model="gpt-4o-mini"),
tools=[...],
prompt=ChatPromptTemplate.from_messages([...]),
)
executor = AgentExecutor(agent=agent, tools=[...], callbacks=[handler])
try:
result = executor.invoke({"input": "Find order #42 and email the summary."})
except CircuitBreakError as e:
print(f"Agent halted: {e.decision.reason.kind}")
print(f"Suggestion: {e.decision.suggestion}")
# 3. Inspect the last decision at any time.
if handler.last_decision and handler.last_decision.is_scope_drift():
print(f"Scope drift score: {handler.last_decision.drift_score:.2f}")
Quick start — LangGraph
LangGraph uses the same langchain-core callback plumbing, so the handler works unchanged:
from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage
from noos import Regulator
from noos_langchain import NoosCallbackHandler
agent = create_react_agent(llm, tools=[...])
handler = NoosCallbackHandler(
Regulator.for_user("alice").with_cost_cap(10_000),
raise_on_circuit_break=True,
)
agent.invoke(
{"messages": [HumanMessage(content="Find order 42")]},
config={"callbacks": [handler]},
)
Full demo: examples/langgraph_agent.py.
Quick start — async
Use AsyncNoosCallbackHandler for ainvoke / astream / LangGraph async:
from noos_langchain import AsyncNoosCallbackHandler
handler = AsyncNoosCallbackHandler(regulator)
await agent_executor.ainvoke(
{"input": "..."},
config={"callbacks": [handler]},
)
What the handler observes
| LangChain hook | Noos event | Decision impact |
|---|---|---|
on_chain_start (root) |
turn_start |
Resets per-turn scope + cost + tool-stats |
on_chat_model_start / on_llm_start |
turn_start (fallback) |
— |
on_llm_new_token |
token (opt-in via emit_tokens=True) |
Feeds confidence signal |
on_llm_end |
turn_complete + cost |
Updates scope-drift, cost-cap, quality-decline predicates |
on_llm_error |
— | No event; regulator view stays consistent with last turn_start |
on_tool_start |
tool_call |
Per-turn tool stats + consecutive-same-tool counter |
on_tool_end |
tool_result(success=True, duration_ms=…) |
Closes the tool-call signature |
on_tool_error |
tool_result(success=False, error_summary=…) |
Feeds tool failure count |
on_chain_end / on_chain_error (root) |
— | Clears turn flag; no event emitted |
After each event that can change the regulator's decision, the handler calls regulator.decide() and stores the result on handler.last_decision. Applications choose how to consume:
- Poll after
invoke— readhandler.last_decisionwhen execution completes. - React mid-run — pass
on_decision=callbackto observe every decision transition. - Abort on halt — pass
raise_on_circuit_break=Trueto raiseCircuitBreakErrorthe moment a circuit-break decision fires.
Migration patterns
From recursion_limit → tool-loop detection
LangChain / CrewAI / AutoGen's recursion_limit caps total step count regardless of what repeats. That missed the $47k LangChain incident where the agent called the same tool with the same arguments 400+ times.
# Before: crude step cap
executor = AgentExecutor(
agent=agent, tools=tools,
max_iterations=20, # still lets the same tool fire 20×
)
# After: fires on the signature of the pathology
handler = NoosCallbackHandler(regulator, raise_on_circuit_break=True)
executor = AgentExecutor(agent=agent, tools=tools, callbacks=[handler])
# CircuitBreak(RepeatedToolCallLoop) fires after 5 consecutive calls
# to the same tool name, regardless of arguments.
From tenacity / manual retry → cost × quality compound halt
# Before: retry N times regardless of quality trend
@retry(stop=stop_after_attempt(5))
def call_with_retry(): ...
# After: halts when BOTH cost cap AND quality decline trip
regulator = Regulator.for_user(uid).with_cost_cap(5_000)
handler = NoosCallbackHandler(regulator, raise_on_circuit_break=True)
# CircuitBreak(CostCapReached) fires when cumulative tokens_out ≥ cap
# AND mean recent quality drops below a threshold.
Paired with Langfuse / Helicone observability
Noos decides pre-delivery; observability tools log post-delivery. Both fit:
from langfuse.callback import CallbackHandler as LangfuseHandler
from noos_langchain import NoosCallbackHandler
# Both in the callbacks list — Langfuse records traces,
# Noos emits halt/warn decisions before the response ships.
executor.invoke(
{"input": "..."},
config={"callbacks": [LangfuseHandler(), NoosCallbackHandler(regulator)]},
)
# Pipe Noos metrics into your existing stack:
for key, value in handler.regulator.metrics_snapshot().items():
statsd_client.gauge(key, value) # noos.confidence, noos.total_tokens_out, ...
Behavioural notes
- One root chain = one Noos turn. Nested chains (
LLMChaininsideAgentExecutor, sub-graph nodes in LangGraph) stay in the same turn; their costs sum; the finalon_llm_endwins for scope-drift scoring. - Costs are cumulative across all LLM calls within the turn. The cost cap (set via
regulator.with_cost_cap(n)) trips when cumulativetokens_outcrossesnAND recent quality is below threshold — see the Noos regulator guide. - Tool-loop detection uses tool name only. Five consecutive calls to the same tool name (regardless of arguments) trigger
CircuitBreak(RepeatedToolCallLoop). Threshold is fixed at 5 per the Rust crate'sTOOL_LOOP_THRESHOLDconstant. on_llm_erroris a no-op. LangChain propagates the error; the regulator view stays consistent with the lastturn_start. The next rooton_chain_startopens a fresh turn cleanly.- Token-per-token emission (
emit_tokens=True) is off by default. LangChain doesn't surface per-token logprobs through callbacks, so the regulator uses its structural confidence fallback regardless. Enable only if you need pre-turn-complete decisions. - Not thread-safe. A
Regulatoris single-threaded. Create one handler per agent run. For concurrent executions, wrap each call in its own handler + regulator, then merge state viaexport_jsonafterward.
Persistence across sessions
Procedural correction memory and learned strategies survive process restarts:
# End of session — persist the regulator's state.
snapshot = regulator.export_json()
redis.set(f"noos:{user_id}", snapshot)
# Next session — restore before constructing the handler.
saved = redis.get(f"noos:{user_id}")
regulator = Regulator.from_json(saved) if saved else Regulator.for_user(user_id)
Correction patterns require at least 3 user_correction events on the same topic cluster before they fire a ProceduralWarning. Implicit-correction detection (fast same-cluster retries without explicit UserCorrection) is available via regulator.with_implicit_correction_window_secs(30.0); see regulator-guide.md §6.5 for the gotchas.
Examples
examples/basic_smoke.py— runs the handler against fabricated LangChain payloads. No LLM or API key required.examples/openai_tools_agent.py— full OpenAI tools agent with tool-loop halt protection. RequiresOPENAI_API_KEY.examples/anthropic_tools_agent.py— same shape against Claude Haiku vialangchain-anthropic. RequiresANTHROPIC_API_KEY.examples/langgraph_agent.py— LangGraph React agent with cost cap + tool-loop halt. RequiresOPENAI_API_KEY.examples/crewai_agent.py— CrewAI agent via the LangChain LLM callback path — no separatenoos-crewaipackage needed. RequiresANTHROPIC_API_KEY.
Related
noos— the core regulator (Python bindings over the Rust crate).nooson crates.io — the Rust crate.- Migration guide (crate) — LangChain
recursion_limit/tenacity/ Mem0 / Langfuse → Noos recipes. - Regulator guide — event lifecycle, Decision recipes, P10 priority rules.
License
MIT.
Metadata
Release files for noos-langchain 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| noos_langchain-0.1.0.tar.gz | 21.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| noos_langchain-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.8 kB
Release files / noos_langchain-0.1.0.tar.gz
| Download URL | noos_langchain-0.1.0.tar.gz |
|---|---|
| Size | 21.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
914234fbfe809806806631768f979ac8fde6e9172a853ad579899c400f361863
|
|
BLAKE2b-256 checksum How to use checksums |
a3db162b9a45a16732304623c83709ce080304d30edc6eb7c0ea030bbbc2a624
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|
Release files / noos_langchain-0.1.0-py3-none-any.whl
| Download URL | noos_langchain-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aefc9ee37def8892db7cc029ca5cd199803fde0b78dcec07a0019392822d7145
|
|
BLAKE2b-256 checksum How to use checksums |
3be2575e7938e4eed579c773699a58f1e36adc6682da4435c88bb9ddca115935
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.3
|