From b3ed117cd86dbbca02a6cd8ccc719156fe1f53eb Mon Sep 17 00:00:00 2001 From: cirilla-zmh Date: Tue, 14 Apr 2026 22:30:13 +0800 Subject: [PATCH 1/2] Add instrumentation for copaw multi agents Change-Id: Iadc503014273de1c9455ff6f7518474083dbf1a8 Co-developed-by: Cursor --- .../README.md | 32 ++++ .../instrumentation/copaw/__init__.py | 38 ++++- .../instrumentation/copaw/_constants.py | 33 ++++ .../instrumentation/copaw/_env_carrier.py | 60 +++++++ .../instrumentation/copaw/_shell_patch.py | 159 ++++++++++++++++++ .../instrumentation/copaw/patch.py | 94 ++++++----- .../tests/test_child_entry_suppression.py | 62 +++++++ .../tests/test_shell_propagate.py | 62 +++++++ 8 files changed, 500 insertions(+), 40 deletions(-) create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_constants.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_env_carrier.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_shell_patch.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_child_entry_suppression.py create mode 100644 instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_shell_propagate.py diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/README.md b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/README.md index 08344f03d..4da62edb0 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/README.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/README.md @@ -128,3 +128,35 @@ LLM call inside the agent. Calls to models, tools, and other AgentScope primitives are **not** duplicated here: use AgentScope (and your existing model client) instrumentations alongside this package so they appear as child spans under this entry when configured. + +## Sub-agent CLI and trace continuity (`multi_agent_collaboration`) + +When a parent CoPaw agent runs a **child** CoPaw process via AgentScope’s +`execute_shell_command` (for example `copaw agents chat`), the default +subprocess inherits `os.environ` only and the trace would **break** across +processes. + +This package also wraps `agentscope.tool._coding._shell.execute_shell_command`. +For commands whose string contains **`copaw`**, **`agents`**, and **`chat`**, it: + +1. Merges the current trace context into the subprocess `env` (W3C + `TRACEPARENT` / `TRACESTATE` and any fields from your configured global + propagators, using the same uppercase-env convention as OpenTelemetry’s + [environment carrier](https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py)). +2. Sets **`COPAW_OTEL_CHILD_AGENT=1`** so the child process can recognize the + call as a linked sub-agent. + +In the child process, `AgentRunner.query_handler` **does not** create +`enter_ai_application_system`. It only `attach`es the extracted parent +context so AgentScope (and other) spans continue **in the same trace** as the +parent. Configure **`OTEL_PROPAGATORS`** to include `baggage` if you rely on +`session_id` / `user_id` baggage from the parent entry across this boundary. + +Advanced: set **`COPAW_OTEL_INJECT_SHELL_TRACE=1`** to inject context for +**every** `execute_shell_command` invocation (still sets +`COPAW_OTEL_CHILD_AGENT=1`). Use only if **all** such children are CoPaw +agents that should suppress entry; otherwise unrelated shell children could +incorrectly skip their entry span. + +The child CoPaw process must load this instrumentation (and OTel export +configuration) the same way as the parent for spans to export correctly. diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/__init__.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/__init__.py index a9ab59112..2ecb1f763 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/__init__.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/__init__.py @@ -16,8 +16,10 @@ LoongSuite CoPaw instrumentation (``copaw >= 0.1.0``). Instruments ``AgentRunner.query_handler`` with ``ExtendedTelemetryHandler.entry`` -(``enter_ai_application_system``). Agent / tool / LLM spans come from AgentScope -and other instrumentations. +(``enter_ai_application_system``), and when AgentScope is present, +``execute_shell_command`` so child ``copaw agents chat`` subprocesses receive +trace context and optional entry suppression (``COPAW_OTEL_CHILD_AGENT``). +Agent / tool / LLM spans otherwise come from AgentScope and other instrumentations. Usage ----- @@ -37,6 +39,10 @@ from wrapt import wrap_function_wrapper +from opentelemetry.instrumentation.copaw._shell_patch import ( + _MODULE_SHELL, + make_execute_shell_command_wrapper, +) from opentelemetry.instrumentation.copaw.package import _instruments from opentelemetry.instrumentation.copaw.patch import ( _MODULE_RUNNER, @@ -57,6 +63,7 @@ class CoPawInstrumentor(BaseInstrumentor): def __init__(self) -> None: super().__init__() self._handler: ExtendedTelemetryHandler | None = None + self._shell_command_wrapped = False def instrumentation_dependencies(self) -> Collection[str]: return _instruments @@ -79,9 +86,36 @@ def _instrument(self, **kwargs: Any) -> None: ) logger.debug("Instrumented CoPaw AgentRunner.query_handler") + try: + shell_wrapper = make_execute_shell_command_wrapper() + wrap_function_wrapper( + _MODULE_SHELL, + "execute_shell_command", + shell_wrapper, + ) + self._shell_command_wrapped = True + logger.debug("Instrumented AgentScope execute_shell_command") + except ImportError: + logger.debug( + "agentscope.tool._coding._shell not importable; " + "skipping execute_shell_command hook" + ) + def _uninstrument(self, **kwargs: Any) -> None: del kwargs self._handler = None + if self._shell_command_wrapped: + self._shell_command_wrapped = False + try: + import agentscope.tool._coding._shell as shell_module # noqa: PLC0415 + + unwrap(shell_module, "execute_shell_command") + logger.debug("Uninstrumented AgentScope execute_shell_command") + except Exception as exc: + logger.warning( + "Failed to uninstrument execute_shell_command: %s", + exc, + ) try: import copaw.app.runner.runner as runner_module # noqa: PLC0415 diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_constants.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_constants.py new file mode 100644 index 000000000..1fbefe463 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_constants.py @@ -0,0 +1,33 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Shared constants for CoPaw subprocess trace linking.""" + +from __future__ import annotations + +import os + +# Set on the child process environment when the parent spawns ``copaw agents chat`` +# (or when ``COPAW_OTEL_INJECT_SHELL_TRACE`` forces injection). The CoPaw +# entry wrapper skips ``enter_ai_application_system`` when this is "1". +COPAW_OTEL_CHILD_AGENT = "COPAW_OTEL_CHILD_AGENT" + +# When set to a truthy value, inject trace context into every shell command +# (still sets COPAW_OTEL_CHILD_AGENT — use only if all such children are CoPaw +# agents that should suppress Entry). +COPAW_OTEL_INJECT_SHELL_TRACE = "COPAW_OTEL_INJECT_SHELL_TRACE" + + +def is_copaw_child_agent_process() -> bool: + return os.environ.get(COPAW_OTEL_CHILD_AGENT) == "1" diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_env_carrier.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_env_carrier.py new file mode 100644 index 000000000..d33856c26 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_env_carrier.py @@ -0,0 +1,60 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Environment variable carrier for trace context (vendored from upstream OTel API). + +The published ``opentelemetry-api`` wheels may not yet ship +``opentelemetry.propagators._envcarrier``; this module mirrors that +implementation so subprocess ``env=`` injection works consistently. +See: https://github.com/open-telemetry/opentelemetry-python/blob/main/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py +""" + +from __future__ import annotations + +import os +from collections.abc import MutableMapping +from typing import Dict, Iterable, List, Mapping, Optional + +from opentelemetry.propagators.textmap import Getter, Setter + + +class EnvironmentGetter(Getter[Mapping[str, str]]): + """Getter for extracting context from a snapshot of ``os.environ``.""" + + def __init__(self) -> None: + self.carrier: Dict[str, str] = { + k.lower(): v for k, v in os.environ.items() + } + + def get(self, carrier: Mapping[str, str], key: str) -> Optional[List[str]]: + del carrier # interface compatibility + val = self.carrier.get(key.lower()) + if val is None: + return None + if isinstance(val, Iterable) and not isinstance(val, str): + return list(val) + return [val] + + def keys(self, carrier: Mapping[str, str]) -> List[str]: + del carrier + return list(self.carrier.keys()) + + +class EnvironmentSetter(Setter[MutableMapping[str, str]]): + """Setter for building an ``env`` dict (keys stored uppercase).""" + + def set( + self, carrier: MutableMapping[str, str], key: str, value: str + ) -> None: + carrier[key.upper()] = value diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_shell_patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_shell_patch.py new file mode 100644 index 000000000..341277c48 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/_shell_patch.py @@ -0,0 +1,159 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inject trace context into AgentScope ``execute_shell_command`` subprocess env.""" + +from __future__ import annotations + +import asyncio +import logging +import os +from typing import Any, Callable + +from opentelemetry import propagate + +from ._constants import ( + COPAW_OTEL_CHILD_AGENT, + COPAW_OTEL_INJECT_SHELL_TRACE, +) +from ._env_carrier import EnvironmentSetter + +logger = logging.getLogger(__name__) + +_MODULE_SHELL = "agentscope.tool._coding._shell" +_PATCH_TARGET = "execute_shell_command" + + +def _truthy_env(name: str) -> bool: + return os.environ.get(name, "").lower() in ("1", "true", "yes", "on") + + +def should_inject_trace_for_shell_command(command: str) -> bool: + """Return True if trace env should be merged for this shell command.""" + if _truthy_env(COPAW_OTEL_INJECT_SHELL_TRACE): + return True + c = command.lower() + return "copaw" in c and "agents" in c and "chat" in c + + +async def _run_shell_command_with_env( + command: str, + timeout: int, + env: dict[str, str], +) -> Any: + """Same behavior as agentscope ``execute_shell_command``, with explicit *env*.""" + from agentscope.message import TextBlock # noqa: PLC0415 + from agentscope.tool._response import ToolResponse # noqa: PLC0415 + + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + bufsize=0, + env=env, + ) + + try: + await asyncio.wait_for(proc.wait(), timeout=timeout) + stdout, stderr = await proc.communicate() + stdout_str = stdout.decode("utf-8") + stderr_str = stderr.decode("utf-8") + returncode = proc.returncode + + except asyncio.TimeoutError: + stderr_suffix = ( + f"TimeoutError: The command execution exceeded " + f"the timeout of {timeout} seconds." + ) + returncode = -1 + try: + proc.terminate() + stdout, stderr = await proc.communicate() + stdout_str = stdout.decode("utf-8") + stderr_str = stderr.decode("utf-8") + if stderr_str: + stderr_str += f"\n{stderr_suffix}" + else: + stderr_str = stderr_suffix + except ProcessLookupError: + stdout_str = "" + stderr_str = stderr_suffix + + return ToolResponse( + content=[ + TextBlock( + type="text", + text=( + f"{returncode}" + f"{stdout_str}" + f"{stderr_str}" + ), + ), + ], + ) + + +def _build_subprocess_env() -> dict[str, str]: + merged = os.environ.copy() + delta: dict[str, str] = {} + try: + propagate.get_global_textmap().inject( + delta, setter=EnvironmentSetter() + ) + except Exception: + logger.debug("Failed to inject trace into env", exc_info=True) + return merged + merged.update(delta) + merged[COPAW_OTEL_CHILD_AGENT] = "1" + return merged + + +def make_execute_shell_command_wrapper() -> Callable[..., Any]: + """Factory for ``wrapt`` wrapper around ``execute_shell_command``.""" + + async def execute_shell_command_wrapper( + wrapped: Any, + instance: Any, + args: Any, + kwargs: Any, + ) -> Any: + del instance + command = "" + if args: + command = str(args[0]) + elif kwargs.get("command") is not None: + command = str(kwargs["command"]) + + timeout = 300 + if len(args) >= 2: + timeout = int(args[1]) + elif "timeout" in kwargs: + timeout = int(kwargs["timeout"]) + + if not should_inject_trace_for_shell_command(command): + return await wrapped(*args, **kwargs) + + env = _build_subprocess_env() + try: + return await _run_shell_command_with_env(command, timeout, env) + except Exception: + logger.debug( + "%s.%s inject path failed; falling back to original", + _MODULE_SHELL, + _PATCH_TARGET, + exc_info=True, + ) + return await wrapped(*args, **kwargs) + + return execute_shell_command_wrapper diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/patch.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/patch.py index 4480a11f5..68fc5de45 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/patch.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/src/opentelemetry/instrumentation/copaw/patch.py @@ -20,14 +20,18 @@ import timeit from typing import Any, Callable +from opentelemetry import context as otel_context +from opentelemetry import propagate from opentelemetry.util.genai.extended_handler import ExtendedTelemetryHandler from opentelemetry.util.genai.types import Error +from ._constants import is_copaw_child_agent_process from ._entry_utils import ( build_entry_invocation, output_message_from_yield_item, parse_query_handler_call, ) +from ._env_carrier import EnvironmentGetter logger = logging.getLogger(__name__) @@ -49,48 +53,62 @@ def query_handler_wrapper( async def _aiter(): msgs, request = parse_query_handler_call(args, kwargs) invocation = build_entry_invocation(instance, msgs, request) - handler.start_entry(invocation) - monotonic_start = timeit.default_timer() - saw_first_token = False - last_assistant = None + child_mode = is_copaw_child_agent_process() + child_ctx_token = None try: - agen = wrapped(*args, **kwargs) - async for item in agen: - if not saw_first_token: - invocation.response_time_to_first_token = int( - (timeit.default_timer() - monotonic_start) - * 1_000_000_000 + if child_mode: + getter = EnvironmentGetter() + parent_ctx = propagate.extract({}, getter=getter) + child_ctx_token = otel_context.attach(parent_ctx) + else: + handler.start_entry(invocation) + monotonic_start = timeit.default_timer() + saw_first_token = False + last_assistant = None + try: + agen = wrapped(*args, **kwargs) + async for item in agen: + if not saw_first_token: + invocation.response_time_to_first_token = int( + (timeit.default_timer() - monotonic_start) + * 1_000_000_000 + ) + saw_first_token = True + out = output_message_from_yield_item(item) + if out is not None: + last_assistant = out + yield item + except BaseException as exc: + if isinstance(exc, GeneratorExit): + if last_assistant is not None: + invocation.output_messages = [last_assistant] + if not child_mode: + handler.stop_entry(invocation) + raise + logger.debug( + "%s.%s raised %s", + _MODULE_RUNNER, + _PATCH_TARGET, + type(exc).__name__, + exc_info=True, + ) + if not child_mode: + handler.fail_entry( + invocation, + Error( + message=str(exc) or type(exc).__name__, + type=type(exc), + ), ) - saw_first_token = True - out = output_message_from_yield_item(item) - if out is not None: - last_assistant = out - yield item - except BaseException as exc: - if isinstance(exc, GeneratorExit): + raise + else: if last_assistant is not None: invocation.output_messages = [last_assistant] - handler.stop_entry(invocation) - raise - logger.debug( - "%s.%s raised %s", - _MODULE_RUNNER, - _PATCH_TARGET, - type(exc).__name__, - exc_info=True, - ) - handler.fail_entry( - invocation, - Error( - message=str(exc) or type(exc).__name__, - type=type(exc), - ), - ) - raise - else: - if last_assistant is not None: - invocation.output_messages = [last_assistant] - handler.stop_entry(invocation) + if not child_mode: + handler.stop_entry(invocation) + finally: + if child_ctx_token is not None: + otel_context.detach(child_ctx_token) return _aiter() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_child_entry_suppression.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_child_entry_suppression.py new file mode 100644 index 000000000..85e9521d0 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_child_entry_suppression.py @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Child CoPaw CLI: suppress Entry span when COPAW_OTEL_CHILD_AGENT is set.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +pytest.importorskip("copaw") +pytest.importorskip("agentscope.message") + +from agentscope.message import Msg, TextBlock # noqa: E402 + + +@pytest.mark.asyncio +async def test_child_agent_process_does_not_emit_entry_span( + instrument, + span_exporter, + monkeypatch, +): + monkeypatch.setenv("COPAW_OTEL_CHILD_AGENT", "1") + monkeypatch.setenv( + "TRACEPARENT", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + ) + + from copaw.app.runner.runner import AgentRunner # noqa: PLC0415 + + async def fake_resolve(self, session_id, query): + del self, session_id, query + denial = Msg( + name="Friday", + role="assistant", + content=[TextBlock(type="text", text="child-no-entry")], + ) + return (denial, True, None) + + monkeypatch.setattr(AgentRunner, "_resolve_pending_approval", fake_resolve) + + runner = AgentRunner(agent_id="child-agent") + req = SimpleNamespace( + session_id="sess-child", user_id="user-child", channel="console" + ) + + async for _ in runner.query_handler([], req): + pass + + assert not span_exporter.get_finished_spans() diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_shell_propagate.py b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_shell_propagate.py new file mode 100644 index 000000000..4d7cf5841 --- /dev/null +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/tests/test_shell_propagate.py @@ -0,0 +1,62 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from opentelemetry import trace +from opentelemetry.instrumentation.copaw._constants import ( + COPAW_OTEL_CHILD_AGENT, + COPAW_OTEL_INJECT_SHELL_TRACE, +) +from opentelemetry.instrumentation.copaw._shell_patch import ( + _build_subprocess_env, + should_inject_trace_for_shell_command, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + + +def test_should_inject_for_copaw_agents_chat_command(): + assert should_inject_trace_for_shell_command( + 'copaw agents chat -m "hello" ' + ) + assert not should_inject_trace_for_shell_command("ls -la") + assert not should_inject_trace_for_shell_command("copaw app") + + +def test_should_inject_when_env_forces_all_shell(monkeypatch): + monkeypatch.setenv(COPAW_OTEL_INJECT_SHELL_TRACE, "1") + assert should_inject_trace_for_shell_command("/bin/true") + + +def test_build_subprocess_env_sets_child_marker_and_traceparent(): + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + prev = trace.get_tracer_provider() + trace.set_tracer_provider(provider) + try: + tracer = trace.get_tracer(__name__) + with tracer.start_as_current_span("parent_shell"): + env = _build_subprocess_env() + finally: + trace.set_tracer_provider(prev) + + assert env[COPAW_OTEL_CHILD_AGENT] == "1" + assert "TRACEPARENT" in env + tp = env["TRACEPARENT"] + assert tp.startswith("00-") From 07b5f161ec85e80061b9dd9f1cd7bfa5fbdcc8aa Mon Sep 17 00:00:00 2001 From: cirilla-zmh Date: Tue, 14 Apr 2026 22:38:55 +0800 Subject: [PATCH 2/2] Add changelog Change-Id: Ibfc5a4ad46670bd1bb18bcefcad95f876ebf389d Co-developed-by: Cursor --- .../loongsuite-instrumentation-copaw/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/CHANGELOG.md index fab126196..476856e3f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-copaw/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-copaw/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- **Multi-agent**: propagate trace context to child CoPaw processes via + ``execute_shell_command``; suppress duplicate entry span in child + ([#164](https://github.com/alibaba/loongsuite-python-agent/pull/164)) + ## Version 0.4.0 (2026-04-03) ### Added