import 'dart:convert'; import 'package:crypto/crypto.dart' as crypto; import 'runtime_models.dart'; enum GoTaskServiceRoute { externalAcpSingle, externalAcpMulti } enum GoTaskServiceCollaborationMode { standard, multiAgent } class ExternalCodeAgentAcpCapabilities { const ExternalCodeAgentAcpCapabilities({ required this.singleAgent, required this.multiAgent, required this.availableExecutionTargets, required this.providerCatalog, required this.gatewayProviders, required this.raw, }); const ExternalCodeAgentAcpCapabilities.empty() : singleAgent = false, multiAgent = false, availableExecutionTargets = const [], providerCatalog = const [], gatewayProviders = const [], raw = const {}; final bool singleAgent; final bool multiAgent; final List availableExecutionTargets; final List providerCatalog; final List gatewayProviders; final Map raw; } class ExternalCodeAgentAcpRoutingResolution { const ExternalCodeAgentAcpRoutingResolution({required this.raw}); final Map raw; String get resolvedExecutionTarget => raw['resolvedExecutionTarget']?.toString().trim() ?? ''; String get resolvedEndpointTarget => raw['resolvedEndpointTarget']?.toString().trim() ?? ''; String get resolvedProviderId => raw['resolvedProviderId']?.toString().trim() ?? ''; String get resolvedGatewayProviderId => raw['resolvedGatewayProviderId']?.toString().trim() ?? ''; String get resolvedModel => raw['resolvedModel']?.toString().trim() ?? ''; List get resolvedSkills { final rawList = raw['resolvedSkills']; if (rawList is! List) { return const []; } return rawList .map((item) => item?.toString().trim() ?? '') .where((item) => item.isNotEmpty) .toList(growable: false); } String get skillResolutionSource => raw['skillResolutionSource']?.toString().trim() ?? ''; bool get needsSkillInstall => _boolValue(raw['needsSkillInstall']) ?? false; String get skillInstallRequestId => raw['skillInstallRequestId']?.toString().trim() ?? ''; List> get skillCandidates => _castMapList(raw['skillCandidates']); bool get unavailable => _boolValue(raw['unavailable']) ?? false; String get unavailableCode => raw['unavailableCode']?.toString().trim() ?? ''; String get unavailableMessage => raw['unavailableMessage']?.toString().trim() ?? ''; } enum ExternalCodeAgentAcpRoutingMode { auto, explicit } class ExternalCodeAgentAcpAvailableSkill { const ExternalCodeAgentAcpAvailableSkill({ required this.id, required this.label, required this.description, this.installed = true, }); final String id; final String label; final String description; final bool installed; Map toJson() { return { 'id': id.trim(), 'label': label.trim(), 'description': description.trim(), 'installed': installed, }; } } class ExternalCodeAgentAcpRoutingConfig { const ExternalCodeAgentAcpRoutingConfig({ required this.mode, required this.preferredGatewayTarget, required this.explicitExecutionTarget, required this.explicitProviderId, required this.explicitModel, required this.explicitSkills, required this.allowSkillInstall, required this.availableSkills, this.installApproval, }); const ExternalCodeAgentAcpRoutingConfig.auto({ this.preferredGatewayTarget = '', this.availableSkills = const [], }) : mode = ExternalCodeAgentAcpRoutingMode.auto, explicitExecutionTarget = '', explicitProviderId = '', explicitModel = '', explicitSkills = const [], allowSkillInstall = false, installApproval = null; final ExternalCodeAgentAcpRoutingMode mode; final String preferredGatewayTarget; final String explicitExecutionTarget; final String explicitProviderId; final String explicitModel; final List explicitSkills; final bool allowSkillInstall; final List availableSkills; final ExternalCodeAgentAcpSkillInstallApproval? installApproval; bool get isAuto => mode == ExternalCodeAgentAcpRoutingMode.auto; ExternalCodeAgentAcpRoutingConfig withoutExplicitModel() { if (explicitModel.trim().isEmpty) { return this; } return ExternalCodeAgentAcpRoutingConfig( mode: mode, preferredGatewayTarget: preferredGatewayTarget, explicitExecutionTarget: explicitExecutionTarget, explicitProviderId: explicitProviderId, explicitModel: '', explicitSkills: explicitSkills, allowSkillInstall: allowSkillInstall, availableSkills: availableSkills, installApproval: installApproval, ); } Map toJson() { return { 'routingMode': mode.name, if (preferredGatewayTarget.trim().isNotEmpty) 'preferredGatewayProviderId': preferredGatewayTarget.trim(), if (explicitExecutionTarget.trim().isNotEmpty) 'explicitExecutionTarget': explicitExecutionTarget.trim(), if (explicitProviderId.trim().isNotEmpty) 'explicitProviderId': explicitProviderId.trim(), if (explicitModel.trim().isNotEmpty) 'explicitModel': explicitModel.trim(), 'explicitSkills': explicitSkills .map((item) => item.trim()) .where((item) => item.isNotEmpty) .toList(growable: false), 'allowSkillInstall': allowSkillInstall, 'availableSkills': availableSkills .map((item) => item.toJson()) .toList(growable: false), if (installApproval != null) 'installApproval': installApproval!.toJson(), }; } } class ExternalCodeAgentAcpSkillInstallApproval { const ExternalCodeAgentAcpSkillInstallApproval({ required this.requestId, required this.approvedSkillKeys, }); final String requestId; final List approvedSkillKeys; Map toJson() { return { 'requestId': requestId.trim(), 'approvedSkillKeys': approvedSkillKeys .map((item) => item.trim()) .where((item) => item.isNotEmpty) .toList(growable: false), }; } } class GoTaskServiceRequest { const GoTaskServiceRequest({ required this.sessionId, required this.threadId, required this.target, required this.prompt, required this.workingDirectory, required this.model, required this.thinking, required this.selectedSkills, required this.inlineAttachments, required this.localAttachments, required this.agentId, required this.metadata, this.routing, this.routingHint = '', this.provider = SingleAgentProvider.unspecified, this.remoteWorkingDirectoryHint = '', this.resumeSession = false, this.collaborationMode = GoTaskServiceCollaborationMode.standard, this.multiAgent = false, }); final String sessionId; final String threadId; final AssistantExecutionTarget target; final String prompt; final String workingDirectory; final String model; final String thinking; final List selectedSkills; final List inlineAttachments; final List localAttachments; final String agentId; final Map metadata; final ExternalCodeAgentAcpRoutingConfig? routing; final String routingHint; final SingleAgentProvider provider; final String remoteWorkingDirectoryHint; final bool resumeSession; final GoTaskServiceCollaborationMode collaborationMode; final bool multiAgent; bool get isMultiAgentRequest => multiAgent || collaborationMode == GoTaskServiceCollaborationMode.multiAgent; AssistantExecutionTarget get normalizedTarget => target; GoTaskServiceRoute get route { if (isMultiAgentRequest) { return GoTaskServiceRoute.externalAcpMulti; } return GoTaskServiceRoute.externalAcpSingle; } String get acpMode { return _gatewaySessionMode; } String get routingExecutionTarget { return normalizedTarget.promptValue; } bool get hasInlineAttachments => inlineAttachments.isNotEmpty; ExternalCodeAgentAcpRoutingConfig get effectiveRouting => routing ?? _synthesizedRouting(); Map toExternalAcpParams() { final resolvedRouting = normalizedTarget.isGateway ? effectiveRouting.withoutExplicitModel() : effectiveRouting; final requestModel = normalizedTarget.isGateway ? '' : model.trim(); final providerId = provider.isUnspecified ? '' : provider.providerId; final agentProviderId = normalizedTarget.isGateway ? '' : providerId; final params = { 'sessionId': sessionId, 'threadId': threadId, 'mode': acpMode, 'taskPrompt': prompt, 'workingDirectory': workingDirectory.trim(), 'selectedSkills': selectedSkills, 'attachments': >[ ...localAttachments.map( (item) => { 'name': item.name, 'description': item.description, 'path': item.path, }, ), ...inlineAttachments.map( (item) => { 'name': item.fileName, 'description': item.mimeType, 'path': item.sourcePath.trim(), }, ), ], if (inlineAttachments.isNotEmpty) 'inlineAttachments': inlineAttachments .map( (item) => { 'name': item.fileName, 'mimeType': item.mimeType, 'content': item.content, 'sizeBytes': goTaskServiceBase64Size(item.content), if (goTaskServiceAttachmentSha256(item).isNotEmpty) 'sha256': goTaskServiceAttachmentSha256(item), if (item.sourcePath.trim().isNotEmpty) 'sourcePath': item.sourcePath.trim(), }, ) .toList(growable: false), if (agentProviderId.isNotEmpty) 'provider': agentProviderId, if (remoteWorkingDirectoryHint.trim().isNotEmpty) 'remoteWorkingDirectoryHint': remoteWorkingDirectoryHint.trim(), if (requestModel.isNotEmpty) 'model': requestModel, if (thinking.trim().isNotEmpty) 'thinking': thinking.trim(), 'routing': resolvedRouting.toJson(), if (routingHint.trim().isNotEmpty) 'routingHint': routingHint.trim(), 'requestedExecutionTarget': normalizedTarget.promptValue, if (_usesGatewaySessionMode(acpMode)) ...{ 'executionTarget': normalizedTarget.promptValue, 'appThreadKey': threadId, if (agentId.trim().isNotEmpty) 'agentId': agentId.trim(), if (metadata.isNotEmpty) 'metadata': metadata, }, }; return params; } ExternalCodeAgentAcpRoutingConfig _synthesizedRouting() { final gatewayTarget = normalizedTarget; final preferredGatewayTarget = switch (gatewayTarget) { AssistantExecutionTarget.agent => kCanonicalGatewayProviderId, AssistantExecutionTarget.gateway => kCanonicalGatewayProviderId, }; final explicitExecutionTarget = switch (gatewayTarget) { AssistantExecutionTarget.agent => 'agent', AssistantExecutionTarget.gateway => 'gateway', }; final explicitProviderId = provider.isUnspecified || gatewayTarget.isGateway ? '' : provider.providerId; final explicitModelValue = gatewayTarget.isGateway ? '' : model.trim(); final explicitSkillsValue = selectedSkills .map((item) => item.trim()) .where((item) => item.isNotEmpty) .toList(growable: false); final hasExplicitSelection = explicitExecutionTarget.isNotEmpty || explicitProviderId.isNotEmpty || explicitModelValue.isNotEmpty || explicitSkillsValue.isNotEmpty; if (!hasExplicitSelection) { return ExternalCodeAgentAcpRoutingConfig.auto( preferredGatewayTarget: preferredGatewayTarget, ); } return ExternalCodeAgentAcpRoutingConfig( mode: ExternalCodeAgentAcpRoutingMode.explicit, preferredGatewayTarget: preferredGatewayTarget, explicitExecutionTarget: explicitExecutionTarget, explicitProviderId: explicitProviderId, explicitModel: explicitModelValue, explicitSkills: explicitSkillsValue, allowSkillInstall: false, availableSkills: const [], ); } } const String _gatewaySessionMode = 'gateway-chat'; bool _usesGatewaySessionMode(String mode) { final normalized = mode.trim(); return normalized == 'gateway' || normalized == _gatewaySessionMode; } class GoTaskServiceUpdate { const GoTaskServiceUpdate({ required this.sessionId, required this.threadId, required this.turnId, required this.type, required this.text, required this.message, required this.pending, required this.error, required this.route, required this.payload, }); final String sessionId; final String threadId; final String turnId; final String type; final String text; final String message; final bool pending; final bool error; final GoTaskServiceRoute route; final Map payload; bool get isDelta => type == 'delta' && text.isNotEmpty; bool get isDone => type == 'done' || payload['event'] == 'completed'; } class GoTaskServiceArtifact { const GoTaskServiceArtifact({ required this.relativePath, required this.label, required this.contentType, required this.encoding, required this.content, required this.downloadUrl, required this.sizeBytes, required this.sha256, }); final String relativePath; final String label; final String contentType; final String encoding; final String content; final String downloadUrl; final int? sizeBytes; final String sha256; bool get hasInlineContent => content.trim().isNotEmpty; factory GoTaskServiceArtifact.fromJson(Map json) { int? parseSize(Object? value) { if (value is int) { return value; } if (value is num) { return value.toInt(); } return int.tryParse(value?.toString() ?? ''); } return GoTaskServiceArtifact( relativePath: json['relativePath']?.toString().trim() ?? json['path']?.toString().trim() ?? json['name']?.toString().trim() ?? '', label: json['label']?.toString().trim() ?? '', contentType: json['contentType']?.toString().trim() ?? '', encoding: json['encoding']?.toString().trim() ?? '', content: json['content']?.toString() ?? '', downloadUrl: json['downloadUrl']?.toString().trim() ?? json['downloadURL']?.toString().trim() ?? json['download_url']?.toString().trim() ?? '', sizeBytes: parseSize(json['sizeBytes'] ?? json['size']), sha256: json['sha256']?.toString().trim() ?? '', ); } } class GoTaskServiceResult { const GoTaskServiceResult({ required this.success, required this.message, required this.turnId, required this.raw, required this.errorMessage, required this.resolvedModel, required this.route, }); final bool success; final String message; final String turnId; final Map raw; final String errorMessage; final String resolvedModel; final GoTaskServiceRoute route; String get resolvedWorkingDirectory => raw['resolvedWorkingDirectory']?.toString().trim() ?? raw['effectiveWorkingDirectory']?.toString().trim() ?? raw['workingDirectory']?.toString().trim() ?? ''; String get resultSummary => raw['resultSummary']?.toString().trim().isNotEmpty == true ? raw['resultSummary'].toString().trim() : raw['summary']?.toString().trim() ?? ''; String get status => _firstNestedGoTaskString(raw, const >[ ['status'], ['error', 'status'], ['details', 'status'], ['payload', 'status'], ['result', 'status'], ]); String get code => _firstNestedGoTaskString(raw, const >[ ['code'], ['error', 'code'], ['error', 'details', 'code'], ['details', 'code'], ['payload', 'code'], ['result', 'code'], ]); bool get isOpenClawRunningTaskHandle { final normalizedStatus = status.trim().toLowerCase(); final runId = raw['runId']?.toString().trim() ?? ''; final artifactScope = raw['artifactScope']?.toString().trim() ?? ''; final provider = raw['resolvedGatewayProviderId']?.toString().trim().toLowerCase() ?? raw['gatewayProviderId']?.toString().trim().toLowerCase() ?? ''; return normalizedStatus == 'running' && runId.isNotEmpty && artifactScope.isNotEmpty && provider.contains('openclaw'); } OpenClawTaskAssociation? get openClawTaskAssociation { final association = OpenClawTaskAssociation.fromJsonOrNull(raw); if (association == null) { return null; } final provider = association.gatewayProviderId.trim().toLowerCase(); return provider.contains('openclaw') ? association : null; } String get resolvedExecutionTarget => raw['resolvedExecutionTarget']?.toString().trim() ?? ''; String get resolvedEndpointTarget => raw['resolvedEndpointTarget']?.toString().trim() ?? ''; String get resolvedProviderId => raw['resolvedProviderId']?.toString().trim() ?? ''; List get resolvedSkills { final rawList = raw['resolvedSkills']; if (rawList is! List) { return const []; } return rawList .map((item) => item?.toString().trim() ?? '') .where((item) => item.isNotEmpty) .toList(growable: false); } String get skillResolutionSource => raw['skillResolutionSource']?.toString().trim() ?? ''; bool get needsSkillInstall => _boolValue(raw['needsSkillInstall']) ?? false; String get skillInstallRequestId => raw['skillInstallRequestId']?.toString().trim() ?? ''; List> get skillCandidates => _castMapList(raw['skillCandidates']); List> get memorySources => _castMapList(raw['memorySources']); List get artifacts { final rawArtifacts = _firstGoTaskArtifactList(raw); if (rawArtifacts is! List) { return const []; } return rawArtifacts .whereType() .map( (item) => GoTaskServiceArtifact.fromJson(item.cast()), ) .where((item) => item.relativePath.isNotEmpty) .toList(growable: false); } WorkspaceRefKind? get resolvedWorkspaceRefKind { final rawValue = raw['resolvedWorkspaceRefKind']?.toString().trim() ?? ''; if (rawValue.isEmpty) { return null; } return WorkspaceRefKindCopy.fromJsonValue(rawValue); } String get remoteWorkingDirectory { final remoteExecution = _castMap(raw['remoteExecution']); return remoteExecution['remoteWorkingDirectory']?.toString().trim() ?? raw['remoteWorkingDirectory']?.toString().trim() ?? resolvedWorkingDirectory; } WorkspaceRefKind? get remoteWorkspaceRefKind { final remoteExecution = _castMap(raw['remoteExecution']); final rawValue = remoteExecution['remoteWorkspaceRefKind']?.toString().trim() ?? raw['remoteWorkspaceRefKind']?.toString().trim() ?? ''; if (rawValue.isEmpty) { return resolvedWorkspaceRefKind; } return WorkspaceRefKindCopy.fromJsonValue(rawValue); } } Object? _firstGoTaskArtifactList(Map result) { final artifacts = []; for (final candidate in [ result['artifacts'], result['files'], result['attachments'], _castMap(result['payload'])['artifacts'], _castMap(result['payload'])['files'], _castMap(result['payload'])['attachments'], _castMap(result['result'])['artifacts'], _castMap(result['result'])['files'], _castMap(result['result'])['attachments'], _castMap(result['data'])['artifacts'], _castMap(result['data'])['files'], _castMap(result['data'])['attachments'], ]) { if (candidate is List) { artifacts.addAll(candidate); } else if (candidate is Map) { final items = _castList(candidate.cast()['items']); if (items.isNotEmpty) { artifacts.addAll(items); } } } return artifacts.isEmpty ? null : artifacts; } String? goTaskServiceGatewayEntryState({ required AssistantExecutionTarget requestedTarget, required GoTaskServiceResult result, }) { final resolvedExecutionTarget = result.resolvedExecutionTarget .trim() .toLowerCase(); switch (resolvedExecutionTarget) { case 'gateway': final resolvedEndpointTarget = result.resolvedEndpointTarget .trim() .toLowerCase(); if (resolvedEndpointTarget.isEmpty || resolvedEndpointTarget == 'gateway' || resolvedEndpointTarget == 'local' || resolvedEndpointTarget == 'remote') { return AssistantExecutionTarget.gateway.promptValue; } throw StateError( 'Bridge protocol mismatch: unsupported resolvedEndpointTarget "$resolvedEndpointTarget".', ); case 'multi-agent': return AssistantExecutionTarget.gateway.promptValue; case 'local': case 'remote': throw StateError( 'Bridge protocol mismatch: unsupported resolvedExecutionTarget "$resolvedExecutionTarget".', ); default: return requestedTarget.isGateway ? AssistantExecutionTarget.gateway.promptValue : requestedTarget.promptValue; } } abstract class GoTaskServiceClient { Future executeTask( GoTaskServiceRequest request, { required void Function(GoTaskServiceUpdate update) onUpdate, }); Future getTask({ required AssistantExecutionTarget target, required OpenClawTaskAssociation association, required GoTaskServiceRoute route, }); Future cancelTask({ required GoTaskServiceRoute route, required AssistantExecutionTarget target, required String sessionId, required String threadId, OpenClawTaskAssociation? association, }); Future dispose(); } GoTaskServiceUpdate? goTaskServiceUpdateFromAcpNotification( Map notification, ) { final method = notification['method']?.toString().trim().toLowerCase() ?? ''; if (method != 'session.update' && method != 'acp.session.update') { return null; } final params = _castMap(notification['params']); final payload = params.isNotEmpty ? params : _castMap(notification['payload']); final type = payload['type']?.toString().trim().toLowerCase() ?? payload['state']?.toString().trim().toLowerCase() ?? payload['event']?.toString().trim().toLowerCase() ?? 'status'; return GoTaskServiceUpdate( sessionId: payload['sessionId']?.toString().trim().isNotEmpty == true ? payload['sessionId'].toString().trim() : payload['threadId']?.toString().trim() ?? '', threadId: payload['threadId']?.toString().trim() ?? '', turnId: payload['turnId']?.toString().trim() ?? '', type: type, text: payload['delta']?.toString() ?? payload['text']?.toString() ?? _castMap(payload['message'])['content']?.toString() ?? '', message: _extractGoTaskDisplayText(payload['message']), pending: _boolValue(payload['pending']) ?? false, error: _boolValue(payload['error']) ?? false, route: (payload['mode']?.toString().trim() == 'multi-agent' || payload['resolvedExecutionTarget']?.toString().trim() == 'multi-agent') ? GoTaskServiceRoute.externalAcpMulti : GoTaskServiceRoute.externalAcpSingle, payload: payload, ); } GoTaskServiceResult goTaskServiceResultFromAcpResponse( Map response, { required GoTaskServiceRoute route, String streamedText = '', String? completedMessage, }) { final result = _castMap(response['result']); final skillCandidates = _castMapList(result['skillCandidates']) .map((item) => item['id']?.toString().trim() ?? '') .where((item) => item.isNotEmpty) .toList(growable: false); final success = _boolValue(result['success']) ?? _inferGoTaskSuccess(result); final structuredFailureText = () { if (success) { return ''; } final errorText = _firstGoTaskFailureText(result); final needsSkillInstall = _boolValue(result['needsSkillInstall']) ?? false; if (needsSkillInstall && skillCandidates.isNotEmpty) { final candidateText = skillCandidates.join(', '); if (errorText.isNotEmpty) { return '$errorText (skills: $candidateText)'; } return 'Skill install required: $candidateText'; } return _diagnosticGoTaskFailureText(result, errorText); }(); final responseText = _extractGoTaskDisplayText(result); final primaryText = (responseText.isNotEmpty ? responseText : completedMessage?.trim().isNotEmpty == true ? completedMessage!.trim() : structuredFailureText.isNotEmpty ? structuredFailureText : streamedText.trim().isNotEmpty ? streamedText.trim() : '') .trim(); final directErrorMessage = _extractGoTaskDisplayText(result['error']); final effectiveErrorMessage = success ? directErrorMessage : structuredFailureText.isNotEmpty ? structuredFailureText : primaryText.isNotEmpty ? primaryText : directErrorMessage; return GoTaskServiceResult( success: success, message: primaryText, turnId: result['turnId']?.toString().trim() ?? '', raw: result, errorMessage: effectiveErrorMessage, resolvedModel: result['model']?.toString().trim() ?? result['resolvedModel']?.toString().trim() ?? '', route: route, ); } bool _inferGoTaskSuccess(Map result) { if (result.containsKey('error')) { return false; } final status = _firstNestedGoTaskString(result, const >[ ['status'], ['details', 'status'], ['payload', 'status'], ['result', 'status'], ]).toLowerCase(); if (status == 'failed' || status == 'error' || status == 'cancelled' || status == 'canceled') { return false; } return true; } String _firstGoTaskFailureText(Map result) { for (final key in const [ 'error', 'errorMessage', 'message', 'unavailableMessage', 'unavailableCode', ]) { final text = _extractGoTaskDisplayText(result[key]); if (text.isNotEmpty) { return text; } } return ''; } String _diagnosticGoTaskFailureText( Map result, String errorText, ) { final text = errorText.trim(); if (text.isEmpty) { return ''; } final diagnostics = []; final code = result['unavailableCode']?.toString().trim() ?? ''; if (code.isNotEmpty && !text.contains(code)) { diagnostics.add('code: $code'); } final upstreamMethod = result['upstreamMethod']?.toString().trim() ?? ''; if (upstreamMethod.isNotEmpty) { diagnostics.add('upstream: $upstreamMethod'); } if (diagnostics.isEmpty) { return text; } return '$text (${diagnostics.join(', ')})'; } String _extractGoTaskDisplayText(Object? value, [Set? visited]) { final seen = visited ?? {}; if (value == null) { return ''; } if (value is String) { return value.trim(); } if (value is Map) { if (!seen.add(value)) { return ''; } final map = value.cast(); for (final key in const [ 'output', 'summary', 'resultSummary', 'message', 'content', 'text', 'output_text', ]) { final extracted = _extractGoTaskTextCandidate(map[key], seen); if (extracted.isNotEmpty) { return extracted; } } final choices = _castList(map['choices']); if (choices.isNotEmpty) { for (final choice in choices) { final extracted = _extractGoTaskDisplayText( _castMap(choice)['message'], seen, ); if (extracted.isNotEmpty) { return extracted; } } } for (final key in const [ 'result', 'payload', 'data', 'response', 'body', ]) { final extracted = _extractGoTaskDisplayText(map[key], seen); if (extracted.isNotEmpty) { return extracted; } } return ''; } if (value is List) { if (!seen.add(value)) { return ''; } final parts = []; for (final item in value) { final extracted = _extractGoTaskDisplayText(item, seen); if (extracted.isNotEmpty) { parts.add(extracted); } } return parts.join('\n').trim(); } return ''; } String _extractGoTaskTextCandidate(Object? value, Set visited) { if (value == null) { return ''; } if (value is String) { return value.trim(); } if (value is List) { return _extractGoTaskDisplayText(value, visited); } if (value is Map) { final map = value.cast(); final type = map['type']?.toString().trim(); if (type == 'output_text') { final text = map['text']?.toString().trim() ?? map['value']?.toString().trim(); if (text != null && text.isNotEmpty) { return text; } } return _extractGoTaskDisplayText(map, visited); } return ''; } Map mergeGoTaskServiceResponseResult( Map response, Map overlay, ) { if (overlay.isEmpty) { return response; } final next = Map.from(response); final result = Map.from(_castMap(next['result'])); overlay.forEach((key, value) { if (value == null) { return; } if (value is String && value.trim().isEmpty) { if (result.containsKey(key)) { return; } } result[key] = value; }); next['result'] = result; return next; } int goTaskServiceBase64Size(String base64) { final normalized = base64.trim().split(',').last.trim(); if (normalized.isEmpty) { return 0; } final padding = normalized.endsWith('==') ? 2 : (normalized.endsWith('=') ? 1 : 0); return (normalized.length * 3 ~/ 4) - padding; } String goTaskServiceAttachmentSha256(GatewayChatAttachmentPayload attachment) { final declared = attachment.sha256.trim().toLowerCase(); if (declared.isNotEmpty) { return declared; } try { return crypto.sha256 .convert(base64Decode(attachment.content.trim().split(',').last.trim())) .toString(); } on FormatException { return ''; } } Map _castMap(Object? value) { if (value is Map) { return value; } if (value is Map) { return value.cast(); } return const {}; } bool? _boolValue(Object? raw) { if (raw is bool) { return raw; } if (raw is num) { return raw != 0; } if (raw is String) { final normalized = raw.trim().toLowerCase(); if (normalized == 'true') { return true; } if (normalized == 'false') { return false; } } return null; } List> _castMapList(Object? raw) { if (raw is! List) { return const >[]; } return raw.map(_castMap).toList(growable: false); } String _firstNestedGoTaskString( Map source, List> paths, ) { for (final path in paths) { Object? current = source; for (final key in path) { if (current is! Map) { current = null; break; } current = current[key]; } final value = current?.toString().trim() ?? ''; if (value.isNotEmpty) { return value; } } return ''; } List _castList(Object? raw) { if (raw is List) { return raw; } return const []; }