hermes-agent-a2a 3.2.2__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.
- hermes_agent_a2a/__init__.py +5 -0
- hermes_agent_a2a/_mode2_worker.py +120 -0
- hermes_agent_a2a/a2a_spec/__init__.py +67 -0
- hermes_agent_a2a/a2a_spec/agent_card.py +112 -0
- hermes_agent_a2a/a2a_spec/hermes_ext.py +24 -0
- hermes_agent_a2a/a2a_spec/push.py +122 -0
- hermes_agent_a2a/a2a_spec/tasks.py +201 -0
- hermes_agent_a2a/hooks.py +344 -0
- hermes_agent_a2a/identity.py +458 -0
- hermes_agent_a2a/persistence.py +259 -0
- hermes_agent_a2a/plugin.py +139 -0
- hermes_agent_a2a/push_delivery.py +364 -0
- hermes_agent_a2a/runtime_state.py +283 -0
- hermes_agent_a2a/schemas.py +342 -0
- hermes_agent_a2a/security.py +297 -0
- hermes_agent_a2a/server.py +2206 -0
- hermes_agent_a2a/sse_handler.py +383 -0
- hermes_agent_a2a/subscription_store.py +118 -0
- hermes_agent_a2a/tool_discovery.py +5 -0
- hermes_agent_a2a/tool_handlers.py +1393 -0
- hermes_agent_a2a/tool_help.py +5 -0
- hermes_agent_a2a/tool_protocol.py +5 -0
- hermes_agent_a2a/tool_registry.py +82 -0
- hermes_agent_a2a/tool_sessions.py +5 -0
- hermes_agent_a2a/tool_workers.py +5 -0
- hermes_agent_a2a/tools.py +5 -0
- hermes_agent_a2a/validators.py +83 -0
- hermes_agent_a2a/worker_registry.py +55 -0
- hermes_agent_a2a-3.2.2.dist-info/METADATA +393 -0
- hermes_agent_a2a-3.2.2.dist-info/RECORD +31 -0
- hermes_agent_a2a-3.2.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Mode 2 ephemeral A2A worker — runs as a subprocess with the hermes-agent venv.
|
|
4
|
+
Reads params from stdin (JSON), writes result to stdout (JSON), errors to stderr.
|
|
5
|
+
|
|
6
|
+
Usage: python _mode2_worker.py < stdin > stdout
|
|
7
|
+
"""
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
MAX_STDIN_BYTES = 1 * 1024 * 1024 # 1 MB hard limit
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
raw = sys.stdin.buffer.read(MAX_STDIN_BYTES + 1)
|
|
17
|
+
if len(raw) > MAX_STDIN_BYTES:
|
|
18
|
+
print("ERROR: stdin exceeds 1MB limit", file=sys.stderr)
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
params = json.loads(raw)
|
|
21
|
+
|
|
22
|
+
agent_home = params["agent_home"]
|
|
23
|
+
message = params["message"]
|
|
24
|
+
timeout = params.get("timeout", 300)
|
|
25
|
+
|
|
26
|
+
# Resolve HERMES_HOME from params or environment — must be explicit for Mode 2.
|
|
27
|
+
# Inherit HERMES_HOME from parent environment if not passed in params.
|
|
28
|
+
_hermes_home = params.get("hermes_home") or os.environ.get("HERMES_HOME", "")
|
|
29
|
+
if not _hermes_home:
|
|
30
|
+
print("ERROR: hermes_home not set in params or HERMES_HOME env var", file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
_hermes_agent = os.path.join(_hermes_home, "hermes-agent")
|
|
33
|
+
_plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
|
34
|
+
# Move hermes-agent to front, keep plugin dir at front if present
|
|
35
|
+
new_path = [_hermes_agent]
|
|
36
|
+
for p in sys.path:
|
|
37
|
+
if p == _hermes_agent or p == _plugin_dir or p.startswith(_plugin_dir + os.sep):
|
|
38
|
+
continue
|
|
39
|
+
new_path.append(p)
|
|
40
|
+
sys.path[:] = new_path
|
|
41
|
+
|
|
42
|
+
# HERMES_HOME controls profile resolution, SOUL loading, etc.
|
|
43
|
+
os.environ["HERMES_HOME"] = agent_home
|
|
44
|
+
|
|
45
|
+
# Re-load hermes_constants so get_hermes_home() picks up the new HERMES_HOME
|
|
46
|
+
import importlib
|
|
47
|
+
import hermes_constants
|
|
48
|
+
importlib.reload(hermes_constants)
|
|
49
|
+
|
|
50
|
+
# Load target profile's .env for API keys
|
|
51
|
+
from hermes_cli.env_loader import load_hermes_dotenv
|
|
52
|
+
load_hermes_dotenv(hermes_home=agent_home)
|
|
53
|
+
|
|
54
|
+
# Read provider and model from target profile config
|
|
55
|
+
import yaml
|
|
56
|
+
profile_config_path = os.path.join(agent_home, "config.yaml")
|
|
57
|
+
cfg = {}
|
|
58
|
+
if os.path.isfile(profile_config_path):
|
|
59
|
+
with open(profile_config_path) as f:
|
|
60
|
+
cfg = yaml.safe_load(f) or {}
|
|
61
|
+
model_cfg = cfg.get("model", {}) or {}
|
|
62
|
+
target_provider = model_cfg.get("provider", "minimax-cn") if isinstance(model_cfg, dict) else "minimax-cn"
|
|
63
|
+
target_model = model_cfg.get("default", "MiniMax-M2.7") if isinstance(model_cfg, dict) else "MiniMax-M2.7"
|
|
64
|
+
|
|
65
|
+
if target_provider == "minimax-cn":
|
|
66
|
+
target_api_mode = "anthropic_messages"
|
|
67
|
+
elif target_provider == "anthropic":
|
|
68
|
+
target_api_mode = "anthropic_messages"
|
|
69
|
+
else:
|
|
70
|
+
target_api_mode = "chat_completions"
|
|
71
|
+
|
|
72
|
+
# Resolve API credentials directly from provider registry
|
|
73
|
+
from hermes_cli.auth import resolve_api_key_provider_credentials
|
|
74
|
+
creds = resolve_api_key_provider_credentials(target_provider)
|
|
75
|
+
|
|
76
|
+
# Suppress AIAgent's startup banner — it prints emoji to stdout which
|
|
77
|
+
# would corrupt the JSON response. Capture and discard; stderr is unaffected.
|
|
78
|
+
import io
|
|
79
|
+
_orig_stdout = sys.stdout
|
|
80
|
+
sys.stdout = io.StringIO()
|
|
81
|
+
try:
|
|
82
|
+
from run_agent import AIAgent
|
|
83
|
+
agent = AIAgent(
|
|
84
|
+
max_iterations=90,
|
|
85
|
+
skip_context_files=False,
|
|
86
|
+
skip_memory=True,
|
|
87
|
+
load_soul_identity=True,
|
|
88
|
+
session_id=f"a2a-m2-{uuid.uuid4().hex[:8]}",
|
|
89
|
+
model=target_model,
|
|
90
|
+
provider=target_provider,
|
|
91
|
+
api_mode=target_api_mode,
|
|
92
|
+
api_key=creds.get("api_key"),
|
|
93
|
+
base_url=creds.get("base_url"),
|
|
94
|
+
)
|
|
95
|
+
conv_result = agent.run_conversation(message)
|
|
96
|
+
finally:
|
|
97
|
+
sys.stdout = _orig_stdout
|
|
98
|
+
|
|
99
|
+
final = ""
|
|
100
|
+
if isinstance(conv_result, dict):
|
|
101
|
+
final = conv_result.get("final_response", "") or str(conv_result)
|
|
102
|
+
else:
|
|
103
|
+
final = str(conv_result)
|
|
104
|
+
|
|
105
|
+
result = {
|
|
106
|
+
"task_id": f"a2a-m2-{uuid.uuid4().hex[:8]}",
|
|
107
|
+
"state": "completed",
|
|
108
|
+
"response": final.strip(),
|
|
109
|
+
"source": f"ephemeral:{os.path.basename(agent_home)}",
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
print(json.dumps(result), flush=True)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
try:
|
|
117
|
+
main()
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
print(str(exc), file=sys.stderr)
|
|
120
|
+
sys.exit(1)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Google A2A-shaped helpers plus Hermes metadata extensions."""
|
|
2
|
+
|
|
3
|
+
from .agent_card import (
|
|
4
|
+
Provider,
|
|
5
|
+
Skill,
|
|
6
|
+
AgentCapabilities,
|
|
7
|
+
ExtendedAgentCard,
|
|
8
|
+
build_extended_agent_card,
|
|
9
|
+
skill_names,
|
|
10
|
+
validate_skill,
|
|
11
|
+
)
|
|
12
|
+
from .hermes_ext import build_hermes_metadata
|
|
13
|
+
from .tasks import (
|
|
14
|
+
TERMINAL_STATES,
|
|
15
|
+
build_task_cancel_payload,
|
|
16
|
+
build_task_get_payload,
|
|
17
|
+
build_task_send_payload,
|
|
18
|
+
extract_text_from_parts,
|
|
19
|
+
is_terminal_state,
|
|
20
|
+
parse_json_rpc_error,
|
|
21
|
+
parse_task_result,
|
|
22
|
+
)
|
|
23
|
+
from .push import (
|
|
24
|
+
AuthenticationInfo,
|
|
25
|
+
TaskPushNotificationConfig,
|
|
26
|
+
TaskPushNotificationConfigList,
|
|
27
|
+
CreateTaskPushNotificationConfigRequest,
|
|
28
|
+
CreateTaskPushNotificationConfigResponse,
|
|
29
|
+
GetTaskPushNotificationConfigRequest,
|
|
30
|
+
GetTaskPushNotificationConfigResponse,
|
|
31
|
+
ListTaskPushNotificationConfigsRequest,
|
|
32
|
+
ListTaskPushNotificationConfigsResponse,
|
|
33
|
+
DeleteTaskPushNotificationConfigRequest,
|
|
34
|
+
DeleteTaskPushNotificationConfigResponse,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"TERMINAL_STATES",
|
|
39
|
+
"build_hermes_metadata",
|
|
40
|
+
"build_task_cancel_payload",
|
|
41
|
+
"build_task_get_payload",
|
|
42
|
+
"build_task_send_payload",
|
|
43
|
+
"extract_text_from_parts",
|
|
44
|
+
"is_terminal_state",
|
|
45
|
+
"parse_json_rpc_error",
|
|
46
|
+
"parse_task_result",
|
|
47
|
+
# Agent Card models (T1-3)
|
|
48
|
+
"Provider",
|
|
49
|
+
"Skill",
|
|
50
|
+
"AgentCapabilities",
|
|
51
|
+
"ExtendedAgentCard",
|
|
52
|
+
"build_extended_agent_card",
|
|
53
|
+
"skill_names",
|
|
54
|
+
"validate_skill",
|
|
55
|
+
# Push notification models (T1-1a)
|
|
56
|
+
"AuthenticationInfo",
|
|
57
|
+
"TaskPushNotificationConfig",
|
|
58
|
+
"TaskPushNotificationConfigList",
|
|
59
|
+
"CreateTaskPushNotificationConfigRequest",
|
|
60
|
+
"CreateTaskPushNotificationConfigResponse",
|
|
61
|
+
"GetTaskPushNotificationConfigRequest",
|
|
62
|
+
"GetTaskPushNotificationConfigResponse",
|
|
63
|
+
"ListTaskPushNotificationConfigsRequest",
|
|
64
|
+
"ListTaskPushNotificationConfigsResponse",
|
|
65
|
+
"DeleteTaskPushNotificationConfigRequest",
|
|
66
|
+
"DeleteTaskPushNotificationConfigResponse",
|
|
67
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Google A2A-shaped helpers plus Hermes metadata extensions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, asdict
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# ExtendedAgentCard dataclasses
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Provider:
|
|
15
|
+
"""Google A2A ExtendedAgentCard Provider.
|
|
16
|
+
|
|
17
|
+
Per spec: organization (required), category (optional).
|
|
18
|
+
"""
|
|
19
|
+
organization: str
|
|
20
|
+
category: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Skill:
|
|
25
|
+
"""Google A2A ExtendedAgentCard Skill.
|
|
26
|
+
|
|
27
|
+
Per spec: id (required), name (required), description (optional), tags (optional).
|
|
28
|
+
"""
|
|
29
|
+
id: str
|
|
30
|
+
name: str
|
|
31
|
+
description: Optional[str] = None
|
|
32
|
+
tags: Optional[List[str]] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class AgentCapabilities:
|
|
37
|
+
"""Google A2A ExtendedAgentCard AgentCapabilities.
|
|
38
|
+
|
|
39
|
+
Per spec: streaming, pushNotifications, stateTransitionHistory — all bool, default False.
|
|
40
|
+
"""
|
|
41
|
+
streaming: bool = False
|
|
42
|
+
pushNotifications: bool = False
|
|
43
|
+
stateTransitionHistory: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ExtendedAgentCard:
|
|
48
|
+
"""Google A2A ExtendedAgentCard.
|
|
49
|
+
|
|
50
|
+
Combines standard AgentCard fields with ExtendedAgentCard-specific fields.
|
|
51
|
+
"""
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
provider: Provider
|
|
55
|
+
agentCapabilities: AgentCapabilities
|
|
56
|
+
defaultInputModes: List[str]
|
|
57
|
+
defaultOutputModes: List[str]
|
|
58
|
+
url: Optional[str] = None
|
|
59
|
+
version: Optional[str] = None
|
|
60
|
+
documentationUrl: Optional[str] = None
|
|
61
|
+
skills: Optional[List[Skill]] = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_extended_agent_card(overrides: Optional[dict] = None) -> dict:
|
|
65
|
+
"""Build a full ExtendedAgentCard dict.
|
|
66
|
+
|
|
67
|
+
Starts with base AgentCard fields and adds ExtendedAgentCard fields.
|
|
68
|
+
Merges any overrides from the argument.
|
|
69
|
+
|
|
70
|
+
Default provider: organization="Hermes Fleet", category="official"
|
|
71
|
+
"""
|
|
72
|
+
card = {
|
|
73
|
+
"name": "hermes-agent",
|
|
74
|
+
"description": "Hermes fleet agent with A2A HTTP/JSON-RPC protocol support — exposes A2A server, HMAC auth, push notifications, SSE streaming, and Telegram session routing.",
|
|
75
|
+
"url": None,
|
|
76
|
+
"version": None,
|
|
77
|
+
"documentationUrl": None,
|
|
78
|
+
"provider": asdict(Provider(organization="Hermes Fleet", category="official")),
|
|
79
|
+
"agentCapabilities": asdict(AgentCapabilities()),
|
|
80
|
+
"defaultInputModes": ["text"],
|
|
81
|
+
"defaultOutputModes": ["text"],
|
|
82
|
+
}
|
|
83
|
+
if overrides:
|
|
84
|
+
for key, value in overrides.items():
|
|
85
|
+
if key in ("provider", "agentCapabilities") and isinstance(value, dict):
|
|
86
|
+
card[key] = {**card[key], **value}
|
|
87
|
+
else:
|
|
88
|
+
card[key] = value
|
|
89
|
+
return card
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Legacy skill helpers (pre-existing)
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def skill_names(agent_info: dict) -> set[str]:
|
|
98
|
+
if not isinstance(agent_info, dict):
|
|
99
|
+
return set()
|
|
100
|
+
known_skills = agent_info.get("metadata", {}).get("skills", []) or agent_info.get("skills", [])
|
|
101
|
+
return {
|
|
102
|
+
str(item.get("name") or item.get("id") or "").lower()
|
|
103
|
+
for item in known_skills
|
|
104
|
+
if isinstance(item, dict) and (item.get("name") or item.get("id"))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def validate_skill(agent_info: dict, skill: str) -> tuple[bool, list[str]]:
|
|
109
|
+
names = skill_names(agent_info)
|
|
110
|
+
if not skill or not names:
|
|
111
|
+
return True, sorted(names)
|
|
112
|
+
return skill.lower() in names, sorted(names)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Hermes metadata extension for Google A2A-shaped task envelopes."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_hermes_metadata(
|
|
7
|
+
route: str = "protocol",
|
|
8
|
+
execution: str = "remote_a2a",
|
|
9
|
+
delivery: Optional[str] = None,
|
|
10
|
+
reply_mode: Optional[str] = None,
|
|
11
|
+
isolation: Optional[str] = None,
|
|
12
|
+
) -> dict:
|
|
13
|
+
metadata = {
|
|
14
|
+
"version": "1",
|
|
15
|
+
"route": route,
|
|
16
|
+
"execution": execution,
|
|
17
|
+
}
|
|
18
|
+
if delivery:
|
|
19
|
+
metadata["delivery"] = delivery
|
|
20
|
+
if reply_mode:
|
|
21
|
+
metadata["reply_mode"] = reply_mode
|
|
22
|
+
if isolation:
|
|
23
|
+
metadata["isolation"] = isolation
|
|
24
|
+
return metadata
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Google A2A Task Push Notification models — T1-1a.
|
|
2
|
+
|
|
3
|
+
Spec reference: Google A2A proto3 spec, tasks/pushNotification methods.
|
|
4
|
+
"""
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class AuthenticationInfo:
|
|
11
|
+
"""Embedded auth info within TaskPushNotificationConfig.
|
|
12
|
+
|
|
13
|
+
Per spec: auth_type (string), auth_code (string, optional).
|
|
14
|
+
All fields are optional.
|
|
15
|
+
"""
|
|
16
|
+
auth_type: Optional[str] = None
|
|
17
|
+
auth_code: Optional[str] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TaskPushNotificationConfig:
|
|
22
|
+
"""Per spec: a registered push notification config for a task.
|
|
23
|
+
|
|
24
|
+
Fields: id, task_id, push_transport_type, endpoint, authentication (opt), metadata (opt).
|
|
25
|
+
push_transport_type values: "webhook", "gcm", etc.
|
|
26
|
+
"""
|
|
27
|
+
id: str
|
|
28
|
+
task_id: str
|
|
29
|
+
push_transport_type: str
|
|
30
|
+
endpoint: str
|
|
31
|
+
authentication: Optional[AuthenticationInfo] = None
|
|
32
|
+
metadata: Optional[dict] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TaskPushNotificationConfigList:
|
|
37
|
+
"""Wrapper for list responses.
|
|
38
|
+
|
|
39
|
+
Fields: items (list), has_more (bool).
|
|
40
|
+
"""
|
|
41
|
+
items: List[TaskPushNotificationConfig] = field(default_factory=list)
|
|
42
|
+
has_more: bool = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class CreateTaskPushNotificationConfigRequest:
|
|
47
|
+
"""Create a push notification config for a task.
|
|
48
|
+
|
|
49
|
+
Fields: id, task_id, push_transport_type, endpoint, authentication (opt), metadata (opt).
|
|
50
|
+
"""
|
|
51
|
+
id: str
|
|
52
|
+
task_id: str
|
|
53
|
+
push_transport_type: str
|
|
54
|
+
endpoint: str
|
|
55
|
+
authentication: Optional[AuthenticationInfo] = None
|
|
56
|
+
metadata: Optional[dict] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class CreateTaskPushNotificationConfigResponse:
|
|
61
|
+
"""Response after creating a push notification config.
|
|
62
|
+
|
|
63
|
+
Fields: config (TaskPushNotificationConfig).
|
|
64
|
+
"""
|
|
65
|
+
config: TaskPushNotificationConfig
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class GetTaskPushNotificationConfigRequest:
|
|
70
|
+
"""Request to retrieve a single push notification config.
|
|
71
|
+
|
|
72
|
+
Fields: task_id, config_id.
|
|
73
|
+
"""
|
|
74
|
+
task_id: str
|
|
75
|
+
config_id: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class GetTaskPushNotificationConfigResponse:
|
|
80
|
+
"""Response with a single push notification config.
|
|
81
|
+
|
|
82
|
+
Fields: config (TaskPushNotificationConfig).
|
|
83
|
+
"""
|
|
84
|
+
config: TaskPushNotificationConfig
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ListTaskPushNotificationConfigsRequest:
|
|
89
|
+
"""Request to list all push notification configs for a task.
|
|
90
|
+
|
|
91
|
+
Fields: task_id.
|
|
92
|
+
"""
|
|
93
|
+
task_id: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class ListTaskPushNotificationConfigsResponse:
|
|
98
|
+
"""Paginated list of push notification configs for a task.
|
|
99
|
+
|
|
100
|
+
Fields: items, has_more.
|
|
101
|
+
"""
|
|
102
|
+
items: List[TaskPushNotificationConfig] = field(default_factory=list)
|
|
103
|
+
has_more: bool = False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class DeleteTaskPushNotificationConfigRequest:
|
|
108
|
+
"""Request to delete a push notification config.
|
|
109
|
+
|
|
110
|
+
Fields: task_id, config_id.
|
|
111
|
+
"""
|
|
112
|
+
task_id: str
|
|
113
|
+
config_id: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class DeleteTaskPushNotificationConfigResponse:
|
|
118
|
+
"""Response after deleting a push notification config.
|
|
119
|
+
|
|
120
|
+
Fields: config_id.
|
|
121
|
+
"""
|
|
122
|
+
config_id: str
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Google A2A-shaped task payload builders and result parsers.
|
|
2
|
+
|
|
3
|
+
Google A2A v1.0 error codes:
|
|
4
|
+
-32700 Parse error
|
|
5
|
+
-32600 Invalid Request
|
|
6
|
+
-32603 Internal error
|
|
7
|
+
-38000 Task not found
|
|
8
|
+
-38001 Task not cancelable
|
|
9
|
+
-38002 Push notification not supported
|
|
10
|
+
-38003 Invalid state transition
|
|
11
|
+
-38004 Non-idempotent task
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
# A2A-spec-compliant error codes
|
|
18
|
+
A2A_ERR_PARSE = -32700
|
|
19
|
+
A2A_ERR_INVALID_REQUEST = -32600
|
|
20
|
+
A2A_ERR_INTERNAL = -32603
|
|
21
|
+
A2A_ERR_TASK_NOT_FOUND = -38000
|
|
22
|
+
A2A_ERR_TASK_NOT_CANCELABLE = -38001
|
|
23
|
+
A2A_ERR_PUSH_NOT_SUPPORTED = -38002
|
|
24
|
+
A2A_ERR_INVALID_STATE_TRANSITION = -38003
|
|
25
|
+
A2A_ERR_NON_IDEMPOTENT = -38004
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TaskArtifactUpdateEvent:
|
|
32
|
+
"""A task artifact update event.
|
|
33
|
+
|
|
34
|
+
Emitted over SSE when a task generates an artifact (partial or final).
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
context_id: REQUIRED — the context ID for this task.
|
|
38
|
+
task_id: REQUIRED — the task this artifact belongs to.
|
|
39
|
+
artifact: REQUIRED — the artifact data dict (A2A artifact shape).
|
|
40
|
+
metadata: optional — additional event metadata.
|
|
41
|
+
"""
|
|
42
|
+
context_id: str
|
|
43
|
+
task_id: str
|
|
44
|
+
artifact: dict
|
|
45
|
+
metadata: Optional[dict] = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict:
|
|
48
|
+
"""Render the event as a plain dict for SSE transport."""
|
|
49
|
+
return {
|
|
50
|
+
"kind": "artifact",
|
|
51
|
+
"contextId": self.context_id,
|
|
52
|
+
"taskId": self.task_id,
|
|
53
|
+
"artifact": self.artifact,
|
|
54
|
+
"metadata": self.metadata or {},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
import enum
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TaskState(str, enum.Enum):
|
|
62
|
+
"""Canonical task states per Google A2A v1.0 spec."""
|
|
63
|
+
|
|
64
|
+
SUBMITTED = "submitted"
|
|
65
|
+
WORKING = "working"
|
|
66
|
+
INPUT_REQUIRED = "input_required"
|
|
67
|
+
AUTH_REQUIRED = "auth_required"
|
|
68
|
+
COMPLETED = "completed"
|
|
69
|
+
FAILED = "failed"
|
|
70
|
+
CANCELED = "canceled"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
TERMINAL_STATES = {"completed", "failed", "canceled"}
|
|
74
|
+
ACTIVE_STATES = {"submitted", "working", "input_required", "auth_required"}
|
|
75
|
+
AUTH_STATES = {"auth_required", "authenticated", "rejected"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_terminal_state(state: str) -> bool:
|
|
79
|
+
return str(state or "").lower() in TERMINAL_STATES
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_task_send_payload(
|
|
83
|
+
task_id: str,
|
|
84
|
+
message: str,
|
|
85
|
+
sender_name: str,
|
|
86
|
+
intent: str = "consultation",
|
|
87
|
+
expected_action: str = "reply",
|
|
88
|
+
skill: Optional[str] = None,
|
|
89
|
+
hermes: Optional[dict] = None,
|
|
90
|
+
request_id: Optional[str] = None,
|
|
91
|
+
) -> dict:
|
|
92
|
+
metadata = {
|
|
93
|
+
"intent": intent,
|
|
94
|
+
"expected_action": expected_action,
|
|
95
|
+
"context_scope": "full",
|
|
96
|
+
"sender_name": sender_name,
|
|
97
|
+
}
|
|
98
|
+
if skill:
|
|
99
|
+
metadata["skill"] = skill
|
|
100
|
+
if hermes:
|
|
101
|
+
metadata["hermes"] = hermes
|
|
102
|
+
|
|
103
|
+
payload = {
|
|
104
|
+
"jsonrpc": "2.0",
|
|
105
|
+
"id": request_id or str(uuid.uuid4()),
|
|
106
|
+
"method": "tasks/send",
|
|
107
|
+
"params": {
|
|
108
|
+
"id": task_id,
|
|
109
|
+
"message": {
|
|
110
|
+
"message_id": str(uuid.uuid4()),
|
|
111
|
+
"role": "user",
|
|
112
|
+
"parts": [{"type": "text", "text": message}],
|
|
113
|
+
"metadata": metadata,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
return payload
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_task_get_payload(task_id: str, request_id: Optional[str] = None) -> dict:
|
|
121
|
+
return {
|
|
122
|
+
"jsonrpc": "2.0",
|
|
123
|
+
"id": request_id or str(uuid.uuid4()),
|
|
124
|
+
"method": "tasks/get",
|
|
125
|
+
"params": {"id": task_id},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_task_cancel_payload(task_id: str, request_id: Optional[str] = None) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"jsonrpc": "2.0",
|
|
132
|
+
"id": request_id or str(uuid.uuid4()),
|
|
133
|
+
"method": "tasks/cancel",
|
|
134
|
+
"params": {"id": task_id},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def extract_text_from_parts(parts) -> str:
|
|
139
|
+
chunks = []
|
|
140
|
+
for part in parts or []:
|
|
141
|
+
if not isinstance(part, dict):
|
|
142
|
+
continue
|
|
143
|
+
if part.get("type") == "text":
|
|
144
|
+
text = part.get("text", "")
|
|
145
|
+
if text:
|
|
146
|
+
chunks.append(str(text))
|
|
147
|
+
elif isinstance(part.get("text"), str):
|
|
148
|
+
chunks.append(part["text"])
|
|
149
|
+
return "\n".join(chunks).strip()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_json_rpc_error(response: dict) -> str:
|
|
153
|
+
rpc_error = response.get("error") if isinstance(response, dict) else None
|
|
154
|
+
if not rpc_error:
|
|
155
|
+
return ""
|
|
156
|
+
if isinstance(rpc_error, dict):
|
|
157
|
+
return rpc_error.get("message") or str(rpc_error)
|
|
158
|
+
return str(rpc_error)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def build_error_response(code: int, message: str, data=None, id=None) -> dict:
|
|
162
|
+
"""Build a spec-compliant JSON-RPC error response: {jsonrpc, code, message, data, id}."""
|
|
163
|
+
err = {"jsonrpc": "2.0", "code": code, "message": message}
|
|
164
|
+
if data is not None:
|
|
165
|
+
err["data"] = data
|
|
166
|
+
if id is not None:
|
|
167
|
+
err["id"] = id
|
|
168
|
+
return err
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_task_result(rpc_result: dict, default_task_id: str = "") -> dict:
|
|
172
|
+
rpc_result = rpc_result or {}
|
|
173
|
+
status = rpc_result.get("status", {}) if isinstance(rpc_result, dict) else {}
|
|
174
|
+
state = status.get("state", "unknown") if isinstance(status, dict) else "unknown"
|
|
175
|
+
task_id = rpc_result.get("id", default_task_id) if isinstance(rpc_result, dict) else default_task_id
|
|
176
|
+
chunks = []
|
|
177
|
+
|
|
178
|
+
for artifact in rpc_result.get("artifacts", []) or []:
|
|
179
|
+
if isinstance(artifact, dict):
|
|
180
|
+
text = extract_text_from_parts(artifact.get("parts", []))
|
|
181
|
+
if text:
|
|
182
|
+
chunks.append(text)
|
|
183
|
+
|
|
184
|
+
status_message = status.get("message", {}) if isinstance(status, dict) else {}
|
|
185
|
+
if isinstance(status_message, dict):
|
|
186
|
+
text = extract_text_from_parts(status_message.get("parts", []))
|
|
187
|
+
if text:
|
|
188
|
+
chunks.append(text)
|
|
189
|
+
|
|
190
|
+
direct_message = rpc_result.get("message", {}) if isinstance(rpc_result, dict) else {}
|
|
191
|
+
if isinstance(direct_message, dict):
|
|
192
|
+
text = extract_text_from_parts(direct_message.get("parts", []))
|
|
193
|
+
if text:
|
|
194
|
+
chunks.append(text)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"task_id": task_id,
|
|
198
|
+
"state": state,
|
|
199
|
+
"response": "\n".join(chunks).strip(),
|
|
200
|
+
"raw_result": rpc_result,
|
|
201
|
+
}
|