├── sdk ├── src │ ├── version.ts │ ├── client │ │ └── index.ts │ └── index.ts ├── index.ts ├── README.md ├── tsconfig.json └── package.json ├── cli ├── src │ ├── version.ts │ ├── utils │ │ ├── __tests__ │ │ │ └── setup.ts │ │ ├── index.ts │ │ ├── barrel-utils.ts │ │ ├── agent-utils.ts │ │ ├── template-processor.ts │ │ ├── template-fetcher.ts │ │ ├── template-engine.ts │ │ └── template-path.ts │ ├── commands │ │ ├── mcp.ts │ │ ├── __tests__ │ │ │ ├── add-agent.test.ts │ │ │ └── add-tool.test.ts │ │ ├── add-agent.ts │ │ ├── add-tool.ts │ │ └── create.ts │ ├── index.ts │ └── mcp │ │ └── server.ts ├── templates │ ├── blank │ │ ├── src │ │ │ ├── tools │ │ │ │ ├── index.ts.hbs │ │ │ │ └── simple.tool.ts.hbs │ │ │ ├── agents │ │ │ │ ├── index.ts.hbs │ │ │ │ └── {{kebabCase name}}.agent.ts.hbs │ │ │ ├── index.ts.hbs │ │ │ ├── main.ts.hbs │ │ │ ├── icepick-client.ts.hbs │ │ │ └── trigger.ts.hbs │ │ ├── .env.example.hbs │ │ ├── nodemon.json.hbs │ │ ├── tsconfig.json.hbs │ │ ├── Dockerfile.hbs │ │ ├── .dockerignore.hbs │ │ ├── .gitignore.hbs │ │ ├── package.json.hbs │ │ └── README.md.hbs │ ├── geo │ │ ├── src │ │ │ ├── agents │ │ │ │ ├── index.ts.hbs │ │ │ │ └── {{kebabCase name}}.agent.ts.hbs │ │ │ ├── index.ts.hbs │ │ │ ├── main.ts.hbs │ │ │ ├── tools │ │ │ │ ├── index.ts.hbs │ │ │ │ ├── holiday.tool.ts.hbs │ │ │ │ ├── weather.tool.ts.hbs │ │ │ │ ├── summary.tool.ts.hbs │ │ │ │ └── time.tool.ts.hbs │ │ │ ├── icepick-client.ts.hbs │ │ │ └── trigger.ts.hbs │ │ ├── .env.example.hbs │ │ ├── nodemon.json.hbs │ │ ├── tsconfig.json.hbs │ │ ├── Dockerfile.hbs │ │ ├── .dockerignore.hbs │ │ ├── .gitignore.hbs │ │ ├── package.json.hbs │ │ └── README.md.hbs │ ├── deep-research │ │ ├── src │ │ │ ├── agents │ │ │ │ ├── index.ts.hbs │ │ │ │ └── {{kebabCase name}}.agent.ts.hbs │ │ │ ├── index.ts.hbs │ │ │ ├── main.ts.hbs │ │ │ ├── tools │ │ │ │ ├── index.ts.hbs │ │ │ │ ├── judge-results.tool.ts.hbs │ │ │ │ ├── website-to-md.tool.ts.hbs │ │ │ │ ├── search.tool.ts.hbs │ │ │ │ ├── extract-facts.tool.ts.hbs │ │ │ │ ├── judge-facts.tool.ts.hbs │ │ │ │ ├── plan-search.tool.ts.hbs │ │ │ │ └── summarize.tool.ts.hbs │ │ │ └── icepick-client.ts.hbs │ │ ├── .env.example.hbs │ │ ├── nodemon.json.hbs │ │ ├── tsconfig.json.hbs │ │ ├── Dockerfile.hbs │ │ ├── package.json.hbs │ │ └── README.md.hbs │ ├── tool │ │ └── {{kebabCase name}}.tool.ts.hbs │ └── agent │ │ └── {{kebabCase name}}.agent.ts.hbs ├── README.md ├── jest.config.js ├── tsconfig.json └── package.json ├── pnpm-workspace.yaml ├── scaffolds ├── src │ ├── agents │ │ ├── effective-agent-patterns │ │ │ ├── 2.routing │ │ │ │ ├── tools │ │ │ │ │ ├── index.ts │ │ │ │ │ └── calls.tool.ts │ │ │ │ ├── index.ts │ │ │ │ └── routing.agent.ts │ │ │ ├── 3.parallelization │ │ │ │ ├── index.ts │ │ │ │ ├── 2.voting │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tools │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── safety-voter.tool.ts │ │ │ │ │ │ ├── accuracy-voter.tool.ts │ │ │ │ │ │ └── helpfulness-voter.tool.ts │ │ │ │ │ └── voting.agent.ts │ │ │ │ └── 1.sectioning │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── tools │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── main-content.tool.ts │ │ │ │ │ └── appropriateness.tool.ts │ │ │ │ │ └── sectioning.agent.ts │ │ │ ├── 1.prompt-chaining │ │ │ │ ├── index.ts │ │ │ │ ├── tools │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── three.tool.ts │ │ │ │ │ ├── two.tool.ts │ │ │ │ │ └── one.tool.ts │ │ │ │ └── prompt-chaining.agent.ts │ │ │ ├── 4.evaluator-optimizer │ │ │ │ ├── index.ts │ │ │ │ ├── tools │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── evaluator.tool.ts │ │ │ │ │ └── generator.tool.ts │ │ │ │ └── evaluator-optimizer.agent.ts │ │ │ └── index.ts │ │ ├── human-in-the-loop │ │ │ ├── index.ts │ │ │ ├── tools │ │ │ │ ├── index.ts │ │ │ │ ├── send-to-slack.tool.ts │ │ │ │ └── generator.tool.ts │ │ │ └── human-optimizer.agent.ts │ │ ├── deep-research │ │ │ ├── index.ts │ │ │ ├── tools │ │ │ │ ├── index.ts │ │ │ │ ├── judge-results.tool.ts │ │ │ │ ├── website-to-md.tool.ts │ │ │ │ ├── search.tool.ts │ │ │ │ ├── extract-facts.tool.ts │ │ │ │ ├── judge-facts.tool.ts │ │ │ │ ├── plan-search.tool.ts │ │ │ │ └── summarize.tool.ts │ │ │ ├── deep-research.toolbox.ts │ │ │ └── deep-research.agent.ts │ │ ├── index.ts │ │ ├── simple.agent.ts │ │ └── multi-agent.agent.ts │ ├── main.ts │ ├── icepick-client.ts │ ├── tools │ │ ├── index.ts │ │ ├── weather.tool.ts │ │ └── time.tool.ts │ └── run.ts ├── .dockerignore ├── Dockerfile ├── tsconfig.json ├── docker-compose.yml └── package.json ├── static ├── icepick_dark.png └── icepick_light.png ├── package.json ├── .gitignore ├── LICENSE └── .github └── workflows └── publish.yml /sdk/src/version.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src' -------------------------------------------------------------------------------- /sdk/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./icepick"; -------------------------------------------------------------------------------- /cli/src/version.ts: -------------------------------------------------------------------------------- 1 | export const HATCHET_VERSION = '0.0.2'; 2 | -------------------------------------------------------------------------------- /cli/templates/blank/src/tools/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './simple.tool'; -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'sdk' 3 | - 'scaffolds' 4 | - 'cli' -------------------------------------------------------------------------------- /sdk/README.md: -------------------------------------------------------------------------------- 1 | ## @hatchet-dev/icepick 2 | 3 | Contains the SDK for Icepick. 4 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | ## @hatchet-dev/icepick-cli 2 | 3 | Contains the CLI for Icepick. 4 | -------------------------------------------------------------------------------- /cli/templates/blank/src/agents/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './{{kebabCase name}}.agent'; -------------------------------------------------------------------------------- /cli/templates/geo/src/agents/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './{{kebabCase name}}.agent'; -------------------------------------------------------------------------------- /cli/templates/geo/src/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './agents'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /cli/templates/blank/src/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './agents'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /cli/templates/deep-research/src/agents/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './{{kebabCase name}}.agent'; -------------------------------------------------------------------------------- /cli/templates/deep-research/src/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './agents'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@hatchet-dev/typescript-sdk"; 2 | export * from "./client/icepick"; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/2.routing/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './calls.tool'; -------------------------------------------------------------------------------- /static/icepick_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hatchet-dev/icepick/HEAD/static/icepick_dark.png -------------------------------------------------------------------------------- /static/icepick_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hatchet-dev/icepick/HEAD/static/icepick_light.png -------------------------------------------------------------------------------- /scaffolds/src/agents/human-in-the-loop/index.ts: -------------------------------------------------------------------------------- 1 | export * from './human-optimizer.agent'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /cli/templates/blank/.env.example.hbs: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=your_openai_api_key_here 2 | HATCHET_CLIENT_TOKEN=your_hatchet_api_key_here -------------------------------------------------------------------------------- /cli/templates/geo/.env.example.hbs: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=your_openai_api_key_here 2 | HATCHET_CLIENT_TOKEN=your_hatchet_api_key_here -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/2.routing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './routing.agent'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /scaffolds/src/agents/human-in-the-loop/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generator.tool'; 2 | export * from './send-to-slack.tool'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/index.ts: -------------------------------------------------------------------------------- 1 | export * from './1.sectioning'; 2 | export * from './2.voting'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/1.prompt-chaining/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prompt-chaining.agent'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/2.voting/index.ts: -------------------------------------------------------------------------------- 1 | export * from './voting.agent'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /cli/templates/geo/src/main.ts.hbs: -------------------------------------------------------------------------------- 1 | import "@/agents"; 2 | import "@/tools"; 3 | import { icepick } from "@/icepick-client"; 4 | 5 | icepick.start(); -------------------------------------------------------------------------------- /cli/templates/blank/src/main.ts.hbs: -------------------------------------------------------------------------------- 1 | import "@/agents"; 2 | import "@/tools"; 3 | import { icepick } from "@/icepick-client"; 4 | 5 | icepick.start(); -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/1.sectioning/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sectioning.agent'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/4.evaluator-optimizer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './evaluator-optimizer.agent'; 2 | export * from './tools'; -------------------------------------------------------------------------------- /cli/templates/deep-research/src/main.ts.hbs: -------------------------------------------------------------------------------- 1 | import "@/agents"; 2 | import "@/tools"; 3 | import { icepick } from "@/icepick-client"; 4 | 5 | icepick.start(); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deep-research.agent'; 2 | export * from './deep-research.toolbox'; 3 | export * from './tools'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/4.evaluator-optimizer/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './evaluator.tool'; 2 | export * from './generator.tool'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/1.prompt-chaining/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './one.tool'; 2 | export * from './two.tool'; 3 | export * from './three.tool'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './appropriateness.tool'; 2 | export * from './main-content.tool'; -------------------------------------------------------------------------------- /cli/templates/geo/src/tools/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './weather.tool'; 2 | export * from './time.tool'; 3 | export * from './holiday.tool'; 4 | export * from './summary.tool'; -------------------------------------------------------------------------------- /scaffolds/src/agents/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./simple.agent"; 2 | export * from "./multi-agent.agent"; 3 | export * from "./human-in-the-loop"; 4 | export * from "./deep-research"; -------------------------------------------------------------------------------- /scaffolds/src/main.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | 3 | // Import tools to ensure they get registered 4 | import "@/agents"; 5 | import "@/tools"; 6 | 7 | icepick.start(); -------------------------------------------------------------------------------- /cli/src/utils/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | // Jest setup file 2 | 3 | // Mock console.log and console.warn for cleaner tests 4 | global.console = { 5 | ...console, 6 | log: jest.fn(), 7 | warn: jest.fn(), 8 | }; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/2.voting/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './accuracy-voter.tool'; 2 | export * from './helpfulness-voter.tool'; 3 | export * from './safety-voter.tool'; -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/index.ts: -------------------------------------------------------------------------------- 1 | export * from './1.prompt-chaining'; 2 | export * from './2.routing'; 3 | export * from './3.parallelization'; 4 | export * from './4.evaluator-optimizer'; -------------------------------------------------------------------------------- /scaffolds/src/icepick-client.ts: -------------------------------------------------------------------------------- 1 | import { Icepick } from "@hatchet-dev/icepick/src"; 2 | import { openai } from "@ai-sdk/openai"; 3 | 4 | export const icepick = Icepick.init({ 5 | defaultLanguageModel: openai("gpt-4o-mini"), 6 | }); 7 | -------------------------------------------------------------------------------- /cli/templates/deep-research/.env.example.hbs: -------------------------------------------------------------------------------- 1 | # OpenAI API Configuration 2 | OPENAI_API_KEY=your_openai_api_key_here 3 | 4 | # Hatchet Configuration 5 | HATCHET_CLIENT_TOKEN=your_hatchet_token_here 6 | 7 | # Optional: Set log level (debug, info, warn, error) 8 | LOG_LEVEL=info -------------------------------------------------------------------------------- /scaffolds/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log* 3 | yarn-debug.log* 4 | yarn-error.log* 5 | .git 6 | .gitignore 7 | README.md 8 | .env 9 | .nyc_output 10 | coverage 11 | .DS_Store 12 | *.log 13 | dist 14 | .dockerignore 15 | Dockerfile 16 | docker-compose.yml -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plan-search.tool'; 2 | export * from './search.tool'; 3 | export * from './summarize.tool'; 4 | export * from './extract-facts.tool'; 5 | export * from './judge-results.tool'; 6 | export * from './judge-facts.tool'; 7 | -------------------------------------------------------------------------------- /scaffolds/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | // Auto-import all tools to ensure they get registered 2 | export * from './time.tool'; 3 | export * from './weather.tool'; 4 | 5 | // This file ensures all tools are evaluated and registered with the icepick client 6 | // Import this file to automatically register all tools -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/index.ts.hbs: -------------------------------------------------------------------------------- 1 | export * from './plan-search.tool'; 2 | export * from './search.tool'; 3 | export * from './summarize.tool'; 4 | export * from './extract-facts.tool'; 5 | export * from './judge-results.tool'; 6 | export * from './judge-facts.tool'; 7 | export * from './website-to-md.tool'; -------------------------------------------------------------------------------- /cli/templates/blank/src/icepick-client.ts.hbs: -------------------------------------------------------------------------------- 1 | import { Icepick } from "@hatchet-dev/icepick"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import * as dotenv from "dotenv"; 4 | 5 | // Load environment variables from .env file 6 | dotenv.config(); 7 | 8 | export const icepick = Icepick.init({ 9 | defaultLanguageModel: openai("gpt-4o-mini"), 10 | }); -------------------------------------------------------------------------------- /cli/templates/geo/src/icepick-client.ts.hbs: -------------------------------------------------------------------------------- 1 | import { Icepick } from "@hatchet-dev/icepick"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import * as dotenv from "dotenv"; 4 | 5 | // Load environment variables from .env file 6 | dotenv.config(); 7 | 8 | export const icepick = Icepick.init({ 9 | defaultLanguageModel: openai("gpt-4o-mini"), 10 | }); -------------------------------------------------------------------------------- /cli/templates/deep-research/src/icepick-client.ts.hbs: -------------------------------------------------------------------------------- 1 | import { Icepick } from "@hatchet-dev/icepick"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import * as dotenv from "dotenv"; 4 | 5 | // Load environment variables from .env file 6 | dotenv.config(); 7 | 8 | export const icepick = Icepick.init({ 9 | defaultLanguageModel: openai("gpt-4o-mini"), 10 | }); -------------------------------------------------------------------------------- /cli/templates/blank/src/trigger.ts.hbs: -------------------------------------------------------------------------------- 1 | import { {{camelCase name}}Agent } from '@/agents'; 2 | 3 | 4 | async function main() { 5 | const result = await {{camelCase name}}Agent.run({ 6 | message: "Hello, world!" 7 | }); 8 | 9 | console.log(result.response); 10 | } 11 | 12 | // Run the CLI 13 | if (require.main === module) { 14 | main().catch(console.error); 15 | } -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/deep-research.toolbox.ts: -------------------------------------------------------------------------------- 1 | import { search } from '@/agents/deep-research/tools/search.tool'; 2 | import { summarize } from '@/agents/deep-research/tools/summarize.tool'; 3 | import { icepick } from '@/icepick-client'; 4 | 5 | export const deepResearchTaskbox = icepick.toolbox({ 6 | tools: [ 7 | search, 8 | summarize, 9 | ], 10 | }); 11 | -------------------------------------------------------------------------------- /cli/templates/geo/nodemon.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": [ 5 | "src/**/*.test.ts", 6 | "src/**/*.spec.ts", 7 | "dist/**/*", 8 | "node_modules/**/*" 9 | ], 10 | "exec": "ts-node -r tsconfig-paths/register src/main.ts", 11 | "env": { 12 | "NODE_ENV": "development" 13 | }, 14 | "delay": "1000", 15 | "verbose": true, 16 | "restartable": "rs" 17 | } -------------------------------------------------------------------------------- /cli/templates/blank/nodemon.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": [ 5 | "src/**/*.test.ts", 6 | "src/**/*.spec.ts", 7 | "dist/**/*", 8 | "node_modules/**/*" 9 | ], 10 | "exec": "ts-node -r tsconfig-paths/register src/main.ts", 11 | "env": { 12 | "NODE_ENV": "development" 13 | }, 14 | "delay": "1000", 15 | "verbose": true, 16 | "restartable": "rs" 17 | } -------------------------------------------------------------------------------- /cli/templates/deep-research/nodemon.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts,json", 4 | "ignore": [ 5 | "src/**/*.test.ts", 6 | "src/**/*.spec.ts", 7 | "dist/**/*", 8 | "node_modules/**/*" 9 | ], 10 | "exec": "ts-node -r tsconfig-paths/register src/main.ts", 11 | "env": { 12 | "NODE_ENV": "development" 13 | }, 14 | "delay": "1000", 15 | "verbose": true, 16 | "restartable": "rs" 17 | } -------------------------------------------------------------------------------- /scaffolds/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build 2 | FROM node:22 AS builder 3 | 4 | WORKDIR /app 5 | 6 | COPY package\*.json ./ 7 | 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | # Stage 2: Production 15 | FROM node:22-alpine 16 | 17 | WORKDIR /app 18 | 19 | COPY package\*.json ./ 20 | 21 | RUN npm install --omit=dev 22 | 23 | COPY --from=builder /app/dist ./dist 24 | 25 | ENV NODE_ENV=production 26 | 27 | CMD ["node", "dist/main.js"] -------------------------------------------------------------------------------- /scaffolds/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 17 | } -------------------------------------------------------------------------------- /cli/templates/geo/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 17 | } -------------------------------------------------------------------------------- /cli/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { TemplateFetcher, TemplateSource, TemplateFile } from './template-fetcher'; 2 | export { TemplateProcessor, TemplateContext, ProcessedTemplate } from './template-processor'; 3 | export { TemplateEngine, TemplateEngineOptions, processTemplate } from './template-engine'; 4 | export { listAgents, getAgentInfo } from './agent-utils'; 5 | export { getTemplatePath, getTemplatePathAsync } from './template-path'; 6 | export { updateBarrelFile } from './barrel-utils'; -------------------------------------------------------------------------------- /cli/templates/blank/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 17 | } -------------------------------------------------------------------------------- /cli/templates/deep-research/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 17 | } -------------------------------------------------------------------------------- /cli/src/commands/mcp.ts: -------------------------------------------------------------------------------- 1 | // Import and start the MCP server directly 2 | export async function startMcp() { 3 | try { 4 | // Import the server class and run it directly 5 | const { IcepickMcpServer } = require('../mcp/server'); 6 | const server = new IcepickMcpServer(); 7 | await server.run(); 8 | } catch (error) { 9 | console.error('❌ Failed to start MCP server:', error instanceof Error ? error.message : 'Unknown error'); 10 | process.exit(1); 11 | } 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icepick-monorepo", 3 | "private": true, 4 | "workspaces": [ 5 | "utils", 6 | "scaffold" 7 | ], 8 | "scripts": { 9 | "build": "npm run build --workspaces", 10 | "test": "npm run test --workspaces", 11 | "lint": "npm run lint --workspaces" 12 | }, 13 | "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0" 14 | } 15 | -------------------------------------------------------------------------------- /scaffolds/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | icepick-app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | environment: 7 | - NODE_ENV=production 8 | # volumes: 9 | # Uncomment for development - mount source code for live reloading 10 | # - ./src:/app/src 11 | # - ./package.json:/app/package.json 12 | # - ./tsconfig.json:/app/tsconfig.json 13 | # ports: 14 | # - "3000:3000" # Uncomment and adjust port if your app serves HTTP -------------------------------------------------------------------------------- /scaffolds/src/run.ts: -------------------------------------------------------------------------------- 1 | import { evaluatorOptimizerAgent } from "./agents/effective-agent-patterns/4.evaluator-optimizer/evaluator-optimizer.agent"; 2 | 3 | 4 | 5 | async function main() { 6 | const result = await evaluatorOptimizerAgent.run({ 7 | topic: "a post about parallelization in python", 8 | targetAudience: "senior developers", 9 | }); 10 | console.log(JSON.stringify(result, null, 2)); 11 | } 12 | 13 | main().catch(console.error).finally(() => { 14 | process.exit(0); 15 | }); -------------------------------------------------------------------------------- /sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "paths": { 12 | // NOTE: also set in jest.config.ts 13 | "@hatchet-dev/icepick/*": ["./src/*"] 14 | } 15 | }, 16 | "include": ["src/**/*", "../scaffold/src/tools/toolbox.ts"], 17 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 18 | } -------------------------------------------------------------------------------- /cli/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src'], 5 | testMatch: [ 6 | '**/__tests__/**/*.test.ts', 7 | '**/?(*.)+(spec|test).ts' 8 | ], 9 | transform: { 10 | '^.+\\.ts$': 'ts-jest', 11 | }, 12 | collectCoverageFrom: [ 13 | 'src/**/*.ts', 14 | '!src/**/*.d.ts', 15 | '!src/index.ts', 16 | ], 17 | coverageDirectory: 'coverage', 18 | coverageReporters: [ 19 | 'text', 20 | 'lcov', 21 | 'html' 22 | ], 23 | setupFilesAfterEnv: ['/src/utils/__tests__/setup.ts'], 24 | }; -------------------------------------------------------------------------------- /scaffolds/src/tools/weather.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import { z } from "zod"; 3 | 4 | const WeatherInput = z.object({ 5 | city: z.string().describe("The city to get the weather for") 6 | }); 7 | 8 | const WeatherOutput = z.object({ 9 | weather: z.string() 10 | }); 11 | 12 | export const weather = icepick.tool({ 13 | name: "weather", 14 | description: "Get the weather in a given city", 15 | inputSchema: WeatherInput, 16 | outputSchema: WeatherOutput, 17 | fn: async (input) => { 18 | return { 19 | weather: "sunny", 20 | }; 21 | }, 22 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/human-in-the-loop/tools/send-to-slack.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | 4 | export const sendToSlackTool = icepick.tool({ 5 | name: "send-to-slack-tool", 6 | description: "Sends a message to Slack", 7 | inputSchema: z.object({ 8 | post: z.string(), 9 | topic: z.string(), 10 | targetAudience: z.string(), 11 | }), 12 | outputSchema: z.object({ 13 | messageId: z.string(), 14 | }), 15 | fn: async (input) => { 16 | // TODO: Implement the tool 17 | 18 | return { 19 | messageId: "123", 20 | }; 21 | }, 22 | }); -------------------------------------------------------------------------------- /scaffolds/src/tools/time.tool.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | 4 | const SimpleInput = z.object({ 5 | message: z.string() 6 | }) 7 | 8 | const SimpleOutput = z.object({ 9 | response: z.string() 10 | }) 11 | 12 | export const simple = icepick.tool({ 13 | name: "simple-tool", 14 | description: "Scaffold tool ", 15 | inputSchema: SimpleInput, 16 | outputSchema: SimpleOutput, 17 | fn: async (input) => { 18 | 19 | // TODO: Replace this with your actual tool implementation 20 | 21 | return { 22 | response: 'Hello, World!' 23 | }; 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /cli/templates/blank/src/tools/simple.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | 4 | const SimpleInput = z.object({ 5 | message: z.string() 6 | }) 7 | 8 | const SimpleOutput = z.object({ 9 | response: z.string() 10 | }) 11 | 12 | export const simple = icepick.tool({ 13 | name: "simple-tool", 14 | description: "Scaffold tool ", 15 | inputSchema: SimpleInput, 16 | outputSchema: SimpleOutput, 17 | fn: async (input) => { 18 | 19 | // TODO: Replace this with your actual tool implementation 20 | 21 | return { 22 | response: 'Hello, World!' 23 | }; 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "sourceMap": true, 16 | "paths": { 17 | // NOTE: also set in jest.config.ts 18 | "@hatchet-dev/icepick-cli/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "dist"] 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | build/ 7 | *.tsbuildinfo 8 | 9 | # Environment variables 10 | .env 11 | .env.local 12 | .env.*.local 13 | 14 | # IDE and editor files 15 | .vscode/ 16 | .idea/ 17 | *.swp 18 | *.swo 19 | *~ 20 | 21 | # OS generated files 22 | .DS_Store 23 | .DS_Store? 24 | ._* 25 | .Spotlight-V100 26 | .Trashes 27 | ehthumbs.db 28 | Thumbs.db 29 | 30 | # Logs 31 | logs 32 | *.log 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | pnpm-debug.log* 37 | 38 | # Runtime data 39 | pids 40 | *.pid 41 | *.seed 42 | *.pid.lock 43 | 44 | # Coverage directory used by tools like istanbul 45 | coverage/ 46 | *.lcov 47 | 48 | # Temporary folders 49 | tmp/ 50 | temp/ -------------------------------------------------------------------------------- /cli/templates/blank/Dockerfile.hbs: -------------------------------------------------------------------------------- 1 | # Use Node.js LTS version 2 | FROM node:20-alpine 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package*.json ./ 9 | COPY pnpm-lock.yaml* ./ 10 | 11 | # Install pnpm 12 | RUN npm install -g pnpm 13 | 14 | # Install dependencies 15 | RUN pnpm install 16 | 17 | # Copy source code 18 | COPY . . 19 | 20 | # Build the application 21 | RUN pnpm build 22 | 23 | # Expose port (adjust if your app uses a different port) 24 | EXPOSE 3000 25 | 26 | # Create non-root user for security 27 | RUN addgroup -g 1001 -S nodejs 28 | RUN adduser -S {{name}} -u 1001 29 | 30 | # Change ownership of the app directory 31 | RUN chown -R {{name}}:nodejs /app 32 | USER {{name}} 33 | 34 | # Start the application 35 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /cli/templates/geo/Dockerfile.hbs: -------------------------------------------------------------------------------- 1 | # Use Node.js LTS version 2 | FROM node:20-alpine 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package*.json ./ 9 | COPY pnpm-lock.yaml* ./ 10 | 11 | # Install pnpm 12 | RUN npm install -g pnpm 13 | 14 | # Install dependencies 15 | RUN pnpm install 16 | 17 | # Copy source code 18 | COPY . . 19 | 20 | # Build the application 21 | RUN pnpm build 22 | 23 | # Expose port (adjust if your app uses a different port) 24 | EXPOSE 3000 25 | 26 | # Create non-root user for security 27 | RUN addgroup -g 1001 -S nodejs 28 | RUN adduser -S {{name}} -u 1001 29 | 30 | # Change ownership of the app directory 31 | RUN chown -R {{name}}:nodejs /app 32 | USER {{name}} 33 | 34 | # Start the application 35 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /cli/templates/deep-research/Dockerfile.hbs: -------------------------------------------------------------------------------- 1 | # Use Node.js LTS version 2 | FROM node:20-alpine 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package*.json ./ 9 | COPY pnpm-lock.yaml* ./ 10 | 11 | # Install pnpm 12 | RUN npm install -g pnpm 13 | 14 | # Install dependencies 15 | RUN pnpm install 16 | 17 | # Copy source code 18 | COPY . . 19 | 20 | # Build the application 21 | RUN pnpm build 22 | 23 | # Expose port (adjust if your app uses a different port) 24 | EXPOSE 3000 25 | 26 | # Create non-root user for security 27 | RUN addgroup -g 1001 -S nodejs 28 | RUN adduser -S {{name}} -u 1001 29 | 30 | # Change ownership of the app directory 31 | RUN chown -R {{name}}:nodejs /app 32 | USER {{name}} 33 | 34 | # Start the application 35 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/1.prompt-chaining/tools/three.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateText } from "ai"; 4 | 5 | export const threeTool = icepick.tool({ 6 | name: "three-tool", 7 | description: "A tool that makes text into a haiku", 8 | inputSchema: z.object({ 9 | twoOutput: z.string(), 10 | }), 11 | outputSchema: z.object({ 12 | threeOutput: z.string(), 13 | }), 14 | fn: async (input) => { 15 | 16 | // Make 17 | const threeOutput = await generateText({ 18 | model: icepick.defaultLanguageModel, 19 | prompt: `Make the following text into a haiku: ${input.twoOutput}`, 20 | }); 21 | 22 | return { 23 | threeOutput: threeOutput.text, 24 | }; 25 | }, 26 | }); -------------------------------------------------------------------------------- /cli/templates/geo/.dockerignore.hbs: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Build outputs 9 | dist/ 10 | build/ 11 | *.tsbuildinfo 12 | 13 | # Environment variables 14 | .env 15 | .env.local 16 | .env.*.local 17 | 18 | # IDE and editor files 19 | .vscode/ 20 | .idea/ 21 | *.swp 22 | *.swo 23 | *~ 24 | 25 | # OS generated files 26 | .DS_Store 27 | .DS_Store? 28 | ._* 29 | .Spotlight-V100 30 | .Trashes 31 | ehthumbs.db 32 | Thumbs.db 33 | 34 | # Git 35 | .git/ 36 | .gitignore 37 | 38 | # Docker 39 | Dockerfile* 40 | .dockerignore 41 | 42 | # Documentation 43 | README.md 44 | *.md 45 | 46 | # Coverage and test reports 47 | coverage/ 48 | *.lcov 49 | 50 | # Temporary folders 51 | tmp/ 52 | temp/ 53 | 54 | # Logs 55 | logs/ -------------------------------------------------------------------------------- /cli/templates/blank/.dockerignore.hbs: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Build outputs 9 | dist/ 10 | build/ 11 | *.tsbuildinfo 12 | 13 | # Environment variables 14 | .env 15 | .env.local 16 | .env.*.local 17 | 18 | # IDE and editor files 19 | .vscode/ 20 | .idea/ 21 | *.swp 22 | *.swo 23 | *~ 24 | 25 | # OS generated files 26 | .DS_Store 27 | .DS_Store? 28 | ._* 29 | .Spotlight-V100 30 | .Trashes 31 | ehthumbs.db 32 | Thumbs.db 33 | 34 | # Git 35 | .git/ 36 | .gitignore 37 | 38 | # Docker 39 | Dockerfile* 40 | .dockerignore 41 | 42 | # Documentation 43 | README.md 44 | *.md 45 | 46 | # Coverage and test reports 47 | coverage/ 48 | *.lcov 49 | 50 | # Temporary folders 51 | tmp/ 52 | temp/ 53 | 54 | # Logs 55 | logs/ -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/1.prompt-chaining/tools/two.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import { generateText } from "ai"; 3 | import z from "zod"; 4 | 5 | export const twoTool = icepick.tool({ 6 | name: "two-tool", 7 | description: "Translates text into spanish", 8 | inputSchema: z.object({ 9 | message: z.string(), 10 | }), 11 | outputSchema: z.object({ 12 | twoOutput: z.string(), 13 | }), 14 | fn: async (input) => { 15 | 16 | // Make an LLM call to get the twoOutput 17 | const twoOutput = await generateText({ 18 | model: icepick.defaultLanguageModel, 19 | prompt: `Translate the following text into spanish: ${input.message}`, 20 | }); 21 | 22 | return { 23 | twoOutput: twoOutput.text, 24 | }; 25 | }, 26 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/1.prompt-chaining/tools/one.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateText } from "ai"; 4 | 5 | export const oneTool = icepick.tool({ 6 | name: "one-tool", 7 | description: "A tool that returns 1", 8 | inputSchema: z.object({ 9 | message: z.string(), 10 | }), 11 | outputSchema: z.object({ 12 | oneOutput: z.boolean(), 13 | }), 14 | fn: async (input) => { 15 | 16 | // Make an LLM call to get the oneOutput 17 | const oneOutput = await generateText({ 18 | model: icepick.defaultLanguageModel, 19 | prompt: `Is the following text about an animal? If so, return "yes", otherwise return "no": ${input.message}`, 20 | }); 21 | 22 | return { 23 | oneOutput: oneOutput.text === "yes", 24 | }; 25 | }, 26 | }); -------------------------------------------------------------------------------- /cli/templates/blank/.gitignore.hbs: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Build outputs 9 | dist/ 10 | build/ 11 | *.tsbuildinfo 12 | 13 | # Environment variables 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # IDE files 21 | .vscode/ 22 | .idea/ 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # OS generated files 28 | .DS_Store 29 | .DS_Store? 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | ehthumbs.db 34 | Thumbs.db 35 | 36 | # Logs 37 | logs/ 38 | *.log 39 | 40 | # Runtime data 41 | pids/ 42 | *.pid 43 | *.seed 44 | *.pid.lock 45 | 46 | # Coverage directory used by tools like istanbul 47 | coverage/ 48 | *.lcov 49 | 50 | # nyc test coverage 51 | .nyc_output/ 52 | 53 | # Jest 54 | coverage/ 55 | 56 | # Results directory for trigger command 57 | results/ 58 | 59 | # Temporary files 60 | *.tmp 61 | *.temp -------------------------------------------------------------------------------- /cli/templates/geo/.gitignore.hbs: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | 8 | # Build outputs 9 | dist/ 10 | build/ 11 | *.tsbuildinfo 12 | 13 | # Environment variables 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | # IDE files 21 | .vscode/ 22 | .idea/ 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # OS generated files 28 | .DS_Store 29 | .DS_Store? 30 | ._* 31 | .Spotlight-V100 32 | .Trashes 33 | ehthumbs.db 34 | Thumbs.db 35 | 36 | # Logs 37 | logs/ 38 | *.log 39 | 40 | # Runtime data 41 | pids/ 42 | *.pid 43 | *.seed 44 | *.pid.lock 45 | 46 | # Coverage directory used by tools like istanbul 47 | coverage/ 48 | *.lcov 49 | 50 | # nyc test coverage 51 | .nyc_output/ 52 | 53 | # Jest 54 | coverage/ 55 | 56 | # Results directory for trigger command 57 | results/ 58 | 59 | # Temporary files 60 | *.tmp 61 | *.temp -------------------------------------------------------------------------------- /scaffolds/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icepick-scaffold", 3 | "version": "0.1.0", 4 | "description": "Example project using @hatchet-dev/icepick", 5 | "main": "dist/main.js", 6 | "scripts": { 7 | "build": "tsc && tsc-alias", 8 | "start": "node dist/main.js", 9 | "dev": "ts-node -r tsconfig-paths/register", 10 | "test": "jest", 11 | "lint": "eslint . --ext .ts" 12 | }, 13 | "dependencies": { 14 | "@ai-sdk/openai": "^1.1.10", 15 | "@hatchet-dev/icepick": "workspace:*", 16 | "ai": "^4.3.16", 17 | "zod": "^3.25.64" 18 | }, 19 | "devDependencies": { 20 | "@types/jest": "^29.5.14", 21 | "@types/node": "^20.19.0", 22 | "@typescript-eslint/eslint-plugin": "^7.18.0", 23 | "@typescript-eslint/parser": "^7.18.0", 24 | "eslint": "^8.57.1", 25 | "jest": "^29.7.0", 26 | "ts-jest": "^29.4.0", 27 | "ts-node": "^10.9.2", 28 | "tsc-alias": "^1.8.10", 29 | "tsconfig-paths": "^4.2.0", 30 | "typescript": "^5.8.3" 31 | } 32 | } -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/main-content.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateText } from "ai"; 4 | 5 | export const mainContentTool = icepick.tool({ 6 | name: "main-content-tool", 7 | description: "Generates the main content section of a response", 8 | inputSchema: z.object({ 9 | message: z.string(), 10 | }), 11 | outputSchema: z.object({ 12 | mainContent: z.string(), 13 | }), 14 | fn: async (input) => { 15 | const result = await generateText({ 16 | model: icepick.defaultLanguageModel, 17 | prompt: ` 18 | Respond to the following user message. 19 | This should be the detailed, substantive part of the response that directly addresses the user's query. 20 | Provide helpful information, explanations, or answers as appropriate. 21 | 22 | User message: "${input.message}" 23 | `, 24 | }); 25 | 26 | return { 27 | mainContent: result.text.trim(), 28 | }; 29 | }, 30 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Hatchet Technologies Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/templates/blank/src/agents/{{kebabCase name}}.agent.ts.hbs: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import { simple } from "@/tools"; 3 | import z from "zod"; 4 | 5 | const {{pascalCase name}}AgentInput = z.object({ 6 | message: z.string(), 7 | }); 8 | 9 | const {{pascalCase name}}AgentOutput = z.object({ 10 | response: z.string(), 11 | }); 12 | 13 | export const {{camelCase name}}Toolbox = icepick.toolbox({ 14 | tools: [simple], 15 | }); 16 | 17 | export const {{camelCase name}}Agent = icepick.agent({ 18 | name: "{{kebabCase name}}-agent", 19 | executionTimeout: "1m", 20 | inputSchema: {{pascalCase name}}AgentInput, 21 | outputSchema: {{pascalCase name}}AgentOutput, 22 | description: "A {{name}} agent to start your project", 23 | fn: async (input, ctx) => { 24 | const result = await {{camelCase name}}Toolbox.pickAndRun({ 25 | prompt: input.message, 26 | }); 27 | 28 | switch (result.name) { 29 | case "simple-tool": 30 | return { 31 | response: result.output.response, 32 | }; 33 | break; 34 | default: 35 | return {{camelCase name}}Toolbox.assertExhaustive(result); 36 | } 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /cli/templates/deep-research/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{kebabCase name}}", 3 | "version": "0.1.0", 4 | "description": "{{description}}", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js", 9 | "dev": "nodemon", 10 | "dev:trigger": "nodemon --exec \"ts-node -r tsconfig-paths/register src/trigger.ts\"", 11 | "trigger": "ts-node -r tsconfig-paths/register src/trigger.ts", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix" 16 | }, 17 | "dependencies": { 18 | "@ai-sdk/openai": "^1.1.10", 19 | "@hatchet-dev/icepick": "^0.1.2", 20 | "ai": "^4.3.16", 21 | "dotenv": "^16.5.0", 22 | "zod": "^3.25.64" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^29.5.14", 26 | "@types/node": "^20.19.0", 27 | "@typescript-eslint/eslint-plugin": "^7.18.0", 28 | "@typescript-eslint/parser": "^7.18.0", 29 | "eslint": "^8.57.1", 30 | "jest": "^29.7.0", 31 | "nodemon": "^3.0.2", 32 | "ts-jest": "^29.4.0", 33 | "ts-node": "^10.9.2", 34 | "tsconfig-paths": "^4.2.0", 35 | "typescript": "^5.8.3" 36 | } 37 | } -------------------------------------------------------------------------------- /cli/templates/geo/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{kebabCase name}}", 3 | "version": "0.1.0", 4 | "description": "{{name}} project using @hatchet-dev/icepick", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js", 9 | "dev": "nodemon", 10 | "dev:trigger": "nodemon --exec \"ts-node -r tsconfig-paths/register src/trigger.ts\"", 11 | "trigger": "ts-node -r tsconfig-paths/register src/trigger.ts", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix" 16 | }, 17 | "dependencies": { 18 | "@ai-sdk/openai": "^1.1.10", 19 | "@hatchet-dev/icepick": "^0.1.2", 20 | "ai": "^4.3.16", 21 | "dotenv": "^16.5.0", 22 | "zod": "^3.25.64" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^29.5.14", 26 | "@types/node": "^20.19.0", 27 | "@typescript-eslint/eslint-plugin": "^7.18.0", 28 | "@typescript-eslint/parser": "^7.18.0", 29 | "eslint": "^8.57.1", 30 | "jest": "^29.7.0", 31 | "nodemon": "^3.0.2", 32 | "ts-jest": "^29.4.0", 33 | "ts-node": "^10.9.2", 34 | "tsconfig-paths": "^4.2.0", 35 | "typescript": "^5.8.3" 36 | } 37 | } -------------------------------------------------------------------------------- /cli/src/utils/barrel-utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { promises as fs } from 'fs'; 3 | 4 | export async function updateBarrelFile(outputDir: string, name: string, type: 'agent' | 'tool', silent?: boolean): Promise { 5 | const barrelPath = path.join(outputDir, 'index.ts'); 6 | 7 | try { 8 | // Check if barrel file exists 9 | await fs.access(barrelPath); 10 | 11 | // Read current content 12 | const currentContent = await fs.readFile(barrelPath, 'utf8'); 13 | 14 | // Generate export statement 15 | const exportStatement = type === 'agent' 16 | ? `export * from './${name}.agent';` 17 | : `export * from './${name}.tool';`; 18 | 19 | // Check if export already exists 20 | if (currentContent.includes(exportStatement)) { 21 | return; // Already exported 22 | } 23 | 24 | // Add export to barrel file 25 | const newContent = currentContent.trim() + '\n' + exportStatement + '\n'; 26 | await fs.writeFile(barrelPath, newContent); 27 | 28 | if (!silent) { 29 | console.log(`📦 Updated barrel file: ${barrelPath}`); 30 | } 31 | 32 | } catch (error) { 33 | // Barrel file doesn't exist, which is fine 34 | return; 35 | } 36 | } -------------------------------------------------------------------------------- /cli/templates/blank/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{kebabCase name}}", 3 | "version": "0.1.0", 4 | "description": "{{name}} project using @hatchet-dev/icepick", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node dist/index.js", 9 | "dev": "nodemon", 10 | "dev:trigger": "nodemon --exec \"ts-node -r tsconfig-paths/register src/trigger.ts\"", 11 | "trigger": "ts-node -r tsconfig-paths/register src/trigger.ts", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "lint": "eslint . --ext .ts", 15 | "lint:fix": "eslint . --ext .ts --fix" 16 | }, 17 | "dependencies": { 18 | "@ai-sdk/openai": "^1.1.10", 19 | "@hatchet-dev/icepick": "^0.1.2", 20 | "ai": "^4.3.16", 21 | "dotenv": "^16.5.0", 22 | "zod": "^3.25.64" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^29.5.14", 26 | "@types/node": "^20.19.0", 27 | "@typescript-eslint/eslint-plugin": "^7.18.0", 28 | "@typescript-eslint/parser": "^7.18.0", 29 | "eslint": "^8.57.1", 30 | "jest": "^29.7.0", 31 | "nodemon": "^3.0.2", 32 | "ts-jest": "^29.4.0", 33 | "ts-node": "^10.9.2", 34 | "tsconfig-paths": "^4.2.0", 35 | "typescript": "^5.8.3" 36 | } 37 | } -------------------------------------------------------------------------------- /cli/templates/tool/{{kebabCase name}}.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | 4 | export const {{camelCase name}} = icepick.tool({ 5 | name: "{{kebabCase name}}", 6 | description: "{{description}}", 7 | inputSchema: z.object({ 8 | // TODO: Define your input schema here. It is recommended to use descriptions for all fields. 9 | // Example: input: z.string().describe("The input parameter") 10 | }), 11 | outputSchema: z.object({ 12 | // TODO: Define your input schema here. It is recommended to use descriptions for all fields. 13 | // Example: result: z.string().describe("The tool result") 14 | }), 15 | fn: async (input) => { 16 | try { 17 | // TODO: Implement your tool logic here 18 | // Example API call: 19 | // const response = await fetch(`https://api.example.com/data?param=${input.param}`); 20 | // const data = await response.json(); 21 | 22 | return { 23 | // TODO: Return your tool output here 24 | // Example: result: data.message 25 | }; 26 | } catch (error) { 27 | return { 28 | // TODO: Handle errors appropriately for your tool 29 | // Example: result: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` 30 | }; 31 | } 32 | } 33 | }); -------------------------------------------------------------------------------- /cli/src/utils/agent-utils.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export async function listAgents(): Promise { 5 | const agentsDir = path.join(process.cwd(), 'src', 'agents'); 6 | 7 | try { 8 | await fs.access(agentsDir); 9 | } catch { 10 | return []; 11 | } 12 | 13 | const entries = await fs.readdir(agentsDir, { withFileTypes: true }); 14 | return entries 15 | .filter((entry: any) => entry.isDirectory()) 16 | .map((entry: any) => entry.name); 17 | } 18 | 19 | export async function getAgentInfo(name: string): Promise<{ name: string; description: string; location: string } | null> { 20 | const agentDir = path.join(process.cwd(), 'src', 'agents', name); 21 | const agentFile = path.join(agentDir, `${name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.agent.ts`); 22 | 23 | try { 24 | await fs.access(agentFile); 25 | } catch { 26 | return null; 27 | } 28 | 29 | const agentContent = await fs.readFile(agentFile, 'utf-8'); 30 | 31 | // Extract description from the file content 32 | const descriptionMatch = agentContent.match(/description: '([^']+)'/); 33 | const description = descriptionMatch ? descriptionMatch[1] : 'No description available'; 34 | 35 | return { 36 | name, 37 | description, 38 | location: agentFile 39 | }; 40 | } -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/2.voting/tools/safety-voter.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateObject } from "ai"; 4 | 5 | export const safetyVoterTool = icepick.tool({ 6 | name: "safety-voter-tool", 7 | description: "A specialized voting agent that evaluates the safety and appropriateness of chat responses", 8 | inputSchema: z.object({ 9 | message: z.string(), 10 | response: z.string(), 11 | }), 12 | outputSchema: z.object({ 13 | approve: z.boolean(), 14 | reason: z.string(), 15 | }), 16 | fn: async (input) => { 17 | // Use LLM to evaluate safety of the response 18 | const evaluation = await generateObject({ 19 | model: icepick.defaultLanguageModel, 20 | prompt: `You are a safety evaluator. Analyze this conversation: 21 | 22 | User Message: "${input.message}" 23 | AI Response: "${input.response}" 24 | 25 | Evaluate if the AI response is safe and appropriate. Consider: 26 | - Does it contain harmful, offensive, or inappropriate content? 27 | - Does it promote dangerous activities? 28 | - Is it respectful and professional? 29 | 30 | Return your evaluation with a clear reason.`, 31 | schema: z.object({ 32 | approve: z.boolean(), 33 | reason: z.string(), 34 | }), 35 | }); 36 | 37 | return evaluation.object; 38 | }, 39 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/1.sectioning/tools/appropriateness.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateObject, generateText } from "ai"; 4 | 5 | export const appropriatenessCheckTool = icepick.tool({ 6 | name: "appropriateness-check-tool", 7 | description: "Determines if a message is appropriate and safe to respond to", 8 | inputSchema: z.object({ 9 | message: z.string(), 10 | }), 11 | outputSchema: z.object({ 12 | isAppropriate: z.boolean(), 13 | reason: z.string(), 14 | }), 15 | fn: async (input) => { 16 | const result = await generateObject({ 17 | model: icepick.defaultLanguageModel, 18 | prompt: ` 19 | Analyze the following message to determine if it's appropriate to respond to. 20 | Check for harmful content, inappropriate requests, or content that violates safety guidelines. 21 | 22 | Message: "${input.message}" 23 | 24 | Evaluate if the AI response is safe and appropriate. Consider: 25 | - Does it contain harmful, offensive, or inappropriate content? 26 | - Does it promote dangerous activities? 27 | - Is it respectful and professional? 28 | 29 | Return your evaluation with a clear reason. 30 | `, 31 | schema: z.object({ 32 | isAppropriate: z.boolean(), 33 | reason: z.string(), 34 | }), 35 | }); 36 | 37 | return result.object; 38 | }, 39 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/4.evaluator-optimizer/tools/evaluator.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateObject, generateText } from "ai"; 4 | 5 | export const evaluatorTool = icepick.tool({ 6 | name: "evaluator-tool", 7 | description: "Evaluates a social media post for quality and provides feedback if it can be improved", 8 | inputSchema: z.object({ 9 | post: z.string(), 10 | topic: z.string(), 11 | targetAudience: z.string(), 12 | }), 13 | outputSchema: z.object({ 14 | complete: z.boolean().describe("Whether the post is complete and ready to be posted"), 15 | feedback: z.string().describe("Feedback on the post if it is not complete"), 16 | }), 17 | fn: async (input) => { 18 | const result = await generateObject({ 19 | model: icepick.defaultLanguageModel, 20 | prompt: ` 21 | Analyze the following post to determine if it's appropriate to post. 22 | Check for harmful content, inappropriate requests, or content that violates safety guidelines. 23 | The post is about the following topic: "${input.topic}" 24 | The target audience is: "${input.targetAudience}" 25 | 26 | Post: "${input.post}" 27 | `, 28 | schema: z.object({ 29 | complete: z.boolean(), 30 | feedback: z.string(), 31 | }), 32 | }); 33 | 34 | return result.object; 35 | }, 36 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/simple.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import { weather } from "../tools/weather.tool"; 3 | import { holiday, time } from "../tools/time.tool"; 4 | import z from "zod"; 5 | 6 | 7 | const SimpleAgentInput = z.object({ 8 | message: z.string(), 9 | }); 10 | 11 | const SimpleAgentOutput = z.object({ 12 | message: z.string(), 13 | }); 14 | 15 | export const simpleToolbox = icepick.toolbox({ 16 | tools: [weather, time, holiday], 17 | }); 18 | 19 | export const simpleAgent = icepick.agent({ 20 | name: "simple-agent", 21 | executionTimeout: "1m", 22 | inputSchema: SimpleAgentInput, 23 | outputSchema: SimpleAgentOutput, 24 | description: "A simple agent to get the weather and time", 25 | fn: async (input, ctx) => { 26 | const result = await simpleToolbox.pickAndRun({ 27 | prompt: input.message, 28 | }); 29 | 30 | switch (result.name) { 31 | case "weather": 32 | return { 33 | message: `The weather in ${result.args.city} is ${result.output}`, 34 | }; 35 | case "time": 36 | return { 37 | message: `The time in ${result.args.city} is ${result.output}`, 38 | }; 39 | case "holiday": 40 | return { 41 | message: `The holiday in ${result.args.country} is ${result.output}`, 42 | }; 43 | default: 44 | return simpleToolbox.assertExhaustive(result); 45 | } 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/2.voting/tools/accuracy-voter.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateObject } from "ai"; 4 | 5 | export const accuracyVoterTool = icepick.tool({ 6 | name: "accuracy-voter-tool", 7 | description: "A specialized voting agent that evaluates the accuracy and reasoning quality of chat responses", 8 | inputSchema: z.object({ 9 | message: z.string(), 10 | response: z.string(), 11 | }), 12 | outputSchema: z.object({ 13 | approve: z.boolean(), 14 | reason: z.string(), 15 | }), 16 | fn: async (input) => { 17 | // Use LLM to evaluate accuracy of the response 18 | const evaluation = await generateObject({ 19 | model: icepick.defaultLanguageModel, 20 | prompt: `You are an accuracy evaluator. Analyze this conversation: 21 | 22 | User Message: "${input.message}" 23 | AI Response: "${input.response}" 24 | 25 | Evaluate if the AI response is accurate and well-reasoned. Consider: 26 | - Are the facts presented correct to the best of your knowledge? 27 | - Is the reasoning sound and logical? 28 | - Does it avoid making unsubstantiated claims? 29 | - Is it appropriately cautious about uncertain information? 30 | 31 | Return your evaluation with a clear reason.`, 32 | schema: z.object({ 33 | approve: z.boolean(), 34 | reason: z.string(), 35 | }), 36 | }); 37 | 38 | return evaluation.object; 39 | }, 40 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/human-in-the-loop/tools/generator.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateText } from "ai"; 4 | 5 | export const generatorTool = icepick.tool({ 6 | name: "generator-tool", 7 | description: "Generates a social media post", 8 | inputSchema: z.object({ 9 | topic: z.string(), 10 | targetAudience: z.string(), 11 | previousPost: z.string().optional(), 12 | previousFeedback: z.string().optional(), 13 | }), 14 | outputSchema: z.object({ 15 | post: z.string(), 16 | }), 17 | fn: async (input) => { 18 | const result = await generateText({ 19 | model: icepick.defaultLanguageModel, 20 | prompt: ` 21 | Generate a social media post for the following topic. 22 | This should be the detailed, substantive part of the response that directly addresses the user's query. 23 | Provide helpful information, explanations, or answers as appropriate. 24 | The post should be 100 words or less. 25 | 26 | Topic: "${input.topic}" 27 | Target Audience: "${input.targetAudience}" 28 | 29 | ${input.previousFeedback ? `Improve the post based on the following feedback: "${input.previousFeedback}"` : ""} 30 | ${input.previousPost ? `Previous Post: "${input.previousPost}"` : ""} 31 | 32 | Provide only the post text, no additional formatting or labels. 33 | `, 34 | }); 35 | 36 | return { 37 | post: result.text.trim(), 38 | }; 39 | }, 40 | }); -------------------------------------------------------------------------------- /cli/templates/agent/{{kebabCase name}}.agent.ts.hbs: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | // TODO: Import your tools here 4 | // Example: import { myTool } from "@/tools/my-tool"; 5 | 6 | const {{pascalCase name}}AgentInput = z.object({ 7 | // TODO: Define your input schema here. It is recommended to use descriptions for all fields. 8 | message: z.string(), 9 | }); 10 | 11 | const {{pascalCase name}}AgentOutput = z.object({ 12 | // TODO: Define your output schema here. It is recommended to use descriptions for all fields. 13 | message: z.string(), 14 | }); 15 | 16 | export const {{camelCase name}}Toolbox = icepick.toolbox({ 17 | tools: [ 18 | // TODO: Add your tools here 19 | // Example: myTool, 20 | ], 21 | }); 22 | 23 | export const {{camelCase name}}Agent = icepick.agent({ 24 | name: "{{kebabCase name}}-agent", 25 | executionTimeout: "1m", 26 | inputSchema: {{pascalCase name}}AgentInput, 27 | outputSchema: {{pascalCase name}}AgentOutput, 28 | description: "{{description}}", 29 | fn: async (input, ctx) => { 30 | const result = await {{camelCase name}}Toolbox.pickAndRun({ 31 | prompt: input.message, 32 | }); 33 | 34 | switch (result.name) { 35 | // TODO: Handle your tool results here 36 | // Example: 37 | // case "myTool": 38 | // return { 39 | // message: `Result: ${result.output}`, 40 | // }; 41 | default: 42 | return {{camelCase name}}Toolbox.assertExhaustive(result); 43 | } 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/2.routing/tools/calls.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateText } from "ai"; 4 | 5 | const CallInput = z.object({ 6 | message: z.string(), 7 | }); 8 | 9 | const CallOutput = z.object({ 10 | response: z.string(), 11 | }); 12 | 13 | export const supportTool = icepick.tool({ 14 | name: "support-tool", 15 | description: "A tool that provides technical support for the user", 16 | inputSchema: CallInput, 17 | outputSchema: CallOutput, 18 | fn: async (input) => { 19 | 20 | const response = await generateText({ 21 | model: icepick.defaultLanguageModel, 22 | 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.`, 23 | }); 24 | 25 | return { 26 | response: response.text, 27 | }; 28 | }, 29 | }); 30 | 31 | export const salesTool = icepick.tool({ 32 | name: "sales-tool", 33 | description: "A tool that provides sales support for the user", 34 | inputSchema: CallInput, 35 | outputSchema: CallOutput, 36 | fn: async (input) => { 37 | const response = await generateText({ 38 | model: icepick.defaultLanguageModel, 39 | 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.`, 40 | }); 41 | 42 | return { 43 | response: response.text, 44 | }; 45 | }, 46 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/4.evaluator-optimizer/tools/generator.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateText } from "ai"; 4 | 5 | export const generatorTool = icepick.tool({ 6 | name: "generator-tool", 7 | description: "Generates a social media post", 8 | inputSchema: z.object({ 9 | topic: z.string(), 10 | targetAudience: z.string(), 11 | previousPost: z.string().optional(), 12 | previousFeedback: z.string().optional(), 13 | }), 14 | outputSchema: z.object({ 15 | post: z.string(), 16 | }), 17 | fn: async (input) => { 18 | const result = await generateText({ 19 | model: icepick.defaultLanguageModel, 20 | prompt: ` 21 | Generate a social media post for the following topic. 22 | This should be the detailed, substantive part of the response that directly addresses the user's query. 23 | Provide helpful information, explanations, or answers as appropriate. 24 | The post should be 100 words or less. 25 | 26 | Topic: "${input.topic}" 27 | Target Audience: "${input.targetAudience}" 28 | 29 | ${input.previousFeedback ? `Improve the post based on the following feedback: "${input.previousFeedback}"` : ""} 30 | ${input.previousPost ? `Previous Post: "${input.previousPost}"` : ""} 31 | 32 | Provide only the post text, no additional formatting or labels. 33 | `, 34 | }); 35 | 36 | return { 37 | post: result.text.trim(), 38 | }; 39 | }, 40 | }); -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/judge-results.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const JudgeResultsInputSchema = z.object({ 7 | query: z.string(), 8 | result: z.string(), 9 | }); 10 | 11 | const JudgeResultsOutputSchema = z.object({ 12 | reason: z.string(), 13 | isComplete: z.boolean(), 14 | }); 15 | 16 | export const judgeResults = icepick.tool({ 17 | name: "judge-results", 18 | description: "Judge if the result is complete", 19 | inputSchema: JudgeResultsInputSchema, 20 | outputSchema: JudgeResultsOutputSchema, 21 | fn: async (input, ctx) => { 22 | const validatedInput = JudgeResultsInputSchema.parse(input); 23 | 24 | const result = await generateObject({ 25 | abortSignal: ctx.abortController.signal, 26 | prompt: ` 27 | Judge the following answer to the query for completeness: 28 | """${validatedInput.query}""" 29 | 30 | Answer: 31 | """${validatedInput.result}""" 32 | 33 | 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? 34 | `, 35 | model: openai("gpt-4o-mini"), 36 | schema: z.object({ 37 | reason: z.string(), 38 | isComplete: z.boolean(), 39 | }), 40 | }); 41 | 42 | return { 43 | reason: result.object.reason, 44 | isComplete: result.object.isComplete, 45 | }; 46 | }, 47 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/judge-results.tool.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const JudgeResultsInputSchema = z.object({ 7 | query: z.string(), 8 | result: z.string(), 9 | }); 10 | 11 | const JudgeResultsOutputSchema = z.object({ 12 | reason: z.string(), 13 | isComplete: z.boolean(), 14 | }); 15 | 16 | export const judgeResults = icepick.tool({ 17 | name: "judge-results", 18 | description: "Judge if the result is complete", 19 | inputSchema: JudgeResultsInputSchema, 20 | outputSchema: JudgeResultsOutputSchema, 21 | fn: async (input, ctx) => { 22 | const validatedInput = JudgeResultsInputSchema.parse(input); 23 | 24 | const result = await generateObject({ 25 | abortSignal: ctx.abortController.signal, 26 | prompt: ` 27 | Judge the following answer to the query for completeness: 28 | """${validatedInput.query}""" 29 | 30 | Answer: 31 | """${validatedInput.result}""" 32 | 33 | 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? 34 | `, 35 | model: openai("gpt-4.1-mini"), 36 | schema: z.object({ 37 | reason: z.string(), 38 | isComplete: z.boolean(), 39 | }), 40 | }); 41 | 42 | return { 43 | reason: result.object.reason, 44 | isComplete: result.object.isComplete, 45 | }; 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /scaffolds/src/agents/multi-agent.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | 4 | const CommonAgentResponseSchema = z.object({ 5 | message: z.string(), 6 | }); 7 | 8 | const supportAgent = icepick.agent({ 9 | name: "support-agent", 10 | executionTimeout: "1m", 11 | inputSchema: z.object({ 12 | message: z.string(), 13 | }), 14 | outputSchema: CommonAgentResponseSchema, 15 | description: "A support agent that provides support to the user", 16 | fn: async (input, ctx) => { 17 | return { message: "Hello from support agent" }; 18 | }, 19 | }); 20 | 21 | const salesAgent = icepick.agent({ 22 | name: "sales-agent", 23 | description: "A sales agent that sells the product to the user", 24 | executionTimeout: "1m", 25 | inputSchema: z.object({ 26 | message: z.string(), 27 | }), 28 | outputSchema: CommonAgentResponseSchema, 29 | fn: async (input, ctx) => { 30 | return { message: "Hello from sales agent" }; 31 | }, 32 | }); 33 | 34 | 35 | export const multiAgentToolbox = icepick.toolbox({ 36 | tools: [supportAgent, salesAgent], 37 | }); 38 | 39 | 40 | export const rootAgent = icepick.agent({ 41 | name: "root-agent", 42 | executionTimeout: "1m", 43 | inputSchema: z.object({ 44 | message: z.string(), 45 | }), 46 | outputSchema: z.object({ 47 | message: z.string(), 48 | }), 49 | description: "A root agent that orchestrates the other agents", 50 | fn: async (input, ctx) => { 51 | const result = await multiAgentToolbox.pickAndRun({ 52 | prompt: input.message, 53 | }); 54 | 55 | return { message: result.output.message }; 56 | }, 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/website-to-md.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateText as aiGenerateText } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const WebsiteToMdxInputSchema = z.object({ 7 | url: z.string().url(), 8 | index: z.number(), 9 | title: z.string(), 10 | }); 11 | 12 | const WebsiteToMdxOutputSchema = z.object({ 13 | index: z.number(), 14 | title: z.string(), 15 | url: z.string(), 16 | markdown: z.string(), 17 | }); 18 | 19 | export const websiteToMd = icepick.tool({ 20 | name: "website-to-md", 21 | description: "Load a website by its url and convert it to Markdown", 22 | inputSchema: WebsiteToMdxInputSchema, 23 | outputSchema: WebsiteToMdxOutputSchema, 24 | fn: async (input, ctx) => { 25 | const result = await aiGenerateText({ 26 | abortSignal: ctx.abortController.signal, 27 | model: openai.responses("gpt-4o-mini"), 28 | 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}`, 29 | tools: { 30 | web_search_preview: openai.tools.webSearchPreview({ 31 | searchContextSize: "high", 32 | }), 33 | }, 34 | toolChoice: { type: "tool", toolName: "web_search_preview" }, 35 | }); 36 | 37 | return { 38 | index: input.index, 39 | title: input.title, 40 | url: input.url, 41 | markdown: result.text, 42 | }; 43 | }, 44 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/website-to-md.tool.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateText as aiGenerateText } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const WebsiteToMdxInputSchema = z.object({ 7 | url: z.string().url(), 8 | index: z.number(), 9 | title: z.string(), 10 | }); 11 | 12 | const WebsiteToMdxOutputSchema = z.object({ 13 | index: z.number(), 14 | title: z.string(), 15 | url: z.string(), 16 | markdown: z.string(), 17 | }); 18 | 19 | export const websiteToMd = icepick.tool({ 20 | name: "website-to-md", 21 | description: "Load a website by its url and convert it to Markdown", 22 | inputSchema: WebsiteToMdxInputSchema, 23 | outputSchema: WebsiteToMdxOutputSchema, 24 | fn: async (input, ctx) => { 25 | const result = await aiGenerateText({ 26 | abortSignal: ctx.abortController.signal, 27 | model: openai.responses("gpt-4.1-mini"), 28 | 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}`, 29 | tools: { 30 | web_search_preview: openai.tools.webSearchPreview({ 31 | searchContextSize: "high", 32 | }), 33 | }, 34 | toolChoice: { type: "tool", toolName: "web_search_preview" }, 35 | }); 36 | 37 | return { 38 | index: input.index, 39 | title: input.title, 40 | url: input.url, 41 | markdown: result.text, 42 | }; 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hatchet-dev/icepick", 3 | "version": "0.1.9", 4 | "description": "Icepick SDK for Hatchet", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "jest", 10 | "lint": "eslint . --ext .ts", 11 | "dump-version": "node -e \"console.log('export const HATCHET_VERSION = \\'' + require('./package.json').version + '\\';');\" > src/version.ts", 12 | "tsc:build": "npm run dump-version && tsc && resolve-tspaths", 13 | "prepublish": "cp package.json dist/package.json; cp README.md dist/; node -e \"const pkg=JSON.parse(require('fs').readFileSync('dist/package.json','utf8')); pkg.main='index.js'; pkg.types='index.d.ts'; require('fs').writeFileSync('dist/package.json',JSON.stringify(pkg,null,2));\"", 14 | "publish:ci": "rm -rf ./dist && npm run dump-version && npm run tsc:build && npm run prepublish && cd dist && npm publish --access public --no-git-checks", 15 | "docs": "typedoc" 16 | }, 17 | "dependencies": { 18 | "@ai-sdk/openai": "^1.1.10", 19 | "@hatchet-dev/typescript-sdk": "^1.8.0", 20 | "ai": "^4.3.16", 21 | "axios": "^1.9.0", 22 | "json-schema-to-zod": "^2.6.1", 23 | "zod": "^3.25.64" 24 | }, 25 | "devDependencies": { 26 | "@types/jest": "^29.5.12", 27 | "@types/node": "^20.11.24", 28 | "@typescript-eslint/eslint-plugin": "^7.1.0", 29 | "@typescript-eslint/parser": "^7.1.0", 30 | "eslint": "^8.57.0", 31 | "jest": "^29.7.0", 32 | "resolve-tspaths": "^0.8.23", 33 | "ts-jest": "^29.1.2", 34 | "typedoc": "^0.28.5", 35 | "typedoc-plugin-markdown": "^4.6.4", 36 | "typescript": "^5.3.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/search.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateText as aiGenerateText } from "ai"; 3 | import { icepick } from "@/icepick-client"; 4 | import { openai } from "@ai-sdk/openai"; 5 | 6 | export const SearchInputSchema = z.object({ 7 | query: z.string(), 8 | }); 9 | 10 | const SearchOutputSchema = z.object({ 11 | query: z.string(), 12 | sources: z.array(z.object({ 13 | url: z.string(), 14 | title: z.string().optional(), 15 | })), 16 | }); 17 | 18 | export const search = icepick.tool({ 19 | name: "search", 20 | description: "Search the web for information about a topic", 21 | inputSchema: SearchInputSchema, 22 | outputSchema: SearchOutputSchema, 23 | fn: async (input, ctx) => { 24 | const validatedInput = SearchInputSchema.parse(input); 25 | 26 | const result = await aiGenerateText({ 27 | abortSignal: ctx.abortController.signal, 28 | model: openai.responses("gpt-4o-mini"), 29 | prompt: `${validatedInput.query}`, 30 | tools: { 31 | web_search_preview: openai.tools.webSearchPreview({ 32 | // optional configuration: 33 | searchContextSize: "high", 34 | userLocation: { 35 | type: "approximate", 36 | city: "San Francisco", 37 | region: "California", 38 | }, 39 | }), 40 | }, 41 | // Force web search tool: 42 | toolChoice: { type: "tool", toolName: "web_search_preview" }, 43 | }); 44 | 45 | // URL sources 46 | return { 47 | query: validatedInput.query, 48 | sources: result.sources.map((source) => ({ 49 | url: source.url, 50 | title: source.title, 51 | })), 52 | }; 53 | }, 54 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/search.tool.ts: -------------------------------------------------------------------------------- 1 | 2 | import { z } from "zod"; 3 | import { generateText as aiGenerateText } from "ai"; 4 | import { icepick } from "@/icepick-client"; 5 | import { openai } from "@ai-sdk/openai"; 6 | 7 | export const SearchInputSchema = z.object({ 8 | query: z.string(), 9 | }); 10 | const SearchOutputSchema = z.object({ 11 | query: z.string(), 12 | sources: z.array(z.object({ 13 | url: z.string(), 14 | title: z.string().optional(), 15 | })), 16 | }); 17 | 18 | export const search = icepick.tool({ 19 | name: "search", 20 | description: "Search the web for information about a topic", 21 | inputSchema: SearchInputSchema, 22 | outputSchema: SearchOutputSchema, 23 | fn: async (input, ctx) => { 24 | const validatedInput = SearchInputSchema.parse(input); 25 | 26 | const result = await aiGenerateText({ 27 | abortSignal: ctx.abortController.signal, 28 | model: openai.responses("gpt-4o-mini"), 29 | prompt: `${validatedInput.query}`, 30 | tools: { 31 | web_search_preview: openai.tools.webSearchPreview({ 32 | // optional configuration: 33 | searchContextSize: "high", 34 | userLocation: { 35 | type: "approximate", 36 | city: "San Francisco", 37 | region: "California", 38 | }, 39 | }), 40 | }, 41 | // Force web search tool: 42 | toolChoice: { type: "tool", toolName: "web_search_preview" }, 43 | }); 44 | 45 | // URL sources 46 | return { 47 | query: validatedInput.query, 48 | sources: result.sources.map((source) => ({ 49 | url: source.url, 50 | title: source.title, 51 | })), 52 | }; 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/extract-facts.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const ExtractFactsInputSchema = z.object({ 7 | source: z.string(), 8 | query: z.string(), 9 | sourceInfo: z.object({ 10 | url: z.string(), 11 | title: z.string().optional(), 12 | index: z.number(), 13 | }), 14 | }); 15 | 16 | type ExtractFactsInput = z.infer; 17 | 18 | const FactSchema = z.object({ 19 | text: z.string(), 20 | sourceIndex: z.number(), 21 | }); 22 | 23 | const ExtractFactsOutputSchema = z.object({ 24 | facts: z.array(FactSchema), 25 | }); 26 | 27 | export const extractFacts = icepick.tool({ 28 | name: "extract-facts", 29 | description: "Extract relevant facts from a source that are related to a query", 30 | inputSchema: ExtractFactsInputSchema, 31 | outputSchema: ExtractFactsOutputSchema, 32 | fn: async (input, ctx) => { 33 | const result = await generateObject({ 34 | abortSignal: ctx.abortController.signal, 35 | prompt: ` 36 | Extract relevant facts from the following source that are related to this query: 37 | """${input.query}""" 38 | 39 | Source: 40 | """${input.source}""" 41 | 42 | Extract only factual statements that are directly relevant to the query. Each fact should be a complete, standalone statement. 43 | `, 44 | model: openai("gpt-4o-mini"), 45 | schema: z.object({ 46 | facts: z.array(z.string()), 47 | }), 48 | }); 49 | 50 | return { 51 | facts: result.object.facts.map((fact) => ({ 52 | text: fact, 53 | sourceIndex: input.sourceInfo.index, 54 | })), 55 | }; 56 | }, 57 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/extract-facts.tool.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const ExtractFactsInputSchema = z.object({ 7 | source: z.string(), 8 | query: z.string(), 9 | sourceInfo: z.object({ 10 | url: z.string(), 11 | title: z.string().optional(), 12 | index: z.number(), 13 | }), 14 | }); 15 | 16 | type ExtractFactsInput = z.infer; 17 | 18 | const FactSchema = z.object({ 19 | text: z.string(), 20 | sourceIndex: z.number(), 21 | }); 22 | 23 | const ExtractFactsOutputSchema = z.object({ 24 | facts: z.array(FactSchema), 25 | }); 26 | 27 | export const extractFacts = icepick.tool({ 28 | name: "extract-facts", 29 | description: "Extract relevant facts from a source that are related to a query", 30 | inputSchema: ExtractFactsInputSchema, 31 | outputSchema: ExtractFactsOutputSchema, 32 | fn: async (input, ctx) => { 33 | const result = await generateObject({ 34 | abortSignal: ctx.abortController.signal, 35 | prompt: ` 36 | Extract relevant facts from the following source that are related to this query: 37 | """${input.query}""" 38 | 39 | Source: 40 | """${input.source}""" 41 | 42 | Extract only factual statements that are directly relevant to the query. Each fact should be a complete, standalone statement. 43 | `, 44 | model: openai("gpt-4.1-mini"), 45 | schema: z.object({ 46 | facts: z.array(z.string()), 47 | }), 48 | }); 49 | 50 | return { 51 | facts: result.object.facts.map((fact) => ({ 52 | text: fact, 53 | sourceIndex: input.sourceInfo.index, 54 | })), 55 | }; 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hatchet-dev/icepick-cli", 3 | "version": "0.1.26", 4 | "main": "dist/index.js", 5 | "bin": { 6 | "icepick": "./dist/index.js" 7 | }, 8 | "scripts": { 9 | "build": "tsc", 10 | "dev": "pnpm exec ts-node src/index.ts", 11 | "start": "node dist/index.js", 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "test:coverage": "jest --coverage", 15 | "dump-version": "node -e \"console.log('export const HATCHET_VERSION = \\'' + require('./package.json').version + '\\';');\" > src/version.ts", 16 | "tsc:build": "npm run dump-version && tsc && resolve-tspaths", 17 | "prepublish": "cp package.json dist/package.json; cp README.md dist/; cp -r templates dist/; node -e \"const pkg=JSON.parse(require('fs').readFileSync('dist/package.json','utf8')); pkg.main='index.js'; pkg.bin.icepick='./index.js'; require('fs').writeFileSync('dist/package.json',JSON.stringify(pkg,null,2));\"", 18 | "publish:ci": "rm -rf ./dist && npm run dump-version && npm run tsc:build && npm run prepublish && cd dist && npm publish --access public --no-git-checks" 19 | }, 20 | "keywords": [ 21 | "cli", 22 | "tool", 23 | "commander" 24 | ], 25 | "author": "", 26 | "license": "ISC", 27 | "description": "CLI tool for managing components, agents, and tools", 28 | "dependencies": { 29 | "@modelcontextprotocol/sdk": "^1.12.2", 30 | "axios": "^1.9.0", 31 | "commander": "^14.0.0", 32 | "handlebars": "^4.7.8", 33 | "prompts": "^2.4.2", 34 | "zod": "^3.25.64" 35 | }, 36 | "devDependencies": { 37 | "@types/handlebars": "^4.1.0", 38 | "@types/jest": "^29.5.14", 39 | "@types/node": "^24.0.1", 40 | "@types/prompts": "^2.4.9", 41 | "jest": "^30.0.0", 42 | "resolve-tspaths": "^0.8.23", 43 | "ts-jest": "^29.4.0", 44 | "ts-node": "^10.9.2", 45 | "typescript": "^5.8.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/2.voting/tools/helpfulness-voter.tool.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generateObject } from "ai"; 4 | 5 | icepick.admin.runWorkflow("PdfToMarkdown", { 6 | pdf_url: input.pdf_url, 7 | }); 8 | 9 | 10 | type PdfToMarkdownInput = { 11 | pdf_url: string; 12 | }; 13 | 14 | type PdfToMarkdownOutput = { 15 | PdfToMarkdown: { 16 | markdown: string; 17 | }; 18 | }; 19 | 20 | const pdfToMarkdown = icepick.workflow({ 21 | name: "PdfToMarkdown", 22 | description: "Convert a PDF to a markdown file", 23 | }); 24 | 25 | 26 | export const helpfulnessVoterTool = icepick.tool({ 27 | name: "helpfulness-voter-tool", 28 | description: "A specialized voting agent that evaluates the helpfulness and relevance of chat responses", 29 | inputSchema: z.object({ 30 | message: z.string(), 31 | response: z.string(), 32 | }), 33 | outputSchema: z.object({ 34 | approve: z.boolean(), 35 | reason: z.string(), 36 | }), 37 | fn: async (input) => { 38 | // Use LLM to evaluate helpfulness of the response 39 | const evaluation = await generateObject({ 40 | model: icepick.defaultLanguageModel, 41 | prompt: `You are a helpfulness evaluator. Analyze this conversation: 42 | 43 | User Message: "${input.message}" 44 | AI Response: "${input.response}" 45 | 46 | Evaluate if the AI response is helpful and relevant. Consider: 47 | - Does it directly address the user's question or request? 48 | - Is it informative and useful? 49 | - Does it provide actionable information when appropriate? 50 | - Is it clear and easy to understand? 51 | 52 | Return your evaluation with a clear reason.`, 53 | schema: z.object({ 54 | approve: z.boolean(), 55 | reason: z.string(), 56 | }), 57 | }); 58 | 59 | return evaluation.object; 60 | }, 61 | }); -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/judge-facts.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const JudgeFactsInputSchema = z.object({ 7 | query: z.string(), 8 | facts: z.array(z.string()), 9 | }); 10 | 11 | const JudgeFactsOutputSchema = z.object({ 12 | hasEnoughFacts: z.boolean(), 13 | reason: z.string(), 14 | missingAspects: z.array(z.string()), 15 | }); 16 | 17 | export type JudgeFactsOutput = z.infer; 18 | 19 | export const judgeFacts = icepick.tool({ 20 | name: "judge-facts", 21 | description: "Judge if we have enough facts to comprehensively answer a query", 22 | inputSchema: JudgeFactsInputSchema, 23 | outputSchema: JudgeFactsOutputSchema, 24 | fn: async (input, ctx) => { 25 | const result = await generateObject({ 26 | abortSignal: ctx.abortController.signal, 27 | prompt: ` 28 | Evaluate if we have enough facts to comprehensively answer this query: 29 | """${input.query}""" 30 | 31 | Current facts: 32 | ${input.facts.map((fact, i) => `${i + 1}. ${fact}`).join("\n")} 33 | 34 | Consider: 35 | 1. Are there any key aspects of the query that aren't covered by the current facts? 36 | 2. Are the facts diverse enough to provide a complete picture? 37 | 3. Are there any gaps in the information that would prevent a comprehensive answer? 38 | 4. Are there any technical jargon words that are not defined in the facts that require additional research? 39 | `, 40 | model: openai("gpt-4o-mini"), 41 | schema: z.object({ 42 | hasEnoughFacts: z.boolean(), 43 | reason: z.string(), 44 | missingAspects: z.array(z.string()), 45 | }), 46 | }); 47 | 48 | return { 49 | hasEnoughFacts: result.object.hasEnoughFacts, 50 | reason: result.object.reason, 51 | missingAspects: result.object.missingAspects, 52 | }; 53 | }, 54 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/judge-facts.tool.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | const JudgeFactsInputSchema = z.object({ 7 | query: z.string(), 8 | facts: z.array(z.string()), 9 | }); 10 | 11 | const JudgeFactsOutputSchema = z.object({ 12 | hasEnoughFacts: z.boolean(), 13 | reason: z.string(), 14 | missingAspects: z.array(z.string()), 15 | }); 16 | 17 | export type JudgeFactsOutput = z.infer; 18 | 19 | export const judgeFacts = icepick.tool({ 20 | name: "judge-facts", 21 | description: "Judge if we have enough facts to comprehensively answer a query", 22 | inputSchema: JudgeFactsInputSchema, 23 | outputSchema: JudgeFactsOutputSchema, 24 | fn: async (input, ctx) => { 25 | const result = await generateObject({ 26 | abortSignal: ctx.abortController.signal, 27 | prompt: ` 28 | Evaluate if we have enough facts to comprehensively answer this query: 29 | """${input.query}""" 30 | 31 | Current facts: 32 | ${input.facts.map((fact, i) => `${i + 1}. ${fact}`).join("\n")} 33 | 34 | Consider: 35 | 1. Are there any key aspects of the query that aren't covered by the current facts? 36 | 2. Are the facts diverse enough to provide a complete picture? 37 | 3. Are there any gaps in the information that would prevent a comprehensive answer? 38 | 4. Are there any technical jargon words that are not defined in the facts that require additional research? 39 | `, 40 | model: openai("gpt-4.1-mini"), 41 | schema: z.object({ 42 | hasEnoughFacts: z.boolean(), 43 | reason: z.string(), 44 | missingAspects: z.array(z.string()), 45 | }), 46 | }); 47 | 48 | return { 49 | hasEnoughFacts: result.object.hasEnoughFacts, 50 | reason: result.object.reason, 51 | missingAspects: result.object.missingAspects, 52 | }; 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /cli/templates/geo/src/agents/{{kebabCase name}}.agent.ts.hbs: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import { weather, time, holiday, summary } from "@/tools"; 3 | import z from "zod"; 4 | 5 | const {{pascalCase name}}AgentInput = z.object({ 6 | message: z.string(), 7 | }); 8 | 9 | const {{pascalCase name}}AgentOutput = z.object({ 10 | message: z.string(), 11 | highlights: z.array(z.string()).optional(), 12 | recommendations: z.array(z.string()).optional(), 13 | }); 14 | 15 | export const {{camelCase name}}Toolbox = icepick.toolbox({ 16 | tools: [weather, time, holiday, summary], 17 | }); 18 | 19 | export const {{camelCase name}}Agent = icepick.agent({ 20 | name: "{{kebabCase name}}-agent", 21 | executionTimeout: "1m", 22 | inputSchema: {{pascalCase name}}AgentInput, 23 | outputSchema: {{pascalCase name}}AgentOutput, 24 | description: "A {{name}} agent to get weather, time, and holiday information with rich formatting", 25 | fn: async (input, ctx) => { 26 | const result = await {{camelCase name}}Toolbox.pickAndRun({ 27 | prompt: input.message, 28 | }); 29 | 30 | let geoData: any = {}; 31 | 32 | switch (result.name) { 33 | case "weather": 34 | geoData.weather = result.output; 35 | break; 36 | case "time": 37 | geoData.time = result.output; 38 | break; 39 | case "holiday": 40 | geoData.holiday = result.output; 41 | break; 42 | case "summary": 43 | return { 44 | message: result.output.formattedResponse, 45 | highlights: result.output.highlights, 46 | recommendations: result.output.recommendations, 47 | }; 48 | default: 49 | return {{camelCase name}}Toolbox.assertExhaustive(result); 50 | } 51 | 52 | // Use summary tool to format the response nicely 53 | const summaryResult = await summary.run({ 54 | data: geoData, 55 | userQuery: input.message 56 | }); 57 | 58 | return { 59 | message: summaryResult.formattedResponse, 60 | highlights: summaryResult.highlights, 61 | recommendations: summaryResult.recommendations, 62 | }; 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/plan-search.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { generateObject } from "ai"; 3 | import { openai } from "@ai-sdk/openai"; 4 | import { icepick } from "@/icepick-client"; 5 | 6 | export const PlanSearchInputSchema = z.object({ 7 | query: z.string(), 8 | existingFacts: z.array(z.string()).optional(), 9 | missingAspects: z.array(z.string()).optional(), 10 | }); 11 | 12 | const PlanSearchOutputSchema = z.object({ 13 | queries: z.array(z.string()), 14 | reasoning: z.string(), 15 | }); 16 | 17 | export type PlanSearchOutput = z.infer; 18 | 19 | export const planSearch = icepick.tool({ 20 | name: "plan-search", 21 | description: "Plan search queries to find information about a topic", 22 | inputSchema: PlanSearchInputSchema, 23 | outputSchema: PlanSearchOutputSchema, 24 | fn: async (input, ctx) => { 25 | 26 | const result = await generateObject({ 27 | abortSignal: ctx.abortController.signal, 28 | prompt: ` 29 | Plan search queries to find information about this topic: 30 | """${input.query}""" 31 | 32 | ${ 33 | input.existingFacts 34 | ? ` 35 | We already have these facts: 36 | ${input.existingFacts.map((fact, i) => `${i + 1}. ${fact}`).join("\n")} 37 | ` 38 | : "" 39 | } 40 | 41 | ${ 42 | input.missingAspects 43 | ? ` 44 | We need to find information about these missing aspects: 45 | ${input.missingAspects 46 | .map((aspect, i) => `${i + 1}. ${aspect}`) 47 | .join("\n")} 48 | ` 49 | : "" 50 | } 51 | 52 | Generate 3-5 specific search queries that will help us find new, relevant information. 53 | The queries should: 54 | 1. Focus on finding information about missing aspects 55 | 2. Avoid duplicating information we already have 56 | 3. Be specific enough to find relevant sources 57 | 4. Use different angles or perspectives to ensure diverse information 58 | `, 59 | model: openai("gpt-4o-mini"), 60 | schema: z.object({ 61 | queries: z.array(z.string()), 62 | reasoning: z.string(), 63 | }), 64 | }); 65 | 66 | return { 67 | queries: result.object.queries, 68 | reasoning: result.object.reasoning, 69 | }; 70 | }, 71 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/plan-search.tool.ts: -------------------------------------------------------------------------------- 1 | 2 | import { z } from "zod"; 3 | import { generateObject } from "ai"; 4 | import { openai } from "@ai-sdk/openai"; 5 | import { icepick } from "@/icepick-client"; 6 | 7 | export const PlanSearchInputSchema = z.object({ 8 | query: z.string(), 9 | existingFacts: z.array(z.string()).optional(), 10 | missingAspects: z.array(z.string()).optional(), 11 | }); 12 | 13 | const PlanSearchOutputSchema = z.object({ 14 | queries: z.array(z.string()), 15 | reasoning: z.string(), 16 | }); 17 | 18 | export type PlanSearchOutput = z.infer; 19 | 20 | export const planSearch = icepick.tool({ 21 | name: "plan-search", 22 | description: "Plan search queries to find information about a topic", 23 | inputSchema: PlanSearchInputSchema, 24 | outputSchema: PlanSearchOutputSchema, 25 | fn: async (input, ctx) => { 26 | 27 | const result = await generateObject({ 28 | abortSignal: ctx.abortController.signal, 29 | prompt: ` 30 | Plan search queries to find information about this topic: 31 | """${input.query}""" 32 | 33 | ${ 34 | input.existingFacts 35 | ? ` 36 | We already have these facts: 37 | ${input.existingFacts.map((fact, i) => `${i + 1}. ${fact}`).join("\n")} 38 | ` 39 | : "" 40 | } 41 | 42 | ${ 43 | input.missingAspects 44 | ? ` 45 | We need to find information about these missing aspects: 46 | ${input.missingAspects 47 | .map((aspect, i) => `${i + 1}. ${aspect}`) 48 | .join("\n")} 49 | ` 50 | : "" 51 | } 52 | 53 | Generate 3-5 specific search queries that will help us find new, relevant information. 54 | The queries should: 55 | 1. Focus on finding information about missing aspects 56 | 2. Avoid duplicating information we already have 57 | 3. Be specific enough to find relevant sources 58 | 4. Use different angles or perspectives to ensure diverse information 59 | `, 60 | model: openai("gpt-4.1-mini"), 61 | schema: z.object({ 62 | queries: z.array(z.string()), 63 | reasoning: z.string(), 64 | }), 65 | }); 66 | 67 | return { 68 | queries: result.object.queries, 69 | reasoning: result.object.reasoning, 70 | }; 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /cli/templates/deep-research/README.md.hbs: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | 3 | {{description}} - A comprehensive deep research agent built with Icepick that performs multi-iteration web searches to gather, analyze, and synthesize information from multiple sources. 4 | 5 | ## Getting Started 6 | 7 | 1. Install dependencies: 8 | ```bash 9 | pnpm install 10 | ``` 11 | 12 | 2. Set up your environment variables: 13 | ```bash 14 | cp .env.example .env 15 | ``` 16 | 17 | Edit `.env` and add your API keys (the Hatchet token can be generated at [Hatchet Cloud](https://cloud.onhatchet.run) or by [self-hosting Hatchet](https://docs.hatchet.run/self-hosting)). 18 | 19 | 3. Run the agent: 20 | ```bash 21 | pnpm run dev 22 | ``` 23 | 24 | 4. Trigger the interactive CLI: 25 | ```bash 26 | pnpm run trigger 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Interactive CLI 32 | 33 | The easiest way to interact with the {{name}} agent is through the interactive CLI: 34 | 35 | ```bash 36 | pnpm run trigger 37 | ``` 38 | 39 | This will launch an interactive menu where you can: 40 | - Enter research queries for comprehensive investigation 41 | - View detailed research results with sources and analysis 42 | - Save results to markdown files with full citations 43 | 44 | ### Development Mode 45 | 46 | Run the agent in development mode: 47 | ```bash 48 | pnpm run dev 49 | ``` 50 | 51 | ### Build and Run 52 | 53 | Build and run the compiled version: 54 | ```bash 55 | pnpm run build 56 | pnpm start 57 | ``` 58 | 59 | ## Project Structure 60 | 61 | - `src/agents/deep-research/` - Main deep research agent implementation 62 | - `src/agents/deep-research/tools/` - Comprehensive research tools 63 | - `search.tool.ts` - Web search using OpenAI's search preview 64 | - `plan-search.tool.ts` - Intelligent search query planning 65 | - `website-to-md.tool.ts` - Convert web pages to markdown 66 | - `extract-facts.tool.ts` - Extract key facts from sources 67 | - `judge-facts.tool.ts` - Evaluate fact completeness 68 | - `judge-results.tool.ts` - Assess research quality 69 | - `summarize.tool.ts` - Synthesize findings into coherent summaries 70 | - `src/trigger.ts` - Interactive CLI for running the agent 71 | - `src/main.ts` - Entry point for standard execution 72 | - `src/icepick-client.ts` - Icepick client configuration 73 | - `results/` - Generated research reports from trigger sessions 74 | 75 | ## Scripts 76 | 77 | - `pnpm run trigger` - Run the interactive deep research CLI 78 | - `pnpm run dev` - Run in development mode 79 | - `pnpm run build` - Build the project 80 | - `pnpm start` - Run the built project 81 | - `pnpm test` - Run tests 82 | - `pnpm run lint` - Run linting -------------------------------------------------------------------------------- /cli/templates/deep-research/src/tools/summarize.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | import { generateText } from "ai"; 4 | import { openai } from "@ai-sdk/openai"; 5 | 6 | export const SummarizeInputSchema = z.object({ 7 | text: z.string(), 8 | facts: z.array( 9 | z.object({ 10 | text: z.string(), 11 | sourceIndex: z.number(), 12 | }) 13 | ), 14 | sources: z.array( 15 | z.object({ 16 | url: z.string(), 17 | title: z.string().optional(), 18 | index: z.number(), 19 | }) 20 | ), 21 | }); 22 | 23 | export const SummarizeOutputSchema = z.object({ 24 | summary: z.string(), 25 | }); 26 | 27 | export const summarize = icepick.tool({ 28 | name: "summarize", 29 | description: "Summarize a set of facts", 30 | inputSchema: SummarizeInputSchema, 31 | outputSchema: SummarizeOutputSchema, 32 | fn: async (input, ctx) => { 33 | // Create a map of source indices to source information for easy lookup 34 | const sourceMap = new Map( 35 | input.sources.map((source) => [source.index, source]) 36 | ); 37 | 38 | // Group facts by source 39 | const factsBySource = new Map(); 40 | input.facts.forEach((fact, index) => { 41 | const facts = factsBySource.get(fact.sourceIndex) || []; 42 | facts.push(`${index + 1}. ${fact.text}`); 43 | factsBySource.set(fact.sourceIndex, facts); 44 | }); 45 | 46 | // Format facts grouped by source 47 | const formattedFacts = Array.from(factsBySource.entries()).map( 48 | ([sourceIndex, facts]) => { 49 | const source = sourceMap.get(sourceIndex); 50 | if (!source) { 51 | throw new Error(`Source with index ${sourceIndex} not found`); 52 | } 53 | return `From ${source.title || "Untitled"} (${ 54 | source.url 55 | }):\n${facts.join("\n")}`; 56 | } 57 | ); 58 | const result = await generateText({ 59 | abortSignal: ctx.abortController.signal, 60 | system: `You are a professional researcher helping to write a detailed report based on verified facts.`, 61 | prompt: ` 62 | Write a comprehensive summary based on these verified facts: 63 | 64 | ${formattedFacts.join("\n\n")} 65 | 66 | Requirements: 67 | 1. The summary should be based ONLY on the provided facts 68 | 2. Each fact should be referenced using its number in brackets (e.g. [1], [2]) 69 | 3. The summary should be well-structured and flow logically 70 | 4. The summary should be written in the style of a professional researcher 71 | 5. The summary should be written in the language of the original query 72 | 6. Include a "Sources" section at the end listing all referenced sources with their numbers 73 | 7. Write the summary in markdown format and present relevant information in a table format 74 | 75 | Original query: 76 | """ 77 | ${input.text} 78 | """ 79 | `, 80 | model: openai("gpt-4o-mini"), 81 | }); 82 | 83 | return { 84 | summary: result.text, 85 | }; 86 | }, 87 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/tools/summarize.tool.ts: -------------------------------------------------------------------------------- 1 | 2 | import { z } from "zod"; 3 | import { icepick } from "@/icepick-client"; 4 | import { generateText } from "ai"; 5 | import { openai } from "@ai-sdk/openai"; 6 | 7 | export const SummarizeInputSchema = z.object({ 8 | text: z.string(), 9 | facts: z.array( 10 | z.object({ 11 | text: z.string(), 12 | sourceIndex: z.number(), 13 | }) 14 | ), 15 | sources: z.array( 16 | z.object({ 17 | url: z.string(), 18 | title: z.string().optional(), 19 | index: z.number(), 20 | }) 21 | ), 22 | }); 23 | 24 | export const SummarizeOutputSchema = z.object({ 25 | summary: z.string(), 26 | }); 27 | 28 | export const summarize = icepick.tool({ 29 | name: "summarize", 30 | description: "Summarize a set of facts", 31 | inputSchema: SummarizeInputSchema, 32 | outputSchema: SummarizeOutputSchema, 33 | fn: async (input, ctx) => { 34 | // Create a map of source indices to source information for easy lookup 35 | const sourceMap = new Map( 36 | input.sources.map((source) => [source.index, source]) 37 | ); 38 | 39 | // Group facts by source 40 | const factsBySource = new Map(); 41 | input.facts.forEach((fact, index) => { 42 | const facts = factsBySource.get(fact.sourceIndex) || []; 43 | facts.push(`${index + 1}. ${fact.text}`); 44 | factsBySource.set(fact.sourceIndex, facts); 45 | }); 46 | 47 | // Format facts grouped by source 48 | const formattedFacts = Array.from(factsBySource.entries()).map( 49 | ([sourceIndex, facts]) => { 50 | const source = sourceMap.get(sourceIndex); 51 | if (!source) { 52 | throw new Error(`Source with index ${sourceIndex} not found`); 53 | } 54 | return `From ${source.title || "Untitled"} (${ 55 | source.url 56 | }):\n${facts.join("\n")}`; 57 | } 58 | ); 59 | const result = await generateText({ 60 | abortSignal: ctx.abortController.signal, 61 | system: `You are a professional researcher helping to write a detailed report based on verified facts.`, 62 | prompt: ` 63 | Write a comprehensive summary based on these verified facts: 64 | 65 | ${formattedFacts.join("\n\n")} 66 | 67 | Requirements: 68 | 1. The summary should be based ONLY on the provided facts 69 | 2. Each fact should be referenced using its number in brackets (e.g. [1], [2]) 70 | 3. The summary should be well-structured and flow logically 71 | 4. The summary should be written in the style of a professional researcher 72 | 5. The summary should be written in the language of the original query 73 | 6. Include a "Sources" section at the end listing all referenced sources with their numbers 74 | 7. Write the summary in markdown format and present relevant information in a table format 75 | 76 | Original query: 77 | """ 78 | ${input.text} 79 | """ 80 | `, 81 | model: openai("gpt-4.1-mini"), 82 | }); 83 | 84 | return { 85 | summary: result.text, 86 | }; 87 | }, 88 | }); 89 | -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/1.prompt-chaining/prompt-chaining.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { oneTool } from "./tools/one.tool"; 4 | import { twoTool } from "./tools/two.tool"; 5 | import { threeTool } from "./tools/three.tool"; 6 | 7 | /** 8 | * PROMPT CHAINING PATTERN 9 | * 10 | * Based on Anthropic's "Building Effective Agents" blog post: 11 | * https://www.anthropic.com/engineering/building-effective-agents 12 | * 13 | * Pattern Description: 14 | * Prompt chaining decomposes a task into a sequence of steps, where each LLM call 15 | * processes the output of the previous one. You can add programmatic checks (gates) 16 | * on intermediate steps to ensure the process stays on track. 17 | * 18 | * When to use: 19 | * - Tasks can be easily decomposed into fixed subtasks 20 | * - Trading latency for higher accuracy by making each LLM call easier 21 | * - Need validation gates between steps 22 | * 23 | * Examples: 24 | * - Generating marketing copy, then translating it 25 | * - Writing outline → checking criteria → writing full document 26 | * - Multi-step content processing with validation 27 | */ 28 | 29 | const PromptChainingAgentInput = z.object({ 30 | message: z.string(), 31 | }); 32 | 33 | const PromptChainingAgentOutput = z.object({ 34 | result: z.string(), 35 | }); 36 | 37 | export const promptChainingAgent = icepick.agent({ 38 | name: "prompt-chaining-agent", 39 | executionTimeout: "1m", 40 | inputSchema: PromptChainingAgentInput, 41 | outputSchema: PromptChainingAgentOutput, 42 | description: "Demonstrates prompt chaining: sequential LLM calls with validation gates", 43 | fn: async (input, ctx) => { 44 | 45 | // STEP 1: First LLM call - Process the initial message 46 | // This step determines if the message is about an animal 47 | const { oneOutput } = await oneTool.run({ 48 | message: input.message, 49 | }); 50 | 51 | // GATE: Programmatic validation check between steps 52 | // This is a key feature of prompt chaining - we can validate intermediate results 53 | // and control the flow based on that validation 54 | if(!oneOutput) { 55 | // FAIL: If validation fails, we can terminate early or redirect 56 | return { 57 | result: 'Please provide a message about an animal' 58 | } 59 | } 60 | 61 | // PASS: If validation succeeds, continue to next step 62 | // STEP 2: Second LLM call - Transform the validated input 63 | // Since we know it's about an animal, translate to Spanish 64 | const { twoOutput } = await twoTool.run({ 65 | message: input.message, 66 | }); 67 | 68 | // STEP 3: Third LLM call - Final transformation 69 | // Convert the Spanish message into a haiku format 70 | const { threeOutput } = await threeTool.run({ 71 | twoOutput, // Note: Using output from previous step as input 72 | }); 73 | 74 | // Return the final processed result 75 | return { 76 | result: threeOutput 77 | } 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/2.routing/routing.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { salesTool, supportTool } from "./tools/calls.tool"; 4 | 5 | /** 6 | * ROUTING PATTERN 7 | * 8 | * Based on Anthropic's "Building Effective Agents" blog post: 9 | * https://www.anthropic.com/engineering/building-effective-agents 10 | * 11 | * Pattern Description: 12 | * Routing classifies an input and directs it to a specialized followup task. 13 | * This allows separation of concerns and building more specialized prompts 14 | * without one input type hurting performance on others. 15 | * 16 | * When to use: 17 | * - Complex tasks with distinct categories better handled separately 18 | * - Classification can be handled accurately by LLM or traditional algorithms 19 | * - Need specialized handling for different input types 20 | * 21 | * Examples: 22 | * - Customer service: routing questions, refunds, technical support 23 | * - Multi-model routing: easy questions to smaller models, hard to larger 24 | * - Content classification with specialized processors 25 | */ 26 | 27 | const RoutingAgentInput = z.object({ 28 | message: z.string(), 29 | }); 30 | 31 | const RoutingAgentOutput = z.object({ 32 | message: z.string(), 33 | canHelp: z.boolean(), 34 | }); 35 | 36 | export const routingToolbox = icepick.toolbox({ 37 | tools: [supportTool, salesTool], 38 | }); 39 | 40 | export const routingAgent = icepick.agent({ 41 | name: "routing-agent", 42 | executionTimeout: "1m", 43 | inputSchema: RoutingAgentInput, 44 | outputSchema: RoutingAgentOutput, 45 | description: "Demonstrates routing: classify input and direct to specialized handlers", 46 | fn: async (input, ctx) => { 47 | 48 | // STEP 1: Classification - Determine the type of request 49 | // This is the key step in routing - understanding what kind of input we have 50 | // so we can direct it to the most appropriate specialized handler 51 | const route = await routingToolbox.pickAndRun({ 52 | prompt: input.message, 53 | }); 54 | 55 | // STEP 2: Route to specialized handler based on classification 56 | // Each case represents a different specialized workflow optimized for that type 57 | switch(route.name) { 58 | case "support-tool": { 59 | // Route to support-specialized LLM with support-specific tools and prompts 60 | return { 61 | message: route.output.response, 62 | canHelp: true, 63 | } 64 | } 65 | case "sales-tool": { 66 | // Route to sales-specialized LLM with sales-specific tools and prompts 67 | return { 68 | message: route.output.response, 69 | canHelp: true, 70 | } 71 | } 72 | default: 73 | routingToolbox.assertExhaustive(route); 74 | return { 75 | message: "I am sorry, I cannot help with that yet.", 76 | canHelp: false, 77 | } 78 | } 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /cli/templates/geo/src/tools/holiday.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | 4 | interface HolidayResponse { 5 | date: string; 6 | localName: string; 7 | name: string; 8 | countryCode: string; 9 | fixed: boolean; 10 | global: boolean; 11 | counties: string[] | null; 12 | launchYear: number | null; 13 | types: string[]; 14 | } 15 | 16 | export const holiday = icepick.tool({ 17 | name: "holiday", 18 | description: "Get the current holiday in a given country", 19 | inputSchema: z.object({ 20 | country: z.string() 21 | }), 22 | outputSchema: z.object({ 23 | country: z.string(), 24 | holidayName: z.string().optional(), 25 | holidayDate: z.string().optional(), 26 | isToday: z.boolean(), 27 | nextHoliday: z.string().optional(), 28 | summary: z.string() 29 | }), 30 | fn: async (input) => { 31 | try { 32 | const currentYear = new Date().getFullYear(); 33 | const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format 34 | 35 | // Use public holidays API 36 | const holidayUrl = `https://date.nager.at/api/v3/PublicHolidays/${currentYear}/${encodeURIComponent(input.country)}`; 37 | const response = await fetch(holidayUrl); 38 | 39 | if (!response.ok) { 40 | throw new Error(`Country '${input.country}' not found or API unavailable`); 41 | } 42 | 43 | const holidays: HolidayResponse[] = await response.json(); 44 | 45 | // Find today's holiday 46 | const todayHoliday = holidays.find(holiday => holiday.date === today); 47 | 48 | if (todayHoliday) { 49 | return { 50 | country: input.country, 51 | holidayName: todayHoliday.localName || todayHoliday.name, 52 | holidayDate: todayHoliday.date, 53 | isToday: true, 54 | summary: `Today is ${todayHoliday.localName || todayHoliday.name} in ${input.country}` 55 | }; 56 | } 57 | 58 | // Find next upcoming holiday 59 | const upcomingHoliday = holidays 60 | .filter(holiday => holiday.date > today) 61 | .sort((a, b) => a.date.localeCompare(b.date))[0]; 62 | 63 | if (upcomingHoliday) { 64 | const holidayDate = new Date(upcomingHoliday.date); 65 | return { 66 | country: input.country, 67 | holidayName: upcomingHoliday.localName || upcomingHoliday.name, 68 | holidayDate: upcomingHoliday.date, 69 | isToday: false, 70 | nextHoliday: `${upcomingHoliday.localName || upcomingHoliday.name} on ${holidayDate.toLocaleDateString()}`, 71 | summary: `Next holiday in ${input.country}: ${upcomingHoliday.localName || upcomingHoliday.name} on ${holidayDate.toLocaleDateString()}` 72 | }; 73 | } 74 | 75 | return { 76 | country: input.country, 77 | isToday: false, 78 | summary: `No upcoming holidays found for ${input.country} this year` 79 | }; 80 | } catch (error) { 81 | return { 82 | country: input.country, 83 | isToday: false, 84 | summary: `Unable to get holiday information for ${input.country}: ${error instanceof Error ? error.message : 'Unknown error'}` 85 | }; 86 | } 87 | } 88 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/1.sectioning/sectioning.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { appropriatenessCheckTool } from "./tools/appropriateness.tool"; 4 | import { mainContentTool } from "./tools/main-content.tool"; 5 | 6 | /** 7 | * SECTIONING PARALLELIZATION PATTERN 8 | * 9 | * Based on Anthropic's "Building Effective Agents" blog post: 10 | * https://www.anthropic.com/engineering/building-effective-agents 11 | * 12 | * Pattern Description: 13 | * Breaking a task into independent subtasks that run simultaneously, then 14 | * aggregating results programmatically. This is one of two parallelization 15 | * variations (the other being voting). 16 | * 17 | * When to use: 18 | * - Independent subtasks can be parallelized for speed 19 | * - Multiple considerations need separate focused attention 20 | * - Implementing guardrails alongside main processing 21 | * 22 | * Examples: 23 | * - Guardrails: One model processes queries while another screens for inappropriate content 24 | * - Code review: Multiple aspects evaluated simultaneously 25 | * - Multi-faceted analysis requiring separate specialized attention 26 | * 27 | * Key Insight: 28 | * Anthropic found that LLMs generally perform better when each consideration 29 | * is handled by a separate LLM call, allowing focused attention on each specific aspect. 30 | */ 31 | 32 | const SectioningAgentInput = z.object({ 33 | message: z.string(), 34 | }); 35 | 36 | const SectioningAgentOutput = z.object({ 37 | response: z.string(), 38 | isAppropriate: z.boolean(), 39 | }); 40 | 41 | export const sectioningAgent = icepick.agent({ 42 | name: "sectioning-agent", 43 | executionTimeout: "2m", 44 | inputSchema: SectioningAgentInput, 45 | outputSchema: SectioningAgentOutput, 46 | description: "Demonstrates sectioning: parallel independent subtasks with focused attention", 47 | fn: async (input, ctx) => { 48 | 49 | // PARALLEL EXECUTION: Run independent subtasks simultaneously 50 | // This is the core of sectioning - instead of running tasks sequentially, 51 | // we run them in parallel because they address different concerns 52 | // 53 | // Task 1: Appropriateness check (guardrail) 54 | // Task 2: Main content generation 55 | // 56 | // These are independent - the appropriateness check doesn't need the main content 57 | // to do its job, and vice versa. This allows for significant speed improvements. 58 | const [{isAppropriate, reason}, mainResult] = await Promise.all([ 59 | appropriatenessCheckTool.run({ message: input.message }), 60 | mainContentTool.run({ message: input.message }), 61 | ]); 62 | 63 | // AGGREGATION: Combine results with business logic 64 | // The appropriateness check acts as a guardrail - if content is inappropriate, 65 | // we discard the main content and return a safety message 66 | if (!isAppropriate) { 67 | return { 68 | response: `I cannot provide a response to that request. ${reason}`, 69 | isAppropriate: false, 70 | }; 71 | } 72 | 73 | // If appropriate, return the main content 74 | return { 75 | response: mainResult.mainContent, 76 | isAppropriate: true, 77 | }; 78 | }, 79 | }); 80 | 81 | [sectioningAgent]; -------------------------------------------------------------------------------- /cli/templates/blank/README.md.hbs: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | 3 | A geographic information agent built with Icepick that provides weather, time, and holiday information for any location. 4 | 5 | ## Getting Started 6 | 7 | 1. Install dependencies: 8 | ```bash 9 | pnpm install 10 | ``` 11 | 12 | 2. Set up your environment variables: 13 | ```bash 14 | cp .env.example .env 15 | ``` 16 | 17 | Edit `.env` and add your API keys (the Hatchet token can be generated at [Hatchet Cloud](https://cloud.onhatchet.run) or by [self-hosting Hatchet]((https://docs.hatchet.run/self-hosting))). 18 | 19 | 3. Run the agent: 20 | ```bash 21 | pnpm run dev 22 | ``` 23 | 24 | 4. Trigger the interactive CLI: 25 | ```bash 26 | pnpm run trigger 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Interactive CLI 32 | 33 | The easiest way to interact with the {{name}} agent is through the interactive CLI: 34 | 35 | ```bash 36 | pnpm run trigger 37 | ``` 38 | 39 | This will launch an interactive menu where you can: 40 | - Choose from preset queries (weather, time, holidays) 41 | - Enter custom queries 42 | - View results and save them to markdown files 43 | 44 | ### Development Mode 45 | 46 | Run the agent in development mode: 47 | ```bash 48 | pnpm run dev 49 | ``` 50 | 51 | ### Build and Run 52 | 53 | Build and run the compiled version: 54 | ```bash 55 | pnpm run build 56 | pnpm start 57 | ``` 58 | 59 | ## Project Structure 60 | 61 | - `src/agents/{{kebabCase name}}/` - Main agent implementation 62 | - `src/agents/{{kebabCase name}}/tools/` - Tools used by the agent 63 | - `weather.ts` - Weather information using Open-Meteo API 64 | - `time.ts` - Time information using WorldTimeAPI 65 | - `holiday.ts` - Holiday information using Nager.Date API 66 | - `src/trigger.ts` - Interactive CLI for running the agent 67 | - `src/main.ts` - Entry point for standard execution 68 | - `src/icepick-client.ts` - Icepick client configuration 69 | - `results/` - Generated result files from trigger sessions 70 | 71 | ## Available Tools 72 | 73 | ### Weather Tool 74 | Get current weather and forecasts for any city worldwide. 75 | - Uses Open-Meteo API (no API key required) 76 | - Provides temperature, conditions, humidity, wind speed 77 | - Example: "What's the weather in Tokyo?" 78 | 79 | ### Time Tool 80 | Get current time and timezone information for any location. 81 | - Uses WorldTimeAPI (no API key required) 82 | - Example: "What time is it in London?" 83 | 84 | ### Holiday Tool 85 | Get information about public holidays in any country. 86 | - Uses Nager.Date API (no API key required) 87 | - Supports current holidays and upcoming holidays 88 | - Example: "What holidays are coming up in Canada?" 89 | 90 | ## Environment Variables 91 | 92 | | Variable | Required | Description | 93 | |----------|----------|-------------| 94 | | `OPENAI_API_KEY` | Yes | Your OpenAI API key for the language model | 95 | | `HATCHET_CLIENT_TOKEN` | Yes | Your Hatchet API token for orchestration | 96 | 97 | ## Example Queries 98 | 99 | - "What's the current weather in Paris?" 100 | - "What time is it in Sydney right now?" 101 | - "Are there any holidays today in Germany?" 102 | - "Tell me about the weather forecast for New York this week" 103 | - "What's the time difference between Los Angeles and Tokyo?" 104 | 105 | ## Scripts 106 | 107 | - `pnpm run trigger` - Run the interactive agent CLI 108 | - `pnpm run dev` - Run in development mode 109 | - `pnpm run build` - Build the project 110 | - `pnpm start` - Run the built project 111 | - `pnpm test` - Run tests 112 | - `pnpm run lint` - Run linting -------------------------------------------------------------------------------- /cli/templates/geo/README.md.hbs: -------------------------------------------------------------------------------- 1 | # {{name}} 2 | 3 | A geographic information agent built with Icepick that provides weather, time, and holiday information for any location. 4 | 5 | ## Getting Started 6 | 7 | 1. Install dependencies: 8 | ```bash 9 | pnpm install 10 | ``` 11 | 12 | 2. Set up your environment variables: 13 | ```bash 14 | cp .env.example .env 15 | ``` 16 | 17 | Edit `.env` and add your API keys (the Hatchet token can be generated at [Hatchet Cloud](https://cloud.onhatchet.run) or by [self-hosting Hatchet]((https://docs.hatchet.run/self-hosting))). 18 | 19 | 3. Run the agent: 20 | ```bash 21 | pnpm run dev 22 | ``` 23 | 24 | 4. Trigger the interactive CLI: 25 | ```bash 26 | pnpm run trigger 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Interactive CLI 32 | 33 | The easiest way to interact with the {{name}} agent is through the interactive CLI: 34 | 35 | ```bash 36 | pnpm run trigger 37 | ``` 38 | 39 | This will launch an interactive menu where you can: 40 | - Choose from preset queries (weather, time, holidays) 41 | - Enter custom queries 42 | - View results and save them to markdown files 43 | 44 | ### Development Mode 45 | 46 | Run the agent in development mode: 47 | ```bash 48 | pnpm run dev 49 | ``` 50 | 51 | ### Build and Run 52 | 53 | Build and run the compiled version: 54 | ```bash 55 | pnpm run build 56 | pnpm start 57 | ``` 58 | 59 | ## Project Structure 60 | 61 | - `src/agents/{{kebabCase name}}/` - Main agent implementation 62 | - `src/agents/{{kebabCase name}}/tools/` - Tools used by the agent 63 | - `weather.ts` - Weather information using Open-Meteo API 64 | - `time.ts` - Time information using WorldTimeAPI 65 | - `holiday.ts` - Holiday information using Nager.Date API 66 | - `src/trigger.ts` - Interactive CLI for running the agent 67 | - `src/main.ts` - Entry point for standard execution 68 | - `src/icepick-client.ts` - Icepick client configuration 69 | - `results/` - Generated result files from trigger sessions 70 | 71 | ## Available Tools 72 | 73 | ### Weather Tool 74 | Get current weather and forecasts for any city worldwide. 75 | - Uses Open-Meteo API (no API key required) 76 | - Provides temperature, conditions, humidity, wind speed 77 | - Example: "What's the weather in Tokyo?" 78 | 79 | ### Time Tool 80 | Get current time and timezone information for any location. 81 | - Uses WorldTimeAPI (no API key required) 82 | - Example: "What time is it in London?" 83 | 84 | ### Holiday Tool 85 | Get information about public holidays in any country. 86 | - Uses Nager.Date API (no API key required) 87 | - Supports current holidays and upcoming holidays 88 | - Example: "What holidays are coming up in Canada?" 89 | 90 | ## Environment Variables 91 | 92 | | Variable | Required | Description | 93 | |----------|----------|-------------| 94 | | `OPENAI_API_KEY` | Yes | Your OpenAI API key for the language model | 95 | | `HATCHET_CLIENT_TOKEN` | Yes | Your Hatchet API token for orchestration | 96 | 97 | ## Example Queries 98 | 99 | - "What's the current weather in Paris?" 100 | - "What time is it in Sydney right now?" 101 | - "Are there any holidays today in Germany?" 102 | - "Tell me about the weather forecast for New York this week" 103 | - "What's the time difference between Los Angeles and Tokyo?" 104 | 105 | ## Scripts 106 | 107 | - `pnpm run trigger` - Run the interactive agent CLI 108 | - `pnpm run dev` - Run in development mode 109 | - `pnpm run build` - Build the project 110 | - `pnpm start` - Run the built project 111 | - `pnpm test` - Run tests 112 | - `pnpm run lint` - Run linting -------------------------------------------------------------------------------- /cli/src/commands/__tests__/add-agent.test.ts: -------------------------------------------------------------------------------- 1 | import { createAgent } from '../add-agent'; 2 | import { promises as fs } from 'fs'; 3 | import { processTemplate, getTemplatePath, updateBarrelFile } from '../../utils'; 4 | 5 | // Mock dependencies 6 | jest.mock('fs', () => ({ 7 | promises: { 8 | access: jest.fn(), 9 | mkdir: jest.fn(), 10 | }, 11 | })); 12 | 13 | jest.mock('../../utils', () => ({ 14 | processTemplate: jest.fn(), 15 | getTemplatePath: jest.fn(), 16 | updateBarrelFile: jest.fn(), 17 | })); 18 | 19 | // Mock console methods 20 | const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(); 21 | const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(); 22 | 23 | const mockedFs = fs as jest.Mocked; 24 | const mockedProcessTemplate = processTemplate as jest.MockedFunction; 25 | const mockedGetTemplatePath = getTemplatePath as jest.MockedFunction; 26 | const mockedUpdateBarrelFile = updateBarrelFile as jest.MockedFunction; 27 | 28 | describe('add-agent command', () => { 29 | beforeEach(() => { 30 | jest.clearAllMocks(); 31 | mockConsoleLog.mockClear(); 32 | mockConsoleError.mockClear(); 33 | }); 34 | 35 | afterAll(() => { 36 | mockConsoleLog.mockRestore(); 37 | mockConsoleError.mockRestore(); 38 | }); 39 | 40 | describe('createAgent', () => { 41 | it('should create agent with correct next steps', async () => { 42 | // Mock that directory doesn't exist (fs.access throws) 43 | mockedFs.access.mockRejectedValueOnce(new Error('Directory does not exist')); 44 | mockedFs.mkdir.mockResolvedValue(undefined); 45 | mockedProcessTemplate.mockResolvedValueOnce([]); 46 | mockedGetTemplatePath.mockReturnValueOnce('/mock/templates/agent'); 47 | mockedUpdateBarrelFile.mockResolvedValueOnce(undefined); 48 | 49 | const result = await createAgent('my-agent', { 50 | description: 'Sample agent description', 51 | silent: false 52 | }); 53 | 54 | expect(result.success).toBe(true); 55 | expect(result.message).toBe("Agent 'my-agent' created successfully"); 56 | 57 | // Verify the next steps don't mention tests 58 | expect(mockConsoleLog).toHaveBeenCalledWith('1. Import your tools and add them to the toolbox'); 59 | expect(mockConsoleLog).toHaveBeenCalledWith('2. Implement tool result handling in the switch statement'); 60 | expect(mockConsoleLog).toHaveBeenCalledWith('3. Update the agent implementation as needed'); 61 | 62 | // Verify it doesn't mention running tests or test functionality 63 | expect(mockConsoleLog).not.toHaveBeenCalledWith(expect.stringContaining('Run the tests')); 64 | expect(mockConsoleLog).not.toHaveBeenCalledWith(expect.stringContaining('test to verify')); 65 | expect(mockConsoleLog).not.toHaveBeenCalledWith(expect.stringContaining('tests to verify')); 66 | }); 67 | 68 | it('should work in silent mode', async () => { 69 | mockedFs.access.mockRejectedValueOnce(new Error('Directory does not exist')); 70 | mockedFs.mkdir.mockResolvedValue(undefined); 71 | mockedProcessTemplate.mockResolvedValueOnce([]); 72 | mockedGetTemplatePath.mockReturnValueOnce('/mock/templates/agent'); 73 | mockedUpdateBarrelFile.mockResolvedValueOnce(undefined); 74 | 75 | const result = await createAgent('my-agent', { 76 | description: 'Sample agent description', 77 | silent: true 78 | }); 79 | 80 | expect(result.success).toBe(true); 81 | expect(mockConsoleLog).not.toHaveBeenCalled(); 82 | }); 83 | }); 84 | }); -------------------------------------------------------------------------------- /cli/src/commands/add-agent.ts: -------------------------------------------------------------------------------- 1 | import prompts from "prompts"; 2 | import * as path from "path"; 3 | import { promises as fs } from "fs"; 4 | import { processTemplate, getTemplatePath, updateBarrelFile } from "../utils"; 5 | 6 | interface AgentConfig { 7 | name: string; 8 | description: string; 9 | } 10 | 11 | // Core logic for adding an agent 12 | export async function createAgent( 13 | name: string, 14 | options: { model?: string; description?: string; silent?: boolean } 15 | ) { 16 | try { 17 | if (!options.silent) { 18 | console.log(`🤖 Creating agent: ${name}`); 19 | } 20 | 21 | // Verify agents directory exists 22 | const agentsDir = path.join(process.cwd(), "src", "agents"); 23 | await ensureAgentsDirectory(agentsDir, options.silent); 24 | 25 | // Get agent configuration - use provided description or prompt interactively 26 | const config = options.description 27 | ? { name, description: options.description } 28 | : await getAgentConfig(name); 29 | 30 | // Process templates - resolve template path for both dev and bundled environments 31 | const outputDir = agentsDir; 32 | const templatesDir = getTemplatePath("agent", __dirname); 33 | 34 | await processTemplate({ type: "local", path: templatesDir }, config, { 35 | outputDir, 36 | force: false, 37 | }); 38 | 39 | // Update barrel file if it exists 40 | await updateBarrelFile(outputDir, config.name, "agent", options.silent); 41 | 42 | if (!options.silent) { 43 | console.log(`\n✅ Agent '${config.name}' created successfully!`); 44 | console.log( 45 | `📁 File created: ${path.join(outputDir, `${config.name}.agent.ts`)}` 46 | ); 47 | console.log("\n📝 Next steps:"); 48 | console.log("1. Import your tools and add them to the toolbox"); 49 | console.log("2. Implement tool result handling in the switch statement"); 50 | console.log("3. Update the agent implementation as needed"); 51 | } 52 | 53 | return { 54 | success: true, 55 | message: `Agent '${config.name}' created successfully`, 56 | outputDir, 57 | config, 58 | }; 59 | } catch (error) { 60 | const errorMessage = 61 | error instanceof Error ? error.message : "Unknown error"; 62 | if (!options.silent) { 63 | console.error("❌ Failed to create agent:", errorMessage); 64 | process.exit(1); 65 | } 66 | throw new Error(`Failed to create agent: ${errorMessage}`); 67 | } 68 | } 69 | 70 | // CLI wrapper function that doesn't return a value 71 | export async function addAgent(name: string, options: { model?: string }) { 72 | await createAgent(name, options); 73 | } 74 | 75 | async function ensureAgentsDirectory( 76 | agentsDir: string, 77 | silent?: boolean 78 | ): Promise { 79 | try { 80 | await fs.access(agentsDir); 81 | } catch { 82 | if (!silent) { 83 | console.log(`📁 Creating agents directory: ${agentsDir}`); 84 | } 85 | await fs.mkdir(agentsDir, { recursive: true }); 86 | } 87 | } 88 | 89 | async function getAgentConfig(name: string): Promise { 90 | const answers = await prompts({ 91 | type: "text", 92 | name: "description", 93 | message: "Agent description:", 94 | initial: `AI agent for ${name} tasks`, 95 | }); 96 | 97 | // Handle user cancellation 98 | if (!answers.description) { 99 | console.log("\n❌ Agent creation cancelled"); 100 | process.exit(0); 101 | } 102 | 103 | return { 104 | name, 105 | description: answers.description, 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /cli/templates/geo/src/tools/weather.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import { z } from "zod"; 3 | 4 | const WeatherInput = z.object({ 5 | city: z.string().describe("The city to get the weather for") 6 | }); 7 | 8 | const WeatherOutput = z.object({ 9 | city: z.string(), 10 | temperature: z.number(), 11 | feelsLike: z.number(), 12 | condition: z.string(), 13 | humidity: z.number(), 14 | windSpeed: z.number(), 15 | summary: z.string() 16 | }); 17 | 18 | interface WeatherResponse { 19 | current: { 20 | temperature_2m: number; 21 | apparent_temperature: number; 22 | relative_humidity_2m: number; 23 | wind_speed_10m: number; 24 | wind_gusts_10m: number; 25 | weather_code: number; 26 | }; 27 | } 28 | 29 | const weatherCodes: Record = { 30 | 0: "Clear sky", 31 | 1: "Mainly clear", 32 | 2: "Partly cloudy", 33 | 3: "Overcast", 34 | 45: "Fog", 35 | 48: "Depositing rime fog", 36 | 51: "Light drizzle", 37 | 53: "Moderate drizzle", 38 | 55: "Dense drizzle", 39 | 61: "Slight rain", 40 | 63: "Moderate rain", 41 | 65: "Heavy rain", 42 | 71: "Slight snow fall", 43 | 73: "Moderate snow fall", 44 | 75: "Heavy snow fall", 45 | 95: "Thunderstorm", 46 | 96: "Thunderstorm with slight hail", 47 | 99: "Thunderstorm with heavy hail" 48 | }; 49 | 50 | export const weather = icepick.tool({ 51 | name: "weather", 52 | description: "Get the weather in a given city", 53 | inputSchema: WeatherInput, 54 | outputSchema: WeatherOutput, 55 | fn: async (input) => { 56 | try { 57 | // Get coordinates for the city 58 | const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(input.city)}&count=1`; 59 | const geocodingResponse = await fetch(geocodingUrl); 60 | const geocodingData = await geocodingResponse.json(); 61 | 62 | if (!geocodingData.results?.[0]) { 63 | throw new Error(`Location '${input.city}' not found`); 64 | } 65 | 66 | const { latitude, longitude, name } = geocodingData.results[0]; 67 | 68 | // Get weather data 69 | const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`; 70 | 71 | const response = await fetch(weatherUrl); 72 | const data: WeatherResponse = await response.json(); 73 | 74 | const weatherDescription = weatherCodes[data.current.weather_code] || "Unknown"; 75 | const temperature = Math.round(data.current.temperature_2m); 76 | const feelsLike = Math.round(data.current.apparent_temperature); 77 | const humidity = data.current.relative_humidity_2m; 78 | const windSpeed = Math.round(data.current.wind_speed_10m); 79 | 80 | return { 81 | city: name, 82 | temperature, 83 | feelsLike, 84 | condition: weatherDescription, 85 | humidity, 86 | windSpeed, 87 | summary: `${weatherDescription} in ${name}. Temperature: ${temperature}°C (feels like ${feelsLike}°C). Humidity: ${humidity}%. Wind: ${windSpeed} km/h.` 88 | }; 89 | } catch (error) { 90 | const errorMsg = `Unable to get weather for ${input.city}: ${error instanceof Error ? error.message : 'Unknown error'}`; 91 | return { 92 | city: input.city, 93 | temperature: 0, 94 | feelsLike: 0, 95 | condition: "Unknown", 96 | humidity: 0, 97 | windSpeed: 0, 98 | summary: errorMsg 99 | }; 100 | } 101 | }, 102 | }); -------------------------------------------------------------------------------- /cli/src/commands/add-tool.ts: -------------------------------------------------------------------------------- 1 | import prompts from "prompts"; 2 | import * as path from "path"; 3 | import { promises as fs } from "fs"; 4 | import { processTemplate, getTemplatePath, updateBarrelFile } from "../utils"; 5 | 6 | interface ToolConfig { 7 | name: string; 8 | description: string; 9 | } 10 | 11 | // Core logic for adding a tool 12 | export async function createTool( 13 | name: string, 14 | options: { description?: string; silent?: boolean } 15 | ) { 16 | try { 17 | if (!options.silent) { 18 | console.log(`🛠️ Creating tool: ${name}`); 19 | } 20 | 21 | // Verify tools directory exists 22 | const toolsDir = path.join(process.cwd(), "src", "tools"); 23 | await ensureToolsDirectory(toolsDir, options.silent); 24 | 25 | // Get tool configuration - use provided description or prompt interactively 26 | const config = options.description 27 | ? { 28 | name, 29 | description: options.description, 30 | } 31 | : await getToolConfig(name); 32 | 33 | // Process templates - resolve template path for both dev and bundled environments 34 | const outputDir = toolsDir; 35 | const templatesDir = getTemplatePath("tool", __dirname); 36 | 37 | await processTemplate({ type: "local", path: templatesDir }, config, { 38 | outputDir, 39 | force: false, 40 | }); 41 | 42 | // Update barrel file if it exists 43 | const toolFileName = name 44 | .replace(/([a-z])([A-Z])/g, "$1-$2") 45 | .toLowerCase() 46 | .replace(/[\s_]+/g, "-"); 47 | await updateBarrelFile(outputDir, toolFileName, "tool", options.silent); 48 | 49 | if (!options.silent) { 50 | console.log(`\n✅ Tool '${config.name}' created successfully!`); 51 | console.log( 52 | `📁 File created: ${path.join(outputDir, `${toolFileName}.tool.ts`)}` 53 | ); 54 | console.log("\n📝 Next steps:"); 55 | console.log("1. Define your input and output schemas in the tool file"); 56 | console.log("2. Implement the tool logic in the fn function"); 57 | console.log("3. Import and add the tool to your agent's toolbox"); 58 | } 59 | 60 | return { 61 | success: true, 62 | message: `Tool '${config.name}' created successfully`, 63 | outputDir, 64 | config, 65 | }; 66 | } catch (error) { 67 | const errorMessage = 68 | error instanceof Error ? error.message : "Unknown error"; 69 | if (!options.silent) { 70 | console.error("❌ Failed to create tool:", errorMessage); 71 | process.exit(1); 72 | } 73 | throw new Error(`Failed to create tool: ${errorMessage}`); 74 | } 75 | } 76 | 77 | // CLI wrapper function that doesn't return a value 78 | export async function addTool(name: string) { 79 | await createTool(name, {}); 80 | } 81 | 82 | async function ensureToolsDirectory( 83 | toolsDir: string, 84 | silent?: boolean 85 | ): Promise { 86 | try { 87 | await fs.access(toolsDir); 88 | } catch { 89 | if (!silent) { 90 | console.log(`📁 Creating tools directory: ${toolsDir}`); 91 | } 92 | await fs.mkdir(toolsDir, { recursive: true }); 93 | } 94 | } 95 | 96 | async function getToolConfig(name: string): Promise { 97 | const questions = [ 98 | { 99 | type: "text" as const, 100 | name: "description", 101 | message: "Tool description:", 102 | initial: `A utility tool for ${name} functionality`, 103 | }, 104 | ]; 105 | 106 | const answers = await prompts(questions); 107 | 108 | // Handle user cancellation 109 | if (Object.keys(answers).length === 0 || !answers.description) { 110 | console.log("\n❌ Tool creation cancelled"); 111 | process.exit(0); 112 | } 113 | 114 | return { 115 | name, 116 | description: answers.description, 117 | }; 118 | } 119 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: typescript 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - "cli/**" 8 | - "sdk/**" 9 | 10 | jobs: 11 | publish-cli: 12 | runs-on: ubuntu-latest 13 | if: github.ref == 'refs/heads/main' 14 | defaults: 15 | run: 16 | working-directory: ./cli 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install pnpm 24 | run: npm install -g pnpm@8 25 | 26 | - uses: actions/setup-node@v4 27 | with: 28 | node-version: "20.x" 29 | registry-url: "https://registry.npmjs.org" 30 | 31 | - name: Get pnpm store directory 32 | shell: bash 33 | run: | 34 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 35 | 36 | - name: Setup pnpm cache 37 | uses: actions/cache@v4 38 | with: 39 | path: ${{ env.STORE_PATH }} 40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm-store- 43 | 44 | - name: Install dependencies 45 | 46 | run: pnpm install 47 | 48 | - name: Build and Publish SDK 49 | run: | 50 | VERSION=$(jq '.version' package.json) 51 | CURRENT_NPM_VERSION=$(pnpm view @hatchet-dev/typescript-sdk version) 52 | 53 | if [[ "$VERSION" == "$CURRENT_NPM_VERSION" ]]; then 54 | echo "Version has not changed. Skipping publish." 55 | exit 0 56 | fi 57 | 58 | ## If the version contains `alpha`, it's an alpha version 59 | ## and we should tag it as such.= 60 | if [[ "$VERSION" == *alpha* ]]; then 61 | pnpm publish:ci:alpha 62 | else 63 | pnpm publish:ci 64 | fi 65 | env: 66 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 67 | 68 | publish-sdk: 69 | runs-on: ubuntu-latest 70 | if: github.ref == 'refs/heads/main' 71 | defaults: 72 | run: 73 | working-directory: ./sdk 74 | steps: 75 | - name: Checkout 76 | uses: actions/checkout@v4 77 | with: 78 | submodules: recursive 79 | 80 | - name: Install pnpm 81 | run: npm install -g pnpm@8 82 | 83 | - uses: actions/setup-node@v4 84 | with: 85 | node-version: "20.x" 86 | registry-url: "https://registry.npmjs.org" 87 | 88 | - name: Get pnpm store directory 89 | shell: bash 90 | run: | 91 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 92 | 93 | - name: Setup pnpm cache 94 | uses: actions/cache@v4 95 | with: 96 | path: ${{ env.STORE_PATH }} 97 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }} 98 | restore-keys: | 99 | ${{ runner.os }}-pnpm-store- 100 | 101 | - name: Install dependencies 102 | 103 | run: pnpm install 104 | 105 | - name: Build and Publish SDK 106 | run: | 107 | VERSION=$(jq '.version' package.json) 108 | CURRENT_NPM_VERSION=$(pnpm view @hatchet-dev/typescript-sdk version) 109 | 110 | if [[ "$VERSION" == "$CURRENT_NPM_VERSION" ]]; then 111 | echo "Version has not changed. Skipping publish." 112 | exit 0 113 | fi 114 | 115 | ## If the version contains `alpha`, it's an alpha version 116 | ## and we should tag it as such.= 117 | if [[ "$VERSION" == *alpha* ]]; then 118 | pnpm publish:ci:alpha 119 | else 120 | pnpm publish:ci 121 | fi 122 | env: 123 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 124 | -------------------------------------------------------------------------------- /cli/src/utils/template-processor.ts: -------------------------------------------------------------------------------- 1 | import Handlebars from 'handlebars'; 2 | import { TemplateFile } from './template-fetcher'; 3 | 4 | export interface TemplateContext { 5 | [key: string]: any; 6 | } 7 | 8 | export interface ProcessedTemplate { 9 | name: string; 10 | content: string; 11 | originalPath: string; 12 | } 13 | 14 | export class TemplateProcessor { 15 | private handlebars: typeof Handlebars; 16 | 17 | constructor() { 18 | this.handlebars = Handlebars.create(); 19 | this.registerHelpers(); 20 | } 21 | 22 | private registerHelpers() { 23 | // Register common helpers 24 | this.handlebars.registerHelper('uppercase', (str: string) => { 25 | return str ? str.toUpperCase() : ''; 26 | }); 27 | 28 | this.handlebars.registerHelper('lowercase', (str: string) => { 29 | return str ? str.toLowerCase() : ''; 30 | }); 31 | 32 | this.handlebars.registerHelper('camelCase', (str: string) => { 33 | return str ? str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '') : ''; 34 | }); 35 | 36 | this.handlebars.registerHelper('pascalCase', (str: string) => { 37 | if (!str) return ''; 38 | const camelCase = str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : ''); 39 | return camelCase.charAt(0).toUpperCase() + camelCase.slice(1); 40 | }); 41 | 42 | this.handlebars.registerHelper('kebabCase', (str: string) => { 43 | return str ? str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().replace(/[\s_]+/g, '-') : ''; 44 | }); 45 | 46 | this.handlebars.registerHelper('snakeCase', (str: string) => { 47 | return str ? str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase().replace(/[\s-]+/g, '_') : ''; 48 | }); 49 | 50 | // Date helper 51 | this.handlebars.registerHelper('currentDate', () => { 52 | return new Date().toISOString().split('T')[0]; 53 | }); 54 | 55 | // Conditional helpers 56 | this.handlebars.registerHelper('eq', (a: any, b: any) => a === b); 57 | this.handlebars.registerHelper('ne', (a: any, b: any) => a !== b); 58 | this.handlebars.registerHelper('gt', (a: any, b: any) => a > b); 59 | this.handlebars.registerHelper('lt', (a: any, b: any) => a < b); 60 | } 61 | 62 | processTemplates(templates: TemplateFile[], context: TemplateContext): ProcessedTemplate[] { 63 | return templates.map(template => this.processTemplate(template, context)); 64 | } 65 | 66 | processTemplate(template: TemplateFile, context: TemplateContext): ProcessedTemplate { 67 | try { 68 | // Compile the template 69 | const compiledTemplate = this.handlebars.compile(template.content); 70 | 71 | // Process the content with the provided context 72 | const processedContent = compiledTemplate(context); 73 | 74 | // Also process the filename if it contains handlebars syntax 75 | const compiledName = this.handlebars.compile(template.name); 76 | let processedName = compiledName(context); 77 | 78 | // Remove .hbs extension from the final filename 79 | if (processedName.endsWith('.hbs')) { 80 | processedName = processedName.slice(0, -4); 81 | } 82 | 83 | return { 84 | name: processedName, 85 | content: processedContent, 86 | originalPath: template.path, 87 | }; 88 | } catch (error) { 89 | throw new Error(`Failed to process template ${template.name}: ${error instanceof Error ? error.message : 'Unknown error'}`); 90 | } 91 | } 92 | 93 | // Method to add custom helpers 94 | registerHelper(name: string, helper: Handlebars.HelperDelegate) { 95 | this.handlebars.registerHelper(name, helper); 96 | } 97 | 98 | // Method to add custom partials 99 | registerPartial(name: string, partial: string) { 100 | this.handlebars.registerPartial(name, partial); 101 | } 102 | 103 | // Method to compile templates (for processing paths) 104 | compile(template: string) { 105 | return this.handlebars.compile(template); 106 | } 107 | } -------------------------------------------------------------------------------- /cli/src/utils/template-fetcher.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { promises as fs } from 'fs'; 3 | import * as path from 'path'; 4 | 5 | export interface TemplateSource { 6 | type: 'github' | 'local'; 7 | path: string; 8 | ref?: string; // For GitHub: branch, tag, or commit 9 | } 10 | 11 | export interface TemplateFile { 12 | name: string; 13 | content: string; 14 | path: string; 15 | } 16 | 17 | export class TemplateFetcher { 18 | async fetchTemplate(source: TemplateSource): Promise { 19 | switch (source.type) { 20 | case 'github': 21 | return this.fetchFromGitHub(source); 22 | case 'local': 23 | return this.fetchFromLocal(source); 24 | default: 25 | throw new Error(`Unsupported template source type: ${(source as any).type}`); 26 | } 27 | } 28 | 29 | private async fetchFromGitHub(source: TemplateSource): Promise { 30 | const { path: repoPath, ref = 'main' } = source; 31 | const [owner, repo, ...pathParts] = repoPath.split('/'); 32 | const templatePath = pathParts.join('/'); 33 | 34 | if (!owner || !repo) { 35 | throw new Error('GitHub path must be in format: owner/repo/path/to/template'); 36 | } 37 | 38 | try { 39 | // Get directory contents from GitHub API 40 | const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${templatePath}?ref=${ref}`; 41 | const response = await axios.get(apiUrl); 42 | 43 | const files: TemplateFile[] = []; 44 | const items = Array.isArray(response.data) ? response.data : [response.data]; 45 | 46 | for (const item of items) { 47 | if (item.type === 'file') { 48 | // Fetch file content 49 | const fileResponse = await axios.get(item.download_url); 50 | files.push({ 51 | name: item.name, 52 | content: fileResponse.data, 53 | path: item.path, 54 | }); 55 | } 56 | } 57 | 58 | return files; 59 | } catch (error) { 60 | throw new Error(`Failed to fetch template from GitHub: ${error instanceof Error ? error.message : 'Unknown error'}`); 61 | } 62 | } 63 | 64 | private async fetchFromLocal(source: TemplateSource): Promise { 65 | const templatePath = path.resolve(source.path); 66 | 67 | try { 68 | const stat = await fs.stat(templatePath); 69 | const files: TemplateFile[] = []; 70 | 71 | if (stat.isDirectory()) { 72 | await this.readDirectoryRecursively(templatePath, templatePath, files); 73 | } else { 74 | // Single file 75 | const content = await fs.readFile(templatePath, 'utf-8'); 76 | files.push({ 77 | name: path.basename(templatePath), 78 | content, 79 | path: templatePath, 80 | }); 81 | } 82 | 83 | return files; 84 | } catch (error) { 85 | throw new Error(`Failed to fetch template from local path: ${error instanceof Error ? error.message : 'Unknown error'}`); 86 | } 87 | } 88 | 89 | private async readDirectoryRecursively( 90 | currentPath: string, 91 | basePath: string, 92 | files: TemplateFile[] 93 | ): Promise { 94 | const entries = await fs.readdir(currentPath, { withFileTypes: true }); 95 | 96 | for (const entry of entries) { 97 | const fullPath = path.join(currentPath, entry.name); 98 | 99 | if (entry.isFile()) { 100 | const content = await fs.readFile(fullPath, 'utf-8'); 101 | // Calculate relative path from the base template directory 102 | const relativePath = path.relative(basePath, fullPath); 103 | 104 | files.push({ 105 | name: entry.name, 106 | content, 107 | path: relativePath, 108 | }); 109 | } else if (entry.isDirectory()) { 110 | // Recursively process subdirectories 111 | await this.readDirectoryRecursively(fullPath, basePath, files); 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /cli/src/commands/create.ts: -------------------------------------------------------------------------------- 1 | import prompts from "prompts"; 2 | import * as path from "path"; 3 | import { promises as fs } from "fs"; 4 | import { processTemplate, getTemplatePath } from "../utils"; 5 | 6 | interface ProjectConfig { 7 | name: string; 8 | description: string; 9 | author: string; 10 | template: string; 11 | } 12 | 13 | export async function create(projectName?: string) { 14 | try { 15 | console.log("🚀 Creating new project..."); 16 | 17 | const config = await getProjectConfig(projectName); 18 | 19 | const outputDir = path.join(process.cwd(), config.name); 20 | 21 | // Check if directory already exists 22 | try { 23 | await fs.access(outputDir); 24 | const overwrite = await prompts({ 25 | type: "confirm", 26 | name: "overwrite", 27 | message: `Directory '${config.name}' already exists. Overwrite?`, 28 | initial: false, 29 | }); 30 | 31 | if (!overwrite.overwrite) { 32 | console.log("❌ Project creation cancelled"); 33 | return; 34 | } 35 | } catch { 36 | // Directory doesn't exist, continue 37 | } 38 | 39 | // Process templates - resolve template path for both dev and bundled environments 40 | const templatePath = getTemplatePath(config.template, __dirname); 41 | 42 | await processTemplate({ type: "local", path: templatePath }, config, { 43 | outputDir, 44 | force: true, 45 | }); 46 | 47 | console.log(`\n✅ Project '${config.name}' created successfully!`); 48 | console.log(`📁 Project created in: ${outputDir}`); 49 | console.log("\n📝 Next steps:"); 50 | console.log(`1. cd ${config.name}`); 51 | console.log("2. Follow the instructions in the README.md file"); 52 | } catch (error) { 53 | console.error( 54 | "❌ Failed to create project:", 55 | error instanceof Error ? error.message : "Unknown error" 56 | ); 57 | process.exit(1); 58 | } 59 | } 60 | 61 | async function getProjectConfig(initialName?: string): Promise { 62 | const questions = [ 63 | { 64 | type: "text" as const, 65 | name: "name", 66 | message: "Project name:", 67 | initial: initialName || "my-project", 68 | validate: (value: string) => { 69 | if (!value || value.trim().length === 0) { 70 | return "Project name is required"; 71 | } 72 | if (!/^[a-z0-9-]+$/.test(value)) { 73 | return "Project name must contain only lowercase letters, numbers, and hyphens"; 74 | } 75 | return true; 76 | }, 77 | }, 78 | { 79 | type: "text" as const, 80 | name: "description", 81 | message: "Project description:", 82 | initial: (prev: string) => `An example Icepick project named ${prev}`, 83 | }, 84 | { 85 | type: "text" as const, 86 | name: "author", 87 | message: "Author:", 88 | initial: "", 89 | }, 90 | { 91 | type: "select" as const, 92 | name: "template", 93 | message: "Project template:", 94 | choices: [ 95 | { 96 | title: "Blank", 97 | value: "blank", 98 | description: "A blank starter project", 99 | }, 100 | { 101 | title: "Deep Research Agent", 102 | value: "deep-research", 103 | description: 104 | "Advanced research agent with multi-iteration web search, fact extraction, and synthesis", 105 | }, 106 | { 107 | title: "Geo Agent", 108 | value: "geo", 109 | description: "Geo agent has tools for weather, time, and holidays", 110 | }, 111 | ], 112 | initial: 0, 113 | }, 114 | ]; 115 | 116 | const answers = await prompts(questions); 117 | 118 | // Handle user cancellation 119 | if (Object.keys(answers).length === 0 || !answers.name) { 120 | console.log("\n❌ Project creation cancelled"); 121 | process.exit(0); 122 | } 123 | 124 | return { 125 | name: answers.name, 126 | description: answers.description, 127 | author: answers.author, 128 | template: answers.template, 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/4.evaluator-optimizer/evaluator-optimizer.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { evaluatorTool } from "./tools/evaluator.tool"; 4 | import { generatorTool } from "./tools/generator.tool"; 5 | 6 | /** 7 | * EVALUATOR-OPTIMIZER PATTERN 8 | * 9 | * Based on Anthropic's "Building Effective Agents" blog post: 10 | * https://www.anthropic.com/engineering/building-effective-agents 11 | * 12 | * Pattern Description: 13 | * One LLM generates a response while another provides evaluation and feedback 14 | * in a loop, iteratively improving the output. This is analogous to the 15 | * iterative writing process a human writer might go through. 16 | * 17 | * When to use: 18 | * - Clear evaluation criteria exist 19 | * - Iterative refinement provides measurable value 20 | * - LLM can provide useful feedback (similar to human feedback improving results) 21 | * - Quality improvements possible through iteration 22 | * 23 | * Examples: 24 | * - Literary translation with nuance refinement 25 | * - Complex search requiring multiple rounds of analysis 26 | * - Content creation with quality improvement loops 27 | * - Creative writing with iterative polish 28 | * 29 | * Key Insight: 30 | * This pattern works well when LLM responses can be demonstrably improved 31 | * when a human articulates feedback, and when the LLM can provide such feedback itself. 32 | */ 33 | 34 | const EvaluatorOptimizerAgentInput = z.object({ 35 | topic: z.string(), 36 | targetAudience: z.string(), 37 | }); 38 | 39 | const EvaluatorOptimizerAgentOutput = z.object({ 40 | post: z.string(), 41 | iterations: z.number(), 42 | }); 43 | 44 | export const evaluatorOptimizerAgent = icepick.agent({ 45 | name: "evaluator-optimizer-agent", 46 | executionTimeout: "2m", 47 | inputSchema: EvaluatorOptimizerAgentInput, 48 | outputSchema: EvaluatorOptimizerAgentOutput, 49 | description: "Demonstrates evaluator-optimizer: iterative generation and refinement loop", 50 | fn: async (input, ctx) => { 51 | 52 | let post: string | undefined; 53 | let feedback: string | undefined; 54 | let iterations = 0; 55 | 56 | // ITERATIVE IMPROVEMENT LOOP 57 | // The loop continues until either: 58 | // 1. The evaluator determines the output is satisfactory (complete = true) 59 | // 2. We reach the maximum number of iterations (prevents infinite loops) 60 | for (let i = 0; i < 3; i++) { 61 | iterations++; 62 | // GENERATION STEP: Create or improve the content 63 | // The generator takes into account: 64 | // - Original requirements (topic, target audience) 65 | // - Previous attempt (if any) 66 | // - Feedback from evaluator (if any) 67 | const { post: newPost } = await generatorTool.run({ 68 | topic: input.topic, 69 | targetAudience: input.targetAudience, 70 | previousPost: post, 71 | previousFeedback: feedback 72 | }); 73 | post = newPost; 74 | 75 | // EVALUATION STEP: Assess the generated content 76 | // The evaluator provides: 77 | // - A completion flag (is this good enough?) 78 | // - Specific feedback for improvement (if not complete) 79 | const evaluatorResult = await evaluatorTool.run({ 80 | post: post, 81 | topic: input.topic, 82 | targetAudience: input.targetAudience 83 | }); 84 | 85 | feedback = evaluatorResult.feedback; 86 | 87 | // COMPLETION CHECK: If evaluator is satisfied, return the result 88 | if (evaluatorResult.complete) { 89 | return { 90 | post: post, 91 | iterations: iterations, 92 | }; 93 | } 94 | 95 | // If not complete, the loop continues with the feedback for the next iteration 96 | } 97 | 98 | // FALLBACK: If we've reached max iterations without completion 99 | // This prevents infinite loops while still returning the best attempt 100 | if (!post) throw new Error("I was unable to generate a post"); 101 | 102 | return { 103 | post: post, 104 | iterations: iterations, 105 | }; 106 | }, 107 | }); 108 | -------------------------------------------------------------------------------- /cli/templates/geo/src/trigger.ts.hbs: -------------------------------------------------------------------------------- 1 | import * as readline from 'readline'; 2 | import { {{camelCase name}}Agent } from '@/agents'; 3 | 4 | const presetQueries = { 5 | '1': "What's the current weather in New York City?", 6 | '2': "What time is it in Tokyo right now?", 7 | '3': "Are there any holidays today in the United States?", 8 | '4': "Tell me about the weather forecast for London this week", 9 | '5': "What's the time difference between Los Angeles and Sydney?", 10 | '6': "What holidays are coming up this month in Canada?" 11 | }; 12 | 13 | function showMenu() { 14 | console.log('\n🎯 {{name}} Agent CLI'); 15 | console.log('='.repeat(40)); 16 | console.log('\nPreset Queries:'); 17 | 18 | Object.entries(presetQueries).forEach(([key, query]) => { 19 | console.log(` ${key}. ${query}`); 20 | }); 21 | 22 | console.log('\n c. Custom query'); 23 | console.log(' h. Show this menu'); 24 | console.log(' q. Quit'); 25 | console.log('\n' + '='.repeat(40)); 26 | } 27 | 28 | async function runAgent(query: string) { 29 | const startTime = Date.now(); 30 | console.log(`\n🤖 Running query: "${query}"`); 31 | console.log('⏳ Processing...\n'); 32 | 33 | try { 34 | const result = await {{camelCase name}}Agent.run({ 35 | message: query 36 | }); 37 | const duration = Date.now() - startTime; 38 | 39 | console.log(`✅ Completed in ${duration}ms\n`); 40 | console.log('📄 Result:'); 41 | console.log('-'.repeat(50)); 42 | 43 | // Print the main message 44 | console.log(result.message); 45 | 46 | // Print highlights if available 47 | if (result.highlights && result.highlights.length > 0) { 48 | console.log('\n🔍 Key Information:'); 49 | result.highlights.forEach((highlight, index) => { 50 | console.log(` • ${highlight}`); 51 | }); 52 | } 53 | 54 | // Print recommendations if available 55 | if (result.recommendations && result.recommendations.length > 0) { 56 | console.log('\n💡 Recommendations:'); 57 | result.recommendations.forEach((recommendation, index) => { 58 | console.log(` • ${recommendation}`); 59 | }); 60 | } 61 | 62 | console.log('-'.repeat(50)); 63 | } catch (error) { 64 | const duration = Date.now() - startTime; 65 | console.log(`❌ Failed after ${duration}ms`); 66 | console.log(`Error: ${error instanceof Error ? error.message : 'Unknown error'}\n`); 67 | } 68 | } 69 | 70 | function prompt(rl: readline.Interface, message: string): Promise { 71 | return new Promise((resolve) => { 72 | rl.question(message, (answer) => resolve(answer.trim())); 73 | }); 74 | } 75 | 76 | async function main() { 77 | const rl = readline.createInterface({ 78 | input: process.stdin, 79 | output: process.stdout 80 | }); 81 | 82 | console.log('\n🚀 Welcome to the {{name}} Agent CLI!'); 83 | console.log('This tool helps you interact with location-based services including weather, time, and holidays.\n'); 84 | 85 | showMenu(); 86 | 87 | while (true) { 88 | const choice = await prompt(rl, '\n🔍 Select an option (1-6, c, h, q): '); 89 | 90 | switch (choice.toLowerCase()) { 91 | case 'q': 92 | console.log('\n👋 Goodbye!'); 93 | rl.close(); 94 | return; 95 | 96 | case 'h': 97 | showMenu(); 98 | break; 99 | 100 | case 'c': 101 | const customQuery = await prompt(rl, '\n💭 Enter your custom query: '); 102 | if (customQuery) { 103 | await runAgent(customQuery); 104 | } else { 105 | console.log('❌ No query provided.'); 106 | } 107 | break; 108 | 109 | default: 110 | if (presetQueries[choice as keyof typeof presetQueries]) { 111 | await runAgent(presetQueries[choice as keyof typeof presetQueries]); 112 | } else { 113 | console.log('❌ Invalid option. Please try again.'); 114 | } 115 | } 116 | } 117 | } 118 | 119 | // Handle graceful shutdown 120 | process.on('SIGINT', () => { 121 | console.log('\n\n👋 Goodbye!'); 122 | process.exit(0); 123 | }); 124 | 125 | // Run the CLI 126 | if (require.main === module) { 127 | main().catch(console.error); 128 | } -------------------------------------------------------------------------------- /cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from "commander"; 4 | import * as path from "path"; 5 | import * as fs from "fs"; 6 | import * as os from "os"; 7 | import { addAgent } from "./commands/add-agent"; 8 | import { addTool } from "./commands/add-tool"; 9 | import { create } from "./commands/create"; 10 | import { startMcp } from "./commands/mcp"; 11 | import { HATCHET_VERSION } from "./version"; 12 | 13 | const program = new Command(); 14 | 15 | program 16 | .name("icepick") 17 | .description("CLI tool for managing components, agents, and tools") 18 | .version(HATCHET_VERSION) 19 | .option( 20 | "-C, --cwd ", 21 | "Change working directory before running command" 22 | ); 23 | 24 | const addCommand = program.command("add").description("Add various resources"); 25 | 26 | addCommand 27 | .command("agent") 28 | .description("Add a new agent") 29 | .argument("", "Agent name") 30 | .action(addAgent); 31 | 32 | addCommand 33 | .command("tool") 34 | .description("Add a new tool") 35 | .argument("", "Tool name") 36 | .action(addTool); 37 | 38 | program 39 | .command("create") 40 | .description("Create a new project") 41 | .argument("[name]", "Project name") 42 | .action(create); 43 | 44 | program 45 | .command("mcp") 46 | .description("Start the Model Context Protocol server") 47 | .action(startMcp); 48 | 49 | program 50 | .command("version") 51 | .description("Show version information") 52 | .action(() => { 53 | console.log(`Hatchet Icepick v${HATCHET_VERSION}`); 54 | }); 55 | 56 | // Handle working directory change 57 | program.hook("preAction", (thisCommand) => { 58 | const options = thisCommand.opts(); 59 | if (options.cwd) { 60 | const targetDir = path.resolve(options.cwd); 61 | 62 | // Verify the directory exists 63 | if (!fs.existsSync(targetDir)) { 64 | console.error(`Error: Directory '${targetDir}' does not exist`); 65 | process.exit(1); 66 | } 67 | 68 | // Verify it's actually a directory 69 | const stat = fs.statSync(targetDir); 70 | if (!stat.isDirectory()) { 71 | console.error(`Error: '${targetDir}' is not a directory`); 72 | process.exit(1); 73 | } 74 | 75 | // Security check: warn about potentially sensitive directories 76 | const resolvedPath = fs.realpathSync(targetDir); // Resolve symlinks 77 | const sensitivePatterns = [ 78 | /^\/$/, // Root directory 79 | /^\/usr/, // System directories 80 | /^\/etc/, // Configuration directories 81 | /^\/var/, // System variable directories 82 | /^\/bin/, // Binary directories 83 | /^\/sbin/, // System binary directories 84 | /^\/lib/, // Library directories 85 | /^\/opt/, // Optional software directories 86 | /^\/proc/, // Process information 87 | /^\/sys/, // System information 88 | /^\/dev/, // Device files 89 | /^\/tmp/, // Temporary files (could be risky) 90 | ]; 91 | 92 | // Check for sensitive directories on Unix-like systems 93 | if (process.platform !== "win32") { 94 | const homeDir = os.homedir(); 95 | const isSensitive = sensitivePatterns.some((pattern) => 96 | pattern.test(resolvedPath) 97 | ); 98 | const isHomeDirectory = resolvedPath === homeDir; 99 | 100 | if (isSensitive) { 101 | console.warn( 102 | `⚠️ Warning: You are about to run icepick in a system directory: ${resolvedPath}` 103 | ); 104 | console.warn( 105 | ` This could create project files in a system location.` 106 | ); 107 | console.warn( 108 | ` Consider using a dedicated workspace directory instead.` 109 | ); 110 | } else if (isHomeDirectory) { 111 | console.warn( 112 | `⚠️ Warning: You are about to run icepick in your home directory: ${resolvedPath}` 113 | ); 114 | console.warn( 115 | ` This will create project files directly in your home directory.` 116 | ); 117 | console.warn( 118 | ` Consider using a dedicated workspace directory like ~/workspace or ~/projects.` 119 | ); 120 | } 121 | } 122 | 123 | // Change the working directory 124 | process.chdir(targetDir); 125 | } 126 | }); 127 | 128 | program.parse(); 129 | -------------------------------------------------------------------------------- /cli/src/utils/template-engine.ts: -------------------------------------------------------------------------------- 1 | import { TemplateFetcher, TemplateSource } from './template-fetcher'; 2 | import { TemplateProcessor, TemplateContext, ProcessedTemplate } from './template-processor'; 3 | import { promises as fs } from 'fs'; 4 | import * as path from 'path'; 5 | 6 | export interface TemplateEngineOptions { 7 | outputDir?: string; 8 | force?: boolean; // Overwrite existing files 9 | } 10 | 11 | export class TemplateEngine { 12 | private fetcher: TemplateFetcher; 13 | private processor: TemplateProcessor; 14 | 15 | constructor() { 16 | this.fetcher = new TemplateFetcher(); 17 | this.processor = new TemplateProcessor(); 18 | } 19 | 20 | /** 21 | * Process templates from a source and optionally write them to disk 22 | */ 23 | async processTemplates( 24 | source: TemplateSource, 25 | context: TemplateContext, 26 | options: TemplateEngineOptions = {} 27 | ): Promise { 28 | // Fetch templates from source 29 | const templates = await this.fetcher.fetchTemplate(source); 30 | 31 | if (templates.length === 0) { 32 | throw new Error('No templates found at the specified source'); 33 | } 34 | 35 | // Process templates with context 36 | const processedTemplates = this.processor.processTemplates(templates, context); 37 | 38 | // Write to disk if output directory is specified 39 | if (options.outputDir) { 40 | await this.writeTemplates(processedTemplates, context, options.outputDir, options.force); 41 | } 42 | 43 | return processedTemplates; 44 | } 45 | 46 | /** 47 | * Write processed templates to disk 48 | */ 49 | private async writeTemplates( 50 | templates: ProcessedTemplate[], 51 | context: TemplateContext, 52 | outputDir: string, 53 | force: boolean = false 54 | ): Promise { 55 | // Ensure output directory exists 56 | await fs.mkdir(outputDir, { recursive: true }); 57 | 58 | for (const template of templates) { 59 | // Process the entire path with Handlebars to support dynamic directory names 60 | const pathTemplate = this.processor.compile(template.originalPath); 61 | let processedPath = pathTemplate(context); 62 | 63 | // Remove .hbs extension from the processed path 64 | if (processedPath.endsWith('.hbs')) { 65 | processedPath = processedPath.slice(0, -4); 66 | } 67 | 68 | const outputPath = path.join(outputDir, processedPath); 69 | 70 | // Check if file exists and force flag is not set 71 | if (!force) { 72 | try { 73 | await fs.access(outputPath); 74 | console.warn(`File ${outputPath} already exists. Use --force to overwrite.`); 75 | continue; 76 | } catch { 77 | // File doesn't exist, continue with writing 78 | } 79 | } 80 | 81 | // Ensure subdirectories exist 82 | const dir = path.dirname(outputPath); 83 | await fs.mkdir(dir, { recursive: true }); 84 | 85 | // Write file 86 | await fs.writeFile(outputPath, template.content, 'utf-8'); 87 | console.log(`Created: ${outputPath}`); 88 | } 89 | } 90 | 91 | /** 92 | * Get available local templates 93 | */ 94 | async getLocalTemplates(templatesDir: string = 'templates'): Promise { 95 | try { 96 | const entries = await fs.readdir(templatesDir, { withFileTypes: true }); 97 | return entries 98 | .filter(entry => entry.isDirectory()) 99 | .map(entry => entry.name); 100 | } catch { 101 | return []; 102 | } 103 | } 104 | 105 | /** 106 | * Register a custom Handlebars helper 107 | */ 108 | registerHelper(name: string, helper: any) { 109 | this.processor.registerHelper(name, helper); 110 | } 111 | 112 | /** 113 | * Register a custom Handlebars partial 114 | */ 115 | registerPartial(name: string, partial: string) { 116 | this.processor.registerPartial(name, partial); 117 | } 118 | } 119 | 120 | // Convenience function for quick template processing 121 | export async function processTemplate( 122 | source: TemplateSource, 123 | context: TemplateContext, 124 | options: TemplateEngineOptions = {} 125 | ): Promise { 126 | const engine = new TemplateEngine(); 127 | return engine.processTemplates(source, context, options); 128 | } -------------------------------------------------------------------------------- /scaffolds/src/agents/effective-agent-patterns/3.parallelization/2.voting/voting.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { safetyVoterTool } from "./tools/safety-voter.tool"; 4 | import { helpfulnessVoterTool } from "./tools/helpfulness-voter.tool"; 5 | import { accuracyVoterTool } from "./tools/accuracy-voter.tool"; 6 | 7 | /** 8 | * VOTING PARALLELIZATION PATTERN 9 | * 10 | * Based on Anthropic's "Building Effective Agents" blog post: 11 | * https://www.anthropic.com/engineering/building-effective-agents 12 | * 13 | * Pattern Description: 14 | * Running the same or similar tasks multiple times to get diverse outputs, 15 | * then using voting logic to determine the final result. This is the second 16 | * of two parallelization variations (the other being sectioning). 17 | * 18 | * When to use: 19 | * - Need multiple perspectives for higher confidence results 20 | * - Quality assurance through consensus 21 | * - Balancing false positives/negatives with vote thresholds 22 | * 23 | * Examples: 24 | * - Code vulnerability review with multiple specialized prompts 25 | * - Content moderation with different evaluation criteria 26 | * - Quality assessment requiring consensus 27 | * 28 | * Key Insight: 29 | * Multiple attempts or perspectives can significantly improve confidence in results, 30 | * especially for subjective or complex evaluation tasks. 31 | */ 32 | 33 | const VotingAgentInput = z.object({ 34 | message: z.string(), 35 | response: z.string(), 36 | }); 37 | 38 | const VotingAgentOutput = z.object({ 39 | approved: z.boolean(), 40 | finalResponse: z.string(), 41 | votingSummary: z.string(), 42 | }); 43 | 44 | export const votingAgent = icepick.agent({ 45 | name: "voting-agent", 46 | executionTimeout: "1m", 47 | inputSchema: VotingAgentInput, 48 | outputSchema: VotingAgentOutput, 49 | description: "Demonstrates voting: multiple parallel evaluations with consensus decision-making", 50 | fn: async (input, ctx) => { 51 | 52 | // PARALLEL VOTING: Run multiple specialized evaluators simultaneously 53 | // Each voter focuses on a different aspect of quality evaluation: 54 | // - Safety: Checks for harmful or inappropriate content 55 | // - Helpfulness: Evaluates whether the response actually helps the user 56 | // - Accuracy: Assesses factual correctness and reliability 57 | // 58 | // This follows Anthropic's pattern of using multiple specialized evaluators 59 | // rather than trying to do all evaluation in a single call 60 | const [safetyVote, helpfulnessVote, accuracyVote] = await Promise.all([ 61 | safetyVoterTool.run({ 62 | message: input.message, 63 | response: input.response, 64 | }), 65 | helpfulnessVoterTool.run({ 66 | message: input.message, 67 | response: input.response, 68 | }), 69 | accuracyVoterTool.run({ 70 | message: input.message, 71 | response: input.response, 72 | }), 73 | ]); 74 | 75 | // VOTE COUNTING: Aggregate the individual votes 76 | const votes = [safetyVote.approve, helpfulnessVote.approve, accuracyVote.approve]; 77 | const approvalCount = votes.filter(vote => vote).length; 78 | const totalVotes = votes.length; 79 | 80 | // CONSENSUS DECISION: Require majority approval 81 | // This threshold can be adjusted based on your needs: 82 | // - Higher threshold (e.g., unanimous) for more conservative decisions 83 | // - Lower threshold for more permissive decisions 84 | // - Different thresholds for different types of content 85 | const approved = approvalCount >= Math.ceil(totalVotes / 2); 86 | 87 | // TRANSPARENCY: Create detailed voting summary 88 | // This provides transparency into the decision-making process, 89 | // which is crucial for debugging and building trust 90 | const votingSummary = `Voting Results (${approvalCount}/${totalVotes} approved): 91 | - Safety: ${safetyVote.approve ? '✓' : '✗'} - ${safetyVote.reason} 92 | - Helpfulness: ${helpfulnessVote.approve ? '✓' : '✗'} - ${helpfulnessVote.reason} 93 | - Accuracy: ${accuracyVote.approve ? '✓' : '✗'} - ${accuracyVote.reason}`; 94 | 95 | return { 96 | approved, 97 | finalResponse: approved 98 | ? input.response 99 | : "I apologize, but I cannot provide that response as it did not meet our quality and safety standards.", 100 | votingSummary, 101 | }; 102 | }, 103 | }); 104 | -------------------------------------------------------------------------------- /cli/templates/geo/src/tools/summary.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | import { generateText } from "ai"; 4 | import { openai } from "@ai-sdk/openai"; 5 | 6 | export const summary = icepick.tool({ 7 | name: "summary", 8 | description: "Summarize and format geographic information using AI", 9 | inputSchema: z.object({ 10 | data: z.object({ 11 | weather: z.object({ 12 | city: z.string(), 13 | temperature: z.number(), 14 | feelsLike: z.number(), 15 | condition: z.string(), 16 | humidity: z.number(), 17 | windSpeed: z.number(), 18 | summary: z.string() 19 | }).optional(), 20 | time: z.object({ 21 | city: z.string(), 22 | timezone: z.string(), 23 | localTime: z.string(), 24 | utcTime: z.string(), 25 | summary: z.string() 26 | }).optional(), 27 | holiday: z.object({ 28 | country: z.string(), 29 | holidayName: z.string().optional(), 30 | holidayDate: z.string().optional(), 31 | isToday: z.boolean(), 32 | nextHoliday: z.string().optional(), 33 | summary: z.string() 34 | }).optional() 35 | }), 36 | userQuery: z.string() 37 | }), 38 | outputSchema: z.object({ 39 | formattedResponse: z.string(), 40 | highlights: z.array(z.string()), 41 | recommendations: z.array(z.string()).optional() 42 | }), 43 | fn: async (input) => { 44 | const { weather, time, holiday } = input.data; 45 | const { userQuery } = input; 46 | 47 | try { 48 | // Prepare data for the LLM 49 | const dataContext = { 50 | weather: weather || null, 51 | time: time || null, 52 | holiday: holiday || null 53 | }; 54 | 55 | const prompt = `You are a helpful assistant that formats geographic information in a user-friendly way. 56 | 57 | User Query: "${userQuery}" 58 | 59 | Available Data: 60 | ${JSON.stringify(dataContext, null, 2)} 61 | 62 | Please provide a response in the following JSON format: 63 | { 64 | "formattedResponse": "A well-formatted, conversational response with emojis and clear structure", 65 | "highlights": ["key", "pieces", "of", "information"], 66 | "recommendations": ["optional", "actionable", "suggestions"] 67 | } 68 | 69 | Guidelines: 70 | - Use emojis to make it visually appealing (🌤️ for weather, 🕐 for time, 🎉 for holidays) 71 | - Structure the response with clear headings and bullet points 72 | - Extract 2-3 key highlights from the data 73 | - Provide practical recommendations based on the information (weather-based clothing advice, holiday suggestions, etc.) 74 | - Be conversational and helpful 75 | - If some data is missing or contains errors, handle it gracefully 76 | - Keep the response concise but informative`; 77 | 78 | const result = await generateText({ 79 | model: openai('gpt-4o-mini'), 80 | prompt, 81 | temperature: 0.7, 82 | }); 83 | 84 | // Parse the LLM response 85 | try { 86 | const parsedResponse = JSON.parse(result.text); 87 | return { 88 | formattedResponse: parsedResponse.formattedResponse || "Unable to format response", 89 | highlights: Array.isArray(parsedResponse.highlights) ? parsedResponse.highlights : [], 90 | recommendations: Array.isArray(parsedResponse.recommendations) ? parsedResponse.recommendations : undefined 91 | }; 92 | } catch (parseError) { 93 | // Fallback if JSON parsing fails 94 | return { 95 | formattedResponse: result.text, 96 | highlights: [], 97 | recommendations: undefined 98 | }; 99 | } 100 | 101 | } catch (error) { 102 | // Fallback to simple formatting if LLM call fails 103 | const parts: string[] = []; 104 | const highlights: string[] = []; 105 | 106 | if (weather && weather.temperature > 0) { 107 | parts.push(`🌤️ Weather: ${weather.summary}`); 108 | highlights.push(`${weather.temperature}°C in ${weather.city}`); 109 | } 110 | 111 | if (time) { 112 | parts.push(`🕐 Time: ${time.summary}`); 113 | highlights.push(time.localTime); 114 | } 115 | 116 | if (holiday) { 117 | parts.push(`🎉 Holiday: ${holiday.summary}`); 118 | if (holiday.holidayName) { 119 | highlights.push(holiday.holidayName); 120 | } 121 | } 122 | 123 | return { 124 | formattedResponse: parts.length > 0 ? parts.join('\n\n') : "Sorry, I couldn't process the geographic information.", 125 | highlights, 126 | recommendations: undefined 127 | }; 128 | } 129 | } 130 | }); -------------------------------------------------------------------------------- /scaffolds/src/agents/human-in-the-loop/human-optimizer.agent.ts: -------------------------------------------------------------------------------- 1 | import { icepick } from "@/icepick-client"; 2 | import z from "zod"; 3 | import { generatorTool } from "./tools/generator.tool"; 4 | import { sendToSlackTool } from "./tools/send-to-slack.tool"; 5 | 6 | /** 7 | * Human-in-the-loop: Generator with Human Feedback 8 | * 9 | * Extends the Evaluator-Optimizer pattern to include a human in the loop. 10 | * Based on ../effective-agent-patterns/4.evaluator-optimizer but replaces 11 | * LLM evaluation with human evaluation via Slack. 12 | * 13 | * Pattern Description: 14 | * An LLM generates content while a human provides evaluation and feedback 15 | * in a loop, iteratively improving the output. This combines automated 16 | * generation with human judgment and expertise. 17 | * 18 | * When to use: 19 | * - Human judgment/expertise is critical for evaluation 20 | * - Quality standards are subjective or domain-specific 21 | * - Human feedback can provide nuanced improvements 22 | * - Iterative refinement with human oversight adds value 23 | * - Real-time human approval is required 24 | * 25 | * Examples: 26 | * - Content creation requiring brand voice approval 27 | * - Marketing copy needing stakeholder sign-off 28 | * - Creative writing with editorial feedback 29 | * - Technical documentation requiring expert review 30 | * - Social media posts needing compliance approval 31 | * 32 | * Key Insight: 33 | * This pattern works well when human expertise and judgment are irreplaceable 34 | * for evaluation, while still leveraging LLM efficiency for generation and iteration. 35 | */ 36 | 37 | const EvaluatorOptimizerAgentInput = z.object({ 38 | topic: z.string(), 39 | targetAudience: z.string(), 40 | }); 41 | 42 | const EvaluatorOptimizerAgentOutput = z.object({ 43 | post: z.string(), 44 | iterations: z.number(), 45 | }); 46 | 47 | type FeedbackEvent = { 48 | messageId: string; 49 | approved: boolean; 50 | feedback?: string; 51 | } 52 | 53 | export const evaluatorOptimizerAgent = icepick.agent({ 54 | name: "human-optimizer-agent", 55 | executionTimeout: "2m", 56 | inputSchema: EvaluatorOptimizerAgentInput, 57 | outputSchema: EvaluatorOptimizerAgentOutput, 58 | description: "Demonstrates human-in-the-loop: iterative generation with human feedback via Slack", 59 | fn: async (input, ctx) => { 60 | 61 | let post: string | undefined; 62 | let feedback: string | undefined; 63 | let iterations = 0; 64 | 65 | // ITERATIVE IMPROVEMENT LOOP 66 | // The loop continues until either: 67 | // 1. The human approves the output via Slack 68 | // 2. We reach the maximum number of iterations (prevents infinite loops) 69 | for (let i = 0; i < 3; i++) { 70 | iterations++; 71 | // GENERATION STEP: Create or improve the content 72 | // The generator takes into account: 73 | // - Original requirements (topic, target audience) 74 | // - Previous attempt (if any) 75 | // - Feedback from human reviewer (if any) 76 | const { post: newPost } = await generatorTool.run({ 77 | topic: input.topic, 78 | targetAudience: input.targetAudience, 79 | previousPost: post, 80 | previousFeedback: feedback 81 | }); 82 | post = newPost; 83 | 84 | // HUMAN REVIEW STEP: Send content to Slack for human evaluation 85 | // This sends the generated post to Slack where a human can: 86 | // - Approve the content (if satisfactory) 87 | // - Provide specific feedback for improvement (if changes needed) 88 | 89 | const slackMessage = await sendToSlackTool.run({ 90 | post: post, 91 | topic: input.topic, 92 | targetAudience: input.targetAudience 93 | }); 94 | 95 | // dispatch an event on an approve or reject with feedback button 96 | // icepick.events.push("feedback:create", { 97 | // messageId: slackResult.messageId, 98 | // approved: false, 99 | // }) 100 | 101 | // Wait for human feedback via Slack interaction 102 | const feedbackEvent = await ctx.waitFor({ 103 | eventKey: "feedback:create", 104 | expression: `input.messageId == "${slackMessage.messageId}"`, 105 | }) 106 | 107 | const event = feedbackEvent["feedback:create"] as FeedbackEvent 108 | 109 | // COMPLETION CHECK: If human approves, return the result 110 | if (event.approved) { 111 | return { 112 | post: post, 113 | iterations: iterations, 114 | }; 115 | } 116 | 117 | feedback = event.feedback; 118 | 119 | // If not approved, the loop continues with the human feedback for the next iteration 120 | } 121 | 122 | // FALLBACK: If we've reached max iterations without human approval 123 | // This prevents infinite loops while still returning the best attempt 124 | if (!post) throw new Error("I was unable to generate a post"); 125 | 126 | return { 127 | post: post, 128 | iterations: iterations, 129 | }; 130 | }, 131 | }); 132 | -------------------------------------------------------------------------------- /cli/src/utils/template-path.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { promises as fs } from 'fs'; 3 | 4 | /** 5 | * Securely resolves template paths within the CLI package only 6 | * @param templateName - The name of the template (e.g., 'geo', 'agent', 'tool') 7 | * @param callerDir - The __dirname of the calling module 8 | * @returns The resolved template path 9 | * @throws Error if template is not found in any location 10 | */ 11 | export function getTemplatePath(templateName: string, callerDir: string): string { 12 | // Security: Find the CLI package root instead of using relative paths 13 | // This prevents accidentally accessing templates from untrusted packages 14 | const cliPackageRoot = findCliPackageRoot(callerDir); 15 | 16 | if (!cliPackageRoot) { 17 | throw new Error( 18 | `Could not locate @hatchet-dev/icepick-cli package root from ${callerDir}. ` + 19 | `This is required for secure template resolution.` 20 | ); 21 | } 22 | 23 | // Template path is always relative to the CLI package root 24 | const templatePath = path.join(cliPackageRoot, 'templates', templateName); 25 | 26 | // Verify the template exists 27 | try { 28 | require('fs').accessSync(templatePath); 29 | return templatePath; 30 | } catch { 31 | throw new Error( 32 | `Template '${templateName}' not found at ${templatePath}. ` + 33 | `Please ensure the CLI was built correctly or the template exists.` 34 | ); 35 | } 36 | } 37 | 38 | /** 39 | * Finds the CLI package root by looking for package.json with correct name 40 | * @param startDir - Directory to start searching from 41 | * @returns The CLI package root directory or null if not found 42 | */ 43 | function findCliPackageRoot(startDir: string): string | null { 44 | let currentDir = path.resolve(startDir); 45 | const maxAttempts = 10; // Prevent infinite loops 46 | let attempts = 0; 47 | 48 | while (attempts < maxAttempts) { 49 | try { 50 | const packageJsonPath = path.join(currentDir, 'package.json'); 51 | 52 | // Check if package.json exists 53 | require('fs').accessSync(packageJsonPath); 54 | 55 | // Read and verify it's the CLI package 56 | const packageJson = JSON.parse(require('fs').readFileSync(packageJsonPath, 'utf8')); 57 | 58 | if (packageJson.name === '@hatchet-dev/icepick-cli') { 59 | return currentDir; 60 | } 61 | } catch { 62 | // package.json doesn't exist or isn't readable, continue searching 63 | } 64 | 65 | const parentDir = path.dirname(currentDir); 66 | if (parentDir === currentDir) { 67 | // Reached filesystem root 68 | break; 69 | } 70 | 71 | currentDir = parentDir; 72 | attempts++; 73 | } 74 | 75 | return null; 76 | } 77 | 78 | /** 79 | * Async version of getTemplatePath for better error handling 80 | * @param templateName - The name of the template (e.g., 'geo', 'agent', 'tool') 81 | * @param callerDir - The __dirname of the calling module 82 | * @returns Promise that resolves to the template path 83 | * @throws Error if template is not found in any location 84 | */ 85 | export async function getTemplatePathAsync(templateName: string, callerDir: string): Promise { 86 | // Security: Find the CLI package root instead of using relative paths 87 | // This prevents accidentally accessing templates from untrusted packages 88 | const cliPackageRoot = await findCliPackageRootAsync(callerDir); 89 | 90 | if (!cliPackageRoot) { 91 | throw new Error( 92 | `Could not locate @hatchet-dev/icepick-cli package root from ${callerDir}. ` + 93 | `This is required for secure template resolution.` 94 | ); 95 | } 96 | 97 | // Template path is always relative to the CLI package root 98 | const templatePath = path.join(cliPackageRoot, 'templates', templateName); 99 | 100 | // Verify the template exists 101 | try { 102 | await fs.access(templatePath); 103 | return templatePath; 104 | } catch { 105 | throw new Error( 106 | `Template '${templateName}' not found at ${templatePath}. ` + 107 | `Please ensure the CLI was built correctly or the template exists.` 108 | ); 109 | } 110 | } 111 | 112 | /** 113 | * Async version of findCliPackageRoot 114 | * @param startDir - Directory to start searching from 115 | * @returns Promise that resolves to the CLI package root directory or null if not found 116 | */ 117 | async function findCliPackageRootAsync(startDir: string): Promise { 118 | let currentDir = path.resolve(startDir); 119 | const maxAttempts = 10; // Prevent infinite loops 120 | let attempts = 0; 121 | 122 | while (attempts < maxAttempts) { 123 | try { 124 | const packageJsonPath = path.join(currentDir, 'package.json'); 125 | 126 | // Check if package.json exists 127 | await fs.access(packageJsonPath); 128 | 129 | // Read and verify it's the CLI package 130 | const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); 131 | const packageJson = JSON.parse(packageJsonContent); 132 | 133 | if (packageJson.name === '@hatchet-dev/icepick-cli') { 134 | return currentDir; 135 | } 136 | } catch { 137 | // package.json doesn't exist or isn't readable, continue searching 138 | } 139 | 140 | const parentDir = path.dirname(currentDir); 141 | if (parentDir === currentDir) { 142 | // Reached filesystem root 143 | break; 144 | } 145 | 146 | currentDir = parentDir; 147 | attempts++; 148 | } 149 | 150 | return null; 151 | } -------------------------------------------------------------------------------- /cli/src/commands/__tests__/add-tool.test.ts: -------------------------------------------------------------------------------- 1 | import { createTool } from "../add-tool"; 2 | import { promises as fs } from "fs"; 3 | import { 4 | processTemplate, 5 | getTemplatePath, 6 | updateBarrelFile, 7 | } from "../../utils"; 8 | 9 | // Mock dependencies 10 | jest.mock("fs", () => ({ 11 | promises: { 12 | access: jest.fn(), 13 | mkdir: jest.fn(), 14 | }, 15 | })); 16 | 17 | jest.mock("../../utils", () => ({ 18 | processTemplate: jest.fn(), 19 | getTemplatePath: jest.fn(), 20 | updateBarrelFile: jest.fn(), 21 | })); 22 | 23 | // Mock console methods 24 | const mockConsoleLog = jest.spyOn(console, "log").mockImplementation(); 25 | const mockConsoleError = jest.spyOn(console, "error").mockImplementation(); 26 | 27 | const mockedFs = fs as jest.Mocked; 28 | const mockedProcessTemplate = processTemplate as jest.MockedFunction< 29 | typeof processTemplate 30 | >; 31 | const mockedGetTemplatePath = getTemplatePath as jest.MockedFunction< 32 | typeof getTemplatePath 33 | >; 34 | const mockedUpdateBarrelFile = updateBarrelFile as jest.MockedFunction< 35 | typeof updateBarrelFile 36 | >; 37 | 38 | describe("add-tool command", () => { 39 | beforeEach(() => { 40 | jest.clearAllMocks(); 41 | mockConsoleLog.mockClear(); 42 | mockConsoleError.mockClear(); 43 | }); 44 | 45 | afterAll(() => { 46 | mockConsoleLog.mockRestore(); 47 | mockConsoleError.mockRestore(); 48 | }); 49 | 50 | describe("createTool", () => { 51 | it("should create tool with correct next steps", async () => { 52 | // Mock that directory doesn't exist (fs.access throws) 53 | mockedFs.access.mockRejectedValueOnce( 54 | new Error("Directory does not exist") 55 | ); 56 | mockedFs.mkdir.mockResolvedValue(undefined); 57 | mockedProcessTemplate.mockResolvedValueOnce([]); 58 | mockedGetTemplatePath.mockReturnValueOnce("/mock/templates/tool"); 59 | mockedUpdateBarrelFile.mockResolvedValueOnce(undefined); 60 | 61 | const result = await createTool("my-tool", { 62 | description: "A sample tool for testing", 63 | silent: false, 64 | }); 65 | 66 | expect(result.success).toBe(true); 67 | expect(result.message).toBe("Tool 'my-tool' created successfully"); 68 | 69 | // Verify the next steps provide proper guidance 70 | expect(mockConsoleLog).toHaveBeenCalledWith( 71 | "1. Define your input and output schemas in the tool file" 72 | ); 73 | expect(mockConsoleLog).toHaveBeenCalledWith( 74 | "2. Implement the tool logic in the fn function" 75 | ); 76 | expect(mockConsoleLog).toHaveBeenCalledWith( 77 | "3. Import and add the tool to your agent's toolbox" 78 | ); 79 | 80 | // Verify it shows the file creation location 81 | expect(mockConsoleLog).toHaveBeenCalledWith( 82 | expect.stringContaining("📁 File created:") 83 | ); 84 | expect(mockConsoleLog).toHaveBeenCalledWith( 85 | expect.stringContaining("src/tools") 86 | ); 87 | expect(mockConsoleLog).toHaveBeenCalledWith( 88 | expect.stringContaining("my-tool.tool.ts") 89 | ); 90 | }); 91 | 92 | it("should work in silent mode", async () => { 93 | mockedFs.access.mockRejectedValueOnce( 94 | new Error("Directory does not exist") 95 | ); 96 | mockedFs.mkdir.mockResolvedValue(undefined); 97 | mockedProcessTemplate.mockResolvedValueOnce([]); 98 | mockedGetTemplatePath.mockReturnValueOnce("/mock/templates/tool"); 99 | mockedUpdateBarrelFile.mockResolvedValueOnce(undefined); 100 | 101 | const result = await createTool("my-tool", { 102 | description: "A sample tool for testing", 103 | silent: true, 104 | }); 105 | 106 | expect(result.success).toBe(true); 107 | expect(mockConsoleLog).not.toHaveBeenCalled(); 108 | }); 109 | 110 | it("should use default category when not provided", async () => { 111 | mockedFs.access.mockRejectedValueOnce( 112 | new Error("Directory does not exist") 113 | ); 114 | mockedFs.mkdir.mockResolvedValue(undefined); 115 | mockedProcessTemplate.mockResolvedValueOnce([]); 116 | mockedGetTemplatePath.mockReturnValueOnce("/mock/templates/tool"); 117 | mockedUpdateBarrelFile.mockResolvedValueOnce(undefined); 118 | 119 | const result = await createTool("my-tool", { 120 | description: "A sample tool", 121 | silent: true, 122 | }); 123 | }); 124 | 125 | it("should call processTemplate with correct parameters", async () => { 126 | mockedFs.access.mockRejectedValueOnce( 127 | new Error("Directory does not exist") 128 | ); 129 | mockedFs.mkdir.mockResolvedValue(undefined); 130 | mockedProcessTemplate.mockResolvedValueOnce([]); 131 | mockedGetTemplatePath.mockReturnValueOnce("/mock/templates/tool"); 132 | mockedUpdateBarrelFile.mockResolvedValueOnce(undefined); 133 | 134 | await createTool("my-tool", { 135 | description: "A sample tool", 136 | silent: true, 137 | }); 138 | 139 | expect(mockedProcessTemplate).toHaveBeenCalledWith( 140 | expect.objectContaining({ 141 | type: "local", 142 | path: "/mock/templates/tool", 143 | }), 144 | expect.objectContaining({ 145 | name: "my-tool", 146 | description: "A sample tool", 147 | }), 148 | expect.objectContaining({ 149 | outputDir: expect.stringMatching(/src\/tools$/), 150 | force: false, 151 | }) 152 | ); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /cli/templates/geo/src/tools/time.tool.ts.hbs: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { icepick } from "@/icepick-client"; 3 | 4 | export const time = icepick.tool({ 5 | name: "time", 6 | description: "Get the current time in a given city or location", 7 | inputSchema: z.object({ 8 | city: z.string().describe("The city or location to get the time for") 9 | }), 10 | outputSchema: z.object({ 11 | city: z.string(), 12 | timezone: z.string(), 13 | localTime: z.string(), 14 | utcTime: z.string(), 15 | summary: z.string() 16 | }), 17 | fn: async (input) => { 18 | // Helper function to format time consistently 19 | const formatTime = (date: Date, timezone: string) => { 20 | return date.toLocaleString('en-US', { 21 | weekday: 'short', 22 | year: 'numeric', 23 | month: 'short', 24 | day: 'numeric', 25 | hour: '2-digit', 26 | minute: '2-digit', 27 | second: '2-digit', 28 | timeZoneName: 'short', 29 | timeZone: timezone 30 | }); 31 | }; 32 | 33 | const utcNow = new Date(); 34 | 35 | try { 36 | // Step 1: Get coordinates and timezone using OpenMeteo Geocoding API 37 | console.info(`Looking up coordinates and timezone for: ${input.city}`); 38 | const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(input.city)}&count=1&language=en&format=json`; 39 | 40 | const geocodingResponse = await fetch(geocodingUrl); 41 | 42 | if (!geocodingResponse.ok) { 43 | throw new Error(`Geocoding API returned ${geocodingResponse.status}`); 44 | } 45 | 46 | const geocodingData = await geocodingResponse.json(); 47 | 48 | if (!geocodingData.results || geocodingData.results.length === 0) { 49 | throw new Error(`Location "${input.city}" not found`); 50 | } 51 | 52 | const location = geocodingData.results[0]; 53 | const { latitude, longitude, name, timezone, country } = location; 54 | 55 | console.info(`Found: ${name}, ${country} (${latitude}, ${longitude}) - Timezone: ${timezone}`); 56 | 57 | // Step 2: Get current time for the timezone using WorldTimeAPI 58 | try { 59 | const timeUrl = `https://worldtimeapi.org/api/timezone/${timezone}`; 60 | const timeResponse = await fetch(timeUrl); 61 | 62 | if (timeResponse.ok) { 63 | const timeData = await timeResponse.json(); 64 | const localTime = new Date(timeData.datetime); 65 | const formattedTime = formatTime(localTime, timezone); 66 | 67 | return { 68 | city: `${name}, ${country}`, 69 | timezone: timezone, 70 | localTime: formattedTime, 71 | utcTime: utcNow.toISOString(), 72 | summary: `${formattedTime} in ${name}, ${country}` 73 | }; 74 | } else { 75 | console.warn(`WorldTimeAPI returned ${timeResponse.status} for ${timezone}`); 76 | } 77 | } catch (timeApiError) { 78 | console.warn(`WorldTimeAPI failed for ${timezone}:`, timeApiError); 79 | } 80 | 81 | // Step 3: Fallback to JavaScript built-in timezone calculation 82 | try { 83 | console.info(`Using JavaScript built-in timezone for ${name} (${timezone})`); 84 | const localTime = new Date(); 85 | const formattedTime = formatTime(localTime, timezone); 86 | 87 | return { 88 | city: `${name}, ${country}`, 89 | timezone: timezone, 90 | localTime: formattedTime, 91 | utcTime: utcNow.toISOString(), 92 | summary: `${formattedTime} in ${name}, ${country} (estimated)` 93 | }; 94 | } catch (timezoneError) { 95 | console.warn(`Built-in timezone calculation failed for ${timezone}:`, timezoneError); 96 | 97 | // Step 4: Calculate timezone offset manually using coordinates 98 | try { 99 | // Rough timezone offset calculation based on longitude 100 | // This is approximate: each 15 degrees of longitude ≈ 1 hour 101 | const approximateOffset = Math.round(longitude / 15); 102 | const offsetMinutes = approximateOffset * 60; 103 | 104 | const localTime = new Date(utcNow.getTime() + (offsetMinutes * 60 * 1000)); 105 | const formattedTime = localTime.toLocaleString('en-US', { 106 | weekday: 'short', 107 | year: 'numeric', 108 | month: 'short', 109 | day: 'numeric', 110 | hour: '2-digit', 111 | minute: '2-digit', 112 | second: '2-digit' 113 | }); 114 | 115 | console.info(`Using coordinate-based time calculation for ${name} (offset: UTC${approximateOffset >= 0 ? '+' : ''}${approximateOffset})`); 116 | 117 | return { 118 | city: `${name}, ${country}`, 119 | timezone: `UTC${approximateOffset >= 0 ? '+' : ''}${approximateOffset}`, 120 | localTime: `${formattedTime} (approx)`, 121 | utcTime: utcNow.toISOString(), 122 | summary: `${formattedTime} in ${name}, ${country} (approximate)` 123 | }; 124 | } catch (coordError) { 125 | console.warn(`Coordinate-based calculation failed:`, coordError); 126 | } 127 | } 128 | 129 | } catch (geoError) { 130 | console.error(`Failed to get location data for "${input.city}":`, geoError); 131 | 132 | // If geocoding fails completely, throw a helpful error 133 | throw new Error(`Unable to find time information for "${input.city}". Please check the spelling or try a different city name.`); 134 | } 135 | 136 | // Final fallback: UTC time (should rarely be reached) 137 | console.warn(`All time calculation methods failed for ${input.city}, returning UTC time`); 138 | const utcFormatted = formatTime(utcNow, 'UTC'); 139 | 140 | return { 141 | city: input.city, 142 | timezone: 'UTC', 143 | localTime: utcFormatted, 144 | utcTime: utcNow.toISOString(), 145 | summary: `${utcFormatted} (UTC) - Local time calculation failed for ${input.city}` 146 | }; 147 | } 148 | }); -------------------------------------------------------------------------------- /cli/templates/deep-research/src/agents/{{kebabCase name}}.agent.ts.hbs: -------------------------------------------------------------------------------- 1 | import { 2 | search, 3 | planSearch, 4 | PlanSearchOutput, 5 | websiteToMd, 6 | summarize, 7 | judgeResults, 8 | extractFacts, 9 | judgeFacts, 10 | JudgeFactsOutput 11 | } from "@/tools"; 12 | import { z } from "zod"; 13 | import { icepick } from "@/icepick-client"; 14 | 15 | const MessageSchema = z.object({ 16 | message: z.string(), 17 | }); 18 | 19 | const SourceSchema = z.object({ 20 | url: z.string(), 21 | title: z.string().optional(), 22 | index: z.number(), 23 | }); 24 | 25 | const ResponseSchema = z.object({ 26 | result: z.object({ 27 | isComplete: z.boolean(), 28 | reason: z.string(), 29 | sources: z.array(SourceSchema), 30 | summary: z.string().optional(), 31 | facts: z.array(z.object({ 32 | text: z.string(), 33 | sourceIndex: z.number(), 34 | })).optional(), 35 | iterations: z.number().optional(), 36 | factsJudgment: z.object({ 37 | reason: z.string(), 38 | hasEnoughFacts: z.boolean(), 39 | missingAspects: z.array(z.string()), 40 | }).optional(), 41 | searchPlans: z.string().optional(), 42 | }), 43 | }); 44 | 45 | type Source = z.infer; 46 | type Fact = { 47 | text: string; 48 | sourceIndex: number; 49 | }; 50 | 51 | export const {{camelCase name}}Agent = icepick.agent({ 52 | name: "{{kebabCase name}}-agent", 53 | description: "A tool that performs deep research on a given query", 54 | inputSchema: MessageSchema, 55 | outputSchema: ResponseSchema, 56 | executionTimeout: "15m", 57 | fn: async (input, ctx) => { 58 | ctx.logger.info(`Starting deep research agent with query: ${input.message}`); 59 | 60 | let iteration = 0; 61 | const maxIterations = 3; 62 | const allFacts: Fact[] = []; 63 | const allSources: Source[] = []; 64 | let missingAspects: string[] = []; 65 | let plan: PlanSearchOutput | undefined = undefined; 66 | let factsJudgment: JudgeFactsOutput | undefined = undefined; 67 | 68 | while (!ctx.cancelled && iteration < maxIterations) { 69 | iteration++; 70 | ctx.logger.info(`Starting iteration ${iteration}/${maxIterations}`); 71 | 72 | // Plan the search based on the query, existing facts, and missing aspects 73 | ctx.logger.info( 74 | `Planning search with ${allFacts.length} existing facts and ${missingAspects.length} missing aspects` 75 | ); 76 | 77 | plan = await planSearch.run({ 78 | query: input.message, 79 | existingFacts: allFacts.map((f) => f.text), 80 | missingAspects: missingAspects, 81 | }); 82 | 83 | ctx.logger.info( 84 | `Search plan for iteration ${iteration}: ${plan.reasoning}. Queries:` 85 | ); 86 | 87 | for (const query of plan.queries) { 88 | ctx.logger.info(`${query}`); 89 | } 90 | 91 | ctx.logger.info(`Executing ${plan.queries.length} search queries`); 92 | const results = await search.run( 93 | plan.queries.map((query: string) => ({ query })) 94 | ); 95 | 96 | // Flatten and deduplicate sources 97 | const newSources = results.flatMap((result) => result.sources); 98 | const uniqueSources = new Map( 99 | newSources.map((source, index) => [source.url, { ...source, index }]) 100 | ); 101 | 102 | ctx.logger.info( 103 | `Found ${newSources.length} new sources, ${uniqueSources.size} unique sources` 104 | ); 105 | 106 | // Add new sources to all sources 107 | allSources.push(...Array.from(uniqueSources.values())); 108 | 109 | // Convert sources to markdown 110 | ctx.logger.info(`Converting ${uniqueSources.size} sources to markdown`); 111 | const mdResults = await websiteToMd.run( 112 | Array.from(uniqueSources.values()) 113 | .sort((a, b) => a.index - b.index) 114 | .map((source) => ({ 115 | url: source.url, 116 | index: source.index, 117 | title: source.title || "", 118 | })) 119 | ); 120 | 121 | // Extract facts from each source 122 | ctx.logger.info("Extracting facts from markdown content"); 123 | const factsResults = await extractFacts.run( 124 | mdResults.map((result) => ({ 125 | source: result.markdown, 126 | query: input.message, 127 | sourceInfo: { 128 | url: result.url, 129 | title: result.title, 130 | index: result.index, 131 | }, 132 | })) 133 | ); 134 | 135 | // Add new facts to all facts 136 | const newFacts = factsResults.flatMap((result) => result.facts); 137 | allFacts.push(...newFacts); 138 | ctx.logger.info( 139 | `Extracted ${newFacts.length} new facts, total facts: ${allFacts.length}` 140 | ); 141 | 142 | // Judge if we have enough facts 143 | ctx.logger.info("Judging if we have enough facts"); 144 | factsJudgment = await judgeFacts.run({ 145 | query: input.message, 146 | facts: allFacts.map((f) => f.text), 147 | }); 148 | 149 | // Update missing aspects for next iteration 150 | missingAspects = factsJudgment.missingAspects; 151 | ctx.logger.info(`Missing aspects: ${missingAspects.join(", ")}`); 152 | 153 | // If we have enough facts or reached max iterations, generate final summary 154 | if (factsJudgment.hasEnoughFacts || iteration >= maxIterations) { 155 | ctx.logger.info( 156 | `Generating final summary (hasEnoughFacts: ${ 157 | factsJudgment.hasEnoughFacts 158 | }, reachedMaxIterations: ${iteration >= maxIterations})` 159 | ); 160 | break; 161 | } 162 | } 163 | 164 | // Always summarize and judge results after the loop 165 | const summarizeResult = await summarize.run({ 166 | text: input.message, 167 | facts: allFacts, 168 | sources: allSources, 169 | }); 170 | 171 | ctx.logger.info("Judging final results"); 172 | const judgeResult = await judgeResults.run({ 173 | query: input.message, 174 | result: summarizeResult.summary, 175 | }); 176 | 177 | ctx.logger.info( 178 | `Deep research complete (isComplete: ${judgeResult.isComplete}, totalFacts: ${allFacts.length}, totalSources: ${allSources.length}, iterations: ${iteration})` 179 | ); 180 | 181 | return { 182 | result: { 183 | isComplete: judgeResult.isComplete, 184 | reason: judgeResult.reason, 185 | sources: allSources, 186 | summary: summarizeResult.summary, 187 | facts: allFacts, 188 | iterations: iteration, 189 | factsJudgment: factsJudgment, 190 | searchPlans: plan?.reasoning, 191 | }, 192 | }; 193 | }, 194 | }); -------------------------------------------------------------------------------- /cli/src/mcp/server.ts: -------------------------------------------------------------------------------- 1 | const { Server } = require("@modelcontextprotocol/sdk/server/index.js"); 2 | const { 3 | StdioServerTransport, 4 | } = require("@modelcontextprotocol/sdk/server/stdio.js"); 5 | const { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | ErrorCode, 9 | McpError, 10 | } = require("@modelcontextprotocol/sdk/types.js"); 11 | const { z } = require("zod"); 12 | const { promises: fs } = require("fs"); 13 | const path = require("path"); 14 | 15 | // Schema for tool arguments 16 | const CreateAgentArgsSchema = z.object({ 17 | name: z.string().describe("Name of the agent to create"), 18 | description: z.string().describe("Description of what the agent does"), 19 | }); 20 | 21 | const CreateToolArgsSchema = z.object({ 22 | name: z.string().describe("Name of the tool to create"), 23 | }); 24 | 25 | export class IcepickMcpServer { 26 | private server: any; 27 | 28 | constructor() { 29 | this.server = new Server( 30 | { 31 | name: "icepick-mcp-server", 32 | version: "1.0.0", 33 | }, 34 | { 35 | capabilities: { 36 | tools: {}, 37 | }, 38 | } 39 | ); 40 | 41 | this.setupToolHandlers(); 42 | this.setupRequestHandlers(); 43 | } 44 | 45 | private setupToolHandlers() { 46 | // Register tools with their descriptions 47 | this.server.setRequestHandler(ListToolsRequestSchema, async () => { 48 | return { 49 | tools: [ 50 | { 51 | name: "scaffold_agent", 52 | description: 53 | "Creates the boilerplate for a new Icepick agent with a simple interface that has a name, description, and execute method. Before using this tool, please refer to the planning instructions at: https://icepick.hatchet.run/mcp/mcp-instructions.md. If a user is asking to create an agent, you should first read this document, then scaffold tools with the `scaffold_tool` tool, and finally use this tool to create the agent.", 54 | inputSchema: { 55 | type: "object", 56 | properties: { 57 | name: { 58 | type: "string", 59 | description: "Name of the agent to create", 60 | }, 61 | description: { 62 | type: "string", 63 | description: "Description of what the agent does", 64 | }, 65 | }, 66 | required: ["name", "description"], 67 | }, 68 | }, 69 | { 70 | name: "scaffold_tool", 71 | description: 72 | "Creates the boilerplate for a new Icepick tool with a simple interface that has a name and execute method. Before using this tool, please refer to the planning instructions at: https://icepick.hatchet.run/mcp/mcp-instructions.md. If a user is asking to create a tool, you should first read this document, then use this tool to scaffold the tool.", 73 | inputSchema: { 74 | type: "object", 75 | properties: { 76 | name: { 77 | type: "string", 78 | description: "Name of the tool to create", 79 | }, 80 | }, 81 | required: ["name"], 82 | }, 83 | }, 84 | { 85 | name: "plan_agent", 86 | description: 87 | "Provides planning instructions for creating a new agent", 88 | inputSchema: { 89 | type: "object", 90 | properties: {}, 91 | }, 92 | }, 93 | ], 94 | }; 95 | }); 96 | } 97 | 98 | private setupRequestHandlers() { 99 | this.server.setRequestHandler( 100 | CallToolRequestSchema, 101 | async (request: any) => { 102 | try { 103 | const { name, arguments: args } = request.params; 104 | 105 | switch (name) { 106 | case "scaffold_agent": 107 | return await this.handleCreateAgent(args); 108 | case "scaffold_tool": 109 | return await this.handleCreateTool(args); 110 | case "plan_agent": 111 | return await this.handlePlanAgent(); 112 | default: 113 | throw new McpError( 114 | ErrorCode.MethodNotFound, 115 | `Unknown tool: ${name}` 116 | ); 117 | } 118 | } catch (error) { 119 | if (error instanceof McpError) { 120 | throw error; 121 | } 122 | throw new McpError( 123 | ErrorCode.InternalError, 124 | `Tool execution failed: ${ 125 | error instanceof Error ? error.message : "Unknown error" 126 | }` 127 | ); 128 | } 129 | } 130 | ); 131 | } 132 | 133 | private async handleCreateAgent(args: any) { 134 | const { name, description } = CreateAgentArgsSchema.parse(args); 135 | 136 | try { 137 | // Use the existing createAgent function 138 | const { createAgent } = require("../commands/add-agent"); 139 | 140 | const result = await createAgent(name, { 141 | description, 142 | silent: true, 143 | }); 144 | 145 | return { 146 | content: [ 147 | { 148 | type: "text", 149 | text: `${result.message}\nFiles created in: ${result.outputDir}`, 150 | }, 151 | ], 152 | }; 153 | } catch (error) { 154 | throw new McpError( 155 | ErrorCode.InternalError, 156 | `Failed to create agent: ${ 157 | error instanceof Error ? error.message : "Unknown error" 158 | }` 159 | ); 160 | } 161 | } 162 | 163 | private async handleCreateTool(args: any) { 164 | const { name } = CreateToolArgsSchema.parse(args); 165 | 166 | try { 167 | // Use the existing addTool command 168 | const { addTool } = require("../commands/add-tool"); 169 | 170 | // Call the command 171 | await addTool(name); 172 | 173 | return { 174 | content: [ 175 | { 176 | type: "text", 177 | text: `Tool '${name}' created successfully.`, 178 | }, 179 | ], 180 | }; 181 | } catch (error) { 182 | throw new McpError( 183 | ErrorCode.InternalError, 184 | `Failed to create tool: ${ 185 | error instanceof Error ? error.message : "Unknown error" 186 | }` 187 | ); 188 | } 189 | } 190 | 191 | private async handlePlanAgent() { 192 | return { 193 | content: [ 194 | { 195 | type: "text", 196 | text: "Please refer to the latest agent planning instructions at: https://icepick.hatchet.run/mcp/mcp-instructions.md", 197 | }, 198 | ], 199 | }; 200 | } 201 | 202 | async run() { 203 | const transport = new StdioServerTransport(); 204 | await this.server.connect(transport); 205 | console.error("Icepick MCP server running on stdio"); 206 | } 207 | } 208 | 209 | // Export for use in CLI command 210 | -------------------------------------------------------------------------------- /scaffolds/src/agents/deep-research/deep-research.agent.ts: -------------------------------------------------------------------------------- 1 | import { search } from "@/agents/deep-research/tools/search.tool"; 2 | import { planSearch, PlanSearchOutput } from "@/agents/deep-research/tools/plan-search.tool"; 3 | import { websiteToMd } from "@/agents/deep-research/tools/website-to-md.tool"; 4 | import { summarize } from "@/agents/deep-research/tools/summarize.tool"; 5 | import { judgeResults } from "@/agents/deep-research/tools/judge-results.tool"; 6 | import { extractFacts } from "@/agents/deep-research/tools/extract-facts.tool"; 7 | import { judgeFacts, JudgeFactsOutput } from "@/agents/deep-research/tools/judge-facts.tool"; 8 | import { z } from "zod"; 9 | import { icepick } from "@/icepick-client"; 10 | 11 | const MessageSchema = z.object({ 12 | message: z.string(), 13 | }); 14 | 15 | const SourceSchema = z.object({ 16 | url: z.string(), 17 | title: z.string().optional(), 18 | index: z.number(), 19 | }); 20 | 21 | const ResponseSchema = z.object({ 22 | result: z.object({ 23 | isComplete: z.boolean(), 24 | reason: z.string(), 25 | sources: z.array(SourceSchema), 26 | summary: z.string().optional(), 27 | facts: z.array(z.object({ 28 | text: z.string(), 29 | sourceIndex: z.number(), 30 | })).optional(), 31 | iterations: z.number().optional(), 32 | factsJudgment: z.object({ 33 | reason: z.string(), 34 | hasEnoughFacts: z.boolean(), 35 | missingAspects: z.array(z.string()), 36 | }).optional(), 37 | searchPlans: z.string().optional(), 38 | }), 39 | }); 40 | 41 | 42 | type Source = z.infer; 43 | type Fact = { 44 | text: string; 45 | sourceIndex: number; 46 | }; 47 | 48 | export const deepResearchAgent = icepick.agent({ 49 | name: "deep-research-agent", 50 | description: "A tool that performs deep research on a given query", 51 | inputSchema: MessageSchema, 52 | outputSchema: ResponseSchema, 53 | executionTimeout: "15m", 54 | fn: async (input, ctx) => { 55 | ctx.logger.info(`Starting deep research agent with query: ${input.message}`); 56 | 57 | let iteration = 0; 58 | const maxIterations = 3; 59 | const allFacts: Fact[] = []; 60 | const allSources: Source[] = []; 61 | let missingAspects: string[] = []; 62 | let plan: PlanSearchOutput | undefined = undefined; 63 | let factsJudgment: JudgeFactsOutput | undefined = undefined; 64 | 65 | while (!ctx.cancelled && iteration < maxIterations) { 66 | iteration++; 67 | ctx.logger.info(`Starting iteration ${iteration}/${maxIterations}`); 68 | 69 | // Plan the search based on the query, existing facts, and missing aspects 70 | ctx.logger.info( 71 | `Planning search with ${allFacts.length} existing facts and ${missingAspects.length} missing aspects` 72 | ); 73 | 74 | plan = await planSearch.run({ 75 | query: input.message, 76 | existingFacts: allFacts.map((f) => f.text), 77 | missingAspects: missingAspects, 78 | }); 79 | 80 | ctx.logger.info( 81 | `Search plan for iteration ${iteration}: ${plan.reasoning}. Queries:` 82 | ); 83 | 84 | for (const query of plan.queries) { 85 | ctx.logger.info(`${query}`); 86 | } 87 | 88 | ctx.logger.info(`Executing ${plan.queries.length} search queries`); 89 | const results = await search.run ( 90 | plan.queries.map((query: string) => ({ query })) 91 | ); 92 | 93 | // Flatten and deduplicate sources 94 | const newSources = results.flatMap((result) => result.sources); 95 | const uniqueSources = new Map( 96 | newSources.map((source, index) => [source.url, { ...source, index }]) 97 | ); 98 | 99 | ctx.logger.info( 100 | `Found ${newSources.length} new sources, ${uniqueSources.size} unique sources` 101 | ); 102 | 103 | // Add new sources to all sources 104 | allSources.push(...Array.from(uniqueSources.values())); 105 | 106 | // Convert sources to markdown 107 | ctx.logger.info(`Converting ${uniqueSources.size} sources to markdown`); 108 | const mdResults = await websiteToMd.run( 109 | Array.from(uniqueSources.values()) 110 | .sort((a, b) => a.index - b.index) 111 | .map((source) => ({ 112 | url: source.url, 113 | index: source.index, 114 | title: source.title || "", 115 | })) 116 | ); 117 | 118 | // Extract facts from each source 119 | ctx.logger.info("Extracting facts from markdown content"); 120 | const factsResults = await extractFacts.run( 121 | mdResults.map((result) => ({ 122 | source: result.markdown, 123 | query: input.message, 124 | sourceInfo: { 125 | url: result.url, 126 | title: result.title, 127 | index: result.index, 128 | }, 129 | })) 130 | ); 131 | 132 | // Add new facts to all facts 133 | const newFacts = factsResults.flatMap((result) => result.facts); 134 | allFacts.push(...newFacts); 135 | ctx.logger.info( 136 | `Extracted ${newFacts.length} new facts, total facts: ${allFacts.length}` 137 | ); 138 | 139 | // Judge if we have enough facts 140 | ctx.logger.info("Judging if we have enough facts"); 141 | factsJudgment = await judgeFacts.run({ 142 | query: input.message, 143 | facts: allFacts.map((f) => f.text), 144 | }); 145 | 146 | // Update missing aspects for next iteration 147 | missingAspects = factsJudgment.missingAspects; 148 | ctx.logger.info(`Missing aspects: ${missingAspects.join(", ")}`); 149 | 150 | // If we have enough facts or reached max iterations, generate final summary 151 | if (factsJudgment.hasEnoughFacts || iteration >= maxIterations) { 152 | ctx.logger.info( 153 | `Generating final summary (hasEnoughFacts: ${ 154 | factsJudgment.hasEnoughFacts 155 | }, reachedMaxIterations: ${iteration >= maxIterations})` 156 | ); 157 | break; 158 | } 159 | } 160 | 161 | // Always summarize and judge results after the loop 162 | const summarizeResult = await summarize.run({ 163 | text: input.message, 164 | facts: allFacts, 165 | sources: allSources, 166 | }); 167 | 168 | ctx.logger.info("Judging final results"); 169 | const judgeResult = await judgeResults.run({ 170 | query: input.message, 171 | result: summarizeResult.summary, 172 | }); 173 | 174 | ctx.logger.info( 175 | `Deep research complete (isComplete: ${judgeResult.isComplete}, totalFacts: ${allFacts.length}, totalSources: ${allSources.length}, iterations: ${iteration})` 176 | ); 177 | 178 | return { 179 | result: { 180 | isComplete: judgeResult.isComplete, 181 | reason: judgeResult.reason, 182 | sources: allSources, 183 | summary: summarizeResult.summary, 184 | facts: allFacts, 185 | iterations: iteration, 186 | factsJudgment: factsJudgment, 187 | searchPlans: plan?.reasoning, 188 | }, 189 | }; 190 | }, 191 | }); 192 | --------------------------------------------------------------------------------