@lazyingart/agintiflow 0.20.10 → 0.20.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.10",
3
+ "version": "0.20.12",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -1441,8 +1441,15 @@ function isMarkdownBlockStart(line, nextLine = "") {
1441
1441
  );
1442
1442
  }
1443
1443
 
1444
+ function unwrapRenderableMarkdownFence(value = "") {
1445
+ const raw = String(value || "");
1446
+ const trimmed = raw.trim();
1447
+ const match = trimmed.match(/^```(?:markdown|md)\s*\n([\s\S]*?)\n```\s*$/i);
1448
+ return match ? match[1] : raw;
1449
+ }
1450
+
1444
1451
  function renderMarkdown(value) {
1445
- const lines = String(value || "").replace(/\r\n?/g, "\n").split("\n");
1452
+ const lines = unwrapRenderableMarkdownFence(value).replace(/\r\n?/g, "\n").split("\n");
1446
1453
  const html = [];
1447
1454
  let paragraph = [];
1448
1455
  let listType = "";
@@ -81,6 +81,25 @@ try {
81
81
  if ((renderedMarkdown.match(/Present/g) || []).length !== 1) {
82
82
  throw new Error("terminal markdown renderer duplicated table rows");
83
83
  }
84
+ const renderedMarkdownFence = stripMarkdown(
85
+ [
86
+ "```markdown",
87
+ "# Saved Story",
88
+ "",
89
+ "| File | Status |",
90
+ "| --- | --- |",
91
+ "| `story-ja.md` | **saved** |",
92
+ "```",
93
+ ].join("\n")
94
+ );
95
+ if (
96
+ renderedMarkdownFence.includes("code markdown") ||
97
+ renderedMarkdownFence.includes("| --- |") ||
98
+ !renderedMarkdownFence.includes("Saved Story") ||
99
+ !renderedMarkdownFence.includes("story-ja.md")
100
+ ) {
101
+ throw new Error("terminal markdown renderer did not unwrap full markdown fences");
102
+ }
84
103
  const renderedDiff = stripMarkdown(
85
104
  [
86
105
  "Diff:",
@@ -291,6 +310,7 @@ try {
291
310
  projectRoot: tempRoot,
292
311
  checks: [
293
312
  "markdown-render",
313
+ "markdown-fence-render",
294
314
  "markdown-table-no-duplicate",
295
315
  "patch-diff-render",
296
316
  "workspace-patch-event-render",
@@ -150,6 +150,17 @@ assert(nativeMarkerText.length === 0, "native marker text should not be treated
150
150
  const jsonBlockToolCalls = parseTextToolCalls('TOOL_CALLS:\n```json\n[{"name":"list_files","arguments":{"path":"/workspace"}}]\n```');
151
151
  assert(jsonBlockToolCalls.length === 1, "Venice JSON text tool-call parser did not detect JSON block calls");
152
152
  assert(jsonBlockToolCalls[0].function.arguments.includes("/workspace"), "Venice JSON text tool-call parser returned wrong arguments");
153
+ const requestedToolCalls = parseTextToolCalls(
154
+ 'Requested tools: write_file({"path":"story-ja.txt","content":"雨の夜、彼女は「また会える」と笑った。","mode":"create"})'
155
+ );
156
+ assert(requestedToolCalls.length === 1, "Requested tools parser did not detect function-call text");
157
+ assert(requestedToolCalls[0].function.name === "write_file", "Requested tools parser returned wrong tool name");
158
+ assert(requestedToolCalls[0].function.arguments.includes("story-ja.txt"), "Requested tools parser returned wrong arguments");
159
+ const multipleRequestedToolCalls = parseTextToolCalls(
160
+ 'Requested tools: list_files({"path":"."}); inspect_project({"path":".","maxDepth":2})'
161
+ );
162
+ assert(multipleRequestedToolCalls.length === 2, "Requested tools parser did not detect multiple function-call texts");
163
+ assert(multipleRequestedToolCalls[1].function.name === "inspect_project", "Requested tools parser returned wrong second requested tool");
153
164
  assert(usesTextToolProtocol({ provider: "venice", model: "gemma-4-uncensored" }), "Venice Gemma should use text tool protocol");
154
165
  assert(usesTextToolProtocol({ provider: "venice", model: "e2ee-venice-uncensored-24b-p" }), "Venice 1.1 should use text tool protocol");
155
166
  assert(usesTextToolProtocol({ provider: "venice", model: "venice-uncensored" }), "Venice legacy 1.1 should use text tool protocol");
@@ -180,6 +191,7 @@ console.log(
180
191
  "auxiliary-catalog",
181
192
  "shared-model-selectors",
182
193
  "venice-text-tool-parser",
194
+ "requested-tools-parser",
183
195
  "cli-models-command",
184
196
  "venice-shortcut",
185
197
  ],
@@ -313,7 +313,7 @@ async function createInitialState(config, sessionId) {
313
313
  ? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived."
314
314
  : "",
315
315
  config.allowFileTools
316
- ? `Workspace file tools are available in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. For large or unfamiliar repositories, call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests as relevant before editing. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
316
+ ? `Workspace file tools are available in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. For large or unfamiliar repositories, call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests as relevant before editing. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. For newly generated standalone prose/docs/stories/assets, choose a descriptive non-conflicting filename from the topic/language and use mode=create; do not overwrite existing files unless the user explicitly asked to update/replace/overwrite that file. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
317
317
  : "No workspace file tools are available.",
318
318
  config.allowWrapperTools
319
319
  ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
@@ -362,7 +362,7 @@ async function createInitialState(config, sessionId) {
362
362
  : `Shell working directory: ${config.commandCwd}`
363
363
  : "",
364
364
  config.allowFileTools
365
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large/unfamiliar codebases. Read AGINTI.md/AGENTS.md/README/manifests when relevant. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. Local preview tools available: open_workspace_file and preview_workspace.`
365
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large/unfamiliar codebases. Read AGINTI.md/AGENTS.md/README/manifests when relevant. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. For newly generated standalone content, choose descriptive non-conflicting filenames and use mode=create unless the user explicitly asked to overwrite/update. Local preview tools available: open_workspace_file and preview_workspace.`
366
366
  : "",
367
367
  projectInstructions.exists ? "AGINTI.md project instructions are loaded into system context for this run." : "AGINTI.md is not present unless you create it.",
368
368
  config.allowWrapperTools
@@ -500,7 +500,7 @@ async function applyContinuationPrompt(state, config, observers) {
500
500
  : `Shell working directory: ${config.commandCwd}. Host platform: ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
501
501
  : "",
502
502
  config.allowFileTools
503
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Read AGINTI.md/AGENTS.md/README/manifests when relevant. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. For generated local files/sites, use open_workspace_file or preview_workspace.`
503
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Read AGINTI.md/AGENTS.md/README/manifests when relevant. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. For generated local files/sites, choose descriptive non-conflicting filenames, use mode=create unless the user explicitly asked to overwrite/update, and use open_workspace_file or preview_workspace.`
504
504
  : "",
505
505
  config.allowWrapperTools
506
506
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -601,6 +601,29 @@ function sanitizeToolArgs(toolName, args) {
601
601
  return safeArgs;
602
602
  }
603
603
 
604
+ function goalClearlyAllowsOverwrite(goal = "") {
605
+ const text = String(goal || "").toLowerCase();
606
+ return (
607
+ /\b(overwrite|replace|update|modify|edit|revise|rewrite|fix|patch|change|append|refresh|regenerate|remember|instruction|instructions|memory|preference|preferences|prefer)\b/i.test(text) ||
608
+ /覆盖|覆寫|替换|替換|更新|修改|修复|修復|编辑|編輯|改写|改寫|追加|记住|記住|指令|说明|說明|偏好|上書き|置換|修正|編集/.test(text)
609
+ );
610
+ }
611
+
612
+ async function implicitOverwriteBlock(toolName, args, config, state) {
613
+ if (toolName !== "write_file" || args.mode !== "overwrite") return null;
614
+ if (goalClearlyAllowsOverwrite(state?.goal || config.goal || "")) return null;
615
+ const target = resolveWorkspacePath(config, args.path || args.file || "");
616
+ const exists = await fs
617
+ .stat(target.absolutePath)
618
+ .then((stat) => stat.isFile())
619
+ .catch(() => false);
620
+ if (!exists) return null;
621
+ return {
622
+ reason: `Refusing to overwrite existing ${target.relativePath} without an explicit update/replace request. Choose a descriptive new filename or ask the user before replacing it.`,
623
+ category: "workspace-overwrite",
624
+ };
625
+ }
626
+
604
627
  function sanitizeToolResult(result) {
605
628
  const safeResult = redactValue(result);
606
629
  if (typeof safeResult.content === "string") {
@@ -709,7 +732,7 @@ async function captureSyntheticSnapshot(store, step, config) {
709
732
  ? "Host tmux tools available: tmux_list_sessions, tmux_capture_pane, tmux_send_keys, tmux_start_session. Use them for long-running jobs and agent terminals; capture before sending input. Docker run_command containers are ephemeral, so tmux there will not persist."
710
733
  : "",
711
734
  config.allowFileTools
712
- ? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches.`
735
+ ? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches. For new standalone generated content, pick a descriptive non-conflicting filename and avoid overwriting unless explicitly requested.`
713
736
  : "Workspace file tools disabled.",
714
737
  config.allowWrapperTools
715
738
  ? `Agent wrappers available: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -805,6 +828,30 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
805
828
  };
806
829
  }
807
830
 
831
+ const overwriteBlock = await implicitOverwriteBlock(toolCall.function.name, args, config, state);
832
+ if (overwriteBlock) {
833
+ await store.appendEvent("tool.blocked", {
834
+ toolName: toolCall.function.name,
835
+ args: safeArgs,
836
+ reason: overwriteBlock.reason,
837
+ category: overwriteBlock.category,
838
+ });
839
+ observers.event("tool.blocked", {
840
+ toolName: toolCall.function.name,
841
+ args: safeArgs,
842
+ reason: overwriteBlock.reason,
843
+ category: overwriteBlock.category,
844
+ });
845
+ return {
846
+ ok: false,
847
+ blocked: true,
848
+ reason: overwriteBlock.reason,
849
+ category: overwriteBlock.category,
850
+ toolName: toolCall.function.name,
851
+ args: safeArgs,
852
+ };
853
+ }
854
+
808
855
  await store.appendEvent("tool.started", {
809
856
  toolName: toolCall.function.name,
810
857
  args: safeArgs,
@@ -241,7 +241,8 @@ function wrapTextLine(value = "", width = 72) {
241
241
  }
242
242
 
243
243
  export function stripMarkdown(text) {
244
- const lines = String(text || "").split(/\r?\n/);
244
+ const unwrapped = unwrapRenderableMarkdownFence(text);
245
+ const lines = String(unwrapped || "").split(/\r?\n/);
245
246
  let inFence = false;
246
247
  let diffContext = 0;
247
248
  const rendered = [];
@@ -304,6 +305,13 @@ export function stripMarkdown(text) {
304
305
  return rendered.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
305
306
  }
306
307
 
308
+ function unwrapRenderableMarkdownFence(text = "") {
309
+ const raw = String(text || "");
310
+ const trimmed = raw.trim();
311
+ const match = trimmed.match(/^```(?:markdown|md)\s*\n([\s\S]*?)\n```\s*$/i);
312
+ return match ? match[1] : raw;
313
+ }
314
+
307
315
  function renderPatchLine(line = "", { active = false } = {}) {
308
316
  const value = String(line || "");
309
317
  if (/^diff --git\s+/.test(value)) return color(value, ansi.bold, ansi.cyan);
@@ -140,9 +140,10 @@ function messagesWithTextToolProtocol(config, messages, tools) {
140
140
 
141
141
  export function parseTextToolCalls(content = "") {
142
142
  const text = String(content || "");
143
- if (!text.includes("[TOOL_CALLS]") && !/TOOL_CALLS\s*:/i.test(text)) return [];
143
+ if (!text.includes("[TOOL_CALLS]") && !/TOOL_CALLS\s*:/i.test(text) && !/Requested tools?\s*:/i.test(text)) return [];
144
144
 
145
145
  const calls = [];
146
+ for (const call of parseRequestedToolCalls(text)) calls.push(call);
146
147
  const jsonBlock = text.match(/TOOL_CALLS\s*:\s*```(?:json)?\s*([\s\S]*?)```/i);
147
148
  if (jsonBlock?.[1]) {
148
149
  try {
@@ -195,10 +196,75 @@ export function parseTextToolCalls(content = "") {
195
196
  return calls;
196
197
  }
197
198
 
199
+ function findMatchingParen(text = "", openIndex = 0) {
200
+ let depth = 0;
201
+ let quote = "";
202
+ let escaped = false;
203
+ for (let index = openIndex; index < text.length; index += 1) {
204
+ const char = text[index];
205
+ if (escaped) {
206
+ escaped = false;
207
+ continue;
208
+ }
209
+ if (quote) {
210
+ if (char === "\\") escaped = true;
211
+ else if (char === quote) quote = "";
212
+ continue;
213
+ }
214
+ if (char === '"' || char === "'") {
215
+ quote = char;
216
+ continue;
217
+ }
218
+ if (char === "(") depth += 1;
219
+ if (char === ")") {
220
+ depth -= 1;
221
+ if (depth === 0) return index;
222
+ }
223
+ }
224
+ return -1;
225
+ }
226
+
227
+ function parseRequestedToolCalls(content = "") {
228
+ const marker = String(content || "").match(/Requested tools?\s*:/i);
229
+ if (!marker) return [];
230
+
231
+ const text = String(content).slice(marker.index + marker[0].length);
232
+ const calls = [];
233
+ let offset = 0;
234
+ while (offset < text.length) {
235
+ const match = text.slice(offset).match(/([A-Za-z0-9_-]+)\s*\(/);
236
+ if (!match) break;
237
+
238
+ const name = match[1]?.trim();
239
+ const openIndex = offset + match.index + match[0].lastIndexOf("(");
240
+ const closeIndex = findMatchingParen(text, openIndex);
241
+ if (!name || closeIndex < 0) break;
242
+
243
+ const rawArgs = text.slice(openIndex + 1, closeIndex).trim() || "{}";
244
+ try {
245
+ JSON.parse(rawArgs);
246
+ } catch {
247
+ offset = closeIndex + 1;
248
+ continue;
249
+ }
250
+ calls.push({
251
+ id: `text-tool-${calls.length + 1}`,
252
+ type: "function",
253
+ function: {
254
+ name,
255
+ arguments: rawArgs,
256
+ },
257
+ });
258
+ offset = closeIndex + 1;
259
+ }
260
+ return calls;
261
+ }
262
+
198
263
  function textBeforeToolCallMarker(content = "") {
199
264
  return String(content || "")
200
265
  .split("[TOOL_CALLS]")[0]
201
266
  .split("TOOL_CALLS:")[0]
267
+ .split(/Requested tools?\s*:/i)[0]
202
268
  .split("<|tool_call>")[0]
203
269
  .trim();
204
270
  }
@@ -414,7 +480,7 @@ export async function createPlan(client, config, state) {
414
480
  ? "Host tmux tools are enabled for long-running sessions. Plan to use tmux_start_session for durable jobs, tmux_capture_pane to monitor, tmux_send_keys to interact after capture, and tmux_list_sessions to discover existing sessions. Do not install or run tmux inside Docker run_command containers; those containers are short-lived and cannot preserve tmux servers."
415
481
  : "",
416
482
  config.allowFileTools
417
- ? `Workspace file tools are enabled in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. For large or unfamiliar repos, plan to call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests and exact files. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
483
+ ? `Workspace file tools are enabled in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. For large or unfamiliar repos, plan to call inspect_project first, then search/read AGINTI.md/AGENTS.md/README/manifests and exact files. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For newly generated standalone prose/docs/stories/assets, choose a descriptive non-conflicting filename from the topic/language instead of generic names like story.txt or output.txt; do not overwrite existing files unless the user explicitly asked to update/replace/overwrite that file. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
418
484
  : "",
419
485
  projectInstructions?.exists
420
486
  ? `Project instructions: AGINTI.md is loaded from ${projectInstructions.path}${projectInstructions.truncated ? " (truncated)" : ""}. Follow it and update it with file tools when the user asks to remember or change project instructions.`
@@ -438,6 +504,7 @@ export async function createPlan(client, config, state) {
438
504
  engineeringGuidance,
439
505
  "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.",
440
506
  "Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
507
+ "When saving new standalone content, think of a specific filename from the request and use mode=create. If that name already exists and the user did not ask to replace it, choose a safe variant such as topic-language-v2.md instead of overwriting.",
441
508
  "For large apps, websites, LaTeX documents, Python/C/shell projects, or system tasks, plan a coherent minimal implementation, then use tools to create files, run checks, and publish artifacts.",
442
509
  "For LaTeX/PDF work, first check whether latexmk or pdflatex already exists in the active host/Docker environment; compile with the existing toolchain before installing packages or rebuilding Docker.",
443
510
  "For web search or current information tasks, plan to use browser tools or safe shell network tools when allowed, then preserve useful source notes if the output depends on them.",
@@ -820,7 +887,7 @@ export async function requestNextStep(client, config, messages) {
820
887
  function: {
821
888
  name: "write_file",
822
889
  description:
823
- "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.",
890
+ "Create or overwrite a small UTF-8 workspace file. Use mode=create with a descriptive non-conflicting filename for newly generated standalone content. Use mode=overwrite only when the user explicitly asks to replace/update that file, or after reading the file and the task is clearly to modify it. Secret paths, .git, node_modules writes, and outside-workspace paths are blocked. The runtime records before/after hashes and a compact diff.",
824
891
  parameters: {
825
892
  type: "object",
826
893
  properties: {