diff --git a/package.json b/package.json index 04322f3af..60102793d 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,7 @@ "packageManager": "pnpm@10.23.0", "dependencies": { "@agentclientprotocol/sdk": "0.13.1", + "@anthropic-ai/claude-agent-sdk": "^0.2.22", "@aws-sdk/client-bedrock": "^3.975.0", "@buape/carbon": "0.14.0", "@clack/prompts": "^0.11.0", @@ -250,7 +251,11 @@ "@sinclair/typebox": "0.34.47", "hono": "4.11.4", "tar": "7.5.4" - } + }, + "ignoredBuiltDependencies": [ + "core-js", + "node-llama-cpp" + ] }, "vitest": { "coverage": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c0f99928..9674b66b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,9 @@ importers: '@agentclientprotocol/sdk': specifier: 0.13.1 version: 0.13.1(zod@4.3.6) + '@anthropic-ai/claude-agent-sdk': + specifier: ^0.2.22 + version: 0.2.22(zod@4.3.6) '@aws-sdk/client-bedrock': specifier: ^3.975.0 version: 3.975.0 @@ -383,12 +386,12 @@ importers: '@microsoft/agents-hosting-extensions-teams': specifier: ^1.2.2 version: 1.2.2 - moltbot: - specifier: workspace:* - version: link:../.. express: specifier: ^5.2.1 version: 5.2.1 + moltbot: + specifier: workspace:* + version: link:../.. proper-lockfile: specifier: ^4.1.2 version: 4.1.2 @@ -519,6 +522,12 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + '@anthropic-ai/claude-agent-sdk@0.2.22': + resolution: {integrity: sha512-AO39PYe8nAfqx748UmQQ26BZAX91sOQomYFdtf5AwMwgOIH0BumrNHsHtrmgBZalsseWn84LAFfKtG5ylGR5Nw==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^4.0.0 + '@anthropic-ai/sdk@0.71.2': resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==} hasBin: true @@ -1100,33 +1109,65 @@ packages: resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-arm64@0.34.5': resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] @@ -1147,27 +1188,54 @@ packages: cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1192,18 +1260,36 @@ packages: cpu: [s390x] os: [linux] + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1227,6 +1313,12 @@ packages: cpu: [ia32] os: [win32] + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3214,11 +3306,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - clawdbot@2026.1.24-3: - resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==} - engines: {node: '>=22.12.0'} - hasBin: true - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -5582,6 +5669,19 @@ snapshots: dependencies: zod: 4.3.6 + '@anthropic-ai/claude-agent-sdk@0.2.22(zod@4.3.6)': + dependencies: + zod: 4.3.6 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + '@anthropic-ai/sdk@0.71.2(zod@4.3.6)': dependencies: json-schema-to-ts: 3.1.1 @@ -6706,25 +6806,47 @@ snapshots: '@img/colour@1.0.0': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + '@img/sharp-libvips-linux-arm@1.2.4': optional: true @@ -6737,20 +6859,39 @@ snapshots: '@img/sharp-libvips-linux-s390x@1.2.4': optional: true + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + '@img/sharp-libvips-linux-x64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 @@ -6771,16 +6912,31 @@ snapshots: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 @@ -6797,6 +6953,9 @@ snapshots: '@img/sharp-win32-ia32@0.34.5': optional: true + '@img/sharp-win32-x64@0.33.5': + optional: true + '@img/sharp-win32-x64@0.34.5': optional: true @@ -9098,84 +9257,6 @@ snapshots: dependencies: clsx: 2.1.1 - clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3): - dependencies: - '@agentclientprotocol/sdk': 0.13.1(zod@4.3.6) - '@aws-sdk/client-bedrock': 3.975.0 - '@buape/carbon': 0.14.0(hono@4.11.4) - '@clack/prompts': 0.11.0 - '@grammyjs/runner': 2.0.3(grammy@1.39.3) - '@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3) - '@homebridge/ciao': 1.3.4 - '@line/bot-sdk': 10.6.0 - '@lydell/node-pty': 1.2.0-beta.3 - '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.49.3 - '@mozilla/readability': 0.6.0 - '@sinclair/typebox': 0.34.47 - '@slack/bolt': 4.6.0(@types/express@5.0.6) - '@slack/web-api': 7.13.0 - '@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) - ajv: 8.17.1 - body-parser: 2.2.2 - chalk: 5.6.2 - chokidar: 5.0.0 - chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482) - cli-highlight: 2.1.11 - commander: 14.0.2 - croner: 9.1.0 - detect-libc: 2.1.2 - discord-api-types: 0.38.37 - dotenv: 17.2.3 - express: 5.2.1 - file-type: 21.3.0 - grammy: 1.39.3 - hono: 4.11.4 - jiti: 2.6.1 - json5: 2.2.3 - jszip: 3.10.1 - linkedom: 0.18.12 - long: 5.3.2 - markdown-it: 14.1.0 - node-edge-tts: 1.2.9 - osc-progress: 0.3.0 - pdfjs-dist: 5.4.530 - playwright-core: 1.58.0 - proper-lockfile: 4.1.2 - qrcode-terminal: 0.12.0 - sharp: 0.34.5 - sqlite-vec: 0.1.7-alpha.2 - tar: 7.5.4 - tslog: 4.10.2 - undici: 7.19.0 - ws: 8.19.0 - yaml: 2.8.2 - zod: 4.3.6 - optionalDependencies: - '@napi-rs/canvas': 0.1.88 - node-llama-cpp: 3.15.0(typescript@5.9.3) - transitivePeerDependencies: - - '@discordjs/opus' - - '@modelcontextprotocol/sdk' - - '@types/express' - - audio-decode - - aws-crt - - bufferutil - - canvas - - debug - - devtools-protocol - - encoding - - ffmpeg-static - - jimp - - link-preview-js - - node-opus - - opusscript - - supports-color - - typescript - - utf-8-validate - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 diff --git a/src/agents/agent-runtime.ts b/src/agents/agent-runtime.ts new file mode 100644 index 000000000..a68b2ed45 --- /dev/null +++ b/src/agents/agent-runtime.ts @@ -0,0 +1,95 @@ +/** + * Agent runtime abstraction layer. + * + * Defines the common interface for agent execution backends (Pi Agent, Claude Agent SDK, etc.). + */ + +import type { ImageContent } from "@mariozechner/pi-ai"; +import type { MoltbotConfig } from "../config/config.js"; +import type { AgentStreamParams } from "../commands/agent/types.js"; +import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js"; +import type { SkillSnapshot } from "./skills.js"; +import type { EmbeddedPiRunResult } from "./pi-embedded-runner/types.js"; + +/** Agent runtime backend discriminant. */ +export type AgentRuntimeKind = "pi" | "ccsdk"; + +/** Result type shared by all agent runtimes. */ +export type AgentRuntimeResult = EmbeddedPiRunResult; + +/** Streaming and event callbacks for agent runs. */ +export type AgentRuntimeCallbacks = { + /** Called when a partial reply chunk is available. */ + onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + /** Called when the assistant message starts. */ + onAssistantMessageStart?: () => void | Promise; + /** Called for block-level reply delivery. */ + onBlockReply?: (payload: { + text?: string; + mediaUrls?: string[]; + audioAsVoice?: boolean; + replyToId?: string; + replyToTag?: boolean; + replyToCurrent?: boolean; + }) => void | Promise; + /** Called when block replies should be flushed. */ + onBlockReplyFlush?: () => void | Promise; + /** Called for reasoning/thinking stream events. */ + onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + /** Called when a tool result is available. */ + onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise; + /** Called for agent lifecycle and internal events. */ + onAgentEvent?: (evt: { stream: string; data: Record }) => void; +}; + +/** Common parameters for agent run invocations. */ +export type AgentRuntimeRunParams = { + sessionId: string; + sessionKey?: string; + sessionFile: string; + workspaceDir: string; + agentDir?: string; + config?: MoltbotConfig; + skillsSnapshot?: SkillSnapshot; + prompt: string; + images?: ImageContent[]; + provider?: string; + model?: string; + authProfileId?: string; + authProfileIdSource?: "auto" | "user"; + thinkLevel?: ThinkLevel; + verboseLevel?: VerboseLevel; + timeoutMs: number; + runId: string; + lane?: string; + abortSignal?: AbortSignal; + extraSystemPrompt?: string; + streamParams?: AgentStreamParams; + /** Messaging context for delivery routing. */ + messageChannel?: string; + agentAccountId?: string; + messageTo?: string; + messageThreadId?: string | number; + groupId?: string | null; + groupChannel?: string | null; + groupSpace?: string | null; + spawnedBy?: string | null; + currentChannelId?: string; + currentThreadTs?: string; + replyToMode?: "off" | "first" | "all"; + hasRepliedRef?: { value: boolean }; +} & AgentRuntimeCallbacks; + +/** + * Agent runtime interface. + * + * Defines the contract for executing agent turns across different backends. + */ +export interface AgentRuntime { + /** Runtime backend discriminant. */ + readonly kind: AgentRuntimeKind; + /** Human-readable display name for the runtime. */ + readonly displayName: string; + /** Execute an agent turn with the given parameters. */ + run(params: AgentRuntimeRunParams): Promise; +} diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index aae983040..6b78a27bd 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -17,6 +17,8 @@ type AgentEntry = NonNullable["list"]>[numb type ResolvedAgentConfig = { name?: string; + runtime?: AgentEntry["runtime"]; + ccsdk?: AgentEntry["ccsdk"]; workspace?: string; agentDir?: string; model?: AgentEntry["model"]; @@ -97,6 +99,8 @@ export function resolveAgentConfig( if (!entry) return undefined; return { name: typeof entry.name === "string" ? entry.name : undefined, + runtime: entry.runtime, + ccsdk: entry.ccsdk, workspace: typeof entry.workspace === "string" ? entry.workspace : undefined, agentDir: typeof entry.agentDir === "string" ? entry.agentDir : undefined, model: diff --git a/src/agents/claude-agent-sdk/index.ts b/src/agents/claude-agent-sdk/index.ts new file mode 100644 index 000000000..9991d63b3 --- /dev/null +++ b/src/agents/claude-agent-sdk/index.ts @@ -0,0 +1,54 @@ +/** + * Claude Agent SDK integration module. + * + * Provides an alternative agent runtime using the Claude Agent SDK. + * + * @module agents/claude-agent-sdk + */ + +export { isSdkAvailable, loadClaudeAgentSdk, resetSdkLoaderForTest } from "./sdk-loader.js"; +export { + buildAnthropicSdkProvider, + buildBedrockSdkProvider, + buildClaudeCliSdkProvider, + buildOpenRouterSdkProvider, + buildVertexSdkProvider, + buildZaiSdkProvider, + resolveProviderConfig, +} from "./provider-config.js"; +export { runSdkAgent } from "./sdk-runner.js"; +export { createCcSdkAgentRuntime, type CcSdkAgentRuntimeContext } from "./sdk-agent-runtime.js"; +export { + bridgeMoltbotToolsToMcpServer, + bridgeMoltbotToolsSync, + buildMcpAllowedTools, + convertToolResult, + extractJsonSchema, + mcpToolName, + resetMcpServerCache, + wrapToolHandler, + type BridgeOptions, + type BridgeResult, +} from "./tool-bridge.js"; +export type { + McpCallToolResult, + McpContentBlock, + McpImageContent, + McpSdkServerConfig, + McpServerConstructor, + McpServerLike, + McpTextContent, + SdkRunnerQueryOptions, +} from "./tool-bridge.types.js"; +export type { + SdkDoneEvent, + SdkErrorEvent, + SdkEvent, + SdkEventType, + SdkProviderConfig, + SdkProviderEnv, + SdkRunnerParams, + SdkTextEvent, + SdkToolResultEvent, + SdkToolUseEvent, +} from "./types.js"; diff --git a/src/agents/claude-agent-sdk/provider-config.ts b/src/agents/claude-agent-sdk/provider-config.ts new file mode 100644 index 000000000..721d43a8c --- /dev/null +++ b/src/agents/claude-agent-sdk/provider-config.ts @@ -0,0 +1,220 @@ +/** + * Provider configuration builders for the Claude Agent SDK. + * + * Supports multiple authentication methods: + * - Anthropic API key (ANTHROPIC_API_KEY) + * - Claude Code CLI OAuth (reuses ~/.claude credentials) + * - z.AI subscription (via ANTHROPIC_AUTH_TOKEN or CLI auth) + * - OpenRouter (Anthropic-compatible API) + * - AWS Bedrock + * - Google Vertex AI + */ + +import type { SdkProviderConfig, SdkProviderEnv } from "./types.js"; +import { readClaudeCliCredentialsCached } from "../cli-credentials.js"; +import { createSubsystemLogger } from "../../logging/subsystem.js"; + +const log = createSubsystemLogger("agents/claude-agent-sdk"); + +/** + * Build provider config for direct Anthropic API access. + */ +export function buildAnthropicSdkProvider(apiKey: string): SdkProviderConfig { + return { + name: "Anthropic", + env: { + ANTHROPIC_API_KEY: apiKey, + }, + }; +} + +/** + * Build provider config for Claude Code CLI/subscription auth. + * + * This method reads credentials from the Claude Code CLI's keychain/file storage, + * enabling subscription-based access (Claude Max, z.AI, etc.) without an API key. + * + * The SDK will automatically use these credentials when they're available in the + * environment or via the CLI's native auth mechanism. + */ +export function buildClaudeCliSdkProvider(): SdkProviderConfig | null { + // Read cached credentials from Claude Code CLI + const credentials = readClaudeCliCredentialsCached({ + allowKeychainPrompt: false, + ttlMs: 60_000, // 1 minute cache + }); + + if (!credentials) { + log.debug("No Claude CLI credentials found"); + return null; + } + + // For OAuth credentials, use the access token as ANTHROPIC_AUTH_TOKEN + if (credentials.type === "oauth") { + // Check if token is expired + if (credentials.expires < Date.now()) { + log.warn("Claude CLI credentials expired", { + expiresAt: new Date(credentials.expires).toISOString(), + }); + // Still return the config - the SDK may handle refresh internally + } + + return { + name: "Claude CLI (subscription)", + env: { + ANTHROPIC_AUTH_TOKEN: credentials.access, + }, + }; + } + + // For token-type credentials (less common) + if (credentials.type === "token") { + return { + name: "Claude CLI (token)", + env: { + ANTHROPIC_AUTH_TOKEN: credentials.token, + }, + }; + } + + return null; +} + +/** + * Build provider config for z.AI subscription access. + * + * z.AI uses Anthropic-compatible API with a different base URL. + */ +export function buildZaiSdkProvider(authToken: string): SdkProviderConfig { + return { + name: "z.AI", + env: { + ANTHROPIC_AUTH_TOKEN: authToken, + // z.AI may use a custom base URL - leave unset to use default + // The SDK or z.AI gateway handles routing + }, + }; +} + +/** + * Build provider config for OpenRouter (Anthropic-compatible). + */ +export function buildOpenRouterSdkProvider(apiKey: string): SdkProviderConfig { + return { + name: "OpenRouter (Anthropic-compatible)", + env: { + ANTHROPIC_BASE_URL: "https://openrouter.ai/api/v1", + ANTHROPIC_API_KEY: apiKey, + }, + }; +} + +/** + * Build provider config for AWS Bedrock. + * + * AWS credentials should be configured via standard AWS mechanisms + * (environment variables, shared credentials file, IAM role, etc.). + */ +export function buildBedrockSdkProvider(): SdkProviderConfig { + return { + name: "AWS Bedrock", + env: { + CLAUDE_CODE_USE_BEDROCK: "1", + }, + }; +} + +/** + * Build provider config for Google Vertex AI. + * + * Google Cloud credentials should be configured via standard GCP mechanisms + * (GOOGLE_APPLICATION_CREDENTIALS, default credentials, etc.). + */ +export function buildVertexSdkProvider(): SdkProviderConfig { + return { + name: "Google Vertex AI", + env: { + CLAUDE_CODE_USE_VERTEX: "1", + }, + }; +} + +/** + * Resolve provider configuration based on available credentials. + * + * Priority order: + * 1. Explicit API key from config/env + * 2. Claude Code CLI credentials (subscription auth) + * 3. Environment variables (fallback) + */ +export function resolveProviderConfig(options?: { + apiKey?: string; + authToken?: string; + baseUrl?: string; + useCliCredentials?: boolean; +}): SdkProviderConfig { + // 1. Explicit API key takes precedence + if (options?.apiKey) { + const config = buildAnthropicSdkProvider(options.apiKey); + if (options.baseUrl) { + config.env.ANTHROPIC_BASE_URL = options.baseUrl; + } + return config; + } + + // 2. Explicit auth token (OAuth/subscription) + if (options?.authToken) { + const env: SdkProviderEnv = { + ANTHROPIC_AUTH_TOKEN: options.authToken, + }; + if (options.baseUrl) { + env.ANTHROPIC_BASE_URL = options.baseUrl; + } + return { + name: "Anthropic (auth token)", + env, + }; + } + + // 3. Try Claude CLI credentials if enabled (default: true) + if (options?.useCliCredentials !== false) { + const cliConfig = buildClaudeCliSdkProvider(); + if (cliConfig) { + if (options?.baseUrl) { + cliConfig.env.ANTHROPIC_BASE_URL = options.baseUrl; + } + return cliConfig; + } + } + + // 4. Check environment variables + const envApiKey = process.env.ANTHROPIC_API_KEY; + const envAuthToken = process.env.ANTHROPIC_AUTH_TOKEN; + + if (envApiKey) { + return { + name: "Anthropic (env)", + env: { + ANTHROPIC_API_KEY: envApiKey, + ANTHROPIC_BASE_URL: options?.baseUrl ?? process.env.ANTHROPIC_BASE_URL, + }, + }; + } + + if (envAuthToken) { + return { + name: "Anthropic (env auth token)", + env: { + ANTHROPIC_AUTH_TOKEN: envAuthToken, + ANTHROPIC_BASE_URL: options?.baseUrl ?? process.env.ANTHROPIC_BASE_URL, + }, + }; + } + + // 5. Return empty config - SDK will use its own credential resolution + log.debug("No explicit credentials provided, SDK will use native auth"); + return { + name: "Anthropic (SDK native)", + env: {}, + }; +} diff --git a/src/agents/claude-agent-sdk/sdk-agent-runtime.ts b/src/agents/claude-agent-sdk/sdk-agent-runtime.ts new file mode 100644 index 000000000..a3b52e466 --- /dev/null +++ b/src/agents/claude-agent-sdk/sdk-agent-runtime.ts @@ -0,0 +1,85 @@ +/** + * Claude Agent SDK runtime implementation. + * + * Implements the AgentRuntime interface using the Claude Agent SDK for execution. + */ + +import type { MoltbotConfig } from "../../config/config.js"; +import type { AgentRuntime, AgentRuntimeRunParams, AgentRuntimeResult } from "../agent-runtime.js"; +import type { AgentCcSdkConfig } from "../../config/types.agents.js"; +import { runSdkAgent } from "./sdk-runner.js"; +import { resolveProviderConfig } from "./provider-config.js"; +import { isSdkAvailable } from "./sdk-loader.js"; +import { createSubsystemLogger } from "../../logging/subsystem.js"; + +const log = createSubsystemLogger("agents/claude-agent-sdk"); + +export type CcSdkAgentRuntimeContext = { + /** Moltbot configuration. */ + config?: MoltbotConfig; + /** Claude Code SDK configuration. */ + ccsdkConfig?: AgentCcSdkConfig; + /** Explicit API key override. */ + apiKey?: string; + /** Explicit auth token override (for subscription auth). */ + authToken?: string; + /** Custom base URL for API requests. */ + baseUrl?: string; +}; + +/** + * Create a Claude Code SDK runtime instance. + * + * The CCSDK runtime uses the Claude Agent SDK for model execution, + * which supports: + * - Claude Code CLI authentication (subscription-based) + * - Anthropic API key authentication + * - AWS Bedrock and Google Vertex AI backends + */ +export function createCcSdkAgentRuntime(context?: CcSdkAgentRuntimeContext): AgentRuntime { + // Pre-check SDK availability + if (!isSdkAvailable()) { + log.warn("Claude Agent SDK not available - runtime will fail on first run"); + } + + // Resolve provider configuration from context + const providerConfig = resolveProviderConfig({ + apiKey: context?.apiKey, + authToken: context?.authToken, + baseUrl: context?.baseUrl, + useCliCredentials: true, // Enable Claude CLI credential resolution + }); + + return { + kind: "ccsdk", + displayName: `Claude Code SDK (${providerConfig.name})`, + + async run(params: AgentRuntimeRunParams): Promise { + log.debug("CCSDK runtime run", { + sessionId: params.sessionId, + runId: params.runId, + provider: providerConfig.name, + }); + + return runSdkAgent({ + sessionId: params.sessionId, + sessionKey: params.sessionKey, + sessionFile: params.sessionFile, + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.config ?? context?.config, + prompt: params.prompt, + model: params.model ? `${params.provider}/${params.model}` : undefined, + providerConfig, + timeoutMs: params.timeoutMs, + runId: params.runId, + abortSignal: params.abortSignal, + extraSystemPrompt: params.extraSystemPrompt, + hooksEnabled: context?.ccsdkConfig?.hooksEnabled, + sdkOptions: context?.ccsdkConfig?.options, + modelTiers: context?.ccsdkConfig?.models, + onAgentEvent: params.onAgentEvent, + }); + }, + }; +} diff --git a/src/agents/claude-agent-sdk/sdk-loader.ts b/src/agents/claude-agent-sdk/sdk-loader.ts new file mode 100644 index 000000000..19aa03ec3 --- /dev/null +++ b/src/agents/claude-agent-sdk/sdk-loader.ts @@ -0,0 +1,84 @@ +/** + * Dynamic loader for the Claude Agent SDK. + * + * The SDK is an optional dependency, so we load it dynamically to avoid + * breaking installations that don't have it installed. + */ + +import { createRequire } from "node:module"; +import { createSubsystemLogger } from "../../logging/subsystem.js"; + +const log = createSubsystemLogger("agents/claude-agent-sdk"); + +/** + * SDK module type (generic since the actual types may not be available). + * + * The Claude Agent SDK exports various classes and functions for agent execution. + * Since it's an optional dependency, we use a generic type here. + */ +export type ClaudeAgentSdkModule = { + // The actual SDK API - shape will vary by version + [key: string]: unknown; +}; + +/** Cached SDK module reference. */ +let sdkModule: ClaudeAgentSdkModule | null = null; +let loadAttempted = false; +let loadError: Error | null = null; + +/** + * Check if the Claude Agent SDK is available. + */ +export function isSdkAvailable(): boolean { + if (loadAttempted) return sdkModule !== null; + try { + // Probe for the module without loading it fully + const require = createRequire(import.meta.url); + require.resolve("@anthropic-ai/claude-agent-sdk"); + return true; + } catch { + return false; + } +} + +/** + * Load the Claude Agent SDK dynamically. + * + * @throws Error if the SDK is not installed + */ +export async function loadClaudeAgentSdk(): Promise { + if (sdkModule) { + return sdkModule; + } + + if (loadAttempted && loadError) { + throw loadError; + } + + loadAttempted = true; + + try { + // Dynamic import to avoid bundling issues + // The SDK package name is resolved at runtime + const moduleName = "@anthropic-ai/claude-agent-sdk"; + sdkModule = (await import(/* @vite-ignore */ moduleName)) as ClaudeAgentSdkModule; + log.info("Claude Agent SDK loaded successfully"); + return sdkModule; + } catch (err) { + loadError = new Error( + "Claude Agent SDK not installed. Install with: npm install @anthropic-ai/claude-agent-sdk", + ); + loadError.cause = err; + log.warn("Failed to load Claude Agent SDK", { error: String(err) }); + throw loadError; + } +} + +/** + * Reset the SDK loader state (for testing). + */ +export function resetSdkLoaderForTest(): void { + sdkModule = null; + loadAttempted = false; + loadError = null; +} diff --git a/src/agents/claude-agent-sdk/sdk-runner.ts b/src/agents/claude-agent-sdk/sdk-runner.ts new file mode 100644 index 000000000..c30e2913a --- /dev/null +++ b/src/agents/claude-agent-sdk/sdk-runner.ts @@ -0,0 +1,276 @@ +/** + * Claude Agent SDK runner. + * + * Executes agent turns using the Claude Agent SDK, handling authentication, + * event streaming, and result adaptation. + */ + +import type { SdkRunnerParams, SdkProviderEnv } from "./types.js"; +import type { CcSdkModelTiers } from "../../config/types.agents.js"; +import type { AgentRuntimeResult } from "../agent-runtime.js"; +import { loadClaudeAgentSdk, type ClaudeAgentSdkModule } from "./sdk-loader.js"; +import { resolveProviderConfig } from "./provider-config.js"; +import { createSubsystemLogger } from "../../logging/subsystem.js"; + +const log = createSubsystemLogger("agents/claude-agent-sdk"); + +/** + * Build environment variables for model tier configuration. + * + * The Claude Code SDK uses these environment variables to select models + * for different task complexity tiers. + */ +function buildModelTierEnv(modelTiers?: CcSdkModelTiers): SdkProviderEnv { + const env: SdkProviderEnv = {}; + + if (modelTiers?.haiku) { + env.ANTHROPIC_DEFAULT_HAIKU_MODEL = modelTiers.haiku; + } + if (modelTiers?.sonnet) { + env.ANTHROPIC_DEFAULT_SONNET_MODEL = modelTiers.sonnet; + } + if (modelTiers?.opus) { + env.ANTHROPIC_DEFAULT_OPUS_MODEL = modelTiers.opus; + } + + return env; +} + +/** + * Run an agent turn using the Claude Agent SDK. + * + * This function: + * 1. Loads the SDK dynamically + * 2. Configures authentication based on available credentials + * 3. Executes the agent turn with the given prompt + * 4. Streams events back to the caller + * 5. Returns results in the standard AgentRuntimeResult format + */ +export async function runSdkAgent(params: SdkRunnerParams): Promise { + const started = Date.now(); + + // Emit lifecycle start event + params.onAgentEvent?.({ + stream: "lifecycle", + data: { + phase: "start", + runtime: "sdk", + sessionId: params.sessionId, + runId: params.runId, + }, + }); + + try { + // Load the SDK + const sdk = await loadClaudeAgentSdk(); + + // Resolve provider configuration + const providerConfig = params.providerConfig ?? resolveProviderConfig(); + + log.info("Starting SDK agent run", { + provider: providerConfig.name, + model: params.model, + sessionId: params.sessionId, + runId: params.runId, + }); + + // Set up environment for the SDK + const originalEnv = { ...process.env }; + try { + // Apply provider environment variables + for (const [key, value] of Object.entries(providerConfig.env)) { + if (value !== undefined) { + process.env[key] = value; + } + } + + // Apply model tier environment variables + const modelTierEnv = buildModelTierEnv(params.modelTiers); + for (const [key, value] of Object.entries(modelTierEnv)) { + if (value !== undefined) { + process.env[key] = value; + } + } + + // Build SDK options + const sdkOptions = { + model: params.model, + workingDirectory: params.workspaceDir, + systemPrompt: params.extraSystemPrompt, + timeout: params.timeoutMs, + signal: params.abortSignal, + ...params.sdkOptions, + }; + + // Run the agent + // Note: The actual SDK API may differ - this is a placeholder implementation + // that will need to be updated based on the actual SDK interface + const result = await runSdkAgentInternal(sdk, params.prompt, sdkOptions, { + onEvent: (event) => { + // Forward events to the caller + params.onAgentEvent?.({ + stream: "sdk", + data: event as Record, + }); + }, + }); + + // Build result + const agentResult: AgentRuntimeResult = { + payloads: result.texts.map((text) => ({ text })), + meta: { + durationMs: Date.now() - started, + agentMeta: { + sessionId: params.sessionId, + provider: "anthropic", + model: params.model ?? "claude-sonnet-4-20250514", + usage: result.usage, + }, + aborted: result.aborted, + }, + }; + + // Emit lifecycle end event + params.onAgentEvent?.({ + stream: "lifecycle", + data: { + phase: "end", + runtime: "sdk", + startedAt: started, + endedAt: Date.now(), + aborted: result.aborted ?? false, + }, + }); + + return agentResult; + } finally { + // Restore original environment + const keysToRestore = [ + ...Object.keys(providerConfig.env), + ...Object.keys(buildModelTierEnv(params.modelTiers)), + ]; + for (const key of keysToRestore) { + if (originalEnv[key] !== undefined) { + process.env[key] = originalEnv[key]; + } else { + delete process.env[key]; + } + } + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + log.error("SDK agent run failed", { + error: errorMessage, + sessionId: params.sessionId, + runId: params.runId, + }); + + // Emit lifecycle error event + params.onAgentEvent?.({ + stream: "lifecycle", + data: { + phase: "error", + runtime: "sdk", + startedAt: started, + endedAt: Date.now(), + error: errorMessage, + }, + }); + + // Return error result + return { + payloads: [ + { + text: `SDK agent error: ${errorMessage}`, + isError: true, + }, + ], + meta: { + durationMs: Date.now() - started, + agentMeta: { + sessionId: params.sessionId, + provider: "anthropic", + model: params.model ?? "unknown", + }, + }, + }; + } +} + +/** + * Internal SDK execution function. + * + * This is a placeholder that needs to be implemented based on the actual + * Claude Agent SDK API. The SDK provides different interfaces for: + * - Simple completions + * - Agentic tool use + * - Streaming responses + */ +async function runSdkAgentInternal( + sdk: ClaudeAgentSdkModule, + _prompt: string, + options: { + model?: string; + workingDirectory?: string; + systemPrompt?: string; + timeout?: number; + signal?: AbortSignal; + }, + _callbacks: { + onEvent?: (event: unknown) => void; + }, +): Promise<{ + texts: string[]; + usage?: { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + total?: number; + }; + aborted?: boolean; +}> { + // Placeholder implementation - needs to be updated based on actual SDK API + // The SDK likely provides something like: + // - sdk.createAgent() or sdk.Agent class + // - agent.run(prompt) or agent.chat(prompt) + // - Event streaming via async iterators or callbacks + + const texts: string[] = []; + const usage: + | { + input?: number; + output?: number; + cacheRead?: number; + cacheWrite?: number; + total?: number; + } + | undefined = undefined; + let aborted = false; + + try { + // Check if SDK has the expected interface + // This needs to be updated based on actual SDK API + if ("Claude" in sdk || "createAgent" in sdk || "Agent" in sdk) { + // TODO: Implement actual SDK call based on SDK's public API + // For now, throw to indicate SDK integration is not yet complete + throw new Error( + "Claude Agent SDK integration not yet implemented. " + + "Please check SDK documentation for the correct API.", + ); + } + + // Fallback: check for simple completion API + throw new Error("Unable to find compatible API in Claude Agent SDK"); + } catch (error) { + if (options.signal?.aborted) { + aborted = true; + texts.push("Agent run was aborted."); + } else { + throw error; + } + } + + return { texts, usage, aborted }; +} diff --git a/src/agents/claude-agent-sdk/tool-bridge.ts b/src/agents/claude-agent-sdk/tool-bridge.ts new file mode 100644 index 000000000..d5699ea30 --- /dev/null +++ b/src/agents/claude-agent-sdk/tool-bridge.ts @@ -0,0 +1,368 @@ +/** + * Bridge between Moltbot tools (AnyAgentTool) and the Claude Agent SDK's + * MCP-based custom tool system. + * + * This module is pure facade/adapter code — no business logic is duplicated. + * Each Moltbot tool's `execute()` function is called as-is; the bridge only + * converts schemas, argument shapes, and result formats. + * + * Usage: + * ```ts + * const mcpConfig = await bridgeMoltbotToolsToMcpServer({ + * name: "moltbot", + * tools: myMoltbotTools, + * }); + * // Pass to SDK query(): + * query({ prompt, options: { mcpServers: { moltbot: mcpConfig } } }) + * ``` + */ + +import type { AgentToolResult } from "@mariozechner/pi-agent-core"; + +import { createSubsystemLogger } from "../../logging/subsystem.js"; +import { normalizeToolName } from "../tool-policy.js"; +import type { AnyAgentTool } from "../tools/common.js"; +import type { + McpCallToolResult, + McpContentBlock, + McpSdkServerConfig, + McpServerConstructor, + McpServerLike, +} from "./tool-bridge.types.js"; + +const log = createSubsystemLogger("agents/claude-agent-sdk/tool-bridge"); + +// --------------------------------------------------------------------------- +// Schema conversion: TypeBox → JSON Schema (passthrough) +// --------------------------------------------------------------------------- + +/** + * Extract a clean JSON Schema object from a TypeBox schema. + * + * TypeBox schemas *are* JSON Schema objects, but they carry internal Symbol + * metadata and potentially circular references. A JSON round-trip strips those + * cleanly. We also ensure a sensible fallback for tools with no schema. + */ +export function extractJsonSchema(tool: AnyAgentTool): Record { + const schema = tool.parameters; + if (!schema || typeof schema !== "object") { + return { type: "object", properties: {} }; + } + try { + return JSON.parse(JSON.stringify(schema)) as Record; + } catch { + // Schema is not serializable (shouldn't happen with TypeBox, but be safe). + log.debug(`Schema for "${tool.name}" is not JSON-serializable, using empty schema`); + return { type: "object", properties: {} }; + } +} + +// --------------------------------------------------------------------------- +// Result conversion: AgentToolResult → McpCallToolResult +// --------------------------------------------------------------------------- + +/** + * Convert a Moltbot `AgentToolResult` to an MCP `CallToolResult`. + * + * Content block shapes are nearly identical between the two systems. + * The main differences handled: + * - `tool_error` blocks → text + `isError: true` + * - `details` metadata → serialized as a `` text block + * - Empty results → "(no output)" fallback + */ +export function convertToolResult(result: AgentToolResult): McpCallToolResult { + const content: McpContentBlock[] = []; + let isError = false; + + for (const block of result.content) { + switch (block.type) { + case "text": + content.push({ type: "text", text: (block as { text: string }).text }); + break; + case "image": + content.push({ + type: "image", + data: (block as { data: string }).data, + mimeType: (block as { mimeType: string }).mimeType, + }); + break; + default: { + // Handle tool_error or any other block type with an 'error' field. + const maybeError = block as { type: string; error?: string; text?: string }; + if (maybeError.type === "tool_error" || maybeError.error) { + const errorText = maybeError.error ?? maybeError.text ?? "Unknown tool error"; + content.push({ type: "text", text: errorText }); + isError = true; + } else if (maybeError.text) { + // Fallback: treat any block with a text field as text content. + content.push({ type: "text", text: maybeError.text }); + } + } + } + } + + // Serialize `details` as an additional text block when present. + // The model benefits from structured metadata; we use an XML tag + // wrapper so it's easy to parse and doesn't pollute the main output. + if (result.details !== undefined && result.details !== null) { + try { + const serialized = JSON.stringify(result.details, null, 2); + content.push({ + type: "text", + text: `\n${serialized}\n`, + }); + } catch { + // details not serializable — skip + } + } + + if (content.length === 0) { + content.push({ type: "text", text: "(no output)" }); + } + + return isError ? { content, isError: true } : { content }; +} + +// --------------------------------------------------------------------------- +// Handler wrapping: AnyAgentTool.execute → MCP handler +// --------------------------------------------------------------------------- + +/** + * Wrap a Moltbot tool's `execute()` as an MCP tool handler. + * + * Differences bridged: + * - Moltbot: `execute(toolCallId, params, signal?, onUpdate?)` + * - MCP: `handler(args) → Promise` + * + * Notable: MCP handlers have no abort signal or streaming update callback. + * We pass the shared `abortSignal` (if provided) and skip `onUpdate`. + */ +export function wrapToolHandler( + tool: AnyAgentTool, + abortSignal?: AbortSignal, +): (args: Record) => Promise { + const normalizedName = normalizeToolName(tool.name); + + return async (args: Record): Promise => { + // Generate a synthetic toolCallId. The Claude Agent SDK doesn't expose its + // internal tool call ID to MCP handlers, so we create a unique one for + // internal tracking/logging. This is safe because Moltbot tools only use + // toolCallId for logging and scoping, not for cross-referencing with the + // model's response. + const toolCallId = `mcp-bridge-${normalizedName}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + try { + const result = await tool.execute( + toolCallId, + args, + abortSignal, // 3rd arg: AbortSignal (undefined if not provided) + undefined, // 4th arg: onUpdate — not supported in MCP tool protocol + ); + return convertToolResult(result); + } catch (err) { + // Propagate AbortError so the SDK runner can handle cancellation. + if (err instanceof Error && err.name === "AbortError") { + return { + content: [{ type: "text", text: `Tool "${normalizedName}" was aborted.` }], + isError: true, + }; + } + + const message = err instanceof Error ? err.message : String(err); + log.error(`Tool ${normalizedName} failed: ${message}`); + if (err instanceof Error && err.stack) { + log.debug(`Tool ${normalizedName} stack:\n${err.stack}`); + } + + return { + content: [{ type: "text", text: `Tool error (${normalizedName}): ${message}` }], + isError: true, + }; + } + }; +} + +// --------------------------------------------------------------------------- +// MCP Server loading (dynamic import — SDK is optional) +// --------------------------------------------------------------------------- + +let cachedMcpServerConstructor: McpServerConstructor | undefined; + +/** + * Dynamically load the McpServer class from the MCP SDK. + * + * The MCP SDK (`@modelcontextprotocol/sdk`) is a transitive dependency of + * `@anthropic-ai/claude-agent-sdk`. When the Claude Agent SDK is installed, + * the MCP SDK is available. We import it lazily to avoid build-time deps. + */ +async function loadMcpServerClass(): Promise { + if (cachedMcpServerConstructor) return cachedMcpServerConstructor; + + // The MCP SDK exports McpServer from this path. + const moduleName: string = "@modelcontextprotocol/sdk/server/mcp.js"; + try { + const mod = (await import(moduleName)) as { McpServer?: McpServerConstructor }; + if (!mod.McpServer || typeof mod.McpServer !== "function") { + throw new Error("McpServer class not found in @modelcontextprotocol/sdk"); + } + cachedMcpServerConstructor = mod.McpServer; + return mod.McpServer; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + throw new Error( + `Failed to load MCP SDK. Ensure @anthropic-ai/claude-agent-sdk is installed ` + + `(it includes @modelcontextprotocol/sdk as a dependency).\n\nError: ${message}`, + ); + } +} + +// --------------------------------------------------------------------------- +// Tool name utilities +// --------------------------------------------------------------------------- + +/** MCP tool naming convention: mcp__{server}__{tool} */ +export function mcpToolName(serverName: string, toolName: string): string { + return `mcp__${serverName}__${toolName}`; +} + +/** Build the allowedTools list for the Claude Agent SDK from bridged tools. */ +export function buildMcpAllowedTools(serverName: string, tools: AnyAgentTool[]): string[] { + return tools.map((t) => mcpToolName(serverName, t.name)); +} + +// --------------------------------------------------------------------------- +// Main bridge: Moltbot tools → MCP server config +// --------------------------------------------------------------------------- + +export type BridgeOptions = { + /** MCP server name (used in mcp__{name}__{tool} naming). */ + name: string; + /** Moltbot tools to bridge. These should already be policy-filtered. */ + tools: AnyAgentTool[]; + /** Optional shared abort signal for all tool executions. */ + abortSignal?: AbortSignal; +}; + +export type BridgeResult = { + /** MCP server config ready for `query({ options: { mcpServers } })`. */ + serverConfig: McpSdkServerConfig; + /** Pre-built `allowedTools` list for the SDK options. */ + allowedTools: string[]; + /** Number of tools registered. */ + toolCount: number; + /** Tool names that were registered. */ + registeredTools: string[]; + /** Tool names that were skipped (e.g., schema extraction failed). */ + skippedTools: string[]; +}; + +/** + * Bridge Moltbot tools into an in-process MCP server config for the Claude Agent SDK. + * + * This is the main entry point. It: + * 1. Dynamically loads the MCP SDK's McpServer class. + * 2. For each Moltbot tool, extracts JSON Schema, wraps the handler, and registers. + * 3. Returns a config object ready for `query({ options: { mcpServers } })`. + * + * No business logic is duplicated — each tool's `execute()` is called directly. + */ +export async function bridgeMoltbotToolsToMcpServer(options: BridgeOptions): Promise { + const McpServer = await loadMcpServerClass(); + const server: McpServerLike = new McpServer({ + name: options.name, + version: "1.0.0", + }); + + const registered: string[] = []; + const skipped: string[] = []; + + for (const tool of options.tools) { + const toolName = tool.name; + if (!toolName?.trim()) { + log.debug("Skipping tool with empty name"); + skipped.push("(unnamed)"); + continue; + } + + try { + const jsonSchema = extractJsonSchema(tool); + const handler = wrapToolHandler(tool, options.abortSignal); + + server.tool(toolName, tool.description ?? `Moltbot tool: ${toolName}`, jsonSchema, handler); + + registered.push(toolName); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log.error(`Failed to register tool "${toolName}": ${message}`); + skipped.push(toolName); + } + } + + const serverConfig: McpSdkServerConfig = { + type: "sdk", + name: options.name, + instance: server, + }; + + return { + serverConfig, + allowedTools: buildMcpAllowedTools(options.name, options.tools), + toolCount: registered.length, + registeredTools: registered, + skippedTools: skipped, + }; +} + +// --------------------------------------------------------------------------- +// Convenience: bridge without async (for tests / when McpServer is provided) +// --------------------------------------------------------------------------- + +/** + * Synchronous variant for when the McpServer class is already available. + * Useful in tests where you can provide a mock McpServer constructor. + */ +export function bridgeMoltbotToolsSync( + options: BridgeOptions & { McpServer: McpServerConstructor }, +): BridgeResult { + const server: McpServerLike = new options.McpServer({ + name: options.name, + version: "1.0.0", + }); + + const registered: string[] = []; + const skipped: string[] = []; + + for (const tool of options.tools) { + const toolName = tool.name; + if (!toolName?.trim()) { + skipped.push("(unnamed)"); + continue; + } + + try { + const jsonSchema = extractJsonSchema(tool); + const handler = wrapToolHandler(tool, options.abortSignal); + server.tool(toolName, tool.description ?? `Moltbot tool: ${toolName}`, jsonSchema, handler); + registered.push(toolName); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log.error(`Failed to register tool "${toolName}": ${message}`); + skipped.push(toolName); + } + } + + return { + serverConfig: { type: "sdk", name: options.name, instance: server }, + allowedTools: buildMcpAllowedTools(options.name, options.tools), + toolCount: registered.length, + registeredTools: registered, + skippedTools: skipped, + }; +} + +/** + * Reset the cached MCP server constructor (for testing). + */ +export function resetMcpServerCache(): void { + cachedMcpServerConstructor = undefined; +} diff --git a/src/agents/claude-agent-sdk/tool-bridge.types.ts b/src/agents/claude-agent-sdk/tool-bridge.types.ts new file mode 100644 index 000000000..bcbd9c84c --- /dev/null +++ b/src/agents/claude-agent-sdk/tool-bridge.types.ts @@ -0,0 +1,117 @@ +/** + * Local type definitions for interop with the Claude Agent SDK and MCP SDK. + * + * These are compatible with the runtime shapes but do not require the packages + * to be installed at build time (both are optional, lazy-loaded integrations). + */ + +// --------------------------------------------------------------------------- +// MCP CallToolResult (from @modelcontextprotocol/sdk/types.js) +// --------------------------------------------------------------------------- + +export type McpTextContent = { + type: "text"; + text: string; +}; + +export type McpImageContent = { + type: "image"; + data: string; + mimeType: string; +}; + +export type McpResourceContent = { + type: "resource"; + uri: string; + text?: string; + blob?: string; +}; + +export type McpContentBlock = McpTextContent | McpImageContent | McpResourceContent; + +export type McpCallToolResult = { + content: McpContentBlock[]; + isError?: boolean; +}; + +// --------------------------------------------------------------------------- +// MCP Server config shapes (passed to SDK query() → options.mcpServers) +// --------------------------------------------------------------------------- + +/** In-process MCP server created via createSdkMcpServer() or new McpServer(). */ +export type McpSdkServerConfig = { + type: "sdk"; + name: string; + /** The McpServer instance — typed as `unknown` to avoid importing the class. */ + instance: unknown; +}; + +// --------------------------------------------------------------------------- +// Claude Agent SDK query() options (subset we use) +// --------------------------------------------------------------------------- + +export type SdkRunnerQueryOptions = { + /** MCP servers to expose to the agent. */ + mcpServers?: Record; + /** Tool allow list (pattern: "mcp__{server}__{tool}"). */ + allowedTools?: string[]; + /** Tool deny list. */ + disallowedTools?: string[]; + /** Built-in Claude Code tools to enable (or a preset). */ + tools?: string[] | { type: "preset"; preset: string }; + /** Environment variables for the SDK runtime (auth, base URL, timeout). */ + env?: Record; + /** Permission mode. */ + permissionMode?: string; + /** Working directory. */ + cwd?: string; + /** System prompt configuration. */ + systemPrompt?: string | { type: "preset"; preset: string }; + /** Max agent turns before stopping. */ + maxTurns?: number; + /** Model to use. */ + model?: string; + /** Additional directories the agent can access. */ + additionalDirectories?: string[]; + /** Where to load Claude Code settings from ("project", etc.). */ + settingSources?: string[]; + /** Include partial message events in the SDK stream. */ + includePartialMessages?: boolean; + /** Claude Code hook callbacks. */ + hooks?: Record; + /** Path to the Claude Code executable to run. */ + pathToClaudeCodeExecutable?: string; +}; + +// --------------------------------------------------------------------------- +// Claude Agent SDK event shapes (defensive — events are untyped at runtime) +// --------------------------------------------------------------------------- + +export type SdkResultEvent = { + type: "result"; + subtype?: "success" | "error"; + result?: string; + error?: string; +}; + +// --------------------------------------------------------------------------- +// MCP Server tool registration (shape of McpServer.tool() from the SDK) +// --------------------------------------------------------------------------- + +/** + * Minimal interface for the McpServer class from `@modelcontextprotocol/sdk`. + * We only use the `tool()` registration method and the constructor. + */ +export interface McpServerLike { + tool( + name: string, + description: string, + inputSchema: Record, + handler: (args: Record) => Promise, + ): void; +} + +/** + * Constructor shape for new McpServer({ name, version }). + */ +export type McpServerConstructor = new (opts: { name: string; version: string }) => McpServerLike; diff --git a/src/agents/claude-agent-sdk/types.ts b/src/agents/claude-agent-sdk/types.ts new file mode 100644 index 000000000..6005040dc --- /dev/null +++ b/src/agents/claude-agent-sdk/types.ts @@ -0,0 +1,141 @@ +/** + * Type definitions for the Claude Agent SDK integration. + */ + +import type { MoltbotConfig } from "../../config/config.js"; +import type { CcSdkModelTiers } from "../../config/types.agents.js"; + +/** Provider environment variables for SDK authentication and model config. */ +export type SdkProviderEnv = { + /** Anthropic API key or OAuth token. */ + ANTHROPIC_API_KEY?: string; + /** OAuth access token (alternative to API key). */ + ANTHROPIC_AUTH_TOKEN?: string; + /** Custom base URL for API requests. */ + ANTHROPIC_BASE_URL?: string; + /** Use Bedrock backend. */ + CLAUDE_CODE_USE_BEDROCK?: string; + /** Use Vertex AI backend. */ + CLAUDE_CODE_USE_VERTEX?: string; + /** Model for fast/simple tasks (Haiku tier). */ + ANTHROPIC_DEFAULT_HAIKU_MODEL?: string; + /** Model for balanced tasks (Sonnet tier). */ + ANTHROPIC_DEFAULT_SONNET_MODEL?: string; + /** Model for complex reasoning (Opus tier). */ + ANTHROPIC_DEFAULT_OPUS_MODEL?: string; +}; + +/** Provider configuration for SDK runner. */ +export type SdkProviderConfig = { + /** Human-readable provider name. */ + name: string; + /** Environment variables to set for authentication. */ + env: SdkProviderEnv; + /** Model override (if different from config). */ + model?: string; +}; + +/** Parameters for SDK runner execution. */ +export type SdkRunnerParams = { + /** Session identifier. */ + sessionId: string; + /** Session key for routing. */ + sessionKey?: string; + /** Path to session file for conversation history. */ + sessionFile: string; + /** Agent workspace directory. */ + workspaceDir: string; + /** Agent data directory. */ + agentDir?: string; + /** Moltbot configuration. */ + config?: MoltbotConfig; + /** User prompt/message. */ + prompt: string; + /** Model to use (provider/model format). */ + model?: string; + /** Provider configuration. */ + providerConfig?: SdkProviderConfig; + /** Timeout in milliseconds. */ + timeoutMs: number; + /** Run identifier for event correlation. */ + runId: string; + /** Abort signal for cancellation. */ + abortSignal?: AbortSignal; + /** Extra system prompt to append. */ + extraSystemPrompt?: string; + /** Enable Claude Code hooks. */ + hooksEnabled?: boolean; + /** Additional SDK options. */ + sdkOptions?: Record; + /** 3-tier model configuration (haiku/sonnet/opus). */ + modelTiers?: CcSdkModelTiers; + /** Called for agent lifecycle events. */ + onAgentEvent?: (evt: { stream: string; data: Record }) => void; +}; + +/** SDK event types from the Claude Agent SDK. */ +export type SdkEventType = + | "assistant_message" + | "tool_use" + | "tool_result" + | "error" + | "done" + | "thinking" + | "text"; + +/** Base SDK event structure. */ +export type SdkEvent = { + type: SdkEventType; + data?: unknown; +}; + +/** SDK text/content event. */ +export type SdkTextEvent = SdkEvent & { + type: "text" | "assistant_message"; + data: { + text?: string; + content?: string; + }; +}; + +/** SDK tool use event. */ +export type SdkToolUseEvent = SdkEvent & { + type: "tool_use"; + data: { + name: string; + input: Record; + id?: string; + }; +}; + +/** SDK tool result event. */ +export type SdkToolResultEvent = SdkEvent & { + type: "tool_result"; + data: { + tool_use_id: string; + content: string; + is_error?: boolean; + }; +}; + +/** SDK error event. */ +export type SdkErrorEvent = SdkEvent & { + type: "error"; + data: { + message: string; + code?: string; + }; +}; + +/** SDK done event. */ +export type SdkDoneEvent = SdkEvent & { + type: "done"; + data?: { + usage?: { + input_tokens?: number; + output_tokens?: number; + cache_read_input_tokens?: number; + cache_creation_input_tokens?: number; + }; + }; +}; diff --git a/src/agents/main-agent-runtime-factory.ts b/src/agents/main-agent-runtime-factory.ts new file mode 100644 index 000000000..8046a68c1 --- /dev/null +++ b/src/agents/main-agent-runtime-factory.ts @@ -0,0 +1,94 @@ +/** + * Main agent runtime factory. + * + * Resolves and creates the appropriate agent runtime based on configuration. + */ + +import type { MoltbotConfig } from "../config/config.js"; +import type { AgentRuntimeKind } from "../config/types.agent-defaults.js"; +import type { AgentRuntime } from "./agent-runtime.js"; +import { resolveAgentConfig } from "./agent-scope.js"; +import { createPiAgentRuntime } from "./pi-agent-runtime.js"; +import { createSubsystemLogger } from "../logging/subsystem.js"; + +const log = createSubsystemLogger("agents/runtime-factory"); + +/** Default runtime when none is configured. */ +const DEFAULT_RUNTIME: AgentRuntimeKind = "pi"; + +/** + * Resolve the runtime kind for an agent. + * + * Resolution order: + * 1. Per-agent runtime (agents.list[].runtime) + * 2. Default runtime (agents.defaults.runtime) + * 3. Fallback to "pi" + */ +export function resolveAgentRuntimeKind(config: MoltbotConfig, agentId: string): AgentRuntimeKind { + // Check per-agent config first + const agentConfig = resolveAgentConfig(config, agentId); + if (agentConfig?.runtime === "pi" || agentConfig?.runtime === "ccsdk") { + return agentConfig.runtime; + } + + // Fall back to defaults + const defaultRuntime = config.agents?.defaults?.runtime; + if (defaultRuntime === "pi" || defaultRuntime === "ccsdk") { + return defaultRuntime; + } + + return DEFAULT_RUNTIME; +} + +/** + * Create an agent runtime for the specified agent. + * + * @param config - Moltbot configuration + * @param agentId - Agent identifier + * @returns The appropriate AgentRuntime instance + */ +export async function createAgentRuntime( + config: MoltbotConfig, + agentId: string, +): Promise { + const runtimeKind = resolveAgentRuntimeKind(config, agentId); + + log.debug("Creating agent runtime", { agentId, runtime: runtimeKind }); + + if (runtimeKind === "ccsdk") { + // Dynamically import to avoid loading SDK when not needed + const { createCcSdkAgentRuntime, isSdkAvailable } = await import("./claude-agent-sdk/index.js"); + + if (!isSdkAvailable()) { + log.warn("CCSDK runtime requested but SDK not available, falling back to Pi runtime", { + agentId, + }); + return createPiAgentRuntime(); + } + + // Get CCSDK-specific config from the agent entry + const agentConfig = resolveAgentConfig(config, agentId); + + return createCcSdkAgentRuntime({ + config, + ccsdkConfig: agentConfig?.ccsdk, + }); + } + + // Default to Pi runtime + return createPiAgentRuntime(); +} + +/** + * Check if the Claude Code SDK runtime is available. + * + * This can be used to conditionally show CCSDK-related options in the UI. + */ +export async function isCcSdkRuntimeAvailable(): Promise { + try { + const { isSdkAvailable } = await import("./claude-agent-sdk/index.js"); + return isSdkAvailable(); + } catch { + return false; + } +} diff --git a/src/agents/pi-agent-runtime.ts b/src/agents/pi-agent-runtime.ts new file mode 100644 index 000000000..6a7490975 --- /dev/null +++ b/src/agents/pi-agent-runtime.ts @@ -0,0 +1,68 @@ +/** + * Pi Agent runtime implementation. + * + * Wraps the existing runEmbeddedPiAgent function to conform to the AgentRuntime interface. + */ + +import type { AgentRuntime, AgentRuntimeRunParams, AgentRuntimeResult } from "./agent-runtime.js"; +import { runEmbeddedPiAgent } from "./pi-embedded.js"; + +/** + * Create a Pi Agent runtime instance. + * + * The Pi Agent runtime is the default backend that uses the Pi AI SDK + * for model execution with moltbot's tool system. + */ +export function createPiAgentRuntime(): AgentRuntime { + return { + kind: "pi", + displayName: "Pi Agent", + + async run(params: AgentRuntimeRunParams): Promise { + return runEmbeddedPiAgent({ + sessionId: params.sessionId, + sessionKey: params.sessionKey, + sessionFile: params.sessionFile, + workspaceDir: params.workspaceDir, + agentDir: params.agentDir, + config: params.config, + skillsSnapshot: params.skillsSnapshot, + prompt: params.prompt, + images: params.images, + provider: params.provider, + model: params.model, + authProfileId: params.authProfileId, + authProfileIdSource: params.authProfileIdSource, + thinkLevel: params.thinkLevel, + verboseLevel: params.verboseLevel, + timeoutMs: params.timeoutMs, + runId: params.runId, + lane: params.lane, + abortSignal: params.abortSignal, + extraSystemPrompt: params.extraSystemPrompt, + streamParams: params.streamParams, + // Messaging context + messageChannel: params.messageChannel, + agentAccountId: params.agentAccountId, + messageTo: params.messageTo, + messageThreadId: params.messageThreadId, + groupId: params.groupId, + groupChannel: params.groupChannel, + groupSpace: params.groupSpace, + spawnedBy: params.spawnedBy, + currentChannelId: params.currentChannelId, + currentThreadTs: params.currentThreadTs, + replyToMode: params.replyToMode, + hasRepliedRef: params.hasRepliedRef, + // Callbacks + onPartialReply: params.onPartialReply, + onAssistantMessageStart: params.onAssistantMessageStart, + onBlockReply: params.onBlockReply, + onBlockReplyFlush: params.onBlockReplyFlush, + onReasoningStream: params.onReasoningStream, + onToolResult: params.onToolResult, + onAgentEvent: params.onAgentEvent, + }); + }, + }; +} diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index 9c6ce0211..22808976c 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -12,6 +12,9 @@ import type { } from "./types.sandbox.js"; import type { MemorySearchConfig } from "./types.tools.js"; +/** Agent runtime backend discriminant. */ +export type AgentRuntimeKind = "pi" | "ccsdk"; + export type AgentModelEntryConfig = { alias?: string; /** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */ @@ -92,6 +95,8 @@ export type CliBackendConfig = { }; export type AgentDefaultsConfig = { + /** Agent runtime backend (pi or sdk). Default: "pi". */ + runtime?: AgentRuntimeKind; /** Primary model and fallbacks (provider/model). */ model?: AgentModelListConfig; /** Optional image-capable model and fallbacks (provider/model). */ diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index f083c1897..cf6ad374b 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -1,4 +1,4 @@ -import type { AgentDefaultsConfig } from "./types.agent-defaults.js"; +import type { AgentDefaultsConfig, AgentRuntimeKind } from "./types.agent-defaults.js"; import type { HumanDelayConfig, IdentityConfig } from "./types.base.js"; import type { GroupChatConfig } from "./types.messages.js"; import type { @@ -21,6 +21,10 @@ export type AgentConfig = { id: string; default?: boolean; name?: string; + /** Agent runtime backend (pi or ccsdk). Overrides agents.defaults.runtime. */ + runtime?: AgentRuntimeKind; + /** Claude Code SDK configuration when runtime is "ccsdk". */ + ccsdk?: AgentCcSdkConfig; workspace?: string; agentDir?: string; model?: AgentModelConfig; @@ -62,6 +66,26 @@ export type AgentConfig = { tools?: AgentToolsConfig; }; +/** Model tier configuration for Claude Code SDK. */ +export type CcSdkModelTiers = { + /** Model for fast/simple tasks (maps to ANTHROPIC_DEFAULT_HAIKU_MODEL). */ + haiku?: string; + /** Model for balanced tasks (maps to ANTHROPIC_DEFAULT_SONNET_MODEL). */ + sonnet?: string; + /** Model for complex reasoning (maps to ANTHROPIC_DEFAULT_OPUS_MODEL). */ + opus?: string; +}; + +/** Claude Code SDK runtime configuration. */ +export type AgentCcSdkConfig = { + /** Enable Claude Code lifecycle hooks (pre-tool, post-tool, etc.). */ + hooksEnabled?: boolean; + /** Additional SDK options passed to the runner. */ + options?: Record; + /** 3-tier model configuration for Claude Code SDK. */ + models?: CcSdkModelTiers; +}; + export type AgentsConfig = { defaults?: AgentDefaultsConfig; list?: AgentConfig[]; diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index a849078ed..aec9c6193 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { + AgentRuntimeKindSchema, HeartbeatSchema, MemorySearchSchema, SandboxBrowserSchema, @@ -13,8 +14,12 @@ import { HumanDelaySchema, } from "./zod-schema.core.js"; +// Re-export for consumers +export { AgentRuntimeKindSchema }; + export const AgentDefaultsSchema = z .object({ + runtime: AgentRuntimeKindSchema.optional(), model: z .object({ primary: z.string().optional(), diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index 7a63e307d..a58bd0c9c 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -413,11 +413,36 @@ export const AgentModelSchema = z.union([ }) .strict(), ]); +/** Agent runtime backend discriminant. */ +export const AgentRuntimeKindSchema = z.union([z.literal("pi"), z.literal("ccsdk")]); + +/** Model tier configuration for Claude Code SDK. */ +export const CcSdkModelTiersSchema = z + .object({ + haiku: z.string().optional(), + sonnet: z.string().optional(), + opus: z.string().optional(), + }) + .strict() + .optional(); + +/** Claude Code SDK runtime configuration schema. */ +export const AgentCcSdkConfigSchema = z + .object({ + hooksEnabled: z.boolean().optional(), + options: z.record(z.string(), z.unknown()).optional(), + models: CcSdkModelTiersSchema, + }) + .strict() + .optional(); + export const AgentEntrySchema = z .object({ id: z.string(), default: z.boolean().optional(), name: z.string().optional(), + runtime: AgentRuntimeKindSchema.optional(), + ccsdk: AgentCcSdkConfigSchema, workspace: z.string().optional(), agentDir: z.string().optional(), model: AgentModelSchema.optional(),