pi-openai-toolkit 0.8.0 → 0.9.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.
@@ -1,160 +1,160 @@
1
- import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
2
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { loadToolkitConfig } from "../config";
4
- import { executeImageGeneration } from "./service";
5
- import { isImageGenerationEnabledForModel } from "./eligibility";
6
- import { renderImageGenerationResult } from "./render";
7
- import {
8
- IMAGE_GENERATION_QUALITIES,
9
- IMAGE_GENERATION_SIZES,
10
- IMAGE_GENERATION_TOOL_NAME,
11
- MAX_IMAGE_PATH_CHARS,
12
- MAX_IMAGE_PROMPT_CHARS,
13
- MAX_REFERENCE_IMAGE_COUNT,
14
- ImageGenerationError,
15
- sanitizeImageDiagnostic,
16
- type ImageGenerationDetails,
17
- } from "./types";
18
-
19
- const registeredApis = new WeakSet<object>();
20
-
21
- const GenerateImageParameters = Type.Object(
22
- {
23
- prompt: Type.String({
24
- minLength: 1,
25
- maxLength: MAX_IMAGE_PROMPT_CHARS,
26
- description: "Image generation or edit prompt. Preserve the user's requested subject and constraints.",
27
- }),
28
- referenceImagePaths: Type.Optional(
29
- Type.Union(
30
- [
31
- Type.Null(),
32
- Type.Array(Type.String({ minLength: 1, maxLength: MAX_IMAGE_PATH_CHARS }), {
33
- minItems: 1,
34
- maxItems: MAX_REFERENCE_IMAGE_COUNT,
35
- }),
36
- ],
37
- {
38
- description:
39
- "Use null when the user did not explicitly identify reference images; otherwise provide one to five user-identified local image paths.",
40
- },
41
- ),
42
- ),
43
- outputPath: Type.Optional(
44
- Type.Union(
45
- [
46
- Type.Null(),
47
- Type.String({
48
- minLength: 1,
49
- maxLength: MAX_IMAGE_PATH_CHARS,
50
- }),
51
- ],
52
- {
53
- description:
54
- "Use null when the user did not explicitly request a destination; otherwise provide the requested explicit .png file path.",
55
- },
56
- ),
57
- ),
58
- size: Type.Optional(
59
- StringEnum(IMAGE_GENERATION_SIZES, {
60
- description: "Requested PNG dimensions, or auto.",
61
- }),
62
- ),
63
- quality: Type.Optional(
64
- StringEnum(IMAGE_GENERATION_QUALITIES, {
65
- description: "Requested image quality, or auto.",
66
- }),
67
- ),
68
- },
69
- { additionalProperties: false },
70
- );
71
-
72
- type GenerateImageToolParams = Static<typeof GenerateImageParameters>;
73
-
74
- function syncImageGenerationTool(
75
- pi: ExtensionAPI,
76
- model: Parameters<typeof isImageGenerationEnabledForModel>[0],
77
- config: Parameters<typeof isImageGenerationEnabledForModel>[1],
78
- ): void {
79
- const eligible = isImageGenerationEnabledForModel(model, config);
80
- const activeTools = pi.getActiveTools();
81
- const active = activeTools.includes(IMAGE_GENERATION_TOOL_NAME);
82
- if (eligible && !active) {
83
- pi.setActiveTools([...activeTools, IMAGE_GENERATION_TOOL_NAME]);
84
- } else if (!eligible && active) {
85
- pi.setActiveTools(activeTools.filter((name) => name !== IMAGE_GENERATION_TOOL_NAME));
86
- }
87
- }
88
-
89
- export function registerImageGenerationExtension(
90
- pi: ExtensionAPI,
91
- loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
92
- executeImage: typeof executeImageGeneration = executeImageGeneration,
93
- ): void {
94
- if (registeredApis.has(pi)) return;
95
- registeredApis.add(pi);
96
-
97
- pi.registerTool<typeof GenerateImageParameters, ImageGenerationDetails, Record<string, never>>({
98
- name: IMAGE_GENERATION_TOOL_NAME,
99
- label: "OpenAI Generate Image",
100
- description:
101
- "Generate a PNG image, or edit from one to five user-approved local reference images, through the current allowlisted Responses model and the hosted gpt-image-2 image_generation tool. This is a paid provider operation.",
102
- promptSnippet: "Generate or edit PNG images through the current allowlisted Responses model and gpt-image-2.",
103
- promptGuidelines: [
104
- "Use openai_generate_image when the user explicitly asks to create, draw, render, or edit a raster image.",
105
- "Do not call openai_generate_image speculatively: it consumes the user's provider or gateway image quota.",
106
- "Keep the image prompt faithful to the user's requested subject and constraints; do not invent unrequested style details.",
107
- "Set referenceImagePaths to null unless the user explicitly identified local files; never invent paths or placeholder strings, and remember upload requires user approval.",
108
- "Set outputPath to null unless the user explicitly asks for a destination; never invent a destination or placeholder string, and otherwise use the default Pi agent artifact.",
109
- "Do not substitute Python, browser automation, shell scripts, or unrelated image tools for an eligible image request.",
110
- ],
111
- parameters: GenerateImageParameters,
112
- executionMode: "sequential",
113
- async execute(toolCallId, params: GenerateImageToolParams, signal, _onUpdate, ctx) {
114
- try {
115
- const result = await executeImage({
116
- params,
117
- toolCallId,
118
- signal,
119
- ctx,
120
- });
121
- return {
122
- content: [{ type: "text", text: result.text }],
123
- details: result.details,
124
- };
125
- } catch (error) {
126
- if (error instanceof ImageGenerationError) {
127
- throw new Error(error.message);
128
- }
129
- throw new Error(
130
- sanitizeImageDiagnostic(error instanceof Error ? error.message : error, "Image generation failed."),
131
- );
132
- }
133
- },
134
- renderResult: renderImageGenerationResult,
135
- });
136
-
137
- pi.on("session_start", (_event, ctx) => {
138
- const { config } = loadConfig();
139
- syncImageGenerationTool(pi, ctx.model, config.imageGeneration);
140
- });
141
-
142
- pi.on("model_select", (event) => {
143
- const { config } = loadConfig();
144
- syncImageGenerationTool(pi, event.model, config.imageGeneration);
145
- });
146
-
147
- pi.on("before_agent_start", (_event, ctx) => {
148
- const { config } = loadConfig();
149
- syncImageGenerationTool(pi, ctx.model, config.imageGeneration);
150
- });
151
- }
152
-
153
- export default function imageGenerationExtension(pi: ExtensionAPI): void {
154
- registerImageGenerationExtension(pi);
155
- }
156
-
157
- export const _extensionTest = {
158
- GenerateImageParameters,
159
- syncImageGenerationTool,
160
- };
1
+ import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { loadToolkitConfig } from "../config";
4
+ import { executeImageGeneration } from "./service";
5
+ import { isImageGenerationEnabledForModel } from "./eligibility";
6
+ import { renderImageGenerationResult } from "./render";
7
+ import {
8
+ IMAGE_GENERATION_QUALITIES,
9
+ IMAGE_GENERATION_SIZES,
10
+ IMAGE_GENERATION_TOOL_NAME,
11
+ MAX_IMAGE_PATH_CHARS,
12
+ MAX_IMAGE_PROMPT_CHARS,
13
+ MAX_REFERENCE_IMAGE_COUNT,
14
+ ImageGenerationError,
15
+ sanitizeImageDiagnostic,
16
+ type ImageGenerationDetails,
17
+ } from "./types";
18
+
19
+ const registeredApis = new WeakSet<object>();
20
+
21
+ const GenerateImageParameters = Type.Object(
22
+ {
23
+ prompt: Type.String({
24
+ minLength: 1,
25
+ maxLength: MAX_IMAGE_PROMPT_CHARS,
26
+ description: "Image generation or edit prompt. Preserve the user's requested subject and constraints.",
27
+ }),
28
+ referenceImagePaths: Type.Optional(
29
+ Type.Union(
30
+ [
31
+ Type.Null(),
32
+ Type.Array(Type.String({ minLength: 1, maxLength: MAX_IMAGE_PATH_CHARS }), {
33
+ minItems: 1,
34
+ maxItems: MAX_REFERENCE_IMAGE_COUNT,
35
+ }),
36
+ ],
37
+ {
38
+ description:
39
+ "Use null when the user did not explicitly identify reference images; otherwise provide one to five user-identified local image paths.",
40
+ },
41
+ ),
42
+ ),
43
+ outputPath: Type.Optional(
44
+ Type.Union(
45
+ [
46
+ Type.Null(),
47
+ Type.String({
48
+ minLength: 1,
49
+ maxLength: MAX_IMAGE_PATH_CHARS,
50
+ }),
51
+ ],
52
+ {
53
+ description:
54
+ "Use null when the user did not explicitly request a destination; otherwise provide the requested explicit .png file path.",
55
+ },
56
+ ),
57
+ ),
58
+ size: Type.Optional(
59
+ StringEnum(IMAGE_GENERATION_SIZES, {
60
+ description: "Requested PNG dimensions, or auto.",
61
+ }),
62
+ ),
63
+ quality: Type.Optional(
64
+ StringEnum(IMAGE_GENERATION_QUALITIES, {
65
+ description: "Requested image quality, or auto.",
66
+ }),
67
+ ),
68
+ },
69
+ { additionalProperties: false },
70
+ );
71
+
72
+ type GenerateImageToolParams = Static<typeof GenerateImageParameters>;
73
+
74
+ function syncImageGenerationTool(
75
+ pi: ExtensionAPI,
76
+ model: Parameters<typeof isImageGenerationEnabledForModel>[0],
77
+ config: Parameters<typeof isImageGenerationEnabledForModel>[1],
78
+ ): void {
79
+ const eligible = isImageGenerationEnabledForModel(model, config);
80
+ const activeTools = pi.getActiveTools();
81
+ const active = activeTools.includes(IMAGE_GENERATION_TOOL_NAME);
82
+ if (eligible && !active) {
83
+ pi.setActiveTools([...activeTools, IMAGE_GENERATION_TOOL_NAME]);
84
+ } else if (!eligible && active) {
85
+ pi.setActiveTools(activeTools.filter((name) => name !== IMAGE_GENERATION_TOOL_NAME));
86
+ }
87
+ }
88
+
89
+ export function registerImageGenerationExtension(
90
+ pi: ExtensionAPI,
91
+ loadConfig: typeof loadToolkitConfig = loadToolkitConfig,
92
+ executeImage: typeof executeImageGeneration = executeImageGeneration,
93
+ ): void {
94
+ if (registeredApis.has(pi)) return;
95
+ registeredApis.add(pi);
96
+
97
+ pi.registerTool<typeof GenerateImageParameters, ImageGenerationDetails, Record<string, never>>({
98
+ name: IMAGE_GENERATION_TOOL_NAME,
99
+ label: "OpenAI Generate Image",
100
+ description:
101
+ "Generate a PNG image, or edit from one to five user-approved local reference images, through the current Responses-capable model and the hosted gpt-image-2 image_generation tool. This is a paid provider operation.",
102
+ promptSnippet: "Generate or edit PNG images through the current Responses-capable model and gpt-image-2.",
103
+ promptGuidelines: [
104
+ "Use openai_generate_image when the user explicitly asks to create, draw, render, or edit a raster image and the active model speaks a Responses API.",
105
+ "Do not call openai_generate_image speculatively: it consumes the user's provider or gateway image quota.",
106
+ "Keep the image prompt faithful to the user's requested subject and constraints; do not invent unrequested style details.",
107
+ "Set referenceImagePaths to null unless the user explicitly identified local files; never invent paths or placeholder strings, and remember upload requires user approval.",
108
+ "Set outputPath to null unless the user explicitly asks for a destination; never invent a destination or placeholder string, and otherwise use the default Pi agent artifact.",
109
+ "Do not substitute Python, browser automation, shell scripts, or unrelated image tools for an eligible image request.",
110
+ ],
111
+ parameters: GenerateImageParameters,
112
+ executionMode: "sequential",
113
+ async execute(toolCallId, params: GenerateImageToolParams, signal, _onUpdate, ctx) {
114
+ try {
115
+ const result = await executeImage({
116
+ params,
117
+ toolCallId,
118
+ signal,
119
+ ctx,
120
+ });
121
+ return {
122
+ content: [{ type: "text", text: result.text }],
123
+ details: result.details,
124
+ };
125
+ } catch (error) {
126
+ if (error instanceof ImageGenerationError) {
127
+ throw new Error(error.message);
128
+ }
129
+ throw new Error(
130
+ sanitizeImageDiagnostic(error instanceof Error ? error.message : error, "Image generation failed."),
131
+ );
132
+ }
133
+ },
134
+ renderResult: renderImageGenerationResult,
135
+ });
136
+
137
+ pi.on("session_start", (_event, ctx) => {
138
+ const { config } = loadConfig();
139
+ syncImageGenerationTool(pi, ctx.model, config.imageGeneration);
140
+ });
141
+
142
+ pi.on("model_select", (event) => {
143
+ const { config } = loadConfig();
144
+ syncImageGenerationTool(pi, event.model, config.imageGeneration);
145
+ });
146
+
147
+ pi.on("before_agent_start", (_event, ctx) => {
148
+ const { config } = loadConfig();
149
+ syncImageGenerationTool(pi, ctx.model, config.imageGeneration);
150
+ });
151
+ }
152
+
153
+ export default function imageGenerationExtension(pi: ExtensionAPI): void {
154
+ registerImageGenerationExtension(pi);
155
+ }
156
+
157
+ export const _extensionTest = {
158
+ GenerateImageParameters,
159
+ syncImageGenerationTool,
160
+ };
@@ -12,6 +12,7 @@ import type { CompactionConfig } from "./types";
12
12
  export { parseModelSpec } from "./runtime";
13
13
 
14
14
  export type NativeFallbackFailureReason =
15
+ | "disabled"
15
16
  | "no-model-configured"
16
17
  | "invalid-model-spec"
17
18
  | "model-not-found"
@@ -53,23 +54,35 @@ function toErrorMessage(error: unknown): string {
53
54
  }
54
55
 
55
56
  /**
56
- * Run pi's native compaction method with the user-configured compaction model.
57
+ * Run pi's native compaction method with a caller-resolved summary model.
57
58
  *
58
- * Only handles the "configured model differs from the current one" case: when no model
59
- * is configured (or it equals the current model), the caller should return undefined from
60
- * session_before_compact so pi runs the same native path itself, keeping its internal
59
+ * The two compaction chains stay separate: the caller passes `remoteCompactModel` only when a
60
+ * remote v2 request was actually attempted and failed, and `nativeFallback.model` only when the
61
+ * active model cannot use remote v2 at all. This function never inspects `remoteCompactModel`
62
+ * itself, so a model that cannot compact remotely can never inherit the remote producer.
63
+ *
64
+ * Only the "resolved model differs from the current one" case is handled here: when the feature
65
+ * is disabled, nothing is resolved, or it equals the current model, the caller returns undefined
66
+ * from session_before_compact so pi runs the same native path itself, keeping its internal
61
67
  * streamFn/thinkingLevel wiring.
62
68
  */
63
69
  export async function runNativeFallbackCompaction(args: {
64
70
  ctx: ExtensionContext;
65
71
  event: SessionBeforeCompactEvent;
66
72
  config: CompactionConfig;
73
+ /** Model spec supplied by the caller for this specific fallback reason. */
74
+ modelSpec?: string;
67
75
  compactFn?: NativeCompactFn;
68
76
  }): Promise<NativeFallbackResult> {
69
77
  const { ctx, event, config } = args;
70
78
  const compactFn = args.compactFn ?? compact;
79
+ const nativeFallback = config.nativeFallback;
80
+
81
+ if (!nativeFallback.enabled) {
82
+ return { ok: false, reason: "disabled" };
83
+ }
71
84
 
72
- const spec = config.model?.trim();
85
+ const spec = args.modelSpec?.trim() ?? "";
73
86
  if (!spec) {
74
87
  return { ok: false, reason: "no-model-configured" };
75
88
  }
@@ -106,7 +119,7 @@ export async function runNativeFallbackCompaction(args: {
106
119
  mergeProviderHeaders(auth.headers),
107
120
  event.customInstructions,
108
121
  event.signal,
109
- config.thinkingLevel,
122
+ nativeFallback.thinkingLevel,
110
123
  undefined,
111
124
  auth.env,
112
125
  );
package/src/types.ts CHANGED
@@ -5,6 +5,7 @@ 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
7
  export const IMAGE_GENERATION_EXTENSION_ID = `${TOOLKIT_ID}:image-generation`;
8
+ export const AUTO_MODE_EXTENSION_ID = `${TOOLKIT_ID}:auto-mode`;
8
9
  export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
9
10
  export const REDACTED_VALUE = "[REDACTED]";
10
11
  /**
@@ -39,6 +40,20 @@ export type DebugArtifactKind =
39
40
 
40
41
  export type CompactionContinuationMode = "inline" | "followUp" | "off";
41
42
 
43
+ /** Native-method fallback compaction: which model runs pi's compact() and how deeply. */
44
+ export type NativeFallbackConfig = {
45
+ enabled: boolean;
46
+ /**
47
+ * "provider/model-id" used for native-method fallback compaction when the active model
48
+ * cannot use remote v2 at all. A failed remote request is instead summarized by
49
+ * `remoteCompactModel`; the two sources never substitute for each other. Unset means the
50
+ * active model via pi's default path.
51
+ */
52
+ model?: string;
53
+ /** Thinking level passed to pi's native compact() when the fallback model runs. */
54
+ thinkingLevel: ThinkingLevel;
55
+ };
56
+
42
57
  export type AutoCompactionConfig = {
43
58
  enabled: boolean;
44
59
  continuation: CompactionContinuationMode;
@@ -58,13 +73,8 @@ export type CompactionConfig = {
58
73
  * remains the checkpoint consumer and continues handling normal requests.
59
74
  */
60
75
  remoteCompactModel?: string;
61
- /**
62
- * "provider/model-id" used for native-method fallback compaction (non-Responses APIs,
63
- * or when the compact endpoint fails). Unset = current model via pi's default path.
64
- */
65
- model?: string;
66
- /** Thinking level passed to pi's native compact() when the fallback model runs. */
67
- thinkingLevel: ThinkingLevel;
76
+ /** Native-method fallback compaction policy (non-Responses APIs, or when the compact endpoint fails). */
77
+ nativeFallback: NativeFallbackConfig;
68
78
  /** Threshold-triggered compaction and same-tool-loop continuation policy. */
69
79
  autoCompaction: AutoCompactionConfig;
70
80
  /** Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction. */
@@ -85,14 +95,32 @@ export type WebSearchConfig = {
85
95
 
86
96
  export type ImageGenerationConfig = {
87
97
  enabled: boolean;
88
- /** Exact provider/model keys allowed to use toolkit-native image generation. */
98
+ };
99
+
100
+ /** `side-effect` reviews bash/write/edit plus extras; `all` reviews every tool call. */
101
+ export type AutoModeGate = "side-effect" | "all";
102
+
103
+ export type AutoModeConfig = {
104
+ enabled: boolean;
105
+ /** Exact provider/model keys whose sessions may engage auto mode. */
89
106
  models: string[];
107
+ /** "provider/model-id" that approves gated tool calls while auto mode is engaged. */
108
+ reviewerModel?: string;
109
+ gate: AutoModeGate;
110
+ /** Extra tool names reviewed on top of the side-effect default set. */
111
+ extraTools: string[];
112
+ /** Reviewer deadline in ms; expiry degrades to human confirmation or fail-closed. */
113
+ timeoutMs: number;
90
114
  };
91
115
 
116
+ export const REVIEWER_TIMEOUT_MIN_MS = 1_000;
117
+ export const REVIEWER_TIMEOUT_MAX_MS = 120_000;
118
+
92
119
  export type ToolkitConfig = {
93
120
  compaction: CompactionConfig;
94
121
  webSearch: WebSearchConfig;
95
122
  imageGeneration: ImageGenerationConfig;
123
+ autoMode: AutoModeConfig;
96
124
  };
97
125
 
98
126
  export type LoadedToolkitConfig = {
@@ -425,12 +453,17 @@ export const DEFAULT_AUTO_COMPACTION_CONFIG: AutoCompactionConfig = {
425
453
  reserveTokens: undefined,
426
454
  };
427
455
 
456
+ export const DEFAULT_NATIVE_FALLBACK_CONFIG: NativeFallbackConfig = {
457
+ enabled: true,
458
+ model: undefined,
459
+ thinkingLevel: "off",
460
+ };
461
+
428
462
  export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
429
463
  enabled: true,
430
464
  allowCompactionContinuityBreak: false,
431
465
  remoteCompactModel: undefined,
432
- model: undefined,
433
- thinkingLevel: "off",
466
+ nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
434
467
  autoCompaction: { ...DEFAULT_AUTO_COMPACTION_CONFIG },
435
468
  responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
436
469
  notifyOnLoad: false,
@@ -447,12 +480,21 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchConfig = {
447
480
  };
448
481
 
449
482
  export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
483
+ enabled: false,
484
+ };
485
+
486
+ export const DEFAULT_AUTO_MODE_CONFIG: AutoModeConfig = {
450
487
  enabled: true,
451
488
  models: [],
489
+ reviewerModel: undefined,
490
+ gate: "side-effect",
491
+ extraTools: [],
492
+ timeoutMs: 30_000,
452
493
  };
453
494
 
454
495
  export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
455
496
  compaction: DEFAULT_COMPACTION_CONFIG,
456
497
  webSearch: DEFAULT_WEB_SEARCH_CONFIG,
457
498
  imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
499
+ autoMode: DEFAULT_AUTO_MODE_CONFIG,
458
500
  };
@@ -15,6 +15,7 @@
15
15
  "extensions/compaction.ts",
16
16
  "extensions/web-search.ts",
17
17
  "extensions/image-generation.ts",
18
+ "extensions/auto-mode.ts",
18
19
  "src/compact-client.ts",
19
20
  "src/config.ts",
20
21
  "src/debug.ts",
@@ -31,6 +32,10 @@
31
32
  "src/image-generation/render.ts",
32
33
  "src/image-generation/service.ts",
33
34
  "src/image-generation/types.ts",
35
+ "src/auto-mode/extension.ts",
36
+ "src/auto-mode/policy.ts",
37
+ "src/auto-mode/reviewer.ts",
38
+ "src/auto-mode/types.ts",
34
39
  "src/model-scope.ts",
35
40
  "src/native-fallback.ts",
36
41
  "src/payload-rewrite.ts",