@lazyingart/agintiflow 0.1.0 → 0.2.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.
@@ -8,12 +8,103 @@ import { SessionStore } from "./session-store.js";
8
8
  import { captureSnapshot } from "./snapshot.js";
9
9
  import { checkToolUse } from "./guardrails.js";
10
10
  import { ensureDockerSandboxReady, runDockerSandboxCommand } from "./docker-sandbox.js";
11
- import { runAgentWrapper, wrapperStatusText } from "./tool-wrappers.js";
11
+ import { normalizeWrapperName, runAgentWrapper, wrapperStatusText } from "./tool-wrappers.js";
12
12
  import { evaluateCommandPolicy } from "./command-policy.js";
13
13
  import { redactSensitiveText, redactValue } from "./redaction.js";
14
+ import { executeWorkspaceTool, summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
15
+ import { normalizeCanvasPayload } from "./artifact-tunnel.js";
14
16
 
15
17
  const exec = promisify(execCallback);
16
18
  const BROWSER_TOOLS = new Set(["open_url", "click", "type", "scroll", "press", "back"]);
19
+ const WORKSPACE_TOOLS = new Set(WORKSPACE_TOOL_NAMES);
20
+
21
+ function preserveAssistantMessage(message) {
22
+ const preserved = {
23
+ role: "assistant",
24
+ content: message.content || "",
25
+ tool_calls: message.tool_calls,
26
+ };
27
+
28
+ const reasoningContent = message.reasoning_content || message.reasoningContent;
29
+ if (reasoningContent) {
30
+ preserved.reasoning_content = reasoningContent;
31
+ }
32
+
33
+ return preserved;
34
+ }
35
+
36
+ export function repairModelMessageHistory(state, config = {}) {
37
+ if (config.provider !== "deepseek" || !Array.isArray(state?.messages)) {
38
+ return { changed: false, droppedAssistantMessages: 0, convertedAssistantMessages: 0, droppedToolMessages: 0 };
39
+ }
40
+
41
+ const repaired = [];
42
+ const droppedToolCallIds = new Set();
43
+ let droppedAssistantMessages = 0;
44
+ let convertedAssistantMessages = 0;
45
+ let droppedToolMessages = 0;
46
+
47
+ for (const message of state.messages) {
48
+ if (message.role === "assistant") {
49
+ const hasReasoningContent = Boolean(message.reasoning_content || message.reasoningContent);
50
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
51
+ const content = String(message.content || "");
52
+
53
+ if (!hasReasoningContent) {
54
+ if (content.startsWith("Execution plan:")) {
55
+ droppedAssistantMessages += 1;
56
+ continue;
57
+ }
58
+
59
+ if (toolCalls.length > 0) {
60
+ for (const call of toolCalls) {
61
+ if (call?.id) droppedToolCallIds.add(call.id);
62
+ }
63
+ droppedAssistantMessages += 1;
64
+ continue;
65
+ }
66
+
67
+ if (content.trim()) {
68
+ repaired.push({
69
+ role: "user",
70
+ content: `Previous assistant response retained as compacted history for DeepSeek thinking mode:\n${content}`,
71
+ });
72
+ convertedAssistantMessages += 1;
73
+ } else {
74
+ droppedAssistantMessages += 1;
75
+ }
76
+ continue;
77
+ }
78
+
79
+ repaired.push(preserveAssistantMessage(message));
80
+ continue;
81
+ }
82
+
83
+ if (message.role === "tool" && droppedToolCallIds.has(message.tool_call_id)) {
84
+ droppedToolMessages += 1;
85
+ continue;
86
+ }
87
+
88
+ repaired.push(message);
89
+ }
90
+
91
+ const changed =
92
+ droppedAssistantMessages > 0 ||
93
+ convertedAssistantMessages > 0 ||
94
+ droppedToolMessages > 0 ||
95
+ repaired.length !== state.messages.length;
96
+
97
+ if (changed) {
98
+ state.messages = repaired;
99
+ }
100
+
101
+ return {
102
+ changed,
103
+ droppedAssistantMessages,
104
+ convertedAssistantMessages,
105
+ droppedToolMessages,
106
+ };
107
+ }
17
108
 
18
109
  function createInitialState(config, sessionId) {
19
110
  const now = new Date().toISOString();
@@ -54,9 +145,15 @@ function createInitialState(config, sessionId) {
54
145
  ? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Read-only commands have no network. Package installs or environment setup require approved package policy and Docker workspace-write mode.`
55
146
  : "A read-only host shell command tool is available for short local inspection tasks."
56
147
  : "No shell command tool is available.",
148
+ config.allowFileTools
149
+ ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, and apply_patch. 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.`
150
+ : "No workspace file tools are available.",
57
151
  config.allowWrapperTools
58
- ? `External coding-agent wrappers are available as advisory tools only. Wrapper status: ${wrapperStatusText()}.`
152
+ ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
59
153
  : "External coding-agent wrappers are disabled.",
154
+ "A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
155
+ "For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
156
+ "For LaTeX/PDF requests, create the needed source/assets, compile with the available allowlisted TeX toolchain, and publish the resulting PDF through send_to_canvas. For subfolder documents, keep outputs beside the source. Use pdflatex-compatible figure formats such as PDF or PNG.",
60
157
  "When done, call finish with a concise result.",
61
158
  ].join(" "),
62
159
  },
@@ -68,10 +165,16 @@ function createInitialState(config, sessionId) {
68
165
  config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
69
166
  config.allowShellTool
70
167
  ? config.useDockerSandbox
71
- ? `Shell working directory mounted into Docker: ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
168
+ ? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
72
169
  : `Shell working directory: ${config.commandCwd}`
73
170
  : "",
74
- config.allowWrapperTools ? `Agent wrappers: ${wrapperStatusText()}` : "",
171
+ config.allowFileTools ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths.` : "",
172
+ config.allowWrapperTools
173
+ ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
174
+ : "",
175
+ "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
176
+ "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
177
+ "LaTeX/PDF requests should produce source artifacts and, when possible, a compiled PDF artifact. For subfolder documents, keep outputs beside the source. For figure-in-document tasks, create PDF/PNG figures that pdflatex can include.",
75
178
  ]
76
179
  .filter(Boolean)
77
180
  .join("\n"),
@@ -161,10 +264,13 @@ function applyContinuationPrompt(state, config, observers) {
161
264
  config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
162
265
  config.allowShellTool
163
266
  ? config.useDockerSandbox
164
- ? `Shell working directory mounted into Docker: ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
267
+ ? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
165
268
  : `Shell working directory: ${config.commandCwd}`
166
269
  : "",
167
- config.allowWrapperTools ? `Agent wrappers: ${wrapperStatusText()}` : "",
270
+ config.allowFileTools ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths.` : "",
271
+ config.allowWrapperTools
272
+ ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
273
+ : "",
168
274
  ]
169
275
  .filter(Boolean)
170
276
  .join("\n"),
@@ -218,6 +324,44 @@ function safeExecutionEnv() {
218
324
  };
219
325
  }
220
326
 
327
+ function hashForLog(value) {
328
+ return crypto.createHash("sha256").update(String(value ?? "")).digest("hex");
329
+ }
330
+
331
+ function sanitizeToolArgs(toolName, args) {
332
+ const safeArgs = redactValue(args);
333
+ if (toolName === "write_file" && typeof args.content === "string") {
334
+ return {
335
+ ...safeArgs,
336
+ content: `[${Buffer.byteLength(args.content, "utf8")} bytes sha256=${hashForLog(args.content)}]`,
337
+ };
338
+ }
339
+ if (toolName === "send_to_canvas" && typeof args.content === "string") {
340
+ return {
341
+ ...safeArgs,
342
+ content: `[${Buffer.byteLength(args.content, "utf8")} bytes sha256=${hashForLog(args.content)}]`,
343
+ };
344
+ }
345
+ if (toolName === "apply_patch") {
346
+ return {
347
+ ...safeArgs,
348
+ search: typeof args.search === "string" ? redactSensitiveText(args.search).slice(0, 160) : safeArgs.search,
349
+ replace: typeof args.replace === "string" ? redactSensitiveText(args.replace).slice(0, 160) : safeArgs.replace,
350
+ };
351
+ }
352
+ return safeArgs;
353
+ }
354
+
355
+ function sanitizeToolResult(result) {
356
+ const safeResult = redactValue(result);
357
+ if (typeof safeResult.content === "string") {
358
+ safeResult.contentPreview = safeResult.content.slice(0, 600);
359
+ safeResult.contentBytes = Buffer.byteLength(safeResult.content, "utf8");
360
+ delete safeResult.content;
361
+ }
362
+ return safeResult;
363
+ }
364
+
221
365
  async function runShellCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
222
366
  if (config.useDockerSandbox) {
223
367
  return runDockerSandboxCommand(command, config, policy);
@@ -246,10 +390,18 @@ async function captureSyntheticSnapshot(store, step, config) {
246
390
  config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
247
391
  config.allowShellTool
248
392
  ? config.useDockerSandbox
249
- ? `Shell tool available in Docker with mounted workspace: ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
393
+ ? `Shell tool available in Docker with mounted workspace /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
250
394
  : `Shell tool available in: ${config.commandCwd}`
251
395
  : "Shell tool disabled.",
252
- config.allowWrapperTools ? `Agent wrappers available: ${wrapperStatusText()}` : "Agent wrappers disabled.",
396
+ config.allowFileTools
397
+ ? `Workspace file tools available in: ${config.commandCwd}. Use workspace-relative paths.`
398
+ : "Workspace file tools disabled.",
399
+ config.allowWrapperTools
400
+ ? `Agent wrappers available: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
401
+ : "Agent wrappers disabled.",
402
+ "Canvas/artifacts tunnel available through send_to_canvas.",
403
+ "For draw/plot/graph/chart/diagram/figure requests, publish a canvas artifact proactively.",
404
+ "For LaTeX/PDF requests, publish the source and compiled PDF artifacts when available. Keep subfolder outputs beside their source and use pdflatex-compatible figure formats.",
253
405
  "Use open_url only if the task actually needs the web.",
254
406
  ]
255
407
  .filter(Boolean)
@@ -274,7 +426,7 @@ async function buildSnapshot(browserState, store, step, config) {
274
426
 
275
427
  async function executeTool(browserState, toolCall, snapshot, config, store, observers, state) {
276
428
  const args = JSON.parse(toolCall.function.arguments || "{}");
277
- const safeArgs = redactValue(args);
429
+ const safeArgs = sanitizeToolArgs(toolCall.function.name, args);
278
430
  const guard = checkToolUse({
279
431
  toolName: toolCall.function.name,
280
432
  args,
@@ -301,7 +453,10 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
301
453
  ok: false,
302
454
  blocked: true,
303
455
  reason: guard.reason,
456
+ category: guard.category,
457
+ needsApproval: guard.needsApproval,
304
458
  toolName: toolCall.function.name,
459
+ args: safeArgs,
305
460
  };
306
461
  }
307
462
 
@@ -361,6 +516,42 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
361
516
  await new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000));
362
517
  }
363
518
  break;
519
+ case "list_files":
520
+ case "read_file":
521
+ case "search_files":
522
+ case "write_file":
523
+ case "apply_patch": {
524
+ const result = await executeWorkspaceTool(toolCall.function.name, args, config);
525
+ const eventResult = sanitizeToolResult(result);
526
+ if (result.blocked) {
527
+ await store.appendEvent("tool.blocked", {
528
+ toolName: toolCall.function.name,
529
+ args: safeArgs,
530
+ reason: result.reason,
531
+ category: result.category,
532
+ });
533
+ observers.event("tool.blocked", {
534
+ toolName: toolCall.function.name,
535
+ args: safeArgs,
536
+ reason: result.reason,
537
+ category: result.category,
538
+ });
539
+ return result;
540
+ }
541
+
542
+ await store.appendEvent("tool.completed", eventResult);
543
+ observers.event("tool.completed", eventResult);
544
+ if (result.change) {
545
+ const change = {
546
+ ...result.change,
547
+ toolName: toolCall.function.name,
548
+ commandCwd: config.commandCwd,
549
+ };
550
+ await store.appendEvent("file.changed", change);
551
+ observers.event("file.changed", change);
552
+ }
553
+ return result;
554
+ }
364
555
  case "run_command": {
365
556
  const policy = evaluateCommandPolicy(String(args.command), config);
366
557
  if (config.useDockerSandbox) {
@@ -403,6 +594,63 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
403
594
  observers.event("tool.completed", result);
404
595
  return result;
405
596
  }
597
+ case "send_to_canvas": {
598
+ const normalized = normalizeCanvasPayload(args, config);
599
+ if (!normalized.ok) {
600
+ await store.appendEvent("tool.blocked", {
601
+ toolName: "send_to_canvas",
602
+ args: safeArgs,
603
+ reason: normalized.reason,
604
+ category: "canvas",
605
+ });
606
+ observers.event("tool.blocked", {
607
+ toolName: "send_to_canvas",
608
+ args: safeArgs,
609
+ reason: normalized.reason,
610
+ category: "canvas",
611
+ });
612
+ return {
613
+ ok: false,
614
+ blocked: true,
615
+ reason: normalized.reason,
616
+ toolName: "send_to_canvas",
617
+ };
618
+ }
619
+
620
+ const canvasItem = {
621
+ ...normalized.payload,
622
+ toolName: "send_to_canvas",
623
+ commandCwd: config.commandCwd,
624
+ };
625
+ await store.appendEvent("canvas.item", canvasItem);
626
+ observers.event("canvas.item", canvasItem);
627
+ if (canvasItem.selected) {
628
+ await store.appendEvent("canvas.selected", {
629
+ artifactId: canvasItem.artifactId,
630
+ title: canvasItem.title,
631
+ source: "agent",
632
+ });
633
+ observers.event("canvas.selected", {
634
+ artifactId: canvasItem.artifactId,
635
+ title: canvasItem.title,
636
+ source: "agent",
637
+ });
638
+ }
639
+
640
+ const result = {
641
+ ok: true,
642
+ toolName: "send_to_canvas",
643
+ args: safeArgs,
644
+ artifactId: canvasItem.artifactId,
645
+ title: canvasItem.title,
646
+ kind: canvasItem.kind,
647
+ path: canvasItem.path,
648
+ selected: canvasItem.selected,
649
+ };
650
+ await store.appendEvent("tool.completed", result);
651
+ observers.event("tool.completed", result);
652
+ return result;
653
+ }
406
654
  case "finish":
407
655
  return { ok: true, done: true, result: String(args.result || ""), toolName: "finish" };
408
656
  default:
@@ -445,6 +693,9 @@ export async function runAgent(config) {
445
693
  const observers = createObservers(config);
446
694
  const browserState = createBrowserState();
447
695
 
696
+ if (config.allowFileTools || config.allowShellTool) {
697
+ await fs.mkdir(config.commandCwd, { recursive: true });
698
+ }
448
699
  await store.ensure();
449
700
 
450
701
  let state = await store.loadState();
@@ -484,16 +735,19 @@ export async function runAgent(config) {
484
735
  if (!state.plan) {
485
736
  const plan = await createPlan(client, config, state);
486
737
  state.plan = plan;
487
- state.messages.push({
488
- role: "assistant",
489
- content: `Execution plan:\n${plan}`,
490
- });
491
738
  await store.savePlan(plan);
492
739
  await store.appendEvent("plan.created", { plan });
493
740
  await store.saveState(state);
494
741
  observers.event("plan.created", { plan });
495
742
  }
496
743
 
744
+ const repair = repairModelMessageHistory(state, config);
745
+ if (repair.changed) {
746
+ await store.appendEvent("history.repaired", repair);
747
+ observers.event("history.repaired", repair);
748
+ await store.saveState(state);
749
+ }
750
+
497
751
  observers.log("session.context", {
498
752
  sessionId,
499
753
  provider: config.provider,
@@ -503,7 +757,9 @@ export async function runAgent(config) {
503
757
  commandCwd: config.commandCwd,
504
758
  allowShellTool: config.allowShellTool,
505
759
  allowWrapperTools: config.allowWrapperTools,
760
+ preferredWrapper: normalizeWrapperName(config.preferredWrapper),
506
761
  wrappers: config.allowWrapperTools ? wrapperStatusText() : "",
762
+ workspaceFileTools: summarizeWorkspaceTools(config),
507
763
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
508
764
  sandboxMode: config.sandboxMode,
509
765
  packageInstallPolicy: config.packageInstallPolicy,
@@ -549,13 +805,18 @@ export async function runAgent(config) {
549
805
  elements: snapshot.elements,
550
806
  browserOpen: Boolean(browserState.page),
551
807
  shellToolAvailable: config.allowShellTool,
808
+ fileToolsAvailable: config.allowFileTools,
809
+ workspaceFileTools: summarizeWorkspaceTools(config),
552
810
  agentWrappersAvailable: config.allowWrapperTools,
811
+ preferredWrapper: normalizeWrapperName(config.preferredWrapper),
553
812
  agentWrappers: config.allowWrapperTools ? wrapperStatusText() : "",
554
813
  shellSandbox: config.useDockerSandbox ? "docker" : "host",
555
814
  sandboxMode: config.sandboxMode,
556
815
  packageInstallPolicy: config.packageInstallPolicy,
557
816
  commandCwd: config.commandCwd,
817
+ plan: state.plan || "",
558
818
  suggestedStartUrl: config.startUrl || "",
819
+ canvasArtifactsAvailable: true,
559
820
  })}`,
560
821
  });
561
822
 
@@ -565,11 +826,7 @@ export async function runAgent(config) {
565
826
  throw new Error("Model returned no assistant message.");
566
827
  }
567
828
 
568
- state.messages.push({
569
- role: "assistant",
570
- content: assistantMessage.content || "",
571
- tool_calls: assistantMessage.tool_calls,
572
- });
829
+ state.messages.push(preserveAssistantMessage(assistantMessage));
573
830
 
574
831
  await store.appendEvent("model.responded", {
575
832
  step,
@@ -618,13 +875,19 @@ export async function runAgent(config) {
618
875
 
619
876
  if (toolResult.toolName === "run_command") {
620
877
  observers.log("command.output", {
621
- command: redactSensitiveText(toolResult.args.command),
622
- stdout: toolResult.stdout,
623
- stderr: toolResult.stderr,
878
+ command: redactSensitiveText(toolResult.args?.command || ""),
879
+ stdout: toolResult.stdout || "",
880
+ stderr: toolResult.stderr || "",
624
881
  commandPolicy: toolResult.commandPolicy,
882
+ blocked: Boolean(toolResult.blocked),
883
+ error: toolResult.error || toolResult.reason || "",
625
884
  });
626
885
  }
627
886
 
887
+ if (WORKSPACE_TOOLS.has(toolResult.toolName)) {
888
+ observers.log("workspace.output", sanitizeToolResult(toolResult));
889
+ }
890
+
628
891
  if (toolResult.toolName === "delegate_agent") {
629
892
  observers.log("wrapper.output", {
630
893
  wrapper: toolResult.wrapper,
@@ -679,6 +942,8 @@ export async function runAgent(config) {
679
942
  return {
680
943
  sessionId,
681
944
  result: "",
945
+ stopped: true,
946
+ reason: "max_steps_reached",
682
947
  };
683
948
  } finally {
684
949
  await closeBrowser(browserState, store);