shariq-pi-extensions 0.2.8 → 0.2.9

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.
@@ -38,7 +38,7 @@ The goal extension adds persistent, branch-safe objectives, progress evidence, b
38
38
 
39
39
  The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
40
40
 
41
- The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. Child settlement is handed to Pi immediately as an extension-originated user follow-up, so Pi queues it while the parent is active or starts a new parent turn when idle with the summary guaranteed in model context; status tools are for explicit inspection, not waiting.
41
+ The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input; status tools are for explicit inspection, not waiting.
42
42
 
43
43
  ### [Orchestration](../extensions/orchestration/README.md)
44
44
 
@@ -50,7 +50,7 @@ The model-facing `create_orchestration` tool starts planning only after an expli
50
50
 
51
51
  Managed PTYs support servers, watchers, long builds, downloads, and interactive processes. The extension tracks up to eight concurrent terminals, retains bounded output, stores full logs in restrictive temporary directories, and stops process groups during shutdown or reload.
52
52
 
53
- Its tools are `start_terminal`, `read_terminal`, `write_terminal`, `list_terminals`, and `stop_terminal`. A model-started terminal immediately hands its completion or failure to Pi as an extension-originated user follow-up, which Pi queues while the parent is active or uses to start a new parent turn when idle with the bounded output guaranteed in model context. Reading a terminal does not suppress that delivery; agents should inspect only for explicit progress requests or immediate interaction.
53
+ Its tools are `start_terminal`, `read_terminal`, `write_terminal`, `list_terminals`, and `stop_terminal`. A model-started terminal keeps completion or failure in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with bounded output guaranteed in model context and never rendered as user-authored or follow-up input. Reading a terminal does not suppress that delivery; agents should inspect only for explicit progress requests or immediate interaction.
54
54
 
55
55
  ## Web access
56
56
 
@@ -10,7 +10,7 @@ Session-scoped background pseudo-terminals for Pi. The extension combines Codex-
10
10
  - `list_terminals` — list running and settled terminals.
11
11
  - `stop_terminal` — stop complete process groups with TERM-to-KILL escalation.
12
12
 
13
- Each output response carries a byte cursor. Pass it to the next read/write operation to avoid repeating output. Long or uncertain commands should use `start_terminal` instead of a large blocking `bash` timeout. Settlement is handed to Pi immediately as an extension-originated user follow-up: it queues while the parent is active or starts a new parent turn when idle with bounded output guaranteed in model context, so the parent can continue other work or end its turn rather than poll.
13
+ Each output response carries a byte cursor. Pass it to the next read/write operation to avoid repeating output. Long or uncertain commands should use `start_terminal` instead of a large blocking `bash` timeout. Settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with bounded output guaranteed in model context and never rendered as user-authored or follow-up input, so the parent can continue other work or end its turn rather than poll.
14
14
 
15
15
  ## User interface
16
16
 
@@ -8,7 +8,7 @@ import type {
8
8
  import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
9
9
  import { Markdown, Text } from "@earendil-works/pi-tui";
10
10
  import { Type } from "typebox";
11
- import { deliverSettlement } from "../shared/settlement-delivery.ts";
11
+ import { settlementDelivery } from "../shared/settlement-delivery.ts";
12
12
  import { oneLine, sanitizeTerminalText, stateLabel } from "../shared/tui-dashboard.ts";
13
13
  import { TerminalManager, MAX_RUNNING_TERMINALS } from "./src/manager.ts";
14
14
  import {
@@ -36,6 +36,7 @@ function resolveCwd(base: string, requested?: string): string {
36
36
  }
37
37
 
38
38
  export default function backgroundTerminals(pi: ExtensionAPI) {
39
+ const deliverSettlement = settlementDelivery(pi);
39
40
  let manager: TerminalManager | undefined;
40
41
  let ui: ExtensionUIContext | undefined;
41
42
  let unsubscribe: (() => void) | undefined;
@@ -89,7 +90,7 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
89
90
  const snapshot = pendingResults.get(id);
90
91
  if (!snapshot) return;
91
92
  pendingResults.delete(id);
92
- deliverSettlement(pi, {
93
+ deliverSettlement({
93
94
  customType: "background-terminal-result",
94
95
  content: formatCompletion(snapshot),
95
96
  display: true,
@@ -131,7 +132,7 @@ export default function backgroundTerminals(pi: ExtensionAPI) {
131
132
  promptSnippet: "Start an interactive or long-running command in a managed background PTY.",
132
133
  promptGuidelines: [
133
134
  "Use start_terminal by default for servers, watchers, downloads, long or uncertain builds and tests, interactive shells, and any command that should not occupy the main turn; reserve bash for short commands whose result is needed immediately. Never use a large bash timeout merely to wait for long work.",
134
- "After start_terminal returns, continue only genuinely independent work. If none remains, end the turn immediately. Terminal settlement is handed to Pi immediately: it queues a follow-up while the parent is active or starts the next parent turn when idle. When that result invokes the parent, continue the original task immediately without waiting for the user or rereading the same terminal; do not call read_terminal, list_terminals, or start a timer merely to check whether it finished.",
135
+ "After start_terminal returns, continue only genuinely independent work. If none remains, end the turn immediately. Terminal settlement stays in a private extension queue while the parent is active and starts one custom-result turn at Pi's safe idle edge. When that result invokes the parent, continue the original task immediately without waiting for the user or rereading the same terminal; do not call read_terminal, list_terminals, or start a timer merely to check whether it finished.",
135
136
  "Use stop_terminal when a managed process is no longer needed. Background terminals are session-scoped and are stopped during session shutdown or reload.",
136
137
  ],
137
138
  parameters: Type.Object({
@@ -3,7 +3,7 @@
3
3
  Controls what ordinary **Enter** does when the interactive Pi agent is already running:
4
4
 
5
5
  - `steer` (default) queues the message before the agent's next model step.
6
- - `interrupt` signals Pi's active abort controller immediately, then preserves the submitted text, images, prompt-template expansion, and normal delivery as the next input.
6
+ - `interrupt` signals Pi's active abort controller immediately, retains the submitted text and images, then starts a fresh turn at Pi's safe `agent_settled` boundary. Multiple inputs received during cancellation are preserved in order in that replacement turn.
7
7
  - `follow-up` queues the message until the active run finishes.
8
8
 
9
9
  Use `/input-mode` for the picker or `/input-mode steer|interrupt|follow-up` for direct selection. The global choice is stored with restrictive permissions in `<agent-dir>/input-mode.json`; non-default modes appear in Pi's status area.
@@ -14,20 +14,30 @@ import {
14
14
  } from "./config.ts";
15
15
 
16
16
  const STATUS_KEY = "input-mode";
17
+ type ModelInput = Parameters<ExtensionAPI["sendUserMessage"]>[0];
17
18
 
18
19
  interface InputModeExtensionOptions {
19
20
  configFile?: string;
20
21
  }
21
22
 
22
- function modelInput(event: InputEvent) {
23
+ function modelInput(event: InputEvent): ModelInput {
23
24
  if (!event.images?.length) return event.text;
24
25
  return [{ type: "text" as const, text: event.text }, ...event.images];
25
26
  }
26
27
 
28
+ function mergeInputs(inputs: ModelInput[]): ModelInput {
29
+ if (inputs.length === 1) return inputs[0]!;
30
+ return inputs.flatMap((input, index) => [
31
+ ...(index === 0 ? [] : [{ type: "text" as const, text: "\n\n---\n\n" }]),
32
+ ...(typeof input === "string" ? [{ type: "text" as const, text: input }] : input),
33
+ ]);
34
+ }
35
+
27
36
  export function createInputModeExtension(options: InputModeExtensionOptions = {}) {
28
37
  return (pi: ExtensionAPI) => {
29
38
  let mode = loadInputMode(options.configFile);
30
39
  let ui: ExtensionUIContext | undefined;
40
+ const pendingInterrupts: ModelInput[] = [];
31
41
 
32
42
  const updateStatus = () => {
33
43
  if (!ui) return;
@@ -74,7 +84,14 @@ export function createInputModeExtension(options: InputModeExtensionOptions = {}
74
84
  updateStatus();
75
85
  });
76
86
 
87
+ pi.on("agent_settled", () => {
88
+ if (pendingInterrupts.length === 0) return;
89
+ const input = mergeInputs(pendingInterrupts.splice(0));
90
+ pi.sendUserMessage(input, { expandPromptTemplates: true });
91
+ });
92
+
77
93
  pi.on("session_shutdown", () => {
94
+ pendingInterrupts.length = 0;
78
95
  ui?.setStatus(STATUS_KEY, undefined);
79
96
  ui = undefined;
80
97
  });
@@ -91,12 +108,12 @@ export function createInputModeExtension(options: InputModeExtensionOptions = {}
91
108
  return { action: "handled" };
92
109
  }
93
110
 
94
- // Abort is signalled synchronously. Returning continue preserves Pi's
95
- // normal template expansion, image handling, history, and queue logic;
96
- // the prompt becomes a fresh turn if abort settlement wins the race, or
97
- // a steering message consumed immediately after the aborted run.
111
+ // Store before signalling abort so even a very fast settlement cannot
112
+ // race past the replacement input. agent_settled is Pi's safe idle edge;
113
+ // it starts one fresh turn after model/tool cancellation completes.
114
+ pendingInterrupts.push(modelInput(event));
98
115
  ctx.abort();
99
- return { action: "continue" };
116
+ return { action: "handled" };
100
117
  });
101
118
  };
102
119
  }
@@ -8,7 +8,7 @@ Internal runtime utilities used by more than one extension. This directory is no
8
8
  - `child-session.ts` owns trust-aware child resources and bounded session shutdown.
9
9
  - `context-utilization.ts` formats model-context usage and capacity.
10
10
  - `dashboard-state.ts` keeps list selection stable as live rows change.
11
- - `settlement-delivery.ts` immediately hands asynchronous results to Pi as extension-originated user follow-ups, guaranteeing model-visible settlement context.
11
+ - `settlement-delivery.ts` coordinates asynchronous output in a private package-wide queue and starts one custom-result turn at Pi's safe idle edge, guaranteeing model-visible context without user-authored or follow-up rendering.
12
12
  - `tool-call-timeout.ts` applies cancellation-aware execution limits to registered tools.
13
13
  - `tui-dashboard.ts` provides bounded, sanitized terminal-dashboard rendering helpers.
14
14
 
@@ -1,19 +1,82 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  type SettlementMessage = Parameters<ExtensionAPI["sendMessage"]>[0];
4
+ type DeliverSettlement = (message: SettlementMessage) => void;
5
+
6
+ type SessionKey = object;
7
+
8
+ interface Coordinator {
9
+ parentActive: boolean;
10
+ shuttingDown: boolean;
11
+ pending: SettlementMessage[];
12
+ flush(): void;
13
+ }
14
+
15
+ const coordinators = new WeakMap<SessionKey, Coordinator>();
16
+
17
+ function createCoordinator(pi: ExtensionAPI): Coordinator {
18
+ const coordinator: Coordinator = {
19
+ parentActive: false,
20
+ shuttingDown: false,
21
+ pending: [],
22
+ flush() {
23
+ if (coordinator.parentActive || coordinator.shuttingDown || coordinator.pending.length === 0) return;
24
+ const batch = coordinator.pending.splice(0);
25
+ for (const message of batch.slice(0, -1)) {
26
+ pi.sendMessage(message, { triggerTurn: false });
27
+ }
28
+ pi.sendMessage(batch.at(-1)!, { triggerTurn: true });
29
+ },
30
+ };
31
+ return coordinator;
32
+ }
4
33
 
5
34
  /**
6
- * Hand settlement to Pi as an extension-originated user follow-up. A custom
7
- * message can wake the parent yet fail to appear in the invoked model turn;
8
- * sendUserMessage guarantees that the bounded result is model-visible. Pi
9
- * starts a turn when idle and queues the same input while the parent is active.
35
+ * Coordinate asynchronous results across every suite extension in one Pi
36
+ * session. Results that settle during a parent run stay in a private local
37
+ * queue until agent_settled; Pi never renders them as queued user/follow-up
38
+ * input. At the safe idle edge, earlier results are appended and the final
39
+ * custom result starts exactly one model turn with the whole batch in context.
10
40
  */
11
- export function deliverSettlement(pi: ExtensionAPI, message: SettlementMessage): void {
12
- const content = typeof message.content === "string"
13
- ? message.content
14
- : message.content
15
- .filter((part) => part.type === "text")
16
- .map((part) => part.text)
17
- .join("\n");
18
- pi.sendUserMessage(content, { deliverAs: "followUp" });
41
+ export function settlementDelivery(pi: ExtensionAPI): DeliverSettlement {
42
+ let coordinator: Coordinator | undefined;
43
+ let sessionKey: SessionKey | undefined;
44
+
45
+ pi.on("session_start", (_event, ctx) => {
46
+ sessionKey = (ctx.sessionManager ?? ctx) as SessionKey;
47
+ coordinator = coordinators.get(sessionKey);
48
+ if (!coordinator) {
49
+ coordinator = createCoordinator(pi);
50
+ coordinators.set(sessionKey, coordinator);
51
+ }
52
+ coordinator.parentActive = false;
53
+ coordinator.shuttingDown = false;
54
+ });
55
+ pi.on("agent_start", () => {
56
+ if (coordinator) coordinator.parentActive = true;
57
+ });
58
+ pi.on("agent_settled", () => {
59
+ if (!coordinator) return;
60
+ coordinator.parentActive = false;
61
+ coordinator.flush();
62
+ });
63
+ pi.on("session_shutdown", () => {
64
+ if (coordinator) {
65
+ coordinator.shuttingDown = true;
66
+ coordinator.parentActive = false;
67
+ coordinator.pending.length = 0;
68
+ }
69
+ if (sessionKey) coordinators.delete(sessionKey);
70
+ coordinator = undefined;
71
+ sessionKey = undefined;
72
+ });
73
+
74
+ return (message) => {
75
+ // session_start precedes model-owned terminal/subagent work. Fail closed if
76
+ // a malformed host calls delivery before initialization instead of showing
77
+ // asynchronous output as user-authored input.
78
+ if (!coordinator || coordinator.shuttingDown) return;
79
+ coordinator.pending.push(message);
80
+ coordinator.flush();
81
+ };
19
82
  }
@@ -19,7 +19,7 @@ The system is deliberately flat. Only the main Pi thread can spawn subagents. Ch
19
19
  - `reply_question` — answer a child’s blocking `ask_parent` request
20
20
  - `task` — atomically reserve capacity for up to the configured limit (maximum 50), start the fan-out in the background, and return child ids immediately
21
21
 
22
- Child sessions receive `message_parent`, `ask_parent`, `list_peers`, and `message_peer`. Peer messages are routed through the main-thread manager and can steer a running child or continue a settled one; they cannot create agents. Child settlement is handed to Pi immediately as an extension-originated user follow-up: it queues while the parent is active or starts a new parent turn when idle with the summary guaranteed in model context, so the main turn can continue independent work or end and remain available to the user.
22
+ Child sessions receive `message_parent`, `ask_parent`, `list_peers`, and `message_peer`. Peer messages are routed through the main-thread manager and can steer a running child or continue a settled one; they cannot create agents. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input, so the main turn can continue independent work or end and remain available to the user.
23
23
 
24
24
  ## Profiles and capabilities
25
25
 
@@ -26,7 +26,7 @@ import {
26
26
  } from "@earendil-works/pi-coding-agent";
27
27
  import { Markdown, Text } from "@earendil-works/pi-tui";
28
28
  import { Type } from "typebox";
29
- import { deliverSettlement } from "../shared/settlement-delivery.ts";
29
+ import { settlementDelivery } from "../shared/settlement-delivery.ts";
30
30
  import {
31
31
  formatElapsed,
32
32
  latestText,
@@ -195,6 +195,7 @@ function resolveChildProjectTrust(options: {
195
195
  }
196
196
 
197
197
  export default function (pi: ExtensionAPI) {
198
+ const deliverSettlement = settlementDelivery(pi);
198
199
  let runtime: SubagentRuntime | undefined;
199
200
  let managerPromise: Promise<SubagentManagerShape> | undefined;
200
201
  let ui: ExtensionUIContext | undefined;
@@ -522,7 +523,7 @@ export default function (pi: ExtensionAPI) {
522
523
  };
523
524
 
524
525
  const deliverResult = (snap: SubagentSnapshot) => {
525
- deliverSettlement(pi, {
526
+ deliverSettlement({
526
527
  customType: "subagent-result",
527
528
  content: buildSubagentResultMessage({
528
529
  id: snap.id,
@@ -668,7 +669,7 @@ export default function (pi: ExtensionAPI) {
668
669
  promptSnippet: SUBAGENT_SPAWN_PROMPT_SNIPPET,
669
670
  promptGuidelines: [
670
671
  ...SUBAGENT_SPAWN_PROMPT_GUIDELINES,
671
- "After spawn_agent starts a child, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch it run. Settlement is handed to Pi immediately; when its attached summary invokes the parent, continue the original task without waiting for another user message.",
672
+ "After spawn_agent starts a child, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch it run. Settlement stays private until it starts a custom-result turn at Pi's safe idle edge; when its attached summary invokes the parent, continue the original task without waiting for another user message.",
672
673
  ],
673
674
  parameters: Type.Object({
674
675
  message: Type.String({
@@ -1096,7 +1097,7 @@ export default function (pi: ExtensionAPI) {
1096
1097
  label: "Start Pi Subagent Tasks",
1097
1098
  description: "Start independent subagent tasks together in the background and return their ids immediately. Completion notices automatically start the next parent turn, so the parent should end its current turn when no independent work remains instead of checking status.",
1098
1099
  promptGuidelines: [
1099
- "After task starts children, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch them run. Settlements are handed to Pi immediately; when their attached summaries invoke the parent, continue the original task without waiting for another user message.",
1100
+ "After task starts children, continue only independent parent work or end the turn immediately. Do not call wait_agent, list_agents, or check_agent merely to watch them run. Settlements stay private until they start a custom-result turn at Pi's safe idle edge; when their attached summaries invoke the parent, continue the original task without waiting for another user message.",
1100
1101
  ],
1101
1102
  parameters: Type.Object({
1102
1103
  tasks: Type.Array(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shariq-pi-extensions",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",
@@ -24,7 +24,7 @@ Provide:
24
24
  - the working directory when it differs from the current directory;
25
25
  - an initial wait only when startup output is needed for the next decision.
26
26
 
27
- After startup, continue only genuinely useful independent work. If none remains, end the turn immediately. Ending the turn is the waiting mechanism: terminal settlement is handed to Pi immediately as an extension-originated user follow-up, queued if the parent is still active, and otherwise starts the next parent turn with the final status and bounded output visible in model context. Do not keep the current turn alive to wait, invent monitoring work, or call terminal tools merely to see whether the process finished.
27
+ After startup, continue only genuinely useful independent work. If none remains, end the turn immediately. Ending the turn is the waiting mechanism: terminal settlement stays in a private extension queue while the parent is active and otherwise starts the next custom-result turn at Pi's safe idle edge, with final status and bounded output visible in model context without appearing as user-authored or follow-up input. Do not keep the current turn alive to wait, invent monitoring work, or call terminal tools merely to see whether the process finished.
28
28
 
29
29
  When that completion follow-up invokes the next turn, treat its model-visible output as the terminal result and continue the original task immediately. Do not wait for another user message, announce that you are still waiting, or call `read_terminal` to retrieve the same result again. If `start_terminal` itself returns a settled result, the output is already synchronous and no second completion notice is needed.
30
30
 
@@ -24,7 +24,7 @@ This skill governs temporary Pi child agents. The `codex-thread-orchestrator` sk
24
24
 
25
25
  ## Wait by notification; inspect progress only when justified
26
26
 
27
- A successful `spawn_agent` or `task` call starts asynchronous work and returns control to the parent. When a child finishes, its settlement is handed to Pi immediately as an extension-originated user follow-up, queued if the parent is still active, and otherwise starts the next main-agent turn with the summary visible in model context. The parent does not need to remain active or check once before ending its turn.
27
+ A successful `spawn_agent` or `task` call starts asynchronous work and returns control to the parent. When a child finishes, its settlement stays in a private extension queue while the parent is active and otherwise starts the next custom-result turn at Pi's safe idle edge, with the summary visible in model context without appearing as user-authored or follow-up input. The parent does not need to remain active or check once before ending its turn.
28
28
 
29
29
  After dispatch:
30
30