diff --git a/AgentCompany-New/src/main/app/ipc-handlers.ts b/AgentCompany-New/src/main/app/ipc-handlers.ts index ee470bce..5c29419e 100644 --- a/AgentCompany-New/src/main/app/ipc-handlers.ts +++ b/AgentCompany-New/src/main/app/ipc-handlers.ts @@ -65,6 +65,7 @@ export function registerIpcHandlers(deps: IpcDeps): void { }); ipcMain.handle(IPC_CHANNELS.importSkillDirectory, (_event, input) => mutate(() => skills.importDirectory(input))); ipcMain.handle(IPC_CHANNELS.bindSkill, (_event, input) => mutate(() => skills.bind(input))); + ipcMain.handle(IPC_CHANNELS.resolveAgentSkills, (_event, input) => mutate(() => skills.resolveRuntimeSnapshot(input))); ipcMain.handle(IPC_CHANNELS.createSocialAccount, (_event, input) => mutate(() => social.createAccount(input))); ipcMain.handle(IPC_CHANNELS.launchSocialLogin, (_event, input) => { diff --git a/AgentCompany-New/src/main/services/skill-service.ts b/AgentCompany-New/src/main/services/skill-service.ts index 9395e278..fc33d30d 100644 --- a/AgentCompany-New/src/main/services/skill-service.ts +++ b/AgentCompany-New/src/main/services/skill-service.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { basename, join } from "node:path"; import type { DatabaseSync } from "node:sqlite"; -import type { BindSkillInput, ImportSkillDirectoryInput } from "../../shared/commands"; +import type { BindSkillInput, ImportSkillDirectoryInput, ResolveAgentSkillsInput } from "../../shared/commands"; import { createId } from "../../shared/ids"; import { AuditService } from "./audit-service"; @@ -127,4 +127,71 @@ export class SkillService { }); return id; } + + resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string { + const agent = this.db.prepare("select id from agents where id = ? and org_id = ?") + .get(input.agentId, input.orgId) as { id: string } | undefined; + if (!agent) throw new Error("Agent was not found."); + const rows = this.db.prepare(` + select + sr.id as revision_id, + sr.version_semver, + sr.manifest_json, + sa.object_key + from skill_bindings sb + join skill_revisions sr on sr.id = sb.pinned_revision_id + join skill_artifacts sa on sa.revision_id = sr.id and sa.artifact_type = 'markdown' + where sb.org_id = ? and sb.agent_id = ? and sb.enabled = 1 + order by sb.priority asc, sr.published_at asc + `).all(input.orgId, input.agentId) as unknown as RuntimeSkillRow[]; + if (rows.length === 0) throw new Error("Agent has no enabled skills."); + + const compiledParts: string[] = []; + const tools = new Set(); + const revisionIds: string[] = []; + for (const row of rows) { + if (!existsSync(row.object_key)) throw new Error(`Skill artifact is missing: ${row.object_key}`); + const manifest = JSON.parse(row.manifest_json) as SkillManifest; + for (const tool of manifest.requiredTools) tools.add(tool); + revisionIds.push(row.revision_id); + compiledParts.push([ + `# Skill: ${manifest.name}`, + `Version: ${row.version_semver}`, + readFileSync(row.object_key, "utf8").trim(), + ].join("\n\n")); + } + + const snapshotId = createId("srs"); + this.db.prepare(` + insert into skill_runtime_snapshots( + id, agent_id, run_id, compiled_prompt, tools_manifest, source_revision_ids, created_at + ) + values (?, ?, ?, ?, ?, ?, ?) + `).run( + snapshotId, + input.agentId, + input.runId, + compiledParts.join("\n\n---\n\n"), + JSON.stringify({ requiredTools: [...tools].sort() }), + JSON.stringify(revisionIds), + new Date().toISOString(), + ); + this.audit.record({ + orgId: input.orgId, + entityType: "skill_runtime_snapshot", + entityId: snapshotId, + actorType: "system", + actorId: null, + action: "resolved", + payload: { agentId: input.agentId, runId: input.runId, revisionIds }, + }); + return snapshotId; + } +} + +interface RuntimeSkillRow { + revision_id: string; + version_semver: string; + manifest_json: string; + object_key: string; } diff --git a/AgentCompany-New/src/preload/index.ts b/AgentCompany-New/src/preload/index.ts index 58fc88a3..6f66cc63 100644 --- a/AgentCompany-New/src/preload/index.ts +++ b/AgentCompany-New/src/preload/index.ts @@ -19,6 +19,7 @@ const api: DesktopApi = { createProviderResponse: (input) => invoke(IPC_CHANNELS.createProviderResponse, input), importSkillDirectory: (input) => invoke(IPC_CHANNELS.importSkillDirectory, input), bindSkill: (input) => invoke(IPC_CHANNELS.bindSkill, input), + resolveAgentSkills: (input) => invoke(IPC_CHANNELS.resolveAgentSkills, input), createSocialAccount: (input) => invoke(IPC_CHANNELS.createSocialAccount, input), launchSocialLogin: (input) => invoke(IPC_CHANNELS.launchSocialLogin, input), importFile: (input) => invoke(IPC_CHANNELS.importFile, input), diff --git a/AgentCompany-New/src/shared/commands.ts b/AgentCompany-New/src/shared/commands.ts index ee19324f..566d00ce 100644 --- a/AgentCompany-New/src/shared/commands.ts +++ b/AgentCompany-New/src/shared/commands.ts @@ -92,6 +92,12 @@ export interface BindSkillInput { revisionId: string; } +export interface ResolveAgentSkillsInput { + orgId: string; + agentId: string; + runId: string; +} + export interface CreateSocialAccountInput { orgId: string; platform: SocialPlatform; diff --git a/AgentCompany-New/src/shared/desktop-api.ts b/AgentCompany-New/src/shared/desktop-api.ts index 960ad960..68584196 100644 --- a/AgentCompany-New/src/shared/desktop-api.ts +++ b/AgentCompany-New/src/shared/desktop-api.ts @@ -12,6 +12,7 @@ import type { ImportFileInput, ImportSkillDirectoryInput, LaunchSocialLoginInput, + ResolveAgentSkillsInput, SaveProviderCredentialInput, SendMessageInput, UpdateTaskStatusInput, @@ -34,6 +35,7 @@ export interface DesktopApi { createProviderResponse(input: CreateProviderResponseInput): Promise>; importSkillDirectory(input: ImportSkillDirectoryInput): Promise>; bindSkill(input: BindSkillInput): Promise>; + resolveAgentSkills(input: ResolveAgentSkillsInput): Promise>; createSocialAccount(input: CreateSocialAccountInput): Promise>; launchSocialLogin(input: LaunchSocialLoginInput): Promise>; importFile(input: ImportFileInput): Promise>; diff --git a/AgentCompany-New/src/shared/ipc.ts b/AgentCompany-New/src/shared/ipc.ts index 433c0129..bc6b778d 100644 --- a/AgentCompany-New/src/shared/ipc.ts +++ b/AgentCompany-New/src/shared/ipc.ts @@ -11,6 +11,7 @@ export const IPC_CHANNELS = { createProviderResponse: "agentcompany:create-provider-response", importSkillDirectory: "agentcompany:import-skill-directory", bindSkill: "agentcompany:bind-skill", + resolveAgentSkills: "agentcompany:resolve-agent-skills", createSocialAccount: "agentcompany:create-social-account", launchSocialLogin: "agentcompany:launch-social-login", importFile: "agentcompany:import-file", diff --git a/AgentCompany-New/tests/unit/domain-services.test.ts b/AgentCompany-New/tests/unit/domain-services.test.ts index 8155b1c3..b5e59509 100644 --- a/AgentCompany-New/tests/unit/domain-services.test.ts +++ b/AgentCompany-New/tests/unit/domain-services.test.ts @@ -76,6 +76,8 @@ describe("domain services", () => { mkdirSync(skillDir, { recursive: true }); writeFileSync(join(skillDir, "SKILL.md"), "---\nname: review-routes\n---\n# Review Routes\n"); let orgId = ""; + let agentId = ""; + let revisionId = ""; connection.transaction(() => { orgId = organization.createOrganization({ @@ -84,13 +86,29 @@ describe("domain services", () => { mission: "Coordinate agents with auditable approvals.", goal: "Ship a verified control plane.", }); - skills.importDirectory({ orgId, directoryPath: skillDir, versionSemver: "1.0.0" }); + agentId = organization.createAgent({ + orgId, + name: "Ada", + role: "Architecture reviewer", + department: "Engineering", + modelProviderId: null, + modelId: null, + 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, "review-routes") as { id: string }; + skills.bind({ orgId, agentId, skillId: skill.id, revisionId }); + skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1" }); }); const state = snapshot.load(); expect(state.skillDefinitions[0]?.slug).toBe("review-routes"); expect(state.skillRevisions[0]?.versionSemver).toBe("1.0.0"); expect(state.skillRevisions[0]?.immutableChecksum).toHaveLength(64); + expect(state.skillRuntimeSnapshots[0]?.sourceRevisionIds).toContain(revisionId); + expect(state.skillRuntimeSnapshots[0]?.compiledPrompt).toContain("# Review Routes"); connection.close(); });