diff --git a/BUILD.bazel b/BUILD.bazel index f3a51c9a39..57d2ecd2c1 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,3 +1,5 @@ +exports_files(["test/worker-causality.json"]) + SHARED_CODEGEN_INPUTS = [ "nodejs/scripts/releaseArtifacts.ts", "nodejs/src/cliVersion.ts", diff --git a/docs/features/execution-identity.md b/docs/features/execution-identity.md new file mode 100644 index 0000000000..3ea026cea9 --- /dev/null +++ b/docs/features/execution-identity.md @@ -0,0 +1,372 @@ +# Observing runtime execution identity + +The SDK exposes identities observed on the runtime wire. Applications own the +association between their requests or Turns and runtime work; the SDK does not +infer that association or publish telemetry facts. + +## Field sources and compatibility + +The core identity fields below are available in SDK 1.0.14 with its default CLI +1.0.85. The newer early tool-context fields have a separate prerequisite +[below](#live-client-tool-trace-context). A configured +CLI path or external server can select a different runtime. Record both versions +and the connection/session lifetime when diagnosing an association; the SDK +version alone does not prove that a runtime emitted an optional field. + +Use the accepted `Session::id()`, not a transformation of an application's +session URI. Hosts can remap URI paths or resolve collisions before creating SDK +sessions. Retain the application's full session identity separately. + +The Rust payloads are generated by +[`scripts/codegen/rust.ts`](../../scripts/codegen/rust.ts) from the runtime's +`api.schema.json` and `session-events.schema.json`. Standalone SDK generation +uses the pinned CLI release schemas; runtime-checkout generation uses explicitly +selected same-checkout schemas. Change those contracts or their generators, not +the generated Rust files, when an additional runtime-owned field is required. + +| Wire source | Rust access | Meaning | +| --- | --- | --- | +| `session.create` result `sessionId`; outer `session.event.params.sessionId` | `Session::id()`, `SessionEventNotification.session_id` | Runtime logical session and event routing context | +| `session.send` result `messageId` | `Session::send()`, `rpc::SendResult.message_id` | Accepted logical user message, not an event or loop iteration | +| `queue.pendingItems.items[].messageId?` | `rpc::QueuePendingItems.message_id` | Same logical message identity, when present | +| `queue.pendingItems.items[].id` | `rpc::QueuePendingItems.id` | Separate opaque queue-item key; batch rows share this key | +| `user.message.data.{messageId?,turnId?,interactionId?}` | `session_events::UserMessageData` | Message identity and the loop/interaction that consumed it, when observed | +| `assistant.turn_start.data.{turnId,interactionId?}` | `session_events::AssistantTurnStartData` | Agent-loop iteration and optional interaction | +| `assistant.message.data.{messageId,originatingMessageId?,turnId?,interactionId?}` | `session_events::AssistantMessageData` | Assistant-message identity, primary initiating message, and optional execution keys | +| `event.{id,parentId?,agentId?}` | `SessionEvent`, `TypedSessionEvent` | Event identity, chronological predecessor, and optional subagent identity | +| `subagent.started.data.toolCallId` with envelope `agentId` | `session_events::SubagentStartedData` | Initial spawning-tool/worker relationship; API spawns without a tool use the worker ID as a lifecycle fallback | +| `session.tasks.startAgent` result `agentId` | `rpc::TasksStartAgentResult.agent_id` | Initial API-spawned worker identity | +| `session.tasks.sendMessage` result `sent` | `rpc::TasksSendMessageResult` | Delivery status, without a message admission ID at this baseline | + +Use `event.typed_data::()` and the corresponding generated +payload types for typed access. A malformed payload returns `None`; it must not +be replaced with a different event's identity. Unknown fields remain available +in the untyped event data. Do not forward that data wholesale into telemetry: +it includes message and tool content. + +Optional fields remain absent on older runtimes. Empty strings are not usable +identities. The high-level Rust `send()` retains its legacy empty-string result +when an acknowledgement lacks `messageId`; consumers must treat it as missing. +The generated `SendResult` instead requires a string `messageId`. Neither +behavior licenses substituting an event ID or a locally generated ID. + +## Distinguish a message, a loop iteration, and an interaction + +An application Turn, including an AHP Turn, is a separate identity. AHP Turn IDs +are opaque and session-scoped; they are not required to be UUIDs and must never +be equated with runtime `turnId`. + +A Turn identity exists only after an actual dispatch receipt or accepted host +action supplies that Turn ID. Ownership claims, active-client selection, +pre-dispatch processing, callback resolution, late acknowledgements and host +acknowledgements alone are not Turn or transport evidence. A pre-dispatch +failure therefore has no Turn identity, and a late ownership acknowledgement +after reconnect cannot retroactively mark a successful submission or a Turn +failure. + +Runtime `turnId` is a loop counter represented as a string. Counters can restart +across invocations. Interaction IDs can be reused for system-sourced work and +no-user HMAC sessions. Even `(sessionId, interactionId, turnId)` is not a +universally unique execution key. + +Record the actual `assistant.turn_start` **event ID** as the loop-occurrence +identity, with its observed runtime session and agent scope. Neither the loop +counter nor the initiating message ID identifies an individual occurrence. + +The exact accepted `messageId` connects a send to `user.message.messageId`. +`assistant.message.originatingMessageId`, when present, identifies the primary +message that started the run, not the assistant message itself: + +* Model/tool iterations and stop-hook corrections retain the primary origin. + A correction can start a new interaction while retaining that origin. +* Immediate steering does not replace the active run's origin. +* For a batch starting a run, the last message ID is primary; earlier messages + provide context. An empty batch has no initiating message. +* A subagent has its own initiating message. Its origin must not be substituted + with the root agent's current message. + +`user.message.delivery` distinguishes `idle`, `steering`, and `queued`; +`source` separately identifies origin. A queued acknowledgement means admission, +not consumption. Pending steering snapshots expose display text and counts, not +a supported per-message identity bridge. Never associate an arbitrary event with +the oldest pending send, queue position, or display text. + +## Consume observations without inventing associations + +1. Subscribe before create/resume using + [PreparedSession](./streaming-events.md#subscribing-before-a-session-starts). + Notifications can precede an RPC response. Drain concurrently or size the + finite buffer; handle `Lagged` as missing evidence, not a successful outcome. + On create with a known session ID, routing is registered before the RPC but + the event loop starts after the response; early notifications wait in the + router queue. Retention does not guarantee subscriber delivery before the + create acknowledgement. Resume starts the event loop before awaiting its + response. +1. Retain the accepted message ID with the application's own request/Turn + identity and session provenance. Allow either the acknowledgement or event + to arrive first; only join matching observed keys. +1. Retain event and agent provenance when handling duplicates or late events. + The SDK forwards duplicate notifications; it does not deduplicate or rewrite + them. Deduplication of replayed evidence must not merge different sessions, + executions, or conflicting payloads. +1. Resume/reconnect does not prove new execution, automatic replay, or identity + continuity across a replaced runtime. Subscribe before resuming; request + persisted history explicitly when needed. `eventLog.read` cursor expiry can + produce overlap, and ephemeral events are not durable history. Keep the + observation's connection/lifetime provenance separate from runtime IDs. +1. Keep incomplete associations incomplete. A returned send ID can be lost when + its waiting future is cancelled after dispatch. Missing interaction, worker, + or recovery keys are not supplied by the event's chronological `parentId`. + +The supported send request has no caller-supplied canonical message ID. Without +the optional admission support [below](#optional-rpc-admission-correlation), a lost +response leaves an arriving `user.message` unable to identify the application's +pending send. Neither queue order nor repurposing `source` as a correlation nonce +supplies that bridge. + +For an initial tool-spawned worker, match the scoped spawning `toolCallId` to +`subagent.started` and retain its envelope `agentId`. An API spawn instead +provides the returned worker ID; its lifecycle fallback is not evidence of a +real tool call. Subsequent worker events retain that worker scope on the root +stream, without requiring an exposed native child-session ID. + +This initial relationship does not establish the cause of a worker follow-up. +When the independent default-off `RUNTIME_WORKER_CAUSAL_CONTEXT` gate is +enabled, optional version-1 `workerCausality` diagnostics can preserve the +accepted queue input and its actual admission on worker `user.message`, owned +`system.notification`, `assistant.turn_start`, and successful +`session.tasks.sendMessage` results. Typed SDK decoding validates only that the +optional graph is supported, closed-shape and within its limits. Consumers must +still validate the graph against the enclosing placement and must not infer a +Turn, successful submission, draft retention, recovery outcome, browser +surface, physical dispatch or UI policy. `source` identifies a sender, not its +sending Turn. Recovery with no primary message still has no `user.message` +admission or `originatingMessageId`. + +Worker identity strings contain valid Unicode scalar values, are non-empty, +contain no Unicode control characters, and encode to at most 256 UTF-8 bytes. +Printable non-ASCII values are supported; invalid surrogate code units are +rejected rather than preserved through replacement encoding. + +Workflow completion notifications use the canonical public +`system.notification.data.kind.type = "workflow_completed"` discriminator and +typed `runId`, `workflowName`, status, usage and attempt fields. The AHP relay +normalizes legacy host `factory_completed` input before public emission, while +runtime history classification continues to recognize that legacy input. New +schemas and SDK types expose `workflow_completed`; they do not silently rename +the discriminator independently of the producer. + +Despite its name, `user.message.parentAgentTaskId` reflects the preparing +`TurnIdentity`'s task identifier, not a parent interaction ID. Do not equate it +with the distinct CAPI `X-Parent-Agent-Id` value. + +There is a narrower, existing product-telemetry bridge in CLI 1.0.85: +the root listener copies the final root-subscription event ID into +`properties.event_id` for `user_message`, `assistant_turn_start`, and +`assistant_turn_end`, corresponding to the same dotted SDK event types. +Child listeners skip these bridge families, so use the final root-bridged ID, +not an original child-log ID. Match the telemetry kind as well as the observed +ID, account for reporting-session normalization, and retain session/agent +provenance. Supplied event IDs are preserved during replay; a duplicate does not +establish a new occurrence. An end event has its own ID, not its start event's ID. + +CLI 1.0.85 does not put that occurrence ID on OpenTelemetry chat spans. The +updated runtime companion adds `copilot.runtime.event_id` from the observed +Turn-start event, behind the default-off `RUNTIME_OPERATION_TRACE_CONTEXT` +flag. Worker iterations use bounded child spans rather than overwriting a +multi-iteration invocation span. This adds no SDK identity or metric tag. +Host-known trace parentage and collector/query availability remain separate +relationships and verification requirements. + +This is not a universal equality join for all product records. The runtime can +normalize non-UUID reporting session IDs, report under a parent's identity, +fill a child event's missing interaction from root state, or remint an event +envelope before root projection. Knowing an occurrence ID also does not resolve +the missing cause of a worker follow-up or recovery submission. Keep raw +observations and explicit provenance; do not add a second telemetry writer or +put diagnostic identifiers in metric tags. + +## Optional RPC admission correlation + +`clientCorrelationId` is optional, caller-owned diagnostic metadata for a single +RPC input. Any SDK client can use it to reconcile an admission with observed +runtime evidence. The SDK does not generate values, associate them with +application measurement units, or publish telemetry. Application-specific +measurement envelopes stay outside this contract. + +The new convenience options are: + +| SDK | Single-message option | +| --- | --- | +| Node.js | `MessageOptions.clientCorrelationId` | +| Python | `client_correlation_id` keyword on `send`, `send_and_wait`, and `send_and_wait_typed` | +| Go | `MessageOptions.ClientCorrelationID` (`*string`) | +| .NET | `MessageOptions.ClientCorrelationId` | +| Java | `MessageOptions.setClientCorrelationId` | +| Rust | `MessageOptions::with_client_correlation_id` | + +Generated `session.send` requests expose the same optional string. For +`session.sendMessages`, set it on individual `SendMessageItem` values, including +preceding context items. There is no batch-level correlation field. Unset +convenience options are omitted, not sent as JSON null. Cloning an options value +preserves explicitly supplied metadata; independent sends do not inherit it. + +SDKs forward supplied strings unchanged, without UUID parsing, lowercasing, or +semantic validation. A supporting native runtime echoes only valid lowercase, +hyphenated 36-character UUIDs when its independent +`RUNTIME_ADMISSION_TRACE_CONTEXT` flag is enabled. This flag is off by default. +Missing, invalid, disabled, or unsupported metadata is ignored without failing +the product send. Receiving an optional field is not proof that another runtime +or connection supports it. + +The echo appears on the actual admitted `user.message.data.clientCorrelationId` +and its genuinely owned pending message row. Canonical `messageId`, queue `id`, +event `id`, and agent-loop `interactionId` retain their existing meanings. +Snapshot-only or hydrated rows do not acquire correlation metadata. Immediate +steering still exposes text-only steering rows; never reconstruct identity from +that text, row order, or timing. Preserve ordinary request headers and W3C trace +context independently. + +This value is not an idempotency key, authorization, or permission to retry. +Reusing a UUID does not deduplicate submissions; several admissions with the +same value remain ambiguous. Retain observed canonical message IDs and existing +application command/Turn ownership. Missing evidence must remain missing. + +Admission correlation is not a frontend outcome. The SDK does not infer a +displayed submission error, preserved draft, recovery result, browser attempt, +surface attribution or physical dispatch from protocol acceptance. Pending +application recovery must be generation-bound and invalidated by a +session/client-generation change or Stop. The original frontend +association-boundary ancestor is github/github-ui#35143 at +`4c2a7dfd381a57206995041ba4e4b1dbd4600e83`; the current authoritative integrated +UI head is `485a6c2c3aad0f77f41aad7057c117490f358c43`. Backend carriers remain generic, +and UI evidence does not enter the runtime contract. + +### Release prerequisites and the existing Rust raw API + +These typed additions come from canonical runtime source +`8001dfcc37b6f40bf63b2eef79fdea8ea3a7796f`. That source checkpoint is local and its +publication is blocked; it is not a released runtime or an available dependency +pin. This SDK's bundled CLI floor, dependency versions, and release-schema pin +are unchanged. Normal reviewed runtime publication, SDK generation/release, and +runtime-version plus flag verification are required before consuming the new +typed contract in a deployed fleet. The default CLI is not claimed to emit it. + +The released Rust SDK 1.0.14 calls its convenience type `MessageOptions`, not +`SendOptions`. It has neither this typed field nor a convenience extension map. +It does expose the public +`Session::client().call(method, Option)` API. A caller can use +that existing raw JSON-RPC seam without an unreleased SDK dependency, supplying +the accepted `Session::id()` and exact optional metadata. Raw +`SessionEvent.data` and raw RPC results preserve new and unknown fields; 1.0.14 +generated typed payloads do not expose the new field. + +Raw calls do not apply the convenience helper's wait guard, attachment +preparation, or trace-provider injection. Callers must preserve their existing +command ownership, serialization, cancellation handling, and request/trace +context. Do not issue a second send as a fallback after uncertain dispatch. +A controlled-peer SDK test proves transport forwarding, not deployed runtime +support or an end-to-end telemetry join. + +The complete canonical generation inputs have SHA-256 checksums: + +* `api.schema.json`: `4584674516106f39048a3347d4fb8a222ec9baf00527dbd59b99f006296ad2e8` +* `session-events.schema.json`: `b40db84f3232adbbfd7c6e8469c41d189f0ea6fe2dd4baecef79651182e2fc0c` + +All six projections are generated together. Existing early tool trace context +and source fields are preserved. The .NET generated send method retains +existing positional data arguments and its previous CLR signature, including the +cancellation token. The extended overload adds correlation before its final +cancellation parameter. Java records retain constructors that omit the new +optional component. + +## Live client-tool trace context + +The generated `ToolExecutionStartData` now exposes optional `traceparent` and +`tracestate` strings on `tool.execution_start.data`, including before a later +`external_tool.requested` callback. No SDK flag, writer, or new event family is +introduced. All six projections come from the canonical schemas in +[runtime commit c4e6d732](https://github.com/github/copilot-agent-runtime/commit/c4e6d7322f6ccf316e2a452b2a08a3ee956cae1a), +delivered in [the runtime companion](https://github.com/github/copilot-agent-runtime/pull/22693). +Use its [producer commit 27867ac0](https://github.com/github/copilot-agent-runtime/commit/27867ac02ac1606ed4d112cc613011e0ebc69ba5) +or a release containing it: the producer also preserves the published tool-span +context when the same canonical start is projected twice, keeping the later +callback parent consistent. The later `c4e6d732` schema corrects four existing +identity descriptions without changing the early-field shapes from `7ab2ab8`. +This is an unreleased producer prerequisite, not a new published SDK/CLI floor. +The standalone CLI release pin is unchanged; it does not supply these new fields. +Release-generation inputs must advance through the normal release process before +this generated exposure can pass a pinned-release freshness check. + +The producer uses the actual active `execute_tool` span, checks the tool, +native-session and start-event identities, and decorates the live wire copy. +Its static `RUNTIME_TOOL_TRACE_CONTEXT` flag is off by default and independent +of model propagation. Missing, disabled, unavailable, or persisted-history +context remains absent. Enabling this flag does not enable an exporter. + +Rust access is +`event.typed_data::()?.traceparent` and `.tracestate`; +both are `Option`. Older payloads deserialize with `None` and serialize +without new keys. The SDK preserves supplied strings rather than interpreting +W3C validity or authorizing execution. Consumers must independently validate +context and ignore invalid telemetry without failing a valid tool operation. +No context is inherited from a previous tool or added when replay omits it. + +For AHP consumers, the agreed existing extension is +`action._meta["com.github.copilot.traceContext"]` on `chat/toolCallStart` and +`chat/toolCallReady`, with value +`{version: 1, traceparent: string, tracestate?: string}`. The wrapper is not part +of the SDK's runtime event data. Host/browser emission and consumption remain +default-off; absent, invalid, or unknown-version metadata preserves old tool +behavior. Retain unrelated metadata and ownership/confirmation checks. + +Ready can invoke a client tool synchronously. Copy the observed matching tool's +context before that handoff, never from activation identity, an arbitrary latest +tool, or a computed tool ID. `ToolInvocation` still carries the later callback +context, but a callback cache cannot retroactively parent earlier execution. +Never delay tools waiting for telemetry. Neither a start event nor trace context +is evidence of permission to execute. + +The earlier unmodified schema inputs for the tool-context-only checkpoint had +SHA-256 checksums (the current inputs are listed in the admission section above): + +* `session-events.schema.json`: `4f70dfad0e6e15b44247f1df4f5df29269527c308d2e10ca49f56d3acd8b69ff` +* `api.schema.json`: `141d44c48ef5665b97032a8a400271b4140c92b2ed228fc9c84736dbd3cf4fcf` + +Relative to the standalone pin, these canonical inputs also contain the existing +optional `FusionResolvedData.hint`, `McpOauthRequiredStaticClientConfig.scope`, +and `McpServerConfigHttp.oauthScopes` additions. They are generated together; +there is no hand-edited schema subset or shadow Rust field. + +## Content-free compatibility fixtures + +[execution-identity-v1.json](../../rust/tests/fixtures/execution-identity-v1.json) +contains synthetic wire fragments, not a captured session or a prescribed event +order. `version` versions the fixture format; `sdkVersion` and `cliVersion` +identify its compatibility baseline. `sendResults` and `pendingItems` are RPC +result bodies; `notifications` contains complete `session.event` notifications. +These fixture metadata fields are not new runtime protocol fields. + +The fragments deliberately include distinct message/event/queue identities, +repeated loop/interaction keys with distinct start-event IDs, a correction +retaining its origin, another session, initial worker scope, resume, and absent +legacy keys. Message/display content and agent descriptions are empty; tool +arguments are absent. +Consumers can replay permutations and duplicates to test their own association +logic without collecting prompts or tool output. SDK tests verify typed +round-tripping and delivery before/after acknowledgements, across interleaved +sessions, cancelled waits with late responses, resume, and new connections. +They do not establish deployed runtime ordering, export coverage, or end-to-end +telemetry joins. + +[tool-trace-context-v1.json](../../rust/tests/fixtures/tool-trace-context-v1.json) +adds full synthetic early-tool notifications using the shared sampled and +unsampled v1 carrier values. Tests retain exact per-tool fields, duplicates, +missing context, and a same-ID persisted copy without live context, without +waiting for any tool callback. The fixture is not a recorded runtime execution. + +## Further reading + +* [Streaming events](./streaming-events.md): Subscription and buffering behavior +* [Steering and queueing](./steering-and-queueing.md): Message admission and delivery +* [Session persistence](./session-persistence.md): Reading and resuming session history diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 0292f98e00..900690f62c 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -222,6 +222,10 @@ session.on(AssistantMessageDeltaEvent.class, event -> A session can emit events before its create or resume call returns. The agent may already be working—especially on resume with `continuePendingWork`—and ephemeral events such as `session.idle` are never written to the session log, so `getMessages` cannot recover them afterwards. A subscription installed after the session handle exists misses that startup window. +For exact message, loop, interaction, and worker field sources, see +[Observing runtime execution identity](./execution-identity.md). Event order alone +does not associate an event with an application's pending request or Turn. + > [!TIP] > **(Rust)** `Client::prepare_session` and `Client::prepare_resume_session` return a `PreparedSession` that owns the session's event channel before any protocol activity happens. Subscribe first, then call `start()`. diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index bbc87c24a7..b7295d6b0f 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -8172,6 +8172,10 @@ internal sealed class SendRequest [JsonPropertyName("billable")] public bool? Billable { get; set; } + /// Optional caller-generated diagnostic UUID for this single message. Native sessions with RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character UUID on user.message and its existing pending message row. Missing, invalid, disabled, or unsupported metadata is ignored without rejecting the send. Does not change messageId, deduplicate submissions, authorize work, or make an uncertain retry safe. + [JsonPropertyName("clientCorrelationId")] + public string? ClientCorrelationId { get; set; } + /// If provided, this is shown in the timeline instead of `prompt`. [JsonPropertyName("displayPrompt")] public string? DisplayPrompt { get; set; } @@ -8245,6 +8249,10 @@ public sealed class SendMessageItem [JsonPropertyName("billable")] internal bool? Billable { get; set; } + /// Optional caller-generated diagnostic UUID for this item only, with the same validation and opt-in native echo as session.send.clientCorrelationId. The batch has no request-level correlation value; each item retains its own value, including preceding context messages. Reused values do not deduplicate messages and remain ambiguous. + [JsonPropertyName("clientCorrelationId")] + public string? ClientCorrelationId { get; set; } + /// If provided, this is shown in the timeline instead of `prompt`. [JsonPropertyName("displayPrompt")] public string? DisplayPrompt { get; set; } @@ -13641,6 +13649,11 @@ public sealed class TasksSendMessageResult /// Whether the message was successfully delivered or steered. [JsonPropertyName("sent")] public bool Sent { get; set; } + + /// Optional exact queue admission receipt on sent=true only. No implicit event or execution-success claim. + [JsonConverter(typeof(WorkerCausalityConverter))] + [JsonPropertyName("workerCausality")] + public WorkerCausality? WorkerCausality { get; set; } } /// Identifier of the target agent task, message content, and optional sender agent ID. @@ -22675,6 +22688,10 @@ public sealed class QueuePendingItems [JsonPropertyName("agentMode")] public SendAgentMode AgentMode { get; set; } + /// Caller-owned diagnostic UUID from the exact accepted native session.send or sendMessages item, when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted for unsupported or identity-less rows, including snapshot-only mirrors. Not an idempotency key, authorization, or permission to retry; repeated values remain ambiguous. + [JsonPropertyName("clientCorrelationId")] + public string? ClientCorrelationId { get; set; } + /// Human-readable text to display for this queue entry in the UI. [JsonPropertyName("displayText")] public string DisplayText { get; set; } = string.Empty; @@ -35903,7 +35920,7 @@ public override void Write(Utf8JsonWriter writer, OptionsUpdateReasoningSummary } -/// Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. +/// Origin of the sandbox choice supplied by the host. Settings-derived origins let managed policy floor the host preference; do not tag explicit session overrides as settings-derived. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -35932,7 +35949,7 @@ public SandboxConfigSource(string value) /// The user's persisted settings disabled the sandbox. public static SandboxConfigSource UserDisabled { get; } = new("user_disabled"); - /// An explicit session-scoped choice selected the sandbox state, such as a command-line flag. + /// A command-line flag selected the sandbox state for this session. public static SandboxConfigSource SessionFlag { get; } = new("session_flag"); /// The user disabled the sandbox for the current session. @@ -41777,18 +41794,40 @@ public async Task SuspendAsync(CancellationToken cancellationToken = default) /// W3C Trace Context traceparent header for distributed tracing of this agent turn. /// W3C Trace Context tracestate header for distributed tracing. /// If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. + /// Optional caller-generated diagnostic UUID for this single message. Native sessions with RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character UUID on user.message and its existing pending message row. Missing, invalid, disabled, or unsupported metadata is ignored without rejecting the send. Does not change messageId, deduplicate submissions, authorize work, or make an uncertain retry safe. /// The to monitor for cancellation requests. The default is . /// Result of sending a user message. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] - public async Task SendAsync(string prompt, string? displayPrompt = null, IList? attachments = null, SendMode? mode = null, bool? prepend = null, bool? billable = null, string? requiredTool = null, string? source = null, SendAgentMode? agentMode = null, IDictionary? requestHeaders = null, ResponseFormat? responseFormat = null, string? traceparent = null, string? tracestate = null, bool? wait = null, CancellationToken cancellationToken = default) + public async Task SendAsync(string prompt, string? displayPrompt = null, IList? attachments = null, SendMode? mode = null, bool? prepend = null, bool? billable = null, string? requiredTool = null, string? source = null, SendAgentMode? agentMode = null, IDictionary? requestHeaders = null, ResponseFormat? responseFormat = null, string? traceparent = null, string? tracestate = null, bool? wait = null, string? clientCorrelationId = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(prompt); _session.ThrowIfDisposed(); - var request = new SendRequest { SessionId = _session.SessionId, Prompt = prompt, DisplayPrompt = displayPrompt, Attachments = attachments, Mode = mode, Prepend = prepend, Billable = billable, RequiredTool = requiredTool, Source = source, AgentMode = agentMode, RequestHeaders = requestHeaders, ResponseFormat = responseFormat, Traceparent = traceparent, Tracestate = tracestate, Wait = wait }; + var request = new SendRequest { SessionId = _session.SessionId, Prompt = prompt, DisplayPrompt = displayPrompt, Attachments = attachments, Mode = mode, Prepend = prepend, Billable = billable, RequiredTool = requiredTool, Source = source, AgentMode = agentMode, RequestHeaders = requestHeaders, ResponseFormat = responseFormat, Traceparent = traceparent, Tracestate = tracestate, Wait = wait, ClientCorrelationId = clientCorrelationId }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.send", [request], cancellationToken); } + /// Sends a user message to the session and returns its message ID. + /// The user message text. + /// If provided, this is shown in the timeline instead of `prompt`. + /// Optional attachments (files, directories, selections, blobs, GitHub references) to include with the message. + /// How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. + /// If true, adds the message to the front of the queue instead of the end. + /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. + /// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange. + /// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent. + /// The UI mode the agent was in when this message was sent. Defaults to the session's current mode. + /// Custom HTTP headers to include in outbound model requests for this turn. Merged with session-level provider headers; per-turn headers augment and overwrite session-level headers with the same key. + /// Provider-native output format for this turn, including all tool-call iterations. Not inherited by later turns or subagents. Ordinary steering inherits the active format; specifying responseFormat with mode: immediate is an error, even while idle. Returned assistant content remains text; the runtime does not parse or validate it. Unsupported models or schemas produce provider errors. + /// W3C Trace Context traceparent header for distributed tracing of this agent turn. + /// W3C Trace Context tracestate header for distributed tracing. + /// If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. + /// The to monitor for cancellation requests. The default is . + /// Result of sending a user message. + [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] + public Task SendAsync(string prompt, string? displayPrompt, IList? attachments, SendMode? mode, bool? prepend, bool? billable, string? requiredTool, string? source, SendAgentMode? agentMode, IDictionary? requestHeaders, ResponseFormat? responseFormat, string? traceparent, string? tracestate, bool? wait, CancellationToken cancellationToken) + => SendAsync(prompt, displayPrompt, attachments, mode, prepend, billable, requiredTool, source, agentMode, requestHeaders, responseFormat, traceparent, tracestate, wait, clientCorrelationId: null, cancellationToken: cancellationToken); + /// Sends zero or more user messages to the session in a single turn and returns their message IDs. All provided messages are appended to the conversation in order, then exactly one agent turn runs over the resulting history. When the list is empty, one turn runs over the existing history with no new user message. Remote-backed (Mission Control) sessions do not support this method and will return an error. /// The user messages to append to the conversation, in order, before running one agent loop. When the batch starts a run, its final message is the primary initiating message; earlier messages provide context, not separate runs or replies. May be empty, in which case a single turn runs over the existing history with no new user message or originatingMessageId. /// How to deliver the messages. `enqueue` (default) appends to the message queue. `immediate` interjects during an in-progress turn. @@ -47557,6 +47596,17 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalWorkflow), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalWorkflow")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalWrite), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalWrite")] [JsonSerializable(typeof(GitHub.Copilot.Verbosity), TypeInfoPropertyName = "SessionEventsVerbosity")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerAdmission), TypeInfoPropertyName = "SessionEventsWorkerAdmission")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerAdmissionKind), TypeInfoPropertyName = "SessionEventsWorkerAdmissionKind")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerBridgeObservation), TypeInfoPropertyName = "SessionEventsWorkerBridgeObservation")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerCausality), TypeInfoPropertyName = "SessionEventsWorkerCausality")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerEventReference), TypeInfoPropertyName = "SessionEventsWorkerEventReference")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerEventType), TypeInfoPropertyName = "SessionEventsWorkerEventType")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerInput), TypeInfoPropertyName = "SessionEventsWorkerInput")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerNotificationMode), TypeInfoPropertyName = "SessionEventsWorkerNotificationMode")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerNotificationReference), TypeInfoPropertyName = "SessionEventsWorkerNotificationReference")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerObservationProvenance), TypeInfoPropertyName = "SessionEventsWorkerObservationProvenance")] +[JsonSerializable(typeof(GitHub.Copilot.WorkerSource), TypeInfoPropertyName = "SessionEventsWorkerSource")] [JsonSerializable(typeof(GitHub.Copilot.WorkflowPermissionOperation), TypeInfoPropertyName = "SessionEventsWorkflowPermissionOperation")] [JsonSerializable(typeof(GitHub.Copilot.WorkflowPermissionPhase), TypeInfoPropertyName = "SessionEventsWorkflowPermissionPhase")] [JsonSerializable(typeof(GitHub.Copilot.WorkflowRunSettledData), TypeInfoPropertyName = "SessionEventsWorkflowRunSettledData")] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 454b3edeb4..48d2b29d39 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -3601,6 +3601,11 @@ public sealed partial class UserMessageData [JsonPropertyName("attachments")] public Attachment[]? Attachments { get; set; } + /// Exact caller-owned diagnostic UUID carried by this accepted native session.send or sendMessages item when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted when input, native ownership, or support is missing. Independent of the canonical messageId; not an idempotency key, authorization, or permission to retry. Multiple messages with the same value remain ambiguous. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("clientCorrelationId")] + public string? ClientCorrelationId { get; set; } + /// The user's message text as displayed in the timeline. [JsonPropertyName("content")] public required string Content { get; set; } @@ -3630,7 +3635,7 @@ public sealed partial class UserMessageData [JsonPropertyName("nativeDocumentPathFallbackPaths")] public string[]? NativeDocumentPathFallbackPaths { get; set; } - /// Parent agent task ID for background telemetry correlated to this user turn. + /// Task ID minted when the runtime prepares this user-message run. This is not a parent interaction ID or worker instance ID and must not be equated with CAPI's X-Parent-Agent-Id. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentAgentTaskId")] public string? ParentAgentTaskId { get; set; } @@ -3659,6 +3664,12 @@ public sealed partial class UserMessageData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("turnId")] public string? TurnId { get; set; } + + /// Optional worker admission observations; self identity requires the matching enclosing message and agent. + [JsonConverter(typeof(WorkerCausalityConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("workerCausality")] + public WorkerCausality? WorkerCausality { get; set; } } /// Empty payload; the event signals that the pending message queue has changed. @@ -3687,6 +3698,12 @@ public sealed partial class AssistantTurnStartData /// Identifier for this turn within the agentic loop, typically a stringified turn number. [JsonPropertyName("turnId")] public required string TurnId { get; set; } + + /// Optional bounded worker observations. Missing or invalid metadata is unavailable, not known-empty. + [JsonConverter(typeof(WorkerCausalityConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("workerCausality")] + public WorkerCausality? WorkerCausality { get; set; } } /// Metadata for an additional model inference attempt within an existing assistant turn. @@ -4944,6 +4961,16 @@ public sealed partial class ToolExecutionStartData [JsonPropertyName("toolTitle")] public string? ToolTitle { get; set; } + /// W3C traceparent of this tool's active runtime execute_tool span. Available on live events when tool-context propagation is enabled; absent when the span is unavailable or on persisted history. This diagnostic context does not authorize execution. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("traceparent")] + public string? Traceparent { get; set; } + + /// Optional W3C tracestate associated with traceparent. Omitted when no valid vendor state is available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tracestate")] + public string? Tracestate { get; set; } + /// Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("turnId")] @@ -5320,7 +5347,7 @@ public sealed partial class SubagentStartedData [JsonPropertyName("taskModelSource")] public SubagentTaskModelSource? TaskModelSource { get; set; } - /// Tool call ID of the parent tool invocation that spawned this sub-agent. + /// Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -5414,7 +5441,7 @@ public sealed partial class SubagentCompletedData [JsonPropertyName("modelSelectionSource")] public SubagentModelSelectionSource? ModelSelectionSource { get; set; } - /// Tool call ID of the parent tool invocation that spawned this sub-agent. + /// Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -5490,7 +5517,7 @@ public sealed partial class SubagentFailedData [JsonPropertyName("modelSelectionSource")] public SubagentModelSelectionSource? ModelSelectionSource { get; set; } - /// Tool call ID of the parent tool invocation that spawned this sub-agent. + /// Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -5674,6 +5701,12 @@ public sealed partial class SystemNotificationData [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("responsesReasoning")] public ResponsesReasoning? ResponsesReasoning { get; set; } + + /// Optional owned worker notification observations; an omitted notification event refers only to this occurrence. + [JsonConverter(typeof(WorkerCausalityConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("workerCausality")] + public WorkerCausality? WorkerCausality { get; set; } } /// Permission request notification requiring client approval with request details. @@ -8134,6 +8167,181 @@ public partial class Attachment } +/// Exact observed event identity. No private registration generation or execution handle. +/// Nested data type for WorkerEventReference. +public sealed partial class WorkerEventReference +{ + /// Actual event agent scope, absent for a root occurrence; at most 256 UTF-8 bytes. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(256)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentId")] + public string? AgentId { get; set; } + + /// Actual event occurrence UUID; copied without normalization. + [JsonPropertyName("eventId")] + public required string EventId { get; set; } + + /// Type of the observed occurrence. + [JsonPropertyName("eventType")] + public required WorkerEventType EventType { get; set; } + + /// Explicit provenance of this reference. + [JsonPropertyName("provenance")] + public required WorkerObservationProvenance Provenance { get; set; } + + /// Actual runtime session scope, at most 256 UTF-8 bytes. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(256)] + [JsonPropertyName("sessionId")] + public required string SessionId { get; set; } +} + +/// An observed worker admission, not a claim that execution succeeded. +/// Nested data type for WorkerAdmission. +public sealed partial class WorkerAdmission +{ + /// Actual AHP participant Turn UUID, not a native turn counter or provenance signal. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("ahpTurnId")] + public string? AhpTurnId { get; set; } + + /// May be omitted only for the matching current worker user.message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("event")] + public WorkerEventReference? Event { get; set; } + + /// The producer's admission kind. + [JsonPropertyName("kind")] + public required WorkerAdmissionKind Kind { get; set; } + + /// Canonical logical message identity, independent of queueItemId; at most 256 UTF-8 bytes. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(256)] + [JsonPropertyName("messageId")] + public required string MessageId { get; set; } +} + +/// One indivisible source-to-reported bridge observation, not a root alias. +/// Nested data type for WorkerBridgeObservation. +public sealed partial class WorkerBridgeObservation +{ + /// Full occurrence actually emitted by that bridge. + [JsonPropertyName("reported")] + public required WorkerEventReference Reported { get; set; } + + /// Full event received by this bridge. + [JsonPropertyName("source")] + public required WorkerEventReference Source { get; set; } +} + +/// Exact accepted worker input, distinct from a message, event, caller correlation or Turn. +/// Nested data type for WorkerInput. +public sealed partial class WorkerInput +{ + /// Actual recipient task, at most 256 UTF-8 bytes. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [MaxLength(256)] + [JsonPropertyName("agentId")] + public required string AgentId { get; set; } + + /// UUID allocated for this queue item by the admitting producer. + [JsonPropertyName("queueItemId")] + public required string QueueItemId { get; set; } + + /// Original invoking occurrence, never replaced by a reported/root alias. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sender")] + public WorkerEventReference? Sender { get; set; } + + /// Exact captured edges in producer order. Requires sender; at most 32 whole pairs. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("senderBridges")] + public WorkerBridgeObservation[]? SenderBridges { get; set; } +} + +/// Exact delivery and optional occurrence of a consumed worker notification. +/// Nested data type for WorkerNotificationReference. +public sealed partial class WorkerNotificationReference +{ + /// Actual notificationDeliveryId UUID. + [JsonPropertyName("deliveryId")] + public required string DeliveryId { get; set; } + + /// May be omitted only on the matching current system.notification. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("event")] + public WorkerEventReference? Event { get; set; } + + /// Actual consumption mode. + [JsonPropertyName("mode")] + public required WorkerNotificationMode Mode { get; set; } +} + +/// One captured source at this placement and observation boundary. +/// Nested data type for WorkerSource. +public sealed partial class WorkerSource +{ + /// Actual admissions in capture order; at most 32. + [JsonPropertyName("admissions")] + public required WorkerAdmission[] Admissions { get; set; } + + /// Exact already-open iteration when an immediate notification was consumed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("admittedDuring")] + public WorkerEventReference? AdmittedDuring { get; set; } + + /// Applicable observations were all captured here, never work completion or success. + [JsonPropertyName("captureComplete")] + public required bool CaptureComplete { get; set; } + + /// Actual completion emission only. Absence is not an execution outcome. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completion")] + public WorkerEventReference? Completion { get; set; } + + /// Indivisible accepted input identity. + [JsonPropertyName("input")] + public required WorkerInput Input { get; set; } + + /// Actual consumed notification, when observed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("notification")] + public WorkerNotificationReference? Notification { get; set; } +} + +/// +/// Optional v1 worker diagnostics. The compact UTF-8 {"workerCausality":value} +/// must fit 4096 bytes after materializing an allowed implicit self-reference. +/// Ignore invalid/unknown/oversize metadata, not the product event. +/// +/// Nested data type for WorkerCausality. +public sealed partial class WorkerCausality +{ + /// + /// Complete placement-aware observed capture, not global causality or execution success. + /// Empty true requires explicit native invocation attestation. + /// + [JsonPropertyName("captureComplete")] + public required bool CaptureComplete { get; set; } + + /// Provenance of this enclosing observation; never inferred from other fields. + [JsonPropertyName("observationProvenance")] + public required WorkerObservationProvenance ObservationProvenance { get; set; } + + /// Sources in capture order, at most 32. Absent/unknown is not known-empty. + [JsonPropertyName("sources")] + public required WorkerSource[] Sources { get; set; } + + /// Supported version, exactly 1. + [JsonPropertyName("version")] + public required long Version { get; set; } +} + /// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. /// Nested data type for FusionStagedTerminal. [Experimental(global::GitHub.Copilot.Diagnostics.Experimental)] @@ -15234,6 +15442,259 @@ public override void Write(Utf8JsonWriter writer, UserMessageDelivery value, Jso } } +/// Producer of an observation, not the execution location of every referenced source. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct WorkerObservationProvenance : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public WorkerObservationProvenance(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Observed by the native runtime. + public static WorkerObservationProvenance Native { get; } = new("native"); + + /// Observed by the AHP coordinator. + public static WorkerObservationProvenance AhpCoordinator { get; } = new("ahp_coordinator"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkerObservationProvenance left, WorkerObservationProvenance right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkerObservationProvenance left, WorkerObservationProvenance right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is WorkerObservationProvenance other && Equals(other); + + /// + public bool Equals(WorkerObservationProvenance other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override WorkerObservationProvenance Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, WorkerObservationProvenance value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkerObservationProvenance)); + } + } +} + +/// Supported observed occurrences. Chronological parentId is not a causal reference. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct WorkerEventType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public WorkerEventType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Observed tool execution start occurrence. + public static WorkerEventType ToolExecutionStart { get; } = new("tool.execution_start"); + + /// Observed admitted user message occurrence. + public static WorkerEventType UserMessage { get; } = new("user.message"); + + /// Observed worker completion occurrence. + public static WorkerEventType SubagentCompleted { get; } = new("subagent.completed"); + + /// Observed worker notification occurrence. + public static WorkerEventType SystemNotification { get; } = new("system.notification"); + + /// Observed assistant turn start occurrence. + public static WorkerEventType AssistantTurnStart { get; } = new("assistant.turn_start"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkerEventType left, WorkerEventType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkerEventType left, WorkerEventType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is WorkerEventType other && Equals(other); + + /// + public bool Equals(WorkerEventType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override WorkerEventType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, WorkerEventType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkerEventType)); + } + } +} + +/// Why this exact worker admission was made. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct WorkerAdmissionKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public WorkerAdmissionKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A queued worker input was admitted. + public static WorkerAdmissionKind QueuedInput { get; } = new("queued_input"); + + /// A system continuation was admitted. + public static WorkerAdmissionKind SystemContinuation { get; } = new("system_continuation"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkerAdmissionKind left, WorkerAdmissionKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkerAdmissionKind left, WorkerAdmissionKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is WorkerAdmissionKind other && Equals(other); + + /// + public bool Equals(WorkerAdmissionKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override WorkerAdmissionKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, WorkerAdmissionKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkerAdmissionKind)); + } + } +} + +/// How the owned notification was consumed. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct WorkerNotificationMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public WorkerNotificationMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The notification was consumed through the queued input path. + public static WorkerNotificationMode Queued { get; } = new("queued"); + + /// The notification was consumed during an already-open iteration. + public static WorkerNotificationMode Immediate { get; } = new("immediate"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(WorkerNotificationMode left, WorkerNotificationMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(WorkerNotificationMode left, WorkerNotificationMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is WorkerNotificationMode other && Equals(other); + + /// + public bool Equals(WorkerNotificationMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override WorkerNotificationMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, WorkerNotificationMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(WorkerNotificationMode)); + } + } +} + /// What the agent was doing when the user interrupted it. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -20807,6 +21268,13 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(UserToolSessionApprovalRead))] [JsonSerializable(typeof(UserToolSessionApprovalWorkflow))] [JsonSerializable(typeof(UserToolSessionApprovalWrite))] +[JsonSerializable(typeof(WorkerAdmission))] +[JsonSerializable(typeof(WorkerBridgeObservation))] +[JsonSerializable(typeof(WorkerCausality))] +[JsonSerializable(typeof(WorkerEventReference))] +[JsonSerializable(typeof(WorkerInput))] +[JsonSerializable(typeof(WorkerNotificationReference))] +[JsonSerializable(typeof(WorkerSource))] [JsonSerializable(typeof(WorkflowPermissionPhase))] [JsonSerializable(typeof(WorkflowRunSettledData))] [JsonSerializable(typeof(WorkflowRunSettledEvent))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index ce50859571..eb33765901 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -324,6 +324,7 @@ public async Task SendAsync(MessageOptions options, CancellationToken ca { SessionId = SessionId, Prompt = options.Prompt, + ClientCorrelationId = options.ClientCorrelationId, DisplayPrompt = options.DisplayPrompt, Attachments = options.Attachments, Mode = options.Mode, @@ -2289,6 +2290,7 @@ internal record SendMessageRequest { public string SessionId { get; init; } = string.Empty; public string Prompt { get; init; } = string.Empty; + public string? ClientCorrelationId { get; init; } public string? DisplayPrompt { get; init; } public IList? Attachments { get; init; } public string? Mode { get; init; } diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index f40a71b95e..91c078040e 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -4131,6 +4131,7 @@ private MessageOptions(MessageOptions? other) AgentMode = other.AgentMode; Source = other.Source; Prompt = other.Prompt; + ClientCorrelationId = other.ClientCorrelationId; DisplayPrompt = other.DisplayPrompt; ResponseSchema = other.ResponseSchema; RequestHeaders = other.RequestHeaders is not null @@ -4143,6 +4144,13 @@ private MessageOptions(MessageOptions? other) /// public string Prompt { get; set; } = string.Empty; /// + /// Optional caller-owned diagnostic UUID for this RPC admission, sent unchanged. + /// Supported runtimes echo accepted values only with RUNTIME_ADMISSION_TRACE_CONTEXT + /// enabled. Not an idempotency key; the SDK never generates this value. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ClientCorrelationId { get; set; } + /// /// File or data attachments to include with the message. /// public IList? Attachments { get; set; } diff --git a/dotnet/src/WorkerCausalityConverter.cs b/dotnet/src/WorkerCausalityConverter.cs new file mode 100644 index 0000000000..5bbd376394 --- /dev/null +++ b/dotnet/src/WorkerCausalityConverter.cs @@ -0,0 +1,242 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace GitHub.Copilot; + +internal sealed class WorkerCausalityConverter : JsonConverter where T : class +{ + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + try + { + using var document = JsonDocument.ParseValue(ref reader); + var value = document.RootElement; + var remaining = 4096; + if (Small(value, ref remaining, 0) && Supported(value)) + { + if (!WithinBudget(value)) + throw new JsonException("optional worker metadata budget"); + return JsonSerializer.Deserialize(value, (JsonTypeInfo)options.GetTypeInfo(typeof(T))); + } + } + catch (Exception error) when (error is JsonException or InvalidOperationException) + { + // Optional diagnostics do not change product event handling. + } + Trace.TraceWarning("Ignoring invalid, unsupported or oversized workerCausality metadata"); + return null; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, value, (JsonTypeInfo)options.GetTypeInfo(typeof(T))); + + private static bool Supported(JsonElement value) + { + if (value.ValueKind != JsonValueKind.Object || + !Fields(value, "version", "observationProvenance", "sources", "captureComplete") || + !value.TryGetProperty("version", out var version) || version.ValueKind != JsonValueKind.Number || !version.TryGetInt32(out var number) || number != 1 || + !value.TryGetProperty("observationProvenance", out var provenance) || provenance.ValueKind != JsonValueKind.String || + provenance.GetString() is not ("native" or "ahp_coordinator") || + !value.TryGetProperty("captureComplete", out var complete) || complete.ValueKind is not (JsonValueKind.True or JsonValueKind.False) || + !value.TryGetProperty("sources", out var sources) || sources.ValueKind != JsonValueKind.Array || sources.GetArrayLength() > 32) + { + return false; + } + foreach (var source in sources.EnumerateArray()) + { + if (source.ValueKind != JsonValueKind.Object || + !Fields(source, "input", "admissions", "captureComplete", "completion", "notification", "admittedDuring") || + !source.TryGetProperty("captureComplete", out var sourceComplete) || sourceComplete.ValueKind is not (JsonValueKind.True or JsonValueKind.False) || + (complete.ValueKind == JsonValueKind.True && sourceComplete.ValueKind != JsonValueKind.True) || + !source.TryGetProperty("admissions", out var admissions) || admissions.ValueKind != JsonValueKind.Array || admissions.GetArrayLength() > 32 || + !source.TryGetProperty("input", out var input) || input.ValueKind != JsonValueKind.Object || + !Fields(input, "queueItemId", "agentId", "sender", "senderBridges") || + !Uuid(input, "queueItemId") || !Text(input, "agentId") || + !OptionalReference(input, "sender", "tool.execution_start") || + !OptionalReference(source, "completion", "subagent.completed") || + !OptionalReference(source, "admittedDuring", "assistant.turn_start")) + { + return false; + } + if (input.TryGetProperty("senderBridges", out var edges)) + { + if (!input.TryGetProperty("sender", out _) || edges.ValueKind != JsonValueKind.Array || edges.GetArrayLength() > 32) + return false; + foreach (var edge in edges.EnumerateArray()) + { + if (edge.ValueKind != JsonValueKind.Object || + !Fields(edge, "source", "reported") || + !edge.TryGetProperty("source", out var origin) || !Reference(origin, "tool.execution_start") || + !edge.TryGetProperty("reported", out var reported) || !Reference(reported, "tool.execution_start")) + return false; + } + } + foreach (var admission in admissions.EnumerateArray()) + { + if (admission.ValueKind != JsonValueKind.Object || + !Fields(admission, "kind", "messageId", "event", "ahpTurnId") || + !Text(admission, "messageId") || + !Text(admission, "kind") || admission.GetProperty("kind").GetString() is not ("queued_input" or "system_continuation") || + (admission.TryGetProperty("ahpTurnId", out _) && !Uuid(admission, "ahpTurnId"))) + return false; + if (admission.TryGetProperty("event", out var occurrence) && + (!Reference(occurrence, "user.message") || !Text(occurrence, "agentId") || + occurrence.GetProperty("agentId").GetString() != input.GetProperty("agentId").GetString())) + return false; + } + if (source.TryGetProperty("notification", out var notification) && + (notification.ValueKind != JsonValueKind.Object || + !Fields(notification, "deliveryId", "event", "mode") || + !Uuid(notification, "deliveryId") || + !Text(notification, "mode") || notification.GetProperty("mode").GetString() is not ("queued" or "immediate") || + !OptionalReference(notification, "event", "system.notification"))) + return false; + } + return true; + } + + private static bool Fields(JsonElement value, params string[] allowed) + { + foreach (var property in value.EnumerateObject()) + if (!allowed.Contains(property.Name)) + return false; + return true; + } + + private static bool Text(JsonElement value, string property) + { + if (!value.TryGetProperty(property, out var text) || text.ValueKind != JsonValueKind.String) + return false; + var content = text.GetString()!; + if (content.Length == 0) + return false; + for (var index = 0; index < content.Length; index++) + { + var character = content[index]; + if (char.IsHighSurrogate(character)) + { + if (index + 1 >= content.Length || !char.IsLowSurrogate(content[index + 1])) + return false; + index++; + } + else if (char.IsLowSurrogate(character) || char.IsControl(character)) + { + return false; + } + } + return Encoding.UTF8.GetByteCount(content) <= 256; + } + + private static bool Uuid(JsonElement value, string property) => + Text(value, property) && Guid.TryParseExact(value.GetProperty(property).GetString(), "D", out _); + + private static bool OptionalReference(JsonElement value, string property, string eventType) => + !value.TryGetProperty(property, out var reference) || Reference(reference, eventType); + + private static bool Reference(JsonElement value, string eventType) => + value.ValueKind == JsonValueKind.Object && + Fields(value, "sessionId", "eventId", "agentId", "eventType", "provenance") && + Text(value, "sessionId") && + Uuid(value, "eventId") && (!value.TryGetProperty("agentId", out _) || Text(value, "agentId")) && + Text(value, "eventType") && value.GetProperty("eventType").GetString() == eventType && + Text(value, "provenance") && value.GetProperty("provenance").GetString() is "native" or "ahp_coordinator"; + + private static bool Small(JsonElement value, ref int remaining, int depth) + { + if (--remaining < 0 || depth > 32) return false; + switch (value.ValueKind) + { + case JsonValueKind.String: + remaining -= value.GetString()!.Length; + break; + case JsonValueKind.Array: + foreach (var item in value.EnumerateArray()) + if (!Small(item, ref remaining, depth + 1)) return false; + break; + case JsonValueKind.Object: + foreach (var property in value.EnumerateObject()) + { + remaining -= property.Name.Length; + if (!Small(property.Value, ref remaining, depth + 1)) return false; + } + break; + } + return remaining >= 0; + } + + private static bool WithinBudget(JsonElement value) => + 3 + CompactStringBytes("workerCausality") + CompactBytes(value) <= 4096; + + private static int CompactBytes(JsonElement value) + { + switch (value.ValueKind) + { + case JsonValueKind.Object: + var objectBytes = 2; + var firstProperty = true; + foreach (var property in value.EnumerateObject()) + { + if (!firstProperty) + objectBytes++; + firstProperty = false; + objectBytes += CompactStringBytes(property.Name) + 1 + CompactBytes(property.Value); + } + return objectBytes; + case JsonValueKind.Array: + var arrayBytes = 2; + var firstItem = true; + foreach (var item in value.EnumerateArray()) + { + if (!firstItem) + arrayBytes++; + firstItem = false; + arrayBytes += CompactBytes(item); + } + return arrayBytes; + case JsonValueKind.String: + return CompactStringBytes(value.GetString()!); + case JsonValueKind.True: + return 4; + case JsonValueKind.False: + return 5; + case JsonValueKind.Null: + return 4; + case JsonValueKind.Number: + return value.GetRawText().Length; + default: + return int.MaxValue; + } + } + + private static int CompactStringBytes(string value) + { + var bytes = 2; + for (var index = 0; index < value.Length; index++) + { + var character = value[index]; + bytes += character switch + { + '"' or '\\' or '\b' or '\f' or '\n' or '\r' or '\t' => 2, + <= '\u001f' => 6, + <= '\u007f' => 1, + <= '\u07ff' => 2, + _ when char.IsHighSurrogate(character) && + index + 1 < value.Length && + char.IsLowSurrogate(value[index + 1]) => 4, + _ => 3, + }; + if (char.IsHighSurrogate(character) && + index + 1 < value.Length && + char.IsLowSurrogate(value[index + 1])) + index++; + } + return bytes; + } +} diff --git a/dotnet/test/GitHub.Copilot.SDK.Test.csproj b/dotnet/test/GitHub.Copilot.SDK.Test.csproj index 3f1c24f973..18843913bb 100644 --- a/dotnet/test/GitHub.Copilot.SDK.Test.csproj +++ b/dotnet/test/GitHub.Copilot.SDK.Test.csproj @@ -32,6 +32,7 @@ + diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index f5c98acb14..0d9725e81e 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -1551,6 +1551,107 @@ public async Task SendAsync_MessageSource_Is_Omitted_By_Default() Assert.All(requests, request => AssertMessageSource(request.Params, null)); } + [Theory] + [InlineData(null)] + [InlineData("01234567-89ab-4cde-8f01-23456789abcd")] + [InlineData("01234567-89AB-4CDE-8F01-23456789ABCD")] + [InlineData("not-a-uuid")] + [InlineData("")] + public async Task RpcAdmissionCorrelation_Preserves_Send_Options(string? correlation) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + var options = new MessageOptions + { + Prompt = "hello", + Source = MessageSource.System, + DisplayPrompt = "display", + RequestHeaders = new Dictionary { ["X-Test"] = "preserved" }, + ClientCorrelationId = correlation, + }; + Assert.Equal(correlation, options.Clone().ClientCorrelationId); + + using var activity = new Activity("rpc-admission-test").SetIdFormat(ActivityIdFormat.W3C); + activity.TraceStateString = "vendor=preserved"; + activity.Start(); + Assert.Equal("message-1", await session.SendAsync(options)); + var request = Assert.Single(server.Requests, request => request.Method == "session.send").Params; + Assert.Equal(session.SessionId, request.GetProperty("sessionId").GetString()); + Assert.Equal("system", request.GetProperty("source").GetString()); + Assert.Equal("display", request.GetProperty("displayPrompt").GetString()); + Assert.Equal("preserved", request.GetProperty("requestHeaders").GetProperty("X-Test").GetString()); + Assert.Equal(activity.Id, request.GetProperty("traceparent").GetString()); + Assert.Equal(activity.TraceStateString, request.GetProperty("tracestate").GetString()); + AssertAdmissionCorrelation(request, correlation); + + server.ClearRequests(); + var result = await session.Rpc.SendAsync("hello", clientCorrelationId: correlation); + Assert.Equal("message-1", result.MessageId); + AssertAdmissionCorrelation(Assert.Single(server.Requests, request => request.Method == "session.send").Params, correlation); + } + + [Fact] + public async Task RpcAdmissionCorrelation_Preserves_Positional_Send_Arguments() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + await session.Rpc.SendAsync("hello", "display", null, null, null, null, null, null, null, null, null, null, null, null, CancellationToken.None); + var request = Assert.Single(server.Requests, request => request.Method == "session.send").Params; + Assert.Equal("display", request.GetProperty("displayPrompt").GetString()); + AssertAdmissionCorrelation(request, null); + + server.ClearRequests(); + await session.Rpc.SendAsync("hello", "display", null, null, null, null, null, null, null, null, null, null, null, null, default); + request = Assert.Single(server.Requests, request => request.Method == "session.send").Params; + Assert.Equal("display", request.GetProperty("displayPrompt").GetString()); + AssertAdmissionCorrelation(request, null); + } + + [Fact] + public async Task RpcAdmissionCorrelation_Is_Per_Input_Not_Per_Batch() + { + const string first = "01234567-89ab-4cde-8f01-23456789abcd"; + const string second = "abcdef01-2345-4678-9abc-def012345678"; + await using var server = await FakeCopilotServer.StartAsync(); + server.UniqueMessageIds = true; + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig()); + var ids = await Task.WhenAll( + session.SendAsync(new MessageOptions { Prompt = "first", ClientCorrelationId = first }), + session.SendAsync(new MessageOptions { Prompt = "second", ClientCorrelationId = second })); + Assert.Equal(2, ids.Distinct().Count()); + foreach (var request in server.Requests.Where(request => request.Method == "session.send")) + { + AssertAdmissionCorrelation(request.Params, request.Params.GetProperty("prompt").GetString() == "first" ? first : second); + } + await session.Rpc.SendMessagesAsync([ + new SendMessageItem { Prompt = "context", ClientCorrelationId = second }, + new SendMessageItem { Prompt = "plain" }, + new SendMessageItem { Prompt = "reused", ClientCorrelationId = first }, + ]); + var batch = Assert.Single(server.Requests, request => request.Method == "session.sendMessages").Params; + AssertAdmissionCorrelation(batch, null); + var messages = batch.GetProperty("messages").EnumerateArray().ToArray(); + Assert.Equal(3, messages.Length); + AssertAdmissionCorrelation(messages[0], second); + AssertAdmissionCorrelation(messages[1], null); + AssertAdmissionCorrelation(messages[2], first); + } + + private static void AssertAdmissionCorrelation(JsonElement request, string? correlation) + { + if (correlation is null) + { + Assert.False(request.TryGetProperty("clientCorrelationId", out _)); + } + else + { + Assert.Equal(correlation, request.GetProperty("clientCorrelationId").GetString()); + } + } + [Theory] [MemberData(nameof(SerializationTests.MessageSources), MemberType = typeof(SerializationTests))] public async Task SendAsync_MessageSource_Preserves_Other_Options(MessageSource? source, string? wireSource) diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 85752af88b..148453354d 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -4,12 +4,67 @@ using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using GitHub.Copilot.Rpc; using Xunit; namespace GitHub.Copilot.Test.Unit; +[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +[JsonSerializable(typeof(QueuePendingItems))] +internal partial class RpcAdmissionJsonContext : JsonSerializerContext; + public class SessionEventSerializationTests { + [Theory] + [InlineData(null)] + [InlineData("01234567-89ab-4cde-8f01-23456789abcd")] + [InlineData("01234567-89AB-4CDE-8F01-23456789ABCD")] + [InlineData("not-a-uuid")] + [InlineData("")] + public void RpcAdmissionCorrelation_RoundTrips_Optional_Projections(string? correlation) + { + var metadata = new JsonObject { ["futureField"] = true }; + if (correlation is not null) + { + metadata["clientCorrelationId"] = correlation; + } + var row = metadata.DeepClone().AsObject(); + row["id"] = "queue-1"; + row["messageId"] = "canonical-1"; + row["kind"] = "message"; + row["displayText"] = "hello"; + row["agentMode"] = "interactive"; + var pending = JsonSerializer.Deserialize(row.ToJsonString(), RpcAdmissionJsonContext.Default.QueuePendingItems)!; + Assert.Equal(correlation, pending.ClientCorrelationId); + Assert.Equal("canonical-1", pending.MessageId); + Assert.Equal("queue-1", pending.Id); + Assert.Equal(correlation is not null, JsonNode.Parse(JsonSerializer.Serialize(pending, RpcAdmissionJsonContext.Default.QueuePendingItems))!.AsObject().ContainsKey("clientCorrelationId")); + + var data = metadata.DeepClone().AsObject(); + data["content"] = "hello"; + data["messageId"] = "canonical-1"; + data["interactionId"] = "agent-loop-1"; + var envelope = new JsonObject + { + ["id"] = "11111111-1111-1111-1111-111111111111", + ["timestamp"] = "2026-09-18T22:00:00+00:00", + ["parentId"] = null, + ["type"] = "user.message", + ["data"] = data, + }; + var decoded = Assert.IsType(SessionEvent.FromJson(envelope.ToJsonString())); + Assert.Equal(correlation, decoded.Data.ClientCorrelationId); + Assert.Equal("canonical-1", decoded.Data.MessageId); + Assert.Equal("agent-loop-1", decoded.Data.InteractionId); + Assert.Equal(correlation is not null, JsonNode.Parse(decoded.ToJson())!["data"]!.AsObject().ContainsKey("clientCorrelationId")); + + row["clientCorrelationId"] = null; + pending = JsonSerializer.Deserialize(row.ToJsonString(), RpcAdmissionJsonContext.Default.QueuePendingItems)!; + Assert.Null(pending.ClientCorrelationId); + Assert.False(JsonNode.Parse(JsonSerializer.Serialize(pending, RpcAdmissionJsonContext.Default.QueuePendingItems))!.AsObject().ContainsKey("clientCorrelationId")); + } + [Theory] [InlineData("session.idle", "{}")] [InlineData("user.message", """{"content":"hello"}""")] @@ -149,6 +204,42 @@ public void UserMessageEvent_MessageId_UsesCamelCaseAndIsOptional(string? messag } } + [Theory] + [InlineData(null, null)] + [InlineData("00-11111111111111111111111111111111-2222222222222222-01", "vendor=value")] + [InlineData("00-11111111111111111111111111111111-2222222222222222-00", null)] + [InlineData("invalid", "invalid")] + [InlineData("", null)] + public void ToolStartEvent_PreservesOptionalTraceContext(string? traceparent, string? tracestate) + { + var data = new JsonObject + { + ["toolCallId"] = "tool-call-a", + ["toolName"] = "client-tool", + }; + if (traceparent is not null) + { + data["traceparent"] = traceparent; + } + if (tracestate is not null) + { + data["tracestate"] = tracestate; + } + var wire = new JsonObject + { + ["id"] = "11111111-1111-1111-1111-111111111111", + ["timestamp"] = "2026-09-18T22:00:00+00:00", + ["parentId"] = "22222222-2222-2222-2222-222222222222", + ["type"] = "tool.execution_start", + ["data"] = data, + }; + var decoded = Assert.IsType(SessionEvent.FromJson(wire.ToJsonString())); + + Assert.Equal(traceparent, decoded.Data.Traceparent); + Assert.Equal(tracestate, decoded.Data.Tracestate); + Assert.True(JsonNode.DeepEquals(wire, JsonNode.Parse(decoded.ToJson()))); + } + public static TheoryData AutoTierSwitchFailureReasons => new() { "policy_rejected", diff --git a/dotnet/test/Unit/WorkerCausalityTests.cs b/dotnet/test/Unit/WorkerCausalityTests.cs new file mode 100644 index 0000000000..54fc367dce --- /dev/null +++ b/dotnet/test/Unit/WorkerCausalityTests.cs @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using GitHub.Copilot.Rpc; +using Xunit; + +namespace GitHub.Copilot.Test.Unit; + +public class WorkerCausalityTests +{ + private static readonly JsonNode Corpus = JsonNode.Parse(File.ReadAllText( + Path.Combine(AppContext.BaseDirectory, "worker-causality.json")))!; + private static readonly JsonSerializerOptions RpcOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver(), + }; + + public static IEnumerable Cases => Corpus["valid"]!.AsArray().Select( + value => new object[] { value!["name"]!.GetValue(), value!.ToJsonString() }); + + private static string Decode(JsonNode value, bool isEvent) + { + var wire = value.ToJsonString(); + return isEvent ? SessionEvent.FromJson(wire).ToJson() : + JsonSerializer.Serialize(JsonSerializer.Deserialize(wire, RpcOptions), RpcOptions); + } + + private static JsonObject Payload(JsonNode value, bool isEvent) => + (isEvent ? value["data"]! : value).AsObject(); + + [Theory] + [MemberData(nameof(Cases))] + public void PublicReadersPreserveSourceAndHistoricalProductBytes(string name, string fixture) + { + var test = JsonNode.Parse(fixture)!; + var isEvent = test["event"] is not null; + var wire = (test[isEvent ? "event" : "result"]!).DeepClone(); + var observed = JsonNode.Parse(Decode(wire, isEvent))!; + Assert.True(JsonNode.DeepEquals(Payload(observed, isEvent)["workerCausality"], + Payload(wire, isEvent)["workerCausality"]), name); + + Payload(wire, isEvent).Remove("workerCausality"); + var baseline = Decode(wire, isEvent); + foreach (var invalid in Corpus["invalid"]!.AsArray()) + { + Payload(wire, isEvent)["workerCausality"] = invalid!["value"]?.DeepClone(); + Assert.Equal(baseline, Decode(wire, isEvent)); + } + } + + [Fact] + public void ExactUtf8BudgetAndUnknownFields() + { + foreach (var test in Corpus["boundaries"]!.AsArray()) + { + var wire = Corpus["valid"]![0]!["event"]!.DeepClone(); + Payload(wire, true)["workerCausality"] = test!["value"]!.DeepClone(); + var observed = Payload(JsonNode.Parse(Decode(wire, true))!, true); + Assert.True( + observed.ContainsKey("workerCausality") == test["accepted"]!.GetValue(), + test["name"]!.GetValue()); + Assert.Equal(Payload(wire, true)["content"]!.GetValue(), observed["content"]!.GetValue()); + } + } + + [Fact] + public void CanonicalWorkflowCompletionDecodesTypedFields() + { + foreach (var fixtureName in new[] { "workflowCompleted", "legacyWorkflowCompleted" }) + { + var parsed = SessionEvent.FromJson(Corpus[fixtureName]!["event"]!.ToJsonString()); + var notification = Assert.IsType(parsed); + var completion = Assert.IsType(notification.Data.Kind); + Assert.Equal("fix-ci", completion.WorkflowName); + Assert.Equal("run-1", completion.RunId); + Assert.Equal(SystemNotificationWorkflowCompletedStatus.Completed, completion.Status); + Assert.Equal(1u, completion.ConsumedSubagents); + } + } + + [Fact] + public void InvalidUnicodeScalarIdentitiesAreUnavailable() + { + foreach (var wire in Corpus["invalidRaw"]!.AsArray().Select( + test => $"{{\"sent\":true,\"workerCausality\":{test!["json"]!.GetValue()}}}")) + { + var result = JsonSerializer.Deserialize(wire, RpcOptions); + Assert.NotNull(result); + Assert.Null(result.WorkerCausality); + Assert.True(result.Sent); + } + } + + [Fact] + public void UppercaseWorkerUuidIdentitiesRoundTripExactly() + { + const string wire = """ + { + "sent": true, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [{ + "input": { + "queueItemId": "ABCDEFAB-CDEF-4ABC-8ABC-ABCDEFABCDEF", + "agentId": "worker", + "sender": { + "sessionId": "session-uppercase", + "eventId": "FEDCBAFE-DCBA-4FED-8FED-FEDCBAFEDCBA", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + "admissions": [{ + "kind": "queued_input", + "messageId": "message-uppercase", + "ahpTurnId": "AAAABBBB-CCCC-4DDD-8EEE-FFFFAAAABBBB" + }], + "captureComplete": true, + "notification": { + "deliveryId": "BBBBCCCC-DDDD-4EEE-8FFF-AAAABBBBCCCC", + "mode": "queued" + } + }], + "captureComplete": true + } + } + """; + var source = JsonNode.Parse(wire)!; + var observed = JsonNode.Parse(Decode(source, false))!; + Assert.True(JsonNode.DeepEquals(source["workerCausality"], observed["workerCausality"])); + } +} diff --git a/go/factory_session_event_compat.go b/go/factory_session_event_compat.go new file mode 100644 index 0000000000..9a4a2f5fb6 --- /dev/null +++ b/go/factory_session_event_compat.go @@ -0,0 +1,27 @@ +package copilot + +import "github.com/github/copilot-sdk/go/rpc" + +// Deprecated: use WorkflowRunUpdatedData. +type FactoryRunUpdatedData = rpc.FactoryRunUpdatedData + +// Deprecated: use WorkflowRunStartedData. +type FactoryRunStartedData = rpc.FactoryRunStartedData + +// Deprecated: use WorkflowRunSettledData. +type FactoryRunSettledData = rpc.FactoryRunSettledData + +// Deprecated: use WorkflowRunSettledStatus. +type FactoryRunSettledStatus = rpc.FactoryRunSettledStatus + +const ( + FactoryRunSettledStatusCancelled = rpc.FactoryRunSettledStatusCancelled + FactoryRunSettledStatusCompleted = rpc.FactoryRunSettledStatusCompleted + FactoryRunSettledStatusError = rpc.FactoryRunSettledStatusError + FactoryRunSettledStatusHalted = rpc.FactoryRunSettledStatusHalted + FactoryRunSettledStatusPaused = rpc.FactoryRunSettledStatusPaused + + SessionEventTypeFactoryRunSettled = rpc.SessionEventTypeFactoryRunSettled + SessionEventTypeFactoryRunStarted = rpc.SessionEventTypeFactoryRunStarted + SessionEventTypeFactoryRunUpdated = rpc.SessionEventTypeFactoryRunUpdated +) diff --git a/go/internal/diagnosticmetadata/worker_causality.go b/go/internal/diagnosticmetadata/worker_causality.go new file mode 100644 index 0000000000..ad4139c89d --- /dev/null +++ b/go/internal/diagnosticmetadata/worker_causality.go @@ -0,0 +1,323 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +// Package diagnosticmetadata keeps optional diagnostics from rejecting product data. +package diagnosticmetadata + +import ( + "encoding/json" + "log" + "regexp" + "strconv" + "unicode" + "unicode/utf8" +) + +var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$`) + +func text(value any) bool { + s, ok := value.(string) + if !ok || s == "" || len(s) > 256 { + return false + } + for _, character := range s { + if unicode.IsControl(character) { + return false + } + } + return true +} + +func uuid(value any) bool { + s, ok := value.(string) + return ok && uuidPattern.MatchString(s) +} + +func hexQuad(value []byte) (uint16, bool) { + if len(value) < 4 { + return 0, false + } + var result uint16 + for _, character := range value[:4] { + result <<= 4 + switch { + case character >= '0' && character <= '9': + result += uint16(character - '0') + case character >= 'a' && character <= 'f': + result += uint16(character-'a') + 10 + case character >= 'A' && character <= 'F': + result += uint16(character-'A') + 10 + default: + return 0, false + } + } + return result, true +} + +func validUnicodeScalarEscapes(raw []byte) bool { + if !utf8.Valid(raw) { + return false + } + inString := false + for index := 0; index < len(raw); index++ { + character := raw[index] + if !inString { + if character == '"' { + inString = true + } + continue + } + if character == '"' { + inString = false + continue + } + if character != '\\' { + continue + } + if index+1 >= len(raw) { + return false + } + if raw[index+1] != 'u' { + index++ + continue + } + codeUnit, ok := hexQuad(raw[index+2:]) + if !ok { + return false + } + if codeUnit >= 0xd800 && codeUnit <= 0xdbff { + if index+11 >= len(raw) || raw[index+6] != '\\' || raw[index+7] != 'u' { + return false + } + low, ok := hexQuad(raw[index+8:]) + if !ok || low < 0xdc00 || low > 0xdfff { + return false + } + index += 11 + } else { + if codeUnit >= 0xdc00 && codeUnit <= 0xdfff { + return false + } + index += 5 + } + } + return !inString +} + +func fields(value map[string]any, allowed ...string) bool { + if len(value) > len(allowed) { + return false + } + for key := range value { + found := false + for _, candidate := range allowed { + if key == candidate { + found = true + break + } + } + if !found { + return false + } + } + return true +} + +func reference(value any, eventType string) bool { + ref, ok := value.(map[string]any) + if !ok { + return false + } + agent, hasAgent := ref["agentId"] + return fields(ref, "sessionId", "eventId", "agentId", "eventType", "provenance") && + text(ref["sessionId"]) && uuid(ref["eventId"]) && + (!hasAgent || text(agent)) && ref["eventType"] == eventType && + (ref["provenance"] == "native" || ref["provenance"] == "ahp_coordinator") +} + +func source(value any) bool { + s, ok := value.(map[string]any) + if !ok { + return false + } + input, inputOK := s["input"].(map[string]any) + admissions, admissionsOK := s["admissions"].([]any) + _, complete := s["captureComplete"].(bool) + if !fields(s, "input", "admissions", "captureComplete", "completion", "notification", "admittedDuring") || + !inputOK || !fields(input, "queueItemId", "agentId", "sender", "senderBridges") || + !admissionsOK || len(admissions) > 32 || !complete || + !uuid(input["queueItemId"]) || !text(input["agentId"]) { + return false + } + sender, hasSender := input["sender"] + if hasSender && !reference(sender, "tool.execution_start") { + return false + } + if raw, exists := input["senderBridges"]; exists { + edges, ok := raw.([]any) + if !ok || !hasSender || len(edges) > 32 { + return false + } + for _, raw := range edges { + edge, ok := raw.(map[string]any) + if !ok || !fields(edge, "source", "reported") || + !reference(edge["source"], "tool.execution_start") || !reference(edge["reported"], "tool.execution_start") { + return false + } + } + } + for _, raw := range admissions { + admission, ok := raw.(map[string]any) + if !ok || !fields(admission, "kind", "messageId", "event", "ahpTurnId") || + !text(admission["messageId"]) || + (admission["kind"] != "queued_input" && admission["kind"] != "system_continuation") { + return false + } + if turn, exists := admission["ahpTurnId"]; exists && !uuid(turn) { + return false + } + if event, exists := admission["event"]; exists { + if !reference(event, "user.message") || event.(map[string]any)["agentId"] != input["agentId"] { + return false + } + } + } + for field, eventType := range map[string]string{"completion": "subagent.completed", "admittedDuring": "assistant.turn_start"} { + if event, exists := s[field]; exists && !reference(event, eventType) { + return false + } + } + if raw, exists := s["notification"]; exists { + n, ok := raw.(map[string]any) + if !ok || !fields(n, "deliveryId", "event", "mode") || + !uuid(n["deliveryId"]) || (n["mode"] != "queued" && n["mode"] != "immediate") { + return false + } + if event, exists := n["event"]; exists && !reference(event, "system.notification") { + return false + } + } + return true +} + +func compactJSONStringSize(value string) int { + size := 2 + for _, character := range value { + switch character { + case '"', '\\', '\b', '\f', '\n', '\r', '\t': + size += 2 + default: + if character < 0x20 { + size += 6 + } else { + size += utf8.RuneLen(character) + } + } + } + return size +} + +func compactJSONSize(value any, depth int) (int, bool) { + if depth > 32 { + return 0, false + } + switch value := value.(type) { + case nil: + return 4, true + case bool: + if value { + return 4, true + } + return 5, true + case float64: + return len(strconv.AppendFloat(nil, value, 'g', -1, 64)), true + case string: + return compactJSONStringSize(value), true + case []any: + size := 2 + for index, item := range value { + itemSize, ok := compactJSONSize(item, depth+1) + if !ok { + return 0, false + } + if index > 0 { + size++ + } + size += itemSize + } + return size, true + case map[string]any: + size := 2 + index := 0 + for key, item := range value { + itemSize, ok := compactJSONSize(item, depth+1) + if !ok { + return 0, false + } + if index > 0 { + size++ + } + size += compactJSONStringSize(key) + 1 + itemSize + index++ + } + return size, true + default: + return 0, false + } +} + +func small(value any, remaining *int, depth int) bool { + *remaining-- + if *remaining < 0 || depth > 32 { + return false + } + switch value := value.(type) { + case string: + *remaining -= len(value) + case []any: + for _, item := range value { + if !small(item, remaining, depth+1) { + return false + } + } + case map[string]any: + for key, item := range value { + if !small(key, remaining, depth+1) || !small(item, remaining, depth+1) { + return false + } + } + } + return *remaining >= 0 +} + +func supported(value map[string]any) bool { + sources, ok := value["sources"].([]any) + _, complete := value["captureComplete"].(bool) + if !fields(value, "version", "observationProvenance", "sources", "captureComplete") || + !ok || len(sources) > 32 || !complete || value["version"] != float64(1) || + (value["observationProvenance"] != "native" && value["observationProvenance"] != "ahp_coordinator") { + return false + } + for _, entry := range sources { + if !source(entry) || (value["captureComplete"] == true && entry.(map[string]any)["captureComplete"] != true) { + return false + } + } + return true +} + +// ReadWorkerCausality decodes availability only, not enclosing self identity or association. +func ReadWorkerCausality(raw json.RawMessage, target any) bool { + if len(raw) == 0 || !validUnicodeScalarEscapes(raw) { + return false + } + var value map[string]any + remaining := 4096 + if json.Unmarshal(raw, &value) == nil && small(value, &remaining, 0) && supported(value) { + size, ok := compactJSONSize(map[string]any{"workerCausality": value}, 0) + if ok && size <= 4096 && json.Unmarshal(raw, target) == nil { + return true + } + } + log.Print("Ignoring invalid, unsupported or oversized workerCausality metadata") + return false +} diff --git a/go/message_source_test.go b/go/message_source_test.go index e813617b9e..ac1c511d48 100644 --- a/go/message_source_test.go +++ b/go/message_source_test.go @@ -12,10 +12,85 @@ import ( "time" "github.com/github/copilot-sdk/go/internal/jsonrpc2" + "github.com/github/copilot-sdk/go/rpc" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" ) +func TestSession_SendAdmissionCorrelation(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) + defer cancel() + for _, value := range []any{nil, "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", "not-a-uuid", ""} { + var correlation *string + if value != nil { + id := value.(string) + correlation = &id + } + for _, generated := range []bool{false, true} { + want := map[string]any{"sessionId": "session-1", "prompt": "hello"} + if correlation != nil { + want["clientCorrelationId"] = *correlation + } + params := captureMessageSourceRequest(t, nil, nil, func(session *Session) { + var messageID string + var err error + if generated { + result, rpcErr := session.RPC.Send(ctx, &rpc.SendRequest{ + Prompt: "hello", ClientCorrelationID: correlation, + }) + err = rpcErr + if result != nil { + messageID = result.MessageID + } + } else { + messageID, err = session.Send(ctx, MessageOptions{ + Prompt: "hello", ClientCorrelationID: correlation, + }) + } + if err != nil || messageID != "message-1" { + t.Fatalf("send result = %q, %v", messageID, err) + } + }) + if !reflect.DeepEqual(params, want) { + t.Fatalf("admission metadata changed: got %#v, want %#v", params, want) + } + } + } +} + +func TestSession_SendMessagesAdmissionCorrelationStaysPerItem(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) + defer cancel() + id := "01234567-89ab-4cde-8f01-23456789abcd" + ids := []string{"context-id", "plain-id", "reused-id"} + params := captureSessionRPCRequest(t, "session.sendMessages", map[string]any{"messageIds": ids}, + nil, nil, false, func(session *Session) { + result, err := session.RPC.SendMessages(ctx, &rpc.SendMessagesRequest{Messages: []rpc.SendMessageItem{ + {Prompt: "context", ClientCorrelationID: &id}, + {Prompt: "plain"}, + {Prompt: "reused", ClientCorrelationID: &id}, + }}) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(result.MessageIDs, ids) { + t.Fatalf("canonical message IDs changed: %#v", result.MessageIDs) + } + }) + want := map[string]any{ + "sessionId": "session-1", + "messages": []any{ + map[string]any{"prompt": "context", "clientCorrelationId": id}, + map[string]any{"prompt": "plain"}, + map[string]any{"prompt": "reused", "clientCorrelationId": id}, + }, + } + if !reflect.DeepEqual(params, want) { + t.Fatalf("unexpected batch wire request: %#v", params) + } +} + func messageSourceTestCases() []struct { name string value MessageSource @@ -203,6 +278,12 @@ func captureMessageSourceRequest(t *testing.T, rpcError *jsonrpc2.Error, events func captureSessionSendRequest(t *testing.T, rpcError *jsonrpc2.Error, events []SessionEvent, beforeResponse bool, invoke func(*Session)) map[string]any { t.Helper() + return captureSessionRPCRequest(t, "session.send", map[string]any{"messageId": "message-1"}, + rpcError, events, beforeResponse, invoke) +} + +func captureSessionRPCRequest(t *testing.T, method string, result map[string]any, rpcError *jsonrpc2.Error, events []SessionEvent, beforeResponse bool, invoke func(*Session)) map[string]any { + t.Helper() stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() @@ -235,15 +316,15 @@ func captureSessionSendRequest(t *testing.T, rpcError *jsonrpc2.Error, events [] errCh <- err return } - if request.Method != "session.send" { - errCh <- fmt.Errorf("expected session.send, got %s", request.Method) + if request.Method != method { + errCh <- fmt.Errorf("expected %s, got %s", method, request.Method) return } response := map[string]any{"jsonrpc": "2.0", "id": request.ID} if rpcError != nil { response["error"] = rpcError } else { - response["result"] = map[string]any{"messageId": "message-1"} + response["result"] = result } data, err := json.Marshal(response) if err != nil { diff --git a/go/rpc/factory_session_event_compat.go b/go/rpc/factory_session_event_compat.go new file mode 100644 index 0000000000..59a3f318a7 --- /dev/null +++ b/go/rpc/factory_session_event_compat.go @@ -0,0 +1,64 @@ +package rpc + +// FactoryRunUpdatedData is the legacy factory-run invalidation event payload. +// +// Use WorkflowRunUpdatedData for current runtime events. +type FactoryRunUpdatedData struct { + Revision int64 `json:"revision"` + RunID string `json:"runId"` +} + +func (*FactoryRunUpdatedData) sessionEventData() {} +func (*FactoryRunUpdatedData) Type() SessionEventType { + return SessionEventTypeFactoryRunUpdated +} + +// FactoryRunStartedData is the legacy factory-run start event payload. +// +// Use WorkflowRunStartedData for current runtime events. +type FactoryRunStartedData struct { + Attempt int64 `json:"attempt"` + FactoryName string `json:"factoryName"` + RunID string `json:"runId"` +} + +func (*FactoryRunStartedData) sessionEventData() {} +func (*FactoryRunStartedData) Type() SessionEventType { + return SessionEventTypeFactoryRunStarted +} + +// FactoryRunSettledData is the legacy factory-run terminal event payload. +// +// Use WorkflowRunSettledData for current runtime events. +type FactoryRunSettledData struct { + ConsumedNanoAiu int64 `json:"consumedNanoAiu"` + ConsumedSubagents int64 `json:"consumedSubagents"` + ElapsedMs int64 `json:"elapsedMs"` + FailureType *string `json:"failureType,omitempty"` + RunID string `json:"runId"` + Status FactoryRunSettledStatus `json:"status"` +} + +func (*FactoryRunSettledData) sessionEventData() {} +func (*FactoryRunSettledData) Type() SessionEventType { + return SessionEventTypeFactoryRunSettled +} + +// FactoryRunSettledStatus is the legacy factory-run terminal status. +// +// Use WorkflowRunSettledStatus for current runtime events. +type FactoryRunSettledStatus string + +const ( + FactoryRunSettledStatusCancelled FactoryRunSettledStatus = "cancelled" + FactoryRunSettledStatusCompleted FactoryRunSettledStatus = "completed" + FactoryRunSettledStatusError FactoryRunSettledStatus = "error" + FactoryRunSettledStatusHalted FactoryRunSettledStatus = "halted" + FactoryRunSettledStatusPaused FactoryRunSettledStatus = "paused" +) + +const ( + SessionEventTypeFactoryRunSettled SessionEventType = "factory.run_settled" + SessionEventTypeFactoryRunStarted SessionEventType = "factory.run_started" + SessionEventTypeFactoryRunUpdated SessionEventType = "factory.run_updated" +) diff --git a/go/rpc/factory_session_event_compat_test.go b/go/rpc/factory_session_event_compat_test.go new file mode 100644 index 0000000000..a952177744 --- /dev/null +++ b/go/rpc/factory_session_event_compat_test.go @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package rpc + +import ( + "encoding/json" + "testing" +) + +func TestLegacyFactoryEventsDecodeToCompatibilityTypes(t *testing.T) { + tests := []struct { + name string + wire string + want SessionEventData + }{ + { + name: "updated", + wire: `{"type":"factory.run_updated","data":{"revision":2,"runId":"run-1"}}`, + want: &FactoryRunUpdatedData{Revision: 2, RunID: "run-1"}, + }, + { + name: "started", + wire: `{"type":"factory.run_started","data":{"attempt":3,"factoryName":"fix-ci","runId":"run-1"}}`, + want: &FactoryRunStartedData{Attempt: 3, FactoryName: "fix-ci", RunID: "run-1"}, + }, + { + name: "settled", + wire: `{"type":"factory.run_settled","data":{"consumedNanoAiu":4,"consumedSubagents":1,"elapsedMs":5,"runId":"run-1","status":"completed"}}`, + want: &FactoryRunSettledData{ + ConsumedNanoAiu: 4, + ConsumedSubagents: 1, + ElapsedMs: 5, + RunID: "run-1", + Status: FactoryRunSettledStatusCompleted, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var event SessionEvent + if err := json.Unmarshal([]byte(test.wire), &event); err != nil { + t.Fatal(err) + } + switch want := test.want.(type) { + case *FactoryRunUpdatedData: + got, ok := event.Data.(*FactoryRunUpdatedData) + if !ok || *got != *want { + t.Fatalf("data = %#v", event.Data) + } + case *FactoryRunStartedData: + got, ok := event.Data.(*FactoryRunStartedData) + if !ok || *got != *want { + t.Fatalf("data = %#v", event.Data) + } + case *FactoryRunSettledData: + got, ok := event.Data.(*FactoryRunSettledData) + if !ok || *got != *want { + t.Fatalf("data = %#v", event.Data) + } + } + }) + } +} diff --git a/go/rpc/message_identity_test.go b/go/rpc/message_identity_test.go index 6124d7084a..616cf76de0 100644 --- a/go/rpc/message_identity_test.go +++ b/go/rpc/message_identity_test.go @@ -2,9 +2,68 @@ package rpc import ( "encoding/json" + "reflect" "testing" ) +func checkAdmissionCorrelationJSON[T any](t *testing.T, base string) { + t.Helper() + for _, value := range []any{nil, "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", "not-a-uuid", ""} { + var expected map[string]any + if err := json.Unmarshal([]byte(base), &expected); err != nil { + t.Fatal(err) + } + if value != nil { + expected["clientCorrelationId"] = value + } + encoded, err := json.Marshal(expected) + if err != nil { + t.Fatal(err) + } + for _, input := range [][]byte{encoded, append(encoded[:len(encoded)-1:len(encoded)-1], []byte(`,"futureField":true}`)...)} { + var decoded T + if err := json.Unmarshal(input, &decoded); err != nil { + t.Fatal(err) + } + roundTrip, err := json.Marshal(decoded) + if err != nil { + t.Fatal(err) + } + var actual map[string]any + if err := json.Unmarshal(roundTrip, &actual); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("round trip changed admission metadata: got %#v, want %#v", actual, expected) + } + } + } + var decoded T + input := base[:len(base)-1] + `,"clientCorrelationId":null}` + if err := json.Unmarshal([]byte(input), &decoded); err != nil { + t.Fatal(err) + } + encoded, err := json.Marshal(decoded) + if err != nil { + t.Fatal(err) + } + var actual map[string]any + if err := json.Unmarshal(encoded, &actual); err != nil { + t.Fatal(err) + } + if _, exists := actual["clientCorrelationId"]; exists { + t.Fatal("nil admission metadata must be omitted") + } +} + +func TestAdmissionCorrelationJSONCompatibility(t *testing.T) { + checkAdmissionCorrelationJSON[SendRequest](t, `{"prompt":"hello"}`) + checkAdmissionCorrelationJSON[SendMessageItem](t, `{"prompt":"hello"}`) + checkAdmissionCorrelationJSON[QueuePendingItems](t, `{"id":"queue-1","messageId":"canonical-1","kind":"message","displayText":"hello","agentMode":"interactive"}`) + checkAdmissionCorrelationJSON[UserMessageData](t, `{"content":"hello","messageId":"canonical-1","interactionId":"agent-loop-1"}`) +} + func TestQueuePendingItemsMessageIDJSONCompatibility(t *testing.T) { var item QueuePendingItems if err := json.Unmarshal([]byte(`{ @@ -99,3 +158,49 @@ func TestUserMessageDataMessageIDJSONCompatibility(t *testing.T) { t.Fatal("messageId should be omitted when absent") } } + +func TestToolExecutionStartTraceContextJSONCompatibility(t *testing.T) { + for _, context := range []map[string]string{ + {}, + { + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "tracestate": "vendor=value", + }, + {"traceparent": "00-11111111111111111111111111111111-2222222222222222-00"}, + {"traceparent": "invalid", "tracestate": "invalid"}, + {"traceparent": ""}, + } { + wire := map[string]string{"toolCallId": "tool-call-a", "toolName": "client-tool"} + for key, value := range context { + wire[key] = value + } + encoded, err := json.Marshal(wire) + if err != nil { + t.Fatal(err) + } + var data ToolExecutionStartData + if err := json.Unmarshal(encoded, &data); err != nil { + t.Fatal(err) + } + for key, value := range map[string]*string{ + "traceparent": data.Traceparent, + "tracestate": data.Tracestate, + } { + expected, present := context[key] + if present && (value == nil || *value != expected) || !present && value != nil { + t.Fatalf("%s presence/value changed", key) + } + } + encoded, err = json.Marshal(data) + if err != nil { + t.Fatal(err) + } + var roundTrip map[string]string + if err := json.Unmarshal(encoded, &roundTrip); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(wire, roundTrip) { + t.Fatal("tool-start data changed during round trip") + } + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 3ccb7e8432..be6acaed05 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -12096,6 +12096,11 @@ type QueuePendingItems struct { // the turn: a plan or autopilot session applies its own write gate, continuation loop and // permission posture to every drained item regardless of the mode stored here. AgentMode SendAgentMode `json:"agentMode"` + // Caller-owned diagnostic UUID from the exact accepted native session.send or sendMessages + // item, when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted for unsupported or + // identity-less rows, including snapshot-only mirrors. Not an idempotency key, + // authorization, or permission to retry; repeated values remain ambiguous. + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` // Human-readable text to display for this queue entry in the UI DisplayText string `json:"displayText"` // Stable opaque id for the canonical queued item. Batch rows share one id. @@ -12963,6 +12968,11 @@ type SendMessageItem struct { // Internal: Billable is part of the SDK's internal API surface and is not intended for // external use. Billable *bool `json:"billable,omitempty"` + // Optional caller-generated diagnostic UUID for this item only, with the same validation + // and opt-in native echo as session.send.clientCorrelationId. The batch has no + // request-level correlation value; each item retains its own value, including preceding + // context messages. Reused values do not deduplicate messages and remain ambiguous. + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` // If provided, this is shown in the timeline instead of `prompt` DisplayPrompt *string `json:"displayPrompt,omitempty"` // The user message text @@ -13048,6 +13058,12 @@ type SendRequest struct { // If false, this message will not trigger a Premium Request Unit charge. User messages // default to billable. Billable *bool `json:"billable,omitempty"` + // Optional caller-generated diagnostic UUID for this single message. Native sessions with + // RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character + // UUID on user.message and its existing pending message row. Missing, invalid, disabled, or + // unsupported metadata is ignored without rejecting the send. Does not change messageId, + // deduplicate submissions, authorize work, or make an uncertain retry safe. + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` // If provided, this is shown in the timeline instead of `prompt` DisplayPrompt *string `json:"displayPrompt,omitempty"` // How to deliver the message. `enqueue` (default) appends to the message queue. `immediate` @@ -17904,6 +17920,9 @@ type TasksSendMessageResult struct { Error *string `json:"error,omitempty"` // Whether the message was successfully delivered or steered Sent bool `json:"sent"` + // Optional exact queue admission receipt on sent=true only. No implicit event or + // execution-success claim. + WorkerCausality *WorkerCausality `json:"workerCausality,omitempty"` } // Agent type, prompt, name, and optional description and model override for the new task. @@ -19047,6 +19066,103 @@ type VisibilitySetResult struct { Synced bool `json:"synced"` } +// An observed worker admission, not a claim that execution succeeded. +// Experimental: WorkerAdmission is part of an experimental API and may change or be removed. +type WorkerAdmission struct { + // Actual AHP participant Turn UUID, not a native turn counter or provenance signal. + AhpTurnID *string `json:"ahpTurnId,omitempty"` + // May be omitted only for the matching current worker user.message. + Event *WorkerEventReference `json:"event,omitempty"` + // The producer's admission kind. + Kind WorkerAdmissionKind `json:"kind"` + // Canonical logical message identity, independent of queueItemId; at most 256 UTF-8 bytes. + MessageID string `json:"messageId"` +} + +// One indivisible source-to-reported bridge observation, not a root alias. +// Experimental: WorkerBridgeObservation is part of an experimental API and may change or be +// removed. +type WorkerBridgeObservation struct { + // Full occurrence actually emitted by that bridge. + Reported WorkerEventReference `json:"reported"` + // Full event received by this bridge. + Source WorkerEventReference `json:"source"` +} + +// Optional v1 worker diagnostics. The compact UTF-8 {"workerCausality":value} +// must fit 4096 bytes after materializing an allowed implicit self-reference. +// Ignore invalid/unknown/oversize metadata, not the product event. +// Experimental: WorkerCausality is part of an experimental API and may change or be removed. +type WorkerCausality struct { + // Complete placement-aware observed capture, not global causality or execution success. + // Empty true requires explicit native invocation attestation. + CaptureComplete bool `json:"captureComplete"` + // Provenance of this enclosing observation; never inferred from other fields. + ObservationProvenance WorkerObservationProvenance `json:"observationProvenance"` + // Sources in capture order, at most 32. Absent/unknown is not known-empty. + Sources []WorkerSource `json:"sources"` + // Supported version, exactly 1. + Version int64 `json:"version"` +} + +// Exact observed event identity. No private registration generation or execution handle. +// Experimental: WorkerEventReference is part of an experimental API and may change or be +// removed. +type WorkerEventReference struct { + // Actual event agent scope, absent for a root occurrence; at most 256 UTF-8 bytes. + AgentID *string `json:"agentId,omitempty"` + // Actual event occurrence UUID; copied without normalization. + EventID string `json:"eventId"` + // Type of the observed occurrence. + EventType WorkerEventType `json:"eventType"` + // Explicit provenance of this reference. + Provenance WorkerObservationProvenance `json:"provenance"` + // Actual runtime session scope, at most 256 UTF-8 bytes. + SessionID string `json:"sessionId"` +} + +// Exact accepted worker input, distinct from a message, event, caller correlation or Turn. +// Experimental: WorkerInput is part of an experimental API and may change or be removed. +type WorkerInput struct { + // Actual recipient task, at most 256 UTF-8 bytes. + AgentID string `json:"agentId"` + // UUID allocated for this queue item by the admitting producer. + QueueItemID string `json:"queueItemId"` + // Original invoking occurrence, never replaced by a reported/root alias. + Sender *WorkerEventReference `json:"sender,omitempty"` + // Exact captured edges in producer order. Requires sender; at most 32 whole pairs. + SenderBridges []WorkerBridgeObservation `json:"senderBridges,omitzero"` +} + +// Exact delivery and optional occurrence of a consumed worker notification. +// Experimental: WorkerNotificationReference is part of an experimental API and may change +// or be removed. +type WorkerNotificationReference struct { + // Actual notificationDeliveryId UUID. + DeliveryID string `json:"deliveryId"` + // May be omitted only on the matching current system.notification. + Event *WorkerEventReference `json:"event,omitempty"` + // Actual consumption mode. + Mode WorkerNotificationMode `json:"mode"` +} + +// One captured source at this placement and observation boundary. +// Experimental: WorkerSource is part of an experimental API and may change or be removed. +type WorkerSource struct { + // Actual admissions in capture order; at most 32. + Admissions []WorkerAdmission `json:"admissions"` + // Exact already-open iteration when an immediate notification was consumed. + AdmittedDuring *WorkerEventReference `json:"admittedDuring,omitempty"` + // Applicable observations were all captured here, never work completion or success. + CaptureComplete bool `json:"captureComplete"` + // Actual completion emission only. Absence is not an execution outcome. + Completion *WorkerEventReference `json:"completion,omitempty"` + // Indivisible accepted input identity. + Input WorkerInput `json:"input"` + // Actual consumed notification, when observed. + Notification *WorkerNotificationReference `json:"notification,omitempty"` +} + // Parameters for cooperatively aborting a workflow body. // Experimental: WorkflowAbortRequest is part of an experimental API and may change or be // removed. @@ -23668,8 +23784,9 @@ const ( ResponseFormatTypeJSONSchema ResponseFormatType = "json_schema" ) -// Origin of the sandbox choice supplied by the host. This value describes preference or -// session intent; it does not authorize bypassing managed policy. +// Origin of the sandbox choice supplied by the host. Settings-derived origins let managed +// policy floor the host preference; do not tag explicit session overrides as +// settings-derived. // Experimental: SandboxConfigSource is part of an experimental API and may change or be // removed. type SandboxConfigSource string @@ -23681,7 +23798,7 @@ const ( SandboxConfigSourceRepositoryPolicy SandboxConfigSource = "repository_policy" // The user disabled the sandbox for the current session. SandboxConfigSourceSessionDisabled SandboxConfigSource = "session_disabled" - // An explicit session-scoped choice selected the sandbox state, such as a command-line flag. + // A command-line flag selected the sandbox state for this session. SandboxConfigSourceSessionFlag SandboxConfigSource = "session_flag" // The client disabled the sandbox because the host cannot enforce it. SandboxConfigSourceUnsupportedHost SandboxConfigSource = "unsupported_host" @@ -24853,6 +24970,59 @@ const ( VerbosityMedium Verbosity = "medium" ) +// Why this exact worker admission was made. +// Experimental: WorkerAdmissionKind is part of an experimental API and may change or be +// removed. +type WorkerAdmissionKind string + +const ( + // A queued worker input was admitted. + WorkerAdmissionKindQueuedInput WorkerAdmissionKind = "queued_input" + // A system continuation was admitted. + WorkerAdmissionKindSystemContinuation WorkerAdmissionKind = "system_continuation" +) + +// Supported observed occurrences. Chronological parentId is not a causal reference. +// Experimental: WorkerEventType is part of an experimental API and may change or be removed. +type WorkerEventType string + +const ( + // Observed assistant turn start occurrence. + WorkerEventTypeAssistantTurnStart WorkerEventType = "assistant.turn_start" + // Observed worker completion occurrence. + WorkerEventTypeSubagentCompleted WorkerEventType = "subagent.completed" + // Observed worker notification occurrence. + WorkerEventTypeSystemNotification WorkerEventType = "system.notification" + // Observed tool execution start occurrence. + WorkerEventTypeToolExecutionStart WorkerEventType = "tool.execution_start" + // Observed admitted user message occurrence. + WorkerEventTypeUserMessage WorkerEventType = "user.message" +) + +// How the owned notification was consumed. +// Experimental: WorkerNotificationMode is part of an experimental API and may change or be +// removed. +type WorkerNotificationMode string + +const ( + // The notification was consumed during an already-open iteration. + WorkerNotificationModeImmediate WorkerNotificationMode = "immediate" + // The notification was consumed through the queued input path. + WorkerNotificationModeQueued WorkerNotificationMode = "queued" +) + +// Producer of an observation, not the execution location of every referenced source. +// Experimental: WorkerObservationProvenance is part of an experimental API and may change +// or be removed. +type WorkerObservationProvenance string + +const ( + // Observed by the AHP coordinator. + WorkerObservationProvenanceAhpCoordinator WorkerObservationProvenance = "ahp_coordinator" + // Observed by the native runtime. + WorkerObservationProvenanceNative WorkerObservationProvenance = "native" +) + // Execution-critical workflow storage operation. // Experimental: WorkflowDurableOperation is part of an experimental API and may change or // be removed. @@ -34725,6 +34895,9 @@ func (a *SessionRPC) Send(ctx context.Context, params *SendRequest) (*SendResult if params.Billable != nil { req["billable"] = *params.Billable } + if params.ClientCorrelationID != nil { + req["clientCorrelationId"] = *params.ClientCorrelationID + } if params.DisplayPrompt != nil { req["displayPrompt"] = *params.DisplayPrompt } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index b9ddaaa861..04f00f38ca 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -6,6 +6,7 @@ package rpc import ( "encoding/json" "errors" + "github.com/github/copilot-sdk/go/internal/diagnosticmetadata" ) func unmarshalAuthInfo(data []byte) (AuthInfo, error) { @@ -7469,12 +7470,13 @@ func (r *SendAttachmentsToMessageParams) UnmarshalJSON(data []byte) error { func (r *SendMessageItem) UnmarshalJSON(data []byte) error { type rawSendMessageItem struct { - Attachments []json.RawMessage `json:"attachments,omitzero"` - Billable *bool `json:"billable,omitempty"` - DisplayPrompt *string `json:"displayPrompt,omitempty"` - Prompt string `json:"prompt"` - RequiredTool *string `json:"requiredTool,omitempty"` - Source *string `json:"source,omitempty"` + Attachments []json.RawMessage `json:"attachments,omitzero"` + Billable *bool `json:"billable,omitempty"` + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + Prompt string `json:"prompt"` + RequiredTool *string `json:"requiredTool,omitempty"` + Source *string `json:"source,omitempty"` } var raw rawSendMessageItem if err := json.Unmarshal(data, &raw); err != nil { @@ -7491,6 +7493,7 @@ func (r *SendMessageItem) UnmarshalJSON(data []byte) error { } } r.Billable = raw.Billable + r.ClientCorrelationID = raw.ClientCorrelationID r.DisplayPrompt = raw.DisplayPrompt r.Prompt = raw.Prompt r.RequiredTool = raw.RequiredTool @@ -7500,20 +7503,21 @@ func (r *SendMessageItem) UnmarshalJSON(data []byte) error { func (r *SendRequest) UnmarshalJSON(data []byte) error { type rawSendRequest struct { - AgentMode *SendAgentMode `json:"agentMode,omitempty"` - Attachments []json.RawMessage `json:"attachments,omitzero"` - Billable *bool `json:"billable,omitempty"` - DisplayPrompt *string `json:"displayPrompt,omitempty"` - Mode *SendMode `json:"mode,omitempty"` - Prepend *bool `json:"prepend,omitempty"` - Prompt string `json:"prompt"` - RequestHeaders map[string]string `json:"requestHeaders,omitzero"` - RequiredTool *string `json:"requiredTool,omitempty"` - ResponseFormat *ResponseFormat `json:"responseFormat,omitempty"` - Source *string `json:"source,omitempty"` - Traceparent *string `json:"traceparent,omitempty"` - Tracestate *string `json:"tracestate,omitempty"` - Wait *bool `json:"wait,omitempty"` + AgentMode *SendAgentMode `json:"agentMode,omitempty"` + Attachments []json.RawMessage `json:"attachments,omitzero"` + Billable *bool `json:"billable,omitempty"` + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` + DisplayPrompt *string `json:"displayPrompt,omitempty"` + Mode *SendMode `json:"mode,omitempty"` + Prepend *bool `json:"prepend,omitempty"` + Prompt string `json:"prompt"` + RequestHeaders map[string]string `json:"requestHeaders,omitzero"` + RequiredTool *string `json:"requiredTool,omitempty"` + ResponseFormat *ResponseFormat `json:"responseFormat,omitempty"` + Source *string `json:"source,omitempty"` + Traceparent *string `json:"traceparent,omitempty"` + Tracestate *string `json:"tracestate,omitempty"` + Wait *bool `json:"wait,omitempty"` } var raw rawSendRequest if err := json.Unmarshal(data, &raw); err != nil { @@ -7531,6 +7535,7 @@ func (r *SendRequest) UnmarshalJSON(data []byte) error { } } r.Billable = raw.Billable + r.ClientCorrelationID = raw.ClientCorrelationID r.DisplayPrompt = raw.DisplayPrompt r.Mode = raw.Mode r.Prepend = raw.Prepend @@ -9505,6 +9510,24 @@ func (r *TasksPromoteCurrentToBackgroundResult) UnmarshalJSON(data []byte) error return nil } +func (r *TasksSendMessageResult) UnmarshalJSON(data []byte) error { + type rawTasksSendMessageResult struct { + Error *string `json:"error,omitempty"` + Sent bool `json:"sent"` + WorkerCausality json.RawMessage `json:"workerCausality,omitempty"` + } + var raw rawTasksSendMessageResult + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.Error = raw.Error + r.Sent = raw.Sent + if !diagnosticmetadata.ReadWorkerCausality(raw.WorkerCausality, &r.WorkerCausality) { + r.WorkerCausality = nil + } + return nil +} + func (r *TasksUpdateRequest) UnmarshalJSON(data []byte) error { type rawTasksUpdateRequest struct { ID string `json:"id"` diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index c4e9734912..0e07cd3d1f 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -6,6 +6,7 @@ package rpc import ( "encoding/json" "errors" + "github.com/github/copilot-sdk/go/internal/diagnosticmetadata" "time" ) @@ -233,6 +234,24 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeFactoryRunSettled: + var d FactoryRunSettledData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeFactoryRunStarted: + var d FactoryRunStartedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeFactoryRunUpdated: + var d FactoryRunUpdatedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeHookEnd: var d HookEndData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -976,6 +995,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { type rawUserMessageData struct { AgentMode *UserMessageAgentMode `json:"agentMode,omitempty"` Attachments []json.RawMessage `json:"attachments,omitzero"` + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` Content string `json:"content"` Delivery *UserMessageDelivery `json:"delivery,omitempty"` InteractionID *string `json:"interactionId,omitempty"` @@ -988,6 +1008,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { SupportedNativeDocumentMIMETypes []string `json:"supportedNativeDocumentMimeTypes,omitzero"` TransformedContent *string `json:"transformedContent,omitempty"` TurnID *string `json:"turnId,omitempty"` + WorkerCausality json.RawMessage `json:"workerCausality,omitempty"` } var raw rawUserMessageData if err := json.Unmarshal(data, &raw); err != nil { @@ -1004,6 +1025,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { r.Attachments = append(r.Attachments, value) } } + r.ClientCorrelationID = raw.ClientCorrelationID r.Content = raw.Content r.Delivery = raw.Delivery r.InteractionID = raw.InteractionID @@ -1016,6 +1038,31 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { r.SupportedNativeDocumentMIMETypes = raw.SupportedNativeDocumentMIMETypes r.TransformedContent = raw.TransformedContent r.TurnID = raw.TurnID + if !diagnosticmetadata.ReadWorkerCausality(raw.WorkerCausality, &r.WorkerCausality) { + r.WorkerCausality = nil + } + return nil +} + +func (r *AssistantTurnStartData) UnmarshalJSON(data []byte) error { + type rawAssistantTurnStartData struct { + InteractionID *string `json:"interactionId,omitempty"` + Model *string `json:"model,omitempty"` + ParentToolCallID *string `json:"parentToolCallId,omitempty"` + TurnID string `json:"turnId"` + WorkerCausality json.RawMessage `json:"workerCausality,omitempty"` + } + var raw rawAssistantTurnStartData + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.InteractionID = raw.InteractionID + r.Model = raw.Model + r.ParentToolCallID = raw.ParentToolCallID + r.TurnID = raw.TurnID + if !diagnosticmetadata.ReadWorkerCausality(raw.WorkerCausality, &r.WorkerCausality) { + r.WorkerCausality = nil + } return nil } @@ -1830,6 +1877,7 @@ func (r *SystemNotificationData) UnmarshalJSON(data []byte) error { Content string `json:"content"` Kind json.RawMessage `json:"kind"` ResponsesReasoning *ResponsesReasoning `json:"responsesReasoning,omitempty"` + WorkerCausality json.RawMessage `json:"workerCausality,omitempty"` } var raw rawSystemNotificationData if err := json.Unmarshal(data, &raw); err != nil { @@ -1844,6 +1892,9 @@ func (r *SystemNotificationData) UnmarshalJSON(data []byte) error { r.Kind = value } r.ResponsesReasoning = raw.ResponsesReasoning + if !diagnosticmetadata.ReadWorkerCausality(raw.WorkerCausality, &r.WorkerCausality) { + r.WorkerCausality = nil + } return nil } diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index b70b94522d..8c2f61e087 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -2230,6 +2230,8 @@ type UserMessageData struct { AgentMode *UserMessageAgentMode `json:"agentMode,omitempty"` // Files, selections, or GitHub references attached to the message Attachments []Attachment `json:"attachments,omitzero"` + // Exact caller-owned diagnostic UUID carried by this accepted native session.send or sendMessages item when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted when input, native ownership, or support is missing. Independent of the canonical messageId; not an idempotency key, authorization, or permission to retry. Multiple messages with the same value remain ambiguous. + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` // The user's message text as displayed in the timeline Content string `json:"content"` // How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. @@ -2242,7 +2244,7 @@ type UserMessageData struct { MessageID *string `json:"messageId,omitempty"` // Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` - // Parent agent task ID for background telemetry correlated to this user turn + // Task ID minted when the runtime prepares this user-message run. This is not a parent interaction ID or worker instance ID and must not be equated with CAPI's X-Parent-Agent-Id. ParentAgentTaskID *string `json:"parentAgentTaskId,omitempty"` // Provider reasoning settings anchored before this model-facing message for cache-stable replay; the historical responsesReasoning name is retained for compatibility ResponsesReasoning *ResponsesReasoning `json:"responsesReasoning,omitempty"` @@ -2254,6 +2256,8 @@ type UserMessageData struct { TransformedContent *string `json:"transformedContent,omitempty"` // The agent-loop turn ID that consumed this message; absent when no agent-loop turn consumed it TurnID *string `json:"turnId,omitempty"` + // Optional worker admission observations; self identity requires the matching enclosing message and agent. + WorkerCausality *WorkerCausality `json:"workerCausality,omitempty"` } func (*UserMessageData) sessionEventData() {} @@ -2907,7 +2911,7 @@ type SubagentCompletedData struct { ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Authority or runtime mechanism responsible for sub-agent model selection ModelSelectionSource *SubagentModelSelectionSource `json:"modelSelectionSource,omitempty"` - // Tool call ID of the parent tool invocation that spawned this sub-agent + // Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed by the sub-agent TotalTokens *int64 `json:"totalTokens,omitempty"` @@ -2944,7 +2948,7 @@ type SubagentFailedData struct { ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Authority or runtime mechanism responsible for sub-agent model selection ModelSelectionSource *SubagentModelSelectionSource `json:"modelSelectionSource,omitempty"` - // Tool call ID of the parent tool invocation that spawned this sub-agent + // Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed before the sub-agent failed TotalTokens *int64 `json:"totalTokens,omitempty"` @@ -2979,7 +2983,7 @@ type SubagentStartedData struct { Resumable *bool `json:"resumable,omitempty"` // Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and workflow agents); absent for sub-agents created through other runtime paths. TaskModelSource *SubagentTaskModelSource `json:"taskModelSource,omitempty"` - // Tool call ID of the parent tool invocation that spawned this sub-agent + // Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. ToolCallID string `json:"toolCallId"` // Root id of the workflow run that spawned this sub-agent, when it was spawned by one. WorkflowRunID *string `json:"workflowRunId,omitempty"` @@ -2996,6 +3000,8 @@ type SystemNotificationData struct { Kind SystemNotification `json:"kind"` // Provider reasoning settings anchored before this model-facing message for cache-stable replay; the historical responsesReasoning name is retained for compatibility ResponsesReasoning *ResponsesReasoning `json:"responsesReasoning,omitempty"` + // Optional owned worker notification observations; an omitted notification event refers only to this occurrence. + WorkerCausality *WorkerCausality `json:"workerCausality,omitempty"` } func (*SystemNotificationData) sessionEventData() {} @@ -3146,6 +3152,10 @@ type ToolExecutionStartData struct { ToolName string `json:"toolName"` // Human-readable display title for the tool, when the selected tool descriptor has a non-empty title. ToolTitle *string `json:"toolTitle,omitempty"` + // W3C traceparent of this tool's active runtime execute_tool span. Available on live events when tool-context propagation is enabled; absent when the span is unavailable or on persisted history. This diagnostic context does not authorize execution. + Traceparent *string `json:"traceparent,omitempty"` + // Optional W3C tracestate associated with traceparent. Omitted when no valid vendor state is available. + Tracestate *string `json:"tracestate,omitempty"` // Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event TurnID *string `json:"turnId,omitempty"` } @@ -3210,6 +3220,8 @@ type AssistantTurnStartData struct { ParentToolCallID *string `json:"parentToolCallId,omitempty"` // Identifier for this turn within the agentic loop, typically a stringified turn number TurnID string `json:"turnId"` + // Optional bounded worker observations. Missing or invalid metadata is unavailable, not known-empty. + WorkerCausality *WorkerCausality `json:"workerCausality,omitempty"` } func (*AssistantTurnStartData) sessionEventData() {} diff --git a/go/session.go b/go/session.go index f4e7696518..0088b996f8 100644 --- a/go/session.go +++ b/go/session.go @@ -430,16 +430,17 @@ func newSession( func (s *Session) Send(ctx context.Context, options MessageOptions) (string, error) { traceparent, tracestate := getTraceContext(ctx) req := sessionSendRequest{ - SessionID: s.SessionID, - Prompt: options.Prompt, - Source: options.Source, - DisplayPrompt: options.DisplayPrompt, - Attachments: options.Attachments, - Mode: options.Mode, - AgentMode: options.AgentMode, - Traceparent: traceparent, - Tracestate: tracestate, - RequestHeaders: options.RequestHeaders, + SessionID: s.SessionID, + Prompt: options.Prompt, + ClientCorrelationID: options.ClientCorrelationID, + Source: options.Source, + DisplayPrompt: options.DisplayPrompt, + Attachments: options.Attachments, + Mode: options.Mode, + AgentMode: options.AgentMode, + Traceparent: traceparent, + Tracestate: tracestate, + RequestHeaders: options.RequestHeaders, } if options.ResponseSchema != nil { strict := true diff --git a/go/types.go b/go/types.go index c33dda4ac5..63c10d5889 100644 --- a/go/types.go +++ b/go/types.go @@ -2503,6 +2503,10 @@ type MessageOptions struct { ResponseSchema map[string]any // Prompt is the message to send Prompt string + // ClientCorrelationID is an optional caller-owned diagnostic UUID for this RPC + // admission, sent unchanged. Supported runtimes echo accepted values only with + // RUNTIME_ADMISSION_TRACE_CONTEXT enabled. It is not an idempotency key. + ClientCorrelationID *string // Source identifies the message origin independently of Mode and AgentMode. // The empty value omits source from the request, preserving runtime defaults. Source MessageSource @@ -2987,17 +2991,18 @@ type sessionAbortRequest struct { } type sessionSendRequest struct { - ResponseFormat *rpc.ResponseFormat `json:"responseFormat,omitempty"` - SessionID string `json:"sessionId"` - Prompt string `json:"prompt"` - Source MessageSource `json:"source,omitempty"` - DisplayPrompt string `json:"displayPrompt,omitempty"` - Attachments []Attachment `json:"attachments,omitempty"` - Mode string `json:"mode,omitempty"` - AgentMode AgentMode `json:"agentMode,omitempty"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` - RequestHeaders map[string]string `json:"requestHeaders,omitempty"` + ResponseFormat *rpc.ResponseFormat `json:"responseFormat,omitempty"` + SessionID string `json:"sessionId"` + Prompt string `json:"prompt"` + ClientCorrelationID *string `json:"clientCorrelationId,omitempty"` + Source MessageSource `json:"source,omitempty"` + DisplayPrompt string `json:"displayPrompt,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` + Mode string `json:"mode,omitempty"` + AgentMode AgentMode `json:"agentMode,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` + RequestHeaders map[string]string `json:"requestHeaders,omitempty"` } // sessionSendResponse is the response from session.send diff --git a/go/worker_causality_test.go b/go/worker_causality_test.go new file mode 100644 index 0000000000..57ce4a2b30 --- /dev/null +++ b/go/worker_causality_test.go @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package copilot + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "testing" + + "github.com/github/copilot-sdk/go/rpc" +) + +type workerCausalityCorpus struct { + Valid []struct { + Name string + Event json.RawMessage + Result json.RawMessage + } + Invalid []struct { + Name string + Value any + } + InvalidRaw []struct { + Name string + JSON string + } + Boundaries []struct { + Name string + Value any + Accepted bool + } + WorkflowCompleted struct { + Event json.RawMessage + } + LegacyWorkflowCompleted struct { + Event json.RawMessage + } +} + +func workerCorpus(t *testing.T) workerCausalityCorpus { + t.Helper() + data, err := os.ReadFile("../test/worker-causality.json") + if err != nil { + t.Fatal(err) + } + var corpus workerCausalityCorpus + if err := json.Unmarshal(data, &corpus); err != nil { + t.Fatal(err) + } + return corpus +} + +func workerProduct(t *testing.T, value map[string]any, event bool) []byte { + t.Helper() + wire, err := json.Marshal(value) + if err != nil { + t.Fatal(err) + } + var product any = &rpc.TasksSendMessageResult{} + if event { + product = &SessionEvent{} + } + if err := json.Unmarshal(wire, product); err != nil { + t.Fatal(err) + } + result, err := json.Marshal(product) + if err != nil { + t.Fatal(err) + } + return result +} + +func workerPayload(t *testing.T, wire []byte, event bool) (map[string]any, map[string]any) { + t.Helper() + var value map[string]any + if err := json.Unmarshal(wire, &value); err != nil { + t.Fatal(err) + } + if event { + return value, value["data"].(map[string]any) + } + return value, value +} + +func TestWorkerCausalityPublicReaders(t *testing.T) { + corpus := workerCorpus(t) + for _, test := range corpus.Valid { + t.Run(test.Name, func(t *testing.T) { + event := len(test.Event) > 0 + wire := test.Result + if event { + wire = test.Event + } + value, payload := workerPayload(t, wire, event) + _, result := workerPayload(t, workerProduct(t, value, event), event) + if !reflect.DeepEqual(result["workerCausality"], payload["workerCausality"]) { + t.Fatal("typed reader changed captured source identities") + } + delete(payload, "workerCausality") + baseline := string(workerProduct(t, value, event)) + for _, invalid := range corpus.Invalid { + payload["workerCausality"] = invalid.Value + if actual := string(workerProduct(t, value, event)); actual != baseline { + t.Fatalf("%s changed product bytes: %s", invalid.Name, actual) + } + } + }) + } +} + +func TestAssistantTurnStartPreservesParentToolCallIDWithWorkerCausality(t *testing.T) { + var event rpc.SessionEvent + wire := []byte(`{ + "type":"assistant.turn_start", + "data":{ + "turnId":"1", + "parentToolCallId":"parent-tool-call", + "workerCausality":{ + "version":1, + "observationProvenance":"native", + "sources":[], + "captureComplete":true + } + } + }`) + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + data, ok := event.Data.(*rpc.AssistantTurnStartData) + if !ok { + t.Fatalf("data type = %T", event.Data) + } + if data.ParentToolCallID == nil || *data.ParentToolCallID != "parent-tool-call" { + t.Fatalf("parent tool call ID = %v", data.ParentToolCallID) + } + encoded, err := json.Marshal(event) + if err != nil { + t.Fatal(err) + } + var roundTrip map[string]any + if err := json.Unmarshal(encoded, &roundTrip); err != nil { + t.Fatal(err) + } + payload := roundTrip["data"].(map[string]any) + if payload["parentToolCallId"] != "parent-tool-call" { + t.Fatalf("round-trip parent tool call ID = %v", payload["parentToolCallId"]) + } +} + +func TestWorkerCausalityExactUTF8Budget(t *testing.T) { + corpus := workerCorpus(t) + for _, test := range corpus.Boundaries { + t.Run(test.Name, func(t *testing.T) { + value, payload := workerPayload(t, corpus.Valid[0].Event, true) + payload["workerCausality"] = test.Value + _, result := workerPayload(t, workerProduct(t, value, true), true) + if _, present := result["workerCausality"]; present != test.Accepted { + t.Fatalf("metadata presence = %v, want %v", present, test.Accepted) + } + if result["content"] != payload["content"] { + t.Fatal("product content changed") + } + }) + } +} + +func TestCanonicalWorkflowCompletionDecodesTypedFields(t *testing.T) { + corpus := workerCorpus(t) + for _, raw := range []json.RawMessage{ + corpus.WorkflowCompleted.Event, + corpus.LegacyWorkflowCompleted.Event, + } { + var event rpc.SessionEvent + if err := json.Unmarshal(raw, &event); err != nil { + t.Fatal(err) + } + data, ok := event.Data.(*rpc.SystemNotificationData) + if !ok { + t.Fatalf("data type = %T", event.Data) + } + kind, ok := data.Kind.(*rpc.SystemNotificationWorkflowCompleted) + if !ok { + t.Fatalf("kind type = %T", data.Kind) + } + if kind.WorkflowName != "fix-ci" || kind.RunID != "run-1" || + kind.Status != rpc.SystemNotificationWorkflowCompletedStatusCompleted || + kind.ConsumedSubagents != 1 { + t.Fatalf("unexpected workflow completion: %+v", kind) + } + } +} + +func TestInvalidUnicodeScalarIdentitiesAreUnavailable(t *testing.T) { + for _, test := range workerCorpus(t).InvalidRaw { + var result rpc.TasksSendMessageResult + wire := []byte(fmt.Sprintf(`{"sent":true,"workerCausality":%s}`, test.JSON)) + if err := json.Unmarshal(wire, &result); err != nil { + t.Fatalf("%s rejected product result: %v", test.Name, err) + } + encoded, err := json.Marshal(result) + if err != nil { + t.Fatal(err) + } + if string(encoded) != `{"sent":true}` { + t.Fatalf("%s preserved invalid metadata: %s", test.Name, encoded) + } + } +} diff --git a/go/zsession_events.go b/go/zsession_events.go index 5fce4a01db..eb88d5f8a3 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -462,6 +462,17 @@ type ( UserToolSessionApprovalWorkflow = rpc.UserToolSessionApprovalWorkflow UserToolSessionApprovalWrite = rpc.UserToolSessionApprovalWrite Verbosity = rpc.Verbosity + WorkerAdmission = rpc.WorkerAdmission + WorkerAdmissionKind = rpc.WorkerAdmissionKind + WorkerBridgeObservation = rpc.WorkerBridgeObservation + WorkerCausality = rpc.WorkerCausality + WorkerEventReference = rpc.WorkerEventReference + WorkerEventType = rpc.WorkerEventType + WorkerInput = rpc.WorkerInput + WorkerNotificationMode = rpc.WorkerNotificationMode + WorkerNotificationReference = rpc.WorkerNotificationReference + WorkerObservationProvenance = rpc.WorkerObservationProvenance + WorkerSource = rpc.WorkerSource WorkflowPermissionOperation = rpc.WorkflowPermissionOperation WorkflowPermissionPhase = rpc.WorkflowPermissionPhase WorkflowRunSettledData = rpc.WorkflowRunSettledData @@ -1067,6 +1078,17 @@ const ( VerbosityHigh = rpc.VerbosityHigh VerbosityLow = rpc.VerbosityLow VerbosityMedium = rpc.VerbosityMedium + WorkerAdmissionKindQueuedInput = rpc.WorkerAdmissionKindQueuedInput + WorkerAdmissionKindSystemContinuation = rpc.WorkerAdmissionKindSystemContinuation + WorkerEventTypeAssistantTurnStart = rpc.WorkerEventTypeAssistantTurnStart + WorkerEventTypeSubagentCompleted = rpc.WorkerEventTypeSubagentCompleted + WorkerEventTypeSystemNotification = rpc.WorkerEventTypeSystemNotification + WorkerEventTypeToolExecutionStart = rpc.WorkerEventTypeToolExecutionStart + WorkerEventTypeUserMessage = rpc.WorkerEventTypeUserMessage + WorkerNotificationModeImmediate = rpc.WorkerNotificationModeImmediate + WorkerNotificationModeQueued = rpc.WorkerNotificationModeQueued + WorkerObservationProvenanceAhpCoordinator = rpc.WorkerObservationProvenanceAhpCoordinator + WorkerObservationProvenanceNative = rpc.WorkerObservationProvenanceNative WorkflowPermissionOperationAuthor = rpc.WorkflowPermissionOperationAuthor WorkflowPermissionOperationRun = rpc.WorkflowPermissionOperationRun WorkflowRunSettledStatusCancelled = rpc.WorkflowRunSettledStatusCancelled diff --git a/java/scripts/codegen/java.test.ts b/java/scripts/codegen/java.test.ts index 2d86ff3cd4..10d87fd03c 100644 --- a/java/scripts/codegen/java.test.ts +++ b/java/scripts/codegen/java.test.ts @@ -62,6 +62,145 @@ function renderPayload(dataSchema: JSONSchema7): string { }, "com.github.copilot.generated"); } +test("admission correlation preserves existing Java record constructors", async () => { + const schema: JSONSchema7 = { + type: "object", + properties: { + prompt: { type: "string" }, + clientCorrelationId: { type: "string" }, + displayPrompt: { type: "string" }, + }, + required: ["prompt"], + }; + const files = await renderRpcTypes({ + session: { + send: { + rpcMethod: "session.send", + params: { ...schema, title: "SessionSendParams" }, + }, + }, + }, {}); + const code = [...files.values()].join("\n"); + assert.match(code, /public SessionSendParams\(String prompt, String displayPrompt\)/); + assert.match(code, /this\(prompt, displayPrompt, null\);/); + + const pending = generateRpcClass("QueuePendingItems", { + type: "object", + properties: { + id: { type: "string" }, + messageId: { type: "string" }, + clientCorrelationId: { type: "string" }, + source: { type: "string" }, + }, + }, new Map(), "com.github.copilot.generated").code; + assert.match( + pending, + /public QueuePendingItems\(String id, String messageId, String source\)/ + ); + assert.match(pending, /this\(id, messageId, source, null\);/); + assert.match(pending, /public QueuePendingItems\(String id, String messageId\)/); + assert.match(pending, /this\(id, messageId, null, null\);/); + + const event = renderEventVariantClass({ + typeName: "user.message", + className: "UserMessageEvent", + dataSchema: { + type: "object", + properties: { + content: { type: "string" }, + clientCorrelationId: { type: "string" }, + messageId: { type: "string" }, + }, + }, + }, "com.github.copilot.generated"); + assert.match(event, /public UserMessageEventData\(String content, String messageId\)/); + assert.match(event, /this\(content, messageId, null\);/); +}); + +test("worker causality preserves existing Java event and RPC record constructors", () => { + const event = renderEventVariantClass({ + typeName: "user.message", + className: "UserMessageEvent", + dataSchema: { + type: "object", + properties: { + workerCausality: { type: "object" }, + content: { type: "string" }, + clientCorrelationId: { type: "string" }, + messageId: { type: "string" }, + }, + }, + }, "com.github.copilot.generated"); + assert.match( + event, + /public UserMessageEventData\(String content, String messageId, String clientCorrelationId\)/ + ); + assert.match( + event, + /@com\.fasterxml\.jackson\.databind\.annotation\.JsonDeserialize\(using = com\.github\.copilot\.WorkerCausalityDeserializer\.class\)/ + ); + assert.match(event, /this\(content, messageId, null, clientCorrelationId\);/); + assert.match(event, /public UserMessageEventData\(String content, String messageId\)/); + assert.match(event, /this\(content, messageId, null, null\);/); + + const notification = renderEventVariantClass({ + typeName: "system.notification", + className: "SystemNotificationEvent", + dataSchema: { + type: "object", + properties: { + workerCausality: { type: "object" }, + content: { type: "string" }, + kind: { type: "object" }, + }, + }, + }, "com.github.copilot.generated"); + assert.match( + notification, + /public SystemNotificationEventData\(String content, Map kind\)/ + ); + assert.match(notification, /this\(content, kind, null\);/); + + const result = generateRpcClass("SessionTasksSendMessageResult", { + type: "object", + properties: { + workerCausality: { type: "object" }, + sent: { type: "boolean" }, + error: { type: "string" }, + }, + }, new Map(), "com.github.copilot.generated").code; + assert.match( + result, + /@com\.fasterxml\.jackson\.databind\.annotation\.JsonDeserialize\(using = com\.github\.copilot\.WorkerCausalityDeserializer\.class\)/ + ); + assert.match(result, /public SessionTasksSendMessageResult\(Boolean sent, String error\)/); + assert.match(result, /this\(sent, error, null\);/); +}); + +test("worker UUID identities remain strings without changing ordinary UUID fields", () => { + const nestedTypes = new Map(); + assert.equal( + schemaTypeToJava( + { type: "string", format: "uuid" }, + true, + "WorkerEventReference", + "eventId", + nestedTypes, + ).javaType, + "String", + ); + assert.equal( + schemaTypeToJava( + { type: "string", format: "uuid" }, + true, + "OtherReference", + "eventId", + nestedTypes, + ).javaType, + "UUID", + ); +}); + for (const keyword of ["anyOf", "oneOf"] as const) { test(`root ${keyword} payload preserves raw JSON and existing data descriptors`, () => { const source = renderPayload({ diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 7f55a05690..9ff5e16fc7 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -1199,6 +1199,59 @@ interface JavaTypeResolution { promotedUnionTypes: Set; } +const WORKER_STRING_UUID_PROPERTIES = new Set([ + "WorkerAdmission.ahpTurnId", + "WorkerEventReference.eventId", + "WorkerInput.queueItemId", + "WorkerNotificationReference.deliveryId", +]); + +const COMPATIBILITY_FIELD_ORDER: Readonly> = { + AssistantTurnStartEventData: ["turnId", "model", "interactionId", "parentToolCallId", "workerCausality"], + SystemNotificationEventData: ["content", "kind", "responsesReasoning", "workerCausality"], + ToolExecutionStartEventData: [ + "toolCallId", "toolName", "toolTitle", "arguments", "shellToolInfo", "model", "rte", + "mcpServerName", "mcpConfigServerName", "mcpToolName", "mcpTransport", "mcpConfigSource", + "turnId", "displayVerbatim", "toolDescription", "parentToolCallId", "fusion", "traceparent", + "tracestate", + ], + UserMessageEventData: [ + "content", "responsesReasoning", "messageId", "transformedContent", "attachments", + "supportedNativeDocumentMimeTypes", "nativeDocumentPathFallbackPaths", "source", "delivery", + "agentMode", "isAutopilotContinuation", "interactionId", "turnId", "parentAgentTaskId", + "workerCausality", "clientCorrelationId", + ], + QueuePendingItems: ["id", "messageId", "kind", "displayText", "agentMode", "source", "clientCorrelationId"], + SendMessageItem: ["prompt", "displayPrompt", "attachments", "billable", "requiredTool", "source", "clientCorrelationId"], + SessionSendParams: [ + "sessionId", "prompt", "displayPrompt", "attachments", "mode", "prepend", "billable", + "requiredTool", "source", "agentMode", "requestHeaders", "responseFormat", "traceparent", + "tracestate", "wait", "clientCorrelationId", "jsonSchema", "type", + ], + SessionTasksSendMessageResult: ["sent", "error", "workerCausality"], +}; + +function isWorkerStringUuidProperty(context: string, propName: string): boolean { + return WORKER_STRING_UUID_PROPERTIES.has(`${context}.${propName}`); +} + +function orderCompatibilityFields( + className: string, + fields: T[], + name: (field: T) => string, +): T[] { + const order = COMPATIBILITY_FIELD_ORDER[className]; + if (!order) return fields; + const indexes = new Map(order.map((fieldName, index) => [fieldName, index])); + return fields + .map((field, originalIndex) => ({ field, originalIndex })) + .sort((left, right) => + (indexes.get(name(left.field)) ?? order.length + left.originalIndex) - + (indexes.get(name(right.field)) ?? order.length + right.originalIndex) + ) + .map(({ field }) => field); +} + export function schemaTypeToJava( schema: JSONSchema7, required: boolean, @@ -1289,6 +1342,9 @@ export function schemaTypeToJava( if (schema.type === "string") { if (schema.format === "uuid") { + if (isWorkerStringUuidProperty(context, propName)) { + return { javaType: "String", imports }; + } imports.add("java.util.UUID"); return { javaType: "UUID", imports }; } @@ -1401,6 +1457,19 @@ interface EventVariant { deprecated?: boolean; } +const LEGACY_FACTORY_EVENT_VARIANTS: EventVariant[] = [ + { typeName: "factory.run_updated", className: "FactoryRunUpdatedEvent", dataSchema: null }, + { typeName: "factory.run_started", className: "FactoryRunStartedEvent", dataSchema: null }, + { typeName: "factory.run_settled", className: "FactoryRunSettledEvent", dataSchema: null }, +]; + +const LEGACY_FACTORY_EVENT_FILES = [ + "FactoryRunUpdatedEvent.java", + "FactoryRunStartedEvent.java", + "FactoryRunSettledEvent.java", + "FactoryRunSettledStatus.java", +]; + function extractEventVariants(schema: JSONSchema7): EventVariant[] { const definitions = schema.definitions as Record; const sessionEvent = definitions?.SessionEvent; @@ -1445,19 +1514,33 @@ async function generateSessionEvents(schemaPath: string): Promise { // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; pendingStandaloneTypes.clear(); + promotedNestedUnionTypes.clear(); promotedSealedUnionTypes.clear(); + if (currentDefinitions.SystemNotification) { + promotedNestedUnionTypes.add("SystemNotification"); + } const variants = extractEventVariants(schema); + const legacyVariants = variants.some((variant) => variant.typeName === "workflow.run_updated") + ? LEGACY_FACTORY_EVENT_VARIANTS.filter( + (legacy) => !variants.some((variant) => variant.typeName === legacy.typeName) + ) + : []; const packageName = "com.github.copilot.generated"; const packageDir = `sdk/src/generated/java/com/github/copilot/generated`; // Generate base SessionEvent class - await generateSessionEventBaseClass(variants, packageName, packageDir); + await generateSessionEventBaseClass([...variants, ...legacyVariants], packageName, packageDir); // Generate one class file per event variant for (const variant of variants) { await generateEventVariantClass(variant, packageName, packageDir); } + for (const fileName of legacyVariants.length > 0 ? LEGACY_FACTORY_EVENT_FILES : []) { + const relativePath = `${packageDir}/${fileName}`; + const content = await fs.readFile(path.join(REPO_ROOT, relativePath), "utf-8"); + await writeGeneratedFile(relativePath, content); + } // Generate standalone types discovered via $ref resolution await generatePendingStandaloneTypes(packageName, packageDir, GENERATED_FROM_SESSION_EVENTS); @@ -1726,10 +1809,11 @@ export function renderEventVariantClass(variant: EventVariant, packageName: stri jsonName: string; javaName: string; javaType: string; + typedAccessorType?: string; description?: string; } - const dataFields: FieldInfo[] = []; + let dataFields: FieldInfo[] = []; if (!hasUnionData && variant.dataSchema?.properties) { for (const [propName, propSchema] of Object.entries(variant.dataSchema.properties)) { @@ -1738,14 +1822,24 @@ export function renderEventVariantClass(variant: EventVariant, packageName: stri // Record components are always boxed (nullable by design). const result = schemaTypeToJava(prop, false, `${variant.className}Data`, propName, nestedTypes); for (const imp of result.imports) allImports.add(imp); + const preserveSystemNotificationKindAbi = + variant.className === "SystemNotificationEvent" && + propName === "kind" && + result.javaType === "SystemNotification"; dataFields.push({ jsonName: propName, javaName: toCamelCase(propName), - javaType: result.javaType, + javaType: preserveSystemNotificationKindAbi ? "Object" : result.javaType, + typedAccessorType: preserveSystemNotificationKindAbi ? result.javaType : undefined, description: prop.description, }); } } + dataFields = orderCompatibilityFields( + `${variant.className}Data`, + dataFields, + (field) => field.jsonName, + ); // Whether a data record should be emitted (always when dataSchema is present) const hasDataSchema = variant.dataSchema !== null; @@ -1828,10 +1922,25 @@ export function renderEventVariantClass(variant: EventVariant, packageName: stri if (field.description) { lines.push(` /** ${field.description} */`); } + if (field.jsonName === "workerCausality") { + lines.push(` @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.github.copilot.WorkerCausalityDeserializer.class)`); + } + if (field.typedAccessorType) { + lines.push(` @com.fasterxml.jackson.databind.annotation.JsonDeserialize(as = ${field.typedAccessorType}.class)`); + } lines.push(` @JsonProperty("${field.jsonName}") ${field.javaType} ${field.javaName}${comma}`); } lines.push(` ) {`); } + lines.push(...renderCompatibilityConstructors(`${variant.className}Data`, dataFields, " ")); + for (const field of dataFields) { + if (!field.typedAccessorType) continue; + lines.push(""); + lines.push(` /** Returns the typed ${field.javaName} value when decoded from the wire. */`); + lines.push(` public ${field.typedAccessorType} typed${toPascalCase(field.javaName)}() {`); + lines.push(` return ${field.javaName} instanceof ${field.typedAccessorType} typed ? typed : null;`); + lines.push(` }`); + } // Render nested types inside Data record for (const [, nested] of nestedTypes) { lines.push(...renderNestedType(nested, 2, nestedTypes, allImports)); @@ -2054,6 +2163,55 @@ function schemaAllowsNull(schema: JSONSchema7): boolean { ); } +/** Preserve existing Java record descriptors when optional diagnostics are added. */ +function renderCompatibilityConstructors( + className: string, + fields: { javaName: string; javaType: string }[], + indent: string, +): string[] { + const names = new Set(fields.map((field) => field.javaName)); + let omissions: string[][] = []; + if (className === "UserMessageEventData") { + omissions = names.has("workerCausality") + ? [["workerCausality"], ["workerCausality", "clientCorrelationId"]] + : [["clientCorrelationId"]]; + } else if (className === "QueuePendingItems") { + omissions = [["clientCorrelationId"], ["clientCorrelationId", "source"]]; + } else if (["SessionSendParams", "SendMessageItem"].includes(className)) { + omissions = [["clientCorrelationId"]]; + } else if (["SystemNotificationEventData", "SessionTasksSendMessageResult"].includes(className)) { + omissions = [["workerCausality"]]; + } else if (className === "AssistantTurnStartEventData") { + omissions = [ + ["workerCausality"], + ["parentToolCallId"], + ["workerCausality", "parentToolCallId"], + ]; + } + return omissions.flatMap((omitted, index) => { + if (!omitted.every((field) => names.has(field))) return []; + const previous = fields.filter((field) => !omitted.includes(field.javaName)); + const parameters = previous.map((field) => `${field.javaType} ${field.javaName}`).join(", "); + const arguments_ = fields + .map((field) => omitted.includes(field.javaName) ? "null" : field.javaName) + .join(", "); + const description = omitted.includes("workerCausality") && omitted.includes("parentToolCallId") + ? "Creates a value without optional worker diagnostics or a parent task tool call ID." + : omitted.includes("workerCausality") + ? "Creates a value without optional worker diagnostics." + : omitted.includes("parentToolCallId") + ? "Creates a value without an optional parent task tool call ID." + : "Creates a value without optional admission correlation metadata."; + return [ + ...(index === 0 ? [] : [""]), + `${indent}/** ${description} */`, + `${indent}public ${className}(${parameters}) {`, + `${indent} this(${arguments_});`, + `${indent}}`, + ]; + }); +} + /** Generate a Java record for a JSON Schema object type. Returns the class content. */ export function generateRpcClass( className: string, @@ -2071,7 +2229,7 @@ export function generateRpcClass( const properties = Object.entries(schema.properties || {}).filter(([propName]) => !omittedProperties.has(propName)); const required = new Set(schema.required || []); - const fields = properties.flatMap(([propName, propSchema]) => { + let fields = properties.flatMap(([propName, propSchema]) => { if (typeof propSchema !== "object") return []; const prop = propSchema as JSONSchema7; // Record components are always boxed (nullable by design). @@ -2085,6 +2243,7 @@ export function generateRpcClass( includeNull: preserveRequiredNulls && required.has(propName) && schemaAllowsNull(prop), }]; }); + fields = orderCompatibilityFields(className, fields, (field) => field.propName); lines.push(`@JsonInclude(JsonInclude.Include.NON_NULL)`); lines.push(`@JsonIgnoreProperties(ignoreUnknown = true)`); @@ -2101,11 +2260,16 @@ export function generateRpcClass( if (f.includeNull) { lines.push(` @JsonInclude(JsonInclude.Include.ALWAYS)`); } + if (f.propName === "workerCausality") { + lines.push(` @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.github.copilot.WorkerCausalityDeserializer.class)`); + } lines.push(` @JsonProperty("${f.propName}") ${f.javaType} ${f.javaName}${comma}`); } lines.push(`) {`); } + lines.push(...renderCompatibilityConstructors(className, fields, " ")); + // Add nested types as nested records/enums inside this record for (const [, nested] of localNestedTypes) { lines.push(...renderNestedType(nested, 1, new Map(), imports)); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java index d020391606..91a487efa7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantTurnStartEvent.java @@ -41,7 +41,24 @@ public record AssistantTurnStartEventData( /** CAPI interaction ID for correlating this turn with upstream telemetry */ @JsonProperty("interactionId") String interactionId, /** Parent task tool call ID when this turn belongs to a sub-agent */ - @JsonProperty("parentToolCallId") String parentToolCallId + @JsonProperty("parentToolCallId") String parentToolCallId, + /** Optional bounded worker observations. Missing or invalid metadata is unavailable, not known-empty. */ + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.github.copilot.WorkerCausalityDeserializer.class) + @JsonProperty("workerCausality") WorkerCausality workerCausality ) { + /** Creates a value without optional worker diagnostics. */ + public AssistantTurnStartEventData(String turnId, String model, String interactionId, String parentToolCallId) { + this(turnId, model, interactionId, parentToolCallId, null); + } + + /** Creates a value without an optional parent task tool call ID. */ + public AssistantTurnStartEventData(String turnId, String model, String interactionId, WorkerCausality workerCausality) { + this(turnId, model, interactionId, null, workerCausality); + } + + /** Creates a value without optional worker diagnostics or a parent task tool call ID. */ + public AssistantTurnStartEventData(String turnId, String model, String interactionId) { + this(turnId, model, interactionId, null, null); + } } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledEvent.java new file mode 100644 index 0000000000..58ffa9cb3c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledEvent.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "factory.run_settled". Ephemeral signal that a factory run reached a terminal status. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class FactoryRunSettledEvent extends SessionEvent { + + @Override + public String getType() { return "factory.run_settled"; } + + @JsonProperty("data") + private FactoryRunSettledEventData data; + + public FactoryRunSettledEventData getData() { return data; } + public void setData(FactoryRunSettledEventData data) { this.data = data; } + + /** Data payload for {@link FactoryRunSettledEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record FactoryRunSettledEventData( + /** Identifier of the factory run that settled. */ + @JsonProperty("runId") String runId, + /** Terminal status the run committed. */ + @JsonProperty("status") FactoryRunSettledStatus status, + /** Subagents this run consumed against its limits. */ + @JsonProperty("consumedSubagents") Long consumedSubagents, + /** AI credits this run consumed, in nano-AIU. */ + @JsonProperty("consumedNanoAiu") Long consumedNanoAiu, + /** Active milliseconds accumulated across every attempt of this run. */ + @JsonProperty("elapsedMs") Long elapsedMs, + /** Typed failure class recorded on the run, when it failed with one (e.g. `factory_limit_reached`). */ + @JsonProperty("failureType") String failureType + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledStatus.java new file mode 100644 index 0000000000..828ddc9ee3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunSettledStatus.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Terminal status a factory run committed. A settled run is never `pending` or `running`, so those two members of the run-status domain are deliberately absent. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum FactoryRunSettledStatus { + /** The {@code completed} variant. */ + COMPLETED("completed"), + /** The {@code halted} variant. */ + HALTED("halted"), + /** The {@code paused} variant. */ + PAUSED("paused"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"), + /** The {@code error} variant. */ + ERROR("error"); + + private final String value; + FactoryRunSettledStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static FactoryRunSettledStatus fromValue(String value) { + for (FactoryRunSettledStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown FactoryRunSettledStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunStartedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunStartedEvent.java new file mode 100644 index 0000000000..691c7185d4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunStartedEvent.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "factory.run_started". Ephemeral signal that a factory run attempt began executing. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class FactoryRunStartedEvent extends SessionEvent { + + @Override + public String getType() { return "factory.run_started"; } + + @JsonProperty("data") + private FactoryRunStartedEventData data; + + public FactoryRunStartedEventData getData() { return data; } + public void setData(FactoryRunStartedEventData data) { this.data = data; } + + /** Data payload for {@link FactoryRunStartedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record FactoryRunStartedEventData( + /** Identifier of the factory run that started. */ + @JsonProperty("runId") String runId, + /** Name of the factory this run executes. Low cardinality by construction. */ + @JsonProperty("factoryName") String factoryName, + /** Attempt number this start committed; a resumed run increments it. */ + @JsonProperty("attempt") Long attempt + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunUpdatedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunUpdatedEvent.java new file mode 100644 index 0000000000..33b542058b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FactoryRunUpdatedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "factory.run_updated". Ephemeral invalidation signal for a changed factory run. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class FactoryRunUpdatedEvent extends SessionEvent { + + @Override + public String getType() { return "factory.run_updated"; } + + @JsonProperty("data") + private FactoryRunUpdatedEventData data; + + public FactoryRunUpdatedEventData getData() { return data; } + public void setData(FactoryRunUpdatedEventData data) { this.data = data; } + + /** Data payload for {@link FactoryRunUpdatedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record FactoryRunUpdatedEventData( + /** Factory run identifier. */ + @JsonProperty("runId") String runId, + /** Monotonic revision now available for the run. */ + @JsonProperty("revision") Long revision + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index 864072a110..252fb42df2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -173,7 +173,10 @@ @JsonSubTypes.Type(value = SessionCanvasRecordedEvent.class, name = "session.canvas.recorded"), @JsonSubTypes.Type(value = SessionCanvasRemovedEvent.class, name = "session.canvas.removed"), @JsonSubTypes.Type(value = SessionExtensionsAttachmentsPushedEvent.class, name = "session.extensions.attachments_pushed"), - @JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete") + @JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete"), + @JsonSubTypes.Type(value = FactoryRunUpdatedEvent.class, name = "factory.run_updated"), + @JsonSubTypes.Type(value = FactoryRunStartedEvent.class, name = "factory.run_started"), + @JsonSubTypes.Type(value = FactoryRunSettledEvent.class, name = "factory.run_settled") }) @javax.annotation.processing.Generated("copilot-sdk-codegen") public abstract sealed class SessionEvent permits @@ -326,6 +329,9 @@ public abstract sealed class SessionEvent permits SessionCanvasRemovedEvent, SessionExtensionsAttachmentsPushedEvent, McpAppToolCallCompleteEvent, + FactoryRunUpdatedEvent, + FactoryRunStartedEvent, + FactoryRunSettledEvent, UnknownSessionEvent { /** Unique event identifier (UUID v4), generated when the event is emitted. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java index 064efdd544..294c840139 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java @@ -34,7 +34,7 @@ public final class SubagentCompletedEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SubagentCompletedEventData( - /** Tool call ID of the parent tool invocation that spawned this sub-agent */ + /** Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. */ @JsonProperty("toolCallId") String toolCallId, /** Internal name of the sub-agent */ @JsonProperty("agentName") String agentName, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java index 1b0d99cbee..f00467b594 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java @@ -34,7 +34,7 @@ public final class SubagentFailedEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SubagentFailedEventData( - /** Tool call ID of the parent tool invocation that spawned this sub-agent */ + /** Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. */ @JsonProperty("toolCallId") String toolCallId, /** Internal name of the sub-agent */ @JsonProperty("agentName") String agentName, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java index 8c8cde1c90..5352339b3a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentStartedEvent.java @@ -34,7 +34,7 @@ public final class SubagentStartedEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SubagentStartedEventData( - /** Tool call ID of the parent tool invocation that spawned this sub-agent */ + /** Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. */ @JsonProperty("toolCallId") String toolCallId, /** Internal name of the sub-agent */ @JsonProperty("agentName") String agentName, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotification.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotification.java new file mode 100644 index 0000000000..e6480dcdfa --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotification.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Structured metadata identifying what triggered this notification + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SystemNotificationAgentCompleted.class, name = "agent_completed"), + @JsonSubTypes.Type(value = SystemNotificationAgentIdle.class, name = "agent_idle"), + @JsonSubTypes.Type(value = SystemNotificationNewInboxMessage.class, name = "new_inbox_message"), + @JsonSubTypes.Type(value = SystemNotificationShellCompleted.class, name = "shell_completed"), + @JsonSubTypes.Type(value = SystemNotificationShellDetachedCompleted.class, name = "shell_detached_completed"), + @JsonSubTypes.Type(value = SystemNotificationInstructionDiscovered.class, name = "instruction_discovered"), + @JsonSubTypes.Type(value = SystemNotificationWorkflowCompleted.class, name = "workflow_completed"), + @JsonSubTypes.Type(value = SystemNotificationUnclassified.class, name = "unclassified") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class SystemNotification { + + /** + * Returns the discriminator value for this variant. + * + * @return the type discriminator + */ + public abstract String getType(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentCompleted.java new file mode 100644 index 0000000000..fddae09691 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentCompleted.java @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for a background agent that completed or failed, including agent ID, type, status, description, and prompt. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationAgentCompleted extends SystemNotification { + + @JsonProperty("type") + private final String type = "agent_completed"; + + @Override + public String getType() { return type; } + + /** Unique task identifier */ + @JsonProperty("agentId") + private String agentId; + + /** Friendly, non-unique name intended for display */ + @JsonProperty("displayName") + private String displayName; + + /** Type of the agent (e.g., explore, task, general-purpose) */ + @JsonProperty("agentType") + private String agentType; + + /** Whether the agent completed successfully or failed */ + @JsonProperty("status") + private SystemNotificationAgentCompletedStatus status; + + /** Human-readable description of the agent task */ + @JsonProperty("description") + private String description; + + /** The full prompt given to the background agent */ + @JsonProperty("prompt") + private String prompt; + + public String getAgentId() { return agentId; } + public void setAgentId(String agentId) { this.agentId = agentId; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getAgentType() { return agentType; } + public void setAgentType(String agentType) { this.agentType = agentType; } + + public SystemNotificationAgentCompletedStatus getStatus() { return status; } + public void setStatus(SystemNotificationAgentCompletedStatus status) { this.status = status; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getPrompt() { return prompt; } + public void setPrompt(String prompt) { this.prompt = prompt; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentCompletedStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentCompletedStatus.java new file mode 100644 index 0000000000..a78d83e3e4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentCompletedStatus.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Whether the agent completed successfully or failed + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SystemNotificationAgentCompletedStatus { + /** The {@code completed} variant. */ + COMPLETED("completed"), + /** The {@code failed} variant. */ + FAILED("failed"); + + private final String value; + SystemNotificationAgentCompletedStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SystemNotificationAgentCompletedStatus fromValue(String value) { + for (SystemNotificationAgentCompletedStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SystemNotificationAgentCompletedStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentIdle.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentIdle.java new file mode 100644 index 0000000000..8d82a00547 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationAgentIdle.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for a background agent that became idle, including agent ID, type, and description. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationAgentIdle extends SystemNotification { + + @JsonProperty("type") + private final String type = "agent_idle"; + + @Override + public String getType() { return type; } + + /** Unique task identifier */ + @JsonProperty("agentId") + private String agentId; + + /** Friendly, non-unique name intended for display */ + @JsonProperty("displayName") + private String displayName; + + /** Type of the agent (e.g., explore, task, general-purpose) */ + @JsonProperty("agentType") + private String agentType; + + /** Human-readable description of the agent task */ + @JsonProperty("description") + private String description; + + public String getAgentId() { return agentId; } + public void setAgentId(String agentId) { this.agentId = agentId; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getAgentType() { return agentType; } + public void setAgentType(String agentType) { this.agentType = agentType; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationEvent.java index 21e2abbafc..923e5e07a8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationEvent.java @@ -37,9 +37,22 @@ public record SystemNotificationEventData( /** The notification text, typically wrapped in XML tags */ @JsonProperty("content") String content, /** Structured metadata identifying what triggered this notification */ + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(as = SystemNotification.class) @JsonProperty("kind") Object kind, /** Provider reasoning settings anchored before this model-facing message for cache-stable replay; the historical responsesReasoning name is retained for compatibility */ - @JsonProperty("responsesReasoning") ResponsesReasoning responsesReasoning + @JsonProperty("responsesReasoning") ResponsesReasoning responsesReasoning, + /** Optional owned worker notification observations; an omitted notification event refers only to this occurrence. */ + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.github.copilot.WorkerCausalityDeserializer.class) + @JsonProperty("workerCausality") WorkerCausality workerCausality ) { + /** Creates a value without optional worker diagnostics. */ + public SystemNotificationEventData(String content, Object kind, ResponsesReasoning responsesReasoning) { + this(content, kind, responsesReasoning, null); + } + + /** Returns the typed kind value when decoded from the wire. */ + public SystemNotification typedKind() { + return kind instanceof SystemNotification typed ? typed : null; + } } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationInstructionDiscovered.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationInstructionDiscovered.java new file mode 100644 index 0000000000..658eba96b6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationInstructionDiscovered.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for an instruction file discovered during tool access, including source, trigger file, and tool. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationInstructionDiscovered extends SystemNotification { + + @JsonProperty("type") + private final String type = "instruction_discovered"; + + @Override + public String getType() { return type; } + + /** Relative path to the discovered instruction file */ + @JsonProperty("sourcePath") + private String sourcePath; + + /** Path of the file access that triggered discovery */ + @JsonProperty("triggerFile") + private String triggerFile; + + /** Tool command that triggered discovery (currently always 'view') */ + @JsonProperty("triggerTool") + private String triggerTool; + + /** Human-readable label for the timeline (e.g., 'AGENTS.md from packages/billing/') */ + @JsonProperty("description") + private String description; + + public String getSourcePath() { return sourcePath; } + public void setSourcePath(String sourcePath) { this.sourcePath = sourcePath; } + + public String getTriggerFile() { return triggerFile; } + public void setTriggerFile(String triggerFile) { this.triggerFile = triggerFile; } + + public String getTriggerTool() { return triggerTool; } + public void setTriggerTool(String triggerTool) { this.triggerTool = triggerTool; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationNewInboxMessage.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationNewInboxMessage.java new file mode 100644 index 0000000000..6cec82db77 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationNewInboxMessage.java @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for a new inbox message, including entry ID, sender details, and summary. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationNewInboxMessage extends SystemNotification { + + @JsonProperty("type") + private final String type = "new_inbox_message"; + + @Override + public String getType() { return type; } + + /** Unique identifier of the inbox entry */ + @JsonProperty("entryId") + private String entryId; + + /** Human-readable name of the sender */ + @JsonProperty("senderName") + private String senderName; + + /** Category of the sender (e.g., sidekick-agent, plugin, hook) */ + @JsonProperty("senderType") + private String senderType; + + /** Short summary shown before the agent decides whether to read the inbox */ + @JsonProperty("summary") + private String summary; + + public String getEntryId() { return entryId; } + public void setEntryId(String entryId) { this.entryId = entryId; } + + public String getSenderName() { return senderName; } + public void setSenderName(String senderName) { this.senderName = senderName; } + + public String getSenderType() { return senderType; } + public void setSenderType(String senderType) { this.senderType = senderType; } + + public String getSummary() { return summary; } + public void setSummary(String summary) { this.summary = summary; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationShellCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationShellCompleted.java new file mode 100644 index 0000000000..5e3d62ea99 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationShellCompleted.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for a shell session that completed, including shell ID, optional exit code, and description. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationShellCompleted extends SystemNotification { + + @JsonProperty("type") + private final String type = "shell_completed"; + + @Override + public String getType() { return type; } + + /** Unique identifier of the shell session */ + @JsonProperty("shellId") + private String shellId; + + /** Exit code of the shell command, if available */ + @JsonProperty("exitCode") + private Long exitCode; + + /** Human-readable description of the command */ + @JsonProperty("description") + private String description; + + public String getShellId() { return shellId; } + public void setShellId(String shellId) { this.shellId = shellId; } + + public Long getExitCode() { return exitCode; } + public void setExitCode(Long exitCode) { this.exitCode = exitCode; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationShellDetachedCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationShellDetachedCompleted.java new file mode 100644 index 0000000000..11eabee738 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationShellDetachedCompleted.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for a detached shell session that completed, including shell ID and description. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationShellDetachedCompleted extends SystemNotification { + + @JsonProperty("type") + private final String type = "shell_detached_completed"; + + @Override + public String getType() { return type; } + + /** Unique identifier of the detached shell session */ + @JsonProperty("shellId") + private String shellId; + + /** Human-readable description of the command */ + @JsonProperty("description") + private String description; + + public String getShellId() { return shellId; } + public void setShellId(String shellId) { this.shellId = shellId; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationUnclassified.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationUnclassified.java new file mode 100644 index 0000000000..77d6315d68 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationUnclassified.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata from an external host that does not match a runtime-owned notification kind. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationUnclassified extends SystemNotification { + + @JsonProperty("type") + private final String type = "unclassified"; + + @Override + public String getType() { return type; } + + /** Opaque metadata supplied by the external host, when present. */ + @JsonProperty("metadata") + private Object metadata; + + public Object getMetadata() { return metadata; } + public void setMetadata(Object metadata) { this.metadata = metadata; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationWorkflowCompleted.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationWorkflowCompleted.java new file mode 100644 index 0000000000..6a367ae5d9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationWorkflowCompleted.java @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * System notification metadata for a workflow execution attempt that reached a terminal state. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SystemNotificationWorkflowCompleted extends SystemNotification { + + @JsonProperty("type") + private final String type = "workflow_completed"; + + @Override + public String getType() { return type; } + + /** Workflow run identifier. */ + @JsonProperty("runId") + private String runId; + + /** Persisted workflow name. */ + @JsonProperty("workflowName") + private String workflowName; + + /** Terminal status reached by this execution attempt. */ + @JsonProperty("status") + private SystemNotificationWorkflowCompletedStatus status; + + /** Subagents consumed by the run across all attempts. */ + @JsonProperty("consumedSubagents") + private Long consumedSubagents; + + /** Accumulated active execution time in milliseconds. */ + @JsonProperty("elapsedMs") + private Long elapsedMs; + + /** Consumed AI usage in nano-AIU. */ + @JsonProperty("consumedNanoAiu") + private Long consumedNanoAiu; + + /** Execution attempt that reached this terminal state. */ + @JsonProperty("attempt") + private Long attempt; + + /** Bounded prompt-safe preview of the completed result. */ + @JsonProperty("resultPreview") + private String resultPreview; + + /** Machine-readable terminal failure details, when present. */ + @JsonProperty("failure") + private Object failure; + + /** Actionable run_dynamic_workflow resume guidance for a resource-limit failure. */ + @JsonProperty("retryGuidance") + private String retryGuidance; + + /** Pause initiator metadata when this attempt settled as paused. */ + @JsonProperty("pauseInfo") + private Object pauseInfo; + + public String getRunId() { return runId; } + public void setRunId(String runId) { this.runId = runId; } + + public String getWorkflowName() { return workflowName; } + public void setWorkflowName(String workflowName) { this.workflowName = workflowName; } + + public SystemNotificationWorkflowCompletedStatus getStatus() { return status; } + public void setStatus(SystemNotificationWorkflowCompletedStatus status) { this.status = status; } + + public Long getConsumedSubagents() { return consumedSubagents; } + public void setConsumedSubagents(Long consumedSubagents) { this.consumedSubagents = consumedSubagents; } + + public Long getElapsedMs() { return elapsedMs; } + public void setElapsedMs(Long elapsedMs) { this.elapsedMs = elapsedMs; } + + public Long getConsumedNanoAiu() { return consumedNanoAiu; } + public void setConsumedNanoAiu(Long consumedNanoAiu) { this.consumedNanoAiu = consumedNanoAiu; } + + public Long getAttempt() { return attempt; } + public void setAttempt(Long attempt) { this.attempt = attempt; } + + public String getResultPreview() { return resultPreview; } + public void setResultPreview(String resultPreview) { this.resultPreview = resultPreview; } + + public Object getFailure() { return failure; } + public void setFailure(Object failure) { this.failure = failure; } + + public String getRetryGuidance() { return retryGuidance; } + public void setRetryGuidance(String retryGuidance) { this.retryGuidance = retryGuidance; } + + public Object getPauseInfo() { return pauseInfo; } + public void setPauseInfo(Object pauseInfo) { this.pauseInfo = pauseInfo; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationWorkflowCompletedStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationWorkflowCompletedStatus.java new file mode 100644 index 0000000000..d53b09f135 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SystemNotificationWorkflowCompletedStatus.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Terminal status reached by a workflow execution attempt. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum SystemNotificationWorkflowCompletedStatus { + /** The {@code completed} variant. */ + COMPLETED("completed"), + /** The {@code halted} variant. */ + HALTED("halted"), + /** The {@code paused} variant. */ + PAUSED("paused"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"), + /** The {@code error} variant. */ + ERROR("error"); + + private final String value; + SystemNotificationWorkflowCompletedStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static SystemNotificationWorkflowCompletedStatus fromValue(String value) { + for (SystemNotificationWorkflowCompletedStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown SystemNotificationWorkflowCompletedStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java index 8bd686beaf..654a2e0760 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java @@ -67,7 +67,11 @@ public record ToolExecutionStartEventData( /** Tool call ID of the parent tool invocation when this event originates from a sub-agent */ @JsonProperty("parentToolCallId") String parentToolCallId, /** Experimental HydraFusion attribution for this tool execution. */ - @JsonProperty("fusion") FusionAttribution fusion + @JsonProperty("fusion") FusionAttribution fusion, + /** W3C traceparent of this tool's active runtime execute_tool span. Available on live events when tool-context propagation is enabled; absent when the span is unavailable or on persisted history. This diagnostic context does not authorize execution. */ + @JsonProperty("traceparent") String traceparent, + /** Optional W3C tracestate associated with traceparent. Omitted when no valid vendor state is available. */ + @JsonProperty("tracestate") String tracestate ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 3b68529dbc..536912d1c9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -61,8 +61,22 @@ public record UserMessageEventData( @JsonProperty("interactionId") String interactionId, /** The agent-loop turn ID that consumed this message; absent when no agent-loop turn consumed it */ @JsonProperty("turnId") String turnId, - /** Parent agent task ID for background telemetry correlated to this user turn */ - @JsonProperty("parentAgentTaskId") String parentAgentTaskId + /** Task ID minted when the runtime prepares this user-message run. This is not a parent interaction ID or worker instance ID and must not be equated with CAPI's X-Parent-Agent-Id. */ + @JsonProperty("parentAgentTaskId") String parentAgentTaskId, + /** Optional worker admission observations; self identity requires the matching enclosing message and agent. */ + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.github.copilot.WorkerCausalityDeserializer.class) + @JsonProperty("workerCausality") WorkerCausality workerCausality, + /** Exact caller-owned diagnostic UUID carried by this accepted native session.send or sendMessages item when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted when input, native ownership, or support is missing. Independent of the canonical messageId; not an idempotency key, authorization, or permission to retry. Multiple messages with the same value remain ambiguous. */ + @JsonProperty("clientCorrelationId") String clientCorrelationId ) { + /** Creates a value without optional worker diagnostics. */ + public UserMessageEventData(String content, ResponsesReasoning responsesReasoning, String messageId, String transformedContent, List attachments, List supportedNativeDocumentMimeTypes, List nativeDocumentPathFallbackPaths, String source, UserMessageDelivery delivery, UserMessageAgentMode agentMode, Boolean isAutopilotContinuation, String interactionId, String turnId, String parentAgentTaskId, String clientCorrelationId) { + this(content, responsesReasoning, messageId, transformedContent, attachments, supportedNativeDocumentMimeTypes, nativeDocumentPathFallbackPaths, source, delivery, agentMode, isAutopilotContinuation, interactionId, turnId, parentAgentTaskId, null, clientCorrelationId); + } + + /** Creates a value without optional worker diagnostics. */ + public UserMessageEventData(String content, ResponsesReasoning responsesReasoning, String messageId, String transformedContent, List attachments, List supportedNativeDocumentMimeTypes, List nativeDocumentPathFallbackPaths, String source, UserMessageDelivery delivery, UserMessageAgentMode agentMode, Boolean isAutopilotContinuation, String interactionId, String turnId, String parentAgentTaskId) { + this(content, responsesReasoning, messageId, transformedContent, attachments, supportedNativeDocumentMimeTypes, nativeDocumentPathFallbackPaths, source, delivery, agentMode, isAutopilotContinuation, interactionId, turnId, parentAgentTaskId, null, null); + } } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerAdmission.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerAdmission.java new file mode 100644 index 0000000000..d60e2ab664 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerAdmission.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * An observed worker admission, not a claim that execution succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerAdmission( + /** The producer's admission kind. */ + @JsonProperty("kind") WorkerAdmissionKind kind, + /** Canonical logical message identity, independent of queueItemId; at most 256 UTF-8 bytes. */ + @JsonProperty("messageId") String messageId, + /** May be omitted only for the matching current worker user.message. */ + @JsonProperty("event") WorkerEventReference event, + /** Actual AHP participant Turn UUID, not a native turn counter or provenance signal. */ + @JsonProperty("ahpTurnId") String ahpTurnId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerAdmissionKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerAdmissionKind.java new file mode 100644 index 0000000000..5cb66b3ebf --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerAdmissionKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Why this exact worker admission was made. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerAdmissionKind { + /** The {@code queued_input} variant. */ + QUEUED_INPUT("queued_input"), + /** The {@code system_continuation} variant. */ + SYSTEM_CONTINUATION("system_continuation"); + + private final String value; + WorkerAdmissionKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerAdmissionKind fromValue(String value) { + for (WorkerAdmissionKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerAdmissionKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerBridgeObservation.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerBridgeObservation.java new file mode 100644 index 0000000000..76fe42af6c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerBridgeObservation.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * One indivisible source-to-reported bridge observation, not a root alias. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerBridgeObservation( + /** Full event received by this bridge. */ + @JsonProperty("source") WorkerEventReference source, + /** Full occurrence actually emitted by that bridge. */ + @JsonProperty("reported") WorkerEventReference reported +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerCausality.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerCausality.java new file mode 100644 index 0000000000..6b3b35e760 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerCausality.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Optional v1 worker diagnostics. The compact UTF-8 {"workerCausality":value} +must fit 4096 bytes after materializing an allowed implicit self-reference. +Ignore invalid/unknown/oversize metadata, not the product event. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerCausality( + /** Supported version, exactly 1. */ + @JsonProperty("version") Long version, + /** Provenance of this enclosing observation; never inferred from other fields. */ + @JsonProperty("observationProvenance") WorkerObservationProvenance observationProvenance, + /** Sources in capture order, at most 32. Absent/unknown is not known-empty. */ + @JsonProperty("sources") List sources, + /** Complete placement-aware observed capture, not global causality or execution success. +Empty true requires explicit native invocation attestation. */ + @JsonProperty("captureComplete") Boolean captureComplete +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerEventReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerEventReference.java new file mode 100644 index 0000000000..ad3a74ef12 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerEventReference.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Exact observed event identity. No private registration generation or execution handle. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerEventReference( + /** Actual runtime session scope, at most 256 UTF-8 bytes. */ + @JsonProperty("sessionId") String sessionId, + /** Actual event occurrence UUID; copied without normalization. */ + @JsonProperty("eventId") String eventId, + /** Actual event agent scope, absent for a root occurrence; at most 256 UTF-8 bytes. */ + @JsonProperty("agentId") String agentId, + /** Type of the observed occurrence. */ + @JsonProperty("eventType") WorkerEventType eventType, + /** Explicit provenance of this reference. */ + @JsonProperty("provenance") WorkerObservationProvenance provenance +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerEventType.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerEventType.java new file mode 100644 index 0000000000..f89bcda964 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerEventType.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Supported observed occurrences. Chronological parentId is not a causal reference. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerEventType { + /** The {@code tool.execution_start} variant. */ + TOOL_EXECUTION_START("tool.execution_start"), + /** The {@code user.message} variant. */ + USER_MESSAGE("user.message"), + /** The {@code subagent.completed} variant. */ + SUBAGENT_COMPLETED("subagent.completed"), + /** The {@code system.notification} variant. */ + SYSTEM_NOTIFICATION("system.notification"), + /** The {@code assistant.turn_start} variant. */ + ASSISTANT_TURN_START("assistant.turn_start"); + + private final String value; + WorkerEventType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerEventType fromValue(String value) { + for (WorkerEventType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerEventType value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerInput.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerInput.java new file mode 100644 index 0000000000..2047557430 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerInput.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Exact accepted worker input, distinct from a message, event, caller correlation or Turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerInput( + /** UUID allocated for this queue item by the admitting producer. */ + @JsonProperty("queueItemId") String queueItemId, + /** Actual recipient task, at most 256 UTF-8 bytes. */ + @JsonProperty("agentId") String agentId, + /** Original invoking occurrence, never replaced by a reported/root alias. */ + @JsonProperty("sender") WorkerEventReference sender, + /** Exact captured edges in producer order. Requires sender; at most 32 whole pairs. */ + @JsonProperty("senderBridges") List senderBridges +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerNotificationMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerNotificationMode.java new file mode 100644 index 0000000000..cbdc6d7687 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerNotificationMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * How the owned notification was consumed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerNotificationMode { + /** The {@code queued} variant. */ + QUEUED("queued"), + /** The {@code immediate} variant. */ + IMMEDIATE("immediate"); + + private final String value; + WorkerNotificationMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerNotificationMode fromValue(String value) { + for (WorkerNotificationMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerNotificationMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerNotificationReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerNotificationReference.java new file mode 100644 index 0000000000..6653814f8f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerNotificationReference.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Exact delivery and optional occurrence of a consumed worker notification. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerNotificationReference( + /** Actual notificationDeliveryId UUID. */ + @JsonProperty("deliveryId") String deliveryId, + /** May be omitted only on the matching current system.notification. */ + @JsonProperty("event") WorkerEventReference event, + /** Actual consumption mode. */ + @JsonProperty("mode") WorkerNotificationMode mode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerObservationProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerObservationProvenance.java new file mode 100644 index 0000000000..2e10b629b2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerObservationProvenance.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Producer of an observation, not the execution location of every referenced source. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerObservationProvenance { + /** The {@code native} variant. */ + NATIVE("native"), + /** The {@code ahp_coordinator} variant. */ + AHP_COORDINATOR("ahp_coordinator"); + + private final String value; + WorkerObservationProvenance(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerObservationProvenance fromValue(String value) { + for (WorkerObservationProvenance v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerObservationProvenance value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/WorkerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerSource.java new file mode 100644 index 0000000000..33dab65b9c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/WorkerSource.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * One captured source at this placement and observation boundary. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerSource( + /** Indivisible accepted input identity. */ + @JsonProperty("input") WorkerInput input, + /** Actual admissions in capture order; at most 32. */ + @JsonProperty("admissions") List admissions, + /** Applicable observations were all captured here, never work completion or success. */ + @JsonProperty("captureComplete") Boolean captureComplete, + /** Actual completion emission only. Absence is not an execution outcome. */ + @JsonProperty("completion") WorkerEventReference completion, + /** Actual consumed notification, when observed. */ + @JsonProperty("notification") WorkerNotificationReference notification, + /** Exact already-open iteration when an immediate notification was consumed. */ + @JsonProperty("admittedDuring") WorkerEventReference admittedDuring +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java index 40c3f8a8c6..09a6c49c1c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java @@ -32,6 +32,17 @@ public record QueuePendingItems( /** Agent mode stored on this queued entry, as stamped when it was enqueued. Items without an explicit mode report interactive. This is not necessarily the mode that will constrain the turn: a plan or autopilot session applies its own write gate, continuation loop and permission posture to every drained item regardless of the mode stored here. */ @JsonProperty("agentMode") SendAgentMode agentMode, /** Optional source tag associated with this pending queue entry. This is an open string, not authenticated authorship. In particular, `user` does not prove that a person typed the message. If the source is absent or unrecognized, consumers must not infer human or agent authorship and should handle the entry neutrally. Consumers should tolerate future source values. */ - @JsonProperty("source") String source + @JsonProperty("source") String source, + /** Caller-owned diagnostic UUID from the exact accepted native session.send or sendMessages item, when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted for unsupported or identity-less rows, including snapshot-only mirrors. Not an idempotency key, authorization, or permission to retry; repeated values remain ambiguous. */ + @JsonProperty("clientCorrelationId") String clientCorrelationId ) { + /** Creates a value without optional admission correlation metadata. */ + public QueuePendingItems(String id, String messageId, QueuePendingItemsKind kind, String displayText, SendAgentMode agentMode, String source) { + this(id, messageId, kind, displayText, agentMode, source, null); + } + + /** Creates a value without optional admission correlation metadata. */ + public QueuePendingItems(String id, String messageId, QueuePendingItemsKind kind, String displayText, SendAgentMode agentMode) { + this(id, messageId, kind, displayText, agentMode, null, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java index f7d55e6c71..6a5f989334 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigSource.java @@ -11,7 +11,7 @@ import javax.annotation.processing.Generated; /** - * Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. + * Origin of the sandbox choice supplied by the host. Settings-derived origins let managed policy floor the host preference; do not tag explicit session overrides as settings-derived. * * @apiNote This type is experimental and may change in a future version. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.java index 4a6696c01d..e4182434a7 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SendMessageItem.java @@ -33,6 +33,12 @@ public record SendMessageItem( /** If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange */ @JsonProperty("requiredTool") String requiredTool, /** Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-` for command-originated messages, `schedule-` for scheduled prompts, or `agent-` for prompts sent by another agent. */ - @JsonProperty("source") String source + @JsonProperty("source") String source, + /** Optional caller-generated diagnostic UUID for this item only, with the same validation and opt-in native echo as session.send.clientCorrelationId. The batch has no request-level correlation value; each item retains its own value, including preceding context messages. Reused values do not deduplicate messages and remain ambiguous. */ + @JsonProperty("clientCorrelationId") String clientCorrelationId ) { + /** Creates a value without optional admission correlation metadata. */ + public SendMessageItem(String prompt, String displayPrompt, List attachments, Boolean billable, String requiredTool, String source) { + this(prompt, displayPrompt, attachments, billable, requiredTool, source, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendParams.java index 964c48b438..d70c5ae06d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionSendParams.java @@ -55,8 +55,14 @@ public record SessionSendParams( /** W3C Trace Context tracestate header for distributed tracing */ @JsonProperty("tracestate") String tracestate, /** If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. */ - @JsonProperty("wait") Boolean wait_ + @JsonProperty("wait") Boolean wait_, + /** Optional caller-generated diagnostic UUID for this single message. Native sessions with RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character UUID on user.message and its existing pending message row. Missing, invalid, disabled, or unsupported metadata is ignored without rejecting the send. Does not change messageId, deduplicate submissions, authorize work, or make an uncertain retry safe. */ + @JsonProperty("clientCorrelationId") String clientCorrelationId ) { + /** Creates a value without optional admission correlation metadata. */ + public SessionSendParams(String sessionId, String prompt, String displayPrompt, List attachments, SendMode mode, Boolean prepend, Boolean billable, String requiredTool, String source, SendAgentMode agentMode, Map requestHeaders, SessionSendParamsResponseFormat responseFormat, String traceparent, String tracestate, Boolean wait_) { + this(sessionId, prompt, displayPrompt, attachments, mode, prepend, billable, requiredTool, source, agentMode, requestHeaders, responseFormat, traceparent, tracestate, wait_, null); + } @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksSendMessageResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksSendMessageResult.java index 4bdf603fb2..8a10214965 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksSendMessageResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksSendMessageResult.java @@ -27,6 +27,13 @@ public record SessionTasksSendMessageResult( /** Whether the message was successfully delivered or steered */ @JsonProperty("sent") Boolean sent, /** Error message if delivery failed */ - @JsonProperty("error") String error + @JsonProperty("error") String error, + /** Optional exact queue admission receipt on sent=true only. No implicit event or execution-success claim. */ + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = com.github.copilot.WorkerCausalityDeserializer.class) + @JsonProperty("workerCausality") WorkerCausality workerCausality ) { + /** Creates a value without optional worker diagnostics. */ + public SessionTasksSendMessageResult(Boolean sent, String error) { + this(sent, error, null); + } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerAdmission.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerAdmission.java new file mode 100644 index 0000000000..92edb593dd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerAdmission.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * An observed worker admission, not a claim that execution succeeded. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerAdmission( + /** The producer's admission kind. */ + @JsonProperty("kind") WorkerAdmissionKind kind, + /** Canonical logical message identity, independent of queueItemId; at most 256 UTF-8 bytes. */ + @JsonProperty("messageId") String messageId, + /** May be omitted only for the matching current worker user.message. */ + @JsonProperty("event") WorkerEventReference event, + /** Actual AHP participant Turn UUID, not a native turn counter or provenance signal. */ + @JsonProperty("ahpTurnId") String ahpTurnId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerAdmissionKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerAdmissionKind.java new file mode 100644 index 0000000000..7259f1e3a8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerAdmissionKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Why this exact worker admission was made. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerAdmissionKind { + /** The {@code queued_input} variant. */ + QUEUED_INPUT("queued_input"), + /** The {@code system_continuation} variant. */ + SYSTEM_CONTINUATION("system_continuation"); + + private final String value; + WorkerAdmissionKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerAdmissionKind fromValue(String value) { + for (WorkerAdmissionKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerAdmissionKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerBridgeObservation.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerBridgeObservation.java new file mode 100644 index 0000000000..713d449430 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerBridgeObservation.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * One indivisible source-to-reported bridge observation, not a root alias. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerBridgeObservation( + /** Full event received by this bridge. */ + @JsonProperty("source") WorkerEventReference source, + /** Full occurrence actually emitted by that bridge. */ + @JsonProperty("reported") WorkerEventReference reported +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerCausality.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerCausality.java new file mode 100644 index 0000000000..6ee72fd991 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerCausality.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Optional v1 worker diagnostics. The compact UTF-8 {"workerCausality":value} +must fit 4096 bytes after materializing an allowed implicit self-reference. +Ignore invalid/unknown/oversize metadata, not the product event. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerCausality( + /** Supported version, exactly 1. */ + @JsonProperty("version") Long version, + /** Provenance of this enclosing observation; never inferred from other fields. */ + @JsonProperty("observationProvenance") WorkerObservationProvenance observationProvenance, + /** Sources in capture order, at most 32. Absent/unknown is not known-empty. */ + @JsonProperty("sources") List sources, + /** Complete placement-aware observed capture, not global causality or execution success. +Empty true requires explicit native invocation attestation. */ + @JsonProperty("captureComplete") Boolean captureComplete +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerEventReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerEventReference.java new file mode 100644 index 0000000000..a86be736ed --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerEventReference.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Exact observed event identity. No private registration generation or execution handle. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerEventReference( + /** Actual runtime session scope, at most 256 UTF-8 bytes. */ + @JsonProperty("sessionId") String sessionId, + /** Actual event occurrence UUID; copied without normalization. */ + @JsonProperty("eventId") String eventId, + /** Actual event agent scope, absent for a root occurrence; at most 256 UTF-8 bytes. */ + @JsonProperty("agentId") String agentId, + /** Type of the observed occurrence. */ + @JsonProperty("eventType") WorkerEventType eventType, + /** Explicit provenance of this reference. */ + @JsonProperty("provenance") WorkerObservationProvenance provenance +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerEventType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerEventType.java new file mode 100644 index 0000000000..118938f2ad --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerEventType.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Supported observed occurrences. Chronological parentId is not a causal reference. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerEventType { + /** The {@code tool.execution_start} variant. */ + TOOL_EXECUTION_START("tool.execution_start"), + /** The {@code user.message} variant. */ + USER_MESSAGE("user.message"), + /** The {@code subagent.completed} variant. */ + SUBAGENT_COMPLETED("subagent.completed"), + /** The {@code system.notification} variant. */ + SYSTEM_NOTIFICATION("system.notification"), + /** The {@code assistant.turn_start} variant. */ + ASSISTANT_TURN_START("assistant.turn_start"); + + private final String value; + WorkerEventType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerEventType fromValue(String value) { + for (WorkerEventType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerEventType value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerInput.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerInput.java new file mode 100644 index 0000000000..1c4cc2b120 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerInput.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Exact accepted worker input, distinct from a message, event, caller correlation or Turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerInput( + /** UUID allocated for this queue item by the admitting producer. */ + @JsonProperty("queueItemId") String queueItemId, + /** Actual recipient task, at most 256 UTF-8 bytes. */ + @JsonProperty("agentId") String agentId, + /** Original invoking occurrence, never replaced by a reported/root alias. */ + @JsonProperty("sender") WorkerEventReference sender, + /** Exact captured edges in producer order. Requires sender; at most 32 whole pairs. */ + @JsonProperty("senderBridges") List senderBridges +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerNotificationMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerNotificationMode.java new file mode 100644 index 0000000000..859d5e864d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerNotificationMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * How the owned notification was consumed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerNotificationMode { + /** The {@code queued} variant. */ + QUEUED("queued"), + /** The {@code immediate} variant. */ + IMMEDIATE("immediate"); + + private final String value; + WorkerNotificationMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerNotificationMode fromValue(String value) { + for (WorkerNotificationMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerNotificationMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerNotificationReference.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerNotificationReference.java new file mode 100644 index 0000000000..32b4865288 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerNotificationReference.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Exact delivery and optional occurrence of a consumed worker notification. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerNotificationReference( + /** Actual notificationDeliveryId UUID. */ + @JsonProperty("deliveryId") String deliveryId, + /** May be omitted only on the matching current system.notification. */ + @JsonProperty("event") WorkerEventReference event, + /** Actual consumption mode. */ + @JsonProperty("mode") WorkerNotificationMode mode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerObservationProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerObservationProvenance.java new file mode 100644 index 0000000000..07cc0b671b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerObservationProvenance.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Producer of an observation, not the execution location of every referenced source. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum WorkerObservationProvenance { + /** The {@code native} variant. */ + NATIVE("native"), + /** The {@code ahp_coordinator} variant. */ + AHP_COORDINATOR("ahp_coordinator"); + + private final String value; + WorkerObservationProvenance(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static WorkerObservationProvenance fromValue(String value) { + for (WorkerObservationProvenance v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown WorkerObservationProvenance value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerSource.java new file mode 100644 index 0000000000..af24785ff4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/WorkerSource.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * One captured source at this placement and observation boundary. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record WorkerSource( + /** Indivisible accepted input identity. */ + @JsonProperty("input") WorkerInput input, + /** Actual admissions in capture order; at most 32. */ + @JsonProperty("admissions") List admissions, + /** Applicable observations were all captured here, never work completion or success. */ + @JsonProperty("captureComplete") Boolean captureComplete, + /** Actual completion emission only. Absence is not an execution outcome. */ + @JsonProperty("completion") WorkerEventReference completion, + /** Actual consumed notification, when observed. */ + @JsonProperty("notification") WorkerNotificationReference notification, + /** Exact already-open iteration when an immediate notification was consumed. */ + @JsonProperty("admittedDuring") WorkerEventReference admittedDuring +) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index eac5d9763c..8f0bf017c6 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -561,6 +561,7 @@ public CompletableFuture send(MessageOptions options) { var request = new SendMessageRequest(); request.setSessionId(sessionId); request.setPrompt(options.getPrompt()); + request.setClientCorrelationId(options.getClientCorrelationId()); request.setAttachments(options.getAttachments()); request.setMode(options.getMode()); request.setSource(options.getSource()); diff --git a/java/sdk/src/main/java/com/github/copilot/WorkerCausalityDeserializer.java b/java/sdk/src/main/java/com/github/copilot/WorkerCausalityDeserializer.java new file mode 100644 index 0000000000..6ddc0711cb --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/WorkerCausalityDeserializer.java @@ -0,0 +1,197 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +/** + * Tolerant decoder for optional worker diagnostics, not a causal association + * validator. + */ +public final class WorkerCausalityDeserializer extends JsonDeserializer implements ContextualDeserializer { + private static final Logger LOG = Logger.getLogger(WorkerCausalityDeserializer.class.getName()); + private static final Pattern UUID = Pattern.compile("[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}"); + private final JavaType target; + + /** Creates the annotation's contextual decoder. */ + public WorkerCausalityDeserializer() { + this(null); + } + + private WorkerCausalityDeserializer(JavaType target) { + this.target = target; + } + + @Override + public JsonDeserializer createContextual(DeserializationContext context, BeanProperty property) { + return new WorkerCausalityDeserializer(property.getType()); + } + + @Override + public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException { + JsonNode value = parser.readValueAsTree(); + if (small(value, new int[]{4096}, 0) && supported(value)) { + String field = "{\"workerCausality\":" + value + "}"; + if (field.getBytes(StandardCharsets.UTF_8).length <= 4096) { + try { + return context.readTreeAsValue(value, target); + } catch (IOException | IllegalArgumentException ignored) { + // Optional diagnostics must not reject the product event. + } + } + } + LOG.warning("Ignoring invalid, unsupported or oversized workerCausality metadata"); + return null; + } + + private static boolean supported(JsonNode value) { + if (!fields(value, "version", "observationProvenance", "sources", "captureComplete") + || !value.path("version").isIntegralNumber() || !value.path("version").canConvertToInt() + || value.path("version").asInt() != 1 + || !(value.path("observationProvenance").asText().equals("native") + || value.path("observationProvenance").asText().equals("ahp_coordinator")) + || !value.path("captureComplete").isBoolean() || !value.path("sources").isArray() + || value.path("sources").size() > 32) { + return false; + } + for (JsonNode source : value.path("sources")) { + JsonNode input = source.path("input"); + if (!fields(source, "input", "admissions", "captureComplete", "completion", "notification", + "admittedDuring") || !fields(input, "queueItemId", "agentId", "sender", "senderBridges") + || !source.path("captureComplete").isBoolean() || !source.path("admissions").isArray() + || source.path("admissions").size() > 32 || !input.isObject() + || (value.path("captureComplete").asBoolean() && !source.path("captureComplete").asBoolean()) + || !uuid(input.path("queueItemId")) || !text(input.path("agentId")) + || !optionalReference(input, "sender", "tool.execution_start") + || !optionalReference(source, "completion", "subagent.completed") + || !optionalReference(source, "admittedDuring", "assistant.turn_start")) { + return false; + } + if (input.has("senderBridges")) { + JsonNode edges = input.path("senderBridges"); + if (!input.has("sender") || !edges.isArray() || edges.size() > 32) { + return false; + } + for (JsonNode edge : edges) { + if (!fields(edge, "source", "reported") || !reference(edge.path("source"), "tool.execution_start") + || !reference(edge.path("reported"), "tool.execution_start")) { + return false; + } + } + } + for (JsonNode admission : source.path("admissions")) { + String kind = admission.path("kind").asText(); + if (!fields(admission, "kind", "messageId", "event", "ahpTurnId") + || !(kind.equals("queued_input") || kind.equals("system_continuation")) + || !text(admission.path("messageId")) + || (admission.has("ahpTurnId") && !uuid(admission.path("ahpTurnId"))) + || (admission.has("event") && (!reference(admission.path("event"), "user.message") + || !admission.path("event").path("agentId").equals(input.path("agentId"))))) { + return false; + } + } + if (source.has("notification")) { + JsonNode notification = source.path("notification"); + String mode = notification.path("mode").asText(); + if (!fields(notification, "deliveryId", "event", "mode") || !uuid(notification.path("deliveryId")) + || !(mode.equals("queued") || mode.equals("immediate")) + || !optionalReference(notification, "event", "system.notification")) { + return false; + } + } + } + return true; + } + + private static boolean fields(JsonNode value, String... allowed) { + if (!value.isObject()) { + return false; + } + var names = value.fieldNames(); + while (names.hasNext()) { + String name = names.next(); + boolean found = false; + for (String candidate : allowed) { + if (candidate.equals(name)) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static boolean text(JsonNode value) { + if (!value.isTextual() || value.textValue().isEmpty()) { + return false; + } + String text = value.textValue(); + for (int index = 0; index < text.length(); index++) { + char character = text.charAt(index); + if (Character.isHighSurrogate(character)) { + if (index + 1 >= text.length() || !Character.isLowSurrogate(text.charAt(index + 1))) { + return false; + } + index++; + } else if (Character.isLowSurrogate(character) || Character.isISOControl(character)) { + return false; + } + } + return text.getBytes(StandardCharsets.UTF_8).length <= 256; + } + + private static boolean uuid(JsonNode value) { + return value.isTextual() && UUID.matcher(value.textValue()).matches(); + } + + private static boolean optionalReference(JsonNode value, String field, String eventType) { + return !value.has(field) || reference(value.path(field), eventType); + } + + private static boolean reference(JsonNode value, String eventType) { + return fields(value, "sessionId", "eventId", "agentId", "eventType", "provenance") + && text(value.path("sessionId")) && uuid(value.path("eventId")) + && (!value.has("agentId") || text(value.path("agentId"))) + && value.path("eventType").asText().equals(eventType) + && (value.path("provenance").asText().equals("native") + || value.path("provenance").asText().equals("ahp_coordinator")); + } + + private static boolean small(JsonNode value, int[] remaining, int depth) { + if (--remaining[0] < 0 || depth > 32) { + return false; + } + if (value.isTextual()) { + remaining[0] -= value.textValue().length(); + } else if (value.isObject()) { + for (var field : value.properties()) { + remaining[0] -= field.getKey().length(); + if (!small(field.getValue(), remaining, depth + 1)) { + return false; + } + } + } else if (value.isArray()) { + for (JsonNode item : value) { + if (!small(item, remaining, depth + 1)) { + return false; + } + } + } + return remaining[0] >= 0; + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/MessageOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/MessageOptions.java index b06264a0c4..b4cd1328eb 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/MessageOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/MessageOptions.java @@ -43,6 +43,7 @@ public class MessageOptions { private String prompt; + private String clientCorrelationId; private List attachments; private String mode; private MessageSource source; @@ -96,6 +97,29 @@ public MessageOptions setPrompt(String prompt) { return this; } + /** + * Gets the caller-owned diagnostic UUID for this RPC admission. + * + * @return the correlation value, or {@code null} when unset + */ + public String getClientCorrelationId() { + return clientCorrelationId; + } + + /** + * Sets optional caller-owned admission metadata, sent unchanged. Supported + * runtimes echo accepted UUIDs only with RUNTIME_ADMISSION_TRACE_CONTEXT + * enabled. This is not an idempotency key; the SDK never generates a value. + * + * @param clientCorrelationId + * the diagnostic UUID, or {@code null} to omit it + * @return this options instance for method chaining + */ + public MessageOptions setClientCorrelationId(String clientCorrelationId) { + this.clientCorrelationId = clientCorrelationId; + return this; + } + /** * Gets the attachments. * @@ -265,6 +289,7 @@ public MessageOptions setDisplayPrompt(String displayPrompt) { public MessageOptions clone() { MessageOptions copy = new MessageOptions(); copy.prompt = this.prompt; + copy.clientCorrelationId = this.clientCorrelationId; copy.attachments = this.attachments != null ? new ArrayList<>(this.attachments) : null; copy.mode = this.mode; copy.source = this.source; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SendMessageRequest.java b/java/sdk/src/main/java/com/github/copilot/rpc/SendMessageRequest.java index fb27324831..613f5a9324 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/SendMessageRequest.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SendMessageRequest.java @@ -31,6 +31,9 @@ public final class SendMessageRequest { @JsonProperty("prompt") private String prompt; + @JsonProperty("clientCorrelationId") + private String clientCorrelationId; + @JsonProperty("attachments") private List attachments; @@ -82,6 +85,19 @@ public void setPrompt(String prompt) { this.prompt = prompt; } + /** Gets the caller-owned admission UUID. @return the value, or {@code null} */ + public String getClientCorrelationId() { + return clientCorrelationId; + } + + /** + * Sets admission metadata. @param clientCorrelationId the value, or + * {@code null} + */ + public void setClientCorrelationId(String clientCorrelationId) { + this.clientCorrelationId = clientCorrelationId; + } + /** Gets the attachments. @return the list of attachments */ public List getAttachments() { return attachments == null ? null : Collections.unmodifiableList(attachments); diff --git a/java/sdk/src/test/java/com/github/copilot/MessageSourceTest.java b/java/sdk/src/test/java/com/github/copilot/MessageSourceTest.java index 6ef9d4e2b0..ce4efc1a74 100644 --- a/java/sdk/src/test/java/com/github/copilot/MessageSourceTest.java +++ b/java/sdk/src/test/java/com/github/copilot/MessageSourceTest.java @@ -31,6 +31,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.generated.rpc.SessionSendParams; +import com.github.copilot.generated.rpc.SessionSendMessagesParams; import com.github.copilot.rpc.AgentMode; import com.github.copilot.rpc.Attachment; import com.github.copilot.rpc.CopilotClientOptions; @@ -180,6 +181,62 @@ void sendWithoutSourcePreservesLegacyPayload() throws Exception { } } + @ParameterizedTest + @NullSource + @ValueSource(strings = {"01234567-89ab-4cde-8f01-23456789abcd", "01234567-89AB-4CDE-8F01-23456789ABCD", + "not-a-uuid", ""}) + void admissionCorrelationIsOptionalAndDoesNotRewriteOptions(String correlation) throws Exception { + try (var server = new SendServer(Outcome.IDLE); + var client = server.createClient(); + var session = client.createSession(sessionConfig()).get(5, TimeUnit.SECONDS)) { + var options = fullOptions().setSource(MessageSource.SYSTEM).setClientCorrelationId(correlation); + assertEquals(correlation, options.clone().getClientCorrelationId()); + assertEquals("message-1", session.send(options).get(5, TimeUnit.SECONDS)); + var wire = server.takeSendParams(); + assertEquals("system", wire.get("source").asText()); + assertEquals("display", wire.get("displayPrompt").asText()); + assertEquals("trace-id", wire.get("requestHeaders").get("X-Trace").asText()); + assertEquals(MAPPER.valueToTree(options.getAttachments()), wire.get("attachments")); + assertEquals(correlation, wire.path("clientCorrelationId").asText(null)); + assertEquals(correlation != null, wire.has("clientCorrelationId")); + + assertNull(session.sendAndWait(options).get(5, TimeUnit.SECONDS)); + assertEquals(correlation, server.takeSendParams().path("clientCorrelationId").asText(null)); + + var params = MAPPER.createObjectNode().put("prompt", "hello"); + if (correlation != null) { + params.put("clientCorrelationId", correlation); + } + var generated = MAPPER.treeToValue(params, SessionSendParams.class); + assertEquals("message-1", session.getRpc().send(generated).get(5, TimeUnit.SECONDS).messageId()); + params.put("sessionId", "source-session"); + assertEquals(params, server.takeSendParams()); + options.setClientCorrelationId(null); + assertFalse(MAPPER.valueToTree(options).has("clientCorrelationId")); + } + } + + @Test + void admissionCorrelationRemainsPerBatchItem() throws Exception { + try (var server = new SendServer(Outcome.IDLE); + var client = server.createClient(); + var session = client.createSession(sessionConfig()).get(5, TimeUnit.SECONDS)) { + var json = MAPPER.readTree(""" + {"messages":[ + {"prompt":"context","clientCorrelationId":"01234567-89ab-4cde-8f01-23456789abcd"}, + {"prompt":"plain"}, + {"prompt":"reused","clientCorrelationId":"01234567-89ab-4cde-8f01-23456789abcd"} + ]} + """); + var params = MAPPER.treeToValue(json, SessionSendMessagesParams.class); + var result = session.getRpc().sendMessages(params).get(5, TimeUnit.SECONDS); + assertEquals(List.of("batch-0", "batch-1", "batch-2"), result.messageIds()); + var wire = server.takeSendParams(); + assertFalse(wire.has("clientCorrelationId")); + assertEquals(json.get("messages"), wire.get("messages")); + } + } + @ParameterizedTest @MethodSource("sourcesAndModes") void sendForwardsSourceWithoutChangingOtherOptions(MessageSource source, String mode) throws Exception { @@ -313,10 +370,12 @@ private void serve() { case "connect" -> Map.of("ok", true, "protocolVersion", 3, "version", "test"); case "session.create" -> Map.of("sessionId", params.path("sessionId").asText()); case "session.send" -> Map.of("messageId", "message-1"); + case "session.sendMessages" -> Map.of("messageIds", java.util.stream.IntStream + .range(0, params.path("messages").size()).mapToObj(index -> "batch-" + index).toList()); case "session.detach" -> Map.of("success", true); default -> Map.of(); }; - boolean send = "session.send".equals(method); + boolean send = "session.send".equals(method) || "session.sendMessages".equals(method); if (send) { sends.add(params); } diff --git a/java/sdk/src/test/java/com/github/copilot/WorkerCausalityTest.java b/java/sdk/src/test/java/com/github/copilot/WorkerCausalityTest.java new file mode 100644 index 0000000000..2b5a5097e9 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/WorkerCausalityTest.java @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.SystemNotificationEvent; +import com.github.copilot.generated.SystemNotificationWorkflowCompleted; +import com.github.copilot.generated.SystemNotificationWorkflowCompletedStatus; +import com.github.copilot.generated.rpc.SessionTasksSendMessageResult; + +class WorkerCausalityTest { + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private static JsonNode corpus() throws Exception { + return MAPPER.readTree(Path.of(System.getProperty("copilot.tests.dir"), "worker-causality.json").toFile()); + } + + private static String decode(JsonNode value, boolean event) throws Exception { + return event + ? MAPPER.writeValueAsString(MAPPER.treeToValue(value, SessionEvent.class)) + : MAPPER.writeValueAsString(MAPPER.treeToValue(value, SessionTasksSendMessageResult.class)); + } + + private static ObjectNode payload(JsonNode value, boolean event) { + return (ObjectNode) (event ? value.get("data") : value); + } + + @Test + void publicReadersPreserveSourceAndHistoricalProductBytes() throws Exception { + JsonNode corpus = corpus(); + for (JsonNode test : corpus.get("valid")) { + boolean event = test.has("event"); + JsonNode wire = test.get(event ? "event" : "result").deepCopy(); + JsonNode observed = MAPPER.readTree(decode(wire, event)); + assertEquals(payload(wire, event).get("workerCausality"), payload(observed, event).get("workerCausality"), + test.get("name").asText()); + + payload(wire, event).remove("workerCausality"); + String baseline = decode(wire, event); + for (JsonNode invalid : corpus.get("invalid")) { + payload(wire, event).set("workerCausality", invalid.get("value")); + assertEquals(baseline, decode(wire, event), test.get("name") + ": " + invalid.get("name")); + } + } + } + + @Test + void exactUtf8BudgetAndUnknownFields() throws Exception { + JsonNode corpus = corpus(); + for (JsonNode test : corpus.get("boundaries")) { + JsonNode wire = corpus.get("valid").get(0).get("event").deepCopy(); + payload(wire, true).set("workerCausality", test.get("value")); + ObjectNode observed = payload(MAPPER.readTree(decode(wire, true)), true); + assertEquals(test.get("accepted").asBoolean(), observed.has("workerCausality"), test.get("name").asText()); + assertEquals(payload(wire, true).get("content"), observed.get("content")); + } + } + + @Test + void canonicalWorkflowCompletionDecodesTypedFields() throws Exception { + for (String fixtureName : new String[]{"workflowCompleted", "legacyWorkflowCompleted"}) { + SessionEvent parsed = MAPPER.treeToValue(corpus().path(fixtureName).path("event"), SessionEvent.class); + SystemNotificationEvent notification = assertInstanceOf(SystemNotificationEvent.class, parsed); + SystemNotificationWorkflowCompleted completion = assertInstanceOf(SystemNotificationWorkflowCompleted.class, + notification.getData().kind()); + assertEquals(completion, notification.getData().typedKind()); + assertEquals("fix-ci", completion.getWorkflowName()); + assertEquals("run-1", completion.getRunId()); + assertEquals(SystemNotificationWorkflowCompletedStatus.COMPLETED, completion.getStatus()); + assertEquals(1L, completion.getConsumedSubagents()); + } + } + + @Test + void invalidUnicodeScalarIdentitiesAreUnavailable() throws Exception { + for (JsonNode test : corpus().path("invalidRaw")) { + String wire = "{\"sent\":true,\"workerCausality\":" + test.path("json").asText() + "}"; + SessionTasksSendMessageResult result = MAPPER.readValue(wire, SessionTasksSendMessageResult.class); + assertEquals(true, result.sent(), test.path("name").asText()); + assertEquals(null, result.workerCausality(), test.path("name").asText()); + } + } + + @Test + void uppercaseWorkerUuidIdentitiesRoundTripExactly() throws Exception { + JsonNode source = MAPPER.readTree(""" + { + "sent": true, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [{ + "input": { + "queueItemId": "ABCDEFAB-CDEF-4ABC-8ABC-ABCDEFABCDEF", + "agentId": "worker", + "sender": { + "sessionId": "session-uppercase", + "eventId": "FEDCBAFE-DCBA-4FED-8FED-FEDCBAFEDCBA", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + "admissions": [{ + "kind": "queued_input", + "messageId": "message-uppercase", + "ahpTurnId": "AAAABBBB-CCCC-4DDD-8EEE-FFFFAAAABBBB" + }], + "captureComplete": true, + "notification": { + "deliveryId": "BBBBCCCC-DDDD-4EEE-8FFF-AAAABBBBCCCC", + "mode": "queued" + } + }], + "captureComplete": true + } + } + """); + JsonNode observed = MAPPER.readTree(decode(source, false)); + assertEquals(source.get("workerCausality"), observed.get("workerCausality")); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java index 3ef4770562..7570c3cab9 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java @@ -12,13 +12,64 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.AllowCopilotExperimental; import com.github.copilot.generated.UserMessageEvent.UserMessageEventData; import com.github.copilot.generated.rpc.QueuePendingItems; +import com.github.copilot.generated.rpc.QueuePendingItemsKind; +import com.github.copilot.generated.rpc.SendAgentMode; +import com.github.copilot.generated.rpc.SendMessageItem; +import com.github.copilot.generated.rpc.SessionSendParams; +@AllowCopilotExperimental class MessageIdentitySerializationTest { private static final ObjectMapper MAPPER = new ObjectMapper(); + @Test + void testPreviousRecordConstructorsStillOmitCorrelation() { + var item = new SendMessageItem("hello", null, null, null, null, null); + var row = new QueuePendingItems("queue-1", "canonical-1", QueuePendingItemsKind.MESSAGE, "hello", + SendAgentMode.INTERACTIVE); + var send = new SessionSendParams(null, "hello", null, null, null, null, null, null, null, null, null, null, + null, null, null); + assertNull(item.clientCorrelationId()); + assertNull(row.clientCorrelationId()); + assertNull(send.clientCorrelationId()); + assertEquals("canonical-1", row.messageId()); + } + + @Test + void testAdmissionCorrelationOptionalRoundTrips() throws Exception { + for (String value : new String[]{null, "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", "not-a-uuid", ""}) { + for (var type : new Class[]{SessionSendParams.class, SendMessageItem.class, QueuePendingItems.class, + UserMessageEventData.class}) { + var wire = MAPPER.createObjectNode(); + if (type == QueuePendingItems.class) { + wire.put("id", "queue-1").put("messageId", "canonical-1").put("kind", "message") + .put("displayText", "hello").put("agentMode", "interactive"); + } else if (type == UserMessageEventData.class) { + wire.put("content", "hello").put("messageId", "canonical-1").put("interactionId", "agent-loop-1"); + } else { + wire.put("prompt", "hello"); + } + if (value != null) { + wire.put("clientCorrelationId", value); + } + var decoded = MAPPER.readValue(wire.toString(), type); + assertEquals(wire, MAPPER.valueToTree(decoded)); + wire.put("futureField", true); + decoded = MAPPER.treeToValue(wire, type); + wire.remove("futureField"); + assertEquals(wire, MAPPER.valueToTree(decoded)); + wire.putNull("clientCorrelationId"); + decoded = MAPPER.treeToValue(wire, type); + assertFalse(MAPPER.valueToTree(decoded).has("clientCorrelationId")); + } + } + } + @Test void testQueuePendingMessageIdUsesCamelCaseAndIsOptional() throws Exception { var item = MAPPER.readValue(""" @@ -63,4 +114,25 @@ void testUserMessageIdUsesCamelCaseAndIsOptional() throws Exception { assertNull(olderMessage.messageId()); assertFalse(MAPPER.valueToTree(olderMessage).has("messageId")); } + + @Test + void testToolStartTraceContextIsOptionalAndPreserved() throws Exception { + for (var context : new String[]{"{}", """ + {"traceparent":"00-11111111111111111111111111111111-2222222222222222-01","tracestate":"vendor=value"} + """, """ + {"traceparent":"00-11111111111111111111111111111111-2222222222222222-00"} + """, """ + {"traceparent":"invalid","tracestate":"invalid"} + """, """ + {"traceparent":""} + """}) { + var wire = MAPPER.createObjectNode().put("toolCallId", "tool-call-a").put("toolName", "client-tool"); + wire.setAll(MAPPER.readValue(context, ObjectNode.class)); + var data = MAPPER.treeToValue(wire, ToolExecutionStartEvent.ToolExecutionStartEventData.class); + + assertEquals(wire.path("traceparent").asText(null), data.traceparent()); + assertEquals(wire.path("tracestate").asText(null), data.tracestate()); + assertEquals(wire, MAPPER.valueToTree(data)); + } + } } diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index bec1818662..3f8458f010 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -4,8 +4,9 @@ */ import type { MessageConnection } from "vscode-jsonrpc/node.js"; +import { withWorkerCausality, withWorkerCausalityEvents } from "../workerCausality.js"; -import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, IndexedSearchState, ManagedSettingsResolvedData, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionDecisionSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, IndexedSearchState, ManagedSettingsResolvedData, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionDecisionSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, WorkerCausality } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -4311,7 +4312,7 @@ export type ResponseFormat = { type: "json_schema"; }; /** - * Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. + * Origin of the sandbox choice supplied by the host. Settings-derived origins let managed policy floor the host preference; do not tag explicit session overrides as settings-derived. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SandboxConfigSource". @@ -4324,7 +4325,7 @@ export type SandboxConfigSource = | "user_enabled" /** The user's persisted settings disabled the sandbox. */ | "user_disabled" - /** An explicit session-scoped choice selected the sandbox state, such as a command-line flag. */ + /** A command-line flag selected the sandbox state for this session. */ | "session_flag" /** The user disabled the sandbox for the current session. */ | "session_disabled" @@ -19489,6 +19490,10 @@ export interface QueuePendingItems { * Optional source tag associated with this pending queue entry. This is an open string, not authenticated authorship. In particular, `user` does not prove that a person typed the message. If the source is absent or unrecognized, consumers must not infer human or agent authorship and should handle the entry neutrally. Consumers should tolerate future source values. */ source?: string; + /** + * Caller-owned diagnostic UUID from the exact accepted native session.send or sendMessages item, when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted for unsupported or identity-less rows, including snapshot-only mirrors. Not an idempotency key, authorization, or permission to retry; repeated values remain ambiguous. + */ + clientCorrelationId?: string; } /** * Snapshot of the session's pending queued items and immediate-steering messages. @@ -20647,6 +20652,10 @@ export interface SendMessageItem { * @internal */ source?: string; + /** + * Optional caller-generated diagnostic UUID for this item only, with the same validation and opt-in native echo as session.send.clientCorrelationId. The batch has no request-level correlation value; each item retains its own value, including preceding context messages. Reused values do not deduplicate messages and remain ambiguous. + */ + clientCorrelationId?: string; } /** * Parameters for sending zero or more user messages to the session in a single turn. Remote-backed (Mission Control) sessions do not support this method and will return an error. @@ -20758,6 +20767,10 @@ export interface SendRequest { * If true, await completion of the agentic loop for this message before returning. Defaults to false (fire-and-forget). When true, the result still contains the same `messageId`; the caller can rely on the agent having processed the message before the call resolves. Transport-dependent tail semantics: on a LOCAL (in-process) session the wait additionally blocks until the completed turn's event tail has been dispatched to this session's in-process subscribers, so a subsequent read of subscriber state already reflects the turn; on a REMOTE session the wait resolves once the loop completes and mirrored delivery follows over the wire. Callers that need the stronger local guarantee on remote sessions should await the event stream explicitly. */ wait?: boolean; + /** + * Optional caller-generated diagnostic UUID for this single message. Native sessions with RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character UUID on user.message and its existing pending message row. Missing, invalid, disabled, or unsupported metadata is ignored without rejecting the send. Does not change messageId, deduplicate submissions, authorize work, or make an uncertain retry safe. + */ + clientCorrelationId?: string; } /** * Result of sending a user message @@ -25200,6 +25213,7 @@ export interface TasksSendMessageResult { * Error message if delivery failed */ error?: string; + workerCausality?: WorkerCausality; } /** * Agent type, prompt, name, and optional description and model override for the new task. @@ -29187,7 +29201,7 @@ export function createServerRpc(connection: MessageConnection) { * @returns Batch of session events returned by a read, with cursor and continuation metadata. */ readPersistedEvents: async (params: SessionsReadPersistedEventsRequest): Promise => - connection.sendRequest("sessions.readPersistedEvents", params), + connection.sendRequest("sessions.readPersistedEvents", params).then(withWorkerCausalityEvents), /** * Finds the local session bound to a GitHub task ID, if any. * @@ -30325,7 +30339,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Indicates whether the message was delivered, with an error message when delivery failed. */ sendMessage: async (params: TasksSendMessageRequest): Promise => - connection.sendRequest("session.tasks.sendMessage", { sessionId, ...params }), + connection.sendRequest("session.tasks.sendMessage", { sessionId, ...params }).then(withWorkerCausality), }, /** @experimental */ skills: { @@ -31714,7 +31728,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * @returns Batch of session events returned by a read, with cursor and continuation metadata. */ read: async (params: EventLogReadRequest): Promise => - connection.sendRequest("session.eventLog.read", { sessionId, ...params }), + connection.sendRequest("session.eventLog.read", { sessionId, ...params }).then(withWorkerCausalityEvents), /** * Returns a snapshot of the current tail cursor without consuming events. * diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 15596e1d37..8198fa7fc4 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -771,6 +771,44 @@ export type UserMessageDelivery = | "steering" /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; +/** + * Producer of an observation, not the execution location of every referenced source. + */ +export type WorkerObservationProvenance = + /** Observed by the native runtime. */ + | "native" + /** Observed by the AHP coordinator. */ + | "ahp_coordinator"; +/** + * Supported observed occurrences. Chronological parentId is not a causal reference. + */ +export type WorkerEventType = + /** Observed tool execution start occurrence. */ + | "tool.execution_start" + /** Observed admitted user message occurrence. */ + | "user.message" + /** Observed worker completion occurrence. */ + | "subagent.completed" + /** Observed worker notification occurrence. */ + | "system.notification" + /** Observed assistant turn start occurrence. */ + | "assistant.turn_start"; +/** + * Why this exact worker admission was made. + */ +export type WorkerAdmissionKind = + /** A queued worker input was admitted. */ + | "queued_input" + /** A system continuation was admitted. */ + | "system_continuation"; +/** + * How the owned notification was consumed. + */ +export type WorkerNotificationMode = + /** The notification was consumed through the queued input path. */ + | "queued" + /** The notification was consumed during an already-open iteration. */ + | "immediate"; /** * Content-safe activity observed while a HydraFusion phase is running. */ @@ -4353,6 +4391,10 @@ export interface UserMessageData { * Files, selections, or GitHub references attached to the message */ attachments?: Attachment[]; + /** + * Exact caller-owned diagnostic UUID carried by this accepted native session.send or sendMessages item when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted when input, native ownership, or support is missing. Independent of the canonical messageId; not an idempotency key, authorization, or permission to retry. Multiple messages with the same value remain ambiguous. + */ + clientCorrelationId?: string; /** * The user's message text as displayed in the timeline */ @@ -4375,7 +4417,7 @@ export interface UserMessageData { */ nativeDocumentPathFallbackPaths?: string[]; /** - * Parent agent task ID for background telemetry correlated to this user turn + * Task ID minted when the runtime prepares this user-message run. This is not a parent interaction ID or worker instance ID and must not be equated with CAPI's X-Parent-Agent-Id. */ parentAgentTaskId?: string; responsesReasoning?: ResponsesReasoning; @@ -4395,6 +4437,7 @@ export interface UserMessageData { * The agent-loop turn ID that consumed this message; absent when no agent-loop turn consumed it */ turnId?: string; + workerCausality?: WorkerCausality; } /** * File attachment @@ -4834,6 +4877,120 @@ export interface AttachmentExtensionContext { */ type: "extension_context"; } +/** + * Optional v1 worker diagnostics. The compact UTF-8 {"workerCausality":value} + * must fit 4096 bytes after materializing an allowed implicit self-reference. + * Ignore invalid/unknown/oversize metadata, not the product event. + */ +export interface WorkerCausality { + /** + * Complete placement-aware observed capture, not global causality or execution success. + * Empty true requires explicit native invocation attestation. + */ + captureComplete: boolean; + observationProvenance: WorkerObservationProvenance; + /** + * Sources in capture order, at most 32. Absent/unknown is not known-empty. + * + * @maxItems 32 + */ + sources: WorkerSource[]; + /** + * Supported version, exactly 1. + */ + version: 1; +} +/** + * One captured source at this placement and observation boundary. + */ +export interface WorkerSource { + /** + * Actual admissions in capture order; at most 32. + * + * @maxItems 32 + */ + admissions: WorkerAdmission[]; + admittedDuring?: WorkerEventReference; + /** + * Applicable observations were all captured here, never work completion or success. + */ + captureComplete: boolean; + completion?: WorkerEventReference; + input: WorkerInput; + notification?: WorkerNotificationReference; +} +/** + * An observed worker admission, not a claim that execution succeeded. + */ +export interface WorkerAdmission { + /** + * Actual AHP participant Turn UUID, not a native turn counter or provenance signal. + */ + ahpTurnId?: string; + event?: WorkerEventReference; + kind: WorkerAdmissionKind; + /** + * Canonical logical message identity, independent of queueItemId; at most 256 UTF-8 bytes. + */ + messageId: string; +} +/** + * Exact observed event identity. No private registration generation or execution handle. + */ +export interface WorkerEventReference { + /** + * Actual event agent scope, absent for a root occurrence; at most 256 UTF-8 bytes. + */ + agentId?: string; + /** + * Actual event occurrence UUID; copied without normalization. + */ + eventId: string; + eventType: WorkerEventType; + provenance: WorkerObservationProvenance; + /** + * Actual runtime session scope, at most 256 UTF-8 bytes. + */ + sessionId: string; +} +/** + * Exact accepted worker input, distinct from a message, event, caller correlation or Turn. + */ +export interface WorkerInput { + /** + * Actual recipient task, at most 256 UTF-8 bytes. + */ + agentId: string; + /** + * UUID allocated for this queue item by the admitting producer. + */ + queueItemId: string; + sender?: WorkerEventReference; + /** + * Exact captured edges in producer order. Requires sender; at most 32 whole pairs. + * + * @maxItems 32 + */ + senderBridges?: WorkerBridgeObservation[]; +} +/** + * One indivisible source-to-reported bridge observation, not a root alias. + */ +export interface WorkerBridgeObservation { + reported: WorkerEventReference; + source: WorkerEventReference; +} +/** + * Exact delivery and optional occurrence of a consumed worker notification. + */ +export interface WorkerNotificationReference { + /** + * Actual notificationDeliveryId UUID. + */ + deliveryId: string; + event?: WorkerEventReference; + mode: WorkerNotificationMode; +} /** * Session event "pending_messages.modified". Empty payload; the event signals that the pending message queue has changed */ @@ -4918,6 +5075,7 @@ export interface AssistantTurnStartData { * Identifier for this turn within the agentic loop, typically a stringified turn number */ turnId: string; + workerCausality?: WorkerCausality; } /** * Session event "assistant.intent". Agent intent description for current activity or plan @@ -6827,6 +6985,14 @@ export interface ToolExecutionStartData { * Human-readable display title for the tool, when the selected tool descriptor has a non-empty title. */ toolTitle?: string; + /** + * W3C traceparent of this tool's active runtime execute_tool span. Available on live events when tool-context propagation is enabled; absent when the span is unavailable or on persisted history. This diagnostic context does not authorize execution. + */ + traceparent?: string; + /** + * Optional W3C tracestate associated with traceparent. Omitted when no valid vendor state is available. + */ + tracestate?: string; /** * Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event */ @@ -7757,7 +7923,7 @@ export interface SubagentStartedData { resumable?: boolean; taskModelSource?: SubagentTaskModelSource; /** - * Tool call ID of the parent tool invocation that spawned this sub-agent + * Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. */ toolCallId: string; /** @@ -7896,7 +8062,7 @@ export interface SubagentCompletedData { modelOverrideReason?: string; modelSelectionSource?: SubagentModelSelectionSource; /** - * Tool call ID of the parent tool invocation that spawned this sub-agent + * Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. */ toolCallId: string; /** @@ -7988,7 +8154,7 @@ export interface SubagentFailedData { modelOverrideReason?: string; modelSelectionSource?: SubagentModelSelectionSource; /** - * Tool call ID of the parent tool invocation that spawned this sub-agent + * Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. */ toolCallId: string; /** @@ -8436,6 +8602,7 @@ export interface SystemNotificationData { content: string; kind: SystemNotification; responsesReasoning?: ResponsesReasoning; + workerCausality?: WorkerCausality; } /** * System notification metadata for a background agent that completed or failed, including agent ID, type, status, description, and prompt. diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 39e023a7bc..475f423ced 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -23,6 +23,7 @@ import type { import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; import { getTraceContext } from "./telemetry.js"; +import { withWorkerCausality, withWorkerCausalityEvents } from "./workerCausality.js"; import { isResponseSchema, toJsonSchema } from "./schema.js"; import { isAttributedPermissionResult } from "./types.js"; import type { @@ -603,11 +604,22 @@ export class CopilotSession { signal.addEventListener("abort", onAbort, { once: true }); } - unsubscribe = this.on("workflow.run_updated", (event) => { - if (event.data.runId === runId) { + const subscribe = this.on.bind(this) as unknown as ( + eventType: string, + handler: (event: SessionEvent) => void + ) => () => void; + const handleRunUpdated = (event: SessionEvent): void => { + const data = event.data as { runId?: unknown }; + if (data.runId === runId) { void read(); } - }); + }; + const unsubscribeFactory = subscribe("factory.run_updated", handleRunUpdated); + const unsubscribeWorkflow = subscribe("workflow.run_updated", handleRunUpdated); + unsubscribe = (): void => { + unsubscribeFactory(); + unsubscribeWorkflow(); + }; pollHandle = setInterval(() => void read(), 5_000); // The re-read is a safety net, not work the process owes anyone: an @@ -727,6 +739,9 @@ export class CopilotSession { ...(await getTraceContext(this.traceContextProvider)), sessionId: this.sessionId, prompt: options.prompt, + ...(options.clientCorrelationId == null + ? {} + : { clientCorrelationId: options.clientCorrelationId }), source: options.source, displayPrompt: options.displayPrompt, attachments: options.attachments, @@ -1111,6 +1126,13 @@ export class CopilotSession { * @internal This method is for internal use by the SDK. */ _dispatchEvent(event: SessionEvent): void { + if ( + event.type === "user.message" || + event.type === "system.notification" || + event.type === "assistant.turn_start" + ) { + event = { ...event, data: withWorkerCausality(event.data) } as SessionEvent; + } // Handle broadcast request events internally (fire-and-forget) this._handleBroadcastEvent(event); @@ -2236,7 +2258,7 @@ export class CopilotSession { sessionId: this.sessionId, }); - return (response as { events: SessionEvent[] }).events; + return withWorkerCausalityEvents(response as { events: SessionEvent[] }).events; } /** diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index c8c9c0a53b..14327227f5 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -3352,6 +3352,13 @@ export interface MessageOptions { */ prompt: string; + /** + * Optional caller-owned diagnostic UUID for this RPC admission, sent unchanged. + * Supported runtimes echo accepted values only with RUNTIME_ADMISSION_TRACE_CONTEXT + * enabled. Not an idempotency key; the SDK never generates this value. + */ + clientCorrelationId?: string; + /** * Optional message provenance. Omitted by default to preserve the runtime's * default for user messages. Use "system" for application-generated context diff --git a/nodejs/src/workerCausality.ts b/nodejs/src/workerCausality.ts new file mode 100644 index 0000000000..fe5490d251 --- /dev/null +++ b/nodejs/src/workerCausality.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +function object(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function fields(value: Record, allowed: readonly string[]): boolean { + return Object.keys(value).every((key) => allowed.includes(key)); +} + +function scalarString(value: string): boolean { + for (let index = 0; index < value.length; index++) { + const unit = value.charCodeAt(index); + if (unit >= 0xd800 && unit <= 0xdbff) { + const next = value.charCodeAt(index + 1); + if (!(next >= 0xdc00 && next <= 0xdfff)) return false; + index++; + } else if (unit >= 0xdc00 && unit <= 0xdfff) { + return false; + } + } + return true; +} + +function text(value: unknown): value is string { + return ( + typeof value === "string" && + value.length > 0 && + scalarString(value) && + Buffer.byteLength(value, "utf8") <= 256 && + !/\p{Cc}/u.test(value) + ); +} + +function small(value: unknown, budget: { remaining: number }, depth = 0): boolean { + budget.remaining--; + if (budget.remaining < 0 || depth > 32) return false; + if (typeof value === "string") budget.remaining -= value.length; + if (object(value)) { + for (const [key, item] of Object.entries(value)) { + if (!small(key, budget, depth + 1) || !small(item, budget, depth + 1)) return false; + } + } else if (Array.isArray(value)) { + for (const item of value) if (!small(item, budget, depth + 1)) return false; + } + return budget.remaining >= 0; +} + +function reference(value: unknown, type: string): boolean { + return ( + object(value) && + fields(value, ["sessionId", "eventId", "agentId", "eventType", "provenance"]) && + text(value.sessionId) && + typeof value.eventId === "string" && + /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(value.eventId) && + (!("agentId" in value) || text(value.agentId)) && + value.eventType === type && + (value.provenance === "native" || value.provenance === "ahp_coordinator") + ); +} + +function source(value: unknown): boolean { + if (!object(value) || !object(value.input)) return false; + const input = value.input; + return ( + fields(value, [ + "input", + "admissions", + "captureComplete", + "completion", + "notification", + "admittedDuring", + ]) && + fields(input, ["queueItemId", "agentId", "sender", "senderBridges"]) && + typeof value.captureComplete === "boolean" && + typeof input.queueItemId === "string" && + /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(input.queueItemId) && + text(input.agentId) && + (!("sender" in input) || reference(input.sender, "tool.execution_start")) && + (!("senderBridges" in input) || + ("sender" in input && + Array.isArray(input.senderBridges) && + input.senderBridges.length <= 32 && + input.senderBridges.every( + (edge: unknown) => + object(edge) && + fields(edge, ["source", "reported"]) && + reference(edge.source, "tool.execution_start") && + reference(edge.reported, "tool.execution_start") + ))) && + Array.isArray(value.admissions) && + value.admissions.length <= 32 && + value.admissions.every( + (item: unknown) => + object(item) && + fields(item, ["kind", "messageId", "event", "ahpTurnId"]) && + (item.kind === "queued_input" || item.kind === "system_continuation") && + text(item.messageId) && + (!("event" in item) || + (reference(item.event, "user.message") && + (item.event as Record).agentId === input.agentId)) && + (!("ahpTurnId" in item) || + (typeof item.ahpTurnId === "string" && + /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test( + item.ahpTurnId + ))) + ) && + (!("completion" in value) || reference(value.completion, "subagent.completed")) && + (!("admittedDuring" in value) || reference(value.admittedDuring, "assistant.turn_start")) && + (!("notification" in value) || + (object(value.notification) && + fields(value.notification, ["deliveryId", "event", "mode"]) && + typeof value.notification.deliveryId === "string" && + /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test( + value.notification.deliveryId + ) && + (value.notification.mode === "queued" || value.notification.mode === "immediate") && + (!("event" in value.notification) || + reference(value.notification.event, "system.notification")))) + ); +} + +/** Decode availability only; consumers must still validate placement and enclosing self identity. */ +export function withWorkerCausality(data: T): T { + if (!("workerCausality" in data)) return data; + const value = data.workerCausality; + const valid = + object(value) && + fields(value, ["version", "observationProvenance", "sources", "captureComplete"]) && + value.version === 1 && + (value.observationProvenance === "native" || + value.observationProvenance === "ahp_coordinator") && + typeof value.captureComplete === "boolean" && + Array.isArray(value.sources) && + value.sources.length <= 32 && + small(value, { remaining: 4096 }) && + value.sources.every( + (item: unknown) => + source(item) && + (!value.captureComplete || + (item as Record).captureComplete === true) + ) && + Buffer.byteLength(JSON.stringify({ workerCausality: value }), "utf8") <= 4096; + if (valid) return data; + console.warn("Ignoring invalid, unsupported or oversized workerCausality metadata"); + const copy = { ...data }; + delete (copy as { workerCausality?: unknown }).workerCausality; + return copy; +} + +/** Apply the same diagnostic tolerance to historical events as live delivery. */ +export function withWorkerCausalityEvents(result: T): T { + if (!("events" in result) || !Array.isArray(result.events)) return result; + const originalEvents = result.events; + const events = originalEvents.map((event) => { + if (!object(event) || !object(event.data)) return event; + if ( + event.type !== "user.message" && + event.type !== "system.notification" && + event.type !== "assistant.turn_start" + ) { + return event; + } + const data = withWorkerCausality(event.data); + return data === event.data ? event : { ...event, data }; + }); + return events.every((event, index) => event === originalEvents[index]) + ? result + : ({ ...result, events } as T); +} diff --git a/nodejs/test/csharp-codegen.test.ts b/nodejs/test/csharp-codegen.test.ts index b603e9e775..9a6ca7510d 100644 --- a/nodejs/test/csharp-codegen.test.ts +++ b/nodejs/test/csharp-codegen.test.ts @@ -175,9 +175,126 @@ describe("C# root event payload unions", () => { expect(code).toContain("public sealed partial class SampleEmptyData"); expect(code).not.toContain("public sealed partial class SampleUnionData { }"); }); + + it("keeps optional worker converter attributes attached after XML documentation", () => { + const code = generateSessionEventsCode({ + definitions: { + WorkerCausality: { + type: "object", + properties: { version: { type: "integer" } }, + required: ["version"], + }, + SessionEvent: { + anyOf: [ + { + type: "object", + properties: { + type: { const: "user.message" }, + data: { + type: "object", + properties: { + content: { type: "string" }, + workerCausality: { + $ref: "#/definitions/WorkerCausality", + description: "Optional worker diagnostics.", + }, + }, + required: ["content"], + }, + }, + required: ["type", "data"], + }, + ], + }, + }, + }); + expect(code).toContain( + "/// Optional worker diagnostics.\n" + + " [JsonConverter(typeof(WorkerCausalityConverter))]\n" + + " [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]\n" + + ' [JsonPropertyName("workerCausality")]' + ); + }); + + it("projects worker UUID identities as strings", () => { + const code = generateSessionEventsCode({ + definitions: { + WorkerEventReference: { + type: "object", + properties: { + eventId: { type: "string", format: "uuid" }, + }, + required: ["eventId"], + }, + WorkerCausality: { + type: "object", + properties: { + event: { $ref: "#/definitions/WorkerEventReference" }, + }, + required: ["event"], + }, + SessionEvent: { + anyOf: [ + { + type: "object", + properties: { + type: { const: "user.message" }, + data: { + type: "object", + properties: { + content: { type: "string" }, + workerCausality: { + $ref: "#/definitions/WorkerCausality", + }, + }, + required: ["content"], + }, + }, + required: ["type", "data"], + }, + ], + }, + }, + }); + expect(code).toContain("public required string EventId { get; set; }"); + expect(code).not.toContain("public required Guid EventId { get; set; }"); + }); }); describe("C# RPC codegen", () => { + it("preserves positional send arguments and the existing CLR signature", () => { + const code = generateRpcCode({ + session: { + send: { + rpcMethod: "session.send", + params: { + title: "SendRequest", + type: "object", + properties: { + sessionId: { type: "string" }, + prompt: { type: "string" }, + clientCorrelationId: { type: "string" }, + displayPrompt: { type: "string" }, + wait: { type: "boolean" }, + }, + required: ["sessionId", "prompt"], + }, + }, + }, + }); + + expect(code).toContain( + "SendAsync(string prompt, string? displayPrompt = null, bool? wait = null, string? clientCorrelationId = null, CancellationToken cancellationToken = default)" + ); + expect(code).toContain("ClientCorrelationId = clientCorrelationId"); + expect(code).toContain( + "SendAsync(string prompt, string? displayPrompt, bool? wait, CancellationToken cancellationToken)" + ); + expect(code).toContain( + "=> SendAsync(prompt, displayPrompt, wait, clientCorrelationId: null, cancellationToken: cancellationToken);" + ); + }); + it.each(["uninstall", "update"])( "separates the session wire envelope from the shared plugins %s request", (method) => { diff --git a/nodejs/test/message-identity-types.test.ts b/nodejs/test/message-identity-types.test.ts index 903fc7533c..51933ff7bd 100644 --- a/nodejs/test/message-identity-types.test.ts +++ b/nodejs/test/message-identity-types.test.ts @@ -1,8 +1,58 @@ -import { describe, expect, it } from "vitest"; -import type { QueuePendingItems } from "../src/generated/rpc.js"; -import type { UserMessageData } from "../src/generated/session-events.js"; +import { describe, expect, expectTypeOf, it } from "vitest"; +import type { QueuePendingItems, SendMessageItem, SendRequest } from "../src/generated/rpc.js"; +import type { ToolExecutionStartData, UserMessageData } from "../src/generated/session-events.js"; describe("generated message identity types", () => { + it("keeps admission correlation optional, string-valued, and independent of messageId", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + string | undefined + >(); + expectTypeOf().toEqualTypeOf(); + + for (const clientCorrelationId of [ + undefined, + "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", + "not-a-uuid", + "", + ]) { + const values: Array< + SendRequest | SendMessageItem | QueuePendingItems | UserMessageData + > = [ + { prompt: "hello", clientCorrelationId }, + { + id: "queue-1", + messageId: "canonical-1", + kind: "message", + displayText: "hello", + agentMode: "interactive", + clientCorrelationId, + }, + { + content: "hello", + messageId: "canonical-1", + interactionId: "agent-loop-1", + clientCorrelationId, + }, + ]; + for (const value of values) { + const decoded = JSON.parse(JSON.stringify(value)); + expect(decoded.clientCorrelationId).toBe(clientCorrelationId); + expect(Object.hasOwn(decoded, "clientCorrelationId")).toBe( + clientCorrelationId !== undefined + ); + expect(decoded.messageId).toBe("messageId" in value ? value.messageId : undefined); + const future = JSON.parse( + JSON.stringify({ ...value, futureField: { enabled: true } }) + ); + expect(future.clientCorrelationId).toBe(clientCorrelationId); + expect(future.futureField).toEqual({ enabled: true }); + } + } + }); + it("exposes optional camelCase message IDs", () => { const queueItemWithIdentity: QueuePendingItems = { id: "queue-1", @@ -30,4 +80,31 @@ describe("generated message identity types", () => { expect(userMessageWithIdentity.messageId).toBe("message-1"); expect(userMessageFromOlderRuntime.messageId).toBeUndefined(); }); + + it("exposes optional early tool context without interpreting it", () => { + const contexts: Array> = [ + {}, + { + traceparent: "00-11111111111111111111111111111111-2222222222222222-01", + tracestate: "vendor=value", + }, + { traceparent: "00-11111111111111111111111111111111-2222222222222222-00" }, + { traceparent: "invalid", tracestate: "invalid" }, + { traceparent: "" }, + ]; + for (const context of contexts) { + const data: ToolExecutionStartData = { + toolCallId: "tool-call-a", + toolName: "client-tool", + ...context, + }; + expect(data.traceparent).toBe(context.traceparent); + expect(data.tracestate).toBe(context.tracestate); + expect(JSON.parse(JSON.stringify(data))).toEqual({ + toolCallId: "tool-call-a", + toolName: "client-tool", + ...context, + }); + } + }); }); diff --git a/nodejs/test/message-source.test.ts b/nodejs/test/message-source.test.ts index 394e42163c..71d911904c 100644 --- a/nodejs/test/message-source.test.ts +++ b/nodejs/test/message-source.test.ts @@ -46,6 +46,117 @@ function sessionPair(traceContextProvider?: ConstructorParameters { + it.each([ + undefined, + "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", + "not-a-uuid", + "", + ])( + "forwards only explicitly supplied strings without changing other options: %s", + async (clientCorrelationId) => { + const trace = { + traceparent: "00-11111111111111111111111111111111-2222222222222222-01", + tracestate: "vendor=preserved", + }; + const { session, server } = sessionPair(() => trace); + const options = { + prompt: "hello", + source: "system", + mode: "enqueue", + displayPrompt: "display", + requestHeaders: { "X-Test": "preserved" }, + clientCorrelationId, + } satisfies MessageOptions; + server.onRequest("session.send", (params: unknown) => { + expect(params).toEqual( + JSON.parse(JSON.stringify({ sessionId: "session-1", ...trace, ...options })) + ); + return { messageId: "canonical-1", futureField: true }; + }); + await expect(session.send(options)).resolves.toBe("canonical-1"); + await expect(session.rpc.send({ ...trace, ...options })).resolves.toMatchObject({ + messageId: "canonical-1", + }); + } + ); + + it("omits null metadata at the untyped convenience boundary", async () => { + const { session, server } = sessionPair(); + server.onRequest("session.send", (params: unknown) => { + expect(params).toEqual({ sessionId: "session-1", prompt: "hello" }); + return { messageId: "canonical-1" }; + }); + await expect( + session.send({ + prompt: "hello", + clientCorrelationId: null, + } as unknown as MessageOptions) + ).resolves.toBe("canonical-1"); + }); + + it("retains admission metadata through sendAndWait", async () => { + const clientCorrelationId = "01234567-89ab-4cde-8f01-23456789abcd"; + const { session, server } = sessionPair(); + server.onRequest("session.send", (params: unknown) => { + expect(params).toEqual({ + sessionId: "session-1", + prompt: "hello", + clientCorrelationId, + }); + session._dispatchEvent({ + type: "session.idle", + id: "idle-1", + parentId: null, + timestamp: "2026-09-24T18:00:00Z", + ephemeral: true, + data: {}, + }); + return { messageId: "canonical-1" }; + }); + await expect( + session.sendAndWait({ prompt: "hello", clientCorrelationId }, 1000) + ).resolves.toBeUndefined(); + }); + + it("keeps concurrent and repeated values per input, not per batch", async () => { + const first = "01234567-89ab-4cde-8f01-23456789abcd"; + const second = "abcdef01-2345-4678-9abc-def012345678"; + const { session, server } = sessionPair(); + const messages = [ + { prompt: "context", clientCorrelationId: second }, + { prompt: "plain" }, + { prompt: "reused", clientCorrelationId: first }, + ]; + let sends = 0; + server.onRequest( + "session.send", + (params: { prompt: string; clientCorrelationId: string }) => { + sends++; + expect(params.clientCorrelationId).toBe(params.prompt === "first" ? first : second); + return { messageId: `${params.prompt}-id` }; + } + ); + server.onRequest("session.sendMessages", (params: unknown) => { + expect(params).toEqual({ sessionId: "session-1", messages }); + return { messageIds: ["context-id", "plain-id", "reused-id"] }; + }); + await expect( + Promise.all([ + session.send({ prompt: "first", clientCorrelationId: first }), + session.send({ prompt: "second", clientCorrelationId: second }), + session.rpc.sendMessages({ messages }), + ]) + ).resolves.toEqual([ + "first-id", + "second-id", + { messageIds: ["context-id", "plain-id", "reused-id"] }, + ]); + expect(sends).toBe(2); + }); +}); + it("omits source when sending a plain human prompt", async () => { const { session, server } = sessionPair(); server.onRequest("session.send", (params: unknown) => { diff --git a/nodejs/test/python-codegen.test.ts b/nodejs/test/python-codegen.test.ts index c381a406c2..9f43a4c21a 100644 --- a/nodejs/test/python-codegen.test.ts +++ b/nodejs/test/python-codegen.test.ts @@ -49,6 +49,52 @@ describe("Python root event payload unions", () => { expect(code).toContain("data = SampleUnionData.from_dict(data_obj)"); expect(code).toContain("return SampleEmptyData()"); }); + + it("projects worker UUID identities as strings", () => { + const code = generatePythonSessionEventsCode({ + definitions: { + WorkerEventReference: { + type: "object", + properties: { + eventId: { type: "string", format: "uuid" }, + }, + required: ["eventId"], + }, + WorkerCausality: { + type: "object", + properties: { + event: { $ref: "#/definitions/WorkerEventReference" }, + }, + required: ["event"], + }, + SessionEvent: { + anyOf: [ + { + type: "object", + properties: { + type: { const: "user.message" }, + data: { + type: "object", + properties: { + content: { type: "string" }, + workerCausality: { + $ref: "#/definitions/WorkerCausality", + }, + }, + required: ["content"], + }, + }, + required: ["type", "data"], + }, + ], + }, + }, + }); + expect(code).toContain("class WorkerEventReference:"); + expect(code).toContain(" event_id: str"); + expect(code).toContain('event_id = from_str(obj.get("eventId"))'); + expect(code).not.toContain('event_id = from_uuid(obj.get("eventId"))'); + }); }); function pythonRequestSnippet(additions: 1 | 2): string { diff --git a/nodejs/test/rust-codegen.test.ts b/nodejs/test/rust-codegen.test.ts index 10362d1439..514f5f3cc6 100644 --- a/nodejs/test/rust-codegen.test.ts +++ b/nodejs/test/rust-codegen.test.ts @@ -3,6 +3,7 @@ import { normalizeSchemaBrandCasing, postProcessSchema, propagateInternalVisibility, + resolveCopilotSchemaPaths, stripBooleanLiterals, } from "../../scripts/codegen/utils.ts"; import type { JSONSchema7 } from "json-schema"; @@ -772,18 +773,14 @@ describe("Rust x-legacy-parameters", () => { ).toThrow(/Rust string enum Kind is requested for different values/); }); - it("keeps every const discriminator of the committed API schema distinct in Rust", () => { + it("keeps every const discriminator of the selected API schema distinct in Rust", async () => { + const { apiSchemaPath } = await resolveCopilotSchemaPaths(); // Mirror the generator's own schema preparation so emission order matches. const schema = propagateInternalVisibility( postProcessSchema( stripBooleanLiterals( normalizeSchemaBrandCasing( - JSON.parse( - readFileSync( - new URL("../../../../generated/api.schema.json", import.meta.url), - "utf8" - ) - ) as ApiSchema + JSON.parse(readFileSync(apiSchemaPath, "utf8")) as ApiSchema ) ) as JSONSchema7 ) diff --git a/nodejs/test/worker-causality.test.ts b/nodejs/test/worker-causality.test.ts new file mode 100644 index 0000000000..7b57341ed2 --- /dev/null +++ b/nodejs/test/worker-causality.test.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { randomUUID } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { PassThrough } from "node:stream"; +import { + createMessageConnection, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node.js"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { approveAll, CopilotSession } from "../src/index.js"; +import type { SessionEvent } from "../src/index.js"; +import { createServerRpc } from "../src/generated/rpc.js"; +import { withWorkerCausalityEvents } from "../src/workerCausality.js"; +import { createSdkTestContext } from "./e2e/harness/sdkTestContext.js"; + +type Wire = Record; +type HistoryRead = { name: string; events: SessionEvent[] }; +const corpus = JSON.parse( + readFileSync(new URL("../../test/worker-causality.json", import.meta.url), "utf8") +) as { + valid: { name: string; event?: Wire; result?: Wire }[]; + invalid: { name: string; value: unknown }[]; + invalidRaw: { name: string; json: string }[]; + boundaries: { name: string; value: unknown; accepted: boolean }[]; + workflowCompleted: { event: Wire }; + legacyWorkflowCompleted: { persistedEvent: Wire }; +}; + +function payload(value: Wire, event: boolean): Wire { + return event ? (value.data as Wire) : value; +} + +function expectWorkflowCompleted(events: SessionEvent[], eventId: string, reader: string): void { + const event = events.find((candidate) => candidate.id === eventId); + expect(event?.type, reader).toBe("system.notification"); + if (event?.type !== "system.notification") { + throw new Error(`${reader} did not return the workflow completion event`); + } + expect(event.data.kind.type, reader).toBe("workflow_completed"); + if (event.data.kind.type !== "workflow_completed") { + throw new Error(`${reader} did not canonicalize the workflow completion event`); + } + expect(event.data.kind.workflowName, reader).toBe("fix-ci"); + expect(event.data.kind, reader).not.toHaveProperty("factoryName"); +} + +function readers() { + const requests = new PassThrough(); + const responses = new PassThrough(); + const client = createMessageConnection( + new StreamMessageReader(responses), + new StreamMessageWriter(requests) + ); + const server = createMessageConnection( + new StreamMessageReader(requests), + new StreamMessageWriter(responses) + ); + let reply: unknown; + server.onRequest(() => reply); + client.listen(); + server.listen(); + const session = new CopilotSession("codec-session", client); + const serverRpc = createServerRpc(client); + const observed: SessionEvent[] = []; + const unsubscribe = session.on((event) => observed.push(event)); + onTestFinished(() => { + unsubscribe(); + client.dispose(); + server.dispose(); + requests.destroy(); + responses.destroy(); + }); + return { + async read(wire: Wire, event: boolean): Promise { + if (event) { + session._dispatchEvent(wire as unknown as SessionEvent); + return observed.at(-1) as unknown as Wire; + } + reply = wire; + return (await session.rpc.tasks.sendMessage({ + id: "worker", + message: "test", + })) as unknown as Wire; + }, + async history(wire: Wire): Promise { + reply = { events: [wire], cursor: "cursor", hasMore: false, cursorStatus: "ok" }; + return [ + { name: "session.getMessages", events: await session.getEvents() }, + { + name: "session.eventLog.read", + events: (await session.rpc.eventLog.read({})).events, + }, + { + name: "sessions.readPersistedEvents", + events: ( + await serverRpc.sessions.readPersistedEvents({ + sessionId: "codec-session", + }) + ).events, + }, + ]; + }, + }; +} + +describe("worker causality public readers", () => { + it.each([ + ["absent events", { method: "session.eventLog.read", state: { phase: "covered" } }], + ["non-array events", { events: { state: "covered" } }], + ])("leaves RPC results with $0 unchanged", (_name, result) => { + expect(withWorkerCausalityEvents(result)).toBe(result); + }); + + it("preserves unrelated workerCausality product data on unsupported event types", async () => { + const reader = readers(); + const event = { + type: "session.start", + data: { + sessionId: "codec-session", + version: 1, + producer: "extension", + copilotVersion: "test", + startTime: "2026-01-02T03:04:05.000Z", + workerCausality: { + owner: "extension-product-data", + }, + }, + id: "10000000-0000-4000-8000-000000000001", + timestamp: "2026-01-02T03:04:05.000Z", + parentId: null, + }; + const expected = JSON.stringify(event); + for (const history of await reader.history(event)) { + expect(JSON.stringify(history.events[0]), history.name).toBe(expected); + } + }); + + it.each(corpus.valid)( + "$name preserves exact observations and historical product bytes", + async (test) => { + const reader = readers(); + const event = test.event !== undefined; + const wire = structuredClone((test.event ?? test.result) as Wire); + const original = JSON.stringify(wire); + expect(payload(await reader.read(wire, event), event).workerCausality).toEqual( + payload(wire, event).workerCausality + ); + expect(JSON.stringify(wire)).toBe(original); + if (event) { + for (const history of await reader.history(wire)) { + expect( + payload(history.events[0] as unknown as Wire, true).workerCausality, + history.name + ).toEqual(payload(wire, true).workerCausality); + expect(JSON.stringify(history.events[0]), history.name).toBe(original); + } + } + + delete payload(wire, event).workerCausality; + const baseline = JSON.stringify(await reader.read(wire, event)); + if (event) { + for (const history of await reader.history(wire)) { + expect(JSON.stringify(history.events[0]), `${history.name} absent`).toBe( + baseline + ); + } + } + for (const invalid of corpus.invalid) { + const candidate = structuredClone(wire); + payload(candidate, event).workerCausality = invalid.value; + expect(JSON.stringify(await reader.read(candidate, event)), invalid.name).toBe( + baseline + ); + if (event) { + for (const history of await reader.history(candidate)) { + expect( + JSON.stringify(history.events[0]), + `${history.name} ${invalid.name}` + ).toBe(baseline); + } + } + } + } + ); + + it.each(corpus.boundaries)( + "$name enforces compact UTF-8 bytes and tolerates unknown fields", + async (test) => { + const reader = readers(); + const wire = structuredClone(corpus.valid[0].event as Wire); + payload(wire, true).workerCausality = test.value; + const data = payload(await reader.read(wire, true), true); + expect("workerCausality" in data).toBe(test.accepted); + expect(data.content).toBe(payload(wire, true).content); + } + ); + + it("decodes the canonical workflow completion notification with typed fields", async () => { + const reader = readers(); + const eventId = corpus.workflowCompleted.event.id; + if (typeof eventId !== "string") { + throw new Error("workflow completion fixture must have an event ID"); + } + for (const history of await reader.history(corpus.workflowCompleted.event)) { + expectWorkflowCompleted(history.events, eventId, history.name); + } + }); + + it.each(corpus.invalidRaw)("$name rejects invalid Unicode scalar identities", async (test) => { + const reader = readers(); + const result = { + sent: true, + workerCausality: JSON.parse(test.json), + }; + expect(await reader.read(result, false)).toEqual({ sent: true }); + }); +}); + +describe.skipIf(process.env.COPILOT_RUNTIME_SOURCE !== "checkout")( + "worker causality persisted history", + async () => { + const { copilotClient: client, env } = await createSdkTestContext(); + + it("canonicalizes legacy workflow completion through every history reader", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + const sessionId = session.sessionId; + await client.rpc.sessions.save({ sessionId }); + await session.disconnect(); + + const startId = randomUUID(); + const timestamp = new Date().toISOString(); + const persistedEvent = { + ...structuredClone(corpus.legacyWorkflowCompleted.persistedEvent), + parentId: startId, + }; + const eventId = persistedEvent.id; + if (typeof eventId !== "string") { + throw new Error("legacy workflow fixture must have an event ID"); + } + const copilotHome = env.COPILOT_HOME; + if (copilotHome === undefined) { + throw new Error("SDK test context must define COPILOT_HOME"); + } + await writeFile( + join(copilotHome, "session-state", sessionId, "events.jsonl"), + `${JSON.stringify({ + type: "session.start", + data: { + sessionId, + version: 1, + producer: "node-sdk-worker-history-test", + copilotVersion: "test", + startTime: timestamp, + }, + id: startId, + timestamp, + parentId: null, + })}\n${JSON.stringify(persistedEvent)}\n` + ); + + const persisted = await client.rpc.sessions.readPersistedEvents({ + sessionId, + max: 1000, + }); + expectWorkflowCompleted(persisted.events, eventId, "sessions.readPersistedEvents"); + + const resumed = await client.resumeSession(sessionId, { + onPermissionRequest: approveAll, + }); + try { + expectWorkflowCompleted(await resumed.getEvents(), eventId, "session.getMessages"); + const active = await resumed.rpc.eventLog.read({ max: 1000, waitMs: 0 }); + expectWorkflowCompleted(active.events, eventId, "session.eventLog.read"); + } finally { + await resumed.disconnect(); + } + }); + } +); diff --git a/python/copilot/_worker_causality.py b/python/copilot/_worker_causality.py new file mode 100644 index 0000000000..af9834af2a --- /dev/null +++ b/python/copilot/_worker_causality.py @@ -0,0 +1,171 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +"""Optional diagnostic decoding; consumers still validate enclosing self identity.""" + +import json +import logging +import re +import unicodedata +from collections.abc import Callable +from typing import Any, TypeVar + +T = TypeVar("T") +_LOG = logging.getLogger(__name__) +_UUID = re.compile(r"[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}") + + +def _uuid(value: Any) -> bool: + return isinstance(value, str) and _UUID.fullmatch(value) is not None + + +def _text(value: Any) -> bool: + return ( + isinstance(value, str) + and bool(value) + and len(value.encode("utf-8")) <= 256 + and all(unicodedata.category(char) not in ("Cc", "Cs") for char in value) + ) + + +def _fields(value: dict[str, Any], allowed: set[str]) -> bool: + return value.keys() <= allowed + + +def _reference(value: Any, event_type: str) -> bool: + return ( + isinstance(value, dict) + and _fields(value, {"sessionId", "eventId", "agentId", "eventType", "provenance"}) + and _text(value.get("sessionId")) + and _uuid(value.get("eventId")) + and ("agentId" not in value or _text(value["agentId"])) + and value.get("eventType") == event_type + and value.get("provenance") in ("native", "ahp_coordinator") + ) + + +def _source(value: Any) -> bool: + if not isinstance(value, dict) or not isinstance(value.get("input"), dict): + return False + source_input = value["input"] + if ( + not _fields( + value, + { + "input", + "admissions", + "captureComplete", + "completion", + "notification", + "admittedDuring", + }, + ) + or not _fields(source_input, {"queueItemId", "agentId", "sender", "senderBridges"}) + or type(value.get("captureComplete")) is not bool + or not _uuid(source_input.get("queueItemId")) + or not _text(source_input.get("agentId")) + or not isinstance(value.get("admissions"), list) + or len(value["admissions"]) > 32 + ): + return False + if "sender" in source_input and not _reference(source_input["sender"], "tool.execution_start"): + return False + if "senderBridges" in source_input: + edges = source_input["senderBridges"] + if "sender" not in source_input or not isinstance(edges, list) or len(edges) > 32: + return False + if any( + not isinstance(edge, dict) + or not _fields(edge, {"source", "reported"}) + or not _reference(edge.get("source"), "tool.execution_start") + or not _reference(edge.get("reported"), "tool.execution_start") + for edge in edges + ): + return False + for admission in value["admissions"]: + if ( + not isinstance(admission, dict) + or not _fields(admission, {"kind", "messageId", "event", "ahpTurnId"}) + or admission.get("kind") not in ("queued_input", "system_continuation") + or not _text(admission.get("messageId")) + or ("ahpTurnId" in admission and not _uuid(admission["ahpTurnId"])) + or ( + "event" in admission + and ( + not _reference(admission["event"], "user.message") + or admission["event"].get("agentId") != source_input["agentId"] + ) + ) + ): + return False + for field, event_type in [ + ("completion", "subagent.completed"), + ("admittedDuring", "assistant.turn_start"), + ]: + if field in value and not _reference(value[field], event_type): + return False + if "notification" in value: + notification = value["notification"] + if ( + not isinstance(notification, dict) + or not _fields(notification, {"deliveryId", "event", "mode"}) + or not _uuid(notification.get("deliveryId")) + or notification.get("mode") not in ("queued", "immediate") + or ( + "event" in notification + and not _reference(notification["event"], "system.notification") + ) + ): + return False + return True + + +def _supported(value: Any) -> bool: + return ( + isinstance(value, dict) + and _fields(value, {"version", "observationProvenance", "sources", "captureComplete"}) + and type(value.get("version")) is int + and value["version"] == 1 + and value.get("observationProvenance") in ("native", "ahp_coordinator") + and type(value.get("captureComplete")) is bool + and isinstance(value.get("sources"), list) + and len(value["sources"]) <= 32 + and all( + _source(source) and (not value["captureComplete"] or source["captureComplete"]) + for source in value["sources"] + ) + ) + + +def _small(value: Any, remaining: list[int], depth: int = 0) -> bool: + remaining[0] -= 1 + if remaining[0] < 0 or depth > 32: + return False + if isinstance(value, str): + remaining[0] -= len(value) + elif isinstance(value, dict): + for key, item in value.items(): + if not _small(key, remaining, depth + 1) or not _small(item, remaining, depth + 1): + return False + elif isinstance(value, list): + for item in value: + if not _small(item, remaining, depth + 1): + return False + return remaining[0] >= 0 + + +def optional_worker_causality(value: Any, load: Callable[[], T]) -> T | None: + if value is None: + return None + try: + if not _small(value, [4096]) or not _supported(value): + raise ValueError("invalid optional worker metadata") + size = 0 + encoder = json.JSONEncoder(ensure_ascii=False, separators=(",", ":")) + for chunk in encoder.iterencode({"workerCausality": value}): + size += len(chunk.encode("utf-8")) + if size > 4096: + raise ValueError("optional worker metadata budget") + return load() + except (AssertionError, AttributeError, KeyError, TypeError, ValueError, UnicodeError): + _LOG.warning("Ignoring invalid, unsupported or oversized workerCausality metadata") + return None diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 8811b53745..eed155c3c5 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -6,7 +6,7 @@ from typing import ClassVar, TYPE_CHECKING -from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, IndexedSearchState, ManagedSettingsResolvedSource, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionDecisionSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskBlocker, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, _load_Attachment, _load_PermissionPromptRequest, _load_UserToolSessionApproval +from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, IndexedSearchState, ManagedSettingsResolvedSource, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionDecisionSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskBlocker, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, WorkerCausality, _load_Attachment, _load_PermissionPromptRequest, _load_UserToolSessionApproval if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -19,6 +19,7 @@ from uuid import UUID import dateutil.parser +from .._worker_causality import optional_worker_causality T = TypeVar("T") EnumT = TypeVar("EnumT", bound=Enum) @@ -10654,8 +10655,9 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. class SandboxConfigSource(Enum): - """Origin of the sandbox choice supplied by the host. This value describes preference or - session intent; it does not authorize bypassing managed policy. + """Origin of the sandbox choice supplied by the host. Settings-derived origins let managed + policy floor the host preference; do not tag explicit session overrides as + settings-derived. Origin of the sandbox choice. Settings-derived origins (never_configured, user_enabled, user_disabled, repository_policy) let managed policy floor a host preference; explicit @@ -11140,6 +11142,12 @@ class SendMessageItem: """If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. """ + client_correlation_id: str | None = None + """Optional caller-generated diagnostic UUID for this item only, with the same validation + and opt-in native echo as session.send.clientCorrelationId. The batch has no + request-level correlation value; each item retains its own value, including preceding + context messages. Reused values do not deduplicate messages and remain ambiguous. + """ display_prompt: str | None = None """If provided, this is shown in the timeline instead of `prompt`""" @@ -11160,10 +11168,11 @@ def from_dict(obj: Any) -> 'SendMessageItem': prompt = from_str(obj.get("prompt")) attachments = from_union([lambda x: from_list(_load_Attachment, x), from_none], obj.get("attachments")) billable = from_union([from_bool, from_none], obj.get("billable")) + client_correlation_id = from_union([from_str, from_none], obj.get("clientCorrelationId")) display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) required_tool = from_union([from_str, from_none], obj.get("requiredTool")) source = from_union([from_str, from_none], obj.get("source")) - return SendMessageItem(prompt, attachments, billable, display_prompt, required_tool, source) + return SendMessageItem(prompt, attachments, billable, client_correlation_id, display_prompt, required_tool, source) def to_dict(self) -> dict: result: dict = {} @@ -11172,6 +11181,8 @@ def to_dict(self) -> dict: result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(Attachment, x), x), from_none], self.attachments) if self.billable is not None: result["billable"] = from_union([from_bool, from_none], self.billable) + if self.client_correlation_id is not None: + result["clientCorrelationId"] = from_union([from_str, from_none], self.client_correlation_id) if self.display_prompt is not None: result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) if self.required_tool is not None: @@ -14765,18 +14776,26 @@ class TasksSendMessageResult: error: str | None = None """Error message if delivery failed""" + worker_causality: WorkerCausality | None = None + """Optional exact queue admission receipt on sent=true only. No implicit event or + execution-success claim. + """ + @staticmethod def from_dict(obj: Any) -> 'TasksSendMessageResult': assert isinstance(obj, dict) sent = from_bool(obj.get("sent")) error = from_union([from_str, from_none], obj.get("error")) - return TasksSendMessageResult(sent, error) + worker_causality = optional_worker_causality(obj.get("workerCausality"), lambda: from_union([WorkerCausality.from_dict, from_none], obj.get("workerCausality"))) + return TasksSendMessageResult(sent, error, worker_causality) def to_dict(self) -> dict: result: dict = {} result["sent"] = from_bool(self.sent) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) + if self.worker_causality is not None: + result["workerCausality"] = from_union([lambda x: to_class(WorkerCausality, x), from_none], self.worker_causality) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -25982,6 +26001,12 @@ class QueuePendingItems: kind: QueuePendingItemsKind """Whether this item is a queued user message or a queued slash command / model change""" + client_correlation_id: str | None = None + """Caller-owned diagnostic UUID from the exact accepted native session.send or sendMessages + item, when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted for unsupported or + identity-less rows, including snapshot-only mirrors. Not an idempotency key, + authorization, or permission to retry; repeated values remain ambiguous. + """ message_id: str | None = None """Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. @@ -26001,9 +26026,10 @@ def from_dict(obj: Any) -> 'QueuePendingItems': display_text = from_str(obj.get("displayText")) id = from_str(obj.get("id")) kind = QueuePendingItemsKind(obj.get("kind")) + client_correlation_id = from_union([from_str, from_none], obj.get("clientCorrelationId")) message_id = from_union([from_str, from_none], obj.get("messageId")) source = from_union([from_str, from_none], obj.get("source")) - return QueuePendingItems(agent_mode, display_text, id, kind, message_id, source) + return QueuePendingItems(agent_mode, display_text, id, kind, client_correlation_id, message_id, source) def to_dict(self) -> dict: result: dict = {} @@ -26011,6 +26037,8 @@ def to_dict(self) -> dict: result["displayText"] = from_str(self.display_text) result["id"] = from_str(self.id) result["kind"] = to_enum(QueuePendingItemsKind, self.kind) + if self.client_correlation_id is not None: + result["clientCorrelationId"] = from_union([from_str, from_none], self.client_correlation_id) if self.message_id is not None: result["messageId"] = from_union([from_str, from_none], self.message_id) if self.source is not None: @@ -33433,6 +33461,13 @@ class SendRequest: """If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. """ + client_correlation_id: str | None = None + """Optional caller-generated diagnostic UUID for this single message. Native sessions with + RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character + UUID on user.message and its existing pending message row. Missing, invalid, disabled, or + unsupported metadata is ignored without rejecting the send. Does not change messageId, + deduplicate submissions, authorize work, or make an uncertain retry safe. + """ display_prompt: str | None = None """If provided, this is shown in the timeline instead of `prompt`""" @@ -33490,6 +33525,7 @@ def from_dict(obj: Any) -> 'SendRequest': agent_mode = from_union([SendAgentMode, from_none], obj.get("agentMode")) attachments = from_union([lambda x: from_list(_load_Attachment, x), from_none], obj.get("attachments")) billable = from_union([from_bool, from_none], obj.get("billable")) + client_correlation_id = from_union([from_str, from_none], obj.get("clientCorrelationId")) display_prompt = from_union([from_str, from_none], obj.get("displayPrompt")) mode = from_union([SendMode, from_none], obj.get("mode")) prepend = from_union([from_bool, from_none], obj.get("prepend")) @@ -33500,7 +33536,7 @@ def from_dict(obj: Any) -> 'SendRequest': traceparent = from_union([from_str, from_none], obj.get("traceparent")) tracestate = from_union([from_str, from_none], obj.get("tracestate")) wait = from_union([from_bool, from_none], obj.get("wait")) - return SendRequest(prompt, agent_mode, attachments, billable, display_prompt, mode, prepend, request_headers, required_tool, response_format, source, traceparent, tracestate, wait) + return SendRequest(prompt, agent_mode, attachments, billable, client_correlation_id, display_prompt, mode, prepend, request_headers, required_tool, response_format, source, traceparent, tracestate, wait) def to_dict(self) -> dict: result: dict = {} @@ -33511,6 +33547,8 @@ def to_dict(self) -> dict: result["attachments"] = from_union([lambda x: from_list(lambda x: to_class(Attachment, x), x), from_none], self.attachments) if self.billable is not None: result["billable"] = from_union([from_bool, from_none], self.billable) + if self.client_correlation_id is not None: + result["clientCorrelationId"] = from_union([from_str, from_none], self.client_correlation_id) if self.display_prompt is not None: result["displayPrompt"] = from_union([from_str, from_none], self.display_prompt) if self.mode is not None: diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 6a71634747..5a2883cf13 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -13,6 +13,7 @@ from uuid import UUID import dateutil.parser +from .._worker_causality import optional_worker_causality T = TypeVar("T") EnumT = TypeVar("EnumT", bound=Enum) @@ -3279,6 +3280,7 @@ class AssistantTurnStartData: interaction_id: str | None = None model: str | None = None parent_tool_call_id: str | None = None + worker_causality: WorkerCausality | None = None @staticmethod def from_dict(obj: Any) -> "AssistantTurnStartData": @@ -3287,11 +3289,13 @@ def from_dict(obj: Any) -> "AssistantTurnStartData": interaction_id = from_union([from_none, from_str], obj.get("interactionId")) model = from_union([from_none, from_str], obj.get("model")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) + worker_causality = optional_worker_causality(obj.get("workerCausality"), lambda: from_union([from_none, WorkerCausality.from_dict], obj.get("workerCausality"))) return AssistantTurnStartData( turn_id=turn_id, interaction_id=interaction_id, model=model, parent_tool_call_id=parent_tool_call_id, + worker_causality=worker_causality, ) def to_dict(self) -> dict: @@ -3303,6 +3307,8 @@ def to_dict(self) -> dict: result["model"] = from_union([from_none, from_str], self.model) if self.parent_tool_call_id is not None: result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) + if self.worker_causality is not None: + result["workerCausality"] = from_union([from_none, lambda x: to_class(WorkerCausality, x)], self.worker_causality) return result @@ -11341,6 +11347,7 @@ class SystemNotificationData: content: str kind: SystemNotification responses_reasoning: ResponsesReasoning | None = None + worker_causality: WorkerCausality | None = None @staticmethod def from_dict(obj: Any) -> "SystemNotificationData": @@ -11348,10 +11355,12 @@ def from_dict(obj: Any) -> "SystemNotificationData": content = from_str(obj.get("content")) kind = _load_SystemNotification(obj.get("kind")) responses_reasoning = from_union([from_none, ResponsesReasoning.from_dict], obj.get("responsesReasoning")) + worker_causality = optional_worker_causality(obj.get("workerCausality"), lambda: from_union([from_none, WorkerCausality.from_dict], obj.get("workerCausality"))) return SystemNotificationData( content=content, kind=kind, responses_reasoning=responses_reasoning, + worker_causality=worker_causality, ) def to_dict(self) -> dict: @@ -11360,6 +11369,8 @@ def to_dict(self) -> dict: result["kind"] = self.kind.to_dict() if self.responses_reasoning is not None: result["responsesReasoning"] = from_union([from_none, lambda x: to_class(ResponsesReasoning, x)], self.responses_reasoning) + if self.worker_causality is not None: + result["workerCausality"] = from_union([from_none, lambda x: to_class(WorkerCausality, x)], self.worker_causality) return result @@ -12354,6 +12365,8 @@ class ToolExecutionStartData: shell_tool_info: ToolExecutionStartShellToolInfo | None = None tool_description: ToolExecutionStartToolDescription | None = None tool_title: str | None = None + traceparent: str | None = None + tracestate: str | None = None turn_id: str | None = None @staticmethod @@ -12375,6 +12388,8 @@ def from_dict(obj: Any) -> "ToolExecutionStartData": shell_tool_info = from_union([from_none, ToolExecutionStartShellToolInfo.from_dict], obj.get("shellToolInfo")) tool_description = from_union([from_none, ToolExecutionStartToolDescription.from_dict], obj.get("toolDescription")) tool_title = from_union([from_none, from_str], obj.get("toolTitle")) + traceparent = from_union([from_none, from_str], obj.get("traceparent")) + tracestate = from_union([from_none, from_str], obj.get("tracestate")) turn_id = from_union([from_none, from_str], obj.get("turnId")) return ToolExecutionStartData( tool_call_id=tool_call_id, @@ -12393,6 +12408,8 @@ def from_dict(obj: Any) -> "ToolExecutionStartData": shell_tool_info=shell_tool_info, tool_description=tool_description, tool_title=tool_title, + traceparent=traceparent, + tracestate=tracestate, turn_id=turn_id, ) @@ -12428,6 +12445,10 @@ def to_dict(self) -> dict: result["toolDescription"] = from_union([from_none, lambda x: to_class(ToolExecutionStartToolDescription, x)], self.tool_description) if self.tool_title is not None: result["toolTitle"] = from_union([from_none, from_str], self.tool_title) + if self.traceparent is not None: + result["traceparent"] = from_union([from_none, from_str], self.traceparent) + if self.tracestate is not None: + result["tracestate"] = from_union([from_none, from_str], self.tracestate) if self.turn_id is not None: result["turnId"] = from_union([from_none, from_str], self.turn_id) return result @@ -12688,6 +12709,7 @@ class UserMessageData: content: str agent_mode: UserMessageAgentMode | None = None attachments: list[Attachment] | None = None + client_correlation_id: str | None = None delivery: UserMessageDelivery | None = None interaction_id: str | None = None is_autopilot_continuation: bool | None = None @@ -12699,6 +12721,7 @@ class UserMessageData: supported_native_document_mime_types: list[str] | None = None transformed_content: str | None = None turn_id: str | None = None + worker_causality: WorkerCausality | None = None @staticmethod def from_dict(obj: Any) -> "UserMessageData": @@ -12706,6 +12729,7 @@ def from_dict(obj: Any) -> "UserMessageData": content = from_str(obj.get("content")) agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageAgentMode, x)], obj.get("agentMode")) attachments = from_union([from_none, lambda x: from_list(_load_Attachment, x)], obj.get("attachments")) + client_correlation_id = from_union([from_none, from_str], obj.get("clientCorrelationId")) delivery = from_union([from_none, lambda x: parse_enum(UserMessageDelivery, x)], obj.get("delivery")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) @@ -12717,10 +12741,12 @@ def from_dict(obj: Any) -> "UserMessageData": supported_native_document_mime_types = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("supportedNativeDocumentMimeTypes")) transformed_content = from_union([from_none, from_str], obj.get("transformedContent")) turn_id = from_union([from_none, from_str], obj.get("turnId")) + worker_causality = optional_worker_causality(obj.get("workerCausality"), lambda: from_union([from_none, WorkerCausality.from_dict], obj.get("workerCausality"))) return UserMessageData( content=content, agent_mode=agent_mode, attachments=attachments, + client_correlation_id=client_correlation_id, delivery=delivery, interaction_id=interaction_id, is_autopilot_continuation=is_autopilot_continuation, @@ -12732,6 +12758,7 @@ def from_dict(obj: Any) -> "UserMessageData": supported_native_document_mime_types=supported_native_document_mime_types, transformed_content=transformed_content, turn_id=turn_id, + worker_causality=worker_causality, ) def to_dict(self) -> dict: @@ -12741,6 +12768,8 @@ def to_dict(self) -> dict: result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageAgentMode, x)], self.agent_mode) if self.attachments is not None: result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.attachments) + if self.client_correlation_id is not None: + result["clientCorrelationId"] = from_union([from_none, from_str], self.client_correlation_id) if self.delivery is not None: result["delivery"] = from_union([from_none, lambda x: to_enum(UserMessageDelivery, x)], self.delivery) if self.interaction_id is not None: @@ -12763,6 +12792,8 @@ def to_dict(self) -> dict: result["transformedContent"] = from_union([from_none, from_str], self.transformed_content) if self.turn_id is not None: result["turnId"] = from_union([from_none, from_str], self.turn_id) + if self.worker_causality is not None: + result["workerCausality"] = from_union([from_none, lambda x: to_class(WorkerCausality, x)], self.worker_causality) return result @@ -12974,6 +13005,232 @@ def to_dict(self) -> dict: return result +@dataclass +class WorkerAdmission: + "An observed worker admission, not a claim that execution succeeded." + kind: WorkerAdmissionKind + message_id: str + ahp_turn_id: str | None = None + event: WorkerEventReference | None = None + + @staticmethod + def from_dict(obj: Any) -> "WorkerAdmission": + assert isinstance(obj, dict) + kind = parse_enum(WorkerAdmissionKind, obj.get("kind")) + message_id = from_str(obj.get("messageId")) + ahp_turn_id = from_union([from_none, from_str], obj.get("ahpTurnId")) + event = from_union([from_none, WorkerEventReference.from_dict], obj.get("event")) + return WorkerAdmission( + kind=kind, + message_id=message_id, + ahp_turn_id=ahp_turn_id, + event=event, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(WorkerAdmissionKind, self.kind) + result["messageId"] = from_str(self.message_id) + if self.ahp_turn_id is not None: + result["ahpTurnId"] = from_union([from_none, from_str], self.ahp_turn_id) + if self.event is not None: + result["event"] = from_union([from_none, lambda x: to_class(WorkerEventReference, x)], self.event) + return result + + +@dataclass +class WorkerBridgeObservation: + "One indivisible source-to-reported bridge observation, not a root alias." + reported: WorkerEventReference + source: WorkerEventReference + + @staticmethod + def from_dict(obj: Any) -> "WorkerBridgeObservation": + assert isinstance(obj, dict) + reported = WorkerEventReference.from_dict(obj.get("reported")) + source = WorkerEventReference.from_dict(obj.get("source")) + return WorkerBridgeObservation( + reported=reported, + source=source, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["reported"] = to_class(WorkerEventReference, self.reported) + result["source"] = to_class(WorkerEventReference, self.source) + return result + + +@dataclass +class WorkerCausality: + "Optional v1 worker diagnostics. The compact UTF-8 {\"workerCausality\":value}\nmust fit 4096 bytes after materializing an allowed implicit self-reference.\nIgnore invalid/unknown/oversize metadata, not the product event." + capture_complete: bool + observation_provenance: WorkerObservationProvenance + sources: list[WorkerSource] + version: int + + @staticmethod + def from_dict(obj: Any) -> "WorkerCausality": + assert isinstance(obj, dict) + capture_complete = from_bool(obj.get("captureComplete")) + observation_provenance = parse_enum(WorkerObservationProvenance, obj.get("observationProvenance")) + sources = from_list(WorkerSource.from_dict, obj.get("sources")) + version = from_int(obj.get("version")) + return WorkerCausality( + capture_complete=capture_complete, + observation_provenance=observation_provenance, + sources=sources, + version=version, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["captureComplete"] = from_bool(self.capture_complete) + result["observationProvenance"] = to_enum(WorkerObservationProvenance, self.observation_provenance) + result["sources"] = from_list(lambda x: to_class(WorkerSource, x), self.sources) + result["version"] = to_int(self.version) + return result + + +@dataclass +class WorkerEventReference: + "Exact observed event identity. No private registration generation or execution handle." + event_id: str + event_type: WorkerEventType + provenance: WorkerObservationProvenance + session_id: str + agent_id: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "WorkerEventReference": + assert isinstance(obj, dict) + event_id = from_str(obj.get("eventId")) + event_type = parse_enum(WorkerEventType, obj.get("eventType")) + provenance = parse_enum(WorkerObservationProvenance, obj.get("provenance")) + session_id = from_str(obj.get("sessionId")) + agent_id = from_union([from_none, from_str], obj.get("agentId")) + return WorkerEventReference( + event_id=event_id, + event_type=event_type, + provenance=provenance, + session_id=session_id, + agent_id=agent_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["eventId"] = from_str(self.event_id) + result["eventType"] = to_enum(WorkerEventType, self.event_type) + result["provenance"] = to_enum(WorkerObservationProvenance, self.provenance) + result["sessionId"] = from_str(self.session_id) + if self.agent_id is not None: + result["agentId"] = from_union([from_none, from_str], self.agent_id) + return result + + +@dataclass +class WorkerInput: + "Exact accepted worker input, distinct from a message, event, caller correlation or Turn." + agent_id: str + queue_item_id: str + sender: WorkerEventReference | None = None + sender_bridges: list[WorkerBridgeObservation] | None = None + + @staticmethod + def from_dict(obj: Any) -> "WorkerInput": + assert isinstance(obj, dict) + agent_id = from_str(obj.get("agentId")) + queue_item_id = from_str(obj.get("queueItemId")) + sender = from_union([from_none, WorkerEventReference.from_dict], obj.get("sender")) + sender_bridges = from_union([from_none, lambda x: from_list(WorkerBridgeObservation.from_dict, x)], obj.get("senderBridges")) + return WorkerInput( + agent_id=agent_id, + queue_item_id=queue_item_id, + sender=sender, + sender_bridges=sender_bridges, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["agentId"] = from_str(self.agent_id) + result["queueItemId"] = from_str(self.queue_item_id) + if self.sender is not None: + result["sender"] = from_union([from_none, lambda x: to_class(WorkerEventReference, x)], self.sender) + if self.sender_bridges is not None: + result["senderBridges"] = from_union([from_none, lambda x: from_list(lambda x: to_class(WorkerBridgeObservation, x), x)], self.sender_bridges) + return result + + +@dataclass +class WorkerNotificationReference: + "Exact delivery and optional occurrence of a consumed worker notification." + delivery_id: str + mode: WorkerNotificationMode + event: WorkerEventReference | None = None + + @staticmethod + def from_dict(obj: Any) -> "WorkerNotificationReference": + assert isinstance(obj, dict) + delivery_id = from_str(obj.get("deliveryId")) + mode = parse_enum(WorkerNotificationMode, obj.get("mode")) + event = from_union([from_none, WorkerEventReference.from_dict], obj.get("event")) + return WorkerNotificationReference( + delivery_id=delivery_id, + mode=mode, + event=event, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["deliveryId"] = from_str(self.delivery_id) + result["mode"] = to_enum(WorkerNotificationMode, self.mode) + if self.event is not None: + result["event"] = from_union([from_none, lambda x: to_class(WorkerEventReference, x)], self.event) + return result + + +@dataclass +class WorkerSource: + "One captured source at this placement and observation boundary." + admissions: list[WorkerAdmission] + capture_complete: bool + input: WorkerInput + admitted_during: WorkerEventReference | None = None + completion: WorkerEventReference | None = None + notification: WorkerNotificationReference | None = None + + @staticmethod + def from_dict(obj: Any) -> "WorkerSource": + assert isinstance(obj, dict) + admissions = from_list(WorkerAdmission.from_dict, obj.get("admissions")) + capture_complete = from_bool(obj.get("captureComplete")) + input = WorkerInput.from_dict(obj.get("input")) + admitted_during = from_union([from_none, WorkerEventReference.from_dict], obj.get("admittedDuring")) + completion = from_union([from_none, WorkerEventReference.from_dict], obj.get("completion")) + notification = from_union([from_none, WorkerNotificationReference.from_dict], obj.get("notification")) + return WorkerSource( + admissions=admissions, + capture_complete=capture_complete, + input=input, + admitted_during=admitted_during, + completion=completion, + notification=notification, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["admissions"] = from_list(lambda x: to_class(WorkerAdmission, x), self.admissions) + result["captureComplete"] = from_bool(self.capture_complete) + result["input"] = to_class(WorkerInput, self.input) + if self.admitted_during is not None: + result["admittedDuring"] = from_union([from_none, lambda x: to_class(WorkerEventReference, x)], self.admitted_during) + if self.completion is not None: + result["completion"] = from_union([from_none, lambda x: to_class(WorkerEventReference, x)], self.completion) + if self.notification is not None: + result["notification"] = from_union([from_none, lambda x: to_class(WorkerNotificationReference, x)], self.notification) + return result + + @dataclass class WorkflowPermissionPhase: "A declared phase shown in a workflow permission prompt." @@ -14627,6 +14884,44 @@ class Verbosity(Enum): HIGH = "high" +class WorkerAdmissionKind(Enum): + "Why this exact worker admission was made." + # A queued worker input was admitted. + QUEUED_INPUT = "queued_input" + # A system continuation was admitted. + SYSTEM_CONTINUATION = "system_continuation" + + +class WorkerEventType(Enum): + "Supported observed occurrences. Chronological parentId is not a causal reference." + # Observed tool execution start occurrence. + TOOL_EXECUTION_START = "tool.execution_start" + # Observed admitted user message occurrence. + USER_MESSAGE = "user.message" + # Observed worker completion occurrence. + SUBAGENT_COMPLETED = "subagent.completed" + # Observed worker notification occurrence. + SYSTEM_NOTIFICATION = "system.notification" + # Observed assistant turn start occurrence. + ASSISTANT_TURN_START = "assistant.turn_start" + + +class WorkerNotificationMode(Enum): + "How the owned notification was consumed." + # The notification was consumed through the queued input path. + QUEUED = "queued" + # The notification was consumed during an already-open iteration. + IMMEDIATE = "immediate" + + +class WorkerObservationProvenance(Enum): + "Producer of an observation, not the execution location of every referenced source." + # Observed by the native runtime. + NATIVE = "native" + # Observed by the AHP coordinator. + AHP_COORDINATOR = "ahp_coordinator" + + class WorkflowPermissionOperation(Enum): "Operation gated by a workflow permission request." # Running a registered workflow, which spends subagents, active time, and AI credits under the approved limits. @@ -15327,6 +15622,17 @@ def session_event_to_dict(x: SessionEvent) -> Any: "UserToolSessionApprovalWorkflow", "UserToolSessionApprovalWrite", "Verbosity", + "WorkerAdmission", + "WorkerAdmissionKind", + "WorkerBridgeObservation", + "WorkerCausality", + "WorkerEventReference", + "WorkerEventType", + "WorkerInput", + "WorkerNotificationMode", + "WorkerNotificationReference", + "WorkerObservationProvenance", + "WorkerSource", "WorkflowPermissionOperation", "WorkflowPermissionPhase", "WorkflowRunSettledData", diff --git a/python/copilot/session.py b/python/copilot/session.py index 9610c8ea21..4dbfacddde 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -104,6 +104,15 @@ logger = logging.getLogger(__name__) TResponse = TypeVar("TResponse", bound=BaseModel) + +class _AdmissionCorrelationKwargs(TypedDict, total=False): + client_correlation_id: str + + +def _admission_correlation_kwargs(value: str | None) -> _AdmissionCorrelationKwargs: + return {"client_correlation_id": value} if value is not None else {} + + # Fixed name of the runtime's built-in tool-search tool. A client can replace # its behavior by registering a tool with this exact name and # ``overrides_built_in_tool=True``. @@ -1767,6 +1776,7 @@ async def send( request_headers: dict[str, str] | None = None, display_prompt: str | None = None, response_schema: dict[str, Any] | type[BaseModel] | None = None, + client_correlation_id: str | None = None, ) -> str: """ Send a message to this session. @@ -1791,6 +1801,9 @@ async def send( ``prompt``. response_schema: JSON Schema or a Pydantic model for this run. Independent sends do not inherit it. Immediate steering cannot specify a schema. + client_correlation_id: Optional caller-owned diagnostic UUID for this RPC + admission, sent unchanged. Supported runtimes echo accepted values only + with RUNTIME_ADMISSION_TRACE_CONTEXT enabled. Not an idempotency key. Returns: The message ID assigned by the server, which can be used to correlate events. @@ -1808,6 +1821,8 @@ async def send( "sessionId": self.session_id, "prompt": prompt, } + if client_correlation_id is not None: + params["clientCorrelationId"] = client_correlation_id if attachments is not None: params["attachments"] = attachments if source is not None: @@ -1859,6 +1874,7 @@ async def send_and_wait( display_prompt: str | None = None, response_schema: dict[str, Any] | type[BaseModel] | None = None, timeout: float = 60.0, + client_correlation_id: str | None = None, ) -> SessionEvent | None: """ Send a message to this session and wait until the session becomes idle. @@ -1891,6 +1907,8 @@ async def send_and_wait( does not abort in-flight agent work. response_schema: A per-run schema. Waits for the last correlated root assistant message without tool requests at non-autopilot idle. + client_correlation_id: Optional caller-owned admission UUID, forwarded + unchanged to :meth:`send`. The SDK never generates this value. Returns: The final assistant message event, or None if none was received. @@ -1918,6 +1936,7 @@ async def send_and_wait( request_headers=request_headers, display_prompt=display_prompt, response_schema=response_schema, + **_admission_correlation_kwargs(client_correlation_id), ), timeout, ) @@ -1966,6 +1985,7 @@ def handler(event: SessionEventTypeAlias) -> None: agent_mode=agent_mode, request_headers=request_headers, display_prompt=display_prompt, + **_admission_correlation_kwargs(client_correlation_id), ) await asyncio.wait_for(idle_event.wait(), timeout=timeout) if error_event: @@ -2013,6 +2033,7 @@ async def send_and_wait_typed( request_headers: dict[str, str] | None = None, display_prompt: str | None = None, timeout: float = 60.0, + client_correlation_id: str | None = None, ) -> TResponse: """Infer a schema using Pydantic, then validate and return the final result. @@ -2022,6 +2043,8 @@ async def send_and_wait_typed( nested models, regardless of model-level alias validation settings. Streaming events remain text. Timeout or cancellation only stops waiting, not the agent. Errors remain session-scoped. + ``client_correlation_id`` is optional caller-owned admission metadata, + forwarded unchanged to :meth:`send`. """ if not isinstance(response_type, type) or not issubclass(response_type, BaseModel): raise TypeError("response_type must be a Pydantic BaseModel subclass") @@ -2039,6 +2062,7 @@ async def send_and_wait_typed( display_prompt=display_prompt, response_schema=response_type, timeout=timeout, + **_admission_correlation_kwargs(client_correlation_id), ) assert response is not None and isinstance(response.data, AssistantMessageData) return response_type.model_validate_json( diff --git a/python/test_message_identity_generated.py b/python/test_message_identity_generated.py index 024223ac96..62b50482ad 100644 --- a/python/test_message_identity_generated.py +++ b/python/test_message_identity_generated.py @@ -1,5 +1,53 @@ -from copilot.generated.rpc import QueuePendingItems -from copilot.generated.session_events import UserMessageData +import json + +import pytest + +from copilot.generated.rpc import QueuePendingItems, SendMessageItem, SendRequest +from copilot.generated.session_events import ToolExecutionStartData, UserMessageData + + +@pytest.mark.parametrize( + ("projection", "base"), + [ + (SendRequest, {"prompt": "hello"}), + (SendMessageItem, {"prompt": "hello"}), + ( + QueuePendingItems, + { + "id": "queue-1", + "messageId": "canonical-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive", + }, + ), + ( + UserMessageData, + {"content": "hello", "messageId": "canonical-1", "interactionId": "agent-loop-1"}, + ), + ], +) +@pytest.mark.parametrize( + "value", + [ + None, + "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", + "not-a-uuid", + "", + ], +) +def test_admission_correlation_optional_round_trip(projection, base, value): + expected = dict(base) + if value is not None: + expected["clientCorrelationId"] = value + decoded = projection.from_dict(json.loads(json.dumps(expected))) + assert decoded.client_correlation_id == value + assert json.loads(json.dumps(decoded.to_dict())) == expected + future = projection.from_dict({**expected, "futureField": {"enabled": True}}) + assert future.to_dict() == expected + explicit_null = projection.from_dict({**base, "clientCorrelationId": None}) + assert explicit_null.to_dict() == base def test_queue_pending_message_id_uses_camel_case_and_is_optional(): @@ -39,3 +87,22 @@ def test_user_message_id_uses_camel_case_and_is_optional(): assert older_message.message_id is None assert "messageId" not in older_message.to_dict() + + +def test_tool_start_trace_context_is_optional_and_preserved(): + for context in [ + {}, + { + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "tracestate": "vendor=value", + }, + {"traceparent": "00-11111111111111111111111111111111-2222222222222222-00"}, + {"traceparent": "invalid", "tracestate": "invalid"}, + {"traceparent": ""}, + ]: + wire = {"toolCallId": "tool-call-a", "toolName": "client-tool", **context} + data = ToolExecutionStartData.from_dict(wire) + + assert data.traceparent == context.get("traceparent") + assert data.tracestate == context.get("tracestate") + assert data.to_dict() == wire diff --git a/python/test_session.py b/python/test_session.py index d194d68c11..9dab6e348e 100644 --- a/python/test_session.py +++ b/python/test_session.py @@ -10,6 +10,7 @@ from _session_test_helpers import get_next_event_of_type, wait_for_event from copilot import AgentMessageSource, MessageSource +from copilot.rpc import SendMessageItem, SendMessagesRequest, SendRequest from copilot.session import Attachment, CopilotSession from copilot.session_events import ( AssistantMessageData, @@ -36,6 +37,87 @@ ] +@pytest.mark.parametrize( + "value", + [ + None, + "01234567-89ab-4cde-8f01-23456789abcd", + "01234567-89AB-4CDE-8F01-23456789ABCD", + "not-a-uuid", + "", + ], +) +@pytest.mark.asyncio +async def test_admission_correlation_preserves_send_and_wait_options(value, monkeypatch): + trace = { + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "tracestate": "vendor=preserved", + } + monkeypatch.setattr("copilot.session.get_trace_context", lambda: trace) + client = Mock() + session = CopilotSession("session-1", client) + expected = { + "sessionId": "session-1", + "prompt": "hello", + "source": "system", + "displayPrompt": "display", + "requestHeaders": {"X-Test": "preserved"}, + **trace, + } + if value is not None: + expected["clientCorrelationId"] = value + + async def respond(method, params, **_kwargs): + assert method == "session.send" + assert params == expected + session._dispatch_event(_event(SessionIdleData(), SessionEventType.SESSION_IDLE)) + return {"messageId": "canonical-1", "futureField": True} + + client.request = AsyncMock(side_effect=respond) + options = { + "source": "system", + "display_prompt": "display", + "request_headers": {"X-Test": "preserved"}, + "client_correlation_id": value, + } + assert await session.send("hello", **options) == "canonical-1" + assert await session.send_and_wait("hello", **options, timeout=1) is None + request = SendRequest.from_dict( + {key: val for key, val in expected.items() if key != "sessionId"} + ) + assert (await session.rpc.send(request)).message_id == "canonical-1" + + +@pytest.mark.asyncio +async def test_admission_correlation_is_per_item_not_per_batch(): + first = "01234567-89ab-4cde-8f01-23456789abcd" + second = "abcdef01-2345-4678-9abc-def012345678" + client = Mock() + client.request = AsyncMock(return_value={"messageIds": ["context-id", "plain-id", "reused-id"]}) + session = CopilotSession("session-1", client) + result = await session.rpc.send_messages( + SendMessagesRequest( + messages=[ + SendMessageItem(prompt="context", client_correlation_id=second), + SendMessageItem(prompt="plain"), + SendMessageItem(prompt="reused", client_correlation_id=first), + ] + ) + ) + assert result.message_ids == ["context-id", "plain-id", "reused-id"] + client.request.assert_awaited_once_with( + "session.sendMessages", + { + "sessionId": "session-1", + "messages": [ + {"prompt": "context", "clientCorrelationId": second}, + {"prompt": "plain"}, + {"prompt": "reused", "clientCorrelationId": first}, + ], + }, + ) + + @pytest.mark.parametrize("agent_id", [None, 42, False, b"reviewer"]) def test_agent_message_source_rejects_non_string_ids(agent_id): with pytest.raises(TypeError, match="agent_id must be a string"): diff --git a/python/test_structured_output.py b/python/test_structured_output.py index 2b626bc6bf..0ada60d1cc 100644 --- a/python/test_structured_output.py +++ b/python/test_structured_output.py @@ -67,11 +67,16 @@ def assert_clean(session): @pytest.mark.asyncio -async def test_typed_output_buffers_pre_ack_events_and_infers_schema(): +@pytest.mark.parametrize("correlation", [None, "01234567-89ab-4cde-8f01-23456789abcd"]) +async def test_typed_output_buffers_pre_ack_events_and_infers_schema(correlation): session, client = fake_session([user(), assistant(), idle()]) - result = await session.send_and_wait_typed("inventory", Inventory, timeout=1) + result = await session.send_and_wait_typed( + "inventory", Inventory, timeout=1, client_correlation_id=correlation + ) assert result == Inventory(count=42, color="red") params = client.request.call_args.args[1] + assert params.get("clientCorrelationId") == correlation + assert ("clientCorrelationId" in params) == (correlation is not None) assert params["responseFormat"] == { "type": "json_schema", "jsonSchema": {"name": "response", "strict": True, "schema": Inventory.model_json_schema()}, diff --git a/python/test_worker_causality.py b/python/test_worker_causality.py new file mode 100644 index 0000000000..e9b119991c --- /dev/null +++ b/python/test_worker_causality.py @@ -0,0 +1,108 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. + +import copy +import json +from pathlib import Path + +import pytest + +from copilot.rpc import TasksSendMessageResult +from copilot.session_events import session_event_from_dict, session_event_to_dict + +CORPUS = json.loads((Path(__file__).parent.parent / "test/worker-causality.json").read_text()) + + +def decode(value, event): + if event: + return session_event_to_dict(session_event_from_dict(value)) + return TasksSendMessageResult.from_dict(value).to_dict() + + +def payload(value, event): + return value["data"] if event else value + + +@pytest.mark.parametrize("case", CORPUS["valid"], ids=lambda case: case["name"]) +def test_worker_causality_public_readers_preserve_source_and_product(case): + event = "event" in case + wire = case["event"] if event else case["result"] + original = copy.deepcopy(wire) + assert ( + payload(decode(wire, event), event)["workerCausality"] + == payload(wire, event)["workerCausality"] + ) + assert wire == original + + legacy = copy.deepcopy(wire) + del payload(legacy, event)["workerCausality"] + baseline = json.dumps(decode(legacy, event), sort_keys=True) + for invalid in CORPUS["invalid"]: + candidate = copy.deepcopy(legacy) + payload(candidate, event)["workerCausality"] = invalid["value"] + assert json.dumps(decode(candidate, event), sort_keys=True) == baseline, invalid["name"] + + +@pytest.mark.parametrize("case", CORPUS["boundaries"], ids=lambda case: case["name"]) +def test_worker_causality_exact_utf8_budget_and_unknown_fields(case): + wire = copy.deepcopy(CORPUS["valid"][0]["event"]) + wire["data"]["workerCausality"] = case["value"] + result = decode(wire, True) + assert ("workerCausality" in result["data"]) == case["accepted"] + assert result["data"]["content"] == wire["data"]["content"] + + +def test_canonical_workflow_completion_decodes_typed_fields(): + for fixture in (CORPUS["workflowCompleted"], CORPUS["legacyWorkflowCompleted"]): + event = session_event_from_dict(fixture["event"]) + assert event.type.value == "system.notification" + assert event.data.kind.type == "workflow_completed" + assert event.data.kind.workflow_name == "fix-ci" + assert event.data.kind.run_id == "run-1" + assert event.data.kind.status.value == "completed" + assert event.data.kind.consumed_subagents == 1 + + +@pytest.mark.parametrize("case", CORPUS["invalidRaw"], ids=lambda case: case["name"]) +def test_invalid_unicode_scalar_identities_are_unavailable(case): + result = TasksSendMessageResult.from_dict( + {"sent": True, "workerCausality": json.loads(case["json"])} + ) + assert result.to_dict() == {"sent": True} + + +def test_uppercase_worker_uuid_identities_round_trip_exactly(): + value = { + "sent": True, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "ABCDEFAB-CDEF-4ABC-8ABC-ABCDEFABCDEF", + "agentId": "worker", + "sender": { + "sessionId": "session-uppercase", + "eventId": "FEDCBAFE-DCBA-4FED-8FED-FEDCBAFEDCBA", + "eventType": "tool.execution_start", + "provenance": "native", + }, + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "message-uppercase", + "ahpTurnId": "AAAABBBB-CCCC-4DDD-8EEE-FFFFAAAABBBB", + } + ], + "captureComplete": True, + "notification": { + "deliveryId": "BBBBCCCC-DDDD-4EEE-8FFF-AAAABBBBCCCC", + "mode": "queued", + }, + } + ], + "captureComplete": True, + }, + } + assert TasksSendMessageResult.from_dict(value).to_dict() == value diff --git a/rust/Cargo.toml b/rust/Cargo.toml index da53a0f071..8a530d40d2 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -48,7 +48,7 @@ async-trait = "0.1" indexmap = { version = "2", features = ["serde"] } schemars = { version = "1", optional = true } serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = { version = "1", features = ["raw_value"] } tokio = { version = "1", features = ["io-util", "sync", "rt", "process", "net", "time", "macros"] } tokio-stream = { version = "0.1", features = ["sync"] } tokio-util = { version = "0.7", default-features = false } @@ -132,7 +132,7 @@ required-features = ["test-support"] dirs = "5" flate2 = "1" native-tls = "0.2" -serde_json = "1" +serde_json = { version = "1", features = ["raw_value"] } # 0.11 adds a second digest/block-buffer stack beside the SDK's transitive 0.10. sha2 = "0.10" tar = "0.4" diff --git a/rust/README.md b/rust/README.md index 705162b71a..0d7e424ca2 100644 --- a/rust/README.md +++ b/rust/README.md @@ -992,6 +992,14 @@ session.rpc().send(request).await?; Both paths use ordinary `session.send`. Source does not select a delivery mode or set billing flags; the runtime applies its existing source behavior. `send_and_wait` still completes on `session.idle` and may return `Ok(None)` when no assistant message was emitted. Genuine errors still propagate. +For optional, caller-owned RPC admission metadata, use +`MessageOptions::with_client_correlation_id` or the generated send request's +`client_correlation_id` field. Batch requests carry it per `SendMessageItem`, +never on the batch. The SDK does not generate or validate UUIDs and omits unset +values. See [RPC admission correlation](../docs/features/execution-identity.md#optional-rpc-admission-correlation) +for runtime/release prerequisites, default-off echo behavior, and the public +raw-call seam available to existing Rust SDK 1.0.14 callers. + ### Progress Reporting (`send_and_wait`) For fire-and-forget messaging where you need to block until the agent finishes: diff --git a/rust/bazel/crates/BUILD.serde_json-1.0.151.bazel b/rust/bazel/crates/BUILD.serde_json-1.0.151.bazel index e02e3ef934..84754570fe 100644 --- a/rust/bazel/crates/BUILD.serde_json-1.0.151.bazel +++ b/rust/bazel/crates/BUILD.serde_json-1.0.151.bazel @@ -41,6 +41,7 @@ rust_library( crate_features = [ "alloc", "default", + "raw_value", "std", ], crate_root = "src/lib.rs", @@ -102,6 +103,7 @@ cargo_build_script( crate_features = [ "alloc", "default", + "raw_value", "std", ], crate_name = "build_script_build", diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 4868d32b76..711a352b2e 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -16,7 +16,7 @@ pub use super::session_events::{ OmittedBinaryOmittedReason, PermissionDecisionSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskBlocker, TaskCompletionOutcome, UserToolSessionApproval, - Verbosity, + Verbosity, WorkerCausality, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -17274,6 +17274,9 @@ pub struct QueueMoveItemResult { pub struct QueuePendingItems { /// Agent mode stored on this queued entry, as stamped when it was enqueued. Items without an explicit mode report interactive. This is not necessarily the mode that will constrain the turn: a plan or autopilot session applies its own write gate, continuation loop and permission posture to every drained item regardless of the mode stored here. pub agent_mode: SendAgentMode, + /// Caller-owned diagnostic UUID from the exact accepted native session.send or sendMessages item, when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted for unsupported or identity-less rows, including snapshot-only mirrors. Not an idempotency key, authorization, or permission to retry; repeated values remain ambiguous. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_correlation_id: Option, /// Human-readable text to display for this queue entry in the UI pub display_text: String, /// Stable opaque id for the canonical queued item. Batch rows share one id. @@ -18502,6 +18505,9 @@ pub struct SendMessageItem { #[doc(hidden)] #[serde(skip_serializing_if = "Option::is_none")] pub(crate) billable: Option, + /// Optional caller-generated diagnostic UUID for this item only, with the same validation and opt-in native echo as session.send.clientCorrelationId. The batch has no request-level correlation value; each item retains its own value, including preceding context messages. Reused values do not deduplicate messages and remain ambiguous. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_correlation_id: Option, /// If provided, this is shown in the timeline instead of `prompt` #[serde(skip_serializing_if = "Option::is_none")] pub display_prompt: Option, @@ -18608,6 +18614,9 @@ pub struct SendRequest { /// If false, this message will not trigger a Premium Request Unit charge. User messages default to billable. #[serde(skip_serializing_if = "Option::is_none")] pub billable: Option, + /// Optional caller-generated diagnostic UUID for this single message. Native sessions with RUNTIME_ADMISSION_TRACE_CONTEXT enabled echo the exact lowercase, hyphenated 36-character UUID on user.message and its existing pending message row. Missing, invalid, disabled, or unsupported metadata is ignored without rejecting the send. Does not change messageId, deduplicate submissions, authorize work, or make an uncertain retry safe. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_correlation_id: Option, /// If provided, this is shown in the timeline instead of `prompt` #[serde(skip_serializing_if = "Option::is_none")] pub display_prompt: Option, @@ -23894,6 +23903,13 @@ pub struct TasksSendMessageResult { pub error: Option, /// Whether the message was successfully delivered or steered pub sent: bool, + /// Optional exact queue admission receipt on sent=true only. No implicit event or execution-success claim. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "crate::worker_causality::deserialize_optional_raw" + )] + pub worker_causality: Option, } /// Agent type, prompt, name, and optional description and model override for the new task. @@ -29946,6 +29962,13 @@ pub struct SessionTasksSendMessageResult { pub error: Option, /// Whether the message was successfully delivered or steered pub sent: bool, + /// Optional exact queue admission receipt on sent=true only. No implicit event or execution-success claim. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "crate::worker_causality::deserialize_optional_raw" + )] + pub worker_causality: Option, } /// Identifies the target session. @@ -40786,7 +40809,7 @@ pub enum ResponseFormatType { JsonSchema, } -/// Origin of the sandbox choice supplied by the host. This value describes preference or session intent; it does not authorize bypassing managed policy. +/// Origin of the sandbox choice supplied by the host. Settings-derived origins let managed policy floor the host preference; do not tag explicit session overrides as settings-derived. /// ///
/// @@ -40805,7 +40828,7 @@ pub enum SandboxConfigSource { /// The user's persisted settings disabled the sandbox. #[serde(rename = "user_disabled")] UserDisabled, - /// An explicit session-scoped choice selected the sandbox state, such as a command-line flag. + /// A command-line flag selected the sandbox state for this session. #[serde(rename = "session_flag")] SessionFlag, /// The user disabled the sandbox for the current session. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index c8b498bcbc..a7a3dcaecb 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -2669,6 +2669,116 @@ pub struct SessionPermissionRecoveryData { pub status: PermissionRecoveryStatus, } +/// Exact observed event identity. No private registration generation or execution handle. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerEventReference { + /// Actual event agent scope, absent for a root occurrence; at most 256 UTF-8 bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_id: Option, + /// Actual event occurrence UUID; copied without normalization. + pub event_id: String, + /// Type of the observed occurrence. + pub event_type: WorkerEventType, + /// Explicit provenance of this reference. + pub provenance: WorkerObservationProvenance, + /// Actual runtime session scope, at most 256 UTF-8 bytes. + pub session_id: SessionId, +} + +/// An observed worker admission, not a claim that execution succeeded. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerAdmission { + /// Actual AHP participant Turn UUID, not a native turn counter or provenance signal. + #[serde(skip_serializing_if = "Option::is_none")] + pub ahp_turn_id: Option, + /// May be omitted only for the matching current worker user.message. + #[serde(skip_serializing_if = "Option::is_none")] + pub event: Option, + /// The producer's admission kind. + pub kind: WorkerAdmissionKind, + /// Canonical logical message identity, independent of queueItemId; at most 256 UTF-8 bytes. + pub message_id: String, +} + +/// One indivisible source-to-reported bridge observation, not a root alias. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerBridgeObservation { + /// Full occurrence actually emitted by that bridge. + pub reported: WorkerEventReference, + /// Full event received by this bridge. + pub source: WorkerEventReference, +} + +/// Exact accepted worker input, distinct from a message, event, caller correlation or Turn. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerInput { + /// Actual recipient task, at most 256 UTF-8 bytes. + pub agent_id: String, + /// UUID allocated for this queue item by the admitting producer. + pub queue_item_id: String, + /// Original invoking occurrence, never replaced by a reported/root alias. + #[serde(skip_serializing_if = "Option::is_none")] + pub sender: Option, + /// Exact captured edges in producer order. Requires sender; at most 32 whole pairs. + #[serde(skip_serializing_if = "Option::is_none")] + pub sender_bridges: Option>, +} + +/// Exact delivery and optional occurrence of a consumed worker notification. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerNotificationReference { + /// Actual notificationDeliveryId UUID. + pub delivery_id: String, + /// May be omitted only on the matching current system.notification. + #[serde(skip_serializing_if = "Option::is_none")] + pub event: Option, + /// Actual consumption mode. + pub mode: WorkerNotificationMode, +} + +/// One captured source at this placement and observation boundary. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerSource { + /// Actual admissions in capture order; at most 32. + pub admissions: Vec, + /// Exact already-open iteration when an immediate notification was consumed. + #[serde(skip_serializing_if = "Option::is_none")] + pub admitted_during: Option, + /// Applicable observations were all captured here, never work completion or success. + pub capture_complete: bool, + /// Actual completion emission only. Absence is not an execution outcome. + #[serde(skip_serializing_if = "Option::is_none")] + pub completion: Option, + /// Indivisible accepted input identity. + pub input: WorkerInput, + /// Actual consumed notification, when observed. + #[serde(skip_serializing_if = "Option::is_none")] + pub notification: Option, +} + +/// Optional v1 worker diagnostics. The compact UTF-8 {"workerCausality":value} +/// must fit 4096 bytes after materializing an allowed implicit self-reference. +/// Ignore invalid/unknown/oversize metadata, not the product event. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorkerCausality { + /// Complete placement-aware observed capture, not global causality or execution success. + /// Empty true requires explicit native invocation attestation. + pub capture_complete: bool, + /// Provenance of this enclosing observation; never inferred from other fields. + pub observation_provenance: WorkerObservationProvenance, + /// Sources in capture order, at most 32. Absent/unknown is not known-empty. + pub sources: Vec, + /// Supported version, exactly 1. + pub version: serde_json::Value, +} + /// Session event "user.message". Payload of `user.message` with displayed and model-transformed content, attachments, source/delivery metadata, mode, and telemetry IDs. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -2679,6 +2789,9 @@ pub struct UserMessageData { /// Files, selections, or GitHub references attached to the message #[serde(skip_serializing_if = "Option::is_none")] pub attachments: Option>, + /// Exact caller-owned diagnostic UUID carried by this accepted native session.send or sendMessages item when RUNTIME_ADMISSION_TRACE_CONTEXT is enabled. Omitted when input, native ownership, or support is missing. Independent of the canonical messageId; not an idempotency key, authorization, or permission to retry. Multiple messages with the same value remain ambiguous. + #[serde(skip_serializing_if = "Option::is_none")] + pub client_correlation_id: Option, /// The user's message text as displayed in the timeline pub content: String, /// How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. @@ -2696,7 +2809,7 @@ pub struct UserMessageData { /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit #[serde(skip_serializing_if = "Option::is_none")] pub native_document_path_fallback_paths: Option>, - /// Parent agent task ID for background telemetry correlated to this user turn + /// Task ID minted when the runtime prepares this user-message run. This is not a parent interaction ID or worker instance ID and must not be equated with CAPI's X-Parent-Agent-Id. #[serde(skip_serializing_if = "Option::is_none")] pub parent_agent_task_id: Option, /// Provider reasoning settings anchored before this model-facing message for cache-stable replay; the historical responsesReasoning name is retained for compatibility @@ -2714,6 +2827,13 @@ pub struct UserMessageData { /// The agent-loop turn ID that consumed this message; absent when no agent-loop turn consumed it #[serde(skip_serializing_if = "Option::is_none")] pub turn_id: Option, + /// Optional worker admission observations; self identity requires the matching enclosing message and agent. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "crate::worker_causality::deserialize_optional" + )] + pub worker_causality: Option, } /// Session event "pending_messages.modified". Empty payload; the event signals that the pending message queue has changed @@ -2736,6 +2856,13 @@ pub struct AssistantTurnStartData { pub parent_tool_call_id: Option, /// Identifier for this turn within the agentic loop, typically a stringified turn number pub turn_id: String, + /// Optional bounded worker observations. Missing or invalid metadata is unavailable, not known-empty. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "crate::worker_causality::deserialize_optional" + )] + pub worker_causality: Option, } /// Session event "assistant.turn_retry". Metadata for an additional model inference attempt within an existing assistant turn @@ -4058,6 +4185,12 @@ pub struct ToolExecutionStartData { /// Human-readable display title for the tool, when the selected tool descriptor has a non-empty title. #[serde(skip_serializing_if = "Option::is_none")] pub tool_title: Option, + /// W3C traceparent of this tool's active runtime execute_tool span. Available on live events when tool-context propagation is enabled; absent when the span is unavailable or on persisted history. This diagnostic context does not authorize execution. + #[serde(skip_serializing_if = "Option::is_none")] + pub traceparent: Option, + /// Optional W3C tracestate associated with traceparent. Omitted when no valid vendor state is available. + #[serde(skip_serializing_if = "Option::is_none")] + pub tracestate: Option, /// Identifier for the agent loop turn this tool was invoked in, matching the corresponding assistant.turn_start event #[serde(skip_serializing_if = "Option::is_none")] pub turn_id: Option, @@ -5038,7 +5171,7 @@ pub struct SubagentStartedData { /// Where the model input for this sub-agent came from. Present when the task planner resolved the launch (the task tool and workflow agents); absent for sub-agents created through other runtime paths. #[serde(skip_serializing_if = "Option::is_none")] pub task_model_source: Option, - /// Tool call ID of the parent tool invocation that spawned this sub-agent + /// Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. pub tool_call_id: String, /// Root id of the workflow run that spawned this sub-agent, when it was spawned by one. #[serde(skip_serializing_if = "Option::is_none")] @@ -5099,7 +5232,7 @@ pub struct SubagentCompletedData { /// Authority or runtime mechanism responsible for sub-agent model selection #[serde(skip_serializing_if = "Option::is_none")] pub model_selection_source: Option, - /// Tool call ID of the parent tool invocation that spawned this sub-agent + /// Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. pub tool_call_id: String, /// Total tokens (input + output) consumed by the sub-agent #[serde(skip_serializing_if = "Option::is_none")] @@ -5146,7 +5279,7 @@ pub struct SubagentFailedData { /// Authority or runtime mechanism responsible for sub-agent model selection #[serde(skip_serializing_if = "Option::is_none")] pub model_selection_source: Option, - /// Tool call ID of the parent tool invocation that spawned this sub-agent + /// Spawning tool invocation ID, or the canonical sub-agent ID used as a fallback for an API launch without a tool invocation. The fallback is not evidence of a tool call. pub tool_call_id: String, /// Total tokens (input + output) consumed before the sub-agent failed #[serde(skip_serializing_if = "Option::is_none")] @@ -5305,17 +5438,172 @@ pub struct SystemMessageData { pub role: SystemMessageRole, } -/// Session event "system.notification". System-generated notification for runtime events like background task completion +/// System notification metadata for a background agent that completed or failed, including agent ID, type, status, description, and prompt. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +pub struct SystemNotificationAgentCompleted { + /// Unique task identifier + pub agent_id: String, + /// Type of the agent (e.g., explore, task, general-purpose) + pub agent_type: String, + /// Human-readable description of the agent task + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Friendly, non-unique name intended for display + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// The full prompt given to the background agent + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt: Option, + /// Whether the agent completed successfully or failed + pub status: SystemNotificationAgentCompletedStatus, + /// Type discriminator. Always "agent_completed". + pub r#type: SystemNotificationAgentCompletedType, +} + +/// System notification metadata for a background agent that became idle, including agent ID, type, and description. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationAgentIdle { + /// Unique task identifier + pub agent_id: String, + /// Type of the agent (e.g., explore, task, general-purpose) + pub agent_type: String, + /// Human-readable description of the agent task + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Friendly, non-unique name intended for display + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Type discriminator. Always "agent_idle". + pub r#type: SystemNotificationAgentIdleType, +} + +/// System notification metadata for a new inbox message, including entry ID, sender details, and summary. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationNewInboxMessage { + /// Unique identifier of the inbox entry + pub entry_id: String, + /// Human-readable name of the sender + pub sender_name: String, + /// Category of the sender (e.g., sidekick-agent, plugin, hook) + pub sender_type: String, + /// Short summary shown before the agent decides whether to read the inbox + pub summary: String, + /// Type discriminator. Always "new_inbox_message". + pub r#type: SystemNotificationNewInboxMessageType, +} + +/// System notification metadata for a shell session that completed, including shell ID, optional exit code, and description. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationShellCompleted { + /// Human-readable description of the command + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Exit code of the shell command, if available + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + /// Unique identifier of the shell session + pub shell_id: String, + /// Type discriminator. Always "shell_completed". + pub r#type: SystemNotificationShellCompletedType, +} + +/// System notification metadata for a detached shell session that completed, including shell ID and description. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationShellDetachedCompleted { + /// Human-readable description of the command + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Unique identifier of the detached shell session + pub shell_id: String, + /// Type discriminator. Always "shell_detached_completed". + pub r#type: SystemNotificationShellDetachedCompletedType, +} + +/// System notification metadata for an instruction file discovered during tool access, including source, trigger file, and tool. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationInstructionDiscovered { + /// Human-readable label for the timeline (e.g., 'AGENTS.md from packages/billing/') + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + /// Relative path to the discovered instruction file + pub source_path: String, + /// Path of the file access that triggered discovery + pub trigger_file: String, + /// Tool command that triggered discovery (currently always 'view') + pub trigger_tool: String, + /// Type discriminator. Always "instruction_discovered". + pub r#type: SystemNotificationInstructionDiscoveredType, +} + +/// System notification metadata for a workflow execution attempt that reached a terminal state. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationWorkflowCompleted { + /// Execution attempt that reached this terminal state. + pub attempt: i64, + /// Consumed AI usage in nano-AIU. + pub consumed_nano_aiu: i64, + /// Subagents consumed by the run across all attempts. + pub consumed_subagents: i64, + /// Accumulated active execution time in milliseconds. + pub elapsed_ms: i64, + /// Machine-readable terminal failure details, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub failure: Option, + /// Pause initiator metadata when this attempt settled as paused. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_info: Option, + /// Bounded prompt-safe preview of the completed result. + #[serde(skip_serializing_if = "Option::is_none")] + pub result_preview: Option, + /// Actionable run_dynamic_workflow resume guidance for a resource-limit failure. + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_guidance: Option, + /// Workflow run identifier. + pub run_id: String, + /// Terminal status reached by this execution attempt. + pub status: SystemNotificationWorkflowCompletedStatus, + /// Type discriminator. Always "workflow_completed". + pub r#type: SystemNotificationWorkflowCompletedType, + /// Persisted workflow name. + pub workflow_name: String, +} + +/// System notification metadata from an external host that does not match a runtime-owned notification kind. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SystemNotificationUnclassified { + /// Opaque metadata supplied by the external host, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + /// Type discriminator. Always "unclassified". + pub r#type: SystemNotificationUnclassifiedType, +} + +/// Session event "system.notification". System-generated notification for runtime events like background task completion +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct SystemNotificationData { /// The notification text, typically wrapped in `` XML tags pub content: String, /// Structured metadata identifying what triggered this notification - pub kind: serde_json::Value, + pub kind: SystemNotification, /// Provider reasoning settings anchored before this model-facing message for cache-stable replay; the historical responsesReasoning name is retained for compatibility #[serde(skip_serializing_if = "Option::is_none")] pub responses_reasoning: Option, + /// Optional owned worker notification observations; an omitted notification event refers only to this occurrence. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + default, + deserialize_with = "crate::worker_causality::deserialize_optional" + )] + pub worker_causality: Option, } /// A parsed command identifier in a shell permission request, including whether it is read-only. @@ -8902,6 +9190,75 @@ pub enum UserMessageDelivery { Unknown, } +/// Producer of an observation, not the execution location of every referenced source. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkerObservationProvenance { + /// Observed by the native runtime. + #[serde(rename = "native")] + Native, + /// Observed by the AHP coordinator. + #[serde(rename = "ahp_coordinator")] + AhpCoordinator, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Supported observed occurrences. Chronological parentId is not a causal reference. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkerEventType { + /// Observed tool execution start occurrence. + #[serde(rename = "tool.execution_start")] + ToolExecutionStart, + /// Observed admitted user message occurrence. + #[serde(rename = "user.message")] + UserMessage, + /// Observed worker completion occurrence. + #[serde(rename = "subagent.completed")] + SubagentCompleted, + /// Observed worker notification occurrence. + #[serde(rename = "system.notification")] + SystemNotification, + /// Observed assistant turn start occurrence. + #[serde(rename = "assistant.turn_start")] + AssistantTurnStart, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Why this exact worker admission was made. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkerAdmissionKind { + /// A queued worker input was admitted. + #[serde(rename = "queued_input")] + QueuedInput, + /// A system continuation was admitted. + #[serde(rename = "system_continuation")] + SystemContinuation, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// How the owned notification was consumed. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum WorkerNotificationMode { + /// The notification was consumed through the queued input path. + #[serde(rename = "queued")] + Queued, + /// The notification was consumed during an already-open iteration. + #[serde(rename = "immediate")] + Immediate, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// What the agent was doing when the user interrupted it. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AgentInterruptedActivity { @@ -9904,6 +10261,123 @@ pub enum SystemMessageRole { Unknown, } +/// Whether the agent completed successfully or failed +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationAgentCompletedStatus { + /// The agent completed successfully. + #[serde(rename = "completed")] + Completed, + /// The agent failed. + #[serde(rename = "failed")] + Failed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Type discriminator. Always "agent_completed". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationAgentCompletedType { + #[serde(rename = "agent_completed")] + #[default] + AgentCompleted, +} + +/// Type discriminator. Always "agent_idle". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationAgentIdleType { + #[serde(rename = "agent_idle")] + #[default] + AgentIdle, +} + +/// Type discriminator. Always "new_inbox_message". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationNewInboxMessageType { + #[serde(rename = "new_inbox_message")] + #[default] + NewInboxMessage, +} + +/// Type discriminator. Always "shell_completed". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationShellCompletedType { + #[serde(rename = "shell_completed")] + #[default] + ShellCompleted, +} + +/// Type discriminator. Always "shell_detached_completed". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationShellDetachedCompletedType { + #[serde(rename = "shell_detached_completed")] + #[default] + ShellDetachedCompleted, +} + +/// Type discriminator. Always "instruction_discovered". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationInstructionDiscoveredType { + #[serde(rename = "instruction_discovered")] + #[default] + InstructionDiscovered, +} + +/// Terminal status reached by a workflow execution attempt. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationWorkflowCompletedStatus { + /// The workflow completed successfully. + #[serde(rename = "completed")] + Completed, + /// The workflow was halted. + #[serde(rename = "halted")] + Halted, + /// The workflow attempt paused intentionally. + #[serde(rename = "paused")] + Paused, + /// The workflow was cancelled. + #[serde(rename = "cancelled")] + Cancelled, + /// The workflow failed. + #[serde(rename = "error")] + Error, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Type discriminator. Always "workflow_completed". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationWorkflowCompletedType { + #[serde(rename = "workflow_completed")] + #[default] + WorkflowCompleted, +} + +/// Type discriminator. Always "unclassified". +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum SystemNotificationUnclassifiedType { + #[serde(rename = "unclassified")] + #[default] + Unclassified, +} + +/// Structured metadata identifying what triggered this notification +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum SystemNotification { + AgentCompleted(SystemNotificationAgentCompleted), + AgentIdle(SystemNotificationAgentIdle), + NewInboxMessage(SystemNotificationNewInboxMessage), + ShellCompleted(SystemNotificationShellCompleted), + ShellDetachedCompleted(SystemNotificationShellDetachedCompleted), + InstructionDiscovered(SystemNotificationInstructionDiscovered), + WorkflowCompleted(SystemNotificationWorkflowCompleted), + Unclassified(SystemNotificationUnclassified), +} + /// Permission kind discriminator #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionRequestShellKind { diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 90c9c2938d..f5e8478b3f 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -29,6 +29,7 @@ pub(crate) type InlineResponseCallback = /// Internal pairing of the response delivery channel with an optional /// inline callback that the read loop runs synchronously before delivery. struct PendingRequest { + method: String, sender: oneshot::Sender, inline_callback: Option, } @@ -250,6 +251,283 @@ fn repair_lone_surrogates(body: &[u8]) -> Option> { repaired } +#[derive(Default)] +struct RepairedWorkerPaths { + result: bool, + result_events: Vec, + notification_event: bool, +} + +struct ReadMessage { + message: JsonRpcMessage, + repaired_worker_paths: RepairedWorkerPaths, +} + +fn skip_json_string(raw: &[u8], index: &mut usize) -> bool { + if raw.get(*index) != Some(&b'"') { + return false; + } + *index += 1; + while let Some(byte) = raw.get(*index) { + match byte { + b'"' => { + *index += 1; + return true; + } + b'\\' => { + *index += if raw.get(*index + 1) == Some(&b'u') { + 6 + } else { + 2 + }; + } + _ => *index += 1, + } + } + false +} + +fn skip_json_whitespace(raw: &[u8], index: &mut usize) { + while raw + .get(*index) + .is_some_and(|byte| matches!(byte, b' ' | b'\n' | b'\r' | b'\t')) + { + *index += 1; + } +} + +fn skip_json_value(raw: &[u8], index: &mut usize) -> bool { + skip_json_whitespace(raw, index); + match raw.get(*index) { + Some(b'"') => skip_json_string(raw, index), + Some(b'{' | b'[') => { + let mut stack = vec![if raw[*index] == b'{' { b'}' } else { b']' }]; + *index += 1; + while let Some(byte) = raw.get(*index) { + match byte { + b'"' => { + if !skip_json_string(raw, index) { + return false; + } + } + b'{' => { + stack.push(b'}'); + *index += 1; + } + b'[' => { + stack.push(b']'); + *index += 1; + } + b'}' | b']' => { + if stack.pop() != Some(*byte) { + return false; + } + *index += 1; + if stack.is_empty() { + return true; + } + } + _ => *index += 1, + } + } + false + } + Some(_) => { + while raw.get(*index).is_some_and(|byte| { + !matches!(byte, b',' | b'}' | b']' | b' ' | b'\n' | b'\r' | b'\t') + }) { + *index += 1; + } + true + } + None => false, + } +} + +fn raw_ascii_string(raw: &[u8]) -> Option { + if raw.first() != Some(&b'"') || raw.last() != Some(&b'"') { + return None; + } + let mut output = String::new(); + let mut index = 1; + while index + 1 < raw.len() { + let byte = raw[index]; + if byte != b'\\' { + if !byte.is_ascii() { + return None; + } + output.push(char::from(byte)); + index += 1; + continue; + } + index += 1; + match raw.get(index)? { + b'"' | b'\\' | b'/' => output.push(char::from(raw[index])), + b'u' => { + let digits = std::str::from_utf8(raw.get(index + 1..index + 5)?).ok()?; + let value = u16::from_str_radix(digits, 16).ok()?; + if value > 0x7f { + return None; + } + output.push(char::from(value as u8)); + index += 4; + } + _ => return None, + } + index += 1; + } + Some(output) +} + +fn raw_object_property<'a>(raw: &'a [u8], property: &str) -> Option<&'a [u8]> { + let mut index = 0; + skip_json_whitespace(raw, &mut index); + if raw.get(index) != Some(&b'{') { + return None; + } + index += 1; + let mut found = None; + loop { + skip_json_whitespace(raw, &mut index); + if raw.get(index) == Some(&b'}') { + return found; + } + let key_start = index; + if !skip_json_string(raw, &mut index) { + return None; + } + let key = raw_ascii_string(&raw[key_start..index]); + skip_json_whitespace(raw, &mut index); + if raw.get(index) != Some(&b':') { + return None; + } + index += 1; + skip_json_whitespace(raw, &mut index); + let value_start = index; + if !skip_json_value(raw, &mut index) { + return None; + } + if key.as_deref() == Some(property) { + found = Some(&raw[value_start..index]); + } + skip_json_whitespace(raw, &mut index); + if raw.get(index) == Some(&b',') { + index += 1; + } else if raw.get(index) == Some(&b'}') { + return found; + } else { + return None; + } + } +} + +fn raw_array_values(raw: &[u8]) -> Option> { + let mut index = 0; + skip_json_whitespace(raw, &mut index); + if raw.get(index) != Some(&b'[') { + return None; + } + index += 1; + let mut values = Vec::new(); + loop { + skip_json_whitespace(raw, &mut index); + if raw.get(index) == Some(&b']') { + return Some(values); + } + let start = index; + if !skip_json_value(raw, &mut index) { + return None; + } + values.push(&raw[start..index]); + skip_json_whitespace(raw, &mut index); + match raw.get(index) { + Some(b',') => index += 1, + Some(b']') => return Some(values), + _ => return None, + } + } +} + +fn raw_event_worker_needs_removal(raw: &[u8]) -> bool { + let Some(event_type) = raw_object_property(raw, "type").and_then(raw_ascii_string) else { + return false; + }; + if !matches!( + event_type.as_str(), + "user.message" | "assistant.turn_start" | "system.notification" + ) { + return false; + } + let Some(data) = raw_object_property(raw, "data") else { + return false; + }; + raw_object_property(data, "workerCausality") + .is_some_and(|worker| repair_lone_surrogates(worker).is_some()) +} + +fn repaired_worker_paths(body: &[u8]) -> RepairedWorkerPaths { + let mut paths = RepairedWorkerPaths::default(); + if let Some(result) = raw_object_property(body, "result") { + paths.result = raw_object_property(result, "workerCausality") + .is_some_and(|worker| repair_lone_surrogates(worker).is_some()); + if let Some(events) = raw_object_property(result, "events").and_then(raw_array_values) { + paths.result_events = events + .iter() + .enumerate() + .filter_map(|(index, event)| raw_event_worker_needs_removal(event).then_some(index)) + .collect(); + } + } + if let Some(params) = raw_object_property(body, "params") { + paths.notification_event = + raw_object_property(params, "event").is_some_and(raw_event_worker_needs_removal); + } + paths +} + +fn remove_repaired_response_worker_causality( + response: &mut JsonRpcResponse, + paths: &RepairedWorkerPaths, + method: &str, +) { + let Some(result) = response.result.as_mut() else { + return; + }; + if method == "session.tasks.sendMessage" && paths.result { + result + .as_object_mut() + .map(|result| result.remove("workerCausality")); + } + if matches!( + method, + "session.eventLog.read" | "session.getMessages" | "sessions.readPersistedEvents" + ) && let Some(events) = result.get_mut("events").and_then(Value::as_array_mut) + { + for index in &paths.result_events { + events + .get_mut(*index) + .and_then(|event| event.get_mut("data")) + .and_then(Value::as_object_mut) + .map(|data| data.remove("workerCausality")); + } + } +} + +fn remove_repaired_notification_worker_causality( + notification: &mut JsonRpcNotification, + paths: &RepairedWorkerPaths, +) { + if paths.notification_event { + notification + .params + .as_mut() + .and_then(|params| params.get_mut("event")) + .and_then(|event| event.get_mut("data")) + .and_then(Value::as_object_mut) + .map(|data| data.remove("workerCausality")); + } +} + /// One framed JSON-RPC message handed to the writer actor. /// /// `frame` is the fully serialized bytes (header + body); the caller pays @@ -413,15 +691,24 @@ impl JsonRpcClient { loop { match Self::read_message(&mut reader).await { - Ok(Some(message)) => match message { + Ok(Some(ReadMessage { + message, + repaired_worker_paths, + })) => match message { JsonRpcMessage::Response(mut response) => { let id = response.id; let pending = pending_requests.write().remove(&id); if let Some(PendingRequest { + method, sender, inline_callback, }) = pending { + remove_repaired_response_worker_causality( + &mut response, + &repaired_worker_paths, + &method, + ); // Run the inline callback synchronously on the // read loop so any state it mutates (e.g. // registering a server-assigned session id with @@ -468,7 +755,13 @@ impl JsonRpcClient { warn!(request_id = %id, "received response for unknown request id"); } } - JsonRpcMessage::Notification(notification) => { + JsonRpcMessage::Notification(mut notification) => { + if notification.method == "session.event" { + remove_repaired_notification_worker_causality( + &mut notification, + &repaired_worker_paths, + ); + } if notification.method == "$/cancelRequest" { if let Some(id) = notification .params @@ -519,7 +812,7 @@ impl JsonRpcClient { async fn read_message( reader: &mut BufReader, - ) -> Result, Error> { + ) -> Result, Error> { let mut line = String::new(); let mut content_length = None; @@ -551,10 +844,14 @@ impl JsonRpcClient { reader.read_exact(&mut body).await?; match serde_json::from_slice::(&body) { - Ok(message) => Ok(Some(message)), + Ok(message) => Ok(Some(ReadMessage { + message, + repaired_worker_paths: RepairedWorkerPaths::default(), + })), Err(error) => { // Dropping an undecodable frame could leave its pending // request waiting forever because this layer has no timeout. + let paths = repaired_worker_paths(&body); match repair_lone_surrogates(&body) .and_then(|repaired| serde_json::from_slice::(&repaired).ok()) { @@ -564,7 +861,10 @@ impl JsonRpcClient { length, "recovered JSON-RPC frame containing unpaired UTF-16 surrogates" ); - Ok(Some(message)) + Ok(Some(ReadMessage { + message, + repaired_worker_paths: paths, + })) } None => Err(error.into()), } @@ -622,6 +922,7 @@ impl JsonRpcClient { self.pending_requests.write().insert( id, PendingRequest { + method: method.to_owned(), sender: tx, inline_callback, }, @@ -856,6 +1157,116 @@ mod tests { } } + #[tokio::test] + async fn framed_response_drops_worker_metadata_changed_by_surrogate_repair() { + async fn read_response(body: &[u8], method: &str) -> JsonRpcResponse { + let frame = format!("Content-Length: {}\r\n\r\n", body.len()); + let (mut writer, reader) = tokio::io::duplex(frame.len() + body.len()); + writer.write_all(frame.as_bytes()).await.unwrap(); + writer.write_all(body).await.unwrap(); + writer.shutdown().await.unwrap(); + let ReadMessage { + message, + repaired_worker_paths, + } = JsonRpcClient::read_message(&mut BufReader::new(reader)) + .await + .unwrap() + .unwrap(); + let JsonRpcMessage::Response(mut response) = message else { + panic!("expected response"); + }; + remove_repaired_response_worker_causality( + &mut response, + &repaired_worker_paths, + method, + ); + response + } + + async fn read_notification(body: &[u8]) -> JsonRpcNotification { + let frame = format!("Content-Length: {}\r\n\r\n", body.len()); + let (mut writer, reader) = tokio::io::duplex(frame.len() + body.len()); + writer.write_all(frame.as_bytes()).await.unwrap(); + writer.write_all(body).await.unwrap(); + writer.shutdown().await.unwrap(); + let ReadMessage { + message, + repaired_worker_paths, + } = JsonRpcClient::read_message(&mut BufReader::new(reader)) + .await + .unwrap() + .unwrap(); + let JsonRpcMessage::Notification(mut notification) = message else { + panic!("expected notification"); + }; + if notification.method == "session.event" { + remove_repaired_notification_worker_causality( + &mut notification, + &repaired_worker_paths, + ); + } + notification + } + + let body = br#"{"jsonrpc":"2.0","id":1,"result":{"sent":true,"other":"\ud800","workerCausality":{"version":1,"observationProvenance":"native","sources":[{"input":{"queueItemId":"11111111-1111-4111-8111-111111111111","agentId":"\ud800"},"admissions":[],"captureComplete":true}],"captureComplete":true}}}"#; + let response = read_response(body, "session.tasks.sendMessage").await; + let result = response.result.unwrap(); + assert_eq!(result["sent"], true); + assert_eq!(result["other"], "\u{fffd}"); + assert!(result.get("workerCausality").is_none()); + + let body = br#"{"jsonrpc":"2.0","id":2,"result":{"sent":true,"other":"\ud800","workerCausality":{"version":1,"observationProvenance":"native","sources":[{"input":{"queueItemId":"11111111-1111-4111-8111-111111111111","agentId":"\ufffd"},"admissions":[],"captureComplete":true}],"captureComplete":true}}}"#; + let response = read_response(body, "session.tasks.sendMessage").await; + assert_eq!( + response.result.unwrap()["workerCausality"]["sources"][0]["input"]["agentId"], + "\u{fffd}" + ); + + let body = br#"{"jsonrpc":"2.0","id":3,"result":{"\ud800":"extension","sent":true,"workerCausality":{"version":1,"observationProvenance":"native","sources":[{"input":{"queueItemId":"11111111-1111-4111-8111-111111111111","agentId":"\ud800"},"admissions":[],"captureComplete":true}],"captureComplete":true}}}"#; + let response = read_response(body, "session.tasks.sendMessage").await; + assert!(response.result.unwrap().get("workerCausality").is_none()); + + let body = br#"{"jsonrpc":"2.0","id":4,"result":{"events":[{"type":"session.start","data":{"workerCausality":{"identity":"\ud800"}}}]}}"#; + let response = read_response(body, "session.eventLog.read").await; + assert_eq!( + response.result.unwrap()["events"][0]["data"]["workerCausality"]["identity"], + "\u{fffd}" + ); + + let body = br#"{"jsonrpc":"2.0","id":8,"result":{"events":[{"type":"user.message","data":{"workerCausality":{"identity":"\ud800"}}}]}}"#; + let response = read_response(body, "session.getMessages").await; + assert!( + response.result.unwrap()["events"][0]["data"] + .get("workerCausality") + .is_none() + ); + + let body = br#"{"jsonrpc":"2.0","id":5,"result":{"sent":true,"workerCausality":{"identity":"\ud800"}}}"#; + let response = read_response(body, "unrelated.method").await; + assert_eq!( + response.result.unwrap()["workerCausality"]["identity"], + "\u{fffd}" + ); + + let body = br#"{"jsonrpc":"2.0","id":6,"result":{"sent":true,"workerCausality":{"identity":"\ud800"},"workerCausality":{"identity":"\ufffd"}}}"#; + let response = read_response(body, "session.tasks.sendMessage").await; + assert_eq!( + response.result.unwrap()["workerCausality"]["identity"], + "\u{fffd}" + ); + + let body = br#"{"jsonrpc":"2.0","id":7,"result":{"sent":true,"workerCausality":{"identity":"\ufffd"},"workerCausality":{"identity":"\ud800"}}}"#; + let response = read_response(body, "session.tasks.sendMessage").await; + assert!(response.result.unwrap().get("workerCausality").is_none()); + + let body = br#"{"jsonrpc":"2.0","method":"unrelated.notification","params":{"event":{"type":"user.message","data":{"workerCausality":{"identity":"\ud800"}}}}}"#; + let notification = read_notification(body).await; + assert_eq!( + notification.params.unwrap()["event"]["data"]["workerCausality"]["identity"], + "\u{fffd}" + ); + } + #[test] fn request_new_sets_version() { let req = JsonRpcRequest::new(42, "test.method", None); diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1b797f8855..ef1bae156e 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -62,6 +62,7 @@ pub mod transforms; /// Protocol types shared between the SDK and the GitHub Copilot CLI. pub mod types; mod wire; +mod worker_causality; /// Session event payload types — auto-generated from the protocol schema. pub mod session_events; diff --git a/rust/src/session.rs b/rust/src/session.rs index a47d7f90e3..3ffba577af 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -745,6 +745,9 @@ impl Session { "sessionId": self.id, "prompt": opts.prompt, }); + if let Some(client_correlation_id) = opts.client_correlation_id { + params["clientCorrelationId"] = serde_json::to_value(client_correlation_id)?; + } if let Some(source) = opts.source { params["source"] = serde_json::to_value(source)?; } diff --git a/rust/src/types.rs b/rust/src/types.rs index 5c5d5b17b6..3d1ba7737c 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -5584,6 +5584,10 @@ pub struct MessageOptions { pub response_schema: Option, /// The user prompt to send. pub prompt: String, + /// Optional caller-owned diagnostic UUID for this RPC admission. + /// Sent unchanged; supported runtimes echo accepted values only when + /// `RUNTIME_ADMISSION_TRACE_CONTEXT` is enabled. Not an idempotency key. + pub client_correlation_id: Option, /// Optional message provenance. When `None`, the field is omitted, /// preserving the runtime's default for user messages. pub source: Option, @@ -5626,6 +5630,7 @@ impl MessageOptions { pub fn new(prompt: impl Into) -> Self { Self { prompt: prompt.into(), + client_correlation_id: None, response_schema: None, source: None, mode: None, @@ -5639,6 +5644,12 @@ impl MessageOptions { } } + /// Set an opaque caller-owned admission UUID without generating or validating it. + pub fn with_client_correlation_id(mut self, client_correlation_id: impl Into) -> Self { + self.client_correlation_id = Some(client_correlation_id.into()); + self + } + /// Set the message provenance without changing its delivery mode. pub fn with_source(mut self, source: MessageSource) -> Self { self.source = Some(source); diff --git a/rust/src/worker_causality.rs b/rust/src/worker_causality.rs new file mode 100644 index 0000000000..71cc4413be --- /dev/null +++ b/rust/src/worker_causality.rs @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +use std::io::{self, Write}; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Deserializer}; +use serde_json::Value; +use serde_json::value::RawValue; + +struct Budget(usize); + +impl Write for Budget { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.0 = self.0.saturating_add(bytes.len()).min(4097); + if self.0 > 4096 { + Err(io::Error::other("optional worker metadata budget")) + } else { + Ok(bytes.len()) + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn text(value: &Value) -> bool { + value.as_str().is_some_and(|value| { + !value.is_empty() && value.len() <= 256 && !value.chars().any(char::is_control) + }) +} + +fn uuid(value: &Value) -> bool { + value.as_str().is_some_and(|value| { + value.len() == 36 + && value.bytes().enumerate().all(|(index, byte)| { + if matches!(index, 8 | 13 | 18 | 23) { + byte == b'-' + } else { + byte.is_ascii_hexdigit() + } + }) + }) +} + +fn fields(value: &Value, allowed: &[&str]) -> bool { + value + .as_object() + .is_some_and(|object| object.keys().all(|key| allowed.contains(&key.as_str()))) +} + +fn reference(value: &Value, event_type: &str) -> bool { + fields( + value, + &["sessionId", "eventId", "agentId", "eventType", "provenance"], + ) && text(&value["sessionId"]) + && uuid(&value["eventId"]) + && value.get("agentId").is_none_or(text) + && value["eventType"] == event_type + && matches!( + value["provenance"].as_str(), + Some("native" | "ahp_coordinator") + ) +} + +fn source(value: &Value) -> bool { + let input = &value["input"]; + let Some(admissions) = value["admissions"].as_array() else { + return false; + }; + if !fields( + value, + &[ + "input", + "admissions", + "captureComplete", + "completion", + "notification", + "admittedDuring", + ], + ) || !fields( + input, + &["queueItemId", "agentId", "sender", "senderBridges"], + ) || !value["captureComplete"].is_boolean() + || !uuid(&input["queueItemId"]) + || !text(&input["agentId"]) + || admissions.len() > 32 + || input + .get("sender") + .is_some_and(|sender| !reference(sender, "tool.execution_start")) + { + return false; + } + if let Some(edges) = input.get("senderBridges") { + let Some(edges) = edges.as_array() else { + return false; + }; + if input.get("sender").is_none() + || edges.len() > 32 + || edges.iter().any(|edge| { + !fields(edge, &["source", "reported"]) + || !reference(&edge["source"], "tool.execution_start") + || !reference(&edge["reported"], "tool.execution_start") + }) + { + return false; + } + } + if admissions.iter().any(|admission| { + !fields(admission, &["kind", "messageId", "event", "ahpTurnId"]) + || !matches!( + admission["kind"].as_str(), + Some("queued_input" | "system_continuation") + ) + || !text(&admission["messageId"]) + || admission.get("ahpTurnId").is_some_and(|turn| !uuid(turn)) + || admission.get("event").is_some_and(|event| { + !reference(event, "user.message") || event["agentId"] != input["agentId"] + }) + }) { + return false; + } + for (field, event_type) in [ + ("completion", "subagent.completed"), + ("admittedDuring", "assistant.turn_start"), + ] { + if value + .get(field) + .is_some_and(|event| !reference(event, event_type)) + { + return false; + } + } + value.get("notification").is_none_or(|notification| { + fields(notification, &["deliveryId", "event", "mode"]) + && uuid(¬ification["deliveryId"]) + && matches!(notification["mode"].as_str(), Some("queued" | "immediate")) + && notification + .get("event") + .is_none_or(|event| reference(event, "system.notification")) + }) +} + +// Availability only: consumers must additionally validate placement and enclosing self identity. +fn supported(value: &Value) -> bool { + fields( + value, + &[ + "version", + "observationProvenance", + "sources", + "captureComplete", + ], + ) && value["version"].as_u64() == Some(1) + && matches!( + value["observationProvenance"].as_str(), + Some("native" | "ahp_coordinator") + ) + && value["captureComplete"].is_boolean() + && value["sources"].as_array().is_some_and(|sources| { + sources.len() <= 32 + && sources.iter().all(|item| { + source(item) + && (value["captureComplete"] != true || item["captureComplete"] == true) + }) + }) +} + +pub(crate) fn deserialize_optional<'de, D: Deserializer<'de>, T: DeserializeOwned>( + deserializer: D, +) -> Result, D::Error> { + let value = Value::deserialize(deserializer)?; + Ok(decode_optional(value)) +} + +pub(crate) fn deserialize_optional_raw<'de, D: Deserializer<'de>, T: DeserializeOwned>( + deserializer: D, +) -> Result, D::Error> { + let raw = Box::::deserialize(deserializer)?; + let Ok(value) = serde_json::from_str::(raw.get()) else { + tracing::warn!("Ignoring invalid, unsupported or oversized workerCausality metadata"); + return Ok(None); + }; + Ok(decode_optional(value)) +} + +fn decode_optional(value: Value) -> Option { + let field = std::collections::BTreeMap::from([("workerCausality", &value)]); + if serde_json::to_writer(&mut Budget(0), &field).is_ok() + && supported(&value) + && let Ok(capture) = serde_json::from_value(value) + { + return Some(capture); + } + tracing::warn!("Ignoring invalid, unsupported or oversized workerCausality metadata"); + None +} diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 00e78e17f7..04406dc7e0 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -12,7 +12,8 @@ use github_copilot_sdk::rpc::{ McpOauthLoginOptions, McpOauthLoginRequest, McpServer, McpStopServerRequest, ModelSetAllowedModelsRequest, ModelSetAllowedModelsResult, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, - SandboxConfig, SendAgentMode, TasksStartAgentRequest, UnsupportedEnqueueCommandResult, + SandboxConfig, SendAgentMode, SendMessageItem, SendRequest, TasksStartAgentRequest, + UnsupportedEnqueueCommandResult, }; use github_copilot_sdk::session_events::{ McpServerStatus, PermissionRequest, PermissionRequestedData, SessionEventData, @@ -379,6 +380,7 @@ fn permission_event_exposes_managed_approval_required() { fn queue_pending_item_metadata_uses_camel_case_wire_names() { let item = QueuePendingItems { agent_mode: SendAgentMode::Interactive, + client_correlation_id: None, display_text: "second message".to_string(), id: "batch-1".to_string(), kind: QueuePendingItemsKind::Message, @@ -396,6 +398,48 @@ fn queue_pending_item_metadata_uses_camel_case_wire_names() { assert_eq!(deserialized.source.as_deref(), Some("api")); } +#[test] +fn admission_correlation_has_optional_string_round_trip_semantics() { + fn check(base: serde_json::Value) { + for value in [ + None, + Some("01234567-89ab-4cde-8f01-23456789abcd"), + Some("01234567-89AB-4CDE-8F01-23456789ABCD"), + Some("not-a-uuid"), + Some(""), + ] { + let mut wire = base.clone(); + if let Some(value) = value { + wire["clientCorrelationId"] = serde_json::json!(value); + } + let decoded: T = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(decoded).unwrap(), wire); + wire["futureField"] = serde_json::json!({"enabled": true}); + let decoded: T = serde_json::from_value(wire.clone()).unwrap(); + wire.as_object_mut().unwrap().remove("futureField"); + assert_eq!(serde_json::to_value(decoded).unwrap(), wire); + } + let mut explicit_null = base.clone(); + explicit_null["clientCorrelationId"] = serde_json::Value::Null; + let decoded: T = serde_json::from_value(explicit_null).unwrap(); + assert_eq!(serde_json::to_value(decoded).unwrap(), base); + let mut invalid_type = base; + invalid_type["clientCorrelationId"] = serde_json::json!(42); + assert!(serde_json::from_value::(invalid_type).is_err()); + } + + check::(serde_json::json!({"prompt": "hello"})); + check::(serde_json::json!({"prompt": "hello"})); + check::(serde_json::json!({ + "id": "queue-1", "messageId": "server-message-1", "kind": "message", + "displayText": "hello", "agentMode": "interactive" + })); + check::(serde_json::json!({ + "content": "hello", "messageId": "server-message-1", + "interactionId": "agent-loop-1", "turnId": "0" + })); +} + #[test] fn queue_pending_message_id_is_optional_for_older_hosts() { let item: QueuePendingItems = serde_json::from_value(serde_json::json!({ diff --git a/rust/tests/execution_identity_test.rs b/rust/tests/execution_identity_test.rs new file mode 100644 index 0000000000..8be6f2c503 --- /dev/null +++ b/rust/tests/execution_identity_test.rs @@ -0,0 +1,233 @@ +//! Compatibility tests for observed identity, not an application association reducer. + +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::rpc::{QueuePendingItemsResult, SendResult}; +use github_copilot_sdk::session_events::{ + AssistantMessageData, AssistantTurnStartData, SubagentStartedData, TypedSessionEvent, + UserMessageData, +}; +use github_copilot_sdk::types::{SessionEvent, SessionEventNotification}; +use serde_json::{Value, json}; + +fn fixture() -> Value { + serde_json::from_str(include_str!("fixtures/execution-identity-v1.json")).unwrap() +} + +fn event(fixture: &Value, name: &str) -> SessionEvent { + serde_json::from_value(fixture["notifications"][name]["params"]["event"].clone()).unwrap() +} + +#[test] +fn identity_fixture_round_trips_through_public_event_types() { + let fixture = fixture(); + assert_eq!(fixture["version"], 1); + assert_eq!(fixture["synthetic"], true); + + for notification in fixture["notifications"].as_object().unwrap().values() { + let routed: SessionEventNotification = + serde_json::from_value(notification["params"].clone()).unwrap(); + let typed: TypedSessionEvent = + serde_json::from_value(notification["params"]["event"].clone()).unwrap(); + + assert_eq!(typed.id, routed.event.id); + assert_eq!(typed.agent_id, routed.event.agent_id); + assert_eq!( + serde_json::to_value(routed).unwrap(), + notification["params"] + ); + assert_eq!( + serde_json::to_value(typed).unwrap(), + notification["params"]["event"] + ); + } +} + +#[test] +fn message_identity_is_distinct_from_event_queue_and_assistant_identity() { + let fixture = fixture(); + let pending: QueuePendingItemsResult = + serde_json::from_value(fixture["pendingItems"].clone()).unwrap(); + assert_eq!( + serde_json::to_value(&pending).unwrap(), + fixture["pendingItems"] + ); + assert_eq!(pending.items[0].id, pending.items[1].id); + + for (name, item) in ["user", "system"].into_iter().zip(&pending.items) { + let accepted: SendResult = + serde_json::from_value(fixture["sendResults"][name].clone()).unwrap(); + let notification = event(&fixture, name); + let user = notification.typed_data::().unwrap(); + assert_eq!( + user.message_id.as_deref(), + Some(accepted.message_id.as_str()) + ); + assert_eq!(item.message_id, user.message_id); + assert_ne!(item.id, accepted.message_id); + assert_ne!(notification.id, accepted.message_id); + assert_ne!( + notification.parent_id.as_deref(), + user.message_id.as_deref() + ); + } + + let user = event(&fixture, "user") + .typed_data::() + .unwrap(); + let assistant = event(&fixture, "assistant") + .typed_data::() + .unwrap(); + assert_eq!(assistant.originating_message_id, user.message_id); + assert_ne!(Some(assistant.message_id), user.message_id); + assert_ne!(assistant.turn_id, user.turn_id); +} + +#[test] +fn loop_and_interaction_keys_can_repeat_without_replacing_message_identity() { + let fixture = fixture(); + let user = event(&fixture, "user") + .typed_data::() + .unwrap(); + let system = event(&fixture, "system") + .typed_data::() + .unwrap(); + assert_eq!(user.turn_id, system.turn_id); + assert_eq!(user.interaction_id, system.interaction_id); + assert_ne!(user.message_id, system.message_id); + + let worker = event(&fixture, "worker"); + let worker_data = worker.typed_data::().unwrap(); + assert!(worker.agent_id.is_some()); + assert_ne!(worker_data.originating_message_id, user.message_id); + assert_ne!(worker_data.interaction_id, user.interaction_id); +} + +#[test] +fn distinct_loop_occurrences_preserve_event_ids_when_counter_keys_repeat() { + let fixture = fixture(); + let first = event(&fixture, "turnStart"); + let second = event(&fixture, "reusedTurnStart"); + assert_eq!(first.data, second.data); + assert_ne!(first.id, second.id); + assert_eq!( + fixture["notifications"]["turnStart"]["params"]["sessionId"], + fixture["notifications"]["reusedTurnStart"]["params"]["sessionId"] + ); +} + +#[test] +fn spawn_and_correction_identity_preserve_distinct_relationships() { + let fixture = fixture(); + let started = event(&fixture, "workerStarted"); + let spawn = started.typed_data::().unwrap(); + let worker = event(&fixture, "worker"); + assert_eq!(started.agent_id, worker.agent_id); + assert_eq!(spawn.tool_call_id, "tool-spawn-a"); + assert_ne!( + started.agent_id.as_deref(), + Some(spawn.tool_call_id.as_str()) + ); + + let user = event(&fixture, "user") + .typed_data::() + .unwrap(); + let corrected = event(&fixture, "correctedAssistant") + .typed_data::() + .unwrap(); + assert_eq!(corrected.originating_message_id, user.message_id); + assert_ne!(corrected.interaction_id, user.interaction_id); +} + +#[test] +fn absent_identity_is_not_inferred_from_an_event_or_assistant_message_id() { + let fixture = fixture(); + let legacy = event(&fixture, "legacy") + .typed_data::() + .unwrap(); + assert!(legacy.message_id.is_none()); + assert!(legacy.turn_id.is_none()); + assert!(legacy.interaction_id.is_none()); + assert!(legacy.parent_agent_task_id.is_none()); + + let mut assistant = event(&fixture, "assistant"); + for key in ["originatingMessageId", "turnId", "interactionId"] { + assistant.data.as_object_mut().unwrap().remove(key); + } + let data = assistant.typed_data::().unwrap(); + assert!(data.originating_message_id.is_none()); + assert!(data.turn_id.is_none()); + assert!(data.interaction_id.is_none()); + assert!(!data.message_id.is_empty()); + + let mut turn = event(&fixture, "turnStart"); + turn.data.as_object_mut().unwrap().remove("interactionId"); + let data = turn.typed_data::().unwrap(); + assert_eq!(data.turn_id, "0"); + assert!(data.interaction_id.is_none()); + turn.data.as_object_mut().unwrap().remove("turnId"); + assert!(turn.typed_data::().is_none()); +} + +#[test] +fn empty_identity_remains_empty_and_malformed_identity_is_not_coerced() { + let fixture = fixture(); + let mut empty = event(&fixture, "user"); + for key in ["messageId", "turnId", "interactionId"] { + empty.data[key] = json!(""); + } + let data = empty.typed_data::().unwrap(); + assert_eq!(data.message_id.as_deref(), Some("")); + assert_eq!(data.turn_id.as_deref(), Some("")); + assert_eq!(data.interaction_id.as_deref(), Some("")); + + for key in ["messageId", "turnId", "interactionId"] { + let mut malformed = event(&fixture, "user"); + malformed.data[key] = json!(0); + assert!(malformed.typed_data::().is_none()); + assert_eq!(malformed.data[key], 0); + } + + for response in [ + json!({}), + json!({"messageId": null}), + json!({"messageId": 0}), + ] { + assert!(serde_json::from_value::(response).is_err()); + } +} + +#[test] +fn identity_fixture_contains_no_message_or_tool_content() { + fn check(value: &Value) { + match value { + Value::Object(fields) => { + for (key, value) in fields { + if matches!( + key.as_str(), + "content" + | "displayText" + | "agentDescription" + | "agentDisplayName" + | "agentName" + ) { + assert_eq!(value, ""); + } + assert!(!matches!( + key.as_str(), + "prompt" + | "arguments" + | "attachments" + | "transformedContent" + | "reasoningText" + | "result" + )); + check(value); + } + } + Value::Array(values) => values.iter().for_each(check), + _ => {} + } + } + check(&fixture()); +} diff --git a/rust/tests/fixtures/execution-identity-v1.json b/rust/tests/fixtures/execution-identity-v1.json new file mode 100644 index 0000000000..298fac094d --- /dev/null +++ b/rust/tests/fixtures/execution-identity-v1.json @@ -0,0 +1,241 @@ +{ + "version": 1, + "synthetic": true, + "sdkVersion": "1.0.14", + "cliVersion": "1.0.85", + "sendResults": { + "user": {"messageId": "20000000-0000-4000-8000-000000000001"}, + "system": {"messageId": "20000000-0000-4000-8000-000000000002"} + }, + "pendingItems": { + "items": [ + { + "agentMode": "interactive", + "displayText": "", + "id": "queue-batch-a", + "kind": "message", + "messageId": "20000000-0000-4000-8000-000000000001" + }, + { + "agentMode": "interactive", + "displayText": "", + "id": "queue-batch-a", + "kind": "message", + "messageId": "20000000-0000-4000-8000-000000000002" + } + ], + "steeringMessages": [] + }, + "notifications": { + "user": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000001", + "timestamp": "2026-09-16T00:00:01Z", + "parentId": "10000000-0000-4000-8000-000000000000", + "type": "user.message", + "data": { + "content": "", + "delivery": "idle", + "source": "user", + "messageId": "20000000-0000-4000-8000-000000000001", + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000001" + } + } + } + }, + "turnStart": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000002", + "timestamp": "2026-09-16T00:00:02Z", + "parentId": "10000000-0000-4000-8000-000000000001", + "type": "assistant.turn_start", + "data": { + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000001" + } + } + } + }, + "assistant": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000003", + "timestamp": "2026-09-16T00:00:03Z", + "parentId": "10000000-0000-4000-8000-000000000002", + "type": "assistant.message", + "data": { + "content": "", + "messageId": "40000000-0000-4000-8000-000000000001", + "originatingMessageId": "20000000-0000-4000-8000-000000000001", + "turnId": "1", + "interactionId": "30000000-0000-4000-8000-000000000001" + } + } + } + }, + "correctedAssistant": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000009", + "timestamp": "2026-09-16T00:00:03.500Z", + "parentId": "10000000-0000-4000-8000-000000000003", + "type": "assistant.message", + "data": { + "content": "", + "messageId": "40000000-0000-4000-8000-000000000003", + "originatingMessageId": "20000000-0000-4000-8000-000000000001", + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000003" + } + } + } + }, + "system": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000004", + "timestamp": "2026-09-16T00:00:04Z", + "parentId": "10000000-0000-4000-8000-000000000003", + "type": "user.message", + "data": { + "content": "", + "delivery": "queued", + "source": "system", + "messageId": "20000000-0000-4000-8000-000000000002", + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000001" + } + } + } + }, + "reusedTurnStart": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000011", + "timestamp": "2026-09-16T00:00:04.250Z", + "parentId": "10000000-0000-4000-8000-000000000004", + "type": "assistant.turn_start", + "data": { + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000001" + } + } + } + }, + "workerStarted": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000010", + "timestamp": "2026-09-16T00:00:04.500Z", + "parentId": "10000000-0000-4000-8000-000000000004", + "agentId": "50000000-0000-4000-8000-000000000001", + "type": "subagent.started", + "data": { + "agentDescription": "", + "agentDisplayName": "", + "agentName": "", + "toolCallId": "tool-spawn-a" + } + } + } + }, + "worker": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000005", + "timestamp": "2026-09-16T00:00:05Z", + "parentId": "10000000-0000-4000-8000-000000000004", + "agentId": "50000000-0000-4000-8000-000000000001", + "type": "assistant.message", + "data": { + "content": "", + "messageId": "40000000-0000-4000-8000-000000000002", + "originatingMessageId": "20000000-0000-4000-8000-000000000003", + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000002" + } + } + } + }, + "otherSession": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-b", + "event": { + "id": "10000000-0000-4000-8000-000000000006", + "timestamp": "2026-09-16T00:00:06Z", + "parentId": "10000000-0000-4000-8000-000000000000", + "type": "user.message", + "data": { + "content": "", + "delivery": "idle", + "source": "user", + "messageId": "20000000-0000-4000-8000-000000000004", + "turnId": "0", + "interactionId": "30000000-0000-4000-8000-000000000001" + } + } + } + }, + "legacy": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000007", + "timestamp": "2026-09-16T00:00:07Z", + "parentId": "10000000-0000-4000-8000-000000000006", + "type": "user.message", + "data": {"content": ""} + } + } + }, + "resume": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "10000000-0000-4000-8000-000000000008", + "timestamp": "2026-09-16T00:00:08Z", + "parentId": "10000000-0000-4000-8000-000000000007", + "type": "session.resume", + "data": { + "eventCount": 7, + "resumeTime": "2026-09-16T00:00:08Z", + "continuePendingWork": true + } + } + } + } + } +} diff --git a/rust/tests/fixtures/tool-trace-context-v1.json b/rust/tests/fixtures/tool-trace-context-v1.json new file mode 100644 index 0000000000..6be7e28c89 --- /dev/null +++ b/rust/tests/fixtures/tool-trace-context-v1.json @@ -0,0 +1,61 @@ +{ + "version": 1, + "synthetic": true, + "runtimeCommit": "7ab2ab8ea278c812664eece9feab83ef376936eb", + "notifications": { + "sampled": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "60000000-0000-4000-8000-000000000001", + "timestamp": "2026-09-23T00:00:01Z", + "parentId": "60000000-0000-4000-8000-000000000000", + "type": "tool.execution_start", + "data": { + "toolCallId": "tool-a", + "toolName": "client-tool", + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "tracestate": "vendor=value" + } + } + } + }, + "unsampled": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "60000000-0000-4000-8000-000000000002", + "timestamp": "2026-09-23T00:00:02Z", + "parentId": "60000000-0000-4000-8000-000000000001", + "type": "tool.execution_start", + "data": { + "toolCallId": "tool-b", + "toolName": "client-tool", + "traceparent": "00-11111111111111111111111111111111-2222222222222222-00" + } + } + } + }, + "absent": { + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": "runtime-session-a", + "event": { + "id": "60000000-0000-4000-8000-000000000003", + "timestamp": "2026-09-23T00:00:03Z", + "parentId": "60000000-0000-4000-8000-000000000002", + "type": "tool.execution_start", + "data": { + "toolCallId": "tool-c", + "toolName": "client-tool" + } + } + } + } + } +} diff --git a/rust/tests/prepared_session_test.rs b/rust/tests/prepared_session_test.rs index f3102d7f40..e000bba173 100644 --- a/rust/tests/prepared_session_test.rs +++ b/rust/tests/prepared_session_test.rs @@ -16,7 +16,7 @@ use futures_util::FutureExt; use github_copilot_sdk::handler::{ McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult, }; -use github_copilot_sdk::session::PreparedSession; +use github_copilot_sdk::session::{PreparedSession, Session}; use github_copilot_sdk::subscription::{EventSubscription, RecvErrorKind}; use github_copilot_sdk::types::{ CloudSessionOptions, CloudSessionRepository, MessageOptions, PermissionRequestData, RequestId, @@ -136,6 +136,10 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(¬ification).unwrap()).await; } + async fn send_notification(&mut self, notification: &Value) { + write_framed(&mut self.write, &serde_json::to_vec(notification).unwrap()).await; + } + /// Emit the startup burst the host cares about: `BURST` ordered events /// followed by an ephemeral `session.idle` that `getMessages` could /// never recover. @@ -1830,3 +1834,297 @@ async fn deferred_create_cancelled_after_callback_registered_is_cleaned_up() { ); drop(session); } + +fn identity_fixture() -> Value { + serde_json::from_str(include_str!("fixtures/execution-identity-v1.json")).unwrap() +} + +async fn start_identity_session( + client: &Client, + server: &mut FakeServer, + session_id: &str, +) -> (Session, EventSubscription) { + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id)) + .unwrap(); + let events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + let request = server.read_request().await; + assert_eq!(request["method"], "session.create"); + server + .respond(&request, json!({"sessionId": session_id})) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), session_id); + (session, events) +} + +async fn expect_identity_event(events: &mut EventSubscription, notification: &Value) { + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!( + serde_json::to_value(event).unwrap(), + notification["params"]["event"] + ); +} + +#[tokio::test] +async fn identity_delivery_does_not_depend_on_send_acknowledgement_order() { + let fixture = identity_fixture(); + let (client, mut server) = make_client(); + let session_id = fixture["notifications"]["user"]["params"]["sessionId"] + .as_str() + .unwrap(); + let (session, mut events) = start_identity_session(&client, &mut server, session_id).await; + let session = Arc::new(session); + + for (name, before_ack) in [("user", true), ("system", false)] { + let sending = tokio::spawn({ + let session = session.clone(); + async move { + let source = if name == "system" { + github_copilot_sdk::MessageSource::System + } else { + github_copilot_sdk::MessageSource::User + }; + session + .send(MessageOptions::new("").with_source(source)) + .await + } + }); + let request = server.read_request().await; + assert_eq!(request["method"], "session.send"); + let notification = &fixture["notifications"][name]; + if before_ack { + server.send_notification(notification).await; + expect_identity_event(&mut events, notification).await; + } + server + .respond(&request, fixture["sendResults"][name].clone()) + .await; + let message_id = timeout(TIMEOUT, sending).await.unwrap().unwrap().unwrap(); + assert_eq!( + message_id, + fixture["sendResults"][name]["messageId"].as_str().unwrap() + ); + if !before_ack { + server.send_notification(notification).await; + expect_identity_event(&mut events, notification).await; + } + } +} + +#[tokio::test] +async fn cancelled_send_response_does_not_supply_the_next_message_identity() { + let fixture = identity_fixture(); + let (client, mut server) = make_client(); + let session_id = fixture["notifications"]["user"]["params"]["sessionId"] + .as_str() + .unwrap(); + let (session, mut events) = start_identity_session(&client, &mut server, session_id).await; + let session = Arc::new(session); + let abandoned = tokio::spawn({ + let session = session.clone(); + async move { session.send("").await } + }); + let abandoned_request = server.read_request().await; + assert_eq!(abandoned_request["method"], "session.send"); + abandoned.abort(); + assert!( + timeout(TIMEOUT, abandoned) + .await + .unwrap() + .unwrap_err() + .is_cancelled() + ); + + let next = tokio::spawn({ + let session = session.clone(); + async move { + session + .send( + MessageOptions::new("").with_source(github_copilot_sdk::MessageSource::System), + ) + .await + } + }); + let next_request = server.read_request().await; + assert_eq!(next_request["method"], "session.send"); + assert_ne!(abandoned_request["id"], next_request["id"]); + server + .respond(&abandoned_request, fixture["sendResults"]["user"].clone()) + .await; + server + .send_notification(&fixture["notifications"]["user"]) + .await; + server + .respond(&next_request, fixture["sendResults"]["system"].clone()) + .await; + server + .send_notification(&fixture["notifications"]["system"]) + .await; + assert_eq!( + timeout(TIMEOUT, next).await.unwrap().unwrap().unwrap(), + fixture["sendResults"]["system"]["messageId"] + .as_str() + .unwrap() + ); + for name in ["user", "system"] { + expect_identity_event(&mut events, &fixture["notifications"][name]).await; + } +} + +#[tokio::test] +async fn identity_delivery_preserves_duplicates_workers_and_session_isolation() { + let fixture = identity_fixture(); + let notifications = &fixture["notifications"]; + let (client, mut server) = make_client(); + let (_first, mut first_events) = start_identity_session( + &client, + &mut server, + notifications["user"]["params"]["sessionId"] + .as_str() + .unwrap(), + ) + .await; + let (_second, mut second_events) = start_identity_session( + &client, + &mut server, + notifications["otherSession"]["params"]["sessionId"] + .as_str() + .unwrap(), + ) + .await; + + for name in [ + "user", + "otherSession", + "system", + "worker", + "workerStarted", + "user", + "legacy", + ] { + server.send_notification(¬ifications[name]).await; + } + let mut conflicting_duplicate = notifications["user"].clone(); + conflicting_duplicate["params"]["event"]["data"]["messageId"] = + fixture["sendResults"]["system"]["messageId"].clone(); + server.send_notification(&conflicting_duplicate).await; + for name in [ + "user", + "system", + "worker", + "workerStarted", + "user", + "legacy", + ] { + expect_identity_event(&mut first_events, ¬ifications[name]).await; + } + expect_identity_event(&mut first_events, &conflicting_duplicate).await; + expect_identity_event(&mut second_events, ¬ifications["otherSession"]).await; + assert!(timeout(QUIET, first_events.recv()).await.is_err()); + assert!(timeout(QUIET, second_events.recv()).await.is_err()); +} + +#[tokio::test] +async fn routed_replay_and_missing_identity_survive_resume_without_reassociation() { + let fixture = identity_fixture(); + let notifications = &fixture["notifications"]; + let session_id = notifications["user"]["params"]["sessionId"] + .as_str() + .unwrap(); + let (client, mut server) = make_client(); + let (session, mut events) = start_identity_session(&client, &mut server, session_id).await; + server.send_notification(¬ifications["user"]).await; + expect_identity_event(&mut events, ¬ifications["user"]).await; + drop(session); + expect_closed(&mut events).await; + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(SessionId::new(session_id)).with_continue_pending_work(true), + ) + .unwrap(); + let mut resumed_events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + let request = server.read_request().await; + assert_eq!(request["method"], "session.resume"); + assert_eq!(request["params"]["continuePendingWork"], true); + for name in ["resume", "user", "legacy", "worker"] { + server.send_notification(¬ifications[name]).await; + } + server + .respond(&request, json!({"sessionId": session_id})) + .await; + server.answer_skills_reload().await; + let resumed = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + assert_eq!(resumed.id().as_str(), session_id); + for name in ["resume", "user", "legacy", "worker"] { + expect_identity_event(&mut resumed_events, ¬ifications[name]).await; + } +} + +#[tokio::test] +async fn a_new_connection_does_not_reuse_observed_identity_for_the_same_session_id() { + let fixture = identity_fixture(); + let notifications = &fixture["notifications"]; + let session_id = notifications["user"]["params"]["sessionId"] + .as_str() + .unwrap(); + for name in ["user", "legacy"] { + let (client, mut server) = make_client(); + let (session, mut events) = start_identity_session(&client, &mut server, session_id).await; + server.send_notification(¬ifications[name]).await; + expect_identity_event(&mut events, ¬ifications[name]).await; + drop(session); + expect_closed(&mut events).await; + } +} + +#[tokio::test] +async fn early_tool_context_is_delivered_without_waiting_for_a_callback_or_reusing_context() { + use github_copilot_sdk::session_events::ToolExecutionStartData; + + let fixture: Value = + serde_json::from_str(include_str!("fixtures/tool-trace-context-v1.json")).unwrap(); + let notifications = &fixture["notifications"]; + let session_id = notifications["sampled"]["params"]["sessionId"] + .as_str() + .unwrap(); + let (client, mut server) = make_client(); + let (_session, mut events) = start_identity_session(&client, &mut server, session_id).await; + + for name in ["sampled", "unsampled", "absent", "sampled"] { + server.send_notification(¬ifications[name]).await; + } + let mut persisted = notifications["sampled"].clone(); + let data = persisted["params"]["event"]["data"] + .as_object_mut() + .unwrap(); + data.remove("traceparent"); + data.remove("tracestate"); + server.send_notification(&persisted).await; + + for wire in [ + ¬ifications["sampled"], + ¬ifications["unsampled"], + ¬ifications["absent"], + ¬ifications["sampled"], + &persisted, + ] { + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + let data = event.typed_data::().unwrap(); + let expected = &wire["params"]["event"]["data"]; + assert_eq!(data.tool_call_id, expected["toolCallId"].as_str().unwrap()); + assert_eq!( + data.traceparent.as_deref(), + expected["traceparent"].as_str() + ); + assert_eq!(data.tracestate.as_deref(), expected["tracestate"].as_str()); + assert_eq!( + serde_json::to_value(event).unwrap(), + wire["params"]["event"] + ); + } + server.expect_quiet().await; +} diff --git a/rust/tests/session_events_test.rs b/rust/tests/session_events_test.rs index aabdd7f4f4..f324d636f0 100644 --- a/rust/tests/session_events_test.rs +++ b/rust/tests/session_events_test.rs @@ -284,3 +284,55 @@ fn user_message_id_is_optional_for_older_hosts() { .is_none() ); } + +#[test] +fn tool_execution_start_preserves_optional_trace_context_without_changing_tool_data() { + let traceparent = "00-33333333333333333333333333333333-4444444444444444-01"; + for (parent, state) in [ + (None, None), + (Some(traceparent), None), + (Some(traceparent), Some("vendor=tool")), + (Some("invalid"), Some("invalid")), + (Some(""), None), + ] { + let mut data = serde_json::json!({ + "toolCallId": "tool-call-a", + "toolName": "client-tool" + }); + if let Some(parent) = parent { + data["traceparent"] = serde_json::json!(parent); + } + if let Some(state) = state { + data["tracestate"] = serde_json::json!(state); + } + let wire = event_envelope("tool.execution_start", data); + let event: TypedSessionEvent = serde_json::from_value(wire.clone()).unwrap(); + let SessionEventData::ToolExecutionStart(data) = &event.payload else { + panic!("expected tool-start event"); + }; + assert_eq!(data.tool_call_id, "tool-call-a"); + assert_eq!(data.tool_name, "client-tool"); + assert_eq!(data.traceparent.as_deref(), parent); + assert_eq!(data.tracestate.as_deref(), state); + assert_eq!(serde_json::to_value(event).unwrap(), wire); + } +} + +#[test] +fn early_tool_context_fixture_round_trips_without_content_or_invented_fields() { + let fixture: serde_json::Value = + serde_json::from_str(include_str!("fixtures/tool-trace-context-v1.json")).unwrap(); + assert_eq!(fixture["synthetic"], true); + for notification in fixture["notifications"].as_object().unwrap().values() { + let wire = ¬ification["params"]["event"]; + let event: TypedSessionEvent = serde_json::from_value(wire.clone()).unwrap(); + assert!(matches!( + &event.payload, + SessionEventData::ToolExecutionStart(_) + )); + assert!(wire["data"].get("arguments").is_none()); + assert!(wire["data"].get("version").is_none()); + assert!(wire["data"].get("_meta").is_none()); + assert_eq!(serde_json::to_value(event).unwrap(), *wire); + } +} diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 73e0b7cd4d..da7db51901 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -23,7 +23,7 @@ use github_copilot_sdk::rpc::{ ConnectorCatalogStatus, ConnectorConnectRequest, ConnectorConnectResult, ConnectorContinueRequest, ConnectorDisconnectResult, ConnectorMcpStatus, ConnectorReconcileRequest, ConnectorStatus, ModelSetAllowedModelsRequest, OpenCanvasInstance, - SendAgentMode, SendMode, SendRequest, SessionRpcConnectors, + SendAgentMode, SendMessagesRequest, SendMode, SendRequest, SessionRpcConnectors, }; use github_copilot_sdk::session_events::{ ManagedSettingsResolvedSource, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, @@ -2283,6 +2283,207 @@ async fn send_injects_session_id() { timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); } +#[tokio::test] +async fn admission_correlation_is_opt_in_and_preserves_existing_send_options() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + for correlation in [ + None, + Some("01234567-89ab-4cde-8f01-23456789abcd"), + Some("01234567-89AB-4CDE-8F01-23456789ABCD"), + Some("not-a-uuid"), + Some(""), + ] { + for typed_rpc in [false, true] { + let mut options = MessageOptions::new("hello"); + assert!(options.client_correlation_id.is_none()); + if let Some(value) = correlation { + options = options.with_client_correlation_id(value); + } + assert_eq!( + options.clone().client_correlation_id.as_deref(), + correlation + ); + let mut expected = serde_json::json!({ + "sessionId": server.session_id, "prompt": "hello", "source": "system", + "mode": "enqueue", "displayPrompt": "display", "agentMode": "plan", + "requestHeaders": {"X-Test": "preserved"}, + "traceparent": "00-11111111111111111111111111111111-2222222222222222-01", + "tracestate": "vendor=preserved" + }); + if let Some(value) = correlation { + expected["clientCorrelationId"] = serde_json::json!(value); + } + let handle = tokio::spawn({ + let session = session.clone(); + let mut request = expected.clone(); + request.as_object_mut().unwrap().remove("sessionId"); + async move { + if typed_rpc { + session + .rpc() + .send(serde_json::from_value(request).unwrap()) + .await + .map(|result| result.message_id) + } else { + session + .send( + options + .with_source(MessageSource::System) + .with_mode(DeliveryMode::Enqueue) + .with_agent_mode(AgentMode::Plan) + .with_display_prompt("display") + .with_request_headers(HashMap::from([( + "X-Test".into(), + "preserved".into(), + )])) + .with_traceparent( + "00-11111111111111111111111111111111-2222222222222222-01", + ) + .with_tracestate("vendor=preserved"), + ) + .await + } + } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.send"); + assert_eq!(request["params"], expected); + server + .respond( + &request, + serde_json::json!({"messageId": "server-message-1", "future": true}), + ) + .await; + assert_eq!( + timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(), + "server-message-1" + ); + } + } +} + +#[tokio::test] +async fn admission_correlation_stays_per_item_across_concurrent_sends_and_batch() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + const FIRST: &str = "01234567-89ab-4cde-8f01-23456789abcd"; + const SECOND: &str = "abcdef01-2345-4678-9abc-def012345678"; + let handle = tokio::spawn({ + let session = session.clone(); + async move { + let batch: SendMessagesRequest = serde_json::from_value(serde_json::json!({ + "messages": [ + {"prompt": "context", "clientCorrelationId": SECOND}, + {"prompt": "plain"}, + {"prompt": "reused", "clientCorrelationId": FIRST} + ] + })) + .unwrap(); + let rpc = session.rpc(); + tokio::join!( + session.send(MessageOptions::new("first").with_client_correlation_id(FIRST)), + session.send(MessageOptions::new("second").with_client_correlation_id(SECOND)), + rpc.send_messages(batch) + ) + } + }); + + let mut requests = Vec::new(); + for _ in 0..3 { + requests.push(timeout(TIMEOUT, server.read_request()).await.unwrap()); + } + for request in requests.into_iter().rev() { + let params = &request["params"]; + assert_eq!(params["sessionId"], server.session_id); + if request["method"] == "session.sendMessages" { + assert!(params.get("clientCorrelationId").is_none()); + assert_eq!( + params["messages"], + serde_json::json!([ + {"prompt": "context", "clientCorrelationId": SECOND}, + {"prompt": "plain"}, + {"prompt": "reused", "clientCorrelationId": FIRST} + ]) + ); + server + .respond( + &request, + serde_json::json!({"messageIds": ["context-id", "plain-id", "reused-id"]}), + ) + .await; + } else { + assert_eq!(request["method"], "session.send"); + let (correlation, message_id) = if params["prompt"] == "first" { + (FIRST, "first-id") + } else { + assert_eq!(params["prompt"], "second"); + (SECOND, "second-id") + }; + assert_eq!(params["clientCorrelationId"], correlation); + server + .respond(&request, serde_json::json!({"messageId": message_id})) + .await; + } + } + let (first, second, batch) = timeout(TIMEOUT, handle).await.unwrap().unwrap(); + assert_eq!(first.unwrap(), "first-id"); + assert_eq!(second.unwrap(), "second-id"); + assert_eq!( + batch.unwrap().message_ids, + ["context-id", "plain-id", "reused-id"] + ); +} + +#[tokio::test] +async fn admission_correlation_event_and_pending_rows_preserve_canonical_message_ids() { + use github_copilot_sdk::session_events::UserMessageData; + + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + let mut events = session.subscribe(); + const CORRELATION: &str = "01234567-89ab-4cde-8f01-23456789abcd"; + for correlation in [Some(CORRELATION), None] { + let mut data = serde_json::json!({ + "content": "hello", "messageId": "canonical-id", + "interactionId": "agent-loop-id", "turnId": "0", "futureField": {"x": true} + }); + if let Some(value) = correlation { + data["clientCorrelationId"] = serde_json::json!(value); + } + server.send_event("user.message", data.clone()).await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.data, data); + let typed = event.typed_data::().unwrap(); + assert_eq!(typed.client_correlation_id.as_deref(), correlation); + assert_eq!(typed.message_id.as_deref(), Some("canonical-id")); + assert_eq!(typed.interaction_id.as_deref(), Some("agent-loop-id")); + assert_ne!(event.id, "canonical-id"); + } + + let handle = tokio::spawn({ + let session = session.clone(); + async move { session.rpc().queue().pending_items().await } + }); + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.queue.pendingItems"); + server.respond(&request, serde_json::json!({"steeringMessages": [], "items": [ + {"id": "queue-id", "messageId": "canonical-id", "clientCorrelationId": CORRELATION, + "kind": "message", "displayText": "hello", "agentMode": "interactive", "futureField": true}, + {"id": "legacy-queue-id", "kind": "message", "displayText": "older", "agentMode": "interactive"} + ]})).await; + let pending = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert_eq!( + pending.items[0].client_correlation_id.as_deref(), + Some(CORRELATION) + ); + assert_eq!(pending.items[0].message_id.as_deref(), Some("canonical-id")); + assert_eq!(pending.items[0].id, "queue-id"); + assert!(pending.items[1].client_correlation_id.is_none()); + assert!(pending.items[1].message_id.is_none()); +} + #[test] fn message_options_source_is_opt_in() { let prompt = "hello".to_string(); diff --git a/rust/tests/worker_causality_test.rs b/rust/tests/worker_causality_test.rs new file mode 100644 index 0000000000..240552757c --- /dev/null +++ b/rust/tests/worker_causality_test.rs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::rpc::TasksSendMessageResult; +use github_copilot_sdk::session_events::{ + SystemNotification, SystemNotificationWorkflowCompletedStatus, TypedSessionEvent, +}; +use serde_json::Value; + +fn corpus() -> Value { + serde_json::from_str(include_str!("../../test/worker-causality.json")).unwrap() +} + +fn decode(value: &Value, event: bool) -> Value { + let wire = serde_json::to_string(value).unwrap(); + if event { + let parsed = + serde_json::from_str::(&wire).expect("deserialize typed event"); + serde_json::to_value(parsed).expect("serialize typed event") + } else { + let parsed = + serde_json::from_str::(&wire).expect("deserialize task result"); + serde_json::to_value(parsed).expect("serialize task result") + } +} + +fn payload(value: &Value, event: bool) -> &Value { + if event { &value["data"] } else { value } +} + +fn payload_mut(value: &mut Value, event: bool) -> &mut Value { + if event { &mut value["data"] } else { value } +} + +#[test] +fn worker_causality_public_readers_preserve_source_and_product() { + let corpus = corpus(); + for case in corpus["valid"].as_array().unwrap() { + let event = case.get("event").is_some(); + let mut wire = case[if event { "event" } else { "result" }].clone(); + assert_eq!( + payload(&decode(&wire, event), event)["workerCausality"], + payload(&wire, event)["workerCausality"], + "{}", + case["name"] + ); + payload_mut(&mut wire, event) + .as_object_mut() + .unwrap() + .remove("workerCausality"); + let baseline = serde_json::to_vec(&decode(&wire, event)).unwrap(); + for invalid in corpus["invalid"].as_array().unwrap() { + payload_mut(&mut wire, event)["workerCausality"] = invalid["value"].clone(); + assert_eq!( + serde_json::to_vec(&decode(&wire, event)).unwrap(), + baseline, + "{}: {}", + case["name"], + invalid["name"] + ); + } + } +} + +#[test] +fn worker_causality_exact_utf8_budget_and_unknown_fields() { + let corpus = corpus(); + for case in corpus["boundaries"].as_array().unwrap() { + let mut wire = corpus["valid"][0]["event"].clone(); + wire["data"]["workerCausality"] = case["value"].clone(); + let decoded = decode(&wire, true); + assert_eq!( + decoded["data"].get("workerCausality").is_some(), + case["accepted"].as_bool().unwrap(), + "{}", + case["name"] + ); + assert_eq!(decoded["data"]["content"], wire["data"]["content"]); + } +} + +#[test] +fn canonical_workflow_completion_decodes_typed_fields() { + let corpus = corpus(); + for fixture_name in ["workflowCompleted", "legacyWorkflowCompleted"] { + let event: TypedSessionEvent = + serde_json::from_value(corpus[fixture_name]["event"].clone()).unwrap(); + let github_copilot_sdk::session_events::SessionEventData::SystemNotification(notification) = + event.payload + else { + panic!("expected system.notification"); + }; + let SystemNotification::WorkflowCompleted(completion) = notification.kind else { + panic!("expected workflow_completed"); + }; + assert_eq!(completion.workflow_name, "fix-ci"); + assert_eq!(completion.run_id, "run-1"); + assert!(matches!( + completion.status, + SystemNotificationWorkflowCompletedStatus::Completed + )); + assert_eq!(completion.consumed_subagents, 1); + } +} + +#[test] +fn invalid_unicode_scalar_identities_are_unavailable() { + let corpus = corpus(); + for case in corpus["invalidRaw"].as_array().unwrap() { + let wire = format!( + r#"{{"sent":true,"workerCausality":{}}}"#, + case["json"].as_str().unwrap() + ); + let result: TasksSendMessageResult = serde_json::from_str(&wire).unwrap(); + assert!(result.sent); + assert!(result.worker_causality.is_none(), "{}", case["name"]); + } +} diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index e0df048404..c3869a5930 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -396,6 +396,17 @@ function requiresArgumentNullCheck(typeName: string, isRequired: boolean): boole return isRequired && !typeName.endsWith("?") && !isNonNullableCSharpValueType(typeName); } +const WORKER_STRING_UUID_PROPERTIES = new Set([ + "WorkerAdmission.AhpTurnId", + "WorkerEventReference.EventId", + "WorkerInput.QueueItemId", + "WorkerNotificationReference.DeliveryId", +]); + +function isWorkerStringUuidProperty(parentClassName: string, propName: string, schema: JSONSchema7): boolean { + return schema.format === "uuid" && WORKER_STRING_UUID_PROPERTIES.has(`${parentClassName}.${propName}`); +} + async function formatCSharpFile(filePath: string): Promise { try { const projectFile = path.join(REPO_ROOT, "dotnet/src/GitHub.Copilot.SDK.csproj"); @@ -1290,6 +1301,9 @@ function resolveSessionPropertyType( nestedClasses: Map, enumOutput: string[] ): string { + if (isWorkerStringUuidProperty(parentClassName, propName, propSchema)) { + return isRequired ? "string" : "string?"; + } if (isOpaqueJson(propSchema)) { return isRequired ? "JsonElement" : "JsonElement?"; } @@ -1433,6 +1447,9 @@ function generateDataClass(variant: EventVariant, knownTypes: Map))]`); + } lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); @@ -1788,6 +1805,9 @@ function getRpcUnionMatchExpression(schema: JSONSchema7, seenRefs: ReadonlySet))]`); + } lines.push(...emitDataAnnotations(prop, " ", csharpType)); if (isSchemaDeprecated(prop)) pushObsoleteAttributes(lines, " "); if (isSchemaExperimental(prop)) pushExperimentalAttribute(lines, " "); @@ -2450,6 +2473,17 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas escapeDescription: false, }); + // Preserve existing data positions; retain the cancellation position with an overload below. + if (method.rpcMethod === "session.send") { + const index = parameterDescriptions.findIndex(({ name }) => name === "clientCorrelationId"); + if (index !== -1) { + const [parameter] = sigParams.splice(index, 1); + const [description] = parameterDescriptions.splice(index, 1); + sigParams.splice(sigParams.length - 1, 0, parameter); + parameterDescriptions.splice(parameterDescriptions.length - 1, 0, description); + } + } + const taskType = !isVoidSchema(resultSchema) ? `Task<${resultClassName}>` : "Task"; const localRequestName = localRequestVariableName(paramEntries, useRequestParameter); lines.push(""); @@ -2471,6 +2505,20 @@ function emitSessionMethod(key: string, method: RpcMethod, lines: string[], clas } else { lines.push(`${indent} await CopilotClient.InvokeRpcAsync(_session.Rpc, "${method.rpcMethod}", [${localRequestName}], cancellationToken);`, `${indent}}`); } + if (method.rpcMethod === "session.send" && parameterDescriptions.some(({ name }) => name === "clientCorrelationId")) { + // Retain the old CLR signature; defaults on the extended overload preserve source calls. + const previousIndexes = parameterDescriptions.flatMap(({ name }, index) => name === "clientCorrelationId" ? [] : [index]); + const previousParameters = previousIndexes.map((index) => sigParams[index].replace(/ = (?:null|default)$/, "")); + const previousDescriptions = previousIndexes.map((index) => parameterDescriptions[index]); + const previousArguments = previousDescriptions.filter(({ name }) => name !== "cancellationToken").map(({ name }) => name); + lines.push(""); + pushRpcMethodXmlDocs(lines, method, indent, previousDescriptions, resultSchema); + if (method.stability === "experimental" && !groupExperimental) pushExperimentalAttribute(lines, indent); + if (method.deprecated && !groupDeprecated) pushObsoleteAttributes(lines, indent); + lines.push(`${indent}${methodVisibility} ${taskType} ${methodName}Async(${previousParameters.join(", ")})`); + lines.push(`${indent} => ${methodName}Async(${previousArguments.join(", ")}, clientCorrelationId: null, cancellationToken: cancellationToken);`); + } + if (legacyEntries) { const assignments = ["SessionId = _session.SessionId", ...paramEntries.flatMap(([name, schema]) => typeof schema === "object" ? [`${toCSharpPropertyName(name, schema)} = request.${toCSharpPropertyName(name, schema)}`] : [])]; diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index bb52ce6ea0..5924f692fd 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -1139,7 +1139,7 @@ function pushGoStructUnmarshalJSON(lines: string[], typeName: string, fields: Go const unionFields = fields .map((field) => ({ field, unionField: goDiscriminatedUnionField(field.goType, ctx) })) .filter((entry): entry is { field: GoStructField; unionField: GoDiscriminatedUnionField } => entry.unionField !== undefined); - if (unionFields.length === 0) return; + if (unionFields.length === 0 && !fields.some((field) => field.propName === "workerCausality")) return; const blockLines: string[] = []; blockLines.push(`func (r *${typeName}) UnmarshalJSON(data []byte) error {`); @@ -1147,6 +1147,7 @@ function pushGoStructUnmarshalJSON(lines: string[], typeName: string, fields: Go for (const field of fields) { const unionField = goDiscriminatedUnionField(field.goType, ctx); let rawType = field.goType; + if (field.propName === "workerCausality") rawType = "json.RawMessage"; if (unionField?.kind === "single") rawType = "json.RawMessage"; if (unionField?.kind === "slice") rawType = "[]json.RawMessage"; if (unionField?.kind === "map") rawType = "map[string]json.RawMessage"; @@ -1160,6 +1161,12 @@ function pushGoStructUnmarshalJSON(lines: string[], typeName: string, fields: Go for (const field of fields) { const unionField = goDiscriminatedUnionField(field.goType, ctx); + if (field.propName === "workerCausality") { + blockLines.push(`\tif !diagnosticmetadata.ReadWorkerCausality(raw.${field.goName}, &r.${field.goName}) {`); + blockLines.push(`\t\tr.${field.goName} = nil`); + blockLines.push(`\t}`); + continue; + } if (!unionField) { blockLines.push(`\tr.${field.goName} = raw.${field.goName}`); continue; @@ -3087,6 +3094,9 @@ function goGeneratedEncodingFileCode(schemaFileName: string, packageName: string if (generatedEncodingCode.includes("time.Time")) { imports.push(`"time"`); } + if (generatedEncodingCode.includes("diagnosticmetadata.")) { + imports.push(`"github.com/github/copilot-sdk/go/internal/diagnosticmetadata"`); + } if (packageName !== "rpc" && generatedEncodingCode.includes("rpc.")) { imports.push(`"github.com/github/copilot-sdk/go/rpc"`); } @@ -3348,6 +3358,15 @@ export function generateGoSessionEventsCode( .join(""), dataClassName: variant.dataClassName, })) + .concat( + variants.some((variant) => variant.typeName === "workflow.run_updated") + ? [ + { constName: "SessionEventTypeFactoryRunSettled", dataClassName: "FactoryRunSettledData" }, + { constName: "SessionEventTypeFactoryRunStarted", dataClassName: "FactoryRunStartedData" }, + { constName: "SessionEventTypeFactoryRunUpdated", dataClassName: "FactoryRunUpdatedData" }, + ] + : [] + ) .sort((left, right) => left.constName.localeCompare(right.constName)); // Type method diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 0285959f11..a6e49237f6 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -2150,6 +2150,17 @@ function getOrCreatePyEnum( return enumName; } +const WORKER_STRING_UUID_PROPERTIES = new Set([ + "WorkerAdmission.ahpTurnId", + "WorkerEventReference.eventId", + "WorkerInput.queueItemId", + "WorkerNotificationReference.deliveryId", +]); + +function isWorkerStringUuidProperty(parentTypeName: string, jsonPropName: string): boolean { + return WORKER_STRING_UUID_PROPERTIES.has(`${parentTypeName}.${jsonPropName}`); +} + function resolvePyPropertyType( propSchema: JSONSchema7, parentTypeName: string, @@ -2335,6 +2346,10 @@ function resolvePyPropertyType( return isRequired ? resolved : pyOptionalResolvedType(resolved); } if (format === "uuid") { + if (isWorkerStringUuidProperty(parentTypeName, jsonPropName)) { + const resolved = pyPrimitiveResolvedType("str", "from_str"); + return isRequired ? resolved : pyOptionalResolvedType(resolved); + } const resolved = pyPrimitiveResolvedType("UUID", "from_uuid", "to_uuid"); return isRequired ? resolved : pyOptionalResolvedType(resolved); } @@ -2556,6 +2571,10 @@ function emitPyClass( lines.push(` assert isinstance(obj, dict)`); for (const field of fieldInfos) { const sourceExpr = `obj.get(${JSON.stringify(field.jsonName)})`; + if (field.jsonName === "workerCausality" && !field.isRequired) { + lines.push(` ${field.fieldName} = optional_worker_causality(${sourceExpr}, lambda: ${field.resolved.fromExpr(sourceExpr)})`); + continue; + } lines.push( ` ${field.fieldName} = ${field.resolved.fromExpr(sourceExpr)}` ); @@ -2807,6 +2826,7 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string { out.push(`from uuid import UUID`); out.push(``); out.push(`import dateutil.parser`); + out.push(`from .._worker_causality import optional_worker_causality`); out.push(``); out.push(`T = TypeVar("T")`); out.push(`EnumT = TypeVar("EnumT", bound=Enum)`); @@ -3476,11 +3496,16 @@ from typing import Any, Protocol, TypeVar, cast from uuid import UUID import dateutil.parser +from .._worker_causality import optional_worker_causality T = TypeVar("T") EnumT = TypeVar("EnumT", bound=Enum) `); + typesCode = typesCode.replace( + /^(\s*)worker_causality = (.*obj\.get\("workerCausality"\).*)$/gm, + '$1worker_causality = optional_worker_causality(obj.get("workerCausality"), lambda: $2)' + ); lines.push(typesCode); if (publicTypeAliases.size > 0) { lines.push(""); diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index a9aafa681b..6091436ed7 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -198,6 +198,10 @@ const RUST_KEYWORDS = new Set([ "while", "yield", ]); +const RAW_WORKER_CAUSALITY_TYPES = new Set([ + "TasksSendMessageResult", + "SessionTasksSendMessageResult", +]); function safeRustFieldName(name: string): string { const snake = toRustFieldName(name); @@ -1127,7 +1131,12 @@ function emitRustStruct( lines.push(` #[serde(rename = "${propName}")]`); } - if (prop.$ref && typeof prop.const === "string") { + if (propName === "workerCausality" && !isReq) { + const workerDeserializer = RAW_WORKER_CAUSALITY_TYPES.has(typeName) + ? "deserialize_optional_raw" + : "deserialize_optional"; + lines.push(` #[serde(default, deserialize_with = "crate::worker_causality::${workerDeserializer}")]`); + } else if (prop.$ref && typeof prop.const === "string") { lines.push( ` #[serde(${isReq ? "" : "default, "}deserialize_with = "${typeName}::deserialize_${snakeField}")]`, ); @@ -1411,6 +1420,7 @@ export function generateSessionEventsCode(schema: JSONSchema7): string { { allowUntaggedUnions: true, allowedUnionTypeNames: [ + "SystemNotification", "ToolExecutionCompleteContent", "ToolExecutionCompleteContentResourceDetails", ], diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index f1a59faa1b..da28d3f6aa 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -726,6 +726,7 @@ async function generateRpc(schemaPath?: string, sessionEventsSchema?: JSONSchema */ import type { MessageConnection } from "vscode-jsonrpc/node.js"; +import { withWorkerCausality, withWorkerCausalityEvents } from "../workerCausality.js"; `); const externalSchemaRefs = collectExternalSchemaRefNames(schema); @@ -1033,7 +1034,17 @@ function emitGroup( includeExperimental: (value as RpcMethod).stability === "experimental" && !parentExperimental, }); lines.push(`${indent}${key}: async (${sigParams.join(", ")}): Promise<${resultType}> =>`); - lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); + if (rpcMethod === "session.tasks.sendMessage") { + lines.push(`${indent} connection.sendRequest<${resultType}>("${rpcMethod}", ${bodyArg}).then(withWorkerCausality),`); + } else if ( + rpcMethod === "session.getMessages" || + rpcMethod === "session.eventLog.read" || + rpcMethod === "sessions.readPersistedEvents" + ) { + lines.push(`${indent} connection.sendRequest<${resultType}>("${rpcMethod}", ${bodyArg}).then(withWorkerCausalityEvents),`); + } else { + lines.push(`${indent} connection.sendRequest("${rpcMethod}", ${bodyArg}),`); + } } else if (typeof value === "object" && value !== null) { const groupExperimental = isNodeFullyExperimental(value as Record); const groupDeprecated = isNodeFullyDeprecated(value as Record); diff --git a/test/worker-causality.json b/test/worker-causality.json new file mode 100644 index 0000000000..ecd61c3694 --- /dev/null +++ b/test/worker-causality.json @@ -0,0 +1,2037 @@ +{ + "version": 1, + "producerEvidence": { + "kind": "runtime_actual_output_conformance", + "sourceTests": [ + "src/runtime/src/subagents/orchestrator/worker_causality_tests.rs", + "src/runtime/src/session/session_notification_delivery/worker_causality_tests.rs", + "src/runtime/src/shared_api/tasks/tests.rs", + "test/cli/ahp/client/relaySession.test.ts", + "src/runtime/src/support/events/tests.rs" + ], + "vectors": [ + "native_nested_user_message", + "queued_notification", + "two_source_assistant_turn_start", + "tasks_send_message_result", + "workflow_completed", + "legacy_factory_completed_history" + ], + "canonicalization": "volatile UUID envelope and identity values are replaced by a fixed token before structural comparison; fixed product content, placement, ordering and typed fields remain exact" + }, + "valid": [ + { + "name": "native_nested_user_message", + "event": { + "type": "user.message", + "data": { + "content": "test input", + "messageId": "bb61b7c0-bc90-4290-a1e4-79558dc35139", + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "2dbbe116-73b0-4011-bfa3-3b571b41404b", + "agentId": "recipient", + "sender": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "senderBridges": [ + { + "source": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "ce52204a-54b0-4ebd-ba13-4854598c4399", + "eventId": "69362832-885b-4787-ab3e-1a2f6c17c73b", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "f651ee35-bdb9-4c91-aa21-0057a9446269", + "eventId": "b13511dc-0c7a-4d9c-9618-c04c1c50c4bc", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + } + ] + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "bb61b7c0-bc90-4290-a1e4-79558dc35139" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + "agentId": "recipient", + "id": "e31ee08e-c3ea-4720-b0d6-c7435b5a429c", + "timestamp": "2026-09-26T00:10:18.598Z", + "parentId": null + } + }, + { + "name": "ahp_coordinator_user_message", + "event": { + "type": "user.message", + "data": { + "content": "first prompt", + "turnId": "ffcc4a93-3bc1-4f78-ac2b-c47f6948b20c", + "messageId": "efbe1be8-3082-45c5-8c02-8dd922db78c0", + "workerCausality": { + "version": 1, + "observationProvenance": "ahp_coordinator", + "sources": [ + { + "input": { + "queueItemId": "9ce2ac75-5062-457e-abe2-11f0f950e5b9", + "agentId": "ahp-agent" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "efbe1be8-3082-45c5-8c02-8dd922db78c0", + "ahpTurnId": "ffcc4a93-3bc1-4f78-ac2b-c47f6948b20c" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + "agentId": "ahp-agent", + "id": "9a2a348c-d403-4153-9554-3acc2ca61ed9", + "timestamp": "2026-09-26T00:10:18.610Z", + "parentId": null + } + }, + { + "name": "queued_notification", + "event": { + "type": "system.notification", + "data": { + "content": "\nAgent \"worker\" (general-purpose) has finished processing and is now idle.\n", + "kind": { + "type": "agent_idle", + "agentId": "worker", + "agentType": "general-purpose" + }, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "bb31eee3-11b8-41bb-b392-b5d4f25061be", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "c7b4146b-6a6a-40e7-873a-6e67438a7923", + "event": { + "sessionId": "b9230967-dd47-416e-bc03-c470afd2c87d", + "eventId": "33db0052-4391-4418-a46e-26e9bac8d5a9", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true, + "completion": { + "sessionId": "b9230967-dd47-416e-bc03-c470afd2c87d", + "eventId": "30f8471f-343a-44b8-ae95-86c8eed36afc", + "agentId": "worker", + "eventType": "subagent.completed", + "provenance": "native" + }, + "notification": { + "deliveryId": "9cef2f54-c16f-49a8-a2cd-58da52108e60", + "mode": "queued" + } + } + ], + "captureComplete": true + } + }, + "id": "3943fdab-2f6a-4b52-9b1f-e23a449fa8bd", + "timestamp": "2026-09-26T00:01:00.730Z", + "parentId": "30f8471f-343a-44b8-ae95-86c8eed36afc" + } + }, + { + "name": "immediate_notification", + "event": { + "type": "system.notification", + "data": { + "content": "\nAgent \"worker-b\" (general-purpose) has finished processing and is now idle.\n", + "kind": { + "type": "agent_idle", + "agentId": "worker-b", + "agentType": "general-purpose" + }, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "68c1fe08-f176-4fe9-acca-3e1714bee0d0", + "agentId": "worker-b" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "f304b273-03ec-4de9-b7fb-c1c20c4a8b44", + "event": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "0be71b00-c858-49b6-b913-70ebd421f398", + "agentId": "worker-b", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true, + "completion": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "bff8358e-e988-4488-967f-5d66ae754df5", + "agentId": "worker-b", + "eventType": "subagent.completed", + "provenance": "native" + }, + "notification": { + "deliveryId": "fdac777b-3e92-4b35-94ab-d3ec905b8d93", + "mode": "immediate" + }, + "admittedDuring": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "3b105bff-0011-4658-9855-70d64f5d6eef", + "eventType": "assistant.turn_start", + "provenance": "native" + } + } + ], + "captureComplete": true + } + }, + "id": "5d7a225b-b974-4acb-a701-c626dfc4292c", + "timestamp": "2026-09-26T00:01:00.735Z", + "parentId": "3b105bff-0011-4658-9855-70d64f5d6eef" + } + }, + { + "name": "two_source_assistant_turn_start", + "event": { + "type": "assistant.turn_start", + "data": { + "turnId": "1", + "interactionId": "test-interaction", + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "68c1fe08-f176-4fe9-acca-3e1714bee0d0", + "agentId": "worker-b" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "f304b273-03ec-4de9-b7fb-c1c20c4a8b44", + "event": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "0be71b00-c858-49b6-b913-70ebd421f398", + "agentId": "worker-b", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true, + "completion": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "bff8358e-e988-4488-967f-5d66ae754df5", + "agentId": "worker-b", + "eventType": "subagent.completed", + "provenance": "native" + }, + "notification": { + "deliveryId": "fdac777b-3e92-4b35-94ab-d3ec905b8d93", + "event": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "5d7a225b-b974-4acb-a701-c626dfc4292c", + "eventType": "system.notification", + "provenance": "native" + }, + "mode": "immediate" + }, + "admittedDuring": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "3b105bff-0011-4658-9855-70d64f5d6eef", + "eventType": "assistant.turn_start", + "provenance": "native" + } + }, + { + "input": { + "queueItemId": "845ea619-b3d7-4371-922f-fa6e2b459aec", + "agentId": "worker-a" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "15fb0cf7-b9c0-4780-b220-eaec4394c2c4", + "event": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "81725409-6a40-4865-b7a6-8a28bb2a86b2", + "agentId": "worker-a", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true, + "completion": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "523256b3-a0fa-47dc-a20e-897532a63282", + "agentId": "worker-a", + "eventType": "subagent.completed", + "provenance": "native" + }, + "notification": { + "deliveryId": "30a214f7-26e5-42d1-ac0a-907045470ce5", + "event": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "de0ae0b2-2b8a-4f8d-825c-9ffc8ce9717a", + "eventType": "system.notification", + "provenance": "native" + }, + "mode": "immediate" + }, + "admittedDuring": { + "sessionId": "804ee2d2-9d44-40be-bc3a-4276ede7a258", + "eventId": "3b105bff-0011-4658-9855-70d64f5d6eef", + "eventType": "assistant.turn_start", + "provenance": "native" + } + } + ], + "captureComplete": true + } + }, + "id": "fa1ff4f8-da1b-4cd6-88cb-9ab17f9e2dd7", + "timestamp": "2026-09-26T00:01:00.739Z", + "parentId": "0473e682-62c4-4967-88ea-4ee0bc7b272f" + } + }, + { + "name": "incomplete_no_completion_start", + "event": { + "type": "assistant.turn_start", + "data": { + "turnId": "0", + "interactionId": "e4ee7454-40c1-43a8-9f23-24ef9fb5dccf", + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "fe58ed76-866d-4c98-96f6-6991bb4c1d91", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "a9b49f68-fd65-40ba-86dc-c5fae4fe62c3", + "event": { + "sessionId": "496e2310-08bb-4a12-9cf9-4d45250b9137", + "eventId": "b3a0df0d-1cbd-4abe-b017-b3a8f61a2679", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": false, + "notification": { + "deliveryId": "b28ffb50-7a63-4bd0-86dc-de1b8f2467a1", + "event": { + "sessionId": "496e2310-08bb-4a12-9cf9-4d45250b9137", + "eventId": "78370a8b-9ffc-4a0f-ad16-057779fe6351", + "eventType": "system.notification", + "provenance": "native" + }, + "mode": "queued" + } + } + ], + "captureComplete": false + } + }, + "id": "041d5901-34b8-4b5e-a5c3-0567150caa75", + "timestamp": "2026-09-26T00:01:00.736Z", + "parentId": "4348bb60-8821-4af3-add3-22461951d959" + } + }, + { + "name": "tasks_send_message_result", + "result": { + "sent": true, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + } + }, + { + "name": "session_id_256_multibyte", + "result": { + "sent": true, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true, + "completion": { + "sessionId": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé", + "eventId": "44444444-4444-4444-8444-444444444444", + "agentId": "worker", + "eventType": "subagent.completed", + "provenance": "native" + } + } + ], + "captureComplete": true + } + } + }, + { + "name": "agent_id_256_multibyte", + "result": { + "sent": true, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + } + }, + { + "name": "message_id_256_multibyte", + "result": { + "sent": true, + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + } + }, + { + "name": "implicit_self_wire_fits_but_expansion_exceeds_budget", + "event": { + "type": "user.message", + "data": { + "content": "test input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "workerCausality": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm" + }, + { + "kind": "queued_input", + "messageId": "00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "id": "22222222-2222-4222-8222-222222222222", + "timestamp": "2026-09-26T00:10:18.598Z", + "parentId": null + }, + "placementValid": false, + "wireBytes": 3540, + "expandedBytes": 4188 + } + ], + "invalid": [ + { + "name": "null", + "value": null + }, + { + "name": "non_object", + "value": true + }, + { + "name": "unsupported_version", + "value": { + "version": 2, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "unknown_top_level_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true, + "future": true + } + }, + { + "name": "unknown_source_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true, + "future": true + } + ], + "captureComplete": true + } + }, + { + "name": "unknown_input_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker", + "future": true + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "unknown_admission_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "2dbbe116-73b0-4011-bfa3-3b571b41404b", + "agentId": "recipient", + "sender": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "senderBridges": [ + { + "source": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "ce52204a-54b0-4ebd-ba13-4854598c4399", + "eventId": "69362832-885b-4787-ab3e-1a2f6c17c73b", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "f651ee35-bdb9-4c91-aa21-0057a9446269", + "eventId": "b13511dc-0c7a-4d9c-9618-c04c1c50c4bc", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + } + ] + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "bb61b7c0-bc90-4290-a1e4-79558dc35139", + "future": true + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "unknown_reference_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "2dbbe116-73b0-4011-bfa3-3b571b41404b", + "agentId": "recipient", + "sender": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native", + "future": true + }, + "senderBridges": [ + { + "source": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "ce52204a-54b0-4ebd-ba13-4854598c4399", + "eventId": "69362832-885b-4787-ab3e-1a2f6c17c73b", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "f651ee35-bdb9-4c91-aa21-0057a9446269", + "eventId": "b13511dc-0c7a-4d9c-9618-c04c1c50c4bc", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + } + ] + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "bb61b7c0-bc90-4290-a1e4-79558dc35139" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "unknown_bridge_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "2dbbe116-73b0-4011-bfa3-3b571b41404b", + "agentId": "recipient", + "sender": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "senderBridges": [ + { + "source": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "future": true + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "ce52204a-54b0-4ebd-ba13-4854598c4399", + "eventId": "69362832-885b-4787-ab3e-1a2f6c17c73b", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "f651ee35-bdb9-4c91-aa21-0057a9446269", + "eventId": "b13511dc-0c7a-4d9c-9618-c04c1c50c4bc", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + } + ] + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "bb61b7c0-bc90-4290-a1e4-79558dc35139" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "unknown_notification_field", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "bb31eee3-11b8-41bb-b392-b5d4f25061be", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "c7b4146b-6a6a-40e7-873a-6e67438a7923", + "event": { + "sessionId": "b9230967-dd47-416e-bc03-c470afd2c87d", + "eventId": "33db0052-4391-4418-a46e-26e9bac8d5a9", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true, + "completion": { + "sessionId": "b9230967-dd47-416e-bc03-c470afd2c87d", + "eventId": "30f8471f-343a-44b8-ae95-86c8eed36afc", + "agentId": "worker", + "eventType": "subagent.completed", + "provenance": "native" + }, + "notification": { + "deliveryId": "9cef2f54-c16f-49a8-a2cd-58da52108e60", + "mode": "queued", + "future": true + } + } + ], + "captureComplete": true + } + }, + { + "name": "malformed_queue_uuid", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "not-a-uuid", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "contradictory_complete_capture", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "fe58ed76-866d-4c98-96f6-6991bb4c1d91", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "a9b49f68-fd65-40ba-86dc-c5fae4fe62c3", + "event": { + "sessionId": "496e2310-08bb-4a12-9cf9-4d45250b9137", + "eventId": "b3a0df0d-1cbd-4abe-b017-b3a8f61a2679", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": false, + "notification": { + "deliveryId": "b28ffb50-7a63-4bd0-86dc-de1b8f2467a1", + "event": { + "sessionId": "496e2310-08bb-4a12-9cf9-4d45250b9137", + "eventId": "78370a8b-9ffc-4a0f-ad16-057779fe6351", + "eventType": "system.notification", + "provenance": "native" + }, + "mode": "queued" + } + } + ], + "captureComplete": true + } + }, + { + "name": "missing_required_sources", + "value": { + "version": 1, + "observationProvenance": "native", + "captureComplete": false + } + }, + { + "name": "session_id_257_bytes", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "2dbbe116-73b0-4011-bfa3-3b571b41404b", + "agentId": "recipient", + "sender": { + "sessionId": "sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "senderBridges": [ + { + "source": { + "sessionId": "0187f098-31d3-4cdc-ab71-e6ab387e01b4", + "eventId": "773e548e-64e5-4418-853b-aabf128ce858", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "ce52204a-54b0-4ebd-ba13-4854598c4399", + "eventId": "69362832-885b-4787-ab3e-1a2f6c17c73b", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + }, + { + "source": { + "sessionId": "23ac4baf-eb8e-4fdc-b8e0-5633e4ed550b", + "eventId": "8f608be4-c132-4e19-8bdd-82ff5ba9b0b8", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + }, + "reported": { + "sessionId": "f651ee35-bdb9-4c91-aa21-0057a9446269", + "eventId": "b13511dc-0c7a-4d9c-9618-c04c1c50c4bc", + "agentId": "leaf-agent", + "eventType": "tool.execution_start", + "provenance": "native" + } + } + ] + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "bb61b7c0-bc90-4290-a1e4-79558dc35139" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "agent_id_257_bytes", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "message_id_257_bytes", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm" + }, + { + "kind": "queued_input", + "messageId": "00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "01xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "02xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "03xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "source", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "session_id_257_multibyte", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true, + "completion": { + "sessionId": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééx", + "eventId": "44444444-4444-4444-8444-444444444444", + "agentId": "worker", + "eventType": "subagent.completed", + "provenance": "native" + } + } + ], + "captureComplete": true + } + }, + { + "name": "agent_id_257_multibyte", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééx" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "message_id_257_multibyte", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "ééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééx" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "session_id_control_character", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [], + "captureComplete": true, + "completion": { + "sessionId": "session\u0000id", + "eventId": "44444444-4444-4444-8444-444444444444", + "agentId": "worker", + "eventType": "subagent.completed", + "provenance": "native" + } + } + ], + "captureComplete": true + } + }, + { + "name": "agent_id_control_character", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker\nagent" + }, + "admissions": [], + "captureComplete": true + } + ], + "captureComplete": true + } + }, + { + "name": "message_id_control_character", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "message…id" + } + ], + "captureComplete": true + } + ], + "captureComplete": true + } + } + ], + "boundaries": [ + { + "name": "exact_4096_ascii", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + }, + "accepted": true + }, + { + "name": "exact_4096_u2028", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + }, + "accepted": true + }, + { + "name": "exact_4096_u2029", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + }, + "accepted": true + }, + { + "name": "4097_ascii", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + }, + "accepted": false + }, + { + "name": "exact_4096_utf8_and_escaping", + "value": { + "version": 1, + "observationProvenance": "native", + "sources": [ + { + "input": { + "queueItemId": "46fc3a43-7f19-4b0a-9e32-d5ef4dc25552", + "agentId": "worker" + }, + "admissions": [ + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + }, + { + "kind": "queued_input", + "messageId": "é🐈\"\\Ωxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "event": { + "sessionId": "s", + "eventId": "33333333-3333-4333-8333-333333333333", + "agentId": "worker", + "eventType": "user.message", + "provenance": "native" + } + } + ], + "captureComplete": true + } + ], + "captureComplete": true + }, + "accepted": true + } + ], + "identityBoundaries": { + "utf8MaxBytes": 256, + "acceptedBytes": 256, + "rejectedBytes": 257, + "multibyteRejectedBytes": 257, + "multibyteAcceptedBytes": 256, + "multibyteAcceptedFields": [ + "sessionId", + "agentId", + "messageId" + ], + "multibyteRejectedFields": [ + "sessionId", + "agentId", + "messageId" + ], + "rejectUnicodeControlCharacters": true, + "requireValidUnicodeScalarValues": true + }, + "expandedGraphBudget": { + "maxBytes": 4096, + "wireBytes": 3540, + "expandedBytes": 4188, + "domain": "fully_materialized_workerCausality_field" + }, + "workflowCompleted": { + "hostInput": { + "kind": "systemNotification", + "content": "Factory run completed", + "metadata": { + "type": "factory_completed", + "runId": "run-1", + "factoryName": "fix-ci", + "status": "completed", + "attempt": 2, + "elapsedMs": 500, + "consumedNanoAiu": 10, + "consumedSubagents": 1 + } + }, + "event": { + "type": "system.notification", + "data": { + "content": "Factory run completed", + "kind": { + "type": "workflow_completed", + "runId": "run-1", + "workflowName": "fix-ci", + "status": "completed", + "attempt": 2, + "elapsedMs": 500, + "consumedNanoAiu": 10, + "consumedSubagents": 1 + } + }, + "id": "55555555-5555-4555-8555-555555555555", + "timestamp": "2026-09-26T00:02:00.000Z", + "parentId": null + } + }, + "invalidRaw": [ + { + "name": "session_id_unpaired_high_surrogate", + "field": "sessionId", + "json": "{\"version\":1,\"observationProvenance\":\"native\",\"sources\":[{\"input\":{\"queueItemId\":\"46fc3a43-7f19-4b0a-9e32-d5ef4dc25552\",\"agentId\":\"worker\"},\"admissions\":[],\"captureComplete\":true,\"completion\":{\"sessionId\":\"\\ud800\",\"eventId\":\"44444444-4444-4444-8444-444444444444\",\"agentId\":\"worker\",\"eventType\":\"subagent.completed\",\"provenance\":\"native\"}}],\"captureComplete\":true}" + }, + { + "name": "agent_id_unpaired_low_surrogate", + "field": "agentId", + "json": "{\"version\":1,\"observationProvenance\":\"native\",\"sources\":[{\"input\":{\"queueItemId\":\"46fc3a43-7f19-4b0a-9e32-d5ef4dc25552\",\"agentId\":\"\\udc00\"},\"admissions\":[],\"captureComplete\":true}],\"captureComplete\":true}" + }, + { + "name": "message_id_unpaired_high_surrogate", + "field": "messageId", + "json": "{\"version\":1,\"observationProvenance\":\"native\",\"sources\":[{\"input\":{\"queueItemId\":\"46fc3a43-7f19-4b0a-9e32-d5ef4dc25552\",\"agentId\":\"worker\"},\"admissions\":[{\"kind\":\"queued_input\",\"messageId\":\"\\ud800\"}],\"captureComplete\":true}],\"captureComplete\":true}" + } + ], + "legacyWorkflowCompleted": { + "persistedEvent": { + "type": "system.notification", + "data": { + "content": "Factory run completed", + "kind": { + "type": "factory_completed", + "runId": "run-1", + "status": "completed", + "attempt": 2, + "elapsedMs": 500, + "consumedNanoAiu": 10, + "consumedSubagents": 1, + "factoryName": "fix-ci" + } + }, + "id": "55555555-5555-4555-8555-555555555555", + "timestamp": "2026-09-26T00:02:00.000Z", + "parentId": null + }, + "event": { + "type": "system.notification", + "data": { + "content": "Factory run completed", + "kind": { + "type": "workflow_completed", + "runId": "run-1", + "workflowName": "fix-ci", + "status": "completed", + "attempt": 2, + "elapsedMs": 500, + "consumedNanoAiu": 10, + "consumedSubagents": 1 + } + }, + "id": "55555555-5555-4555-8555-555555555555", + "timestamp": "2026-09-26T00:02:00.000Z", + "parentId": null + } + } +}