diff --git a/.azure-pipelines/scripts/hosted-agent-samples-ci-skiplist b/.azure-pipelines/scripts/hosted-agent-samples-ci-skiplist index a4d07dd4e..b881754f1 100644 --- a/.azure-pipelines/scripts/hosted-agent-samples-ci-skiplist +++ b/.azure-pipelines/scripts/hosted-agent-samples-ci-skiplist @@ -6,6 +6,10 @@ samples/csharp/hosted-agents/agent-framework/a2a/01-delegation/caller samples/python/hosted-agents/agent-framework/a2a/01-delegation/caller samples/python/hosted-agents/langgraph/a2a/a2a-caller +# Native A2A needs JSON-RPC/card handling, ordered C then A/B deployment and +# separate real-user sign-ins. Its sample-owned runners provide the test contract. +samples/python/hosted-agents/bring-your-own/a2a/session-routing + # Activity and WebSocket protocols need dedicated orchestration/clients. samples/csharp/hosted-agents/bring-your-own/activity/echo samples/csharp/hosted-agents/bring-your-own/activity/github-copilot diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/.gitignore b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/.gitignore new file mode 100644 index 000000000..809d1e8bc --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/.gitignore @@ -0,0 +1,8 @@ +.azure/ +.venv/ +__pycache__/ +*.sqlite* +src/agent/packages/ +src/agent/wheelhouse/ +results/ +deployment/ diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/AGENTS.md b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/AGENTS.md new file mode 100644 index 000000000..a78cd0754 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/AGENTS.md @@ -0,0 +1,46 @@ +# Coding Agent Instructions - A2A session routing + +This project was built with the microsoft-foundry skill. Before working on or answering questions about foundry agents, read the microsoft-foundry skill first. + +This sample hosts three deterministic native A2A 1.0 agents in Foundry. It tests +session routing independently of LLM conversations or model quality. + +## Deployment mode + +Direct code deployment, Python 3.13, with bundled Linux dependencies. No Dockerfile +or model is required. All three services intentionally share the same source +directory; `REPRO_ROLE` selects A, B or C. + +## Key files + +- `azure.yaml`: three agents and a configurable existing project endpoint. +- `src/agent/main.py`: forwarders and C's session-scoped synthetic ledger. +- `src/agent/client.py`: native A2A client and request-scoped identity forwarding. +- `src/agent/requirements.txt`: fully pinned runtime graph. +- `prepare_bundle.ps1`: Linux dependency bundle preparation from Windows. +- `run_experiment.py`: single-user sequential/concurrent matrix. +- `run_real_user.py`: independent sign-ins and receipt comparison. +- `evidence/`: historical synthetic receipts; never replace with live-test output. + +## Dependency workflow + +Follow `samples/python/hosted-agents/DEPENDENCY_POLICY.md`. This sample uses the +fully pinned requirements fallback because the tested preview SDK remote build +failed to recognize the uv-only project, and subsequent remote restores hit TLS +errors. Preserve the tested versions. Regenerate the full graph and run the +repository dependency checker, including `--resolve`, for dependency changes. +The preview SDK packages require the public Azure SDK development feed; index +configuration belongs in install commands, not the requirements artifact. + +## Development and evidence + +Use the standard `logging` module for runtime diagnostics. CLI tools may print. +Run `python -m unittest discover -s tests -v` and `python test_local.py` from a +restored environment. The local test owns and stops its three child processes. +Cloud A2A needs the dedicated runners, not the shared Responses/Invocations +harness. Do not silently substitute another protocol to satisfy CI. + +Never forward caller-supplied identity headers as proof of identity. A/B forward +the SDK's platform call context and use their own managed identities. Test only +synthetic markers. Session IDs, context IDs and boot IDs are distinct concepts. +Do not assert cross-user or concurrent isolation without the relevant receipts. diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/README.md b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/README.md new file mode 100644 index 000000000..019328b8d --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/README.md @@ -0,0 +1,331 @@ +# A2A downstream session routing + +## What this sample demonstrates + +Can two callers reach the same downstream hosted session? This sample uses three +deterministic native A2A 1.0 agents to distinguish context IDs, tasks, hosted +sessions and user identity. It includes the source, dedicated test runners and +synthetic evidence from actual Foundry invocations. + +## How it works + +```text +User 1 --> A --+ + +--> C --> session-local synthetic message ledger +User 1/2 -> B -+ +``` + +All three services run [main.py](src/agent/main.py). `REPRO_ROLE` selects A, B or +C. A/B authenticate using their own managed identities and forward the SDK's +request-scoped platform call headers. C reports: + +- A2A context ID and task ID. +- Platform `FOUNDRY_AGENT_SESSION_ID` and session GUID. +- A boot UUID, which is not a substitute for a session ID. +- The first 20 hexadecimal characters of SHA-256 of the opaque platform user ID. + This fingerprint is for comparison, not authentication. +- A sequence number and previous synthetic markers in a SQLite ledger under the + hosted session's private home directory. + +There is **no LLM, Copilot session, booking, web search or model-generated +answer**. This isolates A2A/Foundry session routing from model behavior. +User messages cannot choose an outbound URL; A/B call only `C_AGENT_ENDPOINT`. +The diagnostic ledger deliberately reveals prior synthetic markers in the same +session. Do not send secrets, real conversations or personal data. + +### Three different identifiers + +| Identifier | Meaning | +|---|---| +| A2A `contextId` | Opaque conversation label supplied by the client or generated when omitted | +| A2A task ID | Identifies one task; separate tasks can share a context | +| Hosted session ID | Actual platform session used for compute and session-local state | + +Returning the same context ID alone is not proof of session reuse. This sample +checks the actual hosted session ID and state history too. + +### Forwarding modes + +The JSON *text content* sent to A/B contains a synthetic `marker` and: + +| Sample-specific `mode` | What A/B send to C | +|---|---| +| `fresh` (default) | Omit downstream context ID | +| `explicit` | Set downstream context to `c_context_id` | +| `inherit` | Copy the incoming A2A context to C | + +These are sample options, not standard A2A protocol fields. The actual A2A +context field is `message.context_id` in Python / `message.contextId` on the wire. + +## Prerequisites + +- An existing Foundry project enabled for native A2A. +- Python 3.13 for the hosted runtime; Python 3.11+ for the local source. +- Azure CLI, `azd >= 1.37.2`, and `azure.ai.agents >= 1.0.0-beta.23`. +- Permission to deploy hosted agents. An authorized owner must be able to grant + **Foundry Agent Consumer** to A/B's managed identities at project scope. +- For the real-user test: two people independently authenticated with authorized + Entra accounts. Do not send tokens to each other. + +Follow the shared [Python startup configuration](../../../README.md#python-startup-configuration). +This diagnostic sample is CLI-only; it does not depend on Agent Inspector or a +VS Code launch configuration. + +## Run and test locally + +From this sample directory, restore the pinned +[requirements.txt](src/agent/requirements.txt) into an isolated environment: + +```powershell +python -m venv .\src\agent\.venv +& .\src\agent\.venv\Scripts\python.exe -m pip install ` + --extra-index-url "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/" ` + -r .\src\agent\requirements.txt + +& .\src\agent\.venv\Scripts\python.exe -m unittest discover -s .\tests -v +& .\src\agent\.venv\Scripts\python.exe .\test_local.py +``` + +The local integration test starts and stops its three child servers on loopback +ports 18191-18193, uses temporary ledger directories, and writes +`results/local.json`. It runs 18 end-to-end calls with 29 context/history +assertions. Local C shares one process/home directory, so this run deliberately +does **not** assert platform-session separation. + +The dependency fallback is intentional: the tested uv-only project was not +recognized by the remote builder, and remote restores subsequently hit TLS +errors. The committed requirements pin the complete graph from the tested local +environment. Preview agentserver packages are retrieved from the public Azure +SDK feed; index configuration is kept out of the requirements artifact. + +## Deploy with Azure Developer CLI + +### Initialize in an empty deployment directory + +Start in this sample directory and resolve its manifest before changing folders: + +```powershell +$manifest = (Resolve-Path .\azure.yaml).Path +$env:AZD_ALPHA_ENABLE_AGENTS_A2A = "true" +$env:AZURE_AI_PROJECT_ENDPOINT = "" + +$deployment = Join-Path $HOME ("a2a-session-routing-" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory $deployment +Set-Location $deployment + +azd auth login +az login +azd ai agent init --src . -t $manifest ` + --project-id "" ` + -e session-routing --no-prompt +azd env set AZURE_AI_PROJECT_ENDPOINT $env:AZURE_AI_PROJECT_ENDPOINT +``` + +Keep the deployment directory outside version control. The manifest references +an existing project and declares no new model deployment. Do not provision +unrelated infrastructure or reuse someone else's live endpoints. + +Before deploying, edit the three service keys **and their `name` values** to +unique names if the defaults already exist in your project. Reusing an existing +agent name creates a new version, not a separate agent. + +### Prepare a Linux dependency bundle + +```powershell +.\prepare_bundle.ps1 +``` + +The helper downloads TLS-verified Linux Python 3.13 wheels and installs them +under `src/agent/packages`. The manifest uses supported `bundled` code deployment +for all three services, avoiding remote dependency-download variability. +The wheel cache and local environments are excluded from upload. No Docker or +ACR is needed; do not import Linux `packages` directly on Windows. + +### Deploy C, then A/B + +Use your renamed keys below if applicable: + +```powershell +azd deploy session-routing-c --no-prompt +azd ai agent show session-routing-c --output json +azd env set C_AGENT_ENDPOINT "" + +azd deploy session-routing-a --no-prompt +azd deploy session-routing-b --no-prompt +azd ai agent show session-routing-a --output json +azd ai agent show session-routing-b --output json +``` + +Verify active status. An authorized owner should inspect A/B's +`instance_identity.principal_id` and grant missing runtime access: + +```powershell +az role assignment create ` + --assignee-object-id "" ` + --assignee-principal-type ServicePrincipal ` + --role "Foundry Agent Consumer" ` + --scope "" +``` + +Repeat only for the two new forwarders. Leave existing unrelated permissions +unchanged. Default Entra isolation must remain enabled on A, B and C. + +## Single-user remote test + +From a restored source environment: + +```powershell +& .\src\agent\.venv\Scripts\python.exe .\run_experiment.py ` + --a "" ` + --b "" ` + --output .\results\same-user.json +``` + +The runner makes 18 parent invocations and 18 downstream calls, persisting every +receipt and failure. It checks fresh sequential/concurrent calls, explicitly +shared contexts, inherited contexts and branch-specific contexts with follow-ups. +Use one intended credential identity: the runner uses `DefaultAzureCredential`. + +## Two real users, one downstream context + +Each person uses their own machine or independent CLI profile and runs `az login` +with their own account. The dedicated client uses `AzureCliCredential` explicitly +so unrelated environment credentials cannot silently select another identity. +Agree on one new opaque context value, for example +`two-real-users-your-run-01`, and use it in both commands. + +User 1 calls A: + +```powershell +& .\src\agent\.venv\Scripts\python.exe .\run_real_user.py invoke ` + --endpoint "" ` + --context "two-real-users-your-run-01" ` + --marker "U1-via-A" --output .\results\u1.json +``` + +User 2 calls B under their own sign-in: + +```powershell +& .\src\agent\.venv\Scripts\python.exe .\run_real_user.py invoke ` + --endpoint "" ` + --context "two-real-users-your-run-01" ` + --marker "U2-via-B" --output .\results\u2.json +``` + +Exchange only the synthetic receipts and compare: + +```powershell +& .\src\agent\.venv\Scripts\python.exe .\run_real_user.py compare ` + .\results\u1.json .\results\u2.json +``` + +The comparator checks equal C contexts, different nonempty user fingerprints, +different actual C sessions, identity preservation through each parent, distinct +C tasks and absence of the other marker in session history. Repeating each +user's call with a new marker and the same context can test retention; coordinating +simultaneous calls can test concurrency. Neither extension was completed in the +recorded real-user run. + +Do not use `x-ms-user-identity` or `--user-identity` here. Those test delegated +identities and require separate permission; they are not two independent sign-ins. + +## Recorded results + +### Same user via A and B + +Run `repro-9af72d559e114a51864d528caabd0bf8` completed on November 4, 2026: +**18 invocations, 18 downstream calls, 57 passing remote assertions**. +The [historical receipts and checks](evidence/same-user.json) are included. + +| Case | Observed C behavior | +|---|---| +| Omitted C context: 2 sequential + 6 concurrent calls | 8 distinct hosted sessions and contexts | +| Same explicit C context: sequential + concurrent | Same hosted session; both caller markers in its history | +| Same upstream context copied by A/B to C | A/B had separate sessions, but C reused one session | +| Different C context per branch, repeated | Separate sessions; each retained its own context history | + +For the shared case, both callers had user fingerprint +`3fa479bce41fc1b2a75d` and sent: + +```text +contextId: repro-9af72d559e114a51864d528caabd0bf8-shared-c +C hosted session: eeac7b653964cc989c6b26e2143e08da4dbec8a18c752e35b6225f3d28c3e95 +``` + +The ledger advanced from sequence 1 through 4. B's sequential request saw A's +earlier marker. A different immediate caller agent was not itself a separate +session boundary in this configuration. + +### Different real users via A and B + +Both completed successfully using C context `two-real-users-nov9-test01`. +Only selected non-secret receipt fields are retained: +[U1](evidence/real-user-u1.json) and [U2](evidence/real-user-u2.json). +U1 was recorded directly; U2's completed response was supplied by the other tester. + +| Field at C | U1 -> A -> C | U2 -> B -> C | +|---|---|---| +| UTC timestamp | 2026-11-09 16:08:50 | 2026-11-10 17:51:03 | +| User fingerprint | `3fa479bce41fc1b2a75d` | `8c98f58254de5d698ab0` | +| Context ID | `two-real-users-nov9-test01` | `two-real-users-nov9-test01` | +| Hosted session ID | `48b40392d95634cce5818562c55536b8596fd2f5f6df2d34bf73de3368c0145` | `f6a0aac7845d438e7db4ad8745900a43892360d6620c1dfb34fbb839e3ed49f` | +| Task ID | `203fe03e-2ec8-4cec-b775-7ad760cfabb4` | `1a038aa4-9078-4ae0-bfca-158514e1b296` | +| Sequence | 1 | 1 | +| Prior session/context messages | Empty | Empty | + +Each parent's user fingerprint matched C's fingerprint. The equal context labels +did not merge the two users' C sessions in these observations. +Verify the saved comparison without invoking Azure: + +```powershell +& .\src\agent\.venv\Scripts\python.exe .\run_real_user.py compare ` + .\evidence\real-user-u1.json .\evidence\real-user-u2.json +``` + +### Blocked and invalid attempts are not passes + +- A synthetic delegated-user attempt using `x-ms-user-identity` returned **403**: + "Caller is not authorized to delegate to another user." Normal access returned + 200. No delegation permissions were changed, and that attempt established no + cross-user routing result. +- U2's first inline-JSON attempt returned HTTP 200 with JSON-RPC error **-32700**. + Authorization/HTTP success was not a completed B -> C invocation. A corrected + file-based request produced the completed U2 receipt above. +- Early deployment versions failed because of build/dependency download issues. + The original cloud evidence used A version 2, B version 3 and C version 2. + Bundled dependencies avoid the observed remote-restore issue. The portable + manifest uses generic new names rather than embedding that deployment. + +### Conclusion and limits + +In the recorded configuration, **same user + same C context reused a C session**; +**different users + the same C context reached different C sessions**. +Use separate downstream context IDs for independent branches of one user's work. +Omit C's context ID for a fresh call; retain a branch's own ID for its follow-ups. + +These are observations, not proof that collisions or service bugs are impossible. +The real-user requests were roughly 26 hours apart: they were **not concurrent**. +Repeat-call real-user history isolation, cross-tenant boundaries and LLM +conversation mixing have not been validated. Matching labels alone, HTTP 200 +alone or a parent's `X-Agent-Session-Id` alone cannot establish C's behavior. + +## Troubleshooting and cleanup + +- JSON-RPC errors can arrive with HTTP 200. Require `TASK_STATE_COMPLETED` and + a diagnostic artifact; the Python client enforces both. +- 403 during A/B -> C: inspect only the new forwarder's runtime role and confirm + its platform call headers are forwarded. +- Different user fingerprints are a test precondition. Do not treat different + marker strings as different authenticated users. +- `results/`, local environments, bundles and deployment state are generated + locally. Do not replace historical evidence or commit credentials. +- Stop local helpers after tests. Delete only the three agents/sessions and + role assignments you created when finished, using your chosen names. + Do not run broad infrastructure deletion against a shared existing project. + +### CI boundary + +The shared cloud harness supports Responses and Invocations, not native A2A. +This sample has an exact-path cloud-deployment exclusion and uses its dedicated +JSON-RPC runners instead; it does not declare a misleading Responses test. +Dependency policy and unit/integration tests remain independently runnable. diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/azure.yaml b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/azure.yaml new file mode 100644 index 000000000..dc79274e3 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/azure.yaml @@ -0,0 +1,89 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +name: a2a-session-routing +infra: + provider: microsoft.foundry +requiredVersions: + azd: '>=1.37.2' + extensions: + azure.ai.agents: '>=1.0.0-beta.23' +services: + ai-project: + host: azure.ai.project + endpoint: ${AZURE_AI_PROJECT_ENDPOINT} + session-routing-c: + name: session-routing-c + host: azure.ai.agent + kind: hosted + language: python + project: src/agent + codeConfiguration: + runtime: python_3_13 + entryPoint: main.py + dependencyResolution: bundled + container: + resources: + cpu: "0.5" + memory: 1Gi + agentEndpoint: + protocols: [a2a] + protocols: + - protocol: a2a + version: 1.0.0 + env: + REPRO_ROLE: C + PYTHONNOUSERSITE: "1" + OTEL_INSTRUMENTATION_A2A_SDK_ENABLED: "false" + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "false" + uses: [ai-project] + session-routing-a: + name: session-routing-a + host: azure.ai.agent + kind: hosted + language: python + project: src/agent + codeConfiguration: + runtime: python_3_13 + entryPoint: main.py + dependencyResolution: bundled + container: + resources: + cpu: "0.5" + memory: 1Gi + agentEndpoint: + protocols: [a2a] + protocols: + - protocol: a2a + version: 1.0.0 + env: + REPRO_ROLE: A + C_AGENT_ENDPOINT: ${C_AGENT_ENDPOINT} + PYTHONNOUSERSITE: "1" + OTEL_INSTRUMENTATION_A2A_SDK_ENABLED: "false" + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "false" + uses: [ai-project] + session-routing-b: + name: session-routing-b + host: azure.ai.agent + kind: hosted + language: python + project: src/agent + codeConfiguration: + runtime: python_3_13 + entryPoint: main.py + dependencyResolution: bundled + container: + resources: + cpu: "0.5" + memory: 1Gi + agentEndpoint: + protocols: [a2a] + protocols: + - protocol: a2a + version: 1.0.0 + env: + REPRO_ROLE: B + C_AGENT_ENDPOINT: ${C_AGENT_ENDPOINT} + PYTHONNOUSERSITE: "1" + OTEL_INSTRUMENTATION_A2A_SDK_ENABLED: "false" + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "false" + uses: [ai-project] diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/real-user-u1.json b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/real-user-u1.json new file mode 100644 index 000000000..d012aec5d --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/real-user-u1.json @@ -0,0 +1,24 @@ +{ + "provenance": "Selected fields from recorded U1 invocation; no tokens or call identifiers", + "timestamp": "2026-11-09T16:08:50Z", + "task_id": "f60005dd-d5f6-484b-9795-bd8383d8e925", + "receipt": { + "agent": "A", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "hosted_session_id": "0eb75b045150f73300XkKgbN8xBbheAvWe29iSvcuTB9sbHYB2", + "downstream": { + "receipt": { + "agent": "C", + "task_id": "203fe03e-2ec8-4cec-b775-7ad760cfabb4", + "context_id": "two-real-users-nov9-test01", + "hosted_session_id": "48b40392d95634cce5818562c55536b8596fd2f5f6df2d34bf73de3368c0145", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "marker": "U1-via-A-nov9-test01", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + } + } +} diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/real-user-u2.json b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/real-user-u2.json new file mode 100644 index 000000000..a8ca1ce07 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/real-user-u2.json @@ -0,0 +1,24 @@ +{ + "provenance": "Selected fields from independently signed-in U2 output supplied by the tester; no tokens or call identifiers", + "timestamp": "2026-11-10T17:51:03Z", + "task_id": "06fe958c-660f-4379-8580-d41fb995af19", + "receipt": { + "agent": "B", + "user_fingerprint": "8c98f58254de5d698ab0", + "hosted_session_id": "0c2c31178841191500EqYqX29WD2pZTyxD9Nm3sRgz5whhrMLE", + "downstream": { + "receipt": { + "agent": "C", + "task_id": "1a038aa4-9078-4ae0-bfca-158514e1b296", + "context_id": "two-real-users-nov9-test01", + "hosted_session_id": "f6a0aac7845d438e7db4ad8745900a43892360d6620c1dfb34fbb839e3ed49f", + "user_fingerprint": "8c98f58254de5d698ab0", + "marker": "U2-via-B-nov9-test01", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "B" + } + } + } +} diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/same-user.json b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/same-user.json new file mode 100644 index 000000000..01723b6f8 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/evidence/same-user.json @@ -0,0 +1,1131 @@ +{ + "run_id": "repro-9af72d559e114a51864d528caabd0bf8", + "local": false, + "observations": [ + { + "label": "fresh-sequential", + "role": "A", + "task_id": "7b4eaca5-cdd1-4bb5-8f6f-f8de00f43239", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-sequential-A", + "task_id": "7b4eaca5-cdd1-4bb5-8f6f-f8de00f43239", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "2f0ddcc3cda9fcb2539f", + "downstream": { + "task_id": "5ee295c9-8098-41e0-b468-9f09e58a47e8", + "context_id": "1bcef731-9cc3-4e4e-83ee-f287a5e71481", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-sequential-A", + "task_id": "5ee295c9-8098-41e0-b468-9f09e58a47e8", + "context_id": "1bcef731-9cc3-4e4e-83ee-f287a5e71481", + "input_context_id": "1bcef731-9cc3-4e4e-83ee-f287a5e71481", + "hosted_session_id": "006577e3b75e990600DZ3tNs1fGR7MZZqBRITZeeaUJxD9r2Uz", + "hosted_session_guid": "e5a01cb61c0f48858081c478e3e45d63", + "request_session_id": null, + "boot_id": "f39512d3-42c6-4c3a-a34d-2da66176207f", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "a9ecdc52968cc0ef7a4b", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-sequential", + "role": "B", + "task_id": "a0efb37b-c338-4bf2-9655-d4b30dee58eb", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-sequential-B", + "task_id": "a0efb37b-c338-4bf2-9655-d4b30dee58eb", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "d698996b2b5c83069403", + "downstream": { + "task_id": "b2ab5e8e-a72f-4e30-95e6-43b2ce0834be", + "context_id": "f5fdee80-1bda-4973-80d8-45384c9adb09", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-sequential-B", + "task_id": "b2ab5e8e-a72f-4e30-95e6-43b2ce0834be", + "context_id": "f5fdee80-1bda-4973-80d8-45384c9adb09", + "input_context_id": "f5fdee80-1bda-4973-80d8-45384c9adb09", + "hosted_session_id": "00be169b395df1ad00JD2DDY5N3ciamVg2nwUQElxnW4HMJLvk", + "hosted_session_guid": "eb0ea4d79a93427eb7f34d6ea7598044", + "request_session_id": null, + "boot_id": "33fb614d-1bea-4e2b-905a-b50deb7c20d6", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "e7e82dc48b4c2239b1e2", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "B" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-concurrent-0", + "role": "A", + "task_id": "776032b2-4671-4451-b1a7-bdfab1c12184", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-0-A", + "task_id": "776032b2-4671-4451-b1a7-bdfab1c12184", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "5b38245b50ea58968a6a", + "downstream": { + "task_id": "18523d46-a7f1-4729-92e1-58be23c695af", + "context_id": "4401351d-5f40-4772-9690-3bb6edd628d0", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-0-A", + "task_id": "18523d46-a7f1-4729-92e1-58be23c695af", + "context_id": "4401351d-5f40-4772-9690-3bb6edd628d0", + "input_context_id": "4401351d-5f40-4772-9690-3bb6edd628d0", + "hosted_session_id": "0c99ad1a91df370300oSBK77Z9icAaOV0CBwaAo0CfqnI4hgbZ", + "hosted_session_guid": "f47cffe0b8a74db882f02be0fa281827", + "request_session_id": null, + "boot_id": "94a810f7-4a2a-4cc3-923d-46997dabc258", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "3530c3476c9f7775823a", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-concurrent-0", + "role": "B", + "task_id": "3b23ffb7-c9c2-408c-a217-c7df4a2471f2", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-0-B", + "task_id": "3b23ffb7-c9c2-408c-a217-c7df4a2471f2", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "cd055ddad81b0770913b", + "downstream": { + "task_id": "f4ec365e-5698-4c74-a675-575e8c77feef", + "context_id": "207f76fe-29c4-44bf-ba23-288052765b18", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-0-B", + "task_id": "f4ec365e-5698-4c74-a675-575e8c77feef", + "context_id": "207f76fe-29c4-44bf-ba23-288052765b18", + "input_context_id": "207f76fe-29c4-44bf-ba23-288052765b18", + "hosted_session_id": "082402fec04c560d00dOoA4WGpoxI3r80EUkuiaLqsQgYKdmur", + "hosted_session_guid": "57757556cf6b41e6840a05ad57b41f62", + "request_session_id": null, + "boot_id": "886ca022-e7bf-4c0f-9777-1963951f61bc", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "799fb905783d26d21b2a", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "B" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-concurrent-1", + "role": "B", + "task_id": "1b1aff92-d7b7-4b7b-af59-30d3f6afce0f", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-1-B", + "task_id": "1b1aff92-d7b7-4b7b-af59-30d3f6afce0f", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "38b22da55bc89ecaaf8e", + "downstream": { + "task_id": "6a3177ed-3618-4739-a4e0-b2d934768a23", + "context_id": "ecbf3a3d-2c1c-4420-8593-c8503d1908eb", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-1-B", + "task_id": "6a3177ed-3618-4739-a4e0-b2d934768a23", + "context_id": "ecbf3a3d-2c1c-4420-8593-c8503d1908eb", + "input_context_id": "ecbf3a3d-2c1c-4420-8593-c8503d1908eb", + "hosted_session_id": "040a845437f4759100DUqoY786TS5hbBQdwEuqgnNOssCUt03e", + "hosted_session_guid": "1e836c37dff54d8e8c0e6b6e5c91b62f", + "request_session_id": null, + "boot_id": "85344df2-953b-4869-b66c-b3de0d716d49", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "68463080ac7346c2f871", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "B" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-concurrent-1", + "role": "A", + "task_id": "3a0bbdc8-1be1-4815-98ae-2daadce0cdd1", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-1-A", + "task_id": "3a0bbdc8-1be1-4815-98ae-2daadce0cdd1", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "ba19b0a8928a477f3ecf", + "downstream": { + "task_id": "f41408c6-c0b8-4eed-b382-3cb9f6f18715", + "context_id": "5b8e291e-27d8-4eae-9f2c-09aeacae31d1", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-1-A", + "task_id": "f41408c6-c0b8-4eed-b382-3cb9f6f18715", + "context_id": "5b8e291e-27d8-4eae-9f2c-09aeacae31d1", + "input_context_id": "5b8e291e-27d8-4eae-9f2c-09aeacae31d1", + "hosted_session_id": "024efa574e8afbc500LZJ3pPrPFgPNC2ntvVADyd3l0vSMDy8L", + "hosted_session_guid": "376796796086465b82cf74ba4f040d7b", + "request_session_id": null, + "boot_id": "91bb831a-1b59-4a66-8b29-5d7b962450d0", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "60f87b16d90891ce7f93", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-concurrent-2", + "role": "B", + "task_id": "9aac583e-ab11-40b4-8e65-54ffeb02af59", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-2-B", + "task_id": "9aac583e-ab11-40b4-8e65-54ffeb02af59", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "6d36379a22895bf25a1b", + "downstream": { + "task_id": "c2869a6f-7ae6-4a6f-ab74-0b5fc9e8f49f", + "context_id": "89f64306-d62a-4ff2-b6a6-fd95e0b1ef29", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-2-B", + "task_id": "c2869a6f-7ae6-4a6f-ab74-0b5fc9e8f49f", + "context_id": "89f64306-d62a-4ff2-b6a6-fd95e0b1ef29", + "input_context_id": "89f64306-d62a-4ff2-b6a6-fd95e0b1ef29", + "hosted_session_id": "0db63d4ac63e4d3800KjRgDSxnbVH4Hd4pDb9QeOHy8bfu7Obm", + "hosted_session_guid": "772d9256c0ef4659825b97f81e92d366", + "request_session_id": null, + "boot_id": "e98a3605-382e-4c53-99d4-39fa6299d093", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "04489d3db930dd7b98cb", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "B" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "fresh-concurrent-2", + "role": "A", + "task_id": "cb74f863-4667-4bb7-bd19-9574d5487461", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-2-A", + "task_id": "cb74f863-4667-4bb7-bd19-9574d5487461", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "5ab86a030933af797dc7", + "downstream": { + "task_id": "8056eb3a-fda4-4fcb-b71e-2f3dbfd97fc8", + "context_id": "b88c3596-81b0-43fa-9454-25ae3a027542", + "requested_context_id": null, + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-fresh-concurrent-2-A", + "task_id": "8056eb3a-fda4-4fcb-b71e-2f3dbfd97fc8", + "context_id": "b88c3596-81b0-43fa-9454-25ae3a027542", + "input_context_id": "b88c3596-81b0-43fa-9454-25ae3a027542", + "hosted_session_id": "0d9b7bfe80d4faf300FTzVOzWPJyHAqTv5FIXvkShys7UqWIoI", + "hosted_session_guid": "41c632f9a34c4045b4d81b2e18f34bdb", + "request_session_id": null, + "boot_id": "94dcbff5-7e7a-405d-9e07-1cd3a918a520", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "622151e6ea024bbdb07b", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "fresh" + } + }, + { + "label": "shared-sequential", + "role": "A", + "task_id": "e01a9066-ff5e-4ce9-87ea-3a15083ed575", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "task_id": "e01a9066-ff5e-4ce9-87ea-3a15083ed575", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "f3e13e87c9fdcac26432", + "downstream": { + "task_id": "c59f5d12-015f-419a-beca-ae49bd58a336", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "task_id": "c59f5d12-015f-419a-beca-ae49bd58a336", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "hosted_session_id": "eeac7b653964cc989c6b26e2143e08da4dbec8a18c752e35b6225f3d28c3e95", + "hosted_session_guid": "d53ff9ac545c456db43849f00cd9d1e3", + "request_session_id": null, + "boot_id": "492a3c01-a780-431a-b94f-59672deeb923", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "c59bac4deadfbe9ea827", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "shared-sequential", + "role": "B", + "task_id": "2aca697d-a7bc-459f-8935-8594d4c4f2e7", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-B", + "task_id": "2aca697d-a7bc-459f-8935-8594d4c4f2e7", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "dc7e2888a785d3044087", + "downstream": { + "task_id": "ba6be92a-cc11-410f-965f-e71be6c2b2a5", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-B", + "task_id": "ba6be92a-cc11-410f-965f-e71be6c2b2a5", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "hosted_session_id": "eeac7b653964cc989c6b26e2143e08da4dbec8a18c752e35b6225f3d28c3e95", + "hosted_session_guid": "d53ff9ac545c456db43849f00cd9d1e3", + "request_session_id": null, + "boot_id": "492a3c01-a780-431a-b94f-59672deeb923", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "4e7e995aa3ec34b05d7a", + "sequence": 2, + "prior_session_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "caller_agent": "A" + } + ], + "prior_context_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "caller_agent": "A" + } + ], + "caller_agent": "B" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "shared-concurrent", + "role": "A", + "task_id": "8be5a927-614f-44ea-b6a3-f2b4dcf793de", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-concurrent-A", + "task_id": "8be5a927-614f-44ea-b6a3-f2b4dcf793de", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "028e4ed9bdb4c0f278ab", + "downstream": { + "task_id": "9b9b176c-35cb-4ec0-9608-59829f460a48", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-concurrent-A", + "task_id": "9b9b176c-35cb-4ec0-9608-59829f460a48", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "hosted_session_id": "eeac7b653964cc989c6b26e2143e08da4dbec8a18c752e35b6225f3d28c3e95", + "hosted_session_guid": "d53ff9ac545c456db43849f00cd9d1e3", + "request_session_id": null, + "boot_id": "492a3c01-a780-431a-b94f-59672deeb923", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "ead1e065ae4c206edbbd", + "sequence": 3, + "prior_session_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "caller_agent": "A" + }, + { + "sequence": 2, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-B", + "caller_agent": "B" + } + ], + "prior_context_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "caller_agent": "A" + }, + { + "sequence": 2, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-B", + "caller_agent": "B" + } + ], + "caller_agent": "A" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "shared-concurrent", + "role": "B", + "task_id": "23ad7435-d5b2-4b8a-97e1-ca7bbd28067d", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-concurrent-B", + "task_id": "23ad7435-d5b2-4b8a-97e1-ca7bbd28067d", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "85854076ad904e2d15fa", + "downstream": { + "task_id": "f21c4981-413b-4bad-bb53-cd0c64221d9f", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-concurrent-B", + "task_id": "f21c4981-413b-4bad-bb53-cd0c64221d9f", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "hosted_session_id": "eeac7b653964cc989c6b26e2143e08da4dbec8a18c752e35b6225f3d28c3e95", + "hosted_session_guid": "d53ff9ac545c456db43849f00cd9d1e3", + "request_session_id": null, + "boot_id": "492a3c01-a780-431a-b94f-59672deeb923", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "3e6339882b095f30b00a", + "sequence": 4, + "prior_session_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "caller_agent": "A" + }, + { + "sequence": 2, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-B", + "caller_agent": "B" + }, + { + "sequence": 3, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-concurrent-A", + "caller_agent": "A" + } + ], + "prior_context_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-A", + "caller_agent": "A" + }, + { + "sequence": 2, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-sequential-B", + "caller_agent": "B" + }, + { + "sequence": 3, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-shared-c", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-shared-concurrent-A", + "caller_agent": "A" + } + ], + "caller_agent": "B" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "inherit-same-context", + "role": "A", + "task_id": "d4b73b79-cf73-4d72-800f-c49713799254", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-inherit-same-context-A", + "task_id": "d4b73b79-cf73-4d72-800f-c49713799254", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "hosted_session_id": "e074e96126b1089c3912147e2616bcee0f31bc90667a347cfc01109f4413991", + "hosted_session_guid": "c66ce49a9664435eb7c9c4acf7e04578", + "request_session_id": null, + "boot_id": "46d453a9-5841-416b-9480-6de1ce8b5f23", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "1453402a11f4ba36ff9e", + "downstream": { + "task_id": "1f1fa34c-88d7-47d1-964e-0af762384a6f", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-inherit-same-context-A", + "task_id": "1f1fa34c-88d7-47d1-964e-0af762384a6f", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "hosted_session_id": "4e9ab3033ec97845802b0170544fd306f41c370d78df9b3817180cc2a69500e", + "hosted_session_guid": "4dc6a5e415c04835a277dcd8fb5eb5e0", + "request_session_id": null, + "boot_id": "4d550139-9e48-4e4d-b386-28dfd663400d", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "290336e6e1be0a40f394", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "inherit" + } + }, + { + "label": "inherit-same-context", + "role": "B", + "task_id": "bc8cdce2-1924-4fb4-acec-ea796d08bf4b", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-inherit-same-context-B", + "task_id": "bc8cdce2-1924-4fb4-acec-ea796d08bf4b", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "hosted_session_id": "89af2454eb4d16312a3cf6c0e4417a2756f2b46cbf7f6fc2b9e89182eeedeb3", + "hosted_session_guid": "f56fc023ceea4c039fdd6d941577e848", + "request_session_id": null, + "boot_id": "97d3357a-b6c0-4c2c-8fd9-58952d7f72b8", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "bb4d1b1c9da466184d04", + "downstream": { + "task_id": "7d25495d-43d1-4efa-b0dc-9736f72d9e5c", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-inherit-same-context-B", + "task_id": "7d25495d-43d1-4efa-b0dc-9736f72d9e5c", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "hosted_session_id": "4e9ab3033ec97845802b0170544fd306f41c370d78df9b3817180cc2a69500e", + "hosted_session_guid": "4dc6a5e415c04835a277dcd8fb5eb5e0", + "request_session_id": null, + "boot_id": "4d550139-9e48-4e4d-b386-28dfd663400d", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "24d28124e09b19cba194", + "sequence": 2, + "prior_session_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-inherit-same-context-A", + "caller_agent": "A" + } + ], + "prior_context_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-same-upstream", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-inherit-same-context-A", + "caller_agent": "A" + } + ], + "caller_agent": "B" + } + }, + "forwarding_mode": "inherit" + } + }, + { + "label": "unique-context", + "role": "A", + "task_id": "26e1814b-546d-4f6b-b44b-5f992ff5b08e", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-A", + "task_id": "26e1814b-546d-4f6b-b44b-5f992ff5b08e", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "14abe49a85cc0451b900", + "downstream": { + "task_id": "63198726-f716-4f51-8cc1-8588a2165b50", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-A", + "task_id": "63198726-f716-4f51-8cc1-8588a2165b50", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "hosted_session_id": "d754f4e8817b63e143900425dcd0d6e2693c530707bedbddf99f8cfd6bbda14", + "hosted_session_guid": "20f16511e82e427d9cd152ce8001c8ba", + "request_session_id": null, + "boot_id": "8f59beb6-81ae-4708-a91f-e12af1907658", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "6a37f8726f33b8e8f89f", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "A" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "unique-context", + "role": "B", + "task_id": "6c8a18c4-6269-4ea7-beba-830e67b696d3", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-B", + "task_id": "6c8a18c4-6269-4ea7-beba-830e67b696d3", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "003085fbea8521850690", + "downstream": { + "task_id": "2cde98fc-d090-488a-90d6-418d11850824", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-B", + "task_id": "2cde98fc-d090-488a-90d6-418d11850824", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "hosted_session_id": "7c8dc9eb2f33c6422b76d10f02446f5fa41912ec442ac246038a693f3b29b4b", + "hosted_session_guid": "4131c446eead406487eed7090712b9d7", + "request_session_id": null, + "boot_id": "a9f43d95-5564-4542-9fc1-78736635dfaf", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "8b2ba78752068dffa1f6", + "sequence": 1, + "prior_session_messages": [], + "prior_context_messages": [], + "caller_agent": "B" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "unique-context-repeat", + "role": "A", + "task_id": "f5a27f12-8f36-4777-9a11-4171aec627ef", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "receipt": { + "agent": "A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-repeat-A", + "task_id": "f5a27f12-8f36-4777-9a11-4171aec627ef", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-A", + "hosted_session_id": "b0030f4cfea77f8da60020ec4fc9dc4ace24d141f1af79b576f99b2632880a6", + "hosted_session_guid": "18a6938603bb4e409a6d7129eb7529a0", + "request_session_id": null, + "boot_id": "d10e4ffd-e4e2-45d0-b633-d2a02aa73c30", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "fd681cfccd349e3b72c4", + "downstream": { + "task_id": "2d255690-4667-42dd-af99-6ab7638377c7", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-repeat-A", + "task_id": "2d255690-4667-42dd-af99-6ab7638377c7", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "hosted_session_id": "d754f4e8817b63e143900425dcd0d6e2693c530707bedbddf99f8cfd6bbda14", + "hosted_session_guid": "20f16511e82e427d9cd152ce8001c8ba", + "request_session_id": null, + "boot_id": "8f59beb6-81ae-4708-a91f-e12af1907658", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "e3830c8f3de900c7ab41", + "sequence": 2, + "prior_session_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-A", + "caller_agent": "A" + } + ], + "prior_context_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-A", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-A", + "caller_agent": "A" + } + ], + "caller_agent": "A" + } + }, + "forwarding_mode": "explicit" + } + }, + { + "label": "unique-context-repeat", + "role": "B", + "task_id": "779eaaa7-6850-4422-9bc4-245add0a7444", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "receipt": { + "agent": "B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-repeat-B", + "task_id": "779eaaa7-6850-4422-9bc4-245add0a7444", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-outer-B", + "hosted_session_id": "63612269eb59de56d12e99135be8445ab0840a880c0ea725211640bf1108a79", + "hosted_session_guid": "9c593201a136401b9e94985125e9fe85", + "request_session_id": null, + "boot_id": "4c92a6c1-35e9-46d8-afe2-5e4daadf394a", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "964a7184e1373b52f7ff", + "downstream": { + "task_id": "ade10468-b86c-41a0-8a8e-140eaf6a1d30", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "requested_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "receipt": { + "agent": "C", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-repeat-B", + "task_id": "ade10468-b86c-41a0-8a8e-140eaf6a1d30", + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "input_context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "hosted_session_id": "7c8dc9eb2f33c6422b76d10f02446f5fa41912ec442ac246038a693f3b29b4b", + "hosted_session_guid": "4131c446eead406487eed7090712b9d7", + "request_session_id": null, + "boot_id": "a9f43d95-5564-4542-9fc1-78736635dfaf", + "user_fingerprint": "3fa479bce41fc1b2a75d", + "call_fingerprint": "5db0f36bd39fb7d555f6", + "sequence": 2, + "prior_session_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-B", + "caller_agent": "B" + } + ], + "prior_context_messages": [ + { + "sequence": 1, + "context_id": "repro-9af72d559e114a51864d528caabd0bf8-c-B", + "marker": "repro-9af72d559e114a51864d528caabd0bf8-unique-context-B", + "caller_agent": "B" + } + ], + "caller_agent": "B" + } + }, + "forwarding_mode": "explicit" + } + } + ], + "checks": [ + { + "name": "fresh-sequential: C context equality is False", + "passed": true + }, + { + "name": "fresh-sequential: distinct C task IDs", + "passed": true + }, + { + "name": "fresh-sequential: actual hosted session IDs are present", + "passed": true + }, + { + "name": "fresh-sequential: C hosted session equality is False", + "passed": true + }, + { + "name": "fresh-sequential: same resolved user", + "passed": true + }, + { + "name": "fresh-sequential: no cross-caller context history", + "passed": true + }, + { + "name": "fresh-concurrent-0: C context equality is False", + "passed": true + }, + { + "name": "fresh-concurrent-0: distinct C task IDs", + "passed": true + }, + { + "name": "fresh-concurrent-0: actual hosted session IDs are present", + "passed": true + }, + { + "name": "fresh-concurrent-0: C hosted session equality is False", + "passed": true + }, + { + "name": "fresh-concurrent-0: same resolved user", + "passed": true + }, + { + "name": "fresh-concurrent-0: no cross-caller context history", + "passed": true + }, + { + "name": "fresh-concurrent-1: C context equality is False", + "passed": true + }, + { + "name": "fresh-concurrent-1: distinct C task IDs", + "passed": true + }, + { + "name": "fresh-concurrent-1: actual hosted session IDs are present", + "passed": true + }, + { + "name": "fresh-concurrent-1: C hosted session equality is False", + "passed": true + }, + { + "name": "fresh-concurrent-1: same resolved user", + "passed": true + }, + { + "name": "fresh-concurrent-1: no cross-caller context history", + "passed": true + }, + { + "name": "fresh-concurrent-2: C context equality is False", + "passed": true + }, + { + "name": "fresh-concurrent-2: distinct C task IDs", + "passed": true + }, + { + "name": "fresh-concurrent-2: actual hosted session IDs are present", + "passed": true + }, + { + "name": "fresh-concurrent-2: C hosted session equality is False", + "passed": true + }, + { + "name": "fresh-concurrent-2: same resolved user", + "passed": true + }, + { + "name": "fresh-concurrent-2: no cross-caller context history", + "passed": true + }, + { + "name": "shared-sequential: C context equality is True", + "passed": true + }, + { + "name": "shared-sequential: distinct C task IDs", + "passed": true + }, + { + "name": "shared-sequential: actual hosted session IDs are present", + "passed": true + }, + { + "name": "shared-sequential: C hosted session equality is True", + "passed": true + }, + { + "name": "shared-sequential: same resolved user", + "passed": true + }, + { + "name": "shared-sequential: C saw the other caller's marker", + "passed": true + }, + { + "name": "shared-concurrent: C context equality is True", + "passed": true + }, + { + "name": "shared-concurrent: distinct C task IDs", + "passed": true + }, + { + "name": "shared-concurrent: actual hosted session IDs are present", + "passed": true + }, + { + "name": "shared-concurrent: C hosted session equality is True", + "passed": true + }, + { + "name": "shared-concurrent: same resolved user", + "passed": true + }, + { + "name": "shared-concurrent: C saw the other caller's marker", + "passed": true + }, + { + "name": "inherit-same-context: C context equality is True", + "passed": true + }, + { + "name": "inherit-same-context: distinct C task IDs", + "passed": true + }, + { + "name": "inherit-same-context: actual hosted session IDs are present", + "passed": true + }, + { + "name": "inherit-same-context: C hosted session equality is True", + "passed": true + }, + { + "name": "inherit-same-context: same resolved user", + "passed": true + }, + { + "name": "inherit-same-context: C saw the other caller's marker", + "passed": true + }, + { + "name": "unique-context: C context equality is False", + "passed": true + }, + { + "name": "unique-context: distinct C task IDs", + "passed": true + }, + { + "name": "unique-context: actual hosted session IDs are present", + "passed": true + }, + { + "name": "unique-context: C hosted session equality is False", + "passed": true + }, + { + "name": "unique-context: same resolved user", + "passed": true + }, + { + "name": "unique-context: no cross-caller context history", + "passed": true + }, + { + "name": "unique-context-repeat: C context equality is False", + "passed": true + }, + { + "name": "unique-context-repeat: distinct C task IDs", + "passed": true + }, + { + "name": "unique-context-repeat: actual hosted session IDs are present", + "passed": true + }, + { + "name": "unique-context-repeat: C hosted session equality is False", + "passed": true + }, + { + "name": "unique-context-repeat: same resolved user", + "passed": true + }, + { + "name": "unique-context-repeat: no cross-caller context history", + "passed": true + }, + { + "name": "each unique C context retains its own prior marker", + "passed": true + }, + { + "name": "all eight default downstream context IDs are distinct", + "passed": true + }, + { + "name": "all eight default C hosted sessions are distinct", + "passed": true + } + ], + "status": "passed" +} \ No newline at end of file diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/prepare_bundle.ps1 b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/prepare_bundle.ps1 new file mode 100644 index 000000000..bc090763f --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/prepare_bundle.ps1 @@ -0,0 +1,26 @@ +$ErrorActionPreference = 'Stop' +$source = Join-Path $PSScriptRoot 'src\agent' +$wheels = Join-Path $source 'wheelhouse' +$packages = Join-Path $source 'packages' +$feed = 'https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/' +$dependencies = Get-Content -LiteralPath (Join-Path $source 'requirements.txt') | + Where-Object { $_ -notmatch '^\s*(#|--|$)' } + +python -m pip download --dest $wheels --no-deps --index-url $feed ` + 'azure-ai-agentserver-a2a==1.0.0a20261029006' ` + 'azure-ai-agentserver-core==2.3.1a20261029006' --no-input +if ($LASTEXITCODE -ne 0) { throw 'Azure SDK wheel download failed' } + +python -m pip download --only-binary=:all: ` + --platform manylinux2014_x86_64 --platform manylinux_2_28_x86_64 ` + --python-version 3.13 --implementation cp --abi cp313 ` + --dest $wheels --find-links $wheels --index-url 'https://pypi.org/simple/' ` + @dependencies --no-input +if ($LASTEXITCODE -ne 0) { throw 'Linux dependency download failed' } + +python -m pip install --target $packages --upgrade --ignore-installed ` + --no-index --find-links $wheels --only-binary=:all: ` + --platform manylinux2014_x86_64 --platform manylinux_2_28_x86_64 ` + --python-version 3.13 --implementation cp --abi cp313 ` + @dependencies --no-input +if ($LASTEXITCODE -ne 0) { throw 'Linux dependency bundle creation failed' } diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/run_experiment.py b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/run_experiment.py new file mode 100644 index 000000000..06fde5e8d --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/run_experiment.py @@ -0,0 +1,138 @@ +"""Run real sequential/concurrent A -> C and B -> C calls and save receipts.""" + +import argparse +import asyncio +import json +import sys +from pathlib import Path +from uuid import uuid4 + +sys.path.insert(0, str(Path(__file__).resolve().parent / "src" / "agent")) + +from azure.identity.aio import DefaultAzureCredential +from client import send + + +async def experiment(a: str, b: str, output: Path, local: bool = False) -> dict: + run_id = "repro-" + uuid4().hex + results: dict = {"run_id": run_id, "local": local, "observations": [], "checks": []} + output.parent.mkdir(parents=True, exist_ok=True) + + def save() -> None: + output.write_text(json.dumps(results, indent=2), encoding="utf-8") + + def check(name: str, condition: bool) -> None: + results["checks"].append({"name": name, "passed": condition}) + save() + if not condition: + raise AssertionError(name) + + async with DefaultAzureCredential() as credential: + async def invoke(role: str, label: str, mode: str, c_context: str | None = None, outer: str | None = None) -> dict: + payload = {"marker": f"{run_id}-{label}-{role}", "mode": mode} + if c_context is not None: + payload["c_context_id"] = c_context + reply = await send( + a if role == "A" else b, + payload, + outer or f"{run_id}-outer-{role}", + credential, + {}, + ) + observation = {"label": label, "role": role, **reply} + results["observations"].append(observation) + save() + return reply["receipt"]["downstream"]["receipt"] + + def verify_pair(label: str, pair: list[dict], shared: bool) -> None: + left, right = pair + check(f"{label}: C context equality is {shared}", + (left["context_id"] == right["context_id"]) == shared) + check(f"{label}: distinct C task IDs", left["task_id"] != right["task_id"]) + if not local: + check(f"{label}: actual hosted session IDs are present", + bool(left["hosted_session_id"]) and bool(right["hosted_session_id"])) + check(f"{label}: C hosted session equality is {shared}", + (left["hosted_session_id"] == right["hosted_session_id"]) == shared) + check(f"{label}: same resolved user", + bool(left["user_fingerprint"]) and left["user_fingerprint"] == right["user_fingerprint"]) + markers = {left["marker"], right["marker"]} + if shared: + check(f"{label}: C saw the other caller's marker", + any(item["marker"] in markers - {receipt["marker"]} + for receipt in pair for item in receipt["prior_context_messages"])) + else: + check(f"{label}: no cross-caller context history", + all(item["marker"] not in markers - {receipt["marker"]} + for receipt in pair for item in receipt["prior_context_messages"])) + + try: + pair = [await invoke("A", "fresh-sequential", "fresh"), + await invoke("B", "fresh-sequential", "fresh")] + verify_pair("fresh-sequential", pair, False) + for index in range(3): + label = f"fresh-concurrent-{index}" + pair = list(await asyncio.gather(invoke("A", label, "fresh"), invoke("B", label, "fresh"))) + verify_pair(label, pair, False) + + shared = f"{run_id}-shared-c" + pair = [await invoke("A", "shared-sequential", "explicit", shared), + await invoke("B", "shared-sequential", "explicit", shared)] + verify_pair("shared-sequential", pair, True) + pair = list(await asyncio.gather( + invoke("A", "shared-concurrent", "explicit", shared), + invoke("B", "shared-concurrent", "explicit", shared), + )) + verify_pair("shared-concurrent", pair, True) + + outer = f"{run_id}-same-upstream" + pair = [await invoke("A", "inherit-same-context", "inherit", outer=outer), + await invoke("B", "inherit-same-context", "inherit", outer=outer)] + verify_pair("inherit-same-context", pair, True) + + unique = [ + await invoke("A", "unique-context", "explicit", f"{run_id}-c-A"), + await invoke("B", "unique-context", "explicit", f"{run_id}-c-B"), + ] + verify_pair("unique-context", unique, False) + repeat = [ + await invoke("A", "unique-context-repeat", "explicit", f"{run_id}-c-A"), + await invoke("B", "unique-context-repeat", "explicit", f"{run_id}-c-B"), + ] + verify_pair("unique-context-repeat", repeat, False) + check("each unique C context retains its own prior marker", + all(any(item["marker"] == before["marker"] for item in after["prior_context_messages"]) + for before, after in zip(unique, repeat))) + fresh_contexts = [ + item["receipt"]["downstream"]["context_id"] + for item in results["observations"] if item["label"].startswith("fresh-") + ] + check("all eight default downstream context IDs are distinct", len(set(fresh_contexts)) == 8) + if not local: + fresh_sessions = [ + item["receipt"]["downstream"]["receipt"]["hosted_session_id"] + for item in results["observations"] if item["label"].startswith("fresh-") + ] + check("all eight default C hosted sessions are distinct", len(set(fresh_sessions)) == 8) + results["status"] = "passed" + except Exception as error: + results["status"] = "failed" + results["error"] = f"{type(error).__name__}: {error}" + save() + raise + finally: + save() + print(json.dumps({"status": results["status"], "run_id": run_id, + "observations": len(results["observations"]), "checks": len(results["checks"]), + "evidence": str(output)}, indent=2)) + return results + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--a", required=True) + parser.add_argument("--b", required=True) + parser.add_argument("--output", type=Path, default=Path("results") / "remote.json") + parser.add_argument("--local", action="store_true") + args = parser.parse_args() + asyncio.run(experiment(args.a, args.b, args.output, args.local)) diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/run_real_user.py b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/run_real_user.py new file mode 100644 index 000000000..beb90ab6d --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/run_real_user.py @@ -0,0 +1,76 @@ +"""Invoke under an independent Azure CLI sign-in, or compare two saved receipts.""" + +import argparse +import asyncio +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent / "src" / "agent")) + +from azure.identity.aio import AzureCliCredential +from client import send + + +def compare(left: dict, right: dict) -> dict: + parents = [item["receipt"] for item in (left, right)] + calls = [item["downstream"]["receipt"] for item in parents] + first, second = calls + checks = { + "same_nonempty_C_context": bool(first["context_id"]) + and first["context_id"] == second["context_id"], + "different_nonempty_C_users": bool(first["user_fingerprint"]) + and bool(second["user_fingerprint"]) + and first["user_fingerprint"] != second["user_fingerprint"], + "different_nonempty_C_sessions": bool(first["hosted_session_id"]) + and bool(second["hosted_session_id"]) + and first["hosted_session_id"] != second["hosted_session_id"], + "user_preserved_through_each_parent": all( + parent["user_fingerprint"] == call["user_fingerprint"] + for parent, call in zip(parents, calls) + ), + "distinct_C_task_ids": bool(first["task_id"]) + and bool(second["task_id"]) + and first["task_id"] != second["task_id"], + "no_other_marker_in_session_history": all( + item["marker"] != other["marker"] + for call, other in ((first, second), (second, first)) + for item in call["prior_session_messages"] + ), + } + return {"passed": all(checks.values()), "checks": checks} + + +async def invoke(args: argparse.Namespace) -> None: + # Bind this test explicitly to the current CLI sign-in, not environment credentials. + async with AzureCliCredential() as credential: + reply = await send( + args.endpoint, + {"marker": args.marker, "mode": "explicit", "c_context_id": args.context}, + args.context, + credential, + {}, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(reply, indent=2), encoding="utf-8") + print(json.dumps(reply["receipt"]["downstream"]["receipt"], indent=2)) + print(f"Saved receipt: {args.output}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + commands = parser.add_subparsers(dest="command", required=True) + call = commands.add_parser("invoke") + call.add_argument("--endpoint", required=True) + call.add_argument("--context", required=True) + call.add_argument("--marker", required=True) + call.add_argument("--output", type=Path, required=True) + comparison = commands.add_parser("compare") + comparison.add_argument("receipts", nargs=2, type=Path) + args = parser.parse_args() + if args.command == "invoke": + asyncio.run(invoke(args)) + else: + result = compare(*(json.loads(path.read_text(encoding="utf-8")) for path in args.receipts)) + print(json.dumps(result, indent=2)) + sys.exit(0 if result["passed"] else 1) diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/.agentignore b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/.agentignore new file mode 100644 index 000000000..39f57c4c6 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/.agentignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.sqlite* +wheelhouse/ diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/client.py b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/client.py new file mode 100644 index 000000000..3b5fe7af3 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/client.py @@ -0,0 +1,90 @@ +"""Use the native A2A 1.0 client with request-scoped platform identity.""" + +import json +from contextlib import AsyncExitStack +from urllib.parse import urlsplit, urlunsplit + +import httpx +from a2a.client import A2ACardResolver, ClientConfig, create_client +from a2a.helpers import get_artifact_text, new_text_message +from a2a.types import Role, SendMessageRequest, TaskState +from azure.core.credentials_async import AsyncTokenCredential +from opentelemetry import propagate + + +async def send( + endpoint: str, + payload: dict, + context_id: str | None, + credential: AsyncTokenCredential, + platform_headers: dict[str, str], +) -> dict: + parsed = urlsplit(endpoint) + if parsed.scheme != "https" and not ( + parsed.scheme == "http" and parsed.hostname in {"127.0.0.1", "localhost"} + ): + raise ValueError("A2A endpoint must be HTTPS or local loopback") + base = urlunsplit(parsed._replace(query="")).rstrip("/") + + async def authenticate(request: httpx.Request) -> None: + if request.url.host != parsed.hostname: + raise ValueError("Agent Card changed the target host") + if request.url.scheme == "https": + token = await credential.get_token("https://ai.azure.com/.default") + request.headers["Authorization"] = "Bearer " + token.token + request.headers.update(platform_headers) + request.headers["A2A-Version"] = "1.0" + propagate.inject(request.headers) + + async with AsyncExitStack() as stack: + http = await stack.enter_async_context( + httpx.AsyncClient( + timeout=300, + event_hooks={"request": [authenticate]}, + ) + ) + card = await A2ACardResolver(http, base).get_agent_card( + relative_card_path=( + "agentCard/v1.0?api-version=v1" + if parsed.scheme == "https" + else ".well-known/agent-card.json" + ) + ) + interfaces = [ + item + for item in card.supported_interfaces + if item.protocol_binding == "JSONRPC" and item.protocol_version == "1.0" + ] + if not interfaces: + raise RuntimeError("Agent Card has no JSONRPC A2A 1.0 interface") + interface = interfaces[0] + if urlsplit(interface.url).hostname != parsed.hostname: + raise ValueError("Agent Card changed the target host") + if parsed.scheme == "https": + if interface.url.split("?")[0].rstrip("/") != base: + raise ValueError("Agent Card changed the configured Foundry endpoint") + if not urlsplit(interface.url).query: + interface.url += "?api-version=v1" + del card.supported_interfaces[:] + card.supported_interfaces.append(interface) + client = await create_client(card, ClientConfig(streaming=False, httpx_client=http)) + stack.push_async_callback(client.close) + message = new_text_message(json.dumps(payload), role=Role.ROLE_USER) + if context_id is not None: + message.context_id = context_id + tasks = [ + event.task + async for event in client.send_message(SendMessageRequest(message=message)) + if event.HasField("task") + ] + if len(tasks) != 1 or tasks[0].status.state != TaskState.TASK_STATE_COMPLETED: + raise RuntimeError(f"Expected one completed task, received {tasks!r}") + task = tasks[0] + if len(task.artifacts) != 1: + raise RuntimeError("Expected exactly one diagnostic artifact") + return { + "task_id": task.id, + "context_id": task.context_id, + "requested_context_id": context_id, + "receipt": json.loads(get_artifact_text(task.artifacts[0])), + } diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/main.py b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/main.py new file mode 100644 index 000000000..15ffebbdd --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/main.py @@ -0,0 +1,172 @@ +"""Deterministic A/B forwarders and C session observer; no model required.""" + +import hashlib +import json +import logging +import os +import sqlite3 +from contextlib import closing +from pathlib import Path +from uuid import uuid4 + +os.environ.setdefault("OTEL_INSTRUMENTATION_A2A_SDK_ENABLED", "false") + +from a2a.helpers import get_message_text, new_task_from_user_message, new_text_part +from a2a.server.agent_execution import AgentExecutor, RequestContext +from a2a.server.events import EventQueue +from a2a.server.tasks import TaskUpdater +from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill +from a2a.utils.errors import TaskNotCancelableError +from azure.ai.agentserver.a2a import A2AAgentServerHost +from azure.ai.agentserver.core import AgentConfig, get_request_context +from azure.identity.aio import DefaultAzureCredential, ManagedIdentityCredential +from client import send + +logger = logging.getLogger("session-repro") +BOOT_ID = str(uuid4()) + + +def digest(value: str | None) -> str | None: + return hashlib.sha256(value.encode()).hexdigest()[:20] if value else None + + +def downstream_context(payload: dict, incoming_context: str) -> str | None: + mode = payload.get("mode", "fresh") + if mode == "fresh": + return None + if mode == "inherit": + if not incoming_context: + raise ValueError("inherit requires an incoming context ID") + return incoming_context + if mode == "explicit": + value = payload.get("c_context_id") + if isinstance(value, str) and value: + return value + raise ValueError("explicit requires c_context_id") + raise ValueError(f"Unknown forwarding mode: {mode}") + + +def record(database: Path, context_id: str, payload: dict) -> dict: + database.parent.mkdir(parents=True, exist_ok=True) + with closing(sqlite3.connect(database, timeout=30, isolation_level=None)) as db: + db.execute( + "CREATE TABLE IF NOT EXISTS calls " + "(sequence INTEGER PRIMARY KEY, context_id TEXT NOT NULL, payload TEXT NOT NULL)" + ) + db.execute("BEGIN IMMEDIATE") + previous = [ + {"sequence": row[0], "context_id": row[1], **json.loads(row[2])} + for row in db.execute("SELECT sequence, context_id, payload FROM calls ORDER BY sequence") + ] + cursor = db.execute( + "INSERT INTO calls(context_id, payload) VALUES (?, ?)", + (context_id, json.dumps(payload)), + ) + sequence = cursor.lastrowid + db.execute("COMMIT") + return { + "sequence": sequence, + "prior_session_messages": previous, + "prior_context_messages": [ + item for item in previous if item["context_id"] == context_id + ], + } + + +class RoutingExecutor(AgentExecutor): + async def execute(self, context: RequestContext, event_queue: EventQueue) -> None: + if context.message is None: + raise ValueError("A2A message is required") + payload = json.loads(get_message_text(context.message)) + if not isinstance(payload, dict) or not isinstance(payload.get("marker"), str): + raise ValueError("Expected an object with a synthetic marker string") + role = os.environ["REPRO_ROLE"] + if role not in {"A", "B", "C"}: + raise ValueError("REPRO_ROLE must be A, B or C") + task = new_task_from_user_message(context.message) + await event_queue.enqueue_event(task) + updater = TaskUpdater(event_queue, task.id, task.context_id) + await updater.start_work() + caller = get_request_context() + receipt = { + "agent": role, + "marker": payload["marker"], + "task_id": task.id, + "context_id": task.context_id, + "input_context_id": context.message.context_id, + "hosted_session_id": os.environ.get("FOUNDRY_AGENT_SESSION_ID"), + "hosted_session_guid": os.environ.get("FOUNDRY_AGENT_SESSION_GUID"), + "request_session_id": caller.session_id, + "boot_id": BOOT_ID, + "user_fingerprint": digest(caller.user_id), + "call_fingerprint": digest(caller.call_id), + } + try: + if role == "C": + # HOME is platform-session scoped; this ledger exposes actual reuse. + database = Path(os.environ.get("REPRO_STATE_DIR", str(Path.home()))) / "session-repro.sqlite" + receipt.update(record(database, task.context_id, payload)) + receipt["caller_agent"] = payload.get("caller_agent") + else: + c_context = downstream_context(payload, task.context_id) + hosted = bool(os.environ.get("FOUNDRY_HOSTING_ENVIRONMENT")) + credential = ( + ManagedIdentityCredential( + client_id=os.environ["FOUNDRY_AGENT_INSTANCE_CLIENT_ID"] + ) + if hosted + else DefaultAzureCredential() + ) + async with credential: + receipt["downstream"] = await send( + os.environ["C_AGENT_ENDPOINT"], + {"marker": payload["marker"], "caller_agent": role}, + c_context, + credential, + caller.platform_headers(), + ) + receipt["forwarding_mode"] = payload.get("mode", "fresh") + await updater.add_artifact( + parts=[new_text_part(json.dumps(receipt), media_type="application/json")], + name="session-observation", + ) + await updater.complete() + logger.info("SESSION_OBSERVATION %s", json.dumps(receipt)) + except Exception: + logger.exception("SESSION_REPRO_FAILED role=%s task=%s", role, task.id) + await updater.failed() + raise + + async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None: + raise TaskNotCancelableError(message="This diagnostic agent does not support cancellation") + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + config = AgentConfig.from_env() + role = os.environ["REPRO_ROLE"] + card = AgentCard( + name=f"Session Repro {role}", + description="Deterministic A2A session routing observer", + version="1.0.0", + capabilities=AgentCapabilities(), + default_input_modes=["text/plain"], + default_output_modes=["application/json"], + supported_interfaces=[ + AgentInterface( + protocol_binding="JSONRPC", + protocol_version="1.0", + url=f"http://127.0.0.1:{config.port}/a2a", + ) + ], + skills=[ + AgentSkill( + id="observe", name="Observe session routing", + description="Accept JSON with marker, mode and optional c_context_id", + tags=["diagnostics"], + ) + ], + ) + A2AAgentServerHost(agent_card=card, agent_executor=RoutingExecutor()).run( + host="0.0.0.0" if config.is_hosted else "127.0.0.1" + ) diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/requirements.txt b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/requirements.txt new file mode 100644 index 000000000..be2472f35 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/src/agent/requirements.txt @@ -0,0 +1,91 @@ +# Fully resolved from the isolated, tested runtime using: python -m pip freeze +# Public preview feed configuration belongs in install commands, not this file. +a2a-sdk==1.2.3 +aiohappyeyeballs==2.7.1 +aiohttp==3.14.5 +aiologic==0.17.1 +aiosignal==1.4.0 +annotated-types==0.8.0 +anyio==4.15.1 +asgiref==3.13.0 +attrs==26.1.0 +azure-ai-agentserver-a2a==1.0.0a20261029006 +azure-ai-agentserver-core==2.3.1a20261029006 +azure-core==1.41.0 +azure-core-tracing-opentelemetry==1.0.0b13 +azure-identity==1.25.1 +azure-monitor-opentelemetry-exporter==1.0.0b58 +certifi==2026.10.23 +cffi==2.1.1 +charset-normalizer==3.5.2 +cryptography==50.0.2 +culsans==0.11.0 +frozenlist==1.8.0 +google-api-core==2.43.0 +google-auth==2.62.0 +googleapis-common-protos==1.76.0 +h11==0.16.0 +h2==4.4.1 +hpack==4.2.0 +httpcore==1.0.9 +httpx==0.28.1 +Hypercorn==0.18.0 +hyperframe==6.1.0 +idna==3.20 +isodate==0.7.2 +json-rpc==1.15.0 +microsoft-opentelemetry==1.5.0 +msal==1.39.0 +msal-extensions==1.3.1 +msrest==0.7.1 +multidict==7.2.1 +oauthlib==4.0.1 +opentelemetry-api==1.45.1 +opentelemetry-exporter-http-transport==0.66b1 +opentelemetry-exporter-otlp-common==0.66b1 +opentelemetry-exporter-otlp-proto-common==1.45.1 +opentelemetry-exporter-otlp-proto-http==1.45.1 +opentelemetry-instrumentation==0.66b1 +opentelemetry-instrumentation-asgi==0.66b1 +opentelemetry-instrumentation-dbapi==0.66b1 +opentelemetry-instrumentation-django==0.66b1 +opentelemetry-instrumentation-fastapi==0.66b1 +opentelemetry-instrumentation-flask==0.66b1 +opentelemetry-instrumentation-httpx==0.66b1 +opentelemetry-instrumentation-logging==0.66b1 +opentelemetry-instrumentation-openai-agents-v2==0.1.0 +opentelemetry-instrumentation-openai-v2==2.3b0 +opentelemetry-instrumentation-psycopg2==0.66b1 +opentelemetry-instrumentation-requests==0.66b1 +opentelemetry-instrumentation-urllib==0.66b1 +opentelemetry-instrumentation-urllib3==0.66b1 +opentelemetry-instrumentation-wsgi==0.66b1 +opentelemetry-proto==1.45.1 +opentelemetry-resource-detector-azure==0.3.0 +opentelemetry-sdk==1.45.1 +opentelemetry-semantic-conventions==0.66b1 +opentelemetry-util-genai==0.3b0 +opentelemetry-util-http==0.66b1 +packaging==26.3 +priority==2.0.0 +propcache==0.5.4 +proto-plus==1.29.0 +protobuf==7.36.2 +psutil==7.2.2 +pyasn1==0.6.5 +pyasn1_modules==0.4.2 +pycparser==3.12 +pydantic==2.14.1 +pydantic_core==2.50.1 +PyJWT==2.15.1 +requests==2.34.2 +requests-oauthlib==2.0.0 +sse-starlette==3.6.0 +sniffio==1.3.1 +starlette==1.10.0 +typing-inspection==0.4.4 +typing_extensions==4.16.0 +urllib3==2.8.0 +wrapt==1.17.3 +wsproto==1.3.2 +yarl==1.25.1 diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/test_local.py b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/test_local.py new file mode 100644 index 000000000..9fd7dc9c5 --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/test_local.py @@ -0,0 +1,65 @@ +"""Own three temporary local servers, run the experiment and clean them up.""" + +import asyncio +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +import httpx + +from run_experiment import experiment + + +async def main() -> None: + source = Path(__file__).parent / "src" / "agent" + processes: list[subprocess.Popen] = [] + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + logs = [] + try: + for role, port in [("C", 18193), ("A", 18191), ("B", 18192)]: + log = (root / f"{role}.log").open("w+", encoding="utf-8") + logs.append(log) + environment = { + **os.environ, + "REPRO_ROLE": role, + "PORT": str(port), + "REPRO_STATE_DIR": str(root / role), + "C_AGENT_ENDPOINT": "http://127.0.0.1:18193/a2a", + } + process = subprocess.Popen( + [sys.executable, str(source / "main.py")], + cwd=source, env=environment, stdout=log, stderr=subprocess.STDOUT, + ) + processes.append(process) + async with httpx.AsyncClient() as http: + for _ in range(120): + if process.poll() is not None: + log.seek(0) + raise RuntimeError(f"{role} exited: {log.read()[-6000:]}") + try: + response = await http.get(f"http://127.0.0.1:{port}/readiness") + if response.status_code == 200: + break + except httpx.ConnectError: + pass + await asyncio.sleep(0.5) + else: + raise TimeoutError(f"{role} did not become ready") + await experiment( + "http://127.0.0.1:18191/a2a", "http://127.0.0.1:18192/a2a", + Path(__file__).parent / "results" / "local.json", local=True, + ) + finally: + for process in processes: + if process.poll() is None: + process.terminate() + process.wait(timeout=30) + for log in logs: + log.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/samples/python/hosted-agents/bring-your-own/a2a/session-routing/tests/test_routing.py b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/tests/test_routing.py new file mode 100644 index 000000000..df61522da --- /dev/null +++ b/samples/python/hosted-agents/bring-your-own/a2a/session-routing/tests/test_routing.py @@ -0,0 +1,74 @@ +import json +import copy +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src" / "agent")) + +from main import downstream_context, record + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from run_real_user import compare + + +class RoutingTests(unittest.TestCase): + def real_user_receipts(self): + evidence = Path(__file__).resolve().parents[1] / "evidence" + return [ + json.loads((evidence / f"real-user-u{number}.json").read_text(encoding="utf-8")) + for number in (1, 2) + ] + + def test_recorded_real_users_are_distinct_and_separate(self): + result = compare(*self.real_user_receipts()) + self.assertTrue(result["passed"], result) + self.assertEqual(len(result["checks"]), 6) + + def test_comparison_rejects_shared_session_or_user_or_cross_history(self): + left, original = self.real_user_receipts() + for field in ("hosted_session_id", "user_fingerprint"): + right = copy.deepcopy(original) + right["receipt"]["downstream"]["receipt"][field] = left["receipt"]["downstream"]["receipt"][field] + self.assertFalse(compare(left, right)["passed"]) + right = copy.deepcopy(original) + right["receipt"]["downstream"]["receipt"]["prior_session_messages"].append( + {"marker": left["receipt"]["downstream"]["receipt"]["marker"]} + ) + self.assertFalse(compare(left, right)["passed"]) + + def test_recorded_same_user_run_has_57_passing_checks(self): + path = Path(__file__).resolve().parents[1] / "evidence" / "same-user.json" + result = json.loads(path.read_text(encoding="utf-8")) + self.assertEqual(result["status"], "passed") + self.assertEqual(len(result["observations"]), 18) + self.assertEqual(len(result["checks"]), 57) + self.assertTrue(all(item["passed"] for item in result["checks"])) + + def test_default_does_not_forward_upstream_context(self): + self.assertIsNone(downstream_context({"marker": "test"}, "upstream")) + + def test_explicit_and_inherited_context(self): + self.assertEqual(downstream_context({"mode": "inherit"}, "upstream"), "upstream") + self.assertEqual( + downstream_context({"mode": "explicit", "c_context_id": "shared"}, "upstream"), + "shared", + ) + + def test_invalid_mode_and_context_are_rejected(self): + for payload in [{"mode": "wrong"}, {"mode": "explicit"}, {"mode": "explicit", "c_context_id": ""}]: + with self.assertRaises(ValueError): + downstream_context(payload, "upstream") + + def test_ledger_distinguishes_session_from_context_history(self): + with tempfile.TemporaryDirectory() as folder: + database = Path(folder) / "calls.sqlite" + first = record(database, "one", {"marker": "A", "caller_agent": "A"}) + other = record(database, "two", {"marker": "B", "caller_agent": "B"}) + repeat = record(database, "one", {"marker": "again", "caller_agent": "B"}) + self.assertEqual(first["prior_session_messages"], []) + self.assertEqual(len(other["prior_session_messages"]), 1) + self.assertEqual(other["prior_context_messages"], []) + self.assertEqual(repeat["prior_context_messages"][0]["marker"], "A") + json.dumps(repeat)