Skip to main content

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:

  1. Poll after invoke — read handler.last_decision when execution completes.
  2. React mid-run — pass on_decision=callback to observe every decision transition.
  3. Abort on halt — pass raise_on_circuit_break=True to raise CircuitBreakError the 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 (LLMChain inside AgentExecutor, sub-graph nodes in LangGraph) stay in the same turn; their costs sum; the final on_llm_end wins 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 cumulative tokens_out crosses n AND 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's TOOL_LOOP_THRESHOLD constant.
  • on_llm_error is a no-op. LangChain propagates the error; the regulator view stays consistent with the last turn_start. The next root on_chain_start opens 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 Regulator is single-threaded. Create one handler per agent run. For concurrent executions, wrap each call in its own handler + regulator, then merge state via export_json afterward.

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

Related

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)

Source distribution for noos-langchain 0.1.0
File Size Uploaded
noos_langchain-0.1.0.tar.gz 21.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for noos-langchain 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page