Resolve skills during task execution
This commit is contained in:
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();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user