pi-subagent-feather 0.1.0 → 0.2.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/README.md +15 -2
- package/extensions/subagent/index.ts +140 -13
- package/package.json +1 -1
- package/src/collector.ts +90 -4
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pi-subagent-feather
|
|
2
2
|
基于 [`@eggmasonvalue/pi-subagent` 2.0.1](https://github.com/eggmasonvalue/pi-subagent) 的独立 MIT 派生版。
|
|
3
3
|
npm 包名 `pi-subagent-feather`,本地目录 `D:\Work\pi-subagent-lite`。
|
|
4
|
-
版本 `0.
|
|
4
|
+
版本 `0.2.0`。MIT 授权,见 LICENSE / NOTICE。
|
|
5
5
|
|
|
6
6
|
目标:保留独立上下文、子会话存档、resume、并行、超时与实时进度;减少父会话重复数据和额外提示词。
|
|
7
7
|
|
|
@@ -99,7 +99,7 @@ subagent_models({})
|
|
|
99
99
|
"resultLines": 200,
|
|
100
100
|
"progressBytes": 2048,
|
|
101
101
|
"stderrBytes": 4096,
|
|
102
|
-
"eventBytes":
|
|
102
|
+
"eventBytes": 1048576
|
|
103
103
|
}
|
|
104
104
|
```
|
|
105
105
|
|
|
@@ -110,6 +110,19 @@ subagent_models({})
|
|
|
110
110
|
- 所有预算必须是正整数;最大 resultBytes 51200 / resultLines 2000 / eventBytes 64MiB。
|
|
111
111
|
- stderr 是运行期最多4KiB的尾部,失败且没有正文时供模型看;不保存在 details。
|
|
112
112
|
|
|
113
|
+
其他可选键(同一 config.json,未知键直接报错防拼写):
|
|
114
|
+
|
|
115
|
+
- `"offlineChildren": true`:子进程附加 `--offline`,跳过启动时的模型目录网络刷新(更快)。适合 API key 型 provider;OAuth 型子代理凭证过期时可能需要先在主会话刷新凭证。
|
|
116
|
+
- `"defaultTimeoutMs": 30000`:调用未传 `timeoutMs` 时的兜底超时,防失控子进程。
|
|
117
|
+
- `"resultKeep": "tail"`:完成的答案超预算时保留结尾(结论常在末尾);默认 `"head"`。流式进度始终保留头部。
|
|
118
|
+
- `eventBytes` 默认已降为 1MiB(真实子进程事件被 Pi 自身截断在 50KiB/2000 行内,1MiB 足够),单事件超限仍丢弃并在下一行恢复。
|
|
119
|
+
- 收集器解析前按事件类型做廉价预过滤,不关注的事件类型跳过 JSON.parse。
|
|
120
|
+
|
|
121
|
+
## /subagents 命令
|
|
122
|
+
|
|
123
|
+
`/subagents` 列出最近的子会话(时间、大小、模型、路径),纯人看界面,不进入模型上下文、零 token。
|
|
124
|
+
有 UI 时可选择一条并复制其 resume 路径;无 UI(或 RPC 降级)时以通知列出。
|
|
125
|
+
|
|
113
126
|
## 开发 / 验证
|
|
114
127
|
|
|
115
128
|
```sh
|
|
@@ -6,10 +6,10 @@ import * as os from "node:os";
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import type { AgentToolResult, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
8
8
|
import { getSupportedThinkingLevels, StringEnum, type Model } from "@earendil-works/pi-ai";
|
|
9
|
-
import { type ExtensionAPI, getAgentDir, getPackageDir, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { type ExtensionAPI, copyToClipboard, getAgentDir, getPackageDir, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { Container, Markdown, Text } from "@earendil-works/pi-tui";
|
|
11
11
|
import { Type } from "typebox";
|
|
12
|
-
import { ChildCollector, DEFAULT_LIMITS, type CollectorLimits, type CollectorUsage } from "../../src/collector.ts";
|
|
12
|
+
import { ChildCollector, DEFAULT_LIMITS, type CollectorLimits, type CollectorUsage, type KeepMode } from "../../src/collector.ts";
|
|
13
13
|
|
|
14
14
|
const CHILD_METADATA_SUFFIX = ".subagent.json";
|
|
15
15
|
const INTERNAL_TOOL_NAMES = new Set(["subagent", "subagent_models"]);
|
|
@@ -318,19 +318,39 @@ function bounded(text: string, bytes: number): string {
|
|
|
318
318
|
return Buffer.from(text.slice(0, end), "utf8").toString("utf8");
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
|
|
321
|
+
interface FeatherConfig {
|
|
322
|
+
limits: CollectorLimits;
|
|
323
|
+
offlineChildren: boolean;
|
|
324
|
+
defaultTimeoutMs?: number;
|
|
325
|
+
resultKeep: KeepMode;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function loadConfig(agentDir = getAgentDir()): FeatherConfig {
|
|
322
329
|
const file = path.join(agentDir, "pi-subagent-feather", "config.json");
|
|
323
|
-
if (!fs.existsSync(file)) return { ...DEFAULT_LIMITS };
|
|
330
|
+
if (!fs.existsSync(file)) return { limits: { ...DEFAULT_LIMITS }, offlineChildren: false, resultKeep: "head" };
|
|
324
331
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
325
332
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid config: ${file}`);
|
|
326
333
|
const limits = { ...DEFAULT_LIMITS };
|
|
334
|
+
const config: FeatherConfig = { limits, offlineChildren: false, resultKeep: "head" };
|
|
327
335
|
const maximum: CollectorLimits = { resultBytes: 51200, resultLines: 2000, progressBytes: 8192, stderrBytes: 16384, eventBytes: 64 * 1024 * 1024 };
|
|
328
|
-
for (const key of Object.keys(
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
for (const key of Object.keys(value)) {
|
|
337
|
+
if (key in limits) {
|
|
338
|
+
if (!Number.isInteger(value[key]) || value[key] < 1 || value[key] > maximum[key as keyof CollectorLimits]) throw new Error(`Invalid ${key} in ${file}`);
|
|
339
|
+
limits[key as keyof CollectorLimits] = value[key];
|
|
340
|
+
} else if (key === "offlineChildren") {
|
|
341
|
+
if (typeof value[key] !== "boolean") throw new Error(`Invalid offlineChildren in ${file}`);
|
|
342
|
+
config.offlineChildren = value[key];
|
|
343
|
+
} else if (key === "defaultTimeoutMs") {
|
|
344
|
+
if (!Number.isInteger(value[key]) || value[key] < 1 || value[key] > 2147483647) throw new Error(`Invalid defaultTimeoutMs in ${file}`);
|
|
345
|
+
config.defaultTimeoutMs = value[key];
|
|
346
|
+
} else if (key === "resultKeep") {
|
|
347
|
+
if (value[key] !== "head" && value[key] !== "tail") throw new Error(`Invalid resultKeep in ${file}`);
|
|
348
|
+
config.resultKeep = value[key];
|
|
349
|
+
} else {
|
|
350
|
+
throw new Error(`Unknown config key "${key}" in ${file}`);
|
|
351
|
+
}
|
|
332
352
|
}
|
|
333
|
-
return
|
|
353
|
+
return config;
|
|
334
354
|
}
|
|
335
355
|
|
|
336
356
|
function resolveSessionFile(sessionDir: string): string | undefined {
|
|
@@ -392,6 +412,8 @@ interface RunChildDependencies {
|
|
|
392
412
|
agentDir?: string;
|
|
393
413
|
invoke?: (args: string[]) => { command: string; args: string[] };
|
|
394
414
|
limits?: Partial<CollectorLimits>;
|
|
415
|
+
resultKeep?: KeepMode;
|
|
416
|
+
offlineChildren?: boolean;
|
|
395
417
|
}
|
|
396
418
|
|
|
397
419
|
async function runChild(
|
|
@@ -399,7 +421,8 @@ async function runChild(
|
|
|
399
421
|
resumePath: string | undefined, signal: AbortSignal | undefined, onUpdate: Update | undefined,
|
|
400
422
|
dependencies: RunChildDependencies = {},
|
|
401
423
|
): Promise<RunResult> {
|
|
402
|
-
const
|
|
424
|
+
const feather = loadConfig(dependencies.agentDir);
|
|
425
|
+
const collector = new ChildCollector({ ...feather.limits, ...dependencies.limits }, { resultKeep: dependencies.resultKeep ?? feather.resultKeep });
|
|
403
426
|
let sessionFile = resumePath, exitCode = -1, status: Status = "running";
|
|
404
427
|
const snapshot = (): Receipt => ({ version: 1, status, label, resumed: Boolean(resumePath), timeoutMs, exitCode,
|
|
405
428
|
sessionFile, sessionId: collector.sessionId, model: config.model ?? collector.model, thinking: config.thinking,
|
|
@@ -411,6 +434,7 @@ async function runChild(
|
|
|
411
434
|
if (!resumePath) await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
412
435
|
const args = ["--mode", "json", "-p"];
|
|
413
436
|
if (resumePath) args.push("--session", resumePath); else args.push("--session-dir", sessionDir);
|
|
437
|
+
if (dependencies.offlineChildren ?? feather.offlineChildren) args.push("--offline");
|
|
414
438
|
if (config.model) args.push("--model", config.model);
|
|
415
439
|
if (config.thinking) args.push("--thinking", config.thinking);
|
|
416
440
|
if (!config.tools.length) args.push("--no-tools"); else args.push("--tools", config.tools.join(","));
|
|
@@ -489,7 +513,78 @@ const SubagentParams = Type.Object({
|
|
|
489
513
|
|
|
490
514
|
export const __testing = { allowedThinkingLevels, compactModelCatalog, loadModelPolicy, metadataPath,
|
|
491
515
|
modelFacingResult, readChildMetadata, resolveFreshModel, resolveResumePath, runChild,
|
|
492
|
-
validateModelPolicy, writeChildMetadata, receiptOf,
|
|
516
|
+
validateModelPolicy, writeChildMetadata, receiptOf, loadConfig, recentChildSessions, formatBytes,
|
|
517
|
+
getPiInvocation, SubagentParams };
|
|
518
|
+
|
|
519
|
+
interface ChildSessionInfo {
|
|
520
|
+
path: string;
|
|
521
|
+
mtimeMs: number;
|
|
522
|
+
bytes: number;
|
|
523
|
+
sessionId?: string;
|
|
524
|
+
model?: string;
|
|
525
|
+
cwd?: string;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function formatBytes(n: number): string {
|
|
529
|
+
if (n < 1024) return `${n}B`;
|
|
530
|
+
if (n < 1024 * 1024) return `${Math.round(n / 1024)}KB`;
|
|
531
|
+
return `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Recent child sessions (newest first). Read-only: stats, sidecar metadata, first-line id. */
|
|
535
|
+
function recentChildSessions(agentDir = getAgentDir(), max = 24): ChildSessionInfo[] {
|
|
536
|
+
const root = path.join(agentDir, "sessions", "subagent-lite");
|
|
537
|
+
let runDirs: string[];
|
|
538
|
+
try {
|
|
539
|
+
runDirs = fs.readdirSync(root);
|
|
540
|
+
} catch {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
const sessions: ChildSessionInfo[] = [];
|
|
544
|
+
for (const run of runDirs) {
|
|
545
|
+
const dir = path.join(root, run);
|
|
546
|
+
let names: string[];
|
|
547
|
+
try {
|
|
548
|
+
names = fs.readdirSync(dir);
|
|
549
|
+
} catch {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
for (const name of names) {
|
|
553
|
+
if (!name.endsWith(".jsonl")) continue;
|
|
554
|
+
const file = path.join(dir, name);
|
|
555
|
+
let stat: fs.Stats;
|
|
556
|
+
try {
|
|
557
|
+
stat = fs.statSync(file);
|
|
558
|
+
} catch {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (!stat.isFile()) continue;
|
|
562
|
+
const info: ChildSessionInfo = { path: file, mtimeMs: stat.mtimeMs, bytes: stat.size };
|
|
563
|
+
try {
|
|
564
|
+
const meta = readChildMetadata(file);
|
|
565
|
+
info.model = meta.model;
|
|
566
|
+
info.cwd = meta.cwd;
|
|
567
|
+
} catch {
|
|
568
|
+
// Sidecar metadata is optional for listing.
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const fd = fs.openSync(file, "r");
|
|
572
|
+
try {
|
|
573
|
+
const buf = Buffer.alloc(512);
|
|
574
|
+
const read = fs.readSync(fd, buf, 0, 512, 0);
|
|
575
|
+
const first = JSON.parse(buf.toString("utf8", 0, read).split("\n", 1)[0]);
|
|
576
|
+
if (first?.type === "session" && first.id) info.sessionId = String(first.id).slice(0, 64);
|
|
577
|
+
} finally {
|
|
578
|
+
fs.closeSync(fd);
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
// First-line probe is optional.
|
|
582
|
+
}
|
|
583
|
+
sessions.push(info);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return sessions.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, max);
|
|
587
|
+
}
|
|
493
588
|
|
|
494
589
|
export default function (pi: ExtensionAPI) {
|
|
495
590
|
const running = new Map<AbortController, Promise<RunResult>>();
|
|
@@ -498,6 +593,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
498
593
|
await Promise.allSettled(running.values());
|
|
499
594
|
running.clear();
|
|
500
595
|
});
|
|
596
|
+
pi.registerCommand("subagents", {
|
|
597
|
+
description: "List recent subagent child sessions (pick one to copy its resume path)",
|
|
598
|
+
handler: async (_args, ctx) => {
|
|
599
|
+
const sessions = recentChildSessions();
|
|
600
|
+
if (sessions.length === 0) {
|
|
601
|
+
ctx.ui.notify("No subagent child sessions found.", "info");
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const lines = sessions.map(s => `${new Date(s.mtimeMs).toLocaleString()} ${formatBytes(s.bytes)} ${s.model ?? "?"} ${s.path}`);
|
|
605
|
+
if (!ctx.hasUI) {
|
|
606
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
let picked: string | undefined;
|
|
610
|
+
try {
|
|
611
|
+
picked = await ctx.ui.select(`Subagent sessions (${sessions.length})`, lines);
|
|
612
|
+
} catch {
|
|
613
|
+
picked = undefined;
|
|
614
|
+
}
|
|
615
|
+
if (!picked) return;
|
|
616
|
+
const chosen = sessions[lines.indexOf(picked)];
|
|
617
|
+
if (!chosen) return;
|
|
618
|
+
try {
|
|
619
|
+
await copyToClipboard(chosen.path);
|
|
620
|
+
ctx.ui.notify(`Copied resume path: ${chosen.path}`, "info");
|
|
621
|
+
} catch {
|
|
622
|
+
ctx.ui.notify(`Resume path: ${chosen.path}`, "info");
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
});
|
|
501
626
|
pi.registerTool({
|
|
502
627
|
name: "subagent_models", label: "Subagent Models",
|
|
503
628
|
description: "List child model policy, defaults and supported thinking levels. No child starts.",
|
|
@@ -537,8 +662,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
537
662
|
tools: params.tools ? [...new Set(params.tools)] : pi.getActiveTools().filter(name => !INTERNAL_TOOL_NAMES.has(name)) };
|
|
538
663
|
}
|
|
539
664
|
const controller = new AbortController();
|
|
540
|
-
const
|
|
541
|
-
|
|
665
|
+
const feather = loadConfig();
|
|
666
|
+
const child = runChild(config, params.task, params.label, params.timeoutMs ?? feather.defaultTimeoutMs, resumePath,
|
|
667
|
+
signal ? AbortSignal.any([signal, controller.signal]) : controller.signal, onUpdate,
|
|
668
|
+
{ resultKeep: feather.resultKeep });
|
|
542
669
|
running.set(controller, child);
|
|
543
670
|
try {
|
|
544
671
|
const result = await child;
|
package/package.json
CHANGED
package/src/collector.ts
CHANGED
|
@@ -22,12 +22,15 @@ export interface CollectorLimits {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** Default budgets, identical in spirit to upstream but chosen for a safe fork. */
|
|
25
|
+
/** Where an oversized completed answer keeps content: head (default) or tail. */
|
|
26
|
+
export type KeepMode = "head" | "tail";
|
|
27
|
+
|
|
25
28
|
export const DEFAULT_LIMITS: CollectorLimits = {
|
|
26
29
|
resultBytes: 8192,
|
|
27
30
|
resultLines: 200,
|
|
28
31
|
progressBytes: 2048,
|
|
29
32
|
stderrBytes: 4096,
|
|
30
|
-
eventBytes:
|
|
33
|
+
eventBytes: 1024 * 1024,
|
|
31
34
|
};
|
|
32
35
|
|
|
33
36
|
/** Accumulated token/cost accounting exposed by {@link ChildCollector.usage}. */
|
|
@@ -266,6 +269,75 @@ class BoundedText {
|
|
|
266
269
|
}
|
|
267
270
|
}
|
|
268
271
|
|
|
272
|
+
/** Anything BoundedText/TailText can append into. */
|
|
273
|
+
interface TextSink {
|
|
274
|
+
append(input: string): void;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Raw-substring markers for the event types this collector reads. Pi writes compact
|
|
279
|
+
* JSON (`"type":"x"`, no spaces), so a line without any needle cannot be one of them;
|
|
280
|
+
* such lines skip JSON.parse entirely. Key order does not matter (scan, not prefix match).
|
|
281
|
+
*/
|
|
282
|
+
const EVENT_TYPE_NEEDLES = [
|
|
283
|
+
"session",
|
|
284
|
+
"message_start",
|
|
285
|
+
"message_update",
|
|
286
|
+
"message_end",
|
|
287
|
+
"tool_execution_start",
|
|
288
|
+
"tool_execution_end",
|
|
289
|
+
].map(type => `"type":"${type}"`);
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Bounded accumulator that keeps the most recent content (tail) instead of the head.
|
|
293
|
+
* Compaction re-slices only when the budget is exceeded; at the default few-KiB budgets
|
|
294
|
+
* the amortized cost per appended byte is O(1).
|
|
295
|
+
*/
|
|
296
|
+
class TailText {
|
|
297
|
+
private readonly maxBytes: number;
|
|
298
|
+
private readonly maxLines: number;
|
|
299
|
+
private readonly separator: string;
|
|
300
|
+
private buf = "";
|
|
301
|
+
private segments = 0;
|
|
302
|
+
truncated = false;
|
|
303
|
+
|
|
304
|
+
constructor(maxBytes: number, maxLines: number, separator: string) {
|
|
305
|
+
this.maxBytes = maxBytes;
|
|
306
|
+
this.maxLines = maxLines;
|
|
307
|
+
this.separator = separator;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
append(input: string): void {
|
|
311
|
+
if (input.length === 0) return;
|
|
312
|
+
if (this.maxBytes <= 0 || this.maxLines <= 0) {
|
|
313
|
+
this.truncated = true;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (this.segments > 0 && this.separator.length > 0) this.buf += this.separator;
|
|
317
|
+
this.segments += 1;
|
|
318
|
+
this.buf += input;
|
|
319
|
+
this.compact();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private compact(): void {
|
|
323
|
+
if (utf8Length(this.buf) > this.maxBytes) {
|
|
324
|
+
this.buf = detach(utf8Tail(this.buf, this.maxBytes));
|
|
325
|
+
this.truncated = true;
|
|
326
|
+
}
|
|
327
|
+
let newlines = 0;
|
|
328
|
+
for (let i = this.buf.indexOf("\n"); i !== -1; i = this.buf.indexOf("\n", i + 1)) newlines += 1;
|
|
329
|
+
if (newlines >= this.maxLines) {
|
|
330
|
+
const lines = this.buf.split("\n");
|
|
331
|
+
this.buf = detach(lines.slice(lines.length - this.maxLines).join("\n"));
|
|
332
|
+
this.truncated = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
value(): string {
|
|
337
|
+
return this.buf;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
269
341
|
function normalizeLimits(overrides?: Partial<CollectorLimits>): CollectorLimits {
|
|
270
342
|
const out: CollectorLimits = { ...DEFAULT_LIMITS };
|
|
271
343
|
if (overrides) {
|
|
@@ -288,7 +360,7 @@ function shortToolName(name: unknown): string {
|
|
|
288
360
|
return dunder >= 0 ? base.slice(dunder + 2) : base;
|
|
289
361
|
}
|
|
290
362
|
|
|
291
|
-
function appendTextContent(acc:
|
|
363
|
+
function appendTextContent(acc: TextSink, content: unknown): void {
|
|
292
364
|
if (typeof content === "string") {
|
|
293
365
|
acc.append(content);
|
|
294
366
|
return;
|
|
@@ -325,6 +397,8 @@ function addUsage(target: CollectorUsage, usage: TurnUsage): void {
|
|
|
325
397
|
*/
|
|
326
398
|
export class ChildCollector {
|
|
327
399
|
readonly limits: CollectorLimits;
|
|
400
|
+
/** Whether oversized completed answers keep their head or tail. */
|
|
401
|
+
readonly resultKeep: KeepMode;
|
|
328
402
|
|
|
329
403
|
/** Most recent completed non-empty assistant answer (bounded). */
|
|
330
404
|
finalText = "";
|
|
@@ -355,8 +429,9 @@ export class ChildCollector {
|
|
|
355
429
|
private bufferBytes = 0;
|
|
356
430
|
private dropping = false;
|
|
357
431
|
|
|
358
|
-
constructor(limits?: Partial<CollectorLimits
|
|
432
|
+
constructor(limits?: Partial<CollectorLimits>, options?: { resultKeep?: KeepMode }) {
|
|
359
433
|
this.limits = normalizeLimits(limits);
|
|
434
|
+
this.resultKeep = options?.resultKeep === "tail" ? "tail" : "head";
|
|
360
435
|
this.partialBuffer = new BoundedText(this.limits.resultBytes, this.limits.resultLines, "");
|
|
361
436
|
this.stdoutDecoder = new StringDecoder("utf8");
|
|
362
437
|
this.stderrDecoder = new StringDecoder("utf8");
|
|
@@ -459,7 +534,9 @@ export class ChildCollector {
|
|
|
459
534
|
const m = message as Record<string, any>;
|
|
460
535
|
if (m.role !== "assistant") return;
|
|
461
536
|
|
|
462
|
-
const acc =
|
|
537
|
+
const acc = this.resultKeep === "tail"
|
|
538
|
+
? new TailText(this.limits.resultBytes, this.limits.resultLines, "\n")
|
|
539
|
+
: new BoundedText(this.limits.resultBytes, this.limits.resultLines, "\n");
|
|
463
540
|
appendTextContent(acc, m.content);
|
|
464
541
|
const text = acc.value();
|
|
465
542
|
if (text.trim().length > 0) {
|
|
@@ -577,6 +654,15 @@ export class ChildCollector {
|
|
|
577
654
|
}
|
|
578
655
|
|
|
579
656
|
private processLine(line: string): void {
|
|
657
|
+
// Cheap pre-filter before JSON.parse: skip lines whose event type we never read.
|
|
658
|
+
let interesting = false;
|
|
659
|
+
for (const needle of EVENT_TYPE_NEEDLES) {
|
|
660
|
+
if (line.includes(needle)) {
|
|
661
|
+
interesting = true;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (!interesting) return;
|
|
580
666
|
const trimmed = line.trim();
|
|
581
667
|
if (trimmed.length === 0) return;
|
|
582
668
|
let event: unknown;
|