@lazyingart/agintiflow 0.20.174 → 0.20.176
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 +2 -2
- package/docs/perception-and-web-research.md +5 -2
- package/package.json +1 -1
- package/public/app.js +96 -10
- package/public/styles.css +4 -0
- package/scripts/smoke-cli-chat.js +6 -2
- package/scripts/smoke-perception-research.js +22 -0
- package/scripts/smoke-web-ui.js +9 -0
- package/src/agent-runner.js +34 -0
- package/src/config.js +7 -5
- package/src/interactive-cli.js +130 -15
- package/src/model-client.js +8 -3
- package/src/perception-tools.js +208 -2
- package/src/scs-controller.js +116 -10
- package/src/scs-evidence.js +163 -2
- package/src/tool-wrappers.js +101 -1
- package/web.js +160 -4
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Most agent tools are either a chat box with hidden state or an expensive one-mod
|
|
|
54
54
|
| Disciplined by default | `AGINTI.md` starts with a behavior contract: surface ambiguity, keep edits surgical, avoid speculative complexity, verify outcomes, and respect permission blockers. |
|
|
55
55
|
| Role-based models | Route, main, spare, wrapper, and auxiliary image roles are separate. You can use cheap route models, stronger main models, optional OpenAI/Qwen/Venice routes, and GRS AI/Venice image tools. |
|
|
56
56
|
| Writing without agent noise | `writing_specialist` drafts novels, books, scripts, essays, and paper prose in an isolated writing-only context, then the main agent handles files, formatting, citations, checks, and artifacts. |
|
|
57
|
-
| Visual and web evidence | `read_image` reads screenshots/figures with typed perception artifacts, `web_research` saves sourced research artifacts, and `research_wrapper` can ask Codex `gpt-5.4-mini` medium for a strict-JSON second opinion. |
|
|
57
|
+
| Visual and web evidence | `read_image` reads screenshots/figures with typed JSON and Markdown perception artifacts, uses OpenAI vision when configured, and falls back to `codex exec --image` when Codex is available. `web_research` saves sourced research artifacts, and `research_wrapper` can ask Codex `gpt-5.4-mini` medium for a strict-JSON second opinion. |
|
|
58
58
|
| Scouts before big work | Parallel scouts can cheaply map architecture, tests, risks, symbols, and integration points before the main executor edits anything. |
|
|
59
59
|
| SCS by default | Student-Committee-Supervisor mode adds a typed gate: committee drafts, student approves/monitors, supervisor executes. Use `/scs off` or `--no-scs` only when speed matters more than validation. |
|
|
60
60
|
| AAPS for large workflows | AAPS describes top-down agentic pipeline scripts; AgInTiFlow can act as the interactive backend that validates, compiles, and executes those workflows. |
|
|
@@ -239,7 +239,7 @@ The website keeps the visual walkthrough in a carousel so this README can stay f
|
|
|
239
239
|
| Writing specialist | A dedicated writing-only LLM call for prose, chapters, scripts, books, essays, research-paper sections, and revisions, with formatter handoff notes for Markdown/LaTeX/Final Draft. |
|
|
240
240
|
| Patch workflow | Codex-style patch envelopes, unified diffs, exact replacements, hashes, compact diffs, and path guardrails. |
|
|
241
241
|
| Parallel scouts | Optional scout calls for architecture, implementation, review, tests, git flow, research, symbol tracing, and dependency risk. |
|
|
242
|
-
| Image reading and web research | `read_image` uses OpenAI vision for workspace images when `OPENAI_API_KEY` is configured. `web_research` preserves source lists, and optional OpenAI hosted web search or `research_wrapper` can be used for higher-confidence research. |
|
|
242
|
+
| Image reading and web research | `read_image` uses OpenAI vision for workspace images when `OPENAI_API_KEY` is configured, otherwise it can fall back to Codex CLI image attachments when available. It saves JSON and Markdown reports and can surface the report in the canvas. `web_research` preserves source lists, and optional OpenAI hosted web search or `research_wrapper` can be used for higher-confidence research. |
|
|
243
243
|
| SCS mode | Default Student-Committee-Supervisor quality gate with independent planning, execution, and validation roles. |
|
|
244
244
|
| AAPS adapter | Optional `@lazyingart/aaps` integration for `.aaps` workflow init, validate, parse, compile, dry-run, and run commands. |
|
|
245
245
|
| Image generation | Optional GRS AI and Venice image tools with saved manifests and canvas artifact previews. |
|
|
@@ -6,7 +6,7 @@ AgInTiFlow separates visual understanding, web search, and wrapper advice so eac
|
|
|
6
6
|
|
|
7
7
|
| Tool | Purpose | Evidence |
|
|
8
8
|
| --- | --- | --- |
|
|
9
|
-
| `read_image` | Read workspace screenshots, plots, scanned text, diagrams, or allowed remote image URLs. | Saves `artifacts/perception
|
|
9
|
+
| `read_image` | Read workspace screenshots, plots, scanned text, diagrams, or allowed remote image URLs. | Saves JSON and Markdown reports under `artifacts/perception/`, records image hashes, and sends the Markdown report to the canvas when used by an agent run. |
|
|
10
10
|
| `web_search` | Cheap raw search snippets. | Returns compact titles, URLs, snippets, and fallback search URL. |
|
|
11
11
|
| `web_research` | Sourced research unit for current or external information. | Saves `artifacts/research/*-web-research.json` with query, mode, source list, and answer. |
|
|
12
12
|
| `research_wrapper` | Read-only strict-JSON second opinion from the selected wrapper. | Saves `artifacts/wrappers/*-research-wrapper.json` with wrapper, model, reasoning, metadata, result, and raw fallback output. |
|
|
@@ -14,7 +14,9 @@ AgInTiFlow separates visual understanding, web search, and wrapper advice so eac
|
|
|
14
14
|
## Defaults
|
|
15
15
|
|
|
16
16
|
- `read_image` uses OpenAI Responses vision when `OPENAI_API_KEY` is configured.
|
|
17
|
-
-
|
|
17
|
+
- If OpenAI vision is unavailable, `read_image` falls back to `codex exec --image` when the Codex CLI is installed and wrapper tools are enabled.
|
|
18
|
+
- Wrapper tools default on when Codex is available, and can still be disabled with `/wrapper off` or `--no-wrapper`-style runtime settings.
|
|
19
|
+
- `read_image` defaults to `AGINTI_PERCEPTION_MODEL=gpt-5.4-mini` and `AGINTI_PERCEPTION_REASONING=medium`, then falls back through `AGINTI_PERCEPTION_FALLBACK_MODELS` or `gpt-4o-mini` if the account lacks access to the preferred OpenAI model.
|
|
18
20
|
- `web_research` defaults to lightweight snippet mode; use `mode=openai` only when hosted OpenAI web search is needed and configured.
|
|
19
21
|
- `web_research mode=openai` defaults to `AGINTI_WEB_RESEARCH_MODEL=gpt-5.4-mini` / `medium`, then falls back through `AGINTI_WEB_RESEARCH_FALLBACK_MODELS` or `gpt-4o-mini`.
|
|
20
22
|
- `research_wrapper` defaults to `AGINTI_RESEARCH_WRAPPER_MODEL=gpt-5.4-mini` and `AGINTI_RESEARCH_WRAPPER_REASONING=medium`.
|
|
@@ -24,6 +26,7 @@ AgInTiFlow separates visual understanding, web search, and wrapper advice so eac
|
|
|
24
26
|
|
|
25
27
|
```text
|
|
26
28
|
/image-read artifacts/screenshots/app.png what looks wrong?
|
|
29
|
+
/image-read --codex artifacts/screenshots/app.png what looks wrong?
|
|
27
30
|
/web-research latest Android Gradle plugin official docs
|
|
28
31
|
/research-wrapper gpt-5.4-mini medium
|
|
29
32
|
/research-wrapper off
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.176",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/public/app.js
CHANGED
|
@@ -1873,6 +1873,28 @@ function renderEmbeddedWorkspaceResult(value = "", entry = {}) {
|
|
|
1873
1873
|
`;
|
|
1874
1874
|
}
|
|
1875
1875
|
|
|
1876
|
+
function compactInline(value = "", limit = 180) {
|
|
1877
|
+
return compactMessage(value, limit);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function toolArgumentPreview(toolName = "", args = {}, data = {}) {
|
|
1881
|
+
if (toolName === "finish") return "";
|
|
1882
|
+
if (toolName === "run_command" && (args.command || data.command)) return args.command || data.command;
|
|
1883
|
+
if (["write_file", "apply_patch", "read_file", "open_workspace_file", "preview_workspace"].includes(toolName)) {
|
|
1884
|
+
return args.path || args.file || data.path || "";
|
|
1885
|
+
}
|
|
1886
|
+
if (["send_to_canvas", "create_artifact"].includes(toolName)) {
|
|
1887
|
+
return [args.title || data.title || "canvas artifact", args.kind || data.kind || "", args.selected ? "selected" : ""]
|
|
1888
|
+
.filter(Boolean)
|
|
1889
|
+
.join(" · ");
|
|
1890
|
+
}
|
|
1891
|
+
if (["open_url", "web_research", "web_search"].includes(toolName)) return args.url || args.query || args.q || data.url || "";
|
|
1892
|
+
const scalar = ["title", "path", "file", "query", "q", "url", "kind"]
|
|
1893
|
+
.map((key) => args[key] || data[key])
|
|
1894
|
+
.find(Boolean);
|
|
1895
|
+
return scalar ? compactInline(scalar) : "";
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1876
1898
|
function renderWorkspaceChangeEvent(entry) {
|
|
1877
1899
|
const data = entry.data || {};
|
|
1878
1900
|
const toolName = data.toolName || data.action || "file.changed";
|
|
@@ -1912,12 +1934,7 @@ function renderToolEvent(entry) {
|
|
|
1912
1934
|
const args = data.args || {};
|
|
1913
1935
|
const resultText = String(data.result || args.result || "");
|
|
1914
1936
|
const embeddedResult = resultText ? renderEmbeddedWorkspaceResult(resultText, entry) : "";
|
|
1915
|
-
const argPreview =
|
|
1916
|
-
toolName === "finish"
|
|
1917
|
-
? ""
|
|
1918
|
-
: toolName === "run_command" && args.command
|
|
1919
|
-
? args.command
|
|
1920
|
-
: args.path || args.url || args.query || args.q || (Object.keys(args).length ? JSON.stringify(args) : "");
|
|
1937
|
+
const argPreview = toolArgumentPreview(toolName, args, data);
|
|
1921
1938
|
const stdout = data.stdout ? outputPreviewText(data.stdout) : null;
|
|
1922
1939
|
const stderr = data.stderr ? outputPreviewText(data.stderr) : null;
|
|
1923
1940
|
return `
|
|
@@ -1990,6 +2007,66 @@ function renderStructuredEvent(entry) {
|
|
|
1990
2007
|
return "";
|
|
1991
2008
|
}
|
|
1992
2009
|
|
|
2010
|
+
function logToolLifecycleKey(data = {}) {
|
|
2011
|
+
const args = data.args || {};
|
|
2012
|
+
const focusedArgs = {
|
|
2013
|
+
path: args.path || args.file || data.path || "",
|
|
2014
|
+
command: args.command || data.command || "",
|
|
2015
|
+
title: args.title || data.title || "",
|
|
2016
|
+
kind: args.kind || data.kind || "",
|
|
2017
|
+
query: args.query || args.q || "",
|
|
2018
|
+
url: args.url || data.url || "",
|
|
2019
|
+
};
|
|
2020
|
+
return `${data.toolName || ""}:${JSON.stringify(focusedArgs)}`;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function logEventDedupContext(entries = [], runResult = "") {
|
|
2024
|
+
const terminalToolKeys = new Set();
|
|
2025
|
+
const finishResultFingerprints = new Set();
|
|
2026
|
+
const workspaceChangePaths = new Set();
|
|
2027
|
+
const runResultFingerprint = compactInline(runResult, 100000);
|
|
2028
|
+
|
|
2029
|
+
for (const entry of entries || []) {
|
|
2030
|
+
const data = entry.data || {};
|
|
2031
|
+
const type = entry.message || entry.eventType || "";
|
|
2032
|
+
if (["tool.completed", "tool.failed", "tool.skipped", "tool.blocked"].includes(type)) {
|
|
2033
|
+
terminalToolKeys.add(logToolLifecycleKey(data));
|
|
2034
|
+
}
|
|
2035
|
+
if (type === "tool.completed" && data.toolName === "finish" && data.result) {
|
|
2036
|
+
finishResultFingerprints.add(compactInline(data.result, 100000));
|
|
2037
|
+
}
|
|
2038
|
+
if (type === "session.finished" && data.result) {
|
|
2039
|
+
finishResultFingerprints.add(compactInline(data.result, 100000));
|
|
2040
|
+
}
|
|
2041
|
+
if (type === "file.changed" && (data.path || data.args?.path)) {
|
|
2042
|
+
workspaceChangePaths.add(String(data.path || data.args?.path || "").replace(/\\/g, "/"));
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
return { terminalToolKeys, finishResultFingerprints, workspaceChangePaths, runResultFingerprint };
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function shouldSuppressLogEntry(entry = {}, context = {}) {
|
|
2050
|
+
const data = entry.data || {};
|
|
2051
|
+
const type = entry.message || entry.eventType || "";
|
|
2052
|
+
const toolName = data.toolName || "";
|
|
2053
|
+
const toolKey = logToolLifecycleKey(data);
|
|
2054
|
+
const resultKey = data.result || data.args?.result ? compactInline(data.result || data.args?.result, 100000) : "";
|
|
2055
|
+
if (type === "tool.started" && context.terminalToolKeys?.has(toolKey)) return true;
|
|
2056
|
+
if (type === "tool.started" && toolName === "finish" && resultKey) {
|
|
2057
|
+
return resultKey === context.runResultFingerprint || context.finishResultFingerprints?.has(resultKey);
|
|
2058
|
+
}
|
|
2059
|
+
if (type === "tool.completed" && toolName === "finish" && resultKey && resultKey === context.runResultFingerprint) return true;
|
|
2060
|
+
if (type === "session.finished" && resultKey) {
|
|
2061
|
+
return resultKey === context.runResultFingerprint || context.finishResultFingerprints?.has(resultKey);
|
|
2062
|
+
}
|
|
2063
|
+
if (type === "tool.completed" && ["write_file", "apply_patch"].includes(toolName)) {
|
|
2064
|
+
const pathKey = String(data.path || data.args?.path || data.args?.file || "").replace(/\\/g, "/");
|
|
2065
|
+
if (pathKey && context.workspaceChangePaths?.has(pathKey)) return true;
|
|
2066
|
+
}
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1993
2070
|
function renderLogs(run) {
|
|
1994
2071
|
logsEl.dataset.mode = "active";
|
|
1995
2072
|
const structuredRunResult = run.result ? renderEmbeddedWorkspaceResult(run.result, { at: run.endedAt || run.updatedAt || "" }) : "";
|
|
@@ -2003,7 +2080,9 @@ function renderLogs(run) {
|
|
|
2003
2080
|
run.error ? `<div class="log-line error">${escapeHtml(`error=${run.error}`)}</div>` : "",
|
|
2004
2081
|
];
|
|
2005
2082
|
|
|
2083
|
+
const dedupContext = logEventDedupContext(run.logs || [], run.result || "");
|
|
2006
2084
|
for (const entry of run.logs || []) {
|
|
2085
|
+
if (shouldSuppressLogEntry(entry, dedupContext)) continue;
|
|
2007
2086
|
const structured = renderStructuredEvent(entry);
|
|
2008
2087
|
if (structured) {
|
|
2009
2088
|
parts.push(structured);
|
|
@@ -2286,10 +2365,17 @@ function renderChat(chatEntries) {
|
|
|
2286
2365
|
}
|
|
2287
2366
|
const role = entry.role === "assistant" ? "assistant" : "user";
|
|
2288
2367
|
const label = role === "assistant" ? t("assistantLabel") : t("youLabel");
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2368
|
+
let content = escapeHtml(entry.content).replace(/\n/g, "<br>");
|
|
2369
|
+
if (role === "assistant") {
|
|
2370
|
+
const embeddedResult = renderEmbeddedWorkspaceResult(entry.content, entry);
|
|
2371
|
+
if (embeddedResult && entry.suppressEmbeddedWorkspaceDiffs) {
|
|
2372
|
+
content = `<div class="markdown-body">${renderMarkdown(embeddedWorkspaceSummary(entry.content))}</div>`;
|
|
2373
|
+
} else if (embeddedResult) {
|
|
2374
|
+
content = `<article class="event-card event-finish assistant-result-card">${embeddedResult}</article>`;
|
|
2375
|
+
} else {
|
|
2376
|
+
content = `<div class="markdown-body">${renderMarkdown(entry.content)}</div>`;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2293
2379
|
return `
|
|
2294
2380
|
<article class="chat-item ${role}">
|
|
2295
2381
|
<div class="chat-meta">${label}${entry.at ? ` · ${new Date(entry.at).toLocaleString()}` : ""}</div>
|
package/public/styles.css
CHANGED
|
@@ -896,8 +896,12 @@ try {
|
|
|
896
896
|
if (!latest.stdout.includes(" write ") || !latest.stdout.includes("+Created by AgInTiFlow mock mode.")) {
|
|
897
897
|
throw new Error("bare aginti resume did not replay formatted file-change diff context");
|
|
898
898
|
}
|
|
899
|
-
if (
|
|
900
|
-
throw new Error("bare aginti resume
|
|
899
|
+
if (latest.stdout.includes('{"result":"Mock run complete')) {
|
|
900
|
+
throw new Error("bare aginti resume rendered raw finish JSON instead of formatted history");
|
|
901
|
+
}
|
|
902
|
+
const mockCompleteCount = (latest.stdout.match(/Mock run complete\./g) || []).length;
|
|
903
|
+
if (mockCompleteCount > 1) {
|
|
904
|
+
throw new Error(`bare aginti resume duplicated the finish result ${mockCompleteCount} times`);
|
|
901
905
|
}
|
|
902
906
|
if (latest.stdout.includes("resume note=showing chat transcript only")) {
|
|
903
907
|
throw new Error("bare aginti resume regressed to chat-only history");
|
|
@@ -58,7 +58,28 @@ async function main() {
|
|
|
58
58
|
assert(image.ok, `read_image dry-run failed: ${image.error || "unknown"}`);
|
|
59
59
|
assert(image.images?.[0]?.sha256, "read_image did not record image hash");
|
|
60
60
|
assert(image.artifactPath, "read_image did not persist a perception artifact");
|
|
61
|
+
assert(image.markdownArtifactPath, "read_image did not persist a Markdown perception artifact");
|
|
62
|
+
assert(image.markdownPath, "read_image did not persist a workspace Markdown report");
|
|
61
63
|
await fs.access(image.artifactPath);
|
|
64
|
+
await fs.access(image.markdownArtifactPath);
|
|
65
|
+
await fs.access(path.join(workspace, image.markdownPath));
|
|
66
|
+
|
|
67
|
+
const codexFallback = await readImage(
|
|
68
|
+
{
|
|
69
|
+
path: "artifacts/screenshots/tiny.png",
|
|
70
|
+
prompt: "Describe this tiny image with the Codex image fallback.",
|
|
71
|
+
provider: "codex",
|
|
72
|
+
codexDryRun: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
...config,
|
|
76
|
+
allowWrapperTools: true,
|
|
77
|
+
},
|
|
78
|
+
store
|
|
79
|
+
);
|
|
80
|
+
assert(codexFallback.ok, `read_image Codex fallback dry-run failed: ${codexFallback.error || "unknown"}`);
|
|
81
|
+
assert(codexFallback.provider === "codex-wrapper-dry-run", "read_image did not select the Codex wrapper fallback path");
|
|
82
|
+
assert(codexFallback.markdownPath, "read_image Codex fallback did not write a Markdown report");
|
|
62
83
|
|
|
63
84
|
const secretBlock = checkToolUse({
|
|
64
85
|
toolName: "read_image",
|
|
@@ -116,6 +137,7 @@ async function main() {
|
|
|
116
137
|
const runStore = new SessionStore(agentConfig.sessionsDir, run.sessionId);
|
|
117
138
|
const events = await runStore.loadEvents();
|
|
118
139
|
assert(events.some((event) => event.type === "tool.completed" && event.data?.toolName === "read_image"), "mock agent did not call read_image");
|
|
140
|
+
assert(events.some((event) => event.type === "canvas.item" && event.data?.toolName === "read_image"), "read_image did not send the Markdown report to canvas");
|
|
119
141
|
|
|
120
142
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
121
143
|
console.log("smoke-perception-research ok");
|
package/scripts/smoke-web-ui.js
CHANGED
|
@@ -224,6 +224,14 @@ try {
|
|
|
224
224
|
const chatTail = (await page.locator("#chat-thread").innerText().catch(() => "")).slice(-1200);
|
|
225
225
|
throw new Error(`web UI did not render formatted finish-result diff event; state=${state}\nlogs tail:\n${logsTail}\nchat tail:\n${chatTail}`);
|
|
226
226
|
}
|
|
227
|
+
const formattedChatText = await page.locator("#chat-thread").innerText();
|
|
228
|
+
if (formattedChatText.includes('{"result":"Mock run complete')) {
|
|
229
|
+
throw new Error("web UI rendered raw finish JSON in chat history");
|
|
230
|
+
}
|
|
231
|
+
const mockCompleteCount = (formattedChatText.match(/Mock run complete\./g) || []).length;
|
|
232
|
+
if (mockCompleteCount > 1) {
|
|
233
|
+
throw new Error(`web UI duplicated the assistant/session finish result ${mockCompleteCount} times`);
|
|
234
|
+
}
|
|
227
235
|
|
|
228
236
|
await page.click("#open-settings");
|
|
229
237
|
await page.waitForSelector("#settings-modal[open]");
|
|
@@ -262,6 +270,7 @@ try {
|
|
|
262
270
|
"formatted-plan-event-card",
|
|
263
271
|
"formatted-file-diff-event-card",
|
|
264
272
|
"formatted-finish-result-diff-card",
|
|
273
|
+
"deduped-finish-history",
|
|
265
274
|
"settings-provider-dropdowns",
|
|
266
275
|
"settings-wrapper-dropdowns",
|
|
267
276
|
],
|
package/src/agent-runner.js
CHANGED
|
@@ -1775,6 +1775,40 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
1775
1775
|
const eventResult = sanitizeToolResult(result);
|
|
1776
1776
|
await store.appendEvent(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
|
1777
1777
|
observers.event(result.ok ? "tool.completed" : "tool.failed", eventResult);
|
|
1778
|
+
if (result.ok && result.markdownPath) {
|
|
1779
|
+
const normalized = normalizeCanvasPayload(
|
|
1780
|
+
{
|
|
1781
|
+
title: "Image reading report",
|
|
1782
|
+
kind: "markdown",
|
|
1783
|
+
path: result.markdownPath,
|
|
1784
|
+
note: result.result?.summary || result.result?.answer || "Image reading report.",
|
|
1785
|
+
selected: true,
|
|
1786
|
+
},
|
|
1787
|
+
config
|
|
1788
|
+
);
|
|
1789
|
+
if (normalized.ok) {
|
|
1790
|
+
const persisted = await persistCanvasPayloadFile(normalized.payload, { config, store });
|
|
1791
|
+
if (persisted.ok) {
|
|
1792
|
+
const canvasItem = {
|
|
1793
|
+
...persisted.payload,
|
|
1794
|
+
toolName: "read_image",
|
|
1795
|
+
commandCwd: config.commandCwd,
|
|
1796
|
+
};
|
|
1797
|
+
await store.appendEvent("canvas.item", canvasItem);
|
|
1798
|
+
observers.event("canvas.item", canvasItem);
|
|
1799
|
+
await store.appendEvent("canvas.selected", {
|
|
1800
|
+
artifactId: canvasItem.artifactId,
|
|
1801
|
+
title: canvasItem.title,
|
|
1802
|
+
source: "read_image",
|
|
1803
|
+
});
|
|
1804
|
+
observers.event("canvas.selected", {
|
|
1805
|
+
artifactId: canvasItem.artifactId,
|
|
1806
|
+
title: canvasItem.title,
|
|
1807
|
+
source: "read_image",
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1778
1812
|
return result;
|
|
1779
1813
|
}
|
|
1780
1814
|
case "json_specialist": {
|
package/src/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { getModelRoleDefaults, getProviderDefaults, normalizeRoutingMode, selectModelRoute } from "./model-routing.js";
|
|
4
4
|
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
|
|
5
|
-
import { normalizeWrapperName } from "./tool-wrappers.js";
|
|
5
|
+
import { isWrapperAvailable, normalizeWrapperName } from "./tool-wrappers.js";
|
|
6
6
|
import { loadProjectEnv, projectPaths, resolveProjectRoot } from "./project.js";
|
|
7
7
|
import { normalizeTaskProfile } from "./task-profiles.js";
|
|
8
8
|
import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
|
|
@@ -113,6 +113,10 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
113
113
|
overrides.allowParallelScouts ?? args.allowParallelScouts ?? process.env.AGINTI_PARALLEL_SCOUTS,
|
|
114
114
|
true
|
|
115
115
|
);
|
|
116
|
+
const preferredWrapper = normalizeWrapperName(
|
|
117
|
+
overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
|
|
118
|
+
);
|
|
119
|
+
const wrapperDefaultEnabled = isWrapperAvailable(preferredWrapper) || isWrapperAvailable("codex");
|
|
116
120
|
|
|
117
121
|
return {
|
|
118
122
|
...defaults,
|
|
@@ -192,7 +196,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
192
196
|
),
|
|
193
197
|
allowWrapperTools: parseBoolean(
|
|
194
198
|
overrides.allowWrapperTools ?? args.allowWrapperTools ?? process.env.ALLOW_WRAPPER_TOOLS,
|
|
195
|
-
|
|
199
|
+
wrapperDefaultEnabled
|
|
196
200
|
),
|
|
197
201
|
allowAuxiliaryTools: parseBoolean(
|
|
198
202
|
overrides.allowAuxiliaryTools ?? args.allowAuxiliaryTools ?? process.env.ALLOW_AUXILIARY_TOOLS,
|
|
@@ -206,9 +210,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
|
|
|
206
210
|
1,
|
|
207
211
|
10
|
|
208
212
|
),
|
|
209
|
-
preferredWrapper
|
|
210
|
-
overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
|
|
211
|
-
),
|
|
213
|
+
preferredWrapper,
|
|
212
214
|
wrapperTimeoutMs: parseNumber(overrides.wrapperTimeoutMs ?? process.env.WRAPPER_TIMEOUT_MS, 120000),
|
|
213
215
|
permissionMode,
|
|
214
216
|
sandboxMode,
|
package/src/interactive-cli.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
} from "./permission-modes.js";
|
|
48
48
|
import { ensureAgintiWebApp, readWebAppPreference, stopAgintiWebApp, writeWebAppPreference } from "./web-autostart.js";
|
|
49
49
|
import { mcpCliCommand, formatMcpCliResult } from "./mcp/tool-bridge.js";
|
|
50
|
+
import { isWrapperAvailable } from "./tool-wrappers.js";
|
|
50
51
|
|
|
51
52
|
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
52
53
|
const ansi = {
|
|
@@ -843,6 +844,47 @@ function embeddedWorkspaceSummary(value = "") {
|
|
|
843
844
|
return summary.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
844
845
|
}
|
|
845
846
|
|
|
847
|
+
function normalizedTimelineText(value = "") {
|
|
848
|
+
return String(value || "").replace(/\r\n?/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function timelineTextFingerprint(value = "") {
|
|
852
|
+
return normalizedTimelineText(value).replace(/\s+/g, " ").trim();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function workspaceChangeFingerprint(change = {}) {
|
|
856
|
+
const pathKey = String(change.path || change.args?.path || "").replace(/\\/g, "/").trim();
|
|
857
|
+
const diffKey = normalizedTimelineText(change.diff || "");
|
|
858
|
+
return pathKey || diffKey ? `${pathKey}\n${diffKey}` : "";
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function eventWorkspaceChangeFingerprint(event = {}) {
|
|
862
|
+
const data = event.data || {};
|
|
863
|
+
if (event.type === "file.changed") return workspaceChangeFingerprint(data);
|
|
864
|
+
if ((event.type === "tool.completed" || event.type === "tool.failed") && data.diff) return workspaceChangeFingerprint(data);
|
|
865
|
+
return "";
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function eventWorkspaceChangePath(event = {}) {
|
|
869
|
+
const data = event.data || {};
|
|
870
|
+
if (event.type !== "file.changed" && !data.diff) return "";
|
|
871
|
+
return String(data.path || data.args?.path || "").replace(/\\/g, "/").trim();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function toolLifecycleKey(event = {}) {
|
|
875
|
+
const data = event.data || {};
|
|
876
|
+
const args = data.args || {};
|
|
877
|
+
const focusedArgs = {
|
|
878
|
+
path: args.path || args.file || data.path || "",
|
|
879
|
+
command: args.command || data.command || "",
|
|
880
|
+
title: args.title || data.title || "",
|
|
881
|
+
kind: args.kind || data.kind || "",
|
|
882
|
+
query: args.query || args.q || "",
|
|
883
|
+
url: args.url || data.url || "",
|
|
884
|
+
};
|
|
885
|
+
return `${data.toolName || ""}:${JSON.stringify(focusedArgs)}`;
|
|
886
|
+
}
|
|
887
|
+
|
|
846
888
|
function printWorkspaceChange(change = {}) {
|
|
847
889
|
if (!change?.diff) return;
|
|
848
890
|
const formatted = formatWorkspaceChange(change);
|
|
@@ -852,12 +894,14 @@ function printWorkspaceChange(change = {}) {
|
|
|
852
894
|
for (const line of formatted.lines) outputLine(`${gutter}${line}`);
|
|
853
895
|
}
|
|
854
896
|
|
|
855
|
-
function printEmbeddedWorkspaceResult(result = "", { labelName = "finish", time = "" } = {}) {
|
|
897
|
+
function printEmbeddedWorkspaceResult(result = "", { labelName = "finish", time = "", bg = ansi.systemBg, includeDiffs = true } = {}) {
|
|
856
898
|
const changes = embeddedWorkspaceChangesFromText(result);
|
|
857
899
|
if (changes.length === 0) return false;
|
|
858
900
|
const summary = embeddedWorkspaceSummary(result);
|
|
859
|
-
if (summary) printHistoryBlock(labelName, summary, { time, bg
|
|
860
|
-
|
|
901
|
+
if (summary) printHistoryBlock(labelName, summary, { time, bg });
|
|
902
|
+
if (includeDiffs) {
|
|
903
|
+
for (const change of changes) printWorkspaceChange(change);
|
|
904
|
+
}
|
|
861
905
|
return true;
|
|
862
906
|
}
|
|
863
907
|
|
|
@@ -1069,7 +1113,7 @@ function printHelp() {
|
|
|
1069
1113
|
` ${command("/webapp [port|start|stop|restart|reuse|enable|disable|status]", "Start, reuse, stop, restart, or configure the local webapp.", "helpWebapp")}`,
|
|
1070
1114
|
` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
|
|
1071
1115
|
` ${command("/web-research <query>", "Run a sourced web_research turn with persisted evidence.", "helpWebSearch")}`,
|
|
1072
|
-
` ${command("/image-read <path> [question]", "Run read_image on a workspace screenshot/image.", "helpWebSearch")}`,
|
|
1116
|
+
` ${command("/image-read [--codex|--openai] <path> [question]", "Run read_image on a workspace screenshot/image.", "helpWebSearch")}`,
|
|
1073
1117
|
` ${command("/research-wrapper [on|off|model reasoning]", "Configure strict JSON wrapper research, default gpt-5.4-mini medium.", "helpWrapper")}`,
|
|
1074
1118
|
` ${command("/scs [on|auto|off|status]", "Toggle Student-Committee-Supervisor gated execution.", "helpEnableScs")}`,
|
|
1075
1119
|
` ${command("/scouts on|off|<1-10>", "Enable parallel DeepSeek scouts and set scout count.", "helpScouts")}`,
|
|
@@ -2296,6 +2340,17 @@ function printHistoryEntry(entry) {
|
|
|
2296
2340
|
const role = entry.role === "assistant" ? "aginti>" : entry.role === "user" ? "user>" : String(entry.role || "note");
|
|
2297
2341
|
const bg = role === "aginti>" ? ansi.agentBg : role === "user>" ? ansi.userBg : ansi.systemBg;
|
|
2298
2342
|
const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
2343
|
+
if (
|
|
2344
|
+
entry.role === "assistant" &&
|
|
2345
|
+
printEmbeddedWorkspaceResult(entry.content, {
|
|
2346
|
+
labelName: role,
|
|
2347
|
+
time,
|
|
2348
|
+
bg,
|
|
2349
|
+
includeDiffs: !entry.suppressEmbeddedWorkspaceDiffs,
|
|
2350
|
+
})
|
|
2351
|
+
) {
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2299
2354
|
printHistoryBlock(role, entry.content, { time, bg });
|
|
2300
2355
|
}
|
|
2301
2356
|
|
|
@@ -2335,16 +2390,66 @@ function isReplayableResumeEvent(event = {}) {
|
|
|
2335
2390
|
function resumeTimelineItems(chat = [], events = [], { limit = 0 } = {}) {
|
|
2336
2391
|
const shownChat = limit > 0 ? chat.slice(-limit) : chat;
|
|
2337
2392
|
const firstChatTime = limit > 0 && shownChat.length > 0 ? timestampMs(shownChat[0].at) : 0;
|
|
2338
|
-
const
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2393
|
+
const eventList = Array.isArray(events) ? events : [];
|
|
2394
|
+
const workspaceChangeKeys = new Set(eventList.map(eventWorkspaceChangeFingerprint).filter(Boolean));
|
|
2395
|
+
const workspaceChangePaths = new Set(eventList.map(eventWorkspaceChangePath).filter(Boolean));
|
|
2396
|
+
const chatResultFingerprints = new Set(
|
|
2397
|
+
(Array.isArray(chat) ? chat : [])
|
|
2398
|
+
.filter((entry) => entry?.role === "assistant" && entry?.content)
|
|
2399
|
+
.map((entry) => timelineTextFingerprint(entry.content))
|
|
2400
|
+
.filter(Boolean)
|
|
2401
|
+
);
|
|
2402
|
+
const finishResultFingerprints = new Set(
|
|
2403
|
+
eventList
|
|
2404
|
+
.filter((event) => (event.type === "tool.completed" && event.data?.toolName === "finish") || event.type === "session.finished")
|
|
2405
|
+
.map((event) => timelineTextFingerprint(event.data?.result || ""))
|
|
2406
|
+
.filter(Boolean)
|
|
2407
|
+
);
|
|
2408
|
+
const terminalToolKeys = new Set(
|
|
2409
|
+
eventList
|
|
2410
|
+
.filter((event) => ["tool.completed", "tool.failed", "tool.skipped", "tool.blocked"].includes(event.type))
|
|
2411
|
+
.map(toolLifecycleKey)
|
|
2412
|
+
.filter(Boolean)
|
|
2413
|
+
);
|
|
2414
|
+
const shouldSuppressEvent = (event = {}) => {
|
|
2415
|
+
const type = String(event.type || "");
|
|
2416
|
+
const data = event.data || {};
|
|
2417
|
+
const toolName = String(data.toolName || "");
|
|
2418
|
+
const resultFingerprint = timelineTextFingerprint(data.result || data.args?.result || "");
|
|
2419
|
+
if (type === "tool.started" && terminalToolKeys.has(toolLifecycleKey(event))) return true;
|
|
2420
|
+
if (type === "tool.started" && toolName === "finish" && resultFingerprint) {
|
|
2421
|
+
return chatResultFingerprints.has(resultFingerprint) || finishResultFingerprints.has(resultFingerprint);
|
|
2422
|
+
}
|
|
2423
|
+
if (["tool.completed", "tool.failed", "tool.skipped"].includes(type)) {
|
|
2424
|
+
if (toolName === "finish" && resultFingerprint && chatResultFingerprints.has(resultFingerprint)) return true;
|
|
2425
|
+
if (["write_file", "apply_patch"].includes(toolName)) {
|
|
2426
|
+
const pathKey = String(data.path || data.args?.path || data.args?.file || "").replace(/\\/g, "/").trim();
|
|
2427
|
+
if (pathKey && workspaceChangePaths.has(pathKey)) return true;
|
|
2428
|
+
const changeKey = eventWorkspaceChangeFingerprint(event);
|
|
2429
|
+
if (changeKey && workspaceChangeKeys.has(changeKey)) return true;
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (type === "session.finished" && resultFingerprint) {
|
|
2433
|
+
return chatResultFingerprints.has(resultFingerprint) || finishResultFingerprints.has(resultFingerprint);
|
|
2434
|
+
}
|
|
2435
|
+
return false;
|
|
2436
|
+
};
|
|
2437
|
+
const chatItems = shownChat.map((entry, index) => {
|
|
2438
|
+
const embeddedChanges = entry.role === "assistant" ? embeddedWorkspaceChangesFromText(entry.content) : [];
|
|
2439
|
+
const suppressEmbeddedWorkspaceDiffs =
|
|
2440
|
+
embeddedChanges.length > 0 && embeddedChanges.every((change) => workspaceChangeKeys.has(workspaceChangeFingerprint(change)));
|
|
2441
|
+
return {
|
|
2442
|
+
kind: "chat",
|
|
2443
|
+
entry: { ...entry, suppressEmbeddedWorkspaceDiffs },
|
|
2444
|
+
order: index * 2,
|
|
2445
|
+
at: timestampMs(entry.at),
|
|
2446
|
+
};
|
|
2447
|
+
});
|
|
2448
|
+
const eventItems = eventList
|
|
2345
2449
|
.map((event, index) => ({ event, index }))
|
|
2346
2450
|
.filter(({ event }) => isReplayableResumeEvent(event))
|
|
2347
2451
|
.filter(({ event }) => !firstChatTime || timestampMs(event.timestamp) >= firstChatTime)
|
|
2452
|
+
.filter(({ event }) => !shouldSuppressEvent(event))
|
|
2348
2453
|
.map(({ event, index }) => ({
|
|
2349
2454
|
kind: "event",
|
|
2350
2455
|
event,
|
|
@@ -2463,6 +2568,9 @@ function toolStatusDetails(data = {}) {
|
|
|
2463
2568
|
if ((tool === "write_file" || tool === "apply_patch" || tool === "read_file" || tool === "open_workspace_file") && args.path) {
|
|
2464
2569
|
return `${tool}: ${compactLine(args.path, 58)}`;
|
|
2465
2570
|
}
|
|
2571
|
+
if ((tool === "send_to_canvas" || tool === "create_artifact") && (args.title || args.kind)) {
|
|
2572
|
+
return `${tool}: ${compactLine([args.title || "canvas artifact", args.kind || "", args.selected ? "selected" : ""].filter(Boolean).join(" · "), 72)}`;
|
|
2573
|
+
}
|
|
2466
2574
|
if ((tool === "open_url" || tool === "web_research" || tool === "web_search") && (args.url || args.query || args.q)) {
|
|
2467
2575
|
return `${tool}: ${compactLine(args.url || args.query || args.q, 58)}`;
|
|
2468
2576
|
}
|
|
@@ -2552,7 +2660,7 @@ function createState(args = {}) {
|
|
|
2552
2660
|
allowParallelScouts: args.allowParallelScouts ?? true,
|
|
2553
2661
|
enableScs: normalizeScsMode(args.enableScs || process.env.AGINTI_SCS_MODE || "on"),
|
|
2554
2662
|
parallelScoutCount: args.parallelScoutCount || 3,
|
|
2555
|
-
allowWrapperTools: args.allowWrapperTools ??
|
|
2663
|
+
allowWrapperTools: args.allowWrapperTools ?? isWrapperAvailable(args.preferredWrapper || "codex"),
|
|
2556
2664
|
allowDestructive: args.allowDestructive ?? permissionDefaults.allowDestructive ?? false,
|
|
2557
2665
|
allowPasswords: args.allowPasswords ?? permissionDefaults.allowPasswords ?? false,
|
|
2558
2666
|
allowOutsideWorkspaceFileTools:
|
|
@@ -3737,21 +3845,27 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3737
3845
|
return true;
|
|
3738
3846
|
}
|
|
3739
3847
|
if (command === "image-read") {
|
|
3740
|
-
const
|
|
3848
|
+
const rawParts = value.split(/\s+/).filter(Boolean);
|
|
3849
|
+
const providerFlag = ["--codex", "--openai"].includes(rawParts[0]) ? rawParts.shift() : "";
|
|
3850
|
+
const provider = providerFlag ? providerFlag.replace(/^--/, "") : "";
|
|
3851
|
+
const [target, ...questionParts] = rawParts;
|
|
3741
3852
|
if (!target) {
|
|
3742
|
-
printAgentMessage("Usage: /image-read <workspace-image-path-or-url> [question]");
|
|
3853
|
+
printAgentMessage("Usage: /image-read [--codex|--openai] <workspace-image-path-or-url> [question]");
|
|
3743
3854
|
return true;
|
|
3744
3855
|
}
|
|
3745
3856
|
const previousProfile = state.taskProfile;
|
|
3746
3857
|
const previousMaxSteps = state.maxSteps;
|
|
3858
|
+
const previousWrapperTools = state.allowWrapperTools;
|
|
3747
3859
|
try {
|
|
3748
3860
|
state.taskProfile = "image";
|
|
3749
3861
|
state.maxSteps = Math.max(state.maxSteps, 12);
|
|
3862
|
+
if (provider === "codex") state.allowWrapperTools = true;
|
|
3750
3863
|
await runPrompt(
|
|
3751
3864
|
[
|
|
3752
3865
|
`Use the read_image tool on ${target}.`,
|
|
3866
|
+
provider ? `Use read_image provider=${provider}.` : "Use read_image provider=auto so it can use OpenAI vision or Codex fallback.",
|
|
3753
3867
|
questionParts.length ? `Question: ${questionParts.join(" ")}` : "Question: describe the image accurately and report visible text, issues, and uncertainty.",
|
|
3754
|
-
"Do not infer from the filename. Report the read_image artifact
|
|
3868
|
+
"Do not infer from the filename. Report the read_image JSON and Markdown artifact paths.",
|
|
3755
3869
|
].join("\n"),
|
|
3756
3870
|
state,
|
|
3757
3871
|
packageDir
|
|
@@ -3759,6 +3873,7 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3759
3873
|
} finally {
|
|
3760
3874
|
state.taskProfile = previousProfile;
|
|
3761
3875
|
state.maxSteps = previousMaxSteps;
|
|
3876
|
+
state.allowWrapperTools = previousWrapperTools;
|
|
3762
3877
|
}
|
|
3763
3878
|
return true;
|
|
3764
3879
|
}
|