@herbertgao/pi-subagents 0.15.4 → 0.16.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 +16 -0
- package/README.md +30 -22
- package/examples/agent-tool-description.md +5 -5
- package/package.json +7 -7
- package/src/agent-file-toggle.ts +7 -1
- package/src/agent-manager.ts +48 -19
- package/src/agent-runner.ts +22 -15
- package/src/custom-agents.ts +26 -2
- package/src/index.ts +219 -40
- package/src/invocation-config.ts +101 -3
- package/src/nested-tools.ts +18 -4
- package/src/settings.ts +99 -0
- package/src/types.ts +22 -3
- package/src/ui/agent-widget.ts +51 -3
- package/src/ui/conversation-viewer.ts +20 -2
- package/src/ui/fleet-list.ts +34 -10
- package/src/usage.ts +129 -1
- package/src/worktree.ts +29 -5
package/src/usage.ts
CHANGED
|
@@ -6,23 +6,151 @@
|
|
|
6
6
|
* stats-derived sum). cacheRead is excluded because each turn's cacheRead is
|
|
7
7
|
* the cumulative cached prefix re-read on that one call — summing across
|
|
8
8
|
* turns counts the prefix N times. See issue #38.
|
|
9
|
+
*
|
|
10
|
+
* That exclusion is about this *display* total, not about what was billed: the
|
|
11
|
+
* prefix really is re-read and re-charged on every call. So `cacheRead` is
|
|
12
|
+
* accumulated here anyway, kept out of `getLifetimeTotal` and used only where
|
|
13
|
+
* billing is the question — reporting to the parent session, whose own messages
|
|
14
|
+
* pi counts the same way (`addUsageToTotals`). Reporting 0 there would make a
|
|
15
|
+
* subagent's rows count differently from every other row in one total.
|
|
16
|
+
*
|
|
17
|
+
* `cost` is a plain sum for the same reason: it is what pi charged for that one
|
|
18
|
+
* message (`usage.cost.total`, priced from the model's rates), not a cumulative
|
|
19
|
+
* figure. Both are optional because a model with no pricing data reports no
|
|
20
|
+
* cost, and because every accumulator predates them; absent reads as 0.
|
|
9
21
|
*/
|
|
10
22
|
export type LifetimeUsage = {
|
|
11
23
|
input: number
|
|
12
24
|
output: number
|
|
13
25
|
cacheWrite: number
|
|
26
|
+
cacheRead?: number
|
|
27
|
+
cost?: number
|
|
14
28
|
}
|
|
15
29
|
|
|
16
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Sum of lifetime *token* components for DISPLAY, or 0 if undefined.
|
|
32
|
+
* Deliberately excludes `cacheRead` (see above) and `cost` — that is money, not
|
|
33
|
+
* tokens, and lives on the same object only because it accumulates on the same
|
|
34
|
+
* events.
|
|
35
|
+
*/
|
|
17
36
|
export function getLifetimeTotal(u?: LifetimeUsage): number {
|
|
18
37
|
return u ? u.input + u.output + u.cacheWrite : 0
|
|
19
38
|
}
|
|
20
39
|
|
|
40
|
+
/** Accumulated cost in USD, or 0 when unpriced/undefined. */
|
|
41
|
+
export function getLifetimeCost(u?: LifetimeUsage): number {
|
|
42
|
+
return u?.cost ?? 0
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
/** Add a usage delta into a target accumulator (mutates target). */
|
|
22
46
|
export function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void {
|
|
23
47
|
into.input += delta.input
|
|
24
48
|
into.output += delta.output
|
|
25
49
|
into.cacheWrite += delta.cacheWrite
|
|
50
|
+
if (delta.cacheRead) into.cacheRead = (into.cacheRead ?? 0) + delta.cacheRead
|
|
51
|
+
if (delta.cost) into.cost = (into.cost ?? 0) + delta.cost
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A pi `Usage`. Rebuilt here rather than imported so this module stays
|
|
56
|
+
* dependency-free for tests; the fields are pi's, and every one of them must be
|
|
57
|
+
* present: pi's `addUsageToTotals` dereferences `usage.cost.total` with no
|
|
58
|
+
* guard, so a partial object throws inside pi rather than at the call site.
|
|
59
|
+
*
|
|
60
|
+
* This is pi's convention for spend in anything handed to a consumer — every
|
|
61
|
+
* extension-facing payload that carries it takes the whole object
|
|
62
|
+
* (`ToolResultEvent`, `ToolResultEventResult`, `AssistantMessage`, …), never a
|
|
63
|
+
* flattened cost. Pi flattens only in computed read APIs it expects you to
|
|
64
|
+
* render, like `SessionStats`. So both places we hand usage to someone else —
|
|
65
|
+
* `AgentToolResult.usage` and the `subagents:completed` / `subagents:failed`
|
|
66
|
+
* events — carry this, and gain whatever pi adds to `Usage` for free.
|
|
67
|
+
*/
|
|
68
|
+
export type ReportedUsage = {
|
|
69
|
+
input: number
|
|
70
|
+
output: number
|
|
71
|
+
cacheRead: number
|
|
72
|
+
cacheWrite: number
|
|
73
|
+
totalTokens: number
|
|
74
|
+
cost: {
|
|
75
|
+
input: number
|
|
76
|
+
output: number
|
|
77
|
+
cacheRead: number
|
|
78
|
+
cacheWrite: number
|
|
79
|
+
total: number
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Render an accumulator as a pi `Usage`, or undefined when nothing was spent —
|
|
85
|
+
* callers attach nothing rather than a zero, so a consumer can tell "spent
|
|
86
|
+
* nothing" from "never ran".
|
|
87
|
+
*
|
|
88
|
+
* `cacheRead` IS included, unlike in `getLifetimeTotal`: pi sums it across a
|
|
89
|
+
* session's own assistant messages, and the prefix genuinely is re-read and
|
|
90
|
+
* re-billed on every call. Only `total` is populated on the cost breakdown; pi
|
|
91
|
+
* reads nothing else from it, and the per-kind split is not tracked.
|
|
92
|
+
*/
|
|
93
|
+
export function toReportedUsage(u: LifetimeUsage): ReportedUsage | undefined {
|
|
94
|
+
const { input, output, cacheWrite, cacheRead = 0, cost = 0 } = u
|
|
95
|
+
if (
|
|
96
|
+
input === 0 &&
|
|
97
|
+
output === 0 &&
|
|
98
|
+
cacheWrite === 0 &&
|
|
99
|
+
cacheRead === 0 &&
|
|
100
|
+
cost === 0
|
|
101
|
+
)
|
|
102
|
+
return undefined
|
|
103
|
+
return {
|
|
104
|
+
input,
|
|
105
|
+
output,
|
|
106
|
+
cacheRead,
|
|
107
|
+
cacheWrite,
|
|
108
|
+
totalTokens: input + output + cacheRead + cacheWrite,
|
|
109
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: cost },
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Subagent spend that the parent session has not been told about yet.
|
|
115
|
+
*
|
|
116
|
+
* Subagents run in their own pi sessions, so none of what they spend appears in
|
|
117
|
+
* the parent's `getSessionStats()`. Pi does aggregate `toolResult.usage` into
|
|
118
|
+
* those stats, though — so the way back into the parent's footer and `/cost` is
|
|
119
|
+
* to hang the spend on a tool result. Background and scheduled agents finish
|
|
120
|
+
* between tool calls with nothing to hang it on, hence a pool: every assistant
|
|
121
|
+
* message lands here as it happens, and the next tool result we return carries
|
|
122
|
+
* whatever has accumulated.
|
|
123
|
+
*
|
|
124
|
+
* Drain empties it, so each message is reported exactly once no matter how many
|
|
125
|
+
* results are returned or how many agents were running.
|
|
126
|
+
*/
|
|
127
|
+
export class PendingUsagePool {
|
|
128
|
+
private pending: LifetimeUsage = {
|
|
129
|
+
input: 0,
|
|
130
|
+
output: 0,
|
|
131
|
+
cacheWrite: 0,
|
|
132
|
+
cacheRead: 0,
|
|
133
|
+
cost: 0,
|
|
134
|
+
}
|
|
135
|
+
private dirty = false
|
|
136
|
+
|
|
137
|
+
add(delta: LifetimeUsage): void {
|
|
138
|
+
addUsage(this.pending, delta)
|
|
139
|
+
this.dirty = true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Take everything accumulated so far as a pi `Usage`, resetting the pool.
|
|
144
|
+
* Returns undefined when nothing is pending, so callers can leave the tool
|
|
145
|
+
* result untouched rather than attaching a zero.
|
|
146
|
+
*/
|
|
147
|
+
drain(): ReportedUsage | undefined {
|
|
148
|
+
if (!this.dirty) return undefined
|
|
149
|
+
const drained = toReportedUsage(this.pending)
|
|
150
|
+
this.pending = { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0 }
|
|
151
|
+
this.dirty = false
|
|
152
|
+
return drained
|
|
153
|
+
}
|
|
26
154
|
}
|
|
27
155
|
|
|
28
156
|
/** Minimal shape we read from upstream `getSessionStats()`. */
|
package/src/worktree.ts
CHANGED
|
@@ -28,6 +28,26 @@ export interface WorktreeInfo {
|
|
|
28
28
|
workPath: string
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Project-wide switch for worktree isolation (`worktreeIsolation` in
|
|
33
|
+
* subagents.json). Default `true` — unchanged behaviour.
|
|
34
|
+
*
|
|
35
|
+
* The `"off"` isolation value gives a model a legal way to decline a worktree,
|
|
36
|
+
* but it still depends on the model choosing it. This is the deterministic half
|
|
37
|
+
* of the same fix: on a large repo where every worktree costs real time and
|
|
38
|
+
* disk (#184), turning it off means no caller can create one, whatever it
|
|
39
|
+
* passes.
|
|
40
|
+
*/
|
|
41
|
+
let worktreeIsolationEnabled = true
|
|
42
|
+
|
|
43
|
+
export function setWorktreeIsolationEnabled(enabled: boolean): void {
|
|
44
|
+
worktreeIsolationEnabled = enabled
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isWorktreeIsolationEnabled(): boolean {
|
|
48
|
+
return worktreeIsolationEnabled
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
export interface WorktreeCleanupResult {
|
|
32
52
|
/** Whether changes were found in the worktree. */
|
|
33
53
|
hasChanges: boolean
|
|
@@ -134,11 +154,15 @@ export function cleanupWorktree(
|
|
|
134
154
|
// Truncate description for commit message (no shell sanitization needed — execFileSync uses argv)
|
|
135
155
|
const safeDesc = agentDescription.slice(0, 200)
|
|
136
156
|
const commitMsg = `pi-agent: ${safeDesc}`
|
|
137
|
-
execFileSync(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
157
|
+
execFileSync(
|
|
158
|
+
"git",
|
|
159
|
+
["commit", "--no-verify", "--no-gpg-sign", "-m", commitMsg],
|
|
160
|
+
{
|
|
161
|
+
cwd: worktree.path,
|
|
162
|
+
stdio: "pipe",
|
|
163
|
+
timeout: 10000,
|
|
164
|
+
},
|
|
165
|
+
)
|
|
142
166
|
} else {
|
|
143
167
|
const currentSha = execFileSync("git", ["rev-parse", "HEAD"], {
|
|
144
168
|
cwd: worktree.path,
|