starweaver_model/request/
instructions.rs1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4use crate::message::{Metadata, ModelMessage, ModelRequest, ModelRequestPart};
5
6pub const INSTRUCTION_DYNAMIC_METADATA: &str = "starweaver_instruction_dynamic";
8pub const INSTRUCTION_ORIGIN_METADATA: &str = "starweaver_instruction_origin";
10pub const CONTEXT_ORIGIN_METADATA: &str = "starweaver_context_origin";
12pub const CONTEXT_TYPE_METADATA: &str = "starweaver_context_type";
14
15pub const INSTRUCTION_ORIGIN_AGENT: &str = "agent_instruction";
17pub const INSTRUCTION_ORIGIN_TOOLSET: &str = "toolset";
19pub const INSTRUCTION_ORIGIN_DYNAMIC_INSTRUCTION: &str = "dynamic_instruction";
21
22pub const CONTEXT_ORIGIN_RUNTIME_CONTEXT: &str = "runtime_context";
24pub const CONTEXT_ORIGIN_ENVIRONMENT_CONTEXT: &str = "environment_context";
26pub const CONTEXT_ORIGIN_HANDOFF: &str = "handoff";
28pub const CONTEXT_ORIGIN_TOOL_RETURN_MEDIA: &str = "tool_return_media";
30
31#[allow(clippy::trivially_copy_pass_by_ref)]
32const fn is_false(value: &bool) -> bool {
33 !*value
34}
35
36#[allow(clippy::redundant_pub_crate)]
37pub(crate) fn is_dynamic_instruction_metadata(metadata: &Metadata) -> bool {
38 metadata
39 .get(INSTRUCTION_DYNAMIC_METADATA)
40 .and_then(Value::as_bool)
41 .unwrap_or(false)
42}
43
44#[must_use]
46pub fn context_origin_metadata(metadata: &Metadata) -> Option<&str> {
47 metadata
48 .get(CONTEXT_ORIGIN_METADATA)
49 .and_then(Value::as_str)
50}
51
52#[allow(clippy::redundant_pub_crate)]
60pub(crate) fn current_instruction_request_index(messages: &[ModelMessage]) -> Option<usize> {
61 let latest = messages
62 .iter()
63 .rposition(|message| matches!(message, ModelMessage::Request(_)))?;
64 let ModelMessage::Request(request) = &messages[latest] else {
65 unreachable!("latest request index points at a request")
66 };
67 if request_has_instruction_material(request) {
68 return Some(latest);
69 }
70 if request_is_control_only(request) {
71 return messages[..latest].iter().rposition(|message| {
72 matches!(message, ModelMessage::Request(request) if request_has_instruction_material(request))
73 });
74 }
75 None
76}
77
78#[allow(clippy::redundant_pub_crate)]
79pub(crate) fn request_has_instruction_material(request: &ModelRequest) -> bool {
80 request
81 .instructions
82 .as_deref()
83 .is_some_and(|instructions| !instructions.trim().is_empty())
84 || request
85 .parts
86 .iter()
87 .any(|part| matches!(part, ModelRequestPart::Instruction { .. }))
88}
89
90fn request_is_control_only(request: &ModelRequest) -> bool {
91 !request.parts.is_empty()
92 && request.parts.iter().all(|part| match part {
93 ModelRequestPart::ToolReturn(_) | ModelRequestPart::RetryPrompt { .. } => true,
94 ModelRequestPart::UserPrompt { metadata, .. } => context_origin_metadata(metadata)
95 .is_some_and(|origin| {
96 matches!(
97 origin,
98 CONTEXT_ORIGIN_TOOL_RETURN_MEDIA
99 | CONTEXT_ORIGIN_RUNTIME_CONTEXT
100 | CONTEXT_ORIGIN_ENVIRONMENT_CONTEXT
101 | CONTEXT_ORIGIN_HANDOFF
102 )
103 }),
104 ModelRequestPart::SystemPrompt { .. } | ModelRequestPart::Instruction { .. } => false,
105 })
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
110pub struct PreparedInstruction {
111 pub text: String,
113 #[serde(default, skip_serializing_if = "is_false")]
115 pub dynamic: bool,
116 #[serde(default, skip_serializing_if = "Map::is_empty")]
118 pub metadata: Map<String, Value>,
119}
120
121impl PreparedInstruction {
122 #[must_use]
124 pub fn static_text(text: impl Into<String>) -> Self {
125 Self {
126 text: text.into(),
127 dynamic: false,
128 metadata: Map::new(),
129 }
130 }
131
132 #[must_use]
134 pub fn dynamic_text(text: impl Into<String>) -> Self {
135 Self {
136 text: text.into(),
137 dynamic: true,
138 metadata: Map::new(),
139 }
140 }
141
142 #[must_use]
144 pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
145 self.metadata.insert(
146 INSTRUCTION_ORIGIN_METADATA.to_string(),
147 serde_json::json!(origin.into()),
148 );
149 self
150 }
151
152 #[must_use]
154 pub fn with_dynamic(mut self, dynamic: bool) -> Self {
155 self.dynamic = dynamic;
156 self.metadata.insert(
157 INSTRUCTION_DYNAMIC_METADATA.to_string(),
158 serde_json::json!(dynamic),
159 );
160 self
161 }
162
163 #[must_use]
165 pub fn sorted(instructions: &[Self]) -> Vec<Self> {
166 let mut sorted = instructions.to_vec();
167 sorted.sort_by_key(|instruction| instruction.dynamic);
168 sorted
169 }
170
171 pub(super) fn to_request_part(&self) -> ModelRequestPart {
172 let mut metadata = self.metadata.clone();
173 metadata.insert(
174 INSTRUCTION_DYNAMIC_METADATA.to_string(),
175 serde_json::json!(self.dynamic),
176 );
177 ModelRequestPart::Instruction {
178 text: self.text.clone(),
179 metadata,
180 }
181 }
182}
183
184pub type InstructionPart = PreparedInstruction;