@czottmann/pi-automode 1.3.0 → 1.4.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.
@@ -55,6 +55,24 @@ async function resolveClassifier(
55
55
  return { model, apiKey: auth.apiKey, headers: auth.headers };
56
56
  }
57
57
 
58
+ export type ClassifierCompletionFn = (
59
+ model: Model<any>,
60
+ options: { systemPrompt: string; messages: UserMessage[] },
61
+ callOptions: {
62
+ apiKey?: string;
63
+ headers?: Record<string, string>;
64
+ signal?: AbortSignal;
65
+ maxTokens: number;
66
+ temperature: number;
67
+ },
68
+ ) => Promise<AssistantMessage>;
69
+
70
+ export type RetryOptions = {
71
+ maxAttempts?: number;
72
+ maxTokens?: number;
73
+ temperature?: number;
74
+ };
75
+
58
76
  /** Parse the classifier's JSON-only response. Invalid output is handled fail-closed by the caller. */
59
77
  export function parseClassifierDecision(
60
78
  message: AssistantMessage,
@@ -90,6 +108,66 @@ export function parseClassifierDecision(
90
108
  return undefined;
91
109
  }
92
110
 
111
+ /**
112
+ * Call the classifier model and parse its decision, retrying when the model
113
+ * returns malformed or truncated output. Small classifier models occasionally
114
+ * ramble into the token cap (stopReason "length") or emit prose instead of
115
+ * JSON; a single retry recovers most of these transient failures.
116
+ *
117
+ * Safety is preserved: an "allow" is only returned when a response actually
118
+ * parses to a valid allow decision. If every attempt fails to parse, the
119
+ * function fails closed with a block decision. Thrown errors (network/auth)
120
+ * are not retried — they fail closed immediately, matching prior behavior.
121
+ */
122
+ export async function classifyWithRetry(
123
+ completeFn: ClassifierCompletionFn,
124
+ classifier: {
125
+ model: Model<any>;
126
+ apiKey?: string;
127
+ headers?: Record<string, string>;
128
+ },
129
+ prompt: { systemPrompt: string; messages: UserMessage[] },
130
+ signal: AbortSignal | undefined,
131
+ options: RetryOptions = {},
132
+ ): Promise<ClassificationDecision> {
133
+ const maxAttempts = options.maxAttempts ?? 2;
134
+ const maxTokens = options.maxTokens ?? 1200;
135
+ const temperature = options.temperature ?? 0;
136
+ let lastReason =
137
+ "Classifier response was not valid decision JSON; auto mode fails closed.";
138
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
139
+ let response: AssistantMessage;
140
+ try {
141
+ response = await completeFn(
142
+ classifier.model,
143
+ prompt,
144
+ {
145
+ apiKey: classifier.apiKey,
146
+ headers: classifier.headers,
147
+ signal,
148
+ maxTokens,
149
+ temperature,
150
+ },
151
+ );
152
+ } catch (error) {
153
+ return {
154
+ decision: "block",
155
+ tier: "none",
156
+ reason: `Classifier failed; auto mode fails closed: ${
157
+ error instanceof Error ? error.message : String(error)
158
+ }`,
159
+ };
160
+ }
161
+ const decision = parseClassifierDecision(response);
162
+ if (decision) return decision;
163
+ lastReason =
164
+ response.stopReason === "length"
165
+ ? "Classifier response was truncated before producing valid decision JSON; auto mode fails closed."
166
+ : "Classifier response was not valid decision JSON; auto mode fails closed.";
167
+ }
168
+ return { decision: "block", tier: "none", reason: lastReason };
169
+ }
170
+
93
171
  export const defaultClassifyAction: ClassifyAction = async (
94
172
  ctx,
95
173
  config,
@@ -120,33 +198,10 @@ export const defaultClassifyAction: ClassifyAction = async (
120
198
  timestamp: Date.now(),
121
199
  };
122
200
 
123
- try {
124
- const response = await complete(
125
- classifier.model,
126
- { systemPrompt: buildClassifierPrompt(config), messages: [userMessage] },
127
- {
128
- apiKey: classifier.apiKey,
129
- headers: classifier.headers,
130
- signal: ctx.signal,
131
- maxTokens: 700,
132
- temperature: 0,
133
- },
134
- );
135
- return (
136
- parseClassifierDecision(response) ?? {
137
- decision: "block",
138
- tier: "none",
139
- reason:
140
- "Classifier response was not valid decision JSON; auto mode fails closed.",
141
- }
142
- );
143
- } catch (error) {
144
- return {
145
- decision: "block",
146
- tier: "none",
147
- reason: `Classifier failed; auto mode fails closed: ${
148
- error instanceof Error ? error.message : String(error)
149
- }`,
150
- };
151
- }
201
+ return classifyWithRetry(
202
+ complete,
203
+ classifier,
204
+ { systemPrompt: buildClassifierPrompt(config), messages: [userMessage] },
205
+ ctx.signal,
206
+ );
152
207
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@czottmann/pi-automode",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Claude Code-style auto mode guardrail for pi.",
5
5
  "repository": {
6
6
  "url": "https://github.com/czottmann/pi-automode"