feat(agent): add transitional Hermes runtime adapter (#734)
This commit was merged in pull request #734.
This commit is contained in:
180
packages/agent/src/hermes-runtime-provider.ts
Normal file
180
packages/agent/src/hermes-runtime-provider.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type {
|
||||
AgentRuntimeProvider,
|
||||
RuntimeAttachHandle,
|
||||
RuntimeAttachMode,
|
||||
RuntimeCapability,
|
||||
RuntimeCapabilitySet,
|
||||
RuntimeHealth,
|
||||
RuntimeMessage,
|
||||
RuntimeScope,
|
||||
RuntimeSession,
|
||||
RuntimeSessionState,
|
||||
RuntimeSessionTree,
|
||||
RuntimeStreamEvent,
|
||||
} from '@mosaicstack/types';
|
||||
|
||||
const HERMES_PROVIDER_ID = 'runtime.hermes';
|
||||
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 {
|
||||
readonly id = HERMES_PROVIDER_ID;
|
||||
|
||||
constructor(private readonly transport: HermesRuntimeTransport) {}
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user