@lazyingart/agintiflow 0.20.5 → 0.20.7
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/docs/model-selection.md +1 -1
- package/package.json +1 -1
- package/references/model-routing-provider-design.md +2 -2
- package/references/venice-model-reference.md +1 -1
- package/scripts/smoke-cli-chat.js +12 -0
- package/scripts/smoke-model-roles.js +14 -2
- package/src/agent-runner.js +30 -1
- package/src/interactive-cli.js +70 -20
- package/src/model-client.js +155 -19
package/docs/model-selection.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -53,7 +53,7 @@ Interactive equivalents:
|
|
|
53
53
|
/auxiliary model grsai/nano-banana-2
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
`/venice` opens a route/main selector for Venice text models. The selector includes `venice/venice-uncensored-1-2` (Venice 1.2), `venice/
|
|
56
|
+
`/venice` opens a route/main selector for Venice text models. The selector includes `venice/venice-uncensored-1-2` (Venice 1.2), `venice/venice-uncensored` (Venice 1.1), and `venice/gemma-4-uncensored` (Gemma 4). In non-interactive shells, `/venice` keeps script compatibility by selecting `venice/venice-uncensored-1-2` for both roles. `/venice 1.2 gemma` sets route to Venice 1.2 and main to Gemma 4; `/venice off` restores `deepseek/deepseek-v4-flash` for route and `deepseek/deepseek-v4-pro` for main.
|
|
57
57
|
|
|
58
58
|
The web UI should expose model names as dropdowns, not free-text fields. The left panel should stay focused on common daily controls, while model-role editing and less-used switches live in an Advanced settings modal. The terminal-like capability panels belong after the runtime log so the left control panel remains short.
|
|
59
59
|
|
|
@@ -74,7 +74,7 @@ Codex wrapper defaults stay separate from native OpenAI API settings: GPT-5.5 me
|
|
|
74
74
|
|
|
75
75
|
| UI bucket | Concrete default | Notes |
|
|
76
76
|
|---|---|---|
|
|
77
|
-
| `venice-uncensored` | `venice-uncensored-1-2` | Venice-native text; `/venice` also exposes `
|
|
77
|
+
| `venice-uncensored` | `venice-uncensored-1-2` | Venice-native text; `/venice` also exposes `venice-uncensored` as Venice 1.1 |
|
|
78
78
|
| `venice-qwen` | `qwen3-6-27b` | Qwen-family text/code |
|
|
79
79
|
| `venice-gpt` | `openai-gpt-55` | OpenAI-family through Venice |
|
|
80
80
|
| `venice-claude` | `claude-sonnet-4-6` | Claude-family through Venice |
|
|
@@ -52,7 +52,7 @@ Store `VENICE_API_KEY` only in ignored local files such as `.aginti/.env` or a s
|
|
|
52
52
|
| `openai-gpt-52` | 256K | GPT-5.2 |
|
|
53
53
|
| `venice-uncensored` | 32K | Venice Uncensored legacy / deprecated |
|
|
54
54
|
|
|
55
|
-
AgInTiFlow exposes the primary Venice text choices through `/venice`: Venice 1.2 (`venice-uncensored-1-2`), Venice 1.1 (`
|
|
55
|
+
AgInTiFlow exposes the primary Venice text choices through `/venice`: Venice 1.2 (`venice-uncensored-1-2`), Venice 1.1 (`venice-uncensored`), and Gemma 4 (`gemma-4-uncensored`). The command can select route and main independently, so a fast Venice route can be paired with a larger Gemma main model when useful. The E2EE 1.1 ID (`e2ee-venice-uncensored-24b-p`) is documented for reference, but the selector uses the working non-E2EE 1.1 route because the E2EE route currently returns an upstream provider error in live tests.
|
|
56
56
|
|
|
57
57
|
## Image And Edit Models
|
|
58
58
|
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import {
|
|
8
8
|
buildLaunchHeaderLines,
|
|
9
9
|
buildPromptLayout,
|
|
10
|
+
buildPromptRenderSequence,
|
|
10
11
|
canonicalSlashPromptBuffer,
|
|
11
12
|
classifyEscapeAction,
|
|
12
13
|
formatWorkspaceChange,
|
|
@@ -131,6 +132,16 @@ try {
|
|
|
131
132
|
if (promptLayout.cursorRow < 0 || promptLayout.cursorColumn < 0) {
|
|
132
133
|
throw new Error("terminal prompt layout returned an invalid cursor location");
|
|
133
134
|
}
|
|
135
|
+
const promptCursorMoveBefore = buildPromptLayout("smooth typing", 13, 90, 24, { commandCwd: "/tmp/aginti-project" });
|
|
136
|
+
const promptCursorMoveAfter = buildPromptLayout("smooth typing", 4, 90, 24, { commandCwd: "/tmp/aginti-project" });
|
|
137
|
+
const cursorOnlySequence = buildPromptRenderSequence(promptCursorMoveAfter, {
|
|
138
|
+
lineCount: promptCursorMoveBefore.renderedRows.length,
|
|
139
|
+
cursorRow: promptCursorMoveBefore.cursorRow,
|
|
140
|
+
renderedRows: promptCursorMoveBefore.renderedRows,
|
|
141
|
+
});
|
|
142
|
+
if (cursorOnlySequence.includes("\x1b[2K") || cursorOnlySequence.includes("\x1b[?25l")) {
|
|
143
|
+
throw new Error("cursor-only prompt moves should not clear/redraw the input panel");
|
|
144
|
+
}
|
|
134
145
|
const paddedPromptLayout = buildPromptLayout("hello", 5, 80, 24, { commandCwd: "/tmp/aginti-project" });
|
|
135
146
|
const paddedRows = paddedPromptLayout.renderedRows.map((line) => line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, ""));
|
|
136
147
|
if (
|
|
@@ -271,6 +282,7 @@ try {
|
|
|
271
282
|
"workspace-patch-event-render",
|
|
272
283
|
"large-launch-header",
|
|
273
284
|
"prompt-layout",
|
|
285
|
+
"prompt-redraw-fast-path",
|
|
274
286
|
"user-prompt-label",
|
|
275
287
|
"escape-policy",
|
|
276
288
|
"live-input-status-layout",
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
modelsForProviderGroup,
|
|
10
10
|
selectModelRoute,
|
|
11
11
|
} from "../src/model-routing.js";
|
|
12
|
-
import { parseTextToolCalls } from "../src/model-client.js";
|
|
12
|
+
import { parseTextToolCalls, usesTextToolProtocol } from "../src/model-client.js";
|
|
13
13
|
|
|
14
14
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
15
|
|
|
@@ -115,6 +115,18 @@ const parsedTextToolCalls = parseTextToolCalls('[TOOL_CALLS]list_files[ARGS]call
|
|
|
115
115
|
assert(parsedTextToolCalls.length === 1, "Venice text tool-call parser did not detect encoded tool call");
|
|
116
116
|
assert(parsedTextToolCalls[0].function.name === "list_files", "Venice text tool-call parser returned wrong tool name");
|
|
117
117
|
assert(parsedTextToolCalls[0].function.arguments.includes('"maxDepth":1'), "Venice text tool-call parser returned wrong arguments");
|
|
118
|
+
const looseTextToolCalls = parseTextToolCalls('[TOOL_CALLS]list_files[ARGS]{"path":"."}[TOOL_CALLS]inspect_project[ARGS]{"path":"."}');
|
|
119
|
+
assert(looseTextToolCalls.length === 2, "Venice loose text tool-call parser did not detect multiple calls");
|
|
120
|
+
assert(looseTextToolCalls[1].function.name === "inspect_project", "Venice loose text tool-call parser returned wrong second tool");
|
|
121
|
+
const nativeMarkerText = parseTextToolCalls('Done. <|tool_call>call:finish{result:<|"|>Done<|"|>}');
|
|
122
|
+
assert(nativeMarkerText.length === 0, "native marker text should not be treated as JSON text tool call");
|
|
123
|
+
const jsonBlockToolCalls = parseTextToolCalls('TOOL_CALLS:\n```json\n[{"name":"list_files","arguments":{"path":"/workspace"}}]\n```');
|
|
124
|
+
assert(jsonBlockToolCalls.length === 1, "Venice JSON text tool-call parser did not detect JSON block calls");
|
|
125
|
+
assert(jsonBlockToolCalls[0].function.arguments.includes("/workspace"), "Venice JSON text tool-call parser returned wrong arguments");
|
|
126
|
+
assert(usesTextToolProtocol({ provider: "venice", model: "gemma-4-uncensored" }), "Venice Gemma should use text tool protocol");
|
|
127
|
+
assert(usesTextToolProtocol({ provider: "venice", model: "e2ee-venice-uncensored-24b-p" }), "Venice 1.1 should use text tool protocol");
|
|
128
|
+
assert(usesTextToolProtocol({ provider: "venice", model: "venice-uncensored" }), "Venice legacy 1.1 should use text tool protocol");
|
|
129
|
+
assert(!usesTextToolProtocol({ provider: "venice", model: "venice-uncensored-1-2" }), "Venice 1.2 should keep native tool calls first");
|
|
118
130
|
|
|
119
131
|
const output = await runCli(["models"]);
|
|
120
132
|
assert(output.includes("/route") && output.includes("/spare") && output.includes("venice-gpt"), "aginti models output missing role details");
|
|
@@ -124,7 +136,7 @@ assert(interactiveOutput.includes("venice=on"), "/venice did not enable Venice r
|
|
|
124
136
|
assert(interactiveOutput.includes("route=venice/venice-uncensored-1-2"), "/venice did not set Venice route role");
|
|
125
137
|
assert(interactiveOutput.includes("main=venice/venice-uncensored-1-2"), "/venice did not set Venice main role");
|
|
126
138
|
const interactiveGemmaOutput = await runInteractive("/venice 1.1 gemma\n");
|
|
127
|
-
assert(interactiveGemmaOutput.includes("route=venice/
|
|
139
|
+
assert(interactiveGemmaOutput.includes("route=venice/venice-uncensored"), "/venice 1.1 did not set Venice 1.1 route role");
|
|
128
140
|
assert(interactiveGemmaOutput.includes("main=venice/gemma-4-uncensored"), "/venice gemma did not set Gemma 4 main role");
|
|
129
141
|
const interactiveOffOutput = await runInteractive("/venice off\n");
|
|
130
142
|
assert(interactiveOffOutput.includes("venice=off"), "/venice off did not restore DeepSeek roles");
|
package/src/agent-runner.js
CHANGED
|
@@ -739,7 +739,7 @@ async function buildSnapshot(browserState, store, step, config) {
|
|
|
739
739
|
|
|
740
740
|
async function injectQueuedUserMessages(store, state, observers) {
|
|
741
741
|
const inbox = await store.drainInbox();
|
|
742
|
-
if (inbox.length === 0) return;
|
|
742
|
+
if (inbox.length === 0) return 0;
|
|
743
743
|
|
|
744
744
|
for (const item of inbox) {
|
|
745
745
|
const content = String(item.content || "").trim();
|
|
@@ -762,6 +762,7 @@ async function injectQueuedUserMessages(store, state, observers) {
|
|
|
762
762
|
priority: item.priority || "normal",
|
|
763
763
|
});
|
|
764
764
|
}
|
|
765
|
+
return inbox.length;
|
|
765
766
|
}
|
|
766
767
|
|
|
767
768
|
async function executeTool(browserState, toolCall, snapshot, config, store, observers, state) {
|
|
@@ -1374,6 +1375,16 @@ export async function runAgent(config) {
|
|
|
1374
1375
|
});
|
|
1375
1376
|
|
|
1376
1377
|
throwIfAborted(config);
|
|
1378
|
+
await store.appendEvent("model.requested", {
|
|
1379
|
+
step,
|
|
1380
|
+
provider: config.provider,
|
|
1381
|
+
model: config.model,
|
|
1382
|
+
});
|
|
1383
|
+
observers.event("model.requested", {
|
|
1384
|
+
step,
|
|
1385
|
+
provider: config.provider,
|
|
1386
|
+
model: config.model,
|
|
1387
|
+
});
|
|
1377
1388
|
const response = await requestNextStep(client, config, state.messages);
|
|
1378
1389
|
const assistantMessage = response.choices[0]?.message;
|
|
1379
1390
|
if (!assistantMessage) {
|
|
@@ -1399,6 +1410,13 @@ export async function runAgent(config) {
|
|
|
1399
1410
|
const toolCalls = assistantMessage.tool_calls || [];
|
|
1400
1411
|
|
|
1401
1412
|
if (toolCalls.length === 0) {
|
|
1413
|
+
const queuedCount = await injectQueuedUserMessages(store, state, observers);
|
|
1414
|
+
if (queuedCount > 0) {
|
|
1415
|
+
state.stepsCompleted = step;
|
|
1416
|
+
state.updatedAt = new Date().toISOString();
|
|
1417
|
+
await store.saveState(state);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1402
1420
|
const fallback = assistantMessage.content?.trim() || "No tool call returned.";
|
|
1403
1421
|
appendChatEntry(state, "assistant", fallback);
|
|
1404
1422
|
await store.appendEvent("session.finished", {
|
|
@@ -1419,6 +1437,7 @@ export async function runAgent(config) {
|
|
|
1419
1437
|
};
|
|
1420
1438
|
}
|
|
1421
1439
|
|
|
1440
|
+
let continueForQueuedInput = false;
|
|
1422
1441
|
for (const toolCall of toolCalls) {
|
|
1423
1442
|
throwIfAborted(config);
|
|
1424
1443
|
const toolResult = await executeTool(browserState, toolCall, snapshot, config, store, observers, state);
|
|
@@ -1462,6 +1481,14 @@ export async function runAgent(config) {
|
|
|
1462
1481
|
}
|
|
1463
1482
|
|
|
1464
1483
|
if (toolResult.done) {
|
|
1484
|
+
const queuedCount = await injectQueuedUserMessages(store, state, observers);
|
|
1485
|
+
if (queuedCount > 0) {
|
|
1486
|
+
state.stepsCompleted = step;
|
|
1487
|
+
state.updatedAt = new Date().toISOString();
|
|
1488
|
+
await store.saveState(state);
|
|
1489
|
+
continueForQueuedInput = true;
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1465
1492
|
state.stepsCompleted = step;
|
|
1466
1493
|
state.updatedAt = new Date().toISOString();
|
|
1467
1494
|
state.meta.lastUrl = browserState.page?.url() || state.meta.lastUrl;
|
|
@@ -1487,6 +1514,8 @@ export async function runAgent(config) {
|
|
|
1487
1514
|
}
|
|
1488
1515
|
}
|
|
1489
1516
|
|
|
1517
|
+
if (continueForQueuedInput) continue;
|
|
1518
|
+
|
|
1490
1519
|
await injectQueuedUserMessages(store, state, observers);
|
|
1491
1520
|
|
|
1492
1521
|
state.stepsCompleted = step;
|
package/src/interactive-cli.js
CHANGED
|
@@ -832,24 +832,58 @@ function cursorLocation(layout, cursor) {
|
|
|
832
832
|
|
|
833
833
|
function clearRenderedPrompt(previous) {
|
|
834
834
|
if (!previous.lineCount) return;
|
|
835
|
+
let sequence = ansi.cursorHide;
|
|
835
836
|
const below = previous.lineCount - 1 - previous.cursorRow;
|
|
836
|
-
if (below > 0)
|
|
837
|
-
|
|
837
|
+
if (below > 0) sequence += `\x1b[${below}B`;
|
|
838
|
+
sequence += `\r${ansi.clearLine}`;
|
|
838
839
|
for (let index = 1; index < previous.lineCount; index += 1) {
|
|
839
|
-
|
|
840
|
+
sequence += `\x1b[1A\r${ansi.clearLine}`;
|
|
840
841
|
}
|
|
842
|
+
sequence += ansi.cursorShow;
|
|
843
|
+
output.write(sequence);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function renderedRowsEqual(left = [], right = []) {
|
|
847
|
+
if (left.length !== right.length) return false;
|
|
848
|
+
return left.every((line, index) => line === right[index]);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
export function buildPromptRenderSequence(layout, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }) {
|
|
852
|
+
const previousRows = Array.isArray(previous.renderedRows) ? previous.renderedRows : [];
|
|
853
|
+
const previousLineCount = Number(previous.lineCount) || 0;
|
|
854
|
+
const previousCursorRow = Number(previous.cursorRow) || 0;
|
|
855
|
+
const nextRows = layout.renderedRows || [];
|
|
856
|
+
|
|
857
|
+
if (previousLineCount > 0 && renderedRowsEqual(previousRows, nextRows)) {
|
|
858
|
+
const rowDelta = layout.cursorRow - previousCursorRow;
|
|
859
|
+
return `${rowDelta > 0 ? `\x1b[${rowDelta}B` : rowDelta < 0 ? `\x1b[${Math.abs(rowDelta)}A` : ""}\r\x1b[${
|
|
860
|
+
layout.cursorColumn + 1
|
|
861
|
+
}G`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
let sequence = ansi.cursorHide;
|
|
865
|
+
if (previousLineCount > 0) {
|
|
866
|
+
const below = Math.max(previousLineCount - 1 - previousCursorRow, 0);
|
|
867
|
+
if (below > 0) sequence += `\x1b[${below}B`;
|
|
868
|
+
sequence += "\r";
|
|
869
|
+
if (previousLineCount > 1) sequence += `\x1b[${previousLineCount - 1}A`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const lineCount = Math.max(previousLineCount, nextRows.length);
|
|
873
|
+
for (let index = 0; index < lineCount; index += 1) {
|
|
874
|
+
sequence += `\r${ansi.clearLine}${nextRows[index] || ""}`;
|
|
875
|
+
if (index < lineCount - 1) sequence += "\n";
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const cursorUp = Math.max(lineCount - 1 - layout.cursorRow, 0);
|
|
879
|
+
if (cursorUp > 0) sequence += `\x1b[${cursorUp}A`;
|
|
880
|
+
sequence += `\r\x1b[${layout.cursorColumn + 1}G${ansi.cursorShow}`;
|
|
881
|
+
return sequence;
|
|
841
882
|
}
|
|
842
883
|
|
|
843
884
|
function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0, renderedRows: [] }, options = {}) {
|
|
844
885
|
const layout = buildPromptLayout(buffer, cursor, terminalWidth(), terminalHeight(), options);
|
|
845
|
-
output.write(
|
|
846
|
-
clearRenderedPrompt(previous);
|
|
847
|
-
output.write(layout.renderedRows.join("\n"));
|
|
848
|
-
|
|
849
|
-
const below = layout.renderedRows.length - 1 - layout.cursorRow;
|
|
850
|
-
if (below > 0) output.write(`\x1b[${below}A`);
|
|
851
|
-
output.write(`\r\x1b[${layout.cursorColumn + 1}G`);
|
|
852
|
-
output.write(ansi.cursorShow);
|
|
886
|
+
output.write(buildPromptRenderSequence(layout, previous));
|
|
853
887
|
return {
|
|
854
888
|
lineCount: layout.renderedRows.length,
|
|
855
889
|
cursorRow: layout.cursorRow,
|
|
@@ -1650,9 +1684,9 @@ function veniceTextModelChoices() {
|
|
|
1650
1684
|
},
|
|
1651
1685
|
{
|
|
1652
1686
|
provider: "venice",
|
|
1653
|
-
model: "
|
|
1687
|
+
model: "venice-uncensored",
|
|
1654
1688
|
label: "Venice 1.1",
|
|
1655
|
-
description: "
|
|
1689
|
+
description: "legacy Venice text model; 32K context",
|
|
1656
1690
|
},
|
|
1657
1691
|
{
|
|
1658
1692
|
provider: "venice",
|
|
@@ -1670,14 +1704,16 @@ function resolveVeniceTextModel(value = "") {
|
|
|
1670
1704
|
}
|
|
1671
1705
|
if (
|
|
1672
1706
|
normalized === "1.1" ||
|
|
1673
|
-
normalized === "venice-1.1"
|
|
1674
|
-
normalized === "e2ee-venice-uncensored-24b-p"
|
|
1707
|
+
normalized === "venice-1.1"
|
|
1675
1708
|
) {
|
|
1676
|
-
return "
|
|
1709
|
+
return "venice-uncensored";
|
|
1677
1710
|
}
|
|
1678
1711
|
if (normalized === "legacy" || normalized === "venice-uncensored") {
|
|
1679
1712
|
return "venice-uncensored";
|
|
1680
1713
|
}
|
|
1714
|
+
if (normalized === "e2ee" || normalized === "e2ee-venice-uncensored-24b-p") {
|
|
1715
|
+
return "e2ee-venice-uncensored-24b-p";
|
|
1716
|
+
}
|
|
1681
1717
|
if (normalized === "gemma" || normalized === "gemma4" || normalized === "gemma-4" || normalized === "gemma-4-uncensored") {
|
|
1682
1718
|
return "gemma-4-uncensored";
|
|
1683
1719
|
}
|
|
@@ -1781,9 +1817,9 @@ function modelRoleChoices(role = "main") {
|
|
|
1781
1817
|
},
|
|
1782
1818
|
{
|
|
1783
1819
|
provider: "venice",
|
|
1784
|
-
model: "
|
|
1820
|
+
model: "venice-uncensored",
|
|
1785
1821
|
label: "Venice 1.1",
|
|
1786
|
-
description: "
|
|
1822
|
+
description: "legacy Venice text route; use /auth venice if missing",
|
|
1787
1823
|
route: true,
|
|
1788
1824
|
main: true,
|
|
1789
1825
|
spare: true,
|
|
@@ -1930,8 +1966,11 @@ function clearSelector(lineCount) {
|
|
|
1930
1966
|
}
|
|
1931
1967
|
}
|
|
1932
1968
|
|
|
1969
|
+
function clearSelectorSequence(lineCount) {
|
|
1970
|
+
return Array.from({ length: Math.max(lineCount, 0) }, () => "\x1b[1A\r\x1b[2K").join("");
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1933
1973
|
function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0 }) {
|
|
1934
|
-
if (lineCount > 0) clearSelector(lineCount);
|
|
1935
1974
|
const width = Math.min(Math.max(terminalWidth() - 2, 60), 110);
|
|
1936
1975
|
const bodyWidth = width - 4;
|
|
1937
1976
|
const safeTitle = compactLine(title, bodyWidth);
|
|
@@ -1948,7 +1987,7 @@ function renderSelector({ title, subtitle, options, selectedIndex, lineCount = 0
|
|
|
1948
1987
|
}),
|
|
1949
1988
|
`╰${"─".repeat(width - 2)}╯`,
|
|
1950
1989
|
];
|
|
1951
|
-
output.write(`${rows.join("\n")}\n`);
|
|
1990
|
+
output.write(`${lineCount > 0 ? clearSelectorSequence(lineCount) : ""}${rows.join("\n")}\n`);
|
|
1952
1991
|
return rows.length;
|
|
1953
1992
|
}
|
|
1954
1993
|
|
|
@@ -2639,6 +2678,7 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
2639
2678
|
printSystemLine(`status=running workingOn=${state.activeGoal}`);
|
|
2640
2679
|
}
|
|
2641
2680
|
let result;
|
|
2681
|
+
let runError = null;
|
|
2642
2682
|
let queuedAfterFinish = [];
|
|
2643
2683
|
try {
|
|
2644
2684
|
result = await runAgent({
|
|
@@ -2666,6 +2706,8 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
2666
2706
|
onEvent: (type, data = {}) => {
|
|
2667
2707
|
if (type === "plan.created") {
|
|
2668
2708
|
printStatusEvent(state, "planned");
|
|
2709
|
+
} else if (type === "model.requested") {
|
|
2710
|
+
printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}`);
|
|
2669
2711
|
} else if (type === "tool.started") {
|
|
2670
2712
|
printStatusEvent(state, "tool", data.toolName || "unknown");
|
|
2671
2713
|
} else if (type === "tool.completed") {
|
|
@@ -2688,10 +2730,18 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
2688
2730
|
}
|
|
2689
2731
|
},
|
|
2690
2732
|
});
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
runError = error;
|
|
2691
2735
|
} finally {
|
|
2692
2736
|
detachInterrupts();
|
|
2693
2737
|
queuedAfterFinish = await liveInput.stop();
|
|
2694
2738
|
}
|
|
2739
|
+
if (runError) {
|
|
2740
|
+
state.status = isAbortError(runError) ? "stopped" : "failed";
|
|
2741
|
+
state.activeGoal = "";
|
|
2742
|
+
printSystemLine(`status=${state.status} session=${state.sessionId}`);
|
|
2743
|
+
throw runError;
|
|
2744
|
+
}
|
|
2695
2745
|
state.sessionId = result.sessionId || state.sessionId;
|
|
2696
2746
|
state.status = result.stopped ? "stopped" : "idle";
|
|
2697
2747
|
state.activeGoal = "";
|
package/src/model-client.js
CHANGED
|
@@ -73,16 +73,110 @@ function toolChoiceForProvider(config, messages = []) {
|
|
|
73
73
|
return messages.some((message) => message.role === "tool") ? "auto" : "required";
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function usesTextToolProtocol(config = {}) {
|
|
77
|
+
if (config.provider !== "venice") return false;
|
|
78
|
+
const model = String(config.model || "").toLowerCase();
|
|
79
|
+
return model === "gemma-4-uncensored" || model === "e2ee-venice-uncensored-24b-p" || model === "venice-uncensored";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function shouldRetryWithTextToolProtocol(error, config = {}) {
|
|
83
|
+
if (config.provider !== "venice") return false;
|
|
84
|
+
const message = [
|
|
85
|
+
error?.message,
|
|
86
|
+
error?.error?.message,
|
|
87
|
+
error?.response?.data?.error?.message,
|
|
88
|
+
error?.response?.data?.message,
|
|
89
|
+
]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" ");
|
|
92
|
+
return /invalid request parameters|tool_choice|parallel_tool_calls|tools/i.test(message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function textToolProtocolPrompt(tools = []) {
|
|
96
|
+
const toolLines = tools.map((tool) => {
|
|
97
|
+
const fn = tool.function || {};
|
|
98
|
+
const properties = fn.parameters?.properties ? Object.keys(fn.parameters.properties).slice(0, 8) : [];
|
|
99
|
+
const required = Array.isArray(fn.parameters?.required) ? fn.parameters.required : [];
|
|
100
|
+
const args = properties.length > 0 ? ` args=${properties.join(",")}${required.length ? ` required=${required.join(",")}` : ""}` : "";
|
|
101
|
+
return `- ${fn.name}: ${String(fn.description || "").slice(0, 180)}${args}`;
|
|
102
|
+
});
|
|
103
|
+
return [
|
|
104
|
+
"This provider/model may not accept native OpenAI function-call parameters.",
|
|
105
|
+
"Use this text tool protocol when you need a tool:",
|
|
106
|
+
'[TOOL_CALLS]tool_name[ARGS]{"arg":"value"}',
|
|
107
|
+
'A strict id form is also accepted: [TOOL_CALLS]tool_name[ARGS]call_short_id[ARGS]{"arg":"value"}',
|
|
108
|
+
'A JSON block form is accepted too: TOOL_CALLS: ```json [{"name":"tool_name","arguments":{"arg":"value"}}] ```',
|
|
109
|
+
"Return only one or more TOOL_CALLS blocks when calling tools; do not wrap them in markdown.",
|
|
110
|
+
"If no tool is needed, answer normally.",
|
|
111
|
+
"Available text tools:",
|
|
112
|
+
...toolLines,
|
|
113
|
+
].join("\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function messagesWithTextToolProtocol(config, messages, tools) {
|
|
117
|
+
const prepared = prepareMessages(config, messages).map((message) => {
|
|
118
|
+
if (message.role === "tool") {
|
|
119
|
+
return {
|
|
120
|
+
role: "user",
|
|
121
|
+
content: `Tool result for ${message.tool_call_id || "previous tool"}:\n${message.content || ""}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (message.role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
125
|
+
return {
|
|
126
|
+
role: "assistant",
|
|
127
|
+
content:
|
|
128
|
+
message.content ||
|
|
129
|
+
`Requested tools: ${message.tool_calls
|
|
130
|
+
.map((call) => `${call.function?.name || "tool"}(${call.function?.arguments || "{}"})`)
|
|
131
|
+
.join("; ")}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return message;
|
|
135
|
+
});
|
|
136
|
+
const protocol = { role: "system", content: textToolProtocolPrompt(tools) };
|
|
137
|
+
if (prepared[0]?.role === "system") return [prepared[0], protocol, ...prepared.slice(1)];
|
|
138
|
+
return [protocol, ...prepared];
|
|
139
|
+
}
|
|
140
|
+
|
|
76
141
|
export function parseTextToolCalls(content = "") {
|
|
77
142
|
const text = String(content || "");
|
|
78
|
-
if (!text.includes("[TOOL_CALLS]")) return [];
|
|
143
|
+
if (!text.includes("[TOOL_CALLS]") && !/TOOL_CALLS\s*:/i.test(text)) return [];
|
|
79
144
|
|
|
80
145
|
const calls = [];
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
146
|
+
const jsonBlock = text.match(/TOOL_CALLS\s*:\s*```(?:json)?\s*([\s\S]*?)```/i);
|
|
147
|
+
if (jsonBlock?.[1]) {
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(jsonBlock[1].trim());
|
|
150
|
+
if (Array.isArray(parsed)) {
|
|
151
|
+
for (const item of parsed) {
|
|
152
|
+
const name = String(item?.name || item?.tool || "").trim();
|
|
153
|
+
if (!name) continue;
|
|
154
|
+
const args = item?.arguments && typeof item.arguments === "object" ? item.arguments : {};
|
|
155
|
+
calls.push({
|
|
156
|
+
id: String(item?.id || `text-tool-${calls.length + 1}`),
|
|
157
|
+
type: "function",
|
|
158
|
+
function: {
|
|
159
|
+
name,
|
|
160
|
+
arguments: JSON.stringify(args),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
// Fall through to bracket parser below.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const chunk of text.split("[TOOL_CALLS]").slice(1)) {
|
|
171
|
+
const match = chunk.match(/^([A-Za-z0-9_-]+)\[ARGS\]([\s\S]*?)$/);
|
|
172
|
+
const name = match?.[1]?.trim();
|
|
173
|
+
let rawArgs = match?.[2]?.trim() || "{}";
|
|
174
|
+
let id = `text-tool-${calls.length + 1}`;
|
|
175
|
+
const strictParts = rawArgs.split("[ARGS]");
|
|
176
|
+
if (strictParts.length >= 2 && !rawArgs.startsWith("{") && !rawArgs.startsWith("[")) {
|
|
177
|
+
id = strictParts.shift()?.trim() || id;
|
|
178
|
+
rawArgs = strictParts.join("[ARGS]").trim() || "{}";
|
|
179
|
+
}
|
|
86
180
|
if (!name) continue;
|
|
87
181
|
try {
|
|
88
182
|
JSON.parse(rawArgs);
|
|
@@ -101,14 +195,39 @@ export function parseTextToolCalls(content = "") {
|
|
|
101
195
|
return calls;
|
|
102
196
|
}
|
|
103
197
|
|
|
198
|
+
function textBeforeToolCallMarker(content = "") {
|
|
199
|
+
return String(content || "")
|
|
200
|
+
.split("[TOOL_CALLS]")[0]
|
|
201
|
+
.split("TOOL_CALLS:")[0]
|
|
202
|
+
.split("<|tool_call>")[0]
|
|
203
|
+
.trim();
|
|
204
|
+
}
|
|
205
|
+
|
|
104
206
|
function normalizeTextToolCallResponse(response) {
|
|
105
207
|
const message = response?.choices?.[0]?.message;
|
|
106
208
|
if (!message || Array.isArray(message.tool_calls) && message.tool_calls.length > 0) return response;
|
|
107
209
|
|
|
108
210
|
const calls = parseTextToolCalls(message.content || "");
|
|
109
|
-
if (calls.length === 0)
|
|
211
|
+
if (calls.length === 0) {
|
|
212
|
+
const cleanedContent = textBeforeToolCallMarker(message.content || "");
|
|
213
|
+
if (!cleanedContent || cleanedContent === message.content) return response;
|
|
214
|
+
return {
|
|
215
|
+
...response,
|
|
216
|
+
choices: response.choices.map((choice, index) =>
|
|
217
|
+
index === 0
|
|
218
|
+
? {
|
|
219
|
+
...choice,
|
|
220
|
+
message: {
|
|
221
|
+
...message,
|
|
222
|
+
content: cleanedContent,
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
: choice
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
110
229
|
|
|
111
|
-
const content =
|
|
230
|
+
const content = textBeforeToolCallMarker(message.content || "");
|
|
112
231
|
return {
|
|
113
232
|
...response,
|
|
114
233
|
choices: response.choices.map((choice, index) =>
|
|
@@ -919,16 +1038,33 @@ export async function requestNextStep(client, config, messages) {
|
|
|
919
1038
|
]);
|
|
920
1039
|
}
|
|
921
1040
|
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1041
|
+
const textToolProtocol = usesTextToolProtocol(config);
|
|
1042
|
+
const nativePayload = {
|
|
1043
|
+
model: config.model,
|
|
1044
|
+
temperature: 0,
|
|
1045
|
+
tool_choice: toolChoiceForProvider(config, messages),
|
|
1046
|
+
parallel_tool_calls: false,
|
|
1047
|
+
messages: prepareMessages(config, messages),
|
|
1048
|
+
tools,
|
|
1049
|
+
};
|
|
1050
|
+
const textPayload = {
|
|
1051
|
+
model: config.model,
|
|
1052
|
+
temperature: 0,
|
|
1053
|
+
messages: messagesWithTextToolProtocol(config, messages, tools),
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
let response;
|
|
1057
|
+
try {
|
|
1058
|
+
response = await client.chat.completions.create(
|
|
1059
|
+
textToolProtocol ? textPayload : nativePayload,
|
|
1060
|
+
requestOptions(config)
|
|
1061
|
+
);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (!textToolProtocol && shouldRetryWithTextToolProtocol(error, config)) {
|
|
1064
|
+
response = await client.chat.completions.create(textPayload, requestOptions(config));
|
|
1065
|
+
} else {
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
933
1069
|
return normalizeTextToolCallResponse(response);
|
|
934
1070
|
}
|