Add complete operator node declarations
Browse files
README.md
CHANGED
|
@@ -69,10 +69,251 @@ legality, state commits, and side effects.
|
|
| 69 |
└─────────────┘ └─────────────┘
|
| 70 |
```
|
| 71 |
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
```python
|
| 75 |
-
from neosyntropy import END, FSM, edge_deterministic
|
| 76 |
|
| 77 |
def signal_is(expected):
|
| 78 |
return lambda state: state.get("signal") == expected
|
|
@@ -94,6 +335,7 @@ graph = FSM(
|
|
| 94 |
repair,
|
| 95 |
compress_memory,
|
| 96 |
success,
|
|
|
|
| 97 |
],
|
| 98 |
edges=[
|
| 99 |
edge_deterministic("Understand", "Decompose", guard=signal_is("CONTINUE")),
|
|
@@ -113,6 +355,15 @@ graph = FSM(
|
|
| 113 |
edge_deterministic("Repair", "Retrieve", guard=signal_is("NEED_INFORMATION")),
|
| 114 |
edge_deterministic("Compress", "Decompose", guard=signal_is("CONTINUE")),
|
| 115 |
edge_deterministic("Success", END),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
],
|
| 117 |
)
|
| 118 |
```
|
|
|
|
| 69 |
└─────────────┘ └─────────────┘
|
| 70 |
```
|
| 71 |
|
| 72 |
+
### Complete node declarations
|
| 73 |
+
|
| 74 |
+
The following definitions make every symbol in the graph explicit. The three
|
| 75 |
+
tool names are application integrations: `search_repository`, `apply_plan`, and
|
| 76 |
+
`run_tests`. Learned operators use schema-constrained model calls; execution and
|
| 77 |
+
verification stay in trusted Python handlers.
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
from typing import Any, Literal
|
| 81 |
+
|
| 82 |
+
from pydantic import BaseModel, ConfigDict
|
| 83 |
+
|
| 84 |
+
from neosyntropy import NodeContext, OpenInput, SchemaNode, TextOutput, node
|
| 85 |
+
|
| 86 |
+
Signal = Literal[
|
| 87 |
+
"CONTINUE",
|
| 88 |
+
"NEED_INFORMATION",
|
| 89 |
+
"EXECUTED",
|
| 90 |
+
"FAILED_TEST",
|
| 91 |
+
"WRONG_PLAN",
|
| 92 |
+
"MEMORY_PRESSURE",
|
| 93 |
+
"COMPLETE",
|
| 94 |
+
]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class StrictModel(BaseModel):
|
| 98 |
+
model_config = ConfigDict(extra="forbid")
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
class TaskInput(StrictModel):
|
| 102 |
+
repository: str
|
| 103 |
+
issue: str
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class UnderstandOutput(StrictModel):
|
| 107 |
+
requirements: list[str]
|
| 108 |
+
unknowns: list[str]
|
| 109 |
+
signal: Literal["CONTINUE", "NEED_INFORMATION"]
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class DecomposeOutput(StrictModel):
|
| 113 |
+
task_tree: list[str]
|
| 114 |
+
search_queries: list[str]
|
| 115 |
+
signal: Literal["CONTINUE", "NEED_INFORMATION"]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class RetrieveOutput(StrictModel):
|
| 119 |
+
retrieved: list[str]
|
| 120 |
+
signal: Literal["CONTINUE", "NEED_INFORMATION"]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class PlanOutput(StrictModel):
|
| 124 |
+
current_plan: str
|
| 125 |
+
signal: Literal["CONTINUE", "NEED_INFORMATION"]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class ExecuteOutput(StrictModel):
|
| 129 |
+
executed: bool
|
| 130 |
+
summary: str
|
| 131 |
+
signal: Literal["EXECUTED"]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class ObserveOutput(StrictModel):
|
| 135 |
+
observations: list[str]
|
| 136 |
+
errors: list[str]
|
| 137 |
+
signal: Literal["CONTINUE", "MEMORY_PRESSURE"]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class Verification(StrictModel):
|
| 141 |
+
passed: bool
|
| 142 |
+
summary: str
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class VerifyOutput(StrictModel):
|
| 146 |
+
verification: Verification
|
| 147 |
+
evidence: list[str]
|
| 148 |
+
signal: Literal[
|
| 149 |
+
"COMPLETE", "FAILED_TEST", "NEED_INFORMATION", "WRONG_PLAN"
|
| 150 |
+
]
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class CompressOutput(StrictModel):
|
| 154 |
+
observations: list[str]
|
| 155 |
+
errors: list[str]
|
| 156 |
+
history: list[str]
|
| 157 |
+
signal: Literal["CONTINUE"]
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class SuccessOutput(StrictModel):
|
| 161 |
+
outcome: Literal["satisfies_spec"]
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# Learned operator: infer explicit requirements without inventing facts.
|
| 165 |
+
understand = SchemaNode(
|
| 166 |
+
id="Understand",
|
| 167 |
+
input_schema=TaskInput,
|
| 168 |
+
output_schema=UnderstandOutput,
|
| 169 |
+
prompt=(
|
| 170 |
+
"<OPERATOR:UNDERSTAND> Read the repository issue and current state. "
|
| 171 |
+
"Return explicit requirements, unresolved unknowns, and a legal signal."
|
| 172 |
+
),
|
| 173 |
+
metadata={"operator": "UNDERSTAND", "model_role": "runtime-structure"},
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# Learned operator: turn requirements into an ordered task tree and searches.
|
| 178 |
+
decompose = SchemaNode(
|
| 179 |
+
id="Decompose",
|
| 180 |
+
input_schema=OpenInput,
|
| 181 |
+
output_schema=DecomposeOutput,
|
| 182 |
+
prompt=(
|
| 183 |
+
"<OPERATOR:DECOMPOSE> Decompose the goal into ordered, testable subgoals. "
|
| 184 |
+
"Generate only repository searches needed by those subgoals."
|
| 185 |
+
),
|
| 186 |
+
prerequisites=("Understand",),
|
| 187 |
+
metadata={"operator": "DECOMPOSE", "model_role": "runtime-deterministic-reasoning"},
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# Trusted tool operator: execute only repository searches already in state.
|
| 192 |
+
@node(
|
| 193 |
+
id="Retrieve",
|
| 194 |
+
input_schema=OpenInput,
|
| 195 |
+
output_schema=RetrieveOutput,
|
| 196 |
+
tools=("search_repository",),
|
| 197 |
+
)
|
| 198 |
+
def retrieve(ctx: NodeContext):
|
| 199 |
+
facts: list[str] = []
|
| 200 |
+
for query in ctx.state.get("search_queries", []):
|
| 201 |
+
result = ctx.tools.invoke("search_repository", {"query": query})
|
| 202 |
+
facts.extend(result if isinstance(result, list) else [str(result)])
|
| 203 |
+
signal = "CONTINUE" if facts else "NEED_INFORMATION"
|
| 204 |
+
output = {"retrieved": facts, "signal": signal}
|
| 205 |
+
return ctx.result(output=output, state_updates=output)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
# Learned operator: propose one bounded implementation plan from evidence.
|
| 209 |
+
propose = SchemaNode(
|
| 210 |
+
id="Propose",
|
| 211 |
+
input_schema=OpenInput,
|
| 212 |
+
output_schema=PlanOutput,
|
| 213 |
+
prompt=(
|
| 214 |
+
"<OPERATOR:PROPOSE> Use requirements, task_tree, and retrieved evidence. "
|
| 215 |
+
"Return one implementation plan grounded in repository symbols."
|
| 216 |
+
),
|
| 217 |
+
metadata={"operator": "PROPOSE", "model_role": "runtime-stochastic-reasoning"},
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
# Trusted tool operator: apply the selected plan and run the real test harness.
|
| 222 |
+
@node(
|
| 223 |
+
id="Execute",
|
| 224 |
+
input_schema=OpenInput,
|
| 225 |
+
output_schema=ExecuteOutput,
|
| 226 |
+
tools=("apply_plan", "run_tests"),
|
| 227 |
+
)
|
| 228 |
+
def execute(ctx: NodeContext):
|
| 229 |
+
plan = ctx.state["current_plan"]
|
| 230 |
+
ctx.tools.invoke("apply_plan", {"plan": plan})
|
| 231 |
+
last_run = ctx.tools.invoke("run_tests", {})
|
| 232 |
+
output = {
|
| 233 |
+
"executed": True,
|
| 234 |
+
"summary": str(last_run),
|
| 235 |
+
"signal": "EXECUTED",
|
| 236 |
+
}
|
| 237 |
+
return ctx.result(output=output, state_updates={**output, "last_run": last_run})
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
# Trusted operator: normalize tool output into observations and errors.
|
| 241 |
+
@node(id="Observe", input_schema=OpenInput, output_schema=ObserveOutput)
|
| 242 |
+
def observe(ctx: NodeContext):
|
| 243 |
+
run = ctx.state.get("last_run", {})
|
| 244 |
+
errors = list(run.get("errors", [])) if isinstance(run, dict) else []
|
| 245 |
+
observations = [str(run)]
|
| 246 |
+
signal = "MEMORY_PRESSURE" if len(observations) > 20 else "CONTINUE"
|
| 247 |
+
output = {"observations": observations, "errors": errors, "signal": signal}
|
| 248 |
+
return ctx.result(output=output, state_updates=output)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# Trusted gate: only real test evidence may produce COMPLETE.
|
| 252 |
+
@node(id="Verify", input_schema=OpenInput, output_schema=VerifyOutput)
|
| 253 |
+
def verify(ctx: NodeContext):
|
| 254 |
+
run = ctx.state.get("last_run", {})
|
| 255 |
+
passed = bool(isinstance(run, dict) and run.get("passed") is True)
|
| 256 |
+
summary = "All required tests passed." if passed else "Required tests failed."
|
| 257 |
+
output = {
|
| 258 |
+
"verification": {"passed": passed, "summary": summary},
|
| 259 |
+
"evidence": [str(run)],
|
| 260 |
+
"signal": "COMPLETE" if passed else "FAILED_TEST",
|
| 261 |
+
}
|
| 262 |
+
return ctx.result(output=output, state_updates=output)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
# Learned operator: repair the plan using concrete failure evidence.
|
| 266 |
+
repair = SchemaNode(
|
| 267 |
+
id="Repair",
|
| 268 |
+
input_schema=OpenInput,
|
| 269 |
+
output_schema=PlanOutput,
|
| 270 |
+
prompt=(
|
| 271 |
+
"<OPERATOR:REPAIR> Read current_plan, errors, and verification evidence. "
|
| 272 |
+
"Return a corrected plan that addresses the observed failure only."
|
| 273 |
+
),
|
| 274 |
+
metadata={"operator": "REPAIR", "model_role": "runtime-stochastic-reasoning"},
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
# Trusted operator: bound state growth without rewriting facts.
|
| 279 |
+
@node(id="Compress", input_schema=OpenInput, output_schema=CompressOutput)
|
| 280 |
+
def compress_memory(ctx: NodeContext):
|
| 281 |
+
output = {
|
| 282 |
+
"observations": list(ctx.state.get("observations", []))[-4:],
|
| 283 |
+
"errors": list(ctx.state.get("errors", []))[-4:],
|
| 284 |
+
"history": list(ctx.state.get("history", []))[-12:],
|
| 285 |
+
"signal": "CONTINUE",
|
| 286 |
+
}
|
| 287 |
+
return ctx.result(output=output, state_updates=output)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
# Terminal node: reachable only through the verified_complete graph guard.
|
| 291 |
+
@node(id="Success", input_schema=OpenInput, output_schema=SuccessOutput)
|
| 292 |
+
def success(ctx: NodeContext):
|
| 293 |
+
output = {"outcome": "satisfies_spec"}
|
| 294 |
+
return ctx.result(output=output, state_updates=output)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
@node(
|
| 298 |
+
id="OperatorFallback",
|
| 299 |
+
input_schema=OpenInput,
|
| 300 |
+
output_schema=TextOutput,
|
| 301 |
+
is_fallback=True,
|
| 302 |
+
)
|
| 303 |
+
def operator_fallback(ctx: NodeContext):
|
| 304 |
+
return ctx.result(output={"message": "No legal operator transition."})
|
| 305 |
+
```
|
| 306 |
+
|
| 307 |
+
Schema-node outputs are validated before their fields are proposed as state
|
| 308 |
+
updates. Python handlers explicitly return `state_updates`; mutating `ctx.state`
|
| 309 |
+
does not commit anything.
|
| 310 |
+
|
| 311 |
+
### Graph wiring
|
| 312 |
+
|
| 313 |
+
The same nodes are connected as a NeoSyntropy FSM:
|
| 314 |
|
| 315 |
```python
|
| 316 |
+
from neosyntropy import END, FSM, edge_deterministic, edge_fallback
|
| 317 |
|
| 318 |
def signal_is(expected):
|
| 319 |
return lambda state: state.get("signal") == expected
|
|
|
|
| 335 |
repair,
|
| 336 |
compress_memory,
|
| 337 |
success,
|
| 338 |
+
operator_fallback,
|
| 339 |
],
|
| 340 |
edges=[
|
| 341 |
edge_deterministic("Understand", "Decompose", guard=signal_is("CONTINUE")),
|
|
|
|
| 355 |
edge_deterministic("Repair", "Retrieve", guard=signal_is("NEED_INFORMATION")),
|
| 356 |
edge_deterministic("Compress", "Decompose", guard=signal_is("CONTINUE")),
|
| 357 |
edge_deterministic("Success", END),
|
| 358 |
+
edge_fallback("Understand", "OperatorFallback"),
|
| 359 |
+
edge_fallback("Decompose", "OperatorFallback"),
|
| 360 |
+
edge_fallback("Retrieve", "OperatorFallback"),
|
| 361 |
+
edge_fallback("Propose", "OperatorFallback"),
|
| 362 |
+
edge_fallback("Execute", "OperatorFallback"),
|
| 363 |
+
edge_fallback("Observe", "OperatorFallback"),
|
| 364 |
+
edge_fallback("Verify", "OperatorFallback"),
|
| 365 |
+
edge_fallback("Repair", "OperatorFallback"),
|
| 366 |
+
edge_fallback("Compress", "OperatorFallback"),
|
| 367 |
],
|
| 368 |
)
|
| 369 |
```
|