Skip to content

feat(ai): add agentic tool-calling loop with search_knowledge, create_note, add_graph_node tools#293

Open
d-oit wants to merge 9 commits into
mainfrom
feat/agentic-tool-loop
Open

feat(ai): add agentic tool-calling loop with search_knowledge, create_note, add_graph_node tools#293
d-oit wants to merge 9 commits into
mainfrom
feat/agentic-tool-loop

Conversation

@d-oit

@d-oit d-oit commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a multi-round agentic loop (up to 5 rounds) in useChat.ts — the LLM can invoke built-in tools and feed results back for further reasoning before generating the final answer
  • 4 built-in tools: search_knowledge, create_note, add_graph_node, get_current_note
  • Tool execution with DI for testability in tool-executor.ts (127 lines of tests)
  • OpenRouter & Kilo providers now pass tool definitions and parse tool_calls from responses
  • Collapsible ToolCallBlock UI in ChatView showing per-tool input/output
  • Fixes mind map readiness (explicit isMindReady state), useMediaQuery listener, stale ESLint comments
  • React Compiler opt-outs for components using @tanstack/react-virtual

Closes #287 — the tool-calling / function-calling implementation for AIHarness.

…_note, add_graph_node tools

Introduces a multi-round agentic loop (up to 5 rounds) in useChat.ts that
enables the LLM to invoke built-in tools and feed results back for further
reasoning before generating the final response.

- Add ToolDefinition, ToolCall, ToolResult types and LLMMessage tool fields
- Add tool-registry.ts with 4 built-in tools (search_knowledge, create_note,
  add_graph_node, get_current_note)
- Add tool-executor.ts with dependency-injected execution logic + tests
- Support tool definitions and tool_calls parsing in OpenRouter & Kilo providers
- Render collapsible ToolCallBlock in ChatView showing input/output per tool
- Fix mind map readiness with explicit isMindReady state
- Opt out of React Compiler for components using @tanstack/react-virtual
- Add targeted eslint-disable comments for intentional patterns
- Remove stale eslint-disable comments and unnecessary useEffect in Editor/DatabaseSettings
- Fix useMediaQuery listener to use MediaQueryListEvent
@github-actions github-actions Bot added config tests Related to automated/manual tests labels Jun 8, 2026
@deepsource-io

deepsource-io Bot commented Jun 8, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 1d652e1...601cf84 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Jun 8, 2026 4:21p.m. Review ↗
Python Jun 8, 2026 4:21p.m. Review ↗
Shell Jun 8, 2026 4:21p.m. Review ↗
SQL Jun 8, 2026 4:21p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@codacy-production

codacy-production Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Not up to standards ⛔

🔴 Issues 2 high

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
ErrorProne 1 high
Security 1 high

View in Codacy

🟢 Metrics 4 complexity · 0 duplication

Metric Results
Complexity 4
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread src/features/ai/ChatView.tsx Outdated
try { return new URL(url).hostname; } catch { return url; }
};

const ToolCallBlock: React.FC<{ toolCall: ToolCallRecord }> = ({ toolCall }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`ToolCallBlock` has a cyclomatic complexity of 7 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

Comment on lines 221 to 227
return () => {
if (mindInstance.current) {
if (currentContainer) currentContainer.replaceChildren();
mindInstance.current = null;
setIsMindReady(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow function expected no return value


Any code paths that do not have explicit returns will return undefined. It is recommended to replace any implicit dead-ends that return undefined with a return null statement.

Comment on lines +11 to +68
export async function executeTool(
toolCall: ToolCall,
context: ToolExecutionContext = {},
): Promise<ToolResult> {
const search = context.search ?? searchKnowledge;

try {
switch (toolCall.name) {
case 'search_knowledge': {
const query = toolCall.arguments.query as string;
const limit = (toolCall.arguments.limit as number | undefined) ?? 5;
const results = await search(query, { limit });
const summary = results.map(r => ({
title: r.title,
type: r.type,
excerpt: r.content.slice(0, 200),
}));
return { toolCallId: toolCall.id, content: JSON.stringify(summary) };
}

case 'create_note': {
const title = toolCall.arguments.title as string;
const content = toolCall.arguments.content as string;
const tags = ((toolCall.arguments.tags as string | undefined) ?? '')
.split(',')
.map(t => t.trim())
.filter(Boolean);

// Create entity (the titled concept) then attach the note
const entity = await repository.createEntity({
name: title,
type: 'note',
description: tags.length > 0 ? `Tags: ${tags.join(', ')}` : undefined,
});
await repository.createNote({ entity_id: entity.id, content, format: 'markdown' });
return { toolCallId: toolCall.id, content: `Note "${title}" created (entity id: ${entity.id})` };
}

case 'add_graph_node': {
const label = toolCall.arguments.label as string;
const type = (toolCall.arguments.type as string | undefined) ?? 'concept';
const description = toolCall.arguments.description as string | undefined;
const entity = await repository.createEntity({ name: label, type, description });
return { toolCallId: toolCall.id, content: `Graph node "${label}" added (id: ${entity.id})` };
}

case 'get_current_note': {
const content = context.getCurrentNoteContent?.() ?? '(no active note)';
return { toolCallId: toolCall.id, content };
}

default:
return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}`, isError: true };
}
} catch (err) {
return { toolCallId: toolCall.id, content: String(err), isError: true };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

getCurrentNoteContent?: () => string;
}

export async function executeTool(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`executeTool` has a cyclomatic complexity of 12 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

…d global scope issues

- Split ToolCallBlock into ToolCallHeader + ToolCallBody sub-components to reduce
  cyclomatic complexity from 7 to ~2 per component
- Extract executeTool switch into individual handler functions + HANDLERS map
  to reduce function complexity from 12 to ~2 per handler
- Add explicit return in MindMapView cleanup arrow function
- Refactor tool-executor.ts into handler-based dispatch pattern with
  centralized error handling
Comment on lines 221 to 228
return () => {
if (mindInstance.current) {
if (currentContainer) currentContainer.replaceChildren();
mindInstance.current = null;
setIsMindReady(false);
}
return;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow function expected no return value


Any code paths that do not have explicit returns will return undefined. It is recommended to replace any implicit dead-ends that return undefined with a return null statement.

Comment on lines +11 to +21
async function handleSearchKnowledge(toolCall: ToolCall, search: typeof searchKnowledge): Promise<ToolResult> {
const query = toolCall.arguments.query as string;
const limit = (toolCall.arguments.limit as number | undefined) ?? 5;
const results = await search(query, { limit });
const summary = results.map(r => ({
title: r.title,
type: r.type,
excerpt: r.content.slice(0, 200),
}));
return { toolCallId: toolCall.id, content: JSON.stringify(summary) };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +23 to +38
async function handleCreateNote(toolCall: ToolCall): Promise<ToolResult> {
const title = toolCall.arguments.title as string;
const content = toolCall.arguments.content as string;
const tags = ((toolCall.arguments.tags as string | undefined) ?? '')
.split(',')
.map(t => t.trim())
.filter(Boolean);

const entity = await repository.createEntity({
name: title,
type: 'note',
description: tags.length > 0 ? `Tags: ${tags.join(', ')}` : undefined,
});
await repository.createNote({ entity_id: entity.id, content, format: 'markdown' });
return { toolCallId: toolCall.id, content: `Note "${title}" created (entity id: ${entity.id})` };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +40 to +46
async function handleAddGraphNode(toolCall: ToolCall): Promise<ToolResult> {
const label = toolCall.arguments.label as string;
const type = (toolCall.arguments.type as string | undefined) ?? 'concept';
const description = toolCall.arguments.description as string | undefined;
const entity = await repository.createEntity({ name: label, type, description });
return { toolCallId: toolCall.id, content: `Graph node "${label}" added (id: ${entity.id})` };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +48 to +51
function handleGetCurrentNote(toolCall: ToolCall, context: ToolExecutionContext): ToolResult {
const content = context.getCurrentNoteContent?.() ?? '(no active note)';
return { toolCallId: toolCall.id, content };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +60 to +75
export async function executeTool(
toolCall: ToolCall,
context: ToolExecutionContext = {},
): Promise<ToolResult> {
const search = context.search ?? searchKnowledge;
const handler = HANDLERS[toolCall.name];
if (!handler) {
return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}`, isError: true };
}
try {
return await handler(toolCall, context, search);
} catch (err) {
logger.error('Tool execution failed', { tool: toolCall.name, error: err });
return { toolCallId: toolCall.id, content: String(err), isError: true };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

CI/CD Tester added 2 commits June 8, 2026 17:48
…ject injection sinks

- Extract arrow function shorthand in useMediaQuery to block body
- Add nosemgrep inline disables for React inline styles (ChatView)
- Add nosemgrep inline disables for object injection sinks
  (useChat tool loop, tool-executor dispatch)
- Extract all JSX inline style objects to typed CSSProperties variables
- Replace for-loop with forEach to avoid object injection sink rule
- Revert executeTool to switch-based dispatch to avoid dynamic property access
Comment on lines +53 to +76
export async function executeTool(
toolCall: ToolCall,
context: ToolExecutionContext = {},
): Promise<ToolResult> {
const search = context.search ?? searchKnowledge;

try {
switch (toolCall.name) {
case 'search_knowledge':
return handleSearchKnowledge(toolCall, search);
case 'create_note':
return handleCreateNote(toolCall);
case 'add_graph_node':
return handleAddGraphNode(toolCall);
case 'get_current_note':
return Promise.resolve(handleGetCurrentNote(toolCall, context));
default:
return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}`, isError: true };
}
} catch (err) {
logger.error('Tool execution failed', { tool: toolCall.name, error: err });
return { toolCallId: toolCall.id, content: String(err), isError: true };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +53 to +76
export async function executeTool(
toolCall: ToolCall,
context: ToolExecutionContext = {},
): Promise<ToolResult> {
const search = context.search ?? searchKnowledge;

try {
switch (toolCall.name) {
case 'search_knowledge':
return handleSearchKnowledge(toolCall, search);
case 'create_note':
return handleCreateNote(toolCall);
case 'add_graph_node':
return handleAddGraphNode(toolCall);
case 'get_current_note':
return Promise.resolve(handleGetCurrentNote(toolCall, context));
default:
return { toolCallId: toolCall.id, content: `Unknown tool: ${toolCall.name}`, isError: true };
}
} catch (err) {
logger.error('Tool execution failed', { tool: toolCall.name, error: err });
return { toolCallId: toolCall.id, content: String(err), isError: true };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

return { toolCallId: toolCall.id, content };
}

export async function executeTool(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`executeTool` has a cyclomatic complexity of 7 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

Comment on lines 221 to 227
return () => {
if (mindInstance.current) {
if (currentContainer) currentContainer.replaceChildren();
mindInstance.current = null;
setIsMindReady(false);
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow function expected no return value


Any code paths that do not have explicit returns will return undefined. It is recommended to replace any implicit dead-ends that return undefined with a return null statement.

Comment on lines +34 to +36
function buildToolCallRecord(tc: ToolCallRecord, result: string, isError?: boolean): ToolCallRecord {
return { id: tc.id, name: tc.name, arguments: tc.arguments, result, isError };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +8 to +10
function createMediaListener(setter: (v: boolean) => void): (e: MediaQueryListEvent) => void {
return (e: MediaQueryListEvent) => { setter(e.matches); };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines 12 to 23
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => window.matchMedia(query).matches);

useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
const listener = createMediaListener(setMatches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query, matches]);
}, [query]);

return matches;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +36 to +47
function ToolCallHeader({ toolCall, expanded, onToggle }: { toolCall: ToolCallRecord; expanded: boolean; onToggle: () => void }): React.ReactElement {
return (
<button type="button" onClick={onToggle} style={btnStyle} aria-expanded={expanded}>
<Wrench size={12} />
<span style={nameStyle}>{toolCall.name}</span>
{toolCall.isError && <span style={errorStyle}>error</span>}
<span style={toolCall.isError ? errorIconStyle : iconBaseStyle}>
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</span>
</button>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +51 to +66
function ToolCallBody({ toolCall }: { toolCall: ToolCallRecord }): React.ReactElement {
return (
<div style={bodyDivStyle}>
<div>
<span style={labelStyle}>Input: </span>
<code style={codeStyle}>{JSON.stringify(toolCall.arguments, null, 2)}</code>
</div>
{toolCall.result !== undefined && (
<div>
<span style={labelStyle}>Result: </span>
<code style={toolCall.isError ? { ...resultCodeBaseStyle, color: '#dc2626' } : resultCodeBaseStyle}>{toolCall.result}</code>
</div>
)}
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +68 to +77
function ToolCallBlock({ toolCall }: { toolCall: ToolCallRecord }): React.ReactElement {
const [expanded, setExpanded] = useState(false);
const toggle = useCallback(() => { setExpanded(v => !v); }, []);
return (
<div style={blockStyle}>
<ToolCallHeader toolCall={toolCall} expanded={expanded} onToggle={toggle} />
{expanded && <ToolCallBody toolCall={toolCall} />}
</div>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

output: number;
}

function buildToolCallRecord(tc: ToolCallRecord, result: string, isError?: boolean): ToolCallRecord {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'buildToolCallRecord' is defined but never used


Unused variables are generally considered a code smell and should be avoided.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config tests Related to automated/manual tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant