@posthog/agent 2.3.104 → 2.3.116
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/dist/agent.js +72 -31
- package/dist/agent.js.map +1 -1
- package/dist/posthog-api.d.ts +6 -2
- package/dist/posthog-api.js +36 -19
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +81 -31
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +81 -31
- package/dist/server/bin.cjs.map +1 -1
- package/dist/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/agent.ts +5 -5
- package/src/posthog-api.test.ts +48 -0
- package/src/posthog-api.ts +54 -20
- package/src/server/agent-server.ts +13 -4
- package/src/session-log-writer.test.ts +216 -0
- package/src/session-log-writer.ts +48 -11
- package/src/types.ts +2 -1
package/dist/types.d.ts
CHANGED
|
@@ -88,7 +88,8 @@ type LogLevel = "debug" | "info" | "warn" | "error";
|
|
|
88
88
|
type OnLogCallback = (level: LogLevel, scope: string, message: string, data?: unknown) => void;
|
|
89
89
|
interface PostHogAPIConfig {
|
|
90
90
|
apiUrl: string;
|
|
91
|
-
getApiKey: () => string
|
|
91
|
+
getApiKey: () => string | Promise<string>;
|
|
92
|
+
refreshApiKey?: () => string | Promise<string>;
|
|
92
93
|
projectId: number;
|
|
93
94
|
userAgent?: string;
|
|
94
95
|
}
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -45,17 +45,17 @@ export class Agent {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
private _configureLlmGateway(overrideUrl?: string): {
|
|
48
|
+
private async _configureLlmGateway(overrideUrl?: string): Promise<{
|
|
49
49
|
gatewayUrl: string;
|
|
50
50
|
apiKey: string;
|
|
51
|
-
} | null {
|
|
51
|
+
} | null> {
|
|
52
52
|
if (!this.posthogAPI) {
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
try {
|
|
57
57
|
const gatewayUrl = overrideUrl ?? this.posthogAPI.getLlmGatewayUrl();
|
|
58
|
-
const apiKey = this.posthogAPI.getApiKey();
|
|
58
|
+
const apiKey = await this.posthogAPI.getApiKey();
|
|
59
59
|
|
|
60
60
|
process.env.OPENAI_BASE_URL = `${gatewayUrl}/v1`;
|
|
61
61
|
process.env.OPENAI_API_KEY = apiKey;
|
|
@@ -74,7 +74,7 @@ export class Agent {
|
|
|
74
74
|
taskRunId: string,
|
|
75
75
|
options: TaskExecutionOptions = {},
|
|
76
76
|
): Promise<InProcessAcpConnection> {
|
|
77
|
-
const gatewayConfig = this._configureLlmGateway(options.gatewayUrl);
|
|
77
|
+
const gatewayConfig = await this._configureLlmGateway(options.gatewayUrl);
|
|
78
78
|
this.logger.info("Configured LLM gateway", {
|
|
79
79
|
adapter: options.adapter,
|
|
80
80
|
});
|
|
@@ -175,7 +175,7 @@ export class Agent {
|
|
|
175
175
|
|
|
176
176
|
async cleanup(): Promise<void> {
|
|
177
177
|
if (this.sessionLogWriter && this.taskRunId) {
|
|
178
|
-
await this.sessionLogWriter.flush(this.taskRunId);
|
|
178
|
+
await this.sessionLogWriter.flush(this.taskRunId, { coalesce: true });
|
|
179
179
|
}
|
|
180
180
|
await this.acpConnection?.cleanup();
|
|
181
181
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { PostHogAPIClient } from "./posthog-api";
|
|
3
|
+
|
|
4
|
+
const mockFetch = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
7
|
+
|
|
8
|
+
describe("PostHogAPIClient", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("refreshes once when fetching task run logs gets an auth failure", async () => {
|
|
14
|
+
const getApiKey = vi.fn().mockResolvedValue("stale-token");
|
|
15
|
+
const refreshApiKey = vi.fn().mockResolvedValue("fresh-token");
|
|
16
|
+
const client = new PostHogAPIClient({
|
|
17
|
+
apiUrl: "https://app.posthog.com",
|
|
18
|
+
getApiKey,
|
|
19
|
+
refreshApiKey,
|
|
20
|
+
projectId: 1,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
mockFetch
|
|
24
|
+
.mockResolvedValueOnce({
|
|
25
|
+
ok: false,
|
|
26
|
+
status: 401,
|
|
27
|
+
statusText: "Unauthorized",
|
|
28
|
+
})
|
|
29
|
+
.mockResolvedValueOnce({
|
|
30
|
+
ok: true,
|
|
31
|
+
text: vi
|
|
32
|
+
.fn()
|
|
33
|
+
.mockResolvedValue(
|
|
34
|
+
`${JSON.stringify({ type: "notification", notification: { method: "foo" } })}\n`,
|
|
35
|
+
),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const logs = await client.fetchTaskRunLogs({
|
|
39
|
+
id: "run-1",
|
|
40
|
+
task: "task-1",
|
|
41
|
+
} as never);
|
|
42
|
+
|
|
43
|
+
expect(logs).toHaveLength(1);
|
|
44
|
+
expect(getApiKey).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(refreshApiKey).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/posthog-api.ts
CHANGED
|
@@ -47,27 +47,63 @@ export class PostHogAPIClient {
|
|
|
47
47
|
return host;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
private
|
|
51
|
-
return
|
|
52
|
-
Authorization: `Bearer ${this.config.getApiKey()}`,
|
|
53
|
-
"Content-Type": "application/json",
|
|
54
|
-
"User-Agent": this.config.userAgent ?? DEFAULT_USER_AGENT,
|
|
55
|
-
};
|
|
50
|
+
private isAuthFailure(status: number): boolean {
|
|
51
|
+
return status === 401 || status === 403;
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
private async
|
|
54
|
+
private async resolveApiKey(forceRefresh = false): Promise<string> {
|
|
55
|
+
if (forceRefresh && this.config.refreshApiKey) {
|
|
56
|
+
return this.config.refreshApiKey();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this.config.getApiKey();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async buildHeaders(
|
|
63
|
+
options: RequestInit,
|
|
64
|
+
forceRefresh = false,
|
|
65
|
+
): Promise<Headers> {
|
|
66
|
+
const headers = new Headers(options.headers);
|
|
67
|
+
headers.set(
|
|
68
|
+
"Authorization",
|
|
69
|
+
`Bearer ${await this.resolveApiKey(forceRefresh)}`,
|
|
70
|
+
);
|
|
71
|
+
headers.set("Content-Type", "application/json");
|
|
72
|
+
headers.set("User-Agent", this.config.userAgent ?? DEFAULT_USER_AGENT);
|
|
73
|
+
return headers;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async performRequest(
|
|
59
77
|
endpoint: string,
|
|
60
|
-
options: RequestInit
|
|
61
|
-
|
|
78
|
+
options: RequestInit,
|
|
79
|
+
forceRefresh = false,
|
|
80
|
+
): Promise<Response> {
|
|
62
81
|
const url = `${this.baseUrl}${endpoint}`;
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
return fetch(url, {
|
|
65
84
|
...options,
|
|
66
|
-
headers:
|
|
67
|
-
...this.headers,
|
|
68
|
-
...options.headers,
|
|
69
|
-
},
|
|
85
|
+
headers: await this.buildHeaders(options, forceRefresh),
|
|
70
86
|
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async performRequestWithRetry(
|
|
90
|
+
endpoint: string,
|
|
91
|
+
options: RequestInit = {},
|
|
92
|
+
): Promise<Response> {
|
|
93
|
+
let response = await this.performRequest(endpoint, options);
|
|
94
|
+
|
|
95
|
+
if (!response.ok && this.isAuthFailure(response.status)) {
|
|
96
|
+
response = await this.performRequest(endpoint, options, true);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async apiRequest<T>(
|
|
103
|
+
endpoint: string,
|
|
104
|
+
options: RequestInit = {},
|
|
105
|
+
): Promise<T> {
|
|
106
|
+
const response = await this.performRequestWithRetry(endpoint, options);
|
|
71
107
|
|
|
72
108
|
if (!response.ok) {
|
|
73
109
|
let errorMessage: string;
|
|
@@ -87,8 +123,8 @@ export class PostHogAPIClient {
|
|
|
87
123
|
return this.config.projectId;
|
|
88
124
|
}
|
|
89
125
|
|
|
90
|
-
getApiKey(): string {
|
|
91
|
-
return this.
|
|
126
|
+
async getApiKey(forceRefresh = false): Promise<string> {
|
|
127
|
+
return this.resolveApiKey(forceRefresh);
|
|
92
128
|
}
|
|
93
129
|
|
|
94
130
|
getLlmGatewayUrl(): string {
|
|
@@ -228,12 +264,10 @@ export class PostHogAPIClient {
|
|
|
228
264
|
*/
|
|
229
265
|
async fetchTaskRunLogs(taskRun: TaskRun): Promise<StoredEntry[]> {
|
|
230
266
|
const teamId = this.getTeamId();
|
|
267
|
+
const endpoint = `/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`;
|
|
231
268
|
|
|
232
269
|
try {
|
|
233
|
-
const response = await
|
|
234
|
-
`${this.baseUrl}/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`,
|
|
235
|
-
{ headers: this.headers },
|
|
236
|
-
);
|
|
270
|
+
const response = await this.performRequestWithRetry(endpoint);
|
|
237
271
|
|
|
238
272
|
if (!response.ok) {
|
|
239
273
|
if (response.status === 404) {
|
|
@@ -523,7 +523,9 @@ export class AgentServer {
|
|
|
523
523
|
// against async log persistence to object storage.
|
|
524
524
|
let assistantMessage: string | undefined;
|
|
525
525
|
try {
|
|
526
|
-
await this.session.logWriter.flush(this.session.payload.run_id
|
|
526
|
+
await this.session.logWriter.flush(this.session.payload.run_id, {
|
|
527
|
+
coalesce: true,
|
|
528
|
+
});
|
|
527
529
|
assistantMessage = this.session.logWriter.getFullAgentResponse(
|
|
528
530
|
this.session.payload.run_id,
|
|
529
531
|
);
|
|
@@ -747,6 +749,9 @@ export class AgentServer {
|
|
|
747
749
|
});
|
|
748
750
|
|
|
749
751
|
this.logger.info("Session initialized successfully");
|
|
752
|
+
this.logger.info(
|
|
753
|
+
`Agent version: ${this.config.version ?? packageJson.version}`,
|
|
754
|
+
);
|
|
750
755
|
|
|
751
756
|
// Signal in_progress so the UI can start polling for updates
|
|
752
757
|
this.posthogAPI
|
|
@@ -1111,7 +1116,9 @@ Important:
|
|
|
1111
1116
|
): Promise<void> {
|
|
1112
1117
|
if (this.session?.payload.run_id === payload.run_id) {
|
|
1113
1118
|
try {
|
|
1114
|
-
await this.session.logWriter.flush(payload.run_id
|
|
1119
|
+
await this.session.logWriter.flush(payload.run_id, {
|
|
1120
|
+
coalesce: true,
|
|
1121
|
+
});
|
|
1115
1122
|
} catch (error) {
|
|
1116
1123
|
this.logger.warn("Failed to flush session logs before completion", {
|
|
1117
1124
|
taskId: payload.task_id,
|
|
@@ -1270,7 +1277,7 @@ Important:
|
|
|
1270
1277
|
}
|
|
1271
1278
|
|
|
1272
1279
|
try {
|
|
1273
|
-
await this.session.logWriter.flush(payload.run_id);
|
|
1280
|
+
await this.session.logWriter.flush(payload.run_id, { coalesce: true });
|
|
1274
1281
|
} catch (error) {
|
|
1275
1282
|
this.logger.warn("Failed to flush logs before Slack relay", {
|
|
1276
1283
|
taskId: payload.task_id,
|
|
@@ -1473,7 +1480,9 @@ Important:
|
|
|
1473
1480
|
}
|
|
1474
1481
|
|
|
1475
1482
|
try {
|
|
1476
|
-
await this.session.logWriter.flush(this.session.payload.run_id
|
|
1483
|
+
await this.session.logWriter.flush(this.session.payload.run_id, {
|
|
1484
|
+
coalesce: true,
|
|
1485
|
+
});
|
|
1477
1486
|
} catch (error) {
|
|
1478
1487
|
this.logger.error("Failed to flush session logs", error);
|
|
1479
1488
|
}
|
|
@@ -159,6 +159,222 @@ describe("SessionLogWriter", () => {
|
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
161
|
|
|
162
|
+
describe("_doFlush does not prematurely coalesce", () => {
|
|
163
|
+
it("does not coalesce buffered chunks during a timed flush", async () => {
|
|
164
|
+
const sessionId = "s1";
|
|
165
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
166
|
+
|
|
167
|
+
// Buffer some chunks (no non-chunk event to trigger coalescing)
|
|
168
|
+
logWriter.appendRawLine(
|
|
169
|
+
sessionId,
|
|
170
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
171
|
+
content: { type: "text", text: "Hello " },
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
logWriter.appendRawLine(
|
|
175
|
+
sessionId,
|
|
176
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
177
|
+
content: { type: "text", text: "world" },
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Flush without any non-chunk event arriving — simulates
|
|
182
|
+
// the 500ms debounce timer firing mid-stream
|
|
183
|
+
await logWriter.flush(sessionId);
|
|
184
|
+
|
|
185
|
+
// No entries should have been sent — chunks are still buffered
|
|
186
|
+
expect(mockAppendLog).not.toHaveBeenCalled();
|
|
187
|
+
|
|
188
|
+
// Now a non-chunk event arrives, triggering natural coalescing
|
|
189
|
+
logWriter.appendRawLine(
|
|
190
|
+
sessionId,
|
|
191
|
+
makeSessionUpdate("usage_update", { used: 100 }),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await logWriter.flush(sessionId);
|
|
195
|
+
|
|
196
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(1);
|
|
197
|
+
const entries: StoredNotification[] = mockAppendLog.mock.calls[0][2];
|
|
198
|
+
expect(entries).toHaveLength(2); // coalesced agent_message + usage_update
|
|
199
|
+
const coalesced = entries[0].notification;
|
|
200
|
+
expect(coalesced.params?.update).toEqual({
|
|
201
|
+
sessionUpdate: "agent_message",
|
|
202
|
+
content: { type: "text", text: "Hello world" },
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("flushAll coalesces on shutdown", () => {
|
|
208
|
+
it("coalesces remaining chunks before flushing", async () => {
|
|
209
|
+
const sessionId = "s1";
|
|
210
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
211
|
+
|
|
212
|
+
logWriter.appendRawLine(
|
|
213
|
+
sessionId,
|
|
214
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
215
|
+
content: { type: "text", text: "partial response" },
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await logWriter.flushAll();
|
|
220
|
+
|
|
221
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(1);
|
|
222
|
+
const entries: StoredNotification[] = mockAppendLog.mock.calls[0][2];
|
|
223
|
+
expect(entries).toHaveLength(1);
|
|
224
|
+
const coalesced = entries[0].notification;
|
|
225
|
+
expect(coalesced.params?.update).toEqual({
|
|
226
|
+
sessionUpdate: "agent_message",
|
|
227
|
+
content: { type: "text", text: "partial response" },
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("flush with coalesce option", () => {
|
|
233
|
+
it("drains chunk buffer when coalesce is true", async () => {
|
|
234
|
+
const sessionId = "s1";
|
|
235
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
236
|
+
|
|
237
|
+
logWriter.appendRawLine(
|
|
238
|
+
sessionId,
|
|
239
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
240
|
+
content: { type: "text", text: "complete text" },
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await logWriter.flush(sessionId, { coalesce: true });
|
|
245
|
+
|
|
246
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(1);
|
|
247
|
+
const entries: StoredNotification[] = mockAppendLog.mock.calls[0][2];
|
|
248
|
+
const coalesced = entries[0].notification;
|
|
249
|
+
expect(coalesced.params?.update).toEqual({
|
|
250
|
+
sessionUpdate: "agent_message",
|
|
251
|
+
content: { type: "text", text: "complete text" },
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("does not coalesce when coalesce is false", async () => {
|
|
256
|
+
const sessionId = "s1";
|
|
257
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
258
|
+
|
|
259
|
+
logWriter.appendRawLine(
|
|
260
|
+
sessionId,
|
|
261
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
262
|
+
content: { type: "text", text: "buffered" },
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
await logWriter.flush(sessionId, { coalesce: false });
|
|
267
|
+
|
|
268
|
+
expect(mockAppendLog).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("direct agent_message supersedes chunks", () => {
|
|
273
|
+
it("discards buffered chunks when a direct agent_message arrives", async () => {
|
|
274
|
+
const sessionId = "s1";
|
|
275
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
276
|
+
|
|
277
|
+
// Buffer partial chunks
|
|
278
|
+
logWriter.appendRawLine(
|
|
279
|
+
sessionId,
|
|
280
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
281
|
+
content: { type: "text", text: "partial " },
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
logWriter.appendRawLine(
|
|
285
|
+
sessionId,
|
|
286
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
287
|
+
content: { type: "text", text: "text" },
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Direct agent_message arrives — authoritative full text
|
|
292
|
+
logWriter.appendRawLine(
|
|
293
|
+
sessionId,
|
|
294
|
+
makeSessionUpdate("agent_message", {
|
|
295
|
+
content: { type: "text", text: "complete full response" },
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await logWriter.flush(sessionId);
|
|
300
|
+
|
|
301
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(1);
|
|
302
|
+
const entries: StoredNotification[] = mockAppendLog.mock.calls[0][2];
|
|
303
|
+
// Only the direct agent_message — no coalesced partial entry
|
|
304
|
+
expect(entries).toHaveLength(1);
|
|
305
|
+
const coalesced = entries[0].notification;
|
|
306
|
+
expect(coalesced.params?.update).toEqual({
|
|
307
|
+
sessionUpdate: "agent_message",
|
|
308
|
+
content: { type: "text", text: "complete full response" },
|
|
309
|
+
});
|
|
310
|
+
expect(logWriter.getLastAgentMessage(sessionId)).toBe(
|
|
311
|
+
"complete full response",
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("is additive with earlier coalesced text in multi-message turns", async () => {
|
|
316
|
+
const sessionId = "s1";
|
|
317
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
318
|
+
|
|
319
|
+
// First assistant message: chunks coalesced by a tool_call event
|
|
320
|
+
logWriter.appendRawLine(
|
|
321
|
+
sessionId,
|
|
322
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
323
|
+
content: { type: "text", text: "first message" },
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
logWriter.appendRawLine(
|
|
327
|
+
sessionId,
|
|
328
|
+
makeSessionUpdate("tool_call", { toolCallId: "tc1" }),
|
|
329
|
+
);
|
|
330
|
+
// "first message" is now coalesced into currentTurnMessages
|
|
331
|
+
|
|
332
|
+
// Second assistant message arrives as direct agent_message
|
|
333
|
+
// (e.g., after tool result, no active chunk buffer)
|
|
334
|
+
logWriter.appendRawLine(
|
|
335
|
+
sessionId,
|
|
336
|
+
makeSessionUpdate("agent_message", {
|
|
337
|
+
content: { type: "text", text: "second message" },
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const response = logWriter.getFullAgentResponse(sessionId);
|
|
342
|
+
// Both messages are preserved — direct message is additive
|
|
343
|
+
expect(response).toBe("first message\n\nsecond message");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("persisted log does not contain stale entries when chunks are superseded", async () => {
|
|
347
|
+
const sessionId = "s1";
|
|
348
|
+
logWriter.register(sessionId, { taskId: "t1", runId: sessionId });
|
|
349
|
+
|
|
350
|
+
// Chunks buffered, then direct agent_message supersedes before coalescing
|
|
351
|
+
logWriter.appendRawLine(
|
|
352
|
+
sessionId,
|
|
353
|
+
makeSessionUpdate("agent_message_chunk", {
|
|
354
|
+
content: { type: "text", text: "partial" },
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
logWriter.appendRawLine(
|
|
358
|
+
sessionId,
|
|
359
|
+
makeSessionUpdate("agent_message", {
|
|
360
|
+
content: { type: "text", text: "complete" },
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await logWriter.flush(sessionId);
|
|
365
|
+
|
|
366
|
+
expect(mockAppendLog).toHaveBeenCalledTimes(1);
|
|
367
|
+
const entries: StoredNotification[] = mockAppendLog.mock.calls[0][2];
|
|
368
|
+
// Only the direct agent_message — no coalesced partial entry
|
|
369
|
+
expect(entries).toHaveLength(1);
|
|
370
|
+
const persisted = entries[0].notification;
|
|
371
|
+
expect(persisted.params?.update).toEqual({
|
|
372
|
+
sessionUpdate: "agent_message",
|
|
373
|
+
content: { type: "text", text: "complete" },
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
162
378
|
describe("register", () => {
|
|
163
379
|
it("does not re-register existing sessions", () => {
|
|
164
380
|
const sessionId = "s1";
|
|
@@ -54,9 +54,12 @@ export class SessionLogWriter {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async flushAll(): Promise<void> {
|
|
57
|
-
|
|
57
|
+
// Coalesce any in-progress chunk buffers before the final flush
|
|
58
|
+
// During normal operation, chunks are coalesced when the next non-chunk
|
|
59
|
+
// event arrives, but on shutdown there may be no subsequent event
|
|
58
60
|
const flushPromises: Promise<void>[] = [];
|
|
59
|
-
for (const sessionId of
|
|
61
|
+
for (const [sessionId, session] of this.sessions) {
|
|
62
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
60
63
|
flushPromises.push(this.flush(sessionId));
|
|
61
64
|
}
|
|
62
65
|
await Promise.all(flushPromises);
|
|
@@ -123,8 +126,14 @@ export class SessionLogWriter {
|
|
|
123
126
|
return;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
// Non-chunk event: flush any buffered chunks first
|
|
127
|
-
this
|
|
129
|
+
// Non-chunk event: flush any buffered chunks first.
|
|
130
|
+
// If this is a direct agent_message AND there are buffered chunks,
|
|
131
|
+
// the direct message supersedes the partial chunks
|
|
132
|
+
if (this.isDirectAgentMessage(message) && session.chunkBuffer) {
|
|
133
|
+
session.chunkBuffer = undefined;
|
|
134
|
+
} else {
|
|
135
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
136
|
+
}
|
|
128
137
|
|
|
129
138
|
const nonChunkAgentText = this.extractAgentMessageText(message);
|
|
130
139
|
if (nonChunkAgentText) {
|
|
@@ -155,7 +164,17 @@ export class SessionLogWriter {
|
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
async flush(
|
|
167
|
+
async flush(
|
|
168
|
+
sessionId: string,
|
|
169
|
+
{ coalesce = false }: { coalesce?: boolean } = {},
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
if (coalesce) {
|
|
172
|
+
const session = this.sessions.get(sessionId);
|
|
173
|
+
if (session) {
|
|
174
|
+
this.emitCoalescedMessage(sessionId, session);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
// Serialize flushes per session
|
|
160
179
|
const prev = this.flushQueues.get(sessionId) ?? Promise.resolve();
|
|
161
180
|
const next = prev.catch(() => {}).then(() => this._doFlush(sessionId));
|
|
@@ -175,9 +194,6 @@ export class SessionLogWriter {
|
|
|
175
194
|
return;
|
|
176
195
|
}
|
|
177
196
|
|
|
178
|
-
// Emit any buffered chunks before flushing
|
|
179
|
-
this.emitCoalescedMessage(sessionId, session);
|
|
180
|
-
|
|
181
197
|
const pending = this.pendingEntries.get(sessionId);
|
|
182
198
|
if (!this.posthogAPI || !pending?.length) {
|
|
183
199
|
return;
|
|
@@ -231,11 +247,21 @@ export class SessionLogWriter {
|
|
|
231
247
|
}
|
|
232
248
|
}
|
|
233
249
|
|
|
234
|
-
private
|
|
235
|
-
|
|
250
|
+
private getSessionUpdateType(
|
|
251
|
+
message: Record<string, unknown>,
|
|
252
|
+
): string | undefined {
|
|
253
|
+
if (message.method !== "session/update") return undefined;
|
|
236
254
|
const params = message.params as Record<string, unknown> | undefined;
|
|
237
255
|
const update = params?.update as Record<string, unknown> | undefined;
|
|
238
|
-
return update?.sessionUpdate
|
|
256
|
+
return update?.sessionUpdate as string | undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private isDirectAgentMessage(message: Record<string, unknown>): boolean {
|
|
260
|
+
return this.getSessionUpdateType(message) === "agent_message";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private isAgentMessageChunk(message: Record<string, unknown>): boolean {
|
|
264
|
+
return this.getSessionUpdateType(message) === "agent_message_chunk";
|
|
239
265
|
}
|
|
240
266
|
|
|
241
267
|
private extractChunkText(message: Record<string, unknown>): string {
|
|
@@ -290,6 +316,17 @@ export class SessionLogWriter {
|
|
|
290
316
|
getFullAgentResponse(sessionId: string): string | undefined {
|
|
291
317
|
const session = this.sessions.get(sessionId);
|
|
292
318
|
if (!session || session.currentTurnMessages.length === 0) return undefined;
|
|
319
|
+
|
|
320
|
+
if (session.chunkBuffer) {
|
|
321
|
+
this.logger.warn(
|
|
322
|
+
"getFullAgentResponse called with non-empty chunk buffer",
|
|
323
|
+
{
|
|
324
|
+
sessionId,
|
|
325
|
+
bufferedLength: session.chunkBuffer.text.length,
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
293
330
|
return session.currentTurnMessages.join("\n\n");
|
|
294
331
|
}
|
|
295
332
|
|
package/src/types.ts
CHANGED
|
@@ -126,7 +126,8 @@ export type OnLogCallback = (
|
|
|
126
126
|
|
|
127
127
|
export interface PostHogAPIConfig {
|
|
128
128
|
apiUrl: string;
|
|
129
|
-
getApiKey: () => string
|
|
129
|
+
getApiKey: () => string | Promise<string>;
|
|
130
|
+
refreshApiKey?: () => string | Promise<string>;
|
|
130
131
|
projectId: number;
|
|
131
132
|
userAgent?: string;
|
|
132
133
|
}
|