Skip to main content
Build your own agent harness using SDK primitives. This gives you full control over the agent loop while letting you import any parts of the sdk you dont want to build yourself.
Most users should use the SDK Tool or MCP integration. This page is for advanced use cases where you need custom control over the agent loop.

Two Approaches

SDK Harness Primitives

Use @morphllm/morphsdk/tools/warp-grep/harness for parsing, formatting, and types. Recommended - battle-tested, type-safe, stays in sync with model updates.

Raw API

Hit the API directly with curl. Full control, but you implement everything yourself.

SDK Harness Primitives

The /harness export gives you the building blocks to construct your own agent loop:
import {
  // Parsing
  parseToolCalls,        // Parse model output → ToolCall[]
  
  // System prompt
  SYSTEM_PROMPT,         // The exact prompt used by the SDK
  
  // Formatting
  formatToolResult,      // Wrap tool output in XML for model
  formatTurnMessage,     // "[Turn X/4] You have Y turns remaining."
  formatAnalyseTree,     // Format directory listings
  
  // Finish resolution
  resolveFinishFiles,    // Read file ranges from finish command
  
  // Constants
  MAX_TURNS,             // 4
  TIMEOUT_MS,            // 30000
  DEFAULT_EXCLUDES,      // Patterns to skip (node_modules, .git, etc.)
  
  // Provider
  LocalRipgrepProvider,  // Local filesystem implementation
  
  // Types
  type ToolCall,
  type ToolName,
  type FinishFileSpec,
  type ChatMessage,
  type WarpGrepProvider,
  type GrepResult,
  type ReadResult,
  type AnalyseEntry,
  type ResolvedFile,
} from '@morphllm/morphsdk/tools/warp-grep/harness';

Quick Example

import {
  parseToolCalls,
  SYSTEM_PROMPT,
  formatToolResult,
  formatTurnMessage,
  formatAnalyseTree,
  resolveFinishFiles,
  MAX_TURNS,
  LocalRipgrepProvider,
} from '@morphllm/morphsdk/tools/warp-grep/harness';

const provider = new LocalRipgrepProvider(repoRoot);

// Build messages
const messages = [
  { role: 'system', content: SYSTEM_PROMPT },
  { role: 'user', content: `<query>${query}</query>` },
];

// Your custom agent loop
for (let turn = 1; turn <= MAX_TURNS; turn++) {
  // Call YOUR model (or morph-warp-grep)
  const response = await callModel(messages);
  messages.push({ role: 'assistant', content: response });

  // Parse tool calls from response
  const toolCalls = parseToolCalls(response);
  const results: string[] = [];

  for (const call of toolCalls) {
    // Handle finish - we're done
    if (call.name === 'finish') {
      const files = await resolveFinishFiles(provider, call.arguments.files);
      return files; // Array of { path, content }
    }

    // Execute tool and format result
    let output: string;
    if (call.name === 'grep') {
      const r = await provider.grep(call.arguments);
      output = r.error || r.lines.join('\n') || 'no matches';
    } else if (call.name === 'read') {
      const r = await provider.read(call.arguments);
      output = r.error || r.lines.join('\n') || '(empty)';
    } else if (call.name === 'analyse') {
      const entries = await provider.analyse(call.arguments);
      output = formatAnalyseTree(entries);
    }

    results.push(formatToolResult(call.name, call.arguments, output));
  }

  // Feed results back to model
  messages.push({
    role: 'user',
    content: results.join('\n') + formatTurnMessage(turn)
  });
}

Full Working Example

/**
 * Custom Warp Grep Harness Example
 * 
 * This example shows how to build your own agent loop using SDK primitives.
 * You control the model, the loop, and can add custom logic at any step.
 */

import {
  parseToolCalls,
  SYSTEM_PROMPT,
  formatToolResult,
  formatTurnMessage,
  formatAnalyseTree,
  resolveFinishFiles,
  MAX_TURNS,
  LocalRipgrepProvider,
  type ToolCall,
  type ResolvedFile,
} from '@morphllm/morphsdk/tools/warp-grep/harness';

interface Message {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

async function callMorphWarpGrep(messages: Message[]): Promise<string> {
  const response = await fetch('https://api.morphllm.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.MORPH_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'morph-warp-grep',
      messages,
      temperature: 0.0,
      max_tokens: 1024,
    }),
  });

  const data = await response.json();
  return data.choices[0].message.content;
}

async function searchCodebase(
  query: string,
  repoRoot: string
): Promise<ResolvedFile[]> {
  const provider = new LocalRipgrepProvider(repoRoot);
  
  const messages: Message[] = [
    { role: 'system', content: SYSTEM_PROMPT },
    { role: 'user', content: `<query>${query}</query>` },
  ];

  // Optional: Add initial repo state
  const initialEntries = await provider.analyse({ path: '.', maxResults: 50 });
  const dirs = initialEntries.filter(e => e.type === 'dir').map(e => e.name);
  const files = initialEntries.filter(e => e.type === 'file').map(e => e.name);
  messages.push({
    role: 'user',
    content: `<repo_root>${repoRoot}</repo_root>\n<top_dirs>${dirs.join(', ')}</top_dirs>\n<top_files>${files.join(', ')}</top_files>`
  });

  for (let turn = 1; turn <= MAX_TURNS; turn++) {
    console.log(`\n--- Turn ${turn}/${MAX_TURNS} ---`);
    
    const response = await callMorphWarpGrep(messages);
    messages.push({ role: 'assistant', content: response });

    const toolCalls = parseToolCalls(response);
    console.log(`Tool calls: ${toolCalls.map(c => c.name).join(', ')}`);

    if (toolCalls.length === 0) {
      console.log('No tool calls - terminating');
      break;
    }

    // Check for finish first
    const finishCall = toolCalls.find(c => c.name === 'finish');
    if (finishCall) {
      console.log('Finish called - resolving files');
      return await resolveFinishFiles(provider, finishCall.arguments.files);
    }

    // Execute all tools in parallel
    const results = await Promise.all(
      toolCalls.map(async (call) => {
        let output: string;
        
        try {
          if (call.name === 'grep') {
            const r = await provider.grep(call.arguments as any);
            output = r.error || r.lines.join('\n') || 'no matches';
          } else if (call.name === 'read') {
            const r = await provider.read(call.arguments as any);
            output = r.error || r.lines.join('\n') || '(empty)';
          } else if (call.name === 'analyse') {
            const entries = await provider.analyse(call.arguments as any);
            output = formatAnalyseTree(entries);
          } else {
            output = `Unknown tool: ${call.name}`;
          }
        } catch (err) {
          output = `Error: ${err}`;
        }

        return formatToolResult(call.name, call.arguments || {}, output);
      })
    );

    messages.push({
      role: 'user',
      content: results.join('\n') + formatTurnMessage(turn)
    });
  }

  return []; // No results if we exhaust turns without finish
}

// Usage
const results = await searchCodebase(
  'Find where authentication tokens are validated',
  '/path/to/repo'
);

for (const file of results) {
  console.log(`\n=== ${file.path} ===`);
  console.log(file.content);
}

API Reference

parseToolCalls(text: string): ToolCall[]

Parse model output into structured tool calls. Automatically removes <think> blocks and extracts <tool_call> tags.
const response = `
<think>Looking for auth code...</think>
<tool_call>grep 'authenticate' src/</tool_call>
<tool_call>analyse src/auth</tool_call>
`;

const calls = parseToolCalls(response);
// [
//   { name: 'grep', arguments: { pattern: 'authenticate', path: 'src/' } },
//   { name: 'analyse', arguments: { path: 'src/auth', pattern: null } }
// ]

formatToolResult(name, args, output, options?)

Format tool output with XML wrapper for model consumption.
const formatted = formatToolResult(
  'grep',
  { pattern: 'auth', path: 'src/' },
  'src/auth.ts:10:function authenticate() {'
);
// <grep_output pattern="auth" path="src/">
// src/auth.ts:10:function authenticate() {
// </grep_output>

formatTurnMessage(turn: number, maxTurns?: number)

Format turn counter message.
formatTurnMessage(1);  // "\n\n[Turn 1/4] You have 3 turns remaining."
formatTurnMessage(3);  // "\n\n[Turn 3/4] You have 1 turn remaining."
formatTurnMessage(4);  // "\n\n[Turn 4/4] This is your LAST turn. You MUST call finish now."

formatAnalyseTree(entries: AnalyseEntry[])

Format directory listing entries as a tree.
const entries = [
  { name: 'src', path: 'src', type: 'dir', depth: 0 },
  { name: 'index.ts', path: 'src/index.ts', type: 'file', depth: 1 },
];

formatAnalyseTree(entries);
// "- [D] src\n  - [F] index.ts"

resolveFinishFiles(provider, files)

Read file ranges specified in a finish command.
const files = [
  { path: 'src/auth.ts', lines: [[1, 50], [100, 120]] },
  { path: 'src/types.ts', lines: [[1, 30]] },
];

const resolved = await resolveFinishFiles(provider, files);
// [{ path: 'src/auth.ts', content: '...' }, { path: 'src/types.ts', content: '...' }]

Constants

ConstantValueDescription
MAX_TURNS4Maximum agent turns before forced finish
TIMEOUT_MS30000Request timeout in milliseconds
DEFAULT_EXCLUDESstring[]60+ patterns to skip (node_modules, .git, dist, etc.)
SYSTEM_PROMPTstringThe exact system prompt used by the SDK

Tool Specifications

The model outputs tool calls that you execute locally. Here’s what each tool expects:
  • grep
  • read
  • analyse
  • finish
Search for regex pattern in files using ripgrep.Syntax: grep '<pattern>' <path>Arguments:
NameTypeDescription
patternstringRegex pattern (without quotes in parsed args)
pathstringDirectory or file to search (. for repo root)
Provider method:
provider.grep({ pattern: 'authenticate', path: 'src/' })
// Returns: { lines: ['src/auth.ts:10:function authenticate()'], error?: string }
Output format: Ripgrep format path:line:content

Raw API Access

For full control without SDK dependencies, hit the API directly:
curl -X POST https://api.morphllm.com/v1/chat/completions \
  -H "Authorization: Bearer $MORPH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "morph-warp-grep",
    "messages": [
      {"role": "system", "content": "You are a code search agent..."},
      {"role": "user", "content": "<query>Find authentication middleware</query>"}
    ],
    "temperature": 0.0,
    "max_tokens": 1024
  }'
Response format:
<think>
This is a specific query about authentication. I'll grep for auth-related patterns.
High confidence I can finish in 2 turns if I find the auth module.
</think>
<tool_call>grep 'authenticate' src/</tool_call>
<tool_call>grep 'middleware' src/</tool_call>
<tool_call>analyse src/auth</tool_call>
When building your own parser:
  1. Remove <think>...</think> blocks
  2. Extract text from <tool_call>...</tool_call> tags
  3. Parse each line as command arg1 arg2...
  4. Handle quoted patterns in grep: grep 'pattern' path
You are a code search agent. Your task is to find all relevant code for a given query.

<workflow>
You have exactly 4 turns. The 4th turn MUST be a `finish` call. Each turn allows up to 8 parallel tool calls.

- Turn 1: Map the territory OR dive deep (based on query specificity)
- Turn 2-3: Refine based on findings
- Turn 4: MUST call `finish` with all relevant code locations
- You MAY call `finish` early if confident—but never before at least 1 search turn.

Remember, if the task feels easy to you, it is strongly desirable to call `finish` early using fewer turns, but quality over speed.
</workflow>

<tools>
### `analyse <path> [pattern]`
Directory tree or file search. Shows structure of a path, optionally filtered by regex pattern.
- `path`: Required. Directory or file path (use `.` for repo root)
- `pattern`: Optional regex to filter results

Examples:
analyse .
analyse src/api
analyse . ".*\.ts$"
analyse src "test.*"

### `read <path>[:start-end]`
Read file contents. Line range is 1-based, inclusive.
- Returns numbered lines for easy reference
- Omit range to read entire file

Examples:
read src/main.py
read src/db/conn.py:10-50
read package.json:1-20

### `grep '<pattern>' <path>`
Ripgrep search. Finds pattern matches across files.
- `'<pattern>'`: Required. Regex pattern wrapped in single quotes
- `<path>`: Required. Directory or file to search (use `.` for repo root)

Examples:
grep 'class.*Service' src/
grep 'def authenticate' .
grep 'import.*from' src/components/
grep 'TODO' .

### `finish <file1:ranges> [file2:ranges ...]`
Submit final answer with all relevant code locations.
- Include generous line ranges—don't be stingy with context
- Ranges are comma-separated: `file.py:10-30,50-60`
- ALWAYS include import statements at the top of files (usually lines 1-20)
- If code spans multiple files, include ALL of them
- Small files can be returned in full

Examples:
finish src/auth.py:1-15,25-50,75-80 src/models/user.py:1-10,20-45
finish src/index.ts:1-100
</tools>

<strategy>
**Before your first tool call, classify the query:**

| Query Type | Turn 1 Strategy | Early Finish? |
|------------|-----------------|---------------|
| **Specific** (function name, error string, unique identifier) | 8 parallel greps on likely paths | Often by turn 2 |
| **Conceptual** (how does X work, where is Y handled) | analyse + 2-3 broad greps | Rarely early |
| **Exploratory** (find all tests, list API endpoints) | analyse at multiple depths | Usually needs 3 turns |

**Parallel call patterns:**
- **Shotgun grep**: Same pattern, 8 different directories—fast coverage
- **Variant grep**: 8 pattern variations (synonyms, naming conventions)—catches inconsistent codebases
- **Funnel**: 1 analyse + 7 greps—orient and search simultaneously
- **Deep read**: 8 reads on files you already identified—gather full context fast
</strategy>

<output_format>
EVERY response MUST follow this exact format:

1. First, wrap your reasoning in `<think>...</think>` tags containing:
   - Query classification (specific/conceptual/exploratory)
   - Confidence estimate (can I finish in 1-2 turns?)
   - This turn's parallel strategy
   - What signals would let me finish early?

2. Then, output tool calls wrapped in `<tool_call>...</tool_call>` tags, one per line.

Example:
<think>
This is a specific query about authentication. I'll grep for auth-related patterns.
High confidence I can finish in 2 turns if I find the auth module.
Strategy: Shotgun grep across likely directories.
</think>
<tool_call>grep 'authenticate' src/</tool_call>
<tool_call>grep 'login' src/</tool_call>
<tool_call>analyse src/auth</tool_call>

No commentary outside `<think>`. No explanations after tool calls.
</output_format>

<finishing_requirements>
When calling `finish`:
- Include the import section (typically lines 1-20) of each file
- Include all function/class definitions that are relevant
- Include any type definitions, interfaces, or constants used
- Better to over-include than leave the user missing context
- If unsure about boundaries, include more rather than less
</finishing_requirements>

Begin your exploration now to find code relevant to the query.

Types Reference

type ToolName = 'grep' | 'read' | 'analyse' | 'finish' | 'glob' | '_skip';

interface ToolCall {
  name: ToolName;
  arguments?: Record<string, unknown>;
}

interface ChatMessage {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

interface FinishFileSpec {
  path: string;
  lines: Array<[number, number]>; // [start, end] pairs
}

interface ResolvedFile {
  path: string;
  content: string;
}

interface GrepResult {
  lines: string[];  // "path:line:content" format
  error?: string;
}

interface ReadResult {
  lines: string[];  // "lineNumber|content" format
  error?: string;
}

interface AnalyseEntry {
  name: string;
  path: string;
  type: 'file' | 'dir';
  depth: number;
}

interface WarpGrepProvider {
  grep(params: { pattern: string; path: string }): Promise<GrepResult>;
  read(params: { path: string; start?: number; end?: number }): Promise<ReadResult>;
  analyse(params: { path: string; pattern?: string | null; maxResults?: number; maxDepth?: number }): Promise<AnalyseEntry[]>;
}