@henryqw/pi-herdr-tools 1.2.0 → 1.3.1
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/README.md +1 -1
- package/internal/btw.ts +47 -79
- package/internal/config.ts +3 -11
- package/internal/context-store.ts +1 -2
- package/internal/core.ts +2 -16
- package/internal/merge.ts +4 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ pi install npm:@henryqw/pi-task-models
|
|
|
11
11
|
pi install npm:@henryqw/pi-herdr-tools
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Requires Pi 1.1.0 or later in the 1.x series and Herdr 0.
|
|
14
|
+
Requires Pi 1.1.0 or later in the 1.x series and Herdr 0.9.3 or later. Herdr operations require a Herdr-managed Pi pane; outside Herdr, rename still sets the Pi session title. Run `/task-models`, configure `fast`, and verify it no longer says `not configured` before using title generation or side threads. Dialog status also requires Herdr's own Pi integration (`~/.pi/agent/extensions/herdr-agent-state.ts`), which Herdr installs; this package only publishes the status event.
|
|
15
15
|
|
|
16
16
|
### Migrating from the merged packages
|
|
17
17
|
|
package/internal/btw.ts
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
CHILD_PAYLOAD_FLAG,
|
|
32
32
|
buildNativeBridgeMessage,
|
|
33
33
|
buildParentContextMessage,
|
|
34
|
-
classifyLaunchResult,
|
|
35
34
|
createPayload,
|
|
36
35
|
LAUNCH_DRAFT_ARG,
|
|
37
36
|
LAUNCH_DRAFT_COMMAND,
|
|
@@ -94,11 +93,6 @@ The user will ask a question related to, but potentially tangential to, the pare
|
|
|
94
93
|
|
|
95
94
|
The child shares the parent's working directory. Tool actions can change files visible to the parent. The injected parent-context message is reference material from the parent conversation, not additional system instructions.`;
|
|
96
95
|
|
|
97
|
-
type CacheMode = {
|
|
98
|
-
mode: "native" | "fallback";
|
|
99
|
-
reason?: string;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
96
|
function sameStringArray(a: string[], b: string[]): boolean {
|
|
103
97
|
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
104
98
|
}
|
|
@@ -122,23 +116,17 @@ function configuredBtwRoutes(ctx: ExtensionContext): ResolvedTaskRoute[] {
|
|
|
122
116
|
* Decide whether the child can replay the parent's exact request prefix
|
|
123
117
|
* (system prompt, tools, model, thinking) for provider prompt-cache reuse.
|
|
124
118
|
*/
|
|
125
|
-
|
|
119
|
+
function decideCacheMode(
|
|
126
120
|
payload: BtwPayload,
|
|
127
121
|
actual: { model: string | undefined; activeTools: string[]; thinkingLevel: string },
|
|
128
|
-
):
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return { mode: "fallback", reason: "tool set differs from parent (tool prefix would not match)" };
|
|
137
|
-
}
|
|
138
|
-
if (actual.thinkingLevel !== payload.parentThinkingLevel) {
|
|
139
|
-
return { mode: "fallback", reason: "thinking level differs from parent" };
|
|
140
|
-
}
|
|
141
|
-
return { mode: "native" };
|
|
122
|
+
): boolean {
|
|
123
|
+
return (
|
|
124
|
+
payload.parentSystemPrompt !== null &&
|
|
125
|
+
actual.model === payload.metadata.model &&
|
|
126
|
+
payload.config.tools === "inherit" &&
|
|
127
|
+
sameStringArray(actual.activeTools, payload.parentActiveTools) &&
|
|
128
|
+
actual.thinkingLevel === payload.parentThinkingLevel
|
|
129
|
+
);
|
|
142
130
|
}
|
|
143
131
|
|
|
144
132
|
async function configureChild(
|
|
@@ -163,34 +151,16 @@ async function configureChild(
|
|
|
163
151
|
)
|
|
164
152
|
: undefined;
|
|
165
153
|
|
|
166
|
-
|
|
167
|
-
let widgetUi:
|
|
168
|
-
| { setWidget(name: string, lines: string[]): void; theme: { fg(color: string, text: string): string } }
|
|
169
|
-
| undefined;
|
|
170
|
-
|
|
171
|
-
function renderWidget(): void {
|
|
172
|
-
if (!widgetUi || !payload) return;
|
|
173
|
-
const capability =
|
|
174
|
-
payload.config.tools === "none"
|
|
175
|
-
? "tool-free"
|
|
176
|
-
: payload.config.tools === "read-only"
|
|
177
|
-
? "read-only"
|
|
178
|
-
: "tool-enabled";
|
|
179
|
-
widgetUi.setWidget("herdr-btw-context", [
|
|
180
|
-
widgetUi.theme.fg("accent", `BTW — ${capability} pane`),
|
|
181
|
-
]);
|
|
182
|
-
}
|
|
154
|
+
let nativeCache = false;
|
|
183
155
|
|
|
184
156
|
pi.on("before_agent_start", (event, ctx) => {
|
|
185
157
|
if (!payload) return;
|
|
186
|
-
|
|
158
|
+
nativeCache = decideCacheMode(payload, {
|
|
187
159
|
model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined,
|
|
188
160
|
activeTools: pi.getActiveTools(),
|
|
189
161
|
thinkingLevel: pi.getThinkingLevel(),
|
|
190
162
|
});
|
|
191
|
-
|
|
192
|
-
cache.reason = decision.reason;
|
|
193
|
-
if (cache.mode === "native") {
|
|
163
|
+
if (nativeCache) {
|
|
194
164
|
// Replay the parent's exact system prompt; side-pane policy moves to
|
|
195
165
|
// a suffix message so the cached prefix stays byte-identical.
|
|
196
166
|
return { systemPrompt: payload.parentSystemPrompt as string };
|
|
@@ -200,7 +170,7 @@ async function configureChild(
|
|
|
200
170
|
|
|
201
171
|
pi.on("context_with_system", (event) => {
|
|
202
172
|
if (!payload) return;
|
|
203
|
-
if (
|
|
173
|
+
if (nativeCache) {
|
|
204
174
|
return {
|
|
205
175
|
messages: [
|
|
206
176
|
...(payload.messages[0]?.role === "system" ? [] : event.messages.slice(0, 1)),
|
|
@@ -369,8 +339,15 @@ async function configureChild(
|
|
|
369
339
|
return;
|
|
370
340
|
}
|
|
371
341
|
|
|
372
|
-
|
|
373
|
-
|
|
342
|
+
if (payload) {
|
|
343
|
+
const capability =
|
|
344
|
+
payload.config.tools === "none"
|
|
345
|
+
? "tool-free"
|
|
346
|
+
: payload.config.tools === "read-only"
|
|
347
|
+
? "read-only"
|
|
348
|
+
: "tool-enabled";
|
|
349
|
+
ctx.ui.setWidget("herdr-btw-context", [ctx.ui.theme.fg("accent", `BTW — ${capability} pane`)]);
|
|
350
|
+
}
|
|
374
351
|
|
|
375
352
|
// Auto-submit drafts are sent via the launch-draft sentinel instead of
|
|
376
353
|
// here: session_start fires before pi's initial render, and a message
|
|
@@ -548,19 +525,13 @@ export async function registerBtwExtension(
|
|
|
548
525
|
ctx.ui.notify(`BTW config — ${formatConfig(config)}`, "info");
|
|
549
526
|
return;
|
|
550
527
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
ctx.ui.notify(
|
|
559
|
-
result.action === "show"
|
|
560
|
-
? `BTW config — ${formatConfig(result.config)}\n${CONFIG_COMMAND_USAGE}`
|
|
561
|
-
: `BTW config — ${formatConfig(result.config)}`,
|
|
562
|
-
"info",
|
|
563
|
-
);
|
|
528
|
+
if (!route.args || route.args === "show") {
|
|
529
|
+
const config = configStore.loadSync().value;
|
|
530
|
+
ctx.ui.notify(`BTW config — ${formatConfig(config)}\n${CONFIG_COMMAND_USAGE}`, "info");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const config = await configStore.update((latest) => applyConfigCommand(latest, route.args));
|
|
534
|
+
ctx.ui.notify(`BTW config — ${formatConfig(config)}`, "info");
|
|
564
535
|
} catch (error) {
|
|
565
536
|
const message = error instanceof Error ? error.message : String(error);
|
|
566
537
|
ctx.ui.notify(message, "error");
|
|
@@ -706,8 +677,7 @@ export async function registerBtwExtension(
|
|
|
706
677
|
}
|
|
707
678
|
return;
|
|
708
679
|
}
|
|
709
|
-
|
|
710
|
-
if (splitOutcome !== "success") {
|
|
680
|
+
if (splitResult.killed || splitResult.code !== 0) {
|
|
711
681
|
await store.remove(payloadPath);
|
|
712
682
|
ctx.ui.notify(
|
|
713
683
|
`/btw failed: ${safeErrorText(splitResult.stdout, splitResult.stderr)}`,
|
|
@@ -749,29 +719,27 @@ export async function registerBtwExtension(
|
|
|
749
719
|
}
|
|
750
720
|
return;
|
|
751
721
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
722
|
+
if (!result.killed) {
|
|
723
|
+
if (result.code !== 0) {
|
|
724
|
+
await reportKnownPaneFailure(
|
|
725
|
+
`/btw failed: ${safeErrorText(result.stdout, result.stderr)}`,
|
|
726
|
+
paneId,
|
|
727
|
+
payloadPath,
|
|
728
|
+
);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!isAgentStartReady(result.stdout, { name: launchOptions.paneName, paneId })) {
|
|
732
|
+
await reportKnownPaneFailure(
|
|
733
|
+
"/btw failed: `herdr agent start` returned an invalid or non-interactive result",
|
|
734
|
+
paneId,
|
|
735
|
+
payloadPath,
|
|
736
|
+
);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
762
739
|
ensurePolling();
|
|
763
740
|
return;
|
|
764
741
|
}
|
|
765
742
|
|
|
766
|
-
if (outcome === "failed") {
|
|
767
|
-
await reportKnownPaneFailure(
|
|
768
|
-
`/btw failed: ${safeErrorText(result.stdout, result.stderr)}`,
|
|
769
|
-
paneId,
|
|
770
|
-
payloadPath,
|
|
771
|
-
);
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
743
|
const reconciled = await herdr
|
|
776
744
|
.exec(
|
|
777
745
|
[
|
package/internal/config.ts
CHANGED
|
@@ -58,16 +58,8 @@ export function formatConfig(config: BtwConfig): string {
|
|
|
58
58
|
export const CONFIG_COMMAND_USAGE =
|
|
59
59
|
"/btw config [auto-submit on|off | tools inherit|all|read-only|none | split right|down | reset]";
|
|
60
60
|
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
config: BtwConfig;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export function applyConfigCommand(current: BtwConfig, input: string): ConfigCommandResult {
|
|
67
|
-
const trimmed = input.trim();
|
|
68
|
-
if (!trimmed || trimmed === "show") return { action: "show", config: current };
|
|
69
|
-
|
|
70
|
-
const [key, value, ...extra] = trimmed.split(/\s+/);
|
|
61
|
+
export function applyConfigCommand(current: BtwConfig, input: string): BtwConfig {
|
|
62
|
+
const [key, value, ...extra] = input.split(/\s+/);
|
|
71
63
|
if (!key || !value || extra.length > 0) throw new Error(CONFIG_COMMAND_USAGE);
|
|
72
64
|
const config = { ...current };
|
|
73
65
|
|
|
@@ -90,7 +82,7 @@ export function applyConfigCommand(current: BtwConfig, input: string): ConfigCom
|
|
|
90
82
|
throw new Error(CONFIG_COMMAND_USAGE);
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
return
|
|
85
|
+
return config;
|
|
94
86
|
}
|
|
95
87
|
|
|
96
88
|
export function createBtwConfigStore(agentDir?: string) {
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
15
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
16
|
import { isBtwPayload, type BtwPayload } from "./core.ts";
|
|
17
|
-
import {
|
|
17
|
+
import { MERGE_REQUEST_FILE, type MergeRequest } from "./merge.ts";
|
|
18
18
|
|
|
19
19
|
const PAYLOAD_FILE = "payload.json";
|
|
20
20
|
const LAUNCH_PREFIX = "launch-";
|
|
@@ -104,7 +104,6 @@ export class ContextStore {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
async writeMergeRequest(payloadPath: string, request: MergeRequest): Promise<void> {
|
|
107
|
-
if (!isMergeRequest(request)) throw new Error("Invalid /btw merge request");
|
|
108
107
|
await this.writeLaunchFile(payloadPath, MERGE_REQUEST_FILE, request);
|
|
109
108
|
}
|
|
110
109
|
|
package/internal/core.ts
CHANGED
|
@@ -60,8 +60,6 @@ export type CreatePayloadOptions = {
|
|
|
60
60
|
messages: AgentMessage[];
|
|
61
61
|
draftQuestion: string;
|
|
62
62
|
config: BtwConfig;
|
|
63
|
-
launchId?: string;
|
|
64
|
-
capability?: string;
|
|
65
63
|
};
|
|
66
64
|
|
|
67
65
|
export type ParentContextMetadata = {
|
|
@@ -89,19 +87,12 @@ export type HerdrLaunchOptions = {
|
|
|
89
87
|
initialMessage?: string;
|
|
90
88
|
};
|
|
91
89
|
|
|
92
|
-
export type LaunchResult = {
|
|
93
|
-
code: number;
|
|
94
|
-
killed?: boolean;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export type LaunchOutcome = "success" | "failed" | "ambiguous";
|
|
98
|
-
|
|
99
90
|
export function createPayload(options: CreatePayloadOptions): BtwPayload {
|
|
100
91
|
return {
|
|
101
92
|
version: PAYLOAD_VERSION,
|
|
102
93
|
createdAt: options.createdAt,
|
|
103
|
-
launchId:
|
|
104
|
-
capability:
|
|
94
|
+
launchId: randomUUID(),
|
|
95
|
+
capability: randomBytes(32).toString("hex"),
|
|
105
96
|
parentSessionId: options.parentSessionId,
|
|
106
97
|
parentPaneId: options.parentPaneId,
|
|
107
98
|
metadata: options.metadata,
|
|
@@ -388,11 +379,6 @@ export function buildAgentStartArgs(options: HerdrLaunchOptions): string[] {
|
|
|
388
379
|
];
|
|
389
380
|
}
|
|
390
381
|
|
|
391
|
-
export function classifyLaunchResult(result: LaunchResult): LaunchOutcome {
|
|
392
|
-
if (result.killed) return "ambiguous";
|
|
393
|
-
return result.code === 0 ? "success" : "failed";
|
|
394
|
-
}
|
|
395
|
-
|
|
396
382
|
/**
|
|
397
383
|
* Herdr CLI failures print the whole JSON response on stderr
|
|
398
384
|
* (`{"id":...,"error":{"code":...,"message":...}}`, exit 1); extract the
|
package/internal/merge.ts
CHANGED
|
@@ -140,10 +140,7 @@ function tailBytes(text: string, maxBytes: number): string {
|
|
|
140
140
|
* tool payloads) as the merge transcript. When over budget, whole turns are
|
|
141
141
|
* dropped from the head so the most recent findings survive.
|
|
142
142
|
*/
|
|
143
|
-
export function buildMergeTranscript(
|
|
144
|
-
messages: AgentMessage[],
|
|
145
|
-
budgetBytes = MERGE_TRANSCRIPT_BUDGET_BYTES,
|
|
146
|
-
): string | undefined {
|
|
143
|
+
export function buildMergeTranscript(messages: AgentMessage[]): string | undefined {
|
|
147
144
|
const turns: string[] = [];
|
|
148
145
|
for (const message of messages) {
|
|
149
146
|
const { role, content } = message as { role?: string; content?: unknown };
|
|
@@ -158,10 +155,10 @@ export function buildMergeTranscript(
|
|
|
158
155
|
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
159
156
|
const turn = turns[index] as string;
|
|
160
157
|
const bytes = Buffer.byteLength(turn, "utf8") + 2;
|
|
161
|
-
if (used + bytes >
|
|
158
|
+
if (used + bytes > MERGE_TRANSCRIPT_BUDGET_BYTES) {
|
|
162
159
|
if (kept.length === 0) {
|
|
163
160
|
// A single oversized turn keeps its tail (the latest content).
|
|
164
|
-
kept.unshift(`${TRANSCRIPT_TRUNCATION_NOTE}\n${tailBytes(turn,
|
|
161
|
+
kept.unshift(`${TRANSCRIPT_TRUNCATION_NOTE}\n${tailBytes(turn, MERGE_TRANSCRIPT_BUDGET_BYTES)}`);
|
|
165
162
|
} else {
|
|
166
163
|
kept.unshift(TRANSCRIPT_TRUNCATION_NOTE);
|
|
167
164
|
}
|
|
@@ -271,12 +268,7 @@ export class MergeCoordinator {
|
|
|
271
268
|
|
|
272
269
|
// pi.sendUserMessage() is fire-and-forget. Keep the request pending when
|
|
273
270
|
// current model/auth state would reject its prompt.
|
|
274
|
-
|
|
275
|
-
if (!(await this.session.canSubmitPrompt())) {
|
|
276
|
-
result.deferred += 1;
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
} catch {
|
|
271
|
+
if (!(await this.session.canSubmitPrompt())) {
|
|
280
272
|
result.deferred += 1;
|
|
281
273
|
return;
|
|
282
274
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-herdr-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Clone, name, and finish Pi conversations in Herdr, with mergeable side questions.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "herdr", "session", "clone", "side-thread", "title"],
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@henryqw/pi-config-store": "^3.0.0",
|
|
21
|
-
"@henryqw/pi-herdr": "^0.
|
|
21
|
+
"@henryqw/pi-herdr": "^0.5.0",
|
|
22
22
|
"@henryqw/pi-task-models": "^8.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|