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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
Goal:
Propagate durable runtime correlation identifiers into observability spans.

Scope:
- Add safe, low-cardinality runtime correlation attributes to AgentKit pipeline
stage and LLM generation spans.
- Include `tape_id` from the active tape.
- Forward only whitelisted `AgentRunContext.trace_metadata` correlation keys.
- Bind HTTP root runs to `turn_id` and `tape_id` trace metadata from
`SessionManager`.
- Keep OTLP/Langfuse exporter privacy filtering intact.

Out of scope:
- Do not add raw prompt, message, content, tool result, secret, or text values
to span attributes.
- Do not change trace id derivation.
- Do not change observability configuration defaults.
- Do not change runtime persistence or replay APIs.

Context:
- ADRs:
- `docs/adr/0029-durable-runtime-identity.md`
- `docs/adr/0028-observability-and-langfuse-integration.md`
- Postmortem patterns consulted:
- `postmortem/patterns/PM-0001-address-code-review-issues.md`
- `postmortem/patterns/PM-0006-add-usage-event-fields-and-fix-tool-name-kwarg-in-pipeline.md`
- `postmortem/patterns/PM-0009-preserve-neutral-bare-anchor-semantics.md`
- `postmortem/patterns/PM-0010-route-incremental-context-append-through-tapeview.md`
- Relevant files:
- `src/agentkit/runtime/pipeline.py`
- `src/coding_agent/ui/session_manager.py`
- `tests/agentkit/runtime/test_pipeline.py`
- `tests/ui/test_session_manager_runtime.py`
- `tests/coding_agent/test_observability.py`

Target tests:
- `uv run pytest tests/agentkit/runtime/test_pipeline.py -k "span" -v`
- `uv run pytest tests/agentkit/runtime/test_pipeline.py -v`
- `uv run pytest tests/agentkit/observability/test_core.py -v`
- `uv run pytest tests/ui/test_session_manager_runtime.py -k "run_id or approval or message_snapshot" -v`
- `uv run pytest tests/coding_agent/test_observability.py -v`
- `uv run ruff check src/agentkit/runtime/pipeline.py src/coding_agent/ui/session_manager.py tests/agentkit/runtime/test_pipeline.py tests/ui/test_session_manager_runtime.py tests/coding_agent/test_observability.py`
- `uv run ruff format --check src/agentkit/runtime/pipeline.py src/coding_agent/ui/session_manager.py tests/agentkit/runtime/test_pipeline.py tests/ui/test_session_manager_runtime.py tests/coding_agent/test_observability.py`

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Stop conditions:
- Stop with a blocker if the work requires exporting raw prompt/message/result
data or weakening the existing OTLP denylist.
- Stop with a blocker if trace metadata needs a broader public API decision.
23 changes: 22 additions & 1 deletion coding-agent/docs/durable_runtime/GOAL_PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Last updated: 2026-05-19
| G07 | Complete | `SessionManager` saves `{run_id}:latest` compacted message snapshots from `ctx.messages` when a runtime store is configured. |
| G08 | Complete | HTTP replay APIs expose runtime run records, latest message snapshots, and runtime events with `last_event_id` filtering. |
| G09 | Complete | `SessionManager` persists durable approval interaction records for pending requests, applied decisions, session auto-approvals, and approval timeouts when a runtime store is configured. |
| G10 | Pending | Not started. |
| G10 | Complete | AgentKit pipeline spans include safe runtime correlation attributes, and HTTP root runs bind `turn_id`/`tape_id` trace metadata without weakening OTLP privacy filtering. |
| G11 | Pending | Not started. |

## G00 Verification
Expand Down Expand Up @@ -128,3 +128,24 @@ Last updated: 2026-05-19
- `uv run pytest tests/approval/test_coordinator.py tests/approval/test_store.py -v`
- `uv run pytest tests/coding_agent/test_pg_runtime_store.py -k "interaction" -v`
- `uv run ruff check src/coding_agent/ui/session_manager.py tests/ui/test_session_manager_runtime.py`

## G10 Verification

- Added `.opencode/prompts/tasks/durable-runtime-observability-correlation.md`.
- Updated `src/agentkit/runtime/pipeline.py`.
- Updated `src/coding_agent/ui/session_manager.py`.
- Updated `tests/agentkit/runtime/test_pipeline.py`.
- Updated `tests/ui/test_session_manager_runtime.py`.
- Updated `tests/coding_agent/test_observability.py`.
- Postmortem patterns consulted:
- `postmortem/patterns/PM-0001-address-code-review-issues.md`
- `postmortem/patterns/PM-0006-add-usage-event-fields-and-fix-tool-name-kwarg-in-pipeline.md`
- `postmortem/patterns/PM-0009-preserve-neutral-bare-anchor-semantics.md`
- `postmortem/patterns/PM-0010-route-incremental-context-append-through-tapeview.md`
- Target tests:
- `uv run pytest tests/agentkit/runtime/test_pipeline.py -k "span" -v`
- `uv run pytest tests/agentkit/runtime/test_pipeline.py -v`
- `uv run pytest tests/agentkit/observability/test_core.py -v`
- `uv run pytest tests/ui/test_session_manager_runtime.py -k "run_id or approval or message_snapshot" -v`
- `uv run pytest tests/coding_agent/test_observability.py -v`
- `uv run ruff check src/agentkit/runtime/pipeline.py src/coding_agent/ui/session_manager.py tests/agentkit/runtime/test_pipeline.py tests/ui/test_session_manager_runtime.py tests/coding_agent/test_observability.py`
36 changes: 34 additions & 2 deletions coding-agent/src/agentkit/runtime/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@
"render": "runtime.stage.apply_directives",
"dispatch": "runtime.stage.dispatch",
}
_TRACE_METADATA_ATTRIBUTE_KEYS = frozenset(
{
"turn_id",
"tape_id",
"tool_call_id",
"interaction_id",
"event_id",
"checkpoint_id",
}
)

StructuredToolResultScopeFactory = Callable[[bool], AbstractContextManager[None]]

Expand Down Expand Up @@ -108,8 +118,31 @@ def _stage_span_attributes(
}
if ctx.session_id:
attributes["session_id"] = ctx.session_id
attributes.update(_runtime_correlation_attributes(ctx))
return attributes


def _safe_trace_metadata_attributes(run_context: AgentRunContext) -> dict[str, Any]:
attributes: dict[str, Any] = {}
for key, value in run_context.trace_metadata.items():
if key not in _TRACE_METADATA_ATTRIBUTE_KEYS:
continue
if isinstance(value, bool | int | float | str):
attributes[key] = value
return attributes


def _runtime_correlation_attributes(ctx: "PipelineContext") -> dict[str, Any]:
attributes: dict[str, Any] = {}
if ctx.run_context is not None:
attributes.update(_safe_trace_metadata_attributes(ctx.run_context))
attributes["run_id"] = ctx.run_context.run_id
if ctx.run_context.agent_id is not None:
attributes["agent_id"] = ctx.run_context.agent_id
if ctx.run_context.parent_run_id is not None:
attributes["parent_run_id"] = ctx.run_context.parent_run_id
if ctx.tape.tape_id:
attributes["tape_id"] = ctx.tape.tape_id
return attributes


Expand All @@ -125,8 +158,7 @@ def _llm_generation_attributes(
}
if ctx.session_id:
attributes["session_id"] = ctx.session_id
if ctx.run_context is not None:
attributes["run_id"] = ctx.run_context.run_id
attributes.update(_runtime_correlation_attributes(ctx))
model = ctx.config.get("model")
if isinstance(model, str) and model:
attributes["model"] = model
Expand Down
4 changes: 4 additions & 0 deletions coding-agent/src/coding_agent/ui/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,11 +1073,15 @@ def _bind_root_run_identity(self, session: Session, ctx: Any, run_id: str) -> No
if run_context is not None:
if not isinstance(run_context, AgentRunContext):
raise TypeError("runtime context run_context must be AgentRunContext")
trace_metadata = dict(run_context.trace_metadata)
trace_metadata["turn_id"] = run_id
trace_metadata["tape_id"] = ctx.tape.tape_id
ctx.run_context = replace(
run_context,
session_id=session.id,
run_id=run_id,
parent_run_id=None,
trace_metadata=trace_metadata,
)

def _run_metadata_for_session(self, session: Session) -> JSONObject:
Expand Down
27 changes: 20 additions & 7 deletions coding-agent/tests/agentkit/runtime/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,14 +376,19 @@ async def mock_stream(messages, tools=None, **kwargs):

plugin._mock_llm.stream = mock_stream
ctx = PipelineContext(
tape=Tape(),
tape=Tape(tape_id="tape-1"),
session_id="session-1",
run_context=AgentRunContext(
session_id="session-1",
run_id="turn-1",
agent_id=None,
run_id="run-1",
agent_id="root",
environment=RuntimeContextEnvironment(),
context_budget=ContextBudget(),
trace_metadata={
"turn_id": "run-1",
"interaction_id": "interaction-1",
"prompt": "do-not-record",
},
),
config={"observation_sink": sink},
)
Expand All @@ -407,10 +412,15 @@ async def mock_stream(messages, tools=None, **kwargs):
assert build_context_span.attributes == {
"stage": "build_context",
"session_id": "session-1",
"run_id": "turn-1",
"run_id": "run-1",
"agent_id": "root",
"tape_id": "tape-1",
"turn_id": "run-1",
"interaction_id": "interaction-1",
"entry_count_before": 0,
"entry_count_after": 0,
}
assert "prompt" not in build_context_span.attributes
model_span = stage_spans[3]
assert model_span.attributes["entry_count_before"] == 0
assert model_span.attributes["entry_count_after"] == 1
Expand All @@ -436,14 +446,15 @@ async def mock_stream(messages, tools=None, **kwargs):

plugin._mock_llm.stream = mock_stream
ctx = PipelineContext(
tape=Tape(),
tape=Tape(tape_id="tape-1"),
session_id="session-1",
run_context=AgentRunContext(
session_id="session-1",
run_id="turn-1",
run_id="run-1",
agent_id=None,
environment=RuntimeContextEnvironment(),
context_budget=ContextBudget(),
trace_metadata={"turn_id": "run-1"},
),
config={
"model": "test-model",
Expand All @@ -458,7 +469,9 @@ async def mock_stream(messages, tools=None, **kwargs):
assert llm_spans[0].status == "ok"
assert llm_spans[0].attributes == {
"session_id": "session-1",
"run_id": "turn-1",
"run_id": "run-1",
"tape_id": "tape-1",
"turn_id": "run-1",
"message_count": 2,
"tool_schema_count": 0,
"model": "test-model",
Expand Down
19 changes: 19 additions & 0 deletions coding-agent/tests/coding_agent/test_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ def test_otlp_sink_posts_span_without_prompt_or_output_content() -> None:
attributes={
"session_id": "session-1",
"input_tokens": 10,
"content": "content-do-not-send",
"message": "message-do-not-send",
"prompt": "do-not-send",
"result": "result-do-not-send",
"secret": "secret-do-not-send",
"text": "text-do-not-send",
},
start_time=1.0,
end_time=2.0,
Expand All @@ -86,6 +91,20 @@ def test_otlp_sink_posts_span_without_prompt_or_output_content() -> None:
assert "llm.generation" in body
assert "input_tokens" in body
assert "do-not-send" not in body
assert "content-do-not-send" not in body
assert "message-do-not-send" not in body
assert "result-do-not-send" not in body
assert "secret-do-not-send" not in body
assert "text-do-not-send" not in body
span = _first_span(json.loads(body))
exported_keys = {
item["key"]
for item in span.get("attributes", [])
if isinstance(item, dict) and isinstance(item.get("key"), str)
}
assert {"content", "message", "prompt", "result", "secret", "text"}.isdisjoint(
exported_keys
)


def test_otlp_sink_groups_spans_by_session_and_run() -> None:
Expand Down
4 changes: 4 additions & 0 deletions coding-agent/tests/ui/test_session_manager_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ def fake_create_agent(**kwargs):
assert session.runtime_ctx.run_context.session_id == session_id
assert session.runtime_ctx.run_context.run_id == run_id
assert session.runtime_ctx.run_context.parent_run_id is None
assert session.runtime_ctx.run_context.trace_metadata == {
"turn_id": run_id,
"tape_id": session.runtime_ctx.tape.tape_id,
}


@pytest.mark.asyncio
Expand Down