pi-long-task 0.7.0 → 0.7.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/CHANGELOG.md +8 -0
- package/README.md +5 -0
- package/package.json +1 -1
- package/src/coordinator.ts +28 -5
- package/src/session_guard.ts +13 -8
- package/src/todo_generator.ts +1 -0
- package/src/worker_session.ts +47 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to Pi Long Task are recorded here. This project follows semantic versioning.
|
|
4
4
|
|
|
5
|
+
## 0.7.1 - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Performance
|
|
8
|
+
|
|
9
|
+
- Coalesce and cap retained worker token-delta events instead of returning thousands of tiny diagnostic objects, while preserving complete final assistant results.
|
|
10
|
+
- Bound and throttle live worker commentary so long streamed responses do not trigger quadratic text processing or excessive progress renders.
|
|
11
|
+
- Keep planner-generated task counts lean by combining tightly coupled implementation, test, and documentation work, and guide workers to batch independent inspection and use focused checks.
|
|
12
|
+
|
|
5
13
|
## 0.7.0 - 2026-09-08
|
|
6
14
|
|
|
7
15
|
### Added
|
package/README.md
CHANGED
|
@@ -258,6 +258,10 @@ Pi Long Task coordinates a long request from planning through task completion:
|
|
|
258
258
|
5. **Write run artifacts:** the coordinator writes the generated/normalized `TODO.md`, `TASK_RESULT.md`, attempt summaries, and final run details to `tmp/pi-long-task/<run-id>/`.
|
|
259
259
|
6. **Commit only when enabled:** if `commit` is `true`, Pi Long Task may create a commit after each completed task using only eligible task changes. If commits are disabled, no commits are created; even when enabled, commits can be skipped when there are no eligible changes or the task outcome is not commit-worthy.
|
|
260
260
|
|
|
261
|
+
### Low-overhead streaming and planning
|
|
262
|
+
|
|
263
|
+
Pi Long Task keeps live status responsive without serializing every model token as a separate diagnostic event. Worker text updates use a bounded rolling status buffer, progress publication is throttled, and retained token deltas are coalesced and capped; the complete final assistant result remains available in each outcome. Planning prompts also tell the planner to use the fewest safe worker handoffs and to keep tightly coupled implementation, tests, and documentation in one assignment when they share context.
|
|
264
|
+
|
|
261
265
|
### Adaptive worker-session reuse
|
|
262
266
|
|
|
263
267
|
Reuse is enabled by default. Related sequential TODOs in the same coordinator run and worktree may share one idle Pi `AgentSession`, which avoids repeated startup and repository exploration. Reuse does not merge task semantics: every TODO still gets its complete current assignment, an explicit boundary from the previous assignment, its own result extraction, attempts, progress, and `TASK_RESULT` outcome.
|
|
@@ -426,6 +430,7 @@ Pi session statistics can be cumulative across reused assignments. `outcomes[].w
|
|
|
426
430
|
|
|
427
431
|
## Feature reference
|
|
428
432
|
|
|
433
|
+
- **Low-overhead execution:** coalesce and bound streamed token diagnostics, throttle live commentary updates, and minimize unnecessary model handoffs without dropping final worker results.
|
|
429
434
|
- **Adaptive TODO-planner budgets:** deterministically extend the normal 5-minute budget for explicit large item sets, up to 15 minutes, while preserving exact caller overrides.
|
|
430
435
|
- **Visible planner timing:** report effective budget, extension reason, elapsed/remaining time, grace entry, and safe partial-output diagnostics across CLI/TUI and headless progress.
|
|
431
436
|
- **Capability-aware planning:** warn when isolated workers are explicitly asked to use disabled extensions or unavailable browser tools, then constrain the plan to honest alternatives or a blocked result.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-long-task",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi coding agent extension that breaks large coding requests into tracked TODOs and runs them in bounded, adaptively reused AI worker sessions. A long-running task runner and subagent orchestrator for Pi, with a live TUI progress sidebar, retries, goal loops, and optional per-task git commits.",
|
|
6
6
|
"keywords": [
|
package/src/coordinator.ts
CHANGED
|
@@ -101,6 +101,11 @@ import {
|
|
|
101
101
|
|
|
102
102
|
export type { CoordinatorStatus } from "./types.ts";
|
|
103
103
|
|
|
104
|
+
const WORKER_PROGRESS_MAX_BUFFER_CHARS = 2_048;
|
|
105
|
+
const WORKER_PROGRESS_MAX_STATUS_CHARS = 800;
|
|
106
|
+
const WORKER_PROGRESS_MIN_CHARACTER_DELTA = 256;
|
|
107
|
+
const WORKER_PROGRESS_MIN_INTERVAL_MS = 100;
|
|
108
|
+
|
|
104
109
|
export const DEFAULT_COORDINATOR_OPTIONS = {
|
|
105
110
|
maxAttemptsPerTask: 3,
|
|
106
111
|
taskTimeoutMs: 900_000,
|
|
@@ -385,7 +390,9 @@ interface RuntimeOptions {
|
|
|
385
390
|
workerCostState: WorkerCostState;
|
|
386
391
|
workerActivityByWorker: Map<string, string>;
|
|
387
392
|
workerTextByWorker: Map<string, string>;
|
|
393
|
+
workerTextLengthByWorker: Map<string, number>;
|
|
388
394
|
workerTextPublishedLengthByWorker: Map<string, number>;
|
|
395
|
+
workerTextPublishedAtByWorker: Map<string, number>;
|
|
389
396
|
plannerDiagnostics: PlannerDiagnostic[];
|
|
390
397
|
capabilityWarnings: WorkerCapabilityWarning[];
|
|
391
398
|
workerSessionMetrics: WorkerSessionMetrics;
|
|
@@ -1183,7 +1190,9 @@ export async function runCoordinator(options: RunCoordinatorOptions): Promise<Co
|
|
|
1183
1190
|
activeTaskReference = taskPlanReference;
|
|
1184
1191
|
runtime.workerActivityByWorker.set(worker, initialActivity);
|
|
1185
1192
|
runtime.workerTextByWorker.delete(worker);
|
|
1193
|
+
runtime.workerTextLengthByWorker.delete(worker);
|
|
1186
1194
|
runtime.workerTextPublishedLengthByWorker.delete(worker);
|
|
1195
|
+
runtime.workerTextPublishedAtByWorker.delete(worker);
|
|
1187
1196
|
emitProgress(
|
|
1188
1197
|
runtime,
|
|
1189
1198
|
`Running TODO ${nextTask.taskId} — ${nextTask.title}${attempt > 1 ? ` (attempt ${attempt})` : ""}...`,
|
|
@@ -2208,7 +2217,9 @@ function buildRuntimeOptions(options: RunCoordinatorOptions): RuntimeOptions {
|
|
|
2208
2217
|
workerCostState: createWorkerCostState(),
|
|
2209
2218
|
workerActivityByWorker: new Map(),
|
|
2210
2219
|
workerTextByWorker: new Map(),
|
|
2220
|
+
workerTextLengthByWorker: new Map(),
|
|
2211
2221
|
workerTextPublishedLengthByWorker: new Map(),
|
|
2222
|
+
workerTextPublishedAtByWorker: new Map(),
|
|
2212
2223
|
plannerDiagnostics: [],
|
|
2213
2224
|
capabilityWarnings,
|
|
2214
2225
|
workerSessionMetrics: createWorkerSessionMetrics(),
|
|
@@ -2668,15 +2679,23 @@ function emitWorkerEventProgress(
|
|
|
2668
2679
|
let activeStatus = runtime.workerActivityByWorker.get(worker);
|
|
2669
2680
|
|
|
2670
2681
|
if (event.type === "message_update" && event.textDelta) {
|
|
2671
|
-
const
|
|
2682
|
+
const previousText = runtime.workerTextByWorker.get(worker) ?? "";
|
|
2683
|
+
const workerText = `${previousText}${event.textDelta}`.slice(-WORKER_PROGRESS_MAX_BUFFER_CHARS);
|
|
2684
|
+
const receivedLength = (runtime.workerTextLengthByWorker.get(worker) ?? 0) + event.textDelta.length;
|
|
2672
2685
|
runtime.workerTextByWorker.set(worker, workerText);
|
|
2686
|
+
runtime.workerTextLengthByWorker.set(worker, receivedLength);
|
|
2673
2687
|
const streamedStatus = activeStatusFromWorkerText(workerText);
|
|
2674
2688
|
const publishedLength = runtime.workerTextPublishedLengthByWorker.get(worker) ?? 0;
|
|
2675
|
-
const
|
|
2676
|
-
|
|
2689
|
+
const publishedAt = runtime.workerTextPublishedAtByWorker.get(worker);
|
|
2690
|
+
const nowMs = runtime.now().getTime();
|
|
2691
|
+
const sentenceBoundary = /[\n.!?:]\s*$/.test(event.textDelta);
|
|
2692
|
+
const enoughTimeElapsed = publishedAt === undefined || nowMs - publishedAt >= WORKER_PROGRESS_MIN_INTERVAL_MS;
|
|
2693
|
+
const enoughNewText = receivedLength - publishedLength >= WORKER_PROGRESS_MIN_CHARACTER_DELTA;
|
|
2694
|
+
if (streamedStatus && ((sentenceBoundary && enoughTimeElapsed) || enoughNewText)) {
|
|
2677
2695
|
activeStatus = streamedStatus;
|
|
2678
2696
|
runtime.workerActivityByWorker.set(worker, activeStatus);
|
|
2679
|
-
runtime.workerTextPublishedLengthByWorker.set(worker,
|
|
2697
|
+
runtime.workerTextPublishedLengthByWorker.set(worker, receivedLength);
|
|
2698
|
+
runtime.workerTextPublishedAtByWorker.set(worker, nowMs);
|
|
2680
2699
|
emitProgress(runtime, activeStatus, {
|
|
2681
2700
|
phase: "worker_tool",
|
|
2682
2701
|
taskId: task.taskId,
|
|
@@ -2694,7 +2713,9 @@ function emitWorkerEventProgress(
|
|
|
2694
2713
|
|
|
2695
2714
|
if (event.type === "message_end") {
|
|
2696
2715
|
runtime.workerTextByWorker.delete(worker);
|
|
2716
|
+
runtime.workerTextLengthByWorker.delete(worker);
|
|
2697
2717
|
runtime.workerTextPublishedLengthByWorker.delete(worker);
|
|
2718
|
+
runtime.workerTextPublishedAtByWorker.delete(worker);
|
|
2698
2719
|
}
|
|
2699
2720
|
|
|
2700
2721
|
if (event.activity) {
|
|
@@ -2780,7 +2801,9 @@ function stripToolOutcomePrefix(activity: string): string {
|
|
|
2780
2801
|
|
|
2781
2802
|
function activeStatusFromWorkerText(text: string): string {
|
|
2782
2803
|
const taskResultIndex = text.indexOf("TASK_RESULT:");
|
|
2783
|
-
|
|
2804
|
+
const normalized = (taskResultIndex >= 0 ? text.slice(0, taskResultIndex) : text).replace(/\s+/g, " ").trim();
|
|
2805
|
+
if (normalized.length <= WORKER_PROGRESS_MAX_STATUS_CHARS) return normalized;
|
|
2806
|
+
return `… ${normalized.slice(-WORKER_PROGRESS_MAX_STATUS_CHARS + 2)}`;
|
|
2784
2807
|
}
|
|
2785
2808
|
|
|
2786
2809
|
function emitObsoleteTaskOutcomeProgress(
|
package/src/session_guard.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface GuardedSessionPromptOptions {
|
|
|
23
23
|
dispose?: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const MAX_CAPTURED_SESSION_EVENTS = 512;
|
|
27
|
+
|
|
26
28
|
export interface GuardedSessionPromptResult {
|
|
27
29
|
assistantText: string;
|
|
28
30
|
/** True when the primary prompt deadline elapsed, even if the prompt safely completed during grace. */
|
|
@@ -54,7 +56,7 @@ export async function runGuardedSessionPrompt(
|
|
|
54
56
|
const events: unknown[] = [];
|
|
55
57
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
56
58
|
let assistantText = "";
|
|
57
|
-
let
|
|
59
|
+
let currentAssistantTextChunks: string[] = [];
|
|
58
60
|
let outputObserved = false;
|
|
59
61
|
let timedOut = false;
|
|
60
62
|
let graceExpired = false;
|
|
@@ -198,18 +200,19 @@ export async function runGuardedSessionPrompt(
|
|
|
198
200
|
} else {
|
|
199
201
|
unsubscribe = session.subscribe((event: unknown) => {
|
|
200
202
|
events.push(event);
|
|
203
|
+
if (events.length > MAX_CAPTURED_SESSION_EVENTS) events.shift();
|
|
201
204
|
if (isAssistantMessageStart(event)) {
|
|
202
|
-
|
|
205
|
+
currentAssistantTextChunks = [];
|
|
206
|
+
assistantText = "";
|
|
203
207
|
}
|
|
204
208
|
const delta = assistantTextDeltaFromEvent(event);
|
|
205
209
|
if (delta !== undefined) {
|
|
206
|
-
|
|
207
|
-
assistantText = currentAssistantText || assistantText;
|
|
210
|
+
if (delta) currentAssistantTextChunks.push(delta);
|
|
208
211
|
outputObserved ||= delta.trim().length > 0;
|
|
209
212
|
} else {
|
|
210
213
|
const text = assistantTextFromEvent(event);
|
|
211
214
|
if (text) {
|
|
212
|
-
|
|
215
|
+
currentAssistantTextChunks = [text];
|
|
213
216
|
assistantText = text;
|
|
214
217
|
outputObserved ||= text.trim().length > 0;
|
|
215
218
|
}
|
|
@@ -259,7 +262,7 @@ export async function runGuardedSessionPrompt(
|
|
|
259
262
|
clearTimers();
|
|
260
263
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
261
264
|
unsubscribe?.();
|
|
262
|
-
assistantText = latestAssistantText(session, events, assistantText);
|
|
265
|
+
assistantText = latestAssistantText(session, events, currentAssistantTextChunks.join("") || assistantText);
|
|
263
266
|
outputObserved ||= assistantText.trim().length > 0 && assistantText !== assistantTextAtStart;
|
|
264
267
|
if (options.dispose !== false) {
|
|
265
268
|
try {
|
|
@@ -331,8 +334,10 @@ function latestAssistantText(session: WorkerSessionLike, events: unknown[], fall
|
|
|
331
334
|
if (fromMessages) {
|
|
332
335
|
return fromMessages;
|
|
333
336
|
}
|
|
334
|
-
|
|
335
|
-
|
|
337
|
+
if (fallback) {
|
|
338
|
+
return fallback;
|
|
339
|
+
}
|
|
340
|
+
return lastAssistantTextFromEvents(events);
|
|
336
341
|
}
|
|
337
342
|
|
|
338
343
|
function timeoutMs(value: number | undefined): number {
|
package/src/todo_generator.ts
CHANGED
|
@@ -259,6 +259,7 @@ export function todoPlanningOnlyPromptBlock(capabilityConstraints: readonly stri
|
|
|
259
259
|
- Do not perform requested end work: do not implement or write code, execute research or report findings, create requested creative output (prose, stories, copy, designs, or assets), or produce any other final deliverable.
|
|
260
260
|
- Use future-worker action language; do not claim work is complete or invent results.
|
|
261
261
|
- Keep repeated task sections compact: use a one-sentence Goal and Done when, plus only the necessary Status and Verify bullets. Omit rationale, lengthy analysis, summaries, duplicated context, unrequested examples, and boilerplate.
|
|
262
|
+
- Minimize worker handoffs because each TODO starts another model assignment. Use the fewest tasks that safely preserve dependencies and explicit boundaries; combine tightly coupled implementation, tests, and documentation that use the same context. Do not create separate setup, audit, or final-verification tasks when that work belongs inside an implementation task.
|
|
262
263
|
- Preserve every instruction, constraint, required deliverable, and acceptance condition from the source request and supplied planning context. Put shared constraints above ## Progress and task-specific requirements in the relevant task.${capabilityBlock}`;
|
|
263
264
|
}
|
|
264
265
|
|
package/src/worker_session.ts
CHANGED
|
@@ -113,6 +113,7 @@ Rules:
|
|
|
113
113
|
- If you need to stop because context is high or the work is blocked, leave the repository in a safe state and report \`status: partial\` or \`status: blocked\`.
|
|
114
114
|
- Use the repository's AGENTS.md/project instructions.
|
|
115
115
|
- Run focused verification commands when practical.
|
|
116
|
+
- Minimize latency: batch independent reads and searches when possible, avoid repeated repository-wide scans, and prefer focused checks over full suites unless the task requires a full suite.
|
|
116
117
|
- Do not run bash commands with timeout greater than ${options.maxBashTimeoutSeconds.toFixed(0)} seconds. For long full-suite checks, run once with a bounded timeout and report any timeout/failure in TASK_RESULT instead of continuing indefinitely.
|
|
117
118
|
- If TODO-file global instructions restrict scope, obey them strictly. If the task appears to require out-of-scope code changes, stop and report \`status: blocked\` instead of changing those files.
|
|
118
119
|
|
|
@@ -253,6 +254,9 @@ export const DEFAULT_WORKER_TOOLS = ["read", "bash", "edit", "write", "grep", "f
|
|
|
253
254
|
export const DEFAULT_WORKER_THINKING_LEVEL = "high";
|
|
254
255
|
export const DEFAULT_TASK_TIMEOUT_SECONDS = 60 * 60;
|
|
255
256
|
export const DEFAULT_GRACEFUL_SHUTDOWN_SECONDS = 60;
|
|
257
|
+
/** Keep streamed diagnostics useful without returning thousands of token-delta objects. */
|
|
258
|
+
export const MAX_CAPTURED_WORKER_EVENTS = 256;
|
|
259
|
+
export const MAX_CAPTURED_STREAM_TEXT_CHARS = 4_096;
|
|
256
260
|
|
|
257
261
|
export interface WorkerSessionLike {
|
|
258
262
|
prompt(text: string, options?: Record<string, unknown>): Promise<void>;
|
|
@@ -532,7 +536,7 @@ export async function runWorkerTaskAssignment(
|
|
|
532
536
|
const compactionEvents: string[] = [];
|
|
533
537
|
const events: CapturedWorkerEvent[] = [];
|
|
534
538
|
let assistantText = "";
|
|
535
|
-
let
|
|
539
|
+
let currentAssistantTextChunks: string[] = [];
|
|
536
540
|
let sessionFile: string | undefined;
|
|
537
541
|
let sessionId: string | undefined;
|
|
538
542
|
let shutdownRequested = false;
|
|
@@ -561,7 +565,7 @@ export async function runWorkerTaskAssignment(
|
|
|
561
565
|
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
562
566
|
|
|
563
567
|
const capture = (event: CapturedWorkerEvent) => {
|
|
564
|
-
events
|
|
568
|
+
retainCapturedWorkerEvent(events, event);
|
|
565
569
|
options.onEvent?.(event);
|
|
566
570
|
};
|
|
567
571
|
|
|
@@ -711,7 +715,8 @@ export async function runWorkerTaskAssignment(
|
|
|
711
715
|
case "message_start": {
|
|
712
716
|
const message = event.message;
|
|
713
717
|
if (isRecord(message) && message.role === "assistant") {
|
|
714
|
-
|
|
718
|
+
currentAssistantTextChunks = [];
|
|
719
|
+
assistantText = "";
|
|
715
720
|
}
|
|
716
721
|
break;
|
|
717
722
|
}
|
|
@@ -719,8 +724,7 @@ export async function runWorkerTaskAssignment(
|
|
|
719
724
|
const assistantEvent = event.assistantMessageEvent;
|
|
720
725
|
if (isRecord(assistantEvent) && assistantEvent.type === "text_delta") {
|
|
721
726
|
const delta = typeof assistantEvent.delta === "string" ? assistantEvent.delta : "";
|
|
722
|
-
|
|
723
|
-
assistantText = currentAssistantText || assistantText;
|
|
727
|
+
if (delta) currentAssistantTextChunks.push(delta);
|
|
724
728
|
}
|
|
725
729
|
break;
|
|
726
730
|
}
|
|
@@ -728,6 +732,7 @@ export async function runWorkerTaskAssignment(
|
|
|
728
732
|
const messageText = assistantMessageText(event.message);
|
|
729
733
|
if (messageText) {
|
|
730
734
|
assistantText = messageText;
|
|
735
|
+
currentAssistantTextChunks = [messageText];
|
|
731
736
|
}
|
|
732
737
|
recordWorkerUsageCost(workerUsageCostFromEvent(event), workerUsageCostKeyFromEvent(event));
|
|
733
738
|
break;
|
|
@@ -777,7 +782,8 @@ export async function runWorkerTaskAssignment(
|
|
|
777
782
|
|
|
778
783
|
if (taskTimeoutSeconds > 0) {
|
|
779
784
|
schedule(() => {
|
|
780
|
-
|
|
785
|
+
const currentAssistantText = currentAssistantTextChunks.join("") || assistantText;
|
|
786
|
+
if (finished || hasCompleteTaskResult(currentAssistantText)) {
|
|
781
787
|
return;
|
|
782
788
|
}
|
|
783
789
|
timedOut = true;
|
|
@@ -792,14 +798,24 @@ export async function runWorkerTaskAssignment(
|
|
|
792
798
|
}
|
|
793
799
|
|
|
794
800
|
await waitForPrompt(prompt);
|
|
795
|
-
assistantText = latestInvocationAssistantText(
|
|
801
|
+
assistantText = latestInvocationAssistantText(
|
|
802
|
+
session,
|
|
803
|
+
currentAssistantTextChunks.join("") || assistantText,
|
|
804
|
+
invocationMessageStart,
|
|
805
|
+
!reusedAssignment,
|
|
806
|
+
);
|
|
796
807
|
|
|
797
808
|
if (!hasCompleteTaskResult(assistantText) && !error && !aborted && !timedOut && !options.abortSignal?.aborted) {
|
|
798
809
|
contextObservations.push(
|
|
799
810
|
"missing TASK_RESULT status after initial prompt, or required fields were incomplete; requested required block once",
|
|
800
811
|
);
|
|
801
812
|
await waitForPrompt(buildMissingTaskResultMessage());
|
|
802
|
-
assistantText = latestInvocationAssistantText(
|
|
813
|
+
assistantText = latestInvocationAssistantText(
|
|
814
|
+
session,
|
|
815
|
+
currentAssistantTextChunks.join("") || assistantText,
|
|
816
|
+
invocationMessageStart,
|
|
817
|
+
!reusedAssignment,
|
|
818
|
+
);
|
|
803
819
|
}
|
|
804
820
|
} catch (exc) {
|
|
805
821
|
failure ??= exc;
|
|
@@ -809,7 +825,12 @@ export async function runWorkerTaskAssignment(
|
|
|
809
825
|
clearTimers();
|
|
810
826
|
options.abortSignal?.removeEventListener("abort", abortListener);
|
|
811
827
|
unsubscribe?.();
|
|
812
|
-
assistantText = latestInvocationAssistantText(
|
|
828
|
+
assistantText = latestInvocationAssistantText(
|
|
829
|
+
session,
|
|
830
|
+
currentAssistantTextChunks.join("") || assistantText,
|
|
831
|
+
invocationMessageStart,
|
|
832
|
+
!reusedAssignment,
|
|
833
|
+
);
|
|
813
834
|
sessionFile = session.sessionFile ?? sessionFile;
|
|
814
835
|
sessionId = session.sessionId ?? sessionId;
|
|
815
836
|
sessionStatsEnd = await workerSessionStatsSnapshot(session);
|
|
@@ -907,6 +928,23 @@ export function buildWorkerSessionCreationFailureOutcome(
|
|
|
907
928
|
};
|
|
908
929
|
}
|
|
909
930
|
|
|
931
|
+
function retainCapturedWorkerEvent(events: CapturedWorkerEvent[], event: CapturedWorkerEvent): void {
|
|
932
|
+
if (event.type === "message_update" && event.textDelta !== undefined) {
|
|
933
|
+
const previous = events.at(-1);
|
|
934
|
+
if (previous?.type === "message_update" && previous.textDelta !== undefined) {
|
|
935
|
+
previous.textDelta = `${previous.textDelta}${event.textDelta}`.slice(-MAX_CAPTURED_STREAM_TEXT_CHARS);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
events.push(event);
|
|
941
|
+
if (events.length <= MAX_CAPTURED_WORKER_EVENTS) return;
|
|
942
|
+
|
|
943
|
+
// Prefer evicting token-stream diagnostics over lifecycle/tool evidence.
|
|
944
|
+
const streamedIndex = events.findIndex((item) => item.type === "message_update");
|
|
945
|
+
events.splice(streamedIndex >= 0 ? streamedIndex : 0, 1);
|
|
946
|
+
}
|
|
947
|
+
|
|
910
948
|
function textFromContentPart(item: unknown): string {
|
|
911
949
|
if (!isRecord(item)) {
|
|
912
950
|
return "";
|