@tintinweb/pi-subagents 0.6.2 → 0.7.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 +28 -0
- package/README.md +54 -10
- package/dist/agent-manager.d.ts +23 -1
- package/dist/agent-manager.js +33 -2
- package/dist/agent-runner.d.ts +27 -0
- package/dist/agent-runner.js +35 -17
- package/dist/default-agents.js +2 -9
- package/dist/index.js +199 -50
- package/dist/schedule-store.d.ts +36 -0
- package/dist/schedule-store.js +144 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +338 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +5 -0
- package/dist/types.d.ts +46 -0
- package/dist/ui/agent-widget.d.ts +15 -8
- package/dist/ui/agent-widget.js +28 -7
- package/dist/ui/conversation-viewer.js +6 -8
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +95 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +49 -0
- package/package.json +10 -6
- package/src/agent-manager.ts +55 -2
- package/src/agent-runner.ts +49 -18
- package/src/default-agents.ts +2 -9
- package/src/index.ts +207 -41
- package/src/schedule-store.ts +143 -0
- package/src/schedule.ts +365 -0
- package/src/settings.ts +14 -0
- package/src/types.ts +52 -0
- package/src/ui/agent-widget.ts +36 -6
- package/src/ui/conversation-viewer.ts +6 -6
- package/src/ui/schedule-menu.ts +104 -0
- package/src/usage.ts +60 -0
- package/.github/workflows/ci.yml +0 -21
- package/biome.json +0 -26
- package/dist/ui/conversation-viewer.test.d.ts +0 -1
- package/dist/ui/conversation-viewer.test.js +0 -254
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
8
8
|
import { extractText } from "../context.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
10
|
+
import { describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
|
|
10
11
|
/** Lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
|
|
11
12
|
const CHROME_LINES = 6;
|
|
12
13
|
const MIN_VIEWPORT = 3;
|
|
@@ -101,13 +102,10 @@ export class ConversationViewer {
|
|
|
101
102
|
const toolUses = this.activity?.toolUses ?? this.record.toolUses;
|
|
102
103
|
if (toolUses > 0)
|
|
103
104
|
headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
headerParts.push(formatTokens(tokens));
|
|
109
|
-
}
|
|
110
|
-
catch { /* */ }
|
|
105
|
+
const tokens = getLifetimeTotal(this.activity?.lifetimeUsage);
|
|
106
|
+
if (tokens > 0) {
|
|
107
|
+
const percent = getSessionContextPercent(this.activity?.session);
|
|
108
|
+
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
111
109
|
}
|
|
112
110
|
lines.push(row(`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`));
|
|
113
111
|
lines.push(hrMid);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule-menu.ts — `/agents → Scheduled jobs` submenu.
|
|
3
|
+
*
|
|
4
|
+
* Minimal v1 surface: list scheduled jobs, select one to inspect details +
|
|
5
|
+
* confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
|
|
6
|
+
* is the canonical creation path), no toggle/cleanup (cancel is enough for
|
|
7
|
+
* "I scheduled something dumb, get rid of it"). Add management surfaces here
|
|
8
|
+
* if real demand emerges.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { SubagentScheduler } from "../schedule.js";
|
|
12
|
+
/**
|
|
13
|
+
* List scheduled jobs; selecting one opens a cancel-confirm with details.
|
|
14
|
+
* Returns when the user backs out or after a cancellation.
|
|
15
|
+
*/
|
|
16
|
+
export declare function showSchedulesMenu(ctx: ExtensionCommandContext, scheduler: SubagentScheduler): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedule-menu.ts — `/agents → Scheduled jobs` submenu.
|
|
3
|
+
*
|
|
4
|
+
* Minimal v1 surface: list scheduled jobs, select one to inspect details +
|
|
5
|
+
* confirm cancellation. No create wizard (the `Agent` tool's `schedule` param
|
|
6
|
+
* is the canonical creation path), no toggle/cleanup (cancel is enough for
|
|
7
|
+
* "I scheduled something dumb, get rid of it"). Add management surfaces here
|
|
8
|
+
* if real demand emerges.
|
|
9
|
+
*/
|
|
10
|
+
/** Format an ISO timestamp as relative time ("in 4h", "2d ago", "—"). */
|
|
11
|
+
function relTime(iso, now = Date.now()) {
|
|
12
|
+
if (!iso)
|
|
13
|
+
return "—";
|
|
14
|
+
const t = new Date(iso).getTime();
|
|
15
|
+
if (Number.isNaN(t))
|
|
16
|
+
return "—";
|
|
17
|
+
const diff = t - now;
|
|
18
|
+
const abs = Math.abs(diff);
|
|
19
|
+
const future = diff > 0;
|
|
20
|
+
if (abs < 60_000)
|
|
21
|
+
return future ? "in <1m" : "<1m ago";
|
|
22
|
+
const m = Math.round(abs / 60_000);
|
|
23
|
+
if (m < 60)
|
|
24
|
+
return future ? `in ${m}m` : `${m}m ago`;
|
|
25
|
+
const h = Math.round(abs / 3_600_000);
|
|
26
|
+
if (h < 24)
|
|
27
|
+
return future ? `in ${h}h` : `${h}h ago`;
|
|
28
|
+
const d = Math.round(abs / 86_400_000);
|
|
29
|
+
return future ? `in ${d}d` : `${d}d ago`;
|
|
30
|
+
}
|
|
31
|
+
/** One-line status icon. */
|
|
32
|
+
function statusIcon(j) {
|
|
33
|
+
if (!j.enabled)
|
|
34
|
+
return "✗";
|
|
35
|
+
if (j.lastStatus === "error")
|
|
36
|
+
return "!";
|
|
37
|
+
if (j.lastStatus === "running")
|
|
38
|
+
return "⋯";
|
|
39
|
+
return "✓";
|
|
40
|
+
}
|
|
41
|
+
/** Compact selectable row — name, schedule, agent type, next/last run, count. */
|
|
42
|
+
function formatJob(j, scheduler) {
|
|
43
|
+
const next = scheduler.getNextRun(j.id);
|
|
44
|
+
return [
|
|
45
|
+
statusIcon(j),
|
|
46
|
+
j.name.padEnd(18).slice(0, 18),
|
|
47
|
+
j.schedule.padEnd(14).slice(0, 14),
|
|
48
|
+
`[${j.subagent_type}]`,
|
|
49
|
+
`next ${relTime(next)}`,
|
|
50
|
+
`last ${relTime(j.lastRun)}`,
|
|
51
|
+
`runs ${j.runCount}`,
|
|
52
|
+
].join(" ");
|
|
53
|
+
}
|
|
54
|
+
/** Multi-line details block for the cancel confirm. */
|
|
55
|
+
function formatDetails(j, scheduler) {
|
|
56
|
+
const next = scheduler.getNextRun(j.id) ?? "—";
|
|
57
|
+
return [
|
|
58
|
+
`name: ${j.name}`,
|
|
59
|
+
`schedule: ${j.schedule} (${j.scheduleType})`,
|
|
60
|
+
`agent: ${j.subagent_type}`,
|
|
61
|
+
`prompt: ${j.prompt.slice(0, 200)}${j.prompt.length > 200 ? "…" : ""}`,
|
|
62
|
+
`created: ${j.createdAt}`,
|
|
63
|
+
`last run: ${j.lastRun ?? "—"} (${j.lastStatus ?? "—"})`,
|
|
64
|
+
`next run: ${next}`,
|
|
65
|
+
`runs: ${j.runCount}`,
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* List scheduled jobs; selecting one opens a cancel-confirm with details.
|
|
70
|
+
* Returns when the user backs out or after a cancellation.
|
|
71
|
+
*/
|
|
72
|
+
export async function showSchedulesMenu(ctx, scheduler) {
|
|
73
|
+
if (!scheduler.isActive()) {
|
|
74
|
+
ctx.ui.notify("Scheduler is not active in this session.", "warning");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const jobs = scheduler.list();
|
|
78
|
+
if (jobs.length === 0) {
|
|
79
|
+
ctx.ui.notify("No scheduled jobs.", "info");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const labels = jobs.map(j => formatJob(j, scheduler));
|
|
83
|
+
const choice = await ctx.ui.select(`Scheduled jobs (${jobs.length}) — select to cancel`, labels);
|
|
84
|
+
if (!choice)
|
|
85
|
+
return;
|
|
86
|
+
const idx = labels.indexOf(choice);
|
|
87
|
+
if (idx < 0)
|
|
88
|
+
return;
|
|
89
|
+
const job = jobs[idx];
|
|
90
|
+
const ok = await ctx.ui.confirm(`Cancel "${job.name}"?`, formatDetails(job, scheduler));
|
|
91
|
+
if (!ok)
|
|
92
|
+
return;
|
|
93
|
+
scheduler.removeJob(job.id);
|
|
94
|
+
ctx.ui.notify(`Cancelled "${job.name}".`, "info");
|
|
95
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
|
|
2
|
+
/**
|
|
3
|
+
* Lifetime usage components, accumulated via `message_end` events. Survives
|
|
4
|
+
* compaction (which replaces session.state.messages and would reset any
|
|
5
|
+
* stats-derived sum). cacheRead is excluded because each turn's cacheRead is
|
|
6
|
+
* the cumulative cached prefix re-read on that one call — summing across
|
|
7
|
+
* turns counts the prefix N times. See issue #38.
|
|
8
|
+
*/
|
|
9
|
+
export type LifetimeUsage = {
|
|
10
|
+
input: number;
|
|
11
|
+
output: number;
|
|
12
|
+
cacheWrite: number;
|
|
13
|
+
};
|
|
14
|
+
/** Sum of lifetime usage components, or 0 if undefined. */
|
|
15
|
+
export declare function getLifetimeTotal(u?: LifetimeUsage): number;
|
|
16
|
+
/** Add a usage delta into a target accumulator (mutates target). */
|
|
17
|
+
export declare function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void;
|
|
18
|
+
/** Minimal shape we read from upstream `getSessionStats()`. */
|
|
19
|
+
export type SessionStatsLike = {
|
|
20
|
+
tokens: {
|
|
21
|
+
input: number;
|
|
22
|
+
output: number;
|
|
23
|
+
cacheWrite: number;
|
|
24
|
+
};
|
|
25
|
+
contextUsage?: {
|
|
26
|
+
percent: number | null;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type SessionLike = {
|
|
30
|
+
getSessionStats(): SessionStatsLike;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Session-scoped token count: input + output + cacheWrite as reported by
|
|
34
|
+
* upstream `getSessionStats().tokens` for the *current* session window.
|
|
35
|
+
*
|
|
36
|
+
* RESETS at compaction — upstream replaces `session.state.messages` and the
|
|
37
|
+
* stats are derived from that array. For a lifetime total that survives
|
|
38
|
+
* compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
|
|
39
|
+
* from an independent accumulator fed by `message_end` events.
|
|
40
|
+
*
|
|
41
|
+
* Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
|
|
42
|
+
* and so counts the cumulative cached prefix N times across N turns
|
|
43
|
+
* (issue #38).
|
|
44
|
+
*/
|
|
45
|
+
export declare function getSessionTokens(session: SessionLike | undefined): number;
|
|
46
|
+
/**
|
|
47
|
+
* Context-window utilization (0–100), or null when unavailable
|
|
48
|
+
* (no model contextWindow, or post-compaction before the next response).
|
|
49
|
+
*/
|
|
50
|
+
export declare function getSessionContextPercent(session: SessionLike | undefined): number | null;
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
|
|
2
|
+
/** Sum of lifetime usage components, or 0 if undefined. */
|
|
3
|
+
export function getLifetimeTotal(u) {
|
|
4
|
+
return u ? u.input + u.output + u.cacheWrite : 0;
|
|
5
|
+
}
|
|
6
|
+
/** Add a usage delta into a target accumulator (mutates target). */
|
|
7
|
+
export function addUsage(into, delta) {
|
|
8
|
+
into.input += delta.input;
|
|
9
|
+
into.output += delta.output;
|
|
10
|
+
into.cacheWrite += delta.cacheWrite;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Session-scoped token count: input + output + cacheWrite as reported by
|
|
14
|
+
* upstream `getSessionStats().tokens` for the *current* session window.
|
|
15
|
+
*
|
|
16
|
+
* RESETS at compaction — upstream replaces `session.state.messages` and the
|
|
17
|
+
* stats are derived from that array. For a lifetime total that survives
|
|
18
|
+
* compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
|
|
19
|
+
* from an independent accumulator fed by `message_end` events.
|
|
20
|
+
*
|
|
21
|
+
* Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
|
|
22
|
+
* and so counts the cumulative cached prefix N times across N turns
|
|
23
|
+
* (issue #38).
|
|
24
|
+
*/
|
|
25
|
+
export function getSessionTokens(session) {
|
|
26
|
+
if (!session)
|
|
27
|
+
return 0;
|
|
28
|
+
try {
|
|
29
|
+
const t = session.getSessionStats().tokens;
|
|
30
|
+
return t.input + t.output + t.cacheWrite;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Context-window utilization (0–100), or null when unavailable
|
|
38
|
+
* (no model contextWindow, or post-compaction before the next response).
|
|
39
|
+
*/
|
|
40
|
+
export function getSessionContextPercent(session) {
|
|
41
|
+
if (!session)
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
return session.getSessionStats().contextUsage?.percent ?? null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tintinweb/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
"agent",
|
|
21
21
|
"autonomous"
|
|
22
22
|
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@mariozechner/pi-ai": ">=0.70.5",
|
|
25
|
+
"@mariozechner/pi-coding-agent": ">=0.70.5",
|
|
26
|
+
"@mariozechner/pi-tui": ">=0.70.5"
|
|
27
|
+
},
|
|
23
28
|
"dependencies": {
|
|
24
|
-
"@
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"@sinclair/typebox": "latest"
|
|
29
|
+
"@sinclair/typebox": "^0.34.49",
|
|
30
|
+
"croner": "^10.0.1",
|
|
31
|
+
"nanoid": "^5.0.0"
|
|
28
32
|
},
|
|
29
33
|
"scripts": {
|
|
30
34
|
"build": "tsc",
|
|
@@ -36,7 +40,7 @@
|
|
|
36
40
|
"lint:fix": "biome check --fix src/ test/"
|
|
37
41
|
},
|
|
38
42
|
"devDependencies": {
|
|
39
|
-
"@biomejs/biome": "^2.
|
|
43
|
+
"@biomejs/biome": "^2.4.14",
|
|
40
44
|
"@types/node": "^25.5.0",
|
|
41
45
|
"typescript": "^6.0.0",
|
|
42
46
|
"vitest": "^4.0.18"
|
package/src/agent-manager.ts
CHANGED
|
@@ -11,10 +11,13 @@ import type { Model } from "@mariozechner/pi-ai";
|
|
|
11
11
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
|
|
13
13
|
import type { AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
|
|
14
|
+
import { addUsage } from "./usage.js";
|
|
14
15
|
import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
|
|
15
16
|
|
|
16
17
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
17
18
|
export type OnAgentStart = (record: AgentRecord) => void;
|
|
19
|
+
export type OnAgentCompact = (record: AgentRecord, info: CompactionInfo) => void;
|
|
20
|
+
export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; tokensBefore: number };
|
|
18
21
|
|
|
19
22
|
/** Default max concurrent background agents. */
|
|
20
23
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
@@ -35,8 +38,16 @@ interface SpawnOptions {
|
|
|
35
38
|
inheritContext?: boolean;
|
|
36
39
|
thinkingLevel?: ThinkingLevel;
|
|
37
40
|
isBackground?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Skip the maxConcurrent queue check for this spawn — start immediately even
|
|
43
|
+
* if the configured concurrency limit would otherwise queue it. Used by the
|
|
44
|
+
* scheduler so a fired job can't be deferred past its trigger window.
|
|
45
|
+
*/
|
|
46
|
+
bypassQueue?: boolean;
|
|
38
47
|
/** Isolation mode — "worktree" creates a temp git worktree for the agent. */
|
|
39
48
|
isolation?: IsolationMode;
|
|
49
|
+
/** Parent abort signal — when aborted, the subagent is also stopped. */
|
|
50
|
+
signal?: AbortSignal;
|
|
40
51
|
/** Called on tool start/end with activity info (for streaming progress to UI). */
|
|
41
52
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
42
53
|
/** Called on streaming text deltas from the assistant response. */
|
|
@@ -45,6 +56,10 @@ interface SpawnOptions {
|
|
|
45
56
|
onSessionCreated?: (session: AgentSession) => void;
|
|
46
57
|
/** Called at the end of each agentic turn with the cumulative count. */
|
|
47
58
|
onTurnEnd?: (turnCount: number) => void;
|
|
59
|
+
/** Called once per assistant message_end with that message's usage delta. */
|
|
60
|
+
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
61
|
+
/** Called when the session successfully compacts. */
|
|
62
|
+
onCompaction?: (info: CompactionInfo) => void;
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
export class AgentManager {
|
|
@@ -52,6 +67,7 @@ export class AgentManager {
|
|
|
52
67
|
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
53
68
|
private onComplete?: OnAgentComplete;
|
|
54
69
|
private onStart?: OnAgentStart;
|
|
70
|
+
private onCompact?: OnAgentCompact;
|
|
55
71
|
private maxConcurrent: number;
|
|
56
72
|
|
|
57
73
|
/** Queue of background agents waiting to start. */
|
|
@@ -59,9 +75,15 @@ export class AgentManager {
|
|
|
59
75
|
/** Number of currently running background agents. */
|
|
60
76
|
private runningBackground = 0;
|
|
61
77
|
|
|
62
|
-
constructor(
|
|
78
|
+
constructor(
|
|
79
|
+
onComplete?: OnAgentComplete,
|
|
80
|
+
maxConcurrent = DEFAULT_MAX_CONCURRENT,
|
|
81
|
+
onStart?: OnAgentStart,
|
|
82
|
+
onCompact?: OnAgentCompact,
|
|
83
|
+
) {
|
|
63
84
|
this.onComplete = onComplete;
|
|
64
85
|
this.onStart = onStart;
|
|
86
|
+
this.onCompact = onCompact;
|
|
65
87
|
this.maxConcurrent = maxConcurrent;
|
|
66
88
|
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
67
89
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
@@ -99,12 +121,14 @@ export class AgentManager {
|
|
|
99
121
|
toolUses: 0,
|
|
100
122
|
startedAt: Date.now(),
|
|
101
123
|
abortController,
|
|
124
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
125
|
+
compactionCount: 0,
|
|
102
126
|
};
|
|
103
127
|
this.agents.set(id, record);
|
|
104
128
|
|
|
105
129
|
const args: SpawnArgs = { pi, ctx, type, prompt, options };
|
|
106
130
|
|
|
107
|
-
if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
|
|
131
|
+
if (options.isBackground && !options.bypassQueue && this.runningBackground >= this.maxConcurrent) {
|
|
108
132
|
// Queue it — will be started when a running agent completes
|
|
109
133
|
this.queue.push({ id, args });
|
|
110
134
|
return id;
|
|
@@ -121,6 +145,15 @@ export class AgentManager {
|
|
|
121
145
|
if (options.isBackground) this.runningBackground++;
|
|
122
146
|
this.onStart?.(record);
|
|
123
147
|
|
|
148
|
+
// Wire parent abort signal to stop the subagent when the parent is interrupted
|
|
149
|
+
let detachParentSignal: (() => void) | undefined;
|
|
150
|
+
if (options.signal) {
|
|
151
|
+
const onParentAbort = () => this.abort(id);
|
|
152
|
+
options.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
153
|
+
detachParentSignal = () => options.signal!.removeEventListener("abort", onParentAbort);
|
|
154
|
+
}
|
|
155
|
+
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
156
|
+
|
|
124
157
|
// Worktree isolation: create a temporary git worktree if requested
|
|
125
158
|
let worktreeCwd: string | undefined;
|
|
126
159
|
let worktreeWarning = "";
|
|
@@ -152,6 +185,15 @@ export class AgentManager {
|
|
|
152
185
|
},
|
|
153
186
|
onTurnEnd: options.onTurnEnd,
|
|
154
187
|
onTextDelta: options.onTextDelta,
|
|
188
|
+
onAssistantUsage: (usage) => {
|
|
189
|
+
addUsage(record.lifetimeUsage, usage);
|
|
190
|
+
options.onAssistantUsage?.(usage);
|
|
191
|
+
},
|
|
192
|
+
onCompaction: (info) => {
|
|
193
|
+
record.compactionCount++;
|
|
194
|
+
this.onCompact?.(record, info);
|
|
195
|
+
options.onCompaction?.(info);
|
|
196
|
+
},
|
|
155
197
|
onSessionCreated: (session) => {
|
|
156
198
|
record.session = session;
|
|
157
199
|
// Flush any steers that arrived before the session was ready
|
|
@@ -173,6 +215,8 @@ export class AgentManager {
|
|
|
173
215
|
record.session = session;
|
|
174
216
|
record.completedAt ??= Date.now();
|
|
175
217
|
|
|
218
|
+
detach();
|
|
219
|
+
|
|
176
220
|
// Final flush of streaming output file
|
|
177
221
|
if (record.outputCleanup) {
|
|
178
222
|
try { record.outputCleanup(); } catch { /* ignore */ }
|
|
@@ -204,6 +248,8 @@ export class AgentManager {
|
|
|
204
248
|
record.error = err instanceof Error ? err.message : String(err);
|
|
205
249
|
record.completedAt ??= Date.now();
|
|
206
250
|
|
|
251
|
+
detach();
|
|
252
|
+
|
|
207
253
|
// Final flush of streaming output file on error
|
|
208
254
|
if (record.outputCleanup) {
|
|
209
255
|
try { record.outputCleanup(); } catch { /* ignore */ }
|
|
@@ -278,6 +324,13 @@ export class AgentManager {
|
|
|
278
324
|
onToolActivity: (activity) => {
|
|
279
325
|
if (activity.type === "end") record.toolUses++;
|
|
280
326
|
},
|
|
327
|
+
onAssistantUsage: (usage) => {
|
|
328
|
+
addUsage(record.lifetimeUsage, usage);
|
|
329
|
+
},
|
|
330
|
+
onCompaction: (info) => {
|
|
331
|
+
record.compactionCount++;
|
|
332
|
+
this.onCompact?.(record, info);
|
|
333
|
+
},
|
|
281
334
|
signal,
|
|
282
335
|
});
|
|
283
336
|
record.status = "completed";
|
package/src/agent-runner.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "@mariozechner/pi-coding-agent";
|
|
17
17
|
import { getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
18
18
|
import { buildParentContext, extractText } from "./context.js";
|
|
19
|
+
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
19
20
|
import { detectEnv } from "./env.js";
|
|
20
21
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
21
22
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
@@ -102,6 +103,17 @@ export interface RunOptions {
|
|
|
102
103
|
onSessionCreated?: (session: AgentSession) => void;
|
|
103
104
|
/** Called at the end of each agentic turn with the cumulative count. */
|
|
104
105
|
onTurnEnd?: (turnCount: number) => void;
|
|
106
|
+
/**
|
|
107
|
+
* Called once per assistant message_end with that message's usage delta.
|
|
108
|
+
* Lets callers maintain a lifetime accumulator that survives compaction
|
|
109
|
+
* (which replaces session.state.messages and resets stats-derived sums).
|
|
110
|
+
*/
|
|
111
|
+
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
112
|
+
/**
|
|
113
|
+
* Called when the session successfully compacts. `tokensBefore` is upstream's
|
|
114
|
+
* pre-compaction context size estimate. Aborted compactions don't fire.
|
|
115
|
+
*/
|
|
116
|
+
onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
export interface RunResult {
|
|
@@ -212,19 +224,11 @@ export async function runAgent(
|
|
|
212
224
|
if (agentConfig) {
|
|
213
225
|
systemPrompt = buildAgentPrompt(agentConfig, effectiveCwd, env, parentSystemPrompt, extras);
|
|
214
226
|
} else {
|
|
215
|
-
// Unknown type fallback: general-purpose (defensive —
|
|
216
|
-
// since index.ts resolves unknown types
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
systemPrompt: "",
|
|
221
|
-
promptMode: "append",
|
|
222
|
-
extensions: true,
|
|
223
|
-
skills: true,
|
|
224
|
-
inheritContext: false,
|
|
225
|
-
runInBackground: false,
|
|
226
|
-
isolated: false,
|
|
227
|
-
}, effectiveCwd, env, parentSystemPrompt, extras);
|
|
227
|
+
// Unknown type fallback: spread the canonical general-purpose config (defensive —
|
|
228
|
+
// unreachable in practice since index.ts resolves unknown types before calling runAgent).
|
|
229
|
+
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
230
|
+
if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
231
|
+
systemPrompt = buildAgentPrompt({ ...fallback, name: type }, effectiveCwd, env, parentSystemPrompt, extras);
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
// When skills is string[], we've already preloaded them into the prompt.
|
|
@@ -350,6 +354,17 @@ export async function runAgent(
|
|
|
350
354
|
if (event.type === "tool_execution_end") {
|
|
351
355
|
options.onToolActivity?.({ type: "end", toolName: event.toolName });
|
|
352
356
|
}
|
|
357
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
358
|
+
const u = (event.message as any).usage;
|
|
359
|
+
if (u) options.onAssistantUsage?.({
|
|
360
|
+
input: u.input ?? 0,
|
|
361
|
+
output: u.output ?? 0,
|
|
362
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (event.type === "compaction_end" && !event.aborted && event.result) {
|
|
366
|
+
options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
|
|
367
|
+
}
|
|
353
368
|
});
|
|
354
369
|
|
|
355
370
|
const collector = collectResponseText(session);
|
|
@@ -382,15 +397,31 @@ export async function runAgent(
|
|
|
382
397
|
export async function resumeAgent(
|
|
383
398
|
session: AgentSession,
|
|
384
399
|
prompt: string,
|
|
385
|
-
options: {
|
|
400
|
+
options: {
|
|
401
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
402
|
+
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
403
|
+
onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
|
|
404
|
+
signal?: AbortSignal;
|
|
405
|
+
} = {},
|
|
386
406
|
): Promise<string> {
|
|
387
407
|
const collector = collectResponseText(session);
|
|
388
408
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
|
389
409
|
|
|
390
|
-
const
|
|
410
|
+
const unsubEvents = (options.onToolActivity || options.onAssistantUsage || options.onCompaction)
|
|
391
411
|
? session.subscribe((event: AgentSessionEvent) => {
|
|
392
|
-
if (event.type === "tool_execution_start") options.onToolActivity
|
|
393
|
-
if (event.type === "tool_execution_end") options.onToolActivity
|
|
412
|
+
if (event.type === "tool_execution_start") options.onToolActivity?.({ type: "start", toolName: event.toolName });
|
|
413
|
+
if (event.type === "tool_execution_end") options.onToolActivity?.({ type: "end", toolName: event.toolName });
|
|
414
|
+
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
415
|
+
const u = (event.message as any).usage;
|
|
416
|
+
if (u) options.onAssistantUsage?.({
|
|
417
|
+
input: u.input ?? 0,
|
|
418
|
+
output: u.output ?? 0,
|
|
419
|
+
cacheWrite: u.cacheWrite ?? 0,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (event.type === "compaction_end" && !event.aborted && event.result) {
|
|
423
|
+
options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
|
|
424
|
+
}
|
|
394
425
|
})
|
|
395
426
|
: () => {};
|
|
396
427
|
|
|
@@ -398,7 +429,7 @@ export async function resumeAgent(
|
|
|
398
429
|
await session.prompt(prompt);
|
|
399
430
|
} finally {
|
|
400
431
|
collector.unsubscribe();
|
|
401
|
-
|
|
432
|
+
unsubEvents();
|
|
402
433
|
cleanupAbort();
|
|
403
434
|
}
|
|
404
435
|
|
package/src/default-agents.ts
CHANGED
|
@@ -16,13 +16,12 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
16
16
|
displayName: "Agent",
|
|
17
17
|
description: "General-purpose agent for complex, multi-step tasks",
|
|
18
18
|
// builtinToolNames omitted — means "all available tools" (resolved at lookup time)
|
|
19
|
+
// inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
|
|
20
|
+
// Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
|
|
19
21
|
extensions: true,
|
|
20
22
|
skills: true,
|
|
21
23
|
systemPrompt: "",
|
|
22
24
|
promptMode: "append",
|
|
23
|
-
inheritContext: false,
|
|
24
|
-
runInBackground: false,
|
|
25
|
-
isolated: false,
|
|
26
25
|
isDefault: true,
|
|
27
26
|
},
|
|
28
27
|
],
|
|
@@ -65,9 +64,6 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
|
|
|
65
64
|
- Do not use emojis
|
|
66
65
|
- Be thorough and precise`,
|
|
67
66
|
promptMode: "replace",
|
|
68
|
-
inheritContext: false,
|
|
69
|
-
runInBackground: false,
|
|
70
|
-
isolated: false,
|
|
71
67
|
isDefault: true,
|
|
72
68
|
},
|
|
73
69
|
],
|
|
@@ -121,9 +117,6 @@ You are STRICTLY PROHIBITED from:
|
|
|
121
117
|
List 3-5 files most critical for implementing this plan:
|
|
122
118
|
- /absolute/path/to/file.ts - [Brief reason]`,
|
|
123
119
|
promptMode: "replace",
|
|
124
|
-
inheritContext: false,
|
|
125
|
-
runInBackground: false,
|
|
126
|
-
isolated: false,
|
|
127
120
|
isDefault: true,
|
|
128
121
|
},
|
|
129
122
|
],
|