Skip to content

Commit 7be5cd5

Browse files
authored
process monitoring (#869)
* process monitoring * oauth ux improvements --------- Co-authored-by: evalstate <evalstate@users.noreply.github.com>
1 parent 67301a7 commit 7be5cd5

12 files changed

Lines changed: 979 additions & 16 deletions

File tree

src/fast_agent/cli/runtime/model_bootstrap.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,8 @@ def activate_model_picker_provider(action: ProviderActivation) -> bool:
445445
from fast_agent.ui import console
446446

447447
handler = get_oauth_provider(action.provider.config_name)
448-
if handler.status().get("present"):
448+
status = handler.status()
449+
if status.get("present") and not status.get("expired"):
449450
return True
450451

451452
typer.echo(f"Starting {handler.display_name} OAuth login...", err=True)

src/fast_agent/history/process_poll_folding.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def _process_status(exchange: PollExchange) -> str | None:
144144
return status if isinstance(status, str) else None
145145

146146

147+
def _has_resource_observation(exchange: PollExchange) -> bool:
148+
return isinstance(exchange.metadata.get("resource_observation"), str)
149+
150+
147151
def _output_line_count(exchange: PollExchange) -> int | None:
148152
return _non_negative_int(exchange.metadata.get("output_line_count"))
149153

@@ -845,6 +849,8 @@ def fold_managed_process_poll_history(
845849
process_status = _process_status(current)
846850
if process_status not in _FOLDABLE_PROCESS_STATUSES:
847851
return None
852+
if _has_resource_observation(current):
853+
return None
848854

849855
reverse_exchanges = [current]
850856
cursor = len(history) - 2
@@ -854,6 +860,8 @@ def fold_managed_process_poll_history(
854860
exchange = _exchange(request, result, request_index=cursor - 1)
855861
if exchange is None or exchange.process_id != current.process_id:
856862
break
863+
if _has_resource_observation(exchange):
864+
break
857865
reverse_exchanges.append(exchange)
858866
cursor -= 2
859867

src/fast_agent/llm/provider/openai/codex_oauth.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -332,16 +332,16 @@ def _load_codex_tokens_with_source() -> tuple[CodexOAuthTokens | None, str | Non
332332
stored = load_oauth_credential("codex")
333333
return (_tokens_from_credential(stored.credential), stored.source) if stored else (None, None)
334334

335+
# Codex CLI credentials are external and read-only. Prefer them before touching
336+
# the OS keyring so an existing auth.json is sufficient on its own.
337+
tokens = _load_codex_cli_tokens()
338+
if tokens:
339+
return tokens, "auth.json"
340+
335341
stored = load_oauth_credential("codex")
336342
if stored:
337343
return _tokens_from_credential(stored.credential), stored.source
338344

339-
# Codex CLI credentials are an external, read-only fallback. Fast-agent-owned
340-
# credentials take precedence so refreshed tokens can persist without modifying
341-
# the CLI's auth.json.
342-
tokens = _load_codex_cli_tokens()
343-
if tokens:
344-
return tokens, "auth.json"
345345
_warn_about_legacy_codex_keyring_credentials()
346346
return None, None
347347

src/fast_agent/llm/provider/openai/xai_oauth.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ class XaiDeviceCode:
3939
interval: float
4040

4141

42+
class _InvalidGrantError(ProviderKeyError):
43+
pass
44+
45+
4246
def _oauth_error(action: str, response: httpx.Response) -> ProviderKeyError:
4347
try:
4448
payload = response.json()
@@ -54,6 +58,15 @@ def _oauth_error(action: str, response: httpx.Response) -> ProviderKeyError:
5458
)
5559

5660

61+
def _oauth_error_code(response: httpx.Response) -> str | None:
62+
try:
63+
payload = response.json()
64+
except ValueError:
65+
return None
66+
error = payload.get("error") if isinstance(payload, dict) else None
67+
return error if isinstance(error, str) else None
68+
69+
5770
def _required_string(payload: dict[str, Any], field: str) -> str:
5871
value = payload.get(field)
5972
if not isinstance(value, str) or not value:
@@ -190,7 +203,10 @@ def refresh_xai_credential(
190203
},
191204
)
192205
if not response.is_success:
193-
raise _oauth_error("token refresh", response)
206+
error = _oauth_error("token refresh", response)
207+
if _oauth_error_code(response) == "invalid_grant":
208+
raise _InvalidGrantError(error.message, error.details)
209+
raise error
194210
payload = response.json()
195211
if not isinstance(payload, dict):
196212
raise ProviderKeyError("Invalid xAI OAuth response", "Expected a JSON object.")
@@ -243,7 +259,11 @@ def get_xai_access_token(
243259
credential = current.credential
244260
expired = credential.expires_at is not None and time.time() >= credential.expires_at
245261
if force_refresh or expired:
246-
credential = refresh_xai_credential(credential, client=client)
262+
try:
263+
credential = refresh_xai_credential(credential, client=client)
264+
except _InvalidGrantError:
265+
delete_oauth_credential(XAI_PROVIDER_ID)
266+
return None
247267
save_oauth_credential(
248268
XAI_PROVIDER_ID,
249269
credential,

0 commit comments

Comments
 (0)