diff --git a/examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb b/examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb new file mode 100644 index 00000000..d2c5500f --- /dev/null +++ b/examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb @@ -0,0 +1,309 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7fb27b941602401d91542211134fc71a", + "metadata": {}, + "source": [ + "# GitHub Copilot SDK tracing with Openlayer\n", + "\n", + "[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)]", + "(https://colab.research.google.com/github/openlayer-ai/openlayer-python/blob/main/examples/tracing/copilot_sdk/copilot_sdk_tracing.ipynb)\n", + "\n", + "This notebook shows how to stream traces from agents built on the ", + "[GitHub Copilot SDK](https://github.com/github/copilot-sdk) to Openlayer.\n", + "\n", + "Each `send()` becomes one Openlayer trace:\n", + "\n", + "```\n", + "AGENT \"GitHub Copilot\" <- the user prompt in, the final answer out\n", + "\u251c\u2500 CHAT_COMPLETION \"turn 0\" <- model, tokens, cost, latency\n", + "\u251c\u2500 TOOL \"bash\" <- arguments in, result out\n", + "\u251c\u2500 AGENT \"subagent: Explore\" <- a `task` dispatch\n", + "\u2502 \u251c\u2500 CHAT_COMPLETION \"turn 0\"\n", + "\u2502 \u2514\u2500 TOOL \"view\"\n", + "\u2514\u2500 CHAT_COMPLETION \"turn 1\"\n", + "```\n", + "\n", + "> **Requires Python 3.11+** \u2014 that is `github-copilot-sdk`'s own floor." + ] + }, + { + "cell_type": "markdown", + "id": "acae54e37e7d407bbb7b55eff062a284", + "metadata": {}, + "source": [ + "## 1. Install" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a63283cbaf04dbcab1f6479b197f3a8", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install openlayer 'github-copilot-sdk>=1.0.11'" + ] + }, + { + "cell_type": "markdown", + "id": "8dd0d8092fe74a7c96281538738b07e2", + "metadata": {}, + "source": [ + "## 2. Credentials\n", + "\n", + "The Copilot SDK authenticates with your logged-in GitHub user by default; set ", + "`GITHUB_TOKEN` if you'd rather be explicit. You need a GitHub account with Copilot access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72eea5119410473aa328ad9291626812", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "os.environ[\"OPENLAYER_API_KEY\"] = \"YOUR_OPENLAYER_API_KEY_HERE\"\n", + "os.environ[\"OPENLAYER_INFERENCE_PIPELINE_ID\"] = \"YOUR_OPENLAYER_INFERENCE_PIPELINE_ID_HERE\"\n", + "# os.environ[\"GITHUB_TOKEN\"] = \"YOUR_GITHUB_TOKEN_HERE\"" + ] + }, + { + "cell_type": "markdown", + "id": "8edb47106e1a46a883d545849b8ab81b", + "metadata": {}, + "source": [ + "## 3. Enable tracing\n", + "\n", + "`init()` is the canonical entry point. With `auto_instrument` on (the default) it\n", + "detects every supported SDK you have installed \u2014 including the Copilot SDK \u2014 and\n", + "patches it, so every session you create is traced with no change to the code that\n", + "builds them.\n", + "\n", + "If you'd rather be explicit about which sessions are traced, pass\n", + "`on_event=openlayer_event_handler()` to `create_session()` instead. Mixing the two\n", + "is safe \u2014 the patch defers to a handler you supply rather than adding a second one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10185d26023b46108eb7d9f57d49d2b3", + "metadata": {}, + "outputs": [], + "source": [ + "from openlayer.lib import init\n", + "\n", + "init()\n" + ] + }, + { + "cell_type": "markdown", + "id": "8763a12b2bbd4a93a75aff182afb95dc", + "metadata": {}, + "source": [ + "## 4. A workspace to work in\n", + "\n", + "Copilot is a coding agent, so give it some real files to look at." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7623eae2785240b9bd12b16a66d81610", + "metadata": {}, + "outputs": [], + "source": [ + "import pathlib\n", + "import tempfile\n", + "\n", + "workspace = pathlib.Path(tempfile.mkdtemp(prefix=\"openlayer-copilot-\"))\n", + "(workspace / \"app.py\").write_text(\"def greet(name):\\n return f'hi {name}'\\n\")\n", + "(workspace / \"README.md\").write_text(\"# Demo\\n\\nA tiny example project.\\n\")\n", + "print(workspace) # noqa: T201\n" + ] + }, + { + "cell_type": "markdown", + "id": "7cdc8c89c7104fffa095e18ddfef8986", + "metadata": {}, + "source": [ + "## Scenario 1 \u2014 a basic session\n", + "\n", + "`PermissionHandler.approve_all` auto-approves tool use. In production you'd supply your own ", + "policy; the decision is captured on the tool step either way." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b118ea5561624da68c537baed56e602f", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from copilot import CopilotClient, PermissionHandler\n", + "\n", + "\n", + "async def basic_session():\n", + " client = CopilotClient(working_directory=str(workspace), log_level=\"error\")\n", + " await client.start()\n", + " try:\n", + " session = await client.create_session(\n", + " working_directory=str(workspace),\n", + " on_permission_request=PermissionHandler.approve_all,\n", + " )\n", + " reply = await session.send_and_wait(\n", + " \"List the files here with bash, then summarize the project in one sentence.\",\n", + " timeout=300,\n", + " )\n", + " print(reply.data.content) # noqa: T201\n", + " await session.disconnect()\n", + " finally:\n", + " await client.stop()\n", + "\n", + "\n", + "await basic_session()" + ] + }, + { + "cell_type": "markdown", + "id": "938c804e27f84196a10c8828c723f798", + "metadata": {}, + "source": [ + "## Scenario 2 \u2014 a client-side tool and a subagent\n", + "\n", + "Tools you define with `define_tool` run in *your* process and still appear as `TOOL` steps. ", + "A `task` dispatch becomes a nested `AGENT` step, with the subagent's own turns and tool calls inside it.\n", + "\n", + "This also shows `openlayer_event_handler()` \u2014 the explicit alternative to `trace_copilot()`, ", + "useful when you build sessions yourself. It composes with your own `on_event`: pass both and ", + "each still receives every event." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "504fb2a444614c0babb325280ed9130a", + "metadata": {}, + "outputs": [], + "source": [ + "from copilot import CopilotClient, PermissionHandler, define_tool\n", + "from pydantic import BaseModel\n", + "\n", + "\n", + "class WeatherParams(BaseModel):\n", + " city: str\n", + "\n", + "\n", + "@define_tool(description=\"Get the current weather for a city.\")\n", + "def get_weather(params: WeatherParams) -> str:\n", + " return f\"It is 22C and sunny in {params.city}.\"\n", + "\n", + "\n", + "async def tools_and_subagent():\n", + " client = CopilotClient(working_directory=str(workspace), log_level=\"error\")\n", + " await client.start()\n", + " try:\n", + " session = await client.create_session(\n", + " working_directory=str(workspace),\n", + " on_permission_request=PermissionHandler.approve_all,\n", + " tools=[get_weather],\n", + " )\n", + " reply = await session.send_and_wait(\n", + " \"Do two things: call get_weather for Lisbon, and delegate to a subagent \"\n", + " \"to read app.py and summarize it.\",\n", + " timeout=300,\n", + " )\n", + " print(reply.data.content) # noqa: T201\n", + " await session.disconnect()\n", + " finally:\n", + " await client.stop()\n", + "\n", + "\n", + "await tools_and_subagent()" + ] + }, + { + "cell_type": "markdown", + "id": "59bbdb311c014d738909a11f9e486628", + "metadata": {}, + "source": [ + "## Scenario 3 \u2014 multi-turn session grouping\n", + "\n", + "Each `send()` is its own trace, but they all carry the same Copilot session id, so Openlayer ", + "groups them into one session." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b43b363d81ae4b689946ece5c682cd59", + "metadata": {}, + "outputs": [], + "source": [ + "from copilot import CopilotClient, PermissionHandler\n", + "\n", + "\n", + "async def multi_turn():\n", + " client = CopilotClient(working_directory=str(workspace), log_level=\"error\")\n", + " await client.start()\n", + " try:\n", + " session = await client.create_session(\n", + " working_directory=str(workspace),\n", + " on_permission_request=PermissionHandler.approve_all,\n", + " )\n", + " for prompt in (\n", + " \"What files are in this directory?\",\n", + " \"What does app.py define?\",\n", + " ):\n", + " reply = await session.send_and_wait(prompt, timeout=300)\n", + " print(f\"Q: {prompt}\\nA: {reply.data.content}\\n\") # noqa: T201\n", + " await session.disconnect()\n", + " finally:\n", + " await client.stop()\n", + "\n", + "\n", + "await multi_turn()" + ] + }, + { + "cell_type": "markdown", + "id": "8a65eabff63a45729fe45fb5ade58bdc", + "metadata": {}, + "source": [ + "## What lands in Openlayer\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Row prompt / output** | the user's message and the final assistant answer |\n", + "| **Cost** | priced by Openlayer from the real provider and model |\n", + "| **Tokens** | input / output / cached / cache-creation, as a non-overlapping partition |\n", + "| **Session** | the Copilot session id, so multi-turn conversations group |\n", + "| **Tools** | arguments, results, success/failure, permission decisions, MCP server |\n", + "| **Subagents** | nested agent steps with their own turns, tools and token totals |\n", + "\n", + "One thing worth knowing: Copilot's own `cost` field is **premium-request units, not dollars** ", + "(a flat per-model multiplier, identical on every call regardless of size). Openlayer therefore ", + "prices the call itself from the provider and model, and keeps Copilot's figure in step metadata ", + "as `copilot_premium_requests`." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/openlayer/lib/__init__.py b/src/openlayer/lib/__init__.py index 55ede7e3..3392fed8 100644 --- a/src/openlayer/lib/__init__.py +++ b/src/openlayer/lib/__init__.py @@ -24,6 +24,8 @@ "unpatch_google_adk", "trace_claude_agent_sdk", "traced_claude_agent_sdk_query", + "trace_copilot", + "untrace_copilot", "trace_gemini", "trace_google_genai", "update_current_trace", @@ -393,6 +395,63 @@ def traced_claude_agent_sdk_query(*, prompt, options=None, inference_pipeline_id ) +# ---------------------------- GitHub Copilot SDK ---------------------------- # +def trace_copilot( + *, + inference_pipeline_id=None, + truncate_tool_output_chars: int = 8192, + capture_reasoning: bool = True, +): + """Enable Openlayer tracing for the GitHub Copilot SDK. + + Monkey-patches ``copilot.CopilotClient.create_session`` so every session + becomes an Openlayer trace with nested steps for assistant turns, tool + calls and subagents, including tokens and cost. Idempotent, and composes + with any ``on_event`` handler you pass yourself. + + Requirements: + ``github-copilot-sdk>=1.0.11`` must be installed: + ``pip install 'github-copilot-sdk>=1.0.11'`` + + Args: + inference_pipeline_id: Optional Openlayer inference pipeline ID. Falls + back to the ``OPENLAYER_INFERENCE_PIPELINE_ID`` env var. + truncate_tool_output_chars: Maximum characters of tool output to + capture per TOOL step. Defaults to 8192. + capture_reasoning: Whether to capture reasoning text into + chat-completion step metadata. Defaults to True. + + Example: + >>> import os + >>> os.environ["OPENLAYER_API_KEY"] = "..." + >>> os.environ["OPENLAYER_INFERENCE_PIPELINE_ID"] = "..." + >>> from openlayer.lib import trace_copilot + >>> trace_copilot() + >>> + >>> from copilot import CopilotClient + >>> client = CopilotClient() + >>> await client.start() + >>> session = await client.create_session() + >>> await session.send_and_wait("Summarize this repository") + """ + # pylint: disable=import-outside-toplevel + from .integrations import copilot_sdk as _integration + + return _integration.trace_copilot( + inference_pipeline_id=inference_pipeline_id, + truncate_tool_output_chars=truncate_tool_output_chars, + capture_reasoning=capture_reasoning, + ) + + +def untrace_copilot(): + """Undo :func:`trace_copilot`, restoring the original ``create_session``.""" + # pylint: disable=import-outside-toplevel + from .integrations import copilot_sdk as _integration + + return _integration.untrace_copilot() + + # -------------------------------- Google Gemini --------------------------------- # def _legacy_gemini_model_class(): """``google.generativeai.GenerativeModel``, or None if not installed.""" diff --git a/src/openlayer/lib/integrations/_auto.py b/src/openlayer/lib/integrations/_auto.py index 81ef2f02..5f69da19 100644 --- a/src/openlayer/lib/integrations/_auto.py +++ b/src/openlayer/lib/integrations/_auto.py @@ -187,6 +187,15 @@ def _do_patch() -> None: _patch_via("google_adk_tracer", "trace_google_adk"), _patch_via("google_adk_tracer", "unpatch_google_adk"), ), + # `github-copilot-sdk` imports as `copilot`. Unlike the entries above this + # one patches a *session factory* rather than a client class, but the + # patch/unpatch contract is the same: idempotent and reversible. + IntegrationSpec( + "copilot", + "copilot", + _patch_via("copilot_sdk", "trace_copilot"), + _patch_via("copilot_sdk", "untrace_copilot"), + ), ) diff --git a/src/openlayer/lib/integrations/copilot_sdk.py b/src/openlayer/lib/integrations/copilot_sdk.py new file mode 100644 index 00000000..89abf3ae --- /dev/null +++ b/src/openlayer/lib/integrations/copilot_sdk.py @@ -0,0 +1,994 @@ +"""Openlayer tracing integration for the GitHub Copilot SDK. + +The Copilot SDK (``github-copilot-sdk`` on PyPI, imported as ``copilot``) drives +the Copilot CLI over JSON-RPC and exposes a rich session event stream. This +module subscribes to that stream and turns each ``send()`` into an Openlayer +trace with nested steps for assistant turns, tool calls and subagents. + +Why we buffer instead of building steps live +-------------------------------------------- +``assistant.usage`` -- which carries every token count and the premium-request +figure -- is ``ephemeral`` and absent from ``session.get_events()``, so we have +to listen live. But Copilot fires tool calls *concurrently*: three +``tool.execution_start`` events arrive before any completion, and the +completions come back out of order. Driving step context managers straight from +the live callbacks would therefore nest sibling tools inside one another. We +buffer live and build the whole trace in one deterministic, correctly-nested +pass when the session goes idle. + +See ``docs/superpowers/specs/2026-08-27-github-copilot-sdk-integration-design.md``. +""" + +from __future__ import annotations + +import enum +import logging +import time +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Callable, Dict, List, Optional, Tuple + +from ..tracing import enums, tracer + +logger = logging.getLogger(__name__) + +__all__ = [ + "CopilotConfig", + "CopilotTraceCollector", + "openlayer_event_handler", + "trace_copilot", + "untrace_copilot", +] + +ROOT_STEP_NAME = "GitHub Copilot" + +# High-frequency events that carry no step-level meaning. Dropped at ingest so +# the buffer stays small -- a single session emits hundreds of these. +_IGNORED_EVENT_TYPES = frozenset( + { + "assistant.message_delta", + "assistant.reasoning_delta", + "assistant.streaming_delta", + "assistant.tool_call_delta", + "tool.execution_partial_result", + "session.background_tasks_changed", + } +) + +# Copilot routes to several model families. The Openlayer cost service keys on +# the *real* provider, and Copilot's model ids match its slugs verbatim +# (verified: anthropic/claude-haiku-4.5 and openai/gpt-5.4 both resolve, while +# github/* returns "No cost data found"). Labelling these "github" would yield a +# silent $0, so an unrecognized prefix omits the provider entirely -- landing +# unpriced is recoverable, landing priced-wrong is not. +_PROVIDER_PREFIXES: Tuple[Tuple[str, str], ...] = ( + ("claude-", "anthropic"), + ("gpt-", "openai"), + ("o1", "openai"), + ("o3", "openai"), + ("o4", "openai"), + ("gemini-", "google"), + ("grok-", "xai"), +) + + +# --------------------------------------------------------------------------- # +# Event accessors (tolerant of dict / dataclass and snake_case / camelCase) +# --------------------------------------------------------------------------- # + + +def _camel(name: str) -> str: + parts = name.split("_") + return parts[0] + "".join(p.title() for p in parts[1:]) + + +def _event_type(event: Any) -> str: + """Return an event's ``type`` as a plain string. + + The live Python binding hands us ``SessionEventType.SESSION_START`` (an + Enum), not ``"session.start"`` -- so every comparison silently fails unless + we coerce. Recorded fixtures carry the serialized string form, which is why + this needs an explicit test rather than only fixture coverage. + """ + raw = event.get("type") if isinstance(event, dict) else getattr(event, "type", None) + if raw is None: + return "" + value = getattr(raw, "value", raw) + return value if isinstance(value, str) else str(value) + + +def _event_data(event: Any) -> Dict[str, Any]: + """Return an event's ``data`` payload as a plain dict.""" + if isinstance(event, dict): + data = event.get("data") + else: + data = getattr(event, "data", None) + if data is None: + return {} + if isinstance(data, dict): + return data + if hasattr(data, "__dict__"): + return dict(vars(data)) + return {} + + +def _field(data: Any, name: str) -> Any: + """Read ``name`` from a payload tolerating snake_case and camelCase. + + The Python binding yields ``tool_call_id``; the TypeScript binding (and the + fixtures captured from it) yields ``toolCallId``. Accepting both keeps a + single code path and makes the two SDK implementations mirror each other. + """ + if not isinstance(data, dict): + return None + if name in data: + return data[name] + return data.get(_camel(name)) + + +def _envelope(event: Any, name: str) -> Any: + """Read a top-level envelope field (``agent_id``, ``timestamp``, ...).""" + if isinstance(event, dict): + return _field(event, name) + value = getattr(event, name, None) + if value is None: + value = getattr(event, _camel(name), None) + return value + + +def _timestamp(event: Any) -> Optional[float]: + """Convert an ISO-8601 event timestamp to epoch seconds.""" + raw = _envelope(event, "timestamp") + if not raw: + return None + if isinstance(raw, (int, float)): + return float(raw) + try: + return datetime.fromisoformat(str(raw).replace("Z", "+00:00")).timestamp() + except (ValueError, TypeError): + return None + + +def _duration_seconds(value: Any) -> Optional[float]: + """Copilot durations arrive as timedelta (Python) or ms number (TS).""" + if value is None: + return None + if hasattr(value, "total_seconds"): + return float(value.total_seconds()) + try: + return float(value) / 1000.0 + except (TypeError, ValueError): + return None + + +def _jsonable(value: Any, _depth: int = 0) -> Any: + """Coerce SDK dataclasses and enums into JSON-serializable structures. + + Live events carry dataclasses (``ToolExecutionCompleteResult``) and enums + where the recorded fixtures carry plain dicts and strings. Anything we put + on a step has to survive JSON serialization at publish time. + """ + if value is None or isinstance(value, (str, int, float, bool)): + return value + if _depth > 6: # defensive: never recurse into a cyclic SDK object forever + return str(value) + if isinstance(value, dict): + return {str(k): _jsonable(v, _depth + 1) for k, v in value.items()} + if isinstance(value, (list, tuple, set, frozenset)): + return [_jsonable(v, _depth + 1) for v in value] + if isinstance(value, enum.Enum): + return _jsonable(value.value, _depth + 1) + # Also unwrap enum-like objects whose only public attribute is ``value``, + # so a binding that hands back a lookalike is not serialized as {"value": x}. + attrs = vars(value) if hasattr(value, "__dict__") else {} + public = {k for k in attrs if not k.startswith("_")} + if public == {"value"}: + return _jsonable(attrs["value"], _depth + 1) + if hasattr(value, "__dict__"): + return { + str(k): _jsonable(v, _depth + 1) + for k, v in vars(value).items() + if not k.startswith("__") + } + return str(value) + + +def _truncate(value: Any, max_chars: int) -> Any: + """Cap oversized tool payloads; results can contain whole files.""" + value = _jsonable(value) + if isinstance(value, str) and len(value) > max_chars: + return value[:max_chars] + "…" % (len(value) - max_chars) + return value + + +# --------------------------------------------------------------------------- # +# Mapping helpers +# --------------------------------------------------------------------------- # + + +def _provider_for_model(model: Optional[str]) -> Optional[str]: + """Map a Copilot model id to the real underlying provider slug.""" + if not model: + return None + lowered = str(model).lower() + for prefix, provider in _PROVIDER_PREFIXES: + if lowered.startswith(prefix): + return provider + logger.debug("Openlayer: no provider mapping for Copilot model %r", model) + return None + + +def _usage_details(usage: Dict[str, Any]) -> Dict[str, int]: + """Build the non-overlapping token partition the cost backend prices. + + Copilot reports ``input_tokens`` as a *superset* that already contains + ``cache_read_tokens`` and ``cache_write_tokens``. The backend sums a price + per recognized key, so the granular categories must be broken out and + subtracted from the base or cached tokens get charged twice. Reasoning stays + folded into ``output_tokens``, matching the convention already established + in ``langchain_callback._build_usage_details``. + """ + input_total = int(_field(usage, "input_tokens") or 0) + output_total = int(_field(usage, "output_tokens") or 0) + cache_read = int(_field(usage, "cache_read_tokens") or 0) + cache_write = int(_field(usage, "cache_write_tokens") or 0) + + details: Dict[str, int] = {} + for key, value in ( + ("input_tokens", input_total - cache_read - cache_write), + ("output_tokens", output_total), + ("cached_tokens", cache_read), + ("cache_creation_tokens", cache_write), + ): + if value and value > 0: + details[key] = value + return details + + +# GitHub meters every call in AIU ("AI Units") and reports the total on +# ``copilot_usage.total_nano_aiu``. Decoding the per-token rates GitHub ships +# alongside it (``copilot_usage._token_details``) for ``claude-haiku-4.5`` gives +# 100 / 500 / 10 / 125 AIU per 1M input / output / cache-read / cache-write +# tokens -- exactly Anthropic's published list prices scaled by 100. So one AIU +# is one US cent, and ``total_nano_aiu / 1e11`` reproduced the priced cost of an +# observed call to twelve decimal places ($0.01675375 both ways). +# +# Confirmed across two vendors: ``gpt-5-mini`` bills 25 / 200 / 2.5 AIU per 1M +# input / output / cache-read tokens, which is OpenAI's $0.25 / $2.00 / $0.025 +# list pricing under the same 1 AIU = $0.01 constant. Live rows carry both +# figures and they agree to twelve decimal places. +# +# Still not proven for *every* vendor Copilot may route to, so this is used as a +# *fallback* for models we cannot map to a provider, and as a cross-check +# alongside the priced cost -- never in place of Openlayer's own pricing when the +# provider is known. +_NANO_AIU_PER_USD = 1e11 + + +def _as_mapping(value: Any) -> Dict[str, Any]: + """Normalize a nested payload to a dict. + + Live events nest dataclasses (``copilot_usage`` is + ``AssistantUsageCopilotUsage``, not a dict) where the recorded fixtures nest + plain dicts -- so an ``isinstance(..., dict)`` guard silently skips real + data. Same trap as the enum-typed ``type`` field. + """ + if isinstance(value, dict): + return value + if value is not None and hasattr(value, "__dict__"): + return dict(vars(value)) + return {} + + +def _metered_cost_usd(usage: Dict[str, Any]) -> Optional[float]: + """GitHub's own metered value of a call, in USD, or None if unavailable.""" + copilot_usage = _as_mapping(_field(usage, "copilot_usage")) + nano_aiu = _field(copilot_usage, "total_nano_aiu") + if not nano_aiu: + return None + try: + return float(nano_aiu) / _NANO_AIU_PER_USD + except (TypeError, ValueError): + return None + + +# --------------------------------------------------------------------------- # +# Configuration and buffered records +# --------------------------------------------------------------------------- # + + +@dataclass +class CopilotConfig: + """Tunable per-integration configuration.""" + + inference_pipeline_id: Optional[str] = None + truncate_tool_output_chars: int = 8192 + capture_reasoning: bool = True + + +@dataclass +class _Interaction: + """One ``send()`` -- the unit that becomes a single Openlayer trace.""" + + interaction_id: str + user_prompt: str = "" + events: List[Any] = field(default_factory=list) + start_time: float = field(default_factory=time.time) + + +@dataclass +class _TurnRecord: + """One LLM call: an ``assistant.turn_start`` .. ``assistant.message`` cycle.""" + + agent_id: Optional[str] + turn_id: Any + model: Optional[str] = None + start_time: Optional[float] = None + end_time: Optional[float] = None + output: str = "" + reasoning: str = "" + usage: Dict[str, Any] = field(default_factory=dict) + api_call_id: Optional[str] = None + error: Optional[Dict[str, Any]] = None + parent_tool_call_id: Optional[str] = None + + +@dataclass +class _ToolRecord: + """One tool execution, or one subagent dispatch.""" + + tool_call_id: str + name: str = "tool" + arguments: Any = None + result: Any = None + error: Any = None + success: Optional[bool] = None + start_time: Optional[float] = None + end_time: Optional[float] = None + parent_tool_call_id: Optional[str] = None + agent_id: Optional[str] = None + mcp_server_name: Optional[str] = None + is_subagent: bool = False + subagent_name: Optional[str] = None + subagent_metadata: Dict[str, Any] = field(default_factory=dict) + permission: Optional[str] = None + + +# --------------------------------------------------------------------------- # +# Collector +# --------------------------------------------------------------------------- # + + +class CopilotTraceCollector: + """Buffers a Copilot session's events and builds traces when it goes idle. + + One instance per session. Wire it up via + ``client.create_session(on_event=collector.handle)``. + """ + + def __init__(self, config: Optional[CopilotConfig] = None) -> None: + self.config = config or CopilotConfig() + self.session_id: Optional[str] = None + self.session_model: Optional[str] = None + self.interactions: Dict[str, _Interaction] = {} + self.built_count = 0 + + # Correlation tables. Needed because assistant.turn_end and + # assistant.usage carry no interaction_id at all. + self._turn_to_interaction: Dict[Tuple[Any, Any], str] = {} + self._tool_to_interaction: Dict[str, str] = {} + self._api_call_to_interaction: Dict[str, str] = {} + self._agent_to_interaction: Dict[str, str] = {} + self._current_interaction_id: Optional[str] = None + + # ------------------------------ ingest ------------------------------ # + + def handle(self, event: Any) -> None: + """Entry point wired to ``create_session(on_event=...)``. + + Must never raise: it runs inside the SDK's own event dispatch, and + observability must not be able to break the customer's agent. + """ + try: + self._handle(event) + except Exception: # pragma: no cover - defensive + logger.debug("Openlayer: failed to handle Copilot event", exc_info=True) + + def flush(self) -> None: + """Build any still-open interactions (e.g. on disconnect).""" + try: + self._build_all() + except Exception: # pragma: no cover - defensive + logger.debug("Openlayer: failed to flush Copilot traces", exc_info=True) + + def _handle(self, event: Any) -> None: + event_type = _event_type(event) + if not event_type or event_type in _IGNORED_EVENT_TYPES: + return + + data = _event_data(event) + + if event_type == "session.start": + self.session_id = _field(data, "session_id") or self.session_id + return + if event_type == "session.model_change": + self.session_model = _field(data, "model") or self.session_model + return + if event_type in ("session.idle", "session.shutdown"): + # "The session went quiescent" -- not "this send finished". Two + # overlapping send() calls are serialized by Copilot and produce a + # single idle with both interactions open, so build them all. + self._build_all() + return + + agent_id = _envelope(event, "agent_id") + + if event_type == "user.message" and not agent_id: + # Root-agent user messages open an interaction. Subagents emit their + # own user.message; those belong to the dispatching interaction. + interaction_id = _field(data, "interaction_id") or "_auto_%d" % len(self.interactions) + interaction = _Interaction( + interaction_id=interaction_id, + user_prompt=_field(data, "content") or "", + start_time=_timestamp(event) or time.time(), + ) + self.interactions[interaction_id] = interaction + self._current_interaction_id = interaction_id + interaction.events.append(event) + return + + interaction_id = self._route(data, event) + if interaction_id is None: + return + interaction = self.interactions.get(interaction_id) + if interaction is None: + return + interaction.events.append(event) + self._remember_correlations(event_type, data, interaction_id, event) + + def _route(self, data: Dict[str, Any], event: Any) -> Optional[str]: + """Find the interaction an event belongs to. + + Order matters. ``assistant.usage`` carries no ``interaction_id`` and no + ``turn_id``, and its ``api_call_id`` is only learned from the + ``assistant.message`` that arrives *after* it -- so it must be routable + by ``parent_tool_call_id`` or ``agent_id`` before any fallback. + """ + explicit = _field(data, "interaction_id") + if explicit and explicit in self.interactions: + return explicit + for key, table in ( + ("tool_call_id", self._tool_to_interaction), + ("parent_tool_call_id", self._tool_to_interaction), + ("api_call_id", self._api_call_to_interaction), + ): + value = _field(data, key) + if value and value in table: + return table[value] + agent_id = _envelope(event, "agent_id") + if agent_id and agent_id in self._agent_to_interaction: + return self._agent_to_interaction[agent_id] + turn_key = (agent_id, _field(data, "turn_id")) + if turn_key in self._turn_to_interaction: + return self._turn_to_interaction[turn_key] + return self._current_interaction_id + + def _remember_correlations( + self, event_type: str, data: Dict[str, Any], interaction_id: str, event: Any + ) -> None: + tool_call_id = _field(data, "tool_call_id") + if tool_call_id: + self._tool_to_interaction[tool_call_id] = interaction_id + api_call_id = _field(data, "api_call_id") + if api_call_id: + self._api_call_to_interaction[api_call_id] = interaction_id + # subagent.started is the first event tying an agentId to an + # interaction, and it arrives before any of that subagent's own events. + if event_type == "subagent.started" and tool_call_id: + self._agent_to_interaction[tool_call_id] = interaction_id + if event_type == "assistant.turn_start": + agent_id = _envelope(event, "agent_id") + self._turn_to_interaction[(agent_id, _field(data, "turn_id"))] = interaction_id + + # ------------------------------ assembly ------------------------------ # + + def _build_all(self) -> None: + for interaction in list(self.interactions.values()): + if interaction.events: + self.build_interaction(interaction) + self.interactions.clear() + self._current_interaction_id = None + + def _assemble( + self, interaction: _Interaction + ) -> Tuple[Dict[Tuple[Any, Any], _TurnRecord], Dict[str, _ToolRecord], List[_TurnRecord]]: + """Fold buffered events into turn and tool records.""" + turns: Dict[Tuple[Any, Any], _TurnRecord] = {} + tools: Dict[str, _ToolRecord] = {} + order: List[_TurnRecord] = [] + # assistant.usage arrives *before* the assistant.message it belongs to + # and carries no turn_id, so park it by api_call_id until the message + # names the turn. Never join on adjacency. + pending_usage: Dict[str, Dict[str, Any]] = {} + + for event in interaction.events: + event_type = _event_type(event) + data = _event_data(event) + agent_id = _envelope(event, "agent_id") + ts = _timestamp(event) + + if event_type == "assistant.turn_start": + key = (agent_id, _field(data, "turn_id")) + turn = _TurnRecord( + agent_id=agent_id, + turn_id=_field(data, "turn_id"), + model=_field(data, "model"), + start_time=ts, + ) + turns[key] = turn + order.append(turn) + + elif event_type == "model.call_start": + key = (agent_id, _field(data, "turn_id")) + turn = turns.get(key) + if turn is not None: + turn.model = _field(data, "model") or turn.model + if ts: + turn.start_time = turn.start_time or ts + + elif event_type == "assistant.usage": + api_call_id = _field(data, "api_call_id") + if api_call_id: + pending_usage[api_call_id] = data + + elif event_type == "assistant.message": + key = (agent_id, _field(data, "turn_id")) + turn = turns.get(key) + if turn is None: + turn = _TurnRecord( + agent_id=agent_id, turn_id=_field(data, "turn_id"), start_time=ts + ) + turns[key] = turn + order.append(turn) + content = _field(data, "content") or "" + turn.output = (turn.output + content) if turn.output else content + turn.model = _field(data, "model") or turn.model + turn.end_time = ts or turn.end_time + turn.parent_tool_call_id = _field(data, "parent_tool_call_id") + api_call_id = _field(data, "api_call_id") + turn.api_call_id = api_call_id + reasoning = _field(data, "reasoning_text") + if reasoning: + turn.reasoning = reasoning + # Join usage to message on api_call_id -- verified 1:1 across + # every turn, including inside subagents. + if api_call_id and api_call_id in pending_usage: + turn.usage = pending_usage.pop(api_call_id) + + elif event_type == "assistant.reasoning": + key = (agent_id, _field(data, "turn_id")) + turn = turns.get(key) + text = _field(data, "text") or _field(data, "content") + if turn is not None and text and not turn.reasoning: + turn.reasoning = text + + elif event_type == "assistant.turn_end": + key = (agent_id, _field(data, "turn_id")) + turn = turns.get(key) + if turn is not None and ts: + turn.end_time = ts + + elif event_type == "model.call_failure": + key = (agent_id, _field(data, "turn_id")) + turn = turns.get(key) + if turn is not None: + turn.error = { + "message": _field(data, "error_message"), + "code": _field(data, "error_code"), + "type": _field(data, "error_type"), + "status_code": _field(data, "status_code"), + } + + elif event_type == "tool.execution_start": + tool_call_id = _field(data, "tool_call_id") + if not tool_call_id: + continue + tools[tool_call_id] = _ToolRecord( + tool_call_id=tool_call_id, + name=_field(data, "tool_name") or "tool", + arguments=_field(data, "arguments"), + start_time=ts, + parent_tool_call_id=_field(data, "parent_tool_call_id"), + agent_id=agent_id, + mcp_server_name=_field(data, "mcp_server_name"), + ) + + elif event_type == "tool.execution_complete": + tool_call_id = _field(data, "tool_call_id") + tool = tools.get(tool_call_id) if tool_call_id else None + if tool is None: + continue + tool.success = _field(data, "success") + tool.error = _field(data, "error") + tool.result = _field(data, "result") + tool.end_time = ts + + elif event_type == "subagent.started": + tool_call_id = _field(data, "tool_call_id") + tool = tools.get(tool_call_id) if tool_call_id else None + if tool is None: + continue + tool.is_subagent = True + tool.subagent_name = _field(data, "agent_display_name") or _field( + data, "agent_name" + ) + tool.subagent_metadata.update( + { + "agent_name": _field(data, "agent_name"), + "agent_description": _field(data, "agent_description"), + "model": _field(data, "model"), + } + ) + + elif event_type == "subagent.completed": + tool_call_id = _field(data, "tool_call_id") + tool = tools.get(tool_call_id) if tool_call_id else None + if tool is None: + continue + tool.subagent_metadata.update( + { + "total_tokens": _field(data, "total_tokens"), + "total_tool_calls": _field(data, "total_tool_calls"), + "duration_ms": _field(data, "duration_ms"), + "cancelled": _field(data, "cancelled"), + } + ) + + elif event_type == "permission.completed": + tool_call_id = _field(data, "tool_call_id") + tool = tools.get(tool_call_id) if tool_call_id else None + if tool is not None: + tool.permission = _field(data, "decision") or _field(data, "outcome") + + return turns, tools, order + + # ------------------------------ building ------------------------------ # + + def build_interaction(self, interaction: _Interaction) -> None: + """Build and publish one Openlayer trace from a buffered interaction.""" + turns, tools, order = self._assemble(interaction) + + # The root's output is the LAST root-agent assistant message. Openlayer + # builds a row's output from the root step, so an empty root silently + # yields an unusable row. + final_output = "" + for turn in reversed(order): + if turn.agent_id is None and turn.output: + final_output = turn.output + break + + # A record's parent is its parent_tool_call_id, else the tool call that + # dispatched its agent, else the root. Never parentId -- that is a + # chronological chain, not a tree. + children: Dict[Optional[str], List[Any]] = {} + + def parent_of(record: Any) -> Optional[str]: + explicit = getattr(record, "parent_tool_call_id", None) + if explicit: + return explicit + agent_id = getattr(record, "agent_id", None) + if agent_id and agent_id in tools: + return agent_id + return None + + for record in list(order) + list(tools.values()): + children.setdefault(parent_of(record), []).append(record) + for bucket in children.values(): + bucket.sort(key=lambda r: r.start_time or 0.0) + + end_times = [r.end_time for r in list(order) + list(tools.values()) if r.end_time] + root_end = max(end_times) if end_times else None + + with tracer.create_step( + name=ROOT_STEP_NAME, + step_type=enums.StepType.AGENT, + inputs={"prompt": interaction.user_prompt}, + output=final_output, + metadata={ + "copilot_session_id": self.session_id, + "copilot_interaction_id": interaction.interaction_id, + "model": self.session_model, + }, + inference_pipeline_id=self.config.inference_pipeline_id, + ) as root: + root.log(start_time=interaction.start_time) + if root_end: + root.log( + end_time=root_end, + latency=(root_end - interaction.start_time) * 1000.0, + ) + self._emit(children, None) + + self.built_count += 1 + + def _emit(self, children: Dict[Optional[str], List[Any]], parent_key: Optional[str]) -> None: + """Recursively open and close steps in correct nesting order. + + Because this pass is synchronous and properly nested, the tracer's step + stack yields the right tree -- the concurrency hazard was removed by + deferring construction until every event was in hand. + """ + for record in children.get(parent_key, []): + if isinstance(record, _TurnRecord): + self._emit_turn(record) + else: + self._emit_tool(record, children) + + def _emit_turn(self, turn: _TurnRecord) -> None: + usage = turn.usage or {} + metadata: Dict[str, Any] = {"turn_id": turn.turn_id} + if turn.agent_id: + metadata["agent_id"] = turn.agent_id + + premium = _field(usage, "cost") + if premium is not None: + # NOT dollars -- Copilot premium-request units, a flat per-model + # multiplier. Kept as metadata so the step's real cost is priced by + # Openlayer from provider+model instead. + metadata["copilot_premium_requests"] = premium + copilot_usage = _as_mapping(_field(usage, "copilot_usage")) + nano_aiu = _field(copilot_usage, "total_nano_aiu") + if nano_aiu is not None: + metadata["copilot_nano_aiu"] = nano_aiu + metered_cost = _metered_cost_usd(usage) + if metered_cost is not None: + # GitHub's own metered value of this call in USD. Recorded even when + # Openlayer prices the step, so the two figures can be compared. + metadata["copilot_metered_cost_usd"] = metered_cost + for key in ("finish_reason", "api_endpoint", "service_request_id", "initiator"): + value = _field(usage, key) + if value is not None: + metadata["copilot_" + key] = value + if turn.reasoning and self.config.capture_reasoning: + metadata["reasoning"] = turn.reasoning + if turn.error: + metadata["error"] = turn.error + + details = _usage_details(usage) + name = "turn %s" % turn.turn_id if turn.turn_id is not None else "assistant turn" + + with tracer.create_step( + name=name, + step_type=enums.StepType.CHAT_COMPLETION, + inputs={"prompt": []}, + output=turn.output, + metadata=_jsonable(metadata), + inference_pipeline_id=self.config.inference_pipeline_id, + ) as step: + provider = _provider_for_model(turn.model) + step.log( + model=turn.model, + provider=provider, + prompt_tokens=details.get("input_tokens"), + completion_tokens=details.get("output_tokens"), + tokens=sum(details.values()) or None, + usage_details=details or None, + ) + if provider is None and metered_cost is not None: + # Without a provider Openlayer cannot price the step, and an + # unpriced step reads as $0. GitHub's own metered figure is far + # better than nothing, so a model we have no mapping for still + # lands costed. + step.log(cost=metered_cost) + if turn.start_time: + step.log(start_time=turn.start_time) + end = turn.end_time or _duration_and_end(turn) + if end and turn.start_time: + step.log(end_time=end, latency=(end - turn.start_time) * 1000.0) + + def _emit_tool(self, tool: _ToolRecord, children: Dict[Optional[str], List[Any]]) -> None: + metadata: Dict[str, Any] = { + "tool_call_id": tool.tool_call_id, + "success": tool.success, + } + if tool.error: + metadata["error"] = tool.error + if tool.mcp_server_name: + metadata["mcp_server_name"] = tool.mcp_server_name + if tool.permission: + metadata["permission"] = tool.permission + + if tool.is_subagent: + step_type = enums.StepType.AGENT + name = "subagent: %s" % (tool.subagent_name or "unknown") + metadata.update(tool.subagent_metadata) + else: + step_type = enums.StepType.TOOL + name = tool.name + + output = _truncate(tool.result, self.config.truncate_tool_output_chars) + + with tracer.create_step( + name=name, + step_type=step_type, + inputs=_jsonable(tool.arguments), + output=output, + metadata=_jsonable(metadata), + inference_pipeline_id=self.config.inference_pipeline_id, + ) as step: + if tool.start_time: + step.log(start_time=tool.start_time) + if tool.end_time and tool.start_time: + step.log( + end_time=tool.end_time, + latency=(tool.end_time - tool.start_time) * 1000.0, + ) + self._emit(children, tool.tool_call_id) + + +def _duration_and_end(turn: _TurnRecord) -> Optional[float]: + """Derive a turn end time from usage duration when turn_end was missed.""" + duration = _duration_seconds(_field(turn.usage or {}, "duration")) + if duration and turn.start_time: + return turn.start_time + duration + return None + + +# --------------------------------------------------------------------------- # +# Public API +# --------------------------------------------------------------------------- # + + +_OPENLAYER_HANDLER_ATTR = "_openlayer_copilot_handler" + + +def _is_openlayer_handler(handler: Any) -> bool: + """True if ``handler`` was produced by :func:`openlayer_event_handler`.""" + return bool(getattr(handler, _OPENLAYER_HANDLER_ATTR, False)) + + +def _compose_handlers( + user_handler: Optional[Callable[[Any], None]], our_handler: Callable[[Any], None] +) -> Callable[[Any], None]: + """Run both handlers; neither may break the other.""" + + def composed(event: Any) -> None: + if user_handler is not None: + try: + user_handler(event) + except Exception: + logger.debug("Openlayer: user on_event handler raised", exc_info=True) + our_handler(event) + + return composed + + +def openlayer_event_handler(**config_kwargs: Any) -> Callable[[Any], None]: + """Return an ``on_event`` handler that traces one Copilot session. + + The returned callable exposes ``.flush()`` and ``.collector`` so callers who + build sessions themselves can publish a partial trace for a session that is + torn down without ever going idle. + + Example: + >>> from openlayer.lib.integrations.copilot_sdk import openlayer_event_handler + >>> handler = openlayer_event_handler() + >>> session = await client.create_session(on_event=handler) + >>> ... + >>> handler.flush() # optional: only needed on an abnormal teardown + """ + collector = CopilotTraceCollector(CopilotConfig(**config_kwargs)) + + # A closure rather than ``collector.handle`` directly: bound methods do not + # accept attribute assignment, and we want ``.flush`` / ``.collector`` on + # the returned callable while keeping it a plain function that can be passed + # straight to ``on_event``. + def handler(event: Any) -> None: + collector.handle(event) + + handler.flush = collector.flush # type: ignore[attr-defined] + handler.collector = collector # type: ignore[attr-defined] + setattr(handler, _OPENLAYER_HANDLER_ATTR, True) + return handler + + +def _wrap_disconnect(session: Any, collector: CopilotTraceCollector) -> None: + """Flush any still-open interaction when the session is disconnected. + + ``session.idle`` is the normal trigger, but a session torn down mid-flight + never emits it, and the buffered work would otherwise be dropped silently. + Flushing is idempotent: ``_build_all`` clears the buffer, so a disconnect + after a normal idle publishes nothing extra. + """ + original_disconnect = getattr(session, "disconnect", None) + if original_disconnect is None: + return + + async def patched_disconnect(*args: Any, **kwargs: Any) -> Any: + try: + collector.flush() + except Exception: # pragma: no cover - defensive + logger.debug("Openlayer: failed to flush on disconnect", exc_info=True) + return await original_disconnect(*args, **kwargs) + + try: + session.disconnect = patched_disconnect + except (AttributeError, TypeError): # pragma: no cover - frozen session object + logger.debug("Openlayer: could not wrap session.disconnect", exc_info=True) + + +_PATCHED = False +_ORIGINAL_CREATE_SESSION: Optional[Callable[..., Any]] = None + + +def trace_copilot(**config_kwargs: Any) -> None: + """Enable Openlayer tracing for every GitHub Copilot SDK session. + + Monkey-patches ``copilot.CopilotClient.create_session`` so each session gets + an Openlayer event handler, composed with any ``on_event`` the caller + supplies. Idempotent. + + Requirements: + ``github-copilot-sdk>=1.0.11`` must be installed: + ``pip install 'github-copilot-sdk>=1.0.11'`` + + Args: + inference_pipeline_id: Optional Openlayer inference pipeline ID. Falls + back to the ``OPENLAYER_INFERENCE_PIPELINE_ID`` env var. + truncate_tool_output_chars: Max characters of tool output per TOOL step. + Defaults to 8192. + capture_reasoning: Whether to capture reasoning text into chat-step + metadata. Defaults to True. + + Example: + >>> from openlayer.lib import trace_copilot + >>> trace_copilot() + >>> client = CopilotClient() + >>> session = await client.create_session() + >>> await session.send_and_wait("Summarize this repo") + """ + global _PATCHED, _ORIGINAL_CREATE_SESSION + if _PATCHED: + return + try: + import copilot + except ImportError as exc: + raise ImportError( + "github-copilot-sdk is required for Copilot SDK tracing. " + "Install with: pip install 'github-copilot-sdk>=1.0.11'" + ) from exc + + _ORIGINAL_CREATE_SESSION = copilot.CopilotClient.create_session + original = _ORIGINAL_CREATE_SESSION + + async def patched_create_session(self: Any, **kwargs: Any) -> Any: + existing = kwargs.get("on_event") + if _is_openlayer_handler(existing): + # The caller already passed an ``openlayer_event_handler()``. Adding + # a second collector would build the same interaction twice and + # publish duplicate rows -- and mixing the two entry points is easy + # to do by following the quickstart and then the "trace specific + # sessions" snippet. Defer to theirs. + session = await original(self, **kwargs) + _wrap_disconnect(session, existing.collector) + return session + handler = openlayer_event_handler(**config_kwargs) + kwargs["on_event"] = _compose_handlers(existing, handler) + session = await original(self, **kwargs) + _wrap_disconnect(session, handler.collector) # type: ignore[attr-defined] + return session + + copilot.CopilotClient.create_session = patched_create_session + _PATCHED = True + + +def untrace_copilot() -> None: + """Undo :func:`trace_copilot`. Primarily for tests.""" + global _PATCHED, _ORIGINAL_CREATE_SESSION + if not _PATCHED or _ORIGINAL_CREATE_SESSION is None: + return + import copilot + + copilot.CopilotClient.create_session = _ORIGINAL_CREATE_SESSION + _ORIGINAL_CREATE_SESSION = None + _PATCHED = False diff --git a/src/openlayer/lib/tracing/tracer.py b/src/openlayer/lib/tracing/tracer.py index 4f89b3a8..f8da604f 100644 --- a/src/openlayer/lib/tracing/tracer.py +++ b/src/openlayer/lib/tracing/tracer.py @@ -223,7 +223,8 @@ def init( logged) instead of accumulating in memory. Defaults to 100. auto_instrument: When truthy (default ``True``), detects every installed supported LLM SDK (openai, anthropic, mistral, groq, gemini, oci, - azure_content_understanding, litellm, portkey, google_adk) and patches + azure_content_understanding, litellm, portkey, google_adk, copilot) + and patches it so newly-constructed clients are auto-traced. Pass ``False`` to skip patching, or a list of names (e.g. ``["openai", "anthropic"]``) to patch only that subset. This is a procedural argument — it is NOT diff --git a/tests/integrations/fixtures/copilot_sdk/session_basic.jsonl b/tests/integrations/fixtures/copilot_sdk/session_basic.jsonl new file mode 100644 index 00000000..9d555940 --- /dev/null +++ b/tests/integrations/fixtures/copilot_sdk/session_basic.jsonl @@ -0,0 +1,55 @@ +{"agent_id": null, "data": {"already_in_use": false, "context": {"base_commit": null, "branch": null, "cwd": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws", "git_root": null, "head_commit": null, "host_type": null, "pending_git_context": null, "repository": null, "repository_host": null}, "context_tier": null, "copilot_version": "1.0.79", "detached_from_spawning_parent_session_id": null, "github_mcp_tool_config": null, "producer": "copilot-agent", "reasoning_effort": null, "reasoning_summary": null, "remote_steerable": false, "selected_model": null, "session_id": "60c4e8f8-b3c1-4d59-a721-f79b63af74f7", "session_limits": null, "start_time": "2026-08-27 12:18:55.025000+00:00", "verbosity": null, "version": 1}, "ephemeral": null, "id": "7f0d0183-cb64-4e04-a5cf-06825c1821e7", "parent_id": null, "raw_type": null, "timestamp": "2026-08-27 12:18:55.041000+00:00", "type": "session.start"} +{"agent_id": null, "data": {"bypass_permissions_disabled": false, "client_managed": false, "device_managed": false, "fail_closed": false, "managed_keys": [], "permissions_allow_intersected": null, "server_managed": false, "settings": null, "source": "none"}, "ephemeral": true, "id": "04172e2c-b1fb-4c02-9b91-2c2fa44aee1d", "parent_id": "7f0d0183-cb64-4e04-a5cf-06825c1821e7", "raw_type": null, "timestamp": "2026-08-27 12:18:55.050000+00:00", "type": "session.managed_settings_resolved"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "8c61a2da-6b83-4d11-9d9b-c28c497c27de", "parent_id": "7f0d0183-cb64-4e04-a5cf-06825c1821e7", "raw_type": null, "timestamp": "2026-08-27 12:18:55.053000+00:00", "type": "pending_messages.modified"} +{"agent_id": null, "data": {"cause": null, "context_tier": null, "new_model": "auto", "previous_model": null, "previous_reasoning_effort": null, "previous_reasoning_summary": null, "previous_verbosity": null, "reasoning_effort": null, "reasoning_summary": null, "verbosity": null}, "ephemeral": null, "id": "80d5f4be-5bf8-401d-ad11-ba163862a9f3", "parent_id": "7f0d0183-cb64-4e04-a5cf-06825c1821e7", "raw_type": null, "timestamp": "2026-08-27 12:18:56.049000+00:00", "type": "session.model_change"} +{"agent_id": null, "data": {"skills": [{"argument_hint": null, "command_name": "customize-cloud-agent", "description": "Skill for customizing the Copilot cloud agent (formerly known as Copilot coding agent) environment, including copilot-setup-steps.yml configuration, preinstalling tools and dependencies, runners, and settings. Use when the user mentions copilot-setup-steps, copilot setup steps, or wants to configure the cloud agent environment.", "enabled": true, "name": "customize-cloud-agent", "path": "/Users/vini/Library/Caches/copilot/pkg/darwin-arm64/1.0.79/builtin/customize-cloud-agent/SKILL.md", "source": "builtin", "user_invocable": false}, {"argument_hint": null, "command_name": "github-pr-media", "description": "Upload an image or video to GitHub's user attachments API and embed it in a pull request description or comment. Use when asked to add screenshots, diagrams, recordings, or other media to a PR or GitHub comment.", "enabled": true, "name": "github-pr-media", "path": "/Users/vini/Library/Caches/copilot/pkg/darwin-arm64/1.0.79/builtin/github-pr-media/SKILL.md", "source": "builtin", "user_invocable": false}]}, "ephemeral": true, "id": "12634645-3331-439d-a5dc-107b89087947", "parent_id": "80d5f4be-5bf8-401d-ad11-ba163862a9f3", "raw_type": null, "timestamp": "2026-08-27 12:18:56.223000+00:00", "type": "session.skills_loaded"} +{"agent_id": null, "data": {"available_models": ["claude-haiku-4.5", "gpt-5-mini"], "candidate_models": ["claude-haiku-4.5", "gpt-5-mini"], "category_scores": {"code_gen": 0.0097, "debugging": 0.0015, "reasoning": 0.0321, "tool_use": 0.0139}, "chosen_model": "claude-haiku-4.5", "chosen_shortfall": 0.0, "confidence": 0.97, "end_to_end_latency_ms": 227.0, "fallback": false, "fallback_reason": null, "has_image": false, "predicted_label": "no_reasoning", "reasoning_bucket": "low", "router_latency_ms": 63.0, "routing_method": "hydra", "sticky_override": false}, "ephemeral": null, "id": "4cdcda66-ef59-497d-b21d-c2e6eabcc763", "parent_id": "80d5f4be-5bf8-401d-ad11-ba163862a9f3", "raw_type": null, "timestamp": "2026-08-27 12:18:56.448000+00:00", "type": "session.auto_mode_resolved"} +{"agent_id": null, "data": {"content": "You are the GitHub Copilot CLI, a terminal assistant built by GitHub. You are an interactive CLI tool that helps users with software engineering tasks.\n\n# Tone and style\n* When providing output or exp\u2026", "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "metadata": null, "name": null, "role": "system"}, "ephemeral": null, "id": "de620d97-2c17-41a7-9f02-5f0f9ff4fc6c", "parent_id": "4cdcda66-ef59-497d-b21d-c2e6eabcc763", "raw_type": null, "timestamp": "2026-08-27 12:18:56.461000+00:00", "type": "system.message"} +{"agent_id": null, "data": {"model": "claude-haiku-4.5"}, "ephemeral": true, "id": "29fbf204-9a82-4cde-95ba-f6a093b5bccb", "parent_id": "de620d97-2c17-41a7-9f02-5f0f9ff4fc6c", "raw_type": null, "timestamp": "2026-08-27 12:18:56.462000+00:00", "type": "session.tools_updated"} +{"agent_id": null, "data": {"agent_mode": null, "attachments": [], "content": "List the files in this directory, then read the largest one and tell me in one sentence what it contains.", "delivery": "idle", "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "is_autopilot_continuation": null, "native_document_path_fallback_paths": null, "parent_agent_task_id": "9fd8b79d-f716-47e8-a3aa-5646c90fdc4c", "source": null, "supported_native_document_mime_types": [], "transformed_content": "2026-08-27T09:18:56.465-03:00\n\nList the files in this directory, then read the largest one and tell me in one sentence what it contains.\n\n\nAvailable tables: todos, todo_deps\n"}, "ephemeral": null, "id": "d053fd78-7d16-48b4-bc8a-9ebebc9ce4cd", "parent_id": "de620d97-2c17-41a7-9f02-5f0f9ff4fc6c", "raw_type": null, "timestamp": "2026-08-27 12:18:56.465000+00:00", "type": "user.message"} +{"agent_id": null, "data": {"title": "List the files in this directory, then read the largest one and tell me in one sentence what it contains."}, "ephemeral": true, "id": "14cc91e4-500c-4296-9c04-ac52d86a5d67", "parent_id": "d053fd78-7d16-48b4-bc8a-9ebebc9ce4cd", "raw_type": null, "timestamp": "2026-08-27 12:18:56.468000+00:00", "type": "session.title_changed"} +{"agent_id": null, "data": {"interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "model": null, "turn_id": "0"}, "ephemeral": null, "id": "07045487-b0d9-40dd-be37-e592ac006435", "parent_id": "d053fd78-7d16-48b4-bc8a-9ebebc9ce4cd", "raw_type": null, "timestamp": "2026-08-27 12:18:56.483000+00:00", "type": "assistant.turn_start"} +{"agent_id": null, "data": {"conversation_tokens": 78, "current_tokens": 10491, "is_initial": true, "messages_length": 3, "system_tokens": 5225, "token_limit": 128000, "tool_definitions_tokens": 5188}, "ephemeral": true, "id": "2d910e31-f4e7-461a-be2d-f368eea112a3", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:56.484000+00:00", "type": "session.usage_info"} +{"agent_id": null, "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "0"}, "ephemeral": true, "id": "5c8af4e3-1552-41d0-a8e4-4c6052e9db20", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:56.487000+00:00", "type": "model.call_start"} +{"agent_id": null, "data": {"total_response_size_bytes": 14}, "ephemeral": true, "id": "667079c4-ef38-4c9d-92e1-79a1de3904c6", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:57.666000+00:00", "type": "assistant.streaming_delta"} +{"agent_id": null, "data": {"delta_content": "The user wants", "reasoning_id": "c920d2ed-6265-4189-933e-81c4ca76af3b"}, "ephemeral": true, "id": "5044512f-64aa-448d-a974-19a268236a82", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:57.667000+00:00", "type": "assistant.reasoning_delta"} +{"agent_id": null, "data": {"total_response_size_bytes": 158}, "ephemeral": true, "id": "868b0080-eb16-487e-b02c-2562b186d7a1", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:58.277000+00:00", "type": "assistant.streaming_delta"} +{"agent_id": null, "data": {"delta_content": " me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start", "reasoning_id": "c920d2ed-6265-4189-933e-81c4ca76af3b"}, "ephemeral": true, "id": "59af88fb-fc51-4857-ab55-b3b12a45948e", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:58.277000+00:00", "type": "assistant.reasoning_delta"} +{"agent_id": null, "data": {"total_response_size_bytes": 275}, "ephemeral": true, "id": "0cfb2d6f-b4d6-4976-b06f-7acee2b1584c", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:58.278000+00:00", "type": "assistant.streaming_delta"} +{"agent_id": null, "data": {"delta_content": " by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501", "reasoning_id": "c920d2ed-6265-4189-933e-81c4ca76af3b"}, "ephemeral": true, "id": "120514e1-584e-4e3a-bc5a-21d0e0605152", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:18:58.278000+00:00", "type": "assistant.reasoning_delta"} +{"agent_id": null, "data": {"input_delta": "{\"co", "tool_call_id": "toolu_01FU6XYF6G4memQfhA8qW55F", "tool_name": "bash", "tool_type": null}, "ephemeral": true, "id": "bf28992f-626f-4da9-8f17-8ae1a6330920", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:19:00.114000+00:00", "type": "assistant.tool_call_delta"} +{"agent_id": null, "data": {"input_delta": "mmand\":", "tool_call_id": "toolu_01FU6XYF6G4memQfhA8qW55F", "tool_name": "bash", "tool_type": null}, "ephemeral": true, "id": "36af4f68-9405-4052-81ac-0bb932069273", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:19:00.114000+00:00", "type": "assistant.tool_call_delta"} +{"agent_id": null, "data": {"input_delta": " \"ls ", "tool_call_id": "toolu_01FU6XYF6G4memQfhA8qW55F", "tool_name": "bash", "tool_type": null}, "ephemeral": true, "id": "0bf872b7-3227-4cec-bbf7-949535ff84a7", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:19:00.115000+00:00", "type": "assistant.tool_call_delta"} +{"agent_id": null, "data": {"_available_tool_count": 16, "_num_tool_calls": 1, "_tool_counts": {"bash": 1}, "_tool_token_count": 5188, "api_call_id": "msg_011CeTEvNdFGjEND3RsV9GSv", "api_endpoint": "/v1/messages", "cache_expires_at": "2026-08-27 12:23:56.500000+00:00", "cache_read_tokens": 0, "cache_write_tokens": 12499, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 10, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 0, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 12499, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 224, "token_type": "output"}], "total_nano_aiu": 1675375000.0}, "cost": 0.33, "duration": "0:00:03.625000", "finish_reason": "tool_calls", "initiator": "user", "input_tokens": 12509, "inter_token_latency": "0:00:00.128785", "interaction_type": "conversation-agent", "model": "claude-haiku-4.5", "output_tokens": 224, "parent_tool_call_id": null, "provider_call_id": "29E8:3AC232:11AD842:1495100:6A902B30", "reasoning_effort": null, "reasoning_tokens": 143, "rte": true, "service_request_id": "e0bb44ce-ebc4-4841-8abb-35f4cf782ec4", "time_to_first_token": "0:00:01.178403"}, "ephemeral": true, "id": "eacb7e74-71cc-476c-8889-3a8f27b2aa6c", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:19:00.126000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {"api_call_id": "msg_011CeTEvNdFGjEND3RsV9GSv", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-7dc0aaea-2628-405f-b886-42f9208d7a13", "content": "", "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "message_id": "a656af25-8d8c-4a46-9010-f7cf47557ee4", "model": "claude-haiku-4.5", "output_tokens": 224, "parent_tool_call_id": null, "phase": null, "reasoning_text": "The user wants me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws\n\nI'll use bash to list files and find the largest one.", "request_id": "29E8:3AC232:11AD842:1495100:6A902B30", "rte": true, "server_tools": null, "service_request_id": "e0bb44ce-ebc4-4841-8abb-35f4cf782ec4", "tool_requests": [{"arguments": {"command": "ls -lhS", "description": "List files by size (largest first)"}, "intention_summary": "List files by size (largest first)", "mcp_server_name": null, "mcp_tool_name": null, "name": "bash", "tool_call_id": "toolu_01FU6XYF6G4memQfhA8qW55F", "tool_title": null, "type": "function"}], "turn_id": "0"}, "ephemeral": null, "id": "53ab4340-b370-4b15-86c5-86b97c512aea", "parent_id": "07045487-b0d9-40dd-be37-e592ac006435", "raw_type": null, "timestamp": "2026-08-27 12:19:00.129000+00:00", "type": "assistant.message"} +{"agent_id": null, "data": {"content": "The user wants me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws\n\nI'll use bash to list files and find the largest one.", "reasoning_id": "c920d2ed-6265-4189-933e-81c4ca76af3b", "rte": true}, "ephemeral": true, "id": "72a8546c-0c0c-434a-8993-f7eac249265e", "parent_id": "53ab4340-b370-4b15-86c5-86b97c512aea", "raw_type": null, "timestamp": "2026-08-27 12:19:00.130000+00:00", "type": "assistant.reasoning"} +{"agent_id": null, "data": {"arguments": {"command": "ls -lhS", "description": "List files by size (largest first)"}, "display_verbatim": null, "mcp_server_name": null, "mcp_tool_name": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "rte": true, "shell_tool_info": {"display_command": null, "has_write_file_redirection": false, "possible_paths": []}, "tool_call_id": "toolu_01FU6XYF6G4memQfhA8qW55F", "tool_description": null, "tool_name": "bash", "turn_id": "0"}, "ephemeral": null, "id": "70ae0565-704b-46b7-a1e0-1cc5b397f8ad", "parent_id": "53ab4340-b370-4b15-86c5-86b97c512aea", "raw_type": null, "timestamp": "2026-08-27 12:19:00.131000+00:00", "type": "tool.execution_start"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "4301bc46-3ca7-44ac-9704-3b730a7ca5f1", "parent_id": "70ae0565-704b-46b7-a1e0-1cc5b397f8ad", "raw_type": null, "timestamp": "2026-08-27 12:19:00.138000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "0d641c48-0fbc-484e-babb-f337b805a89b", "parent_id": "70ae0565-704b-46b7-a1e0-1cc5b397f8ad", "raw_type": null, "timestamp": "2026-08-27 12:19:00.138000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "45962801-ce47-4add-9529-0cc8467e3d05", "parent_id": "70ae0565-704b-46b7-a1e0-1cc5b397f8ad", "raw_type": null, "timestamp": "2026-08-27 12:19:00.142000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {"error": {"code": "denied", "message": "Permission denied and could not request permission from user"}, "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "is_user_requested": null, "mcp_meta": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "result": null, "rte": true, "sandboxed": null, "success": false, "tool_call_id": "toolu_01FU6XYF6G4memQfhA8qW55F", "tool_description": null, "tool_telemetry": {"properties": {"shell_error_category": "permission_denied"}}, "turn_id": "0"}, "ephemeral": null, "id": "cf384e22-5c36-4fe5-9f9e-e171886a82b2", "parent_id": "70ae0565-704b-46b7-a1e0-1cc5b397f8ad", "raw_type": null, "timestamp": "2026-08-27 12:19:00.146000+00:00", "type": "tool.execution_complete"} +{"agent_id": null, "data": {"model": null, "turn_id": "0"}, "ephemeral": null, "id": "aedc2996-01a8-4395-9c3c-86c1b7116bf2", "parent_id": "cf384e22-5c36-4fe5-9f9e-e171886a82b2", "raw_type": null, "timestamp": "2026-08-27 12:19:00.167000+00:00", "type": "assistant.turn_end"} +{"agent_id": null, "data": {"interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "model": null, "turn_id": "1"}, "ephemeral": null, "id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "parent_id": "aedc2996-01a8-4395-9c3c-86c1b7116bf2", "raw_type": null, "timestamp": "2026-08-27 12:19:00.169000+00:00", "type": "assistant.turn_start"} +{"agent_id": null, "data": {"conversation_tokens": 336, "current_tokens": 10749, "is_initial": false, "messages_length": 5, "system_tokens": 5225, "token_limit": 128000, "tool_definitions_tokens": 5188}, "ephemeral": true, "id": "c5c9c65a-a07f-451d-ae58-afaca4b0aa43", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:00.172000+00:00", "type": "session.usage_info"} +{"agent_id": null, "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "1"}, "ephemeral": true, "id": "2a94a917-907b-4bcb-9b66-379a4594dea1", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:00.175000+00:00", "type": "model.call_start"} +{"agent_id": null, "data": {"message_id": "372bf886-57c5-47a1-b8ba-6f91d49f63a0", "phase": null}, "ephemeral": true, "id": "bfcafdf4-e10d-43f7-b5a3-1507f9424f19", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:01.306000+00:00", "type": "assistant.message_start"} +{"agent_id": null, "data": {"delta_content": "Let", "message_id": "372bf886-57c5-47a1-b8ba-6f91d49f63a0", "parent_tool_call_id": null}, "ephemeral": true, "id": "e21dd773-eeb0-4ed3-ac1d-35c1d001b4c1", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:01.306000+00:00", "type": "assistant.message_delta"} +{"agent_id": null, "data": {"delta_content": " me try using the", "message_id": "372bf886-57c5-47a1-b8ba-6f91d49f63a0", "parent_tool_call_id": null}, "ephemeral": true, "id": "0ebd151e-b40c-4cc2-94ad-d4ff41fff077", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:01.307000+00:00", "type": "assistant.message_delta"} +{"agent_id": null, "data": {"delta_content": " view", "message_id": "372bf886-57c5-47a1-b8ba-6f91d49f63a0", "parent_tool_call_id": null}, "ephemeral": true, "id": "01120c1a-b10d-4ca7-bc71-0eaafc527bfc", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:01.307000+00:00", "type": "assistant.message_delta"} +{"agent_id": null, "data": {"_available_tool_count": 16, "_num_tool_calls": 1, "_tool_counts": {"view": 1}, "_tool_token_count": 5188, "api_call_id": "msg_011CeTEve3hKaz8wn5VCvcKi", "api_endpoint": "/v1/messages", "cache_expires_at": "2026-08-27 12:24:00.185000+00:00", "cache_read_tokens": 12499, "cache_write_tokens": 249, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 6, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 12499, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 249, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 122, "token_type": "output"}], "total_nano_aiu": 217715000.0}, "cost": 0.33, "duration": "0:00:01.944000", "finish_reason": "tool_calls", "initiator": "agent", "input_tokens": 12754, "inter_token_latency": "0:00:00.025412", "interaction_type": "conversation-agent", "model": "claude-haiku-4.5", "output_tokens": 122, "parent_tool_call_id": null, "provider_call_id": "29E8:3AC232:11AE5F5:14960C9:6A902B34", "reasoning_effort": null, "reasoning_tokens": 0, "rte": true, "service_request_id": "e2ff792e-9b20-43ed-a6c5-37f901ed10b5", "time_to_first_token": "0:00:01.131452"}, "ephemeral": true, "id": "330e53a0-f3c6-48c2-ae2d-1497b43c02a2", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:02.129000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {"api_call_id": "msg_011CeTEve3hKaz8wn5VCvcKi", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-e35fd329-4333-4b36-8264-b70c3ad1735a", "content": "Let me try using the view tool instead:", "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "message_id": "372bf886-57c5-47a1-b8ba-6f91d49f63a0", "model": "claude-haiku-4.5", "output_tokens": 122, "parent_tool_call_id": null, "phase": null, "reasoning_text": "The user wants me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws\n\nI'll use bash to list files and find the largest one.", "request_id": "29E8:3AC232:11AE5F5:14960C9:6A902B34", "rte": true, "server_tools": null, "service_request_id": "e2ff792e-9b20-43ed-a6c5-37f901ed10b5", "tool_requests": [{"arguments": {"path": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws"}, "intention_summary": "view the file at /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws.", "mcp_server_name": null, "mcp_tool_name": null, "name": "view", "tool_call_id": "toolu_01XGaT85UjmjwdjBvSUyq4pR", "tool_title": null, "type": "function"}], "turn_id": "1"}, "ephemeral": null, "id": "5926bc39-1a73-4aab-82fd-ab266b057180", "parent_id": "d024549c-38c3-4b31-bdd8-ab445fd808c6", "raw_type": null, "timestamp": "2026-08-27 12:19:02.132000+00:00", "type": "assistant.message"} +{"agent_id": null, "data": {"content": "The user wants me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws\n\nI'll use bash to list files and find the largest one.", "reasoning_id": "EtMFCrIBCBEYAipAJxE97+DjjDVs+bGpHa8dQ5VSPOiiCUTaNEFSDl8DtAClc+NOxKQ2705W5y9y/9pgDBuTFT+O+zKldneiyuvr+zIZY2xhdWRlLWhhaWt1LTQtNS0yMDI1MTAwMTgAQgh0aGlua2luZ1okY2VkMWY5OGUtMGI1MS00NjEwLTgyODItNGU1ZDg4Mzg0NTc0chAFJi2jM86AcGV3hjaN37r2iAEBqAG01sDUBrABAhIMF1ZrovH4Ic5Eh0jCGgzf7CUQ+Pi+9vSJjCsiMOY64NBjwtdHREJ9AdnWhwd6RbBs3vPGdFR5EGslesoW08mOBzMZI9fBkyq8FK+OiyrNAzbk7XOz6lHlIXhW6mmQFyn5JXZFsrHb0f+WT11juJg5auJiWXR9CnNg22Jnbx4xPs1tM4IRdAQd241Nl5wiVV46Q81QYQb3T03bpDuKyMLXWNx/vjnSQ0sPhxIfupFIzXmTUv/dr8LLdYnjEnPWCWS55KaGWPYz08IIS/FYG2fH3tYuXURI8JmivWNe/Uq63H3lMPg/Qp2zVNrkSWUhx+5/RRZe5jWLRL3UekQ6AdnZaIBeq7S50ZGILZykh2Qm8DobJiEBWFdnmojzOPZ7l4bvSkPCIRUsicuPT+vw9//aQfjO7ssjuC8ADlf2libVRA3UN8FHb1W8o9nr8MOidj+11EW5AXMCcwjqUtnqdSgNSLNAicQVBhr4uZsgt6NtUSg6mPuFvxnD5DT3I0FA0SWdxbOs6wbbItKuYTqqb689h+f3g09qBGIqlczX24Wdg74vkB4MahAeicJsZu/OniDGHT+1HxOlOIiZhfNNUPgHeRiQ4ROXv0ENraz75a3O94NFJFR9pk5FEySMdz2OtX5xKe7EU/PwXRze9NFsCwEvNSEXNMi9UVGvQpHB+ADcb3xSFFSSsMwpOTbbGDbDWwCG3BeC6Zqa3XVL9l9jGAE=", "rte": true}, "ephemeral": true, "id": "ac86007d-5d99-4acb-9816-bd8f0fdd9a15", "parent_id": "5926bc39-1a73-4aab-82fd-ab266b057180", "raw_type": null, "timestamp": "2026-08-27 12:19:02.132000+00:00", "type": "assistant.reasoning"} +{"agent_id": null, "data": {"arguments": {"path": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws"}, "display_verbatim": null, "mcp_server_name": null, "mcp_tool_name": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "rte": true, "shell_tool_info": null, "tool_call_id": "toolu_01XGaT85UjmjwdjBvSUyq4pR", "tool_description": null, "tool_name": "view", "turn_id": "1"}, "ephemeral": null, "id": "3ff7e21d-9a6b-43cd-98e3-a51031a7286e", "parent_id": "5926bc39-1a73-4aab-82fd-ab266b057180", "raw_type": null, "timestamp": "2026-08-27 12:19:02.133000+00:00", "type": "tool.execution_start"} +{"agent_id": null, "data": {"error": {"code": "denied", "message": "Permission denied and could not request permission from user"}, "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "is_user_requested": null, "mcp_meta": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "result": null, "rte": true, "sandboxed": null, "success": false, "tool_call_id": "toolu_01XGaT85UjmjwdjBvSUyq4pR", "tool_description": null, "tool_telemetry": {"metrics": {"responseTokenLimit": 32000, "resultForLlmLength": 60, "resultLength": 60}, "properties": {"command": "view", "fileExtension": "[\"directory\"]", "inputs": "[\"path\",\"command\"]", "options": "{\"truncateBasedOn\":\"tokenCount\",\"truncateStyle\":\"middle\"}", "resolvedPathAgainstCwd": "false"}, "restrictedProperties": {}}, "turn_id": "1"}, "ephemeral": null, "id": "4d9bffcb-4a6f-4383-b89c-ddd17ee66a46", "parent_id": "3ff7e21d-9a6b-43cd-98e3-a51031a7286e", "raw_type": null, "timestamp": "2026-08-27 12:19:02.137000+00:00", "type": "tool.execution_complete"} +{"agent_id": null, "data": {"model": null, "turn_id": "1"}, "ephemeral": null, "id": "8e58e1f7-756e-4f56-aabb-f419216e0d52", "parent_id": "4d9bffcb-4a6f-4383-b89c-ddd17ee66a46", "raw_type": null, "timestamp": "2026-08-27 12:19:02.147000+00:00", "type": "assistant.turn_end"} +{"agent_id": null, "data": {"interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "model": null, "turn_id": "2"}, "ephemeral": null, "id": "373d63a9-96ad-42c3-af02-01f2b530142a", "parent_id": "8e58e1f7-756e-4f56-aabb-f419216e0d52", "raw_type": null, "timestamp": "2026-08-27 12:19:02.148000+00:00", "type": "assistant.turn_start"} +{"agent_id": null, "data": {"conversation_tokens": 493, "current_tokens": 10906, "is_initial": false, "messages_length": 7, "system_tokens": 5225, "token_limit": 128000, "tool_definitions_tokens": 5188}, "ephemeral": true, "id": "9aec684c-5356-4035-b8ec-8b9bf2d0821d", "parent_id": "373d63a9-96ad-42c3-af02-01f2b530142a", "raw_type": null, "timestamp": "2026-08-27 12:19:02.148000+00:00", "type": "session.usage_info"} +{"agent_id": null, "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "2"}, "ephemeral": true, "id": "04ae2075-c781-4014-a8d9-c625c263b00e", "parent_id": "373d63a9-96ad-42c3-af02-01f2b530142a", "raw_type": null, "timestamp": "2026-08-27 12:19:02.151000+00:00", "type": "model.call_start"} +{"agent_id": null, "data": {"message_id": "1cfb775b-75bd-4569-8f3f-944d5d64bb0f", "phase": null}, "ephemeral": true, "id": "7cc30638-b0e4-4843-805e-e4648b27cb66", "parent_id": "373d63a9-96ad-42c3-af02-01f2b530142a", "raw_type": null, "timestamp": "2026-08-27 12:19:03.260000+00:00", "type": "assistant.message_start"} +{"agent_id": null, "data": {"_available_tool_count": 16, "_num_tool_calls": 0, "_tool_counts": {}, "_tool_token_count": 5188, "api_call_id": "msg_011CeTEvnFZMMX8hePHkcccx", "api_endpoint": "/v1/messages", "cache_expires_at": "2026-08-27 12:24:02.157000+00:00", "cache_read_tokens": 12748, "cache_write_tokens": 296, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 6, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 12748, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 296, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 57, "token_type": "output"}], "total_nano_aiu": 193580000.0}, "cost": 0.33, "duration": "0:00:01.530000", "finish_reason": "stop", "initiator": "agent", "input_tokens": 13050, "inter_token_latency": "0:00:00.070205", "interaction_type": "conversation-agent", "model": "claude-haiku-4.5", "output_tokens": 57, "parent_tool_call_id": null, "provider_call_id": "29E8:3AC232:11AEDA6:149699D:6A902B36", "reasoning_effort": null, "reasoning_tokens": 0, "rte": true, "service_request_id": "590a04cd-ce38-45b7-9825-b49b355549c8", "time_to_first_token": "0:00:01.107547"}, "ephemeral": true, "id": "1caa46c0-a8f4-447b-ae27-1eaae4d73c45", "parent_id": "373d63a9-96ad-42c3-af02-01f2b530142a", "raw_type": null, "timestamp": "2026-08-27 12:19:03.688000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {"api_call_id": "msg_011CeTEvnFZMMX8hePHkcccx", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-97e40043-a738-46a1-94f3-d4eb31c498fe", "content": "I don't have permission to access the current directory. Could you verify the working directory or provide the path to the files you'd like me to examine? Alternatively, if there's a specific file you'd like me to read, please let me know its path.", "interaction_id": "8b56955c-c140-4d44-837e-500dc7029967", "message_id": "1cfb775b-75bd-4569-8f3f-944d5d64bb0f", "model": "claude-haiku-4.5", "output_tokens": 57, "parent_tool_call_id": null, "phase": null, "reasoning_text": "The user wants me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws\n\nI'll use bash to list files and find the largest one.", "request_id": "29E8:3AC232:11AEDA6:149699D:6A902B36", "rte": true, "server_tools": null, "service_request_id": "590a04cd-ce38-45b7-9825-b49b355549c8", "tool_requests": [], "turn_id": "2"}, "ephemeral": null, "id": "a05bc6a4-55f8-485e-aeb4-5b05a8c0176e", "parent_id": "373d63a9-96ad-42c3-af02-01f2b530142a", "raw_type": null, "timestamp": "2026-08-27 12:19:03.689000+00:00", "type": "assistant.message"} +{"agent_id": null, "data": {"content": "The user wants me to:\n1. List the files in the current directory\n2. Find the largest one\n3. Read it\n4. Describe what it contains in one sentence\n\nLet me start by listing the files in the current directory. According to the environment context, I'm in: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws\n\nI'll use bash to list files and find the largest one.", "reasoning_id": "EtMFCrIBCBEYAipAJxE97+DjjDVs+bGpHa8dQ5VSPOiiCUTaNEFSDl8DtAClc+NOxKQ2705W5y9y/9pgDBuTFT+O+zKldneiyuvr+zIZY2xhdWRlLWhhaWt1LTQtNS0yMDI1MTAwMTgAQgh0aGlua2luZ1okY2VkMWY5OGUtMGI1MS00NjEwLTgyODItNGU1ZDg4Mzg0NTc0chAFJi2jM86AcGV3hjaN37r2iAEBqAG01sDUBrABAhIMF1ZrovH4Ic5Eh0jCGgzf7CUQ+Pi+9vSJjCsiMOY64NBjwtdHREJ9AdnWhwd6RbBs3vPGdFR5EGslesoW08mOBzMZI9fBkyq8FK+OiyrNAzbk7XOz6lHlIXhW6mmQFyn5JXZFsrHb0f+WT11juJg5auJiWXR9CnNg22Jnbx4xPs1tM4IRdAQd241Nl5wiVV46Q81QYQb3T03bpDuKyMLXWNx/vjnSQ0sPhxIfupFIzXmTUv/dr8LLdYnjEnPWCWS55KaGWPYz08IIS/FYG2fH3tYuXURI8JmivWNe/Uq63H3lMPg/Qp2zVNrkSWUhx+5/RRZe5jWLRL3UekQ6AdnZaIBeq7S50ZGILZykh2Qm8DobJiEBWFdnmojzOPZ7l4bvSkPCIRUsicuPT+vw9//aQfjO7ssjuC8ADlf2libVRA3UN8FHb1W8o9nr8MOidj+11EW5AXMCcwjqUtnqdSgNSLNAicQVBhr4uZsgt6NtUSg6mPuFvxnD5DT3I0FA0SWdxbOs6wbbItKuYTqqb689h+f3g09qBGIqlczX24Wdg74vkB4MahAeicJsZu/OniDGHT+1HxOlOIiZhfNNUPgHeRiQ4ROXv0ENraz75a3O94NFJFR9pk5FEySMdz2OtX5xKe7EU/PwXRze9NFsCwEvNSEXNMi9UVGvQpHB+ADcb3xSFFSSsMwpOTbbGDbDWwCG3BeC6Zqa3XVL9l9jGAE=", "rte": true}, "ephemeral": true, "id": "09912008-abaf-4b19-bdbe-b1b8ee8baab8", "parent_id": "a05bc6a4-55f8-485e-aeb4-5b05a8c0176e", "raw_type": null, "timestamp": "2026-08-27 12:19:03.689000+00:00", "type": "assistant.reasoning"} +{"agent_id": null, "data": {"model": null, "turn_id": "2"}, "ephemeral": null, "id": "582200b5-fdae-48b2-8fc9-62744e7ce1ee", "parent_id": "a05bc6a4-55f8-485e-aeb4-5b05a8c0176e", "raw_type": null, "timestamp": "2026-08-27 12:19:03.690000+00:00", "type": "assistant.turn_end"} +{"agent_id": null, "data": {"_model_cache_state": [{"_cache_ttl_seconds": 300, "cache_expires_at": "2026-08-27 12:24:02.157000+00:00", "model_id": "claude-haiku-4.5"}], "_total_premium_requests": 0.33, "total_nano_aiu": 2086670000.0}, "ephemeral": null, "id": "034f74aa-61d7-4b68-915d-26ddb5485ea7", "parent_id": "582200b5-fdae-48b2-8fc9-62744e7ce1ee", "raw_type": null, "timestamp": "2026-08-27 12:19:03.693000+00:00", "type": "session.usage_checkpoint"} +{"agent_id": null, "data": {"aborted": null}, "ephemeral": true, "id": "ccc72a5f-14c4-4e47-9923-f9ca0b534a93", "parent_id": "034f74aa-61d7-4b68-915d-26ddb5485ea7", "raw_type": null, "timestamp": "2026-08-27 12:19:03.693000+00:00", "type": "assistant.idle"} +{"agent_id": null, "data": {"aborted": null}, "ephemeral": true, "id": "0ba8dd42-349e-4a23-b618-15e8cee83f7e", "parent_id": "034f74aa-61d7-4b68-915d-26ddb5485ea7", "raw_type": null, "timestamp": "2026-08-27 12:19:03.694000+00:00", "type": "session.idle"} diff --git a/tests/integrations/fixtures/copilot_sdk/session_tools_subagent.jsonl b/tests/integrations/fixtures/copilot_sdk/session_tools_subagent.jsonl new file mode 100644 index 00000000..245b355c --- /dev/null +++ b/tests/integrations/fixtures/copilot_sdk/session_tools_subagent.jsonl @@ -0,0 +1,93 @@ +{"agent_id": null, "data": {"already_in_use": false, "context": {"base_commit": null, "branch": null, "cwd": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws", "git_root": null, "head_commit": null, "host_type": null, "pending_git_context": null, "repository": null, "repository_host": null}, "context_tier": null, "copilot_version": "1.0.79", "detached_from_spawning_parent_session_id": null, "github_mcp_tool_config": null, "producer": "copilot-agent", "reasoning_effort": null, "reasoning_summary": null, "remote_steerable": false, "selected_model": null, "session_id": "89b7a3bf-b6a4-432e-8271-6baa38edf1d3", "session_limits": null, "start_time": "2026-08-27 12:21:28.603000+00:00", "verbosity": null, "version": 1}, "ephemeral": null, "id": "a8a5a710-a6b0-4c89-ad7c-c24906175372", "parent_id": null, "raw_type": null, "timestamp": "2026-08-27 12:21:28.656000+00:00", "type": "session.start"} +{"agent_id": null, "data": {"bypass_permissions_disabled": false, "client_managed": false, "device_managed": false, "fail_closed": false, "managed_keys": [], "permissions_allow_intersected": null, "server_managed": false, "settings": null, "source": "none"}, "ephemeral": true, "id": "2fef70c7-8424-4648-b448-809578eb11db", "parent_id": "a8a5a710-a6b0-4c89-ad7c-c24906175372", "raw_type": null, "timestamp": "2026-08-27 12:21:28.669000+00:00", "type": "session.managed_settings_resolved"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "e8af1eb4-4811-4307-9307-e5841dbf81fc", "parent_id": "a8a5a710-a6b0-4c89-ad7c-c24906175372", "raw_type": null, "timestamp": "2026-08-27 12:21:28.674000+00:00", "type": "pending_messages.modified"} +{"agent_id": null, "data": {"cause": null, "context_tier": null, "new_model": "auto", "previous_model": null, "previous_reasoning_effort": null, "previous_reasoning_summary": null, "previous_verbosity": null, "reasoning_effort": null, "reasoning_summary": null, "verbosity": null}, "ephemeral": null, "id": "e8bae77f-b343-4c09-8e5e-17f85fb492fb", "parent_id": "a8a5a710-a6b0-4c89-ad7c-c24906175372", "raw_type": null, "timestamp": "2026-08-27 12:21:29.551000+00:00", "type": "session.model_change"} +{"agent_id": null, "data": {"skills": [{"argument_hint": null, "command_name": "customize-cloud-agent", "description": "Skill for customizing the Copilot cloud agent (formerly known as Copilot coding agent) environment, including copilot-setup-steps.yml configuration, preinstalling tools and dependencies, runners, and settings. Use when the user mentions copilot-setup-steps, copilot setup steps, or wants to configure the cloud agent environment.", "enabled": true, "name": "customize-cloud-agent", "path": "/Users/vini/Library/Caches/copilot/pkg/darwin-arm64/1.0.79/builtin/customize-cloud-agent/SKILL.md", "source": "builtin", "user_invocable": false}, {"argument_hint": null, "command_name": "github-pr-media", "description": "Upload an image or video to GitHub's user attachments API and embed it in a pull request description or comment. Use when asked to add screenshots, diagrams, recordings, or other media to a PR or GitHub comment.", "enabled": true, "name": "github-pr-media", "path": "/Users/vini/Library/Caches/copilot/pkg/darwin-arm64/1.0.79/builtin/github-pr-media/SKILL.md", "source": "builtin", "user_invocable": false}]}, "ephemeral": true, "id": "f354a21d-3643-4732-bfae-189dab93d218", "parent_id": "e8bae77f-b343-4c09-8e5e-17f85fb492fb", "raw_type": null, "timestamp": "2026-08-27 12:21:29.792000+00:00", "type": "session.skills_loaded"} +{"agent_id": null, "data": {"available_models": ["claude-haiku-4.5", "gpt-5-mini"], "candidate_models": ["claude-haiku-4.5", "gpt-5-mini"], "category_scores": {"code_gen": 0.3495, "debugging": 0.0107, "reasoning": 0.3958, "tool_use": 0.3711}, "chosen_model": "claude-haiku-4.5", "chosen_shortfall": 0.3983, "confidence": 0.72, "end_to_end_latency_ms": 281.0, "fallback": false, "fallback_reason": null, "has_image": false, "predicted_label": "no_reasoning", "reasoning_bucket": "medium", "router_latency_ms": 101.0, "routing_method": "hydra", "sticky_override": false}, "ephemeral": null, "id": "6bc111d2-c81f-4244-984d-645bde5cae81", "parent_id": "e8bae77f-b343-4c09-8e5e-17f85fb492fb", "raw_type": null, "timestamp": "2026-08-27 12:21:30.068000+00:00", "type": "session.auto_mode_resolved"} +{"agent_id": null, "data": {"content": "You are the GitHub Copilot CLI, a terminal assistant built by GitHub. You are an interactive CLI tool that helps users with software engineering tasks.\n\n# Tone and style\n* When providing output or exp\u2026", "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "metadata": null, "name": null, "role": "system"}, "ephemeral": null, "id": "6574bd88-61f9-4c46-99e4-9a9b276b1533", "parent_id": "6bc111d2-c81f-4244-984d-645bde5cae81", "raw_type": null, "timestamp": "2026-08-27 12:21:30.081000+00:00", "type": "system.message"} +{"agent_id": null, "data": {"model": "claude-haiku-4.5"}, "ephemeral": true, "id": "23af3eba-0234-4433-b39c-05e3ad76e5c5", "parent_id": "6574bd88-61f9-4c46-99e4-9a9b276b1533", "raw_type": null, "timestamp": "2026-08-27 12:21:30.083000+00:00", "type": "session.tools_updated"} +{"agent_id": null, "data": {"agent_mode": null, "attachments": [], "content": "Do these three things: (1) call the get_weather tool for Lisbon, (2) run `ls -l` with bash, and (3) delegate to a subagent to summarize what big.py contains.", "delivery": "idle", "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "is_autopilot_continuation": null, "native_document_path_fallback_paths": null, "parent_agent_task_id": "2898e84c-4e1b-4783-949f-9ddb6511c5f8", "source": null, "supported_native_document_mime_types": [], "transformed_content": "2026-08-27T09:21:30.087-03:00\n\nDo these three things: (1) call the get_weather tool for Lisbon, (2) run `ls -l` with bash, and (3) delegate to a subagent to summarize what big.py contains.\n\n\nAvailable tables: todos, todo_deps\n"}, "ephemeral": null, "id": "7642476a-a36b-46da-93d0-25c9cd582489", "parent_id": "6574bd88-61f9-4c46-99e4-9a9b276b1533", "raw_type": null, "timestamp": "2026-08-27 12:21:30.087000+00:00", "type": "user.message"} +{"agent_id": null, "data": {"title": "Do these three things: (1) call the get_weather tool for Lisbon, (2) run `ls -l` with bash, and (3) delegate to a subagent to summarize what big.py contains."}, "ephemeral": true, "id": "484952a0-4827-41de-8ea8-73c3453b95c1", "parent_id": "7642476a-a36b-46da-93d0-25c9cd582489", "raw_type": null, "timestamp": "2026-08-27 12:21:30.090000+00:00", "type": "session.title_changed"} +{"agent_id": null, "data": {"interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "model": null, "turn_id": "0"}, "ephemeral": null, "id": "bf9b429a-39ae-4354-ad02-4330e9762697", "parent_id": "7642476a-a36b-46da-93d0-25c9cd582489", "raw_type": null, "timestamp": "2026-08-27 12:21:30.111000+00:00", "type": "assistant.turn_start"} +{"agent_id": null, "data": {"conversation_tokens": 100, "current_tokens": 10563, "is_initial": true, "messages_length": 3, "system_tokens": 5224, "token_limit": 128000, "tool_definitions_tokens": 5239}, "ephemeral": true, "id": "56627b3e-ea28-4bd7-bda2-0ea2ba5b1e67", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:30.112000+00:00", "type": "session.usage_info"} +{"agent_id": null, "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "0"}, "ephemeral": true, "id": "a8abfffa-5d4b-458f-8789-e03718e930cd", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:30.116000+00:00", "type": "model.call_start"} +{"agent_id": null, "data": {"total_response_size_bytes": 14}, "ephemeral": true, "id": "77c6d080-fa5e-439a-affc-4cf0af51bcd6", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:31.428000+00:00", "type": "assistant.streaming_delta"} +{"agent_id": null, "data": {"delta_content": "The user wants", "reasoning_id": "2f86c4a2-46af-478e-bbec-e89af25bc0d7"}, "ephemeral": true, "id": "a14a5e63-1f73-41ae-af34-e69d94338d27", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:31.428000+00:00", "type": "assistant.reasoning_delta"} +{"agent_id": null, "data": {"total_response_size_bytes": 23}, "ephemeral": true, "id": "1bb08afa-4860-4278-a9b4-b0573bcf590a", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:31.428000+00:00", "type": "assistant.streaming_delta"} +{"agent_id": null, "data": {"delta_content": " me to do", "reasoning_id": "2f86c4a2-46af-478e-bbec-e89af25bc0d7"}, "ephemeral": true, "id": "fb73e919-e18d-4744-b6fd-4f5c175ecfac", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:31.428000+00:00", "type": "assistant.reasoning_delta"} +{"agent_id": null, "data": {"total_response_size_bytes": 37}, "ephemeral": true, "id": "49c33ab6-56da-470a-b520-c7d019a99638", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:31.429000+00:00", "type": "assistant.streaming_delta"} +{"agent_id": null, "data": {"delta_content": " three things:", "reasoning_id": "2f86c4a2-46af-478e-bbec-e89af25bc0d7"}, "ephemeral": true, "id": "1ca3782f-4fc2-4d74-929b-d597e5f1b768", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:31.429000+00:00", "type": "assistant.reasoning_delta"} +{"agent_id": null, "data": {"input_delta": "{\"city", "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_name": "get_weather", "tool_type": null}, "ephemeral": true, "id": "9c4800a8-b80b-4b95-b223-e0b24b32e7db", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:33.340000+00:00", "type": "assistant.tool_call_delta"} +{"agent_id": null, "data": {"input_delta": "\": \"Li", "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_name": "get_weather", "tool_type": null}, "ephemeral": true, "id": "59bf2c41-e814-4b44-9e9d-1a4cb2c8ad50", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:33.340000+00:00", "type": "assistant.tool_call_delta"} +{"agent_id": null, "data": {"input_delta": "sbon\"}", "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_name": "get_weather", "tool_type": null}, "ephemeral": true, "id": "af49ffdc-c402-4042-8867-781027915bb9", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:33.340000+00:00", "type": "assistant.tool_call_delta"} +{"agent_id": null, "data": {"_available_tool_count": 17, "_num_tool_calls": 3, "_tool_counts": {"bash": 1, "get_weather": 1, "task": 1}, "_tool_token_count": 5239, "api_call_id": "msg_011CeTF7ha5jd6tEsQJyQZqf", "api_endpoint": "/v1/messages", "cache_expires_at": "2026-08-27 12:26:30.126000+00:00", "cache_read_tokens": 6179, "cache_write_tokens": 6416, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 10, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 6179, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 6416, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 395, "token_type": "output"}], "total_nano_aiu": 1062290000.0}, "cost": 0.33, "duration": "0:00:04.673000", "finish_reason": "tool_calls", "initiator": "user", "input_tokens": 12605, "inter_token_latency": "0:00:00.044229", "interaction_type": "conversation-agent", "model": "claude-haiku-4.5", "output_tokens": 395, "parent_tool_call_id": null, "provider_call_id": "29D8:38DC23:19954C5:1C5376B:6A902BCA", "reasoning_effort": null, "reasoning_tokens": 130, "rte": true, "service_request_id": "a229c750-5902-470a-9017-33196165a23f", "time_to_first_token": "0:00:01.310968"}, "ephemeral": true, "id": "acd9905c-cdb9-4df8-8931-07d0872b2915", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:34.799000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {"api_call_id": "msg_011CeTF7ha5jd6tEsQJyQZqf", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-0225e8ce-5fa6-4a60-99f7-1965d37c515e", "content": "", "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "message_id": "ad58c6e2-b92b-4c9e-9550-803296ae4b31", "model": "claude-haiku-4.5", "output_tokens": 395, "parent_tool_call_id": null, "phase": null, "reasoning_text": "The user wants me to do three things:\n1. Call get_weather tool for Lisbon\n2. Run `ls -l` with bash\n3. Delegate to a subagent to summarize what big.py contains\n\nLet me do all three in parallel since they're independent. I'll:\n1. Call get_weather with city \"Lisbon\"\n2. Call bash with command \"ls -l\"\n3. Call task tool to delegate to an explore agent to summarize big.py\n\nAll three can be done in parallel in one function_calls block.", "request_id": "29D8:38DC23:19954C5:1C5376B:6A902BCA", "rte": true, "server_tools": null, "service_request_id": "a229c750-5902-470a-9017-33196165a23f", "tool_requests": [{"arguments": {"city": "Lisbon"}, "intention_summary": null, "mcp_server_name": null, "mcp_tool_name": null, "name": "get_weather", "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_title": null, "type": "function"}, {"arguments": {"command": "ls -l", "description": "List files in current directory"}, "intention_summary": "List files in current directory", "mcp_server_name": null, "mcp_tool_name": null, "name": "bash", "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR", "tool_title": null, "type": "function"}, {"arguments": {"agent_type": "explore", "description": "Summarize big.py file", "mode": "sync", "name": "big-py-summary", "prompt": "Find and summarize what the file big.py contains. Provide a concise overview of its purpose, main components, and key functions or classes. Start from the current working directory."}, "intention_summary": "Summarize big.py file", "mcp_server_name": null, "mcp_tool_name": null, "name": "task", "tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "tool_title": null, "type": "function"}], "turn_id": "0"}, "ephemeral": null, "id": "6df83a27-b1f0-420d-8b1f-d2d8de0653bc", "parent_id": "bf9b429a-39ae-4354-ad02-4330e9762697", "raw_type": null, "timestamp": "2026-08-27 12:21:34.805000+00:00", "type": "assistant.message"} +{"agent_id": null, "data": {"content": "The user wants me to do three things:\n1. Call get_weather tool for Lisbon\n2. Run `ls -l` with bash\n3. Delegate to a subagent to summarize what big.py contains\n\nLet me do all three in parallel since they're independent. I'll:\n1. Call get_weather with city \"Lisbon\"\n2. Call bash with command \"ls -l\"\n3. Call task tool to delegate to an explore agent to summarize big.py\n\nAll three can be done in parallel in one function_calls block.", "reasoning_id": "2f86c4a2-46af-478e-bbec-e89af25bc0d7", "rte": true}, "ephemeral": true, "id": "fb098e69-8513-499b-b606-c2992b59c8e3", "parent_id": "6df83a27-b1f0-420d-8b1f-d2d8de0653bc", "raw_type": null, "timestamp": "2026-08-27 12:21:34.806000+00:00", "type": "assistant.reasoning"} +{"agent_id": null, "data": {"arguments": {"city": "Lisbon"}, "display_verbatim": null, "mcp_server_name": null, "mcp_tool_name": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "rte": true, "shell_tool_info": null, "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_description": null, "tool_name": "get_weather", "turn_id": "0"}, "ephemeral": null, "id": "9332c2d2-2355-48ba-8c88-726c46dc5923", "parent_id": "6df83a27-b1f0-420d-8b1f-d2d8de0653bc", "raw_type": null, "timestamp": "2026-08-27 12:21:34.807000+00:00", "type": "tool.execution_start"} +{"agent_id": null, "data": {"arguments": {"command": "ls -l", "description": "List files in current directory"}, "display_verbatim": null, "mcp_server_name": null, "mcp_tool_name": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "rte": true, "shell_tool_info": {"display_command": null, "has_write_file_redirection": false, "possible_paths": []}, "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR", "tool_description": null, "tool_name": "bash", "turn_id": "0"}, "ephemeral": null, "id": "4a2dc110-a46d-45f9-8785-d7c90105a062", "parent_id": "9332c2d2-2355-48ba-8c88-726c46dc5923", "raw_type": null, "timestamp": "2026-08-27 12:21:34.811000+00:00", "type": "tool.execution_start"} +{"agent_id": null, "data": {"arguments": {"agent_type": "explore", "description": "Summarize big.py file", "mode": "sync", "name": "big-py-summary", "prompt": "Find and summarize what the file big.py contains. Provide a concise overview of its purpose, main components, and key functions or classes. Start from the current working directory."}, "display_verbatim": null, "mcp_server_name": null, "mcp_tool_name": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "rte": true, "shell_tool_info": null, "tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "tool_description": null, "tool_name": "task", "turn_id": "0"}, "ephemeral": null, "id": "3798b1a1-7e4a-4ae0-846a-d15b4c6a12de", "parent_id": "4a2dc110-a46d-45f9-8785-d7c90105a062", "raw_type": null, "timestamp": "2026-08-27 12:21:34.811000+00:00", "type": "tool.execution_start"} +{"agent_id": null, "data": {"permission_request": {"args": {"city": "Lisbon"}, "managed_approval_required": null, "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_description": "Get the current weather for a city.", "tool_name": "get_weather"}, "prompt_request": {"args": {"city": "Lisbon"}, "auto_approval": null, "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_description": "Get the current weather for a city.", "tool_name": "get_weather"}, "request_id": "09eda3f9-5b3a-435c-9e74-f298a75d4fec", "resolved_by_hook": null, "risk_assessment": null}, "ephemeral": null, "id": "d1b6d721-e038-4907-8b5c-3dfcafc8e19f", "parent_id": "3798b1a1-7e4a-4ae0-846a-d15b4c6a12de", "raw_type": null, "timestamp": "2026-08-27 12:21:34.816000+00:00", "type": "permission.requested"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "d13f4d37-eec5-49af-988d-d924cfbd14f1", "parent_id": "d1b6d721-e038-4907-8b5c-3dfcafc8e19f", "raw_type": null, "timestamp": "2026-08-27 12:21:34.817000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {"request_id": "09eda3f9-5b3a-435c-9e74-f298a75d4fec", "result": {}, "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie"}, "ephemeral": null, "id": "33338f8d-0c8a-437c-9ddb-09091634a9b1", "parent_id": "d1b6d721-e038-4907-8b5c-3dfcafc8e19f", "raw_type": null, "timestamp": "2026-08-27 12:21:34.818000+00:00", "type": "permission.completed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "74964823-15e1-489a-a041-75b37d33c754", "parent_id": "33338f8d-0c8a-437c-9ddb-09091634a9b1", "raw_type": null, "timestamp": "2026-08-27 12:21:34.819000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "64d1d3b1-ea2e-4bc2-b7c5-916f1f92cdc9", "parent_id": "33338f8d-0c8a-437c-9ddb-09091634a9b1", "raw_type": null, "timestamp": "2026-08-27 12:21:34.819000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {"arguments": {"city": "Lisbon"}, "request_id": "4aa0bf2f-3bef-4e2c-8595-d7135ebb4f94", "session_id": "89b7a3bf-b6a4-432e-8271-6baa38edf1d3", "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_name": "get_weather", "traceparent": null, "tracestate": null, "working_directory": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws"}, "ephemeral": null, "id": "39b7f2fb-9bc7-4f1c-b2e4-e66cfd201278", "parent_id": "33338f8d-0c8a-437c-9ddb-09091634a9b1", "raw_type": null, "timestamp": "2026-08-27 12:21:34.820000+00:00", "type": "external_tool.requested"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"agent_description": "Fast codebase exploration and answering questions. Uses code intelligence, {{grepToolName}}, {{globToolName}}, view, {{shellToolName}} tools in a separate context window to search files and understand code structure. Safe to call in parallel.\n", "agent_display_name": "Explore Agent", "agent_name": "explore", "model": "claude-haiku-4.5", "tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V"}, "ephemeral": null, "id": "30c62b1e-86bf-4b5b-af4d-fa9445bdff3d", "parent_id": "39b7f2fb-9bc7-4f1c-b2e4-e66cfd201278", "raw_type": null, "timestamp": "2026-08-27 12:21:34.823000+00:00", "type": "subagent.started"} +{"agent_id": null, "data": {"permission_request": {"can_offer_session_approval": false, "command_segments": [{"full_command_text": "ls -l", "identifier": "ls"}], "commands": [{"identifier": "ls -l", "read_only": false}], "full_command_text": "ls -l", "has_write_file_redirection": false, "intention": "List files in current directory", "managed_approval_required": null, "possible_paths": [], "possible_urls": [], "request_sandbox_bypass": null, "request_sandbox_bypass_reason": null, "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR", "warning": null}, "prompt_request": {"auto_approval": null, "can_offer_session_approval": false, "command_identifiers": ["ls -l"], "full_command_text": "ls -l", "intention": "List files in current directory", "managed_approval_required": null, "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR", "warning": null}, "request_id": "5bda488a-4ada-495e-8df3-9bf79e470ae3", "resolved_by_hook": null, "risk_assessment": null}, "ephemeral": null, "id": "16a07421-9ffa-448a-89be-cdccfbf7202a", "parent_id": "30c62b1e-86bf-4b5b-af4d-fa9445bdff3d", "raw_type": null, "timestamp": "2026-08-27 12:21:34.825000+00:00", "type": "permission.requested"} +{"agent_id": null, "data": {"request_id": "5bda488a-4ada-495e-8df3-9bf79e470ae3", "result": {}, "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR"}, "ephemeral": null, "id": "301fab39-b1e3-41b4-be46-b03ccafd1666", "parent_id": "16a07421-9ffa-448a-89be-cdccfbf7202a", "raw_type": null, "timestamp": "2026-08-27 12:21:34.828000+00:00", "type": "permission.completed"} +{"agent_id": null, "data": {"request_id": "4aa0bf2f-3bef-4e2c-8595-d7135ebb4f94"}, "ephemeral": null, "id": "2308cb42-34cd-45d7-913f-024ab26da245", "parent_id": "301fab39-b1e3-41b4-be46-b03ccafd1666", "raw_type": null, "timestamp": "2026-08-27 12:21:34.828000+00:00", "type": "external_tool.completed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "4f063056-86c6-4175-b483-1343bea00d4f", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.829000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "5ee38a35-a50e-4037-bdec-fc748b1ddc34", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.829000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "2b531dd9-6ff5-4bae-a4c9-2dd838e72abd", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.830000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "d4368bca-e20c-4e38-8727-5e57683a5afc", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.839000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {"partial_output": "total 16\n-rw-r--r-- 1 vini wheel 2049 Aug 27 09:18 big.py\n-rw-r--r-- 1 vini wheel 29 Aug 27 09:18 small.py\n", "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR"}, "ephemeral": true, "id": "04d89451-12eb-4a49-a50e-67134e88d215", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.847000+00:00", "type": "tool.execution_partial_result"} +{"agent_id": null, "data": {"partial_output": "total 16\n-rw-r--r-- 1 vini wheel 2049 Aug 27 09:18 big.py\n-rw-r--r-- 1 vini wheel 29 Aug 27 09:18 small.py\n", "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR"}, "ephemeral": true, "id": "3c57a41e-6e68-4aa1-a0cc-324a0b866a2d", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.847000+00:00", "type": "tool.execution_partial_result"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "3be66f6c-8dce-4508-9505-57986b4a7762", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.847000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "c852c767-09a0-4646-9aac-4e0141e7ef12", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.848000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "5ba9ee28-61e9-495d-8713-6e484804bddd", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.848000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "af0f2e6a-45b5-421b-9e9b-b9b3801e3a9d", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.849000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "9f3a9f64-ae65-43f5-a40a-8bf348388dea", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.851000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"content": "You are an exploration agent. Answer the question as fast as possible, then stop.\n\n**File paths:**\n- Use absolute paths for all file references (your working directory is shown in the `", "interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "metadata": null, "name": null, "role": "system"}, "ephemeral": null, "id": "aa7bf5c6-59d7-4322-a5df-9f6abe236cfa", "parent_id": "2308cb42-34cd-45d7-913f-024ab26da245", "raw_type": null, "timestamp": "2026-08-27 12:21:34.890000+00:00", "type": "system.message"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"agent_mode": null, "attachments": [], "content": "Find and summarize what the file big.py contains. Provide a concise overview of its purpose, main components, and key functions or classes. Start from the current working directory.", "delivery": "idle", "interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "is_autopilot_continuation": null, "native_document_path_fallback_paths": null, "parent_agent_task_id": "7c6fc151-1157-4d51-9a61-6092756baaa6", "source": "agent-89b7a3bf-b6a4-432e-8271-6baa38edf1d3", "supported_native_document_mime_types": [], "transformed_content": "2026-08-27T09:21:34.891-03:00\n\nFind and summarize what the file big.py contains. Provide a concise overview of its purpose, main components, and key functions or classes. Start from the current working directory."}, "ephemeral": null, "id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "parent_id": "aa7bf5c6-59d7-4322-a5df-9f6abe236cfa", "raw_type": null, "timestamp": "2026-08-27 12:21:34.892000+00:00", "type": "user.message"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "model": null, "turn_id": "0"}, "ephemeral": true, "id": "c59d2699-e88b-4faa-8acc-3158c26a0fba", "parent_id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "raw_type": null, "timestamp": "2026-08-27 12:21:34.898000+00:00", "type": "assistant.turn_start"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "0"}, "ephemeral": true, "id": "c9ef5c22-61e5-4edb-9f53-7e54b97d8a82", "parent_id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "raw_type": null, "timestamp": "2026-08-27 12:21:34.901000+00:00", "type": "model.call_start"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"_available_tool_count": 7, "_num_tool_calls": 1, "_tool_counts": {"view": 1}, "_tool_token_count": 1927, "api_call_id": "msg_011CeTF85PzNVko4FesJVGMr", "api_endpoint": "/v1/messages", "cache_expires_at": null, "cache_read_tokens": 4459, "cache_write_tokens": 617, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 10, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 4459, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 617, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 261, "token_type": "output"}], "total_nano_aiu": 253215000.0}, "cost": 0.33, "duration": "0:00:03.295000", "finish_reason": "tool_calls", "initiator": "sub-agent", "input_tokens": 5086, "inter_token_latency": "0:00:00.065564", "interaction_type": "conversation-subagent", "model": "claude-haiku-4.5", "output_tokens": 261, "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "provider_call_id": "29D8:38DC23:1996F14:1C55442:6A902BCF", "reasoning_effort": null, "reasoning_tokens": 142, "rte": true, "service_request_id": "b283c0f7-fb2f-4206-b4f9-b0eaf5949313", "time_to_first_token": "0:00:01.452775"}, "ephemeral": true, "id": "41a0285e-55ff-44a9-91ab-e7b3516a1b43", "parent_id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "raw_type": null, "timestamp": "2026-08-27 12:21:38.201000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "d2dabef8-ef45-4d70-b6e7-52dbbddc9b07", "parent_id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "raw_type": null, "timestamp": "2026-08-27 12:21:38.202000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "4bab9b94-fae6-4c01-ac30-1bbeecf1bb91", "parent_id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "raw_type": null, "timestamp": "2026-08-27 12:21:38.203000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"api_call_id": "msg_011CeTF85PzNVko4FesJVGMr", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-4133c6d0-ad80-44d2-8ea2-105b83a494b6", "content": "", "interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "message_id": "4b674a04-f16a-4c0c-aeb3-9e095174669e", "model": "claude-haiku-4.5", "output_tokens": 261, "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "phase": null, "reasoning_text": "The user wants me to find and summarize what the file `big.py` contains. They mention to start from the current working directory, which is `/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws`.\n\nLooking at the directory contents snapshot, I can see there are files: `big.py` and `small.py`.\n\nI should read the `big.py` file from the current working directory.", "request_id": "29D8:38DC23:1996F14:1C55442:6A902BCF", "rte": true, "server_tools": null, "service_request_id": "b283c0f7-fb2f-4206-b4f9-b0eaf5949313", "tool_requests": [{"arguments": {"path": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py"}, "intention_summary": "view the file at /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py.", "mcp_server_name": null, "mcp_tool_name": null, "name": "view", "tool_call_id": "toolu_01SgcAmR9KobrmGTHzSSxx3T", "tool_title": null, "type": "function"}], "turn_id": "0"}, "ephemeral": null, "id": "34f4ef7e-210a-4d03-8a5a-87ac0db647a6", "parent_id": "640a157e-9c48-49b9-9707-a43ef3c8aec2", "raw_type": null, "timestamp": "2026-08-27 12:21:38.204000+00:00", "type": "assistant.message"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"arguments": {"path": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py"}, "display_verbatim": null, "mcp_server_name": null, "mcp_tool_name": null, "model": "claude-haiku-4.5", "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "rte": true, "shell_tool_info": null, "tool_call_id": "toolu_01SgcAmR9KobrmGTHzSSxx3T", "tool_description": null, "tool_name": "view", "turn_id": "0"}, "ephemeral": null, "id": "d1a6b884-0f35-4ec2-9570-8763d6ef3d28", "parent_id": "34f4ef7e-210a-4d03-8a5a-87ac0db647a6", "raw_type": null, "timestamp": "2026-08-27 12:21:38.205000+00:00", "type": "tool.execution_start"} +{"agent_id": null, "data": {"permission_request": {"intention": "Read file: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py", "managed_approval_required": null, "path": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py", "request_sandbox_bypass": null, "request_sandbox_bypass_reason": null, "tool_call_id": "toolu_01SgcAmR9KobrmGTHzSSxx3T"}, "prompt_request": {"auto_approval": null, "intention": "Read file: /private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py", "managed_approval_required": null, "path": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py", "tool_call_id": "toolu_01SgcAmR9KobrmGTHzSSxx3T"}, "request_id": "b48ecd49-bca6-47b6-ab9f-e90b220c496d", "resolved_by_hook": null, "risk_assessment": null}, "ephemeral": null, "id": "fcd2b7da-6e0c-4a95-a67b-8caca9db9812", "parent_id": "d1a6b884-0f35-4ec2-9570-8763d6ef3d28", "raw_type": null, "timestamp": "2026-08-27 12:21:38.211000+00:00", "type": "permission.requested"} +{"agent_id": null, "data": {"request_id": "b48ecd49-bca6-47b6-ab9f-e90b220c496d", "result": {}, "tool_call_id": "toolu_01SgcAmR9KobrmGTHzSSxx3T"}, "ephemeral": null, "id": "1799aae9-808c-482d-ba44-f5acd0b5d63c", "parent_id": "fcd2b7da-6e0c-4a95-a67b-8caca9db9812", "raw_type": null, "timestamp": "2026-08-27 12:21:38.212000+00:00", "type": "permission.completed"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"error": null, "interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "is_user_requested": null, "mcp_meta": null, "model": "claude-haiku-4.5", "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "result": {"binary_results_for_llm": null, "citable_sources": null, "content": "1. def f0(x):\n2. \"\"\"Function 0.\"\"\"\n3. return x*0\n4. \n5. def f1(x):\n6. \"\"\"Function 1.\"\"\"\n7. return x*1\n8. \n9. def f2(x):\n10. \"\"\"Function 2.\"\"\"\n11. return x*2\n12. \n13. def f3(x):\n14. \"\"\"Function 3.\"\"\"\n15. return x*3\n16. \n17. def f4(x):\n18. \"\"\"Function 4.\"\"\"\n19. return x*4\n20. \n21. def f5(x):\n22. \"\"\"Function 5.\"\"\"\n23. return x*5\n24. \n25. def f6(x):\n26. \"\"\"Function 6.\"\"\"\n27. return x*6\n28. \n29. def f7(x):\n30. \"\"\"Function 7.\"\"\"\n31. return x*7\n32. \n33. def f8(x):\n34. \"\"\"Function 8.\"\"\"\n35. return x*8\n36. \n37. def f9(x):\n38. \"\"\"Function 9.\"\"\"\n39. return x*9\n40. \n41. def f10(x):\n42. \"\"\"Function 10.\"\"\"\n43. return x*10\n44. \n45. def f11(x):\n46. \"\"\"Function 11.\"\"\"\n47. return x*11\n48. \n49. def f12(x):\n50. \"\"\"Function 12.\"\"\"\n51. return x*12\n52. \n53. def f13(x):\n54. \"\"\"Function 13.\"\"\"\n55. return x*13\n56. \n57. def f14(x):\n58. \"\"\"Function 14.\"\"\"\n59. return x*14\n60. \n61. def f15(x):\n62. \"\"\"Function 15.\"\"\"\n63. return x*15\n64. \n65. def f16(x):\n66. \"\"\"Function 16.\"\"\"\n67. return x*16\n68. \n69. def f17(x):\n70. \"\"\"Function 17.\"\"\"\n71. return x*17\n72. \n73. def f18(x):\n74. \"\"\"Function 18.\"\"\"\n75. return x*18\n76. \n77. def f19(x):\n78. \"\"\"Function 19.\"\"\"\n79. return x*19\n80. \n81. def f20(x):\n82. \"\"\"Function 20.\"\"\"\n83. return x*20\n84. \n85. def f21(x):\n86. \"\"\"Function 21.\"\"\"\n87. return x*21\n88. \n89. def f22(x):\n90. \"\"\"Function 22.\"\"\"\n91. return x*22\n92. \n93. def f23(x):\n94. \"\"\"Function 23.\"\"\"\n95. return x*23\n96. \n97. def f24(x):\n98. \"\"\"Function 24.\"\"\"\n99. return x*24\n100. \n101. def f25(x):\n102. \"\"\"Function 25.\"\"\"\n103. return x*25\n104. \n105. def f26(x):\n106. \"\"\"Function 26.\"\"\"\n107. return x*26\n108. \n109. def f27(x):\n110. \"\"\"Function 27.\"\"\"\n111. return x*27\n112. \n113. def f28(x):\n114. \"\"\"Function 28.\"\"\"\n115. return x*28\n116. \n117. def f29(x):\n118. \"\"\"Function 29.\"\"\"\n119. return x*29\n120. \n121. def f30(x):\n122. \"\"\"Function 30.\"\"\"\n123. return x*30\n124. \n125. def f31(x):\n126. \"\"\"Function 31.\"\"\"\n127. return x*31\n128. \n129. def f32(x):\n130. \"\"\"Function 32.\"\"\"\n131. return x*32\n132. \n133. def f33(x):\n134. \"\"\"Function 33.\"\"\"\n135. return x*33\n136. \n137. def f34(x):\n138. \"\"\"Function 34.\"\"\"\n139. return x*34\n140. \n141. def f35(x):\n142. \"\"\"Function 35.\"\"\"\n143. return x*35\n144. \n145. def f36(x):\n146. \"\"\"Function 36.\"\"\"\n147. return x*36\n148. \n149. def f37(x):\n150. \"\"\"Function 37.\"\"\"\n151. return x*37\n152. \n153. def f38(x):\n154. \"\"\"Function 38.\"\"\"\n155. return x*38\n156. \n157. def f39(x):\n158. \"\"\"Function 39.\"\"\"\n159. return x*39\n160. ", "contents": null, "detailed_content": "\ndiff --git a/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py b/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py\nindex 0000000..0000000 100644\n--- a/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py\n+++ b/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws/big.py\n@@ -1,160 +1,160 @@\n def f0(x):\n \"\"\"Function 0.\"\"\"\n return x*0\n \n def f1(x):\n \"\"\"Function 1.\"\"\"\n return x*1\n \n def f2(x):\n \"\"\"Function 2.\"\"\"\n return x*2\n \n def f3(x):\n \"\"\"Function 3.\"\"\"\n return x*3\n \n def f4(x):\n \"\"\"Function 4.\"\"\"\n return x*4\n \n def f5(x):\n \"\"\"Function 5.\"\"\"\n return x*5\n \n def f6(x):\n \"\"\"Function 6.\"\"\"\n return x*6\n \n def f7(x):\n \"\"\"Function 7.\"\"\"\n return x*7\n \n def f8(x):\n \"\"\"Function 8.\"\"\"\n return x*8\n \n def f9(x):\n \"\"\"Function 9.\"\"\"\n return x*9\n \n def f10(x):\n \"\"\"Function 10.\"\"\"\n return x*10\n \n def f11(x):\n \"\"\"Function 11.\"\"\"\n return x*11\n \n def f12(x):\n \"\"\"Function 12.\"\"\"\n return x*12\n \n def f13(x):\n \"\"\"Function 13.\"\"\"\n return x*13\n \n def f14(x):\n \"\"\"Function 14.\"\"\"\n return x*14\n \n def f15(x):\n \"\"\"Function 15.\"\"\"\n return x*15\n \n def f16(x):\n \"\"\"Function 16.\"\"\"\n return x*16\n \n def f17(x):\n \"\"\"Function 17.\"\"\"\n return x*17\n \n def f18(x):\n \"\"\"Function 18.\"\"\"\n return x*18\n \n def f19(x):\n \"\"\"Function 19.\"\"\"\n return x*19\n \n def f20(x):\n \"\"\"Function 20.\"\"\"\n return x*20\n \n def f21(x):\n \"\"\"Function 21.\"\"\"\n return x*21\n \n def f22(x):\n \"\"\"Function 22.\"\"\"\n return x*22\n \n def f23(x):\n \"\"\"Function 23.\"\"\"\n return x*23\n \n def f24(x):\n \"\"\"Function 24.\"\"\"\n return x*24\n \n def f25(x):\n \"\"\"Function 25.\"\"\"\n return x*25\n \n def f26(x):\n \"\"\"Function 26.\"\"\"\n return x*26\n \n def f27(x):\n \"\"\"Function 27.\"\"\"\n return x*27\n \n def f28(x):\n \"\"\"Function 28.\"\"\"\n return x*28\n \n def f29(x):\n \"\"\"Function 29.\"\"\"\n return x*29\n \n def f30(x):\n \"\"\"Function 30.\"\"\"\n return x*30\n \n def f31(x):\n \"\"\"Function 31.\"\"\"\n return x*31\n \n def f32(x):\n \"\"\"Function 32.\"\"\"\n return x*32\n \n def f33(x):\n \"\"\"Function 33.\"\"\"\n return x*33\n \n def f34(x):\n \"\"\"Function 34.\"\"\"\n return x*34\n \n def f35(x):\n \"\"\"Function 35.\"\"\"\n return x*35\n \n def f36(x):\n \"\"\"Function 36.\"\"\"\n return x*36\n \n def f37(x):\n \"\"\"Function 37.\"\"\"\n return x*37\n \n def f38(x):\n \"\"\"Function 38.\"\"\"\n return x*38\n \n def f39(x):\n \"\"\"Function 39.\"\"\"\n return x*39\n \n", "mcp_meta": null, "structured_content": null, "ui_resource": null}, "rte": true, "sandboxed": null, "success": true, "tool_call_id": "toolu_01SgcAmR9KobrmGTHzSSxx3T", "tool_description": null, "tool_telemetry": {"metrics": {"responseTokenLimit": 32000, "resultForLlmLength": 2741, "resultLength": 2741}, "properties": {"command": "view", "fileExtension": "[\".py\"]", "inputs": "[\"path\",\"command\"]", "options": "{\"truncateBasedOn\":\"tokenCount\",\"truncateStyle\":\"middle\"}", "resolvedPathAgainstCwd": "false", "viewType": "file"}, "restrictedProperties": {}}, "turn_id": "0"}, "ephemeral": null, "id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "parent_id": "1799aae9-808c-482d-ba44-f5acd0b5d63c", "raw_type": null, "timestamp": "2026-08-27 12:21:38.215000+00:00", "type": "tool.execution_complete"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "ff7f2a82-9552-46d9-9c2b-01237ac14082", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:38.217000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"model": null, "turn_id": "0"}, "ephemeral": true, "id": "4dd58e1c-111f-444a-85ee-65819a409138", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:38.223000+00:00", "type": "assistant.turn_end"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "model": null, "turn_id": "1"}, "ephemeral": true, "id": "2955fdb0-5c9c-47e9-ae02-5f656577c131", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:38.223000+00:00", "type": "assistant.turn_start"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "1"}, "ephemeral": true, "id": "d674ee34-3683-402f-b264-2c60a99bda3a", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:38.225000+00:00", "type": "model.call_start"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"delta_content": "##", "message_id": "0189d126-23f1-4aad-ad6d-d4539eaf7380", "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V"}, "ephemeral": true, "id": "225c3c0d-83a1-4665-9863-7c46a74e217d", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:39.109000+00:00", "type": "assistant.message_delta"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"delta_content": " Summary of `big.py`\n\n**Purpose:** A utility module containing simple", "message_id": "0189d126-23f1-4aad-ad6d-d4539eaf7380", "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V"}, "ephemeral": true, "id": "80bb265a-2339-470a-a743-a583b147347a", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:39.637000+00:00", "type": "assistant.message_delta"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"delta_content": " mathematical multiplication functions.\n\n**Main Components:**\n- **40 simple functions** named `f0()", "message_id": "0189d126-23f1-4aad-ad6d-d4539eaf7380", "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V"}, "ephemeral": true, "id": "1ec96ac2-1353-4fa1-8a49-726fa479c7b0", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:39.638000+00:00", "type": "assistant.message_delta"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"_available_tool_count": 7, "_num_tool_calls": 0, "_tool_counts": {}, "_tool_token_count": 1927, "api_call_id": "msg_011CeTF8HBexg1qmKgJneMfz", "api_endpoint": "/v1/messages", "cache_expires_at": null, "cache_read_tokens": 5076, "cache_write_tokens": 1475, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 6, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 5076, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 1475, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 186, "token_type": "output"}], "total_nano_aiu": 328735000.0}, "cost": 0.33, "duration": "0:00:02.959000", "finish_reason": "stop", "initiator": "sub-agent", "input_tokens": 6557, "inter_token_latency": "0:00:00.259051", "interaction_type": "conversation-subagent", "model": "claude-haiku-4.5", "output_tokens": 186, "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "provider_call_id": "29D8:38DC23:19981E1:1C568E0:6A902BD2", "reasoning_effort": null, "reasoning_tokens": 0, "rte": true, "service_request_id": "6bc6efd5-fab7-4316-a75d-71815f0b600e", "time_to_first_token": "0:00:00.883867"}, "ephemeral": true, "id": "79578a1e-17c2-4b9d-a8a2-c119a064dea3", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:41.188000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "43e11e75-bd5c-4020-ba1e-58a487b4f7a7", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:41.189000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "6ed2cbe2-801c-4eb6-9a97-d0464d03dbc1", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:41.189000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"api_call_id": "msg_011CeTF8HBexg1qmKgJneMfz", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-4637bc37-e1a9-43ec-aa4b-ec70801981de", "content": "## Summary of `big.py`\n\n**Purpose:** A utility module containing simple mathematical multiplication functions.\n\n**Main Components:**\n- **40 simple functions** named `f0()` through `f39()`\n- Each function follows an identical pattern with a docstring\n\n**Key Functions:**\n- `f0(x)` through `f39(x)` - Each function takes a single parameter `x` and returns `x` multiplied by the function's index number\n - Example: `f0(x)` returns `x*0`, `f1(x)` returns `x*1`, `f39(x)` returns `x*39`\n\n**Overall:** A straightforward module containing 40 repetitive multiplication functions with predictable behavior. Each function multiplies its input by its corresponding index (0-39).", "interaction_id": "20eb84fb-e525-431a-bb8b-a32042706ccf", "message_id": "0189d126-23f1-4aad-ad6d-d4539eaf7380", "model": "claude-haiku-4.5", "output_tokens": 186, "parent_tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "phase": null, "reasoning_text": "The user wants me to find and summarize what the file `big.py` contains. They mention to start from the current working directory, which is `/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws`.\n\nLooking at the directory contents snapshot, I can see there are files: `big.py` and `small.py`.\n\nI should read the `big.py` file from the current working directory.", "request_id": "29D8:38DC23:19981E1:1C568E0:6A902BD2", "rte": true, "server_tools": null, "service_request_id": "6bc6efd5-fab7-4316-a75d-71815f0b600e", "tool_requests": [], "turn_id": "1"}, "ephemeral": null, "id": "588c31d8-eb5a-435e-a927-77e52b53aadf", "parent_id": "f2ae002a-3a10-4448-81f1-979f3e775dcb", "raw_type": null, "timestamp": "2026-08-27 12:21:41.190000+00:00", "type": "assistant.message"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"model": null, "turn_id": "1"}, "ephemeral": true, "id": "8cc74619-a76b-46be-be76-f6920c4b5360", "parent_id": "588c31d8-eb5a-435e-a927-77e52b53aadf", "raw_type": null, "timestamp": "2026-08-27 12:21:41.190000+00:00", "type": "assistant.turn_end"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "56e0152c-4047-491e-ac83-377c42c5382b", "parent_id": "588c31d8-eb5a-435e-a927-77e52b53aadf", "raw_type": null, "timestamp": "2026-08-27 12:21:41.201000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "31c0a579-bd7a-480f-addd-cef84066a9fc", "parent_id": "588c31d8-eb5a-435e-a927-77e52b53aadf", "raw_type": null, "timestamp": "2026-08-27 12:21:41.202000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "data": {"agent_display_name": "Explore Agent", "agent_name": "explore", "cancelled": null, "duration": "0:00:06.385000", "model": "claude-haiku-4.5", "tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "total_tokens": 12090, "total_tool_calls": 1}, "ephemeral": null, "id": "88ae233c-99ff-4ede-8f07-05bef88568fe", "parent_id": "588c31d8-eb5a-435e-a927-77e52b53aadf", "raw_type": null, "timestamp": "2026-08-27 12:21:41.203000+00:00", "type": "subagent.completed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "eafe0dec-0512-4dd0-baa8-67e1cd84793f", "parent_id": "88ae233c-99ff-4ede-8f07-05bef88568fe", "raw_type": null, "timestamp": "2026-08-27 12:21:41.203000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {}, "ephemeral": true, "id": "954eb7dc-0186-4073-98e2-c2d2e800affb", "parent_id": "88ae233c-99ff-4ede-8f07-05bef88568fe", "raw_type": null, "timestamp": "2026-08-27 12:21:41.204000+00:00", "type": "session.background_tasks_changed"} +{"agent_id": null, "data": {"error": null, "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "is_user_requested": null, "mcp_meta": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "result": {"binary_results_for_llm": null, "citable_sources": null, "content": "It is 22C and sunny in Lisbon.", "contents": null, "detailed_content": "It is 22C and sunny in Lisbon.", "mcp_meta": null, "structured_content": null, "ui_resource": null}, "rte": true, "sandboxed": null, "success": true, "tool_call_id": "toolu_01QdopvJHQsZ7XUzM4oHnFie", "tool_description": null, "tool_telemetry": {}, "turn_id": "0"}, "ephemeral": null, "id": "1b1d5f1a-f2a4-42c9-9326-25536c96988c", "parent_id": "88ae233c-99ff-4ede-8f07-05bef88568fe", "raw_type": null, "timestamp": "2026-08-27 12:21:41.206000+00:00", "type": "tool.execution_complete"} +{"agent_id": null, "data": {"error": null, "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "is_user_requested": null, "mcp_meta": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "result": {"binary_results_for_llm": null, "citable_sources": null, "content": "total 16\n-rw-r--r-- 1 vini wheel 2049 Aug 27 09:18 big.py\n-rw-r--r-- 1 vini wheel 29 Aug 27 09:18 small.py\n", "contents": [{"cwd": "/private/tmp/claude-501/-Users-vini-Workspace-Openlayer-integrations-sdks/b3978e0d-127c-460e-869e-357293ac3d63/scratchpad/copilot-probe/ws", "exit_code": 0, "output_preview": "total 16\n-rw-r--r-- 1 vini wheel 2049 Aug 27 09:18 big.py\n-rw-r--r-- 1 vini wheel 29 Aug 27 09:18 small.py\n", "output_truncated": false, "shell_id": "0"}], "detailed_content": "total 16\n-rw-r--r-- 1 vini wheel 2049 Aug 27 09:18 big.py\n-rw-r--r-- 1 vini wheel 29 Aug 27 09:18 small.py\n", "mcp_meta": null, "structured_content": null, "ui_resource": null}, "rte": true, "sandboxed": null, "success": true, "tool_call_id": "toolu_01EBTnVYRCkb8NdiWsyoTGxR", "tool_description": null, "tool_telemetry": {"metrics": {"commandTimeout": 30000}, "properties": {"asyncOnlyShell": "false", "customTimeout": "false", "detached": "false", "executionMode": "sync", "sandboxApplied": "false", "sandboxOptOutRequested": "false"}}, "turn_id": "0"}, "ephemeral": null, "id": "96838c32-66ef-4e0f-ab59-2946e4e6b0a5", "parent_id": "1b1d5f1a-f2a4-42c9-9326-25536c96988c", "raw_type": null, "timestamp": "2026-08-27 12:21:41.207000+00:00", "type": "tool.execution_complete"} +{"agent_id": null, "data": {"error": null, "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "is_user_requested": null, "mcp_meta": null, "model": "claude-haiku-4.5", "parent_tool_call_id": null, "result": {"binary_results_for_llm": null, "citable_sources": null, "content": "## Summary of `big.py`\n\n**Purpose:** A utility module containing simple mathematical multiplication functions.\n\n**Main Components:**\n- **40 simple functions** named `f0()` through `f39()`\n- Each function follows an identical pattern with a docstring\n\n**Key Functions:**\n- `f0(x)` through `f39(x)` - Each function takes a single parameter `x` and returns `x` multiplied by the function's index number\n - Example: `f0(x)` returns `x*0`, `f1(x)` returns `x*1`, `f39(x)` returns `x*39`\n\n**Overall:** A straightforward module containing 40 repetitive multiplication functions with predictable behavior. Each function multiplies its input by its corresponding index (0-39).", "contents": null, "detailed_content": "## Summary of `big.py`\n\n**Purpose:** A utility module containing simple mathematical multiplication functions.\n\n**Main Components:**\n- **40 simple functions** named `f0()` through `f39()`\n- Each function follows an identical pattern with a docstring\n\n**Key Functions:**\n- `f0(x)` through `f39(x)` - Each function takes a single parameter `x` and returns `x` multiplied by the function's index number\n - Example: `f0(x)` returns `x*0`, `f1(x)` returns `x*1`, `f39(x)` returns `x*39`\n\n**Overall:** A straightforward module containing 40 repetitive multiplication functions with predictable behavior. Each function multiplies its input by its corresponding index (0-39).", "mcp_meta": null, "structured_content": null, "ui_resource": null}, "rte": true, "sandboxed": null, "success": true, "tool_call_id": "toolu_013RHgDCCnyFpE52pN1qXV2V", "tool_description": null, "tool_telemetry": {"metrics": {"numberOfToolCallsMadeByAgent": 1, "response_length": 668}, "properties": {"agent_name": "explore", "agent_type": "explore", "execution_mode": "sync", "prompt_length": "181", "response_length": "668"}, "restrictedProperties": {"agent_id": "big-py-summary", "agent_name": "explore"}}, "turn_id": "0"}, "ephemeral": null, "id": "c0d22056-0fb8-4ddb-96fd-d8dfc9b03cb4", "parent_id": "96838c32-66ef-4e0f-ab59-2946e4e6b0a5", "raw_type": null, "timestamp": "2026-08-27 12:21:41.209000+00:00", "type": "tool.execution_complete"} +{"agent_id": null, "data": {"model": null, "turn_id": "0"}, "ephemeral": null, "id": "497eb10e-3023-46a4-88e7-c51f472282a3", "parent_id": "c0d22056-0fb8-4ddb-96fd-d8dfc9b03cb4", "raw_type": null, "timestamp": "2026-08-27 12:21:41.218000+00:00", "type": "assistant.turn_end"} +{"agent_id": null, "data": {"interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "model": null, "turn_id": "1"}, "ephemeral": null, "id": "ea076ab4-bdf6-4177-8671-a35bc1dc5ee0", "parent_id": "497eb10e-3023-46a4-88e7-c51f472282a3", "raw_type": null, "timestamp": "2026-08-27 12:21:41.219000+00:00", "type": "assistant.turn_start"} +{"agent_id": null, "data": {"conversation_tokens": 808, "current_tokens": 11271, "is_initial": false, "messages_length": 7, "system_tokens": 5224, "token_limit": 128000, "tool_definitions_tokens": 5239}, "ephemeral": true, "id": "522e46c7-be4f-416c-862b-eaf550485ee1", "parent_id": "ea076ab4-bdf6-4177-8671-a35bc1dc5ee0", "raw_type": null, "timestamp": "2026-08-27 12:21:41.220000+00:00", "type": "session.usage_info"} +{"agent_id": null, "data": {"_previous_response_id": null, "model": "claude-haiku-4.5", "turn_id": "1"}, "ephemeral": true, "id": "3da937fc-3b65-4f37-b0d7-053e3b9ddde7", "parent_id": "ea076ab4-bdf6-4177-8671-a35bc1dc5ee0", "raw_type": null, "timestamp": "2026-08-27 12:21:41.222000+00:00", "type": "model.call_start"} +{"agent_id": null, "data": {"message_id": "917ef4ed-490b-46e6-93ea-6b1960aa5e40", "phase": null}, "ephemeral": true, "id": "3e37b21a-904e-4b7f-bb74-7d53b41277a1", "parent_id": "ea076ab4-bdf6-4177-8671-a35bc1dc5ee0", "raw_type": null, "timestamp": "2026-08-27 12:21:42.255000+00:00", "type": "assistant.message_start"} +{"agent_id": null, "data": {"_available_tool_count": 17, "_num_tool_calls": 0, "_tool_counts": {}, "_tool_token_count": 5239, "api_call_id": "msg_011CeTF8WA1np73VJacyyHRj", "api_endpoint": "/v1/messages", "cache_expires_at": "2026-08-27 12:26:41.232000+00:00", "cache_read_tokens": 12595, "cache_write_tokens": 754, "content_filter_triggered": false, "copilot_usage": {"_token_details": [{"batch_size": 1000000, "cost_per_batch": 100000000000, "token_count": 7, "token_type": "input"}, {"batch_size": 1000000, "cost_per_batch": 10000000000, "token_count": 12595, "token_type": "cache_read"}, {"batch_size": 1000000, "cost_per_batch": 125000000000, "token_count": 754, "token_type": "cache_write"}, {"batch_size": 1000000, "cost_per_batch": 500000000000, "token_count": 127, "token_type": "output"}], "total_nano_aiu": 284400000.0}, "cost": 0.33, "duration": "0:00:02.066000", "finish_reason": "stop", "initiator": "agent", "input_tokens": 13356, "inter_token_latency": "0:00:00.206742", "interaction_type": "conversation-agent", "model": "claude-haiku-4.5", "output_tokens": 127, "parent_tool_call_id": null, "provider_call_id": "29D8:38DC23:1999209:1C57AD8:6A902BD5", "reasoning_effort": null, "reasoning_tokens": 0, "rte": true, "service_request_id": "9db204e2-902b-43e6-a4a7-144e2e6b29c3", "time_to_first_token": "0:00:01.032130"}, "ephemeral": true, "id": "1f797f42-565b-4a0d-8df3-48fcb64c581a", "parent_id": "ea076ab4-bdf6-4177-8671-a35bc1dc5ee0", "raw_type": null, "timestamp": "2026-08-27 12:21:43.298000+00:00", "type": "assistant.usage"} +{"agent_id": null, "data": {"api_call_id": "msg_011CeTF8WA1np73VJacyyHRj", "chunk_count": null, "chunk_index": null, "citations": null, "client_request_id": "00000-7b0604cd-46a1-43f4-9078-862dbace3125", "content": "Done! Here's what I found:\n\n**Weather:** It's 22\u00b0C and sunny in Lisbon \ud83c\udf1e\n\n**Directory listing:** Two Python files in the current directory:\n- `big.py` (2049 bytes)\n- `small.py` (29 bytes)\n\n**big.py summary:** A utility module with 40 simple mathematical functions (`f0()` through `f39()`), each multiplying its input by its corresponding index number (e.g., `f5(x)` returns `x*5`).", "interaction_id": "56b1e0b4-f34f-43d0-a86f-0692557873f7", "message_id": "917ef4ed-490b-46e6-93ea-6b1960aa5e40", "model": "claude-haiku-4.5", "output_tokens": 127, "parent_tool_call_id": null, "phase": null, "reasoning_text": "The user wants me to do three things:\n1. Call get_weather tool for Lisbon\n2. Run `ls -l` with bash\n3. Delegate to a subagent to summarize what big.py contains\n\nLet me do all three in parallel since they're independent. I'll:\n1. Call get_weather with city \"Lisbon\"\n2. Call bash with command \"ls -l\"\n3. Call task tool to delegate to an explore agent to summarize big.py\n\nAll three can be done in parallel in one function_calls block.", "request_id": "29D8:38DC23:1999209:1C57AD8:6A902BD5", "rte": true, "server_tools": null, "service_request_id": "9db204e2-902b-43e6-a4a7-144e2e6b29c3", "tool_requests": [], "turn_id": "1"}, "ephemeral": null, "id": "cdd6ad3e-f99d-4de4-8362-4a227a7896cd", "parent_id": "ea076ab4-bdf6-4177-8671-a35bc1dc5ee0", "raw_type": null, "timestamp": "2026-08-27 12:21:43.300000+00:00", "type": "assistant.message"} +{"agent_id": null, "data": {"content": "The user wants me to do three things:\n1. Call get_weather tool for Lisbon\n2. Run `ls -l` with bash\n3. Delegate to a subagent to summarize what big.py contains\n\nLet me do all three in parallel since they're independent. I'll:\n1. Call get_weather with city \"Lisbon\"\n2. Call bash with command \"ls -l\"\n3. Call task tool to delegate to an explore agent to summarize big.py\n\nAll three can be done in parallel in one function_calls block.", "reasoning_id": "EsUFCrIBCBEYAipAxEZVo6VXl5V7Q6WG0A1LMnhXGT2yrx7QNKYH4t9NyVQCWnZxoMQw7RpOU2dXaN6CvYfM9nq0FxgC/cOxlVPpAzIZY2xhdWRlLWhhaWt1LTQtNS0yMDI1MTAwMTgAQgh0aGlua2luZ1okY2VkMWY5OGUtMGI1MS00NjEwLTgyODItNGU1ZDg4Mzg0NTc0chAFJi2jM86AcGV3hjaN37r2iAEBqAHN18DUBrABAhIMM3OXz1+2fTUJJFIYGgz5Vne6wpbmMYkZyLoiMEjaGlA4NC92mWYqFLeKvOqwlEJmUJiTZSsnKS31By+pFwBS9QHE3hAjS4gYVzYfPiq/A2bIw/J1D6LK0twDbft4FYTd3rLO9Rfyi1z2R98LLrRSuUncvUTb82mfi3WddrFm5yyb8SdOLx5sWJ5L8+QiWmBAbK/r/0ANrjuQ/WJLtaaQUGxpklMkP5dkRln/HNHHj7DN1nsc50bzoZT+XccpQrTFzvrmvGfAmypFX3i9dQC6ties53zr8se4tWq14cis1VvMee9NGCFDM+mUg5jpod6qWAVOj90Eog9o+K0iF7KtiDQEn4tfxF4H1ZwhEN86W5+Nyd8ZYB20VVF0Rfb0mYqZs/6PhPbhKg9P62aeB5uroajOlIqaTU6n4ntPgxwZLUllGA2CdquV8ivqrDGHamRz6jaZxp1nrkyGR3EL8NSyZ0huU/p12A+BXEu1dEgjXljSrG/vppe7C54edM9/1MwVkX4FlEYhbk9fzxU8XTonqwbF18bl61bzgCpH+VUcwewNCRHUnh2rF06gECr7UL6zKzZoojC4JQ/xiV/GNjnpbhxV12NpvlZvU8MfrJKP4+2IzSDTpCHPbqm4v6aAKDk22h9UwfWce24bFXaNmWnpnvsrueCfCCXTiAy+wVU7OJQTOerZi8jTqoprf+QE/xgB", "rte": true}, "ephemeral": true, "id": "17894d9c-8ed4-4cc3-82de-24cd232d7579", "parent_id": "cdd6ad3e-f99d-4de4-8362-4a227a7896cd", "raw_type": null, "timestamp": "2026-08-27 12:21:43.300000+00:00", "type": "assistant.reasoning"} +{"agent_id": null, "data": {"model": null, "turn_id": "1"}, "ephemeral": null, "id": "b84bebdc-21cc-4959-b23a-6b4175bd4258", "parent_id": "cdd6ad3e-f99d-4de4-8362-4a227a7896cd", "raw_type": null, "timestamp": "2026-08-27 12:21:43.301000+00:00", "type": "assistant.turn_end"} +{"agent_id": null, "data": {"_model_cache_state": [{"_cache_ttl_seconds": 300, "cache_expires_at": "2026-08-27 12:26:41.232000+00:00", "model_id": "claude-haiku-4.5"}], "_total_premium_requests": 0.33, "total_nano_aiu": 1928640000.0}, "ephemeral": null, "id": "dd58783f-5bf3-426b-a1bd-0419ced3ecd7", "parent_id": "b84bebdc-21cc-4959-b23a-6b4175bd4258", "raw_type": null, "timestamp": "2026-08-27 12:21:43.304000+00:00", "type": "session.usage_checkpoint"} +{"agent_id": null, "data": {"aborted": null}, "ephemeral": true, "id": "c74ec7dd-9876-4003-9f71-ad3495cbe25d", "parent_id": "dd58783f-5bf3-426b-a1bd-0419ced3ecd7", "raw_type": null, "timestamp": "2026-08-27 12:21:43.304000+00:00", "type": "assistant.idle"} +{"agent_id": null, "data": {"aborted": null}, "ephemeral": true, "id": "58ac9671-ed32-4a42-a2e8-a5c87fc6dd31", "parent_id": "dd58783f-5bf3-426b-a1bd-0419ced3ecd7", "raw_type": null, "timestamp": "2026-08-27 12:21:43.304000+00:00", "type": "session.idle"} diff --git a/tests/integrations/test_copilot_sdk.py b/tests/integrations/test_copilot_sdk.py new file mode 100644 index 00000000..a7968526 --- /dev/null +++ b/tests/integrations/test_copilot_sdk.py @@ -0,0 +1,622 @@ +"""Unit tests for the GitHub Copilot SDK integration. + +The fixtures are real captured sessions (trimmed of base64 blobs and the 24KB +CLI system prompt), so these tests exercise the actual wire format rather than +an assumption about it. +""" + +import json +import pathlib +from typing import Any, Dict, List +from unittest.mock import patch + +import pytest + +from openlayer.lib.tracing import tracer as ol_tracer +from openlayer.lib.integrations import copilot_sdk + +FIXTURES = pathlib.Path(__file__).parent / "fixtures" / "copilot_sdk" + +# Captured before any patching so the fixture can delegate to the real resolver. +_REAL_RESOLVE = ol_tracer._resolve + + +def load_fixture(name: str) -> List[Dict[str, Any]]: + with open(FIXTURES / name) as f: + return [json.loads(line) for line in f if line.strip()] + + +@pytest.fixture(autouse=True) +def _disable_publish(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("OPENLAYER_DISABLE_PUBLISH", "true") + monkeypatch.setenv("OPENLAYER_API_KEY", "fake") + monkeypatch.setattr(ol_tracer, "_publish", False, raising=False) + + +@pytest.fixture +def published_traces(monkeypatch: pytest.MonkeyPatch): + """Yield a list that receives each published trace as a dict. + + ``background_publish_enabled`` defaults to True, which hands the trace to a + ThreadPoolExecutor -- so asserting on the captured list right after the + collector runs is a race the test only sometimes wins. Force synchronous + publishing so capture is deterministic regardless of global tracer config + left behind by other suites. + """ + captured: List[Any] = [] + + def capture(trace, *_args, **_kwargs): + captured.append(trace.to_dict()) + + monkeypatch.setattr( + ol_tracer, + "_resolve", + lambda key, *a, **k: False if key == "background_publish_enabled" else _REAL_RESOLVE(key, *a, **k), + ) + + with patch.object(ol_tracer, "_publish", True), patch.object( + ol_tracer, "_upload_and_publish_trace", side_effect=capture + ): + yield captured + + + +def run_fixture(name: str) -> None: + collector = copilot_sdk.CopilotTraceCollector() + for event in load_fixture(name): + collector.handle(event) + + +def _find(step_list, predicate): + """Depth-first search over a published step tree.""" + for step in step_list: + if predicate(step): + return step + found = _find(step.get("steps") or [], predicate) + if found is not None: + return found + return None + + +# --------------------------------------------------------------------------- # +# Buffering / routing +# --------------------------------------------------------------------------- # + + +def test_collector_buffers_events_and_keys_by_interaction_id(): + events = load_fixture("session_basic.jsonl") + collector = copilot_sdk.CopilotTraceCollector() + for event in events: + collector.handle(event) + + # The fixture ends with session.idle, which builds and clears interactions. + assert collector.session_id + assert collector.built_count == 1 + + +def test_delta_events_are_never_buffered(): + collector = copilot_sdk.CopilotTraceCollector() + seen_types = set() + for event in load_fixture("session_basic.jsonl"): + collector.handle(event) + for interaction in collector.interactions.values(): + seen_types.update(copilot_sdk._event_type(e) for e in interaction.events) + + assert not any(t.endswith("_delta") for t in seen_types) + assert "tool.execution_partial_result" not in seen_types + + +def test_handle_never_raises_on_malformed_event(): + collector = copilot_sdk.CopilotTraceCollector() + for junk in (None, {}, {"type": "user.message"}, object(), {"type": "assistant.usage"}): + collector.handle(junk) # must not raise + + +# --------------------------------------------------------------------------- # +# Provider + token mapping +# --------------------------------------------------------------------------- # + + +def test_provider_is_mapped_from_model_prefix_not_labelled_github(): + # Verified against llm-costs.openlayer.com: anthropic/claude-haiku-4.5 and + # openai/gpt-5.4 resolve; github/* returns "No cost data found". + assert copilot_sdk._provider_for_model("claude-haiku-4.5") == "anthropic" + assert copilot_sdk._provider_for_model("gpt-5.4") == "openai" + assert copilot_sdk._provider_for_model("o3-mini") == "openai" + assert copilot_sdk._provider_for_model("gemini-2.5-pro") == "google" + # Unknown prefixes omit provider rather than guess -- unpriced beats wrong. + assert copilot_sdk._provider_for_model("some-future-model") is None + assert copilot_sdk._provider_for_model("") is None + assert copilot_sdk._provider_for_model(None) is None + + +def test_usage_details_is_a_non_overlapping_partition(): + """Copilot's input_tokens already contains cache reads and writes. + + Observed live: input_tokens=12509, cache_write=12499, cache_read=0, and the + SDK's own copilot_usage._token_details reported input=10. The partition must + reproduce that, because the backend sums a price per recognized key. + """ + usage = { + "input_tokens": 12509, + "output_tokens": 224, + "cache_write_tokens": 12499, + "cache_read_tokens": 0, + "reasoning_tokens": 143, + } + details = copilot_sdk._usage_details(usage) + assert details["input_tokens"] == 10 + assert details["output_tokens"] == 224 + assert details["cache_creation_tokens"] == 12499 + assert "cached_tokens" not in details # zero-valued keys are omitted + # reasoning stays folded into output_tokens, matching langchain_callback + assert "reasoning_tokens" not in details + + +def test_usage_details_handles_cache_reads(): + usage = { + "input_tokens": 12754, + "output_tokens": 122, + "cache_write_tokens": 249, + "cache_read_tokens": 12499, + } + details = copilot_sdk._usage_details(usage) + assert details["input_tokens"] == 6 + assert details["cached_tokens"] == 12499 + assert details["cache_creation_tokens"] == 249 + assert sum(details.values()) == 12754 + 122 # partition is exact + + +def test_usage_details_accepts_camel_case(): + """TypeScript-shaped payloads must map identically.""" + details = copilot_sdk._usage_details( + {"inputTokens": 100, "outputTokens": 10, "cacheReadTokens": 40, "cacheWriteTokens": 0} + ) + assert details == {"input_tokens": 60, "output_tokens": 10, "cached_tokens": 40} + + +# --------------------------------------------------------------------------- # +# Trace shape +# --------------------------------------------------------------------------- # + + +def test_chat_steps_carry_tokens_and_priced_provider(published_traces): + run_fixture("session_basic.jsonl") + + assert len(published_traces) == 1 + root = published_traces[-1][0] + chats = [s for s in root["steps"] if s["type"] == "chat_completion"] + assert len(chats) == 3 # three turns in this fixture + for chat in chats: + assert chat["model"] == "claude-haiku-4.5" + assert chat["provider"] == "anthropic" + assert chat["usageDetails"]["output_tokens"] > 0 + # Copilot's premium-request figure must NOT be published as cost; + # leaving cost unset lets Openlayer price it from provider+model. + assert chat["cost"] is None + assert chat["metadata"]["copilot_premium_requests"] == 0.33 + + +def test_root_output_is_final_assistant_message_not_empty(published_traces): + """Openlayer builds a row's output from the root step; an empty root + silently produces an unusable row.""" + run_fixture("session_tools_subagent.jsonl") + + root = published_traces[-1][0] + assert root["type"] == "agent" + assert isinstance(root["output"], str) and len(root["output"]) > 20 + assert "Lisbon" in root["output"] # the final answer, not a reasoning preamble + assert root["inputs"]["prompt"].startswith("Do these three things") + + +def test_concurrent_tool_calls_are_siblings_not_nested(published_traces): + """Three tool.execution_start fire before any completion, and the + completions arrive out of order. A step-stack design would nest them.""" + run_fixture("session_tools_subagent.jsonl") + + root = published_traces[-1][0] + top_level = root["steps"] + names = [s["name"] for s in top_level] + assert "bash" in names + assert "get_weather" in names + # each concurrent call must be a direct child of the root, not of a sibling + for name in ("bash", "get_weather"): + assert any(s["name"] == name for s in top_level) + + +def test_subagent_dispatch_becomes_nested_agent_step(published_traces): + run_fixture("session_tools_subagent.jsonl") + + root = published_traces[-1][0] + subagent = _find( + root["steps"], lambda s: s["type"] == "agent" and "explore" in s["name"].lower() + ) + assert subagent is not None, "the `task` dispatch must become an AGENT step" + # the subagent nests under the `task` tool call, not at the top level + assert subagent not in root["steps"] or subagent["name"].startswith("subagent:") + + inner_chats = [s for s in subagent["steps"] if s["type"] == "chat_completion"] + inner_tools = [s for s in subagent["steps"] if s["type"] == "tool"] + assert len(inner_chats) >= 1 + assert any(t["name"] == "view" for t in inner_tools) + + +def test_tool_step_captures_arguments_and_failure(published_traces): + run_fixture("session_basic.jsonl") + + root = published_traces[-1][0] + bash = _find(root["steps"], lambda s: s["name"] == "bash") + assert bash is not None + assert bash["inputs"]["command"] == "ls -lhS" + # this fixture's bash call was permission-denied + assert bash["metadata"]["success"] is False + assert "denied" in json.dumps(bash["metadata"]["error"]).lower() + + +def test_session_id_is_recorded_for_session_grouping(published_traces): + run_fixture("session_basic.jsonl") + root = published_traces[-1][0] + assert root["metadata"]["copilot_session_id"] + + +# --------------------------------------------------------------------------- # +# Public API +# --------------------------------------------------------------------------- # + + +def test_user_handler_exception_does_not_break_tracing(): + handler = copilot_sdk.openlayer_event_handler() + + def exploding(_event): + raise RuntimeError("customer bug") + + composed = copilot_sdk._compose_handlers(exploding, handler) + composed({"type": "session.start", "data": {"session_id": "s1"}}) # must not raise + + +def test_composed_handler_still_delivers_to_user_handler(): + seen = [] + handler = copilot_sdk.openlayer_event_handler() + composed = copilot_sdk._compose_handlers(seen.append, handler) + composed({"type": "session.start", "data": {"session_id": "s1"}}) + assert len(seen) == 1 + + +# --------------------------------------------------------------------------- # +# Live-shape regressions +# +# The recorded fixtures are JSON, so every value in them is already a plain +# string/dict. The live Python binding is not: it hands back dataclasses and +# Enums. These tests pin the coercions that the fixtures cannot exercise. +# --------------------------------------------------------------------------- # + + +class _FakeEventType: + """Stands in for ``copilot.SessionEventType`` (an Enum with a .value).""" + + def __init__(self, value): + self.value = value + + +class _FakeData: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +class _FakeEvent: + def __init__(self, type_value, **data): + self.type = _FakeEventType(type_value) + self.data = _FakeData(**data) + self.agent_id = None + self.timestamp = "2026-08-27T12:18:55.041000+00:00" + + +def test_event_type_coerces_enum_to_string(): + """The live binding yields SessionEventType.SESSION_START, not a string. + + Without coercion every comparison silently fails and the integration + produces no traces at all -- which fixture-only tests cannot catch. + """ + assert copilot_sdk._event_type(_FakeEvent("session.start")) == "session.start" + assert copilot_sdk._event_type({"type": "session.idle"}) == "session.idle" + assert copilot_sdk._event_type(object()) == "" + + +def test_collector_handles_enum_typed_dataclass_events(): + collector = copilot_sdk.CopilotTraceCollector() + collector.handle(_FakeEvent("session.start", session_id="sess-1")) + assert collector.session_id == "sess-1", "enum-typed events must be understood" + + +def test_jsonable_flattens_dataclasses_and_enums(): + """Tool results arrive as SDK dataclasses; they must survive JSON encoding.""" + result = _FakeData(content="ok", kind=_FakeEventType("text"), nested=_FakeData(n=1)) + flat = copilot_sdk._jsonable(result) + assert flat["content"] == "ok" + assert flat["nested"] == {"n": 1} + json.dumps(flat) # must not raise + + +# --------------------------------------------------------------------------- # +# trace_copilot() -- the one-line entry point the docs lead with. +# +# The Copilot SDK requires Python 3.11+, so these drive a stand-in module +# registered in sys.modules rather than the real package. +# --------------------------------------------------------------------------- # + + +def _run(coro): + """Run a coroutine on a fresh loop and close it (no ResourceWarning).""" + import asyncio + + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + +class _FakeCopilotClient: + """Minimal stand-in for ``copilot.CopilotClient``.""" + + last_on_event = None + + async def create_session(self, **kwargs): + type(self).last_on_event = kwargs.get("on_event") + return "session" + + +@pytest.fixture +def fake_copilot_module(monkeypatch): + """Register a fake ``copilot`` module and undo any patch afterwards.""" + import sys + import types + import importlib.machinery + + module = types.ModuleType("copilot") + module.CopilotClient = _FakeCopilotClient + # _auto probes with importlib.util.find_spec, which raises ValueError for a + # sys.modules entry whose __spec__ is None -- so the stub needs one to be + # seen as "installed". + module.__spec__ = importlib.machinery.ModuleSpec("copilot", None) + monkeypatch.setitem(sys.modules, "copilot", module) + _FakeCopilotClient.last_on_event = None + original = _FakeCopilotClient.create_session + try: + yield module + finally: + copilot_sdk.untrace_copilot() + _FakeCopilotClient.create_session = original + + +def test_trace_copilot_patches_create_session(fake_copilot_module): + original = fake_copilot_module.CopilotClient.create_session + copilot_sdk.trace_copilot() + assert fake_copilot_module.CopilotClient.create_session is not original + + +def test_trace_copilot_is_idempotent(fake_copilot_module): + copilot_sdk.trace_copilot() + patched_once = fake_copilot_module.CopilotClient.create_session + copilot_sdk.trace_copilot() + assert fake_copilot_module.CopilotClient.create_session is patched_once + + +def test_trace_copilot_preserves_a_user_supplied_on_event(fake_copilot_module): + """Patching must not silently drop the caller's own handler.""" + seen = [] + copilot_sdk.trace_copilot() + + client = fake_copilot_module.CopilotClient() + _run(client.create_session(on_event=seen.append)) + + composed = _FakeCopilotClient.last_on_event + assert composed is not None + assert composed is not seen.append, "our handler must wrap, not replace" + composed({"type": "session.start", "data": {"session_id": "s1"}}) + assert len(seen) == 1, "the user's handler still receives every event" + + +def test_trace_copilot_traces_sessions_created_after_patching(fake_copilot_module, published_traces): + """End-to-end through the patch: events fed to the injected handler + must produce a published trace.""" + copilot_sdk.trace_copilot() + client = fake_copilot_module.CopilotClient() + _run(client.create_session()) + + handler = _FakeCopilotClient.last_on_event + assert handler is not None + for event in load_fixture("session_basic.jsonl"): + handler(event) + + assert len(published_traces) == 1 + assert published_traces[0][0]["name"] == copilot_sdk.ROOT_STEP_NAME + + +def test_untrace_copilot_restores_the_original(fake_copilot_module): + original = fake_copilot_module.CopilotClient.create_session + copilot_sdk.trace_copilot() + copilot_sdk.untrace_copilot() + assert fake_copilot_module.CopilotClient.create_session is original + + +def test_jsonable_flattens_enum_values(): + """The enum branch of _jsonable, which the dataclass test did not assert.""" + flat = copilot_sdk._jsonable({"kind": _FakeEventType("text"), "n": 1}) + assert flat["kind"] == "text" + assert flat["n"] == 1 + + +def test_flush_publishes_an_interaction_that_never_went_idle(published_traces): + """A session torn down mid-flight must not silently drop its buffer. + + ``session.idle`` is the normal trigger; a disconnect before it arrives + would otherwise lose everything buffered so far. + """ + collector = copilot_sdk.CopilotTraceCollector() + for event in load_fixture("session_basic.jsonl"): + if copilot_sdk._event_type(event) in ("session.idle", "session.shutdown"): + continue # simulate a session that never goes idle + collector.handle(event) + + assert not published_traces, "nothing should publish before the flush" + collector.flush() + assert len(published_traces) == 1 + assert published_traces[0][0]["name"] == copilot_sdk.ROOT_STEP_NAME + + +def test_flush_is_idempotent_after_a_normal_idle(published_traces): + collector = copilot_sdk.CopilotTraceCollector() + for event in load_fixture("session_basic.jsonl"): + collector.handle(event) + assert len(published_traces) == 1 + collector.flush() # disconnect after a normal idle + assert len(published_traces) == 1, "flush must not double-publish" + + +def test_event_handler_exposes_flush(): + handler = copilot_sdk.openlayer_event_handler() + assert callable(handler) + assert callable(handler.flush) + assert isinstance(handler.collector, copilot_sdk.CopilotTraceCollector) + + +# --------------------------------------------------------------------------- # +# Metered cost from GitHub's own AIU accounting. +# +# copilot_usage.total_nano_aiu is GitHub's authoritative figure for a call. +# Decoding copilot_usage._token_details shows GitHub's per-token rates for +# claude-haiku-4.5 are Anthropic's list prices scaled by exactly 100 across all +# four token categories, i.e. 1 AIU = $0.01 -- so total_nano_aiu / 1e11 equals +# the priced cost to 12 decimal places. See the constant's docstring for the +# caveat about how far that generalizes. +# --------------------------------------------------------------------------- # + + +def test_metered_cost_matches_list_price_for_the_observed_session(): + """The exact numbers captured live from claude-haiku-4.5.""" + usage = { + "copilot_usage": {"total_nano_aiu": 1675375000.0}, + "input_tokens": 12509, + "output_tokens": 224, + "cache_write_tokens": 12499, + "cache_read_tokens": 0, + } + metered = copilot_sdk._metered_cost_usd(usage) + # Anthropic list price for the same partition: + # 10*1e-6 + 0*1e-7 + 12499*1.25e-6 + 224*5e-6 + assert metered == pytest.approx(0.01675375, abs=1e-9) + + +def test_metered_cost_is_none_without_aiu(): + assert copilot_sdk._metered_cost_usd({}) is None + assert copilot_sdk._metered_cost_usd({"copilot_usage": {}}) is None + assert copilot_sdk._metered_cost_usd({"copilot_usage": {"total_nano_aiu": 0}}) is None + + +def test_mapped_provider_leaves_cost_for_openlayer_to_price(published_traces): + """When we know the provider, Openlayer prices it and we get costDetails.""" + run_fixture("session_basic.jsonl") + chat = published_traces[-1][0]["steps"][0] + assert chat["provider"] == "anthropic" + assert chat["cost"] is None, "leave it unset so the backend prices per category" + # ...but the metered figure is still recorded as a cross-check. + assert chat["metadata"]["copilot_metered_cost_usd"] == pytest.approx(0.01675375, abs=1e-9) + + +def test_unmapped_model_falls_back_to_the_metered_cost(published_traces): + """A model we have no prefix for must land priced, not at $0.""" + events = load_fixture("session_basic.jsonl") + for event in events: + data = event.get("data") or {} + if "model" in data: + data["model"] = "some-future-model-9" + + collector = copilot_sdk.CopilotTraceCollector() + for event in events: + collector.handle(event) + + chat = published_traces[-1][0]["steps"][0] + assert chat["provider"] is None, "we must not guess a provider" + assert chat["cost"] == pytest.approx(0.01675375, abs=1e-9), ( + "an unmapped model should still carry GitHub's own metered cost" + ) + + +def test_metered_cost_reads_a_dataclass_shaped_copilot_usage(): + """Live events nest ``copilot_usage`` as a dataclass, not a dict. + + The fixtures nest plain dicts, so an isinstance(..., dict) guard passes + every fixture test while silently dropping the real thing. + """ + usage = {"copilot_usage": _FakeData(total_nano_aiu=1675375000.0)} + assert copilot_sdk._metered_cost_usd(usage) == pytest.approx(0.01675375, abs=1e-9) + + +def test_metered_cost_holds_for_a_second_vendor(): + """Verified live on gpt-5-mini: GitHub bills 25/200/2.5 AIU per 1M + input/output/cache-read, i.e. OpenAI's $0.25/$2.00/$0.025 list prices. + So 1 AIU = $0.01 is not an Anthropic-only coincidence.""" + metered = copilot_sdk._metered_cost_usd({"copilot_usage": {"total_nano_aiu": 55665000.0}}) + expected = 449 * 0.25e-6 + 9216 * 0.025e-6 + 107 * 2.0e-6 + assert metered == pytest.approx(expected, abs=1e-9) + + +def test_patch_does_not_double_trace_when_user_passes_our_own_handler( + fake_copilot_module, published_traces +): + """Mixing trace_copilot() with an explicit openlayer_event_handler() must + not publish the interaction twice. + + Both are documented entry points, so a user following the quickstart and + then copying the "trace specific sessions" snippet ends up with both active + on the same session. Without a guard that yields two collectors and two + identical rows per send. + """ + copilot_sdk.trace_copilot() + client = fake_copilot_module.CopilotClient() + _run(client.create_session(on_event=copilot_sdk.openlayer_event_handler())) + + handler = _FakeCopilotClient.last_on_event + for event in load_fixture("session_basic.jsonl"): + handler(event) + + assert len(published_traces) == 1, "the interaction must publish exactly once" + + +# --------------------------------------------------------------------------- # +# Auto-instrumentation: openlayer.lib.init() should pick this up like any other +# supported SDK, so users need not know a Copilot-specific function name. +# --------------------------------------------------------------------------- # + + +def test_copilot_is_registered_for_auto_instrumentation(): + from openlayer.lib.integrations._auto import REGISTRY + + spec = next((s for s in REGISTRY if s.name == "copilot"), None) + assert spec is not None, "init(auto_instrument=True) must cover the Copilot SDK" + assert spec.probe == "copilot", "github-copilot-sdk imports as `copilot`" + assert spec.unpatch is not None, "must be reversible via unpatch_all()" + + +def test_auto_instrument_patches_and_unpatches_copilot(fake_copilot_module): + from openlayer.lib.integrations import _auto + + original = fake_copilot_module.CopilotClient.create_session + + results = _auto.auto_instrument(["copilot"]) + assert results == {"copilot": True} + assert fake_copilot_module.CopilotClient.create_session is not original + + _auto.unpatch_all() + assert fake_copilot_module.CopilotClient.create_session is original + + +def test_auto_instrument_skips_copilot_when_not_installed(monkeypatch): + import sys + + from openlayer.lib.integrations import _auto + + monkeypatch.setitem(sys.modules, "copilot", None) + monkeypatch.setattr(_auto, "_is_installed", lambda probe: probe != "copilot") + assert _auto.auto_instrument(["copilot"]) == {"copilot": False} diff --git a/tests/integrations/test_copilot_sdk_live.py b/tests/integrations/test_copilot_sdk_live.py new file mode 100644 index 00000000..80d423a4 --- /dev/null +++ b/tests/integrations/test_copilot_sdk_live.py @@ -0,0 +1,105 @@ +"""Live end-to-end test for the GitHub Copilot SDK integration. + +Skipped unless OPENLAYER_COPILOT_LIVE_TEST=1 AND the interpreter is Python 3.11+ -- ``github-copilot-sdk`` declares +``requires_python >= 3.11``, while this SDK still supports 3.9, so the unit +tests deliberately run off recorded fixtures instead of the real package. + +Run with: + OPENLAYER_COPILOT_LIVE_TEST=1 GITHUB_TOKEN=$(gh auth token) \\ + pytest tests/integrations/test_copilot_sdk_live.py -v -s +""" + +import os +import sys +from typing import Any, List +from unittest.mock import patch + +import pytest + +from openlayer.lib.tracing import tracer as ol_tracer + +# Opt-in is explicit rather than keyed on GITHUB_TOKEN alone: that variable is +# commonly exported by developers and by CI steps that have nothing to do with +# Copilot, and an installation token has no Copilot access -- so keying on it +# would turn a normal `pytest` run into a five-minute live session that then +# fails on auth. +pytestmark = [ + pytest.mark.skipif( + sys.version_info < (3, 11), + reason="github-copilot-sdk requires Python 3.11+", + ), + pytest.mark.skipif( + os.environ.get("OPENLAYER_COPILOT_LIVE_TEST") != "1", + reason="set OPENLAYER_COPILOT_LIVE_TEST=1 (and a GitHub token with Copilot access) to run", + ), +] + +_REAL_RESOLVE = ol_tracer._resolve + + +@pytest.mark.asyncio +async def test_live_copilot_session_publishes_trace(monkeypatch, tmp_path): + pytest.importorskip("copilot") + from copilot import CopilotClient, PermissionHandler + + from openlayer.lib.integrations import copilot_sdk + + captured: List[Any] = [] + + def capture(trace, *_args, **_kwargs): + captured.append(trace.to_dict()) + + # Force synchronous publishing so capture is deterministic. + monkeypatch.setattr( + ol_tracer, + "_resolve", + lambda key, *a, **k: ( + False if key == "background_publish_enabled" else _REAL_RESOLVE(key, *a, **k) + ), + ) + + (tmp_path / "hello.py").write_text("print('hello')\n") + (tmp_path / "notes.txt").write_text("some notes\n") + + with patch.object(ol_tracer, "_publish", True), patch.object( + ol_tracer, "_upload_and_publish_trace", side_effect=capture + ): + client = CopilotClient(working_directory=str(tmp_path), log_level="error") + await client.start() + try: + session = await client.create_session( + working_directory=str(tmp_path), + on_permission_request=PermissionHandler.approve_all, + on_event=copilot_sdk.openlayer_event_handler(), + ) + await session.send_and_wait( + "Run `ls` with bash and tell me how many files there are, in one sentence.", + timeout=300, + ) + await session.disconnect() + finally: + await client.stop() + + assert captured, "a trace must be published" + root = captured[-1][0] + + assert root["type"] == "agent" + assert root["name"] == copilot_sdk.ROOT_STEP_NAME + # The root's output is what becomes the row's output column. + assert isinstance(root["output"], str) and root["output"].strip() + assert root["inputs"]["prompt"].startswith("Run `ls`") + assert root["metadata"]["copilot_session_id"] + + chats = [s for s in root["steps"] if s["type"] == "chat_completion"] + assert chats, "at least one chat completion step" + for chat in chats: + assert chat["model"] + # A real provider slug is what lets Openlayer price the call; "github" + # would silently yield $0. + assert chat["provider"] in ("anthropic", "openai", "google", "xai") + assert chat["cost"] is None, "premium-request units must not be published as cost" + assert any(c["usageDetails"].get("output_tokens", 0) > 0 for c in chats) + + tools = [s for s in root["steps"] if s["type"] == "tool"] + assert tools, "the prompt forces a bash tool call" + assert any(t["name"] == "bash" for t in tools)