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:
574
package-lock.json
generated
574
package-lock.json
generated
@@ -10,7 +10,12 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fontsource/geist": "^5.2.8",
|
||||
"@github/copilot-sdk": "^0.1.25",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@langchain/anthropic": "^1.3.19",
|
||||
"@langchain/core": "^1.1.27",
|
||||
"@langchain/langgraph": "^1.1.5",
|
||||
"@langchain/openai": "^1.2.9",
|
||||
"@milkdown/crepe": "^7.18.0",
|
||||
"@milkdown/kit": "^7.18.0",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
@@ -89,6 +94,26 @@
|
||||
"nup": "bin/nup.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sdk": {
|
||||
"version": "0.74.0",
|
||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.74.0.tgz",
|
||||
"integrity": "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-schema-to-ts": "^3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"anthropic-ai-sdk": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
@@ -619,6 +644,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cfworker/json-schema": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
|
||||
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@codemirror/autocomplete": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
|
||||
@@ -3062,6 +3093,133 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@github/copilot": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.411.tgz",
|
||||
"integrity": "sha512-I3/7gw40Iu1O+kTyNPKJHNqDRyOebjsUW6wJsvSVrOpT0TNa3/lfm8xdS2XUuJWkp+PgEG/PRwF7u3DVNdP7bQ==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"bin": {
|
||||
"copilot": "npm-loader.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/copilot-darwin-arm64": "0.0.411",
|
||||
"@github/copilot-darwin-x64": "0.0.411",
|
||||
"@github/copilot-linux-arm64": "0.0.411",
|
||||
"@github/copilot-linux-x64": "0.0.411",
|
||||
"@github/copilot-win32-arm64": "0.0.411",
|
||||
"@github/copilot-win32-x64": "0.0.411"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-darwin-arm64": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.411.tgz",
|
||||
"integrity": "sha512-dtr+iHxTS4f8HlV2JT9Fp0FFoxuiPWCnU3XGmrHK+rY6bX5okPC2daU5idvs77WKUGcH8yHTZtfbKYUiMxKosw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-darwin-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-darwin-x64": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.411.tgz",
|
||||
"integrity": "sha512-zhdbQCbPi1L4iHClackSLx8POfklA+NX9RQLuS48HlKi/0KI/JlaDA/bdbIeMR79wjif5t9gnc/m+RTVmHlRtA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-darwin-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linux-arm64": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.411.tgz",
|
||||
"integrity": "sha512-oZYZ7oX/7O+jzdTUcHkfD1A8YnNRW6mlUgdPjUg+5rXC43bwIdyatAnc0ObY21m9h8ghxGqholoLhm5WnGv1LQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linux-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linux-x64": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.411.tgz",
|
||||
"integrity": "sha512-nnXrKANmmGnkwa3ROlKdAhVNOx8daeMSE8Xh0o3ybKckFv4s38blhKdcxs0RJQRxgAk4p7XXGlDDKNRhurqF1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linux-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk": {
|
||||
"version": "0.1.25",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.25.tgz",
|
||||
"integrity": "sha512-hIgYLPXzWw9bNgrsD5BLKmgVH20ow5Or5UyVXfVe3YgeiaTgFxC4jWSAVHLGB6ufHZUrvbjppcq2dWK63FmDRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/copilot": "^0.0.411",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-win32-arm64": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.411.tgz",
|
||||
"integrity": "sha512-h+Bovb2YVCQSeELZOO7zxv8uht45XHcvAkFbRsc1gf9dl109sSUJIcB4KAhs8Aznk28qksxz7kvdSgUWyQBlIA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-win32-arm64": "copilot.exe"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-win32-x64": {
|
||||
"version": "0.0.411",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.411.tgz",
|
||||
"integrity": "sha512-xmOgi1lGvUBHQJWmq5AK1EP95+Y8xR4TFoK9OCSOaGbQ+LFcX2jF7iavnMolfWwddabew/AMQjsEHlXvbgMG8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-win32-x64": "copilot.exe"
|
||||
}
|
||||
},
|
||||
"node_modules/@hello-pangea/dnd": {
|
||||
"version": "18.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
|
||||
@@ -3610,6 +3768,182 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/anthropic": {
|
||||
"version": "1.3.19",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.19.tgz",
|
||||
"integrity": "sha512-OY7/pi4MBuBA/RCJJGqL0Dx3J7Udqwz35Q9sTv5oWiwt6UgMQKGZ17k8gEXyknpuKcRd3A81M09mzoGL8/6zGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.74.0",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core": {
|
||||
"version": "1.1.27",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.27.tgz",
|
||||
"integrity": "sha512-YVtEz3nqCh8WxtdVXUICmt2BR2An+mn4YRJUBwcHX47Yrh2VwxpO0l97B2N/sNi658m65HnGyz2/hAjF3fzc1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cfworker/json-schema": "^4.0.2",
|
||||
"ansi-styles": "^5.0.0",
|
||||
"camelcase": "6",
|
||||
"decamelize": "1.2.0",
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"langsmith": ">=0.5.0 <1.0.0",
|
||||
"mustache": "^4.2.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/core/node_modules/ansi-styles": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.1.5.tgz",
|
||||
"integrity": "sha512-uJC/asydf/GoHpo9x42lf9hs8ufCkMuJ9sDle5ybP7sMD0XryOfE0E4J3deARk9ZadCCt6zeCoCNu/mTbx8+Sg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@langchain/langgraph-checkpoint": "^1.0.0",
|
||||
"@langchain/langgraph-sdk": "~2.0.0",
|
||||
"@standard-schema/spec": "1.1.0",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.16",
|
||||
"zod": "^3.25.32 || ^4.2.0",
|
||||
"zod-to-json-schema": "^3.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod-to-json-schema": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-checkpoint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz",
|
||||
"integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-2.0.0.tgz",
|
||||
"integrity": "sha512-Xdkl1hve84ZGQ7fgpiBIBvjODhtjbPPccY4snOtYgSdzRXZkESsi2Y7RDKgFe1nC9+DbX+QaYom0raD/XFBKAw==",
|
||||
"deprecated": "This version is not intended for use. Please use 1.x versions of @langchain/langgraph-sdk instead",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"p-queue": "^9.0.1",
|
||||
"p-retry": "^7.1.1",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.16",
|
||||
"react": "^18 || ^19",
|
||||
"react-dom": "^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz",
|
||||
"integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^5.0.1",
|
||||
"p-timeout": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
|
||||
"integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/langgraph-sdk/node_modules/uuid": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
|
||||
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@langchain/openai": {
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.2.9.tgz",
|
||||
"integrity": "sha512-hExRiUoKOg1vfkwBAI5J2C4tqNx5LLZ0CUelG8Ej6K8bS2LfFN9bL4ZNQYqNIwAJNSqpDaV9tknxP2fssZjp+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tiktoken": "^1.0.12",
|
||||
"openai": "^6.18.0",
|
||||
"zod": "^3.25.76 || ^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@langchain/core": "^1.1.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@lezer/common": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
|
||||
@@ -6551,6 +6885,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
|
||||
@@ -7360,7 +7700,6 @@
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
@@ -7499,6 +7838,12 @@
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validate-npm-package-name": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
|
||||
@@ -9266,6 +9611,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
|
||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001770",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
|
||||
@@ -9686,6 +10043,15 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/console-table-printer": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz",
|
||||
"integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"simple-wcswidth": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
|
||||
@@ -10008,6 +10374,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
|
||||
@@ -12362,7 +12737,6 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
@@ -14260,6 +14634,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-network-error": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz",
|
||||
"integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-node-process": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
|
||||
@@ -14615,6 +15001,15 @@
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tiktoken": {
|
||||
"version": "1.0.21",
|
||||
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
|
||||
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -14662,6 +15057,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-to-ts": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
||||
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"ts-algebra": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@@ -14782,6 +15190,52 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.6.tgz",
|
||||
"integrity": "sha512-T/RA2l2MsTYX0z1aW8rQ2hBQZEOuXV2v/6tkfG6R5EotJTKMpw1dERCbvP8ezOP8otyWfnNlQA88ZnMRsQ7CHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/uuid": "^10.0.0",
|
||||
"chalk": "^5.6.2",
|
||||
"console-table-printer": "^2.12.1",
|
||||
"p-queue": "^6.6.2",
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "*",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "*",
|
||||
"@opentelemetry/sdk-trace-base": "*",
|
||||
"openai": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/exporter-trace-otlp-proto": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/sdk-trace-base": {
|
||||
"optional": true
|
||||
},
|
||||
"openai": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/langsmith/node_modules/chalk": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
|
||||
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
@@ -16646,6 +17100,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
||||
@@ -17071,6 +17534,27 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "6.22.0",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.22.0.tgz",
|
||||
"integrity": "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.25 || ^4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -17205,7 +17689,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -17269,6 +17752,55 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
|
||||
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.4",
|
||||
"p-timeout": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-queue/node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/p-retry": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
|
||||
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-network-error": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
|
||||
@@ -20075,6 +20607,12 @@
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-wcswidth": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
|
||||
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sisteransi": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||
@@ -21000,6 +21538,12 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-algebra": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
||||
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-morph": {
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz",
|
||||
@@ -22052,6 +22596,19 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@@ -22170,6 +22727,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
|
||||
"integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.28",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz",
|
||||
@@ -22664,7 +23230,7 @@
|
||||
"version": "3.25.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
|
||||
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
|
||||
@@ -46,7 +46,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/geist": "^5.2.8",
|
||||
"@github/copilot-sdk": "^0.1.25",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@langchain/anthropic": "^1.3.19",
|
||||
"@langchain/core": "^1.1.27",
|
||||
"@langchain/langgraph": "^1.1.5",
|
||||
"@langchain/openai": "^1.2.9",
|
||||
"@milkdown/crepe": "^7.18.0",
|
||||
"@milkdown/kit": "^7.18.0",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
|
||||
4
prd.json
4
prd.json
@@ -350,8 +350,8 @@
|
||||
"Typecheck passes"
|
||||
],
|
||||
"priority": 19,
|
||||
"passes": false,
|
||||
"notes": ""
|
||||
"passes": true,
|
||||
"notes": "Completed: LangGraph-based provider-independent orchestrator. StateGraph with classifyIntent node (structured output via withStructuredOutput(zodSchema) for route classification) + 3 specialist agent nodes (projectAgent, knowledgeAgent, generalAgent) connected via addConditionalEdges. LLM factory (src/main/ai/llm.ts) returns BaseChatModel per provider: ChatOpenAI (openai), ChatAnthropic (anthropic), ChatCopilot adapter (copilot). ChatCopilot (src/main/ai/chat-copilot.ts) wraps @github/copilot-sdk in SimpleChatModel with _call() and _streamResponseChunks(). Streaming via LangGraph streamMode:'messages' + IPC side-channel (ai:stream). Context assembly from DB (buildProjectContext/buildGlobalContext), tRPC context with event.sender, auth/timeout error handling. Typecheck passes."
|
||||
},
|
||||
{
|
||||
"id": "US-020",
|
||||
|
||||
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`
|
||||
---
|
||||
|
||||
134
src/main/ai/chat-copilot.ts
Normal file
134
src/main/ai/chat-copilot.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* ChatCopilot — LangChain-compatible ChatModel adapter for the GitHub Copilot SDK.
|
||||
*
|
||||
* Wraps the CopilotClient's session API so it can be used as a drop-in
|
||||
* BaseChatModel within LangGraph, making the orchestrator provider-agnostic.
|
||||
*/
|
||||
import { SimpleChatModel, type BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import { AIMessageChunk } from '@langchain/core/messages';
|
||||
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
||||
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
||||
import { getCopilotClient } from './copilot';
|
||||
|
||||
const COPILOT_TIMEOUT = 60_000;
|
||||
|
||||
export class ChatCopilot extends SimpleChatModel<BaseChatModelCallOptions> {
|
||||
constructor() {
|
||||
super({});
|
||||
}
|
||||
|
||||
_llmType(): string {
|
||||
return 'copilot';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async _call(messages: BaseMessage[], _options: this['ParsedCallOptions'], _runManager?: CallbackManagerForLLMRun): Promise<string> {
|
||||
const client = getCopilotClient();
|
||||
if (!client) {
|
||||
throw new Error('CopilotClient not initialized. Please add your GitHub token in Settings.');
|
||||
}
|
||||
|
||||
// Extract system message and user prompt from LangChain messages
|
||||
const systemContent = messages
|
||||
.filter((m) => m._getType() === 'system')
|
||||
.map((m) => (typeof m.content === 'string' ? m.content : ''))
|
||||
.join('\n');
|
||||
|
||||
const userContent = messages
|
||||
.filter((m) => m._getType() === 'human')
|
||||
.map((m) => (typeof m.content === 'string' ? m.content : ''))
|
||||
.join('\n');
|
||||
|
||||
const session = await client.createSession({
|
||||
systemMessage: systemContent
|
||||
? { mode: 'replace', content: systemContent }
|
||||
: undefined,
|
||||
availableTools: [],
|
||||
streaming: true,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await session.sendAndWait({ prompt: userContent }, COPILOT_TIMEOUT);
|
||||
return result?.data.content ?? '';
|
||||
} finally {
|
||||
await session.destroy().catch(() => { /* ignore cleanup errors */ });
|
||||
}
|
||||
}
|
||||
|
||||
async *_streamResponseChunks(
|
||||
messages: BaseMessage[],
|
||||
_options: this['ParsedCallOptions'],
|
||||
_runManager?: CallbackManagerForLLMRun,
|
||||
): AsyncGenerator<ChatGenerationChunk> {
|
||||
const client = getCopilotClient();
|
||||
if (!client) {
|
||||
throw new Error('CopilotClient not initialized. Please add your GitHub token in Settings.');
|
||||
}
|
||||
|
||||
const systemContent = messages
|
||||
.filter((m) => m._getType() === 'system')
|
||||
.map((m) => (typeof m.content === 'string' ? m.content : ''))
|
||||
.join('\n');
|
||||
|
||||
const userContent = messages
|
||||
.filter((m) => m._getType() === 'human')
|
||||
.map((m) => (typeof m.content === 'string' ? m.content : ''))
|
||||
.join('\n');
|
||||
|
||||
const session = await client.createSession({
|
||||
systemMessage: systemContent
|
||||
? { mode: 'replace', content: systemContent }
|
||||
: undefined,
|
||||
availableTools: [],
|
||||
streaming: true,
|
||||
});
|
||||
|
||||
// Buffer chunks via event listener and yield them
|
||||
const chunks: string[] = [];
|
||||
let done = false;
|
||||
let resolveNext: (() => void) | null = null;
|
||||
|
||||
const unsubDelta = session.on('assistant.message_delta', (event) => {
|
||||
const delta = event.data.deltaContent;
|
||||
if (delta) {
|
||||
chunks.push(delta);
|
||||
resolveNext?.();
|
||||
}
|
||||
});
|
||||
|
||||
const unsubEnd = session.on('session.idle', () => {
|
||||
done = true;
|
||||
resolveNext?.();
|
||||
});
|
||||
|
||||
// Fire the request (don't await — we'll drain via events)
|
||||
const sendPromise = session.sendAndWait({ prompt: userContent }, COPILOT_TIMEOUT);
|
||||
|
||||
try {
|
||||
while (!done || chunks.length > 0) {
|
||||
if (chunks.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const text = chunks.shift()!;
|
||||
const chunk = new ChatGenerationChunk({
|
||||
message: new AIMessageChunk({ content: text }),
|
||||
text,
|
||||
});
|
||||
await _runManager?.handleLLMNewToken(text);
|
||||
yield chunk;
|
||||
} else if (!done) {
|
||||
await new Promise<void>((resolve) => {
|
||||
resolveNext = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the send completes
|
||||
await sendPromise;
|
||||
} finally {
|
||||
unsubDelta();
|
||||
unsubEnd();
|
||||
await session.destroy().catch(() => { /* ignore cleanup errors */ });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { app } from 'electron';
|
||||
import { registerProvider, type AIProvider } from './provider';
|
||||
|
||||
// Dynamic import type — @github/copilot-sdk is ESM-only
|
||||
type CopilotClientType = import('@github/copilot-sdk').CopilotClient;
|
||||
|
||||
let client: CopilotClientType | null = null;
|
||||
let token: string | null = null;
|
||||
|
||||
const copilotProvider: AIProvider = {
|
||||
@@ -8,13 +13,33 @@ const copilotProvider: AIProvider = {
|
||||
|
||||
async initialize(t: string): Promise<boolean> {
|
||||
token = t;
|
||||
// Actual GitHub Copilot SDK client creation will be added in US-019.
|
||||
// For now, having a token means the provider is ready.
|
||||
return true;
|
||||
try {
|
||||
// Stop existing client if re-initializing
|
||||
if (client) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
await client.stop().catch(() => {});
|
||||
client = null;
|
||||
}
|
||||
|
||||
const { CopilotClient } = await import('@github/copilot-sdk');
|
||||
client = new CopilotClient({
|
||||
githubToken: t,
|
||||
autoStart: true,
|
||||
autoRestart: true,
|
||||
logLevel: 'warning',
|
||||
});
|
||||
await client.start();
|
||||
console.log('[AI] CopilotClient started successfully');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[AI] Failed to start CopilotClient:', err);
|
||||
client = null;
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
isReady(): boolean {
|
||||
return token !== null;
|
||||
return client !== null && token !== null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,4 +48,16 @@ export function getCopilotToken(): string | null {
|
||||
return token;
|
||||
}
|
||||
|
||||
/** Get the CopilotClient instance (null if not initialized). */
|
||||
export function getCopilotClient(): CopilotClientType | null {
|
||||
return client;
|
||||
}
|
||||
|
||||
// Clean shutdown on app quit
|
||||
app.on('before-quit', () => {
|
||||
if (client) {
|
||||
client.stop().catch((err: unknown) => console.error('[AI] Error stopping CopilotClient:', err));
|
||||
}
|
||||
});
|
||||
|
||||
registerProvider(copilotProvider);
|
||||
|
||||
77
src/main/ai/llm.ts
Normal file
77
src/main/ai/llm.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* LLM connector factory — returns a LangChain BaseChatModel for the active provider.
|
||||
*
|
||||
* The agent orchestration (LangGraph) is provider-independent. This module is
|
||||
* the only place that knows how to create provider-specific LLM instances.
|
||||
*/
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { getActiveProviderName } from './provider';
|
||||
import { getToken } from './token';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider-specific factory functions (lazy-loaded)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function createOpenAIModel(token: string): Promise<BaseChatModel> {
|
||||
const { ChatOpenAI } = await import('@langchain/openai');
|
||||
return new ChatOpenAI({
|
||||
apiKey: token,
|
||||
model: 'gpt-4o-mini',
|
||||
temperature: 0.3,
|
||||
streaming: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function createAnthropicModel(token: string): Promise<BaseChatModel> {
|
||||
const { ChatAnthropic } = await import('@langchain/anthropic');
|
||||
return new ChatAnthropic({
|
||||
apiKey: token,
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
temperature: 0.3,
|
||||
streaming: true,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function createCopilotModel(_token: string): Promise<BaseChatModel> {
|
||||
// GitHub Copilot uses the Copilot SDK subprocess for auth and API access.
|
||||
// We wrap it in a LangChain-compatible adapter.
|
||||
const { ChatCopilot } = await import('./chat-copilot');
|
||||
return new ChatCopilot();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MODEL_FACTORIES: Record<string, (token: string) => Promise<BaseChatModel>> = {
|
||||
openai: createOpenAIModel,
|
||||
anthropic: createAnthropicModel,
|
||||
copilot: createCopilotModel,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a LangChain BaseChatModel for the currently active AI provider.
|
||||
* Returns null if no provider is configured or no token is available.
|
||||
*/
|
||||
export async function getLLM(): Promise<BaseChatModel | null> {
|
||||
const providerName = getActiveProviderName();
|
||||
const factory = MODEL_FACTORIES[providerName];
|
||||
if (!factory) {
|
||||
console.log(`[AI] No LLM factory for provider "${providerName}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = await getToken(providerName);
|
||||
if (!token) {
|
||||
console.log(`[AI] No token available for provider "${providerName}"`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await factory(token);
|
||||
} catch (err) {
|
||||
console.error(`[AI] Failed to create LLM for "${providerName}":`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
401
src/main/ai/orchestrator.ts
Normal file
401
src/main/ai/orchestrator.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* @Orchestrator agent — LangGraph-based intent routing.
|
||||
*
|
||||
* The agent logic (routing, state) lives here and is fully LLM-agnostic.
|
||||
* The LLM is a swappable connector obtained via `getLLM()`.
|
||||
*/
|
||||
import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
|
||||
import { SystemMessage, HumanMessage, type BaseMessage } from '@langchain/core/messages';
|
||||
import { z } from 'zod';
|
||||
import { eq, asc } from 'drizzle-orm';
|
||||
import { getDb } from '../db';
|
||||
import { projects, tasks, checkpoints, notes, clients } from '../db/schema';
|
||||
import { getLLM } from './llm';
|
||||
|
||||
const AI_STREAM_CHANNEL = 'ai:stream';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface OrchestrateInput {
|
||||
message: string;
|
||||
context: { type: 'global' | 'project'; projectId?: string };
|
||||
sender?: Electron.WebContents;
|
||||
}
|
||||
|
||||
export interface OrchestrateResult {
|
||||
response: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context assembly (DB queries — provider-independent)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildProjectContext(projectId: string): string {
|
||||
const db = getDb();
|
||||
|
||||
const project = db.select().from(projects).where(eq(projects.id, projectId)).all()[0];
|
||||
if (!project) return 'Project not found.';
|
||||
|
||||
let clientName = '';
|
||||
if (project.clientId) {
|
||||
const client = db.select().from(clients).where(eq(clients.id, project.clientId)).all()[0];
|
||||
if (client) clientName = client.name;
|
||||
}
|
||||
|
||||
const projectTasks = db
|
||||
.select({ title: tasks.title, status: tasks.status, priority: tasks.priority, dueDate: tasks.dueDate })
|
||||
.from(tasks)
|
||||
.where(eq(tasks.projectId, projectId))
|
||||
.orderBy(asc(tasks.createdAt))
|
||||
.all();
|
||||
|
||||
const projectCheckpoints = db
|
||||
.select({ title: checkpoints.title, date: checkpoints.date, isApproved: checkpoints.isApproved })
|
||||
.from(checkpoints)
|
||||
.where(eq(checkpoints.projectId, projectId))
|
||||
.orderBy(asc(checkpoints.date))
|
||||
.all();
|
||||
|
||||
const projectNotes = db
|
||||
.select({ title: notes.title, content: notes.content })
|
||||
.from(notes)
|
||||
.where(eq(notes.projectId, projectId))
|
||||
.orderBy(asc(notes.createdAt))
|
||||
.all();
|
||||
|
||||
const lines: string[] = [
|
||||
`## Project: ${project.name}`,
|
||||
clientName ? `Client: ${clientName}` : '',
|
||||
`Status: ${project.status ?? 'active'}`,
|
||||
project.aiSummary ? `AI Summary: ${project.aiSummary}` : '',
|
||||
'',
|
||||
`### Tasks (${projectTasks.length})`,
|
||||
...projectTasks.map((t) => {
|
||||
const due = t.dueDate ? new Date(t.dueDate).toLocaleDateString() : 'no due date';
|
||||
return `- [${t.status}] ${t.title} (${t.priority}, ${due})`;
|
||||
}),
|
||||
'',
|
||||
`### Checkpoints (${projectCheckpoints.length})`,
|
||||
...projectCheckpoints.map((c) => {
|
||||
const approved = c.isApproved ? 'approved' : 'pending';
|
||||
return `- ${c.title} — ${new Date(c.date).toLocaleDateString()} (${approved})`;
|
||||
}),
|
||||
'',
|
||||
`### Notes (${projectNotes.length})`,
|
||||
...projectNotes.map((n) => {
|
||||
const excerpt = n.content.length > 500 ? n.content.slice(0, 500) + '…' : n.content;
|
||||
return `#### ${n.title}\n${excerpt}`;
|
||||
}),
|
||||
];
|
||||
|
||||
return lines.filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
function buildGlobalContext(): string {
|
||||
const db = getDb();
|
||||
|
||||
const allProjects = db
|
||||
.select({ id: projects.id, name: projects.name, status: projects.status })
|
||||
.from(projects)
|
||||
.where(eq(projects.status, 'active'))
|
||||
.orderBy(asc(projects.name))
|
||||
.all();
|
||||
|
||||
const allTasks = db.select().from(tasks).all();
|
||||
const todoCount = allTasks.filter((t) => t.status === 'todo').length;
|
||||
const inProgressCount = allTasks.filter((t) => t.status === 'in_progress').length;
|
||||
const doneCount = allTasks.filter((t) => t.status === 'done').length;
|
||||
|
||||
const now = Date.now();
|
||||
const weekFromNow = now + 7 * 24 * 60 * 60 * 1000;
|
||||
const upcomingTasks = allTasks
|
||||
.filter((t) => t.dueDate && t.dueDate >= now && t.dueDate <= weekFromNow && t.status !== 'done')
|
||||
.sort((a, b) => (a.dueDate ?? 0) - (b.dueDate ?? 0));
|
||||
|
||||
const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
|
||||
|
||||
const lines: string[] = [
|
||||
`## Workspace Overview`,
|
||||
`Active projects: ${allProjects.length}`,
|
||||
`Tasks: ${allTasks.length} total (${todoCount} todo, ${inProgressCount} in progress, ${doneCount} done)`,
|
||||
'',
|
||||
`### Active Projects`,
|
||||
...allProjects.map((p) => `- ${p.name}`),
|
||||
'',
|
||||
`### Tasks Due This Week (${upcomingTasks.length})`,
|
||||
...upcomingTasks.map((t) => {
|
||||
const projectName = t.projectId ? (projectMap.get(t.projectId) ?? 'Unknown') : 'No project';
|
||||
const due = t.dueDate ? new Date(t.dueDate).toLocaleDateString() : '';
|
||||
return `- ${t.title} [${projectName}] — due ${due}`;
|
||||
}),
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System prompts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeProjectAgentPrompt(contextData: string): string {
|
||||
return `You are @ProjectAgent, an AI assistant specialized in a specific project within Adiuva.
|
||||
|
||||
You have access to the following project data:
|
||||
|
||||
${contextData}
|
||||
|
||||
Answer the user's question based on this project context. Be concise and helpful.
|
||||
When referencing tasks, notes, or checkpoints, mention them by name.
|
||||
If you don't have enough information, say so.`;
|
||||
}
|
||||
|
||||
function makeGeneralAgentPrompt(contextData: string): string {
|
||||
return `You are @GeneralAgent, an AI assistant for the Adiuva workspace.
|
||||
|
||||
You have access to the following workspace data:
|
||||
|
||||
${contextData}
|
||||
|
||||
Help the user with their question based on this workspace context. Provide concise, actionable answers.
|
||||
When discussing tasks or projects, reference them by name.`;
|
||||
}
|
||||
|
||||
function makeKnowledgeAgentPrompt(contextData: string): string {
|
||||
return `You are @KnowledgeAgent, an AI assistant that searches across all project knowledge in Adiuva.
|
||||
|
||||
You have access to the following workspace data:
|
||||
|
||||
${contextData}
|
||||
|
||||
Note: Semantic vector search is not yet available. Answer based on the workspace summary data above.
|
||||
If the user asks about specific note contents that aren't included here, let them know that full cross-project search will be available soon.`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LangGraph State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const RouteSchema = z.object({
|
||||
route: z.enum(['project', 'knowledge', 'general']).describe(
|
||||
'Which specialist agent should handle this request',
|
||||
),
|
||||
});
|
||||
|
||||
const OrchestratorState = Annotation.Root({
|
||||
/** The user's original message */
|
||||
userMessage: Annotation<string>(),
|
||||
/** Chat context (global vs project-scoped) */
|
||||
chatContext: Annotation<{ type: 'global' | 'project'; projectId?: string }>(),
|
||||
/** The route chosen by the orchestrator */
|
||||
route: Annotation<'project' | 'knowledge' | 'general'>(),
|
||||
/** Messages for the specialist agent */
|
||||
messages: Annotation<BaseMessage[]>({
|
||||
reducer: (existing, incoming) =>
|
||||
Array.isArray(incoming) ? existing.concat(incoming) : existing.concat([incoming]),
|
||||
default: () => [],
|
||||
}),
|
||||
/** The final response text */
|
||||
response: Annotation<string>(),
|
||||
});
|
||||
|
||||
type State = typeof OrchestratorState.State;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Graph nodes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Node 1: Classify intent using structured output */
|
||||
async function classifyIntent(state: State): Promise<Partial<State>> {
|
||||
const llm = await getLLM();
|
||||
if (!llm) throw new Error('AI provider not configured. Please add your token in Settings.');
|
||||
|
||||
const routerLLM = llm.withStructuredOutput(RouteSchema);
|
||||
|
||||
const result = await routerLLM.invoke([
|
||||
new SystemMessage(
|
||||
`You are a routing classifier for Adiuva, a project management workspace.
|
||||
Classify the user's message into one of these categories:
|
||||
- "project": Question about a specific project (tasks, notes, checkpoints, progress, summaries)
|
||||
- "knowledge": Cross-project or historical question (e.g., "what did we decide about X?", "find notes about Y")
|
||||
- "general": Everything else (general help, scheduling, task overviews, workspace summaries)`,
|
||||
),
|
||||
new HumanMessage(state.userMessage),
|
||||
]);
|
||||
|
||||
return { route: result.route };
|
||||
}
|
||||
|
||||
/** Node 2a: Project agent — answer project-scoped questions */
|
||||
async function projectAgent(state: State): Promise<Partial<State>> {
|
||||
const llm = await getLLM();
|
||||
if (!llm) throw new Error('AI provider not configured.');
|
||||
|
||||
const projectId = state.chatContext.projectId;
|
||||
const contextData = projectId ? buildProjectContext(projectId) : buildGlobalContext();
|
||||
const systemPrompt = projectId
|
||||
? makeProjectAgentPrompt(contextData)
|
||||
: makeGeneralAgentPrompt(contextData);
|
||||
|
||||
const response = await llm.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(state.userMessage),
|
||||
]);
|
||||
|
||||
const content = typeof response.content === 'string' ? response.content : '';
|
||||
return {
|
||||
messages: [response],
|
||||
response: content,
|
||||
};
|
||||
}
|
||||
|
||||
/** Node 2b: Knowledge agent — cross-project search */
|
||||
async function knowledgeAgent(state: State): Promise<Partial<State>> {
|
||||
const llm = await getLLM();
|
||||
if (!llm) throw new Error('AI provider not configured.');
|
||||
|
||||
const contextData = buildGlobalContext();
|
||||
const systemPrompt = makeKnowledgeAgentPrompt(contextData);
|
||||
|
||||
const response = await llm.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(state.userMessage),
|
||||
]);
|
||||
|
||||
const content = typeof response.content === 'string' ? response.content : '';
|
||||
return {
|
||||
messages: [response],
|
||||
response: content,
|
||||
};
|
||||
}
|
||||
|
||||
/** Node 2c: General agent — workspace-wide questions */
|
||||
async function generalAgent(state: State): Promise<Partial<State>> {
|
||||
const llm = await getLLM();
|
||||
if (!llm) throw new Error('AI provider not configured.');
|
||||
|
||||
const contextData = buildGlobalContext();
|
||||
const systemPrompt = makeGeneralAgentPrompt(contextData);
|
||||
|
||||
const response = await llm.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(state.userMessage),
|
||||
]);
|
||||
|
||||
const content = typeof response.content === 'string' ? response.content : '';
|
||||
return {
|
||||
messages: [response],
|
||||
response: content,
|
||||
};
|
||||
}
|
||||
|
||||
/** Routing function: reads state.route and returns the next node name */
|
||||
function routeDecision(state: State): string {
|
||||
switch (state.route) {
|
||||
case 'project': return 'projectAgent';
|
||||
case 'knowledge': return 'knowledgeAgent';
|
||||
case 'general': return 'generalAgent';
|
||||
default: return 'generalAgent';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compile the graph (singleton, reused across calls)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildGraph() {
|
||||
return new StateGraph(OrchestratorState)
|
||||
.addNode('classifyIntent', classifyIntent)
|
||||
.addNode('projectAgent', projectAgent)
|
||||
.addNode('knowledgeAgent', knowledgeAgent)
|
||||
.addNode('generalAgent', generalAgent)
|
||||
.addEdge(START, 'classifyIntent')
|
||||
.addConditionalEdges('classifyIntent', routeDecision, [
|
||||
'projectAgent', 'knowledgeAgent', 'generalAgent',
|
||||
])
|
||||
.addEdge('projectAgent', END)
|
||||
.addEdge('knowledgeAgent', END)
|
||||
.addEdge('generalAgent', END)
|
||||
.compile();
|
||||
}
|
||||
|
||||
let compiledGraph: ReturnType<typeof buildGraph> | null = null;
|
||||
|
||||
function getGraph() {
|
||||
if (!compiledGraph) {
|
||||
compiledGraph = buildGraph();
|
||||
}
|
||||
return compiledGraph;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Streaming helper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sendStreamChunk(sender: Electron.WebContents | undefined, token: string, done: boolean): void {
|
||||
if (!sender || sender.isDestroyed()) return;
|
||||
sender.send(AI_STREAM_CHANNEL, { token, done });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Orchestrate (public entry point)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function orchestrate(input: OrchestrateInput): Promise<OrchestrateResult> {
|
||||
const { message, context, sender } = input;
|
||||
|
||||
// Quick check: is an LLM available?
|
||||
const llm = await getLLM();
|
||||
if (!llm) {
|
||||
return { response: '', error: 'AI provider not configured. Please add your token in Settings.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const graph = getGraph();
|
||||
|
||||
// Use streaming to push tokens to the renderer in real-time
|
||||
const stream = await graph.stream(
|
||||
{
|
||||
userMessage: message,
|
||||
chatContext: context,
|
||||
route: 'general' as const,
|
||||
response: '',
|
||||
},
|
||||
{ streamMode: 'messages' as const },
|
||||
);
|
||||
|
||||
let fullResponse = '';
|
||||
|
||||
for await (const [chunk, metadata] of stream) {
|
||||
// Only stream tokens from the specialist agent nodes (not the classifier)
|
||||
if (
|
||||
metadata.langgraph_node !== 'classifyIntent' &&
|
||||
chunk.content &&
|
||||
typeof chunk.content === 'string'
|
||||
) {
|
||||
fullResponse += chunk.content;
|
||||
sendStreamChunk(sender, chunk.content, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Signal stream completion
|
||||
sendStreamChunk(sender, '', true);
|
||||
|
||||
return { response: fullResponse };
|
||||
} catch (err) {
|
||||
sendStreamChunk(sender, '', true);
|
||||
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
|
||||
if (errMsg.includes('401') || errMsg.includes('403') || errMsg.includes('auth') || errMsg.includes('Unauthorized')) {
|
||||
return { response: '', error: 'Authentication failed. Please check your token in Settings.' };
|
||||
}
|
||||
if (errMsg.includes('timeout') || errMsg.includes('Timeout')) {
|
||||
return { response: '', error: 'Request timed out. Please try again.' };
|
||||
}
|
||||
|
||||
return { response: '', error: errMsg };
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,12 @@ import {
|
||||
|
||||
export const IPC_CHANNEL = 'trpc';
|
||||
|
||||
/** Context passed to every tRPC procedure via the IPC bridge. */
|
||||
export type TRPCContext = {
|
||||
/** The IPC sender — available for streaming chunks back to the renderer. */
|
||||
sender?: Electron.WebContents;
|
||||
};
|
||||
|
||||
interface IPCRequest {
|
||||
method: 'request';
|
||||
operation: {
|
||||
@@ -57,7 +63,7 @@ export function createIPCHandler<TRouter extends AnyRouter>({
|
||||
router,
|
||||
path,
|
||||
getRawInput: async () => rawInput,
|
||||
ctx: {},
|
||||
ctx: { sender: event.sender } satisfies TRPCContext,
|
||||
type,
|
||||
signal: undefined as unknown as AbortSignal,
|
||||
batchIndex: 0,
|
||||
|
||||
@@ -6,8 +6,10 @@ import { getDb } from '../db';
|
||||
import { clients, projects, tasks, checkpoints, notes, taskComments } from '../db/schema';
|
||||
import { getStore } from '../store';
|
||||
import { saveTokenAndInit, hasActiveToken } from '../ai/provider';
|
||||
import { orchestrate } from '../ai/orchestrator';
|
||||
import type { TRPCContext } from '../ipc';
|
||||
|
||||
const t = initTRPC.create();
|
||||
const t = initTRPC.context<TRPCContext>().create();
|
||||
|
||||
const router = t.router;
|
||||
const publicProcedure = t.procedure;
|
||||
@@ -492,7 +494,18 @@ const aiRouter = router({
|
||||
projectId: z.string().optional(),
|
||||
}),
|
||||
}))
|
||||
.mutation(() => ({ response: '' })),
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await orchestrate({
|
||||
message: input.message,
|
||||
context: input.context,
|
||||
sender: ctx.sender,
|
||||
});
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Unknown error';
|
||||
return { response: '', error: msg };
|
||||
}
|
||||
}),
|
||||
setToken: publicProcedure
|
||||
.input(z.object({ token: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
@@ -18,3 +18,16 @@ contextBridge.exposeInMainWorld('electronTRPC', {
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const AI_STREAM_CHANNEL = 'ai:stream';
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAI', {
|
||||
/** Subscribe to AI streaming chunks. Returns an unsubscribe function. */
|
||||
onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: { token: string; done: boolean }) => cb(data);
|
||||
ipcRenderer.on(AI_STREAM_CHANNEL, handler);
|
||||
return () => {
|
||||
ipcRenderer.removeListener(AI_STREAM_CHANNEL, handler);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,9 +13,14 @@ interface ElectronTRPC {
|
||||
onMessage: (cb: (data: unknown) => void) => (() => void) | void;
|
||||
}
|
||||
|
||||
interface ElectronAI {
|
||||
onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => () => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronTRPC: ElectronTRPC;
|
||||
electronAI: ElectronAI;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,19 @@ export default defineConfig({
|
||||
build: {
|
||||
rollupOptions: {
|
||||
// Externalize native Node modules — they're rebuilt by electron-forge
|
||||
external: ['better-sqlite3', 'keytar'],
|
||||
external: [
|
||||
'better-sqlite3',
|
||||
'keytar',
|
||||
'@github/copilot-sdk',
|
||||
'@github/copilot',
|
||||
// LangChain — externalize to avoid bundling Node.js-specific code
|
||||
'@langchain/core',
|
||||
'@langchain/langgraph',
|
||||
'@langchain/openai',
|
||||
'@langchain/anthropic',
|
||||
'@langchain/langgraph-checkpoint',
|
||||
'@langchain/langgraph-sdk',
|
||||
],
|
||||
output: {
|
||||
entryFileNames: 'main.js',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user