@lazyingart/agintiflow 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/docs/project-agent-lessons.md +41 -0
- package/package.json +1 -1
- package/public/app.js +74 -1
- package/public/index.html +40 -0
- package/public/styles.css +21 -0
- package/scripts/smoke-coding-tools.js +19 -4
- package/scripts/smoke-web-api.js +43 -0
- package/src/agent-runner.js +14 -0
- package/src/cli.js +179 -0
- package/src/config.js +6 -2
- package/src/model-client.js +28 -7
- package/src/project.js +332 -0
- package/src/task-profiles.js +85 -0
- package/src/web-db.js +6 -2
- package/web.js +47 -6
package/README.md
CHANGED
|
@@ -38,8 +38,12 @@ Install the published CLI:
|
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npm install -g @lazyingart/agintiflow
|
|
41
|
+
cd /path/to/your-project
|
|
42
|
+
aginti init
|
|
43
|
+
aginti doctor
|
|
41
44
|
aginti --list-routes
|
|
42
|
-
aginti --
|
|
45
|
+
aginti --list-profiles
|
|
46
|
+
aginti --sandbox-status --sandbox-mode docker-readonly
|
|
43
47
|
```
|
|
44
48
|
|
|
45
49
|
Launch the local web UI from an installed package:
|
|
@@ -49,10 +53,23 @@ aginti web --port 3210
|
|
|
49
53
|
# then open http://127.0.0.1:3210
|
|
50
54
|
```
|
|
51
55
|
|
|
56
|
+
`aginti web` uses the folder it is launched from as the project root, default working directory, session store, and settings database. CLI and web runs share the same project-local `.sessions/` folder.
|
|
57
|
+
|
|
52
58
|
Run the installed CLI without a live provider key by using the local mock route:
|
|
53
59
|
|
|
54
60
|
```bash
|
|
55
|
-
aginti --provider mock --routing manual --allow-
|
|
61
|
+
aginti --provider mock --routing manual --allow-file-tools "Create notes/hello.md with a smoke-test note"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Useful project commands:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
aginti keys status
|
|
68
|
+
printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
|
|
69
|
+
aginti sessions list
|
|
70
|
+
aginti sessions show <session-id>
|
|
71
|
+
aginti resume <session-id> "continue with a short follow-up"
|
|
72
|
+
aginti --profile code --provider mock --routing manual "Create notes/hello.md"
|
|
56
73
|
```
|
|
57
74
|
|
|
58
75
|
Run from a source checkout:
|
|
@@ -101,6 +118,9 @@ The package exposes both `aginti` and `aginti-cli`; they run the same CLI entryp
|
|
|
101
118
|
The web app includes:
|
|
102
119
|
|
|
103
120
|
- Routing dropdown for smart, fast, complex, and manual model selection.
|
|
121
|
+
- Project folder indicator showing project root, command cwd, session folder, and session database.
|
|
122
|
+
- First-run provider setup panel with mock fallback and project-local DeepSeek/OpenAI key save.
|
|
123
|
+
- Task profile dropdown for code, writing, design docs, Python, shell, Node, AAPS, LaTeX, and system maintenance workflows.
|
|
104
124
|
- Provider dropdown for DeepSeek, OpenAI, and local mock mode when manual routing is needed.
|
|
105
125
|
- Language dropdown with 11 persisted UI locales.
|
|
106
126
|
- Editable model field, with DeepSeek v4 flash as the fast default and DeepSeek v4 pro as the complex route.
|
|
@@ -162,6 +182,7 @@ PACKAGE_INSTALL_POLICY=prompt
|
|
|
162
182
|
USE_DOCKER_SANDBOX=true
|
|
163
183
|
DOCKER_SANDBOX_IMAGE=agintiflow-sandbox:latest
|
|
164
184
|
COMMAND_CWD=/home/lachlan/ProjectsLFS/Agent
|
|
185
|
+
AGINTI_TASK_PROFILE=auto
|
|
165
186
|
```
|
|
166
187
|
|
|
167
188
|
Defaults:
|
|
@@ -180,6 +201,15 @@ Provider credentials:
|
|
|
180
201
|
| OpenAI | `OPENAI_API_KEY` | `https://api.openai.com/v1` |
|
|
181
202
|
| DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |
|
|
182
203
|
|
|
204
|
+
Project-local credentials can be stored without committing secrets:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
aginti init
|
|
208
|
+
printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
This writes `.aginti/.env` with `0600` permissions and adds safe `.gitignore` entries. APIs and logs expose only key presence, never raw values.
|
|
212
|
+
|
|
183
213
|
## Agent Wrappers
|
|
184
214
|
|
|
185
215
|
AgInTiFlow can expose external coding agents as advisory tools when `ALLOW_WRAPPER_TOOLS=true` or the web UI toggle is enabled. Wrappers are not a replacement for the core runner; they are used for second opinions, codebase analysis, or planning when they are installed and authenticated. The preferred wrapper defaults to Codex and can be changed with `PREFERRED_WRAPPER=codex`, `aginti --allow-wrappers --wrapper codex`, or the web UI dropdown.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Project-Agent UX Lessons
|
|
2
|
+
|
|
3
|
+
Round 8 distilled implementation lessons from local agent projects and applied the highest-value subset to AgInTiFlow.
|
|
4
|
+
|
|
5
|
+
## Codex
|
|
6
|
+
|
|
7
|
+
- Treat the launch directory as the working project unless the user overrides it.
|
|
8
|
+
- Keep patch/file tools deterministic and auditable: edits should be workspace-local, compactly diffed, and easy to inspect.
|
|
9
|
+
- Resume should be a first-class CLI flow, not only a hidden runtime detail.
|
|
10
|
+
|
|
11
|
+
## Claude Code
|
|
12
|
+
|
|
13
|
+
- Terminal UX should be simple: install, `cd` into a project, run the agent.
|
|
14
|
+
- Project context matters more than global state for everyday coding work.
|
|
15
|
+
- Natural-language prompts should map to file edits, shell checks, and git-aware workflows without extra ceremony.
|
|
16
|
+
|
|
17
|
+
## Gemini CLI
|
|
18
|
+
|
|
19
|
+
- Session history should be project-scoped so runs from different folders do not mix.
|
|
20
|
+
- Checkpoints/resume flows need discoverable commands and clear current-project boundaries.
|
|
21
|
+
- Extensibility works best as named profiles/skills rather than hardcoded one-off prompts.
|
|
22
|
+
|
|
23
|
+
## Copilot SDK
|
|
24
|
+
|
|
25
|
+
- BYOK status should report only presence/absence and env-var names, never raw key material.
|
|
26
|
+
- Session APIs should expose metadata, logs, workspace state, and model/provider choices for web clients.
|
|
27
|
+
- Tool and permission hooks should be visible enough that users can understand why actions were allowed or blocked.
|
|
28
|
+
|
|
29
|
+
## Claw Code
|
|
30
|
+
|
|
31
|
+
- First-run `init` and `doctor` commands reduce setup ambiguity.
|
|
32
|
+
- Container/sandbox readiness should be diagnosed before complex tasks.
|
|
33
|
+
- Parity and smoke checks should run against isolated project folders, not only the source repo.
|
|
34
|
+
|
|
35
|
+
## Applied In AgInTiFlow
|
|
36
|
+
|
|
37
|
+
- `aginti web` now defaults command execution to the folder it was launched from.
|
|
38
|
+
- CLI and web share `.sessions/` in the project root.
|
|
39
|
+
- `aginti init`, `aginti doctor`, `aginti keys`, `aginti sessions`, and `aginti resume` provide basic project lifecycle control.
|
|
40
|
+
- Task profiles wire lightweight skill prompts into CLI/web runs while keeping the LLM responsible for the main plan.
|
|
41
|
+
- Project-local `.aginti/.env` can store provider keys safely with 0600 permissions and ignored git entries.
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -19,6 +19,18 @@ const translations = {
|
|
|
19
19
|
languageLabel: "Language",
|
|
20
20
|
intro:
|
|
21
21
|
"Web-first agent platform with smart model routing, resumable runs, guarded tools, and optional external agent wrappers.",
|
|
22
|
+
projectStatusTitle: "Project folder",
|
|
23
|
+
setupTitle: "Provider setup",
|
|
24
|
+
setupHelp:
|
|
25
|
+
"DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.",
|
|
26
|
+
setupEnvHelp:
|
|
27
|
+
"Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.",
|
|
28
|
+
setupProviderLabel: "Provider",
|
|
29
|
+
setupKeyLabel: "API key",
|
|
30
|
+
saveKeyButton: "Save local key",
|
|
31
|
+
keySavedStatus: "Local key saved. Raw values are never returned by the API.",
|
|
32
|
+
keySaveFailed: "Failed to save local key.",
|
|
33
|
+
taskProfileLabel: "Task profile",
|
|
22
34
|
routingModeLabel: "Routing policy",
|
|
23
35
|
routingSmartOption: "Smart: flash/pro/wrappers",
|
|
24
36
|
routingFastOption: "DeepSeek v4 flash",
|
|
@@ -617,6 +629,13 @@ const providerField = document.querySelector("#provider");
|
|
|
617
629
|
const modelField = document.querySelector("#model");
|
|
618
630
|
const routingHintEl = document.querySelector("#routing-hint");
|
|
619
631
|
const modelRouteStatusEl = document.querySelector("#model-route-status");
|
|
632
|
+
const projectStatusEl = document.querySelector("#project-status");
|
|
633
|
+
const setupCardEl = document.querySelector("#setup-card");
|
|
634
|
+
const setupProviderField = document.querySelector("#setup-provider");
|
|
635
|
+
const setupApiKeyField = document.querySelector("#setup-api-key");
|
|
636
|
+
const saveApiKeyButton = document.querySelector("#save-api-key");
|
|
637
|
+
const setupStatusEl = document.querySelector("#setup-status");
|
|
638
|
+
const taskProfileField = document.querySelector("#taskProfile");
|
|
620
639
|
const sandboxModeField = document.querySelector("#sandboxMode");
|
|
621
640
|
const packageInstallPolicyField = document.querySelector("#packageInstallPolicy");
|
|
622
641
|
const packageWarningEl = document.querySelector("#package-warning");
|
|
@@ -673,6 +692,8 @@ const defaults = {
|
|
|
673
692
|
|
|
674
693
|
let currentLanguage = "en";
|
|
675
694
|
let routingPresets = {};
|
|
695
|
+
let taskProfiles = [];
|
|
696
|
+
let projectInfo = null;
|
|
676
697
|
let currentSessionId = "";
|
|
677
698
|
let pollTimer = null;
|
|
678
699
|
let saveTimer = null;
|
|
@@ -713,6 +734,28 @@ function renderKeyStatus(status = lastKeyStatus) {
|
|
|
713
734
|
} · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · ${t("mockLabel")} ${
|
|
714
735
|
status.mock ? t("availableLabel") : t("missingLabel")
|
|
715
736
|
}`;
|
|
737
|
+
if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function renderProjectStatus(info = projectInfo) {
|
|
741
|
+
projectInfo = info;
|
|
742
|
+
if (!projectStatusEl || !info) return;
|
|
743
|
+
projectStatusEl.textContent = [
|
|
744
|
+
`root=${info.root || ""}`,
|
|
745
|
+
`cwd=${info.commandCwd || ""}`,
|
|
746
|
+
`sessions=${info.sessionsDir || ""}`,
|
|
747
|
+
`db=${info.sessionDbPath || ""}`,
|
|
748
|
+
`shared=${info.sharedSessionFolder ? "yes" : "no"}`,
|
|
749
|
+
].join(" · ");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function renderTaskProfiles(selected = "auto") {
|
|
753
|
+
if (!taskProfileField) return;
|
|
754
|
+
const profiles = taskProfiles.length ? taskProfiles : [{ id: "auto", label: "Auto" }];
|
|
755
|
+
taskProfileField.innerHTML = profiles
|
|
756
|
+
.map((profile) => `<option value="${escapeHtml(profile.id)}">${escapeHtml(profile.label || profile.id)}</option>`)
|
|
757
|
+
.join("");
|
|
758
|
+
taskProfileField.value = profiles.some((profile) => profile.id === selected) ? selected : "auto";
|
|
716
759
|
}
|
|
717
760
|
|
|
718
761
|
function renderWrapperStatus(wrappers = lastWrappers) {
|
|
@@ -893,6 +936,7 @@ function applyLanguage(language, { persist = true } = {}) {
|
|
|
893
936
|
}
|
|
894
937
|
|
|
895
938
|
renderKeyStatus();
|
|
939
|
+
renderProjectStatus();
|
|
896
940
|
renderWrapperStatus();
|
|
897
941
|
renderWorkspacePanel();
|
|
898
942
|
renderSandboxStatus();
|
|
@@ -922,6 +966,7 @@ function formPayload() {
|
|
|
922
966
|
allowFileTools: document.querySelector("#allowFileTools").checked,
|
|
923
967
|
allowWrapperTools: allowWrapperToolsField.checked,
|
|
924
968
|
preferredWrapper: preferredWrapperField.value,
|
|
969
|
+
taskProfile: taskProfileField?.value || "auto",
|
|
925
970
|
useDockerSandbox: sandboxModeField.value !== "host",
|
|
926
971
|
dockerSandboxImage: document.querySelector("#dockerSandboxImage").value.trim(),
|
|
927
972
|
allowPasswords: document.querySelector("#allowPasswords").checked,
|
|
@@ -1673,6 +1718,30 @@ sandboxModeField.addEventListener("change", updatePackageWarning);
|
|
|
1673
1718
|
packageInstallPolicyField.addEventListener("change", updatePackageWarning);
|
|
1674
1719
|
allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
|
|
1675
1720
|
preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
|
|
1721
|
+
taskProfileField?.addEventListener("change", schedulePreferenceSave);
|
|
1722
|
+
|
|
1723
|
+
saveApiKeyButton?.addEventListener("click", async () => {
|
|
1724
|
+
const provider = setupProviderField.value || "deepseek";
|
|
1725
|
+
const apiKey = setupApiKeyField.value.trim();
|
|
1726
|
+
if (!apiKey) {
|
|
1727
|
+
setupStatusEl.textContent = t("setupKeyLabel");
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
setupStatusEl.textContent = t("sendingStatus");
|
|
1731
|
+
const response = await fetch(`/api/keys/${encodeURIComponent(provider)}`, {
|
|
1732
|
+
method: "POST",
|
|
1733
|
+
headers: { "Content-Type": "application/json" },
|
|
1734
|
+
body: JSON.stringify({ apiKey }),
|
|
1735
|
+
});
|
|
1736
|
+
const data = await response.json().catch(() => ({}));
|
|
1737
|
+
setupApiKeyField.value = "";
|
|
1738
|
+
if (!response.ok) {
|
|
1739
|
+
setupStatusEl.textContent = data.error || t("keySaveFailed");
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
setupStatusEl.textContent = t("keySavedStatus");
|
|
1743
|
+
renderKeyStatus(data.keyStatus);
|
|
1744
|
+
});
|
|
1676
1745
|
|
|
1677
1746
|
form.addEventListener("input", schedulePreferenceSave);
|
|
1678
1747
|
form.addEventListener("change", schedulePreferenceSave);
|
|
@@ -1785,6 +1854,8 @@ async function loadConfig() {
|
|
|
1785
1854
|
const data = await response.json();
|
|
1786
1855
|
const prefs = data.preferences || {};
|
|
1787
1856
|
routingPresets = data.routing?.presets || {};
|
|
1857
|
+
taskProfiles = data.taskProfiles || [];
|
|
1858
|
+
projectInfo = data.project || null;
|
|
1788
1859
|
defaults.openai = data.defaults?.openai?.model || defaults.openai;
|
|
1789
1860
|
defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
|
|
1790
1861
|
defaults.mock = data.defaults?.mock?.model || defaults.mock;
|
|
@@ -1794,9 +1865,10 @@ async function loadConfig() {
|
|
|
1794
1865
|
routingModeField.value = prefs.routingMode || "smart";
|
|
1795
1866
|
providerField.value = prefs.provider || "deepseek";
|
|
1796
1867
|
modelField.value = prefs.model || defaults[providerField.value] || "deepseek-v4-flash";
|
|
1868
|
+
renderTaskProfiles(prefs.taskProfile || "auto");
|
|
1797
1869
|
document.querySelector("#startUrl").value = prefs.startUrl || "";
|
|
1798
1870
|
document.querySelector("#allowedDomains").value = prefs.allowedDomains || "";
|
|
1799
|
-
document.querySelector("#commandCwd").value = prefs.commandCwd || "
|
|
1871
|
+
document.querySelector("#commandCwd").value = prefs.commandCwd || data.project?.root || "";
|
|
1800
1872
|
document.querySelector("#headless").checked = prefs.headless ?? data.defaults.headless;
|
|
1801
1873
|
document.querySelector("#maxSteps").value = prefs.maxSteps ?? data.defaults.maxSteps;
|
|
1802
1874
|
sandboxModeField.value = prefs.sandboxMode || (prefs.useDockerSandbox ? "docker-workspace" : "host");
|
|
@@ -1810,6 +1882,7 @@ async function loadConfig() {
|
|
|
1810
1882
|
document.querySelector("#allowDestructive").checked = prefs.allowDestructive ?? false;
|
|
1811
1883
|
|
|
1812
1884
|
renderKeyStatus(data.keyStatus);
|
|
1885
|
+
renderProjectStatus(data.project);
|
|
1813
1886
|
renderWrapperStatus(data.wrappers || []);
|
|
1814
1887
|
renderWorkspacePanel(data.workspace, []);
|
|
1815
1888
|
await refreshWorkspaceChanges();
|
package/public/index.html
CHANGED
|
@@ -36,6 +36,39 @@
|
|
|
36
36
|
<p class="subtle" data-i18n="intro">
|
|
37
37
|
Browser agent with provider selection, resumable runs, persisted settings, and an optional guarded shell tool with Docker sandbox support.
|
|
38
38
|
</p>
|
|
39
|
+
<section class="project-card">
|
|
40
|
+
<strong data-i18n="projectStatusTitle">Project folder</strong>
|
|
41
|
+
<p id="project-status" class="subtle">Loading project context...</p>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
44
|
+
<section id="setup-card" class="setup-card" hidden>
|
|
45
|
+
<div>
|
|
46
|
+
<strong data-i18n="setupTitle">Provider setup</strong>
|
|
47
|
+
<p class="subtle" data-i18n="setupHelp">
|
|
48
|
+
DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.
|
|
49
|
+
</p>
|
|
50
|
+
<p class="subtle" data-i18n="setupEnvHelp">
|
|
51
|
+
Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="grid">
|
|
55
|
+
<label>
|
|
56
|
+
<span data-i18n="setupProviderLabel">Provider</span>
|
|
57
|
+
<select id="setup-provider">
|
|
58
|
+
<option value="deepseek">DeepSeek</option>
|
|
59
|
+
<option value="openai">OpenAI</option>
|
|
60
|
+
</select>
|
|
61
|
+
</label>
|
|
62
|
+
<label>
|
|
63
|
+
<span data-i18n="setupKeyLabel">API key</span>
|
|
64
|
+
<input id="setup-api-key" type="password" autocomplete="off" placeholder="Stored in .aginti/.env" />
|
|
65
|
+
</label>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="actions">
|
|
68
|
+
<button id="save-api-key" type="button" class="secondary" data-i18n="saveKeyButton">Save local key</button>
|
|
69
|
+
<span id="setup-status" class="subtle"></span>
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
39
72
|
|
|
40
73
|
<form id="run-form">
|
|
41
74
|
<label>
|
|
@@ -67,6 +100,13 @@
|
|
|
67
100
|
<p id="routing-hint" class="subtle route-hint"></p>
|
|
68
101
|
<p id="model-route-status" class="subtle route-hint"></p>
|
|
69
102
|
|
|
103
|
+
<label>
|
|
104
|
+
<span data-i18n="taskProfileLabel">Task profile</span>
|
|
105
|
+
<select id="taskProfile" name="taskProfile">
|
|
106
|
+
<option value="auto">Auto</option>
|
|
107
|
+
</select>
|
|
108
|
+
</label>
|
|
109
|
+
|
|
70
110
|
<label>
|
|
71
111
|
<span data-i18n="goalLabel">Goal</span>
|
|
72
112
|
<textarea
|
package/public/styles.css
CHANGED
|
@@ -120,6 +120,27 @@ h1 {
|
|
|
120
120
|
line-height: 1.45;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
.project-card,
|
|
124
|
+
.setup-card {
|
|
125
|
+
display: grid;
|
|
126
|
+
min-width: 0;
|
|
127
|
+
gap: 10px;
|
|
128
|
+
margin: 14px 0;
|
|
129
|
+
padding: 13px;
|
|
130
|
+
border: 1px solid rgba(15, 118, 110, 0.18);
|
|
131
|
+
border-radius: 16px;
|
|
132
|
+
background: linear-gradient(135deg, rgba(204, 251, 241, 0.44), rgba(255, 247, 237, 0.9));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.project-card p,
|
|
136
|
+
.setup-card p {
|
|
137
|
+
overflow-wrap: anywhere;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.setup-card[hidden] {
|
|
141
|
+
display: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
form,
|
|
124
145
|
.checks {
|
|
125
146
|
display: grid;
|
|
@@ -17,7 +17,7 @@ function assert(condition, message) {
|
|
|
17
17
|
if (!condition) throw new Error(message);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async function runMock(goal, sessionId) {
|
|
20
|
+
async function runMock(goal, sessionId, { resume = false } = {}) {
|
|
21
21
|
const config = resolveRuntimeConfig(
|
|
22
22
|
{
|
|
23
23
|
provider: "mock",
|
|
@@ -26,6 +26,7 @@ async function runMock(goal, sessionId) {
|
|
|
26
26
|
goal,
|
|
27
27
|
commandCwd: workspace,
|
|
28
28
|
maxSteps: 5,
|
|
29
|
+
resume: resume ? sessionId : "",
|
|
29
30
|
},
|
|
30
31
|
{
|
|
31
32
|
baseDir: runtimeDir,
|
|
@@ -38,7 +39,7 @@ async function runMock(goal, sessionId) {
|
|
|
38
39
|
allowFileTools: true,
|
|
39
40
|
sandboxMode: "host",
|
|
40
41
|
packageInstallPolicy: "block",
|
|
41
|
-
sessionId,
|
|
42
|
+
sessionId: resume ? "" : sessionId,
|
|
42
43
|
}
|
|
43
44
|
);
|
|
44
45
|
|
|
@@ -78,11 +79,23 @@ try {
|
|
|
78
79
|
"repaired DeepSeek history retained an orphan stale tool message"
|
|
79
80
|
);
|
|
80
81
|
|
|
81
|
-
const writeRun = await runMock("Create
|
|
82
|
-
const written = await fs.readFile(path.join(workspace, "notes/
|
|
82
|
+
const writeRun = await runMock("Create notes/hello.md with a short coding smoke message.", "coding-write");
|
|
83
|
+
const written = await fs.readFile(path.join(workspace, "notes/hello.md"), "utf8");
|
|
83
84
|
assert(written.includes("Created by AgInTiFlow mock mode."), "mock write did not create expected file");
|
|
84
85
|
assert(writeRun.events.some((event) => event.type === "file.changed"), "write run did not persist file.changed event");
|
|
85
86
|
|
|
87
|
+
await runMock("Create notes/resume.md with resumed session content.", "coding-write", { resume: true });
|
|
88
|
+
const resumed = await fs.readFile(path.join(workspace, "notes/resume.md"), "utf8");
|
|
89
|
+
assert(resumed.includes("Created by AgInTiFlow mock mode."), "mock resume did not create a new requested file");
|
|
90
|
+
|
|
91
|
+
let duplicateFailed = false;
|
|
92
|
+
try {
|
|
93
|
+
await runMock("Create notes/hello.md with duplicate content.", "coding-write-duplicate");
|
|
94
|
+
} catch (error) {
|
|
95
|
+
duplicateFailed = /File already exists|Mock tool failed/.test(String(error));
|
|
96
|
+
}
|
|
97
|
+
assert(duplicateFailed, "duplicate mock write did not fail safely");
|
|
98
|
+
|
|
86
99
|
await runMock("Create file: /workspace/virtual-output.txt with virtual Docker path support.", "coding-write-virtual");
|
|
87
100
|
const virtualWritten = await fs.readFile(path.join(workspace, "virtual-output.txt"), "utf8");
|
|
88
101
|
assert(virtualWritten.includes("Created by AgInTiFlow mock mode."), "virtual /workspace path was not mapped safely");
|
|
@@ -123,6 +136,8 @@ try {
|
|
|
123
136
|
checks: [
|
|
124
137
|
"deepseek_history_repair",
|
|
125
138
|
"write_file",
|
|
139
|
+
"duplicate_write_failed",
|
|
140
|
+
"resume_session_write",
|
|
126
141
|
"virtual_workspace_path",
|
|
127
142
|
"apply_patch",
|
|
128
143
|
"block_env",
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -74,6 +74,23 @@ try {
|
|
|
74
74
|
if (!config.keyStatus?.mock) throw new Error("mock provider is not advertised by /api/config");
|
|
75
75
|
if (!config.workspace?.enabled) throw new Error("workspace file tools are not advertised by /api/config");
|
|
76
76
|
if (config.preferences?.preferredWrapper !== "codex") throw new Error("Codex is not the default preferred wrapper");
|
|
77
|
+
if (config.project?.root !== runtimeDir) throw new Error("web project root did not default to launch directory");
|
|
78
|
+
if (config.preferences?.commandCwd !== runtimeDir) throw new Error("commandCwd did not default to project root");
|
|
79
|
+
if (!Array.isArray(config.taskProfiles) || !config.taskProfiles.some((profile) => profile.id === "latex")) {
|
|
80
|
+
throw new Error("task profiles are not advertised by /api/config");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const keyStatus = await fetchJson("/api/keys/status");
|
|
84
|
+
if (typeof keyStatus.keyStatus?.deepseek !== "boolean") throw new Error("key status endpoint is invalid");
|
|
85
|
+
if ("localEnvPath" in keyStatus.keyStatus) throw new Error("key status leaked a local env path");
|
|
86
|
+
const savedKey = await fetchJson("/api/keys/deepseek", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ apiKey: "test-deepseek-key-not-real" }),
|
|
90
|
+
});
|
|
91
|
+
if (!savedKey.ok || !savedKey.keyStatus?.deepseek || "apiKey" in savedKey || "key" in savedKey) {
|
|
92
|
+
throw new Error("local key save endpoint returned invalid or sensitive data");
|
|
93
|
+
}
|
|
77
94
|
|
|
78
95
|
const status = await fetchJson("/api/sandbox/status");
|
|
79
96
|
if (!status.status?.workspaceReadable) throw new Error("sandbox status did not report a readable workspace");
|
|
@@ -111,6 +128,30 @@ try {
|
|
|
111
128
|
if (run.status !== "finished") throw new Error(`mock run failed: ${run.error || "unknown error"}`);
|
|
112
129
|
if (!/Mock run complete/.test(run.result)) throw new Error("mock run did not return the expected result");
|
|
113
130
|
|
|
131
|
+
const fileRunStart = await fetchJson("/api/runs", {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "Content-Type": "application/json" },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
provider: "mock",
|
|
136
|
+
routingMode: "manual",
|
|
137
|
+
model: "mock-agent",
|
|
138
|
+
goal: "Create notes/hello.md with safe web API content.",
|
|
139
|
+
commandCwd: runtimeDir,
|
|
140
|
+
sandboxMode: "host",
|
|
141
|
+
packageInstallPolicy: "block",
|
|
142
|
+
allowShellTool: false,
|
|
143
|
+
allowFileTools: true,
|
|
144
|
+
preferredWrapper: "codex",
|
|
145
|
+
maxSteps: 4,
|
|
146
|
+
headless: true,
|
|
147
|
+
taskProfile: "code",
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
const fileRun = await waitForRun(fileRunStart.sessionId);
|
|
151
|
+
if (fileRun.status !== "finished") throw new Error(`mock file run failed: ${fileRun.error || "unknown error"}`);
|
|
152
|
+
const hello = await fs.readFile(path.join(runtimeDir, "notes", "hello.md"), "utf8");
|
|
153
|
+
if (!hello.includes("Created by AgInTiFlow mock mode.")) throw new Error("mock file run did not create requested path");
|
|
154
|
+
|
|
114
155
|
const chat = await fetchJson(`/api/sessions/${encodeURIComponent(runStart.sessionId)}/chat`);
|
|
115
156
|
if (!Array.isArray(chat.chat) || chat.chat.length < 2) throw new Error("chat history was not persisted");
|
|
116
157
|
|
|
@@ -189,6 +230,8 @@ try {
|
|
|
189
230
|
ok: true,
|
|
190
231
|
endpoints: [
|
|
191
232
|
"/api/config",
|
|
233
|
+
"/api/keys/status",
|
|
234
|
+
"POST /api/keys/:provider",
|
|
192
235
|
"/api/sandbox/status",
|
|
193
236
|
"/api/sandbox/preflight",
|
|
194
237
|
"/api/runs",
|
package/src/agent-runner.js
CHANGED
|
@@ -13,6 +13,7 @@ import { evaluateCommandPolicy } from "./command-policy.js";
|
|
|
13
13
|
import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
14
14
|
import { executeWorkspaceTool, summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
|
|
15
15
|
import { normalizeCanvasPayload } from "./artifact-tunnel.js";
|
|
16
|
+
import { getTaskProfile } from "./task-profiles.js";
|
|
16
17
|
|
|
17
18
|
const exec = promisify(execCallback);
|
|
18
19
|
const BROWSER_TOOLS = new Set(["open_url", "click", "type", "scroll", "press", "back"]);
|
|
@@ -108,6 +109,7 @@ export function repairModelMessageHistory(state, config = {}) {
|
|
|
108
109
|
|
|
109
110
|
function createInitialState(config, sessionId) {
|
|
110
111
|
const now = new Date().toISOString();
|
|
112
|
+
const taskProfile = getTaskProfile(config.taskProfile);
|
|
111
113
|
return {
|
|
112
114
|
sessionId,
|
|
113
115
|
createdAt: now,
|
|
@@ -151,6 +153,7 @@ function createInitialState(config, sessionId) {
|
|
|
151
153
|
config.allowWrapperTools
|
|
152
154
|
? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
|
|
153
155
|
: "External coding-agent wrappers are disabled.",
|
|
156
|
+
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
154
157
|
"A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
|
|
155
158
|
"For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
|
|
156
159
|
"For LaTeX/PDF requests, create the needed source/assets, compile with the available allowlisted TeX toolchain, and publish the resulting PDF through send_to_canvas. For subfolder documents, keep outputs beside the source. Use pdflatex-compatible figure formats such as PDF or PNG.",
|
|
@@ -172,6 +175,7 @@ function createInitialState(config, sessionId) {
|
|
|
172
175
|
config.allowWrapperTools
|
|
173
176
|
? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
174
177
|
: "",
|
|
178
|
+
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
175
179
|
"Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
|
|
176
180
|
"Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
|
|
177
181
|
"LaTeX/PDF requests should produce source artifacts and, when possible, a compiled PDF artifact. For subfolder documents, keep outputs beside the source. For figure-in-document tasks, create PDF/PNG figures that pdflatex can include.",
|
|
@@ -248,6 +252,7 @@ function appendChatEntry(state, role, content) {
|
|
|
248
252
|
function applyContinuationPrompt(state, config, observers) {
|
|
249
253
|
if (!config.resume || !config.goal) return;
|
|
250
254
|
|
|
255
|
+
const taskProfile = getTaskProfile(config.taskProfile);
|
|
251
256
|
ensureChatState(state);
|
|
252
257
|
state.goal = config.goal;
|
|
253
258
|
state.provider = config.provider;
|
|
@@ -271,6 +276,7 @@ function applyContinuationPrompt(state, config, observers) {
|
|
|
271
276
|
config.allowWrapperTools
|
|
272
277
|
? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
273
278
|
: "",
|
|
279
|
+
`Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
|
|
274
280
|
]
|
|
275
281
|
.filter(Boolean)
|
|
276
282
|
.join("\n"),
|
|
@@ -754,6 +760,7 @@ export async function runAgent(config) {
|
|
|
754
760
|
model: config.model,
|
|
755
761
|
routingMode: config.routingMode,
|
|
756
762
|
routeReason: config.routeReason,
|
|
763
|
+
taskProfile: config.taskProfile,
|
|
757
764
|
commandCwd: config.commandCwd,
|
|
758
765
|
allowShellTool: config.allowShellTool,
|
|
759
766
|
allowWrapperTools: config.allowWrapperTools,
|
|
@@ -817,6 +824,7 @@ export async function runAgent(config) {
|
|
|
817
824
|
plan: state.plan || "",
|
|
818
825
|
suggestedStartUrl: config.startUrl || "",
|
|
819
826
|
canvasArtifactsAvailable: true,
|
|
827
|
+
taskProfile: getTaskProfile(config.taskProfile),
|
|
820
828
|
})}`,
|
|
821
829
|
});
|
|
822
830
|
|
|
@@ -899,6 +907,12 @@ export async function runAgent(config) {
|
|
|
899
907
|
});
|
|
900
908
|
}
|
|
901
909
|
|
|
910
|
+
if (config.provider === "mock" && toolResult.ok === false && !toolResult.blocked) {
|
|
911
|
+
throw new Error(
|
|
912
|
+
`Mock tool failed: ${toolResult.error || toolResult.reason || `${toolResult.toolName || "tool"} returned ok=false`}`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
902
916
|
if (toolResult.done) {
|
|
903
917
|
state.stepsCompleted = step;
|
|
904
918
|
state.updatedAt = new Date().toISOString();
|