xworkmate-app/lib/runtime/go_task_service_client.dart

1071 lines
32 KiB
Dart

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 <AssistantExecutionTarget>[],
providerCatalog = const <SingleAgentProvider>[],
gatewayProviders = const <SingleAgentProvider>[],
raw = const <String, dynamic>{};
final bool singleAgent;
final bool multiAgent;
final List<AssistantExecutionTarget> availableExecutionTargets;
final List<SingleAgentProvider> providerCatalog;
final List<SingleAgentProvider> gatewayProviders;
final Map<String, dynamic> raw;
}
class ExternalCodeAgentAcpRoutingResolution {
const ExternalCodeAgentAcpRoutingResolution({required this.raw});
final Map<String, dynamic> 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<String> get resolvedSkills {
final rawList = raw['resolvedSkills'];
if (rawList is! List) {
return const <String>[];
}
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<Map<String, dynamic>> 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<String, dynamic> toJson() {
return <String, dynamic>{
'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 <ExternalCodeAgentAcpAvailableSkill>[],
}) : mode = ExternalCodeAgentAcpRoutingMode.auto,
explicitExecutionTarget = '',
explicitProviderId = '',
explicitModel = '',
explicitSkills = const <String>[],
allowSkillInstall = false,
installApproval = null;
final ExternalCodeAgentAcpRoutingMode mode;
final String preferredGatewayTarget;
final String explicitExecutionTarget;
final String explicitProviderId;
final String explicitModel;
final List<String> explicitSkills;
final bool allowSkillInstall;
final List<ExternalCodeAgentAcpAvailableSkill> 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<String, dynamic> toJson() {
return <String, dynamic>{
'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<String> approvedSkillKeys;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'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<String> selectedSkills;
final List<GatewayChatAttachmentPayload> inlineAttachments;
final List<CollaborationAttachment> localAttachments;
final String agentId;
final Map<String, dynamic> 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<String, dynamic> 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 = <String, dynamic>{
'sessionId': sessionId,
'threadId': threadId,
'mode': acpMode,
'taskPrompt': prompt,
'workingDirectory': workingDirectory.trim(),
'selectedSkills': selectedSkills,
'attachments': <Map<String, dynamic>>[
...localAttachments.map(
(item) => <String, dynamic>{
'name': item.name,
'description': item.description,
'path': item.path,
},
),
...inlineAttachments.map(
(item) => <String, dynamic>{
'name': item.fileName,
'description': item.mimeType,
'path': item.sourcePath.trim(),
},
),
],
if (inlineAttachments.isNotEmpty)
'inlineAttachments': inlineAttachments
.map(
(item) => <String, dynamic>{
'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)) ...<String, dynamic>{
'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 <ExternalCodeAgentAcpAvailableSkill>[],
);
}
}
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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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 <List<String>>[
<String>['status'],
<String>['error', 'status'],
<String>['details', 'status'],
<String>['payload', 'status'],
<String>['result', 'status'],
]);
String get code => _firstNestedGoTaskString(raw, const <List<String>>[
<String>['code'],
<String>['error', 'code'],
<String>['error', 'details', 'code'],
<String>['details', 'code'],
<String>['payload', 'code'],
<String>['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<String> get resolvedSkills {
final rawList = raw['resolvedSkills'];
if (rawList is! List) {
return const <String>[];
}
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<Map<String, dynamic>> get skillCandidates =>
_castMapList(raw['skillCandidates']);
List<Map<String, dynamic>> get memorySources =>
_castMapList(raw['memorySources']);
List<GoTaskServiceArtifact> get artifacts {
final rawArtifacts = _firstGoTaskArtifactList(raw);
if (rawArtifacts is! List) {
return const <GoTaskServiceArtifact>[];
}
return rawArtifacts
.whereType<Map>()
.map(
(item) =>
GoTaskServiceArtifact.fromJson(item.cast<String, dynamic>()),
)
.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<String, dynamic> result) {
final artifacts = <Object?>[];
for (final candidate in <Object?>[
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<String, dynamic>()['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<GoTaskServiceResult> executeTask(
GoTaskServiceRequest request, {
required void Function(GoTaskServiceUpdate update) onUpdate,
});
Future<GoTaskServiceResult> getTask({
required AssistantExecutionTarget target,
required OpenClawTaskAssociation association,
required GoTaskServiceRoute route,
});
Future<void> cancelTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
OpenClawTaskAssociation? association,
});
Future<void> dispose();
}
GoTaskServiceUpdate? goTaskServiceUpdateFromAcpNotification(
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> result) {
if (result.containsKey('error')) {
return false;
}
final status = _firstNestedGoTaskString(result, const <List<String>>[
<String>['status'],
<String>['details', 'status'],
<String>['payload', 'status'],
<String>['result', 'status'],
]).toLowerCase();
if (status == 'failed' ||
status == 'error' ||
status == 'cancelled' ||
status == 'canceled') {
return false;
}
return true;
}
String _firstGoTaskFailureText(Map<String, dynamic> result) {
for (final key in const <String>[
'error',
'errorMessage',
'message',
'unavailableMessage',
'unavailableCode',
]) {
final text = _extractGoTaskDisplayText(result[key]);
if (text.isNotEmpty) {
return text;
}
}
return '';
}
String _diagnosticGoTaskFailureText(
Map<String, dynamic> result,
String errorText,
) {
final text = errorText.trim();
if (text.isEmpty) {
return '';
}
final diagnostics = <String>[];
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<Object>? visited]) {
final seen = visited ?? <Object>{};
if (value == null) {
return '';
}
if (value is String) {
return value.trim();
}
if (value is Map) {
if (!seen.add(value)) {
return '';
}
final map = value.cast<String, dynamic>();
for (final key in const <String>[
'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 <String>[
'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 = <String>[];
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<Object> 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<String, dynamic>();
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<String, dynamic> mergeGoTaskServiceResponseResult(
Map<String, dynamic> response,
Map<String, dynamic> overlay,
) {
if (overlay.isEmpty) {
return response;
}
final next = Map<String, dynamic>.from(response);
final result = Map<String, dynamic>.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<String, dynamic> _castMap(Object? value) {
if (value is Map<String, dynamic>) {
return value;
}
if (value is Map) {
return value.cast<String, dynamic>();
}
return const <String, dynamic>{};
}
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<Map<String, dynamic>> _castMapList(Object? raw) {
if (raw is! List) {
return const <Map<String, dynamic>>[];
}
return raw.map(_castMap).toList(growable: false);
}
String _firstNestedGoTaskString(
Map<String, dynamic> source,
List<List<String>> 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<dynamic> _castList(Object? raw) {
if (raw is List) {
return raw;
}
return const <dynamic>[];
}