@ryan_nookpi/pi-extension-subagent 0.4.10 → 0.5.0

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 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/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 {
@@ -1399,6 +1400,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1399
1400
  }, RUN_TICK_INTERVAL_MS);
1400
1401
 
1401
1402
  let claudeCheckpointSent = !!runState.claudeSessionId;
1403
+ const recordActivity = hiddenFromMain ? () => {} : createRunActivityRecorder(pi, runState);
1402
1404
  void (async () => {
1403
1405
  try {
1404
1406
  const { result, retryCount } = await invokeWithAutoRetry({
@@ -1428,6 +1430,7 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1428
1430
  const current = partial.details?.results?.[0];
1429
1431
  if (!current) return;
1430
1432
  updateRunFromResult(runState, current);
1433
+ recordActivity();
1431
1434
  if (!claudeCheckpointSent && runState.claudeSessionId) {
1432
1435
  claudeCheckpointSent = true;
1433
1436
  if (!hiddenFromMain) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
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 {
@@ -1314,6 +1315,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1314
1315
 
1315
1316
  function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
1316
1317
  let claudeCheckpointSent = !!runState.claudeSessionId;
1318
+ const recordActivity = createRunActivityRecorder(pi, runState);
1317
1319
  return enqueueSubagentInvocation(() =>
1318
1320
  runSingleAgent(
1319
1321
  ctx.cwd,
@@ -1327,6 +1329,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1327
1329
  const current = partial.details?.results?.[0];
1328
1330
  if (!current) return;
1329
1331
  updateRunFromResult(runState, current);
1332
+ recordActivity();
1330
1333
  if (shouldRunAsync && !claudeCheckpointSent && runState.claudeSessionId) {
1331
1334
  claudeCheckpointSent = true;
1332
1335
  pi.sendMessage(buildRunStartMessage(runState, "started"), {