///|
/// Conformance testkit for Posoco (M0-T04).
///
/// Scripted, recording fakes that let tests assert behavior contracts
/// (event order, metadata fidelity, tool-outcome consistency) rather than
/// implementation details. Distinct from the minimal Mock* types used by the
/// blackbox tests: the testkit records call order, arguments, and metadata
/// snapshots, and fails loudly when a script is exhausted.
///
/// The testkit is a test/support layer only — it never enters the production
/// Agent dependency direction.
///|
fn testkit_snapshot_json(value : Json) -> Json {
match value {
Null => Json::null()
True => Json::boolean(true)
False => Json::boolean(false)
Number(number, repr~) => Json::number(number, repr?)
String(text) => Json::string(text)
Array(values) => Json::array(values.map(testkit_snapshot_json))
Object(values) => {
let copied : Map[String, Json] = Map::from_array([])
for key in values.keys() {
copied[key] = testkit_snapshot_json(values[key])
}
Json::object(copied)
}
}
}
///|
fn testkit_snapshot_tool_call(call : ToolCall) -> ToolCall {
{
id: call.id,
name: call.name,
arguments: testkit_snapshot_json(call.arguments),
}
}
///|
fn testkit_snapshot_tool_def(tool : ToolDef) -> ToolDef {
{
name: tool.name,
description: tool.description,
input_schema: testkit_snapshot_json(tool.input_schema),
metadata: match tool.metadata {
Some(value) => Some(testkit_snapshot_json(value))
None => None
},
source: tool.source,
}
}
///|
fn testkit_snapshot_message(message : Message) -> Message {
{
role: message.role,
content: message.content.copy(),
tool_calls: message.tool_calls.map(testkit_snapshot_tool_call),
tool_call_id: message.tool_call_id,
name: message.name,
reasoning_content: message.reasoning_content,
}
}
///|
fn testkit_snapshot_messages(messages : Array[Message]) -> Array[Message] {
messages.map(testkit_snapshot_message)
}
///|
fn testkit_snapshot_tool_result(result : ToolResult) -> ToolResult {
{
content: result.content,
structured: match result.structured {
Some(value) => Some(testkit_snapshot_json(value))
None => None
},
is_error: result.is_error,
}
}
///|
fn testkit_snapshot_tool_outcome(
outcome : ScriptedToolOutcome,
) -> ScriptedToolOutcome {
match outcome {
OutcomeOk(result) => OutcomeOk(testkit_snapshot_tool_result(result))
OutcomeErr(error) => OutcomeErr(error)
}
}
///|
fn testkit_snapshot_metadata(metadata : Map[String, Json]) -> Map[String, Json] {
let copied : Map[String, Json] = Map::from_array([])
for key in metadata.keys() {
copied[key] = testkit_snapshot_json(metadata[key])
}
copied
}
///|
fn testkit_snapshot_session(session : Session) -> Session {
{
messages: testkit_snapshot_messages(session.messages),
metadata: testkit_snapshot_metadata(session.metadata),
}
}
///|
fn testkit_snapshot_response(response : ModelResponse) -> ModelResponse {
{
message: testkit_snapshot_message(response.message),
tool_calls: response.tool_calls.map(testkit_snapshot_tool_call),
finish_reason: response.finish_reason,
usage: response.usage,
reasoning_content: response.reasoning_content,
}
}
///|
fn testkit_snapshot_turn_result(result : TurnResult) -> TurnResult {
{
message: testkit_snapshot_message(result.message),
tool_results: result.tool_results.map(testkit_snapshot_tool_result),
final_session_id: result.final_session_id,
}
}
///|
fn testkit_snapshot_stage(stage : Stage) -> Stage {
match stage {
BeforeTurn => BeforeTurn
BeforeModel(messages) => BeforeModel(testkit_snapshot_messages(messages))
AfterModel(response) => AfterModel(testkit_snapshot_response(response))
ModelError(reason) => ModelError(reason)
BeforeTool(call) => BeforeTool(testkit_snapshot_tool_call(call))
AfterTool(call, result) =>
AfterTool(
testkit_snapshot_tool_call(call),
testkit_snapshot_tool_result(result),
)
ToolError(call, error) => ToolError(testkit_snapshot_tool_call(call), error)
AfterTurn(result) => AfterTurn(testkit_snapshot_turn_result(result))
TurnError(error) => TurnError(error)
}
}
///|
fn testkit_snapshot_event(event : TurnEvent) -> TurnEvent {
match event {
TurnStarted => TurnStarted
ToolCallPending(call) => ToolCallPending(testkit_snapshot_tool_call(call))
ToolCallResult(call~, result~, is_error~) =>
ToolCallResult(
call=testkit_snapshot_tool_call(call),
result=testkit_snapshot_tool_result(result),
is_error~,
)
ModelResponseReceived(message~, usage~, reasoning_content~) =>
ModelResponseReceived(
message=testkit_snapshot_message(message),
usage~,
reasoning_content~,
)
SessionRedirect(from~, to~, messages_before~, messages_after~) =>
SessionRedirect(from~, to~, messages_before~, messages_after~)
TurnCompleted => TurnCompleted
TurnFailed(reason) => TurnFailed(reason)
ToolCallDeferred(call~, reason~) =>
ToolCallDeferred(call=testkit_snapshot_tool_call(call), reason~)
StreamChunkReceived(chunk~) => StreamChunkReceived(chunk~)
Custom(source~, label~, data~) =>
Custom(source~, label~, data=testkit_snapshot_json(data))
}
}
// ---------------------------------------------------------------------------
// ScriptedModel — returns responses/errors/stream chunks by call sequence.
// ---------------------------------------------------------------------------
///|
/// A single scripted model interaction: a response, an exact stream transcript,
/// or a typed error. `Stream` never synthesizes chunks from the final response.
pub(all) enum ScriptedModelStep {
Respond(ModelResponse)
Stream(chunks~ : Array[StreamChunk], response~ : ModelResponse)
Fail(ModelError)
}
///|
fn testkit_snapshot_model_step(step : ScriptedModelStep) -> ScriptedModelStep {
match step {
Respond(response) => Respond(testkit_snapshot_response(response))
Stream(chunks~, response~) =>
Stream(chunks=chunks.copy(), response=testkit_snapshot_response(response))
Fail(error) => Fail(error)
}
}
///|
/// ModelPort fake that plays a fixed script. Each `chat` call consumes one
/// step in order. When the script is exhausted the next call fails with
/// `ModelError::Transport("scripted_model_exhausted at call ")` — it never
/// silently repeats the last response.
pub(all) struct ScriptedModel {
steps : Array[ScriptedModelStep]
mut index : Int
mut calls : Int
received_messages : Array[Array[Message]]
received_tools : Array[Array[ToolDef]]
received_options : Array[ChatOptions]
received_chunks : Array[StreamChunk]
}
///|
pub fn ScriptedModel::new(steps : Array[ScriptedModelStep]) -> ScriptedModel {
{
steps: steps.map(testkit_snapshot_model_step),
index: 0,
calls: 0,
received_messages: [],
received_tools: [],
received_options: [],
received_chunks: [],
}
}
///|
/// Number of chat/chat_streaming calls observed so far.
pub fn ScriptedModel::call_count(self : ScriptedModel) -> Int {
self.calls
}
///|
/// Direct (non-trait) chat entry point for testkit self-tests.
/// Trait methods on concrete types must be dispatched through a `&ModelPort`
/// reference; this wrapper lets tests call chat without an Agent.
pub async fn ScriptedModel::chat_direct(
self : ScriptedModel,
messages : Array[Message],
tools : Array[ToolDef],
options : ChatOptions,
) -> ModelResponse raise ModelError {
(self as &ModelPort).chat(messages, tools, options)
}
///|
/// Direct streaming entry point for testkit self-tests.
pub async fn ScriptedModel::chat_streaming_direct(
self : ScriptedModel,
messages : Array[Message],
tools : Array[ToolDef],
options : ChatOptions,
on_chunk : (StreamChunk) -> Unit,
) -> ModelResponse raise ModelError {
(self as &ModelPort).chat_streaming(messages, tools, options, on_chunk)
}
///|
/// Snapshot of all chat options received, in call order.
pub fn ScriptedModel::options_received(
self : ScriptedModel,
) -> Array[ChatOptions] {
self.received_options.copy()
}
///|
fn ScriptedModel::take_step(
self : ScriptedModel,
messages : Array[Message],
tools : Array[ToolDef],
options : ChatOptions,
) -> ScriptedModelStep raise ModelError {
self.calls = self.calls + 1
self.received_messages.push(testkit_snapshot_messages(messages))
self.received_tools.push(tools.map(testkit_snapshot_tool_def))
self.received_options.push(options)
if self.index < self.steps.length() {
let step = self.steps[self.index]
self.index = self.index + 1
step
} else {
raise Transport(
"scripted_model_exhausted at call \{self.calls} (script had \{self.steps.length()} steps)",
)
}
}
///|
pub impl ModelPort for ScriptedModel with fn chat(
self,
messages : Array[Message],
tools : Array[ToolDef],
options : ChatOptions,
) -> ModelResponse raise ModelError {
match self.take_step(messages, tools, options) {
Respond(resp) => testkit_snapshot_response(resp)
Stream(response~, ..) => testkit_snapshot_response(response)
Fail(e) => raise e
}
}
///|
/// Streaming variant replays exactly the configured chunks, in order.
pub impl ModelPort for ScriptedModel with fn chat_streaming(
self,
messages : Array[Message],
tools : Array[ToolDef],
options : ChatOptions,
on_chunk : (StreamChunk) -> Unit,
) -> ModelResponse raise ModelError {
match self.take_step(messages, tools, options) {
Stream(chunks~, response~) => {
for chunk in chunks {
self.received_chunks.push(chunk)
on_chunk(chunk)
}
testkit_snapshot_response(response)
}
Respond(response) => testkit_snapshot_response(response)
Fail(e) => raise e
}
}
// ---------------------------------------------------------------------------
// RecordingToolProvider — records list/execute order and configurable outcomes.
// ---------------------------------------------------------------------------
///|
/// Configurable outcome for a tool call: success result, or raised runtime error.
/// Constructors are prefixed with `Outcome` to avoid ambiguity with
/// `Result::Ok`/`Result::Err` at unqualified call sites.
pub(all) enum ScriptedToolOutcome {
OutcomeOk(ToolResult)
OutcomeErr(RuntimeError)
}
///|
/// A recorded tool execution: call id, tool name, and outcome category.
pub(all) struct ToolExecRecord {
requested_name : String
call_id : String
tool_name : String
arguments : Json
outcome : ScriptedToolOutcome
}
///|
/// Unified provider operation trace, preserving list/execute interleaving.
pub(all) enum ToolProviderOp {
ListTools
ExecuteTool(requested_name~ : String, call~ : ToolCall)
}
///|
/// ToolProvider fake that declares a fixed tool list and routes execute() by
/// tool name through a configurable outcome map. Unknown tools raise
/// `RuntimeError::UnknownTool`. Every execute is recorded with its call id.
pub(all) struct RecordingToolProvider {
tool_defs : Array[ToolDef]
outcomes : Map[String, ScriptedToolOutcome]
mut list_calls : Int
mut exec_records : Array[ToolExecRecord]
mut ops : Array[ToolProviderOp]
}
///|
pub fn RecordingToolProvider::new(
tool_defs : Array[ToolDef],
outcomes : Map[String, ScriptedToolOutcome],
) -> RecordingToolProvider {
let copied_outcomes : Map[String, ScriptedToolOutcome] = Map::from_array([])
for key in outcomes.keys() {
copied_outcomes[key] = testkit_snapshot_tool_outcome(outcomes[key])
}
{
tool_defs: tool_defs.map(testkit_snapshot_tool_def),
outcomes: copied_outcomes,
list_calls: 0,
exec_records: [],
ops: [],
}
}
///|
/// Number of times list_tools was called.
pub fn RecordingToolProvider::list_call_count(
self : RecordingToolProvider,
) -> Int {
self.list_calls
}
///|
/// Snapshot of recorded executions (call id, tool name, outcome).
pub fn RecordingToolProvider::exec_records(
self : RecordingToolProvider,
) -> Array[ToolExecRecord] {
self.exec_records.map(fn(record) {
{
requested_name: record.requested_name,
call_id: record.call_id,
tool_name: record.tool_name,
arguments: testkit_snapshot_json(record.arguments),
outcome: testkit_snapshot_tool_outcome(record.outcome),
}
})
}
///|
/// Snapshot of list/execute operations in their exact observed order.
pub fn RecordingToolProvider::ops(
self : RecordingToolProvider,
) -> Array[ToolProviderOp] {
self.ops.map(fn(op) {
match op {
ListTools => ListTools
ExecuteTool(requested_name~, call~) =>
ExecuteTool(requested_name~, call=testkit_snapshot_tool_call(call))
}
})
}
///|
/// Direct (non-trait) execute entry point for testkit self-tests.
pub async fn RecordingToolProvider::execute_direct(
self : RecordingToolProvider,
name : String,
call : ToolCall,
) -> ToolResult raise RuntimeError {
(self as &ToolProvider).execute(name, call)
}
///|
pub impl ToolProvider for RecordingToolProvider with fn list_tools(self) {
self.list_calls = self.list_calls + 1
self.ops.push(ListTools)
self.tool_defs.map(testkit_snapshot_tool_def)
}
///|
pub impl ToolProvider for RecordingToolProvider with fn execute(
self,
name : String,
call : ToolCall,
) -> ToolResult raise RuntimeError {
let lookup = self.outcomes.get(call.name)
let outcome = match lookup {
Some(o) => o
None =>
OutcomeErr(
UnknownTool("recording_tool: no outcome configured for '\{call.name}'"),
)
}
self.ops.push(
ExecuteTool(requested_name=name, call=testkit_snapshot_tool_call(call)),
)
self.exec_records.push({
requested_name: name,
call_id: call.id,
tool_name: call.name,
arguments: testkit_snapshot_json(call.arguments),
outcome: testkit_snapshot_tool_outcome(outcome),
})
match outcome {
OutcomeOk(r) => testkit_snapshot_tool_result(r)
OutcomeErr(e) => raise e
}
}
// ---------------------------------------------------------------------------
// RecordingSessionStore — load/save history with metadata snapshots.
// ---------------------------------------------------------------------------
///|
/// A recorded session operation. Constructors are prefixed with `Op` to avoid
/// ambiguity with `SessionError::Load`/`SessionError::Save`.
pub(all) enum SessionOp {
OpLoad(id~ : String, result~ : Result[Session, SessionError])
OpSave(
id~ : String,
metadata~ : Map[String, Json],
result~ : Result[Unit, SessionError]
)
}
///|
/// SessionStore fake that keeps an in-memory map and records every load/save
/// with the metadata snapshot at save time. Load/save failures are
/// configurable per id.
pub(all) struct RecordingSessionStore {
store : Map[String, Session]
mut load_failures : Map[String, SessionError]
mut save_failures : Array[String]
mut ops : Array[SessionOp]
}
///|
pub fn RecordingSessionStore::new() -> RecordingSessionStore {
{
store: Map::from_array([]),
load_failures: Map::from_array([]),
save_failures: [],
ops: [],
}
}
///|
/// Seed a session id with an existing session (including metadata).
pub fn RecordingSessionStore::seed(
self : RecordingSessionStore,
id : String,
session : Session,
) -> Unit {
self.store[id] = testkit_snapshot_session(session)
}
///|
/// Configure load to fail for a given id.
pub fn RecordingSessionStore::fail_load(
self : RecordingSessionStore,
id : String,
err : SessionError,
) -> Unit {
self.load_failures[id] = err
}
///|
/// Configure save to fail for a given id (all subsequent saves to that id).
pub fn RecordingSessionStore::fail_save(
self : RecordingSessionStore,
id : String,
) -> Unit {
self.save_failures.push(id)
}
///|
/// Recorded operations in order.
pub fn RecordingSessionStore::ops(
self : RecordingSessionStore,
) -> Array[SessionOp] {
self.ops.map(fn(op) {
match op {
OpLoad(id~, result~) =>
OpLoad(
id~,
result=match result {
Ok(session) => Ok(testkit_snapshot_session(session))
Err(error) => Err(error)
},
)
OpSave(id~, metadata~, result~) =>
OpSave(id~, metadata=testkit_snapshot_metadata(metadata), result~)
}
})
}
///|
/// All save operations, in order, as (id, metadata_snapshot) pairs.
pub fn RecordingSessionStore::save_snapshots(
self : RecordingSessionStore,
) -> Array[(String, Map[String, Json])] {
let out : Array[(String, Map[String, Json])] = []
for op in self.ops {
match op {
OpSave(id~, metadata~, ..) =>
out.push((id, testkit_snapshot_metadata(metadata)))
_ => ()
}
}
out
}
///|
pub impl SessionStore for RecordingSessionStore with fn load(self, id : String) -> Result[
Session,
SessionError,
] {
let result : Result[Session, SessionError] = match
self.load_failures.get(id) {
Some(e) => Err(e)
None =>
match self.store.get(id) {
Some(s) => Ok(testkit_snapshot_session(s))
None => Ok({ messages: [], metadata: Map::from_array([]) })
}
}
let recorded_result = match result {
Ok(session) => Ok(testkit_snapshot_session(session))
Err(error) => Err(error)
}
self.ops.push(OpLoad(id~, result=recorded_result))
result
}
///|
pub impl SessionStore for RecordingSessionStore with fn save(
self,
id : String,
session : Session,
) -> Result[Unit, SessionError] {
let meta_copy = testkit_snapshot_metadata(session.metadata)
let result : Result[Unit, SessionError] = if self.save_failures.contains(id) {
Err(SessionError::Save("configured save failure for '\{id}'"))
} else {
self.store[id] = testkit_snapshot_session(session)
Ok(())
}
self.ops.push(OpSave(id~, metadata=meta_copy, result~))
result
}
// ---------------------------------------------------------------------------
// RecordingObserver — preserves full event order.
// ---------------------------------------------------------------------------
///|
/// Observer fake that records every TurnEvent in arrival order.
/// Unlike a counter, it keeps the full sequence for trace assertions.
pub(all) struct RecordingObserver {
mut events : Array[TurnEvent]
}
///|
pub fn RecordingObserver::new() -> RecordingObserver {
{ events: [] }
}
///|
/// Snapshot of all recorded events in order.
pub fn RecordingObserver::events(self : RecordingObserver) -> Array[TurnEvent] {
self.events.map(testkit_snapshot_event)
}
///|
pub impl Observer for RecordingObserver with fn on_event(self, event) {
self.events.push(testkit_snapshot_event(event))
}
// ---------------------------------------------------------------------------
// RecordingHook — records stage inputs and outcomes.
// ---------------------------------------------------------------------------
///|
/// The exact decision made by a RecordingHook invocation.
pub(all) enum RecordedHookOutcome {
Returned(Stage)
AbortedWith(String)
DeferredWith(String)
}
///|
fn testkit_snapshot_hook_outcome(
outcome : RecordedHookOutcome,
) -> RecordedHookOutcome {
match outcome {
Returned(stage) => Returned(testkit_snapshot_stage(stage))
AbortedWith(message) => AbortedWith(message)
DeferredWith(reason) => DeferredWith(reason)
}
}
///|
/// A recorded hook invocation: the stage received and its exact decision.
pub(all) struct HookRecord {
stage_in : Stage
outcome : RecordedHookOutcome
}
///|
/// PipelineHook fake with configurable per-stage behavior. By default it is
/// a pass-through (returns the stage unchanged). Records every call.
pub(all) struct RecordingHook {
mut records : Array[HookRecord]
abort_on : Map[String, String]
defer_on : Map[String, String]
}
///|
pub fn RecordingHook::new() -> RecordingHook {
{ records: [], abort_on: Map::from_array([]), defer_on: Map::from_array([]) }
}
///|
/// Configure the hook to abort (HookError::Aborted) when it sees a stage
/// whose debug string contains `trigger`.
pub fn RecordingHook::abort_when(
self : RecordingHook,
trigger : String,
msg : String,
) -> Unit {
self.abort_on[trigger] = msg
}
///|
/// Configure the hook to defer when it sees a stage whose label contains
/// `trigger`.
pub fn RecordingHook::defer_when(
self : RecordingHook,
trigger : String,
reason : String,
) -> Unit {
self.defer_on[trigger] = reason
}
///|
pub fn RecordingHook::records(self : RecordingHook) -> Array[HookRecord] {
self.records.map(fn(record) {
{
stage_in: testkit_snapshot_stage(record.stage_in),
outcome: testkit_snapshot_hook_outcome(record.outcome),
}
})
}
///|
pub impl PipelineHook for RecordingHook with fn on_stage(self, stage) {
let dbg = stage_to_label(stage)
for trigger in self.abort_on.keys() {
if dbg.contains(trigger) {
let message = self.abort_on[trigger]
self.records.push({
stage_in: testkit_snapshot_stage(stage),
outcome: AbortedWith(message),
})
return Err(Aborted(message))
}
}
for trigger in self.defer_on.keys() {
if dbg.contains(trigger) {
let reason = self.defer_on[trigger]
self.records.push({
stage_in: testkit_snapshot_stage(stage),
outcome: DeferredWith(reason),
})
return Err(Deferred(reason~))
}
}
self.records.push({
stage_in: testkit_snapshot_stage(stage),
outcome: Returned(testkit_snapshot_stage(stage)),
})
Ok(stage)
}
///|
/// Minimal label for a Stage variant, used for abort matching.
fn stage_to_label(stage : Stage) -> String {
match stage {
BeforeTurn => "BeforeTurn"
BeforeModel(_) => "BeforeModel"
AfterModel(_) => "AfterModel"
ModelError(_) => "ModelError"
BeforeTool(_) => "BeforeTool"
AfterTool(_, _) => "AfterTool"
ToolError(_, _) => "ToolError"
AfterTurn(_) => "AfterTurn"
TurnError(_) => "TurnError"
}
}
// ---------------------------------------------------------------------------
// Trace assertions — structured helpers that report the first differing index.
// ---------------------------------------------------------------------------
///|
/// Return the first structural trace mismatch. Kept pure so conformance tests
/// can prove missing, duplicate, and out-of-order detection without catching
/// an assertion panic.
pub fn event_trace_mismatch(
expected : Array[TurnEvent],
actual : Array[TurnEvent],
) -> String? {
let n = if expected.length() < actual.length() {
expected.length()
} else {
actual.length()
}
for i = 0; i < n; i = i + 1 {
if expected[i] != actual[i] {
return Some(
"event_trace_mismatch at index \{i}: expected \{expected[i].to_string()}, got \{actual[i].to_string()}",
)
}
}
if expected.length() != actual.length() {
let index = n
if expected.length() > actual.length() {
return Some(
"event_trace_mismatch at index \{index}: expected \{expected[index].to_string()}, got (expected \{expected.length()} events, actual \{actual.length()})",
)
} else {
return Some(
"event_trace_mismatch at index \{index}: expected , got \{actual[index].to_string()} (expected \{expected.length()} events, actual \{actual.length()})",
)
}
}
None
}
///|
/// Assert two event traces are equal, reporting the first differing index,
/// the expected and actual event, when they diverge.
pub fn assert_events_eq(
expected : Array[TurnEvent],
actual : Array[TurnEvent],
) -> Unit {
match event_trace_mismatch(expected, actual) {
Some(message) => abort(message)
None => ()
}
}
///|
/// Assert the recorded events contain a sub-sequence matching `needle`, in
/// order. Reports the first needle event that could not be matched.
pub fn assert_events_contain(
haystack : Array[TurnEvent],
needle : Array[TurnEvent],
) -> Unit {
if needle.is_empty() {
return
}
let mut hi = 0
for ni = 0; ni < needle.length(); ni = ni + 1 {
let mut found = false
while hi < haystack.length() {
if haystack[hi] == needle[ni] {
found = true
hi = hi + 1
break
}
hi = hi + 1
}
if !found {
abort(
"event_not_found_in_order: needle[\{ni}] = \{needle[ni].to_string()} not matched after haystack index \{hi}",
)
}
}
}
// ---------------------------------------------------------------------------
// Testkit constructors — small helpers mirroring the blackbox test helpers.
// ---------------------------------------------------------------------------
///|
/// Build a minimal stop response (no tool calls) for scripted tests.
pub fn tk_stop_response(text : String) -> ModelResponse {
{
message: {
role: Assistant,
content: [Text(text)],
tool_calls: [],
tool_call_id: None,
name: Some("assistant"),
reasoning_content: None,
},
tool_calls: [],
finish_reason: Stop,
usage: None,
reasoning_content: None,
}
}
///|
/// Build a response that requests a single tool call.
pub fn tk_tool_call_response(
tool_name : String,
call_id : String,
args : Json,
) -> ModelResponse {
{
message: {
role: Assistant,
content: [],
tool_calls: [{ id: call_id, name: tool_name, arguments: args }],
tool_call_id: None,
name: None,
reasoning_content: None,
},
tool_calls: [{ id: call_id, name: tool_name, arguments: args }],
finish_reason: ToolCalls,
usage: None,
reasoning_content: None,
}
}
///|
/// Build a user message.
pub fn tk_user_msg(text : String) -> Message {
{
role: User,
content: [Text(text)],
tool_calls: [],
tool_call_id: None,
name: None,
reasoning_content: None,
}
}
///|
/// Build a ToolDef with an empty object schema.
pub fn tk_tool_def(name : String, description : String) -> ToolDef {
{
name,
description,
input_schema: Json::object(Map::from_array([])),
metadata: None,
source: None,
}
}
///|
/// Build a successful ToolResult.
pub fn tk_ok_result(content : String) -> ToolResult {
{ content, structured: None, is_error: false }
}
///|
/// Build a business-error ToolResult (provider returns Ok but is_error=true).
pub fn tk_error_result(content : String) -> ToolResult {
{ content, structured: None, is_error: true }
}
///|
/// Build a default AgentConfig for scripted tests.
pub fn tk_config() -> AgentConfig {
{
max_tool_rounds: 10,
temperature: None,
max_output_tokens: None,
tool_choice: None,
reasoning_effort: None,
model_context_window: None,
}
}