pi-crew 0.9.27 → 0.9.28
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 +15 -0
- package/dist/build-meta.json +18 -36
- package/dist/index.mjs +115 -70
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/discover-agents.ts +4 -0
- package/src/extension/crew-vibes/figures.ts +1 -1
- package/src/extension/crew-vibes/index.ts +10 -21
- package/src/extension/crew-vibes/provider-usage.ts +119 -53
- package/src/extension/team-tool.ts +11 -0
- package/src/prompt/prompt-runtime.ts +65 -0
- package/src/runtime/child-pi.ts +6 -0
- package/src/runtime/pi-args.ts +2 -0
- package/src/runtime/task-runner.ts +1 -0
package/package.json
CHANGED
|
@@ -83,6 +83,8 @@ export interface AgentConfig {
|
|
|
83
83
|
contextMode?: "fresh" | "fork";
|
|
84
84
|
/** Maximum turns for this agent. Overrides runtime config if set. */
|
|
85
85
|
maxTurns?: number;
|
|
86
|
+
/** Cap on output tokens per API call. Set via PI_CREW_MAX_OUTPUT_TOKENS env in child. */
|
|
87
|
+
maxTokens?: number;
|
|
86
88
|
/** Effort level for this agent. Controls how much work the agent puts in. */
|
|
87
89
|
effort?: "low" | "medium" | "high";
|
|
88
90
|
/** Tools to explicitly forbid for this agent. Takes precedence over allowedTools. */
|
|
@@ -431,6 +431,10 @@ function parseAgentFile(filePath: string, source: ResourceSource): AgentConfig |
|
|
|
431
431
|
const n = Number.parseInt(frontmatter.maxTurns, 10);
|
|
432
432
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
433
433
|
})(),
|
|
434
|
+
maxTokens: (() => {
|
|
435
|
+
const n = Number.parseInt(frontmatter.maxTokens, 10);
|
|
436
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
437
|
+
})(),
|
|
434
438
|
effort:
|
|
435
439
|
frontmatter.effort === "low" || frontmatter.effort === "medium" || frontmatter.effort === "high"
|
|
436
440
|
? frontmatter.effort
|
|
@@ -61,7 +61,7 @@ const PUA_CREW_FRAMES: readonly string[] = [
|
|
|
61
61
|
* - `"pua"`: runner-pose glyphs from crew-vibes.ttf, requires font install
|
|
62
62
|
*/
|
|
63
63
|
export function crewFrames(style: "braille" | "pua" = "pua"): readonly string[] {
|
|
64
|
-
// Web terminals (gotty, wetty) cannot render PUA glyphs —
|
|
64
|
+
// Web terminals (gotty, wetty) cannot render PUA glyphs — fall back to braille
|
|
65
65
|
if (style === "pua" && isWebTerminal()) return BRAILLE_FRAMES;
|
|
66
66
|
return style === "pua" ? PUA_CREW_FRAMES : BRAILLE_FRAMES;
|
|
67
67
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type CrewVibesConfig, loadConfig, PROVIDER_STATUS_ID, saveConfig } from "./config.ts";
|
|
3
|
-
import {
|
|
4
|
-
import { fetchProviderUsage, clearProviderUsageCache } from "./provider-usage.ts";
|
|
3
|
+
import { fetchProviderUsage, clearProviderUsageCache, providerSupportsQuota } from "./provider-usage.ts";
|
|
5
4
|
import { intervalForSpeed } from "./figures.ts";
|
|
6
5
|
import {
|
|
7
6
|
asCrewTheme,
|
|
@@ -18,7 +17,6 @@ import {
|
|
|
18
17
|
setSpeedStatus,
|
|
19
18
|
} from "./render.ts";
|
|
20
19
|
import { SpeedAnimator, SpeedTracker } from "./speed.ts";
|
|
21
|
-
import { CAT_FRAMES } from "./cat-frames.ts";
|
|
22
20
|
|
|
23
21
|
export const CREW_VIBES_STATUS_KEY = "pi-crew-vibes";
|
|
24
22
|
|
|
@@ -52,7 +50,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
52
50
|
let capacityTimer: ReturnType<typeof setInterval> | undefined;
|
|
53
51
|
let providerTimer: ReturnType<typeof setInterval> | undefined;
|
|
54
52
|
let lastProviderText: string | undefined;
|
|
55
|
-
let
|
|
53
|
+
let currentProvider: string | undefined;
|
|
56
54
|
|
|
57
55
|
/** Strip ANSI codes to measure visible width. */
|
|
58
56
|
function visibleLen(text: string): number {
|
|
@@ -146,11 +144,6 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
146
144
|
const speed = speedTracker.liveTokS();
|
|
147
145
|
applyIndicator(ctx, speed);
|
|
148
146
|
renderWorking(ctx, speed);
|
|
149
|
-
// Update cat animation frame in web terminals
|
|
150
|
-
if (isWebTerminal()) {
|
|
151
|
-
catFrameIndex = (catFrameIndex + 1) % CAT_FRAMES.length;
|
|
152
|
-
ctx.ui.setWidget("crew-vibes-cat", [...CAT_FRAMES[catFrameIndex]], { placement: "aboveEditor" });
|
|
153
|
-
}
|
|
154
147
|
}, config.speed.renderIntervalMs);
|
|
155
148
|
liveTimer.unref?.();
|
|
156
149
|
}
|
|
@@ -186,7 +179,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
186
179
|
return;
|
|
187
180
|
}
|
|
188
181
|
try {
|
|
189
|
-
const usage = await fetchProviderUsage(config.capacity.providerRefreshMs);
|
|
182
|
+
const usage = await fetchProviderUsage(config.capacity.providerRefreshMs, currentProvider);
|
|
190
183
|
lastProviderText = renderProviderUsage(themeOf(ctx), usage);
|
|
191
184
|
// Re-render combined capacity + provider line
|
|
192
185
|
publishCapacity(ctx);
|
|
@@ -235,6 +228,8 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
235
228
|
speedTracker.resetSession();
|
|
236
229
|
footerAnimator.reset(null);
|
|
237
230
|
clearProviderUsageCache();
|
|
231
|
+
// Initialize provider from current model — model_select only fires on manual switch
|
|
232
|
+
currentProvider = (ctx.model as { provider?: string } | undefined)?.provider;
|
|
238
233
|
if (!config.enabled) {
|
|
239
234
|
clearVibesStatus(ctx);
|
|
240
235
|
return;
|
|
@@ -266,10 +261,6 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
266
261
|
footerAnimator.reset(speedTracker.lastTokS);
|
|
267
262
|
startLiveTimer(ctx);
|
|
268
263
|
lastRenderedAt = 0;
|
|
269
|
-
// Show cat widget in web terminals
|
|
270
|
-
if (isWebTerminal() && ctx.hasUI) {
|
|
271
|
-
ctx.ui.setWidget("crew-vibes-cat", [...CAT_FRAMES[0]], { placement: "aboveEditor" });
|
|
272
|
-
}
|
|
273
264
|
});
|
|
274
265
|
|
|
275
266
|
pi.on("message_update", (event, ctx) => {
|
|
@@ -304,10 +295,6 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
304
295
|
publishSpeedFooter(ctx);
|
|
305
296
|
startFooterTimer(ctx);
|
|
306
297
|
applyIndicator(ctx, speedTracker.lastTokS);
|
|
307
|
-
// Remove cat widget on message end
|
|
308
|
-
if (isWebTerminal() && ctx.hasUI) {
|
|
309
|
-
ctx.ui.setWidget("crew-vibes-cat", undefined);
|
|
310
|
-
}
|
|
311
298
|
});
|
|
312
299
|
|
|
313
300
|
pi.on("turn_end", () => {
|
|
@@ -321,11 +308,14 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
321
308
|
if (ctx && config.enabled && ctx.hasUI) {
|
|
322
309
|
applyIndicator(ctx, speedTracker.lastTokS);
|
|
323
310
|
ctx.ui.setWorkingMessage();
|
|
324
|
-
if (isWebTerminal()) ctx.ui.setWidget("crew-vibes-cat", undefined);
|
|
325
311
|
}
|
|
326
312
|
});
|
|
327
313
|
|
|
328
|
-
pi.on("model_select", (
|
|
314
|
+
pi.on("model_select", (event, ctx) => {
|
|
315
|
+
currentProvider = (event as { model?: { provider?: string } }).model?.provider;
|
|
316
|
+
clearProviderUsageCache();
|
|
317
|
+
publishCapacity(ctx);
|
|
318
|
+
});
|
|
329
319
|
pi.on("session_compact", (_event, ctx) => publishCapacity(ctx));
|
|
330
320
|
pi.on("session_tree", (_event, ctx) => publishCapacity(ctx));
|
|
331
321
|
|
|
@@ -335,7 +325,6 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
335
325
|
stopCapacityTimer();
|
|
336
326
|
stopProviderTimer();
|
|
337
327
|
clearVibesStatus(ctx);
|
|
338
|
-
if (ctx.hasUI) ctx.ui.setWidget("crew-vibes-cat", undefined);
|
|
339
328
|
});
|
|
340
329
|
|
|
341
330
|
async function handleCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
@@ -68,6 +68,21 @@ export function loadZaiToken(): string | undefined {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** Load the Minimax API key from env or auth.json. */
|
|
72
|
+
export function loadMinimaxToken(): string | undefined {
|
|
73
|
+
const envKey = process.env.MINIMAX_API_KEY?.trim();
|
|
74
|
+
if (envKey) return envKey;
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(readFileSync(piAuthPath(), "utf8")) as {
|
|
77
|
+
minimax?: { key?: string };
|
|
78
|
+
};
|
|
79
|
+
const key = data.minimax?.key;
|
|
80
|
+
return typeof key === "string" && key.length > 0 ? key : undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
71
86
|
/** Copilot host entry keys used by the legacy GitHub Copilot CLI. */
|
|
72
87
|
type CopilotHostEntry = {
|
|
73
88
|
oauth_token?: string;
|
|
@@ -262,69 +277,120 @@ export function clearProviderUsageCache(): void {
|
|
|
262
277
|
cachedAt = 0;
|
|
263
278
|
}
|
|
264
279
|
|
|
280
|
+
/** Minimax token plan remains response shape. */
|
|
281
|
+
type MinimaxModelRemain = {
|
|
282
|
+
model_name?: string;
|
|
283
|
+
current_interval_remaining_percent?: number;
|
|
284
|
+
current_weekly_remaining_percent?: number;
|
|
285
|
+
end_time?: number;
|
|
286
|
+
weekly_end_time?: number;
|
|
287
|
+
};
|
|
288
|
+
type MinimaxUsageResponse = {
|
|
289
|
+
model_remains?: MinimaxModelRemain[];
|
|
290
|
+
base_resp?: { status_code?: number; status_msg?: string };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
async function fetchMinimaxUsage(token: string): Promise<ProviderUsage> {
|
|
294
|
+
const data = await withTimeout(10000, async (signal) => {
|
|
295
|
+
const res = await fetch("https://www.minimax.io/v1/token_plan/remains", {
|
|
296
|
+
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
297
|
+
signal,
|
|
298
|
+
});
|
|
299
|
+
if (!res.ok) throw new Error(`minimax usage HTTP ${res.status}`);
|
|
300
|
+
return (await res.json()) as MinimaxUsageResponse;
|
|
301
|
+
});
|
|
302
|
+
if (data.base_resp?.status_code !== 0) throw new Error(data.base_resp?.status_msg || "minimax API error");
|
|
303
|
+
|
|
304
|
+
// Find the "general" model (text/chat). Fall back to first model.
|
|
305
|
+
const models = data.model_remains ?? [];
|
|
306
|
+
const general = models.find((m) => m.model_name === "general") ?? models[0];
|
|
307
|
+
if (!general) throw new Error("minimax: no model data");
|
|
308
|
+
|
|
309
|
+
// remaining_percent → used percent
|
|
310
|
+
const intervalUsed = 100 - (general.current_interval_remaining_percent ?? 100);
|
|
311
|
+
const weeklyUsed = 100 - (general.current_weekly_remaining_percent ?? 100);
|
|
312
|
+
|
|
313
|
+
const intervalReset = typeof general.end_time === "number" ? new Date(general.end_time).toISOString() : null;
|
|
314
|
+
const weeklyReset = typeof general.weekly_end_time === "number" ? new Date(general.weekly_end_time).toISOString() : null;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
providerName: "Minimax",
|
|
318
|
+
fiveHourPercent: intervalUsed,
|
|
319
|
+
fiveHourResetAt: intervalReset,
|
|
320
|
+
weeklyPercent: weeklyUsed,
|
|
321
|
+
weeklyResetAt: weeklyReset,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Providers that have a quota API. */
|
|
326
|
+
const QUOTA_PROVIDERS = new Set(["anthropic", "minimax", "minimax-cn", "zai", "github-copilot"]);
|
|
327
|
+
|
|
328
|
+
/** Check if a provider supports quota checking. */
|
|
329
|
+
export function providerSupportsQuota(provider: string): boolean {
|
|
330
|
+
return QUOTA_PROVIDERS.has(provider);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Fetch usage for a SPECIFIC provider. Returns null if not supported. */
|
|
334
|
+
async function fetchForProvider(provider: string): Promise<ProviderUsage | null> {
|
|
335
|
+
switch (provider) {
|
|
336
|
+
case "anthropic": {
|
|
337
|
+
const token = loadAnthropicToken();
|
|
338
|
+
if (!token) return null;
|
|
339
|
+
const base = await fetchAnthropicUsage(token);
|
|
340
|
+
return { providerName: "Claude", ...base };
|
|
341
|
+
}
|
|
342
|
+
case "minimax":
|
|
343
|
+
case "minimax-cn": {
|
|
344
|
+
const token = loadMinimaxToken();
|
|
345
|
+
if (!token) return null;
|
|
346
|
+
return await fetchMinimaxUsage(token);
|
|
347
|
+
}
|
|
348
|
+
case "zai": {
|
|
349
|
+
const token = loadZaiToken();
|
|
350
|
+
if (!token) return null;
|
|
351
|
+
const usage = await fetchZaiUsage(token);
|
|
352
|
+
usage.providerName = "z.ai";
|
|
353
|
+
return usage;
|
|
354
|
+
}
|
|
355
|
+
case "github-copilot": {
|
|
356
|
+
const token = loadCopilotToken();
|
|
357
|
+
if (!token) return null;
|
|
358
|
+
const pct = await fetchCopilotMonthlyPercent(token);
|
|
359
|
+
if (pct === undefined) return null;
|
|
360
|
+
return {
|
|
361
|
+
providerName: "Copilot",
|
|
362
|
+
fiveHourPercent: 0,
|
|
363
|
+
fiveHourResetAt: null,
|
|
364
|
+
weeklyPercent: pct,
|
|
365
|
+
weeklyResetAt: null,
|
|
366
|
+
copilotMonthlyPercent: pct,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
265
374
|
/**
|
|
266
|
-
* Fetch provider rate-limit usage
|
|
267
|
-
*
|
|
268
|
-
* Tries providers in order: Anthropic → z.ai → Copilot.
|
|
269
|
-
* Returns the first one that has credentials + responds successfully.
|
|
270
|
-
* Returns `null` when no credentials exist or all fetches fail — never throws.
|
|
375
|
+
* Fetch provider rate-limit usage for the current model's provider.
|
|
376
|
+
* Returns `null` when provider has no quota API or no credentials.
|
|
271
377
|
*/
|
|
272
|
-
export async function fetchProviderUsage(maxAgeMs = 300000): Promise<ProviderUsage | null> {
|
|
273
|
-
|
|
378
|
+
export async function fetchProviderUsage(maxAgeMs = 300000, provider?: string): Promise<ProviderUsage | null> {
|
|
379
|
+
if (!provider || !QUOTA_PROVIDERS.has(provider)) {
|
|
380
|
+
cachedUsage = null;
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
274
383
|
if (cachedUsage !== null && Date.now() - cachedAt < maxAgeMs) {
|
|
275
384
|
return cachedUsage;
|
|
276
385
|
}
|
|
277
|
-
|
|
278
386
|
try {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (anthropicToken) {
|
|
282
|
-
const base = await fetchAnthropicUsage(anthropicToken);
|
|
283
|
-
const usage: ProviderUsage = {
|
|
284
|
-
providerName: "Claude",
|
|
285
|
-
fiveHourPercent: base.fiveHourPercent,
|
|
286
|
-
fiveHourResetAt: base.fiveHourResetAt,
|
|
287
|
-
weeklyPercent: base.weeklyPercent,
|
|
288
|
-
weeklyResetAt: base.weeklyResetAt,
|
|
289
|
-
};
|
|
387
|
+
const usage = await fetchForProvider(provider);
|
|
388
|
+
if (usage) {
|
|
290
389
|
cachedUsage = usage;
|
|
291
390
|
cachedAt = Date.now();
|
|
292
|
-
return usage;
|
|
293
391
|
}
|
|
294
|
-
|
|
295
|
-
// Try z.ai
|
|
296
|
-
const zaiToken = loadZaiToken();
|
|
297
|
-
if (zaiToken) {
|
|
298
|
-
const usage = await fetchZaiUsage(zaiToken);
|
|
299
|
-
usage.providerName = "z.ai";
|
|
300
|
-
cachedUsage = usage;
|
|
301
|
-
cachedAt = Date.now();
|
|
302
|
-
return usage;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Try Copilot
|
|
306
|
-
const copilotToken = loadCopilotToken();
|
|
307
|
-
if (copilotToken) {
|
|
308
|
-
const monthlyPercent = await fetchCopilotMonthlyPercent(copilotToken);
|
|
309
|
-
if (monthlyPercent !== undefined) {
|
|
310
|
-
const usage: ProviderUsage = {
|
|
311
|
-
providerName: "Copilot",
|
|
312
|
-
fiveHourPercent: 0,
|
|
313
|
-
fiveHourResetAt: null,
|
|
314
|
-
weeklyPercent: monthlyPercent,
|
|
315
|
-
weeklyResetAt: null,
|
|
316
|
-
copilotMonthlyPercent: monthlyPercent,
|
|
317
|
-
};
|
|
318
|
-
cachedUsage = usage;
|
|
319
|
-
cachedAt = Date.now();
|
|
320
|
-
return usage;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// No credentials for any provider
|
|
325
|
-
return null;
|
|
392
|
+
return usage;
|
|
326
393
|
} catch {
|
|
327
|
-
// Network error / timeout / parse error — fail gracefully.
|
|
328
394
|
return null;
|
|
329
395
|
}
|
|
330
396
|
}
|
|
@@ -498,6 +498,17 @@ export function handleSteer(params: TeamToolParamsValue, ctx: TeamContext): PiTe
|
|
|
498
498
|
}
|
|
499
499
|
task.pendingSteers.push(message);
|
|
500
500
|
saveRunTasks(loaded.manifest, loaded.tasks);
|
|
501
|
+
// Real-time steer delivery: write to steering file so child can read immediately
|
|
502
|
+
try {
|
|
503
|
+
const steeringDir = `${loaded.manifest.artifactsRoot}/steering`;
|
|
504
|
+
fs.mkdirSync(steeringDir, { recursive: true });
|
|
505
|
+
fs.appendFileSync(
|
|
506
|
+
`${steeringDir}/${taskId}.jsonl`,
|
|
507
|
+
JSON.stringify({ type: "steer", message, ts: new Date().toISOString() }) + "\n",
|
|
508
|
+
);
|
|
509
|
+
} catch {
|
|
510
|
+
// Best-effort: file write failure doesn't block the steer from pending array
|
|
511
|
+
}
|
|
501
512
|
appendEvent(loaded.manifest.eventsPath, {
|
|
502
513
|
type: "task.steer_queued",
|
|
503
514
|
runId,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
|
|
3
4
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
5
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|
|
5
6
|
export const PI_CREW_INHERIT_PROJECT_CONTEXT_ENV = "PI_CREW_INHERIT_PROJECT_CONTEXT";
|
|
6
7
|
export const PI_CREW_INHERIT_SKILLS_ENV = "PI_CREW_INHERIT_SKILLS";
|
|
8
|
+
const PI_CREW_MAX_OUTPUT_ENV = "PI_CREW_MAX_OUTPUT";
|
|
9
|
+
const PI_CREW_STEERING_FILE_ENV = "PI_CREW_STEERING_FILE";
|
|
7
10
|
|
|
8
11
|
const PROJECT_CONTEXT_HEADER = "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n";
|
|
9
12
|
const SKILLS_HEADER = "\n\nThe following skills provide specialized instructions for specific tasks.";
|
|
@@ -58,6 +61,68 @@ export function rewriteTeamWorkerPrompt(prompt: string, options: { inheritProjec
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export default function registerPiTeamsPromptRuntime(pi: ExtensionAPI): void {
|
|
64
|
+
// ── Feature 1: maxTokens cap ──────────────────────────────────────────
|
|
65
|
+
// Cap output tokens per API call for background workers. Reads
|
|
66
|
+
// PI_CREW_MAX_OUTPUT_TOKENS env (set by pi-args.ts from agent.maxTokens).
|
|
67
|
+
const maxTokensEnv = process.env[PI_CREW_MAX_OUTPUT_ENV];
|
|
68
|
+
const maxTokensCap = maxTokensEnv ? Number.parseInt(maxTokensEnv, 10) : undefined;
|
|
69
|
+
if (maxTokensCap && maxTokensCap > 0) {
|
|
70
|
+
pi.on("before_provider_request", (event) => {
|
|
71
|
+
const payload = event.payload as Record<string, unknown> | undefined;
|
|
72
|
+
if (!payload || typeof payload !== "object") return;
|
|
73
|
+
// Cap both OpenAI-style max_tokens and Anthropic-style max_tokens
|
|
74
|
+
if (typeof payload.max_tokens === "number" && payload.max_tokens > maxTokensCap) {
|
|
75
|
+
payload.max_tokens = maxTokensCap;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Feature 2: real-time steering ──────────────────────────────────────
|
|
81
|
+
// Poll the steering JSONL file for new steer messages. The parent (team
|
|
82
|
+
// tool) writes steers here in real-time; this reader injects them into
|
|
83
|
+
// the active session via pi.sendMessage with deliverAs:"steer".
|
|
84
|
+
const steeringFile = process.env[PI_CREW_STEERING_FILE_ENV];
|
|
85
|
+
if (steeringFile) {
|
|
86
|
+
let lastOffset = 0;
|
|
87
|
+
const pollSteering = (): void => {
|
|
88
|
+
try {
|
|
89
|
+
const stat = fs.statSync(steeringFile, { throwIfNoEntry: false });
|
|
90
|
+
if (!stat || stat.size <= lastOffset) return;
|
|
91
|
+
const fd = fs.openSync(steeringFile, "r");
|
|
92
|
+
try {
|
|
93
|
+
const buf = Buffer.alloc(stat.size - lastOffset);
|
|
94
|
+
fs.readSync(fd, buf, 0, buf.length, lastOffset);
|
|
95
|
+
lastOffset = stat.size;
|
|
96
|
+
const lines = buf.toString("utf8").split("\n").filter(Boolean);
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
try {
|
|
99
|
+
const entry = JSON.parse(line) as { type?: string; message?: string };
|
|
100
|
+
if (entry.type === "steer" && entry.message) {
|
|
101
|
+
pi.sendMessage(
|
|
102
|
+
{ customType: "crew-steer", content: entry.message, display: false },
|
|
103
|
+
{ deliverAs: "steer" },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Malformed line — skip
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
try {
|
|
112
|
+
fs.closeSync(fd);
|
|
113
|
+
} catch {
|
|
114
|
+
/* already closed */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// File doesn't exist yet or read error — will retry next tick
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const timer = setInterval(pollSteering, 500);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Prompt rewriting (existing) ────────────────────────────────────────
|
|
61
126
|
pi.on("before_agent_start", (event) => {
|
|
62
127
|
const inheritProjectContext = readBooleanEnvAny(PI_CREW_INHERIT_PROJECT_CONTEXT_ENV, PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV);
|
|
63
128
|
const inheritSkills = readBooleanEnvAny(PI_CREW_INHERIT_SKILLS_ENV, PI_TEAMS_INHERIT_SKILLS_ENV);
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -238,6 +238,8 @@ export interface ChildPiRunInput {
|
|
|
238
238
|
excludeContextBash?: boolean;
|
|
239
239
|
/** pi session ID for session naming (aligns with pi-crew run ID) */
|
|
240
240
|
sessionId?: string;
|
|
241
|
+
/** Path to steering JSONL file for real-time steer injection. */
|
|
242
|
+
steeringFile?: string;
|
|
241
243
|
/** Run ID for cleanup tracking */
|
|
242
244
|
runId?: string;
|
|
243
245
|
/** Agent ID for cleanup tracking */
|
|
@@ -317,6 +319,8 @@ const BASE_ALLOWLIST: string[] = [
|
|
|
317
319
|
"PI_TEAMS_PI_BIN",
|
|
318
320
|
"PI_TEAMS_MOCK_CHILD_PI",
|
|
319
321
|
"PI_CREW_ALLOW_MOCK",
|
|
322
|
+
"PI_CREW_MAX_OUTPUT",
|
|
323
|
+
"PI_CREW_STEERING_FILE",
|
|
320
324
|
];
|
|
321
325
|
|
|
322
326
|
export function buildChildPiSpawnOptions(cwd: string, env: NodeJS.ProcessEnv, model?: string): SpawnOptions {
|
|
@@ -888,6 +892,8 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
888
892
|
skillPaths: input.skillPaths,
|
|
889
893
|
role: input.role,
|
|
890
894
|
});
|
|
895
|
+
// Pass steering file path to child for real-time steer injection
|
|
896
|
+
if (input.steeringFile) built.env.PI_CREW_STEERING_FILE = input.steeringFile;
|
|
891
897
|
const spawnSpec = getPiSpawnCommand(built.args);
|
|
892
898
|
try {
|
|
893
899
|
return await new Promise<ChildPiRunResult>((resolve) => {
|
package/src/runtime/pi-args.ts
CHANGED
|
@@ -360,6 +360,8 @@ export function buildPiWorkerArgs(input: BuildPiWorkerArgsInput): BuildPiWorkerA
|
|
|
360
360
|
PI_TEAMS_DEPTH: String(parentDepth + 1),
|
|
361
361
|
PI_TEAMS_MAX_DEPTH: String(maxDepth),
|
|
362
362
|
PI_TEAMS_ROLE: input.agent.name,
|
|
363
|
+
// maxTokens cap for background workers — prompt-runtime reads this to cap API output
|
|
364
|
+
...(input.agent.maxTokens ? { PI_CREW_MAX_OUTPUT: String(input.agent.maxTokens) } : {}),
|
|
363
365
|
},
|
|
364
366
|
tempDir,
|
|
365
367
|
};
|
|
@@ -449,6 +449,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
449
449
|
runId: manifest.runId,
|
|
450
450
|
agentId: task.id,
|
|
451
451
|
artifactsRoot: manifest.artifactsRoot,
|
|
452
|
+
steeringFile: `${manifest.artifactsRoot}/steering/${task.id}.jsonl`,
|
|
452
453
|
onSpawn: (pid) => {
|
|
453
454
|
try {
|
|
454
455
|
({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
|