@goodandready/dsh-goal 0.1.8 → 0.1.10
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/README.md +121 -44
- package/{docs/README.ru.md → README.ru.md} +12 -1
- package/README.zh.md +219 -0
- package/docs/design/DESIGN.md +23 -0
- package/lib/client.js +146 -61
- package/lib/command-handler.js +76 -34
- package/lib/goal-engine.js +49 -5
- package/lib/index.js +344 -131
- package/package.json +6 -4
- package/docs/README.zh.md +0 -143
package/lib/goal-engine.js
CHANGED
|
@@ -58,6 +58,23 @@ export function formatETA(seconds) {
|
|
|
58
58
|
return remainingMins > 0 ? `~${hours}h ${remainingMins}m` : `~${hours}h`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Автоматическое определение языка текста (кириллица -> ru, иероглифы -> zh, иначе en)
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* @param {string} [fallback='en']
|
|
65
|
+
* @returns {'ru' | 'en' | 'zh'}
|
|
66
|
+
*/
|
|
67
|
+
export function detectLanguage(text, fallback = 'en') {
|
|
68
|
+
if (!text || typeof text !== 'string') return fallback;
|
|
69
|
+
if (/[а-яёА-ЯЁ]/i.test(text)) {
|
|
70
|
+
return 'ru';
|
|
71
|
+
}
|
|
72
|
+
if (/[\u4e00-\u9fa5]/.test(text)) {
|
|
73
|
+
return 'zh';
|
|
74
|
+
}
|
|
75
|
+
return 'en';
|
|
76
|
+
}
|
|
77
|
+
|
|
61
78
|
export class GoalEngine {
|
|
62
79
|
constructor(options = {}) {
|
|
63
80
|
this.defaultMaxIterations = options.defaultMaxIterations ?? 25;
|
|
@@ -116,6 +133,9 @@ export class GoalEngine {
|
|
|
116
133
|
if (!goal.tokensUsage) {
|
|
117
134
|
goal.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
118
135
|
}
|
|
136
|
+
if (!goal.lang) {
|
|
137
|
+
goal.lang = detectLanguage(goal.title);
|
|
138
|
+
}
|
|
119
139
|
this.goals.set(sid, goal);
|
|
120
140
|
}
|
|
121
141
|
}
|
|
@@ -135,6 +155,9 @@ export class GoalEngine {
|
|
|
135
155
|
if (!data.tokensUsage) {
|
|
136
156
|
data.tokensUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
137
157
|
}
|
|
158
|
+
if (!data.lang) {
|
|
159
|
+
data.lang = detectLanguage(data.title);
|
|
160
|
+
}
|
|
138
161
|
this.goals.set('default', data);
|
|
139
162
|
}
|
|
140
163
|
if (dirty) {
|
|
@@ -330,6 +353,7 @@ export class GoalEngine {
|
|
|
330
353
|
sessionId: sid,
|
|
331
354
|
title: cleanTitle,
|
|
332
355
|
description: options.description?.trim() || '',
|
|
356
|
+
lang: options.lang || detectLanguage(cleanTitle),
|
|
333
357
|
state: GoalState.RUNNING,
|
|
334
358
|
startedAt: now,
|
|
335
359
|
pausedAt: null,
|
|
@@ -659,6 +683,7 @@ export class GoalEngine {
|
|
|
659
683
|
formattedElapsed: '0s',
|
|
660
684
|
estimatedRemainingSeconds: null,
|
|
661
685
|
formattedETA: null,
|
|
686
|
+
lang: 'en',
|
|
662
687
|
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
663
688
|
milestones: [],
|
|
664
689
|
progressPercent: 0,
|
|
@@ -683,6 +708,7 @@ export class GoalEngine {
|
|
|
683
708
|
state: goal.state,
|
|
684
709
|
title: goal.title,
|
|
685
710
|
description: goal.description,
|
|
711
|
+
lang: goal.lang || detectLanguage(goal.title),
|
|
686
712
|
startedAt: goal.startedAt,
|
|
687
713
|
pausedAt: goal.pausedAt,
|
|
688
714
|
totalPausedDurationMs: goal.totalPausedDurationMs,
|
|
@@ -715,14 +741,16 @@ export class GoalEngine {
|
|
|
715
741
|
}
|
|
716
742
|
|
|
717
743
|
const snapshot = this.getSnapshot(sid);
|
|
744
|
+
const lang = goal.lang || detectLanguage(snapshot.title);
|
|
718
745
|
const hasMilestones = snapshot.milestones.length > 0;
|
|
719
|
-
const milestonesText = hasMilestones
|
|
720
|
-
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
721
|
-
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
722
|
-
|
|
723
746
|
const etaText = snapshot.formattedETA ? ` (ETA: ${snapshot.formattedETA})` : '';
|
|
724
747
|
|
|
725
|
-
|
|
748
|
+
if (lang === 'ru') {
|
|
749
|
+
const milestonesText = hasMilestones
|
|
750
|
+
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
751
|
+
: ' (План работ ещё не сформирован — немедленно вызови goal_set_milestones со списком шагов!)';
|
|
752
|
+
|
|
753
|
+
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
726
754
|
Цель: "${snapshot.title}"
|
|
727
755
|
Время работы: ${snapshot.formattedElapsed}${etaText} | Итерация: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
728
756
|
План работ:
|
|
@@ -732,5 +760,21 @@ ${milestonesText}
|
|
|
732
760
|
1. ${hasMilestones ? 'Выполняй текущий активный пункт плана работ.' : 'ТВОЙ ПЕРВЫЙ ШАГ: Немедленно вызови инструмент goal_set_milestones со списком пунктов плана работ (3-7 конкретных шагов). Запрещено выполнять работу или завершать turn без вызова goal_set_milestones!'}
|
|
733
761
|
2. По мере выполнения каждого шага обязательно отмечай его статус через инструмент goal_update_progress (status: "in_progress" перед началом шага, status: "completed" по его завершении с кратким notes).
|
|
734
762
|
3. Когда все пункты плана будут выполнены, вызови инструмент goal_finish с подробным итоговым резюме достигнутых результатов.`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const milestonesText = hasMilestones
|
|
766
|
+
? snapshot.milestones.map((m, i) => ` ${i + 1}. [${m.status.toUpperCase()}] ${m.title}${m.notes ? ` (${m.notes})` : ''}`).join('\n')
|
|
767
|
+
: ' (Work plan is not yet established — call goal_set_milestones immediately with initial steps!)';
|
|
768
|
+
|
|
769
|
+
return `\n\n[DSH GOAL MODE ACTIVE]
|
|
770
|
+
Goal: "${snapshot.title}"
|
|
771
|
+
Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}
|
|
772
|
+
Work Plan:
|
|
773
|
+
${milestonesText}
|
|
774
|
+
|
|
775
|
+
Goal Mode Instructions (MANDATORY TO FOLLOW):
|
|
776
|
+
1. ${hasMilestones ? 'Execute the current active milestone from the work plan.' : 'YOUR FIRST STEP: Immediately call tool goal_set_milestones with the list of milestones (3-7 concrete steps). You must not execute work or finish turn without calling goal_set_milestones!'}
|
|
777
|
+
2. As each milestone progresses, update its status via tool goal_update_progress (status: "in_progress" before starting, status: "completed" upon completion with brief notes).
|
|
778
|
+
3. When all milestones are completed, call tool goal_finish with a detailed summary of achieved results.`;
|
|
735
779
|
}
|
|
736
780
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
import { GoalEngine, GoalState, MilestoneStatus } from './goal-engine.js';
|
|
4
|
+
import { GoalEngine, GoalState, MilestoneStatus, detectLanguage } from './goal-engine.js';
|
|
5
5
|
import { parseGoalInput, executeGoalSlashCommand, createGoalUserMessage } from './command-handler.js';
|
|
6
6
|
|
|
7
7
|
export const name = '@goodandready/dsh-goal';
|
|
@@ -166,9 +166,12 @@ export function apply(ctx, config = {}) {
|
|
|
166
166
|
|
|
167
167
|
const resumeActiveAgent = (promptText, sessionId = 'default') => {
|
|
168
168
|
const sid = sessionId || 'default';
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
const snap = engine.getSnapshot(sid);
|
|
170
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
171
|
+
const defaultResumePrompt = lang === 'ru'
|
|
172
|
+
? '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.'
|
|
173
|
+
: '▶️ Goal resumed by user. Continue executing the work plan from where you stopped. Update steps via goal_update_progress.';
|
|
174
|
+
const resumeMsg = createGoalUserMessage(promptText || defaultResumePrompt);
|
|
172
175
|
let target = sessionAgents.get(sid);
|
|
173
176
|
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
174
177
|
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
@@ -302,127 +305,322 @@ export function apply(ctx, config = {}) {
|
|
|
302
305
|
}
|
|
303
306
|
});
|
|
304
307
|
|
|
305
|
-
// 3. Инжекция контекста цели в системный промпт (systemPrompt)
|
|
306
|
-
ctx.inject(['systemPrompt'], (pctx) => {
|
|
307
|
-
try {
|
|
308
|
-
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
},
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
308
|
+
// 3. Инжекция контекста цели в системный промпт (systemPrompt)
|
|
309
|
+
ctx.inject(['systemPrompt'], (pctx) => {
|
|
310
|
+
try {
|
|
311
|
+
if (typeof pctx.systemPrompt?.section === 'function') {
|
|
312
|
+
// Clear conflicting core tool:goal prompt section if present
|
|
313
|
+
const globalSections = pctx.systemPrompt.layers?.global?.sections;
|
|
314
|
+
if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
|
|
315
|
+
globalSections.data.delete('tool:goal');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
|
|
319
|
+
? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 2400)
|
|
320
|
+
: 2400;
|
|
321
|
+
|
|
322
|
+
const unregisterSection = pctx.systemPrompt.section({
|
|
323
|
+
name: 'tool:dsh-goal',
|
|
324
|
+
order,
|
|
325
|
+
text: (sessionCtx) => {
|
|
326
|
+
const sid = sessionIdOf(sessionCtx, 'default');
|
|
327
|
+
return engine.getStatePromptInjection(sid);
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (typeof unregisterSection === 'function') {
|
|
332
|
+
pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch (err) {
|
|
336
|
+
console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// 4. Регистрация инструментов для модели (tools)
|
|
341
|
+
ctx.inject(['tools'], (tctx) => {
|
|
342
|
+
if (!tctx.tools?.register) return;
|
|
343
|
+
|
|
344
|
+
// Helper: Safely replace or register tool in ToolRuntime
|
|
345
|
+
const safeRegister = (definition) => {
|
|
346
|
+
try {
|
|
347
|
+
const name = definition.name;
|
|
348
|
+
const globalTools = tctx.tools.layers?.global?.tools;
|
|
349
|
+
if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
|
|
350
|
+
globalTools.data.delete(name);
|
|
351
|
+
}
|
|
352
|
+
const unregister = tctx.tools.register(definition);
|
|
353
|
+
if (typeof unregister === 'function') {
|
|
354
|
+
tctx.effect(() => () => unregister(), `dsh-goal: tool ${name}`);
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const JSON_OUTPUT = {
|
|
362
|
+
schema: { type: 'object', additionalProperties: true },
|
|
363
|
+
render: (_args, val) => [{ type: 'text', text: JSON.stringify(val) }],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
function formatGoalValue(snap) {
|
|
367
|
+
if (!snap || !snap.hasActiveGoal) {
|
|
368
|
+
return { goal: null };
|
|
369
|
+
}
|
|
370
|
+
let phase = 'active';
|
|
371
|
+
if (snap.state === GoalState.PAUSED) phase = 'paused';
|
|
372
|
+
else if (snap.state === GoalState.COMPLETED) phase = 'complete';
|
|
373
|
+
|
|
374
|
+
const roundsStarted = Number.isInteger(snap.iterations) ? snap.iterations : 0;
|
|
375
|
+
const maxGoalRounds = Number.isInteger(snap.maxIterations) ? snap.maxIterations : 25;
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
goal: {
|
|
379
|
+
id: snap.id || 'goal-active',
|
|
380
|
+
revision: 1,
|
|
381
|
+
objective: snap.title || 'Goal',
|
|
382
|
+
phase,
|
|
383
|
+
roundsStarted,
|
|
384
|
+
maxGoalRounds,
|
|
385
|
+
},
|
|
386
|
+
activation: snap.state === GoalState.RUNNING ? 'armed' : 'disarmed',
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Совместимый инструмент 1: get_goal
|
|
391
|
+
const handleGetGoal = async (_args, toolCtx) => {
|
|
392
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
393
|
+
const snap = engine.getSnapshot(sid);
|
|
394
|
+
return formatGoalValue(snap);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
safeRegister({
|
|
398
|
+
name: 'get_goal',
|
|
399
|
+
description: 'Read the current same-session goal, including objective, phase, and round limits.',
|
|
400
|
+
parameters: { type: 'object', properties: {} },
|
|
401
|
+
output: JSON_OUTPUT,
|
|
402
|
+
execute: handleGetGoal,
|
|
403
|
+
handler: handleGetGoal,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Совместимый инструмент 2: create_goal
|
|
407
|
+
const handleCreateGoal = async (args, toolCtx) => {
|
|
408
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
409
|
+
const maxIterations = Number(args.max_goal_rounds) || 25;
|
|
410
|
+
const snap = engine.startGoal(args.objective, { maxIterations }, sid);
|
|
411
|
+
return formatGoalValue(snap);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
safeRegister({
|
|
415
|
+
name: 'create_goal',
|
|
416
|
+
description: 'Create one persisted same-session completion goal for long-running autonomous work.',
|
|
417
|
+
parameters: {
|
|
418
|
+
type: 'object',
|
|
419
|
+
properties: {
|
|
420
|
+
objective: {
|
|
421
|
+
type: 'string',
|
|
422
|
+
description: 'The concrete completion objective.',
|
|
423
|
+
},
|
|
424
|
+
max_goal_rounds: {
|
|
425
|
+
type: 'number',
|
|
426
|
+
description: 'Optional positive integer limit on automatic continuation rounds.',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
required: ['objective'],
|
|
430
|
+
},
|
|
431
|
+
output: JSON_OUTPUT,
|
|
432
|
+
execute: handleCreateGoal,
|
|
433
|
+
handler: handleCreateGoal,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Совместимый инструмент 3: update_goal (перехватчик authority checks и роутер в GoalEngine)
|
|
437
|
+
const handleUpdateGoal = async (args, toolCtx) => {
|
|
438
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
439
|
+
const action = args.action;
|
|
440
|
+
|
|
441
|
+
if (action === 'complete') {
|
|
442
|
+
const summary = args.blocked_reason || args.objective || 'Goal marked complete';
|
|
443
|
+
const snap = engine.completeGoal(summary, sid);
|
|
444
|
+
if (toolCtx?.deferContext) {
|
|
445
|
+
try {
|
|
446
|
+
toolCtx.deferContext({
|
|
447
|
+
type: 'text',
|
|
448
|
+
text: `<goal_complete>\nObjective: ${JSON.stringify(snap.title || summary)}\nThe goal is marked complete. Summarize what was accomplished for the user.\n</goal_complete>`,
|
|
449
|
+
});
|
|
450
|
+
} catch (_) {}
|
|
451
|
+
}
|
|
452
|
+
return formatGoalValue(snap);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (action === 'pause') {
|
|
456
|
+
const reason = args.blocked_reason || 'Paused by model';
|
|
457
|
+
const snap = engine.pause(reason, sid);
|
|
458
|
+
return formatGoalValue(snap);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (action === 'resume') {
|
|
462
|
+
const snap = engine.resume(sid);
|
|
463
|
+
resumeActiveAgent(undefined, sid);
|
|
464
|
+
return formatGoalValue(snap);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (action === 'edit') {
|
|
468
|
+
const snap = engine.getSnapshot(sid);
|
|
469
|
+
if (args.objective) snap.title = args.objective;
|
|
470
|
+
if (args.max_goal_rounds) snap.maxIterations = Number(args.max_goal_rounds);
|
|
471
|
+
engine.emit(sid, true);
|
|
472
|
+
return formatGoalValue(snap);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (action === 'blocked') {
|
|
476
|
+
const reason = args.blocked_reason || 'Goal blocked';
|
|
477
|
+
const snap = engine.pause('Blocked: ' + reason, sid);
|
|
478
|
+
if (toolCtx?.deferContext) {
|
|
479
|
+
try {
|
|
480
|
+
toolCtx.deferContext({
|
|
481
|
+
type: 'text',
|
|
482
|
+
text: `<goal_blocked>\nObjective: ${JSON.stringify(snap.title || 'Goal')}\nBlocked: ${JSON.stringify(reason)}\nExplain to the user what blocked progress.\n</goal_blocked>`,
|
|
483
|
+
});
|
|
484
|
+
} catch (_) {}
|
|
485
|
+
}
|
|
486
|
+
const res = formatGoalValue(snap);
|
|
487
|
+
if (res.goal) {
|
|
488
|
+
res.goal.phase = 'blocked';
|
|
489
|
+
res.goal.blockedReason = { code: 'model-reported', message: reason };
|
|
490
|
+
}
|
|
491
|
+
return res;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return formatGoalValue(engine.getSnapshot(sid));
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
safeRegister({
|
|
498
|
+
name: 'update_goal',
|
|
499
|
+
description: 'Update the active goal: complete, pause, resume, edit, or report blocked.',
|
|
500
|
+
parameters: {
|
|
501
|
+
type: 'object',
|
|
502
|
+
properties: {
|
|
503
|
+
goal_id: { type: 'string', description: 'Exact id returned by get_goal.' },
|
|
504
|
+
revision: { type: 'number', description: 'Exact positive revision returned by get_goal.' },
|
|
505
|
+
action: {
|
|
506
|
+
type: 'string',
|
|
507
|
+
enum: ['edit', 'pause', 'resume', 'complete', 'blocked'],
|
|
508
|
+
description: 'edit | pause | resume | complete | blocked',
|
|
509
|
+
},
|
|
510
|
+
objective: { type: 'string', description: 'Replacement objective; valid with action edit.' },
|
|
511
|
+
max_goal_rounds: { type: 'number', description: 'Replacement cap; valid with action edit.' },
|
|
512
|
+
blocked_reason: { type: 'string', description: 'Concrete blocking condition; required with action blocked.' },
|
|
513
|
+
},
|
|
514
|
+
required: ['action'],
|
|
515
|
+
},
|
|
516
|
+
output: JSON_OUTPUT,
|
|
517
|
+
execute: handleUpdateGoal,
|
|
518
|
+
handler: handleUpdateGoal,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Инструмент 4: Декомпозиция цели на вехи
|
|
522
|
+
const handleSetMilestones = async ({ milestones }, toolCtx) => {
|
|
523
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
524
|
+
const snap = engine.getSnapshot(sid);
|
|
525
|
+
if (!snap.hasActiveGoal) {
|
|
526
|
+
return { error: 'No active goal currently set. Start a goal first.' };
|
|
527
|
+
}
|
|
528
|
+
engine.addMilestones(milestones, true, sid);
|
|
529
|
+
return {
|
|
530
|
+
success: true,
|
|
531
|
+
milestones: engine.getSnapshot(sid).milestones,
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
safeRegister({
|
|
536
|
+
name: 'goal_set_milestones',
|
|
537
|
+
description: 'Break down the current active goal into a sequence of concrete milestones/sub-tasks.',
|
|
538
|
+
parameters: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
properties: {
|
|
541
|
+
milestones: {
|
|
542
|
+
type: 'array',
|
|
543
|
+
items: { type: 'string' },
|
|
544
|
+
description: 'List of milestone titles to accomplish.',
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
required: ['milestones'],
|
|
548
|
+
},
|
|
549
|
+
output: JSON_OUTPUT,
|
|
550
|
+
execute: handleSetMilestones,
|
|
551
|
+
handler: handleSetMilestones,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// Инструмент 5: Обновление статуса вехи
|
|
555
|
+
const handleUpdateProgress = async ({ milestone_id, status, notes }, toolCtx) => {
|
|
556
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
557
|
+
const ok = engine.updateMilestone(milestone_id, status, notes, sid);
|
|
558
|
+
if (!ok) {
|
|
559
|
+
return { error: `Milestone ${milestone_id} not found or no active goal.` };
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
success: true,
|
|
563
|
+
snapshot: engine.getSnapshot(sid),
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
safeRegister({
|
|
568
|
+
name: 'goal_update_progress',
|
|
569
|
+
description: 'Update the status of a specific goal milestone and optionally log progress notes.',
|
|
570
|
+
parameters: {
|
|
571
|
+
type: 'object',
|
|
572
|
+
properties: {
|
|
573
|
+
milestone_id: {
|
|
574
|
+
type: 'string',
|
|
575
|
+
description: 'The ID of the milestone (e.g. "m-1", "m-2").',
|
|
576
|
+
},
|
|
577
|
+
status: {
|
|
578
|
+
type: 'string',
|
|
579
|
+
enum: ['pending', 'in_progress', 'completed', 'failed'],
|
|
580
|
+
description: 'New status for this milestone.',
|
|
581
|
+
},
|
|
582
|
+
notes: {
|
|
583
|
+
type: 'string',
|
|
584
|
+
description: 'Brief summary of what was accomplished or why it failed.',
|
|
585
|
+
},
|
|
586
|
+
},
|
|
587
|
+
required: ['milestone_id', 'status'],
|
|
588
|
+
},
|
|
589
|
+
output: JSON_OUTPUT,
|
|
590
|
+
execute: handleUpdateProgress,
|
|
591
|
+
handler: handleUpdateProgress,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Инструмент 6: Успешное завершение цели
|
|
595
|
+
const handleGoalFinish = async ({ summary }, toolCtx) => {
|
|
596
|
+
const sid = sessionIdOf(toolCtx, 'default');
|
|
597
|
+
const snap = engine.completeGoal(summary, sid);
|
|
598
|
+
return {
|
|
599
|
+
success: true,
|
|
600
|
+
completed: true,
|
|
601
|
+
summary,
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
safeRegister({
|
|
606
|
+
name: 'goal_finish',
|
|
607
|
+
description: 'Conclude the active goal successfully with a final summary and achievements.',
|
|
608
|
+
parameters: {
|
|
609
|
+
type: 'object',
|
|
610
|
+
properties: {
|
|
611
|
+
summary: {
|
|
612
|
+
type: 'string',
|
|
613
|
+
description: 'Final summary of the goal outcome and deliverables.',
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
required: ['summary'],
|
|
617
|
+
},
|
|
618
|
+
output: JSON_OUTPUT,
|
|
619
|
+
execute: handleGoalFinish,
|
|
620
|
+
handler: handleGoalFinish,
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
426
624
|
// 5. Регистрация HTTP REST API маршрутов и SSE событий
|
|
427
625
|
ctx.effect(() => {
|
|
428
626
|
if (!ctx.webServer?.register) return () => {};
|
|
@@ -538,11 +736,16 @@ export function apply(ctx, config = {}) {
|
|
|
538
736
|
res.statusCode = 400;
|
|
539
737
|
return res.end(JSON.stringify({ error: 'Goal title cannot be empty' }));
|
|
540
738
|
}
|
|
739
|
+
const detectedLang = data.lang || detectLanguage(cleanTitle);
|
|
541
740
|
result = engine.startGoal(cleanTitle, {
|
|
542
741
|
description: typeof description === 'string' ? description.trim() : '',
|
|
543
742
|
maxIterations: getConfig().maxIterations,
|
|
743
|
+
lang: detectedLang,
|
|
544
744
|
}, sid);
|
|
545
|
-
|
|
745
|
+
const startPrompt = detectedLang === 'ru'
|
|
746
|
+
? `🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`
|
|
747
|
+
: `🎯 Goal established: "${cleanTitle}". Immediately formulate a work plan (3-7 concrete steps) via tool goal_set_milestones and start executing it.`;
|
|
748
|
+
resumeActiveAgent(startPrompt, sid);
|
|
546
749
|
break;
|
|
547
750
|
}
|
|
548
751
|
case 'pause':
|
|
@@ -637,9 +840,13 @@ export function apply(ctx, config = {}) {
|
|
|
637
840
|
}
|
|
638
841
|
|
|
639
842
|
// Item 3: Smart Progress Guard — detect idle loops without progress
|
|
843
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
640
844
|
const stallCount = engine.incrementStallCount(sid);
|
|
641
845
|
if (stallCount >= 2) {
|
|
642
|
-
|
|
846
|
+
const stallReason = lang === 'ru'
|
|
847
|
+
? 'Агент не продвинулся по плану за последние 2 итерации — требуется внимание оператора'
|
|
848
|
+
: 'Agent made no progress on work plan in the last 2 iterations — operator attention required';
|
|
849
|
+
engine.pause(stallReason, sid);
|
|
643
850
|
return;
|
|
644
851
|
}
|
|
645
852
|
|
|
@@ -650,9 +857,11 @@ export function apply(ctx, config = {}) {
|
|
|
650
857
|
const triggerNextTurn = () => {
|
|
651
858
|
const currentSnap = engine.getSnapshot(sid);
|
|
652
859
|
if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
860
|
+
const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
|
|
861
|
+
const promptText = currentLang === 'ru'
|
|
862
|
+
? 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.'
|
|
863
|
+
: 'Continue autonomous goal execution according to the work plan. Mark each completed step via goal_update_progress, and when all tasks are complete, call goal_finish.';
|
|
864
|
+
const promptMsg = createGoalUserMessage(promptText);
|
|
656
865
|
let target = sessionAgents.get(sid);
|
|
657
866
|
if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {
|
|
658
867
|
const lastSid = sessionIdOf(lastActiveAgent, null);
|
|
@@ -698,7 +907,11 @@ export function apply(ctx, config = {}) {
|
|
|
698
907
|
const sid = sessionIdOf(event, 'default');
|
|
699
908
|
const snap = engine.getSnapshot(sid);
|
|
700
909
|
if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
|
|
701
|
-
|
|
910
|
+
const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
|
|
911
|
+
const pauseReason = lang === 'ru'
|
|
912
|
+
? 'Ожидание подтверждения действия оператором'
|
|
913
|
+
: 'Waiting for operator confirmation/approval';
|
|
914
|
+
engine.pause(pauseReason, sid);
|
|
702
915
|
}
|
|
703
916
|
};
|
|
704
917
|
|