Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions apps/web/tests/integration/expert-manager.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions packages/plugins/experts/src/runtime/execution-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Agent, string>();

async function prepare(agent: Agent, signal: AbortSignal | undefined, creating: boolean): Promise<void> {
const agentsHome = options.agentsHome;
const membership = ctx.agentTeams.tryMembership(agent);
const root = membership?.root ?? agent;
const header = agent.session.header;
Expand Down
10 changes: 8 additions & 2 deletions packages/plugins/experts/src/runtime/preset-compiler.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<Context, Map<string, Promise<void>>>();
Expand Down
7 changes: 3 additions & 4 deletions packages/plugins/experts/src/services/experts-manager.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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');
Expand Down
Loading