Adds assistants.
This commit is contained in:
@@ -4,13 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
|
||||
from castle_core.manifest import (
|
||||
AgentSpec,
|
||||
DeploymentSpec,
|
||||
ProgramSpec,
|
||||
kind_for,
|
||||
@@ -142,6 +143,9 @@ class CastleConfig:
|
||||
# The one deployment concept (manager-discriminated). service/job/tool/static/
|
||||
# reference are *derived views* over this, filtered by kind_for — see below.
|
||||
deployments: dict[str, DeploymentSpec]
|
||||
# Launchable agent CLIs for the dashboard terminal UX (assistant-agnostic).
|
||||
# Optional; empty means the API falls back to a built-in default set.
|
||||
agents: dict[str, AgentSpec] = field(default_factory=dict)
|
||||
|
||||
def deployments_of(self, name: str) -> list[tuple[str, str]]:
|
||||
"""A program's deployments as (deployment-name, kind) pairs, name-sorted.
|
||||
@@ -364,12 +368,18 @@ def load_config(root: Path | None = None) -> CastleConfig:
|
||||
name: _parse_deployment(name, dep_data) for name, dep_data in raw.items()
|
||||
}
|
||||
|
||||
agents: dict[str, AgentSpec] = {
|
||||
name: AgentSpec.model_validate(spec or {})
|
||||
for name, spec in (data.get("agents") or {}).items()
|
||||
}
|
||||
|
||||
config = CastleConfig(
|
||||
root=root,
|
||||
repo=repo_path,
|
||||
gateway=gateway,
|
||||
programs=programs,
|
||||
deployments=deployments,
|
||||
agents=agents,
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -495,6 +505,11 @@ def save_config(config: CastleConfig) -> None:
|
||||
data: dict = {"gateway": gateway_data}
|
||||
if config.repo:
|
||||
data["repo"] = str(config.repo)
|
||||
if config.agents:
|
||||
data["agents"] = {
|
||||
n: s.model_dump(exclude_none=True, exclude_defaults=True)
|
||||
for n, s in config.agents.items()
|
||||
}
|
||||
|
||||
config_path = config.root / "castle.yaml"
|
||||
with open(config_path, "w") as f:
|
||||
|
||||
@@ -197,6 +197,52 @@ class DefaultsSpec(BaseModel):
|
||||
env: EnvMap = Field(default_factory=dict)
|
||||
|
||||
|
||||
# ---------------------
|
||||
# Agent spec — a launchable agent CLI (assistant-agnostic)
|
||||
# ---------------------
|
||||
|
||||
|
||||
class SessionsSpec(BaseModel):
|
||||
"""Declarative session-history capability for an agent (optional).
|
||||
|
||||
Lets the dashboard show a unified picker of an agent's *own* past sessions
|
||||
without castle knowing anything agent-specific in code: it runs
|
||||
``list_command`` (which must print a JSON array of session objects), reads
|
||||
three named fields off each object, and launches ``command`` + ``resume``
|
||||
(with ``{id}`` substituted) to reopen one. Field names default to opencode's
|
||||
shape and are overridable per agent (dotted paths allowed, e.g.
|
||||
``config_summary.session_id``).
|
||||
"""
|
||||
|
||||
list_command: list[str] # argv → JSON array of session objects
|
||||
resume: list[str] # appended to the agent command; "{id}" is substituted
|
||||
id_field: str = "id"
|
||||
title_field: str = "title"
|
||||
time_field: str = "updated"
|
||||
|
||||
|
||||
class AgentSpec(BaseModel):
|
||||
"""A launchable agent CLI for the dashboard's terminal UX.
|
||||
|
||||
Castle just runs ``command args`` inside a pty at ``cwd``; it never parses
|
||||
the agent's output, so any interactive CLI works. This block only names the
|
||||
launch — live sessions (list/resume/kill) are a runtime concern, not config.
|
||||
"""
|
||||
|
||||
command: str
|
||||
args: list[str] = Field(default_factory=list)
|
||||
description: str | None = None
|
||||
cwd: str | None = None # defaults to the castle repo root when unset
|
||||
env: EnvMap = Field(default_factory=dict)
|
||||
# Extra args that open the agent's own session browser / continue its last
|
||||
# conversation (e.g. ["--resume"] or ["--continue"]). Optional and
|
||||
# agent-specific — castle just passes them through. Empty = no such affordance.
|
||||
resume_args: list[str] = Field(default_factory=list)
|
||||
# Optional: declares how to list + resume the agent's own sessions, so the
|
||||
# dashboard can render a unified session picker (see SessionsSpec).
|
||||
sessions: SessionsSpec | None = None
|
||||
|
||||
|
||||
# ---------------------
|
||||
# Program spec — software identity
|
||||
# ---------------------
|
||||
|
||||
Reference in New Issue
Block a user