Skip to main content
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

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.
.text("Analyze the following code for potential bugs.")

.textln(content)

Adds text with a trailing newline. Useful when building prompts incrementally.
.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.
// 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.
// 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.
.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:
/** @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. 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.
.skill("./skills/code-review")
The file is parsed at run() time. The skill directory must contain a SKILL.md with YAML frontmatter:
---
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.
.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:
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.
.cwd("/path/to/project")

Execution

.run()

Executes the prompt and returns the typed result. This is always the final method in the chain.
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.
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<Output> class provides access to streaming events during prompt execution.

Properties

.result

A Promise<Output> that resolves with the final typed result.
const stream = agent.think(schema).text("...").stream();
const result = await stream.result;

Async Iteration

ThoughtStream implements AsyncIterable<ThoughtEvent>, allowing you to iterate over events with for await:
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
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.
{ type: "thought"; text: string }

message

Streaming visible response text from the agent.
{ type: "message"; text: string }

tool_start

Emitted when the agent starts using a tool.
{ 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.
{ 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.
{ type: "tool_done"; id: string; status: "completed" | "failed" }

plan

Emitted when the agent shares its execution plan.
{ 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:
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)
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