1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
//! Human-readable "thinking" steps (issue #488).
//!
//! This module owns the [`ThinkingStep`] model and the deterministic
//! `(step, detail) -> concrete English sentence` naturalizer that every native
//! surface shares: the core `EventLog` projection, the CLI `--thinking` output,
//! the OpenAI/Anthropic API responses, and the Telegram bot. The browser mirrors
//! the same two stages in JavaScript — the curated projection in
//! `src/web/formal_ai_worker.js` and the naturalizer (`naturalizeThinkingStep`,
//! which additionally localizes into the user's language) in `src/web/app.js`.
//! Keeping it in its own module (rather than inside `engine.rs`) keeps each file
//! focused and within the repository's per-file line budget while making
//! "thinking" a first-class concern of the architecture rather than an engine
//! implementation detail.
use serde::{Deserialize, Serialize};
use crate::engine::stable_id;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ThinkingStep {
pub id: String,
pub order: u32,
pub step: String,
pub detail: String,
/// Concrete, human-readable description of this step (issue #488).
///
/// This is the "meta-language description" layer: a single English sentence
/// that surfaces the actual content of the step (the prompt, the computed
/// result, the looked-up entity, the chosen route, the composed answer)
/// rather than a generic category label. UI surfaces translate it into the
/// target user language; non-UI surfaces (CLI, API, Telegram) show it as-is.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub summary: String,
pub level: String,
pub source_event: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
impl ThinkingStep {
#[must_use]
pub fn new(
order: u32,
step: impl Into<String>,
detail: impl Into<String>,
level: impl Into<String>,
source_event: impl Into<String>,
) -> Self {
let step = step.into();
let detail = detail.into();
let level = level.into();
let source_event = source_event.into();
let summary = naturalize_thinking_step(&step, &detail);
let seed = format!("{order}:{step}:{detail}:{level}:{source_event}");
Self {
id: stable_id("thinking_step", &seed),
order,
step,
detail,
summary,
level,
source_event,
parent_id: None,
}
}
/// Attach a parent step id so callers can express recursively composite
/// (fractal) thinking, where finer-granularity sub-steps roll up into a
/// single high-level step (issue #488).
#[must_use]
pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
self.parent_id = Some(parent_id.into());
self
}
}
/// Map a language slug to its English name for concrete thinking summaries.
#[must_use]
pub fn thinking_language_label(code: &str) -> String {
let normalized = code.trim().to_ascii_lowercase();
let primary = normalized
.split(['-', '_'])
.next()
.unwrap_or(normalized.as_str());
match primary {
"en" => "English".to_owned(),
"ru" => "Russian".to_owned(),
"hi" => "Hindi".to_owned(),
"zh" => "Chinese".to_owned(),
"" | "unknown" => "an unrecognized language".to_owned(),
other => other.to_owned(),
}
}
/// Turn a meta-language identifier (`write_program`, `route:greeting`,
/// `concept_lookup:hit`) into a lowercase human phrase (`write program`,
/// `greeting`, `concept lookup hit`).
#[must_use]
pub fn humanize_meta_identifier(value: &str) -> String {
let mut spaced = String::with_capacity(value.len());
let mut previous_lower = false;
for character in value.chars() {
if character.is_ascii_uppercase() && previous_lower {
spaced.push(' ');
}
if matches!(character, '_' | ':' | '.' | '-' | '/') {
spaced.push(' ');
} else {
spaced.push(character);
}
previous_lower = character.is_ascii_lowercase() || character.is_ascii_digit();
}
let collapsed = spaced.split_whitespace().collect::<Vec<_>>().join(" ");
collapsed.trim().to_ascii_lowercase()
}
/// Pick the English indefinite article (`a`/`an`) for the following phrase based
/// on its first letter, so naturalized steps read grammatically ("an arithmetic
/// task", "a greeting task").
fn indefinite_article(phrase: &str) -> &'static str {
match phrase.trim_start().chars().next() {
Some(first) if matches!(first.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u') => "an",
_ => "a",
}
}
/// Strip an `agent_<n>_` sub-agent prefix from a step kind, mirroring the
/// browser worker's nested-agent naming (`agent_0_impulse` -> `impulse`).
fn strip_agent_substep_prefix(step: &str) -> &str {
if let Some(rest) = step.strip_prefix("agent_") {
if let Some(index) = rest.find('_') {
if index > 0 && rest[..index].bytes().all(|b| b.is_ascii_digit()) {
return &rest[index + 1..];
}
}
}
step
}
fn truncate_thinking_detail(value: &str) -> String {
let trimmed = value.trim();
// Issue #1963 (problem 2, "Thinking steps are not fully written, some parts
// are omitted."): the previous 120-char cap clipped the concrete detail of a
// step mid-sentence (e.g. a pasted prompt or a composed answer), so the
// visible reasoning read as truncated rather than complete. 600 chars keeps
// the detail bounded (the panel still scrolls and the fade still applies)
// while letting realistic single-step content render in full. This mirrors
// the JS `thinkingDetailText` helper; keep both constants in sync.
let limit = 600;
if trimmed.chars().count() <= limit {
return trimmed.to_owned();
}
let truncated: String = trimmed.chars().take(limit - 1).collect();
format!("{}…", truncated.trim_end())
}
/// Translate a single `(step, detail)` pair into one concrete English sentence.
///
/// This is the deterministic "meta-language description" stage from issue #488.
/// It is the single source of truth shared by the core projection, the CLI, the
/// OpenAI/Anthropic API surfaces, and (mirrored) the browser worker, so every
/// surface renders the *same* concrete thinking rather than a generic label.
#[must_use]
pub fn naturalize_thinking_step(step: &str, detail: &str) -> String {
let canonical = strip_agent_substep_prefix(step);
let trimmed = truncate_thinking_detail(detail);
let has_detail = !trimmed.is_empty();
match canonical {
"impulse" => {
if has_detail {
format!("Read the request: \"{trimmed}\".")
} else {
"Read the incoming request.".to_owned()
}
}
"detect_language" => {
format!(
"Detect the request language: {}.",
thinking_language_label(detail)
)
}
"resolve_response_language" => {
format!("Plan to answer in {}.", thinking_language_label(detail))
}
"formalize" => {
if has_detail {
let task = humanize_meta_identifier(&trimmed);
format!(
"Formalize the request as {} {task} task.",
indefinite_article(&task)
)
} else {
"Formalize the request into a symbolic tuple.".to_owned()
}
}
"formalize_resolved" => {
if has_detail {
format!(
"Resolve the request to {}.",
humanize_meta_identifier(&trimmed)
)
} else {
"Resolve the request to a concrete entity.".to_owned()
}
}
"clarify_formalization" => {
if has_detail {
format!("Ask for clarification between {trimmed}.")
} else {
"Ask for clarification because the request was ambiguous.".to_owned()
}
}
"dispatch_handler" => {
if has_detail {
format!(
"Route to the {} handler.",
humanize_meta_identifier(&trimmed)
)
} else {
"Route the request to a handler.".to_owned()
}
}
"route_attempt" => {
if has_detail {
format!("Try the {} approach.", humanize_meta_identifier(&trimmed))
} else {
"Try the next candidate approach.".to_owned()
}
}
"match_rule" => {
if has_detail {
format!("Match the {} rule.", humanize_meta_identifier(&trimmed))
} else {
"Match a known rule.".to_owned()
}
}
"compute" => {
if has_detail {
format!("Compute {trimmed}.")
} else {
"Compute the result.".to_owned()
}
}
"compute_engine" => {
if has_detail {
format!("Evaluate with the {}.", humanize_meta_identifier(&trimmed))
} else {
"Evaluate with the calculator.".to_owned()
}
}
"compute_expression" => format!("Reduce the expression {trimmed}."),
"compute_steps" => format!("Apply {trimmed} reduction step(s)."),
"lookup_fact" => {
if has_detail {
format!("Look up {}.", humanize_meta_identifier(&trimmed))
} else {
"Look up the relevant fact.".to_owned()
}
}
"invoke_tool" => {
if has_detail {
format!("Use the {} capability.", humanize_meta_identifier(&trimmed))
} else {
"Use an available capability.".to_owned()
}
}
"rule_verification" => {
if has_detail {
format!(
"Verify the result against the {} rule.",
humanize_meta_identifier(&trimmed)
)
} else {
"Verify the result against the rules.".to_owned()
}
}
"policy_refusal" => {
if has_detail {
format!(
"Decline the request under the {} policy.",
humanize_meta_identifier(&trimmed)
)
} else {
"Decline the request under the safety policy.".to_owned()
}
}
"rule_construction" => "Build a local behavior rule.".to_owned(),
"coreference_binding" => "Resolve what the follow-up refers to.".to_owned(),
"modifier_detection" => "Detect modifiers in the request.".to_owned(),
"program_plan" => {
if has_detail {
format!("Plan the program: {}.", humanize_meta_identifier(&trimmed))
} else {
"Plan the requested program.".to_owned()
}
}
"scan_memory" => {
if has_detail {
format!("Search memory for {trimmed}.")
} else {
"Search memory for relevant facts.".to_owned()
}
}
"user_context" => {
if has_detail {
format!("Apply available context: {trimmed}.")
} else {
"Apply the available context.".to_owned()
}
}
"deformalize" => {
if has_detail {
format!("Compose the answer: \"{trimmed}\".")
} else {
"Compose the answer in natural language.".to_owned()
}
}
"http_chat" => "Exchange a request with the configured endpoint.".to_owned(),
"agent_plan" => {
if has_detail {
format!("Add an agent task: {}.", humanize_meta_identifier(&trimmed))
} else {
"Extend the agent plan.".to_owned()
}
}
"memory" => "Update the local memory bundle.".to_owned(),
"extract_term" => "Extract the search term.".to_owned(),
"group_by_conversation" => "Group matching memories by conversation.".to_owned(),
"fallback" => "Fall back to the general unknown-request strategy.".to_owned(),
other => {
let readable = humanize_meta_identifier(other);
let label = if readable.is_empty() {
"step".to_owned()
} else {
readable
};
if has_detail {
format!("{label}: {trimmed}.")
} else {
format!("{label}.")
}
}
}
}