De-hardcode orchestrator and interaction agent names (#748)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #748.
This commit is contained in:
2026-07-13 18:59:27 +00:00
parent 8dd4e9d541
commit 405984af5a
33 changed files with 579 additions and 304 deletions

View File

@@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest';
import {
InMemoryMosCoordinationPort,
MosCoordinationClient,
InMemoryInteractionCoordinationPort,
InteractionCoordinationClient,
type CoordinationScope,
type MosCoordinationAuthorityError,
type MosCoordinationPort,
type InteractionCoordinationAuthorityError,
type InteractionCoordinationPort,
} from '../index.js';
const scope: CoordinationScope = {
@@ -15,19 +15,19 @@ const scope: CoordinationScope = {
};
function client(
port: MosCoordinationPort,
port: InteractionCoordinationPort,
handoffIdFactory: () => string = (): string => 'handoff-1',
): MosCoordinationClient {
return new MosCoordinationClient(
): InteractionCoordinationClient {
return new InteractionCoordinationClient(
{ interactionAgentId: 'Nova', orchestrationAgentId: 'Conductor' },
port,
handoffIdFactory,
);
}
describe('MosCoordinationClient', (): void => {
describe('InteractionCoordinationClient', (): void => {
it('round-trips handoff, observation, and result through the native port with identities as data', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort();
const adapter = new InMemoryInteractionCoordinationPort();
const coordination = client(adapter);
await expect(
@@ -42,8 +42,8 @@ describe('MosCoordinationClient', (): void => {
correlationId: 'corr-1',
});
adapter.recordActivity('handoff-1', 'running', 'Mos accepted the request');
adapter.recordResult('handoff-1', 'completed', 'Merged by Mos');
adapter.recordActivity('handoff-1', 'running', 'Orchestrator accepted the request');
adapter.recordResult('handoff-1', 'completed', 'Merged by orchestrator');
await expect(coordination.observe('handoff-1', scope)).resolves.toMatchObject({
status: 'completed',
@@ -59,7 +59,7 @@ describe('MosCoordinationClient', (): void => {
targetAgentId: 'Conductor',
status: 'completed',
correlationId: 'corr-1',
summary: 'Merged by Mos',
summary: 'Merged by orchestrator',
});
expect(coordination).not.toHaveProperty('dispatch');
@@ -69,8 +69,8 @@ describe('MosCoordinationClient', (): void => {
expect(coordination).not.toHaveProperty('cancel');
});
it('fails closed before delivery when an unconfigured agent requests Mos work', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort();
it('fails closed before delivery when an unconfigured agent requests orchestrator work', async (): Promise<void> => {
const adapter = new InMemoryInteractionCoordinationPort();
await expect(
client(adapter).handoff(
@@ -79,31 +79,31 @@ describe('MosCoordinationClient', (): void => {
),
).rejects.toMatchObject({
code: 'requester_forbidden',
} satisfies Partial<MosCoordinationAuthorityError>);
} satisfies Partial<InteractionCoordinationAuthorityError>);
});
it('rejects self-delegation configuration before constructing a client', (): void => {
expect(
(): MosCoordinationClient =>
new MosCoordinationClient(
(): InteractionCoordinationClient =>
new InteractionCoordinationClient(
{ interactionAgentId: 'Nova', orchestrationAgentId: 'Nova' },
new InMemoryMosCoordinationPort(),
new InMemoryInteractionCoordinationPort(),
),
).toThrow('Interaction and orchestration identities must differ');
});
it('rejects whitespace-equivalent self-delegation identities', (): void => {
expect(
(): MosCoordinationClient =>
new MosCoordinationClient(
(): InteractionCoordinationClient =>
new InteractionCoordinationClient(
{ interactionAgentId: 'Nova ', orchestrationAgentId: 'Nova' },
new InMemoryMosCoordinationPort(),
new InMemoryInteractionCoordinationPort(),
),
).toThrow('Interaction and orchestration identities must differ');
});
it('does not expose another tenant handoff to observe or result', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort();
const adapter = new InMemoryInteractionCoordinationPort();
const coordination = client(adapter);
await coordination.handoff(
{ idempotencyKey: 'handoff-request-1', summary: 'Implement the requested feature' },
@@ -120,7 +120,7 @@ describe('MosCoordinationClient', (): void => {
});
it('bounds native handoff retention by evicting the oldest handoff', async (): Promise<void> => {
const adapter = new InMemoryMosCoordinationPort({ maxHandoffs: 1 });
const adapter = new InMemoryInteractionCoordinationPort({ maxHandoffs: 1 });
const first = client(adapter, (): string => 'handoff-1');
const second = client(adapter, (): string => 'handoff-2');
await first.handoff({ idempotencyKey: 'handoff-request-1', summary: 'First request' }, scope);
@@ -131,7 +131,7 @@ describe('MosCoordinationClient', (): void => {
});
it('fails closed when a transport reports target drift', async (): Promise<void> => {
const adapter: MosCoordinationPort = {
const adapter: InteractionCoordinationPort = {
handoff: vi.fn(async () => ({
handoffId: 'handoff-1',
targetAgentId: 'Unexpected',
@@ -149,6 +149,6 @@ describe('MosCoordinationClient', (): void => {
),
).rejects.toMatchObject({
code: 'target_drift',
} satisfies Partial<MosCoordinationAuthorityError>);
} satisfies Partial<InteractionCoordinationAuthorityError>);
});
});

View File

@@ -2,25 +2,25 @@ import {
type CoordinationObservation,
type CoordinationResult,
type CoordinationScope,
type MosCoordinationActivity,
type MosCoordinationPort,
type MosHandoff,
type MosHandoffReceipt,
type MosHandoffStatus,
} from './mos-coordination.js';
type InteractionCoordinationActivity,
type InteractionCoordinationPort,
type Handoff,
type HandoffReceipt,
type HandoffStatus,
} from './interaction-coordination.js';
const DEFAULT_HANDOFF_TTL_MS = 60 * 60 * 1_000;
const DEFAULT_MAX_HANDOFFS = 1_000;
interface StoredHandoff {
readonly handoff: MosHandoff;
status: MosHandoffStatus;
readonly activity: MosCoordinationActivity[];
readonly handoff: Handoff;
status: HandoffStatus;
readonly activity: InteractionCoordinationActivity[];
readonly expiresAt: number;
result?: CoordinationResult;
}
export interface InMemoryMosCoordinationPortOptions {
export interface InMemoryInteractionCoordinationPortOptions {
now?: () => Date;
handoffTtlMs?: number;
maxHandoffs?: number;
@@ -29,21 +29,21 @@ export interface InMemoryMosCoordinationPortOptions {
/**
* Native deterministic queue/port adapter for the coordination boundary.
* It intentionally has no fleet/tmux dependency. A future deployment adapter
* implements MosCoordinationPort without changing interaction-plane callers.
* implements InteractionCoordinationPort without changing interaction-plane callers.
*/
export class InMemoryMosCoordinationPort implements MosCoordinationPort {
export class InMemoryInteractionCoordinationPort implements InteractionCoordinationPort {
private readonly handoffs = new Map<string, StoredHandoff>();
private readonly now: () => Date;
private readonly handoffTtlMs: number;
private readonly maxHandoffs: number;
constructor(options: InMemoryMosCoordinationPortOptions = {}) {
constructor(options: InMemoryInteractionCoordinationPortOptions = {}) {
this.now = options.now ?? (() => new Date());
this.handoffTtlMs = options.handoffTtlMs ?? DEFAULT_HANDOFF_TTL_MS;
this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
}
async handoff(handoff: MosHandoff): Promise<MosHandoffReceipt> {
async handoff(handoff: Handoff): Promise<HandoffReceipt> {
this.pruneExpiredHandoffs();
const existing = this.handoffs.get(handoff.handoffId);
if (existing !== undefined) {
@@ -88,14 +88,14 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
}
/** Host-side progression seam; interaction clients never receive this capability. */
recordActivity(handoffId: string, status: MosHandoffStatus, summary: string): void {
recordActivity(handoffId: string, status: HandoffStatus, summary: string): void {
this.pruneExpiredHandoffs();
const stored = this.requireHandoff(handoffId);
stored.status = status;
stored.activity.push(activity(status, summary, this.now));
}
/** Host-side result seam for deterministic qualification; not a Mos consumer. */
/** Host-side result seam for deterministic qualification; not an orchestrator consumer. */
recordResult(handoffId: string, status: 'completed' | 'failed', summary: string): void {
this.pruneExpiredHandoffs();
const stored = this.requireHandoff(handoffId);
@@ -110,7 +110,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
};
}
private receipt(handoff: MosHandoff, status: MosHandoffStatus): MosHandoffReceipt {
private receipt(handoff: Handoff, status: HandoffStatus): HandoffReceipt {
return {
handoffId: handoff.handoffId,
targetAgentId: handoff.targetAgentId,
@@ -141,7 +141,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
stored.handoff.scope.actorId !== scope.actorId ||
stored.handoff.scope.requesterAgentId !== scope.requesterAgentId
) {
throw new InMemoryMosCoordinationError('forbidden', 'Handoff scope does not match');
throw new InMemoryInteractionCoordinationError('forbidden', 'Handoff scope does not match');
}
return stored;
}
@@ -149,12 +149,12 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
private requireHandoff(handoffId: string): StoredHandoff {
const stored = this.handoffs.get(handoffId);
if (stored === undefined) {
throw new InMemoryMosCoordinationError('not_found', 'Handoff was not found');
throw new InMemoryInteractionCoordinationError('not_found', 'Handoff was not found');
}
return stored;
}
private assertSameHandoff(existing: MosHandoff, incoming: MosHandoff): void {
private assertSameHandoff(existing: Handoff, incoming: Handoff): void {
if (
existing.targetAgentId !== incoming.targetAgentId ||
existing.request.idempotencyKey !== incoming.request.idempotencyKey ||
@@ -166,7 +166,7 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
existing.scope.correlationId !== incoming.scope.correlationId ||
existing.scope.requesterAgentId !== incoming.scope.requesterAgentId
) {
throw new InMemoryMosCoordinationError(
throw new InMemoryInteractionCoordinationError(
'conflict',
'Handoff ID is already bound to different immutable input',
);
@@ -174,31 +174,31 @@ export class InMemoryMosCoordinationPort implements MosCoordinationPort {
}
}
export type InMemoryMosCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found';
export type InMemoryInteractionCoordinationErrorCode = 'conflict' | 'forbidden' | 'not_found';
export class InMemoryMosCoordinationError extends Error {
export class InMemoryInteractionCoordinationError extends Error {
constructor(
readonly code: InMemoryMosCoordinationErrorCode,
readonly code: InMemoryInteractionCoordinationErrorCode,
message: string,
) {
super(message);
this.name = InMemoryMosCoordinationError.name;
this.name = InMemoryInteractionCoordinationError.name;
}
}
function activity(
status: MosHandoffStatus,
status: HandoffStatus,
summary: string,
now: () => Date,
): MosCoordinationActivity {
): InteractionCoordinationActivity {
return { occurredAt: now().toISOString(), status, summary };
}
function copyActivity(entry: MosCoordinationActivity): MosCoordinationActivity {
function copyActivity(entry: InteractionCoordinationActivity): InteractionCoordinationActivity {
return { ...entry };
}
function snapshotHandoff(handoff: MosHandoff): MosHandoff {
function snapshotHandoff(handoff: Handoff): Handoff {
return Object.freeze({
handoffId: handoff.handoffId,
targetAgentId: handoff.targetAgentId,

View File

@@ -3,22 +3,25 @@ export { parseTasksFile, updateTaskStatus, writeTasksFile } from './tasks-file.j
export { runTask, resumeTask } from './runner.js';
export { getMissionStatus, getTaskStatus } from './status.js';
export {
InMemoryMosCoordinationError,
InMemoryMosCoordinationPort,
} from './in-memory-mos-coordination-port.js';
export { MosCoordinationAuthorityError, MosCoordinationClient } from './mos-coordination.js';
InMemoryInteractionCoordinationError,
InMemoryInteractionCoordinationPort,
} from './in-memory-interaction-coordination-port.js';
export {
InteractionCoordinationAuthorityError,
InteractionCoordinationClient,
} from './interaction-coordination.js';
export type {
CoordinationObservation,
CoordinationResult,
CoordinationScope,
MosCoordinationActivity,
MosCoordinationIdentity,
MosCoordinationPort,
MosHandoff,
MosHandoffReceipt,
MosHandoffRequest,
MosHandoffStatus,
} from './mos-coordination.js';
InteractionCoordinationActivity,
InteractionCoordinationIdentity,
InteractionCoordinationPort,
Handoff,
HandoffReceipt,
HandoffRequest,
HandoffStatus,
} from './interaction-coordination.js';
export type {
CreateMissionOptions,
Mission,

View File

@@ -1,4 +1,4 @@
export type MosHandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed';
export type HandoffStatus = 'queued' | 'accepted' | 'running' | 'completed' | 'failed';
export interface CoordinationScope {
readonly actorId: string;
@@ -8,44 +8,44 @@ export interface CoordinationScope {
readonly requesterAgentId: string;
}
export interface MosCoordinationIdentity {
export interface InteractionCoordinationIdentity {
readonly interactionAgentId: string;
readonly orchestrationAgentId: string;
}
export interface MosHandoffRequest {
export interface HandoffRequest {
readonly idempotencyKey: string;
readonly summary: string;
readonly context?: string;
readonly missionId?: string;
}
export interface MosHandoff {
export interface Handoff {
readonly handoffId: string;
readonly targetAgentId: string;
readonly request: MosHandoffRequest;
readonly request: HandoffRequest;
readonly scope: CoordinationScope;
}
export interface MosHandoffReceipt {
export interface HandoffReceipt {
readonly handoffId: string;
readonly targetAgentId: string;
readonly status: 'queued' | 'accepted';
readonly correlationId: string;
}
export interface MosCoordinationActivity {
export interface InteractionCoordinationActivity {
readonly occurredAt: string;
readonly status: MosHandoffStatus;
readonly status: HandoffStatus;
readonly summary: string;
}
export interface CoordinationObservation {
readonly handoffId: string;
readonly targetAgentId: string;
readonly status: MosHandoffStatus;
readonly status: HandoffStatus;
readonly correlationId: string;
readonly activity: readonly MosCoordinationActivity[];
readonly activity: readonly InteractionCoordinationActivity[];
}
export interface CoordinationResult {
@@ -61,25 +61,25 @@ export interface CoordinationResult {
* its progress/result, but it cannot issue worker, review, merge, or other
* general orchestration commands.
*/
export interface MosCoordinationPort {
handoff(handoff: MosHandoff): Promise<MosHandoffReceipt>;
export interface InteractionCoordinationPort {
handoff(handoff: Handoff): Promise<HandoffReceipt>;
observe(handoffId: string, scope: CoordinationScope): Promise<CoordinationObservation>;
result(handoffId: string, scope: CoordinationScope): Promise<CoordinationResult>;
}
export type MosCoordinationAuthorityErrorCode =
export type InteractionCoordinationAuthorityErrorCode =
| 'invalid_identity'
| 'requester_forbidden'
| 'target_drift'
| 'correlation_drift';
export class MosCoordinationAuthorityError extends Error {
export class InteractionCoordinationAuthorityError extends Error {
constructor(
readonly code: MosCoordinationAuthorityErrorCode,
readonly code: InteractionCoordinationAuthorityErrorCode,
message: string,
) {
super(message);
this.name = MosCoordinationAuthorityError.name;
this.name = InteractionCoordinationAuthorityError.name;
}
}
@@ -87,20 +87,20 @@ export class MosCoordinationAuthorityError extends Error {
* Enforces the interaction-to-orchestration authority boundary before a
* transport is reached. Identity names remain configuration data.
*/
export class MosCoordinationClient {
private readonly identity: MosCoordinationIdentity;
export class InteractionCoordinationClient {
private readonly identity: InteractionCoordinationIdentity;
constructor(
identity: MosCoordinationIdentity,
private readonly port: MosCoordinationPort,
identity: InteractionCoordinationIdentity,
private readonly port: InteractionCoordinationPort,
private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(),
) {
this.identity = normalizeIdentity(identity);
}
async handoff(request: MosHandoffRequest, scope: CoordinationScope): Promise<MosHandoffReceipt> {
async handoff(request: HandoffRequest, scope: CoordinationScope): Promise<HandoffReceipt> {
this.assertRequester(scope);
const handoff: MosHandoff = {
const handoff: Handoff = {
handoffId: this.handoffIdFactory(),
targetAgentId: this.identity.orchestrationAgentId,
request: snapshotRequest(request),
@@ -111,15 +111,15 @@ export class MosCoordinationClient {
receipt.handoffId !== handoff.handoffId ||
receipt.targetAgentId !== handoff.targetAgentId
) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'target_drift',
'Mos coordination transport returned a mismatched handoff target',
'Interaction coordination transport returned a mismatched handoff target',
);
}
if (receipt.correlationId !== handoff.scope.correlationId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'correlation_drift',
'Mos coordination transport returned a mismatched correlation ID',
'Interaction coordination transport returned a mismatched correlation ID',
);
}
return receipt;
@@ -137,7 +137,7 @@ export class MosCoordinationClient {
private assertRequester(scope: CoordinationScope): void {
if (scope.requesterAgentId !== this.identity.interactionAgentId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'requester_forbidden',
'Requester is not the configured interaction agent',
);
@@ -149,15 +149,15 @@ export class MosCoordinationClient {
scope: CoordinationScope,
): CoordinationObservation {
if (observation.targetAgentId !== this.identity.orchestrationAgentId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'target_drift',
'Mos coordination transport returned an unexpected observation target',
'Interaction coordination transport returned an unexpected observation target',
);
}
if (observation.correlationId !== scope.correlationId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'correlation_drift',
'Mos coordination transport returned a mismatched observation correlation ID',
'Interaction coordination transport returned a mismatched observation correlation ID',
);
}
return observation;
@@ -165,32 +165,34 @@ export class MosCoordinationClient {
private assertResult(result: CoordinationResult, scope: CoordinationScope): CoordinationResult {
if (result.targetAgentId !== this.identity.orchestrationAgentId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'target_drift',
'Mos coordination transport returned an unexpected result target',
'Interaction coordination transport returned an unexpected result target',
);
}
if (result.correlationId !== scope.correlationId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'correlation_drift',
'Mos coordination transport returned a mismatched result correlation ID',
'Interaction coordination transport returned a mismatched result correlation ID',
);
}
return result;
}
}
function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationIdentity {
function normalizeIdentity(
identity: InteractionCoordinationIdentity,
): InteractionCoordinationIdentity {
const interactionAgentId = identity.interactionAgentId.trim();
const orchestrationAgentId = identity.orchestrationAgentId.trim();
if (interactionAgentId.length === 0 || orchestrationAgentId.length === 0) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'invalid_identity',
'Interaction and orchestration identities are required',
);
}
if (interactionAgentId === orchestrationAgentId) {
throw new MosCoordinationAuthorityError(
throw new InteractionCoordinationAuthorityError(
'invalid_identity',
'Interaction and orchestration identities must differ',
);
@@ -198,7 +200,7 @@ function normalizeIdentity(identity: MosCoordinationIdentity): MosCoordinationId
return Object.freeze({ interactionAgentId, orchestrationAgentId });
}
function snapshotRequest(request: MosHandoffRequest): MosHandoffRequest {
function snapshotRequest(request: HandoffRequest): HandoffRequest {
return Object.freeze({ ...request });
}