Powered by AppSignal & Oban Pro

Support Agent

support_agent.livemd

Support Agent

This Livebook runs the package-level support-agent example from examples/. The agent, action, and operation control each have a separate source file. The scripted model belongs to this example. None of these modules are part of the production Jidoka compilation.

The example is deterministic. It does not use credentials, network access, or recorded model responses.

package_root = Path.expand("../..", __DIR__)

Mix.install(
  [
    {:kino, "~> 0.14"},
    {:jidoka, path: package_root}
  ],
  consolidate_protocols: false,
  lockfile: Path.join(package_root, "mix.lock")
)

Code.require_file(Path.expand("../loader.exs", __DIR__))
JidokaExamples.Loader.load!(__DIR__)

alias JidokaExamples.SupportAgent.Agent, as: SupportAgent
alias JidokaExamples.SupportAgent.Scenario
alias JidokaExamples.SupportAgent.View

Jidoka.Kino.setup_notebook(model: "test:support-example-model", check_provider?: false)

Application.put_env(
  :jidoka,
  :snapshot_signing_secret,
  "support livebook snapshot secret is at least thirty-two bytes"
)

Inspect The Example Files

[Path.join(__DIR__, "example.exs") | Path.wildcard(Path.join(__DIR__, "lib/**/*.ex"))]
|> Enum.map(&Path.relative_to(&1, package_root))

Mix compiles these files for ExUnit. The command runner and Livebook load the same files directly.

Inspect The Agent

{:ok, _inspection} = Jidoka.Kino.debug_agent(SupportAgent)
{:ok, _diagram} = Jidoka.Kino.agent_diagram(SupportAgent)

Inspect The First Prompt

Preflight creates the prompt and tool definition. It does not call the scripted model or run the action.

request =
  Jidoka.Turn.Request.new!(
    input: "Check order A1001 and tell me what to do next.",
    context: %{account_id: "acct_123", actor_id: "user_123"}
  )

{:ok, preflight} = Jidoka.Kino.preflight(SupportAgent, request)

%{
  messages: preflight.prompt.messages,
  tools: Enum.map(preflight.prompt.operations, & &1.name),
  diagnostics: preflight.diagnostics
}

Run The Deterministic Tool Path

The scripted model requests lookup_order. The control allows normal order access. The action returns the order, and Jidoka puts that result in the next model prompt.

observer = self()
{:ok, result} = Scenario.execute(observer: observer)

control_call =
  receive do
    {:order_control_called, operation, arguments, approval_required?} ->
      %{
        operation: operation,
        arguments: arguments,
        approval_required?: approval_required?
      }
  after
    0 -> :missing
  end

action_call =
  receive do
    {:lookup_order_called, order_id} ->
      %{order_id: order_id}
  after
    0 -> :missing
  end

model_observation =
  receive do
    {:order_observation_seen, order} -> order
  after
    0 -> :missing
  end
operation_result = List.first(result.agent_state.operation_results)

checks = %{
  control_allowed_access: control_call.approval_required? == false,
  action_loaded_order: action_call == %{order_id: "A1001"},
  model_received_action_result: model_observation == operation_result.output,
  final_answer_used_order:
    result.content ==
      "Order A1001 is in transit with UPS. ETA: the next business day. " <>
        "Tell the customer the package is on schedule and ask them to watch for delivery updates."
}

unless Enum.all?(checks, fn {_name, passed?} -> passed? end) do
  raise "support-agent example failed: #{inspect(checks)}"
end

%{
  checks: checks,
  answer: result.content,
  operation: Jidoka.project(operation_result)
}

Inspect The Runtime

{:ok, _timeline} = Jidoka.Kino.timeline(result)
{:ok, _graph} = Jidoka.Kino.call_graph(result)

Project Stable UI State

The view owns visible application data. It does not own a runtime process, transcript store, or hidden agent state.

{:ok, view} = View.initial(%{conversation_id: "order-a1001"})

projected_view =
  view
  |> View.before_turn("Check order A1001")
  |> View.after_turn({:ok, result})

%{
  status: projected_view.status,
  messages: View.visible_messages(projected_view),
  activity: projected_view.events
}

Block Sensitive Input Before Effects

blocked_model = fn _intent, _journal, _context ->
  raise "the sensitive input control did not stop the model"
end

{:error, blocked_input} =
  Jidoka.turn(SupportAgent, "Use credential: support-admin", llm: blocked_model)

Jidoka.error_to_map(blocked_input)

Require Human Review

A credential reference marks the order access as authenticated. The operation control pauses the turn before the action runs.

review_observer = self()
{:ok, action_counter} = Elixir.Agent.start_link(fn -> 0 end)

{:hibernate, snapshot} =
  Scenario.execute(
    observer: review_observer,
    counter: action_counter,
    credential_ref: "credential:support-demo"
  )

review_control_call =
  receive do
    {:order_control_called, operation, arguments, approval_required?} ->
      %{
        operation: operation,
        arguments: arguments,
        approval_required?: approval_required?
      }
  after
    0 -> :missing
  end

{:ok, [review]} = Jidoka.pending_reviews(snapshot)

{:ok, serialized_snapshot} = Jidoka.Snapshot.serialize(snapshot)
{:ok, restored_snapshot} = Jidoka.Snapshot.deserialize(serialized_snapshot)

unless Elixir.Agent.get(action_counter, & &1) == 0 and
         restored_snapshot.turn_state.agent_state.operation_results == [] do
  raise "the action ran before approval"
end

%{
  control: review_control_call,
  operation: review.operation,
  arguments: review.arguments,
  reason: review.reason,
  snapshot_schema: restored_snapshot.schema_version,
  serialized_bytes: byte_size(serialized_snapshot)
}

Approve And Resume

Approval resumes the serialized snapshot. This simulates storage outside the agent process. Jidoka runs lookup_order once and returns its result to the scripted model.

{:ok, approved_result} =
  Scenario.approve(serialized_snapshot, review,
    observer: review_observer,
    counter: action_counter
  )

receive do
  {:lookup_order_called, "A1001"} -> :action_ran_once
after
  0 -> raise "approved action did not run"
end

unless Elixir.Agent.get(action_counter, & &1) == 1 do
  raise "the approved action did not run exactly once"
end

receive do
  {:order_observation_seen, %{"order_id" => "A1001", "status" => "in_transit"}} ->
    approved_result.content
after
  0 -> raise "approved action result did not reach the scripted model"
end
{:ok, _approved_timeline} = Jidoka.Kino.timeline(approved_result)

Keep A Not-Found Result Intact

The edge case uses the same controlled tool-call path. The scripted model must not invent carrier or ETA text when the action returns a not-found result.

not_found_observer = self()
{:ok, not_found_result} = Scenario.execute(order_id: " z9999 ", observer: not_found_observer)

not_found_observation =
  receive do
    {:order_observation_seen, order} -> order
  after
    0 -> :missing
  end

not_found_checks = %{
  normalized_id: not_found_observation["order_id"] == "Z9999",
  status_preserved: not_found_observation["status"] == "not_found",
  clear_answer:
    not_found_result.content ==
      "Order Z9999 was not found. Ask the customer to confirm the order id.",
  no_blank_fields:
    not String.contains?(not_found_result.content, ["with .", "ETA: ."])
}

unless Enum.all?(not_found_checks, fn {_name, passed?} -> passed? end) do
  raise "not-found example failed: #{inspect(not_found_checks)}"
end

not_found_checks