neoagent 3.2.0 → 3.2.1-beta.1
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/agent-run-lifecycle.md +10 -0
- package/flutter_app/lib/main_operations.dart +553 -19
- package/lib/schema_migrations.js +30 -0
- package/package.json +1 -1
- package/server/db/database.js +2 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +90015 -87169
- package/server/routes/agents.js +35 -2
- package/server/services/ai/loop/agent_engine_core.js +111 -9
- package/server/services/ai/loop/conversation_loop.js +123 -21
- package/server/services/ai/loop/lifecycle.js +108 -0
- package/server/services/ai/loop/model_io.js +31 -14
- package/server/services/ai/loop/tool_dispatch.js +9 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/google.js +2 -2
- package/server/services/ai/providers/grok.js +2 -2
- package/server/services/ai/providers/nvidia.js +2 -2
- package/server/services/ai/providers/ollama.js +7 -6
- package/server/services/ai/providers/openai.js +2 -2
- package/server/services/ai/providers/openaiCodex.js +4 -2
- package/server/services/ai/providers/openrouter.js +2 -2
- package/server/services/messaging/formatting_guides.js +1 -0
|
@@ -45,6 +45,12 @@ runs:
|
|
|
45
45
|
The engine records steps, run events, model usage, timing, and artifacts.
|
|
46
46
|
Repetition guards and loop limits prevent unbounded retries.
|
|
47
47
|
|
|
48
|
+
Run control is checked at model and tool boundaries. Abort and interruption
|
|
49
|
+
are terminal; pause cancels the active model or command, records a checkpoint,
|
|
50
|
+
and parks the in-process run until the authenticated resume action releases it.
|
|
51
|
+
If a state-changing tool is interrupted after dispatch, its outcome is recorded
|
|
52
|
+
as unknown and must be verified before the model can attempt it again.
|
|
53
|
+
|
|
48
54
|
## 4. Complete and deliver
|
|
49
55
|
|
|
50
56
|
The final response is sanitized and stored. Messaging-triggered runs send an
|
|
@@ -55,6 +61,10 @@ The engine emits `run:complete`, persists prompt and usage metrics, refreshes
|
|
|
55
61
|
conversation summaries and working state, and cancels unfinished subagents.
|
|
56
62
|
Failures and user stops produce separate terminal run states.
|
|
57
63
|
|
|
64
|
+
Terminal transitions are first-writer-wins. A late model, tool, verifier, or
|
|
65
|
+
delivery callback cannot overwrite an earlier stop or interruption, and active
|
|
66
|
+
run, step, and delegation rows are settled together.
|
|
67
|
+
|
|
58
68
|
## 5. Post-run processing
|
|
59
69
|
|
|
60
70
|
Completed conversations can run structured memory consolidation. The engine
|
|
@@ -5087,7 +5087,284 @@ _TaskTriggerOption _taskTriggerOptionForType(String type) {
|
|
|
5087
5087
|
);
|
|
5088
5088
|
}
|
|
5089
5089
|
|
|
5090
|
+
class _TaskSchedulePreset {
|
|
5091
|
+
const _TaskSchedulePreset({
|
|
5092
|
+
required this.id,
|
|
5093
|
+
required this.label,
|
|
5094
|
+
required this.description,
|
|
5095
|
+
required this.icon,
|
|
5096
|
+
});
|
|
5097
|
+
|
|
5098
|
+
final String id;
|
|
5099
|
+
final String label;
|
|
5100
|
+
final String description;
|
|
5101
|
+
final IconData icon;
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
const List<_TaskSchedulePreset> _taskSchedulePresets = <_TaskSchedulePreset>[
|
|
5105
|
+
_TaskSchedulePreset(
|
|
5106
|
+
id: 'every_15_minutes',
|
|
5107
|
+
label: 'Every 15 minutes',
|
|
5108
|
+
description: 'Runs four times per hour.',
|
|
5109
|
+
icon: Icons.timer_outlined,
|
|
5110
|
+
),
|
|
5111
|
+
_TaskSchedulePreset(
|
|
5112
|
+
id: 'every_30_minutes',
|
|
5113
|
+
label: 'Every 30 minutes',
|
|
5114
|
+
description: 'Runs twice per hour.',
|
|
5115
|
+
icon: Icons.timelapse_rounded,
|
|
5116
|
+
),
|
|
5117
|
+
_TaskSchedulePreset(
|
|
5118
|
+
id: 'hourly',
|
|
5119
|
+
label: 'Hourly',
|
|
5120
|
+
description: 'Runs once per hour.',
|
|
5121
|
+
icon: Icons.schedule_rounded,
|
|
5122
|
+
),
|
|
5123
|
+
_TaskSchedulePreset(
|
|
5124
|
+
id: 'daily',
|
|
5125
|
+
label: 'Daily',
|
|
5126
|
+
description: 'Runs every day at the selected time.',
|
|
5127
|
+
icon: Icons.today_rounded,
|
|
5128
|
+
),
|
|
5129
|
+
_TaskSchedulePreset(
|
|
5130
|
+
id: 'weekdays',
|
|
5131
|
+
label: 'Weekdays',
|
|
5132
|
+
description: 'Runs Monday through Friday.',
|
|
5133
|
+
icon: Icons.work_outline_rounded,
|
|
5134
|
+
),
|
|
5135
|
+
_TaskSchedulePreset(
|
|
5136
|
+
id: 'weekly',
|
|
5137
|
+
label: 'Weekly',
|
|
5138
|
+
description: 'Runs on selected weekdays.',
|
|
5139
|
+
icon: Icons.view_week_rounded,
|
|
5140
|
+
),
|
|
5141
|
+
_TaskSchedulePreset(
|
|
5142
|
+
id: 'monthly',
|
|
5143
|
+
label: 'Monthly',
|
|
5144
|
+
description: 'Runs once per month on the selected day.',
|
|
5145
|
+
icon: Icons.calendar_month_rounded,
|
|
5146
|
+
),
|
|
5147
|
+
_TaskSchedulePreset(
|
|
5148
|
+
id: 'custom',
|
|
5149
|
+
label: 'Custom Cron',
|
|
5150
|
+
description: 'Advanced manual schedule for special cases.',
|
|
5151
|
+
icon: Icons.tune_rounded,
|
|
5152
|
+
),
|
|
5153
|
+
];
|
|
5154
|
+
|
|
5155
|
+
const List<String> _taskWeekdayLabels = <String>[
|
|
5156
|
+
'Mon',
|
|
5157
|
+
'Tue',
|
|
5158
|
+
'Wed',
|
|
5159
|
+
'Thu',
|
|
5160
|
+
'Fri',
|
|
5161
|
+
'Sat',
|
|
5162
|
+
'Sun',
|
|
5163
|
+
];
|
|
5164
|
+
|
|
5165
|
+
_TaskSchedulePreset _taskSchedulePresetForId(String id) {
|
|
5166
|
+
return _taskSchedulePresets.firstWhere(
|
|
5167
|
+
(entry) => entry.id == id,
|
|
5168
|
+
orElse: () => _taskSchedulePresets[1],
|
|
5169
|
+
);
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
class _TaskScheduleDraft {
|
|
5173
|
+
_TaskScheduleDraft({
|
|
5174
|
+
required this.mode,
|
|
5175
|
+
required this.presetId,
|
|
5176
|
+
required this.time,
|
|
5177
|
+
required this.weekdays,
|
|
5178
|
+
required this.monthDay,
|
|
5179
|
+
required this.customCronExpression,
|
|
5180
|
+
});
|
|
5181
|
+
|
|
5182
|
+
factory _TaskScheduleDraft.fromTask(TaskItem? task) {
|
|
5183
|
+
final runAt = task?.triggerConfig['runAt']?.toString().trim() ?? '';
|
|
5184
|
+
if (runAt.isNotEmpty) {
|
|
5185
|
+
return _TaskScheduleDraft(
|
|
5186
|
+
mode: 'one_time',
|
|
5187
|
+
presetId: 'daily',
|
|
5188
|
+
time: const TimeOfDay(hour: 9, minute: 0),
|
|
5189
|
+
weekdays: <int>{1},
|
|
5190
|
+
monthDay: 1,
|
|
5191
|
+
customCronExpression: '',
|
|
5192
|
+
);
|
|
5193
|
+
}
|
|
5194
|
+
final cron =
|
|
5195
|
+
task?.triggerConfig['cronExpression']?.toString().trim() ??
|
|
5196
|
+
'*/30 * * * *';
|
|
5197
|
+
final parsed = _parseCronExpression(cron);
|
|
5198
|
+
if (parsed != null) return parsed;
|
|
5199
|
+
return _TaskScheduleDraft(
|
|
5200
|
+
mode: 'recurring',
|
|
5201
|
+
presetId: 'custom',
|
|
5202
|
+
time: const TimeOfDay(hour: 9, minute: 0),
|
|
5203
|
+
weekdays: <int>{1},
|
|
5204
|
+
monthDay: 1,
|
|
5205
|
+
customCronExpression: cron,
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
|
|
5209
|
+
String mode;
|
|
5210
|
+
String presetId;
|
|
5211
|
+
TimeOfDay time;
|
|
5212
|
+
Set<int> weekdays;
|
|
5213
|
+
int monthDay;
|
|
5214
|
+
String customCronExpression;
|
|
5215
|
+
|
|
5216
|
+
bool get usesTime =>
|
|
5217
|
+
presetId == 'daily' ||
|
|
5218
|
+
presetId == 'weekdays' ||
|
|
5219
|
+
presetId == 'weekly' ||
|
|
5220
|
+
presetId == 'monthly';
|
|
5221
|
+
|
|
5222
|
+
String get cronExpression {
|
|
5223
|
+
if (presetId == 'custom') return customCronExpression;
|
|
5224
|
+
final minute = time.minute.toString();
|
|
5225
|
+
final hour = time.hour.toString();
|
|
5226
|
+
return switch (presetId) {
|
|
5227
|
+
'every_15_minutes' => '*/15 * * * *',
|
|
5228
|
+
'every_30_minutes' => '*/30 * * * *',
|
|
5229
|
+
'hourly' => '0 * * * *',
|
|
5230
|
+
'daily' => '$minute $hour * * *',
|
|
5231
|
+
'weekdays' => '$minute $hour * * 1-5',
|
|
5232
|
+
'weekly' => '$minute $hour * * ${_cronWeekdays(weekdays)}',
|
|
5233
|
+
'monthly' => '$minute $hour $monthDay * *',
|
|
5234
|
+
'custom' => customCronExpression,
|
|
5235
|
+
_ => '*/30 * * * *',
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
|
|
5239
|
+
String get summary {
|
|
5240
|
+
if (mode == 'one_time') return 'One-time run';
|
|
5241
|
+
if (presetId == 'custom') return 'Custom Cron';
|
|
5242
|
+
final preset = _taskSchedulePresetForId(presetId);
|
|
5243
|
+
if (!usesTime) return preset.label;
|
|
5244
|
+
final timeLabel = _formatTaskScheduleTime(time);
|
|
5245
|
+
if (presetId == 'weekly') {
|
|
5246
|
+
return '${preset.label} ${_formatTaskWeekdays(weekdays)} at $timeLabel';
|
|
5247
|
+
}
|
|
5248
|
+
if (presetId == 'monthly') {
|
|
5249
|
+
return '${preset.label} on day $monthDay at $timeLabel';
|
|
5250
|
+
}
|
|
5251
|
+
return '${preset.label} at $timeLabel';
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
_TaskScheduleDraft? _parseCronExpression(String cron) {
|
|
5256
|
+
final fields = cron.trim().split(RegExp(r'\s+'));
|
|
5257
|
+
if (fields.length != 5) return null;
|
|
5258
|
+
final minute = fields[0];
|
|
5259
|
+
final hour = fields[1];
|
|
5260
|
+
final dayOfMonth = fields[2];
|
|
5261
|
+
final month = fields[3];
|
|
5262
|
+
final dayOfWeek = fields[4];
|
|
5263
|
+
if (cron == '*/15 * * * *') {
|
|
5264
|
+
return _recurringScheduleDraft('every_15_minutes');
|
|
5265
|
+
}
|
|
5266
|
+
if (cron == '*/30 * * * *') {
|
|
5267
|
+
return _recurringScheduleDraft('every_30_minutes');
|
|
5268
|
+
}
|
|
5269
|
+
if (cron == '0 * * * *') return _recurringScheduleDraft('hourly');
|
|
5270
|
+
final parsedMinute = int.tryParse(minute);
|
|
5271
|
+
final parsedHour = int.tryParse(hour);
|
|
5272
|
+
if (parsedMinute == null ||
|
|
5273
|
+
parsedMinute < 0 ||
|
|
5274
|
+
parsedMinute > 59 ||
|
|
5275
|
+
parsedHour == null ||
|
|
5276
|
+
parsedHour < 0 ||
|
|
5277
|
+
parsedHour > 23 ||
|
|
5278
|
+
month != '*') {
|
|
5279
|
+
return null;
|
|
5280
|
+
}
|
|
5281
|
+
final time = TimeOfDay(hour: parsedHour, minute: parsedMinute);
|
|
5282
|
+
if (dayOfMonth == '*' && dayOfWeek == '*') {
|
|
5283
|
+
return _recurringScheduleDraft('daily', time: time);
|
|
5284
|
+
}
|
|
5285
|
+
if (dayOfMonth == '*' && dayOfWeek == '1-5') {
|
|
5286
|
+
return _recurringScheduleDraft('weekdays', time: time);
|
|
5287
|
+
}
|
|
5288
|
+
if (dayOfMonth == '*') {
|
|
5289
|
+
final weekdays = _parseCronWeekdays(dayOfWeek);
|
|
5290
|
+
if (weekdays == null || weekdays.isEmpty) return null;
|
|
5291
|
+
return _recurringScheduleDraft('weekly', time: time, weekdays: weekdays);
|
|
5292
|
+
}
|
|
5293
|
+
if (dayOfWeek == '*') {
|
|
5294
|
+
final parsedDay = int.tryParse(dayOfMonth);
|
|
5295
|
+
if (parsedDay == null || parsedDay < 1 || parsedDay > 31) return null;
|
|
5296
|
+
return _recurringScheduleDraft('monthly', time: time, monthDay: parsedDay);
|
|
5297
|
+
}
|
|
5298
|
+
return null;
|
|
5299
|
+
}
|
|
5300
|
+
|
|
5301
|
+
bool _looksLikeCronExpression(String cron) {
|
|
5302
|
+
return cron.trim().split(RegExp(r'\s+')).length == 5;
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
_TaskScheduleDraft _recurringScheduleDraft(
|
|
5306
|
+
String presetId, {
|
|
5307
|
+
TimeOfDay time = const TimeOfDay(hour: 9, minute: 0),
|
|
5308
|
+
Set<int> weekdays = const <int>{1},
|
|
5309
|
+
int monthDay = 1,
|
|
5310
|
+
}) {
|
|
5311
|
+
return _TaskScheduleDraft(
|
|
5312
|
+
mode: 'recurring',
|
|
5313
|
+
presetId: presetId,
|
|
5314
|
+
time: time,
|
|
5315
|
+
weekdays: Set<int>.from(weekdays),
|
|
5316
|
+
monthDay: monthDay,
|
|
5317
|
+
customCronExpression: '',
|
|
5318
|
+
);
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
Set<int>? _parseCronWeekdays(String value) {
|
|
5322
|
+
final result = <int>{};
|
|
5323
|
+
for (final part in value.split(',')) {
|
|
5324
|
+
final trimmed = part.trim();
|
|
5325
|
+
if (trimmed.isEmpty) return null;
|
|
5326
|
+
final rangeParts = trimmed.split('-');
|
|
5327
|
+
if (rangeParts.length == 2) {
|
|
5328
|
+
final start = _parseCronWeekday(rangeParts[0]);
|
|
5329
|
+
final end = _parseCronWeekday(rangeParts[1]);
|
|
5330
|
+
if (start == null || end == null || start > end) return null;
|
|
5331
|
+
for (var day = start; day <= end; day += 1) {
|
|
5332
|
+
result.add(day);
|
|
5333
|
+
}
|
|
5334
|
+
} else if (rangeParts.length == 1) {
|
|
5335
|
+
final day = _parseCronWeekday(trimmed);
|
|
5336
|
+
if (day == null) return null;
|
|
5337
|
+
result.add(day);
|
|
5338
|
+
} else {
|
|
5339
|
+
return null;
|
|
5340
|
+
}
|
|
5341
|
+
}
|
|
5342
|
+
return result;
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
int? _parseCronWeekday(String value) {
|
|
5346
|
+
final parsed = int.tryParse(value.trim());
|
|
5347
|
+
if (parsed == null || parsed < 0 || parsed > 7) return null;
|
|
5348
|
+
return parsed == 0 ? 7 : parsed;
|
|
5349
|
+
}
|
|
5090
5350
|
|
|
5351
|
+
String _cronWeekdays(Set<int> weekdays) {
|
|
5352
|
+
final sorted = weekdays.where((day) => day >= 1 && day <= 7).toList()..sort();
|
|
5353
|
+
if (sorted.isEmpty) return '1';
|
|
5354
|
+
return sorted.join(',');
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
String _formatTaskScheduleTime(TimeOfDay time) {
|
|
5358
|
+
final hour = time.hour.toString().padLeft(2, '0');
|
|
5359
|
+
final minute = time.minute.toString().padLeft(2, '0');
|
|
5360
|
+
return '$hour:$minute';
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
String _formatTaskWeekdays(Set<int> weekdays) {
|
|
5364
|
+
final sorted = weekdays.where((day) => day >= 1 && day <= 7).toList()..sort();
|
|
5365
|
+
if (sorted.isEmpty) return 'Monday';
|
|
5366
|
+
return sorted.map((day) => _taskWeekdayLabels[day - 1]).join(', ');
|
|
5367
|
+
}
|
|
5091
5368
|
Future<String?> _pickTaskTriggerType(
|
|
5092
5369
|
BuildContext context,
|
|
5093
5370
|
String selectedType,
|
|
@@ -6255,8 +6532,9 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6255
6532
|
}) async {
|
|
6256
6533
|
final nameController = TextEditingController(text: task?.name ?? '');
|
|
6257
6534
|
final triggerType = ValueNotifier<String>(task?.triggerType ?? 'schedule');
|
|
6258
|
-
final
|
|
6259
|
-
|
|
6535
|
+
final scheduleDraft = _TaskScheduleDraft.fromTask(task);
|
|
6536
|
+
final customCronController = TextEditingController(
|
|
6537
|
+
text: scheduleDraft.customCronExpression,
|
|
6260
6538
|
);
|
|
6261
6539
|
final runAtController = TextEditingController(
|
|
6262
6540
|
text: task?.triggerConfig['runAt']?.toString() ?? '',
|
|
@@ -6463,21 +6741,246 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6463
6741
|
if (selectedTriggerType == 'schedule') {
|
|
6464
6742
|
return Column(
|
|
6465
6743
|
children: <Widget>[
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6744
|
+
SegmentedButton<String>(
|
|
6745
|
+
segments: const <ButtonSegment<String>>[
|
|
6746
|
+
ButtonSegment<String>(
|
|
6747
|
+
value: 'recurring',
|
|
6748
|
+
icon: Icon(Icons.repeat_rounded),
|
|
6749
|
+
label: Text('Recurring'),
|
|
6750
|
+
),
|
|
6751
|
+
ButtonSegment<String>(
|
|
6752
|
+
value: 'one_time',
|
|
6753
|
+
icon: Icon(Icons.event_rounded),
|
|
6754
|
+
label: Text('Once'),
|
|
6755
|
+
),
|
|
6756
|
+
],
|
|
6757
|
+
selected: <String>{scheduleDraft.mode},
|
|
6758
|
+
onSelectionChanged: (selection) {
|
|
6759
|
+
setLocalState(() {
|
|
6760
|
+
scheduleDraft.mode = selection.first;
|
|
6761
|
+
if (scheduleDraft.mode == 'recurring') {
|
|
6762
|
+
runAtController.clear();
|
|
6763
|
+
}
|
|
6764
|
+
});
|
|
6765
|
+
},
|
|
6473
6766
|
),
|
|
6474
6767
|
const SizedBox(height: 12),
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6768
|
+
if (scheduleDraft.mode == 'one_time')
|
|
6769
|
+
TextField(
|
|
6770
|
+
controller: runAtController,
|
|
6771
|
+
decoration: const InputDecoration(
|
|
6772
|
+
labelText: 'Run At',
|
|
6773
|
+
helperText:
|
|
6774
|
+
'Use a date and time, for example 2026-07-03T09:00:00.',
|
|
6775
|
+
),
|
|
6776
|
+
)
|
|
6777
|
+
else ...<Widget>[
|
|
6778
|
+
DropdownButtonFormField<String>(
|
|
6779
|
+
initialValue: scheduleDraft.presetId,
|
|
6780
|
+
isExpanded: true,
|
|
6781
|
+
decoration: const InputDecoration(
|
|
6782
|
+
labelText: 'Repeat',
|
|
6783
|
+
),
|
|
6784
|
+
items: <DropdownMenuItem<String>>[
|
|
6785
|
+
..._taskSchedulePresets.map(
|
|
6786
|
+
(preset) => DropdownMenuItem<String>(
|
|
6787
|
+
value: preset.id,
|
|
6788
|
+
child: Text(preset.label),
|
|
6789
|
+
),
|
|
6790
|
+
),
|
|
6791
|
+
],
|
|
6792
|
+
onChanged: (value) {
|
|
6793
|
+
if (value == null) return;
|
|
6794
|
+
setLocalState(() {
|
|
6795
|
+
final currentCron =
|
|
6796
|
+
scheduleDraft.cronExpression;
|
|
6797
|
+
scheduleDraft.presetId = value;
|
|
6798
|
+
if (value == 'custom' &&
|
|
6799
|
+
customCronController.text
|
|
6800
|
+
.trim()
|
|
6801
|
+
.isEmpty) {
|
|
6802
|
+
customCronController.text =
|
|
6803
|
+
currentCron;
|
|
6804
|
+
scheduleDraft.customCronExpression =
|
|
6805
|
+
currentCron;
|
|
6806
|
+
}
|
|
6807
|
+
});
|
|
6808
|
+
},
|
|
6479
6809
|
),
|
|
6480
|
-
|
|
6810
|
+
const SizedBox(height: 12),
|
|
6811
|
+
if (scheduleDraft.presetId ==
|
|
6812
|
+
'custom') ...<Widget>[
|
|
6813
|
+
TextField(
|
|
6814
|
+
controller: customCronController,
|
|
6815
|
+
onChanged: (value) =>
|
|
6816
|
+
scheduleDraft.customCronExpression =
|
|
6817
|
+
value,
|
|
6818
|
+
decoration: const InputDecoration(
|
|
6819
|
+
labelText: 'Cron expression',
|
|
6820
|
+
helperText:
|
|
6821
|
+
'Advanced: minute hour day month weekday.',
|
|
6822
|
+
),
|
|
6823
|
+
),
|
|
6824
|
+
const SizedBox(height: 12),
|
|
6825
|
+
],
|
|
6826
|
+
if (scheduleDraft.usesTime) ...<Widget>[
|
|
6827
|
+
InkWell(
|
|
6828
|
+
borderRadius: BorderRadius.circular(14),
|
|
6829
|
+
onTap: () async {
|
|
6830
|
+
final picked = await showTimePicker(
|
|
6831
|
+
context: context,
|
|
6832
|
+
initialTime: scheduleDraft.time,
|
|
6833
|
+
builder: (context, child) {
|
|
6834
|
+
return MediaQuery(
|
|
6835
|
+
data: MediaQuery.of(context)
|
|
6836
|
+
.copyWith(
|
|
6837
|
+
alwaysUse24HourFormat: true,
|
|
6838
|
+
),
|
|
6839
|
+
child: child!,
|
|
6840
|
+
);
|
|
6841
|
+
},
|
|
6842
|
+
);
|
|
6843
|
+
if (picked == null) return;
|
|
6844
|
+
setLocalState(() {
|
|
6845
|
+
scheduleDraft.time = picked;
|
|
6846
|
+
});
|
|
6847
|
+
},
|
|
6848
|
+
child: InputDecorator(
|
|
6849
|
+
decoration: const InputDecoration(
|
|
6850
|
+
labelText: 'Time',
|
|
6851
|
+
),
|
|
6852
|
+
child: Row(
|
|
6853
|
+
children: <Widget>[
|
|
6854
|
+
const Icon(
|
|
6855
|
+
Icons.access_time_rounded,
|
|
6856
|
+
),
|
|
6857
|
+
const SizedBox(width: 12),
|
|
6858
|
+
Expanded(
|
|
6859
|
+
child: Text(
|
|
6860
|
+
_formatTaskScheduleTime(
|
|
6861
|
+
scheduleDraft.time,
|
|
6862
|
+
),
|
|
6863
|
+
style: const TextStyle(
|
|
6864
|
+
fontWeight: FontWeight.w700,
|
|
6865
|
+
),
|
|
6866
|
+
),
|
|
6867
|
+
),
|
|
6868
|
+
Icon(
|
|
6869
|
+
Icons.unfold_more_rounded,
|
|
6870
|
+
color: _textSecondary,
|
|
6871
|
+
),
|
|
6872
|
+
],
|
|
6873
|
+
),
|
|
6874
|
+
),
|
|
6875
|
+
),
|
|
6876
|
+
const SizedBox(height: 12),
|
|
6877
|
+
],
|
|
6878
|
+
if (scheduleDraft.presetId ==
|
|
6879
|
+
'weekly') ...<Widget>[
|
|
6880
|
+
Align(
|
|
6881
|
+
alignment: Alignment.centerLeft,
|
|
6882
|
+
child: Wrap(
|
|
6883
|
+
spacing: 8,
|
|
6884
|
+
runSpacing: 8,
|
|
6885
|
+
children: List<Widget>.generate(7, (
|
|
6886
|
+
index,
|
|
6887
|
+
) {
|
|
6888
|
+
final day = index + 1;
|
|
6889
|
+
return ChoiceChip(
|
|
6890
|
+
label: Text(
|
|
6891
|
+
_taskWeekdayLabels[index],
|
|
6892
|
+
),
|
|
6893
|
+
selected: scheduleDraft.weekdays
|
|
6894
|
+
.contains(day),
|
|
6895
|
+
onSelected: (selected) {
|
|
6896
|
+
setLocalState(() {
|
|
6897
|
+
if (selected) {
|
|
6898
|
+
scheduleDraft.weekdays.add(
|
|
6899
|
+
day,
|
|
6900
|
+
);
|
|
6901
|
+
} else if (scheduleDraft
|
|
6902
|
+
.weekdays
|
|
6903
|
+
.length >
|
|
6904
|
+
1) {
|
|
6905
|
+
scheduleDraft.weekdays.remove(
|
|
6906
|
+
day,
|
|
6907
|
+
);
|
|
6908
|
+
}
|
|
6909
|
+
});
|
|
6910
|
+
},
|
|
6911
|
+
);
|
|
6912
|
+
}),
|
|
6913
|
+
),
|
|
6914
|
+
),
|
|
6915
|
+
const SizedBox(height: 12),
|
|
6916
|
+
],
|
|
6917
|
+
if (scheduleDraft.presetId ==
|
|
6918
|
+
'monthly') ...<Widget>[
|
|
6919
|
+
DropdownButtonFormField<int>(
|
|
6920
|
+
initialValue: scheduleDraft.monthDay,
|
|
6921
|
+
decoration: const InputDecoration(
|
|
6922
|
+
labelText: 'Day of month',
|
|
6923
|
+
),
|
|
6924
|
+
items:
|
|
6925
|
+
List<DropdownMenuItem<int>>.generate(
|
|
6926
|
+
31,
|
|
6927
|
+
(index) => DropdownMenuItem<int>(
|
|
6928
|
+
value: index + 1,
|
|
6929
|
+
child: Text('Day ${index + 1}'),
|
|
6930
|
+
),
|
|
6931
|
+
),
|
|
6932
|
+
onChanged: (value) {
|
|
6933
|
+
if (value == null) return;
|
|
6934
|
+
setLocalState(() {
|
|
6935
|
+
scheduleDraft.monthDay = value;
|
|
6936
|
+
});
|
|
6937
|
+
},
|
|
6938
|
+
),
|
|
6939
|
+
const SizedBox(height: 12),
|
|
6940
|
+
],
|
|
6941
|
+
InputDecorator(
|
|
6942
|
+
decoration: const InputDecoration(
|
|
6943
|
+
labelText: 'Schedule',
|
|
6944
|
+
),
|
|
6945
|
+
child: Builder(
|
|
6946
|
+
builder: (context) {
|
|
6947
|
+
final preset = _taskSchedulePresetForId(
|
|
6948
|
+
scheduleDraft.presetId,
|
|
6949
|
+
);
|
|
6950
|
+
return Row(
|
|
6951
|
+
children: <Widget>[
|
|
6952
|
+
Icon(preset.icon, color: _accent),
|
|
6953
|
+
const SizedBox(width: 12),
|
|
6954
|
+
Expanded(
|
|
6955
|
+
child: Column(
|
|
6956
|
+
crossAxisAlignment:
|
|
6957
|
+
CrossAxisAlignment.start,
|
|
6958
|
+
mainAxisSize: MainAxisSize.min,
|
|
6959
|
+
children: <Widget>[
|
|
6960
|
+
Text(
|
|
6961
|
+
scheduleDraft.summary,
|
|
6962
|
+
style: const TextStyle(
|
|
6963
|
+
fontWeight:
|
|
6964
|
+
FontWeight.w700,
|
|
6965
|
+
),
|
|
6966
|
+
),
|
|
6967
|
+
const SizedBox(height: 3),
|
|
6968
|
+
Text(
|
|
6969
|
+
preset.description,
|
|
6970
|
+
style: TextStyle(
|
|
6971
|
+
color: _textSecondary,
|
|
6972
|
+
fontSize: 12,
|
|
6973
|
+
),
|
|
6974
|
+
),
|
|
6975
|
+
],
|
|
6976
|
+
),
|
|
6977
|
+
),
|
|
6978
|
+
],
|
|
6979
|
+
);
|
|
6980
|
+
},
|
|
6981
|
+
),
|
|
6982
|
+
),
|
|
6983
|
+
],
|
|
6481
6984
|
],
|
|
6482
6985
|
);
|
|
6483
6986
|
}
|
|
@@ -6835,13 +7338,44 @@ class _TasksPanelState extends State<TasksPanel> {
|
|
|
6835
7338
|
// Manual trigger uses no trigger-specific config.
|
|
6836
7339
|
} else if (selectedTriggerType == 'schedule') {
|
|
6837
7340
|
final runAt = runAtController.text.trim();
|
|
6838
|
-
triggerConfig['mode'] =
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
7341
|
+
triggerConfig['mode'] = scheduleDraft.mode;
|
|
7342
|
+
if (scheduleDraft.mode == 'recurring') {
|
|
7343
|
+
scheduleDraft.customCronExpression =
|
|
7344
|
+
customCronController.text.trim();
|
|
7345
|
+
final cronExpression = scheduleDraft.cronExpression
|
|
6843
7346
|
.trim();
|
|
7347
|
+
if (cronExpression.isEmpty) {
|
|
7348
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
7349
|
+
const SnackBar(
|
|
7350
|
+
content: Text('Please choose a schedule.'),
|
|
7351
|
+
backgroundColor: Colors.red,
|
|
7352
|
+
),
|
|
7353
|
+
);
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
if (scheduleDraft.presetId == 'custom' &&
|
|
7357
|
+
!_looksLikeCronExpression(cronExpression)) {
|
|
7358
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
7359
|
+
const SnackBar(
|
|
7360
|
+
content: Text('Custom Cron must have 5 fields.'),
|
|
7361
|
+
backgroundColor: Colors.red,
|
|
7362
|
+
),
|
|
7363
|
+
);
|
|
7364
|
+
return;
|
|
7365
|
+
}
|
|
7366
|
+
triggerConfig['cronExpression'] = cronExpression;
|
|
6844
7367
|
} else {
|
|
7368
|
+
if (runAt.isEmpty) {
|
|
7369
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
7370
|
+
const SnackBar(
|
|
7371
|
+
content: Text(
|
|
7372
|
+
'Please enter when the task should run.',
|
|
7373
|
+
),
|
|
7374
|
+
backgroundColor: Colors.red,
|
|
7375
|
+
),
|
|
7376
|
+
);
|
|
7377
|
+
return;
|
|
7378
|
+
}
|
|
6845
7379
|
triggerConfig['runAt'] = runAt;
|
|
6846
7380
|
}
|
|
6847
7381
|
} else {
|
package/lib/schema_migrations.js
CHANGED
|
@@ -633,6 +633,34 @@ function migrateBilling(db) {
|
|
|
633
633
|
`);
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
function migrateAgentRunLifecycle(db) {
|
|
637
|
+
db.exec(`
|
|
638
|
+
CREATE TABLE IF NOT EXISTS agent_run_controls (
|
|
639
|
+
run_id TEXT PRIMARY KEY,
|
|
640
|
+
user_id INTEGER NOT NULL,
|
|
641
|
+
action TEXT NOT NULL CHECK(action IN ('pause', 'stop', 'interrupt')),
|
|
642
|
+
reason TEXT DEFAULT '',
|
|
643
|
+
requested_at TEXT DEFAULT (datetime('now')),
|
|
644
|
+
consumed_at TEXT,
|
|
645
|
+
FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE CASCADE,
|
|
646
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
CREATE TABLE IF NOT EXISTS agent_run_checkpoints (
|
|
650
|
+
run_id TEXT PRIMARY KEY,
|
|
651
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
652
|
+
phase TEXT NOT NULL,
|
|
653
|
+
state_json TEXT NOT NULL DEFAULT '{}',
|
|
654
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
655
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
656
|
+
FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
|
|
657
|
+
);
|
|
658
|
+
|
|
659
|
+
CREATE INDEX IF NOT EXISTS idx_agent_run_controls_pending
|
|
660
|
+
ON agent_run_controls(user_id, consumed_at, requested_at);
|
|
661
|
+
`);
|
|
662
|
+
}
|
|
663
|
+
|
|
636
664
|
function runSchemaMigrations(db) {
|
|
637
665
|
removeRetiredCaptureData(db);
|
|
638
666
|
migrateMemoryEmbeddingIndex(db);
|
|
@@ -647,6 +675,7 @@ function runSchemaMigrations(db) {
|
|
|
647
675
|
migrateApprovalPersistence(db);
|
|
648
676
|
migrateToolPoliciesAllowAlways(db);
|
|
649
677
|
migrateBilling(db);
|
|
678
|
+
migrateAgentRunLifecycle(db);
|
|
650
679
|
}
|
|
651
680
|
|
|
652
681
|
module.exports = {
|
|
@@ -664,5 +693,6 @@ module.exports = {
|
|
|
664
693
|
migrateApprovalPersistence,
|
|
665
694
|
migrateToolPoliciesAllowAlways,
|
|
666
695
|
migrateBilling,
|
|
696
|
+
migrateAgentRunLifecycle,
|
|
667
697
|
runSchemaMigrations,
|
|
668
698
|
};
|
package/package.json
CHANGED