feat: migrate to Vercel AI SDK with xAI provider

- Replace custom OpenAI client with AI SDK streamText + @ai-sdk/xai
- Add standalone search_web and search_x tools via chat completions
- Fix follow-up messages by using isProcessingRef to avoid stale closures
- Fallback message history when result.response fails after stream errors
- Remove post-response status line, adjust input padding

Made-with: Cursor
This commit is contained in:
Ismail Pelaseyed committed 2026-03-18 19:58:09 +01:00
1 parent 60035f2240
commit 87bddb4cb3
7 files changed
+293 -357

No files matched your search

+58 -4
View File
@@ -4,12 +4,14 @@
"": {
"name": "@vibe-kit/grok-cli",
"dependencies": {
"@ai-sdk/xai": "^3.0.67",
"@opentui/core": "^0.1.88",
"@opentui/react": "^0.1.88",
"ai": "^6.0.116",
"commander": "^12.0.0",
"dotenv": "^16.4.0",
"openai": "^5.10.1",
"react": "^19.1.0",
"zod": "^4.3.6",
},
"devDependencies": {
"@types/node": "^22.0.0",
@@ -22,6 +24,16 @@
},
},
"packages": {
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="],
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.67", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KQQIDc91dUA5IGFMnXBuvPBeraYNTdpDC1qUS+JG8vE+/299//5sZFafI1kKYUu3f3p7LaZrKXYgZ1Ni7QIRbw=="],
"@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
@@ -112,6 +124,8 @@
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.88", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.88", "@opentui/core-darwin-x64": "0.1.88", "@opentui/core-linux-arm64": "0.1.88", "@opentui/core-linux-x64": "0.1.88", "@opentui/core-win32-arm64": "0.1.88", "@opentui/core-win32-x64": "0.1.88", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-eaDVZfAzZraddOIkgWSHMVkyaY0O20foYnPWKPQx1TY4t7G1oatIoan2zkytx67epW+4BZQ9vGib+61/uNM1MA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.88", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oGRexWwZFeQJymOK5ORrLrwJUbPHMYaFa0EcLnlhvPnymm1xyMcRKm39ez0WSIdtiCCi/PmMHX95CfyyJB5VMA=="],
@@ -128,6 +142,8 @@
"@opentui/react": ["@opentui/react@0.1.88", "", { "dependencies": { "@opentui/core": "0.1.88", "react-reconciler": "^0.32.0" }, "peerDependencies": { "react": ">=19.0.0", "react-devtools-core": "^7.0.1", "ws": "^8.18.0" } }, "sha512-EfEUBlGgCx9LqpbB852L+m7F19NcR8yeUww+C8NytIQeVKJbFIhSLu3r3wjBEGNoYiRk1DtAwWBpG5YaVvkXXw=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
@@ -158,6 +174,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.43.0", "", { "dependencies": { "@typescript-eslint/types": "8.43.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw=="],
"@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
"@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
@@ -166,6 +184,8 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -246,6 +266,8 @@
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
"exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -306,6 +328,8 @@
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
@@ -334,8 +358,6 @@
"omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="],
"openai": ["openai@5.20.1", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "bin": "bin/cli" }, "sha512-UndCB0R5V3iB9I98NyF69zNP6YfwU4+Fjk0eW4HhooTm+Awlpm/MGjJTwJsyNV/qkH1NJi0GG+9odwukGTqExQ=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -454,12 +476,44 @@
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-color/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-contain/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-cover/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-crop/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-displace/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-fisheye/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-flip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-mask/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-print/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-quantize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-resize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-rotate/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/plugin-threshold/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+4 -2
View File
@@ -32,12 +32,14 @@
"author": "Vibe Kit",
"license": "MIT",
"dependencies": {
"@ai-sdk/xai": "^3.0.67",
"@opentui/core": "^0.1.88",
"@opentui/react": "^0.1.88",
"ai": "^6.0.116",
"commander": "^12.0.0",
"dotenv": "^16.4.0",
"openai": "^5.10.1",
"react": "^19.1.0"
"react": "^19.1.0",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^22.0.0",
+106 -111
View File
@@ -1,8 +1,9 @@
import { GrokClient } from "../grok/client.js";
import { TOOLS } from "../grok/tools.js";
import { streamText, stepCountIs, type ModelMessage } from "ai";
import { createProvider, generateTitle as genTitle, type XaiProvider } from "../grok/client.js";
import { createTools } from "../grok/tools.js";
import { BashTool } from "../tools/bash.js";
import { loadCustomInstructions } from "../utils/instructions.js";
import type { AgentMode, Message, StreamChunk, ToolCall, ToolResult } from "../types/index.js";
import type { AgentMode, StreamChunk, ToolCall, ToolResult } from "../types/index.js";
const MAX_TOOL_ROUNDS = 400;
@@ -11,8 +12,8 @@ const MODE_PROMPTS: Record<AgentMode, string> = {
TOOLS:
- bash: Execute any shell command — viewing files, editing (sed/tee/heredocs), searching (grep/rg/find), git, builds, packages.
- search_web: Real-time web search for documentation, APIs, current events.
- search_x: Search X (Twitter) for posts, discussions, trends.
- search_web: Search the web for current information, documentation, APIs, tutorials, etc.
- search_x: Search X/Twitter for real-time posts, discussions, opinions, and trends.
WORKFLOW:
1. Understand the request
@@ -20,6 +21,7 @@ WORKFLOW:
3. Make changes using bash commands
4. Verify changes by reading modified files
5. Run tests or builds to confirm correctness
6. Use search_web or search_x when you need up-to-date information
Be direct. Execute, don't just describe. Show results, not plans.`,
@@ -27,8 +29,6 @@ Be direct. Execute, don't just describe. Show results, not plans.`,
TOOLS:
- bash: ONLY use for reading files (cat, head, tail, find, ls, grep) — NEVER modify files.
- search_web: Research documentation and current information.
- search_x: Research discussions and community sentiment.
BEHAVIOR:
- Create a detailed, step-by-step implementation plan
@@ -43,8 +43,6 @@ Format your plan with clear numbered steps and file paths.`,
TOOLS:
- bash: ONLY for reading files to provide context (cat, find, ls, grep) — NEVER modify.
- search_web: Look up documentation and current information.
- search_x: Research discussions and trends.
BEHAVIOR:
- Answer the user's question directly and thoroughly
@@ -66,29 +64,30 @@ Current working directory: ${cwd}`;
}
export class Agent {
private client: GrokClient;
private provider: XaiProvider;
private bash: BashTool;
private messages: Message[] = [];
private messages: ModelMessage[] = [];
private abortController: AbortController | null = null;
private maxToolRounds: number;
private mode: AgentMode = "agent";
private modelId: string;
private maxTokens: number;
constructor(apiKey: string, baseURL?: string, model?: string, maxToolRounds?: number) {
this.client = new GrokClient(apiKey, model, baseURL);
this.provider = createProvider(apiKey, baseURL);
this.bash = new BashTool();
this.modelId = model || "grok-4-1-fast";
this.maxToolRounds = maxToolRounds || MAX_TOOL_ROUNDS;
this.messages.push({
role: "system",
content: buildSystemPrompt(process.cwd(), this.mode),
});
const envMax = Number(process.env.GROK_MAX_TOKENS);
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
}
getModel(): string {
return this.client.getModel();
return this.modelId;
}
setModel(model: string): void {
this.client.setModel(model);
this.modelId = model;
}
getMode(): AgentMode {
@@ -97,18 +96,14 @@ export class Agent {
setMode(mode: AgentMode): void {
this.mode = mode;
this.messages[0] = {
role: "system",
content: buildSystemPrompt(this.bash.getCwd(), mode),
};
}
getCwd(): string {
return this.bash.getCwd();
}
generateTitle(userMessage: string): Promise<string> {
return this.client.generateTitle(userMessage);
async generateTitle(userMessage: string): Promise<string> {
return genTitle(this.provider, userMessage);
}
abort(): void {
@@ -116,10 +111,7 @@ export class Agent {
}
clearHistory(): void {
this.messages = [{
role: "system",
content: buildSystemPrompt(this.bash.getCwd(), this.mode),
}];
this.messages = [];
}
async *processMessage(userMessage: string): AsyncGenerator<StreamChunk, void, unknown> {
@@ -127,110 +119,113 @@ export class Agent {
this.messages.push({ role: "user", content: userMessage });
let toolRounds = 0;
let assistantText = "";
let streamOk = false;
try {
while (toolRounds < this.maxToolRounds) {
const tools = createTools(this.bash, this.provider);
const system = buildSystemPrompt(this.bash.getCwd(), this.mode);
const result = streamText({
model: this.provider(this.modelId),
system,
messages: this.messages,
tools,
stopWhen: stepCountIs(this.maxToolRounds),
maxRetries: 0,
abortSignal: this.abortController.signal,
temperature: 0.7,
maxOutputTokens: this.maxTokens,
});
for await (const part of result.fullStream) {
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
yield { type: "done" };
return;
}
let accContent = "";
let accToolCalls: ToolCall[] | undefined;
let accReasoning = "";
for await (const delta of this.client.chatStream(this.messages, TOOLS)) {
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
yield { type: "done" };
return;
}
if (delta.content) {
accContent += delta.content;
yield { type: "content", content: delta.content };
}
if (delta.reasoning) {
accReasoning += delta.reasoning;
yield { type: "reasoning", content: delta.reasoning };
}
if (delta.toolCalls) {
accToolCalls = delta.toolCalls;
yield { type: "tool_calls", toolCalls: delta.toolCalls };
}
}
this.messages.push({
role: "assistant",
content: accContent || "",
tool_calls: accToolCalls,
});
if (!accToolCalls || accToolCalls.length === 0) {
break;
}
toolRounds++;
switch (part.type) {
case "text-delta":
assistantText += part.text;
yield { type: "content", content: part.text };
break;
for (const tc of accToolCalls) {
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
yield { type: "done" };
return;
case "reasoning-delta":
yield { type: "reasoning", content: part.text };
break;
case "tool-call": {
const tc = toToolCall(part);
yield { type: "tool_calls", toolCalls: [tc] };
break;
}
const result = await this.executeTool(tc);
case "tool-result": {
const tc: ToolCall = {
id: part.toolCallId,
type: "function",
function: { name: part.toolName, arguments: JSON.stringify(part.input ?? {}) },
};
const tr = toToolResult(part.output);
yield { type: "tool_result", toolCall: tc, toolResult: tr };
break;
}
yield { type: "tool_result", toolCall: tc, toolResult: result };
this.messages.push({
role: "tool",
content: result.success ? (result.output || "Success") : (result.error || "Error"),
tool_call_id: tc.id,
});
case "error":
yield { type: "error", content: String(part.error) };
break;
}
}
if (toolRounds >= this.maxToolRounds) {
yield { type: "content", content: "\n\nMax tool rounds reached." };
try {
const response = await result.response;
this.messages.push(...response.messages);
streamOk = true;
} catch {
// response promise can fail after stream errors — fall back to manual message
}
if (!streamOk && assistantText.trim()) {
this.messages.push({ role: "assistant", content: assistantText });
}
yield { type: "done" };
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
yield { type: "error", content: `Error: ${msg}` };
if (this.abortController.signal.aborted) {
yield { type: "content", content: "\n\n[Cancelled]" };
} else {
const msg = err instanceof Error ? err.message : String(err);
yield { type: "error", content: `Error: ${msg}` };
}
if (assistantText.trim()) {
this.messages.push({ role: "assistant", content: assistantText });
}
yield { type: "done" };
} finally {
this.abortController = null;
}
}
private async executeTool(tc: ToolCall): Promise<ToolResult> {
try {
const args = JSON.parse(tc.function.arguments);
switch (tc.function.name) {
case "bash":
return await this.bash.execute(args.command, args.timeout);
case "search_web":
const webResult = await this.client.searchWeb(args.query);
return { success: true, output: webResult };
case "search_x":
const xResult = await this.client.searchX(args.query);
return { success: true, output: xResult };
default:
return { success: false, error: `Unknown tool: ${tc.function.name}` };
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { success: false, error: `Tool error: ${msg}` };
}
}
}
function toToolCall(part: { toolCallId: string; toolName: string; args?: unknown }): ToolCall {
return {
id: part.toolCallId,
type: "function",
function: {
name: part.toolName,
arguments: JSON.stringify(part.args ?? {}),
},
};
}
function toToolResult(output: unknown): ToolResult {
if (output && typeof output === "object" && "success" in output) {
const r = output as { success: boolean; output?: string };
return {
success: r.success,
output: r.output,
error: r.success ? undefined : r.output,
};
}
return { success: true, output: String(output) };
}
+33 -156
View File
@@ -1,162 +1,39 @@
import OpenAI from "openai";
import type { ToolDefinition, ToolCall, Message } from "../types/index.js";
import { DEFAULT_MODEL } from "./models.js";
import { createXai } from "@ai-sdk/xai";
import { generateText } from "ai";
export interface StreamDelta {
content?: string;
toolCalls?: ToolCall[];
reasoning?: string;
export type XaiProvider = ReturnType<typeof createXai>;
export function createProvider(apiKey: string, baseURL?: string): XaiProvider {
return createXai({
apiKey,
baseURL: baseURL || process.env.GROK_BASE_URL || "https://api.x.ai/v1",
});
}
export class GrokClient {
private client: OpenAI;
private model: string;
private maxTokens: number;
constructor(apiKey: string, model?: string, baseURL?: string) {
this.client = new OpenAI({
apiKey,
baseURL: baseURL || process.env.GROK_BASE_URL || "https://api.x.ai/v1",
timeout: 360_000,
export async function generateTitle(
provider: XaiProvider,
userMessage: string,
): Promise<string> {
try {
const { text } = await generateText({
model: provider("grok-3-mini-fast"),
temperature: 0.5,
maxOutputTokens: 60,
system: [
"You are a title generator. Output ONLY a short title. Nothing else.",
"Rules:",
"- Single line, ≤50 characters",
"- Use the same language as the user message",
"- Focus on the main topic or intent",
"- Keep technical terms, filenames, numbers exact",
"- Remove filler words (the, this, my, a, an)",
"- Never use tools or explain anything",
"- If the message is a greeting, output something like 'Quick chat'",
].join("\n"),
prompt: userMessage,
});
this.model = model || DEFAULT_MODEL;
const envMax = Number(process.env.GROK_MAX_TOKENS);
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
}
getModel(): string {
return this.model;
}
setModel(model: string): void {
this.model = model;
}
async *chatStream(
messages: Message[],
tools: ToolDefinition[],
): AsyncGenerator<StreamDelta, void, unknown> {
const payload: Record<string, unknown> = {
model: this.model,
messages,
temperature: 0.7,
max_tokens: this.maxTokens,
stream: true,
};
if (tools.length > 0) {
payload.tools = tools;
payload.tool_choice = "auto";
}
const stream = (await this.client.chat.completions.create(
payload as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming,
)) as AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>;
const accToolCalls: Record<number, { id: string; name: string; arguments: string }> = {};
for await (const chunk of stream) {
const choice = chunk.choices?.[0];
if (!choice) continue;
const delta: StreamDelta = {};
if (choice.delta?.content) {
delta.content = choice.delta.content;
}
if ((choice.delta as Record<string, unknown>)?.reasoning) {
delta.reasoning = (choice.delta as Record<string, unknown>).reasoning as string;
}
if (choice.delta?.tool_calls) {
for (const tc of choice.delta.tool_calls) {
const idx = tc.index;
if (!accToolCalls[idx]) {
accToolCalls[idx] = { id: tc.id || "", name: "", arguments: "" };
}
if (tc.id) accToolCalls[idx].id = tc.id;
if (tc.function?.name) accToolCalls[idx].name += tc.function.name;
if (tc.function?.arguments) accToolCalls[idx].arguments += tc.function.arguments;
}
}
if (choice.finish_reason === "tool_calls" || choice.finish_reason === "stop") {
const calls = Object.values(accToolCalls);
if (calls.length > 0) {
delta.toolCalls = calls.map((c) => ({
id: c.id,
type: "function" as const,
function: { name: c.name, arguments: c.arguments },
}));
}
}
yield delta;
}
}
async searchWeb(query: string): Promise<string> {
return this.search(query, "web_search");
}
async searchX(query: string): Promise<string> {
return this.search(query, "x_search");
}
async generateTitle(userMessage: string): Promise<string> {
try {
const response = await this.client.chat.completions.create({
model: "grok-3-mini-fast",
temperature: 0.5,
max_tokens: 60,
messages: [
{
role: "system",
content: [
"You are a title generator. Output ONLY a short title. Nothing else.",
"Rules:",
"- Single line, ≤50 characters",
"- Use the same language as the user message",
"- Focus on the main topic or intent",
"- Keep technical terms, filenames, numbers exact",
"- Remove filler words (the, this, my, a, an)",
"- Never use tools or explain anything",
"- If the message is a greeting, output something like 'Quick chat'",
].join("\n"),
},
{ role: "user", content: userMessage },
],
});
const title = response.choices?.[0]?.message?.content?.trim().replace(/^["']|["']$/g, "");
return title || "New session";
} catch {
return "New session";
}
}
private async search(query: string, toolType: "web_search" | "x_search"): Promise<string> {
try {
const response = await this.client.responses.create({
model: this.model,
input: [{ role: "user", content: query }],
tools: [{ type: toolType } as unknown as OpenAI.Responses.Tool],
});
const parts: string[] = [];
for (const item of response.output) {
if (item.type === "message") {
for (const content of item.content) {
if (content.type === "output_text") {
parts.push(content.text);
}
}
}
}
return parts.join("\n") || "No results found.";
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return `Search failed: ${msg}`;
}
return text?.trim().replace(/^["']|["']$/g, "") || "New session";
} catch {
return "New session";
}
}
+85 -55
View File
@@ -1,63 +1,93 @@
import type { ToolDefinition } from "../types/index.js";
import { tool, generateText } from "ai";
import { z } from "zod";
import type { BashTool } from "../tools/bash.js";
import type { XaiProvider } from "./client.js";
export const TOOLS: ToolDefinition[] = [
{
type: "function",
function: {
name: "bash",
const SEARCH_MODEL = "grok-3-mini-fast";
export function createTools(bash: BashTool, provider: XaiProvider) {
return {
bash: tool({
description:
"Execute a bash command. Use this for ALL operations: viewing files (cat, less), editing files (sed, tee), searching (grep, rg, find), git, build tools, package managers, and any other shell command. Commands run in the project working directory.",
parameters: {
type: "object",
properties: {
command: {
type: "string",
description: "The bash command to execute",
},
timeout: {
type: "number",
description:
"Timeout in milliseconds (default: 30000). Use higher values for long-running commands.",
},
},
required: ["command"],
inputSchema: z.object({
command: z.string().describe("The bash command to execute"),
timeout: z
.number()
.optional()
.describe(
"Timeout in milliseconds (default: 30000). Use higher values for long-running commands.",
),
}),
execute: async ({ command, timeout }) => {
const result = await bash.execute(command, timeout);
return {
success: result.success,
output: result.success
? result.output || "Command executed successfully (no output)"
: result.error || "Command failed",
};
},
},
},
{
type: "function",
function: {
name: "search_web",
}),
search_web: tool({
description:
"Search the web for current, real-time information. Returns results from across the internet. Use when the user asks about current events, documentation, APIs, packages, or anything requiring up-to-date information.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query",
},
},
required: ["query"],
"Search the web for current information, documentation, APIs, tutorials, news, or any real-time data. Returns summarized results with sources.",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
try {
const { text } = await generateText({
model: provider(SEARCH_MODEL),
maxOutputTokens: 4096,
providerOptions: {
xai: {
searchParameters: {
mode: "on",
returnCitations: true,
sources: [{ type: "web" }],
},
},
},
prompt: query,
});
return { success: true, output: text };
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { success: false, output: `Web search failed: ${msg}` };
}
},
},
},
{
type: "function",
function: {
name: "search_x",
}),
search_x: tool({
description:
"Search X (formerly Twitter) for posts, discussions, trends, and real-time conversations. Use when the user asks about social media discussions, trending topics, or community sentiment.",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "The search query",
},
},
required: ["query"],
"Search X (Twitter) for real-time posts, discussions, opinions, and trends. Returns relevant posts with authors and engagement data.",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
try {
const { text } = await generateText({
model: provider(SEARCH_MODEL),
maxOutputTokens: 4096,
providerOptions: {
xai: {
searchParameters: {
mode: "on",
returnCitations: true,
sources: [{ type: "x" }],
},
},
},
prompt: query,
});
return { success: true, output: text };
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return { success: false, output: `X search failed: ${msg}` };
}
},
},
},
];
}),
};
}
-20
View File
@@ -4,13 +4,6 @@ export interface ToolResult {
error?: string;
}
export interface Message {
role: "system" | "user" | "assistant" | "tool";
content: string;
tool_calls?: ToolCall[];
tool_call_id?: string;
}
export interface ToolCall {
id: string;
type: "function";
@@ -20,19 +13,6 @@ export interface ToolCall {
};
}
export interface ToolDefinition {
type: "function";
function: {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, unknown>;
required: string[];
};
};
}
export interface ChatEntry {
type: "user" | "assistant" | "tool_call" | "tool_result";
content: string;
+7 -9
View File
@@ -74,6 +74,7 @@ export function App({ agent, initialMessage, onExit }: AppProps) {
const processedInitial = useRef(false);
const contentAccRef = useRef("");
const startTimeRef = useRef(0);
const isProcessingRef = useRef(false);
const setMode = useCallback((m: AgentMode) => { agent.setMode(m); setModeState(m); }, [agent]);
const cycleMode = useCallback(() => {
@@ -103,7 +104,8 @@ export function App({ agent, initialMessage, onExit }: AppProps) {
}, []);
const processMessage = useCallback(async (text: string) => {
if (!text.trim() || isProcessing) return;
if (!text.trim() || isProcessingRef.current) return;
isProcessingRef.current = true;
setIsProcessing(true); setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]); contentAccRef.current = "";
startTimeRef.current = Date.now();
if (!sessionTitle) agent.generateTitle(text.trim()).then(setSessionTitle).catch(() => {});
@@ -152,15 +154,11 @@ export function App({ agent, initialMessage, onExit }: AppProps) {
const f = contentAccRef.current;
setMessages((p) => [...p, { type: "assistant", content: f, timestamp: new Date() }]);
}
const elapsed = Date.now() - startTimeRef.current;
const dur = formatDuration(elapsed);
setMessages((p) => [...p, {
type: "tool_call" as const, content: `▣ ${modeInfo.label} · ${model}${dur ? ` · ${dur}` : ""}`,
timestamp: new Date(),
}]);
contentAccRef.current = ""; setStreamContent(""); setStreamReasoning(""); setActiveToolCalls([]);
isProcessingRef.current = false;
setIsProcessing(false); setTimeout(scrollToBottom, 50);
}, [agent, isProcessing, scrollToBottom, modeInfo, model]);
}, [agent, scrollToBottom, modeInfo, model]);
useEffect(() => { if (initialMessage && !processedInitial.current) { processedInitial.current = true; processMessage(initialMessage); } }, [initialMessage, processMessage]);
@@ -408,7 +406,7 @@ function PromptBox({ t, inputRef, isProcessing, showModelPicker, showSlashMenu,
return (
<box>
<box>
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={2} backgroundColor={t.backgroundElement} flexShrink={0}>
<box paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1} backgroundColor={t.backgroundElement} flexShrink={0}>
<textarea
ref={inputRef} focused={!isProcessing && !showModelPicker && !showSlashMenu}
placeholder={isProcessing ? "Working... (esc to stop)" : (placeholder || "Message Grok...")}