11/**
22 * Renders the deployed-service catalog the model reads before deciding whether
3- * to retrieve.
3+ * to retrieve, plus the notice for a workspace that deploys nothing at all .
44 *
5- * This is a pure function on purpose: the four selection branches below are the
5+ * These are pure functions on purpose: the selection branches below are the
66 * whole routing policy, and they are far easier to pin down in tests than
77 * through a live pre-step.
88 *
1212 * - Truncation is always stated. Silently cutting the list makes the model treat
1313 * it as complete and flatly answer "there is no such knowledge base".
1414 * - An empty scene omits its whole section. `no chat services` is pure noise and
15- * invites the model to handle a case that does not exist.
15+ * invites the model to handle a case that does not exist. An empty CATALOG is
16+ * the opposite case and does get a message: see {@link buildNoServiceNotice}.
17+ * - `bl` is only ever recommended together with how to get it. This plugin talks
18+ * to the API directly and never shells out, so a fully configured deployment
19+ * can have no `bl` on PATH at all.
1620 */
1721
1822import type { ServiceScene } from "./api-types.js" ;
@@ -24,6 +28,17 @@ export const CATALOG_ENTRY_LIMIT = 10;
2428/** Truncation applied to a service description once the backend returns one. */
2529const DESCRIPTION_LIMIT = 200 ;
2630
31+ /**
32+ * Appended once whenever the text tells the model to look further with `bl`.
33+ *
34+ * The install line belongs next to the recommendation, not in the tool
35+ * descriptions: those are static and every token there is spent on every
36+ * request, while this is needed only in the branches that actually name the
37+ * command. A single-service catalog never carries it.
38+ */
39+ const BL_AVAILABILITY_NOTE =
40+ "(`bl` is the Bailian CLI — install it with `npm install -g bailian-cli` if the command is not found.)" ;
41+
2742export interface CatalogInput {
2843 entries : readonly ServiceEntry [ ] ;
2944 /** Server-reported total, which may exceed `entries` when the fetch itself was capped. */
@@ -74,7 +89,7 @@ function renderScene(
7489 scene : ServiceScene ,
7590 defaultAgentId : string | undefined ,
7691 truncatedFetch : boolean ,
77- ) : string [ ] | undefined {
92+ ) : { lines : string [ ] ; usedLookupHint : boolean } | undefined {
7893 const forScene = entries . filter ( ( entry ) => entry . scene === scene ) ;
7994 if ( forScene . length === 0 ) return undefined ;
8095 const lines = [ `${ SCENE_LABEL [ scene ] } :` ] ;
@@ -93,13 +108,14 @@ function renderScene(
93108 ` (default service; ${ others } other${ others === 1 ? "" : "s" } exist — ` +
94109 `run \`bl knowledge service list --scene ${ scene } \` to see them)` ,
95110 ) ;
111+ return { lines, usedLookupHint : true } ;
96112 }
97- return lines ;
113+ return { lines, usedLookupHint : false } ;
98114 }
99115
100116 if ( forScene . length <= CATALOG_ENTRY_LIMIT && ! truncatedFetch ) {
101117 lines . push ( ...forScene . map ( renderEntry ) ) ;
102- return lines ;
118+ return { lines, usedLookupHint : false } ;
103119 }
104120
105121 const shown = [ ...forScene ] . sort ( byRecency ) . slice ( 0 , CATALOG_ENTRY_LIMIT ) ;
@@ -112,7 +128,86 @@ function renderScene(
112128 `${ knownTotal } deployed ${ scene } services — run \`bl knowledge service list --scene ${ scene } ` +
113129 "--name <keyword>` to look for others)" ,
114130 ) ;
115- return lines ;
131+ return { lines, usedLookupHint : true } ;
132+ }
133+
134+ /**
135+ * The notice injected when the workspace is reachable but deploys nothing the
136+ * tools can call.
137+ *
138+ * This deliberately contradicts the "omit what does not exist" rule above, and
139+ * the cases are not symmetric: an empty SECTION is noise because the other
140+ * section still hands the model ids, whereas an empty CATALOG leaves it with no
141+ * id at all while `agent_id` stays required. With nothing said, the model either
142+ * invents an id — earning a raw server rejection — or silently drops the
143+ * retrieval; both read to the user as a broken plugin when the real answer is
144+ * "deploy a service". Naming the fix is the only way out that does not require
145+ * the model to already have loaded the management skill.
146+ * @returns the model-facing notice text.
147+ */
148+ export function buildNoServiceNotice ( ) : string {
149+ return [
150+ "<system-reminder>" ,
151+ "The Bailian knowledge tools (kb_search / kb_chat) are configured, but this workspace has no " +
152+ "deployed knowledge service, so neither tool can be called: both require an `agent_id` and there " +
153+ "is none to use. Do not call them, and do not invent an id." ,
154+ "" ,
155+ "If the user asks for something that should come from their knowledge base, tell them a service has " +
156+ "to be created and deployed first — in the Bailian console (https://bailian.console.aliyun.com/), " +
157+ "or with `bl knowledge service create` followed by `bl knowledge service deploy`. " +
158+ BL_AVAILABILITY_NOTE ,
159+ "</system-reminder>" ,
160+ ] . join ( "\n" ) ;
161+ }
162+
163+ /**
164+ * The service list appended to a call the server rejected, rendered from a
165+ * just-refreshed cache.
166+ *
167+ * Lives here rather than at the call site so every model-facing rendering obeys
168+ * the one set of conventions documented at the top of this file — in particular
169+ * that a shortfall is stated and that `bl` never appears without its install
170+ * line.
171+ * @param scene - the scene the failed call targeted.
172+ * @param entries - that scene's entries as of the refresh, newest first.
173+ * @returns the text to append to the failure.
174+ */
175+ export function buildRefreshedSceneList (
176+ scene : ServiceScene ,
177+ entries : readonly ServiceEntry [ ] ,
178+ ) : string {
179+ if ( entries . length === 0 ) return buildNoSceneServiceNotice ( scene ) ;
180+ const shown = entries . slice ( 0 , CATALOG_ENTRY_LIMIT ) ;
181+ const more = entries . length - shown . length ;
182+ return [
183+ `Deployed ${ scene } services in this workspace, re-read just now:` ,
184+ ...shown . map ( renderEntry ) ,
185+ ...( more > 0
186+ ? [
187+ `(and ${ more } more — run \`bl knowledge service list --scene ${ scene } \` to see them) ` +
188+ BL_AVAILABILITY_NOTE ,
189+ ]
190+ : [ ] ) ,
191+ ] . join ( "\n" ) ;
192+ }
193+
194+ /**
195+ * What a rejected call is told when the refreshed list holds no service for that
196+ * scene at all.
197+ *
198+ * The tool has already failed by this point, so this text is the model's only
199+ * instruction. Without it the bare server rejection ("invalid agent_id") invites
200+ * a retry with another guess, and no guess can succeed.
201+ * @param scene - the scene whose services came back empty.
202+ * @returns the text to append to the failure.
203+ */
204+ function buildNoSceneServiceNotice ( scene : ServiceScene ) : string {
205+ return (
206+ `This workspace has no deployed ${ scene } services at all (re-checked just now), so no agent_id can ` +
207+ "work — do not retry with a different id. Tell the user one has to be created and deployed, in the " +
208+ "Bailian console (https://bailian.console.aliyun.com/) or with `bl knowledge service create` " +
209+ `followed by \`bl knowledge service deploy\`. ${ BL_AVAILABILITY_NOTE } `
210+ ) ;
116211}
117212
118213/**
@@ -129,6 +224,7 @@ export function buildServiceCatalog(input: CatalogInput): string | undefined {
129224 ) ;
130225 const chat = renderScene ( input . entries , "chat" , input . defaultChatAgentId , input . truncated ) ;
131226 if ( search === undefined && chat === undefined ) return undefined ;
227+ const usedLookupHint = search ?. usedLookupHint === true || chat ?. usedLookupHint === true ;
132228 return [
133229 "<system-reminder>" ,
134230 // The header must not name the tools: a scene with no services omits its
@@ -138,12 +234,13 @@ export function buildServiceCatalog(input: CatalogInput): string | undefined {
138234 "Pass an id from the matching section as that tool's `agent_id` argument — it is required and " +
139235 "cannot be guessed." ,
140236 "" ,
141- ...( search ?? [ ] ) ,
237+ ...( search ?. lines ?? [ ] ) ,
142238 ...( search !== undefined && chat !== undefined ? [ "" ] : [ ] ) ,
143- ...( chat ?? [ ] ) ,
239+ ...( chat ?. lines ?? [ ] ) ,
144240 "" ,
145241 "If none of these services covers what the user is asking about, say so plainly rather than " +
146242 "trying the closest-looking id — an unrelated retrieval result is worse than none." ,
243+ ...( usedLookupHint ? [ BL_AVAILABILITY_NOTE ] : [ ] ) ,
147244 "</system-reminder>" ,
148245 ] . join ( "\n" ) ;
149246}
0 commit comments