agent-relay-runner 0.95.1 → 0.96.0
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.
|
|
3
|
+
"version": "0.96.0",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.77"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/control-server.ts
CHANGED
|
@@ -50,6 +50,7 @@ interface ControlServerOptions {
|
|
|
50
50
|
// isPreFlush: true signals a mid-turn call from a PreToolUse hook — flush any
|
|
51
51
|
// un-mirrored narrative before the interactive form appears in chat (#435).
|
|
52
52
|
onSessionTurn?(input: { transcriptPath: string; lastAssistantMessage?: unknown; isPreFlush?: boolean }): Promise<void>;
|
|
53
|
+
onPermissionResolved?(input: ResolvedPermissionPrompt): Promise<void> | void;
|
|
53
54
|
// A provider UserPromptSubmit hook hands over the prompt the human typed
|
|
54
55
|
// directly into the session (web terminal / TUI) so the runner can mirror it
|
|
55
56
|
// into the dashboard chat and start tailing the turn transcript for reasoning.
|
|
@@ -69,6 +70,21 @@ interface ControlServerOptions {
|
|
|
69
70
|
permissionWaitMs?: number;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export interface ResolvedPermissionPrompt {
|
|
74
|
+
provider: "claude";
|
|
75
|
+
kind: "questions" | "plan" | "command" | "tool";
|
|
76
|
+
title: string;
|
|
77
|
+
body: string;
|
|
78
|
+
promptBody?: string;
|
|
79
|
+
toolName: string;
|
|
80
|
+
hookEventName: string;
|
|
81
|
+
approvalId: string;
|
|
82
|
+
decision: ProviderPermissionDecisionInput["decision"];
|
|
83
|
+
decisionLabel: string;
|
|
84
|
+
questions?: unknown[];
|
|
85
|
+
answers?: Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
export function startControlServer(options: ControlServerOptions): ControlServer {
|
|
73
89
|
const monitors = new Set<MonitorSocket>();
|
|
74
90
|
const pendingDeliveries = new Map<string, { resolve(ids: number[]): void; timer: Timer }>();
|
|
@@ -279,6 +295,10 @@ async function handlePermissionRequest(
|
|
|
279
295
|
pendingPermissionRequests.set(approvalId, { resolve, timer });
|
|
280
296
|
});
|
|
281
297
|
|
|
298
|
+
if (decision.reason !== PERMISSION_TIMEOUT_REASON && options.onPermissionResolved) {
|
|
299
|
+
await Promise.resolve(options.onPermissionResolved(claudeResolvedPermissionPrompt(view, decision, body))).catch(() => {});
|
|
300
|
+
}
|
|
301
|
+
|
|
282
302
|
return Response.json(claudePermissionHookResponse(decision, body));
|
|
283
303
|
}
|
|
284
304
|
|
|
@@ -409,6 +429,68 @@ function claudePermissionHookResponse(decision: ProviderPermissionDecisionInput,
|
|
|
409
429
|
};
|
|
410
430
|
}
|
|
411
431
|
|
|
432
|
+
function claudeResolvedPermissionPrompt(view: Record<string, unknown>, decision: ProviderPermissionDecisionInput, body: Record<string, unknown>): ResolvedPermissionPrompt {
|
|
433
|
+
const toolName = typeof body.tool_name === "string" ? body.tool_name : "Tool";
|
|
434
|
+
const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
|
|
435
|
+
const kind = view.kind === "questions" || view.kind === "plan" || view.kind === "command" || view.kind === "tool"
|
|
436
|
+
? view.kind
|
|
437
|
+
: toolName.toLowerCase() === "bash" ? "command" : "tool";
|
|
438
|
+
const questions = Array.isArray(view.questions)
|
|
439
|
+
? view.questions
|
|
440
|
+
: Array.isArray(toolInput.questions) ? toolInput.questions : undefined;
|
|
441
|
+
const promptBody = typeof view.body === "string" ? view.body : "";
|
|
442
|
+
const decisionLabel = claudePermissionDecisionLabel(kind, decision.decision);
|
|
443
|
+
return {
|
|
444
|
+
provider: "claude",
|
|
445
|
+
kind,
|
|
446
|
+
title: typeof view.title === "string" && view.title ? view.title : `Approve ${toolName}`,
|
|
447
|
+
body: claudeResolvedPermissionBody({ kind, title: typeof view.title === "string" && view.title ? view.title : `Approve ${toolName}`, toolName, promptBody, decisionLabel, questions, answers: decision.answers }),
|
|
448
|
+
...(promptBody ? { promptBody } : {}),
|
|
449
|
+
toolName,
|
|
450
|
+
hookEventName: typeof body.hook_event_name === "string" ? body.hook_event_name : "PermissionRequest",
|
|
451
|
+
approvalId: decision.approvalId,
|
|
452
|
+
decision: decision.decision,
|
|
453
|
+
decisionLabel,
|
|
454
|
+
...(questions ? { questions } : {}),
|
|
455
|
+
...(decision.answers ? { answers: decision.answers } : {}),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function claudeResolvedPermissionBody(input: { kind: ResolvedPermissionPrompt["kind"]; title: string; toolName: string; promptBody: string; decisionLabel: string; questions?: unknown[]; answers?: Record<string, string> }): string {
|
|
460
|
+
if (input.kind === "questions") {
|
|
461
|
+
return [
|
|
462
|
+
input.title,
|
|
463
|
+
input.decisionLabel,
|
|
464
|
+
...resolvedQuestionRows(input.questions, input.answers, input.decisionLabel).map((row) => `Q: ${row.question}\nA: ${row.answer}`),
|
|
465
|
+
].filter(Boolean).join("\n\n");
|
|
466
|
+
}
|
|
467
|
+
const subject = input.kind === "plan" ? "Plan prompt resolved" : `${input.toolName} permission resolved`;
|
|
468
|
+
return [subject, `Decision: ${input.decisionLabel}`, input.promptBody.trim()].filter(Boolean).join("\n\n");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function resolvedQuestionRows(questions: unknown[] | undefined, answers: Record<string, string> | undefined, fallback: string): Array<{ question: string; answer: string }> {
|
|
472
|
+
const answerMap = answers ?? {};
|
|
473
|
+
const rows = (questions ?? []).map((question, index) => {
|
|
474
|
+
const record = question && typeof question === "object" && !Array.isArray(question) ? question as Record<string, unknown> : {};
|
|
475
|
+
const label = typeof record.question === "string" && record.question.trim()
|
|
476
|
+
? record.question.trim()
|
|
477
|
+
: typeof record.header === "string" && record.header.trim() ? record.header.trim() : `Question ${index + 1}`;
|
|
478
|
+
const rawAnswer = answerMap[label] ?? answerMap[String(record.question ?? "")] ?? answerMap[String(record.header ?? "")];
|
|
479
|
+
return { question: label, answer: typeof rawAnswer === "string" && rawAnswer.trim() ? rawAnswer.trim() : fallback };
|
|
480
|
+
});
|
|
481
|
+
return rows.length ? rows : Object.entries(answerMap).map(([question, answer]) => ({ question, answer }));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function claudePermissionDecisionLabel(kind: ResolvedPermissionPrompt["kind"], decision: ProviderPermissionDecisionInput["decision"]): string {
|
|
485
|
+
if (kind === "questions") return decision === "answer" ? "Answered" : "Dismissed";
|
|
486
|
+
if (kind === "plan") return decision === "approve" || decision === "approve-session" ? "Approved plan" : "Kept planning";
|
|
487
|
+
if (decision === "approve-session") return "Approved for session";
|
|
488
|
+
if (decision === "approve") return "Approved";
|
|
489
|
+
if (decision === "abort") return "Aborted";
|
|
490
|
+
if (decision === "answer") return "Answered";
|
|
491
|
+
return "Denied";
|
|
492
|
+
}
|
|
493
|
+
|
|
412
494
|
async function handleSessionTurn(req: Request, options: ControlServerOptions): Promise<Response> {
|
|
413
495
|
if (!options.onSessionTurn) return Response.json({ ok: false, reason: "session capture unavailable" });
|
|
414
496
|
const body = await req.json().catch(() => null);
|
package/src/runner-core.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { messagesWithCachedAttachments } from "./attachment-cache";
|
|
|
12
12
|
import { ClaimTracker } from "./claim-tracker";
|
|
13
13
|
import { ClaudeQuotaHarvest } from "./claude-quota-harvest";
|
|
14
14
|
import { readRunnerContextProbeState } from "./context-probe-state";
|
|
15
|
-
import { startControlServer, type ControlServer } from "./control-server";
|
|
15
|
+
import { startControlServer, type ControlServer, type ResolvedPermissionPrompt } from "./control-server";
|
|
16
16
|
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
17
17
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
18
18
|
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
@@ -409,7 +409,7 @@ export class AgentRunner {
|
|
|
409
409
|
// Hot-path-safe: answered instantly from the local snapshot, never a server
|
|
410
410
|
// round-trip. The snapshot is kept warm by the background refresh below (#196).
|
|
411
411
|
onReplyObligations: () => Promise.resolve(this.obligationCache.get()),
|
|
412
|
-
onSessionTurn: (input) => this.publishSessionTurn(input),
|
|
412
|
+
onSessionTurn: (input) => this.publishSessionTurn(input), onPermissionResolved: (input) => this.publishSessionEvent({ from: this.agentId, to: "user", body: input.body, session: { type: "prompt-resolution", origin: "provider", label: input.decisionLabel, ...(this.currentTurnId ? { turnId: this.currentTurnId } : {}), stepId: `prompt-resolution:${input.approvalId}`, promptResolution: input } }),
|
|
413
413
|
onUserPrompt: (input) => this.handleUserPrompt(input),
|
|
414
414
|
onSessionBoundary: (input) => this.handleSessionBoundary(input),
|
|
415
415
|
onHookFatal: (report) => this.reportHookFatal(report),
|