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

# Consulting and background work

> Delegate real work from a live call without stalling it, and keep a promise to deliver later, in the StandIn TypeScript SDK.

A voice model has to answer in under a second or the call sounds broken. Real work does not fit in a second.

Looking something up, reading a file, driving a browser, running a tool: those take ten seconds, or five minutes. A caller listening to silence cannot tell thinking from crashed.

So an agent that does real work is two agents. The fast one talks. The slow one works. This is the seam between them, and it is the same whichever provider does the talking and whichever framework does the working.

## Hold on, let me check

`Consultant` runs the slow agent inside a live call. You supply a factory; what it builds is yours.

```ts theme={null}
import { Consultant } from "@komaa/standin-sdk";

const consultant = new Consultant(() => (query) => myAgent.run(query), 45_000);
const answer = await consultant.ask("what did we bill Contoso last quarter?");
```

The first argument is a **factory**, not the agent: it is called on first use and again after a
timeout, because a timed-out consultation cannot be stopped and whatever state it holds is no longer
yours. The second is the timeout in milliseconds, defaulting to `DEFAULT_CONSULT_TIMEOUT_MS`, which
is 45 seconds. `ask` takes an optional second argument that overrides it for one question, which is
how `resume` below runs the same consultant on a five minute budget instead.

<Warning>
  Wrap the method, do not pass it bare. `() => myAgent.run` type-checks and then throws at runtime the
  moment `run` reads anything on `this`, because a method detached from its instance has no instance.
  The same trap catches the `Poster` on [Meeting recap](/typescript-sdk/minutes) and the `Deliverer`
  below. Every callable this SDK takes must arrive already bound.
</Warning>

Two behaviours are not obvious, and both exist because a caller is listening.

**One at a time, and the second asker is told so.** A model that can delegate can delegate twice before the first answer lands. Queueing the second means it waits out both timeouts and answers far too late, so it is refused immediately with a sentence the model can say out loud.

**A timeout is admitted.** The work cannot be cancelled: the promise runs to its end whatever this returns. The answer says it stopped rather than promising a follow-up nothing will send, and the timed-out agent is dropped so the next question builds a fresh one.

<Warning>
  Your asker must not block. Node has one thread, and a synchronous agent holds it for its whole duration, stopping the call's audio along with everything else. Do the work in a promise, a worker, or another process.
</Warning>

<Note>
  `ask` never throws. It always returns something speakable, because an exception in a tool result is an agent that goes quiet mid-sentence.
</Note>

## I'll send you the result

When the work will not finish while somebody waits, it becomes a promise, and a promise made on a call that is about to end has to outlive the process that made it.

`BackgroundTasks` writes each task to disk **before** the work starts and removes it only once the result has actually been delivered.

```ts theme={null}
import { BackgroundTasks } from "@komaa/standin-sdk";

const tasks = new BackgroundTasks();

// during the call
const task = tasks.remember(query, session.start.threadId);
tasks.begin(task);
// ...
tasks.done(task);

// once, at worker startup
await tasks.resume(
  () => (query) => myAgent.run(query),
  (threadId, text) => postToChat(threadId, text),
);
```

`resume` returns how many tasks it actually delivered. Its third argument is the per-task timeout,
`DEFAULT_TASK_TIMEOUT_MS` or five minutes by default, which is far longer than a consultation's
because nobody is on the line waiting for it. `BackgroundTasks` takes `directory`, `ttlMs` and
`resumeLimit`. The directory defaults to a `tasks` folder under `STANDIN_STATE_DIR`, or under
`~/.standin/state` when that is unset. Deliberately never a temp directory: a temp directory passes
every test and loses every parked promise on the next reboot, which is invisible until somebody is
waiting for an answer that no longer exists.

The two-phase claim is what makes a restart safe. A task waiting to run is a `.json`; a task being run is a `.claimed`. A process that dies mid-run leaves a claim behind, and the next startup takes it back once it is old enough to be sure nobody is still working on it. A delivery that fails puts the task back rather than dropping it.

| Guard                   | Why                                                                                                                  |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Two hour expiry         | An answer long after the question is worse than no answer.                                                           |
| Five tasks per startup  | A restart storm must not fan out one agent per promise across the fleet. What is left stays queued, never discarded. |
| No thread, no task      | A task with nowhere to deliver cannot be kept, so it is dropped loudly at startup rather than run for nothing.       |
| Ten minute claim window | Taking a claim somebody is still working on delivers the same answer twice.                                          |

## The two tools

Both are `ToolSpec` values, ready to register on `CallTools`. They are not built in, because an agent with no second agent behind it should not be told it has one.

```ts theme={null}
import { BACKGROUND_TASK_TOOL, CONSULT_TOOL } from "@komaa/standin-sdk";

tools.register(CONSULT_TOOL, (params) => consultant.ask(String(params.query ?? "")));
tools.register(BACKGROUND_TASK_TOOL, startBackgroundTask);
```

Their descriptions are deliberately distinguishable. Without that, a model picks whichever comes first and every long job is run while somebody waits on the line.

Once the background tool is registered, pass `true` as the `Consultant`'s third argument and a timeout will point the caller at it. Leave it off and it will not, because promising a path that does not exist is worse than admitting the work stopped.

## Next

<CardGroup cols={2}>
  <Card title="Call tools" icon="wrench" href="/typescript-sdk/call-tools">
    Where both tools are registered, beside the built-ins.
  </Card>

  <Card title="Meeting recap" icon="file-lines" href="/typescript-sdk/minutes">
    The other job a `Consultant` is normally handed.
  </Card>

  <Card title="Reaching people" icon="phone-volume" href="/typescript-sdk/reaching-people">
    When the answer should ring somebody back rather than land in a chat.
  </Card>

  <Card title="Configuration" icon="sliders" href="/typescript-sdk/configuration">
    `STANDIN_STATE_DIR` and every other variable the SDK reads.
  </Card>
</CardGroup>
