Repository navigation
feat(ag2-classic): traceAI-ag2-classic for autogen 0.14.x (TH-8237) - #211
Merged
Merged
Conversation
The Node OTLP exporter streams with Transfer-Encoding: chunked and sends no Content-Length. The Receiver read Content-Length only, so every Node export came back 400 with zero spans; two TH-8103 children had to put a de-chunking relay in front of it. Receiver now accepts both framings. It also records one entry per accepted export (path, lower-cased headers, flattened resource attributes) via requests(), so a contract test can assert X-Api-Key/X-Secret-Key and project_name through the shared harness instead of a private recorder. Verified: 6 harness tests pass, and the TanStack example's real Node exporter delivered 4 chunked exports (4 spans, collector path, both auth headers, project_name/project_type) with no relay. Refs: TH-8339, TH-8103
… 0.14.x New package python/frameworks/ag2-classic (traceAI-ag2-classic). setup() enables AG2 Classic's built-in autogen.opentelemetry (instrument_llm_wrapper, instrument_agent, instrument_pattern) on the Future AGI tracer provider from register(); it never creates a provider, sets the global provider or adds an exporter. AG2ClassicSpanProcessor is prepended ahead of the exporting processor and, from keys read in autogen/opentelemetry/instrumentators at 0.14.1: - maps ag2.span.type to gen_ai.span.kind (handoff -> CHAIN, never emitted) - copies the outermost gen_ai.conversation.id to session.id - adds gen_ai.cost.total (from gen_ai.usage.cost) and gen_ai.usage.total_tokens on LLM spans - sets ERROR status from upstream error.type (tool failures are caught inside execute_function, so upstream never sets status) - drops message/tool/human-input/code-output content unless capture_content=True (upstream has no flag outside the LLM wrapper) Version guard rejects non-0.14 autogen, Microsoft AutoGen, ag2 1.x and pyautogen layouts, and accepts the autogen==0.14.0 alias backed by ag2==0.14.0. Tests: mapping and guard unit tests, in-process real autogen runs against a loopback fake OpenAI server, and a shared-harness contract test over the real OTLP/HTTP exporter. Refs: TH-8237
Conversation spans carry the chat-wide aggregate_usage sum as gen_ai.usage.input_tokens/output_tokens (autogen 0.14.1 opentelemetry/instrumentators/agent_instrumentators/chat.py 81-82, 146-147) on top of the per-call tokens on the LLM spans (llm_wrapper.py 123-125). fi-collector promotes those keys on any span (adapter.go) and Observe sums total_tokens over a trace, so totals doubled. Non-LLM spans now keep the values under ag2.usage.*. Tests first: new processor test plus a trace-sum assertion in the two-agent setup test. 59 passed on autogen 0.14.1 and 0.14.0. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
uninstrument() removed AG2ClassicSpanProcessor and shut it down, but upstream instrument_agent patches each agent once with no undo. Agents instrumented earlier kept emitting spans on the same provider, now unfiltered: gen_ai.input/output.messages were exported on invoke_agent and conversation spans, and conversation spans regained the chat-wide gen_ai.usage.* tokens (double count). The same happened to a second handle sharing the processor when the first handle uninstrumented. uninstrument() now restores OpenAIWrapper.create only. The processor stays installed, shared by every handle on the provider, and shuts down with the provider. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
setup() installed the processor and patched OpenAIWrapper.create before instrumenting agents and patterns. If one of those raised, the caller never got the handle and the global patch could not be undone. setup() now restores OpenAIWrapper.create and re-raises; the processor stays, because agents instrumented before the failure keep emitting spans. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
Future AGI's TracerProvider.add_span_processor shuts down and clears every processor on the first call after register() (fi_instrumentation/otel.py 336-339), including AG2ClassicSpanProcessor. Content then leaked to the new processor and conversation spans kept the promoted gen_ai.usage.* tokens. setup() now wraps add_span_processor on the provider instance once. After the original runs, our processor is re-enabled if that call shut it down and re-prepended ahead of every other processor. setup() finds the processor through the wrapper, so repeated setup() calls never stack a second guard or processor. README Privacy and Troubleshooting updated. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
…ation The processor tracked only AG2-scope spans and stopped the ancestor walk at the first untracked parent. A user span between two AG2 conversations (an inner initiate_chat inside the user's own span, agent-as-tool style) hid the outer conversation, so the inner chat id became a second session.id in the same trace. on_start now records (parent span id, AG2 span or None) for every live span on the provider, still bounded by _MAX_LIVE_SPANS, and on_end releases every span. The walk continues through non-AG2 spans. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
Upstream spans come from a plain OpenTelemetry tracer; only Future AGI's own tracer applies context attributes (fi_instrumentation/instrumentation/ _tracers.py:165). using_session / using_user / using_attributes were therefore ignored: session.id was the chat id and user.id was absent. on_start now copies get_attributes_from_context() (read from the span's parent context) onto AG2 spans, without overwriting keys upstream already set. A context session.id is on the span before it ends, so it wins over the gen_ai.conversation.id copy; the conversation span keeps the real chat id in gen_ai.conversation.id. initiate_chats keeps one session per outermost conversation and no session on the multi_conversation parent. The README Session row now says so and points to using_session(...) for one session over the whole call; tests cover both. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
The conversation span still carried upstream's chat-wide gen_ai.usage.cost after the token move. The contract test now asserts, on the OTLP the shared Receiver decoded, that no gen_ai.usage.* / gen_ai.cost.total / llm.cost.total key appears on a non-LLM span and that each trace's gen_ai.usage.total_tokens sum equals its LLM calls x 18 (the reviewer's M1 and M5 mutations passed the contract test before). map_ag2_attributes now moves every gen_ai.usage.* key on a non-LLM span to ag2.usage.*, so the aggregate cost lands in ag2.usage.cost next to the aggregate tokens. README Cost row updated, and notes that upstream reports response.cost on cache hits (oai/client.py:1230-1241), so cached replies over-report cost (upstream behaviour, not fixed here). approved: Nikhil 2026-10-03 blanket Refs: TH-8237
A model call that raises was untested. The loopback fake model now answers HTTP 400 to a request carrying LLM_FAIL_TRIGGER (OpenAI raises BadRequestError, no retry), and failing_llm_chat runs a chat whose only model call is rejected. - test_setup.py: the LLM span keeps status ERROR, carries error.type=BadRequestError and gen_ai.span.kind=LLM, has no usage or cost, and no content leaks on the error path. - Contract test: the same through register()'s exporting processor (which turns only UNSET into OK) and the shared Receiver; the token-sum check counts successful LLM spans only. Upstream sets error.type and re-raises (llm_wrapper.py:97-101) and the SDK sets ERROR, so these pass on the pre-fix code by design; README Errors row documents the behaviour. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
_guard.py called the autogen==0.14.0 wheel an "8-file alias". The PyPI wheel has 6 entries, all under autogen-0.14.0.dist-info/ (LICENSE, METADATA, NOTICE.md, WHEEL, top_level.txt, RECORD), and ships no modules. Docstring only; no behaviour change. approved: Nikhil 2026-10-03 blanket Refs: TH-8237
setup() on a provider that already has AG2ClassicSpanProcessor reused it and re-prepended it, but never undid a shutdown that bypassed the add_span_processor guard (an unbound TracerProvider.add_span_processor, or the multi-processor's shutdown()). A disabled processor returns early in on_end, so spans were exported unmapped and unfiltered (fail-open). Reusing the processor now reopens it. Test: test_setup_again_reopens_a_processor_that_was_shut_down fails before the fix (LLM span exported with no gen_ai.span.kind and raw gen_ai.usage.* keys) and passes after. Review finding N4 (installed review t_7aba4044). approved: Nikhil 2026-10-03 blanket Refs: TH-8237
…ently Content filtering relies on AG2ClassicSpanProcessor running before the exporting processor. The SDK's ConcurrentMultiSpanProcessor runs every on_end in parallel on a thread pool, so that order is not guaranteed and an exporter can serialize a span before content is removed. setup() now logs a WARNING when the provider's active processor is a ConcurrentMultiSpanProcessor; the default SynchronousMultiSpanProcessor (what fi_instrumentation.register() uses) is unchanged and silent. Test: test_setup_warns_when_span_processors_run_concurrently fails before the fix (no warning record: assert 0 == 1) and passes after. Review finding N5 (installed review t_7aba4044). approved: Nikhil 2026-10-03 blanket Refs: TH-8237
…rument uninstrument() restored the captured original OpenAIWrapper.create unconditionally, so a wrapper another library installed after setup() was silently dropped. The handle now records the traced function upstream installed and restores the original only while OpenAIWrapper.create is still that function; otherwise it logs a WARNING, leaves the replacement in place and keeps ownership so a later uninstrument() can still restore. setup()'s rollback path is unchanged (nothing can replace the method between the patch and the rollback). Test: test_uninstrument_leaves_a_later_third_party_wrapper_in_place fails before the fix (OpenAIWrapper.create is the original, not the third-party wrapper) and passes after. Review finding N6 (installed review t_7aba4044). approved: Nikhil 2026-10-03 blanket Refs: TH-8237
The documented install (`pip install "autogen>=0.14.0,<0.15"` then
`pip install traceAI-ag2-classic`) has no model client: autogen's base
requirements do not include openai, so the README quick start raised
ImportError ('openai' is not installed) when it created an agent with an
llm_config. Install and the example docstring now use
`pip install traceAI-ag2-classic "autogen[openai]"`; the package itself
pins autogen to >=0.14.0,<0.15, and the 0.14.0 alias forwards the extra
to ag2[openai]==0.14.0.
Evidence (scratch venvs, Python 3.11, quick start up to setup(), no
model call): old two-line install -> ImportError from
create_openai_client; new command -> QUICKSTART_OK on autogen 0.14.1
with openai 2.54.0.
Review finding N1 (installed review t_7aba4044).
approved: Nikhil 2026-10-03 blanket
Refs: TH-8237
The Session row said gen_ai.conversation.id "keeps the chat id" inside using_session / using_attributes. That holds only for the conversation spans initiate_chat / a_initiate_chat start (upstream sets the key after start, chat.py:65,131). fi's using_session also puts the session id in context under gen_ai.conversation.id, and the processor copies missing context keys at on_start, so LLM, agent, tool, initiate_chats and resumed conversation spans carry the session id there. README reworded. Tests pin the documented behaviour (unit: chat m span; real autogen run: every non-conversation span). They pass before and after by design (doc fix, no code change); a mutation that skips copying gen_ai.conversation.id from context makes both fail with KeyError. Review finding N3 (installed review t_7aba4044). approved: Nikhil 2026-10-03 blanket Refs: TH-8237
The nested-conversation check tracks every live span on the provider and stops tracking at _MAX_LIVE_SPANS (10000); entries are freed in on_end. A README Limits section now says so, and that the nested-session split returns only if that many spans leak or run concurrently on the provider. Upstream ends every span it starts (each instrumentator uses `with tracer.start_as_current_span`, checked in autogen 0.14.1), so a leak needs other code on the same provider. Docs only; no code change. Review finding N2 (installed review t_7aba4044). approved: Nikhil 2026-10-03 blanket Refs: TH-8237
AG2 reports cost 0 for any model missing from its price table ("Model ...
is not found. The cost will be 0."). The processor copied that 0 to
gen_ai.cost.total, and Future AGI takes gen_ai.cost.total as the cost and
skips its own token pricing and the org's custom model prices. A real run
with claude-sonnet-5 through an OpenAI-compatible endpoint stored cost 0 on
every LLM span; without the 0 Future AGI prices those calls (0.0014 each).
Copy the upstream cost only when it is above 0.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fi-instrumentation-otel 1.0.0's register() raises AttributeError
('HTTPSpanExporter' object has no attribute '_headers') with
opentelemetry-exporter-otlp-proto-http 1.45, so a fresh install that
resolves 1.0.0 crashes on startup. Use the floor the other TH-8103
packages already declare.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Content (messages, tool arguments/results, human input, code output) is now exported by default and hidden with FI_HIDE_INPUTS / FI_HIDE_OUTPUTS (and the message/text variants) or setup(config=TraceConfig(...)), like every traceAI instrumentor. capture_content=True/False stays as an optional override. The processor hides the input and output sides independently. Re-verified 2026-10-16 end to end on autogen 0.14.1 and 0.14.0 against a local Future AGI stack; 105 tests pass on both. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
New Python package
traceAI-ag2-classicfor AG2 Classic (TH-8237): theautogen0.14.x distribution fromag2ai/ag2classic@86fa5ccb. It is a native-first enabler onautogen.opentelemetryplus anAG2ClassicSpanProcessor. It depends onautogen>=0.14.0,<0.15only; there is no dependency onag2,pyautogenorautogen-agentchat.The provisional mapping cells from the architecture were replaced with what the installed source actually emits (paths under
autogen/opentelemetry/):ag2.span.type(llm_wrapper.py:74,reply.py:36,chat.py:33,tool.py:26,pattern.py:163). It maps chat to LLM, invoke_agent to AGENT, execute_tool to TOOL, and conversation and speaker_selection to CHAIN.llm_wrapper.py:121-125), so nogenerate_replywrapper is needed. Cost is emitted too (gen_ai.usage.cost), and is copied togen_ai.cost.totalon LLM spans only.session.idcomes fromgen_ai.conversation.id, on the outermost conversation only. Nested speaker-selection chats would otherwise split the trace into several sessions.aggregate_usagesum ongen_ai.usage.*(chat.py:81-82, 146-147). fi-collector promotes those keys on any span, and Observe sums tokens over a trace. Now everygen_ai.usage.*key, cost included, stays on LLM spans only; the conversation aggregate moves toag2.usage.*. The README notes how cost behaves for cached responses.reply.py:51,58;tool.py:36,47; ...). The processor drops those keys unlesscapture_content=True.error.type. The processor sets ERROR status from it and runs first, ahead of fi'sBatchSpanProcessor, which would otherwise turn UNSET into OK.Decisions (approved: Nikhil 2026-10-03 blanket)
autogen==0.14.0is an alias that installsag2==0.14.0, and that package ships the top-levelautogenmodule with an identicalopentelemetrytree. The guard accepts exactly the autogen 0.14.x + ag2 0.14.x pair and rejects every otherag2owner, including 1.x.setup()signature:setup(agents=..., patterns=...)returns a handle withinstrument_agent,instrument_pattern,instrument_a2a_server(opt-in) anduninstrument. Upstream instruments objects one at a time (agent.py:27,pattern.py:23).Tests (run on 2f1a5fc, clean tree, by the coordinator)
The implementer also ran autogen 0.14.1 on Python 3.10 and 3.13 (90 passed each). The documented install was smoke-tested in clean venvs. The old line fails with
ImportError ... 'openai' is not installed; the new"autogen[openai]"line runs the quick start. No files changed outside the package.Limits
initiate_chats, remote agents and the A2A server are checked by mapping only.GroupChatManageremits no conversation span of its own upstream.OpenAIWrapper.createonce).Review
Round 1, an independent fresh-context review of 96ef5b3 (a subagent, not an installed card): CHANGES_REQUESTED. Fixed test first. All final tests run against 96ef5b3 give 22 failed / 59 passed (assertion or KeyError failures only).
uninstrument()left content dropping offadd_span_processorremoved the processorusing_attributesignored on AG2 spansinitiate_chatssessionsusing_sessiongives one session; the default (one per chat) is pinned by a testsetup()not atomicgen_ai.usage.*key off non-LLM spans (13 failed before)_guard.pyalias descriptionRound 2: installed pr-reviewer, then pr-verifier, on af1845a. Verdicts will be added here.
Round 2 follow-ups (af1845a..2f1a5fc)
The installed review passed this PR at
af1845a: pr-reviewer t_7aba4044 APPROVE, pr-verifier t_52edde89 VERIFIED. Its findings were fixed before the demo. Run againstaf1845a, the new tests give 3 failed and 87 passed (the N4, N5 and N6 tests).pip install traceAI-ag2-classic "autogen[openai]"(0af3c23)2f1a5fc)9c2d57d)setup()reopens it (2aee53b)ConcurrentMultiSpanProcessorordering is not guaranteed9df5068)uninstrument()restoredcreateover a later wrapper6410498)Round 3 used installed pr-verifier t_7cf46910 at
2f1a5fcand returned VERIFIED, ready to merge. N1 and N4-N6 are fixed and N2/N3 are documented, with no regressions. The delta does not touch_processor.pyor the mapping. The new setup code can only re-enable filtering (N4), add a log line (N5) or skip a restore (N6).Review is complete at
2f1a5fc. Two P3 follow-ups, left for after merge:setup()again after hand-editing_span_processors.setup()only prepends the processor when it is missing, so an edit that leaves it present but not first keeps that order. Either always prepend in the reuse branch (it is idempotent) or reword the README.setup()reopens it are redacted, but they lackusing_*context attributes. Only spans that straddle the reopen are affected.Video demo
Narrated terminal demo, 6:14, recorded at head
2f1a5fc(the reviewed head), 1080p H.264/AAC, 9 chapters. sha2569ad20da7e548336ff206cbbee2384701681eb3508f1d60f882c8f5f31b624b2c.The video, captions, transcript, chapter list, preview and media check are attached privately to Linear TH-8237 (Future AGI workspace access needed). Every run uses real
autogen0.14.1 with a loopback fake OpenAI-compatible model, placeholder keys and the shared harness receiver: no vendor call and no live fi-collector.2f1a5fc: branch, 0 uncommitted files, 16 commits on3eaadc8initiate_chatwith toolget_weather; 7 spans in 1 trace, 1 POST to/tracer/v1/traceswith both auth headers (values not shown); kinds CHAIN / AGENT / LLM / TOOL; promoted tokens input 22 / total 36 counted once (conversation aggregate kept asag2.usage.input_tokens); no content keys and 0 prompt or tool-argument occurrences by defaultusing_sessioncarriessession.idon all 7 spansaf1845a, no kinds, input tokens 44 for 2 x 11 and content keys exported; at the head, LLM/CHAIN kinds, 22 counted once, no contentDisclosed cuts remove frozen waits only (the 90-test run took 146.72 s under recording load, shown on screen). Checked by the coordinator: sha256, full decode, chapter markers, one 3.1 s pause at the first chapter boundary and none inside a chapter, frames per chapter compared with the driver output, no keys or header values in frames, captions or transcript.
Stacked on #203. Not merged.
Linear: TH-8237. approved: Nikhil 2026-10-03 blanket
Verification sweep (2026-10-12)
dev, and the spans were checked in ClickHouse and in the Observe UI (hierarchy, span types, input/output, model and tokens, sessions, error status, and a secret scan of everything stored).Re-verification (2026-10-16)
Result: one fix pushed (content default), everything else confirmed. Commit
695f8b8on this branch (new commit, no force push).Versions. Latest stable upstream
autogen0.14.1 (PyPI, 2026-10-16;ag2ai/ag2classichas no commit since 2026-07-08) and the pinned floor 0.14.0 (alias forag2==0.14.0).fi-instrumentation-otel1.1.0, Python 3.11. Model: CLIProxygpt-6-luna(OpenAI-compatible, real calls). Target: local Future AGI (th8103-e2e), projectth8103-rick-ag2-classic.What was wrong. The package used a package-specific switch,
setup(capture_content=False)default, so content was off by default. Every traceAI instrumentor followsTraceConfig(content on by default, hidden withFI_HIDE_INPUTS/FI_HIDE_OUTPUTSand the message/text variants).Fix.
setup(config=TraceConfig | None, capture_content: bool | None = None): default readsTraceConfig()(envFI_HIDE_*);capture_content=True/Falsestays as an optional override. The processor hides the input side (gen_ai.input.messages, tool arguments, human prompt,input.value) and the output side (gen_ai.output.messages, tool result, code output, chat summaries,output.value) independently. README, CHANGELOG, example and 8 new/updated tests follow. Docs page: future-agi/docs#993 (draft, base main, no navigation.ts).Tests.
python/frameworks/ag2-classic/tests+python/tests/harness: 105 passed on autogen 0.14.1 and 105 passed on 0.14.0 (Python 3.11).Real E2E (installed as a customer would:
pip install -eof the package +autogen[openai]; tool chatuser/assistantwithget_weather):aab00f16-b6ee-4840-be04-d8819f076e6e848844bb-8d00-4177-a4c0-f937db6785900cd2f18d-135e-4a9f-a18a-c29a78352ebbERROR:ExecutionErroraf156619-d295-4508-924f-f7fa2e5c1f48ERROR:BadRequestErrorFI_HIDE_INPUTS=true FI_HIDE_OUTPUTS=truef2c0921f-b005-4dfb-82ab-c89643c792c0efce4992-a629-4719-83d8-06ecdfa0eb15/71eb69bc-9b84-4519-84db-2b8830e396c0fi_verify(PASS,--expect-type llm tool agent), traceaab00f16:Tokens sit on the two LLM spans only (counted once);
session.idis set; the UI trace view shows the tree and agent graph correctly.Privacy proof. A unique marker (
MK<run><n>) was put in the prompt, the tool result and the tool exception message, and every stored ClickHousespanscolumn (attributes, resource, events) was scanned. Defaults: marker on 7 of 7 spans. Both hide variables set: marker on 0 spans, for the success run, the tool-fails run and the model-fails run. No/Users, user name, host name orTracebackin any stored span in either mode. Known: provider and tool exception messages appear in span status text (standard OpenTelemetry), noted in the README.Limits unchanged. Code-execution, human-input, remote-agent and A2A spans are mapping-tested only; retrieval and a
GroupChatManagerconversation span are not emitted upstream.