@goodandready/dsh-goal 0.1.6 → 0.1.8
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/design/DESIGN.md +26 -0
- package/lib/client.js +356 -15
- package/lib/command-handler.js +199 -192
- package/lib/goal-engine.js +94 -1
- package/lib/index.js +35 -5
- package/package.json +1 -1
package/docs/design/DESIGN.md
CHANGED
|
@@ -90,3 +90,29 @@
|
|
|
90
90
|
- **Crash Hydration**: При перезапуске DSH цели, оставшиеся в статусе `RUNNING`, автоматически гидрируются в `PAUSED` с отметкой времени и пояснением оператору для ручного возобновления в 1 клик.
|
|
91
91
|
- **Web Audio Синтез**: Синтезированные пентатонические колокольчики через `window.AudioContext` при завершении цели (`COMPLETED` — мажорный аккорд, `FAILED` — минорный аккорд) с соблюдением настройки `enableSound`.
|
|
92
92
|
- **Milestone Integrity**: Статус выполнения вех строго контролируется агентом; чекбоксы ручной отметки в UI исключены в соответствии с архитектурным контрактом.
|
|
93
|
+
|
|
94
|
+
8. **2026-09-12 — Сессионная изоляция /goal, устойчивость SSE и строгая REST валидация (v0.1.7)**:
|
|
95
|
+
- **Сквозная сессионная изоляция в слеш-команде**: В `command-handler.js` метод `executeGoalSlashCommand` пробрасывает `sessionId` во все вызовы `engine` (`getSnapshot`, `startGoal`, `pause`, `resume`, `clear`), исключая мутацию глобальной сессии при работе в конкретном чате.
|
|
96
|
+
- **Устойчивость SSE с Exponential Backoff**: На клиенте внедрена автоматическая схема повторного подключения `EventSource` с возрастающей задержкой (от 2s до 30s) и рандомизированным джиттером для защиты от шторма переподключений.
|
|
97
|
+
- **Строгая REST валидация и Enum Guard**: Обработчик `POST /dsh-goal/action` валидирует непустой `title` при старте цели и проверяет допустимость статуса вехи по `MilestoneStatus` enum, возвращая внятный HTTP 400 Bad Request / 404 Not Found.
|
|
98
|
+
- **Безопасность файловой системы**: `GoalEngine.writeStateToDiskSync()` гарантированно создаёт отсутствующие родительские директории через `fs.mkdirSync(dir, { recursive: true })` перед созданием временного файла и атомарным переименованием.
|
|
99
|
+
|
|
100
|
+
### Решение 9: Интеллектуальная оценка времени (ETA), аналитика токенов, кнопка быстрого запуска цели и экспорт отчетов в Markdown
|
|
101
|
+
|
|
102
|
+
**Контекст:**
|
|
103
|
+
После стабилизации сессионной изоляции и SSE-потока пользователям требовалась наглядная оценка оставшегося времени работы над целью, понимание расхода токенов за цикл, удобный запуск цели без ручного ввода команды `/goal` и возможность мгновенно экспортировать структурированный итоговый отчёт в markdown-формате.
|
|
104
|
+
|
|
105
|
+
**Принятые решения:**
|
|
106
|
+
1. **Расчет оставшегося времени (ETA projection):**
|
|
107
|
+
- Метод `getEstimatedRemainingSeconds(sid)` рассчитывает среднее время на выполнение завершенных вех `elapsed / completedCount` и умножает на количество оставшихся шагов.
|
|
108
|
+
- В верхнем баннере и модальном окне отображается живое время работы с прогнозом: `⏱ 45s (ETA ~2m)`.
|
|
109
|
+
2. **Аналитика расхода токенов (Token Usage Analytics):**
|
|
110
|
+
- Накопление счетчиков `promptTokens`, `completionTokens`, `totalTokens` на каждом завершении хода `turn/end` через `engine.addTokenUsage(usage, sid)`.
|
|
111
|
+
- В модальном окне деталей в сетку характеристик добавлен блок «Токены» с подсказкой при наведении с детальным расщеплением.
|
|
112
|
+
3. **Кнопка быстрого запуска цели (Quick Launch button):**
|
|
113
|
+
- Когда цель не активна, в `conversation.input.dock` отображается компактная кнопка с иконкой 🎯.
|
|
114
|
+
- По клику открывается модальное окно с полем ввода для немедленной постановки задачи агенту без ручного ввода слэш-команд.
|
|
115
|
+
- В карточке настроек добавлен переключатель `showQuickLaunchButton` с возможностью сброса к дефолту.
|
|
116
|
+
4. **Экспорт отчета в Markdown (One-click Markdown Export):**
|
|
117
|
+
- Для выполненной цели модальное окно предоставляет кнопку «📋 Скопировать отчёт в Markdown».
|
|
118
|
+
- Генерирует отчет с заголовком, статусом, длительностью, итерациями, расходом токенов, резюме результатов и таблицей вех.
|
package/lib/client.js
CHANGED
|
@@ -182,6 +182,32 @@ window.__ModuleLoader__.load({
|
|
|
182
182
|
display: flex;
|
|
183
183
|
justify-content: center;
|
|
184
184
|
}
|
|
185
|
+
.dsh-goal-dock.dsh-goal-dock-quick {
|
|
186
|
+
justify-content: flex-start;
|
|
187
|
+
margin-bottom: 4px;
|
|
188
|
+
}
|
|
189
|
+
.dsh-goal-quicklaunch-btn {
|
|
190
|
+
appearance: none;
|
|
191
|
+
font: inherit;
|
|
192
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
193
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
194
|
+
border-radius: 999px;
|
|
195
|
+
padding: 4px 11px;
|
|
196
|
+
color: var(--dsw-alias-label-secondary);
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: 6px;
|
|
203
|
+
transition: all 0.15s ease;
|
|
204
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
205
|
+
}
|
|
206
|
+
.dsh-goal-quicklaunch-btn:hover {
|
|
207
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
208
|
+
border-color: var(--dsw-alias-label-tertiary);
|
|
209
|
+
color: var(--dsw-alias-label-primary);
|
|
210
|
+
}
|
|
185
211
|
.dsh-goal-banner {
|
|
186
212
|
box-sizing: border-box;
|
|
187
213
|
width: 100%;
|
|
@@ -382,7 +408,7 @@ window.__ModuleLoader__.load({
|
|
|
382
408
|
}
|
|
383
409
|
.dsh-goal-stat-grid {
|
|
384
410
|
display: grid;
|
|
385
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
411
|
+
grid-template-columns: repeat(auto-fit, minmax(95px, 1fr));
|
|
386
412
|
gap: 10px;
|
|
387
413
|
}
|
|
388
414
|
.dsh-goal-stat-box {
|
|
@@ -395,13 +421,17 @@ window.__ModuleLoader__.load({
|
|
|
395
421
|
gap: 2px;
|
|
396
422
|
}
|
|
397
423
|
.dsh-goal-stat-val {
|
|
398
|
-
font-size:
|
|
424
|
+
font-size: 15px;
|
|
399
425
|
font-weight: 700;
|
|
400
426
|
color: var(--dsw-alias-label-primary);
|
|
427
|
+
overflow: hidden;
|
|
428
|
+
text-overflow: ellipsis;
|
|
429
|
+
white-space: nowrap;
|
|
401
430
|
}
|
|
402
431
|
.dsh-goal-stat-lbl {
|
|
403
432
|
font-size: 11px;
|
|
404
433
|
color: var(--dsw-alias-label-secondary);
|
|
434
|
+
white-space: nowrap;
|
|
405
435
|
}
|
|
406
436
|
.dsh-goal-milestone-item {
|
|
407
437
|
display: flex;
|
|
@@ -421,6 +451,17 @@ window.__ModuleLoader__.load({
|
|
|
421
451
|
justify-content: flex-end;
|
|
422
452
|
align-items: center;
|
|
423
453
|
gap: 10px;
|
|
454
|
+
flex-wrap: wrap;
|
|
455
|
+
}
|
|
456
|
+
.dsh-goal-btn-copy {
|
|
457
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
458
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
459
|
+
color: var(--dsw-alias-label-primary);
|
|
460
|
+
}
|
|
461
|
+
.dsh-goal-btn-copy.copied {
|
|
462
|
+
background: rgba(16, 185, 129, 0.12);
|
|
463
|
+
border-color: var(--dsw-alias-state-success-primary);
|
|
464
|
+
color: var(--dsw-alias-state-success-primary);
|
|
424
465
|
}
|
|
425
466
|
|
|
426
467
|
/* Settings Card (clinebot pattern) */
|
|
@@ -658,12 +699,77 @@ window.__ModuleLoader__.load({
|
|
|
658
699
|
} catch (_) {}
|
|
659
700
|
}
|
|
660
701
|
|
|
702
|
+
// --- ГЕНЕРАТОР MARKDOWN-ОТЧЕТА ---
|
|
703
|
+
function generateMarkdownReport(state) {
|
|
704
|
+
if (!state) return '';
|
|
705
|
+
const title = state.title || 'Goal Report';
|
|
706
|
+
const status = state.state === 'COMPLETED' ? 'COMPLETED' : state.state;
|
|
707
|
+
const elapsed = state.formattedElapsed || '0s';
|
|
708
|
+
const iter = `${state.iterationsCount || 0}/${state.maxIterations || 25}`;
|
|
709
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
710
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
711
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
712
|
+
|
|
713
|
+
let md = `# 🎯 Goal Report: ${title}\n\n`;
|
|
714
|
+
md += `**Status:** \`${status}\` | **Duration:** \`${elapsed}\` | **Iterations:** \`${iter}\`\n`;
|
|
715
|
+
if (totalTokens > 0) {
|
|
716
|
+
md += `**Tokens:** \`${totalTokens.toLocaleString()}\` (Prompt: \`${promptTokens.toLocaleString()}\`, Completion: \`${compTokens.toLocaleString()}\`)\n`;
|
|
717
|
+
}
|
|
718
|
+
md += '\n';
|
|
719
|
+
|
|
720
|
+
if (state.description) {
|
|
721
|
+
md += `### Description\n${state.description}\n\n`;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (state.resultSummary) {
|
|
725
|
+
md += `### Summary & Deliverables\n${state.resultSummary}\n\n`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const milestones = state.milestones || [];
|
|
729
|
+
if (milestones.length > 0) {
|
|
730
|
+
md += `### Milestones\n| # | Status | Title | Notes |\n|---|---|---|---|\n`;
|
|
731
|
+
milestones.forEach((m, idx) => {
|
|
732
|
+
const mark = m.status === 'completed' ? '✅ Done' : m.status === 'in_progress' ? '🔄 In Progress' : '⏳ Pending';
|
|
733
|
+
const cleanTitle = (m.title || '').replace(/\|/g, '\\|');
|
|
734
|
+
const cleanNotes = (m.notes || '').replace(/\|/g, '\\|');
|
|
735
|
+
md += `| ${idx + 1} | ${mark} | ${cleanTitle} | ${cleanNotes || '—'} |\n`;
|
|
736
|
+
});
|
|
737
|
+
md += '\n';
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
md += `*Generated by DSH Goal Engine at ${new Date().toISOString()}*\n`;
|
|
741
|
+
return md;
|
|
742
|
+
}
|
|
743
|
+
|
|
661
744
|
// --- МОДАЛЬНОЕ ОКНО ДЕТАЛЕЙ (MODAL DETAILS) ---
|
|
662
745
|
function GoalDetailsModal({ state, onClose, onAction, t }) {
|
|
663
746
|
if (!state) return null;
|
|
664
747
|
|
|
665
748
|
const milestones = state.milestones || [];
|
|
666
749
|
const isCompleted = state.state === 'COMPLETED';
|
|
750
|
+
const [copied, setCopied] = useState(false);
|
|
751
|
+
|
|
752
|
+
const totalTokens = state.tokensUsage?.totalTokens || 0;
|
|
753
|
+
const promptTokens = state.tokensUsage?.promptTokens || 0;
|
|
754
|
+
const compTokens = state.tokensUsage?.completionTokens || 0;
|
|
755
|
+
const tokensFormatted = totalTokens > 0
|
|
756
|
+
? (totalTokens >= 1000 ? `${(totalTokens / 1000).toFixed(1)}k` : `${totalTokens}`)
|
|
757
|
+
: '—';
|
|
758
|
+
const tokensTooltip = totalTokens > 0
|
|
759
|
+
? `Prompt: ${promptTokens.toLocaleString()} | Completion: ${compTokens.toLocaleString()} | Total: ${totalTokens.toLocaleString()}`
|
|
760
|
+
: 'Токены ещё не зафиксированы';
|
|
761
|
+
|
|
762
|
+
const handleCopyReport = () => {
|
|
763
|
+
const md = generateMarkdownReport(state);
|
|
764
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
765
|
+
navigator.clipboard.writeText(md).then(() => {
|
|
766
|
+
setCopied(true);
|
|
767
|
+
setTimeout(() => setCopied(false), 2500);
|
|
768
|
+
}).catch((err) => {
|
|
769
|
+
console.warn('[dsh-goal] Clipboard copy failed:', err);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
};
|
|
667
773
|
|
|
668
774
|
return React.createElement(
|
|
669
775
|
'div',
|
|
@@ -713,6 +819,18 @@ window.__ModuleLoader__.load({
|
|
|
713
819
|
React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedElapsed || '0s'),
|
|
714
820
|
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('time') || 'Время работы'),
|
|
715
821
|
),
|
|
822
|
+
React.createElement(
|
|
823
|
+
'div',
|
|
824
|
+
{ className: 'dsh-goal-stat-box' },
|
|
825
|
+
React.createElement('span', { className: 'dsh-goal-stat-val' }, state.formattedETA || (isCompleted ? 'Готово' : '—')),
|
|
826
|
+
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('eta') || 'Оценка (ETA)'),
|
|
827
|
+
),
|
|
828
|
+
React.createElement(
|
|
829
|
+
'div',
|
|
830
|
+
{ className: 'dsh-goal-stat-box', title: tokensTooltip },
|
|
831
|
+
React.createElement('span', { className: 'dsh-goal-stat-val' }, tokensFormatted),
|
|
832
|
+
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('tokens') || 'Токены'),
|
|
833
|
+
),
|
|
716
834
|
React.createElement(
|
|
717
835
|
'div',
|
|
718
836
|
{ className: 'dsh-goal-stat-box' },
|
|
@@ -760,6 +878,17 @@ window.__ModuleLoader__.load({
|
|
|
760
878
|
React.createElement(
|
|
761
879
|
'div',
|
|
762
880
|
{ className: 'dsh-goal-modal-foot' },
|
|
881
|
+
isCompleted
|
|
882
|
+
? React.createElement(
|
|
883
|
+
'button',
|
|
884
|
+
{
|
|
885
|
+
className: `dsh-goal-btn dsh-goal-btn-copy${copied ? ' copied' : ''}`,
|
|
886
|
+
onClick: handleCopyReport,
|
|
887
|
+
title: t('copyReport') || 'Скопировать отчёт в Markdown',
|
|
888
|
+
},
|
|
889
|
+
copied ? (t('reportCopied') || '✅ Скопировано!') : (t('copyReport') || '📋 Скопировать отчёт в Markdown'),
|
|
890
|
+
)
|
|
891
|
+
: null,
|
|
763
892
|
isCompleted
|
|
764
893
|
? React.createElement(
|
|
765
894
|
'button',
|
|
@@ -786,6 +915,98 @@ window.__ModuleLoader__.load({
|
|
|
786
915
|
);
|
|
787
916
|
}
|
|
788
917
|
|
|
918
|
+
// --- МОДАЛЬНОЕ ОКНО БЫСТРОГО ЗАПУСКА ЦЕЛИ (QUICK LAUNCH MODAL) ---
|
|
919
|
+
function QuickLaunchModal({ onClose, onStart, t }) {
|
|
920
|
+
const [title, setTitle] = useState('');
|
|
921
|
+
const inputRef = useRef(null);
|
|
922
|
+
|
|
923
|
+
useEffect(() => {
|
|
924
|
+
if (inputRef.current) inputRef.current.focus();
|
|
925
|
+
}, []);
|
|
926
|
+
|
|
927
|
+
const handleSubmit = (e) => {
|
|
928
|
+
if (e) e.preventDefault();
|
|
929
|
+
const trimmed = title.trim();
|
|
930
|
+
if (trimmed) {
|
|
931
|
+
onStart(trimmed);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
return React.createElement(
|
|
936
|
+
'div',
|
|
937
|
+
{ className: 'dsh-goal-modal-overlay', onClick: onClose },
|
|
938
|
+
React.createElement(
|
|
939
|
+
'div',
|
|
940
|
+
{
|
|
941
|
+
className: 'dsh-goal-modal',
|
|
942
|
+
style: { maxWidth: 480 },
|
|
943
|
+
onClick: (e) => e.stopPropagation(),
|
|
944
|
+
},
|
|
945
|
+
React.createElement(
|
|
946
|
+
'div',
|
|
947
|
+
{ className: 'dsh-goal-modal-head' },
|
|
948
|
+
React.createElement(
|
|
949
|
+
'div',
|
|
950
|
+
{ className: 'dsh-goal-modal-title' },
|
|
951
|
+
React.createElement(IconTarget, { size: 18 }),
|
|
952
|
+
t('quickLaunchTitle') || 'Быстрый запуск цели',
|
|
953
|
+
),
|
|
954
|
+
React.createElement(
|
|
955
|
+
'button',
|
|
956
|
+
{ className: 'dsh-goal-btn icon-only close', onClick: onClose, title: t('close') || 'Закрыть' },
|
|
957
|
+
'✕',
|
|
958
|
+
),
|
|
959
|
+
),
|
|
960
|
+
React.createElement(
|
|
961
|
+
'form',
|
|
962
|
+
{ onSubmit: handleSubmit },
|
|
963
|
+
React.createElement(
|
|
964
|
+
'div',
|
|
965
|
+
{ className: 'dsh-goal-modal-body' },
|
|
966
|
+
React.createElement(
|
|
967
|
+
'div',
|
|
968
|
+
{ style: { fontSize: 13, color: 'var(--dsw-alias-label-secondary)', marginBottom: 8 } },
|
|
969
|
+
t('quickLaunchDesc') || 'Сформулируйте задачу для агента в автономном режиме:',
|
|
970
|
+
),
|
|
971
|
+
React.createElement('input', {
|
|
972
|
+
ref: inputRef,
|
|
973
|
+
type: 'text',
|
|
974
|
+
className: 'dsh-goal-card-input',
|
|
975
|
+
style: { width: '100%', boxSizing: 'border-box', fontSize: 13, padding: '8px 10px' },
|
|
976
|
+
placeholder: t('quickLaunchPlaceholder') || 'Например: Реализовать экспорт отчетов и покрыть тестами...',
|
|
977
|
+
value: title,
|
|
978
|
+
onChange: (e) => setTitle(e.target.value),
|
|
979
|
+
onKeyDown: (e) => {
|
|
980
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
981
|
+
e.preventDefault();
|
|
982
|
+
handleSubmit();
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
}),
|
|
986
|
+
),
|
|
987
|
+
React.createElement(
|
|
988
|
+
'div',
|
|
989
|
+
{ className: 'dsh-goal-modal-foot' },
|
|
990
|
+
React.createElement(
|
|
991
|
+
'button',
|
|
992
|
+
{ type: 'button', className: 'dsh-goal-btn', onClick: onClose },
|
|
993
|
+
t('close') || 'Отмена',
|
|
994
|
+
),
|
|
995
|
+
React.createElement(
|
|
996
|
+
'button',
|
|
997
|
+
{
|
|
998
|
+
type: 'submit',
|
|
999
|
+
className: 'dsh-goal-btn dsh-goal-btn-primary',
|
|
1000
|
+
disabled: !title.trim(),
|
|
1001
|
+
},
|
|
1002
|
+
t('startGoal') || 'Запустить цель',
|
|
1003
|
+
),
|
|
1004
|
+
),
|
|
1005
|
+
),
|
|
1006
|
+
),
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
789
1010
|
function sessionIdOf(ctx, props) {
|
|
790
1011
|
try {
|
|
791
1012
|
if (props?.session?.id) return String(props.session.id);
|
|
@@ -809,6 +1030,7 @@ window.__ModuleLoader__.load({
|
|
|
809
1030
|
const sid = sessionIdOf(ctx, props);
|
|
810
1031
|
const [state, setState] = useState(null);
|
|
811
1032
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
1033
|
+
const [isQuickLaunchOpen, setIsQuickLaunchOpen] = useState(false);
|
|
812
1034
|
const [now, setNow] = useState(() => Date.now());
|
|
813
1035
|
const stateRef = useRef(null);
|
|
814
1036
|
stateRef.current = state;
|
|
@@ -869,8 +1091,10 @@ window.__ModuleLoader__.load({
|
|
|
869
1091
|
useEffect(() => {
|
|
870
1092
|
let es = null;
|
|
871
1093
|
let timer = null;
|
|
1094
|
+
let reconnectTimer = null;
|
|
872
1095
|
let isDisposed = false;
|
|
873
1096
|
let sseActive = false;
|
|
1097
|
+
let retryCount = 0;
|
|
874
1098
|
|
|
875
1099
|
const scheduleNextPoll = () => {
|
|
876
1100
|
if (isDisposed || sseActive) return;
|
|
@@ -891,18 +1115,33 @@ window.__ModuleLoader__.load({
|
|
|
891
1115
|
}, delay);
|
|
892
1116
|
};
|
|
893
1117
|
|
|
894
|
-
|
|
895
|
-
|
|
1118
|
+
const connectSSE = () => {
|
|
1119
|
+
if (isDisposed) return;
|
|
1120
|
+
if (typeof window === 'undefined' || typeof window.EventSource === 'undefined') {
|
|
1121
|
+
scheduleNextPoll();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
896
1125
|
try {
|
|
1126
|
+
if (es) {
|
|
1127
|
+
es.close();
|
|
1128
|
+
es = null;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
897
1131
|
const query = sid && sid !== 'default' ? `?sessionId=${encodeURIComponent(sid)}` : '';
|
|
898
1132
|
es = new window.EventSource(`/dsh-goal/events${query}`);
|
|
899
1133
|
|
|
900
1134
|
es.onopen = () => {
|
|
901
1135
|
sseActive = true;
|
|
1136
|
+
retryCount = 0;
|
|
902
1137
|
if (timer) {
|
|
903
1138
|
clearTimeout(timer);
|
|
904
1139
|
timer = null;
|
|
905
1140
|
}
|
|
1141
|
+
if (reconnectTimer) {
|
|
1142
|
+
clearTimeout(reconnectTimer);
|
|
1143
|
+
reconnectTimer = null;
|
|
1144
|
+
}
|
|
906
1145
|
};
|
|
907
1146
|
|
|
908
1147
|
es.onmessage = (event) => {
|
|
@@ -916,22 +1155,38 @@ window.__ModuleLoader__.load({
|
|
|
916
1155
|
};
|
|
917
1156
|
|
|
918
1157
|
es.onerror = () => {
|
|
919
|
-
// При сбое соединения SSE переключаемся на адаптивный fallback поллинг
|
|
920
1158
|
sseActive = false;
|
|
921
|
-
if (
|
|
922
|
-
|
|
1159
|
+
if (es) {
|
|
1160
|
+
es.close();
|
|
1161
|
+
es = null;
|
|
1162
|
+
}
|
|
1163
|
+
if (!isDisposed) {
|
|
1164
|
+
// Запускаем немедленный опрос для непрерывности работы UI
|
|
1165
|
+
if (!timer) scheduleNextPoll();
|
|
1166
|
+
|
|
1167
|
+
// Экспоненциальный backoff для повторного подключения SSE (от 2s до 30s) с джиттером
|
|
1168
|
+
retryCount = Math.min(retryCount + 1, 5);
|
|
1169
|
+
const backoffBase = Math.min(30000, 2000 * Math.pow(1.8, retryCount - 1));
|
|
1170
|
+
const jitter = Math.random() * 1000;
|
|
1171
|
+
const reconnectDelay = Math.round(backoffBase + jitter);
|
|
1172
|
+
|
|
1173
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
1174
|
+
reconnectTimer = setTimeout(() => {
|
|
1175
|
+
reconnectTimer = null;
|
|
1176
|
+
connectSSE();
|
|
1177
|
+
}, reconnectDelay);
|
|
923
1178
|
}
|
|
924
1179
|
};
|
|
925
1180
|
} catch (_) {
|
|
926
1181
|
sseActive = false;
|
|
927
1182
|
scheduleNextPoll();
|
|
928
1183
|
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
connectSSE();
|
|
932
1187
|
|
|
933
1188
|
// Первоначальная загрузка состояния
|
|
934
|
-
fetchState().then((
|
|
1189
|
+
fetchState().then(() => {
|
|
935
1190
|
if (!sseActive) scheduleNextPoll();
|
|
936
1191
|
});
|
|
937
1192
|
|
|
@@ -942,6 +1197,10 @@ window.__ModuleLoader__.load({
|
|
|
942
1197
|
fetchState().then(() => {
|
|
943
1198
|
scheduleNextPoll();
|
|
944
1199
|
});
|
|
1200
|
+
// При возвращении на вкладку можно попытаться сразу переподключить SSE
|
|
1201
|
+
if (!reconnectTimer && !sseActive) {
|
|
1202
|
+
connectSSE();
|
|
1203
|
+
}
|
|
945
1204
|
}
|
|
946
1205
|
}
|
|
947
1206
|
};
|
|
@@ -957,6 +1216,7 @@ window.__ModuleLoader__.load({
|
|
|
957
1216
|
es = null;
|
|
958
1217
|
}
|
|
959
1218
|
if (timer) clearTimeout(timer);
|
|
1219
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
960
1220
|
if (typeof document !== 'undefined' && document.removeEventListener) {
|
|
961
1221
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
962
1222
|
}
|
|
@@ -980,8 +1240,38 @@ window.__ModuleLoader__.load({
|
|
|
980
1240
|
} catch (_) {}
|
|
981
1241
|
};
|
|
982
1242
|
|
|
1243
|
+
const showQuickLaunch = state ? (state.showQuickLaunchButton !== false) : true;
|
|
1244
|
+
|
|
983
1245
|
if (!state || !state.hasActiveGoal) {
|
|
984
|
-
return null;
|
|
1246
|
+
if (!showQuickLaunch) return null;
|
|
1247
|
+
return React.createElement(
|
|
1248
|
+
React.Fragment,
|
|
1249
|
+
null,
|
|
1250
|
+
React.createElement(
|
|
1251
|
+
'div',
|
|
1252
|
+
{ className: 'dsh-goal-dock dsh-goal-dock-quick', 'data-goal-dock': 'true' },
|
|
1253
|
+
React.createElement(
|
|
1254
|
+
'button',
|
|
1255
|
+
{
|
|
1256
|
+
className: 'dsh-goal-quicklaunch-btn',
|
|
1257
|
+
title: t('quickLaunch') || 'Запустить цель (Goal Mode)',
|
|
1258
|
+
onClick: () => setIsQuickLaunchOpen(true),
|
|
1259
|
+
},
|
|
1260
|
+
React.createElement(IconTarget, { size: 14 }),
|
|
1261
|
+
React.createElement('span', null, t('quickLaunch') || 'Запустить цель'),
|
|
1262
|
+
),
|
|
1263
|
+
),
|
|
1264
|
+
isQuickLaunchOpen
|
|
1265
|
+
? React.createElement(QuickLaunchModal, {
|
|
1266
|
+
onClose: () => setIsQuickLaunchOpen(false),
|
|
1267
|
+
onStart: async (title) => {
|
|
1268
|
+
await handleAction('start', { title });
|
|
1269
|
+
setIsQuickLaunchOpen(false);
|
|
1270
|
+
},
|
|
1271
|
+
t,
|
|
1272
|
+
})
|
|
1273
|
+
: null,
|
|
1274
|
+
);
|
|
985
1275
|
}
|
|
986
1276
|
|
|
987
1277
|
const isPaused = state.state === 'PAUSED';
|
|
@@ -996,6 +1286,7 @@ window.__ModuleLoader__.load({
|
|
|
996
1286
|
};
|
|
997
1287
|
|
|
998
1288
|
const liveElapsedStr = formatLiveElapsed();
|
|
1289
|
+
const etaStr = state?.formattedETA ? ` (ETA ${state.formattedETA})` : '';
|
|
999
1290
|
|
|
1000
1291
|
return React.createElement(
|
|
1001
1292
|
React.Fragment,
|
|
@@ -1029,7 +1320,7 @@ window.__ModuleLoader__.load({
|
|
|
1029
1320
|
React.createElement(
|
|
1030
1321
|
'span',
|
|
1031
1322
|
{ className: 'dsh-goal-time' },
|
|
1032
|
-
`⏱ ${liveElapsedStr}`,
|
|
1323
|
+
`⏱ ${liveElapsedStr}${etaStr}`,
|
|
1033
1324
|
),
|
|
1034
1325
|
!isCompleted
|
|
1035
1326
|
? React.createElement(
|
|
@@ -1081,6 +1372,7 @@ window.__ModuleLoader__.load({
|
|
|
1081
1372
|
maxIterations: 25,
|
|
1082
1373
|
autoDrive: true,
|
|
1083
1374
|
enableSound: true,
|
|
1375
|
+
showQuickLaunchButton: true,
|
|
1084
1376
|
};
|
|
1085
1377
|
|
|
1086
1378
|
function parseNumberField(raw) {
|
|
@@ -1139,7 +1431,7 @@ window.__ModuleLoader__.load({
|
|
|
1139
1431
|
function computeSavePlan(draft, snap) {
|
|
1140
1432
|
if (!draft) return [];
|
|
1141
1433
|
|
|
1142
|
-
const fields = ['maxIterations', 'autoDrive', 'enableSound'];
|
|
1434
|
+
const fields = ['maxIterations', 'autoDrive', 'enableSound', 'showQuickLaunchButton'];
|
|
1143
1435
|
const writes = [];
|
|
1144
1436
|
|
|
1145
1437
|
for (const f of fields) {
|
|
@@ -1196,8 +1488,9 @@ window.__ModuleLoader__.load({
|
|
|
1196
1488
|
const maxIterStatus = getFieldStatus('maxIterations', draft?.maxIterations, snap);
|
|
1197
1489
|
const autoDriveStatus = getFieldStatus('autoDrive', draft?.autoDrive, snap);
|
|
1198
1490
|
const enableSoundStatus = getFieldStatus('enableSound', draft?.enableSound, snap);
|
|
1491
|
+
const quickLaunchStatus = getFieldStatus('showQuickLaunchButton', draft?.showQuickLaunchButton, snap);
|
|
1199
1492
|
|
|
1200
|
-
const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty;
|
|
1493
|
+
const dirty = maxIterStatus.isDirty || autoDriveStatus.isDirty || enableSoundStatus.isDirty || quickLaunchStatus.isDirty;
|
|
1201
1494
|
const invalid = maxIterStatus.invalid;
|
|
1202
1495
|
const disabled = !scope || saving || snap?.writable === false;
|
|
1203
1496
|
|
|
@@ -1207,6 +1500,7 @@ window.__ModuleLoader__.load({
|
|
|
1207
1500
|
maxIterations: prev?.maxIterations !== undefined ? prev.maxIterations : maxIterStatus.value,
|
|
1208
1501
|
autoDrive: prev?.autoDrive !== undefined ? prev.autoDrive : autoDriveStatus.value,
|
|
1209
1502
|
enableSound: prev?.enableSound !== undefined ? prev.enableSound : enableSoundStatus.value,
|
|
1503
|
+
showQuickLaunchButton: prev?.showQuickLaunchButton !== undefined ? prev.showQuickLaunchButton : quickLaunchStatus.value,
|
|
1210
1504
|
[field]: val,
|
|
1211
1505
|
}));
|
|
1212
1506
|
};
|
|
@@ -1380,6 +1674,33 @@ window.__ModuleLoader__.load({
|
|
|
1380
1674
|
)
|
|
1381
1675
|
: null,
|
|
1382
1676
|
),
|
|
1677
|
+
React.createElement(
|
|
1678
|
+
'div',
|
|
1679
|
+
{ className: 'dsh-goal-check-row' },
|
|
1680
|
+
React.createElement(
|
|
1681
|
+
'label',
|
|
1682
|
+
{ className: 'dsh-goal-check' },
|
|
1683
|
+
React.createElement('input', {
|
|
1684
|
+
type: 'checkbox',
|
|
1685
|
+
checked: quickLaunchStatus.value !== false,
|
|
1686
|
+
disabled,
|
|
1687
|
+
onChange: (e) => edit('showQuickLaunchButton', e.target.checked),
|
|
1688
|
+
}),
|
|
1689
|
+
t('quickLaunchLabel') || 'Кнопка быстрого запуска цели над полем ввода',
|
|
1690
|
+
),
|
|
1691
|
+
quickLaunchStatus.isOverridden
|
|
1692
|
+
? React.createElement(
|
|
1693
|
+
'button',
|
|
1694
|
+
{
|
|
1695
|
+
type: 'button',
|
|
1696
|
+
className: 'dsh-goal-btn-inline-reset',
|
|
1697
|
+
title: t('resetField') || 'Сбросить к значению по умолчанию',
|
|
1698
|
+
onClick: () => resetFieldToDefault('showQuickLaunchButton'),
|
|
1699
|
+
},
|
|
1700
|
+
React.createElement(IconRotateCcw, { size: 12 }),
|
|
1701
|
+
)
|
|
1702
|
+
: null,
|
|
1703
|
+
),
|
|
1383
1704
|
React.createElement(
|
|
1384
1705
|
'div',
|
|
1385
1706
|
{ className: 'dsh-goal-foot' },
|
|
@@ -1423,6 +1744,8 @@ window.__ModuleLoader__.load({
|
|
|
1423
1744
|
modalTitle: 'План и статус цели',
|
|
1424
1745
|
modalTitleCompleted: 'Цель выполнена: план и результаты',
|
|
1425
1746
|
time: 'Время работы',
|
|
1747
|
+
eta: 'Оценка (ETA)',
|
|
1748
|
+
tokens: 'Токены',
|
|
1426
1749
|
milestones: 'План работ:',
|
|
1427
1750
|
noMilestones: 'Агент формирует план работ...',
|
|
1428
1751
|
close: 'Закрыть',
|
|
@@ -1431,6 +1754,14 @@ window.__ModuleLoader__.load({
|
|
|
1431
1754
|
maxIterLabel: 'Максимум итераций (Safety Limit):',
|
|
1432
1755
|
autoDriveLabel: 'Авто-драйв: продолжать цикл автоматически',
|
|
1433
1756
|
soundLabel: 'Звук по завершении цели',
|
|
1757
|
+
quickLaunchLabel: 'Кнопка быстрого запуска цели над полем ввода',
|
|
1758
|
+
quickLaunch: 'Запустить цель',
|
|
1759
|
+
quickLaunchTitle: 'Быстрый запуск цели',
|
|
1760
|
+
quickLaunchDesc: 'Сформулируйте задачу для агента в автономном режиме:',
|
|
1761
|
+
quickLaunchPlaceholder: 'Например: Реализовать экспорт отчетов и покрыть тестами...',
|
|
1762
|
+
startGoal: 'Запустить цель',
|
|
1763
|
+
copyReport: '📋 Скопировать отчёт в Markdown',
|
|
1764
|
+
reportCopied: '✅ Скопировано!',
|
|
1434
1765
|
save: 'Сохранить',
|
|
1435
1766
|
saving: 'Сохранение…',
|
|
1436
1767
|
discard: 'Отменить правки',
|
|
@@ -1451,6 +1782,8 @@ window.__ModuleLoader__.load({
|
|
|
1451
1782
|
modalTitle: 'Goal Plan & Status',
|
|
1452
1783
|
modalTitleCompleted: 'Goal Completed: Plan & Results',
|
|
1453
1784
|
time: 'Running time',
|
|
1785
|
+
eta: 'ETA',
|
|
1786
|
+
tokens: 'Tokens',
|
|
1454
1787
|
milestones: 'Plan of work:',
|
|
1455
1788
|
noMilestones: 'Agent is preparing the plan of work...',
|
|
1456
1789
|
close: 'Close',
|
|
@@ -1459,6 +1792,14 @@ window.__ModuleLoader__.load({
|
|
|
1459
1792
|
maxIterLabel: 'Max iterations (Safety Limit):',
|
|
1460
1793
|
autoDriveLabel: 'Auto-drive: keep the loop running automatically',
|
|
1461
1794
|
soundLabel: 'Sound when a goal completes',
|
|
1795
|
+
quickLaunchLabel: 'Quick launch goal button above composer dock',
|
|
1796
|
+
quickLaunch: 'Start Goal',
|
|
1797
|
+
quickLaunchTitle: 'Quick Launch Goal',
|
|
1798
|
+
quickLaunchDesc: 'Define the objective for the agent in autonomous mode:',
|
|
1799
|
+
quickLaunchPlaceholder: 'e.g. Implement report export and cover with unit tests...',
|
|
1800
|
+
startGoal: 'Start Goal',
|
|
1801
|
+
copyReport: '📋 Copy Report in Markdown',
|
|
1802
|
+
reportCopied: '✅ Copied!',
|
|
1462
1803
|
save: 'Save',
|
|
1463
1804
|
saving: 'Saving…',
|
|
1464
1805
|
discard: 'Discard changes',
|
package/lib/command-handler.js
CHANGED
|
@@ -1,192 +1,199 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { GoalState } from './goal-engine.js';
|
|
3
|
-
|
|
4
|
-
export const USAGE = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Парсинг строки ввода для команды /goal
|
|
8
|
-
* @param {string} rawInput
|
|
9
|
-
* @returns {{ action: string, text?: string }}
|
|
10
|
-
*/
|
|
11
|
-
export function parseGoalInput(rawInput = '') {
|
|
12
|
-
const input = String(rawInput || '').trim();
|
|
13
|
-
if (!input) {
|
|
14
|
-
return { action: 'show' };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const lower = input.toLowerCase();
|
|
18
|
-
if (lower === 'clear') return { action: 'clear' };
|
|
19
|
-
if (lower === 'pause') return { action: 'pause' };
|
|
20
|
-
if (lower === 'resume') return { action: 'resume' };
|
|
21
|
-
|
|
22
|
-
return { action: 'start', text: input };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Создание валидного identified сообщения пользователя для отправки агенту через followup
|
|
27
|
-
* @param {string} text
|
|
28
|
-
* @returns {Object}
|
|
29
|
-
*/
|
|
30
|
-
export function createGoalUserMessage(text) {
|
|
31
|
-
return {
|
|
32
|
-
id: randomUUID(),
|
|
33
|
-
role: 'user',
|
|
34
|
-
content: [{ type: 'text', text }],
|
|
35
|
-
source: { kind: 'user' },
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Формирование стартового промпта с жестким требованием составить план работ
|
|
41
|
-
* @param {string} goalText
|
|
42
|
-
* @returns {string}
|
|
43
|
-
*/
|
|
44
|
-
export function formatGoalStartPrompt(goalText) {
|
|
45
|
-
return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
|
|
46
|
-
`СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
|
|
47
|
-
`1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
48
|
-
`2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
|
|
49
|
-
`3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
|
|
50
|
-
`4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Форматирование текстового ответа для UI и постановка/управление задачей агента
|
|
55
|
-
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
56
|
-
* @param {{ action: string, text?: string }} parsed
|
|
57
|
-
* @param {Object} [config]
|
|
58
|
-
* @param {Object} [agent]
|
|
59
|
-
* @
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { GoalState } from './goal-engine.js';
|
|
3
|
+
|
|
4
|
+
export const USAGE = 'Использование: /goal [<цель>|clear|pause|resume]';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Парсинг строки ввода для команды /goal
|
|
8
|
+
* @param {string} rawInput
|
|
9
|
+
* @returns {{ action: string, text?: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function parseGoalInput(rawInput = '') {
|
|
12
|
+
const input = String(rawInput || '').trim();
|
|
13
|
+
if (!input) {
|
|
14
|
+
return { action: 'show' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const lower = input.toLowerCase();
|
|
18
|
+
if (lower === 'clear') return { action: 'clear' };
|
|
19
|
+
if (lower === 'pause') return { action: 'pause' };
|
|
20
|
+
if (lower === 'resume') return { action: 'resume' };
|
|
21
|
+
|
|
22
|
+
return { action: 'start', text: input };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Создание валидного identified сообщения пользователя для отправки агенту через followup
|
|
27
|
+
* @param {string} text
|
|
28
|
+
* @returns {Object}
|
|
29
|
+
*/
|
|
30
|
+
export function createGoalUserMessage(text) {
|
|
31
|
+
return {
|
|
32
|
+
id: randomUUID(),
|
|
33
|
+
role: 'user',
|
|
34
|
+
content: [{ type: 'text', text }],
|
|
35
|
+
source: { kind: 'user' },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Формирование стартового промпта с жестким требованием составить план работ
|
|
41
|
+
* @param {string} goalText
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function formatGoalStartPrompt(goalText) {
|
|
45
|
+
return `🎯 Активирован режим цели (Goal Mode): "${goalText}"\n\n` +
|
|
46
|
+
`СТРОГИЙ КОНТРАКТ АВТОНОМНОГО РЕЖИМА:\n` +
|
|
47
|
+
`1. ОБЯЗАТЕЛЬНЫЙ ШАГ №1: Твоим ПЕРВЫМ действием ДО выполнения любой другой работы или ответа должен быть вызов инструмента goal_set_milestones с массивом из 3-7 конкретных, последовательных пунктов плана работ. Пользователь видит этот план в интерфейсе в реальном времени.\n` +
|
|
48
|
+
`2. ШАГ №2: После установки плана переходи к выполнению шагов. Перед началом работы над каждым шагом отметь его статус как in_progress через goal_update_progress(milestone_id, "in_progress").\n` +
|
|
49
|
+
`3. ШАГ №3: После успешного завершения шага отметь его статус как completed через goal_update_progress(milestone_id, "completed", "краткие итоги шага").\n` +
|
|
50
|
+
`4. ШАГ №4: Когда все пункты плана выполнены, вызови инструмент goal_finish(summary) с подробным резюме достигнутых результатов.`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Форматирование текстового ответа для UI и постановка/управление задачей агента
|
|
55
|
+
* @param {import('./goal-engine.js').GoalEngine} engine
|
|
56
|
+
* @param {{ action: string, text?: string }} parsed
|
|
57
|
+
* @param {Object} [config]
|
|
58
|
+
* @param {Object} [agent]
|
|
59
|
+
* @param {string} [sessionId='default']
|
|
60
|
+
* @returns {{ kind: 'success' | 'error', text: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function executeGoalSlashCommand(engine, parsed, config = {}, agent = null, sessionId = 'default') {
|
|
63
|
+
const sid = sessionId || 'default';
|
|
64
|
+
const snap = engine.getSnapshot(sid);
|
|
65
|
+
|
|
66
|
+
switch (parsed.action) {
|
|
67
|
+
case 'show': {
|
|
68
|
+
if (!snap.hasActiveGoal) {
|
|
69
|
+
return {
|
|
70
|
+
kind: 'success',
|
|
71
|
+
text: `🎯 Режим цели (Goal Mode): цель не установлена.\n${USAGE}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const milestonesInfo = snap.milestones.length > 0
|
|
76
|
+
? `\nПлан работ:\n` + snap.milestones.map((m, i) => ` ${i + 1}. [${m.status}] ${m.title}`).join('\n')
|
|
77
|
+
: '';
|
|
78
|
+
|
|
79
|
+
const etaText = snap.formattedETA ? ` (ETA: ${snap.formattedETA})` : '';
|
|
80
|
+
const tokensInfo = (snap.tokensUsage && snap.tokensUsage.totalTokens > 0)
|
|
81
|
+
? `\nТокены: ${snap.tokensUsage.totalTokens.toLocaleString()} (in: ${snap.tokensUsage.promptTokens.toLocaleString()}, out: ${snap.tokensUsage.completionTokens.toLocaleString()})`
|
|
82
|
+
: '';
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
kind: 'success',
|
|
86
|
+
text: `🎯 Цель: «${snap.title}»\n` +
|
|
87
|
+
`Статус: ${snap.state}\n` +
|
|
88
|
+
`Время: ${snap.formattedElapsed}${etaText}\n` +
|
|
89
|
+
`Итераций: ${snap.iterationsCount}/${snap.maxIterations}` +
|
|
90
|
+
tokensInfo +
|
|
91
|
+
milestonesInfo +
|
|
92
|
+
`\n\nКоманды: /goal pause, /goal resume, /goal clear, /goal <новая цель>`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'clear': {
|
|
97
|
+
if (!snap.hasActiveGoal) {
|
|
98
|
+
return {
|
|
99
|
+
kind: 'success',
|
|
100
|
+
text: 'Цель не была установлена.',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
engine.clear(sid);
|
|
104
|
+
if (agent && typeof agent.cancel === 'function') {
|
|
105
|
+
try {
|
|
106
|
+
agent.cancel({ kind: 'user', reason: 'Goal cleared by user' });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.warn('[dsh-goal] Failed to cancel agent on clear:', err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
kind: 'success',
|
|
113
|
+
text: '🎯 Цель сброшена.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case 'pause': {
|
|
118
|
+
if (!snap.hasActiveGoal) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'error',
|
|
121
|
+
text: `Нельзя приостановить: активная цель отсутствует.\n${USAGE}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (snap.state === GoalState.PAUSED) {
|
|
125
|
+
return {
|
|
126
|
+
kind: 'success',
|
|
127
|
+
text: '⏸ Цель уже на паузе.',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
engine.pause('Пауза по команде пользователя (/goal pause)', sid);
|
|
131
|
+
if (agent && typeof agent.cancel === 'function') {
|
|
132
|
+
try {
|
|
133
|
+
agent.cancel({ kind: 'user', reason: 'Goal paused by user' });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn('[dsh-goal] Failed to cancel agent on pause:', err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
kind: 'success',
|
|
140
|
+
text: `⏸ Цель приостановлена: «${snap.title}». Работа агента остановлена. Используйте /goal resume или кнопку ▶️ для продолжения.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case 'resume': {
|
|
145
|
+
if (!snap.hasActiveGoal) {
|
|
146
|
+
return {
|
|
147
|
+
kind: 'error',
|
|
148
|
+
text: `Нельзя возобновить: активная цель отсутствует.\n${USAGE}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (snap.state === GoalState.RUNNING) {
|
|
152
|
+
return {
|
|
153
|
+
kind: 'success',
|
|
154
|
+
text: '▶️ Цель уже выполняется.',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
engine.resume(sid);
|
|
158
|
+
if (agent && typeof agent.followup === 'function') {
|
|
159
|
+
try {
|
|
160
|
+
const resumePrompt = `▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.`;
|
|
161
|
+
agent.followup(createGoalUserMessage(resumePrompt));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.warn('[dsh-goal] Failed to followup agent on resume:', err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
kind: 'success',
|
|
168
|
+
text: `▶️ Цель возобновлена: «${snap.title}». Агент продолжает работу.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
case 'start': {
|
|
173
|
+
const maxIterations = config.maxIterations ?? snap.maxIterations ?? 25;
|
|
174
|
+
const newSnap = engine.startGoal(parsed.text, { maxIterations }, sid);
|
|
175
|
+
|
|
176
|
+
// Если команда вызвана в контексте агента, отправляем цель в очередь модели
|
|
177
|
+
if (agent && typeof agent.followup === 'function') {
|
|
178
|
+
try {
|
|
179
|
+
const userMsg = createGoalUserMessage(formatGoalStartPrompt(parsed.text));
|
|
180
|
+
agent.followup(userMsg);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.warn('[dsh-goal] Failed to dispatch goal to agent followup:', err);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
kind: 'success',
|
|
188
|
+
text: `🎯 Активирована цель: «${newSnap.title}» (макс. ${newSnap.maxIterations} итераций).\n` +
|
|
189
|
+
`Для паузы: /goal pause | Для сброса: /goal clear`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
return {
|
|
195
|
+
kind: 'error',
|
|
196
|
+
text: `Неизвестная команда. ${USAGE}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
package/lib/goal-engine.js
CHANGED
|
@@ -42,11 +42,28 @@ export function formatElapsed(totalSeconds) {
|
|
|
42
42
|
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Форматирование расчетного оставшегося времени (ETA)
|
|
47
|
+
* @param {number|null} seconds
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
export function formatETA(seconds) {
|
|
51
|
+
if (seconds == null || isNaN(seconds)) return null;
|
|
52
|
+
const sec = Math.max(0, Math.floor(seconds));
|
|
53
|
+
if (sec < 60) return `~${sec}s`;
|
|
54
|
+
const mins = Math.round(sec / 60);
|
|
55
|
+
if (mins < 60) return `~${mins}m`;
|
|
56
|
+
const hours = Math.floor(mins / 60);
|
|
57
|
+
const remainingMins = mins % 60;
|
|
58
|
+
return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
export class GoalEngine {
|
|
46
62
|
constructor(options = {}) {
|
|
47
63
|
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
48
64
|
this.autoDrive = options.autoDrive ?? true;
|
|
49
65
|
this.enableSound = options.enableSound ?? true;
|
|
66
|
+
this.showQuickLaunchButton = options.showQuickLaunchButton ?? true;
|
|
50
67
|
this.maxSessions = options.maxSessions ?? 100;
|
|
51
68
|
this.goals = new Map();
|
|
52
69
|
this.listeners = new Set();
|
|
@@ -96,6 +113,9 @@ export class GoalEngine {
|
|
|
96
113
|
if (goal.logs.length > 100) goal.logs = goal.logs.slice(-100);
|
|
97
114
|
dirty = true;
|
|
98
115
|
}
|
|
116
|
+
if (!goal.tokensUsage) {
|
|
117
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
118
|
+
}
|
|
99
119
|
this.goals.set(sid, goal);
|
|
100
120
|
}
|
|
101
121
|
}
|
|
@@ -112,6 +132,9 @@ export class GoalEngine {
|
|
|
112
132
|
if (data.logs.length > 100) data.logs = data.logs.slice(-100);
|
|
113
133
|
dirty = true;
|
|
114
134
|
}
|
|
135
|
+
if (!data.tokensUsage) {
|
|
136
|
+
data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
137
|
+
}
|
|
115
138
|
this.goals.set('default', data);
|
|
116
139
|
}
|
|
117
140
|
if (dirty) {
|
|
@@ -163,6 +186,10 @@ export class GoalEngine {
|
|
|
163
186
|
sessions: sessionsObj,
|
|
164
187
|
...(this.goals.has('default') ? this.goals.get('default') : {}),
|
|
165
188
|
};
|
|
189
|
+
const dir = path.dirname(this.storagePath);
|
|
190
|
+
if (!fs.existsSync(dir)) {
|
|
191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
192
|
+
}
|
|
166
193
|
const tmp = `${this.storagePath}.tmp.${Date.now()}`;
|
|
167
194
|
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
168
195
|
fs.renameSync(tmp, this.storagePath);
|
|
@@ -247,6 +274,9 @@ export class GoalEngine {
|
|
|
247
274
|
if (typeof config.enableSound === 'boolean') {
|
|
248
275
|
this.enableSound = config.enableSound;
|
|
249
276
|
}
|
|
277
|
+
if (typeof config.showQuickLaunchButton === 'boolean') {
|
|
278
|
+
this.showQuickLaunchButton = config.showQuickLaunchButton;
|
|
279
|
+
}
|
|
250
280
|
this.emit();
|
|
251
281
|
}
|
|
252
282
|
|
|
@@ -308,6 +338,7 @@ export class GoalEngine {
|
|
|
308
338
|
iterationsCount: 0,
|
|
309
339
|
maxIterations: options.maxIterations ?? this.defaultMaxIterations,
|
|
310
340
|
milestones: [],
|
|
341
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
311
342
|
logs: [
|
|
312
343
|
{
|
|
313
344
|
timestamp: now,
|
|
@@ -557,6 +588,57 @@ export class GoalEngine {
|
|
|
557
588
|
return Math.floor(elapsedMs / 1000);
|
|
558
589
|
}
|
|
559
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Накопление статистики использования токенов сессии
|
|
593
|
+
* @param {Object} usage
|
|
594
|
+
* @param {string} [sessionId='default']
|
|
595
|
+
*/
|
|
596
|
+
addTokenUsage(usage, sessionId = 'default') {
|
|
597
|
+
if (!usage || typeof usage !== 'object') return;
|
|
598
|
+
const sid = sessionId || 'default';
|
|
599
|
+
const goal = this.goals.get(sid);
|
|
600
|
+
if (!goal) return;
|
|
601
|
+
|
|
602
|
+
if (!goal.tokensUsage) {
|
|
603
|
+
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
607
|
+
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
608
|
+
const total = Number(usage.totalTokens ?? usage.total_tokens ?? (prompt + completion)) || (prompt + completion);
|
|
609
|
+
|
|
610
|
+
goal.tokensUsage.promptTokens += prompt;
|
|
611
|
+
goal.tokensUsage.completionTokens += completion;
|
|
612
|
+
goal.tokensUsage.totalTokens += total;
|
|
613
|
+
|
|
614
|
+
this.emit(sid);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Интеллектуальный расчет прогноза оставшегося времени (ETA)
|
|
619
|
+
* на основе средней скорости выполнения завершенных вех
|
|
620
|
+
* @param {string} [sessionId='default']
|
|
621
|
+
* @returns {number|null}
|
|
622
|
+
*/
|
|
623
|
+
getEstimatedRemainingSeconds(sessionId = 'default') {
|
|
624
|
+
const sid = sessionId || 'default';
|
|
625
|
+
const goal = this.goals.get(sid);
|
|
626
|
+
if (!goal || goal.state !== GoalState.RUNNING) return null;
|
|
627
|
+
|
|
628
|
+
const total = goal.milestones.length;
|
|
629
|
+
if (total === 0) return null;
|
|
630
|
+
|
|
631
|
+
const completedCount = goal.milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
632
|
+
if (completedCount === 0 || completedCount >= total) return null;
|
|
633
|
+
|
|
634
|
+
const elapsed = this.getElapsedSeconds(sid);
|
|
635
|
+
if (elapsed <= 0) return null;
|
|
636
|
+
|
|
637
|
+
const avgSecPerMilestone = elapsed / completedCount;
|
|
638
|
+
const remainingCount = total - completedCount;
|
|
639
|
+
return Math.max(1, Math.round(avgSecPerMilestone * remainingCount));
|
|
640
|
+
}
|
|
641
|
+
|
|
560
642
|
/**
|
|
561
643
|
* Снимок состояния для передачи клиенту / API
|
|
562
644
|
*/
|
|
@@ -575,12 +657,16 @@ export class GoalEngine {
|
|
|
575
657
|
completedAt: null,
|
|
576
658
|
elapsedSeconds: 0,
|
|
577
659
|
formattedElapsed: '0s',
|
|
660
|
+
estimatedRemainingSeconds: null,
|
|
661
|
+
formattedETA: null,
|
|
662
|
+
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
578
663
|
milestones: [],
|
|
579
664
|
progressPercent: 0,
|
|
580
665
|
iterationsCount: 0,
|
|
581
666
|
maxIterations: this.defaultMaxIterations,
|
|
582
667
|
autoDrive: this.autoDrive,
|
|
583
668
|
enableSound: this.enableSound,
|
|
669
|
+
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
584
670
|
};
|
|
585
671
|
}
|
|
586
672
|
|
|
@@ -588,6 +674,7 @@ export class GoalEngine {
|
|
|
588
674
|
const milestones = goal.milestones;
|
|
589
675
|
const completedCount = milestones.filter((m) => m.status === MilestoneStatus.COMPLETED).length;
|
|
590
676
|
const progressPercent = milestones.length > 0 ? Math.round((completedCount / milestones.length) * 100) : 0;
|
|
677
|
+
const estSec = this.getEstimatedRemainingSeconds(sid);
|
|
591
678
|
|
|
592
679
|
return {
|
|
593
680
|
sessionId: sid,
|
|
@@ -602,6 +689,9 @@ export class GoalEngine {
|
|
|
602
689
|
completedAt: goal.completedAt,
|
|
603
690
|
elapsedSeconds: elapsed,
|
|
604
691
|
formattedElapsed: formatElapsed(elapsed),
|
|
692
|
+
estimatedRemainingSeconds: estSec,
|
|
693
|
+
formattedETA: formatETA(estSec),
|
|
694
|
+
tokensUsage: goal.tokensUsage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
605
695
|
iterationsCount: goal.iterationsCount,
|
|
606
696
|
maxIterations: goal.maxIterations,
|
|
607
697
|
milestones,
|
|
@@ -610,6 +700,7 @@ export class GoalEngine {
|
|
|
610
700
|
resultSummary: goal.resultSummary,
|
|
611
701
|
autoDrive: this.autoDrive,
|
|
612
702
|
enableSound: this.enableSound,
|
|
703
|
+
showQuickLaunchButton: this.showQuickLaunchButton,
|
|
613
704
|
};
|
|
614
705
|
}
|
|
615
706
|
|
|
@@ -629,9 +720,11 @@ export class GoalEngine {
|
|
|
629
720
|
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
630
721
|
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
631
722
|
|
|
723
|
+
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
724
|
+
|
|
632
725
|
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
633
726
|
Цель: "${snapshot.title}"
|
|
634
|
-
Время работы: ${snapshot.formattedElapsed} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
727
|
+
Время работы: ${snapshot.formattedElapsed}${etaText} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
635
728
|
План работ:
|
|
636
729
|
${milestonesText}
|
|
637
730
|
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,7 @@ export const Config = z.object({
|
|
|
42
42
|
maxIterations: z.number().default(25).description('Safety limit: max autonomous iterations per goal'),
|
|
43
43
|
autoDrive: z.boolean().default(true).description('Automatically continue the loop after each turn'),
|
|
44
44
|
enableSound: z.boolean().default(true).description('Play synthesized audio chime on goal completion or failure'),
|
|
45
|
+
showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
|
|
45
46
|
storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
|
|
46
47
|
});
|
|
47
48
|
|
|
@@ -59,6 +60,7 @@ export function apply(ctx, config = {}) {
|
|
|
59
60
|
maxIterations: config?.maxIterations ?? 25,
|
|
60
61
|
autoDrive: config?.autoDrive ?? true,
|
|
61
62
|
enableSound: config?.enableSound ?? true,
|
|
63
|
+
showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
|
|
62
64
|
storagePath: config?.storagePath,
|
|
63
65
|
};
|
|
64
66
|
|
|
@@ -71,6 +73,7 @@ export function apply(ctx, config = {}) {
|
|
|
71
73
|
defaultMaxIterations: currentSettings.maxIterations,
|
|
72
74
|
autoDrive: currentSettings.autoDrive,
|
|
73
75
|
enableSound: currentSettings.enableSound,
|
|
76
|
+
showQuickLaunchButton: currentSettings.showQuickLaunchButton,
|
|
74
77
|
storagePath,
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -215,6 +218,7 @@ export function apply(ctx, config = {}) {
|
|
|
215
218
|
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
216
219
|
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
217
220
|
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
221
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
218
222
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
219
223
|
};
|
|
220
224
|
}
|
|
@@ -226,6 +230,7 @@ export function apply(ctx, config = {}) {
|
|
|
226
230
|
maxIterations: typeof live.maxIterations === 'number' ? live.maxIterations : currentSettings.maxIterations,
|
|
227
231
|
autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
|
|
228
232
|
enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
|
|
233
|
+
showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
|
|
229
234
|
storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
|
|
230
235
|
};
|
|
231
236
|
}
|
|
@@ -240,6 +245,7 @@ export function apply(ctx, config = {}) {
|
|
|
240
245
|
defaultMaxIterations: live.maxIterations,
|
|
241
246
|
autoDrive: live.autoDrive,
|
|
242
247
|
enableSound: live.enableSound,
|
|
248
|
+
showQuickLaunchButton: live.showQuickLaunchButton,
|
|
243
249
|
});
|
|
244
250
|
};
|
|
245
251
|
|
|
@@ -526,12 +532,19 @@ export function apply(ctx, config = {}) {
|
|
|
526
532
|
|
|
527
533
|
let result = null;
|
|
528
534
|
switch (action) {
|
|
529
|
-
case 'start':
|
|
530
|
-
|
|
531
|
-
|
|
535
|
+
case 'start': {
|
|
536
|
+
const cleanTitle = typeof title === 'string' ? title.trim() : '';
|
|
537
|
+
if (!cleanTitle) {
|
|
538
|
+
res.statusCode = 400;
|
|
539
|
+
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
540
|
+
}
|
|
541
|
+
result = engine.startGoal(cleanTitle, {
|
|
542
|
+
description: typeof description === 'string' ? description.trim() : '',
|
|
532
543
|
maxIterations: getConfig().maxIterations,
|
|
533
544
|
}, sid);
|
|
545
|
+
resumeActiveAgent(`🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`, sid);
|
|
534
546
|
break;
|
|
547
|
+
}
|
|
535
548
|
case 'pause':
|
|
536
549
|
result = engine.pause(reason || 'Пауза по кнопке интерфейса', sid);
|
|
537
550
|
stopRunningAgents(sid);
|
|
@@ -549,14 +562,24 @@ export function apply(ctx, config = {}) {
|
|
|
549
562
|
stopRunningAgents(sid);
|
|
550
563
|
sessionAgents.delete(sid);
|
|
551
564
|
break;
|
|
552
|
-
case 'update_milestone':
|
|
565
|
+
case 'update_milestone': {
|
|
553
566
|
if (!milestoneId || !status) {
|
|
554
567
|
res.statusCode = 400;
|
|
555
568
|
return res.end(JSON.stringify({ error: 'milestoneId and status are required' }));
|
|
556
569
|
}
|
|
557
|
-
|
|
570
|
+
const validStatuses = Object.values(MilestoneStatus);
|
|
571
|
+
if (!validStatuses.includes(status)) {
|
|
572
|
+
res.statusCode = 400;
|
|
573
|
+
return res.end(JSON.stringify({ error: `Invalid status: ${status}. Must be one of: ${validStatuses.join(', ')}` }));
|
|
574
|
+
}
|
|
575
|
+
const ok = engine.updateMilestone(milestoneId, status, notes, sid);
|
|
576
|
+
if (!ok) {
|
|
577
|
+
res.statusCode = 404;
|
|
578
|
+
return res.end(JSON.stringify({ error: `Milestone with id "${milestoneId}" not found` }));
|
|
579
|
+
}
|
|
558
580
|
result = engine.getSnapshot(sid);
|
|
559
581
|
break;
|
|
582
|
+
}
|
|
560
583
|
default:
|
|
561
584
|
res.statusCode = 400;
|
|
562
585
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
|
@@ -599,6 +622,13 @@ export function apply(ctx, config = {}) {
|
|
|
599
622
|
if (!liveConfig.autoDrive) return; // Учитываем настройку autoDrive в рантайме
|
|
600
623
|
|
|
601
624
|
const sid = sessionIdOf(turn, 'default');
|
|
625
|
+
|
|
626
|
+
// Накопление токенов по завершении хода
|
|
627
|
+
const usage = turn?.usage || turn?.meta?.usage || turn?.response?.usage || turn?.turn?.usage;
|
|
628
|
+
if (usage) {
|
|
629
|
+
engine.addTokenUsage(usage, sid);
|
|
630
|
+
}
|
|
631
|
+
|
|
602
632
|
const snap = engine.getSnapshot(sid);
|
|
603
633
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
604
634
|
// Проверяем причину завершения хода
|