File size: 4,458 Bytes
5710dd0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | /**
* REST client for the pending-interactions endpoints:
* `GET/POST {baseUrl}/api/v1/sessions/{sessionId}/approvals[/...]` and
* `.../questions[/...]`.
*
* These are the only remaining surfaces for listing and answering a session's
* pending approvals/questions: the engine's interaction kernel is a
* process-global singleton with no DI channel on the `/api/v1/debug`
* dispatcher, so the inspector talks to the same public REST surface the
* clients use.
*/
export interface ApprovalWire {
readonly approval_id: string;
readonly session_id: string;
readonly turn_id?: number;
readonly tool_call_id: string;
readonly tool_name: string;
readonly action: string;
readonly tool_input_display: unknown;
readonly created_at: string;
readonly expires_at: string;
}
export interface QuestionOptionWire {
readonly id: string;
readonly label: string;
readonly description?: string;
}
export interface QuestionItemWire {
readonly id: string;
readonly question: string;
readonly header?: string;
readonly body?: string;
readonly options: readonly QuestionOptionWire[];
readonly multi_select?: boolean;
readonly allow_other?: boolean;
readonly other_label?: string;
readonly other_description?: string;
}
export interface QuestionWire {
readonly question_id: string;
readonly session_id: string;
readonly turn_id?: number;
readonly tool_call_id?: string;
readonly questions: readonly QuestionItemWire[];
readonly created_at: string;
}
export type QuestionAnswerWire =
| { readonly kind: 'single'; readonly option_id: string }
| { readonly kind: 'multi'; readonly option_ids: readonly string[] }
| { readonly kind: 'other'; readonly text: string }
| {
readonly kind: 'multi_with_other';
readonly option_ids: readonly string[];
readonly other_text: string;
}
| { readonly kind: 'skipped' };
export interface InteractionsApiOptions {
readonly baseUrl: string;
readonly token?: string;
readonly sessionId: string;
readonly fetchImpl?: typeof fetch;
}
async function call<T>(
opts: InteractionsApiOptions,
method: 'GET' | 'POST',
path: string,
body?: unknown,
expectedCodes: readonly number[] = [0],
): Promise<T> {
const headers: Record<string, string> = {};
const token = opts.token?.trim();
if (token !== undefined && token !== '') {
headers['authorization'] = `Bearer ${token}`;
}
if (body !== undefined) headers['content-type'] = 'application/json';
const doFetch = opts.fetchImpl ?? fetch;
const res = await doFetch(
`${opts.baseUrl}/api/v1/sessions/${encodeURIComponent(opts.sessionId)}${path}`,
{
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
},
);
const envelope = (await res.json()) as { code: number; msg: string; data: unknown };
if (!expectedCodes.includes(envelope.code)) {
throw new Error(`interactions call failed (${envelope.code}): ${envelope.msg}`);
}
return envelope.data as T;
}
export async function listPendingApprovals(
opts: InteractionsApiOptions,
): Promise<readonly ApprovalWire[]> {
const data = await call<{ items: readonly ApprovalWire[] }>(
opts,
'GET',
'/approvals?status=pending',
);
return data.items;
}
export async function listPendingQuestions(
opts: InteractionsApiOptions,
): Promise<readonly QuestionWire[]> {
const data = await call<{ items: readonly QuestionWire[] }>(
opts,
'GET',
'/questions?status=pending',
);
return data.items;
}
export function decideApproval(
opts: InteractionsApiOptions,
approvalId: string,
decision: 'approved' | 'rejected',
): Promise<unknown> {
return call(opts, 'POST', `/approvals/${encodeURIComponent(approvalId)}`, { decision });
}
export function answerQuestion(
opts: InteractionsApiOptions,
questionId: string,
answers: Readonly<Record<string, QuestionAnswerWire>>,
method?: 'enter' | 'space' | 'number_key' | 'click',
): Promise<unknown> {
return call(opts, 'POST', `/questions/${encodeURIComponent(questionId)}`, { answers, method });
}
export function dismissQuestion(
opts: InteractionsApiOptions,
questionId: string,
): Promise<unknown> {
// The dismiss endpoint reports success as a QUESTION_DISMISSED (40909)
// envelope (see kap-server's question dismiss action), not code 0.
return call(opts, 'POST', `/questions/${encodeURIComponent(questionId)}:dismiss`, undefined, [
0,
40909,
]);
}
|