@lazyingart/agintiflow 0.20.199 → 0.20.200

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
@@ -178,6 +178,13 @@ aginti --language de
178
178
  | Resume current project | `aginti resume` (`1` is newest/latest; Space shows more) |
179
179
  | Browse all sessions | `aginti resume --all-sessions` |
180
180
  | Queue into a running session | `aginti queue <session-id> "extra instruction"` |
181
+ | Inspect durable goal revisions | `aginti sessions list` and `aginti sessions show <session-id>` |
182
+
183
+ The runtime uses durable goal lifecycles, safe-boundary inbox interruption,
184
+ progressive tool/context disclosure, bounded read batching, evidence-gated
185
+ completion, and provider attribution. See
186
+ [State-of-the-Art Agent Runtime](docs/state-of-the-art-agent-runtime.md) for the
187
+ embedding and acceptance contract.
181
188
  | Clean empty sessions | `aginti --remove-empty-sessions` |
182
189
  | Check capabilities | `aginti capabilities`, `aginti doctor --capabilities` |
183
190
  | Sync reviewed skills | `aginti skillmesh status`, `aginti skillmesh sync` |
@@ -34,6 +34,30 @@ lightweight launcher without loading the full agent and web runtime.
34
34
 
35
35
  When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The web API exposes `GET /api/sessions/:id/inbox`, `POST /api/sessions/:id/inbox`, `PATCH /api/sessions/:id/inbox/:itemId`, and `DELETE /api/sessions/:id/inbox/:itemId` so browser users can inspect, edit, or remove pending pipe messages before the runner consumes them. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
36
36
 
37
+ Embedding hosts can continue the same durable context without scraping terminal
38
+ output:
39
+
40
+ ```bash
41
+ printf '%s' 'Follow up using the existing evidence.' \
42
+ | aginti resume SESSION_ID --stdin --json
43
+ ```
44
+
45
+ Machine resume emits the same single JSON object as `aginti run --json`, keeps
46
+ the original session ID, restores the saved provider/model/tool/runtime policy,
47
+ and never starts the web UI or writes interactive status lines. Explicit resume
48
+ options may still patch durable runtime fields, but `--stdin` and `--json` are
49
+ transport flags and are not persisted. This is the supported boundary for
50
+ LabCanvas, chat bridges, schedulers, and other embedding hosts that need one
51
+ isolated reusable agent session per conversation.
52
+
53
+ The object includes `goalRevision` and `goalStatus`. A first request starts at
54
+ revision 1. Every nonempty resume prompt advances the revision while retaining
55
+ the bounded prior-goal ledger. Accepted completion marks that revision
56
+ `completed`; a safe interruption or exhausted bounded run marks it `paused`;
57
+ provider/runtime failure marks it `failed`. The next continuation reactivates
58
+ the same session instead of replaying prior work. `aginti sessions show
59
+ SESSION_ID` exposes both the revision history and lifecycle transitions.
60
+
37
61
  The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items and before after-finish queued prompts. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. Idle Esc is ignored so it does not redraw the prompt into the transcript. During a run, Esc waits when `→` pipe messages are still pending and stops the run only when no ASAP pipe message is pending; Ctrl+C always stops. The current command cwd is rendered below the input panel in both idle and running states.
38
62
 
39
63
  The web UI uses a related but browser-appropriate pattern. Enter sends and Shift+Enter adds a newline. `Pipe to run` writes an ASAP inbox item shared with CLI. `Queue after finish` keeps a browser-local next prompt and starts it after the current web-owned run finishes. Both lanes render in a pending panel with Edit and Remove buttons instead of terminal-only keybindings.
@@ -0,0 +1,179 @@
1
+ # State-of-the-Art Agent Runtime
2
+
3
+ AgInTiFlow is designed as an embeddable agent runtime, not a prompt wrapper.
4
+ Its main job is to preserve intent, evidence, and tool progress while providers,
5
+ frontends, and long-running tasks change underneath it.
6
+
7
+ This architecture draws on proven patterns from durable agent systems,
8
+ including the append-only session and lifecycle ideas in
9
+ [`deepseek-ai/deepseek-harness`](https://github.com/deepseek-ai/deepseek-harness),
10
+ while retaining AgInTiFlow's provider-neutral tool and safety boundaries. The
11
+ implementation is local and does not import that repository as a runtime
12
+ dependency.
13
+
14
+ ## Runtime Invariants
15
+
16
+ 1. One logical conversation owns one durable session ID.
17
+ 2. A continuation resumes that session; it does not replay the task from the
18
+ beginning.
19
+ 3. The current user request is authoritative, but prior verified evidence and
20
+ unfinished requirements remain available.
21
+ 4. Tool side effects are accepted only through explicit, validated contracts.
22
+ 5. A final answer is successful only when its required evidence exists.
23
+ 6. Provider failure is not task success and is not silently converted into it.
24
+ 7. Provider handoff preserves the session and goal rather than duplicating
25
+ external work.
26
+
27
+ ## Durable Goal Contract
28
+
29
+ Every session stores a versioned goal contract in `state.json`:
30
+
31
+ - `revision`: advances for every resumed request;
32
+ - `currentHash` and `currentPreview`: identify the authoritative request
33
+ without placing private raw prompts in operational indexes;
34
+ - `history`: bounded revision history with previous-goal and plan hashes;
35
+ - `status`: `active`, `completed`, `paused`, or `failed`;
36
+ - `lifecycle`: bounded status transitions with reason and timestamp.
37
+
38
+ Accepted direct answers, evidence-backed assistant answers, and `finish` tool
39
+ calls mark the current revision completed. User interruption, step exhaustion,
40
+ or a repairable tool-contract stop pauses it. Provider timeout, preflight
41
+ failure, and unexpected runtime errors mark it failed. A new continuation
42
+ reactivates the same session at the next revision.
43
+
44
+ Inspect it without parsing terminal output:
45
+
46
+ ```bash
47
+ aginti sessions list
48
+ aginti sessions show SESSION_ID
49
+ ```
50
+
51
+ Machine responses also expose `goalRevision` and `goalStatus`.
52
+
53
+ ## Append-Only Evidence And Atomic State
54
+
55
+ The canonical session lives under
56
+ `~/.agintiflow/sessions/<session-id>/`. Atomic `state.json` snapshots hold the
57
+ resumable working state. Append-only JSONL events preserve lifecycle, tool,
58
+ model, evidence, inbox, and recovery facts. A project-local index points to the
59
+ canonical session without copying private history.
60
+
61
+ Malformed state is treated as corruption, not as a missing session. Event
62
+ appends are serialized, state saves are atomic and fsynced, and the SQLite
63
+ session index uses WAL plus a bounded busy timeout for multiple frontends.
64
+
65
+ ## Safe Interruption And Inbox
66
+
67
+ `aginti queue SESSION_ID "instruction"`, the CLI composer, and the web app all
68
+ write to the same durable inbox. The runner consumes messages only at safe
69
+ boundaries:
70
+
71
+ - before a model step;
72
+ - after a tool action;
73
+ - before accepting completion.
74
+
75
+ This permits an operator or chat bridge to correct, narrow, extend, or cancel
76
+ the current direction without mutating an in-flight model request or replaying
77
+ completed side effects. ASAP input and after-finish input remain distinct.
78
+
79
+ ## Progressive Context And Tool Disclosure
80
+
81
+ The first model turn receives a focused runtime contract, the current goal,
82
+ relevant project instructions, and only the skills selected for that request.
83
+ It does not receive an indiscriminate dump of every skill or workspace file.
84
+
85
+ Tool access is progressively disclosed:
86
+
87
+ - direct chat can answer without tools;
88
+ - unfamiliar repositories begin with bounded inspection and search;
89
+ - relevant routines are preferred over rebuilding mature workflows;
90
+ - a small batch of up to four independent read-only calls may run in one turn;
91
+ - writes, GUI actions, network writes, and irreversible operations remain
92
+ isolated, ordered calls.
93
+
94
+ Workspace search is bounded by file count, bytes, and elapsed time. Default
95
+ root scans skip generated outputs, artifacts, and private data, while an
96
+ explicit path remains inspectable when the user actually requested it.
97
+
98
+ ## Truthful Completion
99
+
100
+ Completion is checked against an evidence scope. Read-only answers and plans do
101
+ not need irrelevant command or visual evidence. File creation, publication,
102
+ GUI work, and other external actions require the corresponding evidence.
103
+
104
+ The runtime provides:
105
+
106
+ - one bounded retry when a model claims completion without required evidence;
107
+ - one bounded repair for an empty model answer;
108
+ - a concise fallback only when runtime evidence already verifies completion;
109
+ - a resumable stop instead of a false success when evidence remains absent;
110
+ - short-circuiting after a blocked tool so later calls in that batch are not
111
+ dispatched against an invalid state.
112
+
113
+ ## Provider Attribution And Handoff
114
+
115
+ `npm run eval:provider-attribution` compares a raw provider answer with the
116
+ same provider through AgInTiFlow. Results are classified as:
117
+
118
+ - `both_pass`: provider and orchestration both satisfy the contract;
119
+ - `orchestration_loss_or_help`: the runtime changed the outcome, so inspect
120
+ prompting, context, tools, or completion gates;
121
+ - `provider_limit`: the raw model itself failed the contract.
122
+
123
+ This prevents orchestration bugs from being blamed on DeepSeek or a local
124
+ model, and prevents weak model output from triggering unnecessary framework
125
+ rewrites.
126
+
127
+ Embedding hosts should use a provider chain while preserving the same AgInTi
128
+ session. LabCanvas uses DeepSeek first and LocalLLM second. A handoff occurs
129
+ only after a categorized provider failure and must not replay verified side
130
+ effects. Codex and Claude remain explicit opt-in backends rather than hidden
131
+ fallbacks.
132
+
133
+ ## Machine Host Protocol
134
+
135
+ The supported subprocess boundary is:
136
+
137
+ ```bash
138
+ printf '%s' 'Do the task.' | aginti run --stdin --json [runtime options]
139
+ printf '%s' 'Continue with this correction.' \
140
+ | aginti resume SESSION_ID --stdin --json
141
+ ```
142
+
143
+ Exactly one JSON object is emitted. It includes success/failure, session,
144
+ provider, model, resume state, goal revision/status, result, stop state, and
145
+ reason. Interactive banners, update checks, and web startup are suppressed.
146
+ Stopped or failed runs always have `ok: false` even when they contain a useful
147
+ human-facing explanation.
148
+
149
+ ## Established Routines, Not Reinvention
150
+
151
+ AgInTiFlow is the reasoning and supervision layer. Domain work should use the
152
+ existing routine owned by the relevant project:
153
+
154
+ - LazyEdit and AutoPublish for subtitle-aware video publication;
155
+ - LALACHAN/Xiaoyunque for story and video generation;
156
+ - Musia for music and song-first MV workflows;
157
+ - LabCanvas CAD, KiCad, Blender, TeX/PDF, presentation, grant, and figure
158
+ routines;
159
+ - WeChat and WeCom transports for exact-chat delivery.
160
+
161
+ The agent selects, invokes, monitors, and verifies those routines. It does not
162
+ replace them with prompt-specific shell fragments.
163
+
164
+ ## Acceptance Gates
165
+
166
+ A primary-backend release is accepted only after all of these pass:
167
+
168
+ 1. Syntax and deterministic runtime tests.
169
+ 2. Durable run/resume, goal lifecycle, inbox, and session-isolation tests.
170
+ 3. Progressive tool, bounded search, and truthful-completion tests.
171
+ 4. Raw-provider versus agent attribution for DeepSeek and LocalLLM.
172
+ 5. A live read-only established-routine task with no accidental write.
173
+ 6. A live exact artifact-creation task with byte-level verification.
174
+ 7. A live LocalLLM direct-response task.
175
+ 8. Host project tests, chat-bridge self-tests, package dry run, installed
176
+ version check, and existing-runtime restart.
177
+
178
+ These gates keep the architecture fast for ordinary chat, capable for long
179
+ tasks, and honest when a provider or external service is unavailable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.199",
3
+ "version": "0.20.200",
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",
@@ -122,7 +122,7 @@
122
122
  "scripts": {
123
123
  "start": "node run.js",
124
124
  "web": "node web.js",
125
- "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check bin/aginti-public-research.js && node --check bin/aginti-safe-chat.js && node --check src/*.js && node --check src/mcp/*.js && node --check public/*.js && node --check scripts/postinstall-webapp.js && node --check scripts/seed-supervised-homework.js && node --check scripts/smoke-agentlink.js && node --check scripts/smoke-execution-policy.js && node --check scripts/smoke-math-rendering.js && node --check scripts/smoke-mcp.js && node --check scripts/smoke-public-research-wrapper.js && node --check scripts/smoke-runtime-core.js && node --check scripts/smoke-safe-chat.js && node --check scripts/smoke-web-ui.js && node --check scripts/smoke-scs-evidence-visibility.js && node --check scripts/fixtures/mcp-stdio-smoke-server.mjs",
125
+ "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check bin/aginti-public-research.js && node --check bin/aginti-safe-chat.js && node --check src/*.js && node --check src/mcp/*.js && node --check public/*.js && node --check scripts/postinstall-webapp.js && node --check scripts/seed-supervised-homework.js && node --check scripts/eval-provider-attribution.js && node --check scripts/smoke-agentlink.js && node --check scripts/smoke-execution-policy.js && node --check scripts/smoke-math-rendering.js && node --check scripts/smoke-mcp.js && node --check scripts/smoke-public-research-wrapper.js && node --check scripts/smoke-runtime-core.js && node --check scripts/smoke-safe-chat.js && node --check scripts/smoke-web-ui.js && node --check scripts/smoke-scs-evidence-visibility.js && node --check scripts/fixtures/mcp-stdio-smoke-server.mjs",
126
126
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
127
127
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
128
128
  "smoke:dynamic-step-budget": "node scripts/smoke-dynamic-step-budget.js",
@@ -168,13 +168,14 @@
168
168
  "smoke:web-port-fallback": "node scripts/smoke-web-port-fallback.js",
169
169
  "smoke:autoupdate": "node scripts/smoke-auto-update.js",
170
170
  "eval:local-first-agent": "node scripts/local-first-agent-eval.mjs",
171
+ "eval:provider-attribution": "node scripts/eval-provider-attribution.js",
171
172
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
172
173
  "postinstall": "node scripts/postinstall-webapp.js",
173
174
  "supervision:seed": "node scripts/seed-supervised-homework.js",
174
175
  "storage:migrate": "node bin/aginti-cli.js storage migrate",
175
176
  "publish:env": "node scripts/npm-publish-from-env.js publish --access public",
176
177
  "publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
177
- "test": "npm run check && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
178
+ "test": "npm run check && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
178
179
  "pack:dry-run": "npm pack --dry-run",
179
180
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
180
181
  },
@@ -918,6 +918,23 @@ try {
918
918
  ) {
919
919
  throw new Error("resume subcommand options after the session id should remain options, not prompt text");
920
920
  }
921
+ const machineResume = parseResumeCommandArgs([
922
+ "web-agent-smoke",
923
+ "--stdin",
924
+ "--json",
925
+ "--provider",
926
+ "mock",
927
+ ]);
928
+ if (
929
+ machineResume.sessionId !== "web-agent-smoke" ||
930
+ !machineResume.stdin ||
931
+ !machineResume.json ||
932
+ machineResume.optionArgv.includes("--stdin") ||
933
+ machineResume.optionArgv.includes("--json") ||
934
+ parseArgs(machineResume.optionArgv).provider !== "mock"
935
+ ) {
936
+ throw new Error("resume machine transport flags should be parsed outside the durable runtime patch");
937
+ }
921
938
  const resumeRuntimeOptions = [
922
939
  "--provider",
923
940
  "mock",
@@ -18,6 +18,7 @@ import { SessionStore } from "../src/session-store.js";
18
18
  import {
19
19
  attachToolContract,
20
20
  createToolContract,
21
+ safeSequentialToolBatchLimit,
21
22
  toolContractFromResponse,
22
23
  validateToolCallBatch,
23
24
  } from "../src/tool-contract.js";
@@ -816,6 +817,81 @@ assert(
816
817
  "valid offered tool call did not satisfy its exact schema"
817
818
  );
818
819
 
820
+ const safeReadDescriptors = [
821
+ {
822
+ type: "function",
823
+ function: {
824
+ name: "read_file",
825
+ description: "Read a file.",
826
+ parameters: {
827
+ type: "object",
828
+ properties: { path: { type: "string" } },
829
+ required: ["path"],
830
+ additionalProperties: false,
831
+ },
832
+ },
833
+ },
834
+ {
835
+ type: "function",
836
+ function: {
837
+ name: "search_files",
838
+ description: "Search files.",
839
+ parameters: {
840
+ type: "object",
841
+ properties: { path: { type: "string" }, query: { type: "string" } },
842
+ required: ["path", "query"],
843
+ additionalProperties: false,
844
+ },
845
+ },
846
+ },
847
+ {
848
+ type: "function",
849
+ function: {
850
+ name: "list_files",
851
+ description: "List files.",
852
+ parameters: {
853
+ type: "object",
854
+ properties: { path: { type: "string" } },
855
+ required: ["path"],
856
+ additionalProperties: false,
857
+ },
858
+ },
859
+ },
860
+ ];
861
+ const safeReadCalls = [
862
+ contractCall("safe-read", "read_file", { path: "AGENTS.md" }),
863
+ contractCall("safe-search", "search_files", { path: ".", query: "lazyedit" }),
864
+ contractCall("safe-list", "list_files", { path: "." }),
865
+ ];
866
+ const safeReadContract = createToolContract(safeReadDescriptors);
867
+ assert(safeSequentialToolBatchLimit(safeReadCalls) === 4, "safe read batch did not receive the bounded sequential allowance");
868
+ assert(
869
+ validateToolCallBatch(safeReadCalls, safeReadContract, {
870
+ maxToolCalls: safeSequentialToolBatchLimit(safeReadCalls),
871
+ }).ok,
872
+ "valid safe read batch did not pass the exact per-turn contract"
873
+ );
874
+ const mixedReadWriteCalls = [
875
+ safeReadCalls[0],
876
+ contractCall("unsafe-write", "write_file", { path: "blocked.txt", content: "bad" }),
877
+ ];
878
+ assert(safeSequentialToolBatchLimit(mixedReadWriteCalls) === 1, "mixed read/write batch escaped the single-call limit");
879
+ assert(
880
+ !validateToolCallBatch(mixedReadWriteCalls, createToolContract([...safeReadDescriptors, strictWriteDescriptor]), {
881
+ maxToolCalls: safeSequentialToolBatchLimit(mixedReadWriteCalls),
882
+ }).ok,
883
+ "mixed read/write batch unexpectedly passed"
884
+ );
885
+ const oversizedReadCalls = Array.from({ length: 5 }, (_, index) =>
886
+ contractCall(`read-${index}`, "read_file", { path: `file-${index}.txt` })
887
+ );
888
+ assert(
889
+ !validateToolCallBatch(oversizedReadCalls, safeReadContract, {
890
+ maxToolCalls: safeSequentialToolBatchLimit(oversizedReadCalls),
891
+ }).ok,
892
+ "oversized read batch escaped the bounded allowance"
893
+ );
894
+
819
895
  for (const [label, call, expectedCode] of [
820
896
  [
821
897
  "hidden dryRun",
@@ -4,6 +4,8 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { machineRunPayload } from "../src/cli.js";
8
+ import { showProjectSession } from "../src/project.js";
7
9
 
8
10
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
11
  const home = await fs.mkdtemp(path.join(os.tmpdir(), "aginti-run-stdin-"));
@@ -25,8 +27,18 @@ const machineOptions = [
25
27
  "host",
26
28
  ];
27
29
 
28
- async function runMachine(label, runArgs, stdin = "") {
29
- const command = [cliPath, "run", ...runArgs];
30
+ const stoppedPayload = machineRunPayload({
31
+ sessionId: "stopped-session",
32
+ result: "I stopped safely instead of claiming success.",
33
+ stopped: true,
34
+ reason: "tool_contract_violation",
35
+ });
36
+ if (stoppedPayload.ok !== false || stoppedPayload.failed !== true || stoppedPayload.stopped !== true) {
37
+ throw new Error(`stopped machine run was reported as success: ${JSON.stringify(stoppedPayload)}`);
38
+ }
39
+
40
+ async function runMachine(label, commandArgs, stdin = "") {
41
+ const command = [cliPath, ...commandArgs];
30
42
  const output = await new Promise((resolve, reject) => {
31
43
  const child = spawn(process.execPath, command, {
32
44
  cwd: home,
@@ -70,25 +82,76 @@ async function runMachine(label, runArgs, stdin = "") {
70
82
  throw new Error(`${label} leaked interactive metadata\n${output.stdout}`);
71
83
  }
72
84
  if (output.stderr.trim()) throw new Error(`${label} leaked stderr\n${output.stderr}`);
85
+ return payload;
73
86
  }
74
87
 
75
88
  try {
76
89
  await runMachine(
77
90
  "explicit stdin machine run",
78
- ["--stdin", ...machineOptions],
91
+ ["run", "--stdin", ...machineOptions],
79
92
  "Reply briefly that the explicit stdin transport smoke completed."
80
93
  );
81
94
  await runMachine(
82
95
  "positional prompt machine run",
83
- [...machineOptions, "Reply briefly that the positional prompt smoke completed."]
96
+ ["run", ...machineOptions, "Reply briefly that the positional prompt smoke completed."]
84
97
  );
85
98
  await runMachine(
86
99
  "implicit piped stdin machine run",
87
- machineOptions,
100
+ ["run", ...machineOptions],
88
101
  "Reply briefly that the implicit piped stdin smoke completed."
89
102
  );
103
+ const initial = await runMachine(
104
+ "initial resumable machine run",
105
+ ["run", "--stdin", ...machineOptions],
106
+ "Remember the marker AGINTI_MACHINE_RESUME and reply briefly."
107
+ );
108
+ const resumed = await runMachine(
109
+ "resumed stdin machine run",
110
+ ["resume", initial.sessionId, "--stdin", "--json"],
111
+ "Reply briefly that this saved session resumed successfully."
112
+ );
113
+ if (resumed.sessionId !== initial.sessionId) {
114
+ throw new Error(`machine resume changed session id: ${initial.sessionId} -> ${resumed.sessionId}`);
115
+ }
116
+ if (initial.provider !== "mock" || resumed.provider !== "mock" || resumed.resumed !== true) {
117
+ throw new Error(`machine payload omitted provider/resume diagnostics: ${JSON.stringify({ initial, resumed })}`);
118
+ }
119
+ if (
120
+ initial.goalRevision !== 1 ||
121
+ initial.goalStatus !== "completed" ||
122
+ resumed.goalRevision !== 2 ||
123
+ resumed.goalStatus !== "completed"
124
+ ) {
125
+ throw new Error(`machine payload omitted durable goal lifecycle: ${JSON.stringify({ initial, resumed })}`);
126
+ }
127
+ const stored = await showProjectSession(home, initial.sessionId);
128
+ if (stored?.goalRevision !== 2 || stored?.goalStatus !== "completed") {
129
+ throw new Error(`durable goal revision/lifecycle was not completed: ${stored?.goalRevision}/${stored?.goalStatus}`);
130
+ }
131
+ if (!stored.events.some((event) => event.type === "goal.updated" && event.data?.revision === 2)) {
132
+ throw new Error("durable goal update event was not recorded");
133
+ }
134
+ const state = JSON.parse(
135
+ await fs.readFile(path.join(home, ".agintiflow", "sessions", initial.sessionId, "state.json"), "utf8")
136
+ );
137
+ if (
138
+ state.meta?.goalContract?.history?.length !== 2 ||
139
+ state.meta?.goalContract?.lifecycle?.length !== 4 ||
140
+ state.meta.goalContract.lifecycle.at(-1)?.status !== "completed" ||
141
+ state.meta.goalContract.lifecycle.at(-2)?.status !== "active" ||
142
+ state.meta.goalContract.currentPreview !== state.goal ||
143
+ state.meta.goalContract.currentHash !== state.meta.goalContract.history.at(-1)?.hash
144
+ ) {
145
+ throw new Error("durable goal ledger did not retain both revisions and the authoritative current goal");
146
+ }
147
+ const continuation = state.messages.find(
148
+ (message) => message.role === "user" && String(message.content || "").startsWith("Continue with this new request:")
149
+ );
150
+ if (!continuation || continuation.content.length >= 8_000) {
151
+ throw new Error(`focused continuation did not use bounded context: ${continuation?.content?.length || 0}`);
152
+ }
90
153
  } finally {
91
154
  await fs.rm(home, { recursive: true, force: true });
92
155
  }
93
156
 
94
- console.log("run input precedence smoke passed");
157
+ console.log("run and resume input precedence smoke passed");
@@ -110,6 +110,12 @@ assert(selectedIds("review this PR architecture without editing").includes("code
110
110
  assert(selectedIds("supervise a student agent in tmux and verify its artifacts", "supervision").includes("supervision-student"), "supervision prompt did not select supervision-student");
111
111
  assert(selectedIds("supervision").includes("supervision-student"), "single-word supervision prompt did not select supervision-student");
112
112
  assert(!selectedIds("supervision").includes("r-stan"), "single-word supervision prompt incorrectly selected r-stan");
113
+ assert(
114
+ selectedIds(
115
+ "Create acceptance.txt containing exactly AGINTI_STANDALONE_OK followed by one newline, then read it back."
116
+ ).length === 0,
117
+ "generic file work selected unrelated domain skills"
118
+ );
113
119
 
114
120
  const prompt = formatSkillsForPrompt(selectSkillsForGoal("write latex manuscript with figures", { taskProfile: "latex", limit: 3 }));
115
121
  assert(prompt.includes("A skill is Markdown guidance"), "skill prompt does not explain skill semantics");
@@ -139,6 +139,34 @@ try {
139
139
  assert.match(explanation.result.result, /stops recursive calls/i);
140
140
  assert(explanation.events.some((event) => event.type === "session.finished"));
141
141
  assert(!explanation.events.some((event) => event.type === "completion.evidence_rejected"));
142
+ assert(
143
+ String(explanation.state.messages.find((message) => message.role === "system")?.content || "").length < 10_000,
144
+ "focused runtime prompt did not use progressive disclosure"
145
+ );
146
+ assert(
147
+ Math.max(
148
+ ...explanation.state.messages
149
+ .filter((message) => /^Step \d+\/\d+ .*Latest runtime snapshot:/i.test(String(message.content || "")))
150
+ .map((message) => String(message.content || "").length)
151
+ ) < 2_000,
152
+ "focused runtime snapshot repeated the full capability manual"
153
+ );
154
+
155
+ const quotedChatClassification = await runCase({
156
+ id: "quoted-chat-classification",
157
+ taskProfile: "chatops",
158
+ goal: [
159
+ "Context:",
160
+ "- Message 1: Generate a new video from the supplied video, but do not publish.",
161
+ "- Message 2: Return the generated MP4 to the same chat.",
162
+ 'Return exactly one JSON object and no prose: {"intent":"generation_only","publish":false}.',
163
+ ].join("\n"),
164
+ responses: [assistant('{"intent":"generation_only","publish":false}')],
165
+ });
166
+ assert.equal(quotedChatClassification.calls.length, 1);
167
+ assert.equal(quotedChatClassification.result.stopped, undefined);
168
+ assert.equal(quotedChatClassification.result.result, '{"intent":"generation_only","publish":false}');
169
+ assert(!quotedChatClassification.events.some((event) => event.type === "completion.evidence_rejected"));
142
170
 
143
171
  const proseOnlyAction = await runCase({
144
172
  id: "prose-only-action",
@@ -188,6 +216,45 @@ try {
188
216
  assert(verifiedAction.events.some((event) => event.type === "session.finished"));
189
217
  assert(!verifiedAction.events.some((event) => event.type === "completion.repair_requested"));
190
218
 
219
+ const verifiedEmptyCompletion = await runCase({
220
+ id: "verified-empty-completion",
221
+ goal: "Execute the shell command pwd and report the output.",
222
+ taskProfile: "shell",
223
+ allowShellTool: true,
224
+ responses: [
225
+ assistant("", [toolCall("run-empty", "run_command", { command: "pwd" })]),
226
+ assistant(""),
227
+ assistant(""),
228
+ ],
229
+ });
230
+ assert.equal(verifiedEmptyCompletion.calls.length, 3);
231
+ assert.equal(verifiedEmptyCompletion.result.stopped, undefined);
232
+ assert.match(verifiedEmptyCompletion.result.result, /verified.*runtime evidence/i);
233
+ assert.equal(
234
+ verifiedEmptyCompletion.events.filter((event) => event.type === "completion.empty_response_repair_requested").length,
235
+ 1
236
+ );
237
+ assert.equal(
238
+ verifiedEmptyCompletion.events.filter((event) => event.type === "completion.verified_fallback").length,
239
+ 1
240
+ );
241
+ assert(!verifiedEmptyCompletion.result.result.includes("No tool call returned"));
242
+
243
+ const unusableEmptyChat = await runCase({
244
+ id: "unusable-empty-chat",
245
+ goal: "Explain why recursion needs a base case.",
246
+ responses: [assistant(""), assistant("")],
247
+ });
248
+ assert.equal(unusableEmptyChat.calls.length, 2);
249
+ assert.equal(unusableEmptyChat.result.stopped, true);
250
+ assert.equal(unusableEmptyChat.result.reason, "empty_model_response");
251
+ assert.equal(
252
+ unusableEmptyChat.events.filter((event) => event.type === "completion.empty_response_repair_requested").length,
253
+ 1
254
+ );
255
+ assert(!unusableEmptyChat.events.some((event) => event.type === "session.finished"));
256
+ assert(!unusableEmptyChat.result.result.includes("No tool call returned"));
257
+
191
258
  const resumedAction = await runCase({
192
259
  id: "verified-action",
193
260
  goal: "Run printf 4 and report the output.",