From 971bdd95e6fb4ca37437c5c88b309f521a355492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 23 May 2026 22:42:39 -0400 Subject: [PATCH] Resolve skills during task execution --- AgentCompany-New/src/main/app/ipc-handlers.ts | 2 +- .../main/services/task-execution-service.ts | 57 ++++++++++++++++++- .../tests/unit/task-execution-service.test.ts | 34 ++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index 37ac15ee..94b4b46c 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -34,8 +34,8 @@ export function registerIpcHandlers(deps: IpcDeps): void { const approvals = new ApprovalService(deps.connection.db); const chat = new ChatService(deps.connection.db); const providers = new ProviderService(deps.connection.db); - const taskExecution = new TaskExecutionService(deps.connection.db, providers); const skills = new SkillService(deps.connection.db, deps.artifactRoot); + const taskExecution = new TaskExecutionService(deps.connection.db, providers, skills); const social = new SocialService(deps.connection.db); const files = new FileService(deps.connection.db, deps.artifactRoot); const migrations = new MigrationService(deps.connection.db); diff --git a/AgentCompany-New/src/main/services/task-execution-service.ts b/AgentCompany-New/src/main/services/task-execution-service.ts index f073520a..af3e386f 100644 --- a/AgentCompany-New/src/main/services/task-execution-service.ts +++ b/AgentCompany-New/src/main/services/task-execution-service.ts @@ -4,6 +4,7 @@ import { APPROVAL_STATE, PROVIDER_EXECUTION_MODE, RISK_LEVEL } from "../../share import { createId } from "../../shared/ids"; import { AuditService } from "./audit-service"; import { ProviderService } from "./provider-service"; +import { SkillService } from "./skill-service"; const TASK_RUN_STATE = { RUNNING: "running", @@ -66,12 +67,20 @@ interface TaskApprovalRow { state: string; } +interface SkillRuntimeSnapshotRow { + id: string; + compiled_prompt: string; + tools_manifest: string; + source_revision_ids: string; +} + export class TaskExecutionService { private readonly audit: AuditService; constructor( private readonly db: DatabaseSync, private readonly providers: ProviderService = new ProviderService(db), + private readonly skills: SkillService = new SkillService(db, ""), ) { this.audit = new AuditService(db); } @@ -83,8 +92,10 @@ export class TaskExecutionService { const provider = this.requireAgentProvider(input.orgId, agent); const runId = this.createRun(input.orgId, task.id, agent.id, provider.id); let providerRequestId: string | null = null; + let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null; try { + skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(input.orgId, agent.id, runId); const requestId = await this.providers.createResponse({ orgId: input.orgId, providerId: provider.id, @@ -96,6 +107,7 @@ export class TaskExecutionService { project: this.project(task.project_id), goal: this.goal(task.goal_id), workspace: this.workspace(task.workspace_id), + skillRuntimeSnapshot, }), instructions: buildAgentInstructions(agent), executionMode: PROVIDER_EXECUTION_MODE.FOREGROUND, @@ -112,7 +124,13 @@ export class TaskExecutionService { actorType: "system", actorId: null, action: "completed", - payload: { taskId: task.id, agentId: agent.id, providerId: provider.id, providerRequestId: requestId }, + payload: { + taskId: task.id, + agentId: agent.id, + providerId: provider.id, + providerRequestId: requestId, + skillRuntimeSnapshotId: skillRuntimeSnapshot?.id ?? null, + }, }); return runId; } catch (error) { @@ -125,7 +143,14 @@ export class TaskExecutionService { actorType: "system", actorId: null, action: "failed", - payload: { taskId: task.id, agentId: agent.id, providerId: provider.id, providerRequestId, errorMessage: message }, + payload: { + taskId: task.id, + agentId: agent.id, + providerId: provider.id, + providerRequestId, + skillRuntimeSnapshotId: skillRuntimeSnapshot?.id ?? null, + errorMessage: message, + }, }); throw error; } @@ -218,6 +243,23 @@ export class TaskExecutionService { `).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, runId, orgId); } + private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string): SkillRuntimeSnapshotRow | null { + const hasEnabledSkill = this.db.prepare(` + select id from skill_bindings + where org_id = ? and agent_id = ? and enabled = 1 + limit 1 + `).get(orgId, agentId); + if (!hasEnabledSkill) return null; + const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId }); + const snapshot = this.db.prepare(` + select id, compiled_prompt, tools_manifest, source_revision_ids + from skill_runtime_snapshots + where id = ? and agent_id = ? and run_id = ? + `).get(snapshotId, agentId, runId) as SkillRuntimeSnapshotRow | undefined; + if (!snapshot) throw new Error("Skill runtime snapshot was not found."); + return snapshot; + } + private project(projectId: string | null): ProjectRow | null { if (!projectId) return null; return (this.db.prepare("select name, description from projects where id = ?").get(projectId) as unknown as ProjectRow | undefined) ?? null; @@ -250,6 +292,7 @@ function buildTaskPrompt(input: { project: ProjectRow | null; goal: GoalRow | null; workspace: WorkspaceRow | null; + skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null; }): string { const lines = [ "Execute this AgentCompany task.", @@ -274,6 +317,16 @@ function buildTaskPrompt(input: { `Ref: ${input.workspace.repo_ref}`, ); } + if (input.skillRuntimeSnapshot) { + lines.push( + "", + "Skill runtime:", + input.skillRuntimeSnapshot.compiled_prompt, + "", + `Tool allowlist: ${input.skillRuntimeSnapshot.tools_manifest}`, + `Skill revisions: ${input.skillRuntimeSnapshot.source_revision_ids}`, + ); + } lines.push("", "Deliverable: completed work, decisions made, evidence needed, and next action."); return lines.join("\n"); } diff --git a/AgentCompany-New/tests/unit/task-execution-service.test.ts b/AgentCompany-New/tests/unit/task-execution-service.test.ts index 52d7d9ca..270a4e4f 100644 --- a/AgentCompany-New/tests/unit/task-execution-service.test.ts +++ b/AgentCompany-New/tests/unit/task-execution-service.test.ts @@ -1,3 +1,4 @@ +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; import { randomUUID } from "node:crypto"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -7,6 +8,7 @@ import { ApprovalService } from "../../src/main/services/approval-service"; import { OrganizationService } from "../../src/main/services/organization-service"; import { ProviderService } from "../../src/main/services/provider-service"; import { providerAdapters, type ProviderAdapterRegistry } from "../../src/main/services/provider-adapters"; +import { SkillService } from "../../src/main/services/skill-service"; import { SnapshotService } from "../../src/main/services/snapshot-service"; import { TaskExecutionService } from "../../src/main/services/task-execution-service"; import { APPROVAL_STATE, RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain"; @@ -46,11 +48,30 @@ describe("task execution service", () => { }, }; const providers = new ProviderService(connection.db, adapters, testVault); + const skills = new SkillService(connection.db, join(tmpdir(), `agentcompany-task-skills-${randomUUID()}`)); const execution = new TaskExecutionService(connection.db, providers); + const skillDir = join(tmpdir(), `agentcompany-task-skill-${randomUUID()}`); let orgId = ""; + let agentId = ""; let providerId = ""; let taskId = ""; let workspaceId = ""; + let revisionId = ""; + + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, "SKILL.md"), "# Release Execution\n\nVerify release readiness and list concrete evidence.\n"); + writeFileSync(join(skillDir, "skill.manifest.json"), JSON.stringify({ + name: "release-execution", + version: "1.0.0", + entrypoint: "SKILL.md", + capabilities: ["release-readiness"], + required_tools: ["file_search"], + compatible_models: ["openai/*"], + risk_level: "medium", + inputs: { task: "markdown" }, + outputs: { result: "markdown" }, + tests: [], + })); connection.transaction(() => { orgId = organization.createOrganization({ @@ -67,7 +88,7 @@ describe("task execution service", () => { insert into workspaces(id, org_id, name, local_path, repo_url, repo_ref, project_id, is_primary, created_at, updated_at) values (?, ?, ?, ?, ?, ?, null, ?, ?, ?) `).run(workspaceId, orgId, "Task Workspace", "C:/work/task", "https://example.com/task.git", "main", 1, now, now); - const agentId = organization.createAgent({ + agentId = organization.createAgent({ orgId, name: "Ada", role: "Execution lead", @@ -77,6 +98,10 @@ describe("task execution service", () => { budgetCents: 10000, managerAgentId: null, }); + revisionId = skills.importDirectory({ orgId, directoryPath: skillDir, versionSemver: "1.0.0" }); + const skill = connection.db.prepare("select id from skill_definitions where org_id = ? and slug = ?") + .get(orgId, "release-execution") as { id: string }; + skills.bind({ orgId, agentId, skillId: skill.id, revisionId }); taskId = organization.createTask({ orgId, title: "Draft launch checklist", @@ -96,13 +121,20 @@ describe("task execution service", () => { const state = snapshot.load(); const run = state.taskRuns.find((item) => item.id === runId); const request = state.providerRequests.find((item) => item.id === run?.providerRequestId); + const skillRuntimeSnapshot = state.skillRuntimeSnapshots.find((item) => item.runId === runId); expect(run).toMatchObject({ taskId, agentId: expect.any(String), providerId, state: "completed", errorMessage: null }); expect(request).toMatchObject({ providerId, workspaceId, state: "succeeded", responseText: "executed:gpt-5.4" }); + expect(skillRuntimeSnapshot).toMatchObject({ agentId, runId }); + expect(skillRuntimeSnapshot?.compiledPrompt).toContain("Verify release readiness"); + expect(skillRuntimeSnapshot?.sourceRevisionIds).toContain(revisionId); expect(capturedPrompt).toContain("Task: Draft launch checklist"); expect(capturedPrompt).toContain("Assignee: Ada (Execution lead)"); expect(capturedPrompt).toContain("Workspace: Task Workspace"); + expect(capturedPrompt).toContain("Skill runtime:"); + expect(capturedPrompt).toContain("Tool allowlist:"); expect(state.auditEvents.some((event) => event.entityType === "task_run" && event.action === "completed")).toBe(true); + rmSync(skillDir, { recursive: true, force: true }); connection.close(); });