Skip to content

Commit 5e23ca4

Browse files
committed
feat: add agentic molecules - deterministic, oracle-validated execution
- Core types for Molecule.Spec, Action, OracleRef, Attestation - Executor with pre-oracle validation and deterministic hashing - CLI command: opencode molecule run <spec-file> [--dry-run] - Tool registry supporting bash, write, edit, read, grep, glob, list - Tests validating execution and determinism - Examples: hello-world, oracle validation, multi-action - Documentation in examples/molecules/README.md Molecules provide atomic, auditable work units with: - Pre-execution oracle checks (bash exit code validation) - Attestations with input/output hashes and oracle results - Integration with existing OpenCode tools - Colored CLI output with success/failure indicators
1 parent 71a7e8e commit 5e23ca4

14 files changed

Lines changed: 632 additions & 0 deletions

File tree

examples/molecules/README.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Agentic Molecules
2+
3+
Molecules are atomic, auditable work units for the OpenCode CLI. They provide deterministic execution with oracle-validated constraints.
4+
5+
## Quick Start
6+
7+
Execute a molecule from a spec file:
8+
9+
```bash
10+
opencode molecule run <spec-file>
11+
```
12+
13+
Validate a spec without execution:
14+
15+
```bash
16+
opencode molecule run <spec-file> --dry-run
17+
```
18+
19+
## Molecule Spec Format
20+
21+
Molecules are defined in JSON, TypeScript, or JavaScript files:
22+
23+
```json
24+
{
25+
"id": "unique-molecule-id",
26+
"description": "What this molecule does",
27+
"actions": [
28+
{
29+
"toolID": "write",
30+
"params": {
31+
"filePath": "path/to/file.txt",
32+
"content": "File content"
33+
}
34+
}
35+
],
36+
"oracles": [
37+
{
38+
"type": "bash",
39+
"check": "test -d path/to"
40+
}
41+
]
42+
}
43+
```
44+
45+
### Spec Components
46+
47+
- **id**: Unique identifier for the molecule
48+
- **description**: Human-readable description
49+
- **actions**: Array of tool invocations to execute
50+
- **oracles**: Array of pre-execution validation checks
51+
52+
## Available Tools
53+
54+
- `bash` - Execute shell commands
55+
- `write` - Create or overwrite files
56+
- `edit` - Modify existing files
57+
- `read` - Read file contents
58+
- `grep` - Search file contents
59+
- `glob` - Find files by pattern
60+
- `list` - List directory contents
61+
62+
## Oracles
63+
64+
Oracles are bash commands that must succeed (exit code 0) before execution:
65+
66+
```json
67+
{
68+
"type": "bash",
69+
"check": "test -d /required/directory"
70+
}
71+
```
72+
73+
If any oracle fails, the molecule aborts and no actions are executed.
74+
75+
## Attestations
76+
77+
Every molecule execution produces an attestation containing:
78+
79+
- Input hash (deterministic based on spec)
80+
- Output hash (based on action results)
81+
- Oracle results (passed/failed with output)
82+
- Success status
83+
- Timestamp
84+
85+
## Examples
86+
87+
See `examples/molecules/` for sample molecule specs:
88+
89+
- `hello-world.json` - Simple file creation
90+
- `create-readme.json` - File creation with oracle validation
91+
- `oracle-failure-test.json` - Oracle failure blocking execution
92+
- `multi-action.json` - Multiple bash and write actions
93+
94+
## Implementation Status
95+
96+
**Completed:**
97+
98+
- Core executor with pre-oracle validation
99+
- CLI command integration (`opencode molecule run`)
100+
- Deterministic input/output hashing
101+
- Attestation generation
102+
- Tool registry (bash, write, edit, read, grep, glob, list)
103+
- Tests validating execution and determinism
104+
105+
🚧 **Future Enhancements:**
106+
107+
- Post-oracles (validate after execution)
108+
- Rollback on failure
109+
- CAS (Content-Addressable Storage) for attestations
110+
- Ledger for querying execution history
111+
- Additional oracle types (test, lint, policy)
112+
- Molecule chaining and composition
113+
114+
## Architecture
115+
116+
```
117+
Molecule Spec (JSON/TS/JS)
118+
119+
Executor.execute()
120+
121+
1. Hash inputs (spec + timestamp)
122+
2. Run pre-oracles (must pass)
123+
3. Execute actions (using tool registry)
124+
4. Hash outputs (action results + timestamp)
125+
5. Create attestation
126+
127+
ExecutionResult { success, attestation, outputs, errors }
128+
```
129+
130+
## Design Principles
131+
132+
1. **Deterministic** - Same spec → same hashes (mostly, timestamps differ)
133+
2. **Oracle-first** - Pre-execution validation blocks bad changes
134+
3. **Auditable** - Every execution produces an attestation
135+
4. **Tool-based** - Reuses OpenCode's existing tool infrastructure
136+
5. **Minimal** - ~150 LOC for core implementation
137+
138+
## Development
139+
140+
Run tests:
141+
142+
```bash
143+
cd packages/opencode
144+
bun test src/molecule/__tests__/
145+
```
146+
147+
Test CLI locally:
148+
149+
```bash
150+
cd packages/opencode
151+
bun dev molecule run ../../examples/molecules/hello-world.json
152+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"id": "create-readme-with-validation",
3+
"description": "Create a README with pre-validation that directory exists",
4+
"actions": [
5+
{
6+
"toolID": "write",
7+
"params": {
8+
"filePath": "examples/molecules/output/README.md",
9+
"content": "# Molecule Test Project\n\nThis README was created by an Agentic Molecule.\n\n## Features\n- Validated execution\n- Deterministic behavior\n- Auditable attestations\n"
10+
}
11+
}
12+
],
13+
"oracles": [
14+
{
15+
"type": "bash",
16+
"check": "test -d examples/molecules/output && echo 'directory exists'"
17+
}
18+
]
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"id": "hello-world",
3+
"description": "Create a simple hello world file",
4+
"actions": [
5+
{
6+
"toolID": "write",
7+
"params": {
8+
"filePath": "examples/molecules/output/hello-molecule.txt",
9+
"content": "Hello from Agentic Molecules!\n\nThis file was created by a molecule execution.\n"
10+
}
11+
}
12+
],
13+
"oracles": []
14+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"id": "multi-action-example",
3+
"description": "Create multiple files with bash and write tools",
4+
"actions": [
5+
{
6+
"toolID": "bash",
7+
"params": {
8+
"command": "mkdir -p examples/molecules/output/project",
9+
"description": "Create project directory"
10+
}
11+
},
12+
{
13+
"toolID": "write",
14+
"params": {
15+
"filePath": "examples/molecules/output/project/package.json",
16+
"content": "{\n \"name\": \"molecule-example\",\n \"version\": \"1.0.0\",\n \"description\": \"Created by Agentic Molecule\"\n}\n"
17+
}
18+
},
19+
{
20+
"toolID": "write",
21+
"params": {
22+
"filePath": "examples/molecules/output/project/index.js",
23+
"content": "console.log('Hello from Molecule!');\n"
24+
}
25+
}
26+
],
27+
"oracles": [
28+
{
29+
"type": "bash",
30+
"check": "test -d examples/molecules/output"
31+
}
32+
]
33+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"id": "oracle-failure-test",
3+
"description": "Test oracle failure - should block execution",
4+
"actions": [
5+
{
6+
"toolID": "write",
7+
"params": {
8+
"filePath": "examples/molecules/output/should-not-be-created.txt",
9+
"content": "This file should NOT be created because the oracle will fail.\n"
10+
}
11+
}
12+
],
13+
"oracles": [
14+
{
15+
"type": "bash",
16+
"check": "test -d /nonexistent-directory && echo 'directory exists'"
17+
}
18+
]
19+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Molecule Test Project
2+
3+
This README was created by an Agentic Molecule.
4+
5+
## Features
6+
7+
- Validated execution
8+
- Deterministic behavior
9+
- Auditable attestations
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Hello from Agentic Molecules!
2+
3+
This file was created by a molecule execution.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("Hello from Molecule!")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "molecule-example",
3+
"version": "1.0.0",
4+
"description": "Created by Agentic Molecule"
5+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { Argv } from "yargs"
2+
import { cmd } from "./cmd"
3+
import { bootstrap } from "../bootstrap"
4+
import { UI } from "../ui"
5+
import { Executor } from "../../molecule/executor"
6+
import type { Molecule } from "../../molecule/types"
7+
import { BashTool } from "../../tool/bash"
8+
import { WriteTool } from "../../tool/write"
9+
import { EditTool } from "../../tool/edit"
10+
import { ReadTool } from "../../tool/read"
11+
import { GrepTool } from "../../tool/grep"
12+
import { GlobTool } from "../../tool/glob"
13+
import { ListTool } from "../../tool/ls"
14+
15+
export const MoleculeCommand = cmd({
16+
command: "molecule",
17+
describe: "Execute molecules - atomic, auditable work units",
18+
builder: (yargs: Argv) => {
19+
return yargs.command(
20+
"run <spec-file>",
21+
"Execute a molecule from spec file",
22+
(yargs) => {
23+
return yargs
24+
.positional("spec-file", {
25+
describe: "Path to molecule spec file (JSON or TypeScript)",
26+
type: "string",
27+
demandOption: true,
28+
})
29+
.option("dry-run", {
30+
describe: "Validate without executing",
31+
type: "boolean",
32+
default: false,
33+
})
34+
},
35+
async (args) => {
36+
const specFile = args["spec-file"] as string
37+
38+
await bootstrap(process.cwd(), async () => {
39+
const file = Bun.file(specFile)
40+
if (!(await file.exists())) {
41+
UI.error(`Spec file not found: ${specFile}`)
42+
process.exit(1)
43+
}
44+
45+
const content = await file.text()
46+
let spec: Molecule.Spec
47+
48+
if (specFile.endsWith(".json")) {
49+
spec = JSON.parse(content)
50+
} else if (specFile.endsWith(".ts") || specFile.endsWith(".js")) {
51+
const module = await import(specFile)
52+
spec = module.default || module.spec
53+
} else {
54+
UI.error("Spec file must be .json, .ts, or .js")
55+
process.exit(1)
56+
}
57+
58+
if (args["dry-run"]) {
59+
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✓ Spec is valid")
60+
UI.println(UI.Style.TEXT_NORMAL + " ID: " + spec.id)
61+
UI.println(UI.Style.TEXT_NORMAL + " Description: " + spec.description)
62+
UI.println(UI.Style.TEXT_NORMAL + " Actions: " + spec.actions.length)
63+
UI.println(UI.Style.TEXT_NORMAL + " Oracles: " + spec.oracles.length)
64+
return
65+
}
66+
67+
const toolRegistry = new Map()
68+
toolRegistry.set("bash", BashTool)
69+
toolRegistry.set("write", WriteTool)
70+
toolRegistry.set("edit", EditTool)
71+
toolRegistry.set("read", ReadTool)
72+
toolRegistry.set("grep", GrepTool)
73+
toolRegistry.set("glob", GlobTool)
74+
toolRegistry.set("list", ListTool)
75+
76+
const ctx: Executor.Context = {
77+
sessionID: "molecule-" + Date.now(),
78+
messageID: "msg-" + Date.now(),
79+
agent: "build",
80+
toolRegistry,
81+
}
82+
83+
UI.println(UI.Style.TEXT_INFO_BOLD + "▸ Executing molecule: " + spec.id)
84+
UI.println(UI.Style.TEXT_DIM + " " + spec.description)
85+
86+
const startTime = Date.now()
87+
const result = await Executor.execute(spec, ctx)
88+
const duration = Date.now() - startTime
89+
90+
if (result.success) {
91+
UI.println(UI.Style.TEXT_SUCCESS_BOLD + "✓ Success" + UI.Style.TEXT_DIM + ` (${duration}ms)`)
92+
UI.println(UI.Style.TEXT_NORMAL + " Input hash: " + result.attestation.inputHash.slice(0, 16) + "...")
93+
UI.println(UI.Style.TEXT_NORMAL + " Output hash: " + result.attestation.outputHash.slice(0, 16) + "...")
94+
95+
if (result.attestation.oracleResults.length > 0) {
96+
UI.println(UI.Style.TEXT_INFO_BOLD + " Oracles:")
97+
for (const oracle of result.attestation.oracleResults) {
98+
const status = oracle.passed ? "✓" : "✗"
99+
const color = oracle.passed ? UI.Style.TEXT_SUCCESS : UI.Style.TEXT_DANGER
100+
UI.println(color + " " + status + " " + oracle.oracle.check)
101+
}
102+
}
103+
} else {
104+
UI.println(UI.Style.TEXT_DANGER_BOLD + "✗ Failed" + UI.Style.TEXT_DIM + ` (${duration}ms)`)
105+
if (result.errors) {
106+
for (const error of result.errors) {
107+
UI.println(UI.Style.TEXT_DANGER + " " + error.message)
108+
}
109+
}
110+
process.exit(1)
111+
}
112+
})
113+
},
114+
)
115+
},
116+
handler: () => {},
117+
})

0 commit comments

Comments
 (0)