@cortexkit/opencode-magic-context 0.34.2 → 0.35.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.
- package/README.md +3 -1
- package/dist/agents/magic-context-prompt.d.ts +2 -1
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/project-security.d.ts +3 -0
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +42 -1
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-fvpfvt0w.js → conflict-warning-hook-p2pmhvn7.js} +2 -1
- package/dist/features/magic-context/context-authority.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/provider-output-failure.d.ts +15 -0
- package/dist/features/magic-context/dreamer/provider-output-failure.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/mural/compress-cues.d.ts +6 -2
- package/dist/features/magic-context/mural/compress-cues.d.ts.map +1 -1
- package/dist/features/magic-context/mural/storage-mural-cues.d.ts +4 -4
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +2 -14
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts +16 -2
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +10 -3
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts +1 -1
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +2 -1
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/protected-tail-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +5 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts +2 -0
- package/dist/hooks/magic-context/read-session-true-raw-tokens.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +6 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +19 -5
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +3 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index-0c9dmw9m.js +116 -0
- package/dist/{index-z70pxrq1.js → index-4anesz49.js} +726 -611
- package/dist/{index-dtas91s5.js → index-8aaadh98.js} +7 -119
- package/dist/{index-b563sga5.js → index-h8mk7vpa.js} +541 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +796 -809
- package/dist/plugin/hooks/create-session-hooks.d.ts +12 -0
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +1 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts +4 -0
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/{recomp-orchestrator-6cd3nrt7.js → recomp-orchestrator-zc915ceq.js} +3 -2
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/prompt-surface-runtime.d.ts +43 -0
- package/dist/shared/prompt-surface-runtime.d.ts.map +1 -0
- package/dist/shared/prompt-surface.d.ts +49 -0
- package/dist/shared/prompt-surface.d.ts.map +1 -0
- package/dist/{storage-meta-session-xt41zbw4.js → storage-meta-session-53cjs6mr.js} +1 -1
- package/dist/tools/ctx-expand/tools.d.ts +1 -0
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts +1 -0
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts +1 -0
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +1 -0
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts +1 -0
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/tools/light-descriptions.d.ts +6 -0
- package/dist/tools/light-descriptions.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/agents/magic-context-prompt.test.ts +144 -0
- package/src/agents/magic-context-prompt.ts +53 -2
- package/src/config/index.test.ts +53 -0
- package/src/config/index.ts +4 -0
- package/src/config/project-security.test.ts +20 -0
- package/src/config/project-security.ts +23 -0
- package/src/config/schema/magic-context.test.ts +58 -0
- package/src/config/schema/magic-context.ts +61 -1
- package/src/shared/index.ts +2 -0
- package/src/shared/prompt-surface-a1-golden.md +458 -0
- package/src/shared/prompt-surface-runtime.test.ts +141 -0
- package/src/shared/prompt-surface-runtime.ts +250 -0
- package/src/shared/prompt-surface.test.ts +109 -0
- package/src/shared/prompt-surface.ts +138 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type ConfigHarness,
|
|
6
|
+
cortexKitUserConfigBasePath,
|
|
7
|
+
resolveLegacyConfigSourcesForHarness,
|
|
8
|
+
} from "../config/migrate-config-location";
|
|
9
|
+
import {
|
|
10
|
+
CTX_EXPAND_LIGHT_DESCRIPTION,
|
|
11
|
+
CTX_MEMORY_LIGHT_DESCRIPTION,
|
|
12
|
+
CTX_NOTE_LIGHT_DESCRIPTION,
|
|
13
|
+
CTX_REDUCE_LIGHT_DESCRIPTION,
|
|
14
|
+
CTX_SEARCH_LIGHT_DESCRIPTION,
|
|
15
|
+
} from "../tools/light-descriptions";
|
|
16
|
+
import { detectConfigFile } from "./jsonc-parser";
|
|
17
|
+
import {
|
|
18
|
+
type PromptSurfaceConfig,
|
|
19
|
+
type PromptSurfacePreset,
|
|
20
|
+
resolvePromptSurface,
|
|
21
|
+
} from "./prompt-surface";
|
|
22
|
+
|
|
23
|
+
const GUIDANCE_MARKER = "## Magic Context";
|
|
24
|
+
const GUIDANCE_MARKER_LINE = /^## Magic Context[\t ]*\r?$/gm;
|
|
25
|
+
|
|
26
|
+
export const PROMPT_SURFACE_TOOL_IDS = [
|
|
27
|
+
"ctx_reduce",
|
|
28
|
+
"ctx_expand",
|
|
29
|
+
"ctx_note",
|
|
30
|
+
"ctx_memory",
|
|
31
|
+
"ctx_search",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type PromptSurfaceToolId = (typeof PROMPT_SURFACE_TOOL_IDS)[number];
|
|
35
|
+
|
|
36
|
+
export const LIGHT_TOOL_DESCRIPTIONS: Readonly<Record<PromptSurfaceToolId, string>> = {
|
|
37
|
+
ctx_reduce: CTX_REDUCE_LIGHT_DESCRIPTION,
|
|
38
|
+
ctx_expand: CTX_EXPAND_LIGHT_DESCRIPTION,
|
|
39
|
+
ctx_note: CTX_NOTE_LIGHT_DESCRIPTION,
|
|
40
|
+
ctx_memory: CTX_MEMORY_LIGHT_DESCRIPTION,
|
|
41
|
+
ctx_search: CTX_SEARCH_LIGHT_DESCRIPTION,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Preserve the existing full-preset hash exactly while giving other presets a
|
|
46
|
+
* distinct semantic cache identity, including during any future asset fallback.
|
|
47
|
+
*/
|
|
48
|
+
export function promptSurfaceHashMaterial(
|
|
49
|
+
systemContent: string,
|
|
50
|
+
preset: PromptSurfacePreset = "full",
|
|
51
|
+
): string {
|
|
52
|
+
return preset === "full"
|
|
53
|
+
? systemContent
|
|
54
|
+
: `${systemContent}\n\0magic-context-prompt-surface:${preset}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const PROMPT_SURFACE_TOOL_ID_SET = new Set<string>(PROMPT_SURFACE_TOOL_IDS);
|
|
58
|
+
|
|
59
|
+
export interface PromptSurfaceGuidanceSelection {
|
|
60
|
+
/** The configured built-in preset. */
|
|
61
|
+
preset: PromptSurfacePreset;
|
|
62
|
+
/** Complete user-authored primary section captured when a model-key epoch starts. */
|
|
63
|
+
primaryOverride?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PromptSurfaceRegistrationSelection {
|
|
67
|
+
preset: PromptSurfacePreset;
|
|
68
|
+
descriptionFor: (toolId: string, fullDescription: string) => string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PromptSurfaceRuntime {
|
|
72
|
+
resolveRegistration: (
|
|
73
|
+
config: PromptSurfaceConfig | undefined,
|
|
74
|
+
) => PromptSurfaceRegistrationSelection;
|
|
75
|
+
resolveGuidance: (
|
|
76
|
+
config: PromptSurfaceConfig | undefined,
|
|
77
|
+
modelKey: string | undefined,
|
|
78
|
+
) => PromptSurfaceGuidanceSelection;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CreatePromptSurfaceRuntimeOptions {
|
|
82
|
+
harness?: ConfigHarness;
|
|
83
|
+
directory?: string;
|
|
84
|
+
/** Explicit test/integration seam; production derives the USER config directory. */
|
|
85
|
+
userConfigDirectory?: string;
|
|
86
|
+
warn: (message: string) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveUserConfigDirectory(options: CreatePromptSurfaceRuntimeOptions): string {
|
|
90
|
+
if (options.userConfigDirectory) return resolve(options.userConfigDirectory);
|
|
91
|
+
|
|
92
|
+
const sharedBase = cortexKitUserConfigBasePath();
|
|
93
|
+
const shared = detectConfigFile(sharedBase);
|
|
94
|
+
if (shared.format !== "none") return dirname(shared.path);
|
|
95
|
+
|
|
96
|
+
if (options.harness) {
|
|
97
|
+
const legacy = resolveLegacyConfigSourcesForHarness(
|
|
98
|
+
options.directory ?? process.cwd(),
|
|
99
|
+
options.harness,
|
|
100
|
+
).user.find((source) => existsSync(source.path));
|
|
101
|
+
if (legacy) return dirname(legacy.path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return dirname(sharedBase);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function markerCount(content: string): number {
|
|
108
|
+
return content.match(GUIDANCE_MARKER_LINE)?.length ?? 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create one host-registration runtime. Its warning set is shared by tool
|
|
113
|
+
* registration and every guidance epoch, so invalid overrides are reported once
|
|
114
|
+
* instead of on every model call.
|
|
115
|
+
*/
|
|
116
|
+
export function createPromptSurfaceRuntime(
|
|
117
|
+
options: CreatePromptSurfaceRuntimeOptions,
|
|
118
|
+
): PromptSurfaceRuntime {
|
|
119
|
+
const userConfigDirectory = resolveUserConfigDirectory(options);
|
|
120
|
+
const warned = new Set<string>();
|
|
121
|
+
|
|
122
|
+
const warnOnce = (key: string, message: string): void => {
|
|
123
|
+
if (warned.has(key)) return;
|
|
124
|
+
warned.add(key);
|
|
125
|
+
options.warn(message);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const readGuidanceOverride = (configuredPath: string | undefined): string | undefined => {
|
|
129
|
+
if (!configuredPath) return undefined;
|
|
130
|
+
const path = isAbsolute(configuredPath)
|
|
131
|
+
? configuredPath
|
|
132
|
+
: resolve(userConfigDirectory, configuredPath);
|
|
133
|
+
|
|
134
|
+
let content: string;
|
|
135
|
+
try {
|
|
136
|
+
if (!statSync(path).isFile()) {
|
|
137
|
+
warnOnce(
|
|
138
|
+
`guidance-not-file:${path}`,
|
|
139
|
+
`prompt_surface.guidance_override_path (${path}) is not a file; using built-in guidance.`,
|
|
140
|
+
);
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
content = readFileSync(path, "utf8");
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
146
|
+
warnOnce(
|
|
147
|
+
`guidance-unreadable:${path}:${reason}`,
|
|
148
|
+
`prompt_surface.guidance_override_path (${path}) could not be read (${reason}); using built-in guidance.`,
|
|
149
|
+
);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (content.trim().length === 0) {
|
|
154
|
+
warnOnce(
|
|
155
|
+
`guidance-empty:${path}`,
|
|
156
|
+
`prompt_surface.guidance_override_path (${path}) is empty; using built-in guidance.`,
|
|
157
|
+
);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const markers = markerCount(content);
|
|
162
|
+
if (markers !== 1) {
|
|
163
|
+
warnOnce(
|
|
164
|
+
`guidance-marker-count:${path}:${markers}`,
|
|
165
|
+
`prompt_surface.guidance_override_path (${path}) must contain exactly one ${JSON.stringify(GUIDANCE_MARKER)} section marker; found ${markers}. Using built-in guidance.`,
|
|
166
|
+
);
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return content;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
resolveRegistration(config) {
|
|
175
|
+
// OpenCode and Pi expose one immutable provider tool map per host
|
|
176
|
+
// registration. Model routes are intentionally ignored here: only the
|
|
177
|
+
// registration owner's default and user-tier overrides can choose text.
|
|
178
|
+
const { preset } = resolvePromptSurface(config, undefined);
|
|
179
|
+
|
|
180
|
+
const overrides = config?.tool_descriptions ?? {};
|
|
181
|
+
for (const [toolId, description] of Object.entries(overrides)) {
|
|
182
|
+
if (!PROMPT_SURFACE_TOOL_ID_SET.has(toolId)) {
|
|
183
|
+
warnOnce(
|
|
184
|
+
`unknown-tool:${toolId}`,
|
|
185
|
+
`prompt_surface.tool_descriptions.${toolId} is not a known ctx_* tool ID; the override was ignored.`,
|
|
186
|
+
);
|
|
187
|
+
} else if (description.trim().length === 0) {
|
|
188
|
+
warnOnce(
|
|
189
|
+
`empty-tool:${toolId}`,
|
|
190
|
+
`prompt_surface.tool_descriptions.${toolId} is empty; the override was ignored.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
preset,
|
|
197
|
+
descriptionFor(toolId, fullDescription) {
|
|
198
|
+
if (!PROMPT_SURFACE_TOOL_ID_SET.has(toolId)) return fullDescription;
|
|
199
|
+
const override = overrides[toolId];
|
|
200
|
+
if (override !== undefined && override.trim().length > 0) return override;
|
|
201
|
+
return preset === "light"
|
|
202
|
+
? LIGHT_TOOL_DESCRIPTIONS[toolId as PromptSurfaceToolId]
|
|
203
|
+
: fullDescription;
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
resolveGuidance(config, modelKey) {
|
|
209
|
+
const { preset } = resolvePromptSurface(config, modelKey);
|
|
210
|
+
return {
|
|
211
|
+
preset,
|
|
212
|
+
primaryOverride: readGuidanceOverride(config?.guidance_override_path),
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
interface GuidanceEpoch {
|
|
219
|
+
config: PromptSurfaceConfig | undefined;
|
|
220
|
+
modelKey: string | undefined;
|
|
221
|
+
selection: PromptSurfaceGuidanceSelection;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Freeze preset selection and materialized override bytes for one model-key epoch. */
|
|
225
|
+
export function createPromptSurfaceGuidanceEpochCache(runtime: PromptSurfaceRuntime): {
|
|
226
|
+
resolve: (
|
|
227
|
+
sessionId: string,
|
|
228
|
+
config: PromptSurfaceConfig | undefined,
|
|
229
|
+
modelKey: string | undefined,
|
|
230
|
+
) => PromptSurfaceGuidanceSelection;
|
|
231
|
+
clear: (sessionId: string) => void;
|
|
232
|
+
} {
|
|
233
|
+
const epochs = new Map<string, GuidanceEpoch>();
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
resolve(sessionId, config, modelKey) {
|
|
237
|
+
const cached = epochs.get(sessionId);
|
|
238
|
+
if (cached && cached.config === config && cached.modelKey === modelKey) {
|
|
239
|
+
return cached.selection;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const selection = runtime.resolveGuidance(config, modelKey);
|
|
243
|
+
epochs.set(sessionId, { config, modelKey, selection });
|
|
244
|
+
return selection;
|
|
245
|
+
},
|
|
246
|
+
clear(sessionId) {
|
|
247
|
+
epochs.delete(sessionId);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { resolveCacheTtl } from "../hooks/magic-context/event-resolvers";
|
|
4
|
+
import { modelKeyLookupOrder, resolvePromptSurface } from "./prompt-surface";
|
|
5
|
+
|
|
6
|
+
describe("prompt-surface resolution", () => {
|
|
7
|
+
it("keeps cache_ttl and prompt-surface routing on the same lookup walk", () => {
|
|
8
|
+
const routes = {
|
|
9
|
+
"anthropic/claude/sonnet": "light" as const,
|
|
10
|
+
"openai/gpt-4o": "light" as const,
|
|
11
|
+
"google/*": "light" as const,
|
|
12
|
+
"CaseSensitive/model": "light" as const,
|
|
13
|
+
"progressive/base": "light" as const,
|
|
14
|
+
};
|
|
15
|
+
const cacheTtl = {
|
|
16
|
+
default: "full",
|
|
17
|
+
...Object.fromEntries(Object.entries(routes).map(([key, preset]) => [key, preset])),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const cases = [
|
|
21
|
+
["anthropic/claude/sonnet", "light"],
|
|
22
|
+
["anthropic/claude/other", "full"],
|
|
23
|
+
["openai/gpt-4o", "light"],
|
|
24
|
+
["openai/gpt-4o-mini", "light"],
|
|
25
|
+
["google/gemini-pro", "light"],
|
|
26
|
+
["casesensitive/model", "full"],
|
|
27
|
+
["CaseSensitive/model", "light"],
|
|
28
|
+
["progressive/base-extra", "light"],
|
|
29
|
+
["unknown/model", "full"],
|
|
30
|
+
[undefined, "full"],
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
for (const [modelKey, expected] of cases) {
|
|
34
|
+
const ttl = resolveCacheTtl(cacheTtl, modelKey);
|
|
35
|
+
const prompt = resolvePromptSurface({ default: "full", models: routes }, modelKey);
|
|
36
|
+
|
|
37
|
+
expect(ttl).toBe(expected);
|
|
38
|
+
expect(prompt.preset).toBe(expected);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("applies exact, wildcard, then default precedence", () => {
|
|
43
|
+
const config = {
|
|
44
|
+
default: "full" as const,
|
|
45
|
+
models: {
|
|
46
|
+
"provider/model": "light" as const,
|
|
47
|
+
"provider/*": "full" as const,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
expect(resolvePromptSurface(config, "provider/model")).toEqual({
|
|
52
|
+
preset: "light",
|
|
53
|
+
source: "exact",
|
|
54
|
+
});
|
|
55
|
+
expect(resolvePromptSurface(config, "provider/other")).toEqual({
|
|
56
|
+
preset: "full",
|
|
57
|
+
source: "wildcard",
|
|
58
|
+
});
|
|
59
|
+
expect(resolvePromptSurface(config, "other/model")).toEqual({
|
|
60
|
+
preset: "full",
|
|
61
|
+
source: "default",
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("preserves multi-slash model IDs and treats case differences literally", () => {
|
|
66
|
+
expect(modelKeyLookupOrder("provider/model/with/slashes")[0]).toEqual({
|
|
67
|
+
key: "provider/model/with/slashes",
|
|
68
|
+
source: "exact",
|
|
69
|
+
});
|
|
70
|
+
expect(
|
|
71
|
+
resolvePromptSurface(
|
|
72
|
+
{
|
|
73
|
+
default: "full",
|
|
74
|
+
models: { "provider/model/with/slashes": "light" },
|
|
75
|
+
},
|
|
76
|
+
"provider/model/with/slashes",
|
|
77
|
+
),
|
|
78
|
+
).toEqual({ preset: "light", source: "exact" });
|
|
79
|
+
expect(
|
|
80
|
+
resolvePromptSurface(
|
|
81
|
+
{
|
|
82
|
+
default: "full",
|
|
83
|
+
models: { "Provider/*": "light" },
|
|
84
|
+
},
|
|
85
|
+
"provider/model/with/slashes",
|
|
86
|
+
),
|
|
87
|
+
).toEqual({ preset: "full", source: "default" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("falls back when provider or model components are absent", () => {
|
|
91
|
+
const config = {
|
|
92
|
+
default: "light" as const,
|
|
93
|
+
models: { "provider/*": "full" as const },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
expect(resolvePromptSurface(config, "provider")).toEqual({
|
|
97
|
+
preset: "light",
|
|
98
|
+
source: "default",
|
|
99
|
+
});
|
|
100
|
+
expect(resolvePromptSurface(config, "/model")).toEqual({
|
|
101
|
+
preset: "light",
|
|
102
|
+
source: "default",
|
|
103
|
+
});
|
|
104
|
+
expect(resolvePromptSurface(config, "provider/")).toEqual({
|
|
105
|
+
preset: "light",
|
|
106
|
+
source: "default",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** The built-in prompt-surface variants. */
|
|
2
|
+
export type PromptSurfacePreset = "full" | "light";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The configuration consumed by prompt-surface resolution. The schema adds
|
|
6
|
+
* validation and defaults; this structural type keeps the resolver usable by
|
|
7
|
+
* every host without importing a config loader.
|
|
8
|
+
*/
|
|
9
|
+
export interface PromptSurfaceConfig {
|
|
10
|
+
default?: PromptSurfacePreset;
|
|
11
|
+
models?: Readonly<Record<string, PromptSurfacePreset>>;
|
|
12
|
+
guidance_override_path?: string;
|
|
13
|
+
tool_descriptions?: Readonly<Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PromptSurfaceResolutionSource = "exact" | "wildcard" | "default";
|
|
17
|
+
|
|
18
|
+
/** Validate the literal provider/model and provider/* key grammar. */
|
|
19
|
+
export function isValidPromptSurfaceModelKey(key: string): boolean {
|
|
20
|
+
if (key.length === 0 || key.trim() !== key) return false;
|
|
21
|
+
|
|
22
|
+
const slash = key.indexOf("/");
|
|
23
|
+
if (slash <= 0 || slash === key.length - 1) return false;
|
|
24
|
+
|
|
25
|
+
const provider = key.slice(0, slash);
|
|
26
|
+
const modelID = key.slice(slash + 1);
|
|
27
|
+
if (
|
|
28
|
+
provider.trim() !== provider ||
|
|
29
|
+
modelID.trim() !== modelID ||
|
|
30
|
+
provider.includes("*") ||
|
|
31
|
+
(modelID.includes("*") && modelID !== "*")
|
|
32
|
+
) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (modelID === "*") return true;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
modelID.length > 0 &&
|
|
39
|
+
!modelID.startsWith("/") &&
|
|
40
|
+
!modelID.endsWith("/") &&
|
|
41
|
+
!modelID.includes("//")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ModelKeyLookupSource = PromptSurfaceResolutionSource | "bare";
|
|
46
|
+
|
|
47
|
+
export interface ModelKeyCandidate {
|
|
48
|
+
key: string;
|
|
49
|
+
source: ModelKeyLookupSource;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the same progressive model-key candidates used by cache_ttl. The
|
|
54
|
+
* provider/model boundary is the first slash; the rest of the string remains
|
|
55
|
+
* the model ID, including additional slashes. Candidates are case-sensitive.
|
|
56
|
+
*
|
|
57
|
+
* Provider wildcards are checked after progressively less-specific model keys,
|
|
58
|
+
* but before the caller's default. That keeps an exact or base-model override
|
|
59
|
+
* authoritative while still allowing `provider/*` to cover otherwise-unlisted
|
|
60
|
+
* models.
|
|
61
|
+
*/
|
|
62
|
+
export function modelKeyLookupOrder(modelKey: string | undefined): ModelKeyCandidate[] {
|
|
63
|
+
if (!modelKey) return [];
|
|
64
|
+
|
|
65
|
+
const slash = modelKey.indexOf("/");
|
|
66
|
+
if (slash <= 0 || slash === modelKey.length - 1) return [];
|
|
67
|
+
|
|
68
|
+
const provider = modelKey.slice(0, slash);
|
|
69
|
+
let modelID = modelKey.slice(slash + 1);
|
|
70
|
+
const candidates: ModelKeyCandidate[] = [];
|
|
71
|
+
|
|
72
|
+
while (modelID.length > 0) {
|
|
73
|
+
candidates.push({ key: `${provider}/${modelID}`, source: "exact" });
|
|
74
|
+
candidates.push({ key: modelID, source: "bare" });
|
|
75
|
+
|
|
76
|
+
const lastDash = modelID.lastIndexOf("-");
|
|
77
|
+
if (lastDash <= 0) break;
|
|
78
|
+
modelID = modelID.slice(0, lastDash);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
candidates.push({ key: `${provider}/*`, source: "wildcard" });
|
|
82
|
+
|
|
83
|
+
const seen = new Set<string>();
|
|
84
|
+
return candidates.filter((candidate) => {
|
|
85
|
+
if (seen.has(candidate.key)) return false;
|
|
86
|
+
seen.add(candidate.key);
|
|
87
|
+
return true;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve one per-model value using the shared cache_ttl lookup walk. */
|
|
92
|
+
export function resolveModelConfigValue<T>(
|
|
93
|
+
values: Readonly<Record<string, T>> | undefined,
|
|
94
|
+
modelKey: string | undefined,
|
|
95
|
+
): { value: T; source: ModelKeyLookupSource } | undefined {
|
|
96
|
+
if (!values) return undefined;
|
|
97
|
+
|
|
98
|
+
for (const candidate of modelKeyLookupOrder(modelKey)) {
|
|
99
|
+
const value = values[candidate.key];
|
|
100
|
+
if (value !== undefined) {
|
|
101
|
+
return { value, source: candidate.source };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the prompt preset for a model. Invalid or missing model keys use the
|
|
110
|
+
* configured default and are reported as a default resolution rather than a
|
|
111
|
+
* partial match.
|
|
112
|
+
*/
|
|
113
|
+
export function resolvePromptSurface(
|
|
114
|
+
config: PromptSurfaceConfig | undefined,
|
|
115
|
+
modelKey: string | undefined,
|
|
116
|
+
): { preset: PromptSurfacePreset; source: PromptSurfaceResolutionSource } {
|
|
117
|
+
const fallback = config?.default ?? "full";
|
|
118
|
+
const match = resolveModelConfigValue(config?.models, modelKey);
|
|
119
|
+
|
|
120
|
+
// Bare model keys are accepted by cache_ttl for compatibility, but the
|
|
121
|
+
// prompt_surface schema deliberately accepts only provider/model keys and
|
|
122
|
+
// provider wildcards. Ignore a bare entry if an unvalidated object reaches
|
|
123
|
+
// this API so prompt routing keeps its documented grammar.
|
|
124
|
+
if (match && match.source !== "bare") {
|
|
125
|
+
return { preset: match.value, source: match.source };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { preset: fallback, source: "default" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Resolve a cache_ttl-style value with the shared model-key walk. */
|
|
132
|
+
export function resolveModelConfigOrDefault<T>(
|
|
133
|
+
values: Readonly<Record<string, T>>,
|
|
134
|
+
modelKey: string | undefined,
|
|
135
|
+
fallback: T,
|
|
136
|
+
): T {
|
|
137
|
+
return resolveModelConfigValue(values, modelKey)?.value ?? fallback;
|
|
138
|
+
}
|