agent-relay-runner 0.127.8 → 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": {
|
|
@@ -56,8 +56,8 @@ var RUNNER_PROMPT_TEMPLATE_DEFAULTS = [
|
|
|
56
56
|
category: "runner",
|
|
57
57
|
title: "Runner Workspace Lifecycle Note",
|
|
58
58
|
description: "Lifecycle note injected by the runner for isolated workspaces.",
|
|
59
|
-
template: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates. Relay records the typed delivery on the Task, then rebases onto the latest {{baseRef}}, lands your work, and pushes for you.", "After `relay_task_deliver` accepts a complete delivery, the workspace status becomes `ready` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after delivery and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
|
|
60
|
-
defaultTemplate: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates. Relay records the typed delivery on the Task, then rebases onto the latest {{baseRef}}, lands your work, and pushes for you.", "After `relay_task_deliver` accepts a complete delivery, the workspace status becomes `ready` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after delivery and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
|
|
59
|
+
template: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates when the diff touches code/test/config paths. Relay records the typed delivery on the Task, skips code gates for empty/docs-only diffs, then rebases onto the latest {{baseRef}}, lands your work, and pushes for you.", "After `relay_task_deliver` accepts a complete delivery, the workspace status becomes `ready` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after delivery and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
|
|
60
|
+
defaultTemplate: lines("[agent-relay] Isolated workspace: you are in a git worktree on branch {{branch}}, based on {{baseRef}} \u2014 NOT the main checkout. Other agents may work in parallel and land to {{baseRef}}, so {{baseRef}} will move under you. That is expected; don't fight it.", "Do NOT push this branch yourself \u2014 not with `git push`, not with `tl push` or any other push wrapper, and do not manually rebase or merge. A steward may be auto-rebasing this branch in the background; pushing concurrently races it and can leave the worktree mid-rebase. Just commit your work here. When the task is done, call `relay_task_deliver` with disposition `complete`, a summary, commits, and passing `typecheck` + `test` gates when the diff touches code/test/config paths. Relay records the typed delivery on the Task, skips code gates for empty/docs-only diffs, then rebases onto the latest {{baseRef}}, lands your work, and pushes for you.", "After `relay_task_deliver` accepts a complete delivery, the workspace status becomes `ready` \u2014 this is the NORMAL, healthy hand-off state, NOT an escalation or a stall. Relay auto-merges clean rebases roughly every 2 minutes; a steward agent is spawned (after a short delay) ONLY if it can't land deterministically, so seeing no steward means it's working, not stuck. Stop your turn after delivery and go idle; do NOT wait or poll the steward queue. Relay wakes you with `landed-success` when your branch lands and refreshes, or `landed-failure` if landing fatally fails. On success you'll be on a fresh rebased branch whose name gains a `--N` suffix \u2014 expected, keep working there. Never `cd` into the main checkout, and never merge/push/resolve conflicts yourself \u2014 Relay and the steward own all of that. `agent-relay workspace status` anytime shows your current state and the exact next step."),
|
|
61
61
|
variables: ["branch", "baseRef"]
|
|
62
62
|
},
|
|
63
63
|
{
|
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
|