186 lines
7.5 KiB
Python
186 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""prompt_envelope.py — wraps every dispatched job prompt so agents DO work.
|
|
|
|
Why: agents treated job DMs as informational and replied with prose; swarms and
|
|
timers never got created. The envelope puts an executable subagent-spawn tool
|
|
call at the TOP (before the task) and repeats it at the BOTTOM (after the task),
|
|
with a work-first mandate and explicit tool/curl surface in between.
|
|
|
|
Profiles tune the spawn recipe per job family; add new ones to PROFILES.
|
|
"""
|
|
import json
|
|
import re
|
|
|
|
BOX_API = "https://box.muse-dev.online/api/box"
|
|
|
|
# Verified box-namespace private keys on bl (pubkey matches the agent's entry
|
|
# in /srv/board/allowed_signers). Agents without a verified key fall back to
|
|
# the [RESULT] line (harvester) until super provisions their box key.
|
|
BOX_KEY_PATHS = {
|
|
"dev": "/home/super/.ssh/id_dev",
|
|
"pip": "/home/super/.ssh/id_pip",
|
|
"646": "/home/super/.ssh/id_646",
|
|
"opm": "/home/super/.ssh/id_opm",
|
|
}
|
|
|
|
|
|
def _response_rule():
|
|
return (
|
|
"\nRESPONSE RULE: in your reply, every line that is not work output"
|
|
" MUST be a curl tool call against https://box.muse-dev.online/."
|
|
" Fenced bash blocks are executed as tool calls and their output"
|
|
" returns to you; do not narrate the calls.\n"
|
|
)
|
|
|
|
|
|
def _box_surface_block(agent, job_name, job_id, target):
|
|
"""Thread + runtime + direct curl URL + respond command.
|
|
|
|
READ: GET /api/box/jobs/<name> (signed, box namespace; live)
|
|
RESPOND: POST /api/box/job/result (signed, box namespace, own-scope;
|
|
ships with the frontdoor publish -- 404 until then)
|
|
"""
|
|
header = (
|
|
"\n---- BOX RUNTIME ----\n"
|
|
"THREAD: " + str(target) + "\n"
|
|
"RUNTIME: https://box.muse-dev.online/\n"
|
|
"JOB: " + str(job_id) + "\n"
|
|
"AGENT: " + str(agent) + "\n"
|
|
)
|
|
key = BOX_KEY_PATHS.get(agent)
|
|
if not key:
|
|
return (
|
|
header
|
|
+ "\nNOTE: no box signing key is provisioned for '" + str(agent)
|
|
+ "' yet, so curl commands against the box API cannot authenticate."
|
|
+ " Ask super to provision your box key; until then the [RESULT]"
|
|
+ " line is your respond path (the harvester records it).\n"
|
|
+ _response_rule()
|
|
)
|
|
read_cmd = (
|
|
"```bash\n"
|
|
"TS=$(date +%s)\n"
|
|
"SIG=$(printf '%s\\njobs/" + str(job_name) + "' \"$TS\""
|
|
" | ssh-keygen -Y sign -f " + key + " -n box"
|
|
" | python3 -c 'import sys,urllib.parse;"
|
|
" print(urllib.parse.quote(sys.stdin.read().strip()))')\n"
|
|
"curl -s \"" + BOX_API + "/jobs/" + str(job_name)
|
|
+ "?identity=" + str(agent) + "&ts=$TS&sig=$SIG\"\n"
|
|
"```"
|
|
)
|
|
respond_cmd = (
|
|
"```bash\n"
|
|
"TS=$(date +%s)\n"
|
|
"export BOX_TS=$TS\n"
|
|
"export BOX_SIG=$(printf '%s\\njob/result\\n" + str(job_id) + "' \"$TS\""
|
|
" | ssh-keygen -Y sign -f " + key + " -n box)\n"
|
|
"python3 - <<'PYEOF' | curl -s -X POST " + BOX_API + "/job/result"
|
|
" -H 'Content-Type: application/json' -d @-\n"
|
|
"import json, os\n"
|
|
"print(json.dumps({\n"
|
|
" \"identity\": \"" + str(agent) + "\",\n"
|
|
" \"ts\": os.environ[\"BOX_TS\"],\n"
|
|
" \"sig\": os.environ[\"BOX_SIG\"],\n"
|
|
" \"job_id\": \"" + str(job_id) + "\",\n"
|
|
" \"success\": True,\n"
|
|
" \"summary\": \"[RESULT " + str(job_id) + "] <one-line summary>\",\n"
|
|
"}))\n"
|
|
"PYEOF\n"
|
|
"```"
|
|
)
|
|
return (
|
|
header
|
|
+ "\nREAD your work order any time:\n" + read_cmd + "\n"
|
|
+ "\nRESPOND when done -- emit this exact command as a tool call:\n"
|
|
+ respond_cmd + "\n"
|
|
+ _response_rule()
|
|
)
|
|
|
|
UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")
|
|
|
|
# profile -> (spawn count, followup minutes, spawn task hint)
|
|
PROFILES = {
|
|
"pulse": (2, 30, "execute your scope's standing work: set timers, verify them, report per-slot verdicts"),
|
|
"health": (2, 60, "run the prescribed health gates read-only and report per-gate verdicts"),
|
|
"swarm": (2, 20, "sweep swarms and sidechats read-only and report stale or unarchived items"),
|
|
"work": (2, 15, "split the task above into two independent halves and complete each"),
|
|
}
|
|
|
|
|
|
def pick_profile(job_name):
|
|
n = job_name or ""
|
|
if n.startswith("autonomy-pulse"):
|
|
return "pulse"
|
|
if "health" in n:
|
|
return "health"
|
|
if "swarm" in n:
|
|
return "swarm"
|
|
return "work"
|
|
|
|
|
|
def _tool(op, args):
|
|
# no ']' inside the JSON: the harvester's [TOOL ...] regex stops at the first one
|
|
return "[TOOL %s %s]" % (op, json.dumps(args, separators=(", ", ": ")))
|
|
|
|
|
|
def spawn_call(job_id, job_name, profile):
|
|
count, _, hint = PROFILES[profile]
|
|
# no brackets in the task text: the harvester's [TOOL ...] regex stops at the first ']'
|
|
task = "Subagent for job %s (%s): %s." % (job_id, job_name, hint)
|
|
return _tool("swarm.spawn", {"count": count, "task": task[:900], "label": (job_name or "job")[:60]})
|
|
|
|
|
|
def native_followup_call(job_id, job_name, profile, agent):
|
|
"""Muse-native one-shot cron (cron.create runonce) - bridged to followup.create."""
|
|
_, mins, _ = PROFILES[profile]
|
|
return _tool("cron.create", {
|
|
"kind": "runonce", "agent": agent, "in_m": mins,
|
|
"prompt": "Continue job %s (%s): verify with cron.runs and swarm.list then act or close." % (job_id, job_name)})
|
|
|
|
|
|
def followup_call(job_id, job_name, profile, agent):
|
|
_, mins, _ = PROFILES[profile]
|
|
return _tool("followup.create", {
|
|
"agent": agent, "in_m": mins,
|
|
"prompt": "Continue job %s (%s): check swarm results with swarm.list then act or close." % (job_id, job_name)})
|
|
|
|
|
|
def thread_url(target):
|
|
t = str(target)
|
|
return "https://box.muse-dev.online/%s/%s" % ("thread" if UUID_RE.fullmatch(t.lower()) else "sidechat", t)
|
|
|
|
|
|
def wrap(job_name, job_id, agent, target, rendered):
|
|
"""Return the full DM body (without the [JOB id] prefix)."""
|
|
profile = pick_profile(job_name)
|
|
spawn = spawn_call(job_id, job_name, profile)
|
|
follow = native_followup_call(job_id, job_name, profile, agent)
|
|
top = (
|
|
"WORK ORDER - ACTION REQUIRED, NOT INFORMATIONAL.\n"
|
|
"Your FIRST output must be tool calls, not prose. Start now:\n"
|
|
" 1) %s\n"
|
|
" 2) %s\n"
|
|
"Each tool call returns a result message; keep going until the task is done.\n"
|
|
"---- TASK ----\n" % (spawn, follow)
|
|
)
|
|
has_result = "[RESULT" in rendered
|
|
bottom = (
|
|
"\n---- END TASK ----\n"
|
|
"REMINDER (importance: critical): work is only real if a tool call ran. "
|
|
"If you have not yet spawned subagents or set a follow-up timer, do it now:\n"
|
|
" %s\n"
|
|
" %s\n"
|
|
"Native tools (preferred): cron.create (runonce/interval), cron.status, cron.view, cron.runs "
|
|
"(truth about what ran), cron.run, subagent spawn; hooks.dry_run before hooks.enable. "
|
|
"Native cron form: %s\n"
|
|
"Box fallback tools: [TOOL health.check {}] [TOOL swarm.list {}] [TOOL swarm.status {\"swarm_id\": \"<id>\"}] "
|
|
"[TOOL swarm.results {\"swarm_id\": \"<id>\"}] [TOOL quality.check {}]\n"
|
|
% (spawn, follow, native_followup_call(job_id, job_name, profile, agent))
|
|
)
|
|
bottom += _box_surface_block(agent, job_name, job_id, target)
|
|
if not has_result:
|
|
bottom += "Close with: [RESULT %s] <OK|FAILED>: <summary of tool outputs>\n" % job_id
|
|
else:
|
|
bottom += "Close using the [RESULT ...] line specified in the task, summarising tool outputs.\n"
|
|
return top + rendered.strip() + "\n" + bottom
|