Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/hooks/useAgentChat.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useState } from 'react';
import { classifyIntent } from '@/tools/agent/intent';
import { executorFor, AGENT_EXECUTORS } from '@/tools/agent/executors';
import { buildSystemPrompt, parseAction, type LoopTool } from '@/tools/agent/loop.lib';
import { buildSystemPrompt, parseAction, recoverContentAction, type LoopTool } from '@/tools/agent/loop.lib';
import { emptySession, recordUser, applyResolution, historyForPrompt } from '@/tools/agent/session.lib';
import { prefillUrl } from '@/tools/agent/router.lib';
import { buildToolChoicePrompt, parseToolChoice } from '@/tools/agent/select.lib';
Expand Down Expand Up @@ -140,7 +140,10 @@ export function useAgentChat(provider: AgentProvider | null) {
let produced = false;
for (let iter = 0; iter < 8; iter++) {
const raw = await provider.chat(convo);
const act = parseAction(raw);
// Generative tools (svg/canvas) often emit the artifact itself instead of
// a JSON action — a big SVG/code blob rarely survives JSON-escaping — so
// recover it and run the right tool.
const act = parseAction(raw) ?? recoverContentAction(raw, offered.map(e => e.toolId));
if (!act) {
// Unparseable turn (small models emit junk). If we already handed the
// user a result, just close out cleanly — never dump raw JSON to chat.
Expand Down
21 changes: 20 additions & 1 deletion src/tools/agent/loop.lib.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { buildSystemPrompt, parseAction, type LoopTool } from './loop.lib';
import { buildSystemPrompt, parseAction, recoverContentAction, type LoopTool } from './loop.lib';

const TOOLS: LoopTool[] = [
{ name: 'base64_encode', description: 'Encode text to Base64', args: [{ name: 'text', type: 'string', required: true }] },
Expand Down Expand Up @@ -42,3 +42,22 @@ describe('parseAction', () => {
expect(parseAction('{"action":"weird"}')).toBeNull();
});
});

describe('recoverContentAction', () => {
it('recovers an svg-viewer call when the model outputs raw <svg>', () => {
const r = recoverContentAction('Here you go:\n<svg viewBox="0 0 10 10"><rect/></svg>', ['svg-viewer', 'qr-gen']);
expect(r?.action).toBe('call_tool');
expect(r?.action === 'call_tool' && r.tool).toBe('svg-viewer');
expect(r?.action === 'call_tool' && String(r.args.svg)).toContain('<svg');
});
it('recovers a canvas-draw call from a js code block', () => {
const r = recoverContentAction('```js\nctx.fillRect(0,0,5,5)\n```', ['canvas-draw']);
expect(r?.action === 'call_tool' && r.tool).toBe('canvas-draw');
});
it('returns null when the matching tool is not offered', () => {
expect(recoverContentAction('<svg></svg>', ['qr-gen'])).toBeNull();
});
it('returns null when there is no artifact', () => {
expect(recoverContentAction('just chatting here', ['svg-viewer', 'canvas-draw'])).toBeNull();
});
});
17 changes: 17 additions & 0 deletions src/tools/agent/loop.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,20 @@ export function parseAction(raw: string): LoopAction | null {
}
return null;
}

/**
* Recover a content-producing tool call when the model emitted the artifact
* itself — an `<svg>` or a ```js code block — instead of a JSON action. A big
* SVG/code blob rarely survives being embedded (and JSON-escaped) inside the
* action protocol, so models tend to just output it; this routes that raw output
* to the right generative tool. Only fires for tools that are actually offered.
*/
export function recoverContentAction(raw: string, offeredToolIds: string[]): LoopAction | null {
if (offeredToolIds.includes('svg-viewer') && /<svg[\s>][\s\S]*<\/svg>/i.test(raw)) {
return { action: 'call_tool', tool: 'svg-viewer', args: { svg: raw } };
}
if (offeredToolIds.includes('canvas-draw') && /```(?:js|javascript)\b/i.test(raw)) {
return { action: 'call_tool', tool: 'canvas-draw', args: { code: raw } };
}
return null;
}
Loading