1
0

tui-scripted-llm.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import type { Context } from 'cordis'
  2. import type {
  3. GenerateOptions,
  4. LlmModelInfo,
  5. LlmResolvedModelInfo,
  6. StreamChunk,
  7. } from '@deepseek-ai/dsh-llm'
  8. import { CallId, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
  9. const CONTROL_PROBE = '\u001b]2;MODEL_CONTROLLED\u0007\u001b[999CMODEL_CURSOR\u009b31mMODEL_C1'
  10. const INITIAL_TEXT = `I need one decision before I continue. ${CONTROL_PROBE}`
  11. const FINAL_TEXT = 'Decision received. Scripted TUI run complete.'
  12. const DEFAULT_MODE_PROBE = 'Confirm the scripted run left plan mode.'
  13. const DEFAULT_MODE_TEXT = 'Default mode confirmed.'
  14. // The `skill` scenario types `/skill:scripted-skill`; the manual-invocation front
  15. // door delivers the loaded skill as a user turn wrapped in `<skill name="…">`. The
  16. // body marker below lives in the fixture skill, so echoing it back proves the whole
  17. // block (name attribute plus body) reached the model, not just the command text.
  18. const SKILL_BLOCK_OPEN = '<skill name="scripted-skill">'
  19. const SKILL_BODY_MARKER = 'SCRIPTED SKILL BODY MARKER'
  20. const SKILL_RECEIVED_TEXT = 'Scripted skill body received.'
  21. const TITLE_TEXT = 'scripted session title'
  22. // The failing-bash scenario proves the terminal card reports a non-zero exit
  23. // exactly once: the model-facing result carries the `[exit code: N]` marker, and
  24. // the card turns it into its own `[exit N]` pill instead of showing both.
  25. const BASH_FAILURE_PROBE = 'Run the failing scripted command.'
  26. const BASH_FAILURE_COMMAND = 'printf "SCRIPTED_BASH_FAILED\\n"; exit 3'
  27. const BASH_FAILURE_TEXT = 'Scripted bash failure observed.'
  28. const BASH_FAILURE_CALL_ID = CallId('call-bash-failure')
  29. function textChunks(text: string): StreamChunk[] {
  30. return [
  31. { type: 'block-start', index: 0, blockType: 'text' },
  32. ...Array.from(text, (char): StreamChunk => ({ type: 'text-delta', index: 0, text: char })),
  33. { type: 'block-end', index: 0, block: { type: 'text', text } },
  34. { type: 'usage', usage: { inputTokens: 20, outputTokens: text.length } },
  35. { type: 'finish', reason: { kind: 'stop' } },
  36. ]
  37. }
  38. /** Keyless adapter for the real-PTY TUI tests: the two-step conversation and the `/skill:` round-trip. */
  39. class ScriptedTuiAdapter extends LlmAdapter {
  40. override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
  41. return Promise.resolve([
  42. { provider, id: 'tui-scripted-model', name: 'Scripted Base' },
  43. { provider, id: 'tui-scripted-model-pro', name: 'Scripted Pro' },
  44. ])
  45. }
  46. override resolveModel(
  47. provider: string,
  48. model: string,
  49. ): Promise<LlmResolvedModelInfo> {
  50. return Promise.resolve({
  51. provider,
  52. id: model,
  53. name: model === 'tui-scripted-model-pro' ? 'Scripted Pro' : 'Scripted Base',
  54. context: { contextWindow: 128_000 },
  55. ...model !== 'tui-scripted-model-pro'
  56. ? {}
  57. : {
  58. reasoning: {
  59. efforts: [
  60. { id: ReasoningEffortId('off'), name: 'Off' },
  61. { id: ReasoningEffortId('high'), name: 'High' },
  62. { id: ReasoningEffortId('max'), name: 'Max' },
  63. ],
  64. defaultEffort: ReasoningEffortId('high'),
  65. },
  66. },
  67. })
  68. }
  69. override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
  70. // The session-title provider's auxiliary request carries no tool schemas,
  71. // unlike every agent turn; answer it with a fixed title so the PTY test can
  72. // assert the logged title reaches the terminal window title.
  73. if ((options.tools?.length ?? 0) === 0) {
  74. for (const chunk of textChunks(TITLE_TEXT)) yield chunk
  75. return
  76. }
  77. if (
  78. options.model !== 'tui-scripted-model-pro'
  79. || !options.system?.includes('tui-scripted-model-pro')
  80. || options.reasoningEffort !== ReasoningEffortId('max')
  81. ) {
  82. throw new Error('the scripted TUI request did not apply the selected model and reasoning effort')
  83. }
  84. const lastMessage = options.messages.at(-1)
  85. // The loop appends plugin-sourced context (the plan-mode notice, the
  86. // tool-skill catalog) AFTER the admitted prompt, so the scripted trigger
  87. // may sit one or more user messages back: scan the whole trailing run of
  88. // user-role messages since the last assistant message.
  89. const trailingUserTexts: string[] = []
  90. for (let index = options.messages.length - 1; index >= 0; index--) {
  91. const message = options.messages[index]
  92. if (message?.role !== 'user') break
  93. for (const block of message.content) {
  94. if (block.type === 'text') trailingUserTexts.push(block.text)
  95. }
  96. }
  97. const lastText = trailingUserTexts.join('\n')
  98. if (lastText.includes(DEFAULT_MODE_PROBE)) {
  99. if (options.system?.includes('Stay in plan mode for this scripted TUI test.')) {
  100. throw new Error('the scripted TUI request retained plan guidance after /plan off')
  101. }
  102. for (const chunk of textChunks(DEFAULT_MODE_TEXT)) yield chunk
  103. return
  104. }
  105. if (lastText.includes(SKILL_BLOCK_OPEN)) {
  106. const ack = lastText.includes(SKILL_BODY_MARKER)
  107. ? SKILL_RECEIVED_TEXT
  108. : 'Scripted skill block arrived without its body.'
  109. for (const chunk of textChunks(ack)) yield chunk
  110. return
  111. }
  112. const blocks = lastMessage?.content ?? []
  113. if (blocks.some(block => block.type === 'tool-result')) {
  114. const answered = blocks.some(block => block.type === 'tool-result' && block.toolCallId === BASH_FAILURE_CALL_ID)
  115. for (const chunk of textChunks(answered ? BASH_FAILURE_TEXT : FINAL_TEXT)) yield chunk
  116. return
  117. }
  118. if (lastText.includes(BASH_FAILURE_PROBE)) {
  119. const bashArgs = JSON.stringify({ command: BASH_FAILURE_COMMAND, description: 'Run the failing scripted command' })
  120. yield { type: 'block-start', index: 0, blockType: 'tool-call' }
  121. yield { type: 'tool-call-delta', index: 0, id: BASH_FAILURE_CALL_ID, name: 'bash', argumentsDelta: bashArgs }
  122. yield {
  123. type: 'block-end',
  124. index: 0,
  125. block: { type: 'tool-call', id: BASH_FAILURE_CALL_ID, name: 'bash', arguments: bashArgs },
  126. }
  127. yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
  128. yield { type: 'finish', reason: { kind: 'tool-calls' } }
  129. return
  130. }
  131. const args = JSON.stringify({
  132. questions: [{
  133. id: 'mode',
  134. header: 'Execution mode',
  135. question: 'How should the scripted run proceed?',
  136. options: [
  137. { label: 'Safe', description: 'Use the guarded path.' },
  138. { label: 'Fast', description: 'Use the shorter path.' },
  139. ],
  140. }],
  141. })
  142. const callId = CallId('call-ask-mode')
  143. yield { type: 'block-start', index: 0, blockType: 'text' }
  144. for (const char of INITIAL_TEXT) yield { type: 'text-delta', index: 0, text: char }
  145. yield { type: 'block-end', index: 0, block: { type: 'text', text: INITIAL_TEXT } }
  146. yield { type: 'block-start', index: 1, blockType: 'tool-call' }
  147. yield { type: 'tool-call-delta', index: 1, id: callId, name: 'ask_user_question', argumentsDelta: args }
  148. yield {
  149. type: 'block-end',
  150. index: 1,
  151. block: { type: 'tool-call', id: callId, name: 'ask_user_question', arguments: args },
  152. }
  153. yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
  154. yield { type: 'finish', reason: { kind: 'tool-calls' } }
  155. }
  156. }
  157. export const name = 'tui-scripted-llm'
  158. export const inject = ['llm']
  159. /** Register the network-free adapter used by the PTY fixture. */
  160. export function apply(ctx: Context): void {
  161. ctx.llm.registerAdapter(['tui-scripted'], new ScriptedTuiAdapter())
  162. }