Skip to main content

harness_core/
context.rs

1use crate::{ModelOutput, Signal};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5/// A single block of content within the assembled prompt.
6///
7/// Blocks are grouped so that long-stable prefixes (system + guides) stay
8/// cacheable across turns ("prompt caching" pattern).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum Block {
12    /// Plain prompt text.
13    Text(String),
14    /// Reference to a file in the world. The runtime decides whether to
15    /// inline contents or hand the agent a tool call to read it.
16    FileRef {
17        path: String,
18        hash: Option<String>,
19        excerpt: Option<String>,
20    },
21    /// Reference to an activated SKILL.md body.
22    Skill { name: String, body: String },
23    /// A tool call the assistant requested.
24    ToolCall {
25        call_id: String,
26        name: String,
27        args: serde_json::Value,
28    },
29    /// The result of a previous tool call.
30    ToolResult {
31        call_id: String,
32        content: serde_json::Value,
33    },
34    /// Feedback signals from sensors, rendered for the model.
35    Feedback(Vec<Signal>),
36    /// Provider-specific reasoning trace (DeepSeek `reasoning_content`,
37    /// Anthropic `thinking` blocks). Must be echoed back to the provider on
38    /// subsequent calls or the API rejects the request.
39    Reasoning(String),
40}
41
42/// A single conversation turn (assistant or user).
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Turn {
45    pub role: TurnRole,
46    pub blocks: Vec<Block>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51#[non_exhaustive]
52pub enum TurnRole {
53    User,
54    Assistant,
55    System,
56    Tool,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct Task {
61    pub description: String,
62    pub source: Option<String>, // slack url, github issue, etc.
63    pub deadline: Option<i64>,
64}
65
66#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
67pub struct Policy {
68    pub max_iters: u32,
69    pub max_input_tokens: u32,
70    pub max_output_tokens: u32,
71    pub self_correct_rounds: u32,
72}
73
74impl Default for Policy {
75    fn default() -> Self {
76        Self {
77            max_iters: 50,
78            max_input_tokens: 150_000,
79            max_output_tokens: 8_000,
80            self_correct_rounds: 3,
81        }
82    }
83}
84
85/// The model-visible state of an in-progress agent run.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct Context {
88    pub system: Vec<Block>,
89    pub guides: Vec<Block>,
90    pub history: Vec<Turn>,
91    pub task: Task,
92    pub policy: Policy,
93    pub metadata: BTreeMap<String, serde_json::Value>,
94    /// Tools the agent may call this turn. Model adapters translate these to
95    /// the provider's tool-calling format (OpenAI `tools`, Anthropic `tools`, …).
96    pub tools: Vec<crate::ToolSchema>,
97}
98
99impl Context {
100    pub fn new(task: Task) -> Self {
101        Self {
102            system: Vec::new(),
103            guides: Vec::new(),
104            history: Vec::new(),
105            task,
106            policy: Policy::default(),
107            metadata: BTreeMap::new(),
108            tools: Vec::new(),
109        }
110    }
111
112    /// Append a model turn to the history. Captures reasoning content so it
113    /// can be echoed back on subsequent calls (required by DeepSeek thinking
114    /// mode and Anthropic thinking blocks).
115    pub fn push_model_output(&mut self, out: &ModelOutput) {
116        let mut blocks = Vec::new();
117        if let Some(r) = &out.reasoning
118            && !r.is_empty()
119        {
120            blocks.push(Block::Reasoning(r.clone()));
121        }
122        if let Some(t) = &out.text
123            && !t.is_empty()
124        {
125            blocks.push(Block::Text(t.clone()));
126        }
127        for c in &out.tool_calls {
128            blocks.push(Block::ToolCall {
129                call_id: c.id.clone(),
130                name: c.name.clone(),
131                args: c.args.clone(),
132            });
133        }
134        self.history.push(Turn {
135            role: TurnRole::Assistant,
136            blocks,
137        });
138    }
139
140    /// Append feedback signals as a tool-role turn.
141    pub fn push_feedback(&mut self, signals: Vec<Signal>) {
142        self.history.push(Turn {
143            role: TurnRole::Tool,
144            blocks: vec![Block::Feedback(signals)],
145        });
146    }
147}
148
149/// One action the agent has asked to take, paired with the originating tool call.
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct Action {
152    pub tool: String,
153    pub call_id: String,
154    pub args: serde_json::Value,
155}