LangChain callback handler for Noos — pre-delivery halt/warn decisions from an agent's LLM+tool event stream
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file noos_langchain-0.1.0.tar.gz.
File metadata
- Download URL: noos_langchain-0.1.0.tar.gz
- Upload date:
- Size: 21.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
914234fbfe809806806631768f979ac8fde6e9172a853ad579899c400f361863
|
|
| MD5 |
474e298b9cf8f5d8c724ee10857db5c2
|
|
| BLAKE2b-256 |
a3db162b9a45a16732304623c83709ce080304d30edc6eb7c0ea030bbbc2a624
|
File details
Details for the file noos_langchain-0.1.0-py3-none-any.whl.
File metadata
- Download URL: noos_langchain-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aefc9ee37def8892db7cc029ca5cd199803fde0b78dcec07a0019392822d7145
|
|
| MD5 |
53d9546b31afde7f6bddce605ee9830e
|
|
| BLAKE2b-256 |
3be2575e7938e4eed579c773699a58f1e36adc6682da4435c88bb9ddca115935
|