Research Agents: Lets users define research agents that answer chat questions from SEC filings, uploaded documents, and the web, with every figure cited.
| 1 | // What an answer shows while it is being written: the step it is on, worded from the call it makes, |
| 2 | // and the model's latest note to itself, its words beside its tool calls or, where the provider hands |
| 3 | // them back, its thinking. Pure, so the wording is tested. |
| 4 | |
| 5 | /** How much of a note the line shows: its end, where the model is now. */ |
| 6 | const THOUGHT_MAX = 280 |
| 7 | |
| 8 | export interface Working { |
| 9 | step: string |
| 10 | thought: string | null |
| 11 | /** The company add_company is reading in, while it waits. */ |
| 12 | company?: string |
| 13 | } |
| 14 | |
| 15 | /** A tool call as the working line says it. */ |
| 16 | export function stepOf(call: { name: string; input: Record<string, unknown> }): string { |
| 17 | const text = (key: string): string => (typeof call.input[key] === 'string' ? (call.input[key] as string).trim() : '') |
| 18 | switch (call.name) { |
| 19 | case 'search_prism': { |
| 20 | const where = [text('ticker'), text('form'), call.input['fiscal_year'], text('fiscal_period'), text('section')] |
| 21 | .filter((part) => part !== '' && part !== undefined && part !== null) |
| 22 | .join(' ') |
| 23 | return `Searching “${text('query')}”${where ? ` · ${where}` : ''}` |
| 24 | } |
| 25 | case 'fetch_page': { |
| 26 | let host = text('url') |
| 27 | try { |
| 28 | host = new URL(host).host |
| 29 | } catch { |
| 30 | // Said as written. |
| 31 | } |
| 32 | return `Reading ${host}` |
| 33 | } |
| 34 | case 'search_web': |
| 35 | return `Searching the web for “${text('query')}”` |
| 36 | case 'calculate': |
| 37 | return 'Calculating' |
| 38 | case 'add_company': { |
| 39 | const [from, to] = [call.input['from'], call.input['to']] |
| 40 | return `Reading ${text('ticker')} filings, ${from === to ? from : `${from}–${to}`}` |
| 41 | } |
| 42 | case 'activate_skill': |
| 43 | return `Loading skill ${text('name')}` |
| 44 | case 'read_skill_file': |
| 45 | return `Reading ${text('skill')}/${text('path')}` |
| 46 | default: |
| 47 | return `Calling ${call.name}` |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | /** The model's note on a round that calls tools: its words, else its thinking where the payload has it. */ |
| 52 | export function thoughtOf(text: string, raw: unknown[] | null): string | null { |
| 53 | const said = text.trim() || thinkingIn(raw) |
| 54 | if (!said) return null |
| 55 | const flat = said.replace(/\s+/g, ' ') |
| 56 | return flat.length > THOUGHT_MAX ? `…${flat.slice(-THOUGHT_MAX)}` : flat |
| 57 | } |
| 58 | |
| 59 | /** Anthropic's thinking blocks, the only payload a plugin is handed that carries thinking as words. */ |
| 60 | function thinkingIn(raw: unknown[] | null): string { |
| 61 | return (raw ?? []) |
| 62 | .map((block) => |
| 63 | block && typeof block === 'object' && (block as { type?: unknown }).type === 'thinking' |
| 64 | ? String((block as { thinking?: unknown }).thinking ?? '') |
| 65 | : '', |
| 66 | ) |
| 67 | .filter(Boolean) |
| 68 | .join(' ') |
| 69 | .trim() |
| 70 | } |