ms-tau-sdk 1.2.7.dev10__tar.gz → 1.2.7.dev11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/PKG-INFO +1 -1
  2. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/pyproject.toml +1 -1
  3. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/a2a.py +8 -0
  4. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/models.py +7 -0
  5. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/sessions.py +35 -2
  6. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/client.py +8 -0
  7. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/local.py +62 -15
  8. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/routes.py +1 -0
  9. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/manager.py +67 -2
  10. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/session.py +2 -0
  11. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/.gitignore +0 -0
  12. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/CHANGELOG.md +0 -0
  13. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/README.md +0 -0
  14. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/__init__.py +0 -0
  15. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
  16. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  17. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
  18. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  19. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/__init__.py +0 -0
  20. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/chat.py +0 -0
  21. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/dependencies.py +0 -0
  22. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/api/health.py +0 -0
  23. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/app.py +0 -0
  24. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/application.py +0 -0
  25. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/__init__.py +0 -0
  26. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/auth.py +0 -0
  27. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/mcp.py +0 -0
  28. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/backend/models.py +0 -0
  29. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/cli.py +0 -0
  30. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/errors.py +0 -0
  31. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/logging.py +0 -0
  32. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  33. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  34. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  35. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  36. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  37. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/providers/__init__.py +0 -0
  38. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/providers/definitions.py +0 -0
  39. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/providers/factory.py +0 -0
  40. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  41. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/resources/__init__.py +0 -0
  42. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/resources/loader.py +0 -0
  43. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  44. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  45. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  46. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/events.py +0 -0
  47. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  48. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/failures.py +0 -0
  49. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/observability.py +0 -0
  50. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  51. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  52. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  53. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  54. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/sessions/storage.py +0 -0
  55. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/settings.py +0 -0
  56. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/skills.py +0 -0
  57. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/tools/__init__.py +0 -0
  58. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  59. {ms_tau_sdk-1.2.7.dev10 → ms_tau_sdk-1.2.7.dev11}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 1.2.7.dev10
3
+ Version: 1.2.7.dev11
4
4
  Summary: Workspace-bound Tau application primitives for Main Sequence projects
5
5
  Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
6
6
  Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "1.2.7.dev10"
7
+ version = "1.2.7.dev11"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -17,6 +17,7 @@ from typing import Annotated, Any
17
17
  import structlog
18
18
  from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request
19
19
  from fastapi.responses import JSONResponse, StreamingResponse
20
+ from structlog.contextvars import bind_contextvars
20
21
  from tau_agent.types import JSONValue
21
22
 
22
23
  from ms_tau_sdk.backend.client import MainSequenceClient
@@ -1054,6 +1055,13 @@ async def _execute_task(
1054
1055
  dispatch_uid: str | None = None,
1055
1056
  claim: _ClaimedTask | None = None,
1056
1057
  ) -> dict[str, Any]:
1058
+ bind_contextvars(
1059
+ a2a_task_id=task.task_id,
1060
+ task_uid=task.uid,
1061
+ a2a_context_id=task.context_id,
1062
+ session_uid=task.context_id,
1063
+ agent_session_uid=task.context_id,
1064
+ )
1057
1065
  if claim is None:
1058
1066
  claim = await _claim_backend_task(
1059
1067
  client,
@@ -54,3 +54,10 @@ class CancelRequest(ApiModel):
54
54
  )
55
55
  )
56
56
  message: str | None = None
57
+
58
+
59
+ class SessionModelSelection(ApiModel):
60
+ session_uid: str = Field(validation_alias=AliasChoices("sessionUid", "session_uid"))
61
+ provider: str
62
+ model: str
63
+ thinking_level: str | None = Field(default=None, validation_alias="thinkingLevel")
@@ -4,14 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Annotated
6
6
 
7
- from fastapi import APIRouter, Depends, Query
7
+ from fastapi import APIRouter, Depends, HTTPException, Query
8
8
 
9
9
  from ms_tau_sdk.backend.client import MainSequenceClient
10
10
  from ms_tau_sdk.runtime.manager import SessionRuntimeManager
11
11
  from ms_tau_sdk.settings import TauSDKSettings
12
12
 
13
13
  from .dependencies import backend, runtime_manager, settings
14
- from .models import CancelRequest
14
+ from .models import CancelRequest, SessionModelSelection
15
15
 
16
16
  router = APIRouter(prefix="/api/chat")
17
17
  BackendDep = Annotated[MainSequenceClient, Depends(backend)]
@@ -38,6 +38,39 @@ async def session_model(
38
38
  }
39
39
 
40
40
 
41
+ @router.get("/model-providers")
42
+ async def model_providers(client: BackendDep, config: SettingsDep) -> dict[str, object]:
43
+ if not config.local_mode:
44
+ raise HTTPException(status_code=409, detail="Provider catalog is local-mode only")
45
+ return await client.list_model_providers()
46
+
47
+
48
+ @router.put("/session-model")
49
+ async def select_session_model(
50
+ body: SessionModelSelection,
51
+ manager: RuntimeManagerDep,
52
+ config: SettingsDep,
53
+ ) -> dict[str, object]:
54
+ if not config.local_mode:
55
+ raise HTTPException(status_code=409, detail="Model selection is local-mode only")
56
+ session_uid = config.local_session_uid(body.session_uid)
57
+ await manager.change_session_model(
58
+ session_uid,
59
+ provider=body.provider,
60
+ model=body.model,
61
+ thinking_level=body.thinking_level,
62
+ )
63
+ session = await manager.backend.get_session(session_uid)
64
+ return {
65
+ "sessionUid": session_uid,
66
+ "model": {
67
+ "provider": session.active_provider,
68
+ "model": session.active_model,
69
+ "thinkingLevel": session.active_thinking,
70
+ },
71
+ }
72
+
73
+
41
74
  @router.post("/session/cancel")
42
75
  async def cancel_session(
43
76
  body: CancelRequest,
@@ -50,6 +50,7 @@ from .models import (
50
50
  )
51
51
  from .routes import (
52
52
  AGENT_TASKS,
53
+ MODEL_PROVIDERS,
53
54
  agent_session,
54
55
  agent_session_agent_card,
55
56
  agent_session_entries,
@@ -283,6 +284,13 @@ class MainSequenceClient:
283
284
  detail=error.errors(include_input=False),
284
285
  ) from error
285
286
 
287
+ async def list_model_providers(self) -> dict[str, Any]:
288
+ """Read the authenticated user's safe provider catalog from Main Sequence."""
289
+ data = await self._request("GET", MODEL_PROVIDERS, idempotent=True)
290
+ if not isinstance(data, dict) or not isinstance(data.get("providers"), list):
291
+ raise BackendError("Backend model-provider catalog response is invalid")
292
+ return data
293
+
286
294
  async def update_session_config(
287
295
  self,
288
296
  session_uid: str,
@@ -109,6 +109,9 @@ class LocalDevelopmentBackend(MainSequenceClient):
109
109
  async def aclose(self) -> None:
110
110
  await self._services.aclose()
111
111
 
112
+ async def list_model_providers(self) -> dict[str, Any]:
113
+ return await self._services.list_model_providers()
114
+
112
115
  async def _ensure_initialized(self) -> None:
113
116
  if self._initialized:
114
117
  return
@@ -269,8 +272,8 @@ class LocalDevelopmentBackend(MainSequenceClient):
269
272
  raise BackendConflictError("Local provider/model selection is not configured")
270
273
  return provider, model, self.settings.local_thinking
271
274
 
272
- def _runtime_config_sha256(self) -> str:
273
- provider, model, thinking = self._selection()
275
+ def _runtime_config_sha256(self, selection: tuple[str, str, str | None] | None = None) -> str:
276
+ provider, model, thinking = selection or self._selection()
274
277
  payload = json.dumps(
275
278
  {"provider": provider, "model": model, "thinking": thinking},
276
279
  sort_keys=True,
@@ -278,8 +281,10 @@ class LocalDevelopmentBackend(MainSequenceClient):
278
281
  ).encode("utf-8")
279
282
  return "sha256:" + hashlib.sha256(payload).hexdigest()
280
283
 
281
- async def _hydrate_evidence(self, *, holder_id: str) -> ProviderExecutionEvidence:
282
- provider, model, thinking = self._selection()
284
+ async def _hydrate_evidence(
285
+ self, *, holder_id: str, selection: tuple[str, str, str | None] | None = None
286
+ ) -> ProviderExecutionEvidence:
287
+ provider, model, thinking = selection or self._selection()
283
288
  evidence = await self._services.hydrate_local_provider_credential(
284
289
  provider,
285
290
  model=model,
@@ -342,16 +347,24 @@ class LocalDevelopmentBackend(MainSequenceClient):
342
347
  holder_id: str,
343
348
  ) -> ProviderExecutionEvidence:
344
349
  del agent_uid
345
- selected_provider, selected_model, _thinking = self._selection()
350
+ selected_provider, selected_model, selected_thinking = self._selection()
351
+ if session_uid is not None:
352
+ session = await self.get_session(session_uid)
353
+ selected_provider = session.active_provider or ""
354
+ selected_model = session.active_model or ""
355
+ selected_thinking = session.active_thinking
346
356
  if provider != selected_provider or model != selected_model:
347
357
  raise BackendConflictError(
348
- "Local provider refresh does not match the configured selection"
358
+ "Local provider refresh does not match the session selection"
349
359
  )
350
360
  if session_uid is not None:
351
361
  canonical = self.settings.local_session_uid(session_uid)
352
362
  if canonical != session_uid:
353
363
  raise SessionNotFoundError("Local session identifier is invalid")
354
- return await self._hydrate_evidence(holder_id=holder_id)
364
+ return await self._hydrate_evidence(
365
+ holder_id=holder_id,
366
+ selection=(selected_provider, selected_model, selected_thinking),
367
+ )
355
368
 
356
369
  async def get_session(self, session_uid: str) -> AgentSession:
357
370
  def operation() -> AgentSession:
@@ -374,11 +387,35 @@ class LocalDevelopmentBackend(MainSequenceClient):
374
387
  model: str,
375
388
  thinking_level: str | None,
376
389
  ) -> AgentSession:
377
- selected = self._selection()
378
- if (provider, model, thinking_level) != selected:
379
- raise BackendConflictError(
380
- "Local session selection is fixed by TAU_LOCAL_PROVIDER and TAU_LOCAL_MODEL"
381
- )
390
+ if not provider or not model:
391
+ raise BackendConflictError("A provider and model are required")
392
+ selection = (provider, model, thinking_level)
393
+ await self._hydrate_evidence(
394
+ holder_id=f"local-selection-{self.settings.workspace_digest}",
395
+ selection=selection,
396
+ )
397
+
398
+ def operation() -> None:
399
+ with closing(self._connect()) as connection, connection:
400
+ row = connection.execute(
401
+ "SELECT runtime_activity FROM sessions WHERE uid = ?", (session_uid,)
402
+ ).fetchone()
403
+ if row is None:
404
+ raise SessionNotFoundError(f"Local session not found: {session_uid}")
405
+ if row["runtime_activity"] == "working":
406
+ raise BackendConflictError("Cannot change a model while the session is working")
407
+ connection.execute(
408
+ "UPDATE sessions SET provider = ?, model = ?, thinking = ?, "
409
+ "runtime_config_sha256 = ?, updated_at = ? WHERE uid = ?",
410
+ (
411
+ *selection,
412
+ self._runtime_config_sha256(selection),
413
+ _iso(_utcnow()),
414
+ session_uid,
415
+ ),
416
+ )
417
+
418
+ await self._run(operation)
382
419
  return await self.get_session(session_uid)
383
420
 
384
421
  async def get_agent_card(self, session_uid: str) -> AgentCardEnvelope:
@@ -669,13 +706,23 @@ class LocalDevelopmentBackend(MainSequenceClient):
669
706
  session_uid: str,
670
707
  request: TauRuntimeBootstrapRequest,
671
708
  ) -> TauRuntimeBootstrap:
672
- provider, model, thinking = self._selection()
673
- evidence = await self._hydrate_evidence(holder_id=request.holder_id)
709
+ def stored_selection() -> tuple[str, str, str | None]:
710
+ with closing(self._connect()) as connection:
711
+ row = connection.execute(
712
+ "SELECT provider, model, thinking FROM sessions WHERE uid = ?", (session_uid,)
713
+ ).fetchone()
714
+ if row is None:
715
+ return self._selection()
716
+ return str(row["provider"]), str(row["model"]), row["thinking"]
717
+
718
+ selection = await self._run(stored_selection)
719
+ provider, model, thinking = selection
720
+ evidence = await self._hydrate_evidence(holder_id=request.holder_id, selection=selection)
674
721
 
675
722
  def operation() -> TauRuntimeBootstrap:
676
723
  now = _utcnow()
677
724
  expires = now + timedelta(seconds=request.ttl_seconds)
678
- runtime_config_sha256 = self._runtime_config_sha256()
725
+ runtime_config_sha256 = self._runtime_config_sha256(selection)
679
726
  with closing(self._connect()) as connection, connection:
680
727
  connection.execute("BEGIN IMMEDIATE")
681
728
  row = connection.execute(
@@ -9,6 +9,7 @@ API_V1_PREFIX = "/api/v1"
9
9
  RUNTIME_CREDENTIAL_TOKEN = f"{API_V1_PREFIX}/runtime-credentials/token/"
10
10
  AGENT_SESSIONS = f"{API_V1_PREFIX}/agent-sessions/"
11
11
  MODEL_PROVIDER_CREDENTIALS = f"{API_V1_PREFIX}/model-provider-credentials/"
12
+ MODEL_PROVIDERS = f"{API_V1_PREFIX}/model-providers/"
12
13
  AGENT_TASKS = f"{API_V1_PREFIX}/agent-tasks/"
13
14
 
14
15
  type RuntimeLeaseOperation = Literal["acquire", "renew", "release"]
@@ -970,6 +970,9 @@ class SessionRuntimeManager:
970
970
  "principal_type",
971
971
  "coding_agent_service_uid",
972
972
  "agent_session_uid",
973
+ "a2a_task_id",
974
+ "a2a_context_id",
975
+ "task_uid",
973
976
  "agent_uid",
974
977
  "agent_run_uid",
975
978
  "turn_uid",
@@ -1168,9 +1171,71 @@ class SessionRuntimeManager:
1168
1171
  with bound_contextvars(session_uid=session_uid):
1169
1172
  await self._evict_with_context(session_uid)
1170
1173
 
1171
- async def _evict_with_context(self, session_uid: str) -> None:
1174
+ async def change_session_model(
1175
+ self,
1176
+ session_uid: str,
1177
+ *,
1178
+ provider: str,
1179
+ model: str,
1180
+ thinking_level: str | None,
1181
+ ) -> None:
1182
+ """Switch an idle session and reload it with backend-authorized evidence."""
1183
+ if not self.settings.local_mode:
1184
+ raise BackendConflictError("Board model selection requires local mode")
1185
+ async with self._registry_lock:
1186
+ load_lock = self._load_locks.setdefault(session_uid, asyncio.Lock())
1187
+ async with load_lock:
1188
+ runtime = self._runtimes.get(session_uid)
1189
+ if runtime is None:
1190
+ runtime = await self._load(session_uid)
1191
+ async with runtime.lock:
1192
+ if runtime.coding_session.is_running or runtime.runtime_activity == "working":
1193
+ raise BackendConflictError("Cannot change a model while the session is working")
1194
+ if runtime.persistence_task is not None:
1195
+ await asyncio.shield(runtime.persistence_task)
1196
+ previous = await self.backend.get_session(session_uid)
1197
+ if previous.active_provider is None or previous.active_model is None:
1198
+ raise BackendConflictError("Session has no model to restore")
1199
+ runtime.evicting = True
1200
+ try:
1201
+ await self.backend.update_session_config(
1202
+ session_uid,
1203
+ provider=provider,
1204
+ model=model,
1205
+ thinking_level=thinking_level,
1206
+ )
1207
+ except BaseException:
1208
+ runtime.evicting = False
1209
+ raise
1210
+ await self._evict_with_context(session_uid, force=True)
1211
+ try:
1212
+ await self._load(session_uid)
1213
+ except Exception:
1214
+ try:
1215
+ await self.backend.update_session_config(
1216
+ session_uid,
1217
+ provider=previous.active_provider,
1218
+ model=previous.active_model,
1219
+ thinking_level=previous.active_thinking,
1220
+ )
1221
+ await self._load(session_uid)
1222
+ except Exception:
1223
+ logger.exception(
1224
+ "runtime.session.model_change_rollback_failed",
1225
+ session_uid=session_uid,
1226
+ )
1227
+ raise
1228
+ logger.info(
1229
+ "runtime.session.model_changed",
1230
+ session_uid=session_uid,
1231
+ provider=provider,
1232
+ model=model,
1233
+ thinking_level=thinking_level,
1234
+ )
1235
+
1236
+ async def _evict_with_context(self, session_uid: str, *, force: bool = False) -> None:
1172
1237
  runtime = self._runtimes.get(session_uid)
1173
- if runtime is None or runtime.evicting:
1238
+ if runtime is None or (runtime.evicting and not force):
1174
1239
  return
1175
1240
  runtime.evicting = True
1176
1241
  if runtime.lock.locked():
@@ -62,6 +62,8 @@ class ActiveSessionRuntime:
62
62
  if self.persistence_task is not None:
63
63
  await asyncio.shield(self.persistence_task)
64
64
  async with self.lock:
65
+ if self.evicting:
66
+ raise RuntimeError("Session runtime is being reconfigured")
65
67
  self.last_used_at = time.monotonic()
66
68
  settled_event: TauRuntimeEvent | None = None
67
69
  if provenance: