Skip to content

Commit 9d1d617

Browse files
stephentoubCopilot
andcommitted
Derive session event envelopes across SDKs
Share schema-level session event envelope discovery across code generators and use it for Go and Python session-event wrappers so top-level envelope fields like agentId round-trip consistently. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2c7ee08 commit 9d1d617

8 files changed

Lines changed: 340 additions & 161 deletions

File tree

go/generated_session_events.go

Lines changed: 20 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package copilot
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
func TestSessionEventAgentIDRoundTripsKnownEvent(t *testing.T) {
9+
event, err := UnmarshalSessionEvent([]byte(`{
10+
"id": "00000000-0000-0000-0000-000000000001",
11+
"timestamp": "2026-01-01T00:00:00Z",
12+
"parentId": null,
13+
"agentId": "agent-1",
14+
"type": "user.message",
15+
"data": {
16+
"content": "Hello"
17+
}
18+
}`))
19+
if err != nil {
20+
t.Fatalf("failed to unmarshal session event: %v", err)
21+
}
22+
23+
if event.AgentID == nil || *event.AgentID != "agent-1" {
24+
t.Fatalf("expected agent ID to round-trip, got %v", event.AgentID)
25+
}
26+
if _, ok := event.Data.(*UserMessageData); !ok {
27+
t.Fatalf("expected user message data, got %T", event.Data)
28+
}
29+
30+
data, err := event.Marshal()
31+
if err != nil {
32+
t.Fatalf("failed to marshal session event: %v", err)
33+
}
34+
35+
var serialized map[string]any
36+
if err := json.Unmarshal(data, &serialized); err != nil {
37+
t.Fatalf("failed to unmarshal serialized session event: %v", err)
38+
}
39+
if serialized["agentId"] != "agent-1" {
40+
t.Fatalf("expected serialized agentId to round-trip, got %v", serialized["agentId"])
41+
}
42+
}
43+
44+
func TestSessionEventAgentIDRoundTripsUnknownEvent(t *testing.T) {
45+
event, err := UnmarshalSessionEvent([]byte(`{
46+
"id": "00000000-0000-0000-0000-000000000002",
47+
"timestamp": "2026-01-01T00:00:00Z",
48+
"parentId": null,
49+
"agentId": "future-agent",
50+
"type": "future.feature_from_server",
51+
"data": {
52+
"key": "value"
53+
}
54+
}`))
55+
if err != nil {
56+
t.Fatalf("failed to unmarshal session event: %v", err)
57+
}
58+
59+
if event.AgentID == nil || *event.AgentID != "future-agent" {
60+
t.Fatalf("expected agent ID to round-trip, got %v", event.AgentID)
61+
}
62+
if _, ok := event.Data.(*RawSessionEventData); !ok {
63+
t.Fatalf("expected raw session event data, got %T", event.Data)
64+
}
65+
66+
data, err := event.Marshal()
67+
if err != nil {
68+
t.Fatalf("failed to marshal session event: %v", err)
69+
}
70+
71+
var serialized map[string]any
72+
if err := json.Unmarshal(data, &serialized); err != nil {
73+
t.Fatalf("failed to unmarshal serialized session event: %v", err)
74+
}
75+
if serialized["agentId"] != "future-agent" {
76+
t.Fatalf("expected serialized agentId to round-trip, got %v", serialized["agentId"])
77+
}
78+
}

python/copilot/generated/session_events.py

Lines changed: 10 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

python/test_event_forward_compatibility.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
UserMessageAgentMode,
2525
UserMessageAttachmentGithubReferenceType,
2626
session_event_from_dict,
27+
session_event_to_dict,
2728
)
2829

2930

@@ -47,6 +48,39 @@ def test_unknown_event_type_maps_to_unknown(self):
4748
event = session_event_from_dict(unknown_event)
4849
assert event.type == SessionEventType.UNKNOWN, f"Expected UNKNOWN, got {event.type}"
4950

51+
def test_known_event_preserves_top_level_agent_id(self):
52+
"""Known events should preserve the top-level sub-agent envelope ID."""
53+
known_event = {
54+
"id": str(uuid4()),
55+
"timestamp": datetime.now().isoformat(),
56+
"parentId": None,
57+
"agentId": "agent-1",
58+
"type": "user.message",
59+
"data": {"content": "Hello"},
60+
}
61+
62+
event = session_event_from_dict(known_event)
63+
assert event.agent_id == "agent-1"
64+
assert session_event_to_dict(event)["agentId"] == "agent-1"
65+
66+
def test_unknown_event_preserves_top_level_agent_id(self):
67+
"""Unknown events should preserve the top-level sub-agent envelope ID."""
68+
unknown_event = {
69+
"id": str(uuid4()),
70+
"timestamp": datetime.now().isoformat(),
71+
"parentId": None,
72+
"agentId": "future-agent",
73+
"type": "session.future_feature_from_server",
74+
"data": {"key": "value"},
75+
}
76+
77+
event = session_event_from_dict(unknown_event)
78+
assert event.type == SessionEventType.UNKNOWN
79+
assert event.agent_id == "future-agent"
80+
serialized = session_event_to_dict(event)
81+
assert serialized["agentId"] == "future-agent"
82+
assert serialized["type"] == "session.future_feature_from_server"
83+
5084
def test_malformed_uuid_raises_error(self):
5185
"""Malformed UUIDs should raise ValueError for visibility, not be suppressed."""
5286
malformed_event = {

scripts/codegen/csharp.ts

Lines changed: 5 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ import {
3131
isObjectSchema,
3232
isVoidSchema,
3333
getNullableInner,
34+
getSessionEventVariantSchemas,
35+
getSharedSessionEventEnvelopeProperties,
3436
REPO_ROOT,
3537
type ApiSchema,
3638
type DefinitionCollections,
3739
type RpcMethod,
40+
type SessionEventEnvelopeProperty,
3841
} from "./utils.js";
3942

4043
const execFileAsync = promisify(execFile);
@@ -317,12 +320,6 @@ interface EventVariant {
317320
dataDescription?: string;
318321
}
319322

320-
interface EventEnvelopeProperty {
321-
name: string;
322-
schema: JSONSchema7;
323-
required: boolean;
324-
}
325-
326323
let generatedEnums = new Map<string, { enumName: string; values: string[] }>();
327324

328325
/** Schema definitions available during session event generation (for $ref resolution). */
@@ -369,63 +366,6 @@ function extractEventVariants(schema: JSONSchema7): EventVariant[] {
369366
});
370367
}
371368

372-
function getSessionEventVariantSchemas(
373-
schema: JSONSchema7,
374-
definitionCollections: DefinitionCollections = collectDefinitionCollections(schema as Record<string, unknown>)
375-
): JSONSchema7[] {
376-
const sessionEvent =
377-
resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ??
378-
resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections);
379-
if (!sessionEvent?.anyOf) throw new Error("Schema must have SessionEvent definition with anyOf");
380-
381-
return sessionEvent.anyOf.map((variant) => {
382-
const resolvedVariant =
383-
resolveObjectSchema(variant as JSONSchema7, definitionCollections) ??
384-
resolveSchema(variant as JSONSchema7, definitionCollections) ??
385-
(variant as JSONSchema7);
386-
if (typeof resolvedVariant !== "object" || !resolvedVariant.properties) throw new Error("Invalid variant");
387-
return resolvedVariant;
388-
});
389-
}
390-
391-
function getSharedEventEnvelopeProperties(schema: JSONSchema7): EventEnvelopeProperty[] {
392-
const variants = getSessionEventVariantSchemas(schema, sessionDefinitions);
393-
const firstVariant = variants[0];
394-
const firstProperties = firstVariant.properties ?? {};
395-
396-
return Object.entries(firstProperties)
397-
.filter(([name]) => name !== "type" && name !== "data")
398-
.map(([name]) => {
399-
const propertySchemas = variants
400-
.map((variant) => variant.properties?.[name])
401-
.filter((propSchema): propSchema is JSONSchema7 => typeof propSchema === "object" && propSchema !== null);
402-
403-
if (propertySchemas.length !== variants.length) return undefined;
404-
405-
return {
406-
name,
407-
schema: selectEnvelopePropertySchema(propertySchemas),
408-
required: variants.every((variant) => (variant.required ?? []).includes(name)),
409-
};
410-
})
411-
.filter((property): property is EventEnvelopeProperty => property !== undefined);
412-
}
413-
414-
function selectEnvelopePropertySchema(propertySchemas: JSONSchema7[]): JSONSchema7 {
415-
// Some variants further constrain a shared envelope property, e.g. ephemeral const true.
416-
// Generate the base property from the least restrictive schema that has useful metadata.
417-
return (
418-
propertySchemas.find((schema) => !isConstOrEnumSchema(schema) && schema.description) ??
419-
propertySchemas.find((schema) => !isConstOrEnumSchema(schema)) ??
420-
propertySchemas.find((schema) => schema.description) ??
421-
propertySchemas[0]
422-
);
423-
}
424-
425-
function isConstOrEnumSchema(schema: JSONSchema7): boolean {
426-
return "const" in schema || (Array.isArray(schema.enum) && schema.enum.length > 0);
427-
}
428-
429369
/**
430370
* Find a discriminator property shared by all variants in an anyOf.
431371
*/
@@ -732,7 +672,7 @@ function generateDataClass(variant: EventVariant, knownTypes: Map<string, string
732672
}
733673

734674
function emitSessionEventEnvelopeProperty(
735-
property: EventEnvelopeProperty,
675+
property: SessionEventEnvelopeProperty,
736676
knownTypes: Map<string, string>,
737677
nestedClasses: Map<string, string>,
738678
enumOutput: string[]
@@ -767,7 +707,7 @@ function generateSessionEventsCode(schema: JSONSchema7): string {
767707
const knownTypes = new Map<string, string>();
768708
const nestedClasses = new Map<string, string>();
769709
const enumOutput: string[] = [];
770-
const envelopeProperties = getSharedEventEnvelopeProperties(schema);
710+
const envelopeProperties = getSharedSessionEventEnvelopeProperties(schema, sessionDefinitions);
771711

772712
const lines: string[] = [];
773713
lines.push(`${COPYRIGHT}

0 commit comments

Comments
 (0)