dispatch_agents 0.9.0__py3-none-any.whl
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.
- agentservice/__init__.py +0 -0
- agentservice/py.typed +0 -0
- agentservice/v1/__init__.py +0 -0
- agentservice/v1/message_pb2.py +41 -0
- agentservice/v1/message_pb2.pyi +22 -0
- agentservice/v1/message_pb2_grpc.py +4 -0
- agentservice/v1/request_response_pb2.py +46 -0
- agentservice/v1/request_response_pb2.pyi +54 -0
- agentservice/v1/request_response_pb2_grpc.py +4 -0
- agentservice/v1/service_pb2.py +43 -0
- agentservice/v1/service_pb2.pyi +6 -0
- agentservice/v1/service_pb2_grpc.py +129 -0
- dispatch_agents/__init__.py +281 -0
- dispatch_agents/agent_service.py +135 -0
- dispatch_agents/config.py +490 -0
- dispatch_agents/contrib/__init__.py +1 -0
- dispatch_agents/contrib/claude/__init__.py +246 -0
- dispatch_agents/contrib/openai/__init__.py +167 -0
- dispatch_agents/events.py +986 -0
- dispatch_agents/grpc_server.py +565 -0
- dispatch_agents/instrument.py +217 -0
- dispatch_agents/integrations/__init__.py +1 -0
- dispatch_agents/integrations/github/README.md +9 -0
- dispatch_agents/integrations/github/__init__.py +4268 -0
- dispatch_agents/invocation.py +25 -0
- dispatch_agents/llm.py +1017 -0
- dispatch_agents/llm_langchain.py +394 -0
- dispatch_agents/logging_config.py +133 -0
- dispatch_agents/mcp.py +266 -0
- dispatch_agents/memory.py +264 -0
- dispatch_agents/models.py +748 -0
- dispatch_agents/proxy/__init__.py +6 -0
- dispatch_agents/proxy/server.py +1137 -0
- dispatch_agents/proxy/sse_utils.py +76 -0
- dispatch_agents/py.typed +0 -0
- dispatch_agents/resources.py +68 -0
- dispatch_agents/version.py +19 -0
- dispatch_agents-0.9.0.dist-info/METADATA +20 -0
- dispatch_agents-0.9.0.dist-info/RECORD +43 -0
- dispatch_agents-0.9.0.dist-info/WHEEL +4 -0
- dispatch_agents-0.9.0.dist-info/licenses/LICENSE +191 -0
- dispatch_agents-0.9.0.dist-info/licenses/LICENSE-3rdparty.csv +12 -0
- dispatch_agents-0.9.0.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
"""Core models for the Dispatch SDK.
|
|
2
|
+
|
|
3
|
+
This module contains the fundamental data structures used across the entire
|
|
4
|
+
Dispatch ecosystem, including Message types for universal communication and Agent
|
|
5
|
+
for service registration and management.
|
|
6
|
+
|
|
7
|
+
Message Type Hierarchy (similar to LangChain's BaseMessage pattern):
|
|
8
|
+
- BaseMessage: Abstract base with common fields (uid, trace_id, sender_id, ts, payload)
|
|
9
|
+
- TopicMessage: For @on topic handlers (has 'topic' field)
|
|
10
|
+
- FunctionMessage: For @fn direct calls (has 'function_name' field)
|
|
11
|
+
- ScheduleMessage: For scheduled/cron triggers (has 'schedule_name' field)
|
|
12
|
+
- Message: Discriminated union type alias for routing
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
from datetime import UTC, datetime
|
|
17
|
+
from enum import StrEnum, auto
|
|
18
|
+
from typing import Annotated, Any, Literal, TypeAlias
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_now_utc() -> str:
|
|
24
|
+
"""Get the current UTC time in ISO8601 format."""
|
|
25
|
+
return datetime.now(UTC).isoformat()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
JsonSchema: TypeAlias = dict[str, Any]
|
|
29
|
+
"""A JSON Schema document, e.g. from Pydantic's model_json_schema()."""
|
|
30
|
+
|
|
31
|
+
# =============================================================================
|
|
32
|
+
# Feedback Types - Shared across backend, CLI, and SDK
|
|
33
|
+
# =============================================================================
|
|
34
|
+
|
|
35
|
+
FeedbackType: TypeAlias = Literal["bug", "feature_request", "general"]
|
|
36
|
+
"""Type of customer feedback submission."""
|
|
37
|
+
|
|
38
|
+
FeedbackSentiment: TypeAlias = Literal["positive", "negative"]
|
|
39
|
+
"""Thumbs up/down sentiment for feedback."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class StrictBaseModel(BaseModel):
|
|
43
|
+
"""Base model with strict validation that forbids extra fields.
|
|
44
|
+
|
|
45
|
+
All Dispatch models inherit from this to ensure API compatibility
|
|
46
|
+
and catch typos in field names at validation time.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(extra="forbid")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Message Types - Discriminated Union Pattern
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Following LangChain's BaseMessage pattern with Pydantic discriminated unions.
|
|
56
|
+
# The 'type' field acts as discriminator - Pydantic routes to correct subclass
|
|
57
|
+
# BEFORE validation, so strict validation (extra='forbid') works correctly.
|
|
58
|
+
# =============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class BaseMessage(StrictBaseModel):
|
|
62
|
+
"""Abstract base class for all dispatch messages.
|
|
63
|
+
|
|
64
|
+
Similar to LangChain's BaseMessage pattern. Contains common fields
|
|
65
|
+
shared across all message types. Should not be instantiated directly -
|
|
66
|
+
use TopicMessage, FunctionMessage, or ScheduleMessage instead.
|
|
67
|
+
|
|
68
|
+
| Field | Type | Required | Notes |
|
|
69
|
+
| ---------- | ------ | -------- | -------------------------------------------------------------------- |
|
|
70
|
+
| type | string | always | Discriminator: "topic", "function", or "schedule" |
|
|
71
|
+
| payload | object | always | Business data, validated against registered input model |
|
|
72
|
+
| uid | string | always | Unique ID per message |
|
|
73
|
+
| trace_id | string | always | Groups related messages into a workflow/session |
|
|
74
|
+
| sender_id | string | always | ID of the sending agent/tool |
|
|
75
|
+
| ts | string | always | ISO8601 timestamp of when message was created |
|
|
76
|
+
| parent_id | string | optional | UID of parent message for building trace trees (None for root events)|
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
type: str # Abstract - subclasses override with Literal
|
|
80
|
+
payload: Any
|
|
81
|
+
uid: str
|
|
82
|
+
trace_id: str
|
|
83
|
+
sender_id: str
|
|
84
|
+
ts: str
|
|
85
|
+
parent_id: str | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TopicMessage(BaseMessage):
|
|
89
|
+
"""Message routed by topic subscription (@on decorator).
|
|
90
|
+
|
|
91
|
+
Used for event-driven communication where agents subscribe to topics
|
|
92
|
+
and receive messages published to those topics.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
type: Literal["topic"] = "topic"
|
|
96
|
+
topic: str = Field(description="Event topic for subscription-based routing")
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def create(
|
|
100
|
+
cls,
|
|
101
|
+
topic: str,
|
|
102
|
+
payload: Any,
|
|
103
|
+
sender_id: str,
|
|
104
|
+
trace_id: str | None = None,
|
|
105
|
+
parent_id: str | None = None,
|
|
106
|
+
_uid: str | None = None,
|
|
107
|
+
_ts: str | None = None,
|
|
108
|
+
) -> "TopicMessage":
|
|
109
|
+
"""Create a new TopicMessage with auto-generated fields."""
|
|
110
|
+
return cls(
|
|
111
|
+
topic=topic,
|
|
112
|
+
payload=payload,
|
|
113
|
+
sender_id=sender_id,
|
|
114
|
+
trace_id=trace_id or str(uuid.uuid4()),
|
|
115
|
+
parent_id=parent_id,
|
|
116
|
+
uid=_uid or str(uuid.uuid4()),
|
|
117
|
+
ts=_ts or get_now_utc(),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class FunctionMessage(BaseMessage):
|
|
122
|
+
"""Message for direct function invocation (@fn decorator).
|
|
123
|
+
|
|
124
|
+
Used when one agent calls a function on another agent directly,
|
|
125
|
+
bypassing topic-based routing.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
type: Literal["function"] = "function"
|
|
129
|
+
function_name: str = Field(description="Name of the @fn function to invoke")
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def create(
|
|
133
|
+
cls,
|
|
134
|
+
function_name: str,
|
|
135
|
+
payload: Any,
|
|
136
|
+
sender_id: str,
|
|
137
|
+
trace_id: str | None = None,
|
|
138
|
+
parent_id: str | None = None,
|
|
139
|
+
_uid: str | None = None,
|
|
140
|
+
_ts: str | None = None,
|
|
141
|
+
) -> "FunctionMessage":
|
|
142
|
+
"""Create a new FunctionMessage with auto-generated fields."""
|
|
143
|
+
return cls(
|
|
144
|
+
type="function",
|
|
145
|
+
function_name=function_name,
|
|
146
|
+
payload=payload,
|
|
147
|
+
sender_id=sender_id,
|
|
148
|
+
trace_id=trace_id or str(uuid.uuid4()),
|
|
149
|
+
parent_id=parent_id,
|
|
150
|
+
uid=_uid or str(uuid.uuid4()),
|
|
151
|
+
ts=_ts or get_now_utc(),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ScheduleMessage(BaseMessage):
|
|
156
|
+
"""Message triggered by a schedule/cron job.
|
|
157
|
+
|
|
158
|
+
Used for time-based triggers where the system invokes an agent
|
|
159
|
+
function based on a schedule configuration.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
type: Literal["schedule"] = "schedule"
|
|
163
|
+
schedule_name: str = Field(description="Schedule ID that triggered this invocation")
|
|
164
|
+
function_name: str = Field(description="Name of the function being invoked")
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def create(
|
|
168
|
+
cls,
|
|
169
|
+
schedule_name: str,
|
|
170
|
+
function_name: str,
|
|
171
|
+
payload: Any,
|
|
172
|
+
sender_id: str,
|
|
173
|
+
trace_id: str | None = None,
|
|
174
|
+
parent_id: str | None = None,
|
|
175
|
+
_uid: str | None = None,
|
|
176
|
+
_ts: str | None = None,
|
|
177
|
+
) -> "ScheduleMessage":
|
|
178
|
+
"""Create a new ScheduleMessage with auto-generated fields."""
|
|
179
|
+
return cls(
|
|
180
|
+
type="schedule",
|
|
181
|
+
schedule_name=schedule_name,
|
|
182
|
+
function_name=function_name,
|
|
183
|
+
payload=payload,
|
|
184
|
+
sender_id=sender_id,
|
|
185
|
+
trace_id=trace_id or str(uuid.uuid4()),
|
|
186
|
+
parent_id=parent_id,
|
|
187
|
+
uid=_uid or str(uuid.uuid4()),
|
|
188
|
+
ts=_ts or get_now_utc(),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class LLMCallMessage(BaseMessage):
|
|
193
|
+
"""Message representing an LLM inference call.
|
|
194
|
+
|
|
195
|
+
Used to track LLM calls within a trace, enabling unified trace views
|
|
196
|
+
that show LLM interactions alongside invocations and topic messages.
|
|
197
|
+
|
|
198
|
+
The parent_id field can point to:
|
|
199
|
+
- An invocation UID: The LLM call was made by this invocation
|
|
200
|
+
- Another LLM call UID: The LLM call is a continuation (e.g., after tool execution)
|
|
201
|
+
|
|
202
|
+
Children of this LLM call (invocations with parent_id pointing here) represent
|
|
203
|
+
tool call results - invocations triggered by the LLM's tool_calls response.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
type: Literal["llm_call"] = "llm_call"
|
|
207
|
+
|
|
208
|
+
# LLM-specific fields
|
|
209
|
+
model: str = Field(description="Model used (e.g., gpt-4o, claude-3-5-sonnet)")
|
|
210
|
+
provider: str = Field(description="Provider (e.g., openai, anthropic)")
|
|
211
|
+
messages: list[dict[str, Any]] = Field(description="Request messages sent to LLM")
|
|
212
|
+
response: str | None = Field(default=None, description="LLM response text")
|
|
213
|
+
finish_reason: str = Field(
|
|
214
|
+
description="Completion reason: stop, tool_calls, length, etc."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Usage metrics
|
|
218
|
+
input_tokens: int = Field(description="Prompt token count")
|
|
219
|
+
output_tokens: int = Field(description="Completion token count")
|
|
220
|
+
cost_usd: float = Field(description="Calculated cost in USD")
|
|
221
|
+
latency_ms: int = Field(description="Response time in milliseconds")
|
|
222
|
+
|
|
223
|
+
# Optional fields
|
|
224
|
+
tools: list[dict[str, Any]] | None = Field(
|
|
225
|
+
default=None, description="Tool definitions if function calling was used"
|
|
226
|
+
)
|
|
227
|
+
tool_calls: list[dict[str, Any]] | None = Field(
|
|
228
|
+
default=None, description="Tool calls from response"
|
|
229
|
+
)
|
|
230
|
+
variant_name: str | None = Field(
|
|
231
|
+
default=None, description="A/B test variant if applicable"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def create(
|
|
236
|
+
cls,
|
|
237
|
+
*,
|
|
238
|
+
model: str,
|
|
239
|
+
provider: str,
|
|
240
|
+
messages: list[dict[str, Any]],
|
|
241
|
+
finish_reason: str,
|
|
242
|
+
input_tokens: int,
|
|
243
|
+
output_tokens: int,
|
|
244
|
+
cost_usd: float,
|
|
245
|
+
latency_ms: int,
|
|
246
|
+
sender_id: str,
|
|
247
|
+
trace_id: str,
|
|
248
|
+
parent_id: str | None = None,
|
|
249
|
+
response: str | None = None,
|
|
250
|
+
tools: list[dict[str, Any]] | None = None,
|
|
251
|
+
tool_calls: list[dict[str, Any]] | None = None,
|
|
252
|
+
variant_name: str | None = None,
|
|
253
|
+
_uid: str | None = None,
|
|
254
|
+
_ts: str | None = None,
|
|
255
|
+
) -> "LLMCallMessage":
|
|
256
|
+
"""Create a new LLMCallMessage with auto-generated fields.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
model: Model used for inference
|
|
260
|
+
provider: LLM provider
|
|
261
|
+
messages: Request messages
|
|
262
|
+
finish_reason: Why the LLM stopped generating
|
|
263
|
+
input_tokens: Number of input tokens
|
|
264
|
+
output_tokens: Number of output tokens
|
|
265
|
+
cost_usd: Cost in USD
|
|
266
|
+
latency_ms: Latency in milliseconds
|
|
267
|
+
sender_id: Agent/caller that made this LLM call
|
|
268
|
+
trace_id: Trace ID for correlation
|
|
269
|
+
parent_id: Parent invocation or LLM call UID
|
|
270
|
+
response: LLM response text
|
|
271
|
+
tools: Tool definitions
|
|
272
|
+
tool_calls: Tool calls from response
|
|
273
|
+
variant_name: A/B test variant
|
|
274
|
+
_uid: Override UID (auto-generated if not provided)
|
|
275
|
+
_ts: Override timestamp
|
|
276
|
+
"""
|
|
277
|
+
uid = _uid or str(uuid.uuid4())
|
|
278
|
+
return cls(
|
|
279
|
+
type="llm_call",
|
|
280
|
+
model=model,
|
|
281
|
+
provider=provider,
|
|
282
|
+
messages=messages,
|
|
283
|
+
finish_reason=finish_reason,
|
|
284
|
+
input_tokens=input_tokens,
|
|
285
|
+
output_tokens=output_tokens,
|
|
286
|
+
cost_usd=cost_usd,
|
|
287
|
+
latency_ms=latency_ms,
|
|
288
|
+
sender_id=sender_id,
|
|
289
|
+
trace_id=trace_id,
|
|
290
|
+
parent_id=parent_id,
|
|
291
|
+
response=response,
|
|
292
|
+
tools=tools,
|
|
293
|
+
tool_calls=tool_calls,
|
|
294
|
+
variant_name=variant_name,
|
|
295
|
+
# BaseMessage fields - payload is empty for LLM calls (data is in specific fields)
|
|
296
|
+
payload={},
|
|
297
|
+
uid=uid,
|
|
298
|
+
ts=_ts or get_now_utc(),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# Discriminated union - Pydantic routes by 'type' field before validation
|
|
303
|
+
# This means strict validation (extra='forbid') works correctly with subclass fields
|
|
304
|
+
Message = Annotated[
|
|
305
|
+
TopicMessage | FunctionMessage | ScheduleMessage | LLMCallMessage,
|
|
306
|
+
Field(discriminator="type"),
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# =============================================================================
|
|
311
|
+
# Handler Response Payloads
|
|
312
|
+
# =============================================================================
|
|
313
|
+
# These are serialized into the gRPC InvokeResponse.result field.
|
|
314
|
+
# The proto's is_error bool tells the backend which type to deserialize to.
|
|
315
|
+
# =============================================================================
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class SuccessPayload(StrictBaseModel):
|
|
319
|
+
"""Successful handler execution result.
|
|
320
|
+
|
|
321
|
+
Serialized into InvokeResponse.result when is_error=False.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
result: Any = Field(
|
|
325
|
+
description="Handler return value (any JSON-serializable value)"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class ErrorPayload(StrictBaseModel):
|
|
330
|
+
"""Failed handler execution result.
|
|
331
|
+
|
|
332
|
+
Serialized into InvokeResponse.result when is_error=True.
|
|
333
|
+
Contains structured error information for debugging and display.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
error: str = Field(description="Error message")
|
|
337
|
+
error_type: str = Field(
|
|
338
|
+
description="Exception class name (e.g., 'ValidationError', 'ValueError')"
|
|
339
|
+
)
|
|
340
|
+
trace: str | None = Field(default=None, description="Full traceback for debugging")
|
|
341
|
+
details: Any | None = Field(
|
|
342
|
+
default=None,
|
|
343
|
+
description="Additional error details (e.g., Pydantic validation errors list)",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class AgentContainerStatus(StrEnum):
|
|
348
|
+
BUILDING = auto() # agent created/updated when codebuild starts
|
|
349
|
+
DEPLOYING = auto() # image built, ecs rolling update in progress
|
|
350
|
+
DEPLOYED = auto() # ip/dns alias available from ecs, ready to be health-checked
|
|
351
|
+
ERROR = auto() # ecs task failed to launch/retrying
|
|
352
|
+
HEALTHY = auto() # passed health check from in-memory registry
|
|
353
|
+
UNHEALTHY = (
|
|
354
|
+
auto()
|
|
355
|
+
) # deployed agent went from healthy->unhealthy or never passed health check
|
|
356
|
+
DISABLED = (
|
|
357
|
+
auto()
|
|
358
|
+
) # intentionally stopped/disabled, will not be considered for health check
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class FunctionTrigger(StrictBaseModel):
|
|
362
|
+
"""Trigger configuration for an agent function.
|
|
363
|
+
|
|
364
|
+
Defines how and when an agent function is invoked. Currently supports:
|
|
365
|
+
- topic: Event-driven triggers from subscribed topics
|
|
366
|
+
- callable: Direct function calls from other agents via invoke()
|
|
367
|
+
- schedule: Time-based triggers (future)
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
type: Literal["topic", "schedule", "callable"] = Field(
|
|
371
|
+
description="Trigger type: 'topic' for event-driven, 'callable' for direct invocation, 'schedule' for cron-based"
|
|
372
|
+
)
|
|
373
|
+
topic: str | None = Field(
|
|
374
|
+
default=None,
|
|
375
|
+
description="Topic name when type='topic'. Events published to this topic will invoke the function.",
|
|
376
|
+
)
|
|
377
|
+
function_name: str | None = Field(
|
|
378
|
+
default=None,
|
|
379
|
+
description="Function name when type='callable'. Other agents can invoke this function by name.",
|
|
380
|
+
)
|
|
381
|
+
# Future fields for schedule triggers:
|
|
382
|
+
# cron_expression: str | None - Cron syntax for schedule
|
|
383
|
+
# timezone: str | None - Timezone for schedule interpretation
|
|
384
|
+
# enabled: bool - Whether schedule is active
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class AgentFunction(StrictBaseModel):
|
|
388
|
+
"""Agent function with input/output schemas and triggers.
|
|
389
|
+
|
|
390
|
+
Represents a handler function in an agent, including its schemas and
|
|
391
|
+
the triggers that can invoke it (topics, schedules, etc.).
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
name: str = Field(description="Handler function name")
|
|
395
|
+
description: str | None = Field(
|
|
396
|
+
default=None, description="Handler docstring or description"
|
|
397
|
+
)
|
|
398
|
+
input_schema: JsonSchema = Field(
|
|
399
|
+
description="JSON Schema for input payload validation"
|
|
400
|
+
)
|
|
401
|
+
output_schema: JsonSchema | None = Field(
|
|
402
|
+
default=None, description="JSON Schema for output payload (if any)"
|
|
403
|
+
)
|
|
404
|
+
triggers: list[FunctionTrigger] = Field(
|
|
405
|
+
default_factory=list, description="Triggers that invoke this function"
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class Agent(StrictBaseModel):
|
|
410
|
+
"""Agent registration and metadata model.
|
|
411
|
+
|
|
412
|
+
Uses composite uid (org_id#namespace#name) as unique identifier.
|
|
413
|
+
The name field stores the ECS-sanitized agent name.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
# === Persistent fields (stored in DynamoDB) ===
|
|
417
|
+
name: str = Field(
|
|
418
|
+
description="ECS-sanitized agent name (used in uid composite key)"
|
|
419
|
+
)
|
|
420
|
+
org_id: str = Field(description="Organization ID for multi-tenancy")
|
|
421
|
+
namespace: str = Field(description="Namespace for logical isolation within org")
|
|
422
|
+
status: AgentContainerStatus = Field(
|
|
423
|
+
default=AgentContainerStatus.BUILDING,
|
|
424
|
+
description="Agent deployment/health status",
|
|
425
|
+
)
|
|
426
|
+
created_at: str = Field(description="ISO8601 timestamp when agent was created")
|
|
427
|
+
last_updated: str = Field(
|
|
428
|
+
description="ISO8601 timestamp when DB record was updated"
|
|
429
|
+
)
|
|
430
|
+
url: str | None = Field(default=None, description="Agent DNS alias endpoint URL")
|
|
431
|
+
version: str | None = Field(
|
|
432
|
+
default=None, description="Current deployed version from S3"
|
|
433
|
+
)
|
|
434
|
+
last_deployed: str | None = Field(
|
|
435
|
+
default=None,
|
|
436
|
+
description="ISO8601 timestamp of last successful deployment",
|
|
437
|
+
)
|
|
438
|
+
monthly_budget_usd: float | None = Field(
|
|
439
|
+
default=None,
|
|
440
|
+
description="Monthly LLM spend limit in USD. If set, inference requests "
|
|
441
|
+
"are blocked when the agent exceeds this amount in the current month.",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# === Runtime fields (in-memory only, not persisted to DynamoDB) ===
|
|
445
|
+
functions: list[AgentFunction] = Field(
|
|
446
|
+
default_factory=list,
|
|
447
|
+
description="Agent functions with their triggers, schemas, and metadata. "
|
|
448
|
+
"Each function can have multiple triggers (topics, schedules, etc.).",
|
|
449
|
+
)
|
|
450
|
+
last_heartbeat: str | None = Field(
|
|
451
|
+
default=None, description="ISO8601 timestamp of last heartbeat"
|
|
452
|
+
)
|
|
453
|
+
metadata: dict[str, Any] = Field(
|
|
454
|
+
default_factory=dict, description="Additional runtime metadata"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
@field_validator("name")
|
|
458
|
+
def validate_name(cls, v):
|
|
459
|
+
Agent._validate_identifier(v, "Agent name")
|
|
460
|
+
Agent._sanitize_ecs_name(v, fallback=None)
|
|
461
|
+
return v
|
|
462
|
+
|
|
463
|
+
@field_validator("namespace")
|
|
464
|
+
def validate_namespace(cls, v):
|
|
465
|
+
Agent._validate_identifier(v, "Namespace")
|
|
466
|
+
return v
|
|
467
|
+
|
|
468
|
+
@classmethod
|
|
469
|
+
def create(
|
|
470
|
+
cls, name: str, functions: list[AgentFunction] | None = None, **kwargs
|
|
471
|
+
) -> "Agent":
|
|
472
|
+
"""Create a new Agent with ECS-sanitized name and timestamp."""
|
|
473
|
+
sanitized_name = cls._sanitize_ecs_name(name)
|
|
474
|
+
return cls(
|
|
475
|
+
name=sanitized_name,
|
|
476
|
+
functions=functions or [],
|
|
477
|
+
created_at=get_now_utc(),
|
|
478
|
+
last_updated=get_now_utc(),
|
|
479
|
+
**kwargs,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def get_network_url(self, base_url: str | None = None) -> str:
|
|
483
|
+
"""Get the network URL for this agent."""
|
|
484
|
+
if base_url:
|
|
485
|
+
return f"{base_url.rstrip('/')}/{self.name}/dispatch"
|
|
486
|
+
return f"http://{self.name}.trigger/dispatch"
|
|
487
|
+
|
|
488
|
+
def handles_topic(self, topic: str) -> bool:
|
|
489
|
+
"""Check if this agent handles the given topic (supports wildcards)."""
|
|
490
|
+
# Extract all topic triggers from functions
|
|
491
|
+
for function in self.functions:
|
|
492
|
+
for trigger in function.triggers:
|
|
493
|
+
if trigger.type == "topic" and trigger.topic:
|
|
494
|
+
agent_topic = trigger.topic
|
|
495
|
+
if agent_topic.endswith("*"):
|
|
496
|
+
if topic.startswith(agent_topic[:-1]):
|
|
497
|
+
return True
|
|
498
|
+
elif agent_topic == topic:
|
|
499
|
+
return True
|
|
500
|
+
return False
|
|
501
|
+
|
|
502
|
+
@staticmethod
|
|
503
|
+
def transform_topic_schemas_to_functions(
|
|
504
|
+
topic_schemas: dict[str, dict[str, Any]],
|
|
505
|
+
) -> list[AgentFunction]:
|
|
506
|
+
"""Transform legacy topic_schemas dict to functions list.
|
|
507
|
+
|
|
508
|
+
Used during agent registration to convert SDK-provided schemas
|
|
509
|
+
into the new functions format.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
topic_schemas: Dict mapping topic names to schema metadata.
|
|
513
|
+
Each topic maps to a dict with keys: handler_name (str),
|
|
514
|
+
handler_doc (str or None), input_schema (dict),
|
|
515
|
+
output_schema (dict or None).
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
List of AgentFunction objects, one per topic.
|
|
519
|
+
"""
|
|
520
|
+
functions = []
|
|
521
|
+
for topic, schema_metadata in topic_schemas.items():
|
|
522
|
+
function = AgentFunction(
|
|
523
|
+
name=schema_metadata.get("handler_name", "handler"),
|
|
524
|
+
description=schema_metadata.get("handler_doc"),
|
|
525
|
+
input_schema=schema_metadata.get("input_schema", {}),
|
|
526
|
+
output_schema=schema_metadata.get("output_schema"),
|
|
527
|
+
triggers=[FunctionTrigger(type="topic", topic=topic)],
|
|
528
|
+
)
|
|
529
|
+
functions.append(function)
|
|
530
|
+
return functions
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _validate_identifier(
|
|
534
|
+
identifier: str, identifier_type: str = "identifier"
|
|
535
|
+
) -> None:
|
|
536
|
+
"""Validate that an identifier doesn't contain colon character.
|
|
537
|
+
|
|
538
|
+
Colons are reserved as delimiters in task queue names to enable reliable parsing.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
identifier: The string to validate (namespace, agent_name, etc.)
|
|
542
|
+
identifier_type: Description of what's being validated (for error messages)
|
|
543
|
+
|
|
544
|
+
Raises:
|
|
545
|
+
ValueError: If identifier contains a colon
|
|
546
|
+
"""
|
|
547
|
+
if ":" in identifier:
|
|
548
|
+
raise ValueError(
|
|
549
|
+
f"{identifier_type} cannot contain colon ':' character "
|
|
550
|
+
f"(reserved as task queue delimiter): {identifier}"
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
@staticmethod
|
|
554
|
+
def _sanitize_ecs_name(name: str, fallback: str | None = None) -> str:
|
|
555
|
+
"""Return a string valid for ECS names (family/service/container).
|
|
556
|
+
If fallback is None, raise exception if name is invalid.
|
|
557
|
+
Allowed characters are letters, numbers, hyphens, and underscores. Length must be 1-255.
|
|
558
|
+
Any other character is replaced with '-'. If the result is empty, use the fallback.
|
|
559
|
+
"""
|
|
560
|
+
sanitized = "".join(ch if (ch.isalnum() or ch in "-_") else "-" for ch in name)
|
|
561
|
+
sanitized = sanitized.strip("-_")
|
|
562
|
+
if sanitized:
|
|
563
|
+
return sanitized[:255]
|
|
564
|
+
elif fallback is not None:
|
|
565
|
+
return Agent._sanitize_ecs_name(
|
|
566
|
+
fallback, None
|
|
567
|
+
) # recurse to sanitize fallback
|
|
568
|
+
else:
|
|
569
|
+
raise ValueError(
|
|
570
|
+
f"Name '{name}' cannot be sanitized, and no fallback provided (or fallback also could not be sanitized)."
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def build_uid(org_id: str, namespace: str, agent_name: str) -> str:
|
|
575
|
+
"""Build composite agent UID from components.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
org_id: Organization identifier
|
|
579
|
+
namespace: Namespace identifier
|
|
580
|
+
agent_name: Agent name (ECS-sanitized)
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
Composite UID in format: org_id#namespace#agent_name
|
|
584
|
+
|
|
585
|
+
Raises:
|
|
586
|
+
ValueError: If any component contains '#' separator
|
|
587
|
+
|
|
588
|
+
Example:
|
|
589
|
+
>>> Agent.build_uid("org123", "default", "my-agent")
|
|
590
|
+
"org123#default#my-agent"
|
|
591
|
+
"""
|
|
592
|
+
if "#" in org_id or "#" in namespace or "#" in agent_name:
|
|
593
|
+
raise ValueError(
|
|
594
|
+
"Components cannot contain '#' separator. "
|
|
595
|
+
f"org_id={org_id}, namespace={namespace}, agent_name={agent_name}"
|
|
596
|
+
)
|
|
597
|
+
return f"{org_id}#{namespace}#{agent_name}"
|
|
598
|
+
|
|
599
|
+
@property
|
|
600
|
+
def uid(self) -> str:
|
|
601
|
+
"""Computed unique identifier across all orgs and namespaces.
|
|
602
|
+
|
|
603
|
+
This property is computed from org_id, namespace, and name.
|
|
604
|
+
It is NOT stored in DynamoDB - only used as the primary key.
|
|
605
|
+
|
|
606
|
+
Format: org_id#namespace#name
|
|
607
|
+
|
|
608
|
+
Example:
|
|
609
|
+
>>> agent = Agent(name="my-agent", org_id="org123", namespace="default", ...)
|
|
610
|
+
>>> agent.uid
|
|
611
|
+
"org123#default#my-agent"
|
|
612
|
+
"""
|
|
613
|
+
return Agent.build_uid(self.org_id, self.namespace, self.name)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# Router API Models for compatibility across implementations
|
|
617
|
+
class PublishEventBody(StrictBaseModel):
|
|
618
|
+
"""Request body for publishing events to any dispatch router."""
|
|
619
|
+
|
|
620
|
+
topic: str
|
|
621
|
+
sender_id: str = "web-ui"
|
|
622
|
+
payload: Any = Field(default_factory=dict)
|
|
623
|
+
trace_id: str | None = None
|
|
624
|
+
parent_id: str | None = None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class SubscriptionBody(StrictBaseModel):
|
|
628
|
+
"""Request body for agent subscription management."""
|
|
629
|
+
|
|
630
|
+
topics: list[str]
|
|
631
|
+
agent_name: str
|
|
632
|
+
functions: list[AgentFunction] | None = None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class EventRequest(StrictBaseModel):
|
|
636
|
+
"""CLI router specific event request format."""
|
|
637
|
+
|
|
638
|
+
payload: Any
|
|
639
|
+
sender_id: str | None = "router"
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class PublishResponse(StrictBaseModel):
|
|
643
|
+
"""Standard response for event publishing operations.
|
|
644
|
+
|
|
645
|
+
When publishing to a topic, returns invocation IDs for all handlers triggered.
|
|
646
|
+
Clients can poll these invocations to get results (similar to invoke() pattern).
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
message: str
|
|
650
|
+
event_uid: str
|
|
651
|
+
invocation_ids: list[str] = [] # Invocation IDs for handlers triggered
|
|
652
|
+
handler_count: int = 0 # Number of handlers triggered
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
from dispatch_agents.invocation import InvocationStatus
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class InvokeFunctionRequest(StrictBaseModel):
|
|
659
|
+
"""Request to invoke a function on an agent.
|
|
660
|
+
|
|
661
|
+
This is the payload for POST /api/unstable/namespace/{namespace}/invoke
|
|
662
|
+
Used by the SDK's invoke() function and must match backend/local router expectations.
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
agent_name: str = Field(description="Target agent name")
|
|
666
|
+
function_name: str = Field(description="Function name to invoke")
|
|
667
|
+
payload: dict[str, Any] = Field(
|
|
668
|
+
default_factory=dict, description="Input payload for the function"
|
|
669
|
+
)
|
|
670
|
+
trace_id: str | None = Field(
|
|
671
|
+
default=None, description="Optional trace ID for distributed tracing"
|
|
672
|
+
)
|
|
673
|
+
parent_id: str | None = Field(
|
|
674
|
+
default=None, description="Optional parent span ID for distributed tracing"
|
|
675
|
+
)
|
|
676
|
+
timeout_seconds: int | None = Field(
|
|
677
|
+
default=None,
|
|
678
|
+
description="Optional timeout in seconds for the invocation. Defaults to 1 hour (3600s). Maximum is 24 hours (86400s).",
|
|
679
|
+
ge=1,
|
|
680
|
+
le=86400,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
class InvocationStatusResponse(StrictBaseModel):
|
|
685
|
+
"""Response from polling an invocation status.
|
|
686
|
+
|
|
687
|
+
Returned by GET /api/unstable/namespace/{namespace}/invoke/{invocation_id}
|
|
688
|
+
Contains the current status, agent/function info, and result (when completed).
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
invocation_id: str = Field(description="Unique invocation identifier")
|
|
692
|
+
status: InvocationStatus = Field(description="Current invocation status")
|
|
693
|
+
agent_name: str = Field(description="Target agent name")
|
|
694
|
+
function_name: str = Field(description="Function name")
|
|
695
|
+
trace_id: str = Field(description="Trace ID for distributed tracing")
|
|
696
|
+
result: Any | None = Field(
|
|
697
|
+
default=None, description="Result payload (when status is COMPLETED)"
|
|
698
|
+
)
|
|
699
|
+
error: str | None = Field(
|
|
700
|
+
default=None, description="Error message (when status is ERROR)"
|
|
701
|
+
)
|
|
702
|
+
created_at: str = Field(
|
|
703
|
+
description="ISO 8601 timestamp when invocation was created"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
class SubscriptionResponse(StrictBaseModel):
|
|
708
|
+
"""Standard response for subscription management operations."""
|
|
709
|
+
|
|
710
|
+
message: str
|
|
711
|
+
topics: list[str]
|
|
712
|
+
agent_name: str
|
|
713
|
+
subscribers: dict[str, int]
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
########################################################
|
|
717
|
+
# Memory Models
|
|
718
|
+
########################################################
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class KVStoreRequest(StrictBaseModel):
|
|
722
|
+
agent_name: str
|
|
723
|
+
key: str
|
|
724
|
+
value: str = Field(default="")
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class SessionStoreRequest(StrictBaseModel):
|
|
728
|
+
agent_name: str
|
|
729
|
+
session_id: str
|
|
730
|
+
session_data: dict[str, Any] = Field(default_factory=dict)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class MemoryWriteResponse(StrictBaseModel):
|
|
734
|
+
"""Response from a memory write (add/delete) operation."""
|
|
735
|
+
|
|
736
|
+
message: str
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class KVGetResponse(StrictBaseModel):
|
|
740
|
+
"""Response from a long-term memory get operation."""
|
|
741
|
+
|
|
742
|
+
value: str | None
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class SessionGetResponse(StrictBaseModel):
|
|
746
|
+
"""Response from a short-term memory get operation."""
|
|
747
|
+
|
|
748
|
+
session_data: dict[str, Any]
|