AI.Completion (fnord v0.9.40)

View Source

This module sends a request to the model and handles the response. It is able to handle tool calls and responses.

Input options

Output options

Output is controlled by the following mechanisms.

  1. log_msgs - log messages from the user and assistant as info
  2. log_tool_calls - log tool calls as info and tool call results as debug

LOGGER_LEVEL must be set to debug to see the output of tool call results.

Summary

Functions

If a tool produced a very large textual output, attempt to write it to a temporary file and replace the in-memory content with a short placeholder that points to the temp file and includes a preview. Fail silently and return the original content on any error.

Returns a map of tool names to the number of times each tool was called during this completion round - that is, in messages that were appended after AI.Completion.new/1 captured the starting message length.

Types

response()

@type response() ::
  {:ok, t()}
  | {:error, t()}
  | {:error, binary()}
  | {:error, :context_length_exceeded, non_neg_integer()}

t()

@type t() :: %AI.Completion{
  archive_notes: boolean(),
  compact?: bool(),
  conversation_pid: term(),
  initial_message_count: non_neg_integer(),
  is_compacting?: bool(),
  log_msgs: boolean(),
  log_tool_calls: boolean(),
  messages: [AI.Util.msg()],
  model: String.t(),
  name: String.t() | nil,
  replay_conversation: boolean(),
  response: String.t() | nil,
  response_format: map() | nil,
  specs: [AI.Tools.tool_spec()] | nil,
  tool_call_requests: list(),
  tool_round_cap: pos_integer(),
  tool_round_count: non_neg_integer(),
  toolbox: AI.Tools.toolbox() | nil,
  usage: integer(),
  verbosity: String.t() | nil,
  web_search?: boolean()
}

Functions

get(opts)

@spec get(Keyword.t()) :: response()

handle_tool_call(state, map)

maybe_offload_tool_output(content)

If a tool produced a very large textual output, attempt to write it to a temporary file and replace the in-memory content with a short placeholder that points to the temp file and includes a preview. Fail silently and return the original content on any error.

new(opts)

@spec new(Keyword.t()) :: {:ok, t()} | {:error, any()}

new_from_conversation(conversation, opts)

@spec new_from_conversation(Store.Project.Conversation.t(), Keyword.t()) ::
  {:ok, t()} | {:error, :conversation_not_found}

tools_used(state)

@spec tools_used(t()) :: %{required(binary()) => non_neg_integer()}

Returns a map of tool names to the number of times each tool was called during this completion round - that is, in messages that were appended after AI.Completion.new/1 captured the starting message length.

This delimiter is stable in the presence of mid-loop interrupt injection. An earlier implementation used "tools after the last user message" as the round boundary, which broke whenever a user interjection arrived mid-round: the injected user message became the new "last user", hiding any tool calls that had already executed earlier in the same round. That bug silently dropped the editing_tools_used flag and skipped end-of-session worktree merges.

Falls back to scanning all messages when initial_message_count is missing (older state structs that predate this field).

Counts a tool invocation as exactly one unique call_id, not one FunctionCall struct. The Responses-native shape pairs each %AI.Message.FunctionCall{} (request) with a %AI.Message.FunctionCallOutput{} (result) - both carry the same call_id. Deduping by call_id means:

  • the normal paired case (request + output both present) counts once;
  • a future code path that loses one half of the pair still counts the invocation (under "<unknown>" if only the output survives, since the name lives on the request side);

...so downstream gates like editing_tools_used and the save_skill success check don't silently flip off if the message slice ever drifts asymmetric. Legacy nested-tool_calls shape is still accumulated separately for old conversations that predate the canonical structs.