# Conflicts: # agent-teams-controller/src/internal/crossTeam.js # mcp-server/src/tools/messageTools.ts # mcp-server/src/tools/taskTools.ts # src/main/services/team/opencode/bridge/OpenCodeReadinessBridge.ts # src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence.ts # test/main/services/team/OpenCodeProductionE2EEvidence.test.ts # test/main/services/team/OpenCodeProductionGate.live.test.ts # test/main/services/team/OpenCodeReadinessBridge.test.ts
676 lines
24 KiB
TypeScript
676 lines
24 KiB
TypeScript
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<OpenCodeTeamLaunchReadiness>;
|
|
getLastOpenCodeRuntimeSnapshot?(projectPath: string): OpenCodeBridgeRuntimeSnapshot | null;
|
|
launchOpenCodeTeam?(input: OpenCodeLaunchTeamCommandBody): Promise<OpenCodeLaunchTeamCommandData>;
|
|
reconcileOpenCodeTeam?(
|
|
input: OpenCodeReconcileTeamCommandBody
|
|
): Promise<OpenCodeLaunchTeamCommandData>;
|
|
stopOpenCodeTeam?(input: OpenCodeStopTeamCommandBody): Promise<OpenCodeStopTeamCommandData>;
|
|
sendOpenCodeTeamMessage?(
|
|
input: OpenCodeSendMessageCommandBody
|
|
): Promise<OpenCodeSendMessageCommandData>;
|
|
}
|
|
|
|
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<string, string>();
|
|
private readonly lastReadinessByProjectPath = new Map<string, OpenCodeTeamLaunchReadiness>();
|
|
|
|
constructor(private readonly bridge: OpenCodeTeamRuntimeBridgePort) {}
|
|
|
|
async prepare(input: TeamRuntimeLaunchInput): Promise<TeamRuntimePrepareResult> {
|
|
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<TeamRuntimeLaunchResult> {
|
|
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<TeamRuntimeReconcileResult> {
|
|
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<OpenCodeTeamRuntimeMessageResult> {
|
|
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<TeamRuntimeStopResult> {
|
|
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<string> {
|
|
const names = new Set<string>();
|
|
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 [
|
|
'<opencode_app_message_delivery>',
|
|
'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,
|
|
'</opencode_app_message_delivery>',
|
|
'',
|
|
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() || `<index ${index}>`;
|
|
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))];
|
|
}
|