pi-cliffcompaction 0.1.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 +13 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/config.example.json +12 -0
- package/index.ts +185 -0
- package/lib/cliff.ts +134 -0
- package/lib/config.ts +225 -0
- package/lib/decode.ts +103 -0
- package/lib/dialects/anthropic.ts +364 -0
- package/lib/dialects/base.ts +79 -0
- package/lib/dialects/index.ts +48 -0
- package/lib/dialects/openai-chat.ts +225 -0
- package/lib/dialects/openai-responses.ts +331 -0
- package/lib/dialects/pi.ts +249 -0
- package/lib/engine.ts +558 -0
- package/lib/hashing.ts +31 -0
- package/lib/images.ts +268 -0
- package/lib/index.ts +62 -0
- package/lib/json.ts +256 -0
- package/lib/pi-hook.ts +253 -0
- package/lib/store.ts +94 -0
- package/package.json +76 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dialect interface and shared summary helpers.
|
|
3
|
+
*
|
|
4
|
+
* The marker by which a previously injected summary is recognized (and
|
|
5
|
+
* dropped on re-compaction). Keep it stable: changing a byte breaks
|
|
6
|
+
* recognition of summaries produced by earlier versions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Config } from "../config.ts";
|
|
10
|
+
import type { JsonObject } from "../decode.ts";
|
|
11
|
+
|
|
12
|
+
export const SUMMARY_HEADER =
|
|
13
|
+
"The following is a summary of your previous actions (long observations omitted):";
|
|
14
|
+
|
|
15
|
+
export const PI_COMPACTION_PREFIX =
|
|
16
|
+
"The conversation history before this point was compacted into the following summary:";
|
|
17
|
+
|
|
18
|
+
const TASK_NOTIFICATION_RE = /<task-notification>[\s\S]*?<\/task-notification>\s*/g;
|
|
19
|
+
|
|
20
|
+
export function stripTaskNotifications(text: string): string {
|
|
21
|
+
return text.replace(TASK_NOTIFICATION_RE, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Truncate with ellipsis; maxChars <= 0 means unlimited. */
|
|
25
|
+
export function truncate(text: string, maxChars: number): string {
|
|
26
|
+
const value = text || "";
|
|
27
|
+
|
|
28
|
+
if (maxChars && maxChars > 0 && value.length > maxChars) {
|
|
29
|
+
return value.slice(0, maxChars) + "...";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function startsWithSummaryHeader(text: string): boolean {
|
|
36
|
+
return text.startsWith(SUMMARY_HEADER) || text.startsWith(PI_COMPACTION_PREFIX);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type Dialect = {
|
|
40
|
+
name: string;
|
|
41
|
+
digestMessage: (msg: JsonObject) => string;
|
|
42
|
+
isAssistant: (msg: JsonObject) => boolean;
|
|
43
|
+
summarizeMessage: (msg: JsonObject, cfg: Config) => string[];
|
|
44
|
+
userMessage: (text: string) => JsonObject;
|
|
45
|
+
isSummaryMessage: (msg: JsonObject) => boolean;
|
|
46
|
+
sessionKey: (body: JsonObject) => string | null;
|
|
47
|
+
messagesKey: string;
|
|
48
|
+
groupTurns: ((body: JsonObject[]) => JsonObject[][]) | null;
|
|
49
|
+
trimFromHead: ((msg: JsonObject) => boolean) | null;
|
|
50
|
+
openingUserMessages: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function makeDialect(partial: {
|
|
54
|
+
name: string;
|
|
55
|
+
digestMessage: (msg: JsonObject) => string;
|
|
56
|
+
isAssistant: (msg: JsonObject) => boolean;
|
|
57
|
+
summarizeMessage: (msg: JsonObject, cfg: Config) => string[];
|
|
58
|
+
userMessage: (text: string) => JsonObject;
|
|
59
|
+
isSummaryMessage: (msg: JsonObject) => boolean;
|
|
60
|
+
sessionKey: (body: JsonObject) => string | null;
|
|
61
|
+
messagesKey?: string;
|
|
62
|
+
groupTurns?: ((body: JsonObject[]) => JsonObject[][]) | null;
|
|
63
|
+
trimFromHead?: ((msg: JsonObject) => boolean) | null;
|
|
64
|
+
openingUserMessages?: number;
|
|
65
|
+
}): Dialect {
|
|
66
|
+
return {
|
|
67
|
+
name: partial.name,
|
|
68
|
+
digestMessage: partial.digestMessage,
|
|
69
|
+
isAssistant: partial.isAssistant,
|
|
70
|
+
summarizeMessage: partial.summarizeMessage,
|
|
71
|
+
userMessage: partial.userMessage,
|
|
72
|
+
isSummaryMessage: partial.isSummaryMessage,
|
|
73
|
+
sessionKey: partial.sessionKey,
|
|
74
|
+
messagesKey: partial.messagesKey ?? "messages",
|
|
75
|
+
groupTurns: partial.groupTurns ?? null,
|
|
76
|
+
trimFromHead: partial.trimFromHead ?? null,
|
|
77
|
+
openingUserMessages: partial.openingUserMessages ?? 1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API dialects: Anthropic Messages, OpenAI Chat Completions, OpenAI
|
|
3
|
+
* Responses, and Pi agent messages.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DIALECT as anthropic } from "./anthropic.ts";
|
|
7
|
+
import { DIALECT as openaiChat } from "./openai-chat.ts";
|
|
8
|
+
import { DIALECT as openaiResponses } from "./openai-responses.ts";
|
|
9
|
+
import { DIALECT as pi } from "./pi.ts";
|
|
10
|
+
import type { Dialect } from "./base.ts";
|
|
11
|
+
|
|
12
|
+
export type { Dialect } from "./base.ts";
|
|
13
|
+
|
|
14
|
+
export { SUMMARY_HEADER, PI_COMPACTION_PREFIX, truncate, stripTaskNotifications } from "./base.ts";
|
|
15
|
+
|
|
16
|
+
export { DIALECT as anthropicDialect } from "./anthropic.ts";
|
|
17
|
+
|
|
18
|
+
export { DIALECT as openaiChatDialect } from "./openai-chat.ts";
|
|
19
|
+
|
|
20
|
+
export { DIALECT as openaiResponsesDialect } from "./openai-responses.ts";
|
|
21
|
+
|
|
22
|
+
export { DIALECT as piDialect } from "./pi.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pick the dialect for a request path, or null for raw passthrough.
|
|
26
|
+
* /v1/messages/count_tokens is deliberately not matched.
|
|
27
|
+
*/
|
|
28
|
+
export function detect(path: string): Dialect | null {
|
|
29
|
+
const p = path.replace(/\/+$/, "");
|
|
30
|
+
|
|
31
|
+
if (p.endsWith("/v1/messages") || p.endsWith("/messages")) {
|
|
32
|
+
return anthropic;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (p.endsWith("/chat/completions")) {
|
|
36
|
+
return openaiChat;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (p.endsWith("/responses")) {
|
|
40
|
+
return openaiResponses;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (p === "pi" || p.endsWith("/pi")) {
|
|
44
|
+
return pi;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Chat Completions dialect.
|
|
3
|
+
*
|
|
4
|
+
* Tool results have role "tool". Assistant tool calls live in tool_calls
|
|
5
|
+
* (function name + JSON-string arguments); assistant content is the
|
|
6
|
+
* visible thought.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Config } from "../config.ts";
|
|
10
|
+
import {
|
|
11
|
+
asArray,
|
|
12
|
+
asObject,
|
|
13
|
+
asString,
|
|
14
|
+
isArray,
|
|
15
|
+
isRecord,
|
|
16
|
+
isString,
|
|
17
|
+
type JsonArray,
|
|
18
|
+
type JsonObject,
|
|
19
|
+
type JsonValue,
|
|
20
|
+
} from "../decode.ts";
|
|
21
|
+
import { digestBytes, digestObj } from "../hashing.ts";
|
|
22
|
+
import { canonicalJson } from "../json.ts";
|
|
23
|
+
import {
|
|
24
|
+
makeDialect,
|
|
25
|
+
startsWithSummaryHeader,
|
|
26
|
+
stripTaskNotifications,
|
|
27
|
+
truncate,
|
|
28
|
+
type Dialect,
|
|
29
|
+
} from "./base.ts";
|
|
30
|
+
|
|
31
|
+
function canonPart(part: JsonObject): JsonArray {
|
|
32
|
+
const t = asString(part.type);
|
|
33
|
+
|
|
34
|
+
if (t === "text") {
|
|
35
|
+
return ["text", asString(part.text)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (t === "image_url") {
|
|
39
|
+
const url = asString(asObject(part.image_url)?.url);
|
|
40
|
+
|
|
41
|
+
return ["image", digestBytes(url)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return ["other", canonicalJson(part)];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function contentText(content: JsonValue | undefined): string {
|
|
48
|
+
if (isString(content)) {
|
|
49
|
+
return content;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (isArray(content)) {
|
|
53
|
+
const parts: string[] = [];
|
|
54
|
+
|
|
55
|
+
for (const p of content) {
|
|
56
|
+
if (isRecord(p) && asString(p.type) === "text") {
|
|
57
|
+
parts.push(asString(p.text));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return parts.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (content === undefined || content === null) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return String(content);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function digestMessage(msg: JsonObject): string {
|
|
72
|
+
const content = msg.content;
|
|
73
|
+
let blocks: JsonArray = [];
|
|
74
|
+
|
|
75
|
+
if (isString(content)) {
|
|
76
|
+
blocks = [["text", content]];
|
|
77
|
+
} else if (isArray(content)) {
|
|
78
|
+
const out: JsonArray = [];
|
|
79
|
+
|
|
80
|
+
for (const p of content) {
|
|
81
|
+
if (isRecord(p)) {
|
|
82
|
+
out.push(canonPart(p));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
blocks = out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const toolCalls: JsonArray = [];
|
|
90
|
+
|
|
91
|
+
for (const tc of asArray(msg.tool_calls)) {
|
|
92
|
+
if (!isRecord(tc)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fn = asObject(tc.function) ?? {};
|
|
97
|
+
toolCalls.push([asString(tc.id), asString(fn.name), asString(fn.arguments)]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return digestObj([
|
|
101
|
+
asString(msg.role),
|
|
102
|
+
blocks,
|
|
103
|
+
toolCalls,
|
|
104
|
+
asString(msg.tool_call_id),
|
|
105
|
+
asString(msg.name),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isAssistant(msg: JsonObject): boolean {
|
|
110
|
+
return asString(msg.role) === "assistant";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function isSummaryMessage(msg: JsonObject): boolean {
|
|
114
|
+
return asString(msg.role) === "user" && startsWithSummaryHeader(contentText(msg.content));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function sessionKey(_body: JsonObject): string | null {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function summarizeAssistant(msg: JsonObject, cfg: Config): string[] {
|
|
122
|
+
let thinking = "";
|
|
123
|
+
|
|
124
|
+
if (cfg.keepThinking) {
|
|
125
|
+
const raw = msg.reasoning_content ?? msg.reasoning;
|
|
126
|
+
|
|
127
|
+
if (isString(raw)) {
|
|
128
|
+
thinking = truncate(raw.trim(), cfg.thinkingMaxChars);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const thought = truncate(contentText(msg.content).trim(), cfg.thoughtMaxChars);
|
|
133
|
+
const sigs: string[] = [];
|
|
134
|
+
|
|
135
|
+
for (const tc of asArray(msg.tool_calls)) {
|
|
136
|
+
if (!isRecord(tc)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fn = asObject(tc.function) ?? {};
|
|
141
|
+
let args = fn.arguments;
|
|
142
|
+
|
|
143
|
+
if (!isString(args)) {
|
|
144
|
+
args = canonicalJson(args ?? "");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
sigs.push("[" + asString(fn.name, "?") + "] " + truncate(args, cfg.cmdMaxChars));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const lines: string[] = [];
|
|
151
|
+
|
|
152
|
+
if (thinking) {
|
|
153
|
+
lines.push("thinking: " + thinking);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (thought) {
|
|
157
|
+
lines.push("assistant: " + thought);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (sigs.length > 0) {
|
|
161
|
+
lines.push(sigs.join("\n"));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return lines.length > 0 ? [lines.join("\n")] : [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function summarizeMessage(msg: JsonObject, cfg: Config): string[] {
|
|
168
|
+
const role = asString(msg.role);
|
|
169
|
+
|
|
170
|
+
if (role === "assistant") {
|
|
171
|
+
return summarizeAssistant(msg, cfg);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (role === "tool") {
|
|
175
|
+
const text = contentText(msg.content).trim();
|
|
176
|
+
|
|
177
|
+
if (text && text.length <= cfg.resultMaxChars) {
|
|
178
|
+
return ["result: " + text];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (role === "user") {
|
|
185
|
+
let text = contentText(msg.content).trim();
|
|
186
|
+
|
|
187
|
+
if (!text || startsWithSummaryHeader(text)) {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
text = stripTaskNotifications(text).trim();
|
|
192
|
+
|
|
193
|
+
if (!text) {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return ["user: " + truncate(text, cfg.humanMaxChars)];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (role === "system" || role === "developer") {
|
|
201
|
+
const text = contentText(msg.content).trim();
|
|
202
|
+
|
|
203
|
+
if (!text) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [role + ": " + truncate(text, cfg.humanMaxChars)];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function userMessage(text: string): JsonObject {
|
|
214
|
+
return { role: "user", content: text };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const DIALECT: Dialect = makeDialect({
|
|
218
|
+
name: "openai",
|
|
219
|
+
digestMessage,
|
|
220
|
+
isAssistant,
|
|
221
|
+
summarizeMessage,
|
|
222
|
+
userMessage,
|
|
223
|
+
isSummaryMessage,
|
|
224
|
+
sessionKey,
|
|
225
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Responses API dialect (stateless clients: full `input` resent).
|
|
3
|
+
*
|
|
4
|
+
* A model step spans MULTIPLE items: [reasoning?] then message/function_call,
|
|
5
|
+
* followed by function_call_output items. Turn grouping keeps contiguous
|
|
6
|
+
* model-output runs together.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Config } from "../config.ts";
|
|
10
|
+
import {
|
|
11
|
+
asArray,
|
|
12
|
+
asObject,
|
|
13
|
+
asString,
|
|
14
|
+
isArray,
|
|
15
|
+
isRecord,
|
|
16
|
+
isString,
|
|
17
|
+
type JsonArray,
|
|
18
|
+
type JsonObject,
|
|
19
|
+
type JsonValue,
|
|
20
|
+
} from "../decode.ts";
|
|
21
|
+
import { digestObj } from "../hashing.ts";
|
|
22
|
+
import { canonicalJson } from "../json.ts";
|
|
23
|
+
import {
|
|
24
|
+
makeDialect,
|
|
25
|
+
startsWithSummaryHeader,
|
|
26
|
+
stripTaskNotifications,
|
|
27
|
+
truncate,
|
|
28
|
+
type Dialect,
|
|
29
|
+
} from "./base.ts";
|
|
30
|
+
|
|
31
|
+
const MODEL_ITEM_TYPES = new Set([
|
|
32
|
+
"reasoning",
|
|
33
|
+
"function_call",
|
|
34
|
+
"custom_tool_call",
|
|
35
|
+
"local_shell_call",
|
|
36
|
+
"web_search_call",
|
|
37
|
+
"tool_search_call",
|
|
38
|
+
"image_generation_call",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function itemType(item: JsonObject): string {
|
|
42
|
+
const t = asString(item.type);
|
|
43
|
+
|
|
44
|
+
if (t) {
|
|
45
|
+
return t;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Object.prototype.hasOwnProperty.call(item, "role") ? "message" : "other";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isModelOutput(item: JsonObject): boolean {
|
|
52
|
+
const t = itemType(item);
|
|
53
|
+
|
|
54
|
+
if (t === "message") {
|
|
55
|
+
return asString(item.role) === "assistant";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return MODEL_ITEM_TYPES.has(t);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function contentText(content: JsonValue | undefined): string {
|
|
62
|
+
if (isString(content)) {
|
|
63
|
+
return content;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (isArray(content)) {
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
|
|
69
|
+
for (const p of content) {
|
|
70
|
+
if (!isRecord(p)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const t = asString(p.type);
|
|
75
|
+
|
|
76
|
+
if (t === "input_text" || t === "output_text" || t === "text") {
|
|
77
|
+
parts.push(asString(p.text));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return parts.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (content === undefined || content === null) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return String(content);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function canonContent(content: JsonValue | undefined): JsonArray {
|
|
92
|
+
if (isString(content)) {
|
|
93
|
+
return [["text", content]];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isArray(content)) {
|
|
97
|
+
const out: JsonArray = [];
|
|
98
|
+
|
|
99
|
+
for (const p of content) {
|
|
100
|
+
if (!isRecord(p)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const t = asString(p.type);
|
|
105
|
+
|
|
106
|
+
if (t === "input_text" || t === "output_text" || t === "text") {
|
|
107
|
+
out.push(["text", asString(p.text)]);
|
|
108
|
+
} else {
|
|
109
|
+
out.push(["other", canonicalJson(p)]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function digestMessage(item: JsonObject): string {
|
|
120
|
+
const t = itemType(item);
|
|
121
|
+
|
|
122
|
+
if (t === "message") {
|
|
123
|
+
return digestObj(["message", asString(item.role), canonContent(item.content)]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (t === "reasoning") {
|
|
127
|
+
return digestObj([
|
|
128
|
+
"reasoning",
|
|
129
|
+
asString(item.encrypted_content),
|
|
130
|
+
canonicalJson(item.summary ?? []),
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (t === "function_call_output") {
|
|
135
|
+
let out = item.output;
|
|
136
|
+
|
|
137
|
+
if (!isString(out)) {
|
|
138
|
+
out = canonicalJson(out ?? null);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return digestObj(["function_call_output", asString(item.call_id), out]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (t.endsWith("_call") || t === "function_call") {
|
|
145
|
+
let args = item.arguments;
|
|
146
|
+
|
|
147
|
+
if (!isString(args)) {
|
|
148
|
+
args = canonicalJson(args ?? "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return digestObj([t, asString(item.call_id), asString(item.name), args]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const stripped: JsonObject = {};
|
|
155
|
+
|
|
156
|
+
for (const key of Object.keys(item)) {
|
|
157
|
+
if (key !== "id" && key !== "status") {
|
|
158
|
+
stripped[key] = item[key];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return digestObj(["other", canonicalJson(stripped)]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isAssistant(item: JsonObject): boolean {
|
|
166
|
+
return isModelOutput(item);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isSummaryMessage(item: JsonObject): boolean {
|
|
170
|
+
return (
|
|
171
|
+
itemType(item) === "message" &&
|
|
172
|
+
asString(item.role) === "user" &&
|
|
173
|
+
startsWithSummaryHeader(contentText(item.content))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function groupTurns(items: JsonObject[]): JsonObject[][] {
|
|
178
|
+
const turns: JsonObject[][] = [];
|
|
179
|
+
let current: JsonObject[] | null = null;
|
|
180
|
+
let prevModel = false;
|
|
181
|
+
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
const model = isModelOutput(item);
|
|
184
|
+
|
|
185
|
+
if (model && !prevModel) {
|
|
186
|
+
if (current !== null) {
|
|
187
|
+
turns.push(current);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
current = [item];
|
|
191
|
+
} else if (current === null) {
|
|
192
|
+
current = [item];
|
|
193
|
+
} else {
|
|
194
|
+
current.push(item);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
prevModel = model;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (current !== null) {
|
|
201
|
+
turns.push(current);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return turns;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function sessionKey(body: JsonObject): string | null {
|
|
208
|
+
const key = body.prompt_cache_key;
|
|
209
|
+
|
|
210
|
+
if (isString(key) && key) {
|
|
211
|
+
return key;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const meta = asObject(body.client_metadata);
|
|
215
|
+
|
|
216
|
+
if (meta !== null) {
|
|
217
|
+
const tid = meta.thread_id;
|
|
218
|
+
|
|
219
|
+
if (isString(tid) && tid) {
|
|
220
|
+
return tid;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function summarizeMessage(item: JsonObject, cfg: Config): string[] {
|
|
228
|
+
const t = itemType(item);
|
|
229
|
+
|
|
230
|
+
if (t === "message") {
|
|
231
|
+
const role = asString(item.role);
|
|
232
|
+
let text = contentText(item.content).trim();
|
|
233
|
+
|
|
234
|
+
if (!text) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (role === "assistant") {
|
|
239
|
+
return ["assistant: " + truncate(text, cfg.thoughtMaxChars)];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (startsWithSummaryHeader(text)) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (role === "user") {
|
|
247
|
+
text = stripTaskNotifications(text).trim();
|
|
248
|
+
|
|
249
|
+
if (!text) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return ["user: " + truncate(text, cfg.humanMaxChars)];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [role + ": " + truncate(text, cfg.humanMaxChars)];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (t === "reasoning") {
|
|
260
|
+
if (!cfg.keepThinking) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const bits: string[] = [];
|
|
265
|
+
|
|
266
|
+
for (const p of asArray(item.summary)) {
|
|
267
|
+
if (isRecord(p)) {
|
|
268
|
+
bits.push(asString(p.text));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const text = bits.join("\n").trim();
|
|
273
|
+
|
|
274
|
+
if (!text) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return ["thinking: " + truncate(text, cfg.thinkingMaxChars)];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (t === "function_call_output") {
|
|
282
|
+
let out = item.output;
|
|
283
|
+
|
|
284
|
+
if (!isString(out)) {
|
|
285
|
+
out = contentText(out) || canonicalJson(out ?? null);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
out = out.trim();
|
|
289
|
+
|
|
290
|
+
if (out && out.length <= cfg.resultMaxChars) {
|
|
291
|
+
return ["result: " + out];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (t.endsWith("_call")) {
|
|
298
|
+
let args = item.arguments;
|
|
299
|
+
|
|
300
|
+
if (!isString(args)) {
|
|
301
|
+
args = canonicalJson(args ?? "");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const name = asString(item.name) || t;
|
|
305
|
+
|
|
306
|
+
return ["[" + name + "] " + truncate(args, cfg.cmdMaxChars)];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function userMessage(text: string): JsonObject {
|
|
313
|
+
return {
|
|
314
|
+
type: "message",
|
|
315
|
+
role: "user",
|
|
316
|
+
content: [{ type: "input_text", text }],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export const DIALECT: Dialect = makeDialect({
|
|
321
|
+
name: "openai-responses",
|
|
322
|
+
digestMessage,
|
|
323
|
+
isAssistant,
|
|
324
|
+
summarizeMessage,
|
|
325
|
+
userMessage,
|
|
326
|
+
isSummaryMessage,
|
|
327
|
+
sessionKey,
|
|
328
|
+
openingUserMessages: 2,
|
|
329
|
+
messagesKey: "input",
|
|
330
|
+
groupTurns,
|
|
331
|
+
});
|