630 lines
19 KiB
Markdown
630 lines
19 KiB
Markdown
# Content Access Stateful Runtime Routing Implementation Plan
|
|
|
|
> **Historical:** This plan has been implemented. The `StatefulMcpRuntime` protocol
|
|
> is in `wf_sources_mcp.sdk`, upstream transport prefers it for content reads,
|
|
> and `WfMcpService` wires the configured runtime pool as both `tool_executor`
|
|
> and `stateful_runtime`.
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Route broker content reads (`read_resource`, `render_prompt`) through a stateful MCP runtime when one is configured, while preserving one-shot adapter fallback.
|
|
|
|
**Architecture:** Config-built services already pass `McpRuntimePool` as `tool_executor`; that runtime now supports `call_tool`, `read_resource`, and `get_prompt`. This slice makes the policy explicit with a `StatefulMcpRuntime` protocol instead of ad-hoc `hasattr` or string dispatch. Discovery/catalog refresh remains one-shot adapter based; content access prefers stateful runtime and falls back to the adapter only when no stateful runtime is configured.
|
|
|
|
**Tech Stack:** Python 3.14, MCP Python SDK, pytest/pytest-asyncio, ruff, basedpyright.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Modify `src/wf_sources_mcp/sdk/protocols.py`
|
|
- Add `StatefulMcpRuntime` protocol with `call_tool`, `read_resource`, `get_prompt`.
|
|
- Modify `src/wf_sources_mcp/sdk/__init__.py`
|
|
- Export `StatefulMcpRuntime`.
|
|
- Modify `src/wf_mcp/sdk/base.py`
|
|
- Re-export `StatefulMcpRuntime` through compatibility shim.
|
|
- Modify `src/wf_mcp/sdk/__init__.py`
|
|
- Re-export `StatefulMcpRuntime` through old package.
|
|
- Modify `src/wf_mcp/broker/service/upstream_transport.py`
|
|
- Add `stateful_runtime: StatefulMcpRuntime | None`.
|
|
- Keep `tool_executor` compatibility but derive stateful runtime from it when possible.
|
|
- Prefer stateful runtime in `read_resource` and `render_prompt`.
|
|
- Keep adapter fallback.
|
|
- Modify `src/wf_mcp/broker/service/core.py`
|
|
- Pass the configured runtime pool as both `tool_executor` and `stateful_runtime` where available.
|
|
- Modify `src/wf_mcp/broker/config.py`
|
|
- If construction currently passes only `tool_executor=McpRuntimePool(...)`, update service construction if needed.
|
|
- Add/modify tests:
|
|
- `tests/wf_sources_mcp/test_sdk_protocols.py`
|
|
- `tests/wf_mcp/test_compat_imports.py`
|
|
- `tests/wf_mcp/service/test_upstream_transport.py`
|
|
- `tests/wf_mcp/service/test_content_access.py`
|
|
|
|
## Hard Boundaries
|
|
|
|
- Do not route catalog refresh/discovery listing through stateful runtime in this slice.
|
|
- Do not use `getattr(runtime, "read_resource")` dispatch.
|
|
- Do not remove adapter fallback.
|
|
- Do not add runtime raw method or notification support.
|
|
- Do not assume `list_resources` / `list_prompts` are stateless.
|
|
|
|
---
|
|
|
|
### Task 1: Add `StatefulMcpRuntime` Protocol
|
|
|
|
**Files:**
|
|
- Modify: `src/wf_sources_mcp/sdk/protocols.py`
|
|
- Modify: `src/wf_sources_mcp/sdk/__init__.py`
|
|
- Modify: `tests/wf_sources_mcp/test_sdk_protocols.py`
|
|
|
|
- [ ] **Step 1: Add protocol test**
|
|
|
|
Append to `tests/wf_sources_mcp/test_sdk_protocols.py`:
|
|
|
|
```python
|
|
def test_stateful_mcp_runtime_protocol_shape() -> None:
|
|
from wf_sources_mcp.sdk import StatefulMcpRuntime, ToolExecutor
|
|
|
|
assert StatefulMcpRuntime.__name__ == "StatefulMcpRuntime"
|
|
assert ToolExecutor.__name__ == "ToolExecutor"
|
|
```
|
|
|
|
- [ ] **Step 2: Add the protocol**
|
|
|
|
In `src/wf_sources_mcp/sdk/protocols.py`, add after `ToolExecutor`:
|
|
|
|
```python
|
|
class StatefulMcpRuntime(ToolExecutor, Protocol):
|
|
"""Stateful execution/read boundary for configured MCP sources.
|
|
|
|
Implementations keep source session state across calls. Discovery/catalog
|
|
refresh may still use one-shot adapters by policy.
|
|
"""
|
|
|
|
async def read_resource(
|
|
self,
|
|
connection: McpSourceConnection,
|
|
auth: AuthRecord | None,
|
|
uri: str,
|
|
) -> dict[str, Any]: ...
|
|
|
|
async def get_prompt(
|
|
self,
|
|
connection: McpSourceConnection,
|
|
auth: AuthRecord | None,
|
|
prompt_name: str,
|
|
arguments: dict[str, str] | None = None,
|
|
) -> dict[str, Any]: ...
|
|
```
|
|
|
|
Update `__all__`:
|
|
|
|
```python
|
|
__all__ = [
|
|
"BackendAdapter",
|
|
"StatefulMcpRuntime",
|
|
"ToolCallResult",
|
|
"ToolExecutor",
|
|
]
|
|
```
|
|
|
|
- [ ] **Step 3: Export protocol from package root**
|
|
|
|
In `src/wf_sources_mcp/sdk/__init__.py`, import/export `StatefulMcpRuntime`:
|
|
|
|
```python
|
|
from .protocols import BackendAdapter, StatefulMcpRuntime, ToolCallResult, ToolExecutor
|
|
```
|
|
|
|
and include `"StatefulMcpRuntime"` in `__all__`.
|
|
|
|
- [ ] **Step 4: Run protocol tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_sources_mcp/test_sdk_protocols.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
### Task 2: Preserve Compatibility Exports
|
|
|
|
**Files:**
|
|
- Modify: `src/wf_mcp/sdk/base.py`
|
|
- Modify: `src/wf_mcp/sdk/__init__.py`
|
|
- Modify: `tests/wf_mcp/test_compat_imports.py`
|
|
|
|
- [ ] **Step 1: Update compatibility shims**
|
|
|
|
In `src/wf_mcp/sdk/base.py`, re-export `StatefulMcpRuntime` with the existing protocol exports:
|
|
|
|
```python
|
|
from wf_sources_mcp.sdk import BackendAdapter, StatefulMcpRuntime, ToolCallResult
|
|
|
|
__all__ = ["BackendAdapter", "StatefulMcpRuntime", "ToolCallResult"]
|
|
```
|
|
|
|
In `src/wf_mcp/sdk/__init__.py`, update imports and `__all__`:
|
|
|
|
```python
|
|
from wf_sources_mcp.sdk import (
|
|
BackendAdapter,
|
|
McpSdkAdapter,
|
|
StatefulMcpRuntime,
|
|
ToolCallResult,
|
|
)
|
|
|
|
__all__ = ["BackendAdapter", "McpSdkAdapter", "StatefulMcpRuntime", "ToolCallResult"]
|
|
```
|
|
|
|
- [ ] **Step 2: Extend compatibility test**
|
|
|
|
In `tests/wf_mcp/test_compat_imports.py`, extend `test_wf_mcp_sdk_protocol_shims_reexport_wf_sources_mcp_sdk`:
|
|
|
|
```python
|
|
from wf_mcp.sdk import StatefulMcpRuntime as CompatStatefulMcpRuntime
|
|
from wf_mcp.sdk.base import StatefulMcpRuntime as CompatBaseStatefulMcpRuntime
|
|
from wf_sources_mcp.sdk import StatefulMcpRuntime
|
|
|
|
assert CompatStatefulMcpRuntime is StatefulMcpRuntime
|
|
assert CompatBaseStatefulMcpRuntime is StatefulMcpRuntime
|
|
```
|
|
|
|
- [ ] **Step 3: Run compatibility test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_mcp/test_compat_imports.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
### Task 3: Add Stateful Runtime Routing to Upstream Transport
|
|
|
|
**Files:**
|
|
- Modify: `src/wf_mcp/broker/service/upstream_transport.py`
|
|
- Modify: `tests/wf_mcp/service/test_upstream_transport.py`
|
|
|
|
- [ ] **Step 1: Add fake stateful runtime and adapter tests**
|
|
|
|
Append to `tests/wf_mcp/service/test_upstream_transport.py`:
|
|
|
|
```python
|
|
class _StatefulRuntime:
|
|
def __init__(self) -> None:
|
|
self.resources: list[tuple[str, str]] = []
|
|
self.prompts: list[tuple[str, str, dict[str, str] | None]] = []
|
|
|
|
async def call_tool(self, connection, auth, tool_name, payload):
|
|
raise AssertionError("not used by these tests")
|
|
|
|
async def read_resource(self, connection, auth, uri: str):
|
|
self.resources.append((connection.id, uri))
|
|
return {"contents": [{"uri": uri, "text": "stateful resource"}]}
|
|
|
|
async def get_prompt(
|
|
self,
|
|
connection,
|
|
auth,
|
|
prompt_name: str,
|
|
arguments: dict[str, str] | None = None,
|
|
):
|
|
self.prompts.append((connection.id, prompt_name, arguments))
|
|
return {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": {"type": "text", "text": "stateful prompt"},
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
class _ExplodingContentAdapter(FakeAdapter):
|
|
async def read_resource(self, connection, auth, uri):
|
|
raise AssertionError("adapter read_resource should not be used")
|
|
|
|
async def get_prompt(self, connection, auth, prompt_name, arguments=None):
|
|
raise AssertionError("adapter get_prompt should not be used")
|
|
```
|
|
|
|
Add tests:
|
|
|
|
```python
|
|
async def test_upstream_transport_prefers_stateful_runtime_for_resource_reads(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
events: list[McpEvent] = []
|
|
runtime = _StatefulRuntime()
|
|
transport = UpstreamTransportService(
|
|
auth_store=FileStore(tmp_path),
|
|
catalog_store=FileStore(tmp_path),
|
|
event_sink=events.append,
|
|
stateful_runtime=runtime,
|
|
)
|
|
transport.register_adapter("demo", _ExplodingContentAdapter())
|
|
connection = ConnectionConfig(
|
|
id="demo.personal",
|
|
server="demo",
|
|
account="personal",
|
|
metadata=_fake_transport_metadata(),
|
|
)
|
|
|
|
result = await transport.read_resource(
|
|
connection,
|
|
"demo.personal.resource.welcome",
|
|
"fixture://docs/welcome",
|
|
)
|
|
|
|
assert result["contents"][0]["text"] == "stateful resource"
|
|
assert runtime.resources == [("demo.personal", "fixture://docs/welcome")]
|
|
assert [event.kind for event in events] == [
|
|
"resource_read_started",
|
|
"resource_read_completed",
|
|
]
|
|
|
|
|
|
async def test_upstream_transport_prefers_stateful_runtime_for_prompts(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
events: list[McpEvent] = []
|
|
runtime = _StatefulRuntime()
|
|
transport = UpstreamTransportService(
|
|
auth_store=FileStore(tmp_path),
|
|
catalog_store=FileStore(tmp_path),
|
|
event_sink=events.append,
|
|
stateful_runtime=runtime,
|
|
)
|
|
transport.register_adapter("demo", _ExplodingContentAdapter())
|
|
connection = ConnectionConfig(
|
|
id="demo.personal",
|
|
server="demo",
|
|
account="personal",
|
|
metadata=_fake_transport_metadata(),
|
|
)
|
|
|
|
result = await transport.render_prompt(
|
|
connection,
|
|
"demo.personal.prompt.summarize",
|
|
"prompt.summarize",
|
|
{"text": "hello"},
|
|
)
|
|
|
|
assert result["messages"][0]["content"]["text"] == "stateful prompt"
|
|
assert runtime.prompts == [
|
|
("demo.personal", "prompt.summarize", {"text": "hello"})
|
|
]
|
|
assert [event.kind for event in events] == [
|
|
"prompt_get_started",
|
|
"prompt_get_completed",
|
|
]
|
|
```
|
|
|
|
- [ ] **Step 2: Add `stateful_runtime` field and import protocol**
|
|
|
|
In `src/wf_mcp/broker/service/upstream_transport.py`, change import:
|
|
|
|
```python
|
|
from wf_sources_mcp.sdk import BackendAdapter, StatefulMcpRuntime, ToolExecutor
|
|
```
|
|
|
|
Add field to `UpstreamTransportService`:
|
|
|
|
```python
|
|
stateful_runtime: StatefulMcpRuntime | None = None
|
|
```
|
|
|
|
- [ ] **Step 3: Prefer stateful runtime for resource reads**
|
|
|
|
In `read_resource`, replace:
|
|
|
|
```python
|
|
adapter = require_adapter(connection, self.adapters)
|
|
auth = self.load_connection_auth(connection)
|
|
# Compatibility boundary...
|
|
source_connection = mcp_source_connection_from_connection_config(connection)
|
|
```
|
|
|
|
with:
|
|
|
|
```python
|
|
auth = self.load_connection_auth(connection)
|
|
# Compatibility boundary: broker callers still pass ConnectionConfig.
|
|
source_connection = mcp_source_connection_from_connection_config(connection)
|
|
```
|
|
|
|
Then replace the execution line:
|
|
|
|
```python
|
|
result = await adapter.read_resource(source_connection, auth, uri)
|
|
```
|
|
|
|
with:
|
|
|
|
```python
|
|
if self.stateful_runtime is not None:
|
|
result = await self.stateful_runtime.read_resource(
|
|
source_connection,
|
|
auth,
|
|
uri,
|
|
)
|
|
else:
|
|
adapter = require_adapter(connection, self.adapters)
|
|
result = await adapter.read_resource(source_connection, auth, uri)
|
|
```
|
|
|
|
- [ ] **Step 4: Prefer stateful runtime for prompt gets**
|
|
|
|
In `render_prompt`, remove early adapter lookup and replace execution:
|
|
|
|
```python
|
|
if self.stateful_runtime is not None:
|
|
result = await self.stateful_runtime.get_prompt(
|
|
source_connection,
|
|
auth,
|
|
local_name,
|
|
arguments,
|
|
)
|
|
else:
|
|
adapter = require_adapter(connection, self.adapters)
|
|
result = await adapter.get_prompt(source_connection, auth, local_name, arguments)
|
|
```
|
|
|
|
Keep existing start/completed events unchanged.
|
|
|
|
- [ ] **Step 5: Run upstream transport tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_mcp/service/test_upstream_transport.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
### Task 4: Wire Configured Runtime as Stateful Runtime
|
|
|
|
**Files:**
|
|
- Modify: `src/wf_mcp/broker/service/core.py`
|
|
- Modify: `src/wf_mcp/broker/config.py` if needed.
|
|
- Modify: `tests/wf_mcp/service/test_content_access.py`
|
|
|
|
- [ ] **Step 1: Wire service field**
|
|
|
|
In `src/wf_mcp/broker/service/core.py`, when constructing `UpstreamTransportService`, pass:
|
|
|
|
```python
|
|
stateful_runtime=self.tool_executor
|
|
if isinstance(self.tool_executor, object)
|
|
else None,
|
|
```
|
|
|
|
Do not use that exact `isinstance(..., object)` if basedpyright dislikes it. Prefer a simple assignment after adding a `stateful_runtime` field to `WfMcpService` if needed:
|
|
|
|
```python
|
|
stateful_runtime: StatefulMcpRuntime | None = None
|
|
```
|
|
|
|
Then:
|
|
|
|
```python
|
|
stateful_runtime = self.stateful_runtime
|
|
self.upstream = UpstreamTransportService(
|
|
auth_store=auth_store,
|
|
catalog_store=catalog_store,
|
|
event_sink=self.events.record_event,
|
|
tool_executor=self.tool_executor,
|
|
stateful_runtime=stateful_runtime,
|
|
)
|
|
```
|
|
|
|
For config-built services that construct `McpRuntimePool`, set both:
|
|
|
|
```python
|
|
runtime_pool = McpRuntimePool(runtime_factory.create)
|
|
service = WfMcpService(
|
|
...,
|
|
tool_executor=runtime_pool,
|
|
stateful_runtime=runtime_pool,
|
|
)
|
|
```
|
|
|
|
If current `build_service_from_config` already constructs the pool inline, refactor to a local `runtime_pool` variable.
|
|
|
|
- [ ] **Step 2: Add content access service test with stateful runtime**
|
|
|
|
Append to `tests/wf_mcp/service/test_content_access.py`:
|
|
|
|
```python
|
|
class _StatefulRuntime:
|
|
def __init__(self) -> None:
|
|
self.resources: list[str] = []
|
|
self.prompts: list[str] = []
|
|
|
|
async def call_tool(self, connection, auth, tool_name, payload):
|
|
raise AssertionError("not used")
|
|
|
|
async def read_resource(self, connection, auth, uri: str):
|
|
self.resources.append(uri)
|
|
return {"contents": [{"uri": uri, "text": "stateful resource"}]}
|
|
|
|
async def get_prompt(self, connection, auth, prompt_name, arguments=None):
|
|
self.prompts.append(prompt_name)
|
|
return {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": {"type": "text", "text": "stateful prompt"},
|
|
}
|
|
]
|
|
}
|
|
|
|
|
|
async def test_content_access_uses_stateful_runtime_for_upstream_content() -> None:
|
|
runtime = _StatefulRuntime()
|
|
service = WfMcpService(
|
|
store=FileStore(local_temp_root() / "content_stateful_runtime"),
|
|
tool_executor=runtime,
|
|
stateful_runtime=runtime,
|
|
)
|
|
service.register_connection(
|
|
ConnectionConfig(
|
|
id="demo.personal",
|
|
server="demo",
|
|
account="personal",
|
|
metadata={"transport": "stdio", "command": "fake-mcp-server"},
|
|
)
|
|
)
|
|
service.register_adapter("demo", FakeAdapter())
|
|
await service.refresh_connection_catalog("demo.personal")
|
|
|
|
resource = await service.content_access.read_resource(
|
|
"demo.personal.resource.welcome"
|
|
)
|
|
prompt = await service.content_access.render_prompt(
|
|
"demo.personal.prompt.summarize",
|
|
arguments={"text": "hello"},
|
|
)
|
|
|
|
assert resource["contents"][0]["text"] == "stateful resource"
|
|
assert prompt["messages"][0]["content"]["text"] == "stateful prompt"
|
|
assert runtime.resources == ["fixture://resource/welcome"]
|
|
assert runtime.prompts == ["prompt.summarize"]
|
|
```
|
|
|
|
Adjust expected URI if `FakeAdapter` catalog fixture uses a different resource URI; read it from the failure and keep the exact catalog URI.
|
|
|
|
- [ ] **Step 3: Run content tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_mcp/service/test_content_access.py tests/wf_mcp/service/test_upstream_transport.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
---
|
|
|
|
### Task 5: Docs and Plan Archive
|
|
|
|
**Files:**
|
|
- Modify: `docs/current_roadmap.md`
|
|
- Modify: `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`
|
|
- Move after completion: `docs/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md` to `docs/historical/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md`
|
|
|
|
- [ ] **Step 1: Update roadmap**
|
|
|
|
In `docs/current_roadmap.md`, under the MCP upstream source runtime cleanup bullets, add:
|
|
|
|
```markdown
|
|
- Completed: broker content access now prefers a configured stateful MCP
|
|
runtime for `read_resource` and `get_prompt`, with one-shot adapter fallback.
|
|
Catalog refresh/discovery remains one-shot by policy.
|
|
```
|
|
|
|
- [ ] **Step 2: Update long-lived API boundary spec**
|
|
|
|
In `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`, add after the prompt runtime item:
|
|
|
|
```markdown
|
|
13. Complete: broker content access now prefers configured `StatefulMcpRuntime`
|
|
for resource and prompt reads, falling back to the one-shot adapter when no
|
|
stateful runtime is configured. Catalog refresh/discovery remains one-shot.
|
|
```
|
|
|
|
Renumber following item if needed.
|
|
|
|
- [ ] **Step 3: Archive completed plan after implementation**
|
|
|
|
Run only after all code/tests pass:
|
|
|
|
```bash
|
|
git mv docs/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md docs/historical/superpowers/plans/2026-06-07-content-access-stateful-runtime-routing.md
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Final Verification
|
|
|
|
**Files:**
|
|
- All changed files.
|
|
|
|
- [ ] **Step 1: Run focused tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run pytest tests/wf_sources_mcp tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_content_access.py tests/wf_mcp/test_compat_imports.py -q
|
|
```
|
|
|
|
Expected: pass.
|
|
|
|
- [ ] **Step 2: Run source typecheck**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run basedpyright --level error src
|
|
```
|
|
|
|
Expected: `0 errors, 0 warnings, 0 notes`.
|
|
|
|
- [ ] **Step 3: Run focused test typecheck**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run basedpyright --level error tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_content_access.py tests/wf_sources_mcp/test_sdk_protocols.py
|
|
```
|
|
|
|
Expected: `0 errors, 0 warnings, 0 notes`.
|
|
|
|
- [ ] **Step 4: Run focused lint**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
uv run ruff check src/wf_sources_mcp/sdk src/wf_mcp/sdk src/wf_mcp/broker/service/upstream_transport.py src/wf_mcp/broker/service/core.py tests/wf_mcp/service/test_upstream_transport.py tests/wf_mcp/service/test_content_access.py
|
|
```
|
|
|
|
Expected: `All checks passed!`
|
|
|
|
- [ ] **Step 5: Check no discovery routing changed**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
rg -n "stateful_runtime\\.(list_tools|list_resources|list_prompts)" src
|
|
```
|
|
|
|
Expected: no matches.
|
|
|
|
- [ ] **Step 6: Check whitespace**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
git diff --check
|
|
```
|
|
|
|
Expected: no whitespace errors. CRLF warnings are acceptable on Windows.
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: The plan makes stateful-vs-one-shot routing explicit and only for content reads.
|
|
- Placeholder scan: No placeholder steps remain.
|
|
- Type consistency: `StatefulMcpRuntime` is a protocol satisfied by `McpRuntimePool`.
|
|
- Policy boundary: discovery/catalog refresh still uses one-shot adapter; no assumption that resource/prompt listing is stateless.
|
|
|