@lazyingart/agintiflow 0.1.0 → 0.3.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/AGENTS.md +6 -3
- package/README.md +68 -15
- package/docker/sandbox.Dockerfile +8 -0
- package/package.json +11 -3
- package/public/app.js +785 -11
- package/public/index.html +166 -20
- package/public/styles.css +677 -15
- package/scripts/setup-agent-toolchain-docker.sh +51 -0
- package/scripts/smoke-coding-tools.js +138 -0
- package/scripts/smoke-toolchain-docker.js +185 -0
- package/scripts/smoke-web-api.js +217 -0
- package/src/agent-runner.js +286 -21
- package/src/artifact-tunnel.js +401 -0
- package/src/cli.js +47 -3
- package/src/command-policy.js +21 -0
- package/src/config.js +9 -2
- package/src/docker-sandbox.js +38 -8
- package/src/guardrails.js +31 -0
- package/src/model-client.js +300 -6
- package/src/model-routing.js +40 -0
- package/src/session-store.js +4 -0
- package/src/tool-wrappers.js +6 -0
- package/src/web-db.js +48 -5
- package/src/workspace-tools.js +438 -0
- package/web.js +265 -14
package/src/docker-sandbox.js
CHANGED
|
@@ -93,9 +93,17 @@ export function getSandboxLogs(limit = 30) {
|
|
|
93
93
|
return sandboxLogs.slice(-limit);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
function sandboxPackageDir(config) {
|
|
97
|
+
return path.resolve(config.packageDir || config.baseDir || process.cwd());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function sandboxDockerfilePath(config) {
|
|
101
|
+
return path.join(sandboxPackageDir(config), "docker", "sandbox.Dockerfile");
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
export async function getDockerSandboxStatus(config) {
|
|
97
105
|
const image = config.dockerSandboxImage;
|
|
98
|
-
const dockerfilePath =
|
|
106
|
+
const dockerfilePath = sandboxDockerfilePath(config);
|
|
99
107
|
const workspace = config.commandCwd;
|
|
100
108
|
const [dockerReady, dockerfileExists, workspaceExists, workspaceReadable, workspaceWritable] = await Promise.all([
|
|
101
109
|
dockerAvailable(),
|
|
@@ -125,14 +133,15 @@ export async function getDockerSandboxStatus(config) {
|
|
|
125
133
|
|
|
126
134
|
async function buildDockerSandboxImage(config, observers) {
|
|
127
135
|
const image = config.dockerSandboxImage;
|
|
128
|
-
const
|
|
136
|
+
const packageDir = sandboxPackageDir(config);
|
|
137
|
+
const dockerfilePath = sandboxDockerfilePath(config);
|
|
129
138
|
observers?.log?.("docker.building", {
|
|
130
139
|
image,
|
|
131
140
|
dockerfilePath,
|
|
132
141
|
});
|
|
133
142
|
recordSandboxLog("docker.building", { image, dockerfilePath });
|
|
134
143
|
|
|
135
|
-
await execDocker(["build", "-t", image, "-f", dockerfilePath,
|
|
144
|
+
await execDocker(["build", "-t", image, "-f", dockerfilePath, packageDir], {
|
|
136
145
|
timeout: 10 * 60 * 1000,
|
|
137
146
|
maxBuffer: 1024 * 1024,
|
|
138
147
|
});
|
|
@@ -146,7 +155,7 @@ export async function ensureDockerSandboxReady(config, observers, options = {})
|
|
|
146
155
|
const image = config.dockerSandboxImage;
|
|
147
156
|
if (!config.useDockerSandbox || (READY_IMAGES.has(image) && !options.forceBuild)) return;
|
|
148
157
|
|
|
149
|
-
const dockerfilePath =
|
|
158
|
+
const dockerfilePath = sandboxDockerfilePath(config);
|
|
150
159
|
await fs.access(dockerfilePath).catch(() => {
|
|
151
160
|
throw new Error(`Docker sandbox file is missing: ${dockerfilePath}`);
|
|
152
161
|
});
|
|
@@ -163,10 +172,11 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
163
172
|
const sandboxMode = normalizeSandboxMode(config.sandboxMode);
|
|
164
173
|
const mountMode = sandboxMode === "docker-workspace" ? "rw" : "ro";
|
|
165
174
|
const networkMode = policy.needsNetwork ? "bridge" : "none";
|
|
175
|
+
const toolchain = policy.category === "toolchain";
|
|
166
176
|
const readOnlyArgs =
|
|
167
177
|
mountMode === "ro"
|
|
168
178
|
? ["--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,size=128m"]
|
|
169
|
-
: ["--tmpfs",
|
|
179
|
+
: ["--tmpfs", `/tmp:rw,nosuid,nodev,size=${toolchain ? "512m" : "256m"}`];
|
|
170
180
|
|
|
171
181
|
return [
|
|
172
182
|
"run",
|
|
@@ -180,9 +190,9 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
180
190
|
"--pids-limit",
|
|
181
191
|
"256",
|
|
182
192
|
"--memory",
|
|
183
|
-
"768m",
|
|
193
|
+
toolchain ? "2g" : "768m",
|
|
184
194
|
"--cpus",
|
|
185
|
-
"1.5",
|
|
195
|
+
toolchain ? "2" : "1.5",
|
|
186
196
|
...readOnlyArgs,
|
|
187
197
|
...userArgs,
|
|
188
198
|
"-e",
|
|
@@ -204,7 +214,7 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
204
214
|
|
|
205
215
|
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
206
216
|
const result = await execDocker(dockerRunArgs(command, config, policy), {
|
|
207
|
-
timeout: policy.needsNetwork ? 120000 : 15000,
|
|
217
|
+
timeout: policy.needsNetwork ? 120000 : policy.category === "toolchain" ? 90000 : 15000,
|
|
208
218
|
maxBuffer: 300 * 1024,
|
|
209
219
|
});
|
|
210
220
|
|
|
@@ -238,6 +248,23 @@ export async function runDockerPreflight(config, options = {}) {
|
|
|
238
248
|
const manifests = await detectWorkspaceManifests(config.commandCwd);
|
|
239
249
|
let checks = [];
|
|
240
250
|
|
|
251
|
+
if (!config.useDockerSandbox || normalizeSandboxMode(config.sandboxMode) === "host") {
|
|
252
|
+
const result = {
|
|
253
|
+
ok: Boolean(statusBefore.workspaceExists && statusBefore.workspaceReadable),
|
|
254
|
+
status: statusBefore,
|
|
255
|
+
manifests,
|
|
256
|
+
checks,
|
|
257
|
+
logs: getSandboxLogs(),
|
|
258
|
+
};
|
|
259
|
+
recordSandboxLog("sandbox.preflight.completed", {
|
|
260
|
+
ok: result.ok,
|
|
261
|
+
mode: "host",
|
|
262
|
+
manifests,
|
|
263
|
+
checks: [],
|
|
264
|
+
});
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
241
268
|
if (buildImage && statusBefore.dockerAvailable && statusBefore.dockerfileExists && !statusBefore.imageReady) {
|
|
242
269
|
await ensureDockerSandboxReady(config, {
|
|
243
270
|
log: (message, data) => recordSandboxLog(message, data),
|
|
@@ -253,6 +280,9 @@ export async function runDockerPreflight(config, options = {}) {
|
|
|
253
280
|
"npm -v",
|
|
254
281
|
"python3 --version",
|
|
255
282
|
"python3 -m pip --version",
|
|
283
|
+
"python3 -c \"import matplotlib, numpy; print(matplotlib.__version__, numpy.__version__)\"",
|
|
284
|
+
"latexmk -version",
|
|
285
|
+
"pdflatex --version",
|
|
256
286
|
"git --version",
|
|
257
287
|
"rg --version",
|
|
258
288
|
]) {
|
package/src/guardrails.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { evaluateCommandPolicy } from "./command-policy.js";
|
|
2
|
+
import { checkWorkspaceToolUse, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
|
|
3
|
+
import { normalizeWrapperName } from "./tool-wrappers.js";
|
|
2
4
|
|
|
3
5
|
const DESTRUCTIVE_KEYWORDS = [
|
|
4
6
|
"delete",
|
|
@@ -15,6 +17,7 @@ const DESTRUCTIVE_KEYWORDS = [
|
|
|
15
17
|
];
|
|
16
18
|
|
|
17
19
|
const KNOWN_WRAPPERS = new Set(["codex", "claude", "gemini", "copilot", "qwen"]);
|
|
20
|
+
const MAX_CANVAS_CONTENT_BYTES = 120_000;
|
|
18
21
|
const DESTRUCTIVE_PROMPT_HINTS = [
|
|
19
22
|
"delete",
|
|
20
23
|
"remove files",
|
|
@@ -44,6 +47,10 @@ export function isDomainAllowed(urlString, allowedDomains) {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
50
|
+
if (WORKSPACE_TOOL_NAMES.includes(toolName)) {
|
|
51
|
+
return checkWorkspaceToolUse(toolName, args, config);
|
|
52
|
+
}
|
|
53
|
+
|
|
47
54
|
if (toolName === "open_url") {
|
|
48
55
|
if (!/^https?:\/\//.test(String(args.url || ""))) {
|
|
49
56
|
return { allowed: false, reason: "Only http and https URLs are allowed." };
|
|
@@ -102,6 +109,11 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
|
102
109
|
return { allowed: false, reason: `Unknown agent wrapper: ${wrapper}` };
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
const preferredWrapper = normalizeWrapperName(config.preferredWrapper);
|
|
113
|
+
if (wrapper !== preferredWrapper) {
|
|
114
|
+
return { allowed: false, reason: `Only the selected wrapper is enabled for this run: ${preferredWrapper}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
const prompt = String(args.prompt || "").trim();
|
|
106
118
|
if (prompt.length < 8) {
|
|
107
119
|
return { allowed: false, reason: "Agent wrapper prompt is too short." };
|
|
@@ -118,5 +130,24 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
|
118
130
|
return { allowed: true };
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
if (toolName === "send_to_canvas") {
|
|
134
|
+
const title = String(args.title || "").trim();
|
|
135
|
+
if (!title) return { allowed: false, reason: "Canvas title is required." };
|
|
136
|
+
const content = typeof args.content === "string" ? args.content : "";
|
|
137
|
+
if (Buffer.byteLength(content, "utf8") > MAX_CANVAS_CONTENT_BYTES) {
|
|
138
|
+
return {
|
|
139
|
+
allowed: false,
|
|
140
|
+
reason: "Canvas content is too large. Write it to a workspace file and send the path instead.",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const canvasPath = String(args.path || "").trim();
|
|
145
|
+
if (canvasPath) {
|
|
146
|
+
return checkWorkspaceToolUse("read_file", { path: canvasPath }, config);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { allowed: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
return { allowed: true };
|
|
122
153
|
}
|
package/src/model-client.js
CHANGED
|
@@ -1,14 +1,124 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeWrapperName, wrapperStatusText } from "./tool-wrappers.js";
|
|
3
3
|
|
|
4
4
|
export function createClient(config) {
|
|
5
|
+
if (config.provider === "mock") {
|
|
6
|
+
return {
|
|
7
|
+
mock: true,
|
|
8
|
+
provider: "mock",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
return new OpenAI({
|
|
6
13
|
apiKey: config.apiKey,
|
|
7
14
|
baseURL: config.baseURL,
|
|
8
15
|
});
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
function mockToolCall(name, args = {}) {
|
|
19
|
+
return {
|
|
20
|
+
id: `mock-${name}-${Date.now()}`,
|
|
21
|
+
type: "function",
|
|
22
|
+
function: {
|
|
23
|
+
name,
|
|
24
|
+
arguments: JSON.stringify(args),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function latestToolPayload(messages) {
|
|
30
|
+
const toolMessage = [...messages].reverse().find((message) => message.role === "tool" && message.content);
|
|
31
|
+
if (!toolMessage) return null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(toolMessage.content);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function prepareMessages(config, messages) {
|
|
41
|
+
if (config.provider === "deepseek") return messages;
|
|
42
|
+
return messages.map((message) => {
|
|
43
|
+
const prepared = { ...message };
|
|
44
|
+
delete prepared.reasoning_content;
|
|
45
|
+
delete prepared.reasoningContent;
|
|
46
|
+
return prepared;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mockCommandForGoal(goal = "") {
|
|
51
|
+
const text = String(goal).toLowerCase();
|
|
52
|
+
if (/\blist\b|folder contents|directory contents|files?/.test(text)) return "ls -la";
|
|
53
|
+
return "pwd";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mockPathForGoal(goal = "") {
|
|
57
|
+
const text = String(goal);
|
|
58
|
+
const explicit = text.match(/(?:file|path):\s*`?([A-Za-z0-9_./-]+)`?/i)?.[1];
|
|
59
|
+
if (explicit) return explicit;
|
|
60
|
+
if (/\.env/i.test(text)) return ".env";
|
|
61
|
+
if (/outside|escape/i.test(text)) return "../outside-workspace.txt";
|
|
62
|
+
if (/patch/i.test(text)) return "patch-target.txt";
|
|
63
|
+
return "mock-output.txt";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mockWorkspaceToolForGoal(goal = "") {
|
|
67
|
+
const text = String(goal).toLowerCase();
|
|
68
|
+
const targetPath = mockPathForGoal(goal);
|
|
69
|
+
if (/patch|replace|edit/.test(text)) {
|
|
70
|
+
return mockToolCall("apply_patch", {
|
|
71
|
+
path: targetPath,
|
|
72
|
+
search: "old",
|
|
73
|
+
replace: "new",
|
|
74
|
+
expectedReplacements: 1,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (/write|create|file|coding/.test(text)) {
|
|
78
|
+
return mockToolCall("write_file", {
|
|
79
|
+
path: targetPath,
|
|
80
|
+
mode: "create",
|
|
81
|
+
content: `Created by AgInTiFlow mock mode.\nGoal: ${String(goal).slice(0, 160)}\n`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mockCanvasToolForGoal(goal = "") {
|
|
88
|
+
const text = String(goal).toLowerCase();
|
|
89
|
+
if (!/canvas|artifact|image|figure|visual|preview|render/.test(text)) return null;
|
|
90
|
+
return mockToolCall("send_to_canvas", {
|
|
91
|
+
title: "Mock canvas note",
|
|
92
|
+
kind: "markdown",
|
|
93
|
+
content: `# Mock canvas artifact\n\nAgInTiFlow can send selected text, diffs, snapshots, and files into the frontend canvas.\n\nGoal: ${String(goal).slice(0, 160)}`,
|
|
94
|
+
note: "Mock mode exercised the backend-to-frontend artifact tunnel.",
|
|
95
|
+
selected: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mockChatResponse(content, toolCalls = []) {
|
|
100
|
+
return {
|
|
101
|
+
choices: [
|
|
102
|
+
{
|
|
103
|
+
message: {
|
|
104
|
+
role: "assistant",
|
|
105
|
+
content,
|
|
106
|
+
tool_calls: toolCalls,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
11
113
|
export async function createPlan(client, config, state) {
|
|
114
|
+
if (client.mock) {
|
|
115
|
+
return [
|
|
116
|
+
"1. Inspect the request and prefer the local shell when available.",
|
|
117
|
+
"2. Use one safe allowlisted command if it answers the task.",
|
|
118
|
+
"3. Return a concise mock-mode result without using external model credentials.",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
12
122
|
const response = await client.chat.completions.create({
|
|
13
123
|
model: config.model,
|
|
14
124
|
temperature: 0,
|
|
@@ -25,9 +135,17 @@ export async function createPlan(client, config, state) {
|
|
|
25
135
|
state.startUrl ? `Suggested start URL: ${state.startUrl}` : "",
|
|
26
136
|
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
27
137
|
config.allowShellTool
|
|
28
|
-
? `Shell tool is enabled in ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow.`
|
|
138
|
+
? `Shell tool is enabled in ${config.commandCwd}. In Docker, this path is mounted as /workspace. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow.`
|
|
139
|
+
: "",
|
|
140
|
+
config.allowFileTools
|
|
141
|
+
? `Workspace file tools are enabled in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets.`
|
|
29
142
|
: "",
|
|
30
|
-
config.allowWrapperTools
|
|
143
|
+
config.allowWrapperTools
|
|
144
|
+
? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
|
|
145
|
+
: "",
|
|
146
|
+
"A canvas/artifacts tunnel is available through send_to_canvas. Use it when an output should be highlighted visually, such as screenshots, image files, important markdown, diffs, or generated artifact paths. It is optional for ordinary text answers.",
|
|
147
|
+
"When the user asks to draw, plot, graph, chart, diagram, create a figure, or visualize something, include a canvas artifact even if the user does not mention canvas. Prefer a small SVG file or concise markdown figure when file tools are available.",
|
|
148
|
+
"When the user asks for LaTeX, TeX, a paper, manuscript, report, or PDF, plan to create the needed source/assets, compile with the available allowlisted TeX toolchain, and publish the PDF through the canvas tunnel. For subfolder documents, keep outputs beside the source. For generated figures, use pdflatex-compatible formats such as PDF or PNG.",
|
|
31
149
|
"Return a numbered plan only.",
|
|
32
150
|
]
|
|
33
151
|
.filter(Boolean)
|
|
@@ -181,17 +299,114 @@ export async function requestNextStep(client, config, messages) {
|
|
|
181
299
|
});
|
|
182
300
|
}
|
|
183
301
|
|
|
302
|
+
if (config.allowFileTools) {
|
|
303
|
+
tools.splice(
|
|
304
|
+
-1,
|
|
305
|
+
0,
|
|
306
|
+
{
|
|
307
|
+
type: "function",
|
|
308
|
+
function: {
|
|
309
|
+
name: "list_files",
|
|
310
|
+
description:
|
|
311
|
+
"List workspace-local files under the configured working directory. Paths must stay inside the workspace; .git, node_modules, sessions, and sensitive files are skipped.",
|
|
312
|
+
parameters: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
path: { type: "string", description: "Workspace-relative path to list. Defaults to ." },
|
|
316
|
+
maxDepth: { type: "integer", description: "Recursive depth, 1 to 8." },
|
|
317
|
+
limit: { type: "integer", description: "Maximum entries to return." },
|
|
318
|
+
},
|
|
319
|
+
additionalProperties: false,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
type: "function",
|
|
325
|
+
function: {
|
|
326
|
+
name: "read_file",
|
|
327
|
+
description:
|
|
328
|
+
"Read a small UTF-8 workspace file. Secret paths, .git internals, files outside the workspace, binary files, and huge files are blocked.",
|
|
329
|
+
parameters: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: {
|
|
332
|
+
path: { type: "string", description: "Workspace-relative file path." },
|
|
333
|
+
},
|
|
334
|
+
required: ["path"],
|
|
335
|
+
additionalProperties: false,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
type: "function",
|
|
341
|
+
function: {
|
|
342
|
+
name: "search_files",
|
|
343
|
+
description:
|
|
344
|
+
"Search small UTF-8 workspace files for literal text. Secret paths, .git internals, node_modules, and huge files are skipped.",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
query: { type: "string" },
|
|
349
|
+
path: { type: "string", description: "Workspace-relative directory or file. Defaults to ." },
|
|
350
|
+
caseSensitive: { type: "boolean" },
|
|
351
|
+
maxResults: { type: "integer" },
|
|
352
|
+
},
|
|
353
|
+
required: ["query"],
|
|
354
|
+
additionalProperties: false,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
type: "function",
|
|
360
|
+
function: {
|
|
361
|
+
name: "write_file",
|
|
362
|
+
description:
|
|
363
|
+
"Create or overwrite a small UTF-8 workspace file. Use mode=create for new files and mode=overwrite only after reading/understanding the existing file. Secret paths, .git, node_modules writes, and outside-workspace paths are blocked. The runtime records before/after hashes and a compact diff.",
|
|
364
|
+
parameters: {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: {
|
|
367
|
+
path: { type: "string", description: "Workspace-relative file path." },
|
|
368
|
+
content: { type: "string" },
|
|
369
|
+
mode: { type: "string", enum: ["create", "overwrite"] },
|
|
370
|
+
},
|
|
371
|
+
required: ["path", "content"],
|
|
372
|
+
additionalProperties: false,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
type: "function",
|
|
378
|
+
function: {
|
|
379
|
+
name: "apply_patch",
|
|
380
|
+
description:
|
|
381
|
+
"Apply a deterministic workspace-local search/replace patch to one small UTF-8 file. Provide exact search and replacement text. The runtime records before/after hashes and a compact diff.",
|
|
382
|
+
parameters: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
path: { type: "string", description: "Workspace-relative file path." },
|
|
386
|
+
search: { type: "string" },
|
|
387
|
+
replace: { type: "string" },
|
|
388
|
+
expectedReplacements: { type: "integer" },
|
|
389
|
+
},
|
|
390
|
+
required: ["path", "search", "replace"],
|
|
391
|
+
additionalProperties: false,
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
184
398
|
if (config.allowWrapperTools) {
|
|
399
|
+
const selectedWrapper = normalizeWrapperName(config.preferredWrapper);
|
|
185
400
|
tools.splice(-1, 0, {
|
|
186
401
|
type: "function",
|
|
187
402
|
function: {
|
|
188
403
|
name: "delegate_agent",
|
|
189
404
|
description:
|
|
190
|
-
|
|
405
|
+
`Ask the selected external coding agent wrapper (${selectedWrapper}) for advisory help. Use for codebase analysis, implementation strategy, or second-opinion review. The wrapper is instructed to avoid modifying files.`,
|
|
191
406
|
parameters: {
|
|
192
407
|
type: "object",
|
|
193
408
|
properties: {
|
|
194
|
-
wrapper: { type: "string", enum:
|
|
409
|
+
wrapper: { type: "string", enum: [selectedWrapper] },
|
|
195
410
|
prompt: { type: "string" },
|
|
196
411
|
},
|
|
197
412
|
required: ["wrapper", "prompt"],
|
|
@@ -201,12 +416,91 @@ export async function requestNextStep(client, config, messages) {
|
|
|
201
416
|
});
|
|
202
417
|
}
|
|
203
418
|
|
|
419
|
+
tools.splice(-1, 0, {
|
|
420
|
+
type: "function",
|
|
421
|
+
function: {
|
|
422
|
+
name: "send_to_canvas",
|
|
423
|
+
description:
|
|
424
|
+
"Send an optional artifact notification to the frontend canvas/artifacts tunnel. Use for important markdown/text, generated images, screenshots, figures, diffs, or workspace file paths the user should preview. Proactively use this for draw/plot/graph/chart/diagram/figure requests even when the user did not mention canvas. This does not replace finish.",
|
|
425
|
+
parameters: {
|
|
426
|
+
type: "object",
|
|
427
|
+
properties: {
|
|
428
|
+
title: { type: "string", description: "Short display title for the canvas item." },
|
|
429
|
+
kind: {
|
|
430
|
+
type: "string",
|
|
431
|
+
enum: ["text", "markdown", "image", "json", "diff", "file", "pdf"],
|
|
432
|
+
description: "Renderer hint. Use image/file with path, markdown/text/json/diff with content.",
|
|
433
|
+
},
|
|
434
|
+
content: { type: "string", description: "Inline text or markdown content to render." },
|
|
435
|
+
path: { type: "string", description: "Optional workspace-relative file path to preview." },
|
|
436
|
+
note: { type: "string", description: "Short notification message for the artifact explorer." },
|
|
437
|
+
selected: { type: "boolean", description: "Whether the frontend should select this item immediately." },
|
|
438
|
+
},
|
|
439
|
+
required: ["title", "kind"],
|
|
440
|
+
additionalProperties: false,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (client.mock) {
|
|
446
|
+
const toolPayload = latestToolPayload(messages);
|
|
447
|
+
if (toolPayload) {
|
|
448
|
+
const output = [
|
|
449
|
+
toolPayload.stdout,
|
|
450
|
+
toolPayload.stderr,
|
|
451
|
+
toolPayload.error,
|
|
452
|
+
toolPayload.reason,
|
|
453
|
+
toolPayload.path ? `Path: ${toolPayload.path}` : "",
|
|
454
|
+
toolPayload.change?.diff ? `Diff:\n${toolPayload.change.diff}` : "",
|
|
455
|
+
]
|
|
456
|
+
.filter(Boolean)
|
|
457
|
+
.join("\n");
|
|
458
|
+
return mockChatResponse("Mock mode finished after receiving the latest tool result.", [
|
|
459
|
+
mockToolCall("finish", {
|
|
460
|
+
result: [
|
|
461
|
+
"Mock run complete.",
|
|
462
|
+
toolPayload.toolName ? `Tool: ${toolPayload.toolName}` : "",
|
|
463
|
+
toolPayload.args?.command ? `Command: ${toolPayload.args.command}` : "",
|
|
464
|
+
toolPayload.blocked ? "Blocked by guardrail." : "",
|
|
465
|
+
output ? `Output:\n${output}` : "No command output was returned.",
|
|
466
|
+
]
|
|
467
|
+
.filter(Boolean)
|
|
468
|
+
.join("\n"),
|
|
469
|
+
}),
|
|
470
|
+
]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const canvasTool = mockCanvasToolForGoal(config.goal);
|
|
474
|
+
if (canvasTool) {
|
|
475
|
+
return mockChatResponse("Mock mode will publish a canvas artifact for the UI tunnel.", [canvasTool]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (config.allowFileTools) {
|
|
479
|
+
const workspaceTool = mockWorkspaceToolForGoal(config.goal);
|
|
480
|
+
if (workspaceTool) {
|
|
481
|
+
return mockChatResponse("Mock mode will exercise a guarded workspace file tool.", [workspaceTool]);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (config.allowShellTool) {
|
|
486
|
+
return mockChatResponse("Mock mode will use the guarded shell tool for a non-dangerous local inspection.", [
|
|
487
|
+
mockToolCall("run_command", { command: mockCommandForGoal(config.goal) }),
|
|
488
|
+
]);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return mockChatResponse("Mock mode can complete without external model credentials.", [
|
|
492
|
+
mockToolCall("finish", {
|
|
493
|
+
result: `Mock run complete for: ${config.goal}`,
|
|
494
|
+
}),
|
|
495
|
+
]);
|
|
496
|
+
}
|
|
497
|
+
|
|
204
498
|
return client.chat.completions.create({
|
|
205
499
|
model: config.model,
|
|
206
500
|
temperature: 0,
|
|
207
501
|
tool_choice: "auto",
|
|
208
502
|
parallel_tool_calls: false,
|
|
209
|
-
messages,
|
|
503
|
+
messages: prepareMessages(config, messages),
|
|
210
504
|
tools,
|
|
211
505
|
});
|
|
212
506
|
}
|
package/src/model-routing.js
CHANGED
|
@@ -17,9 +17,28 @@ const COMPLEXITY_KEYWORDS = [
|
|
|
17
17
|
"github",
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
+
const COMPLEX_ROUTE_HINTS = [
|
|
21
|
+
/\blatex\b/i,
|
|
22
|
+
/\btexlive\b/i,
|
|
23
|
+
/\bpdflatex\b/i,
|
|
24
|
+
/\blatexmk\b/i,
|
|
25
|
+
/\b(manuscript|research paper|white paper|technical report)\b/i,
|
|
26
|
+
/\bcompile\b.*\bpdf\b/i,
|
|
27
|
+
/\bwrite\b.*\bpdf\b/i,
|
|
28
|
+
];
|
|
29
|
+
|
|
20
30
|
export const ROUTING_MODES = ["smart", "fast", "complex", "manual"];
|
|
21
31
|
|
|
22
32
|
export function getProviderDefaults(provider = "deepseek") {
|
|
33
|
+
if (provider === "mock") {
|
|
34
|
+
return {
|
|
35
|
+
provider: "mock",
|
|
36
|
+
apiKey: "mock-local",
|
|
37
|
+
baseURL: "",
|
|
38
|
+
model: process.env.MOCK_MODEL || "mock-agent",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
if (provider === "openai") {
|
|
24
43
|
return {
|
|
25
44
|
provider: "openai",
|
|
@@ -53,6 +72,13 @@ export function getModelPresets() {
|
|
|
53
72
|
model: process.env.DEEPSEEK_PRO_MODEL || "deepseek-v4-pro",
|
|
54
73
|
description: "Higher-capacity DeepSeek route for multi-step coding and design tasks.",
|
|
55
74
|
},
|
|
75
|
+
mock: {
|
|
76
|
+
id: "mock",
|
|
77
|
+
label: "Local mock",
|
|
78
|
+
provider: "mock",
|
|
79
|
+
model: process.env.MOCK_MODEL || "mock-agent",
|
|
80
|
+
description: "Credential-free local route for UI, API, and tool-routing smoke tests.",
|
|
81
|
+
},
|
|
56
82
|
codexPrimary: {
|
|
57
83
|
id: "codexPrimary",
|
|
58
84
|
label: "Codex primary wrapper",
|
|
@@ -78,6 +104,9 @@ export function scoreTaskComplexity(goal = "") {
|
|
|
78
104
|
for (const keyword of COMPLEXITY_KEYWORDS) {
|
|
79
105
|
if (text.includes(keyword)) score += 1;
|
|
80
106
|
}
|
|
107
|
+
for (const hint of COMPLEX_ROUTE_HINTS) {
|
|
108
|
+
if (hint.test(goal)) score += 3;
|
|
109
|
+
}
|
|
81
110
|
return score;
|
|
82
111
|
}
|
|
83
112
|
|
|
@@ -89,6 +118,17 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
|
|
|
89
118
|
const mode = normalizeRoutingMode(routingMode);
|
|
90
119
|
const presets = getModelPresets();
|
|
91
120
|
|
|
121
|
+
if (provider === "mock") {
|
|
122
|
+
const defaults = getProviderDefaults("mock");
|
|
123
|
+
return {
|
|
124
|
+
routingMode: "manual",
|
|
125
|
+
provider: defaults.provider,
|
|
126
|
+
model: model || defaults.model,
|
|
127
|
+
reason: "Local mock route selected for smoke tests and offline UI/API checks.",
|
|
128
|
+
complexityScore: scoreTaskComplexity(goal),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
92
132
|
if (mode === "manual") {
|
|
93
133
|
const defaults = getProviderDefaults(provider);
|
|
94
134
|
return {
|
package/src/session-store.js
CHANGED
package/src/tool-wrappers.js
CHANGED
|
@@ -6,6 +6,7 @@ import { redactSensitiveText } from "./redaction.js";
|
|
|
6
6
|
const execFile = promisify(execFileCallback);
|
|
7
7
|
|
|
8
8
|
export const WRAPPER_NAMES = ["codex", "claude", "gemini", "copilot", "qwen"];
|
|
9
|
+
export const DEFAULT_WRAPPER_NAME = "codex";
|
|
9
10
|
|
|
10
11
|
const BASE_ADVISORY_PROMPT = [
|
|
11
12
|
"You are being called as an advisory wrapper tool inside AgInTiFlow.",
|
|
@@ -92,6 +93,11 @@ export function isKnownWrapper(wrapper) {
|
|
|
92
93
|
return WRAPPER_NAMES.includes(wrapper);
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
export function normalizeWrapperName(wrapper, fallback = DEFAULT_WRAPPER_NAME) {
|
|
97
|
+
const candidate = String(wrapper || "").trim().toLowerCase();
|
|
98
|
+
return isKnownWrapper(candidate) ? candidate : fallback;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
export function listAgentWrappers() {
|
|
96
102
|
const presets = getModelPresets();
|
|
97
103
|
return [
|