Skip to main content
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.
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.
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 and the Deliverer below. Every callable this SDK takes must arrive already bound.
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.
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.
ask never throws. It always returns something speakable, because an exception in a tool result is an agent that goes quiet mid-sentence.

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.
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.

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.
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

Call tools

Where both tools are registered, beside the built-ins.

Meeting recap

The other job a Consultant is normally handed.

Reaching people

When the answer should ring somebody back rather than land in a chat.

Configuration

STANDIN_STATE_DIR and every other variable the SDK reads.