pi-openai-toolkit 0.14.4 → 0.14.6

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.
Files changed (43) hide show
  1. package/README.md +119 -147
  2. package/README.zh.md +119 -151
  3. package/docs/internals.md +42 -21
  4. package/package.json +127 -127
  5. package/src/auto-mode/classifier.ts +165 -165
  6. package/src/auto-mode/policy.ts +146 -146
  7. package/src/auto-mode/prompt.ts +279 -279
  8. package/src/auto-mode/reviewer.ts +325 -325
  9. package/src/auto-mode/transcript.ts +133 -133
  10. package/src/auto-mode/types.ts +137 -137
  11. package/src/codex-astra/effort-planner.ts +175 -175
  12. package/src/codex-astra/extension.ts +122 -122
  13. package/src/config.ts +621 -592
  14. package/src/context-management/codex-provider.ts +260 -260
  15. package/src/context-management/history-notes.ts +357 -325
  16. package/src/context-management/messages.ts +112 -106
  17. package/src/context-management/namespace-tools.ts +426 -426
  18. package/src/context-management/tool-contract.ts +23 -23
  19. package/src/context-management/tools.ts +306 -311
  20. package/src/context-management/types.ts +168 -161
  21. package/src/context-management/window-budget.ts +83 -83
  22. package/src/context-management/window-manager.ts +60 -51
  23. package/src/context-management/window-request.ts +125 -125
  24. package/src/debug.ts +1 -1
  25. package/src/extension-runtime.ts +34 -9
  26. package/src/image-generation/artifacts.ts +237 -237
  27. package/src/image-generation/client.ts +248 -248
  28. package/src/image-generation/eligibility.ts +18 -18
  29. package/src/image-generation/extension.ts +19 -2
  30. package/src/image-generation/protocol.ts +473 -405
  31. package/src/image-generation/references.ts +194 -194
  32. package/src/image-generation/render.ts +252 -252
  33. package/src/image-generation/service.ts +249 -243
  34. package/src/image-generation/types.ts +189 -184
  35. package/src/model-scope.ts +17 -17
  36. package/src/provider-headers.ts +23 -23
  37. package/src/responses-headers.ts +173 -108
  38. package/src/runtime.ts +513 -477
  39. package/src/types.ts +11 -0
  40. package/src/web-search/extension.ts +80 -80
  41. package/src/web-search/payload.ts +96 -96
  42. package/src/web-search/prompt.ts +20 -20
  43. package/src/web-search/types.ts +45 -45
package/package.json CHANGED
@@ -1,127 +1,127 @@
1
- {
2
- "name": "pi-openai-toolkit",
3
- "version": "0.14.4",
4
- "private": false,
5
- "type": "module",
6
- "engines": {
7
- "node": ">=22.19.0"
8
- },
9
- "description": "OpenAI toolkit for Pi: Codex Remote Context windows, remote compaction v2, hosted Web Search, image gen, auto mode.",
10
- "author": "awoaCrim and contributors",
11
- "license": "MIT",
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/awoaCrim/pi-openai-toolkit.git"
15
- },
16
- "bugs": {
17
- "url": "https://github.com/awoaCrim/pi-openai-toolkit/issues"
18
- },
19
- "homepage": "https://github.com/awoaCrim/pi-openai-toolkit#readme",
20
- "publishConfig": {
21
- "access": "public",
22
- "registry": "https://registry.npmjs.org/"
23
- },
24
- "keywords": [
25
- "pi-package",
26
- "pi-extension",
27
- "openai",
28
- "compaction",
29
- "web-search",
30
- "image-generation",
31
- "auto-mode",
32
- "codex",
33
- "responses"
34
- ],
35
- "pi": {
36
- "extensions": [
37
- "./extensions/compaction.ts",
38
- "./extensions/web-search.ts",
39
- "./extensions/image-generation.ts",
40
- "./extensions/auto-mode.ts",
41
- "./extensions/codex-astra.ts"
42
- ]
43
- },
44
- "files": [
45
- "extensions/compaction.ts",
46
- "extensions/web-search.ts",
47
- "extensions/image-generation.ts",
48
- "extensions/auto-mode.ts",
49
- "src/compact-client.ts",
50
- "src/config.ts",
51
- "src/debug.ts",
52
- "src/details-store.ts",
53
- "src/deferred-tool-carryover.ts",
54
- "src/extension-runtime.ts",
55
- "src/context-management/types.ts",
56
- "src/context-management/messages.ts",
57
- "src/context-management/window-budget.ts",
58
- "src/context-management/window-request.ts",
59
- "src/context-management/window-manager.ts",
60
- "src/context-management/codex-provider.ts",
61
- "src/context-management/history-notes.ts",
62
- "src/context-management/tools.ts",
63
- "src/context-management/namespace-tools.ts",
64
- "src/context-management/tool-contract.ts",
65
- "src/image-generation/artifacts.ts",
66
- "src/image-generation/client.ts",
67
- "src/image-generation/eligibility.ts",
68
- "src/image-generation/extension.ts",
69
- "src/image-generation/protocol.ts",
70
- "src/image-generation/references.ts",
71
- "src/image-generation/render.ts",
72
- "src/image-generation/service.ts",
73
- "src/image-generation/types.ts",
74
- "src/auto-mode/extension.ts",
75
- "src/auto-mode/policy.ts",
76
- "src/auto-mode/prompt.ts",
77
- "src/auto-mode/transcript.ts",
78
- "src/auto-mode/reviewer.ts",
79
- "src/auto-mode/classifier.ts",
80
- "src/auto-mode/types.ts",
81
- "extensions/codex-astra.ts",
82
- "src/codex-astra/effort-planner.ts",
83
- "src/codex-astra/extension.ts",
84
- "src/model-scope.ts",
85
- "src/native-fallback.ts",
86
- "src/payload-rewrite.ts",
87
- "src/provider-headers.ts",
88
- "src/responses-headers.ts",
89
- "src/remote-v2-client.ts",
90
- "src/request-context-cache.ts",
91
- "src/runtime.ts",
92
- "src/serializer.ts",
93
- "src/supported-environment.ts",
94
- "src/types.ts",
95
- "src/web-search/extension.ts",
96
- "src/web-search/payload.ts",
97
- "src/web-search/prompt.ts",
98
- "src/web-search/types.ts",
99
- "docs/internals.md",
100
- "README.md",
101
- "README.zh.md",
102
- "LICENSE",
103
- "NOTICE",
104
- "tsconfig.check.json"
105
- ],
106
- "scripts": {
107
- "test": "bun test",
108
- "test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && node ./scripts/check-lcov.mjs ./coverage/lcov.info",
109
- "test:pi": "bun test ./test/pi-smoke.test.ts",
110
- "typecheck": "tsc -p tsconfig.check.json"
111
- },
112
- "peerDependencies": {
113
- "@earendil-works/pi-agent-core": ">=0.85.1",
114
- "@earendil-works/pi-ai": ">=0.85.1",
115
- "@earendil-works/pi-coding-agent": ">=0.85.1",
116
- "@earendil-works/pi-tui": ">=0.85.1"
117
- },
118
- "devDependencies": {
119
- "@earendil-works/pi-agent-core": "0.85.1",
120
- "@earendil-works/pi-ai": "0.85.1",
121
- "@earendil-works/pi-coding-agent": "0.85.1",
122
- "@earendil-works/pi-tui": "0.85.1",
123
- "@types/node": "24.12.4",
124
- "bun": "1.2.23",
125
- "typescript": "5.9.3"
126
- }
127
- }
1
+ {
2
+ "name": "pi-openai-toolkit",
3
+ "version": "0.14.6",
4
+ "private": false,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "description": "OpenAI toolkit for Pi: Codex Remote Context windows, remote compaction v2, hosted Web Search, image gen, auto mode.",
10
+ "author": "awoaCrim and contributors",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/awoaCrim/pi-openai-toolkit.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/awoaCrim/pi-openai-toolkit/issues"
18
+ },
19
+ "homepage": "https://github.com/awoaCrim/pi-openai-toolkit#readme",
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org/"
23
+ },
24
+ "keywords": [
25
+ "pi-package",
26
+ "pi-extension",
27
+ "openai",
28
+ "compaction",
29
+ "web-search",
30
+ "image-generation",
31
+ "auto-mode",
32
+ "codex",
33
+ "responses"
34
+ ],
35
+ "pi": {
36
+ "extensions": [
37
+ "./extensions/compaction.ts",
38
+ "./extensions/web-search.ts",
39
+ "./extensions/image-generation.ts",
40
+ "./extensions/auto-mode.ts",
41
+ "./extensions/codex-astra.ts"
42
+ ]
43
+ },
44
+ "files": [
45
+ "extensions/compaction.ts",
46
+ "extensions/web-search.ts",
47
+ "extensions/image-generation.ts",
48
+ "extensions/auto-mode.ts",
49
+ "src/compact-client.ts",
50
+ "src/config.ts",
51
+ "src/debug.ts",
52
+ "src/details-store.ts",
53
+ "src/deferred-tool-carryover.ts",
54
+ "src/extension-runtime.ts",
55
+ "src/context-management/types.ts",
56
+ "src/context-management/messages.ts",
57
+ "src/context-management/window-budget.ts",
58
+ "src/context-management/window-request.ts",
59
+ "src/context-management/window-manager.ts",
60
+ "src/context-management/codex-provider.ts",
61
+ "src/context-management/history-notes.ts",
62
+ "src/context-management/tools.ts",
63
+ "src/context-management/namespace-tools.ts",
64
+ "src/context-management/tool-contract.ts",
65
+ "src/image-generation/artifacts.ts",
66
+ "src/image-generation/client.ts",
67
+ "src/image-generation/eligibility.ts",
68
+ "src/image-generation/extension.ts",
69
+ "src/image-generation/protocol.ts",
70
+ "src/image-generation/references.ts",
71
+ "src/image-generation/render.ts",
72
+ "src/image-generation/service.ts",
73
+ "src/image-generation/types.ts",
74
+ "src/auto-mode/extension.ts",
75
+ "src/auto-mode/policy.ts",
76
+ "src/auto-mode/prompt.ts",
77
+ "src/auto-mode/transcript.ts",
78
+ "src/auto-mode/reviewer.ts",
79
+ "src/auto-mode/classifier.ts",
80
+ "src/auto-mode/types.ts",
81
+ "extensions/codex-astra.ts",
82
+ "src/codex-astra/effort-planner.ts",
83
+ "src/codex-astra/extension.ts",
84
+ "src/model-scope.ts",
85
+ "src/native-fallback.ts",
86
+ "src/payload-rewrite.ts",
87
+ "src/provider-headers.ts",
88
+ "src/responses-headers.ts",
89
+ "src/remote-v2-client.ts",
90
+ "src/request-context-cache.ts",
91
+ "src/runtime.ts",
92
+ "src/serializer.ts",
93
+ "src/supported-environment.ts",
94
+ "src/types.ts",
95
+ "src/web-search/extension.ts",
96
+ "src/web-search/payload.ts",
97
+ "src/web-search/prompt.ts",
98
+ "src/web-search/types.ts",
99
+ "docs/internals.md",
100
+ "README.md",
101
+ "README.zh.md",
102
+ "LICENSE",
103
+ "NOTICE",
104
+ "tsconfig.check.json"
105
+ ],
106
+ "scripts": {
107
+ "test": "bun test",
108
+ "test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && node ./scripts/check-lcov.mjs ./coverage/lcov.info",
109
+ "test:pi": "bun test ./test/pi-smoke.test.ts",
110
+ "typecheck": "tsc -p tsconfig.check.json"
111
+ },
112
+ "peerDependencies": {
113
+ "@earendil-works/pi-agent-core": ">=0.85.1",
114
+ "@earendil-works/pi-ai": ">=0.85.1",
115
+ "@earendil-works/pi-coding-agent": ">=0.85.1",
116
+ "@earendil-works/pi-tui": ">=0.85.1"
117
+ },
118
+ "devDependencies": {
119
+ "@earendil-works/pi-agent-core": "0.85.1",
120
+ "@earendil-works/pi-ai": "0.85.1",
121
+ "@earendil-works/pi-coding-agent": "0.85.1",
122
+ "@earendil-works/pi-tui": "0.85.1",
123
+ "@types/node": "24.12.4",
124
+ "bun": "1.2.23",
125
+ "typescript": "5.9.3"
126
+ }
127
+ }
@@ -1,165 +1,165 @@
1
- import type { Api, Model } from "@earendil-works/pi-ai";
2
- import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
3
- import { parseModelSpec } from "../runtime";
4
- import { CLASSIFIER_SYSTEM_PROMPT } from "./prompt";
5
- import { boundReviewText, type ClassifierRisk, type FastDecisionReason } from "./types";
6
-
7
- /** One completed classification of the trajectory, tagged to a tool-call position. */
8
- export type ScoreRecord = {
9
- risk: ClassifierRisk;
10
- /** Tool-call counter value when the sample was taken. */
11
- scoredAtCall: number;
12
- /** Fingerprint of the user turns the score was based on. */
13
- authorizationVersion: string;
14
- sampledAt: number;
15
- };
16
-
17
- /**
18
- * Mutable per-session tracker for the non-blocking pre-scorer.
19
- *
20
- * The counters reproduce Codex's staleness rule: the agent keeps running while a
21
- * classification is in flight, so a score is only trustworthy if it is recent
22
- * enough and nothing the user said has changed underneath it.
23
- */
24
- export type ScoreTracker = {
25
- latestCallIndex: number;
26
- latestScoredIndex: number;
27
- latestFailedIndex: number;
28
- score?: ScoreRecord;
29
- };
30
-
31
- export function createScoreTracker(): ScoreTracker {
32
- return { latestCallIndex: 0, latestScoredIndex: 0, latestFailedIndex: 0 };
33
- }
34
-
35
- /** Reset everything: a new turn or a disengaged mode invalidates prior scoring. */
36
- export function resetScoreTracker(tracker: ScoreTracker): void {
37
- tracker.latestCallIndex = 0;
38
- tracker.latestScoredIndex = 0;
39
- tracker.latestFailedIndex = 0;
40
- tracker.score = undefined;
41
- }
42
-
43
- /**
44
- * Fingerprint of the user-authored conversation. Any new or edited user turn makes
45
- * an existing score stale, because the score's authorization premise changed.
46
- */
47
- export function authorizationVersion(userText: string): string {
48
- const bounded = boundReviewText(userText, 8_000);
49
- let hash = 2166136261;
50
- for (let index = 0; index < bounded.length; index += 1) {
51
- hash ^= bounded.charCodeAt(index);
52
- hash = Math.imul(hash, 16777619);
53
- }
54
- return `${bounded.length}:${(hash >>> 0).toString(36)}`;
55
- }
56
-
57
- export function recordScoredCall(tracker: ScoreTracker, score: ScoreRecord): void {
58
- tracker.score = score;
59
- tracker.latestScoredIndex = Math.max(tracker.latestScoredIndex, score.scoredAtCall);
60
- }
61
-
62
- export function recordFailedCall(tracker: ScoreTracker, scoredAtCall: number): void {
63
- tracker.latestFailedIndex = Math.max(tracker.latestFailedIndex, scoredAtCall);
64
- }
65
-
66
- export type FastDecisionInput = {
67
- tracker: ScoreTracker;
68
- /** Tool-call counter value for the call being gated. */
69
- currentCallIndex: number;
70
- authorizationVersion: string;
71
- maxLag: number;
72
- /** Retries after a denial and widened gates must not take the fast path. */
73
- requiresSynchronousReview?: boolean;
74
- };
75
-
76
- /**
77
- * Decide whether a cached classification can satisfy this gated call without
78
- * running the blocking reviewer. Anything other than `low_risk` defers to the
79
- * synchronous review; the classifier never denies an action on its own.
80
- */
81
- export function fastApprovalEligible(input: FastDecisionInput): {
82
- eligible: boolean;
83
- reason: FastDecisionReason;
84
- } {
85
- const defer = (reason: FastDecisionReason) => ({ eligible: false, reason });
86
-
87
- if (input.requiresSynchronousReview) return defer("requires_synchronous_review");
88
- if (input.tracker.latestFailedIndex > input.tracker.latestScoredIndex) return defer("scoring_failure");
89
-
90
- const score = input.tracker.score;
91
- if (!score) return defer("missing_score");
92
-
93
- const lag = input.currentCallIndex - score.scoredAtCall;
94
- if (lag > input.maxLag) return defer("stale_score");
95
- if (score.authorizationVersion !== input.authorizationVersion) return defer("authorization_changed");
96
- if (score.risk === "low") return { eligible: true, reason: "low_risk" };
97
- return defer("elevated_risk");
98
- }
99
-
100
- export type ClassificationResult =
101
- | { kind: ClassifierRisk; model: string }
102
- | { kind: "failed"; reason: string };
103
-
104
- /**
105
- * Run one non-blocking classification. Output is a single token; anything else is a
106
- * failed sample rather than a verdict, so a confused classifier costs a review and
107
- * never an approval or a denial.
108
- */
109
- export async function classifyTrajectory(params: {
110
- registry: Pick<ModelRegistry, "find" | "complete">;
111
- modelSpec: string;
112
- prompt: string;
113
- timeoutMs: number;
114
- signal?: AbortSignal;
115
- }): Promise<ClassificationResult> {
116
- const parsed = parseModelSpec(params.modelSpec);
117
- if (!parsed) {
118
- return { kind: "failed", reason: `autoMode.classifier.model "${params.modelSpec}" is not provider/model-id.` };
119
- }
120
- const model = params.registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
121
- if (!model) {
122
- return { kind: "failed", reason: `Classifier model ${params.modelSpec} is not available.` };
123
- }
124
- const label = boundReviewText(`${model.provider}/${model.id}`, 256);
125
-
126
- const controller = new AbortController();
127
- const abortFromCaller = () => controller.abort();
128
- if (params.signal?.aborted) controller.abort();
129
- else params.signal?.addEventListener("abort", abortFromCaller);
130
- const timer = setTimeout(() => controller.abort(), params.timeoutMs);
131
-
132
- try {
133
- const response = await params.registry.complete(
134
- model,
135
- {
136
- systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
137
- messages: [
138
- {
139
- role: "user",
140
- content: [{ type: "text", text: params.prompt }],
141
- timestamp: Date.now(),
142
- },
143
- ],
144
- },
145
- { signal: controller.signal, cacheRetention: "none" },
146
- );
147
- if (response.stopReason === "error" || response.stopReason === "aborted") {
148
- return { kind: "failed", reason: response.errorMessage ?? `Classifier stopped: ${response.stopReason}` };
149
- }
150
- const text = response.content
151
- .filter((block): block is { type: "text"; text: string } => block.type === "text")
152
- .map((block) => block.text)
153
- .join("")
154
- .trim()
155
- .toLowerCase();
156
- if (/^high\b/.test(text)) return { kind: "high", model: label };
157
- if (/^low\b/.test(text)) return { kind: "low", model: label };
158
- return { kind: "failed", reason: `Classifier returned unreadable output: ${boundReviewText(text, 80)}` };
159
- } catch (error) {
160
- return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
161
- } finally {
162
- clearTimeout(timer);
163
- params.signal?.removeEventListener("abort", abortFromCaller);
164
- }
165
- }
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
3
+ import { parseModelSpec } from "../runtime";
4
+ import { CLASSIFIER_SYSTEM_PROMPT } from "./prompt";
5
+ import { boundReviewText, type ClassifierRisk, type FastDecisionReason } from "./types";
6
+
7
+ /** One completed classification of the trajectory, tagged to a tool-call position. */
8
+ export type ScoreRecord = {
9
+ risk: ClassifierRisk;
10
+ /** Tool-call counter value when the sample was taken. */
11
+ scoredAtCall: number;
12
+ /** Fingerprint of the user turns the score was based on. */
13
+ authorizationVersion: string;
14
+ sampledAt: number;
15
+ };
16
+
17
+ /**
18
+ * Mutable per-session tracker for the non-blocking pre-scorer.
19
+ *
20
+ * The counters reproduce Codex's staleness rule: the agent keeps running while a
21
+ * classification is in flight, so a score is only trustworthy if it is recent
22
+ * enough and nothing the user said has changed underneath it.
23
+ */
24
+ export type ScoreTracker = {
25
+ latestCallIndex: number;
26
+ latestScoredIndex: number;
27
+ latestFailedIndex: number;
28
+ score?: ScoreRecord;
29
+ };
30
+
31
+ export function createScoreTracker(): ScoreTracker {
32
+ return { latestCallIndex: 0, latestScoredIndex: 0, latestFailedIndex: 0 };
33
+ }
34
+
35
+ /** Reset everything: a new turn or a disengaged mode invalidates prior scoring. */
36
+ export function resetScoreTracker(tracker: ScoreTracker): void {
37
+ tracker.latestCallIndex = 0;
38
+ tracker.latestScoredIndex = 0;
39
+ tracker.latestFailedIndex = 0;
40
+ tracker.score = undefined;
41
+ }
42
+
43
+ /**
44
+ * Fingerprint of the user-authored conversation. Any new or edited user turn makes
45
+ * an existing score stale, because the score's authorization premise changed.
46
+ */
47
+ export function authorizationVersion(userText: string): string {
48
+ const bounded = boundReviewText(userText, 8_000);
49
+ let hash = 2166136261;
50
+ for (let index = 0; index < bounded.length; index += 1) {
51
+ hash ^= bounded.charCodeAt(index);
52
+ hash = Math.imul(hash, 16777619);
53
+ }
54
+ return `${bounded.length}:${(hash >>> 0).toString(36)}`;
55
+ }
56
+
57
+ export function recordScoredCall(tracker: ScoreTracker, score: ScoreRecord): void {
58
+ tracker.score = score;
59
+ tracker.latestScoredIndex = Math.max(tracker.latestScoredIndex, score.scoredAtCall);
60
+ }
61
+
62
+ export function recordFailedCall(tracker: ScoreTracker, scoredAtCall: number): void {
63
+ tracker.latestFailedIndex = Math.max(tracker.latestFailedIndex, scoredAtCall);
64
+ }
65
+
66
+ export type FastDecisionInput = {
67
+ tracker: ScoreTracker;
68
+ /** Tool-call counter value for the call being gated. */
69
+ currentCallIndex: number;
70
+ authorizationVersion: string;
71
+ maxLag: number;
72
+ /** Retries after a denial and widened gates must not take the fast path. */
73
+ requiresSynchronousReview?: boolean;
74
+ };
75
+
76
+ /**
77
+ * Decide whether a cached classification can satisfy this gated call without
78
+ * running the blocking reviewer. Anything other than `low_risk` defers to the
79
+ * synchronous review; the classifier never denies an action on its own.
80
+ */
81
+ export function fastApprovalEligible(input: FastDecisionInput): {
82
+ eligible: boolean;
83
+ reason: FastDecisionReason;
84
+ } {
85
+ const defer = (reason: FastDecisionReason) => ({ eligible: false, reason });
86
+
87
+ if (input.requiresSynchronousReview) return defer("requires_synchronous_review");
88
+ if (input.tracker.latestFailedIndex > input.tracker.latestScoredIndex) return defer("scoring_failure");
89
+
90
+ const score = input.tracker.score;
91
+ if (!score) return defer("missing_score");
92
+
93
+ const lag = input.currentCallIndex - score.scoredAtCall;
94
+ if (lag > input.maxLag) return defer("stale_score");
95
+ if (score.authorizationVersion !== input.authorizationVersion) return defer("authorization_changed");
96
+ if (score.risk === "low") return { eligible: true, reason: "low_risk" };
97
+ return defer("elevated_risk");
98
+ }
99
+
100
+ export type ClassificationResult =
101
+ | { kind: ClassifierRisk; model: string }
102
+ | { kind: "failed"; reason: string };
103
+
104
+ /**
105
+ * Run one non-blocking classification. Output is a single token; anything else is a
106
+ * failed sample rather than a verdict, so a confused classifier costs a review and
107
+ * never an approval or a denial.
108
+ */
109
+ export async function classifyTrajectory(params: {
110
+ registry: Pick<ModelRegistry, "find" | "complete">;
111
+ modelSpec: string;
112
+ prompt: string;
113
+ timeoutMs: number;
114
+ signal?: AbortSignal;
115
+ }): Promise<ClassificationResult> {
116
+ const parsed = parseModelSpec(params.modelSpec);
117
+ if (!parsed) {
118
+ return { kind: "failed", reason: `autoMode.classifier.model "${params.modelSpec}" is not provider/model-id.` };
119
+ }
120
+ const model = params.registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
121
+ if (!model) {
122
+ return { kind: "failed", reason: `Classifier model ${params.modelSpec} is not available.` };
123
+ }
124
+ const label = boundReviewText(`${model.provider}/${model.id}`, 256);
125
+
126
+ const controller = new AbortController();
127
+ const abortFromCaller = () => controller.abort();
128
+ if (params.signal?.aborted) controller.abort();
129
+ else params.signal?.addEventListener("abort", abortFromCaller);
130
+ const timer = setTimeout(() => controller.abort(), params.timeoutMs);
131
+
132
+ try {
133
+ const response = await params.registry.complete(
134
+ model,
135
+ {
136
+ systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
137
+ messages: [
138
+ {
139
+ role: "user",
140
+ content: [{ type: "text", text: params.prompt }],
141
+ timestamp: Date.now(),
142
+ },
143
+ ],
144
+ },
145
+ { signal: controller.signal, cacheRetention: "none" },
146
+ );
147
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
148
+ return { kind: "failed", reason: response.errorMessage ?? `Classifier stopped: ${response.stopReason}` };
149
+ }
150
+ const text = response.content
151
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
152
+ .map((block) => block.text)
153
+ .join("")
154
+ .trim()
155
+ .toLowerCase();
156
+ if (/^high\b/.test(text)) return { kind: "high", model: label };
157
+ if (/^low\b/.test(text)) return { kind: "low", model: label };
158
+ return { kind: "failed", reason: `Classifier returned unreadable output: ${boundReviewText(text, 80)}` };
159
+ } catch (error) {
160
+ return { kind: "failed", reason: error instanceof Error ? error.message : String(error) };
161
+ } finally {
162
+ clearTimeout(timer);
163
+ params.signal?.removeEventListener("abort", abortFromCaller);
164
+ }
165
+ }