@aletheia-labs/adapters-langgraph
v0.1.2
Published
LangGraph node helpers for Aletheia authority-governed memory.
Maintainers
Readme
@aletheia-labs/adapters-langgraph
LangGraph node helpers for Aletheia authority-governed memory.
Status
Experimental 0.1.x adapter. It is intentionally small: LangGraph owns graph
state and branching; Aletheia owns source-bound memory authority. The default
surface is helper-only, with optional recipe fragments for common gates.
Install
pnpm add @aletheia-labs/adapters-langgraph @aletheia-labs/core @langchain/langgraphQuickstart
import {
createExecutionRecheckNode,
createGovernedRecallNode,
createHumanApprovalInterruptNode,
createHumanApprovalResumeNode,
createTryActNode,
actionFingerprint,
decisionSnapshot,
recalledMemoryIds,
routeDecisionOutcome,
} from '@aletheia-labs/adapters-langgraph';
const governedRecall = createGovernedRecallNode({
authority,
buildQuery: (state) => ({
agentId: state.agentId,
scope: state.scope,
limit: 5,
}),
toUpdate: (result) => ({
recallDecision: decisionSnapshot(result.decision),
recalledMemoryIds: recalledMemoryIds(result),
}),
});
const executionRecheck = createExecutionRecheckNode({
authority,
buildAction: (state) => state.action,
buildContext: (state) => ({
agentId: state.agentId,
citedMemoryIds: state.recalledMemoryIds,
scope: state.scope,
}),
toUpdate: (result) => ({
actionDecision: decisionSnapshot(result.decision),
route: routeDecisionOutcome(result.decision.outcome),
}),
});Human Approval Resume
Sensitive actions still return ask_human. The adapter can validate a resumed
human approval without turning the approval into memory authority:
const resumeAfterApproval = createHumanApprovalResumeNode({
authority,
getAction: (state) => state.action,
getContext: (state) => state.context,
getApproval: (state) => state.approval,
toUpdate: (result) => ({
approvalResume: result,
}),
});The resume helper checks:
- the approval fingerprint matches the current action and context;
- the approval is not denied or expired;
- the cited memory still authorizes a local authority probe;
- receiver-side
tryAct()still returns the expected boundary.
With LangGraph checkpoints, wrap the interrupt packet in LangGraph's native interrupt API and resume with a human record:
import { Command, MemorySaver, interrupt } from '@langchain/langgraph';
const buildApprovalPacket = createHumanApprovalInterruptNode({
getAction: (state) => state.action,
getContext: (state) => state.context,
getDecision: (state) => state.actionDecision,
toUpdate: (approvalRequest) => ({ approvalRequest }),
});
const interruptForApproval = async (state) => {
const { approvalRequest } = await buildApprovalPacket(state);
const approval = interrupt(approvalRequest);
return { approval, approvalRequest };
};
const graph = builder.compile({ checkpointer: new MemorySaver() });
await graph.invoke(input, { configurable: { thread_id: 'thread-1' } });
await graph.invoke(new Command({ resume: approvalRecord }), {
configurable: { thread_id: 'thread-1' },
});The checkpoint resumes graph control flow only. The next node should still call
createHumanApprovalResumeNode() so Aletheia can recheck current memory before
the host performs any effect. Durable checkpointers such as SQLite or Postgres
belong to the host runtime; this adapter stays helper-only and does not store
Aletheia authority in LangGraph state.
Experimental Recipes
Recipes package common gates without executing host effects:
import { createGovernedEffectGateFragment } from '@aletheia-labs/adapters-langgraph/recipes';
const effectGate = createGovernedEffectGateFragment({
authority,
buildAction: (state) => state.action,
buildContext: (state) => state.context,
routeNames: {
allowLocalShadow: 'run_local_effect',
askHuman: 'interrupt_for_approval',
conflictBoundaryPacket: 'surface_conflict',
noEffect: 'no_effect',
},
toTryActUpdate: (result) => ({
actionDecision: decisionSnapshot(result.decision),
}),
toExecutionRecheckUpdate: (result, _action, _context, route) => ({
executionDecision: decisionSnapshot(result.decision),
route,
}),
});
builder
.addNode(effectGate.nodeNames.tryAct, effectGate.nodes.tryAct)
.addNode(effectGate.nodeNames.executionRecheck, effectGate.nodes.executionRecheck)
.addConditionalEdges('execution_recheck', (state) => state.route, effectGate.routes);Recipes are available from the root export for convenience and from the
explicit /recipes subpath for hosts that want to keep helper-only imports
separate. createGovernedEffectGateFragment() and
createHumanApprovalGateFragment() expose a
LANGGRAPH_RECIPE_BOUNDARY_CONTRACT declaring that recipes do not execute
tools, grant permission, own checkpointers, own provider clients, or store
durable authority in graph state.
Boundary
- No semantic retrieval, embeddings, or vector store.
- No tool execution.
- No provider credentials.
- No durable authority in graph state.
- Sensitive actions still route to human approval through Aletheia
tryAct(). - Approval packets are evidence for one exact effect, not reusable permission.
The adapter returns ordinary async node functions and recipe fragments. Use them
inside a LangGraph StateGraph, then execute effects only after the
execution-time recheck route reaches a host-owned effect node.
