@@ -289,9 +289,19 @@ export function compactCodingExamplesForIntent(prompt: string): string {
289289}
290290
291291/**
292- * Context-aware action formatting. Replaces the <actions>...</actions>
293- * block in the prompt with a version where only intent-relevant actions
294- * have full <params> — the rest are stubs with just name + description.
292+ * Context-aware action formatting. Replaces the available-actions block in
293+ * the prompt with a version where only intent-relevant actions keep full
294+ * parameter detail — the rest are stubs with just name + description.
295+ *
296+ * Supports two prompt encodings:
297+ * - TOON (current default): the actions provider emits
298+ * actions[N]:
299+ * - ACTION: description
300+ * aliases[..]: ...
301+ * tags[..]: ...
302+ * params[..]: ...
303+ * example: ...
304+ * - XML (legacy): <actions><action><name>..</name>...</action>...</actions>
295305 *
296306 * If no intents are detected (general chat), only universal actions
297307 * (REPLY, NONE, IGNORE) keep full params — all others are stubbed.
@@ -309,7 +319,116 @@ export function compactActionsForIntent(prompt: string): string {
309319 // are always preserved, so the LLM can still select the right action; it
310320 // just won't see detailed param schemas until the user triggers a known intent.
311321
312- // Find the first <actions>...</actions> block (the Available Actions section)
322+ const intentCategories = detectIntentCategories ( prompt ) ;
323+ // When no specific intent is detected, it's general chat — only universal
324+ // actions (REPLY, NONE, IGNORE) need full detail. All other actions get
325+ // stubs so the LLM knows they exist but doesn't waste context on params.
326+ const fullParamActions = buildFullParamActionSet ( intentCategories ) ;
327+
328+ // Try TOON-format first (current default), then fall back to XML for
329+ // legacy prompts that still emit <actions>...</actions> blocks.
330+ const toonCompacted = compactToonActionsBlock ( prompt , fullParamActions ) ;
331+ if ( toonCompacted !== null ) return toonCompacted ;
332+ return compactXmlActionsBlock ( prompt , fullParamActions ) ;
333+ }
334+
335+ /**
336+ * Locate and compact a TOON-formatted "Available Actions" block. Returns
337+ * `null` if no TOON block is found, so the caller can try XML.
338+ */
339+ function compactToonActionsBlock (
340+ prompt : string ,
341+ fullParamActions : Set < string > ,
342+ ) : string | null {
343+ // The actions provider emits "actions[N]:\n- NAME: desc\n params[..]: ...".
344+ // Anchor on that header line.
345+ const headerRe = / ^ a c t i o n s \[ \d + \] : [ \t ] * $ / m;
346+ const headerMatch = headerRe . exec ( prompt ) ;
347+ if ( ! headerMatch ) return null ;
348+
349+ const blockStart = headerMatch . index ;
350+ const headerLine = headerMatch [ 0 ] ;
351+ const bodyStart = blockStart + headerLine . length ;
352+
353+ // Walk forward consuming action entries (`- NAME: ...`) and their
354+ // two-space-indented continuation lines. Stop at the first non-indented
355+ // non-entry line that isn't part of an entry.
356+ const remainder = prompt . slice ( bodyStart ) ;
357+ const lines = remainder . split ( "\n" ) ;
358+
359+ let consumed = 0 ;
360+ if ( lines . length > 0 && lines [ 0 ] === "" ) {
361+ consumed = 1 ;
362+ }
363+
364+ while ( consumed < lines . length ) {
365+ const line = lines [ consumed ] ;
366+ if ( line . startsWith ( "- " ) || line . startsWith ( " " ) ) {
367+ consumed += 1 ;
368+ continue ;
369+ }
370+ if ( line === "" ) {
371+ const next = lines [ consumed + 1 ] ;
372+ if ( next !== undefined && next . startsWith ( "- " ) ) {
373+ consumed += 1 ;
374+ continue ;
375+ }
376+ break ;
377+ }
378+ break ;
379+ }
380+
381+ const bodyLines = lines . slice ( 0 , consumed ) ;
382+ const blockEnd = bodyStart + bodyLines . join ( "\n" ) . length ;
383+
384+ type ToonAction = { name : string ; entryLines : string [ ] } ;
385+ const entries : ToonAction [ ] = [ ] ;
386+ let current : ToonAction | null = null ;
387+ for ( const line of bodyLines ) {
388+ if ( line . startsWith ( "- " ) ) {
389+ if ( current ) entries . push ( current ) ;
390+ const nameMatch = / ^ - ( [ A - Z 0 - 9 _ ] + ) : / . exec ( line ) ;
391+ const name = nameMatch ?. [ 1 ] ?? "" ;
392+ current = { name, entryLines : [ line ] } ;
393+ } else if ( current && ( line . startsWith ( " " ) || line === "" ) ) {
394+ current . entryLines . push ( line ) ;
395+ }
396+ }
397+ if ( current ) entries . push ( current ) ;
398+
399+ if ( entries . length === 0 ) return null ;
400+
401+ const compactedEntries = entries . map ( ( entry ) => {
402+ if ( ! entry . name || fullParamActions . has ( entry . name ) ) {
403+ return entry . entryLines . join ( "\n" ) ;
404+ }
405+ // Stub: keep only `- NAME: description`; drop continuation lines.
406+ return entry . entryLines [ 0 ] ;
407+ } ) ;
408+
409+ while (
410+ compactedEntries . length > 0 &&
411+ compactedEntries [ compactedEntries . length - 1 ] === ""
412+ ) {
413+ compactedEntries . pop ( ) ;
414+ }
415+
416+ const compactedBody = compactedEntries . join ( "\n" ) ;
417+ const before = prompt . slice ( 0 , bodyStart ) ;
418+ const after = prompt . slice ( blockEnd ) ;
419+ const separator = bodyLines . length > 0 && bodyLines [ 0 ] === "" ? "\n" : "" ;
420+ return `${ before } ${ separator } ${ compactedBody } ${ after } ` ;
421+ }
422+
423+ /**
424+ * Legacy XML compaction: locate a `<actions>...</actions>` block and stub
425+ * non-relevant `<action>` entries. Returns the original prompt unchanged
426+ * when no XML block is present.
427+ */
428+ function compactXmlActionsBlock (
429+ prompt : string ,
430+ fullParamActions : Set < string > ,
431+ ) : string {
313432 const actionsStart = prompt . indexOf ( "<actions>" ) ;
314433 if ( actionsStart === - 1 ) return prompt ;
315434 const actionsEnd = prompt . indexOf ( "</actions>" , actionsStart ) ;
@@ -320,13 +439,6 @@ export function compactActionsForIntent(prompt: string): string {
320439 actionsEnd ,
321440 ) ;
322441
323- const intentCategories = detectIntentCategories ( prompt ) ;
324- // When no specific intent is detected, it's general chat — only universal
325- // actions (REPLY, NONE, IGNORE) need full detail. All other actions get
326- // stubs so the LLM knows they exist but doesn't waste context on params.
327- const fullParamActions = buildFullParamActionSet ( intentCategories ) ;
328-
329- // Parse individual <action>...</action> blocks
330442 const actionRegex = / < a c t i o n > ( [ \s \S ] * ?) < \/ a c t i o n > / g;
331443 const compactedActions : string [ ] = [ ] ;
332444
@@ -338,10 +450,8 @@ export function compactActionsForIntent(prompt: string): string {
338450 const actionName = nameMatch [ 1 ] . trim ( ) ;
339451
340452 if ( fullParamActions . has ( actionName ) ) {
341- // Keep full action with params
342453 compactedActions . push ( ` <action>${ actionInner } </action>` ) ;
343454 } else {
344- // Stub: name + description only, strip <params>
345455 const descMatch = actionInner . match (
346456 / < d e s c r i p t i o n > ( [ \s \S ] * ?) < \/ d e s c r i p t i o n > / ,
347457 ) ;
0 commit comments