agent-relay-runner 0.120.5 → 0.121.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.121.0",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"agent-relay-providers": "0.104.3",
|
|
24
|
-
"agent-relay-sdk": "0.2.
|
|
24
|
+
"agent-relay-sdk": "0.2.112",
|
|
25
25
|
"callmux": "0.23.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { open, stat } from "node:fs/promises";
|
|
2
|
+
import { LatestTurnStepAccumulator, type TurnStep } from "./claude-transcript";
|
|
3
|
+
|
|
4
|
+
const FINGERPRINT_BYTES = 4096;
|
|
5
|
+
|
|
6
|
+
export interface ClaudeTranscriptFileIdentity {
|
|
7
|
+
dev: number;
|
|
8
|
+
ino: number;
|
|
9
|
+
birthtimeMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TailFingerprint {
|
|
13
|
+
offset: number;
|
|
14
|
+
bytes: Buffer;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IncrementalTranscriptPollResult {
|
|
18
|
+
available: boolean;
|
|
19
|
+
changed: boolean;
|
|
20
|
+
reset: boolean;
|
|
21
|
+
steps: TurnStep[];
|
|
22
|
+
transcriptLooksComplete: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class IncrementalClaudeTranscriptTail {
|
|
26
|
+
private offset = 0;
|
|
27
|
+
private pending = Buffer.alloc(0);
|
|
28
|
+
private identity: ClaudeTranscriptFileIdentity | undefined;
|
|
29
|
+
private fingerprint: TailFingerprint | undefined;
|
|
30
|
+
private readonly accumulator = new LatestTurnStepAccumulator();
|
|
31
|
+
|
|
32
|
+
async poll(path: string): Promise<IncrementalTranscriptPollResult> {
|
|
33
|
+
let fileStat: Awaited<ReturnType<typeof stat>>;
|
|
34
|
+
try {
|
|
35
|
+
fileStat = await stat(path);
|
|
36
|
+
} catch {
|
|
37
|
+
return this.result({ available: false, changed: false, reset: false });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nextIdentity = { dev: fileStat.dev, ino: fileStat.ino, birthtimeMs: fileStat.birthtimeMs };
|
|
41
|
+
const replaced = this.identity !== undefined && !sameIdentity(this.identity, nextIdentity);
|
|
42
|
+
const truncated = this.offset > fileStat.size;
|
|
43
|
+
let reset = replaced || truncated;
|
|
44
|
+
if (!reset && this.identity && this.fingerprint && fileStat.size >= this.offset) {
|
|
45
|
+
reset = !(await this.fingerprintMatches(path));
|
|
46
|
+
}
|
|
47
|
+
if (reset) this.reset(nextIdentity);
|
|
48
|
+
else if (!this.identity) this.identity = nextIdentity;
|
|
49
|
+
|
|
50
|
+
if (this.offset === fileStat.size) {
|
|
51
|
+
return this.result({ available: true, changed: false, reset });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const length = fileStat.size - this.offset;
|
|
55
|
+
const buffer = Buffer.alloc(length);
|
|
56
|
+
let bytesRead = 0;
|
|
57
|
+
const handle = await open(path, "r");
|
|
58
|
+
try {
|
|
59
|
+
const read = await handle.read(buffer, 0, length, this.offset);
|
|
60
|
+
bytesRead = read.bytesRead;
|
|
61
|
+
} finally {
|
|
62
|
+
await handle.close();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.offset += bytesRead;
|
|
66
|
+
const delta = buffer.subarray(0, bytesRead);
|
|
67
|
+
this.updateFingerprint(delta);
|
|
68
|
+
const changed = bytesRead > 0 && this.consume(delta);
|
|
69
|
+
return this.result({ available: true, changed, reset });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private reset(identity: ClaudeTranscriptFileIdentity): void {
|
|
73
|
+
this.offset = 0;
|
|
74
|
+
this.pending = Buffer.alloc(0);
|
|
75
|
+
this.fingerprint = undefined;
|
|
76
|
+
this.identity = identity;
|
|
77
|
+
this.accumulator.reset();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private async fingerprintMatches(path: string): Promise<boolean> {
|
|
81
|
+
if (!this.fingerprint) return true;
|
|
82
|
+
const buffer = Buffer.alloc(this.fingerprint.bytes.length);
|
|
83
|
+
const handle = await open(path, "r");
|
|
84
|
+
try {
|
|
85
|
+
const read = await handle.read(buffer, 0, buffer.length, this.fingerprint.offset);
|
|
86
|
+
return read.bytesRead === buffer.length && buffer.equals(this.fingerprint.bytes);
|
|
87
|
+
} finally {
|
|
88
|
+
await handle.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private updateFingerprint(delta: Buffer): void {
|
|
93
|
+
if (!delta.length) return;
|
|
94
|
+
const previous = this.fingerprint?.bytes ?? Buffer.alloc(0);
|
|
95
|
+
const combined = previous.length ? Buffer.concat([previous, delta]) : delta;
|
|
96
|
+
const bytes = Buffer.from(combined.subarray(Math.max(0, combined.length - FINGERPRINT_BYTES)));
|
|
97
|
+
this.fingerprint = { offset: this.offset - bytes.length, bytes };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private consume(delta: Buffer): boolean {
|
|
101
|
+
if (!delta.length) return false;
|
|
102
|
+
const combined = this.pending.length ? Buffer.concat([this.pending, delta]) : delta;
|
|
103
|
+
const lastNewline = combined.lastIndexOf(0x0a);
|
|
104
|
+
if (lastNewline === -1) {
|
|
105
|
+
this.pending = Buffer.from(combined);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const complete = combined.subarray(0, lastNewline + 1);
|
|
110
|
+
this.pending = Buffer.from(combined.subarray(lastNewline + 1));
|
|
111
|
+
this.accumulator.appendJsonl(complete.toString("utf8"));
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private result(input: { available: boolean; changed: boolean; reset: boolean }): IncrementalTranscriptPollResult {
|
|
116
|
+
return {
|
|
117
|
+
...input,
|
|
118
|
+
steps: this.accumulator.steps,
|
|
119
|
+
transcriptLooksComplete: this.accumulator.transcriptLooksComplete,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sameIdentity(a: ClaudeTranscriptFileIdentity, b: ClaudeTranscriptFileIdentity): boolean {
|
|
125
|
+
return a.dev === b.dev && a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const sameClaudeTranscriptFileIdentityForTests = sameIdentity;
|
|
@@ -21,7 +21,7 @@ interface TranscriptBlock {
|
|
|
21
21
|
is_error?: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
interface TurnStep {
|
|
24
|
+
export interface TurnStep {
|
|
25
25
|
type: "narration" | "reasoning" | "tool";
|
|
26
26
|
text: string;
|
|
27
27
|
label?: string;
|
|
@@ -83,20 +83,9 @@ function assistantText(entry: TranscriptEntry): string {
|
|
|
83
83
|
* assistant block is written to disk.
|
|
84
84
|
*/
|
|
85
85
|
export function transcriptLooksComplete(jsonl: string): boolean {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const trimmed = line.trim();
|
|
90
|
-
if (!trimmed) continue;
|
|
91
|
-
try {
|
|
92
|
-
const entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
93
|
-
if (isSidechainEntry(entry)) continue;
|
|
94
|
-
if (entry.type === "assistant") lastAssistantStopReason = entry.message?.stop_reason;
|
|
95
|
-
} catch {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return lastAssistantStopReason === "end_turn";
|
|
86
|
+
const state = new LatestTurnStepAccumulator();
|
|
87
|
+
state.appendJsonl(jsonl);
|
|
88
|
+
return state.transcriptLooksComplete;
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
/**
|
|
@@ -251,34 +240,65 @@ export function extractFinalAssistantMessageAfterEntry(jsonl: string, afterEntry
|
|
|
251
240
|
* tailer can emit only the ones it hasn't seen yet.
|
|
252
241
|
*/
|
|
253
242
|
export function extractLatestTurnSteps(jsonl: string): TurnStep[] {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
243
|
+
const state = new LatestTurnStepAccumulator();
|
|
244
|
+
state.appendJsonl(jsonl);
|
|
245
|
+
return state.steps;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export class LatestTurnStepAccumulator {
|
|
249
|
+
private latestSteps: TurnStep[] = [];
|
|
250
|
+
private lastAssistantStopReason: string | undefined;
|
|
251
|
+
private parsedEntries = 0;
|
|
252
|
+
|
|
253
|
+
get steps(): TurnStep[] {
|
|
254
|
+
return [...this.latestSteps];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
get transcriptLooksComplete(): boolean {
|
|
258
|
+
return this.lastAssistantStopReason === "end_turn";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
get entryCount(): number {
|
|
262
|
+
return this.parsedEntries;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
reset(): void {
|
|
266
|
+
this.latestSteps = [];
|
|
267
|
+
this.lastAssistantStopReason = undefined;
|
|
268
|
+
this.parsedEntries = 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
appendJsonl(jsonl: string): void {
|
|
272
|
+
for (const line of jsonl.split("\n")) this.appendLine(line);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
appendLine(line: string): void {
|
|
257
276
|
const trimmed = line.trim();
|
|
258
|
-
if (!trimmed)
|
|
277
|
+
if (!trimmed) return;
|
|
259
278
|
let entry: TranscriptEntry;
|
|
260
279
|
try {
|
|
261
280
|
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
262
281
|
} catch {
|
|
263
|
-
|
|
282
|
+
return;
|
|
264
283
|
}
|
|
265
|
-
|
|
284
|
+
this.parsedEntries += 1;
|
|
285
|
+
if (isSidechainEntry(entry)) return;
|
|
286
|
+
if (entry.type === "assistant") this.lastAssistantStopReason = entry.message?.stop_reason;
|
|
266
287
|
if (isRealUserPrompt(entry)) {
|
|
267
|
-
|
|
268
|
-
|
|
288
|
+
this.latestSteps = [];
|
|
289
|
+
return;
|
|
269
290
|
}
|
|
270
|
-
if (entry.type !== "assistant")
|
|
291
|
+
if (entry.type !== "assistant") return;
|
|
271
292
|
for (const b of blocks(entry.message)) {
|
|
272
293
|
if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
273
|
-
|
|
294
|
+
this.latestSteps.push({ type: "narration", text: b.text.trim() });
|
|
274
295
|
} else if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim()) {
|
|
275
|
-
|
|
296
|
+
this.latestSteps.push({ type: "reasoning", text: b.thinking.trim() });
|
|
276
297
|
} else if (b.type === "tool_use" && typeof b.name === "string" && b.name) {
|
|
277
|
-
|
|
298
|
+
this.latestSteps.push({ type: "tool", label: b.name, text: summarizeToolUse(b.name, b.input) });
|
|
278
299
|
}
|
|
279
300
|
}
|
|
280
301
|
}
|
|
281
|
-
return steps;
|
|
282
302
|
}
|
|
283
303
|
|
|
284
304
|
/**
|
package/src/mcp-outbox.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SendMessageInput } from "agent-relay-sdk";
|
|
2
2
|
import type { RelayHttpClient } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage } from "agent-relay-sdk";
|
|
3
4
|
import { relayMcpEndpoint } from "./relay-mcp";
|
|
4
5
|
import type { OutboxRecord } from "./outbox";
|
|
5
6
|
import { deliverContinuationArchiveRecord } from "./continuation-archive";
|
|
@@ -27,6 +28,23 @@ export async function deliverRunnerOutboxEvent(input: RunnerOutboxDelivery): Pro
|
|
|
27
28
|
});
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
31
|
+
if (record.kind === "session-message-batch") {
|
|
32
|
+
const messages = Array.isArray((record.payload as { messages?: unknown }).messages)
|
|
33
|
+
? (record.payload as { messages: SendMessageInput[] }).messages
|
|
34
|
+
: [];
|
|
35
|
+
const batch = messages.map((message, index) => ({
|
|
36
|
+
...message,
|
|
37
|
+
occurredAt: typeof message.occurredAt === "number" ? message.occurredAt : record.occurredAt,
|
|
38
|
+
idempotencyKey: message.idempotencyKey ?? `${record.idempotencyKey}:${index}`,
|
|
39
|
+
}));
|
|
40
|
+
try {
|
|
41
|
+
await http.sendMessages(batch);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isDeterministicValidationError(error)) throw error;
|
|
44
|
+
await deliverSessionBatchItems(input, batch);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
30
48
|
if (record.kind === "insight") {
|
|
31
49
|
await http.recordInsightObservation({
|
|
32
50
|
...(record.payload as Parameters<RelayHttpClient["recordInsightObservation"]>[0]),
|
|
@@ -55,6 +73,22 @@ export async function deliverRunnerOutboxEvent(input: RunnerOutboxDelivery): Pro
|
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
|
|
76
|
+
async function deliverSessionBatchItems(input: RunnerOutboxDelivery, messages: SendMessageInput[]): Promise<void> {
|
|
77
|
+
for (const message of messages) {
|
|
78
|
+
try {
|
|
79
|
+
await input.http.sendMessage(message);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (!isDeterministicValidationError(error)) throw error;
|
|
82
|
+
logger.warn("outbox", `dropping invalid session batch item: ${errMessage(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isDeterministicValidationError(error: unknown): boolean {
|
|
88
|
+
const status = typeof error === "object" && error !== null ? (error as { status?: unknown }).status : undefined;
|
|
89
|
+
return status === 400 || status === 413 || status === 422;
|
|
90
|
+
}
|
|
91
|
+
|
|
58
92
|
export async function deliverBufferedMcpCall(input: {
|
|
59
93
|
record: OutboxRecord;
|
|
60
94
|
relayUrl: string;
|
package/src/runner-core.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { startControlServer, type ControlServer, type ResolvedPermissionPrompt }
|
|
|
17
17
|
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
18
18
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
19
19
|
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
20
|
+
import { IncrementalClaudeTranscriptTail } from "./adapters/claude-transcript-tail";
|
|
20
21
|
import { getManifest } from "agent-relay-providers";
|
|
21
22
|
import { profileUsesProviderHostGlobals } from "./profile-home";
|
|
22
23
|
import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint, resolveSharedMcpUrl } from "./relay-mcp";
|
|
@@ -147,8 +148,10 @@ const INITIAL_PROMPT_FIRST_READY_TIMEOUT_MS = 30_000;
|
|
|
147
148
|
const INITIAL_PROMPT_RETRY_READY_TIMEOUT_MS = 10_000;
|
|
148
149
|
const MAX_INITIAL_PROMPT_ATTEMPTS = 6;
|
|
149
150
|
// Reasoning tailer poll cadence (item 5). Coarse on purpose — reasoning is a
|
|
150
|
-
// discreet progress signal, not a token stream
|
|
151
|
+
// discreet progress signal, not a token stream; each tick reads only appended
|
|
152
|
+
// transcript bytes.
|
|
151
153
|
const REASONING_POLL_MS = 1_200;
|
|
154
|
+
const SESSION_MIRROR_BATCH_MAX = 100;
|
|
152
155
|
interface RunnerTimelineEvent {
|
|
153
156
|
status: string;
|
|
154
157
|
id?: string;
|
|
@@ -160,9 +163,7 @@ interface RunnerTimelineEvent {
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
interface ReasoningTailState {
|
|
163
|
-
timer: ReturnType<typeof setInterval>;
|
|
164
|
-
seen: Set<string>;
|
|
165
|
-
emittedNarrationKeys: Set<string>;
|
|
166
|
+
timer: ReturnType<typeof setInterval>; emittedNarrationKeys: Set<string>;
|
|
166
167
|
poll(): Promise<void>;
|
|
167
168
|
}
|
|
168
169
|
|
|
@@ -182,6 +183,7 @@ export class AgentRunner {
|
|
|
182
183
|
// their own FIFO queue stops one transient trace failure from head-of-line blocking real-message
|
|
183
184
|
// delivery (and vice versa); its backoff is capped low since a stale live-mirror frame is cheap.
|
|
184
185
|
private readonly sessionOutbox: Outbox;
|
|
186
|
+
private sessionBatchSeq = 0;
|
|
185
187
|
private readonly insights: RunnerInsights;
|
|
186
188
|
private readonly busyReconciler: BusyReconciler;
|
|
187
189
|
private readonly claudeQuotaHarvest: ClaudeQuotaHarvest;
|
|
@@ -1535,6 +1537,39 @@ export class AgentRunner {
|
|
|
1535
1537
|
});
|
|
1536
1538
|
}
|
|
1537
1539
|
|
|
1540
|
+
private publishSessionEventsBatch(inputs: Array<{
|
|
1541
|
+
from: string;
|
|
1542
|
+
to: string;
|
|
1543
|
+
body: string;
|
|
1544
|
+
session: MessageSessionMeta;
|
|
1545
|
+
replyTo?: number;
|
|
1546
|
+
}>): void {
|
|
1547
|
+
for (let i = 0; i < inputs.length; i += SESSION_MIRROR_BATCH_MAX) {
|
|
1548
|
+
const chunk = inputs.slice(i, i + SESSION_MIRROR_BATCH_MAX);
|
|
1549
|
+
if (chunk.length === 1) {
|
|
1550
|
+
this.publishSessionEvent(chunk[0]!);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
const messages = chunk.map((input) => {
|
|
1554
|
+
const stepId = input.session.stepId;
|
|
1555
|
+
return {
|
|
1556
|
+
from: input.from,
|
|
1557
|
+
to: input.to,
|
|
1558
|
+
...(input.replyTo ? { replyTo: input.replyTo } : {}),
|
|
1559
|
+
kind: "session",
|
|
1560
|
+
body: input.body,
|
|
1561
|
+
...(stepId ? { idempotencyKey: `session-step:${input.from}:${input.session.turnId ?? ""}:${stepId}` } : {}),
|
|
1562
|
+
payload: { session: { provider: this.options.provider, ...input.session } },
|
|
1563
|
+
} satisfies SendMessageInput;
|
|
1564
|
+
});
|
|
1565
|
+
this.sessionOutbox.enqueue({
|
|
1566
|
+
kind: "session-message-batch",
|
|
1567
|
+
idempotencyKey: `session-batch:${this.agentId}:${++this.sessionBatchSeq}:${messages.length}`,
|
|
1568
|
+
payload: { messages },
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1538
1573
|
// Map queued records to HTTP calls. Throw to retry, return to ack/delete.
|
|
1539
1574
|
private async deliverOutboxEvent(record: OutboxRecord): Promise<void> {
|
|
1540
1575
|
await deliverRunnerOutboxEvent({
|
|
@@ -1827,6 +1862,7 @@ export class AgentRunner {
|
|
|
1827
1862
|
// so two identical steps in one turn — same tool, same input — both surface (#265).
|
|
1828
1863
|
const seen = new Set<string>();
|
|
1829
1864
|
const emittedNarrationKeys = new Set<string>();
|
|
1865
|
+
const transcriptTail = new IncrementalClaudeTranscriptTail();
|
|
1830
1866
|
const turnIdAtStart = this.currentTurnId;
|
|
1831
1867
|
// On the first poll the new prompt usually hasn't landed in the transcript yet,
|
|
1832
1868
|
// so extractLatestTurnSteps still returns the PRIOR (completed) turn. Seed those
|
|
@@ -1838,24 +1874,25 @@ export class AgentRunner {
|
|
|
1838
1874
|
let seeded = false;
|
|
1839
1875
|
let pendingPoll = Promise.resolve();
|
|
1840
1876
|
const pollBody = async (): Promise<void> => {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
const keyed = stepDedupKeys(steps).map((sig, i) => ({ sig, step: steps[i]! }));
|
|
1877
|
+
const result = await transcriptTail.poll(transcriptPath);
|
|
1878
|
+
if (!result.available) return;
|
|
1879
|
+
if (result.reset) { seen.clear(); emittedNarrationKeys.clear(); seeded = false; }
|
|
1880
|
+
if (!result.changed && (seeded || (!result.steps.length && !result.transcriptLooksComplete))) return;
|
|
1881
|
+
const keyed = stepDedupKeys(result.steps).map((sig, i) => ({ sig, step: result.steps[i]! }));
|
|
1846
1882
|
if (!seeded) {
|
|
1847
1883
|
seeded = true;
|
|
1848
|
-
if (transcriptLooksComplete
|
|
1884
|
+
if (result.transcriptLooksComplete) {
|
|
1849
1885
|
for (const { sig } of keyed) seen.add(sig);
|
|
1850
1886
|
}
|
|
1851
1887
|
}
|
|
1852
1888
|
const turnId = this.currentTurnId ?? turnIdAtStart;
|
|
1853
1889
|
let emitted = 0;
|
|
1890
|
+
const batch: Array<{ from: string; to: string; body: string; session: MessageSessionMeta }> = [];
|
|
1854
1891
|
for (const { sig, step } of keyed) {
|
|
1855
1892
|
if (seen.has(sig)) continue;
|
|
1856
1893
|
seen.add(sig);
|
|
1857
1894
|
emitted += 1;
|
|
1858
|
-
|
|
1895
|
+
batch.push({
|
|
1859
1896
|
from: this.agentId,
|
|
1860
1897
|
to: "user",
|
|
1861
1898
|
body: step.text,
|
|
@@ -1863,6 +1900,7 @@ export class AgentRunner {
|
|
|
1863
1900
|
});
|
|
1864
1901
|
if (step.type === "narration") emittedNarrationKeys.add(sig);
|
|
1865
1902
|
}
|
|
1903
|
+
this.publishSessionEventsBatch(batch);
|
|
1866
1904
|
if (emitted) this.sessionDebug(`reasoning tail emitted ${emitted} step(s) (turn ${turnId ?? "?"}, ${seen.size} total)`);
|
|
1867
1905
|
};
|
|
1868
1906
|
const poll = (): Promise<void> => {
|
|
@@ -1870,7 +1908,7 @@ export class AgentRunner {
|
|
|
1870
1908
|
pendingPoll = next.catch(() => {});
|
|
1871
1909
|
return next;
|
|
1872
1910
|
};
|
|
1873
|
-
this.reasoningTail = {
|
|
1911
|
+
this.reasoningTail = { emittedNarrationKeys, poll, timer: setInterval(() => { void poll(); }, REASONING_POLL_MS) };
|
|
1874
1912
|
this.sessionLog(`reasoning tail started (turn ${turnIdAtStart ?? "?"})`);
|
|
1875
1913
|
void poll();
|
|
1876
1914
|
}
|