@lazyingart/agintiflow 0.8.11 → 0.9.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/README.md CHANGED
@@ -28,7 +28,7 @@ It is designed for workflows where an AI agent should act, but every tool, log,
28
28
  | Core loop | Plan -> use tools -> log events -> finish or resume |
29
29
  | Browser control | Playwright, lazy browser startup, domain allowlists |
30
30
  | Model layer | Smart routing over DeepSeek fast/pro presets with manual OpenAI-compatible fallback |
31
- | Local tools | Guarded workspace file tools, optional shell commands, Docker sandbox support, and advisory agent wrappers |
31
+ | Local tools | Guarded workspace file tools, Codex-style patching, optional shell commands, Docker sandbox support, and advisory agent wrappers |
32
32
  | Memory | Session state, persisted web settings, chat continuation |
33
33
  | Operator UX | Multilingual web UI with provider selection, run output, and conversation history |
34
34
 
@@ -53,6 +53,10 @@ aginti login deepseek
53
53
  # inside chat, use /login or /auth
54
54
  # or non-interactively:
55
55
  printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
56
+
57
+ # optional image-generation auxiliary skill:
58
+ aginti login grsai
59
+ # inside chat, use /auxilliary grsai or /auxiliary grsai
56
60
  ```
57
61
 
58
62
  Start an interactive Codex-style CLI chat from any project folder:
@@ -65,6 +69,10 @@ aginti chat
65
69
 
66
70
  Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
67
71
 
72
+ For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSeek v4 pro by default and exposes `apply_patch` as a deterministic workspace tool. It supports exact replacements, Codex-style patch envelopes, and unified diffs, with preflight checks, path guardrails, hashes, and compact per-file diffs. See [docs/patch-tools.md](docs/patch-tools.md).
73
+
74
+ For raster image work, AgInTiFlow has an optional `image_generation` skill backed by the `generate_image` tool and a local `GRSAI` key. The skill tells DeepSeek when image generation is appropriate; the tool calls GRS AI Nano Banana, saves manifests/images under `artifacts/images`, and sends the result to the canvas. See [docs/auxiliary-image-generation.md](docs/auxiliary-image-generation.md).
75
+
68
76
  Launch the local web UI from an installed package:
69
77
 
70
78
  ```bash
@@ -0,0 +1,62 @@
1
+ # Auxiliary Image Generation
2
+
3
+ AgInTiFlow separates **skills** from **tools**:
4
+
5
+ - A skill is instruction and routing context that teaches the agent when a capability is useful.
6
+ - A tool is the deterministic callable function that performs the action and writes auditable artifacts.
7
+
8
+ The optional image-generation skill is `image_generation`. Its tool is `generate_image`.
9
+
10
+ ## Setup
11
+
12
+ Store the GRS AI key project-locally:
13
+
14
+ ```bash
15
+ aginti login grsai
16
+ # or
17
+ printf '%s' "$GRSAI" | aginti keys set grsai --stdin
18
+ ```
19
+
20
+ Inside interactive chat, use either spelling:
21
+
22
+ ```text
23
+ /auxilliary grsai
24
+ /auxiliary grsai
25
+ ```
26
+
27
+ The key is saved in `.aginti/.env` as `GRSAI` with `0600` permissions. The CLI and web app only report whether the key exists; they never return the raw value.
28
+
29
+ ## Runtime Flow
30
+
31
+ For image, cover, poster, illustration, photo, or logo-concept requests, the model can call:
32
+
33
+ ```json
34
+ {
35
+ "prompt": "A polished cyan robot painting a circuit-board river, high-end product illustration",
36
+ "outputDir": "artifacts/images/robot-cover",
37
+ "outputStem": "robot-cover",
38
+ "aspectRatio": "1:1",
39
+ "imageSize": "2K"
40
+ }
41
+ ```
42
+
43
+ The tool uses the GRS AI Nano Banana API:
44
+
45
+ - `POST https://grsaiapi.com/v1/draw/nano-banana`
46
+ - `POST https://grsaiapi.com/v1/draw/result`
47
+ - `Authorization: Bearer <GRSAI>`
48
+
49
+ Saved workspace artifacts:
50
+
51
+ - `prompt.txt`
52
+ - `request_payload.redacted.json`
53
+ - `submit_response.json`
54
+ - `result_response.json`
55
+ - `task_manifest.json`
56
+ - generated image files, for example `image.png`
57
+
58
+ Generated images are sent to the canvas automatically when available.
59
+
60
+ ## Guardrails
61
+
62
+ Output paths must stay inside the project workspace. Secret paths, `.git`, `node_modules` writes, and oversized reference images are blocked. Reference images may be workspace files, HTTPS URLs, or data URLs.
@@ -0,0 +1,17 @@
1
+ # Patch Tools
2
+
3
+ AgInTiFlow exposes a deterministic `apply_patch` workspace tool for coding-agent edits. It is designed for DeepSeek v4 pro and other routed models to make auditable code changes without relying on free-form shell redirection.
4
+
5
+ ## Supported Patch Modes
6
+
7
+ - Exact replacement: pass `path`, `search`, `replace`, and optionally `expectedReplacements` or `baseHash`.
8
+ - Codex-style patch envelope: pass `patch` with `*** Begin Patch`, `*** Update File`, `*** Add File`, `*** Delete File`, and `*** End Patch`.
9
+ - Unified diff: pass `patch` with standard `--- a/file`, `+++ b/file`, and `@@` hunks.
10
+
11
+ All paths must stay inside the configured workspace. Secret-like paths, `.git`, `node_modules` writes, binary files, and huge files are blocked. Multi-file patches are preflighted before writing, and each changed file records before/after hashes plus a compact diff in the session events.
12
+
13
+ ## Agent Workflow
14
+
15
+ For large codebases, the model should first use `list_files`, `search_files`, and `read_file` to identify the relevant files. It should then call `apply_patch`, run safe tests or linters when available, and summarize changed files and residual risk.
16
+
17
+ Smart routing treats patch/refactor/edit/database tasks as complex work, so DeepSeek v4 pro is selected by default unless the user explicitly chooses another route.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.8.11",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
@@ -41,6 +41,7 @@
41
41
  "scripts/install-docker-ubuntu.sh",
42
42
  "scripts/setup-agent-toolchain-docker.sh",
43
43
  "scripts/real-deepseek-capabilities.js",
44
+ "scripts/smoke-auxiliary-tools.js",
44
45
  "scripts/smoke-cli-chat.js",
45
46
  "scripts/smoke-coding-tools.js",
46
47
  "scripts/smoke-capabilities.js",
@@ -61,12 +62,13 @@
61
62
  "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check src/*.js",
62
63
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
63
64
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
65
+ "smoke:auxiliary-tools": "node scripts/smoke-auxiliary-tools.js",
64
66
  "smoke:cli-chat": "node scripts/smoke-cli-chat.js",
65
67
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
66
68
  "smoke:inbox": "node scripts/smoke-inbox.js",
67
69
  "smoke:web-api": "node scripts/smoke-web-api.js",
68
70
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
69
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:capabilities && npm run smoke:cli-chat && npm run smoke:inbox",
71
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:capabilities && npm run smoke:cli-chat && npm run smoke:inbox",
70
72
  "pack:dry-run": "npm pack --dry-run",
71
73
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
72
74
  },
package/public/app.js CHANGED
@@ -24,7 +24,7 @@ const translations = {
24
24
  setupHelp:
25
25
  "DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.",
26
26
  setupEnvHelp:
27
- "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.",
27
+ "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, LLM_API_KEY, and optional GRSAI for image generation. Mock mode remains available for local tests.",
28
28
  setupProviderLabel: "Provider",
29
29
  setupKeyLabel: "API key",
30
30
  saveKeyButton: "Save local key",
@@ -68,6 +68,7 @@ const translations = {
68
68
  headlessLabel: "Headless browser",
69
69
  shellToolLabel: "Enable shell tool",
70
70
  fileToolLabel: "Enable file tools",
71
+ auxiliaryToolLabel: "Enable auxiliary skills",
71
72
  wrapperToolLabel: "Enable agent wrappers",
72
73
  preferredWrapperLabel: "Preferred wrapper",
73
74
  dockerSandboxLabel: "Use Docker sandbox",
@@ -648,6 +649,7 @@ const logsEl = document.querySelector("#logs");
648
649
  const runMetaEl = document.querySelector("#run-meta");
649
650
  const stopRunButton = document.querySelector("#stop-run");
650
651
  const keyStatusEl = document.querySelector("#key-status");
652
+ const allowAuxiliaryToolsField = document.querySelector("#allowAuxiliaryTools");
651
653
  const allowWrapperToolsField = document.querySelector("#allowWrapperTools");
652
654
  const preferredWrapperField = document.querySelector("#preferredWrapper");
653
655
  const wrapperStatusEl = document.querySelector("#wrapper-status");
@@ -744,7 +746,9 @@ function renderKeyStatus(status = lastKeyStatus) {
744
746
  if (!status) return;
745
747
  keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
746
748
  status.openai ? t("availableLabel") : t("missingLabel")
747
- } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · ${t("mockLabel")} ${
749
+ } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · GRS AI ${
750
+ status.grsai ? t("availableLabel") : t("missingLabel")
751
+ } · ${t("mockLabel")} ${
748
752
  status.mock ? t("availableLabel") : t("missingLabel")
749
753
  }`;
750
754
  if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek);
@@ -979,6 +983,7 @@ function formPayload() {
979
983
  headless: document.querySelector("#headless").checked,
980
984
  allowShellTool: document.querySelector("#allowShellTool").checked,
981
985
  allowFileTools: document.querySelector("#allowFileTools").checked,
986
+ allowAuxiliaryTools: allowAuxiliaryToolsField?.checked ?? true,
982
987
  allowWrapperTools: allowWrapperToolsField.checked,
983
988
  preferredWrapper: preferredWrapperField.value,
984
989
  taskProfile: taskProfileField?.value || "auto",
@@ -2167,6 +2172,7 @@ async function loadConfig() {
2167
2172
  packageInstallPolicyField.value = prefs.packageInstallPolicy || "allow";
2168
2173
  document.querySelector("#allowShellTool").checked = prefs.allowShellTool ?? true;
2169
2174
  document.querySelector("#allowFileTools").checked = prefs.allowFileTools ?? true;
2175
+ if (allowAuxiliaryToolsField) allowAuxiliaryToolsField.checked = prefs.allowAuxiliaryTools ?? true;
2170
2176
  allowWrapperToolsField.checked = prefs.allowWrapperTools ?? false;
2171
2177
  preferredWrapperField.value = prefs.preferredWrapper || "codex";
2172
2178
  document.querySelector("#dockerSandboxImage").value = prefs.dockerSandboxImage || "agintiflow-sandbox:latest";
package/public/index.html CHANGED
@@ -57,6 +57,7 @@
57
57
  <select id="setup-provider">
58
58
  <option value="deepseek">DeepSeek</option>
59
59
  <option value="openai">OpenAI</option>
60
+ <option value="grsai">GRS AI image</option>
60
61
  </select>
61
62
  </label>
62
63
  <label>
@@ -175,6 +176,11 @@
175
176
  <span class="switch" aria-hidden="true"></span>
176
177
  <span data-i18n="fileToolLabel">Enable file tools</span>
177
178
  </label>
179
+ <label class="switch-label">
180
+ <input id="allowAuxiliaryTools" name="allowAuxiliaryTools" type="checkbox" checked />
181
+ <span class="switch" aria-hidden="true"></span>
182
+ <span data-i18n="auxiliaryToolLabel">Enable auxiliary skills</span>
183
+ </label>
178
184
  <label class="switch-label">
179
185
  <input id="allowPasswords" name="allowPasswords" type="checkbox" />
180
186
  <span class="switch" aria-hidden="true"></span>
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { runAgent } from "../src/agent-runner.js";
7
+ import { generateImage, listAuxiliarySkills } from "../src/auxiliary-tools.js";
8
+ import { resolveRuntimeConfig } from "../src/config.js";
9
+ import { providerKeyStatus, setProviderKey } from "../src/project.js";
10
+ import { SessionStore } from "../src/session-store.js";
11
+
12
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auxiliary-"));
14
+ const runtimeDir = path.join(tempRoot, "runtime");
15
+ const workspace = path.join(tempRoot, "workspace");
16
+ await fs.mkdir(workspace, { recursive: true });
17
+
18
+ function assert(condition, message) {
19
+ if (!condition) throw new Error(message);
20
+ }
21
+
22
+ try {
23
+ await setProviderKey(workspace, "grsai", "test-grsai-key");
24
+ const keyStatus = providerKeyStatus(workspace);
25
+ assert(keyStatus.grsai, "GRSAI key status was not detected");
26
+ assert(keyStatus.envVars.grsai.includes("GRSAI"), "GRSAI env var name was not reported");
27
+ assert(listAuxiliarySkills().some((skill) => skill.id === "image_generation"), "image_generation skill missing");
28
+
29
+ const dryRun = await generateImage(
30
+ {
31
+ prompt: "A small cyan robot holding a paintbrush, clean bright product illustration.",
32
+ outputDir: "artifacts/images/dry-run",
33
+ outputStem: "robot",
34
+ dryRun: true,
35
+ },
36
+ {
37
+ commandCwd: workspace,
38
+ allowFileTools: true,
39
+ }
40
+ );
41
+ assert(dryRun.ok && dryRun.dryRun, "generate_image dry run failed");
42
+ await fs.access(path.join(workspace, "artifacts/images/dry-run/task_manifest.json"));
43
+ const payloadText = await fs.readFile(path.join(workspace, "artifacts/images/dry-run/request_payload.redacted.json"), "utf8");
44
+ assert(payloadText.includes("nano-banana-2"), "redacted image payload was not written");
45
+
46
+ const blocked = await generateImage(
47
+ {
48
+ prompt: "blocked",
49
+ outputDir: ".env/images",
50
+ dryRun: true,
51
+ },
52
+ {
53
+ commandCwd: workspace,
54
+ allowFileTools: true,
55
+ }
56
+ );
57
+ assert(blocked.blocked, "generate_image did not block sensitive output path");
58
+
59
+ const config = resolveRuntimeConfig(
60
+ {
61
+ provider: "mock",
62
+ routingMode: "manual",
63
+ model: "mock-agent",
64
+ goal: "Generate an image of a panda astronaut.",
65
+ commandCwd: workspace,
66
+ allowFileTools: true,
67
+ allowAuxiliaryTools: true,
68
+ maxSteps: 4,
69
+ },
70
+ {
71
+ baseDir: runtimeDir,
72
+ packageDir: repoRoot,
73
+ provider: "mock",
74
+ }
75
+ );
76
+ const run = await runAgent(config);
77
+ const store = new SessionStore(config.sessionsDir, run.sessionId);
78
+ const events = await store.loadEvents();
79
+ assert(events.some((event) => event.type === "tool.completed" && event.data?.toolName === "generate_image"), "mock run did not call generate_image");
80
+ await fs.access(path.join(workspace, "artifacts/images/mock-image/task_manifest.json"));
81
+
82
+ console.log(
83
+ JSON.stringify(
84
+ {
85
+ ok: true,
86
+ workspace,
87
+ checks: ["grsai_key_status", "image_skill_listed", "generate_image_dry_run", "generate_image_guardrail", "mock_agent_image_tool"],
88
+ },
89
+ null,
90
+ 2
91
+ )
92
+ );
93
+ } finally {
94
+ await fs.rm(tempRoot, { recursive: true, force: true });
95
+ }
@@ -5,7 +5,9 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { repairModelMessageHistory, runAgent } from "../src/agent-runner.js";
7
7
  import { resolveRuntimeConfig } from "../src/config.js";
8
+ import { selectModelRoute } from "../src/model-routing.js";
8
9
  import { SessionStore } from "../src/session-store.js";
10
+ import { executeWorkspaceTool } from "../src/workspace-tools.js";
9
11
 
10
12
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
13
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-coding-tools-"));
@@ -78,6 +80,12 @@ try {
78
80
  !staleDeepSeekState.messages.some((message) => message.role === "tool" && message.tool_call_id === "stale-call"),
79
81
  "repaired DeepSeek history retained an orphan stale tool message"
80
82
  );
83
+ const patchRoute = selectModelRoute({
84
+ routingMode: "smart",
85
+ provider: "deepseek",
86
+ goal: "patch this large codebase and migrate the database tests",
87
+ });
88
+ assert(/pro/i.test(patchRoute.model), "patch/refactor task did not route to DeepSeek pro");
81
89
 
82
90
  const writeRun = await runMock("Create notes/hello.md with a short coding smoke message.", "coding-write");
83
91
  const written = await fs.readFile(path.join(workspace, "notes/hello.md"), "utf8");
@@ -106,6 +114,78 @@ try {
106
114
  assert(patched === "new\n", "mock patch did not update expected file");
107
115
  assert(patchRun.events.some((event) => event.type === "file.changed"), "patch run did not persist file.changed event");
108
116
 
117
+ await fs.writeFile(path.join(workspace, "patch-target.txt"), "old\n", "utf8");
118
+ const multiPatchRun = await runMock("Apply multi-file Codex patch to replace old and add a note.", "coding-patch-multi");
119
+ const multiPatched = await fs.readFile(path.join(workspace, "patch-target.txt"), "utf8");
120
+ const patchNote = await fs.readFile(path.join(workspace, "notes/patch-note.md"), "utf8");
121
+ assert(multiPatched === "new\n", "mock multi-file patch did not update expected file");
122
+ assert(patchNote.includes("multi-file patch smoke"), "mock multi-file patch did not add expected file");
123
+ assert(
124
+ multiPatchRun.events.filter((event) => event.type === "file.changed").length >= 2,
125
+ "multi-file patch did not persist per-file change events"
126
+ );
127
+
128
+ await fs.writeFile(path.join(workspace, "unified-target.txt"), "alpha\nold\nomega\n", "utf8");
129
+ const unified = await executeWorkspaceTool(
130
+ "apply_patch",
131
+ {
132
+ patch: [
133
+ "--- a/unified-target.txt",
134
+ "+++ b/unified-target.txt",
135
+ "@@ -1,3 +1,3 @@",
136
+ " alpha",
137
+ "-old",
138
+ "+new",
139
+ " omega",
140
+ ].join("\n"),
141
+ },
142
+ {
143
+ commandCwd: workspace,
144
+ allowFileTools: true,
145
+ }
146
+ );
147
+ const unifiedText = await fs.readFile(path.join(workspace, "unified-target.txt"), "utf8");
148
+ assert(unified.ok && unifiedText === "alpha\nnew\nomega\n", "unified apply_patch did not update expected file");
149
+
150
+ const blockedPatch = await executeWorkspaceTool(
151
+ "apply_patch",
152
+ {
153
+ patch: ["*** Begin Patch", "*** Add File: .env", "+TOKEN=blocked", "*** End Patch"].join("\n"),
154
+ },
155
+ {
156
+ commandCwd: workspace,
157
+ allowFileTools: true,
158
+ }
159
+ );
160
+ assert(blockedPatch.blocked, "patch document to sensitive path was not blocked by guardrail");
161
+
162
+ await fs.writeFile(path.join(workspace, "move-source.txt"), "source\n", "utf8");
163
+ await fs.writeFile(path.join(workspace, "move-target.txt"), "target\n", "utf8");
164
+ const moveOverResult = await executeWorkspaceTool(
165
+ "apply_patch",
166
+ {
167
+ patch: [
168
+ "*** Begin Patch",
169
+ "*** Update File: move-source.txt",
170
+ "*** Move to: move-target.txt",
171
+ "@@",
172
+ "-source",
173
+ "+moved",
174
+ "*** End Patch",
175
+ ].join("\n"),
176
+ },
177
+ {
178
+ commandCwd: workspace,
179
+ allowFileTools: true,
180
+ }
181
+ )
182
+ .then(() => "")
183
+ .catch((error) => String(error?.message || error));
184
+ assert(
185
+ /move over an existing file/.test(moveOverResult),
186
+ "patch move over an existing file was not rejected"
187
+ );
188
+
109
189
  const envRun = await runMock("Create file: .env with blocked content.", "coding-block-env");
110
190
  await fs
111
191
  .access(path.join(workspace, ".env"))
@@ -135,11 +215,16 @@ try {
135
215
  workspace,
136
216
  checks: [
137
217
  "deepseek_history_repair",
218
+ "deepseek_pro_patch_route",
138
219
  "write_file",
139
220
  "duplicate_write_failed",
140
221
  "resume_session_write",
141
222
  "virtual_workspace_path",
142
223
  "apply_patch",
224
+ "multi_file_patch",
225
+ "unified_patch",
226
+ "patch_guardrail",
227
+ "patch_move_no_overwrite",
143
228
  "block_env",
144
229
  "block_outside",
145
230
  ],
@@ -17,6 +17,7 @@ import { redactSensitiveText, redactValue } from "./redaction.js";
17
17
  import { executeWorkspaceTool, resolveWorkspacePath, summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
18
18
  import { normalizeCanvasPayload } from "./artifact-tunnel.js";
19
19
  import { getTaskProfile } from "./task-profiles.js";
20
+ import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
20
21
 
21
22
  const exec = promisify(execCallback);
22
23
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -271,15 +272,20 @@ function createInitialState(config, sessionId) {
271
272
  : "A host shell command tool is available under the configured trust policy."
272
273
  : "No shell command tool is available.",
273
274
  config.allowFileTools
274
- ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. 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.`
275
+ ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. 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.`
275
276
  : "No workspace file tools are available.",
276
277
  config.allowWrapperTools
277
278
  ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
278
279
  : "External coding-agent wrappers are disabled.",
280
+ config.allowAuxiliaryTools
281
+ ? `Auxiliary skills are available: ${listAuxiliarySkills()
282
+ .map((skill) => `${skill.id} via ${skill.toolName} (${skill.available ? "key available" : `needs ${skill.keyName}`})`)
283
+ .join(", ")}. Use generate_image for real raster image/photo/illustration/cover/poster/logo requests when appropriate; if the key is missing, ask the user to run /auxilliary grsai or aginti login grsai.`
284
+ : "Auxiliary skills are disabled for this run.",
279
285
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
280
286
  "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.",
281
287
  "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.",
282
- "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.",
288
+ "Work like a practical coding agent: inspect when useful, patch code with apply_patch, run safe checks when they add confidence, iterate on failures, and keep outputs inside the workspace.",
283
289
  "For large projects, decompose into useful files and milestones, implement a coherent minimal version first, then iterate with checks rather than only describing what you would do.",
284
290
  "For website/app/code/LaTeX/Python/C/shell tasks, create or edit real workspace files, run available build/compile/test commands, and surface artifacts through the canvas when useful.",
285
291
  "For research or web-search tasks, use browser tools or safe shell network tools when the current policy allows; cite or save useful sources in workspace notes when the task needs traceability.",
@@ -304,11 +310,16 @@ function createInitialState(config, sessionId) {
304
310
  : `Shell working directory: ${config.commandCwd}`
305
311
  : "",
306
312
  config.allowFileTools
307
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. Local preview tools available: open_workspace_file and preview_workspace.`
313
+ ? `Workspace file tools enabled in: ${config.commandCwd}. 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.`
308
314
  : "",
309
315
  config.allowWrapperTools
310
316
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
311
317
  : "",
318
+ config.allowAuxiliaryTools
319
+ ? `Auxiliary skills: ${listAuxiliarySkills()
320
+ .map((skill) => `${skill.id}:${skill.available ? "available" : "missing-key"}`)
321
+ .join(" ")}`
322
+ : "",
312
323
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
313
324
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
314
325
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
@@ -499,10 +510,20 @@ function sanitizeToolArgs(toolName, args) {
499
510
  if (toolName === "apply_patch") {
500
511
  return {
501
512
  ...safeArgs,
513
+ patch: typeof args.patch === "string" ? `[${Buffer.byteLength(args.patch, "utf8")} bytes sha256=${hashForLog(args.patch)}]` : safeArgs.patch,
502
514
  search: typeof args.search === "string" ? redactSensitiveText(args.search).slice(0, 160) : safeArgs.search,
503
515
  replace: typeof args.replace === "string" ? redactSensitiveText(args.replace).slice(0, 160) : safeArgs.replace,
504
516
  };
505
517
  }
518
+ if (toolName === "generate_image") {
519
+ return {
520
+ ...safeArgs,
521
+ prompt: typeof args.prompt === "string" ? `[${Buffer.byteLength(args.prompt, "utf8")} bytes sha256=${hashForLog(args.prompt)}]` : safeArgs.prompt,
522
+ referenceImages: Array.isArray(args.referenceImages)
523
+ ? args.referenceImages.map((item) => (String(item || "").startsWith("data:") ? `[data-uri ${String(item).length} chars]` : redactSensitiveText(item)))
524
+ : safeArgs.referenceImages,
525
+ };
526
+ }
506
527
  return safeArgs;
507
528
  }
508
529
 
@@ -610,7 +631,7 @@ async function captureSyntheticSnapshot(store, step, config) {
610
631
  : `Shell tool available in: ${config.commandCwd}`
611
632
  : "Shell tool disabled.",
612
633
  config.allowFileTools
613
- ? `Workspace file tools available in: ${config.commandCwd}. Use workspace-relative paths.`
634
+ ? `Workspace file tools available in: ${config.commandCwd}. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches.`
614
635
  : "Workspace file tools disabled.",
615
636
  config.allowWrapperTools
616
637
  ? `Agent wrappers available: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -819,9 +840,10 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
819
840
 
820
841
  await store.appendEvent("tool.completed", eventResult);
821
842
  observers.event("tool.completed", eventResult);
822
- if (result.change) {
843
+ const changes = Array.isArray(result.changes) && result.changes.length ? result.changes : result.change ? [result.change] : [];
844
+ for (const item of changes) {
823
845
  const change = {
824
- ...result.change,
846
+ ...item,
825
847
  toolName: toolCall.function.name,
826
848
  commandCwd: config.commandCwd,
827
849
  };
@@ -872,6 +894,67 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
872
894
  observers.event("tool.completed", result);
873
895
  return result;
874
896
  }
897
+ case "generate_image": {
898
+ const imageResult = await generateImage(args, config);
899
+ const result = {
900
+ ok: Boolean(imageResult.ok),
901
+ toolName: "generate_image",
902
+ args: safeArgs,
903
+ ...imageResult,
904
+ };
905
+ const eventResult = sanitizeToolResult(result);
906
+ await store.appendEvent("tool.completed", eventResult);
907
+ observers.event("tool.completed", eventResult);
908
+
909
+ if (result.ok) {
910
+ const generated = {
911
+ path: result.path,
912
+ imagePaths: result.imagePaths || [],
913
+ manifestPath: result.manifestPath || "",
914
+ promptPath: result.promptPath || "",
915
+ requestPayloadPath: result.requestPayloadPath || "",
916
+ commandCwd: config.commandCwd,
917
+ };
918
+ await store.appendEvent("image.generated", generated);
919
+ observers.event("image.generated", generated);
920
+
921
+ const selectedPath = result.imagePaths?.[0] || result.manifestPath || "";
922
+ if (selectedPath) {
923
+ const normalized = normalizeCanvasPayload(
924
+ {
925
+ title: result.imagePaths?.length ? "Generated image" : "Image generation payload",
926
+ kind: result.imagePaths?.length ? "image" : "json",
927
+ path: selectedPath,
928
+ note: result.summary || "Generated image artifact.",
929
+ selected: Boolean(result.imagePaths?.length),
930
+ },
931
+ config
932
+ );
933
+ if (normalized.ok) {
934
+ const canvasItem = {
935
+ ...normalized.payload,
936
+ toolName: "generate_image",
937
+ commandCwd: config.commandCwd,
938
+ };
939
+ await store.appendEvent("canvas.item", canvasItem);
940
+ observers.event("canvas.item", canvasItem);
941
+ if (canvasItem.selected) {
942
+ await store.appendEvent("canvas.selected", {
943
+ artifactId: canvasItem.artifactId,
944
+ title: canvasItem.title,
945
+ source: "generate_image",
946
+ });
947
+ observers.event("canvas.selected", {
948
+ artifactId: canvasItem.artifactId,
949
+ title: canvasItem.title,
950
+ source: "generate_image",
951
+ });
952
+ }
953
+ }
954
+ }
955
+ }
956
+ return result;
957
+ }
875
958
  case "send_to_canvas": {
876
959
  const normalized = normalizeCanvasPayload(args, config);
877
960
  if (!normalized.ok) {