Skip to content

Commit fa0af8d

Browse files
author
BMO (OpenClaw)
committed
feat: add configurable agent ID support
Allow users to configure which OpenClaw agent the integration communicates with, instead of always defaulting to the implicit 'main' agent. Changes: - const.py: add CONF_AGENT_ID, DEFAULT_AGENT_ID='main', ATTR_AGENT_ID - api.py: accept agent_id in constructor; _headers() now includes the x-openclaw-agent-id header on every request; async_send_message and async_stream_message accept an optional per-call agent_id override - config_flow.py: expose agent_id as a text field in both the manual setup step and the options flow (Settings → Integrations → Configure) - __init__.py: read agent_id from options/data and pass it to the API client; add optional agent_id field to the send_message service schema so automations can address a specific agent per-call - services.yaml: document the new agent_id field on send_message - strings.json / translations/en.json: add UI labels for the new option The gateway routing header x-openclaw-agent-id is always sent; when agent_id is 'main' (the default) the gateway behaviour is unchanged. A per-call override on send_message takes precedence over the config. Implemented with assistance from an AI coding agent (BMO) with human review. Bot-assisted contribution reviewed by a human before submission.
1 parent 9bcac79 commit fa0af8d

7 files changed

Lines changed: 71 additions & 6 deletions

File tree

custom_components/openclaw/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
from .api import OpenClawApiClient, OpenClawApiError
3232
from .const import (
33+
ATTR_AGENT_ID,
3334
ATTR_ATTACHMENTS,
3435
ATTR_MESSAGE,
3536
ATTR_MODEL,
@@ -47,6 +48,7 @@
4748
ATTR_ACCOUNT_ID,
4849
ATTR_TIMESTAMP,
4950
CONF_ADDON_CONFIG_PATH,
51+
CONF_AGENT_ID,
5052
CONF_GATEWAY_HOST,
5153
CONF_GATEWAY_PORT,
5254
CONF_GATEWAY_TOKEN,
@@ -63,6 +65,7 @@
6365
CONF_VOICE_PROVIDER,
6466
CONF_THINKING_TIMEOUT,
6567
CONTEXT_STRATEGY_TRUNCATE,
68+
DEFAULT_AGENT_ID,
6669
DEFAULT_CONTEXT_MAX_CHARS,
6770
DEFAULT_CONTEXT_STRATEGY,
6871
DEFAULT_ENABLE_TOOL_CALLS,
@@ -106,6 +109,7 @@
106109
vol.Required(ATTR_MESSAGE): cv.string,
107110
vol.Optional(ATTR_SESSION_ID): cv.string,
108111
vol.Optional(ATTR_ATTACHMENTS): vol.All(cv.ensure_list, [cv.string]),
112+
vol.Optional(ATTR_AGENT_ID): cv.string,
109113
}
110114
)
111115

@@ -137,13 +141,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenClawConfigEntry) ->
137141
verify_ssl = entry.data.get(CONF_VERIFY_SSL, True)
138142
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
139143

144+
# agent_id can come from options (user-changeable) or from initial config data
145+
agent_id: str = entry.options.get(
146+
CONF_AGENT_ID, entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID)
147+
)
148+
140149
client = OpenClawApiClient(
141150
host=entry.data[CONF_GATEWAY_HOST],
142151
port=entry.data[CONF_GATEWAY_PORT],
143152
token=entry.data[CONF_GATEWAY_TOKEN],
144153
use_ssl=use_ssl,
145154
verify_ssl=verify_ssl,
146155
session=session,
156+
agent_id=agent_id,
147157
)
148158

149159
coordinator = OpenClawCoordinator(hass, client)
@@ -385,6 +395,8 @@ async def handle_send_message(call: ServiceCall) -> None:
385395
"""Handle the openclaw.send_message service call."""
386396
message: str = call.data[ATTR_MESSAGE]
387397
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
398+
# Per-call agent_id overrides the client-level default when provided.
399+
call_agent_id: str | None = call.data.get(ATTR_AGENT_ID)
388400

389401
entry_data = _get_first_entry_data(hass)
390402
if not entry_data:
@@ -420,6 +432,7 @@ async def handle_send_message(call: ServiceCall) -> None:
420432
message=message,
421433
session_id=session_id,
422434
system_prompt=system_prompt,
435+
agent_id=call_agent_id,
423436
)
424437

425438
if options.get(CONF_ENABLE_TOOL_CALLS, DEFAULT_ENABLE_TOOL_CALLS):
@@ -433,6 +446,7 @@ async def handle_send_message(call: ServiceCall) -> None:
433446
),
434447
session_id=session_id,
435448
system_prompt=system_prompt,
449+
agent_id=call_agent_id,
436450
)
437451

438452
assistant_message = _extract_assistant_message(response)

custom_components/openclaw/api.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(
5151
use_ssl: bool = False,
5252
verify_ssl: bool = True,
5353
session: aiohttp.ClientSession | None = None,
54+
agent_id: str = "main",
5455
) -> None:
5556
"""Initialize the API client.
5657
@@ -61,13 +62,15 @@ def __init__(
6162
use_ssl: Use HTTPS instead of HTTP.
6263
verify_ssl: Verify SSL certificates (set False for self-signed certs).
6364
session: Optional aiohttp session (reused from HA).
65+
agent_id: Target OpenClaw agent ID (default: "main").
6466
"""
6567
self._host = host
6668
self._port = port
6769
self._token = token
6870
self._use_ssl = use_ssl
6971
self._verify_ssl = verify_ssl
7072
self._session = session
73+
self._agent_id = agent_id
7174
self._base_url = f"{'https' if use_ssl else 'http'}://{host}:{port}"
7275
# ssl=False disables cert verification for self-signed certs;
7376
# ssl=None uses default verification.
@@ -82,11 +85,18 @@ def update_token(self, token: str) -> None:
8285
"""Update the authentication token (e.g., after addon restart)."""
8386
self._token = token
8487

85-
def _headers(self) -> dict[str, str]:
86-
"""Build request headers with auth token."""
88+
def _headers(self, agent_id: str | None = None) -> dict[str, str]:
89+
"""Build request headers with auth token and agent ID.
90+
91+
Args:
92+
agent_id: Per-call agent ID override. Falls back to the
93+
client-level ``agent_id`` set in the constructor.
94+
"""
95+
effective_agent = agent_id or self._agent_id or "main"
8796
return {
8897
"Authorization": f"Bearer {self._token}",
8998
"Content-Type": "application/json",
99+
"x-openclaw-agent-id": effective_agent,
90100
}
91101

92102
async def _get_session(self) -> aiohttp.ClientSession:
@@ -182,6 +192,7 @@ async def async_send_message(
182192
model: str | None = None,
183193
system_prompt: str | None = None,
184194
stream: bool = False,
195+
agent_id: str | None = None,
185196
) -> dict[str, Any]:
186197
"""Send a chat message (non-streaming).
187198
@@ -190,6 +201,8 @@ async def async_send_message(
190201
session_id: Optional session/conversation ID.
191202
model: Optional model override.
192203
stream: If True, raises ValueError (use async_stream_message).
204+
agent_id: Optional per-call agent ID override (overrides the
205+
client-level default set in the constructor).
193206
194207
Returns:
195208
Complete chat completion response.
@@ -213,7 +226,7 @@ async def async_send_message(
213226
payload["model"] = model
214227

215228
# Pass session_id as a custom header or param if supported by gateway
216-
headers = self._headers()
229+
headers = self._headers(agent_id=agent_id)
217230
if session_id:
218231
headers["X-Session-Id"] = session_id
219232
headers["x-openclaw-session-key"] = session_id
@@ -247,6 +260,7 @@ async def async_stream_message(
247260
session_id: str | None = None,
248261
model: str | None = None,
249262
system_prompt: str | None = None,
263+
agent_id: str | None = None,
250264
) -> AsyncIterator[str]:
251265
"""Send a chat message and stream the response via SSE.
252266
@@ -256,6 +270,8 @@ async def async_stream_message(
256270
message: The user message text.
257271
session_id: Optional session/conversation ID.
258272
model: Optional model override.
273+
agent_id: Optional per-call agent ID override (overrides the
274+
client-level default set in the constructor).
259275
260276
Yields:
261277
Content delta strings from the streaming response.
@@ -275,7 +291,7 @@ async def async_stream_message(
275291
if model:
276292
payload["model"] = model
277293

278-
headers = self._headers()
294+
headers = self._headers(agent_id=agent_id)
279295
if session_id:
280296
headers["X-Session-Id"] = session_id
281297
headers["x-openclaw-session-key"] = session_id

custom_components/openclaw/config_flow.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
ADDON_SLUG,
3535
ADDON_SLUG_FRAGMENTS,
3636
CONF_ADDON_CONFIG_PATH,
37+
CONF_AGENT_ID,
3738
CONF_GATEWAY_HOST,
3839
CONF_GATEWAY_PORT,
3940
CONF_GATEWAY_TOKEN,
@@ -52,6 +53,7 @@
5253
BROWSER_VOICE_LANGUAGES,
5354
CONTEXT_STRATEGY_CLEAR,
5455
CONTEXT_STRATEGY_TRUNCATE,
56+
DEFAULT_AGENT_ID,
5557
DEFAULT_GATEWAY_HOST,
5658
DEFAULT_GATEWAY_PORT,
5759
DEFAULT_CONTEXT_MAX_CHARS,
@@ -411,6 +413,7 @@ async def async_step_manual(
411413
CONF_GATEWAY_TOKEN: token,
412414
CONF_USE_SSL: use_ssl,
413415
CONF_VERIFY_SSL: verify_ssl,
416+
CONF_AGENT_ID: user_input.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
414417
},
415418
)
416419
if "base" not in errors:
@@ -429,6 +432,7 @@ async def async_step_manual(
429432
vol.Required(CONF_GATEWAY_TOKEN): str,
430433
vol.Optional(CONF_USE_SSL, default=False): bool,
431434
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
435+
vol.Optional(CONF_AGENT_ID, default=DEFAULT_AGENT_ID): str,
432436
}
433437
),
434438
errors=errors,
@@ -453,6 +457,13 @@ async def async_step_init(
453457
selected_provider = options.get(CONF_VOICE_PROVIDER, DEFAULT_VOICE_PROVIDER)
454458

455459
schema: dict[Any, Any] = {
460+
vol.Optional(
461+
CONF_AGENT_ID,
462+
default=options.get(
463+
CONF_AGENT_ID,
464+
self._config_entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
465+
),
466+
): str,
456467
vol.Optional(
457468
CONF_INCLUDE_EXPOSED_CONTEXT,
458469
default=options.get(

custom_components/openclaw/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CONF_USE_SSL = "use_ssl"
2424
CONF_VERIFY_SSL = "verify_ssl"
2525
CONF_ADDON_CONFIG_PATH = "addon_config_path"
26+
CONF_AGENT_ID = "agent_id"
2627

2728
# Options
2829
CONF_INCLUDE_EXPOSED_CONTEXT = "include_exposed_context"
@@ -36,6 +37,7 @@
3637
CONF_BROWSER_VOICE_LANGUAGE = "browser_voice_language"
3738
CONF_THINKING_TIMEOUT = "thinking_timeout"
3839

40+
DEFAULT_AGENT_ID = "main"
3941
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
4042
DEFAULT_CONTEXT_MAX_CHARS = 13000
4143
DEFAULT_CONTEXT_STRATEGY = "truncate"
@@ -130,6 +132,7 @@
130132
ATTR_DRY_RUN = "dry_run"
131133
ATTR_MESSAGE_CHANNEL = "message_channel"
132134
ATTR_ACCOUNT_ID = "account_id"
135+
ATTR_AGENT_ID = "agent_id"
133136
ATTR_OK = "ok"
134137
ATTR_RESULT = "result"
135138
ATTR_ERROR = "error"

custom_components/openclaw/services.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ send_message:
2424
selector:
2525
text:
2626
multiline: true
27+
agent_id:
28+
name: Agent ID
29+
description: >
30+
Optional OpenClaw agent ID to route this message to. Overrides the
31+
agent ID configured in the integration options. Defaults to "main".
32+
required: false
33+
example: "main"
34+
selector:
35+
text:
2736

2837
clear_history:
2938
name: Clear History

custom_components/openclaw/strings.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"gateway_port": "Gateway Port",
1919
"gateway_token": "Gateway Token",
2020
"use_ssl": "Use SSL (HTTPS)",
21-
"verify_ssl": "Verify SSL certificate"
21+
"verify_ssl": "Verify SSL certificate",
22+
"agent_id": "Agent ID"
2223
}
2324
}
2425
},
@@ -37,6 +38,7 @@
3738
"title": "OpenClaw Options",
3839
"description": "Configure context and tool-calling behavior.",
3940
"data": {
41+
"agent_id": "Agent ID (e.g. main)",
4042
"include_exposed_context": "Include exposed entities context",
4143
"context_max_chars": "Max context characters",
4244
"context_strategy": "When context exceeds max",
@@ -124,6 +126,10 @@
124126
"attachments": {
125127
"name": "Attachments",
126128
"description": "Optional file attachments."
129+
},
130+
"agent_id": {
131+
"name": "Agent ID",
132+
"description": "Optional OpenClaw agent ID to route this message to. Overrides the configured default. Defaults to \"main\"."
127133
}
128134
}
129135
},

custom_components/openclaw/translations/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"gateway_port": "Gateway Port",
1919
"gateway_token": "Gateway Token",
2020
"use_ssl": "Use SSL (HTTPS)",
21-
"verify_ssl": "Verify SSL certificate"
21+
"verify_ssl": "Verify SSL certificate",
22+
"agent_id": "Agent ID"
2223
}
2324
}
2425
},
@@ -39,6 +40,7 @@
3940
"title": "OpenClaw Options",
4041
"description": "Configure context and tool-calling behavior.",
4142
"data": {
43+
"agent_id": "Agent ID (e.g. main)",
4244
"include_exposed_context": "Include exposed entities context",
4345
"context_max_chars": "Max context characters",
4446
"context_strategy": "When context exceeds max",
@@ -126,6 +128,10 @@
126128
"attachments": {
127129
"name": "Attachments",
128130
"description": "Optional file attachments."
131+
},
132+
"agent_id": {
133+
"name": "Agent ID",
134+
"description": "Optional OpenClaw agent ID to route this message to. Overrides the configured default. Defaults to \"main\"."
129135
}
130136
}
131137
},

0 commit comments

Comments
 (0)