pi-crew 0.9.12 → 0.9.13
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 +34 -0
- package/README.md +1 -135
- package/package.json +1 -1
- package/src/extension/knowledge-injection.ts +236 -15
- package/src/extension/team-tool/chain-dispatch.ts +103 -0
- package/src/extension/team-tool/chain-executor.ts +400 -0
- package/src/extension/team-tool/run.ts +9 -0
- package/src/runtime/agent-memory.ts +5 -2
- package/src/runtime/background-runner.ts +49 -0
- package/src/runtime/chain-runner.ts +47 -8
- package/src/runtime/child-pi.ts +33 -0
- package/src/runtime/handoff-manager.ts +7 -0
- package/src/runtime/live-session-runtime.ts +10 -4
- package/src/runtime/pi-json-output.ts +5 -1
- package/src/runtime/task-output-context.ts +36 -7
- package/src/runtime/task-runner/prompt-builder.ts +5 -1
- package/src/runtime/task-runner.ts +7 -0
- package/src/schema/team-tool-schema.ts +9 -0
|
@@ -99,6 +99,9 @@ export interface HandoffSummary {
|
|
|
99
99
|
|
|
100
100
|
// Context snapshot
|
|
101
101
|
contextSnapshot: string;
|
|
102
|
+
|
|
103
|
+
/** Worker output text propagated through the chain (read from resultArtifact). */
|
|
104
|
+
outputText?: string;
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/**
|
|
@@ -121,6 +124,9 @@ export interface TaskResult {
|
|
|
121
124
|
filesDeleted?: string[];
|
|
122
125
|
decisions?: Decision[];
|
|
123
126
|
error?: string;
|
|
127
|
+
|
|
128
|
+
/** Worker's textual output (read from resultArtifact during chain execution). */
|
|
129
|
+
outputText?: string;
|
|
124
130
|
}
|
|
125
131
|
|
|
126
132
|
/**
|
|
@@ -443,6 +449,7 @@ export class HandoffManager {
|
|
|
443
449
|
},
|
|
444
450
|
|
|
445
451
|
contextSnapshot,
|
|
452
|
+
...(result.outputText ? { outputText: result.outputText } : {}),
|
|
446
453
|
};
|
|
447
454
|
}
|
|
448
455
|
|
|
@@ -4,8 +4,13 @@ import type { AgentConfig } from "../agents/agent-config.ts";
|
|
|
4
4
|
import type { CrewRuntimeConfig } from "../config/config.ts";
|
|
5
5
|
import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
|
|
6
6
|
import { appendEvent } from "../state/event-log.ts";
|
|
7
|
-
import { buildMemoryBlock } from "./agent-memory.ts";
|
|
8
7
|
import { trackTaskUsage } from "./usage-tracker.ts";
|
|
8
|
+
// NOTE: buildMemoryBlock is intentionally NOT imported here. The agent memory
|
|
9
|
+
// block is injected via renderTaskPrompt().full (the USER prompt), which is
|
|
10
|
+
// shared by both the child-pi path (no system prompt) and the live-session
|
|
11
|
+
// path. Adding it to liveSystemPrompt() too duplicated the entire memory
|
|
12
|
+
// block (up to 200 lines) in both the user and system prompts. Keep memory
|
|
13
|
+
// in a single place: the shared user prompt. See G3 fix.
|
|
9
14
|
import { createStreamingOutput, type StreamingOutputHandle } from "./streaming-output.ts";
|
|
10
15
|
import { registerLiveAgent, disposeLiveAgentSession, terminateLiveAgent, updateLiveAgentStatus, trackLiveAgentToolStart, trackLiveAgentToolEnd, trackLiveAgentTurnEnd, trackLiveAgentResponseText, markLiveAgentCompleted } from "./live-agent-manager.ts";
|
|
11
16
|
import { applyLiveAgentControlRequest, applyLiveAgentControlRequests, type LiveAgentControlCursor } from "./live-agent-control.ts";
|
|
@@ -371,8 +376,10 @@ function compressSessionToolDescriptions(session: LiveSessionLike): void {
|
|
|
371
376
|
// is loaded and tree-shakeable, so adding the actual logic later is trivial.
|
|
372
377
|
}
|
|
373
378
|
|
|
374
|
-
function liveSystemPrompt(input: LiveSessionSpawnInput): string {
|
|
375
|
-
|
|
379
|
+
export function liveSystemPrompt(input: LiveSessionSpawnInput): string {
|
|
380
|
+
// Agent MEMORY is intentionally omitted here — it is already injected via
|
|
381
|
+
// renderTaskPrompt().full (the user prompt, shared with the child-pi path).
|
|
382
|
+
// See the import-block note above and the G3 fix.
|
|
376
383
|
const role = input.task.role;
|
|
377
384
|
const styleBlock = buildCommunicationStyle(role);
|
|
378
385
|
const contractBlock = buildOutputContract(role);
|
|
@@ -390,7 +397,6 @@ function liveSystemPrompt(input: LiveSessionSpawnInput): string {
|
|
|
390
397
|
sensitiveConstraint,
|
|
391
398
|
"",
|
|
392
399
|
input.agent.systemPrompt || "Follow the user task exactly and report verification evidence.",
|
|
393
|
-
memory ? `\n${memory}` : "",
|
|
394
400
|
].filter(Boolean).join("\n");
|
|
395
401
|
}
|
|
396
402
|
|
|
@@ -75,7 +75,11 @@ function textFromContent(content: unknown): string[] {
|
|
|
75
75
|
return text;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/** Extract assistant-text fragments from a parsed Pi JSON event.
|
|
79
|
+
* Exported so {@link ChildPiLineObserver} can capture the RAW (uncapped)
|
|
80
|
+
* assistant text for the authoritative result, mirroring the extraction
|
|
81
|
+
* order this function uses inside {@link parsePiJsonOutput}. */
|
|
82
|
+
export function extractText(value: unknown): string[] {
|
|
79
83
|
const obj = asRecord(value);
|
|
80
84
|
if (!obj) return [];
|
|
81
85
|
const message = asRecord(obj.message);
|
|
@@ -14,6 +14,12 @@ export interface DependencyContextEntry {
|
|
|
14
14
|
status: string;
|
|
15
15
|
resultSummary: string;
|
|
16
16
|
resultPath?: string;
|
|
17
|
+
/** Absolute path to the FULL (untruncated) result, teed when the inline
|
|
18
|
+
* resultSummary was materially truncated (>TEE_THRESHOLD_MULTIPLIER). The
|
|
19
|
+
* downstream worker can `read` this to recover the dropped middle. Mirrors
|
|
20
|
+
* the sharedReads recovery path so dependency injection is no longer
|
|
21
|
+
* circular (re-reading resultPath used to yield the same truncated text). */
|
|
22
|
+
fullOutputPath?: string;
|
|
17
23
|
structuredResults?: Record<string, unknown>;
|
|
18
24
|
artifactsProduced?: string[];
|
|
19
25
|
usage?: { inputTokens: number; outputTokens: number; durationMs: number };
|
|
@@ -50,6 +56,16 @@ function containedExists(filePath: string, baseDir?: string): boolean {
|
|
|
50
56
|
*/
|
|
51
57
|
export const MAX_RESULT_INLINE_BYTES = 32_000;
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Tee-recovery multiplier (R2). A shared artifact is teed to disk — so the
|
|
61
|
+
* downstream worker can `read` the dropped middle — when its size exceeds
|
|
62
|
+
* this fraction of {@link MAX_RESULT_INLINE_BYTES}. Lowered from 2.0
|
|
63
|
+
* (64 KB) to 1.25 (40 KB) so the 32–64 KB band, where the head+tail split
|
|
64
|
+
* is already materially lossy for structured content, also gets a recovery
|
|
65
|
+
* path instead of losing the middle forever.
|
|
66
|
+
*/
|
|
67
|
+
export const TEE_THRESHOLD_MULTIPLIER = 1.25;
|
|
68
|
+
|
|
53
69
|
/**
|
|
54
70
|
* Read a file and return its content, truncating to a head+tail slice if it
|
|
55
71
|
* exceeds {@link MAX_RESULT_INLINE_BYTES} characters. Multi-byte UTF-8
|
|
@@ -110,10 +126,11 @@ function writeTeeFile(fullOutputPath: string, content: string): boolean {
|
|
|
110
126
|
* content AND (when tee was actually written) the absolute path to the full
|
|
111
127
|
* file. Returns undefined if the file cannot be read at all.
|
|
112
128
|
*
|
|
113
|
-
* Tee threshold: only when content.length >
|
|
114
|
-
* (
|
|
115
|
-
*
|
|
116
|
-
* with
|
|
129
|
+
* Tee threshold: only when content.length > TEE_THRESHOLD_MULTIPLIER ×
|
|
130
|
+
* MAX_RESULT_INLINE_BYTES (R2: 1.25× = 40 KB; previously 2× = 64 KB). Below
|
|
131
|
+
* the tee threshold the head+tail split is mostly intact and the worker can
|
|
132
|
+
* live with it; at/above the threshold the dropped middle is recoverable via
|
|
133
|
+
* the teed full file. File content is read once and reused for both the
|
|
117
134
|
* pipeline (truncation) and the tee write (full file).
|
|
118
135
|
*
|
|
119
136
|
* Truncation behavior is unchanged from the P0-A pipeline: ANSI strip +
|
|
@@ -131,8 +148,10 @@ export function readIfSmallWithTee(
|
|
|
131
148
|
const content = fs.readFileSync(safePath, "utf-8");
|
|
132
149
|
if (content.length > maxChars) {
|
|
133
150
|
let fullOutputPath: string | undefined;
|
|
134
|
-
// Tee
|
|
135
|
-
|
|
151
|
+
// Tee when truncation is materially lossy (>TEE_THRESHOLD_MULTIPLIER ×
|
|
152
|
+
// threshold). R2: lowered from 2× (64 KB) to 1.25× (40 KB) so the
|
|
153
|
+
// 32–64 KB band also gets a recovery path.
|
|
154
|
+
if (opts.tee && content.length > maxChars * TEE_THRESHOLD_MULTIPLIER) {
|
|
136
155
|
if (writeTeeFile(opts.tee.fullOutputPath, content)) {
|
|
137
156
|
fullOutputPath = opts.tee.fullOutputPath;
|
|
138
157
|
}
|
|
@@ -267,13 +286,18 @@ export function collectDependencyOutputContext(manifest: TeamRunManifest, tasks:
|
|
|
267
286
|
const byStep = new Map(tasks.map((item) => [item.stepId, item]).filter((entry): entry is [string, TeamTaskState] => Boolean(entry[0])));
|
|
268
287
|
const byId = new Map(tasks.map((item) => [item.id, item]));
|
|
269
288
|
const dependencies = task.dependsOn.map((dep) => byStep.get(dep) ?? byId.get(dep)).filter((item): item is TeamTaskState => Boolean(item)).map((item) => {
|
|
270
|
-
const
|
|
289
|
+
const fullOutputPath = item.resultArtifact ? teePathForArtifact(manifest.artifactsRoot, task.id, item.id) : undefined;
|
|
290
|
+
const teeResult = item.resultArtifact
|
|
291
|
+
? readIfSmallWithTee(item.resultArtifact.path, { baseDir: manifest.artifactsRoot, ...(fullOutputPath ? { tee: { fullOutputPath } } : {}) })
|
|
292
|
+
: undefined;
|
|
293
|
+
const resultText = teeResult?.content;
|
|
271
294
|
return {
|
|
272
295
|
taskId: item.id,
|
|
273
296
|
role: item.role,
|
|
274
297
|
status: item.status,
|
|
275
298
|
resultSummary: resultText ?? "",
|
|
276
299
|
resultPath: item.resultArtifact?.path,
|
|
300
|
+
...(teeResult?.fullOutputPath ? { fullOutputPath: teeResult.fullOutputPath } : {}),
|
|
277
301
|
structuredResults: resultText ? tryParseJson(resultText) : undefined,
|
|
278
302
|
artifactsProduced: listTaskArtifacts(manifest, item.id),
|
|
279
303
|
usage: aggregateUsage(item),
|
|
@@ -317,6 +341,11 @@ export function renderDependencyOutputContext(context: DependencyOutputContext):
|
|
|
317
341
|
parts.push("# Dependency Outputs", "");
|
|
318
342
|
for (const dep of context.dependencies) {
|
|
319
343
|
parts.push(`## ${dep.taskId} (${dep.role})`, `Status: ${dep.status}`, dep.resultPath ? `Result artifact: ${dep.resultPath}` : "", "", dep.resultSummary?.trim() || "(no result output)", "");
|
|
344
|
+
// P1-A dependency tee-recovery hint: when the dependency's result was
|
|
345
|
+
// materially truncated (>1.25× MAX_RESULT_INLINE_BYTES) the full RAW
|
|
346
|
+
// content was teed to fullOutputPath. Mirrors the sharedReads hint so the
|
|
347
|
+
// downstream worker can read the dropped middle instead of re-deriving.
|
|
348
|
+
if (dep.fullOutputPath) parts.push(`Full output (if you need the missing middle): ${dep.fullOutputPath}`, "");
|
|
320
349
|
if (dep.structuredResults) parts.push("Structured results:", JSON.stringify(dep.structuredResults, null, 2), "");
|
|
321
350
|
if (dep.artifactsProduced?.length) parts.push(`Artifacts produced: ${dep.artifactsProduced.join(", ")}`, "");
|
|
322
351
|
if (dep.usage) parts.push(`Usage: ${dep.usage.inputTokens} input tokens, ${dep.usage.outputTokens} output tokens, ${dep.usage.durationMs}ms`, "");
|
|
@@ -120,7 +120,11 @@ export async function renderTaskPrompt(manifest: TeamRunManifest, step: Workflow
|
|
|
120
120
|
// O4: project knowledge (.crew/knowledge.md) — workers don't load the
|
|
121
121
|
// pi-crew extension (spawned with --no-extensions), so before_agent_start
|
|
122
122
|
// never fires for them. Inject here so every worker sees project knowledge.
|
|
123
|
-
buildKnowledgeFragment(task.cwd
|
|
123
|
+
buildKnowledgeFragment(task.cwd, {
|
|
124
|
+
goal: manifest.goal,
|
|
125
|
+
taskText: step.task,
|
|
126
|
+
role: step.role,
|
|
127
|
+
}),
|
|
124
128
|
].filter(Boolean).join("\n");
|
|
125
129
|
|
|
126
130
|
// Dynamic suffix: goal, step, skills, task packet, dependency context, memory — changes per task
|
|
@@ -342,6 +342,7 @@ export async function runTeamTask(
|
|
|
342
342
|
let error: string | undefined;
|
|
343
343
|
let modelAttempts: ModelAttemptSummary[] | undefined;
|
|
344
344
|
let parsedOutput: ParsedPiJsonOutput | undefined;
|
|
345
|
+
let rawFinalText: string | undefined;
|
|
345
346
|
let finalStdout = "";
|
|
346
347
|
let transcriptPath: string | undefined;
|
|
347
348
|
let terminalEvidence: OperationTerminalEvidence[] = [];
|
|
@@ -717,6 +718,7 @@ export async function runTeamTask(
|
|
|
717
718
|
childResult.stdout,
|
|
718
719
|
);
|
|
719
720
|
parsedOutput = parsePiJsonOutput(transcriptText);
|
|
721
|
+
rawFinalText = childResult.rawFinalText;
|
|
720
722
|
error =
|
|
721
723
|
childResult.error ||
|
|
722
724
|
(childResult.exitCode && childResult.exitCode !== 0
|
|
@@ -836,6 +838,11 @@ export async function runTeamTask(
|
|
|
836
838
|
kind: "result",
|
|
837
839
|
relativePath: `results/${task.id}.txt`,
|
|
838
840
|
content:
|
|
841
|
+
// Prefer the RAW (uncapped) final assistant text captured before the
|
|
842
|
+
// transcript's 16K compaction — this is the authoritative worker output.
|
|
843
|
+
// Fall back to transcript-derived finalText, then stdout/stderr, so a
|
|
844
|
+
// missing raw capture (mock/error path) never yields empty/garbage.
|
|
845
|
+
cleanResultText(rawFinalText) ??
|
|
839
846
|
cleanResultText(parsedOutput?.finalText) ??
|
|
840
847
|
cleanResultText(finalStdout) ??
|
|
841
848
|
cleanResultText(finalStderr) ??
|
|
@@ -114,6 +114,12 @@ export const TeamToolParams = Type.Object({
|
|
|
114
114
|
goal: Type.Optional(
|
|
115
115
|
Type.String({ description: "High-level objective for a team run." }),
|
|
116
116
|
),
|
|
117
|
+
chain: Type.Optional(
|
|
118
|
+
Type.String({
|
|
119
|
+
description:
|
|
120
|
+
'Chain expression: "step1 -> step2 -> step3". Runs each step as a sequential team run, passing handoff context forward via the goal text. Supports inline goals ("...") and @team references. e.g. chain=\'"Research X" -> "Analyze" -> "Write report"\'.',
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
117
123
|
task: Type.Optional(
|
|
118
124
|
Type.String({
|
|
119
125
|
description: "Concrete task text for direct role/agent execution.",
|
|
@@ -370,6 +376,9 @@ export interface TeamToolParamsValue {
|
|
|
370
376
|
role?: string;
|
|
371
377
|
agent?: string;
|
|
372
378
|
goal?: string;
|
|
379
|
+
/** Chain expression: "step1 -> step2 -> step3". Runs each step as a sequential
|
|
380
|
+
* team run with handoff context passed forward via the goal. */
|
|
381
|
+
chain?: string;
|
|
373
382
|
task?: string;
|
|
374
383
|
singleAgent?: boolean;
|
|
375
384
|
runId?: string;
|