History

Deny AskUserQuestion after aborted waiting turn

- Return a deny permission result when the user cancels a pending Claude question - Add coverage for aborting AskUserQuestion while user input is pending

Julius Marminge committed 8 months ago

Commit fda3fa31 parent 004b6fe2 files · +77 −0Browse files

Unified · Split

2 files
  1. apps/server/src/provider/Layers/ClaudeAdapter.test.ts +68 −0
  2. apps/server/src/provider/Layers/ClaudeAdapter.ts +9 −0
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+68−0
@@ -2497,6 +2497,74 @@ describe("ClaudeAdapterLive", () => { · 2496 unmodified lines
2497 );2497 );
2498 });2498 });
2499 2499
2500 it.effect("denies AskUserQuestion when the waiting turn is aborted", () => {
2501 const harness = makeHarness();
2502 return Effect.gen(function* () {
2503 const adapter = yield* ClaudeAdapter;
2504
2505 const session = yield* adapter.startSession({
2506 threadId: THREAD_ID,
2507 provider: "claudeAgent",
2508 runtimeMode: "approval-required",
2509 });
2510
2511 yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain);
2512
2513 const createInput = harness.getLastCreateQueryInput();
2514 const canUseTool = createInput?.options.canUseTool;
2515 assert.equal(typeof canUseTool, "function");
2516 if (!canUseTool) {
2517 return;
2518 }
2519
2520 const controller = new AbortController();
2521 const permissionPromise = canUseTool(
2522 "AskUserQuestion",
2523 {
2524 questions: [
2525 {
2526 question: "Continue?",
2527 header: "Continue",
2528 options: [{ label: "Yes", description: "Proceed" }],
2529 multiSelect: false,
2530 },
2531 ],
2532 },
2533 {
2534 signal: controller.signal,
2535 toolUseID: "tool-ask-abort",
2536 },
2537 );
2538
2539 const requestedEvent = yield* Stream.runHead(adapter.streamEvents);
2540 assert.equal(requestedEvent._tag, "Some");
2541 if (requestedEvent._tag !== "Some" || requestedEvent.value.type !== "user-input.requested") {
2542 assert.fail("Expected user-input.requested event");
2543 return;
2544 }
2545 assert.equal(requestedEvent.value.threadId, session.threadId);
2546
2547 controller.abort();
2548
2549 const resolvedEvent = yield* Stream.runHead(adapter.streamEvents);
2550 assert.equal(resolvedEvent._tag, "Some");
2551 if (resolvedEvent._tag !== "Some" || resolvedEvent.value.type !== "user-input.resolved") {
2552 assert.fail("Expected user-input.resolved event");
2553 return;
2554 }
2555 assert.deepEqual(resolvedEvent.value.payload.answers, {});
2556
2557 const permissionResult = yield* Effect.promise(() => permissionPromise);
2558 assert.deepEqual(permissionResult, {
2559 behavior: "deny",
2560 message: "User cancelled tool execution.",
2561 } satisfies PermissionResult);
2562 }).pipe(
2563 Effect.provideService(Random.Random, makeDeterministicRandomService()),
2564 Effect.provide(harness.layer),
2565 );
2566 });
2567
2500 it.effect("writes provider-native observability records when enabled", () => {2568 it.effect("writes provider-native observability records when enabled", () => {
2501 const nativeEvents: Array<{2569 const nativeEvents: Array<{
2502 event?: {2570 event?: {
apps/server/src/provider/Layers/ClaudeAdapter.ts+9−0
@@ -2209,6 +2209,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { · 2208 unmodified lines
2209 );2209 );
2210 2210
2211 const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>();2211 const answersDeferred = yield* Deferred.make<ProviderUserInputAnswers>();
2212 let aborted = false;
2212 const pendingInput: PendingUserInput = {2213 const pendingInput: PendingUserInput = {
2213 questions,2214 questions,
2214 answers: answersDeferred,2215 answers: answersDeferred,
@@ -2242,6 +2243,7 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { · 27 unmodified lines
2242 if (!pendingUserInputs.has(requestId)) {2243 if (!pendingUserInputs.has(requestId)) {
2243 return;2244 return;
2244 }2245 }
2246 aborted = true;
2245 pendingUserInputs.delete(requestId);2247 pendingUserInputs.delete(requestId);
2246 Effect.runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers));2248 Effect.runFork(Deferred.succeed(answersDeferred, {} as ProviderUserInputAnswers));
2247 };2249 };
@@ -2272,6 +2274,13 @@ function makeClaudeAdapter(options?: ClaudeAdapterLiveOptions) { · 24 unmodified lines
2272 },2274 },
2273 });2275 });
2274 2276
2277 if (aborted) {
2278 return {
2279 behavior: "deny",
2280 message: "User cancelled tool execution.",
2281 } satisfies PermissionResult;
2282 }
2283
2275 // Return the answers to the SDK in the expected format:2284 // Return the answers to the SDK in the expected format:
2276 // { questions: [...], answers: { questionText: selectedLabel } }2285 // { questions: [...], answers: { questionText: selectedLabel } }
2277 return {2286 return {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t