Tools Update
This commit is contained in:
parent
dba0fdca1e
commit
2e5f3235be
|
|
@ -130,6 +130,36 @@ rojo-chat-panel {
|
|||
}
|
||||
.rcp-assistant-bubble pre code { background: none; padding: 0; }
|
||||
|
||||
/* ── Tool activity ───────────────────────────────────────── */
|
||||
|
||||
.rcp-tool-activity {
|
||||
align-self: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 5px 10px;
|
||||
border-left: 2px solid #2a3050;
|
||||
font-family: ui-monospace, "Cascadia Code", monospace;
|
||||
font-size: 0.78rem;
|
||||
color: #434a68;
|
||||
}
|
||||
|
||||
.rcp-tool-call-line {
|
||||
color: #5a6590;
|
||||
}
|
||||
|
||||
.rcp-tool-call-name {
|
||||
color: #6e7fa8;
|
||||
}
|
||||
|
||||
.rcp-tool-result-line {
|
||||
color: #3d6e45;
|
||||
}
|
||||
|
||||
.rcp-tool-result-line.error {
|
||||
color: #8b3a3a;
|
||||
}
|
||||
|
||||
/* ── Input area ──────────────────────────────────────────── */
|
||||
|
||||
.rcp-input-area {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,48 @@ function extractRawText( node: Node ): string
|
|||
return text;
|
||||
}
|
||||
|
||||
function formatToolArgs( args: Record<string, unknown> ): string
|
||||
{
|
||||
const entries = Object.entries( args );
|
||||
|
||||
if ( 0 === entries.length ) return '';
|
||||
|
||||
return entries.map( ( [ , v ] ) =>
|
||||
{
|
||||
if ( typeof v !== 'string' ) return JSON.stringify( v );
|
||||
const s = v.length > 60 ? v.slice( 0, 57 ) + '…' : v;
|
||||
return `"${ s }"`;
|
||||
} ).join( ', ' );
|
||||
}
|
||||
|
||||
function tryParseResult( raw: string ): unknown
|
||||
{
|
||||
try { return JSON.parse( raw ); }
|
||||
catch { return raw; }
|
||||
}
|
||||
|
||||
function formatToolResult( parsed: unknown ): string
|
||||
{
|
||||
if ( parsed === null || parsed === undefined ) return '✓';
|
||||
if ( typeof parsed !== 'object' ) return String( parsed );
|
||||
|
||||
const obj = parsed as Record<string, unknown>;
|
||||
|
||||
if ( true === obj.ok ) return '✓';
|
||||
|
||||
if ( Array.isArray( parsed ) ) return `${ parsed.length } items`;
|
||||
|
||||
if ( 'content' in obj && typeof obj.content === 'string' )
|
||||
{
|
||||
const preview = obj.content.slice( 0, 40 ).replace( /\n/g, ' ' );
|
||||
return `"${ preview }${ obj.content.length > 40 ? '…' : '' }"`;
|
||||
}
|
||||
|
||||
if ( 'type' in obj ) return `${ obj.type } (${ ( obj as any ).size ?? '' })`;
|
||||
|
||||
return JSON.stringify( parsed ).slice( 0, 60 );
|
||||
}
|
||||
|
||||
interface RojoEntry { id: string; name: string; description: string; path: string; }
|
||||
|
||||
function parentDir( filePath: string ): string
|
||||
|
|
@ -241,6 +283,7 @@ class RojoChatPanel extends HTMLElement
|
|||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let markdown = '';
|
||||
let pendingToolBubble: HTMLElement | null = null;
|
||||
|
||||
while ( true )
|
||||
{
|
||||
|
|
@ -256,8 +299,16 @@ class RojoChatPanel extends HTMLElement
|
|||
|
||||
try
|
||||
{
|
||||
const msg = JSON.parse( trimmed ) as { type: string; text?: string };
|
||||
if ( msg.type === 'CHAT' && msg.text )
|
||||
const msg = JSON.parse( trimmed ) as
|
||||
{
|
||||
type: string;
|
||||
text?: string;
|
||||
name?: string;
|
||||
args?: Record<string, unknown>;
|
||||
result?: string;
|
||||
};
|
||||
|
||||
if ( 'CHAT' === msg.type && msg.text )
|
||||
{
|
||||
if ( !markdown )
|
||||
{
|
||||
|
|
@ -271,6 +322,31 @@ class RojoChatPanel extends HTMLElement
|
|||
history.scrollTop = history.scrollHeight;
|
||||
} );
|
||||
}
|
||||
else if ( 'TOOL_CALL' === msg.type && msg.name )
|
||||
{
|
||||
this._stopThinking();
|
||||
pendingToolBubble = document.createElement( 'div' );
|
||||
pendingToolBubble.className = 'rcp-tool-activity';
|
||||
pendingToolBubble.innerHTML =
|
||||
`<rcp-tool-call-line class="rcp-tool-call-line">` +
|
||||
`⚙ <span class="rcp-tool-call-name">${ msg.name }</span>` +
|
||||
` ${ formatToolArgs( msg.args ?? {} ) }` +
|
||||
`</rcp-tool-call-line>`;
|
||||
history.insertBefore( pendingToolBubble, assistantBubble );
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}
|
||||
else if ( 'TOOL_RESULT' === msg.type && pendingToolBubble )
|
||||
{
|
||||
const parsed = tryParseResult( msg.result ?? '' );
|
||||
const isError = parsed && typeof parsed === 'object' && 'error' in parsed;
|
||||
const label = isError ? `✗ ${ ( parsed as any ).error }` : formatToolResult( parsed );
|
||||
const resultLine = document.createElement( 'rcp-tool-result-line' );
|
||||
resultLine.className = `rcp-tool-result-line${ isError ? ' error' : '' }`;
|
||||
resultLine.textContent = `→ ${ label }`;
|
||||
pendingToolBubble.appendChild( resultLine );
|
||||
pendingToolBubble = null;
|
||||
history.scrollTop = history.scrollHeight;
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export class FileEditorRegistry
|
|||
[
|
||||
{ suffix: 'rojo', editor: 'RojoSettingsPanel' },
|
||||
{ suffix: 'page', editor: 'PageEditorPanel' },
|
||||
{ suffix: 'html', editor: 'CodePanel' },
|
||||
{ suffix: 'js', editor: 'CodePanel' },
|
||||
{ suffix: 'ts', editor: 'CodePanel' },
|
||||
{ suffix: 'css', editor: 'CodePanel' },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { HumanMessage, AIMessage, SystemMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import { HumanMessage, AIMessage, SystemMessage, ToolMessage, BaseMessage } from "@langchain/core/messages";
|
||||
import { RojoToolContext } from "./tools/RojoTool.js";
|
||||
|
||||
export interface AgentConfig
|
||||
{
|
||||
|
|
@ -8,25 +9,27 @@ export interface AgentConfig
|
|||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
systemPrompt?: string;
|
||||
tools?: object[];
|
||||
}
|
||||
|
||||
type MessageRole = "user" | "assistant" | "system";
|
||||
type MessageData = { role: MessageRole; content: string };
|
||||
type SessionData = { id: string; messages: MessageData[] };
|
||||
export type AgentEvent =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "tool_call"; name: string; args: Record<string, unknown> }
|
||||
| { type: "tool_result"; name: string; result: string };
|
||||
|
||||
type ToolExecutor = ( name: string, args: Record<string, unknown>, ctx: RojoToolContext ) => Promise<unknown>;
|
||||
|
||||
type SessionData = { id: string; messages: BaseMessage[] };
|
||||
|
||||
const sessions = new Map<string, SessionData>();
|
||||
|
||||
function toBaseMessages( messages: MessageData[] ): BaseMessage[]
|
||||
{
|
||||
return messages.map( m =>
|
||||
{
|
||||
if ( m.role === "user" ) return new HumanMessage( m.content );
|
||||
if ( m.role === "assistant" ) return new AIMessage( m.content );
|
||||
return new SystemMessage( m.content );
|
||||
} );
|
||||
}
|
||||
|
||||
export async function getAgentStream( id: string, userMessage: string, config: AgentConfig )
|
||||
export async function* getAgentStream(
|
||||
id: string,
|
||||
userMessage: string,
|
||||
config: AgentConfig,
|
||||
toolContext?: RojoToolContext,
|
||||
toolExecutor?: ToolExecutor,
|
||||
): AsyncGenerator<AgentEvent>
|
||||
{
|
||||
const model = new ChatOpenAI(
|
||||
{
|
||||
|
|
@ -41,28 +44,91 @@ export async function getAgentStream( id: string, userMessage: string, config: A
|
|||
|
||||
if ( !sessions.has( id ) )
|
||||
{
|
||||
const messages: MessageData[] = [];
|
||||
const messages: BaseMessage[] = [];
|
||||
|
||||
if ( config.systemPrompt?.trim() )
|
||||
{
|
||||
messages.push( { role: "system", content: config.systemPrompt } );
|
||||
messages.push( new SystemMessage( config.systemPrompt ) );
|
||||
}
|
||||
|
||||
sessions.set( id, { id, messages } );
|
||||
}
|
||||
|
||||
const session = sessions.get( id )!;
|
||||
session.messages.push( { role: "user", content: userMessage } );
|
||||
const tools = config.tools ?? [];
|
||||
const boundModel = tools.length > 0 ? model.bindTools( tools ) : model;
|
||||
|
||||
return model.stream( toBaseMessages( session.messages ) );
|
||||
}
|
||||
session.messages.push( new HumanMessage( userMessage ) );
|
||||
|
||||
export function updateAgentConversation( id: string, assistantMessage: string ): void
|
||||
while ( true )
|
||||
{
|
||||
const session = sessions.get( id );
|
||||
const stream = await boundModel.stream( session.messages );
|
||||
let accumulated: any = null;
|
||||
|
||||
if ( session )
|
||||
for await ( const chunk of stream )
|
||||
{
|
||||
session.messages.push( { role: "assistant", content: assistantMessage } );
|
||||
const text = extractText( chunk.content );
|
||||
|
||||
if ( text ) yield { type: "text", text };
|
||||
|
||||
accumulated = accumulated ? accumulated.concat( chunk ) : chunk;
|
||||
}
|
||||
|
||||
const toolCalls: any[] = accumulated?.tool_calls ?? [];
|
||||
|
||||
console.log( "[RojosAgent] loop end — tool_calls:", JSON.stringify( toolCalls ),
|
||||
"| text length:", extractText( accumulated?.content ).length,
|
||||
"| accumulated null:", accumulated === null || accumulated === undefined,
|
||||
"| content type:", typeof accumulated?.content,
|
||||
"| content:", JSON.stringify( accumulated?.content )?.slice( 0, 200 ) );
|
||||
|
||||
if ( toolCalls.length > 0 && toolContext && toolExecutor )
|
||||
{
|
||||
session.messages.push( new AIMessage( { content: extractText( accumulated?.content ), tool_calls: toolCalls } ) );
|
||||
|
||||
for ( const tc of toolCalls )
|
||||
{
|
||||
yield { type: "tool_call", name: tc.name, args: tc.args };
|
||||
|
||||
let result: unknown;
|
||||
|
||||
try
|
||||
{
|
||||
result = await toolExecutor( tc.name, tc.args, toolContext );
|
||||
}
|
||||
catch ( err )
|
||||
{
|
||||
result = { error: String( err ) };
|
||||
}
|
||||
|
||||
const resultStr = typeof result === "string" ? result : JSON.stringify( result );
|
||||
|
||||
yield { type: "tool_result", name: tc.name, result: resultStr };
|
||||
|
||||
session.messages.push( new ToolMessage( { content: resultStr, tool_call_id: tc.id } ) );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const text = extractText( accumulated?.content );
|
||||
|
||||
if ( text ) session.messages.push( new AIMessage( text ) );
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractText( content: unknown ): string
|
||||
{
|
||||
if ( typeof content === "string" ) return content;
|
||||
|
||||
if ( Array.isArray( content ) )
|
||||
{
|
||||
return content
|
||||
.map( c => typeof c === "string" ? c : ( ( c as any )?.text ?? "" ) )
|
||||
.join( "" );
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import fs from "fs";
|
||||
import { RojoTool, RojoToolContext } from "./RojoTool.js";
|
||||
import { resolveProjectPath } from "../../storage.js";
|
||||
|
||||
export class CreateDirectoryTool extends RojoTool
|
||||
{
|
||||
readonly Definition =
|
||||
{
|
||||
type: "function",
|
||||
function:
|
||||
{
|
||||
name: "create_directory",
|
||||
description: "Create a directory and any missing parent directories at the given path.",
|
||||
parameters:
|
||||
{
|
||||
type: "object",
|
||||
properties:
|
||||
{
|
||||
path: { type: "string", description: "Path relative to the project root." },
|
||||
},
|
||||
required: [ "path" ],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>
|
||||
{
|
||||
const full = resolveProjectPath( ctx.projectId, args.path as string );
|
||||
|
||||
if ( !full ) return { error: "Invalid path" };
|
||||
|
||||
fs.mkdirSync( full, { recursive: true } );
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { RojoTool, RojoToolContext } from "./RojoTool.js";
|
||||
import { readProjectFile } from "../../storage.js";
|
||||
|
||||
export class GetTextContentTool extends RojoTool
|
||||
{
|
||||
readonly Definition =
|
||||
{
|
||||
type: "function",
|
||||
function:
|
||||
{
|
||||
name: "get_text_content",
|
||||
description: "Read and return the text content of a file.",
|
||||
parameters:
|
||||
{
|
||||
type: "object",
|
||||
properties:
|
||||
{
|
||||
path: { type: "string", description: "Path relative to the project root." },
|
||||
},
|
||||
required: [ "path" ],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>
|
||||
{
|
||||
const content = readProjectFile( ctx.projectId, args.path as string );
|
||||
|
||||
if ( null === content ) return { error: "File not found or unreadable" };
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import fs from "fs";
|
||||
import { RojoTool, RojoToolContext } from "./RojoTool.js";
|
||||
import { resolveProjectPath } from "../../storage.js";
|
||||
|
||||
export class ListFilesTool extends RojoTool
|
||||
{
|
||||
readonly Definition =
|
||||
{
|
||||
type: "function",
|
||||
function:
|
||||
{
|
||||
name: "list_files",
|
||||
description: "List the files and subdirectories inside a directory.",
|
||||
parameters:
|
||||
{
|
||||
type: "object",
|
||||
properties:
|
||||
{
|
||||
path: { type: "string", description: "Path relative to the project root." },
|
||||
},
|
||||
required: [ "path" ],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>
|
||||
{
|
||||
const full = resolveProjectPath( ctx.projectId, args.path as string );
|
||||
|
||||
if ( !full ) return { error: "Invalid path" };
|
||||
if ( !fs.existsSync( full ) ) return { error: "Directory does not exist" };
|
||||
|
||||
const entries = fs.readdirSync( full, { withFileTypes: true } );
|
||||
|
||||
return entries.map( e => ( { name: e.name, type: e.isDirectory() ? "directory" : "file" } ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import fs from "fs";
|
||||
import { RojoTool, RojoToolContext } from "./RojoTool.js";
|
||||
import { resolveProjectPath } from "../../storage.js";
|
||||
|
||||
export class ReadPathMetaTool extends RojoTool
|
||||
{
|
||||
readonly Definition =
|
||||
{
|
||||
type: "function",
|
||||
function:
|
||||
{
|
||||
name: "read_path_meta",
|
||||
description: "Get metadata for a file or directory: type (file/directory), size in bytes, and last-modified time.",
|
||||
parameters:
|
||||
{
|
||||
type: "object",
|
||||
properties:
|
||||
{
|
||||
path: { type: "string", description: "Path relative to the project root." },
|
||||
},
|
||||
required: [ "path" ],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>
|
||||
{
|
||||
const full = resolveProjectPath( ctx.projectId, args.path as string );
|
||||
|
||||
if ( !full ) return { error: "Invalid path" };
|
||||
if ( !fs.existsSync( full ) ) return { error: "Path does not exist" };
|
||||
|
||||
const stat = fs.statSync( full );
|
||||
|
||||
return {
|
||||
type: stat.isDirectory() ? "directory" : "file",
|
||||
size: stat.size,
|
||||
modifiedAt: stat.mtime.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export interface RojoToolContext
|
||||
{
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export abstract class RojoTool
|
||||
{
|
||||
abstract readonly Definition: object;
|
||||
abstract execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { RojoTool, RojoToolContext } from "./RojoTool.js";
|
||||
import { ReadPathMetaTool } from "./ReadPathMetaTool.js";
|
||||
import { GetTextContentTool } from "./GetTextContentTool.js";
|
||||
import { WriteTextContentTool } from "./WriteTextContentTool.js";
|
||||
import { ListFilesTool } from "./ListFilesTool.js";
|
||||
import { CreateDirectoryTool } from "./CreateDirectoryTool.js";
|
||||
|
||||
export class RojoToolRegistry
|
||||
{
|
||||
static readonly All: RojoTool[] =
|
||||
[
|
||||
new ReadPathMetaTool(),
|
||||
new GetTextContentTool(),
|
||||
new WriteTextContentTool(),
|
||||
new ListFilesTool(),
|
||||
new CreateDirectoryTool(),
|
||||
];
|
||||
|
||||
static definitions(): object[]
|
||||
{
|
||||
return RojoToolRegistry.All.map( t => t.Definition );
|
||||
}
|
||||
|
||||
static async execute( name: string, args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>
|
||||
{
|
||||
const tool = RojoToolRegistry.All.find(
|
||||
t => ( t.Definition as any ).function?.name === name
|
||||
);
|
||||
|
||||
if ( !tool ) return { error: `Unknown tool: ${ name }` };
|
||||
|
||||
return tool.execute( args, ctx );
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { RojoTool, RojoToolContext } from "./RojoTool.js";
|
||||
import { writeProjectFile } from "../../storage.js";
|
||||
|
||||
export class WriteTextContentTool extends RojoTool
|
||||
{
|
||||
readonly Definition =
|
||||
{
|
||||
type: "function",
|
||||
function:
|
||||
{
|
||||
name: "write_text_content",
|
||||
description: "Write text content to a file, creating it if it does not exist or overwriting it if it does.",
|
||||
parameters:
|
||||
{
|
||||
type: "object",
|
||||
properties:
|
||||
{
|
||||
path: { type: "string", description: "Path relative to the project root." },
|
||||
text: { type: "string", description: "The text content to write." },
|
||||
},
|
||||
required: [ "path", "text" ],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>
|
||||
{
|
||||
const ok = writeProjectFile( ctx.projectId, args.path as string, args.text as string );
|
||||
|
||||
if ( !ok ) return { error: "Could not write file" };
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,8 @@ import fs from "fs";
|
|||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
import { requireAuth } from "../../auth-connector/source/server/auth";
|
||||
import { getAgentStream, updateAgentConversation, AgentConfig } from "../rojos/RojosAgent";
|
||||
import { getAgentStream, AgentConfig } from "../rojos/RojosAgent";
|
||||
import { RojoToolRegistry } from "../rojos/tools/RojoToolRegistry";
|
||||
import { readProjectFile, writeProjectFile, createProjectDirectory } from "../storage";
|
||||
import { checkAccess } from "../projectAccess";
|
||||
import { RojosConfig } from "../rojos/RojosConfig";
|
||||
|
|
@ -210,6 +211,7 @@ router.post( "/chat", async ( req, res ) =>
|
|||
model: RojosConfig.model,
|
||||
apiKey: RojosConfig.apiKey,
|
||||
systemPrompt: "",
|
||||
tools: projectId ? RojoToolRegistry.definitions() : [],
|
||||
};
|
||||
|
||||
if ( projectId && rojoPath )
|
||||
|
|
@ -250,47 +252,39 @@ router.post( "/chat", async ( req, res ) =>
|
|||
}
|
||||
}
|
||||
|
||||
const toolContext = projectId ? { projectId } : undefined;
|
||||
|
||||
try
|
||||
{
|
||||
const agentStream = await getAgentStream( id, message, config );
|
||||
const agentStream = getAgentStream(
|
||||
id,
|
||||
message,
|
||||
config,
|
||||
toolContext,
|
||||
toolContext ? RojoToolRegistry.execute.bind( RojoToolRegistry ) : undefined,
|
||||
);
|
||||
|
||||
res.setHeader( "Content-Type", "text/event-stream" );
|
||||
res.setHeader( "Cache-Control", "no-cache" );
|
||||
res.setHeader( "Connection", "keep-alive" );
|
||||
res.flushHeaders();
|
||||
|
||||
const collected: string[] = [];
|
||||
|
||||
for await ( const chunk of agentStream )
|
||||
for await ( const event of agentStream )
|
||||
{
|
||||
const content = chunk.content;
|
||||
let text = "";
|
||||
|
||||
if ( typeof content === "string" )
|
||||
if ( "text" === event.type )
|
||||
{
|
||||
text = content;
|
||||
res.write( JSON.stringify( { type: "CHAT", text: event.text } ) + "\n" );
|
||||
}
|
||||
else if ( Array.isArray( content ) )
|
||||
else if ( "tool_call" === event.type )
|
||||
{
|
||||
for ( const c of content )
|
||||
res.write( JSON.stringify( { type: "TOOL_CALL", name: event.name, args: event.args } ) + "\n" );
|
||||
}
|
||||
else if ( "tool_result" === event.type )
|
||||
{
|
||||
if ( typeof c === "string" ) text += c;
|
||||
else if ( c && typeof c === "object" && "text" in c ) text += ( c as any ).text;
|
||||
res.write( JSON.stringify( { type: "TOOL_RESULT", name: event.name, result: event.result } ) + "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
if ( text.length > 0 )
|
||||
{
|
||||
collected.push( text );
|
||||
res.write( JSON.stringify( { type: "CHAT", text } ) + "\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
res.write( JSON.stringify( { type: "THINKING" } ) + "\n" );
|
||||
}
|
||||
}
|
||||
|
||||
updateAgentConversation( id, collected.join( "" ) );
|
||||
res.write( JSON.stringify( { type: "DONE" } ) + "\n" );
|
||||
res.end();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ export function createProjectDirectory(projectId: string, dirPath: string): bool
|
|||
return true;
|
||||
}
|
||||
|
||||
export function resolveProjectPath( projectId: string, filePath: string ): string | null
|
||||
{
|
||||
return safeResolve( projectId, filePath );
|
||||
}
|
||||
|
||||
export function projectPathExists(projectId: string, filePath: string): boolean {
|
||||
const full = safeResolve(projectId, filePath);
|
||||
return !!full && fs.existsSync(full);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ var NAV_DATA = {
|
|||
{ title: 'rokojori-auth Restructure', path: 'outline/auth-restructure.html' },
|
||||
{ title: 'Auth Connector Rewrite', path: 'outline/auth-connector-rewrite.html' },
|
||||
{ title: 'User-Based Local Tunneling', path: 'outline/tunneling.html' },
|
||||
{ title: 'Rojo Chat — LLM Tools', path: 'outline/rojo-chat-tools.html' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -23,11 +23,22 @@
|
|||
<div class="lane-header">To Do</div>
|
||||
|
||||
<task-item class="blue hide-content">
|
||||
<task-title>Tab-container: split function broken, panel border update unreliable</task-title>
|
||||
<task-title>Rojo Chat: LLM Tools</task-title>
|
||||
<task-content>
|
||||
The tab-container split function does not work correctly in its current state.
|
||||
Additionally, the update and moving of panel borders is not always applied —
|
||||
borders can appear stuck or misaligned after panel resize or split operations.
|
||||
Allow users to define and use tools in rojo-chat-panel. Tools extend the LLM
|
||||
with callable functions (e.g. file read, web search, custom actions). The chat
|
||||
panel should support declaring a tool set, passing tool definitions to the model,
|
||||
handling tool-call responses, and feeding results back into the conversation.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
<task-item class="blue hide-content">
|
||||
<task-title>Rojo Chat: Claude API</task-title>
|
||||
<task-content>
|
||||
Integrate the Claude API (Anthropic) as a provider option in rojo-chat-panel.
|
||||
Allows testing and using Claude models alongside the existing OpenAI-compatible
|
||||
provider. Covers API key configuration, model selection, and verifying streaming
|
||||
and tool-use work end-to-end with the Anthropic SDK.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
|
|
@ -227,6 +238,16 @@
|
|||
<div class="lane">
|
||||
<div class="lane-header">Done</div>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Tab-container: split function broken, panel border update unreliable</task-title>
|
||||
<task-content>
|
||||
Split rebuilt as a Split > submenu with ↔ Horizontally and ↕ Vertically entries.
|
||||
Section resize observer switched from watching the workspace element to watching
|
||||
each .es-sections element directly — redistribution now fires correctly when a
|
||||
panel is dragged, fixing stuck or misaligned borders.
|
||||
</task-content>
|
||||
</task-item>
|
||||
|
||||
<task-item class="green hide-content">
|
||||
<task-title>Mobile: nav bar z-index too low on projects / index view</task-title>
|
||||
<task-content>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
[ HTML Editor Panel Update ]
|
||||
This update gives the panel a clearer direction:
|
||||
|
||||
Provide a custom subset of html for the main documentation format inside Roject. It should be readable as normal html file on computers and
|
||||
can be part of a webpage directory, which can be defined in css/js/headers. This will be a later part when needed but should be
|
||||
planned with. Since the js part can be critical, only special js will be used when displaying the pages in the editor, effectively stripping
|
||||
away any code.
|
||||
|
||||
[ Naming Changes ]
|
||||
- HTML Editor Panel should be renamed to PageEditorPanel. This change should be done for all involved code.
|
||||
- This editor is no longer responsible for html suffixes.
|
||||
- This editor is now responsible for page suffixes.
|
||||
|
||||
[ Editing Changes ]
|
||||
The editor will use a template for the html document with a standardized layout:
|
||||
|
||||
The <body> will have this:
|
||||
|
||||
<page-header></page-header>
|
||||
|
||||
<page-root>
|
||||
|
||||
<page-block>
|
||||
|
||||
<page-area>
|
||||
|
||||
</page-area>
|
||||
|
||||
</page-block>
|
||||
|
||||
</page-root>
|
||||
|
||||
<page-footer></page-footer>
|
||||
|
||||
|
||||
Whenever a .page file is opened that does not follow this format, the editor needs to reject the editing and fallback to normal text editing.
|
||||
|
||||
[ Blocks and Areas ]
|
||||
Blocks (<page-block>) are full width html element templates with a specific structural layout.
|
||||
They could be something like 3 rectangles arranged in two halfs, 2 rectangles vertically aligned for images
|
||||
and one rectangle on the right for text. Blocks can replaced and areas inside can be changed, but the
|
||||
structure of a block is not changable.
|
||||
|
||||
Areas (<page-area>) are containers for text, images and other inline elements.
|
||||
The allow rich text editing, from asigning font style attributes (color, font-family, font-weight, italic etc)
|
||||
|
||||
|
||||
[ Headers and footers ]
|
||||
Headers <page-header> and Footers <page-footer> will be there, but no features will be added yet.
|
||||
|
||||
|
||||
|
||||
[ UI ]
|
||||
The ui should have two icons on the left which switch between blocks and areas.
|
||||
Blocks will feature a horizontal scrollable list of elements (canvas/html previews), while areas have buttons
|
||||
for formating the selected text.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
[ Tab Container Update ]
|
||||
The menu item "Split" will be a menu instead of an option and allows now to split horizontally (the implementated function) and to split vertically.
|
||||
|
||||
So the new menu points would be
|
||||
|
||||
Split >
|
||||
↔ Horizontally
|
||||
↕ Vertically
|
||||
|
||||
|
||||
The vertical split was planned from the beginning but somehow never implemented properly
|
||||
|
||||
[ Tab Container Closing ]
|
||||
Currently there's no way to close a split tab container. That's why it needs under "Split" the option "Close Container", which will be available unless the container is the last in either the left, center or right slot - so that there's always at least one container in each
|
||||
of them.
|
||||
|
||||
Additionally to the menu point, another interaction should be added: Middle mouse closing. When an editor tab is clicked with middle mouse
|
||||
it should close.
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rojo Chat — LLM Tools — Roject</title>
|
||||
<link rel="stylesheet" href="../_assets_/styles.css">
|
||||
<link rel="stylesheet" href="../_assets_/nav.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<header>
|
||||
<p class="date">Plan — To Do</p>
|
||||
<h1>Rojo Chat — LLM Tools</h1>
|
||||
<p class="subtitle">
|
||||
Extend the Rojo Chat agent with a built-in tool set — file system read/write operations
|
||||
the model can call during a conversation. Generic across any OpenAI-compatible provider
|
||||
(Claude, Gemini, Mistral, local models).
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>What it is</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
LLM tool use (also called function calling) lets a model request that the server
|
||||
run a named function and return the result, then continue generating text using
|
||||
that result as context. The model never executes code directly — it emits a
|
||||
structured tool call, the server runs it, and the result is fed back as a message.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
For Rojo Chat this unlocks agentic workflows: a user can ask the Rojo to
|
||||
"create a new page at docs/intro and add a heading and some intro text" and the
|
||||
model will call <code>create_directory</code>, then <code>write_text_content</code>,
|
||||
then reply with a summary — all in a single conversation turn.
|
||||
</p>
|
||||
<p style="margin-top:0.75rem">
|
||||
The tool format used is the OpenAI function-calling standard, which is supported
|
||||
by all major providers at their OpenAI-compatible endpoints:
|
||||
Claude (<code>api.anthropic.com/v1</code>),
|
||||
Gemini (<code>generativelanguage.googleapis.com/v1beta/openai/</code>),
|
||||
Mistral (<code>api.mistral.ai/v1</code>), and local models via Ollama or LM Studio.
|
||||
No provider-specific branching is needed in the implementation.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Built-in Tools — First Set</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
All tools operate on the current project's file system, scoped to
|
||||
<code>storage/<projectId>/root/</code>. Paths are always relative to
|
||||
the project root. Access checks already in the chat route ensure only authorised
|
||||
users can trigger tool execution.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>read_path_meta</h3>
|
||||
<p>Get metadata for a path — whether it is a file or directory, size in bytes, and last-modified time.</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{
|
||||
"name": "read_path_meta",
|
||||
"description": "Get metadata for a file or directory: type (file/directory), size in bytes, and last-modified time.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path relative to the project root." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}</pre>
|
||||
<p>Returns: <code>{ type: "file"|"directory", size: number, modifiedAt: string }</code></p>
|
||||
<p style="margin-top:0.5rem">Server implementation: <code>fs.stat()</code></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>get_text_content</h3>
|
||||
<p>Read and return the text content of a file.</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{
|
||||
"name": "get_text_content",
|
||||
"description": "Read and return the text content of a file.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path relative to the project root." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}</pre>
|
||||
<p>Returns: file content as a string.</p>
|
||||
<p style="margin-top:0.5rem">Server implementation: <code>readProjectFile()</code> — already in the storage layer.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>write_text_content</h3>
|
||||
<p>Write text to a file, creating it if it does not exist or overwriting it if it does.</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{
|
||||
"name": "write_text_content",
|
||||
"description": "Write text content to a file, creating or overwriting it.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path relative to the project root." },
|
||||
"text": { "type": "string", "description": "The text content to write." }
|
||||
},
|
||||
"required": ["path", "text"]
|
||||
}
|
||||
}</pre>
|
||||
<p>Returns: <code>{ ok: true }</code> on success.</p>
|
||||
<p style="margin-top:0.5rem">Server implementation: <code>writeProjectFile()</code> — already in the storage layer.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>list_files</h3>
|
||||
<p>List the entries (files and subdirectories) inside a directory.</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{
|
||||
"name": "list_files",
|
||||
"description": "List the files and subdirectories inside a directory.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path relative to the project root." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}</pre>
|
||||
<p>Returns: <code>{ name: string, type: "file"|"directory" }[]</code></p>
|
||||
<p style="margin-top:0.5rem">Server implementation: <code>fs.readdir(dir, { withFileTypes: true })</code></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>create_directory</h3>
|
||||
<p>Create a directory (and any missing parent directories) at the given path.</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{
|
||||
"name": "create_directory",
|
||||
"description": "Create a directory and any missing parent directories at the given path.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": { "type": "string", "description": "Path relative to the project root." }
|
||||
},
|
||||
"required": ["path"]
|
||||
}
|
||||
}</pre>
|
||||
<p>Returns: <code>{ ok: true }</code> on success.</p>
|
||||
<p style="margin-top:0.5rem">Server implementation: <code>fs.mkdir(path, { recursive: true })</code></p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>The Tool Execution Loop</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
The current <code>RojosAgent.ts</code> calls <code>model.stream(messages)</code>
|
||||
once and streams text back. With tools, the agent must loop until the model
|
||||
produces a final text reply:
|
||||
</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">loop:
|
||||
stream from model.bindTools(tools)
|
||||
collect full response
|
||||
|
||||
if response contains tool calls:
|
||||
for each tool call:
|
||||
→ execute tool on server
|
||||
→ emit TOOL_CALL event to client
|
||||
→ emit TOOL_RESULT event to client
|
||||
add AIMessage (tool calls) + ToolMessages (results) to history
|
||||
continue loop
|
||||
|
||||
if response is text:
|
||||
stream text chunks to client as CHAT events
|
||||
add AIMessage to history
|
||||
break</pre>
|
||||
<p style="margin-top:0.75rem">
|
||||
<code>getAgentStream</code> in <code>RojosAgent.ts</code> becomes an
|
||||
<code>async function*</code> generator that yields typed events instead of
|
||||
raw LangChain chunks. The route handler in <code>rojos.ts</code> iterates
|
||||
the generator and serialises each event to the wire protocol.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Event types emitted by the generator</h3>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{ type: "text", text: string }
|
||||
{ type: "tool_call", name: string, args: Record<string, unknown> }
|
||||
{ type: "tool_result", name: string, result: string }</pre>
|
||||
<p style="margin-top:0.75rem">
|
||||
These map to the existing NDJSON wire protocol in <code>rojos.ts</code>:
|
||||
</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">{ "type": "CHAT", "text": "..." } // unchanged
|
||||
{ "type": "TOOL_CALL", "name": "...", "args": {...} } // new
|
||||
{ "type": "TOOL_RESULT","name": "...", "result": "..." } // new
|
||||
{ "type": "DONE" } // unchanged</pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Tool context</h3>
|
||||
<p>
|
||||
Every tool execution receives a <code>RojoToolContext</code> carrying the
|
||||
<code>projectId</code> and the resolved <code>projectRoot</code> path
|
||||
(<code>storage/<projectId>/root/</code>). Tools use this to scope
|
||||
all file system operations — no tool ever receives an absolute path from
|
||||
the model.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>File Structure</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
Tools live in their own directory. Each tool is a self-contained class so
|
||||
it is easy to review, test, and extend independently. A registry class owns
|
||||
the full list and dispatches execution by name.
|
||||
</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">source/server/rojos/
|
||||
RojosAgent.ts — modified: generator loop, tool binding
|
||||
RojosConfig.ts — unchanged
|
||||
tools/
|
||||
RojoTool.ts — abstract base class + RojoToolContext interface
|
||||
RojoToolRegistry.ts — holds all tool instances, dispatches execute()
|
||||
ReadPathMetaTool.ts
|
||||
GetTextContentTool.ts
|
||||
WriteTextContentTool.ts
|
||||
ListFilesTool.ts
|
||||
CreateDirectoryTool.ts</pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>RojoTool base class</h3>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">export interface RojoToolContext
|
||||
{
|
||||
projectId: string;
|
||||
projectRoot: string;
|
||||
}
|
||||
|
||||
export abstract class RojoTool
|
||||
{
|
||||
abstract Definition: object;
|
||||
abstract execute( args: Record<string, unknown>, ctx: RojoToolContext ): Promise<unknown>;
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>RojoToolRegistry</h3>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">export class RojoToolRegistry
|
||||
{
|
||||
static readonly All: RojoTool[] = [
|
||||
new ReadPathMetaTool(),
|
||||
new GetTextContentTool(),
|
||||
new WriteTextContentTool(),
|
||||
new ListFilesTool(),
|
||||
new CreateDirectoryTool(),
|
||||
];
|
||||
|
||||
static definitions(): object[]
|
||||
{
|
||||
return RojoToolRegistry.All.map( t => t.Definition );
|
||||
}
|
||||
|
||||
static execute( name: string, args: Record<string, unknown>, ctx: RojoToolContext )
|
||||
{
|
||||
const tool = RojoToolRegistry.All.find( t => ( t.Definition as any ).function?.name === name );
|
||||
if ( !tool ) return Promise.resolve( { error: `Unknown tool: ${ name }` } );
|
||||
return tool.execute( args, ctx );
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Changes to Existing Files</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>source/server/rojos/RojosAgent.ts</h3>
|
||||
<ul style="line-height:1.9;margin-top:0.5rem">
|
||||
<li><code>AgentConfig</code> gets an optional <code>tools: object[]</code> field.</li>
|
||||
<li><code>getAgentStream</code> becomes an <code>async function*</code> generator
|
||||
yielding typed events instead of raw LangChain chunks.</li>
|
||||
<li>When <code>config.tools</code> is non-empty, the model is created with
|
||||
<code>model.bindTools(config.tools)</code> and the loop handles tool call chunks.</li>
|
||||
<li><code>updateAgentConversation</code> receives the full collected
|
||||
<code>AIMessage</code> (including tool calls) and the subsequent
|
||||
<code>ToolMessage</code> array so history stays consistent.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>source/server/routes/rojos.ts — POST /chat</h3>
|
||||
<ul style="line-height:1.9;margin-top:0.5rem">
|
||||
<li>Passes <code>RojoToolRegistry.definitions()</code> into <code>AgentConfig.tools</code>
|
||||
when a <code>projectId</code> is present (so the tool context can be resolved).</li>
|
||||
<li>Passes a <code>RojoToolContext</code> into the generator so tools can be executed.</li>
|
||||
<li>Serialises the new <code>tool_call</code> and <code>tool_result</code> event types
|
||||
to the existing NDJSON wire format.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>source/components/rojo-chat-panel/rojo-chat-panel.ts</h3>
|
||||
<ul style="line-height:1.9;margin-top:0.5rem">
|
||||
<li>Handles <code>TOOL_CALL</code> messages: renders a small dim activity line
|
||||
showing the tool name and arguments (collapsed by default).</li>
|
||||
<li>Handles <code>TOOL_RESULT</code> messages: appends the result status (ok / error)
|
||||
to the same activity line.</li>
|
||||
<li>Visual style: compact, clearly distinct from user and assistant bubbles —
|
||||
not the focus, just informational.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Verification Test</h2>
|
||||
|
||||
<div class="card">
|
||||
<p>
|
||||
After implementation, the following single-message conversation should work
|
||||
end-to-end with any capable model via an external or tunnel endpoint:
|
||||
</p>
|
||||
<pre style="margin:0.75rem 0;padding:0.75rem;background:#0a0c13;border-radius:6px;font-size:0.8rem;line-height:1.7;overflow-x:auto">User: "Create a directory at docs/intro and write an index.page
|
||||
file there with a page header and a short intro paragraph."
|
||||
|
||||
Expected sequence:
|
||||
⚙ create_directory("docs/intro") → ok
|
||||
⚙ write_text_content("docs/intro/index.page", "...") → ok
|
||||
Assistant: "Done. I created docs/intro and wrote index.page with ..."</pre>
|
||||
<p style="margin-top:0.75rem">
|
||||
This test exercises two sequential tool calls in one agent loop iteration,
|
||||
confirming that history is correctly updated between calls and that the model
|
||||
can compose multiple operations from a single natural-language instruction.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Phased Implementation</h2>
|
||||
|
||||
<div class="card" style="border-left: 3px solid #ffb300;">
|
||||
<h3>Phase 1 — Backend tools + loop</h3>
|
||||
<ul style="line-height:1.9">
|
||||
<li>Create <code>source/server/rojos/tools/</code> directory</li>
|
||||
<li>Write <code>RojoTool.ts</code> (abstract base + context interface)</li>
|
||||
<li>Write one class per tool: <code>ReadPathMetaTool</code>, <code>GetTextContentTool</code>,
|
||||
<code>WriteTextContentTool</code>, <code>ListFilesTool</code>, <code>CreateDirectoryTool</code></li>
|
||||
<li>Write <code>RojoToolRegistry.ts</code></li>
|
||||
<li>Rewrite <code>getAgentStream</code> as an async generator with the tool execution loop</li>
|
||||
<li>Wire tools into the <code>POST /chat</code> route</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border-left: 3px solid #ffb300;">
|
||||
<h3>Phase 2 — Frontend display</h3>
|
||||
<ul style="line-height:1.9">
|
||||
<li>Handle <code>TOOL_CALL</code> and <code>TOOL_RESULT</code> message types in
|
||||
<code>rojo-chat-panel.ts</code></li>
|
||||
<li>Render compact activity lines for each tool call / result</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Phase 3 — Future extensions</h3>
|
||||
<ul style="line-height:1.9">
|
||||
<li>Per-rojo tool configuration: enable/disable specific tools per rojo</li>
|
||||
<li>Additional tools: rename, delete, move files</li>
|
||||
<li>User-defined tools: name + description + server-side handler registration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
Roject — Rojo Chat LLM tools plan
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<script>var NAV_ROOT = '../';</script>
|
||||
<script src="../_assets_/nav-data.js"></script>
|
||||
<script src="../_assets_/nav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue