pi-crew 0.11.6 → 0.11.8
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/.githooks/pre-commit +22 -0
- package/.githooks/pre-push +56 -0
- package/CHANGELOG.md +216 -1
- package/dist/index.mjs +531 -508
- package/docs/publishing.md +29 -0
- package/index.ts +3 -3
- package/package.json +9 -6
- package/scripts/install-hooks.mjs +32 -0
- package/skills/real-test-pi-crew/SKILL.md +31 -18
- package/src/config/config-validation.ts +35 -0
- package/src/config/env-vars.ts +38 -0
- package/src/config/types.ts +71 -0
- package/src/extension/registration/foreground-run-controller.ts +19 -10
- package/src/extension/registration/lifecycle-handlers.ts +6 -491
- package/src/extension/registration/lifecycle.ts +7 -90
- package/src/extension/registration/render-loop.ts +502 -0
- package/src/extension/team-tool/doctor.ts +6 -0
- package/src/prompt/prompt-runtime.ts +33 -4
- package/src/runtime/child-pi/child-pi-spawn.ts +14 -0
- package/src/runtime/child-pi/child-pi.ts +130 -6
- package/src/runtime/child-pi/pi-version.ts +187 -0
- package/src/runtime/child-pi/session-recovery.ts +334 -0
- package/src/runtime/classifier/classifier-service.ts +298 -0
- package/src/runtime/dispatch-batch.ts +4 -0
- package/src/runtime/live-session/live-session-runtime.ts +31 -0
- package/src/runtime/model/model-scope.ts +141 -21
- package/src/runtime/model/pi-args.ts +166 -6
- package/src/runtime/peer-dep.ts +24 -0
- package/src/runtime/process/zombie-scanner.ts +168 -7
- package/src/runtime/rpc/README.md +139 -0
- package/src/runtime/rpc/frame-client.ts +328 -0
- package/src/runtime/rpc/rpc-worker.ts +271 -0
- package/src/runtime/rpc/ui-request-policy.ts +112 -0
- package/src/runtime/run-worker.ts +91 -3
- package/src/runtime/scheduling/run-coalesced-task-group.ts +3 -0
- package/src/runtime/skill-instructions.ts +66 -1
- package/src/runtime/surface/resolve-surface.ts +34 -19
- package/src/runtime/surface/surface-spawn.ts +8 -1
- package/src/runtime/task-output-context.ts +191 -32
- package/src/runtime/task-packet.ts +3 -0
- package/src/runtime/task-runner/child-executor.ts +172 -2
- package/src/runtime/task-runner/post-execution.ts +5 -3
- package/src/runtime/task-runner/pre-execution.ts +32 -1
- package/src/runtime/task-runner/prompt-builder.ts +10 -2
- package/src/runtime/task-runner.ts +6 -0
- package/src/schema/config-schema.ts +37 -0
- package/src/schema/team-tool-schema.ts +14 -2
- package/src/state/contracts.ts +5 -0
- package/src/ui/format-helpers.ts +11 -0
- package/src/ui/overlays/mailbox-compose-overlay.ts +15 -1
- package/src/ui/pi-ui-compat.ts +18 -9
- package/src/ui/rail.ts +14 -1
- package/src/ui/settings-overlay.ts +11 -2
- package/src/ui/widget/index.ts +0 -1
- package/src/ui/widget/task-list.ts +10 -3
- package/src/ui/widget/widget-renderer.ts +53 -4
- package/src/utils/relative-time.ts +11 -3
- package/src/utils/visual.ts +32 -2
|
@@ -41,6 +41,12 @@ export interface DependencyContextEntry {
|
|
|
41
41
|
* the workflow (earlier deps = higher relevance because they were
|
|
42
42
|
* explicitly listed first). Defaults to 0.5 (neutral) when unknown. */
|
|
43
43
|
relevance?: number;
|
|
44
|
+
/** Handoff-budget marker: set by {@link applyHandoffBudget} when this
|
|
45
|
+
* entry was downgraded to the compact form (taskId/role/status + summary
|
|
46
|
+
* head ≤240 chars + artifact pointer). The render path emits the compact
|
|
47
|
+
* block for marked entries; `writeTaskInputsArtifact` still serializes the
|
|
48
|
+
* ORIGINAL (untrimmed) context — the trim is render-only, never persisted. */
|
|
49
|
+
budgetTrimmed?: boolean;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface DependencyOutputContext {
|
|
@@ -58,6 +64,12 @@ export interface DependencyOutputContext {
|
|
|
58
64
|
content: string;
|
|
59
65
|
fullOutputPath?: string;
|
|
60
66
|
}>;
|
|
67
|
+
/** Run id (from the team manifest). Populated by
|
|
68
|
+
* `collectDependencyOutputContext` so the handoff-budget trim can build
|
|
69
|
+
* artifact pointers (`artifacts/<runId>/results/<taskId>.txt`). Optional
|
|
70
|
+
* for backward-compat with hand-built fixtures; without it trimmed deps
|
|
71
|
+
* simply omit the pointer line. */
|
|
72
|
+
runId?: string;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
function containedExists(filePath: string, baseDir?: string): boolean {
|
|
@@ -523,7 +535,7 @@ export function collectDependencyOutputContext(
|
|
|
523
535
|
// outputs with compact digest notices before injecting into the worker
|
|
524
536
|
// prompt. OPT-IN: default config protects recent results.
|
|
525
537
|
const sharedReads = pruneSharedReads(rawSharedReads, trimmedDependencies, manifest.artifactsRoot);
|
|
526
|
-
return { dependencies: trimmedDependencies, sharedReads };
|
|
538
|
+
return { dependencies: trimmedDependencies, sharedReads, runId: manifest.runId };
|
|
527
539
|
}
|
|
528
540
|
|
|
529
541
|
// ADR-5 §trust-fence: dependency output is DATA, never instructions. Mirror the
|
|
@@ -536,46 +548,193 @@ function sanitizeFencedBody(body: string): string {
|
|
|
536
548
|
return body.replace(DEPENDENCY_CONTROL_CHAR_PATTERN, "").replace(/<\/dependency-context/g, "</dependency-context");
|
|
537
549
|
}
|
|
538
550
|
|
|
539
|
-
|
|
551
|
+
// ── Handoff budget (est-token cap on this layer) ─────────────────────────────
|
|
552
|
+
|
|
553
|
+
/** Default est-token budget for the dynamic.dependencyContext layer. */
|
|
554
|
+
export const DEFAULT_HANDOFF_BUDGET_TOKENS = 1800;
|
|
555
|
+
/** Budget ceiling: a resolved budget above this is treated as the off-switch
|
|
556
|
+
* (same semantics as ≤0 — clearly not a real handoff cap, so render
|
|
557
|
+
* untrimmed instead of guessing). */
|
|
558
|
+
export const HANDOFF_BUDGET_CEILING_TOKENS = 1_000_000;
|
|
559
|
+
/** Summary head kept per dependency when the budget trims it. */
|
|
560
|
+
export const HANDOFF_SUMMARY_HEAD_CHARS = 240;
|
|
561
|
+
|
|
562
|
+
/** Strict integer env syntax — "12abc" must NOT parse as 12. */
|
|
563
|
+
const HANDOFF_BUDGET_ENV_RE = /^[+-]?\d+$/;
|
|
564
|
+
|
|
565
|
+
/** Est tokens = chars/4 — the same heuristic as `estimateTokens` in
|
|
566
|
+
* src/runtime/task-runner/prompt-builder.ts (SR-02 breakdown). Duplicated
|
|
567
|
+
* here instead of imported: importing prompt-builder would pull its
|
|
568
|
+
* retrieval/workspace-tree graph into every task-output-context consumer
|
|
569
|
+
* (post-execution, aggregate outputs) for a one-line heuristic. Keep the
|
|
570
|
+
* two in sync if either changes. */
|
|
571
|
+
function estimateHandoffTokens(chars: number): number {
|
|
572
|
+
return Math.round(chars / 4);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Budget active iff it is a positive finite number at or below the ceiling;
|
|
576
|
+
* ≤0 or >ceiling = off-switch (render untrimmed). */
|
|
577
|
+
function handoffBudgetActive(budgetTokens: number): boolean {
|
|
578
|
+
return Number.isFinite(budgetTokens) && budgetTokens > 0 && budgetTokens <= HANDOFF_BUDGET_CEILING_TOKENS;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Resolve the handoff budget: env PI_CREW_HANDOFF_BUDGET_TOKENS beats the
|
|
582
|
+
* configured value, which beats the default (1800). A non-numeric/invalid
|
|
583
|
+
* env string NEVER throws — it falls through to config/default. The resolved
|
|
584
|
+
* number may still be ≤0 or >1_000_000 (off-switch at the render site). */
|
|
585
|
+
export function resolveHandoffBudgetTokens(configured: number | undefined): number {
|
|
586
|
+
const raw = getCrewEnv("PI_CREW_HANDOFF_BUDGET_TOKENS");
|
|
587
|
+
if (raw !== undefined) {
|
|
588
|
+
const trimmed = raw.trim();
|
|
589
|
+
if (HANDOFF_BUDGET_ENV_RE.test(trimmed)) {
|
|
590
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
591
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return configured ?? DEFAULT_HANDOFF_BUDGET_TOKENS;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Full-form render lines for one dependency (pre-sanitize). */
|
|
598
|
+
function dependencyEntryLines(dep: DependencyContextEntry): string[] {
|
|
599
|
+
const lines: string[] = [
|
|
600
|
+
`## ${dep.taskId} (${dep.role})`,
|
|
601
|
+
`Status: ${dep.status}`,
|
|
602
|
+
dep.resultPath ? `Result artifact: ${dep.resultPath}` : "",
|
|
603
|
+
"",
|
|
604
|
+
dep.resultSummary?.trim() || "(no result output)",
|
|
605
|
+
"",
|
|
606
|
+
];
|
|
607
|
+
// P1-A dependency tee-recovery hint: when the dependency's result was
|
|
608
|
+
// materially truncated (>1.25× MAX_RESULT_INLINE_BYTES) the full RAW
|
|
609
|
+
// content was teed to fullOutputPath. Mirrors the sharedReads hint so the
|
|
610
|
+
// downstream worker can read the dropped middle instead of re-deriving.
|
|
611
|
+
if (dep.fullOutputPath) lines.push(`Full output (if you need the missing middle): ${dep.fullOutputPath}`, "");
|
|
612
|
+
if (dep.structuredResults) lines.push("Structured results:", JSON.stringify(dep.structuredResults, null, 2), "");
|
|
613
|
+
if (dep.artifactsProduced?.length) lines.push(`Artifacts produced: ${dep.artifactsProduced.join(", ")}`, "");
|
|
614
|
+
if (dep.usage)
|
|
615
|
+
lines.push(`Usage: ${dep.usage.inputTokens} input tokens, ${dep.usage.outputTokens} output tokens, ${dep.usage.durationMs}ms`, "");
|
|
616
|
+
return lines;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Compact handoff-budget form for one dependency (pre-sanitize): keeps the
|
|
620
|
+
* taskId/role/status lines + a ≤240-char summary head (with a truncation
|
|
621
|
+
* marker when the summary was longer) + a relative artifact pointer the
|
|
622
|
+
* worker can `read`. Drops structuredResults, usage, artifactsProduced and
|
|
623
|
+
* the absolute-path lines — all recoverable via the pointer. The pointer is
|
|
624
|
+
* built ONLY from manifest-derived runId/taskId (same trust level as the
|
|
625
|
+
* existing `## <taskId>` headers and the results/<taskId>.txt artifact
|
|
626
|
+
* naming); dep-controlled content never reaches it. */
|
|
627
|
+
function budgetTrimmedDependencyEntryLines(dep: DependencyContextEntry, runId: string | undefined): string[] {
|
|
628
|
+
const summary = dep.resultSummary?.trim() ?? "";
|
|
629
|
+
const head = summary.slice(0, HANDOFF_SUMMARY_HEAD_CHARS);
|
|
630
|
+
const body =
|
|
631
|
+
summary.length > HANDOFF_SUMMARY_HEAD_CHARS ? `${head}\n[trimmed, ${summary.length} chars total]` : summary || "(no result output)";
|
|
632
|
+
return [
|
|
633
|
+
`## ${dep.taskId} (${dep.role})`,
|
|
634
|
+
`Status: ${dep.status}`,
|
|
635
|
+
body,
|
|
636
|
+
...(runId ? [`full output: artifacts/${runId}/results/${dep.taskId}.txt`] : []),
|
|
637
|
+
"",
|
|
638
|
+
];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Full-form render lines for one shared read (pre-sanitize). */
|
|
642
|
+
function sharedReadLines(read: { name: string; path: string; content: string; fullOutputPath?: string }): string[] {
|
|
643
|
+
const lines = [`## shared/${read.name}`, `Path: ${read.path}`];
|
|
644
|
+
// P1-A tee-recovery hint: when the file was materially truncated
|
|
645
|
+
// (>2× threshold) the full content was teed to fullOutputPath so the
|
|
646
|
+
// worker can read the dropped middle if needed. The path is inside
|
|
647
|
+
// artifactsRoot/tee/ and goes through the normal permission gate.
|
|
648
|
+
if (read.fullOutputPath) lines.push(`Full output (if you need the missing middle): ${read.fullOutputPath}`);
|
|
649
|
+
lines.push("", read.content.trim(), "");
|
|
650
|
+
return lines;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Build the unsanitized body parts for a (possibly budget-trimmed) context.
|
|
654
|
+
* Shared by the renderer (sanitize path) and the budget estimator so the
|
|
655
|
+
* estimate can never drift from what is actually rendered. */
|
|
656
|
+
function buildDependencyOutputBodyParts(context: DependencyOutputContext): string[] {
|
|
540
657
|
const parts: string[] = [];
|
|
541
658
|
if (context.dependencies.length) {
|
|
542
659
|
parts.push("# Dependency Outputs", "");
|
|
543
660
|
for (const dep of context.dependencies) {
|
|
544
|
-
parts.push(
|
|
545
|
-
`## ${dep.taskId} (${dep.role})`,
|
|
546
|
-
`Status: ${dep.status}`,
|
|
547
|
-
dep.resultPath ? `Result artifact: ${dep.resultPath}` : "",
|
|
548
|
-
"",
|
|
549
|
-
dep.resultSummary?.trim() || "(no result output)",
|
|
550
|
-
"",
|
|
551
|
-
);
|
|
552
|
-
// P1-A dependency tee-recovery hint: when the dependency's result was
|
|
553
|
-
// materially truncated (>1.25× MAX_RESULT_INLINE_BYTES) the full RAW
|
|
554
|
-
// content was teed to fullOutputPath. Mirrors the sharedReads hint so the
|
|
555
|
-
// downstream worker can read the dropped middle instead of re-deriving.
|
|
556
|
-
if (dep.fullOutputPath) parts.push(`Full output (if you need the missing middle): ${dep.fullOutputPath}`, "");
|
|
557
|
-
if (dep.structuredResults) parts.push("Structured results:", JSON.stringify(dep.structuredResults, null, 2), "");
|
|
558
|
-
if (dep.artifactsProduced?.length) parts.push(`Artifacts produced: ${dep.artifactsProduced.join(", ")}`, "");
|
|
559
|
-
if (dep.usage)
|
|
560
|
-
parts.push(
|
|
561
|
-
`Usage: ${dep.usage.inputTokens} input tokens, ${dep.usage.outputTokens} output tokens, ${dep.usage.durationMs}ms`,
|
|
562
|
-
"",
|
|
563
|
-
);
|
|
661
|
+
parts.push(...(dep.budgetTrimmed ? budgetTrimmedDependencyEntryLines(dep, context.runId) : dependencyEntryLines(dep)));
|
|
564
662
|
}
|
|
565
663
|
}
|
|
566
664
|
if (context.sharedReads.length) {
|
|
567
665
|
parts.push("# Shared Run Context Reads", "");
|
|
568
|
-
for (const read of context.sharedReads)
|
|
569
|
-
parts.push(`## shared/${read.name}`, `Path: ${read.path}`);
|
|
570
|
-
// P1-A tee-recovery hint: when the file was materially truncated
|
|
571
|
-
// (>2× threshold) the full content was teed to fullOutputPath so the
|
|
572
|
-
// worker can read the dropped middle if needed. The path is inside
|
|
573
|
-
// artifactsRoot/tee/ and goes through the normal permission gate.
|
|
574
|
-
if (read.fullOutputPath) parts.push(`Full output (if you need the missing middle): ${read.fullOutputPath}`);
|
|
575
|
-
parts.push("", read.content.trim(), "");
|
|
576
|
-
}
|
|
666
|
+
for (const read of context.sharedReads) parts.push(...sharedReadLines(read));
|
|
577
667
|
}
|
|
578
|
-
return
|
|
668
|
+
return parts;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Measure a candidate context in est tokens (chars/4), mirroring the render
|
|
672
|
+
* byte-for-byte (same parts builder; `.trim()` matches the render join). */
|
|
673
|
+
function measureHandoffTokens(context: DependencyOutputContext): number {
|
|
674
|
+
return estimateHandoffTokens(buildDependencyOutputBodyParts(context).join("\n").trim().length);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Handoff budget: trim the dependency-output context to an est-token
|
|
679
|
+
* (chars/4) budget. PURE — no I/O, no env reads (callers pass the resolved
|
|
680
|
+
* budget from {@link resolveHandoffBudgetTokens}); returns the input
|
|
681
|
+
* reference unchanged when the budget is off (≤0 or >1_000_000) or the
|
|
682
|
+
* context already fits.
|
|
683
|
+
*
|
|
684
|
+
* Trim policy (declaration order): dependencies are probed from ALL-full
|
|
685
|
+
* downward — the largest prefix of dependencies that still fits keeps its
|
|
686
|
+
* FULL output; every dependency after the cutoff is downgraded to the
|
|
687
|
+
* compact form (taskId/role/status + ≤240-char summary head + truncation
|
|
688
|
+
* marker + `artifacts/<runId>/results/<taskId>.txt` pointer). Earlier deps
|
|
689
|
+
* therefore always keep ≥ as much as later ones. Long (>240 chars) sharedRead
|
|
690
|
+
* bodies are replaced by a trim marker (the Path pointer is already
|
|
691
|
+
* rendered); short shared reads pass through.
|
|
692
|
+
*
|
|
693
|
+
* When even the fully-trimmed form exceeds the budget (extreme dependency
|
|
694
|
+
* count), the fully-trimmed context is returned as the bounded best effort —
|
|
695
|
+
* every entry is then ≤ ~240 chars + pointer, so the layer stays small and
|
|
696
|
+
* deterministic.
|
|
697
|
+
*
|
|
698
|
+
* Composes AFTER the L4 byte trim (`enforceDependencyInlineBudget`, 96KB,
|
|
699
|
+
* priority-ordered path-only downgrade) which runs at collect time and is
|
|
700
|
+
* untouched by this function.
|
|
701
|
+
*/
|
|
702
|
+
export function applyHandoffBudget(context: DependencyOutputContext, budgetTokens: number, runId?: string): DependencyOutputContext {
|
|
703
|
+
if (!handoffBudgetActive(budgetTokens)) return context;
|
|
704
|
+
const effectiveRunId = runId ?? context.runId;
|
|
705
|
+
const base: DependencyOutputContext =
|
|
706
|
+
effectiveRunId !== undefined && context.runId !== effectiveRunId ? { ...context, runId: effectiveRunId } : context;
|
|
707
|
+
if (measureHandoffTokens(base) <= budgetTokens) return base;
|
|
708
|
+
const trimmedRead = (read: { name: string; path: string; content: string; fullOutputPath?: string }) => {
|
|
709
|
+
const content = read.content.trim();
|
|
710
|
+
return content.length > HANDOFF_SUMMARY_HEAD_CHARS
|
|
711
|
+
? { ...read, content: `[content trimmed, ${content.length} chars total — read Path above for the full file]` }
|
|
712
|
+
: read;
|
|
713
|
+
};
|
|
714
|
+
const candidateAt = (fullCount: number): DependencyOutputContext => ({
|
|
715
|
+
...base,
|
|
716
|
+
dependencies: base.dependencies.map((dep, index) => (index < fullCount ? dep : { ...dep, budgetTrimmed: true })),
|
|
717
|
+
sharedReads: base.sharedReads.map(trimmedRead),
|
|
718
|
+
});
|
|
719
|
+
for (let fullCount = base.dependencies.length; fullCount >= 0; fullCount--) {
|
|
720
|
+
const candidate = candidateAt(fullCount);
|
|
721
|
+
if (measureHandoffTokens(candidate) <= budgetTokens) return candidate;
|
|
722
|
+
}
|
|
723
|
+
return candidateAt(0);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export interface RenderDependencyOutputContextOptions {
|
|
727
|
+
/** Configured budget (runtime.handoffBudgetTokens). Env
|
|
728
|
+
* PI_CREW_HANDOFF_BUDGET_TOKENS beats this value; when neither is set the
|
|
729
|
+
* default is 1800. A resolved value ≤0 or >1_000_000 disables the trim
|
|
730
|
+
* (off-switch). */
|
|
731
|
+
budgetTokens?: number;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function renderDependencyOutputContext(context: DependencyOutputContext, opts: RenderDependencyOutputContextOptions = {}): string {
|
|
735
|
+
const budgetTokens = resolveHandoffBudgetTokens(opts.budgetTokens);
|
|
736
|
+
const effective = applyHandoffBudget(context, budgetTokens);
|
|
737
|
+
return sanitizeFencedBody(buildDependencyOutputBodyParts(effective).join("\n").trim());
|
|
579
738
|
}
|
|
580
739
|
|
|
581
740
|
export function writeTaskSharedOutput(manifest: TeamRunManifest, step: WorkflowStep, task: TeamTaskState): ArtifactDescriptor | undefined {
|
|
@@ -203,6 +203,9 @@ export const HANDOFF_TEMPLATE = [
|
|
|
203
203
|
"",
|
|
204
204
|
"### Follow-ups",
|
|
205
205
|
"<!-- Any remaining issues or next steps -->",
|
|
206
|
+
"",
|
|
207
|
+
"### Provenance",
|
|
208
|
+
"<!-- Self-report your runtime identity (pre-set in your bash tool env, one line): PI_SESSION_ID, PI_SESSION_FILE, PI_MODEL, PI_PROVIDER, PI_REASONING_LEVEL — e.g. `session=<id> model=<provider>/<model> reasoning=<level>` -->",
|
|
206
209
|
].join("\n");
|
|
207
210
|
|
|
208
211
|
export interface ParsedHandoff {
|
|
@@ -30,6 +30,7 @@ import * as path from "node:path";
|
|
|
30
30
|
import { loadConfig } from "../../config/config.ts";
|
|
31
31
|
import { getCrewEnv, getCrewEnvInt } from "../../config/env-vars.ts";
|
|
32
32
|
import { errors } from "../../errors.ts";
|
|
33
|
+
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
33
34
|
import { appendEventAsync, appendEventBuffered } from "../../state/event-log/event-log.ts";
|
|
34
35
|
import { writeArtifact } from "../../state/stores/artifact-store.ts";
|
|
35
36
|
import { upsertOwnershipEntry } from "../../state/stores/ownership-map.ts";
|
|
@@ -38,6 +39,8 @@ import { type FatalFsCause, failureCauseForAttempt } from "../../utils/fs-errno.
|
|
|
38
39
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
39
40
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
40
41
|
import type { ChildPiLifecycleEvent, ChildPiRunResult } from "../child-pi/child-pi.ts";
|
|
42
|
+
import type { SessionRecoveryInfo } from "../child-pi/session-recovery.ts";
|
|
43
|
+
import { classifyBool, resolveClassifierEnabled, resolveClassifierModel } from "../classifier/classifier-service.ts";
|
|
41
44
|
import {
|
|
42
45
|
appendCrewAgentEventBuffered,
|
|
43
46
|
appendCrewAgentOutputBuffered,
|
|
@@ -393,12 +396,113 @@ export function attemptErrorFor(
|
|
|
393
396
|
return err;
|
|
394
397
|
}
|
|
395
398
|
|
|
399
|
+
/** Outcome of the P2-1 retry-triage classifier consultation. */
|
|
400
|
+
export interface RetryTriageResult {
|
|
401
|
+
/** false → skip the queued re-attempt (classifier judged the failure permanent). */
|
|
402
|
+
proceedWithRetry: boolean;
|
|
403
|
+
/** true ONLY when a real classifier answer informed the decision (event + decision metric emitted). */
|
|
404
|
+
consulted: boolean;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export interface RetryTriageArgs {
|
|
408
|
+
/** Pre-resolved gate (resolveClassifierEnabled) — false short-circuits before ANY registry touch. */
|
|
409
|
+
enabled: boolean;
|
|
410
|
+
/** Host-process model registry (ctx.modelRegistry, threaded unknown). */
|
|
411
|
+
modelRegistry: unknown;
|
|
412
|
+
/** Pre-resolved classifier model id (resolveClassifierModel). */
|
|
413
|
+
classifierModel: string;
|
|
414
|
+
/** Failure summary the attempt loop already computed (task error string). */
|
|
415
|
+
failureSummary: string;
|
|
416
|
+
/** Model the failed attempt used. */
|
|
417
|
+
failedModel: string;
|
|
418
|
+
/** Failed attempt's exit code (null = signal death). */
|
|
419
|
+
exitCode: number | null;
|
|
420
|
+
/** Event-log identity for the task.retry_triage diagnostic event. */
|
|
421
|
+
eventsPath: string;
|
|
422
|
+
runId: string;
|
|
423
|
+
taskId: string;
|
|
424
|
+
/** Optional host metric registry (crew.classifier.decisions_total). */
|
|
425
|
+
metricRegistry?: MetricRegistry;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* P2-1 (pi 1.0.4 adoption) spike consumer: transient-vs-permanent triage for
|
|
430
|
+
* a queued model-fallback re-attempt, asked BEFORE burning the next full
|
|
431
|
+
* worker spawn. ONE bool question built from the failure summary; the answer
|
|
432
|
+
* is honored ONLY when `enabled` (runtime.classifierEnabled, default FALSE →
|
|
433
|
+
* dormant → zero behavior change: the helper returns without touching the
|
|
434
|
+
* registry). Fallback on every classifier failure mode is `proceedWithRetry:
|
|
435
|
+
* true` — the pre-existing retry behavior (classifyBool never-rejects).
|
|
436
|
+
*
|
|
437
|
+
* Emits `crew.classifier.decisions_total{consumer,decision}` + a
|
|
438
|
+
* `task.retry_triage` event ONLY when a real classifier answer informed the
|
|
439
|
+
* decision (fallback paths are already counted by the service's
|
|
440
|
+
* calls_total metric and must not double-report a decision they did not make).
|
|
441
|
+
*/
|
|
442
|
+
export async function triageRetryWithClassifier(args: RetryTriageArgs): Promise<RetryTriageResult> {
|
|
443
|
+
if (!args.enabled) return { proceedWithRetry: true, consulted: false };
|
|
444
|
+
const result = await classifyBool({
|
|
445
|
+
modelRegistry: args.modelRegistry,
|
|
446
|
+
classifierModel: args.classifierModel,
|
|
447
|
+
questionKey: "transient",
|
|
448
|
+
question: {
|
|
449
|
+
type: "bool",
|
|
450
|
+
instructions:
|
|
451
|
+
"A worker task failed and a retry on a different model is queued. Read the failure state. Is the failure TRANSIENT (a retry has a realistic chance of success, e.g. rate limit, provider hiccup, timeout, network error) rather than PERMANENT (a retry will fail the same way, e.g. auth/permission denied, invalid request, missing resource, deterministic error)?",
|
|
452
|
+
criteria: {
|
|
453
|
+
true: "Transient — the queued retry may succeed",
|
|
454
|
+
false: "Permanent — the queued retry will fail the same way",
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
state: {
|
|
458
|
+
// Cap the summary: classifiers reason over small JSON state — the raw
|
|
459
|
+
// error can carry multi-KB stderr tails.
|
|
460
|
+
failureSummary: args.failureSummary.slice(0, 2000),
|
|
461
|
+
failedModel: args.failedModel,
|
|
462
|
+
exitCode: args.exitCode,
|
|
463
|
+
},
|
|
464
|
+
fallback: true,
|
|
465
|
+
metricRegistry: args.metricRegistry,
|
|
466
|
+
metricLabels: { consumer: "retry_triage" },
|
|
467
|
+
});
|
|
468
|
+
if (!result.fromClassifier) return { proceedWithRetry: true, consulted: false };
|
|
469
|
+
const decision = result.decision ? "transient" : "permanent";
|
|
470
|
+
try {
|
|
471
|
+
args.metricRegistry?.counter("crew.classifier.decisions_total", "Classifier-informed decisions by consumer and verdict").inc({
|
|
472
|
+
consumer: "retry_triage",
|
|
473
|
+
decision: result.decision ? "transient_retry" : "permanent_skip",
|
|
474
|
+
});
|
|
475
|
+
} catch (metricError) {
|
|
476
|
+
logInternalError("child-executor.retry-triage.metric", metricError, `decision=${decision}`, "warn");
|
|
477
|
+
}
|
|
478
|
+
void appendEventAsync(args.eventsPath, {
|
|
479
|
+
type: "task.retry_triage",
|
|
480
|
+
runId: args.runId,
|
|
481
|
+
taskId: args.taskId,
|
|
482
|
+
message: `Retry triage classified the failure as ${decision} (classifier ${result.usedModel ?? args.classifierModel})`,
|
|
483
|
+
data: {
|
|
484
|
+
decision,
|
|
485
|
+
classifierModel: result.usedModel,
|
|
486
|
+
configuredModel: args.classifierModel,
|
|
487
|
+
failedModel: args.failedModel,
|
|
488
|
+
stopReason: result.stopReason,
|
|
489
|
+
},
|
|
490
|
+
}).catch(() => {
|
|
491
|
+
/* no-op: best-effort diagnostic append, ignore delivery errors */
|
|
492
|
+
});
|
|
493
|
+
return { proceedWithRetry: result.decision, consulted: true };
|
|
494
|
+
}
|
|
495
|
+
|
|
396
496
|
export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<TaskExecutionResult> {
|
|
397
497
|
const input = ctx.input;
|
|
398
498
|
const manifest: TeamRunManifest = ctx.manifest;
|
|
399
499
|
let task = ctx.task;
|
|
400
500
|
let tasks = ctx.tasks;
|
|
401
|
-
|
|
501
|
+
// R3-1: the child-pi worker's USER message is the dynamic suffix — the
|
|
502
|
+
// run-static header rides --append-system-prompt (compaction-safe).
|
|
503
|
+
// Fallback to the full prompt keeps hand-built contexts (tests, custom
|
|
504
|
+
// callers that skip pre-execution) on the old single-channel behavior.
|
|
505
|
+
const userPrompt = ctx.userPrompt ?? ctx.prompt;
|
|
402
506
|
const skillPaths = ctx.skillPaths;
|
|
403
507
|
const collectedJsonEvents = ctx.collectedJsonEvents;
|
|
404
508
|
const streamBridge: StreamBridgeHandle | undefined = ctx.streamBridge;
|
|
@@ -409,6 +513,9 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
409
513
|
let modelAttempts: ModelAttemptSummary[] | undefined;
|
|
410
514
|
let parsedOutput: ParsedPiJsonOutput | undefined;
|
|
411
515
|
let rawFinalText: string | undefined;
|
|
516
|
+
// W2 (P1-1): last attempt's session-recovery provenance (thread-level,
|
|
517
|
+
// mirrors rawFinalText — escapes the attempt loop's block scope).
|
|
518
|
+
let recoveredFromSession: SessionRecoveryInfo | undefined;
|
|
412
519
|
let intermediateFindings: string | undefined;
|
|
413
520
|
let finalStdout = "";
|
|
414
521
|
let transcriptPath: string | undefined;
|
|
@@ -417,6 +524,12 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
417
524
|
|
|
418
525
|
const modelFallbackPolicy = resolveTaskModelFallbackPolicy(task.cwd);
|
|
419
526
|
const defaultSubagentModel = resolveTaskDefaultSubagentModel(task.cwd);
|
|
527
|
+
// P2-1 (pi 1.0.4 adoption): retry-triage classifier gate — resolved once
|
|
528
|
+
// per task (env PI_CREW_CLASSIFIER_ENABLED beats runtime.classifierEnabled
|
|
529
|
+
// beats default FALSE — dormant). The disabled path never touches the
|
|
530
|
+
// registry, so default-off is a zero behavior change.
|
|
531
|
+
const classifierTriageEnabled = resolveClassifierEnabled(input.runtimeConfig?.classifierEnabled);
|
|
532
|
+
const classifierTriageModel = resolveClassifierModel(input.runtimeConfig?.classifierModel);
|
|
420
533
|
const modelRoutingPlan = buildConfiguredModelRouting({
|
|
421
534
|
overrideModel: input.modelOverride,
|
|
422
535
|
stepModel: input.step.model,
|
|
@@ -652,7 +765,15 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
652
765
|
try {
|
|
653
766
|
childResult = await runWorker({
|
|
654
767
|
cwd: task.cwd,
|
|
655
|
-
task:
|
|
768
|
+
task: userPrompt,
|
|
769
|
+
systemPromptAppend: ctx.systemPromptAppend,
|
|
770
|
+
// R3-19/D5: thread the runtime.hermeticWorkers config gate to the
|
|
771
|
+
// worker argv (--no-extensions). Env PI_CREW_HERMETIC_WORKERS still
|
|
772
|
+
// overrides either way (resolveHermeticWorkers).
|
|
773
|
+
hermeticWorkers: input.runtimeConfig?.hermeticWorkers,
|
|
774
|
+
// DR2/D1: thread runtime.workerTransport to the run-worker seam (env
|
|
775
|
+
// PI_CREW_WORKER_TRANSPORT still wins — resolveWorkerTransport).
|
|
776
|
+
workerTransport: input.runtimeConfig?.workerTransport,
|
|
656
777
|
agent: input.agent,
|
|
657
778
|
model,
|
|
658
779
|
signal: timeoutController.signal,
|
|
@@ -665,6 +786,10 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
665
786
|
parentContext: input.parentContext,
|
|
666
787
|
excludeContextBash: input.runtimeConfig?.excludeContextBash,
|
|
667
788
|
sessionId: manifest.sessionId,
|
|
789
|
+
// W2 (P1-1): thread the runtime.sessionRecovery flag so the config
|
|
790
|
+
// gate reaches the crash-path tail-replay (env/default still work
|
|
791
|
+
// without this — resolveSessionRecoveryEnabled precedence).
|
|
792
|
+
sessionRecovery: input.runtimeConfig?.sessionRecovery,
|
|
668
793
|
role: task.role,
|
|
669
794
|
thinkingOverride: input.teamRoleThinking,
|
|
670
795
|
runId: manifest.runId,
|
|
@@ -997,6 +1122,9 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
997
1122
|
const transcriptText = tailReadWithLineSnap(transcriptPath, MAX_TRANSCRIPT_PARSE_BYTES, childResult.stdout);
|
|
998
1123
|
parsedOutput = parsePiJsonOutput(transcriptText);
|
|
999
1124
|
rawFinalText = childResult.rawFinalText;
|
|
1125
|
+
// W2 (P1-1): keep the most recent recovery info (crash-path only —
|
|
1126
|
+
// undefined on clean exits, so this stays dormant in the happy path).
|
|
1127
|
+
recoveredFromSession = childResult.recoveredFromSession ?? recoveredFromSession;
|
|
1000
1128
|
intermediateFindings = childResult.intermediateFindings;
|
|
1001
1129
|
error = attemptErrorFor(childResult, parsedOutput, task.id);
|
|
1002
1130
|
// bug-026 sub-issue B: classify fatal fs errnos (ENOSPC/EDQUOT/EMFILE/
|
|
@@ -1074,6 +1202,30 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
1074
1202
|
}
|
|
1075
1203
|
}
|
|
1076
1204
|
if (!nextModel || !isRetryableModelFailure(error)) break;
|
|
1205
|
+
// P2-1 spike: before burning the queued (expensive) re-attempt, ask the
|
|
1206
|
+
// host classifier whether the failure looks transient. Dormant unless
|
|
1207
|
+
// classifierTriageEnabled — the disabled path returns without touching
|
|
1208
|
+
// the registry (zero behavior change), and every classifier failure
|
|
1209
|
+
// mode falls back to proceeding with the retry (never-rejects contract).
|
|
1210
|
+
const triage = await triageRetryWithClassifier({
|
|
1211
|
+
enabled: classifierTriageEnabled,
|
|
1212
|
+
modelRegistry: input.modelRegistry,
|
|
1213
|
+
classifierModel: classifierTriageModel,
|
|
1214
|
+
failureSummary: error,
|
|
1215
|
+
failedModel: attempt.model,
|
|
1216
|
+
exitCode,
|
|
1217
|
+
eventsPath: manifest.eventsPath,
|
|
1218
|
+
runId: manifest.runId,
|
|
1219
|
+
taskId: task.id,
|
|
1220
|
+
metricRegistry: input.metricRegistry,
|
|
1221
|
+
});
|
|
1222
|
+
if (!triage.proceedWithRetry) {
|
|
1223
|
+
logs.push(
|
|
1224
|
+
`CLASSIFIER TRIAGE: failure classified PERMANENT — skipping retry on ${nextModel} (classifier ${classifierTriageModel})`,
|
|
1225
|
+
"",
|
|
1226
|
+
);
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1077
1229
|
logs.push(formatModelAttemptNote(attempt, nextModel), "");
|
|
1078
1230
|
}
|
|
1079
1231
|
// E2 (Round 15): when the fallback chain was used and STILL failed, surface
|
|
@@ -1121,6 +1273,12 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
1121
1273
|
const resultCandidates: ReadonlyArray<{ source: ResultSource; text: string | undefined }> = [
|
|
1122
1274
|
{ source: "rawFinalText", text: cleanResultText(rawFinalText) },
|
|
1123
1275
|
{ source: "finalText", text: cleanResultText(parsedOutput?.finalText) },
|
|
1276
|
+
// W2 (P1-1): crash-path session tail-recovery. Ranks BELOW live captures
|
|
1277
|
+
// (raw/finalText — the stream is authoritative when it survived) but
|
|
1278
|
+
// ABOVE stdout/stderr noise: on signal death (exitCode null) the live
|
|
1279
|
+
// captures are empty and this is the recovered last complete assistant
|
|
1280
|
+
// message from the worker's session JSONL.
|
|
1281
|
+
{ source: "session", text: cleanResultText(recoveredFromSession?.text) },
|
|
1124
1282
|
{ source: "stdout", text: cleanResultText(finalStdout) },
|
|
1125
1283
|
{ source: "stderr", text: cleanResultText(finalStderr) },
|
|
1126
1284
|
{ source: "findings", text: cleanResultText(intermediateFindings) },
|
|
@@ -1142,6 +1300,18 @@ export async function runChildProcessTask(ctx: TaskExecutionContext): Promise<Ta
|
|
|
1142
1300
|
"(no output)",
|
|
1143
1301
|
producer: task.id,
|
|
1144
1302
|
});
|
|
1303
|
+
// W2 (P1-1): serialize the recovery provenance as a result sidecar — the
|
|
1304
|
+
// recovered text itself flows through the "session" candidate above; this
|
|
1305
|
+
// records WHERE it came from (session id/file, timestamp, stopReason) so
|
|
1306
|
+
// crash-path recovery is auditable after the fact.
|
|
1307
|
+
if (recoveredFromSession) {
|
|
1308
|
+
writeArtifact(manifest.artifactsRoot, {
|
|
1309
|
+
kind: "result",
|
|
1310
|
+
relativePath: `results/${task.id}.session-recovery.json`,
|
|
1311
|
+
content: JSON.stringify(recoveredFromSession, null, "\t"),
|
|
1312
|
+
producer: task.id,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1145
1315
|
const logArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
1146
1316
|
kind: "log",
|
|
1147
1317
|
relativePath: `logs/${task.id}.log`,
|
|
@@ -86,16 +86,18 @@ export interface TaskExecutionResult {
|
|
|
86
86
|
terminalEvidence: OperationTerminalEvidence[];
|
|
87
87
|
startupEvidence: import("../heartbeat/worker-startup.ts").WorkerStartupEvidence;
|
|
88
88
|
/** Which child-executor fallback branch produced the result artifact
|
|
89
|
-
* content (rawFinalText → finalText → stdout → stderr → findings →
|
|
89
|
+
* content (rawFinalText → finalText → session → stdout → stderr → findings →
|
|
90
90
|
* "(no output)"). Finding #2 (2026-09-29 battery): "stderr"/"none" mean
|
|
91
91
|
* every authoritative worker output source was empty — finalizeTaskResult
|
|
92
92
|
* fails those DETERMINISTICALLY; the stderr-noise classifier stays a
|
|
93
|
-
* defense layer, never the verdict oracle.
|
|
93
|
+
* defense layer, never the verdict oracle. "session" (W2/P1-1) = the text
|
|
94
|
+
* was tail-recovered from the worker's session JSONL on a signal-death
|
|
95
|
+
* crash path — authoritative recovered output, not a failure source. */
|
|
94
96
|
resultSource?: ResultSource;
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
/** Result-artifact provenance tags (see {@link TaskExecutionResult.resultSource}). */
|
|
98
|
-
export type ResultSource = "rawFinalText" | "finalText" | "stdout" | "stderr" | "findings" | "none";
|
|
100
|
+
export type ResultSource = "rawFinalText" | "finalText" | "session" | "stdout" | "stderr" | "findings" | "none";
|
|
99
101
|
|
|
100
102
|
/** Final persisted task state returned by finalizeTaskResult. */
|
|
101
103
|
export interface FinalTaskResult {
|
|
@@ -69,6 +69,17 @@ export interface TaskExecutionContext {
|
|
|
69
69
|
skillNames: string[] | undefined;
|
|
70
70
|
skillPaths: string[] | undefined;
|
|
71
71
|
prompt: string;
|
|
72
|
+
/** R3-1: the USER-message half of the rendered prompt (dynamicSuffix) —
|
|
73
|
+
* what the worker's task.md actually carries when the run-static header
|
|
74
|
+
* moves to the --append-system-prompt channel. `prompt` stays the FULL
|
|
75
|
+
* rendered prompt (stablePrefix + dynamicSuffix) for artifacts and the
|
|
76
|
+
* live-session path. Optional so hand-built contexts (tests, custom
|
|
77
|
+
* callers) keep the old single-channel behavior via the ?? fallback. */
|
|
78
|
+
userPrompt?: string;
|
|
79
|
+
/** R3-1: run-static worker header (stablePrefix) for the append-system-prompt
|
|
80
|
+
* channel — threaded to buildPiWorkerArgs via ChildPiRunInput. Undefined on
|
|
81
|
+
* hand-built contexts → single-channel legacy behavior. */
|
|
82
|
+
systemPromptAppend?: string;
|
|
72
83
|
promptArtifact: ArtifactDescriptor;
|
|
73
84
|
/** G18: prompt-breakdown artifact descriptor (undefined when PI_CREW_PROMPT_BREAKDOWN off). */
|
|
74
85
|
breakdownArtifact?: ArtifactDescriptor;
|
|
@@ -141,7 +152,14 @@ export async function prepareTaskExecutionContext(
|
|
|
141
152
|
// caller provided one) into the dep-context collection — cache hits reuse
|
|
142
153
|
// the closeout's reads byte-identically; undefined keeps uncached behavior.
|
|
143
154
|
const dependencyContext = collectDependencyOutputContext(manifest, input.tasks, input.task, input.step, input.resultReadCache);
|
|
144
|
-
|
|
155
|
+
// Handoff budget: caps the dynamic.dependencyContext layer at an est-token
|
|
156
|
+
// (chars/4) budget (env PI_CREW_HANDOFF_BUDGET_TOKENS > runtime config >
|
|
157
|
+
// default 1800). IMPORTANT: only the RENDER path is budgeted — an explicit
|
|
158
|
+
// input.dependencyContextText override (leader-provided text) passes through
|
|
159
|
+
// UNCHANGED.
|
|
160
|
+
const dependencyContextText =
|
|
161
|
+
input.dependencyContextText ??
|
|
162
|
+
renderDependencyOutputContext(dependencyContext, { budgetTokens: input.runtimeConfig?.handoffBudgetTokens });
|
|
145
163
|
let task: TeamTaskState = {
|
|
146
164
|
...input.task,
|
|
147
165
|
cwd: workspace.cwd,
|
|
@@ -238,6 +256,14 @@ export async function prepareTaskExecutionContext(
|
|
|
238
256
|
step: input.step,
|
|
239
257
|
override: input.skillOverride,
|
|
240
258
|
runId: manifest.runId,
|
|
259
|
+
// R3-23: the child-process spawn path feeds every resolved skill to
|
|
260
|
+
// the worker via --skill (ctx.skillPaths → buildPiWorkerArgs), so the
|
|
261
|
+
// host advertises them in <available_skills> — drop the duplicated
|
|
262
|
+
// per-skill entries from the prompt block. Live-session/scaffold
|
|
263
|
+
// workers get no --skill channel and keep the full block. Externally
|
|
264
|
+
// supplied skillPaths may diverge from this render — stay safe and
|
|
265
|
+
// keep the full block for those callers.
|
|
266
|
+
advertisedByHost: runtimeKind === "child-process" && input.skillPaths === undefined,
|
|
241
267
|
})
|
|
242
268
|
: undefined;
|
|
243
269
|
const skillBlock = input.skillBlock ?? renderedSkills?.block;
|
|
@@ -435,6 +461,11 @@ export async function prepareTaskExecutionContext(
|
|
|
435
461
|
collectedJsonEvents,
|
|
436
462
|
streamBridge,
|
|
437
463
|
startupEvidence,
|
|
464
|
+
// R3-1: channel split for child-pi workers — user message = dynamic
|
|
465
|
+
// suffix; run-static header = system-prompt append channel (compaction
|
|
466
|
+
// summarizes the user-message span, compaction.md:150-160).
|
|
467
|
+
userPrompt: promptResult.dynamicSuffix,
|
|
468
|
+
systemPromptAppend: promptResult.stablePrefix,
|
|
438
469
|
},
|
|
439
470
|
};
|
|
440
471
|
}
|
|
@@ -275,11 +275,14 @@ export interface RenderedTaskPrompt {
|
|
|
275
275
|
/** Full rendered prompt (stablePrefix + dynamicSuffix). */
|
|
276
276
|
full: string;
|
|
277
277
|
/**
|
|
278
|
-
* SR-02 phase 1 (2026-09-23): per-section char counts of the
|
|
278
|
+
* SR-02 phase 1 (2026-09-23): per-section char counts of the worker prompt,
|
|
279
279
|
* keyed by section name — the token-breakdown instrumentation. Populated
|
|
280
280
|
* only when PI_CREW_PROMPT_BREAKDOWN=1 (off by default; zero cost when off:
|
|
281
281
|
* the sections object is built lazily). Pre-execution adds the SYSTEM-side
|
|
282
282
|
* pieces (agent definition, skills) and writes the JSON artifact.
|
|
283
|
+
* R3-1 note: stable.* sections ride the --append-system-prompt channel;
|
|
284
|
+
* total.userPrompt counts the user message (dynamic suffix) only, while
|
|
285
|
+
* total.systemAppend counts the system-channel header.
|
|
283
286
|
*/
|
|
284
287
|
sections?: Record<string, number>;
|
|
285
288
|
}
|
|
@@ -424,6 +427,8 @@ export async function renderTaskPrompt(
|
|
|
424
427
|
const full = [stablePrefix, "", dynamicSuffix].join("\n");
|
|
425
428
|
const sections: Record<string, number> | undefined = promptBreakdownEnabled()
|
|
426
429
|
? {
|
|
430
|
+
// R3-1: the stable.* sections now ride the --append-system-prompt
|
|
431
|
+
// channel; the worker's user message is the dynamic suffix only.
|
|
427
432
|
"stable.runtimeHeader": headerBlock.length,
|
|
428
433
|
"stable.protocol": protocolBlock.length,
|
|
429
434
|
"stable.roleInstructions": roleInstructions.length,
|
|
@@ -440,7 +445,10 @@ export async function renderTaskPrompt(
|
|
|
440
445
|
"dynamic.memory": memoryBlock.length,
|
|
441
446
|
"dynamic.outputSchema": outputSchemaBlock.length,
|
|
442
447
|
"dynamic.taskAndHandoff": taskAndHandoff.length,
|
|
443
|
-
|
|
448
|
+
// R3-1: channel split — the user message carries ONLY the dynamic
|
|
449
|
+
// suffix; the stable prefix went to the system-prompt append channel.
|
|
450
|
+
"total.systemAppend": stablePrefix.length,
|
|
451
|
+
"total.userPrompt": dynamicSuffix.length,
|
|
444
452
|
}
|
|
445
453
|
: undefined;
|
|
446
454
|
return { stablePrefix, dynamicSuffix, full, sections };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
2
2
|
import type { CrewLimitsConfig, CrewRuntimeConfig } from "../config/config.ts";
|
|
3
|
+
import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
3
4
|
import { writeArtifact } from "../state/stores/artifact-store.ts";
|
|
4
5
|
import type { ArtifactDescriptor, OperationTerminalEvidence, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
5
6
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
@@ -51,6 +52,11 @@ export interface TaskRunnerInput {
|
|
|
51
52
|
parentContext?: string;
|
|
52
53
|
parentModel?: unknown;
|
|
53
54
|
modelRegistry?: unknown;
|
|
55
|
+
/** P2-1 (pi 1.0.4 adoption): host-process metric registry threaded from
|
|
56
|
+
* ExecuteTeamRunInput so the retry-triage classifier seam can count its
|
|
57
|
+
* calls/decisions (crew.classifier.* counters). Optional — undefined keeps
|
|
58
|
+
* the uncounted behavior (e.g. tests, hand-built contexts). */
|
|
59
|
+
metricRegistry?: MetricRegistry;
|
|
54
60
|
modelOverride?: string;
|
|
55
61
|
teamRoleModel?: string;
|
|
56
62
|
teamRoleFallbackModels?: string[];
|