generate method

  1. @override
Future<GenerateResult> generate(
  1. GenerateTurnState envelope,
  2. ActionFnArg<ModelResponseChunk, GenerateActionOptions, void> ctx,
  3. Future<GenerateResult> next(
    1. GenerateTurnState envelope,
    2. ActionFnArg<ModelResponseChunk, GenerateActionOptions, void> ctx
    )
)

Middleware for the top-level generate call.

Wraps the entire generation process, including the tool loop.

next is the function to call to proceed with the generation.

Implementation

@override
Future<GenerateResult> generate(
  GenerateTurnState envelope,
  ActionFnArg<ModelResponseChunk, GenerateActionOptions, void> ctx,
  Future<GenerateResult> Function(
    GenerateTurnState envelope,
    ActionFnArg<ModelResponseChunk, GenerateActionOptions, void> ctx,
  )
  next,
) async {
  final options = envelope.request;

  // Capture the latest messages for optional history forwarding. Note:
  // _delegationCount is NOT reset here — the hook runs on every tool-loop
  // turn, but the count must accumulate across the entire generate() call.
  _conversationMessages = options.messages;

  final agentList = <String>[];
  // Whether any sub-agent can leave a continuable handle (server-managed).
  // The continue tool is registered unconditionally (the sync `tools` getter
  // cannot resolve handles), but this async hook can, so the continue
  // guidance is gated on a real capability instead of always shown. Unknown
  // metadata counts as continuable (the safe default, matching isClientManaged).
  var anyContinuable = false;
  for (final agentName in _agentNames) {
    final description = await _discoverDescription(agentName);
    agentList.add('  - ${_makeToolName(_prefix, agentName)}: $description');
    final handle = await _resolveHandle(agentName);
    if (handle != null && !handle.isClientManaged) anyContinuable = true;
  }

  final agentsInstructions = _buildInstructions(agentList, anyContinuable);

  final messages = List<Message>.from(options.messages);

  // Check if we've already injected (multi-turn).
  final alreadyInjected = messages.any(
    (msg) => msg.content.any(
      (part) => part.isText && part.metadata?[_markerKey] == true,
    ),
  );

  if (!alreadyInjected) {
    final systemIdx = messages.indexWhere((m) => m.role == Role.system);
    if (systemIdx != -1) {
      final systemMsg = messages[systemIdx];
      messages[systemIdx] = Message(
        role: systemMsg.role,
        content: [
          ...systemMsg.content,
          TextPart(text: agentsInstructions, metadata: {_markerKey: true}),
        ],
        metadata: systemMsg.metadata,
      );
    } else {
      messages.insert(
        0,
        Message(
          role: Role.system,
          content: [
            TextPart(text: agentsInstructions, metadata: {_markerKey: true}),
          ],
        ),
      );
    }
  }

  final newOptions = GenerateActionOptions(
    model: options.model,
    docs: options.docs,
    messages: messages,
    tools: options.tools,
    toolChoice: options.toolChoice,
    config: options.config,
    output: options.output,
    resume: options.resume,
    returnToolRequests: options.returnToolRequests,
    maxTurns: options.maxTurns,
    stepName: options.stepName,
  );

  return next(envelope.copyWith(request: newOptions), ctx);
}