Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/coding-agent/skills/agent-message/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
name: agent-message
description: Message other active Prime Agent sessions through the daemon. Use to discover active agents and send a direct text message without spoofing sender identity.
---

# Agent Message

Send direct messages to other active Prime Agent sessions through the local
daemon. The daemon derives your sender identity from the current session; do
not try to include a `from` field.

Call directly from the kernel:

```python
agents = await agent_message.list_agents()
receipt = await agent_message.send("worker", "Please inspect the latest result.", mode="auto")
```

## API

- `await agent_message.list_agents()` — returns `current` and `agents`, where
each agent includes active session id, session id, optional name, runtime
kind, cwd, streaming state, and pending message count.
- `await agent_message.send(target, message, mode="auto")` — sends one direct
text message to an active session. `target` is resolved by the daemon like
other live-session selectors. `mode` is `"auto"`, `"follow_up"`, or
`"steer"`.

## Safety

- Broadcast sends are not supported.
- Sender identity is daemon-derived and cannot be spoofed from Python.
- The daemon enforces message size, rate, and pending-queue limits before
accepting delivery.
13 changes: 13 additions & 0 deletions packages/coding-agent/skills/agent-message/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[project]
name = "agent-message"
version = "0.1.0"
description = "Prime Agent session-to-session messaging skill"
requires-python = ">=3.10"
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/agent_message"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Prime Agent session-to-session messaging skill.

All routing and sender identity live in the TypeScript daemon. These functions
only call the host bridge exposed inside the Prime Agent IPython kernel.
"""

from __future__ import annotations

from typing import Any, Literal

from rlm import host_request

MessageMode = Literal["auto", "follow_up", "steer"]


async def list_agents() -> dict[str, Any]:
"""List active daemon sessions addressable by agent_message.send()."""
return await host_request("agent_message.list")


async def send(target: str, message: str, mode: MessageMode = "auto") -> dict[str, Any]:
"""Send one direct text message to another active Prime Agent session.

Args:
target: Active session id, session id/name, or unambiguous suffix.
message: Text payload to deliver.
mode: "auto" queues as follow-up only if the target is streaming;
"follow_up" always uses follow-up when the target is streaming;
"steer" interrupts a streaming target.
"""
if not isinstance(target, str):
raise TypeError(f"target must be str, got {type(target).__name__}")
if not isinstance(message, str):
raise TypeError(f"message must be str, got {type(message).__name__}")
if mode not in ("auto", "follow_up", "steer"):
raise ValueError('mode must be "auto", "follow_up", or "steer"')
return await host_request(
"agent_message.send",
{
"target": target,
"message": message,
"mode": mode,
},
)
123 changes: 123 additions & 0 deletions packages/coding-agent/src/cli/daemon-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const DAEMON_CLIENT_COMMANDS = new Set([
"kill",
"rename",
"prompt",
"send",
"agent-messages",
"steer",
"follow-up",
"state",
Expand Down Expand Up @@ -186,6 +188,12 @@ async function runDaemonClientCommand(parsed: ParsedDaemonClientCommand): Promis
case "prompt":
await runPrompt(client, parsed.positionals);
return;
case "send":
await runSend(client, parsed.positionals, parsed.json);
return;
case "agent-messages":
await runAgentMessages(client, parsed.positionals, parsed.json);
return;
case "steer":
await runMessageCommand(client, "steer", parsed.positionals, parsed.json);
return;
Expand Down Expand Up @@ -762,6 +770,104 @@ async function runPrompt(client: DaemonClient, args: string[]): Promise<void> {
}
}

async function runAgentMessages(client: DaemonClient, args: string[], json: boolean): Promise<void> {
const subcommand = args[0];
switch (subcommand) {
case "status":
await printResponseData(client, { type: "agent_messages_status" }, json);
return;
case "pause":
await printResponseData(client, { type: "agent_messages_pause" }, json);
return;
case "resume":
await printResponseData(client, { type: "agent_messages_resume" }, json);
return;
case "clear": {
const activeSessionId = args[1];
if (!activeSessionId) {
throw new Error("Usage: daemon agent-messages clear <session>");
}
await printResponseData(client, { type: "agent_messages_clear", activeSessionId }, json);
return;
}
default:
throw new Error("Usage: daemon agent-messages <status|pause|resume|clear>");
}
}

async function runSend(client: DaemonClient, args: string[], json: boolean): Promise<void> {
const parsed = parseSendArgs(args);
const response = await client.request({
type: "send_message",
targetActiveSessionId: parsed.targetActiveSessionId,
fromActiveSessionId: parsed.fromActiveSessionId,
deliveryMode: parsed.deliveryMode,
message: parsed.message,
});
const data = requireSuccess(response);
if (json) {
printJson(data);
return;
}
if (isAgentMessageReceipt(data)) {
const target = data.target.sessionName ?? data.target.activeSessionId;
console.log(`Sent to ${target}`);
return;
}
console.log("ok");
}

interface ParsedSendArgs {
targetActiveSessionId: string;
fromActiveSessionId?: string;
deliveryMode?: "auto" | "steer" | "follow_up";
message: string;
}

function parseSendArgs(args: string[]): ParsedSendArgs {
let fromActiveSessionId: string | undefined;
let deliveryMode: "auto" | "steer" | "follow_up" | undefined;
const positionals: string[] = [];

for (let index = 0; index < args.length; index++) {
const arg = args[index];
if (arg === "--from") {
const value = args[index + 1];
if (!value) {
throw new Error("--from requires a session id or name");
}
fromActiveSessionId = value;
index++;
continue;
}
if (arg === "--steer") {
deliveryMode = "steer";
continue;
}
if (arg === "--follow-up") {
deliveryMode = "follow_up";
continue;
}
if (arg === "--auto") {
deliveryMode = "auto";
continue;
}
positionals.push(arg);
}

const targetActiveSessionId = positionals[0];
const message = positionals.slice(1).join(" ").trim();
if (!targetActiveSessionId || !message) {
throw new Error("Usage: daemon send [--from <session>] [--steer|--follow-up] <target-session> <message>");
}
return {
targetActiveSessionId,
fromActiveSessionId,
deliveryMode,
message,
};
}

async function runMessageCommand(
client: DaemonClient,
type: "steer" | "follow_up",
Expand Down Expand Up @@ -1314,6 +1420,18 @@ function isLiveSessionSummary(value: unknown): value is SessionSummary & { activ
return isSessionSummary(value) && typeof value.activeSessionId === "string";
}

function isAgentMessageReceipt(value: unknown): value is { target: { activeSessionId: string; sessionName?: string } } {
if (!value || typeof value !== "object") {
return false;
}
const target = (value as { target?: unknown }).target;
return (
!!target &&
typeof target === "object" &&
typeof (target as { activeSessionId?: unknown }).activeSessionId === "string"
);
}

function printDaemonHelp(): void {
console.log(`${chalk.bold("Usage:")}
${APP_NAME} daemon [options] [session name]
Expand All @@ -1329,6 +1447,8 @@ ${chalk.bold("Commands:")}
attach <session> Attach an interactive terminal to a live session
detach [session] Detach this client from one session or all sessions
prompt <session> <message> Send a prompt, stream events, and exit when idle
send [options] <target> <msg> Send an agent-to-agent message to another live session
agent-messages <cmd> Safety controls: status, pause, resume, clear <session>
steer <session> <message> Queue a steering message
follow-up <session> <message> Queue a follow-up message
rename <session> <name> Rename a live session
Expand All @@ -1345,6 +1465,8 @@ ${chalk.bold("Options:")}
--cwd <dir> Working directory for the created session
--foreground, --no-detach Keep daemon attached to this terminal for debugging
--json Print raw JSON for commands with formatted output; attach streams raw protocol JSON
send options: --from <session>, --steer, --follow-up
agent-messages clear only clears one explicitly named session
Agent options such as --model, --provider, --tools, and --thinking apply to created sessions.

${chalk.bold("Examples:")}
Expand All @@ -1359,6 +1481,7 @@ ${chalk.bold("Examples:")}
${APP_NAME} daemon --socket /tmp/prime-agent.sock list -a
${APP_NAME} daemon --socket /tmp/prime-agent.sock create scratch
${APP_NAME} daemon --socket /tmp/prime-agent.sock prompt <session> "Say hello"
${APP_NAME} daemon --socket /tmp/prime-agent.sock send --from planner worker "Use this context..."
${APP_NAME} daemon --socket /tmp/prime-agent.sock attach <session>
${APP_NAME} daemon --socket /tmp/prime-agent.sock shutdown
`);
Expand Down
Loading