Skip to content

Commit b0c7da1

Browse files
committed
Merge remote-tracking branch 'origin/main' into tmi/issue-2515-m59
2 parents 8e71db2 + 7fdcd6b commit b0c7da1

19 files changed

Lines changed: 1041 additions & 42 deletions

docs/plans/email-full-autonomy.mdx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ icon: "wand-magic-sparkles"
88

99
> **Status:** Phases 1–5 shipped — trust engine, autonomous cycle, closed
1010
> learning loop, REST control surface, automatic correction capture, the
11-
> proactive eval harness, AND the scheduled driver (`AutonomyScheduler` +
12-
> `run_autonomy_job`, opt-in via `GAIA_EMAIL_AUTONOMY_ENABLED`), all tested.
13-
> Remaining follow-ups: registering `run_autonomy_job` on the core `DaemonClock`
14-
> for the daemon-supervised path (cross-process, tracked with #2156), and the npm
15-
> thin-client CLI (`gaia email autonomy …`) wrapping the REST surface.
11+
> proactive eval harness, the scheduled driver (`AutonomyScheduler` +
12+
> `run_autonomy_job`, opt-in via `GAIA_EMAIL_AUTONOMY_ENABLED`), AND the
13+
> `gaia email autonomy {status|set-level|pause|resume|run|trust|kill}`
14+
> thin-client CLI (#2516) wrapping the REST surface over the daemon relay,
15+
> all tested. Remaining follow-up: registering `run_autonomy_job` on the core
16+
> `DaemonClock` for the daemon-supervised path (cross-process, tracked with
17+
> #2156).
1618
>
1719
> **Milestone:** [Autonomy + Messaging Expansion [OSS]](https://github.com/amd/gaia/milestone/6)
1820
>

docs/reference/cli.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,28 @@ gaia email -v -q "Triage my inbox" # verbose logs for benchmarking
10721072

10731073
[→ Full Email Triage Agent Documentation](/guides/email)
10741074

1075+
#### `gaia email autonomy` — autonomy control
1076+
1077+
A thin client over the sidecar's `/v1/email/agent/autonomy*` REST routes, relayed
1078+
through the daemon like every other `gaia email` command (same auth path — no
1079+
sidecar bearer ever reaches the CLI).
1080+
1081+
```bash
1082+
gaia email autonomy status # current level + trust summary
1083+
gaia email autonomy trust # earned-trust ledger
1084+
gaia email autonomy set-level earn_trust # set the level explicitly
1085+
gaia email autonomy pause # stop autonomous activity
1086+
gaia email autonomy resume --level earn_trust # resume (default: earn_trust)
1087+
gaia email autonomy kill # kill switch — level -> off
1088+
gaia email autonomy run --max-messages 25 # trigger one cycle now
1089+
```
1090+
1091+
All subcommands share one persistent `--session-id` (default `cli`) — autonomy
1092+
state lives on that session's in-memory agent, not on disk, so a level you set
1093+
stays set across invocations that use the same session. `run` refuses with an
1094+
actionable error while the level is `off` (#2528) instead of silently reporting
1095+
that nothing happened.
1096+
10751097
---
10761098

10771099
### SD Command

hub/agents/email/npm/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ behind any entry — API shapes, endpoints, and version semantics — see
66

77
## Unreleased
88

9+
- **Triggering an autonomy cycle while autonomy is switched off now tells you
10+
so, instead of quietly reporting nothing happened.** `POST
11+
/v1/email/agent/autonomy/run` used to return the same "nothing to do"
12+
response whether autonomy was disabled or had genuinely run and found
13+
nothing — there was no way to tell which. It now returns an error naming
14+
the current level and how to turn autonomy back on.
915
- **Asking the agent to draft a reply or forward now actually drafts one,
1016
instead of asking you to write it.** The agent would correctly find the
1117
right email, then ask you to supply the reply or forward text — the exact

hub/agents/email/npm/SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,11 @@ a negative outcome that pulls the sender/category back below the trust bar. (Pos
319319
accrual — trust *rising* as suggestions are accepted or left standing — is not yet wired, so
320320
today the ledger only ratchets trust down.) Every auto-action is reversible with undo. A bad
321321
`level` returns **400**; an unknown session returns **404**; undoing an unknown/expired
322-
`action_id` returns **409**.
322+
`action_id` returns **409**; `/run` while the level is `off` returns **409** too — it refuses
323+
rather than returning the same 200 shape a real, found-nothing cycle would (#2528).
324+
325+
The Python host also ships a thin-client CLI over this same surface:
326+
`gaia email autonomy {status|set-level|pause|resume|run|trust|kill}` (#2516).
323327

324328
## Running in a server / long-lived app
325329

hub/agents/email/npm/SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ emits the canonical event vocabulary.
246246
| `GET /v1/email/agent/memory/{id}` | Memory status without changing it. |
247247
| `GET /v1/email/agent/autonomy/{id}` | Inspectable autonomy status: `{ level, enabled, trust_min_samples, trust_threshold, trusted_scope_count, scopes[] }` — the earned-trust ledger, never a black box. |
248248
| `POST /v1/email/agent/autonomy` | Set the autonomy level, `{ session_id, level }` where level ∈ `off` \| `suggest` \| `earn_trust` \| `full` (`off` = kill switch). Bad level → **400**. |
249-
| `POST /v1/email/agent/autonomy/run` | Trigger one observe→decide→act cycle, `{ session_id, max_messages? }``{ level, executed[], proposals[], decisions[], skipped }`. `decisions[]` (#2529) is a per-message log — `{ message_id, tool, action, outcome, reason, sender }` for every candidate considered, whatever the outcome — so a held-back decision (importance guard, confirm floor) is explained, not silent. The daemon clock / scheduler drives this in production. |
249+
| `POST /v1/email/agent/autonomy/run` | Trigger one observe→decide→act cycle, `{ session_id, max_messages? }``{ level, executed[], proposals[], decisions[], skipped }`. `decisions[]` (#2529) is a per-message log — `{ message_id, tool, action, outcome, reason, sender }` for every candidate considered, whatever the outcome — so a held-back decision (importance guard, confirm floor) is explained, not silent. The daemon clock / scheduler drives this in production. Refused with **409** while the session's level is `off` — the kill switch is never mistakable for "ran and found nothing to do" (#2528). |
250250
| `POST /v1/email/agent/autonomy/undo` | Reverse one auto-executed action and record the correction against its trust scope, `{ session_id, action_id }``{ action_id, action_type, message_id, undone, correction_captured }` (#2529). `action_id` comes from a prior `executed[]` entry. Unknown/expired/already-undone id → **409**; an action_type with no reversal implemented → **400**. `correction_captured` is `false` (mutation still reversed) when `action_id` wasn't an autonomy-executed action. |
251251

252252
Sessions are in-process and single-tenant (the sidecar hosts one user's agent); one turn

hub/agents/email/python/CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ contract version is tracked separately as
99

1010
### Added
1111

12+
- **`gaia email autonomy` CLI (#2516).** A thin client over the session-scoped
13+
`/v1/email/agent/autonomy*` REST surface, relayed through the daemon like
14+
every other `gaia email` command (no second auth scheme): `status`,
15+
`set-level`, `pause`, `resume`, `run`, `trust`, `kill`. Closes the gap where
16+
the code and the plan doc both described this command before it existed.
17+
18+
### Fixed
19+
20+
- **`POST /autonomy/run` refuses instead of silently no-oping while autonomy
21+
is `off` (#2528).** Previously the route returned HTTP 200 with the same
22+
empty-report shape whether autonomy was disabled or had genuinely run and
23+
found nothing to do — a caller could not tell the two apart. It now returns
24+
**409**, naming the current level and how to change it.
1225
- **The autonomy trust model can now be exercised end to end — broader candidates, an undo
1326
surface, and per-message decisions (#2529).** The proactive `earn_trust`/`full` loop's
1427
candidate generator (`_autonomy_candidate`) only ever proposed `archive`, so the rest of
@@ -81,6 +94,17 @@ contract version is tracked separately as
8194
`recoverable` flag through to the wire; a recoverable error folds to a
8295
non-terminal status line instead of a terminal `error`, so the retry can
8396
reach completion and the user still sees the failure as it happens.
97+
- **A failed memory startup is now visible in chat, and blames the right
98+
cause (#2519).** When the embedding model wasn't reachable, memory quietly
99+
disabled itself: a log line and a REST field said so, but the agent's
100+
answers made it look like a missing feature ("I don't have a tool to view
101+
saved preferences") rather than a broken one. The agent now prints a
102+
startup warning naming the real problem and the fix. It also used to blame
103+
every failure on `GAIA_MEMORY_DISABLED=1` or a stopped Lemonade — the
104+
common real case is neither: Lemonade is running fine but the embedding
105+
model was never pulled. The message now tells those two apart and gives
106+
the matching remedy (pull the model vs. start Lemonade), since acting on
107+
the wrong one wastes the user's time.
84108
- **`draft_reply` / `draft_forward` actually draft instead of asking for the
85109
text to draft (#2524).** Asked to draft a reply or forward, the agent
86110
correctly located the source message and then asked the user to supply the

hub/agents/email/python/gaia_agent_email/agent.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ class never passes ``use_claude=True`` / ``use_chatgpt=True`` to
8383

8484
from gaia.agents.base.agent import Agent
8585
from gaia.agents.base.console import AgentConsole
86-
from gaia.agents.base.memory import MemoryMixin
86+
from gaia.agents.base.memory import (
87+
MEMORY_UNAVAILABLE_MODEL_NOT_PULLED,
88+
MEMORY_UNAVAILABLE_SERVICE_UNREACHABLE,
89+
MemoryMixin,
90+
)
8791
from gaia.agents.base.tools import _TOOL_REGISTRY
8892
from gaia.agents.registry import get_embedding_model_for_device
8993
from gaia.connectors.errors import AuthRequiredError, ConnectorsError
@@ -684,6 +688,19 @@ def __init__(self, config: Optional[EmailAgentConfig] = None):
684688
),
685689
)
686690

691+
# Surface the degraded-memory state where the user actually is
692+
# (#2519): before this, a failed embedding connectivity probe only
693+
# logged a WARNING and set a REST field nobody was looking at, so a
694+
# user in chat saw the agent quietly claim it never had memory tools
695+
# rather than being told memory failed to come up and how to fix it.
696+
# Skip the deliberate GAIA_MEMORY_DISABLED=1 opt-out here — that's an
697+
# explicit choice (used by tests/CI), not a silent degradation.
698+
if getattr(self, "_memory_unavailable_reason", None) in (
699+
MEMORY_UNAVAILABLE_MODEL_NOT_PULLED,
700+
MEMORY_UNAVAILABLE_SERVICE_UNREACHABLE,
701+
):
702+
self.console.print_warning(self.memory_unavailable_message())
703+
687704
# Exact ctx pin (#1892): set the instance-scoped override on the
688705
# concrete LemonadeClient this agent chats through. Post-super(),
689706
# the client lives at self.chat.llm_client._backend (AgentSDK →
@@ -760,19 +777,19 @@ def memory_status(self) -> dict:
760777
"""Report the current memory state without changing it.
761778
762779
Returns ``{"enabled", "available", "message"}`` where ``available`` is
763-
whether a memory store exists this session (False when disabled at startup
764-
via ``GAIA_MEMORY_DISABLED`` or when Lemonade was unreachable) and
765-
``enabled`` is the effective on/off state (``available`` and not incognito).
780+
whether a memory store exists this session and ``enabled`` is the
781+
effective on/off state (``available`` and not incognito). When
782+
unavailable, ``message`` names the REAL cause — env opt-out, the
783+
embedding model never having been pulled into a running Lemonade, or
784+
Lemonade itself being unreachable — via
785+
``MemoryMixin.memory_unavailable_message()`` (#2519). These are
786+
distinct failures with distinct remedies; conflating them (as this
787+
method used to) sends the user down the wrong fix.
766788
"""
767789
available = getattr(self, "_memory_store", None) is not None
768790
enabled = self.is_memory_enabled()
769791
if not available:
770-
message = (
771-
"Memory is unavailable this session: it was disabled at startup "
772-
"(GAIA_MEMORY_DISABLED=1) or the Lemonade embedding service was "
773-
"unreachable when the agent started. Start lemonade-server and "
774-
"restart the agent to enable it."
775-
)
792+
message = self.memory_unavailable_message()
776793
elif enabled:
777794
message = "Memory is enabled: personalization and persistence are active."
778795
else:
@@ -1306,9 +1323,8 @@ def _autonomy_policy(self) -> "trust.TrustPolicy":
13061323
"""Build the earn-trust policy from current config + the confirm-floor.
13071324
13081325
Rebuilt per cycle so a runtime ``autonomy_level`` change (e.g. via the
1309-
the forthcoming ``gaia email autonomy`` CLI) takes effect on the next
1310-
heartbeat without
1311-
reconstructing the agent.
1326+
``gaia email autonomy set-level`` CLI) takes effect on the next
1327+
heartbeat without reconstructing the agent.
13121328
"""
13131329
ledger = trust.TrustLedger(
13141330
min_samples=self.config.autonomy_trust_min_samples,
@@ -1640,9 +1656,10 @@ def autonomy_status(self) -> Dict[str, Any]:
16401656
Returns the current level, the trust thresholds, and the earned-trust
16411657
ledger — every ``(action, scope)`` with its positive/negative tally and
16421658
whether it has crossed the bar. This is the single read-model the
1643-
planned ``gaia email autonomy status`` / ``trust`` CLI, the REST surface, and
1644-
the Agent-UI panel all render, so autonomy behavior is always
1645-
explainable ("archives news@x because 12/12 correct").
1659+
``gaia email autonomy status`` / ``trust`` CLI and the REST surface
1660+
both render (a future Agent-UI panel is not yet built), so autonomy
1661+
behavior is always explainable ("archives news@x because 12/12
1662+
correct").
16461663
"""
16471664
ledger = trust.TrustLedger(
16481665
min_samples=self.config.autonomy_trust_min_samples,
@@ -1678,8 +1695,8 @@ def run_autonomy_cycle(
16781695
) -> Dict[str, Any]:
16791696
"""Driver-facing entry: run a cycle and persist proposals to GoalStore.
16801697
1681-
This is the seam a ``DaemonClock`` job / the planned ``gaia email autonomy`` CLI
1682-
invokes (mirroring ``run_briefing_job`` for the briefing feature).
1698+
This is the seam a ``DaemonClock`` job / the ``gaia email autonomy run``
1699+
CLI invokes (mirroring ``run_briefing_job`` for the briefing feature).
16831700
Returns a JSON-serializable report — the ``Proposal`` objects are
16841701
replaced by their persisted dict form.
16851702
"""

hub/agents/email/python/gaia_agent_email/agent_routes.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from pydantic import BaseModel, ConfigDict, Field
5353

5454
from gaia.logger import get_logger
55+
from gaia_agent_email import trust
5556

5657
logger = get_logger(__name__)
5758

@@ -402,11 +403,25 @@ async def set_autonomy(request: AutonomyLevelRequest) -> Dict[str, Any]:
402403
raise HTTPException(status_code=400, detail=str(exc)) from exc
403404

404405

405-
@router.post("/autonomy/run")
406+
@router.post(
407+
"/autonomy/run",
408+
responses={
409+
409: {
410+
"description": (
411+
"Autonomy is off for this session — the kill switch refuses "
412+
"the run instead of silently doing nothing (#2528)."
413+
)
414+
}
415+
},
416+
)
406417
async def run_autonomy(request: AutonomyRunRequest) -> Dict[str, Any]:
407418
"""Trigger one observe->decide->act cycle now (the daemon/CLI driver seam).
408419
409420
Runs on a worker thread — the cycle does mailbox I/O and local inference.
421+
Refuses with HTTP 409 while the session's autonomy level is ``off`` (#2528)
422+
— without this, "autonomy is disabled" and "autonomy ran and found
423+
nothing to do" return the identical 200 shape, and a caller can't tell
424+
them apart.
410425
"""
411426
session = registry.get(request.session_id)
412427
if session is None:
@@ -416,6 +431,18 @@ async def run_autonomy(request: AutonomyRunRequest) -> Dict[str, Any]:
416431
raise HTTPException(
417432
status_code=501, detail="This agent build does not expose autonomy."
418433
)
434+
status_fn = getattr(session.agent, "autonomy_status", None)
435+
level = status_fn().get("level") if callable(status_fn) else None
436+
if level == trust.LEVEL_OFF:
437+
raise HTTPException(
438+
status_code=409,
439+
detail=(
440+
f"Autonomy is off for session '{request.session_id}' — the run "
441+
"was refused, not silently skipped. POST /v1/email/agent/autonomy "
442+
f'{{"session_id": "{request.session_id}", '
443+
'"level": "suggest|earn_trust|full"} to enable it first.'
444+
),
445+
)
419446
return await asyncio.to_thread(runner, {"max_messages": request.max_messages})
420447

421448

hub/agents/email/python/gaia_agent_email/spec_html.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,42 @@ def render_endpoint_spec_html() -> str:
867867
<p class="desc">Report the session agent's memory state without changing it:
868868
<code>{ enabled, available, message }</code>.</p>
869869
</div>
870+
871+
<div class="endpoint-block">
872+
<span class="method-badge">GET</span>
873+
<span class="path">/v1/email/agent/autonomy/{session_id}</span>
874+
<p class="desc">Inspectable snapshot of the autonomy engine — level, trust
875+
thresholds, and the earned-trust ledger:
876+
<code>{ level, enabled, trust_min_samples, trust_threshold,
877+
trusted_scope_count, scopes:[{ action_type, scope, positive, negative,
878+
total, score, trusted }] }</code>. This is the read-model
879+
<code>gaia email autonomy status</code> / <code>trust</code> render, so
880+
autonomy behavior is always explainable. 404 if the session is unknown.</p>
881+
</div>
882+
883+
<div class="endpoint-block">
884+
<span class="method-badge">POST</span>
885+
<span class="path">/v1/email/agent/autonomy</span>
886+
<p class="desc">Set the autonomy level at runtime — pause/resume/kill.
887+
Body: <code>{ "session_id": str, "level": "off"|"suggest"|"earn_trust"|"full" }</code>.
888+
<code>off</code> is the kill switch. Returns
889+
<code>{ level, enabled }</code>. <b>400</b> on an unknown level; 404 if
890+
the session is unknown.</p>
891+
</div>
892+
893+
<div class="endpoint-block">
894+
<span class="method-badge">POST</span>
895+
<span class="path">/v1/email/agent/autonomy/run</span>
896+
<p class="desc">Trigger one observe-&gt;decide-&gt;act autonomy cycle now —
897+
the <code>gaia email autonomy run</code> / daemon-scheduler driver seam.
898+
Body: <code>{ "session_id": str, "max_messages"?: int (1-200, default 25) }</code>.
899+
Returns <code>{ level, executed:[...], proposals:[...], skipped,
900+
already_proposed }</code>. <b>409</b> while the session's level is
901+
<code>off</code> — the kill switch refuses the run instead of returning
902+
the same 200 shape a real, found-nothing cycle would (#2528), so a caller
903+
can always tell "disabled" apart from "ran and found nothing to do". 404
904+
if the session is unknown.</p>
905+
</div>
870906
"""
871907

872908
body = f"""<!DOCTYPE html>

hub/agents/email/python/gaia_agent_email/trust.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ class TrustDecision:
9999
"""The policy's verdict for one candidate action.
100100
101101
``action`` is the disposition; ``reason`` is a human-readable rationale
102-
surfaced in the activity feed and the planned ``gaia email autonomy`` output;
102+
surfaced in the activity feed and the ``gaia email autonomy`` CLI output;
103103
``confidence`` is the ledger trust score in ``[0, 1]`` (1.0 for the
104104
hard-coded floor and for explicit preferences).
105105
"""

0 commit comments

Comments
 (0)