code-mode.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. /**
  2. * Code Mode `run_code` transport. Programs call the registry's agent-visible
  3. * tools through nested executions scheduled under the native concurrency
  4. * contract; each sub-dispatch is logged for reconstruction, while only the
  5. * outer curated result enters model history.
  6. * @module @deepseek-ai/dsh-tools/src/code-mode
  7. */
  8. import { CallId, HarnessError } from '@deepseek-ai/dsh-llm'
  9. import type { ContentBlock } from '@deepseek-ai/dsh-llm'
  10. import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
  11. import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
  12. import type { JsonValue } from '@deepseek-ai/dsh-session'
  13. import { defineTool } from './schema.ts'
  14. import { TOOL_REGISTRY_SCHEDULER } from './index.ts'
  15. import type { ToolDefinition, ToolExecutionResult, ToolRegistry, ToolRunContext } from './index.ts'
  16. declare module '@deepseek-ai/dsh-session' {
  17. interface SessionEventMap {
  18. /**
  19. * One sub-dispatch STARTING inside a `run_code` program: the parent
  20. * `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
  21. * numbered in submission order), and the tool `name` with its
  22. * JSON-normalized `arguments` — the exact value dispatched, normalized
  23. * BEFORE dispatch, so this append can never fail on payload shape.
  24. * Appended when the scheduler actually starts the call (not at
  25. * submission), so a start means the tool body pipeline was entered; a
  26. * call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
  27. * ignores it; UIs use it for live per-sub-call running state and pair it
  28. * with `tool/code-dispatch` by `subCallId` (timing = the two events'
  29. * `time` fields).
  30. */
  31. 'tool/code-dispatch-start': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown }
  32. /**
  33. * One bridged sub-dispatch SETTLING: the pairing ids (matching the
  34. * `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
  35. * with the same JSON-normalized `arguments`, and the sub-call's complete
  36. * model-facing outcome in `tool/result`'s own vocabulary
  37. * (`content` + `isError`), so UIs render a sub-call through the exact
  38. * code path that renders a native call. Every started sub-call settles
  39. * with exactly one of these (abort included: the aborted pipeline result
  40. * is an `isError` outcome).
  41. * Log-only: `deriveMessages()` ignores it, so sub-calls never re-enter
  42. * model context; persistence and UIs get every call. Appended inside the
  43. * parent `run_code`'s execution (the bridge drains in-flight dispatches
  44. * before returning), so the turn-enclosure invariant holds by
  45. * construction.
  46. */
  47. 'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] }
  48. }
  49. }
  50. /** The model-facing name of the Code Mode tool. */
  51. export const RUN_CODE_NAME = 'run_code'
  52. /** The `tools:sdk` section order: inside the 100–199 tool-guidance band, after per-tool guidance sections. */
  53. export const SDK_SECTION_ORDER = 150
  54. /**
  55. * Thrown by `run_code` when the program run itself failed — a program
  56. * exception, a budget expiry, an abort, or substrate death. Extends
  57. * {@link HarnessError} (`code: 'CODE_RUN_FAILED'`); the registry's execution
  58. * pipeline converts it into a structured `isError` result whose text carries
  59. * the failure kind plus the captured logs, so the model can self-correct.
  60. */
  61. export class CodeRunFailedError extends HarnessError {
  62. constructor(message: string) {
  63. super(message, 'CODE_RUN_FAILED')
  64. this.name = 'CodeRunFailedError'
  65. }
  66. }
  67. /**
  68. * Snapshot one binding call's argument as lossless JSON, then snapshot that
  69. * detached value again so dispatch and logging stay independent without
  70. * reintroducing structured-clone's platform-specific nesting limit.
  71. */
  72. function jsonNormalizeArgs(value: unknown): { dispatched: unknown; logged: unknown } {
  73. let snapshot: JsonValue | undefined
  74. try {
  75. snapshot = snapshotJsonValue(value) as JsonValue | undefined
  76. } catch (error: unknown) {
  77. throw new Error(`tool arguments must be lossless JSON: ${error instanceof Error ? error.message : String(error)}`)
  78. }
  79. if (snapshot === undefined) {
  80. throw new Error('tool arguments must be lossless JSON (call the tool with an arguments object, e.g. `{}`)')
  81. }
  82. const logged = snapshotJsonValue(snapshot)
  83. /* v8 ignore next -- snapshot is already a detached lossless JSON value. */
  84. if (logged === undefined) {
  85. throw new Error('tool arguments could not be detached for durable logging')
  86. }
  87. return { dispatched: snapshot, logged }
  88. }
  89. /** Two-space JSON presentation, matching the existing shallow `run_code` text contract. */
  90. const JSON_INDENT = ' '
  91. /**
  92. * ECMAScript caps `JSON.stringify`'s `space` string at ten characters. The
  93. * renderer also caps TOTAL indentation there, compacting deeper subtrees, so
  94. * formatted output remains linear in the canonical JSON size.
  95. */
  96. const MAX_JSON_INDENT_CHARS = 10
  97. /** A pending fragment in the iterative JSON presentation traversal. */
  98. type JsonRenderTask =
  99. | { kind: 'text'; text: string }
  100. | { kind: 'value'; value: JsonValue; depth: number; compact: boolean }
  101. /** Render one non-string JSON root without recursive traversal or unbounded indentation growth. */
  102. function renderJsonValue(value: Exclude<JsonValue, string>): string {
  103. const chunks: string[] = []
  104. const tasks: JsonRenderTask[] = [{ kind: 'value', value, depth: 0, compact: false }]
  105. for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
  106. if (task.kind === 'text') {
  107. chunks.push(task.text)
  108. continue
  109. }
  110. const current = task.value
  111. if (current === null || typeof current === 'boolean' || typeof current === 'number') {
  112. chunks.push(String(current))
  113. continue
  114. }
  115. if (typeof current === 'string') {
  116. chunks.push(JSON.stringify(current))
  117. continue
  118. }
  119. const compact = task.compact || (task.depth + 1) * JSON_INDENT.length > MAX_JSON_INDENT_CHARS
  120. const childDepth = task.depth + 1
  121. if (Array.isArray(current)) {
  122. chunks.push('[')
  123. if (current.length === 0) {
  124. chunks.push(']')
  125. continue
  126. }
  127. tasks.push({ kind: 'text', text: compact ? ']' : `\n${JSON_INDENT.repeat(task.depth)}]` })
  128. for (let index = current.length - 1; index >= 0; index--) {
  129. const item = current[index]
  130. /* v8 ignore next -- canonical JsonValue arrays are dense. */
  131. if (item === undefined) throw new Error('cannot render a sparse JSON array')
  132. tasks.push({ kind: 'value', value: item, depth: childDepth, compact })
  133. tasks.push({
  134. kind: 'text',
  135. text: compact
  136. ? index === 0 ? '' : ','
  137. : `${index === 0 ? '\n' : ',\n'}${JSON_INDENT.repeat(childDepth)}`,
  138. })
  139. }
  140. continue
  141. }
  142. const keys = Object.keys(current)
  143. chunks.push('{')
  144. if (keys.length === 0) {
  145. chunks.push('}')
  146. continue
  147. }
  148. tasks.push({ kind: 'text', text: compact ? '}' : `\n${JSON_INDENT.repeat(task.depth)}}` })
  149. for (let index = keys.length - 1; index >= 0; index--) {
  150. const key = keys[index]
  151. /* v8 ignore next -- the loop is bounded by the captured key count. */
  152. if (key === undefined) throw new Error('cannot render a missing JSON object key')
  153. const item = current[key]
  154. /* v8 ignore next -- canonical JsonValue records contain no undefined properties. */
  155. if (item === undefined) throw new Error('cannot render an undefined JSON object property')
  156. tasks.push({ kind: 'value', value: item, depth: childDepth, compact })
  157. tasks.push({
  158. kind: 'text',
  159. text: compact
  160. ? `${index === 0 ? '' : ','}${JSON.stringify(key)}:`
  161. : `${index === 0 ? '\n' : ',\n'}${JSON_INDENT.repeat(childDepth)}${JSON.stringify(key)}: `,
  162. })
  163. }
  164. }
  165. return chunks.join('')
  166. }
  167. /** Render one present program completion value for the model-facing result text. */
  168. function renderValue(value: JsonValue): string {
  169. return typeof value === 'string' ? value : renderJsonValue(value)
  170. }
  171. /** Canonical value returned by the outer Code Mode transport. */
  172. type RunCodeOutput = { logs: string[]; result?: JsonValue }
  173. /**
  174. * Build the `run_code` {@link ToolDefinition}: required `code` and
  175. * `description` parameters, executed through the dispatch bridge described
  176. * above. The
  177. * registry reserves it as presentation infrastructure under non-native modes,
  178. * outside the filterable global/scoped capability layers.
  179. * @param registry - the owning registry (sub-calls go through its `execute`,
  180. * bindings cover its registered tools).
  181. * @param requireRuntime - resolves `ctx.codeRuntime` or throws the loud
  182. * misconfiguration error (shared with the registry's assembly-time checks).
  183. * @param maxParallel - the run's overlap cap for parallel-classified
  184. * sub-calls (the registry passes its validated `maxParallelSubCalls`).
  185. * @returns the registry-ready definition.
  186. */
  187. export function createRunCodeTool(registry: ToolRegistry, requireRuntime: () => CodeRuntime, maxParallel: number): ToolDefinition {
  188. return defineTool({
  189. name: RUN_CODE_NAME,
  190. description:
  191. 'Execute a TypeScript program against the available tools. Write the BODY of an '
  192. + 'async function (erasable syntax only; top-level `await` and `return` work) and '
  193. + 'call tools as `await tools.name(args)` per the declarations in the system prompt. '
  194. + 'Only what you print or return comes back — curate it.',
  195. parameters: {
  196. code: { type: 'string', required: true, description: 'The program: the body of an async TypeScript function.' },
  197. description: {
  198. type: 'string',
  199. required: true,
  200. description: 'Clear, concise description of what this program does in active voice, '
  201. + '5-10 words (shown in the UI). Examples: "Count TODO markers across packages"; '
  202. + '"Read failing test and its fixture"; "Rename config key in every cordis.yml".',
  203. },
  204. },
  205. output: {
  206. schema: {
  207. type: 'object',
  208. additionalProperties: false,
  209. properties: {
  210. logs: { type: 'array', required: true, items: { type: 'string' } },
  211. result: { type: 'json' },
  212. },
  213. },
  214. render: (_args, value) => {
  215. const rendered = value.result === undefined ? '' : renderValue(value.result)
  216. const parts = [value.logs.join('\n'), rendered].filter(part => part.length > 0)
  217. return [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : '(run_code completed with no output)' }]
  218. },
  219. },
  220. async execute(args, exec): Promise<RunCodeOutput> {
  221. if (args.description.trim().length === 0) {
  222. throw new Error('invalid description: expected a non-empty string')
  223. }
  224. const runtime = requireRuntime()
  225. // The run-scoped abort: follows the outer signal in, and fires when the
  226. // run settles for ANY reason, so an in-flight sub-dispatch is aborted
  227. // (its executor kills on this signal) instead of orphaned, and
  228. // queued-unstarted dispatches are abandoned.
  229. const runController = new AbortController()
  230. const onOuterAbort = (): void => { runController.abort(exec.signal.reason) }
  231. exec.signal.addEventListener('abort', onOuterAbort, { once: true })
  232. let dispatches = 0
  233. // The per-run scheduler, reusing the NATIVE concurrency contract through
  234. // the registry's staged view (the loop scheduler's own seam) — and the
  235. // native loop's SEQUENCING: every ordered stage (the dispatch-start
  236. // append, prepare = pre-execute/guards, finalize/finish = post-execute,
  237. // context deferral, the settle append) runs inside ONE driver lane, so
  238. // ordered policy stages never overlap each other and only the
  239. // around-dispatch/body stage runs concurrently. Starts are strictly
  240. // submission-ordered; results commit in submission order through the
  241. // head-of-line cursor. Consecutive parallel-classified calls overlap up
  242. // to maxParallel; an exclusive call waits for the pool to drain, runs
  243. // alone, and holds its barrier until its COMMIT (post-execute included)
  244. // completes, exactly like a native exclusive group. Classification is
  245. // re-read via executionMode() immediately before each start (a registry
  246. // mutation while queued can flip a call exclusive), matching the native
  247. // scheduler's lazy reclassification.
  248. interface PendingDispatch {
  249. /** Ordered stage: append the start event, await prepare (pre-execute/guards), launch the body into `flight`. */
  250. start(): Promise<void>
  251. classify(): 'parallel' | 'exclusive'
  252. abandon(): void
  253. /** Ordered stage: post-execute + context deferral + settle event, in submission order. */
  254. commit(): Promise<void>
  255. /** The launched around-dispatch/body stage; resolved until start() replaces it. */
  256. flight: Promise<void>
  257. /** True once the dispatch stage parked its outcome; the commit cursor waits on it. */
  258. settled: boolean
  259. /** The classification this entry started under; an exclusive holds its barrier through commit(). */
  260. mode?: 'parallel' | 'exclusive'
  261. }
  262. const pendingQueue: PendingDispatch[] = []
  263. const inFlight = new Set<Promise<void>>()
  264. const commitQueue: PendingDispatch[] = []
  265. let exclusiveActive = false
  266. let driving = false
  267. let driverRun: Promise<void> = Promise.resolve()
  268. let wake: (() => void) | undefined
  269. const wakeup = (): void => {
  270. const release = wake
  271. wake = undefined
  272. release?.()
  273. }
  274. /**
  275. * The single ordered lane. Each pass commits the head-of-line settled
  276. * dispatch (ordered post-execute), then starts the next queued entry if
  277. * its slot is free (ordered pre-execute), and otherwise sleeps until a
  278. * body settles or a new submission arrives. One run reaching the
  279. * empty-queues/empty-pool state is quiescence.
  280. */
  281. const drive = (): Promise<void> => {
  282. if (driving) return driverRun
  283. driving = true
  284. driverRun = (async () => {
  285. try {
  286. for (;;) {
  287. // Arm before inspecting state so a settle or submission landing
  288. // between the checks and the await below cannot be lost.
  289. const signal = new Promise<void>((resolve) => { wake = resolve })
  290. const commitHead = commitQueue[0]
  291. if (commitHead !== undefined && commitHead.settled) {
  292. commitQueue.shift()
  293. await commitHead.commit()
  294. // The barrier covers post-execute: later starts wait for the
  295. // exclusive call's full pipeline, as under the native loop.
  296. if (commitHead.mode === 'exclusive') exclusiveActive = false
  297. continue
  298. }
  299. const head = pendingQueue[0]
  300. if (head !== undefined) {
  301. if (runController.signal.aborted) {
  302. pendingQueue.shift()
  303. head.abandon()
  304. continue
  305. }
  306. // Reclassify at start time (fail-closed on registry changes).
  307. const mode = head.classify()
  308. const capacity = !exclusiveActive
  309. && (mode === 'exclusive' ? inFlight.size === 0 : inFlight.size < maxParallel)
  310. if (capacity) {
  311. if (mode === 'exclusive') exclusiveActive = true
  312. head.mode = mode
  313. pendingQueue.shift()
  314. // Joined before start() so the commit cursor sees submission
  315. // order; nothing commits it until `settled` flips.
  316. commitQueue.push(head)
  317. await head.start()
  318. const flight: Promise<void> = head.flight.finally(() => {
  319. inFlight.delete(flight)
  320. wakeup()
  321. })
  322. inFlight.add(flight)
  323. continue
  324. }
  325. }
  326. if (pendingQueue.length === 0 && commitQueue.length === 0 && inFlight.size === 0) return
  327. await signal
  328. }
  329. } finally {
  330. driving = false
  331. wake = undefined
  332. }
  333. })()
  334. return driverRun
  335. }
  336. /** Every dispatch settled AND committed; nothing can start (the run is aborted at call time). */
  337. const drainDispatches = async (): Promise<void> => {
  338. // The abort already fired: the driver abandons queued-unstarted
  339. // entries, awaits the live pool, and drains the ordered commit lane —
  340. // including a commit already in progress when the program returned.
  341. await drive()
  342. }
  343. // Read through a call, not a bare property: the abort state genuinely
  344. // changes across awaits, and a direct `.aborted` re-check after one
  345. // would be narrowed away by control flow analysis.
  346. const runOver = (): boolean => runController.signal.aborted
  347. const binding = (name: string): CodeBindingFunction => async (rawArgs: unknown): Promise<JsonValue> => {
  348. if (runOver()) {
  349. throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} not dispatched`)
  350. }
  351. const normalized = jsonNormalizeArgs(rawArgs)
  352. const n = ++dispatches
  353. const subCallId = CallId(`${String(exec.callId)}:code:${n}`)
  354. const input = {
  355. callId: subCallId,
  356. name,
  357. arguments: normalized.dispatched,
  358. ...exec.agent ? { agent: exec.agent } : {},
  359. parent: exec.token,
  360. signal: runController.signal,
  361. }
  362. type DispatchOutcome = { isError: true; message: string } | { isError: false; value: JsonValue }
  363. const scheduler = registry[TOOL_REGISTRY_SCHEDULER]
  364. const outcome = await new Promise<DispatchOutcome>((resolve, reject) => {
  365. // Set by the dispatch stage (or start() for a pre-settled result): what commit() finalizes in submission order.
  366. let parked:
  367. | { kind: 'post-result' | 'final-result'; exec: ToolRunContext; result: ToolExecutionResult }
  368. | undefined
  369. const settle = (result: ToolExecutionResult): void => {
  370. exec.agent?.session.append('tool/code-dispatch', {
  371. parentCallId: exec.callId,
  372. subCallId,
  373. name,
  374. // The SIBLING parse of the dispatched value: byte-identical JSON,
  375. // but a separate object — a tool mutating its args cannot desync
  376. // this record from what it actually received.
  377. arguments: normalized.logged,
  378. isError: result.isError,
  379. // The registry deep-froze this projection at result finalization;
  380. // append snapshots it again, so the log copy stays detached.
  381. content: result.content,
  382. })
  383. resolve(result.isError
  384. ? { isError: true, message: result.error.message }
  385. : { isError: false, value: result.value })
  386. }
  387. pendingQueue.push({
  388. flight: Promise.resolve(),
  389. settled: false,
  390. // Re-read per driver pass against the same agent view the SDK
  391. // declared; fail-closed exclusive when undeclared/invalid.
  392. classify: () => registry.executionMode(input).kind,
  393. abandon: () => {
  394. reject(new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} tool call abandoned`))
  395. },
  396. async start(): Promise<void> {
  397. exec.agent?.session.append('tool/code-dispatch-start', {
  398. parentCallId: exec.callId,
  399. subCallId,
  400. name,
  401. arguments: normalized.logged,
  402. })
  403. // Ordered prepare runs INSIDE the driver lane: the next entry's
  404. // pre-execute waits for this resolution, as under the native
  405. // scheduler. Only the launched body below overlaps.
  406. const prepared = await scheduler.prepare(input)
  407. if (prepared.kind === 'dispatch') {
  408. this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
  409. parked = { kind: dispatchOutcome.kind, exec: prepared.exec, result: dispatchOutcome.result }
  410. this.settled = true
  411. })
  412. return
  413. }
  414. parked = { kind: prepared.kind, exec: prepared.exec, result: prepared.result }
  415. this.settled = true
  416. },
  417. async commit(): Promise<void> {
  418. /* v8 ignore next -- commit() runs only after `settled` flipped, which set parked. */
  419. if (parked === undefined) return
  420. const result = parked.kind === 'post-result'
  421. ? await scheduler.finalize(parked.exec, parked.result)
  422. : scheduler.finish(parked.exec, parked.result)
  423. for (const context of result.additionalContexts ?? []) {
  424. exec.deferContext(context)
  425. }
  426. // Like the context forwarding above, cross-boundary facts travel
  427. // on the nested result and the composite forwards them: only a
  428. // successful nested result can carry the terminal marker
  429. // (ToolExecutionFailure types it never), so a policy-converted
  430. // failure cannot stop the turn through a recovering program.
  431. if (result.concludesTurn) exec.concludeTurn()
  432. settle(result)
  433. },
  434. })
  435. wakeup()
  436. void drive()
  437. })
  438. // A budget expiry or outer cancel that lands while this call was in
  439. // flight already aborted the dispatch; stop the program now rather
  440. // than hand it a result from a run that is over.
  441. if (runOver()) {
  442. throw new Error(`run_code run is over (${String(runController.signal.reason)}); ${name} result discarded`)
  443. }
  444. // The worker turns a binding rejection into ToolCallError and adds
  445. // only the binding name. Native content and internal error metadata
  446. // stay outside the program-facing failure contract.
  447. if (outcome.isError) throw new Error(outcome.message)
  448. return outcome.value
  449. }
  450. // Null-prototype + defineProperty, mirroring the worker-side namespace
  451. // build: a registered tool named `__proto__` must become an ordinary
  452. // own key (a plain-object assignment would hit the prototype setter,
  453. // silently dropping the binding), and the runtime host resolves
  454. // binding names as own properties only.
  455. const functions: Record<string, CodeBindingFunction> = Object.create(null) as Record<string, CodeBindingFunction>
  456. // Enumerate the CALLING AGENT's visible set (scoped tools join,
  457. // restricted globals vanish) — the same view the SDK section declared,
  458. // so a program can bind exactly what its prompt promised; sub-dispatch
  459. // re-resolves per call through the same view (exec.agent threads down).
  460. for (const schema of registry.schemas(exec.agent)) {
  461. if (schema.name === RUN_CODE_NAME) continue
  462. Object.defineProperty(functions, schema.name, { enumerable: true, value: binding(schema.name) })
  463. }
  464. try {
  465. let result: CodeRunResult
  466. try {
  467. result = await runtime.run({
  468. program: args.code,
  469. bindings: [{
  470. global: 'tools',
  471. functions,
  472. errorClass: { name: 'ToolCallError', memberNameProperty: 'toolName' },
  473. }],
  474. signal: runController.signal,
  475. })
  476. } finally {
  477. // Abort sub-dispatches and drain every in-flight dispatch before
  478. // closing the turn (queued-unstarted ones are abandoned unlogged).
  479. // Binding failures remain observable through their individual promises.
  480. runController.abort('run_code settled')
  481. await drainDispatches()
  482. }
  483. if (result.error) {
  484. const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : ''
  485. throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}`)
  486. }
  487. return {
  488. logs: result.logs,
  489. ...result.value !== undefined ? { result: result.value } : {},
  490. }
  491. } finally {
  492. exec.signal.removeEventListener('abort', onOuterAbort)
  493. }
  494. },
  495. // The model-authored description is the call's always-visible UI label
  496. // (the bash `description` precedent); the program itself rides rawInput.
  497. presentCall: args => ({
  498. card: 'generic',
  499. title: args.description,
  500. kind: 'execute',
  501. rawInput: args.code,
  502. }),
  503. // Deliberately no presentResult: the generic surface fallback keeps this
  504. // title and reads durable result content without duplicating a large raw
  505. // result into the host view payload.
  506. })
  507. }