neoagent 3.0.1-beta.30 → 3.0.1-beta.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/automation.md +20 -0
- package/flutter_app/lib/main_controller.dart +2 -0
- package/flutter_app/lib/main_models.dart +52 -0
- package/flutter_app/lib/main_operations.dart +154 -0
- package/flutter_app/lib/main_timeline.dart +979 -858
- package/flutter_app/lib/src/backend_client.dart +2 -0
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +69274 -69128
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/conversation_loop.js +31 -0
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/tasks/runtime.js +92 -0
- package/server/services/tasks/task_repository.js +13 -0
package/docs/automation.md
CHANGED
|
@@ -85,6 +85,26 @@ Open **Runs** to inspect the trigger, tool calls, approvals, output, and error
|
|
|
85
85
|
for each execution. Delivery requires a configured messaging destination; a
|
|
86
86
|
completed run can still have a delivery error.
|
|
87
87
|
|
|
88
|
+
## Loop budgets
|
|
89
|
+
|
|
90
|
+
Scheduled and event-triggered tasks run autonomously under a per-task loop
|
|
91
|
+
budget. By default, a task can run 24 times or spend 250k model tokens per day.
|
|
92
|
+
At the cap, or when paused, the task is skipped before a model call.
|
|
93
|
+
|
|
94
|
+
Advanced task configs can override the defaults:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"loopBudget": {
|
|
99
|
+
"maxRunsPerDay": 12,
|
|
100
|
+
"maxTokensPerDay": 150000,
|
|
101
|
+
"paused": false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Set `loopBudget.paused` or `loopPaused` to `true` as a per-task kill switch.
|
|
107
|
+
|
|
88
108
|
## Safety
|
|
89
109
|
|
|
90
110
|
- Start with read-only integration accounts.
|
|
@@ -6076,6 +6076,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
6076
6076
|
required String triggerType,
|
|
6077
6077
|
required Map<String, dynamic> triggerConfig,
|
|
6078
6078
|
required String prompt,
|
|
6079
|
+
Map<String, dynamic>? taskConfig,
|
|
6079
6080
|
String? model,
|
|
6080
6081
|
bool enabled = true,
|
|
6081
6082
|
String? agentId,
|
|
@@ -6087,6 +6088,7 @@ class NeoAgentController extends ChangeNotifier {
|
|
|
6087
6088
|
triggerType: triggerType,
|
|
6088
6089
|
triggerConfig: triggerConfig,
|
|
6089
6090
|
prompt: prompt,
|
|
6091
|
+
taskConfig: taskConfig,
|
|
6090
6092
|
model: model,
|
|
6091
6093
|
enabled: enabled,
|
|
6092
6094
|
agentId: agentId ?? _scopedAgentId,
|
|
@@ -3237,6 +3237,8 @@ class TaskItem {
|
|
|
3237
3237
|
required this.triggerType,
|
|
3238
3238
|
required this.triggerSummary,
|
|
3239
3239
|
required this.triggerConfig,
|
|
3240
|
+
required this.taskConfig,
|
|
3241
|
+
required this.loopBudget,
|
|
3240
3242
|
required this.nextRun,
|
|
3241
3243
|
required this.prompt,
|
|
3242
3244
|
required this.model,
|
|
@@ -3268,6 +3270,11 @@ class TaskItem {
|
|
|
3268
3270
|
: const <String, dynamic>{}),
|
|
3269
3271
|
};
|
|
3270
3272
|
final triggerSummary = json['triggerSummary']?.toString() ?? '';
|
|
3273
|
+
final loopBudgetJson = json['loopBudget'] is Map
|
|
3274
|
+
? Map<String, dynamic>.from(json['loopBudget'] as Map)
|
|
3275
|
+
: (taskConfig['loopBudget'] is Map
|
|
3276
|
+
? Map<String, dynamic>.from(taskConfig['loopBudget'] as Map)
|
|
3277
|
+
: const <String, dynamic>{});
|
|
3271
3278
|
return TaskItem(
|
|
3272
3279
|
id: _asInt(json['id']),
|
|
3273
3280
|
agentId: json['agentId']?.toString() ?? json['agent_id']?.toString(),
|
|
@@ -3277,6 +3284,8 @@ class TaskItem {
|
|
|
3277
3284
|
? 'Task trigger'
|
|
3278
3285
|
: triggerSummary,
|
|
3279
3286
|
triggerConfig: triggerConfig,
|
|
3287
|
+
taskConfig: taskConfig,
|
|
3288
|
+
loopBudget: TaskLoopBudget.fromJson(loopBudgetJson),
|
|
3280
3289
|
nextRun: _parseOptionalTimestamp(json['nextRun']?.toString()),
|
|
3281
3290
|
prompt:
|
|
3282
3291
|
json['prompt']?.toString().ifEmpty(
|
|
@@ -3312,6 +3321,8 @@ class TaskItem {
|
|
|
3312
3321
|
final String triggerType;
|
|
3313
3322
|
final String triggerSummary;
|
|
3314
3323
|
final Map<String, dynamic> triggerConfig;
|
|
3324
|
+
final Map<String, dynamic> taskConfig;
|
|
3325
|
+
final TaskLoopBudget loopBudget;
|
|
3315
3326
|
final DateTime? nextRun;
|
|
3316
3327
|
final String prompt;
|
|
3317
3328
|
final String model;
|
|
@@ -3335,6 +3346,47 @@ class TaskItem {
|
|
|
3335
3346
|
bool get isWidgetRefresh => taskType == 'widget_refresh';
|
|
3336
3347
|
}
|
|
3337
3348
|
|
|
3349
|
+
class TaskLoopBudget {
|
|
3350
|
+
const TaskLoopBudget({
|
|
3351
|
+
required this.enabled,
|
|
3352
|
+
required this.paused,
|
|
3353
|
+
required this.maxRunsPerDay,
|
|
3354
|
+
required this.maxTokensPerDay,
|
|
3355
|
+
});
|
|
3356
|
+
|
|
3357
|
+
factory TaskLoopBudget.fromJson(Map<String, dynamic> json) {
|
|
3358
|
+
final maxRuns = _asInt(json['maxRunsPerDay'] ?? json['max_runs_per_day']);
|
|
3359
|
+
final maxTokens = _asInt(
|
|
3360
|
+
json['maxTokensPerDay'] ?? json['max_tokens_per_day'],
|
|
3361
|
+
);
|
|
3362
|
+
final enabled = json['enabled'] == false || json['enabled'] == 'false'
|
|
3363
|
+
? false
|
|
3364
|
+
: true;
|
|
3365
|
+
return TaskLoopBudget(
|
|
3366
|
+
enabled: enabled,
|
|
3367
|
+
paused:
|
|
3368
|
+
json['paused'] == true ||
|
|
3369
|
+
json['pause'] == true ||
|
|
3370
|
+
json['paused'] == 'true' ||
|
|
3371
|
+
json['pause'] == 'true',
|
|
3372
|
+
maxRunsPerDay: maxRuns > 0 ? maxRuns : 24,
|
|
3373
|
+
maxTokensPerDay: maxTokens > 0 ? maxTokens : 250000,
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
final bool enabled;
|
|
3378
|
+
final bool paused;
|
|
3379
|
+
final int maxRunsPerDay;
|
|
3380
|
+
final int maxTokensPerDay;
|
|
3381
|
+
|
|
3382
|
+
Map<String, dynamic> toJson() => <String, dynamic>{
|
|
3383
|
+
'enabled': enabled,
|
|
3384
|
+
'paused': paused,
|
|
3385
|
+
'maxRunsPerDay': maxRunsPerDay,
|
|
3386
|
+
'maxTokensPerDay': maxTokensPerDay,
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3338
3390
|
class WidgetSnapshotItem {
|
|
3339
3391
|
const WidgetSnapshotItem({
|
|
3340
3392
|
required this.id,
|
|
@@ -5296,6 +5296,24 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5296
5296
|
}
|
|
5297
5297
|
}
|
|
5298
5298
|
|
|
5299
|
+
String _compactBudgetNumber(int value) {
|
|
5300
|
+
if (value >= 1000000) {
|
|
5301
|
+
final millions = value / 1000000;
|
|
5302
|
+
return '${millions.toStringAsFixed(millions >= 10 ? 0 : 1)}M';
|
|
5303
|
+
}
|
|
5304
|
+
if (value >= 1000) {
|
|
5305
|
+
final thousands = value / 1000;
|
|
5306
|
+
return '${thousands.toStringAsFixed(thousands >= 10 ? 0 : 1)}k';
|
|
5307
|
+
}
|
|
5308
|
+
return '$value';
|
|
5309
|
+
}
|
|
5310
|
+
|
|
5311
|
+
String _taskBudgetLabel(TaskItem task) {
|
|
5312
|
+
final budget = task.loopBudget;
|
|
5313
|
+
return 'Budget: ${budget.maxRunsPerDay} runs/day · '
|
|
5314
|
+
'${_compactBudgetNumber(budget.maxTokensPerDay)} tokens/day';
|
|
5315
|
+
}
|
|
5316
|
+
|
|
5299
5317
|
Future<void> _showLastRun(TaskItem task) async {
|
|
5300
5318
|
final runId = task.lastRunId.trim();
|
|
5301
5319
|
if (runId.isEmpty) return;
|
|
@@ -5512,6 +5530,10 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5512
5530
|
label: task.enabled ? 'Active' : 'Paused',
|
|
5513
5531
|
color: task.enabled ? _success : _textSecondary,
|
|
5514
5532
|
),
|
|
5533
|
+
if (task.loopBudget.paused) ...<Widget>[
|
|
5534
|
+
const SizedBox(width: 8),
|
|
5535
|
+
_StatusPill(label: 'Loop paused', color: _warning),
|
|
5536
|
+
],
|
|
5515
5537
|
if (task.hasLastRunStatus) ...<Widget>[
|
|
5516
5538
|
const SizedBox(width: 8),
|
|
5517
5539
|
_StatusPill(
|
|
@@ -5542,6 +5564,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5542
5564
|
style: TextStyle(color: _textSecondary),
|
|
5543
5565
|
),
|
|
5544
5566
|
const SizedBox(height: 8),
|
|
5567
|
+
Text(
|
|
5568
|
+
_taskBudgetLabel(task),
|
|
5569
|
+
style: TextStyle(color: _textSecondary),
|
|
5570
|
+
),
|
|
5571
|
+
const SizedBox(height: 8),
|
|
5545
5572
|
Text(task.prompt, style: TextStyle(color: _textPrimary)),
|
|
5546
5573
|
if (task.lastRunLabel.isNotEmpty) ...<Widget>[
|
|
5547
5574
|
const SizedBox(height: 8),
|
|
@@ -5631,6 +5658,10 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5631
5658
|
label: task.enabled ? 'Active' : 'Paused',
|
|
5632
5659
|
color: task.enabled ? _success : _textSecondary,
|
|
5633
5660
|
),
|
|
5661
|
+
if (task.loopBudget.paused) ...<Widget>[
|
|
5662
|
+
const SizedBox(width: 8),
|
|
5663
|
+
_StatusPill(label: 'Loop paused', color: _warning),
|
|
5664
|
+
],
|
|
5634
5665
|
if (task.hasLastRunStatus) ...<Widget>[
|
|
5635
5666
|
const SizedBox(width: 8),
|
|
5636
5667
|
_StatusPill(
|
|
@@ -5660,6 +5691,11 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5660
5691
|
style: TextStyle(color: _textSecondary),
|
|
5661
5692
|
),
|
|
5662
5693
|
const SizedBox(height: 8),
|
|
5694
|
+
Text(
|
|
5695
|
+
_taskBudgetLabel(task),
|
|
5696
|
+
style: TextStyle(color: _textSecondary),
|
|
5697
|
+
),
|
|
5698
|
+
const SizedBox(height: 8),
|
|
5663
5699
|
Text(
|
|
5664
5700
|
linkedWidget.prompt,
|
|
5665
5701
|
style: TextStyle(color: _textPrimary),
|
|
@@ -5841,7 +5877,15 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
5841
5877
|
text: task?.triggerConfig['sender']?.toString() ?? '',
|
|
5842
5878
|
);
|
|
5843
5879
|
final promptController = TextEditingController(text: task?.prompt ?? '');
|
|
5880
|
+
final loopBudget = task?.loopBudget ?? TaskLoopBudget.fromJson(const {});
|
|
5881
|
+
final maxRunsController = TextEditingController(
|
|
5882
|
+
text: loopBudget.maxRunsPerDay.toString(),
|
|
5883
|
+
);
|
|
5884
|
+
final maxTokensController = TextEditingController(
|
|
5885
|
+
text: loopBudget.maxTokensPerDay.toString(),
|
|
5886
|
+
);
|
|
5844
5887
|
var enabled = task?.enabled ?? true;
|
|
5888
|
+
var loopPaused = loopBudget.paused;
|
|
5845
5889
|
var unreadOnly = task?.triggerConfig['unreadOnly'] == true;
|
|
5846
5890
|
var ignoreGroups = task?.triggerConfig['ignoreGroups'] == true;
|
|
5847
5891
|
var selectedModel = _ensureModelValue(
|
|
@@ -6144,6 +6188,88 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6144
6188
|
() => selectedModel = value ?? 'auto',
|
|
6145
6189
|
),
|
|
6146
6190
|
),
|
|
6191
|
+
const SizedBox(height: 12),
|
|
6192
|
+
Container(
|
|
6193
|
+
width: double.infinity,
|
|
6194
|
+
padding: const EdgeInsets.all(14),
|
|
6195
|
+
decoration: BoxDecoration(
|
|
6196
|
+
border: Border.all(
|
|
6197
|
+
color: _border.withValues(alpha: 0.8),
|
|
6198
|
+
),
|
|
6199
|
+
borderRadius: BorderRadius.circular(12),
|
|
6200
|
+
),
|
|
6201
|
+
child: Column(
|
|
6202
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
6203
|
+
children: <Widget>[
|
|
6204
|
+
Row(
|
|
6205
|
+
children: <Widget>[
|
|
6206
|
+
Icon(
|
|
6207
|
+
Icons.speed_outlined,
|
|
6208
|
+
size: 18,
|
|
6209
|
+
color: _textSecondary,
|
|
6210
|
+
),
|
|
6211
|
+
const SizedBox(width: 8),
|
|
6212
|
+
Text(
|
|
6213
|
+
'Loop budget',
|
|
6214
|
+
style: TextStyle(
|
|
6215
|
+
fontWeight: FontWeight.w700,
|
|
6216
|
+
color: _textPrimary,
|
|
6217
|
+
),
|
|
6218
|
+
),
|
|
6219
|
+
],
|
|
6220
|
+
),
|
|
6221
|
+
const SizedBox(height: 12),
|
|
6222
|
+
LayoutBuilder(
|
|
6223
|
+
builder: (context, constraints) {
|
|
6224
|
+
final stacked = constraints.maxWidth < 520;
|
|
6225
|
+
final fields = <Widget>[
|
|
6226
|
+
TextField(
|
|
6227
|
+
controller: maxRunsController,
|
|
6228
|
+
keyboardType: TextInputType.number,
|
|
6229
|
+
decoration: const InputDecoration(
|
|
6230
|
+
labelText: 'Max runs per day',
|
|
6231
|
+
),
|
|
6232
|
+
),
|
|
6233
|
+
TextField(
|
|
6234
|
+
controller: maxTokensController,
|
|
6235
|
+
keyboardType: TextInputType.number,
|
|
6236
|
+
decoration: const InputDecoration(
|
|
6237
|
+
labelText: 'Max tokens per day',
|
|
6238
|
+
),
|
|
6239
|
+
),
|
|
6240
|
+
];
|
|
6241
|
+
if (stacked) {
|
|
6242
|
+
return Column(
|
|
6243
|
+
children: <Widget>[
|
|
6244
|
+
fields[0],
|
|
6245
|
+
const SizedBox(height: 12),
|
|
6246
|
+
fields[1],
|
|
6247
|
+
],
|
|
6248
|
+
);
|
|
6249
|
+
}
|
|
6250
|
+
return Row(
|
|
6251
|
+
children: <Widget>[
|
|
6252
|
+
Expanded(child: fields[0]),
|
|
6253
|
+
const SizedBox(width: 12),
|
|
6254
|
+
Expanded(child: fields[1]),
|
|
6255
|
+
],
|
|
6256
|
+
);
|
|
6257
|
+
},
|
|
6258
|
+
),
|
|
6259
|
+
const SizedBox(height: 8),
|
|
6260
|
+
SwitchListTile(
|
|
6261
|
+
value: loopPaused,
|
|
6262
|
+
contentPadding: EdgeInsets.zero,
|
|
6263
|
+
title: const Text('Pause loop budget'),
|
|
6264
|
+
subtitle: const Text(
|
|
6265
|
+
'Skip this task before it calls the model.',
|
|
6266
|
+
),
|
|
6267
|
+
onChanged: (value) =>
|
|
6268
|
+
setLocalState(() => loopPaused = value),
|
|
6269
|
+
),
|
|
6270
|
+
],
|
|
6271
|
+
),
|
|
6272
|
+
),
|
|
6147
6273
|
if (controller.agentProfiles.isNotEmpty) ...<Widget>[
|
|
6148
6274
|
const SizedBox(height: 12),
|
|
6149
6275
|
DropdownButtonFormField<String>(
|
|
@@ -6264,12 +6390,40 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6264
6390
|
triggerConfig['ignoreGroups'] = ignoreGroups;
|
|
6265
6391
|
}
|
|
6266
6392
|
}
|
|
6393
|
+
final maxRuns = int.tryParse(maxRunsController.text.trim());
|
|
6394
|
+
final maxTokens = int.tryParse(
|
|
6395
|
+
maxTokensController.text.trim(),
|
|
6396
|
+
);
|
|
6397
|
+
if (maxRuns == null ||
|
|
6398
|
+
maxRuns <= 0 ||
|
|
6399
|
+
maxTokens == null ||
|
|
6400
|
+
maxTokens <= 0) {
|
|
6401
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
6402
|
+
const SnackBar(
|
|
6403
|
+
content: Text(
|
|
6404
|
+
'Loop budget values must be positive numbers.',
|
|
6405
|
+
),
|
|
6406
|
+
backgroundColor: Colors.red,
|
|
6407
|
+
),
|
|
6408
|
+
);
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
final taskConfig = <String, dynamic>{
|
|
6412
|
+
...?task?.taskConfig,
|
|
6413
|
+
'loopBudget': <String, dynamic>{
|
|
6414
|
+
'enabled': true,
|
|
6415
|
+
'paused': loopPaused,
|
|
6416
|
+
'maxRunsPerDay': maxRuns,
|
|
6417
|
+
'maxTokensPerDay': maxTokens,
|
|
6418
|
+
},
|
|
6419
|
+
};
|
|
6267
6420
|
await controller.saveTask(
|
|
6268
6421
|
id: task?.id,
|
|
6269
6422
|
name: nameController.text.trim(),
|
|
6270
6423
|
triggerType: selectedTriggerType,
|
|
6271
6424
|
triggerConfig: triggerConfig,
|
|
6272
6425
|
prompt: promptController.text.trim(),
|
|
6426
|
+
taskConfig: taskConfig,
|
|
6273
6427
|
model: selectedModel == 'auto' ? null : selectedModel,
|
|
6274
6428
|
enabled: enabled,
|
|
6275
6429
|
agentId: selectedAgentId,
|