Skip to content

feat: Implement OpenAI-compatible tool-calling / function-calling in AIHarness #287

@d-oit

Description

@d-oit

Problem

The AI harness currently supports only free-form text chat. The AGENTS.md and agent harness concept imply the ability to call tools (e.g. search knowledge, create note, add graph node), but no tool-calling implementation exists in the provider layer (openrouter.ts, kilo.ts) or in useChat.ts. The agentic capability of the app is completely unrealized.

Proposed Implementation

1. Tool definition types

// src/lib/llm/tool-types.ts
export interface ToolDefinition {
  name: string;
  description: string;
  parameters: {
    type: 'object';
    properties: Record<string, { type: string; description: string; enum?: string[] }>;
    required?: string[];
  };
}

export interface ToolCall {
  id: string;
  name: string;
  arguments: Record<string, unknown>;
}

export interface ToolResult {
  toolCallId: string;
  content: string;
  isError?: boolean;
}

2. Built-in tool registry

// src/lib/llm/tool-registry.ts
export const BUILT_IN_TOOLS: ToolDefinition[] = [
  {
    name: 'search_knowledge',
    description: 'Search the local knowledge base for relevant notes and documents',
    parameters: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'The search query' },
        limit: { type: 'number', description: 'Max results to return (default 5)' },
      },
      required: ['query'],
    },
  },
  {
    name: 'create_note',
    description: 'Create a new note in the knowledge studio',
    parameters: {
      type: 'object',
      properties: {
        title: { type: 'string', description: 'Note title' },
        content: { type: 'string', description: 'Note content in Markdown' },
        tags: { type: 'string', description: 'Comma-separated tags' },
      },
      required: ['title', 'content'],
    },
  },
  {
    name: 'add_graph_node',
    description: 'Add a node to the knowledge graph',
    parameters: {
      type: 'object',
      properties: {
        label: { type: 'string', description: 'Node label' },
        type: { type: 'string', description: 'Node type', enum: ['concept', 'person', 'org', 'tech', 'place'] },
        description: { type: 'string', description: 'Node description' },
      },
      required: ['label'],
    },
  },
  {
    name: 'get_current_note',
    description: 'Get the content of the currently active note in the editor',
    parameters: { type: 'object', properties: {}, required: [] },
  },
];

3. Tool executor

// src/lib/llm/tool-executor.ts
export async function executeTool(
  toolCall: ToolCall,
  context: ToolExecutionContext
): Promise<ToolResult> {
  try {
    switch (toolCall.name) {
      case 'search_knowledge': {
        const results = await hybridSearch(context.oramaDb, toolCall.arguments.query as string, {
          limit: (toolCall.arguments.limit as number) ?? 5,
        });
        return {
          toolCallId: toolCall.id,
          content: JSON.stringify(results.map(r => ({ title: r.document.title, excerpt: r.document.content.slice(0, 200) }))),
        };
      }
      case 'create_note': {
        const note = await context.noteStore.create({
          title: toolCall.arguments.title as string,
          content: toolCall.arguments.content as string,
          tags: (toolCall.arguments.tags as string ?? '').split(',').map(t => t.trim()),
        });
        return { toolCallId: toolCall.id, content: `Note created with ID: ${note.id}` };
      }
      case 'add_graph_node': {
        const node = context.graphStore.addNode({
          id: crypto.randomUUID(),
          label: toolCall.arguments.label as string,
          type: (toolCall.arguments.type as string) ?? 'concept',
        });
        return { toolCallId: toolCall.id, content: `Graph node added: ${node.id}` };
      }
      default:
        return { toolCallId: toolCall.id, content: 'Unknown tool', isError: true };
    }
  } catch (err) {
    return { toolCallId: toolCall.id, content: String(err), isError: true };
  }
}

4. Provider layer updates

Add tools support to openrouter.ts and kilo.ts:

// In provider chat() call:
const requestBody = {
  model: opts.model,
  messages,
  tools: opts.tools?.map(t => ({ type: 'function', function: t })),
  tool_choice: opts.tools?.length ? 'auto' : undefined,
};

// Parse tool calls from response:
if (response.choices[0].message.tool_calls) {
  return {
    content: '',
    toolCalls: response.choices[0].message.tool_calls.map(tc => ({
      id: tc.id,
      name: tc.function.name,
      arguments: JSON.parse(tc.function.arguments),
    })),
  };
}

5. Agentic loop in useChat.ts

async function sendWithTools(userMessage: string) {
  let messages = [...chatMessages, { role: 'user', content: userMessage }];
  
  // Agentic loop: max 5 tool-call rounds
  for (let i = 0; i < 5; i++) {
    const response = await provider.chat(messages, { model, tools: BUILT_IN_TOOLS });
    
    if (!response.toolCalls?.length) {
      // Final text response
      setChatMessages([...messages, { role: 'assistant', content: response.content }]);
      break;
    }
    
    // Execute tool calls
    const toolResults = await Promise.all(
      response.toolCalls.map(tc => executeTool(tc, toolContext))
    );
    
    // Append assistant tool-call message + tool results
    messages = [
      ...messages,
      { role: 'assistant', content: '', tool_calls: response.toolCalls },
      ...toolResults.map(r => ({ role: 'tool', tool_call_id: r.toolCallId, content: r.content })),
    ];
  }
}

6. UI: Tool call visibility

In AIHarness.tsx, show tool calls as collapsible blocks in the chat:

[Tool: search_knowledge]
  Query: "quantum computing notes"
  Results: 3 notes found
  ▶ View details

Acceptance Criteria

  • ToolDefinition, ToolCall, ToolResult types defined
  • BUILT_IN_TOOLS registry with search_knowledge, create_note, add_graph_node, get_current_note
  • executeTool() dispatcher implemented
  • openrouter.ts and kilo.ts updated to send/parse tool calls
  • Agentic loop in useChat.ts (max 5 rounds)
  • Tool calls displayed as collapsible blocks in chat UI
  • Models that don't support tool-calling fall back gracefully to text-only
  • Unit tests for tool executor with mocked context

Metadata

Metadata

Assignees

No one assigned

    Labels

    aiai-agentduplicateThis issue or pull request already existsenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions