incremnt 0.8.4 → 0.8.6
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/package.json +1 -1
- package/src/ask-answer-verifier.js +25 -0
- package/src/ask-coach.js +254 -38
- package/src/coach-prompt-assembly.js +34 -0
- package/src/coach-prompt-layers.js +62 -0
- package/src/contract.js +12 -1
- package/src/format.js +33 -0
- package/src/openrouter.js +17 -79
- package/src/program-schedule-action.js +107 -0
- package/src/prompt-security.js +4 -0
- package/src/queries.js +230 -31
- package/src/remote.js +7 -0
- package/src/sync-service.js +31 -7
package/package.json
CHANGED
|
@@ -786,6 +786,30 @@ function checkObservationFollowupVoice(answer, route) {
|
|
|
786
786
|
}];
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
+
const ASK_REPORT_VOICE_PATTERNS = [
|
|
790
|
+
{ label: 'What I see', pattern: /\bWhat I see\b/i },
|
|
791
|
+
{ label: 'What that means', pattern: /\bWhat that means\b/i },
|
|
792
|
+
{ label: 'Recent pattern', pattern: /\bRecent pattern\b/i },
|
|
793
|
+
{ label: 'Facts:', pattern: /^\s*Facts:/im },
|
|
794
|
+
{ label: 'Interpretation:', pattern: /^\s*Interpretation(?:\s*\[[^\]]+\])?:/im },
|
|
795
|
+
{ label: 'Recommendation:', pattern: /^\s*Recommendation(?:\s*\[[^\]]+\])?:/im },
|
|
796
|
+
{ label: 'coach observation', pattern: /\bcoach observations?\b/i },
|
|
797
|
+
{ label: 'planning check', pattern: /\bplanning check\b/i }
|
|
798
|
+
];
|
|
799
|
+
|
|
800
|
+
function checkAskReportVoice(answer, route) {
|
|
801
|
+
if (route === 'coach_observation_followup') return [];
|
|
802
|
+
const hits = uniqueStrings(ASK_REPORT_VOICE_PATTERNS
|
|
803
|
+
.filter(({ pattern }) => pattern.test(answer))
|
|
804
|
+
.map(({ label }) => label));
|
|
805
|
+
if (hits.length === 0) return [];
|
|
806
|
+
return [{
|
|
807
|
+
key: 'ask_report_voice',
|
|
808
|
+
severity: 'advisory',
|
|
809
|
+
reason: `Ask answer used report/artifact phrasing instead of coach voice: ${hits.join(', ')}.`
|
|
810
|
+
}];
|
|
811
|
+
}
|
|
812
|
+
|
|
789
813
|
function checkExpansiveCompleteness(answer, snapshot, routingMetadata, { executeTool = executeCoachReadTool } = {}) {
|
|
790
814
|
const responseProfile = routingMetadata?.responseProfile ?? routingMetadata?.intent?.responseProfile;
|
|
791
815
|
if (responseProfile !== 'expansive') return [];
|
|
@@ -916,6 +940,7 @@ export function verifyAskAnswer({
|
|
|
916
940
|
|
|
917
941
|
const failures = [
|
|
918
942
|
...voiceFailures,
|
|
943
|
+
...checkAskReportVoice(normalized, route),
|
|
919
944
|
...checkSnapshotClaims(normalized, snapshot, routingMetadata, { today, exclude }),
|
|
920
945
|
...checkToolProvenance(normalized, snapshot, routingMetadata, { today, exclude, strictMentionProvenance, executeTool }),
|
|
921
946
|
...checkSessionObservationProvenance(normalized, routingMetadata),
|
package/src/ask-coach.js
CHANGED
|
@@ -278,7 +278,53 @@ function inferredSinceDate(question, today = new Date()) {
|
|
|
278
278
|
return candidate > dateOnlyString(today) ? `${year - 1}-${monthPart}-01` : candidate;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
function
|
|
281
|
+
function deloadScheduleContextFromText(text) {
|
|
282
|
+
const raw = String(text ?? '');
|
|
283
|
+
if (!/\bd(?:e)?load\b/i.test(raw)) return null;
|
|
284
|
+
const match = raw.match(/\b(this|next|coming)\s+(?:training\s+)?week\b/i);
|
|
285
|
+
if (!match) return null;
|
|
286
|
+
return {
|
|
287
|
+
week: match[1].toLowerCase() === 'this' ? 'this' : 'next'
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function latestDeloadScheduleContext(history = []) {
|
|
292
|
+
const messages = (Array.isArray(history) ? history : []).slice(-2);
|
|
293
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
294
|
+
const message = messages[index];
|
|
295
|
+
if (!['user', 'assistant'].includes(message?.role) || typeof message.content !== 'string') continue;
|
|
296
|
+
const context = deloadScheduleContextFromText(message.content);
|
|
297
|
+
if (context) return context;
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function isDeloadScheduleConfirmation(question) {
|
|
303
|
+
const text = String(question ?? '').trim().toLowerCase();
|
|
304
|
+
if (!text) return false;
|
|
305
|
+
if (/\b(no|don'?t|do not|cancel|undo|stop|never mind|nevermind)\b/i.test(text)) return false;
|
|
306
|
+
const wordCount = text.split(/\s+/).filter(Boolean).length;
|
|
307
|
+
if (wordCount > 8) return false;
|
|
308
|
+
return /^(yes|yeah|yep|ok|okay|sure)\b/i.test(text) ||
|
|
309
|
+
/\b(do it|schedule it|set it|make it happen|schedule that|set that)\b/i.test(text);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hasProgramHistoryLanguage(question, { previousRoute = null, isFollowUp = false } = {}) {
|
|
313
|
+
const text = String(question ?? '').toLowerCase();
|
|
314
|
+
const hasProgramLanguage = /\b(program|plan|routine|split)\b/i.test(text);
|
|
315
|
+
if (/\bwhat changed\b[\s\S]{0,80}\b(program|plan|routine|split)\b/i.test(text) ||
|
|
316
|
+
/\b(program|plan|routine|split)\b[\s\S]{0,80}\bwhat changed\b/i.test(text) ||
|
|
317
|
+
/\bwhy\b[\s\S]{0,80}\b(program|plan|routine|split)\b[\s\S]{0,80}\bchang/i.test(text)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const restoreLanguage = /\b(undo that|undo it|revert that|revert it|restore that|restore it|change it back|previous version|last change)\b/i.test(text);
|
|
322
|
+
if (!restoreLanguage) return false;
|
|
323
|
+
if (hasProgramLanguage) return true;
|
|
324
|
+
return isFollowUp && ['program_history', 'program_progress', 'program_schedule_action'].includes(previousRoute);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function routeAskQuestion(snapshot, question, { today = new Date(), previousRoute = null, isFollowUp = false } = {}) {
|
|
282
328
|
const normalizedQuestion = normalizeExerciseName(question ?? '');
|
|
283
329
|
const namedExercises = namedExercisesFromQuestion(snapshot, question);
|
|
284
330
|
const sessionReference = referencedSessionFromQuestion(snapshot, question);
|
|
@@ -287,6 +333,11 @@ function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
|
|
|
287
333
|
const progressLanguage = /\b(progress|progressing|improve|improved|improvement|better|stronger|moved|moving|stalled|flat|since)\b/i.test(question ?? '');
|
|
288
334
|
const profileLanguage = /\b(know about me|about me as a lifter|me as a lifter|lifter profile|training profile)\b/i.test(question ?? '');
|
|
289
335
|
const programLanguage = /\b(program|plan|split|routine|full gym|ppl|upper lower)\b/i.test(question ?? '');
|
|
336
|
+
const deloadWord = 'd(?:e)?load';
|
|
337
|
+
const deloadScheduleContext = deloadScheduleContextFromText(question);
|
|
338
|
+
const deloadScheduleVerb = '(?:make|schedule|set|program|turn|change|adjust)';
|
|
339
|
+
const deloadScheduleLanguage = new RegExp(`\\b${deloadScheduleVerb}\\b[\\s\\S]{0,120}\\b(?:this|next|coming)\\s+(?:training\\s+)?week\\b[\\s\\S]{0,120}\\b${deloadWord}\\b`, 'i').test(question ?? '') ||
|
|
340
|
+
new RegExp(`\\b${deloadScheduleVerb}\\b[\\s\\S]{0,120}\\b${deloadWord}\\b[\\s\\S]{0,120}\\b(?:this|next|coming)\\s+(?:training\\s+)?week\\b`, 'i').test(question ?? '');
|
|
290
341
|
const windowDays = inferredRelativeWindowDays(question);
|
|
291
342
|
const reviewVerb = /\b(doing|going|look(?:s|ing|ed)?|progress\w*|train(?:s|ing)?|workouts?|fitness)\b/i.test(question ?? '');
|
|
292
343
|
const explicitReview = /\b(on (?:the )?(?:right )?track|right track|making (?:good )?progress|overall progress|review (?:my|the)\b|check (?:out\s+)?my\b|how(?:'?s| is| has| have)\s+(?:my|the)\s+(?:training|progress|fitness))\b/i.test(question ?? '');
|
|
@@ -303,6 +354,13 @@ function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
|
|
|
303
354
|
/\b(pr|prs|record|records|max|maxes|1rm|one rep max|one-rep max|strongest)\b/i.test(question ?? '') ||
|
|
304
355
|
/\b(next|tomorrow|up next|coming session|do next|what should i do)\b/i.test(question ?? '');
|
|
305
356
|
|
|
357
|
+
if (deloadScheduleLanguage) {
|
|
358
|
+
return { route: 'program_schedule_action', namedExercises, deloadScheduleContext };
|
|
359
|
+
}
|
|
360
|
+
if (hasProgramHistoryLanguage(question, { previousRoute, isFollowUp })) {
|
|
361
|
+
return { route: 'program_history', namedExercises, deloadScheduleContext };
|
|
362
|
+
}
|
|
363
|
+
|
|
306
364
|
// Broad "how am I doing / on the right track / last N weeks" reviews fan out to
|
|
307
365
|
// sessions + volume + records + body weight rather than a single narrow route.
|
|
308
366
|
// Requires an explicit review cue, or a relative window paired with a review verb.
|
|
@@ -311,16 +369,16 @@ function routeAskQuestion(snapshot, question, { today = new Date() } = {}) {
|
|
|
311
369
|
(!narrowSingleTopic || compositeReviewCue) &&
|
|
312
370
|
broadReviewIntent
|
|
313
371
|
) {
|
|
314
|
-
return { route: 'progress_review', namedExercises, since };
|
|
372
|
+
return { route: 'progress_review', namedExercises, since, deloadScheduleContext };
|
|
315
373
|
}
|
|
316
374
|
if (/\b(body ?weight|weigh|weight trends?|current weight|my weight)\b/i.test(question ?? '')) {
|
|
317
|
-
return { route: 'body_weight', namedExercises };
|
|
375
|
+
return { route: 'body_weight', namedExercises, deloadScheduleContext };
|
|
318
376
|
}
|
|
319
377
|
if (profileLanguage) {
|
|
320
|
-
return { route: 'training_profile', namedExercises, since };
|
|
378
|
+
return { route: 'training_profile', namedExercises, since, deloadScheduleContext };
|
|
321
379
|
}
|
|
322
380
|
if (programLanguage && progressLanguage) {
|
|
323
|
-
return { route: 'program_progress', namedExercises, since };
|
|
381
|
+
return { route: 'program_progress', namedExercises, since, deloadScheduleContext };
|
|
324
382
|
}
|
|
325
383
|
if (since && progressLanguage) {
|
|
326
384
|
return { route: 'exercise_progress_summary', namedExercises, since };
|
|
@@ -374,6 +432,9 @@ function isTerseFollowUpQuestion(question) {
|
|
|
374
432
|
|
|
375
433
|
function requestedActionForRoute(route, question, { isFollowUp = false, carriedPreviousTopic = false } = {}) {
|
|
376
434
|
const text = String(question ?? '').toLowerCase();
|
|
435
|
+
if (route === 'program_schedule_action') return 'schedule_program_action';
|
|
436
|
+
if (route === 'program_history' && /\b(undo|revert|restore|change it back|previous version)\b/.test(text)) return 'explain_restore_status';
|
|
437
|
+
if (route === 'program_history') return 'explain_program_history';
|
|
377
438
|
if (/\b(why|how come)\b/.test(text)) return 'explain_cause';
|
|
378
439
|
if (/\b(build|create|make|generate|draft|rewrite|revise|update)\b/.test(text)) return 'draft_plan';
|
|
379
440
|
if (/\badjust\b/.test(text) && /\b(program|plan|split|routine)\b/.test(text)) return 'draft_plan';
|
|
@@ -388,6 +449,7 @@ function requestedActionForRoute(route, question, { isFollowUp = false, carriedP
|
|
|
388
449
|
exercise_progress: 'explain_exercise',
|
|
389
450
|
volume: 'explain_volume',
|
|
390
451
|
next_session: 'recommend_next_session',
|
|
452
|
+
program_history: 'explain_program_history',
|
|
391
453
|
recovery: 'explain_recovery',
|
|
392
454
|
score: 'explain_score',
|
|
393
455
|
records: 'summarize_records',
|
|
@@ -406,6 +468,7 @@ export const ASK_RESPONSE_PROFILES = Object.freeze({
|
|
|
406
468
|
});
|
|
407
469
|
|
|
408
470
|
function responseProfileForAskIntent(route, requestedAction, question) {
|
|
471
|
+
if (route === 'program_schedule_action' || requestedAction === 'schedule_program_action') return ASK_RESPONSE_PROFILES.structured;
|
|
409
472
|
if (route === 'program_design' || requestedAction === 'draft_plan') return ASK_RESPONSE_PROFILES.structured;
|
|
410
473
|
const text = String(question ?? '').toLowerCase();
|
|
411
474
|
if (
|
|
@@ -445,23 +508,36 @@ function ambiguityFlagsForIntent({ route, namedExercises, question, isFollowUp,
|
|
|
445
508
|
return flags;
|
|
446
509
|
}
|
|
447
510
|
|
|
448
|
-
function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = null, today = new Date() } = {}) {
|
|
449
|
-
const current = routeAskQuestion(snapshot, question, { today });
|
|
511
|
+
function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = null, previousDeloadScheduleContext = null, today = new Date() } = {}) {
|
|
450
512
|
const previous = previousIntent
|
|
451
513
|
? {
|
|
452
514
|
route: previousIntent.route,
|
|
453
515
|
since: previousIntent.timeframe?.since ?? null,
|
|
454
516
|
namedExercises: previousIntent.entities?.exercises ?? [],
|
|
455
517
|
sessionLabel: previousIntent.entities?.sessionLabel ?? null,
|
|
456
|
-
sessionReference: previousIntent.entities?.sessionReference ?? null
|
|
518
|
+
sessionReference: previousIntent.entities?.sessionReference ?? null,
|
|
519
|
+
deloadScheduleContext: previousIntent.deloadScheduleContext ?? null
|
|
457
520
|
}
|
|
458
521
|
: null;
|
|
459
|
-
const isFollowUp = Boolean(previous && isTerseFollowUpQuestion(question));
|
|
522
|
+
const isFollowUp = Boolean((previous || previousDeloadScheduleContext) && isTerseFollowUpQuestion(question));
|
|
523
|
+
const current = routeAskQuestion(snapshot, question, {
|
|
524
|
+
today,
|
|
525
|
+
previousRoute: previous?.route ?? null,
|
|
526
|
+
isFollowUp
|
|
527
|
+
});
|
|
528
|
+
const currentDeloadScheduleContext = current.deloadScheduleContext ?? deloadScheduleContextFromText(question);
|
|
529
|
+
const carriedDeloadScheduleContext = currentDeloadScheduleContext ?? previousDeloadScheduleContext ?? previous?.deloadScheduleContext ?? null;
|
|
530
|
+
const isDeloadScheduleFollowUp = current.route !== 'program_schedule_action' &&
|
|
531
|
+
Boolean(carriedDeloadScheduleContext) &&
|
|
532
|
+
isDeloadScheduleConfirmation(question);
|
|
460
533
|
let route = current.route;
|
|
461
534
|
let since = current.since ?? null;
|
|
462
535
|
let carriedPreviousTopic = false;
|
|
463
536
|
|
|
464
|
-
if (
|
|
537
|
+
if (isDeloadScheduleFollowUp) {
|
|
538
|
+
route = 'program_schedule_action';
|
|
539
|
+
since = null;
|
|
540
|
+
} else if (
|
|
465
541
|
isFollowUp &&
|
|
466
542
|
previous?.route &&
|
|
467
543
|
current.namedExercises.length > 0 &&
|
|
@@ -508,6 +584,9 @@ function classifyAskIntentWithPrevious(snapshot, question, { previousIntent = nu
|
|
|
508
584
|
responseProfile,
|
|
509
585
|
isFollowUp,
|
|
510
586
|
previousRoute: previous?.route ?? null,
|
|
587
|
+
deloadScheduleContext: route === 'program_schedule_action'
|
|
588
|
+
? carriedDeloadScheduleContext
|
|
589
|
+
: currentDeloadScheduleContext,
|
|
511
590
|
ambiguityFlags: ambiguityFlagsForIntent({
|
|
512
591
|
route,
|
|
513
592
|
namedExercises,
|
|
@@ -523,7 +602,11 @@ export function classifyAskIntent(snapshot, question, { history = [], today = ne
|
|
|
523
602
|
for (const previousQuestion of historyUserQuestions(history)) {
|
|
524
603
|
previousIntent = classifyAskIntentWithPrevious(snapshot, previousQuestion, { previousIntent, today });
|
|
525
604
|
}
|
|
526
|
-
return classifyAskIntentWithPrevious(snapshot, question, {
|
|
605
|
+
return classifyAskIntentWithPrevious(snapshot, question, {
|
|
606
|
+
previousIntent,
|
|
607
|
+
previousDeloadScheduleContext: latestDeloadScheduleContext(history),
|
|
608
|
+
today
|
|
609
|
+
});
|
|
527
610
|
}
|
|
528
611
|
|
|
529
612
|
function pushAskContextHeader(lines, snapshot, today = new Date()) {
|
|
@@ -542,6 +625,7 @@ const ASK_FACT_KIND_BY_ROUTE = Object.freeze({
|
|
|
542
625
|
exercise_progress: ['goal_signal', 'injury', 'constraint', 'preference', 'tone'],
|
|
543
626
|
exercise_progress_summary: ['goal_signal', 'injury', 'constraint', 'preference', 'tone'],
|
|
544
627
|
program_progress: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
|
|
628
|
+
program_schedule_action: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
|
|
545
629
|
training_profile: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
|
|
546
630
|
program_design: ['goal_signal', 'preference', 'constraint', 'injury', 'tone'],
|
|
547
631
|
next_session: ['constraint', 'injury', 'preference', 'goal_signal', 'tone'],
|
|
@@ -650,6 +734,8 @@ const ASK_ROUTE_REQUIRED_TOOLS = Object.freeze({
|
|
|
650
734
|
exercise_progress: ['get_exercise_history'],
|
|
651
735
|
exercise_progress_summary: ['get_exercise_progress_summary'],
|
|
652
736
|
program_progress: ['get_program_progress', 'get_exercise_progress_summary', 'get_cycle_progression_summary'],
|
|
737
|
+
program_history: ['get_program_change_history'],
|
|
738
|
+
program_schedule_action: ['get_program_progress', 'get_recent_sessions', 'get_readiness_snapshot'],
|
|
653
739
|
training_profile: ['get_training_profile'],
|
|
654
740
|
records: ['get_records'],
|
|
655
741
|
recent_session: ['get_recent_sessions'],
|
|
@@ -1123,6 +1209,83 @@ function buildNextSessionAskContext(snapshot, { exclude = new Set(), today = new
|
|
|
1123
1209
|
return { context: lines.join('\n'), sections, tools: [nextSession], provenance: [coachToolProvenance('next_session_plan', nextSession)] };
|
|
1124
1210
|
}
|
|
1125
1211
|
|
|
1212
|
+
function mondayWeekStartDateOnly(today = new Date(), weekOffset = 0) {
|
|
1213
|
+
const base = new Date(`${dateOnlyString(today)}T00:00:00.000Z`);
|
|
1214
|
+
const day = base.getUTCDay();
|
|
1215
|
+
const daysSinceMonday = (day + 6) % 7;
|
|
1216
|
+
base.setUTCDate(base.getUTCDate() - daysSinceMonday + weekOffset * 7);
|
|
1217
|
+
return base.toISOString().slice(0, 10);
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function deloadScheduleStartDate(question, today = new Date(), scheduleContext = null) {
|
|
1221
|
+
if (/\bthis\s+(?:training\s+)?week\b/i.test(question ?? '')) {
|
|
1222
|
+
return mondayWeekStartDateOnly(today, 0);
|
|
1223
|
+
}
|
|
1224
|
+
if (/\b(?:next|coming)\s+(?:training\s+)?week\b/i.test(question ?? '')) {
|
|
1225
|
+
return mondayWeekStartDateOnly(today, 1);
|
|
1226
|
+
}
|
|
1227
|
+
return scheduleContext?.week === 'this'
|
|
1228
|
+
? mondayWeekStartDateOnly(today, 0)
|
|
1229
|
+
: mondayWeekStartDateOnly(today, 1);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function appendProgramScheduleActionRequest(lines, { startDate, hasActiveProgram }) {
|
|
1233
|
+
lines.push('');
|
|
1234
|
+
lines.push('Program schedule action request:');
|
|
1235
|
+
if (!hasActiveProgram) {
|
|
1236
|
+
lines.push(' No active program is available. Do not append a <program_schedule_action> block.');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
lines.push(' The user wants to schedule a one-week whole-program deload. This is not a new program draft and not an exercise-specific plan changeset.');
|
|
1240
|
+
lines.push(' If the request is clear, keep prose to 1-2 short sentences and append exactly one trailing <program_schedule_action>{JSON}</program_schedule_action>.');
|
|
1241
|
+
lines.push(` Use this exact JSON shape: {"action":"schedule_deload_week","startDate":"${startDate}","durationWeeks":1,"rationale":"..."}.`);
|
|
1242
|
+
lines.push(' The app will compute the weight changes when the week starts. Do not include weights, reps, set counts, deltas, targets, or any extra JSON keys.');
|
|
1243
|
+
lines.push(' Do not append a <program_draft> or <plan_changeset> block.');
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function buildProgramScheduleActionAskContext(snapshot, question, { exclude = new Set(), today = new Date(), scheduleContext = null } = {}) {
|
|
1247
|
+
const lines = [];
|
|
1248
|
+
const programProgress = executeCoachReadTool(snapshot, 'get_program_progress', { since: null, today, limitExercises: 10 });
|
|
1249
|
+
const recentSessions = executeCoachReadTool(snapshot, 'get_recent_sessions', { limit: 5, today });
|
|
1250
|
+
const readiness = executeCoachReadTool(snapshot, 'get_readiness_snapshot', { today });
|
|
1251
|
+
const program = activeProgram(snapshot);
|
|
1252
|
+
const startDate = deloadScheduleStartDate(question, today, scheduleContext);
|
|
1253
|
+
|
|
1254
|
+
pushAskContextHeader(lines, snapshot, today);
|
|
1255
|
+
lines.push('');
|
|
1256
|
+
lines.push(`Program schedule target: ${program?.name ?? 'No active program'}.`);
|
|
1257
|
+
const trainingLoad = programProgress.facts?.trainingLoad;
|
|
1258
|
+
if (trainingLoad) {
|
|
1259
|
+
const readinessBand = trainingLoad.readiness?.readinessBand ? `, readiness ${trainingLoad.readiness.readinessBand}` : '';
|
|
1260
|
+
lines.push(`Training-load signal: status ${trainingLoad.status ?? 'unknown'}${readinessBand}.`);
|
|
1261
|
+
}
|
|
1262
|
+
if (readiness.facts?.readinessBand) {
|
|
1263
|
+
lines.push(`Readiness signal: ${readiness.facts.readinessBand}.`);
|
|
1264
|
+
}
|
|
1265
|
+
appendActiveProgramScheduleContext(lines, snapshot);
|
|
1266
|
+
if (recentSessions.rows?.length > 0) {
|
|
1267
|
+
lines.push('');
|
|
1268
|
+
lines.push('Recent sessions:');
|
|
1269
|
+
for (const row of recentSessions.rows.slice(0, 5)) {
|
|
1270
|
+
const title = row.title ?? row.dayName ?? row.programDayTitle ?? 'Workout';
|
|
1271
|
+
lines.push(` ${row.date ?? 'unknown date'} - ${title}`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
appendProgramScheduleActionRequest(lines, { startDate, hasActiveProgram: Boolean(program) });
|
|
1275
|
+
appendExcludeNote(lines, exclude);
|
|
1276
|
+
return {
|
|
1277
|
+
context: lines.join('\n'),
|
|
1278
|
+
sections: ['header', 'program_schedule_action', 'current_program_schedule', 'recent_sessions'],
|
|
1279
|
+
programScheduleActionStartDate: startDate,
|
|
1280
|
+
tools: [programProgress, recentSessions, readiness],
|
|
1281
|
+
provenance: [
|
|
1282
|
+
coachToolProvenance('program_schedule_program_progress', programProgress),
|
|
1283
|
+
coachToolProvenance('program_schedule_recent_sessions', recentSessions),
|
|
1284
|
+
coachToolProvenance('program_schedule_readiness', readiness)
|
|
1285
|
+
]
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1126
1289
|
function buildExerciseProgressAskContext(snapshot, namedExercises, { exclude = new Set(), today = new Date() } = {}) {
|
|
1127
1290
|
const lines = [];
|
|
1128
1291
|
const exerciseHistoryTool = executeCoachReadTool(snapshot, 'get_exercise_history', { exercises: namedExercises, limit: 6, today });
|
|
@@ -1286,6 +1449,37 @@ function buildProgramProgressAskContext(snapshot, { exclude = new Set(), since =
|
|
|
1286
1449
|
};
|
|
1287
1450
|
}
|
|
1288
1451
|
|
|
1452
|
+
function buildProgramHistoryAskContext(snapshot, { exclude = new Set(), today = new Date() } = {}) {
|
|
1453
|
+
const lines = [];
|
|
1454
|
+
const history = executeCoachReadTool(snapshot, 'get_program_change_history', { limit: 20 });
|
|
1455
|
+
pushAskContextHeader(lines, snapshot, today);
|
|
1456
|
+
lines.push('');
|
|
1457
|
+
lines.push(`Program change history: ${history.facts?.programName ?? 'No active program'}.`);
|
|
1458
|
+
if (history.rows.length === 0) {
|
|
1459
|
+
lines.push(' No durable program change records are available yet.');
|
|
1460
|
+
} else {
|
|
1461
|
+
for (const change of history.rows.slice(0, 10)) {
|
|
1462
|
+
const when = change.createdAt ? String(change.createdAt).slice(0, 10) : 'unknown date';
|
|
1463
|
+
const source = change.source ? `, source ${change.source}` : '';
|
|
1464
|
+
const status = change.status ? `, status ${change.status}` : '';
|
|
1465
|
+
lines.push(` ${when}: ${change.summary ?? change.kind ?? 'Program changed'} (${change.kind ?? 'unknown'}${source}${status}, id ${change.id ?? 'unknown'}).`);
|
|
1466
|
+
const exercises = change.affectedExercises?.length ? `exercises ${change.affectedExercises.slice(0, 4).join(', ')}` : null;
|
|
1467
|
+
const fields = change.affectedFields?.length ? `fields ${change.affectedFields.slice(0, 5).join(', ')}` : null;
|
|
1468
|
+
if (exercises || fields) lines.push(` Affected: ${[exercises, fields].filter(Boolean).join('; ')}.`);
|
|
1469
|
+
if (change.rationale) lines.push(` Rationale: ${change.rationale}`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
lines.push('');
|
|
1473
|
+
lines.push('Restore status: automatic restore is not available in this slice. You may identify the likely change to restore, but do not claim the app restored or changed the program.');
|
|
1474
|
+
appendExcludeNote(lines, exclude);
|
|
1475
|
+
return {
|
|
1476
|
+
context: lines.join('\n'),
|
|
1477
|
+
sections: ['header', 'program_change_history'],
|
|
1478
|
+
tools: [history],
|
|
1479
|
+
provenance: [coachToolProvenance('program_change_history', history)]
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1289
1483
|
function buildTrainingProfileAskContext(snapshot, { exclude = new Set(), since = null, today = new Date() } = {}) {
|
|
1290
1484
|
const lines = [];
|
|
1291
1485
|
const trainingProfile = executeCoachReadTool(snapshot, 'get_training_profile', { since, today });
|
|
@@ -2037,10 +2231,13 @@ function confidenceBand(intentConfidence, missingDataFlags = []) {
|
|
|
2037
2231
|
return 'high';
|
|
2038
2232
|
}
|
|
2039
2233
|
|
|
2040
|
-
function recommendedActionsForAsk(route, requestedAction, programDraft) {
|
|
2234
|
+
function recommendedActionsForAsk(route, requestedAction, programDraft, programScheduleAction = null) {
|
|
2041
2235
|
if (programDraft) {
|
|
2042
2236
|
return [{ id: 'review-program-draft', label: 'Review drafted plan', kind: 'program_draft' }];
|
|
2043
2237
|
}
|
|
2238
|
+
if (programScheduleAction) {
|
|
2239
|
+
return [{ id: 'review-program-schedule-action', label: 'Review scheduled deload', kind: 'program_schedule_action' }];
|
|
2240
|
+
}
|
|
2044
2241
|
if (requestedAction === 'draft_plan') {
|
|
2045
2242
|
return [{ id: 'ask-for-plan-draft', label: 'Ask for a plan draft', kind: 'follow_up' }];
|
|
2046
2243
|
}
|
|
@@ -2124,6 +2321,7 @@ function followUpSuggestionsForAsk(route, intent, { question = '', missingDataFl
|
|
|
2124
2321
|
body_weight: bodyWeightFollowUpCandidates(missingDataFlags),
|
|
2125
2322
|
score: ['What is pulling my score down?', 'What should I focus on this week?'],
|
|
2126
2323
|
program_progress: ['Pull this block summary.', 'Break down a specific lift.', 'What is the next decision?'],
|
|
2324
|
+
program_history: ['What changed most recently?', 'Why did that change happen?', 'Can that be restored later?'],
|
|
2127
2325
|
program_design: ['Make this plan more conservative.', 'Explain the main changes.']
|
|
2128
2326
|
};
|
|
2129
2327
|
let candidates;
|
|
@@ -2178,7 +2376,7 @@ export function sanitizeAskAnswerVerificationReceipt(value) {
|
|
|
2178
2376
|
return Object.keys(result).length > 0 ? result : null;
|
|
2179
2377
|
}
|
|
2180
2378
|
|
|
2181
|
-
export function buildAskStructuredResponse(answer, routingMetadata = {}, { programDraft = null, planChangeset = null, question = '' } = {}) {
|
|
2379
|
+
export function buildAskStructuredResponse(answer, routingMetadata = {}, { programDraft = null, planChangeset = null, programScheduleAction = null, question = '' } = {}) {
|
|
2182
2380
|
const contextBundle = routingMetadata.contextBundle ?? {};
|
|
2183
2381
|
const intent = routingMetadata.intent ?? contextBundle.intent ?? {};
|
|
2184
2382
|
const answerVerification = sanitizeAskAnswerVerificationReceipt(routingMetadata.answerVerification);
|
|
@@ -2192,12 +2390,13 @@ export function buildAskStructuredResponse(answer, routingMetadata = {}, { progr
|
|
|
2192
2390
|
answer,
|
|
2193
2391
|
confidence,
|
|
2194
2392
|
evidenceUsed: contextBundle.evidenceUsed ?? evidenceUsedFromProvenance(routingMetadata.provenance ?? [], routingMetadata.tools ?? []),
|
|
2195
|
-
recommendedActions: recommendedActionsForAsk(intent.route ?? routingMetadata.route, intent.requestedAction, programDraft),
|
|
2393
|
+
recommendedActions: recommendedActionsForAsk(intent.route ?? routingMetadata.route, intent.requestedAction, programDraft, programScheduleAction),
|
|
2196
2394
|
followUpSuggestions: followUpSuggestionsForAsk(intent.route ?? routingMetadata.route, intent, { question, missingDataFlags }),
|
|
2197
2395
|
limitations: missingDataFlags.filter((flag) => !HIDDEN_LIMITATION_FLAGS.has(flag)).map(limitationText),
|
|
2198
2396
|
answerVerification,
|
|
2199
2397
|
programDraft: programDraft ?? null,
|
|
2200
|
-
planChangeset: planChangeset ?? null
|
|
2398
|
+
planChangeset: planChangeset ?? null,
|
|
2399
|
+
programScheduleAction: programScheduleAction ?? null
|
|
2201
2400
|
};
|
|
2202
2401
|
}
|
|
2203
2402
|
|
|
@@ -2222,29 +2421,30 @@ function appendCoachObservationsContextBeforeExcludeNote(lines, observations, ex
|
|
|
2222
2421
|
}
|
|
2223
2422
|
const section = [
|
|
2224
2423
|
'',
|
|
2225
|
-
'
|
|
2226
|
-
'
|
|
2227
|
-
'
|
|
2228
|
-
'
|
|
2229
|
-
'as a hypothesis the user may contradict; Recommendation is a default, not a directive.'
|
|
2424
|
+
'Longer-window training patterns (derived from training data, not user-stated facts).',
|
|
2425
|
+
'Use these as background unless session evidence below says the current workout directly supports them.',
|
|
2426
|
+
'Treat Evidence as load-bearing. Treat Coach read as a grounded read the user may contradict.',
|
|
2427
|
+
'Treat Next move as a default coaching nudge, not a directive.'
|
|
2230
2428
|
];
|
|
2231
2429
|
for (const observation of usable) {
|
|
2430
|
+
const title = typeof observation.title === 'string' && observation.title.trim()
|
|
2431
|
+
? observation.title.trim()
|
|
2432
|
+
: null;
|
|
2232
2433
|
const header = [
|
|
2233
|
-
`-
|
|
2234
|
-
observation.
|
|
2235
|
-
observation.
|
|
2434
|
+
`- pattern-id=${observation.id}`,
|
|
2435
|
+
observation.kind ? `kind=${observation.kind}` : null,
|
|
2436
|
+
observation.sourceComponent ? `source-component=${observation.sourceComponent}` : null,
|
|
2437
|
+
observation.sourceExercise ? `source-exercise=${observation.sourceExercise}` : null,
|
|
2236
2438
|
`confidence=${Number(observation.confidence ?? 0).toFixed(2)}`,
|
|
2237
|
-
`observation-id=${observation.id}`
|
|
2238
2439
|
].filter(Boolean).join(' ');
|
|
2239
2440
|
section.push(header);
|
|
2240
|
-
section.push(`
|
|
2441
|
+
if (title) section.push(` Pattern: ${title}`);
|
|
2442
|
+
section.push(` Evidence: ${observation.summary}`);
|
|
2241
2443
|
if (observation.interpretationText) {
|
|
2242
|
-
|
|
2243
|
-
section.push(` Interpretation${tag}: ${observation.interpretationText}`);
|
|
2444
|
+
section.push(` Coach read: ${observation.interpretationText}`);
|
|
2244
2445
|
}
|
|
2245
2446
|
if (observation.actionText) {
|
|
2246
|
-
|
|
2247
|
-
section.push(` Recommendation${tag}: ${observation.actionText}`);
|
|
2447
|
+
section.push(` Next move: ${observation.actionText}`);
|
|
2248
2448
|
}
|
|
2249
2449
|
if (observation.outcomeStatus) {
|
|
2250
2450
|
const observedAt = observation.outcomeObservedAt ? ` observed ${observation.outcomeObservedAt}` : '';
|
|
@@ -2387,6 +2587,14 @@ function appendAskAnswerContract(lines, {
|
|
|
2387
2587
|
contract.push(' End with this goal-clarifying question when no clear goal decides the tradeoff: "What are we measuring this against - size, strength, or staying lean?"');
|
|
2388
2588
|
}
|
|
2389
2589
|
|
|
2590
|
+
if (route === 'program_history') {
|
|
2591
|
+
contract.push('Answer contract: program change history.');
|
|
2592
|
+
contract.push(' Answer only from the durable program change history evidence.');
|
|
2593
|
+
contract.push(' If the user asks to undo, revert, restore, or change it back, say automatic restore is not available yet in this slice.');
|
|
2594
|
+
contract.push(' Identify the likely target change by date, summary, kind, and id when the evidence supports it.');
|
|
2595
|
+
contract.push(' Do not claim the program was changed, restored, reverted, or updated.');
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2390
2598
|
if (route === 'recent_session' && sessionObservationComparisons.length > 0) {
|
|
2391
2599
|
contract.push('Answer contract: current session plus durable observations.');
|
|
2392
2600
|
contract.push(' Say what improved in the current session first.');
|
|
@@ -2581,7 +2789,7 @@ function humanObservationEvidenceRows(observation) {
|
|
|
2581
2789
|
|
|
2582
2790
|
function appendCoachPatternToRecheck(lines, observation) {
|
|
2583
2791
|
lines.push('');
|
|
2584
|
-
lines.push('
|
|
2792
|
+
lines.push('Training pattern I previously flagged; re-check it before answering:');
|
|
2585
2793
|
lines.push(` Pattern: ${observation.title}`);
|
|
2586
2794
|
lines.push(` pattern-id=${observation.id}; kind=${observation.kind}; confidence=${observation.confidence.toFixed(2)}`);
|
|
2587
2795
|
if (observation.windowStart || observation.windowEnd) {
|
|
@@ -2593,14 +2801,12 @@ function appendCoachPatternToRecheck(lines, observation) {
|
|
|
2593
2801
|
observation.sourceExercise ? `exercise=${observation.sourceExercise}` : null
|
|
2594
2802
|
].filter(Boolean).join('; ')}`);
|
|
2595
2803
|
}
|
|
2596
|
-
lines.push(`
|
|
2804
|
+
lines.push(` Evidence: ${observation.summary}`);
|
|
2597
2805
|
if (observation.interpretationText) {
|
|
2598
|
-
|
|
2599
|
-
lines.push(` Interpretation${tag}: ${observation.interpretationText}`);
|
|
2806
|
+
lines.push(` Coach read: ${observation.interpretationText}`);
|
|
2600
2807
|
}
|
|
2601
2808
|
if (observation.actionText) {
|
|
2602
|
-
|
|
2603
|
-
lines.push(` Recommendation${tag}: ${observation.actionText}`);
|
|
2809
|
+
lines.push(` Next move: ${observation.actionText}`);
|
|
2604
2810
|
}
|
|
2605
2811
|
if (observation.outcomeStatus || observation.outcomeObservedAt || observation.outcomeNotes) {
|
|
2606
2812
|
lines.push(` Stored outcome: ${[
|
|
@@ -2845,7 +3051,7 @@ export function askObservationFollowUpContext(snapshot, question, observation, {
|
|
|
2845
3051
|
pushAskContextHeader(lines, snapshot, today);
|
|
2846
3052
|
appendCoachPatternToRecheck(lines, target);
|
|
2847
3053
|
lines.push('');
|
|
2848
|
-
lines.push('Follow-up voice rule: answer as the coach who flagged the pattern.
|
|
3054
|
+
lines.push('Follow-up voice rule: answer as the coach who flagged the pattern. Do not name the product artifact, card, note, system, or tooling. Use first-person coaching language such as "I flagged...", "your data shows...", or "I would change...".');
|
|
2849
3055
|
lines.push('Outcome rule: treat the prior pattern as a hypothesis. If current evidence still supports it, say it is still active. If the evidence is improving but not clean, say it is partly true. If current evidence contradicts it or it is stale, say you would retire it now before giving advice.');
|
|
2850
3056
|
appendSessionObservationComparisonsBeforeExcludeNote(lines, comparisonTool.rows, exclude);
|
|
2851
3057
|
for (const tool of [scoreTool, recentTool, exerciseTool, readinessTool, bodyWeightTool].filter(Boolean)) {
|
|
@@ -2954,7 +3160,7 @@ export function askMissingObservationFollowUpContext(snapshot, _question, reques
|
|
|
2954
3160
|
const lines = [];
|
|
2955
3161
|
pushAskContextHeader(lines, snapshot, today);
|
|
2956
3162
|
lines.push('');
|
|
2957
|
-
lines.push('Requested
|
|
3163
|
+
lines.push('Requested training-pattern follow-up:');
|
|
2958
3164
|
lines.push(` observation-id=${String(requestedObservation?.id ?? '').trim() || 'unknown'}; status=missing_current_server_observation`);
|
|
2959
3165
|
lines.push(' The client requested an observation follow-up, but the observation did not match current server observations.');
|
|
2960
3166
|
if (followUpIntent === 'successor_plan') {
|
|
@@ -3067,6 +3273,14 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
3067
3273
|
built = buildExerciseProgressSummaryAskContext(contextSnapshot, namedExerciseItems, { exclude, since, today });
|
|
3068
3274
|
} else if (route === 'program_progress') {
|
|
3069
3275
|
built = buildProgramProgressAskContext(contextSnapshot, { exclude, since, today });
|
|
3276
|
+
} else if (route === 'program_history') {
|
|
3277
|
+
built = buildProgramHistoryAskContext(contextSnapshot, { exclude, today });
|
|
3278
|
+
} else if (route === 'program_schedule_action') {
|
|
3279
|
+
built = buildProgramScheduleActionAskContext(contextSnapshot, question, {
|
|
3280
|
+
exclude,
|
|
3281
|
+
today,
|
|
3282
|
+
scheduleContext: evidencePlan.intent?.deloadScheduleContext ?? null
|
|
3283
|
+
});
|
|
3070
3284
|
} else if (route === 'training_profile') {
|
|
3071
3285
|
built = buildTrainingProfileAskContext(contextSnapshot, { exclude, since, today });
|
|
3072
3286
|
} else if (route === 'records') {
|
|
@@ -3171,7 +3385,8 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
3171
3385
|
...(includedFacts.length > 0 ? ['coach_facts'] : []),
|
|
3172
3386
|
...(includedCoachObservationIds.length > 0 ? ['coach_observations'] : []),
|
|
3173
3387
|
...(sessionObservationComparisons.length > 0 ? ['session_observation_comparisons'] : [])
|
|
3174
|
-
]
|
|
3388
|
+
],
|
|
3389
|
+
programScheduleActionStartDate: built.programScheduleActionStartDate
|
|
3175
3390
|
};
|
|
3176
3391
|
const finalizedEvidencePlan = finalizeEvidencePlan(evidencePlan, tools);
|
|
3177
3392
|
const toolMetadata = askToolMetadata(tools, provenance, { requiredTools: finalizedEvidencePlan.requiredTools });
|
|
@@ -3211,6 +3426,7 @@ export function askRoutedContext(snapshot, question, { exclude = new Set(), coac
|
|
|
3211
3426
|
includedCoachObservationIds,
|
|
3212
3427
|
coachObservationIds: includedCoachObservationIds,
|
|
3213
3428
|
currentSessionIds,
|
|
3429
|
+
...(built.programScheduleActionStartDate ? { programScheduleActionStartDate: built.programScheduleActionStartDate } : {}),
|
|
3214
3430
|
sessionObservationComparisons,
|
|
3215
3431
|
evidencePlan: finalizedEvidencePlan,
|
|
3216
3432
|
contextBundle: contextBundleForMetadata(contextBundle),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SECURITY_PREAMBLE } from './prompt-security.js';
|
|
2
|
+
import {
|
|
3
|
+
ASK_COACH_INTRO,
|
|
4
|
+
ASK_CORE_RULES,
|
|
5
|
+
ASK_DEFENSIVE_RULES,
|
|
6
|
+
ASK_EXPANSIVE_RULES,
|
|
7
|
+
ASK_STRUCTURED_RULES,
|
|
8
|
+
COACH_SOUL
|
|
9
|
+
} from './coach-prompt-layers.js';
|
|
10
|
+
|
|
11
|
+
export function composeAskPrompt(profile = 'expansive') {
|
|
12
|
+
const profileRules = profile === 'structured'
|
|
13
|
+
? `${ASK_DEFENSIVE_RULES}\n\n${ASK_STRUCTURED_RULES}`
|
|
14
|
+
: profile === 'defensive'
|
|
15
|
+
? ASK_DEFENSIVE_RULES
|
|
16
|
+
: ASK_EXPANSIVE_RULES;
|
|
17
|
+
return `${SECURITY_PREAMBLE}${ASK_COACH_INTRO}
|
|
18
|
+
|
|
19
|
+
${COACH_SOUL}
|
|
20
|
+
|
|
21
|
+
${ASK_CORE_RULES}
|
|
22
|
+
|
|
23
|
+
${profileRules}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ASK_PROMPT = composeAskPrompt('expansive');
|
|
27
|
+
export const ASK_DEFENSIVE_PROMPT = composeAskPrompt('defensive');
|
|
28
|
+
export const ASK_STRUCTURED_PROMPT = composeAskPrompt('structured');
|
|
29
|
+
|
|
30
|
+
export function askPromptForResponseProfile(responseProfile) {
|
|
31
|
+
if (responseProfile === 'structured') return ASK_STRUCTURED_PROMPT;
|
|
32
|
+
if (responseProfile === 'defensive') return ASK_DEFENSIVE_PROMPT;
|
|
33
|
+
return ASK_PROMPT;
|
|
34
|
+
}
|