-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackends_routed_cu_bu.py
More file actions
153 lines (131 loc) · 5.3 KB
/
Copy pathbackends_routed_cu_bu.py
File metadata and controls
153 lines (131 loc) · 5.3 KB
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
"""Run ONE agent loop across a desktop (CU) backend and a browser (BU) backend — the
CU↔BU composition a host like Codex.app does, in Shinken. A RoutedSession holds both
surfaces, routes each ACI action to the right one, and tags every action + observation with
`source` provenance. It quacks like a Sandbox, so shinken.operator.drive runs over it.
Scripted, no installs: the CU surface is the codex-style MCP computer-use backend and the BU
surface is the browser-runtime backend, each over an in-memory faithful peer.
Run: python examples/backends_routed_cu_bu.py
"""
from __future__ import annotations
import base64
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "sdk" / "python" / "src"))
from shinken.backends import RoutedSession, get_backend, route_for_target # noqa: E402
from shinken.backends.mcp_computer import McpComputerBackend # noqa: E402
from shinken.operator import ScriptedAgent, drive # noqa: E402
from shinken.providers.base import SandboxSpec # noqa: E402
def fake_mcp(_spec):
def call(tool, args):
if tool == "get_app_state":
return {
"content": [
{"type": "text", "text": '‹1› window "Notes"\n‹4› button "New"'},
{"type": "image", "data": base64.b64encode(b"\x89PNG").decode()},
]
}
return {"content": [{"type": "text", "text": "ok"}]}
return call
def fake_browser(_spec):
state = {"url": "about:blank"}
def execute_cdp(tab, method, params):
if method == "Page.captureScreenshot":
return {"data": base64.b64encode(b"\x89PNGbu").decode()}
if method == "Accessibility.getFullAXTree":
return {
"nodes": [
{
"nodeId": "1",
"role": {"value": "link"},
"name": {"value": "Docs"},
"childIds": [],
"backendDOMNodeId": 1,
}
]
}
if method == "DOMSnapshot.captureSnapshot":
return {
"documents": [
{
"nodes": {"backendNodeId": [1]},
"layout": {"nodeIndex": [0], "bounds": [[5, 6, 50, 20]]},
}
]
}
if method == "Runtime.evaluate":
return {"result": {"value": state["url"]}}
if method == "Page.navigate":
state["url"] = params.get("url")
return {}
return (
type(
"C",
(),
{
"execute_cdp": staticmethod(execute_cdp),
"read_clipboard_text": lambda self, t: "",
"write_clipboard_text": lambda self, t, x: None,
},
)(),
1,
)
def main() -> int:
# CU surface (desktop, app-scoped) + BU surface (browser)
cu_prov = McpComputerBackend(transport_factory=fake_mcp, app="Notes")
bu_prov = get_backend("browser-runtime", client_factory=fake_browser)
cu_h, bu_h = (
cu_prov.create(SandboxSpec(metadata={"app": "Notes"})),
bu_prov.create(),
)
cu, bu = cu_prov.connect(cu_h), bu_prov.connect(bu_h)
ws = RoutedSession({"cu": cu, "bu": bu}, default="cu")
try:
caps = ws.capabilities
print("routed capabilities (union):", caps.verbs)
print("per-source:", caps.per_source)
print(
"route_for_target('https://docs') ->",
route_for_target("https://docs"),
"| route_for_target('Notes') ->",
route_for_target("Notes"),
)
# one batch spanning both surfaces; each action says which surface (or implies it)
batch = [
{"verb": "observe", "structured": True, "surface": "cu"},
{
"verb": "click",
"target": {"kind": "element_ref", "ref": "e4"},
"surface": "cu",
},
{"verb": "navigate", "url": "https://example.com/docs"}, # implies BU
{"verb": "observe", "structured": True, "surface": "bu"},
{
"verb": "click",
"target": {"kind": "point_px", "x": 12, "y": 34},
"surface": "bu",
},
]
res = ws.act_batch(batch, batch_id="demo")
print("\nrouted batch results (source-tagged):")
for r in res["results"]:
print(f" [{r['source']}] {r['verb']:9} ok={r['ok']}")
print("\nprovenance log (what a trajectory records):")
for e in ws.events:
print(f" #{e['i']} source={e['source']} verb={e['verb']} ok={e['ok']}")
# and the Operator loop drives the routed session unchanged
agent = ScriptedAgent(
[
[{"verb": "navigate", "url": "https://example.com"}],
[{"verb": "observe", "structured": True, "surface": "bu"}],
]
)
out = drive(ws, agent, max_steps=3)
print(f"\ndrive() over the routed session: {out.to_dict()}")
finally:
ws.close()
cu_prov.destroy(cu_h)
bu_prov.destroy(bu_h)
print("\nOK — one operator loop spanned CU + BU with per-action source provenance.")
return 0
if __name__ == "__main__":
raise SystemExit(main())