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")
Tools allow the AI agent to call back into your code during prompt execution.
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,
})
)
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:
- Tells the AI how to format tool calls
- Validates incoming calls at runtime
- 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.
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 }
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".
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
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:
.think(schema) - Always first (starts the builder)
.cwd(path) - Configuration
.skill() - Skill attachments
.text() / .textln() / .quote() / .code() - Main prompt instructions
.tool() - Tool definitions
.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