@tea-agent/loop-agent 0.16.25 → 0.17.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/CHANGELOG.md +46 -0
- package/README.md +14 -3
- package/dist/cli/command-definitions.js +43 -0
- package/dist/cli/program.js +26 -0
- package/dist/commands/dag-approve.js +4 -0
- package/dist/commands/dag-resume.js +1 -0
- package/dist/commands/dag-validate.js +6 -0
- package/dist/commands/operator.js +44 -0
- package/dist/commands/task-contract.js +271 -0
- package/dist/executors/dag-pi-executor.js +118 -16
- package/dist/executors/pi-executor.js +206 -13
- package/dist/executors/pi-sdk-executor.js +21 -6
- package/dist/executors/shell-executor.js +85 -8
- package/dist/executors/shell-presets.js +16 -3
- package/dist/executors/shell-write-guard.js +64 -2
- package/dist/shared/operator/capabilities.js +255 -0
- package/dist/shared/operator/envelope.js +59 -0
- package/dist/shared/operator/index.js +4 -0
- package/dist/shared/operator/registry.js +38 -0
- package/dist/shared/operator/types.js +5 -0
- package/dist/task/contract/adopt.js +166 -0
- package/dist/task/contract/apply.js +326 -0
- package/dist/task/contract/canonicalize.js +60 -0
- package/dist/task/contract/constants.js +29 -0
- package/dist/task/contract/diff.js +177 -0
- package/dist/task/contract/hash.js +42 -0
- package/dist/task/contract/import-revision.js +96 -0
- package/dist/task/contract/index.js +17 -0
- package/dist/task/contract/journal.js +155 -0
- package/dist/task/contract/lock.js +153 -0
- package/dist/task/contract/observe.js +296 -0
- package/dist/task/contract/paths.js +19 -0
- package/dist/task/contract/project.js +170 -0
- package/dist/task/contract/recover.js +312 -0
- package/dist/task/contract/request-ledger.js +37 -0
- package/dist/task/contract/schema.js +151 -0
- package/dist/task/contract/transaction.js +160 -0
- package/dist/task/contract/types.js +1 -0
- package/dist/task/contract/validate-draft.js +106 -0
- package/dist/task/index.js +3 -0
- package/dist/task/operator/capabilities.js +6 -0
- package/dist/task/operator/envelope.js +2 -0
- package/dist/task/operator/index.js +5 -0
- package/dist/task/operator/registry.js +2 -0
- package/dist/task/operator/types.js +1 -0
- package/dist/task/runtime.js +5 -1
- package/dist/task/source-references.js +7 -0
- package/dist/worker/cli.js +150 -32
- package/dist/worker/console/app-data.js +185 -0
- package/dist/worker/console/dag-confirmation.js +313 -0
- package/dist/worker/console/doctor.js +169 -0
- package/dist/worker/console/draft-store.js +80 -0
- package/dist/worker/console/index.js +15 -0
- package/dist/worker/console/interview/assessment.js +67 -0
- package/dist/worker/console/interview/session.js +100 -0
- package/dist/worker/console/interview/tools.js +109 -0
- package/dist/worker/console/loopback.js +16 -0
- package/dist/worker/console/observe-health-match.js +174 -0
- package/dist/worker/console/observe-link.js +33 -0
- package/dist/worker/console/operation-runner.js +166 -0
- package/dist/worker/console/operation-sse.js +158 -0
- package/dist/worker/console/operation-store.js +147 -0
- package/dist/worker/console/operator-actions.js +769 -0
- package/dist/worker/console/pi-readiness.js +94 -0
- package/dist/worker/console/recovery-cta.js +133 -0
- package/dist/worker/console/repo-fingerprint.js +29 -0
- package/dist/worker/console/resource-loader.js +95 -0
- package/dist/worker/console/routes.js +368 -0
- package/dist/worker/console/security.js +126 -0
- package/dist/worker/console/server.js +149 -0
- package/dist/worker/console/sibling-controller.js +28 -0
- package/dist/worker/console/static/assets/index-CbnMgdWa.js +9 -0
- package/dist/worker/console/static/assets/index-Dnj0RVs8.css +1 -0
- package/dist/worker/console/static/index.html +13 -0
- package/dist/worker/console/vite.config.js +27 -0
- package/dist/worker/delivery/git-transaction.js +43 -8
- package/dist/worker/observe/health.js +57 -0
- package/dist/worker/observe/paths.js +81 -0
- package/dist/worker/observe/routes.js +142 -27
- package/dist/worker/observe/spec-evidence.js +84 -0
- package/dist/worker/observe/static/api.js +23 -0
- package/dist/worker/observe/static/state.js +26 -0
- package/dist/worker/observe/static/styles.css +10 -0
- package/dist/worker/observe/static/views/dag-inspector.js +173 -6
- package/dist/workflows/dag/backend-test-analysis-contract.js +34 -9
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -0
- package/dist/workflows/dag/frontend-repair.js +1 -10
- package/dist/workflows/dag/init-hybrid.js +372 -115
- package/dist/workflows/dag/node-execution.js +57 -6
- package/dist/workflows/dag/project-governance-context.js +508 -0
- package/dist/workflows/dag/prompt.js +46 -1
- package/dist/workflows/dag/retry-policy.js +16 -1
- package/dist/workflows/dag/runner.js +9 -0
- package/dist/workflows/dag/skill-snapshot.js +1 -0
- package/dist/workflows/dag/task-contract-binding.js +138 -0
- package/dist/workflows/dag/types.js +84 -10
- package/dist/workflows/dag/validate.js +53 -7
- package/docs/README.md +2 -0
- package/docs/architecture/evolution.md +2 -0
- package/docs/architecture/system-overview.md +6 -0
- package/docs/architecture/worker-and-feature.md +7 -0
- package/docs/templates/agent-dag.schema.json +64 -2
- package/docs/templates/agent-dag.supervised-implementation.json +1 -0
- package/docs/templates/backend-test-dag.classify.prompt.md +1 -1
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -5
- package/docs/templates/backend-test-dag.json +26 -154
- package/docs/templates/backend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/backend-test-dag.review-cases.prompt.md +2 -2
- package/package.json +8 -2
- package/skills/agent-worker/SKILL.md +1 -0
- package/skills/agent-worker/references/agent-worker-operator.md +3 -2
- package/skills/frontend-design-review/SKILL.md +25 -16
- package/skills/frontend-implementation/references/node-contracts.md +5 -5
- package/skills/loop-agent/references/command-reference.md +48 -1
- package/skills/loop-agent/references/hybrid-dag.md +4 -4
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
|
|
3
4
|
import { executePiStep, } from "./pi-executor.js";
|
|
4
5
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
5
|
-
import { readGitStatusPorcelain,
|
|
6
|
+
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
6
7
|
export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
7
8
|
export const DAG_PI_WRITE_TOOLS = [
|
|
8
9
|
"read",
|
|
@@ -240,12 +241,15 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
240
241
|
};
|
|
241
242
|
}
|
|
242
243
|
let beforeStatus;
|
|
244
|
+
let beforePathFingerprints;
|
|
243
245
|
if (isWriteTask) {
|
|
244
246
|
try {
|
|
245
247
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
248
|
+
beforePathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, snapshotGitStatusPorcelain(beforeStatus));
|
|
246
249
|
}
|
|
247
250
|
catch {
|
|
248
251
|
beforeStatus = undefined;
|
|
252
|
+
beforePathFingerprints = undefined;
|
|
249
253
|
}
|
|
250
254
|
}
|
|
251
255
|
const result = await piStepFn({
|
|
@@ -278,15 +282,19 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
278
282
|
}
|
|
279
283
|
let writeGuardOk = true;
|
|
280
284
|
let writeGuardViolations = [];
|
|
285
|
+
let changeManifestAfterStatus;
|
|
286
|
+
let changeManifestChangedFiles;
|
|
281
287
|
if (beforeStatus !== undefined) {
|
|
282
288
|
try {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
const afterStatus = await readGitStatusPorcelain(input.cwd);
|
|
290
|
+
const afterSnapshot = snapshotGitStatusPorcelain(afterStatus);
|
|
291
|
+
const afterPathFingerprints = await snapshotGitStatusPathFingerprints(input.cwd, afterSnapshot);
|
|
292
|
+
const changedFiles = pathsChangedDuringRun(snapshotGitStatusPorcelain(beforeStatus), afterSnapshot, beforePathFingerprints, afterPathFingerprints);
|
|
293
|
+
changeManifestAfterStatus = afterStatus;
|
|
294
|
+
changeManifestChangedFiles = changedFiles;
|
|
295
|
+
const guard = validateShellWriteGuardFromDiff({
|
|
296
|
+
changedFiles,
|
|
297
|
+
task: input.task,
|
|
290
298
|
concurrentSiblingWriteSets: meta.concurrentSiblingWriteSets,
|
|
291
299
|
});
|
|
292
300
|
writeGuardOk = guard.ok;
|
|
@@ -300,6 +308,16 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
300
308
|
}
|
|
301
309
|
}
|
|
302
310
|
if (writeGuardOk) {
|
|
311
|
+
if (beforeStatus !== undefined && changeManifestChangedFiles !== undefined) {
|
|
312
|
+
await persistWriterChangeManifest({
|
|
313
|
+
runDir: meta.runDir,
|
|
314
|
+
nodeId: input.task.id,
|
|
315
|
+
writerNodeId: input.task.id,
|
|
316
|
+
changedFiles: changeManifestChangedFiles,
|
|
317
|
+
beforeStatus,
|
|
318
|
+
afterStatus: changeManifestAfterStatus ?? "",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
303
321
|
return mapped;
|
|
304
322
|
}
|
|
305
323
|
const stderrParts = [mapped.stderr];
|
|
@@ -335,11 +353,18 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
|
335
353
|
return assistantText;
|
|
336
354
|
const lines = assistantText.split(/\r?\n/);
|
|
337
355
|
let protocolIndex = -1;
|
|
356
|
+
let protocolNextLineIndex = -1;
|
|
338
357
|
let protocolLine = "";
|
|
339
358
|
for (const [index, line] of lines.entries()) {
|
|
340
|
-
const
|
|
341
|
-
|
|
359
|
+
const nextLine = lines[index + 1];
|
|
360
|
+
const normalized = normalizeProtocolLine(line, firstProtocolLine, nextLine);
|
|
361
|
+
if (normalized !== undefined) {
|
|
342
362
|
protocolIndex = index;
|
|
363
|
+
if (normalizeProtocolLine(line, firstProtocolLine) === undefined &&
|
|
364
|
+
nextLine !== undefined &&
|
|
365
|
+
normalizedProtocolValue(nextLine) !== "") {
|
|
366
|
+
protocolNextLineIndex = index + 1;
|
|
367
|
+
}
|
|
343
368
|
protocolLine = normalized;
|
|
344
369
|
break;
|
|
345
370
|
}
|
|
@@ -347,10 +372,14 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
|
347
372
|
if (protocolIndex < 0)
|
|
348
373
|
return assistantText;
|
|
349
374
|
if (protocolIndex === 0) {
|
|
350
|
-
|
|
375
|
+
const remaining = lines.slice(protocolNextLineIndex === 1 ? 2 : 1);
|
|
376
|
+
return [protocolLine, ...remaining].join("\n");
|
|
351
377
|
}
|
|
352
378
|
const before = lines.slice(0, protocolIndex);
|
|
353
379
|
const after = lines.slice(protocolIndex + 1);
|
|
380
|
+
if (protocolNextLineIndex === protocolIndex + 1) {
|
|
381
|
+
after.shift();
|
|
382
|
+
}
|
|
354
383
|
while (before.at(-1)?.trim() === "" &&
|
|
355
384
|
after.at(0)?.trim() === "") {
|
|
356
385
|
after.shift();
|
|
@@ -364,8 +393,81 @@ function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
|
364
393
|
? `${protocolLine}\n\n${bodyLines.join("\n")}`
|
|
365
394
|
: protocolLine;
|
|
366
395
|
}
|
|
367
|
-
function normalizeProtocolLine(line) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
396
|
+
function normalizeProtocolLine(line, firstProtocolLine, nextLine) {
|
|
397
|
+
const normalizedLine = stripMarkdownLineDecorations(line);
|
|
398
|
+
if (!firstProtocolLine)
|
|
399
|
+
return normalizedLine;
|
|
400
|
+
const label = firstProtocolLine.replace(/[::]\s*$/, "").trim();
|
|
401
|
+
const labelPattern = label
|
|
402
|
+
.split(/[_\-\s]+/)
|
|
403
|
+
.filter(Boolean)
|
|
404
|
+
.map(escapeRegExp)
|
|
405
|
+
.join("[_\\-\\s]+");
|
|
406
|
+
if (!labelPattern)
|
|
407
|
+
return undefined;
|
|
408
|
+
const direct = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]\\s*(.+)$`, "i"));
|
|
409
|
+
if (direct) {
|
|
410
|
+
const value = normalizedProtocolValue(direct[1] ?? "");
|
|
411
|
+
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
412
|
+
}
|
|
413
|
+
const splitValue = normalizedLine.match(new RegExp(`^${labelPattern}\\s*[::]?\\s*$`, "i"));
|
|
414
|
+
if (splitValue && nextLine !== undefined) {
|
|
415
|
+
const value = normalizedProtocolValue(nextLine);
|
|
416
|
+
return value ? `${firstProtocolLine} ${value}` : undefined;
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
function normalizedProtocolValue(value) {
|
|
421
|
+
let normalized = stripMarkdownLineDecorations(value);
|
|
422
|
+
normalized = normalized.replace(/^[::]\s*/, "").trim();
|
|
423
|
+
normalized = normalized.replace(/[。.!;;,,]+$/u, "").trim();
|
|
424
|
+
normalized = stripMarkdownLineDecorations(normalized);
|
|
425
|
+
return normalized;
|
|
426
|
+
}
|
|
427
|
+
function stripMarkdownLineDecorations(line) {
|
|
428
|
+
let trimmed = line.trim();
|
|
429
|
+
trimmed = trimmed.replace(/^(?:>\s*)+/, "");
|
|
430
|
+
trimmed = trimmed.replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, "");
|
|
431
|
+
const emphasized = trimmed.match(/^(\*{1,3}|_{1,3}|`{1,3})\s*(.*?)\s*\1$/);
|
|
432
|
+
trimmed = (emphasized?.[2] ?? trimmed).trim();
|
|
433
|
+
return trimmed;
|
|
434
|
+
}
|
|
435
|
+
function escapeRegExp(value) {
|
|
436
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Validate the writer's observed diff against its declared write boundary.
|
|
440
|
+
* Inlined mirror of runPostRunWriteGuard that reuses the already-computed diff
|
|
441
|
+
* so the change manifest and the guard share one source of truth.
|
|
442
|
+
*/
|
|
443
|
+
function validateShellWriteGuardFromDiff(input) {
|
|
444
|
+
return validateShellWriteGuard({
|
|
445
|
+
changedPaths: input.changedFiles,
|
|
446
|
+
writePolicy: input.task.writePolicy,
|
|
447
|
+
writeSet: input.task.writeSet,
|
|
448
|
+
allowedPaths: input.task.allowedPaths,
|
|
449
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
450
|
+
concurrentSiblingWriteSets: input.concurrentSiblingWriteSets,
|
|
451
|
+
allowSiblingWriteSetAttribution: input.task.writePolicy === "exclusive" &&
|
|
452
|
+
(input.concurrentSiblingWriteSets?.length ?? 0) > 0,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Persist the writer's actual changeset as a run-owned artifact so the
|
|
457
|
+
* project governance context resolver can attribute exactly this run's
|
|
458
|
+
* writes (not final `git status`, which may include pre-existing changes).
|
|
459
|
+
*/
|
|
460
|
+
async function persistWriterChangeManifest(input) {
|
|
461
|
+
const manifest = {
|
|
462
|
+
schemaVersion: 1,
|
|
463
|
+
writerNodeId: input.writerNodeId,
|
|
464
|
+
changedFiles: input.changedFiles,
|
|
465
|
+
beforeStatusSha256: createHash("sha256")
|
|
466
|
+
.update(input.beforeStatus)
|
|
467
|
+
.digest("hex"),
|
|
468
|
+
afterStatusSha256: createHash("sha256")
|
|
469
|
+
.update(input.afterStatus)
|
|
470
|
+
.digest("hex"),
|
|
471
|
+
};
|
|
472
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, "change-manifest.json", manifest);
|
|
371
473
|
}
|
|
@@ -50,6 +50,10 @@ export function extractSubagentStats(stdout) {
|
|
|
50
50
|
export const DEFAULT_TIMEOUT_MS = 1_800_000;
|
|
51
51
|
export const MAX_ALLOWED_TIMEOUT_MS = 1_800_000;
|
|
52
52
|
const PI_BACKEND_ENV = 'CODE_AGENT_PI_BACKEND';
|
|
53
|
+
const OUTPUT_PREVIEW_HEAD_CHARS = 64_000;
|
|
54
|
+
const OUTPUT_PREVIEW_TAIL_CHARS = 64_000;
|
|
55
|
+
const MAX_STREAM_JSONL_LINE_CHARS = 16_000_000;
|
|
56
|
+
const MAX_ASSISTANT_TEXT_CHARS = 16_000_000;
|
|
53
57
|
const PI_CLI_PACKAGE_NAMES = [
|
|
54
58
|
'@earendil-works/pi-coding-agent',
|
|
55
59
|
'@mariozechner/pi-coding-agent',
|
|
@@ -186,6 +190,10 @@ function resolvePiCliPath() {
|
|
|
186
190
|
path.join(nvmBase, nodeVersion, 'lib', 'node_modules'),
|
|
187
191
|
].flatMap((nodeModulesRoot) => PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(nodeModulesRoot, packageName)));
|
|
188
192
|
const otherCandidates = [
|
|
193
|
+
// Prefer the project-local Pi dependency. `npm run` adds node_modules/.bin
|
|
194
|
+
// to PATH, but direct invocations from the DAG runner may not, so resolve
|
|
195
|
+
// the package from the current workspace explicitly.
|
|
196
|
+
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot(path.resolve(process.cwd(), 'node_modules'), packageName)),
|
|
189
197
|
// Global npm installation (non-nvm)
|
|
190
198
|
...PI_CLI_PACKAGE_NAMES.map((packageName) => piCliPathFromPackageRoot('/usr/local/lib/node_modules', packageName)),
|
|
191
199
|
// Bun installation
|
|
@@ -327,8 +335,167 @@ function extractAssistantTextFromMessage(message) {
|
|
|
327
335
|
function isRecord(value) {
|
|
328
336
|
return typeof value === 'object' && value !== null;
|
|
329
337
|
}
|
|
338
|
+
export class BoundedTextPreview {
|
|
339
|
+
label;
|
|
340
|
+
headMax;
|
|
341
|
+
tailMax;
|
|
342
|
+
parts = [];
|
|
343
|
+
head = '';
|
|
344
|
+
tail = '';
|
|
345
|
+
bufferedChars = 0;
|
|
346
|
+
totalChars = 0;
|
|
347
|
+
truncated = false;
|
|
348
|
+
constructor(label, headMax = OUTPUT_PREVIEW_HEAD_CHARS, tailMax = OUTPUT_PREVIEW_TAIL_CHARS) {
|
|
349
|
+
this.label = label;
|
|
350
|
+
this.headMax = headMax;
|
|
351
|
+
this.tailMax = tailMax;
|
|
352
|
+
}
|
|
353
|
+
append(chunk) {
|
|
354
|
+
if (!chunk)
|
|
355
|
+
return;
|
|
356
|
+
this.totalChars += chunk.length;
|
|
357
|
+
if (!this.truncated) {
|
|
358
|
+
if (this.bufferedChars + chunk.length <= this.headMax + this.tailMax) {
|
|
359
|
+
this.parts.push(chunk);
|
|
360
|
+
this.bufferedChars += chunk.length;
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const combined = this.parts.join('') + chunk;
|
|
364
|
+
this.head = combined.slice(0, this.headMax);
|
|
365
|
+
this.tail = combined.slice(-this.tailMax);
|
|
366
|
+
this.parts = [];
|
|
367
|
+
this.bufferedChars = 0;
|
|
368
|
+
this.truncated = true;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.tail = (this.tail + chunk).slice(-this.tailMax);
|
|
372
|
+
}
|
|
373
|
+
text() {
|
|
374
|
+
if (!this.truncated)
|
|
375
|
+
return this.parts.join('');
|
|
376
|
+
const omitted = Math.max(0, this.totalChars - this.head.length - this.tail.length);
|
|
377
|
+
return [
|
|
378
|
+
this.head,
|
|
379
|
+
`[${this.label} truncated: omitted ${omitted} chars; kept first ${this.head.length} and last ${this.tail.length}]`,
|
|
380
|
+
this.tail,
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export class PiJsonlStreamCollector {
|
|
385
|
+
lineBuffer = '';
|
|
386
|
+
discardUntilNewline = false;
|
|
387
|
+
assistantText = '';
|
|
388
|
+
parsedEvents = 0;
|
|
389
|
+
tokensUsed = 0;
|
|
390
|
+
outputTooLarge = false;
|
|
391
|
+
subagentTotalCalls = 0;
|
|
392
|
+
subagentFailedCalls = 0;
|
|
393
|
+
subagentModes = new Set();
|
|
394
|
+
subagentAgents = new Set();
|
|
395
|
+
append(chunk) {
|
|
396
|
+
if (!chunk)
|
|
397
|
+
return;
|
|
398
|
+
const lines = chunk.split(/\r?\n/);
|
|
399
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
400
|
+
const part = lines[index] ?? '';
|
|
401
|
+
const isLast = index === lines.length - 1;
|
|
402
|
+
if (this.discardUntilNewline) {
|
|
403
|
+
if (!isLast)
|
|
404
|
+
this.discardUntilNewline = false;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (this.lineBuffer.length + part.length > MAX_STREAM_JSONL_LINE_CHARS) {
|
|
408
|
+
this.outputTooLarge = true;
|
|
409
|
+
this.lineBuffer = '';
|
|
410
|
+
if (isLast)
|
|
411
|
+
this.discardUntilNewline = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
this.lineBuffer += part;
|
|
415
|
+
if (!isLast) {
|
|
416
|
+
this.consumeLine(this.lineBuffer);
|
|
417
|
+
this.lineBuffer = '';
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
finish() {
|
|
422
|
+
if (!this.discardUntilNewline && this.lineBuffer.trim()) {
|
|
423
|
+
this.consumeLine(this.lineBuffer);
|
|
424
|
+
}
|
|
425
|
+
this.lineBuffer = '';
|
|
426
|
+
return {
|
|
427
|
+
assistantText: this.assistantText.trim(),
|
|
428
|
+
outputTooLarge: this.outputTooLarge,
|
|
429
|
+
parsedEvents: this.parsedEvents,
|
|
430
|
+
subagentStats: this.formatSubagentStats(),
|
|
431
|
+
tokensUsed: this.tokensUsed,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
consumeLine(line) {
|
|
435
|
+
const trimmed = line.trim();
|
|
436
|
+
if (!trimmed)
|
|
437
|
+
return;
|
|
438
|
+
try {
|
|
439
|
+
const event = JSON.parse(trimmed);
|
|
440
|
+
this.parsedEvents += 1;
|
|
441
|
+
this.consumeEvent(event);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// tolerate non-json noise
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
consumeEvent(event) {
|
|
448
|
+
const candidate = extractAssistantTextFromEvent(event);
|
|
449
|
+
if (candidate) {
|
|
450
|
+
if (candidate.length > MAX_ASSISTANT_TEXT_CHARS) {
|
|
451
|
+
this.outputTooLarge = true;
|
|
452
|
+
this.assistantText = candidate.slice(0, MAX_ASSISTANT_TEXT_CHARS);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
this.assistantText = candidate;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const usage = extractUsageFromEvent(event);
|
|
459
|
+
if (usage !== null)
|
|
460
|
+
this.tokensUsed += usage;
|
|
461
|
+
if (event.type === 'tool_execution_start' && event.toolName === 'subagent') {
|
|
462
|
+
this.subagentTotalCalls += 1;
|
|
463
|
+
if (isRecord(event.input)) {
|
|
464
|
+
if (event.input.agent)
|
|
465
|
+
this.subagentAgents.add(String(event.input.agent));
|
|
466
|
+
if (event.input.tasks)
|
|
467
|
+
this.subagentModes.add('parallel');
|
|
468
|
+
if (event.input.chain)
|
|
469
|
+
this.subagentModes.add('chain');
|
|
470
|
+
if (event.input.agent && !event.input.tasks && !event.input.chain) {
|
|
471
|
+
this.subagentModes.add('single');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (event.type === 'tool_execution_end' && event.toolName === 'subagent') {
|
|
476
|
+
if (event.isError === true)
|
|
477
|
+
this.subagentFailedCalls += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
formatSubagentStats() {
|
|
481
|
+
if (this.subagentTotalCalls === 0)
|
|
482
|
+
return 'calls=0';
|
|
483
|
+
const parts = [`calls=${this.subagentTotalCalls}`];
|
|
484
|
+
if (this.subagentFailedCalls > 0)
|
|
485
|
+
parts.push(`${this.subagentFailedCalls} failed`);
|
|
486
|
+
if (this.subagentModes.size > 0)
|
|
487
|
+
parts.push(Array.from(this.subagentModes).sort().join('+'));
|
|
488
|
+
if (this.subagentAgents.size > 0) {
|
|
489
|
+
parts.push(`agents=[${Array.from(this.subagentAgents).sort().join(',')}]`);
|
|
490
|
+
}
|
|
491
|
+
return parts.join(' | ');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
export function createPiJsonlStreamCollector() {
|
|
495
|
+
return new PiJsonlStreamCollector();
|
|
496
|
+
}
|
|
330
497
|
function withSubagentStats(result) {
|
|
331
|
-
return { ...result, subagentStats: extractSubagentStats(result.stdout) };
|
|
498
|
+
return { ...result, subagentStats: result.subagentStats ?? extractSubagentStats(result.stdout) };
|
|
332
499
|
}
|
|
333
500
|
function withCliBackend(result) {
|
|
334
501
|
return { ...result, backend: 'cli' };
|
|
@@ -354,7 +521,10 @@ async function runCliOrchestration(options, meta) {
|
|
|
354
521
|
const fallbackResult = await executeSingleCliAttempt(options, primary.fallback);
|
|
355
522
|
attemptedModels.push(fallbackResult.modelDisplay);
|
|
356
523
|
const combinedStderr = [primaryResult.stderr, fallbackResult.stderr].filter(Boolean).join('\n--- fallback ---\n');
|
|
357
|
-
const combinedStdout = [primaryResult.stdout, fallbackResult.stdout].filter(Boolean).join('\n');
|
|
524
|
+
const combinedStdout = [primaryResult.stdout, fallbackResult.stdout].filter(Boolean).join('\n--- fallback ---\n');
|
|
525
|
+
const combinedStdoutTruncated = primaryResult.stdoutTruncated || fallbackResult.stdoutTruncated;
|
|
526
|
+
const combinedStderrTruncated = primaryResult.stderrTruncated || fallbackResult.stderrTruncated;
|
|
527
|
+
const combinedOutputTooLarge = primaryResult.outputTooLarge || fallbackResult.outputTooLarge;
|
|
358
528
|
let fallbackIssues = [];
|
|
359
529
|
if (fallbackResult.ok && options.validateOutput) {
|
|
360
530
|
fallbackIssues = options.validateOutput(fallbackResult.assistantText);
|
|
@@ -367,6 +537,9 @@ async function runCliOrchestration(options, meta) {
|
|
|
367
537
|
sdkAttempted: meta?.sdkAttempted,
|
|
368
538
|
stderr: combinedStderr,
|
|
369
539
|
stdout: combinedStdout,
|
|
540
|
+
stdoutTruncated: combinedStdoutTruncated,
|
|
541
|
+
stderrTruncated: combinedStderrTruncated,
|
|
542
|
+
outputTooLarge: combinedOutputTooLarge,
|
|
370
543
|
}));
|
|
371
544
|
}
|
|
372
545
|
if (fallbackResult.ok && fallbackIssues.length > 0) {
|
|
@@ -379,6 +552,9 @@ async function runCliOrchestration(options, meta) {
|
|
|
379
552
|
sdkAttempted: meta?.sdkAttempted,
|
|
380
553
|
stderr: combinedStderr + `\n[invalid-output] ${fallbackIssues.join('; ')}`,
|
|
381
554
|
stdout: combinedStdout,
|
|
555
|
+
stdoutTruncated: combinedStdoutTruncated,
|
|
556
|
+
stderrTruncated: combinedStderrTruncated,
|
|
557
|
+
outputTooLarge: combinedOutputTooLarge,
|
|
382
558
|
}));
|
|
383
559
|
}
|
|
384
560
|
return withSubagentStats(withCliBackend({
|
|
@@ -388,6 +564,9 @@ async function runCliOrchestration(options, meta) {
|
|
|
388
564
|
sdkAttempted: meta?.sdkAttempted,
|
|
389
565
|
stderr: combinedStderr,
|
|
390
566
|
stdout: combinedStdout,
|
|
567
|
+
stdoutTruncated: combinedStdoutTruncated,
|
|
568
|
+
stderrTruncated: combinedStderrTruncated,
|
|
569
|
+
outputTooLarge: combinedOutputTooLarge,
|
|
391
570
|
}));
|
|
392
571
|
}
|
|
393
572
|
if (primaryInvalidOutput) {
|
|
@@ -516,12 +695,18 @@ async function executeSingleCliAttempt(options, modelConfig) {
|
|
|
516
695
|
},
|
|
517
696
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
518
697
|
});
|
|
519
|
-
|
|
520
|
-
|
|
698
|
+
const stdoutPreview = new BoundedTextPreview('stdout');
|
|
699
|
+
const stderrPreview = new BoundedTextPreview('stderr', 16_000, 16_000);
|
|
700
|
+
const stdoutCollector = new PiJsonlStreamCollector();
|
|
521
701
|
child.stdout.setEncoding('utf-8');
|
|
522
702
|
child.stderr.setEncoding('utf-8');
|
|
523
|
-
child.stdout.on('data', (chunk) => {
|
|
524
|
-
|
|
703
|
+
child.stdout.on('data', (chunk) => {
|
|
704
|
+
stdoutPreview.append(chunk);
|
|
705
|
+
stdoutCollector.append(chunk);
|
|
706
|
+
});
|
|
707
|
+
child.stderr.on('data', (chunk) => {
|
|
708
|
+
stderrPreview.append(chunk);
|
|
709
|
+
});
|
|
525
710
|
child.on('error', (err) => {
|
|
526
711
|
if (timeoutHandle)
|
|
527
712
|
clearTimeout(timeoutHandle);
|
|
@@ -535,28 +720,34 @@ async function executeSingleCliAttempt(options, modelConfig) {
|
|
|
535
720
|
if (sigkillHandle)
|
|
536
721
|
clearTimeout(sigkillHandle);
|
|
537
722
|
const durationMs = Date.now() - startedAt;
|
|
538
|
-
const
|
|
539
|
-
const
|
|
723
|
+
const stdout = stdoutPreview.text();
|
|
724
|
+
const stderr = stderrPreview.text();
|
|
725
|
+
const collected = stdoutCollector.finish();
|
|
540
726
|
const failureCategory = classifyPiFailure({
|
|
541
|
-
assistantText,
|
|
727
|
+
assistantText: collected.assistantText,
|
|
542
728
|
exitCode: code,
|
|
543
729
|
stderr,
|
|
544
730
|
stdout,
|
|
545
731
|
timedOut,
|
|
732
|
+
outputTooLarge: collected.outputTooLarge,
|
|
546
733
|
});
|
|
547
734
|
resolve({
|
|
548
|
-
assistantText,
|
|
735
|
+
assistantText: collected.assistantText,
|
|
549
736
|
command: ['pi', ...piArgs],
|
|
550
737
|
durationMs,
|
|
551
738
|
exitCode: code,
|
|
552
739
|
failureCategory,
|
|
553
740
|
modelDisplay,
|
|
554
|
-
ok: code === 0 && assistantText.length > 0,
|
|
555
|
-
parsedEvents,
|
|
741
|
+
ok: code === 0 && collected.assistantText.length > 0 && !collected.outputTooLarge,
|
|
742
|
+
parsedEvents: collected.parsedEvents,
|
|
556
743
|
stderr,
|
|
557
744
|
stdout,
|
|
745
|
+
stdoutTruncated: stdoutPreview.truncated,
|
|
746
|
+
stderrTruncated: stderrPreview.truncated,
|
|
747
|
+
outputTooLarge: collected.outputTooLarge,
|
|
558
748
|
timedOut,
|
|
559
|
-
tokensUsed,
|
|
749
|
+
tokensUsed: collected.tokensUsed,
|
|
750
|
+
subagentStats: collected.subagentStats,
|
|
560
751
|
});
|
|
561
752
|
});
|
|
562
753
|
if (timeoutMs > 0) {
|
|
@@ -585,6 +776,8 @@ export function shouldRetryWithFallback(stderr, stdout) {
|
|
|
585
776
|
export function classifyPiFailure(input) {
|
|
586
777
|
if (input.timedOut)
|
|
587
778
|
return 'timeout';
|
|
779
|
+
if (input.outputTooLarge)
|
|
780
|
+
return 'output-too-large';
|
|
588
781
|
if (input.exitCode === 0 && input.assistantText.trim())
|
|
589
782
|
return 'success';
|
|
590
783
|
const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { classifyPiFailure, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from './pi-executor.js';
|
|
3
|
+
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from './pi-executor.js';
|
|
4
4
|
import { serializeSessionEvent } from './pi-event-serializer.js';
|
|
5
5
|
let sdkSessionFactoryOverride;
|
|
6
6
|
let sdkImportOverrideForTests;
|
|
@@ -283,7 +283,8 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
283
283
|
const startedAt = Date.now();
|
|
284
284
|
let timedOut = false;
|
|
285
285
|
let stderr = '';
|
|
286
|
-
const
|
|
286
|
+
const stdoutPreview = new BoundedTextPreview('stdout');
|
|
287
|
+
const stdoutCollector = createPiJsonlStreamCollector();
|
|
287
288
|
const usageSamples = [];
|
|
288
289
|
let session;
|
|
289
290
|
let timeoutHandle;
|
|
@@ -307,7 +308,8 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
307
308
|
if (!shouldPersistSessionEvent(event))
|
|
308
309
|
return;
|
|
309
310
|
const line = serializeSessionEvent(event);
|
|
310
|
-
|
|
311
|
+
stdoutPreview.append(`${line}\n`);
|
|
312
|
+
stdoutCollector.append(`${line}\n`);
|
|
311
313
|
sessionEventAppender?.append(line, event);
|
|
312
314
|
});
|
|
313
315
|
const filePrefix = options.attachedFiles.map((file) => `@${file}`).join(' ');
|
|
@@ -363,13 +365,23 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
}
|
|
366
|
-
const stdout =
|
|
368
|
+
const stdout = stdoutPreview.text();
|
|
367
369
|
const durationMs = Date.now() - startedAt;
|
|
368
|
-
const
|
|
370
|
+
const collected = stdoutCollector.finish();
|
|
371
|
+
const fallbackParsed = collected.parsedEvents > 0
|
|
372
|
+
? undefined
|
|
373
|
+
: extractAssistantTextFromPiJson(stdout);
|
|
374
|
+
const assistantText = collected.parsedEvents > 0
|
|
375
|
+
? collected.assistantText
|
|
376
|
+
: fallbackParsed?.assistantText ?? '';
|
|
377
|
+
const parsedEvents = collected.parsedEvents > 0
|
|
378
|
+
? collected.parsedEvents
|
|
379
|
+
: fallbackParsed?.parsedEvents ?? 0;
|
|
369
380
|
const tokensUsed = aggregateSdkTokenUsage(usageSamples);
|
|
370
381
|
const failureCategory = classifyPiFailure({
|
|
371
382
|
assistantText,
|
|
372
383
|
exitCode: timedOut ? 1 : stderr ? 1 : 0,
|
|
384
|
+
outputTooLarge: collected.outputTooLarge,
|
|
373
385
|
stderr,
|
|
374
386
|
stdout,
|
|
375
387
|
timedOut,
|
|
@@ -382,11 +394,14 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
382
394
|
exitCode: timedOut || stderr ? 1 : 0,
|
|
383
395
|
failureCategory,
|
|
384
396
|
modelDisplay,
|
|
385
|
-
ok: !timedOut && !stderr && assistantText.length > 0,
|
|
397
|
+
ok: !timedOut && !stderr && assistantText.length > 0 && !collected.outputTooLarge,
|
|
386
398
|
parsedEvents,
|
|
387
399
|
stderr,
|
|
388
400
|
stdout,
|
|
401
|
+
stdoutTruncated: stdoutPreview.truncated,
|
|
402
|
+
outputTooLarge: collected.outputTooLarge,
|
|
389
403
|
timedOut,
|
|
390
404
|
tokensUsed,
|
|
405
|
+
subagentStats: collected.subagentStats,
|
|
391
406
|
};
|
|
392
407
|
}
|