jeopi-cli 16.3.0 → 16.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.
- package/CHANGELOG.md +16 -0
- package/dist/cli.js +2731 -2707
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/memories/index.d.ts +9 -0
- package/dist/types/memory-backend/types.d.ts +11 -0
- package/dist/types/slash-commands/helpers/skill-verify.d.ts +5 -0
- package/dist/types/task/model-tier-classifier.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/learn.d.ts +4 -0
- package/dist/types/tools/path-write-lock.d.ts +10 -0
- package/dist/types/tools/vision-verify.d.ts +34 -0
- package/package.json +12 -12
- package/src/autolearn/controller.ts +21 -5
- package/src/config/settings-schema.ts +24 -0
- package/src/edit/index.ts +2 -1
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/memories/index.ts +10 -2
- package/src/memory-backend/local-backend.ts +34 -0
- package/src/memory-backend/types.ts +11 -0
- package/src/prompts/agents/designer.md +1 -0
- package/src/prompts/tools/learn.md +2 -0
- package/src/prompts/tools/vision-verify-system.md +16 -0
- package/src/prompts/tools/vision-verify.md +20 -0
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/skill-verify.ts +171 -0
- package/src/task/index.ts +26 -1
- package/src/task/model-tier-classifier.ts +43 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/learn.ts +16 -1
- package/src/tools/path-write-lock.ts +44 -0
- package/src/tools/vision-verify.ts +347 -0
- package/src/tools/write.ts +12 -9
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "jeopi-agent-core";
|
|
3
|
+
import { instrumentedCompleteSimple, resolveTelemetry } from "jeopi-agent-core";
|
|
4
|
+
import { type Api, completeSimple, type ImageContent, type Model, type ToolExample } from "jeopi-ai";
|
|
5
|
+
import { prompt } from "jeopi-utils";
|
|
6
|
+
import { extractTextContent } from "../commit/utils";
|
|
7
|
+
|
|
8
|
+
import { expandRoleAlias, getModelMatchPreferences, resolveModelFromString } from "../config/model-resolver";
|
|
9
|
+
import visionVerifyDescription from "../prompts/tools/vision-verify.md" with { type: "text" };
|
|
10
|
+
import visionVerifySystemPromptTemplate from "../prompts/tools/vision-verify-system.md" with { type: "text" };
|
|
11
|
+
import {
|
|
12
|
+
ImageInputTooLargeError,
|
|
13
|
+
type LoadedImageInput,
|
|
14
|
+
loadImageAttachmentInput,
|
|
15
|
+
loadImageInput,
|
|
16
|
+
MAX_IMAGE_INPUT_BYTES,
|
|
17
|
+
webpExclusionForModel,
|
|
18
|
+
} from "../utils/image-loading";
|
|
19
|
+
import type { ToolSession } from "./index";
|
|
20
|
+
import { ToolError } from "./tool-errors";
|
|
21
|
+
|
|
22
|
+
const visionVerifySchema = type({
|
|
23
|
+
goal: type("string").describe("what the UI/artifact should look like or achieve"),
|
|
24
|
+
screenshot: type("string").describe(
|
|
25
|
+
"path to the current screenshot, or an attachment reference like inspect_image accepts",
|
|
26
|
+
),
|
|
27
|
+
"baseline?": type("string").describe(
|
|
28
|
+
"optional path to a PRIOR screenshot of the same view, for regression comparison",
|
|
29
|
+
),
|
|
30
|
+
"+": "reject",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type VisionVerifyParams = typeof visionVerifySchema.infer;
|
|
34
|
+
|
|
35
|
+
interface ImageAttachmentReference {
|
|
36
|
+
index: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IMAGE_ATTACHMENT_REFERENCE_REGEX =
|
|
40
|
+
/^\s*(?:\[?Image #([1-9]\d*)(?:,[^\]\n]*)?\]?|(?:attachment|image):\/\/([1-9]\d*))\s*$/i;
|
|
41
|
+
|
|
42
|
+
function parseImageAttachmentReference(path: string): ImageAttachmentReference | null {
|
|
43
|
+
const match = IMAGE_ATTACHMENT_REFERENCE_REGEX.exec(path);
|
|
44
|
+
if (!match) return null;
|
|
45
|
+
const rawIndex = match[1] ?? match[2];
|
|
46
|
+
if (!rawIndex) return null;
|
|
47
|
+
return { index: Number(rawIndex) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatAvailableImageAttachments(attachments: readonly { label: string; uri: string }[]): string {
|
|
51
|
+
if (attachments.length === 0) return "none";
|
|
52
|
+
return attachments.map(attachment => `${attachment.label} -> ${attachment.uri}`).join(", ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadAttachmentReferenceInput(options: {
|
|
56
|
+
path: string;
|
|
57
|
+
reference: ImageAttachmentReference;
|
|
58
|
+
attachments: readonly { label: string; uri: string; image: ImageContent }[];
|
|
59
|
+
autoResize: boolean;
|
|
60
|
+
excludeWebP: boolean | undefined;
|
|
61
|
+
}): Promise<LoadedImageInput | null> {
|
|
62
|
+
const attachment = options.attachments[options.reference.index - 1];
|
|
63
|
+
if (!attachment) {
|
|
64
|
+
const available = formatAvailableImageAttachments(options.attachments);
|
|
65
|
+
if (options.attachments.length === 0) {
|
|
66
|
+
throw new ToolError(
|
|
67
|
+
`No image attachments are available in this turn. path="${options.path}" must be a readable file path or attachment URI.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
throw new ToolError(
|
|
71
|
+
`Could not resolve image attachment '${options.path}'. Available image attachments: ${available}. Pass an attachment URI or a readable filesystem path.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return loadImageAttachmentInput({
|
|
75
|
+
image: attachment.image,
|
|
76
|
+
label: attachment.label,
|
|
77
|
+
uri: attachment.uri,
|
|
78
|
+
autoResize: options.autoResize,
|
|
79
|
+
maxBytes: MAX_IMAGE_INPUT_BYTES,
|
|
80
|
+
excludeWebP: options.excludeWebP,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Resolve a `screenshot`/`baseline` param to a loaded image, honoring attachment references the same way `inspect_image` does. */
|
|
85
|
+
async function resolveVisionVerifyImageInput(options: {
|
|
86
|
+
path: string;
|
|
87
|
+
session: ToolSession;
|
|
88
|
+
autoResize: boolean;
|
|
89
|
+
excludeWebP: boolean | undefined;
|
|
90
|
+
}): Promise<LoadedImageInput | null> {
|
|
91
|
+
const attachmentReference = parseImageAttachmentReference(options.path);
|
|
92
|
+
if (attachmentReference) {
|
|
93
|
+
return loadAttachmentReferenceInput({
|
|
94
|
+
path: options.path,
|
|
95
|
+
reference: attachmentReference,
|
|
96
|
+
attachments: options.session.getImageAttachments?.() ?? [],
|
|
97
|
+
autoResize: options.autoResize,
|
|
98
|
+
excludeWebP: options.excludeWebP,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return loadImageInput({
|
|
102
|
+
path: options.path,
|
|
103
|
+
cwd: options.session.cwd,
|
|
104
|
+
autoResize: options.autoResize,
|
|
105
|
+
maxBytes: MAX_IMAGE_INPUT_BYTES,
|
|
106
|
+
excludeWebP: options.excludeWebP,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function loadVisionVerifyImage(options: {
|
|
111
|
+
path: string;
|
|
112
|
+
label: string;
|
|
113
|
+
session: ToolSession;
|
|
114
|
+
autoResize: boolean;
|
|
115
|
+
excludeWebP: boolean | undefined;
|
|
116
|
+
}): Promise<LoadedImageInput> {
|
|
117
|
+
let imageInput: LoadedImageInput | null;
|
|
118
|
+
try {
|
|
119
|
+
imageInput = await resolveVisionVerifyImageInput(options);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error instanceof ImageInputTooLargeError) {
|
|
122
|
+
throw new ToolError(error.message);
|
|
123
|
+
}
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
if (!imageInput) {
|
|
127
|
+
throw new ToolError(
|
|
128
|
+
`vision_verify only supports PNG, JPEG, GIF, and WEBP files detected by file content (${options.label}="${options.path}").`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return imageInput;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Structured verdict the model is instructed to return. */
|
|
135
|
+
interface VisionVerifyVerdict {
|
|
136
|
+
matches: boolean;
|
|
137
|
+
summary: string;
|
|
138
|
+
gaps: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isVisionVerifyVerdict(value: unknown): value is VisionVerifyVerdict {
|
|
142
|
+
if (!value || typeof value !== "object") return false;
|
|
143
|
+
const candidate = value as Record<string, unknown>;
|
|
144
|
+
return (
|
|
145
|
+
typeof candidate.matches === "boolean" &&
|
|
146
|
+
typeof candidate.summary === "string" &&
|
|
147
|
+
Array.isArray(candidate.gaps) &&
|
|
148
|
+
candidate.gaps.every(gap => typeof gap === "string")
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseVisionVerifyVerdict(text: string): VisionVerifyVerdict {
|
|
153
|
+
let parsed: unknown;
|
|
154
|
+
try {
|
|
155
|
+
const trimmed = text.trim();
|
|
156
|
+
const jsonText = trimmed.startsWith("{") ? trimmed : (trimmed.match(/\{[\s\S]*\}/)?.[0] ?? trimmed);
|
|
157
|
+
parsed = JSON.parse(jsonText);
|
|
158
|
+
} catch {
|
|
159
|
+
throw new ToolError(`vision_verify model response was not valid JSON. Raw response: ${text}`, {
|
|
160
|
+
rawResponse: text,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (!isVisionVerifyVerdict(parsed)) {
|
|
164
|
+
throw new ToolError(
|
|
165
|
+
`vision_verify model response did not match the expected {matches, summary, gaps} shape. Raw response: ${text}`,
|
|
166
|
+
{ rawResponse: text },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function renderVisionVerifySummary(verdict: VisionVerifyVerdict): string {
|
|
173
|
+
const lines = [verdict.matches ? "Matches: yes" : "Matches: no", verdict.summary];
|
|
174
|
+
if (verdict.gaps.length > 0) {
|
|
175
|
+
lines.push("Gaps:");
|
|
176
|
+
for (const gap of verdict.gaps) lines.push(`- ${gap}`);
|
|
177
|
+
}
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface VisionVerifyToolDetails {
|
|
182
|
+
model: string;
|
|
183
|
+
matches: boolean;
|
|
184
|
+
gaps: string[];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export class VisionVerifyTool implements AgentTool<typeof visionVerifySchema, VisionVerifyToolDetails> {
|
|
188
|
+
readonly name = "vision_verify";
|
|
189
|
+
readonly approval = "read" as const;
|
|
190
|
+
readonly label = "VisionVerify";
|
|
191
|
+
readonly loadMode = "discoverable";
|
|
192
|
+
readonly summary = "Check a screenshot/artifact against a stated goal with a vision model";
|
|
193
|
+
readonly description: string;
|
|
194
|
+
readonly parameters = visionVerifySchema;
|
|
195
|
+
readonly strict = false;
|
|
196
|
+
|
|
197
|
+
readonly examples: readonly ToolExample<typeof visionVerifySchema.infer>[] = [
|
|
198
|
+
{
|
|
199
|
+
caption: "Verify a UI change matches the task goal",
|
|
200
|
+
call: {
|
|
201
|
+
goal: "Settings page shows a 'Save' button that is disabled until a field changes",
|
|
202
|
+
screenshot: "screenshots/settings-after.png",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
caption: "Regression check against a prior known-good state",
|
|
207
|
+
call: {
|
|
208
|
+
goal: "Header layout unchanged aside from the new notification icon",
|
|
209
|
+
screenshot: "screenshots/header-after.png",
|
|
210
|
+
baseline: "screenshots/header-before.png",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
constructor(
|
|
216
|
+
private readonly session: ToolSession,
|
|
217
|
+
private readonly completeImageRequest: typeof completeSimple = completeSimple,
|
|
218
|
+
) {
|
|
219
|
+
this.description = prompt.render(visionVerifyDescription);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async execute(
|
|
223
|
+
_toolCallId: string,
|
|
224
|
+
params: VisionVerifyParams,
|
|
225
|
+
signal?: AbortSignal,
|
|
226
|
+
_onUpdate?: AgentToolUpdateCallback<VisionVerifyToolDetails>,
|
|
227
|
+
_context?: AgentToolContext,
|
|
228
|
+
): Promise<AgentToolResult<VisionVerifyToolDetails>> {
|
|
229
|
+
if (this.session.settings.get("images.blockImages")) {
|
|
230
|
+
throw new ToolError(
|
|
231
|
+
"Image submission is disabled by settings (images.blockImages=true). Disable it to use vision_verify.",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const modelRegistry = this.session.modelRegistry;
|
|
236
|
+
if (!modelRegistry) {
|
|
237
|
+
throw new ToolError("Model registry is unavailable for vision_verify.");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const availableModels = modelRegistry.getAvailable();
|
|
241
|
+
if (availableModels.length === 0) {
|
|
242
|
+
throw new ToolError("No models available for vision_verify.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const matchPreferences = getModelMatchPreferences(this.session.settings);
|
|
246
|
+
const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
|
|
247
|
+
if (!pattern) return undefined;
|
|
248
|
+
const expanded = expandRoleAlias(pattern, this.session.settings);
|
|
249
|
+
return resolveModelFromString(expanded, availableModels, matchPreferences);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
|
|
253
|
+
const model =
|
|
254
|
+
resolvePattern("pi/vision") ??
|
|
255
|
+
resolvePattern("pi/default") ??
|
|
256
|
+
resolvePattern(activeModelPattern) ??
|
|
257
|
+
availableModels[0];
|
|
258
|
+
if (!model) {
|
|
259
|
+
throw new ToolError("Unable to resolve a model for vision_verify.");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!model.input.includes("image")) {
|
|
263
|
+
throw new ToolError(
|
|
264
|
+
`Resolved model ${model.provider}/${model.id} does not support image input. Configure a vision-capable model for modelRoles.vision.`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const apiKey = await modelRegistry.getApiKey(model);
|
|
269
|
+
if (!apiKey) {
|
|
270
|
+
throw new ToolError(
|
|
271
|
+
`No API key available for ${model.provider}/${model.id}. Configure credentials for this provider or choose another vision-capable model.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const autoResize = this.session.settings.get("images.autoResize");
|
|
276
|
+
const excludeWebP = webpExclusionForModel(model);
|
|
277
|
+
|
|
278
|
+
const currentImage = await loadVisionVerifyImage({
|
|
279
|
+
path: params.screenshot,
|
|
280
|
+
label: "screenshot",
|
|
281
|
+
session: this.session,
|
|
282
|
+
autoResize,
|
|
283
|
+
excludeWebP,
|
|
284
|
+
});
|
|
285
|
+
const baselineImage = params.baseline
|
|
286
|
+
? await loadVisionVerifyImage({
|
|
287
|
+
path: params.baseline,
|
|
288
|
+
label: "baseline",
|
|
289
|
+
session: this.session,
|
|
290
|
+
autoResize,
|
|
291
|
+
excludeWebP,
|
|
292
|
+
})
|
|
293
|
+
: undefined;
|
|
294
|
+
|
|
295
|
+
const userContent: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [];
|
|
296
|
+
if (baselineImage) {
|
|
297
|
+
userContent.push({ type: "text", text: "Baseline (prior):" });
|
|
298
|
+
userContent.push({ type: "image", data: baselineImage.data, mimeType: baselineImage.mimeType });
|
|
299
|
+
userContent.push({ type: "text", text: "Current:" });
|
|
300
|
+
}
|
|
301
|
+
userContent.push({ type: "image", data: currentImage.data, mimeType: currentImage.mimeType });
|
|
302
|
+
userContent.push({ type: "text", text: `Goal: ${params.goal}` });
|
|
303
|
+
|
|
304
|
+
const telemetry = resolveTelemetry(this.session.getTelemetry?.(), this.session.getSessionId?.() ?? undefined);
|
|
305
|
+
const response = await instrumentedCompleteSimple(
|
|
306
|
+
model,
|
|
307
|
+
{
|
|
308
|
+
systemPrompt: [prompt.render(visionVerifySystemPromptTemplate)],
|
|
309
|
+
messages: [
|
|
310
|
+
{
|
|
311
|
+
role: "user",
|
|
312
|
+
content: userContent,
|
|
313
|
+
timestamp: Date.now(),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
apiKey: modelRegistry.resolver(model, this.session.getSessionId?.() ?? undefined),
|
|
319
|
+
signal,
|
|
320
|
+
},
|
|
321
|
+
{ telemetry, oneshotKind: "vision_verify", completeImpl: this.completeImageRequest },
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (response.stopReason === "error") {
|
|
325
|
+
throw new ToolError(response.errorMessage ?? "vision_verify request failed.");
|
|
326
|
+
}
|
|
327
|
+
if (response.stopReason === "aborted") {
|
|
328
|
+
throw new ToolError("vision_verify request aborted.");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const text = extractTextContent(response);
|
|
332
|
+
if (!text) {
|
|
333
|
+
throw new ToolError("vision_verify model returned no text output.");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const verdict = parseVisionVerifyVerdict(text);
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: renderVisionVerifySummary(verdict) }],
|
|
340
|
+
details: {
|
|
341
|
+
model: `${model.provider}/${model.id}`,
|
|
342
|
+
matches: verdict.matches,
|
|
343
|
+
gaps: verdict.gaps,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/tools/write.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
|
|
42
42
|
import { type OutputMeta, outputMeta } from "./output-meta";
|
|
43
43
|
import { formatPathRelativeToCwd, isInternalUrlPath, pathTargetsSsh, peelWriteUrlSelector } from "./path-utils";
|
|
44
|
+
import { withPathWriteLock } from "./path-write-lock";
|
|
44
45
|
import { enforcePlanModeWrite, resolvePlanPath, unwrapHashlineHeaderPath } from "./plan-mode-guard";
|
|
45
46
|
import {
|
|
46
47
|
cachedRenderedString,
|
|
@@ -322,15 +323,17 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
322
323
|
const enableFormat = enableLsp && session.settings.get("lsp.formatOnWrite");
|
|
323
324
|
const enableDiagnostics = enableLsp && session.settings.get("lsp.diagnosticsOnWrite");
|
|
324
325
|
const dedup = enableDiagnostics && session.settings.get("lsp.diagnosticsDeduplicate");
|
|
325
|
-
this.#writethrough =
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
326
|
+
this.#writethrough = withPathWriteLock(
|
|
327
|
+
enableLsp
|
|
328
|
+
? createLspWritethrough(session.cwd, {
|
|
329
|
+
enableFormat,
|
|
330
|
+
enableDiagnostics,
|
|
331
|
+
transformDiagnostics: dedup
|
|
332
|
+
? (path, result) => getDiagnosticsLedger(session).reduce(path, result)
|
|
333
|
+
: undefined,
|
|
334
|
+
})
|
|
335
|
+
: writethroughNoop,
|
|
336
|
+
);
|
|
334
337
|
this.description = prompt.render(writeDescription);
|
|
335
338
|
}
|
|
336
339
|
|