| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { feature } from 'bun:bundle' |
| import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs' |
| import { getSystemPrompt } from '../../constants/prompts.js' |
| import { TEAMMATE_MESSAGE_TAG } from '../../constants/xml.js' |
| import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' |
| import { |
| processMailboxPermissionResponse, |
| registerPermissionCallback, |
| unregisterPermissionCallback, |
| } from '../../hooks/useSwarmPermissionPoller.js' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from '../../services/analytics/index.js' |
| import { getAutoCompactThreshold } from '../../services/compact/autoCompact.js' |
| import { |
| buildPostCompactMessages, |
| compactConversation, |
| ERROR_MESSAGE_USER_ABORT, |
| } from '../../services/compact/compact.js' |
| import { resetMicrocompactState } from '../../services/compact/microCompact.js' |
| import type { AppState } from '../../state/AppState.js' |
| import type { Tool, ToolUseContext } from '../../Tool.js' |
| import { appendTeammateMessage } from '../../tasks/InProcessTeammateTask/InProcessTeammateTask.js' |
| import type { |
| InProcessTeammateTaskState, |
| TeammateIdentity, |
| } from '../../tasks/InProcessTeammateTask/types.js' |
| import { appendCappedMessage } from '../../tasks/InProcessTeammateTask/types.js' |
| import { |
| createActivityDescriptionResolver, |
| createProgressTracker, |
| getProgressUpdate, |
| updateProgressFromMessage, |
| } from '../../tasks/LocalAgentTask/LocalAgentTask.js' |
| import type { CustomAgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' |
| import { runAgent } from '../../tools/AgentTool/runAgent.js' |
| import { awaitClassifierAutoApproval } from '../../tools/BashTool/bashPermissions.js' |
| import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' |
| import { SEND_MESSAGE_TOOL_NAME } from '../../tools/SendMessageTool/constants.js' |
| import { TASK_CREATE_TOOL_NAME } from '../../tools/TaskCreateTool/constants.js' |
| import { TASK_GET_TOOL_NAME } from '../../tools/TaskGetTool/constants.js' |
| import { TASK_LIST_TOOL_NAME } from '../../tools/TaskListTool/constants.js' |
| import { TASK_UPDATE_TOOL_NAME } from '../../tools/TaskUpdateTool/constants.js' |
| import { TEAM_CREATE_TOOL_NAME } from '../../tools/TeamCreateTool/constants.js' |
| import { TEAM_DELETE_TOOL_NAME } from '../../tools/TeamDeleteTool/constants.js' |
| import type { Message } from '../../types/message.js' |
| import type { PermissionDecision } from '../../types/permissions.js' |
| import { |
| createAssistantAPIErrorMessage, |
| createUserMessage, |
| } from '../../utils/messages.js' |
| import { evictTaskOutput } from '../../utils/task/diskOutput.js' |
| import { evictTerminalTask } from '../../utils/task/framework.js' |
| import { tokenCountWithEstimation } from '../../utils/tokens.js' |
| import { createAbortController } from '../abortController.js' |
| import { type AgentContext, runWithAgentContext } from '../agentContext.js' |
| import { count } from '../array.js' |
| import { logForDebugging } from '../debug.js' |
| import { cloneFileStateCache } from '../fileStateCache.js' |
| import { |
| SUBAGENT_REJECT_MESSAGE, |
| SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX, |
| } from '../messages.js' |
| import type { ModelAlias } from '../model/aliases.js' |
| import { |
| applyPermissionUpdates, |
| persistPermissionUpdates, |
| } from '../permissions/PermissionUpdate.js' |
| import type { PermissionUpdate } from '../permissions/PermissionUpdateSchema.js' |
| import { hasPermissionsToUseTool } from '../permissions/permissions.js' |
| import { emitTaskTerminatedSdk } from '../sdkEventQueue.js' |
| import { sleep } from '../sleep.js' |
| import { jsonStringify } from '../slowOperations.js' |
| import { asSystemPrompt } from '../systemPromptType.js' |
| import { claimTask, listTasks, type Task, updateTask } from '../tasks.js' |
| import type { TeammateContext } from '../teammateContext.js' |
| import { runWithTeammateContext } from '../teammateContext.js' |
| import { |
| createIdleNotification, |
| getLastPeerDmSummary, |
| isPermissionResponse, |
| isShutdownRequest, |
| markMessageAsReadByIndex, |
| readMailbox, |
| writeToMailbox, |
| } from '../teammateMailbox.js' |
| import { unregisterAgent as unregisterPerfettoAgent } from '../telemetry/perfettoTracing.js' |
| import { createContentReplacementState } from '../toolResultStorage.js' |
| import { TEAM_LEAD_NAME } from './constants.js' |
| import { |
| getLeaderSetToolPermissionContext, |
| getLeaderToolUseConfirmQueue, |
| } from './leaderPermissionBridge.js' |
| import { |
| createPermissionRequest, |
| sendPermissionRequestViaMailbox, |
| } from './permissionSync.js' |
| import { TEAMMATE_SYSTEM_PROMPT_ADDENDUM } from './teammatePromptAddendum.js' |
|
|
| type SetAppStateFn = (updater: (prev: AppState) => AppState) => void |
|
|
| const PERMISSION_POLL_INTERVAL_MS = 500 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function createInProcessCanUseTool( |
| identity: TeammateIdentity, |
| abortController: AbortController, |
| onPermissionWaitMs?: (waitMs: number) => void, |
| ): CanUseToolFn { |
| return async ( |
| tool, |
| input, |
| toolUseContext, |
| assistantMessage, |
| toolUseID, |
| forceDecision, |
| ) => { |
| const result = |
| forceDecision ?? |
| (await hasPermissionsToUseTool( |
| tool, |
| input, |
| toolUseContext, |
| assistantMessage, |
| toolUseID, |
| )) |
|
|
| |
| if (result.behavior !== 'ask') { |
| return result |
| } |
|
|
| |
| |
| |
| if ( |
| feature('BASH_CLASSIFIER') && |
| tool.name === BASH_TOOL_NAME && |
| result.pendingClassifierCheck |
| ) { |
| const classifierDecision = await awaitClassifierAutoApproval( |
| result.pendingClassifierCheck, |
| abortController.signal, |
| toolUseContext.options.isNonInteractiveSession, |
| ) |
| if (classifierDecision) { |
| return { |
| behavior: 'allow', |
| updatedInput: input as Record<string, unknown>, |
| decisionReason: classifierDecision, |
| } |
| } |
| } |
|
|
| |
| if (abortController.signal.aborted) { |
| return { behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE } |
| } |
|
|
| const appState = toolUseContext.getAppState() |
|
|
| const description = await (tool as Tool).description(input as never, { |
| isNonInteractiveSession: toolUseContext.options.isNonInteractiveSession, |
| toolPermissionContext: appState.toolPermissionContext, |
| tools: toolUseContext.options.tools, |
| }) |
|
|
| if (abortController.signal.aborted) { |
| return { behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE } |
| } |
|
|
| const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue() |
|
|
| |
| if (setToolUseConfirmQueue) { |
| return new Promise<PermissionDecision>(resolve => { |
| let decisionMade = false |
| const permissionStartMs = Date.now() |
|
|
| |
| |
| const reportPermissionWait = () => { |
| onPermissionWaitMs?.(Date.now() - permissionStartMs) |
| } |
|
|
| const onAbortListener = () => { |
| if (decisionMade) return |
| decisionMade = true |
| reportPermissionWait() |
| resolve({ behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE }) |
| setToolUseConfirmQueue(queue => |
| queue.filter(item => item.toolUseID !== toolUseID), |
| ) |
| } |
|
|
| abortController.signal.addEventListener('abort', onAbortListener, { |
| once: true, |
| }) |
|
|
| setToolUseConfirmQueue(queue => [ |
| ...queue, |
| { |
| assistantMessage, |
| tool: tool as Tool, |
| description, |
| input, |
| toolUseContext, |
| toolUseID, |
| permissionResult: result, |
| permissionPromptStartTimeMs: permissionStartMs, |
| workerBadge: identity.color |
| ? { name: identity.agentName, color: identity.color } |
| : undefined, |
| onUserInteraction() { |
| |
| }, |
| onAbort() { |
| if (decisionMade) return |
| decisionMade = true |
| abortController.signal.removeEventListener( |
| 'abort', |
| onAbortListener, |
| ) |
| reportPermissionWait() |
| resolve({ behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE }) |
| }, |
| async onAllow( |
| updatedInput: Record<string, unknown>, |
| permissionUpdates: PermissionUpdate[], |
| feedback?: string, |
| contentBlocks?: ContentBlockParam[], |
| ) { |
| if (decisionMade) return |
| decisionMade = true |
| abortController.signal.removeEventListener( |
| 'abort', |
| onAbortListener, |
| ) |
| reportPermissionWait() |
| persistPermissionUpdates(permissionUpdates) |
| |
| if (permissionUpdates.length > 0) { |
| const setToolPermissionContext = |
| getLeaderSetToolPermissionContext() |
| if (setToolPermissionContext) { |
| const currentAppState = toolUseContext.getAppState() |
| const updatedContext = applyPermissionUpdates( |
| currentAppState.toolPermissionContext, |
| permissionUpdates, |
| ) |
| |
| |
| |
| setToolPermissionContext(updatedContext, { |
| preserveMode: true, |
| }) |
| } |
| } |
| const trimmedFeedback = feedback?.trim() |
| resolve({ |
| behavior: 'allow', |
| updatedInput, |
| userModified: false, |
| acceptFeedback: trimmedFeedback || undefined, |
| ...(contentBlocks && |
| contentBlocks.length > 0 && { contentBlocks }), |
| }) |
| }, |
| onReject(feedback?: string, contentBlocks?: ContentBlockParam[]) { |
| if (decisionMade) return |
| decisionMade = true |
| abortController.signal.removeEventListener( |
| 'abort', |
| onAbortListener, |
| ) |
| reportPermissionWait() |
| const message = feedback |
| ? `${SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX}${feedback}` |
| : SUBAGENT_REJECT_MESSAGE |
| resolve({ behavior: 'ask', message, contentBlocks }) |
| }, |
| async recheckPermission() { |
| if (decisionMade) return |
| const freshResult = await hasPermissionsToUseTool( |
| tool, |
| input, |
| toolUseContext, |
| assistantMessage, |
| toolUseID, |
| ) |
| if (freshResult.behavior === 'allow') { |
| decisionMade = true |
| abortController.signal.removeEventListener( |
| 'abort', |
| onAbortListener, |
| ) |
| reportPermissionWait() |
| setToolUseConfirmQueue(queue => |
| queue.filter(item => item.toolUseID !== toolUseID), |
| ) |
| resolve({ |
| ...freshResult, |
| updatedInput: input, |
| userModified: false, |
| }) |
| } |
| }, |
| }, |
| ]) |
| }) |
| } |
|
|
| |
| return new Promise<PermissionDecision>(resolve => { |
| const request = createPermissionRequest({ |
| toolName: (tool as Tool).name, |
| toolUseId: toolUseID, |
| input, |
| description, |
| permissionSuggestions: result.suggestions, |
| workerId: identity.agentId, |
| workerName: identity.agentName, |
| workerColor: identity.color, |
| teamName: identity.teamName, |
| }) |
|
|
| |
| registerPermissionCallback({ |
| requestId: request.id, |
| toolUseId: toolUseID, |
| onAllow( |
| updatedInput: Record<string, unknown> | undefined, |
| permissionUpdates: PermissionUpdate[], |
| _feedback?: string, |
| contentBlocks?: ContentBlockParam[], |
| ) { |
| cleanup() |
| persistPermissionUpdates(permissionUpdates) |
| const finalInput = |
| updatedInput && Object.keys(updatedInput).length > 0 |
| ? updatedInput |
| : input |
| resolve({ |
| behavior: 'allow', |
| updatedInput: finalInput, |
| userModified: false, |
| ...(contentBlocks && contentBlocks.length > 0 && { contentBlocks }), |
| }) |
| }, |
| onReject(feedback?: string, contentBlocks?: ContentBlockParam[]) { |
| cleanup() |
| const message = feedback |
| ? `${SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX}${feedback}` |
| : SUBAGENT_REJECT_MESSAGE |
| resolve({ behavior: 'ask', message, contentBlocks }) |
| }, |
| }) |
|
|
| |
| void sendPermissionRequestViaMailbox(request) |
|
|
| |
| const pollInterval = setInterval( |
| async (abortController, cleanup, resolve, identity, request) => { |
| if (abortController.signal.aborted) { |
| cleanup() |
| resolve({ behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE }) |
| return |
| } |
|
|
| const allMessages = await readMailbox( |
| identity.agentName, |
| identity.teamName, |
| ) |
| for (let i = 0; i < allMessages.length; i++) { |
| const msg = allMessages[i] |
| if (msg && !msg.read) { |
| const parsed = isPermissionResponse(msg.text) |
| if (parsed && parsed.request_id === request.id) { |
| await markMessageAsReadByIndex( |
| identity.agentName, |
| identity.teamName, |
| i, |
| ) |
| if (parsed.subtype === 'success') { |
| processMailboxPermissionResponse({ |
| requestId: parsed.request_id, |
| decision: 'approved', |
| updatedInput: parsed.response?.updated_input, |
| permissionUpdates: parsed.response?.permission_updates, |
| }) |
| } else { |
| processMailboxPermissionResponse({ |
| requestId: parsed.request_id, |
| decision: 'rejected', |
| feedback: parsed.error, |
| }) |
| } |
| return |
| } |
| } |
| } |
| }, |
| PERMISSION_POLL_INTERVAL_MS, |
| abortController, |
| cleanup, |
| resolve, |
| identity, |
| request, |
| ) |
|
|
| const onAbortListener = () => { |
| cleanup() |
| resolve({ behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE }) |
| } |
|
|
| abortController.signal.addEventListener('abort', onAbortListener, { |
| once: true, |
| }) |
|
|
| function cleanup() { |
| clearInterval(pollInterval) |
| unregisterPermissionCallback(request.id) |
| abortController.signal.removeEventListener('abort', onAbortListener) |
| } |
| }) |
| } |
| } |
|
|
| |
| |
| |
| |
| function formatAsTeammateMessage( |
| from: string, |
| content: string, |
| color?: string, |
| summary?: string, |
| ): string { |
| const colorAttr = color ? ` color="${color}"` : '' |
| const summaryAttr = summary ? ` summary="${summary}"` : '' |
| return `<${TEAMMATE_MESSAGE_TAG} teammate_id="${from}"${colorAttr}${summaryAttr}>\n${content}\n</${TEAMMATE_MESSAGE_TAG}>` |
| } |
|
|
| |
| |
| |
| export type InProcessRunnerConfig = { |
| |
| identity: TeammateIdentity |
| |
| taskId: string |
| |
| prompt: string |
| |
| agentDefinition?: CustomAgentDefinition |
| |
| teammateContext: TeammateContext |
| |
| toolUseContext: ToolUseContext |
| |
| abortController: AbortController |
| |
| model?: string |
| |
| systemPrompt?: string |
| |
| systemPromptMode?: 'default' | 'replace' | 'append' |
| |
| allowedTools?: string[] |
| |
| |
| allowPermissionPrompts?: boolean |
| |
| description?: string |
| |
| |
| invokingRequestId?: string |
| } |
|
|
| |
| |
| |
| export type InProcessRunnerResult = { |
| |
| success: boolean |
| |
| error?: string |
| |
| messages: Message[] |
| } |
|
|
| |
| |
| |
| function updateTaskState( |
| taskId: string, |
| updater: (task: InProcessTeammateTaskState) => InProcessTeammateTaskState, |
| setAppState: SetAppStateFn, |
| ): void { |
| setAppState(prev => { |
| const task = prev.tasks[taskId] |
| if (!task || task.type !== 'in_process_teammate') { |
| return prev |
| } |
| const updated = updater(task) |
| if (updated === task) { |
| return prev |
| } |
| return { |
| ...prev, |
| tasks: { |
| ...prev.tasks, |
| [taskId]: updated, |
| }, |
| } |
| }) |
| } |
|
|
| |
| |
| |
| |
| async function sendMessageToLeader( |
| from: string, |
| text: string, |
| color: string | undefined, |
| teamName: string, |
| ): Promise<void> { |
| await writeToMailbox( |
| TEAM_LEAD_NAME, |
| { |
| from, |
| text, |
| timestamp: new Date().toISOString(), |
| color, |
| }, |
| teamName, |
| ) |
| } |
|
|
| |
| |
| |
| |
| async function sendIdleNotification( |
| agentName: string, |
| agentColor: string | undefined, |
| teamName: string, |
| options?: { |
| idleReason?: 'available' | 'interrupted' | 'failed' |
| summary?: string |
| completedTaskId?: string |
| completedStatus?: 'resolved' | 'blocked' | 'failed' |
| failureReason?: string |
| }, |
| ): Promise<void> { |
| const notification = createIdleNotification(agentName, options) |
|
|
| await sendMessageToLeader( |
| agentName, |
| jsonStringify(notification), |
| agentColor, |
| teamName, |
| ) |
| } |
|
|
| |
| |
| |
| |
| function findAvailableTask(tasks: Task[]): Task | undefined { |
| const unresolvedTaskIds = new Set( |
| tasks.filter(t => t.status !== 'completed').map(t => t.id), |
| ) |
|
|
| return tasks.find(task => { |
| if (task.status !== 'pending') return false |
| if (task.owner) return false |
| return task.blockedBy.every(id => !unresolvedTaskIds.has(id)) |
| }) |
| } |
|
|
| |
| |
| |
| function formatTaskAsPrompt(task: Task): string { |
| let prompt = `Complete all open tasks. Start with task #${task.id}: \n\n ${task.subject}` |
|
|
| if (task.description) { |
| prompt += `\n\n${task.description}` |
| } |
|
|
| return prompt |
| } |
|
|
| |
| |
| |
| |
| async function tryClaimNextTask( |
| taskListId: string, |
| agentName: string, |
| ): Promise<string | undefined> { |
| try { |
| const tasks = await listTasks(taskListId) |
| const availableTask = findAvailableTask(tasks) |
|
|
| if (!availableTask) { |
| return undefined |
| } |
|
|
| const result = await claimTask(taskListId, availableTask.id, agentName) |
|
|
| if (!result.success) { |
| logForDebugging( |
| `[inProcessRunner] Failed to claim task #${availableTask.id}: ${result.reason}`, |
| ) |
| return undefined |
| } |
|
|
| |
| await updateTask(taskListId, availableTask.id, { status: 'in_progress' }) |
|
|
| logForDebugging( |
| `[inProcessRunner] Claimed task #${availableTask.id}: ${availableTask.subject}`, |
| ) |
|
|
| return formatTaskAsPrompt(availableTask) |
| } catch (err) { |
| logForDebugging(`[inProcessRunner] Error checking task list: ${err}`) |
| return undefined |
| } |
| } |
|
|
| |
| |
| |
| type WaitResult = |
| | { |
| type: 'shutdown_request' |
| request: ReturnType<typeof isShutdownRequest> |
| originalMessage: string |
| } |
| | { |
| type: 'new_message' |
| message: string |
| from: string |
| color?: string |
| summary?: string |
| } |
| | { |
| type: 'aborted' |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function waitForNextPromptOrShutdown( |
| identity: TeammateIdentity, |
| abortController: AbortController, |
| taskId: string, |
| getAppState: () => AppState, |
| setAppState: SetAppStateFn, |
| taskListId: string, |
| ): Promise<WaitResult> { |
| const POLL_INTERVAL_MS = 500 |
|
|
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} starting poll loop (abort=${abortController.signal.aborted})`, |
| ) |
|
|
| let pollCount = 0 |
| while (!abortController.signal.aborted) { |
| |
| const appState = getAppState() |
| const task = appState.tasks[taskId] |
| if ( |
| task && |
| task.type === 'in_process_teammate' && |
| task.pendingUserMessages.length > 0 |
| ) { |
| const message = task.pendingUserMessages[0]! |
| |
| setAppState(prev => { |
| const prevTask = prev.tasks[taskId] |
| if (!prevTask || prevTask.type !== 'in_process_teammate') { |
| return prev |
| } |
| return { |
| ...prev, |
| tasks: { |
| ...prev.tasks, |
| [taskId]: { |
| ...prevTask, |
| pendingUserMessages: prevTask.pendingUserMessages.slice(1), |
| }, |
| }, |
| } |
| }) |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} found pending user message (poll #${pollCount})`, |
| ) |
| return { |
| type: 'new_message', |
| message, |
| from: 'user', |
| } |
| } |
|
|
| |
| if (pollCount > 0) { |
| await sleep(POLL_INTERVAL_MS) |
| } |
| pollCount++ |
|
|
| |
| if (abortController.signal.aborted) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} aborted while waiting (poll #${pollCount})`, |
| ) |
| return { type: 'aborted' } |
| } |
|
|
| |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} poll #${pollCount}: checking mailbox`, |
| ) |
| try { |
| |
| |
| |
| const allMessages = await readMailbox( |
| identity.agentName, |
| identity.teamName, |
| ) |
|
|
| |
| |
| |
| let shutdownIndex = -1 |
| let shutdownParsed: ReturnType<typeof isShutdownRequest> = null |
| for (let i = 0; i < allMessages.length; i++) { |
| const m = allMessages[i] |
| if (m && !m.read) { |
| const parsed = isShutdownRequest(m.text) |
| if (parsed) { |
| shutdownIndex = i |
| shutdownParsed = parsed |
| break |
| } |
| } |
| } |
|
|
| if (shutdownIndex !== -1) { |
| const msg = allMessages[shutdownIndex]! |
| const skippedUnread = count( |
| allMessages.slice(0, shutdownIndex), |
| m => !m.read, |
| ) |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} received shutdown request from ${shutdownParsed?.from} (prioritized over ${skippedUnread} unread messages)`, |
| ) |
| await markMessageAsReadByIndex( |
| identity.agentName, |
| identity.teamName, |
| shutdownIndex, |
| ) |
| return { |
| type: 'shutdown_request', |
| request: shutdownParsed, |
| originalMessage: msg.text, |
| } |
| } |
|
|
| |
| |
| |
| |
| let selectedIndex = -1 |
|
|
| |
| for (let i = 0; i < allMessages.length; i++) { |
| const m = allMessages[i] |
| if (m && !m.read && m.from === TEAM_LEAD_NAME) { |
| selectedIndex = i |
| break |
| } |
| } |
|
|
| |
| if (selectedIndex === -1) { |
| selectedIndex = allMessages.findIndex(m => !m.read) |
| } |
|
|
| if (selectedIndex !== -1) { |
| const msg = allMessages[selectedIndex] |
| if (msg) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} received new message from ${msg.from} (index ${selectedIndex})`, |
| ) |
| await markMessageAsReadByIndex( |
| identity.agentName, |
| identity.teamName, |
| selectedIndex, |
| ) |
| return { |
| type: 'new_message', |
| message: msg.text, |
| from: msg.from, |
| color: msg.color, |
| summary: msg.summary, |
| } |
| } |
| } |
| } catch (err) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} poll error: ${err}`, |
| ) |
| |
| } |
|
|
| |
| const taskPrompt = await tryClaimNextTask(taskListId, identity.agentName) |
| if (taskPrompt) { |
| return { |
| type: 'new_message', |
| message: taskPrompt, |
| from: 'task-list', |
| } |
| } |
| } |
|
|
| logForDebugging( |
| `[inProcessRunner] ${identity.agentName} exiting poll loop (abort=${abortController.signal.aborted}, polls=${pollCount})`, |
| ) |
| return { type: 'aborted' } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function runInProcessTeammate( |
| config: InProcessRunnerConfig, |
| ): Promise<InProcessRunnerResult> { |
| const { |
| identity, |
| taskId, |
| prompt, |
| description, |
| agentDefinition, |
| teammateContext, |
| toolUseContext, |
| abortController, |
| model, |
| systemPrompt, |
| systemPromptMode, |
| allowedTools, |
| allowPermissionPrompts, |
| invokingRequestId, |
| } = config |
| const { setAppState } = toolUseContext |
|
|
| logForDebugging( |
| `[inProcessRunner] Starting agent loop for ${identity.agentId}`, |
| ) |
|
|
| |
| const agentContext: AgentContext = { |
| agentId: identity.agentId, |
| parentSessionId: identity.parentSessionId, |
| agentName: identity.agentName, |
| teamName: identity.teamName, |
| agentColor: identity.color, |
| planModeRequired: identity.planModeRequired, |
| isTeamLead: false, |
| agentType: 'teammate', |
| invokingRequestId, |
| invocationKind: 'spawn', |
| invocationEmitted: false, |
| } |
|
|
| |
| let teammateSystemPrompt: string |
| if (systemPromptMode === 'replace' && systemPrompt) { |
| teammateSystemPrompt = systemPrompt |
| } else { |
| const fullSystemPromptParts = await getSystemPrompt( |
| toolUseContext.options.tools, |
| toolUseContext.options.mainLoopModel, |
| undefined, |
| toolUseContext.options.mcpClients, |
| ) |
|
|
| const systemPromptParts = [ |
| ...fullSystemPromptParts, |
| TEAMMATE_SYSTEM_PROMPT_ADDENDUM, |
| ] |
|
|
| |
| if (agentDefinition) { |
| const customPrompt = agentDefinition.getSystemPrompt() |
| if (customPrompt) { |
| systemPromptParts.push(`\n# Custom Agent Instructions\n${customPrompt}`) |
| } |
|
|
| |
| if (agentDefinition.memory) { |
| logEvent('tengu_agent_memory_loaded', { |
| ...(process.env.USER_TYPE === 'ant' |
| ? { |
| agent_type: |
| agentDefinition.agentType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| } |
| : {}), |
| scope: |
| agentDefinition.memory as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| source: |
| 'in-process-teammate' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| }) |
| } |
| } |
|
|
| |
| if (systemPromptMode === 'append' && systemPrompt) { |
| systemPromptParts.push(systemPrompt) |
| } |
|
|
| teammateSystemPrompt = systemPromptParts.join('\n') |
| } |
|
|
| |
| |
| |
| const resolvedAgentDefinition: CustomAgentDefinition = { |
| agentType: identity.agentName, |
| whenToUse: `In-process teammate: ${identity.agentName}`, |
| getSystemPrompt: () => teammateSystemPrompt, |
| |
| |
| |
| tools: agentDefinition?.tools |
| ? [ |
| ...new Set([ |
| ...agentDefinition.tools, |
| SEND_MESSAGE_TOOL_NAME, |
| TEAM_CREATE_TOOL_NAME, |
| TEAM_DELETE_TOOL_NAME, |
| TASK_CREATE_TOOL_NAME, |
| TASK_GET_TOOL_NAME, |
| TASK_LIST_TOOL_NAME, |
| TASK_UPDATE_TOOL_NAME, |
| ]), |
| ] |
| : ['*'], |
| source: 'projectSettings', |
| permissionMode: 'default', |
| |
| |
| ...(agentDefinition?.model ? { model: agentDefinition.model } : {}), |
| } |
|
|
| |
| const allMessages: Message[] = [] |
| |
| const wrappedInitialPrompt = formatAsTeammateMessage( |
| 'team-lead', |
| prompt, |
| undefined, |
| description, |
| ) |
| let currentPrompt = wrappedInitialPrompt |
| let shouldExit = false |
|
|
| |
| |
| |
| |
| await tryClaimNextTask(identity.parentSessionId, identity.agentName) |
|
|
| try { |
| |
| updateTaskState( |
| taskId, |
| task => ({ |
| ...task, |
| messages: appendCappedMessage( |
| task.messages, |
| createUserMessage({ content: wrappedInitialPrompt }), |
| ), |
| }), |
| setAppState, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| let teammateReplacementState = toolUseContext.contentReplacementState |
| ? createContentReplacementState() |
| : undefined |
|
|
| |
| while (!abortController.signal.aborted && !shouldExit) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} processing prompt: ${currentPrompt.substring(0, 50)}...`, |
| ) |
|
|
| |
| |
| |
| const currentWorkAbortController = createAbortController() |
|
|
| |
| updateTaskState( |
| taskId, |
| task => ({ ...task, currentWorkAbortController }), |
| setAppState, |
| ) |
|
|
| |
| |
| |
| const userMessage = createUserMessage({ content: currentPrompt }) |
| const promptMessages: Message[] = [userMessage] |
|
|
| |
| let contextMessages = allMessages |
| const tokenCount = tokenCountWithEstimation(allMessages) |
| if ( |
| tokenCount > |
| getAutoCompactThreshold(toolUseContext.options.mainLoopModel) |
| ) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} compacting history (${tokenCount} tokens)`, |
| ) |
| |
| |
| |
| const isolatedContext: ToolUseContext = { |
| ...toolUseContext, |
| readFileState: cloneFileStateCache(toolUseContext.readFileState), |
| onCompactProgress: undefined, |
| setStreamMode: undefined, |
| } |
| const compactedSummary = await compactConversation( |
| allMessages, |
| isolatedContext, |
| { |
| systemPrompt: asSystemPrompt([]), |
| userContext: {}, |
| systemContext: {}, |
| toolUseContext: isolatedContext, |
| forkContextMessages: [], |
| }, |
| true, |
| undefined, |
| true, |
| ) |
| contextMessages = buildPostCompactMessages(compactedSummary) |
| |
| |
| resetMicrocompactState() |
| |
| |
| |
| if (teammateReplacementState) { |
| teammateReplacementState = createContentReplacementState() |
| } |
| |
| allMessages.length = 0 |
| allMessages.push(...contextMessages) |
|
|
| |
| |
| |
| updateTaskState( |
| taskId, |
| task => ({ ...task, messages: [...contextMessages, userMessage] }), |
| setAppState, |
| ) |
| } |
|
|
| |
| |
| const forkContextMessages = |
| contextMessages.length > 0 ? [...contextMessages] : undefined |
|
|
| |
| |
| allMessages.push(userMessage) |
|
|
| |
| const tracker = createProgressTracker() |
| const resolveActivity = createActivityDescriptionResolver( |
| toolUseContext.options.tools, |
| ) |
| const iterationMessages: Message[] = [] |
|
|
| |
| const currentAppState = toolUseContext.getAppState() |
| const currentTask = currentAppState.tasks[taskId] |
| const currentPermissionMode = |
| currentTask && currentTask.type === 'in_process_teammate' |
| ? currentTask.permissionMode |
| : 'default' |
| const iterationAgentDefinition = { |
| ...resolvedAgentDefinition, |
| permissionMode: currentPermissionMode, |
| } |
|
|
| |
| let workWasAborted = false |
|
|
| |
| await runWithTeammateContext(teammateContext, async () => { |
| return runWithAgentContext(agentContext, async () => { |
| |
| updateTaskState( |
| taskId, |
| task => ({ ...task, status: 'running', isIdle: false }), |
| setAppState, |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| for await (const message of runAgent({ |
| agentDefinition: iterationAgentDefinition, |
| promptMessages, |
| toolUseContext, |
| canUseTool: createInProcessCanUseTool( |
| identity, |
| currentWorkAbortController, |
| (waitMs: number) => { |
| updateTaskState( |
| taskId, |
| task => ({ |
| ...task, |
| totalPausedMs: (task.totalPausedMs ?? 0) + waitMs, |
| }), |
| setAppState, |
| ) |
| }, |
| ), |
| isAsync: true, |
| canShowPermissionPrompts: allowPermissionPrompts ?? true, |
| forkContextMessages, |
| querySource: 'agent:custom', |
| override: { abortController: currentWorkAbortController }, |
| model: model as ModelAlias | undefined, |
| preserveToolUseResults: true, |
| availableTools: toolUseContext.options.tools, |
| allowedTools, |
| contentReplacementState: teammateReplacementState, |
| })) { |
| |
| if (abortController.signal.aborted) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} lifecycle aborted`, |
| ) |
| break |
| } |
|
|
| |
| if (currentWorkAbortController.signal.aborted) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} current work aborted (Escape pressed)`, |
| ) |
| workWasAborted = true |
| break |
| } |
|
|
| iterationMessages.push(message) |
| allMessages.push(message) |
|
|
| updateProgressFromMessage( |
| tracker, |
| message, |
| resolveActivity, |
| toolUseContext.options.tools, |
| ) |
| const progress = getProgressUpdate(tracker) |
|
|
| updateTaskState( |
| taskId, |
| task => { |
| |
| let inProgressToolUseIDs = task.inProgressToolUseIDs |
| if (message.type === 'assistant') { |
| for (const block of message.message.content) { |
| if (block.type === 'tool_use') { |
| inProgressToolUseIDs = new Set([ |
| ...(inProgressToolUseIDs ?? []), |
| block.id, |
| ]) |
| } |
| } |
| } else if (message.type === 'user') { |
| const content = message.message.content |
| if (Array.isArray(content)) { |
| for (const block of content) { |
| if ( |
| typeof block === 'object' && |
| 'type' in block && |
| block.type === 'tool_result' |
| ) { |
| if (inProgressToolUseIDs) { |
| inProgressToolUseIDs = new Set(inProgressToolUseIDs) |
| inProgressToolUseIDs.delete(block.tool_use_id) |
| } |
| } |
| } |
| } |
| } |
|
|
| return { |
| ...task, |
| progress, |
| messages: appendCappedMessage(task.messages, message), |
| inProgressToolUseIDs, |
| } |
| }, |
| setAppState, |
| ) |
| } |
|
|
| return { success: true, messages: iterationMessages } |
| }) |
| }) |
|
|
| |
| updateTaskState( |
| taskId, |
| task => ({ ...task, currentWorkAbortController: undefined }), |
| setAppState, |
| ) |
|
|
| |
| if (abortController.signal.aborted) { |
| break |
| } |
|
|
| |
| if (workWasAborted) { |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} work interrupted, returning to idle`, |
| ) |
|
|
| |
| const interruptMessage = createAssistantAPIErrorMessage({ |
| content: ERROR_MESSAGE_USER_ABORT, |
| }) |
| updateTaskState( |
| taskId, |
| task => ({ |
| ...task, |
| messages: appendCappedMessage(task.messages, interruptMessage), |
| }), |
| setAppState, |
| ) |
| } |
|
|
| |
| const prevAppState = toolUseContext.getAppState() |
| const prevTask = prevAppState.tasks[taskId] |
| const wasAlreadyIdle = |
| prevTask?.type === 'in_process_teammate' && prevTask.isIdle |
|
|
| |
| updateTaskState( |
| taskId, |
| task => { |
| |
| task.onIdleCallbacks?.forEach(cb => cb()) |
| return { ...task, isIdle: true, onIdleCallbacks: [] } |
| }, |
| setAppState, |
| ) |
|
|
| |
| |
| |
|
|
| |
| if (!wasAlreadyIdle) { |
| await sendIdleNotification( |
| identity.agentName, |
| identity.color, |
| identity.teamName, |
| { |
| idleReason: workWasAborted ? 'interrupted' : 'available', |
| summary: getLastPeerDmSummary(allMessages), |
| }, |
| ) |
| } else { |
| logForDebugging( |
| `[inProcessRunner] Skipping duplicate idle notification for ${identity.agentName}`, |
| ) |
| } |
|
|
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} finished prompt, waiting for next`, |
| ) |
|
|
| |
| const waitResult = await waitForNextPromptOrShutdown( |
| identity, |
| abortController, |
| taskId, |
| toolUseContext.getAppState, |
| setAppState, |
| identity.parentSessionId, |
| ) |
|
|
| switch (waitResult.type) { |
| case 'shutdown_request': |
| |
| |
| |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} received shutdown request - passing to model`, |
| ) |
| currentPrompt = formatAsTeammateMessage( |
| waitResult.request?.from || 'team-lead', |
| waitResult.originalMessage, |
| ) |
| |
| appendTeammateMessage( |
| taskId, |
| createUserMessage({ content: currentPrompt }), |
| setAppState, |
| ) |
| break |
|
|
| case 'new_message': |
| |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} received new message from ${waitResult.from}`, |
| ) |
| |
| |
| if (waitResult.from === 'user') { |
| currentPrompt = waitResult.message |
| } else { |
| currentPrompt = formatAsTeammateMessage( |
| waitResult.from, |
| waitResult.message, |
| waitResult.color, |
| waitResult.summary, |
| ) |
| |
| |
| |
| appendTeammateMessage( |
| taskId, |
| createUserMessage({ content: currentPrompt }), |
| setAppState, |
| ) |
| } |
| break |
|
|
| case 'aborted': |
| logForDebugging( |
| `[inProcessRunner] ${identity.agentId} aborted while waiting`, |
| ) |
| shouldExit = true |
| break |
| } |
| } |
|
|
| |
| let alreadyTerminal = false |
| let toolUseId: string | undefined |
| updateTaskState( |
| taskId, |
| task => { |
| |
| |
| |
| if (task.status !== 'running') { |
| alreadyTerminal = true |
| return task |
| } |
| toolUseId = task.toolUseId |
| task.onIdleCallbacks?.forEach(cb => cb()) |
| task.unregisterCleanup?.() |
| return { |
| ...task, |
| status: 'completed' as const, |
| notified: true, |
| endTime: Date.now(), |
| messages: task.messages?.length ? [task.messages.at(-1)!] : undefined, |
| pendingUserMessages: [], |
| inProgressToolUseIDs: undefined, |
| abortController: undefined, |
| unregisterCleanup: undefined, |
| currentWorkAbortController: undefined, |
| onIdleCallbacks: [], |
| } |
| }, |
| setAppState, |
| ) |
| void evictTaskOutput(taskId) |
| |
| evictTerminalTask(taskId, setAppState) |
| |
| |
| if (!alreadyTerminal) { |
| emitTaskTerminatedSdk(taskId, 'completed', { |
| toolUseId, |
| summary: identity.agentId, |
| }) |
| } |
|
|
| unregisterPerfettoAgent(identity.agentId) |
| return { success: true, messages: allMessages } |
| } catch (error) { |
| const errorMessage = |
| error instanceof Error ? error.message : 'Unknown error' |
|
|
| logForDebugging( |
| `[inProcessRunner] Agent ${identity.agentId} failed: ${errorMessage}`, |
| ) |
|
|
| |
| let alreadyTerminal = false |
| let toolUseId: string | undefined |
| updateTaskState( |
| taskId, |
| task => { |
| if (task.status !== 'running') { |
| alreadyTerminal = true |
| return task |
| } |
| toolUseId = task.toolUseId |
| task.onIdleCallbacks?.forEach(cb => cb()) |
| task.unregisterCleanup?.() |
| return { |
| ...task, |
| status: 'failed' as const, |
| notified: true, |
| error: errorMessage, |
| isIdle: true, |
| endTime: Date.now(), |
| onIdleCallbacks: [], |
| messages: task.messages?.length ? [task.messages.at(-1)!] : undefined, |
| pendingUserMessages: [], |
| inProgressToolUseIDs: undefined, |
| abortController: undefined, |
| unregisterCleanup: undefined, |
| currentWorkAbortController: undefined, |
| } |
| }, |
| setAppState, |
| ) |
| void evictTaskOutput(taskId) |
| |
| evictTerminalTask(taskId, setAppState) |
| |
| if (!alreadyTerminal) { |
| emitTaskTerminatedSdk(taskId, 'failed', { |
| toolUseId, |
| summary: identity.agentId, |
| }) |
| } |
|
|
| |
| await sendIdleNotification( |
| identity.agentName, |
| identity.color, |
| identity.teamName, |
| { |
| idleReason: 'failed', |
| completedStatus: 'failed', |
| failureReason: errorMessage, |
| }, |
| ) |
|
|
| unregisterPerfettoAgent(identity.agentId) |
| return { |
| success: false, |
| error: errorMessage, |
| messages: allMessages, |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function startInProcessTeammate(config: InProcessRunnerConfig): void { |
| |
| |
| |
| const agentId = config.identity.agentId |
| void runInProcessTeammate(config).catch(error => { |
| logForDebugging(`[inProcessRunner] Unhandled error in ${agentId}: ${error}`) |
| }) |
| } |
|
|