feat: integration with agents

This commit is contained in:
Gab 2026-03-27 08:42:09 +11:00
parent cf023340a6
commit b9ced47bf8
11 changed files with 1341 additions and 223 deletions

View File

@ -36,6 +36,14 @@ class SyncedTool(BaseModel):
auth_via: str = "tf_proxy"
# Coder agent specific fields for prompting/model configuration
interpolation_string: Optional[str] = None
goals: Optional[str] = None
temperature: Optional[float] = None
max_tokens: Optional[int] = None
llm_base_model: Optional[str] = None
llm_provider: Optional[str] = None
class ToolSyncResult(BaseModel):
"""Result of tool sync operation."""
@ -121,7 +129,7 @@ def parse_agent(agent) -> SyncedTool:
agent: Agent from TF SDK
Returns:
SyncedTool instance
SyncedTool instance with full agent configuration
"""
return SyncedTool(
id=agent.id,
@ -130,6 +138,14 @@ def parse_agent(agent) -> SyncedTool:
tool_type=ToolType.CODER_AGENT,
is_agent_skill=False,
auth_via="tf_agent",
# Agent prompting configuration
interpolation_string=getattr(agent, 'interpolation_string', None),
goals=getattr(agent, 'goals', None),
# Agent model configuration
temperature=getattr(agent, 'temperature', None),
max_tokens=getattr(agent, 'max_tokens', None),
llm_base_model=getattr(agent, 'llm_base_model', None),
llm_provider=getattr(agent, 'llm_provider', None),
)

View File

@ -0,0 +1,310 @@
"""
End-to-end tests for ToothFairyAI agent sync and prompt injection.
Tests the entire flow from Python sync to TypeScript agent loading to prompt building.
"""
import json
import os
import tempfile
from pathlib import Path
# Test 1: Verify parse_agent extracts all required fields
def test_parse_agent_extracts_all_fields():
print("\n" + "=" * 80)
print("TEST 1: Verify parse_agent extracts all required fields")
print("=" * 80)
from tf_sync.tools import parse_agent, SyncedTool, ToolType
# Create a mock agent with all the fields we need
class MockAgent:
id = "test-agent-123"
label = "Code Reviewer"
description = "Reviews code for quality and best practices"
interpolation_string = "You are a code reviewer. Always check for bugs and suggest improvements."
goals = "Review code thoroughly. Provide actionable feedback. Ensure code quality."
temperature = 0.3
max_tokens = 4096
llm_base_model = "claude-3-5-sonnet"
llm_provider = "toothfairyai"
mode = "coder"
agent = MockAgent()
result = parse_agent(agent)
print(f"\nParsed tool:")
print(f" id: {result.id}")
print(f" name: {result.name}")
print(f" description: {result.description}")
print(f" tool_type: {result.tool_type}")
print(f" interpolation_string: {result.interpolation_string}")
print(f" goals: {result.goals}")
print(f" temperature: {result.temperature}")
print(f" max_tokens: {result.max_tokens}")
print(f" llm_base_model: {result.llm_base_model}")
print(f" llm_provider: {result.llm_provider}")
# Verify all fields are populated
assert result.id == "test-agent-123", f"Expected id='test-agent-123', got '{result.id}'"
assert result.name == "Code Reviewer", f"Expected name='Code Reviewer', got '{result.name}'"
assert result.tool_type == ToolType.CODER_AGENT, f"Expected CODER_AGENT, got {result.tool_type}"
assert result.interpolation_string == "You are a code reviewer. Always check for bugs and suggest improvements.", \
f"interpolation_string not set correctly"
assert result.goals == "Review code thoroughly. Provide actionable feedback. Ensure code quality.", \
f"goals not set correctly"
assert result.temperature == 0.3, f"Expected temperature=0.3, got {result.temperature}"
assert result.max_tokens == 4096, f"Expected max_tokens=4096, got {result.max_tokens}"
assert result.llm_base_model == "claude-3-5-sonnet", f"llm_base_model not set correctly"
assert result.llm_provider == "toothfairyai", f"llm_provider not set correctly"
print("\n✅ TEST 1 PASSED: parse_agent extracts all fields correctly")
return True
# Test 2: Verify model mapping for different llm_provider values
def test_model_mapping_for_tf_providers():
print("\n" + "=" * 80)
print("TEST 2: Verify model mapping for different llm_provider values")
print("=" * 80)
from tf_sync.tools import parse_agent, ToolType
# Test with None provider (should map to toothfairyai in TypeScript)
class MockAgentNone:
id = "test-agent-none"
label = "Agent None"
description = "Test"
interpolation_string = "Test prompt"
goals = "Test goals"
temperature = 0.7
max_tokens = 2048
llm_base_model = "gpt-4"
llm_provider = None
mode = "coder"
# Test with "toothfairyai" provider
class MockAgentTF:
id = "test-agent-tf"
label = "Agent TF"
description = "Test"
interpolation_string = "Test prompt"
goals = "Test goals"
temperature = 0.7
max_tokens = 2048
llm_base_model = "gpt-4"
llm_provider = "toothfairyai"
mode = "coder"
# Test with "tf" provider
class MockAgentTFShort:
id = "test-agent-tf-short"
label = "Agent TF Short"
description = "Test"
interpolation_string = "Test prompt"
goals = "Test goals"
temperature = 0.7
max_tokens = 2048
llm_base_model = "gpt-4"
llm_provider = "tf"
mode = "coder"
# Test with external provider (should NOT map to toothfairyai)
class MockAgentExternal:
id = "test-agent-external"
label = "Agent External"
description = "Test"
interpolation_string = "Test prompt"
goals = "Test goals"
temperature = 0.7
max_tokens = 2048
llm_base_model = "claude-3-5-sonnet"
llm_provider = "anthropic"
mode = "coder"
results = {
"None provider": parse_agent(MockAgentNone()),
"toothfairyai provider": parse_agent(MockAgentTF()),
"tf provider": parse_agent(MockAgentTFShort()),
"anthropic provider": parse_agent(MockAgentExternal()),
}
for name, result in results.items():
print(f"\n{name}:")
print(f" llm_provider: {result.llm_provider}")
print(f" llm_base_model: {result.llm_base_model}")
# The TypeScript code will check if llm_provider is None, "toothfairyai", or "tf"
# and map to toothfairyai provider. Here we just verify the values are preserved.
assert results["None provider"].llm_provider is None
assert results["toothfairyai provider"].llm_provider == "toothfairyai"
assert results["tf provider"].llm_provider == "tf"
assert results["anthropic provider"].llm_provider == "anthropic"
print("\n✅ TEST 2 PASSED: Provider values are preserved correctly for TypeScript mapping")
return True
# Test 3: Verify SyncedTool serializes correctly to JSON
def test_synced_tool_json_serialization():
print("\n" + "=" * 80)
print("TEST 3: Verify SyncedTool serializes correctly to JSON")
print("=" * 80)
from tf_sync.tools import parse_agent
class MockAgent:
id = "test-agent-json"
label = "JSON Test Agent"
description = "Test JSON serialization"
interpolation_string = "You are a JSON test agent."
goals = "Test JSON output."
temperature = 0.5
max_tokens = 8192
llm_base_model = "gpt-4-turbo"
llm_provider = "toothfairyai"
mode = "coder"
result = parse_agent(MockAgent())
# Simulate what tfcode.js does
tool_data = {
"id": result.id,
"name": result.name,
"description": result.description,
"tool_type": result.tool_type.value,
"request_type": result.request_type.value if result.request_type else None,
"url": result.url,
"auth_via": result.auth_via,
"interpolation_string": result.interpolation_string,
"goals": result.goals,
"temperature": result.temperature,
"max_tokens": result.max_tokens,
"llm_base_model": result.llm_base_model,
"llm_provider": result.llm_provider,
}
print(f"\nSerialized JSON:")
print(json.dumps(tool_data, indent=2))
# Verify all fields are present in JSON
assert tool_data["id"] == "test-agent-json"
assert tool_data["name"] == "JSON Test Agent"
assert tool_data["tool_type"] == "coder_agent"
assert tool_data["interpolation_string"] == "You are a JSON test agent."
assert tool_data["goals"] == "Test JSON output."
assert tool_data["temperature"] == 0.5
assert tool_data["max_tokens"] == 8192
assert tool_data["llm_base_model"] == "gpt-4-turbo"
assert tool_data["llm_provider"] == "toothfairyai"
print("\n✅ TEST 3 PASSED: SyncedTool serializes correctly to JSON")
return True
# Test 4: Create a mock tools.json and verify TypeScript can parse it
def test_tools_json_format():
print("\n" + "=" * 80)
print("TEST 4: Verify tools.json format matches TypeScript expectations")
print("=" * 80)
# Create a mock tools.json content
mock_tools = {
"success": True,
"tools": [
{
"id": "coder-agent-1",
"name": "Code Reviewer",
"description": "Reviews code for quality and best practices",
"tool_type": "coder_agent",
"request_type": None,
"url": None,
"auth_via": "tf_agent",
"interpolation_string": "You are a code reviewer. Your job is to review code thoroughly and provide actionable feedback.",
"goals": "Review all code changes. Identify bugs. Suggest improvements. Ensure best practices.",
"temperature": 0.3,
"max_tokens": 4096,
"llm_base_model": "claude-3-5-sonnet",
"llm_provider": "toothfairyai",
},
{
"id": "coder-agent-2",
"name": "Test Writer",
"description": "Writes comprehensive tests",
"tool_type": "coder_agent",
"request_type": None,
"url": None,
"auth_via": "tf_agent",
"interpolation_string": "You are a test writer. Write comprehensive tests for all code.",
"goals": "Write unit tests. Write integration tests. Ensure code coverage.",
"temperature": 0.5,
"max_tokens": 8192,
"llm_base_model": None,
"llm_provider": None, # Should map to toothfairyai in TypeScript
},
],
"by_type": {
"coder_agent": 2,
},
}
print(f"\nMock tools.json content:")
print(json.dumps(mock_tools, indent=2))
# Verify the structure matches what TypeScript expects
assert mock_tools["success"] == True
assert len(mock_tools["tools"]) == 2
for tool in mock_tools["tools"]:
assert "id" in tool
assert "name" in tool
assert "tool_type" in tool
assert "interpolation_string" in tool
assert "goals" in tool
assert "temperature" in tool
assert "max_tokens" in tool
assert "llm_base_model" in tool
assert "llm_provider" in tool
assert tool["tool_type"] == "coder_agent"
print("\n✅ TEST 4 PASSED: tools.json format matches TypeScript expectations")
return True
def run_all_tests():
"""Run all tests in sequence."""
print("\n" + "=" * 80)
print("RUNNING ALL PYTHON TESTS")
print("=" * 80)
tests = [
test_parse_agent_extracts_all_fields,
test_model_mapping_for_tf_providers,
test_synced_tool_json_serialization,
test_tools_json_format,
]
passed = 0
failed = 0
for test in tests:
try:
if test():
passed += 1
else:
failed += 1
except Exception as e:
print(f"\n❌ TEST FAILED: {test.__name__}")
print(f" Error: {e}")
failed += 1
print("\n" + "=" * 80)
print(f"PYTHON TEST RESULTS: {passed} passed, {failed} failed")
print("=" * 80)
return failed == 0
if __name__ == "__main__":
import sys
success = run_all_tests()
sys.exit(0 if success else 1)

View File

@ -1,62 +1,72 @@
#!/usr/bin/env node
import { spawn } from 'child_process';
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import * as readline from 'readline';
import { fileURLToPath } from 'url';
import { spawn } from "child_process"
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs"
import { join, dirname } from "path"
import { homedir } from "os"
import * as readline from "readline"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const TFCODE_DIR = join(homedir(), '.tfcode');
const TOOLS_FILE = join(TFCODE_DIR, 'tools.json');
const CREDENTIALS_FILE = join(TFCODE_DIR, 'credentials.json');
const CONFIG_FILE = join(TFCODE_DIR, 'config.json');
const TFCODE_DIR = join(homedir(), ".tfcode")
const TOOLS_FILE = join(TFCODE_DIR, "tools.json")
const CREDENTIALS_FILE = join(TFCODE_DIR, "credentials.json")
const CONFIG_FILE = join(TFCODE_DIR, "config.json")
const COLORS = {
reset: '\x1b[0m',
bold: '\x1b[1m',
green: '\x1b[32m',
red: '\x1b[31m',
cyan: '\x1b[36m',
dim: '\x1b[90m',
yellow: '\x1b[33m',
magenta: '\x1b[35m'
};
reset: "\x1b[0m",
bold: "\x1b[1m",
green: "\x1b[32m",
red: "\x1b[31m",
cyan: "\x1b[36m",
dim: "\x1b[90m",
yellow: "\x1b[33m",
magenta: "\x1b[35m",
}
function log(msg) { console.log(msg); }
function success(msg) { console.log(`${COLORS.green}${COLORS.reset} ${msg}`); }
function error(msg) { console.error(`${COLORS.red}${COLORS.reset} ${msg}`); }
function info(msg) { console.log(`${COLORS.cyan}${COLORS.reset} ${msg}`); }
function log(msg) {
console.log(msg)
}
function success(msg) {
console.log(`${COLORS.green}${COLORS.reset} ${msg}`)
}
function error(msg) {
console.error(`${COLORS.red}${COLORS.reset} ${msg}`)
}
function info(msg) {
console.log(`${COLORS.cyan}${COLORS.reset} ${msg}`)
}
function ensureConfigDir() {
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true });
if (!existsSync(TFCODE_DIR)) mkdirSync(TFCODE_DIR, { recursive: true })
}
function loadConfig() {
const envConfig = {
workspace_id: process.env.TF_WORKSPACE_ID,
api_key: process.env.TF_API_KEY,
region: process.env.TF_REGION
};
if (envConfig.workspace_id && envConfig.api_key) return envConfig;
if (existsSync(CONFIG_FILE)) {
try { return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
region: process.env.TF_REGION,
}
return null;
if (envConfig.workspace_id && envConfig.api_key) return envConfig
if (existsSync(CONFIG_FILE)) {
try {
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
} catch {}
}
return null
}
function saveConfig(config) {
ensureConfigDir();
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
ensureConfigDir()
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
}
function runPythonSync(method, config = null) {
const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || '';
const apiKey = config?.api_key || process.env.TF_API_KEY || '';
const region = config?.region || process.env.TF_REGION || 'au';
const wsId = config?.workspace_id || process.env.TF_WORKSPACE_ID || ""
const apiKey = config?.api_key || process.env.TF_API_KEY || ""
const region = config?.region || process.env.TF_REGION || "au"
const pythonCode = `
import json, sys, os
@ -77,222 +87,430 @@ try:
elif method == "sync":
config = load_config()
result = sync_tools(config)
tools_data = [{"id": t.id, "name": t.name, "description": t.description, "tool_type": t.tool_type.value, "request_type": t.request_type.value if t.request_type else None, "url": t.url, "auth_via": t.auth_via} for t in result.tools]
tools_data = [{"id": t.id, "name": t.name, "description": t.description, "tool_type": t.tool_type.value, "request_type": t.request_type.value if t.request_type else None, "url": t.url, "auth_via": t.auth_via, "interpolation_string": t.interpolation_string, "goals": t.goals, "temperature": t.temperature, "max_tokens": t.max_tokens, "llm_base_model": t.llm_base_model, "llm_provider": t.llm_provider} for t in result.tools]
print(json.dumps({"success": result.success, "tools": tools_data, "by_type": result.by_type, "error": result.error}))
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
`;
`
return new Promise((resolve, reject) => {
const proc = spawn(process.env.TFCODE_PYTHON_PATH || 'python3', ['-c', pythonCode], { env: { ...process.env } });
let stdout = '', stderr = '';
proc.stdout.on('data', (d) => stdout += d);
proc.stderr.on('data', (d) => stderr += d);
proc.on('close', (code) => {
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`));
else try { resolve(JSON.parse(stdout.trim())); } catch (e) { reject(new Error(`Parse error: ${stdout}`)); }
});
proc.on('error', reject);
});
const proc = spawn(process.env.TFCODE_PYTHON_PATH || "python3", ["-c", pythonCode], { env: { ...process.env } })
let stdout = "",
stderr = ""
proc.stdout.on("data", (d) => (stdout += d))
proc.stderr.on("data", (d) => (stderr += d))
proc.on("close", (code) => {
if (code !== 0 && !stdout) reject(new Error(`Python failed: ${stderr}`))
else
try {
resolve(JSON.parse(stdout.trim()))
} catch (e) {
reject(new Error(`Parse error: ${stdout}`))
}
})
proc.on("error", reject)
})
}
function loadCachedTools() {
if (!existsSync(TOOLS_FILE)) return null;
try { return JSON.parse(readFileSync(TOOLS_FILE, 'utf-8')); } catch { return null; }
if (!existsSync(TOOLS_FILE)) return null
try {
return JSON.parse(readFileSync(TOOLS_FILE, "utf-8"))
} catch {
return null
}
}
function saveToolsCache(tools) {
ensureConfigDir();
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2));
ensureConfigDir()
writeFileSync(TOOLS_FILE, JSON.stringify(tools, null, 2))
}
async function question(prompt) {
return new Promise((resolve) => {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question(prompt, (answer) => { rl.close(); resolve(answer.trim()); });
});
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
rl.question(prompt, (answer) => {
rl.close()
resolve(answer.trim())
})
})
}
async function select(prompt, options) {
log('');
log(prompt);
log('');
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`));
log('');
const answer = await question('Select (1-' + options.length + '): ');
const idx = parseInt(answer) - 1;
return idx >= 0 && idx < options.length ? idx : 0;
log("")
log(prompt)
log("")
options.forEach((opt, i) => log(` ${COLORS.cyan}${i + 1}.${COLORS.reset} ${opt}`))
log("")
const answer = await question("Select (1-" + options.length + "): ")
const idx = parseInt(answer) - 1
return idx >= 0 && idx < options.length ? idx : 0
}
async function interactiveSetup() {
log('');
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`);
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log('');
log('This will guide you through setting up your ToothFairyAI credentials.');
log('');
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`);
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`);
log('');
log("")
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} tfcode Setup${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log("This will guide you through setting up your ToothFairyAI credentials.")
log("")
log(`${COLORS.dim}You can find your credentials at:${COLORS.reset}`)
log(`${COLORS.dim} https://app.toothfairyai.com → Settings → API Keys${COLORS.reset}`)
log("")
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`);
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`);
log('');
const workspaceId = await question('Enter your Workspace ID: ');
if (!workspaceId) { error('Workspace ID is required'); process.exit(1); }
log('');
log(`${COLORS.bold}Step 1: Workspace ID${COLORS.reset}`)
log(`${COLORS.dim}This is your workspace UUID${COLORS.reset}`)
log("")
const workspaceId = await question("Enter your Workspace ID: ")
if (!workspaceId) {
error("Workspace ID is required")
process.exit(1)
}
log("")
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`);
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`);
log('');
const apiKey = await question('Enter your API Key: ');
if (!apiKey) { error('API Key is required'); process.exit(1); }
log('');
log(`${COLORS.bold}Step 2: API Key${COLORS.reset}`)
log(`${COLORS.dim}Paste or type your API key${COLORS.reset}`)
log("")
const apiKey = await question("Enter your API Key: ")
if (!apiKey) {
error("API Key is required")
process.exit(1)
}
log("")
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`);
const regions = ['dev (Development)', 'au (Australia)', 'eu (Europe)', 'us (United States)'];
const regionIdx = await select('Select your region:', regions);
const region = ['dev', 'au', 'eu', 'us'][regionIdx];
log(`${COLORS.bold}Step 3: Region${COLORS.reset}`)
const regions = ["dev (Development)", "au (Australia)", "eu (Europe)", "us (United States)"]
const regionIdx = await select("Select your region:", regions)
const region = ["dev", "au", "eu", "us"][regionIdx]
log('');
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log('');
log(`${COLORS.bold}Summary:${COLORS.reset}`);
log(` Workspace ID: ${workspaceId}`);
log(` API Key: ***${apiKey.slice(-4)}`);
log(` Region: ${region}`);
log('');
log("")
log(`${COLORS.bold}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log(`${COLORS.bold}Summary:${COLORS.reset}`)
log(` Workspace ID: ${workspaceId}`)
log(` API Key: ***${apiKey.slice(-4)}`)
log(` Region: ${region}`)
log("")
const confirm = await question('Save these credentials? (Y/n): ');
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') { log('Setup cancelled.'); return; }
const config = { workspace_id: workspaceId, api_key: apiKey, region };
saveConfig(config);
success('Credentials saved to ~/.tfcode/config.json');
log('');
const testNow = await question('Validate credentials now? (Y/n): ');
if (testNow.toLowerCase() === 'n' || testNow.toLowerCase() === 'no') return;
log('');
info('Validating credentials...');
log('');
try {
const result = await runPythonSync('validate', config);
if (result.success) {
success('Credentials valid!');
log(` API URL: ${result.base_url}`);
log(` Workspace ID: ${result.workspace_id}`);
log('');
const syncNow = await question('Sync tools now? (Y/n): ');
if (syncNow.toLowerCase() === 'n' || syncNow.toLowerCase() === 'no') return;
log('');
info('Syncing tools...');
log('');
const syncResult = await runPythonSync('sync', config);
if (syncResult.success) {
saveToolsCache(syncResult);
success(`Synced ${syncResult.tools.length} tools`);
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
log('');
log('By type:');
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`);
}
} else {
error(`Sync failed: ${syncResult.error}`);
}
} else {
error(`Validation failed: ${result.error}`);
}
} catch (e) {
error(`Failed: ${e.message}`);
const confirm = await question("Save these credentials? (Y/n): ")
if (confirm.toLowerCase() === "n" || confirm.toLowerCase() === "no") {
log("Setup cancelled.")
return
}
log('');
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`);
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`);
log('');
log('Commands:');
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`);
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`);
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`);
log('');
const config = { workspace_id: workspaceId, api_key: apiKey, region }
saveConfig(config)
success("Credentials saved to ~/.tfcode/config.json")
log("")
const testNow = await question("Validate credentials now? (Y/n): ")
if (testNow.toLowerCase() === "n" || testNow.toLowerCase() === "no") return
log("")
info("Validating credentials...")
log("")
try {
const result = await runPythonSync("validate", config)
if (result.success) {
success("Credentials valid!")
log(` API URL: ${result.base_url}`)
log(` Workspace ID: ${result.workspace_id}`)
log("")
const syncNow = await question("Sync tools now? (Y/n): ")
if (syncNow.toLowerCase() === "n" || syncNow.toLowerCase() === "no") return
log("")
info("Syncing tools...")
log("")
const syncResult = await runPythonSync("sync", config)
if (syncResult.success) {
saveToolsCache(syncResult)
success(`Synced ${syncResult.tools.length} tools`)
if (syncResult.by_type && Object.keys(syncResult.by_type).length > 0) {
log("")
log("By type:")
for (const [type, count] of Object.entries(syncResult.by_type)) log(` ${type}: ${count}`)
}
} else {
error(`Sync failed: ${syncResult.error}`)
}
} else {
error(`Validation failed: ${result.error}`)
}
} catch (e) {
error(`Failed: ${e.message}`)
}
log("")
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.green} Setup Complete!${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.green}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log("Commands:")
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Check credentials`)
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools`)
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List tools`)
log("")
}
function showHelp() {
log('');
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`);
log('');
log('Commands:');
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`);
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`);
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`);
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`);
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`);
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`);
log('');
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`);
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`);
log('');
log("")
log(`${COLORS.bold}tfcode${COLORS.reset} - ToothFairyAI's AI coding agent`)
log("")
log("Commands:")
log(` ${COLORS.cyan}tfcode setup${COLORS.reset} Interactive credential setup`)
log(` ${COLORS.cyan}tfcode validate${COLORS.reset} Test credentials`)
log(` ${COLORS.cyan}tfcode sync${COLORS.reset} Sync tools from workspace`)
log(` ${COLORS.cyan}tfcode tools list${COLORS.reset} List synced tools`)
log(` ${COLORS.cyan}tfcode test-agent <id>${COLORS.reset} Test agent prompt injection`)
log(` ${COLORS.cyan}tfcode debug${COLORS.reset} Show debug info`)
log(` ${COLORS.cyan}tfcode --help${COLORS.reset} Show this help`)
log(` ${COLORS.cyan}tfcode --version${COLORS.reset} Show version`)
log("")
log(`${COLORS.dim}For full TUI, run from source:${COLORS.reset}`)
log(`${COLORS.dim} bun run packages/tfcode/src/index.ts${COLORS.reset}`)
log("")
}
const args = process.argv.slice(2);
const command = args[0];
if (args.includes('--help') || args.includes('-h')) {
showHelp();
} else if (args.includes('--version') || args.includes('-v')) {
log('tfcode v1.0.0-beta.9');
} else if (command === 'setup') {
interactiveSetup();
} else if (command === 'validate') {
(async () => {
const config = loadConfig();
if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); }
info('Validating...');
try {
const result = await runPythonSync('validate', config);
if (result.success) { success('Credentials valid'); log(` API URL: ${result.base_url}`); }
else { error(`Failed: ${result.error}`); process.exit(1); }
} catch (e) { error(`Failed: ${e.message}`); process.exit(1); }
})();
} else if (command === 'sync') {
(async () => {
const config = loadConfig();
if (!config) { error('No credentials. Run: tfcode setup'); process.exit(1); }
info('Syncing tools...');
try {
const result = await runPythonSync('sync', config);
if (result.success) {
saveToolsCache(result);
success(`Synced ${result.tools.length} tools`);
if (result.by_type) { log(''); log('By type:'); for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`); }
} else { error(`Failed: ${result.error}`); process.exit(1); }
} catch (e) { error(`Failed: ${e.message}`); process.exit(1); }
})();
} else if (command === 'tools' && args[1] === 'list') {
const cached = loadCachedTools();
if (!cached?.success) { error('No tools. Run: tfcode sync'); process.exit(1); }
let tools = cached.tools;
if (args[3] === '--type' && args[4]) tools = tools.filter(t => t.tool_type === args[4]);
log(`\n${tools.length} tool(s):\n`);
for (const t of tools) {
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`);
log(` Type: ${t.tool_type}`);
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`);
log(` Auth: ${t.auth_via}\n`);
function showDebugInfo() {
log("")
log(`${COLORS.bold}Debug Information${COLORS.reset}`)
log("")
log(` ${COLORS.bold}TFCODE_DIR:${COLORS.reset} ${TFCODE_DIR}`)
log(` ${COLORS.bold}TOOLS_FILE:${COLORS.reset} ${TOOLS_FILE}`)
log(` ${COLORS.bold}CONFIG_FILE:${COLORS.reset} ${CONFIG_FILE}`)
log(` ${COLORS.bold}CREDENTIALS_FILE:${COLORS.reset} ${CREDENTIALS_FILE}`)
log("")
log(` ${COLORS.bold}tools.json exists:${COLORS.reset} ${existsSync(TOOLS_FILE)}`)
if (existsSync(TOOLS_FILE)) {
const tools = loadCachedTools()
log(` ${COLORS.bold}tools.json valid:${COLORS.reset} ${tools?.success ?? false}`)
log(` ${COLORS.bold}tools count:${COLORS.reset} ${tools?.tools?.length ?? 0}`)
const coderAgents = tools?.tools?.filter((t) => t.tool_type === "coder_agent") ?? []
log(` ${COLORS.bold}coder_agent count:${COLORS.reset} ${coderAgents.length}`)
if (coderAgents.length > 0) {
log("")
log(` ${COLORS.bold}Coder Agents:${COLORS.reset}`)
coderAgents.forEach((a) => {
log(` - ${a.name} (id: ${a.id})`)
log(` interpolation_string: ${a.interpolation_string ? "YES" : "NO"}`)
log(` goals: ${a.goals ? "YES" : "NO"}`)
log(` llm_provider: ${a.llm_provider ?? "(null)"}`)
log(` llm_base_model: ${a.llm_base_model ?? "(null)"}`)
})
}
}
log("")
}
function testAgentPrompt(agentId) {
const tools = loadCachedTools()
if (!tools?.success) {
error("No tools. Run: tfcode sync")
process.exit(1)
}
const agent = tools.tools.find((t) => t.id === agentId || t.name === agentId)
if (!agent) {
error(`Agent not found: ${agentId}`)
log("")
log("Available coder agents:")
tools.tools
.filter((t) => t.tool_type === "coder_agent")
.forEach((t) => {
log(` ${COLORS.cyan}${t.id}${COLORS.reset} - ${t.name}`)
})
process.exit(1)
}
log("")
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} Agent Data from tools.json${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log(` ${COLORS.bold}id:${COLORS.reset} ${agent.id}`)
log(` ${COLORS.bold}name:${COLORS.reset} ${agent.name}`)
log(` ${COLORS.bold}description:${COLORS.reset} ${agent.description || "(none)"}`)
log(` ${COLORS.bold}tool_type:${COLORS.reset} ${agent.tool_type}`)
log(` ${COLORS.bold}auth_via:${COLORS.reset} ${agent.auth_via}`)
log("")
log(` ${COLORS.bold}interpolation_string:${COLORS.reset}`)
if (agent.interpolation_string) {
log(` ${agent.interpolation_string.substring(0, 200)}${agent.interpolation_string.length > 200 ? "..." : ""}`)
} else {
log(` ${COLORS.dim}(none)${COLORS.reset}`)
}
log("")
log(` ${COLORS.bold}goals:${COLORS.reset}`)
if (agent.goals) {
log(` ${agent.goals.substring(0, 200)}${agent.goals.length > 200 ? "..." : ""}`)
} else {
log(` ${COLORS.dim}(none)${COLORS.reset}`)
}
log("")
log(` ${COLORS.bold}temperature:${COLORS.reset} ${agent.temperature ?? "(none)"}`)
log(` ${COLORS.bold}max_tokens:${COLORS.reset} ${agent.max_tokens ?? "(none)"}`)
log(` ${COLORS.bold}llm_base_model:${COLORS.reset} ${agent.llm_base_model ?? "(none)"}`)
log(` ${COLORS.bold}llm_provider:${COLORS.reset} ${agent.llm_provider ?? "(none)"}`)
log("")
// Build highlighted instructions
const isTFProvider = !agent.llm_provider || agent.llm_provider === "toothfairyai" || agent.llm_provider === "tf"
const hasPrompt = agent.interpolation_string && agent.interpolation_string.trim().length > 0
const hasGoals = agent.goals && agent.goals.trim().length > 0
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} Model Mapping${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
log(` ${COLORS.bold}isTFProvider:${COLORS.reset} ${isTFProvider}`)
log(
` ${COLORS.bold}mapped model:${COLORS.reset} ${isTFProvider && agent.llm_base_model ? `toothfairyai/${agent.llm_base_model}` : "(no mapping)"}`,
)
log("")
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta} Highlighted Instructions Preview${COLORS.reset}`)
log(`${COLORS.bold}${COLORS.magenta}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLORS.reset}`)
log("")
if (!hasPrompt && !hasGoals) {
log(
` ${COLORS.dim}(No interpolation_string or goals - no highlighted instructions will be generated)${COLORS.reset}`,
)
} else {
log("")
log("═══════════════════════════════════════════════════════════════════════════════")
log("⚠️ ULTRA IMPORTANT - AGENT CONFIGURATION ⚠️")
log("═══════════════════════════════════════════════════════════════════════════════")
log("")
log(`You are acting as the agent: "${agent.name}"`)
if (agent.description) {
log(`Description: ${agent.description}`)
}
log("")
log("The following instructions and goals are MANDATORY and MUST be followed")
log("with the HIGHEST PRIORITY. These override any conflicting default behaviors.")
log("═══════════════════════════════════════════════════════════════════════════════")
if (hasPrompt) {
log("")
log("┌─────────────────────────────────────────────────────────────────────────────┐")
log(`│ 🎯 AGENT "${agent.name}" INSTRUCTIONS (CRITICAL - MUST FOLLOW) │`)
log("└─────────────────────────────────────────────────────────────────────────────┘")
log("")
log(agent.interpolation_string)
}
if (hasGoals) {
log("")
log("┌─────────────────────────────────────────────────────────────────────────────┐")
log(`│ 🎯 AGENT "${agent.name}" GOALS (CRITICAL - MUST ACHIEVE) │`)
log("└─────────────────────────────────────────────────────────────────────────────┘")
log("")
log(agent.goals)
}
log("")
log("═══════════════════════════════════════════════════════════════════════════════")
log(`⚠️ END OF ULTRA IMPORTANT AGENT "${agent.name}" CONFIGURATION ⚠️`)
log("═══════════════════════════════════════════════════════════════════════════════")
}
log("")
}
const args = process.argv.slice(2)
const command = args[0]
if (args.includes("--help") || args.includes("-h")) {
showHelp()
} else if (args.includes("--version") || args.includes("-v")) {
log("tfcode v1.0.0-beta.9")
} else if (command === "setup") {
interactiveSetup()
} else if (command === "validate") {
;(async () => {
const config = loadConfig()
if (!config) {
error("No credentials. Run: tfcode setup")
process.exit(1)
}
info("Validating...")
try {
const result = await runPythonSync("validate", config)
if (result.success) {
success("Credentials valid")
log(` API URL: ${result.base_url}`)
} else {
error(`Failed: ${result.error}`)
process.exit(1)
}
} catch (e) {
error(`Failed: ${e.message}`)
process.exit(1)
}
})()
} else if (command === "sync") {
;(async () => {
const config = loadConfig()
if (!config) {
error("No credentials. Run: tfcode setup")
process.exit(1)
}
info("Syncing tools...")
try {
const result = await runPythonSync("sync", config)
if (result.success) {
saveToolsCache(result)
success(`Synced ${result.tools.length} tools`)
if (result.by_type) {
log("")
log("By type:")
for (const [t, c] of Object.entries(result.by_type)) log(` ${t}: ${c}`)
}
} else {
error(`Failed: ${result.error}`)
process.exit(1)
}
} catch (e) {
error(`Failed: ${e.message}`)
process.exit(1)
}
})()
} else if (command === "tools" && args[1] === "list") {
const cached = loadCachedTools()
if (!cached?.success) {
error("No tools. Run: tfcode sync")
process.exit(1)
}
let tools = cached.tools
if (args[2] === "--type" && args[3]) tools = tools.filter((t) => t.tool_type === args[3])
log(`\n${tools.length} tool(s):\n`)
for (const t of tools) {
log(` ${COLORS.cyan}${t.name}${COLORS.reset}`)
log(` Type: ${t.tool_type}`)
if (t.description) log(` ${COLORS.dim}${t.description.slice(0, 60)}${COLORS.reset}`)
log(` Auth: ${t.auth_via}\n`)
}
} else if (command === "test-agent") {
const agentId = args[1]
if (!agentId) {
error("Usage: tfcode test-agent <agent-id-or-name>")
process.exit(1)
}
testAgentPrompt(agentId)
} else if (command === "debug") {
showDebugInfo()
} else if (!command) {
// Show help instead of trying TUI (TUI requires full build)
showHelp();
showHelp()
} else {
error(`Unknown command: ${command}`);
showHelp();
process.exit(1);
error(`Unknown command: ${command}`)
showHelp()
process.exit(1)
}

View File

@ -8,6 +8,7 @@ import { Instance } from "../project/instance"
import { Truncate } from "../tool/truncate"
import { Auth } from "../auth"
import { ProviderTransform } from "../provider/transform"
import os from "os"
import PROMPT_GENERATE from "./generate.txt"
import PROMPT_COMPACTION from "./prompt/compaction.txt"
@ -41,6 +42,7 @@ export namespace Agent {
.optional(),
variant: z.string().optional(),
prompt: z.string().optional(),
goals: z.string().optional(),
options: z.record(z.string(), z.any()),
steps: z.number().int().positive().optional(),
})
@ -263,7 +265,8 @@ export namespace Agent {
}
async function loadTFCoderAgents(): Promise<Info[]> {
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
// tools.json is synced to ~/.tfcode/tools.json by the CLI
const toolsPath = path.join(os.homedir(), ".tfcode", "tools.json")
try {
const content = await Bun.file(toolsPath).text()
const data = JSON.parse(content)
@ -271,19 +274,33 @@ export namespace Agent {
return data.tools
.filter((t: any) => t.tool_type === "coder_agent")
.map(
(t: any): Info => ({
.map((t: any): Info => {
// Map model for ToothFairyAI providers only
// Only map when llm_provider is None, "toothfairyai", or "tf"
const isTFProvider = !t.llm_provider || t.llm_provider === "toothfairyai" || t.llm_provider === "tf"
const model =
isTFProvider && t.llm_base_model
? { modelID: t.llm_base_model as ModelID, providerID: "toothfairyai" as ProviderID }
: undefined
return {
name: t.name,
description: t.description,
mode: "primary" as const,
permission: Permission.fromConfig({ "*": "allow" }),
native: false,
prompt: t.interpolation_string,
goals: t.goals,
temperature: t.temperature,
model,
options: {
tf_agent_id: t.id,
tf_auth_via: t.auth_via,
tf_max_tokens: t.max_tokens,
},
}),
)
}
})
} catch {
return []
}

View File

@ -68,10 +68,16 @@ export namespace LLM {
const isOpenaiOauth = provider.id === "openai" && auth?.type === "oauth"
const system: string[] = []
// Build highlighted agent instructions for ToothFairyAI agents
const tfHighlightedInstructions = buildTFAgentInstructions(input.agent)
system.push(
[
// use agent prompt otherwise provider prompt
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
// highlighted TF agent instructions (if any)
...(tfHighlightedInstructions ? [tfHighlightedInstructions] : []),
// any custom prompt passed into this call
...input.system,
// any custom prompt from last user message
@ -168,7 +174,7 @@ export namespace LLM {
const maxOutputTokens =
isOpenaiOauth || provider.id.includes("github-copilot")
? undefined
: ProviderTransform.maxOutputTokens(input.model)
: (input.agent.options?.tf_max_tokens ?? ProviderTransform.maxOutputTokens(input.model))
const tools = await resolveTools(input)
@ -320,4 +326,63 @@ export namespace LLM {
}
return false
}
/**
* Build highlighted instructions for ToothFairyAI agents.
* When a TF agent is selected, its instructions and goals are highlighted
* as ULTRA IMPORTANT to ensure they are followed with highest priority.
*/
function buildTFAgentInstructions(agent: Agent.Info): string | null {
// Only highlight for ToothFairyAI agents (non-native with tf_agent_id)
const isTFAgent = !agent.native && agent.options?.tf_agent_id
if (!isTFAgent) return null
const parts: string[] = []
// Check if agent has custom prompt (interpolation_string) or goals
const hasPrompt = agent.prompt && agent.prompt.trim().length > 0
const hasGoals = agent.goals && agent.goals.trim().length > 0
if (!hasPrompt && !hasGoals) return null
parts.push("")
parts.push("═══════════════════════════════════════════════════════════════════════════════")
parts.push("⚠️ ULTRA IMPORTANT - AGENT CONFIGURATION ⚠️")
parts.push("═══════════════════════════════════════════════════════════════════════════════")
parts.push("")
parts.push(`You are acting as the agent: "${agent.name}"`)
if (agent.description) {
parts.push(`Description: ${agent.description}`)
}
parts.push("")
parts.push("The following instructions and goals are MANDATORY and MUST be followed")
parts.push("with the HIGHEST PRIORITY. These override any conflicting default behaviors.")
parts.push("═══════════════════════════════════════════════════════════════════════════════")
if (hasPrompt) {
parts.push("")
parts.push("┌─────────────────────────────────────────────────────────────────────────────┐")
parts.push(`│ 🎯 AGENT "${agent.name}" INSTRUCTIONS (CRITICAL - MUST FOLLOW) │`)
parts.push("└─────────────────────────────────────────────────────────────────────────────┘")
parts.push("")
parts.push(agent.prompt!)
}
if (hasGoals) {
parts.push("")
parts.push("┌─────────────────────────────────────────────────────────────────────────────┐")
parts.push(`│ 🎯 AGENT "${agent.name}" GOALS (CRITICAL - MUST ACHIEVE) │`)
parts.push("└─────────────────────────────────────────────────────────────────────────────┘")
parts.push("")
parts.push(agent.goals!)
}
parts.push("")
parts.push("═══════════════════════════════════════════════════════════════════════════════")
parts.push(`⚠️ END OF ULTRA IMPORTANT AGENT "${agent.name}" CONFIGURATION ⚠️`)
parts.push("═══════════════════════════════════════════════════════════════════════════════")
parts.push("")
return parts.join("\n")
}
}

View File

@ -0,0 +1,492 @@
import { afterAll, beforeAll, beforeEach, afterEach, test, expect, describe } from "bun:test"
import path from "path"
import fs from "fs"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
import { LLM } from "../../src/session/llm"
import { Provider } from "../../src/provider/provider"
import { Global } from "../../src/global"
import { Filesystem } from "../../src/util/filesystem"
import { ModelsDev } from "../../src/provider/models"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { SessionID, MessageID } from "../../src/session/schema"
import type { MessageV2 } from "../../src/session/message-v2"
// Server for capturing LLM requests
const state = {
server: null as ReturnType<typeof Bun.serve> | null,
queue: [] as Array<{ path: string; response: Response; resolve: (value: any) => void }>,
}
function deferred<T>() {
const result = {} as { promise: Promise<T>; resolve: (value: T) => void }
result.promise = new Promise((resolve) => {
result.resolve = resolve
})
return result
}
function waitRequest(pathname: string, response: Response) {
const pending = deferred<{ url: URL; headers: Headers; body: Record<string, unknown> }>()
state.queue.push({ path: pathname, response, resolve: pending.resolve })
return pending.promise
}
function createChatStream(text: string) {
const payload =
[
`data: ${JSON.stringify({
id: "chatcmpl-1",
object: "chat.completion.chunk",
choices: [{ delta: { role: "assistant" } }],
})}`,
`data: ${JSON.stringify({
id: "chatcmpl-1",
object: "chat.completion.chunk",
choices: [{ delta: { content: text } }],
})}`,
`data: ${JSON.stringify({
id: "chatcmpl-1",
object: "chat.completion.chunk",
choices: [{ delta: {}, finish_reason: "stop" }],
})}`,
"data: [DONE]",
].join("\n\n") + "\n\n"
return new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(payload))
controller.close()
},
})
}
async function loadFixture(providerID: string, modelID: string) {
const fixturePath = path.join(import.meta.dir, "../tool/fixtures/models-api.json")
const data = await Filesystem.readJson<Record<string, ModelsDev.Provider>>(fixturePath)
const provider = data[providerID]
if (!provider) throw new Error(`Missing provider in fixture: ${providerID}`)
const model = provider.models[modelID]
if (!model) throw new Error(`Missing model in fixture: ${modelID}`)
return { provider, model }
}
// Test the flow from tools.json -> Agent.Info -> highlighted instructions
describe("ToothFairyAI Agent Loading", () => {
let originalDataPath: string
beforeEach(async () => {
originalDataPath = Global.Path.data
const testDataDir = path.join(path.dirname(originalDataPath), "tf-agent-test-data")
;(Global.Path as { data: string }).data = testDataDir
await fs.promises.mkdir(path.join(testDataDir, ".tfcode"), { recursive: true })
})
afterEach(async () => {
await Instance.disposeAll()
;(Global.Path as { data: string }).data = originalDataPath
})
describe("loadTFCoderAgents", () => {
test("parses tools.json with full agent data", async () => {
const toolsData = {
success: true,
tools: [
{
id: "coder-agent-1",
name: "Code Reviewer",
description: "Reviews code for quality",
tool_type: "coder_agent",
request_type: null,
url: null,
auth_via: "tf_agent",
interpolation_string: "You are a code reviewer. Review code thoroughly.",
goals: "Identify bugs. Suggest improvements.",
temperature: 0.3,
max_tokens: 4096,
llm_base_model: "claude-3-5-sonnet",
llm_provider: "toothfairyai",
},
{
id: "coder-agent-2",
name: "Test Writer",
description: "Writes tests",
tool_type: "coder_agent",
request_type: null,
url: null,
auth_via: "tf_agent",
interpolation_string: "You are a test writer.",
goals: "Write comprehensive tests.",
temperature: 0.5,
max_tokens: null,
llm_base_model: "gpt-4",
llm_provider: null,
},
],
by_type: { coder_agent: 2 },
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData, null, 2))
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agents = await Agent.list()
const codeReviewer = agents.find((a) => a.name === "Code Reviewer")
const testWriter = agents.find((a) => a.name === "Test Writer")
expect(codeReviewer).toBeDefined()
expect(codeReviewer?.description).toBe("Reviews code for quality")
expect(codeReviewer?.prompt).toBe("You are a code reviewer. Review code thoroughly.")
expect(codeReviewer?.goals).toBe("Identify bugs. Suggest improvements.")
expect(codeReviewer?.temperature).toBe(0.3)
expect(codeReviewer?.native).toBe(false)
expect(codeReviewer?.options?.tf_agent_id).toBe("coder-agent-1")
expect(codeReviewer?.options?.tf_auth_via).toBe("tf_agent")
expect(codeReviewer?.options?.tf_max_tokens).toBe(4096)
expect(String(codeReviewer?.model?.providerID)).toBe("toothfairyai")
expect(String(codeReviewer?.model?.modelID)).toBe("claude-3-5-sonnet")
expect(testWriter).toBeDefined()
expect(String(testWriter?.model?.providerID)).toBe("toothfairyai")
expect(String(testWriter?.model?.modelID)).toBe("gpt-4")
},
})
})
test("maps tf provider to toothfairyai", async () => {
const toolsData = {
success: true,
tools: [
{
id: "tf-provider-agent",
name: "TF Provider Agent",
description: "Test",
tool_type: "coder_agent",
interpolation_string: "Test",
goals: "Test",
temperature: 0.7,
max_tokens: 2048,
llm_base_model: "test-model",
llm_provider: "tf",
},
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("TF Provider Agent")
expect(String(agent?.model?.providerID)).toBe("toothfairyai")
expect(String(agent?.model?.modelID)).toBe("test-model")
},
})
})
test("does not map external providers", async () => {
const toolsData = {
success: true,
tools: [
{
id: "external-agent",
name: "External Agent",
description: "Test",
tool_type: "coder_agent",
interpolation_string: "Test",
goals: "Test",
temperature: 0.7,
max_tokens: 2048,
llm_base_model: "claude-3-5-sonnet",
llm_provider: "anthropic",
},
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("External Agent")
expect(agent?.model).toBeUndefined()
},
})
})
test("handles agent without interpolation_string or goals", async () => {
const toolsData = {
success: true,
tools: [
{
id: "minimal-agent",
name: "Minimal Agent",
description: "Test",
tool_type: "coder_agent",
interpolation_string: null,
goals: null,
temperature: null,
max_tokens: null,
llm_base_model: null,
llm_provider: null,
},
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("Minimal Agent")
expect(agent).toBeDefined()
expect(agent?.prompt).toBeNull()
expect(agent?.goals).toBeNull()
expect(agent?.model).toBeUndefined()
},
})
})
})
})
// Separate describe block for LLM stream tests
describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
let originalDataPath: string
beforeAll(() => {
state.server = Bun.serve({
port: 0,
async fetch(req) {
const next = state.queue.shift()
if (!next) return new Response("unexpected request", { status: 500 })
const url = new URL(req.url)
const body = (await req.json()) as Record<string, unknown>
next.resolve({ url, headers: req.headers, body })
if (!url.pathname.endsWith(next.path)) return new Response("not found", { status: 404 })
return next.response
},
})
})
afterAll(() => {
state.server?.stop()
})
beforeEach(async () => {
state.queue.length = 0
originalDataPath = Global.Path.data
const testDataDir = path.join(path.dirname(originalDataPath), "tf-agent-test-data")
;(Global.Path as { data: string }).data = testDataDir
await fs.promises.mkdir(path.join(testDataDir, ".tfcode"), { recursive: true })
})
afterEach(async () => {
await Instance.disposeAll()
;(Global.Path as { data: string }).data = originalDataPath
})
test("includes highlighted TF agent instructions in system prompt", async () => {
const server = state.server
if (!server) throw new Error("Server not initialized")
const providerID = "alibaba"
const modelID = "qwen-plus"
const fixture = await loadFixture(providerID, modelID)
// Setup TF agent with this model
const toolsData = {
success: true,
tools: [
{
id: "code-reviewer-123",
name: "Code Reviewer",
description: "Reviews code for quality and best practices",
tool_type: "coder_agent",
auth_via: "tf_agent",
interpolation_string: "You are a code reviewer. Always check for bugs, security issues, and suggest improvements.",
goals: "Review all code thoroughly. Provide actionable feedback. Ensure code quality standards.",
temperature: 0.3,
max_tokens: 4096,
llm_base_model: modelID,
llm_provider: providerID,
},
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData, null, 2))
const request = waitRequest(
"/chat/completions",
new Response(createChatStream("I'll review your code."), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
}),
)
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: [providerID],
provider: {
[providerID]: {
options: {
apiKey: "test-key",
baseURL: `${server.url.origin}/v1`,
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("Code Reviewer")
expect(agent).toBeDefined()
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(modelID))
const sessionID = SessionID.make("test-session")
const user: MessageV2.User = {
id: MessageID.make("user-1"),
sessionID,
role: "user",
time: { created: Date.now() },
agent: "Code Reviewer",
model: { providerID: ProviderID.make(providerID), modelID: ModelID.make(modelID) },
}
const stream = await LLM.stream({
user,
sessionID,
model: resolved,
agent: agent!,
system: [],
abort: new AbortController().signal,
messages: [{ role: "user", content: "Hello" }],
tools: {},
})
for await (const _ of stream.fullStream) {}
const capture = await request
const body = capture.body
const messages = body.messages as Array<{ role: string; content: string }>
const systemMessage = messages.find((m) => m.role === "system")
expect(systemMessage).toBeDefined()
const systemContent = systemMessage!.content
expect(systemContent).toContain("ULTRA IMPORTANT - AGENT CONFIGURATION")
expect(systemContent).toContain('You are acting as the agent: "Code Reviewer"')
expect(systemContent).toContain("Reviews code for quality and best practices")
expect(systemContent).toContain("AGENT \"Code Reviewer\" INSTRUCTIONS")
expect(systemContent).toContain("You are a code reviewer. Always check for bugs, security issues, and suggest improvements.")
expect(systemContent).toContain("AGENT \"Code Reviewer\" GOALS")
expect(systemContent).toContain("Review all code thoroughly. Provide actionable feedback. Ensure code quality standards.")
},
})
})
test("does NOT include highlighted instructions for native agents", async () => {
const server = state.server
if (!server) throw new Error("Server not initialized")
const providerID = "alibaba"
const modelID = "qwen-plus"
const fixture = await loadFixture(providerID, modelID)
const request = waitRequest(
"/chat/completions",
new Response(createChatStream("Hello"), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
}),
)
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: [providerID],
provider: {
[providerID]: {
options: {
apiKey: "test-key",
baseURL: `${server.url.origin}/v1`,
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const agent = await Agent.get("build")
expect(agent).toBeDefined()
expect(agent?.native).toBe(true)
const resolved = await Provider.getModel(ProviderID.make(providerID), ModelID.make(modelID))
const sessionID = SessionID.make("test-session")
const user: MessageV2.User = {
id: MessageID.make("user-1"),
sessionID,
role: "user",
time: { created: Date.now() },
agent: "build",
model: { providerID: ProviderID.make(providerID), modelID: ModelID.make(modelID) },
}
const stream = await LLM.stream({
user,
sessionID,
model: resolved,
agent: agent!,
system: [],
abort: new AbortController().signal,
messages: [{ role: "user", content: "Hello" }],
tools: {},
})
for await (const _ of stream.fullStream) {}
const capture = await request
const body = capture.body
const messages = body.messages as Array<{ role: string; content: string }>
const systemMessage = messages.find((m) => m.role === "system")
expect(systemMessage).toBeDefined()
const systemContent = systemMessage!.content
expect(systemContent).not.toContain("ULTRA IMPORTANT - AGENT CONFIGURATION")
expect(systemContent).not.toContain("You are acting as the agent:")
},
})
})
})