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
129 changes: 129 additions & 0 deletions reference/python/src/awp/atelier_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""AtelierOS integration helpers.

AtelierOS (https://github.com/veegee82/AtelierOS) wraps a single LLM-CLI
agent (Claude Code, Codex CLI, Gemini CLI, ...) into a multi-channel,
multi-persona service with audit-evident hash-chain logging. When
AtelierOS dispatches a complex task to AWP via ``AWPAgent.run(task,
state)``, it injects two optional auto-fields into the state dict so
AWP-workers can route their LLM calls through the AtelierOS Engine
layer instead of bypassing it:

* ``state["worker_engine_factory"]`` — a callable
``factory(engine_id: str | None = None) -> WorkerEngine | None``
The worker calls the factory to obtain a configured engine instance
(Claude Code, Codex CLI, ...) for its LLM step. Calling without an
arg returns the chat's default engine; passing a specific id
overrides per worker step (Phase-4 multi-engine workflows).

* ``state["meta"]["engine_id"]`` — the resolved default engine id for
audit attribution. AtelierOS uses this to verify the integrity rule
"every audit event with engine_id has an enclosing awp_task_id".

Both fields are **optional**. Workers that do not honour the convention
keep working — they just bypass the AtelierOS Engine layer and run
whatever LLM their internals reach for. This module exists to make the
honoured path easy to write: import the helpers, the boilerplate is
done.

This module has zero hard dependencies on AtelierOS — it works against
any caller that follows the same convention.
"""
from __future__ import annotations

from typing import Any, Callable, Dict, Optional

# The reserved state-key carrying the engine factory. Spec layer-04
# section 2.2 names this as a reserved key (workers MUST NOT write to
# it; the orchestrator owns it).
WORKER_ENGINE_FACTORY_KEY: str = "worker_engine_factory"

# The conventional location for the engine_id label in state.meta.
META_ENGINE_ID_PATH: tuple = ("meta", "engine_id")


def extract_engine_factory(state: Dict[str, Any]) -> Optional[Callable[..., Any]]:
"""Return the worker_engine_factory callable from state, or None.

Safe to call on any state dict — missing key, wrong type, or
non-callable value all return None gracefully. Workers can therefore
write::

factory = extract_engine_factory(state)
if factory is None:
# No AtelierOS context — fall back to internal LLM client.
engine = None
else:
engine = factory("claude_code") # or factory() for default
# ... use engine to make the LLM call ...
"""
if not isinstance(state, dict):
return None
value = state.get(WORKER_ENGINE_FACTORY_KEY)
if value is None or not callable(value):
return None
return value


def extract_engine_id(state: Dict[str, Any],
default: Optional[str] = None) -> Optional[str]:
"""Return state["meta"]["engine_id"] or default.

Useful for workers that want to log/audit which engine they ended up
using without forcing the factory call.
"""
if not isinstance(state, dict):
return default
meta = state.get("meta")
if not isinstance(meta, dict):
return default
eid = meta.get("engine_id")
if isinstance(eid, str) and eid:
return eid
return default


def resolve_engine(state: Dict[str, Any],
engine_id: Optional[str] = None) -> Optional[Any]:
"""Convenience: pull the factory from state and call it once.

Returns the WorkerEngine instance for ``engine_id`` (or the chat's
default if engine_id is None), or None if either:

* the factory is missing from state (caller has no AtelierOS
context), OR
* the factory exists but returns None for the requested engine
(engine binary not installed on this host, etc.).

Workers that want a specific engine for *this step* (e.g. a code-
generation step that prefers ``claude_code`` even when the chat
defaults to ``codex_cli``) should pass an explicit engine_id.
"""
factory = extract_engine_factory(state)
if factory is None:
return None
try:
return factory(engine_id) if engine_id else factory()
except Exception:
# Factory contract is "fail graceful, return None on missing".
# Wrap defensively in case a malformed factory raises.
return None


def has_atelier_context(state: Dict[str, Any]) -> bool:
"""True iff this state was injected by an AtelierOS-style caller.

Useful for branching code that should behave differently when AWP
runs standalone vs. inside AtelierOS — e.g. emitting AtelierOS-
audit-compatible events only when the context is present.
"""
return extract_engine_factory(state) is not None


__all__ = [
"WORKER_ENGINE_FACTORY_KEY",
"META_ENGINE_ID_PATH",
"extract_engine_factory",
"extract_engine_id",
"resolve_engine",
"has_atelier_context",
]
150 changes: 150 additions & 0 deletions reference/python/tests/test_atelier_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""Tests for awp.atelier_integration helpers.

These cover the convention-side guarantees AWP-workers rely on when
running inside AtelierOS:

* extract_engine_factory: returns the callable, None on missing /
wrong type / non-callable
* extract_engine_id: reads state["meta"]["engine_id"], falls back
* resolve_engine: factory + call composed; returns None gracefully
when factory missing OR factory raises
* has_atelier_context: True iff factory is present and callable

Pure-Python, no AWP-runtime dependency, sub-millisecond per case.

Run: python -m unittest reference.python.tests.test_atelier_integration
"""
from __future__ import annotations

import unittest

from awp.atelier_integration import (
WORKER_ENGINE_FACTORY_KEY,
extract_engine_factory,
extract_engine_id,
has_atelier_context,
resolve_engine,
)


class _StubEngine:
"""Stand-in for a WorkerEngine — just enough to be identifiable."""

def __init__(self, engine_id: str):
self.engine_id = engine_id


def _factory(engine_id: str | None = None) -> _StubEngine | None:
"""Stand-in worker_engine_factory."""
if engine_id is None:
engine_id = "claude_code" # the convention's default
if engine_id in ("claude_code", "codex_cli"):
return _StubEngine(engine_id)
return None


def _raising_factory(engine_id: str | None = None):
raise RuntimeError("malformed factory")


class ExtractEngineFactoryTests(unittest.TestCase):
def test_returns_callable_when_present(self):
state = {WORKER_ENGINE_FACTORY_KEY: _factory}
out = extract_engine_factory(state)
self.assertIs(out, _factory)

def test_missing_key_returns_none(self):
self.assertIsNone(extract_engine_factory({}))

def test_non_dict_state_returns_none(self):
self.assertIsNone(extract_engine_factory(None)) # type: ignore[arg-type]
self.assertIsNone(extract_engine_factory("not a dict")) # type: ignore[arg-type]
self.assertIsNone(extract_engine_factory([])) # type: ignore[arg-type]

def test_non_callable_value_returns_none(self):
self.assertIsNone(extract_engine_factory({WORKER_ENGINE_FACTORY_KEY: "string"}))
self.assertIsNone(extract_engine_factory({WORKER_ENGINE_FACTORY_KEY: 42}))
self.assertIsNone(extract_engine_factory({WORKER_ENGINE_FACTORY_KEY: None}))

def test_reserved_key_constant_value(self):
# Spec layer-04 names this exact key. Changing it is a breaking
# convention change.
self.assertEqual(WORKER_ENGINE_FACTORY_KEY, "worker_engine_factory")


class ExtractEngineIdTests(unittest.TestCase):
def test_returns_meta_engine_id(self):
state = {"meta": {"engine_id": "claude_code"}}
self.assertEqual(extract_engine_id(state), "claude_code")

def test_missing_meta_returns_default(self):
self.assertIsNone(extract_engine_id({}))
self.assertEqual(extract_engine_id({}, "fallback"), "fallback")

def test_meta_not_a_dict(self):
self.assertIsNone(extract_engine_id({"meta": "not a dict"}))
self.assertEqual(
extract_engine_id({"meta": [1, 2, 3]}, "fallback"),
"fallback",
)

def test_engine_id_not_a_string(self):
self.assertIsNone(extract_engine_id({"meta": {"engine_id": 42}}))
self.assertIsNone(extract_engine_id({"meta": {"engine_id": ""}}))

def test_non_dict_state(self):
self.assertIsNone(extract_engine_id(None)) # type: ignore[arg-type]
self.assertEqual(
extract_engine_id("not a dict", "fallback"), # type: ignore[arg-type]
"fallback",
)


class ResolveEngineTests(unittest.TestCase):
def test_resolves_default_engine(self):
state = {WORKER_ENGINE_FACTORY_KEY: _factory}
engine = resolve_engine(state)
self.assertIsNotNone(engine)
self.assertEqual(engine.engine_id, "claude_code")

def test_resolves_specific_engine(self):
state = {WORKER_ENGINE_FACTORY_KEY: _factory}
engine = resolve_engine(state, "codex_cli")
self.assertIsNotNone(engine)
self.assertEqual(engine.engine_id, "codex_cli")

def test_unknown_engine_id_returns_none(self):
state = {WORKER_ENGINE_FACTORY_KEY: _factory}
self.assertIsNone(resolve_engine(state, "nonexistent"))

def test_no_factory_returns_none(self):
self.assertIsNone(resolve_engine({}))
self.assertIsNone(resolve_engine({}, "claude_code"))

def test_raising_factory_returns_none_graceful(self):
state = {WORKER_ENGINE_FACTORY_KEY: _raising_factory}
self.assertIsNone(resolve_engine(state))
self.assertIsNone(resolve_engine(state, "claude_code"))

def test_non_dict_state(self):
self.assertIsNone(resolve_engine(None)) # type: ignore[arg-type]


class HasAtelierContextTests(unittest.TestCase):
def test_true_when_factory_present(self):
state = {WORKER_ENGINE_FACTORY_KEY: _factory}
self.assertTrue(has_atelier_context(state))

def test_false_when_missing(self):
self.assertFalse(has_atelier_context({}))

def test_false_when_non_callable(self):
self.assertFalse(has_atelier_context({WORKER_ENGINE_FACTORY_KEY: "x"}))

def test_false_when_state_is_not_a_dict(self):
self.assertFalse(has_atelier_context(None)) # type: ignore[arg-type]
self.assertFalse(has_atelier_context([])) # type: ignore[arg-type]


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions spec/versions/1.0/layers/04-memory-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ The following state keys are reserved by the runtime. Agents MUST NOT write to t
| `_errors` | Error accumulator for the current run. |
| `_trace` | Trace context for distributed tracing. |
| `_workflow` | Workflow-level metadata (name, version). |
| `worker_engine_factory` | OPTIONAL host-injected callable. When present, callers wrapping AWP (e.g. AtelierOS) inject a factory `factory(engine_id: str | None = None) -> WorkerEngine | None` that workers MAY call to obtain an LLM-CLI engine instance for their LLM step. Workers calling the factory route their LLM calls through the host's engine layer; workers ignoring it use whatever LLM client they were built against. The reference helper is `awp.atelier_integration` (`extract_engine_factory`, `resolve_engine`). Spec status: OPTIONAL convention; absence is the legacy single-LLM-source behaviour. |

### 2.4 Persistence

Expand Down
Loading