blob: a1449be86effcf21f9f0875e1bec7dc0cf77d73c [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { randomUUID } from 'node:crypto';
import { normalizeMessageContent } from '@maka/core/events';
import { type UserMessageInput } from '@maka/core/runtime-inputs';
import { agentGraphIdForRootSession } from '@maka/runtime/stream-graph-coordinator';
import {
recoverAgentGraphSupervisorContextOverflow,
type AgentGraphSupervisorContextRecoveryDiagnostic,
type AgentGraphSupervisorTurnOutcome,
} from '@maka/runtime/agent-graph-supervisor-wake';
import {
RuntimeHostedRootConflictError,
RuntimeMessageAuthorityInvariantError,
} from '@maka/runtime/message-authority';
import { type SessionManager } from '@maka/runtime/session-manager';
import type {
HostedExecutionAdmission,
HostedExecutionAdmissionResult,
HostedExecutionAuthority,
HostedExecutionSnapshot,
} from './hosted-execution-authority.js';
import {
waitForHostedExecutionIdleOrAbort,
waitForHostedExecutionTerminal,
} from './hosted-execution-wait.js';
type AgentGraphExecutionAuthority = Pick<
HostedExecutionAuthority,
'admit' | 'reconcile' | 'requestStop' | 'runExclusiveSessionOperation' | 'subscribe' | 'whenIdle'
>;
type AgentGraphRuntime = Pick<SessionManager, 'compactSession' | 'sendMessage'>;
export interface HostAgentGraphExecutionCoordinatorOptions {
readonly executions: AgentGraphExecutionAuthority;
readonly runtime: AgentGraphRuntime;
readonly newId?: () => string;
readonly currentGraphId?: (rootSessionId: string) => Promise<string>;
}
/** Maps Agent Graph wake attempts onto the domain-neutral Hosted Execution port. */
export class HostAgentGraphExecutionCoordinator {
readonly #executions: AgentGraphExecutionAuthority;
readonly #runtime: AgentGraphRuntime;
readonly #newId: () => string;
readonly #currentGraphId: (rootSessionId: string) => Promise<string>;
constructor(options: HostAgentGraphExecutionCoordinatorOptions) {
this.#executions = options.executions;
this.#runtime = options.runtime;
this.#newId = options.newId ?? randomUUID;
this.#currentGraphId =
options.currentGraphId ??
(async (rootSessionId) => agentGraphIdForRootSession(rootSessionId));
}
async run(
sessionId: string,
input: UserMessageInput,
abortSignal: AbortSignal,
isCurrent: () => Promise<boolean>,
): Promise<AgentGraphSupervisorTurnOutcome> {
await assertAgentGraphInput(sessionId, input, this.#currentGraphId);
const origin = input.origin;
if (origin?.kind !== 'agent_graph') {
throw new RuntimeMessageAuthorityInvariantError('Agent Graph execution lost its origin');
}
const execution = {
sessionId,
turnId: input.turnId,
runId: this.#newId(),
};
const userMessageId = this.#newId();
let admitted: HostedExecutionAdmissionResult;
for (;;) {
throwIfAborted(abortSignal);
if (!(await isCurrent())) return superseded(input.turnId);
let gateCancelled = false;
try {
admitted = await this.#executions.admit({
...execution,
userMessageId,
execution: {
kind: 'agent_graph_supervisor_wake',
graphId: origin.graphId,
wakeId: origin.wakeId,
attemptId: origin.attemptId,
},
content: normalizeMessageContent(input),
turnOrchestration: input.turnOrchestration,
admitExecution: async () => {
gateCancelled = !(await isCurrent());
return gateCancelled ? 'cancelled' : 'executing';
},
start: ({ runId, userMessageId: admittedMessageId, onRunStarted }) => {
if (runId !== execution.runId || admittedMessageId !== userMessageId) {
throw new RuntimeMessageAuthorityInvariantError(
'Hosted Execution changed the Agent Graph attempt identity',
);
}
return this.#runtime.sendMessage(sessionId, input, {
runId,
userMessageId,
durability: 'required',
onRunStarted: async (startedRunId) => {
if (startedRunId !== runId) {
throw new RuntimeMessageAuthorityInvariantError(
'Runtime changed the Agent Graph Run identity',
);
}
await onRunStarted();
},
});
},
} satisfies HostedExecutionAdmission);
break;
} catch (error) {
if (gateCancelled) return superseded(input.turnId);
if (!(error instanceof RuntimeHostedRootConflictError)) throw error;
const whenIdle = this.#executions.whenIdle(sessionId);
if (whenIdle) await waitForHostedExecutionIdleOrAbort(whenIdle, abortSignal);
}
}
const terminal = await this.#waitForTerminal(execution, admitted, abortSignal);
return classifyAgentGraphOutcome(terminal);
}
recoverContextOverflow(
sessionId: string,
compactTurnId: string,
abortSignal: AbortSignal,
): Promise<AgentGraphSupervisorContextRecoveryDiagnostic | undefined> {
return this.#executions.runExclusiveSessionOperation(
{ sessionId, abortSignal, stopSource: 'graph_supervisor' },
() =>
recoverAgentGraphSupervisorContextOverflow({
rootSessionId: sessionId,
compactTurnId,
abortSignal,
compactSession: (targetSessionId, input) =>
this.#runtime.compactSession(targetSessionId, input),
}),
);
}
async #waitForTerminal(
execution: {
readonly sessionId: string;
readonly turnId: string;
readonly runId: string;
},
admitted: HostedExecutionAdmissionResult,
abortSignal: AbortSignal,
): Promise<HostedExecutionSnapshot> {
let stopTask: Promise<unknown> | undefined;
const stop = (): void => {
stopTask ??= this.#executions.requestStop({
execution,
source: 'graph_supervisor',
});
void stopTask.catch(() => undefined);
};
abortSignal.addEventListener('abort', stop, { once: true });
if (abortSignal.aborted) stop();
try {
const terminal = await waitForHostedExecutionTerminal(
this.#executions,
execution,
admitted.snapshot,
{ completion: admitted.completion },
);
await admitted.settled;
return terminal;
} finally {
abortSignal.removeEventListener('abort', stop);
await stopTask;
}
}
}
async function assertAgentGraphInput(
sessionId: string,
input: UserMessageInput,
currentGraphId: (rootSessionId: string) => Promise<string>,
): Promise<void> {
if (
input.origin?.kind !== 'agent_graph' ||
input.origin.graphId !== (await currentGraphId(sessionId)) ||
input.turnOrchestration?.mode !== 'graph' ||
input.turnOrchestration.source !== 'host_api'
) {
throw new RuntimeMessageAuthorityInvariantError(
'Agent Graph supervisor execution requires the Session graph origin and Host Graph orchestration',
);
}
}
function classifyAgentGraphOutcome(
snapshot: HostedExecutionSnapshot,
): AgentGraphSupervisorTurnOutcome {
if (snapshot.status === 'completed') return { kind: 'completed', turnId: snapshot.turnId };
if (snapshot.status === 'cancelled') return { kind: 'aborted', turnId: snapshot.turnId };
if (snapshot.status === 'failed') {
if (snapshot.failureClass === 'context_overflow') {
return {
kind: 'context_overflow',
turnId: snapshot.turnId,
reason: snapshot.failureClass,
};
}
return {
kind: 'errored',
turnId: snapshot.turnId,
reason: `Turn ended with ${snapshot.failureClass}`,
};
}
return {
kind: 'errored',
turnId: snapshot.turnId,
reason: `Turn ended in non-terminal status ${snapshot.status}`,
};
}
function superseded(turnId: string): AgentGraphSupervisorTurnOutcome {
return {
kind: 'superseded',
turnId,
reason: 'Agent graph supervisor checkpoint was superseded before root admission.',
};
}
function throwIfAborted(signal: AbortSignal): void {
if (signal.aborted) {
throw new DOMException('Agent Graph supervisor execution was aborted', 'AbortError');
}
}