cc-transcript 10.2.0__tar.gz → 10.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/PKG-INFO +1 -1
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/activity_probe.py +13 -10
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/signals.py +1 -1
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/tools.py +23 -9
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/pyproject.toml +1 -1
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/activity.rs +61 -13
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/mining.rs +8 -14
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/types.rs +13 -1
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/Cargo.lock +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/Cargo.toml +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/LICENSE +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/README.md +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/_parser_rs.pyi +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/command.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/context.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/models.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/query.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/render.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/cc_transcript/store.py +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/Cargo.toml +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/build.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/command.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/event.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/filter.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/lib.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/model.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/parse.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/protocol.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/python.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/score.rs +0 -0
- {cc_transcript-10.2.0 → cc_transcript-10.4.0}/rust/src/value.rs +0 -0
|
@@ -10,8 +10,8 @@ stays identical to. The verdict spans undelivered task notifications (the
|
|
|
10
10
|
turn, and pending async launches (async Agent/Task, live Workflow) session-wide — a
|
|
11
11
|
launch counts as completed only once its notification was delivered or drained, never
|
|
12
12
|
when merely enqueued — and flags unanswered non-human-facing tool calls in the current
|
|
13
|
-
turn as mid-tool. Compact-summary user lines do not open turns,
|
|
14
|
-
|
|
13
|
+
turn as mid-tool. Compact-summary user lines do not open turns, matching captain-hook,
|
|
14
|
+
which consumes this probe since the 10.2.0 shared-classifier fix.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
@@ -30,6 +30,7 @@ from cc_transcript.models import (
|
|
|
30
30
|
)
|
|
31
31
|
from cc_transcript.notifications import Notifications
|
|
32
32
|
from cc_transcript.parser import parse_events_from_bytes
|
|
33
|
+
from cc_transcript.tools import expand_tool_names, matches_names
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from collections.abc import Sequence
|
|
@@ -39,7 +40,10 @@ if TYPE_CHECKING:
|
|
|
39
40
|
from cc_transcript.models import TranscriptEvent
|
|
40
41
|
|
|
41
42
|
DEFAULT_WAITING_TOOLS = frozenset({"Monitor", "ScheduleWakeup", "SendMessage", "TeamCreate"})
|
|
42
|
-
DEFAULT_HUMAN_FACING_TOOLS =
|
|
43
|
+
DEFAULT_HUMAN_FACING_TOOLS = expand_tool_names("AskUserQuestion|ExitPlanMode")
|
|
44
|
+
|
|
45
|
+
BACKGROUND_TOOLS = expand_tool_names("Agent|Task|Bash")
|
|
46
|
+
TASK_TOOLS = expand_tool_names("Agent|Task")
|
|
43
47
|
|
|
44
48
|
PendingKind = Literal[
|
|
45
49
|
"waiting_tool",
|
|
@@ -85,13 +89,12 @@ class SessionActivityProbe:
|
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
def ephemeral_wait(block: ToolUseBlock, waiting_tools: frozenset[str]) -> PendingKind | None:
|
|
88
|
-
if block.name
|
|
92
|
+
if matches_names(block.name, waiting_tools):
|
|
89
93
|
return "waiting_tool"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return "subagentless_task"
|
|
94
|
+
if matches_names(block.name, BACKGROUND_TOOLS) and block.input.get("run_in_background") is True:
|
|
95
|
+
return "background"
|
|
96
|
+
if matches_names(block.name, TASK_TOOLS) and not isinstance(block.input.get("subagent_type"), str):
|
|
97
|
+
return "subagentless_task"
|
|
95
98
|
return None
|
|
96
99
|
|
|
97
100
|
|
|
@@ -151,7 +154,7 @@ def probe_events(
|
|
|
151
154
|
waiting_kind = (ephemeral_wait(block, waiting_tools) if in_current_turn else None) or pending_async(
|
|
152
155
|
block, result, notifications
|
|
153
156
|
)
|
|
154
|
-
unmatched = in_current_turn and result is None and block.name
|
|
157
|
+
unmatched = in_current_turn and result is None and not matches_names(block.name, human_facing_tools)
|
|
155
158
|
is_waiting = is_waiting or waiting_kind is not None
|
|
156
159
|
mid_tool = mid_tool or unmatched
|
|
157
160
|
kind = waiting_kind or ("mid_tool" if unmatched else None)
|
|
@@ -132,7 +132,7 @@ def last_edit_index(events: Sequence[TranscriptEvent], index: int, spec: MiningS
|
|
|
132
132
|
i
|
|
133
133
|
for i in range(index - 1, max(index - spec.reentry_lookback, 0) - 1, -1)
|
|
134
134
|
if isinstance(event := events[i], AssistantEvent)
|
|
135
|
-
if any(isinstance(b, ToolUseBlock) and b.name
|
|
135
|
+
if any(isinstance(b, ToolUseBlock) and matches_names(b.name, spec.edit_tools) for b in event.blocks)
|
|
136
136
|
),
|
|
137
137
|
None,
|
|
138
138
|
)
|
|
@@ -32,6 +32,14 @@ TOOL_ALIASES: dict[str, str] = {
|
|
|
32
32
|
|
|
33
33
|
TOOL_ALIASES_REVERSE: dict[str, str] = {v: k for k, v in TOOL_ALIASES.items()}
|
|
34
34
|
|
|
35
|
+
# cc-context routes real file edits through MCP tools whose bare names match no
|
|
36
|
+
# builtin edit gate; alias its write surface to the builtins those gates watch so
|
|
37
|
+
# an edit through the MCP can't slip past a Tool("Edit"/"Write"/"MultiEdit") guard.
|
|
38
|
+
MCP_TOOL_ALIASES: dict[str, str] = {
|
|
39
|
+
"ccx_code_edit": "Edit",
|
|
40
|
+
"ccx_code_replace": "Write",
|
|
41
|
+
}
|
|
42
|
+
|
|
35
43
|
READ_VERBS = frozenset({"get", "list", "search", "read", "view", "fetch", "query", "describe", "show", "find"})
|
|
36
44
|
|
|
37
45
|
|
|
@@ -482,28 +490,34 @@ def file_path_of(call: ToolCall) -> str | None:
|
|
|
482
490
|
|
|
483
491
|
|
|
484
492
|
def expand_tool_names(spec: str) -> frozenset[str]:
|
|
485
|
-
"""Expand a pipe-separated tool spec to include
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
)
|
|
493
|
+
"""Expand a pipe-separated tool spec to include alias and MCP bare spellings."""
|
|
494
|
+
base = set(spec.split("|"))
|
|
495
|
+
expanded = base | {alias for n in base for alias in (TOOL_ALIASES.get(n), TOOL_ALIASES_REVERSE.get(n)) if alias}
|
|
496
|
+
return frozenset(expanded | {bare for bare, builtin in MCP_TOOL_ALIASES.items() if builtin in expanded})
|
|
490
497
|
|
|
491
498
|
|
|
492
499
|
def matches_names(actual: str, names: Container[str]) -> bool:
|
|
493
500
|
"""Whether ``actual`` is one of ``names``, exactly or as an MCP tool suffix.
|
|
494
501
|
|
|
495
502
|
True when ``actual`` is in ``names``, or when it splits as
|
|
496
|
-
``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>``
|
|
497
|
-
|
|
498
|
-
|
|
503
|
+
``mcp__<server>__<tool>`` on the first two ``__`` and ``<tool>`` — or its
|
|
504
|
+
:data:`MCP_TOOL_ALIASES` builtin equivalent — is in ``names``. That alias
|
|
505
|
+
closes the edit-gate bypass where cc-context's ``ccx_code_edit`` /
|
|
506
|
+
``ccx_code_replace`` write through the MCP under names no ``Edit``/``Write``
|
|
507
|
+
gate would catch. Harness-rename aliases are not closed over — ``names`` is
|
|
508
|
+
taken verbatim; pre-expand with :func:`expand_tool_names` for those.
|
|
499
509
|
|
|
500
510
|
Example:
|
|
501
511
|
>>> matches_names("mcp__github__Grep", {"Grep"})
|
|
502
512
|
True
|
|
513
|
+
>>> matches_names("mcp__cc-context__ccx_code_edit", {"Edit"})
|
|
514
|
+
True
|
|
503
515
|
>>> matches_names("Execute", {"Bash"})
|
|
504
516
|
False
|
|
505
517
|
"""
|
|
506
|
-
return actual in names or (
|
|
518
|
+
return actual in names or (
|
|
519
|
+
(mp := mcp_parts(actual)) is not None and (mp[1] in names or MCP_TOOL_ALIASES.get(mp[1]) in names)
|
|
520
|
+
)
|
|
507
521
|
|
|
508
522
|
|
|
509
523
|
def tool_name_matches(actual: str, spec: str) -> bool:
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
use std::collections::{HashMap, HashSet};
|
|
2
2
|
|
|
3
|
-
use
|
|
3
|
+
use once_cell::sync::Lazy;
|
|
4
|
+
|
|
5
|
+
use crate::types::{matches_names, Entry, ToolResultBlock, ToolUseBlock, UserEntry};
|
|
4
6
|
use crate::value::{field, field_str};
|
|
5
7
|
|
|
6
8
|
const NOTIFICATION_MARKER: &str = "<task-notification>";
|
|
7
9
|
|
|
10
|
+
// tools.py expand_tool_names("Agent|Task|Bash") / ("Agent|Task"), pre-expanded
|
|
11
|
+
// here because the alias table never crosses the language boundary.
|
|
12
|
+
static BACKGROUND_TOOLS: Lazy<HashSet<String>> =
|
|
13
|
+
Lazy::new(|| HashSet::from(["Agent", "Task", "Bash", "Execute"].map(String::from)));
|
|
14
|
+
static TASK_TOOLS: Lazy<HashSet<String>> =
|
|
15
|
+
Lazy::new(|| HashSet::from(["Agent", "Task"].map(String::from)));
|
|
16
|
+
|
|
8
17
|
pub struct ActivityOpts {
|
|
9
18
|
pub waiting_tools: HashSet<String>,
|
|
10
19
|
pub human_facing_tools: HashSet<String>,
|
|
@@ -16,7 +25,11 @@ impl Default for ActivityOpts {
|
|
|
16
25
|
waiting_tools: HashSet::from(
|
|
17
26
|
["Monitor", "ScheduleWakeup", "SendMessage", "TeamCreate"].map(String::from),
|
|
18
27
|
),
|
|
19
|
-
|
|
28
|
+
// tools.py expand_tool_names("AskUserQuestion|ExitPlanMode"), pre-expanded
|
|
29
|
+
// here because the alias table never crosses the language boundary.
|
|
30
|
+
human_facing_tools: HashSet::from(
|
|
31
|
+
["AskUserQuestion", "ExitPlanMode", "ExitSpecMode"].map(String::from),
|
|
32
|
+
),
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
}
|
|
@@ -61,8 +74,8 @@ pub struct SessionActivity {
|
|
|
61
74
|
|
|
62
75
|
/// Whether a user entry opens a turn (activity.py ``native_user_classifier``):
|
|
63
76
|
/// a real prompt — non-meta, non-sidechain, not an interruption marker, with
|
|
64
|
-
/// non-blank text. Compact-summary exclusion
|
|
65
|
-
///
|
|
77
|
+
/// non-blank text. Compact-summary exclusion matches captain-hook, which
|
|
78
|
+
/// consumes this probe since the 10.2.0 shared-classifier fix.
|
|
66
79
|
fn opens_turn(user: &UserEntry) -> bool {
|
|
67
80
|
!(user.meta.is_meta || user.meta.is_sidechain || user.meta.is_compact_summary || user.interrupted())
|
|
68
81
|
&& !user.content.text().trim().is_empty()
|
|
@@ -78,18 +91,19 @@ fn current_turn_start(entries: &[Entry]) -> usize {
|
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/// conditions.py ``ephemeral_wait``: a waiting tool, a backgrounded
|
|
81
|
-
/// Agent/Task/Bash, or a subagentless Agent/Task
|
|
94
|
+
/// Agent/Task/Bash, or a subagentless Agent/Task — every name matched alias-
|
|
95
|
+
/// and MCP-prefix-aware (tools.py matches_names).
|
|
82
96
|
fn ephemeral_wait(tool_use: &ToolUseBlock, waiting_tools: &HashSet<String>) -> Option<PendingKind> {
|
|
83
|
-
if
|
|
97
|
+
if matches_names(&tool_use.name, waiting_tools) {
|
|
84
98
|
return Some(PendingKind::WaitingTool);
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
_ => None,
|
|
100
|
+
if matches_names(&tool_use.name, &BACKGROUND_TOOLS) && tool_use.run_in_background == Some(true) {
|
|
101
|
+
return Some(PendingKind::Background);
|
|
102
|
+
}
|
|
103
|
+
if matches_names(&tool_use.name, &TASK_TOOLS) && tool_use.subagent_type.is_none() {
|
|
104
|
+
return Some(PendingKind::SubagentlessTask);
|
|
92
105
|
}
|
|
106
|
+
None
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
/// conditions.py ``pending_async``: an async Agent/Task launch or a live
|
|
@@ -217,7 +231,7 @@ pub fn session_activity(entries: &[Entry], opts: &ActivityOpts) -> SessionActivi
|
|
|
217
231
|
.or_else(|| pending_async(tool_use, result, ¬ifications));
|
|
218
232
|
let unmatched = in_current_turn
|
|
219
233
|
&& result.is_none()
|
|
220
|
-
&& !
|
|
234
|
+
&& !matches_names(&tool_use.name, &opts.human_facing_tools);
|
|
221
235
|
is_waiting |= waiting_kind.is_some();
|
|
222
236
|
mid_tool |= unmatched;
|
|
223
237
|
let Some(kind) = waiting_kind.or(unmatched.then_some(PendingKind::MidTool)) else {
|
|
@@ -545,6 +559,17 @@ mod tests {
|
|
|
545
559
|
assert!(activity.pending.is_empty());
|
|
546
560
|
}
|
|
547
561
|
|
|
562
|
+
#[test]
|
|
563
|
+
fn background_execute_alias_in_current_turn_is_waiting() {
|
|
564
|
+
let entries = vec![
|
|
565
|
+
user("build it"),
|
|
566
|
+
tool_use("Execute", "e1", r#"{"command":"make","run_in_background":true}"#),
|
|
567
|
+
];
|
|
568
|
+
let activity = activity(&entries);
|
|
569
|
+
assert!(activity.is_waiting);
|
|
570
|
+
assert_eq!(activity.pending[0].kind, PendingKind::Background);
|
|
571
|
+
}
|
|
572
|
+
|
|
548
573
|
#[test]
|
|
549
574
|
fn subagentless_agent_is_waiting() {
|
|
550
575
|
let entries = vec![
|
|
@@ -601,6 +626,17 @@ mod tests {
|
|
|
601
626
|
assert_eq!(activity.pending[0].kind, PendingKind::WaitingTool);
|
|
602
627
|
}
|
|
603
628
|
|
|
629
|
+
#[test]
|
|
630
|
+
fn mcp_prefixed_waiting_tool_is_waiting() {
|
|
631
|
+
let entries = vec![
|
|
632
|
+
user("ping the pool"),
|
|
633
|
+
tool_use("mcp__pool__SendMessage", "s1", r#"{"text":"hi"}"#),
|
|
634
|
+
];
|
|
635
|
+
let activity = activity(&entries);
|
|
636
|
+
assert!(activity.is_waiting);
|
|
637
|
+
assert_eq!(activity.pending[0].kind, PendingKind::WaitingTool);
|
|
638
|
+
}
|
|
639
|
+
|
|
604
640
|
#[test]
|
|
605
641
|
fn pending_ask_user_question_is_quiet() {
|
|
606
642
|
let entries = vec![
|
|
@@ -613,6 +649,18 @@ mod tests {
|
|
|
613
649
|
assert!(activity.pending.is_empty());
|
|
614
650
|
}
|
|
615
651
|
|
|
652
|
+
#[test]
|
|
653
|
+
fn mcp_prefixed_human_facing_tool_is_not_mid_tool() {
|
|
654
|
+
let entries = vec![
|
|
655
|
+
user("choose"),
|
|
656
|
+
tool_use("mcp__someserver__AskUserQuestion", "q1", r#"{"questions":[]}"#),
|
|
657
|
+
];
|
|
658
|
+
let activity = activity(&entries);
|
|
659
|
+
assert!(!activity.is_waiting);
|
|
660
|
+
assert!(!activity.mid_tool);
|
|
661
|
+
assert!(activity.pending.is_empty());
|
|
662
|
+
}
|
|
663
|
+
|
|
616
664
|
#[test]
|
|
617
665
|
fn unmatched_bash_in_current_turn_is_mid_tool() {
|
|
618
666
|
let entries = vec![user("list"), tool_use("Bash", "b1", r#"{"command":"ls"}"#)];
|
|
@@ -13,7 +13,9 @@ use crate::protocol::{
|
|
|
13
13
|
embedded_user_text, interrupt_marker, is_bare_interrupt_marker, ANSWERED_PREFIX, ANSWERED_TRAILER,
|
|
14
14
|
DENIAL_PREFIX, INTERRUPT_MARKER_RE,
|
|
15
15
|
};
|
|
16
|
-
use crate::types::{
|
|
16
|
+
use crate::types::{
|
|
17
|
+
matches_names, tool_use_index, Entry, EntryMeta, Question, ToolResultBlock, ToolUseBlock, UserEntry,
|
|
18
|
+
};
|
|
17
19
|
use crate::value::{field, field_bool, field_str};
|
|
18
20
|
|
|
19
21
|
// Source kinds (mining/sourcekind.py).
|
|
@@ -473,23 +475,11 @@ impl Events {
|
|
|
473
475
|
let lo = index.saturating_sub(spec.reentry_lookback);
|
|
474
476
|
(lo..index)
|
|
475
477
|
.rev()
|
|
476
|
-
.find(|&i| self.entries[i].tool_uses().any(|tu|
|
|
478
|
+
.find(|&i| self.entries[i].tool_uses().any(|tu| matches_names(&tu.name, &spec.edit_tools)))
|
|
477
479
|
.map(|i| i as i64)
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
481
|
-
/// matches_names (tools.py matches_names): exact membership, or the ``<tool>``
|
|
482
|
-
/// segment of an ``mcp__<server>__<tool>`` name split on the first two ``__``.
|
|
483
|
-
/// Alias closure happens Python-side (tools.py expand_tool_names); the spec JSON
|
|
484
|
-
/// arrives pre-expanded and the alias table never crosses the language boundary.
|
|
485
|
-
fn matches_names(actual: &str, names: &HashSet<String>) -> bool {
|
|
486
|
-
names.contains(actual)
|
|
487
|
-
|| actual
|
|
488
|
-
.strip_prefix("mcp__")
|
|
489
|
-
.and_then(|rest| rest.split_once("__"))
|
|
490
|
-
.is_some_and(|(_, tool)| names.contains(tool))
|
|
491
|
-
}
|
|
492
|
-
|
|
493
483
|
// ── text-shape helpers (mining/signals.py) ───────────────────────────────────
|
|
494
484
|
|
|
495
485
|
/// marker_in (mining/signals.py marker_in): whether any tool-result block's
|
|
@@ -1316,6 +1306,10 @@ mod tests {
|
|
|
1316
1306
|
assert!(matches_names("mcp__conductor__ExitPlanMode", &names));
|
|
1317
1307
|
assert!(!matches_names("mcp__ExitPlanMode", &names));
|
|
1318
1308
|
assert!(!matches_names("AskUserQuestion", &names));
|
|
1309
|
+
|
|
1310
|
+
let edit_names: HashSet<String> = ["Edit".to_string(), "ccx_code_edit".to_string()].into();
|
|
1311
|
+
assert!(matches_names("mcp__cc-context__ccx_code_edit", &edit_names));
|
|
1312
|
+
assert!(!matches_names("mcp__cc-context__ccx_code_read", &edit_names));
|
|
1319
1313
|
}
|
|
1320
1314
|
|
|
1321
1315
|
#[test]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use std::collections::HashMap;
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
2
|
|
|
3
3
|
use chrono::{DateTime, FixedOffset};
|
|
4
4
|
use sonic_rs::Value;
|
|
@@ -225,6 +225,18 @@ pub fn tool_use_index(entries: &[Entry]) -> HashMap<&str, &ToolUseBlock> {
|
|
|
225
225
|
.collect()
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/// matches_names (tools.py matches_names): exact membership, or the ``<tool>``
|
|
229
|
+
/// segment of an ``mcp__<server>__<tool>`` name split on the first two ``__``.
|
|
230
|
+
/// Alias closure happens Python-side (tools.py expand_tool_names); spec JSON
|
|
231
|
+
/// arrives pre-expanded and the alias table never crosses the language boundary.
|
|
232
|
+
pub(crate) fn matches_names(actual: &str, names: &HashSet<String>) -> bool {
|
|
233
|
+
names.contains(actual)
|
|
234
|
+
|| actual
|
|
235
|
+
.strip_prefix("mcp__")
|
|
236
|
+
.and_then(|rest| rest.split_once("__"))
|
|
237
|
+
.is_some_and(|(_, tool)| names.contains(tool))
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
/// The space-joined text of the ``Text`` blocks — the ``text`` field of the
|
|
229
241
|
/// Python ``AssistantEvent``/``PrintMessage``.
|
|
230
242
|
pub fn joined_text(blocks: &[ContentBlock]) -> String {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|