@ryan_nookpi/pi-extension-subagent 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/activity.ts +148 -0
- package/agents.ts +24 -19
- package/commands.ts +5 -0
- package/package.json +2 -1
- package/tool-execute.ts +15 -1
- package/utils/agent-utils.ts +4 -6
package/activity.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Throttled `subagent-activity` custom entries.
|
|
3
|
+
*
|
|
4
|
+
* Lets host UIs (e.g. Picky) observe live activity of headless subagent runs
|
|
5
|
+
* by appending change-driven, throttled entries to the parent session JSONL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { CommandRunState } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export const SUBAGENT_ACTIVITY_CUSTOM_TYPE = "subagent-activity";
|
|
12
|
+
export const SUBAGENT_ACTIVITY_SCHEMA_VERSION = 1;
|
|
13
|
+
export const ACTIVITY_MIN_INTERVAL_MS = 2500;
|
|
14
|
+
export const ACTIVITY_LAST_LINE_MAX_CHARS = 160;
|
|
15
|
+
|
|
16
|
+
export interface ActivitySnapshot {
|
|
17
|
+
status: CommandRunState["status"];
|
|
18
|
+
runId: number;
|
|
19
|
+
agent: string;
|
|
20
|
+
batchId?: string;
|
|
21
|
+
pipelineId?: string;
|
|
22
|
+
pipelineStepIndex?: number;
|
|
23
|
+
lastToolName?: string;
|
|
24
|
+
toolCallCount: number;
|
|
25
|
+
lastLine?: string;
|
|
26
|
+
contextTokens?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ActivityPayload {
|
|
30
|
+
runId: number;
|
|
31
|
+
agent: string;
|
|
32
|
+
batchId?: string;
|
|
33
|
+
pipelineId?: string;
|
|
34
|
+
pipelineStepIndex?: number;
|
|
35
|
+
lastToolName?: string;
|
|
36
|
+
toolCallCount: number;
|
|
37
|
+
lastLine?: string;
|
|
38
|
+
contextTokens?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ActivityThrottleState {
|
|
42
|
+
lastEmittedAt: number;
|
|
43
|
+
lastEmittedToolName?: string;
|
|
44
|
+
lastEmittedToolCallCount?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createActivityThrottleState(): ActivityThrottleState {
|
|
48
|
+
return { lastEmittedAt: 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ANSI_PATTERN =
|
|
52
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequences use control characters by definition.
|
|
53
|
+
/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|[\u0000-\u0008\u000b-\u001f\u007f]/g;
|
|
54
|
+
|
|
55
|
+
/** Strip ANSI sequences, collapse to a single line, and cap length. */
|
|
56
|
+
export function normalizeActivityLine(text: string | undefined): string | undefined {
|
|
57
|
+
if (!text) return undefined;
|
|
58
|
+
const line = text
|
|
59
|
+
.replace(/\r\n?/g, "\n")
|
|
60
|
+
.replace(ANSI_PATTERN, "")
|
|
61
|
+
.split("\n")
|
|
62
|
+
.map((part) => part.trim())
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.pop();
|
|
65
|
+
if (!line) return undefined;
|
|
66
|
+
return line.length > ACTIVITY_LAST_LINE_MAX_CHARS ? line.slice(0, ACTIVITY_LAST_LINE_MAX_CHARS) : line;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Change-driven throttle policy. Returns a payload to emit, or null.
|
|
71
|
+
*
|
|
72
|
+
* Emits only while the run is `running`, only when `lastToolName` or
|
|
73
|
+
* `toolCallCount` changed since the last emission, and at most once per
|
|
74
|
+
* `ACTIVITY_MIN_INTERVAL_MS` per run. A change suppressed by the interval
|
|
75
|
+
* stays pending and emits on a later evaluation.
|
|
76
|
+
*/
|
|
77
|
+
export function evaluateActivityEmission(
|
|
78
|
+
state: ActivityThrottleState,
|
|
79
|
+
snapshot: ActivitySnapshot,
|
|
80
|
+
now: number,
|
|
81
|
+
): ActivityPayload | null {
|
|
82
|
+
if (snapshot.status !== "running") return null;
|
|
83
|
+
// Continuations reset toolCalls to 0 while a stale lastToolName may linger
|
|
84
|
+
// on the run state, so a positive count is required regardless of the name.
|
|
85
|
+
if (snapshot.toolCallCount <= 0) return null;
|
|
86
|
+
|
|
87
|
+
const changed =
|
|
88
|
+
snapshot.lastToolName !== state.lastEmittedToolName || snapshot.toolCallCount !== state.lastEmittedToolCallCount;
|
|
89
|
+
if (!changed) return null;
|
|
90
|
+
if (state.lastEmittedAt !== 0 && now - state.lastEmittedAt < ACTIVITY_MIN_INTERVAL_MS) return null;
|
|
91
|
+
|
|
92
|
+
state.lastEmittedAt = now;
|
|
93
|
+
state.lastEmittedToolName = snapshot.lastToolName;
|
|
94
|
+
state.lastEmittedToolCallCount = snapshot.toolCallCount;
|
|
95
|
+
|
|
96
|
+
const payload: ActivityPayload = {
|
|
97
|
+
runId: snapshot.runId,
|
|
98
|
+
agent: snapshot.agent,
|
|
99
|
+
toolCallCount: snapshot.toolCallCount,
|
|
100
|
+
};
|
|
101
|
+
if (snapshot.batchId) payload.batchId = snapshot.batchId;
|
|
102
|
+
if (snapshot.pipelineId) payload.pipelineId = snapshot.pipelineId;
|
|
103
|
+
if (snapshot.pipelineStepIndex != null) payload.pipelineStepIndex = snapshot.pipelineStepIndex;
|
|
104
|
+
if (snapshot.lastToolName) payload.lastToolName = snapshot.lastToolName;
|
|
105
|
+
const lastLine = normalizeActivityLine(snapshot.lastLine);
|
|
106
|
+
if (lastLine) payload.lastLine = lastLine;
|
|
107
|
+
if (snapshot.contextTokens != null && snapshot.contextTokens > 0) payload.contextTokens = snapshot.contextTokens;
|
|
108
|
+
return payload;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function snapshotFromRunState(runState: CommandRunState): ActivitySnapshot {
|
|
112
|
+
return {
|
|
113
|
+
status: runState.status,
|
|
114
|
+
runId: runState.id,
|
|
115
|
+
agent: runState.agent,
|
|
116
|
+
batchId: runState.batchId,
|
|
117
|
+
pipelineId: runState.pipelineId,
|
|
118
|
+
pipelineStepIndex: runState.pipelineStepIndex,
|
|
119
|
+
lastToolName: runState.lastToolName,
|
|
120
|
+
toolCallCount: runState.toolCalls,
|
|
121
|
+
lastLine: runState.lastLine,
|
|
122
|
+
contextTokens: runState.usage?.contextTokens,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Best-effort recorder called from run update handlers. Never throws;
|
|
128
|
+
* activity logging must not affect run execution or completion delivery.
|
|
129
|
+
*/
|
|
130
|
+
export function createRunActivityRecorder(
|
|
131
|
+
pi: Pick<ExtensionAPI, "appendEntry">,
|
|
132
|
+
runState: CommandRunState,
|
|
133
|
+
): () => void {
|
|
134
|
+
const throttleState = createActivityThrottleState();
|
|
135
|
+
return () => {
|
|
136
|
+
try {
|
|
137
|
+
const payload = evaluateActivityEmission(throttleState, snapshotFromRunState(runState), Date.now());
|
|
138
|
+
if (!payload) return;
|
|
139
|
+
pi.appendEntry(SUBAGENT_ACTIVITY_CUSTOM_TYPE, {
|
|
140
|
+
schemaVersion: SUBAGENT_ACTIVITY_SCHEMA_VERSION,
|
|
141
|
+
recordedAt: new Date().toISOString(),
|
|
142
|
+
...payload,
|
|
143
|
+
});
|
|
144
|
+
} catch {
|
|
145
|
+
// Session teardown may invalidate the old extension runtime.
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
package/agents.ts
CHANGED
|
@@ -140,25 +140,30 @@ function loadAgentsFromDir(dir: string, source: "user" | "project", options: Loa
|
|
|
140
140
|
continue;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
143
|
+
try {
|
|
144
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);
|
|
145
|
+
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") continue;
|
|
146
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
147
|
+
|
|
148
|
+
const tools = normalizeTools(frontmatter.tools, format);
|
|
149
|
+
const model = normalizeModel(typeof frontmatter.model === "string" ? frontmatter.model : undefined, format);
|
|
150
|
+
const thinking = normalizeThinkingLevel(
|
|
151
|
+
typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
152
|
+
);
|
|
153
|
+
const runtime: AgentRuntime = frontmatter.runtime === "claude" ? "claude" : "pi";
|
|
154
|
+
|
|
155
|
+
agents.push({
|
|
156
|
+
name: frontmatter.name,
|
|
157
|
+
description: frontmatter.description,
|
|
158
|
+
tools,
|
|
159
|
+
model,
|
|
160
|
+
thinking,
|
|
161
|
+
systemPrompt: attachCommonSubagentRule(body, runtime),
|
|
162
|
+
source,
|
|
163
|
+
filePath,
|
|
164
|
+
runtime,
|
|
165
|
+
});
|
|
166
|
+
} catch {}
|
|
162
167
|
}
|
|
163
168
|
|
|
164
169
|
return agents;
|
package/commands.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Container, Key, matchesKey, Spacer, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
|
+
import { createRunActivityRecorder } from "./activity.js";
|
|
12
13
|
import { discoverAgents } from "./agents.js";
|
|
13
14
|
import { loadSubagentConfig } from "./config.js";
|
|
14
15
|
import {
|
|
@@ -1249,6 +1250,8 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1249
1250
|
runState.model = undefined;
|
|
1250
1251
|
runState.retryCount = 0;
|
|
1251
1252
|
runState.lastRetryReason = undefined;
|
|
1253
|
+
runState.errorClass = undefined;
|
|
1254
|
+
runState.autoAbortReason = undefined;
|
|
1252
1255
|
runState.removed = false;
|
|
1253
1256
|
runState.deliveryMode = deliveryMode;
|
|
1254
1257
|
runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
|
|
@@ -1399,6 +1402,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1399
1402
|
}, RUN_TICK_INTERVAL_MS);
|
|
1400
1403
|
|
|
1401
1404
|
let claudeCheckpointSent = !!runState.claudeSessionId;
|
|
1405
|
+
const recordActivity = hiddenFromMain ? () => {} : createRunActivityRecorder(pi, runState);
|
|
1402
1406
|
void (async () => {
|
|
1403
1407
|
try {
|
|
1404
1408
|
const { result, retryCount } = await invokeWithAutoRetry({
|
|
@@ -1428,6 +1432,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
1428
1432
|
const current = partial.details?.results?.[0];
|
|
1429
1433
|
if (!current) return;
|
|
1430
1434
|
updateRunFromResult(runState, current);
|
|
1435
|
+
recordActivity();
|
|
1431
1436
|
if (!claudeCheckpointSent && runState.claudeSessionId) {
|
|
1432
1437
|
claudeCheckpointSent = true;
|
|
1433
1438
|
if (!hiddenFromMain) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryan_nookpi/pi-extension-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"files": [
|
|
22
22
|
"above-widget.ts",
|
|
23
|
+
"activity.ts",
|
|
23
24
|
"agents.ts",
|
|
24
25
|
"claude-args.ts",
|
|
25
26
|
"claude-sdk-runner.ts",
|
package/tool-execute.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { createRunActivityRecorder } from "./activity.js";
|
|
11
12
|
import { discoverAgents } from "./agents.js";
|
|
12
13
|
import { parseSubagentToolCommand, SUBAGENT_CLI_HELP_TEXT } from "./cli.js";
|
|
13
14
|
import {
|
|
@@ -1253,6 +1254,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1253
1254
|
runState.lastOutput = "";
|
|
1254
1255
|
runState.usage = undefined;
|
|
1255
1256
|
runState.model = undefined;
|
|
1257
|
+
runState.retryCount = 0;
|
|
1258
|
+
runState.lastRetryReason = undefined;
|
|
1259
|
+
runState.errorClass = undefined;
|
|
1260
|
+
runState.autoAbortReason = undefined;
|
|
1256
1261
|
runState.removed = false;
|
|
1257
1262
|
runState.turnCount = Math.max(DEFAULT_TURN_COUNT, runState.turnCount || DEFAULT_TURN_COUNT) + 1;
|
|
1258
1263
|
runState.contextMode = runState.contextMode ?? (config.inheritMainContext ? "main" : "sub");
|
|
@@ -1314,6 +1319,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1314
1319
|
|
|
1315
1320
|
function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
|
|
1316
1321
|
let claudeCheckpointSent = !!runState.claudeSessionId;
|
|
1322
|
+
const recordActivity = createRunActivityRecorder(pi, runState);
|
|
1317
1323
|
return enqueueSubagentInvocation(() =>
|
|
1318
1324
|
runSingleAgent(
|
|
1319
1325
|
ctx.cwd,
|
|
@@ -1327,6 +1333,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1327
1333
|
const current = partial.details?.results?.[0];
|
|
1328
1334
|
if (!current) return;
|
|
1329
1335
|
updateRunFromResult(runState, current);
|
|
1336
|
+
recordActivity();
|
|
1330
1337
|
if (shouldRunAsync && !claudeCheckpointSent && runState.claudeSessionId) {
|
|
1331
1338
|
claudeCheckpointSent = true;
|
|
1332
1339
|
pi.sendMessage(buildRunStartMessage(runState, "started"), {
|
|
@@ -1976,7 +1983,14 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
|
|
|
1976
1983
|
pipeline.stepRunIds.push(runState.id);
|
|
1977
1984
|
chainLaunches.push(toLaunchSummary(runState, "chain"));
|
|
1978
1985
|
|
|
1979
|
-
|
|
1986
|
+
let finalized: FinalizedRun;
|
|
1987
|
+
try {
|
|
1988
|
+
finalized = await launchRunInBackground(runState, taskForAgent);
|
|
1989
|
+
} catch (error: unknown) {
|
|
1990
|
+
finalized = finalizeRunError(runState, error);
|
|
1991
|
+
} finally {
|
|
1992
|
+
clearRunAbortState(runState);
|
|
1993
|
+
}
|
|
1980
1994
|
if (runState.removed) {
|
|
1981
1995
|
terminalStatus = finalized.isError ? "error" : "stopped";
|
|
1982
1996
|
pipeline.stepResults.push({
|
package/utils/agent-utils.ts
CHANGED
|
@@ -88,12 +88,10 @@ export const CLAUDE_MODEL_ALIAS_MAP: Record<string, string> = {
|
|
|
88
88
|
* Normalize a comma-separated tool list according to the given format.
|
|
89
89
|
* For "claude" format, maps Claude tool names to pi equivalents.
|
|
90
90
|
*/
|
|
91
|
-
export function normalizeTools(rawTools:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.split(",")
|
|
96
|
-
.map((t) => t.trim())
|
|
91
|
+
export function normalizeTools(rawTools: unknown, format: "pi" | "claude"): string[] | undefined {
|
|
92
|
+
const parsed = (Array.isArray(rawTools) ? rawTools : typeof rawTools === "string" ? rawTools.split(",") : [])
|
|
93
|
+
.filter((tool): tool is string => typeof tool === "string")
|
|
94
|
+
.map((tool) => tool.trim())
|
|
97
95
|
.filter(Boolean);
|
|
98
96
|
|
|
99
97
|
if (parsed.length === 0) return undefined;
|