agent-relay-runner 0.127.9 → 0.127.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.127.
|
|
3
|
+
"version": "0.127.10",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"agent-relay-providers": "0.104.4",
|
|
26
|
-
"agent-relay-sdk": "0.2.
|
|
26
|
+
"agent-relay-sdk": "0.2.123",
|
|
27
27
|
"callmux": "0.23.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
package/src/control-server.ts
CHANGED
|
@@ -323,34 +323,19 @@ async function handlePermissionRequest(
|
|
|
323
323
|
const toolInput = isRecord(body.tool_input) ? body.tool_input : undefined;
|
|
324
324
|
const approvalId = crypto.randomUUID();
|
|
325
325
|
|
|
326
|
-
// #1173: a PreToolUse "questions" prompt (the provider's AskUserQuestion) used to block this hook
|
|
327
|
-
// response for up to ~15 minutes waiting on the human — which meant the turn's reasoning that
|
|
328
|
-
// led to the question stayed buffered in the provider's process (never flushed to the transcript
|
|
329
|
-
// JSONL) for exactly as long as the human took to answer, so the dashboard could only ever
|
|
330
|
-
// show the question AFTER the reasoning that motivated it. Denying the tool call immediately
|
|
331
|
-
// is the ordinary, already-supported "rejected tool call" hook contract: the provider persists the
|
|
332
|
-
// buffered turn + a rejected tool_result right away, on its own, same as any other denied tool
|
|
333
|
-
// — so the settle-poll harvest below (previously doomed to time out for this one case) now
|
|
334
|
-
// finds the freshly-flushed anchor within moments. The human's eventual answer is delivered
|
|
335
|
-
// afterward as a normal injected prompt (see the `.then` below), not as this hook's response.
|
|
336
326
|
const probeView = handler.buildView(approvalId, body, true);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
327
|
+
const answerInput = permissionRequestQuestionAnswers(body);
|
|
328
|
+
if (body.hook_event_name === "PermissionRequest" && probeView.kind === "questions" && answerInput) {
|
|
329
|
+
return Response.json(handler.buildHookResponse({ approvalId, decision: "answer", answers: answerInput }, body));
|
|
330
|
+
}
|
|
331
|
+
if (
|
|
332
|
+
(body.hook_event_name === "PreToolUse" || body.hook_event_name === "PermissionRequest")
|
|
333
|
+
&& probeView.kind === "questions"
|
|
334
|
+
&& handler.buildAnswerInjectionPrompt
|
|
335
|
+
) {
|
|
336
|
+
return holdQuestionWithImmediateDeny({
|
|
343
337
|
options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput,
|
|
344
|
-
|
|
345
|
-
recommendedAction: "Answer the question in Agent Relay.",
|
|
346
|
-
suppressResolvedStatus: true,
|
|
347
|
-
}).then(({ decision, view }) => {
|
|
348
|
-
const text = decision.decision === "answer" && decision.answers && Object.keys(decision.answers).length
|
|
349
|
-
? handler.buildAnswerInjectionPrompt!(view, decision, body)
|
|
350
|
-
: pendingQuestionDismissalPrompt(decision);
|
|
351
|
-
return options.onAnswerInject?.({ approvalId, text });
|
|
352
|
-
}).catch(() => {});
|
|
353
|
-
return response;
|
|
338
|
+
});
|
|
354
339
|
}
|
|
355
340
|
|
|
356
341
|
const { decision } = await awaitPermissionDecision({
|
|
@@ -361,6 +346,51 @@ async function handlePermissionRequest(
|
|
|
361
346
|
return Response.json(handler.buildHookResponse(decision, body));
|
|
362
347
|
}
|
|
363
348
|
|
|
349
|
+
function permissionRequestQuestionAnswers(body: Record<string, unknown>): Record<string, string> | undefined {
|
|
350
|
+
if (body.hook_event_name !== "PermissionRequest") return undefined;
|
|
351
|
+
const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
|
|
352
|
+
if (!isRecord(toolInput.answers)) return undefined;
|
|
353
|
+
const answers = Object.fromEntries(
|
|
354
|
+
Object.entries(toolInput.answers)
|
|
355
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1].trim().length > 0),
|
|
356
|
+
);
|
|
357
|
+
return Object.keys(answers).length ? answers : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function holdQuestionWithImmediateDeny(params: {
|
|
361
|
+
options: ControlServerOptions;
|
|
362
|
+
pendingPermissionRequests: Map<string, PendingPermissionWaiter>;
|
|
363
|
+
handler: ProviderPermissionPromptHandler;
|
|
364
|
+
approvalId: string;
|
|
365
|
+
body: Record<string, unknown>;
|
|
366
|
+
occurredAt: number;
|
|
367
|
+
transcriptPath: string;
|
|
368
|
+
toolName?: string;
|
|
369
|
+
toolInput?: unknown;
|
|
370
|
+
}): Response {
|
|
371
|
+
const { options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput } = params;
|
|
372
|
+
// #1173/#1194: a fresh "questions" prompt (the provider's AskUserQuestion) must not block
|
|
373
|
+
// its own hook response while the human answers. Denying immediately makes the provider persist
|
|
374
|
+
// the buffered turn plus rejected tool_result, so the pre-flush harvest below can publish the
|
|
375
|
+
// reasoning before the dashboard form appears. The eventual answer is injected as a normal prompt.
|
|
376
|
+
const response = Response.json(handler.buildHookResponse(
|
|
377
|
+
{ approvalId, decision: "deny", reason: PENDING_QUESTION_DENY_REASON },
|
|
378
|
+
body,
|
|
379
|
+
));
|
|
380
|
+
void awaitPermissionDecision({
|
|
381
|
+
options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput,
|
|
382
|
+
blockedReason: "pendingQuestion",
|
|
383
|
+
recommendedAction: "Answer the question in Agent Relay.",
|
|
384
|
+
suppressResolvedStatus: true,
|
|
385
|
+
}).then(({ decision, view }) => {
|
|
386
|
+
const text = decision.decision === "answer" && decision.answers && Object.keys(decision.answers).length
|
|
387
|
+
? handler.buildAnswerInjectionPrompt!(view, decision, body)
|
|
388
|
+
: pendingQuestionDismissalPrompt(decision);
|
|
389
|
+
return options.onAnswerInject?.({ approvalId, text });
|
|
390
|
+
}).catch(() => {});
|
|
391
|
+
return response;
|
|
392
|
+
}
|
|
393
|
+
|
|
364
394
|
// Shared by both the traditional block-until-decided flow and the #1173 deny-then-hold flow:
|
|
365
395
|
// pre-flushes narrative (#435/#1116), builds+persists the interactive view, publishes the
|
|
366
396
|
// blocked providerState, and resolves once a decision arrives (or the wait times out). Callers
|