@clien-ai/mcp 0.8.1 → 0.9.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 +30 -5
- package/dist/tools/collections.js +290 -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/interview-scripts.js +360 -0
- package/dist/tools/interview-scripts.js.map +1 -0
- package/dist/tools/interview.js +18 -2
- package/dist/tools/interview.js.map +1 -1
- package/dist/tools/output-schemas.js +27 -0
- package/dist/tools/output-schemas.js.map +1 -1
- package/dist/tools/personas.js +262 -39
- package/dist/tools/personas.js.map +1 -1
- package/dist/tools/registry.js +230 -22
- 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 +70 -36
- 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 +124 -4
- package/dist/types/report.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FUL-341 / FUL-536 / FUL-560 — the one rule for reading and RENDERING a source's content type to
|
|
3
|
+
* an agent.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a module and not two copies
|
|
6
|
+
*
|
|
7
|
+
* `list_community_signals` (`collections.ts`) and `search_forums` (`scoped-research.ts`) render the
|
|
8
|
+
* SAME underlying rows — a forum thread a run discovered, before and after it is promoted into
|
|
9
|
+
* `community_signals`. FUL-247's rule is that two surfaces over one dataset must not disagree about
|
|
10
|
+
* what is worth an agent's attention; FUL-536 is what happens when they do, and it took two green
|
|
11
|
+
* guards and a production run to notice. So the vocabulary, the fail-closed normalizer and the
|
|
12
|
+
* rendered line live in ONE place that both import, the same way `engagement-display.ts` owns the
|
|
13
|
+
* engagement line for the same two surfaces.
|
|
14
|
+
*
|
|
15
|
+
* ## The rule
|
|
16
|
+
*
|
|
17
|
+
* State the provenance in BOTH directions, always. Rendering a type only when one is present makes
|
|
18
|
+
* "unclassified" indistinguishable from "this surface was not told", and silence reads as
|
|
19
|
+
* reassurance — which is the fail-open FUL-341 exists to close. An agent deciding whether to trust
|
|
20
|
+
* a quote needs the negative case spelled out, and it needs the CONSEQUENCE, not just a null field.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ THE TWO EXCLUSIONS ARE THE SAME BUT ARE WORDED SEPARATELY ON PURPOSE. Since FUL-531 both kinds
|
|
23
|
+
* carry the identical exclusion — `GROUNDING_ELIGIBLE_CONTENT_TYPES` (`app/lib/db/community-db.ts`)
|
|
24
|
+
* is `CONTENT_TYPES` minus `FLAGGED_CONTENT_TYPES`, so neither reaches the automatic synthesis
|
|
25
|
+
* prompt, and `buildCommunityThemes` grounds no theme on either. They stay worded SEPARATELY
|
|
26
|
+
* because they do not MEAN the same thing: `marketing_seo` is a source we classified and judged
|
|
27
|
+
* vendor copy, `unclassified` is one nobody has looked at. Collapsing them would tell an agent that
|
|
28
|
+
* an unexamined source had been examined.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* FUL-341 — the canonical content-type vocabulary, hand-copied.
|
|
32
|
+
*
|
|
33
|
+
* `mcp/` publishes to npm on its own cadence and cannot import from the Next.js app, so this is a
|
|
34
|
+
* forced duplicate of `CONTENT_TYPES` in `app/lib/validation/claim-states.ts` (itself already
|
|
35
|
+
* pinned to the agent's copy). This repo's answer to exactly that situation is a coupling test:
|
|
36
|
+
* `__tests__/integration/mcp/community-content-type-coupling.test.ts` pins this set equal to the
|
|
37
|
+
* app's, so widening the vocabulary in one package fails the suite rather than silently teaching
|
|
38
|
+
* two surfaces to disagree about the same row.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ That test parses this declaration out of THIS FILE as text. Moving or renaming it means
|
|
41
|
+
* updating the test's path and regex, not just the import sites.
|
|
42
|
+
*/
|
|
43
|
+
export const CANONICAL_CONTENT_TYPES = ['user_voice', 'report_evidence', 'marketing_seo'];
|
|
44
|
+
/**
|
|
45
|
+
* The four states, enumerated for a tool DESCRIPTION or a schema `.describe()`.
|
|
46
|
+
*
|
|
47
|
+
* A description is an executable interface: an agent reads it BEFORE its first call and is primed
|
|
48
|
+
* by it, so a trust qualifier that exists only in the rendered output is discoverable one call too
|
|
49
|
+
* late (FUL-341). Carries no leading clause — each surface supplies its own ("every thread carries
|
|
50
|
+
* a `source type:` line", "every receipt carries…") — and no trailing space.
|
|
51
|
+
*/
|
|
52
|
+
export const CONTENT_TYPE_STATES = '`user_voice` (a real person\'s first-hand account), `report_evidence` ' +
|
|
53
|
+
'(authoritative/third-person reporting), `marketing_seo` (vendor marketing or SEO content), or ' +
|
|
54
|
+
'`unclassified`.';
|
|
55
|
+
/**
|
|
56
|
+
* The two exclusions, in the ONE wording every surface states them in.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ Both are real and both are enforced: `GROUNDING_ELIGIBLE_CONTENT_TYPES` (`community-db.ts`)
|
|
59
|
+
* is `CONTENT_TYPES` minus `FLAGGED_CONTENT_TYPES` since FUL-531, so neither kind reaches the
|
|
60
|
+
* automatic synthesis prompt, and `buildCommunityThemes` grounds no theme on either. They are
|
|
61
|
+
* still stated SEPARATELY because they do not mean the same thing — one source was classified and
|
|
62
|
+
* judged vendor copy, the other has not been looked at — and an agent that conflated them would
|
|
63
|
+
* treat an unexamined source as an examined one.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ FUL-560 extracted this from the two `registry.ts` adverts that already held it word-for-word
|
|
66
|
+
* by hand. The `get_report` receipt pools were the surface that had NO wording at all, and adding
|
|
67
|
+
* a fifth hand-copy to fix that is the drift FUL-247 exists to prevent — so the copies became one
|
|
68
|
+
* constant, pinned across every surface by `content-type-display.test.ts`. No leading or trailing
|
|
69
|
+
* space; each caller joins it.
|
|
70
|
+
*/
|
|
71
|
+
export const CONTENT_TYPE_EXCLUSIONS = '`marketing_seo` is shown for awareness but is EXCLUDED from grounding AND from automatic ' +
|
|
72
|
+
'community synthesis — it is vendor marketing or SEO content, never cite it as user ' +
|
|
73
|
+
'evidence. `unclassified` is EXCLUDED from both as well, and means the classification is ' +
|
|
74
|
+
'genuinely unknown, NOT that the source was checked and found benign.';
|
|
75
|
+
/**
|
|
76
|
+
* FUL-560 — the receipt-pool paragraph, in the ONE wording every tool that RENDERS the trust digest
|
|
77
|
+
* advertises it in.
|
|
78
|
+
*
|
|
79
|
+
* `composeReportText` puts the same two receipt pools in the result text of `get_report`,
|
|
80
|
+
* `clien_research` and `clien_research_status`, so all three emit these `source type:` lines and all
|
|
81
|
+
* three must explain them: an agent that ran the hero tool and never calls `get_report` would
|
|
82
|
+
* otherwise meet the vocabulary for the first time in the output, which is the discoverable-one-call-
|
|
83
|
+
* too-late failure {@link CONTENT_TYPE_STATES} exists to avoid. Three hand-copies of a paragraph is
|
|
84
|
+
* also exactly the drift that made this module necessary — hence one constant, and
|
|
85
|
+
* `content-type-display.test.ts` DERIVES the tools that must carry it from the source rather than
|
|
86
|
+
* listing them, so a fourth digest surface fails the suite instead of shipping unadverted.
|
|
87
|
+
*
|
|
88
|
+
* Opens as a noun phrase so it can be a numbered item in `get_report`'s list or the object of a
|
|
89
|
+
* lead-in sentence elsewhere. No trailing space; each caller joins it.
|
|
90
|
+
*/
|
|
91
|
+
export const CONTENT_TYPE_DIGEST_ADVERT = 'the SOURCE TYPE on every receipt — ' +
|
|
92
|
+
CONTENT_TYPE_STATES +
|
|
93
|
+
' Every "RRCP-" market/competitor receipt carries a `source type:` line; a "RCP-" persona receipt ' +
|
|
94
|
+
'is a first-hand `user_voice` post unless its row says otherwise, and any that is not carries the ' +
|
|
95
|
+
'same line marked \u26a0. ' +
|
|
96
|
+
CONTENT_TYPE_EXCLUSIONS;
|
|
97
|
+
/**
|
|
98
|
+
* Normalize a persisted classification, failing CLOSED.
|
|
99
|
+
*
|
|
100
|
+
* Anything outside the canonical set — a typo'd literal, a value from a newer app version this
|
|
101
|
+
* client predates — becomes `null`, exactly like a missing one. A `!== null` check here instead
|
|
102
|
+
* would let an unrecognized string read as CLASSIFIED and present unvetted evidence to an agent as
|
|
103
|
+
* grounding-eligible. Version skew makes that a real case for a published client, not a
|
|
104
|
+
* hypothetical one.
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ FUL-536 adds a second reason on the `search_forums` side. There the value is read out of a
|
|
107
|
+
* stored `report_data` document rather than a CHECK-constrained column, and `ForumThreadSchema` is
|
|
108
|
+
* `.passthrough()` — so a hallucinated `contentType: 'user_voice'` from an older run is a shape the
|
|
109
|
+
* parser accepts. `tagFastPathContentTypes` (FUL-535) deletes model-emitted types at the agent's
|
|
110
|
+
* read chokepoint for NEW data; this is what stops an old one being rendered as a classification.
|
|
111
|
+
*/
|
|
112
|
+
export function canonicalContentType(value) {
|
|
113
|
+
return typeof value === 'string' && CANONICAL_CONTENT_TYPES.includes(value)
|
|
114
|
+
? value
|
|
115
|
+
: null;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The continuation-line indent every surface renders this on. One constant, so the two
|
|
119
|
+
* compositions below cannot drift apart by a space.
|
|
120
|
+
*/
|
|
121
|
+
const CONTINUATION = '\n ';
|
|
122
|
+
/**
|
|
123
|
+
* The sentence itself, without the indent — what a reader actually reads.
|
|
124
|
+
*
|
|
125
|
+
* Split out by FUL-560 purely so {@link contentTypeDeviationLine} can prefix a marker without
|
|
126
|
+
* restating any of the three branches. Nothing calls this directly; the exported line builders
|
|
127
|
+
* are the API, and holding the wording in ONE expression is the whole point of this module.
|
|
128
|
+
*/
|
|
129
|
+
function contentTypeSentence(value) {
|
|
130
|
+
const contentType = canonicalContentType(value);
|
|
131
|
+
return !contentType
|
|
132
|
+
? 'source type: unclassified - excluded from grounding and community synthesis'
|
|
133
|
+
: contentType === 'marketing_seo'
|
|
134
|
+
? 'source type: marketing_seo - vendor marketing, excluded from grounding and community synthesis'
|
|
135
|
+
: `source type: ${contentType}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The `source type:` continuation line, for a row rendered in the `\n ` continuation shape both
|
|
139
|
+
* surfaces use. Never returns an empty string: see the both-directions rule above.
|
|
140
|
+
*
|
|
141
|
+
* Accepts the absent value in either spelling — `null` for a `community_signals` column, `undefined`
|
|
142
|
+
* for a `report_data` thread key FUL-535 deliberately left unstamped — because the two surfaces
|
|
143
|
+
* store "we don't know" differently and neither should have to widen its own types to say so.
|
|
144
|
+
*/
|
|
145
|
+
export function contentTypeProvenanceLine(value) {
|
|
146
|
+
return `${CONTINUATION}${contentTypeSentence(value)}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* FUL-560 — the same sentence, marked as a DEVIATION from a pool's stated invariant.
|
|
150
|
+
*
|
|
151
|
+
* `get_report`'s PERSONA receipt pool is the one surface where the type is a near-constant rather
|
|
152
|
+
* than a per-row fact: `stampEvidenceMetadata` prunes every non-`user_voice` source out of
|
|
153
|
+
* `persona.sources` upstream, so a per-row line there would repeat one word down the whole pool
|
|
154
|
+
* and bury the row that broke the rule. The pool states the invariant once in its header and each
|
|
155
|
+
* row that does NOT satisfy it carries this line — which is the both-directions rule kept, not
|
|
156
|
+
* traded away: a reader is told the rule AND told, loudly, wherever it does not hold.
|
|
157
|
+
*
|
|
158
|
+
* ⚠️ The marker is emphasis, NOT vocabulary. It prefixes the shared sentence rather than replacing
|
|
159
|
+
* any of it, so the wording an agent matches on stays byte-identical to every other surface — the
|
|
160
|
+
* FUL-247 drift this module exists to prevent. A row whose type is absent or non-canonical is a
|
|
161
|
+
* deviation too: `user_voice` is a positive claim, and anything else is the caller failing closed.
|
|
162
|
+
*/
|
|
163
|
+
export function contentTypeDeviationLine(value) {
|
|
164
|
+
return `${CONTINUATION}\u26a0 ${contentTypeSentence(value)}`;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=content-type-display.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-type-display.js","sourceRoot":"","sources":["../../src/tools/content-type-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,YAAY,EAAE,iBAAiB,EAAE,eAAe,CAAU,CAAA;AAElG;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,wEAAwE;IACxE,gGAAgG;IAChG,iBAAiB,CAAA;AAEnB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAClC,2FAA2F;IAC3F,qFAAqF;IACrF,0FAA0F;IAC1F,sEAAsE,CAAA;AAExE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,0BAA0B,GACrC,qCAAqC;IACrC,mBAAmB;IACnB,mGAAmG;IACnG,mGAAmG;IACnG,2BAA2B;IAC3B,uBAAuB,CAAA;AAEzB;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAc;IACjD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,uBAA6C,CAAC,QAAQ,CAAC,KAAK,CAAC;QAChG,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAI,CAAA;AACV,CAAC;AAED;;;GAGG;AACH,MAAM,YAAY,GAAG,OAAO,CAAA;AAE5B;;;;;;GAMG;AACH,SAAS,mBAAmB,CAAC,KAAc;IACzC,MAAM,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAA;IAC/C,OAAO,CAAC,WAAW;QACjB,CAAC,CAAC,6EAA6E;QAC/E,CAAC,CAAC,WAAW,KAAK,eAAe;YAC/B,CAAC,CAAC,gGAAgG;YAClG,CAAC,CAAC,gBAAgB,WAAW,EAAE,CAAA;AACrC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,OAAO,GAAG,YAAY,GAAG,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAA;AACvD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAc;IACrD,OAAO,GAAG,YAAY,UAAU,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAA;AAC9D,CAAC"}
|
|
@@ -0,0 +1,360 @@
|
|
|
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
|
+
* ⚠️ Stated because nothing downstream enforces it: `interview_persona` takes no
|
|
207
|
+
* hypothesis id, so the tie between an answer and the hypothesis it tests exists
|
|
208
|
+
* only for as long as the CALLER carries `hypothesisId` forward. An agent that
|
|
209
|
+
* runs these questions and reports the answers without it has produced exactly the
|
|
210
|
+
* unanchored interview this tool was built to prevent.
|
|
211
|
+
*/
|
|
212
|
+
const LINKAGE_LINE = 'LINKAGE: each script names the `hypothesisId` it was written to test. Carry that id alongside ' +
|
|
213
|
+
'whatever the interview returns — `interview_persona` does not record it for you — or the ' +
|
|
214
|
+
'answers cannot be tied back to the hypothesis, which is the whole point of asking them.';
|
|
215
|
+
/**
|
|
216
|
+
* The trust caveat.
|
|
217
|
+
*
|
|
218
|
+
* A verdict printed beside a question reads as settled unless something says
|
|
219
|
+
* otherwise (FUL-247). These verdicts are the run's reading of its own hypothesis,
|
|
220
|
+
* not the grading of a claim: the per-claim GROUNDED / SPECULATION / NO_RECEIPT
|
|
221
|
+
* states live in the source run's claim spine, which this separately released
|
|
222
|
+
* client cannot reconstruct.
|
|
223
|
+
*/
|
|
224
|
+
const SCRIPTS_TRUST_LINE = 'TRUST: the verdict and confidence on each script are the source run\'s reading of that ' +
|
|
225
|
+
'hypothesis, not a graded claim. Nothing here carries a GROUNDED / SPECULATION / NO_RECEIPT ' +
|
|
226
|
+
'state — read the source run with `get_report` for those. A high confidence is a reason to ask ' +
|
|
227
|
+
'a different question, never a reason to skip asking.';
|
|
228
|
+
const CHANNEL_NOTE = 'The full record is in `structuredContent.interviewScripts` (also mirrored in `_meta`).';
|
|
229
|
+
/** `Weak signal (inconclusive), confidence 52%` — or the honest partial forms. */
|
|
230
|
+
function verdictClause(script) {
|
|
231
|
+
const label = safeInline(script.verdictLabel, VERDICT_MAX);
|
|
232
|
+
const status = safeInline(script.status, VERDICT_MAX);
|
|
233
|
+
const confidence = script.confidence === null ? null : `confidence ${Math.round(script.confidence * 100)}%`;
|
|
234
|
+
const verdict = label && status ? `${label} (${status})` : label ?? status ?? 'verdict not recorded';
|
|
235
|
+
return confidence ? `${verdict}, ${confidence}` : `${verdict}, confidence not recorded`;
|
|
236
|
+
}
|
|
237
|
+
/** The question list for one script, capped and — when capped — saying so. */
|
|
238
|
+
function questionBlock(questions) {
|
|
239
|
+
const flat = questions
|
|
240
|
+
.map((q) => collapseWhitespace(typeof q === 'string' ? q : ''))
|
|
241
|
+
.filter((q) => q.length > 0);
|
|
242
|
+
// Branch on the RENDERED questions, not on `questions.length`: `['']` has
|
|
243
|
+
// length 1 and collapses to nothing, and a "Questions:" header over no
|
|
244
|
+
// questions reads as a rendering bug rather than as a script that lost them.
|
|
245
|
+
if (flat.length === 0) {
|
|
246
|
+
return ' Questions: none readable in the stored script.';
|
|
247
|
+
}
|
|
248
|
+
const shown = flat.slice(0, QUESTION_RENDER_CAP);
|
|
249
|
+
const lines = shown.map((q, i) => ` ${i + 1}. ${truncate(q, QUESTION_MAX)}`);
|
|
250
|
+
const dropped = flat.length - shown.length;
|
|
251
|
+
const more = dropped > 0
|
|
252
|
+
? `\n (${dropped} more in \`structuredContent.interviewScripts\`)`
|
|
253
|
+
: '';
|
|
254
|
+
return ` Questions:\n${lines.join('\n')}${more}`;
|
|
255
|
+
}
|
|
256
|
+
/** One ranked script as its own block. */
|
|
257
|
+
function renderScript(script, rank) {
|
|
258
|
+
const id = safeId(script.hypothesisId);
|
|
259
|
+
const statement = safeInline(script.statement, STATEMENT_MAX);
|
|
260
|
+
const whoToAsk = safeInline(script.whoToAsk, WHO_TO_ASK_MAX);
|
|
261
|
+
// The heading names the hypothesis, because a question set detached from the
|
|
262
|
+
// hypothesis it serves is the thing this tool exists to stop being handed out.
|
|
263
|
+
const heading = statement
|
|
264
|
+
? `${rank}. ${statement}`
|
|
265
|
+
: `${rank}. (this run recorded no statement for the hypothesis this script serves)`;
|
|
266
|
+
const lines = [
|
|
267
|
+
heading,
|
|
268
|
+
id
|
|
269
|
+
? ` Hypothesis: ${id} — ${verdictClause(script)}`
|
|
270
|
+
: ` Hypothesis: NOT IDENTIFIED — ${verdictClause(script)}. Without an id these answers ` +
|
|
271
|
+
'cannot be tied back to a hypothesis.',
|
|
272
|
+
];
|
|
273
|
+
if (whoToAsk)
|
|
274
|
+
lines.push(` Who to ask: ${whoToAsk}`);
|
|
275
|
+
lines.push(questionBlock(script.questions));
|
|
276
|
+
return lines.join('\n');
|
|
277
|
+
}
|
|
278
|
+
export async function listInterviewScripts(input, deps) {
|
|
279
|
+
const parsed = ListInterviewScriptsInputSchema.safeParse(input);
|
|
280
|
+
if (!parsed.success) {
|
|
281
|
+
throw new CollectionToolError(`Invalid input — ${formatZodError(parsed.error)}`);
|
|
282
|
+
}
|
|
283
|
+
const { project_id: projectId } = parsed.data;
|
|
284
|
+
const { scripts, sourceRun } = await fetchInterviewScripts(deps, projectId);
|
|
285
|
+
let text;
|
|
286
|
+
if (scripts === null) {
|
|
287
|
+
// ⚠️ THE HONEST EMPTY STATE. "Absent" is not "settled" and it is not "we looked
|
|
288
|
+
// and there was nothing worth asking". A scoped scan or a failed run leaves a
|
|
289
|
+
// project with no scripts while its hypotheses are wide open, so this sentence
|
|
290
|
+
// has to say that nothing is on record rather than imply a finding.
|
|
291
|
+
text =
|
|
292
|
+
`No interview scripts recorded for project ${projectId}.\n\n` +
|
|
293
|
+
'This means NO completed research run has written interview scripts for this project. It ' +
|
|
294
|
+
'is NOT a finding about the hypotheses: not "they are settled", not "there is nothing ' +
|
|
295
|
+
'worth asking", not zero. There is simply nothing on record.\n\n' +
|
|
296
|
+
'This tool never generates scripts — it reads what a run stored, and it will keep ' +
|
|
297
|
+
'returning this until one exists. Run `clien_research` (a full run) to produce them. ' +
|
|
298
|
+
'`scan_competitors` and `search_forums` are scoped runs and never write scripts, so ' +
|
|
299
|
+
'running those will leave this empty.';
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const runId = safeId(sourceRun?.id ?? null);
|
|
303
|
+
const when = safeInline(sourceRun?.createdAt ?? null, TIMESTAMP_MAX);
|
|
304
|
+
if (scripts.length === 0) {
|
|
305
|
+
// ⚠️ A DIFFERENT FACT FROM THE `null` BRANCH. A run IS on record and it did
|
|
306
|
+
// write a scripts array — the endpoint only names a run that did. Nothing in
|
|
307
|
+
// it could be read, which is a defect in the stored report, not a statement
|
|
308
|
+
// about the hypotheses. Collapsing the two would tell a caller no research
|
|
309
|
+
// has been done when some has.
|
|
310
|
+
text =
|
|
311
|
+
`Project ${projectId} has a research run that wrote interview scripts, and none of ` +
|
|
312
|
+
'them could be read.\n\n' +
|
|
313
|
+
(runId
|
|
314
|
+
? `Run ${runId}${when ? ` (recorded ${when})` : ''} stored a scripts array whose ` +
|
|
315
|
+
'entries are unusable — a malformed report, not an answer about the hypotheses. '
|
|
316
|
+
: 'The run that wrote them stored a scripts array whose entries are unusable — a ' +
|
|
317
|
+
'malformed report, not an answer about the hypotheses. ') +
|
|
318
|
+
'Do not treat this as "nothing worth asking". Read the run itself with `get_report` for ' +
|
|
319
|
+
'its hypotheses, or run `clien_research` again to write a fresh set.\n\n' +
|
|
320
|
+
provenanceLine(sourceRun?.id ?? null, sourceRun?.createdAt ?? null) +
|
|
321
|
+
'\n\n' +
|
|
322
|
+
CHANNEL_NOTE;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const shown = scripts.slice(0, SCRIPT_RENDER_CAP);
|
|
326
|
+
const dropped = scripts.length - shown.length;
|
|
327
|
+
const capNote = dropped > 0
|
|
328
|
+
? `\n\n(${dropped} more script${dropped === 1 ? '' : 's'} in ` +
|
|
329
|
+
'`structuredContent.interviewScripts`.)'
|
|
330
|
+
: '';
|
|
331
|
+
text =
|
|
332
|
+
`Interview scripts for project ${projectId} — ${scripts.length} script` +
|
|
333
|
+
`${scripts.length === 1 ? '' : 's'}` +
|
|
334
|
+
`${runId ? ` from research run ${runId}` : ''}${when ? ` (recorded ${when})` : ''}.\n\n` +
|
|
335
|
+
'These are the STORED output of that run, written when it ran. Nothing was generated to ' +
|
|
336
|
+
'answer this call.\n\n' +
|
|
337
|
+
'RANKED least-settled first: the hypothesis nearest a coin flip goes first, because a ' +
|
|
338
|
+
'real answer there moves the verdict most. That order is the run\'s, not this ' +
|
|
339
|
+
'client\'s.\n\n' +
|
|
340
|
+
shown.map((script, i) => renderScript(script, i + 1)).join('\n\n') +
|
|
341
|
+
capNote +
|
|
342
|
+
'\n\n' +
|
|
343
|
+
LINKAGE_LINE +
|
|
344
|
+
'\n\n' +
|
|
345
|
+
provenanceLine(sourceRun?.id ?? null, sourceRun?.createdAt ?? null) +
|
|
346
|
+
'\n\n' +
|
|
347
|
+
SCRIPTS_TRUST_LINE +
|
|
348
|
+
'\n\n' +
|
|
349
|
+
CHANNEL_NOTE;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
content: [{ type: 'text', text }],
|
|
354
|
+
// Both keys are present even when null — the PRESENCE is what tells a
|
|
355
|
+
// structured consumer "no run has written scripts" apart from "this tool did
|
|
356
|
+
// not answer".
|
|
357
|
+
_meta: { interviewScripts: scripts, sourceRun },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
//# sourceMappingURL=interview-scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interview-scripts.js","sourceRoot":"","sources":["../../src/tools/interview-scripts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,OAAO,EACP,gBAAgB,GAGjB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,OAAO,EAAoB,MAAM,eAAe,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAErF,MAAM,wBAAwB,GAAG,MAAM,CAAA;AAEvC,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IACtD,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,IAAI,EAAE;SACN,QAAQ,CACP,mEAAmE;QACjE,mFAAmF;QACnF,qFAAqF;QACrF,qFAAqF;QACrF,qFAAqF;QACrF,iFAAiF;QACjF,oFAAoF;QACpF,iDAAiD,CACpD;CACJ,CAAC,CAAA;AAyDF,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AAC5E,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACrE,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AAChD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,GAA0B;IAC/D,OAAO;QACL,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;QACtC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;QAChC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC1B,YAAY,EAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;QACtC,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC;QACtC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC9B,2EAA2E;QAC3E,sEAAsE;QACtE,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;YACrC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;YACjE,CAAC,CAAC,EAAE;KACP,CAAA;AACH,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,GAAoB;IAC9D,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAA;AACjE,CAAC;AAED,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,EAAE,CAAA;AAE5B;;;GAGG;AACH,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAE9B,MAAM,aAAa,GAAG,GAAG,CAAA;AACzB,MAAM,cAAc,GAAG,GAAG,CAAA;AAC1B,MAAM,YAAY,GAAG,GAAG,CAAA;AACxB,MAAM,WAAW,GAAG,EAAE,CAAA;AACtB,MAAM,aAAa,GAAG,EAAE,CAAA;AAYxB;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,qBAAqB,CAClC,IAAwB,EACxB,SAAiB;IAEjB,MAAM,MAAM,GAAG,4BAA4B,CAAA;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;IAEhD,IAAI,QAAqB,CAAA;IACzB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,0BAA0B,KAAK,CAAC,QAAQ,EAAE,EAAE,EAAE;YACjF,SAAS,EAAE,wBAAwB;SACpC,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAwB,CAAA;QAClC,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YACzD,MAAM,IAAI,mBAAmB,CAC3B,mBAAmB,wBAAwB,GAAG,IAAI,eAAe,MAAM,IAAI;gBACzE,oFAAoF,CACvF,CAAA;QACH,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE;QAAE,MAAM,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAA;IAE3E,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAmC,CAAA;IACxF,MAAM,UAAU,GAAG,IAAI,mBAAmB,CACxC,aAAa,MAAM,uDAAuD;QACxE,uFAAuF;QACvF,0FAA0F;QAC1F,qFAAqF;QACrF,uEAAuE,CAC1E,CAAA;IACD,IACE,IAAI,KAAK,IAAI;QACb,OAAO,IAAI,KAAK,QAAQ;QACxB,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QACnB,CAAC,CAAC,kBAAkB,IAAI,IAAI,CAAC,EAC7B,CAAC;QACD,MAAM,UAAU,CAAA;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAA;IAC7B,MAAM,SAAS,GACb,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACrE,CAAC,CAAC,2BAA2B,CAAC,MAAyB,CAAC;QACxD,CAAC,CAAC,IAAI,CAAA;IAEV,MAAM,GAAG,GAAG,IAAI,CAAC,gBAAgB,CAAA;IACjC,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,MAAM,UAAU,CAAA;IAEzC,MAAM,OAAO,GAAG,GAAG;SAChB,MAAM,CAAC,CAAC,KAAK,EAAkC,EAAE,CAChD,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CACrE;SACA,GAAG,CAAC,sBAAsB,CAAC,CAAA;IAE9B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;AAC/B,CAAC;AAED,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,SAAS,cAAc,CAAC,KAAoB,EAAE,SAAwB;IACpE,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC1B,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;IACjD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,CACL,uFAAuF;YACvF,mFAAmF;YACnF,qBAAqB,CACtB,CAAA;IACH,CAAC;IACD,OAAO,CACL,8DAA8D,IAAI,EAAE;QACpE,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACnC,6FAA6F;QAC7F,iDAAiD,IAAI,kCAAkC,CACxF,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,YAAY,GAChB,gGAAgG;IAChG,2FAA2F;IAC3F,yFAAyF,CAAA;AAE3F;;;;;;;;GAQG;AACH,MAAM,kBAAkB,GACtB,yFAAyF;IACzF,6FAA6F;IAC7F,gGAAgG;IAChG,sDAAsD,CAAA;AAExD,MAAM,YAAY,GAChB,wFAAwF,CAAA;AAE1F,kFAAkF;AAClF,SAAS,aAAa,CAAC,MAA4B;IACjD,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC,CAAA;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACrD,MAAM,UAAU,GACd,MAAM,CAAC,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,GAAG,GAAG,CAAC,GAAG,CAAA;IAE1F,MAAM,OAAO,GACX,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,MAAM,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM,IAAI,sBAAsB,CAAA;IACtF,OAAO,UAAU,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,UAAU,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,2BAA2B,CAAA;AACzF,CAAC;AAED,8EAA8E;AAC9E,SAAS,aAAa,CAAC,SAAmB;IACxC,MAAM,IAAI,GAAG,SAAS;SACnB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SAC9D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC9B,0EAA0E;IAC1E,uEAAuE;IACvE,6EAA6E;IAC7E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,mDAAmD,CAAA;IAC5D,CAAC;IACD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAA;IAChD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAC,EAAE,CAAC,CAAA;IAChF,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;IAC1C,MAAM,IAAI,GACR,OAAO,GAAG,CAAC;QACT,CAAC,CAAC,WAAW,OAAO,kDAAkD;QACtE,CAAC,CAAC,EAAE,CAAA;IACR,OAAO,kBAAkB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAA;AACpD,CAAC;AAED,0CAA0C;AAC1C,SAAS,YAAY,CAAC,MAA4B,EAAE,IAAY;IAC9D,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;IACtC,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,CAAC,CAAA;IAC7D,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAA;IAE5D,6EAA6E;IAC7E,+EAA+E;IAC/E,MAAM,OAAO,GAAG,SAAS;QACvB,CAAC,CAAC,GAAG,IAAI,KAAK,SAAS,EAAE;QACzB,CAAC,CAAC,GAAG,IAAI,0EAA0E,CAAA;IAErF,MAAM,KAAK,GAAG;QACZ,OAAO;QACP,EAAE;YACA,CAAC,CAAC,kBAAkB,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,EAAE;YACnD,CAAC,CAAC,mCAAmC,aAAa,CAAC,MAAM,CAAC,gCAAgC;gBACxF,sCAAsC;KAC3C,CAAA;IACD,IAAI,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,kBAAkB,QAAQ,EAAE,CAAC,CAAA;IACtD,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;IAC3C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAc,EACd,IAAwB;IAExB,MAAM,MAAM,GAAG,+BAA+B,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC/D,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,mBAAmB,CAAC,mBAAmB,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAClF,CAAC;IACD,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC,IAAI,CAAA;IAE7C,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,qBAAqB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAE3E,IAAI,IAAY,CAAA;IAChB,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,gFAAgF;QAChF,8EAA8E;QAC9E,+EAA+E;QAC/E,oEAAoE;QACpE,IAAI;YACF,6CAA6C,SAAS,OAAO;gBAC7D,0FAA0F;gBAC1F,uFAAuF;gBACvF,iEAAiE;gBACjE,mFAAmF;gBACnF,sFAAsF;gBACtF,qFAAqF;gBACrF,sCAAsC,CAAA;IAC1C,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,EAAE,EAAE,IAAI,IAAI,CAAC,CAAA;QAC3C,MAAM,IAAI,GAAG,UAAU,CAAC,SAAS,EAAE,SAAS,IAAI,IAAI,EAAE,aAAa,CAAC,CAAA;QAEpE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,4EAA4E;YAC5E,6EAA6E;YAC7E,4EAA4E;YAC5E,2EAA2E;YAC3E,+BAA+B;YAC/B,IAAI;gBACF,WAAW,SAAS,gEAAgE;oBACpF,yBAAyB;oBACzB,CAAC,KAAK;wBACJ,CAAC,CAAC,OAAO,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,gCAAgC;4BAChF,iFAAiF;wBACnF,CAAC,CAAC,gFAAgF;4BAChF,wDAAwD,CAAC;oBAC7D,yFAAyF;oBACzF,yEAAyE;oBACzE,cAAc,CAAC,SAAS,EAAE,EAAE,IAAI,IAAI,EAAE,SAAS,EAAE,SAAS,IAAI,IAAI,CAAC;oBACnE,MAAM;oBACN,YAAY,CAAA;QAChB,CAAC;aAAM,CAAC;YACN,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAA;YACjD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAA;YAC7C,MAAM,OAAO,GACX,OAAO,GAAG,CAAC;gBACT,CAAC,CAAC,QAAQ,OAAO,eAAe,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM;oBAC5D,wCAAwC;gBAC1C,CAAC,CAAC,EAAE,CAAA;YAER,IAAI;gBACF,iCAAiC,SAAS,MAAM,OAAO,CAAC,MAAM,SAAS;oBACvE,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE;oBACpC,GAAG,KAAK,CAAC,CAAC,CAAC,sBAAsB,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,OAAO;oBACxF,yFAAyF;oBACzF,uBAAuB;oBACvB,uFAAuF;oBACvF,+EAA+E;oBAC/E,gBAAgB;oBAChB,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;oBAClE,OAAO;oBACP,MAAM;oBACN,YAAY;oBACZ,MAAM;oBACN,cAAc,CAAC,SAAS,EAAE,EAAE,IAAI,IAAI,EAAE,SAAS,EAAE,SAAS,IAAI,IAAI,CAAC;oBACnE,MAAM;oBACN,kBAAkB;oBAClB,MAAM;oBACN,YAAY,CAAA;QAChB,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACjC,sEAAsE;QACtE,6EAA6E;QAC7E,eAAe;QACf,KAAK,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE;KAChD,CAAA;AACH,CAAC"}
|
package/dist/tools/interview.js
CHANGED
|
@@ -112,8 +112,8 @@ function makeCtx(deps) {
|
|
|
112
112
|
* insufficient-credit stop with balance/required/buy-url guidance; 401 drops the
|
|
113
113
|
* cached session (so the next tool call rebuilds from disk) and surfaces a
|
|
114
114
|
* re-auth message; 404 means the interview/project isn't owned by the caller;
|
|
115
|
-
*
|
|
116
|
-
* generic HTTP error.
|
|
115
|
+
* 403 with a lock code surfaces the upgrade path; 400 surfaces the server's
|
|
116
|
+
* validation detail; everything else surfaces a generic HTTP error.
|
|
117
117
|
*
|
|
118
118
|
* FUL-161: the body is read and parsed ONCE up front rather than after the 404
|
|
119
119
|
* branch, because the 402 branch needs its `balance`/`required`/`buy_url` and a
|
|
@@ -159,6 +159,22 @@ async function throwForResponse(response, deps, action, options) {
|
|
|
159
159
|
deps.invalidateSession?.();
|
|
160
160
|
throw new InterviewToolError('Session expired. Re-run any Clien.ai tool to re-authenticate, then try again.');
|
|
161
161
|
}
|
|
162
|
+
// FUL-503. Without this branch a subscription lock reads as
|
|
163
|
+
// `Failed to start interview (HTTP 403)` — indistinguishable from a bug, so the
|
|
164
|
+
// agent retries something no retry can fix. Mirrors `personas.ts`, including
|
|
165
|
+
// naming the resource that is actually locked.
|
|
166
|
+
//
|
|
167
|
+
// FUL-553 widened where these codes come from: `start` was the only action that
|
|
168
|
+
// could produce them, and now `turn` and `complete`/`reopen` do too, because a
|
|
169
|
+
// locked project is read-only IN FULL rather than merely uncreatable-in. The
|
|
170
|
+
// copy is already action-parameterised, so it needed no change — but note that
|
|
171
|
+
// a PROJECT_LOCKED on a `turn` refers to an interview that DOES exist and is
|
|
172
|
+
// simply frozen, unlike the same code on a `start`.
|
|
173
|
+
if (status === 403 && (parsed?.code === 'PROJECT_LOCKED' || parsed?.code === 'PERSONA_LOCKED')) {
|
|
174
|
+
throw new InterviewToolError(parsed.code === 'PROJECT_LOCKED'
|
|
175
|
+
? `Could not ${action} — the project is locked due to subscription limits and is read-only. Upgrade to Pro to change it.`
|
|
176
|
+
: `Could not ${action} — the persona is locked due to subscription limits. Upgrade to Pro to interview it.`);
|
|
177
|
+
}
|
|
162
178
|
if (status === 404) {
|
|
163
179
|
throw new InterviewToolError(options?.notFound ??
|
|
164
180
|
`Could not ${action} — interview or project not found, or you don't own it. ` +
|