gini-toolkit 6.0.1.dev0__py3-none-any.whl

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.
Files changed (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,106 @@
1
+ """Tiny, single-aspect verifiers — the deterministic truth-agents (GINI_MISSIONS_AGENT_ARCHITECTURE
2
+ .md §3). Each checks ONE aspect and returns Verdicts; none of them reason. They wrap the evaluators we
3
+ already have (`domain/objectives`, `domain/legality`) so P1 is mostly a reframing, not new logic.
4
+
5
+ State-dependency keys (coarse for now — the exact scheme is an open decision): "topology" for
6
+ structural checks over the graph, "runtime" for behavioral probes. The blackboard re-runs a verifier
7
+ only when a changed key intersects its `deps()`.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from ..domain import legality as _legality
14
+ from ..domain import objectives as _obj
15
+ from .contracts import Fact, Verdict
16
+
17
+ TOPOLOGY, RUNTIME = "topology", "runtime"
18
+
19
+
20
+ @dataclass
21
+ class WorldView:
22
+ """What a verifier is handed: the live topology, an objectives World over it, the lesson, and an
23
+ optional behavioral runner. Built once per blackboard update and shared across verifiers."""
24
+ topology: object
25
+ world: object
26
+ lesson: object
27
+ runner: object = None
28
+
29
+ @classmethod
30
+ def of(cls, topology, lesson, *, runner=None):
31
+ return cls(topology=topology, world=_obj.TopologyWorld(topology), lesson=lesson, runner=runner)
32
+
33
+
34
+ class ObjectiveVerifier:
35
+ """One tiny verifier per objective — met/unmet (behavioral → pending counts as not-yet-met)."""
36
+
37
+ def __init__(self, objective) -> None:
38
+ self.objective = objective
39
+ self.id = f"objective:{objective.id}"
40
+
41
+ def deps(self) -> tuple[str, ...]:
42
+ return (RUNTIME,) if self.objective.is_behavioral() else (TOPOLOGY,)
43
+
44
+ def check(self, view: WorldView) -> list[Verdict]:
45
+ r = _obj.evaluate(self.objective, view.world, view.runner)
46
+ return [Verdict(self.id, subject=self.objective.id, value=r.met,
47
+ evidence=Fact("status", r.status), deps=self.deps())]
48
+
49
+
50
+ class OffTaskVerifier:
51
+ """Off-task element aspect: are any placed elements outside the mission's relevant family?"""
52
+ id = "legality:off_task"
53
+
54
+ def __init__(self, lesson) -> None:
55
+ self.lesson = lesson
56
+
57
+ def deps(self) -> tuple[str, ...]:
58
+ return (TOPOLOGY,)
59
+
60
+ def check(self, view: WorldView) -> list[Verdict]:
61
+ devices = _legality.off_task_devices(self.lesson, view.topology)
62
+ return [Verdict(self.id, subject="off_task", value=(not devices),
63
+ evidence=Fact("devices", list(devices)), deps=(TOPOLOGY,))]
64
+
65
+
66
+ class IllegalLinkVerifier:
67
+ """Connection-grammar aspect: are any drawn links disallowed?"""
68
+ id = "legality:illegal_links"
69
+
70
+ def deps(self) -> tuple[str, ...]:
71
+ return (TOPOLOGY,)
72
+
73
+ def check(self, view: WorldView) -> list[Verdict]:
74
+ bad = _legality.illegal_links(view.topology)
75
+ return [Verdict(self.id, subject="illegal_links", value=(not bad),
76
+ evidence=Fact("links", list(bad)), deps=(TOPOLOGY,))]
77
+
78
+
79
+ class ForbidVerifier:
80
+ """One tiny verifier per lesson forbid-rule — the rule must stay FALSE (value True = still safe)."""
81
+
82
+ def __init__(self, index: int, forbid) -> None:
83
+ self.index = index
84
+ self.forbid = forbid
85
+ self.id = f"forbid:{index}"
86
+
87
+ def deps(self) -> tuple[str, ...]:
88
+ return (TOPOLOGY,)
89
+
90
+ def check(self, view: WorldView) -> list[Verdict]:
91
+ try:
92
+ tripped = bool(self.forbid.check) and _obj.evaluate_check(self.forbid.check, view.world)
93
+ except _obj.PredicateError:
94
+ tripped = False
95
+ return [Verdict(self.id, subject=f"forbid:{self.index}", value=(not tripped),
96
+ evidence=Fact("say", self.forbid.say), deps=(TOPOLOGY,))]
97
+
98
+
99
+ def for_lesson(lesson) -> list:
100
+ """The full set of tiny verifiers a lesson needs: one per objective + the legality aspects + one
101
+ per forbid rule. This is 'many tiny', assembled per mission."""
102
+ vs: list = [ObjectiveVerifier(o) for o in lesson.objectives]
103
+ vs.append(OffTaskVerifier(lesson))
104
+ vs.append(IllegalLinkVerifier())
105
+ vs.extend(ForbidVerifier(i, f) for i, f in enumerate(getattr(lesson, "forbid", []) or []))
106
+ return vs
gini/agent/wizard.py ADDED
@@ -0,0 +1,178 @@
1
+ """Wizard brain — LLM-driven, goal-directed building.
2
+
3
+ The Wizard is "X-ray with a goal", and the goal-relevance is decided by the model, per
4
+ step, in the context of the canvas so far:
5
+
6
+ 1. **Pick a starter** — given the goal + the element catalog (with descriptions), the
7
+ model names the single best first element. We place it.
8
+ 2. **Walk the build** — around the current element the grammar proposes the *valid*
9
+ neighbours; the model filters them to the ones that serve the goal (one batched
10
+ call) and gives a one-line reason each. The student taps one; it's added + connected;
11
+ repeat from there.
12
+
13
+ The grammar guarantees validity (the model only ever sees valid candidates); the model
14
+ adds goal-relevance. This module is the *pure* part — building prompts and parsing
15
+ replies — so it's testable without a live model. The threading/UI lives in the assistant.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from ..domain.devices import REGISTRY, by_category
20
+
21
+
22
+ def element_catalog() -> str:
23
+ """A compact 'Label: one-line description' list of the placeable elements."""
24
+ lines = []
25
+ for items in by_category().values():
26
+ for d in items:
27
+ desc = d.description.split(". ")[0].rstrip(".")
28
+ lines.append(f"- {d.label}: {desc}.")
29
+ return "\n".join(lines)
30
+
31
+
32
+ def _label_key_map() -> dict[str, str]:
33
+ m: dict[str, str] = {}
34
+ for key, d in REGISTRY.items():
35
+ if not d.hidden:
36
+ m[d.label.lower()] = key
37
+ m[key.lower()] = key
38
+ return m
39
+
40
+
41
+ # ---------------------------------------------------------------- starter ---- #
42
+ def starter_prompt(goal: str, catalog: str) -> str:
43
+ # NOTE: do NOT put a concrete element in the example — a small model will copy it and
44
+ # answer the same element for every goal. Use a neutral placeholder, and make the model
45
+ # state its final choice on an explicit PICK: line so we read its CONCLUSION, not the
46
+ # first element it happens to mention while reasoning.
47
+ return (f'A student wants to build this and nothing else: "{goal}".\n\n'
48
+ f"Choose the single best FIRST element strictly from this list:\n{catalog}\n\n"
49
+ "Match the goal's DOMAIN:\n"
50
+ "- An IP / LAN / subnet / routing / Ethernet / VLAN goal is a COMPUTER-NETWORKING "
51
+ "goal — start with a Router or Switch. Do NOT start with a VPC, Kubernetes, or "
52
+ "any cloud element just because the goal contains the word 'network'.\n"
53
+ "- A cloud / Kubernetes / serverless / database / web-service goal is a CLOUD "
54
+ "goal — start with the cloud element it's built around.\n"
55
+ "Pick the ONE element that is the foundation for THAT specific goal. "
56
+ "You may reason briefly, but END with ONE final line in EXACTLY this form, "
57
+ "copying a real element name from the list:\n"
58
+ " PICK: <element name> - <short reason it fits this goal>")
59
+
60
+
61
+ def coach_prompt(issues, canvas_summary: str) -> str:
62
+ """Coach mode: the issues are DETECTED deterministically (the advisory lint) — the model
63
+ only coaches. `issues` = [{level, device, message}]. The model must prioritize + explain,
64
+ never invent problems and never claim it's fine when issues were found."""
65
+ lines = "\n".join(f"- {(i.get('device') or 'topology')}: {i.get('message', '')}"
66
+ for i in issues)
67
+ return (f"You are reviewing a student's GINI network/cloud topology and coaching them on "
68
+ f"what to fix.\nCanvas: {canvas_summary or '(empty)'}\n\n"
69
+ f"These problems were detected (ground truth — do NOT invent any others, and do "
70
+ f"NOT say the topology is fine):\n{lines}\n\n"
71
+ "In 2-4 short sentences, tell the student what to fix FIRST and why, in plain "
72
+ "language, referring to devices by name. Prioritize and guide — don't just "
73
+ "restate the list.")
74
+
75
+
76
+ def os_coach_prompt(events, card: str, remaining: int, focus: str = "") -> str:
77
+ """OS Coach: measured, Socratic help grounded in the LIVE xv6 kernel state. `events` are
78
+ detected deterministically by the StateWatcher (ground truth — the model coaches, it does
79
+ not invent). The model gives ONE nudge, never the full answer, so the help is a hint-ladder
80
+ rather than an offload — the 'measured' delivery on top of the state-grounded moat."""
81
+ ev = "\n".join(f"- {e.kind}: {e.detail}" for e in events) \
82
+ or "- (no new events — the run looks steady right now)"
83
+ return (
84
+ "You are Coaching a student through an experiment on a REAL xv6 kernel running inside "
85
+ "GINI. Below is the live kernel state (ground truth) and the teachable moments GINI "
86
+ "detected in it. Coach SOCRATICALLY: ask ONE guiding question, or give ONE targeted "
87
+ "hint, that leads the student to NOTICE and reason about what is happening. Do NOT dump "
88
+ "the full explanation, do NOT write code, and do NOT invent processes or values that "
89
+ "aren't in the state. Refer to processes by pid.\n\n"
90
+ f"Live kernel state:\n{card}\n\nDetected moments:\n{ev}\n\n"
91
+ + (f"{focus}\n\n" if focus else "")
92
+ + f"(The student has {remaining} Coach hint(s) left this session, so make it count.)\n"
93
+ "Reply with 2-3 sentences: one nudge.")
94
+
95
+
96
+ def starter_retry_prompt(goal: str, names: str) -> str:
97
+ """A terse re-ask used when the model didn't return a clean, valid pick."""
98
+ return (f'Goal: "{goal}".\nValid elements: {names}.\n'
99
+ "Reply with ONLY this one line and nothing else:\n"
100
+ "PICK: <one exact element name from the list above>")
101
+
102
+
103
+ def element_names() -> str:
104
+ """Just the comma-separated element labels (for the terse retry prompt)."""
105
+ from .. domain.devices import by_category
106
+ return ", ".join(d.label for items in by_category().values() for d in items)
107
+
108
+
109
+ def _validate(name: str) -> str | None:
110
+ """Strict: a candidate string is accepted ONLY if it is exactly one element label/key
111
+ (after dropping a leading article). No fuzzy matching, no guessing among several."""
112
+ n = (name or "").strip().strip(".\"'`").lower()
113
+ for art in ("a ", "an ", "the "):
114
+ if n.startswith(art):
115
+ n = n[len(art):].strip()
116
+ return _label_key_map().get(n)
117
+
118
+
119
+ def parse_starter(text: str) -> tuple[str | None, str]:
120
+ """Return (type_key, reason) ONLY when the model gave a clean, validated pick:
121
+ 1) a 'PICK: <element> - <reason>' line whose element validates exactly; or
122
+ 2) a line that IS '<element> - <reason>' (its lead segment validates exactly).
123
+ Anything ambiguous/rambling returns (None, '') — the caller re-asks rather than guess."""
124
+ for raw in (text or "").splitlines():
125
+ line = raw.strip()
126
+ if line[:5].lower() == "pick:":
127
+ line = line[5:].strip()
128
+ elif not line:
129
+ continue
130
+ head, _, reason = line.partition(" - ")
131
+ if " — " in head and not _validate(head): # tolerate an em-dash separator
132
+ head, _, reason = line.partition(" — ")
133
+ key = _validate(head)
134
+ if key is not None:
135
+ return key, reason.strip(" -:–—\t").strip()
136
+ return None, ""
137
+
138
+
139
+ # ------------------------------------------------------------- neighbours ---- #
140
+ def filter_prompt(goal: str, current_label: str, candidates, canvas_summary: str) -> str:
141
+ """`candidates` = list[(type_key, label)] — the grammar-valid neighbours of the
142
+ current element. Ask the model which serve the goal."""
143
+ opts = "\n".join(f"- {lbl}" for _k, lbl in candidates)
144
+ canvas = f"\nWhat's built so far: {canvas_summary}\n" if canvas_summary else "\n"
145
+ return (f'The student\'s goal is: "{goal}".\n'
146
+ f"They just placed a {current_label}.{canvas}"
147
+ f"These elements can validly connect to the {current_label}:\n{opts}\n\n"
148
+ "List ONLY the ones that move the design toward the goal, best first, each on "
149
+ "its own line as: <Element name> - <short reason it helps the goal>. "
150
+ "Omit ones that don't serve this goal.")
151
+
152
+
153
+ def parse_filter(text: str, candidates) -> list[tuple[str, str]]:
154
+ """Parse the model's picks back to (type_key, reason), restricted to `candidates`."""
155
+ by_label = {lbl.lower(): key for key, lbl in candidates}
156
+ out: list[tuple[str, str]] = []
157
+ seen: set[str] = set()
158
+ for raw in (text or "").splitlines():
159
+ line = raw.strip(" -*•\t")
160
+ if not line:
161
+ continue
162
+ low = line.lower()
163
+ for lbl in sorted(by_label, key=len, reverse=True): # longest label first
164
+ if low.startswith(lbl) and by_label[lbl] not in seen:
165
+ key = by_label[lbl]
166
+ reason = line[len(lbl):].lstrip(" -:–—").strip()
167
+ out.append((key, reason or "fits the goal"))
168
+ seen.add(key)
169
+ break
170
+ if out:
171
+ return out
172
+ # fallback: scan the whole text for any candidate label mentioned, in candidate order
173
+ low = (text or "").lower()
174
+ for key, lbl in candidates:
175
+ if lbl.lower() in low and key not in seen:
176
+ out.append((key, "fits the goal"))
177
+ seen.add(key)
178
+ return out
gini/agent/xv6_pack.py ADDED
@@ -0,0 +1,74 @@
1
+ """Xv6Pack — the OS DomainPack.
2
+
3
+ Plugs xv6/OS onto the domain-neutral engine (composition, probes, grader, catalog, Teaching Center)
4
+ exactly the way NetworkingPack does for networking (see agent/domains.py: "Cloud and xv6/OS become
5
+ future packs implementing the same protocol"). Nothing here is a new engine — it is adapters:
6
+
7
+ • fragments — the OS assignments, authored as YAML in domain/missions/os/ (loaded like any pack).
8
+ • behavioral oracle — the OS win-conditions are ordinary `measure(...)` probes; the runner that
9
+ computes them from live kernel telemetry is `domain.xv6_runner.Xv6Runner` (built + tested). The
10
+ grader evaluates a behavioral objective via `objectives.evaluate(obj, world, runner=Xv6Runner)`.
11
+ • palette / vocabulary / knowledge — the xv6 element + reused capability roles + OS concept notes.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ from .contracts import Observation
18
+
19
+
20
+ class Xv6Observer:
21
+ """Senses the running xv6 (its Machine state / telemetry) as an Observation for the blackboard.
22
+ Best-effort: whatever `world` the framework hands it (a MachineState or a serialised view)."""
23
+ id = "xv6"
24
+
25
+ def observe(self, world) -> list:
26
+ data = world.to_dict() if hasattr(world, "to_dict") else world
27
+ return [Observation(source="xv6", data=data)]
28
+
29
+
30
+ def _os_missions_dir() -> Path:
31
+ return Path(__file__).resolve().parent.parent / "domain" / "missions" / "os"
32
+
33
+
34
+ class Xv6Pack:
35
+ """The OS domain pack. Same seven-member protocol as NetworkingPack."""
36
+ name = "os"
37
+
38
+ def verifiers(self, lesson) -> list:
39
+ return [] # structural verifiers reused; OS grading is behavioral
40
+
41
+ def observers(self) -> list:
42
+ return [Xv6Observer()]
43
+
44
+ def vocabulary(self):
45
+ from ..domain import capabilities # reuse the capability is-a hierarchy
46
+ return capabilities
47
+
48
+ def predicates(self) -> set:
49
+ from ..domain.objectives import _PREDICATES # structural predicates reused as-is
50
+ return set(_PREDICATES)
51
+
52
+ def fragments(self) -> list:
53
+ """The OS assignments — YAML fragments in domain/missions/os/, loaded like any pack."""
54
+ from ..domain import fragment_yaml as _fy
55
+ loaded, _warn = _fy.load_dir(str(_os_missions_dir()), strict=False)
56
+ return list(loaded.values())
57
+
58
+ def palette(self) -> dict:
59
+ from ..domain.devices import REGISTRY
60
+ return {k: v for k, v in REGISTRY.items()
61
+ if k in ("xv6", "terminal", "storage_volume")}
62
+
63
+ def knowledge(self):
64
+ from . import kb
65
+ return kb
66
+
67
+ # -- the behavioral oracle seam (not in the protocol, but how OS probes get graded) --------- #
68
+ def runner(self, machine_state, window: int = 60):
69
+ """An `Xv6Runner` over the live kernel telemetry — the runner the grader passes to
70
+ `objectives.evaluate(...)` for this domain's behavioral (`measure(...)`) objectives."""
71
+ from ..domain.xv6_runner import Xv6Runner
72
+ shadows = machine_state.shadows() if hasattr(machine_state, "shadows") else {}
73
+ return Xv6Runner(getattr(machine_state, "latest", None),
74
+ getattr(machine_state, "timeline", None), shadows, window)
gini/app/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .context import AppContext, EventBus, Settings
2
+
3
+ __all__ = ["AppContext", "EventBus", "Settings"]