blob: 78156730d1afb6361912d14729427d6a1009d9aa [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 { requireCount, requireEntityId, requireExactRecord, requireUtf8String } from './codec.js';
import { invalidProtocolFrame } from './errors.js';
import { defineOperation } from './operation-spec.js';
export const SESSION_EVENT_WAIT_PAGE_SIZE = 64;
export type SessionEventWaitConsumer =
| { kind: 'tool_invocation'; runId: string; invocationId: string; operationId: string }
| {
kind: 'goal_waiting';
goalId: string;
controlGeneration: number;
expectedGoalRevision: number;
};
export interface SessionEventWaitCancelInput {
sessionId: string;
waitId: string;
consumer: SessionEventWaitConsumer;
}
export interface SessionEventWaitProjection extends SessionEventWaitCancelInput {
sourceTurnId: string;
resource: { providerId: string; resourceType: string; resourceId: string };
deadlineAt: number | null;
phase: 'observing' | 'result_ready';
observation: 'checking' | 'pending' | 'unknown' | null;
reasonCode: string | null;
}
export interface SessionEventWaitsProjection {
items: SessionEventWaitProjection[];
nextCursor: string | null;
}
export interface SessionEventWaitQueryInput {
sessionId: string;
afterWaitId?: string;
}
export interface SessionEventWaitQueryResult extends SessionEventWaitsProjection {
sessionId: string;
}
export interface SessionEventWaitCancelResult {
sessionId: string;
outcome: 'cancelled' | 'conflict';
}
function consumer(value: unknown): SessionEventWaitConsumer {
const kind = value && typeof value === 'object' ? (value as { kind?: unknown }).kind : undefined;
if (kind === 'tool_invocation') {
const r = requireExactRecord(value, 'Tool wait consumer', [
'kind',
'runId',
'invocationId',
'operationId',
]);
return {
kind,
runId: requireEntityId(r.runId, 'runId'),
invocationId: requireEntityId(r.invocationId, 'invocationId'),
operationId: requireEntityId(r.operationId, 'operationId'),
};
}
if (kind === 'goal_waiting') {
const r = requireExactRecord(value, 'Goal waiting consumer', [
'kind',
'goalId',
'controlGeneration',
'expectedGoalRevision',
]);
return {
kind,
goalId: requireEntityId(r.goalId, 'goalId'),
controlGeneration: requireCount(r.controlGeneration, 'Control generation'),
expectedGoalRevision: requireCount(r.expectedGoalRevision, 'Goal revision'),
};
}
throw invalidProtocolFrame('Invalid event wait consumer');
}
export function decodeSessionEventWaitCancelInput(value: unknown): SessionEventWaitCancelInput {
const r = requireExactRecord(value, 'Event wait cancellation', [
'sessionId',
'waitId',
'consumer',
]);
return {
sessionId: requireEntityId(r.sessionId, 'sessionId'),
waitId: requireEntityId(r.waitId, 'waitId'),
consumer: consumer(r.consumer),
};
}
export function decodeSessionEventWaitsProjection(value: unknown): SessionEventWaitsProjection {
const r = requireExactRecord(value, 'Session event waits', ['items', 'nextCursor']);
if (!Array.isArray(r.items) || r.items.length > SESSION_EVENT_WAIT_PAGE_SIZE)
throw invalidProtocolFrame('Invalid event wait page');
const seen = new Set<string>();
const items = r.items.map((item): SessionEventWaitProjection => {
const w = requireExactRecord(item, 'Event wait projection', [
'sessionId',
'waitId',
'consumer',
'sourceTurnId',
'resource',
'deadlineAt',
'phase',
'observation',
'reasonCode',
]);
const resource = requireExactRecord(w.resource, 'Event wait resource', [
'providerId',
'resourceType',
'resourceId',
]);
if (
(w.phase !== 'observing' && w.phase !== 'result_ready') ||
![null, 'checking', 'pending', 'unknown'].includes(w.observation as never)
)
throw invalidProtocolFrame('Invalid event wait phase');
if (
(w.phase === 'result_ready' && w.observation !== null) ||
(w.phase === 'observing' && w.observation === null)
)
throw invalidProtocolFrame('Inconsistent event wait phase');
const waitId = requireEntityId(w.waitId, 'waitId');
if (seen.has(waitId)) throw invalidProtocolFrame('Duplicate event wait');
seen.add(waitId);
return {
sessionId: requireEntityId(w.sessionId, 'sessionId'),
waitId,
consumer: consumer(w.consumer),
sourceTurnId: requireEntityId(w.sourceTurnId, 'sourceTurnId'),
resource: {
providerId: requireUtf8String(resource.providerId, 'Provider', 128),
resourceType: requireUtf8String(resource.resourceType, 'Resource type', 128),
resourceId: requireUtf8String(resource.resourceId, 'Resource reference', 512),
},
deadlineAt: w.deadlineAt === null ? null : requireCount(w.deadlineAt, 'Deadline'),
phase: w.phase,
observation: w.observation as SessionEventWaitProjection['observation'],
reasonCode: w.reasonCode === null ? null : requireUtf8String(w.reasonCode, 'Reason', 128),
};
});
return {
items,
nextCursor: r.nextCursor === null ? null : requireEntityId(r.nextCursor, 'nextCursor'),
};
}
const errors = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'not_found',
'session_archived',
'internal_failure',
] as const;
export const SESSION_EVENT_WAIT_OPERATION_SPECS = {
'session.event-waits.query': defineOperation({
mode: 'query',
availability: 'ready',
errors,
decodeInput(value): SessionEventWaitQueryInput {
const r = requireExactRecord(value, 'Event wait query', [
'sessionId',
...(value && typeof value === 'object' && Object.hasOwn(value, 'afterWaitId')
? ['afterWaitId']
: []),
]);
return {
sessionId: requireEntityId(r.sessionId, 'sessionId'),
...(r.afterWaitId === undefined
? {}
: { afterWaitId: requireEntityId(r.afterWaitId, 'afterWaitId') }),
};
},
decodeOutput(value): SessionEventWaitQueryResult {
const r = requireExactRecord(value, 'Event wait query result', [
'sessionId',
'items',
'nextCursor',
]);
const sessionId = requireEntityId(r.sessionId, 'sessionId');
const page = decodeSessionEventWaitsProjection({ items: r.items, nextCursor: r.nextCursor });
if (page.items.some((w) => w.sessionId !== sessionId))
throw invalidProtocolFrame('Event wait belongs to another Session');
return { sessionId, ...page };
},
assertOutputForInput(input, output) {
if (input.sessionId !== output.sessionId)
throw invalidProtocolFrame('Event wait query Session mismatch');
},
}),
'session.event-wait.cancel': defineOperation({
mode: 'control',
availability: 'ready',
errors,
decodeInput: decodeSessionEventWaitCancelInput,
decodeOutput(value): SessionEventWaitCancelResult {
const r = requireExactRecord(value, 'Event wait cancellation result', [
'sessionId',
'outcome',
]);
if (r.outcome !== 'cancelled' && r.outcome !== 'conflict')
throw invalidProtocolFrame('Invalid event wait cancellation outcome');
return { sessionId: requireEntityId(r.sessionId, 'sessionId'), outcome: r.outcome };
},
assertOutputForInput(input, output) {
if (input.sessionId !== output.sessionId)
throw invalidProtocolFrame('Event wait cancellation Session mismatch');
},
}),
} as const;