Vancetope — Marvin Think Engine
Marvin is the deep-think Engine. It builds a dynamic, persistent Task-Tree, where each node is an autonomous marvin-worker. Each node goes through a deterministic 5-phase state machine (
SCOPE → REFLECT → POST_CHILDREN → CONCLUDE → VALIDATE). The plan IS the tree — there is no separate plan structure; before each LLM call, the Engine renders a live view of the tree as context. Brain the size of a planet, and for once it’s allowed to use it on the actual problem.
1. Role and Classification
Marvin v2 differs structurally from the other Engines:
| Engine | Data Model | Character |
|---|---|---|
arthur |
Chat History (linear) | Reactive Session-Chat |
ford |
Chat History (linear) | Generalist-Worker, one question → one answer |
vogon |
State Machine + Strategy-State (static) | Deterministic Multi-Phase Plan |
marvin |
Task-Tree (dynamically growing, Mongo-persistent) | Autonomous Worker-Nodes with 5-Phase Lifecycle |
Marvin’s Use Cases:
- “Research nuclear power and write me a report” — Root-Worker
decomposes by aspects (History / Tech / Ecology), calls
web-researchvia CALL_RECIPE, synthesizes. - “Analyze these 5 PDFs for contradictions” — Worker spawns per-PDF-Children, each loads its PDF, then POST_CHILDREN synthesizes.
- “Plan and implement Feature X” — Worker decomposes into Requirements Analysis → Design → Tasks per component.
What Marvin is not:
- Not a Chat Orchestrator (that’s Arthur)
- Not a State Machine Runner with fixed phases (that’s Vogon)
- Not a Generalist-Worker with Tool-Loop (that’s Ford) — Marvin calls specialized Recipes via CALL_RECIPE; it plans, reflects, validates itself.
1.1 v1 → v2 Transition
Marvin v2 is a complete rewrite. What has changed:
| v1 | v2 |
|---|---|
| TaskKinds: PLAN, WORKER, EXPAND_FROM_DOC, USER_INPUT, AGGREGATE | TaskKinds: WORKER, EXPAND_FROM_DOC, USER_INPUT |
| PLAN-Node decomposed the entire plan upfront | Root is a WORKER, the plan grows successively |
| AGGREGATE-Node synthesized siblings | POST_CHILDREN phase of the Parent synthesizes |
WORKER_SCHEMA_POSTFIX on all Workers |
Phase-Schemas per LLM-Call; Special-Recipes unchanged |
allowedSubTaskRecipes / recipesOnlyViaExpand |
availableRecipes (CALL_RECIPE-Whitelist) |
| Static plan in JSON schemas, KIND-Blocks in Prompts | Dynamic plan snapshot as live view, no KIND-boilerplate |
The trigger for the rewrite was a recurring live observation
(2026-05-24): specialized Workers (web-research, analyze, …)
to which the Marvin-Output-Contract was appended, reproducibly
chose NEEDS_SUBTASKS as an easy way out, instead of
completing their specific mandate. An LLM personality cannot
reliably be “specialized Worker + Routing Decision-Maker” in
one response. The separation into marvin-worker (Orchestrator)
- CALL_RECIPE (specialized Workers in native mode) solves this structurally.
2. Data Model
2.1 Engine State
Marvin lives outside the ThinkProcessDocument. The Process
carries standard fields + engineParams. The actual tree is located
in a separate Collection.
// vance-shared/src/main/java/de/mhus/vance/shared/marvin/MarvinNodeDocument.java
@Document(collection = "marvin_nodes")
public class MarvinNodeDocument {
@Id String id;
String tenantId;
String processId; // Marvin's ThinkProcess id
@Nullable String parentId; // null for root
int position; // sort order among siblings
String goal; // what this node should achieve
TaskKind taskKind; // WORKER | EXPAND_FROM_DOC | USER_INPUT
Map<String, Object> taskSpec; // kind-specific spec
NodeStatus status; // PENDING | RUNNING | WAITING | DONE | FAILED | SKIPPED
Map<String, Object> artifacts; // result, summary, partialResult, recipeReplies
@Nullable String failureReason;
@Nullable String spawnedProcessId; // for legacy reverse-lookup
@Nullable String inboxItemId; // USER_INPUT → inbox-item id
// ───── State-machine cursor ─────
@Nullable WorkerPhase currentPhase; // SCOPE | REFLECT | POST_CHILDREN | CONCLUDE | VALIDATE
int reflectIter; // 0..3
int validateIter; // 0..2
int concludeRetries; // 0..2
boolean awaitingPostChildren; // children-fanout in flight
@Nullable String candidateResult; // CONCLUDE candidate, awaiting VALIDATE
List<String> calledSubProcessIds; // CALL_RECIPE sub-processes
List<PhaseIteration> phaseHistory; // audit trail
}
MarvinNodeService is the only access to the Collection.
Indexes: (processId, status, position), (processId, parentId,
position), spawnedProcessId, inboxItemId,
calledSubProcessIds (sparse).
2.2 TaskKind (vance-api)
public enum TaskKind {
WORKER, // marvin-worker with 5-phase lifecycle
EXPAND_FROM_DOC, // deterministic fanout from list/tree/records-Document
USER_INPUT // Inbox-Item Wait-Point
}
PLAN and AGGREGATE from v1 are removed without replacement — the Root is a WORKER, POST_CHILDREN replaces AGGREGATE.
2.3 Phase Records (vance-api)
Five records, one per phase. All non-null-by-default (JSpecify) with explicitly nullable fields:
ScopeOutput(action, recipeCall, newTasks, userInput, problem, reason)ReflectOutput(action, recipeCall, newTasks, userInput, problem, reason)PostChildrenOutput(action, newTasks, problem, reason)ConcludeOutput(result, postActions, reason)ValidateOutput(verdict, issues, hint, reason)
Plus Action-Enums: ScopeAction, ReflectAction,
PostChildrenAction, ValidateVerdict.
Plus Helper-Records: RecipeCall, NewTaskSpec, UserInputSpec,
PostActionSpec, PhaseIteration.
3. Lifecycle
MarvinEngine implements ThinkEngine. Lifecycle:
start → createRoot(WORKER) + scheduleTurn
runTurn → drainPending → findNextActionableNode → executeNode → idle
steer → scheduleTurn (Marvin async, no synchronous Reply)
stop → closeProcess(STOPPED)
Important: Marvin’s asyncSteer() = true. Parent-Engines (Arthur,
Vogon) do NOT block on a Marvin-process_steer — they
queue the input and wait for the DONE-ProcessEvent.
4. Node Status Transitions
PENDING ──→ RUNNING ──(sync done)──→ DONE
│
├──(NEEDS_SUBTASKS)──→ DONE (awaitingPostChildren=true; DFS-transparent)
│ │
│ └─(children all terminal, sweeper resurrects)──→
│ PENDING (phase=POST_CHILDREN) → RUNNING
│
├──(NEEDS_USER_INPUT)─→ DONE (after USER_INPUT sibling created; sibling holds WAITING)
│
├──(BLOCKED_BY_PROBLEM / HARD_FAIL / parse error)──→ FAILED
│
└──(idle / replan)──→ SKIPPED
WORKER-Nodes go directly from PENDING → RUNNING; they remain RUNNING during the phase loop.
Important — no WAITING phase for NEEDS_SUBTASKS-Parents.
After NEEDS_SUBTASKS, the Engine marks the Parent as DONE with
awaitingPostChildren=true. DONE is DFS-transparent — the
walker descends into the newly spawned Children. As soon as all
Children are terminal, the reactivatePostChildrenParents-
Sweeper at the top of each runTurn resets the Parent to
PENDING + currentPhase=POST_CHILDREN. The next DFS-Pick
then drives it through POST_CHILDREN → CONCLUDE → VALIDATE.
WAITING only exists for USER_INPUT-Nodes (Inbox-Item
spawned, waiting for response).
5. The Five Phases
Each WORKER-Node goes through these phases in exactly this order with hard iteration caps. Phase outputs are JSON objects with phase-specific schemas.
┌─────────┐
│ SCOPE │ ← what needs to be done?
└────┬────┘
│
┌────────┼─────────┬─────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
CALL_ PROCEED NEEDS_ NEEDS_ BLOCKED_
RECIPE TO_ SUBTASKS USER_INPUT BY_PROBLEM
│ CONCLUDE (TERMINAL)
│ │ │ │
│ │ ▼ ▼
│ │ [children USER_INPUT
│ │ execute] sibling (TERMINAL)
│ │ │
│ │ ▼
│ │ POST_CHILDREN
│ │ │
│ │ ┌─────┴────┬────────────┐
│ │ ▼ ▼ ▼
│ │ PROCEED NEEDS_ BLOCKED_
│ │ TO_ SUBTASKS BY_PROBLEM
│ │ CONCLUDE (bounded (TERMINAL)
│ │ by depth)
│ │ │ │
│ │ │ └─→ another level
│ │ ▼
│ ▼ ▼
▼ CONCLUDE
REFLECT │
(iter ≤ 3) ▼
│ VALIDATE (iter ≤ 2)
│ │
│ ┌─────┼────────────┬──────────┐
│ ▼ ▼ ▼ ▼
│ PASS RETRY_ NEED_ HARD_
│ │ CONCLUDE MORE_ FAIL
│ ▼ │ DATA (TERMINAL)
│ DONE back to │
│ CONCLUDE back to
│ REFLECT (if cap left)
└──── (CALL_RECIPE next iteration)
5.1 SCOPE — initial decision
First LLM call. Sees: Goal, availableRecipes list, Live-Plan- Snapshot. Immediately decides the initial move.
Output:
{"action": "CALL_RECIPE" | "PROCEED_TO_CONCLUDE" | "NEEDS_SUBTASKS"
| "NEEDS_USER_INPUT" | "BLOCKED_BY_PROBLEM",
"recipeCall": {"recipe": "...", "steerContent": "..."},
"newTasks": [{"goal":"...","taskKind":"WORKER","taskSpec":{}}],
"userInput": {"type":"DECISION","title":"...","body":"..."},
"problem": "<short>",
"reason": "<one-line>"}
5.2 REFLECT — post-CALL_RECIPE evaluation
Reached via CALL_RECIPE. Cap: 3 iterations per node.
Sees Goal + Plan + previous Recipe-Replies (each as
<<< Result of CALL_RECIPE('...'): … >>> blocks).
Schema identical to SCOPE (same Actions). If Cap exhausted (3 CALL_RECIPEs already run) → forced PROCEED_TO_CONCLUDE.
5.3 POST_CHILDREN — children-fanout synthesis
Reached after NEEDS_SUBTASKS, as soon as all Children are terminal. Sees Plan-Snapshot + explicit Children-Results-Block.
Output:
{"action": "PROCEED_TO_CONCLUDE" | "NEEDS_SUBTASKS" | "BLOCKED_BY_PROBLEM",
"newTasks": [...],
"problem": "...",
"reason": "..."}
NEEDS_SUBTASKS here only allowed if Tree-Depth < maxTreeDepth
(default 5). Otherwise forced PROCEED_TO_CONCLUDE.
5.4 CONCLUDE — final answer
Synthesis phase. Produces the candidate result + optional engine-side postActions.
Output:
{"result": "<complete Markdown response>",
"postActions": [{"tool":"doc_create",
"args":{"path":"...","kind":"text","content":"{{ node.result }}"}}],
"reason": "..."}
CONCLUDE may go through up to 2 Retries (RETRY_CONCLUDE- Verdict from VALIDATE).
5.5 VALIDATE — critical review
Fresh LLM call with critical prompt. Sees Goal + Candidate- Result + Plan-Snapshot. Cap: 2 iterations per node.
Output:
{"verdict": "PASS" | "RETRY_CONCLUDE" | "NEED_MORE_DATA" | "HARD_FAIL",
"issues": ["<problem 1>", "<problem 2>"],
"hint": "<what to improve>",
"reason": "..."}
Verdict-Routing:
PASS→ markDone(candidate) + runPostActions → TERMINAL DONERETRY_CONCLUDE→ back to CONCLUDE;issues+hintas Memory-Hint. IfconcludeRetries< 2.NEED_MORE_DATA→ back to REFLECT withhint. IfreflectIter< cap.HARD_FAIL→ markFailed → TERMINAL FAILED. HARD_FAIL wins against Cap-Forced-DONE — at iter 2 with HARD_FAIL, it remains FAILED.
If validateIter >= cap: forced markDone(last Candidate) +
Audit-Warning.
5.6 Iteration-Cap Overview
| Phase | Cap | On Cap Hit |
|---|---|---|
| SCOPE | 1 | — |
| REFLECT | 3 CALL_RECIPEs | forced PROCEED_TO_CONCLUDE |
| POST_CHILDREN | 1 + (tree-depth bounded NEEDS_SUBTASKS) | forced PROCEED_TO_CONCLUDE |
| CONCLUDE | 1 initial + 2 Re-Conclude | last Candidate accepted |
| VALIDATE | 2 | see §5.5 |
Worst-Case LLM-Calls per node: 1+3+1+3+2 = 10 Calls. Best-Case: 1+1+1 = 3 Calls (SCOPE→CONCLUDE→VALIDATE PASS). Typical: 1+1+1+1 = 4 Calls (with 1 Recipe-Call).
6. Plan-Snapshot — the dynamic Plan
The Tree IS the Plan. There is no separate Plan data structure.
Before each LLM call, PlanSnapshotRenderer (vance-brain)
renders a compact text view from the Mongo nodes.
6.1 Render Format
┌─ LIVE PLAN (current state — supersedes any earlier view) ───────┐
│ ROOT (running) — Nuclear Power Deep-Research │
│ #1 (DONE) — History & Political Context │
│ └ First reactors 1942; Chernobyl 1986; Phase-Out 2011 │
│ #2 (running) — Tech Status of Modern Reactors [YOU ARE HERE] │
│ #2.1 (DONE) — SMR Concepts │
│ └ SMRs are smaller modules, NuScale certified │
│ #2.2 (running) — ITER Update │
│ #3 (planned) — Ecological Impacts │
└─ end of plan ───────────────────────────────────────────────────
- Line per node:
<path> (<status>) — <title> - For DONE: additionally
└ <truncated summary, max 200 chars> - Current node:
[YOU ARE HERE] - Hard-Cap: 4000 chars total
6.2 Pruning for Large Trees
If the full render would be > 4000 chars, prune as follows:
- Path-to-root: full
- Direct siblings: full with Summary
- Own children: full with Summary
- Distant branches:
#3.2 (+ 5 subtasks, 4 DONE, 1 planned)
6.3 Memory Rotation
Plan-Snapshots are ephemeral. The MarvinEngine
assembles the LLM-Memory fresh for each call from:
- System-Prompt (static marvin-worker-System-Prompt)
- Phase-Control-User-Message with current Plan-Snapshot embedded
Previous Reasoning-Turns (Assistant responses, Recipe-Replies) remain in the persistent Chat-History; they are not patched between turns with outdated Plan-Snapshots.
6.4 SCOPE-Prompt Instruction for the Plan
The System-Prompt explicitly states:
Before deciding your action, check the PLAN:
- Is your goal already covered by a sibling or cousin node?
→ reply with PROCEED_TO_CONCLUDE pointing to that, or
BLOCKED_BY_PROBLEM "duplicate of #X.Y".
- Are you a fine-grained branch of an aspect already explored?
→ don't re-decompose; do the work yourself.
- Are there gaps in the PLAN you should NOT spawn because they
belong to a sibling? → stay in your lane.
7. CALL_RECIPE Mechanism
The most important routing mechanism. Process for SCOPE or REFLECT
CALL_RECIPE:
- Engine validates:
recipeCall.recipe∈params.availableRecipes. Otherwise Failure-Marker in the Reply. - Block: marvin-via-CALL_RECIPE. If the called Recipe
has
engine: marvin→ Reject. Prevents unbounded Cross-Marvin-Nesting in v1. - Block: Self-Recursion. If the called Recipe is identical to the running Marvin-Recipe → Reject.
- Engine synchronously spawns a Sub-Process with
recipeCall.recipeandrecipeCall.steerContentas initial steer. - Special-Recipes run in native mode — the Marvin-Phase- Contract is NOT layered on top.
- Engine waits until Sub-Process CLOSED (DONE/STOPPED/FAILED).
- Engine reads the last Assistant-Text, appends it as
USER-Message to marvin-worker’s Memory (hard-truncated at
recipeReplyTruncateChars, default 8000):
<<< Result of CALL_RECIPE('web-research'):
<reply text>
[truncated; full reply persisted in sub-process history]
>>>
- Engine triggers next LLM-Turn in Phase REFLECT.
8. NEEDS_SUBTASKS Mechanism
For SCOPE / REFLECT / POST_CHILDREN NEEDS_SUBTASKS:
- Engine appends
newTasksas WORKER-Children (or EXPAND_FROM_DOC, USER_INPUT) under the node. - Node is marked DONE with
awaitingPostChildren=trueandartifacts.spawnedChildren=<count>. DONE is DFS-transparent — the walker descends into the Children. (If the node were set to WAITING, the DFS would block and never reach the Children — see §4.) - Marvin’s DFS processes Children pre-order. Each Child goes through its own 5-phase state machine.
- At the top of each
runTurn,reactivatePostChildrenParentsruns: finds DONE-nodes withawaitingPostChildren=truewhose Children are all terminal, resets them toPENDING + currentPhase=POST_CHILDREN, clears the flag. - The next DFS-Pick drives the re-activated Parent through POST_CHILDREN → CONCLUDE → VALIDATE → DONE.
Tree-Depth-Cap: NEEDS_SUBTASKS from POST_CHILDREN only allowed
if current depth < maxTreeDepth. Otherwise forced
PROCEED_TO_CONCLUDE.
9. NEEDS_USER_INPUT Mechanism
For SCOPE / REFLECT NEEDS_USER_INPUT:
- Engine inserts a
USER_INPUT-node as a sibling directly after the current node (insertSiblingAfter). - Current node goes DONE with
artifacts.awaitingUserInputNode. - The
USER_INPUT-node goes to RUNNING in the next runTurn (runUserInput), creates the Inbox-Item, parks in WAITING. - When the User response arrives (
InboxAnswer-Event),handleInboxAnswercloses the node DONE. - Marvin’s DFS continues through the tree.
10. EXPAND_FROM_DOC
Unchanged from v1. Deterministic fanout from a
list/tree/records-Document. DocumentExpander reads the
Document, iterates items, spawns a child per item according to
childTemplate with {{ item.text }} / {{ record.<field> }}-
substitution. No LLM call.
taskSpec:
{"documentRef": {"path": "essays/outline.md"},
"treeMode": "FLAT" | "RECURSIVE",
"childTemplate": {
"taskKind": "WORKER",
"goal": "Write chapter: {{ item.text }}",
"taskSpec": {
"postActions": [{"tool":"doc_create", "...": "..."}]
}
}}
11. postActions (engine-side persistence)
Worker emits postActions as part of the CONCLUDE-Output.
Engine executes them deterministically after VALIDATE PASS —
no LLM-Tool-Call, no hallucination risks.
Supported tools (v1):
doc_create— canonical, upsert by path (find → update, else create). Required:path,kind,content. SeeMarvinEngine.execDocCreate.
Arguments:
path(string, required) — project-relativekind(string, required) — Document-Kind;"text"for free-text outputscontent(string, required) — body to writetitle(string, optional)
Pebble-Render-Context:
{{ node.result }}— Worker’s CONCLUDE-Result{{ node.goal }}{{ node.summary }}(fallback){{ process.goal }}— root process goal{{ process.id }}| slugfilter — URL-safe slug
Reserved Path-Prefixes (Engine-owned, must NEVER
be written): recipes/, _user/, _vance/, _slart/,
_tenant/, _zaphod-drafts/, _vogon-drafts/, _marvin-drafts/.
12. ParentReport (summarizeForParent)
When Marvin runs as a Child-Process (typically: Arthur spawns
Marvin via process_create), it reports to the Parent after DONE:
- Primary: Root-WORKER’s
artifacts.result(this is the final CONCLUDE-response). - Fallback: Concatenation of the Root-Children-Results with Header
N of M succeeded.
Plus payload-metadata (rootNodeId, nodeCount, failedChildren).
13. Recipe Conventions
13.1 marvin-worker (recipes/marvin-worker.yaml)
The only Worker-Recipe — the Engine itself knows its name
as WORKER_RECIPE_NAME and uses it implicitly. System-Prompt
is located under prompts/marvin-worker-system.md (Document-Cascade).
13.2 Recipe for Marvin-Process (recipes/marvin.yaml)
Convenience-Recipe for engine: marvin without further configuration:
engine: marvin
params:
model: default:analyze
availableRecipes: [] # empty = only direct work
maxTreeNodes: 200
maxTreeDepth: 5
reflectMaxIterations: 3
validateMaxIterations: 2
concludeMaxRetries: 2
13.3 Special Tool Recipes
web-research, analyze, code-read, etc. retain their
native System-Prompt. They are NEVER used directly as Marvin-WORKER-
Nodes — marvin-worker calls them via CALL_RECIPE.
13.4 Slart-Marvin-Architect
Generates Marvin-Recipes with:
engine: marvinparams.availableRecipes: [recipe1, ...]promptPrefix— narrative Goal-steering (Pebble-Template)- Optional:
maxTreeDepth, Iteration-Caps
Three templates (research-aggregate-write, doc-driven-chapters,
decide-with-user-input) cover the most common patterns. Details
in vance-brain/.../slartibartfast/architect/MarvinArchitect.java.
14. Engine-Params (params:)
| Param | Default | Description |
|---|---|---|
model |
default:analyze |
LLM-Model-Alias |
availableRecipes |
[] |
Whitelist for CALL_RECIPE |
maxTreeNodes |
200 | Global Tree-Size-Cap |
maxTreeDepth |
5 | NEEDS_SUBTASKS-Depth-Cap |
reflectMaxIterations |
3 | REFLECT-Loop-Cap per node |
validateMaxIterations |
2 | VALIDATE-Loop-Cap per node |
concludeMaxRetries |
2 | CONCLUDE-Retry-Cap per node |
defaultExecutionMode |
(kind-default) | SEQUENTIAL/PARALLEL for DFS |
defaultExecutionModePerKind |
— | Per-TaskKind override |
15. Persistence / Audit
15.1 PhaseHistory
Each LLM-Call creates an entry in node.phaseHistory:
record PhaseIteration(
WorkerPhase phase,
int iterationIndex,
String outputJson, // parsed output, JSON-serialized
String model, // "provider:modelName" alias
@Nullable Integer promptTokens,
@Nullable Integer completionTokens,
Instant timestamp)
Not displayed in LLM-Memory — only for UI/Audit/Replay.
15.2 CalledSubProcessIds
For CALL_RECIPE, the Sub-Process-Id is appended to
node.calledSubProcessIds. Reverse-Lookup via
MarvinNodeRepository.findByCalledSubProcessIdsContaining.
15.3 Draft Persistence (_marvin-drafts/)
Every WORKER-Node that passes VALIDATE (or Cap-Forced-DONE) with
substantial result automatically writes a
Markdown file under:
_marvin-drafts/<processId>/<position-path>__<slug>.md
position-path is the hyphen-separated path of positions
from the root to the node (e.g., 0-1-2 = Root.child[1].child[2]).
slug is the slug-version of the node’s Goal (URL-safe, max 60 chars).
Content: YAML-Front-Matter with node metadata (nodeId, taskKind,
status, currentPhase, Iteration-Counter, completedAt,
phaseHistory-overview) + raw Markdown-Result.
Deterministic, engine-side, no LLM-Tool-Call. Failures are logged and ignored — Drafts do not break the Engine.
Usage: Audit & Debugging — an operator can inspect every intermediate state without digging into Mongo / Chat-Logs.
15.4 Chat-History
Engine also writes CALL_RECIPE-Replies as USER-Messages to
the normal Chat-History (ChatMessageService.append) — best
effort, Inspector-/Web-UI-visibility. Failures are logged
and ignored (the source of truth remains
node.artifacts.recipeReplies).
16. Plan-Snapshot to Progress-Channel
At the end of each runTurn, emitPlanSnapshot pushes a
PlanPayload to the User-Progress-Side-Channel (see
user-progress-channel.md). The Web-UI renders the tree
live from this. Format: PlanNode-Records with id, kind, title,
status, meta.phase, meta.failureReason, children.
17. Failure Modes and Limits
| Failure | Detection | Effect |
|---|---|---|
| Phase-Parser-Error | invalid JSON / missing mandatory fields | Node FAILED with Parse-Error |
| BLOCKED_BY_PROBLEM | LLM emits in SCOPE/REFLECT/POST_CHILDREN | Node FAILED with problem |
| HARD_FAIL (VALIDATE) | LLM-Critique | Node FAILED |
| Cap-Exhausted (REFLECT/CONCLUDE) | Counter reaches Cap | Forced PROCEED_TO_CONCLUDE / forced markDone |
| Cap-Exhausted (VALIDATE) | Counter reaches Cap | Forced markDone + Audit-Warning |
maxTreeNodes exceeded |
runTurn-Check | Node FAILED, finalizeIdle |
maxTreeDepth reached |
POST_CHILDREN-Check | Forced PROCEED_TO_CONCLUDE |
| CALL_RECIPE on unknown Recipe | RecipeResolver-Lookup | Failure-Marker in Reply, REFLECT evaluates |
| CALL_RECIPE on marvin-Recipe | Engine-Check | Failure-Marker, REFLECT evaluates |
| Self-Recursion (same Recipe) | Engine-Check | Failure-Marker, REFLECT evaluates |
| Sub-Process FAILED/STOPPED | ProcessEvent | Failure-Marker in Reply, REFLECT evaluates |
18. What is deliberately NOT included in v1
- Per-Child-Supervisor: Parent gets a mini-Reflect-Phase after EVERY Child. Tricky to prompt correctly.
- Different Models per Phase: VALIDATE could use a smaller,
faster model. Engine-Param
phaseModels: {validate: "default:fast"}. - Custom Phase-Prompts: Recipe could override phase-specific System-Prompts.
- Marvin-via-CALL_RECIPE: blocked. v2 could limit and allow Cross-Marvin-Depth.
- Streaming-Output during CONCLUDE: currently batch-Reply.
- NEEDS_USER_INPUT mid-VALIDATE: currently modeled as HARD_FAIL.
19. Example Flow
Goal: “Research nuclear power and write a report.”
Recipe: params.availableRecipes: [web-research].
Tick 1 (root WORKER, Phase SCOPE):
LLM sees: Goal, [web-research] available, Plan-Snapshot (Root running).
Output: action=CALL_RECIPE, recipeCall={web-research, "Research Nuclear Power History"}
→ Engine spawns web-research-Sub-Process.
Tick 2 (root WORKER, Phase REFLECT iter 1/3):
LLM sees: Goal, Plan, "<<< Result of CALL_RECIPE('web-research'): ... >>>".
Output: action=CALL_RECIPE, recipeCall={web-research, "Research Tech Status"}.
→ Sub-Process #2.
Tick 3 (root WORKER, Phase REFLECT iter 2/3):
LLM sees: Goal, Plan, 2 Recipe-Replies.
Output: action=PROCEED_TO_CONCLUDE, reason="sufficient material".
Tick 4 (root WORKER, Phase CONCLUDE):
LLM sees: Goal, Plan, 2 Recipe-Replies (in chat history).
Output: result="# Nuclear Power Report\n...",
postActions=[{doc_create, path="research/nuclear-power/report.md",
kind="text", content="{{ node.result }}"}].
Tick 5 (root WORKER, Phase VALIDATE iter 1/2):
LLM sees: Goal, Plan, Candidate.
Output: verdict=PASS, reason="complete and well-structured".
→ markDone(result), runPostActions(write file), Node TERMINAL DONE.
→ Tree terminal → process CLOSED.
→ Parent (Arthur) receives ParentReport with the report text.
5 LLM-Calls total, 2 Recipe-Calls, one file written. Worker saw no siblings in the Plan-Snapshot → knew it was solely responsible.
20. References
- Plan-Doc:
planning/marvin-node-state-machine.md— development- accompanying architectural sketch (covers the model). - Implementation:
vance-brain/.../marvin/MarvinEngine.java,PlanSnapshotRenderer.java,MarvinNodeStateMachine.java,PhaseOutputParser.java. - vance-api:
de.mhus.vance.api.marvin.*(Records + Enums). - vance-shared:
MarvinNodeDocument,MarvinNodeService. - marvin-worker-System-Prompt:
prompts/marvin-worker-system.md(overridable by tenant in Document-Cascade). - Slart-Marvin-Architect-Manual:
manuals/slartibartfast/marvin-architect/SHAPE.md. - Tests:
vance-brain/.../marvin/PlanSnapshotRendererTest,PhaseOutputParserTest,MarvinNodeStateMachineTest;qa/ai-test/.../ArthurMarvinRecipeTest,SlartibartfastMarvinRecipeLlmTest.