jeopi-cli 16.5.5 → 16.6.1
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 +19 -0
- package/README.md +80 -0
- package/dist/cli.js +3897 -4058
- package/dist/types/cli/messenger-cli.d.ts +10 -0
- package/dist/types/commands/messenger.d.ts +18 -0
- package/dist/types/messenger/config.d.ts +4 -0
- package/dist/types/messenger/discord.d.ts +10 -0
- package/dist/types/messenger/gateway.d.ts +9 -0
- package/dist/types/messenger/http.d.ts +9 -0
- package/dist/types/messenger/session.d.ts +17 -0
- package/dist/types/messenger/slack.d.ts +2 -0
- package/dist/types/messenger/telegram.d.ts +2 -0
- package/dist/types/messenger/types.d.ts +38 -0
- package/dist/types/tools/builtin-names.d.ts +2 -1
- package/package.json +12 -12
- package/src/cli/messenger-cli.ts +78 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/messenger.ts +30 -0
- package/src/config/model-registry.ts +110 -26
- package/src/messenger/config.ts +111 -0
- package/src/messenger/discord.ts +382 -0
- package/src/messenger/gateway.ts +153 -0
- package/src/messenger/http.ts +87 -0
- package/src/messenger/session.ts +219 -0
- package/src/messenger/slack.ts +333 -0
- package/src/messenger/telegram.ts +244 -0
- package/src/messenger/types.ts +47 -0
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/controllers/streaming-reveal.ts +21 -2
- package/src/session/session-persistence.ts +22 -4
- package/src/tools/builtin-names.ts +11 -2
- package/src/tools/index.ts +2 -2
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { logger } from "jeopi-utils";
|
|
2
|
+
import { MessengerHttpError, messengerFetchJson, splitMessengerText, waitForMessengerRetry } from "./http";
|
|
3
|
+
import type { MessengerAdapter, MessengerMessage, MessengerPlatformConfig, MessengerTransportDeps } from "./types";
|
|
4
|
+
|
|
5
|
+
interface TelegramIdentity {
|
|
6
|
+
id: number;
|
|
7
|
+
username: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class TelegramApiError extends Error {
|
|
11
|
+
constructor(
|
|
12
|
+
readonly status: number,
|
|
13
|
+
readonly retryAfterMs = 0,
|
|
14
|
+
) {
|
|
15
|
+
super("Telegram API request failed");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isInteger(value: unknown): value is number {
|
|
24
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPositiveId(value: unknown): value is number {
|
|
28
|
+
return isInteger(value) && value > 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseIdentity(value: unknown): TelegramIdentity {
|
|
32
|
+
if (
|
|
33
|
+
!isRecord(value) ||
|
|
34
|
+
!isPositiveId(value.id) ||
|
|
35
|
+
value.is_bot !== true ||
|
|
36
|
+
typeof value.username !== "string" ||
|
|
37
|
+
!/^[a-zA-Z0-9_]{1,32}$/.test(value.username)
|
|
38
|
+
) {
|
|
39
|
+
throw new TelegramApiError(400);
|
|
40
|
+
}
|
|
41
|
+
return { id: value.id, username: value.username };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stripMention(
|
|
45
|
+
text: string,
|
|
46
|
+
entities: unknown,
|
|
47
|
+
identity: TelegramIdentity,
|
|
48
|
+
): { text: string; mentioned: boolean } {
|
|
49
|
+
const mention = `@${identity.username.toLowerCase()}`;
|
|
50
|
+
const ranges: { start: number; end: number }[] = [];
|
|
51
|
+
if (Array.isArray(entities)) {
|
|
52
|
+
for (const entity of entities) {
|
|
53
|
+
if (
|
|
54
|
+
!isRecord(entity) ||
|
|
55
|
+
!isInteger(entity.offset) ||
|
|
56
|
+
!isPositiveId(entity.length) ||
|
|
57
|
+
entity.offset < 0 ||
|
|
58
|
+
entity.offset + entity.length > text.length
|
|
59
|
+
) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const start = entity.offset;
|
|
63
|
+
const end = start + entity.length;
|
|
64
|
+
const value = text.slice(start, end).toLowerCase();
|
|
65
|
+
if (
|
|
66
|
+
(entity.type === "mention" && value === mention) ||
|
|
67
|
+
(entity.type === "text_mention" && isRecord(entity.user) && entity.user.id === identity.id)
|
|
68
|
+
) {
|
|
69
|
+
ranges.push({ start, end });
|
|
70
|
+
} else if (
|
|
71
|
+
entity.type === "bot_command" &&
|
|
72
|
+
/^\/[a-z0-9_]+@[a-z0-9_]+$/.test(value) &&
|
|
73
|
+
value.endsWith(mention)
|
|
74
|
+
) {
|
|
75
|
+
ranges.push({ start: end - mention.length, end });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else if (entities === undefined) {
|
|
79
|
+
// Username is validated before interpolation; reject email/name substrings.
|
|
80
|
+
const pattern = new RegExp(`(^|[^\\p{L}\\p{N}_@])@${identity.username}(?![\\p{L}\\p{N}_])`, "giu");
|
|
81
|
+
for (const match of text.matchAll(pattern)) {
|
|
82
|
+
const start = match.index + match[1].length;
|
|
83
|
+
ranges.push({ start, end: start + mention.length });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (ranges.length === 0) return { text, mentioned: false };
|
|
87
|
+
ranges.sort((left, right) => left.start - right.start);
|
|
88
|
+
let cursor = 0;
|
|
89
|
+
let stripped = "";
|
|
90
|
+
for (const range of ranges) {
|
|
91
|
+
if (range.start < cursor) continue;
|
|
92
|
+
stripped += text.slice(cursor, range.start);
|
|
93
|
+
cursor = range.end;
|
|
94
|
+
}
|
|
95
|
+
return { text: (stripped + text.slice(cursor)).trim(), mentioned: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseMessage(value: unknown, identity: TelegramIdentity): MessengerMessage | undefined {
|
|
99
|
+
if (
|
|
100
|
+
!isRecord(value) ||
|
|
101
|
+
!isPositiveId(value.message_id) ||
|
|
102
|
+
typeof value.text !== "string" ||
|
|
103
|
+
!isRecord(value.from) ||
|
|
104
|
+
!isPositiveId(value.from.id) ||
|
|
105
|
+
value.from.is_bot !== false ||
|
|
106
|
+
!isRecord(value.chat) ||
|
|
107
|
+
!isInteger(value.chat.id) ||
|
|
108
|
+
value.chat.id === 0 ||
|
|
109
|
+
(value.chat.type !== "private" && value.chat.type !== "group" && value.chat.type !== "supergroup") ||
|
|
110
|
+
(value.message_thread_id !== undefined && !isPositiveId(value.message_thread_id)) ||
|
|
111
|
+
value.edit_date !== undefined ||
|
|
112
|
+
value.sender_chat !== undefined
|
|
113
|
+
) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const content = stripMention(value.text, value.entities, identity);
|
|
117
|
+
const direct = value.chat.type === "private";
|
|
118
|
+
if (!content.text.trim()) return undefined;
|
|
119
|
+
return {
|
|
120
|
+
platform: "telegram",
|
|
121
|
+
accountId: String(identity.id),
|
|
122
|
+
channelId: String(value.chat.id),
|
|
123
|
+
senderId: String(value.from.id),
|
|
124
|
+
messageId: String(value.message_id),
|
|
125
|
+
text: content.text,
|
|
126
|
+
...(value.message_thread_id === undefined ? {} : { threadId: String(value.message_thread_id) }),
|
|
127
|
+
direct,
|
|
128
|
+
mentioned: content.mentioned,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isPermanent(error: unknown): boolean {
|
|
133
|
+
if (!(error instanceof TelegramApiError || error instanceof MessengerHttpError)) return false;
|
|
134
|
+
return error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createTelegramAdapter(
|
|
138
|
+
config: MessengerPlatformConfig,
|
|
139
|
+
env: Record<string, string | undefined> = process.env,
|
|
140
|
+
deps: MessengerTransportDeps = {},
|
|
141
|
+
): MessengerAdapter {
|
|
142
|
+
const token = env[config.tokenEnv];
|
|
143
|
+
if (!token?.trim()) throw new Error("Telegram bot token is missing");
|
|
144
|
+
const baseUrl = `https://api.telegram.org/bot${encodeURIComponent(token).replaceAll("%3A", ":")}/`;
|
|
145
|
+
let running = false;
|
|
146
|
+
let offset = 0;
|
|
147
|
+
|
|
148
|
+
async function request(method: string, parameters: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
|
149
|
+
const response = await messengerFetchJson(
|
|
150
|
+
`${baseUrl}${method}`,
|
|
151
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(parameters) },
|
|
152
|
+
signal,
|
|
153
|
+
deps.fetch,
|
|
154
|
+
);
|
|
155
|
+
if (!isRecord(response) || typeof response.ok !== "boolean") throw new TelegramApiError(400);
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
const status = isInteger(response.error_code) ? response.error_code : 400;
|
|
158
|
+
const retryAfter = isRecord(response.parameters) ? response.parameters.retry_after : undefined;
|
|
159
|
+
throw new TelegramApiError(status, isPositiveId(retryAfter) ? Math.min(retryAfter, 300) * 1_000 : 0);
|
|
160
|
+
}
|
|
161
|
+
if (!("result" in response)) throw new TelegramApiError(400);
|
|
162
|
+
return response.result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
platform: "telegram",
|
|
167
|
+
async start(onMessage, signal) {
|
|
168
|
+
if (signal.aborted) return;
|
|
169
|
+
if (running) throw new Error("Telegram adapter is already running");
|
|
170
|
+
running = true;
|
|
171
|
+
const stopped = Promise.withResolvers<void>();
|
|
172
|
+
const onAbort = () => stopped.resolve();
|
|
173
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
174
|
+
let identity: TelegramIdentity | undefined;
|
|
175
|
+
let retryMs = 1_000;
|
|
176
|
+
try {
|
|
177
|
+
while (!signal.aborted) {
|
|
178
|
+
try {
|
|
179
|
+
identity ??= parseIdentity(await request("getMe", {}, signal));
|
|
180
|
+
if (signal.aborted) break;
|
|
181
|
+
const updates = await request(
|
|
182
|
+
"getUpdates",
|
|
183
|
+
{ offset, timeout: 25, allowed_updates: ["message"] },
|
|
184
|
+
signal,
|
|
185
|
+
);
|
|
186
|
+
if (!Array.isArray(updates)) throw new TelegramApiError(400);
|
|
187
|
+
retryMs = 1_000;
|
|
188
|
+
for (const update of updates) {
|
|
189
|
+
if (signal.aborted) break;
|
|
190
|
+
if (
|
|
191
|
+
!isRecord(update) ||
|
|
192
|
+
!isInteger(update.update_id) ||
|
|
193
|
+
update.update_id < 0 ||
|
|
194
|
+
update.update_id === Number.MAX_SAFE_INTEGER
|
|
195
|
+
) {
|
|
196
|
+
throw new TelegramApiError(400);
|
|
197
|
+
}
|
|
198
|
+
if (update.update_id < offset) continue;
|
|
199
|
+
// Acknowledge even ignored/failed callbacks: replaying can duplicate replies.
|
|
200
|
+
offset = update.update_id + 1;
|
|
201
|
+
const message = parseMessage(update.message, identity);
|
|
202
|
+
if (!message) continue;
|
|
203
|
+
const processing = Promise.resolve()
|
|
204
|
+
.then(() => onMessage(message))
|
|
205
|
+
.catch(() => {
|
|
206
|
+
logger.warn("Telegram message handler failed");
|
|
207
|
+
});
|
|
208
|
+
await Promise.race([processing, stopped.promise]);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (signal.aborted) break;
|
|
212
|
+
if (isPermanent(error)) throw new Error("Telegram authentication or polling configuration failed");
|
|
213
|
+
const delay =
|
|
214
|
+
error instanceof TelegramApiError || error instanceof MessengerHttpError
|
|
215
|
+
? Math.max(retryMs, error.retryAfterMs ?? 0)
|
|
216
|
+
: retryMs;
|
|
217
|
+
await waitForMessengerRetry(delay, signal);
|
|
218
|
+
retryMs = Math.min(retryMs * 2, 30_000);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
signal.removeEventListener("abort", onAbort);
|
|
223
|
+
running = false;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async send(message, text, signal) {
|
|
227
|
+
const threadId = message.threadId === undefined ? undefined : Number(message.threadId);
|
|
228
|
+
if (threadId !== undefined && !isPositiveId(threadId)) throw new Error("Telegram topic identifier is invalid");
|
|
229
|
+
for (const chunk of splitMessengerText(text, 4_096)) {
|
|
230
|
+
// Never retry an ambiguous send failure: Telegram may already have delivered it.
|
|
231
|
+
const result = await request(
|
|
232
|
+
"sendMessage",
|
|
233
|
+
{
|
|
234
|
+
chat_id: message.channelId,
|
|
235
|
+
text: chunk,
|
|
236
|
+
...(threadId === undefined ? {} : { message_thread_id: threadId }),
|
|
237
|
+
},
|
|
238
|
+
signal,
|
|
239
|
+
);
|
|
240
|
+
if (!isRecord(result) || !isPositiveId(result.message_id)) throw new TelegramApiError(400);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type MessengerPlatform = "discord" | "telegram" | "slack";
|
|
2
|
+
|
|
3
|
+
export interface MessengerMessage {
|
|
4
|
+
platform: MessengerPlatform;
|
|
5
|
+
accountId: string;
|
|
6
|
+
channelId: string;
|
|
7
|
+
senderId: string;
|
|
8
|
+
messageId: string;
|
|
9
|
+
text: string;
|
|
10
|
+
threadId?: string;
|
|
11
|
+
direct: boolean;
|
|
12
|
+
mentioned: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MessengerAdapter {
|
|
16
|
+
platform: MessengerPlatform;
|
|
17
|
+
start(onMessage: (message: MessengerMessage) => Promise<void>, signal: AbortSignal): Promise<void>;
|
|
18
|
+
send(message: MessengerMessage, text: string, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MessengerPlatformConfig {
|
|
22
|
+
tokenEnv: string;
|
|
23
|
+
appTokenEnv?: string;
|
|
24
|
+
allowedUserIds: string[];
|
|
25
|
+
allowedChannelIds?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MessengerConfig {
|
|
29
|
+
cwd: string;
|
|
30
|
+
sessionDir: string;
|
|
31
|
+
toolNames: string[];
|
|
32
|
+
platforms: Partial<Record<MessengerPlatform, MessengerPlatformConfig>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MessengerTransportDeps {
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
WebSocket?: typeof WebSocket;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type MessengerReply = (message: MessengerMessage, signal: AbortSignal) => Promise<string>;
|
|
41
|
+
/** A partially initialized SDK must not be retried inside a live gateway. */
|
|
42
|
+
export class MessengerFatalError extends Error {
|
|
43
|
+
constructor() {
|
|
44
|
+
super("Messenger session initialization failed; restart required");
|
|
45
|
+
this.name = "MessengerFatalError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -726,7 +726,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
726
726
|
);
|
|
727
727
|
|
|
728
728
|
// Fast path: reuse Markdown children when shape is stable during streaming
|
|
729
|
-
if (this.#tryFastPathUpdate(message)) return;
|
|
729
|
+
if (this.#tryFastPathUpdate(message, opts)) return;
|
|
730
730
|
|
|
731
731
|
// Clear content container
|
|
732
732
|
this.#contentContainer.clear();
|
|
@@ -215,6 +215,7 @@ export class StreamingRevealController {
|
|
|
215
215
|
#component: StreamingRevealComponent | undefined;
|
|
216
216
|
#timer: NodeJS.Timeout | undefined;
|
|
217
217
|
#revealed = 0;
|
|
218
|
+
#targetDirty = false;
|
|
218
219
|
#hideThinkingBlock = false;
|
|
219
220
|
#proseOnlyThinking = true;
|
|
220
221
|
#smoothStreaming = true;
|
|
@@ -275,6 +276,9 @@ export class StreamingRevealController {
|
|
|
275
276
|
if (!this.#component) return;
|
|
276
277
|
if (!this.#smoothStreaming) {
|
|
277
278
|
const total = this.#visibleUnits(message);
|
|
279
|
+
this.#revealed = total;
|
|
280
|
+
this.#targetDirty = false;
|
|
281
|
+
this.#stopTimer();
|
|
278
282
|
this.#component.updateContent(this.#build(message, total), { transient: true });
|
|
279
283
|
return;
|
|
280
284
|
}
|
|
@@ -283,6 +287,7 @@ export class StreamingRevealController {
|
|
|
283
287
|
// A tool call is a transcript-order boundary: finish any leading
|
|
284
288
|
// assistant text before EventController renders the separate tool card.
|
|
285
289
|
this.#revealed = total;
|
|
290
|
+
this.#targetDirty = false;
|
|
286
291
|
this.#stopTimer();
|
|
287
292
|
this.#component.updateContent(this.#build(message, this.#revealed), {
|
|
288
293
|
transient: true,
|
|
@@ -292,8 +297,15 @@ export class StreamingRevealController {
|
|
|
292
297
|
if (this.#revealed > total) {
|
|
293
298
|
this.#revealed = total;
|
|
294
299
|
}
|
|
295
|
-
this.#
|
|
296
|
-
|
|
300
|
+
if (this.#revealed < total) {
|
|
301
|
+
// ponytail: reuse the reveal timer; only add another scheduler if streaming needs a separate cadence.
|
|
302
|
+
this.#targetDirty = false;
|
|
303
|
+
this.#syncTimer(total);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Caught-up rewrites still need one render of the newest target at the next tick.
|
|
307
|
+
this.#startTimer();
|
|
308
|
+
this.#targetDirty = true;
|
|
297
309
|
}
|
|
298
310
|
|
|
299
311
|
stop(): void {
|
|
@@ -301,6 +313,7 @@ export class StreamingRevealController {
|
|
|
301
313
|
this.#target = undefined;
|
|
302
314
|
this.#component = undefined;
|
|
303
315
|
this.#revealed = 0;
|
|
316
|
+
this.#targetDirty = false;
|
|
304
317
|
this.#unitCounter.reset();
|
|
305
318
|
}
|
|
306
319
|
|
|
@@ -377,6 +390,12 @@ export class StreamingRevealController {
|
|
|
377
390
|
}
|
|
378
391
|
const total = this.#visibleUnits(target);
|
|
379
392
|
if (this.#revealed >= total) {
|
|
393
|
+
if (this.#targetDirty) {
|
|
394
|
+
this.#targetDirty = false;
|
|
395
|
+
this.#revealed = total;
|
|
396
|
+
this.#renderCurrent();
|
|
397
|
+
this.#requestRender();
|
|
398
|
+
}
|
|
380
399
|
this.#stopTimer();
|
|
381
400
|
return;
|
|
382
401
|
}
|
|
@@ -59,9 +59,14 @@ function shouldExternalizeImagePayload(
|
|
|
59
59
|
return (key === TEXT_CONTENT_KEY && isImageBlock(value)) || key === "images";
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Signature and encrypted fields bind their carrier to its exact bytes. */
|
|
63
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
64
|
+
return typeof value === "string" && value.length > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
/**
|
|
63
68
|
* Recursively truncate large strings in an object for session persistence.
|
|
64
|
-
* - Truncates
|
|
69
|
+
* - Truncates oversized strings except signed/encrypted blocks and signature keys
|
|
65
70
|
* - Externalizes oversized image payloads to blob refs
|
|
66
71
|
* - Updates lineCount when content is truncated
|
|
67
72
|
* - Returns original object if no changes needed (structural sharing)
|
|
@@ -76,16 +81,29 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
|
|
|
76
81
|
if (shouldExternalizeImagePayload(obj, key)) {
|
|
77
82
|
return { ...obj, data: externalizeImageDataSync(blobStore, obj.data, obj.mimeType) };
|
|
78
83
|
}
|
|
84
|
+
// Signed content and encrypted reasoning must replay byte-for-byte. Keep the
|
|
85
|
+
// whole carrier, including paired text and tool arguments; unsigned content
|
|
86
|
+
// still follows the normal size and transient-field sanitization rules.
|
|
87
|
+
if (typeof obj === "object" && "type" in obj) {
|
|
88
|
+
const signed =
|
|
89
|
+
(obj.type === "thinking" && "thinkingSignature" in obj && isNonEmptyString(obj.thinkingSignature)) ||
|
|
90
|
+
(obj.type === "text" && "textSignature" in obj && isNonEmptyString(obj.textSignature)) ||
|
|
91
|
+
(obj.type === "toolCall" && "thoughtSignature" in obj && isNonEmptyString(obj.thoughtSignature));
|
|
92
|
+
const redacted = obj.type === "redactedThinking" && "data" in obj && isNonEmptyString(obj.data);
|
|
93
|
+
const encryptedReasoning =
|
|
94
|
+
obj.type === "reasoning" && "encrypted_content" in obj && isNonEmptyString(obj.encrypted_content);
|
|
95
|
+
if (signed || redacted || encryptedReasoning) return obj;
|
|
96
|
+
}
|
|
79
97
|
|
|
80
98
|
if (typeof obj === "string") {
|
|
81
99
|
if (key === "image_url" && isImageDataUrl(obj)) {
|
|
82
100
|
return externalizeImageDataUrlSync(blobStore, obj);
|
|
83
101
|
}
|
|
84
102
|
if (obj.length > MAX_PERSIST_CHARS) {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
103
|
+
// Unknown carrier shapes must retain opaque signatures too: clearing or
|
|
104
|
+
// truncating one loses provider context or makes replay invalid.
|
|
87
105
|
if (key === "thinkingSignature" || key === "thoughtSignature" || key === "textSignature") {
|
|
88
|
-
return
|
|
106
|
+
return obj;
|
|
89
107
|
}
|
|
90
108
|
const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
|
|
91
109
|
return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}`;
|
|
@@ -35,15 +35,24 @@ export const BUILTIN_TOOL_NAMES = [
|
|
|
35
35
|
|
|
36
36
|
export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
|
|
37
37
|
|
|
38
|
+
export const HIDDEN_TOOL_NAMES = ["yield", "report_finding", "report_tool_issue", "resolve", "goal"] as const;
|
|
39
|
+
|
|
38
40
|
const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = new Map([
|
|
39
41
|
["search", "grep"],
|
|
40
42
|
["find", "glob"],
|
|
41
43
|
]);
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
const CANONICAL_TOOL_NAMES: Record<string, true> = Object.fromEntries(
|
|
46
|
+
[...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES].map(name => [name, true]),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
|
|
44
50
|
export function normalizeToolName(name: string): string {
|
|
45
51
|
const normalized = name.toLowerCase();
|
|
46
|
-
return
|
|
52
|
+
return (
|
|
53
|
+
LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(normalized) ??
|
|
54
|
+
(Object.hasOwn(CANONICAL_TOOL_NAMES, normalized) ? normalized : name)
|
|
55
|
+
);
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
/** Normalize and deduplicate tool names while preserving first-seen order. */
|
package/src/tools/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ import { AstEditTool } from "./ast-edit";
|
|
|
40
40
|
import { AstGrepTool } from "./ast-grep";
|
|
41
41
|
import { BashTool } from "./bash";
|
|
42
42
|
import { BrowserTool } from "./browser";
|
|
43
|
-
import { type BuiltinToolName, normalizeToolNames } from "./builtin-names";
|
|
43
|
+
import { type BuiltinToolName, type HIDDEN_TOOL_NAMES, normalizeToolNames } from "./builtin-names";
|
|
44
44
|
import { type CheckpointState, CheckpointTool, type CompletedRewindState, RewindTool } from "./checkpoint";
|
|
45
45
|
import { DebugTool } from "./debug";
|
|
46
46
|
import { EvalTool } from "./eval";
|
|
@@ -494,7 +494,7 @@ export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
|
494
494
|
report_tool_issue: s => createReportToolIssueTool(s),
|
|
495
495
|
resolve: s => new ResolveTool(s),
|
|
496
496
|
goal: s => new GoalTool(s),
|
|
497
|
-
}
|
|
497
|
+
} satisfies Record<(typeof HIDDEN_TOOL_NAMES)[number], ToolFactory>;
|
|
498
498
|
|
|
499
499
|
export type ToolName = BuiltinToolName;
|
|
500
500
|
|