Packages
fnord
0.9.37
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/message/function_call.ex
defmodule AI.Message.FunctionCall do
@moduledoc """
Assistant-emitted tool call request. Wire shape:
%{type: "function_call", call_id: "...", name: "tool_name",
arguments: "<json-encoded-string>"}
## Critical invariant
`arguments` MUST be a JSON-encoded **string**, NEVER a decoded map. The
previous Responses API attempt drove off this cliff: when arguments are
stored as maps, `Util.string_keys_to_atoms/1` recurses into them on read
and atomizes LLM-emitted garbage keys, exhausting the BEAM atom table.
See engram memory "Conversation file corruption - responses branch tool
arguments".
Callers that need to parse the arguments map can do so on demand at the
call site (where the keys are bounded by the tool's schema), but the
canonical form on disk and in memory is the raw JSON string.
"""
@behaviour AI.Message
@derive {Jason.Encoder, only: [:type, :call_id, :name, :arguments]}
@enforce_keys [:call_id, :name, :arguments]
defstruct [:call_id, :name, :arguments, type: "function_call"]
@type t :: %__MODULE__{
call_id: binary(),
name: binary(),
arguments: binary()
}
@doc """
Build a function-call request. `arguments` must already be a JSON string.
"""
@spec new(binary(), binary(), binary()) :: t()
def new(call_id, name, arguments)
when is_binary(call_id) and is_binary(name) and is_binary(arguments) do
%__MODULE__{call_id: call_id, name: name, arguments: arguments}
end
# FunctionCall items have no textual content of their own; transcript
# rendering happens at the call site where the corresponding output is also
# available.
@impl AI.Message
def text(%__MODULE__{}), do: nil
@impl AI.Message
def for_transcript(%__MODULE__{name: name, arguments: args}) do
"""
# TOOL CALL: #{name}
Arguments: #{args}
"""
end
@impl AI.Message
def to_map(%__MODULE__{call_id: call_id, name: name, arguments: arguments}) do
%{type: "function_call", call_id: call_id, name: name, arguments: arguments}
end
@doc """
Hydrate from a Responses-shaped map (atom or string keys). Re-encodes
`arguments` to a JSON string if a legacy v0 conversation file stored it as
a decoded map - the atom-table-safe invariant on disk is "string", not
"map".
"""
@spec from_map(map()) :: t()
def from_map(raw) do
call_id = AI.Message.get(raw, :call_id) || AI.Message.get(raw, :id) || ""
name = AI.Message.get(raw, :name) || ""
arguments = normalize_arguments(AI.Message.get(raw, :arguments))
%__MODULE__{call_id: call_id, name: name, arguments: arguments}
end
defp normalize_arguments(args) when is_binary(args), do: args
defp normalize_arguments(args) when is_map(args) do
# Defensive re-encode for older shapes that decoded the JSON. SafeJson is
# the project's wrapper around Jason; this never raises because we always
# have a real map here.
case SafeJson.encode(args) do
{:ok, json} -> json
{:error, _} -> "{}"
end
end
defp normalize_arguments(nil), do: "{}"
end