pub struct TaskContract {Show 18 fields
pub goal: String,
pub file: PathBuf,
pub root: Option<PathBuf>,
pub constraints: Vec<String>,
pub verify: Verification,
pub max_steps: u32,
pub max_duration: Option<Duration>,
pub max_tokens: Option<u64>,
pub max_retries: u32,
pub mcp: Vec<McpServer>,
pub images: Vec<Media>,
pub commit_identity: Identity,
pub tools: Toolbox,
pub retry: RetryPolicy,
pub stall: StallPolicy,
pub context: ContextBudget,
pub exec_timeout: Duration,
pub skills: Option<PathBuf>,
}Expand description
A single unit of work handed to the harness.
The agent edits one file to meet Verification, bounded by budgets. v0.2
adds the time and cost (token) budgets and the retry limit; a 0.1.0 caller
that only set goal, file, verify, and max_steps still compiles —
the new bounds default to unbounded / two retries.
Every entry point in the crate takes one of these, so it is where a run’s definition of done and its ceilings are decided — before the model is asked anything, and independently of which provider will serve it:
use io_harness::{TaskContract, Verification};
use std::time::Duration;
let contract = TaskContract::workspace(
"make `parse` return an error on empty input instead of panicking",
"/path/to/repo",
// The criterion is checked by compiling and running, so a plausible-looking
// stub cannot satisfy it. This is the half of the contract that decides
// whether the run *succeeded*, as opposed to merely stopping.
Verification::WorkspaceTestPasses {
files: vec!["src/parse.rs".into()],
test_src: "#[test] fn empty_is_err() { assert!(parse(\"\").is_err()); }".into(),
},
)
// And this is the half that decides when it stops regardless. All three are
// independent stops with their own `RunOutcome`, so a run that ran out of
// money is distinguishable afterwards from one that ran out of ideas.
.with_max_steps(20)
.with_time_budget(Duration::from_secs(900))
.with_token_budget(200_000)
// Surfaced to the model verbatim. A constraint is guidance, not a boundary —
// what the agent may actually touch is the `Policy`'s job, because the model
// can ignore a sentence and cannot ignore a refusal.
.with_constraint("do not change the public signature of `parse`");
assert_eq!(contract.max_steps, 20);
assert!(contract.root.is_some()); // workspace mode: grep, find, read, writeTaskContract::new is the other constructor: one file, one tool, and no
policy enforcement — a policy passed to a single-file run is refused with
Error::Config rather than silently ignored. Reach
for TaskContract::workspace for anything with a boundary.
The with_* builders that add capability rather than bounds —
with_mcp, with_tools,
with_skills — are workspace-mode only and are
validated at run start, so a duplicate tool name or an unreadable skills
directory fails the run before the first completion is billed.
Fields§
§goal: StringPlain-language goal, e.g. “add a hello function that returns 42”.
file: PathBufThe one file the agent may read and write in single-file mode. In
workspace mode (root is Some) it is unused.
root: Option<PathBuf>Workspace root for multi-file mode. None (the 0.1/0.2 default) runs the
single-file loop over file. Some(dir) runs the workspace loop, where
the agent greps/finds/reads/writes several files under dir.
constraints: Vec<String>Extra rules the agent must respect, surfaced to the model verbatim.
verify: VerificationThe checkable success criterion. The run succeeds when this passes.
max_steps: u32Step budget: hard cap on loop iterations. The run stops when reached.
max_duration: Option<Duration>Time budget: the run stops if it runs longer than this. None = unbounded.
max_tokens: Option<u64>Cost budget, measured in total tokens summed across completions (no
price telemetry exists, so cost is counted in tokens). None = unbounded.
max_retries: u32How many times a failing provider/tool step is retried before the run escalates the error. Defaults to 2.
mcp: Vec<McpServer>MCP servers to connect for this run. Their tools are offered to the model
beside the built-ins, namespaced mcp__<server>__<tool>.
Empty by default, so a 0.7.0-era contract behaves exactly as before. Workspace mode only: single-file mode has one tool and no tool layer to extend.
images: Vec<Media>media only.Images handed to the agent alongside the goal, shown to the model on every step of the run.
This is the caller’s half of the image capability: the task is about
these, so they persist for the whole run rather than being attached once.
The agent’s own half — looking at an image already in the workspace — is
the view_image built-in, which is gated on the path the model names.
Empty by default, so a 0.14.0-era contract behaves exactly as before. A
provider that does not accept images refuses a run carrying any, before
anything is sent; see crate::Provider::accepts_images.
commit_identity: IdentityWho a commit the agent makes is attributed to.
Defaults to an agent identity at a domain reserved so it can never exist.
git commit fails outright with no user.email configured, so this
cannot be left to the machine, and inheriting the repository’s identity
would attribute the agent’s commit to whichever human configured that
checkout.
tools: ToolboxTools the embedding program supplies itself, offered to the model beside the built-ins and governed by the same policy and trace.
Empty by default, so a 0.8.1-era contract behaves exactly as before. In
process, unlike TaskContract::mcp: see crate::tools::Tool for what
registration does and does not authorize.
retry: RetryPolicyHow long to wait between provider attempts, and how long a wait may grow.
Applies only to a failure that
is_retryable; an
authentication failure escalates on its first occurrence however patient
this is.
stall: StallPolicyWhen to decide the agent has stopped making progress, and how many times to
tell it. StallPolicy { window: 0, .. } switches detection off.
context: ContextBudgetHow much of each request the observation log may occupy.
Defaults to ContextBudget::default. Separate from
TaskContract::max_tokens because they bound different things: that is
what the whole run may spend, this is what one request may carry —
though the two are related, since the share is taken of what the spend
budget has left.
exec_timeout: DurationHow long a command the agent runs with the exec tool may take before it
is killed and reported as a timeout.
Defaults to DEFAULT_EXEC_TIMEOUT. Set it
with TaskContract::with_exec_timeout. Separate from
TaskContract::max_duration because they bound different things: that is
how long the whole run may take, this is how long any one command may hang
before the run gets its turn back — without it, a wedged command consumes
the run’s whole time budget and the run reports a budget stop, which is
the wrong diagnosis for what happened.
skills: Option<PathBuf>Directory of skill files to offer the agent, or None (the default) for
no skills.
The path is held rather than the discovered set, because reading a
directory is fallible and a builder method is not: discovery happens at
run start, so a directory that does not exist fails the run with
Error::Config naming the path — the same point
and the same way TaskContract::tools is arbitrated.
Implementations§
Source§impl TaskContract
impl TaskContract
Sourcepub fn new(
goal: impl Into<String>,
file: impl Into<PathBuf>,
verify: Verification,
) -> Self
pub fn new( goal: impl Into<String>, file: impl Into<PathBuf>, verify: Verification, ) -> Self
Minimal contract: goal, target file, and a success criterion. Defaults to 8 steps, no time/token budget, 2 retries, no constraints.
Sourcepub fn workspace(
goal: impl Into<String>,
root: impl Into<PathBuf>,
verify: Verification,
) -> Self
pub fn workspace( goal: impl Into<String>, root: impl Into<PathBuf>, verify: Verification, ) -> Self
A workspace task: the agent may grep, find, read, and write several files
under root. verify should be a multi-file variant
(Verification::EachCompilesRust / Verification::WorkspaceTestPasses).
Defaults match TaskContract::new (12 steps here, since repo tasks take
more turns), no time/token budget, 2 retries.
Sourcepub fn with_mcp<I>(self, servers: I) -> Selfwhere
I: IntoIterator<Item = McpServer>,
pub fn with_mcp<I>(self, servers: I) -> Selfwhere
I: IntoIterator<Item = McpServer>,
Connect these MCP servers for the run and offer their tools to the model.
Each server is authorized before it is reached — spawning a stdio server is an exec check on its binary, dialling an HTTP one is a network check on its host — so configuring a server here does not grant access to it.
Sourcepub fn with_images<I>(self, images: I) -> Selfwhere
I: IntoIterator<Item = Media>,
Available on crate feature media only.
pub fn with_images<I>(self, images: I) -> Selfwhere
I: IntoIterator<Item = Media>,
media only.Hand the agent images to look at, alongside the goal.
A new named method rather than a parameter on any of the sixteen entry
points: every one of them takes a TaskContract, so attaching here works
with all of them and changes no existing signature.
Construct each crate::Media with crate::Media::image, which
refuses a media type no provider documents and refuses an image over the
per-image size bound. The total carried by one request is bounded too —
see crate::provider::MAX_REQUEST_IMAGE_BYTES.
Sourcepub fn with_commit_identity(
self,
name: impl Into<String>,
email: impl Into<String>,
) -> Self
pub fn with_commit_identity( self, name: impl Into<String>, email: impl Into<String>, ) -> Self
Attribute the agent’s commits to this name and address.
Replaces the default agent identity. Neither may be empty or contain a control character — both reach the commit object and the reflog.
Sourcepub fn with_tools(self, tools: Toolbox) -> Self
pub fn with_tools(self, tools: Toolbox) -> Self
Register in-process tools for the run and offer them to the model.
Registration makes a tool available; it does not authorize it. Each call
is an Act::Exec check on the tool’s name, and a
registered tool runs with the embedding program’s own privileges — see
crate::tools::Tool for the full bound.
A name that shadows a built-in, uses the mcp__ prefix, or duplicates
another registered tool fails the run with Error::Config
before the first completion.
Workspace mode only, like TaskContract::with_mcp: single-file mode has
one tool and no tool layer to extend.
Sourcepub fn with_skills(self, dir: impl Into<PathBuf>) -> Self
pub fn with_skills(self, dir: impl Into<PathBuf>) -> Self
Offer the agent the skills in dir — see crate::skills for the
layout.
The directory is read at run start, not here, so a path that does not
exist, is not a directory, or holds more than
MAX_SKILLS skills fails the run with
Error::Config naming it, before the first
completion.
A skill is instructions the model may choose to read. Offering one grants nothing: the read goes through the policy when it happens, and anything the model then does is checked as it always is.
Sourcepub fn with_max_steps(self, max_steps: u32) -> Self
pub fn with_max_steps(self, max_steps: u32) -> Self
Override the step budget.
Sourcepub fn with_time_budget(self, max_duration: Duration) -> Self
pub fn with_time_budget(self, max_duration: Duration) -> Self
Set the time budget.
Sourcepub fn with_token_budget(self, max_tokens: u64) -> Self
pub fn with_token_budget(self, max_tokens: u64) -> Self
Set the cost budget, in total tokens across all completions.
Sourcepub fn with_exec_timeout(self, exec_timeout: Duration) -> Self
pub fn with_exec_timeout(self, exec_timeout: Duration) -> Self
Set how long a command the agent runs with exec may take.
A new named builder rather than a parameter, like every other capability
this crate has added: every entry point takes a TaskContract, so setting
it here works with all of them and changes no existing signature.
use io_harness::{TaskContract, Verification, DEFAULT_EXEC_TIMEOUT};
use std::time::Duration;
// A repository whose cold build is slower than the default ceiling. Raise
// it rather than watching honest work be killed as if it had hung.
let patient = TaskContract::workspace("build and test it", "/monorepo", Verification::None)
.with_exec_timeout(Duration::from_secs(2400));
// And the other direction: an unattended fleet job that would rather give
// up on a command than sit behind it.
let impatient = TaskContract::workspace("lint it", "/repo", Verification::None)
.with_exec_timeout(Duration::from_secs(60));
assert!(patient.exec_timeout > DEFAULT_EXEC_TIMEOUT);
assert!(impatient.exec_timeout < DEFAULT_EXEC_TIMEOUT);Sourcepub fn with_context_budget(self, context: ContextBudget) -> Self
pub fn with_context_budget(self, context: ContextBudget) -> Self
Set how much of each request the observation log may occupy.
Sits beside TaskContract::with_token_budget because they are the two
halves of one thing: that bounds what the run may spend, this bounds what
any one request carries of what it has already observed.
Sourcepub fn with_retry_policy(self, retry: RetryPolicy) -> Self
pub fn with_retry_policy(self, retry: RetryPolicy) -> Self
Set how long to wait between provider attempts.
Sourcepub fn with_stall_policy(self, stall: StallPolicy) -> Self
pub fn with_stall_policy(self, stall: StallPolicy) -> Self
Set when a run decides its agent has stalled, and how often to tell it.
StallPolicy { window: 0, .. } disables detection, restoring 0.10.0
behaviour exactly.
Applies to workspace and sub-agent runs. A single-file run
(TaskContract::new) ignores it: it has one tool and one file, so
“repeated a call without changing anything” describes its only move.
Sourcepub fn with_max_retries(self, max_retries: u32) -> Self
pub fn with_max_retries(self, max_retries: u32) -> Self
Override the retry limit for failing provider/tool steps.
Sourcepub fn with_constraint(self, constraint: impl Into<String>) -> Self
pub fn with_constraint(self, constraint: impl Into<String>) -> Self
Add a constraint the agent must respect.
Trait Implementations§
Source§impl Clone for TaskContract
impl Clone for TaskContract
Source§fn clone(&self) -> TaskContract
fn clone(&self) -> TaskContract
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreAuto Trait Implementations§
impl !RefUnwindSafe for TaskContract
impl !UnwindSafe for TaskContract
impl Freeze for TaskContract
impl Send for TaskContract
impl Sync for TaskContract
impl Unpin for TaskContract
impl UnsafeUnpin for TaskContract
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
fn is_in_subset(&self) -> bool
self is actually part of its subset T (and can be converted to it).Source§fn to_subset_unchecked(&self) -> SS
fn to_subset_unchecked(&self) -> SS
self.to_subset but without any property checks. Always succeeds.Source§fn from_subset(element: &SS) -> SP
fn from_subset(element: &SS) -> SP
self to the equivalent element of its superset.