Skip to content

Commit c24afc1

Browse files
authored
Limit the tokens returned by the runtime errors tool/resource (#139)
Fixes #134
1 parent 93a9191 commit c24afc1

File tree

3 files changed

+128
-18
lines changed

3 files changed

+128
-18
lines changed

.github/workflows/pull_request_label.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
jobs:
1414
label:
1515
permissions:
16+
issues: write
1617
pull-requests: write
1718
runs-on: ubuntu-latest
1819
steps:

pkgs/dart_mcp_server/lib/src/mixins/dtd.dart

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,14 @@ base mixin DartToolingDaemonSupport
109109
final resource = Resource(
110110
uri: '$runtimeErrorsScheme://${debugSession.id}',
111111
name: debugSession.name,
112+
description:
113+
'Recent runtime errors seen for debug session '
114+
'"${debugSession.name}".',
112115
);
113116
addResource(resource, (request) async {
114-
final errors = errorService.errors;
115117
return ReadResourceResult(
116118
contents: [
117-
for (var error in errors)
119+
for (var error in errorService.errorLog.errors)
118120
TextResourceContents(uri: resource.uri, text: error),
119121
],
120122
);
@@ -294,7 +296,7 @@ base mixin DartToolingDaemonSupport
294296
(await _AppErrorsListener.forVmService(
295297
vmService,
296298
this,
297-
)).errors.clear();
299+
)).errorLog.clear();
298300
}
299301

300302
final vm = await vmService.getVM();
@@ -372,9 +374,9 @@ base mixin DartToolingDaemonSupport
372374
vmService,
373375
this,
374376
);
375-
final errors = errorService.errors;
377+
final errorLog = errorService.errorLog;
376378

377-
if (errors.isEmpty) {
379+
if (errorLog.errors.isEmpty) {
378380
return CallToolResult(
379381
content: [TextContent(text: 'No runtime errors found.')],
380382
);
@@ -383,14 +385,14 @@ base mixin DartToolingDaemonSupport
383385
content: [
384386
TextContent(
385387
text:
386-
'Found ${errors.length} '
387-
'error${errors.length == 1 ? '' : 's'}:\n',
388+
'Found ${errorLog.errors.length} '
389+
'error${errorLog.errors.length == 1 ? '' : 's'}:\n',
388390
),
389-
...errors.map((e) => TextContent(text: e.toString())),
391+
for (final e in errorLog.errors) TextContent(text: e.toString()),
390392
],
391393
);
392394
if (request.arguments?['clearRuntimeErrors'] == true) {
393-
errorService.errors.clear();
395+
errorService.errorLog.clear();
394396
}
395397
return result;
396398
} catch (e) {
@@ -614,9 +616,9 @@ base mixin DartToolingDaemonSupport
614616
static final getRuntimeErrorsTool = Tool(
615617
name: 'get_runtime_errors',
616618
description:
617-
'Retrieves the list of runtime errors that have occurred in the active '
618-
'Dart or Flutter application. Requires "${connectTool.name}" to be '
619-
'successfully called first.',
619+
'Retrieves the most recent runtime errors that have occurred in the '
620+
'active Dart or Flutter application. Requires "${connectTool.name}" to '
621+
'be successfully called first.',
620622
annotations: ToolAnnotations(
621623
title: 'Get runtime errors',
622624
readOnlyHint: true,
@@ -767,7 +769,7 @@ base mixin DartToolingDaemonSupport
767769
/// Listens on a VM service for errors.
768770
class _AppErrorsListener {
769771
/// All the errors recorded so far (may be cleared explicitly).
770-
final List<String> errors;
772+
final ErrorLog errorLog;
771773

772774
/// A broadcast stream of all errors that come in after you start listening.
773775
Stream<String> get errorsStream => _errorsController.stream;
@@ -785,7 +787,7 @@ class _AppErrorsListener {
785787
final VmService _vmService;
786788

787789
_AppErrorsListener._(
788-
this.errors,
790+
this.errorLog,
789791
this._errorsController,
790792
this._extensionEventsListener,
791793
this._stderrEventsListener,
@@ -809,8 +811,8 @@ class _AppErrorsListener {
809811
// list but also expose it to clients so they can know when new errors
810812
// are added.
811813
final errorsController = StreamController<String>.broadcast();
812-
final errors = <String>[];
813-
errorsController.stream.listen(errors.add);
814+
final errorLog = ErrorLog();
815+
errorsController.stream.listen(errorLog.add);
814816
// We need to listen to streams with history so that we can get errors
815817
// that occurred before this tool call.
816818
// TODO(https://github.com/dart-lang/ai/issues/57): this can result in
@@ -849,7 +851,7 @@ class _AppErrorsListener {
849851
logger.log(LoggingLevel.error, 'Error subscribing to app errors: $e');
850852
}
851853
return _AppErrorsListener._(
852-
errors,
854+
errorLog,
853855
errorsController,
854856
extensionEvents,
855857
stderrEvents,
@@ -859,7 +861,7 @@ class _AppErrorsListener {
859861
}
860862

861863
Future<void> shutdown() async {
862-
errors.clear();
864+
errorLog.clear();
863865
await _errorsController.close();
864866
await _extensionEventsListener.cancel();
865867
await _stderrEventsListener.cancel();
@@ -958,3 +960,52 @@ extension type DebugSession.fromJson(Map<String, Object?> _value)
958960
if (vmServiceUri != null) 'vmServiceUri': vmServiceUri,
959961
});
960962
}
963+
964+
/// Manages a log of errors with a maximum size in terms of total characters.
965+
@visibleForTesting
966+
class ErrorLog {
967+
Iterable<String> get errors => _errors;
968+
final List<String> _errors = [];
969+
int _characters = 0;
970+
971+
/// The number of characters used by all errors in the log.
972+
@visibleForTesting
973+
int get characters => _characters;
974+
975+
final int _maxSize;
976+
977+
ErrorLog({
978+
// One token is ~4 characters. Allow up to 5k tokens by default, so 20k
979+
// characters.
980+
int maxSize = 20000,
981+
}) : _maxSize = maxSize;
982+
983+
/// Adds a new [error] to the log.
984+
void add(String error) {
985+
if (error.length > _maxSize) {
986+
// If we get a single error over the max size, just trim it and clear
987+
// all other errors.
988+
final trimmed = error.substring(0, _maxSize);
989+
_errors.clear();
990+
_characters = trimmed.length;
991+
_errors.add(trimmed);
992+
} else {
993+
// Otherwise, we append the error and then remove as many errors from the
994+
// front as we need to in order to get under the max size.
995+
_characters += error.length;
996+
_errors.add(error);
997+
var removeCount = 0;
998+
while (_characters > _maxSize) {
999+
_characters -= _errors[removeCount].length;
1000+
removeCount++;
1001+
}
1002+
_errors.removeRange(0, removeCount);
1003+
}
1004+
}
1005+
1006+
/// Clears all errors.
1007+
void clear() {
1008+
_characters = 0;
1009+
_errors.clear();
1010+
}
1011+
}

pkgs/dart_mcp_server/test/tools/dtd_test.dart

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,64 @@ void main() {
576576
});
577577
});
578578
});
579+
580+
group('ErrorLog', () {
581+
test('adds errors and respects max size', () {
582+
final log = ErrorLog(maxSize: 10);
583+
log.add('abc');
584+
expect(log.errors, ['abc']);
585+
expect(log.characters, 3);
586+
587+
log.add('defg');
588+
expect(log.errors, ['abc', 'defg']);
589+
expect(log.characters, 7);
590+
591+
log.add('hijkl');
592+
expect(log.errors, ['defg', 'hijkl']);
593+
expect(log.characters, 9);
594+
595+
log.add('mnopq');
596+
expect(log.errors, ['hijkl', 'mnopq']);
597+
expect(log.characters, 10);
598+
});
599+
600+
test('handles single error larger than max size', () {
601+
final log = ErrorLog(maxSize: 10);
602+
log.add('abcdefghijkl');
603+
expect(log.errors, ['abcdefghij']);
604+
expect(log.characters, 10);
605+
606+
log.add('mnopqrstuvwxyz');
607+
expect(log.errors, ['mnopqrstuv']);
608+
expect(log.characters, 10);
609+
});
610+
611+
test('clear removes all errors', () {
612+
final log = ErrorLog(maxSize: 10);
613+
log
614+
..add('abc')
615+
..add('def');
616+
log.clear();
617+
expect(log.errors, isEmpty);
618+
expect(log.characters, 0);
619+
});
620+
621+
test('add, clear,clear and then add again', () {
622+
final log = ErrorLog(maxSize: 10);
623+
log
624+
..add('abc')
625+
..add('def');
626+
log.clear();
627+
expect(log.errors, isEmpty);
628+
expect(log.characters, 0);
629+
log.add('ghi');
630+
expect(log.errors, ['ghi']);
631+
expect(log.characters, 3);
632+
log.add('jklmnopqrstuv');
633+
expect(log.errors, ['jklmnopqrs']);
634+
expect(log.characters, 10);
635+
});
636+
});
579637
}
580638

581639
extension on Iterable<Resource> {

0 commit comments

Comments
 (0)