Skip to content

Commit 18cd260

Browse files
Release v0.5.0
1 parent 10f14fd commit 18cd260

13 files changed

Lines changed: 486 additions & 320 deletions

File tree

dispatch_cli/commands/llm.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def configure_provider(
267267
"default_model": model,
268268
"scope": "org",
269269
"set_default": set_default,
270+
"allow_overwrite": True,
270271
},
271272
headers=auth_headers,
272273
timeout=30,
@@ -891,6 +892,7 @@ def setup_wizard(
891892
"default_model": model,
892893
"scope": remote_scope,
893894
"set_default": set_default,
895+
"allow_overwrite": True,
894896
},
895897
headers=auth_headers,
896898
timeout=30,

dispatch_cli/logger.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
- Syntax-highlighted code blocks
88
"""
99

10+
import sys
1011
from contextlib import contextmanager
12+
from io import StringIO
1113
from typing import Literal
1214

1315
from rich.console import Console
@@ -28,8 +30,6 @@ def __init__(self, verbose: bool = False):
2830
Args:
2931
verbose: If True, show all messages including debug. If False, only show important messages.
3032
"""
31-
import sys
32-
3333
self.verbose = verbose
3434
self.console = Console()
3535
self._live_context: Live | None = None
@@ -52,18 +52,12 @@ def _print(
5252
**kwargs: Extra fields for Rich console
5353
"""
5454
if self._is_piped:
55-
from io import StringIO
56-
57-
from rich.console import Console as PlainConsole
58-
5955
# Render any Rich object to plain text
6056
string_io = StringIO()
61-
plain_console = PlainConsole(
62-
file=string_io, force_terminal=False, no_color=True
63-
)
57+
plain_console = Console(file=string_io, force_terminal=False, no_color=True)
6458
plain_console.print(message)
6559
plain = string_io.getvalue().rstrip("\n")
66-
print(f"{plain_prefix}{plain}", flush=True)
60+
print(f"{plain_prefix}{plain}", file=sys.stderr, flush=True)
6761
else:
6862
self.console.print(message, **kwargs)
6963

dispatch_cli/mcp/client.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
CreateScheduleResponse,
1111
DeleteScheduleRequest,
1212
DeleteScheduleResponse,
13+
EventRecord,
14+
EventTraceResponse,
1315
GetScheduleRequest,
1416
GetScheduleResponse,
1517
ListSchedulesRequest,
1618
ListSchedulesResponse,
1719
RebootAgentResponse,
20+
RecentTracesResponse,
1821
StopAgentResponse,
22+
TopicListItem,
1923
UpdateScheduleRequest,
2024
UpdateScheduleResponse,
2125
)
@@ -147,6 +151,50 @@ def get_topic_schema(self, topic: str, namespace: str | None = None) -> dict:
147151
resp.raise_for_status()
148152
return resp.json()
149153

154+
def list_topics(self, namespace: str) -> list[TopicListItem]:
155+
"""List all topics in namespace."""
156+
url = self._namespaced_url("/events/topics", namespace)
157+
resp = self.client.get(url)
158+
resp.raise_for_status()
159+
return [TopicListItem.model_validate(t) for t in resp.json()]
160+
161+
def get_recent_events(
162+
self,
163+
namespace: str,
164+
topic: str | None = None,
165+
limit: int = 20,
166+
) -> list[EventRecord]:
167+
"""Get recent events, optionally filtered by topic."""
168+
url = self._namespaced_url("/events/recent", namespace)
169+
params: dict[str, str | int] = {"limit": limit}
170+
if topic:
171+
params["topic"] = topic
172+
resp = self.client.get(url, params=params)
173+
resp.raise_for_status()
174+
return [EventRecord.model_validate(e) for e in resp.json()]
175+
176+
def get_event_trace(self, trace_id: str, namespace: str) -> EventTraceResponse:
177+
"""Get full event trace by trace ID."""
178+
url = self._namespaced_url(f"/events/trace/{trace_id}", namespace)
179+
resp = self.client.get(url)
180+
resp.raise_for_status()
181+
return EventTraceResponse.model_validate(resp.json())
182+
183+
def get_recent_traces(
184+
self,
185+
namespace: str,
186+
topic: str | None = None,
187+
limit: int = 50,
188+
) -> RecentTracesResponse:
189+
"""Get recent trace summaries."""
190+
url = self._namespaced_url("/events/traces/recent", namespace)
191+
params: dict[str, str | int] = {"limit": limit}
192+
if topic:
193+
params["topic"] = topic
194+
resp = self.client.get(url, params=params)
195+
resp.raise_for_status()
196+
return RecentTracesResponse.model_validate(resp.json())
197+
150198
# Invoke Operations
151199
def invoke_function(
152200
self,

dispatch_cli/mcp/models.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Shared Pydantic models for MCP client and tools."""
22

3-
from typing import Any
3+
from typing import Any, Literal
44

55
from pydantic import BaseModel, Field
66

@@ -146,3 +146,82 @@ class RebootAgentResponse(BaseModel):
146146
description="Deployment job ID for polling status with get_deploy_status"
147147
)
148148
version: str = Field(description="Agent version being deployed")
149+
150+
151+
# Topic & Event Models
152+
153+
154+
class SubscribedHandler(BaseModel):
155+
"""A handler subscribed to a topic."""
156+
157+
agent_name: str
158+
handler_name: str
159+
160+
161+
class TopicListItem(BaseModel):
162+
"""A topic item as returned by the list topics endpoint."""
163+
164+
topic: str
165+
topic_id: str | None = None
166+
created_at: str | None = None
167+
namespace: str | None = None
168+
webhook_enabled: bool | None = None
169+
webhook_provider: str | None = None
170+
subscribers: list[str] = []
171+
subscribed_handlers: list[SubscribedHandler] = []
172+
integration: str | None = None
173+
schema_: dict[str, Any] | None = Field(default=None, alias="schema")
174+
schema_locked: bool = False
175+
description: str | None = None
176+
sdk_docs_url: str | None = None
177+
178+
model_config = {"populate_by_name": True}
179+
180+
181+
class EventRecord(BaseModel):
182+
"""A single event record from the event history."""
183+
184+
uid: str | None = None
185+
message_type: str | None = None
186+
topic: str | None = None
187+
function_name: str | None = None
188+
schedule_name: str | None = None
189+
source: str | None = None
190+
timestamp: str | None = None
191+
trace_id: str | None = None
192+
parent_id: str | None = None
193+
payload: dict[str, Any] | None = None
194+
195+
196+
class TraceSummary(BaseModel):
197+
"""Summary of a trace (session) with agent invocations."""
198+
199+
trace_id: str
200+
first_event_timestamp: str
201+
event_count: int
202+
trigger: str
203+
trigger_type: Literal["topic", "function", "schedule", "unknown"]
204+
trigger_agent: str | None = None
205+
trigger_function: str | None = None
206+
schedule_name: str | None = None
207+
last_activity: str
208+
root_event_uid: str | None = None
209+
root_topic: str | None = None
210+
agents_involved: list[str]
211+
212+
213+
class RecentTracesResponse(BaseModel):
214+
"""Response from the recent traces endpoint."""
215+
216+
total_events: int
217+
unique_traces: int
218+
traces: list[TraceSummary]
219+
220+
221+
class EventTraceResponse(BaseModel):
222+
"""Response from the event trace endpoint."""
223+
224+
events: list[dict[str, Any]] = Field(
225+
description="Tree-structured events with invocation enrichment"
226+
)
227+
llm_summary: dict[str, Any] | None = None

dispatch_cli/mcp/operator/tools.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,16 @@
2727
CreateScheduleResponse,
2828
DeleteScheduleRequest,
2929
DeleteScheduleResponse,
30+
EventRecord,
31+
EventTraceResponse,
3032
GetScheduleRequest,
3133
GetScheduleResponse,
3234
ListSchedulesRequest,
3335
ListSchedulesResponse,
3436
RebootAgentResponse,
37+
RecentTracesResponse,
3538
StopAgentResponse,
39+
TopicListItem,
3640
UpdateScheduleRequest,
3741
UpdateScheduleResponse,
3842
)
@@ -140,6 +144,47 @@ class PublishEventRequest(BaseModel):
140144
)
141145

142146

147+
class ListTopicsRequest(BaseModel):
148+
"""Request payload for listing topics."""
149+
150+
namespace: str = Field(
151+
description="Namespace (required). Use the namespace from the agent's dispatch.yaml, or call list_namespaces to discover valid namespaces."
152+
)
153+
154+
155+
class GetRecentEventsRequest(BaseModel):
156+
"""Request payload for getting recent events."""
157+
158+
namespace: str = Field(
159+
description="Namespace (required). Use the namespace from the agent's dispatch.yaml, or call list_namespaces to discover valid namespaces."
160+
)
161+
topic: str | None = Field(default=None, description="Optional topic filter")
162+
limit: int = Field(
163+
default=20, description="Max events to return (1-100)", ge=1, le=100
164+
)
165+
166+
167+
class GetEventTraceRequest(BaseModel):
168+
"""Request payload for getting an event trace."""
169+
170+
trace_id: str = Field(description="Trace ID to look up")
171+
namespace: str = Field(
172+
description="Namespace (required). Use the namespace from the agent's dispatch.yaml, or call list_namespaces to discover valid namespaces."
173+
)
174+
175+
176+
class GetRecentTracesRequest(BaseModel):
177+
"""Request payload for getting recent traces."""
178+
179+
namespace: str = Field(
180+
description="Namespace (required). Use the namespace from the agent's dispatch.yaml, or call list_namespaces to discover valid namespaces."
181+
)
182+
topic: str | None = Field(default=None, description="Optional topic filter")
183+
limit: int = Field(
184+
default=50, description="Max traces to return (1-100)", ge=1, le=100
185+
)
186+
187+
143188
class GetAgentFunctionsRequest(BaseModel):
144189
"""Request payload for getting agent functions."""
145190

@@ -1152,6 +1197,76 @@ async def publish_event(request: PublishEventRequest) -> PublishEventResponse:
11521197
result = client.publish_event(request.topic, request.payload, namespace=ns)
11531198
return PublishEventResponse(**result)
11541199

1200+
@mcp.tool()
1201+
async def list_topics(request: ListTopicsRequest) -> list[TopicListItem]:
1202+
"""List all topics in a namespace.
1203+
1204+
Returns topics with their subscribed handlers, webhook configuration,
1205+
and schema information.
1206+
1207+
Args:
1208+
request: ListTopicsRequest with namespace
1209+
1210+
Returns:
1211+
List of TopicListItem with topic details and subscribers
1212+
"""
1213+
ns = _get_namespace(request.namespace)
1214+
return client.list_topics(namespace=ns)
1215+
1216+
@mcp.tool()
1217+
async def get_recent_events(
1218+
request: GetRecentEventsRequest,
1219+
) -> list[EventRecord]:
1220+
"""Get recent events, optionally filtered by topic.
1221+
1222+
Args:
1223+
request: GetRecentEventsRequest with namespace, optional topic filter, and limit
1224+
1225+
Returns:
1226+
List of EventRecord with event details
1227+
"""
1228+
ns = _get_namespace(request.namespace)
1229+
return client.get_recent_events(
1230+
namespace=ns, topic=request.topic, limit=request.limit
1231+
)
1232+
1233+
@mcp.tool()
1234+
async def get_event_trace(request: GetEventTraceRequest) -> EventTraceResponse:
1235+
"""Get the full event trace tree for a given trace ID.
1236+
1237+
Returns a tree-structured view of all events in the trace, enriched
1238+
with invocation status, LLM call summaries, and MCP tool calls.
1239+
1240+
Args:
1241+
request: GetEventTraceRequest with trace_id and namespace
1242+
1243+
Returns:
1244+
EventTraceResponse with trace_id, total_events, tree-structured events, and optional llm_summary
1245+
"""
1246+
ns = _get_namespace(request.namespace)
1247+
return client.get_event_trace(trace_id=request.trace_id, namespace=ns)
1248+
1249+
@mcp.tool()
1250+
async def get_recent_traces(
1251+
request: GetRecentTracesRequest,
1252+
) -> RecentTracesResponse:
1253+
"""Get recent trace summaries for agent invocations.
1254+
1255+
Returns summaries of recent traces, including trigger type, involved
1256+
agents, and event counts. Useful for discovering trace IDs to inspect
1257+
with get_event_trace.
1258+
1259+
Args:
1260+
request: GetRecentTracesRequest with namespace, optional topic filter, and limit
1261+
1262+
Returns:
1263+
RecentTracesResponse with total_events, unique_traces, and list of TraceSummary
1264+
"""
1265+
ns = _get_namespace(request.namespace)
1266+
return client.get_recent_traces(
1267+
namespace=ns, topic=request.topic, limit=request.limit
1268+
)
1269+
11551270
@mcp.tool()
11561271
async def get_agent_functions(
11571272
request: GetAgentFunctionsRequest,

0 commit comments

Comments
 (0)