Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/gds-games/ogs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Open Games — Typed DSL for Compositional Game Theory."""

__version__ = "0.2.0"
__version__ = "0.3.0"

from ogs.dsl.base import OpenGame
from ogs.dsl.compile import compile_to_ir
from ogs.dsl.composition import FeedbackFlow
from ogs.dsl.spec_bridge import compile_pattern_to_spec
from ogs.dsl.games import AtomicGame, DecisionGame
from ogs.dsl.pattern import Pattern
Expand All @@ -20,6 +21,7 @@
PatternIR,
)
from ogs.ir.serialization import IRDocument, load_ir, save_ir
from ogs.registry import discover_patterns
from ogs.reports.generator import generate_reports
from ogs.verification.engine import verify
from ogs.verification.findings import Finding, Severity, VerificationReport
Expand All @@ -30,6 +32,7 @@
"DecisionGame",
"AtomicGame",
"Pattern",
"FeedbackFlow",
# Compilation
"compile_to_ir",
"compile_pattern_to_spec",
Expand All @@ -44,6 +47,8 @@
"save_ir",
"load_ir",
"IRDocument",
# Registry
"discover_patterns",
# IR Models
"PatternIR",
"OpenGameIR",
Expand Down
14 changes: 13 additions & 1 deletion packages/gds-games/ogs/dsl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@
from ogs.dsl.library import (
history as history,
)
from ogs.dsl.library import (
multi_agent_composition as multi_agent_composition,
)
from ogs.dsl.library import (
outcome as outcome,
)
from ogs.dsl.library import (
parallel as parallel,
)
from ogs.dsl.library import (
policy as policy,
)
Expand All @@ -41,6 +47,7 @@
from ogs.dsl.base import OpenGame
from ogs.dsl.composition import (
CorecursiveLoop,
FeedbackFlow,
FeedbackLoop,
Flow,
ParallelComposition,
Expand Down Expand Up @@ -84,7 +91,9 @@ def __getattr__(name: str):
_library_names = {
"context_builder",
"history",
"multi_agent_composition",
"outcome",
"parallel",
"policy",
"reactive_decision",
"reactive_decision_agent",
Expand Down Expand Up @@ -118,6 +127,7 @@ def __getattr__(name: str):
"CounitGame",
# Composition
"Flow",
"FeedbackFlow",
"SequentialComposition",
"ParallelComposition",
"FeedbackLoop",
Expand All @@ -133,8 +143,10 @@ def __getattr__(name: str):
# Library (lazy)
"context_builder",
"history",
"policy",
"multi_agent_composition",
"outcome",
"parallel",
"policy",
"reactive_decision",
"reactive_decision_agent",
# Errors
Expand Down
80 changes: 79 additions & 1 deletion packages/gds-games/ogs/dsl/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

from gds.blocks.composition import FeedbackLoop as _GDSFeedbackLoop
from gds.blocks.composition import ParallelComposition as _GDSParallelComposition
Expand All @@ -91,6 +91,11 @@ class Flow(BaseModel, frozen=True):
Uses game-theory naming (``source_game``/``target_game``). Provides
``source_block``/``target_block`` properties for GDS interop (GDS
composition validators access these attributes).

``source_game`` and ``target_game`` accept either a ``str`` (game name)
or an ``OpenGame`` instance. When an ``OpenGame`` is provided it is
coerced to ``game.name`` immediately at construction time, so the IR
and verifier always receive plain strings.
"""

source_game: str
Expand All @@ -99,6 +104,18 @@ class Flow(BaseModel, frozen=True):
target_port: str
direction: FlowDirection = FlowDirection.COVARIANT
Comment on lines 101 to 105
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flow now claims to accept OpenGame objects for source_game/target_game, but the field types are still str. This makes passing OpenGame values a static type error (and the new library.py code does so). Consider adding a TYPE_CHECKING __init__ overload (or adjusting annotations) so type checkers accept str | OpenGame while the stored attribute remains str.

Copilot uses AI. Check for mistakes.

@model_validator(mode="before")
@classmethod
def _resolve_game_refs(cls, data: Any) -> Any:
"""Coerce OpenGame instances to their name strings."""
if not isinstance(data, dict):
return data
for field in ("source_game", "target_game"):
val = data.get(field)
if isinstance(val, OpenGame):
data[field] = val.name
return data

@property
def source_block(self) -> str:
"""GDS-compatible alias for ``source_game``."""
Expand All @@ -110,6 +127,31 @@ def target_block(self) -> str:
return self.target_game


class FeedbackFlow(Flow):
"""A ``Flow`` with ``direction`` defaulting to ``CONTRAVARIANT``.

Use this inside ``FeedbackLoop.feedback_wiring`` to avoid repeating
``direction=FlowDirection.CONTRAVARIANT`` on every flow::

FeedbackLoop(
name="...",
inner=chain,
feedback_wiring=[
FeedbackFlow(source_game="Outcome", source_port="Outcome",
target_game="Reactive Decision", target_port="Outcome"),
FeedbackFlow(source_game="Reactive Decision", source_port="Experience",
target_game="Policy", target_port="Experience"),
],
)

Equivalent to ``Flow(..., direction=FlowDirection.CONTRAVARIANT)`` for
each entry, but without the repetition. Also accepts ``OpenGame`` objects
for ``source_game``/``target_game`` (inherited from ``Flow``).
"""

direction: FlowDirection = FlowDirection.CONTRAVARIANT


class SequentialComposition(StackComposition, OpenGame):
"""``g1 >> g2`` — sequential composition where output of g1 feeds input of g2.

Expand Down Expand Up @@ -218,6 +260,42 @@ class ParallelComposition(_GDSParallelComposition, OpenGame):
def flatten(self) -> list[AtomicGame]: # type: ignore[override]
return self.left.flatten() + self.right.flatten()

@classmethod
def from_list(
cls,
games: list[OpenGame],
name: str | None = None,
) -> ParallelComposition:
"""Compose a list of games in parallel.

Equivalent to ``games[0] | games[1] | ... | games[N-1]`` but
accepts a dynamic list, enabling N-agent patterns without
manually enumerating the ``|`` chain.

Args:
games: At least 2 ``OpenGame`` instances.
name: Optional name override for the resulting composition.
Defaults to ``" | ".join(g.name for g in games)``.

Raises:
ValueError: If fewer than 2 games are provided.

Example::

agents = [reactive_decision_agent(f"Agent {i}") for i in range(1, 4)]
agents_parallel = ParallelComposition.from_list(agents)
"""
if len(games) < 2:
raise ValueError(
f"ParallelComposition.from_list() requires at least 2 games, got {len(games)}"
)
result: ParallelComposition = games[0] | games[1] # type: ignore[assignment]
for g in games[2:]:
result = result | g # type: ignore[assignment]
if name is not None:
result = result.model_copy(update={"name": name})
return result


class FeedbackLoop(_GDSFeedbackLoop, OpenGame):
"""Wraps a game with contravariant S->R feedback within a single timestep.
Expand Down
Loading