Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f98c257
chore(vendor): bump tinyinference submodule
senamakel Oct 7, 2026
84d3458
fix(harness): correct dialect, cache, validation, and prompt handling
senamakel Oct 7, 2026
b2e6bfc
refactor(run-ledger): remove unused row and team operation modules
senamakel Oct 7, 2026
ff9186e
chore: tidy imports and trailing whitespace
senamakel Oct 7, 2026
ceccefa
refactor(session): simplify run ledger operation handling
senamakel Oct 7, 2026
b44db70
refactor(session): simplify run ledger operation handling
senamakel Oct 7, 2026
cb40dd2
refactor(run-ledger): share paged list query across list functions
senamakel Oct 7, 2026
c407162
feat(checkpoint): add file and sqlite checkpoint backends
senamakel Oct 7, 2026
c64f7f8
style: collapse wrapped let bindings onto single lines
senamakel Oct 7, 2026
1099beb
test(harness): add testkit module and agent loop tests
senamakel Oct 7, 2026
9214fd8
test(agent-loop): wrap long tool_call_response line
senamakel Oct 7, 2026
4a3c57c
test(harness): add testkit module tests
senamakel Oct 7, 2026
e7138a0
test: update Usage import path in testkit tests
senamakel Oct 7, 2026
700b8f1
test(steering): cover steering module behaviour
senamakel Oct 7, 2026
6e7ecdd
refactor(media): reuse testkit response builders in media tests
senamakel Oct 7, 2026
6339210
test(media): drop unused message imports
senamakel Oct 7, 2026
3901742
test(integration): update e2e tests for revised agent loop APIs
senamakel Oct 7, 2026
b36da98
style: reformat long lines in integration tests
senamakel Oct 7, 2026
1f56083
feat(session): add paging module for session history
senamakel Oct 7, 2026
76694c8
refactor(session): use shared paging helper for run ledger lists
senamakel Oct 7, 2026
50f0a34
refactor(paging): move limit and offset into PagedQuery state
senamakel Oct 7, 2026
2897faa
refactor(session): use shared paged query builder for session listing
senamakel Oct 7, 2026
b346027
Merge remote-tracking branch 'upstream/main' into dedupe-clones
senamakel Oct 7, 2026
c8d8dc0
chore(vendor): bump tinyinference submodule
senamakel Oct 7, 2026
b317d36
Fix extracted paging test module conventions
senamakel Oct 7, 2026
540ccea
fix(session): keep paging bindings local to fetch
senamakel Oct 7, 2026
498b999
fix(session): reject negative pagination
senamakel Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions crates/tinyagents-graph/src/checkpoint/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,19 +340,8 @@ fn io_err(context: &str, err: impl std::fmt::Display) -> TinyAgentsError {
/// addressing/parent/pending-writes wiring of the default
/// [`Checkpointer::get_tuple`].
fn tuple_from_checkpoint<State>(checkpoint: Checkpoint<State>) -> CheckpointTuple<State> {
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let parent_config = checkpoint
.parent_checkpoint_id
.as_ref()
.map(|parent| CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(parent.clone()),
namespace: checkpoint.namespace.clone(),
});
let config = checkpoint.config();
let parent_config = checkpoint.parent_config();
let pending_writes = checkpoint.pending_writes.clone();
CheckpointTuple {
config,
Expand Down
22 changes: 3 additions & 19 deletions crates/tinyagents-graph/src/checkpoint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,7 @@ where
checkpoint: Checkpoint<State>,
writes: &[PendingWrite],
) -> Result<CheckpointId> {
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let config = checkpoint.config();
let id = self.put(checkpoint).await?;
self.put_writes(&config, writes).await?;
Ok(id)
Expand Down Expand Up @@ -342,20 +338,8 @@ where
else {
return Ok(None);
};
let resolved = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let parent_config =
checkpoint
.parent_checkpoint_id
.as_ref()
.map(|parent| CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(parent.clone()),
namespace: checkpoint.namespace.clone(),
});
let resolved = checkpoint.config();
let parent_config = checkpoint.parent_config();
let pending_writes = self.resolved_writes(&resolved, &checkpoint).await?;
Ok(Some(CheckpointTuple {
config: resolved,
Expand Down
22 changes: 3 additions & 19 deletions crates/tinyagents-graph/src/checkpoint/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,11 +524,7 @@ where
self.put(checkpoint).await?;
return Ok(id);
}
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let config = checkpoint.config();
let checkpoint_id = checkpoint.checkpoint_id.clone();
let writes = writes.to_vec();
let conn = self.conn.clone();
Expand Down Expand Up @@ -718,20 +714,8 @@ where
// needed here.
let mut out = Vec::with_capacity(records.len());
for checkpoint in records {
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let parent_config =
checkpoint
.parent_checkpoint_id
.as_ref()
.map(|parent| CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(parent.clone()),
namespace: checkpoint.namespace.clone(),
});
let config = checkpoint.config();
let parent_config = checkpoint.parent_config();
let pending_writes = writes
.get(&checkpoint.checkpoint_id)
.cloned()
Expand Down
20 changes: 20 additions & 0 deletions crates/tinyagents-graph/src/checkpoint/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,26 @@ pub struct BarrierArrivals {
}

impl<State> Checkpoint<State> {
/// The address of this checkpoint: its thread, its id, and its namespace.
pub fn config(&self) -> CheckpointConfig {
CheckpointConfig {
thread_id: self.thread_id.clone(),
checkpoint_id: Some(self.checkpoint_id.clone()),
namespace: self.namespace.clone(),
}
}

/// The address of the checkpoint this one was written after, if any.
pub fn parent_config(&self) -> Option<CheckpointConfig> {
self.parent_checkpoint_id
.as_ref()
.map(|parent| CheckpointConfig {
thread_id: self.thread_id.clone(),
checkpoint_id: Some(parent.clone()),
namespace: self.namespace.clone(),
})
}

/// Builds a fresh checkpoint format v2 record.
///
/// Sensible defaults for everything except `state` and `tasks`: a
Expand Down
24 changes: 4 additions & 20 deletions crates/tinyagents-graph/src/compiled/boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,11 +604,7 @@ where
.with_versions_seen(versions_seen)
.with_metadata(metadata);
let writes = checkpoint.pending_writes.clone();
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let config = checkpoint.config();
let id = checkpointer.put(checkpoint).await?;
if !writes.is_empty() {
checkpointer.put_writes(&config, &writes).await?;
Expand Down Expand Up @@ -748,11 +744,7 @@ where
boundary.pending,
));
let writes = checkpoint.pending_writes.clone();
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let config = checkpoint.config();
let id = checkpointer.put(checkpoint).await?;
// Also record the ledger through the write protocol, so backends
// that implement it can answer "did this task run?" without loading
Expand Down Expand Up @@ -784,11 +776,7 @@ where
let checkpoint =
self.build_loop_checkpoint(ctx, thread, boundary, step, interrupts, interrupted);
let writes = checkpoint.pending_writes.clone();
let config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let config = checkpoint.config();
let id = checkpointer.put(checkpoint).await?;
checkpointer.put_writes(&config, &writes).await?;
self.emit(
Expand Down Expand Up @@ -838,11 +826,7 @@ where
// the ledger tooling sees no completion markers for any checkpoint
// written under `DurabilityMode::Async`.
let writes = checkpoint.pending_writes.clone();
let write_config = CheckpointConfig {
thread_id: checkpoint.thread_id.clone(),
checkpoint_id: Some(checkpoint.checkpoint_id.clone()),
namespace: checkpoint.namespace.clone(),
};
let write_config = checkpoint.config();

match tokio::runtime::Handle::try_current() {
Ok(handle) => {
Expand Down
27 changes: 2 additions & 25 deletions crates/tinyagents-harness/src/agent_loop/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use sha2::{Digest, Sha256};
use tinyinference_llm::message::{ContentBlock, Message};
use tinyinference_llm::message::Message;
use tinyinference_llm::model::{
ModelRequest, ModelResponse, PromptSegment, SegmentRole, ToolChoice,
};
use tinyinference_llm::prompt_tools::replace_text_blocks;
use tinyinference_llm::tool::{ToolCall, ToolSchema};
use tinytools_agent::dialect::{CodeDialect, CodeStyle, PFormatDialect};
use tinytools_agent::types::{ParseDiagnostic, ParseOptions, ParsedToolCall};
Expand Down Expand Up @@ -617,30 +618,6 @@ pub(super) fn recover_text_calls(
replace_text_blocks(std::mem::take(&mut response.message.content), outcome.text);
}

/// Keeps every non-text block in place and substitutes one cleaned text at
/// the first text block's position; an empty `cleaned` emits no text block.
fn replace_text_blocks(content: Vec<ContentBlock>, cleaned: String) -> Vec<ContentBlock> {
let mut out = Vec::with_capacity(content.len());
let mut inserted = false;
for block in content {
match block {
ContentBlock::Text(_) => {
if !inserted {
if !cleaned.is_empty() {
out.push(ContentBlock::Text(cleaned.clone()));
}
inserted = true;
}
}
other => out.push(other),
}
}
if !inserted && !cleaned.is_empty() {
out.push(ContentBlock::Text(cleaned));
}
out
}

/// Scrubs tool-call markup from streamed visible text and collects the
/// calls it completes, minting harness ids for them.
///
Expand Down
104 changes: 16 additions & 88 deletions crates/tinyagents-harness/src/agent_loop/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,23 +363,8 @@ impl ChatModel<()> for ProfiledModel {

/// Builds a tool-call assistant response (no text, one tool call).
fn tool_call_response(id: &str, name: &str, arguments: serde_json::Value) -> ModelResponse {
ModelResponse {
message: AssistantMessage {
id: Some(format!("msg-{id}")),
content: Vec::new(),
tool_calls: vec![ToolCall::new(id, name, arguments)],
usage: Some(Usage::new(7, 3)),
origin: None,
},
usage: Some(Usage::new(7, 3)),
finish_reason: Some("tool_calls".to_string()),
raw: None,
resolved_model: None,
continue_turn: None,
served_from_cache: false,
correlation: None,
resolved_route: None,
}
crate::testkit::tool_call_response(ToolCall::new(id, name, arguments))
.with_usage(Usage::new(7, 3))
}

/// Builds a tool-call response whose arguments the provider could not parse,
Expand All @@ -391,44 +376,13 @@ fn invalid_tool_call_response(id: &str, name: &str, raw: &str) -> ModelResponse
"openai response contained invalid JSON arguments for tool call `{id}` (`{name}`): \
EOF while parsing a value; raw arguments: {raw:?}"
);
ModelResponse {
message: AssistantMessage {
id: Some(format!("msg-{id}")),
content: Vec::new(),
tool_calls: vec![ToolCall::invalid(id, name, raw, reason)],
usage: Some(Usage::new(7, 3)),
origin: None,
},
usage: Some(Usage::new(7, 3)),
finish_reason: Some("tool_calls".to_string()),
raw: None,
resolved_model: None,
continue_turn: None,
served_from_cache: false,
correlation: None,
resolved_route: None,
}
crate::testkit::tool_call_response(ToolCall::invalid(id, name, raw, reason))
.with_usage(Usage::new(7, 3))
}

/// Builds a plain-text assistant response with explicit usage.
fn text_response(text: &str, input: u64, output: u64) -> ModelResponse {
ModelResponse {
message: AssistantMessage {
id: None,
content: vec![ContentBlock::Text(text.to_string())],
tool_calls: Vec::new(),
usage: Some(Usage::new(input, output)),
origin: None,
},
usage: Some(Usage::new(input, output)),
finish_reason: Some("stop".to_string()),
raw: None,
resolved_model: None,
continue_turn: None,
served_from_cache: false,
correlation: None,
resolved_route: None,
}
crate::testkit::text_response(text).with_usage(Usage::new(input, output))
}

/// Builds a *truncated empty* completion: `finish_reason == "length"` with no
Expand All @@ -437,49 +391,23 @@ fn text_response(text: &str, input: u64, output: u64) -> ModelResponse {
/// channel. `reasoning_tokens` is folded into `output_tokens` so the usage
/// mirrors a real length-truncated response.
fn truncated_empty_response(reasoning_tokens: u64) -> ModelResponse {
ModelResponse {
message: AssistantMessage {
id: None,
content: Vec::new(),
tool_calls: Vec::new(),
usage: Some(Usage::new(4, reasoning_tokens)),
origin: None,
},
usage: Some(Usage::new(4, reasoning_tokens)),
finish_reason: Some("length".to_string()),
raw: None,
resolved_model: None,
continue_turn: None,
served_from_cache: false,
correlation: None,
resolved_route: None,
}
let mut response = crate::testkit::text_response(String::new())
.with_usage(Usage::new(4, reasoning_tokens))
.with_finish_reason("length");
response.message.content = Vec::new();
response
}

/// A provider may finish normally after emitting only the hidden reasoning
/// channel. Unlike a length-truncated response, another call should keep the
/// same output-token limit rather than doubling it.
fn reasoning_only_stop_response() -> ModelResponse {
ModelResponse {
message: AssistantMessage {
id: None,
content: vec![ContentBlock::Thinking {
text: "A greeting that never reached visible content".into(),
signature: None,
}],
tool_calls: Vec::new(),
usage: Some(Usage::new(4, 20)),
origin: None,
},
usage: Some(Usage::new(4, 20)),
finish_reason: Some("stop".to_string()),
raw: None,
resolved_model: None,
continue_turn: None,
served_from_cache: false,
correlation: None,
resolved_route: None,
}
let mut response = crate::testkit::text_response(String::new()).with_usage(Usage::new(4, 20));
response.message.content = vec![ContentBlock::Thinking {
text: "A greeting that never reached visible content".into(),
signature: None,
}];
response
}

/// Middleware that appends a user message to every model request.
Expand Down
20 changes: 1 addition & 19 deletions crates/tinyagents-harness/src/cache/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use serde_json::Value;
use sha2::{Digest, Sha256};
use tinyinference_llm::cache::canonical_value;

/// Renders a finalized SHA-256 digest as a 64-character lowercase hex string.
pub(super) fn hex_digest(digest: impl AsRef<[u8]>) -> String {
Expand Down Expand Up @@ -57,22 +58,3 @@ pub(super) fn fnv1a_hex(data: &[u8]) -> String {
}
format!("{hash:016x}")
}

/// Recursively sorts the keys of every JSON object so that the serialized form
/// is canonical regardless of insertion order.
pub(super) fn canonical_value(v: Value) -> Value {
match v {
Value::Object(map) => {
let mut pairs: Vec<(String, Value)> = map.into_iter().collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
Value::Object(
pairs
.into_iter()
.map(|(k, val)| (k, canonical_value(val)))
.collect(),
)
}
Value::Array(arr) => Value::Array(arr.into_iter().map(canonical_value).collect()),
other => other,
}
}
Loading
Loading