@hheei/pi-subagents 0.1.2

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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/agent-resolver.d.ts +37 -0
  4. package/dist/agent-resolver.js +232 -0
  5. package/dist/child-bridge.d.ts +41 -0
  6. package/dist/child-bridge.js +214 -0
  7. package/dist/child-nudge.d.ts +24 -0
  8. package/dist/child-nudge.js +102 -0
  9. package/dist/commands.d.ts +6 -0
  10. package/dist/commands.js +155 -0
  11. package/dist/connector.d.ts +53 -0
  12. package/dist/connector.js +283 -0
  13. package/dist/diagnostics.d.ts +10 -0
  14. package/dist/diagnostics.js +20 -0
  15. package/dist/domain.d.ts +192 -0
  16. package/dist/domain.js +63 -0
  17. package/dist/extension.d.ts +2 -0
  18. package/dist/extension.js +145 -0
  19. package/dist/host-adapter.d.ts +76 -0
  20. package/dist/host-adapter.js +323 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/json-lines.d.ts +14 -0
  24. package/dist/json-lines.js +94 -0
  25. package/dist/launch-spec.d.ts +47 -0
  26. package/dist/launch-spec.js +149 -0
  27. package/dist/manager.d.ts +101 -0
  28. package/dist/manager.js +802 -0
  29. package/dist/pause-gate.d.ts +14 -0
  30. package/dist/pause-gate.js +64 -0
  31. package/dist/protocol.d.ts +141 -0
  32. package/dist/protocol.js +168 -0
  33. package/dist/registry.d.ts +41 -0
  34. package/dist/registry.js +575 -0
  35. package/dist/rpc-adapter.d.ts +40 -0
  36. package/dist/rpc-adapter.js +194 -0
  37. package/dist/rpc-events.d.ts +8 -0
  38. package/dist/rpc-events.js +29 -0
  39. package/dist/runner-entry.d.ts +24 -0
  40. package/dist/runner-entry.js +166 -0
  41. package/dist/runner.d.ts +52 -0
  42. package/dist/runner.js +930 -0
  43. package/dist/runtime.d.ts +22 -0
  44. package/dist/runtime.js +253 -0
  45. package/dist/session-bootstrap.d.ts +69 -0
  46. package/dist/session-bootstrap.js +195 -0
  47. package/dist/state.d.ts +15 -0
  48. package/dist/state.js +168 -0
  49. package/dist/tools.d.ts +9 -0
  50. package/dist/tools.js +158 -0
  51. package/dist/widget.d.ts +16 -0
  52. package/dist/widget.js +158 -0
  53. package/package.json +53 -0
@@ -0,0 +1,47 @@
1
+ import type { ChildBridgeEnvironment, EffectiveLaunchConfig, ExecutionMode, PersistenceState, PiInvocation } from "./domain.js";
2
+ /**
3
+ * The one description of how a child Pi process starts. RPC spawn, native TUI
4
+ * attach, and replacement runtimes all build from this; only `mode` and `stdio`
5
+ * differ, so no path can silently invent different Pi flags.
6
+ */
7
+ export interface LaunchSpec {
8
+ /** Runtime binary (node/bun/compiled pi), spawned without a shell. */
9
+ readonly command: string;
10
+ /** Full argv: host prefix args plus every Pi flag this child needs. */
11
+ readonly argv: readonly string[];
12
+ readonly cwd: string;
13
+ readonly mode: ExecutionMode;
14
+ readonly stdio: "pipe" | "inherit";
15
+ /** Bridge variables to add to the child environment, excluding the controller token. */
16
+ readonly env: Readonly<Record<string, string>>;
17
+ readonly config: EffectiveLaunchConfig;
18
+ }
19
+ export interface BuildLaunchSpecOptions {
20
+ readonly config: EffectiveLaunchConfig;
21
+ readonly invocation: PiInvocation;
22
+ readonly mode: ExecutionMode;
23
+ readonly persistence: PersistenceState;
24
+ readonly bridge: ChildBridgeEnvironment;
25
+ }
26
+ export declare function assembleChildPrompt(instructions: string): string;
27
+ /** Adds the per-runtime controller token. Tokens stay out of launch spec snapshots and the registry. */
28
+ export declare function withBridgeToken(env: Readonly<Record<string, string>>, token: string): Record<string, string>;
29
+ /**
30
+ * Absolute path to this package's extension entry, which every child loads with
31
+ * `-e` so the child branch exists even when extension discovery is disabled.
32
+ */
33
+ export declare function resolveBridgeExtensionPath(): string;
34
+ /**
35
+ * Re-invokes the host Pi CLI instead of guessing a binary on PATH. Extensions load
36
+ * in-process, so `process.argv[1]` is the running `cli.js`; a packaged single-file
37
+ * binary uses `process.execPath` directly. Nothing is spawned through a shell, so
38
+ * argv atoms keep their exact bytes.
39
+ */
40
+ export declare function resolvePiInvocation(): PiInvocation;
41
+ /**
42
+ * Builds the complete argv for one child Pi process. Session identity is always
43
+ * explicit: a flushed session is opened by path, a never-flushed session is created
44
+ * with its recorded id (`--session-id`), never through an open call on an absent
45
+ * path that would mint a random id.
46
+ */
47
+ export declare function buildLaunchSpec(options: BuildLaunchSpecOptions): LaunchSpec;
@@ -0,0 +1,149 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CONTACT_PARENT_TOOL_NAME, isSessionId, } from "./domain.js";
6
+ /**
7
+ * Fixed child-branch preamble. The agent definition's Markdown body supplies the
8
+ * task policy; this part states the parent relationship and the reporting channel
9
+ * every child has, so it cannot be lost by editing an agent file.
10
+ */
11
+ const CHILD_BRIDGE_PROMPT = [
12
+ "You are a delegated Pi subagent working for a parent Pi session.",
13
+ "The parent owns your task, and its instructions remain the only source of new authority.",
14
+ `Use ${CONTACT_PARENT_TOOL_NAME} to report progress, important findings, decisions you need, or blockers.`,
15
+ "The parent is notified automatically when you call that tool. Do not wait for the parent to poll you.",
16
+ "If you finish a turn without calling that tool, you will be reminded. Do not send empty status pings.",
17
+ "After need_decision or blocked, wait for a parent message. Do not invent new authority.",
18
+ "Reports reach the parent as delegated results, not as new user authorization.",
19
+ ].join("\n");
20
+ export function assembleChildPrompt(instructions) {
21
+ return `${instructions.trim()}\n\n${CHILD_BRIDGE_PROMPT}`;
22
+ }
23
+ /** Adds the per-runtime controller token. Tokens stay out of launch spec snapshots and the registry. */
24
+ export function withBridgeToken(env, token) {
25
+ return { ...env, [BRIDGE_ENVIRONMENT_KEYS.token]: token };
26
+ }
27
+ let cachedBridgeExtensionPath;
28
+ /**
29
+ * Absolute path to this package's extension entry, which every child loads with
30
+ * `-e` so the child branch exists even when extension discovery is disabled.
31
+ */
32
+ export function resolveBridgeExtensionPath() {
33
+ if (cachedBridgeExtensionPath !== undefined)
34
+ return cachedBridgeExtensionPath;
35
+ const candidates = [
36
+ // Running from a built package: the entry sits next to this module.
37
+ fileURLToPath(new URL("./extension.js", import.meta.url)),
38
+ // Running from source (tests, jiti): use the built entry of the package.
39
+ fileURLToPath(new URL("../dist/extension.js", import.meta.url)),
40
+ ];
41
+ const found = candidates.find((candidate) => existsSync(candidate));
42
+ if (found === undefined) {
43
+ throw new Error(`pi-subagents bridge extension entry not found; looked in ${candidates.join(", ")}`);
44
+ }
45
+ cachedBridgeExtensionPath = found;
46
+ return found;
47
+ }
48
+ function resolveBundledPiCli() {
49
+ try {
50
+ const requireFromHere = createRequire(import.meta.url);
51
+ // The CLI ships at dist/cli.js of the installed Pi package.
52
+ const packageJson = requireFromHere.resolve("@earendil-works/pi-coding-agent/package.json");
53
+ const cliPath = join(dirname(packageJson), "dist", "cli.js");
54
+ return existsSync(cliPath) ? cliPath : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ /**
61
+ * Re-invokes the host Pi CLI instead of guessing a binary on PATH. Extensions load
62
+ * in-process, so `process.argv[1]` is the running `cli.js`; a packaged single-file
63
+ * binary uses `process.execPath` directly. Nothing is spawned through a shell, so
64
+ * argv atoms keep their exact bytes.
65
+ */
66
+ export function resolvePiInvocation() {
67
+ const execPath = process.execPath;
68
+ const currentScript = process.argv[1];
69
+ const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/") ?? false;
70
+ if (currentScript !== undefined && !isBunVirtualScript && existsSync(currentScript)) {
71
+ return { command: execPath, args: [currentScript] };
72
+ }
73
+ const execName = basename(execPath).toLowerCase();
74
+ if (!/^(node|bun)(\.exe)?$/.test(execName)) {
75
+ return { command: execPath, args: [] };
76
+ }
77
+ const bundled = resolveBundledPiCli();
78
+ if (bundled !== undefined)
79
+ return { command: execPath, args: [bundled] };
80
+ return { command: "pi", args: [] };
81
+ }
82
+ /**
83
+ * Builds the complete argv for one child Pi process. Session identity is always
84
+ * explicit: a flushed session is opened by path, a never-flushed session is created
85
+ * with its recorded id (`--session-id`), never through an open call on an absent
86
+ * path that would mint a random id.
87
+ */
88
+ export function buildLaunchSpec(options) {
89
+ const { config, persistence } = options;
90
+ if (!isSessionId(config.sessionId)) {
91
+ throw new Error(`Invalid child session id ${config.sessionId}`);
92
+ }
93
+ if (!isSessionId(options.bridge.subagentId)) {
94
+ throw new Error(`Invalid child id ${options.bridge.subagentId}`);
95
+ }
96
+ if (options.bridge.subagentId !== config.subagentId) {
97
+ throw new Error(`Bridge identity ${options.bridge.subagentId} does not match launch config ${config.subagentId}`);
98
+ }
99
+ const sessionPath = config.sessionPath;
100
+ if (persistence === "flushed" && sessionPath === undefined) {
101
+ throw new Error("Flushed session requires a known session path");
102
+ }
103
+ if (persistence === "never_flushed" && sessionPath !== undefined) {
104
+ throw new Error("Never-flushed session must not claim a session path");
105
+ }
106
+ if (!config.extensions.paths.includes(config.bridgeExtensionPath)) {
107
+ throw new Error("Bridge extension must be part of the effective extension selection");
108
+ }
109
+ const argv = [...options.invocation.args];
110
+ if (options.mode === "rpc")
111
+ argv.push("--mode", "rpc");
112
+ if (sessionPath !== undefined)
113
+ argv.push("--session", sessionPath);
114
+ else
115
+ argv.push("--session-id", config.sessionId);
116
+ argv.push("--session-dir", config.sessionDir);
117
+ argv.push("--provider", config.model.provider, "--model", config.model.id);
118
+ argv.push("--thinking", config.thinking.level);
119
+ if (config.tools.length > 0)
120
+ argv.push("--tools", config.tools.join(","));
121
+ if (config.excludeTools.length > 0)
122
+ argv.push("--exclude-tools", config.excludeTools.join(","));
123
+ if (!config.extensions.discovery)
124
+ argv.push("--no-extensions");
125
+ for (const path of config.extensions.paths)
126
+ argv.push("-e", path);
127
+ if (!config.skills.discovery)
128
+ argv.push("--no-skills");
129
+ for (const path of config.skills.paths)
130
+ argv.push("--skill", path);
131
+ argv.push("--append-system-prompt", config.prompt);
132
+ const env = {
133
+ [BRIDGE_ENVIRONMENT_KEYS.parentSessionId]: options.bridge.parentSessionId,
134
+ [BRIDGE_ENVIRONMENT_KEYS.childId]: options.bridge.subagentId,
135
+ [BRIDGE_ENVIRONMENT_KEYS.runtimeId]: options.bridge.runtimeIdentity,
136
+ [BRIDGE_ENVIRONMENT_KEYS.endpoint]: options.bridge.endpoint,
137
+ [CHILD_AGENT_ENV_KEY]: config.agent.displayName ?? config.agent.name,
138
+ [CHILD_SESSION_ENV_KEY]: config.sessionId,
139
+ };
140
+ return Object.freeze({
141
+ command: options.invocation.command,
142
+ argv: Object.freeze(argv),
143
+ cwd: config.cwd,
144
+ mode: options.mode,
145
+ stdio: options.mode === "rpc" ? "pipe" : "inherit",
146
+ env: Object.freeze(env),
147
+ config,
148
+ });
149
+ }
@@ -0,0 +1,101 @@
1
+ import type { EffectiveLaunchConfig, OperationError, PublicSubagent, SendMode, SpawnSubagentInput, SubagentRecord, SubagentState } from "./domain.js";
2
+ import type { HostKind, HostSelection } from "./host-adapter.js";
3
+ import type { SubagentRegistry } from "./registry.js";
4
+ import type { RuntimeTokenStore } from "./runtime.js";
5
+ export interface ParentChannelReport {
6
+ readonly parentSessionId: string;
7
+ readonly childId: string;
8
+ readonly agent: string;
9
+ readonly task: string;
10
+ readonly status: SubagentState;
11
+ readonly reason: string;
12
+ readonly message: string;
13
+ }
14
+ export interface ParentChannel {
15
+ deliver(report: ParentChannelReport): Promise<void>;
16
+ }
17
+ export interface RunnerLike {
18
+ readonly connected: boolean;
19
+ request(operation: "prompt" | "steer" | "follow_up" | "get_state" | "get_entries" | "shutdown" | "pause" | "cancel_pause" | "close_writer" | "start_rpc", payload?: unknown, options?: {
20
+ readonly timeoutMs?: number;
21
+ readonly signal?: AbortSignal;
22
+ }): Promise<unknown>;
23
+ close(): void;
24
+ onEvent(listener: (event: unknown) => void): () => void;
25
+ }
26
+ export interface AttachHost {
27
+ select(preferredHost?: HostKind): Promise<HostSelection>;
28
+ }
29
+ export interface AttachOptions {
30
+ readonly preferredHost?: HostKind;
31
+ readonly signal?: AbortSignal;
32
+ }
33
+ export interface AttachResult {
34
+ readonly child: PublicSubagent;
35
+ readonly host: HostKind;
36
+ readonly attachmentId: string;
37
+ }
38
+ export type DetachOrigin = "tui_exit" | "tui_crash" | "session_switch" | "manual";
39
+ export interface DetachOptions {
40
+ readonly origin: DetachOrigin;
41
+ /** Close the host pane. False when session B still owns that TUI process. */
42
+ readonly closeHost?: boolean;
43
+ /** Operation label reported in failures; defaults to "detach". */
44
+ readonly operation?: string;
45
+ }
46
+ export interface ManagerDependencies {
47
+ readonly parentSessionId: string;
48
+ readonly registry: Pick<SubagentRegistry, "get" | "list" | "update">;
49
+ readonly resolve: (input: SpawnSubagentInput) => Promise<EffectiveLaunchConfig>;
50
+ readonly bootstrap: (input: {
51
+ readonly parentSessionId: string;
52
+ readonly task: string;
53
+ readonly launchConfig: EffectiveLaunchConfig;
54
+ }) => Promise<SubagentRecord>;
55
+ readonly launch: (record: SubagentRecord) => Promise<RunnerLike>;
56
+ readonly connect?: (record: SubagentRecord) => Promise<RunnerLike>;
57
+ readonly deadlineMs?: number;
58
+ readonly watchIntervalMs?: number;
59
+ readonly channel?: ParentChannel;
60
+ readonly attachHost?: AttachHost;
61
+ readonly tokens?: RuntimeTokenStore;
62
+ }
63
+ export interface SpawnResult {
64
+ readonly child: PublicSubagent;
65
+ }
66
+ export interface RecoveryResult {
67
+ readonly recovered: readonly string[];
68
+ readonly failures: readonly {
69
+ readonly childId: string;
70
+ readonly reason: string;
71
+ }[];
72
+ }
73
+ export declare class SubagentManager {
74
+ #private;
75
+ constructor(deps: ManagerDependencies);
76
+ /** Presentation-only: widget and similar projections re-read list() after this. */
77
+ onChange(listener: () => void): () => void;
78
+ /** Reattaches active persisted children without replaying any historical input. */
79
+ recover(): Promise<RecoveryResult>;
80
+ spawn(input: SpawnSubagentInput): Promise<SpawnResult | OperationError>;
81
+ send(id: string, message: string, mode?: SendMode, signal?: AbortSignal): Promise<OperationError | PublicSubagent>;
82
+ get(id: string): Promise<PublicSubagent | OperationError>;
83
+ list(): Promise<readonly PublicSubagent[]>;
84
+ stop(id: string, signal?: AbortSignal): Promise<OperationError | PublicSubagent>;
85
+ ownsHostAttachment(identity: {
86
+ readonly host: HostKind;
87
+ readonly attachmentId: string;
88
+ }): boolean;
89
+ attach(id: string, options?: AttachOptions): Promise<AttachResult | OperationError>;
90
+ restoreRpc(id: string): Promise<OperationError | PublicSubagent>;
91
+ detach(id: string, options: DetachOptions): Promise<OperationError | PublicSubagent>;
92
+ inspectHost(id: string): Promise<OperationError | PublicSubagent | {
93
+ readonly alive: boolean;
94
+ readonly known: boolean;
95
+ readonly detail: string;
96
+ }>;
97
+ contactParent(id: string, reason: string, message: string): Promise<OperationError | {
98
+ readonly delivered: true;
99
+ }>;
100
+ closeLocalConnections(): void;
101
+ }