import { randomUUID } from 'crypto'; import type { AgentActionMode, TaskRef } from '@shared/types/team'; import type { OpenCodeBridgeRuntimeSnapshot, OpenCodeLaunchTeamCommandBody, OpenCodeLaunchTeamCommandData, OpenCodeReconcileTeamCommandBody, OpenCodeSendMessageCommandBody, OpenCodeSendMessageCommandData, OpenCodeStopTeamCommandBody, OpenCodeStopTeamCommandData, OpenCodeTeamMemberLaunchBridgeState, } from '../opencode/bridge/OpenCodeBridgeCommandContract'; import type { OpenCodeTeamLaunchReadiness } from '../opencode/readiness/OpenCodeTeamLaunchReadiness'; import type { TeamLaunchRuntimeAdapter, TeamRuntimeLaunchInput, TeamRuntimeLaunchResult, TeamRuntimeMemberLaunchEvidence, TeamRuntimeMemberStopEvidence, TeamRuntimePrepareResult, TeamRuntimeReconcileInput, TeamRuntimeReconcileResult, TeamRuntimeStopInput, TeamRuntimeStopResult, } from './TeamRuntimeAdapter'; export interface OpenCodeTeamRuntimeBridgePort { checkOpenCodeTeamLaunchReadiness(input: { projectPath: string; selectedModel: string | null; requireExecutionProbe: boolean; }): Promise; getLastOpenCodeRuntimeSnapshot?(projectPath: string): OpenCodeBridgeRuntimeSnapshot | null; launchOpenCodeTeam?(input: OpenCodeLaunchTeamCommandBody): Promise; reconcileOpenCodeTeam?( input: OpenCodeReconcileTeamCommandBody ): Promise; stopOpenCodeTeam?(input: OpenCodeStopTeamCommandBody): Promise; sendOpenCodeTeamMessage?( input: OpenCodeSendMessageCommandBody ): Promise; } export interface OpenCodeTeamRuntimeMessageInput { runId?: string; teamName: string; laneId: string; memberName: string; cwd: string; text: string; messageId?: string; replyRecipient?: string; actionMode?: AgentActionMode; taskRefs?: TaskRef[]; } export interface OpenCodeTeamRuntimeMessageResult { ok: boolean; providerId: 'opencode'; memberName: string; sessionId?: string; runtimePid?: number; diagnostics: string[]; } const REQUIRED_READY_CHECKPOINTS = new Set([ 'required_tools_proven', 'delivery_ready', 'member_ready', 'run_ready', ]); export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter { readonly providerId = 'opencode' as const; private readonly lastProjectPathByTeamName = new Map(); private readonly lastReadinessByProjectPath = new Map(); constructor(private readonly bridge: OpenCodeTeamRuntimeBridgePort) {} async prepare(input: TeamRuntimeLaunchInput): Promise { const runtimeOnly = input.runtimeOnly === true; const readiness = await this.bridge.checkOpenCodeTeamLaunchReadiness({ projectPath: input.cwd, selectedModel: input.model ?? null, requireExecutionProbe: !runtimeOnly, }); this.lastReadinessByProjectPath.set(input.cwd, readiness); if (!readiness.launchAllowed) { return { ok: false, providerId: this.providerId, reason: readiness.state, retryable: isRetryableReadinessState(readiness.state), diagnostics: mergeDiagnostics(readiness.diagnostics, readiness.missing), warnings: [], }; } return { ok: true, providerId: this.providerId, modelId: readiness.modelId, diagnostics: readiness.diagnostics, warnings: [], }; } getLastOpenCodeTeamLaunchReadiness(projectPath: string): OpenCodeTeamLaunchReadiness | null { return this.lastReadinessByProjectPath.get(projectPath) ?? null; } async launch(input: TeamRuntimeLaunchInput): Promise { const memberValidationDiagnostics = validateOpenCodeRuntimeMembers(input.expectedMembers); if (memberValidationDiagnostics.length > 0) { return blockedLaunchResult( input, 'opencode_invalid_expected_members', memberValidationDiagnostics ); } const prepared = await this.prepare(input); if (!prepared.ok) { return blockedLaunchResult(input, prepared.reason, prepared.diagnostics, prepared.warnings); } if (!this.bridge.launchOpenCodeTeam) { return blockedLaunchResult(input, 'opencode_launch_bridge_missing', [ 'OpenCode readiness passed, but the state-changing launch bridge is not registered.', ]); } const selectedModel = prepared.modelId ?? input.model?.trim() ?? ''; if (!selectedModel) { return blockedLaunchResult(input, 'opencode_model_unavailable', [ 'OpenCode launch requires a selected raw model id.', ]); } const runtimeSnapshot = this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null; this.lastProjectPathByTeamName.set(input.teamName, input.cwd); const data = await this.bridge.launchOpenCodeTeam({ runId: input.runId, laneId: input.laneId?.trim() || 'primary', teamId: input.teamName, teamName: input.teamName, projectPath: input.cwd, selectedModel, members: input.expectedMembers.map((member) => ({ name: member.name, role: member.role?.trim() || member.workflow?.trim() || 'teammate', prompt: buildMemberBootstrapPrompt(input, member), })), leadPrompt: input.prompt?.trim() ?? '', expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null, manifestHighWatermark: null, }); return mapOpenCodeLaunchDataToRuntimeResult(input, data, prepared.warnings); } async reconcile(input: TeamRuntimeReconcileInput): Promise { const memberValidationDiagnostics = validateOpenCodeRuntimeMembers(input.expectedMembers); if (memberValidationDiagnostics.length > 0) { return { ...blockedLaunchResult( { runId: input.runId, teamName: input.teamName, cwd: input.expectedMembers[0]?.cwd ?? '', providerId: this.providerId, skipPermissions: false, expectedMembers: input.expectedMembers, previousLaunchState: input.previousLaunchState, }, 'opencode_invalid_expected_members', memberValidationDiagnostics ), snapshot: input.previousLaunchState, }; } if (this.bridge.reconcileOpenCodeTeam) { const projectPath = input.expectedMembers[0]?.cwd ?? this.lastProjectPathByTeamName.get(input.teamName); const runtimeSnapshot = projectPath ? (this.bridge.getLastOpenCodeRuntimeSnapshot?.(projectPath) ?? null) : null; const data = await this.bridge.reconcileOpenCodeTeam({ runId: input.runId, laneId: input.laneId?.trim() || 'primary', teamId: input.teamName, teamName: input.teamName, projectPath, expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null, manifestHighWatermark: null, reconcileAttemptId: `opencode-reconcile-${randomUUID()}`, expectedMembers: input.expectedMembers.map((member) => ({ name: member.name, model: member.model ?? null, })), reason: input.reason, }); const mapped = mapOpenCodeLaunchDataToRuntimeResult( { runId: input.runId, teamName: input.teamName, cwd: input.expectedMembers[0]?.cwd ?? '', providerId: this.providerId, skipPermissions: false, expectedMembers: input.expectedMembers, previousLaunchState: input.previousLaunchState, }, data, [] ); return { ...mapped, snapshot: input.previousLaunchState, }; } const snapshot = input.previousLaunchState; if (!snapshot) { return { runId: input.runId, teamName: input.teamName, launchPhase: 'reconciled', teamLaunchState: 'partial_pending', members: {}, snapshot: null, warnings: [], diagnostics: ['No previous OpenCode launch snapshot was available for reconciliation.'], }; } return { runId: input.runId, teamName: input.teamName, launchPhase: snapshot.launchPhase, teamLaunchState: snapshot.teamLaunchState, members: Object.fromEntries( Object.entries(snapshot.members).map(([memberName, member]) => [ memberName, { memberName, providerId: this.providerId, launchState: member.launchState, agentToolAccepted: member.agentToolAccepted, runtimeAlive: member.bootstrapConfirmed === true, bootstrapConfirmed: member.bootstrapConfirmed, hardFailure: member.hardFailure, hardFailureReason: member.hardFailureReason, diagnostics: member.diagnostics ?? [], } satisfies TeamRuntimeMemberLaunchEvidence, ]) ), snapshot, warnings: [], diagnostics: [`OpenCode launch snapshot reconciled from ${input.reason}.`], }; } async sendMessageToMember( input: OpenCodeTeamRuntimeMessageInput ): Promise { if (!this.bridge.sendOpenCodeTeamMessage) { return { ok: false, providerId: this.providerId, memberName: input.memberName, diagnostics: ['OpenCode message bridge is not registered.'], }; } const data = await this.bridge.sendOpenCodeTeamMessage({ runId: input.runId, laneId: input.laneId, teamId: input.teamName, teamName: input.teamName, projectPath: input.cwd, memberName: input.memberName, text: buildOpenCodeRuntimeMessageText(input), messageId: input.messageId, agent: 'teammate', }); return { ok: data.accepted, providerId: this.providerId, memberName: input.memberName, sessionId: data.sessionId, runtimePid: data.runtimePid, diagnostics: data.diagnostics.map((diagnostic) => diagnostic.message), }; } async stop(input: TeamRuntimeStopInput): Promise { if (this.bridge.stopOpenCodeTeam) { const projectPath = input.cwd ?? this.lastProjectPathByTeamName.get(input.teamName); const runtimeSnapshot = projectPath ? (this.bridge.getLastOpenCodeRuntimeSnapshot?.(projectPath) ?? null) : null; const data = await this.bridge.stopOpenCodeTeam({ runId: input.runId, laneId: input.laneId?.trim() || 'primary', teamId: input.teamName, teamName: input.teamName, projectPath, expectedCapabilitySnapshotId: runtimeSnapshot?.capabilitySnapshotId ?? null, manifestHighWatermark: null, reason: input.reason, force: input.force, }); if (data.stopped) { this.lastProjectPathByTeamName.delete(input.teamName); } return { runId: input.runId, teamName: input.teamName, stopped: data.stopped, members: Object.fromEntries( Object.entries(data.members).map(([memberName, member]) => [ memberName, { memberName, providerId: this.providerId, stopped: member.stopped, sessionId: member.sessionId, diagnostics: member.diagnostics, } satisfies TeamRuntimeMemberStopEvidence, ]) ), warnings: data.warnings.map((warning) => warning.message), diagnostics: data.diagnostics.map(formatOpenCodeBridgeDiagnostic), }; } const members = input.previousLaunchState ? Object.fromEntries( Object.keys(input.previousLaunchState.members).map((memberName) => [ memberName, { memberName, providerId: this.providerId, stopped: true, diagnostics: [ 'No live OpenCode session stop command is wired in this adapter shell.', ], } satisfies TeamRuntimeMemberStopEvidence, ]) ) : {}; return { runId: input.runId, teamName: input.teamName, stopped: true, members, warnings: [], diagnostics: input.previousLaunchState ? ['OpenCode stop was acknowledged without live session ownership changes.'] : ['No previous OpenCode launch snapshot was available to stop.'], }; } } function mapOpenCodeLaunchDataToRuntimeResult( input: TeamRuntimeLaunchInput, data: OpenCodeLaunchTeamCommandData, prepareWarnings: string[] ): TeamRuntimeLaunchResult { const checkpointNames = extractCheckpointNames(data); const readyCheckpointsPresent = [...REQUIRED_READY_CHECKPOINTS].every((name) => checkpointNames.has(name) ); const bridgeReady = data.teamLaunchState === 'ready'; const missingExpectedMembers = input.expectedMembers .map((member) => member.name) .filter((memberName) => data.members[memberName] == null); const unconfirmedExpectedMembers = input.expectedMembers .map((member) => member.name) .filter((memberName) => data.members[memberName]?.launchState !== 'confirmed_alive'); const anyExpectedMemberFailed = input.expectedMembers.some( (member) => data.members[member.name]?.launchState === 'failed' ); const allExpectedMembersConfirmed = input.expectedMembers.length > 0 && unconfirmedExpectedMembers.length === 0; const success = bridgeReady && readyCheckpointsPresent && allExpectedMembersConfirmed; const checkpointDiagnostic = success ? [] : bridgeReady && !readyCheckpointsPresent ? [ `OpenCode bridge reported ready without all required durable checkpoints: missing ${[ ...REQUIRED_READY_CHECKPOINTS, ] .filter((name) => !checkpointNames.has(name)) .join(', ')}`, ] : []; const incompleteReadyDiagnostic = bridgeReady && readyCheckpointsPresent && !allExpectedMembersConfirmed ? [ `OpenCode bridge reported ready before all expected members were confirmed: pending ${unconfirmedExpectedMembers.join(', ')}`, ] : []; const members = Object.fromEntries( input.expectedMembers.map((member) => { const bridgeMember = data.members[member.name]; const fallbackLaunchState = bridgeMember ? bridgeMember.launchState : data.teamLaunchState === 'failed' ? 'failed' : 'created'; return [ member.name, mapBridgeMemberToRuntimeEvidence( member.name, fallbackLaunchState, bridgeMember?.sessionId, bridgeMember?.runtimePid, bridgeMember?.pendingPermissionRequestIds, bridgeMember != null, [ ...(bridgeMember ? [] : [ `OpenCode bridge response did not include ${member.name}; keeping the member pending until lane state materializes.`, ]), ...(bridgeMember?.diagnostics ?? []), ...(bridgeMember?.evidence ?? []).map( (evidence) => `${evidence.kind} at ${evidence.observedAt}` ), ...checkpointDiagnostic, ...(missingExpectedMembers.includes(member.name) ? incompleteReadyDiagnostic : []), ] ), ]; }) ); return { runId: input.runId, teamName: input.teamName, launchPhase: success ? 'finished' : data.teamLaunchState === 'launching' || (bridgeReady && !anyExpectedMemberFailed) ? 'active' : 'finished', teamLaunchState: success ? 'clean_success' : anyExpectedMemberFailed || data.teamLaunchState === 'failed' ? 'partial_failure' : data.teamLaunchState === 'launching' || data.teamLaunchState === 'permission_blocked' || bridgeReady ? 'partial_pending' : 'partial_failure', members, warnings: [...prepareWarnings, ...data.warnings.map((warning) => warning.message)], diagnostics: [ ...data.diagnostics.map(formatOpenCodeBridgeDiagnostic), ...checkpointDiagnostic, ...incompleteReadyDiagnostic, ], }; } function mapBridgeMemberToRuntimeEvidence( memberName: string, launchState: OpenCodeTeamMemberLaunchBridgeState, sessionId: string | undefined, runtimePid: number | undefined, pendingPermissionRequestIds: string[] | undefined, runtimeMaterialized: boolean, diagnostics: string[] ): TeamRuntimeMemberLaunchEvidence { const confirmed = launchState === 'confirmed_alive'; const failed = launchState === 'failed'; const hasRuntimePid = typeof runtimePid === 'number' && Number.isFinite(runtimePid) && runtimePid > 0; const pendingRuntimeObserved = launchState === 'created' && hasRuntimePid; const livenessKind = confirmed ? 'confirmed_bootstrap' : pendingRuntimeObserved ? 'runtime_process_candidate' : launchState === 'permission_blocked' ? 'permission_blocked' : runtimeMaterialized || sessionId ? 'runtime_process_candidate' : 'registered_only'; const runtimeDiagnostic = pendingRuntimeObserved ? 'OpenCode runtime pid reported by bridge without local process verification' : launchState === 'permission_blocked' ? 'OpenCode runtime is waiting for permission approval' : runtimeMaterialized || sessionId ? 'OpenCode session exists without verified runtime pid' : undefined; return { memberName, providerId: 'opencode', launchState: failed ? 'failed_to_start' : confirmed ? 'confirmed_alive' : launchState === 'permission_blocked' ? 'runtime_pending_permission' : 'runtime_pending_bootstrap', agentToolAccepted: confirmed || pendingRuntimeObserved || launchState === 'permission_blocked' || runtimeMaterialized || Boolean(sessionId), runtimeAlive: confirmed, bootstrapConfirmed: confirmed, hardFailure: failed, hardFailureReason: failed ? 'OpenCode bridge reported member launch failure' : undefined, pendingPermissionRequestIds: pendingPermissionRequestIds && pendingPermissionRequestIds.length > 0 ? [...new Set(pendingPermissionRequestIds)] : undefined, sessionId, ...(hasRuntimePid ? { runtimePid } : {}), livenessKind, ...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}), ...(runtimeDiagnostic ? { runtimeDiagnostic } : {}), diagnostics, }; } function extractCheckpointNames(data: OpenCodeLaunchTeamCommandData): Set { const names = new Set(); for (const checkpoint of data.durableCheckpoints ?? []) { if (checkpoint.name.trim()) names.add(checkpoint.name); } for (const member of Object.values(data.members)) { for (const evidence of member.evidence) { if (evidence.kind.trim()) names.add(evidence.kind); } } return names; } function buildMemberBootstrapPrompt( input: TeamRuntimeLaunchInput, member: TeamRuntimeLaunchInput['expectedMembers'][number] ): string { const teamPrompt = input.prompt?.trim(); const role = member.role?.trim() || member.workflow?.trim() || 'teammate'; const workflow = member.workflow?.trim(); return [ `You are ${member.name}, a ${role} on team "${input.teamName}".`, teamPrompt ? `Team launch context:\n${teamPrompt}` : null, workflow ? `Workflow:\n${workflow}` : null, '', 'This OpenCode session is already attached by the desktop app. Do NOT create local team files, run join scripts, or search the project for a fake team registry.', 'Use the app MCP tools exposed by the "agent-teams" server for team communication and task state.', 'The desktop bridge may prepend runtime identity and bootstrap instructions. Follow those first.', 'After runtime identity check-in, if you have not already done so, call MCP tool agent-teams_member_briefing (or mcp__agent-teams__member_briefing if that is the exposed name) with:', `{ "teamName": "${input.teamName}", "memberName": "${member.name}", "runtimeProvider": "opencode" }`, 'If that tool is not available, stay idle and wait for app-delivered instructions. Do not improvise a replacement workflow.', '', 'When you need to message the human user, team lead, or another teammate, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send) with teamName, to, from, text, and optional summary.', `Always set from="${member.name}" when sending a team message from this OpenCode teammate.`, 'Do not answer team/app messages only as plain assistant text when agent-teams_message_send is available.', ] .filter((line): line is string => line !== null) .join('\n'); } function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput): string { const replyRecipient = input.replyRecipient?.trim() || 'user'; const taskRefs = input.taskRefs?.length ? JSON.stringify(input.taskRefs) : null; return [ '', 'You are running in OpenCode, not Claude Code or Codex native.', 'To make your reply visible in the app Messages UI, call MCP tool agent-teams_message_send (or mcp__agent-teams__message_send if that is the exposed name).', `Use teamName="${input.teamName}", to="${replyRecipient}", from="${input.memberName}", text, and summary.`, 'Do not answer only with plain assistant text when agent-teams_message_send is available.', 'Do not use SendMessage or runtime_deliver_message for ordinary visible replies.', input.actionMode ? `Action mode for this message: ${input.actionMode}.` : null, taskRefs ? `If your reply is about these tasks, include taskRefs exactly: ${taskRefs}` : null, input.messageId ? `The inbound app messageId is "${input.messageId}"; keep it only as context unless a tool explicitly asks for provenance.` : null, '', '', input.text, ] .filter((line): line is string => line !== null) .join('\n'); } function validateOpenCodeRuntimeMembers( members: TeamRuntimeLaunchInput['expectedMembers'] ): string[] { if (members.length === 0) { return ['OpenCode runtime adapter requires at least one expected OpenCode member.']; } return members.flatMap((member, index) => { const name = member.name.trim() || ``; if (member.providerId === 'opencode') { return []; } return [ `OpenCode runtime adapter received non-OpenCode member "${name}" with provider "${member.providerId}".`, ]; }); } function formatOpenCodeBridgeDiagnostic(diagnostic: { code: string; severity: 'info' | 'warning' | 'error'; message: string; }): string { return `${diagnostic.severity}:${diagnostic.code}: ${diagnostic.message}`; } function blockedLaunchResult( input: TeamRuntimeLaunchInput, reason: string, diagnostics: string[], warnings: string[] = [] ): TeamRuntimeLaunchResult { const hardFailureReason = reason === 'unknown_error' && diagnostics[0]?.trim() ? diagnostics[0].trim() : reason; const members = Object.fromEntries( input.expectedMembers.map((member) => [ member.name, { memberName: member.name, providerId: 'opencode' as const, launchState: 'failed_to_start' as const, agentToolAccepted: false, runtimeAlive: false, bootstrapConfirmed: false, hardFailure: true, hardFailureReason, diagnostics, }, ]) ); return { runId: input.runId, teamName: input.teamName, launchPhase: 'finished', teamLaunchState: 'partial_failure', members, warnings, diagnostics, }; } function isRetryableReadinessState(state: OpenCodeTeamLaunchReadiness['state']): boolean { return ( state === 'not_installed' || state === 'not_authenticated' || state === 'runtime_store_blocked' || state === 'mcp_unavailable' || state === 'model_unavailable' || state === 'unknown_error' ); } function mergeDiagnostics(left: string[], right: string[]): string[] { return [...new Set([...left, ...right].filter((value) => value.trim().length > 0))]; }