jeopi-cli 16.2.30 → 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 +32 -0
- package/dist/cli.js +2824 -2736
- package/dist/types/config/settings-schema.d.ts +32 -2
- package/dist/types/goals/runtime.d.ts +16 -0
- package/dist/types/goals/verifier.d.ts +9 -0
- package/dist/types/memories/index.d.ts +9 -0
- package/dist/types/memory-backend/types.d.ts +11 -0
- package/dist/types/session/agent-session.d.ts +22 -7
- 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/autoresearch/index.ts +60 -6
- package/src/config/settings-schema.ts +38 -2
- package/src/edit/index.ts +2 -1
- package/src/goals/runtime.ts +51 -0
- package/src/goals/verifier.ts +100 -0
- 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/agents/goal-verifier.md +51 -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/sdk.ts +2 -0
- package/src/session/agent-session.ts +51 -8
- package/src/slash-commands/builtin-registry.ts +9 -0
- package/src/slash-commands/helpers/skill-verify.ts +171 -0
- package/src/task/agents.ts +2 -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
package/src/tools/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
|
66
66
|
import { loadSshTool } from "./ssh";
|
|
67
67
|
import { isSubagentToolEnabled, SubagentTool } from "./subagent";
|
|
68
68
|
import { type TodoPhase, TodoTool } from "./todo";
|
|
69
|
+
import { VisionVerifyTool } from "./vision-verify";
|
|
69
70
|
import { WriteTool } from "./write";
|
|
70
71
|
import { YieldTool } from "./yield";
|
|
71
72
|
|
|
@@ -106,6 +107,7 @@ export * from "./ssh";
|
|
|
106
107
|
export * from "./subagent";
|
|
107
108
|
export * from "./todo";
|
|
108
109
|
export * from "./tts";
|
|
110
|
+
export * from "./vision-verify";
|
|
109
111
|
export * from "./write";
|
|
110
112
|
export * from "./yield";
|
|
111
113
|
|
|
@@ -466,6 +468,7 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
|
|
|
466
468
|
grep: s => new GrepTool(s),
|
|
467
469
|
lsp: LspTool.createIf,
|
|
468
470
|
inspect_image: s => new InspectImageTool(s),
|
|
471
|
+
vision_verify: s => new VisionVerifyTool(s),
|
|
469
472
|
browser: s => new BrowserTool(s),
|
|
470
473
|
checkpoint: CheckpointTool.createIf,
|
|
471
474
|
rewind: RewindTool.createIf,
|
|
@@ -621,6 +624,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
621
624
|
if (name === "ast_grep") return session.settings.get("astGrep.enabled");
|
|
622
625
|
if (name === "ast_edit") return session.settings.get("astEdit.enabled");
|
|
623
626
|
if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
|
|
627
|
+
if (name === "vision_verify") return session.settings.get("vision_verify.enabled");
|
|
624
628
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
625
629
|
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
|
626
630
|
if (name === "search_tool_bm25") return discoveryActive;
|
package/src/tools/learn.ts
CHANGED
|
@@ -9,6 +9,12 @@ import type { ToolSession } from ".";
|
|
|
9
9
|
const learnSchema = type({
|
|
10
10
|
memory: type("string").describe("the durable, self-contained lesson to remember (what, when, why)"),
|
|
11
11
|
"context?": type("string").describe("optional source context for the lesson"),
|
|
12
|
+
"verified?": type("boolean").describe(
|
|
13
|
+
"true only if you independently re-checked this claim against the repo (ran a command, read a file, ran a test) rather than merely inferring it",
|
|
14
|
+
),
|
|
15
|
+
"evidence?": type("string").describe(
|
|
16
|
+
"the short check that confirmed the claim (a command, a file path, a test name); only meaningful when verified is true",
|
|
17
|
+
),
|
|
12
18
|
"skill?": type({
|
|
13
19
|
action: "'create' | 'update'",
|
|
14
20
|
name: type("string").describe("kebab-case skill name"),
|
|
@@ -65,6 +71,8 @@ export class LearnTool implements AgentTool<typeof learnSchema> {
|
|
|
65
71
|
cwd: state.session.sessionManager.getCwd(),
|
|
66
72
|
context: params.context ?? null,
|
|
67
73
|
tool: "learn",
|
|
74
|
+
verified: params.verified ?? false,
|
|
75
|
+
evidence: params.evidence ?? null,
|
|
68
76
|
},
|
|
69
77
|
scope: "bank",
|
|
70
78
|
extract: true,
|
|
@@ -81,7 +89,14 @@ export class LearnTool implements AgentTool<typeof learnSchema> {
|
|
|
81
89
|
} else if (backend === "local") {
|
|
82
90
|
const result = await localBackend.save?.(
|
|
83
91
|
{ agentDir: this.session.settings.getAgentDir(), cwd: this.session.settings.getCwd() },
|
|
84
|
-
{
|
|
92
|
+
{
|
|
93
|
+
content: params.memory,
|
|
94
|
+
context: params.context,
|
|
95
|
+
source: "coding-agent-learn",
|
|
96
|
+
importance: 0.8,
|
|
97
|
+
verified: params.verified,
|
|
98
|
+
evidence: params.evidence,
|
|
99
|
+
},
|
|
85
100
|
);
|
|
86
101
|
if (!result || result.stored === 0) {
|
|
87
102
|
throw new Error("Lesson was empty after sanitization; nothing stored.");
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { WritethroughCallback } from "../lsp";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Non-isolated concurrent subagents (`task.isolation.mode="none"`, the
|
|
5
|
+
* default) share the parent session's cwd with no other collision-avoidance
|
|
6
|
+
* mechanism: two sibling subagents editing the same file can each perform an
|
|
7
|
+
* unguarded read-modify-write, and the interleaving of their disk writes can
|
|
8
|
+
* silently drop one of them. This wrapper does not prevent LOGICAL conflicts
|
|
9
|
+
* (two agents making incompatible edits to the same file) — it only
|
|
10
|
+
* serializes the physical writethrough call per destination path, so one
|
|
11
|
+
* write can no longer be lost mid-flight to another write racing the same
|
|
12
|
+
* path.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Per-path write chains serializing the writethrough callback for a given destination. */
|
|
16
|
+
const pathWriteChains = new Map<string, Promise<unknown>>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap a {@link WritethroughCallback} so concurrent writes to the same `dst`
|
|
20
|
+
* are serialized instead of racing. `dst` is used as-is for the chain key:
|
|
21
|
+
* every caller (edit's hashline/patch/replace paths, and the write tool)
|
|
22
|
+
* resolves `dst` to an absolute path via `resolvePlanPath`/`resolveToCwd`
|
|
23
|
+
* before invoking the writethrough, so the same logical file always arrives
|
|
24
|
+
* under the same string.
|
|
25
|
+
*/
|
|
26
|
+
export function withPathWriteLock(callback: WritethroughCallback): WritethroughCallback {
|
|
27
|
+
return async (dst, content, signal, file, batch, getDeferred) => {
|
|
28
|
+
// Serialize the writethrough per path: parallel edits to the same file
|
|
29
|
+
// (sibling subagents sharing a cwd, or two shared tool calls in one
|
|
30
|
+
// turn) would otherwise let concurrent disk writes race and drop one.
|
|
31
|
+
const run = (pathWriteChains.get(dst) ?? Promise.resolve()).then(() =>
|
|
32
|
+
callback(dst, content, signal, file, batch, getDeferred),
|
|
33
|
+
);
|
|
34
|
+
const guarded = run.catch(() => {});
|
|
35
|
+
pathWriteChains.set(dst, guarded);
|
|
36
|
+
try {
|
|
37
|
+
return await run;
|
|
38
|
+
} finally {
|
|
39
|
+
// Drop the entry once this write is the chain tail, so the map does
|
|
40
|
+
// not retain one promise per distinct path for the process lifetime.
|
|
41
|
+
if (pathWriteChains.get(dst) === guarded) pathWriteChains.delete(dst);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -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
|
|