Contratos de Servicios e Interfaces en Go
La arquitectura de gz-ia está diseñada bajo los principios de Inversión de Dependencias (DIP) y Arquitectura Limpia (Hexagonal). El núcleo funcional no se acopla a las librerías de interfaz de usuario (cli o tui), a binarios externos ni a drivers específicos, sino que expone contratos de interfaz estrictos definidos en paquetes independientes bajo internal/features/ (internal/features).
1. Mapa de Interfaces del Sistema
2. features/session.Service
El servicio de sesión es el orquestador principal del ciclo de vida de los agentes. Se encuentra implementado en internal/features/session/service.go.
Definición del Contrato
type Service interface {
StartChat(ctx context.Context, req StartChatRequest) error
List(ctx context.Context) ([]SessionRecord, error)
GetRecord(ctx context.Context, id string) (*SessionRecord, error)
GetSession(ctx context.Context, id string) (*SessionRecord, error)
Get(ctx context.Context, id string, opts MergeOptions) (*workspace.MergeResult, error)
Read(ctx context.Context, id string, statOnly bool) (string, error)
Kill(ctx context.Context, id string) error
Resume(ctx context.Context, id string) error
Delete(ctx context.Context, id string) error
Path(ctx context.Context, id string) (string, error)
Diff(ctx context.Context, id string, statOnly bool) (string, error)
Merge(ctx context.Context, id string, opts MergeOptions) (*workspace.MergeResult, error)
Metrics(ctx context.Context, id string) (*metrics.SessionMetrics, error)
LogEvent(ctx context.Context, evt *logger.Event) error
GetEvents(ctx context.Context, id string) ([]logger.Event, error)
WatchEvents(ctx context.Context, id string) (<-chan logger.Event, error)
}Estructuras de Petición y Opciones
type StartChatRequest struct {
ID string
Provider string
WorkingDir string
InitialPrompt string
PermissionLevel PermissionLevel
BinaryPath string
OnLaunch func(id string, isIsolated bool)
}
type MergeOptions struct {
Squash bool
NoCommit bool
}Patrón Functional Options en Inicialización
session.NewService permite la inyección de mocks o implementaciones alternativas en entornos de pruebas y producción:
func NewService(workDir string, opts ...Option) Service
// Opciones de inyección disponibles:
WithStore(s Store) Option
WithRunner(r Runner) Option
WithKiller(k ProcessKiller) Option
WithWorkspace(ws workspace.Provider) Option
WithMetrics(m metrics.Service) Option
WithLogger(l logger.Service) Option3. features/session.Store y Persistencia Atómica
La persistencia del estado de las sesiones se administra mediante internal/features/session/store.go.
Definición del Contrato
type Store interface {
Save(s *SessionRecord) error
Get(id string) (*SessionRecord, error)
List() ([]SessionRecord, error)
Delete(id string) error
}Modelo de Datos SessionRecord
type SessionRecord struct {
ID string `json:"id"`
PID int `json:"pid"`
Provider string `json:"provider"`
Status SessionStatus `json:"status"` // running, completed, failed, killed
PermissionLevel PermissionLevel `json:"permission_level"` // readonly, supervised, autonomous
WorkingDir string `json:"working_dir"`
InitialPrompt string `json:"initial_prompt,omitempty"`
StartedAt time.Time `json:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
ExitCode int `json:"exit_code"`
IsIsolated bool `json:"is_isolated"`
WorktreeDir string `json:"worktree_dir,omitempty"`
BranchName string `json:"branch_name,omitempty"`
}Mecanismo de Escritura Atómica en Disco
Para prevenir estados inconsistentes ante fallos de alimentación, interrupciones SIGKILL o lecturas concurrentes, FileStore implementa un patrón de escritura en archivo temporal y renombrado atómico soportado por el kernel Linux:
4. features/session.Runner & OSRunner (TTY y Señales POSIX)
Ubicado en internal/features/session/session.go y internal/features/session/killer.go.
Contratos de Ejecución y Terminación
type Runner interface {
Run(ctx context.Context, binary string, args []string, dir string, onStart func(pid int)) (exitCode int, err error)
}
type ProcessKiller interface {
Kill(pid int) error
}Conexión Interactiva de TTY y Callback de PID
OSRunner realiza el amarre físico de la terminal del usuario con el proceso agéntico hijo:
func (r *OSRunner) Run(ctx context.Context, binary string, args []string, dir string, onStart func(pid int)) (int, error) {
binPath, err := ResolveBinaryPath(binary)
if err != nil {
return -1, err
}
cmd := exec.CommandContext(ctx, binPath, args...)
cmd.Dir = dir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return -1, err
}
// Notificación inmediata del PID para actualización atómica en el Store
if onStart != nil && cmd.Process != nil {
onStart(cmd.Process.Pid)
}
waitErr := cmd.Wait()
// Captura fiel del código de salida POSIX
exitCode := 0
if waitErr != nil {
var exitError *exec.ExitError
if errors.As(waitErr, &exitError) {
exitCode = exitError.ExitCode()
} else {
exitCode = 1
}
}
return exitCode, waitErr
}OSProcessKiller: Secuencia Gradual SIGTERM $\to$ SIGKILL
func (k *OSProcessKiller) Kill(pid int) error {
proc, _ := os.FindProcess(pid)
// 1. Verificación de Liveness
if err := proc.Signal(syscall.Signal(0)); err != nil {
return fmt.Errorf("el proceso con PID %d ya no se encuentra en ejecución", pid)
}
// 2. Envío de señal de parada ordenada
_ = proc.Signal(syscall.SIGTERM)
time.Sleep(100 * time.Millisecond) // Periodo de gracia
// 3. Forzado definitivo si no ha terminado
if err := proc.Signal(syscall.Signal(0)); err == nil {
_ = proc.Kill() // SIGKILL
}
return nil
}5. features/session.Driver (Multi-Driver Agéntico)
Ubicado en internal/features/session/driver.go.
Contrato de la Interfaz
type Driver interface {
ID() string
DisplayName() string
BinaryName() string
IsAvailable() bool
InstallHint() string
BuildArgs(cfg Config) ([]string, error)
}Matriz de Mapeo de Flags por Proveedor
| Driver | readonly | supervised (default) | autonomous | Reanudación (Resume: true) | Prompt Inicial (InitialPrompt) |
|---|---|---|---|---|---|
agy (Google Antigravity) | --mode plan | (sin flags adicionales) | --dangerously-skip-permissions | --continue | -i <prompt> |
claude (Claude Code) | (modo interactivo estándar) | (modo interactivo estándar) | --dangerously-skip-permissions | --resume | -p <prompt> |
opencode (OpenCode) | (modo interactivo estándar) | (modo interactivo estándar) | --dangerously-skip-permissions | --continue | <prompt> (posicional) |
pi-agent (Pi Agent) | (modo interactivo estándar) | (modo interactivo estándar) | --dangerously-skip-permissions | --resume | -p <prompt> |
Registro Dinámico de Proveedores
ListDrivers() []Driver: Retorna la lista inmutable de drivers soportados.GetDriver(id string) (Driver, error): Resuelve el driver por ID; si el parámetro está vacío, asume"agy".FirstAvailableDriver() Driver: Itera el registro y selecciona el primer agente cuyo ejecutable esté disponible en$PATH.
6. features/workspace.Provider & GitClient
Ubicado en internal/features/workspace/workspace.go.
Definición de Contratos
type GitClient interface {
LookPath() (string, error)
Run(ctx context.Context, dir string, args ...string) (string, error)
}
type Provider interface {
IsGitAvailable(ctx context.Context, dir string) bool
ResolveProjectRoot(ctx context.Context, dir string) string
Prepare(ctx context.Context, sessionID string, baseDir string) (*Workspace, error)
Cleanup(ctx context.Context, ws *Workspace) error
CleanupWorktree(ctx context.Context, baseDir string, worktreeDir string, branchName string) error
DiffWorktree(ctx context.Context, baseDir string, worktreeDir string, branchName string, statOnly bool) (string, error)
MergeWorktree(ctx context.Context, sessionID string, baseDir string, worktreeDir string, branchName string, squash bool, noCommit bool) (*MergeResult, error)
}Flujo de Aprovisionamiento y Resolución de Raíz
ResolveProjectRoot: Ejecutagit rev-parse --show-toplevel. Si se invoca la CLI dentro de un subdirectorio profundo de un monorepo o proyecto, el arnés resuelve automáticamente el directorio raíz del repositorio para alojar las carpetas.harness/worktrees/y.harness/sessions/.Prepare: Comprueba si el directorio está dentro de un repositorio Git (git rev-parse --is-inside-work-tree).- Si Git está disponible: Crea el directorio
.harness/worktrees/<sessionID>y ejecutagit worktree add -b harness/<sessionID> <worktreeDir>. - Si no es un repositorio Git: Retorna
Workspace{IsIsolated: false, TargetDir: baseDir}(fallback transparente sin error).
- Si Git está disponible: Crea el directorio
Cálculo de Diffs con Detección de Untracked Files
Al ejecutar DiffWorktree:
- Resuelve la base de comparación con
git merge-base HEAD harness/<sessionID>. - Si existen archivos nuevos sin seguir en Git (
untracked, marcados con??engit status --porcelain), ejecuta temporalmentegit add -N -- <file>para que formen parte del diff estándar, y los restaura inmediatamente después congit reset -q.
Integración Atómica y Commits Automáticos de Seguridad
Al ejecutar MergeWorktree:
- Safety Commit: Si en el worktree del agente quedaron cambios pendientes sin comitear,
gz-iaejecuta automáticamentegit add -Ay comitea los cambios en la rama del worktree con el mensajeharness(<sessionID>): session changes. Si el entorno carece deuser.nameouser.email, efectúa fallback transparente con banderas de configuración en línea (-c user.name=gz-ia -c user.email=gz-ia@localhost). - Fusión en la Rama Base: Ejecuta el merge según las opciones (
squash,noCommito merge commit regular).
7. features/logger.Service & Store (Observabilidad de Eventos)
Ubicado en internal/features/logger/model.go, store.go (internal/features/logger/store.go) y service.go (internal/features/logger/service.go).
Definición de Contratos
type Store interface {
Append(sessionID string, evt *Event) error
ReadEvents(sessionID string) ([]Event, error)
EventFilePath(sessionID string) string
}
type Service interface {
Emit(ctx context.Context, evt *Event) error
GetEvents(ctx context.Context, sessionID string) ([]Event, error)
Watch(ctx context.Context, sessionID string) (<-chan Event, error)
}Modelo de Evento (Event)
type Stage string
const (
StageRead Stage = "READ"
StagePending Stage = "PENDING"
StageFinish Stage = "FINISH"
)
type Status string
const (
StatusOK Status = "OK"
StatusFailed Status = "FAILED"
)
type Event struct {
ID string `json:"id"`
SessionID string `json:"session_id"`
AgentID string `json:"agent_id,omitempty"`
Role string `json:"role,omitempty"`
Action string `json:"action"`
Stage Stage `json:"stage"`
Status *Status `json:"status"` // Exactly null, "OK" o "FAILED"
Timestamp time.Time `json:"timestamp"`
DurationMs *int64 `json:"duration_ms,omitempty"`
Error string `json:"error,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}Concurrencia Thread-Safe y Transmisión en Tiempo Real (Watch)
- Persistencia Append-Only:
logger.FileStoresincroniza escrituras concurrentes mediantesync.RWMutex, asegurando que múltiples procesos o goroutines puedan escribir sobre.harness/sessions/<sessionID>.events.jsonlsin corromper líneas JSON. - Transmisión Fan-Out:
Watch(ctx, sessionID)despacha los eventos históricos y los nuevos eventos en vivo mediante canales con buffer (chan Event, 100) complementados por un ticker periódico de 100ms que sincroniza eventos agregados desde el disco por procesos externos.
8. features/metrics.Service (Telemetría de Tokens y Subagentes)
Ubicado en internal/features/metrics/model.go, collector.go (internal/features/metrics/collector.go) y service.go (internal/features/metrics/service.go).
Definición de Contratos
type Service interface {
GetMetrics(ctx context.Context, sessionID string, workDir string) (*SessionMetrics, error)
}Heurística de Tokens y Desglose Analítico
El recolector analiza los archivos de traza (transcript.jsonl) generados por Antigravity y clasifica el consumo computacional según la heurística estándar de la industria (~4 caracteres por token):
func EstimateTokens(charCount int) int {
if charCount <= 0 {
return 0
}
return (charCount + 3) / 4
}- Thinking Tokens: Longitud en caracteres del bloque de razonamiento interno (
step.Thinking). - Prompt Tokens: Longitud de los pasos de entrada del usuario (
step.Type == "USER_INPUT"ostep.Source != "MODEL"). - Completion Tokens: Longitud de las respuestas generadas por el modelo (
step.Type == "PLANNER_RESPONSE"), sumado a los nombres y argumentos JSON de cada herramienta invocada (tc.Name+tc.Args).
Parseo Recursivo de Subagentes
Cuando el arnés detecta una herramienta invoke_subagent:
- Deserializa el argumento
Subagents(ParseSubagentArgs). - Extrae el
conversationIdasignado al subagente mediante expresiones regulares sobre los pasos posteriores. - Invoca
populateSubagent(sub)para abrir de forma recursiva el transcript hijo en~/.gemini/antigravity-cli/brain/<subID>/.system_generated/logs/transcript.jsonl, construyendo un árbol completo de métricas.
9. features/updater.Service (Actualización Continua y Reemplazo Atómico)
Ubicado en internal/features/updater/model.go y service.go (internal/features/updater/service.go).
Definición de Contratos
type Service interface {
CheckLatest(ctx context.Context) (*ReleaseInfo, error)
Update(ctx context.Context, targetVer string, installDir string) (*UpdateResult, error)
}Normalización y Comparación SemVer
La función NormalizeVersion elimina prefijos de tag comunes (tag/ o v) para estandarizar cadenas SemVer (v0.0.1 $\to$ 0.0.1), permitiendo que CompareVersions compare segmentos mayores, menores y de parche ([3]int) de manera fiable.
Consulta Dual a Registros (Nexus REST + Forgejo API)
Reemplazo Atómico de Binarios (Solución al ETXTBSY de Linux)
En los kernels de GNU/Linux, escribir directamente sobre un archivo ejecutable que está actualmente en ejecución genera el error ETXTBSY (Text file busy). Para garantizar una actualización 100% libre de errores:
// 1. Crear archivo temporal en el mismo sistema de archivos/directorio
tmpFile, err := os.CreateTemp(installDir, fmt.Sprintf(".%s-tmp-*", baseName))
// 2. Extraer el binario en el archivo temporal
_, err = io.Copy(tmpFile, tarReader)
// 3. Aplicar permisos ejecutables
err = tmpFile.Chmod(0755)
tmpFile.Close()
// 4. Renombrar de forma atómica sobre el destino
err = os.Rename(tmpFile.Name(), filepath.Join(installDir, "gz-ia"))Al utilizar os.Rename, la llamada al sistema subyacente rename(2) sustituye la entrada en la tabla de inodos del directorio de forma atómica; el proceso activo retiene su descriptor de archivo abierto hasta finalizar su ejecución sin conflicto alguno.
