diff --git a/apps/web/tests/integration/expert-manager.test.mjs b/apps/web/tests/integration/expert-manager.test.mjs index aa55cd31b1..a57d72e3f5 100644 --- a/apps/web/tests/integration/expert-manager.test.mjs +++ b/apps/web/tests/integration/expert-manager.test.mjs @@ -6,6 +6,7 @@ import { dirname, join } from 'node:path'; import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; import { Context } from '@deepseek-ai/cordis'; +import { createScope } from '@deepseek-ai/dsh-scope'; import Tools from '@deepseek-ai/dsh-tools'; import { registerExpertManagementTools } from '../../../../packages/plugins/experts/dist/tools/management-tools.js'; import { FileSystem, LocalFileSystem, WorkingDirectory, createMessage, Agents, AgentLoop, Sessions, Projections, SystemPrompt, Llm, skillTool, SkillRequestAdapter } from '../helpers/skill-runtime.mjs'; @@ -1031,6 +1032,43 @@ test('expert presets default to the persisted DSH_HOME when DSH_AGENTS_HOME is a } finally { await h.cleanup(); } }); +test('publish and the execution guard read $DSH_HOME/agents when DSH_AGENTS_HOME is unset', async () => { + const h = await boot(undefined, { profileAgents: true }); + try { + const a = actor('owner-a'); + const doc = name => `---\nname: ${name}\ndescription: Team professional\n---\n# ${name}\n\nFull domain reasoning and professional delivery.`; + const files = { + '.workdsh-expert/plugin.json': JSON.stringify({ name: 'home-team', expertType: 'team', agentName: 'home-team-team-lead', agents: ['./agents/home-team-team-lead.md', './agents/analyst.md', './agents/reviewer.md'], teamInfo: { leadAgent: 'home-team-team-lead', memberAgents: ['analyst', 'reviewer'] }, workflows: [{ id: 'report', title: 'Report', trigger: 'Produce a report', deliverable: 'Verified report', stages: [{ id: 'draft', worker: 'analyst', reviewer: 'reviewer', dependsOn: [] }] }] }), + 'settings.json': JSON.stringify({ agent: 'home-team-team-lead' }), + 'agents/home-team-team-lead.md': doc('home-team-team-lead'), + 'agents/analyst.md': doc('analyst'), + 'agents/reviewer.md': doc('reviewer'), + 'README.md': 'Agents home regression.', + }; + const draft = await h.experts.createDraft(a, definitionFromDocuments(files), { operationId: 'home-team-create' }); + const check = await h.experts.validate(a, draft.expertId, draft.revision); + assert.equal(check.publishable, true, JSON.stringify(check.issues)); + const request = await h.experts.requestPublishConfirmation(a, draft.expertId, draft.revision); + const proof = await h.experts.confirmPublish(a, request.confirmationToken); + const receipt = await h.experts.publish(a, draft.expertId, draft.revision, check.dependencyLockDigest, proof, { operationId: 'home-team-publish' }); + const publishedDir = expertPresetDir(receipt.presetRevisionRef, h.agentsHome); + assert.equal(expertPresetDir(receipt.presetRevisionRef), publishedDir); + assert.equal(await readExpertPreset(receipt.presetRevisionRef), await readFile(join(publishedDir, 'preset.json'), 'utf8')); + const plan = await h.experts.prepareExecution(a, draft.expertId); + const created = await h.experts.createExecution(a, plan.executionPlanId, { operationId: 'home-team-exec' }); + h.ctx.workdshIdentity.resolve = async () => a; + await h.ctx.plugin(SystemPrompt); + const agentScope = createScope(h.ctx, created.sessionId); + registerExpertExecutionGuard(h.ctx); + const entered = await h.ctx.waterfall('agent/pre-step', { + agent: { id: created.sessionId, ctx: agentScope.ctx, session: { id: created.sessionId, header: { agentPreset: created.binding.presetRevisionRef, cwd: h.root } } }, + messages: [], turn: 1, step: 1, signal: new AbortController().signal, + }, async () => ({ kind: 'enter', messages: [] })); + assert.equal(entered.kind, 'enter'); + await agentScope.dispose(); + } finally { await h.cleanup(); } +}); + test('one missing expert preset does not disable the manager after a cold restart', async () => { const root = await mkdtemp(join(tmpdir(), 'workdsh-broken-expert-')); let h = await boot(root); diff --git a/packages/plugins/experts/src/runtime/execution-guard.ts b/packages/plugins/experts/src/runtime/execution-guard.ts index 17c10a21c1..9227971d16 100644 --- a/packages/plugins/experts/src/runtime/execution-guard.ts +++ b/packages/plugins/experts/src/runtime/execution-guard.ts @@ -3,17 +3,16 @@ import type { Agent } from '@deepseek-ai/dsh-agent'; import type {} from '@deepseek-ai/dsh-experimental-agent-team'; import * as Persona from '@deepseek-ai/dsh-persona'; import * as SkillFiles from '@deepseek-ai/dsh-skill-filesystem'; -import { expertPresetDir, readExpertPreset } from './preset-compiler.js'; +import { expertPersonaConfig, expertPresetDir, readExpertPreset, resolveAgentsHome } from './preset-compiler.js'; import { join } from 'node:path'; import { ExpertsError } from '../domain/values.js'; -import { expertPersonaConfig } from './preset-compiler.js'; /** Asset admission and role composition only. No child driver, mailbox or Team state. */ export function registerExpertExecutionGuard(ctx: Context, options: { agentsHome?: string } = {}): void { + const agentsHome = resolveAgentsHome(options.agentsHome); const installed = new WeakMap(); async function prepare(agent: Agent, signal: AbortSignal | undefined, creating: boolean): Promise { - const agentsHome = options.agentsHome; const membership = ctx.agentTeams.tryMembership(agent); const root = membership?.root ?? agent; const header = agent.session.header; diff --git a/packages/plugins/experts/src/runtime/preset-compiler.ts b/packages/plugins/experts/src/runtime/preset-compiler.ts index 577a93a3f3..6e834d0dfc 100644 --- a/packages/plugins/experts/src/runtime/preset-compiler.ts +++ b/packages/plugins/experts/src/runtime/preset-compiler.ts @@ -1,6 +1,6 @@ import { assetBytes, validateResources, type PackageAssets } from '../authoring/package-resources.js'; import { mkdir, readFile, writeFile, readdir, lstat, chmod, mkdtemp, rename, rm } from 'node:fs/promises'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import { Context } from '@deepseek-ai/cordis'; import type { PresetDefinition } from '@deepseek-ai/dsh-agent-preset-registry'; import type {} from '@deepseek-ai/cordis-plugin-loader'; @@ -74,13 +74,19 @@ export function presetIdFor(input: CompileInput): string { return `wd-exp-${kebab(input.expertId)}-${digest}`; } +/** Explicit directory, then DSH_AGENTS_HOME, then $DSH_HOME/agents, then ~/.agents. */ +export function resolveAgentsHome(explicit?: string): string { + const selected = explicit ?? process.env.DSH_AGENTS_HOME ?? (process.env.DSH_HOME ? join(process.env.DSH_HOME, 'agents') : join(homedir(), '.agents')); + return resolve(selected); +} + /** * Compile (or reuse) the immutable preset directory for one expert revision. * Idempotent: an existing directory whose composition digest matches is reused. */ export function expertPresetDir(presetId: string, agentsHome?: string): string { if (!/^wd-exp-[a-z0-9-]+$/.test(presetId)) throw new Error('experts/invalid-preset-id'); - return join(agentsHome ?? process.env.DSH_AGENTS_HOME ?? join(homedir(), '.agents'), '.workdsh-state', 'experts', 'presets', presetId); + return join(resolveAgentsHome(agentsHome), '.workdsh-state', 'experts', 'presets', presetId); } const registrations = new WeakMap>>(); diff --git a/packages/plugins/experts/src/services/experts-manager.ts b/packages/plugins/experts/src/services/experts-manager.ts index e17c573f33..5544175d98 100644 --- a/packages/plugins/experts/src/services/experts-manager.ts +++ b/packages/plugins/experts/src/services/experts-manager.ts @@ -1,13 +1,12 @@ import { definitionFromDocuments, parseExpertDocument } from '../authoring/documents.js'; import { randomUUID } from 'node:crypto'; -import { homedir } from 'node:os'; -import { join, resolve } from 'node:path'; +import { join } from 'node:path'; import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import type { ConnectorManagementService } from 'workdsh-contracts'; import { Context, Service } from '@deepseek-ai/cordis'; import type { KvTable } from '@deepseek-ai/dsh-storage-domain'; import type { SessionCreateRequest, SessionCreateValue } from '@deepseek-ai/dsh-api-session-controller'; -import { expertPresetDir, readExpertPreset, registerExpertPreset } from '../runtime/preset-compiler.js'; +import { expertPresetDir, readExpertPreset, registerExpertPreset, resolveAgentsHome } from '../runtime/preset-compiler.js'; import { EXPERT_LIMITS, ExpertsError, @@ -231,7 +230,7 @@ export class ExpertsManager extends Service implements ExpertsService { constructor(ctx: Context, options: ExpertsManagerOptions = {}) { super(ctx, 'workdshExperts'); - const agentsHome = resolve(options.agentsHome ?? process.env.DSH_AGENTS_HOME ?? (process.env.DSH_HOME ? join(process.env.DSH_HOME, 'agents') : join(homedir(), '.agents'))); + const agentsHome = resolveAgentsHome(options.agentsHome); this.agentsHome = agentsHome; this.includeDefaultSkillRoots = options.includeDefaultSkillRoots; this.stagingRoot = join(agentsHome, '.workdsh-state', 'experts', 'staging');