File size: 4,979 Bytes
e706de2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# Code Explanation: aot-agent.js
This example demonstrates the **Atom of Thought** prompting pattern using a mathematical calculator as the domain.
## Three-Phase Architecture
### Phase 1: Planning (LLM)
```javascript
async function generatePlan(userPrompt) {
const grammar = await llama.createGrammarForJsonSchema(planSchema);
const planText = await session.prompt(userPrompt, { grammar });
return grammar.parse(planText);
}
```
**Key points:**
- LLM outputs **structured JSON** (enforced by grammar)
- LLM does NOT execute calculations
- Each atom represents one operation
- Dependencies are explicit (`dependsOn` array)
**Example output:**
```json
{
"atoms": [
{"id": 1, "kind": "tool", "name": "add", "input": {"a": 15, "b": 7}},
{"id": 2, "kind": "tool", "name": "multiply", "input": {"a": "<result_of_1>", "b": 3}},
{"id": 3, "kind": "tool", "name": "subtract", "input": {"a": "<result_of_2>", "b": 10}},
{"id": 4, "kind": "final", "name": "report", "dependsOn": [3]}
]
}
```
### Phase 2: Validation (System)
```javascript
function validatePlan(plan) {
const allowedTools = new Set(Object.keys(tools));
for (const atom of plan.atoms) {
if (ids.has(atom.id)) throw new Error(`Duplicate ID`);
if (atom.kind === "tool" && !allowedTools.has(atom.name)) {
throw new Error(`Unknown tool: ${atom.name}`);
}
}
}
```
**Validates:**
- No duplicate atom IDs
- Only allowed tools are referenced
- Dependencies make sense
- JSON structure is correct
### Phase 3: Execution (System)
```javascript
function executePlan(plan) {
const state = {};
for (const atom of sortedAtoms) {
// Resolve dependencies
let resolvedInput = {};
for (const [key, value] of Object.entries(atom.input)) {
if (value.startsWith('<result_of_')) {
const refId = parseInt(value.match(/\d+/)[0]);
resolvedInput[key] = state[refId];
}
}
// Execute
state[atom.id] = tools[atom.name](resolvedInput.a, resolvedInput.b);
}
}
```
**Key behaviors:**
- Executes atoms in order (sorted by ID)
- Resolves `<result_of_N>` references from state
- Each atom stores its result in `state[atom.id]`
- Execution is **deterministic** (same plan + same state = same result)
## Why This Matters
### Comparison with ReAct
| Aspect | ReAct | Atom of Thought |
|--------|-------|-----------------|
| **Planning** | Implicit (in LLM reasoning) | Explicit (JSON structure) |
| **Execution** | LLM decides next step | System follows plan |
| **Validation** | None | Before execution |
| **Debugging** | Hard (trace through text) | Easy (inspect atoms) |
| **Testing** | Hard (mock LLM) | Easy (test executor) |
| **Failures** | May hallucinate | Fail at specific atom |
### Benefits
1. **No hidden reasoning**: Every operation is an explicit atom
2. **Testable**: Execute plan without LLM involvement
3. **Debuggable**: Know exactly which atom failed
4. **Auditable**: Plan is a data structure, not text
5. **Deterministic**: Same input = same output (given same plan)
## Tool Implementation
Tools are **pure functions** with no side effects:
```javascript
const tools = {
add: (a, b) => {
const result = a + b;
console.log(`EXECUTING: add(${a}, ${b}) = ${result}`);
return result;
},
// ... more tools
};
```
**Why pure functions?**
- Easy to test
- Easy to replay
- No hidden state
- Composable
## State Flow
```
User Question
β
[LLM generates plan]
β
{atoms: [...]} β JSON plan
β
[System validates]
β
Plan valid
β
[System executes atom 1] β state[1] = result
β
[System executes atom 2] β state[2] = result (uses state[1])
β
[System executes atom 3] β state[3] = result (uses state[2])
β
Final Answer
```
## Error Handling
```javascript
// Atom validation fails β re-prompt LLM
validatePlan(plan); // throws if invalid
// Tool execution fails β stop at that atom
if (b === 0) throw new Error("Division by zero");
// Dependency missing β clear error message
if (!(depId in state)) {
throw new Error(`Atom ${atom.id} depends on incomplete atom ${depId}`);
}
```
## When to Use AoT
β
**Use AoT when:**
- Execution must be auditable
- Failures must be recoverable
- Multiple steps with dependencies
- Testing is important
- Compliance matters
β **Don't use AoT when:**
- Single-step tasks
- Creative/exploratory tasks
- Brainstorming
- Natural conversation
## Extension Ideas
1. **Add compensation atoms** for rollback
2. **Add retry logic** per atom
3. **Parallelize independent atoms** (atoms with no shared dependencies)
4. **Persist plan** for debugging
5. **Visualize atom graph** (dependency tree) |