@lazyingart/agintiflow 0.20.175 → 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/scripts/smoke-perception-research.js +22 -0
- package/src/agent-runner.js +34 -0
- package/src/config.js +7 -5
- package/src/interactive-cli.js +13 -5
- 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 +6 -2
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",
|
|
@@ -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/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 = {
|
|
@@ -1112,7 +1113,7 @@ function printHelp() {
|
|
|
1112
1113
|
` ${command("/webapp [port|start|stop|restart|reuse|enable|disable|status]", "Start, reuse, stop, restart, or configure the local webapp.", "helpWebapp")}`,
|
|
1113
1114
|
` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
|
|
1114
1115
|
` ${command("/web-research <query>", "Run a sourced web_research turn with persisted evidence.", "helpWebSearch")}`,
|
|
1115
|
-
` ${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")}`,
|
|
1116
1117
|
` ${command("/research-wrapper [on|off|model reasoning]", "Configure strict JSON wrapper research, default gpt-5.4-mini medium.", "helpWrapper")}`,
|
|
1117
1118
|
` ${command("/scs [on|auto|off|status]", "Toggle Student-Committee-Supervisor gated execution.", "helpEnableScs")}`,
|
|
1118
1119
|
` ${command("/scouts on|off|<1-10>", "Enable parallel DeepSeek scouts and set scout count.", "helpScouts")}`,
|
|
@@ -2659,7 +2660,7 @@ function createState(args = {}) {
|
|
|
2659
2660
|
allowParallelScouts: args.allowParallelScouts ?? true,
|
|
2660
2661
|
enableScs: normalizeScsMode(args.enableScs || process.env.AGINTI_SCS_MODE || "on"),
|
|
2661
2662
|
parallelScoutCount: args.parallelScoutCount || 3,
|
|
2662
|
-
allowWrapperTools: args.allowWrapperTools ??
|
|
2663
|
+
allowWrapperTools: args.allowWrapperTools ?? isWrapperAvailable(args.preferredWrapper || "codex"),
|
|
2663
2664
|
allowDestructive: args.allowDestructive ?? permissionDefaults.allowDestructive ?? false,
|
|
2664
2665
|
allowPasswords: args.allowPasswords ?? permissionDefaults.allowPasswords ?? false,
|
|
2665
2666
|
allowOutsideWorkspaceFileTools:
|
|
@@ -3844,21 +3845,27 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3844
3845
|
return true;
|
|
3845
3846
|
}
|
|
3846
3847
|
if (command === "image-read") {
|
|
3847
|
-
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;
|
|
3848
3852
|
if (!target) {
|
|
3849
|
-
printAgentMessage("Usage: /image-read <workspace-image-path-or-url> [question]");
|
|
3853
|
+
printAgentMessage("Usage: /image-read [--codex|--openai] <workspace-image-path-or-url> [question]");
|
|
3850
3854
|
return true;
|
|
3851
3855
|
}
|
|
3852
3856
|
const previousProfile = state.taskProfile;
|
|
3853
3857
|
const previousMaxSteps = state.maxSteps;
|
|
3858
|
+
const previousWrapperTools = state.allowWrapperTools;
|
|
3854
3859
|
try {
|
|
3855
3860
|
state.taskProfile = "image";
|
|
3856
3861
|
state.maxSteps = Math.max(state.maxSteps, 12);
|
|
3862
|
+
if (provider === "codex") state.allowWrapperTools = true;
|
|
3857
3863
|
await runPrompt(
|
|
3858
3864
|
[
|
|
3859
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.",
|
|
3860
3867
|
questionParts.length ? `Question: ${questionParts.join(" ")}` : "Question: describe the image accurately and report visible text, issues, and uncertainty.",
|
|
3861
|
-
"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.",
|
|
3862
3869
|
].join("\n"),
|
|
3863
3870
|
state,
|
|
3864
3871
|
packageDir
|
|
@@ -3866,6 +3873,7 @@ async function handleCommand(line, state, packageDir) {
|
|
|
3866
3873
|
} finally {
|
|
3867
3874
|
state.taskProfile = previousProfile;
|
|
3868
3875
|
state.maxSteps = previousMaxSteps;
|
|
3876
|
+
state.allowWrapperTools = previousWrapperTools;
|
|
3869
3877
|
}
|
|
3870
3878
|
return true;
|
|
3871
3879
|
}
|
package/src/model-client.js
CHANGED
|
@@ -723,7 +723,7 @@ export async function createPlan(client, config, state) {
|
|
|
723
723
|
"For substantial writing work such as novels, chapters, books, scripts, essays, LaTeX manuscripts, or research-paper prose, plan to call writing_specialist with only the writing brief/canon/style/draft context. The main agent should handle files, citations, checks, and Markdown/LaTeX/Final Draft formatting after the isolated writing draft returns.",
|
|
724
724
|
"For repetitive schema-bound extraction, annotation, conversion, or validation tasks, use json_specialist with only the task, input, schema, and focused instructions. It calls the model directly for strict JSON, tries provider-native structured output when supported, and keeps agent/runtime/tool context out of the specialist prompt.",
|
|
725
725
|
config.allowFileTools
|
|
726
|
-
? "read_image is available for workspace-local or allowed remote screenshots/images
|
|
726
|
+
? "read_image is available for workspace-local or allowed remote screenshots/images. It tries OpenAI vision first when OPENAI_API_KEY is configured, then Codex CLI image wrapper when available and wrappers are enabled. It returns typed visual observations and persists JSON plus Markdown artifacts; never guess from filenames."
|
|
727
727
|
: "",
|
|
728
728
|
config.allowParallelScouts
|
|
729
729
|
? `Parallel scout notes may be injected before execution for complex tasks. Scout count: ${config.parallelScoutCount}.`
|
|
@@ -1179,7 +1179,7 @@ export async function requestNextStep(client, config, messages) {
|
|
|
1179
1179
|
function: {
|
|
1180
1180
|
name: "read_image",
|
|
1181
1181
|
description:
|
|
1182
|
-
"Read and describe workspace-local screenshots/images or allowed remote image URLs
|
|
1182
|
+
"Read and describe workspace-local screenshots/images or allowed remote image URLs. Use for UI screenshots, plots, microscopy images, scanned text, diagrams, and visual debugging. It records hashes and persists JSON plus Markdown perception artifacts. Defaults to OpenAI vision, then falls back to the Codex image wrapper when Codex CLI is available and wrappers are enabled.",
|
|
1183
1183
|
parameters: {
|
|
1184
1184
|
type: "object",
|
|
1185
1185
|
properties: {
|
|
@@ -1191,8 +1191,9 @@ export async function requestNextStep(client, config, messages) {
|
|
|
1191
1191
|
description: "Optional multiple workspace-relative image paths or HTTPS image URLs, maximum 4.",
|
|
1192
1192
|
},
|
|
1193
1193
|
prompt: { type: "string", description: "Question or reading instruction for the image(s)." },
|
|
1194
|
+
provider: { type: "string", enum: ["auto", "openai", "codex"], description: "Perception backend. auto tries OpenAI then Codex wrapper." },
|
|
1194
1195
|
detail: { type: "string", enum: ["low", "high", "auto"], description: "Vision detail level. Defaults to auto." },
|
|
1195
|
-
model: { type: "string", description: "Optional
|
|
1196
|
+
model: { type: "string", description: "Optional perception model. OpenAI defaults to AGINTI_PERCEPTION_MODEL; Codex defaults to the research wrapper model." },
|
|
1196
1197
|
reasoning: { type: "string", enum: ["low", "medium", "high", "xhigh"], description: "Optional reasoning effort. Defaults to medium." },
|
|
1197
1198
|
},
|
|
1198
1199
|
additionalProperties: false,
|
|
@@ -1577,6 +1578,8 @@ export async function requestNextStep(client, config, messages) {
|
|
|
1577
1578
|
toolPayload.error,
|
|
1578
1579
|
toolPayload.reason,
|
|
1579
1580
|
toolPayload.summary ? `Summary: ${toolPayload.summary}` : "",
|
|
1581
|
+
toolPayload.result?.summary ? `Summary: ${toolPayload.result.summary}` : "",
|
|
1582
|
+
toolPayload.result?.answer ? `Answer: ${toolPayload.result.answer}` : "",
|
|
1580
1583
|
toolPayload.counts ? `Counts: ${JSON.stringify(toolPayload.counts)}` : "",
|
|
1581
1584
|
Array.isArray(toolPayload.recommendedReads) && toolPayload.recommendedReads.length
|
|
1582
1585
|
? `Recommended reads: ${toolPayload.recommendedReads.join(", ")}`
|
|
@@ -1585,6 +1588,8 @@ export async function requestNextStep(client, config, messages) {
|
|
|
1585
1588
|
? `Results:\n${toolPayload.results.map((item, index) => `${index + 1}. ${item.title} ${item.url}`).join("\n")}`
|
|
1586
1589
|
: "",
|
|
1587
1590
|
toolPayload.path ? `Path: ${toolPayload.path}` : "",
|
|
1591
|
+
toolPayload.markdownPath ? `Markdown: ${toolPayload.markdownPath}` : "",
|
|
1592
|
+
toolPayload.artifactPath ? `Artifact: ${toolPayload.artifactPath}` : "",
|
|
1588
1593
|
Array.isArray(toolPayload.changes)
|
|
1589
1594
|
? toolPayload.changes
|
|
1590
1595
|
.map((change) => [change.path ? `Path: ${change.path}` : "", change.diff ? `Diff:\n${change.diff}` : ""].filter(Boolean).join("\n"))
|
package/src/perception-tools.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import OpenAI from "openai";
|
|
5
5
|
import { isDomainAllowed } from "./guardrails.js";
|
|
6
6
|
import { redactSensitiveText } from "./redaction.js";
|
|
7
|
-
import { normalizeWrapperName, runAgentWrapper } from "./tool-wrappers.js";
|
|
7
|
+
import { isWrapperAvailable, normalizeWrapperName, runAgentWrapper, runCodexImageWrapper } from "./tool-wrappers.js";
|
|
8
8
|
import { searchWeb } from "./web-search.js";
|
|
9
9
|
import { resolveWorkspacePath } from "./workspace-tools.js";
|
|
10
10
|
|
|
@@ -154,6 +154,92 @@ async function persistToolArtifact(store, subdir, stem, payload) {
|
|
|
154
154
|
return filePath;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function markdownList(items = []) {
|
|
158
|
+
const values = Array.isArray(items) ? items.map((item) => String(item || "").trim()).filter(Boolean) : [];
|
|
159
|
+
return values.length ? values.map((item) => `- ${item}`).join("\n") : "- None recorded.";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function imageReadMarkdown(payload = {}) {
|
|
163
|
+
const result = payload.result || {};
|
|
164
|
+
const images = Array.isArray(payload.images) ? payload.images : [];
|
|
165
|
+
return [
|
|
166
|
+
"# Image Reading Report",
|
|
167
|
+
"",
|
|
168
|
+
`- Status: ${payload.ok ? "ok" : "failed"}`,
|
|
169
|
+
`- Provider: ${payload.provider || ""}`,
|
|
170
|
+
payload.wrapper ? `- Wrapper: ${payload.wrapper}` : "",
|
|
171
|
+
payload.model ? `- Model: ${payload.model}` : "",
|
|
172
|
+
payload.reasoning ? `- Reasoning: ${payload.reasoning}` : "",
|
|
173
|
+
payload.detail ? `- Detail: ${payload.detail}` : "",
|
|
174
|
+
payload.prompt ? `- Prompt: ${payload.prompt}` : "",
|
|
175
|
+
payload.error ? `- Error: ${payload.error}` : "",
|
|
176
|
+
"",
|
|
177
|
+
"## Images",
|
|
178
|
+
"",
|
|
179
|
+
images.length
|
|
180
|
+
? images
|
|
181
|
+
.map(
|
|
182
|
+
(image, index) =>
|
|
183
|
+
`${index + 1}. ${image.path || image.url || "image"} (${image.mime || "unknown"}, ${image.sizeBytes || 0} bytes, sha256=${image.sha256 || ""})`
|
|
184
|
+
)
|
|
185
|
+
.join("\n")
|
|
186
|
+
: "No loaded images were recorded.",
|
|
187
|
+
"",
|
|
188
|
+
"## Summary",
|
|
189
|
+
"",
|
|
190
|
+
result.summary || payload.rawText || payload.error || "No summary was returned.",
|
|
191
|
+
"",
|
|
192
|
+
"## Answer",
|
|
193
|
+
"",
|
|
194
|
+
result.answer || result.summary || payload.rawText || payload.error || "No answer was returned.",
|
|
195
|
+
"",
|
|
196
|
+
"## Visible Text",
|
|
197
|
+
"",
|
|
198
|
+
markdownList(result.visibleText),
|
|
199
|
+
"",
|
|
200
|
+
"## Observations",
|
|
201
|
+
"",
|
|
202
|
+
markdownList(result.observations),
|
|
203
|
+
"",
|
|
204
|
+
"## Issues",
|
|
205
|
+
"",
|
|
206
|
+
markdownList(result.issues),
|
|
207
|
+
"",
|
|
208
|
+
"## Uncertainty",
|
|
209
|
+
"",
|
|
210
|
+
markdownList(result.uncertainty || result.uncertainties),
|
|
211
|
+
"",
|
|
212
|
+
]
|
|
213
|
+
.join("\n")
|
|
214
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
215
|
+
.trimEnd() + "\n";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function persistMarkdownArtifact(store, config, subdir, stem, payload) {
|
|
219
|
+
const content = imageReadMarkdown(payload);
|
|
220
|
+
const filename = `${isoStamp()}-${stem}.md`;
|
|
221
|
+
let markdownArtifactPath = "";
|
|
222
|
+
let markdownPath = "";
|
|
223
|
+
|
|
224
|
+
if (store?.artifactsDir) {
|
|
225
|
+
await store.ensure();
|
|
226
|
+
const outputDir = path.join(store.artifactsDir, subdir);
|
|
227
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
228
|
+
markdownArtifactPath = path.join(outputDir, filename);
|
|
229
|
+
await fs.writeFile(markdownArtifactPath, content, "utf8");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (config?.commandCwd) {
|
|
233
|
+
const workspaceDir = path.join(config.commandCwd, "artifacts", subdir);
|
|
234
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
235
|
+
const workspacePath = path.join(workspaceDir, filename);
|
|
236
|
+
await fs.writeFile(workspacePath, content, "utf8");
|
|
237
|
+
markdownPath = path.relative(config.commandCwd, workspacePath).replace(/\\/g, "/");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { markdownArtifactPath, markdownPath };
|
|
241
|
+
}
|
|
242
|
+
|
|
157
243
|
function imageMimeForPath(inputPath, contentType = "") {
|
|
158
244
|
const type = String(contentType || "").split(";")[0].trim().toLowerCase();
|
|
159
245
|
if (type.startsWith("image/")) return type;
|
|
@@ -310,17 +396,132 @@ async function callOpenAiImageRead(args, images, config) {
|
|
|
310
396
|
};
|
|
311
397
|
}
|
|
312
398
|
|
|
399
|
+
function codexImageReadPrompt(args, images) {
|
|
400
|
+
return [
|
|
401
|
+
"Inspect the attached image(s) directly and return strict JSON only. Do not wrap in Markdown.",
|
|
402
|
+
"Use this schema:",
|
|
403
|
+
JSON.stringify({
|
|
404
|
+
summary: "short factual summary",
|
|
405
|
+
visibleText: ["OCR text or labels, empty if none"],
|
|
406
|
+
observations: ["concrete visible details"],
|
|
407
|
+
issues: ["possible UI/data/quality issues, empty if none"],
|
|
408
|
+
answer: "direct answer to the user's question",
|
|
409
|
+
uncertainty: ["limits, ambiguity, or details not visible"],
|
|
410
|
+
}),
|
|
411
|
+
"Rules: describe only visible evidence. Do not infer from filenames. If uncertain, say so.",
|
|
412
|
+
`User question: ${String(args.prompt || args.question || "Describe the image accurately.").trim()}`,
|
|
413
|
+
`Image metadata: ${images
|
|
414
|
+
.map((image, index) => `${index + 1}. ${image.path || image.url} ${image.mime} ${image.sizeBytes} bytes sha256=${image.sha256}`)
|
|
415
|
+
.join("; ")}`,
|
|
416
|
+
].join("\n");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function codexReadableImagePaths(images, store) {
|
|
420
|
+
const imagePaths = [];
|
|
421
|
+
for (const image of images) {
|
|
422
|
+
if (image.absolutePath) {
|
|
423
|
+
imagePaths.push(image.absolutePath);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (!store?.artifactsDir) {
|
|
427
|
+
throw new Error("Codex image fallback needs a local image file; remote image copy store is unavailable.");
|
|
428
|
+
}
|
|
429
|
+
await store.ensure();
|
|
430
|
+
const ext = image.mime === "image/jpeg" ? ".jpg" : image.mime === "image/webp" ? ".webp" : image.mime === "image/gif" ? ".gif" : ".png";
|
|
431
|
+
const outputDir = path.join(store.artifactsDir, "perception", "codex-inputs");
|
|
432
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
433
|
+
const outputPath = path.join(outputDir, `${isoStamp()}-${image.sha256.slice(0, 12)}${ext}`);
|
|
434
|
+
const raw = Buffer.from(String(image.dataUrl || "").replace(/^data:image\/[a-z0-9.+-]+;base64,/i, ""), "base64");
|
|
435
|
+
if (!raw.length) throw new Error("Remote image copy for Codex fallback was empty.");
|
|
436
|
+
await fs.writeFile(outputPath, raw);
|
|
437
|
+
imagePaths.push(outputPath);
|
|
438
|
+
}
|
|
439
|
+
return imagePaths;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function callCodexImageRead(args, images, config, store, priorError = null) {
|
|
443
|
+
if (args.codexDryRun) {
|
|
444
|
+
return {
|
|
445
|
+
provider: "codex-wrapper-dry-run",
|
|
446
|
+
wrapper: "codex",
|
|
447
|
+
model: args.model || config.researchWrapperModel || "gpt-5.4-mini",
|
|
448
|
+
reasoning: args.reasoning || config.researchWrapperReasoning || "medium",
|
|
449
|
+
rawText: "",
|
|
450
|
+
parsed: {
|
|
451
|
+
summary: "Codex image fallback dry run",
|
|
452
|
+
visibleText: [],
|
|
453
|
+
observations: ["Codex image wrapper fallback path was selected."],
|
|
454
|
+
issues: [],
|
|
455
|
+
answer: "dry run",
|
|
456
|
+
uncertainty: [],
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (config.allowWrapperTools === false && args.provider !== "codex") {
|
|
461
|
+
throw new Error("Codex image fallback is disabled because wrapper tools are off.");
|
|
462
|
+
}
|
|
463
|
+
if (!isWrapperAvailable("codex")) {
|
|
464
|
+
throw new Error("Codex image fallback is unavailable because the Codex CLI was not found on PATH.");
|
|
465
|
+
}
|
|
466
|
+
const imagePaths = await codexReadableImagePaths(images, store);
|
|
467
|
+
const wrapperConfig = {
|
|
468
|
+
...config,
|
|
469
|
+
wrapperModel: args.model || config.researchWrapperModel || process.env.AGINTI_RESEARCH_WRAPPER_MODEL || "gpt-5.4-mini",
|
|
470
|
+
wrapperReasoning: args.reasoning || config.researchWrapperReasoning || process.env.AGINTI_RESEARCH_WRAPPER_REASONING || "medium",
|
|
471
|
+
};
|
|
472
|
+
const wrapperResult = await runCodexImageWrapper(
|
|
473
|
+
{
|
|
474
|
+
prompt: [
|
|
475
|
+
codexImageReadPrompt(args, images),
|
|
476
|
+
priorError ? `OpenAI vision fallback reason: ${redactSensitiveText(priorError instanceof Error ? priorError.message : String(priorError))}` : "",
|
|
477
|
+
]
|
|
478
|
+
.filter(Boolean)
|
|
479
|
+
.join("\n\n"),
|
|
480
|
+
imagePaths,
|
|
481
|
+
},
|
|
482
|
+
wrapperConfig
|
|
483
|
+
);
|
|
484
|
+
if (!wrapperResult.ok) throw new Error(wrapperResult.error || "Codex image fallback failed.");
|
|
485
|
+
const parsed = firstJsonObject(wrapperResult.stdout || "");
|
|
486
|
+
return {
|
|
487
|
+
provider: "codex-wrapper",
|
|
488
|
+
wrapper: "codex",
|
|
489
|
+
model: wrapperResult.model || wrapperConfig.wrapperModel,
|
|
490
|
+
reasoning: wrapperResult.reasoning || wrapperConfig.wrapperReasoning,
|
|
491
|
+
rawText: compactText(wrapperResult.stdout || ""),
|
|
492
|
+
parsed,
|
|
493
|
+
stderr: compactText(wrapperResult.stderr || "", 4000),
|
|
494
|
+
fallback: Boolean(priorError || wrapperResult.fallback),
|
|
495
|
+
fallbackReason: priorError ? redactSensitiveText(priorError instanceof Error ? priorError.message : String(priorError)) : "",
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function callImageRead(args, images, config, store) {
|
|
500
|
+
const provider = String(args.provider || args.engine || "auto").trim().toLowerCase();
|
|
501
|
+
if (args.dryRun) {
|
|
502
|
+
return { provider: "dry-run", rawText: "", parsed: { summary: "read_image dry run", answer: "dry run" } };
|
|
503
|
+
}
|
|
504
|
+
if (provider === "codex") return callCodexImageRead(args, images, config, store);
|
|
505
|
+
if (provider === "openai") return callOpenAiImageRead(args, images, config);
|
|
506
|
+
try {
|
|
507
|
+
return await callOpenAiImageRead(args, images, config);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return callCodexImageRead(args, images, config, store, error);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
313
513
|
export async function readImage(args = {}, config = {}, store = null) {
|
|
314
514
|
const loadedImages = [];
|
|
315
515
|
let payload = null;
|
|
316
516
|
try {
|
|
317
517
|
const images = await loadImageInputs(args, config);
|
|
318
518
|
loadedImages.push(...images);
|
|
319
|
-
const analysis =
|
|
519
|
+
const analysis = await callImageRead(args, images, config, store);
|
|
320
520
|
payload = {
|
|
321
521
|
ok: true,
|
|
322
522
|
toolName: "read_image",
|
|
323
523
|
provider: analysis.provider,
|
|
524
|
+
wrapper: analysis.wrapper || "",
|
|
324
525
|
model: analysis.model || "",
|
|
325
526
|
reasoning: analysis.reasoning || "",
|
|
326
527
|
detail: analysis.detail || "",
|
|
@@ -328,8 +529,12 @@ export async function readImage(args = {}, config = {}, store = null) {
|
|
|
328
529
|
images: images.map(({ dataUrl, absolutePath, ...image }) => image),
|
|
329
530
|
result: analysis.parsed || { summary: analysis.rawText, answer: analysis.rawText, uncertainty: ["Model output was not valid JSON."] },
|
|
330
531
|
rawText: analysis.parsed ? "" : analysis.rawText,
|
|
532
|
+
stderr: analysis.stderr || "",
|
|
533
|
+
fallback: Boolean(analysis.fallback),
|
|
534
|
+
fallbackReason: analysis.fallbackReason || "",
|
|
331
535
|
};
|
|
332
536
|
payload.artifactPath = await persistToolArtifact(store, "perception", "read-image", payload);
|
|
537
|
+
Object.assign(payload, await persistMarkdownArtifact(store, config, "perception", "read-image", payload));
|
|
333
538
|
return payload;
|
|
334
539
|
} catch (error) {
|
|
335
540
|
payload = {
|
|
@@ -339,6 +544,7 @@ export async function readImage(args = {}, config = {}, store = null) {
|
|
|
339
544
|
images: loadedImages.map(({ dataUrl, absolutePath, ...image }) => image),
|
|
340
545
|
};
|
|
341
546
|
payload.artifactPath = await persistToolArtifact(store, "perception", "read-image-failed", payload);
|
|
547
|
+
Object.assign(payload, await persistMarkdownArtifact(store, config, "perception", "read-image-failed", payload));
|
|
342
548
|
return payload;
|
|
343
549
|
}
|
|
344
550
|
}
|
package/src/scs-controller.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
buildScsEvidenceLedger,
|
|
6
6
|
deriveScsTaskContract,
|
|
7
7
|
deterministicFinishBlocker,
|
|
8
|
+
evaluateScsSemanticContract,
|
|
8
9
|
evaluateScsEvidence,
|
|
9
10
|
finishResultClaimsBlocker,
|
|
10
11
|
hasScsBlockerEvidence,
|
|
@@ -140,6 +141,25 @@ function fallbackBlockedPlan(goal = "", studentReason = "") {
|
|
|
140
141
|
.join("\n");
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
function fallbackHardContractPlan(goal = "", contract = {}, studentReason = "") {
|
|
145
|
+
const exactOutputPaths = normalizeStringList(contract.exactOutputPaths, []);
|
|
146
|
+
const requiredTextTerms = normalizeStringList(contract.requiredTextTerms, []);
|
|
147
|
+
const forbiddenTextTerms = normalizeStringList(contract.forbiddenTextTerms, []);
|
|
148
|
+
return [
|
|
149
|
+
"1. Execute the user's target work under the deterministic hard-contract fallback plan.",
|
|
150
|
+
exactOutputPaths.length
|
|
151
|
+
? `2. Write the requested output exactly at: ${exactOutputPaths.join(", ")}. If an output file already exists and the user allowed overwrite/update, overwrite it intentionally.`
|
|
152
|
+
: "2. Create or update the requested output artifact at the user-specified location.",
|
|
153
|
+
requiredTextTerms.length ? `3. Ensure the output contains these required term(s): ${requiredTextTerms.join(", ")}.` : "",
|
|
154
|
+
forbiddenTextTerms.length ? `4. Ensure the output does not contain these forbidden term(s): ${forbiddenTextTerms.join(", ")}.` : "",
|
|
155
|
+
"5. Run concrete validation commands or inspections for file existence and content before finish.",
|
|
156
|
+
studentReason ? `6. Preserve the validator concern while executing: ${compact(studentReason, 180)}` : "",
|
|
157
|
+
goal ? `7. Original goal remains authoritative: ${compact(goal, 220)}` : "",
|
|
158
|
+
]
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
143
163
|
function parseJsonObject(content = "") {
|
|
144
164
|
const text = String(content || "").trim();
|
|
145
165
|
if (!text) return null;
|
|
@@ -222,6 +242,47 @@ function normalizePlanText(plan) {
|
|
|
222
242
|
return redacted.length <= 1800 ? redacted : `${redacted.slice(0, 1776)} ... [truncated]`;
|
|
223
243
|
}
|
|
224
244
|
|
|
245
|
+
function formatHardContractForPrompt(contract = {}) {
|
|
246
|
+
const lines = [];
|
|
247
|
+
const exactOutputPaths = normalizeStringList(contract.exactOutputPaths, []);
|
|
248
|
+
const requiredTextTerms = normalizeStringList(contract.requiredTextTerms, []);
|
|
249
|
+
const forbiddenTextTerms = normalizeStringList(contract.forbiddenTextTerms, []);
|
|
250
|
+
const forbiddenActions = normalizeStringList(contract.forbiddenActions, []);
|
|
251
|
+
if (exactOutputPaths.length) lines.push(`Exact output path(s): ${exactOutputPaths.join(", ")}`);
|
|
252
|
+
if (requiredTextTerms.length) lines.push(`Required text term(s) in the output: ${requiredTextTerms.join(", ")}`);
|
|
253
|
+
if (forbiddenTextTerms.length) lines.push(`Forbidden text term(s) in the output: ${forbiddenTextTerms.join(", ")}`);
|
|
254
|
+
if (forbiddenActions.length) lines.push(`Forbidden action(s): ${forbiddenActions.join("; ")}`);
|
|
255
|
+
return lines.length
|
|
256
|
+
? [
|
|
257
|
+
"Inferred hard task contract. Preserve these literally; do not replace them with weaker or contradictory criteria:",
|
|
258
|
+
...lines.map((line) => `- ${line}`),
|
|
259
|
+
].join("\n")
|
|
260
|
+
: "";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function deterministicPlanContractIssue(committee = {}, contract = {}) {
|
|
264
|
+
const planText = `${committee.phaseGoal || ""}\n${committee.plan || ""}\n${(committee.acceptanceCriteria || []).join("\n")}`;
|
|
265
|
+
const missingPath = normalizeStringList(contract.exactOutputPaths, []).filter((item) => !planText.includes(item));
|
|
266
|
+
const missingRequiredTerms = normalizeStringList(contract.requiredTextTerms, []).filter((item) => !planText.includes(item));
|
|
267
|
+
const forbiddenTermsInPlan = normalizeStringList(contract.forbiddenTextTerms, []).filter((item) => planText.includes(item));
|
|
268
|
+
if (missingPath.length || missingRequiredTerms.length || forbiddenTermsInPlan.length) {
|
|
269
|
+
return {
|
|
270
|
+
decision: "veto_plan",
|
|
271
|
+
confidence: 0.94,
|
|
272
|
+
evidence: [
|
|
273
|
+
missingPath.length ? `Plan omitted exact output path(s): ${missingPath.join(", ")}` : "",
|
|
274
|
+
missingRequiredTerms.length ? `Plan omitted required text term(s): ${missingRequiredTerms.join(", ")}` : "",
|
|
275
|
+
forbiddenTermsInPlan.length ? `Plan includes forbidden text term(s): ${forbiddenTermsInPlan.join(", ")}` : "",
|
|
276
|
+
].filter(Boolean),
|
|
277
|
+
reason:
|
|
278
|
+
"The phase plan does not preserve the user's inferred hard task contract. It must carry exact paths and required/forbidden output terms into acceptance criteria.",
|
|
279
|
+
next_required_action:
|
|
280
|
+
"Committee must draft a new plan whose acceptance criteria explicitly include every exact output path, required text term, and forbidden text term from the hard contract.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
225
286
|
function isBrowserSubmitGoal(goal = "") {
|
|
226
287
|
const text = String(goal || "");
|
|
227
288
|
const browserSignal =
|
|
@@ -345,6 +406,7 @@ export function buildScsEvidencePack(state = {}, context = {}) {
|
|
|
345
406
|
export function buildSupervisorInstruction(scs = {}) {
|
|
346
407
|
const criteria = normalizeStringList(scs.acceptanceCriteria);
|
|
347
408
|
const stopConditions = normalizeStringList(scs.stopConditions);
|
|
409
|
+
const hardContract = formatHardContractForPrompt(scs.taskContract || {});
|
|
348
410
|
return [
|
|
349
411
|
"SCS mode is enabled. SCS means Student-Committee-Supervisor; do not redefine the acronym.",
|
|
350
412
|
"You are the supervisor executor in that Student-Committee-Supervisor pipeline.",
|
|
@@ -355,6 +417,7 @@ export function buildSupervisorInstruction(scs = {}) {
|
|
|
355
417
|
"If tool evidence invalidates the plan, stop repeating the failed path and explain the blocker through finish or wait for student review.",
|
|
356
418
|
"Approved phase plan:",
|
|
357
419
|
scs.plan || fallbackPlan(),
|
|
420
|
+
hardContract,
|
|
358
421
|
criteria.length ? `Acceptance criteria:\n${criteria.map((item) => `- ${item}`).join("\n")}` : "",
|
|
359
422
|
stopConditions.length ? `Stop conditions:\n${stopConditions.map((item) => `- ${item}`).join("\n")}` : "",
|
|
360
423
|
"Before calling finish, include concrete evidence: files changed, commands/checks run, artifacts created, or a clear limitation.",
|
|
@@ -376,6 +439,7 @@ function committeeSystemPrompt({ phaseKind = "initial" } = {}) {
|
|
|
376
439
|
: "",
|
|
377
440
|
browserStateReconciliationGuidance(),
|
|
378
441
|
formatBehaviorContractForPrompt({ mode: "plan" }),
|
|
442
|
+
"Preserve exact user hard constraints literally: output paths, required words/phrases, forbidden words/phrases, model/duration/tier, and explicit no-action instructions. Do not invent contradictory acceptance criteria such as a different length target or output location.",
|
|
379
443
|
"Return strict JSON with keys: role, phase_goal, plan, acceptance_criteria, allowed_tools, stop_conditions.",
|
|
380
444
|
]
|
|
381
445
|
.filter(Boolean)
|
|
@@ -389,6 +453,7 @@ function studentPlanGatePrompt() {
|
|
|
389
453
|
"Judge whether the committee phase plan is safe, scoped, minimal, permission-aware, and evidence-oriented.",
|
|
390
454
|
"You cannot execute tools or approve your own work. If the plan is weak, veto it so the committee must draft a better plan.",
|
|
391
455
|
"For browser tasks, reject plans that stop merely because a state field is unknown when the user requested a target state and a bounded set-then-verify path is available.",
|
|
456
|
+
"Reject any plan whose phase goal, steps, or acceptance criteria omit exact output paths, required output phrases, or forbidden output phrases from the hard task contract. Also reject plans that add contradictory constraints not requested by the user.",
|
|
392
457
|
formatBehaviorContractForPrompt({ mode: "plan" }),
|
|
393
458
|
"Return strict JSON with keys: role, decision, confidence, evidence, reason, next_required_action.",
|
|
394
459
|
].join(" ");
|
|
@@ -468,6 +533,11 @@ async function createScsPhase(client, config, state, context = {}, options = {})
|
|
|
468
533
|
"approve_plan"
|
|
469
534
|
);
|
|
470
535
|
let lastValidatorConcern = validatorFeedback?.reason || validatorFeedback?.nextRequiredAction || "";
|
|
536
|
+
const taskContract = deriveScsTaskContract({
|
|
537
|
+
goal: state.goal,
|
|
538
|
+
taskProfile: context.taskProfile || "",
|
|
539
|
+
acceptanceCriteria: [],
|
|
540
|
+
});
|
|
471
541
|
|
|
472
542
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
473
543
|
const rawCommittee = await callJson(
|
|
@@ -482,6 +552,7 @@ async function createScsPhase(client, config, state, context = {}, options = {})
|
|
|
482
552
|
role: "user",
|
|
483
553
|
content: [
|
|
484
554
|
`Goal and evidence:\n${evidence}`,
|
|
555
|
+
formatHardContractForPrompt(taskContract),
|
|
485
556
|
validatorFeedback
|
|
486
557
|
? `Student validator feedback that caused this ${phaseKind}:\n${compactJson(validatorFeedback, 1800)}`
|
|
487
558
|
: "",
|
|
@@ -496,6 +567,13 @@ async function createScsPhase(client, config, state, context = {}, options = {})
|
|
|
496
567
|
"SCS committee"
|
|
497
568
|
);
|
|
498
569
|
committee = normalizeCommitteePlan(rawCommittee, state.goal);
|
|
570
|
+
const deterministicIssue = deterministicPlanContractIssue(committee, taskContract);
|
|
571
|
+
if (deterministicIssue) {
|
|
572
|
+
student = normalizeDecision(deterministicIssue, "veto_plan");
|
|
573
|
+
lastValidatorConcern = student.reason || student.nextRequiredAction || lastValidatorConcern;
|
|
574
|
+
if (attempt < 2) continue;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
499
577
|
|
|
500
578
|
const rawStudent = await callJson(
|
|
501
579
|
client,
|
|
@@ -507,7 +585,11 @@ async function createScsPhase(client, config, state, context = {}, options = {})
|
|
|
507
585
|
},
|
|
508
586
|
{
|
|
509
587
|
role: "user",
|
|
510
|
-
content:
|
|
588
|
+
content: [
|
|
589
|
+
`Goal/evidence:\n${evidence}`,
|
|
590
|
+
formatHardContractForPrompt(taskContract),
|
|
591
|
+
`Committee plan:\n${compactJson(committee, 4000)}`,
|
|
592
|
+
].join("\n\n"),
|
|
511
593
|
},
|
|
512
594
|
],
|
|
513
595
|
student,
|
|
@@ -519,15 +601,19 @@ async function createScsPhase(client, config, state, context = {}, options = {})
|
|
|
519
601
|
}
|
|
520
602
|
|
|
521
603
|
if (student.decision === "veto_plan") {
|
|
604
|
+
const hardContractPlan = fallbackHardContractPlan(state.goal, taskContract, student.reason || lastValidatorConcern);
|
|
605
|
+
const hardContractCriteria = [
|
|
606
|
+
...normalizeStringList(taskContract.exactOutputPaths, []).map((item) => `Exact output path is used: ${item}`),
|
|
607
|
+
...normalizeStringList(taskContract.requiredTextTerms, []).map((item) => `Output contains required text: ${item}`),
|
|
608
|
+
...normalizeStringList(taskContract.forbiddenTextTerms, []).map((item) => `Output omits forbidden text: ${item}`),
|
|
609
|
+
"Concrete file/content validation evidence is collected before finish.",
|
|
610
|
+
];
|
|
522
611
|
committee = normalizeCommitteePlan(
|
|
523
612
|
{
|
|
524
|
-
phase_goal: "
|
|
525
|
-
plan:
|
|
526
|
-
acceptance_criteria:
|
|
527
|
-
|
|
528
|
-
"The final report names the student validator concern and requested clarification or override.",
|
|
529
|
-
],
|
|
530
|
-
stop_conditions: ["Any attempt to proceed with target work without an approved phase plan."],
|
|
613
|
+
phase_goal: "Execute the user's target work using the deterministic hard-contract fallback plan.",
|
|
614
|
+
plan: hardContractPlan,
|
|
615
|
+
acceptance_criteria: hardContractCriteria,
|
|
616
|
+
stop_conditions: ["A required file path is inaccessible.", "A required/forbidden text constraint cannot be satisfied."],
|
|
531
617
|
},
|
|
532
618
|
state.goal
|
|
533
619
|
);
|
|
@@ -536,9 +622,9 @@ async function createScsPhase(client, config, state, context = {}, options = {})
|
|
|
536
622
|
decision: "approve_plan",
|
|
537
623
|
confidence: 0.9,
|
|
538
624
|
reason:
|
|
539
|
-
"Student veto remained after committee retries;
|
|
625
|
+
"Student veto remained after committee retries; runtime synthesized a deterministic hard-contract fallback plan that preserves exact user constraints.",
|
|
540
626
|
evidence: student.evidence || [],
|
|
541
|
-
next_required_action: "
|
|
627
|
+
next_required_action: "supervisor_execute_hard_contract_plan",
|
|
542
628
|
},
|
|
543
629
|
"approve_plan"
|
|
544
630
|
);
|
|
@@ -756,8 +842,28 @@ export async function reviewScsFinish(client, config, state, result = "", contex
|
|
|
756
842
|
});
|
|
757
843
|
const evidenceLedger = buildScsEvidenceLedger({ state, context });
|
|
758
844
|
const evidenceEvaluation = evaluateScsEvidence(taskContract, evidenceLedger);
|
|
845
|
+
const semanticEvaluation = evaluateScsSemanticContract(taskContract, {
|
|
846
|
+
commandCwd: config.commandCwd || process.cwd(),
|
|
847
|
+
});
|
|
759
848
|
const deterministicBlocker = deterministicFinishBlocker(taskContract, evidenceLedger, evidenceEvaluation);
|
|
760
849
|
const hasRealBlocker = hasScsBlockerEvidence(evidenceLedger) && finishResultClaimsBlocker(result);
|
|
850
|
+
if (!semanticEvaluation.ok && !hasRealBlocker) {
|
|
851
|
+
return normalizeDecision(
|
|
852
|
+
{
|
|
853
|
+
decision: "finish_rejected",
|
|
854
|
+
confidence: 0.97,
|
|
855
|
+
reason: `SCS semantic hard-contract gate rejected finish: ${semanticEvaluation.reason}`,
|
|
856
|
+
evidence: [
|
|
857
|
+
...(semanticEvaluation.missingFiles || []).map((item) => `missing file: ${item}`),
|
|
858
|
+
...(semanticEvaluation.missingRequiredText || []).map((item) => `missing required text: ${item}`),
|
|
859
|
+
...(semanticEvaluation.presentForbiddenText || []).map((item) => `forbidden text present: ${item}`),
|
|
860
|
+
],
|
|
861
|
+
next_required_action:
|
|
862
|
+
"Revise the exact output file(s) to satisfy the required and forbidden text terms, then run concrete validation commands before finishing.",
|
|
863
|
+
},
|
|
864
|
+
"finish_rejected"
|
|
865
|
+
);
|
|
866
|
+
}
|
|
761
867
|
const requiresEvidence =
|
|
762
868
|
taskContract.requiresExternalEvidence ||
|
|
763
869
|
finishRequiresExternalEvidence(context.goal || state.goal || "", context.taskProfile || config.taskProfile || "");
|
package/src/scs-evidence.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
2
4
|
|
|
3
5
|
const CATEGORY_LABELS = {
|
|
@@ -62,6 +64,89 @@ function unique(items = []) {
|
|
|
62
64
|
return [...new Set(items.filter(Boolean))];
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function uniqueLimited(items = [], limit = 16) {
|
|
68
|
+
return unique(items.map((item) => compact(item, 120)).filter(Boolean)).slice(0, limit);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function quotedTerms(text = "") {
|
|
72
|
+
const terms = [];
|
|
73
|
+
const patterns = [
|
|
74
|
+
/“([^”]{1,80})”/g,
|
|
75
|
+
/"([^"\n]{1,80})"/g,
|
|
76
|
+
/'([^'\n]{1,80})'/g,
|
|
77
|
+
/`([^`\n]{1,80})`/g,
|
|
78
|
+
];
|
|
79
|
+
for (const pattern of patterns) {
|
|
80
|
+
for (const match of String(text || "").matchAll(pattern)) {
|
|
81
|
+
const term = String(match[1] || "").trim();
|
|
82
|
+
if (!term || /[\\/]/.test(term)) continue;
|
|
83
|
+
terms.push(term);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return terms;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function splitInlineTerms(text = "") {
|
|
90
|
+
return String(text || "")
|
|
91
|
+
.split(/[、,,;;]/)
|
|
92
|
+
.map((item) => item.replace(/[。.!!??::]/g, "").trim())
|
|
93
|
+
.filter((item) => item.length >= 2 && item.length <= 40 && !/[\\/]/.test(item));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function inferExactOutputPaths(goal = "") {
|
|
97
|
+
const paths = [];
|
|
98
|
+
const lines = String(goal || "").split(/\n+/);
|
|
99
|
+
const pathPattern = /(?:^|[\s"'`::])((?:~|\.{1,2}|[A-Za-z0-9_\-\u4e00-\u9fff])[\w./~\-\u4e00-\u9fff ]{0,220}\.(?:md|txt|json|ya?ml|html|css|js|ts|tsx|jsx|py|sh|csv|tex|svg|png|jpe?g|webp|mp4|mov|pdf|docx))(?:$|[\s"'`,,。;;])/gi;
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (!/\b(save|saved|write|written|output|create|store)\b|保存|写入|寫入|输出|輸出|创建|建立/.test(line)) continue;
|
|
102
|
+
for (const match of line.matchAll(pathPattern)) {
|
|
103
|
+
const raw = String(match[1] || "").trim();
|
|
104
|
+
if (!raw) continue;
|
|
105
|
+
paths.push(raw);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return uniqueLimited(paths, 8);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inferRequiredTextTerms(goal = "") {
|
|
112
|
+
const terms = [];
|
|
113
|
+
const lines = String(goal || "").split(/\n+/);
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const positiveSegment = String(line || "").split(
|
|
116
|
+
/(?:并)?确认没有|(?:并)?確認沒有|没有|沒有|\b(?:does not contain|do not contain|not contain|not include|without)\b/i
|
|
117
|
+
)[0];
|
|
118
|
+
if (
|
|
119
|
+
/\b(must|require|required|include|contain|contains|check|verify|grep|keyword|keywords)\b/i.test(line) ||
|
|
120
|
+
/必须|必須|要求|包含|检查|檢查|验证|驗證|关键词|關鍵詞|自检|自檢/.test(line)
|
|
121
|
+
) {
|
|
122
|
+
terms.push(...quotedTerms(positiveSegment));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return uniqueLimited(terms, 24);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function inferForbiddenTextTerms(goal = "") {
|
|
129
|
+
const terms = [];
|
|
130
|
+
const lines = String(goal || "").split(/\n+/);
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const text = String(line || "");
|
|
133
|
+
if (
|
|
134
|
+
/\b(?:do not|don't|dont|must not|never)\s+(?:write|include|mention|contain)\b/i.test(text) ||
|
|
135
|
+
/不要(?:写|寫|包含|提到)|不得(?:写|寫|包含|提到)|禁止(?:写|寫|包含|提到)|(?:确认|確認)(?:没有|沒有)/.test(text)
|
|
136
|
+
) {
|
|
137
|
+
const tail =
|
|
138
|
+
text.match(/(?:不要(?:写|寫|包含|提到)|不得(?:写|寫|包含|提到)|禁止(?:写|寫|包含|提到))(.+)/)?.[1] ||
|
|
139
|
+
text.match(/(?:确认|確認)(?:没有|沒有)(.+)/)?.[1] ||
|
|
140
|
+
text.match(/\b(?:do not|don't|dont|must not|never)\s+(?:write|include|mention|contain)\s+(.+)/i)?.[1] ||
|
|
141
|
+
"";
|
|
142
|
+
terms.push(...quotedTerms(tail));
|
|
143
|
+
const unquotedTail = tail.replace(/“[^”]+”|"[^"\n]+"|'[^'\n]+'|`[^`\n]+`/g, "");
|
|
144
|
+
terms.push(...splitInlineTerms(unquotedTail).filter((item) => !/^(and|or|the|a|an|other|其他|上一集道具)$/.test(item)));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return uniqueLimited(terms, 16);
|
|
148
|
+
}
|
|
149
|
+
|
|
65
150
|
function blockerFromPayload(payload = {}, source = "tool") {
|
|
66
151
|
if (!payload || typeof payload !== "object") return null;
|
|
67
152
|
if (!payload.blocked && payload.ok !== false && !payload.permissionAdvice) return null;
|
|
@@ -182,7 +267,7 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
|
|
|
182
267
|
if (textHas(text, /\b(artifact|canvas|pdf|image|video|screenshot|cover|plot|chart|figure|docx|archive|copy to|export|generated|generate|draft)\b/) || /输出|产物|图片|视频|截图|封面|生成/.test(text)) {
|
|
183
268
|
categories.add("artifact");
|
|
184
269
|
}
|
|
185
|
-
if (textHas(text, /\b(browser|chrome|chromium|cdp|devtools|playwright|selenium|web[- ]?ui|website|page|tab|composer|click|type|upload|attach|submit|form)\b/) ||
|
|
270
|
+
if (textHas(text, /\b(browser|chrome|chromium|cdp|devtools|playwright|selenium|web[- ]?ui|website|page|tab|composer|click|type|upload|attach|submit|form)\b/) || /浏览器|网页|页面|上传|提交|附件|资产库/.test(text)) {
|
|
186
271
|
categories.add("browser");
|
|
187
272
|
}
|
|
188
273
|
if (textHas(text, /\b(screenshot|visible|visual|see|inspect image|open image|read_image|thumbnail)\b/) || /截图|可见|缩略图/.test(text)) {
|
|
@@ -205,6 +290,10 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
|
|
|
205
290
|
function inferForbiddenActions(goal = "") {
|
|
206
291
|
const text = String(goal || "");
|
|
207
292
|
const forbidden = [];
|
|
293
|
+
const isAction = (value = "") =>
|
|
294
|
+
/\b(use|open|click|browse|browser|upload|attach|submit|publish|deploy|run|execute|install|delete|remove|commit|push|call|api)\b/i.test(
|
|
295
|
+
value
|
|
296
|
+
) || /浏览器|网页|打开|点击|上传|提交|发布|部署|运行|执行|安装|删除|复制|移动|提交代码|推送|调用|API/.test(value);
|
|
208
297
|
const patterns = [
|
|
209
298
|
{ re: /\b(do not|don't|dont|never|no need to|without)\s+([^.\n;]+)/gi, prefix: "User forbids" },
|
|
210
299
|
{ re: /不要([^。\n;]+)/g, prefix: "User forbids" },
|
|
@@ -212,7 +301,8 @@ function inferForbiddenActions(goal = "") {
|
|
|
212
301
|
];
|
|
213
302
|
for (const { re, prefix } of patterns) {
|
|
214
303
|
for (const match of text.matchAll(re)) {
|
|
215
|
-
|
|
304
|
+
const value = compact(match[2] || match[1], 160);
|
|
305
|
+
if (isAction(value)) forbidden.push(`${prefix}: ${value}`);
|
|
216
306
|
}
|
|
217
307
|
}
|
|
218
308
|
return unique(forbidden).slice(0, 8);
|
|
@@ -240,10 +330,78 @@ export function deriveScsTaskContract({ goal = "", taskProfile = "", acceptanceC
|
|
|
240
330
|
requiresExternalEvidence,
|
|
241
331
|
requiredEvidence,
|
|
242
332
|
forbiddenActions: inferForbiddenActions(goal),
|
|
333
|
+
exactOutputPaths: inferExactOutputPaths(goal),
|
|
334
|
+
requiredTextTerms: inferRequiredTextTerms(goal),
|
|
335
|
+
forbiddenTextTerms: inferForbiddenTextTerms(goal),
|
|
243
336
|
successCriteria: unique(acceptanceCriteria).slice(0, 10),
|
|
244
337
|
};
|
|
245
338
|
}
|
|
246
339
|
|
|
340
|
+
function resolveContractPath(commandCwd = process.cwd(), rawPath = "") {
|
|
341
|
+
const text = String(rawPath || "").trim();
|
|
342
|
+
if (!text) return "";
|
|
343
|
+
if (text.startsWith("~/")) return path.join(process.env.HOME || commandCwd, text.slice(2));
|
|
344
|
+
if (path.isAbsolute(text)) return text;
|
|
345
|
+
return path.resolve(commandCwd || process.cwd(), text);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function evaluateScsSemanticContract(contract = {}, { commandCwd = process.cwd() } = {}) {
|
|
349
|
+
const exactOutputPaths = Array.isArray(contract.exactOutputPaths) ? contract.exactOutputPaths : [];
|
|
350
|
+
const requiredTextTerms = Array.isArray(contract.requiredTextTerms) ? contract.requiredTextTerms : [];
|
|
351
|
+
const forbiddenTextTerms = Array.isArray(contract.forbiddenTextTerms) ? contract.forbiddenTextTerms : [];
|
|
352
|
+
if (!exactOutputPaths.length && !requiredTextTerms.length && !forbiddenTextTerms.length) {
|
|
353
|
+
return { ok: true, checked: false, reason: "No semantic file contract was inferred." };
|
|
354
|
+
}
|
|
355
|
+
if (!exactOutputPaths.length) {
|
|
356
|
+
return {
|
|
357
|
+
ok: true,
|
|
358
|
+
checked: false,
|
|
359
|
+
reason: "Semantic text terms were inferred, but no exact output path was inferred for deterministic file inspection.",
|
|
360
|
+
requiredTextTerms,
|
|
361
|
+
forbiddenTextTerms,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const files = exactOutputPaths.map((rawPath) => {
|
|
366
|
+
const absolutePath = resolveContractPath(commandCwd, rawPath);
|
|
367
|
+
try {
|
|
368
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
369
|
+
return { rawPath, absolutePath, exists: true, content };
|
|
370
|
+
} catch {
|
|
371
|
+
return { rawPath, absolutePath, exists: false, content: "" };
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
const missingFiles = files.filter((file) => !file.exists).map((file) => file.rawPath);
|
|
375
|
+
const combinedContent = files.map((file) => file.content).join("\n");
|
|
376
|
+
const missingRequiredText = requiredTextTerms.filter((term) => !combinedContent.includes(term));
|
|
377
|
+
const presentForbiddenText = forbiddenTextTerms.filter((term) => combinedContent.includes(term));
|
|
378
|
+
const ok = missingFiles.length === 0 && missingRequiredText.length === 0 && presentForbiddenText.length === 0;
|
|
379
|
+
return {
|
|
380
|
+
ok,
|
|
381
|
+
checked: true,
|
|
382
|
+
exactOutputPaths,
|
|
383
|
+
requiredTextTerms,
|
|
384
|
+
forbiddenTextTerms,
|
|
385
|
+
missingFiles,
|
|
386
|
+
missingRequiredText,
|
|
387
|
+
presentForbiddenText,
|
|
388
|
+
inspectedFiles: files.map((file) => ({
|
|
389
|
+
path: file.rawPath,
|
|
390
|
+
exists: file.exists,
|
|
391
|
+
chars: file.content.length,
|
|
392
|
+
})),
|
|
393
|
+
reason: ok
|
|
394
|
+
? "Exact output files satisfy inferred semantic hard constraints."
|
|
395
|
+
: [
|
|
396
|
+
missingFiles.length ? `Missing exact output files: ${missingFiles.join(", ")}` : "",
|
|
397
|
+
missingRequiredText.length ? `Missing required text terms: ${missingRequiredText.join(", ")}` : "",
|
|
398
|
+
presentForbiddenText.length ? `Forbidden text terms present: ${presentForbiddenText.join(", ")}` : "",
|
|
399
|
+
]
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
.join("; "),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
247
405
|
function eventToEvidence(event = {}) {
|
|
248
406
|
const type = String(event.type || "");
|
|
249
407
|
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
@@ -415,6 +573,9 @@ export function summarizeScsContractEvidence({ contract = {}, ledger = {}, evalu
|
|
|
415
573
|
description: item.description,
|
|
416
574
|
})),
|
|
417
575
|
forbiddenActions: contract.forbiddenActions || [],
|
|
576
|
+
exactOutputPaths: contract.exactOutputPaths || [],
|
|
577
|
+
requiredTextTerms: contract.requiredTextTerms || [],
|
|
578
|
+
forbiddenTextTerms: contract.forbiddenTextTerms || [],
|
|
418
579
|
successCriteria: contract.successCriteria || [],
|
|
419
580
|
},
|
|
420
581
|
evidenceLedger: {
|
package/src/tool-wrappers.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { getModelPresets } from "./model-routing.js";
|
|
3
6
|
import { redactSensitiveText } from "./redaction.js";
|
|
4
7
|
|
|
@@ -11,7 +14,7 @@ const BASE_ADVISORY_PROMPT = [
|
|
|
11
14
|
"Return concise findings, commands to consider, or an implementation plan.",
|
|
12
15
|
].join(" ");
|
|
13
16
|
|
|
14
|
-
function commandExists(command) {
|
|
17
|
+
export function commandExists(command) {
|
|
15
18
|
try {
|
|
16
19
|
if (process.platform === "win32") {
|
|
17
20
|
execFileSync("where", [command], { stdio: "ignore" });
|
|
@@ -225,6 +228,11 @@ export function listAgentWrappers() {
|
|
|
225
228
|
];
|
|
226
229
|
}
|
|
227
230
|
|
|
231
|
+
export function isWrapperAvailable(wrapper) {
|
|
232
|
+
const candidate = normalizeWrapperName(wrapper);
|
|
233
|
+
return Boolean(wrapperCommand(candidate, "", {}, {}) && commandExists(wrapperCommand(candidate, "", {}, {}).command));
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
export function wrapperStatusText() {
|
|
229
237
|
return listAgentWrappers()
|
|
230
238
|
.map((wrapper) => `${wrapper.name}:${wrapper.available ? "available" : "missing"}`)
|
|
@@ -284,3 +292,95 @@ export async function runAgentWrapper({ wrapper, prompt }, config) {
|
|
|
284
292
|
};
|
|
285
293
|
}
|
|
286
294
|
}
|
|
295
|
+
|
|
296
|
+
function codexImageArgs(prompt, imagePaths, config, preset, outputFile) {
|
|
297
|
+
const imageArgs = [];
|
|
298
|
+
for (const imagePath of imagePaths || []) {
|
|
299
|
+
imageArgs.push("--image", imagePath);
|
|
300
|
+
}
|
|
301
|
+
return [
|
|
302
|
+
"exec",
|
|
303
|
+
"--model",
|
|
304
|
+
preset.model,
|
|
305
|
+
"-c",
|
|
306
|
+
`model_reasoning_effort="${preset.reasoning}"`,
|
|
307
|
+
"--sandbox",
|
|
308
|
+
"read-only",
|
|
309
|
+
"--cd",
|
|
310
|
+
config.commandCwd,
|
|
311
|
+
"--skip-git-repo-check",
|
|
312
|
+
"--output-last-message",
|
|
313
|
+
outputFile,
|
|
314
|
+
...imageArgs,
|
|
315
|
+
"--",
|
|
316
|
+
buildPrompt(prompt),
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function runCodexImageWrapper({ prompt, imagePaths = [] }, config) {
|
|
321
|
+
if (!commandExists("codex")) {
|
|
322
|
+
return { ok: false, wrapper: "codex", error: "Codex CLI is not available on PATH." };
|
|
323
|
+
}
|
|
324
|
+
if (!Array.isArray(imagePaths) || imagePaths.length === 0) {
|
|
325
|
+
return { ok: false, wrapper: "codex", error: "At least one image file is required for Codex image reading." };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const presets = getModelPresets({
|
|
329
|
+
wrapperModel: config.wrapperModel,
|
|
330
|
+
wrapperReasoning: config.wrapperReasoning,
|
|
331
|
+
});
|
|
332
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "aginti-codex-image-"));
|
|
333
|
+
const runOnce = async (preset, label) => {
|
|
334
|
+
const outputFile = path.join(tempDir, `${label}.txt`);
|
|
335
|
+
const result = await runWrapperProcess(
|
|
336
|
+
{
|
|
337
|
+
command: "codex",
|
|
338
|
+
args: codexImageArgs(prompt, imagePaths, config, preset, outputFile),
|
|
339
|
+
},
|
|
340
|
+
config
|
|
341
|
+
);
|
|
342
|
+
const lastMessage = await fs.readFile(outputFile, "utf8").catch(() => "");
|
|
343
|
+
return {
|
|
344
|
+
stdout: cleanOutput(lastMessage || result.stdout, 24000),
|
|
345
|
+
stderr: cleanOutput(result.stderr, 4000),
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const result = await runOnce(presets.codexPrimary, "primary");
|
|
351
|
+
return {
|
|
352
|
+
ok: true,
|
|
353
|
+
wrapper: "codex",
|
|
354
|
+
model: presets.codexPrimary.model,
|
|
355
|
+
reasoning: presets.codexPrimary.reasoning,
|
|
356
|
+
stdout: result.stdout,
|
|
357
|
+
stderr: result.stderr,
|
|
358
|
+
};
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error?.name === "AbortError" || error?.code === "ABORT_ERR") throw error;
|
|
361
|
+
try {
|
|
362
|
+
const fallback = await runOnce(presets.codexSpare, "fallback");
|
|
363
|
+
return {
|
|
364
|
+
ok: true,
|
|
365
|
+
wrapper: "codex",
|
|
366
|
+
fallback: true,
|
|
367
|
+
model: presets.codexSpare.model,
|
|
368
|
+
reasoning: presets.codexSpare.reasoning,
|
|
369
|
+
stdout: fallback.stdout,
|
|
370
|
+
stderr: fallback.stderr,
|
|
371
|
+
};
|
|
372
|
+
} catch (fallbackError) {
|
|
373
|
+
if (fallbackError?.name === "AbortError" || fallbackError?.code === "ABORT_ERR") throw fallbackError;
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
wrapper: "codex",
|
|
377
|
+
error: redactSensitiveText(fallbackError instanceof Error ? fallbackError.message : String(fallbackError)),
|
|
378
|
+
primaryError: redactSensitiveText(error instanceof Error ? error.message : String(error)),
|
|
379
|
+
stdout: cleanOutput(fallbackError.stdout, 8000),
|
|
380
|
+
stderr: cleanOutput(fallbackError.stderr, 4000),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
} finally {
|
|
384
|
+
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
385
|
+
}
|
|
386
|
+
}
|
package/web.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
getProviderDefaults,
|
|
18
18
|
normalizeRoutingMode,
|
|
19
19
|
} from "./src/model-routing.js";
|
|
20
|
-
import { listAgentWrappers, normalizeWrapperName } from "./src/tool-wrappers.js";
|
|
20
|
+
import { isWrapperAvailable, listAgentWrappers, normalizeWrapperName } from "./src/tool-wrappers.js";
|
|
21
21
|
import { getDockerSandboxStatus, getSandboxLogs, runDockerPreflight } from "./src/docker-sandbox.js";
|
|
22
22
|
import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./src/command-policy.js";
|
|
23
23
|
import { summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./src/workspace-tools.js";
|
|
@@ -574,7 +574,11 @@ function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
|
|
|
574
574
|
? Math.min(Math.max(parsedParallelScoutCount, 1), 10)
|
|
575
575
|
: Number(current.parallelScoutCount) || 3,
|
|
576
576
|
allowWrapperTools:
|
|
577
|
-
typeof body.allowWrapperTools === "boolean"
|
|
577
|
+
typeof body.allowWrapperTools === "boolean"
|
|
578
|
+
? body.allowWrapperTools
|
|
579
|
+
: current.allowWrapperTools !== undefined
|
|
580
|
+
? Boolean(current.allowWrapperTools)
|
|
581
|
+
: isWrapperAvailable(normalizeWrapperName(body.preferredWrapper || current.preferredWrapper || "codex")),
|
|
578
582
|
preferredWrapper: normalizeWrapperName(body.preferredWrapper || current.preferredWrapper || "codex"),
|
|
579
583
|
wrapperTimeoutMs:
|
|
580
584
|
Number.isFinite(parsedWrapperTimeoutMs) && parsedWrapperTimeoutMs >= 10000
|