@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 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 --sandbox-status --sandbox-mode docker-readonly --cwd "$PWD"
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-shell --cwd "$PWD" "Report the current directory"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
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 || "/home/lachlan/ProjectsLFS/Agent";
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 file: notes/mock-output.txt with a short coding smoke message.", "coding-write");
82
- const written = await fs.readFile(path.join(workspace, "notes/mock-output.txt"), "utf8");
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",
@@ -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",
@@ -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();