Перейти к содержанию

Архитектура

У glia одно правило: никакого скрытого потока управления. Хотите понять, что делает агент, — читаете agent.py. Хотите понять, какое состояние он хранит, — читаете trajectory.py. Это вся система.

Составные части

          ┌─────────────────────────────────────────────┐
          │                  Agent                       │
          │  цикл: вызвать модель → запустить инструменты │
          └───────┬───────────────┬──────────────┬───────┘
                  │               │              │
          ┌───────▼──────┐ ┌──────▼──────┐ ┌─────▼───────┐
          │     LLM      │ │ ToolRegistry│ │  Trajectory │
          │  (протокол)  │ │  (@tool fns)│ │ состояние + │
          └───────┬──────┘ └─────────────┘ │   события   │
                  │                         └─────────────┘
        ┌─────────┴─────────┐
   ┌────▼─────┐        ┌─────▼──────┐
   │ ClaudeLLM│        │  EchoLLM   │
   │(Anthropic)        │ (офлайн/CI)│
   └──────────┘        └────────────┘
Модуль Ответственность
types.py Блоки контента, Message, Usage. Неизменяемые, сериализуемые в JSON.
llm.py Граница провайдера: LLMRequest, LLMResponse, протокол LLM.
tools.py Декоратор @tool (схема из аннотаций типов), ToolRegistry.
trajectory.py Состояние запуска и типы Event. Сам «стеклянный ящик».
agent.py Цикл. run() и run_events().
memory.py Инженерия контекста: компакторы.
guardrails.py Валидаторы входа/выхода.
structured.py Структурированный вывод через принудительный вызов инструмента.
checkpoint.py Сохранение/загрузка траектории; hook-чек-пойнтер.
evals.py Механизм «эвалы как тесты».
approval.py Human-in-the-loop подтверждение инструментов.
providers/ EchoLLM (офлайн) и ClaudeLLM (Anthropic).

Цикл, точно

Agent.run_events() — единственный источник поведения. Каждая итерация:

  1. (опционально) компакция — если Compactor считает траекторию слишком большой, сначала сжать её, породив событие Compacted.
  2. собрать LLMRequest из текущей траектории и схем инструментов.
  3. вызвать провайдера (ModelCallModelResponse). Если stream=True и провайдер это поддерживает, сначала переизлучаются дельты текста как события ModelDelta. Сообщение ассистента и его Usage добавляются в траекторию.
  4. если модель запросила инструменты: объявить все вызовы (ToolCalled), при наличии политики прогнать каждый через шлюз подтверждения (ApprovalRequestedApprovalResolved), выполнить одобренные — по умолчанию параллельно (asyncio.gather) — испустить ToolReturned в порядке вызовов, добавить все результаты одним ходом пользователя, повторить.
  5. иначе: прогнать выходные guardrails и завершить (RunFinished).

Почему так

  • Блоки — закрытое объединение, а не иерархия классов. Вы делаете isinstance/match по четырём типам; нет подклассов, которые нужно «открывать», и нет системы плагинов.
  • Результаты инструментов — ход user. Это соглашение Anthropic, и оно держит нашу модель сообщений и формат провода согласованными без «долга» на перевод.
  • События — записи, а не команды. Hooks наблюдают; они никогда не меняют цикл. Это делает поведение читаемым, а hooks — безопасными.
  • Граница провайдера намеренно крошечная. Адаптация к новой модели или изменению API затрагивает один файл.
  • EchoLLM — полноправный гражданин. Детерминированное офлайн-тестирование всего цикла — цель проектирования, а не «довесок».

Как расширять

  • Новый провайдер: реализуйте async def generate(request) -> LLMResponse. ~40 строк. Простейший пример — providers/echo.py.
  • Новая стратегия контекста: реализуйте протокол Compactor (should_compact + compact). См. memory.py.
  • Новый guardrail: функция (text) -> None, которая бросает GuardrailTripped. Это весь интерфейс.
  • Наблюдаемость: добавьте hook. Через него проходит каждое событие — экспортируйте в логи, трассировщик или UI.