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
Problem
The AI harness currently supports only free-form text chat. The
AGENTS.mdand 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 inuseChat.ts. The agentic capability of the app is completely unrealized.Proposed Implementation
1. Tool definition types
2. Built-in tool registry
3. Tool executor
4. Provider layer updates
Add
toolssupport toopenrouter.tsandkilo.ts:5. Agentic loop in
useChat.ts6. UI: Tool call visibility
In
AIHarness.tsx, show tool calls as collapsible blocks in the chat:Acceptance Criteria
ToolDefinition,ToolCall,ToolResulttypes definedBUILT_IN_TOOLSregistry withsearch_knowledge,create_note,add_graph_node,get_current_noteexecuteTool()dispatcher implementedopenrouter.tsandkilo.tsupdated to send/parse tool callsuseChat.ts(max 5 rounds)