@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 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 **0.84.0 or newer**: the [`SubagentWorkflow`](#subagentworkflow) tool builds on `constrainedSampling` (pi 0.82.0) and pi-tui's `stripTerminalSequences` (0.84.0). The `peerDependencies` range declares it, so npm flags an older pi at install time.
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.18.4",
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.19.0",
74
- "reviewedVersion": "0.19.0",
72
+ "version": "0.20.0",
73
+ "reviewedVersion": "0.20.0",
75
74
  "repository": "https://github.com/tintinweb/pi-subagents",
76
- "commit": "95d10867f391636e6563e3885e972618083f306b"
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 "@sinclair/typebox"
38
+ import { Type } from "typebox"
39
39
  import { abortable } from "./abortable.js"
40
40
  import { hasAgentBadge, renderAgentName } from "./agent-color.js"
41
41
  import {
@@ -1,4 +1,4 @@
1
- import { Type } from "@sinclair/typebox"
1
+ import { Type } from "typebox"
2
2
  import type {
3
3
  AgentConfig,
4
4
  IsolationMode,
@@ -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, projected by pi's own `sessionEntryToContextMessages`, not
16
- * `inherit_context`'s text rendering of them.
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
- * Its `thinkingLevel` is NOT used, and is the one place the newer API would be
35
- * better. `getSessionContextSettings` starts at "off" and moves only on an
36
- * explicit `thinking_level_change` entry, so a session where nobody ran
37
- * `/think` reports "off" rather than the level it is really using. Omitting the
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, ThinkingLevel } from "./types.js"
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, _cloneCtx) => {
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
- ctx,
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
- // The conversation as the main session resolves it: compaction applied,
159
- // branch summaries substituted.
160
- const conversation = buildSessionContext(
161
- ctx.sessionManager.getEntries(),
162
- ctx.sessionManager.getLeafId(),
163
- )
164
- // Pi 0.82.0 added this; below it the field is absent and the clone takes
165
- // the settings level instead, which is what a session that never ran
166
- // `/think` is on anyway. Same shim shape as `modelRuntime` below.
167
- const thinkingLevel = (ctx as { thinkingLevel?: ThinkingLevel })
168
- .thinkingLevel
169
- const sessionManager = SessionManager.inMemory(ctx.cwd)
170
- for (const entry of conversation.messages) {
171
- // The live prompt is copied below. Do not replay the parent's system
172
- // messages: newer Pi also stores its tool declarations there.
173
- if ((entry.role as string) === "system") continue
174
- // Summary messages are projections, not appendable session entries.
175
- // Preserve exactly the user message Pi would send for each summary.
176
- if (
177
- entry.role === "compactionSummary" ||
178
- entry.role === "branchSummary"
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
- // Nothing about the copy is worth persisting, and an in-memory manager
203
- // is also what keeps the real session untouched.
204
- sessionManager,
205
- resourceLoader,
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
@@ -6,7 +6,7 @@ import {
6
6
  type ExtensionContext,
7
7
  type ToolDefinition,
8
8
  } from "@earendil-works/pi-coding-agent"
9
- import { Type } from "@sinclair/typebox"
9
+ import { Type } from "typebox"
10
10
  import { abortable } from "./abortable.js"
11
11
  import {
12
12
  buildAgentRegistry,
@@ -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 spawn writes
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
- let writtenCount = startIndex ?? 1
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 and both
11
- * are installed here. `@sinclair/typebox` (0.34) dispatches on a `Kind` symbol
12
- * that a schema arriving over the wire does not carry, so `Value.Check` throws
13
- * `Unknown type` on a plain JSON Schema — and `Type.Unsafe` does not help, it
14
- * stamps a `Kind` that is not registered. `typebox` v1 is a standards JSON
15
- * Schema validator and takes the schema as-is. It is also the package pi itself
16
- * types `ToolDefinition.parameters` against, so the same schema object serves
17
- * both roles with no conversion.
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
  *