@giladbarnea/pi-user-agents 0.0.12 → 0.0.13
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/INTERNALS.md +8 -2
- package/attach.ts +12 -9
- package/package.json +1 -1
- package/runner.ts +29 -7
package/INTERNALS.md
CHANGED
|
@@ -81,11 +81,11 @@ The attached agent parks idle first (`runAttachedTurns` waits for an instruction
|
|
|
81
81
|
|
|
82
82
|
`h` and `-h` hand a child session to a Pi running in a new [herdr](https://herdr.dev) pane. Both go through `openPiInHerdrPane` (`herdr.ts`), a thin wrapper over the `herdr` CLI: `pane layout --current` picks the split direction (a wide caller pane splits right, a narrow or tall one down), `pane split --current --cwd <cwd> --no-focus` creates the pane without taking the user's focus, and `agent start <name> --kind pi --pane <id> -- <pi args>` starts pi there and returns once it is ready for input. The agent name is `agent-` plus the first 13 characters of the child's session id, which hold a v7 id's complete millisecond timestamp, so names stay unique among live agents. Every command runs with the caller's environment, so `--current` resolves the pane this Pi lives in; both actions are available only under `HERDR_ENV=1`, and the widget hint hides otherwise.
|
|
83
83
|
|
|
84
|
-
Both paths hand the pane's Pi `forwardedArgsForPane(...)`, the dispatch's forwarded tokens minus two groups: the *inert* options (`role: "inert"` in the option table — the session, approval, offline, and API-key families), which a background run ignores but a real `pi` process would obey (`--no-session` would run the pane ephemeral, `--fork` would open another file), and `--model`, `--provider`, `--thinking`, which the pane's Pi gets explicitly or from its file.
|
|
84
|
+
Both paths hand the pane's Pi the parent's launch extension options (see [Child extensions](#child-extensions)), then `forwardedArgsForPane(...)`, the dispatch's forwarded tokens minus two groups: the *inert* options (`role: "inert"` in the option table — the session, approval, offline, and API-key families), which a background run ignores but a real `pi` process would obey (`--no-session` would run the pane ephemeral, `--fork` would open another file), and `--model`, `--provider`, `--thinking`, which the pane's Pi gets explicitly or from its file.
|
|
85
85
|
|
|
86
86
|
**The move (`h`)** is attach in reverse, for another process, in three steps ordered so that failures cost as little as possible. First the delivery resolves the file — like `/agent-attach`, from the session directory listing by exact id; a child that has not answered yet has no file and the move stops here — and splits the pane; a herdr failure at this point leaves the agent untouched. Then the widget ends the agent here: abort, retire, wait for `finished` so the child session is disposed, and keep its row as a completed snapshot. Only then does pi start in the pane, so the file has one writer at a time; the dispatch record's forwarded args replay as CLI flags and the model and thinking level come from the file, as on any resume. A live agent has to be interrupted first. Once the agent is ended, its snapshot row survives whatever happens next — with `herdrPane` set on success, so it renders `herdr pane w1:p3` in place of the status word and withholds `h`, or without it on failure, still squashable. This is the squash pattern: the row persists as the record, and no transcript breadcrumb is written; `d` on the row announces `Detached session …` like any other, which stays true — the file is on disk.
|
|
87
87
|
|
|
88
|
-
**The dispatch (`-h`)** never creates an in-process child session, and branches off before the child resource load that only a background child needs. The child's session manager is built as usual (dispatch record, inherited snapshot) plus explicit `model_change` and `thinking_level_change` entries appended after the snapshot: pi resumes a session with the model of whichever comes last on the path, a `model_change` entry or an assistant message, and writes `model_change` itself only on its empty-session path, which the pane's Pi never takes for an inherited context. Without the entries a later `/agent-attach` or `/resume` would take the model of main's last answer, or the default. `flushSessionFile` then writes the file to disk, because pi defers a session file until its first assistant message and a snapshot with no assistant message (`-i`, or a main session that has not answered yet) would otherwise exist only in memory; when the snapshot already flushed it, the file is left alone. The pane's Pi runs `pi <forwarded args> --provider <p> --model <id> --thinking <level> --session <file> "<pane preamble + task>"`: the flags repeat the model and thinking level because an isolated child's file has no messages, and pi then takes the flags over the file. The positional prompt carries `PANE_DISPATCH_PREAMBLE`, which tells the child it is interactive in a pane rather than an ephemeral background process; `splitAtDispatchBoundary` accepts either preamble, so attach recognizes the child later. The widget row is a snapshot from birth, with nothing to squash; `-h` and `-s` conflict at parse time.
|
|
88
|
+
**The dispatch (`-h`)** never creates an in-process child session, and branches off before the child resource load that only a background child needs. The child's session manager is built as usual (dispatch record, inherited snapshot) plus explicit `model_change` and `thinking_level_change` entries appended after the snapshot: pi resumes a session with the model of whichever comes last on the path, a `model_change` entry or an assistant message, and writes `model_change` itself only on its empty-session path, which the pane's Pi never takes for an inherited context. Without the entries a later `/agent-attach` or `/resume` would take the model of main's last answer, or the default. `flushSessionFile` then writes the file to disk, because pi defers a session file until its first assistant message and a snapshot with no assistant message (`-i`, or a main session that has not answered yet) would otherwise exist only in memory; when the snapshot already flushed it, the file is left alone. The pane's Pi runs `pi <launch extension options> <forwarded args> --provider <p> --model <id> --thinking <level> --session <file> "<pane preamble + task>"`: the flags repeat the model and thinking level because an isolated child's file has no messages, and pi then takes the flags over the file. The positional prompt carries `PANE_DISPATCH_PREAMBLE`, which tells the child it is interactive in a pane rather than an ephemeral background process; `splitAtDispatchBoundary` accepts either preamble, so attach recognizes the child later. The widget row is a snapshot from birth, with nothing to squash; `-h` and `-s` conflict at parse time.
|
|
89
89
|
|
|
90
90
|
## Runtime sharing
|
|
91
91
|
|
|
@@ -93,6 +93,12 @@ Child agent sessions share the parent's `ModelRuntime` instance. This is critica
|
|
|
93
93
|
|
|
94
94
|
The shared runtime is extracted from the parent's `ModelRegistry` facade (`ctx.modelRegistry`) which wraps a `ModelRuntime` as a plain `.runtime` property.
|
|
95
95
|
|
|
96
|
+
## Child extensions
|
|
97
|
+
|
|
98
|
+
A child loads the extensions its parent loaded. Settings discovery covers configured extensions, but `pi -e <path>` and `pi --no-extensions` live only in the parent process's arguments. `parentExtensionArgs` re-reads them from `process.argv` with Pi's own parser and resolves local `-e` paths against the launch directory, as Pi does. Every child path puts them before the dispatch's forwarded options, as on one pi command line: the background child's resource loader, `/agent-attach`, and both herdr pane paths. The dispatch's `-e` adds extensions, and its `--no-extensions` drops configured ones while explicit `-e` paths still load, as in pi.
|
|
99
|
+
|
|
100
|
+
The shared runtime alone is not enough. It carries the parent's providers, but a `-e` extension's tools, commands, and hooks exist only where the extension loads.
|
|
101
|
+
|
|
96
102
|
## Model name & alias resolution
|
|
97
103
|
|
|
98
104
|
User-defined model aliases (`name` in `models.json` `modelOverrides`) take precedence: `-m opus` resolves to `claude-bridge/claude-opus-4-6` directly, without ambiguity. Other model values must exactly match a live model ID or canonical `provider/id` reference before the extension calls the SDK's `resolveCliModel`. Partial and custom model IDs are rejected.
|
package/attach.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { herdrAgentName, insideHerdr, splitPane, startPi } from "./herdr.js";
|
|
|
6
6
|
import { conversationFingerprint } from "./rebase.js";
|
|
7
7
|
import {
|
|
8
8
|
assistantText,
|
|
9
|
-
buildChildResourceLoaderOptions,
|
|
10
9
|
createChildServices,
|
|
11
10
|
DISPATCH_PREAMBLES,
|
|
11
|
+
parentExtensionArgs,
|
|
12
12
|
parseForwardedArgs,
|
|
13
13
|
reportAgentFailure,
|
|
14
14
|
resolveToolOptions,
|
|
@@ -278,10 +278,7 @@ async function attachUserAgent(
|
|
|
278
278
|
// dispatch, or a /fork) restores pi defaults.
|
|
279
279
|
const record = readDispatchRecord(childSessionManager);
|
|
280
280
|
const dispatchArgs = parseForwardedArgs(record?.forwardedArgs ?? []);
|
|
281
|
-
const services = await createChildServices(
|
|
282
|
-
ctx,
|
|
283
|
-
buildChildResourceLoaderOptions(dispatchArgs, ctx.cwd),
|
|
284
|
-
);
|
|
281
|
+
const services = await createChildServices(ctx, record?.forwardedArgs ?? []);
|
|
285
282
|
// A non-empty session manager takes pi's resume path: messages, model, and thinking level
|
|
286
283
|
// are restored from the child's own file; tools and resources come from the dispatch record.
|
|
287
284
|
const { session, modelFallbackMessage } = await createAgentSessionFromServices({
|
|
@@ -332,9 +329,10 @@ async function attachUserAgent(
|
|
|
332
329
|
|
|
333
330
|
/**
|
|
334
331
|
* The herdr mechanism: attach a child session to a Pi in a new herdr pane instead of to this
|
|
335
|
-
* process. The pane's Pi resumes the child's file with
|
|
336
|
-
* CLI flags (see `forwardedArgsForPane`); the model and thinking level
|
|
337
|
-
* any resume. The file is resolved before the pane is split, so an agent
|
|
332
|
+
* process. The pane's Pi resumes the child's file with this Pi's launch extension options and the
|
|
333
|
+
* dispatch record's options as CLI flags (see `forwardedArgsForPane`); the model and thinking level
|
|
334
|
+
* come from the file, as on any resume. The file is resolved before the pane is split, so an agent
|
|
335
|
+
* without one costs no pane.
|
|
338
336
|
*/
|
|
339
337
|
export function createHerdrDelivery(
|
|
340
338
|
getSessionContext: () => ExtensionCommandContext | undefined,
|
|
@@ -350,7 +348,12 @@ export function createHerdrDelivery(
|
|
|
350
348
|
)?.file;
|
|
351
349
|
if (!file) throw new Error("the agent has no session file yet (it has not answered)");
|
|
352
350
|
const record = readDispatchRecord(SessionManager.open(file));
|
|
353
|
-
const piArgs = [
|
|
351
|
+
const piArgs = [
|
|
352
|
+
...parentExtensionArgs(),
|
|
353
|
+
...forwardedArgsForPane(record?.forwardedArgs ?? []),
|
|
354
|
+
"--session",
|
|
355
|
+
file,
|
|
356
|
+
];
|
|
354
357
|
const paneId = await splitPane(ctx.cwd);
|
|
355
358
|
return { paneId, start: () => startPi(paneId, herdrAgentName(agent.sessionId), piArgs) };
|
|
356
359
|
},
|
package/package.json
CHANGED
package/runner.ts
CHANGED
|
@@ -147,10 +147,7 @@ async function startUserAgent(
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// The pane's Pi loads its own resources; only a background child needs them here.
|
|
150
|
-
const services = await createChildServices(
|
|
151
|
-
ctx,
|
|
152
|
-
buildChildResourceLoaderOptions(parsedForwardedArgs, ctx.cwd),
|
|
153
|
-
);
|
|
150
|
+
const services = await createChildServices(ctx, parsed.forwardedArgs);
|
|
154
151
|
runningAgents.add(runningAgent);
|
|
155
152
|
logSteering(runningAgent.id, "agent-created", { command, taskLength: parsed.task.length });
|
|
156
153
|
if (ctx.hasUI) {
|
|
@@ -204,6 +201,7 @@ async function dispatchToHerdrPane(
|
|
|
204
201
|
// Explicit on the CLI too: an isolated child's file has no messages, and pi then takes the
|
|
205
202
|
// flags over the file.
|
|
206
203
|
const paneId = await openPiInHerdrPane(ctx.cwd, herdrAgentName(runningAgent.sessionId), [
|
|
204
|
+
...parentExtensionArgs(),
|
|
207
205
|
...forwardedArgsForPane(parsed.forwardedArgs),
|
|
208
206
|
"--provider",
|
|
209
207
|
model.provider,
|
|
@@ -367,24 +365,46 @@ type ForwardedOptions = ChildToolOptions & {
|
|
|
367
365
|
thinkingLevel?: ThinkingLevel;
|
|
368
366
|
};
|
|
369
367
|
|
|
370
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* The cwd-bound services for one child session; the parent's ModelRuntime rides along (see INTERNALS).
|
|
370
|
+
* Resources come from the parent's launch extension options, then the dispatch's forwarded pi options.
|
|
371
|
+
*/
|
|
371
372
|
export function createChildServices(
|
|
372
373
|
ctx: ExtensionCommandContext,
|
|
373
|
-
|
|
374
|
+
forwardedArgs: readonly string[],
|
|
374
375
|
): Promise<AgentSessionServices> {
|
|
375
376
|
const agentDir = getAgentDir();
|
|
376
377
|
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
377
378
|
projectTrusted: ctx.isProjectTrusted(),
|
|
378
379
|
});
|
|
380
|
+
const resourceArgs = parseForwardedArgs([...parentExtensionArgs(), ...forwardedArgs]);
|
|
379
381
|
return createAgentSessionServices({
|
|
380
382
|
cwd: ctx.cwd,
|
|
381
383
|
agentDir,
|
|
382
384
|
settingsManager,
|
|
383
385
|
modelRuntime: parentModelRuntime(ctx),
|
|
384
|
-
resourceLoaderOptions,
|
|
386
|
+
resourceLoaderOptions: buildChildResourceLoaderOptions(resourceArgs, ctx.cwd),
|
|
385
387
|
});
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
/**
|
|
391
|
+
* The extension options this Pi process was launched with, as pi CLI tokens. Pi keeps them out of
|
|
392
|
+
* settings, so a child that loads extensions from settings alone misses the parent's `-e` extensions.
|
|
393
|
+
*
|
|
394
|
+
* @example parentExtensionArgs(["--mode", "rpc", "-ne", "-e", "./a.ts", "-e", "npm:b", "hi"], "/work") // ["--no-extensions", "--extension", "/work/a.ts", "--extension", "npm:b"]
|
|
395
|
+
*/
|
|
396
|
+
export function parentExtensionArgs(
|
|
397
|
+
launchArgs: readonly string[] = process.argv.slice(2),
|
|
398
|
+
launchCwd: string = process.cwd(),
|
|
399
|
+
): string[] {
|
|
400
|
+
const launch = parseArgs([...launchArgs]);
|
|
401
|
+
const extensions = resolveResourcePaths(launch.extensions ?? [], launchCwd).flatMap((source) => [
|
|
402
|
+
"--extension",
|
|
403
|
+
source,
|
|
404
|
+
]);
|
|
405
|
+
return launch.noExtensions ? ["--no-extensions", ...extensions] : extensions;
|
|
406
|
+
}
|
|
407
|
+
|
|
388
408
|
/**
|
|
389
409
|
* The parent's ModelRuntime: the extension facade wraps it as a plain property. Sharing it keeps
|
|
390
410
|
* extension-registered providers (their process-global dedup guard) available to the child.
|
|
@@ -532,6 +552,8 @@ export function buildChildResourceLoaderOptions(
|
|
|
532
552
|
/** Resolve resource paths to absolute against the session cwd, matching how the CLI hands them to the loader. */
|
|
533
553
|
function resolveResourcePaths(values: string[], cwd: string): string[] {
|
|
534
554
|
return values.map((value) => {
|
|
555
|
+
// Package sources such as npm:<pkg> are not paths; pi resolves them itself.
|
|
556
|
+
if (/^(npm|git|github|https?|ssh|builtin):/.test(value)) return value;
|
|
535
557
|
if (value === "~" || value.startsWith("~/")) return path.join(os.homedir(), value.slice(1));
|
|
536
558
|
return path.isAbsolute(value) ? value : path.resolve(cwd, value);
|
|
537
559
|
});
|