feat(drafts): the drafts tools, the preview digest on every confirm, the digest tool (P4)
Accounting reason: a confirm must act on the thing that was previewed. Every tool with confirmed (post_entry, reverse_entry, run_schedules, dispose_asset, reconcile, close_period, create_invoice, create_bill, write_off_invoice, purge_tin, consolidate_duplicates, and the S6 settlement tools receive_payment and pay_bill, which gain confirmed and preview on a clone of the ledger) returns preview (SHA-256 of the canonical preview) and refuses a confirm whose preview is missing or stale (stale_preview, before anything is written). A draft's digest is its own state and the settings seq; an operation's is the tool, its arguments as sent, the settings seq and the journal head seq, so another session's write forces a re-preview (D-50). New tools books.list_drafts, books.set_draft_state, books.discard_draft, books.preview_entry; reports carry as_of_status (drafts open on or before their end, unreconciled accounts, settings problems) and take strict; the CPA package is refused while a draft dated in it is open unless allow_open_drafts gives a reason, journaled as a decision; books.digest takes since_seq (what was posted since a journal line, backdated entries included: defect 6) and lists zero_net_accounts, drafts_by_state and the reconciliation split. Needs attention gains 'draft awaiting receipt' beside 'draft not posted'; the drafts handbook is rewritten. Every over-the-wire harness confirms the way an agent does. Docs: mcp-interface, rules B-43, parity, testing (with the For-P12 paragraph), architecture, and the unit's As-built block. Tests: drafts.rs (a_draft_waits_for_its_receipt_and_posts, the_gate_moved_and_the_mcp_error_is_unchanged, a_reversal_needs_no_receipt_of_its_own, discard_unblocks_the_close_over_the_wire, report_warns_on_draft_before_as_of, cpa_package_refuses_open_draft, stale_preview_refused, every_confirm_tool_takes_a_preview, digest_since_seq_is_posting_time, legacy_reversed_replays_identically, gui_and_server_gate_agree), attention_parity.rs, format_upgrade.rs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
7299cf90f2
commit
6a66a2855b
49 files changed
+2526
-202
No files matched your search
@@ -366,8 +366,32 @@ impl Wire {
|
||||
resp
|
||||
}
|
||||
|
||||
/// Call a tool: the `tools/call` answer.
|
||||
pub fn call(&mut self, tool: &str, args: Value) -> Value {
|
||||
/// Call a tool: the `tools/call` answer. A call with `confirmed: true`
|
||||
/// and no `preview` is made the way an agent makes it (plan P4, D-50):
|
||||
/// the same call without `confirmed` first, then the confirm with the
|
||||
/// `preview` it returned. A preview that is refused is the answer
|
||||
/// (the refusal a confirm would have met); a tool that returns no
|
||||
/// preview is called as given. Use [`Self::call_raw`] to send exactly
|
||||
/// these arguments.
|
||||
pub fn call(&mut self, tool: &str, mut args: Value) -> Value {
|
||||
if args.get("confirmed") == Some(&json!(true)) && args.get("preview").is_none() {
|
||||
let mut first = args.clone();
|
||||
first["confirmed"] = json!(false);
|
||||
let seen = self.call_raw(tool, first);
|
||||
match answer_of(&seen) {
|
||||
Err(_) => return seen,
|
||||
Ok(out) => {
|
||||
if let Some(digest) = out.get("preview").and_then(Value::as_str) {
|
||||
args["preview"] = json!(digest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.call_raw(tool, args)
|
||||
}
|
||||
|
||||
/// Call a tool with exactly these arguments.
|
||||
pub fn call_raw(&mut self, tool: &str, args: Value) -> Value {
|
||||
self.rpc("tools/call", json!({"name": tool, "arguments": args}))
|
||||
}
|
||||
|
||||
@@ -597,6 +621,28 @@ pub fn upload_args(bytes: &[u8], file_name: &str, mime_type: &str, purpose: &str
|
||||
"mime_type": mime_type, "file_name": file_name, "purpose": purpose})
|
||||
}
|
||||
|
||||
/// `tools/call` through `rpc` (a harness's own request function), the way
|
||||
/// an agent confirms (plan P4, D-50): a call with `confirmed: true` and no
|
||||
/// `preview` is made first without `confirmed`, then with the `preview` it
|
||||
/// returned. A refused preview is the answer; a tool that returns no
|
||||
/// preview is called as given.
|
||||
pub fn tools_call(mut rpc: impl FnMut(&str, Value) -> Value, name: &str, mut args: Value) -> Value {
|
||||
if args.get("confirmed") == Some(&json!(true)) && args.get("preview").is_none() {
|
||||
let mut first = args.clone();
|
||||
first["confirmed"] = json!(false);
|
||||
let seen = rpc("tools/call", json!({"name": name, "arguments": first}));
|
||||
match answer_of(&seen) {
|
||||
Err(_) => return seen,
|
||||
Ok(out) => {
|
||||
if let Some(digest) = out.get("preview").and_then(Value::as_str) {
|
||||
args["preview"] = json!(digest);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rpc("tools/call", json!({"name": name, "arguments": args}))
|
||||
}
|
||||
|
||||
/// A `tools/call` answer: Ok(its structured result) or Err(the refusal's
|
||||
/// or the protocol error's text).
|
||||
pub fn answer_of(r: &Value) -> Result<Value, String> {
|
||||
@@ -985,7 +1031,22 @@ pub async fn connect_at(run: &Path, book: &str, token: Option<String>) -> Result
|
||||
}
|
||||
|
||||
/// Call a tool: Ok(structured result) or Err(the error text).
|
||||
pub async fn call(c: &Client, tool: &str, args: Value) -> Result<Value, String> {
|
||||
/// Call a tool the way an agent confirms (plan P4, D-50): a call with
|
||||
/// `confirmed: true` and no `preview` is made first without `confirmed`,
|
||||
/// then with the `preview` it returned. A refused preview is the answer.
|
||||
pub async fn call(c: &Client, tool: &str, mut args: Value) -> Result<Value, String> {
|
||||
if args.get("confirmed") == Some(&json!(true)) && args.get("preview").is_none() {
|
||||
let mut first = args.clone();
|
||||
first["confirmed"] = json!(false);
|
||||
let seen = call_once(c, tool, first).await?;
|
||||
if let Some(digest) = seen.get("preview").and_then(Value::as_str) {
|
||||
args["preview"] = json!(digest);
|
||||
}
|
||||
}
|
||||
call_once(c, tool, args).await
|
||||
}
|
||||
|
||||
async fn call_once(c: &Client, tool: &str, args: Value) -> Result<Value, String> {
|
||||
use rmcp::model::CallToolRequestParams;
|
||||
let args = args.as_object().cloned().unwrap_or_default();
|
||||
// A client whose server went away may wait forever: a call that
|
||||
|
||||
Reference in new issue
Block a user