diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 9b6f60c45..fce4b4708 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -686,7 +686,7 @@ internal sealed class ModeSetRequest /// RPC data type for NameGet operations. public sealed class NameGetResult { - /// The session name, falling back to the auto-generated summary, or null if neither exists. + /// The session name (user-set or auto-generated), or null if not yet set. [JsonPropertyName("name")] public string? Name { get; set; } } @@ -829,6 +829,10 @@ public sealed class WorkspacesGetWorkspaceResultWorkspace /// Gets or sets the updated_at value. [JsonPropertyName("updated_at")] public DateTimeOffset? UpdatedAt { get; set; } + + /// Gets or sets the user_named value. + [JsonPropertyName("user_named")] + public bool? UserNamed { get; set; } } /// RPC data type for WorkspacesGetWorkspace operations. @@ -987,6 +991,10 @@ public sealed class AgentInfo /// Unique identifier of the custom agent. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + + /// Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. + [JsonPropertyName("path")] + public string? Path { get; set; } } /// RPC data type for AgentList operations. @@ -1074,6 +1082,286 @@ internal sealed class SessionAgentReloadRequest public string SessionId { get; set; } = string.Empty; } +/// RPC data type for TasksStartAgent operations. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksStartAgentResult +{ + /// Generated agent ID for the background task. + [JsonPropertyName("agentId")] + public string AgentId { get; set; } = string.Empty; +} + +/// RPC data type for TasksStartAgent operations. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksStartAgentRequest +{ + /// Type of agent to start (e.g., 'explore', 'task', 'general-purpose'). + [JsonPropertyName("agentType")] + public string AgentType { get; set; } = string.Empty; + + /// Short description of the task. + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// Optional model override. + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// Short name for the agent, used to generate a human-readable ID. + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Task prompt for the agent. + [JsonPropertyName("prompt")] + public string Prompt { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// Polymorphic base type discriminated by type. +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "type", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskInfoAgent), "agent")] +[JsonDerivedType(typeof(TaskInfoShell), "shell")] +public partial class TaskInfo +{ + /// The type discriminator. + [JsonPropertyName("type")] + public virtual string Type { get; set; } = string.Empty; +} + + +/// The agent variant of . +public partial class TaskInfoAgent : TaskInfo +{ + /// + [JsonIgnore] + public override string Type => "agent"; + + /// ISO 8601 timestamp when the current active period began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonConverter(typeof(MillisecondsTimeSpanConverter))] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeTimeMs")] + public TimeSpan? ActiveTimeMs { get; set; } + + /// Type of agent running this task. + [JsonPropertyName("agentType")] + public required string AgentType { get; set; } + + /// Whether the task is currently in the original sync wait and can be moved to background mode. False once it is already backgrounded, idle, finished, or no longer has a promotable sync waiter. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canPromoteToBackground")] + public bool? CanPromoteToBackground { get; set; } + + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Error message when the task failed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// How the agent is currently being managed by the runtime. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskAgentInfoExecutionMode? ExecutionMode { get; set; } + + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// ISO 8601 timestamp when the agent entered idle state. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } + + /// Most recent response text from the agent. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("latestResponse")] + public string? LatestResponse { get; set; } + + /// Model used for the task when specified. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// Prompt passed to the agent. + [JsonPropertyName("prompt")] + public required string Prompt { get; set; } + + /// Result text from the task when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public string? Result { get; set; } + + /// ISO 8601 timestamp when the task was started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// Current lifecycle status of the task. + [JsonPropertyName("status")] + public required TaskAgentInfoStatus Status { get; set; } + + /// Tool call ID associated with this agent task. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } +} + +/// The shell variant of . +public partial class TaskInfoShell : TaskInfo +{ + /// + [JsonIgnore] + public override string Type => "shell"; + + /// Whether the shell runs inside a managed PTY session or as an independent background process. + [JsonPropertyName("attachmentMode")] + public required TaskShellInfoAttachmentMode AttachmentMode { get; set; } + + /// Whether this shell task can be promoted to background mode. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("canPromoteToBackground")] + public bool? CanPromoteToBackground { get; set; } + + /// Command being executed. + [JsonPropertyName("command")] + public required string Command { get; set; } + + /// ISO 8601 timestamp when the task finished. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Short description of the task. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Whether the shell command is currently sync-waited or background-managed. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("executionMode")] + public TaskShellInfoExecutionMode? ExecutionMode { get; set; } + + /// Unique task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// Path to the detached shell log, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("logPath")] + public string? LogPath { get; set; } + + /// Process ID when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("pid")] + public long? Pid { get; set; } + + /// ISO 8601 timestamp when the task was started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// Current lifecycle status of the task. + [JsonPropertyName("status")] + public required TaskShellInfoStatus Status { get; set; } +} + +/// RPC data type for TaskList operations. +[Experimental(Diagnostics.Experimental)] +public sealed class TaskList +{ + /// Currently tracked tasks. + [JsonPropertyName("tasks")] + public IList Tasks { get => field ??= []; set; } +} + +/// RPC data type for SessionTasksList operations. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionTasksListRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for TasksPromoteToBackground operations. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksPromoteToBackgroundResult +{ + /// Whether the task was successfully promoted to background mode. + [JsonPropertyName("promoted")] + public bool Promoted { get; set; } +} + +/// RPC data type for TasksPromoteToBackground operations. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksPromoteToBackgroundRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for TasksCancel operations. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksCancelResult +{ + /// Whether the task was successfully cancelled. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} + +/// RPC data type for TasksCancel operations. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksCancelRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for TasksRemove operations. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksRemoveResult +{ + /// Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). + [JsonPropertyName("removed")] + public bool Removed { get; set; } +} + +/// RPC data type for TasksRemove operations. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksRemoveRequest +{ + /// Task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// RPC data type for Skill operations. public sealed class Skill { @@ -2539,6 +2827,89 @@ public enum InstructionsSourcesType } +/// How the agent is currently being managed by the runtime. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TaskAgentInfoExecutionMode +{ + /// The sync variant. + [JsonStringEnumMemberName("sync")] + Sync, + /// The background variant. + [JsonStringEnumMemberName("background")] + Background, +} + + +/// Current lifecycle status of the task. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TaskAgentInfoStatus +{ + /// The running variant. + [JsonStringEnumMemberName("running")] + Running, + /// The idle variant. + [JsonStringEnumMemberName("idle")] + Idle, + /// The completed variant. + [JsonStringEnumMemberName("completed")] + Completed, + /// The failed variant. + [JsonStringEnumMemberName("failed")] + Failed, + /// The cancelled variant. + [JsonStringEnumMemberName("cancelled")] + Cancelled, +} + + +/// Whether the shell runs inside a managed PTY session or as an independent background process. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TaskShellInfoAttachmentMode +{ + /// The attached variant. + [JsonStringEnumMemberName("attached")] + Attached, + /// The detached variant. + [JsonStringEnumMemberName("detached")] + Detached, +} + + +/// Whether the shell command is currently sync-waited or background-managed. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TaskShellInfoExecutionMode +{ + /// The sync variant. + [JsonStringEnumMemberName("sync")] + Sync, + /// The background variant. + [JsonStringEnumMemberName("background")] + Background, +} + + +/// Current lifecycle status of the task. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum TaskShellInfoStatus +{ + /// The running variant. + [JsonStringEnumMemberName("running")] + Running, + /// The idle variant. + [JsonStringEnumMemberName("idle")] + Idle, + /// The completed variant. + [JsonStringEnumMemberName("completed")] + Completed, + /// The failed variant. + [JsonStringEnumMemberName("failed")] + Failed, + /// The cancelled variant. + [JsonStringEnumMemberName("cancelled")] + Cancelled, +} + + /// Configuration source: user, workspace, plugin, or builtin. [JsonConverter(typeof(JsonStringEnumConverter))] public enum McpServerSource @@ -2943,6 +3314,7 @@ internal SessionRpc(JsonRpc rpc, string sessionId) Instructions = new InstructionsApi(rpc, sessionId); Fleet = new FleetApi(rpc, sessionId); Agent = new AgentApi(rpc, sessionId); + Tasks = new TasksApi(rpc, sessionId); Skills = new SkillsApi(rpc, sessionId); Mcp = new McpApi(rpc, sessionId); Plugins = new PluginsApi(rpc, sessionId); @@ -2983,6 +3355,9 @@ internal SessionRpc(JsonRpc rpc, string sessionId) /// Agent APIs. public AgentApi Agent { get; } + /// Tasks APIs. + public TasksApi Tasks { get; } + /// Skills APIs. public SkillsApi Skills { get; } @@ -3290,6 +3665,55 @@ public async Task ReloadAsync(CancellationToken cancellationT } } +/// Provides session-scoped Tasks APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal TasksApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.tasks.startAgent". + public async Task StartAgentAsync(string agentType, string prompt, string name, string? description = null, string? model = null, CancellationToken cancellationToken = default) + { + var request = new TasksStartAgentRequest { SessionId = _sessionId, AgentType = agentType, Prompt = prompt, Name = name, Description = description, Model = model }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.tasks.startAgent", [request], cancellationToken); + } + + /// Calls "session.tasks.list". + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var request = new SessionTasksListRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.tasks.list", [request], cancellationToken); + } + + /// Calls "session.tasks.promoteToBackground". + public async Task PromoteToBackgroundAsync(string id, CancellationToken cancellationToken = default) + { + var request = new TasksPromoteToBackgroundRequest { SessionId = _sessionId, Id = id }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.tasks.promoteToBackground", [request], cancellationToken); + } + + /// Calls "session.tasks.cancel". + public async Task CancelAsync(string id, CancellationToken cancellationToken = default) + { + var request = new TasksCancelRequest { SessionId = _sessionId, Id = id }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.tasks.cancel", [request], cancellationToken); + } + + /// Calls "session.tasks.remove". + public async Task RemoveAsync(string id, CancellationToken cancellationToken = default) + { + var request = new TasksRemoveRequest { SessionId = _sessionId, Id = id }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.tasks.remove", [request], cancellationToken); + } +} + /// Provides session-scoped Skills APIs. [Experimental(Diagnostics.Experimental)] public sealed class SkillsApi @@ -3905,6 +4329,7 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncFailed LLM API call metadata for telemetry. +/// Represents the model.call_failure event. +public partial class ModelCallFailureEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "model.call_failure"; + + /// The model.call_failure event payload. + [JsonPropertyName("data")] + public required ModelCallFailureData Data { get; set; } +} + /// Turn abort information including the reason for termination. /// Represents the abort event. public partial class AbortEvent : SessionEvent @@ -1272,6 +1286,11 @@ public partial class SessionInfoData [JsonPropertyName("message")] public required string Message { get; set; } + /// Optional actionable tip displayed with this message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tip")] + public string? Tip { get; set; } + /// Optional URL associated with this message that the user can open in a browser. [Url] [StringSyntax(StringSyntaxAttribute.Uri)] @@ -1961,6 +1980,49 @@ public partial class AssistantUsageData public double? TtftMs { get; set; } } +/// Failed LLM API call metadata for telemetry. +public partial class ModelCallFailureData +{ + /// Completion ID from the model provider (e.g., chatcmpl-abc123). + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("apiCallId")] + public string? ApiCallId { get; set; } + + /// Duration of the failed API call in milliseconds. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("durationMs")] + public double? DurationMs { get; set; } + + /// Raw provider/runtime error message for restricted telemetry. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorMessage")] + public string? ErrorMessage { get; set; } + + /// What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("initiator")] + public string? Initiator { get; set; } + + /// Model identifier used for the failed API call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// GitHub request tracing ID (x-github-request-id header) for server-side log correlation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("providerCallId")] + public string? ProviderCallId { get; set; } + + /// Where the failed model call originated. + [JsonPropertyName("source")] + public required ModelCallFailureSource Source { get; set; } + + /// HTTP status code from the failed request. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("statusCode")] + public long? StatusCode { get; set; } +} + /// Turn abort information including the reason for termination. public partial class AbortData { @@ -3552,7 +3614,7 @@ public partial class SystemNotificationNewInboxMessage : SystemNotification [JsonPropertyName("senderName")] public required string SenderName { get; set; } - /// Category of the sender (e.g., ambient-agent, plugin, hook). + /// Category of the sender (e.g., sidekick-agent, plugin, hook). [JsonPropertyName("senderType")] public required string SenderType { get; set; } @@ -4485,6 +4547,21 @@ public enum AssistantMessageToolRequestType Custom, } +/// Where the failed model call originated. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ModelCallFailureSource +{ + /// The top_level variant. + [JsonStringEnumMemberName("top_level")] + TopLevel, + /// The subagent variant. + [JsonStringEnumMemberName("subagent")] + Subagent, + /// The mcp_sampling variant. + [JsonStringEnumMemberName("mcp_sampling")] + McpSampling, +} + /// Theme variant this icon is intended for. [JsonConverter(typeof(JsonStringEnumConverter))] public enum ToolExecutionCompleteContentResourceLinkIconTheme @@ -4794,6 +4871,8 @@ public enum ExtensionsLoadedExtensionStatus [JsonSerializable(typeof(McpOauthRequiredEvent))] [JsonSerializable(typeof(McpOauthRequiredStaticClientConfig))] [JsonSerializable(typeof(McpServersLoadedServer))] +[JsonSerializable(typeof(ModelCallFailureData))] +[JsonSerializable(typeof(ModelCallFailureEvent))] [JsonSerializable(typeof(PendingMessagesModifiedData))] [JsonSerializable(typeof(PendingMessagesModifiedEvent))] [JsonSerializable(typeof(PermissionCompletedData))] diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 1d5cf8839..48bbcd6ce 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -95,6 +95,7 @@ public IReadOnlyDictionary GetEnvironment() .ToDictionary(e => (string)e.Key, e => e.Value?.ToString()); env["COPILOT_API_URL"] = ProxyUrl; + env["COPILOT_HOME"] = HomeDir; env["XDG_CONFIG_HOME"] = HomeDir; env["XDG_STATE_HOME"] = HomeDir; diff --git a/dotnet/test/SessionFsTests.cs b/dotnet/test/SessionFsTests.cs index 1d0e6d2e5..a007a6c30 100644 --- a/dotnet/test/SessionFsTests.cs +++ b/dotnet/test/SessionFsTests.cs @@ -16,7 +16,7 @@ public class SessionFsTests(E2ETestFixture fixture, ITestOutputHelper output) private static readonly SessionFsConfig SessionFsConfig = new() { InitialCwd = "/", - SessionStatePath = "/session-state", + SessionStatePath = CreateSessionStatePath(), Conventions = SessionFsSetProviderConventions.Posix, }; @@ -38,7 +38,7 @@ public async Task Should_Route_File_Operations_Through_The_Session_Fs_Provider() Assert.Contains("300", msg?.Data.Content ?? string.Empty); await session.DisposeAsync(); - var eventsPath = GetStoredPath(providerRoot, session.SessionId, "/session-state/events.jsonl"); + var eventsPath = GetStoredPath(providerRoot, session.SessionId, $"{SessionFsConfig.SessionStatePath}/events.jsonl"); await WaitForConditionAsync(() => File.Exists(eventsPath)); var content = await ReadAllTextSharedAsync(eventsPath); Assert.Contains("300", content); @@ -69,7 +69,7 @@ public async Task Should_Load_Session_Data_From_Fs_Provider_On_Resume() Assert.Contains("100", msg?.Data.Content ?? string.Empty); await session1.DisposeAsync(); - var eventsPath = GetStoredPath(providerRoot, sessionId, "/session-state/events.jsonl"); + var eventsPath = GetStoredPath(providerRoot, sessionId, $"{SessionFsConfig.SessionStatePath}/events.jsonl"); await WaitForConditionAsync(() => File.Exists(eventsPath)); var session2 = await client.ResumeSessionAsync(sessionId, new ResumeSessionConfig @@ -165,11 +165,11 @@ await session.SendAndWaitAsync(new MessageOptions var messages = await session.GetMessagesAsync(); var toolResult = FindToolCallResult(messages, "get_big_string"); Assert.NotNull(toolResult); - Assert.Contains("/session-state/temp/", toolResult); + Assert.Contains($"{SessionFsConfig.SessionStatePath}/temp/", toolResult); var match = System.Text.RegularExpressions.Regex.Match( toolResult!, - @"([/\\]session-state[/\\]temp[/\\][^\s]+)"); + $"({System.Text.RegularExpressions.Regex.Escape(SessionFsConfig.SessionStatePath)}/temp/[^\\s]+)"); Assert.True(match.Success); var fileContent = await ReadAllTextSharedAsync(GetStoredPath(providerRoot, session.SessionId, match.Groups[1].Value)); @@ -206,7 +206,7 @@ public async Task Should_Succeed_With_Compaction_While_Using_SessionFs() await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" }); - var eventsPath = GetStoredPath(providerRoot, session.SessionId, "/session-state/events.jsonl"); + var eventsPath = GetStoredPath(providerRoot, session.SessionId, $"{SessionFsConfig.SessionStatePath}/events.jsonl"); await WaitForConditionAsync(() => File.Exists(eventsPath), TimeSpan.FromSeconds(30)); var contentBefore = await ReadAllTextSharedAsync(eventsPath); Assert.DoesNotContain("checkpointNumber", contentBefore); @@ -244,13 +244,13 @@ public async Task Should_Write_Workspace_Metadata_Via_SessionFs() Assert.Contains("56", msg?.Data.Content ?? string.Empty); // WorkspaceManager should have created workspace.yaml via sessionFs - var workspaceYamlPath = GetStoredPath(providerRoot, session.SessionId, "/session-state/workspace.yaml"); + var workspaceYamlPath = GetStoredPath(providerRoot, session.SessionId, $"{SessionFsConfig.SessionStatePath}/workspace.yaml"); await WaitForConditionAsync(() => File.Exists(workspaceYamlPath)); var yaml = await ReadAllTextSharedAsync(workspaceYamlPath); Assert.Contains("id:", yaml); // Checkpoint index should also exist - var indexPath = GetStoredPath(providerRoot, session.SessionId, "/session-state/checkpoints/index.md"); + var indexPath = GetStoredPath(providerRoot, session.SessionId, $"{SessionFsConfig.SessionStatePath}/checkpoints/index.md"); await WaitForConditionAsync(() => File.Exists(indexPath)); await session.DisposeAsync(); @@ -278,7 +278,7 @@ public async Task Should_Persist_Plan_Md_Via_SessionFs() await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2 + 3?" }); await session.Rpc.Plan.UpdateAsync("# Test Plan\n\nThis is a test."); - var planPath = GetStoredPath(providerRoot, session.SessionId, "/session-state/plan.md"); + var planPath = GetStoredPath(providerRoot, session.SessionId, $"{SessionFsConfig.SessionStatePath}/plan.md"); await WaitForConditionAsync(() => File.Exists(planPath)); var content = await ReadAllTextSharedAsync(planPath); Assert.Contains("# Test Plan", content); @@ -323,6 +323,17 @@ private CopilotClient CreateSessionFsClient(string providerRoot, bool useStdio = private static string CreateProviderRoot() => Path.Join(Path.GetTempPath(), $"copilot-sessionfs-{Guid.NewGuid():N}"); + private static string CreateSessionStatePath() + { + if (OperatingSystem.IsWindows()) + { + return "/session-state"; + } + + return Path.Join(Path.GetTempPath(), $"copilot-sessionfs-state-{Guid.NewGuid():N}", "session-state") + .Replace(Path.DirectorySeparatorChar, '/'); + } + private static string GetStoredPath(string providerRoot, string sessionId, string sessionPath) { var safeSessionId = NormalizeRelativePathSegment(sessionId, nameof(sessionId)); diff --git a/go/generated_session_events.go b/go/generated_session_events.go index 8b393bec9..19a376e7a 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -263,6 +263,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeModelCallFailure: + var d ModelCallFailureData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAbort: var d AbortData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -588,6 +594,7 @@ const ( SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta" SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end" SessionEventTypeAssistantUsage SessionEventType = "assistant.usage" + SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" SessionEventTypeAbort SessionEventType = "abort" SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested" SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start" @@ -903,6 +910,28 @@ type ExternalToolRequestedData struct { func (*ExternalToolRequestedData) sessionEventData() {} +// Failed LLM API call metadata for telemetry +type ModelCallFailureData struct { + // Completion ID from the model provider (e.g., chatcmpl-abc123) + APICallID *string `json:"apiCallId,omitempty"` + // Duration of the failed API call in milliseconds + DurationMs *float64 `json:"durationMs,omitempty"` + // Raw provider/runtime error message for restricted telemetry + ErrorMessage *string `json:"errorMessage,omitempty"` + // What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls + Initiator *string `json:"initiator,omitempty"` + // Model identifier used for the failed API call + Model *string `json:"model,omitempty"` + // GitHub request tracing ID (x-github-request-id header) for server-side log correlation + ProviderCallID *string `json:"providerCallId,omitempty"` + // Where the failed model call originated + Source ModelCallFailureSource `json:"source"` + // HTTP status code from the failed request + StatusCode *int64 `json:"statusCode,omitempty"` +} + +func (*ModelCallFailureData) sessionEventData() {} + // Hook invocation completion details including output, success status, and error information type HookEndData struct { // Error details when the hook failed @@ -937,6 +966,8 @@ type SessionInfoData struct { InfoType string `json:"infoType"` // Human-readable informational message for display in the timeline Message string `json:"message"` + // Optional actionable tip displayed with this message + Tip *string `json:"tip,omitempty"` // Optional URL associated with this message that the user can open in a browser URL *string `json:"url,omitempty"` } @@ -2028,7 +2059,7 @@ type SystemNotification struct { Prompt *string `json:"prompt,omitempty"` // Human-readable name of the sender SenderName *string `json:"senderName,omitempty"` - // Category of the sender (e.g., ambient-agent, plugin, hook) + // Category of the sender (e.g., sidekick-agent, plugin, hook) SenderType *string `json:"senderType,omitempty"` // Unique identifier of the shell session ShellID *string `json:"shellId,omitempty"` @@ -2464,6 +2495,15 @@ const ( PermissionRequestMemoryDirectionDownvote PermissionRequestMemoryDirection = "downvote" ) +// Where the failed model call originated +type ModelCallFailureSource string + +const ( + ModelCallFailureSourceTopLevel ModelCallFailureSource = "top_level" + ModelCallFailureSourceSubagent ModelCallFailureSource = "subagent" + ModelCallFailureSourceMcpSampling ModelCallFailureSource = "mcp_sampling" +) + // Whether the agent completed successfully or failed type SystemNotificationAgentCompletedStatus string diff --git a/go/internal/e2e/session_fs_test.go b/go/internal/e2e/session_fs_test.go index 05cbd23b4..85a6a24b9 100644 --- a/go/internal/e2e/session_fs_test.go +++ b/go/internal/e2e/session_fs_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "regexp" + "runtime" "strings" "testing" "time" @@ -17,6 +18,12 @@ import ( func TestSessionFs(t *testing.T) { ctx := testharness.NewTestContext(t) providerRoot := t.TempDir() + sessionStatePath := createSessionStatePath(t) + sessionFsConfig := &copilot.SessionFsConfig{ + InitialCwd: "/", + SessionStatePath: sessionStatePath, + Conventions: rpc.SessionFSSetProviderConventionsPosix, + } createSessionFsHandler := func(session *copilot.Session) copilot.SessionFsProvider { return &testSessionFsHandler{ root: providerRoot, @@ -60,7 +67,7 @@ func TestSessionFs(t *testing.T) { t.Fatalf("Failed to disconnect session: %v", err) } - events, err := os.ReadFile(p(session.SessionID, "/session-state/events.jsonl")) + events, err := os.ReadFile(p(session.SessionID, sessionStatePath+"/events.jsonl")) if err != nil { t.Fatalf("Failed to read events file: %v", err) } @@ -98,7 +105,7 @@ func TestSessionFs(t *testing.T) { t.Fatalf("Failed to disconnect first session: %v", err) } - if _, err := os.Stat(p(sessionID, "/session-state/events.jsonl")); err != nil { + if _, err := os.Stat(p(sessionID, sessionStatePath+"/events.jsonl")); err != nil { t.Fatalf("Expected events file to exist before resume: %v", err) } @@ -189,10 +196,10 @@ func TestSessionFs(t *testing.T) { t.Fatalf("Failed to get messages: %v", err) } toolResult := findToolCallResult(messages, "get_big_string") - if !strings.Contains(toolResult, "/session-state/temp/") { - t.Fatalf("Expected tool result to reference /session-state/temp/, got %q", toolResult) + if !strings.Contains(toolResult, sessionStatePath+"/temp/") { + t.Fatalf("Expected tool result to reference %s/temp/, got %q", sessionStatePath, toolResult) } - match := regexp.MustCompile(`(/session-state/temp/[^\s]+)`).FindStringSubmatch(toolResult) + match := regexp.MustCompile(`(` + regexp.QuoteMeta(sessionStatePath) + `/temp/[^\s]+)`).FindStringSubmatch(toolResult) if len(match) < 2 { t.Fatalf("Expected temp file path in tool result, got %q", toolResult) } @@ -221,7 +228,7 @@ func TestSessionFs(t *testing.T) { t.Fatalf("Failed to send message: %v", err) } - eventsPath := p(session.SessionID, "/session-state/events.jsonl") + eventsPath := p(session.SessionID, sessionStatePath+"/events.jsonl") if err := waitForFile(eventsPath, 5*time.Second); err != nil { t.Fatalf("Timed out waiting for events file: %v", err) } @@ -271,7 +278,7 @@ func TestSessionFs(t *testing.T) { } // WorkspaceManager should have created workspace.yaml via sessionFs - workspaceYamlPath := p(session.SessionID, "/session-state/workspace.yaml") + workspaceYamlPath := p(session.SessionID, sessionStatePath+"/workspace.yaml") if err := waitForFile(workspaceYamlPath, 5*time.Second); err != nil { t.Fatalf("Timed out waiting for workspace.yaml: %v", err) } @@ -284,7 +291,7 @@ func TestSessionFs(t *testing.T) { } // Checkpoint index should also exist - indexPath := p(session.SessionID, "/session-state/checkpoints/index.md") + indexPath := p(session.SessionID, sessionStatePath+"/checkpoints/index.md") if err := waitForFile(indexPath, 5*time.Second); err != nil { t.Fatalf("Timed out waiting for checkpoints/index.md: %v", err) } @@ -313,7 +320,7 @@ func TestSessionFs(t *testing.T) { t.Fatalf("Failed to update plan: %v", err) } - planPath := p(session.SessionID, "/session-state/plan.md") + planPath := p(session.SessionID, sessionStatePath+"/plan.md") if err := waitForFile(planPath, 5*time.Second); err != nil { t.Fatalf("Timed out waiting for plan.md: %v", err) } @@ -331,10 +338,12 @@ func TestSessionFs(t *testing.T) { }) } -var sessionFsConfig = &copilot.SessionFsConfig{ - InitialCwd: "/", - SessionStatePath: "/session-state", - Conventions: rpc.SessionFSSetProviderConventionsPosix, +func createSessionStatePath(t *testing.T) string { + t.Helper() + if runtime.GOOS == "windows" { + return "/session-state" + } + return filepath.ToSlash(filepath.Join(t.TempDir(), "session-state")) } type testSessionFsHandler struct { diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 9f73c2267..a2d684706 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -156,6 +156,7 @@ func (c *TestContext) Env() []string { // Add overrides (later values take precedence in most systems) env = append(env, "COPILOT_API_URL="+c.ProxyURL, + "COPILOT_HOME="+c.HomeDir, "XDG_CONFIG_HOME="+c.HomeDir, "XDG_STATE_HOME="+c.HomeDir, ) diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 2efcb494f..311081247 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -184,6 +184,23 @@ type RPCTypes struct { SkillsEnableRequest SkillsEnableRequest `json:"SkillsEnableRequest"` SkillsEnableResult SkillsEnableResult `json:"SkillsEnableResult"` SkillsReloadResult SkillsReloadResult `json:"SkillsReloadResult"` + TaskAgentInfo TaskAgentInfo `json:"TaskAgentInfo"` + TaskAgentInfoExecutionMode TaskInfoExecutionMode `json:"TaskAgentInfoExecutionMode"` + TaskAgentInfoStatus TaskInfoStatus `json:"TaskAgentInfoStatus"` + TaskInfo TaskInfo `json:"TaskInfo"` + TaskList TaskList `json:"TaskList"` + TasksCancelRequest TasksCancelRequest `json:"TasksCancelRequest"` + TasksCancelResult TasksCancelResult `json:"TasksCancelResult"` + TaskShellInfo TaskShellInfo `json:"TaskShellInfo"` + TaskShellInfoAttachmentMode TaskShellInfoAttachmentMode `json:"TaskShellInfoAttachmentMode"` + TaskShellInfoExecutionMode TaskInfoExecutionMode `json:"TaskShellInfoExecutionMode"` + TaskShellInfoStatus TaskInfoStatus `json:"TaskShellInfoStatus"` + TasksPromoteToBackgroundRequest TasksPromoteToBackgroundRequest `json:"TasksPromoteToBackgroundRequest"` + TasksPromoteToBackgroundResult TasksPromoteToBackgroundResult `json:"TasksPromoteToBackgroundResult"` + TasksRemoveRequest TasksRemoveRequest `json:"TasksRemoveRequest"` + TasksRemoveResult TasksRemoveResult `json:"TasksRemoveResult"` + TasksStartAgentRequest TasksStartAgentRequest `json:"TasksStartAgentRequest"` + TasksStartAgentResult TasksStartAgentResult `json:"TasksStartAgentResult"` Tool Tool `json:"Tool"` ToolCallResult ToolCallResult `json:"ToolCallResult"` ToolList ToolList `json:"ToolList"` @@ -273,6 +290,9 @@ type AgentInfo struct { DisplayName string `json:"displayName"` // Unique identifier of the custom agent Name string `json:"name"` + // Absolute local file path of the agent definition. Only set for file-based agents loaded + // from disk; remote agents do not have a path. + Path *string `json:"path,omitempty"` } // Experimental: AgentList is part of an experimental API and may change or be removed. @@ -777,7 +797,7 @@ type ModelsListRequest struct { } type NameGetResult struct { - // The session name, falling back to the auto-generated summary, or null if neither exists + // The session name (user-set or auto-generated), or null if not yet set Name *string `json:"name"` } @@ -1301,6 +1321,192 @@ type SkillsEnableResult struct { type SkillsReloadResult struct { } +type TaskAgentInfo struct { + // ISO 8601 timestamp when the current active period began + ActiveStartedAt *time.Time `json:"activeStartedAt,omitempty"` + // Accumulated active execution time in milliseconds + ActiveTimeMS *int64 `json:"activeTimeMs,omitempty"` + // Type of agent running this task + AgentType string `json:"agentType"` + // Whether the task is currently in the original sync wait and can be moved to background + // mode. False once it is already backgrounded, idle, finished, or no longer has a + // promotable sync waiter. + CanPromoteToBackground *bool `json:"canPromoteToBackground,omitempty"` + // ISO 8601 timestamp when the task finished + CompletedAt *time.Time `json:"completedAt,omitempty"` + // Short description of the task + Description string `json:"description"` + // Error message when the task failed + Error *string `json:"error,omitempty"` + // How the agent is currently being managed by the runtime + ExecutionMode *TaskInfoExecutionMode `json:"executionMode,omitempty"` + // Unique task identifier + ID string `json:"id"` + // ISO 8601 timestamp when the agent entered idle state + IdleSince *time.Time `json:"idleSince,omitempty"` + // Most recent response text from the agent + LatestResponse *string `json:"latestResponse,omitempty"` + // Model used for the task when specified + Model *string `json:"model,omitempty"` + // Prompt passed to the agent + Prompt string `json:"prompt"` + // Result text from the task when available + Result *string `json:"result,omitempty"` + // ISO 8601 timestamp when the task was started + StartedAt time.Time `json:"startedAt"` + // Current lifecycle status of the task + Status TaskInfoStatus `json:"status"` + // Tool call ID associated with this agent task + ToolCallID string `json:"toolCallId"` + // Task kind + Type TaskAgentInfoType `json:"type"` +} + +type TaskInfo struct { + // ISO 8601 timestamp when the current active period began + ActiveStartedAt *time.Time `json:"activeStartedAt,omitempty"` + // Accumulated active execution time in milliseconds + ActiveTimeMS *int64 `json:"activeTimeMs,omitempty"` + // Type of agent running this task + AgentType *string `json:"agentType,omitempty"` + // Whether the task is currently in the original sync wait and can be moved to background + // mode. False once it is already backgrounded, idle, finished, or no longer has a + // promotable sync waiter. + // + // Whether this shell task can be promoted to background mode + CanPromoteToBackground *bool `json:"canPromoteToBackground,omitempty"` + // ISO 8601 timestamp when the task finished + CompletedAt *time.Time `json:"completedAt,omitempty"` + // Short description of the task + Description string `json:"description"` + // Error message when the task failed + Error *string `json:"error,omitempty"` + // How the agent is currently being managed by the runtime + // + // Whether the shell command is currently sync-waited or background-managed + ExecutionMode *TaskInfoExecutionMode `json:"executionMode,omitempty"` + // Unique task identifier + ID string `json:"id"` + // ISO 8601 timestamp when the agent entered idle state + IdleSince *time.Time `json:"idleSince,omitempty"` + // Most recent response text from the agent + LatestResponse *string `json:"latestResponse,omitempty"` + // Model used for the task when specified + Model *string `json:"model,omitempty"` + // Prompt passed to the agent + Prompt *string `json:"prompt,omitempty"` + // Result text from the task when available + Result *string `json:"result,omitempty"` + // ISO 8601 timestamp when the task was started + StartedAt time.Time `json:"startedAt"` + // Current lifecycle status of the task + Status TaskInfoStatus `json:"status"` + // Tool call ID associated with this agent task + ToolCallID *string `json:"toolCallId,omitempty"` + // Task kind + Type TaskInfoType `json:"type"` + // Whether the shell runs inside a managed PTY session or as an independent background + // process + AttachmentMode *TaskShellInfoAttachmentMode `json:"attachmentMode,omitempty"` + // Command being executed + Command *string `json:"command,omitempty"` + // Path to the detached shell log, when available + LogPath *string `json:"logPath,omitempty"` + // Process ID when available + PID *int64 `json:"pid,omitempty"` +} + +// Experimental: TaskList is part of an experimental API and may change or be removed. +type TaskList struct { + // Currently tracked tasks + Tasks []TaskInfo `json:"tasks"` +} + +type TaskShellInfo struct { + // Whether the shell runs inside a managed PTY session or as an independent background + // process + AttachmentMode TaskShellInfoAttachmentMode `json:"attachmentMode"` + // Whether this shell task can be promoted to background mode + CanPromoteToBackground *bool `json:"canPromoteToBackground,omitempty"` + // Command being executed + Command string `json:"command"` + // ISO 8601 timestamp when the task finished + CompletedAt *time.Time `json:"completedAt,omitempty"` + // Short description of the task + Description string `json:"description"` + // Whether the shell command is currently sync-waited or background-managed + ExecutionMode *TaskInfoExecutionMode `json:"executionMode,omitempty"` + // Unique task identifier + ID string `json:"id"` + // Path to the detached shell log, when available + LogPath *string `json:"logPath,omitempty"` + // Process ID when available + PID *int64 `json:"pid,omitempty"` + // ISO 8601 timestamp when the task was started + StartedAt time.Time `json:"startedAt"` + // Current lifecycle status of the task + Status TaskInfoStatus `json:"status"` + // Task kind + Type TaskShellInfoType `json:"type"` +} + +// Experimental: TasksCancelRequest is part of an experimental API and may change or be removed. +type TasksCancelRequest struct { + // Task identifier + ID string `json:"id"` +} + +// Experimental: TasksCancelResult is part of an experimental API and may change or be removed. +type TasksCancelResult struct { + // Whether the task was successfully cancelled + Cancelled bool `json:"cancelled"` +} + +// Experimental: TasksPromoteToBackgroundRequest is part of an experimental API and may change or be removed. +type TasksPromoteToBackgroundRequest struct { + // Task identifier + ID string `json:"id"` +} + +// Experimental: TasksPromoteToBackgroundResult is part of an experimental API and may change or be removed. +type TasksPromoteToBackgroundResult struct { + // Whether the task was successfully promoted to background mode + Promoted bool `json:"promoted"` +} + +// Experimental: TasksRemoveRequest is part of an experimental API and may change or be removed. +type TasksRemoveRequest struct { + // Task identifier + ID string `json:"id"` +} + +// Experimental: TasksRemoveResult is part of an experimental API and may change or be removed. +type TasksRemoveResult struct { + // Whether the task was removed. Returns false if the task does not exist or is still + // running/idle (cancel it first). + Removed bool `json:"removed"` +} + +// Experimental: TasksStartAgentRequest is part of an experimental API and may change or be removed. +type TasksStartAgentRequest struct { + // Type of agent to start (e.g., 'explore', 'task', 'general-purpose') + AgentType string `json:"agentType"` + // Short description of the task + Description *string `json:"description,omitempty"` + // Optional model override + Model *string `json:"model,omitempty"` + // Short name for the agent, used to generate a human-readable ID + Name string `json:"name"` + // Task prompt for the agent + Prompt string `json:"prompt"` +} + +// Experimental: TasksStartAgentResult is part of an experimental API and may change or be removed. +type TasksStartAgentResult struct { + // Generated agent ID for the background task + AgentID string `json:"agentId"` +} + type Tool struct { // Description of what the tool does Description string `json:"description"` @@ -1585,6 +1791,7 @@ type WorkspaceClass struct { Summary *string `json:"summary,omitempty"` SummaryCount *int64 `json:"summary_count,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"` + UserNamed *bool `json:"user_named,omitempty"` } type WorkspacesListFilesResult struct { @@ -1867,6 +2074,55 @@ const ( ShellKillSignalSIGTERM ShellKillSignal = "SIGTERM" ) +// How the agent is currently being managed by the runtime +// +// Whether the shell command is currently sync-waited or background-managed +type TaskInfoExecutionMode string + +const ( + TaskInfoExecutionModeBackground TaskInfoExecutionMode = "background" + TaskInfoExecutionModeSync TaskInfoExecutionMode = "sync" +) + +// Current lifecycle status of the task +type TaskInfoStatus string + +const ( + TaskInfoStatusCancelled TaskInfoStatus = "cancelled" + TaskInfoStatusCompleted TaskInfoStatus = "completed" + TaskInfoStatusIdle TaskInfoStatus = "idle" + TaskInfoStatusFailed TaskInfoStatus = "failed" + TaskInfoStatusRunning TaskInfoStatus = "running" +) + +type TaskAgentInfoType string + +const ( + TaskAgentInfoTypeAgent TaskAgentInfoType = "agent" +) + +// Whether the shell runs inside a managed PTY session or as an independent background +// process +type TaskShellInfoAttachmentMode string + +const ( + TaskShellInfoAttachmentModeAttached TaskShellInfoAttachmentMode = "attached" + TaskShellInfoAttachmentModeDetached TaskShellInfoAttachmentMode = "detached" +) + +type TaskInfoType string + +const ( + TaskInfoTypeAgent TaskInfoType = "agent" + TaskInfoTypeShell TaskInfoType = "shell" +) + +type TaskShellInfoType string + +const ( + TaskShellInfoTypeShell TaskShellInfoType = "shell" +) + type UIElicitationArrayAnyOfFieldType string const ( @@ -2527,6 +2783,94 @@ func (a *AgentApi) Reload(ctx context.Context) (*AgentReloadResult, error) { return &result, nil } +// Experimental: TasksApi contains experimental APIs that may change or be removed. +type TasksApi sessionApi + +func (a *TasksApi) StartAgent(ctx context.Context, params *TasksStartAgentRequest) (*TasksStartAgentResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["agentType"] = params.AgentType + req["prompt"] = params.Prompt + req["name"] = params.Name + if params.Description != nil { + req["description"] = *params.Description + } + if params.Model != nil { + req["model"] = *params.Model + } + } + raw, err := a.client.Request("session.tasks.startAgent", req) + if err != nil { + return nil, err + } + var result TasksStartAgentResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *TasksApi) List(ctx context.Context) (*TaskList, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request("session.tasks.list", req) + if err != nil { + return nil, err + } + var result TaskList + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *TasksApi) PromoteToBackground(ctx context.Context, params *TasksPromoteToBackgroundRequest) (*TasksPromoteToBackgroundResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + } + raw, err := a.client.Request("session.tasks.promoteToBackground", req) + if err != nil { + return nil, err + } + var result TasksPromoteToBackgroundResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *TasksApi) Cancel(ctx context.Context, params *TasksCancelRequest) (*TasksCancelResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + } + raw, err := a.client.Request("session.tasks.cancel", req) + if err != nil { + return nil, err + } + var result TasksCancelResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *TasksApi) Remove(ctx context.Context, params *TasksRemoveRequest) (*TasksRemoveResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + } + raw, err := a.client.Request("session.tasks.remove", req) + if err != nil { + return nil, err + } + var result TasksRemoveResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: SkillsApi contains experimental APIs that may change or be removed. type SkillsApi sessionApi @@ -2992,6 +3336,7 @@ type SessionRpc struct { Instructions *InstructionsApi Fleet *FleetApi Agent *AgentApi + Tasks *TasksApi Skills *SkillsApi Mcp *McpApi Plugins *PluginsApi @@ -3042,6 +3387,7 @@ func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { r.Instructions = (*InstructionsApi)(&r.common) r.Fleet = (*FleetApi)(&r.common) r.Agent = (*AgentApi)(&r.common) + r.Tasks = (*TasksApi)(&r.common) r.Skills = (*SkillsApi)(&r.common) r.Mcp = (*McpApi)(&r.common) r.Plugins = (*PluginsApi)(&r.common) diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index ac03d9a99..f2a929d73 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.36-0", + "@github/copilot": "^1.0.39-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -663,26 +663,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.36-0.tgz", - "integrity": "sha512-M1mxNbdRkiQv4qApKgV33jK6AsA3TqlMAtKyaDv9sJzE/kZa4IRUAUrmO+3d3C+ojZa/Yffjy0/6dC6kllhI4g==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.39-0.tgz", + "integrity": "sha512-OuN6wGgUv0WQydOCUuhYRFwUwTkfktI9fGdSih+SKUE+nTZze8JBz8Sg68K0ZLlqdD0OcF0ac9wMAfunlutvsw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.36-0", - "@github/copilot-darwin-x64": "1.0.36-0", - "@github/copilot-linux-arm64": "1.0.36-0", - "@github/copilot-linux-x64": "1.0.36-0", - "@github/copilot-win32-arm64": "1.0.36-0", - "@github/copilot-win32-x64": "1.0.36-0" + "@github/copilot-darwin-arm64": "1.0.39-0", + "@github/copilot-darwin-x64": "1.0.39-0", + "@github/copilot-linux-arm64": "1.0.39-0", + "@github/copilot-linux-x64": "1.0.39-0", + "@github/copilot-win32-arm64": "1.0.39-0", + "@github/copilot-win32-x64": "1.0.39-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.36-0.tgz", - "integrity": "sha512-S0/oT9eo2WvjteWjtjougfh6tokq1Upye6tWeTHWq001E2UvrBN69+cQJNcNQUkO2C2AVvoqiI5RJT/E+HDrww==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.39-0.tgz", + "integrity": "sha512-DquiC7DZo+OmP2AtQUW27FCBsMGLshX9MEedWczjDgQ5YK2iMwACQLMeULdURssXJWXjvQQZMTTo0wsow59lnA==", "cpu": [ "arm64" ], @@ -696,9 +696,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.36-0.tgz", - "integrity": "sha512-msY1h6J2j005HMHxYqXO6Q5rJdqAjkUnnBwne5p3s71EHGekOl5U8GJs1Q2Y287+e9ZKSY68ANt/JB0Gq2ivmA==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.39-0.tgz", + "integrity": "sha512-NPjVkyl6QqYLGWlkqSiegcSUuI59RE3Qt4cOTALGG9TZmGYa0Z60o26LYrANkUyyerLl8MDI14oIgtl52nuBrQ==", "cpu": [ "x64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.36-0.tgz", - "integrity": "sha512-2yIKaU5XdC0xkFXt80pU+Uqr7pU2lHHzcBehzhDHfDeZVNq8jQj61Ka9r9NBjU3W8c3f99ctPMN8gErFnc5L/A==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.39-0.tgz", + "integrity": "sha512-Rv2EsthoR40FPn+afObJ+Jef0Lbpb3S6TAKNz+1MHv71hlVVxNKBVCGXVCKIehVgwE8rQGKz+pTy2+Gbprim9A==", "cpu": [ "arm64" ], @@ -728,9 +728,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.36-0.tgz", - "integrity": "sha512-tiEnl40MmEc4uybJ8at9TagmEcMsNHDiqzER72PYAD4mKwWklZ3RAClaVgzQlTxn1tMdNM7gbajyqSivLmo+rA==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.39-0.tgz", + "integrity": "sha512-7z8lmFLAVWRgZ7WoSEQsF2XAMeenWU5kgjljhbupDGV1yhW9Ycrx7RhB3cBtmyvmal+OzFjOpYlTiLi0Ul3kwA==", "cpu": [ "x64" ], @@ -744,9 +744,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.36-0.tgz", - "integrity": "sha512-c6hT1lnl7B44tLJGmyugvqPQ51bIMXtTeCb7Z5riPd4Sv17gsU+gLWewJLddAnu+0XhjzlBsIHv9GUtxGPCgWQ==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.39-0.tgz", + "integrity": "sha512-HtPnEV+Mt1H1RF54NHQa4qagj7llYkCcnHmc8jzkj810DE8iU4aI2u5K2fmU9/z/hvF1+223bEXRnSKAinyjmw==", "cpu": [ "arm64" ], @@ -760,9 +760,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.36-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.36-0.tgz", - "integrity": "sha512-RJy3RtlX+34denR3+ttBZsbyeaGoA2nlYoEtnijsQquK37on4gTwVavRbvjfVa2pL+aMuKhRD+xdvsUd+Fu4Lg==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.39-0.tgz", + "integrity": "sha512-N3Q5G6hDLKeiU+40mgdZk3Sk3b6/+pvNE3Tp5B8LK/Z3CvE2fQKYRXJx8iSDNtP48QwRqwHdrCGQVwDtEtSDAQ==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index a7caa354a..2c7b12e1f 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.36-0", + "@github/copilot": "^1.0.39-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 32128f5e7..0e97a2e36 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.32", + "@github/copilot": "^1.0.39-0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 6ee14deed..d792e2aca 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -171,6 +171,43 @@ export type SessionFsSetProviderConventions = "windows" | "posix"; * via the `definition` "ShellKillSignal". */ export type ShellKillSignal = "SIGTERM" | "SIGKILL" | "SIGINT"; +/** + * Current lifecycle status of the task + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskAgentInfoStatus". + */ +export type TaskAgentInfoStatus = "running" | "idle" | "completed" | "failed" | "cancelled"; +/** + * How the agent is currently being managed by the runtime + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskAgentInfoExecutionMode". + */ +export type TaskAgentInfoExecutionMode = "sync" | "background"; + +export type TaskInfo = TaskAgentInfo | TaskShellInfo; +/** + * Current lifecycle status of the task + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskShellInfoStatus". + */ +export type TaskShellInfoStatus = "running" | "idle" | "completed" | "failed" | "cancelled"; +/** + * Whether the shell runs inside a managed PTY session or as an independent background process + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskShellInfoAttachmentMode". + */ +export type TaskShellInfoAttachmentMode = "attached" | "detached"; +/** + * Whether the shell command is currently sync-waited or background-managed + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskShellInfoExecutionMode". + */ +export type TaskShellInfoExecutionMode = "sync" | "background"; /** * Tool call result (string or expanded result object) * @@ -273,6 +310,10 @@ export interface AgentInfo { * Description of the agent's purpose */ description: string; + /** + * Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. + */ + path?: string; } /** @experimental */ @@ -901,7 +942,7 @@ export interface ModeSetRequest { export interface NameGetResult { /** - * The session name, falling back to the auto-generated summary, or null if neither exists + * The session name (user-set or auto-generated), or null if not yet set */ name: string | null; } @@ -1562,6 +1603,205 @@ export interface SkillsEnableRequest { name: string; } +export interface TaskAgentInfo { + /** + * Task kind + */ + type: "agent"; + /** + * Unique task identifier + */ + id: string; + /** + * Tool call ID associated with this agent task + */ + toolCallId: string; + /** + * Short description of the task + */ + description: string; + status: TaskAgentInfoStatus; + /** + * ISO 8601 timestamp when the task was started + */ + startedAt: string; + /** + * ISO 8601 timestamp when the task finished + */ + completedAt?: string; + /** + * Accumulated active execution time in milliseconds + */ + activeTimeMs?: number; + /** + * ISO 8601 timestamp when the current active period began + */ + activeStartedAt?: string; + /** + * Error message when the task failed + */ + error?: string; + /** + * Type of agent running this task + */ + agentType: string; + /** + * Prompt passed to the agent + */ + prompt: string; + /** + * Result text from the task when available + */ + result?: string; + /** + * Model used for the task when specified + */ + model?: string; + executionMode?: TaskAgentInfoExecutionMode; + /** + * Whether the task is currently in the original sync wait and can be moved to background mode. False once it is already backgrounded, idle, finished, or no longer has a promotable sync waiter. + */ + canPromoteToBackground?: boolean; + /** + * Most recent response text from the agent + */ + latestResponse?: string; + /** + * ISO 8601 timestamp when the agent entered idle state + */ + idleSince?: string; +} + +export interface TaskShellInfo { + /** + * Task kind + */ + type: "shell"; + /** + * Unique task identifier + */ + id: string; + /** + * Short description of the task + */ + description: string; + status: TaskShellInfoStatus; + /** + * ISO 8601 timestamp when the task was started + */ + startedAt: string; + /** + * ISO 8601 timestamp when the task finished + */ + completedAt?: string; + /** + * Command being executed + */ + command: string; + attachmentMode: TaskShellInfoAttachmentMode; + executionMode?: TaskShellInfoExecutionMode; + /** + * Whether this shell task can be promoted to background mode + */ + canPromoteToBackground?: boolean; + /** + * Path to the detached shell log, when available + */ + logPath?: string; + /** + * Process ID when available + */ + pid?: number; +} + +/** @experimental */ +export interface TaskList { + /** + * Currently tracked tasks + */ + tasks: TaskInfo[]; +} + +/** @experimental */ +export interface TasksCancelRequest { + /** + * Task identifier + */ + id: string; +} + +/** @experimental */ +export interface TasksCancelResult { + /** + * Whether the task was successfully cancelled + */ + cancelled: boolean; +} + +/** @experimental */ +export interface TasksPromoteToBackgroundRequest { + /** + * Task identifier + */ + id: string; +} + +/** @experimental */ +export interface TasksPromoteToBackgroundResult { + /** + * Whether the task was successfully promoted to background mode + */ + promoted: boolean; +} + +/** @experimental */ +export interface TasksRemoveRequest { + /** + * Task identifier + */ + id: string; +} + +/** @experimental */ +export interface TasksRemoveResult { + /** + * Whether the task was removed. Returns false if the task does not exist or is still running/idle (cancel it first). + */ + removed: boolean; +} + +/** @experimental */ +export interface TasksStartAgentRequest { + /** + * Type of agent to start (e.g., 'explore', 'task', 'general-purpose') + */ + agentType: string; + /** + * Task prompt for the agent + */ + prompt: string; + /** + * Short name for the agent, used to generate a human-readable ID + */ + name: string; + /** + * Short description of the task + */ + description?: string; + /** + * Optional model override + */ + model?: string; +} + +/** @experimental */ +export interface TasksStartAgentResult { + /** + * Generated agent ID for the background task + */ + agentId: string; +} + export interface Tool { /** * Tool identifier (e.g., "bash", "grep", "str_replace_editor") @@ -1910,8 +2150,9 @@ export interface WorkspacesGetWorkspaceResult { repository?: string; host_type?: "github" | "ado"; branch?: string; - summary?: string; name?: string; + user_named?: boolean; + summary?: string; summary_count?: number; created_at?: string; updated_at?: string; @@ -2066,6 +2307,19 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.agent.reload", { sessionId }), }, /** @experimental */ + tasks: { + startAgent: async (params: TasksStartAgentRequest): Promise => + connection.sendRequest("session.tasks.startAgent", { sessionId, ...params }), + list: async (): Promise => + connection.sendRequest("session.tasks.list", { sessionId }), + promoteToBackground: async (params: TasksPromoteToBackgroundRequest): Promise => + connection.sendRequest("session.tasks.promoteToBackground", { sessionId, ...params }), + cancel: async (params: TasksCancelRequest): Promise => + connection.sendRequest("session.tasks.cancel", { sessionId, ...params }), + remove: async (params: TasksRemoveRequest): Promise => + connection.sendRequest("session.tasks.remove", { sessionId, ...params }), + }, + /** @experimental */ skills: { list: async (): Promise => connection.sendRequest("session.skills.list", { sessionId }), diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index ebaf68a01..ad541ea88 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -36,6 +36,7 @@ export type SessionEvent = | AssistantMessageDeltaEvent | AssistantTurnEndEvent | AssistantUsageEvent + | ModelCallFailureEvent | AbortEvent | ToolUserRequestedEvent | ToolExecutionStartEvent @@ -121,6 +122,10 @@ export type UserMessageAttachmentGithubReferenceType = "issue" | "pr" | "discuss * Tool call type: "function" for standard tool calls, "custom" for grammar-based tool calls. Defaults to "function" when absent. */ export type AssistantMessageToolRequestType = "function" | "custom"; +/** + * Where the failed model call originated + */ +export type ModelCallFailureSource = "top_level" | "subagent" | "mcp_sampling"; /** * A content block within a tool result, which may be text, terminal output, image, audio, or a resource */ @@ -587,6 +592,10 @@ export interface InfoData { * Human-readable informational message for display in the timeline */ message: string; + /** + * Optional actionable tip displayed with this message + */ + tip?: string; /** * Optional URL associated with this message that the user can open in a browser */ @@ -2118,6 +2127,61 @@ export interface AssistantUsageQuotaSnapshot { */ usedRequests: number; } +export interface ModelCallFailureEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ModelCallFailureData; + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + type: "model.call_failure"; +} +/** + * Failed LLM API call metadata for telemetry + */ +export interface ModelCallFailureData { + /** + * Completion ID from the model provider (e.g., chatcmpl-abc123) + */ + apiCallId?: string; + /** + * Duration of the failed API call in milliseconds + */ + durationMs?: number; + /** + * Raw provider/runtime error message for restricted telemetry + */ + errorMessage?: string; + /** + * What initiated this API call (e.g., "sub-agent", "mcp-sampling"); absent for user-initiated calls + */ + initiator?: string; + /** + * Model identifier used for the failed API call + */ + model?: string; + /** + * GitHub request tracing ID (x-github-request-id header) for server-side log correlation + */ + providerCallId?: string; + source: ModelCallFailureSource; + /** + * HTTP status code from the failed request + */ + statusCode?: number; +} export interface AbortEvent { /** * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. @@ -3097,7 +3161,7 @@ export interface SystemNotificationNewInboxMessage { */ senderName: string; /** - * Category of the sender (e.g., ambient-agent, plugin, hook) + * Category of the sender (e.g., sidekick-agent, plugin, hook) */ senderType: string; /** diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index d9680a9ba..f17419295 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -30,6 +30,7 @@ export async function createSdkTestContext({ copilotClientOptions?: CopilotClientOptions; } = {}) { const homeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-config-"))); + const copilotHomeDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-home-"))); const workDir = realpathSync(fs.mkdtempSync(join(os.tmpdir(), "copilot-test-work-"))); const openAiEndpoint = new CapiProxy(); @@ -37,6 +38,7 @@ export async function createSdkTestContext({ const env = { ...process.env, COPILOT_API_URL: proxyUrl, + COPILOT_HOME: copilotHomeDir, // TODO: I'm not convinced the SDK should default to using whatever config you happen to have in your homedir. // The SDK config should be independent of the regular CLI app. Likewise it shouldn't mix sessions from the @@ -86,6 +88,7 @@ export async function createSdkTestContext({ afterAll(async () => { await copilotClient.stop(); await openAiEndpoint.stop(anyTestFailed); + await rmDir("remove e2e test copilotHomeDir", copilotHomeDir); await rmDir("remove e2e test homeDir", homeDir); await rmDir("remove e2e test workDir", workDir); }); diff --git a/nodejs/test/e2e/session_fs.test.ts b/nodejs/test/e2e/session_fs.test.ts index f455ffcd1..f6af24d34 100644 --- a/nodejs/test/e2e/session_fs.test.ts +++ b/nodejs/test/e2e/session_fs.test.ts @@ -4,6 +4,9 @@ import { SessionCompactionCompleteEvent } from "@github/copilot/sdk"; import { MemoryProvider, VirtualProvider } from "@platformatic/vfs"; +import { mkdtempSync, realpathSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; import { describe, expect, it, onTestFinished } from "vitest"; import { CopilotClient } from "../../src/client.js"; import type { SessionFsReaddirWithTypesEntry } from "../../src/generated/rpc.js"; @@ -18,6 +21,14 @@ import { } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; +const sessionStatePath = + process.platform === "win32" + ? "/session-state" + : join( + realpathSync(mkdtempSync(join(tmpdir(), "copilot-sessionfs-state-"))), + "session-state" + ).replace(/\\/g, "/"); + describe("Session Fs", async () => { // Single provider for the describe block — session IDs are unique per test, // so no cross-contamination between tests. @@ -43,7 +54,9 @@ describe("Session Fs", async () => { expect(msg?.data.content).toContain("300"); await session.disconnect(); - const buf = await provider.readFile(p(session.sessionId, "/session-state/events.jsonl")); + const buf = await provider.readFile( + p(session.sessionId, `${sessionStatePath}/events.jsonl`) + ); const content = buf.toString("utf8"); expect(content).toContain("300"); }); @@ -60,7 +73,7 @@ describe("Session Fs", async () => { await session1.disconnect(); // The events file should exist before resume - expect(await provider.exists(p(sessionId, "/session-state/events.jsonl"))).toBe(true); + expect(await provider.exists(p(sessionId, `${sessionStatePath}/events.jsonl`))).toBe(true); const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, @@ -116,8 +129,10 @@ describe("Session Fs", async () => { // The tool result should reference a temp file under the session state path const messages = await session.getMessages(); const toolResult = findToolCallResult(messages, "get_big_string"); - expect(toolResult).toContain("/session-state/temp/"); - const filename = toolResult?.match(/(\/session-state\/temp\/[^\s]+)/)?.[1]; + expect(toolResult).toContain(`${sessionStatePath}/temp/`); + const filename = toolResult?.match( + new RegExp(`(${escapeRegExp(sessionStatePath)}/temp/[^\\s]+)`) + )?.[1]; expect(filename).toBeDefined(); // Verify the file was written with the correct content via the provider @@ -135,13 +150,13 @@ describe("Session Fs", async () => { expect(msg?.data.content).toContain("56"); // WorkspaceManager should have created workspace.yaml via sessionFs - const workspaceYamlPath = p(session.sessionId, "/session-state/workspace.yaml"); + const workspaceYamlPath = p(session.sessionId, `${sessionStatePath}/workspace.yaml`); await expect.poll(() => provider.exists(workspaceYamlPath)).toBe(true); const yaml = await provider.readFile(workspaceYamlPath, "utf8"); expect(yaml).toContain("id:"); // Checkpoint index should also exist - const indexPath = p(session.sessionId, "/session-state/checkpoints/index.md"); + const indexPath = p(session.sessionId, `${sessionStatePath}/checkpoints/index.md`); await expect.poll(() => provider.exists(indexPath)).toBe(true); await session.disconnect(); @@ -157,7 +172,7 @@ describe("Session Fs", async () => { await session.sendAndWait({ prompt: "What is 2 + 3?" }); await session.rpc.plan.update({ content: "# Test Plan\n\nThis is a test." }); - const planPath = p(session.sessionId, "/session-state/plan.md"); + const planPath = p(session.sessionId, `${sessionStatePath}/plan.md`); await expect.poll(() => provider.exists(planPath)).toBe(true); const content = await provider.readFile(planPath, "utf8"); expect(content).toContain("# Test Plan"); @@ -176,7 +191,7 @@ describe("Session Fs", async () => { await session.sendAndWait({ prompt: "What is 2+2?" }); - const eventsPath = p(session.sessionId, "/session-state/events.jsonl"); + const eventsPath = p(session.sessionId, `${sessionStatePath}/events.jsonl`); await expect.poll(() => provider.exists(eventsPath)).toBe(true); const contentBefore = await provider.readFile(eventsPath, "utf8"); expect(contentBefore).not.toContain("checkpointNumber"); @@ -212,10 +227,14 @@ function findToolName(messages: SessionEvent[], toolCallId: string): string | un const sessionFsConfig: SessionFsConfig = { initialCwd: "/", - sessionStatePath: "/session-state", + sessionStatePath, conventions: "posix", }; +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function createTestSessionFsHandler( session: CopilotSession, provider: VirtualProvider diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 00eaae928..397e3166a 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -156,19 +156,27 @@ class AgentInfo: name: str """Unique identifier of the custom agent""" + path: str | None = None + """Absolute local file path of the agent definition. Only set for file-based agents loaded + from disk; remote agents do not have a path. + """ + @staticmethod def from_dict(obj: Any) -> 'AgentInfo': assert isinstance(obj, dict) description = from_str(obj.get("description")) display_name = from_str(obj.get("displayName")) name = from_str(obj.get("name")) - return AgentInfo(description, display_name, name) + path = from_union([from_str, from_none], obj.get("path")) + return AgentInfo(description, display_name, name, path) def to_dict(self) -> dict: result: dict = {} result["description"] = from_str(self.description) result["displayName"] = from_str(self.display_name) result["name"] = from_str(self.name) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -884,7 +892,7 @@ def to_dict(self) -> dict: @dataclass class NameGetResult: name: str | None = None - """The session name, falling back to the auto-generated summary, or null if neither exists""" + """The session name (user-set or auto-generated), or null if not yet set""" @staticmethod def from_dict(obj: Any) -> 'NameGetResult': @@ -1740,6 +1748,200 @@ def to_dict(self) -> dict: result["name"] = from_str(self.name) return result +class TaskInfoExecutionMode(Enum): + """How the agent is currently being managed by the runtime + + Whether the shell command is currently sync-waited or background-managed + """ + BACKGROUND = "background" + SYNC = "sync" + +class TaskInfoStatus(Enum): + """Current lifecycle status of the task""" + + CANCELLED = "cancelled" + COMPLETED = "completed" + FAILED = "failed" + IDLE = "idle" + RUNNING = "running" + +class TaskAgentInfoType(Enum): + AGENT = "agent" + +class TaskShellInfoAttachmentMode(Enum): + """Whether the shell runs inside a managed PTY session or as an independent background + process + """ + ATTACHED = "attached" + DETACHED = "detached" + +class TaskInfoType(Enum): + AGENT = "agent" + SHELL = "shell" + +class TaskShellInfoType(Enum): + SHELL = "shell" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksCancelRequest: + id: str + """Task identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksCancelRequest': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + return TasksCancelRequest(id) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksCancelResult: + cancelled: bool + """Whether the task was successfully cancelled""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksCancelResult': + assert isinstance(obj, dict) + cancelled = from_bool(obj.get("cancelled")) + return TasksCancelResult(cancelled) + + def to_dict(self) -> dict: + result: dict = {} + result["cancelled"] = from_bool(self.cancelled) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksPromoteToBackgroundRequest: + id: str + """Task identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksPromoteToBackgroundRequest': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + return TasksPromoteToBackgroundRequest(id) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksPromoteToBackgroundResult: + promoted: bool + """Whether the task was successfully promoted to background mode""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksPromoteToBackgroundResult': + assert isinstance(obj, dict) + promoted = from_bool(obj.get("promoted")) + return TasksPromoteToBackgroundResult(promoted) + + def to_dict(self) -> dict: + result: dict = {} + result["promoted"] = from_bool(self.promoted) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksRemoveRequest: + id: str + """Task identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksRemoveRequest': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + return TasksRemoveRequest(id) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksRemoveResult: + removed: bool + """Whether the task was removed. Returns false if the task does not exist or is still + running/idle (cancel it first). + """ + + @staticmethod + def from_dict(obj: Any) -> 'TasksRemoveResult': + assert isinstance(obj, dict) + removed = from_bool(obj.get("removed")) + return TasksRemoveResult(removed) + + def to_dict(self) -> dict: + result: dict = {} + result["removed"] = from_bool(self.removed) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksStartAgentRequest: + agent_type: str + """Type of agent to start (e.g., 'explore', 'task', 'general-purpose')""" + + name: str + """Short name for the agent, used to generate a human-readable ID""" + + prompt: str + """Task prompt for the agent""" + + description: str | None = None + """Short description of the task""" + + model: str | None = None + """Optional model override""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksStartAgentRequest': + assert isinstance(obj, dict) + agent_type = from_str(obj.get("agentType")) + name = from_str(obj.get("name")) + prompt = from_str(obj.get("prompt")) + description = from_union([from_str, from_none], obj.get("description")) + model = from_union([from_str, from_none], obj.get("model")) + return TasksStartAgentRequest(agent_type, name, prompt, description, model) + + def to_dict(self) -> dict: + result: dict = {} + result["agentType"] = from_str(self.agent_type) + result["name"] = from_str(self.name) + result["prompt"] = from_str(self.prompt) + if self.description is not None: + result["description"] = from_union([from_str, from_none], self.description) + if self.model is not None: + result["model"] = from_union([from_str, from_none], self.model) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksStartAgentResult: + agent_id: str + """Generated agent ID for the background task""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksStartAgentResult': + assert isinstance(obj, dict) + agent_id = from_str(obj.get("agentId")) + return TasksStartAgentResult(agent_id) + + def to_dict(self) -> dict: + result: dict = {} + result["agentId"] = from_str(self.agent_id) + return result + @dataclass class Tool: description: str @@ -3230,6 +3432,83 @@ def to_dict(self) -> dict: result["skills"] = from_list(lambda x: to_class(Skill, x), self.skills) return result +@dataclass +class TaskShellInfo: + attachment_mode: TaskShellInfoAttachmentMode + """Whether the shell runs inside a managed PTY session or as an independent background + process + """ + command: str + """Command being executed""" + + description: str + """Short description of the task""" + + id: str + """Unique task identifier""" + + started_at: datetime + """ISO 8601 timestamp when the task was started""" + + status: TaskInfoStatus + """Current lifecycle status of the task""" + + type: TaskShellInfoType + """Task kind""" + + can_promote_to_background: bool | None = None + """Whether this shell task can be promoted to background mode""" + + completed_at: datetime | None = None + """ISO 8601 timestamp when the task finished""" + + execution_mode: TaskInfoExecutionMode | None = None + """Whether the shell command is currently sync-waited or background-managed""" + + log_path: str | None = None + """Path to the detached shell log, when available""" + + pid: int | None = None + """Process ID when available""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskShellInfo': + assert isinstance(obj, dict) + attachment_mode = TaskShellInfoAttachmentMode(obj.get("attachmentMode")) + command = from_str(obj.get("command")) + description = from_str(obj.get("description")) + id = from_str(obj.get("id")) + started_at = from_datetime(obj.get("startedAt")) + status = TaskInfoStatus(obj.get("status")) + type = TaskShellInfoType(obj.get("type")) + can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) + completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) + execution_mode = from_union([TaskInfoExecutionMode, from_none], obj.get("executionMode")) + log_path = from_union([from_str, from_none], obj.get("logPath")) + pid = from_union([from_int, from_none], obj.get("pid")) + return TaskShellInfo(attachment_mode, command, description, id, started_at, status, type, can_promote_to_background, completed_at, execution_mode, log_path, pid) + + def to_dict(self) -> dict: + result: dict = {} + result["attachmentMode"] = to_enum(TaskShellInfoAttachmentMode, self.attachment_mode) + result["command"] = from_str(self.command) + result["description"] = from_str(self.description) + result["id"] = from_str(self.id) + result["startedAt"] = self.started_at.isoformat() + result["status"] = to_enum(TaskInfoStatus, self.status) + result["type"] = to_enum(TaskShellInfoType, self.type) + if self.can_promote_to_background is not None: + result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) + if self.completed_at is not None: + result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) + if self.execution_mode is not None: + result["executionMode"] = from_union([lambda x: to_enum(TaskInfoExecutionMode, x), from_none], self.execution_mode) + if self.log_path is not None: + result["logPath"] = from_union([from_str, from_none], self.log_path) + if self.pid is not None: + result["pid"] = from_union([from_int, from_none], self.pid) + return result + @dataclass class ToolList: tools: list[Tool] @@ -3560,6 +3839,7 @@ class Workspace: summary: str | None = None summary_count: int | None = None updated_at: datetime | None = None + user_named: bool | None = None @staticmethod def from_dict(obj: Any) -> 'Workspace': @@ -3581,7 +3861,8 @@ def from_dict(obj: Any) -> 'Workspace': summary = from_union([from_str, from_none], obj.get("summary")) summary_count = from_union([from_int, from_none], obj.get("summary_count")) updated_at = from_union([from_datetime, from_none], obj.get("updated_at")) - return Workspace(id, branch, chronicle_sync_dismissed, created_at, cwd, git_root, host_type, mc_last_event_id, mc_session_id, mc_task_id, name, remote_steerable, repository, session_sync_level, summary, summary_count, updated_at) + user_named = from_union([from_bool, from_none], obj.get("user_named")) + return Workspace(id, branch, chronicle_sync_dismissed, created_at, cwd, git_root, host_type, mc_last_event_id, mc_session_id, mc_task_id, name, remote_steerable, repository, session_sync_level, summary, summary_count, updated_at, user_named) def to_dict(self) -> dict: result: dict = {} @@ -3618,6 +3899,8 @@ def to_dict(self) -> dict: result["summary_count"] = from_union([from_int, from_none], self.summary_count) if self.updated_at is not None: result["updated_at"] = from_union([lambda x: x.isoformat(), from_none], self.updated_at) + if self.user_named is not None: + result["user_named"] = from_union([from_bool, from_none], self.user_named) return result @dataclass @@ -4399,6 +4682,281 @@ def to_dict(self) -> dict: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) return result +@dataclass +class TaskAgentInfo: + agent_type: str + """Type of agent running this task""" + + description: str + """Short description of the task""" + + id: str + """Unique task identifier""" + + prompt: str + """Prompt passed to the agent""" + + started_at: datetime + """ISO 8601 timestamp when the task was started""" + + status: TaskInfoStatus + """Current lifecycle status of the task""" + + tool_call_id: str + """Tool call ID associated with this agent task""" + + type: TaskAgentInfoType + """Task kind""" + + active_started_at: datetime | None = None + """ISO 8601 timestamp when the current active period began""" + + active_time_ms: int | None = None + """Accumulated active execution time in milliseconds""" + + can_promote_to_background: bool | None = None + """Whether the task is currently in the original sync wait and can be moved to background + mode. False once it is already backgrounded, idle, finished, or no longer has a + promotable sync waiter. + """ + completed_at: datetime | None = None + """ISO 8601 timestamp when the task finished""" + + error: str | None = None + """Error message when the task failed""" + + execution_mode: TaskInfoExecutionMode | None = None + """How the agent is currently being managed by the runtime""" + + idle_since: datetime | None = None + """ISO 8601 timestamp when the agent entered idle state""" + + latest_response: str | None = None + """Most recent response text from the agent""" + + model: str | None = None + """Model used for the task when specified""" + + result: str | None = None + """Result text from the task when available""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskAgentInfo': + assert isinstance(obj, dict) + agent_type = from_str(obj.get("agentType")) + description = from_str(obj.get("description")) + id = from_str(obj.get("id")) + prompt = from_str(obj.get("prompt")) + started_at = from_datetime(obj.get("startedAt")) + status = TaskInfoStatus(obj.get("status")) + tool_call_id = from_str(obj.get("toolCallId")) + type = TaskAgentInfoType(obj.get("type")) + active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) + active_time_ms = from_union([from_int, from_none], obj.get("activeTimeMs")) + can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) + completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) + error = from_union([from_str, from_none], obj.get("error")) + execution_mode = from_union([TaskInfoExecutionMode, from_none], obj.get("executionMode")) + idle_since = from_union([from_datetime, from_none], obj.get("idleSince")) + latest_response = from_union([from_str, from_none], obj.get("latestResponse")) + model = from_union([from_str, from_none], obj.get("model")) + result = from_union([from_str, from_none], obj.get("result")) + return TaskAgentInfo(agent_type, description, id, prompt, started_at, status, tool_call_id, type, active_started_at, active_time_ms, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, result) + + def to_dict(self) -> dict: + result: dict = {} + result["agentType"] = from_str(self.agent_type) + result["description"] = from_str(self.description) + result["id"] = from_str(self.id) + result["prompt"] = from_str(self.prompt) + result["startedAt"] = self.started_at.isoformat() + result["status"] = to_enum(TaskInfoStatus, self.status) + result["toolCallId"] = from_str(self.tool_call_id) + result["type"] = to_enum(TaskAgentInfoType, self.type) + if self.active_started_at is not None: + result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) + if self.active_time_ms is not None: + result["activeTimeMs"] = from_union([from_int, from_none], self.active_time_ms) + if self.can_promote_to_background is not None: + result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) + if self.completed_at is not None: + result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.execution_mode is not None: + result["executionMode"] = from_union([lambda x: to_enum(TaskInfoExecutionMode, x), from_none], self.execution_mode) + if self.idle_since is not None: + result["idleSince"] = from_union([lambda x: x.isoformat(), from_none], self.idle_since) + if self.latest_response is not None: + result["latestResponse"] = from_union([from_str, from_none], self.latest_response) + if self.model is not None: + result["model"] = from_union([from_str, from_none], self.model) + if self.result is not None: + result["result"] = from_union([from_str, from_none], self.result) + return result + +@dataclass +class TaskInfo: + description: str + """Short description of the task""" + + id: str + """Unique task identifier""" + + started_at: datetime + """ISO 8601 timestamp when the task was started""" + + status: TaskInfoStatus + """Current lifecycle status of the task""" + + type: TaskInfoType + """Task kind""" + + active_started_at: datetime | None = None + """ISO 8601 timestamp when the current active period began""" + + active_time_ms: int | None = None + """Accumulated active execution time in milliseconds""" + + agent_type: str | None = None + """Type of agent running this task""" + + can_promote_to_background: bool | None = None + """Whether the task is currently in the original sync wait and can be moved to background + mode. False once it is already backgrounded, idle, finished, or no longer has a + promotable sync waiter. + + Whether this shell task can be promoted to background mode + """ + completed_at: datetime | None = None + """ISO 8601 timestamp when the task finished""" + + error: str | None = None + """Error message when the task failed""" + + execution_mode: TaskInfoExecutionMode | None = None + """How the agent is currently being managed by the runtime + + Whether the shell command is currently sync-waited or background-managed + """ + idle_since: datetime | None = None + """ISO 8601 timestamp when the agent entered idle state""" + + latest_response: str | None = None + """Most recent response text from the agent""" + + model: str | None = None + """Model used for the task when specified""" + + prompt: str | None = None + """Prompt passed to the agent""" + + result: str | None = None + """Result text from the task when available""" + + tool_call_id: str | None = None + """Tool call ID associated with this agent task""" + + attachment_mode: TaskShellInfoAttachmentMode | None = None + """Whether the shell runs inside a managed PTY session or as an independent background + process + """ + command: str | None = None + """Command being executed""" + + log_path: str | None = None + """Path to the detached shell log, when available""" + + pid: int | None = None + """Process ID when available""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskInfo': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + id = from_str(obj.get("id")) + started_at = from_datetime(obj.get("startedAt")) + status = TaskInfoStatus(obj.get("status")) + type = TaskInfoType(obj.get("type")) + active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) + active_time_ms = from_union([from_int, from_none], obj.get("activeTimeMs")) + agent_type = from_union([from_str, from_none], obj.get("agentType")) + can_promote_to_background = from_union([from_bool, from_none], obj.get("canPromoteToBackground")) + completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) + error = from_union([from_str, from_none], obj.get("error")) + execution_mode = from_union([TaskInfoExecutionMode, from_none], obj.get("executionMode")) + idle_since = from_union([from_datetime, from_none], obj.get("idleSince")) + latest_response = from_union([from_str, from_none], obj.get("latestResponse")) + model = from_union([from_str, from_none], obj.get("model")) + prompt = from_union([from_str, from_none], obj.get("prompt")) + result = from_union([from_str, from_none], obj.get("result")) + tool_call_id = from_union([from_str, from_none], obj.get("toolCallId")) + attachment_mode = from_union([TaskShellInfoAttachmentMode, from_none], obj.get("attachmentMode")) + command = from_union([from_str, from_none], obj.get("command")) + log_path = from_union([from_str, from_none], obj.get("logPath")) + pid = from_union([from_int, from_none], obj.get("pid")) + return TaskInfo(description, id, started_at, status, type, active_started_at, active_time_ms, agent_type, can_promote_to_background, completed_at, error, execution_mode, idle_since, latest_response, model, prompt, result, tool_call_id, attachment_mode, command, log_path, pid) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["id"] = from_str(self.id) + result["startedAt"] = self.started_at.isoformat() + result["status"] = to_enum(TaskInfoStatus, self.status) + result["type"] = to_enum(TaskInfoType, self.type) + if self.active_started_at is not None: + result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) + if self.active_time_ms is not None: + result["activeTimeMs"] = from_union([from_int, from_none], self.active_time_ms) + if self.agent_type is not None: + result["agentType"] = from_union([from_str, from_none], self.agent_type) + if self.can_promote_to_background is not None: + result["canPromoteToBackground"] = from_union([from_bool, from_none], self.can_promote_to_background) + if self.completed_at is not None: + result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.execution_mode is not None: + result["executionMode"] = from_union([lambda x: to_enum(TaskInfoExecutionMode, x), from_none], self.execution_mode) + if self.idle_since is not None: + result["idleSince"] = from_union([lambda x: x.isoformat(), from_none], self.idle_since) + if self.latest_response is not None: + result["latestResponse"] = from_union([from_str, from_none], self.latest_response) + if self.model is not None: + result["model"] = from_union([from_str, from_none], self.model) + if self.prompt is not None: + result["prompt"] = from_union([from_str, from_none], self.prompt) + if self.result is not None: + result["result"] = from_union([from_str, from_none], self.result) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_str, from_none], self.tool_call_id) + if self.attachment_mode is not None: + result["attachmentMode"] = from_union([lambda x: to_enum(TaskShellInfoAttachmentMode, x), from_none], self.attachment_mode) + if self.command is not None: + result["command"] = from_union([from_str, from_none], self.command) + if self.log_path is not None: + result["logPath"] = from_union([from_str, from_none], self.log_path) + if self.pid is not None: + result["pid"] = from_union([from_int, from_none], self.pid) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskList: + tasks: list[TaskInfo] + """Currently tracked tasks""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskList': + assert isinstance(obj, dict) + tasks = from_list(TaskInfo.from_dict, obj.get("tasks")) + return TaskList(tasks) + + def to_dict(self) -> dict: + result: dict = {} + result["tasks"] = from_list(lambda x: to_class(TaskInfo, x), self.tasks) + return result + @dataclass class RPC: account_get_quota_request: AccountGetQuotaRequest @@ -4552,6 +5110,23 @@ class RPC: skills_disable_request: SkillsDisableRequest skills_discover_request: SkillsDiscoverRequest skills_enable_request: SkillsEnableRequest + task_agent_info: TaskAgentInfo + task_agent_info_execution_mode: TaskInfoExecutionMode + task_agent_info_status: TaskInfoStatus + task_info: TaskInfo + task_list: TaskList + tasks_cancel_request: TasksCancelRequest + tasks_cancel_result: TasksCancelResult + task_shell_info: TaskShellInfo + task_shell_info_attachment_mode: TaskShellInfoAttachmentMode + task_shell_info_execution_mode: TaskInfoExecutionMode + task_shell_info_status: TaskInfoStatus + tasks_promote_to_background_request: TasksPromoteToBackgroundRequest + tasks_promote_to_background_result: TasksPromoteToBackgroundResult + tasks_remove_request: TasksRemoveRequest + tasks_remove_result: TasksRemoveResult + tasks_start_agent_request: TasksStartAgentRequest + tasks_start_agent_result: TasksStartAgentResult tool: Tool tool_call_result: ToolCallResult tool_list: ToolList @@ -4745,6 +5320,23 @@ def from_dict(obj: Any) -> 'RPC': skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest")) skills_discover_request = SkillsDiscoverRequest.from_dict(obj.get("SkillsDiscoverRequest")) skills_enable_request = SkillsEnableRequest.from_dict(obj.get("SkillsEnableRequest")) + task_agent_info = TaskAgentInfo.from_dict(obj.get("TaskAgentInfo")) + task_agent_info_execution_mode = TaskInfoExecutionMode(obj.get("TaskAgentInfoExecutionMode")) + task_agent_info_status = TaskInfoStatus(obj.get("TaskAgentInfoStatus")) + task_info = TaskInfo.from_dict(obj.get("TaskInfo")) + task_list = TaskList.from_dict(obj.get("TaskList")) + tasks_cancel_request = TasksCancelRequest.from_dict(obj.get("TasksCancelRequest")) + tasks_cancel_result = TasksCancelResult.from_dict(obj.get("TasksCancelResult")) + task_shell_info = TaskShellInfo.from_dict(obj.get("TaskShellInfo")) + task_shell_info_attachment_mode = TaskShellInfoAttachmentMode(obj.get("TaskShellInfoAttachmentMode")) + task_shell_info_execution_mode = TaskInfoExecutionMode(obj.get("TaskShellInfoExecutionMode")) + task_shell_info_status = TaskInfoStatus(obj.get("TaskShellInfoStatus")) + tasks_promote_to_background_request = TasksPromoteToBackgroundRequest.from_dict(obj.get("TasksPromoteToBackgroundRequest")) + tasks_promote_to_background_result = TasksPromoteToBackgroundResult.from_dict(obj.get("TasksPromoteToBackgroundResult")) + tasks_remove_request = TasksRemoveRequest.from_dict(obj.get("TasksRemoveRequest")) + tasks_remove_result = TasksRemoveResult.from_dict(obj.get("TasksRemoveResult")) + tasks_start_agent_request = TasksStartAgentRequest.from_dict(obj.get("TasksStartAgentRequest")) + tasks_start_agent_result = TasksStartAgentResult.from_dict(obj.get("TasksStartAgentResult")) tool = Tool.from_dict(obj.get("Tool")) tool_call_result = ToolCallResult.from_dict(obj.get("ToolCallResult")) tool_list = ToolList.from_dict(obj.get("ToolList")) @@ -4783,7 +5375,7 @@ def from_dict(obj: Any) -> 'RPC': workspaces_list_files_result = WorkspacesListFilesResult.from_dict(obj.get("WorkspacesListFilesResult")) workspaces_read_file_request = WorkspacesReadFileRequest.from_dict(obj.get("WorkspacesReadFileRequest")) workspaces_read_file_result = WorkspacesReadFileResult.from_dict(obj.get("WorkspacesReadFileResult")) - return RPC(account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_list, agent_reload_result, agent_select_request, agent_select_result, auth_info_type, commands_handle_pending_command_request, commands_handle_pending_command_result, current_model, discovered_mcp_server, discovered_mcp_server_source, discovered_mcp_server_type, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, filter_mapping, filter_mapping_string, filter_mapping_value, fleet_start_request, fleet_start_result, handle_tool_call_result, history_compact_context_window, history_compact_result, history_truncate_request, history_truncate_result, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_oauth_login_request, mcp_oauth_login_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_type, mcp_server_config_local, mcp_server_config_local_type, mcp_server_list, mcp_server_source, mcp_server_status, model, model_billing, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_policy, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_request, permission_decision, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_request_result, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, server_skill, server_skill_list, session_auth_status, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_log_level, session_mode, sessions_fork_request, sessions_fork_result, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, tool, tool_call_result, tool_list, tools_handle_pending_tool_call, tools_handle_pending_tool_call_request, tools_list_request, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_handle_pending_elicitation_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_usage, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_files_result, workspaces_read_file_request, workspaces_read_file_result) + return RPC(account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_list, agent_reload_result, agent_select_request, agent_select_result, auth_info_type, commands_handle_pending_command_request, commands_handle_pending_command_result, current_model, discovered_mcp_server, discovered_mcp_server_source, discovered_mcp_server_type, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, filter_mapping, filter_mapping_string, filter_mapping_value, fleet_start_request, fleet_start_result, handle_tool_call_result, history_compact_context_window, history_compact_result, history_truncate_request, history_truncate_result, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_oauth_login_request, mcp_oauth_login_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_type, mcp_server_config_local, mcp_server_config_local_type, mcp_server_list, mcp_server_source, mcp_server_status, model, model_billing, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_policy, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_request, permission_decision, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_reject, permission_decision_request, permission_decision_user_not_available, permission_request_result, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, server_skill, server_skill_list, session_auth_status, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_log_level, session_mode, sessions_fork_request, sessions_fork_result, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, task_agent_info, task_agent_info_execution_mode, task_agent_info_status, task_info, task_list, tasks_cancel_request, tasks_cancel_result, task_shell_info, task_shell_info_attachment_mode, task_shell_info_execution_mode, task_shell_info_status, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_remove_request, tasks_remove_result, tasks_start_agent_request, tasks_start_agent_result, tool, tool_call_result, tool_list, tools_handle_pending_tool_call, tools_handle_pending_tool_call_request, tools_list_request, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_handle_pending_elicitation_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_usage, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_files_result, workspaces_read_file_request, workspaces_read_file_result) def to_dict(self) -> dict: result: dict = {} @@ -4938,6 +5530,23 @@ def to_dict(self) -> dict: result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request) result["SkillsDiscoverRequest"] = to_class(SkillsDiscoverRequest, self.skills_discover_request) result["SkillsEnableRequest"] = to_class(SkillsEnableRequest, self.skills_enable_request) + result["TaskAgentInfo"] = to_class(TaskAgentInfo, self.task_agent_info) + result["TaskAgentInfoExecutionMode"] = to_enum(TaskInfoExecutionMode, self.task_agent_info_execution_mode) + result["TaskAgentInfoStatus"] = to_enum(TaskInfoStatus, self.task_agent_info_status) + result["TaskInfo"] = to_class(TaskInfo, self.task_info) + result["TaskList"] = to_class(TaskList, self.task_list) + result["TasksCancelRequest"] = to_class(TasksCancelRequest, self.tasks_cancel_request) + result["TasksCancelResult"] = to_class(TasksCancelResult, self.tasks_cancel_result) + result["TaskShellInfo"] = to_class(TaskShellInfo, self.task_shell_info) + result["TaskShellInfoAttachmentMode"] = to_enum(TaskShellInfoAttachmentMode, self.task_shell_info_attachment_mode) + result["TaskShellInfoExecutionMode"] = to_enum(TaskInfoExecutionMode, self.task_shell_info_execution_mode) + result["TaskShellInfoStatus"] = to_enum(TaskInfoStatus, self.task_shell_info_status) + result["TasksPromoteToBackgroundRequest"] = to_class(TasksPromoteToBackgroundRequest, self.tasks_promote_to_background_request) + result["TasksPromoteToBackgroundResult"] = to_class(TasksPromoteToBackgroundResult, self.tasks_promote_to_background_result) + result["TasksRemoveRequest"] = to_class(TasksRemoveRequest, self.tasks_remove_request) + result["TasksRemoveResult"] = to_class(TasksRemoveResult, self.tasks_remove_result) + result["TasksStartAgentRequest"] = to_class(TasksStartAgentRequest, self.tasks_start_agent_request) + result["TasksStartAgentResult"] = to_class(TasksStartAgentResult, self.tasks_start_agent_result) result["Tool"] = to_class(Tool, self.tool) result["ToolCallResult"] = to_class(ToolCallResult, self.tool_call_result) result["ToolList"] = to_class(ToolList, self.tool_list) @@ -5268,6 +5877,36 @@ async def reload(self, *, timeout: float | None = None) -> AgentReloadResult: return AgentReloadResult.from_dict(await self._client.request("session.agent.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class TasksApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def start_agent(self, params: TasksStartAgentRequest, *, timeout: float | None = None) -> TasksStartAgentResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return TasksStartAgentResult.from_dict(await self._client.request("session.tasks.startAgent", params_dict, **_timeout_kwargs(timeout))) + + async def list(self, *, timeout: float | None = None) -> TaskList: + return TaskList.from_dict(await self._client.request("session.tasks.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + async def promote_to_background(self, params: TasksPromoteToBackgroundRequest, *, timeout: float | None = None) -> TasksPromoteToBackgroundResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return TasksPromoteToBackgroundResult.from_dict(await self._client.request("session.tasks.promoteToBackground", params_dict, **_timeout_kwargs(timeout))) + + async def cancel(self, params: TasksCancelRequest, *, timeout: float | None = None) -> TasksCancelResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return TasksCancelResult.from_dict(await self._client.request("session.tasks.cancel", params_dict, **_timeout_kwargs(timeout))) + + async def remove(self, params: TasksRemoveRequest, *, timeout: float | None = None) -> TasksRemoveResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return TasksRemoveResult.from_dict(await self._client.request("session.tasks.remove", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class SkillsApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -5472,6 +6111,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.instructions = InstructionsApi(client, session_id) self.fleet = FleetApi(client, session_id) self.agent = AgentApi(client, session_id) + self.tasks = TasksApi(client, session_id) self.skills = SkillsApi(client, session_id) self.mcp = McpApi(client, session_id) self.plugins = PluginsApi(client, session_id) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index bebed90d9..32599fdc3 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -136,6 +136,7 @@ class SessionEventType(Enum): ASSISTANT_MESSAGE_DELTA = "assistant.message_delta" ASSISTANT_TURN_END = "assistant.turn_end" ASSISTANT_USAGE = "assistant.usage" + MODEL_CALL_FAILURE = "model.call_failure" ABORT = "abort" TOOL_USER_REQUESTED = "tool.user_requested" TOOL_EXECUTION_START = "tool.execution_start" @@ -1597,6 +1598,60 @@ def to_dict(self) -> dict: return result +@dataclass +class ModelCallFailureData: + "Failed LLM API call metadata for telemetry" + source: ModelCallFailureSource + api_call_id: str | None = None + duration_ms: float | None = None + error_message: str | None = None + initiator: str | None = None + model: str | None = None + provider_call_id: str | None = None + status_code: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "ModelCallFailureData": + assert isinstance(obj, dict) + source = parse_enum(ModelCallFailureSource, obj.get("source")) + api_call_id = from_union([from_none, from_str], obj.get("apiCallId")) + duration_ms = from_union([from_none, from_float], obj.get("durationMs")) + error_message = from_union([from_none, from_str], obj.get("errorMessage")) + initiator = from_union([from_none, from_str], obj.get("initiator")) + model = from_union([from_none, from_str], obj.get("model")) + provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) + status_code = from_union([from_none, from_int], obj.get("statusCode")) + return ModelCallFailureData( + source=source, + api_call_id=api_call_id, + duration_ms=duration_ms, + error_message=error_message, + initiator=initiator, + model=model, + provider_call_id=provider_call_id, + status_code=status_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["source"] = to_enum(ModelCallFailureSource, self.source) + if self.api_call_id is not None: + result["apiCallId"] = from_union([from_none, from_str], self.api_call_id) + if self.duration_ms is not None: + result["durationMs"] = from_union([from_none, to_float], self.duration_ms) + if self.error_message is not None: + result["errorMessage"] = from_union([from_none, from_str], self.error_message) + if self.initiator is not None: + result["initiator"] = from_union([from_none, from_str], self.initiator) + if self.model is not None: + result["model"] = from_union([from_none, from_str], self.model) + if self.provider_call_id is not None: + result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) + if self.status_code is not None: + result["statusCode"] = from_union([from_none, to_int], self.status_code) + return result + + @dataclass class PendingMessagesModifiedData: "Empty payload; the event signals that the pending message queue has changed" @@ -2427,6 +2482,7 @@ class SessionInfoData: "Informational message for timeline display with categorization" info_type: str message: str + tip: str | None = None url: str | None = None @staticmethod @@ -2434,10 +2490,12 @@ def from_dict(obj: Any) -> "SessionInfoData": assert isinstance(obj, dict) info_type = from_str(obj.get("infoType")) message = from_str(obj.get("message")) + tip = from_union([from_none, from_str], obj.get("tip")) url = from_union([from_none, from_str], obj.get("url")) return SessionInfoData( info_type=info_type, message=message, + tip=tip, url=url, ) @@ -2445,6 +2503,8 @@ def to_dict(self) -> dict: result: dict = {} result["infoType"] = from_str(self.info_type) result["message"] = from_str(self.message) + if self.tip is not None: + result["tip"] = from_union([from_none, from_str], self.tip) if self.url is not None: result["url"] = from_union([from_none, from_str], self.url) return result @@ -4279,6 +4339,13 @@ class McpServersLoadedServerStatus(Enum): NOT_CONFIGURED = "not_configured" +class ModelCallFailureSource(Enum): + "Where the failed model call originated" + TOP_LEVEL = "top_level" + SUBAGENT = "subagent" + MCP_SAMPLING = "mcp_sampling" + + class PermissionCompletedKind(Enum): "The outcome of the permission request" APPROVED = "approved" @@ -4433,7 +4500,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data @dataclass @@ -4489,6 +4556,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.ASSISTANT_MESSAGE_DELTA: data = AssistantMessageDeltaData.from_dict(data_obj) case SessionEventType.ASSISTANT_TURN_END: data = AssistantTurnEndData.from_dict(data_obj) case SessionEventType.ASSISTANT_USAGE: data = AssistantUsageData.from_dict(data_obj) + case SessionEventType.MODEL_CALL_FAILURE: data = ModelCallFailureData.from_dict(data_obj) case SessionEventType.ABORT: data = AbortData.from_dict(data_obj) case SessionEventType.TOOL_USER_REQUESTED: data = ToolUserRequestedData.from_dict(data_obj) case SessionEventType.TOOL_EXECUTION_START: data = ToolExecutionStartData.from_dict(data_obj) diff --git a/python/e2e/test_commands.py b/python/e2e/test_commands.py index f2eb7cdf1..6cc68e246 100644 --- a/python/e2e/test_commands.py +++ b/python/e2e/test_commands.py @@ -113,6 +113,7 @@ def _get_env(self) -> dict: env.update( { "COPILOT_API_URL": self.proxy_url, + "COPILOT_HOME": self.home_dir, "XDG_CONFIG_HOME": self.home_dir, "XDG_STATE_HOME": self.home_dir, } diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py index 3b3b75d0a..386f2eeb0 100644 --- a/python/e2e/test_multi_client.py +++ b/python/e2e/test_multi_client.py @@ -129,6 +129,7 @@ def get_env(self) -> dict: env.update( { "COPILOT_API_URL": self.proxy_url, + "COPILOT_HOME": self.home_dir, "XDG_CONFIG_HOME": self.home_dir, "XDG_STATE_HOME": self.home_dir, } diff --git a/python/e2e/test_session_fs.py b/python/e2e/test_session_fs.py index 18c266c64..f44b91a16 100644 --- a/python/e2e/test_session_fs.py +++ b/python/e2e/test_session_fs.py @@ -6,6 +6,7 @@ import datetime as dt import os import re +import tempfile from pathlib import Path import pytest @@ -26,9 +27,17 @@ pytestmark = pytest.mark.asyncio(loop_scope="module") +SESSION_STATE_PATH = ( + "/session-state" + if os.name == "nt" + else (Path(tempfile.mkdtemp(prefix="copilot-sessionfs-state-")) / "session-state") + .resolve() + .as_posix() +) + SESSION_FS_CONFIG: SessionFsConfig = { "initial_cwd": "/", - "session_state_path": "/session-state", + "session_state_path": SESSION_STATE_PATH, "conventions": "posix", } @@ -71,7 +80,7 @@ async def test_should_route_file_operations_through_the_session_fs_provider( await session.disconnect() events_path = provider_path( - provider_root, session.session_id, "/session-state/events.jsonl" + provider_root, session.session_id, f"{SESSION_STATE_PATH}/events.jsonl" ) assert "300" in events_path.read_text(encoding="utf-8") @@ -93,7 +102,9 @@ async def test_should_load_session_data_from_fs_provider_on_resume( assert "100" in msg.data.content await session1.disconnect() - assert provider_path(provider_root, session_id, "/session-state/events.jsonl").exists() + assert provider_path( + provider_root, session_id, f"{SESSION_STATE_PATH}/events.jsonl" + ).exists() session2 = await session_fs_client.resume_session( session_id, @@ -169,8 +180,8 @@ def get_big_string() -> str: messages = await session.get_messages() tool_result = find_tool_call_result(messages, "get_big_string") assert tool_result is not None - assert "/session-state/temp/" in tool_result - match = re.search(r"(/session-state/temp/[^\s]+)", tool_result) + assert f"{SESSION_STATE_PATH}/temp/" in tool_result + match = re.search(rf"({re.escape(SESSION_STATE_PATH)}/temp/[^\s]+)", tool_result) assert match is not None temp_file = provider_path(provider_root, session.session_id, match.group(1)) @@ -200,7 +211,7 @@ def on_event(event: SessionEvent): await session.send_and_wait("What is 2+2?") events_path = provider_path( - provider_root, session.session_id, "/session-state/events.jsonl" + provider_root, session.session_id, f"{SESSION_STATE_PATH}/events.jsonl" ) await wait_for_path(events_path) assert "checkpointNumber" not in events_path.read_text(encoding="utf-8") @@ -228,7 +239,7 @@ async def test_should_write_workspace_metadata_via_sessionfs( # WorkspaceManager should have created workspace.yaml via sessionFs workspace_yaml_path = provider_path( - provider_root, session.session_id, "/session-state/workspace.yaml" + provider_root, session.session_id, f"{SESSION_STATE_PATH}/workspace.yaml" ) await wait_for_path(workspace_yaml_path) yaml_content = workspace_yaml_path.read_text(encoding="utf-8") @@ -236,7 +247,7 @@ async def test_should_write_workspace_metadata_via_sessionfs( # Checkpoint index should also exist index_path = provider_path( - provider_root, session.session_id, "/session-state/checkpoints/index.md" + provider_root, session.session_id, f"{SESSION_STATE_PATH}/checkpoints/index.md" ) await wait_for_path(index_path) @@ -257,7 +268,9 @@ async def test_should_persist_plan_md_via_sessionfs( await session.send_and_wait("What is 2 + 3?") await session.rpc.plan.update(PlanUpdateRequest(content="# Test Plan\n\nThis is a test.")) - plan_path = provider_path(provider_root, session.session_id, "/session-state/plan.md") + plan_path = provider_path( + provider_root, session.session_id, f"{SESSION_STATE_PATH}/plan.md" + ) await wait_for_path(plan_path) content = plan_path.read_text(encoding="utf-8") assert "# Test Plan" in content diff --git a/python/e2e/test_ui_elicitation_multi_client.py b/python/e2e/test_ui_elicitation_multi_client.py index 4c63fb6b2..e12202b2f 100644 --- a/python/e2e/test_ui_elicitation_multi_client.py +++ b/python/e2e/test_ui_elicitation_multi_client.py @@ -120,6 +120,7 @@ def _get_env(self) -> dict: env.update( { "COPILOT_API_URL": self.proxy_url, + "COPILOT_HOME": self.home_dir, "XDG_CONFIG_HOME": self.home_dir, "XDG_STATE_HOME": self.home_dir, } diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index c2028c14a..09a279d0d 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -133,6 +133,7 @@ def get_env(self) -> dict: env.update( { "COPILOT_API_URL": self.proxy_url, + "COPILOT_HOME": self.home_dir, "XDG_CONFIG_HOME": self.home_dir, "XDG_STATE_HOME": self.home_dir, } diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index 51538fe1d..ea6f5be9c 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.32", + "@github/copilot": "^1.0.39-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", @@ -462,27 +462,27 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.32.tgz", - "integrity": "sha512-ydEYAztJQa1sLQw+WPmnkkt3Sf/k2Smn/7szzYvt1feUOdNIak1gHpQhKcgPr2w252gjVLRWjOiynoeLVW0Fbw==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.39-0.tgz", + "integrity": "sha512-OuN6wGgUv0WQydOCUuhYRFwUwTkfktI9fGdSih+SKUE+nTZze8JBz8Sg68K0ZLlqdD0OcF0ac9wMAfunlutvsw==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.32", - "@github/copilot-darwin-x64": "1.0.32", - "@github/copilot-linux-arm64": "1.0.32", - "@github/copilot-linux-x64": "1.0.32", - "@github/copilot-win32-arm64": "1.0.32", - "@github/copilot-win32-x64": "1.0.32" + "@github/copilot-darwin-arm64": "1.0.39-0", + "@github/copilot-darwin-x64": "1.0.39-0", + "@github/copilot-linux-arm64": "1.0.39-0", + "@github/copilot-linux-x64": "1.0.39-0", + "@github/copilot-win32-arm64": "1.0.39-0", + "@github/copilot-win32-x64": "1.0.39-0" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.32.tgz", - "integrity": "sha512-RtGHpnrbP1eVtpzitLqC0jkBlo63PJiByv6W/NTtLw4ZAllumb5kMk8JaTtydKl9DCOHA0wfXbG5/JkGXuQ81g==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.39-0.tgz", + "integrity": "sha512-DquiC7DZo+OmP2AtQUW27FCBsMGLshX9MEedWczjDgQ5YK2iMwACQLMeULdURssXJWXjvQQZMTTo0wsow59lnA==", "cpu": [ "arm64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.32.tgz", - "integrity": "sha512-eyF6uy8gcZ4m/0UdM9UoykMDotZ8hZPJ1xIg0iHy4wrNtkYOaAspAoVpOkm50ODOQAHJ5PVV+9LuT6IoeL+wHQ==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.39-0.tgz", + "integrity": "sha512-NPjVkyl6QqYLGWlkqSiegcSUuI59RE3Qt4cOTALGG9TZmGYa0Z60o26LYrANkUyyerLl8MDI14oIgtl52nuBrQ==", "cpu": [ "x64" ], @@ -514,9 +514,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.32.tgz", - "integrity": "sha512-acRAu5ehFPnw3hQSIxcmi7wzv8PAYd+nqdxZXizOi++en3QWgez7VEXiKLe9Ukf50iiGReg19yvWV4iDOGC0HQ==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.39-0.tgz", + "integrity": "sha512-Rv2EsthoR40FPn+afObJ+Jef0Lbpb3S6TAKNz+1MHv71hlVVxNKBVCGXVCKIehVgwE8rQGKz+pTy2+Gbprim9A==", "cpu": [ "arm64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.32.tgz", - "integrity": "sha512-lw86YDwkTKwmeVpfnPErDe9DhemrOHN+l92xOU9wQSH5/d+HguXwRb3e4cQjlxsGLS+/fWRGtwf+u2fbQ37avw==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.39-0.tgz", + "integrity": "sha512-7z8lmFLAVWRgZ7WoSEQsF2XAMeenWU5kgjljhbupDGV1yhW9Ycrx7RhB3cBtmyvmal+OzFjOpYlTiLi0Ul3kwA==", "cpu": [ "x64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.32.tgz", - "integrity": "sha512-+eZpuzgBbLHMIzltH541wfbbMy0HEdG91ISzRae3qPCssf3Ad85sat6k7FWTRBSZBFrN7z4yMQm5gROqDJYGSA==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.39-0.tgz", + "integrity": "sha512-HtPnEV+Mt1H1RF54NHQa4qagj7llYkCcnHmc8jzkj810DE8iU4aI2u5K2fmU9/z/hvF1+223bEXRnSKAinyjmw==", "cpu": [ "arm64" ], @@ -565,9 +565,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.32.tgz", - "integrity": "sha512-R6SW1dsEVmPMhrN/WRTetS4gVxcuYcxi2zfDPOfcjW3W0iD0Vwpt3MlqwBaU2UL36j+rnTnmiOA+g82FIBCYVg==", + "version": "1.0.39-0", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.39-0.tgz", + "integrity": "sha512-N3Q5G6hDLKeiU+40mgdZk3Sk3b6/+pvNE3Tp5B8LK/Z3CvE2fQKYRXJx8iSDNtP48QwRqwHdrCGQVwDtEtSDAQ==", "cpu": [ "x64" ], diff --git a/test/harness/package.json b/test/harness/package.json index af521775b..6b0c60b4e 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -11,7 +11,7 @@ "test": "vitest run" }, "devDependencies": { - "@github/copilot": "^1.0.32", + "@github/copilot": "^1.0.39-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "openai": "^6.17.0", diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index f19674052..0a57c5e01 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -302,6 +302,28 @@ describe("ReplayingCapiProxy", () => { ); }); + test("strips system_reminder from user messages", async () => { + const requestBody = JSON.stringify({ + messages: [ + { + role: "user", + content: + "What is 2+2?\n\n\nNo tables currently exist.\n", + }, + ], + }); + const responseBody = JSON.stringify({ + choices: [{ message: { role: "assistant", content: "4" } }], + }); + + const outputPath = await createProxy([ + { url: "/chat/completions", requestBody, responseBody }, + ]); + + const result = await readYamlOutput(outputPath); + expect(result.conversations[0].messages[0].content).toBe("What is 2+2?"); + }); + test("strips agent_instructions from user messages", async () => { const requestBody = JSON.stringify({ messages: [ diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 967c18084..f8864cccd 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -854,6 +854,7 @@ function normalizeUserMessage(content: string): string { return content .replace(/.*?<\/current_datetime>/g, "") .replace(/[\s\S]*?<\/reminder>/g, "") + .replace(/[\s\S]*?<\/system_reminder>/g, "") .replace(/[\s\S]*?<\/agent_instructions>/g, "") .replace( /Please create a detailed summary of the conversation so far\. The history is being compacted[\s\S]*/, @@ -864,10 +865,15 @@ function normalizeUserMessage(content: string): string { function normalizeLargeOutputFilepaths(result: string): string { // Replaces filenames like 1774637043987-copilot-tool-output-tk7puw.txt with PLACEHOLDER-copilot-tool-output-PLACEHOLDER - return result.replace( - /\d+-copilot-tool-output-[a-z0-9.]+/g, - "PLACEHOLDER-copilot-tool-output-PLACEHOLDER", - ); + return result + .replace( + /\d+-copilot-tool-output-[a-z0-9.]+/g, + "PLACEHOLDER-copilot-tool-output-PLACEHOLDER", + ) + .replace( + /(?:[A-Za-z]:)?[^\s"'`]*[\\/]session-state[\\/]temp[\\/]PLACEHOLDER-copilot-tool-output-PLACEHOLDER/g, + "/session-state/temp/PLACEHOLDER-copilot-tool-output-PLACEHOLDER", + ); } // Transforms a single OpenAI-style inbound response message into normalized form