source dump of claude code
at main 917 lines 28 kB view raw
1import { feature } from 'bun:bundle' 2import { z } from 'zod/v4' 3import { isReplBridgeActive } from '../../bootstrap/state.js' 4import { getReplBridgeHandle } from '../../bridge/replBridgeHandle.js' 5import type { Tool, ToolUseContext } from '../../Tool.js' 6import { buildTool, type ToolDef } from '../../Tool.js' 7import { findTeammateTaskByAgentId } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js' 8import { 9 isLocalAgentTask, 10 queuePendingMessage, 11} from '../../tasks/LocalAgentTask/LocalAgentTask.js' 12import { isMainSessionTask } from '../../tasks/LocalMainSessionTask.js' 13import { toAgentId } from '../../types/ids.js' 14import { generateRequestId } from '../../utils/agentId.js' 15import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js' 16import { logForDebugging } from '../../utils/debug.js' 17import { errorMessage } from '../../utils/errors.js' 18import { truncate } from '../../utils/format.js' 19import { gracefulShutdown } from '../../utils/gracefulShutdown.js' 20import { lazySchema } from '../../utils/lazySchema.js' 21import { parseAddress } from '../../utils/peerAddress.js' 22import { semanticBoolean } from '../../utils/semanticBoolean.js' 23import { jsonStringify } from '../../utils/slowOperations.js' 24import type { BackendType } from '../../utils/swarm/backends/types.js' 25import { TEAM_LEAD_NAME } from '../../utils/swarm/constants.js' 26import { readTeamFileAsync } from '../../utils/swarm/teamHelpers.js' 27import { 28 getAgentId, 29 getAgentName, 30 getTeammateColor, 31 getTeamName, 32 isTeamLead, 33 isTeammate, 34} from '../../utils/teammate.js' 35import { 36 createShutdownApprovedMessage, 37 createShutdownRejectedMessage, 38 createShutdownRequestMessage, 39 writeToMailbox, 40} from '../../utils/teammateMailbox.js' 41import { resumeAgentBackground } from '../AgentTool/resumeAgent.js' 42import { SEND_MESSAGE_TOOL_NAME } from './constants.js' 43import { DESCRIPTION, getPrompt } from './prompt.js' 44import { renderToolResultMessage, renderToolUseMessage } from './UI.js' 45 46const StructuredMessage = lazySchema(() => 47 z.discriminatedUnion('type', [ 48 z.object({ 49 type: z.literal('shutdown_request'), 50 reason: z.string().optional(), 51 }), 52 z.object({ 53 type: z.literal('shutdown_response'), 54 request_id: z.string(), 55 approve: semanticBoolean(), 56 reason: z.string().optional(), 57 }), 58 z.object({ 59 type: z.literal('plan_approval_response'), 60 request_id: z.string(), 61 approve: semanticBoolean(), 62 feedback: z.string().optional(), 63 }), 64 ]), 65) 66 67const inputSchema = lazySchema(() => 68 z.object({ 69 to: z 70 .string() 71 .describe( 72 feature('UDS_INBOX') 73 ? '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)' 74 : 'Recipient: teammate name, or "*" for broadcast to all teammates', 75 ), 76 summary: z 77 .string() 78 .optional() 79 .describe( 80 'A 5-10 word summary shown as a preview in the UI (required when message is a string)', 81 ), 82 message: z.union([ 83 z.string().describe('Plain text message content'), 84 StructuredMessage(), 85 ]), 86 }), 87) 88type InputSchema = ReturnType<typeof inputSchema> 89 90export type Input = z.infer<InputSchema> 91 92export type MessageRouting = { 93 sender: string 94 senderColor?: string 95 target: string 96 targetColor?: string 97 summary?: string 98 content?: string 99} 100 101export type MessageOutput = { 102 success: boolean 103 message: string 104 routing?: MessageRouting 105} 106 107export type BroadcastOutput = { 108 success: boolean 109 message: string 110 recipients: string[] 111 routing?: MessageRouting 112} 113 114export type RequestOutput = { 115 success: boolean 116 message: string 117 request_id: string 118 target: string 119} 120 121export type ResponseOutput = { 122 success: boolean 123 message: string 124 request_id?: string 125} 126 127export type SendMessageToolOutput = 128 | MessageOutput 129 | BroadcastOutput 130 | RequestOutput 131 | ResponseOutput 132 133function findTeammateColor( 134 appState: { 135 teamContext?: { teammates: { [id: string]: { color?: string } } } 136 }, 137 name: string, 138): string | undefined { 139 const teammates = appState.teamContext?.teammates 140 if (!teammates) return undefined 141 for (const teammate of Object.values(teammates)) { 142 if ('name' in teammate && (teammate as { name: string }).name === name) { 143 return teammate.color 144 } 145 } 146 return undefined 147} 148 149async function handleMessage( 150 recipientName: string, 151 content: string, 152 summary: string | undefined, 153 context: ToolUseContext, 154): Promise<{ data: MessageOutput }> { 155 const appState = context.getAppState() 156 const teamName = getTeamName(appState.teamContext) 157 const senderName = 158 getAgentName() || (isTeammate() ? 'teammate' : TEAM_LEAD_NAME) 159 const senderColor = getTeammateColor() 160 161 await writeToMailbox( 162 recipientName, 163 { 164 from: senderName, 165 text: content, 166 summary, 167 timestamp: new Date().toISOString(), 168 color: senderColor, 169 }, 170 teamName, 171 ) 172 173 const recipientColor = findTeammateColor(appState, recipientName) 174 175 return { 176 data: { 177 success: true, 178 message: `Message sent to ${recipientName}'s inbox`, 179 routing: { 180 sender: senderName, 181 senderColor, 182 target: `@${recipientName}`, 183 targetColor: recipientColor, 184 summary, 185 content, 186 }, 187 }, 188 } 189} 190 191async function handleBroadcast( 192 content: string, 193 summary: string | undefined, 194 context: ToolUseContext, 195): Promise<{ data: BroadcastOutput }> { 196 const appState = context.getAppState() 197 const teamName = getTeamName(appState.teamContext) 198 199 if (!teamName) { 200 throw new Error( 201 'Not in a team context. Create a team with Teammate spawnTeam first, or set CLAUDE_CODE_TEAM_NAME.', 202 ) 203 } 204 205 const teamFile = await readTeamFileAsync(teamName) 206 if (!teamFile) { 207 throw new Error(`Team "${teamName}" does not exist`) 208 } 209 210 const senderName = 211 getAgentName() || (isTeammate() ? 'teammate' : TEAM_LEAD_NAME) 212 if (!senderName) { 213 throw new Error( 214 'Cannot broadcast: sender name is required. Set CLAUDE_CODE_AGENT_NAME.', 215 ) 216 } 217 218 const senderColor = getTeammateColor() 219 220 const recipients: string[] = [] 221 for (const member of teamFile.members) { 222 if (member.name.toLowerCase() === senderName.toLowerCase()) { 223 continue 224 } 225 recipients.push(member.name) 226 } 227 228 if (recipients.length === 0) { 229 return { 230 data: { 231 success: true, 232 message: 'No teammates to broadcast to (you are the only team member)', 233 recipients: [], 234 }, 235 } 236 } 237 238 for (const recipientName of recipients) { 239 await writeToMailbox( 240 recipientName, 241 { 242 from: senderName, 243 text: content, 244 summary, 245 timestamp: new Date().toISOString(), 246 color: senderColor, 247 }, 248 teamName, 249 ) 250 } 251 252 return { 253 data: { 254 success: true, 255 message: `Message broadcast to ${recipients.length} teammate(s): ${recipients.join(', ')}`, 256 recipients, 257 routing: { 258 sender: senderName, 259 senderColor, 260 target: '@team', 261 summary, 262 content, 263 }, 264 }, 265 } 266} 267 268async function handleShutdownRequest( 269 targetName: string, 270 reason: string | undefined, 271 context: ToolUseContext, 272): Promise<{ data: RequestOutput }> { 273 const appState = context.getAppState() 274 const teamName = getTeamName(appState.teamContext) 275 const senderName = getAgentName() || TEAM_LEAD_NAME 276 const requestId = generateRequestId('shutdown', targetName) 277 278 const shutdownMessage = createShutdownRequestMessage({ 279 requestId, 280 from: senderName, 281 reason, 282 }) 283 284 await writeToMailbox( 285 targetName, 286 { 287 from: senderName, 288 text: jsonStringify(shutdownMessage), 289 timestamp: new Date().toISOString(), 290 color: getTeammateColor(), 291 }, 292 teamName, 293 ) 294 295 return { 296 data: { 297 success: true, 298 message: `Shutdown request sent to ${targetName}. Request ID: ${requestId}`, 299 request_id: requestId, 300 target: targetName, 301 }, 302 } 303} 304 305async function handleShutdownApproval( 306 requestId: string, 307 context: ToolUseContext, 308): Promise<{ data: ResponseOutput }> { 309 const teamName = getTeamName() 310 const agentId = getAgentId() 311 const agentName = getAgentName() || 'teammate' 312 313 logForDebugging( 314 `[SendMessageTool] handleShutdownApproval: teamName=${teamName}, agentId=${agentId}, agentName=${agentName}`, 315 ) 316 317 let ownPaneId: string | undefined 318 let ownBackendType: BackendType | undefined 319 if (teamName) { 320 const teamFile = await readTeamFileAsync(teamName) 321 if (teamFile && agentId) { 322 const selfMember = teamFile.members.find(m => m.agentId === agentId) 323 if (selfMember) { 324 ownPaneId = selfMember.tmuxPaneId 325 ownBackendType = selfMember.backendType 326 } 327 } 328 } 329 330 const approvedMessage = createShutdownApprovedMessage({ 331 requestId, 332 from: agentName, 333 paneId: ownPaneId, 334 backendType: ownBackendType, 335 }) 336 337 await writeToMailbox( 338 TEAM_LEAD_NAME, 339 { 340 from: agentName, 341 text: jsonStringify(approvedMessage), 342 timestamp: new Date().toISOString(), 343 color: getTeammateColor(), 344 }, 345 teamName, 346 ) 347 348 if (ownBackendType === 'in-process') { 349 logForDebugging( 350 `[SendMessageTool] In-process teammate ${agentName} approving shutdown - signaling abort`, 351 ) 352 353 if (agentId) { 354 const appState = context.getAppState() 355 const task = findTeammateTaskByAgentId(agentId, appState.tasks) 356 if (task?.abortController) { 357 task.abortController.abort() 358 logForDebugging( 359 `[SendMessageTool] Aborted controller for in-process teammate ${agentName}`, 360 ) 361 } else { 362 logForDebugging( 363 `[SendMessageTool] Warning: Could not find task/abortController for ${agentName}`, 364 ) 365 } 366 } 367 } else { 368 if (agentId) { 369 const appState = context.getAppState() 370 const task = findTeammateTaskByAgentId(agentId, appState.tasks) 371 if (task?.abortController) { 372 logForDebugging( 373 `[SendMessageTool] Fallback: Found in-process task for ${agentName} via AppState, aborting`, 374 ) 375 task.abortController.abort() 376 377 return { 378 data: { 379 success: true, 380 message: `Shutdown approved (fallback path). Agent ${agentName} is now exiting.`, 381 request_id: requestId, 382 }, 383 } 384 } 385 } 386 387 setImmediate(async () => { 388 await gracefulShutdown(0, 'other') 389 }) 390 } 391 392 return { 393 data: { 394 success: true, 395 message: `Shutdown approved. Sent confirmation to team-lead. Agent ${agentName} is now exiting.`, 396 request_id: requestId, 397 }, 398 } 399} 400 401async function handleShutdownRejection( 402 requestId: string, 403 reason: string, 404): Promise<{ data: ResponseOutput }> { 405 const teamName = getTeamName() 406 const agentName = getAgentName() || 'teammate' 407 408 const rejectedMessage = createShutdownRejectedMessage({ 409 requestId, 410 from: agentName, 411 reason, 412 }) 413 414 await writeToMailbox( 415 TEAM_LEAD_NAME, 416 { 417 from: agentName, 418 text: jsonStringify(rejectedMessage), 419 timestamp: new Date().toISOString(), 420 color: getTeammateColor(), 421 }, 422 teamName, 423 ) 424 425 return { 426 data: { 427 success: true, 428 message: `Shutdown rejected. Reason: "${reason}". Continuing to work.`, 429 request_id: requestId, 430 }, 431 } 432} 433 434async function handlePlanApproval( 435 recipientName: string, 436 requestId: string, 437 context: ToolUseContext, 438): Promise<{ data: ResponseOutput }> { 439 const appState = context.getAppState() 440 const teamName = appState.teamContext?.teamName 441 442 if (!isTeamLead(appState.teamContext)) { 443 throw new Error( 444 'Only the team lead can approve plans. Teammates cannot approve their own or other plans.', 445 ) 446 } 447 448 const leaderMode = appState.toolPermissionContext.mode 449 const modeToInherit = leaderMode === 'plan' ? 'default' : leaderMode 450 451 const approvalResponse = { 452 type: 'plan_approval_response', 453 requestId, 454 approved: true, 455 timestamp: new Date().toISOString(), 456 permissionMode: modeToInherit, 457 } 458 459 await writeToMailbox( 460 recipientName, 461 { 462 from: TEAM_LEAD_NAME, 463 text: jsonStringify(approvalResponse), 464 timestamp: new Date().toISOString(), 465 }, 466 teamName, 467 ) 468 469 return { 470 data: { 471 success: true, 472 message: `Plan approved for ${recipientName}. They will receive the approval and can proceed with implementation.`, 473 request_id: requestId, 474 }, 475 } 476} 477 478async function handlePlanRejection( 479 recipientName: string, 480 requestId: string, 481 feedback: string, 482 context: ToolUseContext, 483): Promise<{ data: ResponseOutput }> { 484 const appState = context.getAppState() 485 const teamName = appState.teamContext?.teamName 486 487 if (!isTeamLead(appState.teamContext)) { 488 throw new Error( 489 'Only the team lead can reject plans. Teammates cannot reject their own or other plans.', 490 ) 491 } 492 493 const rejectionResponse = { 494 type: 'plan_approval_response', 495 requestId, 496 approved: false, 497 feedback, 498 timestamp: new Date().toISOString(), 499 } 500 501 await writeToMailbox( 502 recipientName, 503 { 504 from: TEAM_LEAD_NAME, 505 text: jsonStringify(rejectionResponse), 506 timestamp: new Date().toISOString(), 507 }, 508 teamName, 509 ) 510 511 return { 512 data: { 513 success: true, 514 message: `Plan rejected for ${recipientName} with feedback: "${feedback}"`, 515 request_id: requestId, 516 }, 517 } 518} 519 520export const SendMessageTool: Tool<InputSchema, SendMessageToolOutput> = 521 buildTool({ 522 name: SEND_MESSAGE_TOOL_NAME, 523 searchHint: 'send messages to agent teammates (swarm protocol)', 524 maxResultSizeChars: 100_000, 525 526 userFacingName() { 527 return 'SendMessage' 528 }, 529 530 get inputSchema(): InputSchema { 531 return inputSchema() 532 }, 533 shouldDefer: true, 534 535 isEnabled() { 536 return isAgentSwarmsEnabled() 537 }, 538 539 isReadOnly(input) { 540 return typeof input.message === 'string' 541 }, 542 543 backfillObservableInput(input) { 544 if ('type' in input) return 545 if (typeof input.to !== 'string') return 546 547 if (input.to === '*') { 548 input.type = 'broadcast' 549 if (typeof input.message === 'string') input.content = input.message 550 } else if (typeof input.message === 'string') { 551 input.type = 'message' 552 input.recipient = input.to 553 input.content = input.message 554 } else if (typeof input.message === 'object' && input.message !== null) { 555 const msg = input.message as { 556 type?: string 557 request_id?: string 558 approve?: boolean 559 reason?: string 560 feedback?: string 561 } 562 input.type = msg.type 563 input.recipient = input.to 564 if (msg.request_id !== undefined) input.request_id = msg.request_id 565 if (msg.approve !== undefined) input.approve = msg.approve 566 const content = msg.reason ?? msg.feedback 567 if (content !== undefined) input.content = content 568 } 569 }, 570 571 toAutoClassifierInput(input) { 572 if (typeof input.message === 'string') { 573 return `to ${input.to}: ${input.message}` 574 } 575 switch (input.message.type) { 576 case 'shutdown_request': 577 return `shutdown_request to ${input.to}` 578 case 'shutdown_response': 579 return `shutdown_response ${input.message.approve ? 'approve' : 'reject'} ${input.message.request_id}` 580 case 'plan_approval_response': 581 return `plan_approval ${input.message.approve ? 'approve' : 'reject'} to ${input.to}` 582 } 583 }, 584 585 async checkPermissions(input, _context) { 586 if (feature('UDS_INBOX') && parseAddress(input.to).scheme === 'bridge') { 587 return { 588 behavior: 'ask' as const, 589 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.`, 590 // safetyCheck (not mode) — permissions.ts guards this before both 591 // bypassPermissions (step 1g) and auto-mode's allowlist/classifier. 592 // Cross-machine prompt injection must stay bypass-immune. 593 decisionReason: { 594 type: 'safetyCheck', 595 reason: 596 'Cross-machine bridge message requires explicit user consent', 597 classifierApprovable: false, 598 }, 599 } 600 } 601 return { behavior: 'allow' as const, updatedInput: input } 602 }, 603 604 async validateInput(input, _context) { 605 if (input.to.trim().length === 0) { 606 return { 607 result: false, 608 message: 'to must not be empty', 609 errorCode: 9, 610 } 611 } 612 const addr = parseAddress(input.to) 613 if ( 614 (addr.scheme === 'bridge' || addr.scheme === 'uds') && 615 addr.target.trim().length === 0 616 ) { 617 return { 618 result: false, 619 message: 'address target must not be empty', 620 errorCode: 9, 621 } 622 } 623 if (input.to.includes('@')) { 624 return { 625 result: false, 626 message: 627 'to must be a bare teammate name or "*" — there is only one team per session', 628 errorCode: 9, 629 } 630 } 631 if (feature('UDS_INBOX') && parseAddress(input.to).scheme === 'bridge') { 632 // Structured-message rejection first — it's the permanent constraint. 633 // Showing "not connected" first would make the user reconnect only to 634 // hit this error on retry. 635 if (typeof input.message !== 'string') { 636 return { 637 result: false, 638 message: 639 'structured messages cannot be sent cross-session — only plain text', 640 errorCode: 9, 641 } 642 } 643 // postInterClaudeMessage derives from= via getReplBridgeHandle() — 644 // check handle directly for the init-timing window. Also check 645 // isReplBridgeActive() to reject outbound-only (CCR mirror) mode 646 // where the bridge is write-only and peer messaging is unsupported. 647 if (!getReplBridgeHandle() || !isReplBridgeActive()) { 648 return { 649 result: false, 650 message: 651 'Remote Control is not connected — cannot send to a bridge: target. Reconnect with /remote-control first.', 652 errorCode: 9, 653 } 654 } 655 return { result: true } 656 } 657 if ( 658 feature('UDS_INBOX') && 659 parseAddress(input.to).scheme === 'uds' && 660 typeof input.message === 'string' 661 ) { 662 // UDS cross-session send: summary isn't rendered (UI.tsx returns null 663 // for string messages), so don't require it. Structured messages fall 664 // through to the rejection below. 665 return { result: true } 666 } 667 if (typeof input.message === 'string') { 668 if (!input.summary || input.summary.trim().length === 0) { 669 return { 670 result: false, 671 message: 'summary is required when message is a string', 672 errorCode: 9, 673 } 674 } 675 return { result: true } 676 } 677 678 if (input.to === '*') { 679 return { 680 result: false, 681 message: 'structured messages cannot be broadcast (to: "*")', 682 errorCode: 9, 683 } 684 } 685 if (feature('UDS_INBOX') && parseAddress(input.to).scheme !== 'other') { 686 return { 687 result: false, 688 message: 689 'structured messages cannot be sent cross-session — only plain text', 690 errorCode: 9, 691 } 692 } 693 694 if ( 695 input.message.type === 'shutdown_response' && 696 input.to !== TEAM_LEAD_NAME 697 ) { 698 return { 699 result: false, 700 message: `shutdown_response must be sent to "${TEAM_LEAD_NAME}"`, 701 errorCode: 9, 702 } 703 } 704 705 if ( 706 input.message.type === 'shutdown_response' && 707 !input.message.approve && 708 (!input.message.reason || input.message.reason.trim().length === 0) 709 ) { 710 return { 711 result: false, 712 message: 'reason is required when rejecting a shutdown request', 713 errorCode: 9, 714 } 715 } 716 717 return { result: true } 718 }, 719 720 async description() { 721 return DESCRIPTION 722 }, 723 724 async prompt() { 725 return getPrompt() 726 }, 727 728 mapToolResultToToolResultBlockParam(data, toolUseID) { 729 return { 730 tool_use_id: toolUseID, 731 type: 'tool_result' as const, 732 content: [ 733 { 734 type: 'text' as const, 735 text: jsonStringify(data), 736 }, 737 ], 738 } 739 }, 740 741 async call(input, context, canUseTool, assistantMessage) { 742 if (feature('UDS_INBOX') && typeof input.message === 'string') { 743 const addr = parseAddress(input.to) 744 if (addr.scheme === 'bridge') { 745 // Re-check handle — checkPermissions blocks on user approval (can be 746 // minutes). validateInput's check is stale if the bridge dropped 747 // during the prompt wait; without this, from="unknown" ships. 748 // Also re-check isReplBridgeActive for outbound-only mode. 749 if (!getReplBridgeHandle() || !isReplBridgeActive()) { 750 return { 751 data: { 752 success: false, 753 message: `Remote Control disconnected before send — cannot deliver to ${input.to}`, 754 }, 755 } 756 } 757 /* eslint-disable @typescript-eslint/no-require-imports */ 758 const { postInterClaudeMessage } = 759 require('../../bridge/peerSessions.js') as typeof import('../../bridge/peerSessions.js') 760 /* eslint-enable @typescript-eslint/no-require-imports */ 761 const result = await postInterClaudeMessage( 762 addr.target, 763 input.message, 764 ) 765 const preview = input.summary || truncate(input.message, 50) 766 return { 767 data: { 768 success: result.ok, 769 message: result.ok 770 ? `${preview}” → ${input.to}` 771 : `Failed to send to ${input.to}: ${result.error ?? 'unknown'}`, 772 }, 773 } 774 } 775 if (addr.scheme === 'uds') { 776 /* eslint-disable @typescript-eslint/no-require-imports */ 777 const { sendToUdsSocket } = 778 require('../../utils/udsClient.js') as typeof import('../../utils/udsClient.js') 779 /* eslint-enable @typescript-eslint/no-require-imports */ 780 try { 781 await sendToUdsSocket(addr.target, input.message) 782 const preview = input.summary || truncate(input.message, 50) 783 return { 784 data: { 785 success: true, 786 message: `${preview}” → ${input.to}`, 787 }, 788 } 789 } catch (e) { 790 return { 791 data: { 792 success: false, 793 message: `Failed to send to ${input.to}: ${errorMessage(e)}`, 794 }, 795 } 796 } 797 } 798 } 799 800 // Route to in-process subagent by name or raw agentId before falling 801 // through to ambient-team resolution. Stopped agents are auto-resumed. 802 if (typeof input.message === 'string' && input.to !== '*') { 803 const appState = context.getAppState() 804 const registered = appState.agentNameRegistry.get(input.to) 805 const agentId = registered ?? toAgentId(input.to) 806 if (agentId) { 807 const task = appState.tasks[agentId] 808 if (isLocalAgentTask(task) && !isMainSessionTask(task)) { 809 if (task.status === 'running') { 810 queuePendingMessage( 811 agentId, 812 input.message, 813 context.setAppStateForTasks ?? context.setAppState, 814 ) 815 return { 816 data: { 817 success: true, 818 message: `Message queued for delivery to ${input.to} at its next tool round.`, 819 }, 820 } 821 } 822 // task exists but stopped — auto-resume 823 try { 824 const result = await resumeAgentBackground({ 825 agentId, 826 prompt: input.message, 827 toolUseContext: context, 828 canUseTool, 829 invokingRequestId: assistantMessage?.requestId, 830 }) 831 return { 832 data: { 833 success: true, 834 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}`, 835 }, 836 } 837 } catch (e) { 838 return { 839 data: { 840 success: false, 841 message: `Agent "${input.to}" is stopped (${task.status}) and could not be resumed: ${errorMessage(e)}`, 842 }, 843 } 844 } 845 } else { 846 // task evicted from state — try resume from disk transcript. 847 // agentId is either a registered name or a format-matching raw ID 848 // (toAgentId validates the createAgentId format, so teammate names 849 // never reach this block). 850 try { 851 const result = await resumeAgentBackground({ 852 agentId, 853 prompt: input.message, 854 toolUseContext: context, 855 canUseTool, 856 invokingRequestId: assistantMessage?.requestId, 857 }) 858 return { 859 data: { 860 success: true, 861 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}`, 862 }, 863 } 864 } catch (e) { 865 return { 866 data: { 867 success: false, 868 message: `Agent "${input.to}" is registered but has no transcript to resume. It may have been cleaned up. (${errorMessage(e)})`, 869 }, 870 } 871 } 872 } 873 } 874 } 875 876 if (typeof input.message === 'string') { 877 if (input.to === '*') { 878 return handleBroadcast(input.message, input.summary, context) 879 } 880 return handleMessage(input.to, input.message, input.summary, context) 881 } 882 883 if (input.to === '*') { 884 throw new Error('structured messages cannot be broadcast') 885 } 886 887 switch (input.message.type) { 888 case 'shutdown_request': 889 return handleShutdownRequest(input.to, input.message.reason, context) 890 case 'shutdown_response': 891 if (input.message.approve) { 892 return handleShutdownApproval(input.message.request_id, context) 893 } 894 return handleShutdownRejection( 895 input.message.request_id, 896 input.message.reason!, 897 ) 898 case 'plan_approval_response': 899 if (input.message.approve) { 900 return handlePlanApproval( 901 input.to, 902 input.message.request_id, 903 context, 904 ) 905 } 906 return handlePlanRejection( 907 input.to, 908 input.message.request_id, 909 input.message.feedback ?? 'Plan needs revision', 910 context, 911 ) 912 } 913 }, 914 915 renderToolUseMessage, 916 renderToolResultMessage, 917 } satisfies ToolDef<InputSchema, SendMessageToolOutput>)