Phase 1: Add Copilot SDK foundation alongside existing langchaingo agent#6883
Phase 1: Add Copilot SDK foundation alongside existing langchaingo agent#6883
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds the GitHub Copilot SDK (v0.1.25) as a foundational dependency and creates new agent infrastructure that will eventually replace the existing langchaingo-based implementation. This is Phase 1 of a 3-phase migration, where all new code coexists alongside existing code without any deletions or modifications to current functionality.
Changes:
- Adds GitHub Copilot SDK dependency and creates wrapper types (
CopilotClientManager,SessionConfigBuilder) that bridge azd config to SDK types - Implements
CopilotAgentandCopilotAgentFactorythat satisfy the existingAgentinterface for seamless future switchover - Creates event handling infrastructure (
SessionEventLogger,SessionFileLogger) that maps SDK events to azd's existing UX patterns - Adds 8 new config options (
ai.agent.*) for customizing agent behavior (model, tools, MCP servers, system message)
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
go.mod / go.sum |
Adds copilot-sdk/go v0.1.25 and transitive dependency jsonschema-go v0.4.2 (both marked indirect) |
resources/config_options.yaml |
Adds 8 new config keys for Copilot SDK agent customization |
pkg/llm/copilot_client.go |
Manager wrapping SDK client lifecycle with azd-specific error messages |
pkg/llm/copilot_client_test.go |
Tests for client manager instantiation (2 cases) |
pkg/llm/session_config.go |
Bridge converting azd config → SDK SessionConfig with MCP server merging |
pkg/llm/session_config_test.go |
Tests for config reading, tool control, MCP merging (8 cases) |
internal/agent/copilot_agent.go |
Agent implementation using SDK Session.SendAndWait, reuses existing UX patterns |
internal/agent/copilot_agent_factory.go |
Factory creating agents with SDK client, session, hooks, event handlers |
internal/agent/logging/session_event_handler.go |
Event handlers mapping SDK events to thought channel + file logging |
internal/agent/logging/session_event_handler_test.go |
Tests for event handling, tool input extraction, composite handler (10 cases) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| type: object | ||
| example: '["read_file", "write_file"]' | ||
| - key: ai.agent.tools.excluded | ||
| description: "Denylist of tools blocked from the agent." | ||
| type: object |
There was a problem hiding this comment.
The type field should be array instead of object for ai.agent.tools.available and ai.agent.tools.excluded. These config keys are documented to accept JSON arrays like ["read_file", "write_file"] and the code reads them using getStringSliceFromConfig() which calls cfg.GetSlice(). The object type is misleading.
| type: object | |
| example: '["read_file", "write_file"]' | |
| - key: ai.agent.tools.excluded | |
| description: "Denylist of tools blocked from the agent." | |
| type: object | |
| type: array | |
| example: '["read_file", "write_file"]' | |
| - key: ai.agent.tools.excluded | |
| description: "Denylist of tools blocked from the agent." | |
| type: array |
| userConfig, err := b.userConfigManager.Load() | ||
| if err != nil { | ||
| // Use defaults if config can't be loaded | ||
| return cfg, nil |
There was a problem hiding this comment.
When userConfigManager.Load() fails, the function returns the default config successfully (line 43), which silently swallows the error. Based on established patterns in the codebase (see stored memory), errors from userConfigManager.Load() are typically handled with graceful fallback. However, it would be more robust to log the error at debug level so users can troubleshoot config loading issues if needed. Consider adding a debug log statement before returning the default config.
| // Wire permission hooks | ||
| sessionConfig.Hooks = &copilot.SessionHooks{ | ||
| OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) ( | ||
| *copilot.PreToolUseHookOutput, error, | ||
| ) { | ||
| // Allow all tools by default — SDK handles its own permission model. | ||
| // In Phase 2, azd-specific security policies (path validation) will be wired here. | ||
| return &copilot.PreToolUseHookOutput{}, nil |
There was a problem hiding this comment.
The OnPreToolUse hook always returns an empty PreToolUseHookOutput (line 107), which allows all tools to execute without any validation. The comment mentions "azd-specific security policies (path validation) will be wired here in Phase 2", but until then, this creates a security gap. Tools could potentially access files outside the project directory, execute arbitrary commands, or perform other dangerous operations without validation. Consider at minimum implementing basic path validation to prevent directory traversal attacks even in Phase 1, or documenting this security limitation prominently in the PR description and release notes.
| type: string | ||
| example: "gpt-4.1" | ||
| - key: ai.agent.mode | ||
| description: "Default agent mode for Copilot SDK sessions." |
There was a problem hiding this comment.
The config option ai.agent.mode is documented in config_options.yaml (lines 89-93) but is never read or used in the SessionConfigBuilder.Build() method. Either this config option should be removed from config_options.yaml, or the code should read it and set it on the SessionConfig. If it's intended for Phase 2, that should be documented in a comment.
| description: "Default agent mode for Copilot SDK sessions." | |
| description: "Default agent mode for Copilot SDK sessions. Reserved for future use (Phase 2) and not yet applied by the runtime." |
cli/azd/pkg/llm/session_config.go
Outdated
| // Skill control | ||
| if dirs := getStringSliceFromConfig(userConfig, "ai.agent.skills.directories"); len(dirs) > 0 { | ||
| cfg.SkillDirectories = dirs | ||
| } | ||
| if disabled := getStringSliceFromConfig(userConfig, "ai.agent.skills.disabled"); len(disabled) > 0 { | ||
| cfg.DisabledSkills = disabled | ||
| } | ||
|
|
There was a problem hiding this comment.
The code reads config options ai.agent.skills.directories (line 68) and ai.agent.skills.disabled (line 72) but these are not documented in resources/config_options.yaml. Either add documentation for these config options or remove the code that reads them if they're not intended for Phase 1.
| // Skill control | |
| if dirs := getStringSliceFromConfig(userConfig, "ai.agent.skills.directories"); len(dirs) > 0 { | |
| cfg.SkillDirectories = dirs | |
| } | |
| if disabled := getStringSliceFromConfig(userConfig, "ai.agent.skills.disabled"); len(disabled) > 0 { | |
| cfg.DisabledSkills = disabled | |
| } |
| // NewCopilotAgent creates a new CopilotAgent backed by the given copilot.Session. | ||
| func NewCopilotAgent( | ||
| session *copilot.Session, | ||
| console input.Console, | ||
| opts ...CopilotAgentOption, | ||
| ) *CopilotAgent { | ||
| agent := &CopilotAgent{ | ||
| session: session, | ||
| console: console, | ||
| watchForFileChanges: true, | ||
| } | ||
|
|
||
| for _, opt := range opts { | ||
| opt(agent) | ||
| } | ||
|
|
||
| return agent | ||
| } |
There was a problem hiding this comment.
CopilotAgent and CopilotAgentFactory lack unit tests. These are core components that implement the Agent interface and manage complex lifecycle operations (cleanup ordering, event subscription, context cancellation). Consider adding tests for: CopilotAgent.SendMessage success/error paths, CopilotAgentFactory.Create cleanup on error paths, and proper resource cleanup ordering. The existing agent code has test coverage (e.g., agent_factory_test.go for the langchaingo implementation), so the same standard should apply here.
| if probe.Type == "http" { | ||
| var remote map[string]any | ||
| if err := json.Unmarshal(data, &remote); err != nil { | ||
| continue | ||
| } | ||
| result[name] = copilot.MCPServerConfig(remote) | ||
| } else { | ||
| var local map[string]any | ||
| if err := json.Unmarshal(data, &local); err != nil { | ||
| continue | ||
| } | ||
| result[name] = copilot.MCPServerConfig(local) | ||
| } |
There was a problem hiding this comment.
The logic for handling http vs stdio MCP servers is redundant. Both branches (lines 161-166 and 168-172) do essentially the same thing: unmarshal into map[string]any and assign to result[name]. The type detection logic is useful, but the separate handling can be simplified to a single code path since both produce the same MCPServerConfig type.
| if probe.Type == "http" { | |
| var remote map[string]any | |
| if err := json.Unmarshal(data, &remote); err != nil { | |
| continue | |
| } | |
| result[name] = copilot.MCPServerConfig(remote) | |
| } else { | |
| var local map[string]any | |
| if err := json.Unmarshal(data, &local); err != nil { | |
| continue | |
| } | |
| result[name] = copilot.MCPServerConfig(local) | |
| } | |
| // Unmarshal into a generic map and store as MCPServerConfig. | |
| // Both HTTP and stdio configurations share the same representation. | |
| var serverCfg map[string]any | |
| if err := json.Unmarshal(data, &serverCfg); err != nil { | |
| continue | |
| } | |
| result[name] = copilot.MCPServerConfig(serverCfg) |
| cleanupTasks := map[string]func() error{} | ||
|
|
||
| cleanup := func() error { | ||
| for name, task := range cleanupTasks { | ||
| if err := task(); err != nil { | ||
| log.Printf("failed to cleanup %s: %v", name, err) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Start the Copilot client (spawns copilot-agent-runtime) | ||
| if err := f.clientManager.Start(ctx); err != nil { | ||
| return nil, err | ||
| } | ||
| cleanupTasks["copilot-client"] = f.clientManager.Stop | ||
|
|
||
| // Create thought channel for UX streaming | ||
| thoughtChan := make(chan logging.Thought) | ||
| cleanupTasks["thoughtChan"] = func() error { | ||
| close(thoughtChan) | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The cleanup order may cause a panic. The thoughtChan is closed before the session is destroyed or events are unsubscribed. If session events are still being processed after close(thoughtChan) executes, event handlers will panic when attempting to send to a closed channel. The cleanup order should be: 1) unsubscribe from session events, 2) destroy session, 3) close thoughtChan, 4) close file logger, 5) stop client. Consider using an ordered cleanup approach (e.g., slice of cleanup tasks executed in reverse order) instead of a map which has undefined iteration order.
| var watcher watch.Watcher | ||
| if a.watchForFileChanges { | ||
| var err error | ||
| watcher, err = watch.NewWatcher(ctx) | ||
| if err != nil { | ||
| cancelCtx() | ||
| return "", fmt.Errorf("failed to start watcher: %w", err) | ||
| } | ||
| } |
There was a problem hiding this comment.
The watcher is created with ctx (line 84) but thoughtsCtx is canceled in the defer (line 98). The watcher goroutine will only stop when the original ctx is done, not when thoughtsCtx is canceled. This could cause the watcher to keep running longer than intended. Consider passing thoughtsCtx to watch.NewWatcher() instead of ctx to ensure the watcher stops when the SendMessage operation completes.
Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go) as a new dependency and create the foundational types for a Copilot SDK-based agent implementation. All new code coexists alongside the existing langchaingo agent — no existing code is modified or deleted. New files: - pkg/llm/copilot_client.go: CopilotClientManager wrapping copilot.Client lifecycle (Start, Stop, GetAuthStatus, ListModels) - pkg/llm/session_config.go: SessionConfigBuilder that reads ai.agent.* config keys and produces copilot.SessionConfig, including MCP server merging (built-in + user-configured) and tool control - internal/agent/copilot_agent.go: CopilotAgent implementing the Agent interface backed by copilot.Session with SendAndWait - internal/agent/copilot_agent_factory.go: CopilotAgentFactory that creates CopilotAgent instances with SDK client, session, permission hooks, MCP servers, and event handlers - internal/agent/logging/session_event_handler.go: SessionEventLogger, SessionFileLogger, and CompositeEventHandler for SDK SessionEvent streaming to UX thought channel and daily log files Config additions (resources/config_options.yaml): - ai.agent.model: Default model for Copilot SDK sessions - ai.agent.mode: Agent mode (autopilot/interactive/plan) - ai.agent.mcp.servers: Additional MCP servers - ai.agent.tools.available/excluded: Tool allow/deny lists - ai.agent.systemMessage: Custom system prompt append - ai.agent.copilot.logLevel: SDK log level Resolves #6871, #6872, #6873, #6874, #6875 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Register CopilotProvider as a named 'copilot' model provider in the IoC container. This makes 'copilot' the default agent type when ai.agent.model.type is not explicitly configured. Changes: - Add LlmTypeCopilot constant and CopilotProvider (copilot_provider.go) - Default GetDefaultModel() to 'copilot' when no model type is set - Register 'copilot' provider in container.go - Update init.go to set 'copilot' instead of 'github-copilot' - Update error message to list copilot as supported type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add [copilot] and [copilot-event] prefixed log statements throughout the Copilot SDK agent pipeline for troubleshooting: - CopilotClientManager: Start/stop state transitions - CopilotAgentFactory: MCP server count, session config details, PreToolUse/PostToolUse/ErrorOccurred hook invocations - CopilotAgent.SendMessage: prompt size, response size, errors - SessionEventLogger: every event type received, plus detail for assistant.message, tool.execution_start, and assistant.reasoning Run with AZD_DEBUG=true to see log output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
AgentFactory.Create() now checks the configured model type. When it's 'copilot' (the new default), it delegates to CopilotAgentFactory which creates a CopilotAgent backed by the Copilot SDK session. No call site changes needed — existing code calling AgentFactory.Create() gets the Copilot SDK agent automatically. Changes: - AgentFactory now takes CopilotAgentFactory as a dependency - Create() checks model type and delegates to CopilotAgentFactory - Register CopilotAgentFactory, CopilotClientManager, and SessionConfigBuilder in IoC container (cmd/container.go) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set SDK LogLevel to 'debug' by default to surface the command and args the SDK uses when spawning the copilot CLI process. This will help diagnose the 'exit status 1' error during client.Start(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CLI v0.0.419-0 now supports --headless and --stdio flags required by the SDK. Updated Go SDK from v0.1.25 to v0.1.26-preview.0 for latest compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…package
The copilot CLI shim in PATH (from @github/copilot npm package) doesn't
support --headless --stdio flags required by the Go SDK. However, the
@github/copilot-sdk npm package bundles a newer native binary at
node_modules/@github/copilot-{platform}/copilot[.exe] that does.
CopilotClientManager now auto-discovers this binary with resolution order:
1. COPILOT_CLI_PATH environment variable
2. Native binary from @github/copilot-sdk npm package (platform-specific)
3. Falls back to 'copilot' in PATH (SDK default)
Also adds a passing e2e test (TestCopilotSDK_E2E) that validates the full
SDK lifecycle: client start → auth check → list models → create session →
send prompt → receive response → cleanup. Pure native binary, no Node.js.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The empty PreToolUseHookOutput{} was interpreted as deny by the SDK,
blocking all tool calls. Set PermissionDecision to 'allow' explicitly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The SDK has two separate permission mechanisms: 1. PreToolUse hooks (lifecycle interception) - already set to 'allow' 2. OnPermissionRequest handler (CLI permission prompts) - was NOT set Without OnPermissionRequest, the CLI's permission requests go unanswered and default to deny, blocking all tool calls. Use the SDK's built-in PermissionHandler.ApproveAll to approve all requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The SDK defaults to 60s timeout when the context has no deadline, which is too short for agent init tasks (discovery, IaC generation, Dockerfile creation, etc.). Set a 10-minute timeout per message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace SendAndWait (which imposes a 60s default timeout) with Send (non-blocking) + explicit wait for session.idle event. The agent task runs until the SDK signals completion or the parent context is cancelled. No artificial timeout. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two major changes to CopilotAgentFactory:
1. Required plugin auto-install:
- Runs 'copilot plugin install microsoft/GitHub-Copilot-for-Azure:plugin'
before starting each session (idempotent, non-interactive)
- Uses the resolved CLI binary path from CopilotClientManager
- Logs warnings on install failure but doesn't block session creation
2. Wire azd consent system into SDK permission handlers:
- OnPermissionRequest: delegates to ConsentManager.CheckConsent()
for CLI-level permission requests. If consent requires prompting,
uses ConsentChecker to show azd's interactive consent UX with
scoped persistence (session/project/global).
- OnPreToolUse: checks ConsentManager before each tool execution.
If no rule exists, prompts via ConsentChecker.PromptAndGrantConsent()
which stores the user's choice at their selected scope.
- Replaces the previous PermissionHandler.ApproveAll with proper
consent-gated approval flow.
Also:
- CopilotAgentFactory now takes ConsentManager as a dependency
- CopilotClientManager exposes CLIPath() for plugin install commands
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d606a7b to
b3ff2be
Compare
The Dev struct needs to be declared as 'type Dev mg.Namespace' for mage to discover it as a namespace. Also adds github.com/magefile/mage to go.mod as required by the magefile's mg import. This fixes 'mage dev:install' and 'mage dev:uninstall' targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The copilot-sdk pulled in otel core v1.42.0 (schema 1.39.0) but otel/sdk remained at v1.38.0 (schema 1.37.0). This mismatch caused a panic in resource.New() at startup. Upgraded all OTel SDK packages to v1.42.0 to match the core version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Updated from v0.1.26-preview.0 to v0.1.32. Adapted to API change where PermissionRequest.Kind is now a typed PermissionRequestKind instead of plain string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
OTel SDK v1.42.0 uses semconv/v1.40.0 internally for resource.Default(), but our resource.go imported semconv/v1.39.0. The schema URL mismatch (1.40.0 vs 1.39.0) caused a panic on resource.Merge(). Updated import to match the SDK's semconv version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The OnPermissionRequest and OnPreToolUse handlers were conflicting: OnPermissionRequest was calling CheckToolConsent (which only checks rules, doesn't prompt) and falling through to 'denied' when no rules existed. Meanwhile OnPreToolUse correctly called PromptAndGrantConsent. Fix: OnPermissionRequest now approves all CLI-level permission requests (file access, shell, URLs). Fine-grained per-tool consent with user prompting is handled exclusively by the OnPreToolUse hook. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the thought-channel indirection with AgentDisplay — a direct event-driven UX renderer that subscribes to session.On() and handles all SDK event types using existing azd UX components. New: internal/agent/display.go - AgentDisplay manages Canvas + Spinner + VisualElement per SendMessage - Handles 15+ event types: turn lifecycle, tool execution with elapsed timer, reasoning/thinking display, streaming message deltas, errors, warnings, skill invocations, subagent delegation - WaitForIdle() blocks until session.idle with final content capture - Thread-safe state management for concurrent event handling Changes: - CopilotAgent.SendMessage() creates AgentDisplay per turn, subscribes it to session events, and uses WaitForIdle() for completion - CopilotAgentFactory no longer creates thought channel or SessionEventLogger — file logger remains for audit trail - Export TruncateString from logging package for shared use Removes: - Thought channel and WithCopilotThoughtChannel option - renderThoughts() goroutine (replaced by AgentDisplay) - SessionEventLogger dependency in factory (UX moved to display) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two fixes to AgentDisplay: 1. Reasoning: accumulate reasoning_delta chunks into a rolling buffer instead of replacing with each delta. Also handle streaming_delta events with phase='thinking' for models that emit reasoning that way. Canvas.Update() called after each reasoning update for immediate display. 2. Paths: extractToolInputSummary now converts absolute paths to relative (via filepath.Rel to cwd) for cleaner display. Paths that escape cwd are shown absolute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reasoning display now works as a scrolling window: - VisualElement shows last ~5 lines of reasoning below the spinner, updating live as delta chunks stream in - Full reasoning accumulates in a buffer (no truncation) - On tool start or turn end, the complete reasoning is printed as a persistent dimmed block above the canvas, then the buffer resets Removed latestThought field — reasoning state is now entirely managed via reasoningBuf. Removed AssistantMessageDelta handler (message deltas don't need UX rendering — final content comes via AssistantMessage). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ensurePlugins now lists installed plugins first. If a required plugin is already installed, it runs 'copilot plugin update' instead of a full install. New plugins get 'copilot plugin install'. Also restructured requiredPlugins as pluginSpec with Source (install path) and Name (installed name for update command). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two changes: 1. Wire OnUserInputRequest in CopilotAgentFactory: Enables the agent's built-in ask_user tool. When the agent asks a question, the handler renders it using azd's UX components: - Choices → ux.NewSelect() with azd styling - Freeform → ux.NewPrompt() for text input This lets the agent ask clarifying questions during execution (architecture choices, service selection, config options). 2. Simplify initAppWithAgent() from 6-step loop to single prompt: Replaces 6 hardcoded steps + inter-step feedback loops + post- completion summary aggregation with a single prompt that delegates to azure-prepare and azure-validate skills from the Azure plugin. The skills handle all orchestration internally and can ask the user questions via ask_user when needed. Removed: initStep struct, step definitions, collectAndApplyFeedback(), postCompletionSummary(), feedback import (~140 lines). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eractive mode Three UX improvements: 1. Spinner text: uses assistant.intent events (short task descriptions like 'Analyzing project structure') instead of static 'Working...' Truncated to 80 chars to stay concise. 2. Reasoning display: moved above the spinner instead of below. Layout is now: blank line → reasoning (last 5 lines, gray) → blank line → spinner. More natural reading order. 3. Mode: SendMessage now explicitly sets Mode to 'interactive' so the agent asks for approval before executing tools. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… fix skill triggers
Three changes:
1. ask_user: Strip markdown formatting (bold, italic, backticks, headings)
from question text and choice labels before rendering in azd UX prompts.
Choices still return the original value to the agent.
2. flushReasoning: Render accumulated reasoning with output.WithMarkdown()
instead of raw gray text, giving proper formatting for code blocks,
lists, and other markdown content.
3. init prompt: Use natural trigger phrases ('prepare this application
for deployment to Azure', 'validate that everything is ready') that
match the azure-prepare and azure-validate skill description triggers,
instead of referencing skill names directly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the agent sends choices with AllowFreeform=true, append an 'Other (type your own answer)' option to the Select list. If selected, follow up with a freeform Prompt and return WasFreeform: true. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rompt Use @skill-name syntax and numbered steps to ensure both skills are invoked in order. Previous natural language triggers may not have reliably activated the skills. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sets SessionConfig.ConfigDir to .azure/copilot in the current working directory so session state is project-local instead of global ~/.copilot. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ConfigDir override to .azure/copilot broke plugin discovery — the CLI loads plugins from ConfigDir/installed-plugins/ which was empty. Reverted to default ~/.copilot so installed plugins (and their skills) are found. Set WorkingDirectory instead for tool operations. Updated init prompt to use fully qualified azure:azure-prepare and azure:azure-validate skill names (plugin:skill-name format). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CLI in --headless --stdio mode (used by the SDK) doesn't auto-discover installed plugins from ~/.copilot/installed-plugins/. This meant skills from the Azure plugin were never loaded. Fix: discoverInstalledPluginDirs() scans ~/.copilot/installed-plugins/ for plugin directories (verified by presence of skills/ or .claude-plugin/) and passes each via --plugin-dir CLIArgs to the SDK client. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Only load the microsoft/GitHub-Copilot-for-Azure plugin via --plugin-dir. Checks both _direct and marketplace install paths. Other plugins will be handled separately later. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The --plugin-dir CLI flag isn't supported by all copilot binary builds. Instead, pass the Azure plugin's skills directory via SessionConfig. SkillDirectories, which is sent via JSON-RPC createSession and works reliably in headless mode. discoverAzurePluginSkillDirs() finds the skills/ directory from the installed Azure plugin and adds it to SkillDirectories alongside any user-configured skill directories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Track lastIntent and reuse it when the spinner resets after turn start or tool completion. Only falls back to 'Thinking...' if no intent has been received yet. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
toRelativePath now tries both cwd and ~/.copilot/installed-plugins/ as base directories. Paths under the plugins root (skill files) are shown relative instead of absolute. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The agent uses a report_intent tool (not assistant.intent events) to signal what it's working on. Extract the intent text from the tool's arguments and use it as the spinner text. The report_intent tool is suppressed from UX display (no 'Ran report_intent' completion line). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Subagent events now show richer UX:
- started: '◆ {DisplayName} — {Description}'
- completed: '✔ {DisplayName} completed' with summary
- failed: '✖ {DisplayName} failed: {error}'
- deselected: resets subagent state
Tool calls inside a subagent are indented with 2 spaces to show
nesting visually:
◆ Azure Prepare — Prepare apps for deployment
✔ Ran read_file with path: main.go
✔ Ran write_file with path: azure.yaml
✔ Azure Prepare completed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add blank line before 'Using skill:' and 'Delegating to:' lines - Suppress tool call display for report_intent, ask_user, task, and skill: prefixed tools — these are internal/UX tools that shouldn't show as 'Ran X' completion lines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
First run (no ai.agent.reasoningEffort config): - Prompt for reasoning effort (low/medium/high) with cost guidance - Prompt for model selection (default or specific model) - Save both to azd config Subsequent runs: - Show info note with current model and reasoning level - Point to 'azd config set' for changes Also: - Wire ai.agent.reasoningEffort to SessionConfig.ReasoningEffort - Add reasoningEffort to config_options.yaml Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Multiple session.idle events can arrive during a session (after permission prompts, between tool calls). The first idle was consumed by WaitForIdle before the assistant message arrived, causing the display to clear with no summary shown. Fix: only signal idleCh when finalContent has been set by an assistant.message event. Early idle events are ignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Read .mcp.json from the installed Azure plugin and merge its MCP servers (azure, foundry-mcp, context7) into SessionConfig.MCPServers alongside built-in and user-configured servers. Merge order: built-in → Azure plugin → user config (last wins). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause: assistant.message from an earlier turn set finalContent, then a session.idle between tool calls found hasContent=true and signaled WaitForIdle prematurely — before the actual final message. Fixes: - Reset finalContent on every assistant.turn_start so only the last turn's message is considered - Handle session.task_complete and session.shutdown as additional completion signals - Add debug logging to assistant.message, session.idle, and WaitForIdle for future troubleshooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Azure Dev CLI Install InstructionsInstall scriptsMacOS/Linux
bash: pwsh: WindowsPowerShell install MSI install Standalone Binary
MSI
Documentationlearn.microsoft.com documentationtitle: Azure Developer CLI reference
|
Overview
Add the GitHub Copilot SDK (github.com/github/copilot-sdk/go v0.1.25) as a new dependency and create the foundational types for a Copilot SDK-based agent implementation. All new code coexists alongside the existing langchaingo agent — no existing code is modified or deleted.
Resolves #6871, #6872, #6873, #6874, #6875
Part of Epic #6870
What's New
New Files (7)
pkg/llm/copilot_client.goCopilotClientManagerwrappingcopilot.Clientlifecycle (Start, Stop, GetAuthStatus, ListModels)pkg/llm/session_config.goSessionConfigBuilderreadsai.agent.*config keys →copilot.SessionConfigwith MCP server merging and tool controlinternal/agent/copilot_agent.goCopilotAgentimplementingAgentinterface backed bycopilot.Session.SendAndWait()internal/agent/copilot_agent_factory.goCopilotAgentFactorycreates agents with SDK client, session, permission hooks, MCP servers, event handlersinternal/agent/logging/session_event_handler.goSessionEventLogger+SessionFileLogger+CompositeEventHandlerfor SDK SessionEvent streamingNew Config Options (
resources/config_options.yaml)ai.agent.modelgpt-4.1)ai.agent.modeautopilot/interactive/planai.agent.mcp.serversai.agent.tools.availableai.agent.tools.excludedai.agent.systemMessageai.agent.copilot.logLevelTest Files (3)
pkg/llm/copilot_client_test.go— Client manager instantiation testspkg/llm/session_config_test.go— Config bridge tests (model, system message, tool control, MCP server merging)internal/agent/logging/session_event_handler_test.go— Event handler tests (assistant messages, tool start, reasoning, composite handler)Key Design Decisions
CopilotAgentimplements the sameAgentinterface as existingConversationalAzdAiAgent, so call sites can switch in Phase 2 without API changesmap[string]any: Matches SDK's flexible type definition, withconvertServerConfig()bridging from azd'smcp.ServerConfigPreToolUse/PostToolUsehooks wired as pass-through stubs; azd-specific security policies will be added in Phase 2CopilotAgent.renderThoughts()reuses the same spinner + canvas UX pattern as the existing agentTesting
go build ./...✅pkg/llm,internal/agent/...,pkg/config)