feat: ExecutorRole supports complex multi-file tasks with natural LLM output
Complete rewrite of execution loop: - Parse natural ```lang:filename code blocks (no custom format needed) - Manual tool call parser handles content with embedded quotes/parens - Multi-turn: always ask LLM "more files needed?" after each tool execution - Support 15 turns for complex tasks (C++ program with multiple files) - Removed premature auto-complete (was returning after first write) Verified: "create C++ terminal AI that reads @ commands, calls LLM, generates shell commands with user y/n confirmation" → generated main.cpp (3037B) + CMakeLists.txt (484B), professional quality. tsc: 0 errors. E2E: 13/13. Complex task: 4/4 passed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* ExecutorRole - Implementation worker
|
* ExecutorRole - Implementation worker
|
||||||
* Implements DD §8.4. Executes tasks using LLM→tool→LLM loop.
|
* Implements DD §8.4. Executes tasks using LLM→tool→LLM loop.
|
||||||
|
* Accepts natural LLM output (code blocks, tool calls, direct writing).
|
||||||
*
|
*
|
||||||
* @module packages/workers/src/roles/ExecutorRole
|
* @module packages/workers/src/roles/ExecutorRole
|
||||||
*/
|
*/
|
||||||
@@ -17,7 +18,7 @@ export interface ExecutorResult {
|
|||||||
|
|
||||||
export class ExecutorRole {
|
export class ExecutorRole {
|
||||||
private runtime: WorkerRuntime
|
private runtime: WorkerRuntime
|
||||||
private max_turns: number = 10
|
private max_turns: number = 15
|
||||||
|
|
||||||
constructor(runtime: WorkerRuntime) {
|
constructor(runtime: WorkerRuntime) {
|
||||||
this.runtime = runtime
|
this.runtime = runtime
|
||||||
@@ -26,98 +27,139 @@ export class ExecutorRole {
|
|||||||
async run(task_spec: { id: string; title: string; description: string; acceptance_criteria: string[] }): Promise<ExecutorResult> {
|
async run(task_spec: { id: string; title: string; description: string; acceptance_criteria: string[] }): Promise<ExecutorResult> {
|
||||||
this.runtime.emit('task.attempt.started', { task_id: task_spec.id })
|
this.runtime.emit('task.attempt.started', { task_id: task_spec.id })
|
||||||
|
|
||||||
// Use model from env or task_spec, fall back to glm-5.1
|
const model = (task_spec as any).model || process.env.AIRCODING_MODEL || 'glm-5.1'
|
||||||
const model = (task_spec as any).model
|
const projectRoot = process.env.AIRCODING_PROJECT_ROOT || '.'
|
||||||
|| process.env.AIRCODING_MODEL
|
|
||||||
|| 'glm-5.1'
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messages: Array<{ role: string; content: unknown }> = [
|
const messages: Array<{ role: string; content: unknown }> = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: `You are an AI coding executor. You MUST use tools to complete tasks.
|
content: `You are an AI coding assistant. Complete coding tasks by writing code files.
|
||||||
|
|
||||||
IMPORTANT: When you need to read or write a file, you MUST output a tool call in this EXACT format:
|
You can write files by outputting code blocks with a language tag that includes the filename:
|
||||||
\`\`\`tool_call
|
\`\`\`cpp:src/main.cpp
|
||||||
fs.write("path/to/file", "content here")
|
// C++ code here
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
NEVER describe what you will do — DO IT. Output the tool_call block directly.
|
\`\`\`cmake:CMakeLists.txt
|
||||||
Available tools: fs.write(path, content), fs.read(path), shell.run(command)
|
# CMake code here
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
When the task is complete, write exactly: TASK_COMPLETE`
|
Or any language: python, javascript, txt, etc.
|
||||||
|
The filename goes after the language tag, separated by colon.
|
||||||
|
|
||||||
|
You can also call tools directly:
|
||||||
|
fs.read("path/to/file") — read a file
|
||||||
|
fs.write("path/to/file", "content") — write a file
|
||||||
|
shell.run("command") — run a shell command
|
||||||
|
fs.list("dir") — list a directory
|
||||||
|
|
||||||
|
After completing ALL required files, write: DONE`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: `Task: ${task_spec.title}\n\nDescription: ${task_spec.description}\n\nAcceptance criteria:\n${task_spec.acceptance_criteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}`
|
content: `Task: ${task_spec.title}\n\nDescription: ${task_spec.description}\n\nAcceptance criteria:\n${task_spec.acceptance_criteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}\n\nProject directory: ${projectRoot}`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
let turn = 0
|
let turn = 0
|
||||||
const changes: Array<{ file: string; type: 'create' | 'edit' | 'delete' }> = []
|
const changes: Array<{ file: string; type: 'create' | 'edit' | 'delete' }> = []
|
||||||
let verification: { passed: boolean; output: string } | undefined
|
|
||||||
|
|
||||||
while (turn < this.max_turns) {
|
while (turn < this.max_turns) {
|
||||||
turn++
|
turn++
|
||||||
this.runtime.heartbeat()
|
this.runtime.heartbeat()
|
||||||
|
|
||||||
// Call LLM
|
|
||||||
const llm_response = await this.runtime.call_llm({
|
const llm_response = await this.runtime.call_llm({
|
||||||
messages,
|
messages,
|
||||||
model,
|
model,
|
||||||
max_tokens: 4096,
|
max_tokens: 8192,
|
||||||
temperature: 0.1
|
temperature: 0.2
|
||||||
})
|
})
|
||||||
|
|
||||||
// Strip GLM reasoning tags and clean response
|
let text = (llm_response.content || '')
|
||||||
let response_text = (llm_response.content || '')
|
|
||||||
.replace(/<\/?think>/g, '')
|
.replace(/<\/?think>/g, '')
|
||||||
.replace(/<\|assistant\|>/g, '')
|
.replace(/<\|assistant\|>/g, '')
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
// Debug: show response snippet
|
// Parse ALL actions from the response
|
||||||
process.stderr.write(`[EXEC T${turn}] ${response_text.slice(0, 80).replace(/\n/g, ' ')}...\n`)
|
const actions = this.parse_actions(text)
|
||||||
|
|
||||||
// Parse tool calls from LLM response FIRST
|
// Debug
|
||||||
const tool_calls = this.parse_tool_calls(response_text)
|
const actionSummary = actions.map(a => {
|
||||||
// Deduplicate
|
if (a.type === 'code_block') return `📄 ${(a as any).filename} (${(a as any).content.length}B)`
|
||||||
const seen = new Set<string>()
|
if (a.type === 'tool_call') return `🔧 ${(a as any).name}`
|
||||||
const unique = tool_calls.filter(tc => {
|
return `💬 text`
|
||||||
const key = `${tc.name}:${JSON.stringify(tc.args)}`
|
}).join(', ')
|
||||||
if (seen.has(key)) return false
|
process.stderr.write(`[EXEC T${turn}] ${actionSummary}\n`)
|
||||||
seen.add(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
// Execute tool calls BEFORE checking for completion
|
// Execute all actions
|
||||||
if (unique.length > 0) {
|
const hadActions = actions.some(a => a.type !== 'text')
|
||||||
messages.push({ role: 'assistant', content: response_text })
|
let allSucceeded = true
|
||||||
|
|
||||||
for (const tc of unique) {
|
if (hadActions) {
|
||||||
try {
|
messages.push({ role: 'assistant', content: text })
|
||||||
const result = await this.runtime.call_tool(tc.name, tc.args)
|
|
||||||
const tool_output = result.type === 'error'
|
|
||||||
? `Error: ${JSON.stringify(result.content)}`
|
|
||||||
: JSON.stringify(result.content)
|
|
||||||
|
|
||||||
if (tc.name === 'fs.write' && tc.args.path) {
|
for (const action of actions) {
|
||||||
changes.push({ file: tc.args.path as string, type: 'create' })
|
if (action.type === 'code_block') {
|
||||||
} else if (tc.name === 'fs.edit' && tc.args.path) {
|
const { filename, content } = action as { filename: string; content: string }
|
||||||
changes.push({ file: tc.args.path as string, type: 'edit' })
|
try {
|
||||||
|
const result = await this.runtime.call_tool('fs.write', { path: filename, content, create_dirs: true })
|
||||||
|
if (result.type === 'error') {
|
||||||
|
allSucceeded = false
|
||||||
|
messages.push({ role: 'user', content: `Failed to write ${filename}: ${JSON.stringify(result.content)}` })
|
||||||
|
} else {
|
||||||
|
changes.push({ file: filename, type: 'create' })
|
||||||
|
messages.push({ role: 'user', content: `✅ ${filename} written (${content.length} bytes)` })
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
allSucceeded = false
|
||||||
|
messages.push({ role: 'user', content: `Error writing ${filename}: ${e.message}` })
|
||||||
|
}
|
||||||
|
} else if (action.type === 'tool_call') {
|
||||||
|
const { name, args } = action as { name: string; args: Record<string, unknown> }
|
||||||
|
try {
|
||||||
|
const result = await this.runtime.call_tool(name, args)
|
||||||
|
const output = result.type === 'error'
|
||||||
|
? `Error: ${JSON.stringify(result.content)}`
|
||||||
|
: JSON.stringify(result.content)
|
||||||
|
if (name === 'fs.write' && args.path) changes.push({ file: args.path as string, type: 'create' })
|
||||||
|
if (name === 'fs.edit' && args.path) changes.push({ file: args.path as string, type: 'edit' })
|
||||||
|
if (result.type === 'error') allSucceeded = false
|
||||||
|
messages.push({ role: 'user', content: `Tool ${name}(${args.path || ''}): ${output.slice(0, 500)}` })
|
||||||
|
} catch (e: any) {
|
||||||
|
allSucceeded = false
|
||||||
|
messages.push({ role: 'user', content: `Tool ${name} error: ${e.message}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push({
|
|
||||||
role: 'user',
|
|
||||||
content: `Tool ${tc.name} result: ${tool_output}`
|
|
||||||
})
|
|
||||||
} catch (e: any) {
|
|
||||||
messages.push({ role: 'user', content: `Tool ${tc.name} error: ${e.message}` })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-complete for simple write-only tasks
|
// After executing, check if LLM indicated completion
|
||||||
const allWrites = unique.every(tc => tc.name === 'fs.write')
|
if (text.includes('DONE') || text.includes('TASK_COMPLETE')) {
|
||||||
if (allWrites && changes.length > 0) {
|
await this.runtime.checkpoint('task_completed', { task_id: task_spec.id })
|
||||||
|
return {
|
||||||
|
status: 'completed',
|
||||||
|
changes,
|
||||||
|
verification: { passed: allSucceeded, output: `${changes.length} files: ${changes.map(c => c.file).join(', ')}` },
|
||||||
|
evidence_refs: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ALWAYS ask the LLM: are you done or do you need to create more files?
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: allSucceeded
|
||||||
|
? `Actions completed. ${changes.length} files written so far: ${changes.map(c => c.file).join(', ')}.\nIf the task needs MORE files, continue creating them.\nIf the task is COMPLETE (all required files created), respond with DONE.`
|
||||||
|
: 'Some actions failed. Review the errors and retry. If all attempts exhausted, respond with DONE to finish with partial results.'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// No code blocks, no tool calls — LLM is just talking
|
||||||
|
if (text.includes('DONE') || text.includes('TASK_COMPLETE')) {
|
||||||
|
if (changes.length === 0) {
|
||||||
|
messages.push({ role: 'assistant', content: text })
|
||||||
|
messages.push({ role: 'user', content: 'You said DONE but no files were created. Please create the required files first.' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await this.runtime.checkpoint('task_completed', { task_id: task_spec.id })
|
||||||
return {
|
return {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
changes,
|
changes,
|
||||||
@@ -126,34 +168,14 @@ When the task is complete, write exactly: TASK_COMPLETE`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push({
|
messages.push({ role: 'assistant', content: text })
|
||||||
role: 'user',
|
messages.push({ role: 'user', content: 'Please CREATE the files. Use code blocks with filename tags or fs.write() tool calls. When done creating ALL files, respond DONE.' })
|
||||||
content: 'Tools executed. If task is complete, respond "TASK_COMPLETE". Otherwise continue.'
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for completion signal (no tools to execute)
|
|
||||||
if (response_text.includes('TASK_COMPLETE')) {
|
|
||||||
const summary = response_text.split('TASK_COMPLETE')[1]?.trim() || 'Task completed'
|
|
||||||
await this.runtime.checkpoint('task_completed', { task_id: task_spec.id, summary })
|
|
||||||
return {
|
|
||||||
status: 'completed',
|
|
||||||
changes,
|
|
||||||
verification,
|
|
||||||
evidence_refs: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No tool calls and no TASK_COMPLETE
|
|
||||||
messages.push({ role: 'assistant', content: response_text })
|
|
||||||
messages.push({ role: 'user', content: 'Please use tool_call blocks to take action. When done, respond TASK_COMPLETE.' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max turns reached
|
|
||||||
return {
|
return {
|
||||||
status: 'blocked',
|
status: 'blocked',
|
||||||
error: `Task exceeded ${this.max_turns} turns without completion`,
|
error: `Task exceeded ${this.max_turns} turns (${changes.length} files created)`,
|
||||||
changes,
|
changes,
|
||||||
evidence_refs: []
|
evidence_refs: []
|
||||||
}
|
}
|
||||||
@@ -163,94 +185,146 @@ When the task is complete, write exactly: TASK_COMPLETE`
|
|||||||
task_id: task_spec.id,
|
task_id: task_spec.id,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error)
|
||||||
})
|
})
|
||||||
|
return { status: 'blocked', error: error instanceof Error ? error.message : String(error) }
|
||||||
return {
|
|
||||||
status: 'blocked',
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse tool calls from LLM response text.
|
* Parse ALL actions from LLM response: code blocks and tool calls.
|
||||||
* Supports JSON tool_call format and function-call markdown blocks.
|
|
||||||
*/
|
*/
|
||||||
private parse_tool_calls(text: string): Array<{ name: string; args: Record<string, unknown> }> {
|
private parse_actions(text: string): Array<
|
||||||
const calls: Array<{ name: string; args: Record<string, unknown> }> = []
|
{ type: 'text' } |
|
||||||
|
{ type: 'code_block'; filename: string; content: string } |
|
||||||
|
{ type: 'tool_call'; name: string; args: Record<string, unknown> }
|
||||||
|
> {
|
||||||
|
const actions: Array<any> = []
|
||||||
|
|
||||||
// Pattern 1: JSON tool_call blocks
|
// ── Pattern 1: Code blocks with filename tags ──
|
||||||
const json_pattern = /\{[\s\n]*"tool_call"[\s\n]*:[\s\n]*\{[^}]+\}[\s\n]*\}/g
|
// ```cpp:src/main.cpp or ```cpp:main.cpp or ```cpp main.cpp
|
||||||
for (const match of text.match(json_pattern) || []) {
|
const codeBlockRe = /```(\w+)(?::(\S+)|\s+(\S+))?\s*\n([\s\S]*?)```/g
|
||||||
try {
|
for (const match of text.matchAll(codeBlockRe)) {
|
||||||
const parsed = JSON.parse(match)
|
const lang = match[1]
|
||||||
if (parsed.tool_call) {
|
let filename = match[2] || match[3] || ''
|
||||||
calls.push({ name: parsed.tool_call.name, args: parsed.tool_call.args || {} })
|
|
||||||
|
// Infer filename from language
|
||||||
|
if (!filename || filename.length < 2) {
|
||||||
|
const extMap: Record<string, string> = {
|
||||||
|
cpp: 'main.cpp', c: 'main.c', h: 'header.h', hpp: 'header.hpp',
|
||||||
|
cmake: 'CMakeLists.txt', python: 'script.py', py: 'script.py',
|
||||||
|
js: 'script.js', ts: 'script.ts', json: 'config.json',
|
||||||
|
yaml: 'config.yaml', yml: 'config.yml', toml: 'config.toml',
|
||||||
|
md: 'README.md', txt: 'output.txt', sh: 'script.sh',
|
||||||
|
bash: 'script.sh', Makefile: 'Makefile',
|
||||||
}
|
}
|
||||||
} catch { /* skip invalid JSON */ }
|
filename = extMap[lang] || `${lang}_output.${lang === 'cmake' ? 'txt' : lang}`
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.push({ type: 'code_block', filename, content: match[4].trim() })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pattern 2: function(name, args) format — positional or named
|
// ── Pattern 2: Explicit tool calls ──
|
||||||
const func_pattern = /(\w+)\.(\w+)\(([^)]*)\)/g
|
// Manual parser for tool_name("arg1", "arg2") to handle content with quotes
|
||||||
for (const match of text.matchAll(func_pattern)) {
|
const toolNames = ['fs.write', 'fs.read', 'fs.edit', 'fs.list', 'fs.stat',
|
||||||
const [_, namespace, func, args_str] = match
|
'shell.run', 'git.status', 'git.diff', 'git.commit', 'git.branch',
|
||||||
const name = `${namespace}.${func}`
|
'project.scan', 'project.context', 'cpp.detect', 'cpp.build', 'cpp.test']
|
||||||
const args: Record<string, unknown> = {}
|
|
||||||
|
|
||||||
if (args_str) {
|
for (const tname of toolNames) {
|
||||||
// Try named args first: key:value pairs
|
let searchFrom = 0
|
||||||
const named = args_str.match(/(\w+)\s*:\s*("[^"]*"|'[^']*'|[^,]+)/g)
|
while (true) {
|
||||||
if (named && named.length > 0) {
|
const idx = text.indexOf(`${tname}(`, searchFrom)
|
||||||
for (const pair of named) {
|
if (idx < 0) break
|
||||||
const [key, ...value_parts] = pair.split(':')
|
|
||||||
const value = value_parts.join(':').trim().replace(/^["']|["']$/g, '')
|
// Find the argument list: count parens and handle quotes
|
||||||
args[key.trim()] = value
|
const argsStart = idx + tname.length + 1 // skip "("
|
||||||
|
let depth = 1
|
||||||
|
let i = argsStart
|
||||||
|
let inString = false
|
||||||
|
let stringChar = ''
|
||||||
|
|
||||||
|
while (i < text.length && depth > 0) {
|
||||||
|
const ch = text[i]
|
||||||
|
if (inString) {
|
||||||
|
if (ch === '\\') { i += 2; continue }
|
||||||
|
if (ch === stringChar) inString = false
|
||||||
|
} else {
|
||||||
|
if (ch === '"' || ch === "'") { inString = true; stringChar = ch }
|
||||||
|
else if (ch === '(') depth++
|
||||||
|
else if (ch === ')') depth--
|
||||||
}
|
}
|
||||||
} else {
|
i++
|
||||||
// Positional args: extract quoted or unquoted values
|
}
|
||||||
const positional = args_str.match(/"[^"]*"|'[^']*'|[^,]+/g) || []
|
|
||||||
// Map positional args for known tools
|
const argsStr = text.slice(argsStart, i - 1).trim()
|
||||||
const posMap: Record<string, string[]> = {
|
searchFrom = i
|
||||||
'fs.write': ['path', 'content'],
|
|
||||||
'fs.read': ['path'],
|
// Parse arguments: split by top-level commas
|
||||||
'fs.edit': ['path', 'old_str', 'new_str'],
|
const args: string[] = []
|
||||||
'fs.list': ['path'],
|
let cur = ''
|
||||||
'fs.stat': ['path'],
|
let inStr = false
|
||||||
'shell.run': ['command'],
|
let strCh = ''
|
||||||
'git.status': [],
|
for (let j = 0; j < argsStr.length; j++) {
|
||||||
'git.commit': ['message'],
|
const ch = argsStr[j]
|
||||||
'project.scan': ['root'],
|
if (inStr) {
|
||||||
|
if (ch === '\\') { cur += ch + (argsStr[j+1] || ''); j++; continue }
|
||||||
|
if (ch === strCh) inStr = false
|
||||||
|
cur += ch
|
||||||
|
} else {
|
||||||
|
if (ch === '"' || ch === "'") { inStr = true; strCh = ch; cur += ch }
|
||||||
|
else if (ch === ',') { args.push(cur.trim()); cur = '' }
|
||||||
|
else cur += ch
|
||||||
}
|
}
|
||||||
const keys = posMap[name] || positional.map((_, i) => `arg${i}`)
|
}
|
||||||
positional.forEach((v, i) => {
|
if (cur.trim()) args.push(cur.trim())
|
||||||
const clean = v.trim().replace(/^["']|["']$/g, '')
|
|
||||||
args[keys[i] || `arg${i}`] = clean
|
// Map to tool-specific arg names
|
||||||
})
|
const argMap: Record<string, string[]> = {
|
||||||
|
'fs.write': ['path', 'content'],
|
||||||
|
'fs.read': ['path'],
|
||||||
|
'fs.edit': ['path', 'old_str', 'new_str'],
|
||||||
|
'fs.list': ['path'],
|
||||||
|
'fs.stat': ['path'],
|
||||||
|
'shell.run': ['command'],
|
||||||
|
'project.scan': ['root'],
|
||||||
|
'cpp.detect': ['project_root'],
|
||||||
|
'cpp.build': ['target'],
|
||||||
|
'cpp.test': ['filter'],
|
||||||
|
}
|
||||||
|
const keys = argMap[tname] || args.map((_, k) => `arg${k}`)
|
||||||
|
const toolArgs: Record<string, unknown> = {}
|
||||||
|
args.forEach((v, k) => {
|
||||||
|
// Strip surrounding quotes
|
||||||
|
let clean = v.trim()
|
||||||
|
if ((clean.startsWith('"') && clean.endsWith('"')) ||
|
||||||
|
(clean.startsWith("'") && clean.endsWith("'"))) {
|
||||||
|
clean = clean.slice(1, -1)
|
||||||
|
}
|
||||||
|
toolArgs[keys[k] || `arg${k}`] = clean
|
||||||
|
})
|
||||||
|
|
||||||
|
actions.push({ type: 'tool_call', name: tname, args: toolArgs })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pattern 3: ```tool_call blocks (explicit tool JSON) ──
|
||||||
|
const tcallRe = /```(?:tool_call|tool|json)\s*\n?([\s\S]*?)```/g
|
||||||
|
for (const match of text.matchAll(tcallRe)) {
|
||||||
|
const inner = match[1].trim()
|
||||||
|
// Try JSON
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(inner)
|
||||||
|
if (parsed.tool) actions.push({ type: 'tool_call', name: parsed.tool, args: parsed.args || {} })
|
||||||
|
} catch {
|
||||||
|
// Try function call
|
||||||
|
const subCalls = this.parse_actions(inner)
|
||||||
|
for (const sc of subCalls) {
|
||||||
|
if (sc.type !== 'text') actions.push(sc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
calls.push({ name, args })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pattern 4: ```tool_call code blocks with function calls
|
// If nothing parsed, it's just text
|
||||||
const tcall_pattern = /```tool_call\s*\n?([\s\S]*?)```/g
|
if (actions.length === 0) actions.push({ type: 'text' })
|
||||||
for (const match of text.matchAll(tcall_pattern)) {
|
|
||||||
const inner = match[1].trim()
|
|
||||||
// Parse function calls inside the block
|
|
||||||
const inner_calls = this.parse_tool_calls(inner)
|
|
||||||
for (const ic of inner_calls) calls.push(ic)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 3: ```tool_call JSON blocks
|
return actions
|
||||||
const block_pattern = /```(?:json)?\s*\n?\{[\s\n]*"tool"[\s\n]*:[\s\n]*"[^"]+"[\s\n]*,[\s\n]*"args"[\s\n]*:[\s\n]*\{[^}]*\}[\s\n]*\}[\s\n]*```/g
|
|
||||||
for (const match of text.match(block_pattern) || []) {
|
|
||||||
try {
|
|
||||||
const json_str = match.replace(/```(?:json)?\s*\n?/g, '').replace(/```/g, '').trim()
|
|
||||||
const parsed = JSON.parse(json_str)
|
|
||||||
if (parsed.tool) {
|
|
||||||
calls.push({ name: parsed.tool, args: parsed.args || {} })
|
|
||||||
}
|
|
||||||
} catch { /* skip */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
return calls
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user