feat: add AgentRuntime abstraction for Claude Agent SDK Support

* added AgentRuntime interface and implementation for PiAgent and ClaudeCode SDK (ccsdk)
* support for Claude Code Max and z.AI Coding Plan subscriptions (which are limited to Claude Code)
* tool bridge to ensure that the Claude Code SDK agent loop is capable of invoking the same tools as pi-agent

I used Claude Opus 4.5 to help craft this PR.
This commit is contained in:
David Garson 2026-01-28 09:43:40 -07:00
parent 7800194f33
commit 0a2e444b21
18 changed files with 1839 additions and 88 deletions

View File

@ -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": {

253
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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<void>;
/** Called when the assistant message starts. */
onAssistantMessageStart?: () => void | Promise<void>;
/** Called for block-level reply delivery. */
onBlockReply?: (payload: {
text?: string;
mediaUrls?: string[];
audioAsVoice?: boolean;
replyToId?: string;
replyToTag?: boolean;
replyToCurrent?: boolean;
}) => void | Promise<void>;
/** Called when block replies should be flushed. */
onBlockReplyFlush?: () => void | Promise<void>;
/** Called for reasoning/thinking stream events. */
onReasoningStream?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
/** Called when a tool result is available. */
onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
/** Called for agent lifecycle and internal events. */
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => 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<AgentRuntimeResult>;
}

View File

@ -17,6 +17,8 @@ type AgentEntry = NonNullable<NonNullable<MoltbotConfig["agents"]>["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:

View File

@ -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";

View File

@ -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: {},
};
}

View File

@ -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<AgentRuntimeResult> {
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,
});
},
};
}

View File

@ -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<ClaudeAgentSdkModule> {
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;
}

View File

@ -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<AgentRuntimeResult> {
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<string, unknown>,
});
},
});
// 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 };
}

View File

@ -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<string, unknown> {
const schema = tool.parameters;
if (!schema || typeof schema !== "object") {
return { type: "object", properties: {} };
}
try {
return JSON.parse(JSON.stringify(schema)) as Record<string, unknown>;
} 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 `<tool-details>` text block
* - Empty results "(no output)" fallback
*/
export function convertToolResult(result: AgentToolResult<unknown>): 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: `<tool-details>\n${serialized}\n</tool-details>`,
});
} 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<CallToolResult>`
*
* 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<string, unknown>) => Promise<McpCallToolResult> {
const normalizedName = normalizeToolName(tool.name);
return async (args: Record<string, unknown>): Promise<McpCallToolResult> => {
// 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<McpServerConstructor> {
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<BridgeResult> {
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;
}

View File

@ -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<string, McpSdkServerConfig>;
/** 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<string, string>;
/** 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<string, unknown>;
/** 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<string, unknown>,
handler: (args: Record<string, unknown>) => Promise<McpCallToolResult>,
): void;
}
/**
* Constructor shape for new McpServer({ name, version }).
*/
export type McpServerConstructor = new (opts: { name: string; version: string }) => McpServerLike;

View File

@ -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<string, unknown>;
/** 3-tier model configuration (haiku/sonnet/opus). */
modelTiers?: CcSdkModelTiers;
/** Called for agent lifecycle events. */
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => 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<string, unknown>;
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;
};
};
};

View File

@ -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<AgentRuntime> {
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<boolean> {
try {
const { isSdkAvailable } = await import("./claude-agent-sdk/index.js");
return isSdkAvailable();
} catch {
return false;
}
}

View File

@ -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<AgentRuntimeResult> {
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,
});
},
};
}

View File

@ -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). */

View File

@ -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<string, unknown>;
/** 3-tier model configuration for Claude Code SDK. */
models?: CcSdkModelTiers;
};
export type AgentsConfig = {
defaults?: AgentDefaultsConfig;
list?: AgentConfig[];

View File

@ -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(),

View File

@ -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(),