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:
65
progress.txt
65
progress.txt
@@ -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`
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user