@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.
@@ -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
- return `\n\n[DSH GOAL MODE ACTIVE]
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 resumeMsg = createGoalUserMessage(
170
- promptText || '▶️ Цель возобновлена пользователем. Продолжай выполнение плана работ с того места, где остановился. Отмечай шаги через goal_update_progress.',
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
- const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
310
- ? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 600)
311
- : 600;
312
-
313
- const unregisterSection = pctx.systemPrompt.section({
314
- name: 'tool:dsh-goal',
315
- order,
316
- text: (sessionCtx) => {
317
- const sid = sessionIdOf(sessionCtx, 'default');
318
- return engine.getStatePromptInjection(sid);
319
- },
320
- });
321
-
322
- if (typeof unregisterSection === 'function') {
323
- pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
324
- }
325
- }
326
- } catch (err) {
327
- console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
328
- }
329
- });
330
-
331
- // 4. Регистрация инструментов для модели (tools)
332
- ctx.inject(['tools'], (tctx) => {
333
- if (!tctx.tools?.register) return;
334
-
335
- // Инструмент 1: Декомпозиция цели на вехи
336
- tctx.tools.register({
337
- name: 'goal_set_milestones',
338
- description: 'Break down the current active goal into a sequence of concrete milestones/sub-tasks.',
339
- parameters: {
340
- type: 'object',
341
- properties: {
342
- milestones: {
343
- type: 'array',
344
- items: { type: 'string' },
345
- description: 'List of milestone titles to accomplish.',
346
- },
347
- },
348
- required: ['milestones'],
349
- },
350
- handler: async ({ milestones }, toolCtx) => {
351
- const sid = sessionIdOf(toolCtx, 'default');
352
- const snap = engine.getSnapshot(sid);
353
- if (!snap.hasActiveGoal) {
354
- return { error: 'No active goal currently set. Start a goal first.' };
355
- }
356
- engine.addMilestones(milestones, true, sid);
357
- return {
358
- success: true,
359
- milestones: engine.getSnapshot(sid).milestones,
360
- };
361
- },
362
- });
363
-
364
- // Инструмент 2: Обновление статуса вехи
365
- tctx.tools.register({
366
- name: 'goal_update_progress',
367
- description: 'Update the status of a specific goal milestone and optionally log progress notes.',
368
- parameters: {
369
- type: 'object',
370
- properties: {
371
- milestone_id: {
372
- type: 'string',
373
- description: 'The ID of the milestone (e.g. "m-1", "m-2").',
374
- },
375
- status: {
376
- type: 'string',
377
- enum: ['pending', 'in_progress', 'completed', 'failed'],
378
- description: 'New status for this milestone.',
379
- },
380
- notes: {
381
- type: 'string',
382
- description: 'Brief summary of what was accomplished or why it failed.',
383
- },
384
- },
385
- required: ['milestone_id', 'status'],
386
- },
387
- handler: async ({ milestone_id, status, notes }, toolCtx) => {
388
- const sid = sessionIdOf(toolCtx, 'default');
389
- const ok = engine.updateMilestone(milestone_id, status, notes, sid);
390
- if (!ok) {
391
- return { error: `Milestone ${milestone_id} not found or no active goal.` };
392
- }
393
- return {
394
- success: true,
395
- snapshot: engine.getSnapshot(sid),
396
- };
397
- },
398
- });
399
-
400
- // Инструмент 3: Успешное завершение цели
401
- tctx.tools.register({
402
- name: 'goal_finish',
403
- description: 'Conclude the active goal successfully with a final summary and achievements.',
404
- parameters: {
405
- type: 'object',
406
- properties: {
407
- summary: {
408
- type: 'string',
409
- description: 'Final summary of the goal outcome and deliverables.',
410
- },
411
- },
412
- required: ['summary'],
413
- },
414
- handler: async ({ summary }, toolCtx) => {
415
- const sid = sessionIdOf(toolCtx, 'default');
416
- const snap = engine.completeGoal(summary, sid);
417
- return {
418
- success: true,
419
- completed: true,
420
- summary,
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
- resumeActiveAgent(`🎯 Цель установлена: "${cleanTitle}". Немедленно сформируй план работ (3-7 конкретных шагов) через инструмент goal_set_milestones и начни его выполнение.`, sid);
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
- engine.pause('Агент не продвинулся по плану за последние 2 итерации — требуется внимание оператора', sid);
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 promptMsg = createGoalUserMessage(
654
- 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.',
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
- engine.pause('Ожидание подтверждения действия оператором', sid);
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