🔶 法律 AI,而非带脚手架的编码代理
英文标题:🔶 Legal AI, not a coding agent with scaffolding
本文以补丁匹配、内容压缩和变更追溯为核心,对比了面向编码的 AI 代理(如 Codex)与专为法律用途设计的系统(如 Lexifina)在机制上的关键差异。示例表明,Codex 的文本严格匹配虽能防止过时补丁,但缺乏对交叉引用等结构依赖的审查,而 Lexifina 则将依赖检查缺失转化为可审计的警告。在内容压缩方面,Lexifina 保留可恢复的精确工具输出地址,避免以摘要替代证据;红线对比则需追溯到具体的编辑指令与父版本链,而非仅输出统一差异。这些差异提示开发者:法律 AI 必须将证据溯源、依赖检查与决策透明作为核心产品要求,而非可选的指导层,这对保障律师专业判断和降低执业风险至关重要。
27.2 KB
中文
当前,法律 AI 的大部分使用都是通过为编码构建的代理进行的。让我们聚焦于专为法律目的打造的系统可能切实实现什么。
一般而言,法律 AI 应当找出最有依据的论证,识别出正确的证据,并使这些证据易于核实。
代理工作区是正确的交互界面。用户可以看到一条包含模型所用工具和来源的审计跟踪。用户可以通过创建侧边聊天来控制上下文,并定义特定的来源和技能,从而影响模型行为。
代理的事实依据必须落实到单个声明和编辑层次。仅仅检索一个外部文档并将其作为整体引用是远远不够的。这种精细程度取决于论证本身——可能需要引用单个句子、一个段落或一组页面。每个来源的层级、状态和预期目的都应该清晰明确。这些信息可以通过本体论或类似的图网络存储起来。例如,区分法律权威与客户事实,当前文档措辞与先前草案措辞。
当 AI 建议一项编辑时,律师应该能够看到为什么它做出了这个修改。这包括原始指令、它审查过的条款、它考虑过的事实、它所依据的法律渊源,以及任何可以通过确定性计算并整齐打包给用户的警告或不确定性。
AI 不应自动进行修改。它应该将提议的编辑显示为可以单独处理的红线标注(或修订痕迹!)。这种工作流在一定程度上能阻止用户盲目接受修改。不用说,所有修改都会被记录,并且随时可以通过底层的版本控制系统撤销。让一个概率性过程在没有版本控制系统的情况下编辑文档,简直就是鲁莽之举。
技能、记忆和内容压缩应当保留对证据的访问能力。它们不应用摘要来代替证据。这在法律应用场景中是远远不够的。相反,它们应该留下一种查找工件,以便能够查看确切的原始内容来获取事实信息。
AI,即使经过证据基础增强,也绝不能损害法律判断。来源可能过时、狭隘、不完整,或仅限于某一司法管辖区。系统应该让这些局限性更加可见,而不是隐藏它们,并让律师能够利用最新信息做出充分知情的决定。
编码代理仅强制执行机械正确性,将解释作为指导方针。律师之所以受到处罚,正是因为证据基础被视为一个可选的指导层,而非核心的产品或工作流要求。而令人遗憾的是,这正是当前法律 AI 工具的现状。
删除条款:补丁是否匹配,其依赖是否已检查?
举个例子:删除第 12.7 节,因为其义务似乎在其他地方重复了。这里有两种可能出错的方式:编辑准备好之后,文本可能已经发生变化,或者另一个条款仍然引用了第 12.7 节。
Codex 的本地补丁路径直接解决了第一种失败。它在当前文件中搜索旧的文本行:
codex/codex-rs/apply-patch/src/lib.rs:765
Code example+−
CodePseudocode
let mut pattern: &[String] = &chunk.old_lines;
let mut found =
seek_sequence::seek_sequence(original_lines, pattern, line_index, chunk.is_end_of_file);
如果预期的文本已不存在,补丁就会失败,而不是应用到一个过时的位置:
codex/codex-rs/apply-patch/src/lib.rs:787
Code example+−
CodePseudocode
if let Some(start_idx) = found {
replacements.push((start_idx, pattern.len(), new_slice.to_vec()));
line_index = start_idx + pattern.len();
} else {
return Err(ApplyPatchError::ComputeReplacements(format!(
"Failed to find expected lines in {}:\n{}",
path,
chunk.old_lines.join("\n"),
)));
}
这是一个很强的文本不变性,但它对法律文档的依赖关系却只字未提。Codex 可以搜索交叉引用,但原生的补丁应用并不要求必须完成这一搜索。
Lexifina 会记录对某个章节的结构化交叉引用查找是否成功完成:
Code example+−
CodePseudocode
def _completed_cross_reference_terms(ctx: ToolUseContext) -> set[str]:
terms: set[str] = set()
for row in getattr(ctx.document_state, "tool_call_log", None) or []:
if str(row.get("tool_name") or "") != "get_cross_references":
continue
if str(row.get("status") or "").lower() != "ok" or row.get("is_error"):
continue
raw_input = row.get("raw_input")
if not isinstance(raw_input, dict):
raw_input = row.get("input_preview")
if not isinstance(raw_input, dict):
continue
term = _normalized_reference_term(raw_input.get("term"))
if term:
terms.add(term)
return terms
对于一个整节的删除,若缺少了这样的已记录审查,就会成为编辑级的警告:
Code example+−
CodePseudocode
def _cross_reference_advisories(
*,
ctx: ToolUseContext,
original_text: str,
new_text: str,
) -> List[Dict[str, Any]]:
if not original_text.strip() or new_text.strip():
return []
label = _section_reference_label(original_text)
if not label or _normalized_reference_term(label) in _completed_cross_reference_terms(ctx):
return []
return [
{
"code": "cross_reference_review_not_recorded",
"severity": "warning",
"message": (
f"No completed structured cross-reference review for {label} "
"was recorded before this deletion. The deletion remains staged "
"for your decision."
),
"source": "deterministic_legal_review",
"details": {"section_label": label},
}
]
每个系统防范的是不同类型的故障。Codex 防止一个过时的代码块被应用到不再匹配的文本上。Lexifina 则把结构性依赖检查的缺失带入审查对象中。
内容压缩:模型能否恢复确切的工具输出?
假设一个研究工具返回了一份长篇意见摘录,模型使用了其中的两个段落,而在起草工作完成之前,对话内容被压缩了。一份叙述性摘要写着“该案例支持此论点”,但这并不是证据。关键问题在于,活跃代理是否能重新获取确切的工具输出。
在 Codex 的本地压缩路径中,替换历史是根据保留的用户消息和一份生成的摘要构建的:
codex/codex-rs/core/src/compact.rs:323
Code example+−
CodePseudocode
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
这就是模型活跃的替换历史。它并没有给摘要附加一个指向单个工具结果的查询入口。但这并不意味着 Codex 删除了其持久化的回滚记录,并且代理或许能够重新读取源文件或重新运行工具。
Lexifina 的压缩标记则为模型留下了一个可执行的地址,用于查询一个被清理的结果:
Code example+−
CodePseudocode
# Marker template — note the {tool_use_id} placeholder is filled per
# tool result so the model can call read_tool_result with the right
# argument without searching for the id. Format string, not f-string,
# so callers that import this module can introspect it.
TOOL_RESULT_CLEARED_MARKER_TEMPLATE = (
"[Tool result cleared during compaction — full content available "
'via read_tool_result(tool_use_id="{tool_use_id}") if needed.]"
)
read_tool_result 会将该 ID 解析为持久化的完整输出,并返回所请求的范围,而无需让另一个模型来重建它:
Code example+−
CodePseudocode
path = Path(RESULT_PERSIST_DIR) / process_id / f"{input_data.tool_use_id}.txt"
if not path.exists():
return ToolResult(
is_error=True,
assistant_text=(
f"No persisted result found for tool_use_id="
f"{input_data.tool_use_id!r}. Either the id is wrong "
"or the result was small enough to ship inline (no "
"truncation notice = no persisted file). Check the "
"tool result you copied the id from."
),
summary="not found",
)
Code example+−
CodePseudocode
bounded_len = min(int(input_data.length), MAX_FETCH_CHARS)
try:
with path.open("r", encoding="utf-8") as f:
f.seek(int(input_data.offset))
chunk = f.read(bounded_len)
但此恢复仅限于当前运行:模型可见的文件是临时的,在运行结束时就会被删除:
Code example+−
CodePseudocode
# Result-size cap: oversized rendered text is persisted to
# disk and the model-facing block becomes a head/tail preview
# with the tool_use_id. Skipped when the tool itself opts out
# via max_result_size_chars=math.inf (read_tool_result, where
# persisting a fetched chunk would create a circular loop) or
# when the result is an error (errors are usually small and
# already structured for the model — persisting them just adds
# a layer of indirection). The persisted file is cleaned up
# at end-of-run by routers/agentic.py.
简而言之,压缩可以替换大量的文本,而无需强制下一个模型去信任一份转述;tool_use_id 始终是一条返回确切持久化工具文本的路径。Codex 的本地替换历史保留了一份摘要,但其本身并未暴露一个等价的调用标识查找机制。
红线标注显示了变更内容。但它能证明是哪个起草决策导致了这个变更吗?
假设版本 3 将赔偿上限设定为根据协议已支付的费用。随后团队回退到更早的一个草案,并创建了版本 6,在该版本中此上限被移除了。律师的问题不仅仅是“文本有什么不同?”。而是:当前起草分支是否移除了赔偿上限,是哪一个被接受的编辑移除了它,产生该编辑的指令是什么,以及答案是基于已保存的变更记录还是后来才做的重建?
Codex 维护着一个精确的净差异,用于记录当前请求期间由已提交补丁造成的文件变更:
codex/codex-rs/core/src/turn_diff_tracker.rs:48
Code example+−
CodePseudocode
/// Tracks the net text diff for the current turn from committed apply_patch
/// mutations, without rereading the workspace filesystem.
pub struct TurnDiffTracker {
valid: bool,
display_roots_by_environment: HashMap<String, PathBuf>,
baseline_by_path: HashMap<TrackedPath, TrackedContent>,
current_by_path: HashMap<TrackedPath, TrackedContent>,
origin_by_current_path: HashMap<TrackedPath, TrackedPath>,
next_revision: u64,
rendered_diffs: HashMap<DiffCacheKey, Option<String>>,
unified_diff: Option<String>,
只有精确的补丁增量才会被纳入这个跟踪器。一个非精确的增量会直接使结果失效,从而避免可能产生误导的差异被保留下来:
codex/codex-rs/core/src/turn_diff_tracker.rs:93
Code example+−
CodePseudocode
pub fn track_delta(&mut self, environment_id: &str, delta: &AppliedPatchDelta) {
if !self.valid {
return;
}
if !delta.is_exact() {
self.invalidate();
return;
}
for change in delta.changes() {
self.apply_change(environment_id, change);
}
self.refresh_unified_diff();
}
发送给客户端的事件包含了这个统一差异:
codex/codex-rs/core/src/session/turn.rs:2495
Code example+−
CodePseudocode
if should_emit_turn_diff {
let unified_diff = {
let tracker = turn_diff_tracker.lock().await;
tracker.get_unified_diff()
};
if let Some(unified_diff) = unified_diff {
let msg = EventMsg::TurnDiff(TurnDiffEvent { unified_diff });
sess.clone().send_event(&turn_context, msg).await;
}
}
事件的有效负载仅仅是一个文本字段:
codex/codex-rs/protocol/src/protocol.rs:3669
Code example+−
CodePseudocode
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnDiffEvent {
pub unified_diff: String,
}
对于“此次 Codex 请求变更了什么”这个问题,这是一个很坚实的答案。但它并没有让差异事件本身成为一份已保存的文档谱系记录:这个代码块并不包含父版本、被接受的编辑标识、触发指令、审核者、应用时间或置信度字段。Codex 可以保留周围的一系列事件,而版本控制系统也可以添加提交历史。更明确的要点在于,这些关系并非这个原生差异对象的不变量。
Lexifina 首先遵循文档的实际父链。它不会假定版本 6 是从版本 5 衍生而来的——这个假设在回退或分支之后是错误的:
Code example+−
CodePseudocode
# Walk the parent chain rather than blindly sorting by version
# number. Branched versions (e.g. user reverted to v3 then applied
# changes to produce v6 with parent=3) describe their delta against
# their explicit parent, not against v(N-1). Following the parent
# field gives the
English / 原文
The majority of legal AI use is transacted through agents built for coding. Let’s focus on what a system built for a legal purpose might tangibly achieve.
Generally, legal AI should find the strongest supportable arguments, identify the correct evidence, and make that evidence easy to verify.
An agent workspace is the correct interface. The user sees an audit trail including the tools and sources used by the model. They are able to control the context by creating side chats, and define specific sources and skills to manipulate model behaviour.
Agent grounding must happen at the level of individual claims and edits. It is simply not good enough to retrieve an external document and cite it as a whole. This level of granularity depends on the argument- which may call for an individual sentence, a paragraph, or a set of pages. The hierarchy, status, and intended purpose of each source should be explicit. That information can be stored via an ontology or similar graph network. For example, legal authority vs client facts, current document language vs previous drafting language.
When the AI suggests an edit, the lawyer should be able to see why it made that change. That includes the original instruction, the clauses it reviewed, the facts it considered, the legal sources it relied on, and any warnings or uncertainties that can be calculated deterministically and packaged neatly for the user.
AI should not make changes automatically. It should show the proposed edit as a redline (or tracked change!) that can be processed individually. This workflow goes some way to deterrings users from blindly accepting changes. Needless to say, changes are logged and revocable at any time using an underlying version control system. Letting a probabilistic process edit a document without a version control system would simply be reckless.
Skills, memory and compaction should preserve access to evidence. They should not replace evidence with summaries. That is not good enough in a legal use-case. Rather, it should leave behind a lookup artifact that allows the exact original content to be viewed for factual information.
AI, even evidence grounded, must never subvert legal judgment. Sources may be outdated, narrow, incomplete, or limited to a particular jurisdiction. The system should make these limitations more visible, not less, and enable a lawyer to make fully informed decisions with up-to-date information.
Coding agents only enforce mechanical correctness, with interpretation as a guideline. Lawyers are being sanctioned because evidence grounding is being treated as an optional guidance layer, rather than a core product or workflow requirement. And unfortunately, that is the current story for legal AI tools.
Deleting a clause: did the patch match, and were its dependencies checked?
Take the instruction: delete Section 12.7 because its obligation appears to be duplicated elsewhere. There are two different ways this can go wrong. The text may have changed since the edit was prepared, or another clause may still refer to Section 12.7.
Codex's native patch path addresses the first failure directly. It looks for the old lines in the current file:
codex/codex-rs/apply-patch/src/lib.rs:765
Code example+−
CodePseudocode
let mut pattern: &[String] = &chunk.old_lines;
let mut found =
seek_sequence::seek_sequence(original_lines, pattern, line_index, chunk.is_end_of_file);
If the expected text is no longer present, the patch fails rather than applying against a stale location:
codex/codex-rs/apply-patch/src/lib.rs:787
Code example+−
CodePseudocode
if let Some(start_idx) = found {
replacements.push((start_idx, pattern.len(), new_slice.to_vec()));
line_index = start_idx + pattern.len();
} else {
return Err(ApplyPatchError::ComputeReplacements(format!(
"Failed to find expected lines in {}:\n{}",
path,
chunk.old_lines.join("\n"),
)));
}
That is a strong textual invariant, but it says nothing about legal-document dependencies. Codex can search for cross-references, but the native patch application does not require that search to have happened.
Lexifina records whether a structured cross-reference lookup for the section completed successfully:
Code example+−
CodePseudocode
def _completed_cross_reference_terms(ctx: ToolUseContext) -> set[str]:
terms: set[str] = set()
for row in getattr(ctx.document_state, "tool_call_log", None) or []:
if str(row.get("tool_name") or "") != "get_cross_references":
continue
if str(row.get("status") or "").lower() != "ok" or row.get("is_error"):
continue
raw_input = row.get("raw_input")
if not isinstance(raw_input, dict):
raw_input = row.get("input_preview")
if not isinstance(raw_input, dict):
continue
term = _normalized_reference_term(raw_input.get("term"))
if term:
terms.add(term)
return terms
For a whole-section deletion, absence of that recorded review becomes an edit-level warning:
Code example+−
CodePseudocode
def _cross_reference_advisories(
*,
ctx: ToolUseContext,
original_text: str,
new_text: str,
) -> List[Dict[str, Any]]:
if not original_text.strip() or new_text.strip():
return []
label = _section_reference_label(original_text)
if not label or _normalized_reference_term(label) in _completed_cross_reference_terms(ctx):
return []
return [
{
"code": "cross_reference_review_not_recorded",
"severity": "warning",
"message": (
f"No completed structured cross-reference review for {label} "
"was recorded before this deletion. The deletion remains staged "
"for your decision."
),
"source": "deterministic_legal_review",
"details": {"section_label": label},
}
]
Each system protects against different failures. Codex prevents a stale hunk from being applied to text it no longer matches. Lexifina carries the absence of a structural-dependency check into the review object.
Compaction: can the model recover the exact tool output?
Suppose a research tool returned a long opinion extract, the model used two paragraphs from it, and the conversation was compacted before drafting finished. A narrative summary that says “the case supports the argument” is not the evidence. The relevant question is whether the active agent can get the exact tool output back.
In Codex's local compaction path, the replacement history is built from retained user messages and a generated summary:
codex/codex-rs/core/src/compact.rs:323
Code example+−
CodePseudocode
let history_snapshot = sess.clone_history().await;
let history_items = history_snapshot.raw_items();
let summary_suffix = get_last_assistant_message_from_turn(history_items).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_user_messages(history_items);
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
That is the model's active replacement history. It does not add a per-tool-result lookup to the summary. This does not mean Codex deletes its durable rollout, and an agent may be able to reread a source file or rerun a tool.
Lexifina's compaction marker leaves the model an executable address for a cleared result:
Code example+−
CodePseudocode
# Marker template — note the {tool_use_id} placeholder is filled per
# tool result so the model can call read_tool_result with the right
# argument without searching for the id. Format string, not f-string,
# so callers that import this module can introspect it.
TOOL_RESULT_CLEARED_MARKER_TEMPLATE = (
"[Tool result cleared during compaction — full content available "
'via read_tool_result(tool_use_id="{tool_use_id}") if needed.]'
)
read_tool_result resolves that id to the persisted full output and returns the requested range without asking another model to reconstruct it:
Code example+−
CodePseudocode
path = Path(RESULT_PERSIST_DIR) / process_id / f"{input_data.tool_use_id}.txt"
if not path.exists():
return ToolResult(
is_error=True,
assistant_text=(
f"No persisted result found for tool_use_id="
f"{input_data.tool_use_id!r}. Either the id is wrong "
"or the result was small enough to ship inline (no "
"truncation notice = no persisted file). Check the "
"tool result you copied the id from."
),
summary="not found",
)
Code example+−
CodePseudocode
bounded_len = min(int(input_data.length), MAX_FETCH_CHARS)
try:
with path.open("r", encoding="utf-8") as f:
f.seek(int(input_data.offset))
chunk = f.read(bounded_len)
This recovery is limited to the active run: the model-facing file is temporary and deleted when the run ends:
Code example+−
CodePseudocode
# Result-size cap: oversized rendered text is persisted to
# disk and the model-facing block becomes a head/tail preview
# with the tool_use_id. Skipped when the tool itself opts out
# via max_result_size_chars=math.inf (read_tool_result, where
# persisting a fetched chunk would create a circular loop) or
# when the result is an error (errors are usually small and
# already structured for the model — persisting them just adds
# a layer of indirection). The persisted file is cleaned up
# at end-of-run by routers/agentic.py.
In short, compaction can replace bulk text without forcing the next model to trust a paraphrase; the tool_use_id remains a route back to the exact persisted tool text. Codex's local replacement history retains a summary but does not itself expose an equivalent call-id lookup.
A redline shows what changed. Can it prove which drafting decision changed it?
Suppose version 3 capped indemnity at the fees paid under the agreement. The team then reverted to an earlier draft and created version 6, where the cap is gone. The lawyer's question is not merely “what text differs?” It is: did the current drafting branch remove the cap, which accepted edit did it, what instruction produced that edit, and is the answer based on the saved change record or a reconstruction made later?
Codex maintains an exact net diff for file changes made by committed patches during the current request:
codex/codex-rs/core/src/turn_diff_tracker.rs:48
Code example+−
CodePseudocode
/// Tracks the net text diff for the current turn from committed apply_patch
/// mutations, without rereading the workspace filesystem.
pub struct TurnDiffTracker {
valid: bool,
display_roots_by_environment: HashMap<String, PathBuf>,
baseline_by_path: HashMap<TrackedPath, TrackedContent>,
current_by_path: HashMap<TrackedPath, TrackedContent>,
origin_by_current_path: HashMap<TrackedPath, TrackedPath>,
next_revision: u64,
rendered_diffs: HashMap<DiffCacheKey, Option<String>>,
unified_diff: Option<String>,
Only exact patch deltas are admitted to that tracker. An inexact delta invalidates the result rather than allowing a potentially misleading diff to survive:
codex/codex-rs/core/src/turn_diff_tracker.rs:93
Code example+−
CodePseudocode
pub fn track_delta(&mut self, environment_id: &str, delta: &AppliedPatchDelta) {
if !self.valid {
return;
}
if !delta.is_exact() {
self.invalidate();
return;
}
for change in delta.changes() {
self.apply_change(environment_id, change);
}
self.refresh_unified_diff();
}
The event emitted to the client contains that unified diff:
codex/codex-rs/core/src/session/turn.rs:2495
Code example+−
CodePseudocode
if should_emit_turn_diff {
let unified_diff = {
let tracker = turn_diff_tracker.lock().await;
tracker.get_unified_diff()
};
if let Some(unified_diff) = unified_diff {
let msg = EventMsg::TurnDiff(TurnDiffEvent { unified_diff });
sess.clone().send_event(&turn_context, msg).await;
}
}
The event's entire payload is one textual field:
codex/codex-rs/protocol/src/protocol.rs:3669
Code example+−
CodePseudocode
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct TurnDiffEvent {
pub unified_diff: String,
}
That is a strong answer to “what did this Codex request change?” It does not make the diff event itself a saved-document lineage record: the hunk has no parent version, accepted edit id, triggering instruction, reviewer, application time, or confidence field. Codex can retain surrounding rollout events, and a version-control system can add commit history. The narrower point is that those relationships are not invariants of this native diff object.
Lexifina first follows the document's actual parent chain. It does not assume that version 6 descended from version 5, which would be wrong after a revert or branch:
Code example+−
CodePseudocode
# Walk the parent chain rather than blindly sorting by version
# number. Branched versions (e.g. user reverted to v3 then applied
# changes to produce v6 with parent=3) describe their delta against
# their explicit parent, not against v(N-1). Following the parent
# field gives the true evolution graph of the current document.
chain = _walk_parent_chain(versions)
all_registered: List[Dict[str, Any]] = []
for i in range(len(chain) - 1):
registered = register_pair_diffcites(
ctx.document_state,
version_a=chain[i],
version_b=chain[i + 1],
)
When the child version contains its apply-time change records, Lexifina uses the exact before-and-after strings captured when the user accepted the edit, before later parsing could renumber sections:
Code example+−
CodePseudocode
def _extract_apply_time_hunks(
version_a: Dict[str, Any],
version_b: Dict[str, Any],
) -> List[Dict[str, Any]]:
"""Tier 0: authoritative hunks from ``versions[N].changes``.
The apply route (`routers/changes.py:3158`) writes one entry per
accepted edit on the resulting version, capturing the exact
pre-edit and post-edit text strings BEFORE the post-apply re-parse
that renumbers section IDs. Reading that array gives us
position-blind, intent-bearing hunks for free — no LCS, no
section-id-equality join, no positional drift.
The resulting hunk carries the version pair and text alongside the drafting provenance available for that accepted edit:
Code example+−
CodePseudocode
hunks.append(
{
"from_version": from_v,
"to_version": to_v,
"from_date": from_date,
"to_date": to_date,
"section_id": sid,
"section_heading": heading,
"change_kind": change_kind,
"removed_text": original,
"added_text": new_text,
"explanation": (
str(entry.get("explanation") or "").strip() or None
),
"prompt_text": (
str(source_prompt_text or "").strip() or None
),
"request_user": (
str(request_user or "").strip() or None
),
"applied_at": applied_at,
"run_id": str(run_id or "").strip() or None,
"edit_id": str(edit_id).strip() if edit_id else None,
"confidence": "high",
"source": "apply_time",
}
)
Lexifina also states when it cannot make that stronger claim. If a legacy or manually edited version has no apply-time record, it reconstructs a line diff and labels the source and confidence differently:
Code example+−
CodePseudocode
class RiskDiffcite(BaseModel):
"""One atomic hunk between two persisted document versions.
Two `source` paths populate this:
- ``apply_time``: built from ``versions[N].changes`` — the exact
``original``/``new`` strings captured at the moment the user
clicked Accept. Position-blind by construction (no re-derivation
from post-apply structure). Carries apply-time provenance
(explanation, triggering request, who applied, when).
``confidence`` is "high".
- ``line_diff``: a fallback hunk computed via
``difflib.unified_diff`` over the version's flat text content.
Position-blind via LCS (same approach as the version-history
panel's Monaco diff). Used when ``versions[N].changes`` is
missing (legacy versions, manual editor edits that bypass the
apply route, the v1 baseline). ``confidence`` is "medium".
Finally, the model cannot create a convincing change receipt merely by inventing a [[diff_n]] marker. Markers absent from the registered version-diff evidence are removed before the response is returned:
Code example+−
CodePseudocode
def sanitize_diff_markers(text: Optional[str], valid_diff_ids: Iterable[str]) -> str:
"""Drop every ``[[diff_X]]`` whose id is not in the registry. Collapse
whitespace introduced by the removals so the prose still reads
cleanly. Identical surface to `sanitize_pin_markers`."""
if not text:
return ""
valid = set(valid_diff_ids)
def _replace(match: re.Match[str]) -> str:
diff_id = match.group(1)
return match.group(0) if diff_id in valid else ""
cleaned = DIFF_ID_PATTERN.sub(_replace, text)
What this proves: both systems can produce an exact textual delta, but the native objects answer different questions. Codex proves the net file change made by committed patches during the current request. Lexifina can make a citable hunk prove which parent-and-child document versions it came from and, when an apply-time record exists, attach the exact accepted text, instruction, explanation, user, time, run, and edit id; when that stronger record is absent, it labels the reconstruction as lower-confidence evidence.
An agent harness boils down to a multi-layered series of defences for an intended use-case. There is a lot of work that can still be done in this direction for legal AI, and we have highlighted some here.