@herbertgao/pi-subagents 0.18.4 → 0.19.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 +14 -0
- package/README.md +1 -1
- package/package.json +4 -5
- package/src/index.ts +1 -1
- package/src/invocation-config.ts +1 -1
- package/src/mention-clone.ts +51 -74
- package/src/nested-tools.ts +1 -1
- package/src/output-file.ts +15 -6
- package/src/workflow/collisions.ts +4 -1
- package/src/workflow/json-schema.ts +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#273](https://github.com/HerbertGao/pi-extensions/pull/273) [`e6ee379`](https://github.com/HerbertGao/pi-extensions/commit/e6ee379393c9f5dde67955d4c28da650835d857f) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Rebase `@herbertgao/pi-subagents` onto upstream v0.20.0 (`13106ab`) and adopt upstream's own mention-clone rewrite: the clone takes its history through `SessionManager.inMemory` and receives the live system prompt through a `DefaultResourceLoader` override instead of the previous context-projection path. The local behavior that hands the spawn the main session's context augmented with the clone's `tools` and `executeTool` is retained, so mentioned agents keep Pi 1.x tool-call capabilities while staying attributed to the main session.
|
|
8
|
+
|
|
9
|
+
Two local patches remain because their upstream pull requests are still open: preserving a worktree when cleanup fails (`tintinweb/pi-subagents#268`, with the refused `--no-gpg-sign` preservation commit) and retaining unconsumed terminal results past the automatic cleanup timer (`tintinweb/pi-subagents#348`). Upstream now declares `typebox` as a host-provided peer and drops `@sinclair/typebox` from runtime dependencies. No installed configuration, credentials, or sessions are migrated.
|
|
10
|
+
|
|
11
|
+
## 0.18.5
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#258](https://github.com/HerbertGao/pi-extensions/pull/258) [`4f505b1`](https://github.com/HerbertGao/pi-extensions/commit/4f505b1456c5ea766121ee915f14ad29e48973e7) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Keep Pi 1.x tool-call capabilities when starting a mentioned agent through a conversation clone, while preserving attribution to the main session.
|
|
16
|
+
|
|
3
17
|
## 0.18.4
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Or load directly for development:
|
|
|
47
47
|
pi -e ./src/index.ts
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Requires pi **
|
|
50
|
+
Requires pi **1.1.0 or newer**: pi 0.86 and 0.87 moved the system prompt and conversation history into the session transcript, and the `@handle` conversation clone and the agent transcript files are built on that model. The `peerDependencies` range declares it, so npm flags an older pi at install time.
|
|
51
51
|
|
|
52
52
|
### Other hosts
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Claude Code-style autonomous subagents and workflow orchestration for Pi, with HerbertGao-maintained UI extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
"@earendil-works/pi-ai": "*",
|
|
56
56
|
"@earendil-works/pi-coding-agent": "*",
|
|
57
57
|
"@earendil-works/pi-tui": "*",
|
|
58
|
-
"@sinclair/typebox": "*",
|
|
59
58
|
"typebox": "*"
|
|
60
59
|
},
|
|
61
60
|
"engines": {
|
|
@@ -70,9 +69,9 @@
|
|
|
70
69
|
},
|
|
71
70
|
"x-upstream": {
|
|
72
71
|
"package": "@tintinweb/pi-subagents",
|
|
73
|
-
"version": "0.
|
|
74
|
-
"reviewedVersion": "0.
|
|
72
|
+
"version": "0.20.0",
|
|
73
|
+
"reviewedVersion": "0.20.0",
|
|
75
74
|
"repository": "https://github.com/tintinweb/pi-subagents",
|
|
76
|
-
"commit": "
|
|
75
|
+
"commit": "13106ab6f608b3acc54185da5299ee7fbaabb6b9"
|
|
77
76
|
}
|
|
78
77
|
}
|
package/src/index.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
Spacer,
|
|
36
36
|
Text,
|
|
37
37
|
} from "@earendil-works/pi-tui"
|
|
38
|
-
import { Type } from "
|
|
38
|
+
import { Type } from "typebox"
|
|
39
39
|
import { abortable } from "./abortable.js"
|
|
40
40
|
import { hasAgentBadge, renderAgentName } from "./agent-color.js"
|
|
41
41
|
import {
|
package/src/invocation-config.ts
CHANGED
package/src/mention-clone.ts
CHANGED
|
@@ -12,31 +12,23 @@
|
|
|
12
12
|
* So the turn happens somewhere else. The conversation is cloned into a
|
|
13
13
|
* throwaway in-memory session — same messages, same system prompt, same model —
|
|
14
14
|
* and that copy takes the turn off-screen. A literal clone: the session's own
|
|
15
|
-
* entries,
|
|
16
|
-
*
|
|
15
|
+
* entries, not `inherit_context`'s text rendering of them.
|
|
16
|
+
*
|
|
17
|
+
* Pi builds every request from the session manager, so that is where the
|
|
18
|
+
* clone's history goes: the parent's root-to-leaf entries, handed to
|
|
19
|
+
* `SessionManager.inMemory`. The clone projects them itself, compaction and
|
|
20
|
+
* branch summaries included, so a long conversation clones as what the main
|
|
21
|
+
* model is actually working from. A conversation with nothing in it yet clones
|
|
22
|
+
* to nothing in it yet, which is the correct answer rather than a failure.
|
|
17
23
|
*
|
|
18
24
|
* Cloned from memory rather than from the session file, which cannot be relied
|
|
19
25
|
* on: `SessionManager._persist` withholds every write until the first assistant
|
|
20
26
|
* message lands, so a fork taken before then reads an empty file and throws.
|
|
21
|
-
* `buildSessionContext()` has no such timing, and is compaction-aware — it walks
|
|
22
|
-
* the leaf path and substitutes the summary for entries folded into it, so a
|
|
23
|
-
* long conversation clones as what the main model is actually working from. A
|
|
24
|
-
* conversation with nothing in it yet clones to nothing in it yet, which is the
|
|
25
|
-
* correct answer rather than a failure.
|
|
26
|
-
*
|
|
27
|
-
* It is also the oldest of the equivalent Pi APIs — `buildContextEntries` on
|
|
28
|
-
* ReadonlySessionManager and the `sessionEntryToContextMessages` export both
|
|
29
|
-
* arrived in 0.80.5 — where this one has been exported unchanged from before
|
|
30
|
-
* the declared peer floor, and is the same code path (`byId` is only an index
|
|
31
|
-
* cache, so passing it or not cannot change the result). Keeping the floor
|
|
32
|
-
* honest costs nothing here: see the `compat-floor-pi` job.
|
|
33
27
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* field instead lets `createAgentSession` resolve it from settings, which is
|
|
39
|
-
* that real level.
|
|
28
|
+
* The system prompt cannot ride along in those entries. Pi rebuilds it from the
|
|
29
|
+
* session's own options on every request and patches the difference over the
|
|
30
|
+
* transcript, so the parent's prompt would be replaced by the clone's. It goes
|
|
31
|
+
* in through the resource loader's prompt override instead.
|
|
40
32
|
*
|
|
41
33
|
* Three details make the spawn belong to the real session rather than the
|
|
42
34
|
* clone:
|
|
@@ -63,8 +55,6 @@
|
|
|
63
55
|
|
|
64
56
|
import type { Model } from "@earendil-works/pi-ai"
|
|
65
57
|
import {
|
|
66
|
-
buildSessionContext,
|
|
67
|
-
convertToLlm,
|
|
68
58
|
createAgentSession,
|
|
69
59
|
DefaultResourceLoader,
|
|
70
60
|
type ExtensionContext,
|
|
@@ -74,7 +64,7 @@ import {
|
|
|
74
64
|
} from "@earendil-works/pi-coding-agent"
|
|
75
65
|
import { runInChildSessionContext } from "./child-context.js"
|
|
76
66
|
import { agentMentionReminder } from "./mention.js"
|
|
77
|
-
import type { SubagentType
|
|
67
|
+
import type { SubagentType } from "./types.js"
|
|
78
68
|
|
|
79
69
|
export interface MentionCloneOptions {
|
|
80
70
|
/** The MAIN session's context — what the spawn is attributed to, and the
|
|
@@ -108,7 +98,7 @@ export async function runMentionClone(
|
|
|
108
98
|
let spawned = false
|
|
109
99
|
const cloneAgentTool: ToolDefinition = {
|
|
110
100
|
...agentTool,
|
|
111
|
-
execute: (_cloneToolCallId, params, signal, onUpdate,
|
|
101
|
+
execute: (_cloneToolCallId, params, signal, onUpdate, cloneCtx) => {
|
|
112
102
|
// One spawn per mention. The clone has a single tool and every reason to
|
|
113
103
|
// stop after using it, but a model that decides to "also" launch a second
|
|
114
104
|
// agent would do it where nobody can see and nobody asked.
|
|
@@ -138,7 +128,8 @@ export async function runMentionClone(
|
|
|
138
128
|
} as typeof params,
|
|
139
129
|
signal,
|
|
140
130
|
onUpdate,
|
|
141
|
-
|
|
131
|
+
// Keep Pi's tool-call capabilities, but attribute the spawn to the main session.
|
|
132
|
+
{ ...ctx, tools: cloneCtx.tools, executeTool: cloneCtx.executeTool },
|
|
142
133
|
)
|
|
143
134
|
},
|
|
144
135
|
}
|
|
@@ -155,54 +146,40 @@ export async function runMentionClone(
|
|
|
155
146
|
const parentModelRuntime = (
|
|
156
147
|
ctx.modelRegistry as unknown as { runtime?: unknown }
|
|
157
148
|
).runtime
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for (const message of convertToLlm([entry]))
|
|
181
|
-
sessionManager.appendMessage(message)
|
|
182
|
-
} else {
|
|
183
|
-
sessionManager.appendMessage(entry)
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const systemPrompt = ctx.getSystemPrompt?.()
|
|
187
|
-
const created = await runInChildSessionContext(async () => {
|
|
188
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
189
|
-
cwd: ctx.cwd,
|
|
190
|
-
agentDir: getAgentDir(),
|
|
191
|
-
extensionFactories: [
|
|
192
|
-
(pi) => {
|
|
193
|
-
pi.on("before_agent_start", () =>
|
|
194
|
-
systemPrompt ? { systemPrompt } : undefined,
|
|
195
|
-
)
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
})
|
|
199
|
-
await resourceLoader.reload()
|
|
200
|
-
return createAgentSession({
|
|
149
|
+
// Absent when the session has no level of its own; the clone then takes the
|
|
150
|
+
// settings level, which is what a session that never ran `/think` is on.
|
|
151
|
+
const thinkingLevel = ctx.thinkingLevel
|
|
152
|
+
// The live system prompt, not the one the clone would build from cwd and
|
|
153
|
+
// agentDir — extensions contribute to it per turn. Everything the loader
|
|
154
|
+
// would add on top (AGENTS.md, skills, APPEND_SYSTEM.md) is already in the
|
|
155
|
+
// rendered prompt, so it is suppressed, as agent-runner does.
|
|
156
|
+
const systemPrompt = ctx.getSystemPrompt()
|
|
157
|
+
const loader = new DefaultResourceLoader({
|
|
158
|
+
cwd: ctx.cwd,
|
|
159
|
+
agentDir: getAgentDir(),
|
|
160
|
+
noExtensions: true,
|
|
161
|
+
noSkills: true,
|
|
162
|
+
noPromptTemplates: true,
|
|
163
|
+
noThemes: true,
|
|
164
|
+
noContextFiles: true,
|
|
165
|
+
systemPromptOverride: (base: string | undefined) => systemPrompt || base,
|
|
166
|
+
appendSystemPromptOverride: () => [],
|
|
167
|
+
})
|
|
168
|
+
await runInChildSessionContext(() => loader.reload())
|
|
169
|
+
const created = await runInChildSessionContext(() =>
|
|
170
|
+
createAgentSession({
|
|
201
171
|
cwd: ctx.cwd,
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
172
|
+
// The parent's root-to-leaf path, which the clone projects itself:
|
|
173
|
+
// compaction applied, branch summaries substituted. Not `getEntries()`,
|
|
174
|
+
// whose last entry is not the leaf after `/tree` navigation. In memory,
|
|
175
|
+
// so nothing about the copy is persisted and the real session stays
|
|
176
|
+
// untouched.
|
|
177
|
+
sessionManager: SessionManager.inMemory(
|
|
178
|
+
ctx.cwd,
|
|
179
|
+
undefined,
|
|
180
|
+
ctx.sessionManager.getBranch(),
|
|
181
|
+
),
|
|
182
|
+
resourceLoader: loader,
|
|
206
183
|
model: ctx.model as Model<never> | undefined,
|
|
207
184
|
...(thinkingLevel && { thinkingLevel }),
|
|
208
185
|
modelRegistry: ctx.modelRegistry,
|
|
@@ -219,8 +196,8 @@ export async function runMentionClone(
|
|
|
219
196
|
// agent-runner's `tools: sessionTools` beside its nested `customTools`.
|
|
220
197
|
tools: [cloneAgentTool.name],
|
|
221
198
|
customTools: [cloneAgentTool],
|
|
222
|
-
} as Parameters<typeof createAgentSession>[0])
|
|
223
|
-
|
|
199
|
+
} as Parameters<typeof createAgentSession>[0]),
|
|
200
|
+
)
|
|
224
201
|
session = created.session
|
|
225
202
|
|
|
226
203
|
// User text first, reminder after — the order Claude Code's attachment
|
package/src/nested-tools.ts
CHANGED
package/src/output-file.ts
CHANGED
|
@@ -122,17 +122,27 @@ export function streamToOutputFile(
|
|
|
122
122
|
cwd: string,
|
|
123
123
|
startIndex?: number,
|
|
124
124
|
): () => void {
|
|
125
|
-
// Index of the first message this stream is responsible for. A
|
|
126
|
-
// messages[0] as the initial prompt entry, so it starts at 1. A resume hands
|
|
125
|
+
// Index of the first message this stream is responsible for. A resume hands
|
|
127
126
|
// in the session's length as of just before the run: the session already
|
|
128
127
|
// holds every prior turn, and re-emitting those would duplicate history that
|
|
129
|
-
// is already in the file.
|
|
130
|
-
|
|
128
|
+
// is already in the file. A spawn has already written its prompt as the
|
|
129
|
+
// initial entry, so it starts after the first user message — found on the
|
|
130
|
+
// first flush, because pi puts a system message ahead of it and adds both
|
|
131
|
+
// only once the prompt runs.
|
|
132
|
+
let writtenCount = startIndex
|
|
131
133
|
|
|
132
134
|
const flush = () => {
|
|
133
135
|
const messages = session.messages
|
|
136
|
+
if (writtenCount === undefined) {
|
|
137
|
+
const prompt = messages.findIndex((m) => m.role === "user")
|
|
138
|
+
if (prompt === -1) return
|
|
139
|
+
writtenCount = prompt + 1
|
|
140
|
+
}
|
|
134
141
|
while (writtenCount < messages.length) {
|
|
135
|
-
const msg = messages[writtenCount]
|
|
142
|
+
const msg = messages[writtenCount++]
|
|
143
|
+
// Pi's prompt and tool-loadout records; a Claude Code transcript has no
|
|
144
|
+
// such entry.
|
|
145
|
+
if (msg.role === "system") continue
|
|
136
146
|
const entry = {
|
|
137
147
|
isSidechain: true,
|
|
138
148
|
agentId,
|
|
@@ -151,7 +161,6 @@ export function streamToOutputFile(
|
|
|
151
161
|
} catch {
|
|
152
162
|
/* ignore write errors */
|
|
153
163
|
}
|
|
154
|
-
writtenCount++
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
|
|
@@ -46,11 +46,14 @@ import { SUBAGENT_TOOL_NAMES } from "../agent-runner.js"
|
|
|
46
46
|
*
|
|
47
47
|
* Our own name, because pi resolves a duplicate registration silently, and
|
|
48
48
|
* Claude Code's bare `Workflow`, because a port of that tool is what a second
|
|
49
|
-
* workflow extension most likely calls itself.
|
|
49
|
+
* workflow extension most likely calls itself. Lowercase `workflow` is the same
|
|
50
|
+
* tool by pi convention: it is what `@quintinshaw/pi-dynamic-workflows`
|
|
51
|
+
* registers, and the match here is exact, so the case has to be listed by hand.
|
|
50
52
|
*/
|
|
51
53
|
export const FOREIGN_WORKFLOW_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
52
54
|
SUBAGENT_TOOL_NAMES.WORKFLOW,
|
|
53
55
|
"Workflow",
|
|
56
|
+
"workflow",
|
|
54
57
|
])
|
|
55
58
|
|
|
56
59
|
/** The fields of a registered tool this decision reads. */
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ## Which typebox
|
|
9
9
|
*
|
|
10
|
-
* **`typebox`, not `@sinclair/typebox`.** They are different packages
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* both roles with no
|
|
10
|
+
* **`typebox`, not `@sinclair/typebox`.** They are different packages.
|
|
11
|
+
* `@sinclair/typebox` (0.34) dispatches on a `Kind` symbol that a schema
|
|
12
|
+
* arriving over the wire does not carry, so `Value.Check` throws `Unknown type`
|
|
13
|
+
* on a plain JSON Schema — and `Type.Unsafe` does not help, it stamps a `Kind`
|
|
14
|
+
* that is not registered. `typebox` v1 is a standards JSON Schema validator and
|
|
15
|
+
* takes the schema as-is. It is also the package pi itself types
|
|
16
|
+
* `ToolDefinition.parameters` against and supplies to extensions at runtime
|
|
17
|
+
* (a peer dependency here), so the same schema object serves both roles with no
|
|
18
|
+
* conversion.
|
|
18
19
|
*
|
|
19
20
|
* ## Why we validate at all
|
|
20
21
|
*
|