@clien-ai/mcp 0.8.1 → 0.10.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/README.md +38 -6
- package/dist/tools/collections.js +308 -5
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/content-type-display.js +166 -0
- package/dist/tools/content-type-display.js.map +1 -0
- package/dist/tools/credits.js +299 -0
- package/dist/tools/credits.js.map +1 -0
- package/dist/tools/interview-scripts.js +370 -0
- package/dist/tools/interview-scripts.js.map +1 -0
- package/dist/tools/interview.js +148 -6
- package/dist/tools/interview.js.map +1 -1
- package/dist/tools/output-schemas.js +47 -0
- package/dist/tools/output-schemas.js.map +1 -1
- package/dist/tools/personas.js +276 -48
- package/dist/tools/personas.js.map +1 -1
- package/dist/tools/registry.js +316 -24
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/report-digest.js +179 -3
- package/dist/tools/report-digest.js.map +1 -1
- package/dist/tools/reports.js +9 -2
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/research.js +102 -5
- package/dist/tools/research.js.map +1 -1
- package/dist/tools/scoped-research.js +270 -42
- package/dist/tools/scoped-research.js.map +1 -1
- package/dist/tools/status.js +2 -2
- package/dist/tools/status.js.map +1 -1
- package/dist/types/report.js +207 -5
- package/dist/types/report.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `list_interview_scripts` (FUL-514) — the questions a run says are worth asking.
|
|
3
|
+
*
|
|
4
|
+
* Before this tool, MCP had the ACTION without the PLAN. An agent could call
|
|
5
|
+
* `interview_persona` but could not first read what the research decided was worth
|
|
6
|
+
* asking, so it either invented questions unanchored to the run's open hypotheses
|
|
7
|
+
* — in which case the answers cannot feed back into the claim spine — or
|
|
8
|
+
* re-derived them from `get_report`'s hypothesis list, which is the FUL-510
|
|
9
|
+
* failure again: an agent recomputing something the run already produced and
|
|
10
|
+
* stored. The web app has handed a human this screen for free since FUL-327.
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ THIS TOOL NEVER GENERATES A SCRIPT. Scripts are the deterministic output of a
|
|
13
|
+
* research run (`agent/src/interview-script.ts`), written into the run's report
|
|
14
|
+
* and immutable thereafter. Deriving them on a miss would make a free read do
|
|
15
|
+
* model work and spend money on a GET, so a project with none gets the honest
|
|
16
|
+
* empty state and nothing else. The wording of that empty state matters as much as
|
|
17
|
+
* the guarantee: "no run has written scripts" is not "there is nothing worth
|
|
18
|
+
* asking".
|
|
19
|
+
*
|
|
20
|
+
* Contract:
|
|
21
|
+
* - list_interview_scripts → GET /api/interview-scripts?projectId →
|
|
22
|
+
* { interviewScripts: <script>[] | null, sourceRun: { id, createdAt } | null }
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ VOCABULARY, and why this differs from `collections.ts`. The two collections
|
|
25
|
+
* and `get_market` read PROMOTED TABLES, so their endpoints return raw snake_case
|
|
26
|
+
* rows and those modules map at the tool boundary. Interview scripts are not a
|
|
27
|
+
* table — they live inside `validation_jobs.report_data.interviewScripts` — so
|
|
28
|
+
* `/api/interview-scripts` returns a shape the app has already mapped, ranked and
|
|
29
|
+
* joined to its hypotheses. The mapper below therefore NORMALISES (type-checks
|
|
30
|
+
* every field, degrades a bad one to null) rather than renames. It is still
|
|
31
|
+
* written out field by field, for the same reason the collection mappers are: the
|
|
32
|
+
* field list is part of the public MCP contract and must be readable in review.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ RANKING AND SELECTION ARE THE SERVER'S. Which run owns a project's scripts
|
|
35
|
+
* (`latestScriptedRunQuery` — newest non-scoped run that actually wrote an array)
|
|
36
|
+
* and what order they come in (`buildRankedScripts` — least-settled verdict first)
|
|
37
|
+
* are decided once, app-side, by the same functions the web screen renders from.
|
|
38
|
+
* Re-sorting here is what would let the two surfaces disagree about which
|
|
39
|
+
* hypothesis is most worth a real answer.
|
|
40
|
+
*
|
|
41
|
+
* Everything else follows its siblings: the shared `apiCall` client, the shared
|
|
42
|
+
* 401/404/400 mapping, and the rule that "I could not read the answer" and "the
|
|
43
|
+
* answer is none" must never look the same. Read-only, so a timeout is always safe
|
|
44
|
+
* to retry — no work lost, no credit spent.
|
|
45
|
+
*/
|
|
46
|
+
import { z } from 'zod';
|
|
47
|
+
import { CollectionToolError, formatZodError, makeCtx, throwForResponse, } from './collections.js';
|
|
48
|
+
import { apiCall } from './research.js';
|
|
49
|
+
import { safeId, safeInline, truncate, collapseWhitespace } from './render-safety.js';
|
|
50
|
+
const SCRIPTS_FETCH_TIMEOUT_MS = 30_000;
|
|
51
|
+
export const ListInterviewScriptsInputSchema = z.object({
|
|
52
|
+
project_id: z
|
|
53
|
+
.string()
|
|
54
|
+
.uuid()
|
|
55
|
+
.describe('UUID of the project whose interview scripts to read. Get it from ' +
|
|
56
|
+
'`list_projects` (or `create_project`). Rejected with a 404 if you do not own the ' +
|
|
57
|
+
'project. Returns the STORED OUTPUT OF ONE SPECIFIC RESEARCH RUN — the scripts that ' +
|
|
58
|
+
'run wrote when it ran, one per hypothesis, ranked least-settled first — not a live ' +
|
|
59
|
+
'suggestion and not something generated when you call this. The run is named in the ' +
|
|
60
|
+
'result; pass its id to `get_report` for the hypotheses behind the questions. A ' +
|
|
61
|
+
'project can legitimately have NO scripts; that is reported as "no run has written ' +
|
|
62
|
+
'any", never as "there is nothing worth asking".'),
|
|
63
|
+
});
|
|
64
|
+
function asText(value) {
|
|
65
|
+
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A 0–1 confidence, or null.
|
|
69
|
+
*
|
|
70
|
+
* Out-of-range and non-finite numbers become null rather than being printed. The
|
|
71
|
+
* app drops them for the same reason at its own render seam: a percentage derived
|
|
72
|
+
* from 999 reads as a measurement, and this tool has no way to caption it as
|
|
73
|
+
* nonsense in the one line it gets.
|
|
74
|
+
*/
|
|
75
|
+
function asFraction(value) {
|
|
76
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
77
|
+
return null;
|
|
78
|
+
return value >= 0 && value <= 1 ? value : null;
|
|
79
|
+
}
|
|
80
|
+
export function toInterviewScriptEntry(row) {
|
|
81
|
+
return {
|
|
82
|
+
hypothesisId: asText(row.hypothesisId),
|
|
83
|
+
statement: asText(row.statement),
|
|
84
|
+
status: asText(row.status),
|
|
85
|
+
verdictLabel: asText(row.verdictLabel),
|
|
86
|
+
confidence: asFraction(row.confidence),
|
|
87
|
+
whoToAsk: asText(row.whoToAsk),
|
|
88
|
+
// A drifted null (or a string where an array belongs) costs the questions,
|
|
89
|
+
// never the script: the hypothesis it names is still worth reporting.
|
|
90
|
+
questions: Array.isArray(row.questions)
|
|
91
|
+
? row.questions.filter((q) => typeof q === 'string')
|
|
92
|
+
: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function toInterviewScriptsSourceRun(row) {
|
|
96
|
+
return { id: asText(row.id), createdAt: asText(row.createdAt) };
|
|
97
|
+
}
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Render caps
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
/**
|
|
102
|
+
* Upper bound on scripts RENDERED, matching `report-digest.ts`'s `LIST_RENDER_CAP`
|
|
103
|
+
* for personas and hypotheses. A run writes one script per hypothesis and the
|
|
104
|
+
* producer's hypothesis cap is far below this, so it should never fire — but an
|
|
105
|
+
* unfired cap that would truncate silently is the thing this package refuses.
|
|
106
|
+
* When it fires it says so, and says where the rest are.
|
|
107
|
+
*/
|
|
108
|
+
const SCRIPT_RENDER_CAP = 25;
|
|
109
|
+
/**
|
|
110
|
+
* Questions rendered per script. The generator writes exactly 3; the cap exists
|
|
111
|
+
* for a report written by some other build, and announces itself the same way.
|
|
112
|
+
*/
|
|
113
|
+
const QUESTION_RENDER_CAP = 12;
|
|
114
|
+
const STATEMENT_MAX = 240;
|
|
115
|
+
const WHO_TO_ASK_MAX = 240;
|
|
116
|
+
const QUESTION_MAX = 300;
|
|
117
|
+
const VERDICT_MAX = 40;
|
|
118
|
+
const TIMESTAMP_MAX = 40;
|
|
119
|
+
/**
|
|
120
|
+
* GET /api/interview-scripts for one project.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ THE NULL AND THE THROW ARE DIFFERENT ANSWERS, and the difference is decided by
|
|
123
|
+
* KEY PRESENCE, not truthiness. `{ "interviewScripts": null }` is the endpoint
|
|
124
|
+
* saying "no run has written scripts for this project" — a real, quotable fact. A
|
|
125
|
+
* 200 whose body has no `interviewScripts` key at all (an HTML error page from a
|
|
126
|
+
* proxy, a truncated response, a reshaped envelope) is the endpoint saying nothing,
|
|
127
|
+
* and must not arrive at an agent wearing the empty state's words: that empty state
|
|
128
|
+
* ends with "run `clien_research`", which DEBITS CREDITS. A transport failure read
|
|
129
|
+
* as "this project has no plan" can cost the user real money.
|
|
130
|
+
*/
|
|
131
|
+
async function fetchInterviewScripts(deps, projectId) {
|
|
132
|
+
const action = 'read the interview scripts';
|
|
133
|
+
const ctx = makeCtx(deps);
|
|
134
|
+
const query = new URLSearchParams({ projectId });
|
|
135
|
+
let response;
|
|
136
|
+
try {
|
|
137
|
+
response = await apiCall(ctx, 'GET', `/api/interview-scripts?${query.toString()}`, {
|
|
138
|
+
timeoutMs: SCRIPTS_FETCH_TIMEOUT_MS,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const e = err;
|
|
143
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError') {
|
|
144
|
+
throw new CollectionToolError(`Timed out after ${SCRIPTS_FETCH_TIMEOUT_MS / 1000}s trying to ${action}. ` +
|
|
145
|
+
'Try again in a moment — this is a read, so nothing was spent and nothing was lost.');
|
|
146
|
+
}
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
if (!response.ok)
|
|
150
|
+
await throwForResponse(response, deps, action, projectId);
|
|
151
|
+
const json = (await response.json().catch(() => null));
|
|
152
|
+
const unreadable = new CollectionToolError(`Could not ${action} — the server returned a 200 with an unreadable body ` +
|
|
153
|
+
'(no `interviewScripts` key). This is NOT "the project has no interview scripts": the ' +
|
|
154
|
+
'request failed and scripts may well exist. Retry — this is a read, so nothing was spent ' +
|
|
155
|
+
'and nothing was lost. Do not tell anyone this project has no interview plan on the ' +
|
|
156
|
+
'strength of this, and do not start a paid research run because of it.');
|
|
157
|
+
if (json === null ||
|
|
158
|
+
typeof json !== 'object' ||
|
|
159
|
+
Array.isArray(json) ||
|
|
160
|
+
!('interviewScripts' in json)) {
|
|
161
|
+
throw unreadable;
|
|
162
|
+
}
|
|
163
|
+
const rawRun = json.sourceRun;
|
|
164
|
+
const sourceRun = rawRun !== null && typeof rawRun === 'object' && !Array.isArray(rawRun)
|
|
165
|
+
? toInterviewScriptsSourceRun(rawRun)
|
|
166
|
+
: null;
|
|
167
|
+
const raw = json.interviewScripts;
|
|
168
|
+
if (raw === null)
|
|
169
|
+
return { scripts: null, sourceRun };
|
|
170
|
+
if (!Array.isArray(raw))
|
|
171
|
+
throw unreadable;
|
|
172
|
+
const scripts = raw
|
|
173
|
+
.filter((entry) => entry !== null && typeof entry === 'object' && !Array.isArray(entry))
|
|
174
|
+
.map(toInterviewScriptEntry);
|
|
175
|
+
return { scripts, sourceRun };
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Render
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* The provenance sentence, in the TEXT channel because that is the one an agent
|
|
182
|
+
* always receives (FUL-243: `_meta` is a mirror, Claude Code drops it).
|
|
183
|
+
*
|
|
184
|
+
* A run id this client cannot state EXACTLY gets its own sentence rather than
|
|
185
|
+
* silence. Silence would read as "there is nothing to say about where these came
|
|
186
|
+
* from", when the truth is that the provenance is unknown — and a question set
|
|
187
|
+
* with unknown provenance is exactly the one an agent must not attribute to
|
|
188
|
+
* "the research".
|
|
189
|
+
*/
|
|
190
|
+
function provenanceLine(runId, createdAt) {
|
|
191
|
+
const safe = safeId(runId);
|
|
192
|
+
const when = safeInline(createdAt, TIMESTAMP_MAX);
|
|
193
|
+
if (!safe) {
|
|
194
|
+
return ('PROVENANCE: these scripts name NO source run — either the response omitted it or the ' +
|
|
195
|
+
'run behind them has since been deleted. Report them as recorded, never as "run X ' +
|
|
196
|
+
'recommends asking".');
|
|
197
|
+
}
|
|
198
|
+
return (`PROVENANCE: every script above was written by research run ${safe}` +
|
|
199
|
+
(when ? ` (recorded ${when})` : '') +
|
|
200
|
+
'. A report is an immutable snapshot, so the verdicts beside each question are that run\'s, ' +
|
|
201
|
+
`not today's — call \`get_report\` with job_id=${safe} for the hypotheses behind them.`);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The linkage instruction.
|
|
205
|
+
*
|
|
206
|
+
* ⚠️ It names PARAMETERS now, not a thing to remember (FUL-577). This line used to
|
|
207
|
+
* ask the agent to carry `hypothesisId` forward by hand, because `interview_persona`
|
|
208
|
+
* took no hypothesis id and `interviews` had no column for one — the strongest thing
|
|
209
|
+
* a read-only tool could do, and still a written instruction rather than a stored
|
|
210
|
+
* relation. `interview_persona action:start` now accepts the pair and the row keeps
|
|
211
|
+
* it, so telling an agent to remember something it can instead write down would be
|
|
212
|
+
* the worst of both.
|
|
213
|
+
*
|
|
214
|
+
* BOTH halves, because an id alone is ambiguous: hypotheses live inside a run's
|
|
215
|
+
* `report_data`, so the same `H3` recurs across runs of one project meaning
|
|
216
|
+
* different things. That is why `sourceRun.id` is named here and not merely
|
|
217
|
+
* available in the provenance line.
|
|
218
|
+
*/
|
|
219
|
+
const LINKAGE_LINE = 'LINKAGE: each script names the `hypothesisId` it was written to test. Pass it to ' +
|
|
220
|
+
'`interview_persona action:start` as `hypothesis_id`, together with this call\'s ' +
|
|
221
|
+
'`sourceRun.id` as `hypothesis_run_id`, and the interview RECORDS which hypothesis it tested — ' +
|
|
222
|
+
'you no longer have to carry the id through the conversation, and `get_interview` / ' +
|
|
223
|
+
'`list_interviews` hand it back. Both parameters or neither: an id is only meaningful against ' +
|
|
224
|
+
'the run that defined it.';
|
|
225
|
+
/**
|
|
226
|
+
* The trust caveat.
|
|
227
|
+
*
|
|
228
|
+
* A verdict printed beside a question reads as settled unless something says
|
|
229
|
+
* otherwise (FUL-247). These verdicts are the run's reading of its own hypothesis,
|
|
230
|
+
* not the grading of a claim: the per-claim GROUNDED / SPECULATION / NO_RECEIPT
|
|
231
|
+
* states live in the source run's claim spine, which this separately released
|
|
232
|
+
* client cannot reconstruct.
|
|
233
|
+
*/
|
|
234
|
+
const SCRIPTS_TRUST_LINE = 'TRUST: the verdict and confidence on each script are the source run\'s reading of that ' +
|
|
235
|
+
'hypothesis, not a graded claim. Nothing here carries a GROUNDED / SPECULATION / NO_RECEIPT ' +
|
|
236
|
+
'state — read the source run with `get_report` for those. A high confidence is a reason to ask ' +
|
|
237
|
+
'a different question, never a reason to skip asking.';
|
|
238
|
+
const CHANNEL_NOTE = 'The full record is in `structuredContent.interviewScripts` (also mirrored in `_meta`).';
|
|
239
|
+
/** `Weak signal (inconclusive), confidence 52%` — or the honest partial forms. */
|
|
240
|
+
function verdictClause(script) {
|
|
241
|
+
const label = safeInline(script.verdictLabel, VERDICT_MAX);
|
|
242
|
+
const status = safeInline(script.status, VERDICT_MAX);
|
|
243
|
+
const confidence = script.confidence === null ? null : `confidence ${Math.round(script.confidence * 100)}%`;
|
|
244
|
+
const verdict = label && status ? `${label} (${status})` : label ?? status ?? 'verdict not recorded';
|
|
245
|
+
return confidence ? `${verdict}, ${confidence}` : `${verdict}, confidence not recorded`;
|
|
246
|
+
}
|
|
247
|
+
/** The question list for one script, capped and — when capped — saying so. */
|
|
248
|
+
function questionBlock(questions) {
|
|
249
|
+
const flat = questions
|
|
250
|
+
.map((q) => collapseWhitespace(typeof q === 'string' ? q : ''))
|
|
251
|
+
.filter((q) => q.length > 0);
|
|
252
|
+
// Branch on the RENDERED questions, not on `questions.length`: `['']` has
|
|
253
|
+
// length 1 and collapses to nothing, and a "Questions:" header over no
|
|
254
|
+
// questions reads as a rendering bug rather than as a script that lost them.
|
|
255
|
+
if (flat.length === 0) {
|
|
256
|
+
return ' Questions: none readable in the stored script.';
|
|
257
|
+
}
|
|
258
|
+
const shown = flat.slice(0, QUESTION_RENDER_CAP);
|
|
259
|
+
const lines = shown.map((q, i) => ` ${i + 1}. ${truncate(q, QUESTION_MAX)}`);
|
|
260
|
+
const dropped = flat.length - shown.length;
|
|
261
|
+
const more = dropped > 0
|
|
262
|
+
? `\n (${dropped} more in \`structuredContent.interviewScripts\`)`
|
|
263
|
+
: '';
|
|
264
|
+
return ` Questions:\n${lines.join('\n')}${more}`;
|
|
265
|
+
}
|
|
266
|
+
/** One ranked script as its own block. */
|
|
267
|
+
function renderScript(script, rank) {
|
|
268
|
+
const id = safeId(script.hypothesisId);
|
|
269
|
+
const statement = safeInline(script.statement, STATEMENT_MAX);
|
|
270
|
+
const whoToAsk = safeInline(script.whoToAsk, WHO_TO_ASK_MAX);
|
|
271
|
+
// The heading names the hypothesis, because a question set detached from the
|
|
272
|
+
// hypothesis it serves is the thing this tool exists to stop being handed out.
|
|
273
|
+
const heading = statement
|
|
274
|
+
? `${rank}. ${statement}`
|
|
275
|
+
: `${rank}. (this run recorded no statement for the hypothesis this script serves)`;
|
|
276
|
+
const lines = [
|
|
277
|
+
heading,
|
|
278
|
+
id
|
|
279
|
+
? ` Hypothesis: ${id} — ${verdictClause(script)}`
|
|
280
|
+
: ` Hypothesis: NOT IDENTIFIED — ${verdictClause(script)}. Without an id these answers ` +
|
|
281
|
+
'cannot be tied back to a hypothesis.',
|
|
282
|
+
];
|
|
283
|
+
if (whoToAsk)
|
|
284
|
+
lines.push(` Who to ask: ${whoToAsk}`);
|
|
285
|
+
lines.push(questionBlock(script.questions));
|
|
286
|
+
return lines.join('\n');
|
|
287
|
+
}
|
|
288
|
+
export async function listInterviewScripts(input, deps) {
|
|
289
|
+
const parsed = ListInterviewScriptsInputSchema.safeParse(input);
|
|
290
|
+
if (!parsed.success) {
|
|
291
|
+
throw new CollectionToolError(`Invalid input — ${formatZodError(parsed.error)}`);
|
|
292
|
+
}
|
|
293
|
+
const { project_id: projectId } = parsed.data;
|
|
294
|
+
const { scripts, sourceRun } = await fetchInterviewScripts(deps, projectId);
|
|
295
|
+
let text;
|
|
296
|
+
if (scripts === null) {
|
|
297
|
+
// ⚠️ THE HONEST EMPTY STATE. "Absent" is not "settled" and it is not "we looked
|
|
298
|
+
// and there was nothing worth asking". A scoped scan or a failed run leaves a
|
|
299
|
+
// project with no scripts while its hypotheses are wide open, so this sentence
|
|
300
|
+
// has to say that nothing is on record rather than imply a finding.
|
|
301
|
+
text =
|
|
302
|
+
`No interview scripts recorded for project ${projectId}.\n\n` +
|
|
303
|
+
'This means NO completed research run has written interview scripts for this project. It ' +
|
|
304
|
+
'is NOT a finding about the hypotheses: not "they are settled", not "there is nothing ' +
|
|
305
|
+
'worth asking", not zero. There is simply nothing on record.\n\n' +
|
|
306
|
+
'This tool never generates scripts — it reads what a run stored, and it will keep ' +
|
|
307
|
+
'returning this until one exists. Run `clien_research` (a full run) to produce them. ' +
|
|
308
|
+
'`scan_competitors` and `search_forums` are scoped runs and never write scripts, so ' +
|
|
309
|
+
'running those will leave this empty.';
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const runId = safeId(sourceRun?.id ?? null);
|
|
313
|
+
const when = safeInline(sourceRun?.createdAt ?? null, TIMESTAMP_MAX);
|
|
314
|
+
if (scripts.length === 0) {
|
|
315
|
+
// ⚠️ A DIFFERENT FACT FROM THE `null` BRANCH. A run IS on record and it did
|
|
316
|
+
// write a scripts array — the endpoint only names a run that did. Nothing in
|
|
317
|
+
// it could be read, which is a defect in the stored report, not a statement
|
|
318
|
+
// about the hypotheses. Collapsing the two would tell a caller no research
|
|
319
|
+
// has been done when some has.
|
|
320
|
+
text =
|
|
321
|
+
`Project ${projectId} has a research run that wrote interview scripts, and none of ` +
|
|
322
|
+
'them could be read.\n\n' +
|
|
323
|
+
(runId
|
|
324
|
+
? `Run ${runId}${when ? ` (recorded ${when})` : ''} stored a scripts array whose ` +
|
|
325
|
+
'entries are unusable — a malformed report, not an answer about the hypotheses. '
|
|
326
|
+
: 'The run that wrote them stored a scripts array whose entries are unusable — a ' +
|
|
327
|
+
'malformed report, not an answer about the hypotheses. ') +
|
|
328
|
+
'Do not treat this as "nothing worth asking". Read the run itself with `get_report` for ' +
|
|
329
|
+
'its hypotheses, or run `clien_research` again to write a fresh set.\n\n' +
|
|
330
|
+
provenanceLine(sourceRun?.id ?? null, sourceRun?.createdAt ?? null) +
|
|
331
|
+
'\n\n' +
|
|
332
|
+
CHANNEL_NOTE;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
const shown = scripts.slice(0, SCRIPT_RENDER_CAP);
|
|
336
|
+
const dropped = scripts.length - shown.length;
|
|
337
|
+
const capNote = dropped > 0
|
|
338
|
+
? `\n\n(${dropped} more script${dropped === 1 ? '' : 's'} in ` +
|
|
339
|
+
'`structuredContent.interviewScripts`.)'
|
|
340
|
+
: '';
|
|
341
|
+
text =
|
|
342
|
+
`Interview scripts for project ${projectId} — ${scripts.length} script` +
|
|
343
|
+
`${scripts.length === 1 ? '' : 's'}` +
|
|
344
|
+
`${runId ? ` from research run ${runId}` : ''}${when ? ` (recorded ${when})` : ''}.\n\n` +
|
|
345
|
+
'These are the STORED output of that run, written when it ran. Nothing was generated to ' +
|
|
346
|
+
'answer this call.\n\n' +
|
|
347
|
+
'RANKED least-settled first: the hypothesis nearest a coin flip goes first, because a ' +
|
|
348
|
+
'real answer there moves the verdict most. That order is the run\'s, not this ' +
|
|
349
|
+
'client\'s.\n\n' +
|
|
350
|
+
shown.map((script, i) => renderScript(script, i + 1)).join('\n\n') +
|
|
351
|
+
capNote +
|
|
352
|
+
'\n\n' +
|
|
353
|
+
LINKAGE_LINE +
|
|
354
|
+
'\n\n' +
|
|
355
|
+
provenanceLine(sourceRun?.id ?? null, sourceRun?.createdAt ?? null) +
|
|
356
|
+
'\n\n' +
|
|
357
|
+
SCRIPTS_TRUST_LINE +
|
|
358
|
+
'\n\n' +
|
|
359
|
+
CHANNEL_NOTE;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
content: [{ type: 'text', text }],
|
|
364
|
+
// Both keys are present even when null — the PRESENCE is what tells a
|
|
365
|
+
// structured consumer "no run has written scripts" apart from "this tool did
|
|
366
|
+
// not answer".
|
|
367
|
+
_meta: { interviewScripts: scripts, sourceRun },
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
//# sourceMappingURL=interview-scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interview-scripts.js","sourceRoot":"","sources":["../../src/tools/interview-scripts.ts"],"names":[],"mappings":"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"}
|
package/dist/tools/interview.js
CHANGED
|
@@ -41,8 +41,26 @@ import { z } from 'zod';
|
|
|
41
41
|
import { ToolError } from './errors.js';
|
|
42
42
|
import { deprecatedAlias } from './param-aliases.js';
|
|
43
43
|
import { apiCall, categorizeFetchError, } from './research.js';
|
|
44
|
-
import { safeBlock, safeInline, safeId, truncate } from './render-safety.js';
|
|
44
|
+
import { safeBlock, safeInline, safeId, truncate, ID_MAX } from './render-safety.js';
|
|
45
45
|
const INTERVIEW_FETCH_TIMEOUT_MS = 60_000;
|
|
46
|
+
/**
|
|
47
|
+
* The shape a `hypothesis_id` must have to be storable (FUL-577).
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ It is `safeId`'s rule, restated as a Zod check rather than re-derived — the
|
|
50
|
+
* cap IS `ID_MAX` and the character class is the one `safeId` rejects on. That is
|
|
51
|
+
* not a coincidence to be tidied away: `safeId` is what decides whether the value
|
|
52
|
+
* can ever be handed BACK to an agent (it returns the input byte-identical or
|
|
53
|
+
* `null`, never a clipped third thing), so accepting an id it would refuse means
|
|
54
|
+
* writing something no reader can be shown. Rejecting it at the input boundary is
|
|
55
|
+
* the difference between a clear parameter error and a value that silently renders
|
|
56
|
+
* as nothing on every later read.
|
|
57
|
+
*
|
|
58
|
+
* The app enforces the same rule at `HYPOTHESIS_ID_REGEX` in
|
|
59
|
+
* `app/lib/validation/schemas.ts`, and the database again as a CHECK. This copy is
|
|
60
|
+
* the one that gives the agent the error in the turn it made the mistake.
|
|
61
|
+
*/
|
|
62
|
+
const HYPOTHESIS_ID_MAX = ID_MAX;
|
|
63
|
+
const HYPOTHESIS_ID_REGEX = /^[^\s\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+$/u;
|
|
46
64
|
/** Upper bound on a turn's question — matches the server's `MESSAGE_CONTENT_MAX`. */
|
|
47
65
|
const CONTENT_MAX = 10_000;
|
|
48
66
|
/** FUL-233: legacy camelCase spellings `interview_persona` still accepts. */
|
|
@@ -94,6 +112,26 @@ export const InterviewPersonaInputSchema = z.object({
|
|
|
94
112
|
.describe('action=start: set true for a scratch/throwaway interview that should NOT count toward the ' +
|
|
95
113
|
'project\'s aggregate insights (sets include_in_insights=false). Leave false (the default) ' +
|
|
96
114
|
'for a real interview you want reflected in project-level analysis.'),
|
|
115
|
+
hypothesis_id: z
|
|
116
|
+
.string()
|
|
117
|
+
.max(HYPOTHESIS_ID_MAX)
|
|
118
|
+
.regex(HYPOTHESIS_ID_REGEX, 'hypothesis_id must be a single token with no spaces')
|
|
119
|
+
.optional()
|
|
120
|
+
.describe('action=start: the `hypothesisId` of the `list_interview_scripts` script you are about to ' +
|
|
121
|
+
'ask, RECORDED ON THE INTERVIEW. Send it with `hypothesis_run_id` (that call\'s ' +
|
|
122
|
+
'`sourceRun.id`) or not at all — an id alone is ambiguous, because the same "H3" recurs ' +
|
|
123
|
+
'across runs of one project meaning different things. Optional: omit both for an ' +
|
|
124
|
+
'interview that tests no particular hypothesis. Once stored it comes back on ' +
|
|
125
|
+
'`get_interview` and `list_interviews`, so the answers stay tied to the hypothesis ' +
|
|
126
|
+
'without you carrying the id through the conversation.'),
|
|
127
|
+
hypothesis_run_id: z
|
|
128
|
+
.string()
|
|
129
|
+
.uuid()
|
|
130
|
+
.optional()
|
|
131
|
+
.describe('action=start: the research run that defined `hypothesis_id` — `sourceRun.id` from the same ' +
|
|
132
|
+
'`list_interview_scripts` call. Required whenever `hypothesis_id` is sent, and refused ' +
|
|
133
|
+
'without it. Recorded as provenance, NOT checked against the run: a run can be deleted ' +
|
|
134
|
+
'and the interview outlives it.'),
|
|
97
135
|
// --- Deprecated camelCase aliases (FUL-233) — rewritten before parse. ---
|
|
98
136
|
projectId: deprecatedAlias('project_id', z.string().uuid()),
|
|
99
137
|
personaId: deprecatedAlias('persona_id', z.string().uuid()),
|
|
@@ -112,8 +150,8 @@ function makeCtx(deps) {
|
|
|
112
150
|
* insufficient-credit stop with balance/required/buy-url guidance; 401 drops the
|
|
113
151
|
* cached session (so the next tool call rebuilds from disk) and surfaces a
|
|
114
152
|
* re-auth message; 404 means the interview/project isn't owned by the caller;
|
|
115
|
-
*
|
|
116
|
-
* generic HTTP error.
|
|
153
|
+
* 403 with a lock code surfaces the upgrade path; 400 surfaces the server's
|
|
154
|
+
* validation detail; everything else surfaces a generic HTTP error.
|
|
117
155
|
*
|
|
118
156
|
* FUL-161: the body is read and parsed ONCE up front rather than after the 404
|
|
119
157
|
* branch, because the 402 branch needs its `balance`/`required`/`buy_url` and a
|
|
@@ -159,6 +197,22 @@ async function throwForResponse(response, deps, action, options) {
|
|
|
159
197
|
deps.invalidateSession?.();
|
|
160
198
|
throw new InterviewToolError('Session expired. Re-run any Clien.ai tool to re-authenticate, then try again.');
|
|
161
199
|
}
|
|
200
|
+
// FUL-503. Without this branch a subscription lock reads as
|
|
201
|
+
// `Failed to start interview (HTTP 403)` — indistinguishable from a bug, so the
|
|
202
|
+
// agent retries something no retry can fix. Mirrors `personas.ts`, including
|
|
203
|
+
// naming the resource that is actually locked.
|
|
204
|
+
//
|
|
205
|
+
// FUL-553 widened where these codes come from: `start` was the only action that
|
|
206
|
+
// could produce them, and now `turn` and `complete`/`reopen` do too, because a
|
|
207
|
+
// locked project is read-only IN FULL rather than merely uncreatable-in. The
|
|
208
|
+
// copy is already action-parameterised, so it needed no change — but note that
|
|
209
|
+
// a PROJECT_LOCKED on a `turn` refers to an interview that DOES exist and is
|
|
210
|
+
// simply frozen, unlike the same code on a `start`.
|
|
211
|
+
if (status === 403 && (parsed?.code === 'PROJECT_LOCKED' || parsed?.code === 'PERSONA_LOCKED')) {
|
|
212
|
+
throw new InterviewToolError(parsed.code === 'PROJECT_LOCKED'
|
|
213
|
+
? `Could not ${action} — the project is locked due to subscription limits and is read-only. Upgrade to Pro to change it.`
|
|
214
|
+
: `Could not ${action} — the persona is locked due to subscription limits. Upgrade to Pro to interview it.`);
|
|
215
|
+
}
|
|
162
216
|
if (status === 404) {
|
|
163
217
|
throw new InterviewToolError(options?.notFound ??
|
|
164
218
|
`Could not ${action} — interview or project not found, or you don't own it. ` +
|
|
@@ -210,6 +264,22 @@ function formatTranscript(interview) {
|
|
|
210
264
|
// `list_interviews` already renders the id, so its absence here was pure skew.
|
|
211
265
|
const personaId = safeId(interview.personaId);
|
|
212
266
|
const persona = personaId ? `\npersona_id: ${personaId}` : '';
|
|
267
|
+
// FUL-577: the hypothesis this interview was written to test, on the surface an
|
|
268
|
+
// agent is most likely to read back or relay. Before this, the tie between these
|
|
269
|
+
// answers and the hypothesis they test existed only in the calling agent's
|
|
270
|
+
// memory, which is exactly what made it unreportable by anyone else.
|
|
271
|
+
//
|
|
272
|
+
// `safeId` on BOTH halves, and printed only when both survive. The run id is what
|
|
273
|
+
// makes the hypothesis id mean anything, so an id whose run could not be rendered
|
|
274
|
+
// would be an unresolvable reference presented as provenance — worse than
|
|
275
|
+
// silence. `safeId` is exact-or-nothing, so a poisoned value degrades to this
|
|
276
|
+
// same branch rather than minting a header line (FUL-247/253).
|
|
277
|
+
const hypothesisId = safeId(interview.hypothesisId);
|
|
278
|
+
const hypothesisRunId = safeId(interview.hypothesisRunId);
|
|
279
|
+
const hypothesis = hypothesisId && hypothesisRunId
|
|
280
|
+
? `\nhypothesis: ${hypothesisId} (from research run ${hypothesisRunId} — ` +
|
|
281
|
+
'call `get_report` with that job_id for the hypothesis behind the questions)'
|
|
282
|
+
: '';
|
|
213
283
|
// FUL-335 — the disclosure travels WITH the transcript, not just on the tool
|
|
214
284
|
// description. A transcript is the one result an agent is likely to relay or
|
|
215
285
|
// paste wholesale, and the `Persona:` label above names a ROLE, not a nature:
|
|
@@ -221,18 +291,35 @@ function formatTranscript(interview) {
|
|
|
221
291
|
'person. These replies are simulated research signal; never relay them as human testimony.';
|
|
222
292
|
const header = `Interview ${safeId(interview.id) ?? '?'} — ` +
|
|
223
293
|
`status: ${safeInline(interview.status, 40) ?? 'unknown'}, ` +
|
|
224
|
-
`${messages.length} message${messages.length === 1 ? '' : 's'}.${persona}${disclosure}`;
|
|
294
|
+
`${messages.length} message${messages.length === 1 ? '' : 's'}.${persona}${hypothesis}${disclosure}`;
|
|
225
295
|
return lines.length > 0 ? `${header}\n\n${lines.join('\n\n')}` : header;
|
|
226
296
|
}
|
|
227
297
|
async function startInterview(input, ctx, deps) {
|
|
228
298
|
if (!input.project_id || !input.persona_id) {
|
|
229
299
|
throw new InterviewToolError("action 'start' requires project_id and persona_id.");
|
|
230
300
|
}
|
|
301
|
+
// FUL-577: both or neither, refused here rather than by a 400 from the server.
|
|
302
|
+
// An id with no run is the ambiguity the linkage exists to remove, and an agent
|
|
303
|
+
// that gets the refusal in the turn it made the mistake can fix it; one that gets
|
|
304
|
+
// a generic `400 Invalid request` from an endpoint it cannot see usually retries
|
|
305
|
+
// the same call.
|
|
306
|
+
if ((input.hypothesis_id === undefined) !== (input.hypothesis_run_id === undefined)) {
|
|
307
|
+
throw new InterviewToolError("action 'start' takes hypothesis_id and hypothesis_run_id TOGETHER or neither — a " +
|
|
308
|
+
'hypothesis id is only meaningful against the run that defined it. Both come from one ' +
|
|
309
|
+
'`list_interview_scripts` call: the script\'s `hypothesisId`, and that call\'s ' +
|
|
310
|
+
'`sourceRun.id`.');
|
|
311
|
+
}
|
|
231
312
|
const response = await call(ctx, 'POST', '/api/interviews', 'start interview', {
|
|
232
313
|
projectId: input.project_id,
|
|
233
314
|
personaId: input.persona_id,
|
|
234
315
|
withFirstMessage: true,
|
|
235
316
|
ephemeral: input.ephemeral,
|
|
317
|
+
// Sent only when named. An older app deploy's zod schema STRIPS keys it does
|
|
318
|
+
// not know, so sending `undefined` and sending nothing are the same request —
|
|
319
|
+
// which is precisely why the echo below is checked rather than assumed.
|
|
320
|
+
...(input.hypothesis_id !== undefined
|
|
321
|
+
? { hypothesisId: input.hypothesis_id, hypothesisRunId: input.hypothesis_run_id }
|
|
322
|
+
: {}),
|
|
236
323
|
});
|
|
237
324
|
if (!response.ok)
|
|
238
325
|
await throwForResponse(response, deps, 'start interview');
|
|
@@ -255,13 +342,56 @@ async function startInterview(input, ctx, deps) {
|
|
|
255
342
|
content: [
|
|
256
343
|
{
|
|
257
344
|
type: 'text',
|
|
258
|
-
text: `Started interview ${safeId(interview.id) ?? 'unknown'}${who}.${insightsNote}
|
|
345
|
+
text: `Started interview ${safeId(interview.id) ?? 'unknown'}${who}.${insightsNote}` +
|
|
346
|
+
`${hypothesisNote(input, interview)}${greeting}\n` +
|
|
259
347
|
"Ask questions with action='turn'; close it with action='complete' when done.",
|
|
260
348
|
},
|
|
261
349
|
],
|
|
262
350
|
_meta: { interview, personaName: json?.personaName, firstMessageContent: json?.firstMessageContent },
|
|
263
351
|
};
|
|
264
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* What `start` says about the hypothesis linkage — CONFIRMED FROM THE ECHO, never
|
|
355
|
+
* from the fact that we sent it (FUL-577).
|
|
356
|
+
*
|
|
357
|
+
* ⚠️ This is the version-skew seam that actually costs something here, and it is on
|
|
358
|
+
* the WRITE. `@clien-ai/mcp` ships to npm on its own cadence and routinely meets an
|
|
359
|
+
* app deploy older than itself; that deploy's Zod body schema does not know
|
|
360
|
+
* `hypothesisId`, and Zod STRIPS unknown keys rather than rejecting them. So the
|
|
361
|
+
* request succeeds, the interview is created, nothing anywhere reports a problem —
|
|
362
|
+
* and the linkage this issue exists to store was silently dropped. An agent told
|
|
363
|
+
* "recorded" then reports its answers as anchored to a hypothesis no row mentions,
|
|
364
|
+
* which is a worse outcome than the prose instruction this replaced: at least that
|
|
365
|
+
* one was honest about being unenforced.
|
|
366
|
+
*
|
|
367
|
+
* Comparing the round trip is the only check that distinguishes the two, and it
|
|
368
|
+
* costs nothing — the created interview is already in the response. Silence when
|
|
369
|
+
* the caller asked for a linkage is not an option: it reads as success.
|
|
370
|
+
*/
|
|
371
|
+
function hypothesisNote(input, interview) {
|
|
372
|
+
if (input.hypothesis_id === undefined)
|
|
373
|
+
return '';
|
|
374
|
+
const storedId = interview.hypothesisId;
|
|
375
|
+
const storedRunId = interview.hypothesisRunId;
|
|
376
|
+
// ⚠️ The run id is compared CASE-INSENSITIVELY. Postgres renders a `uuid` lowercase
|
|
377
|
+
// whatever case it was written in, while both this schema and the route's
|
|
378
|
+
// `UUID_REGEX` accept an uppercase one — so an agent that pasted `9F2…` would get the
|
|
379
|
+
// loud "not recorded" warning about a linkage that stored perfectly. The hypothesis id
|
|
380
|
+
// is `text` and echoes back byte for byte, so it stays an exact comparison.
|
|
381
|
+
const runIdEchoed = storedRunId != null &&
|
|
382
|
+
input.hypothesis_run_id != null &&
|
|
383
|
+
storedRunId.toLowerCase() === input.hypothesis_run_id.toLowerCase();
|
|
384
|
+
if (storedId === input.hypothesis_id && runIdEchoed) {
|
|
385
|
+
return (` Recorded against hypothesis ${safeId(storedId) ?? 'unknown'} from research run ` +
|
|
386
|
+
`${safeId(storedRunId) ?? 'unknown'} — the answers stay tied to it, so you do not have to ` +
|
|
387
|
+
'carry the id yourself.');
|
|
388
|
+
}
|
|
389
|
+
return (' ⚠️ THE HYPOTHESIS LINKAGE WAS NOT RECORDED: the server did not echo back the id this call ' +
|
|
390
|
+
'sent, which usually means the Clien.ai deploy answering predates the field. The interview ' +
|
|
391
|
+
'itself is fine and every turn will work. Do NOT report its answers as anchored to that ' +
|
|
392
|
+
'hypothesis on the strength of this call — nothing stored the tie, so carry the id yourself ' +
|
|
393
|
+
'if you need it.');
|
|
394
|
+
}
|
|
265
395
|
async function sendTurn(input, ctx, deps) {
|
|
266
396
|
if (!input.interview_id || !input.content) {
|
|
267
397
|
throw new InterviewToolError("action 'turn' requires interview_id and content.");
|
|
@@ -530,7 +660,19 @@ export async function listInterviews(input, deps) {
|
|
|
530
660
|
// off the end of a row.
|
|
531
661
|
const startedAt = safeInline(iv.startedAt, 40);
|
|
532
662
|
const started = startedAt ? ` — started ${startedAt}` : '';
|
|
533
|
-
|
|
663
|
+
// FUL-577: the hypothesis a listed interview was written to test. This is the
|
|
664
|
+
// surface where it earns the most — "which interviews tested H3?" was a
|
|
665
|
+
// question nobody could answer from stored data, and the list is where an
|
|
666
|
+
// agent goes looking. Printed only when BOTH halves render, for the reason
|
|
667
|
+
// given on `formatTranscript`: an id without its run is an unresolvable
|
|
668
|
+
// reference dressed as provenance. `safeId` keeps a poisoned value from
|
|
669
|
+
// forging a sibling `- ` row (FUL-253).
|
|
670
|
+
const hypothesisId = safeId(iv.hypothesisId);
|
|
671
|
+
const hypothesisRunId = safeId(iv.hypothesisRunId);
|
|
672
|
+
const hypothesis = hypothesisId && hypothesisRunId
|
|
673
|
+
? ` — tests hypothesis ${hypothesisId} of run ${hypothesisRunId}`
|
|
674
|
+
: '';
|
|
675
|
+
return `- ${status} interview${persona} — ${messages}${started}${hypothesis} (id: ${safeId(iv.id) ?? 'unknown'})`;
|
|
534
676
|
});
|
|
535
677
|
const shown = offset + interviews.length;
|
|
536
678
|
const more = shown < total ? `\n\n${total - shown} more not shown — page with offset=${shown}.` : '';
|