@goodandready/dsh-goal 0.1.10 → 0.2.0

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/lib/index.js CHANGED
@@ -43,6 +43,8 @@ export const Config = z.object({
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
45
  showQuickLaunchButton: z.boolean().default(true).description('Show quick launch goal button above composer dock'),
46
+ consecutiveToolFailureLimit: z.number().default(3).description('Auto-pause goal if N consecutive turns encounter tool execution errors (0 to disable)'),
47
+ enableBrowserNotifications: z.boolean().default(true).description('Show desktop notifications on goal completion or failure'),
46
48
  storagePath: z.string().default('').description('Custom filesystem path for persistent state storage'),
47
49
  });
48
50
 
@@ -61,6 +63,8 @@ export function apply(ctx, config = {}) {
61
63
  autoDrive: config?.autoDrive ?? true,
62
64
  enableSound: config?.enableSound ?? true,
63
65
  showQuickLaunchButton: config?.showQuickLaunchButton ?? true,
66
+ consecutiveToolFailureLimit: config?.consecutiveToolFailureLimit ?? 3,
67
+ enableBrowserNotifications: config?.enableBrowserNotifications ?? true,
64
68
  storagePath: config?.storagePath,
65
69
  };
66
70
 
@@ -74,6 +78,7 @@ export function apply(ctx, config = {}) {
74
78
  autoDrive: currentSettings.autoDrive,
75
79
  enableSound: currentSettings.enableSound,
76
80
  showQuickLaunchButton: currentSettings.showQuickLaunchButton,
81
+ consecutiveToolFailureLimit: currentSettings.consecutiveToolFailureLimit,
77
82
  storagePath,
78
83
  });
79
84
 
@@ -222,6 +227,8 @@ export function apply(ctx, config = {}) {
222
227
  autoDrive: typeof live.autoDrive === 'boolean' ? live.autoDrive : currentSettings.autoDrive,
223
228
  enableSound: typeof live.enableSound === 'boolean' ? live.enableSound : currentSettings.enableSound,
224
229
  showQuickLaunchButton: typeof live.showQuickLaunchButton === 'boolean' ? live.showQuickLaunchButton : currentSettings.showQuickLaunchButton,
230
+ consecutiveToolFailureLimit: typeof live.consecutiveToolFailureLimit === 'number' ? live.consecutiveToolFailureLimit : currentSettings.consecutiveToolFailureLimit,
231
+ enableBrowserNotifications: typeof live.enableBrowserNotifications === 'boolean' ? live.enableBrowserNotifications : currentSettings.enableBrowserNotifications,
225
232
  storagePath: typeof live.storagePath === 'string' ? live.storagePath : currentSettings.storagePath,
226
233
  };
227
234
  }
@@ -249,6 +256,7 @@ export function apply(ctx, config = {}) {
249
256
  autoDrive: live.autoDrive,
250
257
  enableSound: live.enableSound,
251
258
  showQuickLaunchButton: live.showQuickLaunchButton,
259
+ consecutiveToolFailureLimit: live.consecutiveToolFailureLimit,
252
260
  });
253
261
  };
254
262
 
@@ -305,322 +313,322 @@ export function apply(ctx, config = {}) {
305
313
  }
306
314
  });
307
315
 
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
-
316
+ // 3. Инжекция контекста цели в системный промпт (systemPrompt)
317
+ ctx.inject(['systemPrompt'], (pctx) => {
318
+ try {
319
+ if (typeof pctx.systemPrompt?.section === 'function') {
320
+ // Clear conflicting core tool:goal prompt section if present
321
+ const globalSections = pctx.systemPrompt.layers?.global?.sections;
322
+ if (globalSections?.data instanceof Map && globalSections.data.has('tool:goal')) {
323
+ globalSections.data.delete('tool:goal');
324
+ }
325
+
326
+ const order = typeof pctx.systemPrompt.getSectionOrder === 'function'
327
+ ? (pctx.systemPrompt.getSectionOrder('TOOL_GOAL') || 2400)
328
+ : 2400;
329
+
330
+ const unregisterSection = pctx.systemPrompt.section({
331
+ name: 'tool:dsh-goal',
332
+ order,
333
+ text: (sessionCtx) => {
334
+ const sid = sessionIdOf(sessionCtx, 'default');
335
+ return engine.getStatePromptInjection(sid);
336
+ },
337
+ });
338
+
339
+ if (typeof unregisterSection === 'function') {
340
+ pctx.effect(() => () => unregisterSection(), 'dsh-goal: system prompt section');
341
+ }
342
+ }
343
+ } catch (err) {
344
+ console.warn('[dsh-goal] SystemPrompt section register skipped:', err.message);
345
+ }
346
+ });
347
+
348
+ // 4. Регистрация инструментов для модели (tools)
349
+ ctx.inject(['tools'], (tctx) => {
350
+ if (!tctx.tools?.register) return;
351
+
352
+ // Helper: Safely replace or register tool in ToolRuntime
353
+ const safeRegister = (definition) => {
354
+ try {
355
+ const name = definition.name;
356
+ const globalTools = tctx.tools.layers?.global?.tools;
357
+ if (globalTools?.data instanceof Map && globalTools.data.has(name)) {
358
+ globalTools.data.delete(name);
359
+ }
360
+ const unregister = tctx.tools.register(definition);
361
+ if (typeof unregister === 'function') {
362
+ tctx.effect(() => () => unregister(), `dsh-goal: tool ${name}`);
363
+ }
364
+ } catch (err) {
365
+ console.warn(`[dsh-goal] Tool ${definition.name} registration skipped:`, err.message);
366
+ }
367
+ };
368
+
369
+ const JSON_OUTPUT = {
370
+ schema: { type: 'object', additionalProperties: true },
371
+ render: (_args, val) => [{ type: 'text', text: JSON.stringify(val) }],
372
+ };
373
+
374
+ function formatGoalValue(snap) {
375
+ if (!snap || !snap.hasActiveGoal) {
376
+ return { goal: null };
377
+ }
378
+ let phase = 'active';
379
+ if (snap.state === GoalState.PAUSED) phase = 'paused';
380
+ else if (snap.state === GoalState.COMPLETED) phase = 'complete';
381
+
382
+ const roundsStarted = Number.isInteger(snap.iterations) ? snap.iterations : 0;
383
+ const maxGoalRounds = Number.isInteger(snap.maxIterations) ? snap.maxIterations : 25;
384
+
385
+ return {
386
+ goal: {
387
+ id: snap.id || 'goal-active',
388
+ revision: 1,
389
+ objective: snap.title || 'Goal',
390
+ phase,
391
+ roundsStarted,
392
+ maxGoalRounds,
393
+ },
394
+ activation: snap.state === GoalState.RUNNING ? 'armed' : 'disarmed',
395
+ };
396
+ }
397
+
398
+ // Совместимый инструмент 1: get_goal
399
+ const handleGetGoal = async (_args, toolCtx) => {
400
+ const sid = sessionIdOf(toolCtx, 'default');
401
+ const snap = engine.getSnapshot(sid);
402
+ return formatGoalValue(snap);
403
+ };
404
+
405
+ safeRegister({
406
+ name: 'get_goal',
407
+ description: 'Read the current same-session goal, including objective, phase, and round limits.',
408
+ parameters: { type: 'object', properties: {} },
409
+ output: JSON_OUTPUT,
410
+ execute: handleGetGoal,
411
+ handler: handleGetGoal,
412
+ });
413
+
414
+ // Совместимый инструмент 2: create_goal
415
+ const handleCreateGoal = async (args, toolCtx) => {
416
+ const sid = sessionIdOf(toolCtx, 'default');
417
+ const maxIterations = Number(args.max_goal_rounds) || 25;
418
+ const snap = engine.startGoal(args.objective, { maxIterations }, sid);
419
+ return formatGoalValue(snap);
420
+ };
421
+
422
+ safeRegister({
423
+ name: 'create_goal',
424
+ description: 'Create one persisted same-session completion goal for long-running autonomous work.',
425
+ parameters: {
426
+ type: 'object',
427
+ properties: {
428
+ objective: {
429
+ type: 'string',
430
+ description: 'The concrete completion objective.',
431
+ },
432
+ max_goal_rounds: {
433
+ type: 'number',
434
+ description: 'Optional positive integer limit on automatic continuation rounds.',
435
+ },
436
+ },
437
+ required: ['objective'],
438
+ },
439
+ output: JSON_OUTPUT,
440
+ execute: handleCreateGoal,
441
+ handler: handleCreateGoal,
442
+ });
443
+
444
+ // Совместимый инструмент 3: update_goal (перехватчик authority checks и роутер в GoalEngine)
445
+ const handleUpdateGoal = async (args, toolCtx) => {
446
+ const sid = sessionIdOf(toolCtx, 'default');
447
+ const action = args.action;
448
+
449
+ if (action === 'complete') {
450
+ const summary = args.blocked_reason || args.objective || 'Goal marked complete';
451
+ const snap = engine.completeGoal(summary, sid);
452
+ if (toolCtx?.deferContext) {
453
+ try {
454
+ toolCtx.deferContext({
455
+ type: 'text',
456
+ text: `<goal_complete>\nObjective: ${JSON.stringify(snap.title || summary)}\nThe goal is marked complete. Summarize what was accomplished for the user.\n</goal_complete>`,
457
+ });
458
+ } catch (_) {}
459
+ }
460
+ return formatGoalValue(snap);
461
+ }
462
+
463
+ if (action === 'pause') {
464
+ const reason = args.blocked_reason || 'Paused by model';
465
+ const snap = engine.pause(reason, sid);
466
+ return formatGoalValue(snap);
467
+ }
468
+
469
+ if (action === 'resume') {
470
+ const snap = engine.resume(sid);
471
+ resumeActiveAgent(undefined, sid);
472
+ return formatGoalValue(snap);
473
+ }
474
+
475
+ if (action === 'edit') {
476
+ const snap = engine.getSnapshot(sid);
477
+ if (args.objective) snap.title = args.objective;
478
+ if (args.max_goal_rounds) snap.maxIterations = Number(args.max_goal_rounds);
479
+ engine.emit(sid, true);
480
+ return formatGoalValue(snap);
481
+ }
482
+
483
+ if (action === 'blocked') {
484
+ const reason = args.blocked_reason || 'Goal blocked';
485
+ const snap = engine.pause('Blocked: ' + reason, sid);
486
+ if (toolCtx?.deferContext) {
487
+ try {
488
+ toolCtx.deferContext({
489
+ type: 'text',
490
+ text: `<goal_blocked>\nObjective: ${JSON.stringify(snap.title || 'Goal')}\nBlocked: ${JSON.stringify(reason)}\nExplain to the user what blocked progress.\n</goal_blocked>`,
491
+ });
492
+ } catch (_) {}
493
+ }
494
+ const res = formatGoalValue(snap);
495
+ if (res.goal) {
496
+ res.goal.phase = 'blocked';
497
+ res.goal.blockedReason = { code: 'model-reported', message: reason };
498
+ }
499
+ return res;
500
+ }
501
+
502
+ return formatGoalValue(engine.getSnapshot(sid));
503
+ };
504
+
505
+ safeRegister({
506
+ name: 'update_goal',
507
+ description: 'Update the active goal: complete, pause, resume, edit, or report blocked.',
508
+ parameters: {
509
+ type: 'object',
510
+ properties: {
511
+ goal_id: { type: 'string', description: 'Exact id returned by get_goal.' },
512
+ revision: { type: 'number', description: 'Exact positive revision returned by get_goal.' },
513
+ action: {
514
+ type: 'string',
515
+ enum: ['edit', 'pause', 'resume', 'complete', 'blocked'],
516
+ description: 'edit | pause | resume | complete | blocked',
517
+ },
518
+ objective: { type: 'string', description: 'Replacement objective; valid with action edit.' },
519
+ max_goal_rounds: { type: 'number', description: 'Replacement cap; valid with action edit.' },
520
+ blocked_reason: { type: 'string', description: 'Concrete blocking condition; required with action blocked.' },
521
+ },
522
+ required: ['action'],
523
+ },
524
+ output: JSON_OUTPUT,
525
+ execute: handleUpdateGoal,
526
+ handler: handleUpdateGoal,
527
+ });
528
+
529
+ // Инструмент 4: Декомпозиция цели на вехи
530
+ const handleSetMilestones = async ({ milestones }, toolCtx) => {
531
+ const sid = sessionIdOf(toolCtx, 'default');
532
+ const snap = engine.getSnapshot(sid);
533
+ if (!snap.hasActiveGoal) {
534
+ return { error: 'No active goal currently set. Start a goal first.' };
535
+ }
536
+ engine.addMilestones(milestones, true, sid);
537
+ return {
538
+ success: true,
539
+ milestones: engine.getSnapshot(sid).milestones,
540
+ };
541
+ };
542
+
543
+ safeRegister({
544
+ name: 'goal_set_milestones',
545
+ description: 'Break down the current active goal into a sequence of concrete milestones/sub-tasks.',
546
+ parameters: {
547
+ type: 'object',
548
+ properties: {
549
+ milestones: {
550
+ type: 'array',
551
+ items: { type: 'string' },
552
+ description: 'List of milestone titles to accomplish.',
553
+ },
554
+ },
555
+ required: ['milestones'],
556
+ },
557
+ output: JSON_OUTPUT,
558
+ execute: handleSetMilestones,
559
+ handler: handleSetMilestones,
560
+ });
561
+
562
+ // Инструмент 5: Обновление статуса вехи
563
+ const handleUpdateProgress = async ({ milestone_id, status, notes }, toolCtx) => {
564
+ const sid = sessionIdOf(toolCtx, 'default');
565
+ const ok = engine.updateMilestone(milestone_id, status, notes, sid);
566
+ if (!ok) {
567
+ return { error: `Milestone ${milestone_id} not found or no active goal.` };
568
+ }
569
+ return {
570
+ success: true,
571
+ snapshot: engine.getSnapshot(sid),
572
+ };
573
+ };
574
+
575
+ safeRegister({
576
+ name: 'goal_update_progress',
577
+ description: 'Update the status of a specific goal milestone and optionally log progress notes.',
578
+ parameters: {
579
+ type: 'object',
580
+ properties: {
581
+ milestone_id: {
582
+ type: 'string',
583
+ description: 'The ID of the milestone (e.g. "m-1", "m-2").',
584
+ },
585
+ status: {
586
+ type: 'string',
587
+ enum: ['pending', 'in_progress', 'completed', 'failed'],
588
+ description: 'New status for this milestone.',
589
+ },
590
+ notes: {
591
+ type: 'string',
592
+ description: 'Brief summary of what was accomplished or why it failed.',
593
+ },
594
+ },
595
+ required: ['milestone_id', 'status'],
596
+ },
597
+ output: JSON_OUTPUT,
598
+ execute: handleUpdateProgress,
599
+ handler: handleUpdateProgress,
600
+ });
601
+
602
+ // Инструмент 6: Успешное завершение цели
603
+ const handleGoalFinish = async ({ summary }, toolCtx) => {
604
+ const sid = sessionIdOf(toolCtx, 'default');
605
+ const snap = engine.completeGoal(summary, sid);
606
+ return {
607
+ success: true,
608
+ completed: true,
609
+ summary,
610
+ };
611
+ };
612
+
613
+ safeRegister({
614
+ name: 'goal_finish',
615
+ description: 'Conclude the active goal successfully with a final summary and achievements.',
616
+ parameters: {
617
+ type: 'object',
618
+ properties: {
619
+ summary: {
620
+ type: 'string',
621
+ description: 'Final summary of the goal outcome and deliverables.',
622
+ },
623
+ },
624
+ required: ['summary'],
625
+ },
626
+ output: JSON_OUTPUT,
627
+ execute: handleGoalFinish,
628
+ handler: handleGoalFinish,
629
+ });
630
+ });
631
+
624
632
  // 5. Регистрация HTTP REST API маршрутов и SSE событий
625
633
  ctx.effect(() => {
626
634
  if (!ctx.webServer?.register) return () => {};
@@ -748,6 +756,20 @@ export function apply(ctx, config = {}) {
748
756
  resumeActiveAgent(startPrompt, sid);
749
757
  break;
750
758
  }
759
+ case 'nudge': {
760
+ const text = (typeof data.text === 'string' ? data.text : (data.notes || data.nudge || '')).trim();
761
+ if (!text) {
762
+ res.statusCode = 400;
763
+ return res.end(JSON.stringify({ error: 'Nudge text cannot be empty' }));
764
+ }
765
+ result = engine.nudge(text, sid);
766
+ if (data.resume) {
767
+ engine.resume(sid);
768
+ const prompt = engine.getStatePromptInjection(sid);
769
+ resumeActiveAgent(prompt, sid);
770
+ }
771
+ break;
772
+ }
751
773
  case 'pause':
752
774
  result = engine.pause(reason || 'Пауза по кнопке интерфейса', sid);
753
775
  stopRunningAgents(sid);
@@ -834,6 +856,29 @@ export function apply(ctx, config = {}) {
834
856
 
835
857
  const snap = engine.getSnapshot(sid);
836
858
  if (snap.hasActiveGoal && snap.state === GoalState.RUNNING) {
859
+ // Tool-Failure Breaker: отслеживание повторяющихся ошибок инструментов
860
+ const steps = turn?.steps || turn?.turn?.steps || [];
861
+ const hasStepError = Array.isArray(steps) && steps.some((s) => {
862
+ return s?.status === 'error' || s?.error || s?.toolResult?.isError;
863
+ });
864
+ const hasTurnError = Boolean(turn?.error || turn?.reason?.kind === 'error');
865
+ const isToolFailure = hasStepError || hasTurnError;
866
+
867
+ if (isToolFailure) {
868
+ const failCount = engine.incrementToolFailureCount(sid);
869
+ const limit = liveConfig.consecutiveToolFailureLimit;
870
+ if (limit > 0 && failCount >= limit) {
871
+ const lang = snap.lang || (snap.title ? detectLanguage(snap.title) : 'en');
872
+ const pauseReason = lang === 'ru'
873
+ ? `Повторяющаяся ошибка инструментов (${failCount} подряд) — цель приостановлена для защиты от зацикливания`
874
+ : `Repeated tool failure (${failCount} consecutive) — goal paused to prevent token burn`;
875
+ engine.pause(pauseReason, sid);
876
+ return;
877
+ }
878
+ } else {
879
+ engine.resetToolFailureCount(sid);
880
+ }
881
+
837
882
  // Проверяем причину завершения хода
838
883
  if (turn?.reason?.kind === 'aborted' || turn?.reason?.kind === 'error' || turn?.error) {
839
884
  return;
@@ -858,9 +903,18 @@ export function apply(ctx, config = {}) {
858
903
  const currentSnap = engine.getSnapshot(sid);
859
904
  if (currentSnap.hasActiveGoal && currentSnap.state === GoalState.RUNNING) {
860
905
  const currentLang = currentSnap.lang || (currentSnap.title ? detectLanguage(currentSnap.title) : 'en');
861
- const promptText = currentLang === 'ru'
906
+ let promptText = currentLang === 'ru'
862
907
  ? 'Продолжай автономное выполнение цели согласно плану работ. Отмечай каждый выполненный шаг через goal_update_progress, а по завершении всех задач вызови goal_finish.'
863
908
  : '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.';
909
+
910
+ if (currentSnap.pendingNudge) {
911
+ const userNudge = engine.consumePendingNudge(sid);
912
+ if (userNudge) {
913
+ promptText = (currentLang === 'ru'
914
+ ? `🚨 СРОЧНОЕ УТОЧНЕНИЕ / НАПРАВЛЕНИЕ ОТ ПОЛЬЗОВАТЕЛЯ:\n"${userNudge}"\nСкорректируй выполнение с учётом этого замечания.\n\n`
915
+ : `🚨 URGENT USER CLARIFICATION / STEERING:\n"${userNudge}"\nAdjust execution adhering to this feedback.\n\n`) + promptText;
916
+ }
917
+ }
864
918
  const promptMsg = createGoalUserMessage(promptText);
865
919
  let target = sessionAgents.get(sid);
866
920
  if (!target && lastActiveAgent && typeof lastActiveAgent.followup === 'function') {