Skip to content
1 change: 1 addition & 0 deletions apps/desktop/src/main/__tests__/browser-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ describe('browser tool execution', () => {
{
signal: new AbortController().signal,
accept: async () => undefined,
requestInteraction: async () => assert.fail('Unexpected provider interaction'),
},
);
assert.equal(resolved, 2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ function connectionHarness(
return provider.call(frame, {
signal: new AbortController().signal,
accept: async () => undefined,
requestInteraction: async () => assert.fail('Unexpected provider interaction'),
});
},
disconnect: () => resolveClosed?.(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ test('forwards Host cancellation to an admitted Desktop invocation', async () =>
const inFlight = provider.call(capabilityFrame(), {
signal: controller.signal,
accept: async () => undefined,
requestInteraction: async () => assert.fail('Unexpected provider interaction'),
});

await started;
Expand Down Expand Up @@ -723,5 +724,6 @@ async function call(
return provider.call(frame, {
signal: new AbortController().signal,
accept: async (evidence) => accept(evidence),
requestInteraction: async () => assert.fail('Unexpected provider interaction'),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ test('MCP capability publication freezes an accepted callable tool snapshot', as
accept: async () => {
accepted = true;
},
requestInteraction: async () => assert.fail('Unexpected provider interaction'),
},
);
assert.deepEqual(result, { content: [{ type: 'text', text: '{"path":"README.md"}' }] });
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/backend-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ export interface HostedInteractionBridge {
request: FormRequestEvent;
settlement: HostedFormSettlement;
}): Promise<void>;
/** Withdraw one exact producer-owned form without closing the surrounding Run. */
withdrawFormRequest(requestId: string): Promise<void>;
admitSandboxBoundaryRequest(input: {
request: SandboxBoundaryRequestEvent;
settlement: HostedSandboxBoundarySettlement;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const INTERACTION_FORM_VALUE_MAX_BYTES = 2_048;
export const INTERACTION_CLOSURE_REASONS = [
'turn_stopped',
'turn_terminal',
'producer_cancelled',
'timed_out',
'host_restarted',
'provider_disconnected',
Expand Down
177 changes: 177 additions & 0 deletions packages/runtime-host/src/__tests__/client-capability-channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,180 @@ test('Client Capability channel forwards admitted tool progress before the resul
]);
channel.close(new Error('test complete'));
});

test('Client Capability channel correlates one admitted nested form before the final result', async () => {
let registrationId = '';
const written: unknown[] = [];
let channel!: ClientCapabilityChannel;
const provider: ClientCapabilityProvider = {
offers: () => [
{
offerId: 'fixture',
version: '0',
affinity: 'call',
hostPathAccess: 'none',
label: 'Fixture',
tools: [{ serverId: 'fixture', name: 'deploy', inputSchema: { type: 'object' } }],
},
],
call: async (_frame, options) => {
await options.accept({ kind: 'none' });
const answer = await options.requestInteraction({
message: 'Choose a target',
requester: { name: 'deploy', source: 'Fixture' },
fields: [
{
kind: 'single_select',
name: 'target',
label: 'Target',
required: true,
options: [
{ value: 'staging', label: 'Staging' },
{ value: 'production', label: 'Production' },
],
},
],
});
assert.deepEqual(answer, { action: 'accept', values: { target: 'staging' } });
return { content: [{ type: 'text', text: 'deployed' }] };
},
};
channel = new ClientCapabilityChannel({
write: async (frame) => {
written.push(frame);
if (frame.kind === 'client.capability.accepted') {
queueMicrotask(() =>
channel.accept({
kind: 'client.capability.admitted',
invocationId: frame.invocationId,
}),
);
} else if (frame.kind === 'client.capability.interaction_request') {
queueMicrotask(() =>
channel.accept({
kind: 'client.capability.interaction_result',
invocationId: frame.invocationId,
interactionId: frame.interactionId,
result: { action: 'accept', values: { target: 'staging' } },
}),
);
}
},
replace: async (input) => {
registrationId = input.registrationId;
return { registrationId, revision: 1 };
},
unregister: async (input) => ({ registrationId: input.registrationId, revision: 2 }),
onFailure: (error) => {
throw error;
},
});
await channel.replace(provider, 1_000);
channel.accept({
kind: 'client.capability.call',
invocationId: 'nested-form',
registrationId,
offerId: 'fixture',
serverId: 'fixture',
toolName: 'deploy',
arguments: {},
sessionId: 'session',
turnId: 'turn',
toolCallId: 'tool-call',
});
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));

const interaction = written.find(
(frame) =>
typeof frame === 'object' &&
frame !== null &&
'kind' in frame &&
frame.kind === 'client.capability.interaction_request',
);
assert.ok(interaction);
assert.deepEqual(written.at(-1), {
kind: 'client.capability.result',
invocationId: 'nested-form',
result: { content: [{ type: 'text', text: 'deployed' }] },
});
channel.accept({ kind: 'client.capability.release', invocationId: 'nested-form' });
channel.close(new Error('test complete'));
});

test('Client Capability release rejects a pending nested form', async () => {
let registrationId = '';
let interactionStarted!: () => void;
const started = new Promise<void>((resolve) => {
interactionStarted = resolve;
});
let observedError: unknown;
let channel!: ClientCapabilityChannel;
const provider: ClientCapabilityProvider = {
offers: () => [
{
offerId: 'fixture',
version: '0',
affinity: 'call',
hostPathAccess: 'none',
label: 'Fixture',
tools: [{ serverId: 'fixture', name: 'deploy', inputSchema: { type: 'object' } }],
},
],
call: async (_frame, options) => {
await options.accept({ kind: 'none' });
try {
await options.requestInteraction({
message: 'Choose a target',
requester: { name: 'deploy' },
fields: [{ kind: 'boolean', name: 'confirm', label: 'Confirm', required: true }],
});
return { content: [] };
} catch (error) {
observedError = error;
throw error;
}
},
};
channel = new ClientCapabilityChannel({
write: async (frame) => {
if (frame.kind === 'client.capability.accepted') {
queueMicrotask(() =>
channel.accept({
kind: 'client.capability.admitted',
invocationId: frame.invocationId,
}),
);
} else if (frame.kind === 'client.capability.interaction_request') {
interactionStarted();
}
},
replace: async (input) => {
registrationId = input.registrationId;
return { registrationId, revision: 1 };
},
unregister: async (input) => ({ registrationId: input.registrationId, revision: 2 }),
onFailure: (error) => {
throw error;
},
});
await channel.replace(provider, 1_000);
channel.accept({
kind: 'client.capability.call',
invocationId: 'released-form',
registrationId,
offerId: 'fixture',
serverId: 'fixture',
toolName: 'deploy',
arguments: {},
sessionId: 'session',
turnId: 'turn',
toolCallId: 'tool-call',
});
await started;

channel.accept({ kind: 'client.capability.release', invocationId: 'released-form' });
await new Promise((resolve) => setImmediate(resolve));
assert.equal(observedError instanceof Error && observedError.name, 'AbortError');
channel.close(new Error('test complete'));
});
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,88 @@ test('service-only registration lifecycle does not invalidate model backends', a
assert.equal(modelToolChanges, 2);
});

test('close waits for nested Client Capability interaction cleanup', async () => {
const coordinator = createCoordinator();
let connection!: ClientCapabilityConnection;
let interactionStarted!: () => void;
const started = new Promise<void>((resolve) => {
interactionStarted = resolve;
});
let finishCleanup!: () => void;
const cleanup = new Promise<void>((resolve) => {
finishCleanup = resolve;
});
connection = coordinator.attachConnection(clientCapabilityConnectionIdentity('connection-a'), {
send: async (frame) => {
if (frame.kind === 'client.capability.call') {
connection.accept({
kind: 'client.capability.accepted',
invocationId: frame.invocationId,
admissionEvidence: { kind: 'none' },
});
} else if (frame.kind === 'client.capability.admitted') {
connection.accept({
kind: 'client.capability.interaction_request',
invocationId: frame.invocationId,
interactionId: 'interaction-a',
request: {
message: 'Choose a target',
requester: { name: 'deploy' },
fields: [
{ kind: 'string', name: 'target', label: 'Target', required: true, maxLength: 256 },
],
},
});
}
},
});
await replace(coordinator, 'connection-a', 'registration-a', 'deploy');
assert.deepEqual(await coordinator.bindSession('session-a', 'connection-a'), { ok: true });
const snapshot = coordinator.snapshotForSession('session-a');
assert.ok(snapshot);
const call = Promise.resolve(
snapshot.tools[0]!.impl(
{},
{
sessionId: 'session-a',
turnId: 'turn-a',
cwd: '/tmp',
toolCallId: 'tool-call-a',
abortSignal: new AbortController().signal,
emitOutput: () => undefined,
requestUserForm: async (_form, options) => {
interactionStarted();
const signal = options?.cancellationSignal;
assert.ok(signal);
if (!signal.aborted) {
await new Promise<void>((resolve) =>
signal.addEventListener('abort', () => resolve(), { once: true }),
);
}
await cleanup;
throw signal.reason;
},
},
),
);
void call.catch(() => undefined);
await started;
snapshot.release();

const connectionClosing = connection.close();
await new Promise<void>((resolve) => setImmediate(resolve));

let closed = false;
const closing = coordinator.close().then(() => {
closed = true;
});
await new Promise<void>((resolve) => setImmediate(resolve));
assert.equal(closed, false);
finishCleanup();
await Promise.all([connectionClosing, closing]);
await assert.rejects(call, ToolOutcomeUnknownError);
});

async function invoke(tool: NonNullable<ReturnType<typeof toolAt>>): Promise<unknown> {
return tool.impl(
{},
Expand Down
Loading