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.
@@ -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
  };
@@ -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,2 +0,0 @@
1
- export { default } from "../src/reasoning-translation/extension.ts";
2
- export { registerReasoningTranslationExtension } from "../src/reasoning-translation/extension.ts";
@@ -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
- }