feat(tess): wire durable interaction surfaces (#732)
This commit was merged in pull request #732.
This commit is contained in:
@@ -10,6 +10,7 @@ describe('generic interaction CLI', (): void => {
|
||||
expect(command.commands.map((item) => item.name()).sort()).toEqual([
|
||||
'attach',
|
||||
'chat',
|
||||
'enroll',
|
||||
'health',
|
||||
'recover',
|
||||
'send',
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Command } from 'commander';
|
||||
import { withAuth } from './with-auth.js';
|
||||
import {
|
||||
attachInteractionSession,
|
||||
enrollInteractionSession,
|
||||
fetchInteractionHealth,
|
||||
fetchInteractionSessions,
|
||||
fetchInteractionStatus,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
recoverInteractionSession,
|
||||
sendInteractionMessage,
|
||||
stopInteractionSession,
|
||||
streamInteractionSession,
|
||||
} from '../tui/gateway-api.js';
|
||||
|
||||
interface InteractionOptions {
|
||||
@@ -103,18 +105,48 @@ export function registerInteractionCommand(program: Command): Command {
|
||||
});
|
||||
|
||||
options(
|
||||
command.command('attach <sessionId>').description('Create a scoped read or write attachment'),
|
||||
command
|
||||
.command('enroll <sessionId> <provider> <runtimeSessionId>')
|
||||
.description('Bind an authorized runtime session to a durable conversation handle'),
|
||||
).action(
|
||||
async (
|
||||
sessionId: string,
|
||||
providerId: string,
|
||||
runtimeSessionId: string,
|
||||
opts: InteractionOptions,
|
||||
) => {
|
||||
const request = await authRequest(opts);
|
||||
print(
|
||||
await enrollInteractionSession(request.gateway, request.cookie, {
|
||||
...request,
|
||||
sessionId,
|
||||
providerId,
|
||||
runtimeSessionId,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
options(
|
||||
command
|
||||
.command('attach <sessionId>')
|
||||
.description('Create a scoped read or write attachment and stream the session'),
|
||||
)
|
||||
.option('--control', 'Request control mode (provider policy may deny it)')
|
||||
.action(async (sessionId: string, opts: InteractionOptions & { control?: boolean }) => {
|
||||
const request = await authRequest(opts);
|
||||
print(
|
||||
await attachInteractionSession(request.gateway, request.cookie, {
|
||||
...request,
|
||||
sessionId,
|
||||
mode: opts.control ? 'control' : 'read',
|
||||
}),
|
||||
);
|
||||
const attachment = await attachInteractionSession(request.gateway, request.cookie, {
|
||||
...request,
|
||||
sessionId,
|
||||
mode: opts.control ? 'control' : 'read',
|
||||
});
|
||||
print(attachment);
|
||||
for await (const event of streamInteractionSession(request.gateway, request.cookie, {
|
||||
...request,
|
||||
sessionId,
|
||||
})) {
|
||||
print(event);
|
||||
}
|
||||
});
|
||||
|
||||
const send = options(
|
||||
|
||||
@@ -406,6 +406,32 @@ export async function fetchInteractionTree(
|
||||
return handleResponse<unknown[]>(res, 'Failed to get interaction session tree');
|
||||
}
|
||||
|
||||
export async function enrollInteractionSession(
|
||||
gatewayUrl: string,
|
||||
sessionCookie: string,
|
||||
request: InteractionRequest & {
|
||||
sessionId: string;
|
||||
providerId: string;
|
||||
runtimeSessionId: string;
|
||||
},
|
||||
): Promise<{ status: string; sessionId: string }> {
|
||||
const res = await fetch(
|
||||
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/enroll`)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId),
|
||||
body: JSON.stringify({
|
||||
providerId: request.providerId,
|
||||
runtimeSessionId: request.runtimeSessionId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return handleResponse<{ status: string; sessionId: string }>(
|
||||
res,
|
||||
'Failed to enroll interaction session',
|
||||
);
|
||||
}
|
||||
|
||||
export async function attachInteractionSession(
|
||||
gatewayUrl: string,
|
||||
sessionCookie: string,
|
||||
@@ -422,6 +448,49 @@ export async function attachInteractionSession(
|
||||
return handleResponse<unknown>(res, 'Failed to attach interaction session');
|
||||
}
|
||||
|
||||
export async function* streamInteractionSession(
|
||||
gatewayUrl: string,
|
||||
sessionCookie: string,
|
||||
request: InteractionRequest & { sessionId: string; cursor?: string },
|
||||
): AsyncIterable<unknown> {
|
||||
const params = request.cursor?.trim()
|
||||
? `?${new URLSearchParams({ cursor: request.cursor.trim() }).toString()}`
|
||||
: '';
|
||||
const res = await fetch(
|
||||
`${gatewayUrl}${interactionPath(request.agentName, `/sessions/${encodeURIComponent(request.sessionId)}/stream${params}`)}`,
|
||||
{ headers: interactionHeaders(sessionCookie, gatewayUrl, request.correlationId) },
|
||||
);
|
||||
if (!res.ok || !res.body) {
|
||||
const body = await res.text().catch(() => '');
|
||||
throw new Error(`Failed to stream interaction session (${res.status}): ${body}`);
|
||||
}
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let pending = '';
|
||||
try {
|
||||
for (;;) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
pending += decoder.decode(chunk.value, { stream: true });
|
||||
for (;;) {
|
||||
const separator = pending.indexOf('\n\n');
|
||||
if (separator < 0) break;
|
||||
const frame = pending.slice(0, separator);
|
||||
pending = pending.slice(separator + 2);
|
||||
const data = frame
|
||||
.split('\n')
|
||||
.find((line: string): boolean => line.startsWith('data:'))
|
||||
?.slice('data:'.length)
|
||||
.trim();
|
||||
if (data) yield JSON.parse(data) as unknown;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendInteractionMessage(
|
||||
gatewayUrl: string,
|
||||
sessionCookie: string,
|
||||
|
||||
Reference in New Issue
Block a user