Limit the tokens returned by the runtime errors tool/resource (#139)
Fixes https://github.com/dart-lang/ai/issues/134
diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml
index 54e3df5..1333e86 100644
--- a/.github/workflows/pull_request_label.yml
+++ b/.github/workflows/pull_request_label.yml
@@ -13,6 +13,7 @@
jobs:
label:
permissions:
+ issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
index efd9524..1dfffe4 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
@@ -109,12 +109,14 @@
final resource = Resource(
uri: '$runtimeErrorsScheme://${debugSession.id}',
name: debugSession.name,
+ description:
+ 'Recent runtime errors seen for debug session '
+ '"${debugSession.name}".',
);
addResource(resource, (request) async {
- final errors = errorService.errors;
return ReadResourceResult(
contents: [
- for (var error in errors)
+ for (var error in errorService.errorLog.errors)
TextResourceContents(uri: resource.uri, text: error),
],
);
@@ -294,7 +296,7 @@
(await _AppErrorsListener.forVmService(
vmService,
this,
- )).errors.clear();
+ )).errorLog.clear();
}
final vm = await vmService.getVM();
@@ -372,9 +374,9 @@
vmService,
this,
);
- final errors = errorService.errors;
+ final errorLog = errorService.errorLog;
- if (errors.isEmpty) {
+ if (errorLog.errors.isEmpty) {
return CallToolResult(
content: [TextContent(text: 'No runtime errors found.')],
);
@@ -383,14 +385,14 @@
content: [
TextContent(
text:
- 'Found ${errors.length} '
- 'error${errors.length == 1 ? '' : 's'}:\n',
+ 'Found ${errorLog.errors.length} '
+ 'error${errorLog.errors.length == 1 ? '' : 's'}:\n',
),
- ...errors.map((e) => TextContent(text: e.toString())),
+ for (final e in errorLog.errors) TextContent(text: e.toString()),
],
);
if (request.arguments?['clearRuntimeErrors'] == true) {
- errorService.errors.clear();
+ errorService.errorLog.clear();
}
return result;
} catch (e) {
@@ -614,9 +616,9 @@
static final getRuntimeErrorsTool = Tool(
name: 'get_runtime_errors',
description:
- 'Retrieves the list of runtime errors that have occurred in the active '
- 'Dart or Flutter application. Requires "${connectTool.name}" to be '
- 'successfully called first.',
+ 'Retrieves the most recent runtime errors that have occurred in the '
+ 'active Dart or Flutter application. Requires "${connectTool.name}" to '
+ 'be successfully called first.',
annotations: ToolAnnotations(
title: 'Get runtime errors',
readOnlyHint: true,
@@ -767,7 +769,7 @@
/// Listens on a VM service for errors.
class _AppErrorsListener {
/// All the errors recorded so far (may be cleared explicitly).
- final List<String> errors;
+ final ErrorLog errorLog;
/// A broadcast stream of all errors that come in after you start listening.
Stream<String> get errorsStream => _errorsController.stream;
@@ -785,7 +787,7 @@
final VmService _vmService;
_AppErrorsListener._(
- this.errors,
+ this.errorLog,
this._errorsController,
this._extensionEventsListener,
this._stderrEventsListener,
@@ -809,8 +811,8 @@
// list but also expose it to clients so they can know when new errors
// are added.
final errorsController = StreamController<String>.broadcast();
- final errors = <String>[];
- errorsController.stream.listen(errors.add);
+ final errorLog = ErrorLog();
+ errorsController.stream.listen(errorLog.add);
// We need to listen to streams with history so that we can get errors
// that occurred before this tool call.
// TODO(https://github.com/dart-lang/ai/issues/57): this can result in
@@ -849,7 +851,7 @@
logger.log(LoggingLevel.error, 'Error subscribing to app errors: $e');
}
return _AppErrorsListener._(
- errors,
+ errorLog,
errorsController,
extensionEvents,
stderrEvents,
@@ -859,7 +861,7 @@
}
Future<void> shutdown() async {
- errors.clear();
+ errorLog.clear();
await _errorsController.close();
await _extensionEventsListener.cancel();
await _stderrEventsListener.cancel();
@@ -958,3 +960,52 @@
if (vmServiceUri != null) 'vmServiceUri': vmServiceUri,
});
}
+
+/// Manages a log of errors with a maximum size in terms of total characters.
+@visibleForTesting
+class ErrorLog {
+ Iterable<String> get errors => _errors;
+ final List<String> _errors = [];
+ int _characters = 0;
+
+ /// The number of characters used by all errors in the log.
+ @visibleForTesting
+ int get characters => _characters;
+
+ final int _maxSize;
+
+ ErrorLog({
+ // One token is ~4 characters. Allow up to 5k tokens by default, so 20k
+ // characters.
+ int maxSize = 20000,
+ }) : _maxSize = maxSize;
+
+ /// Adds a new [error] to the log.
+ void add(String error) {
+ if (error.length > _maxSize) {
+ // If we get a single error over the max size, just trim it and clear
+ // all other errors.
+ final trimmed = error.substring(0, _maxSize);
+ _errors.clear();
+ _characters = trimmed.length;
+ _errors.add(trimmed);
+ } else {
+ // Otherwise, we append the error and then remove as many errors from the
+ // front as we need to in order to get under the max size.
+ _characters += error.length;
+ _errors.add(error);
+ var removeCount = 0;
+ while (_characters > _maxSize) {
+ _characters -= _errors[removeCount].length;
+ removeCount++;
+ }
+ _errors.removeRange(0, removeCount);
+ }
+ }
+
+ /// Clears all errors.
+ void clear() {
+ _characters = 0;
+ _errors.clear();
+ }
+}
diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart
index 83849e3..349a7fe 100644
--- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart
@@ -576,6 +576,64 @@
});
});
});
+
+ group('ErrorLog', () {
+ test('adds errors and respects max size', () {
+ final log = ErrorLog(maxSize: 10);
+ log.add('abc');
+ expect(log.errors, ['abc']);
+ expect(log.characters, 3);
+
+ log.add('defg');
+ expect(log.errors, ['abc', 'defg']);
+ expect(log.characters, 7);
+
+ log.add('hijkl');
+ expect(log.errors, ['defg', 'hijkl']);
+ expect(log.characters, 9);
+
+ log.add('mnopq');
+ expect(log.errors, ['hijkl', 'mnopq']);
+ expect(log.characters, 10);
+ });
+
+ test('handles single error larger than max size', () {
+ final log = ErrorLog(maxSize: 10);
+ log.add('abcdefghijkl');
+ expect(log.errors, ['abcdefghij']);
+ expect(log.characters, 10);
+
+ log.add('mnopqrstuvwxyz');
+ expect(log.errors, ['mnopqrstuv']);
+ expect(log.characters, 10);
+ });
+
+ test('clear removes all errors', () {
+ final log = ErrorLog(maxSize: 10);
+ log
+ ..add('abc')
+ ..add('def');
+ log.clear();
+ expect(log.errors, isEmpty);
+ expect(log.characters, 0);
+ });
+
+ test('add, clear,clear and then add again', () {
+ final log = ErrorLog(maxSize: 10);
+ log
+ ..add('abc')
+ ..add('def');
+ log.clear();
+ expect(log.errors, isEmpty);
+ expect(log.characters, 0);
+ log.add('ghi');
+ expect(log.errors, ['ghi']);
+ expect(log.characters, 3);
+ log.add('jklmnopqrstuv');
+ expect(log.errors, ['jklmnopqrs']);
+ expect(log.characters, 10);
+ });
+ });
}
extension on Iterable<Resource> {