# Chain Runner Architecture & Tool Calling System

This directory contains the refactored chain runner system for Obsidian Copilot, providing multiple chain execution strategies with different tool calling approaches.

## Overview

The chain runner system provides two distinct tool calling approaches:

1. **Copilot Plus** (CopilotPlusChainRunner) - Uses native tool calling for intent analysis
2. **Autonomous Agent** (AutonomousAgentChainRunner) - Uses native LangChain tool calling with ReAct pattern

## Architecture

```
chainRunner/
├── BaseChainRunner.ts                 # Abstract base class with shared functionality
├── LLMChainRunner.ts                  # Basic LLM interaction (no tools)
├── VaultQAChainRunner.ts              # Vault-only Q&A with retrieval
├── CopilotPlusChainRunner.ts          # Legacy tool calling system
├── ProjectChainRunner.ts              # Project-aware extension of Plus
├── AutonomousAgentChainRunner.ts   # Native tool calling with ReAct agent loop
├── index.ts                           # Main exports
└── utils/
    ├── ThinkBlockStreamer.ts          # Handles thinking content from models
    ├── xmlParsing.ts                  # XML escape/unescape utilities (for context envelope)
    ├── toolExecution.ts               # Tool execution helpers
    └── modelAdapter.ts                # Model-specific adaptations
```

## Tool Calling Systems Comparison

### 1. Model-Based Tool Planning (CopilotPlusChainRunner)

**How it works:**

- Uses chat model with `bindTools()` to plan which tools to call
- Model outputs tool calls via native `tool_calls` property on AIMessage
- Executes tools synchronously before sending to LLM for final response
- Enhances user message with tool outputs as context
- Supports `@` commands for explicit tool invocation (`@vault`, `@websearch`, `@memory`)

**Flow:**

```
User Message → Model Planning → Tool Execution → Enhanced Prompt → LLM Response
```

**Example:**

```typescript
// 1. Plan tools using model
const { toolCalls, salientTerms } = await this.planToolCalls(message, chatModel);

// 2. Process @commands (add localSearch, webSearch, etc. if needed)
toolCalls = await this.processAtCommands(message, toolCalls, { salientTerms });

// 3. Execute tools
const toolOutputs = await this.executeToolCalls(toolCalls);

// 4. Send to LLM
const response = await this.streamMultimodalResponse(message, toolOutputs, ...);
```

**Tools Available:**

- `localSearch` - Search vault content
- `webSearch` - Search the web
- `getCurrentTime` - Get current time
- `getFileTree` - Get file structure
- `pomodoroTool` - Pomodoro timer
- `youtubeTranscription` - YouTube video transcription

### 2. Autonomous Agent (AutonomousAgentChainRunner)

**How it works:**

- Uses native LangChain tool calling via `bindTools()` with ReAct pattern
- AI decides autonomously which tools to use via structured `tool_calls`
- Iterative loop where AI can call multiple tools in sequence
- Each tool result informs the next decision via `ToolMessage`

**Flow:**

```
User Message → AI Reasoning → tool_calls → Tool Execution →
ToolMessage → AI Analysis → More Tools? → Final Response
```

**Native Tool Call Format:**

```typescript
// AIMessage.tool_calls contains structured tool calls
const toolCalls = response.tool_calls; // Array of { name, args, id }

// Example tool call:
{
  name: "localSearch",
  args: {
    query: "machine learning notes",
    salientTerms: ["machine", "learning", "AI", "algorithms"]
  },
  id: "call_abc123"
}
```

**ReAct Loop:**

```typescript
// Bind tools to model for native tool calling
const boundModel = chatModel.bindTools(availableTools);

while (iteration < maxIterations) {
  // 1. Get AI response with potential tool calls
  const response = await boundModel.invoke(messages);
  messages.push(response);

  // 2. Check for tool calls in structured format
  if (!response.tool_calls || response.tool_calls.length === 0) {
    // No tools needed - final response
    break;
  }

  // 3. Execute each tool and add ToolMessage
  for (const toolCall of response.tool_calls) {
    const result = await executeSequentialToolCall(toolCall, availableTools);
    messages.push(
      new ToolMessage({
        content: JSON.stringify(result),
        tool_call_id: toolCall.id,
        name: toolCall.name,
      })
    );
  }

  // 4. Continue loop - AI sees tool results via ToolMessage
}
```

### ReAct Prompting Flow

Each iteration sends the following message structure to the LLM:

**Iteration 1 (Initial):**

```
messages = [
  SystemMessage: "You are a helpful assistant... [tool descriptions via bindTools]"
  HumanMessage: "What did I write about machine learning last week?"
]
```

**Iteration 1 Response:**

```
AIMessage: {
  content: "",  // May be empty or contain reasoning
  tool_calls: [{
    id: "call_abc123",
    name: "getTimeRangeMs",
    args: { description: "last week" }
  }]
}
```

**Iteration 2 (After Tool Execution):**

```
messages = [
  SystemMessage: "..."
  HumanMessage: "What did I write about machine learning last week?"
  AIMessage: { tool_calls: [getTimeRangeMs] }
  ToolMessage: { tool_call_id: "call_abc123", content: '{"startTime":1736..., "endTime":1737...}' }
]
```

**Iteration 2 Response:**

```
AIMessage: {
  content: "",
  tool_calls: [{
    id: "call_def456",
    name: "localSearch",
    args: {
      query: "machine learning",
      salientTerms: ["machine learning", "ML", "AI"],
      timeRange: { startTime: 1736..., endTime: 1737... }
    }
  }]
}
```

**Iteration 3 (After Second Tool):**

```
messages = [
  SystemMessage: "..."
  HumanMessage: "What did I write about machine learning last week?"
  AIMessage: { tool_calls: [getTimeRangeMs] }
  ToolMessage: { tool_call_id: "call_abc123", content: '{"startTime":..., "endTime":...}' }
  AIMessage: { tool_calls: [localSearch] }
  ToolMessage: { tool_call_id: "call_def456", content: '{"documents": [...5 results...]}' }
]
```

**Final Response (No tool_calls):**

```
AIMessage: {
  content: "Based on your notes from last week, you wrote about...",
  tool_calls: []  // Empty = final response, exit loop
}
```

### Key Points

1. **Tool schemas** are provided via `bindTools()` - the LLM sees them in its context
2. **AIMessage with tool_calls** triggers tool execution; **AIMessage without tool_calls** is final response
3. **ToolMessage** correlates with AIMessage via `tool_call_id`
4. **Conversation history grows** - each iteration sees all previous messages
5. **Max 4 iterations** to prevent infinite loops

## Key Differences

| Aspect             | Copilot Plus                    | Autonomous Agent                      |
| ------------------ | ------------------------------- | ------------------------------------- |
| **Tool Decision**  | Model-based intent planning     | AI decides autonomously (ReAct)       |
| **Tool Execution** | Pre-LLM, synchronous            | During conversation, iterative        |
| **Tool Format**    | Native tool calling (bindTools) | Native tool calling (bindTools)       |
| **Reasoning**      | Intent analysis → tools         | AI reasoning → tools → more reasoning |
| **Iterations**     | Single pass                     | Up to 4 iterations                    |
| **Tool Chaining**  | Limited                         | Full chaining support                 |

## LangChain Tool Interface

### Overview

Tools are created using native LangChain's `tool()` function via the `createLangChainTool` helper, with Zod schema validation. Tool metadata (execution control, display info) is stored separately in `ToolRegistry`.

```typescript
// Tool creation returns a LangChain StructuredTool
const myTool = createLangChainTool({
  name: string;
  description: string;
  schema: z.ZodType;
  func: (args) => Promise<string | object>;
});

// Tool metadata stored in ToolRegistry
interface ToolMetadata {
  id: string;
  displayName: string;
  description: string;
  category: "search" | "time" | "file" | "media" | "mcp" | "memory" | "custom";
  isAlwaysEnabled?: boolean;
  timeoutMs?: number;
  isBackground?: boolean;
  isPlusOnly?: boolean;
}
```

### Creating Tools

All tools are created using `createLangChainTool` with Zod schemas:

#### Tool with No Parameters

```typescript
const indexTool = createLangChainTool({
  name: "indexVault",
  description: "Index the vault to the Copilot index",
  schema: z.object({}), // Empty object for no parameters
  func: async () => {
    // Tool implementation
    return { status: "complete" };
  },
});

// Register with metadata
registry.register({
  tool: indexTool,
  metadata: {
    id: "indexVault",
    displayName: "Index Vault",
    description: "Index the vault",
    category: "file",
    isBackground: true,
  },
});
```

#### Tool with Parameters

```typescript
// Define schema with validation rules
const searchSchema = z.object({
  query: z.string().min(1).describe("The search query"),
  salientTerms: z.array(z.string()).min(1).describe("Key terms extracted from query"),
  timeRange: z
    .object({
      startTime: z.any(),
      endTime: z.any(),
    })
    .optional()
    .describe("Time range for search"),
});

// Create tool with automatic validation
const searchTool = createLangChainTool({
  name: "localSearch",
  description: "Search for notes based on query and time range",
  schema: searchSchema,
  func: async ({ query, salientTerms, timeRange }) => {
    // Handler receives fully typed and validated arguments
    return performSearch(query, salientTerms, timeRange);
  },
});

// Register with metadata
registry.register({
  tool: searchTool,
  metadata: {
    id: "localSearch",
    displayName: "Vault Search",
    category: "search",
    timeoutMs: 30000,
  },
});
```

### Benefits of LangChain Native Tools

1. **Type Safety**: Full TypeScript type inference from Zod schemas
2. **Runtime Validation**: All inputs validated before reaching handler
3. **Native LangChain Integration**: Compatible with `bindTools()` and LangChain tooling ecosystem
4. **Better Error Messages**: Zod provides detailed validation errors
5. **Separation of Concerns**: Tool implementation separate from execution metadata
6. **Future-Proof**: Ready for native tool calling when models support it

### Advanced Zod Patterns

#### Complex Validation

```typescript
const emailToolSchema = z.object({
  to: z.string().email().describe("Recipient email"),
  subject: z.string().min(1).max(100).describe("Email subject"),
  body: z.string().min(1).describe("Email content"),
  cc: z.array(z.string().email()).optional().describe("CC recipients"),
});
```

#### Union Types for Actions

```typescript
const actionSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("search"),
    query: z.string().min(1),
  }),
  z.object({
    type: z.literal("create"),
    content: z.string().min(1),
    tags: z.array(z.string()).default([]),
  }),
  z.object({
    type: z.literal("delete"),
    id: z.string().uuid(),
  }),
]);

const actionTool = createLangChainTool({
  name: "performAction",
  description: "Perform various actions",
  schema: actionSchema,
  func: async (action) => {
    // TypeScript knows exactly which type based on discriminator
    switch (action.type) {
      case "search":
        return search(action.query);
      case "create":
        return create(action.content, action.tags);
      case "delete":
        return deleteItem(action.id);
    }
  },
});
```

#### Custom Validation

```typescript
const filePathSchema = z
  .string()
  .refine((val) => val.endsWith(".md") || val.endsWith(".canvas"), {
    message: "File must be .md or .canvas",
  })
  .refine((val) => !val.includes(".."), { message: "Path traversal not allowed" })
  .describe("Path to markdown or canvas file");
```

#### Transformations

```typescript
const dateToolSchema = z.object({
  date: z
    .string()
    .describe("Date in ISO format or natural language")
    .transform((str) => new Date(str)),
  timezone: z.string().default("UTC").describe("Timezone identifier"),
});
```

### Schema Composition

```typescript
// Base schemas that can be reused
const timeRangeSchema = z
  .object({
    startTime: z.date(),
    endTime: z.date(),
  })
  .refine((data) => data.endTime > data.startTime, {
    message: "End time must be after start time",
  });

const paginationSchema = z.object({
  page: z.number().int().positive().default(1),
  pageSize: z.number().int().positive().max(100).default(20),
});

// Compose into larger schemas
const searchWithPaginationSchema = z.object({
  query: z.string().min(1).describe("Search query"),
  filters: z.record(z.string()).optional().describe("Additional filters"),
  timeRange: timeRangeSchema.optional(),
  pagination: paginationSchema,
});
```

### Default Values

```typescript
const configSchema = z.object({
  temperature: z.number().min(0).max(2).default(0.7),
  maxTokens: z.number().int().positive().default(1000),
  model: z.enum(["gpt-4", "gpt-3.5-turbo"]).default("gpt-4"),
});

// Handler receives object with defaults applied
const configTool = createLangChainTool({
  name: "updateConfig",
  schema: configSchema,
  func: async (config) => {
    // config.temperature is always defined (0.7 if not provided)
    // config.maxTokens is always defined (1000 if not provided)
    // config.model is always defined ("gpt-4" if not provided)
    return updateConfiguration(config);
  },
});
```

### Handling Validation Errors with Retry

When AI-generated parameters fail Zod validation, the tool execution will return a formatted error. The autonomous agent automatically handles this through its iterative loop:

```typescript
// Example tool with strict validation
const searchToolWithValidation = createLangChainTool({
  name: "searchNotes",
  description: "Search notes with specific criteria",
  schema: z.object({
    query: z.string().min(2, "Query must be at least 2 characters"),
    limit: z.number().int().min(1).max(100),
    sortBy: z.enum(["relevance", "date", "title"]),
  }),
  func: async ({ query, limit, sortBy }) => {
    return performSearch(query, limit, sortBy);
  },
});

// When the AI provides invalid parameters:
// Input: { query: "a", limit: 200, sortBy: "random" }
//
// The flow:
// 1. Tool execution catches Zod validation error
// 2. Returns: "Tool searchNotes validation failed: query: Query must be at least 2 characters,
//             limit: Number must be less than or equal to 100, sortBy: Invalid enum value"
// 3. This error is added to the conversation as a user message
// 4. The AI sees the error in the next iteration and can retry with corrected parameters
// 5. The autonomous agent continues up to 4 iterations, allowing multiple retry attempts

// Example conversation flow:
// Iteration 1: AI calls tool with invalid params → receives error
// Iteration 2: AI understands error and retries with { query: "search term", limit: 50, sortBy: "date" } → success
```

The validation errors are automatically formatted to be clear and actionable, helping the AI self-correct. The autonomous agent's iterative design naturally provides retry capability with the AI learning from each error.

## Native Tool Calling Details

### Tool Execution (`toolExecution.ts`)

```typescript
// Execute individual tool with timeout and error handling
async function executeSequentialToolCall(
  toolCall: ToolCall,
  availableTools: any[]
): Promise<ToolExecutionResult> {
  // 30-second timeout per tool
  // Error handling and validation
  // Result formatting
}

// ToolCall interface (from native tool calling)
interface ToolCall {
  name: string;
  args: Record<string, unknown>;
  id?: string; // Used for ToolMessage correlation
}
```

### Available Tools in Agent Mode

All tools from the Copilot Plus system plus autonomous decision-making:

- **localSearch** - Vault content search with salient terms and query expansion
- **webSearch** - Web search with chat history context
- **getFileTree** - File structure exploration
- **getCurrentTime** / **getTimeRangeMs** - Time-based queries
- **pomodoroTool** - Productivity timer
- **indexTool** - Vault indexing operations
- **youtubeTranscription** - Video content analysis

### System Prompt Engineering

The Autonomous Agent mode uses a comprehensive system prompt that:

1. **Describes available tools** - Tool schemas are provided via `bindTools()`
2. **Provides behavioral guidance** - When to use each tool, how to chain them
3. **Sets expectations** for reasoning and tool chaining
4. **Includes critical requirements** (e.g., salientTerms for localSearch)

Tool descriptions are provided automatically via Zod schemas. Model adapters add behavioral guidance:

```typescript
// Example: Model adapter adds tool usage guidance
enhanceSystemPrompt(basePrompt: string): string {
  return basePrompt + `

When searching notes, always provide both "query" (string) and "salientTerms" (array of key terms).
Use getTimeRangeMs before localSearch for time-based queries.
`;
}
```

## Benefits of Autonomous Agent

1. **Autonomous Tool Selection** - AI decides what tools to use without pre-analysis
2. **Tool Chaining** - Can use results from one tool to inform the next
3. **Complex Workflows** - Multi-step reasoning with tool support
4. **Model Agnostic** - Works with any LLM that supports native tool calling
5. **No External Dependencies** - No Brevilabs API required
6. **Transparency** - User can see the AI's reasoning process via Agent Reasoning Block
7. **Native Integration** - Uses LangChain's `bindTools()` for proper tool calling support

## Usage

### Enable Autonomous Agent

```typescript
// In settings
settings.enableAutonomousAgent = true;

// ChainManager automatically selects the appropriate runner
const runner = chainManager.getChainRunner(); // Returns AutonomousAgentChainRunner
```

### Example Query Flow

**User Input:** "Find my notes about machine learning and research current best practices"

**Autonomous Agent Process:**

1. **Iteration 1**: AI reasons about the task → calls `localSearch` for ML notes
2. **Iteration 2**: Analyzes vault results → calls `webSearch` for current practices
3. **Iteration 3**: Synthesizes both sources → provides comprehensive response

**Legacy Process:**

1. Intent analysis determines both tools needed
2. Executes both tools
3. Single LLM call with all context

## Error Handling & Fallbacks

### Autonomous Agent Fallbacks

```typescript
try {
  // Sequential thinking execution
} catch (error) {
  // Automatic fallback to CopilotPlusChainRunner
  const fallbackRunner = new CopilotPlusChainRunner(this.chainManager);
  return await fallbackRunner.run(/* same parameters */);
}
```

### Tool Execution Safeguards

- 30-second timeout per tool
- Graceful error handling with descriptive messages
- Tool availability validation
- Result validation and formatting

## Model Adapter Pattern

### Overview

The Model Adapter pattern handles model-specific quirks and requirements cleanly, keeping the core logic model-agnostic.

### Architecture

```typescript
interface ModelAdapter {
  enhanceSystemPrompt(basePrompt: string, toolDescriptions: string): string;
  enhanceUserMessage(message: string, requiresTools: boolean): string;
  needsSpecialHandling(): boolean;
}
```

> **Note:** With native tool calling via `bindTools()`, tool calls are returned in structured `response.tool_calls` format. Model adapters now focus on behavioral guidance rather than parsing or response sanitization.

### Current Adapters

1. **BaseModelAdapter** - Default behavior for well-behaved models
2. **GPTModelAdapter** - Aggressive prompting for GPT models that often skip tool calls
3. **ClaudeModelAdapter** - Specialized handling for Claude thinking models (3.7 Sonnet, Claude 4)
4. **GeminiModelAdapter** - Ready for Gemini-specific handling

### Adding a New Model

```typescript
class NewModelAdapter extends BaseModelAdapter {
  enhanceSystemPrompt(basePrompt: string, toolDescriptions: string): string {
    const base = super.enhanceSystemPrompt(basePrompt, toolDescriptions);
    return base + "\n\n[Model-specific instructions here]";
  }

  enhanceUserMessage(message: string, requiresTools: boolean): string {
    // Add model-specific hints if needed
    return requiresTools ? `${message}\n[Model-specific hint]` : message;
  }
}

// Register in ModelAdapterFactory
if (modelName.includes("newmodel")) {
  return new NewModelAdapter(modelName);
}
```

### Claude Model Adapter Features

The `ClaudeModelAdapter` includes specialized handling for Claude thinking models:

#### Thinking Model Support

- **Claude 3.7 Sonnet** and **Claude 4** - Automatic thinking mode configuration
- **Think Block Preservation** - Maintains valuable reasoning context in responses
- **Temperature Control** - Disables temperature for thinking models (as required by API)

> **Note:** With native tool calling, tool calls are returned in structured `response.tool_calls` format. Intermediate tool calls are hidden from users and displayed via the Agent Reasoning Block. Only final responses are streamed to the UI.

#### Agent Reasoning Block

The reasoning process is now displayed via the Agent Reasoning Block component, which shows:

```
⏱️ 2.3s elapsed
├─ Searching notes for "piano", "learning", "practice"...
├─ Found 5 notes: Piano Practice.md, Learning Music.md...
└─ Generating response...
```

### Benefits

1. **Separation of Concerns** - Model quirks isolated from core logic
2. **Maintainability** - Easy to find and update model-specific code
3. **Extensibility** - Simple to add support for new models
4. **Testing** - Model adapters can be unit tested independently
5. **Clean Core** - Autonomous agent logic remains model-agnostic
6. **Hallucination Prevention** - Specialized handling for problematic models
7. **Streaming Protection** - Prevents bad content from reaching users
8. **Generalizable Solutions** - Uses threshold-based detection over regex patterns

## Future Considerations

1. **Tool Discovery** - Dynamic tool registration
2. **Custom Tools** - User-defined tool capabilities
3. **Parallel Execution** - Multiple tools simultaneously
4. **Tool Result Caching** - Avoid redundant calls
5. **Advanced Reasoning** - More sophisticated decision trees
6. **Tool Permissions** - User control over tool access (human-in-the-loop approval)
7. **Deep Search** - Iterative search refinement for complex queries
8. **Response Validation** - Adapters could validate model outputs
9. **Model-Specific Optimizations** - Expand adapter capabilities for emerging models

The autonomous agent approach using native tool calling represents a significant evolution from traditional tool calling, enabling more sophisticated AI reasoning and autonomous task completion.
