218 lines
7.0 KiB
TypeScript
218 lines
7.0 KiB
TypeScript
import type {
|
|
AgentRuntimeProvider,
|
|
RuntimeAttachHandle,
|
|
RuntimeAttachMode,
|
|
RuntimeCapability,
|
|
RuntimeCapabilitySet,
|
|
RuntimeHealth,
|
|
RuntimeMessage,
|
|
RuntimeScope,
|
|
RuntimeSession,
|
|
RuntimeSessionState,
|
|
RuntimeSessionTree,
|
|
RuntimeStreamEvent,
|
|
TransitionalCapabilityInventoryEntry,
|
|
TransitionalCapabilityInventoryProvider,
|
|
TransitionalRuntimeCapability,
|
|
} from '@mosaicstack/types';
|
|
|
|
const HERMES_PROVIDER_ID = 'runtime.hermes';
|
|
const TRANSITIONAL_CAPABILITIES: readonly TransitionalRuntimeCapability[] = [
|
|
'kanban',
|
|
'skills',
|
|
'memory',
|
|
'tools',
|
|
'cron',
|
|
];
|
|
const RUNTIME_CAPABILITIES: readonly RuntimeCapability[] = [
|
|
'session.list',
|
|
'session.tree',
|
|
'session.stream',
|
|
'session.send',
|
|
'session.attach',
|
|
'session.terminate',
|
|
];
|
|
|
|
/** Legacy transport boundary. These shapes are intentionally adapter-local. */
|
|
export interface HermesLegacySession {
|
|
conversation_id: string;
|
|
agent_id: string;
|
|
parent_conversation_id?: string;
|
|
status: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
export interface HermesRuntimeTransport {
|
|
capabilities(scope: RuntimeScope): Promise<string[]>;
|
|
health(scope: RuntimeScope): Promise<{ status: string; detail?: string }>;
|
|
sessions(scope: RuntimeScope): Promise<HermesLegacySession[]>;
|
|
stream(
|
|
sessionId: string,
|
|
cursor: string | undefined,
|
|
scope: RuntimeScope,
|
|
): AsyncIterable<RuntimeStreamEvent>;
|
|
send(sessionId: string, message: RuntimeMessage, scope: RuntimeScope): Promise<void>;
|
|
attach(
|
|
sessionId: string,
|
|
mode: RuntimeAttachMode,
|
|
scope: RuntimeScope,
|
|
): Promise<RuntimeAttachHandle>;
|
|
detach(attachmentId: string, scope: RuntimeScope): Promise<void>;
|
|
terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise<void>;
|
|
}
|
|
|
|
export class HermesRuntimeProviderError extends Error {
|
|
constructor(
|
|
readonly code: 'capability_unsupported' | 'invalid_request',
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = HermesRuntimeProviderError.name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transitional Hermes adapter. Legacy identifiers and schemas do not cross this
|
|
* boundary: callers only observe Mosaic AgentRuntimeProvider contracts.
|
|
*/
|
|
export class HermesRuntimeProvider
|
|
implements AgentRuntimeProvider, TransitionalCapabilityInventoryProvider
|
|
{
|
|
readonly id = HERMES_PROVIDER_ID;
|
|
|
|
constructor(private readonly transport: HermesRuntimeTransport) {}
|
|
|
|
/**
|
|
* Full AC-TESS-05 migration inventory. These operations are deliberately
|
|
* unsupported until their Mosaic-owned plugin contracts exist.
|
|
*/
|
|
async transitionalCapabilityMatrix(
|
|
_scope: RuntimeScope,
|
|
): Promise<TransitionalCapabilityInventoryEntry[]> {
|
|
return TRANSITIONAL_CAPABILITIES.map((capability) => ({ capability, status: 'unsupported' }));
|
|
}
|
|
|
|
async assertTransitionalCapability(
|
|
capability: TransitionalRuntimeCapability,
|
|
scope: RuntimeScope,
|
|
): Promise<void> {
|
|
const entry = (await this.transitionalCapabilityMatrix(scope)).find(
|
|
(candidate) => candidate.capability === capability,
|
|
);
|
|
if (!entry || entry.status !== 'supported') {
|
|
throw new HermesRuntimeProviderError(
|
|
'capability_unsupported',
|
|
`Hermes transitional capability is unsupported: ${capability}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async capabilities(scope: RuntimeScope): Promise<RuntimeCapabilitySet> {
|
|
const legacyCapabilities = await this.transport.capabilities(scope);
|
|
return {
|
|
supported: RUNTIME_CAPABILITIES.filter((capability) =>
|
|
legacyCapabilities.includes(capability),
|
|
),
|
|
};
|
|
}
|
|
|
|
async health(scope: RuntimeScope): Promise<RuntimeHealth> {
|
|
const health = await this.transport.health(scope);
|
|
return {
|
|
status: health.status === 'healthy' || health.status === 'degraded' ? health.status : 'down',
|
|
checkedAt: new Date().toISOString(),
|
|
...(health.detail ? { detail: health.detail } : {}),
|
|
};
|
|
}
|
|
|
|
async listSessions(scope: RuntimeScope): Promise<RuntimeSession[]> {
|
|
await this.requireCapability('session.list', scope);
|
|
return (await this.transport.sessions(scope)).map((session) => this.session(session));
|
|
}
|
|
|
|
async getSessionTree(scope: RuntimeScope): Promise<RuntimeSessionTree[]> {
|
|
await this.requireCapability('session.tree', scope);
|
|
const sessions = (await this.transport.sessions(scope)).map((session) => this.session(session));
|
|
const nodes = new Map<string, RuntimeSessionTree>(
|
|
sessions.map((session) => [session.id, { session, children: [] }]),
|
|
);
|
|
const roots: RuntimeSessionTree[] = [];
|
|
for (const session of sessions) {
|
|
const node = nodes.get(session.id)!;
|
|
const parent = session.parentSessionId ? nodes.get(session.parentSessionId) : undefined;
|
|
if (parent) parent.children.push(node);
|
|
else roots.push(node);
|
|
}
|
|
return roots;
|
|
}
|
|
|
|
async *streamSession(
|
|
sessionId: string,
|
|
cursor: string | undefined,
|
|
scope: RuntimeScope,
|
|
): AsyncIterable<RuntimeStreamEvent> {
|
|
await this.requireCapability('session.stream', scope);
|
|
yield* this.transport.stream(sessionId, cursor, scope);
|
|
}
|
|
async sendMessage(
|
|
sessionId: string,
|
|
message: RuntimeMessage,
|
|
scope: RuntimeScope,
|
|
): Promise<void> {
|
|
await this.requireCapability('session.send', scope);
|
|
if (!message.content.trim())
|
|
throw new HermesRuntimeProviderError('invalid_request', 'Message content is required');
|
|
await this.transport.send(sessionId, message, scope);
|
|
}
|
|
async attach(
|
|
sessionId: string,
|
|
mode: RuntimeAttachMode,
|
|
scope: RuntimeScope,
|
|
): Promise<RuntimeAttachHandle> {
|
|
await this.requireCapability('session.attach', scope);
|
|
return this.transport.attach(sessionId, mode, scope);
|
|
}
|
|
async detach(attachmentId: string, scope: RuntimeScope): Promise<void> {
|
|
await this.transport.detach(attachmentId, scope);
|
|
}
|
|
async terminate(sessionId: string, approvalRef: string, scope: RuntimeScope): Promise<void> {
|
|
await this.requireCapability('session.terminate', scope);
|
|
if (!approvalRef.trim())
|
|
throw new HermesRuntimeProviderError('invalid_request', 'Termination approval is required');
|
|
await this.transport.terminate(sessionId, approvalRef, scope);
|
|
}
|
|
|
|
private async requireCapability(
|
|
capability: RuntimeCapability,
|
|
scope: RuntimeScope,
|
|
): Promise<void> {
|
|
if (!(await this.capabilities(scope)).supported.includes(capability)) {
|
|
throw new HermesRuntimeProviderError(
|
|
'capability_unsupported',
|
|
`Hermes does not bridge ${capability}`,
|
|
);
|
|
}
|
|
}
|
|
private session(value: HermesLegacySession): RuntimeSession {
|
|
return {
|
|
id: value.conversation_id,
|
|
providerId: this.id,
|
|
runtimeId: value.agent_id,
|
|
...(value.parent_conversation_id ? { parentSessionId: value.parent_conversation_id } : {}),
|
|
state: state(value.status),
|
|
createdAt: value.created_at,
|
|
updatedAt: value.updated_at,
|
|
};
|
|
}
|
|
}
|
|
function state(value: string): RuntimeSessionState {
|
|
return (
|
|
(
|
|
{ running: 'active', waiting: 'idle', starting: 'starting', stopped: 'stopped' } as Record<
|
|
string,
|
|
RuntimeSessionState
|
|
>
|
|
)[value] ?? 'failed'
|
|
);
|
|
}
|