@tangle-network/agent-runtime / tool-loop
optionaltoolCallId?:string
toolName:
string
args:
Record<string,unknown>
One OpenAI-shaped tool-call entry carried on an assistant message.
id:
string
type:
"function"
function:
object
name:
string
arguments:
string
finalText:
string
toolResults:
object[]
call:
ToolLoopCall
label:
string
outcome:
ToolCallOutcome
turns:
number
stopReason:
ToolLoopStopReason
cappedOut:
boolean
Use stopReason !== 'completed' instead.
systemPrompt:
string
userMessage:
string
optionalpriorMessages?:ToolLoopMessage[]
streamTurn: (
messages) =>AsyncIterable<ToolLoopEvent>
AsyncIterable<ToolLoopEvent>
executeToolCall: (
call) =>Promise<ToolCallOutcome>
Promise<ToolCallOutcome>
isExecutableTool: (
toolName) =>boolean
string
boolean
optionalmaxToolTurns?:number
Runaway-backstop cap. Default 200 — set far above any legitimate workflow.
For per-workflow limits, use maxCostUsd or deadlineMs instead.
optionaldeadlineMs?:number
Wall-clock deadline in ms since epoch (Date.now()-based). When exceeded the
loop stops with stopReason deadline.
optionalmaxCostUsd?:number
Maximum total cost in USD. Requires costOf to meter each tool call.
optionalcostOf?: (call,outcome) =>number
Return the USD cost of one outcome. Required for maxCostUsd to work.
number
optionalrenderResult?: (label,outcome) =>string
string
string
optionallabelFor?: (call) =>string
string
optionalrunId?:string
optionalscenarioId?:string
optionalhooks?:RuntimeHooks
Raw
systemPrompt:
string
userMessage:
string
optionalpriorMessages?:ToolLoopMessage[]
streamTurn: (
messages) =>AsyncIterable<Raw>
AsyncIterable<Raw>
extractText: (
event) =>string
Raw
string
extractToolCall: (
event) =>ToolLoopCall|null
Raw
ToolLoopCall | null
isExecutableTool: (
toolName) =>boolean
string
boolean
executeToolCall: (
call) =>Promise<ToolCallOutcome>
Promise<ToolCallOutcome>
optionalmaxToolTurns?:number
Runaway-backstop cap. Default 200 — set far above any legitimate workflow.
optionaldeadlineMs?:number
Wall-clock deadline in ms since epoch (Date.now()-based).
optionalmaxCostUsd?:number
Maximum total cost in USD. Requires costOf to meter each tool call.
optionalcostOf?: (call,outcome) =>number
Return the USD cost of one outcome. Required for maxCostUsd to work.
number
optionalrenderResult?: (label,outcome) =>string
string
string
optionallabelFor?: (call) =>string
string
optionalrunId?:string
optionalscenarioId?:string
optionalhooks?:RuntimeHooks
ToolCallOutcome = {
ok:true;result:unknown; } | {ok:false;code:string;message:string;status?:number; }
Outcome of one tool dispatch — structurally compatible with a hub/integration tool-outcome union, so callers can fold either through the loop.
ToolLoopMessage =
object
A message in the running conversation the loop sends to streamTurn.
The base { role, content } covers system / user / plain assistant
turns. Two optional fields carry the OpenAI function-calling contract so a
strict model (Claude, and any OpenAI-compatible provider that validates tool
history) reads its own tool use back instead of re-issuing the same call:
- an assistant turn that emitted tool calls carries
tool_calls, and itscontentisnullwhen the turn was tool-only; - each tool result is its own
{ role: 'tool', tool_call_id, content }message keyed to the call that produced it.
Widening is additive: a streamTurn that reads only role + content still
works; one that forwards the whole message to an OpenAI-compatible endpoint
now sends correct tool history.
role:
string
content:
string|null
optionaltool_calls?:ToolLoopAssistantToolCall[]
optionaltool_call_id?:string
ToolLoopEvent = {
type:"text";text:string; } | {type:"tool_call";call:ToolLoopCall; } | {type:"other";event:unknown; }
ToolLoopStopReason =
"completed"|"stuck-loop"|"backstop"|"deadline"|"budget"
Why the loop stopped. completed = model finished naturally; stuck-loop =
≥3 consecutive identical tool calls (same tool + args); backstop = hit the
runaway-backstop cap (200 by default); deadline = wall-clock deadlineMs
exceeded; budget = maxCostUsd exhausted. Non-completed stops are infra /
resource outcomes — eval scoring must distinguish them from capability failure.
StreamToolLoopYield<
Raw> = {kind:"event";event:Raw; } | {kind:"tool_result";toolName:string;toolCallId?:string;label:string;outcome:ToolCallOutcome; } | {kind:"capped";pending:number;stopReason:Exclude<ToolLoopStopReason,"completed">; }
Raw
runToolLoop(
opts):Promise<ToolLoopResult>
Run the bounded tool loop and return the final text + every executed tool outcome. Awaitable — callers needing to stream events to a UI use streamToolLoop.
Promise<ToolLoopResult>
streamToolLoop<
Raw>(opts):AsyncGenerator<StreamToolLoopYield<Raw>,void,unknown>
Streaming bounded tool loop: yields each raw turn event (the caller maps +
telemetries + re-emits it) and each executed tool_result; emits one
capped if it stops for any non-completed reason with calls still pending.
Raw
AsyncGenerator<StreamToolLoopYield<Raw>, void, unknown>