fix: preserve code queue prompt observation fields
This commit is contained in:
@@ -497,14 +497,15 @@ fn preview(text: &str, max_chars: usize) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
fn prefix_preview(text: &str, max_chars: usize) -> String {
|
||||
if text.chars().count() <= max_chars {
|
||||
return text.to_string();
|
||||
}
|
||||
let take = max_chars.saturating_sub(1);
|
||||
let mut result = text.chars().take(take).collect::<String>();
|
||||
result.push('…');
|
||||
result
|
||||
fn text_preview(value: &str, max_chars: usize) -> Value {
|
||||
let chars = value.chars().count();
|
||||
let truncated = chars > max_chars;
|
||||
json!({
|
||||
"text": if truncated { preview(value, max_chars) } else { value.to_string() },
|
||||
"chars": chars,
|
||||
"truncated": truncated,
|
||||
"omittedChars": if truncated { chars.saturating_sub(max_chars.saturating_sub(20)) } else { 0 }
|
||||
})
|
||||
}
|
||||
|
||||
fn strip_after_marker(text: &str, marker: &str) -> Option<String> {
|
||||
@@ -664,13 +665,17 @@ fn task_list_response(_state: &AppState, task: &TaskMeta, lite: bool) -> Value {
|
||||
};
|
||||
let final_text = final_response(task);
|
||||
let agent_port = code_agent_port(&task.model);
|
||||
let preview_limit = if lite { 360 } else { 2000 };
|
||||
json!({
|
||||
"id": task.id,
|
||||
"queueId": task.queue_id,
|
||||
"queueEnteredAt": task.task_json.as_ref().and_then(|value| value.get("queueEnteredAt")).and_then(Value::as_str).unwrap_or(&task.created_at),
|
||||
"prompt": if lite { prefix_preview(&display_prompt, 360) } else { preview(&task.prompt, 2000) },
|
||||
"basePrompt": if lite { prefix_preview(&task.base_prompt, 360) } else { preview(&task.base_prompt, 2000) },
|
||||
"displayPrompt": if lite { prefix_preview(&display_prompt, 360) } else { preview(&display_prompt, 2000) },
|
||||
"prompt": task.prompt,
|
||||
"basePrompt": task.base_prompt,
|
||||
"displayPrompt": display_prompt,
|
||||
"promptPreview": text_preview(&task.prompt, preview_limit),
|
||||
"basePromptPreview": text_preview(&task.base_prompt, preview_limit),
|
||||
"displayPromptPreview": text_preview(&display_prompt, preview_limit),
|
||||
"promptChars": task.prompt.chars().count(),
|
||||
"basePromptChars": task.base_prompt.chars().count(),
|
||||
"displayPromptChars": display_prompt.chars().count(),
|
||||
@@ -729,8 +734,15 @@ fn task_meta_response(state: &AppState, task: &TaskMeta) -> Value {
|
||||
let map = base.as_object_mut().expect("task response object");
|
||||
let output_count = json_array_len(task.task_json.as_ref(), "output", task.output_count);
|
||||
let prompt_history_count = json_array_len(task.task_json.as_ref(), "promptHistory", 0);
|
||||
let display_prompt = if task.base_prompt.is_empty() {
|
||||
user_prompt_for_display(&task.prompt)
|
||||
} else {
|
||||
task.base_prompt.clone()
|
||||
};
|
||||
map.insert("prompt".to_string(), json!(task.prompt));
|
||||
map.insert("basePrompt".to_string(), json!(task.base_prompt));
|
||||
map.insert("displayPrompt".to_string(), json!(display_prompt));
|
||||
map.insert("initialPrompt".to_string(), json!(task.prompt));
|
||||
map.insert("finalResponse".to_string(), json!(final_response(task)));
|
||||
map.insert(
|
||||
"promptHistory".to_string(),
|
||||
@@ -2200,3 +2212,136 @@ fn run_healthcheck(port: u16) -> bool {
|
||||
let mut body = String::new();
|
||||
stream.read_to_string(&mut body).is_ok() && body.contains("\"ok\": true")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_state() -> AppState {
|
||||
AppState {
|
||||
config: Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 4278,
|
||||
database_url: "postgres://unused".to_string(),
|
||||
log_file: "/tmp/unidesk-code-queue-mgr-test.jsonl".to_string(),
|
||||
deploy_service_id: "code-queue-mgr".to_string(),
|
||||
deploy_repo: "".to_string(),
|
||||
deploy_commit: "".to_string(),
|
||||
deploy_requested_commit: "".to_string(),
|
||||
mgr_pool_max: 1,
|
||||
trace_pool_max: 1,
|
||||
default_provider_id: "D601".to_string(),
|
||||
default_workdir: "/workspace".to_string(),
|
||||
remote_default_workdir: "/home/ubuntu".to_string(),
|
||||
default_model: "gpt-5.5".to_string(),
|
||||
code_models: vec!["gpt-5.5".to_string()],
|
||||
default_max_attempts: 1,
|
||||
default_reasoning_effort: None,
|
||||
model_reasoning_efforts: json!({}),
|
||||
},
|
||||
started_at: now_iso(),
|
||||
logs: Arc::new(Mutex::new(VecDeque::new())),
|
||||
schema_ready: Arc::new(Mutex::new(false)),
|
||||
schema_last_error: Arc::new(Mutex::new(None)),
|
||||
last_database_error: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn long_prompt(marker: &str) -> String {
|
||||
let filler = (0..76)
|
||||
.map(|index| format!("long-prompt-line-{index:02} {}", "abcdefghijklmnopqrstuvwxyz0123456789".repeat(2)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
[
|
||||
"Code Queue long prompt observation contract".to_string(),
|
||||
"".to_string(),
|
||||
filler,
|
||||
"".to_string(),
|
||||
"验收标准:".to_string(),
|
||||
format!("所有原始 prompt 观察层必须保留这个唯一 tail marker: {marker}"),
|
||||
]
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn test_task(prompt: String) -> TaskMeta {
|
||||
let at = now_iso();
|
||||
TaskMeta {
|
||||
id: "codex_1779000000000_longprompt".to_string(),
|
||||
queue_id: "e2e-long-prompt".to_string(),
|
||||
status: "queued".to_string(),
|
||||
provider_id: "D601".to_string(),
|
||||
execution_mode: "default".to_string(),
|
||||
model: "gpt-5.5".to_string(),
|
||||
cwd: "/home/ubuntu".to_string(),
|
||||
prompt: prompt.clone(),
|
||||
base_prompt: prompt,
|
||||
reference_task_ids: json!([]),
|
||||
reference_injection: None,
|
||||
reasoning_effort: None,
|
||||
max_attempts: 1,
|
||||
current_attempt: 0,
|
||||
current_mode: None,
|
||||
codex_thread_id: None,
|
||||
active_turn_id: None,
|
||||
created_at: at.clone(),
|
||||
updated_at: at,
|
||||
started_at: None,
|
||||
finished_at: None,
|
||||
read_at: None,
|
||||
last_error: None,
|
||||
last_judge: None,
|
||||
output_count: 1,
|
||||
event_count: 0,
|
||||
attempt_count: 0,
|
||||
last_output_seq: 1,
|
||||
task_json: Some(json!({
|
||||
"queueEnteredAt": now_iso(),
|
||||
"finalResponse": "",
|
||||
"stepCount": 0,
|
||||
"llmStepCount": 0,
|
||||
"judgeFailCount": 0,
|
||||
"cancelRequested": false,
|
||||
"attempts": [],
|
||||
"output": [],
|
||||
"events": [],
|
||||
"promptHistory": []
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
fn field_text<'a>(value: &'a Value, key: &str) -> &'a str {
|
||||
value.get(key).and_then(Value::as_str).unwrap_or("")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_observation_preserves_raw_fields_and_explicit_previews() {
|
||||
let state = test_state();
|
||||
let marker = "RUST_PROMPT_OBSERVATION_TAIL_MARKER";
|
||||
let prompt = long_prompt(marker);
|
||||
assert!(prompt.chars().count() > 2500);
|
||||
let task = test_task(prompt);
|
||||
let create = task_list_response(&state, &task, false);
|
||||
let overview = task_list_response(&state, &task, true);
|
||||
let meta = task_meta_response(&state, &task);
|
||||
let raw_fields = [
|
||||
field_text(&create, "prompt"),
|
||||
field_text(&create, "basePrompt"),
|
||||
field_text(&create, "displayPrompt"),
|
||||
field_text(&overview, "prompt"),
|
||||
field_text(&overview, "basePrompt"),
|
||||
field_text(&overview, "displayPrompt"),
|
||||
field_text(&meta, "prompt"),
|
||||
field_text(&meta, "basePrompt"),
|
||||
field_text(&meta, "displayPrompt"),
|
||||
field_text(&meta, "initialPrompt"),
|
||||
];
|
||||
assert!(raw_fields.iter().all(|value| value.contains(marker)), "{raw_fields:?}");
|
||||
assert!(raw_fields.iter().all(|value| !value.contains("...<truncated>")), "{raw_fields:?}");
|
||||
|
||||
let preview = overview.get("promptPreview").and_then(Value::as_object).expect("promptPreview metadata");
|
||||
assert_eq!(preview.get("truncated").and_then(Value::as_bool), Some(true));
|
||||
assert_eq!(preview.get("chars").and_then(Value::as_u64), Some(field_text(&overview, "prompt").chars().count() as u64));
|
||||
assert!(preview.get("text").and_then(Value::as_str).unwrap_or("").contains("...<truncated>"));
|
||||
assert!(!preview.get("text").and_then(Value::as_str).unwrap_or("").contains(marker));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import postgres from "postgres";
|
||||
import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl";
|
||||
import { codeQueuePromptResponseFields, safePreview } from "./prompt-observation";
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
type JsonRecord = Record<string, JsonValue>;
|
||||
@@ -434,12 +435,6 @@ function timestampMs(value: string | Date | null | undefined): number | null {
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
|
||||
function safePreview(value: unknown, maxChars: number): string {
|
||||
const text = String(value ?? "");
|
||||
if (text.length <= maxChars) return text;
|
||||
return `${text.slice(0, Math.max(0, maxChars - 20))}\n...<truncated>`;
|
||||
}
|
||||
|
||||
function prefixPreview(value: unknown, maxChars: number): string {
|
||||
const text = String(value ?? "");
|
||||
return text.length <= maxChars ? text : `${text.slice(0, Math.max(0, maxChars - 1))}…`;
|
||||
@@ -1635,17 +1630,12 @@ function taskStatisticsSummary(tasks: QueueTask[], days = 14): JsonRecord {
|
||||
}
|
||||
|
||||
function taskListResponse(task: QueueTask, lite = true): JsonRecord {
|
||||
const displayPrompt = task.basePrompt || userPromptForDisplay(task.prompt);
|
||||
const promptFields = codeQueuePromptResponseFields(task, { lite, userPromptForDisplay });
|
||||
return {
|
||||
id: task.id,
|
||||
queueId: queueIdOf(task),
|
||||
queueEnteredAt: task.queueEnteredAt,
|
||||
prompt: lite ? prefixPreview(displayPrompt, 360) : safePreview(displayPrompt, 2000),
|
||||
basePrompt: lite ? prefixPreview(task.basePrompt, 360) : safePreview(task.basePrompt, 2000),
|
||||
displayPrompt: lite ? prefixPreview(displayPrompt, 360) : safePreview(displayPrompt, 2000),
|
||||
promptChars: task.prompt.length,
|
||||
basePromptChars: task.basePrompt.length,
|
||||
displayPromptChars: displayPrompt.length,
|
||||
...promptFields,
|
||||
promptEditable: queuedTaskPromptEditable(task),
|
||||
finalResponseChars: task.finalResponse.length,
|
||||
stepCount: numberField(task.stepCount ?? task.llmStepCount, 0),
|
||||
@@ -1695,6 +1685,8 @@ function taskMetaResponse(task: QueueTask): JsonRecord {
|
||||
...taskListResponse(task, false),
|
||||
prompt: task.prompt,
|
||||
basePrompt: task.basePrompt,
|
||||
displayPrompt: task.basePrompt || userPromptForDisplay(task.prompt),
|
||||
initialPrompt: task.prompt,
|
||||
finalResponse: task.finalResponse,
|
||||
promptHistory: toJsonValue(task.promptHistory),
|
||||
attempts: toJsonValue(task.attempts),
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||
export type JsonRecord = Record<string, JsonValue>;
|
||||
|
||||
export interface PromptObservationTask {
|
||||
prompt: string;
|
||||
basePrompt: string;
|
||||
}
|
||||
|
||||
export interface PromptObservationOptions {
|
||||
lite?: boolean;
|
||||
userPromptForDisplay: (prompt: string) => string;
|
||||
}
|
||||
|
||||
export function safePreview(value: unknown, maxChars: number): string {
|
||||
const text = String(value ?? "");
|
||||
if (text.length <= maxChars) return text;
|
||||
return `${text.slice(0, Math.max(0, maxChars - 20))}\n...<truncated>`;
|
||||
}
|
||||
|
||||
export function textPreview(value: unknown, maxChars: number): JsonRecord {
|
||||
const text = String(value ?? "");
|
||||
const truncated = text.length > maxChars;
|
||||
return {
|
||||
text: truncated ? safePreview(text, maxChars) : text,
|
||||
chars: text.length,
|
||||
truncated,
|
||||
omittedChars: truncated ? text.length - Math.max(0, maxChars - 20) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function codeQueuePromptResponseFields(task: PromptObservationTask, options: PromptObservationOptions): JsonRecord {
|
||||
const displayPrompt = task.basePrompt || options.userPromptForDisplay(task.prompt);
|
||||
const previewLimit = options.lite === true ? 360 : 2000;
|
||||
return {
|
||||
prompt: task.prompt,
|
||||
basePrompt: task.basePrompt,
|
||||
displayPrompt,
|
||||
promptPreview: textPreview(task.prompt, previewLimit),
|
||||
basePromptPreview: textPreview(task.basePrompt, previewLimit),
|
||||
displayPromptPreview: textPreview(displayPrompt, previewLimit),
|
||||
promptChars: task.prompt.length,
|
||||
basePromptChars: task.basePrompt.length,
|
||||
displayPromptChars: displayPrompt.length,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user