diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs
index 7842f77ef..d4eddd94d 100644
--- a/dotnet/src/Generated/SessionEvents.cs
+++ b/dotnet/src/Generated/SessionEvents.cs
@@ -103,22 +103,27 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(UserMessageEvent), "user.message")]
public partial class SessionEvent
{
+ /// Sub-agent instance identifier. Absent for events from the root/main agent and session-level events.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("agentId")]
+ public string? AgentId { get; set; }
+
+ /// When true, the event is transient and not persisted to the session event log on disk.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("ephemeral")]
+ public bool? Ephemeral { get; set; }
+
/// Unique event identifier (UUID v4), generated when the event is emitted.
[JsonPropertyName("id")]
public Guid Id { get; set; }
- /// ISO 8601 timestamp when the event was created.
- [JsonPropertyName("timestamp")]
- public DateTimeOffset Timestamp { get; set; }
-
/// ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
[JsonPropertyName("parentId")]
public Guid? ParentId { get; set; }
- /// When true, the event is transient and not persisted to the session event log on disk.
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("ephemeral")]
- public bool? Ephemeral { get; set; }
+ /// ISO 8601 timestamp when the event was created.
+ [JsonPropertyName("timestamp")]
+ public DateTimeOffset Timestamp { get; set; }
///
/// The event type discriminator.
diff --git a/dotnet/test/ForwardCompatibilityTests.cs b/dotnet/test/ForwardCompatibilityTests.cs
index d3f5b7785..71df3f0ea 100644
--- a/dotnet/test/ForwardCompatibilityTests.cs
+++ b/dotnet/test/ForwardCompatibilityTests.cs
@@ -20,6 +20,7 @@ public void FromJson_KnownEventType_DeserializesNormally()
"id": "00000000-0000-0000-0000-000000000001",
"timestamp": "2026-01-01T00:00:00Z",
"parentId": null,
+ "agentId": "agent-1",
"type": "user.message",
"data": {
"content": "Hello"
@@ -31,6 +32,7 @@ public void FromJson_KnownEventType_DeserializesNormally()
Assert.IsType(result);
Assert.Equal("user.message", result.Type);
+ Assert.Equal("agent-1", result.AgentId);
}
[Fact]
@@ -41,6 +43,7 @@ public void FromJson_UnknownEventType_ReturnsBaseSessionEvent()
"id": "12345678-1234-1234-1234-123456789abc",
"timestamp": "2026-06-15T10:30:00Z",
"parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
+ "agentId": "future-agent",
"type": "future.feature_from_server",
"data": { "key": "value" }
}
@@ -50,6 +53,7 @@ public void FromJson_UnknownEventType_ReturnsBaseSessionEvent()
Assert.IsType(result);
Assert.Equal("unknown", result.Type);
+ Assert.Equal("future-agent", result.AgentId);
}
[Fact]
diff --git a/dotnet/test/SessionEventSerializationTests.cs b/dotnet/test/SessionEventSerializationTests.cs
index 93e5ae935..0651e2420 100644
--- a/dotnet/test/SessionEventSerializationTests.cs
+++ b/dotnet/test/SessionEventSerializationTests.cs
@@ -18,6 +18,7 @@ public class SessionEventSerializationTests
Id = Guid.Parse("11111111-1111-1111-1111-111111111111"),
Timestamp = DateTimeOffset.Parse("2026-03-15T21:26:02.642Z"),
ParentId = Guid.Parse("22222222-2222-2222-2222-222222222222"),
+ AgentId = "agent-1",
Data = new AssistantMessageData
{
MessageId = "msg-1",
@@ -134,6 +135,7 @@ public void SessionEvent_ToJson_RoundTrips_JsonElementBackedPayloads(SessionEven
switch (expectedType)
{
case "assistant.message":
+ Assert.Equal("agent-1", root.GetProperty("agentId").GetString());
Assert.Equal(
"README.md",
root.GetProperty("data")
diff --git a/go/generated_session_events.go b/go/generated_session_events.go
index f5b393310..59e5fb19d 100644
--- a/go/generated_session_events.go
+++ b/go/generated_session_events.go
@@ -25,14 +25,16 @@ func (r RawSessionEventData) MarshalJSON() ([]byte, error) { return r.Raw, nil }
// SessionEvent represents a single session event with a typed data payload.
type SessionEvent struct {
- // Unique event identifier (UUID v4), generated when the event is emitted.
+ // Sub-agent instance identifier. Absent for events from the root/main agent and session-level events.
+ AgentID *string `json:"agentId,omitempty"`
+ // When true, the event is transient and not persisted to the session event log on disk
+ Ephemeral *bool `json:"ephemeral,omitempty"`
+ // Unique event identifier (UUID v4), generated when the event is emitted
ID string `json:"id"`
- // ISO 8601 timestamp when the event was created.
- Timestamp time.Time `json:"timestamp"`
- // ID of the preceding event in the session. Null for the first event.
+ // ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
ParentID *string `json:"parentId"`
- // When true, the event is transient and not persisted.
- Ephemeral *bool `json:"ephemeral,omitempty"`
+ // ISO 8601 timestamp when the event was created
+ Timestamp time.Time `json:"timestamp"`
// The event type discriminator.
Type SessionEventType `json:"type"`
// Typed event payload. Use a type switch to access per-event fields.
@@ -53,10 +55,11 @@ func (r *SessionEvent) Marshal() ([]byte, error) {
func (e *SessionEvent) UnmarshalJSON(data []byte) error {
type rawEvent struct {
+ AgentID *string `json:"agentId,omitempty"`
+ Ephemeral *bool `json:"ephemeral,omitempty"`
ID string `json:"id"`
- Timestamp time.Time `json:"timestamp"`
ParentID *string `json:"parentId"`
- Ephemeral *bool `json:"ephemeral,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
Type SessionEventType `json:"type"`
Data json.RawMessage `json:"data"`
}
@@ -64,10 +67,11 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
+ e.AgentID = raw.AgentID
+ e.Ephemeral = raw.Ephemeral
e.ID = raw.ID
- e.Timestamp = raw.Timestamp
e.ParentID = raw.ParentID
- e.Ephemeral = raw.Ephemeral
+ e.Timestamp = raw.Timestamp
e.Type = raw.Type
switch raw.Type {
@@ -547,18 +551,20 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error {
func (e SessionEvent) MarshalJSON() ([]byte, error) {
type rawEvent struct {
+ AgentID *string `json:"agentId,omitempty"`
+ Ephemeral *bool `json:"ephemeral,omitempty"`
ID string `json:"id"`
- Timestamp time.Time `json:"timestamp"`
ParentID *string `json:"parentId"`
- Ephemeral *bool `json:"ephemeral,omitempty"`
+ Timestamp time.Time `json:"timestamp"`
Type SessionEventType `json:"type"`
Data any `json:"data"`
}
return json.Marshal(rawEvent{
+ AgentID: e.AgentID,
+ Ephemeral: e.Ephemeral,
ID: e.ID,
- Timestamp: e.Timestamp,
ParentID: e.ParentID,
- Ephemeral: e.Ephemeral,
+ Timestamp: e.Timestamp,
Type: e.Type,
Data: e.Data,
})
diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go
new file mode 100644
index 000000000..bf4846570
--- /dev/null
+++ b/go/session_event_serialization_test.go
@@ -0,0 +1,78 @@
+package copilot
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestSessionEventAgentIDRoundTripsKnownEvent(t *testing.T) {
+ event, err := UnmarshalSessionEvent([]byte(`{
+ "id": "00000000-0000-0000-0000-000000000001",
+ "timestamp": "2026-01-01T00:00:00Z",
+ "parentId": null,
+ "agentId": "agent-1",
+ "type": "user.message",
+ "data": {
+ "content": "Hello"
+ }
+ }`))
+ if err != nil {
+ t.Fatalf("failed to unmarshal session event: %v", err)
+ }
+
+ if event.AgentID == nil || *event.AgentID != "agent-1" {
+ t.Fatalf("expected agent ID to round-trip, got %v", event.AgentID)
+ }
+ if _, ok := event.Data.(*UserMessageData); !ok {
+ t.Fatalf("expected user message data, got %T", event.Data)
+ }
+
+ data, err := event.Marshal()
+ if err != nil {
+ t.Fatalf("failed to marshal session event: %v", err)
+ }
+
+ var serialized map[string]any
+ if err := json.Unmarshal(data, &serialized); err != nil {
+ t.Fatalf("failed to unmarshal serialized session event: %v", err)
+ }
+ if serialized["agentId"] != "agent-1" {
+ t.Fatalf("expected serialized agentId to round-trip, got %v", serialized["agentId"])
+ }
+}
+
+func TestSessionEventAgentIDRoundTripsUnknownEvent(t *testing.T) {
+ event, err := UnmarshalSessionEvent([]byte(`{
+ "id": "00000000-0000-0000-0000-000000000002",
+ "timestamp": "2026-01-01T00:00:00Z",
+ "parentId": null,
+ "agentId": "future-agent",
+ "type": "future.feature_from_server",
+ "data": {
+ "key": "value"
+ }
+ }`))
+ if err != nil {
+ t.Fatalf("failed to unmarshal session event: %v", err)
+ }
+
+ if event.AgentID == nil || *event.AgentID != "future-agent" {
+ t.Fatalf("expected agent ID to round-trip, got %v", event.AgentID)
+ }
+ if _, ok := event.Data.(*RawSessionEventData); !ok {
+ t.Fatalf("expected raw session event data, got %T", event.Data)
+ }
+
+ data, err := event.Marshal()
+ if err != nil {
+ t.Fatalf("failed to marshal session event: %v", err)
+ }
+
+ var serialized map[string]any
+ if err := json.Unmarshal(data, &serialized); err != nil {
+ t.Fatalf("failed to unmarshal serialized session event: %v", err)
+ }
+ if serialized["agentId"] != "future-agent" {
+ t.Fatalf("expected serialized agentId to round-trip, got %v", serialized["agentId"])
+ }
+}
diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py
index a99969fc1..1ef8d9edd 100644
--- a/python/copilot/generated/session_events.py
+++ b/python/copilot/generated/session_events.py
@@ -4773,6 +4773,7 @@ class SessionEvent:
id: UUID
timestamp: datetime
type: SessionEventType
+ agent_id: str | None = None
ephemeral: bool | None = None
parent_id: UUID | None = None
raw_type: str | None = None
@@ -4782,10 +4783,11 @@ def from_dict(obj: Any) -> "SessionEvent":
assert isinstance(obj, dict)
raw_type = from_str(obj.get("type"))
event_type = SessionEventType(raw_type)
- event_id = from_uuid(obj.get("id"))
- timestamp = from_datetime(obj.get("timestamp"))
- ephemeral = from_union([from_bool, from_none], obj.get("ephemeral"))
+ agent_id = from_union([from_none, from_str], obj.get("agentId"))
+ ephemeral = from_union([from_none, from_bool], obj.get("ephemeral"))
+ id = from_uuid(obj.get("id"))
parent_id = from_union([from_none, from_uuid], obj.get("parentId"))
+ timestamp = from_datetime(obj.get("timestamp"))
data_obj = obj.get("data")
match event_type:
case SessionEventType.SESSION_START: data = SessionStartData.from_dict(data_obj)
@@ -4869,9 +4871,10 @@ def from_dict(obj: Any) -> "SessionEvent":
case _: data = RawSessionEventData.from_dict(data_obj)
return SessionEvent(
data=data,
- id=event_id,
+ id=id,
timestamp=timestamp,
type=event_type,
+ agent_id=agent_id,
ephemeral=ephemeral,
parent_id=parent_id,
raw_type=raw_type if event_type == SessionEventType.UNKNOWN else None,
@@ -4883,8 +4886,10 @@ def to_dict(self) -> dict:
result["id"] = to_uuid(self.id)
result["timestamp"] = to_datetime(self.timestamp)
result["type"] = self.raw_type if self.type == SessionEventType.UNKNOWN and self.raw_type is not None else to_enum(SessionEventType, self.type)
+ if self.agent_id is not None:
+ result["agentId"] = from_union([from_none, from_str], self.agent_id)
if self.ephemeral is not None:
- result["ephemeral"] = from_bool(self.ephemeral)
+ result["ephemeral"] = from_union([from_none, from_bool], self.ephemeral)
result["parentId"] = from_union([from_none, to_uuid], self.parent_id)
return result
diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py
index 733a9b24b..10ba0644a 100644
--- a/python/test_event_forward_compatibility.py
+++ b/python/test_event_forward_compatibility.py
@@ -24,6 +24,7 @@
UserMessageAgentMode,
UserMessageAttachmentGithubReferenceType,
session_event_from_dict,
+ session_event_to_dict,
)
@@ -47,6 +48,39 @@ def test_unknown_event_type_maps_to_unknown(self):
event = session_event_from_dict(unknown_event)
assert event.type == SessionEventType.UNKNOWN, f"Expected UNKNOWN, got {event.type}"
+ def test_known_event_preserves_top_level_agent_id(self):
+ """Known events should preserve the top-level sub-agent envelope ID."""
+ known_event = {
+ "id": str(uuid4()),
+ "timestamp": datetime.now().isoformat(),
+ "parentId": None,
+ "agentId": "agent-1",
+ "type": "user.message",
+ "data": {"content": "Hello"},
+ }
+
+ event = session_event_from_dict(known_event)
+ assert event.agent_id == "agent-1"
+ assert session_event_to_dict(event)["agentId"] == "agent-1"
+
+ def test_unknown_event_preserves_top_level_agent_id(self):
+ """Unknown events should preserve the top-level sub-agent envelope ID."""
+ unknown_event = {
+ "id": str(uuid4()),
+ "timestamp": datetime.now().isoformat(),
+ "parentId": None,
+ "agentId": "future-agent",
+ "type": "session.future_feature_from_server",
+ "data": {"key": "value"},
+ }
+
+ event = session_event_from_dict(unknown_event)
+ assert event.type == SessionEventType.UNKNOWN
+ assert event.agent_id == "future-agent"
+ serialized = session_event_to_dict(event)
+ assert serialized["agentId"] == "future-agent"
+ assert serialized["type"] == "session.future_feature_from_server"
+
def test_malformed_uuid_raises_error(self):
"""Malformed UUIDs should raise ValueError for visibility, not be suppressed."""
malformed_event = {
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index 9c8332c09..6853bde57 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -31,10 +31,13 @@ import {
isObjectSchema,
isVoidSchema,
getNullableInner,
+ getSessionEventVariantSchemas,
+ getSharedSessionEventEnvelopeProperties,
REPO_ROOT,
type ApiSchema,
type DefinitionCollections,
type RpcMethod,
+ type SessionEventEnvelopeProperty,
} from "./utils.js";
const execFileAsync = promisify(execFile);
@@ -343,26 +346,16 @@ function getOrCreateEnum(parentClassName: string, propName: string, values: stri
function extractEventVariants(schema: JSONSchema7): EventVariant[] {
const definitionCollections = collectDefinitionCollections(schema as Record);
- const sessionEvent =
- resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ??
- resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections);
- if (!sessionEvent?.anyOf) throw new Error("Schema must have SessionEvent definition with anyOf");
-
- return sessionEvent.anyOf
+ return getSessionEventVariantSchemas(schema, definitionCollections)
.map((variant) => {
- const resolvedVariant =
- resolveObjectSchema(variant as JSONSchema7, definitionCollections) ??
- resolveSchema(variant as JSONSchema7, definitionCollections) ??
- (variant as JSONSchema7);
- if (typeof resolvedVariant !== "object" || !resolvedVariant.properties) throw new Error("Invalid variant");
- const typeSchema = resolvedVariant.properties.type as JSONSchema7;
+ const typeSchema = variant.properties!.type as JSONSchema7;
const typeName = typeSchema?.const as string;
if (!typeName) throw new Error("Variant must have type.const");
const baseName = typeToClassName(typeName);
const dataSchema =
- resolveObjectSchema(resolvedVariant.properties.data as JSONSchema7, definitionCollections) ??
- resolveSchema(resolvedVariant.properties.data as JSONSchema7, definitionCollections) ??
- (resolvedVariant.properties.data as JSONSchema7);
+ resolveObjectSchema(variant.properties!.data as JSONSchema7, definitionCollections) ??
+ resolveSchema(variant.properties!.data as JSONSchema7, definitionCollections) ??
+ (variant.properties!.data as JSONSchema7);
return {
typeName,
className: `${baseName}Event`,
@@ -678,6 +671,35 @@ function generateDataClass(variant: EventVariant, knownTypes: Map,
+ nestedClasses: Map,
+ enumOutput: string[]
+): string[] {
+ const csharpName = toPascalCase(property.name);
+ const csharpType = resolveSessionPropertyType(
+ property.schema,
+ "SessionEvent",
+ csharpName,
+ property.required,
+ knownTypes,
+ nestedClasses,
+ enumOutput
+ );
+ const lines: string[] = [];
+
+ lines.push(...xmlDocPropertyComment(property.schema.description, property.name, " "));
+ lines.push(...emitDataAnnotations(property.schema, " "));
+ if (isSchemaDeprecated(property.schema)) lines.push(` [Obsolete("This member is deprecated and will be removed in a future version.")]`);
+ if (isDurationProperty(property.schema)) lines.push(` [JsonConverter(typeof(MillisecondsTimeSpanConverter))]`);
+ if (!property.required) lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`);
+ lines.push(` [JsonPropertyName("${property.name}")]`);
+ lines.push(` public ${csharpType} ${csharpName} { get; set; }`, "");
+
+ return lines;
+}
+
function generateSessionEventsCode(schema: JSONSchema7): string {
generatedEnums.clear();
sessionDefinitions = collectDefinitionCollections(schema as Record);
@@ -685,23 +707,7 @@ function generateSessionEventsCode(schema: JSONSchema7): string {
const knownTypes = new Map();
const nestedClasses = new Map();
const enumOutput: string[] = [];
-
- // Extract descriptions for base class properties from the first variant
- const sessionEventDefinition =
- resolveSchema({ $ref: "#/definitions/SessionEvent" }, sessionDefinitions) ??
- resolveSchema({ $ref: "#/$defs/SessionEvent" }, sessionDefinitions);
- const firstVariant =
- typeof sessionEventDefinition === "object" ? (sessionEventDefinition.anyOf?.[0] as JSONSchema7 | undefined) : undefined;
- const resolvedFirstVariant =
- resolveObjectSchema(firstVariant, sessionDefinitions) ??
- resolveSchema(firstVariant, sessionDefinitions) ??
- firstVariant;
- const baseProps =
- typeof resolvedFirstVariant === "object" && resolvedFirstVariant?.properties ? resolvedFirstVariant.properties : {};
- const baseDesc = (name: string) => {
- const prop = baseProps[name];
- return typeof prop === "object" ? (prop as JSONSchema7).description : undefined;
- };
+ const envelopeProperties = getSharedSessionEventEnvelopeProperties(schema, sessionDefinitions);
const lines: string[] = [];
lines.push(`${COPYRIGHT}
@@ -731,14 +737,9 @@ namespace GitHub.Copilot.SDK;
lines.push(`[JsonDerivedType(typeof(${variant.className}), "${variant.typeName}")]`);
}
lines.push(`public partial class SessionEvent`, `{`);
- lines.push(...xmlDocComment(baseDesc("id"), " "));
- lines.push(` [JsonPropertyName("id")]`, ` public Guid Id { get; set; }`, "");
- lines.push(...xmlDocComment(baseDesc("timestamp"), " "));
- lines.push(` [JsonPropertyName("timestamp")]`, ` public DateTimeOffset Timestamp { get; set; }`, "");
- lines.push(...xmlDocComment(baseDesc("parentId"), " "));
- lines.push(` [JsonPropertyName("parentId")]`, ` public Guid? ParentId { get; set; }`, "");
- lines.push(...xmlDocComment(baseDesc("ephemeral"), " "));
- lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`, ` [JsonPropertyName("ephemeral")]`, ` public bool? Ephemeral { get; set; }`, "");
+ for (const property of envelopeProperties) {
+ lines.push(...emitSessionEventEnvelopeProperty(property, knownTypes, nestedClasses, enumOutput));
+ }
lines.push(` /// `, ` /// The event type discriminator.`, ` /// `);
lines.push(` [JsonIgnore]`, ` public virtual string Type => "unknown";`, "");
lines.push(` /// Deserializes a JSON string into a .`);
diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts
index c1acc4980..3a4b76cbc 100644
--- a/scripts/codegen/go.ts
+++ b/scripts/codegen/go.ts
@@ -32,9 +32,12 @@ import {
withSharedDefinitions,
refTypeName,
resolveRef,
+ getSessionEventVariantSchemas,
+ getSharedSessionEventEnvelopeProperties,
type ApiSchema,
type DefinitionCollections,
type RpcMethod,
+ type SessionEventEnvelopeProperty,
} from "./utils.js";
const execFileAsync = promisify(execFile);
@@ -305,6 +308,13 @@ interface GoEventVariant {
dataDescription?: string;
}
+interface GoEventEnvelopeProperty extends SessionEventEnvelopeProperty {
+ fieldName: string;
+ typeName: string;
+ jsonTag: string;
+ description?: string;
+}
+
interface GoCodegenCtx {
structs: string[];
enums: string[];
@@ -315,25 +325,15 @@ interface GoCodegenCtx {
function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] {
const definitionCollections = collectDefinitionCollections(schema as Record);
- const sessionEvent =
- resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ??
- resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections);
- if (!sessionEvent?.anyOf) throw new Error("Schema must have SessionEvent definition with anyOf");
-
- return (sessionEvent.anyOf as JSONSchema7[])
+ return getSessionEventVariantSchemas(schema, definitionCollections)
.map((variant) => {
- const resolvedVariant =
- resolveObjectSchema(variant as JSONSchema7, definitionCollections) ??
- resolveSchema(variant as JSONSchema7, definitionCollections) ??
- (variant as JSONSchema7);
- if (typeof resolvedVariant !== "object" || !resolvedVariant.properties) throw new Error("Invalid variant");
- const typeSchema = resolvedVariant.properties.type as JSONSchema7;
+ const typeSchema = variant.properties!.type as JSONSchema7;
const typeName = typeSchema?.const as string;
if (!typeName) throw new Error("Variant must have type.const");
const dataSchema =
- resolveObjectSchema(resolvedVariant.properties.data as JSONSchema7, definitionCollections) ??
- resolveSchema(resolvedVariant.properties.data as JSONSchema7, definitionCollections) ??
- ((resolvedVariant.properties.data as JSONSchema7) || {});
+ resolveObjectSchema(variant.properties!.data as JSONSchema7, definitionCollections) ??
+ resolveSchema(variant.properties!.data as JSONSchema7, definitionCollections) ??
+ ((variant.properties!.data as JSONSchema7) || {});
return {
typeName,
dataClassName: `${toPascalCase(typeName)}Data`,
@@ -343,6 +343,36 @@ function extractGoEventVariants(schema: JSONSchema7): GoEventVariant[] {
});
}
+function getGoSharedEventEnvelopeProperties(schema: JSONSchema7, ctx: GoCodegenCtx): GoEventEnvelopeProperty[] {
+ return getSharedSessionEventEnvelopeProperties(schema, ctx.definitions)
+ .map((property) => {
+ const { name, schema, required } = property;
+ const typeName = resolveGoPropertyType(schema, "SessionEvent", name, required && !getNullableInner(schema), ctx);
+ const omit = required ? "" : ",omitempty";
+
+ return {
+ name,
+ schema,
+ required,
+ fieldName: toGoFieldName(name),
+ typeName,
+ jsonTag: `json:"${name}${omit}"`,
+ description: schema.description,
+ };
+ });
+}
+
+function emitGoEnvelopeStructField(property: GoEventEnvelopeProperty, includeComment: boolean): string[] {
+ const lines: string[] = [];
+ if (includeComment && property.description) {
+ for (const line of property.description.split(/\r?\n/)) {
+ lines.push(`\t// ${line}`);
+ }
+ }
+ lines.push(`\t${property.fieldName} ${property.typeName} \`${property.jsonTag}\``);
+ return lines;
+}
+
/**
* Find a const-valued discriminator property shared by all anyOf variants.
*/
@@ -736,6 +766,7 @@ function generateGoSessionEventsCode(schema: JSONSchema7): string {
generatedNames: new Set(),
definitions: collectDefinitionCollections(schema as Record),
};
+ const envelopeProperties = getGoSharedEventEnvelopeProperties(schema, ctx);
// Generate per-event data structs
const dataStructs: string[] = [];
@@ -834,15 +865,9 @@ function generateGoSessionEventsCode(schema: JSONSchema7): string {
// SessionEvent struct
out.push(`// SessionEvent represents a single session event with a typed data payload.`);
out.push(`type SessionEvent struct {`);
- out.push(`\t// Unique event identifier (UUID v4), generated when the event is emitted.`);
- out.push(`\tID string \`json:"id"\``);
- out.push(`\t// ISO 8601 timestamp when the event was created.`);
- out.push(`\tTimestamp time.Time \`json:"timestamp"\``);
- // parentId: string or null
- out.push(`\t// ID of the preceding event in the session. Null for the first event.`);
- out.push(`\tParentID *string \`json:"parentId"\``);
- out.push(`\t// When true, the event is transient and not persisted.`);
- out.push(`\tEphemeral *bool \`json:"ephemeral,omitempty"\``);
+ for (const property of envelopeProperties) {
+ out.push(...emitGoEnvelopeStructField(property, true));
+ }
out.push(`\t// The event type discriminator.`);
out.push(`\tType SessionEventType \`json:"type"\``);
out.push(`\t// Typed event payload. Use a type switch to access per-event fields.`);
@@ -869,10 +894,11 @@ function generateGoSessionEventsCode(schema: JSONSchema7): string {
// Custom UnmarshalJSON
out.push(`func (e *SessionEvent) UnmarshalJSON(data []byte) error {`);
out.push(`\ttype rawEvent struct {`);
- out.push(`\t\tID string \`json:"id"\``);
- out.push(`\t\tTimestamp time.Time \`json:"timestamp"\``);
- out.push(`\t\tParentID *string \`json:"parentId"\``);
- out.push(`\t\tEphemeral *bool \`json:"ephemeral,omitempty"\``);
+ for (const property of envelopeProperties) {
+ for (const line of emitGoEnvelopeStructField(property, false)) {
+ out.push(`\t${line}`);
+ }
+ }
out.push(`\t\tType SessionEventType \`json:"type"\``);
out.push(`\t\tData json.RawMessage \`json:"data"\``);
out.push(`\t}`);
@@ -880,10 +906,9 @@ function generateGoSessionEventsCode(schema: JSONSchema7): string {
out.push(`\tif err := json.Unmarshal(data, &raw); err != nil {`);
out.push(`\t\treturn err`);
out.push(`\t}`);
- out.push(`\te.ID = raw.ID`);
- out.push(`\te.Timestamp = raw.Timestamp`);
- out.push(`\te.ParentID = raw.ParentID`);
- out.push(`\te.Ephemeral = raw.Ephemeral`);
+ for (const property of envelopeProperties) {
+ out.push(`\te.${property.fieldName} = raw.${property.fieldName}`);
+ }
out.push(`\te.Type = raw.Type`);
out.push(``);
out.push(`\tswitch raw.Type {`);
@@ -915,18 +940,18 @@ function generateGoSessionEventsCode(schema: JSONSchema7): string {
// Custom MarshalJSON
out.push(`func (e SessionEvent) MarshalJSON() ([]byte, error) {`);
out.push(`\ttype rawEvent struct {`);
- out.push(`\t\tID string \`json:"id"\``);
- out.push(`\t\tTimestamp time.Time \`json:"timestamp"\``);
- out.push(`\t\tParentID *string \`json:"parentId"\``);
- out.push(`\t\tEphemeral *bool \`json:"ephemeral,omitempty"\``);
+ for (const property of envelopeProperties) {
+ for (const line of emitGoEnvelopeStructField(property, false)) {
+ out.push(`\t${line}`);
+ }
+ }
out.push(`\t\tType SessionEventType \`json:"type"\``);
out.push(`\t\tData any \`json:"data"\``);
out.push(`\t}`);
out.push(`\treturn json.Marshal(rawEvent{`);
- out.push(`\t\tID: e.ID,`);
- out.push(`\t\tTimestamp: e.Timestamp,`);
- out.push(`\t\tParentID: e.ParentID,`);
- out.push(`\t\tEphemeral: e.Ephemeral,`);
+ for (const property of envelopeProperties) {
+ out.push(`\t\t${property.fieldName}: e.${property.fieldName},`);
+ }
out.push(`\t\tType: e.Type,`);
out.push(`\t\tData: e.Data,`);
out.push(`\t})`);
diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts
index 6a3fe3b7d..a4cbcd6d4 100644
--- a/scripts/codegen/python.ts
+++ b/scripts/codegen/python.ts
@@ -31,9 +31,12 @@ import {
resolveObjectSchema,
resolveSchema,
withSharedDefinitions,
+ getSessionEventVariantSchemas,
+ getSharedSessionEventEnvelopeProperties,
type ApiSchema,
type DefinitionCollections,
type RpcMethod,
+ type SessionEventEnvelopeProperty,
} from "./utils.js";
// ── Utilities ───────────────────────────────────────────────────────────────
@@ -475,6 +478,13 @@ interface PyEventVariant {
dataDescription?: string;
}
+interface PyEventEnvelopeProperty extends SessionEventEnvelopeProperty {
+ jsonName: string;
+ fieldName: string;
+ hasDefault: boolean;
+ resolved: PyResolvedType;
+}
+
interface PyResolvedType {
annotation: string;
fromExpr: (expr: string) => string;
@@ -621,33 +631,18 @@ function toPythonLiteral(value: unknown): string | undefined {
function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] {
const definitionCollections = collectDefinitionCollections(schema as Record);
- const sessionEvent =
- resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ??
- resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections);
- if (!sessionEvent?.anyOf) {
- throw new Error("Schema must have SessionEvent definition with anyOf");
- }
-
- return (sessionEvent.anyOf as JSONSchema7[])
+ return getSessionEventVariantSchemas(schema, definitionCollections)
.map((variant) => {
- const resolvedVariant =
- resolveObjectSchema(variant as JSONSchema7, definitionCollections) ??
- resolveSchema(variant as JSONSchema7, definitionCollections) ??
- (variant as JSONSchema7);
- if (typeof resolvedVariant !== "object" || !resolvedVariant.properties) {
- throw new Error("Invalid event variant");
- }
-
- const typeSchema = resolvedVariant.properties.type as JSONSchema7;
+ const typeSchema = variant.properties!.type as JSONSchema7;
const typeName = typeSchema?.const as string;
if (!typeName) {
throw new Error("Event variant must define type.const");
}
const dataSchema =
- resolveObjectSchema(resolvedVariant.properties.data as JSONSchema7, definitionCollections) ??
- resolveSchema(resolvedVariant.properties.data as JSONSchema7, definitionCollections) ??
- ((resolvedVariant.properties.data as JSONSchema7) || {});
+ resolveObjectSchema(variant.properties!.data as JSONSchema7, definitionCollections) ??
+ resolveSchema(variant.properties!.data as JSONSchema7, definitionCollections) ??
+ ((variant.properties!.data as JSONSchema7) || {});
return {
typeName,
dataClassName: `${toPascalCase(typeName)}Data`,
@@ -657,6 +652,23 @@ function extractPyEventVariants(schema: JSONSchema7): PyEventVariant[] {
});
}
+function getPySharedEventEnvelopeProperties(schema: JSONSchema7, ctx: PyCodegenCtx): PyEventEnvelopeProperty[] {
+ return getSharedSessionEventEnvelopeProperties(schema, ctx.definitions)
+ .map((property) => {
+ const { name, schema, required } = property;
+ const resolved = resolvePyPropertyType(schema, "SessionEvent", name, required, ctx);
+
+ return {
+ ...property,
+ jsonName: name,
+ fieldName: toSnakeCase(name),
+ required,
+ hasDefault: !required || resolved.annotation.includes(" | None"),
+ resolved,
+ };
+ });
+}
+
function findPyDiscriminator(
variants: JSONSchema7[]
): { property: string; mapping: Map } | null {
@@ -1267,6 +1279,9 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string {
for (const variant of variants) {
emitPyClass(variant.dataClassName, variant.dataSchema, ctx, variant.dataDescription);
}
+ const envelopeProperties = getPySharedEventEnvelopeProperties(schema, ctx);
+ const envelopePropertiesWithoutDefaults = envelopeProperties.filter((property) => !property.hasDefault);
+ const envelopePropertiesWithDefaults = envelopeProperties.filter((property) => property.hasDefault);
const eventTypeLines: string[] = [];
eventTypeLines.push(`class SessionEventType(Enum):`);
@@ -1507,11 +1522,13 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string {
out.push(`@dataclass`);
out.push(`class SessionEvent:`);
out.push(` data: SessionEventData`);
- out.push(` id: UUID`);
- out.push(` timestamp: datetime`);
+ for (const property of envelopePropertiesWithoutDefaults) {
+ out.push(` ${property.fieldName}: ${property.resolved.annotation}`);
+ }
out.push(` type: SessionEventType`);
- out.push(` ephemeral: bool | None = None`);
- out.push(` parent_id: UUID | None = None`);
+ for (const property of envelopePropertiesWithDefaults) {
+ out.push(` ${property.fieldName}: ${property.resolved.annotation} = None`);
+ }
out.push(` raw_type: str | None = None`);
out.push(``);
out.push(` @staticmethod`);
@@ -1519,10 +1536,9 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string {
out.push(` assert isinstance(obj, dict)`);
out.push(` raw_type = from_str(obj.get("type"))`);
out.push(` event_type = SessionEventType(raw_type)`);
- out.push(` event_id = from_uuid(obj.get("id"))`);
- out.push(` timestamp = from_datetime(obj.get("timestamp"))`);
- out.push(` ephemeral = from_union([from_bool, from_none], obj.get("ephemeral"))`);
- out.push(` parent_id = from_union([from_none, from_uuid], obj.get("parentId"))`);
+ for (const property of envelopeProperties) {
+ out.push(` ${property.fieldName} = ${property.resolved.fromExpr(`obj.get(${JSON.stringify(property.jsonName)})`)}`);
+ }
out.push(` data_obj = obj.get("data")`);
out.push(` match event_type:`);
for (const variant of variants) {
@@ -1533,25 +1549,34 @@ export function generatePythonSessionEventsCode(schema: JSONSchema7): string {
out.push(` case _: data = RawSessionEventData.from_dict(data_obj)`);
out.push(` return SessionEvent(`);
out.push(` data=data,`);
- out.push(` id=event_id,`);
- out.push(` timestamp=timestamp,`);
+ for (const property of envelopePropertiesWithoutDefaults) {
+ out.push(` ${property.fieldName}=${property.fieldName},`);
+ }
out.push(` type=event_type,`);
- out.push(` ephemeral=ephemeral,`);
- out.push(` parent_id=parent_id,`);
+ for (const property of envelopePropertiesWithDefaults) {
+ out.push(` ${property.fieldName}=${property.fieldName},`);
+ }
out.push(` raw_type=raw_type if event_type == SessionEventType.UNKNOWN else None,`);
out.push(` )`);
out.push(``);
out.push(` def to_dict(self) -> dict:`);
out.push(` result: dict = {}`);
out.push(` result["data"] = self.data.to_dict()`);
- out.push(` result["id"] = to_uuid(self.id)`);
- out.push(` result["timestamp"] = to_datetime(self.timestamp)`);
+ for (const property of envelopePropertiesWithoutDefaults) {
+ out.push(` result[${JSON.stringify(property.jsonName)}] = ${property.resolved.toExpr(`self.${property.fieldName}`)}`);
+ }
out.push(
` result["type"] = self.raw_type if self.type == SessionEventType.UNKNOWN and self.raw_type is not None else to_enum(SessionEventType, self.type)`
);
- out.push(` if self.ephemeral is not None:`);
- out.push(` result["ephemeral"] = from_bool(self.ephemeral)`);
- out.push(` result["parentId"] = from_union([from_none, to_uuid], self.parent_id)`);
+ for (const property of envelopePropertiesWithDefaults) {
+ const valueExpr = property.resolved.toExpr(`self.${property.fieldName}`);
+ if (property.required) {
+ out.push(` result[${JSON.stringify(property.jsonName)}] = ${valueExpr}`);
+ } else {
+ out.push(` if self.${property.fieldName} is not None:`);
+ out.push(` result[${JSON.stringify(property.jsonName)}] = ${valueExpr}`);
+ }
+ }
out.push(` return result`);
out.push(``);
out.push(``);
diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts
index 4a4c31f3f..16aaa0bfe 100644
--- a/scripts/codegen/utils.ts
+++ b/scripts/codegen/utils.ts
@@ -29,6 +29,12 @@ export interface DefinitionCollections {
$defs?: Record;
}
+export interface SessionEventEnvelopeProperty {
+ name: string;
+ schema: JSONSchema7;
+ required: boolean;
+}
+
export interface JSONSchema7WithDefs extends JSONSchema7, DefinitionCollections {}
export type SchemaWithSharedDefinitions = T & {
@@ -497,6 +503,66 @@ export function resolveObjectSchema(
return resolved;
}
+export function getSessionEventVariantSchemas(
+ schema: JSONSchema7,
+ definitionCollections: DefinitionCollections = collectDefinitionCollections(schema as Record)
+): JSONSchema7[] {
+ const sessionEvent =
+ resolveSchema({ $ref: "#/definitions/SessionEvent" }, definitionCollections) ??
+ resolveSchema({ $ref: "#/$defs/SessionEvent" }, definitionCollections);
+ if (!sessionEvent?.anyOf) throw new Error("Schema must have SessionEvent definition with anyOf");
+
+ return (sessionEvent.anyOf as JSONSchema7[]).map((variant) => {
+ const resolvedVariant =
+ resolveObjectSchema(variant, definitionCollections) ??
+ resolveSchema(variant, definitionCollections) ??
+ variant;
+ if (typeof resolvedVariant !== "object" || !resolvedVariant.properties) throw new Error("Invalid event variant");
+ return resolvedVariant;
+ });
+}
+
+export function getSharedSessionEventEnvelopeProperties(
+ schema: JSONSchema7,
+ definitionCollections: DefinitionCollections = collectDefinitionCollections(schema as Record)
+): SessionEventEnvelopeProperty[] {
+ const variants = getSessionEventVariantSchemas(schema, definitionCollections);
+ const firstVariant = variants[0];
+ const firstProperties = firstVariant.properties ?? {};
+
+ return Object.entries(firstProperties)
+ .filter(([name]) => name !== "type" && name !== "data")
+ .map(([name]) => {
+ const propertySchemas = variants
+ .map((variant) => variant.properties?.[name])
+ .filter((propSchema): propSchema is JSONSchema7 => typeof propSchema === "object" && propSchema !== null);
+
+ if (propertySchemas.length !== variants.length) return undefined;
+
+ return {
+ name,
+ schema: selectSessionEventEnvelopePropertySchema(propertySchemas),
+ required: variants.every((variant) => (variant.required ?? []).includes(name)),
+ };
+ })
+ .filter((property): property is SessionEventEnvelopeProperty => property !== undefined);
+}
+
+function selectSessionEventEnvelopePropertySchema(propertySchemas: JSONSchema7[]): JSONSchema7 {
+ // Some variants further constrain a shared envelope property, e.g. ephemeral const true.
+ // Generate the base property from the least restrictive schema that has useful metadata.
+ return (
+ propertySchemas.find((schema) => !isConstOrEnumSchema(schema) && schema.description) ??
+ propertySchemas.find((schema) => !isConstOrEnumSchema(schema)) ??
+ propertySchemas.find((schema) => schema.description) ??
+ propertySchemas[0]
+ );
+}
+
+function isConstOrEnumSchema(schema: JSONSchema7): boolean {
+ return "const" in schema || (Array.isArray(schema.enum) && schema.enum.length > 0);
+}
+
export function hasSchemaPayload(schema: JSONSchema7 | null | undefined): boolean {
if (!schema) return false;
if (schema.properties) return Object.keys(schema.properties).length > 0;