diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index dc2554b88d..660d2483d9 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -70,9 +70,11 @@ "typescript-eslint": "^8.56.1" }, "dependencies": { + "@ai-sdk/openai": "^1.3.22", "@bufbuild/protobuf": "^2.11.0", "@types/qs": "^6.15.0", "abort-controller-x": "^0.4.3", + "ai": "^4.3.16", "axios": "^1.13.5", "long": "^5.3.1", "nice-grpc": "^2.1.14", @@ -80,6 +82,7 @@ "protobufjs": "^7.4.0", "qs": "^6.14.2", "semver": "^7.7.4", + "tsx": "^4.21.0", "yaml": "^2.8.2", "zod": "^3.24.2", "zod-to-json-schema": "^3.24.1" diff --git a/sdks/typescript/pnpm-lock.yaml b/sdks/typescript/pnpm-lock.yaml index 39b33fd0ca..0589fea122 100644 --- a/sdks/typescript/pnpm-lock.yaml +++ b/sdks/typescript/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: .: dependencies: + '@ai-sdk/openai': + specifier: ^1.3.22 + version: 1.3.24(zod@3.25.76) '@bufbuild/protobuf': specifier: ^2.11.0 version: 2.11.0 @@ -24,6 +27,9 @@ importers: abort-controller-x: specifier: ^0.4.3 version: 0.4.3 + ai: + specifier: ^4.3.16 + version: 4.3.19(react@19.2.4)(zod@3.25.76) axios: specifier: ^1.13.5 version: 1.13.6 @@ -45,6 +51,9 @@ importers: semver: specifier: ^7.7.4 version: 7.7.4 + tsx: + specifier: ^4.21.0 + version: 4.21.0 yaml: specifier: ^2.8.2 version: 2.8.2 @@ -152,6 +161,38 @@ importers: packages: + '@ai-sdk/openai@1.3.24': + resolution: {integrity: sha512-GYXnGJTHRTZc4gJMSmFRgEQudjqd4PUN0ZjQhPwOAYH1yOAvQoG/Ikqs+HyISRbLPCrhbZnPKCNHuRU4OfpW0Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -337,6 +378,162 @@ packages: '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -689,6 +886,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -934,6 +1134,16 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ai@4.3.19: + resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} @@ -1119,6 +1329,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -1251,6 +1465,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -1264,6 +1482,9 @@ packages: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1343,6 +1564,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2042,6 +2268,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2054,6 +2283,11 @@ packages: engines: {node: '>=6'} hasBin: true + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2397,6 +2631,10 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + engines: {node: '>=0.10.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -2466,6 +2704,9 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2609,6 +2850,11 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + synckit@0.11.12: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2627,6 +2873,10 @@ packages: thread-stream@3.1.0: resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2718,6 +2968,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2813,6 +3068,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -2910,6 +3170,40 @@ packages: snapshots: + '@ai-sdk/openai@1.3.24(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.76 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@19.2.4)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + react: 19.2.4 + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.76 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.12 @@ -3126,6 +3420,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)': dependencies: eslint: 10.0.3 @@ -3427,8 +3799,7 @@ snapshots: '@opentelemetry/api': 1.9.0 optional: true - '@opentelemetry/api@1.9.0': - optional: true + '@opentelemetry/api@1.9.0': {} '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: @@ -3613,6 +3984,8 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@types/diff-match-patch@1.0.36': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} @@ -3834,6 +4207,18 @@ snapshots: agent-base@7.1.4: {} + ai@4.3.19(react@19.2.4)(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/react': 1.2.12(react@19.2.4)(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.76 + optionalDependencies: + react: 19.2.4 + ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -4079,6 +4464,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + char-regex@1.0.2: {} chownr@3.0.0: {} @@ -4198,12 +4585,16 @@ snapshots: delayed-stream@1.0.0: {} + dequal@2.0.3: {} + detect-libc@1.0.3: {} detect-libc@2.0.4: {} detect-newline@3.1.0: {} + diff-match-patch@1.0.5: {} + diff-sequences@29.6.3: {} diff@8.0.3: {} @@ -4333,6 +4724,35 @@ snapshots: is-symbol: 1.1.1 optional: true + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + escalade@3.2.0: {} escape-string-regexp@2.0.0: {} @@ -5297,6 +5717,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -5306,6 +5728,12 @@ snapshots: json5@2.2.3: {} + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.6.2 + diff-match-patch: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5640,6 +6068,8 @@ snapshots: react-is@18.3.1: {} + react@19.2.4: {} + real-require@0.2.0: {} reflect.getprototypeof@1.0.10: @@ -5727,6 +6157,8 @@ snapshots: safe-stable-stringify@2.5.0: {} + secure-json-parse@2.7.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -5888,6 +6320,12 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swr@2.4.1(react@19.2.4): + dependencies: + dequal: 2.0.3 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + synckit@0.11.12: dependencies: '@pkgr/core': 0.2.9 @@ -5915,6 +6353,8 @@ snapshots: dependencies: real-require: 0.2.0 + throttleit@2.1.0: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) @@ -6007,6 +6447,13 @@ snapshots: tslib@2.8.1: optional: true + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6139,6 +6586,10 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: diff --git a/sdks/typescript/src/clients/hatchet-client/client-config.ts b/sdks/typescript/src/clients/hatchet-client/client-config.ts index d2bd765e98..4d3daa31a0 100644 --- a/sdks/typescript/src/clients/hatchet-client/client-config.ts +++ b/sdks/typescript/src/clients/hatchet-client/client-config.ts @@ -2,6 +2,7 @@ import { ChannelCredentials } from 'nice-grpc'; import { z } from 'zod'; import type { Context } from '@hatchet/v1/client/worker/context'; import { Logger, LogLevel } from '@util/logger'; +import { LanguageModel } from 'ai'; const ClientTLSConfigSchema = z.object({ tls_strategy: z.enum(['tls', 'mtls', 'none']).optional(), @@ -136,5 +137,6 @@ export type ClientConfig = Omit< } & { logger: LogConstructor; middleware?: TaskMiddleware; + defaultLanguageModel?: LanguageModel; }; export type ClientTLSConfig = z.infer; diff --git a/sdks/typescript/src/v1/client/client.ts b/sdks/typescript/src/v1/client/client.ts index a892488435..a6e9400e19 100644 --- a/sdks/typescript/src/v1/client/client.ts +++ b/sdks/typescript/src/v1/client/client.ts @@ -66,6 +66,28 @@ import { } from '../types'; import { AdminClient } from './admin'; import { DurableContext } from './worker/context'; +import { CreateToolboxOpts, Toolbox, ToolDeclaration } from "./toolbox"; +import { LanguageModel } from 'ai'; + +export interface AgentDeclaration< + InputSchema extends z.ZodType, + OutputSchema extends z.ZodType +> extends TaskWorkflowDeclaration, z.infer> { + inputSchema: InputSchema; + outputSchema: OutputSchema; + description: string; +} + +export abstract class Registerable { + abstract get register(): BaseWorkflowDeclaration[]; +} + +type BaseOrRegisterable = BaseWorkflowDeclaration | Registerable; + +interface StartOptions extends CreateWorkerOpts { + name?: string; + register?: Array | Array>; +} type MergeIfNonEmpty> = keyof Extra extends never ? Base @@ -92,6 +114,9 @@ export class HatchetClient< private _axiosConfig: AxiosRequestConfig | undefined; private _clientFactory: ClientFactory; private _credentials: ChannelCredentials; + private toolboxes: Map> = new Map(); + private workflowToFilePath: Map = new Map(); + private registry: Map = new Map(); /** * @deprecated v0 client will be removed in a future release, please upgrade to v1 @@ -118,6 +143,7 @@ export class HatchetClient< tenantId: string; logger: Logger; + defaultLanguageModel: LanguageModel | undefined; _isV1: boolean | undefined = true; @@ -130,12 +156,14 @@ export class HatchetClient< * @param config - Optional configuration for the client * @param options - Optional client options * @param axiosConfig - Optional Axios configuration for HTTP requests + * @param defaultLanguageModel * @internal */ constructor( config?: Partial, options?: HatchetClientOptions, - axiosConfig?: AxiosRequestConfig + axiosConfig?: AxiosRequestConfig, + defaultLanguageModel?: LanguageModel ) { try { const loaded = ConfigLoader.loadClientConfig(config, { @@ -153,6 +181,7 @@ export class HatchetClient< const clientConfig = { ...valid, logger: logConstructor, + defaultLanguageModel: defaultLanguageModel, }; this._config = clientConfig; @@ -174,6 +203,7 @@ export class HatchetClient< this._options = options; this._axiosConfig = axiosConfig; + this.defaultLanguageModel = this.config.defaultLanguageModel; } catch (e) { if (e instanceof z.ZodError) { throw new Error(`Invalid client config: ${e.message}`, { cause: e }); @@ -730,4 +760,122 @@ export class HatchetClient< runRef = any>(id: string): WorkflowRunRef { return this.runs.runRef(id); } + + /** + * Creates a new agent with Zod schema validation. + * @template InputSchema The Zod schema for input validation + * @template OutputSchema The Zod schema for output validation + * @param options Agent configuration options including input and output schemas + * @returns An AgentDeclaration instance + */ + agent< + InputSchema extends z.ZodType, + OutputSchema extends z.ZodType + >( + options: { + name: string; + description: string; + inputSchema: InputSchema; + outputSchema: OutputSchema; + fn: ( + input: z.infer, + ctx: DurableContext> + ) => Promise>; + } & Omit, z.infer>, 'fn'> + ): AgentDeclaration; + + /** + * Implementation of the agent method. + */ + agent(options: any): AgentDeclaration { + const { inputSchema, outputSchema, fn, description, ...rest } = options; + + const wrappedFn = async (input: any, ctx?: any) => { + const validatedInput = inputSchema.parse(input); + const result = await fn(validatedInput, ctx); + return outputSchema.parse(result); + }; + + const declaration = CreateDurableTaskWorkflow( + { ...rest, fn: wrappedFn }, + this + ) as AgentDeclaration; + declaration.inputSchema = inputSchema; + declaration.outputSchema = outputSchema; + declaration.description = description; + const agent = declaration as AgentDeclaration; + this.registry.set(agent.name, agent); + return agent; + } + + /** + * Creates a new tool with Zod schema validation. + * @template Name The literal type of the tool name + * @template InputSchema The Zod schema for input validation + * @template OutputSchema The Zod schema for output validation + * @param options Tool configuration options including input and output schemas + * @returns A ToolDeclaration instance + */ + tool( + options: { + name: Name; + description: string; + inputSchema: InputSchema; + outputSchema: OutputSchema; + fn: (input: z.infer, ctx?: any) => Promise>; + } & Omit, z.infer>, 'fn'> + ): ToolDeclaration & { name: Name }; + + /** + * Implementation of the tool method. + */ + tool(options: any): ToolDeclaration & { name: string } { + const { inputSchema, outputSchema, fn, description, ...rest } = options; + + // Wrap the function to validate input and output + const wrappedFn = async (input: any, ctx?: any) => { + const validatedInput = inputSchema.parse(input); + const result = await fn(validatedInput, ctx); + return outputSchema.parse(result); + }; + + const declaration = CreateTaskWorkflow({ ...rest, fn: wrappedFn }, this) as ToolDeclaration; + + // Add schema information to the declaration + declaration.inputSchema = inputSchema; + declaration.outputSchema = outputSchema; + declaration.description = description; + + // Preserve the literal type of the `name` field through a cast so callers + // can discriminate on `name` later. + const tool = declaration as typeof declaration & { name: typeof options.name }; + this.registry.set(tool.name, tool); + return tool; + } + + /** + * Creates a new toolbox. + * @param options The toolbox configuration options + * @returns A Toolbox instance + */ + toolbox>>( + options: CreateToolboxOpts + ): Toolbox { + const toolbox = new Toolbox(options, this); + // Store the toolbox with a generated key based on tool names + const toolboxKey = Array.from(options.tools) + .map((t) => t.name) + .sort() + .join(':'); + this.toolboxes.set(toolboxKey, toolbox); + this.registry.set(toolboxKey, toolbox); + return toolbox; + } + + /** + * Gets a toolbox by its key (used internally) + */ + _getToolbox(key: string): Toolbox | undefined { + return this.toolboxes.get(key); + } } diff --git a/sdks/typescript/src/v1/client/toolbox.ts b/sdks/typescript/src/v1/client/toolbox.ts new file mode 100644 index 0000000000..df44a26e65 --- /dev/null +++ b/sdks/typescript/src/v1/client/toolbox.ts @@ -0,0 +1,258 @@ +import { z } from 'zod'; +import { generateText, zodSchema } from 'ai'; +import { TaskWorkflowDeclaration } from '../declaration'; +import { HatchetClient, Registerable } from './client'; + +export interface ToolDeclaration< + InputSchema extends z.ZodType, + OutputSchema extends z.ZodType, +> extends TaskWorkflowDeclaration, z.infer> { + inputSchema: InputSchema; + outputSchema: OutputSchema; + description: string; +} + +// Helper to pull the right ToolDeclaration from T by its `name` +type ToolByName< + T extends readonly ToolDeclaration[], + N extends T[number]['name'], +> = Extract; + +// Map over each name and give it the correct input/output +type TransformersFor[]> = { + [N in T[number]['name']]: ( + output: z.infer['outputSchema']>, + args: z.infer['inputSchema']> + ) => Promise; +}; + +type ToolResultMap[]> = { + [N in T[number]['name']]: { + name: N; + output: z.infer['outputSchema']>; + args: z.infer['inputSchema']>; + }; +}[T[number]['name']]; + +/** + * Options used when creating a `Toolbox` instance. + * + * @typeParam T - Immutable array of `ToolDeclaration`s that will populate the toolbox. + */ +export interface CreateToolboxOpts>> { + /** Array of tool declarations to register on the toolbox. */ + tools: T; +} + +export type ToolSet = { + [key: string]: { + /** + The schema of the input that the tool expects. The language model will use this to generate the input. + It is also used to validate the output of the language model. + Use descriptions to make the input understandable for the language model. + */ + parameters: any; + /** + An optional description of what the tool does. + Will be used by the language model to decide whether to use the tool. + Not used for provider-defined tools. + */ + description?: string; + }; +}; + +/** + * Input for the pick workflow. + */ +type PickInput = { + /** + * The prompt to use for the pick workflow. + */ + prompt: string; + + /** + * The maximum number of tools to allow the pick workflow to take. + */ + maxTools?: number; +}; + +/** + * Runtime helper that exposes a collection of Hatchet workflows as "tools" which can be + * automatically selected and executed by a language model using the OpenAI function-calling interface. + * + * The class wires the supplied tool declarations into the Hatchet runtime and also + * registers an internal `pick-tool` workflow that decides—at execution time—what tool(s) + * should be invoked to satisfy a natural-language prompt. + * + * @typeParam T - Immutable array of `ToolDeclaration`s that are made available in this toolbox. + */ +export class Toolbox>> implements Registerable { + private toolboxKey: string; + toolSetForAI: ToolSet; + + /** + * Creates a new `Toolbox`. + * + * @param props - Toolbox construction options containing the tool declarations. + * @param client - The `HatchetClient` client used to register and execute workflows. + */ + constructor( + private props: CreateToolboxOpts, + private client: HatchetClient + ) { + // Generate a key for this toolbox based on tool names + this.toolboxKey = Array.from(this.props.tools) + .map((t) => t.name) + .sort() + .join(':'); + + // Create toolset for AI SDK using the actual Zod schemas + this.toolSetForAI = Array.from(this.props.tools).reduce((acc, tool) => { + if (!tool.name || !tool.inputSchema) { + throw new Error(`Tool must have a name and inputSchema`); + } + + return { + ...acc, + [tool.name]: { + parameters: zodSchema(tool.inputSchema), + description: tool.description, + }, + }; + }, {}); + } + + /** + * Implements the `Registerable` interface so the toolbox and its supporting workflows + * can be registered with Hatchet. + * + * @returns All user-supplied tool declarations plus the internal `pick-tool` workflow. + */ + get register(): TaskWorkflowDeclaration[] { + return [...this.props.tools, pickToolFactory(this.client)]; + } + + /** + * Uses the language model to choose up to `maxTools` tools from this toolbox that best satisfy + * the provided prompt. Only the selection step is performed—no tool execution happens here. + * + * @param prompt - Natural-language description of what the caller wants to achieve. + * @param [maxTools] - Optional upper bound on how many tools may be selected (defaults to 1). + * + * @returns An array containing the chosen tool names together with the generated input arguments. + */ + async pick({ prompt, maxTools }: PickInput) { + const result = await pickToolFactory(this.client).run({ + prompt, + toolboxKey: this.toolboxKey, + maxTools, + }); + + return result.steps.flatMap((step) => + step.map((toolCall) => ({ + name: toolCall.toolName, + input: toolCall.args, + })) + ); + } + + /** + * Convenience method that first runs `pick` and then immediately executes the chosen tool(s). + * + * When `maxTools` is omitted or set to `1` the return value is a single `ToolResultMap` object. + * For values greater than `1` an array of `ToolResultMap` objects is returned in the order the + * tools were selected. + * + * @typeParam R - Inferred map of tool names to transformer functions derived from the toolbox. + * @param opts - Same input accepted by `pick`; the `maxTools` property determines the shape of the response. + */ + async pickAndRun>( + opts: Omit & { maxTools?: undefined } + ): Promise>; + + async pickAndRun>( + opts: PickInput & { maxTools: 1 } + ): Promise>; + + // Overload: `maxTools` > 1 ➜ array of results + async pickAndRun>( + opts: PickInput & { maxTools: number } + ): Promise[]>; + + // Implementation + async pickAndRun>(opts: PickInput): Promise { + // 1) pick tools + const picked = await this.pick(opts); + + // 2) run them + const results = await Promise.all( + picked.map(async ({ name, input }) => { + const tool = this.props.tools.find((t) => t.name === name); + if (!tool) { + throw new Error(`Tool "${name}" not found in toolbox`); + } + return await tool.run(input); + }) + ); + + // 3) zip back into the correctly typed union + const zipped = picked.map(({ name, input }, i) => ({ + name, + output: results[i], + args: input, + })) as ToolResultMap[]; + + // 4) Return single object or array based on opts.maxTools + if (opts.maxTools === undefined || opts.maxTools === 1) { + return zipped[0]; + } + + return zipped; + } + + /** + * Helper method to assert that the toolbox result is exhaustive + * for handling results in a switch statement. + * + * @param result - The result to assert is exhaustive. + * @returns The result. + * @throws An error if the result is not exhaustive. + */ + assertExhaustive(result: never): never { + throw new Error(`Unhandled toolbox result: ${(result as any).name}`); + } + + /** + * Gets the original tool declarations (used internally by pick-tool) + */ + getTools(): T { + return this.props.tools; + } +} + +type PickInputWithToolboxKey = PickInput & { + toolboxKey: string; +}; + +const pickToolFactory = (icepick: HatchetClient) => + icepick.task({ + name: 'pick-tool', + executionTimeout: '5m', + fn: async (input: PickInputWithToolboxKey) => { + // Get the toolbox from the client using the key + const toolbox = icepick._getToolbox(input.toolboxKey); + if (!toolbox) { + throw new Error(`Toolbox not found for key: ${input.toolboxKey}`); + } + + // Use the toolbox's AI-ready toolset + const { steps } = await generateText({ + model: icepick.defaultLanguageModel!, + tools: toolbox.toolSetForAI, + maxSteps: input.maxTools ?? 1, + prompt: input.prompt, + }); + + return { steps: steps.map((step) => step.toolCalls) }; + }, + }); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/deep-research.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/deep-research.agent.ts new file mode 100644 index 0000000000..bda868f632 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/deep-research.agent.ts @@ -0,0 +1,190 @@ +import { search } from '@/agents/deep-research/tools/search.tool'; +import { planSearch, PlanSearchOutput } from '@/agents/deep-research/tools/plan-search.tool'; +import { websiteToMd } from '@/agents/deep-research/tools/website-to-md.tool'; +import { summarize } from '@/agents/deep-research/tools/summarize.tool'; +import { judgeResults } from '@/agents/deep-research/tools/judge-results.tool'; +import { extractFacts } from '@/agents/deep-research/tools/extract-facts.tool'; +import { judgeFacts, JudgeFactsOutput } from '@/agents/deep-research/tools/judge-facts.tool'; +import { z } from 'zod'; +import { client } from './../../client'; + +const MessageSchema = z.object({ + message: z.string(), +}); + +const SourceSchema = z.object({ + url: z.string(), + title: z.string().optional(), + index: z.number(), +}); + +const ResponseSchema = z.object({ + result: z.object({ + isComplete: z.boolean(), + reason: z.string(), + sources: z.array(SourceSchema), + summary: z.string().optional(), + facts: z + .array( + z.object({ + text: z.string(), + sourceIndex: z.number(), + }) + ) + .optional(), + iterations: z.number().optional(), + factsJudgment: z + .object({ + reason: z.string(), + hasEnoughFacts: z.boolean(), + missingAspects: z.array(z.string()), + }) + .optional(), + searchPlans: z.string().optional(), + }), +}); + +type Source = z.infer; +type Fact = { + text: string; + sourceIndex: number; +}; + +export const deepResearchAgent = client.agent({ + name: 'deep-research-agent', + description: 'A tool that performs deep research on a given query', + inputSchema: MessageSchema, + outputSchema: ResponseSchema, + executionTimeout: '15m', + fn: async (input, ctx) => { + ctx.logger.info(`Starting deep research agent with query: ${input.message}`); + + let iteration = 0; + const maxIterations = 3; + const allFacts: Fact[] = []; + const allSources: Source[] = []; + let missingAspects: string[] = []; + let plan: PlanSearchOutput | undefined = undefined; + let factsJudgment: JudgeFactsOutput | undefined = undefined; + + while (!ctx.cancelled && iteration < maxIterations) { + iteration++; + ctx.logger.info(`Starting iteration ${iteration}/${maxIterations}`); + + // Plan the search based on the query, existing facts, and missing aspects + ctx.logger.info( + `Planning search with ${allFacts.length} existing facts and ${missingAspects.length} missing aspects` + ); + + plan = await planSearch.run({ + query: input.message, + existingFacts: allFacts.map((f) => f.text), + missingAspects: missingAspects, + }); + + ctx.logger.info(`Search plan for iteration ${iteration}: ${plan.reasoning}. Queries:`); + + for (const query of plan.queries) { + ctx.logger.info(`${query}`); + } + + ctx.logger.info(`Executing ${plan.queries.length} search queries`); + const results = await search.run(plan.queries.map((query: string) => ({ query }))); + + // Flatten and deduplicate sources + const newSources = results.flatMap((result) => result.sources); + const uniqueSources = new Map( + newSources.map((source, index) => [source.url, { ...source, index }]) + ); + + ctx.logger.info( + `Found ${newSources.length} new sources, ${uniqueSources.size} unique sources` + ); + + // Add new sources to all sources + allSources.push(...Array.from(uniqueSources.values())); + + // Convert sources to markdown + ctx.logger.info(`Converting ${uniqueSources.size} sources to markdown`); + const mdResults = await websiteToMd.run( + Array.from(uniqueSources.values()) + .sort((a, b) => a.index - b.index) + .map((source) => ({ + url: source.url, + index: source.index, + title: source.title || '', + })) + ); + + // Extract facts from each source + ctx.logger.info('Extracting facts from markdown content'); + const factsResults = await extractFacts.run( + mdResults.map((result) => ({ + source: result.markdown, + query: input.message, + sourceInfo: { + url: result.url, + title: result.title, + index: result.index, + }, + })) + ); + + // Add new facts to all facts + const newFacts = factsResults.flatMap((result) => result.facts); + allFacts.push(...newFacts); + ctx.logger.info(`Extracted ${newFacts.length} new facts, total facts: ${allFacts.length}`); + + // Judge if we have enough facts + ctx.logger.info('Judging if we have enough facts'); + factsJudgment = await judgeFacts.run({ + query: input.message, + facts: allFacts.map((f) => f.text), + }); + + // Update missing aspects for next iteration + missingAspects = factsJudgment.missingAspects; + ctx.logger.info(`Missing aspects: ${missingAspects.join(', ')}`); + + // If we have enough facts or reached max iterations, generate final summary + if (factsJudgment.hasEnoughFacts || iteration >= maxIterations) { + ctx.logger.info( + `Generating final summary (hasEnoughFacts: ${ + factsJudgment.hasEnoughFacts + }, reachedMaxIterations: ${iteration >= maxIterations})` + ); + break; + } + } + + // Always summarize and judge results after the loop + const summarizeResult = await summarize.run({ + text: input.message, + facts: allFacts, + sources: allSources, + }); + + ctx.logger.info('Judging final results'); + const judgeResult = await judgeResults.run({ + query: input.message, + result: summarizeResult.summary, + }); + + ctx.logger.info( + `Deep research complete (isComplete: ${judgeResult.isComplete}, totalFacts: ${allFacts.length}, totalSources: ${allSources.length}, iterations: ${iteration})` + ); + + return { + result: { + isComplete: judgeResult.isComplete, + reason: judgeResult.reason, + sources: allSources, + summary: summarizeResult.summary, + facts: allFacts, + iterations: iteration, + factsJudgment: factsJudgment, + searchPlans: plan?.reasoning, + }, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/deep-research.toolbox.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/deep-research.toolbox.ts new file mode 100644 index 0000000000..b23e6d2c90 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/deep-research.toolbox.ts @@ -0,0 +1,10 @@ +import { search } from '@/agents/deep-research/tools/search.tool'; +import { summarize } from '@/agents/deep-research/tools/summarize.tool'; +import { icepick } from '@/icepick-client'; + +export const deepResearchTaskbox = icepick.toolbox({ + tools: [ + search, + summarize, + ], +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/index.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/index.ts new file mode 100644 index 0000000000..70b5867963 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/index.ts @@ -0,0 +1,3 @@ +export * from './deep-research.agent'; +export * from './deep-research.toolbox'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/extract-facts.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/extract-facts.tool.ts new file mode 100644 index 0000000000..e345ca0dbb --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/extract-facts.tool.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { client } from './../../../client'; + +const ExtractFactsInputSchema = z.object({ + source: z.string(), + query: z.string(), + sourceInfo: z.object({ + url: z.string(), + title: z.string().optional(), + index: z.number(), + }), +}); + +type ExtractFactsInput = z.infer; + +const FactSchema = z.object({ + text: z.string(), + sourceIndex: z.number(), +}); + +const ExtractFactsOutputSchema = z.object({ + facts: z.array(FactSchema), +}); + +export const extractFacts = client.tool({ + name: "extract-facts", + description: "Extract relevant facts from a source that are related to a query", + inputSchema: ExtractFactsInputSchema, + outputSchema: ExtractFactsOutputSchema, + fn: async (input, ctx) => { + const result = await generateObject({ + abortSignal: ctx.abortController.signal, + prompt: ` +Extract relevant facts from the following source that are related to this query: +"""${input.query}""" + +Source: +"""${input.source}""" + +Extract only factual statements that are directly relevant to the query. Each fact should be a complete, standalone statement. +`, + model: openai("gpt-4.1-mini"), + schema: z.object({ + facts: z.array(z.string()), + }), + }); + + return { + facts: result.object.facts.map((fact) => ({ + text: fact, + sourceIndex: input.sourceInfo.index, + })), + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/index.ts new file mode 100644 index 0000000000..60d4b4fb82 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/index.ts @@ -0,0 +1,6 @@ +export * from './plan-search.tool'; +export * from './search.tool'; +export * from './summarize.tool'; +export * from './extract-facts.tool'; +export * from './judge-results.tool'; +export * from './judge-facts.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/judge-facts.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/judge-facts.tool.ts new file mode 100644 index 0000000000..501b116e58 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/judge-facts.tool.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { client } from './../../../client'; + +const JudgeFactsInputSchema = z.object({ + query: z.string(), + facts: z.array(z.string()), +}); + +const JudgeFactsOutputSchema = z.object({ + hasEnoughFacts: z.boolean(), + reason: z.string(), + missingAspects: z.array(z.string()), +}); + +export type JudgeFactsOutput = z.infer; + +export const judgeFacts = client.tool({ + name: "judge-facts", + description: "Judge if we have enough facts to comprehensively answer a query", + inputSchema: JudgeFactsInputSchema, + outputSchema: JudgeFactsOutputSchema, + fn: async (input, ctx) => { + const result = await generateObject({ + abortSignal: ctx.abortController.signal, + prompt: ` +Evaluate if we have enough facts to comprehensively answer this query: +"""${input.query}""" + +Current facts: +${input.facts.map((fact, i) => `${i + 1}. ${fact}`).join("\n")} + +Consider: +1. Are there any key aspects of the query that aren't covered by the current facts? +2. Are the facts diverse enough to provide a complete picture? +3. Are there any gaps in the information that would prevent a comprehensive answer? +4. Are there any technical jargon words that are not defined in the facts that require additional research? +`, + model: openai("gpt-4.1-mini"), + schema: z.object({ + hasEnoughFacts: z.boolean(), + reason: z.string(), + missingAspects: z.array(z.string()), + }), + }); + + return { + hasEnoughFacts: result.object.hasEnoughFacts, + reason: result.object.reason, + missingAspects: result.object.missingAspects, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/judge-results.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/judge-results.tool.ts new file mode 100644 index 0000000000..b0efa27ecc --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/judge-results.tool.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { client } from './../../../client'; + +const JudgeResultsInputSchema = z.object({ + query: z.string(), + result: z.string(), +}); + +const JudgeResultsOutputSchema = z.object({ + reason: z.string(), + isComplete: z.boolean(), +}); + +export const judgeResults = client.tool({ + name: "judge-results", + description: "Judge if the result is complete", + inputSchema: JudgeResultsInputSchema, + outputSchema: JudgeResultsOutputSchema, + fn: async (input, ctx) => { + const validatedInput = JudgeResultsInputSchema.parse(input); + + const result = await generateObject({ + abortSignal: ctx.abortController.signal, + prompt: ` +Judge the following answer to the query for completeness: +"""${validatedInput.query}""" + +Answer: +"""${validatedInput.result}""" + +Completeness means that the answer includes all the information that is relevant to the query and that the answer is not missing any important details. Does the answer leave any new questions unanswered? +`, + model: openai("gpt-4.1-mini"), + schema: z.object({ + reason: z.string(), + isComplete: z.boolean(), + }), + }); + + return { + reason: result.object.reason, + isComplete: result.object.isComplete, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/plan-search.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/plan-search.tool.ts new file mode 100644 index 0000000000..eab98b11e8 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/plan-search.tool.ts @@ -0,0 +1,72 @@ + +import { z } from "zod"; +import { generateObject } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { client } from './../../../client'; + +export const PlanSearchInputSchema = z.object({ + query: z.string(), + existingFacts: z.array(z.string()).optional(), + missingAspects: z.array(z.string()).optional(), +}); + +const PlanSearchOutputSchema = z.object({ + queries: z.array(z.string()), + reasoning: z.string(), +}); + +export type PlanSearchOutput = z.infer; + +export const planSearch = client.tool({ + name: "plan-search", + description: "Plan search queries to find information about a topic", + inputSchema: PlanSearchInputSchema, + outputSchema: PlanSearchOutputSchema, + fn: async (input, ctx) => { + + const result = await generateObject({ + abortSignal: ctx.abortController.signal, + prompt: ` +Plan search queries to find information about this topic: +"""${input.query}""" + +${ + input.existingFacts + ? ` +We already have these facts: +${input.existingFacts.map((fact, i) => `${i + 1}. ${fact}`).join("\n")} +` + : "" +} + +${ + input.missingAspects + ? ` +We need to find information about these missing aspects: +${input.missingAspects + .map((aspect, i) => `${i + 1}. ${aspect}`) + .join("\n")} +` + : "" +} + +Generate 3-5 specific search queries that will help us find new, relevant information. +The queries should: +1. Focus on finding information about missing aspects +2. Avoid duplicating information we already have +3. Be specific enough to find relevant sources +4. Use different angles or perspectives to ensure diverse information +`, + model: openai("gpt-4.1-mini"), + schema: z.object({ + queries: z.array(z.string()), + reasoning: z.string(), + }), + }); + + return { + queries: result.object.queries, + reasoning: result.object.reasoning, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/search.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/search.tool.ts new file mode 100644 index 0000000000..7dab05809b --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/search.tool.ts @@ -0,0 +1,54 @@ + +import { z } from "zod"; +import { generateText as aiGenerateText } from "ai"; +import { client } from './../../../client'; +import { openai } from "@ai-sdk/openai"; + +export const SearchInputSchema = z.object({ + query: z.string(), +}); +const SearchOutputSchema = z.object({ + query: z.string(), + sources: z.array(z.object({ + url: z.string(), + title: z.string().optional(), + })), +}); + +export const search = client.tool({ + name: "search", + description: "Search the web for information about a topic", + inputSchema: SearchInputSchema, + outputSchema: SearchOutputSchema, + fn: async (input, ctx) => { + const validatedInput = SearchInputSchema.parse(input); + + const result = await aiGenerateText({ + abortSignal: ctx.abortController.signal, + model: openai.responses("gpt-4o-mini"), + prompt: `${validatedInput.query}`, + tools: { + web_search_preview: openai.tools.webSearchPreview({ + // optional configuration: + searchContextSize: "high", + userLocation: { + type: "approximate", + city: "San Francisco", + region: "California", + }, + }), + }, + // Force web search tool: + toolChoice: { type: "tool", toolName: "web_search_preview" }, + }); + + // URL sources + return { + query: validatedInput.query, + sources: result.sources.map((source) => ({ + url: source.url, + title: source.title, + })), + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/summarize.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/summarize.tool.ts new file mode 100644 index 0000000000..aed965d7ae --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/summarize.tool.ts @@ -0,0 +1,88 @@ + +import { z } from "zod"; +import { client } from './../../../client'; +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; + +export const SummarizeInputSchema = z.object({ + text: z.string(), + facts: z.array( + z.object({ + text: z.string(), + sourceIndex: z.number(), + }) + ), + sources: z.array( + z.object({ + url: z.string(), + title: z.string().optional(), + index: z.number(), + }) + ), +}); + +export const SummarizeOutputSchema = z.object({ + summary: z.string(), +}); + +export const summarize = client.tool({ + name: "summarize", + description: "Summarize a set of facts", + inputSchema: SummarizeInputSchema, + outputSchema: SummarizeOutputSchema, + fn: async (input, ctx) => { + // Create a map of source indices to source information for easy lookup + const sourceMap = new Map( + input.sources.map((source) => [source.index, source]) + ); + + // Group facts by source + const factsBySource = new Map(); + input.facts.forEach((fact, index) => { + const facts = factsBySource.get(fact.sourceIndex) || []; + facts.push(`${index + 1}. ${fact.text}`); + factsBySource.set(fact.sourceIndex, facts); + }); + + // Format facts grouped by source + const formattedFacts = Array.from(factsBySource.entries()).map( + ([sourceIndex, facts]) => { + const source = sourceMap.get(sourceIndex); + if (!source) { + throw new Error(`Source with index ${sourceIndex} not found`); + } + return `From ${source.title || "Untitled"} (${ + source.url + }):\n${facts.join("\n")}`; + } + ); + const result = await generateText({ + abortSignal: ctx.abortController.signal, + system: `You are a professional researcher helping to write a detailed report based on verified facts.`, + prompt: ` +Write a comprehensive summary based on these verified facts: + +${formattedFacts.join("\n\n")} + +Requirements: +1. The summary should be based ONLY on the provided facts +2. Each fact should be referenced using its number in brackets (e.g. [1], [2]) +3. The summary should be well-structured and flow logically +4. The summary should be written in the style of a professional researcher +5. The summary should be written in the language of the original query +6. Include a "Sources" section at the end listing all referenced sources with their numbers +7. Write the summary in markdown format and present relevant information in a table format + +Original query: +""" +${input.text} +""" +`, + model: openai("gpt-4.1-mini"), + }); + + return { + summary: result.text, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/website-to-md.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/website-to-md.tool.ts new file mode 100644 index 0000000000..258f9b71a3 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/deep-research/tools/website-to-md.tool.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; +import { generateText as aiGenerateText } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { client } from './../../../client'; + +const WebsiteToMdxInputSchema = z.object({ + url: z.string().url(), + index: z.number(), + title: z.string(), +}); + +const WebsiteToMdxOutputSchema = z.object({ + index: z.number(), + title: z.string(), + url: z.string(), + markdown: z.string(), +}); + +export const websiteToMd = client.tool({ + name: "website-to-md", + description: "Load a website by its url and convert it to Markdown", + inputSchema: WebsiteToMdxInputSchema, + outputSchema: WebsiteToMdxOutputSchema, + fn: async (input, ctx) => { + const result = await aiGenerateText({ + abortSignal: ctx.abortController.signal, + model: openai.responses("gpt-4.1-mini"), + prompt: `Convert the content of this webpage to clean, well-formatted Markdown. Preserve the structure, headings, and important content while removing unnecessary elements like ads and navigation menus. Only include the content from the page, do not write any additional text. URL: ${input.url}`, + tools: { + web_search_preview: openai.tools.webSearchPreview({ + searchContextSize: "high", + }), + }, + toolChoice: { type: "tool", toolName: "web_search_preview" }, + }); + + return { + index: input.index, + title: input.title, + url: input.url, + markdown: result.text, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/index.ts new file mode 100644 index 0000000000..ba1ea7e98a --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/index.ts @@ -0,0 +1,2 @@ +export * from './prompt-chaining.agent'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/prompt-chaining.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/prompt-chaining.agent.ts new file mode 100644 index 0000000000..3fdbfcb491 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/prompt-chaining.agent.ts @@ -0,0 +1,79 @@ +import { client } from './../../../client'; +import z from "zod"; +import { oneTool } from "./tools/one.tool"; +import { twoTool } from "./tools/two.tool"; +import { threeTool } from "./tools/three.tool"; + +/** + * PROMPT CHAINING PATTERN + * + * Based on Anthropic's "Building Effective Agents" blog post: + * https://www.anthropic.com/engineering/building-effective-agents + * + * Pattern Description: + * Prompt chaining decomposes a task into a sequence of steps, where each LLM call + * processes the output of the previous one. You can add programmatic checks (gates) + * on intermediate steps to ensure the process stays on track. + * + * When to use: + * - Tasks can be easily decomposed into fixed subtasks + * - Trading latency for higher accuracy by making each LLM call easier + * - Need validation gates between steps + * + * Examples: + * - Generating marketing copy, then translating it + * - Writing outline → checking criteria → writing full document + * - Multi-step content processing with validation + */ + +const PromptChainingAgentInput = z.object({ + message: z.string(), +}); + +const PromptChainingAgentOutput = z.object({ + result: z.string(), +}); + +export const promptChainingAgent = client.agent({ + name: "prompt-chaining-agent", + executionTimeout: "1m", + inputSchema: PromptChainingAgentInput, + outputSchema: PromptChainingAgentOutput, + description: "Demonstrates prompt chaining: sequential LLM calls with validation gates", + fn: async (input, ctx) => { + + // STEP 1: First LLM call - Process the initial message + // This step determines if the message is about an animal + const { oneOutput } = await oneTool.run({ + message: input.message, + }); + + // GATE: Programmatic validation check between steps + // This is a key feature of prompt chaining - we can validate intermediate results + // and control the flow based on that validation + if(!oneOutput) { + // FAIL: If validation fails, we can terminate early or redirect + return { + result: 'Please provide a message about an animal' + } + } + + // PASS: If validation succeeds, continue to next step + // STEP 2: Second LLM call - Transform the validated input + // Since we know it's about an animal, translate to Spanish + const { twoOutput } = await twoTool.run({ + message: input.message, + }); + + // STEP 3: Third LLM call - Final transformation + // Convert the Spanish message into a haiku format + const { threeOutput } = await threeTool.run({ + twoOutput, // Note: Using output from previous step as input + }); + + // Return the final processed result + return { + result: threeOutput + } + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/index.ts new file mode 100644 index 0000000000..75de59a483 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/index.ts @@ -0,0 +1,3 @@ +export * from './one.tool'; +export * from './two.tool'; +export * from './three.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/one.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/one.tool.ts new file mode 100644 index 0000000000..d158ff0ed3 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/one.tool.ts @@ -0,0 +1,26 @@ +import { client } from './../../../../client'; +import z from "zod"; +import { generateText } from "ai"; + +export const oneTool = client.tool({ + name: "one-tool", + description: "A tool that returns 1", + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: z.object({ + oneOutput: z.boolean(), + }), + fn: async (input) => { + + // Make an LLM call to get the oneOutput + const oneOutput = await generateText({ + model: client.defaultLanguageModel, + prompt: `Is the following text about an animal? If so, return "yes", otherwise return "no": ${input.message}`, + }); + + return { + oneOutput: oneOutput.text === "yes", + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/three.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/three.tool.ts new file mode 100644 index 0000000000..f8b40f0781 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/three.tool.ts @@ -0,0 +1,26 @@ +import { client } from './../../../../client'; +import z from "zod"; +import { generateText } from "ai"; + +export const threeTool = client.tool({ + name: "three-tool", + description: "A tool that makes text into a haiku", + inputSchema: z.object({ + twoOutput: z.string(), + }), + outputSchema: z.object({ + threeOutput: z.string(), + }), + fn: async (input) => { + + // Make + const threeOutput = await generateText({ + model: client.defaultLanguageModel, + prompt: `Make the following text into a haiku: ${input.twoOutput}`, + }); + + return { + threeOutput: threeOutput.text, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/two.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/two.tool.ts new file mode 100644 index 0000000000..6a0b7fde8c --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/1.prompt-chaining/tools/two.tool.ts @@ -0,0 +1,26 @@ +import { client } from './../../../../client'; +import { generateText } from "ai"; +import z from "zod"; + +export const twoTool = client.tool({ + name: "two-tool", + description: "Translates text into spanish", + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: z.object({ + twoOutput: z.string(), + }), + fn: async (input) => { + + // Make an LLM call to get the twoOutput + const twoOutput = await generateText({ + model: client.defaultLanguageModel, + prompt: `Translate the following text into spanish: ${input.message}`, + }); + + return { + twoOutput: twoOutput.text, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/index.ts new file mode 100644 index 0000000000..a483b09ba4 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/index.ts @@ -0,0 +1,2 @@ +export * from './routing.agent'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/routing.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/routing.agent.ts new file mode 100644 index 0000000000..496d96a695 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/routing.agent.ts @@ -0,0 +1,80 @@ +import { client } from './../../../client'; +import z from "zod"; +import { salesTool, supportTool } from "./tools/calls.tool"; + +/** + * ROUTING PATTERN + * + * Based on Anthropic's "Building Effective Agents" blog post: + * https://www.anthropic.com/engineering/building-effective-agents + * + * Pattern Description: + * Routing classifies an input and directs it to a specialized followup task. + * This allows separation of concerns and building more specialized prompts + * without one input type hurting performance on others. + * + * When to use: + * - Complex tasks with distinct categories better handled separately + * - Classification can be handled accurately by LLM or traditional algorithms + * - Need specialized handling for different input types + * + * Examples: + * - Customer service: routing questions, refunds, technical support + * - Multi-model routing: easy questions to smaller models, hard to larger + * - Content classification with specialized processors + */ + +const RoutingAgentInput = z.object({ + message: z.string(), +}); + +const RoutingAgentOutput = z.object({ + message: z.string(), + canHelp: z.boolean(), +}); + +export const routingToolbox = client.toolbox({ + tools: [supportTool, salesTool], +}); + +export const routingAgent = client.agent({ + name: "routing-agent", + executionTimeout: "1m", + inputSchema: RoutingAgentInput, + outputSchema: RoutingAgentOutput, + description: "Demonstrates routing: classify input and direct to specialized handlers", + fn: async (input, ctx) => { + + // STEP 1: Classification - Determine the type of request + // This is the key step in routing - understanding what kind of input we have + // so we can direct it to the most appropriate specialized handler + const route = await routingToolbox.pickAndRun({ + prompt: input.message, + }); + + // STEP 2: Route to specialized handler based on classification + // Each case represents a different specialized workflow optimized for that type + switch(route.name) { + case "support-tool": { + // Route to support-specialized LLM with support-specific tools and prompts + return { + message: route.output.response, + canHelp: true, + } + } + case "sales-tool": { + // Route to sales-specialized LLM with sales-specific tools and prompts + return { + message: route.output.response, + canHelp: true, + } + } + default: + routingToolbox.assertExhaustive(route); + return { + message: "I am sorry, I cannot help with that yet.", + canHelp: false, + } + } + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/tools/calls.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/tools/calls.tool.ts new file mode 100644 index 0000000000..ad0d86b39d --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/tools/calls.tool.ts @@ -0,0 +1,45 @@ +import { client } from './../../../../client'; +import z from 'zod'; +import { generateText } from 'ai'; + +const CallInput = z.object({ + message: z.string(), +}); + +const CallOutput = z.object({ + response: z.string(), +}); + +export const supportTool = client.tool({ + name: 'support-tool', + description: 'A tool that provides technical support for the user', + inputSchema: CallInput, + outputSchema: CallOutput, + fn: async (input) => { + const response = await generateText({ + model: client.defaultLanguageModel, + prompt: `You are a support agent. The answer is usually to turn it on and off. The user has asked the following question: ${input.message}. Please provide a response to the user.`, + }); + + return { + response: response.text, + }; + }, +}); + +export const salesTool = client.tool({ + name: 'sales-tool', + description: 'A tool that provides sales support for the user', + inputSchema: CallInput, + outputSchema: CallOutput, + fn: async (input) => { + const response = await generateText({ + model: client.defaultLanguageModel, + prompt: `You are a sales agent. The product cost is $42.The user has asked the following question: ${input.message}. Please provide a response to the user.`, + }); + + return { + response: response.text, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/tools/index.ts new file mode 100644 index 0000000000..3b8809621b --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/2.routing/tools/index.ts @@ -0,0 +1 @@ +export * from './calls.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/index.ts new file mode 100644 index 0000000000..4575bc3d54 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/index.ts @@ -0,0 +1,2 @@ +export * from './sectioning.agent'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/sectioning.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/sectioning.agent.ts new file mode 100644 index 0000000000..4d4b91a5c4 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/sectioning.agent.ts @@ -0,0 +1,81 @@ +import { client } from './../../../../client'; +import z from "zod"; +import { appropriatenessCheckTool } from "./tools/appropriateness.tool"; +import { mainContentTool } from "./tools/main-content.tool"; + +/** + * SECTIONING PARALLELIZATION PATTERN + * + * Based on Anthropic's "Building Effective Agents" blog post: + * https://www.anthropic.com/engineering/building-effective-agents + * + * Pattern Description: + * Breaking a task into independent subtasks that run simultaneously, then + * aggregating results programmatically. This is one of two parallelization + * variations (the other being voting). + * + * When to use: + * - Independent subtasks can be parallelized for speed + * - Multiple considerations need separate focused attention + * - Implementing guardrails alongside main processing + * + * Examples: + * - Guardrails: One model processes queries while another screens for inappropriate content + * - Code review: Multiple aspects evaluated simultaneously + * - Multi-faceted analysis requiring separate specialized attention + * + * Key Insight: + * Anthropic found that LLMs generally perform better when each consideration + * is handled by a separate LLM call, allowing focused attention on each specific aspect. + */ + +const SectioningAgentInput = z.object({ + message: z.string(), +}); + +const SectioningAgentOutput = z.object({ + response: z.string(), + isAppropriate: z.boolean(), +}); + +export const sectioningAgent = client.agent({ + name: "sectioning-agent", + executionTimeout: "2m", + inputSchema: SectioningAgentInput, + outputSchema: SectioningAgentOutput, + description: "Demonstrates sectioning: parallel independent subtasks with focused attention", + fn: async (input, ctx) => { + + // PARALLEL EXECUTION: Run independent subtasks simultaneously + // This is the core of sectioning - instead of running tasks sequentially, + // we run them in parallel because they address different concerns + // + // Task 1: Appropriateness check (guardrail) + // Task 2: Main content generation + // + // These are independent - the appropriateness check doesn't need the main content + // to do its job, and vice versa. This allows for significant speed improvements. + const [{isAppropriate, reason}, mainResult] = await Promise.all([ + appropriatenessCheckTool.run({ message: input.message }), + mainContentTool.run({ message: input.message }), + ]); + + // AGGREGATION: Combine results with business logic + // The appropriateness check acts as a guardrail - if content is inappropriate, + // we discard the main content and return a safety message + if (!isAppropriate) { + return { + response: `I cannot provide a response to that request. ${reason}`, + isAppropriate: false, + }; + } + + // If appropriate, return the main content + return { + response: mainResult.mainContent, + isAppropriate: true, + }; + }, +}); + + [sectioningAgent]; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/appropriateness.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/appropriateness.tool.ts new file mode 100644 index 0000000000..410214793e --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/appropriateness.tool.ts @@ -0,0 +1,39 @@ +import { client } from './../../../../../client'; +import z from "zod"; +import { generateObject, generateText } from "ai"; + +export const appropriatenessCheckTool = client.tool({ + name: "appropriateness-check-tool", + description: "Determines if a message is appropriate and safe to respond to", + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: z.object({ + isAppropriate: z.boolean(), + reason: z.string(), + }), + fn: async (input) => { + const result = await generateObject({ + model: client.defaultLanguageModel, + prompt: ` +Analyze the following message to determine if it's appropriate to respond to. +Check for harmful content, inappropriate requests, or content that violates safety guidelines. + +Message: "${input.message}" + +Evaluate if the AI response is safe and appropriate. Consider: +- Does it contain harmful, offensive, or inappropriate content? +- Does it promote dangerous activities? +- Is it respectful and professional? + +Return your evaluation with a clear reason. +`, + schema: z.object({ + isAppropriate: z.boolean(), + reason: z.string(), + }), + }); + + return result.object; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/index.ts new file mode 100644 index 0000000000..daf077c590 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/index.ts @@ -0,0 +1,2 @@ +export * from './appropriateness.tool'; +export * from './main-content.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/main-content.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/main-content.tool.ts new file mode 100644 index 0000000000..c66b0bb862 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/main-content.tool.ts @@ -0,0 +1,30 @@ +import { client } from './../../../../../client'; +import z from "zod"; +import { generateText } from "ai"; + +export const mainContentTool = client.tool({ + name: "main-content-tool", + description: "Generates the main content section of a response", + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: z.object({ + mainContent: z.string(), + }), + fn: async (input) => { + const result = await generateText({ + model: client.defaultLanguageModel, + prompt: ` + Respond to the following user message. + This should be the detailed, substantive part of the response that directly addresses the user's query. + Provide helpful information, explanations, or answers as appropriate. + + User message: "${input.message}" + `, + }); + + return { + mainContent: result.text.trim(), + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/index.ts new file mode 100644 index 0000000000..991a536772 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/index.ts @@ -0,0 +1,2 @@ +export * from './voting.agent'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/accuracy-voter.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/accuracy-voter.tool.ts new file mode 100644 index 0000000000..ce5dfa7295 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/accuracy-voter.tool.ts @@ -0,0 +1,40 @@ +import { client } from './../../../../../client'; +import z from "zod"; +import { generateObject } from "ai"; + +export const accuracyVoterTool = icepick.tool({ + name: "accuracy-voter-tool", + description: "A specialized voting agent that evaluates the accuracy and reasoning quality of chat responses", + inputSchema: z.object({ + message: z.string(), + response: z.string(), + }), + outputSchema: z.object({ + approve: z.boolean(), + reason: z.string(), + }), + fn: async (input) => { + // Use LLM to evaluate accuracy of the response + const evaluation = await generateObject({ + model: client.defaultLanguageModel, + prompt: `You are an accuracy evaluator. Analyze this conversation: + +User Message: "${input.message}" +AI Response: "${input.response}" + +Evaluate if the AI response is accurate and well-reasoned. Consider: +- Are the facts presented correct to the best of your knowledge? +- Is the reasoning sound and logical? +- Does it avoid making unsubstantiated claims? +- Is it appropriately cautious about uncertain information? + +Return your evaluation with a clear reason.`, + schema: z.object({ + approve: z.boolean(), + reason: z.string(), + }), + }); + + return evaluation.object; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/helpfulness-voter.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/helpfulness-voter.tool.ts new file mode 100644 index 0000000000..4f8f70d39b --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/helpfulness-voter.tool.ts @@ -0,0 +1,61 @@ +import { client } from './../../../../../client'; +import z from "zod"; +import { generateObject } from "ai"; + +icepick.admin.runWorkflow("PdfToMarkdown", { + pdf_url: input.pdf_url, +}); + + +type PdfToMarkdownInput = { + pdf_url: string; +}; + +type PdfToMarkdownOutput = { + PdfToMarkdown: { + markdown: string; + }; +}; + +const pdfToMarkdown = client.workflow({ + name: "PdfToMarkdown", + description: "Convert a PDF to a markdown file", +}); + + +export const helpfulnessVoterTool = client.tool({ + name: "helpfulness-voter-tool", + description: "A specialized voting agent that evaluates the helpfulness and relevance of chat responses", + inputSchema: z.object({ + message: z.string(), + response: z.string(), + }), + outputSchema: z.object({ + approve: z.boolean(), + reason: z.string(), + }), + fn: async (input) => { + // Use LLM to evaluate helpfulness of the response + const evaluation = await generateObject({ + model: client.defaultLanguageModel, + prompt: `You are a helpfulness evaluator. Analyze this conversation: + +User Message: "${input.message}" +AI Response: "${input.response}" + +Evaluate if the AI response is helpful and relevant. Consider: +- Does it directly address the user's question or request? +- Is it informative and useful? +- Does it provide actionable information when appropriate? +- Is it clear and easy to understand? + +Return your evaluation with a clear reason.`, + schema: z.object({ + approve: z.boolean(), + reason: z.string(), + }), + }); + + return evaluation.object; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/index.ts new file mode 100644 index 0000000000..a85be82d98 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/index.ts @@ -0,0 +1,3 @@ +export * from './accuracy-voter.tool'; +export * from './helpfulness-voter.tool'; +export * from './safety-voter.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/safety-voter.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/safety-voter.tool.ts new file mode 100644 index 0000000000..160cdbeb07 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/tools/safety-voter.tool.ts @@ -0,0 +1,39 @@ +import { client } from './../../../../../client'; +import z from "zod"; +import { generateObject } from "ai"; + +export const safetyVoterTool = client.tool({ + name: "safety-voter-tool", + description: "A specialized voting agent that evaluates the safety and appropriateness of chat responses", + inputSchema: z.object({ + message: z.string(), + response: z.string(), + }), + outputSchema: z.object({ + approve: z.boolean(), + reason: z.string(), + }), + fn: async (input) => { + // Use LLM to evaluate safety of the response + const evaluation = await generateObject({ + model: client.defaultLanguageModel, + prompt: `You are a safety evaluator. Analyze this conversation: + +User Message: "${input.message}" +AI Response: "${input.response}" + +Evaluate if the AI response is safe and appropriate. Consider: +- Does it contain harmful, offensive, or inappropriate content? +- Does it promote dangerous activities? +- Is it respectful and professional? + +Return your evaluation with a clear reason.`, + schema: z.object({ + approve: z.boolean(), + reason: z.string(), + }), + }); + + return evaluation.object; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/voting.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/voting.agent.ts new file mode 100644 index 0000000000..10b0b7d4ba --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/2.voting/voting.agent.ts @@ -0,0 +1,102 @@ +import { client } from './../../../../client'; +import z from 'zod'; +import { safetyVoterTool } from './tools/safety-voter.tool'; +import { helpfulnessVoterTool } from './tools/helpfulness-voter.tool'; +import { accuracyVoterTool } from './tools/accuracy-voter.tool'; + +/** + * VOTING PARALLELIZATION PATTERN + * + * Based on Anthropic's "Building Effective Agents" blog post: + * https://www.anthropic.com/engineering/building-effective-agents + * + * Pattern Description: + * Running the same or similar tasks multiple times to get diverse outputs, + * then using voting logic to determine the final result. This is the second + * of two parallelization variations (the other being sectioning). + * + * When to use: + * - Need multiple perspectives for higher confidence results + * - Quality assurance through consensus + * - Balancing false positives/negatives with vote thresholds + * + * Examples: + * - Code vulnerability review with multiple specialized prompts + * - Content moderation with different evaluation criteria + * - Quality assessment requiring consensus + * + * Key Insight: + * Multiple attempts or perspectives can significantly improve confidence in results, + * especially for subjective or complex evaluation tasks. + */ + +const VotingAgentInput = z.object({ + message: z.string(), + response: z.string(), +}); + +const VotingAgentOutput = z.object({ + approved: z.boolean(), + finalResponse: z.string(), + votingSummary: z.string(), +}); + +export const votingAgent = client.agent({ + name: 'voting-agent', + executionTimeout: '1m', + inputSchema: VotingAgentInput, + outputSchema: VotingAgentOutput, + description: 'Demonstrates voting: multiple parallel evaluations with consensus decision-making', + fn: async (input, ctx) => { + // PARALLEL VOTING: Run multiple specialized evaluators simultaneously + // Each voter focuses on a different aspect of quality evaluation: + // - Safety: Checks for harmful or inappropriate content + // - Helpfulness: Evaluates whether the response actually helps the user + // - Accuracy: Assesses factual correctness and reliability + // + // This follows Anthropic's pattern of using multiple specialized evaluators + // rather than trying to do all evaluation in a single call + const [safetyVote, helpfulnessVote, accuracyVote] = await Promise.all([ + safetyVoterTool.run({ + message: input.message, + response: input.response, + }), + helpfulnessVoterTool.run({ + message: input.message, + response: input.response, + }), + accuracyVoterTool.run({ + message: input.message, + response: input.response, + }), + ]); + + // VOTE COUNTING: Aggregate the individual votes + const votes = [safetyVote.approve, helpfulnessVote.approve, accuracyVote.approve]; + const approvalCount = votes.filter((vote) => vote).length; + const totalVotes = votes.length; + + // CONSENSUS DECISION: Require majority approval + // This threshold can be adjusted based on your needs: + // - Higher threshold (e.g., unanimous) for more conservative decisions + // - Lower threshold for more permissive decisions + // - Different thresholds for different types of content + const approved = approvalCount >= Math.ceil(totalVotes / 2); + + // TRANSPARENCY: Create detailed voting summary + // This provides transparency into the decision-making process, + // which is crucial for debugging and building trust + const votingSummary = `Voting Results (${approvalCount}/${totalVotes} approved): +- Safety: ${safetyVote.approve ? '✓' : '✗'} - ${safetyVote.reason} +- Helpfulness: ${helpfulnessVote.approve ? '✓' : '✗'} - ${helpfulnessVote.reason} +- Accuracy: ${accuracyVote.approve ? '✓' : '✗'} - ${accuracyVote.reason}`; + + return { + approved, + finalResponse: approved + ? input.response + : 'I apologize, but I cannot provide that response as it did not meet our quality and safety standards.', + votingSummary, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/index.ts new file mode 100644 index 0000000000..4b95fdd9fc --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/3.parallelization/index.ts @@ -0,0 +1,2 @@ +export * from './1.sectioning'; +export * from './2.voting'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/evaluator-optimizer.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/evaluator-optimizer.agent.ts new file mode 100644 index 0000000000..a34b350162 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/evaluator-optimizer.agent.ts @@ -0,0 +1,106 @@ +import { client } from './../../../client'; +import z from 'zod'; +import { evaluatorTool } from './tools/evaluator.tool'; +import { generatorTool } from './tools/generator.tool'; + +/** + * EVALUATOR-OPTIMIZER PATTERN + * + * Based on Anthropic's "Building Effective Agents" blog post: + * https://www.anthropic.com/engineering/building-effective-agents + * + * Pattern Description: + * One LLM generates a response while another provides evaluation and feedback + * in a loop, iteratively improving the output. This is analogous to the + * iterative writing process a human writer might go through. + * + * When to use: + * - Clear evaluation criteria exist + * - Iterative refinement provides measurable value + * - LLM can provide useful feedback (similar to human feedback improving results) + * - Quality improvements possible through iteration + * + * Examples: + * - Literary translation with nuance refinement + * - Complex search requiring multiple rounds of analysis + * - Content creation with quality improvement loops + * - Creative writing with iterative polish + * + * Key Insight: + * This pattern works well when LLM responses can be demonstrably improved + * when a human articulates feedback, and when the LLM can provide such feedback itself. + */ + +const EvaluatorOptimizerAgentInput = z.object({ + topic: z.string(), + targetAudience: z.string(), +}); + +const EvaluatorOptimizerAgentOutput = z.object({ + post: z.string(), + iterations: z.number(), +}); + +export const evaluatorOptimizerAgent = client.agent({ + name: 'evaluator-optimizer-agent', + executionTimeout: '2m', + inputSchema: EvaluatorOptimizerAgentInput, + outputSchema: EvaluatorOptimizerAgentOutput, + description: 'Demonstrates evaluator-optimizer: iterative generation and refinement loop', + fn: async (input, ctx) => { + let post: string | undefined; + let feedback: string | undefined; + let iterations = 0; + + // ITERATIVE IMPROVEMENT LOOP + // The loop continues until either: + // 1. The evaluator determines the output is satisfactory (complete = true) + // 2. We reach the maximum number of iterations (prevents infinite loops) + for (let i = 0; i < 3; i++) { + iterations++; + // GENERATION STEP: Create or improve the content + // The generator takes into account: + // - Original requirements (topic, target audience) + // - Previous attempt (if any) + // - Feedback from evaluator (if any) + const { post: newPost } = await generatorTool.run({ + topic: input.topic, + targetAudience: input.targetAudience, + previousPost: post, + previousFeedback: feedback, + }); + post = newPost; + + // EVALUATION STEP: Assess the generated content + // The evaluator provides: + // - A completion flag (is this good enough?) + // - Specific feedback for improvement (if not complete) + const evaluatorResult = await evaluatorTool.run({ + post: post, + topic: input.topic, + targetAudience: input.targetAudience, + }); + + feedback = evaluatorResult.feedback; + + // COMPLETION CHECK: If evaluator is satisfied, return the result + if (evaluatorResult.complete) { + return { + post: post, + iterations: iterations, + }; + } + + // If not complete, the loop continues with the feedback for the next iteration + } + + // FALLBACK: If we've reached max iterations without completion + // This prevents infinite loops while still returning the best attempt + if (!post) throw new Error('I was unable to generate a post'); + + return { + post: post, + iterations: iterations, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/index.ts new file mode 100644 index 0000000000..4e13cb7227 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/index.ts @@ -0,0 +1,2 @@ +export * from './evaluator-optimizer.agent'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/evaluator.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/evaluator.tool.ts new file mode 100644 index 0000000000..c65d595b55 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/evaluator.tool.ts @@ -0,0 +1,36 @@ +import { client } from './../../../../client'; +import z from "zod"; +import { generateObject, generateText } from "ai"; + +export const evaluatorTool = client.tool({ + name: "evaluator-tool", + description: "Evaluates a social media post for quality and provides feedback if it can be improved", + inputSchema: z.object({ + post: z.string(), + topic: z.string(), + targetAudience: z.string(), + }), + outputSchema: z.object({ + complete: z.boolean().describe("Whether the post is complete and ready to be posted"), + feedback: z.string().describe("Feedback on the post if it is not complete"), + }), + fn: async (input) => { + const result = await generateObject({ + model: client.defaultLanguageModel, + prompt: ` + Analyze the following post to determine if it's appropriate to post. + Check for harmful content, inappropriate requests, or content that violates safety guidelines. + The post is about the following topic: "${input.topic}" + The target audience is: "${input.targetAudience}" + + Post: "${input.post}" + `, + schema: z.object({ + complete: z.boolean(), + feedback: z.string(), + }), + }); + + return result.object; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/generator.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/generator.tool.ts new file mode 100644 index 0000000000..78bb8af46b --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/generator.tool.ts @@ -0,0 +1,40 @@ +import { client } from './../../../../client'; +import z from "zod"; +import { generateText } from "ai"; + +export const generatorTool = client.tool({ + name: "generator-tool", + description: "Generates a social media post", + inputSchema: z.object({ + topic: z.string(), + targetAudience: z.string(), + previousPost: z.string().optional(), + previousFeedback: z.string().optional(), + }), + outputSchema: z.object({ + post: z.string(), + }), + fn: async (input) => { + const result = await generateText({ + model: client.defaultLanguageModel, + prompt: ` + Generate a social media post for the following topic. + This should be the detailed, substantive part of the response that directly addresses the user's query. + Provide helpful information, explanations, or answers as appropriate. + The post should be 100 words or less. + + Topic: "${input.topic}" + Target Audience: "${input.targetAudience}" + + ${input.previousFeedback ? `Improve the post based on the following feedback: "${input.previousFeedback}"` : ""} + ${input.previousPost ? `Previous Post: "${input.previousPost}"` : ""} + + Provide only the post text, no additional formatting or labels. + `, + }); + + return { + post: result.text.trim(), + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/index.ts new file mode 100644 index 0000000000..bb32421312 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/4.evaluator-optimizer/tools/index.ts @@ -0,0 +1,2 @@ +export * from './evaluator.tool'; +export * from './generator.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/README.md b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/README.md new file mode 100644 index 0000000000..04fe2baf88 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/README.md @@ -0,0 +1,176 @@ +# Effective Agent Patterns + +This directory contains implementations of the effective agent patterns described in [Anthropic's Building Effective Agents](https://www.anthropic.com/engineering/building-effective-agents) blog post, adapted for the Icepick framework. + +## Overview + +Anthropic's research with dozens of teams has shown that the most successful agent implementations use simple, composable patterns rather than complex frameworks. These patterns can be combined and customized to fit different use cases. + +## Pattern Categories + +### Workflows +**Workflows** are systems where LLMs and tools are orchestrated through predefined code paths. They offer predictability and consistency for well-defined tasks. + +### Agents +**Agents** are systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks. They're better when flexibility and model-driven decision-making are needed. + +--- + +## 1. Prompt Chaining + +**Location**: [`1.prompt-chaining/`](./1.prompt-chaining/) + +### Pattern Description +Prompt chaining decomposes a task into a sequence of steps, where each LLM call processes the output of the previous one. You can add programmatic checks (gates) on intermediate steps to ensure the process stays on track. + +### When to Use +- Tasks can be easily decomposed into fixed subtasks +- Trading latency for higher accuracy by making each LLM call an easier task +- Need validation gates between steps + +### Examples +- Generating marketing copy, then translating it +- Writing an outline, checking criteria, then writing the full document +- Multi-step content processing with validation + +### Implementation Notes +The example demonstrates: +- **Sequential processing**: Each tool runs after the previous one completes +- **Gating logic**: Validates intermediate results before proceeding +- **Clear flow**: `oneTool` → validation → `twoTool` → `threeTool` + +--- + +## 2. Routing + +**Location**: [`2.routing/`](./2.routing/) + +### Pattern Description +Routing classifies an input and directs it to a specialized followup task. This allows separation of concerns and building more specialized prompts without one input type hurting performance on others. + +### When to Use +- Complex tasks with distinct categories better handled separately +- Classification can be handled accurately by LLM or traditional algorithms +- Need specialized handling for different input types + +### Examples +- Customer service: routing questions, refunds, technical support +- Multi-model routing: easy questions to smaller models, hard ones to larger models +- Content classification with specialized processors + +### Implementation Notes +The example shows: +- **Classification first**: Determines the type of request +- **Specialized handlers**: Different tools for sales vs support +- **Fallback handling**: Graceful degradation for unhandled cases + +--- + +## 3. Parallelization + +**Location**: [`3.parallelization/`](./3.parallelization/) + +Parallelization has two key variations: + +### 3.1 Sectioning +**Location**: [`3.parallelization/1.sectioning/`](./3.parallelization/1.sectioning/) + +#### Pattern Description +Breaking a task into independent subtasks that run simultaneously, then aggregating results programmatically. + +#### When to Use +- Independent subtasks can be parallelized for speed +- Multiple considerations need separate focused attention +- Implementing guardrails alongside main processing + +#### Examples +- Guardrails: One model processes queries while another screens for inappropriate content +- Code review: Multiple aspects evaluated simultaneously +- Multi-faceted analysis requiring separate specialized attention + +#### Implementation Notes +- **Parallel execution**: `Promise.all()` runs appropriateness check and main content generation simultaneously +- **Independent concerns**: Each subprocess handles a different aspect +- **Conditional logic**: Results combined based on appropriateness check + +### 3.2 Voting +**Location**: [`3.parallelization/2.voting/`](./3.parallelization/2.voting/) + +#### Pattern Description +Running the same or similar tasks multiple times to get diverse outputs, then using voting logic to determine the final result. + +#### When to Use +- Need multiple perspectives for higher confidence +- Quality assurance through consensus +- Balancing false positives/negatives with vote thresholds + +#### Examples +- Code vulnerability review with multiple specialized prompts +- Content moderation with different evaluation criteria +- Quality assessment requiring consensus + +#### Implementation Notes +- **Multiple evaluators**: Safety, helpfulness, and accuracy voters run in parallel +- **Voting logic**: Majority approval (2/3) required +- **Detailed feedback**: Each voter provides reasoning for transparency + +--- + +## 4. Evaluator-Optimizer + +**Location**: [`4.evaluator-optimizer/`](./4.evaluator-optimizer/) + +### Pattern Description +One LLM generates a response while another provides evaluation and feedback in a loop, iteratively improving the output. + +### When to Use +- Clear evaluation criteria exist +- Iterative refinement provides measurable value +- LLM can provide useful feedback (similar to human feedback improving results) +- Quality improvements possible through iteration + +### Examples +- Literary translation with nuance refinement +- Complex search requiring multiple rounds +- Content creation with quality improvement loops +- Creative writing with iterative polish + +### Implementation Notes +The example demonstrates: +- **Iterative loop**: Up to 3 rounds of generation and evaluation +- **Feedback incorporation**: Previous feedback guides next generation +- **Completion criteria**: Evaluator determines when quality is sufficient +- **Fallback**: Maximum iterations prevent infinite loops + +--- + +## Missing Patterns + +These patterns from Anthropic's post are not yet implemented but could be added: + +### Orchestrator-Workers +A central LLM dynamically breaks down tasks and delegates to worker LLMs. Useful for unpredictable subtasks like complex coding changes or multi-source research. + +### Autonomous Agents +Systems where LLMs plan and operate independently with tool usage, maintaining control over task completion. Suitable for open-ended problems requiring many unpredictable steps. + +--- + +## Key Principles + +1. **Start Simple**: Begin with basic prompts and only add complexity when needed +2. **Measure Performance**: Use comprehensive evaluation to guide iterations +3. **Composable Patterns**: These patterns can be combined for complex use cases +4. **Tool Design**: Invest in clear tool documentation and interfaces (Agent-Computer Interface) +5. **Transparency**: Show the agent's planning and decision-making steps + +## Usage Tips + +1. **Pattern Selection**: Choose the simplest pattern that solves your problem +2. **Combination**: These patterns can be nested and combined +3. **Evaluation**: Always measure if complexity improvements justify the costs +4. **Iteration**: Start with one pattern and evolve based on real performance data + +--- + +For more details on each pattern, explore the individual directories and their implementations. diff --git a/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/index.ts b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/index.ts new file mode 100644 index 0000000000..e1aa45bced --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/effective-agent-patterns/index.ts @@ -0,0 +1,4 @@ +export * from './1.prompt-chaining'; +export * from './2.routing'; +export * from './3.parallelization'; +export * from './4.evaluator-optimizer'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/human-optimizer.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/human-optimizer.agent.ts new file mode 100644 index 0000000000..f1295b1a35 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/human-optimizer.agent.ts @@ -0,0 +1,131 @@ +import { client } from './../../client'; +import z from "zod"; +import { generatorTool } from "./tools/generator.tool"; +import { sendToSlackTool } from "./tools/send-to-slack.tool"; + +/** + * Human-in-the-loop: Generator with Human Feedback + * + * Extends the Evaluator-Optimizer pattern to include a human in the loop. + * Based on ../effective-agent-patterns/4.evaluator-optimizer but replaces + * LLM evaluation with human evaluation via Slack. + * + * Pattern Description: + * An LLM generates content while a human provides evaluation and feedback + * in a loop, iteratively improving the output. This combines automated + * generation with human judgment and expertise. + * + * When to use: + * - Human judgment/expertise is critical for evaluation + * - Quality standards are subjective or domain-specific + * - Human feedback can provide nuanced improvements + * - Iterative refinement with human oversight adds value + * - Real-time human approval is required + * + * Examples: + * - Content creation requiring brand voice approval + * - Marketing copy needing stakeholder sign-off + * - Creative writing with editorial feedback + * - Technical documentation requiring expert review + * - Social media posts needing compliance approval + * + * Key Insight: + * This pattern works well when human expertise and judgment are irreplaceable + * for evaluation, while still leveraging LLM efficiency for generation and iteration. + */ + +const EvaluatorOptimizerAgentInput = z.object({ + topic: z.string(), + targetAudience: z.string(), +}); + +const EvaluatorOptimizerAgentOutput = z.object({ + post: z.string(), + iterations: z.number(), +}); + +type FeedbackEvent = { + messageId: string; + approved: boolean; + feedback?: string; +} + +export const evaluatorOptimizerAgent = client.agent({ + name: "human-optimizer-agent", + executionTimeout: "2m", + inputSchema: EvaluatorOptimizerAgentInput, + outputSchema: EvaluatorOptimizerAgentOutput, + description: "Demonstrates human-in-the-loop: iterative generation with human feedback via Slack", + fn: async (input, ctx) => { + + let post: string | undefined; + let feedback: string | undefined; + let iterations = 0; + + // ITERATIVE IMPROVEMENT LOOP + // The loop continues until either: + // 1. The human approves the output via Slack + // 2. We reach the maximum number of iterations (prevents infinite loops) + for (let i = 0; i < 3; i++) { + iterations++; + // GENERATION STEP: Create or improve the content + // The generator takes into account: + // - Original requirements (topic, target audience) + // - Previous attempt (if any) + // - Feedback from human reviewer (if any) + const { post: newPost } = await generatorTool.run({ + topic: input.topic, + targetAudience: input.targetAudience, + previousPost: post, + previousFeedback: feedback + }); + post = newPost; + + // HUMAN REVIEW STEP: Send content to Slack for human evaluation + // This sends the generated post to Slack where a human can: + // - Approve the content (if satisfactory) + // - Provide specific feedback for improvement (if changes needed) + + const slackMessage = await sendToSlackTool.run({ + post: post, + topic: input.topic, + targetAudience: input.targetAudience + }); + + // dispatch an event on an approve or reject with feedback button + // icepick.events.push("feedback:create", { + // messageId: slackResult.messageId, + // approved: false, + // }) + + // Wait for human feedback via Slack interaction + const feedbackEvent = await ctx.waitFor({ + eventKey: "feedback:create", + expression: `input.messageId == "${slackMessage.messageId}"`, + }) + + const event = feedbackEvent["feedback:create"] as FeedbackEvent + + // COMPLETION CHECK: If human approves, return the result + if (event.approved) { + return { + post: post, + iterations: iterations, + }; + } + + feedback = event.feedback; + + // If not approved, the loop continues with the human feedback for the next iteration + } + + // FALLBACK: If we've reached max iterations without human approval + // This prevents infinite loops while still returning the best attempt + if (!post) throw new Error("I was unable to generate a post"); + + return { + post: post, + iterations: iterations, + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/index.ts b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/index.ts new file mode 100644 index 0000000000..d01cfba2c2 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/index.ts @@ -0,0 +1,2 @@ +export * from './human-optimizer.agent'; +export * from './tools'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/generator.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/generator.tool.ts new file mode 100644 index 0000000000..359bd570b2 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/generator.tool.ts @@ -0,0 +1,40 @@ +import { client } from './../../../client'; +import z from "zod"; +import { generateText } from "ai"; + +export const generatorTool = client.tool({ + name: "generator-tool", + description: "Generates a social media post", + inputSchema: z.object({ + topic: z.string(), + targetAudience: z.string(), + previousPost: z.string().optional(), + previousFeedback: z.string().optional(), + }), + outputSchema: z.object({ + post: z.string(), + }), + fn: async (input) => { + const result = await generateText({ + model: client.defaultLanguageModel, + prompt: ` + Generate a social media post for the following topic. + This should be the detailed, substantive part of the response that directly addresses the user's query. + Provide helpful information, explanations, or answers as appropriate. + The post should be 100 words or less. + + Topic: "${input.topic}" + Target Audience: "${input.targetAudience}" + + ${input.previousFeedback ? `Improve the post based on the following feedback: "${input.previousFeedback}"` : ""} + ${input.previousPost ? `Previous Post: "${input.previousPost}"` : ""} + + Provide only the post text, no additional formatting or labels. + `, + }); + + return { + post: result.text.trim(), + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/index.ts b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/index.ts new file mode 100644 index 0000000000..ae02d4aaf1 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/index.ts @@ -0,0 +1,2 @@ +export * from './generator.tool'; +export * from './send-to-slack.tool'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/send-to-slack.tool.ts b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/send-to-slack.tool.ts new file mode 100644 index 0000000000..5787471975 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/human-in-the-loop/tools/send-to-slack.tool.ts @@ -0,0 +1,22 @@ +import { client } from './../../../client'; +import z from "zod"; + +export const sendToSlackTool = client.tool({ + name: "send-to-slack-tool", + description: "Sends a message to Slack", + inputSchema: z.object({ + post: z.string(), + topic: z.string(), + targetAudience: z.string(), + }), + outputSchema: z.object({ + messageId: z.string(), + }), + fn: async (input) => { + // TODO: Implement the tool + + return { + messageId: "123", + }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/index.ts b/sdks/typescript/src/v1/examples/agent/agents/index.ts new file mode 100644 index 0000000000..ffb5db8e5f --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/index.ts @@ -0,0 +1,4 @@ +export * from './simple.agent'; +export * from './multi-agent.agent'; +export * from './human-in-the-loop'; +export * from './deep-research'; diff --git a/sdks/typescript/src/v1/examples/agent/agents/multi-agent.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/multi-agent.agent.ts new file mode 100644 index 0000000000..e5e7c61ca3 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/multi-agent.agent.ts @@ -0,0 +1,55 @@ +import { client } from './../client'; +import z from 'zod'; + +const CommonAgentResponseSchema = z.object({ + message: z.string(), +}); + +const supportAgent = client.agent({ + name: 'support-agent', + executionTimeout: '1m', + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: CommonAgentResponseSchema, + description: 'A support agent that provides support to the user', + fn: async (input, ctx) => { + return { message: 'Hello from support agent' }; + }, +}); + +const salesAgent = client.agent({ + name: 'sales-agent', + description: 'A sales agent that sells the product to the user', + executionTimeout: '1m', + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: CommonAgentResponseSchema, + fn: async (input, ctx) => { + return { message: 'Hello from sales agent' }; + }, +}); + +export const multiAgentToolbox = client.toolbox({ + tools: [supportAgent, salesAgent], +}); + +export const rootAgent = client.agent({ + name: 'root-agent', + executionTimeout: '1m', + inputSchema: z.object({ + message: z.string(), + }), + outputSchema: z.object({ + message: z.string(), + }), + description: 'A root agent that orchestrates the other agents', + fn: async (input, ctx) => { + const result = await multiAgentToolbox.pickAndRun({ + prompt: input.message, + }); + + return { message: result.output.message }; + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/agents/simple.agent.ts b/sdks/typescript/src/v1/examples/agent/agents/simple.agent.ts new file mode 100644 index 0000000000..c26f21d6ff --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/agents/simple.agent.ts @@ -0,0 +1,46 @@ +import { client } from './../client'; +import { weather } from '../tools/weather.tool'; +import { holiday, time } from '../tools/time.tool'; +import z from 'zod'; + +const SimpleAgentInput = z.object({ + message: z.string(), +}); + +const SimpleAgentOutput = z.object({ + message: z.string(), +}); + +export const simpleToolbox = client.toolbox({ + tools: [weather, time, holiday], +}); + +export const simpleAgent = client.agent({ + name: 'simple-agent', + executionTimeout: '1m', + inputSchema: SimpleAgentInput, + outputSchema: SimpleAgentOutput, + description: 'A simple agent to get the weather and time', + fn: async (input, ctx) => { + const result = await simpleToolbox.pickAndRun({ + prompt: input.message, + }); + + switch (result.name) { + case 'weather': + return { + message: `The weather in ${result.args.city} is ${result.output}`, + }; + case 'time': + return { + message: `The time in ${result.args.city} is ${result.output}`, + }; + case 'holiday': + return { + message: `The holiday in ${result.args.country} is ${result.output}`, + }; + default: + return simpleToolbox.assertExhaustive(result); + } + }, +}); diff --git a/sdks/typescript/src/v1/examples/agent/client.ts b/sdks/typescript/src/v1/examples/agent/client.ts new file mode 100644 index 0000000000..39679e7554 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/client.ts @@ -0,0 +1,6 @@ +import { openai } from '@ai-sdk/openai'; +import { Hatchet } from '@hatchet/index'; + +export const client = Hatchet.init({ + defaultLanguageModel: openai('gpt-4o-mini'), +}); diff --git a/sdks/typescript/src/v1/examples/agent/main.ts b/sdks/typescript/src/v1/examples/agent/main.ts new file mode 100644 index 0000000000..afb99c96e4 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/main.ts @@ -0,0 +1,7 @@ +// import { client } from './client'; + +// Import tools to ensure they get registered +import '@/agents'; +import '@/tools'; + +// client.start(); diff --git a/sdks/typescript/src/v1/examples/agent/run.ts b/sdks/typescript/src/v1/examples/agent/run.ts new file mode 100644 index 0000000000..6b268adf0f --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/run.ts @@ -0,0 +1,15 @@ +import { evaluatorOptimizerAgent } from './agents/effective-agent-patterns/4.evaluator-optimizer/evaluator-optimizer.agent'; + +async function main() { + const result = await evaluatorOptimizerAgent.run({ + topic: 'a post about parallelization in python', + targetAudience: 'senior developers', + }); + console.log(JSON.stringify(result, null, 2)); +} + +main() + .catch(console.error) + .finally(() => { + process.exit(0); + }); diff --git a/sdks/typescript/src/v1/examples/agent/tools/index.ts b/sdks/typescript/src/v1/examples/agent/tools/index.ts new file mode 100644 index 0000000000..1fc6b1eac6 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/tools/index.ts @@ -0,0 +1,6 @@ +// Auto-import all tools to ensure they get registered +export * from './time.tool'; +export * from './weather.tool'; + +// This file ensures all tools are evaluated and registered with the icepick client +// Import this file to automatically register all tools diff --git a/sdks/typescript/src/v1/examples/agent/tools/time.tool.ts b/sdks/typescript/src/v1/examples/agent/tools/time.tool.ts new file mode 100644 index 0000000000..1f7ffcead3 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/tools/time.tool.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; +import { client } from './../client'; + +const SimpleInput = z.object({ + message: z.string() +}) + +const SimpleOutput = z.object({ + response: z.string() +}) + +export const simple = client.tool({ + name: "simple-tool", + description: "Scaffold tool ", + inputSchema: SimpleInput, + outputSchema: SimpleOutput, + fn: async (input) => { + + // TODO: Replace this with your actual tool implementation + + return { + response: 'Hello, World!' + }; + } +}); diff --git a/sdks/typescript/src/v1/examples/agent/tools/weather.tool.ts b/sdks/typescript/src/v1/examples/agent/tools/weather.tool.ts new file mode 100644 index 0000000000..697630f841 --- /dev/null +++ b/sdks/typescript/src/v1/examples/agent/tools/weather.tool.ts @@ -0,0 +1,22 @@ +import { client } from './../client'; +import { z } from 'zod'; + +const WeatherInput = z.object({ + city: z.string().describe('The city to get the weather for'), +}); + +const WeatherOutput = z.object({ + weather: z.string(), +}); + +export const weather = client.tool({ + name: 'weather', + description: 'Get the weather in a given city', + inputSchema: WeatherInput, + outputSchema: WeatherOutput, + fn: async (input) => { + return { + weather: 'sunny', + }; + }, +});