laserbrain 0.2.0__tar.gz
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.
- laserbrain-0.2.0/PKG-INFO +157 -0
- laserbrain-0.2.0/README.md +144 -0
- laserbrain-0.2.0/laserbrain/__init__.py +411 -0
- laserbrain-0.2.0/laserbrain/adapters.py +140 -0
- laserbrain-0.2.0/laserbrain.egg-info/PKG-INFO +157 -0
- laserbrain-0.2.0/laserbrain.egg-info/SOURCES.txt +8 -0
- laserbrain-0.2.0/laserbrain.egg-info/dependency_links.txt +1 -0
- laserbrain-0.2.0/laserbrain.egg-info/top_level.txt +1 -0
- laserbrain-0.2.0/pyproject.toml +23 -0
- laserbrain-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: laserbrain
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Attach the smart recursion harness to any agent loop — a provably-correct, external check for when an AI agent (or team) has drifted from its goal.
|
|
5
|
+
Author: phronesis
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://phronesis.world/laserbrain
|
|
8
|
+
Project-URL: Research, https://phronesis.world/laserbrain/research
|
|
9
|
+
Project-URL: Demo, https://phronesis.world/laserbrain/demo
|
|
10
|
+
Keywords: ai,agents,oversight,drift,mcp,llm,multi-agent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# laserbrain
|
|
15
|
+
|
|
16
|
+
Attach the **smart recursion harness** to any agent loop — a provably-correct,
|
|
17
|
+
external check for when an AI agent (or a team of agents) has drifted from its goal.
|
|
18
|
+
|
|
19
|
+
An agent watching only itself provably can't catch its own drift: each step looks
|
|
20
|
+
fine next to the last while it wanders far from where it began. laserbrain is a
|
|
21
|
+
**fixed reference** it checks against instead. There's a proof.
|
|
22
|
+
[The theorem and the studies (nulls included).](https://phronesis.world/laserbrain/research)
|
|
23
|
+
· [Watch it work.](https://phronesis.world/laserbrain/demo)
|
|
24
|
+
|
|
25
|
+
The check is a pure function, so this SDK runs it **locally and free** — no key, no
|
|
26
|
+
latency. Add a key and it also mirrors to the API for retained drift history,
|
|
27
|
+
alerts and the fleet view: you pay to *see* your agents drift, not for the check.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install laserbrain
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The check (local, free)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from laserbrain import Harness
|
|
37
|
+
|
|
38
|
+
hz = Harness() # add key="lb_live_…" to also retain history
|
|
39
|
+
v = hz.check(goal="build the JSON parser", progress="advancing", distance=6)
|
|
40
|
+
if v.drifting:
|
|
41
|
+
print(v.reason, "—", v.advice) # e.g. "goal-drift — your goal no longer matches…"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`progress` is one of `advancing | stuck | circling`; `distance` is 0–10 to done.
|
|
45
|
+
Reasons: `advancing`, `grounded`, `goal-drift`, `stalled`, `self-report:stuck/circling`,
|
|
46
|
+
`ungrammatical`.
|
|
47
|
+
|
|
48
|
+
## The act layer — close the loop
|
|
49
|
+
|
|
50
|
+
Give laserbrain your step function and it detects drift *and* injects the return, so
|
|
51
|
+
the agent recovers instead of spinning. Your step reads `ctx["return"]` and steers back.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def step(ctx):
|
|
55
|
+
if ctx.get("return"): # laserbrain told us to return to ground
|
|
56
|
+
... # steer the agent back toward its goal
|
|
57
|
+
...
|
|
58
|
+
return dict(goal="build the JSON parser", progress="advancing", distance=d, done=d == 0)
|
|
59
|
+
|
|
60
|
+
ctx = Harness().run(step, on_return=lambda v, ctx: print("↩", v.advice))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Recursion teams — styled multi-agent oversight
|
|
64
|
+
|
|
65
|
+
A **recursion team** styles each role's recursion: a `deep` explorer tolerates
|
|
66
|
+
displacement, a `tight` checker returns fast. laserbrain runs the team, watches the
|
|
67
|
+
shared goal (the fixed reference), and injects the return per role — catching the
|
|
68
|
+
**echo/agreement spiral** a self-watching group can't see.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from laserbrain import Team
|
|
72
|
+
|
|
73
|
+
def agent(role, history, injected):
|
|
74
|
+
# your LLM call for this role; `injected` is a return-to-ground note (or None)
|
|
75
|
+
return position, distance
|
|
76
|
+
|
|
77
|
+
Team("adversarial-deliberation", goal="…").run(agent)
|
|
78
|
+
# presets: deep-search · iterative-refinement · adversarial-deliberation
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Oversight, provenance, continuity
|
|
82
|
+
|
|
83
|
+
**Human-in-the-loop.** A self-correcting return usually takes. When it doesn't —
|
|
84
|
+
the agent keeps drifting past `escalate_after` steps — laserbrain escalates *that
|
|
85
|
+
drift* to a human. The human doesn't watch every step; they see only what the fixed
|
|
86
|
+
reference caught. Their decision overrides the auto-return.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
def on_escalate(v, ctx):
|
|
90
|
+
return ask_a_human(v.reason, v.advice) # Slack, a queue, a webhook — you wire it
|
|
91
|
+
# returning a decision injects it as the return
|
|
92
|
+
Harness().run(step, escalate_after=3, on_escalate=on_escalate)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Provenance.** Every check is written to a hash-chained ledger — tamper-evident and
|
|
96
|
+
verifiable offline, by anyone, no key. Editing a past verdict to hide a drift breaks
|
|
97
|
+
the chain at that link.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
hz.export_audit("run.json")
|
|
101
|
+
from laserbrain import verify_audit
|
|
102
|
+
verify_audit(json.load(open("run.json"))) # (True, -1) intact · (False, i) broken at link i
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Team continuity.** Snapshot a running team and resume it in a later session — the
|
|
106
|
+
shared goal (the fixed reference) and the dialogue carry over, so the group re-grounds
|
|
107
|
+
instead of starting cold.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
snap = team.snapshot() # JSON-safe; persist it anywhere
|
|
111
|
+
team = Team.restore(snap) # keeps watching the same ground
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Framework adapters
|
|
115
|
+
|
|
116
|
+
Already on LangGraph, CrewAI, AutoGen, or the OpenAI Agents SDK? Attach laserbrain
|
|
117
|
+
without changing your loop. Because it checks a fixed reference, it needs the agent
|
|
118
|
+
to *spell* its state — so each adapter takes an `extract` that maps your framework's
|
|
119
|
+
state to `(goal, progress, distance)`. **No adapter imports a framework**: each
|
|
120
|
+
returns a plain callable you hand to the framework's own hook, so install only the
|
|
121
|
+
one you use.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from laserbrain.adapters import guard, langgraph_node, crewai_step_callback, middleware
|
|
125
|
+
|
|
126
|
+
# generic — wrap any step that returns dict(goal=, progress=, distance=)
|
|
127
|
+
@guard
|
|
128
|
+
def step(state): ...
|
|
129
|
+
|
|
130
|
+
# LangGraph — a node that writes the Verdict into graph state; branch on it
|
|
131
|
+
g.add_node("laserbrain", langgraph_node(extract=lambda s: (s["goal"], "advancing", s["dist"])))
|
|
132
|
+
g.add_edge("agent", "laserbrain")
|
|
133
|
+
g.add_conditional_edges("laserbrain",
|
|
134
|
+
lambda s: "return" if s["laserbrain"].drifting else "agent") # .advice steers the return
|
|
135
|
+
|
|
136
|
+
# CrewAI — a step_callback that fires each agent step
|
|
137
|
+
Agent(..., step_callback=crewai_step_callback(lambda o: (o.goal, o.status, o.dist)))
|
|
138
|
+
|
|
139
|
+
# anything else (AutoGen, OpenAI Agents, a custom loop) — one check per step
|
|
140
|
+
lb = middleware(extract=my_extract)
|
|
141
|
+
v = lb(step_output)
|
|
142
|
+
if v.drifting: reinject(v.advice)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each adapter runs the check locally and free; pass `key=`/`run_id=` (or your own
|
|
146
|
+
`Harness`) to also retain history.
|
|
147
|
+
|
|
148
|
+
## What's proven, and what isn't
|
|
149
|
+
|
|
150
|
+
The **single-agent** detector mirrors the frozen, published instrument
|
|
151
|
+
(`drift.ts @ 6b483de7`) and rests on a theorem: detection is sound and complete, and
|
|
152
|
+
no self-monitoring agent can be. The **multi-agent** dialogue and recursion teams are
|
|
153
|
+
a prototype extension — useful, not (yet) a theorem. Whether *returning* an agent
|
|
154
|
+
keeps the answer as good is [an honest open question](https://phronesis.world/laserbrain/research);
|
|
155
|
+
this SDK gives you the detection and the return mechanism, and says plainly what each is.
|
|
156
|
+
|
|
157
|
+
MIT · [phronesis.world/laserbrain](https://phronesis.world/laserbrain)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# laserbrain
|
|
2
|
+
|
|
3
|
+
Attach the **smart recursion harness** to any agent loop — a provably-correct,
|
|
4
|
+
external check for when an AI agent (or a team of agents) has drifted from its goal.
|
|
5
|
+
|
|
6
|
+
An agent watching only itself provably can't catch its own drift: each step looks
|
|
7
|
+
fine next to the last while it wanders far from where it began. laserbrain is a
|
|
8
|
+
**fixed reference** it checks against instead. There's a proof.
|
|
9
|
+
[The theorem and the studies (nulls included).](https://phronesis.world/laserbrain/research)
|
|
10
|
+
· [Watch it work.](https://phronesis.world/laserbrain/demo)
|
|
11
|
+
|
|
12
|
+
The check is a pure function, so this SDK runs it **locally and free** — no key, no
|
|
13
|
+
latency. Add a key and it also mirrors to the API for retained drift history,
|
|
14
|
+
alerts and the fleet view: you pay to *see* your agents drift, not for the check.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install laserbrain
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## The check (local, free)
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from laserbrain import Harness
|
|
24
|
+
|
|
25
|
+
hz = Harness() # add key="lb_live_…" to also retain history
|
|
26
|
+
v = hz.check(goal="build the JSON parser", progress="advancing", distance=6)
|
|
27
|
+
if v.drifting:
|
|
28
|
+
print(v.reason, "—", v.advice) # e.g. "goal-drift — your goal no longer matches…"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`progress` is one of `advancing | stuck | circling`; `distance` is 0–10 to done.
|
|
32
|
+
Reasons: `advancing`, `grounded`, `goal-drift`, `stalled`, `self-report:stuck/circling`,
|
|
33
|
+
`ungrammatical`.
|
|
34
|
+
|
|
35
|
+
## The act layer — close the loop
|
|
36
|
+
|
|
37
|
+
Give laserbrain your step function and it detects drift *and* injects the return, so
|
|
38
|
+
the agent recovers instead of spinning. Your step reads `ctx["return"]` and steers back.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
def step(ctx):
|
|
42
|
+
if ctx.get("return"): # laserbrain told us to return to ground
|
|
43
|
+
... # steer the agent back toward its goal
|
|
44
|
+
...
|
|
45
|
+
return dict(goal="build the JSON parser", progress="advancing", distance=d, done=d == 0)
|
|
46
|
+
|
|
47
|
+
ctx = Harness().run(step, on_return=lambda v, ctx: print("↩", v.advice))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Recursion teams — styled multi-agent oversight
|
|
51
|
+
|
|
52
|
+
A **recursion team** styles each role's recursion: a `deep` explorer tolerates
|
|
53
|
+
displacement, a `tight` checker returns fast. laserbrain runs the team, watches the
|
|
54
|
+
shared goal (the fixed reference), and injects the return per role — catching the
|
|
55
|
+
**echo/agreement spiral** a self-watching group can't see.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from laserbrain import Team
|
|
59
|
+
|
|
60
|
+
def agent(role, history, injected):
|
|
61
|
+
# your LLM call for this role; `injected` is a return-to-ground note (or None)
|
|
62
|
+
return position, distance
|
|
63
|
+
|
|
64
|
+
Team("adversarial-deliberation", goal="…").run(agent)
|
|
65
|
+
# presets: deep-search · iterative-refinement · adversarial-deliberation
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Oversight, provenance, continuity
|
|
69
|
+
|
|
70
|
+
**Human-in-the-loop.** A self-correcting return usually takes. When it doesn't —
|
|
71
|
+
the agent keeps drifting past `escalate_after` steps — laserbrain escalates *that
|
|
72
|
+
drift* to a human. The human doesn't watch every step; they see only what the fixed
|
|
73
|
+
reference caught. Their decision overrides the auto-return.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
def on_escalate(v, ctx):
|
|
77
|
+
return ask_a_human(v.reason, v.advice) # Slack, a queue, a webhook — you wire it
|
|
78
|
+
# returning a decision injects it as the return
|
|
79
|
+
Harness().run(step, escalate_after=3, on_escalate=on_escalate)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Provenance.** Every check is written to a hash-chained ledger — tamper-evident and
|
|
83
|
+
verifiable offline, by anyone, no key. Editing a past verdict to hide a drift breaks
|
|
84
|
+
the chain at that link.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
hz.export_audit("run.json")
|
|
88
|
+
from laserbrain import verify_audit
|
|
89
|
+
verify_audit(json.load(open("run.json"))) # (True, -1) intact · (False, i) broken at link i
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Team continuity.** Snapshot a running team and resume it in a later session — the
|
|
93
|
+
shared goal (the fixed reference) and the dialogue carry over, so the group re-grounds
|
|
94
|
+
instead of starting cold.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
snap = team.snapshot() # JSON-safe; persist it anywhere
|
|
98
|
+
team = Team.restore(snap) # keeps watching the same ground
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Framework adapters
|
|
102
|
+
|
|
103
|
+
Already on LangGraph, CrewAI, AutoGen, or the OpenAI Agents SDK? Attach laserbrain
|
|
104
|
+
without changing your loop. Because it checks a fixed reference, it needs the agent
|
|
105
|
+
to *spell* its state — so each adapter takes an `extract` that maps your framework's
|
|
106
|
+
state to `(goal, progress, distance)`. **No adapter imports a framework**: each
|
|
107
|
+
returns a plain callable you hand to the framework's own hook, so install only the
|
|
108
|
+
one you use.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from laserbrain.adapters import guard, langgraph_node, crewai_step_callback, middleware
|
|
112
|
+
|
|
113
|
+
# generic — wrap any step that returns dict(goal=, progress=, distance=)
|
|
114
|
+
@guard
|
|
115
|
+
def step(state): ...
|
|
116
|
+
|
|
117
|
+
# LangGraph — a node that writes the Verdict into graph state; branch on it
|
|
118
|
+
g.add_node("laserbrain", langgraph_node(extract=lambda s: (s["goal"], "advancing", s["dist"])))
|
|
119
|
+
g.add_edge("agent", "laserbrain")
|
|
120
|
+
g.add_conditional_edges("laserbrain",
|
|
121
|
+
lambda s: "return" if s["laserbrain"].drifting else "agent") # .advice steers the return
|
|
122
|
+
|
|
123
|
+
# CrewAI — a step_callback that fires each agent step
|
|
124
|
+
Agent(..., step_callback=crewai_step_callback(lambda o: (o.goal, o.status, o.dist)))
|
|
125
|
+
|
|
126
|
+
# anything else (AutoGen, OpenAI Agents, a custom loop) — one check per step
|
|
127
|
+
lb = middleware(extract=my_extract)
|
|
128
|
+
v = lb(step_output)
|
|
129
|
+
if v.drifting: reinject(v.advice)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Each adapter runs the check locally and free; pass `key=`/`run_id=` (or your own
|
|
133
|
+
`Harness`) to also retain history.
|
|
134
|
+
|
|
135
|
+
## What's proven, and what isn't
|
|
136
|
+
|
|
137
|
+
The **single-agent** detector mirrors the frozen, published instrument
|
|
138
|
+
(`drift.ts @ 6b483de7`) and rests on a theorem: detection is sound and complete, and
|
|
139
|
+
no self-monitoring agent can be. The **multi-agent** dialogue and recursion teams are
|
|
140
|
+
a prototype extension — useful, not (yet) a theorem. Whether *returning* an agent
|
|
141
|
+
keeps the answer as good is [an honest open question](https://phronesis.world/laserbrain/research);
|
|
142
|
+
this SDK gives you the detection and the return mechanism, and says plainly what each is.
|
|
143
|
+
|
|
144
|
+
MIT · [phronesis.world/laserbrain](https://phronesis.world/laserbrain)
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""
|
|
2
|
+
laserbrain — attach the smart recursion harness to any agent loop.
|
|
3
|
+
|
|
4
|
+
The check is a pure function, so it runs LOCALLY and free (the open grammar, no
|
|
5
|
+
key, no latency). Give it a key and it also mirrors to the API for retained drift
|
|
6
|
+
history, alerts and the fleet view — you pay to *see* your agents drift, not for
|
|
7
|
+
the check. Model-agnostic: you provide the agent, laserbrain closes the loop.
|
|
8
|
+
|
|
9
|
+
from laserbrain import Harness
|
|
10
|
+
hz = Harness() # local + free (add key=... to retain)
|
|
11
|
+
v = hz.check(goal="build the parser", progress="advancing", distance=6)
|
|
12
|
+
if v.drifting: ... # v.advice tells the agent to return
|
|
13
|
+
|
|
14
|
+
hz.run(step, on_return=lambda v, ctx: ...) # the act layer: auto-inject the return
|
|
15
|
+
|
|
16
|
+
from laserbrain import Team
|
|
17
|
+
Team("adversarial-deliberation", goal="…").run(agent_fn) # a styled recursion team
|
|
18
|
+
|
|
19
|
+
The single-agent detector mirrors the frozen drift.ts @ 6b483de7 (the published
|
|
20
|
+
instrument); the multi-agent dialogue + recursion teams are the prototype extension.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
import hashlib, json, re, urllib.request
|
|
25
|
+
|
|
26
|
+
__all__ = ['Harness', 'Team', 'Verdict', 'PRESETS', 'norm', 'verify_audit']
|
|
27
|
+
API_DEFAULT = 'https://laserbrain-mcp.degibug.workers.dev'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── provenance: a tamper-evident, hash-chained ledger of every check ───────────
|
|
31
|
+
def _canon(d):
|
|
32
|
+
return json.dumps(d, sort_keys=True, separators=(',', ':'))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _link(prev, body):
|
|
36
|
+
return hashlib.sha256((prev + _canon(body)).encode()).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def verify_audit(chain):
|
|
40
|
+
"""Independently verify an exported audit chain. Returns (ok, first_bad_index):
|
|
41
|
+
(True, -1) if intact, else (False, i) at the first tampered/broken link.
|
|
42
|
+
Free and offline — anyone can audit an agent's run without a key."""
|
|
43
|
+
prev = ''
|
|
44
|
+
for i, rec in enumerate(chain):
|
|
45
|
+
body = {k: rec[k] for k in rec if k != 'hash'}
|
|
46
|
+
if rec.get('prev') != prev or _link(prev, body) != rec.get('hash'):
|
|
47
|
+
return (False, i)
|
|
48
|
+
prev = rec['hash']
|
|
49
|
+
return (True, -1)
|
|
50
|
+
|
|
51
|
+
# ── the fixed-reference primitive (frozen: drift.ts @ 6b483de7) ────────────────
|
|
52
|
+
_STOP = {'the', 'a', 'an', 'to', 'of', 'and', 'or', 'for', 'in', 'on', 'at', 'is', 'it', 'this',
|
|
53
|
+
'that', 'with', 'my', 'your', 'our', 'i', 'we', 'be', 'as', 'by', 'from', 'into', 'out',
|
|
54
|
+
'up', 'so', 'then'}
|
|
55
|
+
_STEM = re.compile(r"(ings?|edly|ed|ers?|es|s|tion|ment)$")
|
|
56
|
+
_PROGRESS = {'advancing', 'stuck', 'circling'}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def norm(s):
|
|
60
|
+
out = set()
|
|
61
|
+
for w in re.findall(r"[a-z0-9']+", str(s).lower()):
|
|
62
|
+
if w in _STOP:
|
|
63
|
+
continue
|
|
64
|
+
r = _STEM.sub('', w) if len(w) > 4 else w
|
|
65
|
+
if r:
|
|
66
|
+
out.add(r)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _jac(a, b):
|
|
71
|
+
if not a and not b:
|
|
72
|
+
return 0.0
|
|
73
|
+
return 1 - len(a & b) / len(a | b)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _sim(a, b):
|
|
77
|
+
return 1 - _jac(a, b)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _asdist(d):
|
|
81
|
+
try:
|
|
82
|
+
return max(0, min(10, int(float(d))))
|
|
83
|
+
except Exception:
|
|
84
|
+
return 5
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _displacement(goal, progress, distance, ground):
|
|
88
|
+
return (0.5 * _jac(norm(goal), norm(ground['goal']))
|
|
89
|
+
+ 0.3 * abs(_asdist(distance) - ground['dist']) / 10
|
|
90
|
+
+ 0.2 * (0 if progress == ground['progress'] else 1))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_DRIFT = ('ungrammatical', 'goal-drift', 'stalled')
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _isdrift(reason):
|
|
97
|
+
return reason in _DRIFT or reason.startswith('self-report')
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class Verdict:
|
|
102
|
+
drifting: bool
|
|
103
|
+
reason: str
|
|
104
|
+
phi: float
|
|
105
|
+
advice: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class _Run:
|
|
109
|
+
"""Single-agent drift state for one task run."""
|
|
110
|
+
def __init__(self):
|
|
111
|
+
self.ground = None
|
|
112
|
+
self.first_goal = set()
|
|
113
|
+
self.dist_hist = []
|
|
114
|
+
self.trace = [] # (reason, drifting)
|
|
115
|
+
|
|
116
|
+
def step(self, goal, progress, distance):
|
|
117
|
+
prev = _isdrift(self.trace[-1][0]) if self.trace else False
|
|
118
|
+
|
|
119
|
+
def emit(reason, drifting, advice, phi=0.0):
|
|
120
|
+
self.trace.append((reason, drifting))
|
|
121
|
+
return Verdict(drifting, reason, round(phi, 2), advice)
|
|
122
|
+
|
|
123
|
+
goal = str(goal or '').strip()
|
|
124
|
+
if not goal or progress not in _PROGRESS:
|
|
125
|
+
return emit('ungrammatical', True, 'You cannot spell a clear goal and a valid progress. Return to ground.')
|
|
126
|
+
d = _asdist(distance)
|
|
127
|
+
if self.ground is None:
|
|
128
|
+
self.ground = {'goal': goal, 'progress': progress, 'dist': d}
|
|
129
|
+
self.first_goal = norm(goal)
|
|
130
|
+
self.dist_hist = [d]
|
|
131
|
+
return emit('grounded', False, 'Ground state set — continue, and check each step.')
|
|
132
|
+
phi = _displacement(goal, progress, d, self.ground)
|
|
133
|
+
if progress in ('stuck', 'circling') and phi > 0.15:
|
|
134
|
+
return emit(f'self-report:{progress}', prev,
|
|
135
|
+
f'You reported {progress} and have moved from ground. Return to your goal.' if prev
|
|
136
|
+
else f'You reported {progress}. If it holds next step, return to ground.', phi)
|
|
137
|
+
g = norm(goal)
|
|
138
|
+
anchor = (len(g & self.first_goal) / len(g | self.first_goal)) if (g or self.first_goal) else 0.0
|
|
139
|
+
if anchor < 0.30:
|
|
140
|
+
return emit('goal-drift', True, f'Your goal no longer matches the one you started with (overlap {anchor:.2f}). Return.', phi)
|
|
141
|
+
self.dist_hist.append(d)
|
|
142
|
+
dh = self.dist_hist
|
|
143
|
+
if len(dh) > 4 and min(dh[-4:]) >= dh[-5]:
|
|
144
|
+
return emit('stalled', prev,
|
|
145
|
+
"Distance stopped falling and you were already off ground — return." if prev
|
|
146
|
+
else "Distance isn't falling. If it holds, return.", phi)
|
|
147
|
+
return emit('advancing', False, f'On track (Φ={phi:.2f}). Continue.', phi)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _post(api, key, path, body):
|
|
151
|
+
try:
|
|
152
|
+
r = urllib.request.Request(api + path, method='POST', data=json.dumps(body).encode(),
|
|
153
|
+
headers={'authorization': f'Bearer {key}', 'content-type': 'application/json',
|
|
154
|
+
'user-agent': 'laserbrain-sdk/0.2'})
|
|
155
|
+
with urllib.request.urlopen(r, timeout=8) as resp:
|
|
156
|
+
return json.load(resp)
|
|
157
|
+
except Exception:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _get(api, key, path):
|
|
162
|
+
try:
|
|
163
|
+
r = urllib.request.Request(api + path, headers={'authorization': f'Bearer {key}',
|
|
164
|
+
'user-agent': 'laserbrain-sdk/0.2'})
|
|
165
|
+
with urllib.request.urlopen(r, timeout=8) as resp:
|
|
166
|
+
return json.load(resp)
|
|
167
|
+
except Exception:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Harness:
|
|
172
|
+
"""Single-agent harness. Local + free; pass key= to also retain history via the API."""
|
|
173
|
+
def __init__(self, key=None, run_id=None, api=API_DEFAULT):
|
|
174
|
+
self.key, self.api = key, api
|
|
175
|
+
self.run_id = run_id or 'run'
|
|
176
|
+
self._run = _Run()
|
|
177
|
+
self._audit = [] # append-only, hash-chained ledger (survives reset)
|
|
178
|
+
|
|
179
|
+
def _record(self, goal, progress, distance, v):
|
|
180
|
+
prev = self._audit[-1]['hash'] if self._audit else ''
|
|
181
|
+
body = {'i': len(self._audit), 'run_id': self.run_id, 'goal': str(goal or ''),
|
|
182
|
+
'progress': progress, 'distance': _asdist(distance),
|
|
183
|
+
'reason': v.reason, 'drifting': v.drifting, 'phi': v.phi, 'prev': prev}
|
|
184
|
+
body['hash'] = _link(prev, {k: body[k] for k in body if k != 'hash'})
|
|
185
|
+
self._audit.append(body)
|
|
186
|
+
|
|
187
|
+
def check(self, goal, progress='advancing', distance=5, tokens=None, overhead=False) -> Verdict:
|
|
188
|
+
v = self._run.step(goal, progress, distance)
|
|
189
|
+
self._record(goal, progress, distance, v)
|
|
190
|
+
if self.key: # mirror to the API for retained history / alerts (best-effort)
|
|
191
|
+
body = {'run_id': self.run_id, 'goal': goal, 'progress': progress, 'distance': distance}
|
|
192
|
+
if tokens is not None:
|
|
193
|
+
body['tokens'] = tokens
|
|
194
|
+
body['overhead'] = overhead
|
|
195
|
+
_post(self.api, self.key, '/v1/drift', body)
|
|
196
|
+
return v
|
|
197
|
+
|
|
198
|
+
def audit(self):
|
|
199
|
+
"""The tamper-evident ledger of every check this harness ran. Verify it with
|
|
200
|
+
laserbrain.verify_audit(chain) — offline, no key. Append-only across reset()."""
|
|
201
|
+
return list(self._audit)
|
|
202
|
+
|
|
203
|
+
def export_audit(self, path):
|
|
204
|
+
"""Write the audit chain to a JSON file a reviewer can independently verify."""
|
|
205
|
+
with open(path, 'w') as f:
|
|
206
|
+
json.dump(self._audit, f, indent=2)
|
|
207
|
+
return path
|
|
208
|
+
|
|
209
|
+
def escalate(self, verdict, streak=None, detail=None):
|
|
210
|
+
"""Raise a persisting drift to the hosted human-in-the-loop queue (needs key).
|
|
211
|
+
The local on_escalate hook is the general mechanism; this is the managed
|
|
212
|
+
surface (a review queue + a decision a human makes on the dashboard).
|
|
213
|
+
Returns the escalation id to poll with resolution(), or None offline."""
|
|
214
|
+
if not self.key:
|
|
215
|
+
return None
|
|
216
|
+
body = {'run_id': self.run_id, 'reason': verdict.reason, 'advice': verdict.advice}
|
|
217
|
+
if streak is not None:
|
|
218
|
+
body['streak'] = streak
|
|
219
|
+
if detail:
|
|
220
|
+
body['detail'] = detail
|
|
221
|
+
return (_post(self.api, self.key, '/v1/escalation', body) or {}).get('esc_id')
|
|
222
|
+
|
|
223
|
+
def resolution(self, esc_id):
|
|
224
|
+
"""Poll a hosted escalation for the human's decision. Returns
|
|
225
|
+
{'decision': 'return'|'allow'|'stop', 'note': …} once decided, else None."""
|
|
226
|
+
if not self.key or not esc_id:
|
|
227
|
+
return None
|
|
228
|
+
r = _get(self.api, self.key, f'/v1/escalation?id={esc_id}')
|
|
229
|
+
if r and r.get('status') == 'decided':
|
|
230
|
+
return {'decision': r.get('decision'), 'note': r.get('note')}
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
def reset(self):
|
|
234
|
+
self._run = _Run() # new task run; the audit ledger keeps accumulating
|
|
235
|
+
|
|
236
|
+
def run(self, step, max_steps=30, on_return=None, escalate_after=None, on_escalate=None):
|
|
237
|
+
"""The act layer. `step(ctx)` -> dict(goal, progress, distance, tokens?, done?).
|
|
238
|
+
On drift, on_return(verdict, ctx) fires and ctx['return'] = advice so your next
|
|
239
|
+
step can steer back. If the drift *persists* for `escalate_after` steps without
|
|
240
|
+
recovering, on_escalate(verdict, ctx) fires — the human-in-the-loop hook; if it
|
|
241
|
+
returns a decision string, that overrides the auto-return (a human's call is
|
|
242
|
+
injected instead). Returns the final ctx."""
|
|
243
|
+
ctx = {'returns': 0, 'streak': 0}
|
|
244
|
+
for _ in range(max_steps):
|
|
245
|
+
s = step(ctx) or {}
|
|
246
|
+
v = self.check(s.get('goal', ''), s.get('progress', 'advancing'), s.get('distance', 5), s.get('tokens'))
|
|
247
|
+
ctx['verdict'] = v
|
|
248
|
+
if v.drifting:
|
|
249
|
+
ctx['returns'] += 1
|
|
250
|
+
ctx['streak'] += 1
|
|
251
|
+
ctx['return'] = v.advice # the act: inject the return into the loop
|
|
252
|
+
if on_return:
|
|
253
|
+
on_return(v, ctx) # on_return is a notification hook, not the injection
|
|
254
|
+
if escalate_after and ctx['streak'] >= escalate_after and not ctx.get('escalated'):
|
|
255
|
+
ctx['escalated'] = True # a self-correcting return didn't take — get a human
|
|
256
|
+
decision = (on_escalate or (lambda v, c: None))(v, ctx)
|
|
257
|
+
if decision: # a human's decision overrides the auto-return
|
|
258
|
+
ctx['return'] = ctx['decision'] = decision
|
|
259
|
+
else:
|
|
260
|
+
ctx['streak'] = 0 # recovered — a fresh streak can escalate again
|
|
261
|
+
ctx.pop('return', None)
|
|
262
|
+
ctx.pop('escalated', None)
|
|
263
|
+
if s.get('done') or _asdist(s.get('distance', 5)) == 0:
|
|
264
|
+
break
|
|
265
|
+
return ctx
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ── multi-agent: dialogue + recursion teams (prototype extension) ──────────────
|
|
269
|
+
_ECHO_MIN, _PROG_WIN, _GOAL_MIN = 0.25, 3, 0.30
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class _Dialogue:
|
|
273
|
+
def __init__(self, goal):
|
|
274
|
+
self.goal = norm(goal)
|
|
275
|
+
self.dist_hist, self.echo_hist, self.turns = [], [], []
|
|
276
|
+
|
|
277
|
+
def step(self, agent, position, distance, restated_goal=None):
|
|
278
|
+
pos = norm(position)
|
|
279
|
+
d = _asdist(distance)
|
|
280
|
+
|
|
281
|
+
def emit(reason, drifting, advice, echo=0.0):
|
|
282
|
+
self.turns.append({'agent': agent, 'pos': pos, 'reason': reason, 'drifting': drifting})
|
|
283
|
+
return {'reason': reason, 'drifting': drifting, 'echo': round(echo, 2), 'dist': d, 'advice': advice}
|
|
284
|
+
|
|
285
|
+
if self.goal and not self.turns and not restated_goal and not pos:
|
|
286
|
+
pass
|
|
287
|
+
if not self.goal:
|
|
288
|
+
g = norm(restated_goal or position or '')
|
|
289
|
+
if not g:
|
|
290
|
+
return emit('ungrammatical', True, 'The first turn must spell the shared goal.')
|
|
291
|
+
self.goal = g
|
|
292
|
+
self.dist_hist, self.echo_hist = [d], [0.0]
|
|
293
|
+
return emit('grounded', False, 'Shared goal set — the fixed reference for the group.')
|
|
294
|
+
if not pos:
|
|
295
|
+
return emit('ungrammatical', True, 'This agent cannot spell its position.')
|
|
296
|
+
others = [set(t['pos']) for t in self.turns[-3:] if t['agent'] != agent]
|
|
297
|
+
echo = max((_sim(pos, o) for o in others), default=0.0)
|
|
298
|
+
self.echo_hist.append(echo)
|
|
299
|
+
mean_echo = sum(self.echo_hist[-3:]) / len(self.echo_hist[-3:])
|
|
300
|
+
self.dist_hist.append(d)
|
|
301
|
+
dh = self.dist_hist
|
|
302
|
+
stalled = len(dh) > _PROG_WIN and dh[-1] >= dh[-1 - _PROG_WIN]
|
|
303
|
+
if restated_goal and _sim(norm(restated_goal), self.goal) < _GOAL_MIN:
|
|
304
|
+
return emit('topic-drift', True, 'The dialogue has left the shared goal — return to it.', echo)
|
|
305
|
+
last = self.turns[-1]['reason'] if self.turns else None
|
|
306
|
+
if stalled and mean_echo >= _ECHO_MIN:
|
|
307
|
+
return emit('echo-spiral', last == 'echo-spiral', 'The agents agree while the goal gets no closer — break the loop.', echo)
|
|
308
|
+
if stalled:
|
|
309
|
+
return emit('deliberation-stall', last == 'deliberation-stall', 'No progress toward the shared goal — return to it.', echo)
|
|
310
|
+
return emit('advancing', False, f'On track — closing on the goal (dist {d}).', echo)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
_ALL_MODES = ['ungrammatical', 'topic-drift', 'echo-spiral', 'deliberation-stall', 'goal-drift', 'stalled', 'self-report:stuck', 'self-report:circling']
|
|
314
|
+
_DEPTH = {
|
|
315
|
+
'deep': {'ungrammatical', 'topic-drift', 'goal-drift'},
|
|
316
|
+
'balanced': {'ungrammatical', 'topic-drift', 'goal-drift', 'echo-spiral', 'self-report:stuck', 'self-report:circling'},
|
|
317
|
+
'tight': set(_ALL_MODES),
|
|
318
|
+
}
|
|
319
|
+
PRESETS = {
|
|
320
|
+
'deep-search': [
|
|
321
|
+
{'role': 'explorer', 'recurse': 'deep'},
|
|
322
|
+
{'role': 'checker', 'recurse': 'tight', 'return': 'Restate the goal and verify the last step against it.'},
|
|
323
|
+
],
|
|
324
|
+
'iterative-refinement': [
|
|
325
|
+
{'role': 'drafter', 'recurse': 'balanced'},
|
|
326
|
+
{'role': 'critic', 'recurse': 'balanced', 'modes': ['echo-spiral', 'topic-drift', 'ungrammatical'], 'return': 'You are agreeing, not improving. Name one concrete flaw and change it.'},
|
|
327
|
+
],
|
|
328
|
+
'adversarial-deliberation': [
|
|
329
|
+
{'role': 'advocate-a', 'recurse': 'deep'},
|
|
330
|
+
{'role': 'advocate-b', 'recurse': 'deep'},
|
|
331
|
+
{'role': 'synthesizer', 'recurse': 'tight', 'return': 'The debate is looping. State the single decision that resolves the shared goal.'},
|
|
332
|
+
],
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _style_return(reason, role):
|
|
337
|
+
if reason in ('advancing', 'grounded'):
|
|
338
|
+
return False
|
|
339
|
+
acts = set(role['modes']) if role.get('modes') else _DEPTH.get(role['recurse'], _DEPTH['balanced'])
|
|
340
|
+
return reason in acts
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class Team:
|
|
344
|
+
"""Run a styled recursion team and close the loop — detect, then inject the return."""
|
|
345
|
+
def __init__(self, preset, goal, key=None, api=API_DEFAULT):
|
|
346
|
+
if isinstance(preset, str):
|
|
347
|
+
if preset not in PRESETS:
|
|
348
|
+
raise ValueError(f'unknown preset {preset!r}; choose {list(PRESETS)}')
|
|
349
|
+
self.roles = PRESETS[preset]
|
|
350
|
+
self.name = preset
|
|
351
|
+
else:
|
|
352
|
+
self.roles, self.name = list(preset), 'custom'
|
|
353
|
+
self.goal, self.key, self.api = goal, key, api
|
|
354
|
+
self._dlg = _Dialogue(goal)
|
|
355
|
+
|
|
356
|
+
def snapshot(self):
|
|
357
|
+
"""Serialize the team's shared ground + dialogue so a later session can resume
|
|
358
|
+
it instead of starting cold — subjective continuity for a group. JSON-safe."""
|
|
359
|
+
d = self._dlg
|
|
360
|
+
return {'name': self.name, 'roles': self.roles, 'goal': self.goal,
|
|
361
|
+
'dlg': {'goal': sorted(d.goal), 'dist_hist': list(d.dist_hist), 'echo_hist': list(d.echo_hist),
|
|
362
|
+
'turns': [{'agent': t['agent'], 'pos': sorted(t['pos']),
|
|
363
|
+
'reason': t['reason'], 'drifting': t['drifting']} for t in d.turns]}}
|
|
364
|
+
|
|
365
|
+
@classmethod
|
|
366
|
+
def restore(cls, snap, key=None, api=API_DEFAULT):
|
|
367
|
+
"""Resume a team from snapshot(): the shared goal (the fixed reference) and the
|
|
368
|
+
dialogue history carry over, so the group keeps watching the same ground."""
|
|
369
|
+
t = cls(list(snap['roles']), snap['goal'], key=key, api=api)
|
|
370
|
+
t.name = snap['name']
|
|
371
|
+
d = t._dlg
|
|
372
|
+
s = snap['dlg']
|
|
373
|
+
d.goal = set(s['goal'])
|
|
374
|
+
d.dist_hist, d.echo_hist = list(s['dist_hist']), list(s['echo_hist'])
|
|
375
|
+
d.turns = [{'agent': x['agent'], 'pos': set(x['pos']), 'reason': x['reason'],
|
|
376
|
+
'drifting': x['drifting']} for x in s['turns']]
|
|
377
|
+
return t
|
|
378
|
+
|
|
379
|
+
def run(self, agent_fn, max_turns=12, on_return=None, verbose=True):
|
|
380
|
+
"""`agent_fn(role, history, injected) -> (position, distance)`. On a role's
|
|
381
|
+
policy firing, its return advice is injected into the NEXT turn. Returns a
|
|
382
|
+
transcript list of dicts."""
|
|
383
|
+
transcript, injected = [], None
|
|
384
|
+
for turn in range(max_turns):
|
|
385
|
+
role = self.roles[turn % len(self.roles)]
|
|
386
|
+
pos, dist = agent_fn(role, transcript, injected)
|
|
387
|
+
injected = None
|
|
388
|
+
r = self._dlg.step(role['role'], pos, dist)
|
|
389
|
+
act = _style_return(r['reason'], role)
|
|
390
|
+
rec = {'turn': turn, 'role': role['role'], 'recurse': role['recurse'],
|
|
391
|
+
'reason': r['reason'], 'echo': r['echo'], 'dist': r['dist'], 'return': act}
|
|
392
|
+
transcript.append(rec)
|
|
393
|
+
if verbose:
|
|
394
|
+
print(f" {role['role']:12}({role['recurse']:8}): {r['reason']:18} echo={r['echo']:<4} dist={r['dist']}" + (' ↩ RETURN' if act else ''))
|
|
395
|
+
if self.key:
|
|
396
|
+
_post(self.api, self.key, '/v1/dialogue',
|
|
397
|
+
{'conv_id': self.name, 'agent': role['role'], 'position': pos, 'distance': dist,
|
|
398
|
+
'team': self.name, 'role': role['role'], **({'goal': self.goal} if turn == 0 else {})})
|
|
399
|
+
if act:
|
|
400
|
+
injected = role.get('return', 'Return to the shared goal and take the step that most directly resolves it.')
|
|
401
|
+
(on_return or (lambda a, c: None))(injected, rec)
|
|
402
|
+
if _asdist(dist) == 0:
|
|
403
|
+
if verbose:
|
|
404
|
+
print(' ✓ resolved.')
|
|
405
|
+
break
|
|
406
|
+
return transcript
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# framework adapters (LangGraph, CrewAI, generic) — imported last to avoid a cycle
|
|
410
|
+
from .adapters import guard, langgraph_node, crewai_step_callback, middleware # noqa: E402
|
|
411
|
+
__all__ += ['guard', 'langgraph_node', 'crewai_step_callback', 'middleware']
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
laserbrain.adapters — attach the harness to the popular agent frameworks.
|
|
3
|
+
|
|
4
|
+
laserbrain checks a fixed reference, so it needs the agent to *spell* its state.
|
|
5
|
+
Frameworks don't hand you that, so every adapter takes an `extract` you provide:
|
|
6
|
+
|
|
7
|
+
extract(x) -> (goal:str, progress:'advancing'|'stuck'|'circling', distance:0..10)
|
|
8
|
+
|
|
9
|
+
Nothing here imports a framework. Each adapter returns a plain callable you pass
|
|
10
|
+
to the framework's own hook — a graph node, a step_callback, a per-step check — so
|
|
11
|
+
the frameworks stay optional: install only the one you use. The check still runs
|
|
12
|
+
locally and free; pass key=/run_id= (or your own Harness) to also retain history.
|
|
13
|
+
|
|
14
|
+
from laserbrain.adapters import guard, langgraph_node, crewai_step_callback
|
|
15
|
+
|
|
16
|
+
@guard # step returns dict(goal=, progress=, distance=)
|
|
17
|
+
def step(state): ...
|
|
18
|
+
|
|
19
|
+
g.add_node('laserbrain', langgraph_node(extract=my_extract)) # LangGraph
|
|
20
|
+
Agent(..., step_callback=crewai_step_callback(my_extract)) # CrewAI
|
|
21
|
+
v = middleware(my_extract)(obj) # anything else: one call per step
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
from . import Harness, Verdict
|
|
25
|
+
|
|
26
|
+
__all__ = ['guard', 'langgraph_node', 'crewai_step_callback', 'middleware', 'dict_extract']
|
|
27
|
+
|
|
28
|
+
STATE_KEY = 'laserbrain'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _harness(harness, key, run_id):
|
|
32
|
+
return harness if harness is not None else Harness(key=key, run_id=run_id or 'run')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def dict_extract(x):
|
|
36
|
+
"""Default extractor: read (goal, progress, distance) from a dict or an object
|
|
37
|
+
carrying those keys/attributes."""
|
|
38
|
+
get = x.get if isinstance(x, dict) else (lambda k, d=None: getattr(x, k, d))
|
|
39
|
+
return get('goal', ''), get('progress', 'advancing'), get('distance', 5)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── 1. generic decorator — the framework-agnostic core ─────────────────────────
|
|
43
|
+
def guard(fn=None, *, extract=None, harness=None, key=None, run_id=None, on_return=None):
|
|
44
|
+
"""Wrap any agent step. After it runs, laserbrain checks the result; the Verdict
|
|
45
|
+
is attached (result['laserbrain'] when the result is a dict), and on drift
|
|
46
|
+
on_return(verdict, result) is called if given.
|
|
47
|
+
|
|
48
|
+
@guard # result is dict(goal, progress, distance)
|
|
49
|
+
def step(state): ...
|
|
50
|
+
|
|
51
|
+
@guard(extract=lambda out: (out.goal, out.status, out.dist))
|
|
52
|
+
def step(state): ...
|
|
53
|
+
|
|
54
|
+
The wrapped function gains a `.harness` attribute for reset()/history."""
|
|
55
|
+
hz = _harness(harness, key, run_id)
|
|
56
|
+
ex = extract or dict_extract
|
|
57
|
+
|
|
58
|
+
def wrap(f):
|
|
59
|
+
def wrapped(*a, **k):
|
|
60
|
+
out = f(*a, **k)
|
|
61
|
+
g, p, d = ex(out)
|
|
62
|
+
v = hz.check(g, p, d)
|
|
63
|
+
if isinstance(out, dict):
|
|
64
|
+
out[STATE_KEY] = v
|
|
65
|
+
if v.drifting and on_return:
|
|
66
|
+
on_return(v, out)
|
|
67
|
+
return out
|
|
68
|
+
wrapped.harness = hz
|
|
69
|
+
return wrapped
|
|
70
|
+
|
|
71
|
+
return wrap(fn) if callable(fn) else wrap
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── 2. LangGraph — a node that writes the Verdict into graph state ─────────────
|
|
75
|
+
def langgraph_node(extract=None, harness=None, key=None, run_id=None, state_key=STATE_KEY):
|
|
76
|
+
"""Return a LangGraph node: state -> {state_key: Verdict}. Add it after your
|
|
77
|
+
agent node and branch on the verdict with a conditional edge.
|
|
78
|
+
|
|
79
|
+
g.add_node('laserbrain', langgraph_node(extract=my_extract))
|
|
80
|
+
g.add_edge('agent', 'laserbrain')
|
|
81
|
+
g.add_conditional_edges('laserbrain',
|
|
82
|
+
lambda s: 'return' if s['laserbrain'].drifting else 'agent')
|
|
83
|
+
|
|
84
|
+
On drift, state['laserbrain'].advice is the return-to-ground message to feed
|
|
85
|
+
back into your agent node (your 'return' branch)."""
|
|
86
|
+
hz = _harness(harness, key, run_id)
|
|
87
|
+
ex = extract or dict_extract
|
|
88
|
+
|
|
89
|
+
def node(state):
|
|
90
|
+
g, p, d = ex(state)
|
|
91
|
+
return {state_key: hz.check(g, p, d)}
|
|
92
|
+
|
|
93
|
+
node.harness = hz
|
|
94
|
+
return node
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── 3. CrewAI — a step_callback that fires each agent step ─────────────────────
|
|
98
|
+
def crewai_step_callback(extract, harness=None, key=None, run_id=None, on_return=None):
|
|
99
|
+
"""Return a callback for CrewAI's step_callback=. It fires on each agent step;
|
|
100
|
+
extract(step_output) -> (goal, progress, distance). On drift it calls
|
|
101
|
+
on_return(verdict, step_output) — default prints the return advice.
|
|
102
|
+
|
|
103
|
+
Agent(..., step_callback=crewai_step_callback(my_extract))
|
|
104
|
+
Crew(..., step_callback=crewai_step_callback(my_extract))"""
|
|
105
|
+
hz = _harness(harness, key, run_id)
|
|
106
|
+
on_return = on_return or (lambda v, o: print(f'[laserbrain] ↩ {v.reason}: {v.advice}'))
|
|
107
|
+
|
|
108
|
+
def cb(step_output):
|
|
109
|
+
try:
|
|
110
|
+
g, p, d = extract(step_output)
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
v = hz.check(g, p, d)
|
|
114
|
+
if v.drifting:
|
|
115
|
+
on_return(v, step_output)
|
|
116
|
+
return v
|
|
117
|
+
|
|
118
|
+
cb.harness = hz
|
|
119
|
+
return cb
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── 4. generic middleware — one call per step, for anything else ───────────────
|
|
123
|
+
def middleware(extract=None, harness=None, key=None, run_id=None):
|
|
124
|
+
"""Return a per-step checker `check(x) -> Verdict` for any framework that gives
|
|
125
|
+
you a hook (AutoGen reply functions, the OpenAI Agents SDK, a custom loop).
|
|
126
|
+
Call it wherever a step completes; branch on verdict.drifting and feed
|
|
127
|
+
verdict.advice back to steer the agent.
|
|
128
|
+
|
|
129
|
+
lb = middleware(extract=my_extract)
|
|
130
|
+
v = lb(step_output)
|
|
131
|
+
if v.drifting: reinject(v.advice)"""
|
|
132
|
+
hz = _harness(harness, key, run_id)
|
|
133
|
+
ex = extract or dict_extract
|
|
134
|
+
|
|
135
|
+
def check(x):
|
|
136
|
+
g, p, d = ex(x)
|
|
137
|
+
return hz.check(g, p, d)
|
|
138
|
+
|
|
139
|
+
check.harness = hz
|
|
140
|
+
return check
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: laserbrain
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Attach the smart recursion harness to any agent loop — a provably-correct, external check for when an AI agent (or team) has drifted from its goal.
|
|
5
|
+
Author: phronesis
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://phronesis.world/laserbrain
|
|
8
|
+
Project-URL: Research, https://phronesis.world/laserbrain/research
|
|
9
|
+
Project-URL: Demo, https://phronesis.world/laserbrain/demo
|
|
10
|
+
Keywords: ai,agents,oversight,drift,mcp,llm,multi-agent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# laserbrain
|
|
15
|
+
|
|
16
|
+
Attach the **smart recursion harness** to any agent loop — a provably-correct,
|
|
17
|
+
external check for when an AI agent (or a team of agents) has drifted from its goal.
|
|
18
|
+
|
|
19
|
+
An agent watching only itself provably can't catch its own drift: each step looks
|
|
20
|
+
fine next to the last while it wanders far from where it began. laserbrain is a
|
|
21
|
+
**fixed reference** it checks against instead. There's a proof.
|
|
22
|
+
[The theorem and the studies (nulls included).](https://phronesis.world/laserbrain/research)
|
|
23
|
+
· [Watch it work.](https://phronesis.world/laserbrain/demo)
|
|
24
|
+
|
|
25
|
+
The check is a pure function, so this SDK runs it **locally and free** — no key, no
|
|
26
|
+
latency. Add a key and it also mirrors to the API for retained drift history,
|
|
27
|
+
alerts and the fleet view: you pay to *see* your agents drift, not for the check.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install laserbrain
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The check (local, free)
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from laserbrain import Harness
|
|
37
|
+
|
|
38
|
+
hz = Harness() # add key="lb_live_…" to also retain history
|
|
39
|
+
v = hz.check(goal="build the JSON parser", progress="advancing", distance=6)
|
|
40
|
+
if v.drifting:
|
|
41
|
+
print(v.reason, "—", v.advice) # e.g. "goal-drift — your goal no longer matches…"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`progress` is one of `advancing | stuck | circling`; `distance` is 0–10 to done.
|
|
45
|
+
Reasons: `advancing`, `grounded`, `goal-drift`, `stalled`, `self-report:stuck/circling`,
|
|
46
|
+
`ungrammatical`.
|
|
47
|
+
|
|
48
|
+
## The act layer — close the loop
|
|
49
|
+
|
|
50
|
+
Give laserbrain your step function and it detects drift *and* injects the return, so
|
|
51
|
+
the agent recovers instead of spinning. Your step reads `ctx["return"]` and steers back.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
def step(ctx):
|
|
55
|
+
if ctx.get("return"): # laserbrain told us to return to ground
|
|
56
|
+
... # steer the agent back toward its goal
|
|
57
|
+
...
|
|
58
|
+
return dict(goal="build the JSON parser", progress="advancing", distance=d, done=d == 0)
|
|
59
|
+
|
|
60
|
+
ctx = Harness().run(step, on_return=lambda v, ctx: print("↩", v.advice))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Recursion teams — styled multi-agent oversight
|
|
64
|
+
|
|
65
|
+
A **recursion team** styles each role's recursion: a `deep` explorer tolerates
|
|
66
|
+
displacement, a `tight` checker returns fast. laserbrain runs the team, watches the
|
|
67
|
+
shared goal (the fixed reference), and injects the return per role — catching the
|
|
68
|
+
**echo/agreement spiral** a self-watching group can't see.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from laserbrain import Team
|
|
72
|
+
|
|
73
|
+
def agent(role, history, injected):
|
|
74
|
+
# your LLM call for this role; `injected` is a return-to-ground note (or None)
|
|
75
|
+
return position, distance
|
|
76
|
+
|
|
77
|
+
Team("adversarial-deliberation", goal="…").run(agent)
|
|
78
|
+
# presets: deep-search · iterative-refinement · adversarial-deliberation
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Oversight, provenance, continuity
|
|
82
|
+
|
|
83
|
+
**Human-in-the-loop.** A self-correcting return usually takes. When it doesn't —
|
|
84
|
+
the agent keeps drifting past `escalate_after` steps — laserbrain escalates *that
|
|
85
|
+
drift* to a human. The human doesn't watch every step; they see only what the fixed
|
|
86
|
+
reference caught. Their decision overrides the auto-return.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
def on_escalate(v, ctx):
|
|
90
|
+
return ask_a_human(v.reason, v.advice) # Slack, a queue, a webhook — you wire it
|
|
91
|
+
# returning a decision injects it as the return
|
|
92
|
+
Harness().run(step, escalate_after=3, on_escalate=on_escalate)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Provenance.** Every check is written to a hash-chained ledger — tamper-evident and
|
|
96
|
+
verifiable offline, by anyone, no key. Editing a past verdict to hide a drift breaks
|
|
97
|
+
the chain at that link.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
hz.export_audit("run.json")
|
|
101
|
+
from laserbrain import verify_audit
|
|
102
|
+
verify_audit(json.load(open("run.json"))) # (True, -1) intact · (False, i) broken at link i
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Team continuity.** Snapshot a running team and resume it in a later session — the
|
|
106
|
+
shared goal (the fixed reference) and the dialogue carry over, so the group re-grounds
|
|
107
|
+
instead of starting cold.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
snap = team.snapshot() # JSON-safe; persist it anywhere
|
|
111
|
+
team = Team.restore(snap) # keeps watching the same ground
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Framework adapters
|
|
115
|
+
|
|
116
|
+
Already on LangGraph, CrewAI, AutoGen, or the OpenAI Agents SDK? Attach laserbrain
|
|
117
|
+
without changing your loop. Because it checks a fixed reference, it needs the agent
|
|
118
|
+
to *spell* its state — so each adapter takes an `extract` that maps your framework's
|
|
119
|
+
state to `(goal, progress, distance)`. **No adapter imports a framework**: each
|
|
120
|
+
returns a plain callable you hand to the framework's own hook, so install only the
|
|
121
|
+
one you use.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from laserbrain.adapters import guard, langgraph_node, crewai_step_callback, middleware
|
|
125
|
+
|
|
126
|
+
# generic — wrap any step that returns dict(goal=, progress=, distance=)
|
|
127
|
+
@guard
|
|
128
|
+
def step(state): ...
|
|
129
|
+
|
|
130
|
+
# LangGraph — a node that writes the Verdict into graph state; branch on it
|
|
131
|
+
g.add_node("laserbrain", langgraph_node(extract=lambda s: (s["goal"], "advancing", s["dist"])))
|
|
132
|
+
g.add_edge("agent", "laserbrain")
|
|
133
|
+
g.add_conditional_edges("laserbrain",
|
|
134
|
+
lambda s: "return" if s["laserbrain"].drifting else "agent") # .advice steers the return
|
|
135
|
+
|
|
136
|
+
# CrewAI — a step_callback that fires each agent step
|
|
137
|
+
Agent(..., step_callback=crewai_step_callback(lambda o: (o.goal, o.status, o.dist)))
|
|
138
|
+
|
|
139
|
+
# anything else (AutoGen, OpenAI Agents, a custom loop) — one check per step
|
|
140
|
+
lb = middleware(extract=my_extract)
|
|
141
|
+
v = lb(step_output)
|
|
142
|
+
if v.drifting: reinject(v.advice)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Each adapter runs the check locally and free; pass `key=`/`run_id=` (or your own
|
|
146
|
+
`Harness`) to also retain history.
|
|
147
|
+
|
|
148
|
+
## What's proven, and what isn't
|
|
149
|
+
|
|
150
|
+
The **single-agent** detector mirrors the frozen, published instrument
|
|
151
|
+
(`drift.ts @ 6b483de7`) and rests on a theorem: detection is sound and complete, and
|
|
152
|
+
no self-monitoring agent can be. The **multi-agent** dialogue and recursion teams are
|
|
153
|
+
a prototype extension — useful, not (yet) a theorem. Whether *returning* an agent
|
|
154
|
+
keeps the answer as good is [an honest open question](https://phronesis.world/laserbrain/research);
|
|
155
|
+
this SDK gives you the detection and the return mechanism, and says plainly what each is.
|
|
156
|
+
|
|
157
|
+
MIT · [phronesis.world/laserbrain](https://phronesis.world/laserbrain)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
laserbrain
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "laserbrain"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Attach the smart recursion harness to any agent loop — a provably-correct, external check for when an AI agent (or team) has drifted from its goal."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "phronesis" }]
|
|
13
|
+
keywords = ["ai", "agents", "oversight", "drift", "mcp", "llm", "multi-agent"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://phronesis.world/laserbrain"
|
|
18
|
+
Research = "https://phronesis.world/laserbrain/research"
|
|
19
|
+
Demo = "https://phronesis.world/laserbrain/demo"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["laserbrain*"]
|