Skip to content

Commit 89b07ef

Browse files
authored
feat: MCP server enhancements + dimos mcp CLI + agent context (DIM-686, DIM-687) (#1451)
* feat(cli): add daemon mode for dimos run (DIM-681) * fix: address greptile review — fd leak, wrong PID, fabricated log path - close devnull/stderr_file after dup2 (fd leak) - remove PID from pre-fork output (printed parent PID, not daemon PID) - show log_dir not log_dir/dimos.jsonl (file doesn't exist yet) - keep tests in tests/ (dimos/conftest.py breaks isolated tests) * feat(cli): add dimos stop and dimos status commands (DIM-682, DIM-684) dimos status — shows running instances with run-id, pid, blueprint, uptime, log dir dimos stop — sends SIGTERM (or SIGKILL with --force), waits 5s, escalates if needed --pid to target specific instance, --all to stop everything cleans registry on stop also adds get_most_recent() to run_registry. 8 new tests covering sigterm, sigkill, escalation, dead process cleanup, most-recent lookup. * test: add e2e daemon lifecycle tests with PingPong blueprint lightweight 2-module blueprint (PingModule + PongModule) that needs no hardware, no LFS data, and no replay files. tests real forkserver workers and module deployment. covers: - single worker lifecycle (build -> health check -> registry -> stop) - multiple workers (2 workers, both alive) - health check detects dead worker (SIGKILL -> detect failure) - registry entry JSON field roundtrip - stale entry cleanup (dead PIDs removed, live entries kept) * fix: rename stderr.log to daemon.log (addresses greptile review) both stdout and stderr redirect to the same file after daemonize(), so stderr.log was misleading. daemon.log better describes the contents. * fix: resolve mypy type errors in stop command (DIM-681) * feat: per-run log directory with unified main.jsonl (DIM-685) folds DIM-685 into daemon PR to avoid merge conflicts on dimos.py. - set_run_log_dir() before blueprint.build() routes structlog to <log_base>/<run-id>/main.jsonl - workers inherit DIMOS_RUN_LOG_DIR env var via forkserver - FileHandler replaces RotatingFileHandler (multi-process rotation unsafe) - fallback: env var check -> legacy per-pid files - 6 unit tests for routing logic * fix: migrate existing FileHandlers when set_run_log_dir is called setup_logger() runs at import time throughout the codebase, creating FileHandlers pointing to the legacy log path. set_run_log_dir() was resetting the global path but not updating these existing handlers, so main.jsonl was created but stayed empty (0 bytes). fix: iterate all stdlib loggers and redirect their FileHandlers to the new per-run path. verified: main.jsonl now receives structured JSON logs (1050 bytes, 5 lines in test run). * chore: move daemon tests to dimos/core/ for CI discovery testpaths in pyproject.toml is ['dimos'], so tests/ at repo root wouldn't be picked up by CI. moved all 3 test files to dimos/core/ alongside existing core tests. all 41 tests pass with conftest active. * chore: mark e2e daemon tests as slow matches convention from test_worker.py — forkserver-based tests are marked slow so they run in CI but are skipped in local default pytest. local default: 36 tests (unit only) CI (-m 'not (tool or mujoco)'): 41 tests (unit + e2e) * test: add CLI integration tests for dimos stop and dimos status (DIM-682, DIM-684) 16 tests using typer CliRunner with real subprocess PIDs: status (7 tests): - no instances, running instance details, uptime formatting - MCP port, multiple instances, dead PID filtering stop (9 tests): - default most recent, --pid, --pid not found - --all, --all empty, --force SIGKILL - already-dead cleanup, SIGTERM verification * test: add e2e CLI tests against real running blueprint (DIM-682, DIM-684) 3 new e2e tests that exercise dimos status and stop against a live PingPong blueprint with real forkserver workers: - status shows live blueprint details (run_id, PID, blueprint name) - registry entry findable after real build, workers alive - coord.stop() kills workers, registry cleaned * fix: address paul's review comments - move system imports to top of run(), logger.info before heavy imports - remove hardcoded MCP port line from daemon output - add n_workers/n_modules properties to ModuleCoordinator (public API) - single-instance model: remove --all/--pid from stop, simplify status - use _get_user_data_dir() for XDG-compliant registry/log paths - remove mcp_port from RunEntry (should be in GlobalConfig) - inline _shutdown_handler as closure in install_signal_handlers - add SIGKILL wait poll (2s) to avoid zombie race with port conflict check - replace handler._open() private API with new FileHandler construction - fix e2e _clean_registry to monkeypatch REGISTRY_DIR - reset logging_config module globals in test fixture - move test imports to module level * fix: drop daemon.log, redirect all stdio to /dev/null structlog FileHandler writes to main.jsonl — daemon.log only ever captured signal shutdown messages. no useful content. * fix: restore LOG_BASE_DIR import, remove duplicate set_run_log_dir import fixes mypy name-defined error from import reorganization * fix: address remaining paul review comments - simplify health_check to single alive check (build is synchronous) - remove --health-timeout flag (no longer polling) - add workers property to ModuleCoordinator (public API) - separate try-excepts for kill/wait in sleeper fixture cleanup - move repeated imports to top in test_per_run_logs * fix: address all remaining paul review comments - convert all test helpers to fixtures with cleanup - replace os.environ['CI']='1' at import time with monkeypatch fixture - replace all time.sleep() with polling loops in tests - mark slow stop tests @pytest.mark.slow - move stop_entry logic from CLI to run_registry - remove # type: ignore, properly type _stop_entry - remove duplicate port conflict test - remove redundant monkeypatch cleanup in test_per_run_logs - list(glob()) to avoid mutation during iteration in cleanup_stale - XDG_STATE_HOME compliant _get_state_dir() for registry/log paths - remove redundant cli_config_overrides in e2e builds - move duplicate imports to module level in e2e tests * fix: remove module docstring from test_daemon.py * feat: MCP server enhancements, dimos mcp CLI, agent context, stress tests (DIM-686, DIM-687) MCP server: - new dimos/status and dimos/list_modules JSON-RPC methods CLI: - dimos mcp list-tools/call/status/modules commands - uses stdlib urllib (no requests dependency) agent context (DIM-687): - generate_context() writes context.md on dimos run stress tests (23 tests): - MCP lifecycle, tools, error handling, rapid calls, restart cycles - all tests use fixtures with cleanup, poll loops (no sleep) Closes DIM-686, Closes DIM-687 * fix: address greptile review on PR #1451 - remove dimos restart from agent context (not in this branch yet) - handle JSON-RPC errors in dimos mcp call (show error, exit 1) - pass skills as parameter to dimos/status and dimos/list_modules handlers - fix hardcoded port in curl example (use mcp_port parameter) - fix double stop() in test_mcp_dead_after_stop (standalone coordinator) - use tmp_path for log_dir in mcp_entry fixture (test isolation) * feat: dimos agent-send CLI + MCP method - dimos/agent_send MCP method publishes on /human_input LCM channel - dimos agent-send 'message' CLI wraps the MCP call - 4 new tests: MCP send, empty message, CLI send, no-server * fix: address greptile review round 2 - escape f-string curly braces in curl example (agent_context.py) - fix double stop() in test_registry_cleanup_after_stop - add JSON-RPC error handling to all MCP CLI commands - add type annotation for LCM transport - add agent-send to generated context.md CLI commands * feat: module IO introspection via MCP + CLI - dimos/module_io MCP method: skills grouped by module with schema - dimos mcp module-io CLI: human-readable module/skill listing - 2 new tests * fix: daemon context generation + standalone e2e stress tests Bug fixes found during e2e testing: - worker.pid: catch AssertionError after daemonize() when is_alive() asserts _parent_pid == os.getpid() (double-fork changes PID, but stored process.pid is still valid) - agent_context: fix f-string ValueError from triple braces in curl example — split into f-string + plain string concat New standalone test scripts (no pytest): - e2e_mcp_killtest.py: SIGKILL/restart stress test (3 cycles, verifies MCP recovers after crash, tests all endpoints) - e2e_devex_test.py: full developer experience test simulating OpenClaw agent workflow (daemon start → CLI ops → logs → stop) Register stress-test blueprint in all_blueprints.py for `dimos run stress-test --daemon` support. * refactor: strip module-io, fix greptile review issues 7-13 - Remove dimos/module_io handler + 'dimos mcp module-io' CLI command (showed skill descriptions, not actual IO — misleading) - Remove unused rpc_calls param from _handle_dimos_agent_send - Add JSON-RPC error checking to mcp_list_tools, mcp_status, mcp_modules - Fix empty tool content: exit 0 with '(no output)' instead of exit 1 - Add Content-Type header to curl example in agent context - Fix double-stop in test_registry_cleanup_after_stop - Remove module-io references from standalone test scripts * cleanup: remove agent_context.py, fix final greptile nits - Delete agent_context.py entirely — runtime info is available via `dimos status` and `dimos mcp status`, no need for a separate file - Remove generate_context() calls from CLI (daemon + foreground paths) - Fix non-deterministic module list in dimos/status (set → dict.fromkeys) - Remove unused heartbeat: Out[str] from StressTestModule - Remove dead list literal from e2e_mcp_killtest.py * fix: address latest greptile review round - Remove stale module-io step from e2e_devex_test (command was removed) - Fix step numbering in devex test (7-10 sequential, no gaps) - Fix double remove() on registry entry — let fixture handle cleanup - Remove misleading 'non-default to avoid conflicts' port comment * fix: resolve mypy errors in worker.py and stress_test_module - worker.py: use typed local variable for process.pid instead of direct return (fixes no-any-return) - stress_test_module.py: add return type annotation to start() * perf: class-scoped MCP fixtures, 125s → 51s test runtime - Make mcp_shared fixture class-scoped: blueprint starts once per class instead of once per test (~4s setup saved per test) - Move no-server tests (dead_after_stop, cli_no_server_error, agent_send_cli_no_server) to dedicated TestMCPNoServer class to avoid port conflict with shared fixture - Add full type annotations to all fixtures and helpers - Add docstring explaining performance rationale - Remove unused mcp_blueprint fixture (replaced by mcp_shared) * fix: resolve remaining CI failures (mypy + all_blueprints) - Fix mypy errors in standalone test scripts (e2e_devex_test.py, e2e_mcp_killtest.py): typed CompletedProcess, multiprocessing.Event, dict params, assert pid not None before os.kill - Regenerate all_blueprints.py (stress-test entry now alphabetically sorted) * refactor: McpAdapter class + convert custom methods to @Skill tools Address Paul review comments on PR #1451: - New McpAdapter class replacing 3 duplicated _mcp_call implementations - Convert dimos/status, list_modules, agent_send to @Skill on McpServer - CLI thin wrappers over McpAdapter, added --json-args flag - worker.py: os.kill(pid, 0) for pid check - Renamed test files (demo_ prefix for non-CI, integration for pytest) - transport.start()/stop(), removed skill_count, requests in pyproject 29/29 tests pass locally (41s). * fix: alphabetical order in all_blueprints.py for demo-mcp-stress-test * fix: catch HTTPError in McpAdapter, guard None pid in Worker - McpAdapter.call(): catch requests.HTTPError and re-raise as McpError so CLI callers get clean error messages instead of raw tracebacks - Worker.pid: check for None before os.kill() — unstarted processes have pid=None which would raise TypeError (not caught by OSError) * fix: server_status returns main process PID, not worker PID McpServer runs in a forkserver worker, so os.getpid() returns the worker PID. Read from RunEntry instead to get the main daemon PID that dimos stop/status actually needs. * refactor: use click.ParamType for --arg parsing in mcp call Replace manual string splitting with _KeyValueType click.ParamType per Paul's review suggestion. Validation and JSON coercion now handled by click's type system instead of inline loop. * fix: viewer_backend → viewer rename + KeyValueType test fix - Update test_mcp_integration.py and demo_mcp_killtest.py to use 'viewer' instead of 'viewer_backend' (renamed in #1477) - Fix test_cli_call_tool_wrong_arg_format: exit code 2 (click ParamType validation) instead of 1, check KEY=VALUE in output - Merge dev to pick up #1477 rename and #1494 replay loop * fix: mypy arg-type error for KeyValueType dict(args)
1 parent b9eaf0f commit 89b07ef

12 files changed

Lines changed: 1551 additions & 19 deletions

dimos/agents/mcp/mcp_adapter.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# Copyright 2025-2026 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Lightweight MCP JSON-RPC client adapter.
16+
17+
``McpAdapter`` provides a typed Python interface to a running MCP server.
18+
It is used by:
19+
20+
* The ``dimos mcp`` CLI commands
21+
* Integration / e2e tests
22+
* Any code that needs to talk to a local MCP server
23+
24+
Usage::
25+
26+
adapter = McpAdapter("http://localhost:9990/mcp")
27+
adapter.wait_for_ready(timeout=10)
28+
tools = adapter.list_tools()
29+
result = adapter.call_tool("echo", {"message": "hi"})
30+
"""
31+
32+
from __future__ import annotations
33+
34+
import time
35+
from typing import Any
36+
import uuid
37+
38+
import requests
39+
40+
from dimos.utils.logging_config import setup_logger
41+
42+
logger = setup_logger()
43+
44+
DEFAULT_TIMEOUT = 30
45+
46+
47+
class McpError(Exception):
48+
"""Raised when the MCP server returns a JSON-RPC error."""
49+
50+
def __init__(self, message: str, code: int | None = None) -> None:
51+
self.code = code
52+
super().__init__(message)
53+
54+
55+
class McpAdapter:
56+
"""Thin JSON-RPC client for a running MCP server."""
57+
58+
def __init__(self, url: str | None = None, timeout: int = DEFAULT_TIMEOUT) -> None:
59+
if url is None:
60+
from dimos.core.global_config import global_config
61+
62+
url = f"http://localhost:{global_config.mcp_port}/mcp"
63+
self.url = url
64+
self.timeout = timeout
65+
66+
# ------------------------------------------------------------------
67+
# Low-level JSON-RPC
68+
# ------------------------------------------------------------------
69+
70+
def call(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
71+
"""Send a JSON-RPC request and return the parsed response.
72+
73+
Raises ``requests.ConnectionError`` if the server is unreachable.
74+
"""
75+
payload: dict[str, Any] = {
76+
"jsonrpc": "2.0",
77+
"id": str(uuid.uuid4()),
78+
"method": method,
79+
}
80+
if params:
81+
payload["params"] = params
82+
83+
resp = requests.post(self.url, json=payload, timeout=self.timeout)
84+
try:
85+
resp.raise_for_status()
86+
except requests.HTTPError as e:
87+
raise McpError(f"HTTP {resp.status_code}: {e}") from e
88+
return resp.json() # type: ignore[no-any-return]
89+
90+
# ------------------------------------------------------------------
91+
# MCP standard methods
92+
# ------------------------------------------------------------------
93+
94+
def initialize(self) -> dict[str, Any]:
95+
"""Send ``initialize`` and return server info."""
96+
return self.call("initialize")
97+
98+
def list_tools(self) -> list[dict[str, Any]]:
99+
"""Return the list of available tools."""
100+
result = self._unwrap(self.call("tools/list"))
101+
return result.get("tools", []) # type: ignore[no-any-return]
102+
103+
def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
104+
"""Call a tool by name and return the result dict."""
105+
return self._unwrap(self.call("tools/call", {"name": name, "arguments": arguments or {}}))
106+
107+
def call_tool_text(self, name: str, arguments: dict[str, Any] | None = None) -> str:
108+
"""Call a tool and return just the first text content item."""
109+
result = self.call_tool(name, arguments)
110+
content = result.get("content", [])
111+
if not content:
112+
return ""
113+
return content[0].get("text", str(content[0])) # type: ignore[no-any-return]
114+
115+
# ------------------------------------------------------------------
116+
# Readiness probes
117+
# ------------------------------------------------------------------
118+
119+
def wait_for_ready(self, timeout: float = 10.0, interval: float = 0.5) -> bool:
120+
"""Poll until the MCP server responds, or return False on timeout."""
121+
deadline = time.monotonic() + timeout
122+
while time.monotonic() < deadline:
123+
try:
124+
resp = requests.post(
125+
self.url,
126+
json={"jsonrpc": "2.0", "id": "probe", "method": "initialize"},
127+
timeout=2,
128+
)
129+
if resp.status_code == 200:
130+
return True
131+
except requests.ConnectionError:
132+
pass
133+
time.sleep(interval)
134+
return False
135+
136+
def wait_for_down(self, timeout: float = 10.0, interval: float = 0.5) -> bool:
137+
"""Poll until the MCP server stops responding."""
138+
deadline = time.monotonic() + timeout
139+
while time.monotonic() < deadline:
140+
try:
141+
requests.post(
142+
self.url,
143+
json={"jsonrpc": "2.0", "id": "probe", "method": "initialize"},
144+
timeout=1,
145+
)
146+
except (requests.ConnectionError, requests.ReadTimeout):
147+
return True
148+
time.sleep(interval)
149+
return False
150+
151+
# ------------------------------------------------------------------
152+
# Class methods for discovery
153+
# ------------------------------------------------------------------
154+
155+
@classmethod
156+
def from_run_entry(cls, entry: Any | None = None, timeout: int = DEFAULT_TIMEOUT) -> McpAdapter:
157+
"""Create an adapter from a RunEntry, or discover the latest one.
158+
159+
Falls back to the default URL if no entry is found.
160+
"""
161+
if entry is None:
162+
from dimos.core.run_registry import list_runs
163+
164+
runs = list_runs(alive_only=True)
165+
entry = runs[0] if runs else None
166+
167+
if entry is not None and hasattr(entry, "mcp_url") and entry.mcp_url:
168+
return cls(url=entry.mcp_url, timeout=timeout)
169+
170+
# Fall back to default URL using GlobalConfig port
171+
from dimos.core.global_config import global_config
172+
173+
url = f"http://localhost:{global_config.mcp_port}/mcp"
174+
return cls(url=url, timeout=timeout)
175+
176+
# ------------------------------------------------------------------
177+
# Internals
178+
# ------------------------------------------------------------------
179+
180+
@staticmethod
181+
def _unwrap(response: dict[str, Any]) -> dict[str, Any]:
182+
"""Extract the ``result`` from a JSON-RPC response, raising on error."""
183+
if "error" in response:
184+
err = response["error"]
185+
msg = err.get("message", str(err)) if isinstance(err, dict) else str(err)
186+
raise McpError(msg, code=err.get("code") if isinstance(err, dict) else None)
187+
return response.get("result", {}) # type: ignore[no-any-return]
188+
189+
def __repr__(self) -> str:
190+
return f"McpAdapter(url={self.url!r})"

dimos/agents/mcp/mcp_server.py

Lines changed: 78 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,25 @@
1515

1616
import asyncio
1717
import json
18+
import os
1819
from typing import TYPE_CHECKING, Any
1920

2021
from fastapi import FastAPI
2122
from fastapi.middleware.cors import CORSMiddleware
2223
from fastapi.responses import JSONResponse
24+
from starlette.requests import Request # noqa: TC002
2325
from starlette.responses import Response
2426
import uvicorn
2527

28+
from dimos.agents.annotation import skill
29+
from dimos.core.core import rpc
30+
from dimos.core.module import Module
31+
from dimos.core.rpc_client import RpcCall, RPCClient
2632
from dimos.utils.logging_config import setup_logger
2733

2834
logger = setup_logger()
2935

3036

31-
from starlette.requests import Request # noqa: TC002
32-
33-
from dimos.core.core import rpc
34-
from dimos.core.module import Module
35-
from dimos.core.rpc_client import RpcCall, RPCClient
36-
3737
if TYPE_CHECKING:
3838
import concurrent.futures
3939

@@ -51,6 +51,11 @@
5151
app.state.rpc_calls = {}
5252

5353

54+
# ---------------------------------------------------------------------------
55+
# JSON-RPC helpers
56+
# ---------------------------------------------------------------------------
57+
58+
5459
def _jsonrpc_result(req_id: Any, result: Any) -> dict[str, Any]:
5560
return {"jsonrpc": "2.0", "id": req_id, "result": result}
5661

@@ -63,6 +68,11 @@ def _jsonrpc_error(req_id: Any, code: int, message: str) -> dict[str, Any]:
6368
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
6469

6570

71+
# ---------------------------------------------------------------------------
72+
# JSON-RPC handlers (standard MCP protocol only)
73+
# ---------------------------------------------------------------------------
74+
75+
6676
def _handle_initialize(req_id: Any) -> dict[str, Any]:
6777
return _jsonrpc_result(
6878
req_id,
@@ -77,11 +87,11 @@ def _handle_initialize(req_id: Any) -> dict[str, Any]:
7787
def _handle_tools_list(req_id: Any, skills: list[SkillInfo]) -> dict[str, Any]:
7888
tools = []
7989

80-
for skill in skills:
81-
schema = json.loads(skill.args_schema)
90+
for s in skills:
91+
schema = json.loads(s.args_schema)
8292
description = schema.pop("description", None)
8393
schema.pop("title", None)
84-
tool = {"name": skill.func_name, "inputSchema": schema}
94+
tool: dict[str, Any] = {"name": s.func_name, "inputSchema": schema}
8595
if description:
8696
tool["description"] = description
8797
tools.append(tool)
@@ -128,7 +138,7 @@ async def handle_request(
128138
params = request.get("params", {}) or {}
129139
req_id = request.get("id")
130140

131-
# JSON-RPC notifications have no "id" the server must not reply.
141+
# JSON-RPC notifications have no "id" -- the server must not reply.
132142
if "id" not in request:
133143
return None
134144

@@ -158,6 +168,11 @@ async def mcp_endpoint(request: Request) -> Response:
158168
return JSONResponse(result)
159169

160170

171+
# ---------------------------------------------------------------------------
172+
# McpServer Module
173+
# ---------------------------------------------------------------------------
174+
175+
161176
class McpServer(Module):
162177
def __init__(self) -> None:
163178
super().__init__()
@@ -183,12 +198,62 @@ def stop(self) -> None:
183198
@rpc
184199
def on_system_modules(self, modules: list[RPCClient]) -> None:
185200
assert self.rpc is not None
186-
app.state.skills = [skill for module in modules for skill in (module.get_skills() or [])]
201+
app.state.skills = [
202+
skill_info for module in modules for skill_info in (module.get_skills() or [])
203+
]
187204
app.state.rpc_calls = {
188-
skill.func_name: RpcCall(None, self.rpc, skill.func_name, skill.class_name, [])
189-
for skill in app.state.skills
205+
skill_info.func_name: RpcCall(
206+
None, self.rpc, skill_info.func_name, skill_info.class_name, []
207+
)
208+
for skill_info in app.state.skills
190209
}
191210

211+
# ------------------------------------------------------------------
212+
# Introspection skills (exposed as MCP tools via tools/list)
213+
# ------------------------------------------------------------------
214+
215+
@skill
216+
def server_status(self) -> str:
217+
"""Get MCP server status: main process PID, deployed modules, and skill count."""
218+
from dimos.core.run_registry import get_most_recent
219+
220+
skills: list[SkillInfo] = app.state.skills
221+
modules = list(dict.fromkeys(s.class_name for s in skills))
222+
entry = get_most_recent()
223+
pid = entry.pid if entry else os.getpid()
224+
return json.dumps(
225+
{
226+
"pid": pid,
227+
"modules": modules,
228+
"skills": [s.func_name for s in skills],
229+
}
230+
)
231+
232+
@skill
233+
def list_modules(self) -> str:
234+
"""List deployed modules and their skills."""
235+
skills: list[SkillInfo] = app.state.skills
236+
modules: dict[str, list[str]] = {}
237+
for s in skills:
238+
modules.setdefault(s.class_name, []).append(s.func_name)
239+
return json.dumps({"modules": modules})
240+
241+
@skill
242+
def agent_send(self, message: str) -> str:
243+
"""Send a message to the running DimOS agent via LCM."""
244+
if not message:
245+
raise ValueError("Message cannot be empty")
246+
247+
from dimos.core.transport import pLCMTransport
248+
249+
transport: pLCMTransport[str] = pLCMTransport("/human_input")
250+
try:
251+
transport.start()
252+
transport.publish(message)
253+
return f"Message sent to agent: {message[:100]}"
254+
finally:
255+
transport.stop()
256+
192257
def _start_server(self, port: int | None = None) -> None:
193258
from dimos.core.global_config import global_config
194259

0 commit comments

Comments
 (0)