diff --git a/src/main/services/agent-run-loop.ts b/src/main/services/agent-run-loop.ts index fb1e24e5..d88b33c9 100644 --- a/src/main/services/agent-run-loop.ts +++ b/src/main/services/agent-run-loop.ts @@ -41,6 +41,11 @@ export interface AgentRunLoopDeps { onToolResults?: () => void; } +/** Every tool the model can call in a run. */ +export function runTools(externalTools: ToolDefinition[]): ToolDefinition[] { + return [...AGENT_TOOLS, ...WORKSPACE_TOOLS, ...WEB_TOOLS, ...externalTools]; +} + /** * Drives a real tool-use loop against the live provider: prompt the model with * the agent tools, execute every tool call it makes, feed results back, and @@ -48,7 +53,7 @@ export interface AgentRunLoopDeps { * iteration cap is hit (which throws rather than silently truncating). */ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopInput): Promise { - const allTools = [...AGENT_TOOLS, ...WORKSPACE_TOOLS, ...WEB_TOOLS, ...(input.externalTools ?? [])]; + const allTools = runTools(input.externalTools ?? []); const messages: AgentTurnMessage[] = [{ role: "user", text: input.initialUserTurn }]; let toolCallCount = 0; let lastRequestId = ""; diff --git a/src/main/services/task-execution-service.ts b/src/main/services/task-execution-service.ts index 617455ab..1a607c71 100644 --- a/src/main/services/task-execution-service.ts +++ b/src/main/services/task-execution-service.ts @@ -9,7 +9,7 @@ import { OrganizationService } from "./organization-service"; import { ChatService } from "./chat-service"; import { deliverChatReplyIfMissing } from "./chat-agent-wake-service"; import { SkillService } from "./skill-service"; -import { runAgentLoop } from "./agent-run-loop"; +import { runAgentLoop, runTools } from "./agent-run-loop"; import type { AgentToolContext } from "./agent-tools"; import { buildAgentInstructions, @@ -197,6 +197,7 @@ export class TaskExecutionService { handoffLines: renderTaskHandoff(this.db, orgId, task.handoff_json), knowledgeLines: relevantKnowledgeLines(this.db, { orgId, agentId: agent.id, projectId: task.project_id, goalId: task.goal_id, taskId: task.id }, `${task.title}\n${task.description}`), priorCompletedRuns: this.completedRunCount(task.id, runId), + availableToolNames: runTools(externalTools).map((tool) => tool.name), }), toolContext, externalTools, diff --git a/src/main/services/task-prompts.ts b/src/main/services/task-prompts.ts index a793ba7b..f8768797 100644 --- a/src/main/services/task-prompts.ts +++ b/src/main/services/task-prompts.ts @@ -160,6 +160,14 @@ function roleVoice(input: AgentInstructionContext): string { return "You own your craft. Ask the question early instead of guessing, do the work properly, and when you are stuck for more than a few minutes say so in the thread instead of spinning in place."; } +function skillToolLines(toolsManifest: string, available: string[]): string[] { + const { requiredTools } = JSON.parse(toolsManifest) as { requiredTools: string[] }; + const missing = requiredTools.filter((tool) => !available.includes(tool)); + if (missing.length === 0) return [""]; + console.warn(`[skills] run lacks tools its skills name: ${missing.join(", ")}`); + return ["", `Your skills name tools you do not have in this run: ${missing.join(", ")}. Work without them, and say so if that blocks the task.`]; +} + export function buildTaskPrompt(input: { task: TaskExecutionTaskRow; agent: TaskExecutionAgentRow; @@ -177,6 +185,8 @@ export function buildTaskPrompt(input: { knowledgeLines: string[]; /** Completed runs this task already spent without reaching done. */ priorCompletedRuns: number; + /** Names of the tools the model can call this run. */ + availableToolNames: string[]; }): string { const lines = [ "Execute this AgentCompany task.", @@ -214,8 +224,7 @@ export function buildTaskPrompt(input: { "", "Skills you carry this run — these are mandatory working practice, not suggestions. Apply each one to how you work and what you produce:", input.skillRuntimeSnapshot.compiled_prompt, - "", - `Tool allowlist: ${input.skillRuntimeSnapshot.tools_manifest}`, + ...skillToolLines(input.skillRuntimeSnapshot.tools_manifest, input.availableToolNames), `Skill revisions: ${input.skillRuntimeSnapshot.source_revision_ids}`, ); } diff --git a/tests/unit/task-execution-service.test.ts b/tests/unit/task-execution-service.test.ts index 5fb9a21b..968c4ad6 100644 --- a/tests/unit/task-execution-service.test.ts +++ b/tests/unit/task-execution-service.test.ts @@ -135,7 +135,8 @@ describe("task execution service", () => { expect(capturedPrompt).toContain("Execution context:"); expect(capturedPrompt).toContain("Secret scope:"); expect(capturedPrompt).toContain("Skills you carry this run"); - expect(capturedPrompt).toContain("Tool allowlist:"); + expect(capturedPrompt).not.toContain("Tool allowlist"); + expect(capturedPrompt).toContain("Your skills name tools you do not have in this run: file_search. Work without them, and say so if that blocks the task."); expect(state.auditEvents.some((event) => event.entityType === "task_run" && event.action === "completed")).toBe(true); rmSync(skillDir, { recursive: true, force: true }); connection.close();