generateWithTools method
- String prompt, {
- LLMGenerationOptions? llmOptions,
- ToolCallingOptions? options,
- ToolChoiceMode? toolChoice,
- String? forcedToolName,
- bool? validateCalls,
Generate text with tool calling support. Delegates the full
parse-execute-loop to commons via
rac_tool_calling_session_create_proto; Dart only runs registered
executors when commons emits a tool_call event.
toolChoice mirrors the OpenAI tool_choice knob: callers can pin
the LLM to NONE / AUTO / SPECIFIC without having to manually mutate
a ToolCallingOptions proto. When non-null it overrides
options.toolChoice for this call.
forcedToolName is the companion to toolChoice=SPECIFIC — the
tool name the LLM is forced to invoke. Overrides
options.forcedToolName when non-null.
Mirrors Swift RunAnywhere.generateWithTools(prompt:options:toolOptions:toolChoice:forcedToolName:validateCalls:)
(RunAnywhere+ToolCalling.swift:250-280, makeRunLoopRequest:491-548):
tool options take precedence per knob, falling back to the LLM
generation llmOptions; top_p always comes from the LLM options;
validate_calls is left UNSET unless the caller supplies it (commons
applies its documented default).
Implementation
Future<ToolCallingResult> generateWithTools(
String prompt, {
LLMGenerationOptions? llmOptions,
ToolCallingOptions? options,
ToolChoiceMode? toolChoice,
String? forcedToolName,
bool? validateCalls,
}) async {
// Swift default: `options: RALLMGenerationOptions = .defaults()`.
final llm = llmOptions ?? _defaultLLMOptions();
// Swift: `toolOptions ?? (options.hasToolCalling ? options.toolCalling
// : RAToolCallingOptions.defaults())`.
final opts =
(options ??
(llm.hasToolCalling()
? llm.toolCalling
: _defaultToolOptions()))
.deepCopy();
if (toolChoice != null) {
opts.toolChoice = toolChoice;
}
if (forcedToolName != null) {
opts.forcedToolName = forcedToolName;
}
final tools = opts.tools.isNotEmpty ? opts.tools : getRegisteredTools();
final autoExecute = opts.hasAutoExecute() ? opts.autoExecute : true;
// Mirrors Swift makeRunLoopRequest (RunAnywhere+ToolCalling.swift:491-548).
final request = ToolCallingSessionCreateRequest(
prompt: prompt,
tools: tools,
format: opts.format,
maxToolCalls: opts.maxToolCallCount,
keepToolsAvailable: opts.keepToolsAvailable,
maxTokens: (opts.hasMaxTokens() && opts.maxTokens > 0)
? opts.maxTokens
: llm.maxTokens,
temperature: opts.hasTemperature() ? opts.temperature : llm.temperature,
topP: llm.topP,
// Suppress thinking when either options surface asks for it.
disableThinking: opts.disableThinking || llm.disableThinking,
autoExecute: autoExecute,
replaceSystemPrompt: opts.replaceSystemPrompt,
requireJsonArguments: opts.requireJsonArguments,
);
// `validate_calls` is `optional bool` on the proto — leave it UNSET when
// the caller did not supply a value so commons applies its documented
// default (true).
if (validateCalls != null) {
request.validateCalls = validateCalls;
}
if (opts.toolChoice != ToolChoiceMode.TOOL_CHOICE_MODE_UNSPECIFIED) {
request.toolChoice = opts.toolChoice;
}
if (opts.hasForcedToolName() && opts.forcedToolName.isNotEmpty) {
request.forcedToolName = opts.forcedToolName;
}
// System prompt: tool options win, then the LLM options.
if (opts.hasSystemPrompt() && opts.systemPrompt.isNotEmpty) {
request.systemPrompt = opts.systemPrompt;
} else if (llm.hasSystemPrompt() && llm.systemPrompt.isNotEmpty) {
request.systemPrompt = llm.systemPrompt;
}
final session = DartBridgeToolCalling.shared.createSession(request);
// Publish the active session handle so consumers can
// call `RunAnywhereTools.shared.cancelGeneration()` to interrupt the
// in-flight loop (mirrors RunAnywhereLLM.cancelGeneration). The native
// session id is published synchronously before native generation starts,
// so expose it as soon as the worker forwards that callback.
unawaited(
session.sessionId
.then((id) => _activeSessionHandle = id)
.catchError((Object _) => _activeSessionHandle),
);
final collectedCalls = <ToolCall>[];
final collectedResults = <ToolResult>[];
final completer = Completer<ToolCallingResult>();
late final StreamSubscription<ToolCallingSessionEvent> sub;
sub = session.events.listen(
(event) async {
switch (event.whichKind()) {
case ToolCallingSessionEvent_Kind.toolCall:
final call = event.toolCall;
collectedCalls.add(call);
_logger.info('Tool call detected: ${call.name}');
if (!autoExecute) {
if (!completer.isCompleted) {
completer.complete(
ToolCallingResult(
text: '',
toolCalls: collectedCalls,
toolResults: collectedResults,
isComplete: false,
),
);
}
await sub.cancel();
return;
}
// Forward the result to the session worker, which fills in its own
// session id and runs the next turn off the UI isolate; the turn's
// events arrive back on this stream.
try {
final result = await execute(call);
collectedResults.add(result);
session.stepWithResult(
toolCallId: call.id,
resultJson: result.resultJson,
error: result.error,
);
} catch (e) {
_logger.error('Tool executor threw: $e');
session.stepWithResult(
toolCallId: call.id,
resultJson: '',
error: e.toString(),
);
}
break;
case ToolCallingSessionEvent_Kind.finalResult:
if (!completer.isCompleted) {
completer.complete(event.finalResult);
}
await sub.cancel();
break;
case ToolCallingSessionEvent_Kind.errorBytes:
if (!completer.isCompleted) {
completer.completeError(
StateError('Tool calling session error bytes received'),
);
}
await sub.cancel();
break;
case ToolCallingSessionEvent_Kind.llmStreamEventBytes:
case ToolCallingSessionEvent_Kind.notSet:
break;
}
},
onError: (Object error, StackTrace stackTrace) async {
if (!completer.isCompleted) {
completer.completeError(error, stackTrace);
}
await sub.cancel();
},
);
try {
return await completer.future;
} finally {
// Clear the published handle BEFORE close — once close
// returns, any pending cancelGeneration() call would race a freshly
// started session.
if (_activeSessionHandle == session.resolvedSessionId) {
_activeSessionHandle = 0;
}
await session.close();
}
}