incremnt 0.8.7 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/SKILL.md +2 -1
- package/package.json +4 -1
- package/src/ask-answer-verifier.js +23 -2
- package/src/ask-coach/contexts.js +1472 -0
- package/src/ask-coach/evidence-plan.js +342 -0
- package/src/ask-coach/observations.js +8 -0
- package/src/ask-coach/orchestrator.js +1641 -0
- package/src/ask-coach/renderers.js +4 -0
- package/src/ask-coach/routing.js +610 -0
- package/src/ask-coach/structured-response.js +5 -0
- package/src/ask-coach-routing.js +4 -0
- package/src/ask-coach.js +26 -3506
- package/src/ask-replay.js +70 -4
- package/src/ask-starter-prompts.js +206 -0
- package/src/auth.js +26 -0
- package/src/coach-advice.js +32 -0
- package/src/coach-facts.js +214 -0
- package/src/coach-prompt-layers.js +26 -30
- package/src/contract.js +27 -7
- package/src/exercise-aliases.js +6 -0
- package/src/format.js +17 -0
- package/src/index.js +1 -1
- package/src/lib.js +41 -36
- package/src/mcp.js +1 -1
- package/src/openrouter.js +279 -161
- package/src/plan-changeset.js +14 -8
- package/src/program-draft.js +97 -1
- package/src/prompt-changelog.js +16 -0
- package/src/prompt-security.js +34 -3
- package/src/promptfoo-evals.js +2 -0
- package/src/promptfoo-langfuse-scores.js +10 -0
- package/src/prompts/ask.js +22 -0
- package/src/prompts/checkpoint.js +14 -0
- package/src/prompts/coach-facts.js +24 -0
- package/src/prompts/cycle.js +23 -0
- package/src/prompts/starter-graph.js +7 -0
- package/src/prompts/vitals.js +9 -0
- package/src/prompts/weekly-checkin.js +20 -0
- package/src/prompts/workout.js +47 -0
- package/src/queries/coach-observations.js +8 -0
- package/src/queries/coach-read-tools.js +6 -0
- package/src/queries/commands.js +3 -0
- package/src/queries/common.js +9 -0
- package/src/queries/core.js +6354 -0
- package/src/queries/exercise-identity.js +6 -0
- package/src/queries/health.js +12 -0
- package/src/queries/programs.js +17 -0
- package/src/queries/records-progress.js +10 -0
- package/src/queries/sessions.js +12 -0
- package/src/queries/weekly.js +5 -0
- package/src/queries.js +10 -6283
- package/src/remote.js +51 -0
- package/src/score-context.js +58 -3
- package/src/summary-evals.js +201 -45
- package/src/sync-service.js +1118 -104
- package/src/training-language-public-terms.json +97 -0
- package/src/training-language.js +94 -0
- package/src/transport.js +7 -1
- package/src/validate.js +11 -1
package/src/ask-replay.js
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
AI_PROMPT_VERSIONS,
|
|
6
|
-
generateAskAnswerAgentic
|
|
6
|
+
generateAskAnswerAgentic,
|
|
7
|
+
generateAskAnswerStarterGraph
|
|
7
8
|
} from './openrouter.js';
|
|
8
9
|
import { verifyAskAnswer } from './ask-answer-verifier.js';
|
|
9
10
|
import {
|
|
@@ -42,6 +43,22 @@ function invocationNames(invocations) {
|
|
|
42
43
|
return asArray(invocations).map((invocation) => invocation?.name).filter(Boolean);
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function graphToolNames(generationMetadata = {}, testCase = null) {
|
|
47
|
+
return asArray(
|
|
48
|
+
generationMetadata.graph?.toolNames
|
|
49
|
+
?? generationMetadata.graphToolNames
|
|
50
|
+
?? testCase?.expectedStarterGraph?.toolNames
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function graphNodeNames(generationMetadata = {}, testCase = null) {
|
|
55
|
+
return asArray(
|
|
56
|
+
generationMetadata.graph?.nodes
|
|
57
|
+
?? generationMetadata.graphNodes
|
|
58
|
+
?? testCase?.expectedStarterGraph?.nodes
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
function askReplayChecks(testCase, context, output, generationMetadata = {}) {
|
|
46
63
|
const assertions = testCase.replayAssertions ?? {};
|
|
47
64
|
const metadata = context?.routedMetadata ?? {};
|
|
@@ -104,6 +121,30 @@ function askReplayChecks(testCase, context, output, generationMetadata = {}) {
|
|
|
104
121
|
});
|
|
105
122
|
}
|
|
106
123
|
|
|
124
|
+
if (assertions.graphTools) {
|
|
125
|
+
const actual = graphToolNames(generationMetadata, generationMetadata.mode === 'live' ? null : testCase);
|
|
126
|
+
const passed = includesAll(actual, assertions.graphTools);
|
|
127
|
+
checks.push({
|
|
128
|
+
key: 'ask_replay_graph_tools',
|
|
129
|
+
passed,
|
|
130
|
+
reason: passed
|
|
131
|
+
? 'Starter graph tools include all replay requirements.'
|
|
132
|
+
: `Expected graph tools to include ${assertions.graphTools.join(', ')}; got ${actual.join(', ')}.`
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (assertions.graphNodes) {
|
|
137
|
+
const actual = graphNodeNames(generationMetadata, generationMetadata.mode === 'live' ? null : testCase);
|
|
138
|
+
const passed = includesAll(actual, assertions.graphNodes);
|
|
139
|
+
checks.push({
|
|
140
|
+
key: 'ask_replay_graph_nodes',
|
|
141
|
+
passed,
|
|
142
|
+
reason: passed
|
|
143
|
+
? 'Starter graph nodes include all replay requirements.'
|
|
144
|
+
: `Expected graph nodes to include ${assertions.graphNodes.join(', ')}; got ${actual.join(', ')}.`
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
107
148
|
for (const assertion of asArray(assertions.requiredContextPatterns)) {
|
|
108
149
|
const passed = patternMatches(context?.trainingData, assertion.pattern);
|
|
109
150
|
checks.push({
|
|
@@ -164,6 +205,8 @@ function askReplayChecks(testCase, context, output, generationMetadata = {}) {
|
|
|
164
205
|
if (assertions.promptVersion) {
|
|
165
206
|
const expected = assertions.promptVersion === 'currentAskAgentic'
|
|
166
207
|
? AI_PROMPT_VERSIONS.askAgentic
|
|
208
|
+
: assertions.promptVersion === 'currentAskStarterGraph'
|
|
209
|
+
? AI_PROMPT_VERSIONS.askStarterGraph
|
|
167
210
|
: assertions.promptVersion;
|
|
168
211
|
const actual = generationMetadata.promptVersion ?? AI_PROMPT_VERSIONS.askAgentic;
|
|
169
212
|
checks.push({
|
|
@@ -201,7 +244,8 @@ export async function runAskReplayCase(testCase, {
|
|
|
201
244
|
live = false,
|
|
202
245
|
apiKey = process.env.OPENROUTER_API_KEY,
|
|
203
246
|
model = null,
|
|
204
|
-
generateAskAnswerAgenticImpl = generateAskAnswerAgentic
|
|
247
|
+
generateAskAnswerAgenticImpl = generateAskAnswerAgentic,
|
|
248
|
+
generateAskAnswerStarterGraphImpl = generateAskAnswerStarterGraph
|
|
205
249
|
} = {}) {
|
|
206
250
|
const snapshot = await loadSummaryEvalSnapshot(testCase);
|
|
207
251
|
const context = buildSummaryEvalContext(snapshot, testCase);
|
|
@@ -210,12 +254,27 @@ export async function runAskReplayCase(testCase, {
|
|
|
210
254
|
let output = testCase.output;
|
|
211
255
|
let generationMetadata = {
|
|
212
256
|
mode: 'stored',
|
|
213
|
-
promptVersion:
|
|
257
|
+
promptVersion: testCase.starterPrompt?.family
|
|
258
|
+
? AI_PROMPT_VERSIONS.askStarterGraph
|
|
259
|
+
: AI_PROMPT_VERSIONS.askAgentic
|
|
214
260
|
};
|
|
215
261
|
|
|
216
262
|
if (live) {
|
|
217
263
|
if (!apiKey) throw new Error('OPENROUTER_API_KEY is required for live Ask replay.');
|
|
218
|
-
const
|
|
264
|
+
const question = context.question ?? testCase.question;
|
|
265
|
+
const starterPrompt = testCase.starterPrompt;
|
|
266
|
+
const result = starterPrompt?.family
|
|
267
|
+
? await generateAskAnswerStarterGraphImpl(context.trainingData, question, {
|
|
268
|
+
apiKey,
|
|
269
|
+
model: model ?? context.model,
|
|
270
|
+
history: context.history ?? [],
|
|
271
|
+
tone: context.tone,
|
|
272
|
+
snapshot,
|
|
273
|
+
starterPrompt,
|
|
274
|
+
routingMetadata: context.routedMetadata ?? undefined,
|
|
275
|
+
exclude: testCase.exclude ?? []
|
|
276
|
+
})
|
|
277
|
+
: await generateAskAnswerAgenticImpl(context.trainingData, question, {
|
|
219
278
|
apiKey,
|
|
220
279
|
model: model ?? context.model,
|
|
221
280
|
history: context.history ?? [],
|
|
@@ -231,6 +290,7 @@ export async function runAskReplayCase(testCase, {
|
|
|
231
290
|
promptVersion: result.promptVersion,
|
|
232
291
|
promptSurface: result.promptSurface,
|
|
233
292
|
toolInvocations: result.toolInvocations ?? [],
|
|
293
|
+
graph: result.graph,
|
|
234
294
|
langfuseTraceId: result.langfuseTraceId,
|
|
235
295
|
langfuseObservationId: result.langfuseObservationId
|
|
236
296
|
};
|
|
@@ -267,6 +327,9 @@ export async function runAskReplayCase(testCase, {
|
|
|
267
327
|
requiredTools: context.routedMetadata?.evidencePlan?.requiredTools ?? [],
|
|
268
328
|
routedTools: context.routedMetadata?.evidencePlan?.executedTools ?? [],
|
|
269
329
|
agenticTools: invocationNames(generationMetadata.toolInvocations),
|
|
330
|
+
graphTools: graphToolNames(generationMetadata, testCase),
|
|
331
|
+
graphNodes: graphNodeNames(generationMetadata, testCase),
|
|
332
|
+
starterPrompt: testCase.starterPrompt ?? null,
|
|
270
333
|
executedTools: context.routedMetadata?.evidencePlan?.executedTools ?? [],
|
|
271
334
|
answerVerification,
|
|
272
335
|
output,
|
|
@@ -333,6 +396,9 @@ export function formatAskReplayMarkdown(report) {
|
|
|
333
396
|
if (result.mode === 'live') {
|
|
334
397
|
lines.push(`Agentic tools: ${result.agenticTools.join(', ') || 'none'}`);
|
|
335
398
|
}
|
|
399
|
+
if (result.graphTools.length > 0) {
|
|
400
|
+
lines.push(`Graph tools: ${result.graphTools.join(', ')}`);
|
|
401
|
+
}
|
|
336
402
|
lines.push('');
|
|
337
403
|
lines.push(result.output);
|
|
338
404
|
lines.push('');
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
export const STARTER_PROMPT_FAMILIES = Object.freeze([
|
|
2
|
+
'recent_session_review',
|
|
3
|
+
'lift_progress',
|
|
4
|
+
'pr_review',
|
|
5
|
+
'training_review',
|
|
6
|
+
'next_session_prep',
|
|
7
|
+
'plan_creation',
|
|
8
|
+
'evergreen_strength',
|
|
9
|
+
'muscle_coverage',
|
|
10
|
+
'consistency_review',
|
|
11
|
+
'comeback_review',
|
|
12
|
+
'score_review'
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export const STARTER_GRAPH_VERSION = 'ask_starter_langgraph_v1';
|
|
16
|
+
|
|
17
|
+
const STARTER_PROMPT_FAMILY_SET = new Set(STARTER_PROMPT_FAMILIES);
|
|
18
|
+
const CONTEXT_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/;
|
|
19
|
+
const MAX_CONTEXT_FIELDS = 10;
|
|
20
|
+
const MAX_CONTEXT_VALUE_CHARS = 200;
|
|
21
|
+
|
|
22
|
+
export function starterWorkflowEnabled(env = process.env) {
|
|
23
|
+
return ['1', 'true', 'yes', 'on'].includes(String(env.ASK_STARTER_LANGGRAPH_ENABLED ?? '').trim().toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeStarterPrompt(raw) {
|
|
27
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
28
|
+
|
|
29
|
+
const family = typeof raw.family === 'string' ? raw.family.trim() : '';
|
|
30
|
+
if (!STARTER_PROMPT_FAMILY_SET.has(family)) return null;
|
|
31
|
+
|
|
32
|
+
const question = typeof raw.question === 'string' ? raw.question.trim() : '';
|
|
33
|
+
if (!question || question.length > 500) return null;
|
|
34
|
+
|
|
35
|
+
const context = normalizeStarterPromptContext(raw.context);
|
|
36
|
+
return {
|
|
37
|
+
family,
|
|
38
|
+
question,
|
|
39
|
+
...(Object.keys(context).length > 0 ? { context } : {})
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isSupportedStarterPrompt(starterPrompt) {
|
|
44
|
+
return Boolean(starterPrompt && STARTER_PROMPT_FAMILY_SET.has(starterPrompt.family));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function starterGraphFallbackReason(error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
49
|
+
if (/timeout|abort/i.test(message)) return 'timeout';
|
|
50
|
+
if (/unsupported/i.test(message)) return 'unsupported';
|
|
51
|
+
if (/invalid/i.test(message)) return 'invalid_output';
|
|
52
|
+
return 'error';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function starterPromptToolPlan(starterPrompt) {
|
|
56
|
+
if (!isSupportedStarterPrompt(starterPrompt)) return [];
|
|
57
|
+
|
|
58
|
+
const exercise = starterPrompt.context?.exerciseName
|
|
59
|
+
?? starterPrompt.context?.exercise
|
|
60
|
+
?? null;
|
|
61
|
+
const exerciseParams = exercise ? { exercises: [exercise] } : {};
|
|
62
|
+
|
|
63
|
+
switch (starterPrompt.family) {
|
|
64
|
+
case 'lift_progress':
|
|
65
|
+
return [
|
|
66
|
+
{ name: 'get_exercise_history', params: { ...exerciseParams, limit: 8, recencyCutoffDays: 90 } },
|
|
67
|
+
{ name: 'get_exercise_progress_summary', params: { ...exerciseParams, limit: 12 } },
|
|
68
|
+
{ name: 'get_records', params: { ...exerciseParams, limit: exercise ? 5 : 15 } }
|
|
69
|
+
];
|
|
70
|
+
case 'pr_review':
|
|
71
|
+
case 'evergreen_strength':
|
|
72
|
+
return [
|
|
73
|
+
{ name: 'get_records', params: { ...exerciseParams, limit: exercise ? 5 : 15 } },
|
|
74
|
+
{ name: 'get_exercise_progress_summary', params: { ...exerciseParams, limit: 10 } }
|
|
75
|
+
];
|
|
76
|
+
case 'recent_session_review':
|
|
77
|
+
return [
|
|
78
|
+
{ name: 'get_recent_sessions', params: { limit: 1, includeStale: true, recencyCutoffDays: 30 } },
|
|
79
|
+
{ name: 'compare_session_to_observations', params: { observationLimit: 5, includeOutcomeHistory: true } },
|
|
80
|
+
{ name: 'get_next_session', params: { historyLimit: 6, recencyCutoffDays: 90 } }
|
|
81
|
+
];
|
|
82
|
+
case 'training_review':
|
|
83
|
+
case 'consistency_review':
|
|
84
|
+
case 'comeback_review':
|
|
85
|
+
return [
|
|
86
|
+
{ name: 'get_recent_sessions', params: { limit: 8, includeStale: true, recencyCutoffDays: 60 } },
|
|
87
|
+
{ name: 'get_weekly_volume', params: {} },
|
|
88
|
+
{ name: 'get_records', params: { limit: 15 } },
|
|
89
|
+
{ name: 'get_readiness_snapshot', params: { recentDays: 14 } },
|
|
90
|
+
{ name: 'get_body_weight_snapshot', params: { recentDays: 30 } },
|
|
91
|
+
...(isStarterScoreQuestion(starterPrompt) ? [{ name: 'get_increment_score', params: { historyDays: 21 } }] : [])
|
|
92
|
+
];
|
|
93
|
+
case 'score_review':
|
|
94
|
+
return [
|
|
95
|
+
{ name: 'get_increment_score', params: { historyDays: 21 } },
|
|
96
|
+
{ name: 'get_recent_sessions', params: { limit: 4, includeStale: true, recencyCutoffDays: 30 } }
|
|
97
|
+
];
|
|
98
|
+
case 'next_session_prep':
|
|
99
|
+
return [
|
|
100
|
+
{ name: 'get_next_session', params: { historyLimit: 8, recencyCutoffDays: 90 } },
|
|
101
|
+
{ name: 'get_recent_sessions', params: { limit: 3, includeStale: true, recencyCutoffDays: 45 } },
|
|
102
|
+
{ name: 'get_exercise_history', params: { limit: 8, recencyCutoffDays: 90 } }
|
|
103
|
+
];
|
|
104
|
+
case 'plan_creation':
|
|
105
|
+
return [
|
|
106
|
+
{ name: 'get_training_profile', params: {} },
|
|
107
|
+
{ name: 'get_recent_sessions', params: { limit: 8, includeStale: true, recencyCutoffDays: 90 } },
|
|
108
|
+
{ name: 'get_program_progress', params: { limitExercises: 10 } }
|
|
109
|
+
];
|
|
110
|
+
case 'muscle_coverage':
|
|
111
|
+
return [
|
|
112
|
+
{ name: 'get_muscle_volume_trend', params: { weeks: 4 } },
|
|
113
|
+
{ name: 'get_recent_sessions', params: { limit: 8, includeStale: true, recencyCutoffDays: 60 } }
|
|
114
|
+
];
|
|
115
|
+
default:
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isStarterScoreQuestion(starterPrompt) {
|
|
121
|
+
return /\b(?:increment\s+)?score\b/i.test(String(starterPrompt?.question ?? ''));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function starterPromptAnswerContract(starterPrompt) {
|
|
125
|
+
switch (starterPrompt?.family) {
|
|
126
|
+
case 'lift_progress':
|
|
127
|
+
return 'Answer with the lift trend, the evidence, and what to do on the next exposure.';
|
|
128
|
+
case 'recent_session_review':
|
|
129
|
+
return 'Answer with one win, one watch item if supported, and the next move.';
|
|
130
|
+
case 'training_review':
|
|
131
|
+
case 'consistency_review':
|
|
132
|
+
case 'comeback_review':
|
|
133
|
+
return 'Answer with the broad trend, the main driver, and the decision question or next action.';
|
|
134
|
+
case 'score_review':
|
|
135
|
+
return 'Explain the current 7-day Score in plain training language: what helped, what held it back, and whether the trend is improving. Do not dump raw sub-scores or tool output.';
|
|
136
|
+
case 'next_session_prep':
|
|
137
|
+
return 'Answer with expected work for the next session and what to watch.';
|
|
138
|
+
case 'plan_creation':
|
|
139
|
+
return 'Draft a concise plan answer and append exactly one trailing <program_draft>{JSON}</program_draft> block if the evidence supports a valid draft. The draft JSON may only use countable reps from 1-30; do not include timed holds, seconds, distance, duration, or extra set keys. Never apply the plan.';
|
|
140
|
+
case 'evergreen_strength':
|
|
141
|
+
case 'pr_review':
|
|
142
|
+
return 'Answer with the relevant record or PR evidence and what it means now.';
|
|
143
|
+
case 'muscle_coverage':
|
|
144
|
+
return 'Answer from effective-set muscle data, naming the main under-trained areas and the next adjustment.';
|
|
145
|
+
default:
|
|
146
|
+
return 'Answer the tapped starter prompt using only the supplied training evidence.';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function starterPromptStructuredIntentHint(starterPrompt) {
|
|
151
|
+
switch (starterPrompt?.family) {
|
|
152
|
+
case 'lift_progress':
|
|
153
|
+
case 'pr_review':
|
|
154
|
+
case 'evergreen_strength':
|
|
155
|
+
return { route: 'exercise_progress_summary' };
|
|
156
|
+
case 'recent_session_review':
|
|
157
|
+
return { route: 'recent_session' };
|
|
158
|
+
case 'training_review':
|
|
159
|
+
case 'consistency_review':
|
|
160
|
+
case 'comeback_review':
|
|
161
|
+
return { route: 'progress_review' };
|
|
162
|
+
case 'score_review':
|
|
163
|
+
return { route: 'score' };
|
|
164
|
+
case 'next_session_prep':
|
|
165
|
+
return { route: 'next_session' };
|
|
166
|
+
case 'plan_creation':
|
|
167
|
+
return { route: 'program_design', requestedAction: 'draft_plan' };
|
|
168
|
+
case 'muscle_coverage':
|
|
169
|
+
return { route: 'volume' };
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function applyStarterPromptStructuredIntentHint(routingMetadata, starterPrompt) {
|
|
176
|
+
if (!routingMetadata || typeof routingMetadata !== 'object') return routingMetadata;
|
|
177
|
+
const hint = starterPromptStructuredIntentHint(starterPrompt);
|
|
178
|
+
if (!hint) return routingMetadata;
|
|
179
|
+
|
|
180
|
+
const currentRoute = routingMetadata.intent?.route ?? routingMetadata.route;
|
|
181
|
+
const shouldApplyRoute = !currentRoute || currentRoute === 'general';
|
|
182
|
+
const intent = {
|
|
183
|
+
...(routingMetadata.intent ?? {}),
|
|
184
|
+
...(shouldApplyRoute ? { route: hint.route } : {}),
|
|
185
|
+
...(hint.requestedAction && !routingMetadata.intent?.requestedAction ? { requestedAction: hint.requestedAction } : {})
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
...routingMetadata,
|
|
189
|
+
...(shouldApplyRoute ? { route: hint.route } : {}),
|
|
190
|
+
intent
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeStarterPromptContext(raw) {
|
|
195
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
196
|
+
const context = {};
|
|
197
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
198
|
+
if (Object.keys(context).length >= MAX_CONTEXT_FIELDS) break;
|
|
199
|
+
if (!CONTEXT_KEY_RE.test(key)) continue;
|
|
200
|
+
if (!['string', 'number', 'boolean'].includes(typeof value)) continue;
|
|
201
|
+
const text = String(value).trim();
|
|
202
|
+
if (!text || text.length > MAX_CONTEXT_VALUE_CHARS) continue;
|
|
203
|
+
context[key] = text;
|
|
204
|
+
}
|
|
205
|
+
return context;
|
|
206
|
+
}
|
package/src/auth.js
CHANGED
|
@@ -362,6 +362,32 @@ export async function refreshRemoteSession(baseUrl, token, previousSession = nul
|
|
|
362
362
|
});
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
export async function refreshRemoteContractMetadata(baseUrl, token, previousSession = null) {
|
|
366
|
+
const remoteContract = await fetchRemoteContract(baseUrl, token);
|
|
367
|
+
const checkedAt = new Date().toISOString();
|
|
368
|
+
|
|
369
|
+
return writeSessionState({
|
|
370
|
+
version: previousSession?.version ?? 1,
|
|
371
|
+
mode: 'remote',
|
|
372
|
+
account: previousSession?.account ?? null,
|
|
373
|
+
auth: previousSession?.auth ?? {
|
|
374
|
+
accessToken: token,
|
|
375
|
+
refreshToken: null,
|
|
376
|
+
expiresAt: null
|
|
377
|
+
},
|
|
378
|
+
sync: {
|
|
379
|
+
...(previousSession?.sync ?? {}),
|
|
380
|
+
contractCheckedAt: checkedAt
|
|
381
|
+
},
|
|
382
|
+
transport: {
|
|
383
|
+
...(previousSession?.transport ?? {}),
|
|
384
|
+
baseUrl,
|
|
385
|
+
contractVersion: remoteContract.contractVersion,
|
|
386
|
+
capabilities: remoteContract.capabilities ?? null
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
365
391
|
export async function createRemoteAgentToken(baseUrl, bearerToken, {
|
|
366
392
|
name,
|
|
367
393
|
access = 'read',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { sanitizeCoachAdviceCandidate } from './ask-coach.js';
|
|
2
|
+
|
|
3
|
+
function collapseBlankLines(text) {
|
|
4
|
+
return String(text ?? '')
|
|
5
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
6
|
+
.trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function extractCoachAdvice(rawText) {
|
|
10
|
+
const text = String(rawText ?? '');
|
|
11
|
+
const match = text.match(/<coach_advice>\s*([\s\S]*?)\s*<\/coach_advice>/i);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return { answerText: text.trim(), coachAdvice: null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const answerText = collapseBlankLines(text.replace(match[0], ''));
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(match[1]);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.warn('askCoach: <coach_advice> JSON parse failed - dropping advice:', err.message);
|
|
22
|
+
return { answerText, coachAdvice: null };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const coachAdvice = sanitizeCoachAdviceCandidate(parsed);
|
|
26
|
+
if (!coachAdvice) {
|
|
27
|
+
console.warn('askCoach: <coach_advice> payload failed validation - dropping advice');
|
|
28
|
+
return { answerText, coachAdvice: null };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { answerText, coachAdvice };
|
|
32
|
+
}
|
package/src/coach-facts.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
export const COACH_FACT_KINDS = Object.freeze(['preference', 'constraint', 'injury', 'goal_signal', 'tone']);
|
|
2
2
|
|
|
3
|
+
export const COACH_FACT_PRESENTATION = Object.freeze([
|
|
4
|
+
{ kind: 'goal_signal', key: 'goals', title: 'Goals', kindTitle: 'Goal' },
|
|
5
|
+
{ kind: 'constraint', key: 'constraints', title: 'Constraints', kindTitle: 'Constraint' },
|
|
6
|
+
{ kind: 'injury', key: 'injuries', title: 'Injuries', kindTitle: 'Injury' },
|
|
7
|
+
{ kind: 'preference', key: 'preferences', title: 'Preferences', kindTitle: 'Preference' },
|
|
8
|
+
{ kind: 'tone', key: 'tone', title: 'Tone', kindTitle: 'Tone' }
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const COACH_FACT_GROUP_KEYS = Object.freeze(
|
|
12
|
+
Object.fromEntries(COACH_FACT_PRESENTATION.map((section) => [section.kind, section.key]))
|
|
13
|
+
);
|
|
14
|
+
|
|
3
15
|
const COACH_FACT_KIND_SET = new Set(COACH_FACT_KINDS);
|
|
4
16
|
|
|
5
17
|
const PROMPT_INJECTION_PATTERNS = [
|
|
@@ -22,6 +34,51 @@ const EPHEMERAL_PATTERNS = [
|
|
|
22
34
|
/\b(current|currently|latest|recent)\s+(?:best|e1rm|1rm|volume|pr|record|session|workout)\b/i
|
|
23
35
|
];
|
|
24
36
|
|
|
37
|
+
const EPHEMERAL_RESPONSE_REQUEST_PATTERNS = [
|
|
38
|
+
/\b(?:i\s+want|i\s+asked\s+for|i\s+ask\s+for|i(?:'d| would)?\s+like|wants|asked for|asks for|would like)\b.{0,80}\b(right now|for this answer|current answer)\b/i,
|
|
39
|
+
/\b(?:i\s+want|i\s+asked\s+for|i\s+ask\s+for|i(?:'d| would)?\s+like|wants|asked for|asks for|would like)\b.{0,80}\b(latest|recent|last|current|today|this week|this month|past|six[-\s]?month|\d+\s+(?:days|weeks|months|years))\b.{0,80}\b(coaching\s+)?(read|take|answer|response|summary|breakdown|overview|feedback)\b/i,
|
|
40
|
+
/\b(?:i\s+want|i\s+asked\s+for|i\s+ask\s+for|i(?:'d| would)?\s+like|wants|asked for|asks for|would like)\b.{0,80}\b(coaching\s+)?(read|take|answer|response|summary|breakdown|overview|feedback)\b.{0,80}\b(latest|recent|last|current|today|this week|this month|past|six[-\s]?month|\d+\s+(?:days|weeks|months|years))\b/i,
|
|
41
|
+
/\bnot just\b.{0,60}\b(latest|recent|last|cards?|session|workout)\b/i,
|
|
42
|
+
/\b(last|past)\s+(?:\d+\s+)?(?:days|weeks|months|years)\b.{0,80}\b(read|take|answer|response|summary|breakdown|overview|feedback)\b/i,
|
|
43
|
+
/\b(?:i\s+want|i\s+asked\s+for|i\s+ask\s+for|i(?:'d| would)?\s+like|wants|asked for|asks for|would like)\b.{0,80}\b(guidance|feedback|recommendation|what to do next|progress checks?|progress updates?|evidence explained|session highlights explained)\b/i,
|
|
44
|
+
/\b(?:i\s+want|i\s+asked\s+for|i\s+ask\s+for|i(?:'d| would)?\s+like|wants|asked for|asks for|would like)\b.{0,80}\b(brand[-\s]?new plan|plan from scratch|program draft|plan artifact|changeset)\b/i
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const DURABLE_GOAL_PATTERNS = [
|
|
48
|
+
/\b(?:lose|gain|build|improve|increase|maintain|cut|bulk)\b.{0,80}\b(?:weight|muscle|strength|bench|squat|deadlift|press|pull[-\s]?up|fitness|conditioning|mobility|body fat|hypertrophy)\b/i,
|
|
49
|
+
/\b(?:weight|muscle|strength|bench|squat|deadlift|press|pull[-\s]?up|fitness|conditioning|mobility|body fat|hypertrophy)\b.{0,80}\b(?:goal|target|priority|focus)\b/i,
|
|
50
|
+
/\b(?:goal|target|priority|focus)\b.{0,80}\b(?:lose|gain|build|improve|increase|maintain|cut|bulk)\b/i,
|
|
51
|
+
/\b(?:bench|squat|deadlift|press|pull[-\s]?up|strength|muscle|weight)\b.{0,80}\bprogress\b/i,
|
|
52
|
+
/\bprioriti[sz]e\b.{0,80}\b(?:shoulder[-\s]?friendly|pressing|pulling|strength|hypertrophy|muscle|mobility)\b/i
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const ASK_INTENT_ONLY_PATTERNS = [
|
|
56
|
+
/\b(?:what should i do next|what do i do next|what are my goals|what's my goal|explain this|check my progress|how am i progressing|what evidence matters|what matters here)\b/i,
|
|
57
|
+
/\b(?:recommendation|recommend|feedback|guidance|advice)\b.{0,60}\b(?:what to do next|next|now|today|this week|for this)\b/i
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const FIRST_PERSON_DURABLE_SIGNAL_PATTERNS = [
|
|
61
|
+
/\bmy goal is\b/i,
|
|
62
|
+
/\bi want to\b.{0,100}\b(?:lose|gain|build|improve|increase|maintain|cut|bulk|avoid)\b/i,
|
|
63
|
+
/\bi(?:'d| would)? prefer\b/i,
|
|
64
|
+
/\bi want to avoid\b/i,
|
|
65
|
+
/\bi can only\b/i,
|
|
66
|
+
/\bi only have\b/i,
|
|
67
|
+
/\bwork sometimes cuts\b/i,
|
|
68
|
+
/\b(?:my|the)\s+(?:shoulder|knee|elbow|hip|back|wrist|ankle|neck)\s+(?:hurts|aches|is sore|is injured)\b/i,
|
|
69
|
+
/\bi can(?:['’]t|not)\b.{0,80}\bbecause\b/i,
|
|
70
|
+
/\bbe concise\b/i,
|
|
71
|
+
/\bdon(?:['’]t| not)\s+be\s+demeaning\b/i
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const REMOVAL_INTENT_PATTERNS = [
|
|
75
|
+
/\b(?:remove|delete|clear)\s+(?:that|this|my)?\s*(?:goal|memory|fact|preference|constraint|injury|tone)\b/i,
|
|
76
|
+
/\bforget\s+(?:that|this|my)?\b/i,
|
|
77
|
+
/\bi don(?:['’]t| not)\s+want\b.{0,120}\bas\s+a\s+goal\s+anymore\b/i,
|
|
78
|
+
/\bthat(?:['’]s| is)\s+not\s+true\s+anymore\b/i,
|
|
79
|
+
/\bno\s+longer\s+(?:true|a goal|relevant)\b/i
|
|
80
|
+
];
|
|
81
|
+
|
|
25
82
|
export function normalizeCoachFactText(value) {
|
|
26
83
|
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
27
84
|
}
|
|
@@ -57,9 +114,87 @@ export function coachFactPolicyViolation(candidate) {
|
|
|
57
114
|
if (PROMPT_INJECTION_PATTERNS.some((pattern) => pattern.test(fact))) return 'prompt_injection';
|
|
58
115
|
if (DERIVED_TRAINING_PATTERNS.some((pattern) => pattern.test(fact))) return 'derived_training_fact';
|
|
59
116
|
if (EPHEMERAL_PATTERNS.some((pattern) => pattern.test(fact))) return 'ephemeral_fact';
|
|
117
|
+
if (EPHEMERAL_RESPONSE_REQUEST_PATTERNS.some((pattern) => pattern.test(fact))) return 'ephemeral_response_request';
|
|
118
|
+
if (kind === 'goal_signal' && !DURABLE_GOAL_PATTERNS.some((pattern) => pattern.test(fact))) return 'non_durable_goal_signal';
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function coachFactPersistenceDecision(candidate) {
|
|
123
|
+
const reason = coachFactPolicyViolation(candidate);
|
|
124
|
+
return { persist: !reason, reason };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function coachFactRetrievalViolation(candidate) {
|
|
128
|
+
const reason = coachFactPolicyViolation(candidate);
|
|
129
|
+
if (reason) return reason;
|
|
60
130
|
return null;
|
|
61
131
|
}
|
|
62
132
|
|
|
133
|
+
export function shouldAttemptCoachFactExtraction(currentUserText, { sourceSurface = 'ask' } = {}) {
|
|
134
|
+
const text = normalizeCoachFactText(currentUserText);
|
|
135
|
+
if (!text) return { shouldAttempt: false, skippedReason: 'empty_user_turn' };
|
|
136
|
+
if (detectCoachFactRemovalIntent(text).isRemovalIntent) {
|
|
137
|
+
return { shouldAttempt: false, skippedReason: 'memory_removal_request' };
|
|
138
|
+
}
|
|
139
|
+
if (EPHEMERAL_RESPONSE_REQUEST_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
140
|
+
return { shouldAttempt: false, skippedReason: 'ephemeral_response_request' };
|
|
141
|
+
}
|
|
142
|
+
if (ASK_INTENT_ONLY_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
143
|
+
return { shouldAttempt: false, skippedReason: 'ask_intent_only' };
|
|
144
|
+
}
|
|
145
|
+
if (!FIRST_PERSON_DURABLE_SIGNAL_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
146
|
+
return { shouldAttempt: false, skippedReason: 'no_durable_first_person_signal' };
|
|
147
|
+
}
|
|
148
|
+
return { shouldAttempt: true, sourceSurface };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function detectCoachFactRemovalIntent(currentUserText) {
|
|
152
|
+
const text = normalizeCoachFactText(currentUserText);
|
|
153
|
+
if (!text) return { isRemovalIntent: false, text: '' };
|
|
154
|
+
const isRemovalIntent = REMOVAL_INTENT_PATTERNS.some((pattern) => pattern.test(text));
|
|
155
|
+
const kindPreference = /\bgoal\b/i.test(text)
|
|
156
|
+
? 'goal_signal'
|
|
157
|
+
: /\bconstraint\b/i.test(text)
|
|
158
|
+
? 'constraint'
|
|
159
|
+
: /\binjur(?:y|ies)\b/i.test(text)
|
|
160
|
+
? 'injury'
|
|
161
|
+
: /\btone|concise|demeaning|style\b/i.test(text)
|
|
162
|
+
? 'tone'
|
|
163
|
+
: /\bprefer|preference\b/i.test(text)
|
|
164
|
+
? 'preference'
|
|
165
|
+
: null;
|
|
166
|
+
return { isRemovalIntent, text, kindPreference };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function coachFactRemovalMatches(currentUserText, facts, { limit = 3 } = {}) {
|
|
170
|
+
const intent = detectCoachFactRemovalIntent(currentUserText);
|
|
171
|
+
if (!intent.isRemovalIntent) return [];
|
|
172
|
+
const requestTokens = new Set(intent.text.toLowerCase().match(/[a-z0-9]{4,}/g) ?? []);
|
|
173
|
+
const stopTokens = new Set(['remove', 'delete', 'clear', 'forget', 'that', 'this', 'goal', 'memory', 'fact', 'anymore', 'longer', 'true', 'want']);
|
|
174
|
+
const usefulRequestTokens = [...requestTokens].filter((token) => !stopTokens.has(token));
|
|
175
|
+
const scored = [];
|
|
176
|
+
for (const row of Array.isArray(facts) ? facts : []) {
|
|
177
|
+
const kind = normalizeCoachFactKind(row?.kind);
|
|
178
|
+
const fact = normalizeCoachFactText(row?.fact);
|
|
179
|
+
const id = String(row?.id ?? '').trim();
|
|
180
|
+
if (!id || !fact || !isCoachFactKind(kind) || row?.supersededAt || row?.superseded_at) continue;
|
|
181
|
+
const factTokens = new Set(fact.toLowerCase().match(/[a-z0-9]{4,}/g) ?? []);
|
|
182
|
+
const overlap = usefulRequestTokens.filter((token) => factTokens.has(token)).length;
|
|
183
|
+
const quoted = [...String(intent.text).matchAll(/["“”']([^"“”']{3,80})["“”']/g)]
|
|
184
|
+
.some((match) => fact.toLowerCase().includes(String(match[1]).toLowerCase()));
|
|
185
|
+
const kindScore = intent.kindPreference && intent.kindPreference === kind ? 2 : 0;
|
|
186
|
+
const score = (quoted ? 10 : 0) + overlap + kindScore;
|
|
187
|
+
const hasContentTarget = usefulRequestTokens.length > 0 || /["“”']([^"“”']{3,80})["“”']/.test(intent.text);
|
|
188
|
+
if ((hasContentTarget && (quoted || overlap > 0)) || (!hasContentTarget && kindScore > 0)) {
|
|
189
|
+
scored.push({ row, score });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return scored
|
|
193
|
+
.sort((a, b) => b.score - a.score)
|
|
194
|
+
.slice(0, Math.max(1, Math.min(Number(limit) || 3, 10)))
|
|
195
|
+
.map((item) => item.row);
|
|
196
|
+
}
|
|
197
|
+
|
|
63
198
|
export function normalizeCoachFactCandidate(candidate) {
|
|
64
199
|
if (!candidate || typeof candidate !== 'object') return null;
|
|
65
200
|
const normalized = {
|
|
@@ -74,6 +209,60 @@ export function normalizeCoachFactCandidate(candidate) {
|
|
|
74
209
|
};
|
|
75
210
|
}
|
|
76
211
|
|
|
212
|
+
export function evaluateCoachFactCandidates(candidates, { limit = 3 } = {}) {
|
|
213
|
+
const proposed = Array.isArray(candidates) ? candidates : [];
|
|
214
|
+
const accepted = [];
|
|
215
|
+
const rejected = [];
|
|
216
|
+
|
|
217
|
+
for (const candidate of proposed) {
|
|
218
|
+
const normalized = {
|
|
219
|
+
kind: normalizeCoachFactKind(candidate?.kind),
|
|
220
|
+
fact: normalizeCoachFactText(candidate?.fact),
|
|
221
|
+
confidence: Number(candidate?.confidence ?? 0.7)
|
|
222
|
+
};
|
|
223
|
+
const reason = coachFactPolicyViolation(normalized);
|
|
224
|
+
if (reason) {
|
|
225
|
+
rejected.push({
|
|
226
|
+
kind: isCoachFactKind(normalized.kind) ? normalized.kind : 'unknown',
|
|
227
|
+
reason
|
|
228
|
+
});
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
accepted.push({
|
|
232
|
+
...normalized,
|
|
233
|
+
confidence: Number.isFinite(normalized.confidence) ? Math.max(0, Math.min(1, normalized.confidence)) : 0.7
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const facts = dedupeCoachFactCandidates(accepted, { limit });
|
|
238
|
+
const rejectionReasonCounts = rejected.reduce((counts, item) => {
|
|
239
|
+
counts[item.reason] = (counts[item.reason] ?? 0) + 1;
|
|
240
|
+
return counts;
|
|
241
|
+
}, {});
|
|
242
|
+
const acceptedKindCounts = facts.reduce((counts, item) => {
|
|
243
|
+
counts[item.kind] = (counts[item.kind] ?? 0) + 1;
|
|
244
|
+
return counts;
|
|
245
|
+
}, {});
|
|
246
|
+
const rejectedKindCounts = rejected.reduce((counts, item) => {
|
|
247
|
+
const kind = isCoachFactKind(item.kind) ? item.kind : 'unknown';
|
|
248
|
+
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
249
|
+
return counts;
|
|
250
|
+
}, {});
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
facts,
|
|
254
|
+
telemetry: {
|
|
255
|
+
proposedFactCount: proposed.length,
|
|
256
|
+
acceptedFactCount: facts.length,
|
|
257
|
+
rejectedFactCount: rejected.length,
|
|
258
|
+
acceptedKinds: acceptedKindCounts,
|
|
259
|
+
rejectedKinds: rejectedKindCounts,
|
|
260
|
+
rejectionReasons: rejectionReasonCounts,
|
|
261
|
+
rejectedFacts: rejected
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
77
266
|
export function coachFactConflictKey(candidate) {
|
|
78
267
|
const kind = normalizeCoachFactKind(candidate?.kind);
|
|
79
268
|
const fact = normalizeCoachFactText(candidate?.fact).toLowerCase();
|
|
@@ -111,3 +300,28 @@ export function dedupeCoachFactCandidates(candidates, { limit = 3 } = {}) {
|
|
|
111
300
|
.sort((a, b) => b.confidence - a.confidence)
|
|
112
301
|
.slice(0, limit);
|
|
113
302
|
}
|
|
303
|
+
|
|
304
|
+
export function coachFactsResponsePayload(facts) {
|
|
305
|
+
const rows = Array.isArray(facts) ? facts : [];
|
|
306
|
+
const grouped = Object.fromEntries(COACH_FACT_PRESENTATION.map((section) => [section.key, []]));
|
|
307
|
+
for (const fact of rows) {
|
|
308
|
+
const groupKey = COACH_FACT_GROUP_KEYS[fact?.kind];
|
|
309
|
+
if (groupKey) grouped[groupKey].push(fact);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
facts: rows,
|
|
314
|
+
grouped,
|
|
315
|
+
sections: COACH_FACT_PRESENTATION.map((section) => ({
|
|
316
|
+
key: section.key,
|
|
317
|
+
kind: section.kind,
|
|
318
|
+
title: section.title,
|
|
319
|
+
facts: grouped[section.key] ?? []
|
|
320
|
+
})),
|
|
321
|
+
availableKinds: COACH_FACT_PRESENTATION.map((section) => ({
|
|
322
|
+
kind: section.kind,
|
|
323
|
+
title: section.kindTitle,
|
|
324
|
+
sectionTitle: section.title
|
|
325
|
+
}))
|
|
326
|
+
};
|
|
327
|
+
}
|