Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"packageManager": "pnpm@11.24.0",
"scripts": {
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:check": "prettier --write . && git diff --exit-code -- src/application src/transports test/transports",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.base.json && pnpm --filter @mindrail/contracts typecheck",
"test": "vitest run",
Expand Down
174 changes: 174 additions & 0 deletions src/application/in-memory-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import type { Checkpoint } from '@mindrail/contracts';

import { RuntimeError } from '../runtime/errors.ts';
import { InMemoryControlPlane } from '../runtime/in-memory-control-plane.ts';
import type { ProtocolCommand } from '../runtime/protocol.ts';
import type { ApplicationDispatcher } from './ports.ts';
import type {
ApplicationCommand,
ApplicationCommandName,
ApplicationQuery,
ApplicationQueryName,
CommandFailure,
CommandResponse,
QueryFailure,
QueryResponse,
} from './protocol.ts';

export const IN_MEMORY_UNSUPPORTED_COMMANDS = [
'RegisterAgent',
'StartSession',
'HeartbeatSession',
'EndSession',
'RenewLease',
'ReleaseLease',
'BlockTask',
'ResumeTask',
'RequestPermission',
'RecordPermissionDecision',
] as const satisfies readonly ApplicationCommandName[];

export const IN_MEMORY_UNSUPPORTED_QUERIES = [
'ListGoals',
'ListGoalTasks',
'ListClaimableTasks',
'GetTaskExecutionView',
'GetAgent',
'GetSession',
'GetPermissionRequest',
'ListPendingHumanPermissions',
'ListPermissionDecisions',
] as const satisfies readonly ApplicationQueryName[];

export function createInMemoryApplicationDispatcher(
controlPlane: InMemoryControlPlane,
): ApplicationDispatcher {
return {
dispatchCommand(command) {
if (isCurrentRuntimeCommand(command)) {
return controlPlane.execute(command);
}
return unsupportedCommand(command);
},

dispatchQuery(query) {
try {
switch (query.query) {
case 'GetWorkspace':
return querySuccess(query, controlPlane.getWorkspace(query.workspaceId));
case 'GetGoal':
return querySuccess(query, controlPlane.getGoal(query.workspaceId, query.goalId));
case 'GetTask':
return querySuccess(query, controlPlane.getTask(query.workspaceId, query.taskId));
case 'GetLease':
return querySuccess(query, controlPlane.getLease(query.workspaceId, query.leaseId));
case 'ListTaskCheckpoints':
return querySuccess(
query,
pageCheckpoints(
controlPlane.listTaskCheckpoints(query.workspaceId, query.taskId),
query.limit,
query.cursor,
),
);
case 'ListGoals':
case 'ListGoalTasks':
case 'ListClaimableTasks':
case 'GetTaskExecutionView':
case 'GetAgent':
case 'GetSession':
case 'GetPermissionRequest':
case 'ListPendingHumanPermissions':
case 'ListPermissionDecisions':
return unsupportedQuery(query);
}
} catch (error) {
if (error instanceof RuntimeError) {
return queryFailure(query, error.code, error.message);
}
return queryFailure(query, 'INTERNAL_ERROR', 'Application query failed.');
}
},
};
}

function isCurrentRuntimeCommand(command: ApplicationCommand): command is ProtocolCommand {
return !IN_MEMORY_UNSUPPORTED_COMMANDS.includes(
command.command as (typeof IN_MEMORY_UNSUPPORTED_COMMANDS)[number],
);
}

function unsupportedCommand(command: ApplicationCommand): CommandFailure {
return {
protocolVersion: '0.1',
commandId: command.commandId,
...(command.correlationId === undefined ? {} : { correlationId: command.correlationId }),
replayed: false,
error: {
code: 'UNSUPPORTED_OPERATION',
message: `${command.command} is not integrated in this runtime composition.`,
retryable: false,
},
};
}

function unsupportedQuery(query: ApplicationQuery): QueryFailure {
return queryFailure(
query,
'UNSUPPORTED_OPERATION',
`${query.query} is not integrated in this runtime composition.`,
);
}

function querySuccess(query: ApplicationQuery, result: unknown): QueryResponse {
return {
protocolVersion: '0.1',
...(query.correlationId === undefined ? {} : { correlationId: query.correlationId }),
result,
};
}

function queryFailure(
query: ApplicationQuery,
code: QueryFailure['error']['code'],
message: string,
): QueryFailure {
return {
protocolVersion: '0.1',
...(query.correlationId === undefined ? {} : { correlationId: query.correlationId }),
error: { code, message, retryable: false },
};
}

function pageCheckpoints(
checkpoints: Checkpoint[],
limit: number,
cursor: string | undefined,
): { items: Checkpoint[]; nextCursor?: string } {
const offset = decodeCursor(cursor);
const items = checkpoints.slice(offset, offset + limit);
const nextOffset = offset + items.length;
return {
items,
...(nextOffset < checkpoints.length ? { nextCursor: encodeCursor(nextOffset) } : {}),
};
}

function decodeCursor(cursor: string | undefined): number {
if (cursor === undefined) return 0;
const match = /^c([0-9]+)$/.exec(cursor);
if (match?.[1] === undefined) {
throw new RuntimeError('INVALID_INPUT', 'Cursor is invalid for this query.');
}
const offset = Number(match[1]);
if (!Number.isSafeInteger(offset) || offset < 0) {
throw new RuntimeError('INVALID_INPUT', 'Cursor is invalid for this query.');
}
return offset;
}

function encodeCursor(offset: number): string {
return `c${offset}`;
}

export type InMemoryCommandResponse = CommandResponse;
32 changes: 32 additions & 0 deletions src/application/ports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ActorRef } from '@mindrail/contracts';

import type {
ApplicationCommand,
ApplicationCommandName,
ApplicationQuery,
ApplicationQueryName,
CommandResponse,
QueryResponse,
} from './protocol.ts';

export interface AuthenticatedPrincipal {
subject: string;
}

export interface PrincipalClaim {
workspaceId: string;
actor: ActorRef;
sessionId?: string;
operation:
| { kind: 'command'; name: ApplicationCommandName }
| { kind: 'query'; name: ApplicationQueryName };
}

export interface PrincipalAuthorizer {
authorize(principal: AuthenticatedPrincipal, claim: PrincipalClaim): boolean | Promise<boolean>;
}

export interface ApplicationDispatcher {
dispatchCommand(command: ApplicationCommand): CommandResponse | Promise<CommandResponse>;
dispatchQuery(query: ApplicationQuery): QueryResponse | Promise<QueryResponse>;
}
Loading
Loading