pi-openai-toolkit 0.5.0 → 0.7.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/README.md +22 -33
- package/README_zh.md +22 -33
- package/package.json +8 -12
- package/src/config.ts +70 -51
- package/src/deferred-tool-carryover.ts +280 -0
- package/src/extension-runtime.ts +9 -1
- package/src/inline-compaction.ts +1264 -0
- package/src/payload-rewrite.ts +22 -16
- package/src/types.ts +94 -20
- package/tsconfig.check.json +3 -8
- package/extensions/reasoning-translation.ts +0 -2
- package/src/reasoning-translation/client.ts +0 -187
- package/src/reasoning-translation/extension.ts +0 -105
- package/src/reasoning-translation/persistence.ts +0 -73
- package/src/reasoning-translation/segmenter.ts +0 -60
- package/src/reasoning-translation/state.ts +0 -399
- package/src/reasoning-translation/types.ts +0 -64
package/src/payload-rewrite.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
SessionMessageEntry,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { ResponsesCompatibleRequestPayload } from "./runtime";
|
|
10
|
+
import { rewritePayloadWithDeferredToolCarryover } from "./deferred-tool-carryover";
|
|
10
11
|
import type { NativeCompactionEntry } from "./types";
|
|
11
12
|
import {
|
|
12
13
|
compareResponsesInputParity,
|
|
@@ -503,6 +504,25 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
503
504
|
postCompactionTailMessages,
|
|
504
505
|
actualPostCompactionTail,
|
|
505
506
|
);
|
|
507
|
+
const baseRewrittenPayload: ResponsesCompatibleRequestPayload = {
|
|
508
|
+
...args.payload,
|
|
509
|
+
...(freshPreamble.instructions !== undefined ? { instructions: freshPreamble.instructions } : {}),
|
|
510
|
+
input: [
|
|
511
|
+
...freshPreamble.leadingInput,
|
|
512
|
+
...compactedWindow,
|
|
513
|
+
...actualPostCompactionTail,
|
|
514
|
+
...freshPreamble.trailingInput,
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
const carryoverRewrite = rewritePayloadWithDeferredToolCarryover({
|
|
518
|
+
payload: baseRewrittenPayload,
|
|
519
|
+
carryover: details.deferredToolCarryover,
|
|
520
|
+
compactionEntryId: args.compactionEntry.id,
|
|
521
|
+
checkpointEndIndex: freshPreamble.leadingInput.length + compactedWindow.length,
|
|
522
|
+
compat: args.model.compat as
|
|
523
|
+
| { supportsAdditionalTools?: boolean; supportsToolSearch?: boolean }
|
|
524
|
+
| undefined,
|
|
525
|
+
});
|
|
506
526
|
|
|
507
527
|
return {
|
|
508
528
|
ok: true,
|
|
@@ -517,23 +537,9 @@ function buildNativeReplaySegmentsInternal<TApi extends Api>(args: {
|
|
|
517
537
|
compactedWindow,
|
|
518
538
|
postCompactionTail,
|
|
519
539
|
originalPiReplayInput,
|
|
520
|
-
replayInput:
|
|
521
|
-
...freshPreamble.leadingInput,
|
|
522
|
-
...compactedWindow,
|
|
523
|
-
...actualPostCompactionTail,
|
|
524
|
-
...freshPreamble.trailingInput,
|
|
525
|
-
],
|
|
526
|
-
},
|
|
527
|
-
rewrittenPayload: {
|
|
528
|
-
...args.payload,
|
|
529
|
-
...(freshPreamble.instructions !== undefined ? { instructions: freshPreamble.instructions } : {}),
|
|
530
|
-
input: [
|
|
531
|
-
...freshPreamble.leadingInput,
|
|
532
|
-
...compactedWindow,
|
|
533
|
-
...actualPostCompactionTail,
|
|
534
|
-
...freshPreamble.trailingInput,
|
|
535
|
-
],
|
|
540
|
+
replayInput: carryoverRewrite.payload.input,
|
|
536
541
|
},
|
|
542
|
+
rewrittenPayload: carryoverRewrite.payload,
|
|
537
543
|
};
|
|
538
544
|
}
|
|
539
545
|
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { CompactionEntry, CompactionResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CompactionEntry, CompactionResult, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
|
|
4
4
|
export const TOOLKIT_ID = "pi-openai-toolkit";
|
|
5
5
|
export const COMPACTION_EXTENSION_ID = `${TOOLKIT_ID}:compaction`;
|
|
6
6
|
export const WEB_SEARCH_EXTENSION_ID = `${TOOLKIT_ID}:web-search`;
|
|
7
|
-
export const REASONING_TRANSLATION_EXTENSION_ID = `${TOOLKIT_ID}:reasoning-translation`;
|
|
8
7
|
export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
|
|
9
8
|
export const REDACTED_VALUE = "[REDACTED]";
|
|
10
9
|
/**
|
|
@@ -37,6 +36,15 @@ export type DebugArtifactKind =
|
|
|
37
36
|
| "compaction-event"
|
|
38
37
|
| "lifecycle";
|
|
39
38
|
|
|
39
|
+
export type CompactionContinuationMode = "inline" | "followUp" | "off";
|
|
40
|
+
|
|
41
|
+
export type AutoCompactionConfig = {
|
|
42
|
+
enabled: boolean;
|
|
43
|
+
continuation: CompactionContinuationMode;
|
|
44
|
+
unsupportedFallback: "followUp" | "off";
|
|
45
|
+
reserveTokens?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
40
48
|
export type CompactionConfig = {
|
|
41
49
|
enabled: boolean;
|
|
42
50
|
/**
|
|
@@ -56,6 +64,8 @@ export type CompactionConfig = {
|
|
|
56
64
|
model?: string;
|
|
57
65
|
/** Thinking level passed to pi's native compact() when the fallback model runs. */
|
|
58
66
|
thinkingLevel: ThinkingLevel;
|
|
67
|
+
/** Threshold-triggered compaction and same-tool-loop continuation policy. */
|
|
68
|
+
autoCompaction: AutoCompactionConfig;
|
|
59
69
|
/** Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction. */
|
|
60
70
|
responsesApis: string[];
|
|
61
71
|
notifyOnLoad: boolean;
|
|
@@ -72,19 +82,9 @@ export type WebSearchConfig = {
|
|
|
72
82
|
models: string[];
|
|
73
83
|
};
|
|
74
84
|
|
|
75
|
-
export type ReasoningTranslationConfig = {
|
|
76
|
-
enabled: boolean;
|
|
77
|
-
/** Exact provider/model keys whose thinking output may be translated. */
|
|
78
|
-
models: string[];
|
|
79
|
-
/** Separate provider/model-id used only for translation requests. */
|
|
80
|
-
model?: string;
|
|
81
|
-
targetLanguage: string;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
85
|
export type ToolkitConfig = {
|
|
85
86
|
compaction: CompactionConfig;
|
|
86
87
|
webSearch: WebSearchConfig;
|
|
87
|
-
reasoningTranslation: ReasoningTranslationConfig;
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
export type LoadedToolkitConfig = {
|
|
@@ -146,10 +146,21 @@ export type NativeCompactionIdentity = {
|
|
|
146
146
|
baseUrl: string;
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
export const CACHE_STACK_ACTIVATION_ENTRY_TYPE = "pi-cache-stack.activation-state.v1" as const;
|
|
150
|
+
|
|
151
|
+
export type DeferredToolCarryoverV1 = {
|
|
152
|
+
version: 1;
|
|
153
|
+
source: typeof CACHE_STACK_ACTIVATION_ENTRY_TYPE;
|
|
154
|
+
toolNames: string[];
|
|
155
|
+
catalogHash?: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
149
158
|
export type NativeCompactionDetails = NativeCompactionIdentity & {
|
|
150
159
|
strategy: NativeCompactionStrategy;
|
|
151
160
|
/** Actual producer of the opaque checkpoint; absent on legacy same-model entries. */
|
|
152
161
|
compactionModel?: NativeCompactionIdentity;
|
|
162
|
+
/** Cache-stack activation state captured at this opaque checkpoint. */
|
|
163
|
+
deferredToolCarryover?: DeferredToolCarryoverV1;
|
|
153
164
|
compactedWindow: unknown[];
|
|
154
165
|
compactResponseId?: string;
|
|
155
166
|
createdAt: string;
|
|
@@ -160,6 +171,7 @@ export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
|
|
|
160
171
|
|
|
161
172
|
export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
|
|
162
173
|
compactionModel?: NativeCompactionIdentity;
|
|
174
|
+
deferredToolCarryover?: DeferredToolCarryoverV1;
|
|
163
175
|
compactedWindow: unknown[];
|
|
164
176
|
compactResponseId?: string;
|
|
165
177
|
createdAt?: string;
|
|
@@ -273,6 +285,64 @@ export function isNativeCompactionIdentity(value: unknown): value is NativeCompa
|
|
|
273
285
|
);
|
|
274
286
|
}
|
|
275
287
|
|
|
288
|
+
export function isDeferredToolCarryover(value: unknown): value is DeferredToolCarryoverV1 {
|
|
289
|
+
if (!isRecord(value) || value.version !== 1 || value.source !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
Array.isArray(value.toolNames) &&
|
|
295
|
+
value.toolNames.every((name) => isNonEmptyString(name)) &&
|
|
296
|
+
(value.catalogHash === undefined || typeof value.catalogHash === "string")
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeDeferredToolCarryover(value: DeferredToolCarryoverV1): DeferredToolCarryoverV1 {
|
|
301
|
+
if (!isDeferredToolCarryover(value)) {
|
|
302
|
+
throw new Error("Invalid deferred tool carryover");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
version: 1,
|
|
307
|
+
source: CACHE_STACK_ACTIVATION_ENTRY_TYPE,
|
|
308
|
+
toolNames: [...new Set(value.toolNames.map((name) => name.trim()).filter(Boolean))].sort(),
|
|
309
|
+
...(typeof value.catalogHash === "string" ? { catalogHash: value.catalogHash } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function decodeCacheStackActivationState(value: unknown): DeferredToolCarryoverV1 | undefined {
|
|
314
|
+
if (!isRecord(value) || value.version !== 1 || typeof value.catalogHash !== "string") {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
if (!Array.isArray(value.activatedTools) || !value.activatedTools.every((name) => typeof name === "string")) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return normalizeDeferredToolCarryover({
|
|
322
|
+
version: 1,
|
|
323
|
+
source: CACHE_STACK_ACTIVATION_ENTRY_TYPE,
|
|
324
|
+
toolNames: value.activatedTools,
|
|
325
|
+
catalogHash: value.catalogHash,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Read only the latest valid cache-stack activation snapshot on this branch. */
|
|
330
|
+
export function getLatestDeferredToolCarryover(
|
|
331
|
+
entries: readonly SessionEntry[],
|
|
332
|
+
): DeferredToolCarryoverV1 | undefined {
|
|
333
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
334
|
+
const entry = entries[index];
|
|
335
|
+
if (entry?.type !== "custom" || entry.customType !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const decoded = decodeCacheStackActivationState(entry.data);
|
|
340
|
+
if (decoded) return decoded;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
276
346
|
export function isNativeCompactionDetails(value: unknown): value is NativeCompactionDetails {
|
|
277
347
|
if (!isRecord(value) || !isNativeCompactionIdentity(value)) {
|
|
278
348
|
return false;
|
|
@@ -283,6 +353,7 @@ export function isNativeCompactionDetails(value: unknown): value is NativeCompac
|
|
|
283
353
|
(candidate.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ||
|
|
284
354
|
candidate.strategy === REMOTE_V2_COMPACTION_STRATEGY) &&
|
|
285
355
|
(candidate.compactionModel === undefined || isNativeCompactionIdentity(candidate.compactionModel)) &&
|
|
356
|
+
(candidate.deferredToolCarryover === undefined || isDeferredToolCarryover(candidate.deferredToolCarryover)) &&
|
|
286
357
|
Array.isArray(candidate.compactedWindow) &&
|
|
287
358
|
candidate.compactedWindow.every(isCompactedWindowItem) &&
|
|
288
359
|
isNonEmptyString(candidate.createdAt) &&
|
|
@@ -310,6 +381,9 @@ export function createNativeCompactionDetails(input: CreateNativeCompactionDetai
|
|
|
310
381
|
baseUrl: normalizeString(input.compactionModel.baseUrl),
|
|
311
382
|
}
|
|
312
383
|
: undefined,
|
|
384
|
+
deferredToolCarryover: input.deferredToolCarryover
|
|
385
|
+
? normalizeDeferredToolCarryover(input.deferredToolCarryover)
|
|
386
|
+
: undefined,
|
|
313
387
|
compactedWindow: input.compactedWindow.map((item) => cloneStructuredValue(item)),
|
|
314
388
|
compactResponseId: isNonEmptyString(input.compactResponseId) ? normalizeString(input.compactResponseId) : undefined,
|
|
315
389
|
createdAt: isNonEmptyString(input.createdAt) ? normalizeString(input.createdAt) : new Date().toISOString(),
|
|
@@ -336,12 +410,20 @@ export function createNativeCompactionResult(
|
|
|
336
410
|
};
|
|
337
411
|
}
|
|
338
412
|
|
|
413
|
+
export const DEFAULT_AUTO_COMPACTION_CONFIG: AutoCompactionConfig = {
|
|
414
|
+
enabled: true,
|
|
415
|
+
continuation: "inline",
|
|
416
|
+
unsupportedFallback: "followUp",
|
|
417
|
+
reserveTokens: undefined,
|
|
418
|
+
};
|
|
419
|
+
|
|
339
420
|
export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|
340
421
|
enabled: true,
|
|
341
422
|
allowCompactionContinuityBreak: false,
|
|
342
423
|
remoteCompactModel: undefined,
|
|
343
424
|
model: undefined,
|
|
344
425
|
thinkingLevel: "off",
|
|
426
|
+
autoCompaction: { ...DEFAULT_AUTO_COMPACTION_CONFIG },
|
|
345
427
|
responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
|
|
346
428
|
notifyOnLoad: false,
|
|
347
429
|
debug: false,
|
|
@@ -356,15 +438,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchConfig = {
|
|
|
356
438
|
models: [],
|
|
357
439
|
};
|
|
358
440
|
|
|
359
|
-
export const DEFAULT_REASONING_TRANSLATION_CONFIG: ReasoningTranslationConfig = {
|
|
360
|
-
enabled: true,
|
|
361
|
-
models: [],
|
|
362
|
-
model: undefined,
|
|
363
|
-
targetLanguage: "Simplified Chinese",
|
|
364
|
-
};
|
|
365
|
-
|
|
366
441
|
export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
|
|
367
442
|
compaction: DEFAULT_COMPACTION_CONFIG,
|
|
368
443
|
webSearch: DEFAULT_WEB_SEARCH_CONFIG,
|
|
369
|
-
reasoningTranslation: DEFAULT_REASONING_TRANSLATION_CONFIG,
|
|
370
444
|
};
|
package/tsconfig.check.json
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
"include": [
|
|
15
15
|
"extensions/compaction.ts",
|
|
16
16
|
"extensions/web-search.ts",
|
|
17
|
-
"extensions/reasoning-translation.ts",
|
|
18
17
|
"src/compact-client.ts",
|
|
19
18
|
"src/config.ts",
|
|
20
19
|
"src/debug.ts",
|
|
21
20
|
"src/details-store.ts",
|
|
21
|
+
"src/deferred-tool-carryover.ts",
|
|
22
|
+
"src/inline-compaction.ts",
|
|
22
23
|
"src/extension-runtime.ts",
|
|
23
24
|
"src/native-fallback.ts",
|
|
24
25
|
"src/payload-rewrite.ts",
|
|
@@ -32,12 +33,6 @@
|
|
|
32
33
|
"src/web-search/extension.ts",
|
|
33
34
|
"src/web-search/payload.ts",
|
|
34
35
|
"src/web-search/prompt.ts",
|
|
35
|
-
"src/web-search/types.ts"
|
|
36
|
-
"src/reasoning-translation/client.ts",
|
|
37
|
-
"src/reasoning-translation/extension.ts",
|
|
38
|
-
"src/reasoning-translation/persistence.ts",
|
|
39
|
-
"src/reasoning-translation/segmenter.ts",
|
|
40
|
-
"src/reasoning-translation/state.ts",
|
|
41
|
-
"src/reasoning-translation/types.ts"
|
|
36
|
+
"src/web-search/types.ts"
|
|
42
37
|
]
|
|
43
38
|
}
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Context, Model, Provider, Usage } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { parseModelSpec } from "../runtime";
|
|
4
|
-
import { MAX_TRANSLATION_OUTPUT_TOKENS, TRANSLATION_TIMEOUT_MS } from "./types";
|
|
5
|
-
|
|
6
|
-
export type TranslatorFailureReason =
|
|
7
|
-
| "invalid-model-spec"
|
|
8
|
-
| "model-not-found"
|
|
9
|
-
| "provider-not-found"
|
|
10
|
-
| "auth-failed"
|
|
11
|
-
| "aborted"
|
|
12
|
-
| "request-failed"
|
|
13
|
-
| "empty-output";
|
|
14
|
-
|
|
15
|
-
export type TranslateSegmentResult =
|
|
16
|
-
| { ok: true; text: string; usage?: Usage }
|
|
17
|
-
| { ok: false; reason: TranslatorFailureReason; errorMessage?: string };
|
|
18
|
-
|
|
19
|
-
export type ResolvedTranslator = {
|
|
20
|
-
model: Model<Api>;
|
|
21
|
-
provider: Provider;
|
|
22
|
-
apiKey?: string;
|
|
23
|
-
headers?: Record<string, string | null>;
|
|
24
|
-
env?: Record<string, string>;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
function errorMessage(error: unknown): string {
|
|
28
|
-
return error instanceof Error ? error.message : String(error);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isAbortError(error: unknown): boolean {
|
|
32
|
-
return (
|
|
33
|
-
(error instanceof DOMException && error.name === "AbortError") ||
|
|
34
|
-
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function buildTranslationPrompt(targetLanguage: string): string {
|
|
39
|
-
return [
|
|
40
|
-
`Translate the following reasoning into ${targetLanguage}.`,
|
|
41
|
-
"Return only the faithful translation, with no summary, explanation, commentary, or added content.",
|
|
42
|
-
"Preserve code, commands, file paths, URLs, identifiers, formatting markers, and technical terms exactly unless they must be translated for grammatical context.",
|
|
43
|
-
].join(" ");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function resolveTranslator(
|
|
47
|
-
ctx: ExtensionContext,
|
|
48
|
-
modelSpec: string | undefined,
|
|
49
|
-
): Promise<ResolvedTranslator | { ok: false; reason: TranslatorFailureReason; errorMessage?: string }> {
|
|
50
|
-
if (!modelSpec) return { ok: false, reason: "invalid-model-spec" };
|
|
51
|
-
const parsed = parseModelSpec(modelSpec);
|
|
52
|
-
if (!parsed) return { ok: false, reason: "invalid-model-spec" };
|
|
53
|
-
|
|
54
|
-
let model: Model<Api> | undefined;
|
|
55
|
-
try {
|
|
56
|
-
model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return { ok: false, reason: "model-not-found", errorMessage: errorMessage(error) };
|
|
59
|
-
}
|
|
60
|
-
if (!model) {
|
|
61
|
-
return { ok: false, reason: "model-not-found" };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let provider: Provider | undefined;
|
|
65
|
-
try {
|
|
66
|
-
provider = ctx.modelRegistry.getProvider(parsed.provider);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
return { ok: false, reason: "provider-not-found", errorMessage: errorMessage(error) };
|
|
69
|
-
}
|
|
70
|
-
if (!provider) {
|
|
71
|
-
return { ok: false, reason: "provider-not-found" };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
76
|
-
if (!auth.ok) return { ok: false, reason: "auth-failed", errorMessage: auth.error };
|
|
77
|
-
return {
|
|
78
|
-
// Auth resolution may provide an OAuth-specific endpoint. The provider
|
|
79
|
-
// receives the effective model directly, so preserve that base URL while
|
|
80
|
-
// disabling reasoning on the cloned translator model.
|
|
81
|
-
model: {
|
|
82
|
-
...model,
|
|
83
|
-
...(auth.baseUrl ? { baseUrl: auth.baseUrl } : {}),
|
|
84
|
-
reasoning: false,
|
|
85
|
-
},
|
|
86
|
-
provider,
|
|
87
|
-
apiKey: auth.apiKey,
|
|
88
|
-
headers: auth.headers,
|
|
89
|
-
env: auth.env,
|
|
90
|
-
};
|
|
91
|
-
} catch (error) {
|
|
92
|
-
return { ok: false, reason: "auth-failed", errorMessage: errorMessage(error) };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function extractText(message: AssistantMessage): string {
|
|
97
|
-
return message.content
|
|
98
|
-
.filter((content): content is Extract<AssistantMessage["content"][number], { type: "text" }> => content.type === "text")
|
|
99
|
-
.map((content) => content.text)
|
|
100
|
-
.join("")
|
|
101
|
-
.trim();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function translateSegment(args: {
|
|
105
|
-
translator: ResolvedTranslator;
|
|
106
|
-
targetLanguage: string;
|
|
107
|
-
sourceSegment: string;
|
|
108
|
-
signal: AbortSignal;
|
|
109
|
-
}): Promise<TranslateSegmentResult> {
|
|
110
|
-
if (args.signal.aborted) return { ok: false, reason: "aborted" };
|
|
111
|
-
|
|
112
|
-
const context: Context = {
|
|
113
|
-
systemPrompt: buildTranslationPrompt(args.targetLanguage),
|
|
114
|
-
messages: [{ role: "user", content: args.sourceSegment, timestamp: Date.now() }],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const controller = new AbortController();
|
|
118
|
-
let rejectAbort: ((reason?: unknown) => void) | undefined;
|
|
119
|
-
let timedOut = false;
|
|
120
|
-
const abortPromise = new Promise<never>((_, reject) => {
|
|
121
|
-
rejectAbort = reject;
|
|
122
|
-
});
|
|
123
|
-
const onAbort = () => {
|
|
124
|
-
controller.abort();
|
|
125
|
-
rejectAbort?.(new DOMException("Translation request aborted", "AbortError"));
|
|
126
|
-
};
|
|
127
|
-
args.signal.addEventListener("abort", onAbort, { once: true });
|
|
128
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
// Keep the call safe even when a ResolvedTranslator is supplied by a test
|
|
132
|
-
// seam or another internal caller instead of resolveTranslator().
|
|
133
|
-
const effectiveModel = args.translator.model.reasoning
|
|
134
|
-
? { ...args.translator.model, reasoning: false }
|
|
135
|
-
: args.translator.model;
|
|
136
|
-
const stream = args.translator.provider.streamSimple(effectiveModel, context, {
|
|
137
|
-
apiKey: args.translator.apiKey,
|
|
138
|
-
headers: args.translator.headers,
|
|
139
|
-
env: args.translator.env,
|
|
140
|
-
signal: controller.signal,
|
|
141
|
-
timeoutMs: TRANSLATION_TIMEOUT_MS,
|
|
142
|
-
maxTokens: Math.max(1, Math.min(args.translator.model.maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)),
|
|
143
|
-
});
|
|
144
|
-
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
145
|
-
timeoutId = setTimeout(() => {
|
|
146
|
-
timedOut = true;
|
|
147
|
-
controller.abort();
|
|
148
|
-
reject(new Error(`Translation request timed out after ${TRANSLATION_TIMEOUT_MS} ms.`));
|
|
149
|
-
}, TRANSLATION_TIMEOUT_MS);
|
|
150
|
-
});
|
|
151
|
-
const message = await Promise.race([stream.result(), abortPromise, timeoutPromise]);
|
|
152
|
-
if (args.signal.aborted) return { ok: false, reason: "aborted" };
|
|
153
|
-
if (timedOut) {
|
|
154
|
-
return {
|
|
155
|
-
ok: false,
|
|
156
|
-
reason: "request-failed",
|
|
157
|
-
errorMessage: `Translation request timed out after ${TRANSLATION_TIMEOUT_MS} ms.`,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
if (message.stopReason === "aborted") {
|
|
161
|
-
return { ok: false, reason: "request-failed", errorMessage: "Translator aborted the request." };
|
|
162
|
-
}
|
|
163
|
-
if (message.stopReason !== "stop" && message.stopReason !== "length") {
|
|
164
|
-
return { ok: false, reason: "request-failed", errorMessage: message.errorMessage };
|
|
165
|
-
}
|
|
166
|
-
const text = extractText(message);
|
|
167
|
-
return text.length > 0
|
|
168
|
-
? { ok: true, text, usage: message.usage }
|
|
169
|
-
: { ok: false, reason: "empty-output" };
|
|
170
|
-
} catch (error) {
|
|
171
|
-
if (args.signal.aborted) return { ok: false, reason: "aborted" };
|
|
172
|
-
if (timedOut) {
|
|
173
|
-
return {
|
|
174
|
-
ok: false,
|
|
175
|
-
reason: "request-failed",
|
|
176
|
-
errorMessage: `Translation request timed out after ${TRANSLATION_TIMEOUT_MS} ms.`,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (isAbortError(error)) {
|
|
180
|
-
return { ok: false, reason: "request-failed", errorMessage: errorMessage(error) };
|
|
181
|
-
}
|
|
182
|
-
return { ok: false, reason: "request-failed", errorMessage: errorMessage(error) };
|
|
183
|
-
} finally {
|
|
184
|
-
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
185
|
-
args.signal.removeEventListener("abort", onAbort);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { loadToolkitConfig } from "../config";
|
|
3
|
-
import type { LoadedToolkitConfig } from "../types";
|
|
4
|
-
import { ReasoningTranslationState } from "./state";
|
|
5
|
-
|
|
6
|
-
function refreshThinkingDisplay(ctx: ExtensionContext | undefined): void {
|
|
7
|
-
if (!ctx || ctx.mode !== "tui" || !ctx.hasUI) return;
|
|
8
|
-
try {
|
|
9
|
-
// Pi exposes no direct transcript invalidation API. Calling the public
|
|
10
|
-
// setter rebuilds assistant components and reruns transformers, while the
|
|
11
|
-
// no-argument form avoids leaving "Translating…" as the global hidden label.
|
|
12
|
-
ctx.ui.setHiddenThinkingLabel();
|
|
13
|
-
} catch {
|
|
14
|
-
// A display refresh is additive; never break the main model stream.
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function restoreThinkingDisplay(ctx: ExtensionContext | undefined): void {
|
|
19
|
-
if (!ctx || ctx.mode !== "tui" || !ctx.hasUI) return;
|
|
20
|
-
try {
|
|
21
|
-
ctx.ui.setHiddenThinkingLabel();
|
|
22
|
-
} catch {
|
|
23
|
-
// Shutdown/reload must remain failure-open.
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function registerReasoningTranslationExtension(
|
|
28
|
-
pi: ExtensionAPI,
|
|
29
|
-
loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
|
|
30
|
-
): void {
|
|
31
|
-
let latestContext: ExtensionContext | undefined;
|
|
32
|
-
const state = new ReasoningTranslationState({
|
|
33
|
-
onDisplayChanged: () => refreshThinkingDisplay(latestContext),
|
|
34
|
-
appendEntry: (customType, data) => pi.appendEntry(customType, data),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
pi.registerMarkdownTransformer((markdown, context) => {
|
|
38
|
-
if (context.messageType !== "assistant-thinking" || latestContext?.mode !== "tui") return markdown;
|
|
39
|
-
return state.lookupDisplay(markdown) ?? markdown;
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
pi.on("session_start", (_event, ctx) => {
|
|
43
|
-
latestContext = ctx;
|
|
44
|
-
state.abortActive();
|
|
45
|
-
const { config } = loadConfig() as LoadedToolkitConfig;
|
|
46
|
-
state.hydrate(ctx, config.reasoningTranslation);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
50
|
-
latestContext = ctx;
|
|
51
|
-
state.abortActive();
|
|
52
|
-
const { config } = loadConfig() as LoadedToolkitConfig;
|
|
53
|
-
state.hydrate(ctx, config.reasoningTranslation);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
pi.on("session_before_switch", () => {
|
|
57
|
-
state.abortActive();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
pi.on("session_before_fork", () => {
|
|
61
|
-
state.abortActive();
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
pi.on("session_before_tree", () => {
|
|
65
|
-
state.abortActive();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
pi.on("session_shutdown", (_event, ctx) => {
|
|
69
|
-
if (latestContext === ctx) latestContext = undefined;
|
|
70
|
-
state.abortActive();
|
|
71
|
-
restoreThinkingDisplay(ctx);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
pi.on("model_select", (_event, ctx) => {
|
|
75
|
-
latestContext = ctx;
|
|
76
|
-
state.abortActive();
|
|
77
|
-
const { config } = loadConfig() as LoadedToolkitConfig;
|
|
78
|
-
state.setDisplayEligibility(ctx, config.reasoningTranslation);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
pi.on("message_start", (event, ctx) => {
|
|
82
|
-
latestContext = ctx;
|
|
83
|
-
const { config } = loadConfig() as LoadedToolkitConfig;
|
|
84
|
-
state.beginMessage(event.message, ctx, config.reasoningTranslation);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
pi.on("message_update", (event, ctx) => {
|
|
88
|
-
latestContext = ctx;
|
|
89
|
-
if (ctx.mode !== "tui") return;
|
|
90
|
-
const update = event.assistantMessageEvent;
|
|
91
|
-
if (update.type !== "thinking_delta" && update.type !== "thinking_end") return;
|
|
92
|
-
state.handleMessageUpdate(event.message, update.type, update.contentIndex);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
pi.on("message_end", (event, ctx) => {
|
|
96
|
-
latestContext = ctx;
|
|
97
|
-
if (ctx.mode !== "tui") return;
|
|
98
|
-
state.finishMessage(event.message);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export default function reasoningTranslationExtension(pi: ExtensionAPI): void {
|
|
104
|
-
registerReasoningTranslationExtension(pi);
|
|
105
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
-
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
5
|
-
REASONING_TRANSLATION_ENTRY_TYPE,
|
|
6
|
-
type ReasoningTranslationEntryV1,
|
|
7
|
-
cloneUsage,
|
|
8
|
-
isReasoningTranslationEntry,
|
|
9
|
-
} from "./types";
|
|
10
|
-
|
|
11
|
-
export type ThinkingRunProjection = {
|
|
12
|
-
runKey: number;
|
|
13
|
-
sourceMarkdown: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export function projectThinkingRuns(message: Pick<AssistantMessage, "content">): ThinkingRunProjection[] {
|
|
17
|
-
const runs: ThinkingRunProjection[] = [];
|
|
18
|
-
for (let index = 0; index < message.content.length; index++) {
|
|
19
|
-
const content = message.content[index];
|
|
20
|
-
if (content.type !== "thinking") continue;
|
|
21
|
-
|
|
22
|
-
const runKey = index;
|
|
23
|
-
const blocks: string[] = [];
|
|
24
|
-
for (; index < message.content.length; index++) {
|
|
25
|
-
const block = message.content[index];
|
|
26
|
-
if (block.type !== "thinking") break;
|
|
27
|
-
const thinking = block.thinking.trim();
|
|
28
|
-
if (thinking) blocks.push(thinking);
|
|
29
|
-
}
|
|
30
|
-
index--;
|
|
31
|
-
if (blocks.length > 0) {
|
|
32
|
-
runs.push({ runKey, sourceMarkdown: blocks.join("\n\n") });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return runs;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function hashThinkingMarkdown(sourceMarkdown: string): string {
|
|
39
|
-
return createHash("sha256").update(sourceMarkdown, "utf8").digest("hex");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function createReasoningTranslationEntry(args: {
|
|
43
|
-
sourceMarkdown: string;
|
|
44
|
-
displayText: string;
|
|
45
|
-
targetLanguage: string;
|
|
46
|
-
translator: { provider: string; model: string };
|
|
47
|
-
usage?: import("@earendil-works/pi-ai").Usage;
|
|
48
|
-
}): ReasoningTranslationEntryV1 {
|
|
49
|
-
return {
|
|
50
|
-
version: 1,
|
|
51
|
-
sourceHash: hashThinkingMarkdown(args.sourceMarkdown),
|
|
52
|
-
displayText: args.displayText,
|
|
53
|
-
targetLanguage: args.targetLanguage,
|
|
54
|
-
translator: { ...args.translator },
|
|
55
|
-
usage: cloneUsage(args.usage),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function replayReasoningTranslations(
|
|
60
|
-
entries: readonly SessionEntry[],
|
|
61
|
-
): Map<string, ReasoningTranslationEntryV1> {
|
|
62
|
-
const translations = new Map<string, ReasoningTranslationEntryV1>();
|
|
63
|
-
for (const entry of entries) {
|
|
64
|
-
if (entry.type !== "custom" || entry.customType !== REASONING_TRANSLATION_ENTRY_TYPE) continue;
|
|
65
|
-
if (!isReasoningTranslationEntry(entry.data)) continue;
|
|
66
|
-
translations.set(entry.data.sourceHash, {
|
|
67
|
-
...entry.data,
|
|
68
|
-
translator: { ...entry.data.translator },
|
|
69
|
-
usage: cloneUsage(entry.data.usage),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return translations;
|
|
73
|
-
}
|