apply method
Implementation
bool apply(ChatContentUpdate update, {Object? restoreId}) {
if (update.identity != identity) return false;
if (restoreId != null && restoreId != _restoreId) return false;
// The adapter must explicitly route replay; live data cannot mutate staging.
if (_restoreId != null && restoreId == null) return false;
final target = restoreId == null ? _messages : _staged!;
if (update.kind == ChatContentKind.turnCompleted) {
for (final message in target.whereType<ChatMessage>().where(
(m) => m.turnId == update.turnId,
)) {
final chunk = _counters[message]?.finish();
if (chunk != null) _append(message, chunk, target);
}
if (restoreId == null) revision++;
return true;
}
final guard = ChatStructuredUpdateGuard(
budget: inputBudget,
resource: 'content update',
);
final metadata = guard.copyJsonValue(
update.metadata,
field: 'content metadata',
);
final copied = Map<String, Object?>.from(metadata as Map);
final role = switch (update.kind) {
ChatContentKind.user => ChatMessageRole.user,
ChatContentKind.textDelta => ChatMessageRole.assistant,
ChatContentKind.tool => ChatMessageRole.tool,
_ => ChatMessageRole.status,
};
var index = -1;
if (update.kind == ChatContentKind.tool) {
final id = update.toolCallId;
if (id == null || id.isEmpty) {
throw ArgumentError('toolCallId is required');
}
copied['toolCallId'] = id;
index = findChatToolMessage(target, id, turnId: update.turnId);
} else if (update.kind == ChatContentKind.textDelta ||
update.kind == ChatContentKind.thoughtDelta) {
if (update.kind == ChatContentKind.thoughtDelta) {
copied['kind'] = 'thought';
}
if (copied.keys.every((key) => key == 'kind') &&
target.isNotEmpty &&
target.last.turnId == update.turnId &&
target.last.role == role &&
target.last.metadata['kind'] == copied['kind']) {
index = target.length - 1;
}
} else if (update.kind == ChatContentKind.plan) {
copied['kind'] = 'plan';
}
final previous = index < 0 ? null : target[index];
final isDelta =
update.kind == ChatContentKind.textDelta ||
update.kind == ChatContentKind.thoughtDelta;
if (isDelta) {
ChatMessage next;
if (previous is ChatMessage) {
next = previous;
} else {
next = ChatMessage(
role: role,
text: '',
metadata: copied,
inputBudget: inputBudget,
);
next.projectionClaimOwnership(_owner, update.turnId);
}
final counter = _counters.putIfAbsent(
next,
() => ChatUtf8LineBudgetCounter(
maxBytes: update.kind == ChatContentKind.thoughtDelta
? inputBudget.maxThoughtTextBytes
: inputBudget.maxMessageTextBytes,
maxLines: inputBudget.maxMessageTextLines,
resource: 'message text',
),
);
if (index < 0 && target.length >= inputBudget.maxCollectionItems) {
_counters.remove(next);
throw StateError('Content message budget exceeded');
}
final checkpoint = counter.checkpoint();
try {
_append(next, counter.append(update.text), target);
counter.commit(checkpoint);
} catch (_) {
counter.rollback(checkpoint);
if (index < 0) _counters.remove(next);
rethrow;
}
if (index < 0) target.add(next);
if (restoreId == null) revision++;
return true;
}
final text = guard.copyString(
update.text.trim().isEmpty ? previous?.text ?? '' : update.text,
field: 'content text',
);
final next = ChatMessageData(
role: role,
text: text,
turnId: update.turnId,
timestamp: previous?.timestamp,
revision: (previous?.revision ?? -1) + 1,
metadata: mergeChatToolMetadata(previous?.metadata ?? const {}, copied),
);
_checkRetainedBudget(target, next, replaced: previous);
if (index < 0) {
if (target.length >= inputBudget.maxCollectionItems) {
throw StateError('Content message budget exceeded');
}
target.add(next);
} else {
target[index] = next;
}
if (restoreId == null) revision++;
return true;
}