ms-tau-sdk 1.2.12.dev20__tar.gz → 1.2.12.dev21__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.
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/CHANGELOG.md +19 -5
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/PKG-INFO +10 -1
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/README.md +9 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/pyproject.toml +1 -1
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +10 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/chat.py +20 -3
- ms_tau_sdk-1.2.12.dev21/src/ms_tau_sdk/api/local_chat.py +256 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/app.py +6 -1
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/client.py +26 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/local.py +243 -5
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/models.py +31 -0
- ms_tau_sdk-1.2.12.dev21/src/ms_tau_sdk/protocols/chat_history.py +496 -0
- ms_tau_sdk-1.2.12.dev21/src/ms_tau_sdk/runtime/live_turns.py +135 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/manager.py +171 -4
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/settings.py +5 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/.gitignore +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/app.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/config.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/state.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/a2a.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/conversations.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/dependencies.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/health.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/inspection.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/models.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/sessions.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/application.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/auth.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/mcp.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/routes.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/cli.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/errors.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/logging.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/definitions.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/factory.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/loader.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/events.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/extensions.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/failures.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/observability.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/provenance.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/session.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/task_context.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/sessions/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/sessions/storage.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/skills.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/tools/__init__.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
- {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/tools/task_control.py +0 -0
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
4
|
-
|
|
3
|
+
## 1.2.12 — 2026-09-27
|
|
4
|
+
|
|
5
|
+
- Local `/api/chat` sessions now survive a UI reload and a Tau restart. Local mode records each chat
|
|
6
|
+
session and adds `GET /api/local/v1/chat-sessions` to list them and
|
|
7
|
+
`GET /api/local/v1/chat-sessions/{sessionUid}/history` to read one back. The history uses the
|
|
8
|
+
shape of the platform's `GET /api/v1/agent-sessions/{uid}/history/`: user and assistant text,
|
|
9
|
+
reasoning, and tool calls with their arguments, results, and `isError`, projected from the Tau
|
|
10
|
+
transcript by the platform's rules. A turn still running is returned in `inProgressMessage`.
|
|
11
|
+
`GET /api/local/v1/agent` returns the Agent's name and description from the workspace's
|
|
12
|
+
`.agents/agent_card.json`. A local chat turn now runs to its durable end when its client
|
|
13
|
+
disconnects or reloads, and only `POST /api/chat/session/cancel` stops it. A second turn for a
|
|
14
|
+
session that is still running returns 409 `session_busy`. After a Stop, the next turn in a local
|
|
15
|
+
session used to fail with "Runtime cancellation was requested" until the idle runtime was
|
|
16
|
+
evicted; the session's next turn now runs on a fresh runtime. Managed chat behavior is unchanged.
|
|
17
|
+
With `MAINSEQUENCE_TAU_TRUSTED_ORIGINS` set, CORS also exposes `X-Agent-Session-Uid` and
|
|
18
|
+
`x-vercel-ai-ui-message-stream`. See ADR 0018. Fixes #47.
|
|
5
19
|
- Added `MAINSEQUENCE_TAU_PROVIDER_TIMEOUT_SECONDS`, the HTTP timeout for model-provider calls.
|
|
6
20
|
It was fixed at 60 seconds for every provider hydrated from the platform, with no setting
|
|
7
21
|
or environment variable to change it. A self-hosted OpenAI-compatible provider such as an
|
|
@@ -9,15 +23,15 @@
|
|
|
9
23
|
prompt exhausted all three attempts, and Tau reported only an empty transport failure. The
|
|
10
24
|
default stays 60 seconds, and an explicit `timeout_seconds` passed to `ProviderFactory.build`
|
|
11
25
|
still takes precedence.
|
|
26
|
+
|
|
27
|
+
## 1.2.11 — 2026-09-27
|
|
28
|
+
|
|
12
29
|
- Added authenticated-process-scoped discovery and bounded hydration for direct local A2A Message
|
|
13
30
|
conversations. The SDK now persists exact public requester/responder Messages independently from
|
|
14
31
|
Tau execution entries, returns stable context and Message identities across restart, supports
|
|
15
32
|
cursor pagination and exact completed-request replay, and exposes
|
|
16
33
|
`GET /api/local/v1/conversations` plus its bounded Message-history route. Existing internal
|
|
17
34
|
sessions are not heuristically reconstructed. Fixes #45.
|
|
18
|
-
|
|
19
|
-
## 1.2.11 — 2026-09-27
|
|
20
|
-
|
|
21
35
|
- Added a version-locked `tau-ai==0.4.2` compatibility patch for OpenAI-compatible transport
|
|
22
36
|
failures that exhausted retries with an empty terminal message. The SDK now preserves the
|
|
23
37
|
concrete HTTPX error type, transport phase, retry evidence, total provider duration, one failure
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ms-tau-sdk
|
|
3
|
-
Version: 1.2.12.
|
|
3
|
+
Version: 1.2.12.dev21
|
|
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
|
|
@@ -172,6 +172,15 @@ events as distinct sections. Correlated provider failures appear first as one hu
|
|
|
172
172
|
incident with cause, phase, retries, duration, and bounded traceback frames; raw JSON is an
|
|
173
173
|
expandable secondary view.
|
|
174
174
|
|
|
175
|
+
Local `/api/chat` sessions are reload-safe as well. List them with
|
|
176
|
+
`GET /api/local/v1/chat-sessions` and read one back with
|
|
177
|
+
`GET /api/local/v1/chat-sessions/{sessionUid}/history`, which returns the platform's chat history
|
|
178
|
+
shape: user and assistant text, reasoning, and tool calls with their results, plus a turn still
|
|
179
|
+
running in `inProgressMessage`. Continue a session by sending its `sessionUid` to `POST /api/chat`.
|
|
180
|
+
A local chat turn keeps running when its client disconnects; stop it with
|
|
181
|
+
`POST /api/chat/session/cancel`. `GET /api/local/v1/agent` returns the Agent's name and description
|
|
182
|
+
from the workspace's `.agents/agent_card.json`.
|
|
183
|
+
|
|
175
184
|
Direct local A2A Message conversations are also reload-safe without becoming Tasks. List them with
|
|
176
185
|
`GET /api/local/v1/conversations`, then hydrate one with
|
|
177
186
|
`GET /api/local/v1/conversations/{contextId}/messages`; both reads are bounded and scoped to the
|
|
@@ -141,6 +141,15 @@ events as distinct sections. Correlated provider failures appear first as one hu
|
|
|
141
141
|
incident with cause, phase, retries, duration, and bounded traceback frames; raw JSON is an
|
|
142
142
|
expandable secondary view.
|
|
143
143
|
|
|
144
|
+
Local `/api/chat` sessions are reload-safe as well. List them with
|
|
145
|
+
`GET /api/local/v1/chat-sessions` and read one back with
|
|
146
|
+
`GET /api/local/v1/chat-sessions/{sessionUid}/history`, which returns the platform's chat history
|
|
147
|
+
shape: user and assistant text, reasoning, and tool calls with their results, plus a turn still
|
|
148
|
+
running in `inProgressMessage`. Continue a session by sending its `sessionUid` to `POST /api/chat`.
|
|
149
|
+
A local chat turn keeps running when its client disconnects; stop it with
|
|
150
|
+
`POST /api/chat/session/cancel`. `GET /api/local/v1/agent` returns the Agent's name and description
|
|
151
|
+
from the workspace's `.agents/agent_card.json`.
|
|
152
|
+
|
|
144
153
|
Direct local A2A Message conversations are also reload-safe without becoming Tasks. List them with
|
|
145
154
|
`GET /api/local/v1/conversations`, then hydrate one with
|
|
146
155
|
`GET /api/local/v1/conversations/{contextId}/messages`; both reads are bounded and scoped to the
|
|
@@ -81,6 +81,16 @@ a platform AgentSession. Local mode supports:
|
|
|
81
81
|
rescheduling safe unclaimed work and terminalizing stale uncertain work rather than replaying
|
|
82
82
|
possible side effects.
|
|
83
83
|
|
|
84
|
+
For a chat UI over `POST /api/chat`, do not read `runtime.sqlite3` or keep a browser-only
|
|
85
|
+
transcript either. List sessions with `GET /api/local/v1/chat-sessions?limit=<n>&cursor=<opaque>`
|
|
86
|
+
and hydrate one with `GET /api/local/v1/chat-sessions/{sessionUid}/history`. The history has the
|
|
87
|
+
platform's `GET /api/v1/agent-sessions/{uid}/history/` shape, including reasoning, tool calls with
|
|
88
|
+
results, and a running turn in `inProgressMessage`, so one reader serves local and deployed
|
|
89
|
+
Agents. Reuse the listed `sessionUid` in `POST /api/chat`. A local chat turn keeps running when
|
|
90
|
+
the client disconnects or reloads; stop it only with `POST /api/chat/session/cancel`, and queue a
|
|
91
|
+
new message while a session is `working` (`POST /api/chat` returns 409 `session_busy`). Title the
|
|
92
|
+
UI from `GET /api/local/v1/agent`, which reads the workspace's `.agents/agent_card.json`.
|
|
93
|
+
|
|
84
94
|
For the default direct Message flow, do not read `runtime.sqlite3` or keep a browser-only source of
|
|
85
95
|
truth. Discover conversations with `GET /api/local/v1/conversations?limit=<n>&cursor=<opaque>` and
|
|
86
96
|
hydrate the latest bounded Message tail with
|
|
@@ -95,11 +95,24 @@ async def chat(
|
|
|
95
95
|
message_count=len(body.messages) if body.messages else None,
|
|
96
96
|
),
|
|
97
97
|
)
|
|
98
|
+
# A local turn is recorded as a chat session and outlives this request, so a
|
|
99
|
+
# reload or a closed tab leaves it running; only session/cancel stops it. A
|
|
100
|
+
# managed turn stays bound to the request that streams it.
|
|
101
|
+
local_turn = (
|
|
102
|
+
await manager.start_local_chat_turn(session_uid, prompt, provenance=provenance)
|
|
103
|
+
if manager.settings.local_mode
|
|
104
|
+
else None
|
|
105
|
+
)
|
|
106
|
+
events = (
|
|
107
|
+
local_turn.events()
|
|
108
|
+
if local_turn is not None
|
|
109
|
+
else manager.prompt(session_uid, prompt, provenance=provenance)
|
|
110
|
+
)
|
|
98
111
|
|
|
99
112
|
async def stream() -> AsyncIterator[bytes]:
|
|
100
113
|
encoder = AssistantUiEncoder()
|
|
101
114
|
try:
|
|
102
|
-
async for event in
|
|
115
|
+
async for event in events:
|
|
103
116
|
for payload in encoder.encode(event):
|
|
104
117
|
yield encoder.sse(payload)
|
|
105
118
|
try:
|
|
@@ -107,9 +120,10 @@ async def chat(
|
|
|
107
120
|
yield encoder.sse(payload)
|
|
108
121
|
yield encoder.done()
|
|
109
122
|
finally:
|
|
110
|
-
|
|
123
|
+
if local_turn is None:
|
|
124
|
+
manager.mark_response_delivered(session_uid)
|
|
111
125
|
except asyncio.CancelledError:
|
|
112
|
-
if not encoder.finished:
|
|
126
|
+
if local_turn is None and not encoder.finished:
|
|
113
127
|
await manager.cancel(session_uid)
|
|
114
128
|
raise
|
|
115
129
|
except Exception as error:
|
|
@@ -144,6 +158,9 @@ async def chat(
|
|
|
144
158
|
}
|
|
145
159
|
)
|
|
146
160
|
yield encoder.done()
|
|
161
|
+
finally:
|
|
162
|
+
if local_turn is not None:
|
|
163
|
+
local_turn.detach()
|
|
147
164
|
|
|
148
165
|
return StreamingResponse(
|
|
149
166
|
stream(),
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Reload-safe local `/api/chat` sessions and the local Agent's identity (ADR 0018)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated, Any
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
14
|
+
|
|
15
|
+
from ms_tau_sdk.backend.client import MainSequenceClient
|
|
16
|
+
from ms_tau_sdk.backend.models import LocalChatSessionSummary
|
|
17
|
+
from ms_tau_sdk.protocols.chat_history import (
|
|
18
|
+
HISTORY_VERSION,
|
|
19
|
+
HistoryMessage,
|
|
20
|
+
current_branch,
|
|
21
|
+
iso_timestamp,
|
|
22
|
+
latest_message_preview,
|
|
23
|
+
project_history,
|
|
24
|
+
)
|
|
25
|
+
from ms_tau_sdk.runtime.manager import SessionRuntimeManager
|
|
26
|
+
from ms_tau_sdk.settings import TauSDKSettings
|
|
27
|
+
|
|
28
|
+
from .dependencies import backend, runtime_manager, settings
|
|
29
|
+
|
|
30
|
+
router = APIRouter(prefix="/api/local/v1")
|
|
31
|
+
BackendDep = Annotated[MainSequenceClient, Depends(backend)]
|
|
32
|
+
RuntimeManagerDep = Annotated[SessionRuntimeManager, Depends(runtime_manager)]
|
|
33
|
+
SettingsDep = Annotated[TauSDKSettings, Depends(settings)]
|
|
34
|
+
logger = structlog.get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
AGENT_CARD_PATH = Path(".agents") / "agent_card.json"
|
|
37
|
+
LOCAL_AGENT_NAME = "Local Main Sequence TAU Agent"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _require_local(config: TauSDKSettings) -> None:
|
|
41
|
+
if not config.local_mode:
|
|
42
|
+
raise HTTPException(
|
|
43
|
+
status_code=409,
|
|
44
|
+
detail="Local chat sessions require TAU_LOCAL_MODE=true",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class AgentIdentity:
|
|
50
|
+
name: str | None = None
|
|
51
|
+
description: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_agent_identity(workspace: Path) -> AgentIdentity:
|
|
55
|
+
"""Read the name and description of the workspace's `.agents/agent_card.json`.
|
|
56
|
+
|
|
57
|
+
The platform takes a deployed Agent's name and description from the same file.
|
|
58
|
+
A missing or unreadable card leaves both unset rather than inventing them.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
path = workspace / AGENT_CARD_PATH
|
|
62
|
+
try:
|
|
63
|
+
card = json.loads(path.read_text(encoding="utf-8"))
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
return AgentIdentity()
|
|
66
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
67
|
+
logger.warning(
|
|
68
|
+
"local_agent.card_unreadable",
|
|
69
|
+
message="The workspace Agent Card could not be read",
|
|
70
|
+
path=str(AGENT_CARD_PATH),
|
|
71
|
+
error_type=type(error).__name__,
|
|
72
|
+
)
|
|
73
|
+
return AgentIdentity()
|
|
74
|
+
if not isinstance(card, dict):
|
|
75
|
+
return AgentIdentity()
|
|
76
|
+
|
|
77
|
+
def text(key: str) -> str | None:
|
|
78
|
+
value = card.get(key)
|
|
79
|
+
return value.strip() if isinstance(value, str) and value.strip() else None
|
|
80
|
+
|
|
81
|
+
return AgentIdentity(name=text("name"), description=text("description"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def workspace_agent_identity(workspace: Path) -> AgentIdentity:
|
|
85
|
+
return await asyncio.to_thread(_read_agent_identity, workspace)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True, slots=True)
|
|
89
|
+
class _SessionProjection:
|
|
90
|
+
summary: LocalChatSessionSummary
|
|
91
|
+
messages: list[HistoryMessage]
|
|
92
|
+
in_progress: HistoryMessage | None
|
|
93
|
+
status: str
|
|
94
|
+
error: str | None
|
|
95
|
+
updated_at: datetime
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _project_session(
|
|
99
|
+
client: MainSequenceClient,
|
|
100
|
+
manager: SessionRuntimeManager,
|
|
101
|
+
config: TauSDKSettings,
|
|
102
|
+
session_uid: str,
|
|
103
|
+
) -> _SessionProjection:
|
|
104
|
+
"""Project a session's durable branch plus the turn this process is running.
|
|
105
|
+
|
|
106
|
+
The running turn is read before the durable entries. If that turn committed in
|
|
107
|
+
between, its durable entries already hold everything the live view had.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
live_turn = manager.live_turn(session_uid)
|
|
111
|
+
live_entries = live_turn.entries() if live_turn is not None else None
|
|
112
|
+
transcript = await client.get_local_chat_transcript(
|
|
113
|
+
session_uid,
|
|
114
|
+
turn_uid=live_turn.turn_uid if live_turn is not None else None,
|
|
115
|
+
)
|
|
116
|
+
if transcript.turn_committed:
|
|
117
|
+
live_entries = None
|
|
118
|
+
durable = [
|
|
119
|
+
item.entry
|
|
120
|
+
for item in transcript.entries
|
|
121
|
+
if live_entries is None or live_turn is None or item.turn_uid != live_turn.turn_uid
|
|
122
|
+
]
|
|
123
|
+
branch = current_branch(durable)
|
|
124
|
+
projected = project_history(
|
|
125
|
+
[*branch, *(live_entries or [])],
|
|
126
|
+
target_agent_uid=config.local_agent_uid,
|
|
127
|
+
)
|
|
128
|
+
messages = projected.messages
|
|
129
|
+
in_progress: HistoryMessage | None = None
|
|
130
|
+
if (
|
|
131
|
+
live_entries is not None
|
|
132
|
+
and messages
|
|
133
|
+
and messages[-1]["role"] == "assistant"
|
|
134
|
+
and projected.source_indexes[-1] >= len(branch)
|
|
135
|
+
):
|
|
136
|
+
# The running turn's newest assistant message, streaming or awaiting a tool.
|
|
137
|
+
in_progress = {**messages.pop(), "completedAt": None}
|
|
138
|
+
running = (
|
|
139
|
+
live_entries is not None
|
|
140
|
+
or transcript.session.working
|
|
141
|
+
or manager.local_session_running(session_uid)
|
|
142
|
+
)
|
|
143
|
+
status = "running" if running else ("error" if projected.last_turn_error else "completed")
|
|
144
|
+
updated_at = transcript.session.updated_at
|
|
145
|
+
if projected.last_timestamp is not None:
|
|
146
|
+
updated_at = max(updated_at, projected.last_timestamp)
|
|
147
|
+
return _SessionProjection(
|
|
148
|
+
summary=transcript.session,
|
|
149
|
+
messages=messages,
|
|
150
|
+
in_progress=in_progress,
|
|
151
|
+
status=status,
|
|
152
|
+
error=projected.last_turn_error if status == "error" else None,
|
|
153
|
+
updated_at=updated_at,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _session_summary(
|
|
158
|
+
summary: LocalChatSessionSummary,
|
|
159
|
+
*,
|
|
160
|
+
message_count: int,
|
|
161
|
+
latest_message_preview: str | None,
|
|
162
|
+
updated_at: datetime,
|
|
163
|
+
working: bool,
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
return {
|
|
166
|
+
"sessionUid": summary.session_uid,
|
|
167
|
+
"title": summary.title,
|
|
168
|
+
"messageCount": message_count,
|
|
169
|
+
"latestMessagePreview": latest_message_preview,
|
|
170
|
+
"createdAt": iso_timestamp(summary.created_at),
|
|
171
|
+
"updatedAt": iso_timestamp(updated_at),
|
|
172
|
+
"working": working,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@router.get("/chat-sessions")
|
|
177
|
+
async def list_chat_sessions(
|
|
178
|
+
client: BackendDep,
|
|
179
|
+
manager: RuntimeManagerDep,
|
|
180
|
+
config: SettingsDep,
|
|
181
|
+
limit: int = Query(default=50, ge=1, le=100),
|
|
182
|
+
cursor: str | None = Query(default=None, min_length=1, max_length=1024),
|
|
183
|
+
) -> dict[str, Any]:
|
|
184
|
+
_require_local(config)
|
|
185
|
+
try:
|
|
186
|
+
page = await client.list_local_chat_sessions(limit=limit, cursor=cursor)
|
|
187
|
+
except ValueError as error:
|
|
188
|
+
raise HTTPException(status_code=400, detail=str(error)) from error
|
|
189
|
+
sessions: list[dict[str, Any]] = []
|
|
190
|
+
for summary in page.sessions:
|
|
191
|
+
if manager.live_turn(summary.session_uid) is None:
|
|
192
|
+
sessions.append(
|
|
193
|
+
_session_summary(
|
|
194
|
+
summary,
|
|
195
|
+
message_count=summary.message_count,
|
|
196
|
+
latest_message_preview=summary.latest_message_preview,
|
|
197
|
+
updated_at=summary.updated_at,
|
|
198
|
+
working=(summary.working or manager.local_session_running(summary.session_uid)),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
continue
|
|
202
|
+
projection = await _project_session(client, manager, config, summary.session_uid)
|
|
203
|
+
visible = [
|
|
204
|
+
*projection.messages,
|
|
205
|
+
*([projection.in_progress] if projection.in_progress is not None else []),
|
|
206
|
+
]
|
|
207
|
+
sessions.append(
|
|
208
|
+
_session_summary(
|
|
209
|
+
summary,
|
|
210
|
+
message_count=len(visible),
|
|
211
|
+
latest_message_preview=latest_message_preview(visible),
|
|
212
|
+
updated_at=max(summary.updated_at, projection.updated_at),
|
|
213
|
+
working=projection.status == "running",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
return {"sessions": sessions, "nextCursor": page.next_cursor}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@router.get("/chat-sessions/{session_uid}/history")
|
|
220
|
+
async def chat_session_history(
|
|
221
|
+
session_uid: str,
|
|
222
|
+
client: BackendDep,
|
|
223
|
+
manager: RuntimeManagerDep,
|
|
224
|
+
config: SettingsDep,
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
_require_local(config)
|
|
227
|
+
canonical = config.local_session_uid(session_uid)
|
|
228
|
+
projection = await _project_session(client, manager, config, canonical)
|
|
229
|
+
identity = await workspace_agent_identity(config.workspace)
|
|
230
|
+
return {
|
|
231
|
+
"version": HISTORY_VERSION,
|
|
232
|
+
"session": {
|
|
233
|
+
"sessionId": canonical,
|
|
234
|
+
"threadId": canonical,
|
|
235
|
+
"agentName": identity.name or LOCAL_AGENT_NAME,
|
|
236
|
+
"agentUid": config.local_agent_uid,
|
|
237
|
+
"agentSessionUid": canonical,
|
|
238
|
+
"status": projection.status,
|
|
239
|
+
"startedAt": iso_timestamp(projection.summary.created_at),
|
|
240
|
+
"updatedAt": iso_timestamp(projection.updated_at),
|
|
241
|
+
"error": projection.error,
|
|
242
|
+
},
|
|
243
|
+
"messages": projection.messages,
|
|
244
|
+
"inProgressMessage": projection.in_progress,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@router.get("/agent")
|
|
249
|
+
async def local_agent(config: SettingsDep) -> dict[str, str | None]:
|
|
250
|
+
_require_local(config)
|
|
251
|
+
identity = await workspace_agent_identity(config.workspace)
|
|
252
|
+
return {
|
|
253
|
+
"name": identity.name,
|
|
254
|
+
"displayName": identity.name,
|
|
255
|
+
"description": identity.description,
|
|
256
|
+
}
|
|
@@ -13,7 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
13
13
|
from fastapi.responses import JSONResponse
|
|
14
14
|
|
|
15
15
|
from ms_tau_sdk import __version__
|
|
16
|
-
from ms_tau_sdk.api import a2a, chat, conversations, health, inspection, sessions
|
|
16
|
+
from ms_tau_sdk.api import a2a, chat, conversations, health, inspection, local_chat, sessions
|
|
17
17
|
from ms_tau_sdk.application import ApplicationServices
|
|
18
18
|
from ms_tau_sdk.errors import TauSDKError
|
|
19
19
|
from ms_tau_sdk.logging import RequestContextMiddleware, configure_logging
|
|
@@ -24,6 +24,9 @@ from ms_tau_sdk.settings import TauSDKSettings, get_settings
|
|
|
24
24
|
|
|
25
25
|
logger = structlog.get_logger(__name__)
|
|
26
26
|
LOCAL_UNSUPPORTED_PATH_PREFIXES = ("/internal/a2a",)
|
|
27
|
+
# Response headers a trusted cross-origin chat client must read: the canonical
|
|
28
|
+
# session of a `POST /api/chat` stream and the stream protocol marker.
|
|
29
|
+
CORS_EXPOSED_HEADERS = ("X-Agent-Session-Uid", "x-vercel-ai-ui-message-stream")
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def create_app(
|
|
@@ -113,6 +116,7 @@ def create_app(
|
|
|
113
116
|
allow_credentials=True,
|
|
114
117
|
allow_methods=["*"],
|
|
115
118
|
allow_headers=["*"],
|
|
119
|
+
expose_headers=list(CORS_EXPOSED_HEADERS),
|
|
116
120
|
)
|
|
117
121
|
app.add_middleware(RequestContextMiddleware)
|
|
118
122
|
|
|
@@ -152,6 +156,7 @@ def create_app(
|
|
|
152
156
|
app.include_router(chat.router)
|
|
153
157
|
app.include_router(a2a.router)
|
|
154
158
|
app.include_router(conversations.router)
|
|
159
|
+
app.include_router(local_chat.router)
|
|
155
160
|
app.include_router(sessions.router)
|
|
156
161
|
app.include_router(inspection.router)
|
|
157
162
|
return app
|
|
@@ -33,6 +33,8 @@ from .models import (
|
|
|
33
33
|
AgentTaskEventPage,
|
|
34
34
|
AgentTaskExecutionAttempt,
|
|
35
35
|
AgentTaskSnapshot,
|
|
36
|
+
LocalChatSessionPage,
|
|
37
|
+
LocalChatTranscript,
|
|
36
38
|
LocalConversationMessagePage,
|
|
37
39
|
LocalConversationPage,
|
|
38
40
|
ProviderControl,
|
|
@@ -185,6 +187,30 @@ class MainSequenceClient:
|
|
|
185
187
|
del context_id, limit, before_sequence
|
|
186
188
|
raise LocalModeUnsupportedError("Local conversation history requires TAU local mode")
|
|
187
189
|
|
|
190
|
+
async def begin_local_chat_turn(self, session_uid: str, prompt: str) -> None:
|
|
191
|
+
"""Record that `/api/chat` is starting a turn in a local session."""
|
|
192
|
+
|
|
193
|
+
del session_uid, prompt
|
|
194
|
+
raise LocalModeUnsupportedError("Local chat sessions require TAU local mode")
|
|
195
|
+
|
|
196
|
+
async def list_local_chat_sessions(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
limit: int,
|
|
200
|
+
cursor: str | None,
|
|
201
|
+
) -> LocalChatSessionPage:
|
|
202
|
+
del limit, cursor
|
|
203
|
+
raise LocalModeUnsupportedError("Local chat session discovery requires TAU local mode")
|
|
204
|
+
|
|
205
|
+
async def get_local_chat_transcript(
|
|
206
|
+
self,
|
|
207
|
+
session_uid: str,
|
|
208
|
+
*,
|
|
209
|
+
turn_uid: str | None = None,
|
|
210
|
+
) -> LocalChatTranscript:
|
|
211
|
+
del session_uid, turn_uid
|
|
212
|
+
raise LocalModeUnsupportedError("Local chat session history requires TAU local mode")
|
|
213
|
+
|
|
188
214
|
async def _request(
|
|
189
215
|
self,
|
|
190
216
|
method: str,
|