# Agent Source: https://thinkwell.sh/api/agent Open connections to agents with the open() API. The `open()` function is the main entry point for Thinkwell. It connects to an AI agent (like Claude Code) and returns an `Agent` instance with a fluent API for blending deterministic code with LLM-powered reasoning. ## Opening an Agent Use `open()` to establish a connection to an AI agent by name. ### Named Agents Thinkwell provides built-in support for popular AI agents: ```typescript theme={null} import { open } from "thinkwell"; // Connect to Claude Code const agent = await open('claude'); // Or connect to other supported agents const codexAgent = await open('codex'); const geminiAgent = await open('gemini'); ``` Supported agent names: | Name | Command | | ------------ | ---------------------------------------------- | | `'claude'` | `npx -y @agentclientprotocol/claude-agent-acp` | | `'codex'` | `npx -y @zed-industries/codex-acp` | | `'gemini'` | `npx -y @google/gemini-cli --experimental-acp` | | `'kiro'` | `kiro-cli acp` | | `'opencode'` | `opencode acp` | | `'auggie'` | `auggie --acp` | ### Custom Commands You can also connect using any custom command that implements the Agent Client Protocol: ```typescript theme={null} const agent = await open({ cmd: 'my-custom-agent --acp' }); ``` ### Connection Options The `open()` function accepts optional configuration: ```typescript theme={null} const agent = await open('claude', { // Environment variables for the agent process env: { ANTHROPIC_API_KEY: process.env.MY_API_KEY, }, // Connection timeout in milliseconds timeout: 30000, }); ``` ### Environment Variable Overrides `open()` checks two environment variables before resolving the agent: * `$THINKWELL_AGENT` — an agent name: `THINKWELL_AGENT=opencode thinkwell script.ts` * `$THINKWELL_AGENT_CMD` — a command string: `THINKWELL_AGENT_CMD="myagent --acp" thinkwell script.ts` If both are set, `$THINKWELL_AGENT_CMD` takes precedence. Either way, the env override applies regardless of what the script passes to `open()`. This lets you swap agents at runtime without changing code. ## Ephemeral vs Persistent Sessions Thinkwell supports two patterns for interacting with agents: ### Ephemeral Sessions (Single-Turn) Use `agent.think()` for one-off prompts that don't need conversation history. Each call creates an ephemeral session that is automatically closed when the prompt completes. ```typescript theme={null} import { open } from "thinkwell"; /** * A summary of content. * @JSONSchema */ interface Summary { title: string; points: string[]; } const agent = await open('claude'); const summary = await agent .think(Summary.Schema) .text("Summarize this document:") .quote(documentContent) .run(); console.log(summary.title); console.log(summary.points); agent.close(); ``` Ephemeral sessions are ideal for: * Independent, self-contained tasks * Stateless operations that don't need context * Parallel processing of multiple prompts ### Persistent Sessions (Multi-Turn) Use `agent.createSession()` for multi-turn conversations where the agent needs to remember previous interactions. ```typescript theme={null} import { open } from "thinkwell"; const agent = await open('claude'); const session = await agent.createSession({ cwd: "/my/project" }); // First turn: analyze the codebase const analysis = await session .think(AnalysisSchema) .text("Analyze this codebase for potential issues") .run(); // Second turn: the agent remembers the analysis const fixes = await session .think(FixesSchema) .text("Suggest fixes for the top 3 issues you found") .run(); session.close(); agent.close(); ``` Persistent sessions are ideal for: * Multi-step workflows where context matters * Iterative refinement of results * Conversations that build on previous responses ## Code Examples ### Basic Prompt with Structured Output ```typescript theme={null} import { open } from "thinkwell"; /** * Sentiment analysis result. * @JSONSchema */ interface Sentiment { /** Overall sentiment: positive, negative, or neutral */ sentiment: "positive" | "negative" | "neutral"; /** Confidence score from 0 to 1 */ confidence: number; /** Brief explanation */ explanation: string; } async function analyzeSentiment(text: string) { const agent = await open('claude'); try { return await agent .think(Sentiment.Schema) .text("Analyze the sentiment of this text:") .quote(text) .run(); } finally { agent.close(); } } ``` ### Using Custom Tools ```typescript theme={null} import { open } from "thinkwell"; /** * A greeting message. * @JSONSchema */ interface Greeting { message: string; } const agent = await open('claude'); const greeting = await agent .think(Greeting.Schema) .text("Create a greeting appropriate for the current time of day.") .tool( "current_time", "Returns the current date and time.", async () => ({ time: new Date().toLocaleTimeString(), date: new Date().toLocaleDateString(), }) ) .run(); agent.close(); ``` ### Multi-Turn Session with Working Directory ```typescript theme={null} import { open } from "thinkwell"; const agent = await open('claude'); // Create a session scoped to a specific project directory const session = await agent.createSession({ cwd: "/path/to/project", systemPrompt: "You are a helpful code review assistant.", }); // The agent can access files relative to the working directory const review = await session .think(CodeReviewSchema) .text("Review the main entry point of this project") .run(); // Follow-up questions maintain context const details = await session .think(DetailSchema) .text("Explain more about the third issue you mentioned") .run(); session.close(); agent.close(); ``` ## API Reference ### `open(name, options?)` Opens a connection to a named agent. **Parameters:** | Parameter | Type | Description | | --------- | -------------- | --------------------------------------------------------------------------------- | | `name` | `AgentName` | Agent name: `'claude'`, `'codex'`, `'gemini'`, `'kiro'`, `'opencode'`, `'auggie'` | | `options` | `AgentOptions` | Optional connection configuration | **Returns:** `Promise` ### `open(options)` Opens a connection using a custom command. **Parameters:** | Parameter | Type | Description | | --------- | -------------------- | --------------------------------- | | `options` | `CustomAgentOptions` | Options with required `cmd` field | **Returns:** `Promise` **AgentOptions:** | Property | Type | Description | | --------- | ------------------------ | ------------------------------------------- | | `env` | `Record` | Environment variables for the agent process | | `timeout` | `number` | Connection timeout in milliseconds | **CustomAgentOptions** (extends AgentOptions): | Property | Type | Description | | -------- | -------- | -------------------------------------------- | | `cmd` | `string` | The shell command to spawn the agent process | *** ### `agent.think(schema)` Creates a `Plan` for constructing a single-turn prompt. Each call creates an ephemeral session that is automatically closed when the prompt completes. **Parameters:** | Parameter | Type | Description | | --------- | ------------------------ | ------------------------------------- | | `schema` | `SchemaProvider` | Defines the expected output structure | **Returns:** `Plan` *** ### `agent.createSession(options?)` Creates a persistent session for multi-turn conversations. **Parameters:** | Parameter | Type | Description | | --------- | ---------------- | ------------------------------ | | `options` | `SessionOptions` | Optional session configuration | **Returns:** `Promise` **SessionOptions:** | Property | Type | Description | | -------------- | -------- | --------------------------------------------------------------- | | `cwd` | `string` | Working directory for the session (defaults to `process.cwd()`) | | `systemPrompt` | `string` | System prompt for the session | *** ### `agent.close()` Closes the connection to the agent. This shuts down the conductor and invalidates any active sessions. **Returns:** `void` ## See Also * [API Overview](/api/overview) - Plan methods and schema providers * [Sessions](/api/sessions) - Multi-turn conversation sessions # Overview Source: https://thinkwell.sh/api/overview Overview of the Thinkwell API. Thinkwell provides a TypeScript API for integrating AI agents into your applications. The library connects to AI coding agents via the Agent Client Protocol (ACP) and provides a fluent interface for building prompts, managing sessions, and handling typed responses. ## Module Structure Thinkwell exports everything from a single package: ```typescript theme={null} import { open } from "thinkwell"; import type { Agent, Session } from "thinkwell"; ``` | Export | Description | | --------- | -------------------------------------------------------- | | `open()` | Main entry point — opens a connection to a named agent | | `Agent` | The agent interface returned by `open()` | | `Session` | Manages multi-turn conversations with persistent context | | `Plan` | Fluent API for building prompts with tools | See [Agent](/api/agent) for the list of supported agent names. ## Architecture Thinkwell follows a layered architecture: 1. **Agent** - Establishes a connection to an AI agent via ACP. Create one with `open()`. 2. **Sessions** - Agents support two interaction modes: * **Ephemeral**: Use `agent.think()` for single-turn interactions * **Persistent**: Use `agent.createSession()` for multi-turn conversations 3. **Plan** - A fluent API for constructing prompts. Chain methods to build content, then call `.run()` to execute. ## Structured Output with @JSONSchema Thinkwell uses the `@JSONSchema` JSDoc tag to define expected AI output structures. The CLI generates JSON Schemas from your TypeScript interfaces at build time: ```typescript theme={null} /** * Analysis result from code review. * @JSONSchema */ export interface AnalysisResult { /** List of issues found */ issues: Array<{ severity: "low" | "medium" | "high"; description: string; }>; /** Overall summary */ summary: string; } // The schema is available as a static property const result = await agent.think(AnalysisResult.Schema).text("...").run(); ``` JSDoc comments become `description` fields in the generated schema, helping the AI understand what each field should contain. ## Next Steps * [Agent](/api/agent) - Opening agents, named agents, and managing sessions * [Plan](/api/plan) - Building prompts with the fluent API * [Sessions](/api/sessions) - Multi-turn conversations # Plan Source: https://thinkwell.sh/api/plan The fluent API for building prompts. The `Plan` class provides a fluent interface for constructing prompts. You obtain a `Plan` by calling `agent.think()` or `session.think()`, then chain methods to build up your prompt content before executing with `.run()`. `Plan` was previously named `ThinkBuilder`. The old name is still available as a deprecated type alias. ## Basic Usage ```typescript theme={null} import { open } from "thinkwell"; /** @JSONSchema */ interface Summary { title: string; points: string[]; } const agent = await open('claude'); const result = await agent .think(Summary.Schema) // Start building with output schema .text("Summarize this:") // Add prompt text .quote(documentContent) // Add quoted content .run(); // Execute and get typed result agent.close(); ``` ## Content Methods These methods add content to your prompt. ### `.text(content)` Adds literal text to the prompt. ```typescript theme={null} .text("Analyze the following code for potential bugs.") ``` ### `.textln(content)` Adds text with a trailing newline. Useful when building prompts incrementally. ```typescript theme={null} .textln("First, identify the main function.") .textln("Then, trace the data flow.") ``` ### `.quote(content, label?)` Adds content wrapped in XML-style tags. Use this for user input, documents, or any content that should be clearly delimited from instructions. ```typescript theme={null} // Without label .quote(userInput) // With label for clarity .quote(customerFeedback, "feedback") .quote(companyPolicy, "policy document") ``` The label helps the AI understand what the quoted content represents. ### `.code(content, language?)` Adds content as a fenced Markdown code block. Use this when including source code in your prompt. ```typescript theme={null} // Without language hint .code(sourceCode) // With language for syntax context .code(jsCode, "javascript") .code(pythonCode, "python") ``` ## Tool Methods Tools allow the AI agent to call back into your code during prompt execution. ### `.tool(name, description, handler)` - Simple Form Register a tool that takes no input parameters. ```typescript theme={null} .tool( "current_time", "Returns the current date and time.", async () => ({ time: new Date().toLocaleTimeString(), date: new Date().toLocaleDateString(), timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, }) ) ``` ### `.tool(name, description, inputSchema, handler)` - With Input Schema Register a tool with typed, validated input. Define the input shape using an interface with `@JSONSchema`: ```typescript theme={null} /** @JSONSchema */ interface SearchInput { /** Glob pattern to match files */ pattern: string; /** Maximum number of results */ limit?: number; } .tool( "search_files", "Search for files matching a glob pattern.", SearchInput.Schema, async (input) => { // input is typed as SearchInput const files = await glob(input.pattern); return { files: files.slice(0, input.limit ?? 10) }; } ) ``` The schema serves three purposes: 1. Tells the AI how to format tool calls 2. Validates incoming calls at runtime 3. Provides TypeScript typing for your handler ## Skill Methods Skills let you give the agent reusable, self-contained capabilities following the [Agent Skills standard](https://agentskills.io/). Skills support progressive disclosure — only metadata is loaded initially, with full instructions loaded on demand when the agent activates a skill. ### `.skill(path)` - Stored Skill Load a skill from a `SKILL.md` file on disk. ```typescript theme={null} .skill("./skills/code-review") ``` The file is parsed at `run()` time. The skill directory must contain a `SKILL.md` with YAML frontmatter: ```yaml theme={null} --- name: code-review description: Reviews code for bugs, style issues, and best practices. --- # Code Review ## Steps 1. Read the files to review 2. Identify bugs, style issues, and improvement opportunities ... ``` Reference files and assets in the skill directory can be accessed via the `read_skill_file` tool. ### `.skill(definition)` - Virtual Skill Define a skill programmatically without filesystem artifacts. ```typescript theme={null} .skill({ name: "test-writer", description: "Generates unit tests for TypeScript functions.", body: ` # Test Writer ## Steps 1. Analyze the function signature and behavior 2. Generate comprehensive test cases 3. Use the \`count-assertions\` tool to verify coverage ## Available Tools ### count-assertions Count assertions in a test file. Input: \`{ "path": "string" }\` `, tools: [{ name: "count-assertions", description: "Count assertions in a test file", handler: async ({ path }) => { const content = await fs.readFile(path, "utf-8"); const matches = content.match(/expect\(/g) || []; return { count: matches.length }; }, }], }) ``` Virtual skills can include handler functions via the `tools` array. These are invoked through the `call_skill_tool` dispatcher — they don't appear as individual MCP tools, preserving progressive disclosure. ### Skill Name Rules Skill names must follow the Agent Skills spec: * 1-64 characters * Lowercase alphanumeric plus hyphens * No leading, trailing, or consecutive hyphens ### Multiple Skills You can attach multiple skills to a single prompt: ```typescript theme={null} const result = await agent .think(OutputSchema) .skill("./skills/code-review") .skill({ name: "test-writer", description: "Generates unit tests.", body: "...", }) .text("Review the auth module and write tests for any issues found") .run(); ``` ## Configuration Methods ### `.cwd(path)` Sets the working directory for the session. This affects where the agent looks for files when using its built-in tools. ```typescript theme={null} .cwd("/path/to/project") ``` ## Execution ### `.run()` Executes the prompt and returns the typed result. This is always the final method in the chain. ```typescript theme={null} const result = await agent .think(OutputSchema) .text("Your prompt here") .run(); // result is typed according to OutputSchema ``` ### `.stream()` Executes the prompt and returns a `ThoughtStream` — a handle providing both the final typed result and an async iterable of intermediate progress events. ```typescript theme={null} const stream = agent .think(OutputSchema) .text("Analyze this codebase") .stream(); // Iterate over events as they arrive for await (const event of stream) { if (event.type === "thought") { process.stderr.write(event.text); } } // Get the final result const result = await stream.result; ``` Execution begins eagerly when `stream()` is called — you don't need to iterate to start the operation. ## ThoughtStream The `ThoughtStream` class provides access to streaming events during prompt execution. ### Properties #### `.result` A `Promise` that resolves with the final typed result. ```typescript theme={null} const stream = agent.think(schema).text("...").stream(); const result = await stream.result; ``` ### Async Iteration `ThoughtStream` implements `AsyncIterable`, allowing you to iterate over events with `for await`: ```typescript theme={null} for await (const event of stream) { switch (event.type) { case "thought": ui.updateThinking(event.text); break; case "tool_start": ui.showToolActivity(event.title, event.kind); break; case "tool_done": ui.clearToolActivity(event.id); break; case "plan": ui.renderPlan(event.entries); break; } } ``` ### Execution Semantics The async iterator and result promise have independent lifecycles: * **Iterate without awaiting `.result`** — the result resolves in the background * **Await `.result` without iterating** — events buffer internally * **Do both concurrently** — iterate and await simultaneously * **Early termination** — break out of the loop and still await `.result` ```typescript theme={null} const stream = agent.think(schema).text("...").stream(); for await (const event of stream) { if (event.type === "thought") { process.stderr.write(event.text); } if (someCondition) break; // Early exit is safe } // Result is still available after breaking const result = await stream.result; ``` ## ThoughtEvent Types Events emitted during streaming are represented as a discriminated union: ### `thought` Streaming internal reasoning / chain-of-thought from the agent. ```typescript theme={null} { type: "thought"; text: string } ``` ### `message` Streaming visible response text from the agent. ```typescript theme={null} { type: "message"; text: string } ``` ### `tool_start` Emitted when the agent starts using a tool. ```typescript theme={null} { type: "tool_start"; id: string; title: string; kind?: ToolKind } ``` The `kind` field indicates the category of tool: `"read"`, `"edit"`, `"delete"`, `"move"`, `"search"`, `"execute"`, `"think"`, `"fetch"`, `"switch_mode"`, or `"other"`. ### `tool_update` Emitted during tool execution with progress or intermediate content. ```typescript theme={null} { type: "tool_update"; id: string; status: string; content?: ToolContent[] } ``` The `content` array may include: * `{ type: "content"; content: ContentBlock }` — text, image, or resource link * `{ type: "diff"; path: string; oldText: string; newText: string }` — file diff * `{ type: "terminal"; terminalId: string }` — terminal output reference ### `tool_done` Emitted when a tool completes. ```typescript theme={null} { type: "tool_done"; id: string; status: "completed" | "failed" } ``` ### `plan` Emitted when the agent shares its execution plan. ```typescript theme={null} { type: "plan"; entries: PlanEntry[] } ``` Each `PlanEntry` has: * `content: string` — description of the plan step * `status: "pending" | "in_progress" | "completed"` * `priority: "high" | "medium" | "low"` ## Complete Example Here's a complete example showing multiple Plan features: ```typescript theme={null} import { open } from "thinkwell"; import * as fs from "fs/promises"; /** * Result of analyzing a codebase. * @JSONSchema */ interface CodeAnalysis { /** Main programming language detected */ language: string; /** List of potential issues */ issues: Array<{ file: string; line: number; description: string; severity: "low" | "medium" | "high"; }>; /** Summary of the codebase */ summary: string; } /** * Input for reading a file. * @JSONSchema */ interface ReadFileInput { /** Path to the file to read */ path: string; } async function analyzeProject(projectPath: string) { const agent = await open('claude'); try { const analysis = await agent .think(CodeAnalysis.Schema) .cwd(projectPath) .text(` Analyze this codebase for potential issues. Use the read_file tool to examine source files. Focus on bugs, security issues, and code smells. `) .tool( "read_file", "Read the contents of a file in the project.", ReadFileInput.Schema, async (input) => { const content = await fs.readFile(input.path, "utf-8"); return { content }; } ) .tool( "list_files", "List all files in the project directory.", async () => { const files = await fs.readdir(projectPath, { recursive: true }); return { files }; } ) .run(); return analysis; } finally { agent.close(); } } ``` ## Method Chaining Order While most methods can be called in any order, we recommend this sequence for readability: 1. `.think(schema)` - Always first (starts the builder) 2. `.cwd(path)` - Configuration 3. `.skill()` - Skill attachments 4. `.text()` / `.textln()` / `.quote()` / `.code()` - Main prompt instructions 5. `.tool()` - Tool definitions 6. `.run()` or `.stream()` - Always last (executes the prompt) ```typescript theme={null} const result = await agent .think(OutputSchema) // 1. Schema .cwd("/my/project") // 2. Config .skill("./skills/code-review") // 3. Skills .text("Analyze this code:") // 4. Instructions .code(sourceCode, "typescript") .tool("helper", "...", handler) // 5. Tools .run(); // 6. Execute // Or with streaming: const stream = agent .think(OutputSchema) .skill("./skills/code-review") .text("Analyze this code:") .stream(); // 6. Execute with streaming ``` # Sessions Source: https://thinkwell.sh/api/sessions Creating multiple sessions with an agent. Sessions enable multi-turn conversations with an agent, maintaining context across multiple `think()` calls. This is useful when you need the agent to remember previous interactions within a conversation. ## Creating a Session Create a session using `agent.createSession()`: ```typescript theme={null} import { open } from "thinkwell"; const agent = await open('claude'); const session = await agent.createSession({ cwd: "/my/project" }); ``` ## Multi-Turn Conversations Unlike standalone `agent.think()` calls, prompts sent through a session maintain conversation context. The agent remembers what was discussed in previous turns: ```typescript theme={null} /** @JSONSchema */ interface Analysis { issues: string[]; severity: "low" | "medium" | "high"; } /** @JSONSchema */ interface Fixes { suggestions: string[]; } // First turn - analyze the codebase const analysis = await session .think(Analysis.Schema) .text("Analyze this codebase for potential issues") .run(); console.log(`Found ${analysis.issues.length} issues`); // Second turn - the agent remembers the analysis const fixes = await session .think(Fixes.Schema) .text("Suggest fixes for the top issues you identified") .run(); for (const suggestion of fixes.suggestions) { console.log(`- ${suggestion}`); } ``` ## Session Options When creating a session, you can configure it with `SessionOptions`: ```typescript theme={null} const session = await agent.createSession({ cwd: "/path/to/working/directory", systemPrompt: "You are a helpful code reviewer." }); ``` | Option | Type | Description | | -------------- | -------- | --------------------------------------------------------------- | | `cwd` | `string` | Working directory for the session. Defaults to `process.cwd()`. | | `systemPrompt` | `string` | System prompt that applies to all turns in this session. | ## Session API ### `sessionId` The unique identifier for this session: ```typescript theme={null} console.log(`Session ID: ${session.sessionId}`); ``` ### `think(schema)` Create a `Plan` for constructing a prompt. Works the same as `agent.think()`, but maintains conversation context within the session: ```typescript theme={null} const result = await session .think(OutputSchema) .text("Your prompt here") .run(); ``` ### `close()` Close the session when you're done. After closing, no more prompts can be sent through this session: ```typescript theme={null} session.close(); ``` The agent connection remains open and can be used for other sessions or standalone calls. ## Complete Example Here's a complete example showing a multi-turn code review session: ```typescript theme={null} import { open } from "thinkwell"; /** @JSONSchema */ interface ReviewResult { summary: string; issues: Array<{ file: string; line: number; description: string; }>; } /** @JSONSchema */ interface FixPlan { steps: string[]; estimatedEffort: "trivial" | "small" | "medium" | "large"; } async function reviewCode(projectPath: string) { const agent = await open('claude'); const session = await agent.createSession({ cwd: projectPath }); try { // First turn: review the code const review = await session .think(ReviewResult.Schema) .text("Review the code in this project for bugs and improvements") .run(); console.log(`Review: ${review.summary}`); console.log(`Found ${review.issues.length} issues\n`); // Second turn: create a fix plan based on the review const plan = await session .think(FixPlan.Schema) .text("Create a plan to fix the most critical issues you found") .run(); console.log(`Estimated effort: ${plan.estimatedEffort}`); console.log("Steps:"); plan.steps.forEach((step, i) => console.log(`${i + 1}. ${step}`)); } finally { session.close(); agent.close(); } } reviewCode("/path/to/project"); ``` ## Sessions vs Standalone Calls Use **sessions** when: * You need multi-turn conversations * Context from previous interactions matters * You're building a conversational workflow Use **standalone `agent.think()` calls** when: * Each request is independent * You don't need conversation history * You want simpler, stateless interactions # Code of Conduct Source: https://thinkwell.sh/community/code-of-conduct Our community code of conduct. # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. # Contributing Source: https://thinkwell.sh/community/contributing How to contribute to Thinkwell. Thank you for your interest in contributing to Thinkwell! This guide will help you get set up and explain our development workflow. ## Development Setup ### Prerequisites * **Node.js 24+** with native TypeScript support * **pnpm** as the package manager ### Clone and Install ```bash theme={null} git clone https://github.com/dherman/thinkwell.git cd thinkwell pnpm install ``` ### Run in Development Mode You can run TypeScript files directly using Node.js with native TypeScript support: ```bash theme={null} node --experimental-transform-types src/cli.ts ``` ### Build To build the project: ```bash theme={null} pnpm build ``` This uses esbuild to bundle the code, followed by [@yao-pkg/pkg](https://github.com/yao-pkg/pkg) for binary packaging. ### Important: Do Not Use Bun This project explicitly does not use Bun for runtime or binary distribution. We encountered fundamental limitations with Bun's module resolution from its virtual filesystem that prevented user scripts from importing packages. See `doc/rfd/pkg-migration.md` in the repository for the full technical analysis. ## Running Tests Run the test suite with: ```bash theme={null} pnpm test ``` To run tests in watch mode during development: ```bash theme={null} pnpm test:watch ``` ## Commit Message Format This project uses [Conventional Commits](https://www.conventionalcommits.org/) for clear history and automated release management. All commit messages should follow this format: ``` [optional scope]: [optional body] [optional footer(s)] ``` ### Commit Types | Type | Description | Version Bump | | ---------- | --------------------------------------------------- | ------------ | | `feat` | A new feature | Minor | | `fix` | A bug fix | Patch | | `docs` | Documentation only changes | None | | `style` | Code style changes (formatting, semicolons, etc.) | None | | `refactor` | Code changes that neither fix bugs nor add features | None | | `perf` | Performance improvements | None | | `test` | Adding or updating tests | None | | `chore` | Maintenance tasks, dependency updates | None | | `ci` | CI/CD configuration changes | None | | `build` | Build system or external dependency changes | None | ### Common Scopes * `acp` - Core protocol changes * `thinkwell` - High-level API changes * `conductor` - Conductor-specific changes * `deps` - Dependency updates ### Breaking Changes Indicate breaking changes by adding `!` after the type: ``` feat!: change API to use async traits ``` Or include `BREAKING CHANGE:` in the commit footer: ``` feat: redesign conductor protocol BREAKING CHANGE: conductor now requires explicit capability registration ``` ### Examples ``` feat(conductor): add support for dynamic proxy chains fix(acp): resolve deadlock in message routing docs: update README with installation instructions chore(deps): bump @agentclientprotocol/sdk to 0.12.0 ``` ## Pull Request Process 1. **Create a branch** from `main` with a descriptive name: ```bash theme={null} git checkout -b feat/add-new-connector ``` 2. **Make your changes** with clear, focused commits following the conventional commits format. 3. **Run tests** to ensure your changes work correctly: ```bash theme={null} pnpm test ``` 4. **Push your branch** and open a pull request against `main`. 5. **Describe your changes** in the PR description. Include: * What the change does * Why it's needed * Any breaking changes or migration steps 6. **Address review feedback** by pushing additional commits. We squash merge PRs, so don't worry about commit count. ## Code Style Guidelines ### TypeScript * Use TypeScript for all source code * Prefer explicit types over `any` * Use JSDoc comments for public APIs * Use the `@JSONSchema` decorator for interfaces that need schema generation ### Formatting The project uses consistent formatting rules. Before committing: ```bash theme={null} pnpm lint pnpm format ``` ### File Organization * Source code lives in `src/` * Tests are co-located with source files or in `__tests__/` directories * Documentation source is in `website/` ### Naming Conventions * Use `camelCase` for variables and functions * Use `PascalCase` for classes, interfaces, and types * Use `UPPER_SNAKE_CASE` for constants * Use descriptive names that convey intent ## Getting Help If you have questions or need help: * Open a [GitHub Issue](https://github.com/dherman/thinkwell/issues) * Check existing issues for similar problems * Read through the [documentation](/get-started/introduction) We appreciate all contributions, whether it's fixing a typo, improving documentation, or adding new features. Thank you for helping make Thinkwell better! # Hello, World Source: https://thinkwell.sh/examples/hello-world A minimal example. This is the simplest example of using Thinkwell. It demonstrates the core concepts: connecting to an agent, defining a structured output with `@JSONSchema`, providing a custom tool, and running a thinking session. ## The Full Example ```typescript theme={null} import { open } from "thinkwell"; /** * A friendly greeting. * @JSONSchema */ export interface Greeting { /** The greeting message */ message: string; } async function main() { const agent = await open('claude'); try { const greeting = await agent .think(Greeting.Schema) .text(` Use the current_time tool to get the current time, and create a friendly greeting message appropriate for that time of day. `) .tool( "current_time", "Produces the current date, time, and time zone.", async () => { const now = new Date(); return { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, time: now.toLocaleTimeString(), date: now.toLocaleDateString(), dayOfWeek: now.toLocaleDateString('en-US', { weekday: 'long' }), }; } ) .run(); console.log(`✨ ${greeting.message}`); } finally { agent.close(); } } main(); ``` ## Understanding the Code ### Opening an Agent ```typescript theme={null} const agent = await open('claude'); ``` Thinkwell works by connecting to an external AI agent. The `open()` function accepts a named agent string and establishes a connection using the Agent Client Protocol (ACP). You can override the agent at runtime by setting the `THINKWELL_AGENT` or `THINKWELL_AGENT_CMD` environment variable. ### Defining Structured Output with @JSONSchema ```typescript theme={null} /** * A friendly greeting. * @JSONSchema */ export interface Greeting { /** The greeting message */ message: string; } ``` The `@JSONSchema` JSDoc tag is the key to type-safe AI outputs. When you annotate an interface with `@JSONSchema`, Thinkwell automatically generates a JSON Schema at build time and attaches it to the interface as a static `.Schema` property. This means: * **At build time**: Thinkwell transforms your TypeScript interface into a JSON Schema * **At runtime**: The schema is passed to the AI agent, which constrains its output to match * **In your code**: You get full TypeScript type checking on the result The JSDoc comments on the interface and its properties become `description` fields in the schema, helping the AI understand what each field should contain. ### Building a Thinking Session ```typescript theme={null} const greeting = await agent .think(Greeting.Schema) .text(`...prompt...`) .tool("current_time", "...", async () => { ... }) .run(); ``` Thinkwell uses a fluent builder pattern to construct thinking sessions: 1. **`.think(schema)`** - Starts a new session and specifies the expected output schema 2. **`.text(prompt)`** - Provides the prompt or instructions for the AI 3. **`.tool(name, description, handler)`** - Registers a tool the AI can call 4. **`.run()`** - Executes the session and returns the typed result ### Providing Tools ```typescript theme={null} .tool( "current_time", "Produces the current date, time, and time zone.", async () => { const now = new Date(); return { timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone, time: now.toLocaleTimeString(), date: now.toLocaleDateString(), dayOfWeek: now.toLocaleDateString('en-US', { weekday: 'long' }), }; } ) ``` Tools extend what the AI can do by giving it access to real-world data and capabilities. Each tool has: * **A name** - How the AI refers to and invokes the tool * **A description** - Helps the AI understand when and how to use the tool * **A handler function** - The actual implementation that runs when called In this example, the `current_time` tool lets the AI ask "what time is it?" so it can generate an appropriate greeting like "Good morning!" or "Good evening!" ### Cleanup ```typescript theme={null} try { // ... use the agent ... } finally { agent.close(); } ``` Always close the agent connection when you're done. The `try/finally` pattern ensures cleanup happens even if an error occurs. ## Running the Example Save the code to a file (e.g., `greeting.ts`) and run it with the Thinkwell CLI: ```bash theme={null} thinkwell greeting.ts ``` You should see output like: ``` ✨ Good afternoon! Hope you're having a wonderful Wednesday! ``` The exact message will vary based on the current time and day. # JavaScript Unminifier Source: https://thinkwell.sh/examples/javascript-unminifier Build simple code transformations using agents. This example demonstrates how to build a pipeline that transforms minified JavaScript into readable code, mixing LLM-powered analysis with deterministic transformations. It showcases Thinkwell's core pattern: use deterministic tools where they excel, and delegate semantic understanding to the AI agent. ## What the Unminifier Does The unminifier takes minified JavaScript (like `underscore-umd-min.js`) and produces readable code through five steps: 1. **Pretty-print** with [Prettier](https://prettier.io) (deterministic) 2. **Convert UMD to ESM** with [ast-grep](https://ast-grep.github.io) pattern matching (deterministic) 3. **Extract functions** - identify all top-level functions with minified names (LLM) 4. **Analyze functions** - suggest descriptive names for each function in parallel batches (LLM) 5. **Apply renames** with Babel's scope-aware renaming (deterministic) This pipeline illustrates a key insight: some transformations are purely mechanical (formatting, module conversion, renaming), while others require semantic understanding (identifying and naming functions). Thinkwell lets you mix both approaches naturally. ## The Schema Definitions The LLM steps (3 and 4) use structured outputs defined with the `@JSONSchema` annotation: ```typescript theme={null} /** * Information about a function found in the code. * @JSONSchema */ export interface FunctionInfo { /** Current function name (may be minified) */ name: string; /** Function signature including parameters */ signature: string; /** Approximate line number where the function is defined */ lineNumber: number; } /** * List of functions extracted from the code. * @JSONSchema */ export interface FunctionList { /** List of all top-level functions in the code */ functions: FunctionInfo[]; } /** * Analysis result for a single function. * @JSONSchema */ export interface FunctionAnalysis { /** The original minified function name */ originalName: string; /** Suggested descriptive name (camelCase, no underscores unless conventional) */ suggestedName: string; /** Brief description of what the function does */ purpose: string; /** Confidence level in the suggested name */ confidence: "high" | "medium" | "low"; } /** * Batch of function analyses. * @JSONSchema */ export interface FunctionAnalysisBatch { /** Array of function analyses */ analyses: FunctionAnalysis[]; } ``` Notice how each schema serves the LLM steps: * `FunctionList` uses a nested structure to return multiple items * `FunctionAnalysis` includes a `confidence` field with literal union types for quality signals * `FunctionAnalysisBatch` wraps multiple analyses for efficient batched processing ## Step 1: Pretty-Print with Prettier The first step is purely mechanical - no LLM required. Use [Prettier](https://prettier.io) to make the minified code readable: ```typescript theme={null} import * as prettier from "prettier"; export async function formatCode(code: string): Promise { return prettier.format(code, { parser: "babel", printWidth: 100, tabWidth: 2, semi: true, singleQuote: false, }); } ``` This demonstrates an important pattern: **use deterministic tools where they excel**. Prettier handles formatting perfectly — there's no need to involve an LLM. ## Step 2: Convert UMD to ESM UMD boilerplate follows a predictable pattern, so there's no need for an LLM here. We use [ast-grep](https://ast-grep.github.io) to match the UMD wrapper and rewrite it in a single pass: ```typescript theme={null} import * as os from "os"; import * as path from "path"; import { execFile } from "child_process"; const tmpFile = path.join(os.tmpdir(), `unminify-${Date.now()}.js`); await fs.writeFile(tmpFile, prettyCode); await new Promise((resolve, reject) => { execFile("ast-grep", [ "run", "--pattern", "!($IIFE)(this, function () { $$$BODY return $RET; })", "--rewrite", "$$$BODY\nexport default $RET;", "--lang", "js", "--update-all", tmpFile, ], (err) => err ? reject(err) : resolve()); }); const esmCode = await formatCode(await fs.readFile(tmpFile, "utf-8")); await fs.unlink(tmpFile).catch(() => {}); ``` The ast-grep pattern `!($IIFE)(this, function () { $$$BODY return $RET; })` matches the UMD IIFE wrapper. The `$$$BODY` metavariable captures all statements inside, and `$RET` captures the return value. The rewrite replaces the entire wrapper with just the body plus `export default $RET;`. Once again, this demonstrates the benefits of using deterministic tools where they excel: ast-grep is convenient and effective for matching simple syntactic patterns. Not only that, it's highly efficient and cheaper; transforming a large source file like Underscore with an LLM chews through quite a few tokens. ## Step 3: Extract Function List Next, ask the agent to identify all functions that need renaming: ```typescript theme={null} const functionList = await agent .think(FunctionList.Schema) .text(` Extract a list of all top-level function declarations and function expressions assigned to variables in this code. Include the function name, its signature (parameters), and approximate line number. Focus on functions with short (1-2 character) names that appear to be minified. `) .code(esmCode, "javascript") .run(); console.log(`Found ${functionList.functions.length} functions to analyze`); ``` The structured `FunctionList` output gives you an array you can iterate over in TypeScript - bridging the gap between natural language analysis and programmatic control flow. ## Step 4: Parallel Batch Analysis Analyzing functions one at a time would be slow. Instead, batch them and process in parallel with a concurrency limit: ```typescript theme={null} import pLimit from "p-limit"; import _ from "lodash"; const renames: Map = new Map(); const limit = pLimit(5); // Max 5 concurrent requests const batches = _.chunk(functionList.functions, 30); // 30 functions per batch const results = await Promise.all( batches.map((batch) => limit(async () => { const functionListText = batch .map((f) => ` - "${f.name}" with signature ${f.signature}`) .join("\n"); return agent .think(FunctionAnalysisBatch.Schema) .text(` Analyze each of the following minified functions and suggest better, more descriptive names. IMPORTANT: For each function, the 'originalName' field in your response must be the EXACT minified name shown in quotes below. Functions to analyze: ${functionListText} Here is the full code for context: `) .code(esmCode, "javascript") .run(); }) ) ); // Collect all renames for (const batch of results) { for (const analysis of batch.analyses) { if (analysis.suggestedName !== analysis.originalName) { renames.set(analysis.originalName, analysis.suggestedName); console.log(` ${analysis.originalName} -> ${analysis.suggestedName}`); } } } ``` Key patterns here: * **`p-limit`** avoids the risk of rate-limiting or API bans * **`_.chunk`** groups functions into manageable batches * **`FunctionAnalysisBatch`** returns multiple analyses per request, reducing round trips * **Sending full source code** as context helps the LLM understand function interdependencies ## Step 5: Apply Renames Renaming is another task that doesn't need an LLM — Babel's scope-aware renaming handles it correctly and instantly: ```typescript theme={null} import { parse as babelParse } from "@babel/parser"; import { generate } from "@babel/generator"; import { default as _traverse } from "@babel/traverse"; const traverse: typeof _traverse = (_traverse as any).default ?? _traverse; const ast = babelParse(esmCode, { sourceType: "module", plugins: [] }); let renameCount = 0; traverse(ast, { Program(path: any) { for (const [oldName, newName] of renames) { if (path.scope.getBinding(oldName)) { path.scope.rename(oldName, newName); renameCount++; } } }, }); const renamedCode = generate(ast, { retainLines: false, comments: true }).code; const finalCode = await formatCode(renamedCode); console.log(`Applied ${renameCount} renames`); ``` Babel's `scope.rename()` is scope-aware, so it correctly renames function definitions and all their usages without touching unrelated identifiers that happen to share the same name. This is more reliable than asking an LLM to regenerate the entire file — and runs in milliseconds. ## The Complete Pipeline Here's how it all fits together — notice how LLM and deterministic steps interleave naturally: ```typescript theme={null} import { open } from "thinkwell"; import * as fs from "fs/promises"; import * as os from "os"; import * as path from "path"; import { execFile } from "child_process"; import * as prettier from "prettier"; import pLimit from "p-limit"; import _ from "lodash"; import { parse as babelParse } from "@babel/parser"; import { generate } from "@babel/generator"; import { default as _traverse } from "@babel/traverse"; const traverse: typeof _traverse = (_traverse as any).default ?? _traverse; async function main() { const agent = await open('claude'); try { const minifiedCode = await fs.readFile("underscore-umd-min.js", "utf-8"); // Step 1: Pretty-print (deterministic — Prettier) const prettyCode = await formatCode(minifiedCode); // Step 2: Convert UMD to ESM (deterministic — ast-grep) const tmpFile = path.join(os.tmpdir(), `unminify-${Date.now()}.js`); await fs.writeFile(tmpFile, prettyCode); await new Promise((resolve, reject) => { execFile("ast-grep", [ "run", "--pattern", "!($IIFE)(this, function () { $$$BODY return $RET; })", "--rewrite", "$$$BODY\nexport default $RET;", "--lang", "js", "--update-all", tmpFile, ], (err) => err ? reject(err) : resolve()); }); const esmCode = await formatCode(await fs.readFile(tmpFile, "utf-8")); // Step 3: Extract function list (LLM) const functionList = await agent .think(FunctionList.Schema) .text(`Extract all top-level functions...`) .code(esmCode, "javascript") .run(); // Step 4: Analyze functions in parallel batches (LLM) const renames = new Map(); // ... batch processing with p-limit ... // Step 5: Apply renames (deterministic — Babel) const ast = babelParse(esmCode, { sourceType: "module", plugins: [] }); traverse(ast, { Program(path: any) { for (const [oldName, newName] of renames) { if (path.scope.getBinding(oldName)) { path.scope.rename(oldName, newName); } } }, }); const finalCode = await formatCode(generate(ast).code); await fs.writeFile("underscore.js", finalCode, "utf-8"); } finally { agent.close(); } } main(); ``` ## Running the Example ```bash theme={null} thinkwell examples/src/unminify.ts ``` Sample output: ``` === Unminify Demo === Reading minified code... Input: 18432 characters Step 1: Pretty-printing with Prettier... Pretty-printed: 892 lines Step 2: Converting UMD wrapper to ESM... ESM module: 879 lines Step 3: Extracting function list... Found 47 functions to analyze Step 4: Analyzing functions in batches... Processing 2 batches... j -> isArrayLike w -> createCallback A -> optimizeCb ... Step 5: Applying renames with Babel... Applied 47 renames === Done! === ``` ## Key Takeaways 1. **Use deterministic tools where they excel** - Prettier for formatting, ast-grep for structural rewrites, Babel for scope-aware renaming 2. **Reserve the LLM for semantic understanding** - only steps 3 and 4 (function extraction and naming) actually need AI 3. **Parallel processing with `p-limit`** keeps LLM-powered steps efficient without exhausting account limits 4. **Batch schemas like `FunctionAnalysisBatch`** reduce round trips for repeated LLM operations 5. **The `.code()` method** clearly delineates code blocks in prompts 6. **Deterministic steps are faster and more reliable** - the ast-grep rewrite runs in \~130ms vs \~200s for LLM regeneration # Meeting Scheduler Source: https://thinkwell.sh/examples/meeting-scheduler Let the AI understand language while a solver finds the answer. Scheduling a meeting sounds simple until you try it. Each person describes their availability differently — "mornings work for me," "not before 11," "only Wednesday and Thursday." A human can read these and understand them, but finding a time that actually works for everyone is a separate problem: a search through all the possible combinations. This example splits that work between two systems that are each good at their part: 1. **An AI agent** reads natural-language availability and translates it into structured data 2. **A constraint solver** takes that structured data and finds a time slot that satisfies everyone — or reports that no such time exists The result is a script that takes something like this: ```json theme={null} [ { "name": "Alice", "availability": "Free Monday, Wednesday, and Friday mornings before noon" }, { "name": "Bob", "availability": "Available Tuesday through Thursday, any time after 10am" }, { "name": "Carol", "availability": "Only free Wednesday and Thursday, 10am to 2pm" }, { "name": "Dave", "availability": "Open Monday through Friday but not before 11am or after 3pm" } ] ``` …and produces a meeting time that works for all four people, along with a friendly summary. ## Why Two Systems? AI agents are great at understanding language. If someone says "mornings before noon," an agent can figure out that means 9am–12pm on business days. But agents aren't reliable at combinatorial search — finding the intersection of four people's overlapping windows across five days and eight hours. They might get it right, but they might not, and you'd have no way to be sure. Constraint solvers work the other way around. They're designed to search through combinations exhaustively and either find a valid solution or prove that none exists. But they need precise, structured input — they can't read "I'm free Tuesday afternoons." By combining both, you get the best of each: the agent handles language understanding, and the solver handles the search. The answer is guaranteed to be correct if the agent parsed the availability correctly. ## The Data Types First, define the structured types that bridge the two systems. These use Thinkwell's `@JSONSchema` annotation so the AI agent's output is automatically validated against them. A `TimeWindow` represents a single block of availability: ```typescript theme={null} /** * A time window when an attendee is available. * @JSONSchema */ export interface TimeWindow { /** Day of the week */ day: "monday" | "tuesday" | "wednesday" | "thursday" | "friday"; /** * Earliest hour (inclusive, 24-hour format, e.g. 9 = 9am) * @minimum 0 * @maximum 23 */ earliestHour: number; /** * Latest hour (exclusive, 24-hour format, e.g. 17 = 5pm) * @minimum 1 * @maximum 24 */ latestHour: number; } ``` `ParsedConstraints` collects each attendee's windows into a single structure: ```typescript theme={null} /** * Parsed availability for one attendee. * @JSONSchema */ export interface AttendeeConstraints { /** The attendee's name */ name: string; /** Time windows when this attendee is available to meet */ available: TimeWindow[]; } /** * All attendees' parsed availability constraints. * @JSONSchema */ export interface ParsedConstraints { /** Parsed constraints for each attendee */ attendees: AttendeeConstraints[]; } ``` And a `ScheduleResult` for the final human-friendly output: ```typescript theme={null} /** * The final scheduling result. * @JSONSchema */ export interface ScheduleResult { /** Whether a valid meeting time was found */ scheduled: boolean; /** A friendly summary of the result */ summary: string; } ``` Notice how the `@minimum` and `@maximum` JSDoc tags on `TimeWindow` add numeric constraints to the schema. This helps the agent stay within valid ranges — hours should be between 0 and 24, not arbitrary numbers. ## Step 1: Parse Availability with the Agent Load the attendee data and ask the agent to translate each person's natural-language availability into structured `TimeWindow` objects: ```typescript theme={null} const raw = await fs.readFile( new URL("attendees.json", import.meta.url), "utf-8", ); const attendees: { name: string; availability: string }[] = JSON.parse(raw); const attendeeList = attendees .map((a) => `- ${a.name}: ${a.availability}`) .join("\n"); const parsed = await agent .think(ParsedConstraints.Schema) .text(` Parse each attendee's natural-language availability into structured time windows. Each window specifies a day, earliest hour (inclusive), and latest hour (exclusive) in 24-hour format. Business hours are 9am (9) to 5pm (17). If someone says "mornings", interpret that as 9-12. If they say "afternoons", interpret as 12-17. If no time range is specified for a day, assume full business hours (9-17). Attendees: `) .text(attendeeList) .run(); ``` The prompt provides clear rules for ambiguous terms — what "mornings" means, what the default hours are — so the agent produces consistent results. The `ParsedConstraints.Schema` ensures the output matches the expected shape exactly. For example, Alice's "Free Monday, Wednesday, and Friday mornings before noon" becomes: ```json theme={null} [ { "day": "monday", "earliestHour": 9, "latestHour": 12 }, { "day": "wednesday", "earliestHour": 9, "latestHour": 12 }, { "day": "friday", "earliestHour": 9, "latestHour": 12 } ] ``` ## Step 2: Find a Valid Time with the Solver Now comes the part where a solver really shines. We need to find a single `(day, hour)` pair that falls within at least one availability window for every attendee. With four people and multiple windows each, there are a lot of combinations to check. The [`z3-solver`](https://www.npmjs.com/package/z3-solver) npm package lets you describe what a valid answer looks like, and it searches for one automatically: ```typescript theme={null} import { init } from "z3-solver"; async function solve( constraints: ParsedConstraints ): Promise<{ day: number; hour: number } | null> { const { Context } = await init(); const { Solver, Int, And, Or, isIntVal } = new Context("main"); const day = Int.const("day"); const hour = Int.const("hour"); const solver = new Solver(); solver.set("timeout", 10000); // The meeting must be on a weekday during business hours solver.add(day.ge(0), day.le(4)); solver.add(hour.ge(9), hour.le(17)); // For each attendee, the meeting must fall in one of their windows for (const attendee of constraints.attendees) { if (attendee.available.length === 0) continue; const windows = attendee.available.map((w) => { const dayIndex = DAY_NAMES.indexOf(w.day); return And( day.eq(dayIndex), hour.ge(w.earliestHour), hour.lt(w.latestHour), ); }); solver.add(windows.length === 1 ? windows[0] : Or(...windows)); } const result = await solver.check(); if (result === "sat") { const model = solver.model(); const dayVal = model.eval(day); const hourVal = model.eval(hour); if (isIntVal(dayVal) && isIntVal(hourVal)) { return { day: Number(dayVal.value()), hour: Number(hourVal.value()) }; } } return null; } ``` Here's what's happening: * **`day` and `hour`** are variables — the solver will figure out their values * **`solver.add(...)`** tells the solver what has to be true: the day must be a weekday (0–4), the hour must be within business hours (9–17), and each attendee's windows must include the chosen time * **`solver.check()`** does the actual search — if it returns `"sat"` (satisfiable), a valid time exists and we can read it from the model The key idea is that you describe the *rules*, not the *search strategy*. The solver handles the search. And if no valid time exists — say Carol is only free Wednesday but Alice can't do Wednesdays — it tells you that too, rather than giving a wrong answer. ## Step 3: Summarize the Result Finally, ask the agent to produce a friendly summary: ```typescript theme={null} const solverOutput = solution ? `Found a valid time: ${DAY_LABELS[solution.day]} at ${solution.hour}:00` : "No valid meeting time exists that satisfies all constraints."; const result = await agent .think(ScheduleResult.Schema) .text(` Summarize this meeting scheduling result in a friendly, concise way. Mention the attendees by name and the chosen time (or explain the conflict). Attendees: ${attendees.map((a) => a.name).join(", ")} Solver result: ${solverOutput} `) .run(); console.log(result.summary); ``` This is a nice example of using the agent for what it's good at — turning structured data into natural language — while keeping it out of the parts that need to be exact. ## The Full Script Here's everything together: ```typescript theme={null} #!/usr/bin/env thinkwell import { open } from "thinkwell"; import { init } from "z3-solver"; import * as fs from "fs/promises"; // --- Type Definitions --- /** * A time window when an attendee is available. * @JSONSchema */ export interface TimeWindow { /** Day of the week */ day: "monday" | "tuesday" | "wednesday" | "thursday" | "friday"; /** * Earliest hour (inclusive, 24-hour format, e.g. 9 = 9am) * @minimum 0 * @maximum 23 */ earliestHour: number; /** * Latest hour (exclusive, 24-hour format, e.g. 17 = 5pm) * @minimum 1 * @maximum 24 */ latestHour: number; } /** * Parsed availability for one attendee. * @JSONSchema */ export interface AttendeeConstraints { /** The attendee's name */ name: string; /** Time windows when this attendee is available to meet */ available: TimeWindow[]; } /** * All attendees' parsed availability constraints. * @JSONSchema */ export interface ParsedConstraints { /** Parsed constraints for each attendee */ attendees: AttendeeConstraints[]; } /** * The final scheduling result. * @JSONSchema */ export interface ScheduleResult { /** Whether a valid meeting time was found */ scheduled: boolean; /** A friendly summary of the result */ summary: string; } // --- Constraint Solver --- const DAY_NAMES = ["monday", "tuesday", "wednesday", "thursday", "friday"] as const; const DAY_LABELS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]; async function solve( constraints: ParsedConstraints ): Promise<{ day: number; hour: number } | null> { const { Context } = await init(); const { Solver, Int, And, Or, isIntVal } = new Context("main"); const day = Int.const("day"); const hour = Int.const("hour"); const solver = new Solver(); solver.set("timeout", 10000); solver.add(day.ge(0), day.le(4)); solver.add(hour.ge(9), hour.le(17)); for (const attendee of constraints.attendees) { if (attendee.available.length === 0) continue; const windows = attendee.available.map((w) => { const dayIndex = DAY_NAMES.indexOf(w.day); return And( day.eq(dayIndex), hour.ge(w.earliestHour), hour.lt(w.latestHour), ); }); solver.add(windows.length === 1 ? windows[0] : Or(...windows)); } const result = await solver.check(); if (result === "sat") { const model = solver.model(); const dayVal = model.eval(day); const hourVal = model.eval(hour); if (isIntVal(dayVal) && isIntVal(hourVal)) { return { day: Number(dayVal.value()), hour: Number(hourVal.value()) }; } } return null; } // --- Main --- async function main() { const agent = await open("claude"); try { console.log("=== Meeting Scheduler ===\n"); const raw = await fs.readFile( new URL("attendees.json", import.meta.url), "utf-8", ); const attendees: { name: string; availability: string }[] = JSON.parse(raw); console.log("Attendees:"); for (const a of attendees) { console.log(` - ${a.name}: "${a.availability}"`); } console.log(); console.log("Parsing availability constraints..."); const attendeeList = attendees .map((a) => `- ${a.name}: ${a.availability}`) .join("\n"); const parsed = await agent .think(ParsedConstraints.Schema) .text(` Parse each attendee's natural-language availability into structured time windows. Each window specifies a day, earliest hour (inclusive), and latest hour (exclusive) in 24-hour format. Business hours are 9am (9) to 5pm (17). If someone says "mornings", interpret that as 9-12. If they say "afternoons", interpret as 12-17. If no time range is specified for a day, assume full business hours (9-17). Attendees: `) .text(attendeeList) .run(); for (const a of parsed.attendees) { const windows = a.available .map((w) => `${w.day} ${w.earliestHour}:00-${w.latestHour}:00`) .join(", "); console.log(` ${a.name}: ${windows}`); } console.log(); console.log("Solving constraints..."); const solution = await solve(parsed); const solverOutput = solution ? `Found a valid time: ${DAY_LABELS[solution.day]} at ${solution.hour}:00` : "No valid meeting time exists that satisfies all constraints."; console.log(` ${solverOutput}\n`); const result = await agent .think(ScheduleResult.Schema) .text(` Summarize this meeting scheduling result in a friendly, concise way. Mention the attendees by name and the chosen time (or explain the conflict). Attendees: ${attendees.map((a) => a.name).join(", ")} Solver result: ${solverOutput} `) .run(); console.log("--- Result ---\n"); console.log(result.summary); } finally { await agent.close(); } } main(); ``` ## Running the Example ```bash theme={null} thinkwell src/schedule.ts ``` Sample output: ``` === Meeting Scheduler === Attendees: - Alice: "Free Monday, Wednesday, and Friday mornings before noon" - Bob: "Available Tuesday through Thursday, any time after 10am" - Carol: "Only free Wednesday and Thursday, 10am to 2pm" - Dave: "Open Monday through Friday but not before 11am or after 3pm" Parsing availability constraints... Alice: monday 9:00-12:00, wednesday 9:00-12:00, friday 9:00-12:00 Bob: tuesday 10:00-17:00, wednesday 10:00-17:00, thursday 10:00-17:00 Carol: wednesday 10:00-14:00, thursday 10:00-14:00 Dave: monday 11:00-15:00, tuesday 11:00-15:00, wednesday 11:00-15:00, thursday 11:00-15:00, friday 11:00-15:00 Solving constraints... Found a valid time: Wednesday at 11:00 --- Result --- Great news! Alice, Bob, Carol, and Dave can all meet on Wednesday at 11:00 AM. ``` ## Key Takeaways 1. **Use AI for language, solvers for search.** The agent is great at understanding what "mornings before noon" means. The solver is great at finding the intersection of everyone's availability. Neither is great at the other's job. 2. **Structured types are the bridge.** The `@JSONSchema` types define a clear contract between the two systems. The agent fills in the structure; the solver reads it. 3. **Guaranteed correctness where it matters.** The solver doesn't guess — it either finds a valid time or proves none exists. This is a stronger guarantee than asking an agent to "figure out when everyone is free." 4. **The pattern generalizes.** Any problem where you need to understand fuzzy human input and then find an exact answer is a good fit for this approach: resource allocation, timetabling, configuration, logistics, and more. # Sentiment Analysis Source: https://thinkwell.sh/examples/sentiment-analysis Use npm packages in tools. This example demonstrates how to wrap npm packages as tools for an LLM agent. We'll use the popular [`sentiment`](https://www.npmjs.com/package/sentiment) package to give Claude the ability to perform quantitative sentiment analysis on text passages. ## The Complete Example ```typescript theme={null} import { open } from "thinkwell"; import * as fs from "fs/promises"; import Sentiment from "sentiment"; /** * A section of a document with its sentiment analysis. * @JSONSchema */ export interface DocumentSection { /** The section title */ title: string; /** The sentiment score from the analysis tool */ sentimentScore: number; /** A brief summary of the section */ summary: string; } /** * Analysis of a document's sentiment and content. * @JSONSchema */ export interface DocumentAnalysis { /** The overall emotional tone of the document */ overallTone: "positive" | "negative" | "mixed" | "neutral"; /** Analysis of each section */ sections: DocumentSection[]; /** A recommendation based on the analysis */ recommendation: string; } /** * A text passage to analyze. * @JSONSchema */ export interface TextPassage { /** The text passage to analyze */ text: string; } // Initialize the sentiment analyzer (from the `sentiment` npm package) const sentimentAnalyzer = new Sentiment(); async function main() { const agent = await open('claude'); try { const feedback = await fs.readFile( new URL("feedback.txt", import.meta.url), "utf-8" ); const analysis = await agent .think(DocumentAnalysis.Schema) .text(` Analyze the following customer feedback document. Use the sentiment analysis tool to measure the emotional tone of each section, then provide an overall analysis with recommendations. `) .quote(feedback, "feedback") // Custom tool: wraps the `sentiment` npm package as an MCP tool .tool( "analyze_sentiment", "Analyze the sentiment of a text passage.", TextPassage.Schema, async (passage) => { const result = sentimentAnalyzer.analyze(passage.text); return { score: result.score, comparative: result.comparative, positive: result.positive, negative: result.negative, }; } ) .run(); console.log(`Overall Tone: ${analysis.overallTone}`); console.log(`Recommendation: ${analysis.recommendation}`); } finally { agent.close(); } } main(); ``` Run this example with: ```bash theme={null} npx thinkwell sentiment.ts ``` ## Using npm Packages as Tools The key insight here is that any npm package can be wrapped as a tool. The `sentiment` package provides AFINN-based sentiment analysis, but the LLM doesn't know how to use it directly. By wrapping it in a tool, we give the agent the ability to call this deterministic algorithm whenever it needs precise sentiment measurements. ```typescript theme={null} import Sentiment from "sentiment"; const sentimentAnalyzer = new Sentiment(); // Later, in the agent call: .tool( "analyze_sentiment", "Analyze the sentiment of a text passage.", TextPassage.Schema, async (passage) => { const result = sentimentAnalyzer.analyze(passage.text); return { score: result.score, comparative: result.comparative, positive: result.positive, negative: result.negative, }; } ) ``` The tool receives a `TextPassage` object (validated against the schema) and returns the raw sentiment analysis result. The agent can then interpret these numbers in context. ## Typed Tool Inputs with Schemas Tools can accept structured input by providing a schema as the third argument. Here we define a `TextPassage` interface with the `@JSONSchema` decorator: ```typescript theme={null} /** * A text passage to analyze. * @JSONSchema */ export interface TextPassage { /** The text passage to analyze */ text: string; } ``` When you pass `TextPassage.Schema` to the `.tool()` method, Thinkwell: 1. Exposes the schema to the LLM so it knows how to format tool calls 2. Validates incoming tool calls against the schema 3. Provides full TypeScript typing for the handler function's `passage` parameter This pattern ensures that tool inputs are always well-formed, and your handler code gets proper type checking. ## Including Document Content with `.quote()` The `.quote()` method adds content to the prompt in a clearly delimited format. This is ideal for including documents, user input, or any content that should be treated as data rather than instructions. ```typescript theme={null} .quote(feedback, "feedback") ``` The second argument is an optional label that helps the LLM understand what the quoted content represents. In the prompt, this renders as a clearly marked block that the agent can reference. Without a label: ```typescript theme={null} .quote(someContent) ``` With a label for clarity: ```typescript theme={null} .quote(feedback, "customer-feedback") .quote(policy, "company-policy-document") ``` ## Complex Schema Patterns This example showcases several schema features working together: ### Nested Types The `DocumentAnalysis` schema contains an array of `DocumentSection` objects: ```typescript theme={null} /** * A section of a document with its sentiment analysis. * @JSONSchema */ export interface DocumentSection { title: string; sentimentScore: number; summary: string; } /** * Analysis of a document's sentiment and content. * @JSONSchema */ export interface DocumentAnalysis { overallTone: "positive" | "negative" | "mixed" | "neutral"; sections: DocumentSection[]; recommendation: string; } ``` Thinkwell automatically resolves references between schemas when generating the JSON Schema for the LLM. ### String Literal Unions (Enums) TypeScript string literal unions become enum constraints in the generated schema: ```typescript theme={null} overallTone: "positive" | "negative" | "mixed" | "neutral"; ``` This ensures the LLM can only return one of these four values, and TypeScript knows the type is constrained to these literals. ### Descriptive JSDoc Comments Every property should have a JSDoc comment explaining its purpose: ```typescript theme={null} /** The sentiment score from the analysis tool */ sentimentScore: number; ``` These descriptions appear in the generated JSON Schema and help the LLM understand what each field should contain. Good descriptions lead to better, more consistent outputs. ## Why Wrap Packages as Tools? LLMs are powerful reasoners but they can struggle with precise calculations or algorithms that require exact execution. By wrapping deterministic packages as tools, you get the best of both worlds: * **The LLM** handles understanding context, breaking down the document into sections, and synthesizing a recommendation * **The npm package** handles the precise, algorithmic work of calculating sentiment scores This pattern applies to many use cases: date parsing, math operations, data validation, API calls, database queries, and more. If there's an npm package that does something well, you can make it available to your agent as a tool. # CLI Reference Source: https://thinkwell.sh/get-started/cli Complete reference for the thinkwell command-line interface. The `thinkwell` CLI is the primary way to run and build Thinkwell agent scripts. ## Usage ```bash theme={null} thinkwell [args...] # Run a TypeScript script thinkwell run [args...] # Explicit run command thinkwell init # Initialize thinkwell in current directory thinkwell new # Create a new project in a new directory thinkwell check # Type-check project (no output files) thinkwell build # Compile project with @JSONSchema support thinkwell bundle # Compile to standalone executable thinkwell --help # Show help message thinkwell --version # Show version ``` ## Running Scripts (Zero-Config) The simplest way to run a Thinkwell script is to pass it directly to the CLI: ```bash theme={null} thinkwell my-agent.ts ``` This runs your TypeScript file with native TypeScript support—no compilation step required. The CLI automatically processes `@JSONSchema` annotations. You can pass arguments to your script after the filename: ```bash theme={null} thinkwell my-agent.ts --input data.json --verbose ``` ### The `run` Subcommand The explicit `run` subcommand is equivalent to passing a script directly: ```bash theme={null} thinkwell run my-agent.ts --input data.json ``` ## Project Setup ### `thinkwell init` Initialize thinkwell in an existing directory: ```bash theme={null} thinkwell init ``` This command: 1. Creates `package.json` if none exists 2. Adds `thinkwell` and `typescript` dependencies using the detected package manager (pnpm, yarn, or npm) Options: * `--yes, -y` - Proceed without prompting for confirmation (CI-friendly) ### `thinkwell new` Create a new project in a new directory: ```bash theme={null} thinkwell new my-agent ``` This creates a new directory with: * `package.json` with thinkwell dependency * `tsconfig.json` for TypeScript * `src/main.ts` with example agent code * `.gitignore` * `.env.example` ## Type Checking ### `thinkwell check` Check a project for type errors: ```bash theme={null} thinkwell check ``` This will essentially perform the same type checking as `thinkwell build` without writing any output files, which is faster for catching errors during development. In a workspace (pnpm or npm), all TypeScript packages are checked by default. | Option | Description | | ---------------------- | ---------------------------------------------------- | | `-p, --package ` | Check a specific workspace package (can be repeated) | | `--pretty` | Enable colorized output (default: true if TTY) | | `--no-pretty` | Disable colorized output | Exit codes: * `0` - No type errors * `1` - Type errors found * `2` - Configuration error ### Examples ```bash theme={null} # Check the current project thinkwell check # Check a specific workspace package thinkwell check -p acp # Check multiple packages thinkwell check -p acp -p protocol # Disable colorized output (for CI) thinkwell check --no-pretty ``` ## Building Projects ### `thinkwell build` Compile your TypeScript project using the standard TypeScript compiler with `@JSONSchema` namespace injection: ```bash theme={null} thinkwell build ``` This command compiles your project according to your `tsconfig.json`, applying `@JSONSchema` transformations first. Output (`.js`, `.d.ts`, source maps) is written to your configured `outDir`. | Option | Description | | ---------------------- | -------------------------------------------------- | | `-w, --watch` | Watch for file changes and recompile | | `-p, --project ` | Path to tsconfig.json (default: `./tsconfig.json`) | | `-q, --quiet` | Suppress all output except errors | | `--verbose` | Show detailed build output | ### Examples ```bash theme={null} # Build the project thinkwell build # Watch and rebuild on changes thinkwell build --watch # Use a specific tsconfig thinkwell build -p tsconfig.app.json # Suppress success output (for CI) thinkwell build --quiet ``` ### Configuration via package.json Control which files receive `@JSONSchema` transformation: ```json theme={null} { "thinkwell": { "build": { "include": ["src/**/*.ts"], "exclude": ["**/*.test.ts", "**/__fixtures__/**"] } } } ``` Files not matched by `include` (or matched by `exclude`) are still compiled by TypeScript—they just skip `@JSONSchema` transformation. ## Bundling Executables ### `thinkwell bundle` Compile your script into a standalone executable that can run without Node.js or Thinkwell installed: ```bash theme={null} thinkwell bundle src/agent.ts ``` The resulting binary includes: * Node.js 24 runtime with native TypeScript support * All thinkwell packages * Your bundled application code ### Bundle Options | Option | Description | | ----------------------- | ------------------------------------------------- | | `-o, --output ` | Output file path (default: `./-`) | | `-t, --target ` | Target platform (can be specified multiple times) | | `--include ` | Additional files to embed as assets | | `-e, --external ` | Exclude package from bundling (can be repeated) | | `-m, --minify` | Minify the bundled code for smaller output | | `-w, --watch` | Watch for changes and rebuild automatically | | `-n, --dry-run` | Show what would be built without building | | `-q, --quiet` | Suppress all output except errors (for CI) | | `-v, --verbose` | Show detailed build output | ### Target Platforms | Target | Description | | -------------- | -------------------------- | | `host` | Current platform (default) | | `darwin-arm64` | macOS on Apple Silicon | | `darwin-x64` | macOS on Intel | | `linux-x64` | Linux on x64 | | `linux-arm64` | Linux on ARM64 | ### Examples ```bash theme={null} # Bundle for current platform thinkwell bundle src/agent.ts # Specify output path thinkwell bundle src/agent.ts -o dist/my-agent # Bundle for Linux thinkwell bundle src/agent.ts --target linux-x64 # Multi-platform build thinkwell bundle src/agent.ts -t darwin-arm64 -t linux-x64 # Preview build without executing thinkwell bundle src/agent.ts --dry-run # Keep sqlite3 as external dependency thinkwell bundle src/agent.ts -e sqlite3 # Minify for smaller binary thinkwell bundle src/agent.ts --minify # Watch mode for development thinkwell bundle src/agent.ts --watch ``` ### Configuration via package.json Set bundle defaults in your `package.json`: ```json theme={null} { "thinkwell": { "bundle": { "output": "dist/my-agent", "targets": ["darwin-arm64", "linux-x64"], "external": ["sqlite3"], "minify": true } } } ``` CLI options override `package.json` settings. Binaries are approximately 70-90 MB due to the embedded Node.js runtime. The `--minify` flag reduces bundle size, though the Node.js runtime dominates the total size. ## Environment Variables | Variable | Description | | --------------------- | ----------------------------------------------------------------------- | | `THINKWELL_AGENT` | Override the agent name (see [supported names](#supported-agent-names)) | | `THINKWELL_AGENT_CMD` | Override the agent command (e.g., `myagent --acp`) | | `THINKWELL_CACHE_DIR` | Override the cache directory (default: `~/.cache/thinkwell`) | | `DEBUG` | Enable debug output for troubleshooting | The agent environment variables let you swap agents at runtime without modifying code. For example, a script that calls `open('claude')` can be run with a different agent: ```bash theme={null} # Run with OpenCode instead of Claude THINKWELL_AGENT=opencode thinkwell my-script.ts # Run with a custom agent command THINKWELL_AGENT_CMD="myagent --acp" thinkwell my-script.ts ``` If both are set, `THINKWELL_AGENT_CMD` takes precedence. ### Supported Agent Names | Name | Command | | ---------- | ---------------------------------------------- | | `claude` | `npx -y @agentclientprotocol/claude-agent-acp` | | `codex` | `npx -y @zed-industries/codex-acp` | | `gemini` | `npx -y @google/gemini-cli --experimental-acp` | | `kiro` | `kiro-cli acp` | | `opencode` | `opencode acp` | | `auggie` | `auggie --acp` | # Writing with Coding Agents Source: https://thinkwell.sh/get-started/coding-agents Use coding agents like Claude Code, Cursor, and Codex to write Thinkwell scripts. # Writing Thinkwell Scripts with Coding Agents Thinkwell ships an [Agent Skill](https://agentskills.io/) that teaches coding agents how to write Thinkwell code. Once installed, agents like Claude Code, Cursor, Codex, and [45+ others](https://agentskills.io/) can use the `@JSONSchema` pattern, Plan API, and CLI correctly without you having to explain the framework from scratch. ## Install the Skill Run this once in your project directory: ```bash theme={null} npx skills add dherman/thinkwell --skill thinkwell ``` This downloads the Thinkwell skill into your project's `.agents/skills/` directory, where compatible coding agents automatically discover it. ## Example: Asking an Agent to Write a Script With the skill installed, you can ask your coding agent to write Thinkwell scripts directly. Here's an example prompt you might give: > Write a Thinkwell script that reads a TypeScript file, analyzes it for code smells, and returns structured results with a severity level for each issue found. A coding agent with the Thinkwell skill would produce something like this: ```typescript theme={null} import { open } from "thinkwell"; import { readFile } from "fs/promises"; /** * A code quality issue found in the source. * @JSONSchema */ export interface CodeSmell { /** Short name of the issue (e.g., "long-method", "deep-nesting") */ name: string; /** Human-readable description of the problem */ description: string; /** Line number where the issue starts */ line: number; /** How severe this issue is */ severity: "info" | "warning" | "error"; /** Suggested fix */ suggestion: string; } /** * Analysis results for a source file. * @JSONSchema */ export interface AnalysisResult { /** Overall quality score from 1-10 */ score: number; /** Issues found in the code */ issues: CodeSmell[]; /** Brief summary of the analysis */ summary: string; } async function main() { const filePath = process.argv[2]; if (!filePath) { console.error("Usage: thinkwell analyze.ts "); process.exit(1); } const source = await readFile(filePath, "utf-8"); const agent = await open("claude"); try { const result = await agent .think(AnalysisResult.Schema) .text("Analyze the following TypeScript file for code smells and quality issues.") .code(source, "typescript") .run(); console.log(`Quality score: ${result.score}/10`); console.log(`Summary: ${result.summary}\n`); for (const issue of result.issues) { console.log(`[${issue.severity.toUpperCase()}] Line ${issue.line}: ${issue.name}`); console.log(` ${issue.description}`); console.log(` Fix: ${issue.suggestion}\n`); } } finally { agent.close(); } } main(); ``` Run it with: ```bash theme={null} thinkwell analyze.ts src/utils.ts ``` ## Tips for Good Results **Be specific about your schema.** Instead of "analyze this code," describe the fields you want back: "return a score, a list of issues with severity levels, and a summary." **Mention tools if you need them.** If your script should call external APIs or read files, tell the agent: "Add a tool that fetches the latest npm package version." **Start simple.** Ask for a basic script first, then iterate. "Add streaming output" or "add a session so it can ask follow-up questions" are easy follow-ups once the core script works. **Use the CLI.** Remind the agent that Thinkwell scripts run with `thinkwell script.ts` — no build step needed. # Introduction Source: https://thinkwell.sh/get-started/introduction Get started with Thinkwell. # What is Thinkwell? Thinkwell is a TypeScript framework built to **make scripting AI agents easy**. With Thinkwell, you can write scripts that seamlessly combine traditional programming logic with the creative problem-solving capabilities of large language models. Your code stays in control while the AI handles the parts that benefit from reasoning and natural language understanding. ## How It Works Thinkwell uses the [Agent Client Protocol (ACP)](https://agentclientprotocol.com) to connect to AI agents like Claude Code, Codex, Gemini CLI, and others. This means you write one script and can run it against different AI backends. Here's a simple example: ```typescript theme={null} import { open } from "thinkwell"; /** @JSONSchema */ interface Analysis { summary: string; sentiment: "positive" | "negative" | "neutral"; } const agent = await open('claude'); const result = await agent .think(Analysis.Schema) .text("Analyze the following customer feedback:") .quote(customerFeedback) .run(); console.log(`Sentiment: ${result.sentiment}`); console.log(`Summary: ${result.summary}`); agent.close(); ``` ## Why Thinkwell? **Zero-config.** Run TypeScript files directly with `thinkwell script.ts`. Schemas are generated on the fly. **Tools that just work.** Give the agent access to your functions with a simple decorator pattern. Thinkwell handles the protocol details. **Agent-agnostic.** Write your logic once, then swap between Claude Code, Codex, or other ACP-compatible agents without changing your code. **Type safety from end to end.** Define your expected outputs as TypeScript interfaces and get compile-time checking plus runtime validation. No more hand-crafting MCP schemas or parsing prompt results. ## Next Steps Ready to dive in? Head to the [Quick Start](/get-started/quickstart) guide to set up your first Thinkwell project. # Quick Start Source: https://thinkwell.sh/get-started/quickstart Quick instructions to get started with Thinkwell. This guide will walk you through creating your first Thinkwell agent in just a few minutes. ## Installation with Homebrew (Recommended) Homebrew is the simplest way to install Thinkwell system-wide: ```bash theme={null} brew install dherman/thinkwell/thinkwell ``` ## Manual Installation ```bash theme={null} mkdir -p ~/.local/bin curl -L https://github.com/dherman/thinkwell/releases/latest/download/thinkwell-darwin-arm64.tar.gz | tar -xz -C ~/.local/bin mv ~/.local/bin/thinkwell-darwin-arm64 ~/.local/bin/thinkwell ``` ```bash theme={null} mkdir -p ~/.local/bin curl -L https://github.com/dherman/thinkwell/releases/latest/download/thinkwell-darwin-x64.tar.gz | tar -xz -C ~/.local/bin mv ~/.local/bin/thinkwell-darwin-x64 ~/.local/bin/thinkwell ``` ```bash theme={null} mkdir -p ~/.local/bin curl -L https://github.com/dherman/thinkwell/releases/latest/download/thinkwell-linux-arm64.tar.gz | tar -xz -C ~/.local/bin mv ~/.local/bin/thinkwell-linux-arm64 ~/.local/bin/thinkwell ``` ```bash theme={null} mkdir -p ~/.local/bin curl -L https://github.com/dherman/thinkwell/releases/latest/download/thinkwell-linux-x64.tar.gz | tar -xz -C ~/.local/bin mv ~/.local/bin/thinkwell-linux-x64 ~/.local/bin/thinkwell ``` ## Zero-Install (Node.js) If you don't want to install anything, you can use Thinkwell via `npx`: ```bash theme={null} npx -y thinkwell --help ``` ## Create Your First Agent Create a new TypeScript file called `greeting.ts`: ```typescript theme={null} import { open } from "thinkwell"; /** * A friendly greeting. * @JSONSchema */ export interface Greeting { /** The greeting message */ message: string; } async function main() { const agent = await open('claude'); try { const greeting = await agent .think(Greeting.Schema) .text("Create a friendly greeting message.") .run(); console.log(greeting.message); } finally { agent.close(); } } main(); ``` ## Run Your Script The Thinkwell CLI can run TypeScript files directly without any compilation step: ```bash theme={null} thinkwell greeting.ts ``` You should see a friendly greeting message printed to your console. ## Understanding the Code Let's break down the key concepts: ### Imports Thinkwell provides a standard npm package import: * `thinkwell` - The main package, providing `open()`, `Agent`, and other APIs ### The @JSONSchema Pattern The `@JSONSchema` JSDoc tag is central to how Thinkwell works. When you annotate an interface with `@JSONSchema`, Thinkwell automatically generates a JSON Schema and attaches it as a static `Schema` property: ```typescript theme={null} /** * A friendly greeting. * @JSONSchema */ export interface Greeting { /** The greeting message */ message: string; } ``` This allows you to use `Greeting.Schema` when calling `agent.think()`, which tells the agent what structured output to produce. The JSDoc comments on properties become descriptions in the generated schema, helping the AI understand what each field should contain. ### Agent Lifecycle 1. **Open** - `open('claude')` connects to the AI backend by name 2. **Think** - `agent.think(Schema).text(prompt).run()` sends your prompt and returns structured output matching your schema 3. **Close** - Always close the agent when done to clean up resources ## Next Steps Now that you have a basic agent working, explore: * [API Overview](/api/overview) - Learn about schemas, tools, and the Plan API * [Sessions](/api/sessions) - Create multi-turn conversations with persistent context