@oh-my-pi/pi-coding-agent 15.11.8 → 15.12.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 +36 -2
- package/dist/cli.js +8083 -7692
- package/dist/types/collab/crypto.d.ts +1 -6
- package/dist/types/collab/guest.d.ts +2 -0
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +14 -1
- package/dist/types/config/settings-schema.d.ts +40 -5
- package/dist/types/export/custom-share.d.ts +1 -2
- package/dist/types/export/html/index.d.ts +39 -1
- package/dist/types/export/share.d.ts +43 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/agent-hub.d.ts +19 -1
- package/dist/types/modes/components/status-line/component.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +7 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -1
- package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/session-observer-registry.d.ts +7 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/codex-auto-reset.d.ts +8 -4
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/task/types.d.ts +9 -0
- package/package.json +13 -14
- package/scripts/build-binary.ts +4 -0
- package/scripts/bundle-dist.ts +4 -0
- package/scripts/generate-share-viewer.ts +34 -0
- package/src/collab/crypto.ts +10 -4
- package/src/collab/guest.ts +31 -2
- package/src/collab/host.ts +73 -11
- package/src/collab/protocol.ts +48 -7
- package/src/commands/join.ts +1 -1
- package/src/config/settings-schema.ts +40 -4
- package/src/config/settings.ts +12 -0
- package/src/export/custom-share.ts +1 -1
- package/src/export/html/index.ts +122 -17
- package/src/export/html/share-loader.js +102 -0
- package/src/export/html/template.css +745 -459
- package/src/export/html/template.html +6 -3
- package/src/export/html/template.js +240 -915
- package/src/export/html/tool-views.generated.js +38 -0
- package/src/export/share.ts +268 -0
- package/src/internal-urls/docs-index.generated.ts +73 -73
- package/src/main.ts +22 -9
- package/src/modes/components/agent-hub.ts +541 -410
- package/src/modes/components/status-line/component.ts +38 -5
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tips.txt +3 -1
- package/src/modes/controllers/command-controller.ts +55 -96
- package/src/modes/controllers/event-controller.ts +45 -16
- package/src/modes/controllers/input-controller.ts +104 -4
- package/src/modes/controllers/selector-controller.ts +11 -15
- package/src/modes/controllers/session-focus-controller.ts +112 -0
- package/src/modes/interactive-mode.ts +44 -2
- package/src/modes/session-observer-registry.ts +11 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +16 -13
- package/src/prompts/tools/job.md +1 -1
- package/src/session/agent-session.ts +65 -7
- package/src/session/codex-auto-reset.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +62 -35
- package/src/task/executor.ts +14 -0
- package/src/task/index.ts +5 -1
- package/src/task/render.ts +76 -5
- package/src/task/types.ts +9 -0
- package/src/tiny/worker.ts +17 -95
- package/src/tools/job.ts +6 -9
- package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
- package/dist/types/export/html/template.generated.d.ts +0 -1
- package/dist/types/export/html/template.macro.d.ts +0 -5
- package/dist/types/tiny/compiled-runtime.d.ts +0 -35
- package/scripts/generate-template.ts +0 -33
- package/src/bun-imports.d.ts +0 -28
- package/src/export/html/template.generated.ts +0 -2
- package/src/export/html/template.macro.ts +0 -25
- package/src/tiny/compiled-runtime.ts +0 -179
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
* eligibility off exact limit ids (`openai-codex:primary` /
|
|
25
25
|
* `openai-codex:secondary`) and `usedFraction`, never off `status`.
|
|
26
26
|
*
|
|
27
|
-
* ANTI-WASTE GATES (in evaluation order): the policy must be
|
|
28
|
-
*
|
|
27
|
+
* ANTI-WASTE GATES (in evaluation order): the policy must not be set to "no";
|
|
28
|
+
* the active model must be Codex (not Spark — a Spark block lives on a
|
|
29
29
|
* separate meter and it is unknown whether a credit even resets it); a fresh
|
|
30
30
|
* usage report for the active account must confirm `limitReached`; the WEEKLY
|
|
31
31
|
* (secondary) window must be genuinely exhausted — a 5h-only block self-heals
|
|
@@ -38,12 +38,14 @@
|
|
|
38
38
|
* read-only views are passed in so the predicate itself stays deterministic.
|
|
39
39
|
*/
|
|
40
40
|
import type { OAuthAccountIdentity, ResetCreditTarget, UsageReport } from "@oh-my-pi/pi-ai";
|
|
41
|
+
import type { CodexAutoRedeemMode } from "../config/settings-schema";
|
|
41
42
|
import { reportMatchesActiveAccount } from "../slash-commands/helpers/active-oauth-account";
|
|
42
43
|
|
|
43
44
|
/** Weekly window counts as exhausted at `usedFraction >= 0.999` (used_percent >= 99.9). */
|
|
44
45
|
export const WEEKLY_EXHAUSTED_MIN_FRACTION = 0.999;
|
|
45
46
|
/** A weekly reset can never be more than one window length (7d) away; +1h slack for skew. */
|
|
46
47
|
export const MAX_PLAUSIBLE_REMAINING_MS = 7 * 24 * 3_600_000 + 60 * 60_000;
|
|
48
|
+
|
|
47
49
|
/** Report must be no older than the 5-min usage cache TTL plus slack. */
|
|
48
50
|
export const REPORT_FRESHNESS_MS = 10 * 60_000;
|
|
49
51
|
/** Per-account cooldown that catches blockKey drift across a minute boundary. */
|
|
@@ -51,6 +53,14 @@ export const ATTEMPT_COOLDOWN_MS = 60_000;
|
|
|
51
53
|
/** Minute bucket for blockKey, absorbing `reset_after_seconds`-derived jitter. */
|
|
52
54
|
export const DEBOUNCE_BUCKET_MS = 60_000;
|
|
53
55
|
|
|
56
|
+
export function shouldEvaluateCodexAutoRedeem(mode: CodexAutoRedeemMode): boolean {
|
|
57
|
+
return mode !== "no";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shouldPromptCodexAutoRedeem(mode: CodexAutoRedeemMode): boolean {
|
|
61
|
+
return mode === "unset";
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export type CodexAutoRedeemSkipReason =
|
|
55
65
|
| "disabled"
|
|
56
66
|
| "wrong-provider"
|
|
@@ -83,16 +93,18 @@ export interface CodexAutoRedeemInput {
|
|
|
83
93
|
lastAttemptAtByAccount: ReadonlyMap<string, number>;
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
export interface CodexAutoRedeemRedeemDecision {
|
|
97
|
+
redeem: true;
|
|
98
|
+
target: ResetCreditTarget;
|
|
99
|
+
accountKey: string;
|
|
100
|
+
blockKey: string;
|
|
101
|
+
weeklyResetAtMs: number;
|
|
102
|
+
remainingMs: number;
|
|
103
|
+
availableCount: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
export type CodexAutoRedeemDecision =
|
|
87
|
-
|
|
|
88
|
-
redeem: true;
|
|
89
|
-
target: ResetCreditTarget;
|
|
90
|
-
accountKey: string;
|
|
91
|
-
blockKey: string;
|
|
92
|
-
weeklyResetAtMs: number;
|
|
93
|
-
remainingMs: number;
|
|
94
|
-
availableCount: number;
|
|
95
|
-
}
|
|
107
|
+
| CodexAutoRedeemRedeemDecision
|
|
96
108
|
| { redeem: false; reason: CodexAutoRedeemSkipReason };
|
|
97
109
|
|
|
98
110
|
/** Trimmed lowercase, or undefined when blank. Mirrors `normalizeIdentityValue` in active-oauth-account.ts. */
|
|
@@ -4,8 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
5
5
|
import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
|
|
6
6
|
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
7
|
-
import {
|
|
8
|
-
import { $ } from "bun";
|
|
7
|
+
import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils";
|
|
9
8
|
import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
|
|
10
9
|
import { CollabHost } from "../collab/host";
|
|
11
10
|
import type { SettingPath, SettingValue } from "../config/settings";
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
resolveActiveProjectRegistryPath,
|
|
16
15
|
resolveOrDefaultProjectRegistryPath,
|
|
17
16
|
} from "../discovery/helpers.js";
|
|
17
|
+
import { shareSession } from "../export/share";
|
|
18
18
|
import { PluginManager } from "../extensibility/plugins";
|
|
19
19
|
import {
|
|
20
20
|
getInstalledPluginsRegistryPath,
|
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
MarketplaceManager,
|
|
25
25
|
} from "../extensibility/plugins/marketplace";
|
|
26
26
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
27
|
+
import { theme } from "../modes/theme/theme";
|
|
27
28
|
import type { InteractiveModeContext } from "../modes/types";
|
|
28
29
|
import type { AgentSession, FreshSessionResult } from "../session/agent-session";
|
|
29
30
|
import { formatShakeSummary, type ShakeMode } from "../session/shake-types";
|
|
31
|
+
import { urlHyperlinkAlways } from "../tui";
|
|
30
32
|
import { getChangelogPath, parseChangelog } from "../utils/changelog";
|
|
31
33
|
import { buildContextReportText } from "./helpers/context-report";
|
|
32
34
|
import { formatDuration } from "./helpers/format";
|
|
@@ -60,6 +62,30 @@ function refreshStatusLine(ctx: InteractiveModeContext): void {
|
|
|
60
62
|
ctx.ui.requestRender();
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/** Scheme-less display form of a browser deep link: accent + underline, OSC-8 linked to the full URL. */
|
|
66
|
+
function collabWebLinkClickable(webLink: string): string {
|
|
67
|
+
const display = theme.fg("accent", `\x1b[4m${webLink.replace(/^https?:\/\//, "")}\x1b[24m`);
|
|
68
|
+
return urlHyperlinkAlways(webLink, display);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Join hint printed by /collab: compact terminal link + clickable browser deep link. */
|
|
72
|
+
function collabLinkHint(host: CollabHost, heading: string, view = false): string {
|
|
73
|
+
const bullet = theme.fg("accent", theme.format.bullet);
|
|
74
|
+
const link = view ? host.viewLink : host.link;
|
|
75
|
+
const webLink = view ? host.webViewLink : host.webLink;
|
|
76
|
+
return [
|
|
77
|
+
theme.fg("success", heading),
|
|
78
|
+
` ${bullet} ${theme.fg("muted", view ? "Watch from another terminal:" : "Join from another terminal:")} ${APP_NAME} join "${link}"`,
|
|
79
|
+
` ${bullet} ${theme.fg("muted", "or any web browser:")} ${collabWebLinkClickable(webLink)}`,
|
|
80
|
+
theme.fg(
|
|
81
|
+
"dim",
|
|
82
|
+
view
|
|
83
|
+
? "Anyone with this link can watch the session but cannot prompt the agent."
|
|
84
|
+
: "Anyone with the link can read the session and prompt the agent. Read-only link: /collab view",
|
|
85
|
+
),
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
function formatFreshSessionResult(result: FreshSessionResult): string {
|
|
64
90
|
const stateLabel = result.closedProviderSessions === 1 ? "provider state" : "provider states";
|
|
65
91
|
return `Fresh provider session started (${result.closedProviderSessions} ${stateLabel} pruned).`;
|
|
@@ -414,31 +440,21 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
414
440
|
},
|
|
415
441
|
{
|
|
416
442
|
name: "share",
|
|
417
|
-
description: "Share session
|
|
443
|
+
description: "Share session via an encrypted link (secret gist or share server)",
|
|
418
444
|
handle: async (_command, runtime) => {
|
|
419
|
-
const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
|
|
420
445
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
const
|
|
427
|
-
if (result.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
runtime,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
const gistUrl = result.stdout.toString("utf-8").trim();
|
|
434
|
-
const gistId = gistUrl.split("/").pop();
|
|
435
|
-
if (!gistId) return usage("Failed to parse gist ID from gh output", runtime);
|
|
436
|
-
await runtime.output(`Share URL: https://gistpreview.github.io/?${gistId}\nGist: ${gistUrl}`);
|
|
446
|
+
const result = await shareSession(runtime.sessionManager, {
|
|
447
|
+
serverUrl: runtime.settings.get("share.serverUrl"),
|
|
448
|
+
state: runtime.session.state,
|
|
449
|
+
obfuscator: runtime.settings.get("share.redactSecrets") ? runtime.session.obfuscator : undefined,
|
|
450
|
+
});
|
|
451
|
+
const lines = [`Share URL: ${result.url}`];
|
|
452
|
+
if (result.gistUrl) lines.push(`Gist: ${result.gistUrl}`);
|
|
453
|
+
if (result.truncated) lines.push("Note: large content was trimmed to fit the share size limit.");
|
|
454
|
+
await runtime.output(lines.join("\n"));
|
|
437
455
|
return commandConsumed();
|
|
438
|
-
} catch {
|
|
439
|
-
return usage(
|
|
440
|
-
} finally {
|
|
441
|
-
await fs.rm(tmpFile, { force: true }).catch(() => {});
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return usage(`Failed to share session: ${errorMessage(err)}`, runtime);
|
|
442
458
|
}
|
|
443
459
|
},
|
|
444
460
|
handleTui: async (_command, runtime) => {
|
|
@@ -449,7 +465,12 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
449
465
|
{
|
|
450
466
|
name: "collab",
|
|
451
467
|
description: "Share this session live via a relay",
|
|
452
|
-
inlineHint: "[start|stop|status] [relayUrl]",
|
|
468
|
+
inlineHint: "[start|view|stop|status] [relayUrl]",
|
|
469
|
+
subcommands: [
|
|
470
|
+
{ name: "view", description: "Share a read-only link (guests can watch, not prompt)" },
|
|
471
|
+
{ name: "status", description: "Show link + participants" },
|
|
472
|
+
{ name: "stop", description: "Stop sharing" },
|
|
473
|
+
],
|
|
453
474
|
allowArgs: true,
|
|
454
475
|
handleTui: async (command, runtime) => {
|
|
455
476
|
const ctx = runtime.ctx;
|
|
@@ -467,10 +488,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
467
488
|
}
|
|
468
489
|
if (first === "status") {
|
|
469
490
|
if (ctx.collabHost) {
|
|
470
|
-
const names = ctx.collabHost.participants.map(p =>
|
|
471
|
-
|
|
491
|
+
const names = ctx.collabHost.participants.map(p =>
|
|
492
|
+
p.role === "host" ? `${p.name} (host)` : p.readOnly ? `${p.name} (view-only)` : p.name,
|
|
493
|
+
);
|
|
494
|
+
ctx.showStatus(`Collab: ${names.join(", ")} — ${collabWebLinkClickable(ctx.collabHost.webLink)}`);
|
|
472
495
|
} else if (ctx.collabGuest) {
|
|
473
|
-
ctx.showStatus(
|
|
496
|
+
ctx.showStatus(
|
|
497
|
+
ctx.collabGuest.readOnly
|
|
498
|
+
? "In a collab session as a read-only guest (/leave to exit)"
|
|
499
|
+
: "In a collab session as a guest (/leave to exit)",
|
|
500
|
+
);
|
|
474
501
|
} else {
|
|
475
502
|
ctx.showStatus("Not in a collab session");
|
|
476
503
|
}
|
|
@@ -480,11 +507,15 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
480
507
|
ctx.showError("Already in a collab session as a guest (/leave first)");
|
|
481
508
|
return;
|
|
482
509
|
}
|
|
510
|
+
const view = first === "view";
|
|
483
511
|
if (ctx.collabHost) {
|
|
484
|
-
ctx.
|
|
512
|
+
ctx.showStatus(
|
|
513
|
+
collabLinkHint(ctx.collabHost, view ? "Read-only collab link" : "Collab session active", view),
|
|
514
|
+
{ dim: false },
|
|
515
|
+
);
|
|
485
516
|
return;
|
|
486
517
|
}
|
|
487
|
-
const explicitUrl = first === "start" ? args.slice(
|
|
518
|
+
const explicitUrl = first === "start" || view ? args.slice(first.length).trim() : args;
|
|
488
519
|
const relayInput = explicitUrl || ctx.settings.get("collab.relayUrl") || "";
|
|
489
520
|
if (!relayInput) {
|
|
490
521
|
ctx.showError(
|
|
@@ -502,11 +533,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
502
533
|
return;
|
|
503
534
|
}
|
|
504
535
|
ctx.collabHost = host;
|
|
505
|
-
ctx.session
|
|
506
|
-
"info",
|
|
507
|
-
`Collab link: ${host.link}\nAnyone with this link can read the session and prompt the agent.`,
|
|
508
|
-
"collab",
|
|
509
|
-
);
|
|
536
|
+
ctx.showStatus(collabLinkHint(host, "Collab session started!", view), { dim: false });
|
|
510
537
|
},
|
|
511
538
|
},
|
|
512
539
|
{
|
package/src/task/executor.ts
CHANGED
|
@@ -195,6 +195,13 @@ export interface ExecutorOptions {
|
|
|
195
195
|
index: number;
|
|
196
196
|
id: string;
|
|
197
197
|
parentToolCallId?: string;
|
|
198
|
+
/**
|
|
199
|
+
* Spawn runs as a detached background job (parent turn not blocked on it).
|
|
200
|
+
* Rides the subagent lifecycle/progress payloads so HUD-style surfaces can
|
|
201
|
+
* skip spawns the transcript already renders inline. See
|
|
202
|
+
* {@link SubagentLifecyclePayload.detached}.
|
|
203
|
+
*/
|
|
204
|
+
detached?: boolean;
|
|
198
205
|
modelOverride?: string | string[];
|
|
199
206
|
/**
|
|
200
207
|
* Active model selector of the parent session, used as an auth-aware fallback
|
|
@@ -656,6 +663,7 @@ interface RunMonitorArgs {
|
|
|
656
663
|
onProgress?: (progress: AgentProgress) => void;
|
|
657
664
|
eventBus?: EventBus;
|
|
658
665
|
parentToolCallId?: string;
|
|
666
|
+
detached?: boolean;
|
|
659
667
|
sessionFile?: string;
|
|
660
668
|
/** Soft assistant-request budget; 0 disables the guard. */
|
|
661
669
|
softRequestBudget: number;
|
|
@@ -836,6 +844,7 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
836
844
|
agentSource: agent.source,
|
|
837
845
|
task,
|
|
838
846
|
parentToolCallId: args.parentToolCallId,
|
|
847
|
+
detached: args.detached,
|
|
839
848
|
assignment,
|
|
840
849
|
progress: { ...progress },
|
|
841
850
|
sessionFile: args.sessionFile,
|
|
@@ -1413,6 +1422,7 @@ interface FinalizeRunArgs {
|
|
|
1413
1422
|
artifactsDir?: string;
|
|
1414
1423
|
eventBus?: EventBus;
|
|
1415
1424
|
parentToolCallId?: string;
|
|
1425
|
+
detached?: boolean;
|
|
1416
1426
|
sessionFile?: string;
|
|
1417
1427
|
startTime: number;
|
|
1418
1428
|
}
|
|
@@ -1511,6 +1521,7 @@ async function finalizeRunResult(args: FinalizeRunArgs): Promise<SingleResult> {
|
|
|
1511
1521
|
id,
|
|
1512
1522
|
agent: agent.name,
|
|
1513
1523
|
parentToolCallId: args.parentToolCallId,
|
|
1524
|
+
detached: args.detached,
|
|
1514
1525
|
agentSource: agent.source,
|
|
1515
1526
|
description: args.description,
|
|
1516
1527
|
status: progress.status as "completed" | "failed" | "aborted",
|
|
@@ -1677,6 +1688,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1677
1688
|
onProgress,
|
|
1678
1689
|
eventBus: options.eventBus,
|
|
1679
1690
|
parentToolCallId: options.parentToolCallId,
|
|
1691
|
+
detached: options.detached,
|
|
1680
1692
|
sessionFile: subtaskSessionFile,
|
|
1681
1693
|
softRequestBudget,
|
|
1682
1694
|
maxRuntimeMs,
|
|
@@ -1921,6 +1933,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1921
1933
|
id,
|
|
1922
1934
|
agent: agent.name,
|
|
1923
1935
|
parentToolCallId: options.parentToolCallId,
|
|
1936
|
+
detached: options.detached,
|
|
1924
1937
|
agentSource: agent.source,
|
|
1925
1938
|
description: options.description,
|
|
1926
1939
|
status: "started",
|
|
@@ -2127,6 +2140,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2127
2140
|
artifactsDir: options.artifactsDir,
|
|
2128
2141
|
eventBus: options.eventBus,
|
|
2129
2142
|
parentToolCallId: options.parentToolCallId,
|
|
2143
|
+
detached: options.detached,
|
|
2130
2144
|
sessionFile: subtaskSessionFile,
|
|
2131
2145
|
startTime,
|
|
2132
2146
|
});
|
package/src/task/index.ts
CHANGED
|
@@ -705,6 +705,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
705
705
|
undefined,
|
|
706
706
|
agentId,
|
|
707
707
|
progress.index,
|
|
708
|
+
true,
|
|
708
709
|
);
|
|
709
710
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
710
711
|
const singleResult = result.details?.results[0];
|
|
@@ -897,8 +898,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
897
898
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
898
899
|
preAllocatedId?: string,
|
|
899
900
|
spawnIndex = 0,
|
|
901
|
+
detached = false,
|
|
900
902
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
901
|
-
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex);
|
|
903
|
+
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex, detached);
|
|
902
904
|
}
|
|
903
905
|
|
|
904
906
|
/** Spawn a fresh subagent and run it to completion. */
|
|
@@ -909,6 +911,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
909
911
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
910
912
|
preAllocatedId?: string,
|
|
911
913
|
spawnIndex = 0,
|
|
914
|
+
detached = false,
|
|
912
915
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
913
916
|
const startTime = Date.now();
|
|
914
917
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
@@ -1145,6 +1148,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1145
1148
|
description: params.description,
|
|
1146
1149
|
index: spawnIndex,
|
|
1147
1150
|
parentToolCallId: toolCallId,
|
|
1151
|
+
detached,
|
|
1148
1152
|
id: agentId,
|
|
1149
1153
|
taskDepth,
|
|
1150
1154
|
modelOverride,
|
package/src/task/render.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
|
15
15
|
import {
|
|
16
16
|
formatBadge,
|
|
17
17
|
formatDuration,
|
|
18
|
+
formatExpandHint,
|
|
18
19
|
formatMoreItems,
|
|
19
20
|
formatStatusIcon,
|
|
20
21
|
replaceTabs,
|
|
@@ -542,6 +543,12 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
|
|
|
542
543
|
return lines;
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Agent rows shown per collapsed task list; the rest fold into a single
|
|
548
|
+
* `… N more agents` summary line (expand uncaps).
|
|
549
|
+
*/
|
|
550
|
+
const COLLAPSED_AGENT_LIMIT = 4;
|
|
551
|
+
|
|
545
552
|
/**
|
|
546
553
|
* Render the per-item list (`id` + ui `description`) for a batch call's
|
|
547
554
|
* streaming preview. The args stream in token by token, so the array grows
|
|
@@ -552,7 +559,7 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): strin
|
|
|
552
559
|
if (!Array.isArray(tasks) || tasks.length === 0) return [];
|
|
553
560
|
|
|
554
561
|
const bullet = theme.fg("dim", "•");
|
|
555
|
-
const cap = Math.min(tasks.length,
|
|
562
|
+
const cap = Math.min(tasks.length, COLLAPSED_AGENT_LIMIT);
|
|
556
563
|
const lines: string[] = [];
|
|
557
564
|
for (let i = 0; i < cap; i++) {
|
|
558
565
|
const task = tasks[i] as Partial<TaskItem> | undefined;
|
|
@@ -1170,6 +1177,53 @@ function orderResultsForDisplay(results: readonly SingleResult[]): SingleResult[
|
|
|
1170
1177
|
return [...results].sort((a, b) => a.durationMs - b.durationMs || a.index - b.index);
|
|
1171
1178
|
}
|
|
1172
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* Summary line for progress rows folded away by the collapsed cap: per-status
|
|
1182
|
+
* counts plus the expand hint, e.g. `… 21 more agents (18 pending · 3 done)`.
|
|
1183
|
+
*/
|
|
1184
|
+
function formatHiddenProgressLine(hidden: readonly AgentProgress[], theme: Theme): string {
|
|
1185
|
+
const counts: Record<AgentProgress["status"], number> = {
|
|
1186
|
+
pending: 0,
|
|
1187
|
+
running: 0,
|
|
1188
|
+
completed: 0,
|
|
1189
|
+
failed: 0,
|
|
1190
|
+
aborted: 0,
|
|
1191
|
+
};
|
|
1192
|
+
for (const p of hidden) counts[p.status]++;
|
|
1193
|
+
const parts: string[] = [];
|
|
1194
|
+
if (counts.completed > 0) parts.push(theme.fg("dim", `${counts.completed} done`));
|
|
1195
|
+
if (counts.running > 0) parts.push(theme.fg("dim", `${counts.running} running`));
|
|
1196
|
+
if (counts.pending > 0) parts.push(theme.fg("dim", `${counts.pending} pending`));
|
|
1197
|
+
if (counts.failed > 0) parts.push(theme.fg("error", `${counts.failed} failed`));
|
|
1198
|
+
if (counts.aborted > 0) parts.push(theme.fg("error", `${counts.aborted} aborted`));
|
|
1199
|
+
const breakdown =
|
|
1200
|
+
parts.length > 0
|
|
1201
|
+
? `${theme.fg("dim", " (")}${parts.join(theme.fg("dim", theme.sep.dot))}${theme.fg("dim", ")")}`
|
|
1202
|
+
: "";
|
|
1203
|
+
const hint = formatExpandHint(theme, false, true);
|
|
1204
|
+
return `${theme.fg("dim", formatMoreItems(hidden.length, "agent"))}${breakdown}${hint ? ` ${hint}` : ""}`;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Pick the agent rows that stay visible when a finalized batch is collapsed:
|
|
1209
|
+
* problem rows (aborted/failed/merge-failed) claim slots first so they are
|
|
1210
|
+
* never folded away, then fastest finishers fill the remainder. The pick is
|
|
1211
|
+
* filtered out of the display order, so visible rows keep the expanded layout.
|
|
1212
|
+
*/
|
|
1213
|
+
function selectCollapsedResults(ordered: readonly SingleResult[]): readonly SingleResult[] {
|
|
1214
|
+
if (ordered.length <= COLLAPSED_AGENT_LIMIT) return ordered;
|
|
1215
|
+
const picked = new Set<SingleResult>();
|
|
1216
|
+
for (const result of ordered) {
|
|
1217
|
+
if (picked.size >= COLLAPSED_AGENT_LIMIT) break;
|
|
1218
|
+
if (result.aborted || result.exitCode !== 0 || result.error) picked.add(result);
|
|
1219
|
+
}
|
|
1220
|
+
for (const result of ordered) {
|
|
1221
|
+
if (picked.size >= COLLAPSED_AGENT_LIMIT) break;
|
|
1222
|
+
picked.add(result);
|
|
1223
|
+
}
|
|
1224
|
+
return ordered.filter(result => picked.has(result));
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1173
1227
|
/**
|
|
1174
1228
|
* Render the tool result.
|
|
1175
1229
|
*/
|
|
@@ -1248,13 +1302,30 @@ export function renderResult(
|
|
|
1248
1302
|
const shouldRenderProgress =
|
|
1249
1303
|
Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
|
|
1250
1304
|
if (shouldRenderProgress && details.progress) {
|
|
1251
|
-
orderProgressForDisplay(details.progress)
|
|
1305
|
+
const ordered = orderProgressForDisplay(details.progress);
|
|
1306
|
+
// Collapsed view keeps the live edge: finished rows sort to the top of
|
|
1307
|
+
// the display order, so folding from the top keeps running/pending
|
|
1308
|
+
// agents (and their current-tool lines) visible while one summary line
|
|
1309
|
+
// stands in for everything above it.
|
|
1310
|
+
const visible = expanded ? ordered : ordered.slice(Math.max(0, ordered.length - COLLAPSED_AGENT_LIMIT));
|
|
1311
|
+
if (visible.length < ordered.length) {
|
|
1312
|
+
lines.push(formatHiddenProgressLine(ordered.slice(0, ordered.length - visible.length), theme));
|
|
1313
|
+
}
|
|
1314
|
+
for (const progress of visible) {
|
|
1252
1315
|
lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
|
|
1253
|
-
}
|
|
1316
|
+
}
|
|
1254
1317
|
} else if (details.results && details.results.length > 0) {
|
|
1255
|
-
orderResultsForDisplay(details.results)
|
|
1318
|
+
const ordered = orderResultsForDisplay(details.results);
|
|
1319
|
+
const visible = expanded ? ordered : selectCollapsedResults(ordered);
|
|
1320
|
+
for (const res of visible) {
|
|
1256
1321
|
lines.push(...renderAgentResult(res, "", " ", expanded, theme));
|
|
1257
|
-
}
|
|
1322
|
+
}
|
|
1323
|
+
if (visible.length < ordered.length) {
|
|
1324
|
+
const hint = formatExpandHint(theme, false, true);
|
|
1325
|
+
lines.push(
|
|
1326
|
+
`${theme.fg("dim", formatMoreItems(ordered.length - visible.length, "agent"))}${hint ? ` ${hint}` : ""}`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1258
1329
|
|
|
1259
1330
|
const abortedCount = details.results.filter(r => r.aborted).length;
|
|
1260
1331
|
const mergeFailedCount = details.results.filter(r => !r.aborted && r.exitCode === 0 && r.error).length;
|
package/src/task/types.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface SubagentProgressPayload {
|
|
|
45
45
|
assignment?: string;
|
|
46
46
|
progress: AgentProgress;
|
|
47
47
|
sessionFile?: string;
|
|
48
|
+
/** See {@link SubagentLifecyclePayload.detached}. */
|
|
49
|
+
detached?: boolean;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** Payload emitted on TASK_SUBAGENT_EVENT_CHANNEL */
|
|
@@ -63,6 +65,13 @@ export interface SubagentLifecyclePayload {
|
|
|
63
65
|
sessionFile?: string;
|
|
64
66
|
parentToolCallId?: string;
|
|
65
67
|
index: number;
|
|
68
|
+
/**
|
|
69
|
+
* Spawn runs as a detached background job: the parent turn keeps working
|
|
70
|
+
* while this agent runs. Sync task spawns (parent blocked on the call) and
|
|
71
|
+
* eval `agent()` bridge spawns (rendered inside their eval cell) leave this
|
|
72
|
+
* unset — surfaces like the subagent HUD only list detached spawns.
|
|
73
|
+
*/
|
|
74
|
+
detached?: boolean;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/**
|
package/src/tiny/worker.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as fs from "node:fs/promises";
|
|
2
1
|
import { createRequire } from "node:module";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import type {
|
|
@@ -7,10 +6,16 @@ import type {
|
|
|
7
6
|
TextGenerationStringOutput,
|
|
8
7
|
StoppingCriteria as TransformersStoppingCriteria,
|
|
9
8
|
} from "@huggingface/transformers";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
ensureRuntimeInstalled,
|
|
11
|
+
getTinyModelsCacheDir,
|
|
12
|
+
installRuntimeModuleResolver,
|
|
13
|
+
isCompiledBinary,
|
|
14
|
+
prompt,
|
|
15
|
+
resolveRuntimeModule,
|
|
16
|
+
} from "@oh-my-pi/pi-utils";
|
|
11
17
|
import packageJson from "../../package.json" with { type: "json" };
|
|
12
18
|
import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with { type: "text" };
|
|
13
|
-
import { installRuntimeModuleResolver, resolveRuntimeModule } from "./compiled-runtime";
|
|
14
19
|
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
|
|
15
20
|
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
|
|
16
21
|
import {
|
|
@@ -31,8 +36,6 @@ const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
|
|
|
31
36
|
const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
32
37
|
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
|
|
33
38
|
const sourceRequire = createRequire(import.meta.url);
|
|
34
|
-
const INSTALL_LOCK_ATTEMPTS = 240;
|
|
35
|
-
const INSTALL_LOCK_SLEEP_MS = 250;
|
|
36
39
|
|
|
37
40
|
const tinyModelDevicePreference = resolveTinyModelDevicePreference();
|
|
38
41
|
const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
|
|
@@ -97,10 +100,6 @@ function errorText(error: unknown): string {
|
|
|
97
100
|
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
function isErrnoCode(error: unknown, code: string): boolean {
|
|
101
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
103
|
function sendLog(
|
|
105
104
|
transport: TinyTitleTransport,
|
|
106
105
|
level: "debug" | "warn" | "error",
|
|
@@ -118,69 +117,6 @@ function getTinyTitleRuntimeDir(): string {
|
|
|
118
117
|
);
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
async function acquireInstallLock(runtimeDir: string): Promise<() => Promise<void>> {
|
|
122
|
-
const lockDir = `${runtimeDir}.lock`;
|
|
123
|
-
await fs.mkdir(path.dirname(lockDir), { recursive: true });
|
|
124
|
-
for (let attempt = 0; attempt < INSTALL_LOCK_ATTEMPTS; attempt++) {
|
|
125
|
-
try {
|
|
126
|
-
await fs.mkdir(lockDir);
|
|
127
|
-
return async () => {
|
|
128
|
-
await fs.rm(lockDir, { recursive: true, force: true });
|
|
129
|
-
};
|
|
130
|
-
} catch (error) {
|
|
131
|
-
if (!isErrnoCode(error, "EEXIST")) throw error;
|
|
132
|
-
await Bun.sleep(INSTALL_LOCK_SLEEP_MS);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
throw new Error(`Timed out waiting for tiny title runtime install lock: ${lockDir}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function isCompiledRuntimeInstalled(runtimeDir: string): Promise<boolean> {
|
|
139
|
-
return Bun.file(path.join(runtimeDir, "node_modules", "@huggingface", "transformers", "package.json")).exists();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function writeRuntimeManifest(runtimeDir: string): Promise<void> {
|
|
143
|
-
await fs.mkdir(runtimeDir, { recursive: true });
|
|
144
|
-
await Bun.write(
|
|
145
|
-
path.join(runtimeDir, "package.json"),
|
|
146
|
-
`${JSON.stringify(
|
|
147
|
-
{
|
|
148
|
-
private: true,
|
|
149
|
-
type: "module",
|
|
150
|
-
dependencies: {
|
|
151
|
-
[TRANSFORMERS_PACKAGE]: getTransformersVersionSpec(),
|
|
152
|
-
},
|
|
153
|
-
trustedDependencies: ["onnxruntime-node"],
|
|
154
|
-
},
|
|
155
|
-
null,
|
|
156
|
-
"\t",
|
|
157
|
-
)}\n`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
162
|
-
if (!stream) return "";
|
|
163
|
-
return new Response(stream).text();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
167
|
-
const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
|
|
168
|
-
env: { ...Bun.env, BUN_BE_BUN: "1" },
|
|
169
|
-
stdout: "pipe",
|
|
170
|
-
stderr: "pipe",
|
|
171
|
-
});
|
|
172
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
173
|
-
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
174
|
-
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
175
|
-
proc.exited,
|
|
176
|
-
]);
|
|
177
|
-
if (exitCode === 0) return;
|
|
178
|
-
const output = `${stdout}\n${stderr}`.trim();
|
|
179
|
-
throw new Error(
|
|
180
|
-
`Failed to install tiny title runtime with ${process.execPath} install (exit ${exitCode}): ${output}`,
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
120
|
function sendRuntimeInstallProgress(
|
|
185
121
|
transport: TinyTitleTransport,
|
|
186
122
|
requestId: string,
|
|
@@ -198,28 +134,6 @@ function sendRuntimeInstallProgress(
|
|
|
198
134
|
});
|
|
199
135
|
}
|
|
200
136
|
|
|
201
|
-
async function ensureCompiledTransformersRuntime(
|
|
202
|
-
transport: TinyTitleTransport,
|
|
203
|
-
requestId: string,
|
|
204
|
-
modelKey: TinyLocalModelKey,
|
|
205
|
-
): Promise<string> {
|
|
206
|
-
const runtimeDir = getTinyTitleRuntimeDir();
|
|
207
|
-
if (await isCompiledRuntimeInstalled(runtimeDir)) return runtimeDir;
|
|
208
|
-
|
|
209
|
-
sendRuntimeInstallProgress(transport, requestId, modelKey, "initiate");
|
|
210
|
-
const releaseLock = await acquireInstallLock(runtimeDir);
|
|
211
|
-
try {
|
|
212
|
-
if (await isCompiledRuntimeInstalled(runtimeDir)) return runtimeDir;
|
|
213
|
-
await writeRuntimeManifest(runtimeDir);
|
|
214
|
-
sendRuntimeInstallProgress(transport, requestId, modelKey, "download");
|
|
215
|
-
await runRuntimeInstall(runtimeDir);
|
|
216
|
-
sendRuntimeInstallProgress(transport, requestId, modelKey, "done");
|
|
217
|
-
return runtimeDir;
|
|
218
|
-
} finally {
|
|
219
|
-
await releaseLock();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
137
|
/**
|
|
224
138
|
* Prepare the freshly-installed compiled runtime for loading: stub `sharp`
|
|
225
139
|
* (the tiny models are text-generation only, so the native image pipeline is
|
|
@@ -252,7 +166,15 @@ async function loadTransformers(
|
|
|
252
166
|
if (transformersRuntime) return transformersRuntime;
|
|
253
167
|
transformersRuntime = (async () => {
|
|
254
168
|
if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as TransformersRuntime);
|
|
255
|
-
const runtimeDir = await
|
|
169
|
+
const runtimeDir = await ensureRuntimeInstalled({
|
|
170
|
+
runtimeDir: getTinyTitleRuntimeDir(),
|
|
171
|
+
install: {
|
|
172
|
+
dependencies: { [TRANSFORMERS_PACKAGE]: getTransformersVersionSpec() },
|
|
173
|
+
trustedDependencies: ["onnxruntime-node"],
|
|
174
|
+
},
|
|
175
|
+
probePackage: TRANSFORMERS_PACKAGE,
|
|
176
|
+
onPhase: phase => sendRuntimeInstallProgress(transport, requestId, modelKey, phase),
|
|
177
|
+
});
|
|
256
178
|
const entry = await prepareCompiledRuntime(runtimeDir);
|
|
257
179
|
const require_ = createRequire(entry);
|
|
258
180
|
return configureTransformers(require_(entry) as TransformersRuntime);
|
package/src/tools/job.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { ToolError } from "./tool-errors";
|
|
25
25
|
|
|
26
26
|
const jobSchema = z.object({
|
|
27
|
-
poll: z.array(z.string()).optional().describe("job ids to wait for"),
|
|
27
|
+
poll: z.array(z.string()).optional().describe("job ids to wait for; omit to wait on all running jobs"),
|
|
28
28
|
cancel: z.array(z.string()).optional().describe("job ids to cancel"),
|
|
29
29
|
list: z.boolean().optional().describe("snapshot all jobs"),
|
|
30
30
|
});
|
|
@@ -512,14 +512,11 @@ export const jobToolRenderer = {
|
|
|
512
512
|
itemType: "job",
|
|
513
513
|
renderItem: job => {
|
|
514
514
|
const lines: string[] = [];
|
|
515
|
-
const icon =
|
|
516
|
-
job.status
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
uiTheme,
|
|
521
|
-
job.status === "running" ? options.spinnerFrame : undefined,
|
|
522
|
-
);
|
|
515
|
+
const icon = formatStatusIcon(
|
|
516
|
+
statusToIcon(job.status),
|
|
517
|
+
uiTheme,
|
|
518
|
+
job.status === "running" ? options.spinnerFrame : undefined,
|
|
519
|
+
);
|
|
523
520
|
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
524
521
|
// Task jobs label themselves with their agent id, which is also
|
|
525
522
|
// the job id — drop the id column instead of stuttering it twice.
|
|
Binary file
|