@lazyingart/agintiflow 0.20.11 → 0.20.13

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.11",
3
+ "version": "0.20.13",
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",
@@ -9,7 +9,7 @@ import {
9
9
  modelsForProviderGroup,
10
10
  selectModelRoute,
11
11
  } from "../src/model-routing.js";
12
- import { parseTextToolCalls, usesTextToolProtocol } from "../src/model-client.js";
12
+ import { normalizeTextToolCallResponse, parseTextToolCalls, usesTextToolProtocol } from "../src/model-client.js";
13
13
  import { modelRoleChoices } from "../src/interactive-cli.js";
14
14
 
15
15
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -161,6 +161,33 @@ const multipleRequestedToolCalls = parseTextToolCalls(
161
161
  );
162
162
  assert(multipleRequestedToolCalls.length === 2, "Requested tools parser did not detect multiple function-call texts");
163
163
  assert(multipleRequestedToolCalls[1].function.name === "inspect_project", "Requested tools parser returned wrong second requested tool");
164
+ const malformedRequestedToolResponse = normalizeTextToolCallResponse({
165
+ choices: [
166
+ {
167
+ message: {
168
+ role: "assistant",
169
+ content: 'Requested tools: write_file({"path":"story.md","content":"unfinished',
170
+ },
171
+ },
172
+ ],
173
+ });
174
+ const malformedMessage = malformedRequestedToolResponse.choices[0].message;
175
+ assert(malformedMessage.tool_calls?.[0]?.function?.name === "wait", "malformed requested tool text should trigger a safe retry tool");
176
+ assert(!malformedMessage.content.includes("write_file("), "malformed requested tool text should not be surfaced as assistant content");
177
+ const cleanedMalformedSuffix = normalizeTextToolCallResponse({
178
+ choices: [
179
+ {
180
+ message: {
181
+ role: "assistant",
182
+ content: 'Here is a normal answer. Requested tools: write_file({"path":"story.md","content":"unfinished',
183
+ },
184
+ },
185
+ ],
186
+ });
187
+ assert(
188
+ cleanedMalformedSuffix.choices[0].message.content === "Here is a normal answer.",
189
+ "malformed tool suffix should be stripped from otherwise usable assistant content"
190
+ );
164
191
  assert(usesTextToolProtocol({ provider: "venice", model: "gemma-4-uncensored" }), "Venice Gemma should use text tool protocol");
165
192
  assert(usesTextToolProtocol({ provider: "venice", model: "e2ee-venice-uncensored-24b-p" }), "Venice 1.1 should use text tool protocol");
166
193
  assert(usesTextToolProtocol({ provider: "venice", model: "venice-uncensored" }), "Venice legacy 1.1 should use text tool protocol");
@@ -192,6 +219,7 @@ console.log(
192
219
  "shared-model-selectors",
193
220
  "venice-text-tool-parser",
194
221
  "requested-tools-parser",
222
+ "malformed-text-tool-retry",
195
223
  "cli-models-command",
196
224
  "venice-shortcut",
197
225
  ],
@@ -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);
@@ -107,6 +107,7 @@ function textToolProtocolPrompt(tools = []) {
107
107
  'A strict id form is also accepted: [TOOL_CALLS]tool_name[ARGS]call_short_id[ARGS]{"arg":"value"}',
108
108
  'A JSON block form is accepted too: TOOL_CALLS: ```json [{"name":"tool_name","arguments":{"arg":"value"}}] ```',
109
109
  "Return only one or more TOOL_CALLS blocks when calling tools; do not wrap them in markdown.",
110
+ "Keep tool-call JSON valid and complete. For long write_file content, prefer a concise complete file or smaller follow-up edits over emitting huge/truncated JSON.",
110
111
  "If no tool is needed, answer normally.",
111
112
  "Available text tools:",
112
113
  ...toolLines,
@@ -140,7 +141,7 @@ function messagesWithTextToolProtocol(config, messages, tools) {
140
141
 
141
142
  export function parseTextToolCalls(content = "") {
142
143
  const text = String(content || "");
143
- if (!text.includes("[TOOL_CALLS]") && !/TOOL_CALLS\s*:/i.test(text) && !/Requested tools?\s*:/i.test(text)) return [];
144
+ if (!hasTextToolCallMarker(text)) return [];
144
145
 
145
146
  const calls = [];
146
147
  for (const call of parseRequestedToolCalls(text)) calls.push(call);
@@ -196,6 +197,11 @@ export function parseTextToolCalls(content = "") {
196
197
  return calls;
197
198
  }
198
199
 
200
+ function hasTextToolCallMarker(content = "") {
201
+ const text = String(content || "");
202
+ return text.includes("[TOOL_CALLS]") || /TOOL_CALLS\s*:/i.test(text) || /Requested tools?\s*:/i.test(text);
203
+ }
204
+
199
205
  function findMatchingParen(text = "", openIndex = 0) {
200
206
  let depth = 0;
201
207
  let quote = "";
@@ -269,14 +275,30 @@ function textBeforeToolCallMarker(content = "") {
269
275
  .trim();
270
276
  }
271
277
 
272
- function normalizeTextToolCallResponse(response) {
278
+ export function normalizeTextToolCallResponse(response) {
273
279
  const message = response?.choices?.[0]?.message;
274
280
  if (!message || Array.isArray(message.tool_calls) && message.tool_calls.length > 0) return response;
275
281
 
276
282
  const calls = parseTextToolCalls(message.content || "");
277
283
  if (calls.length === 0) {
278
284
  const cleanedContent = textBeforeToolCallMarker(message.content || "");
279
- if (!cleanedContent || cleanedContent === message.content) return response;
285
+ if (!hasTextToolCallMarker(message.content || "")) return response;
286
+ if (cleanedContent) {
287
+ return {
288
+ ...response,
289
+ choices: response.choices.map((choice, index) =>
290
+ index === 0
291
+ ? {
292
+ ...choice,
293
+ message: {
294
+ ...message,
295
+ content: cleanedContent,
296
+ },
297
+ }
298
+ : choice
299
+ ),
300
+ };
301
+ }
280
302
  return {
281
303
  ...response,
282
304
  choices: response.choices.map((choice, index) =>
@@ -285,7 +307,18 @@ function normalizeTextToolCallResponse(response) {
285
307
  ...choice,
286
308
  message: {
287
309
  ...message,
288
- content: cleanedContent,
310
+ content:
311
+ "The previous text tool request was malformed or truncated and was not executed. Retry with one valid tool call using the configured tool interface. For long files, write a complete concise file or split the work into smaller valid edits; do not show raw tool-call JSON to the user.",
312
+ tool_calls: [
313
+ {
314
+ id: "text-tool-retry-1",
315
+ type: "function",
316
+ function: {
317
+ name: "wait",
318
+ arguments: JSON.stringify({ ms: 1 }),
319
+ },
320
+ },
321
+ ],
289
322
  },
290
323
  }
291
324
  : choice
@@ -480,7 +513,7 @@ export async function createPlan(client, config, state) {
480
513
  ? "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."
481
514
  : "",
482
515
  config.allowFileTools
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 generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
516
+ ? `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.`
484
517
  : "",
485
518
  projectInstructions?.exists
486
519
  ? `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.`
@@ -504,6 +537,7 @@ export async function createPlan(client, config, state) {
504
537
  engineeringGuidance,
505
538
  "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.",
506
539
  "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.",
540
+ "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.",
507
541
  "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.",
508
542
  "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.",
509
543
  "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.",
@@ -886,7 +920,7 @@ export async function requestNextStep(client, config, messages) {
886
920
  function: {
887
921
  name: "write_file",
888
922
  description:
889
- "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.",
923
+ "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.",
890
924
  parameters: {
891
925
  type: "object",
892
926
  properties: {