feat: Integrate GitHub Copilot SDK and LangChain for provider-independent orchestration

- Added @github/copilot-sdk and related dependencies for GitHub Copilot integration.
- Implemented ChatCopilot adapter for LangChain compatibility.
- Created LLM factory to return provider-specific models (OpenAI, Anthropic, Copilot).
- Developed Orchestrator agent using LangGraph for intent routing and context assembly.
- Enhanced IPC communication for streaming AI responses to the renderer.
- Updated progress documentation with implementation details and learnings.
This commit is contained in:
Roberto Musso
2026-02-23 17:58:00 +01:00
parent c1aa6829c9
commit 00a43e0fbc
13 changed files with 1348 additions and 14 deletions

View File

@@ -351,3 +351,68 @@
- Token is stored per-provider so multiple providers can have tokens stored simultaneously; switching providers just changes which key is read
- `electron-store` dot-notation access (`store.get('a.b')`) works but loses type safety; prefer `store.get('encryptedTokens')` then access the nested key on the result object
---
## 2026-02-23 - US-019
- What was implemented:
- Installed `@github/copilot-sdk` (v0.1.25) — official GitHub Copilot SDK with CopilotClient for programmatic CLI control via JSON-RPC
- Updated `src/main/ai/copilot.ts` — CopilotClient singleton created via dynamic `import()` (SDK is ESM-only), `initialize()` starts the client with `githubToken`, `getCopilotClient()` exported, clean shutdown on `app.before-quit`
- Added `@github/copilot-sdk` and `@github/copilot` to `vite.main.config.mts` externals (native CLI binary + prebuilds must stay in node_modules)
- Created IPC streaming side-channel:
- `src/preload/trpc.ts` — exposed `window.electronAI.onStreamChunk(cb)` via contextBridge on `ai:stream` IPC channel
- `src/main/ipc.ts` — added `TRPCContext` type with optional `sender: Electron.WebContents`, passed `event.sender` into tRPC context
- `src/renderer/lib/ipcLink.ts` — added `Window.electronAI` type declaration
- Updated `src/main/router/index.ts` — `initTRPC.context<TRPCContext>().create()`, `ai.chat` mutation now calls `orchestrate()` with streaming + error handling
- Created `src/main/ai/orchestrator.ts` (new) — core Orchestrator agent:
- System prompt instructs model to use exactly one routing tool per message
- 3 routing tools: `route_to_project`, `route_to_knowledge`, `route_to_general` (each with JSON schema params + handler callback)
- `buildProjectContext(projectId)` — fetches project, tasks, checkpoints, notes from DB
- `buildGlobalContext()` — fetches active projects, task counts, upcoming tasks due this week
- Two-phase orchestration: (1) Orchestrator session classifies intent via tool call, (2) Specialist session generates streamed response
- Specialist agent prompts: @ProjectAgent (scoped project data), @KnowledgeAgent (stub — LanceDB pending US-023), @GeneralAgent (workspace summary)
- Streaming via `session.on('assistant.message_delta')` → `sender.send('ai:stream', { token, done })`
- Error classification: auth (401/403) → friendly message, timeout → retry prompt, generic → error message
- Typecheck passes (zero errors), no new lint errors
- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/copilot.ts`, `src/main/ai/orchestrator.ts` (new), `src/main/ipc.ts`, `src/preload/trpc.ts`, `src/main/router/index.ts`, `src/renderer/lib/ipcLink.ts`, `prd.json`, `progress.txt`
- **Learnings for future iterations:**
- `@github/copilot-sdk` is ESM-only (`"type": "module"`) — cannot `require()` from CJS Electron main process. Use `await import('@github/copilot-sdk')` inside async functions
- The SDK depends on `@github/copilot` (the CLI binary, ~400MB) which includes native prebuilds, ripgrep, tree-sitter WASM, etc. Both must be externalized in Vite config
- `CopilotClient` manages a CLI subprocess via JSON-RPC. `autoStart: true` + `autoRestart: true` handles lifecycle. Call `client.stop()` on `app.before-quit`
- SDK tools use a `handler: async (args) => ToolResult` callback pattern — the SDK calls your handler when the model invokes the tool. This is different from OpenAI's function-calling (where you check tool_calls in the response)
- `session.sendAndWait()` blocks until `session.idle`, returns `AssistantMessageEvent | undefined`. Default timeout is 60s
- `session.on('assistant.message_delta', cb)` fires for each streaming chunk with `{ messageId, deltaContent }`. Returns an unsubscribe function
- `SessionConfig.availableTools` controls which tools are exposed to the model. Set to only your custom tool names to disable all built-in Copilot tools
- `SystemMessageConfig` has `mode: 'replace'` to fully replace the SDK's default system prompt — necessary for agent specialization
- For IPC streaming: extend the preload to expose a separate channel (`ai:stream`) via `contextBridge.exposeInMainWorld()`, and use `event.sender.send()` from the main process. This avoids touching the tRPC request-response infrastructure
- `TRPCContext` with optional `sender` field preserves backward compatibility — non-AI procedures get `sender` but don't use it
---
## 2026-02-23 - US-019 Refactor: LangGraph Provider-Independent Orchestration
- What was implemented:
- Installed `@langchain/langgraph`, `@langchain/core`, `@langchain/openai`, `@langchain/anthropic` for provider-independent agent orchestration
- Created `src/main/ai/llm.ts` (new) — LLM factory returning `BaseChatModel` for the active provider:
- `openai` → `ChatOpenAI` (gpt-4o-mini, streaming)
- `anthropic` → `ChatAnthropic` (claude-sonnet, streaming)
- `copilot` → `ChatCopilot` adapter wrapping the Copilot SDK
- Created `src/main/ai/chat-copilot.ts` (new) — LangChain-compatible `SimpleChatModel` adapter for GitHub Copilot SDK:
- `_call()` creates a Copilot session, sends messages, returns response text
- `_streamResponseChunks()` yields `ChatGenerationChunk` tokens via `assistant.message_delta` event listener + async generator pattern
- Refactored `src/main/ai/orchestrator.ts` — replaced Copilot SDK sessions with LangGraph `StateGraph`:
- `OrchestratorState` annotation: `userMessage`, `chatContext`, `route`, `messages`, `response`
- `classifyIntent` node: uses `llm.withStructuredOutput(RouteSchema)` for intent classification (project/knowledge/general)
- `projectAgent`, `knowledgeAgent`, `generalAgent` nodes: each gets context from DB + invokes LLM
- `addConditionalEdges` routes from classifier to specialist based on `state.route`
- Streaming via LangGraph `streamMode: 'messages'` — tokens from specialist nodes forwarded to renderer via IPC
- Graph is compiled once (singleton) and reused across calls
- Updated `vite.main.config.mts` — added all `@langchain/*` packages to externals
- Context assembly functions (`buildProjectContext`, `buildGlobalContext`) and system prompts preserved unchanged
- Typecheck passes (zero errors), no new lint errors
- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/llm.ts` (new), `src/main/ai/chat-copilot.ts` (new), `src/main/ai/orchestrator.ts`, `progress.txt`
- **Learnings for future iterations:**
- LangGraph packages ship dual ESM/CJS (`require` + `import` in exports map) — no dynamic import needed unlike `@github/copilot-sdk`
- `SimpleChatModel` from `@langchain/core` only requires implementing `_call()` and `_llmType()` — much simpler than full `BaseChatModel`
- `withStructuredOutput(zodSchema)` on a `BaseChatModel` forces the LLM to return JSON matching the schema — ideal for intent routing without custom tool handlers
- LangGraph `streamMode: 'messages'` yields `[chunk, metadata]` tuples; `metadata.langgraph_node` identifies which graph node produced each token — use this to filter out classifier tokens
- `Annotation.Root({})` with a `reducer` function enables append-only message arrays in state — matches LangGraph's immutable state update pattern
- The graph is compiled once via `buildGraph()` singleton — no per-request overhead for graph construction
- Architecture: agent logic (LangGraph) is now fully decoupled from the LLM provider. Adding a new provider only requires a new factory function in `llm.ts`
---