pi-tool-duration 0.2.2 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-09-21
4
+
5
+ ### Changed
6
+
7
+ - Annotate every tool result by default with `[host tool-call elapsed: Xs]`. Set `PI_TOOL_DURATION_THRESHOLD_MS=1000` to retain slow/failed-only reporting.
8
+ - Require Pi 0.87.0 or later, using the same native extension APIs on official Pi and the maintained fork.
9
+ - Resolve timings through a stateless backward ancestry lookup instead of rebuilding the complete historical timing map for every request.
10
+
11
+ ### Fixed
12
+
13
+ - Preserve prompt and tool updates in model context so duration annotations do not collapse the reusable request prefix.
14
+ - Associate timing with the finalized result when a later extension replaces its timestamp, preserving existing saved timing records.
15
+
16
+ ### Tests
17
+
18
+ - Exercise native Responses serialization, dynamic prompt/tool prefixes, and finalized timestamps.
19
+ - Verify actual timed results through disk restoration and maintained-fork checkpoints.
20
+
21
+ ## [0.2.3] - 2026-09-18
22
+
23
+ ### Fixed
24
+
25
+ - Preserve timing in native compaction input when long tool output is truncated, including split-turn summaries. Ordinary requests still append one duration marker, and recorded tool content and details remain unchanged.
26
+
3
27
  ## [0.2.2] - 2026-09-18
4
28
 
5
29
  ### Fixed
package/README.md CHANGED
@@ -1,81 +1,83 @@
1
1
  # pi-tool-duration
2
2
 
3
- Appends `[duration: Xs]` to slow Pi tool results so the model can tell when a tool actually took time.
3
+ Makes host-observed tool-call timing visible to the model without changing Pi's terminal output.
4
4
 
5
5
  ```text
6
6
  hi
7
- [duration: 5.0s]
7
+ [host tool-call elapsed: 5.0s]
8
8
  ```
9
9
 
10
- ## Why
11
-
12
- Pi already shows tool timing in the TUI (`Took Xs`), but that timing is UI-only. This extension adds the elapsed time to the model-visible tool result for slow calls.
10
+ Every completed tool result is annotated by default. An optional threshold limits successful-call annotations; failed calls are always annotated.
13
11
 
14
12
  ## How it works
15
13
 
16
- The extension measures from Pi's `tool_execution_start` through `tool_execution_end`. When elapsed time is at or above the configured threshold, or Pi marks the result as failed, it saves the timing in a hidden session entry. Before each model request, it appends one text block to that request's copy of the tool result:
17
-
18
- ```text
19
- [duration: 5.0s]
20
- ```
14
+ The extension measures from Pi's `tool_execution_start` through `tool_execution_end` using a monotonic clock. It saves the timing in a hidden session entry and appends one text block to the model-request copy of the result. Durations are rounded to tenths of a second.
21
15
 
22
- Compaction summaries also receive annotated copies of tool results. Recorded tool output stays unchanged, including after reload, resume, and branch navigation. Timings remain available to the model across those transitions without duplicating Pi's native TUI timing such as `Took 5.0s`.
16
+ The measurement includes preflight and result processing. In a parallel batch, it can include time spent preparing later siblings, but stops when this call finishes even if another call continues. For a tool that launches a background job, it measures the launch call. Parallel durations are not additive task elapsed time.
23
17
 
24
- Markers already saved as tool text by versions through 0.2.1 remain unchanged. They cannot be safely distinguished from genuine tool output with the same text.
18
+ Recorded tool content, details, images, and native terminal rendering stay unchanged. Timings survive reload, resume, forks, branch navigation, and retained compaction history. Request-time lookup walks backward only as far as needed to find the visible results and their preceding timing records; older or unidentifiable results can require a longer walk.
25
19
 
26
- Scope: Pi tools that emit tool execution events, including built-ins and extension tools. Direct `!` / `!!` shell commands and RPC `bash` command messages are not tool results and are not annotated.
20
+ Compaction input copies put timing before tool output so Pi's truncation preserves it in the summarizer's input. Generated summaries may omit individual timings. Native branch summaries exclude tool results. Historical markers keep their original text, and tool output resembling a marker is never removed or rewritten.
27
21
 
28
- Pi starts these timers during sequential tool-call preflight. In a parallel batch, a call's elapsed time can therefore include time spent preparing later siblings. This mirrors Pi's TUI timing.
22
+ Scope: built-in and extension tools that emit Pi execution events. Direct `!` / `!!` shell commands and RPC `bash` command messages are not tool results and are not annotated.
29
23
 
30
24
  ## Install
31
25
 
32
- Requires Pi 0.84.0 or later.
26
+ Requires Pi **0.87.0 or later**. Tested against official Pi and the maintained `fitchmultz/pi` fork.
33
27
 
34
28
  ```bash
35
- pi install . # local, global settings
36
- pi install -l --approve . # local, project settings
37
- pi install npm:pi-tool-duration # published package
29
+ pi install npm:pi-tool-duration
38
30
  ```
39
31
 
40
- ## Try without installing
41
-
42
- From this repo:
32
+ For local development:
43
33
 
44
34
  ```bash
45
- pi --no-extensions -e .
46
- # or
47
- pi --no-extensions -e ./extensions/tool-duration/index.ts
35
+ pi install . # global settings
36
+ pi install -l --approve . # project settings
37
+ pi --no-extensions -e . # try without installing
48
38
  ```
49
39
 
50
- `--no-extensions` prevents a duplicate flag conflict when another copy is already installed.
40
+ `--no-extensions` prevents a duplicate flag conflict when another copy is already installed. Restart Pi after updating extension code; `/reload` reinitializes the loaded code but does not replace it.
51
41
 
52
42
  ## Configure
53
43
 
54
- Default threshold: `1000` ms.
44
+ Default threshold: **0 ms**, including fast calls.
55
45
 
56
46
  ```bash
57
- PI_TOOL_DURATION_THRESHOLD_MS=0 pi --no-extensions -e . # annotate every tool result
58
- pi --no-extensions -e . --tool-duration-threshold-ms 500 # annotate tools taking >= 500ms
47
+ # Restore slow/failed-only reporting:
48
+ PI_TOOL_DURATION_THRESHOLD_MS=1000 pi --no-extensions -e .
49
+
50
+ # CLI value takes precedence:
51
+ pi --no-extensions -e . --tool-duration-threshold-ms 500
59
52
  ```
60
53
 
61
- Invalid values are ignored. An invalid CLI value falls through to the environment value; an invalid environment value falls back to the default.
54
+ Invalid CLI values fall through to the environment value; invalid environment values fall back to zero. A successful result below a configured threshold stays unchanged. Missing timing does not establish a zero-duration call.
55
+
56
+ ## GPT-6 Astra
57
+
58
+ Use Pi's native OpenAI or OpenAI Codex Responses provider. The extension uses `context_with_system` to preserve the positions of prompt and tool updates, allowing native caching and incremental requests to work.
59
+
60
+ Prefer Pi's built-in model definitions. To adjust a model's limits, use [`modelOverrides`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/models.md#per-model-overrides), which preserves native compatibility metadata. A same-ID entry in `models` replaces that metadata.
61
+
62
+ Transport selection, reasoning settings, asynchronous tool execution, and steering remain Pi's responsibility. The extension does not alter provider requests or add model instructions.
62
63
 
63
64
  ## Verify
64
65
 
65
- In a session running the extension, ask Pi to use bash:
66
+ Ask Pi to run a tool, for example:
66
67
 
67
68
  ```text
68
- Use bash to run: sleep 5; echo hi
69
+ Use bash to run: sleep 1; echo hi
69
70
  ```
70
71
 
71
- The model sees:
72
+ The model receives the output plus a host timing marker. Pi's terminal retains its native rendering.
72
73
 
73
- ```text
74
- hi
75
- [duration: 5.0s]
76
- ```
74
+ ## Development
75
+
76
+ Run `npm ci --ignore-scripts` and `npm run check:compat` for typechecking, unit/native runtime tests, and a pack dry-run. There is no production build or `prepare` step. The development host is pinned to official `0.87.0`; CI separately qualifies official Pi and the maintained fork. The host-provided Pi peer stays wildcard and optional rather than bundling a runtime.
77
+
78
+ Runtime tests use the installed host's manifest `bin.pi` entry and a local scripted Responses provider in an isolated HOME. `PI_HOST_CLI`, `PI_COMPAT_EXPECTED_VERSION`, and `PI_COMPAT_EXPECTED_PACKAGE_DIR` can assert the selected graph.
77
79
 
78
- A fast successful tool below the threshold stays unchanged. A failed tool result delivered to the model is always annotated, even below the threshold.
80
+ The restoration test uses the installed host by default; `PI_HOST_INDEX` can select another host's absolute `dist/index.js`. It executes a timed tool and verifies reload plus separate-process disk restoration on both hosts. Native checkpoint restoration also runs when available and is required when `PI_COMPAT_HOST=fork` or `PI_REQUIRE_CHECKPOINT=1`. These tests use local scripted model completions without provider network calls or credentials.
79
81
 
80
82
  ## License
81
83
 
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * pi-tool-duration
3
3
  *
4
- * Appends `[duration: Xs]` to slow or failed tool messages so the model sees
5
- * how long a call actually took. pi already measures this for the TUI
6
- * ("Took Xs") but the model does not see that timing.
4
+ * Adds host-observed tool-call timing to model-request copies without
5
+ * changing recorded tool output or Pi's native terminal rendering.
7
6
  */
8
- import type { ContextEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import type { ContextWithSystemEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
8
 
10
- const DEFAULT_THRESHOLD_MS = 1000;
9
+ const DEFAULT_THRESHOLD_MS = 0;
11
10
  const TIMING_ENTRY = "pi-tool-duration";
12
11
 
13
12
  type SavedTiming = { toolCallId: string; timestamp: number; duration: string };
@@ -27,26 +26,56 @@ function thresholdMs(pi: ExtensionAPI): number {
27
26
  );
28
27
  }
29
28
 
30
- function withDurations(messages: ContextEvent["messages"], ctx: ExtensionContext) {
29
+ function timingKey(message: { timestamp: number; toolCallId: string }): string {
30
+ return `${message.timestamp}:${message.toolCallId}`;
31
+ }
32
+
33
+ function withDurations(
34
+ messages: ContextWithSystemEvent["messages"],
35
+ ctx: ExtensionContext,
36
+ position: "append" | "prepend" = "append",
37
+ ) {
38
+ const remaining = new Set(messages.filter((message) => message.role === "toolResult").map(timingKey));
39
+ if (!remaining.size) return messages;
40
+
31
41
  const saved = new Map<string, string>();
32
- for (const entry of ctx.sessionManager.getBranch()) {
33
- if (entry.type !== "custom" || entry.customType !== TIMING_ENTRY) continue;
34
- const timing = entry.data as SavedTiming | undefined;
35
- if (
36
- typeof timing?.toolCallId !== "string" ||
37
- typeof timing.timestamp !== "number" ||
38
- typeof timing.duration !== "string"
39
- ) continue;
40
- saved.set(`${timing.timestamp}:${timing.toolCallId}`, timing.duration);
42
+ let pending: { key: string; toolCallId: string } | undefined;
43
+ // ponytail: old or unknown results can scan the full ancestry; native timing metadata would remove this join.
44
+ for (let id = ctx.sessionManager.getLeafId(); id && remaining.size;) {
45
+ const entry = ctx.sessionManager.getEntry(id);
46
+ if (!entry) break;
47
+ id = entry.parentId;
48
+ if (entry.type === "message") {
49
+ if (pending) {
50
+ remaining.delete(pending.key);
51
+ pending = undefined;
52
+ if (!remaining.size) break;
53
+ }
54
+ if (entry.message.role === "toolResult" && remaining.has(timingKey(entry.message))) {
55
+ pending = { key: timingKey(entry.message), toolCallId: entry.message.toolCallId };
56
+ }
57
+ } else if (pending && entry.type === "custom" && entry.customType === TIMING_ENTRY) {
58
+ const timing = entry.data as SavedTiming | undefined;
59
+ if (
60
+ typeof timing?.toolCallId !== "string" ||
61
+ typeof timing.timestamp !== "number" ||
62
+ typeof timing.duration !== "string"
63
+ ) continue;
64
+ // The following result's timestamp may have been replaced by another message_end handler.
65
+ if (timing.toolCallId === pending.toolCallId) saved.set(pending.key, timing.duration);
66
+ remaining.delete(pending.key);
67
+ pending = undefined;
68
+ }
41
69
  }
42
70
 
43
71
  return messages.map((message) => {
44
72
  if (message.role !== "toolResult") return message;
45
- const duration = saved.get(`${message.timestamp}:${message.toolCallId}`);
73
+ const duration = saved.get(timingKey(message));
46
74
  if (!duration) return message;
75
+ const marker = { type: "text" as const, text: duration };
47
76
  return {
48
77
  ...message,
49
- content: [...message.content, { type: "text" as const, text: duration }],
78
+ content: position === "prepend" ? [marker, ...message.content] : [...message.content, marker],
50
79
  };
51
80
  });
52
81
  }
@@ -72,7 +101,7 @@ export default function (pi: ExtensionAPI) {
72
101
 
73
102
  const ms = performance.now() - startedAt;
74
103
  if (!event.isError && ms < thresholdMs(pi)) return;
75
- durations.set(event.toolCallId, `[duration: ${(ms / 1000).toFixed(1)}s]`);
104
+ durations.set(event.toolCallId, `[host tool-call elapsed: ${(ms / 1000).toFixed(1)}s]`);
76
105
  });
77
106
 
78
107
  pi.on("message_end", (event) => {
@@ -89,12 +118,13 @@ export default function (pi: ExtensionAPI) {
89
118
  });
90
119
  });
91
120
 
92
- pi.on("context", (event, ctx) => ({ messages: withDurations(event.messages, ctx) }));
121
+ pi.on("context_with_system", (event, ctx) => ({ messages: withDurations(event.messages, ctx) }));
93
122
 
94
123
  pi.on("session_before_compact", ({ preparation }, ctx) => {
95
- // Native summarization bypasses context hooks. Replace its inputs, never the saved messages.
96
- preparation.messagesToSummarize = withDurations(preparation.messagesToSummarize, ctx);
97
- preparation.turnPrefixMessages = withDurations(preparation.turnPrefixMessages, ctx);
124
+ // Native summarization bypasses context hooks and truncates tool text from the end.
125
+ // Put timing first in its input copies, never in saved messages or ordinary requests.
126
+ preparation.messagesToSummarize = withDurations(preparation.messagesToSummarize, ctx, "prepend");
127
+ preparation.turnPrefixMessages = withDurations(preparation.turnPrefixMessages, ctx, "prepend");
98
128
  });
99
129
 
100
130
  const clearTimings = () => {
@@ -102,7 +132,6 @@ export default function (pi: ExtensionAPI) {
102
132
  durations.clear();
103
133
  };
104
134
  pi.on("session_start", clearTimings);
105
- pi.on("session_shutdown", clearTimings);
106
135
  pi.on("agent_end", clearTimings);
107
136
  pi.on("agent_settled", clearTimings);
108
137
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-tool-duration",
3
- "version": "0.2.2",
4
- "description": "Append model-visible durations to slow Pi tool results",
3
+ "version": "0.3.0",
4
+ "description": "Make host-observed Pi tool-call timing visible to the model",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
7
7
  "license": "MIT",
@@ -31,6 +31,7 @@
31
31
  ],
32
32
  "scripts": {
33
33
  "typecheck": "tsc --noEmit",
34
+ "check:compat": "npm run check",
34
35
  "test": "node --test test/*.test.mjs",
35
36
  "check": "npm run typecheck && npm test && npm pack --dry-run"
36
37
  },
@@ -40,12 +41,12 @@
40
41
  ]
41
42
  },
42
43
  "devDependencies": {
43
- "@earendil-works/pi-coding-agent": "0.84.0",
44
+ "@earendil-works/pi-coding-agent": "0.87.0",
44
45
  "typebox": "1.3.7",
45
46
  "typescript": "^5.9.3"
46
47
  },
47
48
  "peerDependencies": {
48
- "@earendil-works/pi-coding-agent": ">=0.84.0"
49
+ "@earendil-works/pi-coding-agent": "*"
49
50
  },
50
51
  "peerDependenciesMeta": {
51
52
  "@earendil-works/pi-coding-agent": {