@lazyingart/agintiflow 0.20.195 → 0.20.196
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/smoke-auxiliary-tools.js +33 -0
- package/scripts/smoke-cli-chat.js +5 -0
- package/scripts/smoke-coding-tools.js +14 -0
- package/scripts/smoke-model-roles.js +49 -0
- package/src/auxiliary-tools.js +147 -3
- package/src/cli.js +25 -6
- package/src/project.js +10 -0
- package/src/scs-controller.js +5 -3
- package/src/scs-evidence.js +46 -3
- package/src/workspace-tools.js +9 -1
- package/src/writing-specialist.js +66 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.196",
|
|
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",
|
|
@@ -135,6 +135,7 @@
|
|
|
135
135
|
"dependencies": {
|
|
136
136
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
137
137
|
"express": "^5.1.0",
|
|
138
|
+
"fast-xml-parser": "^5.8.0",
|
|
138
139
|
"openai": "^6.3.0",
|
|
139
140
|
"playwright": "^1.55.0"
|
|
140
141
|
}
|
|
@@ -73,6 +73,31 @@ try {
|
|
|
73
73
|
await fs.access(path.join(workspace, "artifacts/images/dry-run/task_manifest.json"));
|
|
74
74
|
const payloadText = await fs.readFile(path.join(workspace, "artifacts/images/dry-run/request_payload.redacted.json"), "utf8");
|
|
75
75
|
assert(payloadText.includes("nano-banana-2"), "redacted image payload was not written");
|
|
76
|
+
const referencePng = Buffer.from(
|
|
77
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAYAAACJ7f8GAAAADElEQVR42mP8z8AARAAAIf4BfQKxMQAAAABJRU5ErkJggg==",
|
|
78
|
+
"base64"
|
|
79
|
+
);
|
|
80
|
+
await fs.mkdir(path.join(workspace, "refs"), { recursive: true });
|
|
81
|
+
await fs.writeFile(path.join(workspace, "refs/reference.png"), referencePng);
|
|
82
|
+
const referenceDryRun = await generateImage(
|
|
83
|
+
{
|
|
84
|
+
prompt: "Use the reference image geometry for a diagnostic dry run.",
|
|
85
|
+
outputDir: "artifacts/images/reference-dry-run",
|
|
86
|
+
outputStem: "reference",
|
|
87
|
+
referenceImages: ["refs/reference.png"],
|
|
88
|
+
matchReferenceSize: true,
|
|
89
|
+
dryRun: true,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
commandCwd: workspace,
|
|
93
|
+
allowFileTools: true,
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
assert(referenceDryRun.ok && /Reference-size matching/i.test(referenceDryRun.geometryNotice || ""), "reference dry run did not report geometry notice");
|
|
97
|
+
const referenceManifest = JSON.parse(await fs.readFile(path.join(workspace, "artifacts/images/reference-dry-run/task_manifest.json"), "utf8"));
|
|
98
|
+
assert(referenceManifest.referenceImages?.[0]?.dimensions?.width === 2, "reference manifest did not record source image width");
|
|
99
|
+
assert(referenceManifest.referenceImages?.[0]?.dimensions?.height === 3, "reference manifest did not record source image height");
|
|
100
|
+
assert(referenceManifest.matchReferenceSize === true, "reference manifest did not record matchReferenceSize");
|
|
76
101
|
const veniceDryRun = await generateImage(
|
|
77
102
|
{
|
|
78
103
|
provider: "venice",
|
|
@@ -139,6 +164,12 @@ try {
|
|
|
139
164
|
assert(cliImageResult.actualFormat === "png", "direct image CLI did not select PNG fallback");
|
|
140
165
|
assert(/raster PNG/i.test(cliImageResult.formatNotice || ""), "direct image CLI did not explain SVG-to-PNG fallback");
|
|
141
166
|
await fs.access(path.join(workspace, "artifacts/images/cli-svg-fallback/task_manifest.json"));
|
|
167
|
+
const cliImageHelp = await execFile(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "--no-auto-update", "image", "--help"], {
|
|
168
|
+
cwd: repoRoot,
|
|
169
|
+
env: { ...process.env, AGINTIFLOW_HOME: process.env.AGINTIFLOW_HOME },
|
|
170
|
+
});
|
|
171
|
+
assert(cliImageHelp.stdout.includes("--reference"), "aginti image --help did not show reference-image options");
|
|
172
|
+
assert(cliImageHelp.stdout.includes("--match-reference-size"), "aginti image --help did not show match-reference-size");
|
|
142
173
|
|
|
143
174
|
const blocked = await generateImage(
|
|
144
175
|
{
|
|
@@ -187,10 +218,12 @@ try {
|
|
|
187
218
|
"image_skill_listed",
|
|
188
219
|
"venice_image_skill_listed",
|
|
189
220
|
"generate_image_dry_run",
|
|
221
|
+
"reference_image_manifest_dimensions",
|
|
190
222
|
"auto_venice_when_grsai_missing",
|
|
191
223
|
"venice_generate_image_dry_run",
|
|
192
224
|
"svg_request_png_fallback",
|
|
193
225
|
"direct_image_cli_svg_request_png_fallback",
|
|
226
|
+
"direct_image_cli_subcommand_help",
|
|
194
227
|
"generate_image_guardrail",
|
|
195
228
|
"mock_agent_image_tool",
|
|
196
229
|
],
|
|
@@ -786,6 +786,10 @@ try {
|
|
|
786
786
|
if (!zhHelpResult.stdout.includes("命令:") || !zhHelpResult.stdout.includes("输入普通任务")) {
|
|
787
787
|
throw new Error("interactive --language zh-Hans did not localize CLI help");
|
|
788
788
|
}
|
|
789
|
+
const zhLeadingLanguageStatus = await runCli(["--language", "zh-Hans", "chat"], "/status\n/exit\n");
|
|
790
|
+
if (!zhLeadingLanguageStatus.stdout.includes("language=zh-Hans")) {
|
|
791
|
+
throw new Error("leading --language zh-Hans was stripped before interactive status");
|
|
792
|
+
}
|
|
789
793
|
const skillsResult = await runChat("/skills website\n/exit\n");
|
|
790
794
|
if (!skillsResult.stdout.includes("website-app") || !skillsResult.stdout.includes("Website And App Builder")) {
|
|
791
795
|
throw new Error("interactive /skills did not show matching built-in skills");
|
|
@@ -956,6 +960,7 @@ try {
|
|
|
956
960
|
"prompt-redraw-fast-path",
|
|
957
961
|
"committed-user-no-cwd-footer",
|
|
958
962
|
"cli-i18n",
|
|
963
|
+
"leading-language-option",
|
|
959
964
|
"user-prompt-label",
|
|
960
965
|
"escape-policy",
|
|
961
966
|
"composer-history-recall",
|
|
@@ -179,6 +179,19 @@ try {
|
|
|
179
179
|
assert(cdataSvgResult.ok === false, "CDATA-wrapped SVG write should be marked not ok");
|
|
180
180
|
assert(cdataSvgResult.artifactValidation?.kind === "svg", "SVG validation result missing");
|
|
181
181
|
assert(/CDATA/i.test(cdataSvgResult.artifactValidation.errors.join(" ")), "CDATA SVG validation did not explain the wrapper problem");
|
|
182
|
+
const invalidXmlSvgResult = await executeWorkspaceTool(
|
|
183
|
+
"write_file",
|
|
184
|
+
{
|
|
185
|
+
path: "figures/invalid-unescaped-text.svg",
|
|
186
|
+
content: '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="40"><text x="4" y="24">latency < 50 ms</text></svg>',
|
|
187
|
+
},
|
|
188
|
+
workspaceToolConfig
|
|
189
|
+
);
|
|
190
|
+
assert(invalidXmlSvgResult.ok === false, "SVG write with unescaped text '<' should be marked not ok");
|
|
191
|
+
assert(
|
|
192
|
+
/XML parser rejected/i.test(invalidXmlSvgResult.artifactValidation?.errors?.join(" ") || ""),
|
|
193
|
+
"invalid SVG XML was not rejected by the XML parser"
|
|
194
|
+
);
|
|
182
195
|
const validSvgResult = await executeWorkspaceTool(
|
|
183
196
|
"write_file",
|
|
184
197
|
{
|
|
@@ -1265,6 +1278,7 @@ try {
|
|
|
1265
1278
|
"resume_runtime_time_context",
|
|
1266
1279
|
"virtual_workspace_path",
|
|
1267
1280
|
"svg_cdata_validation_failure",
|
|
1281
|
+
"svg_unescaped_text_xml_validation_failure",
|
|
1268
1282
|
"svg_standalone_validation_pass",
|
|
1269
1283
|
"apply_patch",
|
|
1270
1284
|
"multi_file_patch",
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import { buildScsEvidenceLedger, deriveScsTaskContract, evaluateScsEvidence } from "../src/scs-evidence.js";
|
|
25
25
|
import { resolveRuntimeConfig } from "../src/config.js";
|
|
26
26
|
import { classifyGoalIntent, isDirectAnswerIntent } from "../src/goal-intent.js";
|
|
27
|
+
import { languageWriterDefaults } from "../src/writing-specialist.js";
|
|
27
28
|
|
|
28
29
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
30
|
|
|
@@ -111,6 +112,10 @@ const envSnapshot = {
|
|
|
111
112
|
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL,
|
|
112
113
|
LLM_BASE_URL: process.env.LLM_BASE_URL,
|
|
113
114
|
AGINTI_MAIN_REASONING: process.env.AGINTI_MAIN_REASONING,
|
|
115
|
+
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
|
116
|
+
DEEPSEEK_API_KEY: process.env.DEEPSEEK_API_KEY,
|
|
117
|
+
AGINTI_WRITING_PROVIDER_ZH: process.env.AGINTI_WRITING_PROVIDER_ZH,
|
|
118
|
+
AGINTI_WRITING_MODEL_ZH: process.env.AGINTI_WRITING_MODEL_ZH,
|
|
114
119
|
};
|
|
115
120
|
process.env.OPENAI_BASE_URL = "https://openai-compatible.example/v1";
|
|
116
121
|
process.env.LLM_BASE_URL = "https://generic-compatible.example/v1";
|
|
@@ -123,6 +128,18 @@ process.env.AGINTI_MAIN_REASONING = "none";
|
|
|
123
128
|
assert(getModelRoleDefaults().main.reasoning === "", "none main reasoning should normalize to omitted reasoning");
|
|
124
129
|
assert(normalizeReasoningEffort("min") === "minimal", "min reasoning alias should normalize to minimal");
|
|
125
130
|
assert(normalizeReasoningEffort("extra-high") === "xhigh", "extra-high reasoning alias should normalize to xhigh");
|
|
131
|
+
process.env.DEEPSEEK_API_KEY = "test-deepseek-key";
|
|
132
|
+
process.env.OPENAI_API_KEY = "test-openai-key";
|
|
133
|
+
delete process.env.AGINTI_WRITING_PROVIDER_ZH;
|
|
134
|
+
delete process.env.AGINTI_WRITING_MODEL_ZH;
|
|
135
|
+
const zhWriterRoute = languageWriterDefaults({ language: "zh-Hans", writingBrief: "写一段小说。" }, { provider: "mock", model: "mock-agent" });
|
|
136
|
+
assert(zhWriterRoute.provider === "deepseek" && zhWriterRoute.model === "deepseek-v4-pro", "Chinese writing should default to DeepSeek Pro when available");
|
|
137
|
+
const enWriterRoute = languageWriterDefaults({ language: "en", writingBrief: "Write a scene." }, { provider: "mock", model: "mock-agent" });
|
|
138
|
+
assert(enWriterRoute.provider === "openai" && enWriterRoute.model, "English writing should default to OpenAI when available");
|
|
139
|
+
process.env.AGINTI_WRITING_PROVIDER_ZH = "qwen";
|
|
140
|
+
process.env.AGINTI_WRITING_MODEL_ZH = "qwen-test-writer";
|
|
141
|
+
const zhEnvWriterRoute = languageWriterDefaults({ language: "zh-Hans", writingBrief: "写一段小说。" }, { provider: "mock", model: "mock-agent" });
|
|
142
|
+
assert(zhEnvWriterRoute.provider === "qwen" && zhEnvWriterRoute.model === "qwen-test-writer", "language-specific writer env should override auto routing");
|
|
126
143
|
for (const [key, value] of Object.entries(envSnapshot)) {
|
|
127
144
|
if (value === undefined) delete process.env[key];
|
|
128
145
|
else process.env[key] = value;
|
|
@@ -578,6 +595,10 @@ const jsonObjectContract = deriveScsTaskContract({
|
|
|
578
595
|
const virtualFileContract = deriveScsTaskContract({
|
|
579
596
|
goal: "Create file: /workspace/virtual-output.txt with virtual Docker path support.",
|
|
580
597
|
});
|
|
598
|
+
const requiredWriterToolContract = deriveScsTaskContract({
|
|
599
|
+
goal: "Call writing_specialist again and create final/story.md.",
|
|
600
|
+
taskProfile: "writing",
|
|
601
|
+
});
|
|
581
602
|
const outputListContract = deriveScsTaskContract({
|
|
582
603
|
goal: [
|
|
583
604
|
"Create:",
|
|
@@ -647,6 +668,10 @@ assert(
|
|
|
647
668
|
!virtualFileContract.requiredEvidence.some((item) => item.category === "artifact"),
|
|
648
669
|
"virtual output filename should require file evidence without treating output in the filename as an artifact"
|
|
649
670
|
);
|
|
671
|
+
assert(
|
|
672
|
+
requiredWriterToolContract.requiredToolCalls.includes("writing_specialist"),
|
|
673
|
+
"SCS should infer explicitly required specialist tool calls"
|
|
674
|
+
);
|
|
650
675
|
assert(
|
|
651
676
|
outputListContract.exactOutputPaths.includes("work/demo/generate_items.py") &&
|
|
652
677
|
outputListContract.exactOutputPaths.includes("work/demo/review_items.py") &&
|
|
@@ -691,6 +716,28 @@ const checkedCodeEval = evaluateScsEvidence(
|
|
|
691
716
|
})
|
|
692
717
|
);
|
|
693
718
|
assert(checkedCodeEval.ok, "code task should finish when file and command evidence are both present");
|
|
719
|
+
const missingWriterToolEval = evaluateScsEvidence(
|
|
720
|
+
requiredWriterToolContract,
|
|
721
|
+
buildScsEvidenceLedger({
|
|
722
|
+
context: { events: [{ type: "file.changed", data: { path: "final/story.md" } }] },
|
|
723
|
+
})
|
|
724
|
+
);
|
|
725
|
+
assert(
|
|
726
|
+
!missingWriterToolEval.ok && missingWriterToolEval.missingToolCalls.includes("writing_specialist"),
|
|
727
|
+
"SCS should reject finish when required specialist call is missing"
|
|
728
|
+
);
|
|
729
|
+
const presentWriterToolEval = evaluateScsEvidence(
|
|
730
|
+
requiredWriterToolContract,
|
|
731
|
+
buildScsEvidenceLedger({
|
|
732
|
+
context: {
|
|
733
|
+
events: [
|
|
734
|
+
{ type: "file.changed", data: { path: "final/story.md" } },
|
|
735
|
+
{ type: "tool.completed", data: { toolName: "writing_specialist", ok: true, artifactPath: "artifacts/writer.json" } },
|
|
736
|
+
],
|
|
737
|
+
},
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
assert(presentWriterToolEval.ok, "SCS should accept required specialist call when tool evidence is present");
|
|
694
741
|
|
|
695
742
|
const blockedFileFinish = await reviewScsFinish(
|
|
696
743
|
{ mock: true },
|
|
@@ -740,6 +787,7 @@ console.log(
|
|
|
740
787
|
"role-defaults",
|
|
741
788
|
"openai-base-url",
|
|
742
789
|
"provider-default-reasoning",
|
|
790
|
+
"writing-specialist-language-routing",
|
|
743
791
|
"openai-chat-reasoning-payload",
|
|
744
792
|
"goal-intent-direct-answer",
|
|
745
793
|
"route-overrides",
|
|
@@ -754,6 +802,7 @@ console.log(
|
|
|
754
802
|
"scs-student-validator-replan",
|
|
755
803
|
"scs-evidence-stdout",
|
|
756
804
|
"scs-contract-evidence-ledger",
|
|
805
|
+
"scs-required-tool-call-contract",
|
|
757
806
|
"cli-models-command",
|
|
758
807
|
"venice-shortcut",
|
|
759
808
|
],
|
package/src/auxiliary-tools.js
CHANGED
|
@@ -130,6 +130,66 @@ function mimeFromPath(filePath) {
|
|
|
130
130
|
return "image/png";
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function imageDimensionsFromBuffer(buffer) {
|
|
134
|
+
if (!Buffer.isBuffer(buffer) || buffer.length < 12) return null;
|
|
135
|
+
|
|
136
|
+
if (buffer.length >= 24 && buffer.readUInt32BE(0) === 0x89504e47 && buffer.toString("ascii", 12, 16) === "IHDR") {
|
|
137
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20), format: "png" };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (buffer.toString("ascii", 0, 3) === "GIF" && buffer.length >= 10) {
|
|
141
|
+
return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8), format: "gif" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
145
|
+
let offset = 2;
|
|
146
|
+
while (offset + 9 < buffer.length) {
|
|
147
|
+
while (buffer[offset] === 0xff) offset += 1;
|
|
148
|
+
const marker = buffer[offset];
|
|
149
|
+
offset += 1;
|
|
150
|
+
if ([0xd8, 0xd9, 0x01].includes(marker) || (marker >= 0xd0 && marker <= 0xd7)) continue;
|
|
151
|
+
if (offset + 2 > buffer.length) break;
|
|
152
|
+
const length = buffer.readUInt16BE(offset);
|
|
153
|
+
if (length < 2 || offset + length > buffer.length) break;
|
|
154
|
+
if (
|
|
155
|
+
[0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker) &&
|
|
156
|
+
offset + 7 < buffer.length
|
|
157
|
+
) {
|
|
158
|
+
return { width: buffer.readUInt16BE(offset + 5), height: buffer.readUInt16BE(offset + 3), format: "jpeg" };
|
|
159
|
+
}
|
|
160
|
+
offset += length;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
165
|
+
let offset = 12;
|
|
166
|
+
while (offset + 8 <= buffer.length) {
|
|
167
|
+
const chunk = buffer.toString("ascii", offset, offset + 4);
|
|
168
|
+
const size = buffer.readUInt32LE(offset + 4);
|
|
169
|
+
const data = offset + 8;
|
|
170
|
+
if (chunk === "VP8X" && data + 10 <= buffer.length) {
|
|
171
|
+
const width = 1 + buffer.readUIntLE(data + 4, 3);
|
|
172
|
+
const height = 1 + buffer.readUIntLE(data + 7, 3);
|
|
173
|
+
return { width, height, format: "webp" };
|
|
174
|
+
}
|
|
175
|
+
if (chunk === "VP8 " && data + 10 <= buffer.length && buffer[data + 3] === 0x9d && buffer[data + 4] === 0x01 && buffer[data + 5] === 0x2a) {
|
|
176
|
+
return {
|
|
177
|
+
width: buffer.readUInt16LE(data + 6) & 0x3fff,
|
|
178
|
+
height: buffer.readUInt16LE(data + 8) & 0x3fff,
|
|
179
|
+
format: "webp",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
offset += 8 + size + (size % 2);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function dimensionsEqual(a, b) {
|
|
190
|
+
return Boolean(a && b && Number(a.width) === Number(b.width) && Number(a.height) === Number(b.height));
|
|
191
|
+
}
|
|
192
|
+
|
|
133
193
|
function parseJsonResponse(raw) {
|
|
134
194
|
try {
|
|
135
195
|
const parsed = JSON.parse(raw);
|
|
@@ -163,6 +223,33 @@ function redactPayload(payload) {
|
|
|
163
223
|
return copy;
|
|
164
224
|
}
|
|
165
225
|
|
|
226
|
+
function extractFailureReason(payload = {}) {
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
const reasons = [];
|
|
229
|
+
const interesting = /^(?:failure_reason|failureReason|fail_reason|failReason|reason|error|error_message|errorMessage|message|msg|detail|details|code|status)$/i;
|
|
230
|
+
const walk = (value, depth = 0, key = "") => {
|
|
231
|
+
if (value === null || value === undefined || depth > 4 || reasons.length >= 12) return;
|
|
232
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
233
|
+
if (!interesting.test(key)) return;
|
|
234
|
+
const text = redactSensitiveText(String(value)).replace(/\s+/g, " ").trim();
|
|
235
|
+
if (text && !seen.has(`${key}:${text}`)) {
|
|
236
|
+
seen.add(`${key}:${text}`);
|
|
237
|
+
reasons.push(`${key}: ${text}`);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (Array.isArray(value)) {
|
|
242
|
+
value.slice(0, 8).forEach((item) => walk(item, depth + 1, key));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (typeof value === "object") {
|
|
246
|
+
for (const [childKey, childValue] of Object.entries(value)) walk(childValue, depth + 1, childKey);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
walk(payload);
|
|
250
|
+
return reasons.join("; ").slice(0, 1200) || "";
|
|
251
|
+
}
|
|
252
|
+
|
|
166
253
|
function grsaiKey() {
|
|
167
254
|
return String(process.env.GRSAI || process.env.GRSAI_API_KEY || "").trim();
|
|
168
255
|
}
|
|
@@ -181,8 +268,18 @@ export function listAuxiliarySkills() {
|
|
|
181
268
|
async function referenceToUrl(item, config) {
|
|
182
269
|
const value = String(item || "").trim();
|
|
183
270
|
if (!value) return { url: "", redacted: "" };
|
|
184
|
-
if (/^https?:\/\//i.test(value)) return { url: value, redacted: value };
|
|
185
|
-
if (value.startsWith("data:"))
|
|
271
|
+
if (/^https?:\/\//i.test(value)) return { url: value, redacted: value, source: "url", dimensions: null };
|
|
272
|
+
if (value.startsWith("data:")) {
|
|
273
|
+
const match = value.match(/^data:([^;,]+);base64,([\s\S]+)$/i);
|
|
274
|
+
const buffer = match ? Buffer.from(match[2], "base64") : null;
|
|
275
|
+
return {
|
|
276
|
+
url: value,
|
|
277
|
+
redacted: `<data-uri length=${value.length}>`,
|
|
278
|
+
source: "data-uri",
|
|
279
|
+
bytes: buffer?.length || 0,
|
|
280
|
+
dimensions: buffer ? imageDimensionsFromBuffer(buffer) : null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
186
283
|
|
|
187
284
|
const target = resolveWorkspacePath(config, value);
|
|
188
285
|
const stat = await fs.stat(target.absolutePath);
|
|
@@ -192,6 +289,10 @@ async function referenceToUrl(item, config) {
|
|
|
192
289
|
return {
|
|
193
290
|
url: `data:${mimeFromPath(target.relativePath)};base64,${buffer.toString("base64")}`,
|
|
194
291
|
redacted: `<${target.relativePath} data-uri bytes=${buffer.length}>`,
|
|
292
|
+
source: "workspace-file",
|
|
293
|
+
path: target.relativePath,
|
|
294
|
+
bytes: buffer.length,
|
|
295
|
+
dimensions: imageDimensionsFromBuffer(buffer),
|
|
195
296
|
};
|
|
196
297
|
}
|
|
197
298
|
|
|
@@ -302,6 +403,7 @@ async function downloadImage(url, destination, signal = null) {
|
|
|
302
403
|
return {
|
|
303
404
|
bytes: buffer.length,
|
|
304
405
|
sha256: hashBuffer(buffer),
|
|
406
|
+
dimensions: imageDimensionsFromBuffer(buffer),
|
|
305
407
|
};
|
|
306
408
|
}
|
|
307
409
|
|
|
@@ -313,6 +415,7 @@ async function writeBase64Image(image, destination) {
|
|
|
313
415
|
return {
|
|
314
416
|
bytes: buffer.length,
|
|
315
417
|
sha256: hashBuffer(buffer),
|
|
418
|
+
dimensions: imageDimensionsFromBuffer(buffer),
|
|
316
419
|
};
|
|
317
420
|
}
|
|
318
421
|
|
|
@@ -401,6 +504,12 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
|
|
|
401
504
|
imagePaths.push(relativePath);
|
|
402
505
|
downloads.push({ path: relativePath, ...info });
|
|
403
506
|
}
|
|
507
|
+
const referenceDimensions = manifest.referenceDimensions || null;
|
|
508
|
+
const matchReferenceSize = Boolean(manifest.matchReferenceSize);
|
|
509
|
+
const geometryMismatch =
|
|
510
|
+
matchReferenceSize && referenceDimensions
|
|
511
|
+
? downloads.find((item) => item.dimensions && !dimensionsEqual(item.dimensions, referenceDimensions))
|
|
512
|
+
: null;
|
|
404
513
|
|
|
405
514
|
manifest.provider = "venice";
|
|
406
515
|
manifest.host = base;
|
|
@@ -411,6 +520,9 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
|
|
|
411
520
|
manifest.status = "succeeded";
|
|
412
521
|
manifest.finishedAt = new Date().toISOString();
|
|
413
522
|
manifest.downloadedFiles = downloads;
|
|
523
|
+
if (geometryMismatch) {
|
|
524
|
+
manifest.geometryNotice = `Reference size requested (${referenceDimensions.width}x${referenceDimensions.height}), but generated output ${geometryMismatch.path} is ${geometryMismatch.dimensions.width}x${geometryMismatch.dimensions.height}.`;
|
|
525
|
+
}
|
|
414
526
|
if (resultPayload.id) manifest.taskId = String(resultPayload.id);
|
|
415
527
|
await writeJson(manifestPath, manifest);
|
|
416
528
|
|
|
@@ -427,6 +539,7 @@ async function generateVeniceImages({ prompt, args, target, outputStem, manifest
|
|
|
427
539
|
requestedFormat: formatInfo.requestedFormat,
|
|
428
540
|
actualFormat: format,
|
|
429
541
|
formatNotice: formatInfo.notice,
|
|
542
|
+
geometryNotice: manifest.geometryNotice || "",
|
|
430
543
|
summary: `${imagePaths.length} image(s) generated through Venice${formatInfo.notice ? ` (${formatInfo.notice})` : ""}`,
|
|
431
544
|
};
|
|
432
545
|
}
|
|
@@ -456,6 +569,15 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
456
569
|
const converted = await referenceToUrl(item, config);
|
|
457
570
|
if (converted.url) references.push(converted);
|
|
458
571
|
}
|
|
572
|
+
const referenceMetadata = references.map((item) => ({
|
|
573
|
+
source: item.source || "",
|
|
574
|
+
path: item.path || "",
|
|
575
|
+
redacted: item.redacted || "",
|
|
576
|
+
bytes: item.bytes || 0,
|
|
577
|
+
dimensions: item.dimensions || null,
|
|
578
|
+
}));
|
|
579
|
+
const referenceDimensions = references.find((item) => item.dimensions)?.dimensions || null;
|
|
580
|
+
const matchReferenceSize = Boolean(args.matchReferenceSize || args.match_ref_size || args.matchRefSize);
|
|
459
581
|
|
|
460
582
|
const provider = args.provider ? normalizeImageProvider(args.provider) : defaultImageProvider(config);
|
|
461
583
|
const formatInfo = normalizeOutputFormat(args);
|
|
@@ -479,6 +601,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
479
601
|
|
|
480
602
|
const redactedPayload = redactPayload(payload);
|
|
481
603
|
redactedPayload.urls = references.map((item) => item.redacted);
|
|
604
|
+
if (matchReferenceSize) redactedPayload.matchReferenceSize = true;
|
|
482
605
|
const promptPath = path.join(target.absolutePath, "prompt.txt");
|
|
483
606
|
const requestPath = path.join(target.absolutePath, "request_payload.redacted.json");
|
|
484
607
|
const manifestPath = path.join(target.absolutePath, "task_manifest.json");
|
|
@@ -496,9 +619,18 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
496
619
|
outputDir: target.relativePath,
|
|
497
620
|
promptFile: path.posix.join(target.relativePath, "prompt.txt"),
|
|
498
621
|
requestPayloadRedacted: path.posix.join(target.relativePath, "request_payload.redacted.json"),
|
|
622
|
+
referenceImages: referenceMetadata,
|
|
623
|
+
referenceDimensions,
|
|
624
|
+
matchReferenceSize,
|
|
499
625
|
status: args.dryRun ? "prepared" : "started",
|
|
500
626
|
createdAt: new Date().toISOString(),
|
|
501
627
|
};
|
|
628
|
+
if (matchReferenceSize && !referenceDimensions) {
|
|
629
|
+
manifest.geometryNotice = "Reference-size matching was requested, but no local reference image dimensions could be detected.";
|
|
630
|
+
} else if (matchReferenceSize) {
|
|
631
|
+
manifest.geometryNotice =
|
|
632
|
+
"Reference-size matching was requested. AgInTiFlow records and checks output dimensions, but the current image backends may not preserve exact source geometry.";
|
|
633
|
+
}
|
|
502
634
|
await writeJson(manifestPath, manifest);
|
|
503
635
|
|
|
504
636
|
if (args.dryRun) {
|
|
@@ -515,6 +647,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
515
647
|
requestedFormat: formatInfo.requestedFormat,
|
|
516
648
|
actualFormat: formatInfo.actualFormat,
|
|
517
649
|
formatNotice: formatInfo.notice,
|
|
650
|
+
geometryNotice: manifest.geometryNotice || "",
|
|
518
651
|
summary: `Prepared redacted image-generation payload without calling the provider.${formatInfo.notice ? ` ${formatInfo.notice}` : ""}`,
|
|
519
652
|
};
|
|
520
653
|
}
|
|
@@ -568,10 +701,13 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
568
701
|
|
|
569
702
|
const status = resultPayload.status || resultPayload?.data?.status;
|
|
570
703
|
if (status !== "succeeded") {
|
|
704
|
+
const failureReason = extractFailureReason(resultPayload) || `status: ${status || "unknown"}`;
|
|
571
705
|
manifest.status = status || "failed";
|
|
706
|
+
manifest.failureReason = failureReason;
|
|
707
|
+
manifest.resultResponse = path.posix.join(target.relativePath, "result_response.json");
|
|
572
708
|
manifest.finishedAt = new Date().toISOString();
|
|
573
709
|
await writeJson(manifestPath, manifest);
|
|
574
|
-
throw new Error(`Image generation failed with status ${status || "unknown"}. See ${manifestPath}.`);
|
|
710
|
+
throw new Error(`Image generation failed with status ${status || "unknown"}: ${failureReason}. See ${manifestPath}.`);
|
|
575
711
|
}
|
|
576
712
|
|
|
577
713
|
const urls = resultUrls(resultPayload);
|
|
@@ -588,6 +724,10 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
588
724
|
imagePaths.push(relativePath);
|
|
589
725
|
downloads.push({ path: relativePath, ...info });
|
|
590
726
|
}
|
|
727
|
+
const geometryMismatch =
|
|
728
|
+
matchReferenceSize && referenceDimensions
|
|
729
|
+
? downloads.find((item) => item.dimensions && !dimensionsEqual(item.dimensions, referenceDimensions))
|
|
730
|
+
: null;
|
|
591
731
|
|
|
592
732
|
manifest.status = "succeeded";
|
|
593
733
|
manifest.requestedFormat = formatInfo.requestedFormat;
|
|
@@ -595,6 +735,9 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
595
735
|
if (formatInfo.notice) manifest.formatNotice = formatInfo.notice;
|
|
596
736
|
manifest.finishedAt = new Date().toISOString();
|
|
597
737
|
manifest.downloadedFiles = downloads;
|
|
738
|
+
if (geometryMismatch) {
|
|
739
|
+
manifest.geometryNotice = `Reference size requested (${referenceDimensions.width}x${referenceDimensions.height}), but generated output ${geometryMismatch.path} is ${geometryMismatch.dimensions.width}x${geometryMismatch.dimensions.height}.`;
|
|
740
|
+
}
|
|
598
741
|
await writeJson(manifestPath, manifest);
|
|
599
742
|
|
|
600
743
|
return {
|
|
@@ -609,6 +752,7 @@ export async function generateImage(args = {}, config = {}) {
|
|
|
609
752
|
requestedFormat: formatInfo.requestedFormat,
|
|
610
753
|
actualFormat: manifest.actualFormat,
|
|
611
754
|
formatNotice: formatInfo.notice,
|
|
755
|
+
geometryNotice: manifest.geometryNotice || "",
|
|
612
756
|
summary: `${imagePaths.length} image(s) generated${formatInfo.notice ? ` (${formatInfo.notice})` : ""}`,
|
|
613
757
|
};
|
|
614
758
|
}
|
package/src/cli.js
CHANGED
|
@@ -825,6 +825,7 @@ function parseImageCommandArgs(argv = []) {
|
|
|
825
825
|
imageSize: "",
|
|
826
826
|
host: "",
|
|
827
827
|
referenceImages: [],
|
|
828
|
+
matchReferenceSize: false,
|
|
828
829
|
commandCwd: "",
|
|
829
830
|
requestTimeoutMs: "",
|
|
830
831
|
pollTimeoutMs: "",
|
|
@@ -861,6 +862,11 @@ function parseImageCommandArgs(argv = []) {
|
|
|
861
862
|
index += 1;
|
|
862
863
|
continue;
|
|
863
864
|
}
|
|
865
|
+
if (arg === "--match-reference-size" || arg === "--match-ref-size") {
|
|
866
|
+
result.matchReferenceSize = true;
|
|
867
|
+
index += 1;
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
864
870
|
if (arg === "--stdin") {
|
|
865
871
|
result.stdin = true;
|
|
866
872
|
index += 1;
|
|
@@ -919,9 +925,10 @@ function parseImageCommandArgs(argv = []) {
|
|
|
919
925
|
|
|
920
926
|
function printImageCommandUsage() {
|
|
921
927
|
console.log(
|
|
922
|
-
'Usage: aginti image [generate] [--json] [--dry-run] [--provider grsai|venice] [--model MODEL] [--format png|webp|svg] [--output-dir DIR] [--output-stem STEM] [--aspect-ratio 1:1] [--image-size 1K|2K|4K|1024x1024] [--reference path-or-url] "prompt"'
|
|
928
|
+
'Usage: aginti image [generate] [--json] [--dry-run] [--provider grsai|venice] [--model MODEL] [--format png|webp|svg] [--output-dir DIR] [--output-stem STEM] [--aspect-ratio 1:1] [--image-size 1K|2K|4K|1024x1024] [--reference path-or-url] [--match-reference-size] "prompt"'
|
|
923
929
|
);
|
|
924
930
|
console.log("Direct image CLI calls the same generate_image tool as the web API. SVG/vector requests return PNG with requestedFormat/actualFormat/formatNotice.");
|
|
931
|
+
console.log("--reference accepts local paths, URLs, or data URIs. Diagnostics record reference/output dimensions when available.");
|
|
925
932
|
console.log('Agent-mediated image work is still available as: aginti --image "draw a poster"');
|
|
926
933
|
}
|
|
927
934
|
|
|
@@ -946,11 +953,13 @@ function printImageCommandResult(result = {}) {
|
|
|
946
953
|
console.log(`format: ${requested}${requested === actual ? "" : ` -> ${actual}`}`);
|
|
947
954
|
}
|
|
948
955
|
if (result.formatNotice) console.log(`notice: ${result.formatNotice}`);
|
|
956
|
+
if (result.geometryNotice) console.log(`geometry: ${result.geometryNotice}`);
|
|
949
957
|
if (result.path) console.log(`path: ${result.path}`);
|
|
950
958
|
if (result.imagePaths?.length) console.log(`images: ${result.imagePaths.join(", ")}`);
|
|
951
959
|
if (result.manifestPath) console.log(`manifest: ${result.manifestPath}`);
|
|
952
960
|
if (result.promptPath) console.log(`prompt: ${result.promptPath}`);
|
|
953
961
|
if (result.requestPayloadPath) console.log(`request: ${result.requestPayloadPath}`);
|
|
962
|
+
if (result.failureReason) console.log(`failure: ${result.failureReason}`);
|
|
954
963
|
if (result.reason) console.log(`reason: ${result.reason}`);
|
|
955
964
|
}
|
|
956
965
|
|
|
@@ -994,6 +1003,7 @@ async function handleImageCommand(argv, { commandCwd = process.cwd() } = {}) {
|
|
|
994
1003
|
imageSize: parsed.imageSize || undefined,
|
|
995
1004
|
host: parsed.host || undefined,
|
|
996
1005
|
referenceImages: parsed.referenceImages,
|
|
1006
|
+
matchReferenceSize: parsed.matchReferenceSize,
|
|
997
1007
|
requestTimeoutMs: parsed.requestTimeoutMs || undefined,
|
|
998
1008
|
pollTimeoutMs: parsed.pollTimeoutMs || undefined,
|
|
999
1009
|
pollIntervalMs: parsed.pollIntervalMs || undefined,
|
|
@@ -1107,6 +1117,7 @@ function stripLeadingGlobalOptions(argv = []) {
|
|
|
1107
1117
|
let index = 0;
|
|
1108
1118
|
const options = {
|
|
1109
1119
|
commandCwd: "",
|
|
1120
|
+
language: "",
|
|
1110
1121
|
};
|
|
1111
1122
|
while (index < argv.length) {
|
|
1112
1123
|
const arg = argv[index];
|
|
@@ -1115,7 +1126,15 @@ function stripLeadingGlobalOptions(argv = []) {
|
|
|
1115
1126
|
continue;
|
|
1116
1127
|
}
|
|
1117
1128
|
if (arg === "--language" || arg === "--lang" || arg === "-L") {
|
|
1118
|
-
|
|
1129
|
+
const first = readOption(argv, index);
|
|
1130
|
+
const second = argv[index + 2] && !String(argv[index + 2]).startsWith("--") ? argv[index + 2] : "";
|
|
1131
|
+
if (["cn", "zh"].includes(String(first || "").toLowerCase()) && ["s", "t"].includes(String(second || "").toLowerCase())) {
|
|
1132
|
+
options.language = resolveLanguage(`${first}-${second}`);
|
|
1133
|
+
index += 3;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
options.language = resolveLanguage(first);
|
|
1137
|
+
index += first ? 2 : 1;
|
|
1119
1138
|
continue;
|
|
1120
1139
|
}
|
|
1121
1140
|
if (arg === "--cwd") {
|
|
@@ -1992,7 +2011,7 @@ async function handleStorageCommand(argv) {
|
|
|
1992
2011
|
}
|
|
1993
2012
|
|
|
1994
2013
|
export async function main(argv = process.argv.slice(2)) {
|
|
1995
|
-
if (argv[0] === "help" || argv
|
|
2014
|
+
if (argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
|
|
1996
2015
|
printUsage();
|
|
1997
2016
|
return;
|
|
1998
2017
|
}
|
|
@@ -2268,7 +2287,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2268
2287
|
const parsedResumeOptions = parseArgs(resumeOptions.optionArgv);
|
|
2269
2288
|
exitOnUnknownOptions(parsedResumeOptions);
|
|
2270
2289
|
const webLaunch = await maybeEnsureDefaultWebApp(parsedResumeOptions, { commandCwd });
|
|
2271
|
-
await startInteractiveCli(agentDefaults({ ...parsedResumeOptions, resume: sessionId, commandCwd: parsedResumeOptions.commandCwd || commandCwd }), {
|
|
2290
|
+
await startInteractiveCli(agentDefaults({ ...parsedResumeOptions, language: parsedResumeOptions.language || stripped.options.language, resume: sessionId, commandCwd: parsedResumeOptions.commandCwd || commandCwd }), {
|
|
2272
2291
|
packageDir,
|
|
2273
2292
|
packageVersion: packageJson.version,
|
|
2274
2293
|
webAppUrl: webLaunch.ok ? webLaunch.url : "",
|
|
@@ -2278,7 +2297,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2278
2297
|
}
|
|
2279
2298
|
const parsedResumeArgs = parseArgs([...resumeOptions.optionArgv, prompt]);
|
|
2280
2299
|
exitOnUnknownOptions(parsedResumeArgs);
|
|
2281
|
-
const resumeArgs = agentDefaults({ ...parsedResumeArgs, resume: sessionId, goal: prompt, commandCwd: parsedResumeArgs.commandCwd || commandCwd });
|
|
2300
|
+
const resumeArgs = agentDefaults({ ...parsedResumeArgs, language: parsedResumeArgs.language || stripped.options.language, resume: sessionId, goal: prompt, commandCwd: parsedResumeArgs.commandCwd || commandCwd });
|
|
2282
2301
|
if (!(await ensureDeepSeekKeyForOneShot(resumeArgs))) process.exit(1);
|
|
2283
2302
|
await maybeEnsureDefaultWebApp(resumeArgs, { commandCwd });
|
|
2284
2303
|
const config = loadConfig(resumeArgs, { packageDir });
|
|
@@ -2287,7 +2306,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
2287
2306
|
}
|
|
2288
2307
|
|
|
2289
2308
|
const parsedArgs = parseArgs(commandArgv);
|
|
2290
|
-
const args = { ...parsedArgs, commandCwd: parsedArgs.commandCwd || commandCwd };
|
|
2309
|
+
const args = { ...parsedArgs, language: parsedArgs.language || stripped.options.language, commandCwd: parsedArgs.commandCwd || commandCwd };
|
|
2291
2310
|
exitOnUnknownOptions(args);
|
|
2292
2311
|
|
|
2293
2312
|
if (args.webapp) {
|
package/src/project.js
CHANGED
|
@@ -52,6 +52,16 @@ const LOCAL_ENV_KEYS = new Set([
|
|
|
52
52
|
"AGINTI_WRAPPER_REASONING",
|
|
53
53
|
"AGINTI_AUX_PROVIDER",
|
|
54
54
|
"AGINTI_AUX_MODEL",
|
|
55
|
+
"AGINTI_WRITING_PROVIDER",
|
|
56
|
+
"AGINTI_WRITING_MODEL",
|
|
57
|
+
"AGINTI_WRITING_PROVIDER_ZH",
|
|
58
|
+
"AGINTI_WRITING_MODEL_ZH",
|
|
59
|
+
"AGINTI_WRITING_PROVIDER_EN",
|
|
60
|
+
"AGINTI_WRITING_MODEL_EN",
|
|
61
|
+
"AGINTI_WRITING_PROVIDER_JA",
|
|
62
|
+
"AGINTI_WRITING_MODEL_JA",
|
|
63
|
+
"AGINTI_WRITING_PROVIDER_KO",
|
|
64
|
+
"AGINTI_WRITING_MODEL_KO",
|
|
55
65
|
"QWEN_API_KEY",
|
|
56
66
|
"QWEN_DEFAULT_MODEL",
|
|
57
67
|
"QWEN_BASE_URL",
|
package/src/scs-controller.js
CHANGED
|
@@ -211,6 +211,7 @@ function fallbackHardContractPlan(goal = "", contract = {}, studentReason = "")
|
|
|
211
211
|
const exactInputPaths = normalizeStringList(contract.exactInputPaths, []);
|
|
212
212
|
const requiredTextTerms = normalizeStringList(contract.requiredTextTerms, []);
|
|
213
213
|
const forbiddenTextTerms = normalizeStringList(contract.forbiddenTextTerms, []);
|
|
214
|
+
const requiredToolCalls = normalizeStringList(contract.requiredToolCalls, []);
|
|
214
215
|
return [
|
|
215
216
|
"1. Execute the user's target work under the deterministic hard-contract fallback plan.",
|
|
216
217
|
exactOutputPaths.length
|
|
@@ -219,9 +220,10 @@ function fallbackHardContractPlan(goal = "", contract = {}, studentReason = "")
|
|
|
219
220
|
exactInputPaths.length ? `3. Use these exact user-specified input/reference path(s): ${exactInputPaths.join(", ")}.` : "",
|
|
220
221
|
requiredTextTerms.length ? `4. Ensure the output contains these required term(s): ${requiredTextTerms.join(", ")}.` : "",
|
|
221
222
|
forbiddenTextTerms.length ? `5. Ensure the output does not contain these forbidden term(s): ${forbiddenTextTerms.join(", ")}.` : "",
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
requiredToolCalls.length ? `6. Call these explicitly required tool(s) before finish: ${requiredToolCalls.join(", ")}.` : "",
|
|
224
|
+
"7. Run concrete validation commands or inspections for file existence and content before finish.",
|
|
225
|
+
studentReason ? `8. Preserve the validator concern while executing: ${compact(studentReason, 180)}` : "",
|
|
226
|
+
goal ? `9. Original goal remains authoritative: ${compact(goal, 220)}` : "",
|
|
225
227
|
]
|
|
226
228
|
.filter(Boolean)
|
|
227
229
|
.join("\n");
|
package/src/scs-evidence.js
CHANGED
|
@@ -457,6 +457,33 @@ function inferForbiddenActions(goal = "") {
|
|
|
457
457
|
return unique(forbidden).slice(0, 8);
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
function inferRequiredToolCalls(goal = "") {
|
|
461
|
+
const source = stripForbiddenLanguage(String(goal || ""));
|
|
462
|
+
const knownTools = [
|
|
463
|
+
"writing_specialist",
|
|
464
|
+
"json_specialist",
|
|
465
|
+
"read_image",
|
|
466
|
+
"generate_image",
|
|
467
|
+
"send_to_canvas",
|
|
468
|
+
"start_long_job",
|
|
469
|
+
"long_job_status",
|
|
470
|
+
];
|
|
471
|
+
const required = [];
|
|
472
|
+
for (const toolName of knownTools) {
|
|
473
|
+
const index = source.indexOf(toolName);
|
|
474
|
+
if (index < 0) continue;
|
|
475
|
+
const window = source.slice(Math.max(0, index - 90), Math.min(source.length, index + toolName.length + 90));
|
|
476
|
+
const strongToolInstruction =
|
|
477
|
+
/\b(?:must|explicitly|again|call|invoke|require(?:d|s)?\s+(?:tool|call|use))\b/i.test(window) ||
|
|
478
|
+
/必须(?:调用|使用)|必須(?:調用|使用)|明确(?:调用|使用)|明確(?:調用|使用)|再次(?:调用|使用)|调用|調用/.test(window);
|
|
479
|
+
const specialistUseInstruction = /_specialist$/.test(toolName) && (/\buse\b/i.test(window) || /使用/.test(window));
|
|
480
|
+
if (strongToolInstruction || specialistUseInstruction) {
|
|
481
|
+
required.push(toolName);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return unique(required).slice(0, 8);
|
|
485
|
+
}
|
|
486
|
+
|
|
460
487
|
function stripForbiddenLanguage(goal = "") {
|
|
461
488
|
return String(goal || "")
|
|
462
489
|
.replace(/\b(do not|don't|dont|never|no need to|without)\s+([^.\n;]+)/gi, "")
|
|
@@ -466,7 +493,8 @@ function stripForbiddenLanguage(goal = "") {
|
|
|
466
493
|
|
|
467
494
|
export function deriveScsTaskContract({ goal = "", taskProfile = "", acceptanceCriteria = [] } = {}) {
|
|
468
495
|
const requirementCategories = inferRequirementCategories(goal, taskProfile, acceptanceCriteria);
|
|
469
|
-
const
|
|
496
|
+
const requiredToolCalls = inferRequiredToolCalls(goal);
|
|
497
|
+
const requiresExternalEvidence = requirementCategories.length > 0 || requiredToolCalls.length > 0 || goalRequiresEvidence(goal, taskProfile);
|
|
470
498
|
const requiredEvidence = requirementCategories.map((category) => ({
|
|
471
499
|
id: category,
|
|
472
500
|
category,
|
|
@@ -483,6 +511,7 @@ export function deriveScsTaskContract({ goal = "", taskProfile = "", acceptanceC
|
|
|
483
511
|
forbiddenActions: inferForbiddenActions(goal),
|
|
484
512
|
exactOutputPaths,
|
|
485
513
|
exactInputPaths,
|
|
514
|
+
requiredToolCalls,
|
|
486
515
|
requiredTextTerms: inferRequiredTextTerms(goal),
|
|
487
516
|
forbiddenTextTerms: inferForbiddenTextTerms(goal),
|
|
488
517
|
successCriteria: unique(acceptanceCriteria).slice(0, 10),
|
|
@@ -679,6 +708,7 @@ export function buildScsEvidenceLedger({ state = {}, context = {} } = {}) {
|
|
|
679
708
|
const messageEvidence = messages.flatMap(messageToEvidence);
|
|
680
709
|
const items = [...eventEvidence, ...messageEvidence].slice(-80);
|
|
681
710
|
const categories = unique(items.map((item) => item.category));
|
|
711
|
+
const toolNames = unique(items.map((item) => item.toolName).filter(Boolean));
|
|
682
712
|
const blockers = [...events.map(eventToBlocker), ...messages.map(messageToBlocker)]
|
|
683
713
|
.filter(Boolean)
|
|
684
714
|
.slice(-20)
|
|
@@ -690,6 +720,7 @@ export function buildScsEvidenceLedger({ state = {}, context = {} } = {}) {
|
|
|
690
720
|
version: 1,
|
|
691
721
|
itemCount: items.length,
|
|
692
722
|
categories,
|
|
723
|
+
toolNames,
|
|
693
724
|
blockerCount: blockers.length,
|
|
694
725
|
blockers,
|
|
695
726
|
items: items.map((item, index) => ({
|
|
@@ -702,6 +733,8 @@ export function buildScsEvidenceLedger({ state = {}, context = {} } = {}) {
|
|
|
702
733
|
export function evaluateScsEvidence(contract = {}, ledger = {}) {
|
|
703
734
|
const required = Array.isArray(contract.requiredEvidence) ? contract.requiredEvidence : [];
|
|
704
735
|
const ledgerCategories = new Set(Array.isArray(ledger.categories) ? ledger.categories : []);
|
|
736
|
+
const requiredToolCalls = Array.isArray(contract.requiredToolCalls) ? contract.requiredToolCalls : [];
|
|
737
|
+
const ledgerToolNames = new Set(Array.isArray(ledger.toolNames) ? ledger.toolNames : []);
|
|
705
738
|
const satisfied = [];
|
|
706
739
|
const missing = [];
|
|
707
740
|
for (const requirement of required) {
|
|
@@ -711,17 +744,23 @@ export function evaluateScsEvidence(contract = {}, ledger = {}) {
|
|
|
711
744
|
missing.push(requirement);
|
|
712
745
|
}
|
|
713
746
|
}
|
|
747
|
+
const missingToolCalls = requiredToolCalls.filter((toolName) => !ledgerToolNames.has(toolName));
|
|
714
748
|
const hasAnyEvidence = Number(ledger.itemCount || 0) > 0;
|
|
715
|
-
const
|
|
749
|
+
const evidenceOk = !contract.requiresExternalEvidence || (missing.length === 0 && hasAnyEvidence);
|
|
750
|
+
const ok = evidenceOk && missingToolCalls.length === 0;
|
|
716
751
|
return {
|
|
717
752
|
ok,
|
|
718
753
|
requiresExternalEvidence: Boolean(contract.requiresExternalEvidence),
|
|
719
754
|
hasAnyEvidence,
|
|
720
755
|
satisfied,
|
|
721
756
|
missing,
|
|
757
|
+
requiredToolCalls,
|
|
758
|
+
missingToolCalls,
|
|
722
759
|
reason: ok
|
|
723
760
|
? "Evidence satisfies the deterministic task contract."
|
|
724
|
-
:
|
|
761
|
+
: missingToolCalls.length
|
|
762
|
+
? `Missing required tool calls: ${missingToolCalls.join(", ")}.`
|
|
763
|
+
: missing.length
|
|
725
764
|
? `Missing evidence categories: ${missing.map((item) => item.category).join(", ")}.`
|
|
726
765
|
: "Task requires external evidence but the ledger is empty.",
|
|
727
766
|
};
|
|
@@ -741,6 +780,7 @@ export function summarizeScsContractEvidence({ contract = {}, ledger = {}, evalu
|
|
|
741
780
|
forbiddenActions: contract.forbiddenActions || [],
|
|
742
781
|
exactOutputPaths: contract.exactOutputPaths || [],
|
|
743
782
|
exactInputPaths: contract.exactInputPaths || [],
|
|
783
|
+
requiredToolCalls: contract.requiredToolCalls || [],
|
|
744
784
|
requiredTextTerms: contract.requiredTextTerms || [],
|
|
745
785
|
forbiddenTextTerms: contract.forbiddenTextTerms || [],
|
|
746
786
|
successCriteria: contract.successCriteria || [],
|
|
@@ -748,6 +788,7 @@ export function summarizeScsContractEvidence({ contract = {}, ledger = {}, evalu
|
|
|
748
788
|
evidenceLedger: {
|
|
749
789
|
itemCount: ledger.itemCount || 0,
|
|
750
790
|
categories: ledger.categories || [],
|
|
791
|
+
toolNames: ledger.toolNames || [],
|
|
751
792
|
recentItems: (ledger.items || []).slice(-12).map((item) => ({
|
|
752
793
|
id: item.id,
|
|
753
794
|
category: item.category,
|
|
@@ -770,6 +811,7 @@ export function summarizeScsContractEvidence({ contract = {}, ledger = {}, evalu
|
|
|
770
811
|
ok: Boolean(evaluation.ok),
|
|
771
812
|
reason: evaluation.reason || "",
|
|
772
813
|
missing: (evaluation.missing || []).map((item) => item.category),
|
|
814
|
+
missingToolCalls: evaluation.missingToolCalls || [],
|
|
773
815
|
satisfied: (evaluation.satisfied || []).map((item) => item.category),
|
|
774
816
|
},
|
|
775
817
|
};
|
|
@@ -794,6 +836,7 @@ export function deterministicFinishBlocker(contract = {}, ledger = {}, evaluatio
|
|
|
794
836
|
evidence: [
|
|
795
837
|
`Required: ${(contract.requiredEvidence || []).map((item) => item.category).join(", ") || "external evidence"}`,
|
|
796
838
|
`Present: ${(ledger.categories || []).join(", ") || "none"}`,
|
|
839
|
+
...(evaluation.missingToolCalls?.length ? [`Required tool calls missing: ${evaluation.missingToolCalls.join(", ")}`] : []),
|
|
797
840
|
],
|
|
798
841
|
nextRequiredAction:
|
|
799
842
|
"Collect the missing concrete evidence, verify the requested state or artifact, then ask SCS to finish again; if impossible, report a real blocker with proof.",
|
package/src/workspace-tools.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { XMLValidator } from "fast-xml-parser";
|
|
4
5
|
import { hasSensitiveText, redactSensitiveText } from "./redaction.js";
|
|
5
6
|
|
|
6
7
|
export const WORKSPACE_TOOL_NAMES = ["inspect_project", "list_files", "read_file", "search_files", "write_file", "apply_patch"];
|
|
@@ -174,6 +175,13 @@ function validateSvgArtifact(relativePath, content) {
|
|
|
174
175
|
const cdataOpen = (trimmed.match(/<!\[CDATA\[/gi) || []).length;
|
|
175
176
|
const cdataClose = (trimmed.match(/\]\]>/g) || []).length;
|
|
176
177
|
if (cdataOpen !== cdataClose) errors.push("CDATA open/close markers are unbalanced.");
|
|
178
|
+
const xmlValidation = trimmed ? XMLValidator.validate(trimmed, { allowBooleanAttributes: false }) : true;
|
|
179
|
+
if (xmlValidation !== true) {
|
|
180
|
+
const err = xmlValidation?.err || {};
|
|
181
|
+
errors.push(
|
|
182
|
+
`XML parser rejected SVG${err.line ? ` at line ${err.line}${err.col ? `:${err.col}` : ""}` : ""}: ${err.msg || "invalid XML syntax"}`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
177
185
|
errors.push(...validateXmlTagStack(trimmed));
|
|
178
186
|
if (/<text\b/i.test(trimmed) && !/(viewBox|width|height)=/i.test(trimmed)) {
|
|
179
187
|
warnings.push("SVG contains text but no obvious sizing/viewBox metadata; render-preview before claiming visual fit.");
|
|
@@ -181,7 +189,7 @@ function validateSvgArtifact(relativePath, content) {
|
|
|
181
189
|
return {
|
|
182
190
|
kind: "svg",
|
|
183
191
|
ok: errors.length === 0,
|
|
184
|
-
parser: "
|
|
192
|
+
parser: "fast-xml-parser+xml-stack-guard",
|
|
185
193
|
root: root || "",
|
|
186
194
|
errors,
|
|
187
195
|
warnings,
|
|
@@ -87,8 +87,62 @@ function normalizeWritingRequest(args = {}) {
|
|
|
87
87
|
length: normalizeText(args.length || args.targetLength || ""),
|
|
88
88
|
formatIntent: normalizeText(args.formatIntent || args.outputFormat || ""),
|
|
89
89
|
temperature: normalizeTemperature(args.temperature, kind),
|
|
90
|
-
provider: String(args.provider ||
|
|
91
|
-
model: String(args.model ||
|
|
90
|
+
provider: String(args.provider || "").trim(),
|
|
91
|
+
model: String(args.model || "").trim(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function likelyLanguageFromText(request = {}) {
|
|
96
|
+
const explicit = String(request.language || "").trim();
|
|
97
|
+
if (explicit) return explicit;
|
|
98
|
+
const text = [request.writingBrief, request.target, request.audience, request.canon, request.styleGuide, request.priorDraft, request.constraints]
|
|
99
|
+
.join("\n")
|
|
100
|
+
.slice(0, 8000);
|
|
101
|
+
if (/[\u3040-\u30ff]/.test(text)) return "ja";
|
|
102
|
+
if (/[\uac00-\ud7af]/.test(text)) return "ko";
|
|
103
|
+
if (/[\u4e00-\u9fff]/.test(text)) return "zh-Hans";
|
|
104
|
+
return "en";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function languageEnvSuffix(language = "") {
|
|
108
|
+
const normalized = String(language || "").toLowerCase();
|
|
109
|
+
if (normalized.startsWith("zh")) return "ZH";
|
|
110
|
+
if (normalized.startsWith("ja")) return "JA";
|
|
111
|
+
if (normalized.startsWith("ko")) return "KO";
|
|
112
|
+
if (normalized.startsWith("en")) return "EN";
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasProviderKey(provider = "") {
|
|
117
|
+
const normalized = String(provider || "").toLowerCase();
|
|
118
|
+
if (normalized === "openai") return Boolean(process.env.OPENAI_API_KEY || process.env.LLM_API_KEY);
|
|
119
|
+
if (normalized === "deepseek") return Boolean(process.env.DEEPSEEK_API_KEY || process.env.LLM_API_KEY);
|
|
120
|
+
if (normalized === "openrouter") return Boolean(process.env.OPENROUTER_API_KEY);
|
|
121
|
+
if (normalized === "qwen") return Boolean(process.env.QWEN_API_KEY);
|
|
122
|
+
if (normalized === "venice") return Boolean(process.env.VENICE_API_KEY);
|
|
123
|
+
if (normalized === "mock") return true;
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function languageWriterDefaults(request = {}, config = {}) {
|
|
128
|
+
const language = likelyLanguageFromText(request);
|
|
129
|
+
const suffix = languageEnvSuffix(language);
|
|
130
|
+
const envProvider = String((suffix && process.env[`AGINTI_WRITING_PROVIDER_${suffix}`]) || process.env.AGINTI_WRITING_PROVIDER || "").trim();
|
|
131
|
+
const envModel = String((suffix && process.env[`AGINTI_WRITING_MODEL_${suffix}`]) || process.env.AGINTI_WRITING_MODEL || "").trim();
|
|
132
|
+
if (envProvider || envModel) {
|
|
133
|
+
return { provider: envProvider, model: envModel, language, reason: "writer-env" };
|
|
134
|
+
}
|
|
135
|
+
if (String(language || "").toLowerCase().startsWith("zh") && hasProviderKey("deepseek")) {
|
|
136
|
+
return { provider: "deepseek", model: process.env.DEEPSEEK_PRO_MODEL || "deepseek-v4-pro", language, reason: "zh-deepseek-default" };
|
|
137
|
+
}
|
|
138
|
+
if (String(language || "").toLowerCase().startsWith("en") && hasProviderKey("openai")) {
|
|
139
|
+
return { provider: "openai", model: process.env.OPENAI_DEFAULT_MODEL || process.env.LLM_MODEL || "gpt-5.4", language, reason: "en-openai-default" };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
provider: config.provider || "",
|
|
143
|
+
model: config.model || "",
|
|
144
|
+
language,
|
|
145
|
+
reason: "session-default",
|
|
92
146
|
};
|
|
93
147
|
}
|
|
94
148
|
|
|
@@ -193,8 +247,9 @@ export async function runWritingSpecialist(args = {}, config = {}, store = null)
|
|
|
193
247
|
.update(JSON.stringify(redactValue(request)))
|
|
194
248
|
.digest("hex");
|
|
195
249
|
let result;
|
|
196
|
-
|
|
197
|
-
let
|
|
250
|
+
const writerDefaults = languageWriterDefaults(request, config);
|
|
251
|
+
let model = request.model || writerDefaults.model || config.model || "";
|
|
252
|
+
let provider = request.provider || writerDefaults.provider || config.provider || "";
|
|
198
253
|
let rawContent = "";
|
|
199
254
|
|
|
200
255
|
try {
|
|
@@ -204,11 +259,13 @@ export async function runWritingSpecialist(args = {}, config = {}, store = null)
|
|
|
204
259
|
result = mockWritingResult(request);
|
|
205
260
|
} else {
|
|
206
261
|
const providerDefaults = request.provider ? getProviderDefaults(request.provider) : {};
|
|
262
|
+
const selectedProviderDefaults = provider ? getProviderDefaults(provider) : {};
|
|
207
263
|
const writingConfig = {
|
|
208
264
|
...config,
|
|
265
|
+
...selectedProviderDefaults,
|
|
209
266
|
...providerDefaults,
|
|
210
267
|
provider: provider || config.provider,
|
|
211
|
-
model: model || providerDefaults.model || config.model,
|
|
268
|
+
model: model || providerDefaults.model || selectedProviderDefaults.model || config.model,
|
|
212
269
|
};
|
|
213
270
|
model = writingConfig.model;
|
|
214
271
|
provider = writingConfig.provider;
|
|
@@ -254,9 +311,11 @@ export async function runWritingSpecialist(args = {}, config = {}, store = null)
|
|
|
254
311
|
task: request.task,
|
|
255
312
|
kind: request.kind,
|
|
256
313
|
language: request.language,
|
|
314
|
+
detectedLanguage: writerDefaults.language,
|
|
257
315
|
target: request.target,
|
|
258
316
|
length: request.length,
|
|
259
317
|
provider: request.provider,
|
|
318
|
+
routeReason: writerDefaults.reason,
|
|
260
319
|
formatIntent: request.formatIntent,
|
|
261
320
|
requestFingerprint,
|
|
262
321
|
},
|
|
@@ -273,8 +332,10 @@ export async function runWritingSpecialist(args = {}, config = {}, store = null)
|
|
|
273
332
|
task: request.task,
|
|
274
333
|
kind: request.kind,
|
|
275
334
|
language: request.language,
|
|
335
|
+
detectedLanguage: writerDefaults.language,
|
|
276
336
|
target: request.target,
|
|
277
337
|
provider: request.provider,
|
|
338
|
+
routeReason: writerDefaults.reason,
|
|
278
339
|
requestFingerprint,
|
|
279
340
|
},
|
|
280
341
|
error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
|