> ## Documentation Index
> Fetch the complete documentation index at: https://docs.truscan.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Research a question, reported as it happens

> The same capability as POST /api/research, with progress. Not a second implementation: it runs the same pipeline, and the `done` event carries exactly what the other route returns.

The response is a server-sent event stream, so it is exempt from the usual `{success, result, message}` envelope. Each frame is an `event:` name and a `data:` payload holding one event object.

Events arrive in this order. `started` immediately, which exists only to prove the stream is open while the model plans, because that call alone can take ten seconds. `planned` with the sub-questions. One `searched` per sub-question, as each lands rather than all together at the end. `sources` with everything gathered so far. `writing` once the reading is done. Then `delta` fragments as the answer is generated, and finally `done`.

Treat everything before `done` as a view of the wait. The deltas are raw model output, and the finished answer has had any citation pointing at no source stripped out of it, so the two can differ. A client that rebuilt the report from fragments would also have no way to notice a dropped connection.

POST rather than GET because the question is a body, which the browser EventSource API cannot send. Read it with fetch and parse the frames.



## OpenAPI

````yaml /api-reference/research.json post /api/research/stream
openapi: 3.1.0
info:
  title: Truscan Research
  version: 1.0.0
  description: >-
    A question in, a cited report out.


    This is what a single search cannot do. It plans the sub-questions a subject
    actually needs, runs a search for each, reads the pages that come back,
    decides what is still missing, searches again, and writes what the answers
    add up to.


    Every claim carries a citation into `sources`, and every source in that list
    was read rather than merely found: a page whose body could not be extracted
    is dropped before the report is written. A citation pointing at a source
    number that does not exist is removed rather than renumbered, because a
    confidently wrong attribution is worse than an uncited sentence, which a
    reader can at least see and judge.


    One report is one charge. The searches behind it and the pages they read are
    included: a report that needed a second round does not cost more than one
    that got lucky first time.


    Expect this to take tens of seconds. It is doing the work a person would.
servers:
  - url: https://api.truscan.co
security:
  - bearerAuth: []
paths:
  /api/research/stream:
    post:
      summary: Research a question, reported as it happens
      description: >-
        The same capability as POST /api/research, with progress. Not a second
        implementation: it runs the same pipeline, and the `done` event carries
        exactly what the other route returns.


        The response is a server-sent event stream, so it is exempt from the
        usual `{success, result, message}` envelope. Each frame is an `event:`
        name and a `data:` payload holding one event object.


        Events arrive in this order. `started` immediately, which exists only to
        prove the stream is open while the model plans, because that call alone
        can take ten seconds. `planned` with the sub-questions. One `searched`
        per sub-question, as each lands rather than all together at the end.
        `sources` with everything gathered so far. `writing` once the reading is
        done. Then `delta` fragments as the answer is generated, and finally
        `done`.


        Treat everything before `done` as a view of the wait. The deltas are raw
        model output, and the finished answer has had any citation pointing at
        no source stripped out of it, so the two can differ. A client that
        rebuilt the report from fragments would also have no way to notice a
        dropped connection.


        POST rather than GET because the question is a body, which the browser
        EventSource API cannot send. Read it with fetch and parse the frames.
      operationId: researchStream
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResearchRequest'
      responses:
        '200':
          description: A server-sent event stream.
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/ResearchEvent'
        '401':
          $ref: '#/components/responses/Failure'
        '422':
          $ref: '#/components/responses/Failure'
        '429':
          $ref: '#/components/responses/Failure'
        '503':
          $ref: '#/components/responses/Failure'
components:
  schemas:
    ResearchRequest:
      type: object
      required:
        - question
      properties:
        question:
          type: string
          maxLength: 1000
          description: What to research. `query` is accepted as an alias.
          examples:
            - how do solid state batteries differ from lithium ion in practice
        depth:
          type: integer
          minimum: 1
          maximum: 3
          default: 2
          description: >-
            Rounds of search-and-read. Round one searches what the question
            obviously needs; each round after asks what is still missing and
            searches for that. A round may end the report early by finding
            nothing missing, which is a correct answer rather than a shortcut.
        max_sources:
          type: integer
          minimum: 1
          maximum: 24
          default: 24
          description: How many pages may be cited.
        freshness:
          type: string
          enum:
            - any
            - day
            - week
            - month
            - year
          description: Restricts every search behind the report.
        lang:
          type: string
    ResearchEvent:
      type: object
      description: One frame of the stream. Which fields are present depends on type.
      required:
        - type
      properties:
        type:
          type: string
          enum:
            - started
            - planned
            - searched
            - sources
            - writing
            - delta
            - done
            - error
        round:
          type: integer
          description: On planned and searched.
        questions:
          type: array
          items:
            $ref: '#/components/schemas/Step'
          description: 'On planned: the sub-questions this round will search.'
        step:
          allOf:
            - $ref: '#/components/schemas/Step'
          description: 'On searched: the sub-question that just finished.'
        sources:
          type: array
          items:
            $ref: '#/components/schemas/Source'
          description: 'On sources: everything gathered so far, renumbered.'
        text:
          type: string
          description: 'On delta: a fragment of the answer.'
        report:
          allOf:
            - $ref: '#/components/schemas/Report'
          description: 'On done: the finished report. This one is the authority.'
        message:
          type: string
          description: 'On error: a sentence explaining why there will be no report.'
    Step:
      type: object
      properties:
        round:
          type: integer
        question:
          type: string
          description: The sub-question.
        query:
          type: string
          description: The search it became.
        found:
          type: integer
          description: New sources this step contributed.
    Source:
      type: object
      properties:
        index:
          type: integer
          description: The number used in the answer's citations, starting at 1.
        url:
          type: string
        title:
          type: string
        domain:
          type: string
        excerpt:
          type: string
          description: The part of the page the report was written from.
        published_at:
          type: string
          format: date-time
        cited:
          type: boolean
          description: >-
            Whether the answer actually referred to this source. One that was
            read and not used is still returned, and still worth telling apart
            from one that was.
    Report:
      type: object
      properties:
        id:
          type: string
          examples:
            - rsch_8d3ddecc038c10d4
        question:
          type: string
        answer:
          type: string
          description: >-
            The report, in markdown, with citations written as [n] pointing into
            sources. Empty when nothing readable was found, in which case
            sources is empty too.
        sources:
          type: array
          items:
            $ref: '#/components/schemas/Source'
        steps:
          type: array
          items:
            $ref: '#/components/schemas/Step'
          description: >-
            What the research actually did. Not debug output: a report you
            cannot audit is one you have to take on trust, and the sub-questions
            are often as useful as the answer.
        stats:
          $ref: '#/components/schemas/Stats'
    Failure:
      type: object
      properties:
        success:
          type: boolean
          const: false
        message:
          type: string
        result:
          type: object
          properties:
            code:
              type: string
    Stats:
      type: object
      properties:
        took_ms:
          type: integer
        rounds:
          type: integer
          description: >-
            Search-and-read passes that ran. May be fewer than the depth asked
            for, if nothing was found to be missing.
        searches:
          type: integer
        pages:
          type: integer
        model_calls:
          type: integer
        tokens:
          type: integer
        cached:
          type: boolean
          description: >-
            The same question was asked recently and this is the stored report.
            A cached report is not charged.
  responses:
    Failure:
      description: >-
        The envelope with success false. result carries the machine-readable
        code and nothing else; the HTTP status line carries the real status.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Failure'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An API key, sent as `Authorization: Bearer enc_…`. A dashboard session
        cookie works on the same route.

````