Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -219,5 +219,5 @@ __marimo__/
/.pre-commit-cache/

# Agents
# codex-instructions
codex-instructions
local
174 changes: 0 additions & 174 deletions codex-instructions/sdk-production-refactor.md

This file was deleted.

54 changes: 27 additions & 27 deletions instrumentation-packages/codon-instrumentation-langgraph/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This document explains how to convert an existing LangGraph `StateGraph` into a
## Why Wrap a LangGraph Graph?
- **Zero instrumentation boilerplate:** every LangGraph node is auto-wrapped with `track_node`, producing OpenTelemetry spans without manual decorators.
- **Stable identifiers:** nodes become `NodeSpec`s with deterministic SHA-256 IDs, and the overall graph gets a logic ID for caching, retries, and provenance.
- **Audit-first runtime:** executions use Codon’s token scheduler, producing a detailed ledger (token enqueue/dequeue, node completions, custom events) for compliance.
- **Telemetry-first runtime:** executions use native LangGraph semantics while Codon emits spans for each node invocation and workload run metadata for downstream analysis.
- **Drop-in ergonomics:** call `LangGraphWorkloadAdapter.from_langgraph(graph, ...)` and keep your existing LangGraph code unchanged.

---
Expand All @@ -33,7 +33,7 @@ from myproject.langgraph import build_graph

langgraph = build_graph() # returns StateGraph or compiled graph

workload = LangGraphWorkloadAdapter.from_langgraph(
graph = LangGraphWorkloadAdapter.from_langgraph(
langgraph,
name="ResearchAgent",
version="1.0.0",
Expand All @@ -42,16 +42,14 @@ workload = LangGraphWorkloadAdapter.from_langgraph(
)

initial_state = {"topic": "Sustainable cities"}
report = workload.execute({"state": initial_state}, deployment_id="dev")
print(report.node_results("writer")[-1])
print(f"Ledger entries: {len(report.ledger)}")
result = graph.invoke({"topic": "Sustainable cities"})
print(result)
```

### What Happened?
1. Every LangGraph node was registered as a Codon node via `add_node`, producing a `NodeSpec`.
2. Edges in the LangGraph became workload edges, so `runtime.emit` drives execution.
3. `execute` seeded tokens with the provided state, ran the graph in token order, and captured telemetry & audit logs.
4. You can inspect `report.ledger` for compliance, or `report.node_results(...)` for business outputs.
1. Every LangGraph node was registered as a Codon `NodeSpec` for deterministic IDs.
2. The adapter returned an instrumented graph that preserves native LangGraph execution semantics.
3. Telemetry spans are emitted via callbacks during normal LangGraph invocation (no `execute` call required).

---

Expand All @@ -63,7 +61,7 @@ The adapter inspects your graph to extract:

Need finer control? Provide a `node_overrides` mapping where each entry is either a plain dict or `NodeOverride` object. You can specify the role, callable used for `NodeSpec` introspection, model metadata, and explicit schemas:
```python
workload = LangGraphWorkloadAdapter.from_langgraph(
graph = LangGraphWorkloadAdapter.from_langgraph(
langgraph,
name="SupportBot",
version="2.3.0",
Expand All @@ -83,9 +81,7 @@ Any fields you omit fall back to the adapter defaults. Overrides propagate to te
---

## Handling State
- The adapter expects your token payload to contain a dictionary under the `"state"` key.
- Each LangGraph node receives that state, invokes the original runnable, and emits updated state to successors.
- Shared run-level data lives in `runtime.state`; you can read it from within nodes for cross-node coordination.
- The adapter does not alter LangGraph state semantics. Nodes receive the same state and return the same updates they would without Codon instrumentation.

Example node signature inside your LangGraph graph:
```python
Expand All @@ -95,21 +91,21 @@ async def researcher(state):
insights = await fetch_insights(plan)
return {"insights": insights}
```
When wrapped by the adapter, the Codon node sees `message["state"]` and merges the returned dict with the existing state.
When wrapped by the adapter, the original LangGraph node callable is preserved and invoked as usual.

---

## Entry Nodes
By default the adapter infers entry nodes as those with no incoming edges. You can override this by supplying `entry_nodes`:
```python
workload = LangGraphWorkloadAdapter.from_langgraph(
graph = LangGraphWorkloadAdapter.from_langgraph(
langgraph,
name="OpsAgent",
version="0.4.1",
entry_nodes=["bootstrap"],
)
```
At execution time you can still override entry nodes via `workload.execute(..., entry_nodes=[...])` if needed.
Entry nodes are still inferred from the LangGraph structure; override them by changing the graph itself before compilation.

---

Expand Down Expand Up @@ -146,33 +142,34 @@ langgraph.add_edge("writer", "critic")
langgraph.add_edge("critic", "writer") # feedback loop
langgraph.add_edge("critic", "finalize")

workload = LangGraphWorkloadAdapter.from_langgraph(
graph = LangGraphWorkloadAdapter.from_langgraph(
langgraph,
name="ReflectiveAgent",
version="0.1.0",
)
result = workload.execute({"state": {"topic": "urban gardens"}}, deployment_id="demo")
print(result.node_results("finalize")[-1])
result = graph.invoke({"topic": "urban gardens"})
print(result)
```
The ledger records each iteration through the loop, and `runtime.state` tracks iteration counts for auditing.
Each iteration is reflected in node spans and the graph snapshot span ties the run to the full graph definition.

---

## Adapter Options & Artifacts
- Use `compile_kwargs={...}` when calling `LangGraphWorkloadAdapter.from_langgraph(...)` to compile your graph with checkpointers, memory stores, or any other LangGraph runtime extras. The adapter still inspects the pre-compiled graph for node metadata while compiling with the provided extras so the runtime is ready to go.
- Set `return_artifacts=True` to receive a `LangGraphAdapterResult` containing the `CodonWorkload`, the original state graph, and the compiled graph. This makes it easy to hand both artifacts to downstream systems (e.g., background runners) without re-compiling.
- Provide `runtime_config={...}` during adaptation to establish default invocation options (e.g., base callbacks, tracing settings). At execution time, pass `langgraph_config={...}` to `workload.execute(...)` to layer per-run overrides; both configs are merged and supplied alongside Codon’s telemetry callback.
- Regardless of the return value, the resulting workload exposes `langgraph_state_graph`, `langgraph_compiled_graph`, `langgraph_compile_kwargs`, and `langgraph_runtime_config` for quick access to the underlying LangGraph objects.
- Set `return_artifacts=True` to receive a `LangGraphAdapterResult` containing the `CodonWorkload`, the original state graph, the compiled graph, and the instrumented graph wrapper. This makes it easy to hand both artifacts to downstream systems (e.g., background runners) without re-compiling.
- Provide `runtime_config={...}` during adaptation to establish default invocation options (e.g., base callbacks, tracing settings). At invocation time, pass `config={...}` to `graph.invoke(...)` (or `graph.ainvoke(...)`) to layer per-run overrides; both configs are merged and supplied alongside Codon’s telemetry callbacks.
- The returned graph exposes `workload`, `langgraph_state_graph`, `langgraph_compiled_graph`, `langgraph_compile_kwargs`, and `langgraph_runtime_config` for quick access to the underlying LangGraph objects and metadata.

---

## Telemetry & Audit Integration
- Call `initialize_telemetry(service_name=...)` once during process startup to export spans via OTLP. The initializer now lives in the core SDK (`codon_sdk.instrumentation.initialize_telemetry`) and is re-exported here. It defaults the endpoint to `https://ingest.codonops.ai:4317`, injects `x-codon-api-key` from the argument or `CODON_API_KEY` env, and respects `OTEL_EXPORTER_OTLP_ENDPOINT`/`OTEL_SERVICE_NAME` overrides. If you already have an OTEL tracer provider (e.g., via auto-instrumentation), set `CODON_ATTACH_TO_EXISTING_OTEL_PROVIDER=true` or pass `attach_to_existing=True` to add Codon’s exporter to the existing provider instead of replacing it.
- Each node span now carries workload metadata (`codon.workload.id`, `codon.workload.run_id`, `codon.workload.logic_id`, `codon.workload.deployment_id`, `codon.organization.id`) so traces can be rolled up by workload, deployment, or organization without joins.
- Each graph invocation emits a single graph snapshot span (`agent.graph`) with node/edge structure serialized in `codon.graph.definition_json` for full topology visibility.
- `LangGraphTelemetryCallback` is attached automatically when invoking LangChain runnables; it captures model vendor/identifier, token usage (prompt, completion, total), and response metadata, all of which is emitted as span attributes (`codon.tokens.*`, `codon.model.*`, `codon.node.raw_attributes_json`).
- Instrumentation writes into the shared `NodeTelemetryPayload` (`runtime.telemetry`) defined by the SDK so future mixins collect the same schema-aligned fields without reimplementing bookkeeping.
- Node inputs/outputs and latency are recorded alongside status codes, enabling the `trace_events` schema to be populated directly from exported span data.
- The audit ledger still covers token enqueue/dequeue, node completions, custom events (`runtime.record_event`), and stop requests for replay and compliance workflows.
- Telemetry spans cover node inputs/outputs, latency, model usage, and workload/run identifiers without altering LangGraph execution.

### Analytics Alignment
- The span attribute set is designed to satisfy the MVP telemetry tables in `docs/design/Codon Telemetry Data Schema - MVP Version.txt`. You can aggregate by `nodespec_id` or `logic_id` to compute token totals, error rates, or latency buckets per node.
Expand All @@ -181,9 +178,12 @@ The ledger records each iteration through the loop, and `runtime.state` tracks i
---

## Limitations & Roadmap
- Conditional edges: currently you emit along every registered edge; to mimic conditionals, have your node wrapper decide which edges receive tokens. Future versions aim to map LangGraph’s conditional constructs directly.
- Streaming tokens / concurrency: not yet supported; the adapter processes tokens sequentially (though you can extend it for concurrency).
- Persistence: the workload runtime is in-memory today. Roadmap includes pluggable stores for tokens/state/audit (see `docs/vision/codon-workload-design-philosophy.md`).
- Conditional edges: telemetry spans are emitted only for nodes actually executed; the graph snapshot span provides the full topology for downstream analysis.
- Streaming tokens: relies on LangGraph/LangChain streaming support; Codon captures model usage when providers expose usage metadata.
- Persistence: execution remains native to LangGraph; persistence is governed by your graph checkpointers and stores.
- Direct SDK calls: if a node bypasses LangChain runnables and calls provider SDKs directly, token usage callbacks are not emitted.
- Custom runnables: objects that do not implement `invoke/ainvoke` cannot be auto-wrapped for config injection.
- Async context boundaries: background tasks may lose ContextVar state, preventing automatic config propagation.

---

Expand Down
Loading
Loading