@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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
};
|