diff --git a/bun.lock b/bun.lock index 5532dd7..e35b067 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 7dc0491..e37fbf7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/agent/agent.ts b/src/agent/agent.ts index d69ccb0..02a34a2 100644 --- a/src/agent/agent.ts +++ b/src/agent/agent.ts @@ -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 = { 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 { - return this.client.generateTitle(userMessage); + async generateTitle(userMessage: string): Promise { + 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 { @@ -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 { - 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) }; } diff --git a/src/grok/client.ts b/src/grok/client.ts index ea82de3..2d4c87b 100644 --- a/src/grok/client.ts +++ b/src/grok/client.ts @@ -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; + +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 { + 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 { - const payload: Record = { - 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; - - const accToolCalls: Record = {}; - - 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)?.reasoning) { - delta.reasoning = (choice.delta as Record).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 { - return this.search(query, "web_search"); - } - - async searchX(query: string): Promise { - return this.search(query, "x_search"); - } - - async generateTitle(userMessage: string): Promise { - 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 { - 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"; } } diff --git a/src/grok/tools.ts b/src/grok/tools.ts index 50b401a..6d5f962 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -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}` }; + } }, - }, - }, -]; + }), + }; +} diff --git a/src/types/index.ts b/src/types/index.ts index bfd4ac3..575b839 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; - required: string[]; - }; - }; -} - export interface ChatEntry { type: "user" | "assistant" | "tool_call" | "tool_result"; content: string; diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 594aab1..cc486fc 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -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 ( - +