blob: 7af73261c25a637474ff0ca5a2f6745265233602 [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 { failureClassFromCompleteStopReason, type SessionEvent } from '@maka/core/events';
import { findProjectByIdentity } from '@maka/core/project';
import { type StoredMessage } from '@maka/core/session';
import type { UserMessageInput } from '@maka/core/runtime-inputs';
import type { ExecutionBoundaryReadModel } from '@maka/core/sandbox-boundary';
import type { SessionSummary } from '@maka/core/session';
import {
projectSessionCatalogSummary,
readRuntimeHostSessions,
readRuntimeHostProjects,
RuntimeHostOperationError,
type RuntimeHostConnection,
type RuntimeHostProfile,
} from '@maka/runtime-host/client';
import { runtimeHostProfileUsesHostWorkspace } from '@maka/runtime-host/profile-kind';
import type { InteractionPendingSnapshot, SessionCatalogItem } from '@maka/runtime-host/protocol';
import {
runMakaTextCliCore,
type MakaRunContext,
type MakaRunContextInput,
type MakaRunDeps,
type MakaRunEnvironmentDeps,
type MakaRunOutcome,
type MakaRunRuntime,
} from './run-command-core.js';
import {
connectRuntimeHostCli,
resolveRuntimeHostCliTarget,
type RuntimeHostCliConnectionContext,
} from './runtime-host-cli-context.js';
import {
createRuntimeHostMakaSessionDriver,
type RuntimeHostMakaSessionDriver,
} from './runtime-host-session-driver.js';
import type { CreateSessionRequest, MakaPreparedSessionTurn } from './session-driver.js';
import {
formatRuntimeHostCliTaskBlockers,
isRuntimeHostCliTaskBlocked,
readRuntimeHostCliTaskReadiness,
} from './runtime-host-task-readiness.js';
import { resolveMakaClientDataRoot } from './workspace-root.js';
const GRAPH_POLL_INTERVAL_MS = 25;
export interface RuntimeHostRunCommandDeps {
connect(
rootPath: string,
hostProfileId: string | undefined,
clientDataRoot: string,
): Promise<RuntimeHostCliConnectionContext>;
createContext(
connection: RuntimeHostConnection,
catalog: RuntimeHostCliConnectionContext['catalog'],
input: Parameters<MakaRunDeps['createContext']>[0],
profile: RuntimeHostProfile,
): MakaRunContext | Promise<MakaRunContext>;
run: typeof runMakaTextCliCore;
}
export interface RuntimeHostTextCliOptions {
readonly cliCommand: string;
readonly clientDataRoot: string;
}
export interface RuntimeHostRunContextDeps {
createDriver(
input: Parameters<typeof createRuntimeHostMakaSessionDriver>[0],
): RuntimeHostMakaSessionDriver;
}
export async function runRuntimeHostTextCli(
argv: readonly string[],
overrides: Partial<MakaRunEnvironmentDeps> = {},
commandOverrides: Partial<RuntimeHostRunCommandDeps> = {},
options: RuntimeHostTextCliOptions = {
cliCommand: 'maka',
clientDataRoot: resolveMakaClientDataRoot(),
},
): Promise<number> {
const commandDeps = { ...defaultRuntimeHostRunCommandDeps(), ...commandOverrides };
let connected: RuntimeHostCliConnectionContext | undefined;
const connect = async (
rootPath: string,
hostProfileId?: string,
): Promise<RuntimeHostCliConnectionContext> => {
connected ??= await commandDeps.connect(rootPath, hostProfileId, options.clientDataRoot);
return connected;
};
try {
return await commandDeps.run(
argv,
{
listSessions: async (rootPath, hostProfileId) =>
runtimeHostSessionSummaries(
await readRuntimeHostSessions((await connect(rootPath, hostProfileId)).connection),
),
createContext: async (input) => {
const context = await connect(input.workspaceRoot, input.hostProfileId);
const preparedInput = await prepareRuntimeHostRunInput(
context.connection,
context.catalog,
context.profile,
input,
options.cliCommand,
);
return commandDeps.createContext(
context.connection,
context.catalog,
preparedInput,
context.profile,
);
},
},
{ ...overrides, cliCommand: () => options.cliCommand },
);
} finally {
await connected?.close().catch(() => undefined);
}
}
function defaultRuntimeHostRunCommandDeps(): RuntimeHostRunCommandDeps {
return {
connect: (rootPath, hostProfileId, clientDataRoot) =>
connectRuntimeHostCli({
rootPath,
...(hostProfileId ? { profileId: hostProfileId } : {}),
clientDataRoot,
}),
createContext: (connection, catalog, input) =>
createRuntimeHostRunContext(connection, catalog, input),
run: runMakaTextCliCore,
};
}
export function createRuntimeHostRunContext(
connection: RuntimeHostConnection,
catalog: RuntimeHostCliConnectionContext['catalog'],
input: Parameters<MakaRunDeps['createContext']>[0],
overrides: Partial<RuntimeHostRunContextDeps> = {},
): MakaRunContext {
const target = resolveRuntimeHostCliTarget(catalog, {
...(input.requestedConnectionSlug ? { connectionSlug: input.requestedConnectionSlug } : {}),
...(input.requestedModel ? { model: input.requestedModel } : {}),
});
const contextDeps = {
createDriver: createRuntimeHostMakaSessionDriver,
...overrides,
};
const driver = contextDeps.createDriver({
connection,
cwd: input.cwd,
llmConnectionId: target.connection.connectionId,
llmConnectionSlug: target.connection.slug,
model: target.model,
executionLocation:
!input.hostProfileId || input.hostProfileId === 'local'
? { kind: 'client_path' }
: { kind: 'host' },
...(input.projectId ? { workspace: { kind: 'project', projectId: input.projectId } } : {}),
});
const runtime = new RuntimeHostRunRuntime(
connection,
driver,
input.runOutcomeObserver,
input.enableAgentGraph === true,
input.sessionCwdOverride,
input.maxSteps,
);
return {
runtime,
target: { connection: { slug: target.connection.slug }, model: target.model },
...(input.enableAgentGraph
? {
agentGraph: {
reserveActivity: () => ({ release: () => {} }),
waitForCompletion: (sessionId: string) => runtime.waitForGraphCompletion(sessionId),
},
}
: {}),
close: async () => runtime.close(),
};
}
async function prepareRuntimeHostRunInput(
connection: RuntimeHostConnection,
catalog: RuntimeHostCliConnectionContext['catalog'],
profile: RuntimeHostProfile,
input: Parameters<MakaRunDeps['createContext']>[0],
cliCommand: string,
): Promise<Parameters<MakaRunDeps['createContext']>[0]> {
let projectId = input.projectId;
if (runtimeHostProfileUsesHostWorkspace(profile.kind) && !input.resumeSessionId) {
if (!projectId) {
throw new Error(`Runtime Host profile ${profile.id} requires --project for a new Session`);
}
const project = findProjectByIdentity(await readRuntimeHostProjects(connection), projectId);
if (!project || project.archivedAt !== null || !project.available) {
throw new Error(`Runtime Host Project is unavailable: ${projectId}`);
}
projectId = project.id;
}
const preparedInput = projectId === input.projectId ? input : { ...input, projectId };
const snapshot = await readRuntimeHostCliTaskReadiness({
connection,
catalog,
cwd: preparedInput.cwd,
...(runtimeHostProfileUsesHostWorkspace(profile.kind)
? { workspaceState: 'ready' as const }
: {}),
...(preparedInput.requestedConnectionSlug
? { connectionSlug: preparedInput.requestedConnectionSlug }
: {}),
...(preparedInput.requestedModel ? { model: preparedInput.requestedModel } : {}),
});
if (isRuntimeHostCliTaskBlocked(snapshot)) {
throw new Error(
`Task is not ready:\n${formatRuntimeHostCliTaskBlockers(snapshot, cliCommand)}`,
);
}
return preparedInput;
}
type ActiveRuntimeHostTurn = {
readonly sessionId: string;
readonly turnId: string;
readonly runId: string;
outcome: TurnOutcomeClassifier;
};
class RuntimeHostRunRuntime implements MakaRunRuntime {
readonly #connection: RuntimeHostConnection;
readonly #driver: RuntimeHostMakaSessionDriver;
readonly #observer: ((outcome: MakaRunOutcome) => void | Promise<void>) | undefined;
readonly #graphEnabled: boolean;
readonly #sessionCwdOverride: MakaRunContextInput['sessionCwdOverride'];
readonly #maxSteps: number | undefined;
readonly #unsubscribeTranscriptReplacements: () => void;
#sessionId: string | undefined;
#activeTurn: ActiveRuntimeHostTurn | undefined;
#stopRequested = false;
#closed = false;
readonly #interactions: NonInteractiveInteractionController;
#graphAdmissionTurnIds = new Set<string>();
#latestTranscriptReplacement: readonly StoredMessage[] | undefined;
readonly #graphTerminalWaiters = new Map<
string,
Set<{
resolve(messages: readonly StoredMessage[]): void;
reject(error: Error): void;
timer: ReturnType<typeof setTimeout>;
}>
>();
constructor(
connection: RuntimeHostConnection,
driver: RuntimeHostMakaSessionDriver,
observer: ((outcome: MakaRunOutcome) => void | Promise<void>) | undefined,
graphEnabled: boolean,
sessionCwdOverride: MakaRunContextInput['sessionCwdOverride'],
maxSteps: number | undefined,
) {
this.#connection = connection;
this.#driver = driver;
this.#observer = observer;
this.#graphEnabled = graphEnabled;
this.#sessionCwdOverride = sessionCwdOverride;
this.#maxSteps = maxSteps;
this.#interactions = new NonInteractiveInteractionController(driver, (pending) =>
this.#stopForInteraction(pending),
);
this.#unsubscribeTranscriptReplacements = driver.subscribeTranscriptReplacements(
(sessionId, turnId, messages) => {
this.#acceptRootTranscript(sessionId, turnId, messages);
this.#acceptGraphTranscript(messages);
},
);
}
async createSession(input: CreateSessionRequest): Promise<SessionSummary> {
const created = await this.#driver.createSession(input);
this.#sessionId = created.id;
return created;
}
async readExecutionBoundary(sessionId: string): Promise<ExecutionBoundaryReadModel> {
await this.#attach(sessionId);
return this.#connection.request('session.execution_boundary.query', { sessionId });
}
async *sendMessage(sessionId: string, input: UserMessageInput): AsyncIterable<SessionEvent> {
await this.#attach(sessionId);
if (this.#stopRequested) throw new Error('Turn was cancelled before start');
if (input.turnOrchestration?.mode === 'graph') {
this.#graphAdmissionTurnIds = graphSupervisorTurnIds(await this.#driver.readMessages());
}
const maxSteps = input.maxSteps ?? this.#maxSteps;
const turn = await this.#driver.preparePrompt(input.text, {
turnId: input.turnId,
...(input.turnOrchestration ? { turnOrchestration: input.turnOrchestration } : {}),
...(maxSteps !== undefined ? { maxSteps } : {}),
});
if (!turn.runId) throw new Error('Runtime Host did not return a Run identity');
const activeTurn = {
sessionId: turn.sessionId,
turnId: turn.turnId,
runId: turn.runId,
outcome: new TurnOutcomeClassifier(turn.runId),
};
this.#activeTurn = activeTurn;
if (this.#stopRequested) {
await this.#stopTurn(activeTurn);
if (input.turnOrchestration?.mode === 'graph') await this.#stopGraph(sessionId);
}
try {
yield* this.#observeTurn(turn, activeTurn);
} finally {
if (this.#activeTurn === activeTurn) this.#activeTurn = undefined;
}
}
async respondToSandboxBoundary(
sessionId: string,
response: { requestId: string; decision: 'deny' },
): Promise<void> {
await this.#attach(sessionId);
await this.#driver.respondToSandboxBoundary(response);
}
async resumeLatest(sessionId: string): Promise<AsyncIterable<SessionEvent> | null> {
await this.#attach(sessionId);
const plan = await this.#connection.request('turn.resume.query', { sessionId });
return plan.disposition === 'ready' ? this.#driver.resumeLatest() : null;
}
async stopSession(sessionId: string): Promise<void> {
this.#stopRequested = true;
await this.#attach(sessionId);
const stops: Promise<unknown>[] = [
this.#activeTurn ? this.#stopTurn(this.#activeTurn) : this.#driver.stop(),
];
if (this.#graphEnabled) {
stops.push(this.#stopGraph(sessionId));
}
const settled = await Promise.allSettled(stops);
const failure = settled.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
);
this.#cancelGraphTerminalWaiters(new Error('Agent Graph wait was cancelled'));
if (failure) throw failure.reason;
}
async setExecutionBoundaryKind(sessionId: string, kind: 'managed' | 'bypass'): Promise<void> {
await this.#attach(sessionId);
await this.#driver.setPermissionMode(kind === 'bypass' ? 'bypass' : 'ask');
}
async waitForGraphCompletion(sessionId: string): Promise<void> {
let terminalStatus: 'completed' | 'failed' | 'stopped' | undefined;
for (;;) {
await this.#interactions.settle();
if (this.#stopRequested) throw new Error('Agent Graph wait was cancelled');
try {
const graph = await this.#connection.request('agent.graph.query', {
rootSessionId: sessionId,
});
await this.#interactions.settle();
if (this.#stopRequested) throw new Error('Agent Graph wait was cancelled');
if (graph.status === 'empty' || graph.status === 'completed') {
terminalStatus = 'completed';
break;
}
if (graph.status === 'failed' || graph.status === 'stopped') {
terminalStatus = graph.status;
break;
}
} catch (error) {
if (error instanceof RuntimeHostOperationError && error.code === 'not_found') return;
throw error;
}
await this.#interactions.race(delay(GRAPH_POLL_INTERVAL_MS));
}
if (terminalStatus !== 'completed') {
throw new Error(`Agent Graph ${terminalStatus}`);
}
await this.#interactions.settle();
let messages: readonly StoredMessage[] = await this.#driver.readMessages();
let graphTurnId = lastNewGraphSupervisorTurnId(messages, this.#graphAdmissionTurnIds);
let outcome = graphTurnId ? outcomeFromStoredTurn(messages, graphTurnId) : undefined;
if (graphTurnId && !outcome) {
messages = await this.#waitForGraphTurnTerminal(graphTurnId);
graphTurnId = lastNewGraphSupervisorTurnId(messages, this.#graphAdmissionTurnIds);
outcome = graphTurnId ? outcomeFromStoredTurn(messages, graphTurnId) : undefined;
if (!outcome)
throw new Error('Agent Graph final Turn did not reach a durable terminal boundary');
}
if (outcome) await this.#observer?.(outcome);
}
close(): Promise<void> {
this.#closed = true;
this.#interactions.close();
this.#unsubscribeTranscriptReplacements();
this.#cancelGraphTerminalWaiters(new Error('Runtime Host run context closed'));
return Promise.resolve();
}
async #attach(sessionId: string): Promise<void> {
if (this.#sessionId === sessionId) return;
const switched = await this.#driver.switchSession(sessionId);
if (
this.#sessionCwdOverride?.sessionId === sessionId &&
switched.summary.cwd !== this.#sessionCwdOverride.cwd
) {
const moveSession = this.#driver.moveSession;
if (!moveSession) {
throw new Error('The selected Runtime Host does not allow Client path relocation');
}
const moved = await moveSession(this.#sessionCwdOverride.cwd);
if (moved.cwd !== this.#sessionCwdOverride.cwd) {
throw new Error(
`Runtime Host cannot resume Session ${sessionId}: its working directory could not be canonicalized`,
);
}
}
this.#sessionId = sessionId;
}
async *#observeTurn(
turn: MakaPreparedSessionTurn,
active: ActiveRuntimeHostTurn,
): AsyncIterable<SessionEvent> {
const events = turn.events[Symbol.asyncIterator]();
for (;;) {
const next = await this.#interactions.race(events.next());
if (next.done) break;
const event = next.value;
if (
event.type === 'user_question_request' ||
event.type === 'form_request' ||
event.type === 'sandbox_boundary_request'
) {
continue;
}
active.outcome.accept(observationFromSessionEvent(event));
yield event;
}
await this.#interactions.settle();
await this.#observer?.(active.outcome.outcome('fail'));
}
async #stopTurn(turn: { sessionId: string; turnId: string; runId: string }): Promise<void> {
await this.#connection.request('turn.stop', {
sessionId: turn.sessionId,
turnId: turn.turnId,
runId: turn.runId,
});
}
async #stopGraph(sessionId: string): Promise<void> {
try {
await this.#connection.request('agent.graph.stop', { rootSessionId: sessionId });
} catch (error) {
if (!(error instanceof RuntimeHostOperationError) || error.code !== 'not_found') throw error;
}
}
#acceptGraphTranscript(messages: readonly StoredMessage[]): void {
this.#latestTranscriptReplacement = messages;
for (const [turnId, waiters] of this.#graphTerminalWaiters) {
if (!outcomeFromStoredTurn(messages, turnId)) continue;
this.#graphTerminalWaiters.delete(turnId);
for (const waiter of waiters) {
clearTimeout(waiter.timer);
waiter.resolve(messages);
}
}
}
#acceptRootTranscript(
sessionId: string,
turnId: string,
messages: readonly StoredMessage[],
): void {
const active = this.#activeTurn;
if (!active || active.sessionId !== sessionId || active.turnId !== turnId) return;
active.outcome = classifierFromStoredTurn(messages, turnId, active.runId);
}
#waitForGraphTurnTerminal(turnId: string): Promise<readonly StoredMessage[]> {
if (this.#closed) return Promise.reject(new Error('Runtime Host run context closed'));
if (this.#stopRequested) return Promise.reject(new Error('Agent Graph wait was cancelled'));
const latest = this.#latestTranscriptReplacement;
if (latest && outcomeFromStoredTurn(latest, turnId)) return Promise.resolve(latest);
return new Promise<readonly StoredMessage[]>((resolve, reject) => {
let waiters = this.#graphTerminalWaiters.get(turnId);
if (!waiters) {
waiters = new Set();
this.#graphTerminalWaiters.set(turnId, waiters);
}
const waiter = {
resolve,
reject,
timer: setTimeout(() => {
waiters?.delete(waiter);
if (waiters?.size === 0) this.#graphTerminalWaiters.delete(turnId);
reject(new Error('Agent Graph final Turn did not reach a durable terminal boundary'));
}, 45_000),
};
waiters.add(waiter);
});
}
async #stopForInteraction(pending: InteractionPendingSnapshot): Promise<void> {
this.#stopRequested = true;
const stops: Promise<unknown>[] = [
this.#stopTurn({
sessionId: pending.sessionId,
turnId: pending.turnId,
runId: pending.runId,
}),
];
if (this.#graphEnabled) stops.push(this.#stopGraph(pending.sessionId));
const settled = await Promise.allSettled(stops);
const failure = settled.find(
(result): result is PromiseRejectedResult => result.status === 'rejected',
);
if (failure) throw failure.reason;
}
#cancelGraphTerminalWaiters(error: Error): void {
for (const waiters of this.#graphTerminalWaiters.values()) {
for (const waiter of waiters) {
clearTimeout(waiter.timer);
waiter.reject(error);
}
}
this.#graphTerminalWaiters.clear();
}
}
function runtimeHostSessionSummaries(items: readonly SessionCatalogItem[]): SessionSummary[] {
return items.flatMap((item) => ('kind' in item ? [] : [projectSessionCatalogSummary(item)]));
}
type TurnOutcomeObservation =
| { readonly kind: 'output'; readonly text: string }
| {
readonly kind: 'terminal';
readonly update: 'replace' | 'if_unset';
readonly status: 'completed';
}
| {
readonly kind: 'terminal';
readonly update: 'replace' | 'if_unset';
readonly status: 'failed';
readonly failure: NonNullable<MakaRunOutcome['failure']>;
}
| {
readonly kind: 'tool_result';
readonly toolUseId: string;
readonly outcome: 'sandbox_failure' | 'success';
};
type TerminalOutcomeObservation = Extract<TurnOutcomeObservation, { kind: 'terminal' }>;
class TurnOutcomeClassifier {
readonly #outcomeId: string;
readonly #unresolvedSandboxFailures = new Set<string>();
#finalOutput: string | undefined;
#terminal: TerminalOutcomeObservation | undefined;
constructor(outcomeId: string) {
this.#outcomeId = outcomeId;
}
accept(observation: TurnOutcomeObservation | undefined): void {
switch (observation?.kind) {
case undefined:
return;
case 'output':
this.#finalOutput = observation.text;
return;
case 'terminal':
if (observation.update === 'replace' || this.#terminal === undefined) {
this.#terminal = observation;
}
return;
case 'tool_result': {
if (observation.outcome === 'sandbox_failure') {
this.#unresolvedSandboxFailures.add(observation.toolUseId);
}
// No clearing path: `maka run` denies every widening request, so the
// boundary cannot move mid-Turn and a later success cannot prove that
// a blocked call recovered. The failure stays unresolved to the end.
return;
}
}
}
outcome(incomplete: 'fail'): MakaRunOutcome;
outcome(incomplete: 'pending'): MakaRunOutcome | undefined;
outcome(incomplete: 'fail' | 'pending'): MakaRunOutcome | undefined {
const terminal = this.#terminal;
if (!terminal && incomplete === 'pending') return undefined;
const completed = terminal?.status === 'completed';
const sandboxBoundary = this.#unresolvedSandboxFailures.size > 0 ? 'unresolved' : 'none';
const failure =
terminal?.status === 'failed'
? terminal.failure
: {
class: 'missing_terminal_event',
message: 'Turn ended unexpectedly',
};
return {
outcomeId: this.#outcomeId,
status: completed ? 'completed' : 'failed',
...(completed && this.#finalOutput !== undefined ? { finalOutput: this.#finalOutput } : {}),
...(!completed ? { failure } : {}),
sandboxBoundary,
};
}
}
function observationFromSessionEvent(event: SessionEvent): TurnOutcomeObservation | undefined {
if (event.type === 'text_complete' && event.text.trim().length > 0) {
return { kind: 'output', text: event.text };
}
if (event.type === 'error') {
return {
kind: 'terminal',
update: 'replace',
status: 'failed',
failure: { class: event.reason ?? event.code ?? 'runtime_error', message: event.message },
};
}
if (event.type === 'abort') {
return {
kind: 'terminal',
update: 'replace',
status: 'failed',
failure: { class: 'aborted', message: 'Turn was cancelled' },
};
}
if (event.type === 'complete') {
return observationFromCompleteEvent(event);
}
return event.type === 'tool_result' ? observationFromToolResult(event) : undefined;
}
function observationFromStoredMessage(message: StoredMessage): TurnOutcomeObservation | undefined {
if (message.type === 'assistant' && message.text.trim().length > 0) {
return { kind: 'output', text: message.text };
}
if (message.type === 'turn_state' && message.status === 'completed') {
return { kind: 'terminal', update: 'replace', status: 'completed' };
}
if (message.type === 'turn_state' && message.status === 'aborted') {
return {
kind: 'terminal',
update: 'replace',
status: 'failed',
failure: { class: 'aborted', message: 'Turn was cancelled' },
};
}
if (message.type === 'turn_state' && message.status === 'failed') {
return {
kind: 'terminal',
update: 'replace',
status: 'failed',
failure: {
class: message.errorClass ?? 'runtime_error',
message: 'Agent Graph final Turn failed',
},
};
}
return message.type === 'tool_result' ? observationFromToolResult(message) : undefined;
}
function observationFromCompleteEvent(
event: Extract<SessionEvent, { type: 'complete' }>,
): TerminalOutcomeObservation {
if (event.stopReason === 'user_stop') {
return {
kind: 'terminal',
update: 'if_unset',
status: 'failed',
failure: { class: 'aborted', message: 'Turn was cancelled' },
};
}
const failureClass = failureClassFromCompleteStopReason(event.stopReason);
return failureClass
? {
kind: 'terminal',
update: 'if_unset',
status: 'failed',
failure: { class: failureClass },
}
: { kind: 'terminal', update: 'if_unset', status: 'completed' };
}
function observationFromToolResult(
result: Pick<Extract<SessionEvent, { type: 'tool_result' }>, 'content' | 'isError' | 'toolUseId'>,
): TurnOutcomeObservation | undefined {
if (result.isError && result.content.kind === 'text' && result.content.sandboxFailure) {
return {
kind: 'tool_result',
toolUseId: result.toolUseId,
outcome: 'sandbox_failure',
};
}
return result.isError
? undefined
: { kind: 'tool_result', toolUseId: result.toolUseId, outcome: 'success' };
}
function graphSupervisorTurnIds(messages: readonly StoredMessage[]): Set<string> {
return new Set(
messages.flatMap((message) =>
message.type === 'user' && message.origin?.kind === 'agent_graph' ? [message.turnId] : [],
),
);
}
function lastNewGraphSupervisorTurnId(
messages: readonly StoredMessage[],
admissionTurnIds: ReadonlySet<string>,
): string | undefined {
return [...messages]
.reverse()
.find(
(message) =>
message.type === 'user' &&
message.origin?.kind === 'agent_graph' &&
!admissionTurnIds.has(message.turnId),
)?.turnId;
}
function outcomeFromStoredTurn(
messages: readonly StoredMessage[],
turnId: string,
): MakaRunOutcome | undefined {
return classifierFromStoredTurn(messages, turnId, turnId).outcome('pending');
}
function classifierFromStoredTurn(
messages: readonly StoredMessage[],
turnId: string,
outcomeId: string,
): TurnOutcomeClassifier {
const classifier = new TurnOutcomeClassifier(outcomeId);
for (const message of messages) {
if (message.turnId === turnId) classifier.accept(observationFromStoredMessage(message));
}
return classifier;
}
class NonInteractiveInteractionController {
readonly #driver: RuntimeHostMakaSessionDriver;
readonly #stop: (pending: InteractionPendingSnapshot) => Promise<void>;
readonly #handled = new Set<string>();
readonly #tasks = new Set<Promise<void>>();
readonly #unsubscribe: () => void;
#failure: Error | undefined;
readonly #failureWaiters = new Set<(error: Error) => void>();
constructor(
driver: RuntimeHostMakaSessionDriver,
stop: (pending: InteractionPendingSnapshot) => Promise<void>,
) {
this.#driver = driver;
this.#stop = stop;
this.#unsubscribe = driver.subscribePendingInteractions((pending) => this.#accept(pending));
}
race<T>(operation: Promise<T>): Promise<T> {
this.throwIfFailed();
return new Promise((resolve, reject) => {
this.#failureWaiters.add(reject);
// Detach completed waits so the controller does not retain consumed event payloads.
operation.then(
(value) => {
this.#failureWaiters.delete(reject);
resolve(value);
},
(error) => {
this.#failureWaiters.delete(reject);
reject(error);
},
);
});
}
async settle(): Promise<void> {
await Promise.all([...this.#tasks]);
this.throwIfFailed();
}
throwIfFailed(): void {
if (this.#failure) throw this.#failure;
}
close(): void {
this.#unsubscribe();
}
#accept(pending: InteractionPendingSnapshot): void {
if (this.#handled.has(pending.interactionId)) return;
this.#handled.add(pending.interactionId);
const task = this.#handle(pending).catch((error) => {
this.#fail(error instanceof Error ? error : new Error(String(error)));
});
this.#tasks.add(task);
void task.finally(() => this.#tasks.delete(task));
}
async #handle(pending: InteractionPendingSnapshot): Promise<void> {
if (pending.request.kind === 'sandbox_boundary') {
await this.#driver.respondToSandboxBoundary({
requestId: pending.interactionId,
decision: 'deny',
});
return;
}
await this.#stop(pending);
throw new Error(
pending.request.kind === 'question'
? 'interactive user questions are unavailable in non-interactive mode'
: pending.request.kind === 'form'
? 'interactive user forms are unavailable in non-interactive mode'
: 'interactive permission requests are unavailable in non-interactive mode',
);
}
#fail(error: Error): void {
if (this.#failure) return;
this.#failure = error;
for (const reject of this.#failureWaiters) reject(error);
this.#failureWaiters.clear();
}
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}