This commit is contained in:
2026-04-25 06:45:36 +09:00
commit e77acee8ba
1903 changed files with 513282 additions and 0 deletions
+917
View File
@@ -0,0 +1,917 @@
import { feature } from 'bun:bundle'
import { z } from 'zod/v4'
import { isReplBridgeActive } from '../../bootstrap/state.js'
import { getReplBridgeHandle } from '../../bridge/replBridgeHandle.js'
import type { Tool, ToolUseContext } from '../../Tool.js'
import { buildTool, type ToolDef } from '../../Tool.js'
import { findTeammateTaskByAgentId } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js'
import {
isLocalAgentTask,
queuePendingMessage,
} from '../../tasks/LocalAgentTask/LocalAgentTask.js'
import { isMainSessionTask } from '../../tasks/LocalMainSessionTask.js'
import { toAgentId } from '../../types/ids.js'
import { generateRequestId } from '../../utils/agentId.js'
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
import { logForDebugging } from '../../utils/debug.js'
import { errorMessage } from '../../utils/errors.js'
import { truncate } from '../../utils/format.js'
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
import { lazySchema } from '../../utils/lazySchema.js'
import { parseAddress } from '../../utils/peerAddress.js'
import { semanticBoolean } from '../../utils/semanticBoolean.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import type { BackendType } from '../../utils/swarm/backends/types.js'
import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js'
import { readTeamFileAsync } from '../../utils/swarm/teamHelpers.js'
import {
getAgentId,
getAgentName,
getTeammateColor,
getTeamName,
isTeamLead,
isTeammate,
} from '../../utils/teammate.js'
import {
createShutdownApprovedMessage,
createShutdownRejectedMessage,
createShutdownRequestMessage,
writeToMailbox,
} from '../../utils/teammateMailbox.js'
import { resumeAgentBackground } from '../AgentTool/resumeAgent.js'
import { SEND_MESSAGE_TOOL_NAME } from './constants.js'
import { DESCRIPTION, getPrompt } from './prompt.js'
import { renderToolResultMessage, renderToolUseMessage } from './UI.js'
const StructuredMessage = lazySchema(() =>
z.discriminatedUnion('type', [
z.object({
type: z.literal('shutdown_request'),
reason: z.string().optional(),
}),
z.object({
type: z.literal('shutdown_response'),
request_id: z.string(),
approve: semanticBoolean(),
reason: z.string().optional(),
}),
z.object({
type: z.literal('plan_approval_response'),
request_id: z.string(),
approve: semanticBoolean(),
feedback: z.string().optional(),
}),
]),
)
const inputSchema = lazySchema(() =>
z.object({
to: z
.string()
.describe(
feature('UDS_INBOX')
? 'Recipient: teammate name, "*" for broadcast, "uds:<socket-path>" for a local peer, or "bridge:<session-id>" for a Remote Control peer (use ListPeers to discover)'
: 'Recipient: teammate name, or "*" for broadcast to all teammates',
),
summary: z
.string()
.optional()
.describe(
'A 5-10 word summary shown as a preview in the UI (required when message is a string)',
),
message: z.union([
z.string().describe('Plain text message content'),
StructuredMessage(),
]),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
export type Input = z.infer<InputSchema>
export type MessageRouting = {
sender: string
senderColor?: string
target: string
targetColor?: string
summary?: string
content?: string
}
export type MessageOutput = {
success: boolean
message: string
routing?: MessageRouting
}
export type BroadcastOutput = {
success: boolean
message: string
recipients: string[]
routing?: MessageRouting
}
export type RequestOutput = {
success: boolean
message: string
request_id: string
target: string
}
export type ResponseOutput = {
success: boolean
message: string
request_id?: string
}
export type SendMessageToolOutput =
| MessageOutput
| BroadcastOutput
| RequestOutput
| ResponseOutput
function findTeammateColor(
appState: {
teamContext?: { teammates: { [id: string]: { color?: string } } }
},
name: string,
): string | undefined {
const teammates = appState.teamContext?.teammates
if (!teammates) return undefined
for (const teammate of Object.values(teammates)) {
if ('name' in teammate && (teammate as { name: string }).name === name) {
return teammate.color
}
}
return undefined
}
async function handleMessage(
recipientName: string,
content: string,
summary: string | undefined,
context: ToolUseContext,
): Promise<{ data: MessageOutput }> {
const appState = context.getAppState()
const teamName = getTeamName(appState.teamContext)
const senderName =
getAgentName() || (isTeammate() ? 'teammate' : TEAM_LEAD_NAME)
const senderColor = getTeammateColor()
await writeToMailbox(
recipientName,
{
from: senderName,
text: content,
summary,
timestamp: new Date().toISOString(),
color: senderColor,
},
teamName,
)
const recipientColor = findTeammateColor(appState, recipientName)
return {
data: {
success: true,
message: `Message sent to ${recipientName}'s inbox`,
routing: {
sender: senderName,
senderColor,
target: `@${recipientName}`,
targetColor: recipientColor,
summary,
content,
},
},
}
}
async function handleBroadcast(
content: string,
summary: string | undefined,
context: ToolUseContext,
): Promise<{ data: BroadcastOutput }> {
const appState = context.getAppState()
const teamName = getTeamName(appState.teamContext)
if (!teamName) {
throw new Error(
'Not in a team context. Create a team with Teammate spawnTeam first, or set CLAUDE_CODE_TEAM_NAME.',
)
}
const teamFile = await readTeamFileAsync(teamName)
if (!teamFile) {
throw new Error(`Team "${teamName}" does not exist`)
}
const senderName =
getAgentName() || (isTeammate() ? 'teammate' : TEAM_LEAD_NAME)
if (!senderName) {
throw new Error(
'Cannot broadcast: sender name is required. Set CLAUDE_CODE_AGENT_NAME.',
)
}
const senderColor = getTeammateColor()
const recipients: string[] = []
for (const member of teamFile.members) {
if (member.name.toLowerCase() === senderName.toLowerCase()) {
continue
}
recipients.push(member.name)
}
if (recipients.length === 0) {
return {
data: {
success: true,
message: 'No teammates to broadcast to (you are the only team member)',
recipients: [],
},
}
}
for (const recipientName of recipients) {
await writeToMailbox(
recipientName,
{
from: senderName,
text: content,
summary,
timestamp: new Date().toISOString(),
color: senderColor,
},
teamName,
)
}
return {
data: {
success: true,
message: `Message broadcast to ${recipients.length} teammate(s): ${recipients.join(', ')}`,
recipients,
routing: {
sender: senderName,
senderColor,
target: '@team',
summary,
content,
},
},
}
}
async function handleShutdownRequest(
targetName: string,
reason: string | undefined,
context: ToolUseContext,
): Promise<{ data: RequestOutput }> {
const appState = context.getAppState()
const teamName = getTeamName(appState.teamContext)
const senderName = getAgentName() || TEAM_LEAD_NAME
const requestId = generateRequestId('shutdown', targetName)
const shutdownMessage = createShutdownRequestMessage({
requestId,
from: senderName,
reason,
})
await writeToMailbox(
targetName,
{
from: senderName,
text: jsonStringify(shutdownMessage),
timestamp: new Date().toISOString(),
color: getTeammateColor(),
},
teamName,
)
return {
data: {
success: true,
message: `Shutdown request sent to ${targetName}. Request ID: ${requestId}`,
request_id: requestId,
target: targetName,
},
}
}
async function handleShutdownApproval(
requestId: string,
context: ToolUseContext,
): Promise<{ data: ResponseOutput }> {
const teamName = getTeamName()
const agentId = getAgentId()
const agentName = getAgentName() || 'teammate'
logForDebugging(
`[SendMessageTool] handleShutdownApproval: teamName=${teamName}, agentId=${agentId}, agentName=${agentName}`,
)
let ownPaneId: string | undefined
let ownBackendType: BackendType | undefined
if (teamName) {
const teamFile = await readTeamFileAsync(teamName)
if (teamFile && agentId) {
const selfMember = teamFile.members.find(m => m.agentId === agentId)
if (selfMember) {
ownPaneId = selfMember.tmuxPaneId
ownBackendType = selfMember.backendType
}
}
}
const approvedMessage = createShutdownApprovedMessage({
requestId,
from: agentName,
paneId: ownPaneId,
backendType: ownBackendType,
})
await writeToMailbox(
TEAM_LEAD_NAME,
{
from: agentName,
text: jsonStringify(approvedMessage),
timestamp: new Date().toISOString(),
color: getTeammateColor(),
},
teamName,
)
if (ownBackendType === 'in-process') {
logForDebugging(
`[SendMessageTool] In-process teammate ${agentName} approving shutdown - signaling abort`,
)
if (agentId) {
const appState = context.getAppState()
const task = findTeammateTaskByAgentId(agentId, appState.tasks)
if (task?.abortController) {
task.abortController.abort()
logForDebugging(
`[SendMessageTool] Aborted controller for in-process teammate ${agentName}`,
)
} else {
logForDebugging(
`[SendMessageTool] Warning: Could not find task/abortController for ${agentName}`,
)
}
}
} else {
if (agentId) {
const appState = context.getAppState()
const task = findTeammateTaskByAgentId(agentId, appState.tasks)
if (task?.abortController) {
logForDebugging(
`[SendMessageTool] Fallback: Found in-process task for ${agentName} via AppState, aborting`,
)
task.abortController.abort()
return {
data: {
success: true,
message: `Shutdown approved (fallback path). Agent ${agentName} is now exiting.`,
request_id: requestId,
},
}
}
}
setImmediate(async () => {
await gracefulShutdown(0, 'other')
})
}
return {
data: {
success: true,
message: `Shutdown approved. Sent confirmation to team-lead. Agent ${agentName} is now exiting.`,
request_id: requestId,
},
}
}
async function handleShutdownRejection(
requestId: string,
reason: string,
): Promise<{ data: ResponseOutput }> {
const teamName = getTeamName()
const agentName = getAgentName() || 'teammate'
const rejectedMessage = createShutdownRejectedMessage({
requestId,
from: agentName,
reason,
})
await writeToMailbox(
TEAM_LEAD_NAME,
{
from: agentName,
text: jsonStringify(rejectedMessage),
timestamp: new Date().toISOString(),
color: getTeammateColor(),
},
teamName,
)
return {
data: {
success: true,
message: `Shutdown rejected. Reason: "${reason}". Continuing to work.`,
request_id: requestId,
},
}
}
async function handlePlanApproval(
recipientName: string,
requestId: string,
context: ToolUseContext,
): Promise<{ data: ResponseOutput }> {
const appState = context.getAppState()
const teamName = appState.teamContext?.teamName
if (!isTeamLead(appState.teamContext)) {
throw new Error(
'Only the team lead can approve plans. Teammates cannot approve their own or other plans.',
)
}
const leaderMode = appState.toolPermissionContext.mode
const modeToInherit = leaderMode === 'plan' ? 'default' : leaderMode
const approvalResponse = {
type: 'plan_approval_response',
requestId,
approved: true,
timestamp: new Date().toISOString(),
permissionMode: modeToInherit,
}
await writeToMailbox(
recipientName,
{
from: TEAM_LEAD_NAME,
text: jsonStringify(approvalResponse),
timestamp: new Date().toISOString(),
},
teamName,
)
return {
data: {
success: true,
message: `Plan approved for ${recipientName}. They will receive the approval and can proceed with implementation.`,
request_id: requestId,
},
}
}
async function handlePlanRejection(
recipientName: string,
requestId: string,
feedback: string,
context: ToolUseContext,
): Promise<{ data: ResponseOutput }> {
const appState = context.getAppState()
const teamName = appState.teamContext?.teamName
if (!isTeamLead(appState.teamContext)) {
throw new Error(
'Only the team lead can reject plans. Teammates cannot reject their own or other plans.',
)
}
const rejectionResponse = {
type: 'plan_approval_response',
requestId,
approved: false,
feedback,
timestamp: new Date().toISOString(),
}
await writeToMailbox(
recipientName,
{
from: TEAM_LEAD_NAME,
text: jsonStringify(rejectionResponse),
timestamp: new Date().toISOString(),
},
teamName,
)
return {
data: {
success: true,
message: `Plan rejected for ${recipientName} with feedback: "${feedback}"`,
request_id: requestId,
},
}
}
export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> =
buildTool({
name: SEND_MESSAGE_TOOL_NAME,
searchHint: 'send messages to agent teammates (swarm protocol)',
maxResultSizeChars: 100_000,
userFacingName() {
return 'SendMessage'
},
get inputSchema(): InputSchema {
return inputSchema()
},
shouldDefer: true,
isEnabled() {
return isAgentSwarmsEnabled()
},
isReadOnly(input) {
return typeof input.message === 'string'
},
backfillObservableInput(input) {
if ('type' in input) return
if (typeof input.to !== 'string') return
if (input.to === '*') {
input.type = 'broadcast'
if (typeof input.message === 'string') input.content = input.message
} else if (typeof input.message === 'string') {
input.type = 'message'
input.recipient = input.to
input.content = input.message
} else if (typeof input.message === 'object' && input.message !== null) {
const msg = input.message as {
type?: string
request_id?: string
approve?: boolean
reason?: string
feedback?: string
}
input.type = msg.type
input.recipient = input.to
if (msg.request_id !== undefined) input.request_id = msg.request_id
if (msg.approve !== undefined) input.approve = msg.approve
const content = msg.reason ?? msg.feedback
if (content !== undefined) input.content = content
}
},
toAutoClassifierInput(input) {
if (typeof input.message === 'string') {
return `to ${input.to}: ${input.message}`
}
switch (input.message.type) {
case 'shutdown_request':
return `shutdown_request to ${input.to}`
case 'shutdown_response':
return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}`
case 'plan_approval_response':
return `plan_approval ${input.message.approve ? 'approve' : 'reject'} to ${input.to}`
}
},
async checkPermissions(input, _context) {
if (feature('UDS_INBOX') && parseAddress(input.to).scheme === 'bridge') {
return {
behavior: 'ask' as const,
message: `Send a message to Remote Control session ${input.to}? It arrives as a user prompt on the receiving Claude (possibly another machine) via Anthropic's servers.`,
// safetyCheck (not mode) — permissions.ts guards this before both
// bypassPermissions (step 1g) and auto-mode's allowlist/classifier.
// Cross-machine prompt injection must stay bypass-immune.
decisionReason: {
type: 'safetyCheck',
reason:
'Cross-machine bridge message requires explicit user consent',
classifierApprovable: false,
},
}
}
return { behavior: 'allow' as const, updatedInput: input }
},
async validateInput(input, _context) {
if (input.to.trim().length === 0) {
return {
result: false,
message: 'to must not be empty',
errorCode: 9,
}
}
const addr = parseAddress(input.to)
if (
(addr.scheme === 'bridge' || addr.scheme === 'uds') &&
addr.target.trim().length === 0
) {
return {
result: false,
message: 'address target must not be empty',
errorCode: 9,
}
}
if (input.to.includes('@')) {
return {
result: false,
message:
'to must be a bare teammate name or "*" — there is only one team per session',
errorCode: 9,
}
}
if (feature('UDS_INBOX') && parseAddress(input.to).scheme === 'bridge') {
// Structured-message rejection first — it's the permanent constraint.
// Showing "not connected" first would make the user reconnect only to
// hit this error on retry.
if (typeof input.message !== 'string') {
return {
result: false,
message:
'structured messages cannot be sent cross-session — only plain text',
errorCode: 9,
}
}
// postInterClaudeMessage derives from= via getReplBridgeHandle() —
// check handle directly for the init-timing window. Also check
// isReplBridgeActive() to reject outbound-only (CCR mirror) mode
// where the bridge is write-only and peer messaging is unsupported.
if (!getReplBridgeHandle() || !isReplBridgeActive()) {
return {
result: false,
message:
'Remote Control is not connected — cannot send to a bridge: target. Reconnect with /remote-control first.',
errorCode: 9,
}
}
return { result: true }
}
if (
feature('UDS_INBOX') &&
parseAddress(input.to).scheme === 'uds' &&
typeof input.message === 'string'
) {
// UDS cross-session send: summary isn't rendered (UI.tsx returns null
// for string messages), so don't require it. Structured messages fall
// through to the rejection below.
return { result: true }
}
if (typeof input.message === 'string') {
if (!input.summary || input.summary.trim().length === 0) {
return {
result: false,
message: 'summary is required when message is a string',
errorCode: 9,
}
}
return { result: true }
}
if (input.to === '*') {
return {
result: false,
message: 'structured messages cannot be broadcast (to: "*")',
errorCode: 9,
}
}
if (feature('UDS_INBOX') && parseAddress(input.to).scheme !== 'other') {
return {
result: false,
message:
'structured messages cannot be sent cross-session — only plain text',
errorCode: 9,
}
}
if (
input.message.type === 'shutdown_response' &&
input.to !== TEAM_LEAD_NAME
) {
return {
result: false,
message: `shutdown_response must be sent to "${TEAM_LEAD_NAME}"`,
errorCode: 9,
}
}
if (
input.message.type === 'shutdown_response' &&
!input.message.approve &&
(!input.message.reason || input.message.reason.trim().length === 0)
) {
return {
result: false,
message: 'reason is required when rejecting a shutdown request',
errorCode: 9,
}
}
return { result: true }
},
async description() {
return DESCRIPTION
},
async prompt() {
return getPrompt()
},
mapToolResultToToolResultBlockParam(data, toolUseID) {
return {
tool_use_id: toolUseID,
type: 'tool_result' as const,
content: [
{
type: 'text' as const,
text: jsonStringify(data),
},
],
}
},
async call(input, context, canUseTool, assistantMessage) {
if (feature('UDS_INBOX') && typeof input.message === 'string') {
const addr = parseAddress(input.to)
if (addr.scheme === 'bridge') {
// Re-check handle — checkPermissions blocks on user approval (can be
// minutes). validateInput's check is stale if the bridge dropped
// during the prompt wait; without this, from="unknown" ships.
// Also re-check isReplBridgeActive for outbound-only mode.
if (!getReplBridgeHandle() || !isReplBridgeActive()) {
return {
data: {
success: false,
message: `Remote Control disconnected before send — cannot deliver to ${input.to}`,
},
}
}
/* eslint-disable @typescript-eslint/no-require-imports */
const { postInterClaudeMessage } =
require('../../bridge/peerSessions.js') as typeof import('../../bridge/peerSessions.js')
/* eslint-enable @typescript-eslint/no-require-imports */
const result = await postInterClaudeMessage(
addr.target,
input.message,
)
const preview = input.summary || truncate(input.message, 50)
return {
data: {
success: result.ok,
message: result.ok
? `${preview}” → ${input.to}`
: `Failed to send to ${input.to}: ${result.error ?? 'unknown'}`,
},
}
}
if (addr.scheme === 'uds') {
/* eslint-disable @typescript-eslint/no-require-imports */
const { sendToUdsSocket } =
require('../../utils/udsClient.js') as typeof import('../../utils/udsClient.js')
/* eslint-enable @typescript-eslint/no-require-imports */
try {
await sendToUdsSocket(addr.target, input.message)
const preview = input.summary || truncate(input.message, 50)
return {
data: {
success: true,
message: `${preview}” → ${input.to}`,
},
}
} catch (e) {
return {
data: {
success: false,
message: `Failed to send to ${input.to}: ${errorMessage(e)}`,
},
}
}
}
}
// Route to in-process subagent by name or raw agentId before falling
// through to ambient-team resolution. Stopped agents are auto-resumed.
if (typeof input.message === 'string' && input.to !== '*') {
const appState = context.getAppState()
const registered = appState.agentNameRegistry.get(input.to)
const agentId = registered ?? toAgentId(input.to)
if (agentId) {
const task = appState.tasks[agentId]
if (isLocalAgentTask(task) && !isMainSessionTask(task)) {
if (task.status === 'running') {
queuePendingMessage(
agentId,
input.message,
context.setAppStateForTasks ?? context.setAppState,
)
return {
data: {
success: true,
message: `Message queued for delivery to ${input.to} at its next tool round.`,
},
}
}
// task exists but stopped — auto-resume
try {
const result = await resumeAgentBackground({
agentId,
prompt: input.message,
toolUseContext: context,
canUseTool,
invokingRequestId: assistantMessage?.requestId,
})
return {
data: {
success: true,
message: `Agent "${input.to}" was stopped (${task.status}); resumed it in the background with your message. You'll be notified when it finishes. Output: ${result.outputFile}`,
},
}
} catch (e) {
return {
data: {
success: false,
message: `Agent "${input.to}" is stopped (${task.status}) and could not be resumed: ${errorMessage(e)}`,
},
}
}
} else {
// task evicted from state — try resume from disk transcript.
// agentId is either a registered name or a format-matching raw ID
// (toAgentId validates the createAgentId format, so teammate names
// never reach this block).
try {
const result = await resumeAgentBackground({
agentId,
prompt: input.message,
toolUseContext: context,
canUseTool,
invokingRequestId: assistantMessage?.requestId,
})
return {
data: {
success: true,
message: `Agent "${input.to}" had no active task; resumed from transcript in the background with your message. You'll be notified when it finishes. Output: ${result.outputFile}`,
},
}
} catch (e) {
return {
data: {
success: false,
message: `Agent "${input.to}" is registered but has no transcript to resume. It may have been cleaned up. (${errorMessage(e)})`,
},
}
}
}
}
}
if (typeof input.message === 'string') {
if (input.to === '*') {
return handleBroadcast(input.message, input.summary, context)
}
return handleMessage(input.to, input.message, input.summary, context)
}
if (input.to === '*') {
throw new Error('structured messages cannot be broadcast')
}
switch (input.message.type) {
case 'shutdown_request':
return handleShutdownRequest(input.to, input.message.reason, context)
case 'shutdown_response':
if (input.message.approve) {
return handleShutdownApproval(input.message.request_id, context)
}
return handleShutdownRejection(
input.message.request_id,
input.message.reason!,
)
case 'plan_approval_response':
if (input.message.approve) {
return handlePlanApproval(
input.to,
input.message.request_id,
context,
)
}
return handlePlanRejection(
input.to,
input.message.request_id,
input.message.feedback ?? 'Plan needs revision',
context,
)
}
},
renderToolUseMessage,
renderToolResultMessage,
} satisfies ToolDef<InputSchema, SendMessageToolOutput>)
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { MessageResponse } from '../../components/MessageResponse.js';
import { Text } from '../../ink.js';
import { jsonParse } from '../../utils/slowOperations.js';
import type { Input, SendMessageToolOutput } from './SendMessageTool.js';
export function renderToolUseMessage(input: Partial<Input>): React.ReactNode {
if (typeof input.message !== 'object' || input.message === null) {
return null;
}
if (input.message.type === 'plan_approval_response') {
return input.message.approve ? `approve plan from: ${input.to}` : `reject plan from: ${input.to}`;
}
return null;
}
export function renderToolResultMessage(content: SendMessageToolOutput | string, _progressMessages: unknown, {
verbose
}: {
verbose: boolean;
}): React.ReactNode {
const result: SendMessageToolOutput = typeof content === 'string' ? jsonParse(content) : content;
if ('routing' in result && result.routing) {
return null;
}
if ('request_id' in result && 'target' in result) {
return null;
}
return <MessageResponse>
<Text dimColor>{result.message}</Text>
</MessageResponse>;
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1lc3NhZ2VSZXNwb25zZSIsIlRleHQiLCJqc29uUGFyc2UiLCJJbnB1dCIsIlNlbmRNZXNzYWdlVG9vbE91dHB1dCIsInJlbmRlclRvb2xVc2VNZXNzYWdlIiwiaW5wdXQiLCJQYXJ0aWFsIiwiUmVhY3ROb2RlIiwibWVzc2FnZSIsInR5cGUiLCJhcHByb3ZlIiwidG8iLCJyZW5kZXJUb29sUmVzdWx0TWVzc2FnZSIsImNvbnRlbnQiLCJfcHJvZ3Jlc3NNZXNzYWdlcyIsInZlcmJvc2UiLCJyZXN1bHQiLCJyb3V0aW5nIl0sInNvdXJjZXMiOlsiVUkudHN4Il0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCB7IE1lc3NhZ2VSZXNwb25zZSB9IGZyb20gJy4uLy4uL2NvbXBvbmVudHMvTWVzc2FnZVJlc3BvbnNlLmpzJ1xuaW1wb3J0IHsgVGV4dCB9IGZyb20gJy4uLy4uL2luay5qcydcbmltcG9ydCB7IGpzb25QYXJzZSB9IGZyb20gJy4uLy4uL3V0aWxzL3Nsb3dPcGVyYXRpb25zLmpzJ1xuaW1wb3J0IHR5cGUgeyBJbnB1dCwgU2VuZE1lc3NhZ2VUb29sT3V0cHV0IH0gZnJvbSAnLi9TZW5kTWVzc2FnZVRvb2wuanMnXG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sVXNlTWVzc2FnZShpbnB1dDogUGFydGlhbDxJbnB1dD4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBpZiAodHlwZW9mIGlucHV0Lm1lc3NhZ2UgIT09ICdvYmplY3QnIHx8IGlucHV0Lm1lc3NhZ2UgPT09IG51bGwpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG4gIGlmIChpbnB1dC5tZXNzYWdlLnR5cGUgPT09ICdwbGFuX2FwcHJvdmFsX3Jlc3BvbnNlJykge1xuICAgIHJldHVybiBpbnB1dC5tZXNzYWdlLmFwcHJvdmVcbiAgICAgID8gYGFwcHJvdmUgcGxhbiBmcm9tOiAke2lucHV0LnRvfWBcbiAgICAgIDogYHJlamVjdCBwbGFuIGZyb206ICR7aW5wdXQudG99YFxuICB9XG4gIHJldHVybiBudWxsXG59XG5cbmV4cG9ydCBmdW5jdGlvbiByZW5kZXJUb29sUmVzdWx0TWVzc2FnZShcbiAgY29udGVudDogU2VuZE1lc3NhZ2VUb29sT3V0cHV0IHwgc3RyaW5nLFxuICBfcHJvZ3Jlc3NNZXNzYWdlczogdW5rbm93bixcbiAgeyB2ZXJib3NlIH06IHsgdmVyYm9zZTogYm9vbGVhbiB9LFxuKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgcmVzdWx0OiBTZW5kTWVzc2FnZVRvb2xPdXRwdXQgPVxuICAgIHR5cGVvZiBjb250ZW50ID09PSAnc3RyaW5nJyA/IGpzb25QYXJzZShjb250ZW50KSA6IGNvbnRlbnRcblxuICBpZiAoJ3JvdXRpbmcnIGluIHJlc3VsdCAmJiByZXN1bHQucm91dGluZykge1xuICAgIHJldHVybiBudWxsXG4gIH1cblxuICBpZiAoJ3JlcXVlc3RfaWQnIGluIHJlc3VsdCAmJiAndGFyZ2V0JyBpbiByZXN1bHQpIHtcbiAgICByZXR1cm4gbnVsbFxuICB9XG5cbiAgcmV0dXJuIChcbiAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgPFRleHQgZGltQ29sb3I+e3Jlc3VsdC5tZXNzYWdlfTwvVGV4dD5cbiAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPQSxLQUFLLE1BQU0sT0FBTztBQUN6QixTQUFTQyxlQUFlLFFBQVEscUNBQXFDO0FBQ3JFLFNBQVNDLElBQUksUUFBUSxjQUFjO0FBQ25DLFNBQVNDLFNBQVMsUUFBUSwrQkFBK0I7QUFDekQsY0FBY0MsS0FBSyxFQUFFQyxxQkFBcUIsUUFBUSxzQkFBc0I7QUFFeEUsT0FBTyxTQUFTQyxvQkFBb0JBLENBQUNDLEtBQUssRUFBRUMsT0FBTyxDQUFDSixLQUFLLENBQUMsQ0FBQyxFQUFFSixLQUFLLENBQUNTLFNBQVMsQ0FBQztFQUMzRSxJQUFJLE9BQU9GLEtBQUssQ0FBQ0csT0FBTyxLQUFLLFFBQVEsSUFBSUgsS0FBSyxDQUFDRyxPQUFPLEtBQUssSUFBSSxFQUFFO0lBQy9ELE9BQU8sSUFBSTtFQUNiO0VBQ0EsSUFBSUgsS0FBSyxDQUFDRyxPQUFPLENBQUNDLElBQUksS0FBSyx3QkFBd0IsRUFBRTtJQUNuRCxPQUFPSixLQUFLLENBQUNHLE9BQU8sQ0FBQ0UsT0FBTyxHQUN4QixzQkFBc0JMLEtBQUssQ0FBQ00sRUFBRSxFQUFFLEdBQ2hDLHFCQUFxQk4sS0FBSyxDQUFDTSxFQUFFLEVBQUU7RUFDckM7RUFDQSxPQUFPLElBQUk7QUFDYjtBQUVBLE9BQU8sU0FBU0MsdUJBQXVCQSxDQUNyQ0MsT0FBTyxFQUFFVixxQkFBcUIsR0FBRyxNQUFNLEVBQ3ZDVyxpQkFBaUIsRUFBRSxPQUFPLEVBQzFCO0VBQUVDO0FBQThCLENBQXJCLEVBQUU7RUFBRUEsT0FBTyxFQUFFLE9BQU87QUFBQyxDQUFDLENBQ2xDLEVBQUVqQixLQUFLLENBQUNTLFNBQVMsQ0FBQztFQUNqQixNQUFNUyxNQUFNLEVBQUViLHFCQUFxQixHQUNqQyxPQUFPVSxPQUFPLEtBQUssUUFBUSxHQUFHWixTQUFTLENBQUNZLE9BQU8sQ0FBQyxHQUFHQSxPQUFPO0VBRTVELElBQUksU0FBUyxJQUFJRyxNQUFNLElBQUlBLE1BQU0sQ0FBQ0MsT0FBTyxFQUFFO0lBQ3pDLE9BQU8sSUFBSTtFQUNiO0VBRUEsSUFBSSxZQUFZLElBQUlELE1BQU0sSUFBSSxRQUFRLElBQUlBLE1BQU0sRUFBRTtJQUNoRCxPQUFPLElBQUk7RUFDYjtFQUVBLE9BQ0UsQ0FBQyxlQUFlO0FBQ3BCLE1BQU0sQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUNBLE1BQU0sQ0FBQ1IsT0FBTyxDQUFDLEVBQUUsSUFBSTtBQUMzQyxJQUFJLEVBQUUsZUFBZSxDQUFDO0FBRXRCIiwiaWdub3JlTGlzdCI6W119
+1
View File
@@ -0,0 +1 @@
export const SEND_MESSAGE_TOOL_NAME = 'SendMessage'
+49
View File
@@ -0,0 +1,49 @@
import { feature } from 'bun:bundle'
export const DESCRIPTION = 'Send a message to another agent'
export function getPrompt(): string {
const udsRow = feature('UDS_INBOX')
? `\n| \`"uds:/path/to.sock"\` | Local Claude session's socket (same machine; use \`ListPeers\`) |
| \`"bridge:session_..."\` | Remote Control peer session (cross-machine; use \`ListPeers\`) |`
: ''
const udsSection = feature('UDS_INBOX')
? `\n\n## Cross-session
Use \`ListPeers\` to discover targets, then:
\`\`\`json
{"to": "uds:/tmp/cc-socks/1234.sock", "message": "check if tests pass over there"}
{"to": "bridge:session_01AbCd...", "message": "what branch are you on?"}
\`\`\`
A listed peer is alive and will process your message — no "busy" state; messages enqueue and drain at the receiver's next tool round. Your message arrives wrapped as \`<cross-session-message from="...">\`. **To reply to an incoming message, copy its \`from\` attribute as your \`to\`.**`
: ''
return `
# SendMessage
Send a message to another agent.
\`\`\`json
{"to": "researcher", "summary": "assign task 1", "message": "start on task #1"}
\`\`\`
| \`to\` | |
|---|---|
| \`"researcher"\` | Teammate by name |
| \`"*"\` | Broadcast to all teammates — expensive (linear in team size), use only when everyone genuinely needs it |${udsRow}
Your plain text output is NOT visible to other agents — to communicate, you MUST call this tool. Messages from teammates are delivered automatically; you don't check an inbox. Refer to teammates by name, never by UUID. When relaying, don't quote the original — it's already rendered to the user.${udsSection}
## Protocol responses (legacy)
If you receive a JSON message with \`type: "shutdown_request"\` or \`type: "plan_approval_request"\`, respond with the matching \`_response\` type — echo the \`request_id\`, set \`approve\` true/false:
\`\`\`json
{"to": "team-lead", "message": {"type": "shutdown_response", "request_id": "...", "approve": true}}
{"to": "researcher", "message": {"type": "plan_approval_response", "request_id": "...", "approve": false, "feedback": "add error handling"}}
\`\`\`
Approving shutdown terminates your process. Rejecting plan sends the teammate back to revise. Don't originate \`shutdown_request\` unless asked. Don't send structured JSON status messages — use TaskUpdate.
`.trim()
}