Skip to content
10 changes: 8 additions & 2 deletions packages/daemon/src/lib/rpc-handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { SpaceAgentRepository } from '../../storage/repositories/space-agent-rep
import { SpaceLongHorizonAgentRepository } from '../../storage/repositories/space-long-horizon-agent-repository.ts';
import { SpaceAgentTemplateRepository } from '../../storage/repositories/space-agent-template-repository.ts';
import { SpaceAgentTemplateManager } from '../space/managers/space-agent-template-manager.ts';
import { countWorkflowSlotsReferencingTemplate } from '../space/managers/space-agent-template-references.ts';
import {
deliverSpaceAgentMessage,
type SpaceAgentInjectionOutcome,
Expand Down Expand Up @@ -540,7 +541,10 @@ export function setupRPCHandlers(deps: RPCHandlerDependencies): RPCHandlerSetupR
spaceAgentRepo,
longHorizonAgentRepo
);
const spaceWorkflowManager = new SpaceWorkflowManager(spaceWorkflowRepo, agentLookup);
const agentTemplateRepo = new SpaceAgentTemplateRepository(deps.db.getDatabase());
const spaceWorkflowManager = new SpaceWorkflowManager(spaceWorkflowRepo, agentLookup, {
hasTemplate: (key: string) => agentTemplateRepo.getByKey(key) !== null,
});

const spaceTaskManagerFactory: SpaceTaskManagerFactory = (spaceId: string) => {
return new SpaceTaskManager(
Expand Down Expand Up @@ -918,7 +922,9 @@ export function setupRPCHandlers(deps: RPCHandlerDependencies): RPCHandlerSetupR
deps.db,
longHorizonAgentRepo,
spaceRuntimeService,
new SpaceAgentTemplateManager(new SpaceAgentTemplateRepository(deps.db.getDatabase()))
new SpaceAgentTemplateManager(agentTemplateRepo, undefined, (key: string) =>
countWorkflowSlotsReferencingTemplate(deps.db.getDatabase(), key)
)
);

setupSessionHandlers(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
validateAgentModelPool,
validateSpaceAgentTools,
} from '../space/managers/space-agent-manager.ts';
import { SpaceAgentTemplateManager } from '../space/managers/space-agent-template-manager.ts';
import type { SpaceAgentTemplateManager } from '../space/managers/space-agent-template-manager.ts';
import { SpaceAgentTemplateReapplyService } from '../space/managers/space-agent-template-reapply-service.ts';
import type { SpaceManager } from '../space/managers/space-manager.ts';
import type { SpaceRuntimeService } from '../space/runtime/space-runtime-service.ts';
Expand Down
133 changes: 95 additions & 38 deletions packages/daemon/src/lib/rpc-handlers/space-export-import-handlers.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import type { Database as BunDatabase } from '../../storage/sqlite-compat.ts';
import { generateUUID } from '@hyperneo/shared';
import type {
CreateSpaceWorkerAgentParams,
CreateSpaceWorkflowParams,
ExportedSpaceWorkerAgent,
ExportedSpaceWorkflow,
MessageHub,
Space,
SpaceExportBundle,
SpaceLongHorizonAgent,
SpaceWorkerAgent,
SpaceWorkflow,
CreateSpaceWorkerAgentParams,
UpdateSpaceWorkerAgentParams,
CreateSpaceWorkflowParams,
WorkflowNodeInput,
SpaceExportBundle,
ExportedSpaceWorkerAgent,
ExportedSpaceWorkflow,
} from '@hyperneo/shared';
import type { DaemonInternalEventMap, InternalEventBus } from '../internal-event-bus.ts';
import type { SpaceManager } from '../space/managers/space-manager.ts';
import type { SpaceWorkflowManager } from '../space/managers/space-workflow-manager.ts';
import { generateUUID } from '@hyperneo/shared';
import type { SpaceAgentRepository } from '../../storage/repositories/space-agent-repository.ts';
import type { SpaceLongHorizonAgent } from '@hyperneo/shared';
import { SpaceAgentTemplateRepository } from '../../storage/repositories/space-agent-template-repository.ts';
import {
coordinatorLongHorizonAgentId,
type SpaceLongHorizonAgentRepository,
} from '../../storage/repositories/space-long-horizon-agent-repository.ts';
import type { SpaceWorkflowRepository } from '../../storage/repositories/space-workflow-repository.ts';
import { exportBundle, validateExportBundle, normalizeOverride } from '../space/export-format.ts';
import type { Database as BunDatabase } from '../../storage/sqlite-compat.ts';
import type { DaemonInternalEventMap, InternalEventBus } from '../internal-event-bus.ts';
import { Logger } from '../logger.ts';
import {
MIGRATED_WORKER_TEMPLATE_KEY,
isRunnableUnifiedAgent,
} from '../space/agents/worker-long-horizon-mapper.ts';
getLongHorizonAgentTemplate,
getLongHorizonAgentTemplates,
} from '../space/agents/long-horizon-agent-templates.ts';
import {
publishUnifiedAgentCreated,
publishUnifiedAgentUpdated,
} from '../space/agents/unified-agent-events.ts';
import {
isRunnableUnifiedAgent,
MIGRATED_WORKER_TEMPLATE_KEY,
} from '../space/agents/worker-long-horizon-mapper.ts';
import { exportBundle, normalizeOverride, validateExportBundle } from '../space/export-format.ts';
import type { SpaceManager } from '../space/managers/space-manager.ts';
import type { SpaceWorkflowManager } from '../space/managers/space-workflow-manager.ts';
import { RESERVED_SPACE_AGENT_HANDLES, slugifyWithinLimit } from '../space/slug.ts';
import { getLongHorizonAgentTemplate } from '../space/agents/long-horizon-agent-templates.ts';
import { Logger } from '../logger.ts';

const log = new Logger('space-export-import-handlers');
const RESERVED_AGENT_HANDLE_SET = new Set<string>(RESERVED_SPACE_AGENT_HANDLES);
Expand Down Expand Up @@ -307,7 +311,8 @@ export function buildWorkflowCreateParams(
exported: ExportedSpaceWorkflow,
importedAgentNameToId: Map<string, string>,
existingAgentNameToId: Map<string, string>,
usedWorkflowHandles?: Set<string>
usedWorkflowHandles?: Set<string>,
storedTemplateKeys?: ReadonlySet<string>
): { params: CreateSpaceWorkflowParams; nodeNameToId: Map<string, string>; warnings: string[] } {
const warnings: string[] = [];

Expand Down Expand Up @@ -344,18 +349,30 @@ export function buildWorkflowCreateParams(
name: a.name,
};
const templateKey = a.templateKey?.trim();
if (templateKey) {
const agentRef = a.agentRef?.trim() ?? '';
const resolvedAgentId =
agentRef === ''
? null
: (normalizedImportedAgentNameToId.get(nameKey(agentRef)) ??
normalizedExistingAgentNameToId.get(nameKey(agentRef)) ??
null);
const builtInTemplate = templateKey
? getLongHorizonAgentTemplate(templateKey) !== undefined
: false;
const storedTemplate = templateKey ? (storedTemplateKeys?.has(templateKey) ?? false) : false;
if (resolvedAgentId) {
entry.agentId = resolvedAgentId;
Comment thread
lsm marked this conversation as resolved.
if (templateKey && builtInTemplate && !storedTemplate) {
entry.templateKey = templateKey;
Comment thread
lsm marked this conversation as resolved.
}
} else if (templateKey && (builtInTemplate || storedTemplate)) {
entry.templateKey = templateKey;
entry.agentId = '';
} else if (templateKey) {
entry.templateKey = templateKey;
} else {
const agentRef = a.agentRef?.trim() ?? '';
const agentId =
normalizedImportedAgentNameToId.get(nameKey(agentRef)) ??
normalizedExistingAgentNameToId.get(nameKey(agentRef)) ??
null;
if (!agentId) {
warnings.push(`node "${exportedNode.name}" references unknown agent "${agentRef}"`);
}
entry.agentId = agentId ?? '';
warnings.push(`node "${exportedNode.name}" references unknown agent "${agentRef}"`);
entry.agentId = '';
}
if (typeof a.model === 'string' && a.model.trim()) entry.model = a.model.trim();
if (a.thinkingLevel !== undefined) entry.thinkingLevel = a.thinkingLevel;
Expand Down Expand Up @@ -427,16 +444,24 @@ function validateWorkflowForPreview(
exported: ExportedSpaceWorkflow,
importedAgentNames: Set<string>,
existingAgentNameToId: Map<string, string>,
agentNameToRole: Map<string, string>
agentNameToRole: Map<string, string>,
knownTemplateKeys: ReadonlySet<string>
): string[] {
const errors: string[] = [];

for (const node of exported.nodes) {
for (const a of node.agents) {
const templateKey = a.templateKey?.trim();
if (templateKey) {
if (!getLongHorizonAgentTemplate(templateKey)) {
errors.push(`node "${node.name}" references unknown template "${templateKey}"`);
if (!knownTemplateKeys.has(templateKey)) {
const agentRef = a.agentRef?.trim() ?? '';
const agentRefResolvable =
agentRef !== '' &&
(importedAgentNames.has(nameKey(agentRef)) ||
existingAgentNameToId.has(nameKey(agentRef)));
if (!agentRefResolvable) {
errors.push(`node "${node.name}" references unknown template "${templateKey}"`);
}
}
continue;
}
Expand Down Expand Up @@ -556,16 +581,25 @@ export function setupSpaceExportImportHandlers(
}

const allAgents = unifiedExportAgents(agentRepo, longHorizonAgentRepo, params.spaceId);
const liveById = new Map(allAgents.map((a) => [a.id, a]));

const referencedAgentIds = new Set<string>();
for (const wf of workflows) {
for (const node of wf.nodes ?? []) {
for (const a of node.agents ?? []) {
if (a.agentId?.trim()) referencedAgentIds.add(a.agentId.trim());
const agentId = a.agentId?.trim();
if (!agentId) continue;
if (
a.templateKey?.trim() &&
(getLongHorizonAgentTemplate(a.templateKey.trim()) !== undefined ||
!liveById.has(agentId))
Comment on lines +593 to +595

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate names for retained built-in fallbacks

When spaceExport.workflows handles a built-in-template slot with an exportable retained fallback and another exportable agent has the same normalized display name, this early exit omits the fallback from referencedAgentIds, so the ambiguity check never runs. exportWorkflow still emits its agentRef, and the later name-based filtering includes both same-named agents; spaceImport.execute then rejects the exported bundle through findDuplicateBundleAgentNames. Either omit the unnecessary fallback from this export or continue validating its name.

Useful? React with 👍 / 👎.

) {
continue;
}
referencedAgentIds.add(agentId);
}
}
}
const liveById = new Map(allAgents.map((a) => [a.id, a]));
const allSpaceAgents = longHorizonAgentRepo.listBySpaceId(params.spaceId);
const coordinatorByHandle = longHorizonAgentRepo.getCoordinator(params.spaceId);
const unexportable = [...referencedAgentIds].filter((id) => !liveById.has(id));
Expand Down Expand Up @@ -631,6 +665,7 @@ export function setupSpaceExportImportHandlers(
const space = await requireSpace(spaceManager, params.spaceId);

let agents = unifiedExportAgents(agentRepo, longHorizonAgentRepo, params.spaceId);
const exportableIds = new Set(agents.map((a) => a.id));
if (params.agentIds?.length) {
const idSet = new Set(params.agentIds);
agents = agents.filter((a) => idSet.has(a.id));
Expand All @@ -644,14 +679,23 @@ export function setupSpaceExportImportHandlers(
}

const referencedAgentIds = new Set<string>();
const exportedIdSet = new Set(agents.map((a) => a.id));
for (const wf of workflows) {
for (const node of wf.nodes ?? []) {
for (const a of node.agents ?? []) {
if (a.agentId?.trim()) referencedAgentIds.add(a.agentId.trim());
const agentId = a.agentId?.trim();
if (!agentId) continue;
if (
a.templateKey?.trim() &&
(getLongHorizonAgentTemplate(a.templateKey.trim()) !== undefined ||
!exportableIds.has(agentId))
) {
continue;
}
referencedAgentIds.add(agentId);
Comment thread
lsm marked this conversation as resolved.
Comment thread
lsm marked this conversation as resolved.
}
}
}
const exportedIdSet = new Set(agents.map((a) => a.id));
const missingRefs = [...referencedAgentIds].filter((id) => !exportedIdSet.has(id));
if (missingRefs.length > 0) {
throw new Error(
Expand Down Expand Up @@ -745,6 +789,13 @@ export function setupSpaceExportImportHandlers(
.map((a) => nameKey(a.name))
);

const knownTemplateKeys = new Set<string>(
getLongHorizonAgentTemplates().map((template) => template.key)
);
for (const template of new SpaceAgentTemplateRepository(db).list()) {
knownTemplateKeys.add(template.key);
}

for (const wf of bundle.workflows) {
const existing = existingWorkflowByName.get(wf.name);
if (existing) {
Expand All @@ -757,7 +808,8 @@ export function setupSpaceExportImportHandlers(
wf,
importedAgentNames,
existingAgentNameToId,
agentNameToRole
agentNameToRole,
knownTemplateKeys
);
for (const err of errors) {
validationErrors.push(`Workflow "${wf.name}": ${err}`);
Expand Down Expand Up @@ -987,7 +1039,7 @@ export function setupSpaceExportImportHandlers(
allWarnings,
preservedHandle
);
let updated = agentRepo.update(existing.id, updateParams);
const updated = agentRepo.update(existing.id, updateParams);
const twinRow = longHorizonAgentRepo.getById(existing.id);
const authoritativeOverlay =
!!twinRow && twinRow.templateKey !== MIGRATED_WORKER_TEMPLATE_KEY;
Expand Down Expand Up @@ -1057,6 +1109,10 @@ export function setupSpaceExportImportHandlers(

const workflowResults: ImportedItem[] = [];

const storedTemplateKeys = new Set<string>(
new SpaceAgentTemplateRepository(db).list().map((template) => template.key)
);

for (const exportedWorkflow of bundle.workflows) {
const existing = existingWorkflowByName.get(exportedWorkflow.name);

Expand Down Expand Up @@ -1103,7 +1159,8 @@ export function setupSpaceExportImportHandlers(
exportedWorkflow,
importedAgentNameToId,
existingAgentNameToId,
usedWorkflowHandles
usedWorkflowHandles,
storedTemplateKeys
);

const exportedHandle =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type {
CreateSpaceAgentTemplateParams,
SettingSource,
SpaceAgentAutonomyLevel,
ThinkingLevel,
WorkerAgentModelPoolEntry,
} from '@hyperneo/shared';
import { slugify } from '../slug.ts';

export const MIGRATED_WORKFLOW_TEMPLATE_PREFIX = 'migrated.';

export interface WorkflowAgentTemplateSynthesisSource {
id: string;
handle: string;
displayName: string;
description?: string | null;
instructions?: string | null;
autonomyLevel?: SpaceAgentAutonomyLevel | null;
model?: string | null;
provider?: string | null;
thinkingLevel?: ThinkingLevel | null;
settingSources?: readonly SettingSource[] | null;
tools?: readonly string[] | null;
modelPool?: readonly WorkerAgentModelPoolEntry[] | null;
}

export interface OrphanWorkflowAgentTemplateRef {
agentId: string;
slotName: string;
}

export function allocateMigratedTemplateKey(
seed: string,
agentId: string,
claimed: ReadonlySet<string>
): string {
const base = `${MIGRATED_WORKFLOW_TEMPLATE_PREFIX}${slugify(seed)}`;
if (!claimed.has(base)) return base;
const suffix = agentId
.replace(/[^a-z0-9]/gi, '')
.slice(0, 8)
.toLowerCase();
const uniquified = suffix ? `${base}.${suffix}` : `${base}.x`;
if (!claimed.has(uniquified)) return uniquified;
let attempt = 2;
while (claimed.has(`${uniquified}.${attempt}`)) attempt += 1;
return `${uniquified}.${attempt}`;
}

export function synthesizeWorkflowAgentTemplate(
source: WorkflowAgentTemplateSynthesisSource,
key: string
): CreateSpaceAgentTemplateParams {
return {
key,
handle: slugify(source.handle || source.displayName || source.id),
displayName: source.displayName,
description: source.description ?? '',
instructions: source.instructions ?? '',
suggestedAutonomyLevel: source.autonomyLevel ?? 2,
model: source.model ?? null,
provider: source.provider ?? null,
modelPool: copyEntries(source.modelPool),
thinkingLevel: source.thinkingLevel ?? null,
settingSources: source.settingSources ? [...source.settingSources] : null,
tools: copyEntries(source.tools),
};
}

export function synthesizeOrphanWorkflowAgentTemplate(
ref: OrphanWorkflowAgentTemplateRef,
key: string
): CreateSpaceAgentTemplateParams {
const displayName = ref.slotName.trim() || ref.agentId;
return {
key,
handle: slugify(displayName),
displayName,
description: '',
instructions: '',
suggestedAutonomyLevel: 2,
model: null,
provider: null,
modelPool: null,
thinkingLevel: null,
settingSources: null,
tools: null,
};
}

function copyEntries<T>(entries: readonly T[] | null | undefined): T[] | null {
return entries && entries.length > 0 ? [...entries] : null;
}
Loading
Loading