@taylorsatula/pi-timestamp 0.2.1 → 0.2.2
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/README.md +7 -20
- package/extensions/timestamp.ts +19 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,20 +12,13 @@ They're injected poorly if they're that annoying.
|
|
|
12
12
|
[11:34pm - 9/1/26]
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Injection happens only at **turn boundaries**, never mid-tool-loop
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
to its content. Last message is an assistant message carrying `tool_calls`, a tool
|
|
23
|
-
result, or an Anthropic user message containing a `tool_result` block (that API's
|
|
24
|
-
tool-result encoding) → skip.
|
|
25
|
-
- **OpenAI Responses API** (conversation under `input`, typed items): last item is a
|
|
26
|
-
user item → append to its content blocks. Last item is an assistant `message` item
|
|
27
|
-
(vocalization) → append to its `output_text` blocks. Last item is a `function_call` or
|
|
28
|
-
`function_call_output` item → skip.
|
|
15
|
+
Injection happens only at **turn boundaries**, never mid-tool-loop:
|
|
16
|
+
|
|
17
|
+
- Last message is a **user message** → append the timestamp to its content.
|
|
18
|
+
- Last message is an **assistant message with text content** (a vocalization between tool
|
|
19
|
+
steps) → append to its content.
|
|
20
|
+
- Last message is an assistant message carrying `tool_calls`, or a tool result (mid-turn
|
|
21
|
+
churn) → skip.
|
|
29
22
|
|
|
30
23
|
## Why plain-text tail append (design notes)
|
|
31
24
|
|
|
@@ -42,9 +35,6 @@ differs per API):
|
|
|
42
35
|
- **Content-shape aware.** String content concatenates directly; block-array content
|
|
43
36
|
(Anthropic-shaped) appends into or after the last text block; unknown shapes are left
|
|
44
37
|
untouched rather than guessed at.
|
|
45
|
-
- **Wire-format aware.** Chat Completions/Anthropic payloads (`messages`) and OpenAI
|
|
46
|
-
Responses payloads (`input`, typed items) are both handled; any other payload shape
|
|
47
|
-
passes through untouched.
|
|
48
38
|
|
|
49
39
|
## Installation
|
|
50
40
|
|
|
@@ -71,9 +61,6 @@ Or add to `~/.pi/agent/settings.json` under `packages`:
|
|
|
71
61
|
- Version 0.2.0 changed the mechanism (plain-text tail append replacing the synthetic
|
|
72
62
|
tool-call pair). Sessions started before the upgrade may still show one old-style pair
|
|
73
63
|
until restart.
|
|
74
|
-
- Version 0.2.1 fixed a crash on OpenAI Responses-API models (`before_provider_request`
|
|
75
|
-
payloads carry the conversation under `input`, not `messages`) and added
|
|
76
|
-
Anthropic `tool_result` skipping.
|
|
77
64
|
|
|
78
65
|
## Development
|
|
79
66
|
|
package/extensions/timestamp.ts
CHANGED
|
@@ -14,15 +14,6 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
14
14
|
// never after tool results (mid-loop churn).
|
|
15
15
|
// - Idempotent per request: a previously appended tag is stripped before the
|
|
16
16
|
// fresh one is written.
|
|
17
|
-
//
|
|
18
|
-
// Payload shapes (before_provider_request delivers the provider-serialized
|
|
19
|
-
// request body, which differs per API):
|
|
20
|
-
// - Chat Completions / Anthropic: conversation lives under `messages`.
|
|
21
|
-
// - OpenAI Responses API: conversation lives under `input`, with typed items
|
|
22
|
-
// (input_text / output_text content blocks, function_call and
|
|
23
|
-
// function_call_output tool traffic) instead of chat messages.
|
|
24
|
-
|
|
25
|
-
type Rec = Record<string, unknown>;
|
|
26
17
|
|
|
27
18
|
const TS_STRIP = /\n\[\d{1,2}:\d{2}(am|pm) - \d{1,2}\/\d{1,2}\/\d{2}\]\s*$/;
|
|
28
19
|
|
|
@@ -39,73 +30,44 @@ function formatTimestamp(): string {
|
|
|
39
30
|
return `[${hours}:${String(minutes).padStart(2, "0")}${ampm} - ${month}/${day}/${year}]`;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
33
|
+
function injectable(msgs: Array<Record<string, unknown>> | undefined): boolean {
|
|
34
|
+
if (!Array.isArray(msgs) || msgs.length === 0) return false;
|
|
35
|
+
const last = msgs[msgs.length - 1];
|
|
36
|
+
if (!last) return false;
|
|
37
|
+
|
|
38
|
+
if (last.role === "user") return true;
|
|
48
39
|
|
|
49
|
-
function chatShouldInject(last: Rec): boolean {
|
|
50
|
-
if (last.role === "user") {
|
|
51
|
-
const content = last.content;
|
|
52
|
-
if (Array.isArray(content) && content.some((b) => (b as Rec).type === "tool_result")) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
40
|
if (last.role === "assistant") {
|
|
58
41
|
const hasToolCalls = Array.isArray(last.tool_calls) && last.tool_calls.length > 0;
|
|
59
42
|
const hasContent = last.content != null && last.content !== "";
|
|
60
43
|
return !hasToolCalls && hasContent; // vocalization → inject; tool call → never
|
|
61
44
|
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// OpenAI Responses shape: payload.input
|
|
66
|
-
// user: { role: "user", content: [{ type: "input_text", text }, ...] }
|
|
67
|
-
// vocalization: { type: "message", role: "assistant", content: [{ type: "output_text", ... }] }
|
|
68
|
-
// tool call: { type: "function_call", ... } / result: { type: "function_call_output", ... }
|
|
69
45
|
|
|
70
|
-
|
|
71
|
-
if (last.role === "user") return true;
|
|
72
|
-
if (last.type === "message" && last.role === "assistant") {
|
|
73
|
-
return Array.isArray(last.content) && last.content.length > 0;
|
|
74
|
-
}
|
|
75
|
-
return false; // function_call / function_call_output → mid-loop, skip
|
|
46
|
+
return false; // tool results → mid-loop, skip
|
|
76
47
|
}
|
|
77
48
|
|
|
78
49
|
export default function (pi: ExtensionAPI) {
|
|
79
50
|
pi.on("before_provider_request", (event) => {
|
|
80
|
-
const payload = event.payload as
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: undefined;
|
|
87
|
-
if (!items) return; // unknown payload shape → leave untouched
|
|
88
|
-
|
|
89
|
-
const last = items[items.length - 1];
|
|
90
|
-
if (!last) return;
|
|
91
|
-
if (!(isResponses ? responsesShouldInject(last) : chatShouldInject(last))) return;
|
|
51
|
+
const payload = event.payload as Record<string, unknown>;
|
|
52
|
+
// OpenAI Responses API carries the conversation in `input` instead of `messages`.
|
|
53
|
+
const key = Array.isArray(payload.messages) ? "messages" : Array.isArray(payload.input) ? "input" : null;
|
|
54
|
+
if (!key) return;
|
|
55
|
+
const msgs = payload[key] as Array<Record<string, unknown>>;
|
|
56
|
+
if (!injectable(msgs)) return;
|
|
92
57
|
|
|
93
58
|
const ts = `\n${formatTimestamp()}`;
|
|
59
|
+
const last = msgs[msgs.length - 1];
|
|
94
60
|
|
|
95
61
|
if (typeof last.content === "string") {
|
|
96
62
|
last.content = last.content.replace(TS_STRIP, "") + ts;
|
|
97
63
|
} else if (Array.isArray(last.content)) {
|
|
98
|
-
// Provider-shaped block arrays: normalize the last
|
|
99
|
-
// place, or append a
|
|
100
|
-
const blocks = [...(last.content as
|
|
64
|
+
// Provider-shaped block arrays (e.g. Anthropic): normalize the last
|
|
65
|
+
// text block in place, or append a text block when none exists.
|
|
66
|
+
const blocks = [...(last.content as Array<Record<string, unknown>>)];
|
|
101
67
|
const idx = blocks.length - 1;
|
|
102
|
-
const lastBlock = blocks[idx] as { text?:
|
|
103
|
-
if (lastBlock && typeof lastBlock.text === "string") {
|
|
68
|
+
const lastBlock = blocks[idx] as { type?: string; text?: string } | undefined;
|
|
69
|
+
if (lastBlock && lastBlock.type === "text" && typeof lastBlock.text === "string") {
|
|
104
70
|
blocks[idx] = { ...lastBlock, text: lastBlock.text.replace(TS_STRIP, "") + ts };
|
|
105
|
-
} else if (isResponses && last.role === "assistant") {
|
|
106
|
-
blocks.push({ type: "output_text", text: ts, annotations: [] });
|
|
107
|
-
} else if (isResponses) {
|
|
108
|
-
blocks.push({ type: "input_text", text: ts });
|
|
109
71
|
} else {
|
|
110
72
|
blocks.push({ type: "text", text: ts });
|
|
111
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taylorsatula/pi-timestamp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Timestamp injection extension for Pi — appends the current wall-clock time to the tail of user messages and vocalizations just-in-time (cache-safe, no synthetic tool calls), so the model always knows what time it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|