@posthog/agent 2.3.657 → 2.3.663

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.
@@ -1,5 +1,10 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import type { Logger } from "../utils/logger";
3
+ import {
4
+ createNodeStreamingUpload,
5
+ type StreamingUpload,
6
+ type StreamingUploadFactory,
7
+ } from "./streaming-upload";
3
8
 
4
9
  interface TaskRunEventStreamSenderConfig {
5
10
  apiUrl: string;
@@ -13,12 +18,11 @@ interface TaskRunEventStreamSenderConfig {
13
18
  retryDelayMs?: number;
14
19
  requestTimeoutMs?: number;
15
20
  stopTimeoutMs?: number;
16
- maxBatchEvents?: number;
17
- maxBatchBytes?: number;
18
21
  maxEventBytes?: number;
19
22
  maxStreamEvents?: number;
20
23
  maxStreamBytes?: number;
21
24
  streamWindowMs?: number;
25
+ createStreamingUpload?: StreamingUploadFactory;
22
26
  }
23
27
 
24
28
  interface EventEnvelope {
@@ -32,21 +36,20 @@ interface IngestResponse {
32
36
 
33
37
  interface ActiveStream {
34
38
  abortController: AbortController;
35
- writer: WritableStreamDefaultWriter<Uint8Array>;
39
+ upload: StreamingUpload;
36
40
  responsePromise: Promise<Response>;
37
41
  startedAtMs: number;
38
42
  sentThroughSeq: number;
39
43
  sentEvents: number;
40
44
  sentBytes: number;
45
+ windowTimer: ReturnType<typeof setTimeout> | null;
41
46
  }
42
47
 
43
- type StreamingRequestInit = RequestInit & { duplex: "half" };
44
-
45
48
  const DEFAULT_MAX_BUFFERED_EVENTS = 20_000;
46
49
  const DEFAULT_MAX_STREAM_EVENTS = 900;
47
50
  const DEFAULT_MAX_STREAM_BYTES = 4_000_000;
48
51
  const DEFAULT_MAX_EVENT_BYTES = 900_000;
49
- const DEFAULT_WRITE_DELAY_MS = 0;
52
+ const DEFAULT_FLUSH_DELAY_MS = 0;
50
53
  const DEFAULT_RETRY_DELAY_MS = 1_000;
51
54
  const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
52
55
  const DEFAULT_STOP_TIMEOUT_MS = 30_000;
@@ -59,17 +62,18 @@ export class TaskRunEventStreamSender {
59
62
  private readonly maxStreamEvents: number;
60
63
  private readonly maxStreamBytes: number;
61
64
  private readonly maxEventBytes: number;
62
- private readonly writeDelayMs: number;
65
+ private readonly flushDelayMs: number;
63
66
  private readonly retryDelayMs: number;
64
67
  private readonly requestTimeoutMs: number;
65
68
  private readonly stopTimeoutMs: number;
66
69
  private readonly streamWindowMs: number;
70
+ private readonly createStreamingUpload: StreamingUploadFactory;
67
71
  private readonly encoder = new TextEncoder();
68
72
  private sequence = 0;
69
73
  private lastKnownAcceptedSeq = 0;
70
74
  private bufferedEvents: EventEnvelope[] = [];
71
- private writeTimer: ReturnType<typeof setTimeout> | null = null;
72
- private writePromise: Promise<void> | null = null;
75
+ private flushTimer: ReturnType<typeof setTimeout> | null = null;
76
+ private flushPromise: Promise<void> | null = null;
73
77
  private streamClosePromise: Promise<void> | null = null;
74
78
  private activeStream: ActiveStream | null = null;
75
79
  private stopPromise: Promise<void> | null = null;
@@ -87,19 +91,17 @@ export class TaskRunEventStreamSender {
87
91
  )}/runs/${encodeURIComponent(config.runId)}/event_stream/`;
88
92
  this.maxBufferedEvents =
89
93
  config.maxBufferedEvents ?? DEFAULT_MAX_BUFFERED_EVENTS;
90
- this.maxStreamEvents =
91
- config.maxStreamEvents ??
92
- config.maxBatchEvents ??
93
- DEFAULT_MAX_STREAM_EVENTS;
94
- this.maxStreamBytes =
95
- config.maxStreamBytes ?? config.maxBatchBytes ?? DEFAULT_MAX_STREAM_BYTES;
94
+ this.maxStreamEvents = config.maxStreamEvents ?? DEFAULT_MAX_STREAM_EVENTS;
95
+ this.maxStreamBytes = config.maxStreamBytes ?? DEFAULT_MAX_STREAM_BYTES;
96
96
  this.maxEventBytes = config.maxEventBytes ?? DEFAULT_MAX_EVENT_BYTES;
97
- this.writeDelayMs = config.flushDelayMs ?? DEFAULT_WRITE_DELAY_MS;
97
+ this.flushDelayMs = config.flushDelayMs ?? DEFAULT_FLUSH_DELAY_MS;
98
98
  this.retryDelayMs = config.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
99
99
  this.requestTimeoutMs =
100
100
  config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
101
101
  this.stopTimeoutMs = config.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
102
102
  this.streamWindowMs = config.streamWindowMs ?? DEFAULT_STREAM_WINDOW_MS;
103
+ this.createStreamingUpload =
104
+ config.createStreamingUpload ?? createNodeStreamingUpload;
103
105
  }
104
106
 
105
107
  enqueue(event: Record<string, unknown>): void {
@@ -114,7 +116,7 @@ export class TaskRunEventStreamSender {
114
116
  event,
115
117
  };
116
118
  this.bufferedEvents.push(envelope);
117
- this.scheduleWrite();
119
+ this.scheduleFlush();
118
120
  }
119
121
 
120
122
  async stop(): Promise<void> {
@@ -125,20 +127,20 @@ export class TaskRunEventStreamSender {
125
127
 
126
128
  this.stopped = true;
127
129
 
128
- if (this.writeTimer) {
129
- clearTimeout(this.writeTimer);
130
- this.writeTimer = null;
130
+ if (this.flushTimer) {
131
+ clearTimeout(this.flushTimer);
132
+ this.flushTimer = null;
131
133
  }
132
134
 
133
135
  this.stopPromise = this.drainForStop();
134
136
  await this.stopPromise;
135
137
  }
136
138
 
137
- private scheduleWrite(delayMs = this.writeDelayMs): void {
138
- if (this.writeTimer || this.writePromise || this.stopped) return;
139
+ private scheduleFlush(delayMs = this.flushDelayMs): void {
140
+ if (this.flushTimer || this.flushPromise || this.stopped) return;
139
141
 
140
- this.writeTimer = setTimeout(() => {
141
- this.writeTimer = null;
142
+ this.flushTimer = setTimeout(() => {
143
+ this.flushTimer = null;
142
144
  void this.flush();
143
145
  }, delayMs);
144
146
  }
@@ -180,8 +182,8 @@ export class TaskRunEventStreamSender {
180
182
  }
181
183
 
182
184
  private async flush(): Promise<boolean> {
183
- if (this.writePromise) {
184
- await this.writePromise.catch(() => undefined);
185
+ if (this.flushPromise) {
186
+ await this.flushPromise.catch(() => undefined);
185
187
  }
186
188
 
187
189
  if (this.bufferedEvents.length === 0) {
@@ -189,11 +191,11 @@ export class TaskRunEventStreamSender {
189
191
  }
190
192
 
191
193
  const previousBufferLength = this.bufferedEvents.length;
192
- const writePromise = this.writeBufferedEvents();
193
- this.writePromise = writePromise;
194
+ const flushPromise = this.flushBufferedEvents();
195
+ this.flushPromise = flushPromise;
194
196
 
195
197
  try {
196
- await writePromise;
198
+ await flushPromise;
197
199
  return this.bufferedEvents.length < previousBufferLength;
198
200
  } catch (error) {
199
201
  this.config.logger.warn(
@@ -202,20 +204,20 @@ export class TaskRunEventStreamSender {
202
204
  );
203
205
  await this.abortActiveStream();
204
206
  if (!this.stopped) {
205
- this.scheduleWrite(this.retryDelayMs);
207
+ this.scheduleFlush(this.retryDelayMs);
206
208
  }
207
209
  return false;
208
210
  } finally {
209
- if (this.writePromise === writePromise) {
210
- this.writePromise = null;
211
+ if (this.flushPromise === flushPromise) {
212
+ this.flushPromise = null;
211
213
  }
212
214
  if (!this.stopped && this.hasUnwrittenBufferedEvents()) {
213
- this.scheduleWrite(0);
215
+ this.scheduleFlush(0);
214
216
  }
215
217
  }
216
218
  }
217
219
 
218
- private async writeBufferedEvents(): Promise<void> {
220
+ private async flushBufferedEvents(): Promise<void> {
219
221
  while (true) {
220
222
  const stream = await this.ensureActiveStream();
221
223
  const nextEvent = this.bufferedEvents.find(
@@ -232,7 +234,7 @@ export class TaskRunEventStreamSender {
232
234
  continue;
233
235
  }
234
236
 
235
- await stream.writer.write(this.encoder.encode(line));
237
+ await stream.upload.write(this.encoder.encode(line));
236
238
  stream.sentThroughSeq = nextEvent.seq;
237
239
  stream.sentEvents += 1;
238
240
  stream.sentBytes += lineBytes;
@@ -254,7 +256,7 @@ export class TaskRunEventStreamSender {
254
256
  (event) => event.seq > stream.sentThroughSeq,
255
257
  );
256
258
  if (hasUnwrittenEvents) {
257
- await this.writeBufferedEvents();
259
+ await this.flushBufferedEvents();
258
260
  continue;
259
261
  }
260
262
 
@@ -272,7 +274,7 @@ export class TaskRunEventStreamSender {
272
274
  continue;
273
275
  }
274
276
 
275
- await stream.writer.write(this.encoder.encode(line));
277
+ await stream.upload.write(this.encoder.encode(line));
276
278
  stream.sentBytes += lineBytes;
277
279
  return;
278
280
  }
@@ -310,32 +312,77 @@ export class TaskRunEventStreamSender {
310
312
 
311
313
  await this.syncSequenceWithServer();
312
314
 
313
- const bodyStream = new TransformStream<Uint8Array, Uint8Array>();
314
315
  const abortController = new AbortController();
315
- const requestInit: StreamingRequestInit = {
316
- method: "POST",
316
+ const upload = this.createStreamingUpload({
317
+ url: this.ingestUrl,
317
318
  headers: this.buildHeaders(),
318
- body: bodyStream.readable as BodyInit,
319
- signal: abortController.signal,
320
- duplex: "half",
321
- };
322
- const responsePromise = fetch(this.ingestUrl, requestInit);
319
+ abortController,
320
+ });
323
321
  const activeStream: ActiveStream = {
324
322
  abortController,
325
- writer: bodyStream.writable.getWriter(),
326
- responsePromise,
323
+ upload,
324
+ responsePromise: upload.responsePromise,
327
325
  startedAtMs: Date.now(),
328
326
  sentThroughSeq: this.lastKnownAcceptedSeq,
329
327
  sentEvents: 0,
330
328
  sentBytes: 0,
329
+ windowTimer: null,
331
330
  };
332
331
  this.activeStream = activeStream;
333
- responsePromise.catch((error) => {
332
+ this.scheduleStreamWindowClose(activeStream);
333
+ upload.responsePromise.catch((error) => {
334
334
  void this.handleActiveStreamResponseFailure(activeStream, error);
335
335
  });
336
336
  return activeStream;
337
337
  }
338
338
 
339
+ private scheduleStreamWindowClose(
340
+ stream: ActiveStream,
341
+ delayOverrideMs?: number,
342
+ ): void {
343
+ this.clearStreamWindowClose(stream);
344
+ // Rotate long-lived uploads even when the agent goes idle; this is a
345
+ // transport boundary, not a batching window.
346
+ const delayMs =
347
+ delayOverrideMs ??
348
+ Math.max(0, stream.startedAtMs + this.streamWindowMs - Date.now());
349
+ stream.windowTimer = setTimeout(() => {
350
+ stream.windowTimer = null;
351
+ void this.closeExpiredStream(stream);
352
+ }, delayMs);
353
+ }
354
+
355
+ private clearStreamWindowClose(stream: ActiveStream): void {
356
+ if (!stream.windowTimer) {
357
+ return;
358
+ }
359
+ clearTimeout(stream.windowTimer);
360
+ stream.windowTimer = null;
361
+ }
362
+
363
+ private async closeExpiredStream(stream: ActiveStream): Promise<void> {
364
+ if (this.activeStream !== stream || this.stopped) {
365
+ return;
366
+ }
367
+
368
+ if (this.flushPromise) {
369
+ this.scheduleStreamWindowClose(stream, 50);
370
+ return;
371
+ }
372
+
373
+ try {
374
+ await this.closeActiveStream();
375
+ } catch (error) {
376
+ this.config.logger.warn(
377
+ "Task run event ingest stream window close failed",
378
+ this.describeError(error),
379
+ );
380
+ if (!this.stopped && this.bufferedEvents.length > 0) {
381
+ this.scheduleFlush(this.retryDelayMs);
382
+ }
383
+ }
384
+ }
385
+
339
386
  private async handleActiveStreamResponseFailure(
340
387
  stream: ActiveStream,
341
388
  error: unknown,
@@ -357,7 +404,7 @@ export class TaskRunEventStreamSender {
357
404
  );
358
405
  }
359
406
  if (!this.stopped && this.bufferedEvents.length > 0) {
360
- this.scheduleWrite(this.retryDelayMs);
407
+ this.scheduleFlush(this.retryDelayMs);
361
408
  }
362
409
  }
363
410
 
@@ -377,6 +424,7 @@ export class TaskRunEventStreamSender {
377
424
  try {
378
425
  await closePromise;
379
426
  } finally {
427
+ this.clearStreamWindowClose(stream);
380
428
  if (this.activeStream === stream) {
381
429
  this.activeStream = null;
382
430
  }
@@ -388,7 +436,7 @@ export class TaskRunEventStreamSender {
388
436
 
389
437
  private async closeStream(stream: ActiveStream): Promise<void> {
390
438
  try {
391
- await stream.writer.close();
439
+ await stream.upload.close();
392
440
  } catch (error) {
393
441
  stream.abortController.abort();
394
442
  this.sequenceSynced = false;
@@ -418,10 +466,11 @@ export class TaskRunEventStreamSender {
418
466
  }
419
467
 
420
468
  stream.abortController.abort();
469
+ this.clearStreamWindowClose(stream);
421
470
  try {
422
- await stream.writer.abort();
471
+ await stream.upload.abort();
423
472
  } catch {
424
- // The writer may already be closed by fetch after the abort.
473
+ // The upload may already be closed by the transport after the abort.
425
474
  } finally {
426
475
  if (this.activeStream === stream) {
427
476
  this.activeStream = null;
@@ -0,0 +1,160 @@
1
+ import { Buffer } from "node:buffer";
2
+ import {
3
+ type ClientRequest,
4
+ request as httpRequest,
5
+ type IncomingHttpHeaders,
6
+ } from "node:http";
7
+ import { request as httpsRequest } from "node:https";
8
+ import { URL } from "node:url";
9
+
10
+ export interface StreamingUpload {
11
+ write(chunk: Uint8Array): Promise<void>;
12
+ close(): Promise<void>;
13
+ abort(): Promise<void>;
14
+ responsePromise: Promise<Response>;
15
+ }
16
+
17
+ export interface StreamingUploadFactoryInput {
18
+ url: string;
19
+ headers: Record<string, string>;
20
+ abortController: AbortController;
21
+ }
22
+
23
+ export type StreamingUploadFactory = (
24
+ input: StreamingUploadFactoryInput,
25
+ ) => StreamingUpload;
26
+
27
+ function headersFromIncoming(headers: IncomingHttpHeaders): Headers {
28
+ const result = new Headers();
29
+ for (const [name, value] of Object.entries(headers)) {
30
+ if (value === undefined) {
31
+ continue;
32
+ }
33
+ if (Array.isArray(value)) {
34
+ for (const item of value) {
35
+ result.append(name, item);
36
+ }
37
+ } else {
38
+ result.set(name, String(value));
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+
44
+ function abortError(): Error {
45
+ const error = new Error("aborted");
46
+ error.name = "AbortError";
47
+ return error;
48
+ }
49
+
50
+ function writeRequestChunk(
51
+ request: ClientRequest,
52
+ chunk: Uint8Array,
53
+ ): Promise<void> {
54
+ return new Promise((resolve, reject) => {
55
+ const onError = (error: Error): void => {
56
+ request.off("error", onError);
57
+ reject(error);
58
+ };
59
+ request.once("error", onError);
60
+ request.write(Buffer.from(chunk), (error?: Error | null) => {
61
+ request.off("error", onError);
62
+ if (error) {
63
+ reject(error);
64
+ return;
65
+ }
66
+ resolve();
67
+ });
68
+ });
69
+ }
70
+
71
+ function closeRequest(request: ClientRequest): Promise<void> {
72
+ return new Promise((resolve, reject) => {
73
+ const onError = (error: Error): void => {
74
+ request.off("error", onError);
75
+ reject(error);
76
+ };
77
+ request.once("error", onError);
78
+ request.end(() => {
79
+ request.off("error", onError);
80
+ resolve();
81
+ });
82
+ });
83
+ }
84
+
85
+ export function createNodeStreamingUpload({
86
+ url,
87
+ headers,
88
+ abortController,
89
+ }: StreamingUploadFactoryInput): StreamingUpload {
90
+ const parsedUrl = new URL(url);
91
+ const requestFactory =
92
+ parsedUrl.protocol === "https:"
93
+ ? httpsRequest
94
+ : parsedUrl.protocol === "http:"
95
+ ? httpRequest
96
+ : undefined;
97
+ if (!requestFactory) {
98
+ throw new Error(`Unsupported event ingest protocol: ${parsedUrl.protocol}`);
99
+ }
100
+ const request = requestFactory(parsedUrl, {
101
+ method: "POST",
102
+ headers,
103
+ });
104
+
105
+ let closed = false;
106
+ const responsePromise = new Promise<Response>((resolve, reject) => {
107
+ request.on("response", (response) => {
108
+ const chunks: Buffer[] = [];
109
+ response.on("data", (chunk: Buffer | string) => {
110
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
111
+ });
112
+ response.on("end", () => {
113
+ resolve(
114
+ new Response(Buffer.concat(chunks), {
115
+ status: response.statusCode ?? 0,
116
+ statusText: response.statusMessage,
117
+ headers: headersFromIncoming(response.headers),
118
+ }),
119
+ );
120
+ });
121
+ response.on("error", reject);
122
+ });
123
+ request.on("error", reject);
124
+ });
125
+
126
+ const abortRequest = (): void => {
127
+ closed = true;
128
+ if (!request.destroyed) {
129
+ request.destroy(abortError());
130
+ }
131
+ };
132
+ abortController.signal.addEventListener("abort", abortRequest, {
133
+ once: true,
134
+ });
135
+ void responsePromise
136
+ .finally(() => {
137
+ abortController.signal.removeEventListener("abort", abortRequest);
138
+ })
139
+ .catch(() => undefined);
140
+
141
+ return {
142
+ async write(chunk: Uint8Array): Promise<void> {
143
+ if (closed) {
144
+ throw new Error("Cannot write to closed event ingest stream");
145
+ }
146
+ await writeRequestChunk(request, chunk);
147
+ },
148
+ async close(): Promise<void> {
149
+ if (closed) {
150
+ return;
151
+ }
152
+ closed = true;
153
+ await closeRequest(request);
154
+ },
155
+ async abort(): Promise<void> {
156
+ abortRequest();
157
+ },
158
+ responsePromise,
159
+ };
160
+ }
@@ -15,6 +15,8 @@ export interface AgentServerConfig {
15
15
  apiKey: string;
16
16
  projectId: number;
17
17
  jwtPublicKey: string; // RS256 public key for JWT verification
18
+ eventIngestToken?: string;
19
+ eventIngestStreamWindowMs?: number;
18
20
  mode: AgentMode;
19
21
  taskId: string;
20
22
  runId: string;
package/src/types.ts CHANGED
@@ -125,6 +125,8 @@ export interface TaskExecutionOptions {
125
125
  processCallbacks?: ProcessSpawnedCallback;
126
126
  /** Callback invoked when the agent calls the create_output tool for structured output */
127
127
  onStructuredOutput?: (output: Record<string, unknown>) => Promise<void>;
128
+ /** Additional directories the agent process can access beyond cwd. */
129
+ additionalDirectories?: string[];
128
130
  }
129
131
 
130
132
  export type LogLevel = "debug" | "info" | "warn" | "error";