208 lines
6.6 KiB
TypeScript
208 lines
6.6 KiB
TypeScript
export type MosHandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed';
|
|
|
|
export interface CoordinationScope {
|
|
readonly actorId: string;
|
|
readonly tenantId: string;
|
|
readonly correlationId: string;
|
|
/** Trusted gateway/configuration identity; never supplied by a channel client. */
|
|
readonly requesterAgentId: string;
|
|
}
|
|
|
|
export interface MosCoordinationIdentity {
|
|
readonly interactionAgentId: string;
|
|
readonly orchestrationAgentId: string;
|
|
}
|
|
|
|
export interface MosHandoffRequest {
|
|
readonly idempotencyKey: string;
|
|
readonly summary: string;
|
|
readonly context?: string;
|
|
readonly missionId?: string;
|
|
}
|
|
|
|
export interface MosHandoff {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly request: MosHandoffRequest;
|
|
readonly scope: CoordinationScope;
|
|
}
|
|
|
|
export interface MosHandoffReceipt {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly status: 'queued' | 'accepted';
|
|
readonly correlationId: string;
|
|
}
|
|
|
|
export interface MosCoordinationActivity {
|
|
readonly occurredAt: string;
|
|
readonly status: MosHandoffStatus;
|
|
readonly summary: string;
|
|
}
|
|
|
|
export interface CoordinationObservation {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly status: MosHandoffStatus;
|
|
readonly correlationId: string;
|
|
readonly activity: readonly MosCoordinationActivity[];
|
|
}
|
|
|
|
export interface CoordinationResult {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly status: 'completed' | 'failed' | 'pending';
|
|
readonly correlationId: string;
|
|
readonly summary?: string;
|
|
}
|
|
|
|
/**
|
|
* Transport-neutral boundary. The interaction plane can request work and read
|
|
* its progress/result, but it cannot issue worker, review, merge, or other
|
|
* general orchestration commands.
|
|
*/
|
|
export interface MosCoordinationPort {
|
|
handoff(handoff: MosHandoff): Promise<MosHandoffReceipt>;
|
|
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
|
|
result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
|
|
}
|
|
|
|
export type MosCoordinationAuthorityErrorCode =
|
|
| 'invalid_identity'
|
|
| 'requester_forbidden'
|
|
| 'target_drift'
|
|
| 'correlation_drift';
|
|
|
|
export class MosCoordinationAuthorityError extends Error {
|
|
constructor(
|
|
readonly code: MosCoordinationAuthorityErrorCode,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = MosCoordinationAuthorityError.name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enforces the interaction-to-orchestration authority boundary before a
|
|
* transport is reached. Identity names remain configuration data.
|
|
*/
|
|
export class MosCoordinationClient {
|
|
private readonly identity: MosCoordinationIdentity;
|
|
|
|
constructor(
|
|
identity: MosCoordinationIdentity,
|
|
private readonly port: MosCoordinationPort,
|
|
private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
|
|
) {
|
|
this.identity = normalizeIdentity(identity);
|
|
}
|
|
|
|
async handoff(request: MosHandoffRequest, scope: CoordinationScope): Promise<MosHandoffReceipt> {
|
|
this.assertRequester(scope);
|
|
const handoff: MosHandoff = {
|
|
handoffId: this.handoffIdFactory(),
|
|
targetAgentId: this.identity.orchestrationAgentId,
|
|
request: snapshotRequest(request),
|
|
scope: snapshotScope(scope),
|
|
};
|
|
const receipt = await this.port.handoff(handoff);
|
|
if (
|
|
receipt.handoffId !== handoff.handoffId ||
|
|
receipt.targetAgentId !== handoff.targetAgentId
|
|
) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'target_drift',
|
|
'Mos coordination transport returned a mismatched handoff target',
|
|
);
|
|
}
|
|
if (receipt.correlationId !== handoff.scope.correlationId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'correlation_drift',
|
|
'Mos coordination transport returned a mismatched correlation ID',
|
|
);
|
|
}
|
|
return receipt;
|
|
}
|
|
|
|
async observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation> {
|
|
this.assertRequester(scope);
|
|
return this.assertObservation(await this.port.observe(handoffId, snapshotScope(scope)), scope);
|
|
}
|
|
|
|
async result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult> {
|
|
this.assertRequester(scope);
|
|
return this.assertResult(await this.port.result(handoffId, snapshotScope(scope)), scope);
|
|
}
|
|
|
|
private assertRequester(scope: CoordinationScope): void {
|
|
if (scope.requesterAgentId !== this.identity.interactionAgentId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'requester_forbidden',
|
|
'Requester is not the configured interaction agent',
|
|
);
|
|
}
|
|
}
|
|
|
|
private assertObservation(
|
|
observation: CoordinationObservation,
|
|
scope: CoordinationScope,
|
|
): CoordinationObservation {
|
|
if (observation.targetAgentId !== this.identity.orchestrationAgentId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'target_drift',
|
|
'Mos coordination transport returned an unexpected observation target',
|
|
);
|
|
}
|
|
if (observation.correlationId !== scope.correlationId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'correlation_drift',
|
|
'Mos coordination transport returned a mismatched observation correlation ID',
|
|
);
|
|
}
|
|
return observation;
|
|
}
|
|
|
|
private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult {
|
|
if (result.targetAgentId !== this.identity.orchestrationAgentId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'target_drift',
|
|
'Mos coordination transport returned an unexpected result target',
|
|
);
|
|
}
|
|
if (result.correlationId !== scope.correlationId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'correlation_drift',
|
|
'Mos coordination transport returned a mismatched result correlation ID',
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationIdentity {
|
|
const interactionAgentId = identity.interactionAgentId.trim();
|
|
const orchestrationAgentId = identity.orchestrationAgentId.trim();
|
|
if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'invalid_identity',
|
|
'Interaction and orchestration identities are required',
|
|
);
|
|
}
|
|
if (interactionAgentId === orchestrationAgentId) {
|
|
throw new MosCoordinationAuthorityError(
|
|
'invalid_identity',
|
|
'Interaction and orchestration identities must differ',
|
|
);
|
|
}
|
|
return Object.freeze({ interactionAgentId, orchestrationAgentId });
|
|
}
|
|
|
|
function snapshotRequest(request: MosHandoffRequest): MosHandoffRequest {
|
|
return Object.freeze({ ...request });
|
|
}
|
|
|
|
function snapshotScope(scope: CoordinationScope): CoordinationScope {
|
|
return Object.freeze({ ...scope });
|
|
}
|