pi-tool-duration 0.2.3 → 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 +18 -0
- package/README.md +39 -37
- package/extensions/tool-duration/index.ts +42 -19
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
3
21
|
## [0.2.3] - 2026-09-18
|
|
4
22
|
|
|
5
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,81 +1,83 @@
|
|
|
1
1
|
# pi-tool-duration
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
[
|
|
7
|
+
[host tool-call elapsed: 5.0s]
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
From this repo:
|
|
32
|
+
For local development:
|
|
43
33
|
|
|
44
34
|
```bash
|
|
45
|
-
pi
|
|
46
|
-
#
|
|
47
|
-
pi --no-extensions -e
|
|
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:
|
|
44
|
+
Default threshold: **0 ms**, including fast calls.
|
|
55
45
|
|
|
56
46
|
```bash
|
|
57
|
-
|
|
58
|
-
pi --no-extensions -e .
|
|
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
|
|
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
|
-
|
|
66
|
+
Ask Pi to run a tool, for example:
|
|
66
67
|
|
|
67
68
|
```text
|
|
68
|
-
Use bash to run: sleep
|
|
69
|
+
Use bash to run: sleep 1; echo hi
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
The model
|
|
72
|
+
The model receives the output plus a host timing marker. Pi's terminal retains its native rendering.
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
7
|
+
import type { ContextWithSystemEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
|
|
10
|
-
const DEFAULT_THRESHOLD_MS =
|
|
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,51 @@ function thresholdMs(pi: ExtensionAPI): number {
|
|
|
27
26
|
);
|
|
28
27
|
}
|
|
29
28
|
|
|
29
|
+
function timingKey(message: { timestamp: number; toolCallId: string }): string {
|
|
30
|
+
return `${message.timestamp}:${message.toolCallId}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
30
33
|
function withDurations(
|
|
31
|
-
messages:
|
|
34
|
+
messages: ContextWithSystemEvent["messages"],
|
|
32
35
|
ctx: ExtensionContext,
|
|
33
36
|
position: "append" | "prepend" = "append",
|
|
34
37
|
) {
|
|
38
|
+
const remaining = new Set(messages.filter((message) => message.role === "toolResult").map(timingKey));
|
|
39
|
+
if (!remaining.size) return messages;
|
|
40
|
+
|
|
35
41
|
const saved = new Map<string, string>();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|
|
45
69
|
}
|
|
46
70
|
|
|
47
71
|
return messages.map((message) => {
|
|
48
72
|
if (message.role !== "toolResult") return message;
|
|
49
|
-
const duration = saved.get(
|
|
73
|
+
const duration = saved.get(timingKey(message));
|
|
50
74
|
if (!duration) return message;
|
|
51
75
|
const marker = { type: "text" as const, text: duration };
|
|
52
76
|
return {
|
|
@@ -77,7 +101,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
77
101
|
|
|
78
102
|
const ms = performance.now() - startedAt;
|
|
79
103
|
if (!event.isError && ms < thresholdMs(pi)) return;
|
|
80
|
-
durations.set(event.toolCallId, `[
|
|
104
|
+
durations.set(event.toolCallId, `[host tool-call elapsed: ${(ms / 1000).toFixed(1)}s]`);
|
|
81
105
|
});
|
|
82
106
|
|
|
83
107
|
pi.on("message_end", (event) => {
|
|
@@ -94,7 +118,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
94
118
|
});
|
|
95
119
|
});
|
|
96
120
|
|
|
97
|
-
pi.on("
|
|
121
|
+
pi.on("context_with_system", (event, ctx) => ({ messages: withDurations(event.messages, ctx) }));
|
|
98
122
|
|
|
99
123
|
pi.on("session_before_compact", ({ preparation }, ctx) => {
|
|
100
124
|
// Native summarization bypasses context hooks and truncates tool text from the end.
|
|
@@ -108,7 +132,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
108
132
|
durations.clear();
|
|
109
133
|
};
|
|
110
134
|
pi.on("session_start", clearTimings);
|
|
111
|
-
pi.on("session_shutdown", clearTimings);
|
|
112
135
|
pi.on("agent_end", clearTimings);
|
|
113
136
|
pi.on("agent_settled", clearTimings);
|
|
114
137
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-tool-duration",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
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.
|
|
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": "
|
|
49
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
49
50
|
},
|
|
50
51
|
"peerDependenciesMeta": {
|
|
51
52
|
"@earendil-works/pi-coding-agent": {
|