feat: agents integration

This commit is contained in:
Gab 2026-03-27 08:52:47 +11:00
parent b9ced47bf8
commit 16bdb0707f
4 changed files with 125 additions and 67 deletions

View File

@ -286,18 +286,18 @@ export namespace Agent {
return {
name: t.name,
description: t.description,
description: t.description ?? undefined,
mode: "primary" as const,
permission: Permission.fromConfig({ "*": "allow" }),
native: false,
prompt: t.interpolation_string,
goals: t.goals,
temperature: t.temperature,
prompt: t.interpolation_string ?? undefined,
goals: t.goals ?? undefined,
temperature: t.temperature ?? undefined,
model,
options: {
tf_agent_id: t.id,
tf_auth_via: t.auth_via,
tf_max_tokens: t.max_tokens,
tf_max_tokens: t.max_tokens ?? undefined,
},
}
})

View File

@ -34,6 +34,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
const tfFallbackModel = createMemo(() => {
const m = { providerID: "toothfairyai", modelID: "mystica-15" }
if (isModelValid(m)) return m
return undefined
})
const agent = iife(() => {
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const visibleAgents = createMemo(() => sync.data.agent.filter((x) => !x.hidden))
@ -192,10 +198,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const currentModel = createMemo(() => {
const a = agent.current()
const isTFAgent = !!a.options?.tf_agent_id
return (
getFirstValidModel(
() => modelStore.model[a.name],
() => a.model,
() => (isTFAgent ? tfFallbackModel() : undefined),
fallbackModel,
) ?? undefined
)
@ -297,6 +305,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
})
},
setDefault(model: { providerID: string; modelID: string }) {
setModelStore("model", agent.current().name, model)
},
toggleFavorite(model: { providerID: string; modelID: string }) {
batch(() => {
if (!isModelValid(model)) {
@ -381,19 +392,24 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
// Automatically update model when agent changes
createEffect(() => {
const value = agent.current()
const isTFAgent = !!value.options?.tf_agent_id
if (value.model) {
if (isModelValid(value.model))
if (isModelValid(value.model)) {
model.set({
providerID: value.model.providerID,
modelID: value.model.modelID,
})
else
} else if (isTFAgent) {
// For TF agents with invalid model, force default to toothfairyai/mystica-15
model.setDefault({ providerID: "toothfairyai", modelID: "mystica-15" })
} else {
toast.show({
variant: "warning",
message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
duration: 3000,
})
}
}
})
const result = {

View File

@ -116,11 +116,10 @@ export namespace LLM {
mergeDeep(variant),
)
// Remove TF-specific options for non-ToothFairyAI providers
if (input.model.providerID !== "toothfairyai") {
// Remove TF-specific internal tracking fields (never passed to APIs)
delete options.tf_agent_id
delete options.tf_auth_via
}
delete options.tf_max_tokens
if (isOpenaiOauth) {
options.instructions = system.join("\n")

View File

@ -1,6 +1,7 @@
import { afterAll, beforeAll, beforeEach, afterEach, test, expect, describe } from "bun:test"
import path from "path"
import fs from "fs"
import os from "os"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Agent } from "../../src/agent/agent"
@ -13,6 +14,8 @@ import { ProviderID, ModelID } from "../../src/provider/schema"
import { SessionID, MessageID } from "../../src/session/schema"
import type { MessageV2 } from "../../src/session/message-v2"
const TF_TOOLS_PATH = path.join(os.homedir(), ".tfcode", "tools.json")
// Server for capturing LLM requests
const state = {
server: null as ReturnType<typeof Bun.serve> | null,
@ -75,17 +78,34 @@ async function loadFixture(providerID: string, modelID: string) {
describe("ToothFairyAI Agent Loading", () => {
let originalDataPath: string
let originalToolsContent: string | null = null
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 })
// Backup existing tools.json if it exists
try {
originalToolsContent = await Bun.file(TF_TOOLS_PATH).text()
} catch {
originalToolsContent = null
}
await fs.promises.mkdir(path.dirname(TF_TOOLS_PATH), { recursive: true })
})
afterEach(async () => {
await Instance.disposeAll()
;(Global.Path as { data: string }).data = originalDataPath
// Restore original tools.json
if (originalToolsContent !== null) {
await fs.promises.writeFile(TF_TOOLS_PATH, originalToolsContent)
} else {
try {
await fs.promises.unlink(TF_TOOLS_PATH)
} catch {}
}
})
describe("loadTFCoderAgents", () => {
@ -127,7 +147,7 @@ describe("ToothFairyAI Agent Loading", () => {
by_type: { coder_agent: 2 },
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
const toolsPath = TF_TOOLS_PATH
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData, null, 2))
await using tmp = await tmpdir()
@ -177,7 +197,7 @@ describe("ToothFairyAI Agent Loading", () => {
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
const toolsPath = TF_TOOLS_PATH
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
await using tmp = await tmpdir()
@ -211,7 +231,7 @@ describe("ToothFairyAI Agent Loading", () => {
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
const toolsPath = TF_TOOLS_PATH
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
await using tmp = await tmpdir()
@ -244,7 +264,7 @@ describe("ToothFairyAI Agent Loading", () => {
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
const toolsPath = TF_TOOLS_PATH
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData))
await using tmp = await tmpdir()
@ -254,8 +274,8 @@ describe("ToothFairyAI Agent Loading", () => {
fn: async () => {
const agent = await Agent.get("Minimal Agent")
expect(agent).toBeDefined()
expect(agent?.prompt).toBeNull()
expect(agent?.goals).toBeNull()
expect(agent?.prompt).toBeUndefined()
expect(agent?.goals).toBeUndefined()
expect(agent?.model).toBeUndefined()
},
})
@ -266,6 +286,7 @@ describe("ToothFairyAI Agent Loading", () => {
// Separate describe block for LLM stream tests
describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
let originalDataPath: string
let originalToolsContent: string | null = null
beforeAll(() => {
state.server = Bun.serve({
@ -291,12 +312,28 @@ describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
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 })
// Backup existing tools.json if it exists
try {
originalToolsContent = await Bun.file(TF_TOOLS_PATH).text()
} catch {
originalToolsContent = null
}
await fs.promises.mkdir(path.dirname(TF_TOOLS_PATH), { recursive: true })
})
afterEach(async () => {
await Instance.disposeAll()
;(Global.Path as { data: string }).data = originalDataPath
// Restore original tools.json
if (originalToolsContent !== null) {
await fs.promises.writeFile(TF_TOOLS_PATH, originalToolsContent)
} else {
try {
await fs.promises.unlink(TF_TOOLS_PATH)
} catch {}
}
})
test("includes highlighted TF agent instructions in system prompt", async () => {
@ -317,7 +354,8 @@ describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
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.",
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,
@ -327,8 +365,7 @@ describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
],
}
const toolsPath = path.join(Global.Path.data, ".tfcode", "tools.json")
await fs.promises.writeFile(toolsPath, JSON.stringify(toolsData, null, 2))
await fs.promises.writeFile(TF_TOOLS_PATH, JSON.stringify(toolsData, null, 2))
const request = waitRequest(
"/chat/completions",
@ -386,7 +423,8 @@ describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
tools: {},
})
for await (const _ of stream.fullStream) {}
for await (const _ of stream.fullStream) {
}
const capture = await request
const body = capture.body
@ -400,10 +438,14 @@ describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
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.")
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.",
)
},
})
})
@ -473,7 +515,8 @@ describe("ToothFairyAI Agent Instructions in LLM Stream", () => {
tools: {},
})
for await (const _ of stream.fullStream) {}
for await (const _ of stream.fullStream) {
}
const capture = await request
const body = capture.body