blob: ce28216e3637c467b1f9afba533fd116947bfe4e [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 { z } from 'zod';
import {
WORKHUB_COORDINATION_SESSION_ID,
type WorkHubDelegationAssignedMessage,
} from '@maka/core/session';
import type { MakaTool } from '@maka/runtime/tool-runtime';
import { isSessionNotFoundError, type ExecutionStoresWriter } from '@maka/storage/execution-stores';
import type { RootTurnCoordinator } from './root-turn-coordinator.js';
import type { HostMessageCoordinator } from './message-coordinator.js';
import type { SessionAdmissionGate, SessionAdmissionLease } from './session-admission-gate.js';
import { projectSessionInteractions } from './interaction-projection.js';
import {
HostWorkHubResultCoordinator,
type WorkHubResultObservation,
} from './workhub-result-coordinator.js';
export function createWorkHubResultRuntime(options: {
stores: ExecutionStoresWriter<'interactive'>;
executions: RootTurnCoordinator;
messages: HostMessageCoordinator;
admission: SessionAdmissionGate;
readTurnResult(sessionId: string, turnId: string): Promise<string>;
acquireResidency(): { release(): void };
onError(error: unknown): void;
}) {
const { stores, executions, messages, admission, readTurnResult } = options;
async function listAssignments() {
const targets = (await stores.sessionStore.listHeaders())
.filter((h) => h.id !== WORKHUB_COORDINATION_SESSION_ID && !h.isArchived)
.map((h) => h.id);
const result: WorkHubDelegationAssignedMessage[] = [];
for (let i = 0; i < targets.length; i += 256)
result.push(
...(await stores.sessionStore.readActiveWorkHubAssignmentsByTarget(
targets.slice(i, i + 256),
)),
);
return result.filter((a) => a.returnResults);
}
async function pending(sessionId: string) {
return projectSessionInteractions(
await stores.interactionStore.listSessionPending(sessionId),
await stores.sessionStore.listPendingSandboxBoundaryRequests(sessionId),
).pending;
}
async function isActive(assignment: WorkHubDelegationAssignedMessage): Promise<boolean> {
try {
const header = await stores.sessionStore.readHeaderSnapshot(assignment.targetSessionId);
if (header.isArchived) return false;
const active = await stores.sessionStore.readActiveWorkHubAssignmentsByTarget([
assignment.targetSessionId,
]);
if (!active.some((a) => a.delegationId === assignment.delegationId && a.returnResults))
return false;
// Retirement intent takes precedence even before its final receipt exists.
if (await stores.sessionStore.readWorkHubReplacement(assignment.delegationId)) return false;
if (await stores.sessionStore.readWorkHubStopRequest(assignment.delegationId)) {
const resolution = await stores.sessionStore.readWorkHubStopResolution(
assignment.delegationId,
);
if (resolution?.outcome !== 'not_owned') return false;
}
return true;
} catch (error) {
if (isSessionNotFoundError(error)) return false;
throw error;
}
}
async function inspectLocked(
assignment: WorkHubDelegationAssignedMessage,
lease: SessionAdmissionLease,
includeResult = true,
): Promise<WorkHubResultObservation | undefined> {
try {
// The lightweight sweep receives active assignments from listAssignments.
// Delivery and tool reads still revalidate under both Session admissions.
if (includeResult && !(await isActive(assignment))) return undefined;
const disposition = await messages.readMessageExecutionDispositionAdmitted(
assignment.targetSessionId,
assignment.targetMessageId,
lease,
);
if (disposition.kind === 'cancelled') {
return {
turnId: assignment.targetTurnId,
runId: `message:${assignment.targetMessageId}`,
eventKey: 'message_cancelled_before_execution',
status: 'cancelled',
result: 'The delegated message was cancelled before execution.',
details: { abortSource: 'message_cancelled_before_execution' },
sharedTurn: false,
};
}
if (disposition.kind !== 'owned_root' && disposition.kind !== 'shared_turn') return undefined;
const identity = await executions.readLatestRootTurnLineage({
sessionId: assignment.targetSessionId,
turnId: disposition.turnId,
runId: disposition.runId,
});
const snapshot = await executions.read(identity);
const sharedTurn = disposition.kind === 'shared_turn';
if (snapshot.status === 'waiting_for_user' || snapshot.status === 'running') {
const requests = (await pending(assignment.targetSessionId)).filter(
(p) => p.turnId === identity.turnId,
);
if (!requests.length) return undefined;
return {
turnId: identity.turnId,
runId: identity.runId,
eventKey:
'interaction:' +
requests
.map((p) => p.interactionId)
.sort()
.join(','),
status: 'waiting_for_user',
result:
'The delegated task needs user input. This existing execution is waiting for an interaction in the original task. Handle it there before sending another instruction.',
details: requests.map((p) => ({ interactionId: p.interactionId, request: p.request })),
sharedTurn,
};
}
if (
snapshot.status !== 'completed' &&
snapshot.status !== 'failed' &&
snapshot.status !== 'cancelled'
)
return undefined;
const answer = includeResult
? await readTurnResult(assignment.targetSessionId, identity.turnId)
: '';
return {
turnId: identity.turnId,
runId: identity.runId,
eventKey: snapshot.terminalEventId,
status: snapshot.status,
result: answer,
details:
snapshot.status === 'failed'
? {
failureClass: snapshot.failureClass ?? null,
message: snapshot.failureMessage ?? null,
}
: snapshot.status === 'cancelled'
? { abortSource: snapshot.abortSource ?? null }
: null,
sharedTurn,
};
} catch (error) {
if (isSessionNotFoundError(error)) return undefined;
throw error;
}
}
async function inspect(
assignment: WorkHubDelegationAssignedMessage,
lease?: SessionAdmissionLease,
includeResult = true,
) {
return lease
? inspectLocked(assignment, lease, includeResult)
: admission.runMany([WORKHUB_COORDINATION_SESSION_ID, assignment.targetSessionId], (lane) =>
inspectLocked(assignment, lane, includeResult),
);
}
const coordinator = new HostWorkHubResultCoordinator({
listAssignments,
inspect,
deliver: (origin, prepare) => executions.startWorkHubResult(origin, prepare),
acquireResidency: options.acquireResidency,
onError: options.onError,
});
function notify(sessionId: string): void {
coordinator.notify(sessionId);
}
const parameters = z
.object({
actionId: z.string().min(1),
operation: z.literal('read').default('read'),
offset: z.number().int().nonnegative().default(0),
})
.strict();
const tool: MakaTool<z.infer<typeof parameters>> = {
name: 'WorkHubResult',
description:
'Read a delegated task result in pages. For missing information, ask in WorkHub and send the answer to the target as a subsequent instruction. Use actionId from a Host result notification. Never use this tool to approve permissions. The read operation returns Unicode character offsets.',
parameters,
categoryHint: 'read',
recoveryMode: 'never_auto_retry',
async impl(raw, ctx) {
const input = parameters.parse(raw);
if (ctx.sessionId !== WORKHUB_COORDINATION_SESSION_ID)
throw new Error('WorkHub result access requires the coordination Session');
const assignment = await stores.sessionStore.readWorkHubAssignment(input.actionId);
if (!assignment?.returnResults) throw new Error('Unknown WorkHub result assignment');
const observation = await inspect(assignment);
if (!observation) {
if (input.operation !== 'read') throw new Error('No pending delegated question');
const active = await admission.runMany(
[WORKHUB_COORDINATION_SESSION_ID, assignment.targetSessionId],
() => isActive(assignment),
);
return active
? {
status: 'pending',
targetSessionId: assignment.targetSessionId,
message:
'No result is ready yet. The Host will automatically notify WorkHub when a result or user question is available. Acknowledge the delegation and end this response; do not poll tools to wait.',
}
: { status: 'obsolete', targetSessionId: assignment.targetSessionId };
}
if (input.operation === 'read') {
const chars = Array.from(observation.result),
end = input.offset + 16000;
return {
...observation,
result: chars.slice(input.offset, end).join(''),
nextOffset: end < chars.length ? end : null,
};
}
},
};
// Exposed for the archive guard: the receipt read needs the same observation
// (and therefore the same event identity) delivery itself is keyed on.
return { coordinator, tool, notify, inspect };
}