feat(persona-chat): practice mode — play the contact
An anchored conversation now has a mode. 'about' is the existing behaviour (a persona talks with the user about the thread). 'as_contact' has the model play the contact so the user can rehearse the conversation: its own prompt (stay in the contact's voice, don't invent facts, coach on 'out of character'), no persona prompt, and only the thread tools — no memory, library, calendar or other-thread access, and no knowledge writes. The shared persona/file-chat loop now refuses tool calls that weren't in the turn's catalog, so the gates hold even when a model names a tool it wasn't offered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ac574d4d2
commit
3f1bf42f1c
8 files changed
+284
-29
No files matched your search
+28
-10
@@ -2466,6 +2466,13 @@ pub async fn run_streaming_agentic_loop_with_entry(
|
||||
let mut last_prompt_eval_count: Option<i32> = None;
|
||||
let mut last_eval_count: Option<i32> = None;
|
||||
let mut final_content = String::new();
|
||||
// Only tools in this turn's catalog may run. The catalog is how every
|
||||
// gate (per-persona corrections, practice mode's narrowed set, absent
|
||||
// backing tables) is expressed, and `execute_tool` dispatches on name
|
||||
// alone — without this a model that names a tool it was never offered
|
||||
// (one it saw in an earlier turn, or simply guessed) would still run it.
|
||||
let offered: std::collections::HashSet<String> =
|
||||
tools.iter().map(|t| t.function.name.clone()).collect();
|
||||
|
||||
for iteration in 0..max_iterations {
|
||||
if !entry.is_running() {
|
||||
@@ -2536,17 +2543,28 @@ pub async fn run_streaming_agentic_loop_with_entry(
|
||||
arguments: tool_call.function.arguments.clone(),
|
||||
})
|
||||
.await;
|
||||
let result = generator
|
||||
.execute_tool(
|
||||
&tool_call.function.name,
|
||||
&tool_call.function.arguments,
|
||||
backend,
|
||||
image_base64,
|
||||
normalized,
|
||||
user_id,
|
||||
active_persona,
|
||||
let result = if offered.contains(&tool_call.function.name) {
|
||||
generator
|
||||
.execute_tool(
|
||||
&tool_call.function.name,
|
||||
&tool_call.function.arguments,
|
||||
backend,
|
||||
image_base64,
|
||||
normalized,
|
||||
user_id,
|
||||
active_persona,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
log::warn!(
|
||||
"refusing tool '{}': not offered this turn",
|
||||
tool_call.function.name
|
||||
);
|
||||
format!(
|
||||
"Error: tool '{}' is not available here.",
|
||||
tool_call.function.name
|
||||
)
|
||||
.await;
|
||||
};
|
||||
let (result_preview, result_truncated) = truncate_tool_result(&result);
|
||||
let _ = entry
|
||||
.push_event(ChatStreamEvent::ToolResult {
|
||||
|
||||
Reference in new issue
Block a user