# AgentTeam Exec · Agent Management

Status: **final implementation contract** for #t2208 / Gitea #3265.

## 1. Goal

Let an Agent manage other Agents owned by the **same Human** through `agentteam exec agent <action>`. Structural changes go through Human approval; runtime-state changes execute directly under ownership checks. Keep it simple — extreme crash recovery is out of scope.

## 2. Actions (7)

| Action | Kind | Semantics |
|---|---|---|
| `start` | direct | Enable auto-wake + start runtime. |
| `stop` | direct | Disable auto-wake + stop runtime. Idempotent (`already_stopped`). |
| `restart` | direct | Restart a running Agent (running-only; stopped → conflict). |
| `create` | approval | Create an Agent definition; async after approval. |
| `update` | approval | Update fields; runtime-param changes may auto-restart on apply. |
| `delete` | approval | Delete 1–10 Agents; partial allowed. |
| `duplicate` | approval | 1–10 copies; strict order, partial allowed. |

Out of scope: Role domain, reset/clear-workspace, raw API keys.

## 3. Exec contracts (Desktop side)

```
agentteam exec agent start   { "requestId", "agentId" }  → { "status": "started" | "already_started" }
agentteam exec agent stop    { "requestId", "agentId" }  → { "status": "stopping" | "already_stopped" }
agentteam exec agent restart { "requestId", "agentId" }  → { "status": "restarted" }
agentteam exec agent create    { "requestId", "name", "runtime", "machineId", ... }  → { "status": "pending", "approvalId" }
agentteam exec agent update    { "requestId", "agentId", <change fields> }           → { "status": "pending", "approvalId" }
agentteam exec agent delete    { "requestId", "agentIds": [1..10] }                  → { "status": "pending", "approvalId" }
agentteam exec agent duplicate { "requestId", "sourceAgentId", "count"?: 1..10, ... } → { "status": "pending", "approvalId" }
```

Each action has an action-specific response DTO with explicit `jsonschema:"enum=..."` tags. Desktop forwards via `AgentRuntimePath("/managed-agents/<action>")`; `list/get` stay local.

## 4. Server routes & owner resolution

Routes mount under `/api/v1/workspaces/:workspaceId/agents/:agentId/runtime/managed-agents/<action>`, authenticated by `RequireAgentRuntimePrincipal`. Owner resolved via `AgentManagedByUser` (same workspace + same Human). The Agent never impersonates a Human session.

## 5. Direct lifecycle path (start / stop / restart)

**Simple receipt by requestId for dedup:**
- `agent_lifecycle_receipts` keyed `UNIQUE(applicant_agent_id, action, request_id)`.
- States: `prepared → dispatching → succeeded | indeterminate`.
- **Reserve** (blind upsert `INSERT...ON DUPLICATE KEY UPDATE`): same requestId + same payload → replay; different payload → conflict.
- **Replay by state:**
  - `succeeded` → return original result.
  - `dispatching` → 409 `request_in_progress` (a concurrent or crashed attempt holds it; **never re-dispatch**).
  - `indeterminate` → 409 `runtime_outcome_unknown`.
  - `prepared` → fall through to dispatch.
- **Dispatch:** execute via `Start/StopAgentRuntimeForUser` or `Runtime.RestartAgent`. Status reflects **pre-call running truth** (start: running→`already_started`; stop: !running→`already_stopped`).
- **Dispatch error** (after MarkDispatching): record `indeterminate` + return the original error. A replay returns `runtime_outcome_unknown`. The user checks actual state and retries with a **new requestId**.
- **No lease, generation, fence, heartbeat, stop-proof, multi-phase, or auto-crash-recovery.**

## 6. Approval path (create / update / delete / duplicate)

Submit → freeze proposal → submit to existing `approvalapp.SubjectRegistry` (like `team_role_proposal`). Returns `{status:"pending", approvalId}`.

On approve: the subject handler creates an `agent_operations` row (status=queued) + items, and the card transitions to `processing`. An async worker picks it up and executes via shared domain services. On reject: card resolved, no operation.

**No stale base-digest, lease/generation/fence, per-effect ledger, effectKey, exactly-once, crash auto-recovery, or independent projection watermark.**

## 7. Agent Operation ledger (minimal)

```
agent_operations:
  id, workspace_id, applicant_agent_id, owner_user_id, action,
  payload (frozen proposal), approval_id, request_id,
  status (queued | running | applied | partial | failed),
  created_at, updated_at

agent_operation_items:
  id, operation_id (FK), item_index,
  input (JSON: per-item request — agentId for delete, source+mode for duplicate, create params for create),
  status (pending | applied | failed),
  result (JSON: per-item output — e.g. created agentId, nullable),
  created_at, updated_at
```

A single workspace-group-scoped worker uses a `queued→running` CAS to take an operation, runs items in strict input order through shared domain services, and aggregates partial failure. One item failing does not cancel the rest. Queued work is always claimed before finalization retries.

A `running` operation with any `pending` item is **not** auto-reclaimed — if the worker process dies, the operator checks actual state and submits a new request. A `running` operation with items but no pending item is finalization-ready: the worker may reclaim it only to retry the idempotent same-card terminal projection, never to execute items again.

## 8. Approval card lifecycle

One card per batch: `pending` (active, awaiting Human) → approve → `processing` (active, worker executing) → terminal (resolved: `applied`/`partial`/`failed`). Reject → resolved. The card uses the existing `ClientNotificationID` in-place update mechanism; no new notification stream.

Every lifecycle state preserves the credential-free presentation snapshot frozen
at submission. Terminal cards must continue to show the original operation and
target Agent display names plus IDs even when a successful delete has removed
those Agent rows. Create and duplicate results may additionally show the safe
created Agent IDs. Card rendering must not re-query deleted Agents or expose raw
operation input, payload, credentials, or internal error text.

Finalization order is observer → idempotent in-place resolve of that card → operation terminal CAS. If card projection fails, the operation stays `running` with no pending items so a later worker pass retries finalization only. Lifecycle receipts, operations, and operation items are retained for idempotency and audit; this feature does not automatically delete them.

## 9. Error semantics

Direct lifecycle error codes (all 409 unless noted): `machine_offline`, `agent_not_running`, `request_in_progress`, `runtime_outcome_unknown`, `idempotency_conflict`. Store errors mapped explicitly (`not_found` 404, `forbidden` 403, `bad_request` 400, etc.).

## 10. Security invariants

Same workspace + same Human owner. Owner Human is sole approver. No raw API key in payload or card. No Human session impersonation.

## 11. Reuse points

Owner resolution: `AgentManagedByUser`. Lifecycle: `Start/StopAgentRuntimeForUser`, `Runtime.RestartAgent`. CRUD: extract `CreateAgent`/`UpdateAgent`/`DeleteAgent`/`DuplicateAgent` from WebAgent handlers (shared by Web + worker). Approval: existing `SubjectRegistry`/`DecideWithEffect`/Approval card. Worker: `PlatformService.ProcessOnce` pattern.
