Skip to Content
ArchitectureReasoning serviceAsking and answering
Architecture · Reasoning

Asking the twin, and checking what comes back.

A question passes through the queue, the Compass or Strategy loop, the five tools, the checks on the final turn and the source links.

What it is for

A reader asks a question in plain words: who owns a team, where a change would concentrate risk, what a what-ifWhat-ifA scenario that changes one or more dependencies in the model and reports the result. A what-if changes nothing in the records, and its result is indicative. would do to the dependencyDependencyA link where one unit needs something from another. A dependency is not a reporting line: it shows where work relies on something else.-chain index. The answer is built from records the twinTwinNovusLens’s model of one organisation, built from records about its teams, services and how they rely on one another. It is only as current as those records. returned while that question was being answered, and it shows the records it rests on. Questions are kept for 24 hours, so the asker can come back to them.

How it works

A question goes to one of two routes. The Compass route uses the standard instructions for composing answers. The Strategy route uses its own prompt for broad strategic what-ifs over both the structure records and the meaning canonMeaning canonThe organisation’s supplied written descriptions of its operations, systems and terms. NovusLens stores these separately from records of teams, dependencies and measurements.. Both routes run the same loop, compass.run_loop, with a different system prompt and budget.

The queue

questions.submit writes a row to the question_requests table, defined in apps/graph/migrations/0024_retained_questions.sql, and hands the work to two background workers. The limits are:

  • at most 8 requests queued or running at once, counted in the table, so the limit holds across service instances;
  • at most 2 pending requests per viewer;
  • a question of 1 to 4000 characters;
  • a 15-minute decision deadline, set by the table default. The deadline is checked at every progress step, but a model call already in flight may finish after it;
  • results kept for 24 hours from creation.

A request moves from queued to running. It ends as complete, failed or interrupted. A complete result is stored; interruption is described below. Stopping waiting (questions.stop_waiting) only sets a flag. It does not cancel the computation, which continues within its budget.

The asker’s scopeScopeThe work, data, activity or access that a rule, statement or calculation covers. For access, it names the records an account can see. For team measures, “Own operations” covers the selected unit. “Including descendants” adds the units beneath it. is recorded when the question is submitted. Every progress step recalculates it. If the scope has narrowed or access has been revoked, the request is marked interrupted with the reason access_changed. Reading a saved answer under a changed scope returns a neutral “access changed” state, not the old question text or result. Reading a complete answer also checks its supporting evidence. Changed evidence gives superseded; unreadable evidence gives unavailable. Both ask the reader to ask again; neither is stored on the row.

If the service restarts, nothing in questions.py re-queues a request. A row left queued or running is reported as interrupted once its deadline has passed. Nothing replays a tool call on restart.

The five stored states, and the two ways a saved answer can read differently later.

The loop

Each model turn must return one JSONJSONJavaScript Object Notation: a text format for data, using named fields, lists and values. Configuration files and service messages use it. object. It is either a tool call, {"tool": ..., "args": {...}}, or a final answer. A tool call asks a named function to retrieve or calculate something. Calls run one at a time. Each result goes back into the transcript, labelled as data, and is cut to 8000 characters. The loop allows MAX_CALLS tool calls: 6 for Compass and 8 for Strategy.

Two things happen before the model is asked anything. Ownership and key-person questions are first tried against the records directly, through accountability.answer_from_records, and answered without a model call when that matches. If the model’s output is not one JSON object, it is asked once more to emit exactly one. A final answer given with no tool call at all is sent back once, with an instruction to use the tools. If it still makes no call, the reader gets the insufficient-information sentence.

The tools

The loop can call five tools, listed in tools.TOOLS. Each handler audits the call and returns only visible cellsCellA box on the map that stands for a team, service, supplier or AI agent. It is a unit you can inspect, not necessarily a department or a person.. It filters verdict wording before returning the payloadPayloadThe data carried inside a message, apart from the surrounding fields that identify and date it..

ToolArgumentsWhat it returns
query_graphselect (cells, edges, findings, authority, requests, summary, search), qStructure from the twin. summary gives the organisation header, including critical path lengthCPLCritical path length. The total coordination weight of the longest chain of dependencies in the model. NovusLens reports it as index points, not elapsed time. (CPL) per layer.
query_meaningq, cell_idKnowledge entries from the meaning canon, with their source and attribution.
simulate_scenarioname, overrides (edges with consumer, provider, handoff)A measure recomputed under the stated overrides, with the change in index pointsIndex pointsThe unit of the dependency-chain index. Index points are structural weights, not hours or money, and every answer and what-if reports the index in them. and a framing line.
retrieve_evidencecell_id, attribute, layerThe provenanceProvenanceThe record of where a value came from, who stated it and when. Every value keeps this record. behind a value.
highlightlens (critical-path, rigid, unowned, authority-gaps, domain-misalignment, regulation, control-gaps, key-person)The cells, path members and edges that a lens emphasises.

Every tool result carries a coverageCoverageWhat the available records cover, and what is missing. A count such as “3 of 5 dependencies measured” names the measured portion and the gap. marker: found when records came back, empty when the search ran and matched nothing, and failed when it errored. An absence may rest only on an empty search.

Checks before an answer is shown

When the model gives a final answer, run_loop checks it in this order:

  1. If CONWAY_STRUCTURED_ANSWERS is set to 1, the answer may be a structured final turn. answer_contract.validate requires facts and calculations to name a record returned in this turn. Unsupported claims are withheld. The model gets one repair attempt.
  2. guards.filter_verdicts replaces wording that states a compliance verdict with a marker.
  3. guards.claims_check withholds a sentence whose couplingCouplingA link where one team or service depends on another. Its Loose, Buffered or Rigid class describes the time needed for a handoff. class or day or hour figure does not appear in the tool results.
  4. guards.protected_conclusions withholds assurances, authorisations and statements of what will happen, whatever their phrasing.
  5. If the opening sentence is withheld, the model gets one chance to answer again. If the opening is still unsupported, the whole answer becomes the insufficient-information sentence.
  6. An absence claim is framed as a statement about what the viewer can see. If it rests on a set of records the loop did not search, the model is sent back once to search it.
  7. An answer without a Why: line is sent back once. If it is still missing, the omission is stated.
  8. An answer about a scenario carries the indicativeIndicativeThe label on a scenario or what-if. An indicative result shows one possibility under stated assumptions. It is never a measurement of the future. framing, whether or not the model wrote it. The Strategy route also gets its own framing.
  9. A scenario with no computable baseline replaces the answer with the insufficient-information sentence.

After the loop, questions.execute builds source cards from the retrieved records. claims.attach_claims then links each sentence to the cards whose names it mentions, numbered in order of first use. This links sentences to sources. It does not decide whether a sentence is true; that is the work of the checks above. The saved answer records its time, latest projectionProjectionThe regular pass that brings the read model up to date with the canonical log. It runs every 30 seconds by default. time, guard version and whether its final turn was structured.

When a saved answer is read again, its supporting evidence is checked against the present state. If a cited findingFindingA difference in the records that needs a decision. It includes its sources and can describe a slow handoff, disputed fact or missing owner. or meaning entryMeaning entryA stored written account supplied by the organisation, such as a policy or definition. Answers can cite it without treating it as a measured fact. is no longer visible, the answer disappears from the list. A direct read reports unavailable. If the assessment it cites has changed, the answer is reported as superseded, and the reader is told to ask again. If that check cannot be completed, the saved answer is withheld as unavailable.

Model settings

Compass and Strategy both call chat in apps/mesh/src/conway_mesh/build_agent.py. The call sends system and user messages with model temperatureModel temperatureA setting that controls how much a language model varies its choice of words. Lower values usually produce less varied answers. 0.1 and reasoning_effort: low. On OpenRouter, a JSON request omits temperature when no provider of the model accepts it with the other requested parameters. Both routes use a 240-second timeout and max_tokens: 8192. If the reply content is empty, the code uses the reasoning field instead.

The endpointEndpointA particular address on a service that accepts a defined kind of request. defaults to http://localhost:11434/v1/chat/completions, the local port Ollama uses by default. Set CONWAY_LLM_ENDPOINT to point it elsewhere. The model names are set as follows.

VariableDefaultRead inEffect
CONWAY_LLM_ENDPOINThttp://localhost:11434/v1/chat/completionsconway_mesh/build_agent.pyThe chat-completions URL used for every Compass and Strategy call.
CONWAY_COMPASS_MODELqwen3:30bcompass.pyThe model name sent on every Compass call.
CONWAY_STRATEGY_MODELFalls back to CONWAY_COMPASS_MODEL, then qwen3:30bstrategy.pyThe model name sent on every Strategy call.
CONWAY_STRUCTURED_ANSWERSUnset, so offcompass.pyWhen set to 1, adds the structured final-turn instructions to the system prompt. Check 1 above then applies.

build_agent.py also reads CONWAY_BUILD_MODEL and CONWAY_LLM_MODEL. Compass and Strategy supply their own model names, so those variables do not change this loop.

Interfaces

The service exposes the queue over HTTPHTTPHypertext Transfer Protocol: the rules a browser or program uses to send a request to a web service and receive its response.. The routes are POST /questions to submit, GET /questions to list and GET /questions/{request_id} to read one, and POST /questions/{request_id}/stop-waiting. The full list is on the HTTP endpoints page. Some endpoints call compass.ask or strategy.ask directly, without the queue.

The queue’s internal entry points are questions.submit, questions.read and questions.stop_waiting. The loop calls the five tools in tools.TOOLS and the model through chat. For the overview of the service, see Reasoning.

Invariants and failure behaviour

  • Each tool call carries a viewer. Every tool resolves that viewer to a role and visible cells before it reads anything.
  • Tool results are data. The transcript labels them as data, so an instruction inside a document is quoted, not followed.
  • A tool that raises returns an error, with coverage failed. The model sees that and cannot treat the search as an absence.
  • If the retry still returns unusable JSON, the response is “I could not compose an answer this turn — please rephrase.”
  • An unexpected exception in questions.execute marks the request failed. An answer over 500,000 characters of JSON is refused the same way.
  • Scenario results are indicative only. A scenario re-computes a measure under the overrides the reader stated, and the tool says so in its own result. A scenario answer always carries that framing.
  • Stopping waiting never cancels computation. A restart leaves queued or running requests to run out their deadline and then report them as interrupted.
Last updated