210 lines
6.8 KiB
TypeScript
210 lines
6.8 KiB
TypeScript
export type HandoffStatus = '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 InteractionCoordinationIdentity {
|
|
readonly interactionAgentId: string;
|
|
readonly orchestrationAgentId: string;
|
|
}
|
|
|
|
export interface HandoffRequest {
|
|
readonly idempotencyKey: string;
|
|
readonly summary: string;
|
|
readonly context?: string;
|
|
readonly missionId?: string;
|
|
}
|
|
|
|
export interface Handoff {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly request: HandoffRequest;
|
|
readonly scope: CoordinationScope;
|
|
}
|
|
|
|
export interface HandoffReceipt {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly status: 'queued' | 'accepted';
|
|
readonly correlationId: string;
|
|
}
|
|
|
|
export interface InteractionCoordinationActivity {
|
|
readonly occurredAt: string;
|
|
readonly status: HandoffStatus;
|
|
readonly summary: string;
|
|
}
|
|
|
|
export interface CoordinationObservation {
|
|
readonly handoffId: string;
|
|
readonly targetAgentId: string;
|
|
readonly status: HandoffStatus;
|
|
readonly correlationId: string;
|
|
readonly activity: readonly InteractionCoordinationActivity[];
|
|
}
|
|
|
|
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 InteractionCoordinationPort {
|
|
handoff(handoff: Handoff): Promise<HandoffReceipt>;
|
|
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
|
|
result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
|
|
}
|
|
|
|
export type InteractionCoordinationAuthorityErrorCode =
|
|
| 'invalid_identity'
|
|
| 'requester_forbidden'
|
|
| 'target_drift'
|
|
| 'correlation_drift';
|
|
|
|
export class InteractionCoordinationAuthorityError extends Error {
|
|
constructor(
|
|
readonly code: InteractionCoordinationAuthorityErrorCode,
|
|
message: string,
|
|
) {
|
|
super(message);
|
|
this.name = InteractionCoordinationAuthorityError.name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enforces the interaction-to-orchestration authority boundary before a
|
|
* transport is reached. Identity names remain configuration data.
|
|
*/
|
|
export class InteractionCoordinationClient {
|
|
private readonly identity: InteractionCoordinationIdentity;
|
|
|
|
constructor(
|
|
identity: InteractionCoordinationIdentity,
|
|
private readonly port: InteractionCoordinationPort,
|
|
private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
|
|
) {
|
|
this.identity = normalizeIdentity(identity);
|
|
}
|
|
|
|
async handoff(request: HandoffRequest, scope: CoordinationScope): Promise<HandoffReceipt> {
|
|
this.assertRequester(scope);
|
|
const handoff: Handoff = {
|
|
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 InteractionCoordinationAuthorityError(
|
|
'target_drift',
|
|
'Interaction coordination transport returned a mismatched handoff target',
|
|
);
|
|
}
|
|
if (receipt.correlationId !== handoff.scope.correlationId) {
|
|
throw new InteractionCoordinationAuthorityError(
|
|
'correlation_drift',
|
|
'Interaction 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 InteractionCoordinationAuthorityError(
|
|
'requester_forbidden',
|
|
'Requester is not the configured interaction agent',
|
|
);
|
|
}
|
|
}
|
|
|
|
private assertObservation(
|
|
observation: CoordinationObservation,
|
|
scope: CoordinationScope,
|
|
): CoordinationObservation {
|
|
if (observation.targetAgentId !== this.identity.orchestrationAgentId) {
|
|
throw new InteractionCoordinationAuthorityError(
|
|
'target_drift',
|
|
'Interaction coordination transport returned an unexpected observation target',
|
|
);
|
|
}
|
|
if (observation.correlationId !== scope.correlationId) {
|
|
throw new InteractionCoordinationAuthorityError(
|
|
'correlation_drift',
|
|
'Interaction 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 InteractionCoordinationAuthorityError(
|
|
'target_drift',
|
|
'Interaction coordination transport returned an unexpected result target',
|
|
);
|
|
}
|
|
if (result.correlationId !== scope.correlationId) {
|
|
throw new InteractionCoordinationAuthorityError(
|
|
'correlation_drift',
|
|
'Interaction coordination transport returned a mismatched result correlation ID',
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function normalizeIdentity(
|
|
identity: InteractionCoordinationIdentity,
|
|
): InteractionCoordinationIdentity {
|
|
const interactionAgentId = identity.interactionAgentId.trim();
|
|
const orchestrationAgentId = identity.orchestrationAgentId.trim();
|
|
if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) {
|
|
throw new InteractionCoordinationAuthorityError(
|
|
'invalid_identity',
|
|
'Interaction and orchestration identities are required',
|
|
);
|
|
}
|
|
if (interactionAgentId === orchestrationAgentId) {
|
|
throw new InteractionCoordinationAuthorityError(
|
|
'invalid_identity',
|
|
'Interaction and orchestration identities must differ',
|
|
);
|
|
}
|
|
return Object.freeze({ interactionAgentId, orchestrationAgentId });
|
|
}
|
|
|
|
function snapshotRequest(request: HandoffRequest): HandoffRequest {
|
|
return Object.freeze({ ...request });
|
|
}
|
|
|
|
function snapshotScope(scope: CoordinationScope): CoordinationScope {
|
|
return Object.freeze({ ...scope });
|
|
}
|