diff --git a/docs/features/image-input.md b/docs/features/image-input.md index aa3bf2f64..147a5258f 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -117,7 +117,7 @@ func main() { Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ { - Type: copilot.File, + Type: copilot.AttachmentTypeFile, Path: &path, }, }, @@ -143,7 +143,7 @@ session.Send(ctx, copilot.MessageOptions{ Prompt: "Describe what you see in this image", Attachments: []copilot.Attachment{ { - Type: copilot.File, + Type: copilot.AttachmentTypeFile, Path: &path, }, }, diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 606c0b052..0014ec7f0 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -749,6 +749,7 @@ public Task SetModelAsync(string model, CancellationToken cancellationToken = de /// The message to log. /// Log level (default: info). /// When true, the message is not persisted to disk. + /// Optional URL to associate with the log entry. /// Optional cancellation token. /// /// @@ -758,9 +759,9 @@ public Task SetModelAsync(string model, CancellationToken cancellationToken = de /// await session.LogAsync("Temporary status", ephemeral: true); /// /// - public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default) + public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, string? url = null, CancellationToken cancellationToken = default) { - await Rpc.LogAsync(message, level, ephemeral, cancellationToken); + await Rpc.LogAsync(message, level, ephemeral, url, cancellationToken); } /// diff --git a/go/generated_session_events.go b/go/generated_session_events.go index 9c28c07f3..d68436e8d 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -1243,78 +1243,78 @@ type ToolRequest struct { type AgentMode string const ( - AgentModeShell AgentMode = "shell" - Autopilot AgentMode = "autopilot" - Interactive AgentMode = "interactive" - Plan AgentMode = "plan" + AgentModeShell AgentMode = "shell" + AgentModeAutopilot AgentMode = "autopilot" + AgentModeInteractive AgentMode = "interactive" + AgentModePlan AgentMode = "plan" ) // Type of GitHub reference type ReferenceType string const ( - Discussion ReferenceType = "discussion" - Issue ReferenceType = "issue" - PR ReferenceType = "pr" + ReferenceTypeDiscussion ReferenceType = "discussion" + ReferenceTypeIssue ReferenceType = "issue" + ReferenceTypePr ReferenceType = "pr" ) type AttachmentType string const ( - Blob AttachmentType = "blob" - Directory AttachmentType = "directory" - File AttachmentType = "file" - GithubReference AttachmentType = "github_reference" - Selection AttachmentType = "selection" + AttachmentTypeBlob AttachmentType = "blob" + AttachmentTypeDirectory AttachmentType = "directory" + AttachmentTypeFile AttachmentType = "file" + AttachmentTypeGithubReference AttachmentType = "github_reference" + AttachmentTypeSelection AttachmentType = "selection" ) // Hosting platform type of the repository (github or ado) type HostType string const ( - ADO HostType = "ado" - Github HostType = "github" + HostTypeAdo HostType = "ado" + HostTypeGithub HostType = "github" ) // Discovery source type Source string const ( - Project Source = "project" - User Source = "user" + SourceProject Source = "project" + SourceUser Source = "user" ) // Current status: running, disabled, failed, or starting type ExtensionStatus string const ( - PurpleDisabled ExtensionStatus = "disabled" - PurpleFailed ExtensionStatus = "failed" - Running ExtensionStatus = "running" - Starting ExtensionStatus = "starting" + ExtensionStatusDisabled ExtensionStatus = "disabled" + ExtensionStatusFailed ExtensionStatus = "failed" + ExtensionStatusRunning ExtensionStatus = "running" + ExtensionStatusStarting ExtensionStatus = "starting" ) // Whether the agent completed successfully or failed type KindStatus string const ( - Completed KindStatus = "completed" - FluffyFailed KindStatus = "failed" + KindStatusCompleted KindStatus = "completed" + KindStatusFailed KindStatus = "failed" ) type KindType string const ( - AgentCompleted KindType = "agent_completed" - AgentIdle KindType = "agent_idle" - ShellCompleted KindType = "shell_completed" - ShellDetachedCompleted KindType = "shell_detached_completed" + KindTypeAgentCompleted KindType = "agent_completed" + KindTypeAgentIdle KindType = "agent_idle" + KindTypeShellCompleted KindType = "shell_completed" + KindTypeShellDetachedCompleted KindType = "shell_detached_completed" ) type Mode string const ( - Form Mode = "form" + ModeForm Mode = "form" ) // The type of operation performed on the plan file @@ -1323,66 +1323,66 @@ const ( type Operation string const ( - Create Operation = "create" - Delete Operation = "delete" - Update Operation = "update" + OperationCreate Operation = "create" + OperationDelete Operation = "delete" + OperationUpdate Operation = "update" ) type PermissionRequestKind string const ( - CustomTool PermissionRequestKind = "custom-tool" - Hook PermissionRequestKind = "hook" - KindShell PermissionRequestKind = "shell" - MCP PermissionRequestKind = "mcp" - Memory PermissionRequestKind = "memory" - Read PermissionRequestKind = "read" - URL PermissionRequestKind = "url" - Write PermissionRequestKind = "write" + PermissionRequestKindCustomTool PermissionRequestKind = "custom-tool" + PermissionRequestKindHook PermissionRequestKind = "hook" + PermissionRequestKindShell PermissionRequestKind = "shell" + PermissionRequestKindMcp PermissionRequestKind = "mcp" + PermissionRequestKindMemory PermissionRequestKind = "memory" + PermissionRequestKindRead PermissionRequestKind = "read" + PermissionRequestKindURL PermissionRequestKind = "url" + PermissionRequestKindWrite PermissionRequestKind = "write" ) type RequestedSchemaType string const ( - Object RequestedSchemaType = "object" + RequestedSchemaTypeObject RequestedSchemaType = "object" ) // Theme variant this icon is intended for type Theme string const ( - Dark Theme = "dark" - Light Theme = "light" + ThemeDark Theme = "dark" + ThemeLight Theme = "light" ) type ContentType string const ( - Audio ContentType = "audio" - Image ContentType = "image" - Resource ContentType = "resource" - ResourceLink ContentType = "resource_link" - Terminal ContentType = "terminal" - Text ContentType = "text" + ContentTypeAudio ContentType = "audio" + ContentTypeImage ContentType = "image" + ContentTypeResource ContentType = "resource" + ContentTypeResourceLink ContentType = "resource_link" + ContentTypeTerminal ContentType = "terminal" + ContentTypeText ContentType = "text" ) // The outcome of the permission request type ResultKind string const ( - Approved ResultKind = "approved" - DeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy" - DeniedByRules ResultKind = "denied-by-rules" - DeniedInteractivelyByUser ResultKind = "denied-interactively-by-user" - DeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user" + ResultKindApproved ResultKind = "approved" + ResultKindDeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy" + ResultKindDeniedByRules ResultKind = "denied-by-rules" + ResultKindDeniedInteractivelyByUser ResultKind = "denied-interactively-by-user" + ResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user" ) // Message role: "system" for system prompts, "developer" for developer-injected instructions type Role string const ( - Developer Role = "developer" - System Role = "system" + RoleDeveloper Role = "developer" + RoleSystem Role = "system" ) // Connection status: connected, failed, pending, disabled, or not_configured @@ -1391,27 +1391,27 @@ const ( type ServerStatus string const ( - Connected ServerStatus = "connected" - FluffyDisabled ServerStatus = "disabled" - NotConfigured ServerStatus = "not_configured" - Pending ServerStatus = "pending" - TentacledFailed ServerStatus = "failed" + ServerStatusConnected ServerStatus = "connected" + ServerStatusDisabled ServerStatus = "disabled" + ServerStatusNotConfigured ServerStatus = "not_configured" + ServerStatusPending ServerStatus = "pending" + ServerStatusFailed ServerStatus = "failed" ) // Whether the session ended normally ("routine") or due to a crash/fatal error ("error") type ShutdownType string const ( - Error ShutdownType = "error" - Routine ShutdownType = "routine" + ShutdownTypeError ShutdownType = "error" + ShutdownTypeRoutine ShutdownType = "routine" ) // Origin type of the session being handed off type SourceType string const ( - Local SourceType = "local" - Remote SourceType = "remote" + SourceTypeLocal SourceType = "local" + SourceTypeRemote SourceType = "remote" ) // Tool call type: "function" for standard tool calls, "custom" for grammar-based tool @@ -1419,78 +1419,78 @@ const ( type ToolRequestType string const ( - Custom ToolRequestType = "custom" - Function ToolRequestType = "function" + ToolRequestTypeCustom ToolRequestType = "custom" + ToolRequestTypeFunction ToolRequestType = "function" ) type SessionEventType string const ( - Abort SessionEventType = "abort" - AssistantIntent SessionEventType = "assistant.intent" - AssistantMessage SessionEventType = "assistant.message" - AssistantMessageDelta SessionEventType = "assistant.message_delta" - AssistantReasoning SessionEventType = "assistant.reasoning" - AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" - AssistantStreamingDelta SessionEventType = "assistant.streaming_delta" - AssistantTurnEnd SessionEventType = "assistant.turn_end" - AssistantTurnStart SessionEventType = "assistant.turn_start" - AssistantUsage SessionEventType = "assistant.usage" - CommandCompleted SessionEventType = "command.completed" - CommandQueued SessionEventType = "command.queued" - ElicitationCompleted SessionEventType = "elicitation.completed" - ElicitationRequested SessionEventType = "elicitation.requested" - ExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" - ExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" - ExternalToolCompleted SessionEventType = "external_tool.completed" - ExternalToolRequested SessionEventType = "external_tool.requested" - HookEnd SessionEventType = "hook.end" - HookStart SessionEventType = "hook.start" - PendingMessagesModified SessionEventType = "pending_messages.modified" - PermissionCompleted SessionEventType = "permission.completed" - PermissionRequested SessionEventType = "permission.requested" - SessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" - SessionCompactionComplete SessionEventType = "session.compaction_complete" - SessionCompactionStart SessionEventType = "session.compaction_start" - SessionContextChanged SessionEventType = "session.context_changed" - SessionError SessionEventType = "session.error" - SessionExtensionsLoaded SessionEventType = "session.extensions_loaded" - SessionHandoff SessionEventType = "session.handoff" - SessionIdle SessionEventType = "session.idle" - SessionInfo SessionEventType = "session.info" - SessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" - SessionMCPServersLoaded SessionEventType = "session.mcp_servers_loaded" - SessionModeChanged SessionEventType = "session.mode_changed" - SessionModelChange SessionEventType = "session.model_change" - SessionPlanChanged SessionEventType = "session.plan_changed" - SessionResume SessionEventType = "session.resume" - SessionShutdown SessionEventType = "session.shutdown" - SessionSkillsLoaded SessionEventType = "session.skills_loaded" - SessionSnapshotRewind SessionEventType = "session.snapshot_rewind" - SessionStart SessionEventType = "session.start" - SessionTaskComplete SessionEventType = "session.task_complete" - SessionTitleChanged SessionEventType = "session.title_changed" - SessionToolsUpdated SessionEventType = "session.tools_updated" - SessionTruncation SessionEventType = "session.truncation" - SessionUsageInfo SessionEventType = "session.usage_info" - SessionWarning SessionEventType = "session.warning" - SessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" - SkillInvoked SessionEventType = "skill.invoked" - SubagentCompleted SessionEventType = "subagent.completed" - SubagentDeselected SessionEventType = "subagent.deselected" - SubagentFailed SessionEventType = "subagent.failed" - SubagentSelected SessionEventType = "subagent.selected" - SubagentStarted SessionEventType = "subagent.started" - SystemMessage SessionEventType = "system.message" - SystemNotification SessionEventType = "system.notification" - ToolExecutionComplete SessionEventType = "tool.execution_complete" - ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" - ToolExecutionProgress SessionEventType = "tool.execution_progress" - ToolExecutionStart SessionEventType = "tool.execution_start" - ToolUserRequested SessionEventType = "tool.user_requested" - UserInputCompleted SessionEventType = "user_input.completed" - UserInputRequested SessionEventType = "user_input.requested" - UserMessage SessionEventType = "user.message" + SessionEventTypeAbort SessionEventType = "abort" + SessionEventTypeAssistantIntent SessionEventType = "assistant.intent" + SessionEventTypeAssistantMessage SessionEventType = "assistant.message" + SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta" + SessionEventTypeAssistantReasoning SessionEventType = "assistant.reasoning" + SessionEventTypeAssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" + SessionEventTypeAssistantStreamingDelta SessionEventType = "assistant.streaming_delta" + SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end" + SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start" + SessionEventTypeAssistantUsage SessionEventType = "assistant.usage" + SessionEventTypeCommandCompleted SessionEventType = "command.completed" + SessionEventTypeCommandQueued SessionEventType = "command.queued" + SessionEventTypeElicitationCompleted SessionEventType = "elicitation.completed" + SessionEventTypeElicitationRequested SessionEventType = "elicitation.requested" + SessionEventTypeExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed" + SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" + SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed" + SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested" + SessionEventTypeHookEnd SessionEventType = "hook.end" + SessionEventTypeHookStart SessionEventType = "hook.start" + SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified" + SessionEventTypePermissionCompleted SessionEventType = "permission.completed" + SessionEventTypePermissionRequested SessionEventType = "permission.requested" + SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" + SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" + SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed" + SessionEventTypeSessionError SessionEventType = "session.error" + SessionEventTypeSessionExtensionsLoaded SessionEventType = "session.extensions_loaded" + SessionEventTypeSessionHandoff SessionEventType = "session.handoff" + SessionEventTypeSessionIdle SessionEventType = "session.idle" + SessionEventTypeSessionInfo SessionEventType = "session.info" + SessionEventTypeSessionMcpServerStatusChanged SessionEventType = "session.mcp_server_status_changed" + SessionEventTypeSessionMcpServersLoaded SessionEventType = "session.mcp_servers_loaded" + SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" + SessionEventTypeSessionModelChange SessionEventType = "session.model_change" + SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed" + SessionEventTypeSessionResume SessionEventType = "session.resume" + SessionEventTypeSessionShutdown SessionEventType = "session.shutdown" + SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded" + SessionEventTypeSessionSnapshotRewind SessionEventType = "session.snapshot_rewind" + SessionEventTypeSessionStart SessionEventType = "session.start" + SessionEventTypeSessionTaskComplete SessionEventType = "session.task_complete" + SessionEventTypeSessionTitleChanged SessionEventType = "session.title_changed" + SessionEventTypeSessionToolsUpdated SessionEventType = "session.tools_updated" + SessionEventTypeSessionTruncation SessionEventType = "session.truncation" + SessionEventTypeSessionUsageInfo SessionEventType = "session.usage_info" + SessionEventTypeSessionWarning SessionEventType = "session.warning" + SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed" + SessionEventTypeSkillInvoked SessionEventType = "skill.invoked" + SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed" + SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected" + SessionEventTypeSubagentFailed SessionEventType = "subagent.failed" + SessionEventTypeSubagentSelected SessionEventType = "subagent.selected" + SessionEventTypeSubagentStarted SessionEventType = "subagent.started" + SessionEventTypeSystemMessage SessionEventType = "system.message" + SessionEventTypeSystemNotification SessionEventType = "system.notification" + SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete" + SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result" + SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress" + SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" + SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" + SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed" + SessionEventTypeUserInputRequested SessionEventType = "user_input.requested" + SessionEventTypeUserMessage SessionEventType = "user.message" ) type ContextUnion struct { diff --git a/go/internal/e2e/agent_and_compact_rpc_test.go b/go/internal/e2e/agent_and_compact_rpc_test.go index 338f4da67..cbd52a326 100644 --- a/go/internal/e2e/agent_and_compact_rpc_test.go +++ b/go/internal/e2e/agent_and_compact_rpc_test.go @@ -215,7 +215,7 @@ func TestAgentSelectionRpc(t *testing.T) { } }) - t.Run("should return empty list when no custom agents configured", func(t *testing.T) { + t.Run("should return no custom agents when none configured", func(t *testing.T) { client := copilot.NewClient(&copilot.ClientOptions{ CLIPath: cliPath, UseStdio: copilot.Bool(true), @@ -238,8 +238,13 @@ func TestAgentSelectionRpc(t *testing.T) { t.Fatalf("Failed to list agents: %v", err) } - if len(result.Agents) != 0 { - t.Errorf("Expected empty agent list, got %d agents", len(result.Agents)) + // The CLI may return built-in/default agents even when no custom agents + // are configured, so just verify none of the known custom agent names appear. + customNames := map[string]bool{"test-agent": true, "another-agent": true} + for _, agent := range result.Agents { + if customNames[agent.Name] { + t.Errorf("Expected no custom agents, but found %q", agent.Name) + } } if err := client.Stop(); err != nil { diff --git a/go/internal/e2e/compaction_test.go b/go/internal/e2e/compaction_test.go index aee80704d..888ab2aa9 100644 --- a/go/internal/e2e/compaction_test.go +++ b/go/internal/e2e/compaction_test.go @@ -36,10 +36,10 @@ func TestCompaction(t *testing.T) { var compactionCompleteEvents []copilot.SessionEvent session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.SessionCompactionStart { + if event.Type == copilot.SessionEventTypeSessionCompactionStart { compactionStartEvents = append(compactionStartEvents, event) } - if event.Type == copilot.SessionCompactionComplete { + if event.Type == copilot.SessionEventTypeSessionCompactionComplete { compactionCompleteEvents = append(compactionCompleteEvents, event) } }) @@ -105,7 +105,7 @@ func TestCompaction(t *testing.T) { var compactionEvents []copilot.SessionEvent session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.SessionCompactionStart || event.Type == copilot.SessionCompactionComplete { + if event.Type == copilot.SessionEventTypeSessionCompactionStart || event.Type == copilot.SessionEventTypeSessionCompactionComplete { compactionEvents = append(compactionEvents, event) } }) diff --git a/go/internal/e2e/multi_client_test.go b/go/internal/e2e/multi_client_test.go index 9571ab58e..3c7dc34c3 100644 --- a/go/internal/e2e/multi_client_test.go +++ b/go/internal/e2e/multi_client_test.go @@ -79,13 +79,13 @@ func TestMultiClient(t *testing.T) { client2Completed := make(chan struct{}, 1) session1.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ExternalToolRequested { + if event.Type == copilot.SessionEventTypeExternalToolRequested { select { case client1Requested <- struct{}{}: default: } } - if event.Type == copilot.ExternalToolCompleted { + if event.Type == copilot.SessionEventTypeExternalToolCompleted { select { case client1Completed <- struct{}{}: default: @@ -93,13 +93,13 @@ func TestMultiClient(t *testing.T) { } }) session2.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ExternalToolRequested { + if event.Type == copilot.SessionEventTypeExternalToolRequested { select { case client2Requested <- struct{}{}: default: } } - if event.Type == copilot.ExternalToolCompleted { + if event.Type == copilot.SessionEventTypeExternalToolCompleted { select { case client2Completed <- struct{}{}: default: @@ -120,7 +120,7 @@ func TestMultiClient(t *testing.T) { } // Wait for all broadcast events to arrive on both clients - timeout := time.After(10 * time.Second) + timeout := time.After(30 * time.Second) for _, ch := range []chan struct{}{client1Requested, client2Requested, client1Completed, client2Completed} { select { case <-ch: @@ -197,10 +197,10 @@ func TestMultiClient(t *testing.T) { // Both clients should have seen permission.requested events mu1.Lock() - c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested) + c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested) mu1.Unlock() mu2.Lock() - c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested) + c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested) mu2.Unlock() if len(c1PermRequested) == 0 { @@ -212,10 +212,10 @@ func TestMultiClient(t *testing.T) { // Both clients should have seen permission.completed events with approved result mu1.Lock() - c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted) + c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted) mu1.Unlock() mu2.Lock() - c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted) + c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted) mu2.Unlock() if len(c1PermCompleted) == 0 { @@ -293,10 +293,10 @@ func TestMultiClient(t *testing.T) { // Both clients should have seen permission.requested events mu1.Lock() - c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested) + c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested) mu1.Unlock() mu2.Lock() - c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested) + c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested) mu2.Unlock() if len(c1PermRequested) == 0 { @@ -308,10 +308,10 @@ func TestMultiClient(t *testing.T) { // Both clients should see the denial in the completed event mu1.Lock() - c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted) + c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted) mu1.Unlock() mu2.Lock() - c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted) + c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted) mu2.Unlock() if len(c1PermCompleted) == 0 { diff --git a/go/internal/e2e/permissions_test.go b/go/internal/e2e/permissions_test.go index 328e7e788..98f620043 100644 --- a/go/internal/e2e/permissions_test.go +++ b/go/internal/e2e/permissions_test.go @@ -173,7 +173,7 @@ func TestPermissions(t *testing.T) { permissionDenied := false session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ToolExecutionComplete && + if event.Type == copilot.SessionEventTypeToolExecutionComplete && event.Data.Success != nil && !*event.Data.Success && event.Data.Error != nil && event.Data.Error.ErrorClass != nil && strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") { @@ -223,7 +223,7 @@ func TestPermissions(t *testing.T) { permissionDenied := false session2.On(func(event copilot.SessionEvent) { - if event.Type == copilot.ToolExecutionComplete && + if event.Type == copilot.SessionEventTypeToolExecutionComplete && event.Data.Success != nil && !*event.Data.Success && event.Data.Error != nil && event.Data.Error.ErrorClass != nil && strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") { diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index ebcbe1130..3d69b97ad 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -219,16 +219,16 @@ func TestSessionRpc(t *testing.T) { if err != nil { t.Fatalf("Failed to get mode: %v", err) } - if initial.Mode != rpc.Interactive { + if initial.Mode != rpc.ModeInteractive { t.Errorf("Expected initial mode 'interactive', got %q", initial.Mode) } // Switch to plan mode - planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Plan}) + planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModePlan}) if err != nil { t.Fatalf("Failed to set mode to plan: %v", err) } - if planResult.Mode != rpc.Plan { + if planResult.Mode != rpc.ModePlan { t.Errorf("Expected mode 'plan', got %q", planResult.Mode) } @@ -237,16 +237,16 @@ func TestSessionRpc(t *testing.T) { if err != nil { t.Fatalf("Failed to get mode after plan: %v", err) } - if afterPlan.Mode != rpc.Plan { + if afterPlan.Mode != rpc.ModePlan { t.Errorf("Expected mode 'plan' after set, got %q", afterPlan.Mode) } // Switch back to interactive - interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Interactive}) + interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModeInteractive}) if err != nil { t.Fatalf("Failed to set mode to interactive: %v", err) } - if interactiveResult.Mode != rpc.Interactive { + if interactiveResult.Mode != rpc.ModeInteractive { t.Errorf("Expected mode 'interactive', got %q", interactiveResult.Mode) } }) diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index c3c9cc009..f6542e9f7 100644 --- a/go/internal/e2e/session_test.go +++ b/go/internal/e2e/session_test.go @@ -506,7 +506,7 @@ func TestSession(t *testing.T) { toolStartCh := make(chan *copilot.SessionEvent, 1) toolStartErrCh := make(chan error, 1) go func() { - evt, err := testharness.GetNextEventOfType(session, copilot.ToolExecutionStart, 60*time.Second) + evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeToolExecutionStart, 60*time.Second) if err != nil { toolStartErrCh <- err } else { @@ -517,7 +517,7 @@ func TestSession(t *testing.T) { sessionIdleCh := make(chan *copilot.SessionEvent, 1) sessionIdleErrCh := make(chan error, 1) go func() { - evt, err := testharness.GetNextEventOfType(session, copilot.SessionIdle, 60*time.Second) + evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeSessionIdle, 60*time.Second) if err != nil { sessionIdleErrCh <- err } else { @@ -565,7 +565,7 @@ func TestSession(t *testing.T) { // Verify messages contain an abort event hasAbortEvent := false for _, msg := range messages { - if msg.Type == copilot.Abort { + if msg.Type == copilot.SessionEventTypeAbort { hasAbortEvent = true break } @@ -913,7 +913,7 @@ func TestSetModelWithReasoningEffort(t *testing.T) { modelChanged := make(chan copilot.SessionEvent, 1) session.On(func(event copilot.SessionEvent) { - if event.Type == copilot.SessionModelChange { + if event.Type == copilot.SessionEventTypeSessionModelChange { select { case modelChanged <- event: default: @@ -986,7 +986,7 @@ func TestSessionLog(t *testing.T) { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Info message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Info message", 5*time.Second) if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" { t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType) } @@ -996,11 +996,11 @@ func TestSessionLog(t *testing.T) { }) t.Run("should log warning message", func(t *testing.T) { - if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.Warning}); err != nil { + if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.LevelWarning}); err != nil { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionWarning, "Warning message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionWarning, "Warning message", 5*time.Second) if evt.Data.WarningType == nil || *evt.Data.WarningType != "notification" { t.Errorf("Expected warningType 'notification', got %v", evt.Data.WarningType) } @@ -1010,11 +1010,11 @@ func TestSessionLog(t *testing.T) { }) t.Run("should log error message", func(t *testing.T) { - if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.Error}); err != nil { + if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.LevelError}); err != nil { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionError, "Error message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionError, "Error message", 5*time.Second) if evt.Data.ErrorType == nil || *evt.Data.ErrorType != "notification" { t.Errorf("Expected errorType 'notification', got %v", evt.Data.ErrorType) } @@ -1028,7 +1028,7 @@ func TestSessionLog(t *testing.T) { t.Fatalf("Log failed: %v", err) } - evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Ephemeral message", 5*time.Second) + evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Ephemeral message", 5*time.Second) if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" { t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType) } diff --git a/go/internal/e2e/testharness/helper.go b/go/internal/e2e/testharness/helper.go index 05947c806..3b521f330 100644 --- a/go/internal/e2e/testharness/helper.go +++ b/go/internal/e2e/testharness/helper.go @@ -67,7 +67,7 @@ func GetNextEventOfType(session *copilot.Session, eventType copilot.SessionEvent case result <- &event: default: } - case copilot.SessionError: + case copilot.SessionEventTypeSessionError: msg := "session error" if event.Data.Message != nil { msg = *event.Data.Message diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 7929994cf..b6d171bfa 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -506,48 +506,48 @@ type SessionShellKillParams struct { type Mode string const ( - Autopilot Mode = "autopilot" - Interactive Mode = "interactive" - Plan Mode = "plan" + ModeAutopilot Mode = "autopilot" + ModeInteractive Mode = "interactive" + ModePlan Mode = "plan" ) // Connection status: connected, failed, pending, disabled, or not_configured type ServerStatus string const ( - Connected ServerStatus = "connected" - NotConfigured ServerStatus = "not_configured" - Pending ServerStatus = "pending" - PurpleDisabled ServerStatus = "disabled" - PurpleFailed ServerStatus = "failed" + ServerStatusConnected ServerStatus = "connected" + ServerStatusNotConfigured ServerStatus = "not_configured" + ServerStatusPending ServerStatus = "pending" + ServerStatusDisabled ServerStatus = "disabled" + ServerStatusFailed ServerStatus = "failed" ) // Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) type Source string const ( - Project Source = "project" - User Source = "user" + SourceProject Source = "project" + SourceUser Source = "user" ) // Current status: running, disabled, failed, or starting type ExtensionStatus string const ( - FluffyDisabled ExtensionStatus = "disabled" - FluffyFailed ExtensionStatus = "failed" - Running ExtensionStatus = "running" - Starting ExtensionStatus = "starting" + ExtensionStatusDisabled ExtensionStatus = "disabled" + ExtensionStatusFailed ExtensionStatus = "failed" + ExtensionStatusRunning ExtensionStatus = "running" + ExtensionStatusStarting ExtensionStatus = "starting" ) type Kind string const ( - Approved Kind = "approved" - DeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy" - DeniedByRules Kind = "denied-by-rules" - DeniedInteractivelyByUser Kind = "denied-interactively-by-user" - DeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user" + KindApproved Kind = "approved" + KindDeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy" + KindDeniedByRules Kind = "denied-by-rules" + KindDeniedInteractivelyByUser Kind = "denied-interactively-by-user" + KindDeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user" ) // Log severity level. Determines how the message is displayed in the timeline. Defaults to @@ -555,18 +555,18 @@ const ( type Level string const ( - Error Level = "error" - Info Level = "info" - Warning Level = "warning" + LevelError Level = "error" + LevelInfo Level = "info" + LevelWarning Level = "warning" ) // Signal to send (default: SIGTERM) type Signal string const ( - Sigint Signal = "SIGINT" - Sigkill Signal = "SIGKILL" - Sigterm Signal = "SIGTERM" + SignalSIGINT Signal = "SIGINT" + SignalSIGKILL Signal = "SIGKILL" + SignalSIGTERM Signal = "SIGTERM" ) type ResultUnion struct { @@ -977,20 +977,20 @@ type McpRpcApi struct { sessionID string } -func (a *McpRpcApi) List(ctx context.Context) (*SessionMcpListResult, error) { +func (a *McpRpcApi) List(ctx context.Context) (*SessionMCPListResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.mcp.list", req) if err != nil { return nil, err } - var result SessionMcpListResult + var result SessionMCPListResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } return &result, nil } -func (a *McpRpcApi) Enable(ctx context.Context, params *SessionMcpEnableParams) (*SessionMcpEnableResult, error) { +func (a *McpRpcApi) Enable(ctx context.Context, params *SessionMCPEnableParams) (*SessionMCPEnableResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["serverName"] = params.ServerName @@ -999,14 +999,14 @@ func (a *McpRpcApi) Enable(ctx context.Context, params *SessionMcpEnableParams) if err != nil { return nil, err } - var result SessionMcpEnableResult + var result SessionMCPEnableResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } return &result, nil } -func (a *McpRpcApi) Disable(ctx context.Context, params *SessionMcpDisableParams) (*SessionMcpDisableResult, error) { +func (a *McpRpcApi) Disable(ctx context.Context, params *SessionMCPDisableParams) (*SessionMCPDisableResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} if params != nil { req["serverName"] = params.ServerName @@ -1015,20 +1015,20 @@ func (a *McpRpcApi) Disable(ctx context.Context, params *SessionMcpDisableParams if err != nil { return nil, err } - var result SessionMcpDisableResult + var result SessionMCPDisableResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } return &result, nil } -func (a *McpRpcApi) Reload(ctx context.Context) (*SessionMcpReloadResult, error) { +func (a *McpRpcApi) Reload(ctx context.Context) (*SessionMCPReloadResult, error) { req := map[string]interface{}{"sessionId": a.sessionID} raw, err := a.client.Request("session.mcp.reload", req) if err != nil { return nil, err } - var result SessionMcpReloadResult + var result SessionMCPReloadResult if err := json.Unmarshal(raw, &result); err != nil { return nil, err } diff --git a/go/samples/chat.go b/go/samples/chat.go index f984f758a..4d5e98d7d 100644 --- a/go/samples/chat.go +++ b/go/samples/chat.go @@ -35,11 +35,11 @@ func main() { session.On(func(event copilot.SessionEvent) { var output string switch event.Type { - case copilot.AssistantReasoning: + case copilot.SessionEventTypeAssistantReasoning: if event.Data.Content != nil { output = fmt.Sprintf("[reasoning: %s]", *event.Data.Content) } - case copilot.ToolExecutionStart: + case copilot.SessionEventTypeToolExecutionStart: if event.Data.ToolName != nil { output = fmt.Sprintf("[tool: %s]", *event.Data.ToolName) } diff --git a/go/session.go b/go/session.go index d2a5785be..107ac9824 100644 --- a/go/session.go +++ b/go/session.go @@ -182,17 +182,17 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses unsubscribe := s.On(func(event SessionEvent) { switch event.Type { - case AssistantMessage: + case SessionEventTypeAssistantMessage: mu.Lock() eventCopy := event lastAssistantMessage = &eventCopy mu.Unlock() - case SessionIdle: + case SessionEventTypeSessionIdle: select { case idleCh <- struct{}{}: default: } - case SessionError: + case SessionEventTypeSessionError: errMsg := "session error" if event.Data.Message != nil { errMsg = *event.Data.Message @@ -501,7 +501,7 @@ func (s *Session) processEvents() { // cause RPC deadlocks. func (s *Session) handleBroadcastEvent(event SessionEvent) { switch event.Type { - case ExternalToolRequested: + case SessionEventTypeExternalToolRequested: requestID := event.Data.RequestID toolName := event.Data.ToolName if requestID == nil || toolName == nil { @@ -524,7 +524,7 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler, tp, ts) - case PermissionRequested: + case SessionEventTypePermissionRequested: requestID := event.Data.RequestID if requestID == nil || event.Data.PermissionRequest == nil { return @@ -585,7 +585,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{ RequestID: requestID, Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{ - Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser, + Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser, }, }) } @@ -600,7 +600,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{ RequestID: requestID, Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{ - Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser, + Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser, }, }) return @@ -770,8 +770,8 @@ func (s *Session) SetModel(ctx context.Context, model string, opts ...SetModelOp // LogOptions configures optional parameters for [Session.Log]. type LogOptions struct { - // Level sets the log severity. Valid values are [rpc.Info] (default), - // [rpc.Warning], and [rpc.Error]. + // Level sets the log severity. Valid values are [rpc.LevelInfo] (default), + // [rpc.LevelWarning], and [rpc.LevelError]. Level rpc.Level // Ephemeral marks the message as transient so it is not persisted // to the session event log on disk. When nil the server decides the @@ -791,7 +791,7 @@ type LogOptions struct { // session.Log(ctx, "Processing started") // // // Warning with options -// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.Warning}) +// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.LevelWarning}) // // // Ephemeral message (not persisted) // session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: copilot.Bool(true)}) diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 1bc431ee6..4cdd3ebc7 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -2393,21 +2393,21 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id - async def list(self, *, timeout: float | None = None) -> SessionMcpListResult: - return SessionMcpListResult.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def list(self, *, timeout: float | None = None) -> SessionMCPListResult: + return SessionMCPListResult.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) - async def enable(self, params: SessionMcpEnableParams, *, timeout: float | None = None) -> SessionMcpEnableResult: + async def enable(self, params: SessionMCPEnableParams, *, timeout: float | None = None) -> SessionMCPEnableResult: params_dict = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id - return SessionMcpEnableResult.from_dict(await self._client.request("session.mcp.enable", params_dict, **_timeout_kwargs(timeout))) + return SessionMCPEnableResult.from_dict(await self._client.request("session.mcp.enable", params_dict, **_timeout_kwargs(timeout))) - async def disable(self, params: SessionMcpDisableParams, *, timeout: float | None = None) -> SessionMcpDisableResult: + async def disable(self, params: SessionMCPDisableParams, *, timeout: float | None = None) -> SessionMCPDisableResult: params_dict = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id - return SessionMcpDisableResult.from_dict(await self._client.request("session.mcp.disable", params_dict, **_timeout_kwargs(timeout))) + return SessionMCPDisableResult.from_dict(await self._client.request("session.mcp.disable", params_dict, **_timeout_kwargs(timeout))) - async def reload(self, *, timeout: float | None = None) -> SessionMcpReloadResult: - return SessionMcpReloadResult.from_dict(await self._client.request("session.mcp.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def reload(self, *, timeout: float | None = None) -> SessionMCPReloadResult: + return SessionMCPReloadResult.from_dict(await self._client.request("session.mcp.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) class PluginsApi: diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index ec5958676..0f585c81b 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -155,7 +155,7 @@ async def test_should_deselect_current_agent(self): @pytest.mark.asyncio async def test_should_return_empty_list_when_no_custom_agents_configured(self): - """Test listing agents returns empty when none configured.""" + """Test listing agents returns no custom agents when none configured.""" client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True)) try: @@ -165,7 +165,13 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): ) result = await session.rpc.agent.list() - assert result.agents == [] + # The CLI may return built-in/default agents even when no custom agents + # are configured. Verify no custom test agents appear in the list. + custom_names = {"test-agent", "another-agent"} + for agent in result.agents: + assert agent.name not in custom_names, ( + f"Expected no custom agents, but found {agent.name!r}" + ) await session.disconnect() await client.stop() diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index cb5d90cd2..4a6cbc27b 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -190,6 +190,7 @@ async def test_both_clients_see_tool_request_and_completion_events( ): """Both clients see tool request and completion events.""" + class SeedParams(BaseModel): seed: str = Field(description="A seed value") diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index 1ebc50797..21c329eb1 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -44,6 +44,77 @@ function toGoFieldName(jsonName: string): string { .join(""); } +/** + * Post-process Go enum constants so every constant follows the canonical + * Go `TypeNameValue` convention. quicktype disambiguates collisions with + * whimsical prefixes (Purple, Fluffy, …) that we replace. + */ +function postProcessEnumConstants(code: string): string { + const renames = new Map(); + + // Match constant declarations inside const ( … ) blocks. + const constLineRe = /^\s+(\w+)\s+(\w+)\s*=\s*"([^"]+)"/gm; + let m; + while ((m = constLineRe.exec(code)) !== null) { + const [, constName, typeName, value] = m; + if (constName.startsWith(typeName)) continue; + + // Use the same initialism logic as toPascalCase so "url" → "URL", "mcp" → "MCP", etc. + const valuePascal = value + .split(/[._-]/) + .map((w) => goInitialisms.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)) + .join(""); + const desired = typeName + valuePascal; + if (constName !== desired) { + renames.set(constName, desired); + } + } + + // Replace each const block in place, then fix switch-case references + // in marshal/unmarshal functions. This avoids renaming struct fields. + + // Phase 1: Rename inside const ( … ) blocks + code = code.replace(/^(const \([\s\S]*?\n\))/gm, (block) => { + let b = block; + for (const [oldName, newName] of renames) { + b = b.replace(new RegExp(`\\b${oldName}\\b`, "g"), newName); + } + return b; + }); + + // Phase 2: Rename inside func bodies (marshal/unmarshal helpers use case statements) + code = code.replace(/^(func \([\s\S]*?\n\})/gm, (funcBlock) => { + let b = funcBlock; + for (const [oldName, newName] of renames) { + b = b.replace(new RegExp(`\\b${oldName}\\b`, "g"), newName); + } + return b; + }); + + return code; +} + +/** + * Extract a mapping from (structName, jsonFieldName) → goFieldName + * so the wrapper code references the actual quicktype-generated field names. + */ +function extractFieldNames(qtCode: string): Map> { + const result = new Map>(); + const structRe = /^type\s+(\w+)\s+struct\s*\{([^}]*)\}/gm; + let sm; + while ((sm = structRe.exec(qtCode)) !== null) { + const [, structName, body] = sm; + const fields = new Map(); + const fieldRe = /^\s+(\w+)\s+[^`\n]+`json:"([^",]+)/gm; + let fm; + while ((fm = fieldRe.exec(body)) !== null) { + fields.set(fm[2], fm[1]); + } + result.set(structName, fields); + } + return result; +} + async function formatGoFile(filePath: string): Promise { try { await execFileAsync("go", ["fmt", filePath]); @@ -92,7 +163,7 @@ async function generateSessionEvents(schemaPath?: string): Promise { `; - const outPath = await writeGeneratedFile("go/generated_session_events.go", banner + result.lines.join("\n")); + const outPath = await writeGeneratedFile("go/generated_session_events.go", banner + postProcessEnumConstants(result.lines.join("\n"))); console.log(` ✓ ${outPath}`); await formatGoFile(outPath); @@ -153,6 +224,22 @@ async function generateRpc(schemaPath?: string): Promise { rendererOptions: { package: "copilot", "just-types": "true" }, }); + // Post-process quicktype output: fix enum constant names + let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n"); + qtCode = postProcessEnumConstants(qtCode); + + // Extract actual type names generated by quicktype (may differ from toPascalCase) + const actualTypeNames = new Map(); + const structRe = /^type\s+(\w+)\s+struct\b/gm; + let sm; + while ((sm = structRe.exec(qtCode)) !== null) { + actualTypeNames.set(sm[1].toLowerCase(), sm[1]); + } + const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name; + + // Extract field name mappings (quicktype may rename fields to avoid Go keyword conflicts) + const fieldNames = extractFieldNames(qtCode); + // Build method wrappers const lines: string[] = []; lines.push(`// AUTO-GENERATED FILE - DO NOT EDIT`); @@ -168,19 +255,18 @@ async function generateRpc(schemaPath?: string): Promise { lines.push(`)`); lines.push(``); - // Add quicktype-generated types (skip package line) - const qtLines = qtResult.lines.filter((l) => !l.startsWith("package ")); - lines.push(...qtLines); + // Add quicktype-generated types + lines.push(qtCode); lines.push(``); // Emit ServerRpc if (schema.server) { - emitRpcWrapper(lines, schema.server, false); + emitRpcWrapper(lines, schema.server, false, resolveType, fieldNames); } // Emit SessionRpc if (schema.session) { - emitRpcWrapper(lines, schema.session, true); + emitRpcWrapper(lines, schema.session, true, resolveType, fieldNames); } const outPath = await writeGeneratedFile("go/rpc/generated_rpc.go", lines.join("\n")); @@ -189,7 +275,7 @@ async function generateRpc(schemaPath?: string): Promise { await formatGoFile(outPath); } -function emitRpcWrapper(lines: string[], node: Record, isSession: boolean): void { +function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map>): void { const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v)); @@ -205,7 +291,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, apiName, key, value, isSession); + emitMethod(lines, apiName, key, value, isSession, resolveType, fieldNames); } } @@ -224,7 +310,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods (server only) for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, wrapperName, key, value, isSession); + emitMethod(lines, wrapperName, key, value, isSession, resolveType, fieldNames); } // Constructor @@ -244,15 +330,15 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); } -function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean): void { +function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map>): void { const methodName = toPascalCase(name); - const resultType = toPascalCase(method.rpcMethod) + "Result"; + const resultType = resolveType(toPascalCase(method.rpcMethod) + "Result"); const paramProps = method.params?.properties || {}; const requiredParams = new Set(method.params?.required || []); const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId"); const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0; - const paramsType = hasParams ? toPascalCase(method.rpcMethod) + "Params" : ""; + const paramsType = hasParams ? resolveType(toPascalCase(method.rpcMethod) + "Params") : ""; const sig = hasParams ? `func (a *${receiver}) ${methodName}(ctx context.Context, params *${paramsType}) (*${resultType}, error)` @@ -265,7 +351,7 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc if (hasParams) { lines.push(` if params != nil {`); for (const pName of nonSessionParams) { - const goField = toGoFieldName(pName); + const goField = fieldNames.get(paramsType)?.get(pName) ?? toGoFieldName(pName); const isOptional = !requiredParams.has(pName); if (isOptional) { // Optional fields are pointers - only add when non-nil and dereference diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 65563d741..6e2dd5dba 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -215,6 +215,16 @@ async function generateRpc(schemaPath?: string): Promise { // Modernize to Python 3.11+ syntax typesCode = modernizePython(typesCode); + // Extract actual class names generated by quicktype (may differ from toPascalCase, + // e.g. quicktype produces "SessionMCPList" not "SessionMcpList") + const actualTypeNames = new Map(); + const classRe = /^class\s+(\w+)\b/gm; + let cm; + while ((cm = classRe.exec(typesCode)) !== null) { + actualTypeNames.set(cm[1].toLowerCase(), cm[1]); + } + const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name; + const lines: string[] = []; lines.push(`""" AUTO-GENERATED FILE - DO NOT EDIT @@ -239,17 +249,17 @@ def _timeout_kwargs(timeout: float | None) -> dict: // Emit RPC wrapper classes if (schema.server) { - emitRpcWrapper(lines, schema.server, false); + emitRpcWrapper(lines, schema.server, false, resolveType); } if (schema.session) { - emitRpcWrapper(lines, schema.session, true); + emitRpcWrapper(lines, schema.session, true, resolveType); } const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", lines.join("\n")); console.log(` ✓ ${outPath}`); } -function emitRpcWrapper(lines: string[], node: Record, isSession: boolean): void { +function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string): void { const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v)); const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v)); @@ -272,7 +282,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio lines.push(``); for (const [key, value] of Object.entries(groupNode as Record)) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession); + emitMethod(lines, key, value, isSession, resolveType); } lines.push(``); } @@ -301,19 +311,19 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio // Top-level methods for (const [key, value] of topLevelMethods) { if (!isRpcMethod(value)) continue; - emitMethod(lines, key, value, isSession); + emitMethod(lines, key, value, isSession, resolveType); } lines.push(``); } -function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean): void { +function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string): void { const methodName = toSnakeCase(name); - const resultType = toPascalCase(method.rpcMethod) + "Result"; + const resultType = resolveType(toPascalCase(method.rpcMethod) + "Result"); const paramProps = method.params?.properties || {}; const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId"); const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0; - const paramsType = toPascalCase(method.rpcMethod) + "Params"; + const paramsType = resolveType(toPascalCase(method.rpcMethod) + "Params"); // Build signature with typed params + optional timeout const sig = hasParams