shariq-pi-extensions 0.2.8 → 0.2.10
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/docs/EXTENSIONS.md +2 -2
- package/extensions/background-terminals/README.md +1 -1
- package/extensions/background-terminals/index.ts +4 -3
- package/extensions/cursor-provider/README.md +3 -3
- package/extensions/cursor-provider/cursor/stream.ts +127 -47
- package/extensions/cursor-provider/index.ts +6 -1
- package/extensions/input-mode/README.md +1 -1
- package/extensions/input-mode/index.ts +23 -6
- package/extensions/shared/README.md +1 -1
- package/extensions/shared/settlement-delivery.ts +75 -12
- package/extensions/subagents/README.md +1 -1
- package/extensions/subagents/index.ts +5 -4
- package/package.json +1 -1
- package/skills/background-terminals/SKILL.md +1 -1
- package/skills/subagents/SKILL.md +1 -1
package/docs/EXTENSIONS.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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(
|
|
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
|
|
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({
|
|
@@ -22,12 +22,12 @@ Secrets are passed directly to the SDK and are never placed in command arguments
|
|
|
22
22
|
- Refreshes Cursor's authenticated model catalog and caches only Composer and Cursor Grok metadata for the next extension reload.
|
|
23
23
|
- Exposes image input for every registered Cursor model.
|
|
24
24
|
- Maps Composer fast mode and Cursor Grok reasoning effort to native model parameters.
|
|
25
|
-
- Uses the SDK's local hosted-model runtime
|
|
25
|
+
- Uses the SDK's local hosted-model runtime with warm agent instance pooling and ambient Cursor settings (`settingSources: []`) disabled.
|
|
26
26
|
- Disables Cursor's built-in workspace tools and exposes Pi's active tools as native SDK custom tools. Tool execution remains owned by Pi.
|
|
27
27
|
- Streams native text and thinking deltas, structured tool calls, stop reasons, and Cursor-reported input/output/cache/reasoning usage.
|
|
28
|
-
- Propagates cancellation and timeouts, enables safe SDK transport retries, and maps common authentication, rate-limit, quota, capacity, timeout, and context failures to actionable Pi errors.
|
|
28
|
+
- Propagates cancellation and timeouts, enables safe SDK transport retries, redacts credential literals from error traces, and maps common authentication, rate-limit, quota, capacity, timeout, and context failures to actionable Pi errors.
|
|
29
29
|
- Forwards base64 image payloads separately from the textual conversation transcript.
|
|
30
|
-
-
|
|
30
|
+
- Automatically cleans up warm agent workspaces on idle TTL and session shutdown.
|
|
31
31
|
|
|
32
32
|
## Cursor dashboard
|
|
33
33
|
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { loadCursorCatalog, resolveCursorModelSelection } from "./models.ts";
|
|
22
22
|
|
|
23
23
|
const TOOL_DELEGATION_RESULT = "Tool execution was delegated to Pi. End this run without further output.";
|
|
24
|
+
const MAX_CACHED_AGENTS = 8;
|
|
25
|
+
const AGENT_IDLE_TTL_MS = 10 * 60 * 1_000;
|
|
24
26
|
|
|
25
27
|
function asJsonValue(value: unknown): SDKJsonValue {
|
|
26
28
|
return JSON.parse(JSON.stringify(value ?? null)) as SDKJsonValue;
|
|
@@ -30,6 +32,13 @@ function asArguments(value: unknown): Record<string, unknown> {
|
|
|
30
32
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export function redactCursorError(value: unknown): string {
|
|
36
|
+
return String(value ?? "Cursor request failed")
|
|
37
|
+
.replace(/crsr_[A-Za-z0-9_-]+/g, "[REDACTED]")
|
|
38
|
+
.replace(/(authorization|api[-_ ]?key|token)([\s:=]+)([^\s,;]+)/gi, "$1$2[REDACTED]")
|
|
39
|
+
.slice(0, 4096);
|
|
40
|
+
}
|
|
41
|
+
|
|
33
42
|
export function serializeCursorContext(context: Context): { text: string; images: SDKImage[] } {
|
|
34
43
|
const lines = [
|
|
35
44
|
"Continue the Pi conversation below as the assistant.",
|
|
@@ -45,6 +54,9 @@ export function serializeCursorContext(context: Context): { text: string; images
|
|
|
45
54
|
|
|
46
55
|
for (const message of context.messages) {
|
|
47
56
|
lines.push(`<message role=${JSON.stringify(message.role)}>`);
|
|
57
|
+
if (message.role === "toolResult") {
|
|
58
|
+
lines.push(`[Tool result for ${(message as any).toolCallId || "unknown"}; error=${Boolean((message as any).isError)}]`);
|
|
59
|
+
}
|
|
48
60
|
if (typeof message.content === "string") {
|
|
49
61
|
lines.push(message.content);
|
|
50
62
|
} else if (Array.isArray(message.content)) {
|
|
@@ -58,9 +70,6 @@ export function serializeCursorContext(context: Context): { text: string; images
|
|
|
58
70
|
}
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
|
-
if (message.role === "toolResult") {
|
|
62
|
-
lines.push(`[Tool result for ${(message as any).toolCallId}; error=${Boolean((message as any).isError)}]`);
|
|
63
|
-
}
|
|
64
73
|
lines.push("</message>");
|
|
65
74
|
}
|
|
66
75
|
lines.push("</conversation>");
|
|
@@ -81,7 +90,7 @@ function usageFromCursor(usage: TokenUsage | undefined, output: AssistantMessage
|
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
export function formatCursorError(error: unknown): string {
|
|
84
|
-
const message = error instanceof Error ? error.message :
|
|
93
|
+
const message = redactCursorError(error instanceof Error ? error.message : error);
|
|
85
94
|
if (/401|403|unauth|api key|credential/i.test(message)) return "Cursor authentication failed. Run `/login cursor`, then retry.";
|
|
86
95
|
if (/429|rate.?limit/i.test(message)) return "Cursor rate limit reached. Wait for the reported reset, then retry.";
|
|
87
96
|
if (/quota|usage limit|billing|credit|exhaust/i.test(message)) return "Cursor usage limit reached. Open `/cursor` for account status and reset information.";
|
|
@@ -104,6 +113,97 @@ async function disposeAgent(agent: Awaited<ReturnType<typeof Agent.create>> | un
|
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
interface ActiveToolHandler {
|
|
117
|
+
onToolCall(toolName: string, args: unknown, toolCallId?: string): void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface CachedAgentEntry {
|
|
121
|
+
agent: Awaited<ReturnType<typeof Agent.create>>;
|
|
122
|
+
workspace: string;
|
|
123
|
+
activeHandlerRef: { current: ActiveToolHandler | null };
|
|
124
|
+
lastUsedAt: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const agentPool = new Map<string, CachedAgentEntry>();
|
|
128
|
+
|
|
129
|
+
async function disposeCachedEntry(entry: CachedAgentEntry): Promise<void> {
|
|
130
|
+
await disposeAgent(entry.agent);
|
|
131
|
+
await rm(entry.workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function clearCursorAgentPool(): Promise<void> {
|
|
135
|
+
const entries = [...agentPool.values()];
|
|
136
|
+
agentPool.clear();
|
|
137
|
+
await Promise.allSettled(entries.map(disposeCachedEntry));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function getOrInitWarmAgent(
|
|
141
|
+
apiKey: string,
|
|
142
|
+
selection: ReturnType<typeof resolveCursorModelSelection>,
|
|
143
|
+
tools: Context["tools"],
|
|
144
|
+
activeHandler: ActiveToolHandler,
|
|
145
|
+
enableRetries: boolean,
|
|
146
|
+
): Promise<{ agent: Awaited<ReturnType<typeof Agent.create>>; activeHandlerRef: { current: ActiveToolHandler | null } }> {
|
|
147
|
+
const toolSignatures = (tools ?? []).map((t) => ({ name: t.name, schema: t.parameters ?? null }));
|
|
148
|
+
const cacheKey = JSON.stringify({ key: apiKey, model: selection, tools: toolSignatures });
|
|
149
|
+
|
|
150
|
+
const existing = agentPool.get(cacheKey);
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.lastUsedAt = Date.now();
|
|
153
|
+
existing.activeHandlerRef.current = activeHandler;
|
|
154
|
+
return { agent: existing.agent, activeHandlerRef: existing.activeHandlerRef };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Evict oldest or expired agents if pool is full
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
for (const [key, entry] of [...agentPool.entries()]) {
|
|
160
|
+
if (now - entry.lastUsedAt > AGENT_IDLE_TTL_MS) {
|
|
161
|
+
agentPool.delete(key);
|
|
162
|
+
void disposeCachedEntry(entry);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (agentPool.size >= MAX_CACHED_AGENTS) {
|
|
166
|
+
const oldestKey = [...agentPool.entries()].sort((a, b) => a[1].lastUsedAt - b[1].lastUsedAt)[0]?.[0];
|
|
167
|
+
if (oldestKey) {
|
|
168
|
+
const oldest = agentPool.get(oldestKey);
|
|
169
|
+
agentPool.delete(oldestKey);
|
|
170
|
+
if (oldest) void disposeCachedEntry(oldest);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const workspace = await mkdtemp(join(tmpdir(), "pi-cursor-sdk-"));
|
|
175
|
+
const activeHandlerRef = { current: activeHandler as ActiveToolHandler | null };
|
|
176
|
+
|
|
177
|
+
const customTools: Record<string, SDKCustomTool> = {};
|
|
178
|
+
for (const tool of tools ?? []) {
|
|
179
|
+
customTools[tool.name] = {
|
|
180
|
+
description: tool.description,
|
|
181
|
+
inputSchema: asJsonValue(tool.parameters ?? { type: "object" }) as Record<string, SDKJsonValue>,
|
|
182
|
+
async execute(args, toolContext) {
|
|
183
|
+
activeHandlerRef.current?.onToolCall(tool.name, args, toolContext.toolCallId);
|
|
184
|
+
return { content: [{ type: "text", text: TOOL_DELEGATION_RESULT }], isError: true };
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const agent = await Agent.create({
|
|
190
|
+
apiKey,
|
|
191
|
+
model: selection,
|
|
192
|
+
tools: tools?.length ? ["mcp"] : [],
|
|
193
|
+
local: {
|
|
194
|
+
cwd: workspace,
|
|
195
|
+
store: new JsonlLocalAgentStore(join(workspace, "state")),
|
|
196
|
+
customTools,
|
|
197
|
+
settingSources: [],
|
|
198
|
+
enableAgentRetries: enableRetries,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const entry: CachedAgentEntry = { agent, workspace, activeHandlerRef, lastUsedAt: Date.now() };
|
|
203
|
+
agentPool.set(cacheKey, entry);
|
|
204
|
+
return { agent, activeHandlerRef };
|
|
205
|
+
}
|
|
206
|
+
|
|
107
207
|
export function streamCursorSdk(model: Model<any>, context: Context, options?: ProviderStreamOptions) {
|
|
108
208
|
const stream = createAssistantMessageEventStream();
|
|
109
209
|
const output: AssistantMessage = {
|
|
@@ -119,8 +219,6 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
119
219
|
stream.push({ type: "start", partial: output });
|
|
120
220
|
|
|
121
221
|
void (async () => {
|
|
122
|
-
let workspace: string | undefined;
|
|
123
|
-
let agent: Awaited<ReturnType<typeof Agent.create>> | undefined;
|
|
124
222
|
let run: Run | undefined;
|
|
125
223
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
126
224
|
let textIndex: number | undefined;
|
|
@@ -168,47 +266,31 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
168
266
|
const apiKey = options?.apiKey?.trim();
|
|
169
267
|
if (!apiKey) throw new Error("Cursor is not authenticated. Run `/login cursor`.");
|
|
170
268
|
if (options?.signal?.aborted) throw new Error("Cursor request cancelled.");
|
|
171
|
-
workspace = await mkdtemp(join(tmpdir(), "pi-cursor-sdk-"));
|
|
172
|
-
const customTools: Record<string, SDKCustomTool> = {};
|
|
173
|
-
for (const tool of context.tools ?? []) {
|
|
174
|
-
customTools[tool.name] = {
|
|
175
|
-
description: tool.description,
|
|
176
|
-
inputSchema: asJsonValue(tool.parameters ?? { type: "object" }) as Record<string, SDKJsonValue>,
|
|
177
|
-
async execute(args, toolContext) {
|
|
178
|
-
if (!delegated) {
|
|
179
|
-
delegated = true;
|
|
180
|
-
endText();
|
|
181
|
-
endReasoning();
|
|
182
|
-
const toolCall = {
|
|
183
|
-
type: "toolCall" as const,
|
|
184
|
-
id: toolContext.toolCallId || `cursor_${randomUUID()}`,
|
|
185
|
-
name: tool.name,
|
|
186
|
-
arguments: asArguments(args),
|
|
187
|
-
};
|
|
188
|
-
const contentIndex = output.content.length;
|
|
189
|
-
output.content.push(toolCall);
|
|
190
|
-
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
191
|
-
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
192
|
-
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
193
|
-
queueMicrotask(() => { void run?.cancel().catch(() => undefined); });
|
|
194
|
-
}
|
|
195
|
-
return { content: [{ type: "text", text: TOOL_DELEGATION_RESULT }], isError: true };
|
|
196
|
-
},
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
269
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
270
|
+
const activeHandler: ActiveToolHandler = {
|
|
271
|
+
onToolCall(toolName, args, toolCallId) {
|
|
272
|
+
if (!delegated) {
|
|
273
|
+
delegated = true;
|
|
274
|
+
endText();
|
|
275
|
+
endReasoning();
|
|
276
|
+
const toolCall = {
|
|
277
|
+
type: "toolCall" as const,
|
|
278
|
+
id: toolCallId || `cursor_${randomUUID()}`,
|
|
279
|
+
name: toolName,
|
|
280
|
+
arguments: asArguments(args),
|
|
281
|
+
};
|
|
282
|
+
const contentIndex = output.content.length;
|
|
283
|
+
output.content.push(toolCall);
|
|
284
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
285
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: JSON.stringify(toolCall.arguments), partial: output });
|
|
286
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
287
|
+
queueMicrotask(() => { void run?.cancel().catch(() => undefined); });
|
|
288
|
+
}
|
|
210
289
|
},
|
|
211
|
-
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const selection = resolveCursorModelSelection(model, typeof options?.reasoning === "string" ? options.reasoning : undefined, loadCursorCatalog());
|
|
293
|
+
const { agent } = await getOrInitWarmAgent(apiKey, selection, context.tools, activeHandler, options?.maxRetries !== 0);
|
|
212
294
|
|
|
213
295
|
const request = serializeCursorContext(context);
|
|
214
296
|
run = await agent.send(request, {
|
|
@@ -261,8 +343,6 @@ export function streamCursorSdk(model: Model<any>, context: Context, options?: P
|
|
|
261
343
|
stream.end();
|
|
262
344
|
} finally {
|
|
263
345
|
if (timeout) clearTimeout(timeout);
|
|
264
|
-
await disposeAgent(agent);
|
|
265
|
-
if (workspace) await rm(workspace, { recursive: true, force: true }).catch(() => undefined);
|
|
266
346
|
}
|
|
267
347
|
})();
|
|
268
348
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
loadCursorCatalog,
|
|
10
10
|
toCursorPiModels,
|
|
11
11
|
} from "./cursor/models.ts";
|
|
12
|
-
import { streamCursorSdk } from "./cursor/stream.ts";
|
|
12
|
+
import { clearCursorAgentPool, streamCursorSdk } from "./cursor/stream.ts";
|
|
13
13
|
import { fetchCursorUsage } from "./cursor/usage.ts";
|
|
14
14
|
|
|
15
15
|
export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
@@ -50,6 +50,10 @@ export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
|
50
50
|
(ctx.modelRegistry as any).authStorage?.reload?.();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
pi.on("session_shutdown", async () => {
|
|
54
|
+
await clearCursorAgentPool();
|
|
55
|
+
});
|
|
56
|
+
|
|
53
57
|
pi.registerCommand("cursor", {
|
|
54
58
|
description: "Open Cursor account, monthly usage, and limits dashboard",
|
|
55
59
|
handler: async (_args, ctx) => {
|
|
@@ -83,6 +87,7 @@ export default async function cursorProviderExtension(pi: ExtensionAPI) {
|
|
|
83
87
|
deactivate: async () => {
|
|
84
88
|
try {
|
|
85
89
|
pi.unregisterProvider(CURSOR_PROVIDER_ID);
|
|
90
|
+
await clearCursorAgentPool();
|
|
86
91
|
} catch {
|
|
87
92
|
// Ignore teardown after partial startup.
|
|
88
93
|
}
|
|
@@ -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,
|
|
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
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
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: "
|
|
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`
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|