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.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/agent/verifiers.py
ADDED
|
@@ -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