feat(messaging): balanced TOOL parsing, DM shorthand, box.exec, tools.list
- response-harvester: extract [TOOL]/[EXEC] JSON args with balanced-brace
scanning (']' and nesting inside args no longer truncate calls); add
[DM {...}] shorthand mapping to dm.send; native aliases (dm, box,
tools) plus arg-synonym normalization; formatters and expanded hints.
- exec-constrained: new read-only box.exec op (27 allowlisted box-ctl
reads) and tools.list op backed by --list-ops for dynamic discovery.
- prompt_envelope: advertise dm.send/box.exec/tools.list in every timer
DM; add dm_call builder.
- lookup_engine + regex_patterns.json: canonical tool_call pattern
accepts the DM engine, ']' in args, one nesting level.
- tests/test_tool_calls.py: 38 tests; docs/INBAND-MESSAGING-SPEC.md:
accepted decision record (Final).
This commit is contained in:
+136
-14
@@ -154,6 +154,12 @@ NATIVE_ALIASES = {
|
||||
"subagents.spawn": "swarm.spawn",
|
||||
"subagent.list": "swarm.list",
|
||||
"subagent.status": "swarm.status",
|
||||
"dm": "dm.send",
|
||||
"message.send": "dm.send",
|
||||
"box": "box.exec",
|
||||
"box.run": "box.exec",
|
||||
"tools": "tools.list",
|
||||
"tools.list": "tools.list",
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +190,23 @@ def normalize_native_call(op, args):
|
||||
break
|
||||
if "count" not in args and "n" in args:
|
||||
args["count"] = args.pop("n")
|
||||
elif op == "dm.send":
|
||||
if "message" not in args:
|
||||
for k in ("text", "body", "content", "msg"):
|
||||
if k in args:
|
||||
args["message"] = args.pop(k)
|
||||
break
|
||||
if "target" not in args:
|
||||
for k in ("thread", "sidechat", "channel"):
|
||||
if k in args:
|
||||
args["target"] = args.pop(k)
|
||||
break
|
||||
elif op == "box.exec":
|
||||
if "action" not in args:
|
||||
for k in ("cmd", "verb", "command", "run"):
|
||||
if k in args:
|
||||
args["action"] = args.pop(k)
|
||||
break
|
||||
elif op.startswith("tmux."):
|
||||
if "session" not in args:
|
||||
for k in ("name", "target", "s"):
|
||||
@@ -198,24 +221,96 @@ def normalize_native_call(op, args):
|
||||
return op, args
|
||||
|
||||
|
||||
_TOOL_OPEN_RE = re.compile(r"\[(TOOL|EXEC)\s+([a-zA-Z0-9_.-]+)\s*")
|
||||
_DM_OPEN_RE = re.compile(r"\[DM\s+")
|
||||
|
||||
|
||||
def _extract_balanced_json(s, i):
|
||||
"""Extract one JSON object starting at s[i] == '{' (brace-aware, string-aware).
|
||||
|
||||
Returns (obj, end_index) with end_index just past the closing brace,
|
||||
or (None, i) when no balanced object is present. Unlike a first-']'
|
||||
regex this tolerates ']' (and nested objects/arrays) inside args.
|
||||
"""
|
||||
if i >= len(s) or s[i] != "{":
|
||||
return None, i
|
||||
depth = 0
|
||||
in_str = False
|
||||
esc = False
|
||||
for j in range(i, len(s)):
|
||||
c = s[j]
|
||||
if in_str:
|
||||
if esc:
|
||||
esc = False
|
||||
elif c == "\\":
|
||||
esc = True
|
||||
elif c == '"':
|
||||
in_str = False
|
||||
elif c == '"':
|
||||
in_str = True
|
||||
elif c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
try:
|
||||
return json.loads(s[i:j + 1]), j + 1
|
||||
except Exception:
|
||||
return None, i
|
||||
return None, i
|
||||
|
||||
|
||||
def _scan_bracket_calls(text):
|
||||
"""Yield (op, args) for [TOOL op {...}] / [EXEC op {...}] / [DM {...}].
|
||||
|
||||
JSON args are extracted with balanced-brace scanning so ']' inside
|
||||
strings, arrays, or nested objects no longer truncates the call.
|
||||
Non-JSON tails keep the legacy first-']' behavior (raw passthrough).
|
||||
"""
|
||||
out = []
|
||||
spans = []
|
||||
for m in _TOOL_OPEN_RE.finditer(text or ""):
|
||||
spans.append((m.start(), "tool", m.group(2).strip(), m.end()))
|
||||
for m in _DM_OPEN_RE.finditer(text or ""):
|
||||
spans.append((m.start(), "dm", "dm.send", m.end()))
|
||||
spans.sort()
|
||||
for _, kind, op, pos in spans:
|
||||
if pos < len(text) and text[pos] == "{":
|
||||
args, _ = _extract_balanced_json(text, pos)
|
||||
if args is None:
|
||||
continue
|
||||
if not isinstance(args, dict):
|
||||
args = {"raw": args}
|
||||
elif kind == "dm":
|
||||
continue # [DM ...] requires a JSON object; skip bare forms
|
||||
else:
|
||||
end = text.find("]", pos)
|
||||
if end == -1:
|
||||
continue
|
||||
raw_args = text[pos:end].strip()
|
||||
if not raw_args:
|
||||
args = {}
|
||||
else:
|
||||
try:
|
||||
args = json.loads(raw_args)
|
||||
if not isinstance(args, dict):
|
||||
args = {"raw": args}
|
||||
except Exception:
|
||||
args = {"raw": raw_args}
|
||||
out.append((op, args))
|
||||
return out
|
||||
|
||||
|
||||
def parse_tool_calls(text):
|
||||
"""
|
||||
Extract structured tool/exec calls from assistant messages.
|
||||
Supports:
|
||||
1. [TOOL <op> <json_args>] or [EXEC <op> <json_args>]
|
||||
2. ```box / ```tool / ```exec JSON blocks
|
||||
1. [TOOL <op> <json_args>] or [EXEC <op> <json_args>] (JSON may nest)
|
||||
2. [DM <json_args>] shorthand for dm.send
|
||||
3. ```box / ```tool / ```exec JSON blocks
|
||||
"""
|
||||
calls = []
|
||||
for m in re.finditer(r"\[(?:TOOL|EXEC)\s+([a-zA-Z0-9_.-]+)(?:\s+(.*?))?\]", text or ""):
|
||||
op = m.group(1).strip()
|
||||
raw_args = (m.group(2) or "").strip()
|
||||
args = {}
|
||||
if raw_args:
|
||||
try:
|
||||
args = json.loads(raw_args)
|
||||
except Exception:
|
||||
args = {"raw": raw_args}
|
||||
calls.append((op, args))
|
||||
calls.extend(_scan_bracket_calls(text))
|
||||
|
||||
for m in re.finditer(r"```(?:box|tool|exec)\s*\n(.*?)```", text or "", re.DOTALL):
|
||||
block = m.group(1).strip()
|
||||
@@ -311,6 +406,10 @@ def format_tool_result_for_chat(op, raw_output):
|
||||
data = json.loads(raw_output)
|
||||
except Exception:
|
||||
s = str(raw_output).strip()
|
||||
if op == "box.exec":
|
||||
if len(s) > 900:
|
||||
s = s[:900] + "\n…(truncated, refine the call for detail)"
|
||||
return f"box result:\n```\n{s}\n```"
|
||||
return s[:500] if len(s) > 500 else s
|
||||
|
||||
if op == "health.check" and isinstance(data, dict) and "fleet" in data:
|
||||
@@ -385,6 +484,26 @@ def format_tool_result_for_chat(op, raw_output):
|
||||
sw = data.get("swarm", {})
|
||||
return f"Swarm `{sw.get('swarm_id')}`: {sw.get('status')} ({sw.get('done', 0)}/{sw.get('count', 0)} slots completed)."
|
||||
|
||||
if op == "tools.list" and isinstance(data, dict):
|
||||
ops = data.get("ops", [])
|
||||
if not ops:
|
||||
return "No tools registered."
|
||||
ro = [o["op"] for o in ops if not o.get("side_effecting")]
|
||||
se = [o["op"] for o in ops if o.get("side_effecting")]
|
||||
lines = [f"{len(ops)} tools available via [TOOL <op> <args>]."]
|
||||
lines.append("read-only: " + (", ".join(ro) if ro else "none"))
|
||||
if se:
|
||||
lines.append("side-effecting: " + ", ".join(se))
|
||||
return "\n".join(lines)
|
||||
|
||||
if op == "box.exec" and isinstance(data, dict):
|
||||
if data.get("ok") is False:
|
||||
return f"box call failed: {data.get('error', 'unknown error')}"
|
||||
out = json.dumps(data)
|
||||
if len(out) > 900:
|
||||
out = out[:900] + "\n…(truncated, refine the call for detail)"
|
||||
return f"box result:\n```\n{out}\n```"
|
||||
|
||||
# General fallback: compact JSON capped to 400 chars
|
||||
s = json.dumps(data)
|
||||
return s[:400] + "..." if len(s) > 400 else s
|
||||
@@ -774,9 +893,12 @@ def process_messages(raw_messages, agent, thread_id, thread_name, last_wm, follo
|
||||
thread_url = f"https://box.muse-dev.online/thread/{thread_id}"
|
||||
tool_hint = (
|
||||
f"[Runtime Context: {thread_url}]\n"
|
||||
f"Tools available: [TOOL <op> <args>] or curl -sk -X POST https://exec.muse-dev.online/exec\n"
|
||||
f"Tools: [TOOL <op> <args>] or curl -sk -X POST https://exec.muse-dev.online/exec\n"
|
||||
f" • [TOOL tools.list {{}}] — discover every op dynamically\n"
|
||||
f" • [TOOL swarm.spawn {{\"count\": 1, \"task\": \"<task>\"}}] — spawn subagents\n"
|
||||
f" • [DM {{\"to\": \"<agent>\", \"target\": \"<sidechat>\", \"message\": \"<text>\"}}] — send a DM\n"
|
||||
f" • [TOOL box.exec {{\"action\": \"fleet-status\"}}] — call box (read-only actions)\n"
|
||||
f" • [TOOL followup.create {{\"in_m\": 5, \"prompt\": \"<reminder>\"}}]\n"
|
||||
f" • [TOOL swarm.spawn {{\"count\": 1, \"task\": \"<task>\"}}]\n"
|
||||
f" • [TOOL health.check {{}}]\n\n"
|
||||
f"[Directive: Take next action or close with [RESULT <job_id>] <summary>]"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user