Resolve skills during task execution

This commit is contained in:
ZacharyZhang-NY committed 2026-05-23 22:42:39 -04:00
1 parent ddd3b09bd1
commit 971bdd95e6
3 files changed
+89 -4

No files matched your search

@@ -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);
@@ -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");
}
@@ -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();
});