agent-relay-runner 0.68.1 → 0.69.1
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
|
@@ -27,6 +27,20 @@ export function obligationRequiresExplicitReply(obligation: ReplyObligation): bo
|
|
|
27
27
|
return obligation.from !== "user";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function responseCaptureReplyRoute(
|
|
31
|
+
obligations: ReplyObligation[],
|
|
32
|
+
currentTurnStartedAt?: number,
|
|
33
|
+
opts: { suppressUser?: boolean } = {},
|
|
34
|
+
): { replyToMessageId?: number; to: string } | null {
|
|
35
|
+
const eligible = [...obligations].reverse()
|
|
36
|
+
.filter((o) => currentTurnStartedAt === undefined || o.createdAt <= currentTurnStartedAt);
|
|
37
|
+
const explicit = eligible.find(obligationRequiresExplicitReply);
|
|
38
|
+
if (explicit) return { replyToMessageId: explicit.messageId, to: explicit.from };
|
|
39
|
+
const user = eligible.find((o) => o.from === "user");
|
|
40
|
+
if (!user) return { to: "user" };
|
|
41
|
+
return opts.suppressUser ? null : { replyToMessageId: user.messageId, to: "user" };
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
type ReplyObligationFetch = () => Promise<ReplyObligation[]>;
|
|
31
45
|
|
|
32
46
|
interface ReplyObligationCacheOptions {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MessageSessionMeta, SendMessageInput } from "agent-relay-sdk";
|
|
2
|
+
|
|
3
|
+
interface ReportOutbox {
|
|
4
|
+
enqueue(record: { kind: string; idempotencyKey?: string; payload: SendMessageInput }): void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type SessionPublisher = (input: {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
body: string;
|
|
11
|
+
session: MessageSessionMeta;
|
|
12
|
+
replyTo?: number;
|
|
13
|
+
}) => void | Promise<void>;
|
|
14
|
+
|
|
15
|
+
export async function publishCapturedResponse(input: {
|
|
16
|
+
publishSessionEvent: SessionPublisher;
|
|
17
|
+
outbox: ReportOutbox;
|
|
18
|
+
provider: string;
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
body: string;
|
|
22
|
+
session: MessageSessionMeta;
|
|
23
|
+
replyTo?: number;
|
|
24
|
+
}): Promise<void> {
|
|
25
|
+
await input.publishSessionEvent({
|
|
26
|
+
from: input.from,
|
|
27
|
+
to: input.to,
|
|
28
|
+
body: input.body,
|
|
29
|
+
...(input.replyTo ? { replyTo: input.replyTo } : {}),
|
|
30
|
+
session: input.session,
|
|
31
|
+
});
|
|
32
|
+
enqueueCapturedResponseReport(input);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function enqueueCapturedResponseReport(input: {
|
|
36
|
+
outbox: ReportOutbox;
|
|
37
|
+
provider: string;
|
|
38
|
+
from: string;
|
|
39
|
+
to: string;
|
|
40
|
+
body: string;
|
|
41
|
+
session: MessageSessionMeta;
|
|
42
|
+
replyTo?: number;
|
|
43
|
+
}): void {
|
|
44
|
+
if (!input.replyTo || input.to === "user") return;
|
|
45
|
+
const turnId = input.session.turnId ?? "";
|
|
46
|
+
input.outbox.enqueue({
|
|
47
|
+
kind: "session-message",
|
|
48
|
+
idempotencyKey: `response-report:${input.from}:${input.replyTo}:${turnId}`,
|
|
49
|
+
payload: {
|
|
50
|
+
from: input.from,
|
|
51
|
+
to: input.to,
|
|
52
|
+
replyTo: input.replyTo,
|
|
53
|
+
kind: "chat",
|
|
54
|
+
body: input.body,
|
|
55
|
+
replyExpected: false,
|
|
56
|
+
payload: { responseCapture: { provider: input.provider, ...input.session } },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
package/src/runner-core.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissio
|
|
|
10
10
|
import { messagesWithCachedAttachments } from "./attachment-cache";
|
|
11
11
|
import { ClaimTracker } from "./claim-tracker";
|
|
12
12
|
import { startControlServer, type ControlServer } from "./control-server";
|
|
13
|
-
import { ReplyObligationCache, obligationRequiresExplicitReply } from "./reply-obligation-cache";
|
|
13
|
+
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
14
14
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
15
15
|
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
16
16
|
import { profileUsesHostProviderGlobals } from "./profile-home";
|
|
@@ -22,6 +22,7 @@ import { ensureSessionScratch, reapSessionScratch, sweepStaleSessions, type Sess
|
|
|
22
22
|
import { deliverBufferedMcpCall as deliverBufferedMcpOutboxCall, deliverRunnerOutboxEvent } from "./mcp-outbox";
|
|
23
23
|
import { RunnerInsights } from "./runner-insights";
|
|
24
24
|
import { BusyReconciler } from "./busy-reconciler";
|
|
25
|
+
import { publishCapturedResponse } from "./response-capture-report";
|
|
25
26
|
import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
26
27
|
import {
|
|
27
28
|
appliedAgentProfileMetadata,
|
|
@@ -725,6 +726,7 @@ export class AgentRunner {
|
|
|
725
726
|
});
|
|
726
727
|
await this.options.adapter.deliver(this.process, prepared);
|
|
727
728
|
for (const message of deliverable) {
|
|
729
|
+
await this.http.recordMessageDeliveryAttempt(message.id, { agentId: this.agentId, status: "delivered" }).catch(() => {});
|
|
728
730
|
await this.http.markRead(message.id, this.agentId).catch(() => {});
|
|
729
731
|
if (!taskIdFromMessage(message)) this.claims.finishClaim("message", String(message.id));
|
|
730
732
|
}
|
|
@@ -1336,7 +1338,7 @@ export class AgentRunner {
|
|
|
1336
1338
|
const turnId = this.currentTurnId;
|
|
1337
1339
|
this.stopReasoningTail();
|
|
1338
1340
|
// Optional correlation for threading + obligation clearing — never a capture gate.
|
|
1339
|
-
let replyToMessageId: number | undefined;
|
|
1341
|
+
let replyToMessageId: number | undefined, replyTarget = "user";
|
|
1340
1342
|
const pendingPrompt = this.pendingPromptMessageId;
|
|
1341
1343
|
if (pendingPrompt) {
|
|
1342
1344
|
replyToMessageId = pendingPrompt;
|
|
@@ -1344,8 +1346,8 @@ export class AgentRunner {
|
|
|
1344
1346
|
} else {
|
|
1345
1347
|
// Correlation-only (threading + obligation clearing) — the local snapshot is fresh
|
|
1346
1348
|
// enough and never blocks the response-capture path (#196).
|
|
1347
|
-
const
|
|
1348
|
-
replyToMessageId =
|
|
1349
|
+
const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt);
|
|
1350
|
+
replyToMessageId = route?.replyToMessageId; replyTarget = route?.to ?? "user";
|
|
1349
1351
|
}
|
|
1350
1352
|
|
|
1351
1353
|
// Grab and reset the pre-flush cursor before extraction: the cursor may be
|
|
@@ -1389,13 +1391,7 @@ export class AgentRunner {
|
|
|
1389
1391
|
}
|
|
1390
1392
|
|
|
1391
1393
|
this.sessionLog(`response captured for turn ${turnId ?? "?"} (${body.length} chars${replyToMessageId ? `, replyTo #${replyToMessageId}` : ", no replyTo"})`);
|
|
1392
|
-
await this.publishSessionEvent({
|
|
1393
|
-
from: this.agentId,
|
|
1394
|
-
to: "user",
|
|
1395
|
-
body,
|
|
1396
|
-
...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
|
|
1397
|
-
session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) },
|
|
1398
|
-
});
|
|
1394
|
+
await publishCapturedResponse({ publishSessionEvent: (event) => this.publishSessionEvent(event), outbox: this.sessionOutbox, provider: this.options.provider, from: this.agentId, to: replyTarget, body, replyTo: replyToMessageId, session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) } });
|
|
1399
1395
|
// The agent's reply may have cleared an obligation — refresh the snapshot so the next
|
|
1400
1396
|
// turn-end doesn't re-prompt for a message already answered (#196).
|
|
1401
1397
|
if (replyToMessageId) this.obligationCache.markDirty();
|
|
@@ -1600,24 +1596,19 @@ export class AgentRunner {
|
|
|
1600
1596
|
}
|
|
1601
1597
|
if (event.type === "response") {
|
|
1602
1598
|
// Dashboard prompt injection is already answered by this captured App Server
|
|
1603
|
-
// response.
|
|
1604
|
-
//
|
|
1605
|
-
let replyToMessageId: number | undefined;
|
|
1599
|
+
// response. User obligations still belong to the session mirror; spawner
|
|
1600
|
+
// obligations (#413) route to the spawn-parent with replyTo to clear the debt.
|
|
1601
|
+
let replyToMessageId: number | undefined, replyTarget = "user";
|
|
1606
1602
|
const pendingPrompt = this.pendingPromptMessageId;
|
|
1607
1603
|
if (pendingPrompt) {
|
|
1608
1604
|
replyToMessageId = pendingPrompt;
|
|
1609
1605
|
this.pendingPromptMessageId = undefined;
|
|
1610
|
-
} else
|
|
1611
|
-
|
|
1612
|
-
return;
|
|
1606
|
+
} else {
|
|
1607
|
+
const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt, { suppressUser: true });
|
|
1608
|
+
if (!route) return; // The session mirror will satisfy the user obligation; don't double-post (#196).
|
|
1609
|
+
replyToMessageId = route.replyToMessageId; replyTarget = route.to;
|
|
1613
1610
|
}
|
|
1614
|
-
await this.publishSessionEvent({
|
|
1615
|
-
from: this.agentId,
|
|
1616
|
-
to: "user",
|
|
1617
|
-
body,
|
|
1618
|
-
...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
|
|
1619
|
-
session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) },
|
|
1620
|
-
});
|
|
1611
|
+
await publishCapturedResponse({ publishSessionEvent: (sessionEvent) => this.publishSessionEvent(sessionEvent), outbox: this.sessionOutbox, provider: this.options.provider, from: this.agentId, to: replyTarget, body, replyTo: replyToMessageId, session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) } });
|
|
1621
1612
|
if (replyToMessageId) this.obligationCache.markDirty();
|
|
1622
1613
|
return;
|
|
1623
1614
|
}
|
|
@@ -1660,10 +1651,6 @@ export class AgentRunner {
|
|
|
1660
1651
|
return false;
|
|
1661
1652
|
}
|
|
1662
1653
|
|
|
1663
|
-
private obligationPredatesCurrentTurn(obligation: { createdAt: number }): boolean {
|
|
1664
|
-
return this.currentTurnStartedAt === undefined || obligation.createdAt <= this.currentTurnStartedAt;
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1654
|
// Force-clear a stuck provider-turn claim directly. Unlike the idle status path
|
|
1668
1655
|
// it does NOT depend on a matching claim id (the Stop hook keys busy as
|
|
1669
1656
|
// provider-turn:provider-turn, but reconciliation has no specific id), and it
|