mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-09 01:49:49 -10:00
feat(tools): make load_skill eager so the prompt's own instruction is callable
The pinned `## Skills` index told the model to call `load_skill`, and
`load_skill` was deferred behind `tool_search` — so the instruction named a
tool that was not in the array printed beside it. Using one skill cost a
discovery hop, a `name="list"` round trip, and a `change:tool_surface` re-pin
of the entire cached prefix. That is the "models are reluctant to use skills"
report: not reluctance, an uncallable instruction.
`load_skill` joins DEFAULT_ACTIVE_NATIVE_TOOLS for the parent and, through the
same catalog authority, for children. Measured cost: the parent surface goes
88,398B -> 88,642B, +244B, and the ceiling is re-measured in this commit as
that guard asks. The product already pins a 6,400-10,000 token skills index
into every session; withholding the 244 bytes that act on it was the
imbalance.
`query` searches names and descriptions over the same routing metadata the
ambient index carries, so a truncated index no longer forces pulling the whole
catalogue to find one skill.
Wording, which is the larger half:
- Usage line now says to call `load_skill` with the matching name *before
starting the work*, and demotes `name="list"` to the truncated-index case
rather than making it the first step.
- The tool description drops "into the next turn's context" (reads as deferred
payoff) and the comparison against `File action="read"`, a compatibility
name that per docs/TOOL_SURFACE.md never reaches a model catalog — it
justified itself against tools the model cannot see. It gains a trigger
clause, matching the convention skills' own descriptions must follow.
Contracts moved with the code rather than around it: the default-active
contract, the plan request head, the hook classifier table (recording that it
keeps `other` — making it eager must not silently re-gate it), both child wire
catalogs, and the two small-surface expectations.
Also fixes the Windows build: `sandbox_visible_tempdir`/`_fixture_root` from
4e27b4ee1 have only `#[cfg(unix)]` callers, so on Windows they were dead code
and CI builds tests with `-Dwarnings`. That is the `Test (windows-latest)`
failure on the current tip.
Evidence (macOS aarch64, hermetic home, nextest --profile ci):
- `codewhale-tui --lib`: 13059 run, 13058 passed, 1 failed, 19 skipped. The
one failure is `run_subagent_task_claims_before_delivery_and_then_finalizes`,
a 1s timing assertion this machine misses at 4.2s; it passes in CI on macOS
and Windows and is unrelated to this change.
- `cargo fmt --all -- --check` exit 0.
- `npx vitest run` (web): 51 files, 471 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
de4287a60a
commit
91b5898a2b
17 files changed
+127
-34
No files matched your search
@@ -61,6 +61,16 @@ tag, packages, checksums and release assets exist.
|
||||
than a shared one (#6247).
|
||||
|
||||
### Added
|
||||
- Skills are reachable in one call. The pinned `## Skills` index told the model
|
||||
to call `load_skill`, but the tool was deferred behind `tool_search`, so it
|
||||
was never in the tool array that instruction was printed beside: using a
|
||||
skill cost a discovery hop, a `name="list"` round trip, and a
|
||||
`change:tool_surface` re-pin of the whole cached prefix. `load_skill` is now
|
||||
eager for the parent and for children — +244B of pinned catalog, measured,
|
||||
against a re-prefill avoided every time a skill is used — and it takes a
|
||||
`query` that searches names and descriptions, so a truncated index no longer
|
||||
forces pulling the whole catalogue to find one skill. The usage line now says
|
||||
to load the matching skill by name before starting the work.
|
||||
- A fast lane the router cannot serve now says so. `provider_router_candidates`
|
||||
answers `cheap: None` for any pair its tables do not know, and a
|
||||
`Faster`/`Auto` child on such a pair used to run at the parent's model and
|
||||
|
||||
@@ -61,6 +61,16 @@ tag, packages, checksums and release assets exist.
|
||||
than a shared one (#6247).
|
||||
|
||||
### Added
|
||||
- Skills are reachable in one call. The pinned `## Skills` index told the model
|
||||
to call `load_skill`, but the tool was deferred behind `tool_search`, so it
|
||||
was never in the tool array that instruction was printed beside: using a
|
||||
skill cost a discovery hop, a `name="list"` round trip, and a
|
||||
`change:tool_surface` re-pin of the whole cached prefix. `load_skill` is now
|
||||
eager for the parent and for children — +244B of pinned catalog, measured,
|
||||
against a re-prefill avoided every time a skill is used — and it takes a
|
||||
`query` that searches names and descriptions, so a truncated index no longer
|
||||
forces pulling the whole catalogue to find one skill. The usage line now says
|
||||
to load the matching skill by name before starting the work.
|
||||
- A fast lane the router cannot serve now says so. `provider_router_candidates`
|
||||
answers `cheap: None` for any pair its tables do not know, and a
|
||||
`Faster`/`Auto` child on such a pair used to run at the parent's model and
|
||||
|
||||
@@ -2063,8 +2063,9 @@ mod tests {
|
||||
// The real child catalog fixture (not a hand-built tool list) must
|
||||
// survive Messages serialization with exactly one canonical `read`
|
||||
// entry — no dedup, filter, or sanitizer may drop or duplicate it.
|
||||
// Skills are discoverable through tool_search, so the child wire
|
||||
// catalog carries no load_skill at all.
|
||||
// `load_skill` is eager in DEFAULT_ACTIVE_NATIVE_TOOLS, and children
|
||||
// resolve the same catalog authority the parent does, so it appears
|
||||
// here exactly once like any other default tool.
|
||||
let tools = crate::tools::subagent::kimi_general_child_request_tools_fixture();
|
||||
assert_eq!(
|
||||
tools.iter().filter(|tool| tool.name == "read").count(),
|
||||
@@ -2076,8 +2077,8 @@ mod tests {
|
||||
.iter()
|
||||
.filter(|tool| tool.name == "load_skill")
|
||||
.count(),
|
||||
0,
|
||||
"load_skill is not part of the child wire catalog"
|
||||
1,
|
||||
"child wire catalog carries one canonical load_skill"
|
||||
);
|
||||
let client = test_client();
|
||||
let mut request = request_with("claude-sonnet-4-6", None, None, None);
|
||||
@@ -2100,9 +2101,13 @@ mod tests {
|
||||
"read keeps a valid object schema: {}",
|
||||
reads[0]
|
||||
);
|
||||
assert!(
|
||||
serialized.iter().all(|tool| tool["name"] != "load_skill"),
|
||||
"load_skill must not appear on the child Messages wire"
|
||||
assert_eq!(
|
||||
serialized
|
||||
.iter()
|
||||
.filter(|tool| tool["name"] == "load_skill")
|
||||
.count(),
|
||||
1,
|
||||
"exactly one canonical load_skill definition reaches the Messages wire"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -388,8 +388,8 @@ async fn responses_stream_fails_fast_on_non_retryable_provider_error() {
|
||||
fn responses_body_serializes_the_child_catalog_without_duplication() {
|
||||
// Mirror of the Anthropic contract: the real child catalog fixture
|
||||
// maps 1:1 into Responses function tools with one canonical `read` entry.
|
||||
// Skills are discoverable through tool_search, so the child wire catalog
|
||||
// carries no load_skill at all.
|
||||
// `load_skill` is eager in DEFAULT_ACTIVE_NATIVE_TOOLS and children resolve
|
||||
// the same catalog authority, so it maps through exactly once too.
|
||||
let tools = crate::tools::subagent::kimi_general_child_request_tools_fixture();
|
||||
let mut request = minimal_responses_request();
|
||||
request.tools = Some(tools);
|
||||
@@ -411,9 +411,13 @@ fn responses_body_serializes_the_child_catalog_without_duplication() {
|
||||
"read keeps a valid parameters schema: {}",
|
||||
reads[0]
|
||||
);
|
||||
assert!(
|
||||
serialized.iter().all(|tool| tool["name"] != "load_skill"),
|
||||
"load_skill must not appear on the child Responses wire"
|
||||
assert_eq!(
|
||||
serialized
|
||||
.iter()
|
||||
.filter(|tool| tool["name"] == "load_skill")
|
||||
.count(),
|
||||
1,
|
||||
"exactly one canonical load_skill definition reaches the Responses wire"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10733,7 +10733,7 @@ fn core_primitives_and_todo_write_default_to_eager() {
|
||||
|
||||
#[test]
|
||||
fn default_active_contract_keeps_discovery_and_core_tools_eager() {
|
||||
const EXPECTED_NATIVE: [&str; 10] = [
|
||||
const EXPECTED_NATIVE: [&str; 11] = [
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
@@ -10744,6 +10744,7 @@ fn default_active_contract_keeps_discovery_and_core_tools_eager() {
|
||||
"create_goal",
|
||||
"get_goal",
|
||||
"update_goal",
|
||||
"load_skill",
|
||||
];
|
||||
assert_eq!(
|
||||
default_active_native_tool_names(),
|
||||
@@ -10783,7 +10784,15 @@ fn default_active_contract_keeps_discovery_and_core_tools_eager() {
|
||||
#[test]
|
||||
fn non_yolo_mode_retains_default_defer_policy() {
|
||||
let always_load = HashSet::new();
|
||||
for core in ["read", "write", "edit", "bash", "agent", "todo_write"] {
|
||||
for core in [
|
||||
"read",
|
||||
"write",
|
||||
"edit",
|
||||
"bash",
|
||||
"agent",
|
||||
"todo_write",
|
||||
"load_skill",
|
||||
] {
|
||||
assert!(!should_default_defer_tool(core, &always_load));
|
||||
}
|
||||
for searchable in [
|
||||
@@ -10791,7 +10800,6 @@ fn non_yolo_mode_retains_default_defer_policy() {
|
||||
"File",
|
||||
"Git",
|
||||
"Run",
|
||||
"load_skill",
|
||||
"remember",
|
||||
REQUEST_USER_INPUT_NAME,
|
||||
"read_file",
|
||||
@@ -11284,6 +11292,7 @@ async fn runtime_contract_tool_metric_uses_canonical_mode_surfaces() {
|
||||
"tool_search",
|
||||
"workflow",
|
||||
"write",
|
||||
"load_skill",
|
||||
]);
|
||||
|
||||
for mode in ["plan", "act", "operate"] {
|
||||
|
||||
@@ -54,6 +54,12 @@ pub(crate) const DEFAULT_ACTIVE_NATIVE_TOOLS: &[&str] = &[
|
||||
// Continuation instructions require these controls. Hiding them behind
|
||||
// discovery leaves a model unable to stop the work it was asked to run.
|
||||
"create_goal", "get_goal", "update_goal",
|
||||
// The pinned `## Skills` index tells the model to call `load_skill`, so
|
||||
// the tool has to be on the wire for that instruction to be true. Behind
|
||||
// `tool_search` it cost a discovery hop plus a `change:tool_surface`
|
||||
// re-pin every time a skill was used, against ~134 pinned bytes to have
|
||||
// it eager beside the index the prefix already carries.
|
||||
"load_skill",
|
||||
];
|
||||
|
||||
const CORE_ACTION_TOOL_FALLBACKS: &[CoreActionToolFallback] = &[
|
||||
|
||||
@@ -82,7 +82,8 @@ fn first_turn_surface_is_stable_across_plan_work_and_operate() {
|
||||
"todo_write",
|
||||
"create_goal",
|
||||
"get_goal",
|
||||
"update_goal"
|
||||
"update_goal",
|
||||
"load_skill"
|
||||
]
|
||||
);
|
||||
let expected = [
|
||||
@@ -92,6 +93,7 @@ fn first_turn_surface_is_stable_across_plan_work_and_operate() {
|
||||
"get_goal",
|
||||
"update_goal",
|
||||
"edit",
|
||||
"load_skill",
|
||||
"read",
|
||||
"todo_write",
|
||||
"tool_search",
|
||||
|
||||
@@ -5882,6 +5882,10 @@ command = "echo project"
|
||||
("create_goal", "other"),
|
||||
("get_goal", "other"),
|
||||
("update_goal", "other"),
|
||||
// Reads the skill registry, not caller-named paths, so it keeps
|
||||
// the classification it already had as a deferred tool. Making it
|
||||
// eager must not silently re-gate it.
|
||||
("load_skill", "other"),
|
||||
];
|
||||
for name in crate::core::engine::tool_catalog::DEFAULT_ACTIVE_NATIVE_TOOLS {
|
||||
let expected = EXPECTED.iter().find(|(n, _)| n == name).map(|(_, c)| *c);
|
||||
|
||||
@@ -1642,7 +1642,8 @@ fn render_skills_block_with_configured_root(
|
||||
Skills are optional instruction packs. This index exposes routing metadata; bodies stay unloaded.\n\n\
|
||||
### Available skills\n";
|
||||
const USAGE: &str = "\n### Usage\n\
|
||||
- When the user names a skill or one may help, call `load_skill` with `name=\"list\"`; load the exact skill before use.\n\
|
||||
- When the user names a skill, or an entry above matches the task, call `load_skill` with that exact name before starting the work.\n\
|
||||
- The index above is the catalogue; use `query` to search it, or `name=\"list\"`, only when no entry matches or the index was truncated.\n\
|
||||
- Do not carry a skill across turns unless re-mentioned. Skill instructions do not expand tool, approval, or trust authority.\n\
|
||||
- If a named skill is unavailable, say so and continue. Do not execute untrusted skill scripts unless the user asks.\n";
|
||||
const WARNING_HEADING: &str = "\n### Skill load warnings\n";
|
||||
|
||||
@@ -128,7 +128,7 @@ fn render_available_skills_context_lists_paths_and_usage() {
|
||||
|
||||
assert!(rendered.contains("## Skills"));
|
||||
assert!(rendered.contains("- test-skill: A test skill"));
|
||||
assert!(rendered.contains("load the exact skill before use"));
|
||||
assert!(rendered.contains("call `load_skill` with that exact name before starting the work"));
|
||||
assert!(rendered.contains("do not expand tool, approval, or trust authority"));
|
||||
assert!(
|
||||
rendered.contains(&expected_path),
|
||||
|
||||
@@ -96,6 +96,9 @@ thread_local! {
|
||||
/// from inside the sandbox, and it says nothing about `/dev` or `/proc`, which
|
||||
/// bwrap also replaces. Use it for the sandbox probes; plain `tempfile::tempdir`
|
||||
/// stays correct everywhere else.
|
||||
// Every caller is `#[cfg(unix)]` (the bwrap probes); on Windows these would
|
||||
// be dead code and CI builds tests with `-Dwarnings`.
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn sandbox_visible_tempdir() -> tempfile::TempDir {
|
||||
let root = sandbox_visible_fixture_root();
|
||||
tempfile::tempdir_in(root).unwrap_or_else(|error| {
|
||||
@@ -111,6 +114,7 @@ pub(crate) fn sandbox_visible_tempdir() -> tempfile::TempDir {
|
||||
/// already owns and that is outside `/tmp` in every normal layout. A target
|
||||
/// directory deliberately placed under `/tmp` would silently reintroduce
|
||||
/// #6305, so say so instead of handing back a shadowed path.
|
||||
#[cfg(unix)]
|
||||
fn sandbox_visible_fixture_root() -> &'static Path {
|
||||
static ROOT: OnceLock<PathBuf> = OnceLock::new();
|
||||
ROOT.get_or_init(|| {
|
||||
|
||||
@@ -36,10 +36,11 @@ impl ToolSpec for LoadSkillTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Load a skill (SKILL.md body + companion file list) into the next turn's context. \
|
||||
Use name=\"list\" to discover the complete enabled catalogue, then load an exact \
|
||||
skill when the user names it or the task clearly matches its description. Faster \
|
||||
than File action=\"read\" plus File action=\"list\"."
|
||||
"Load a named skill's SKILL.md body and companion file list into this turn. Use when \
|
||||
the user names a skill, or when an entry in the system prompt's `## Skills` index \
|
||||
matches the task -- load it before starting the work, not after. Pass query=\"...\" to \
|
||||
search names and descriptions, or name=\"list\" for the whole catalogue. Resolves \
|
||||
global and plugin skills that `read` cannot reach."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
@@ -49,6 +50,10 @@ impl ToolSpec for LoadSkillTool {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Skill id to load. Omit or pass \"list\" to see all available skills."
|
||||
},
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search term matched against skill names and descriptions. Use when the index was truncated or no name is known."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -100,12 +105,37 @@ impl ToolSpec for LoadSkillTool {
|
||||
.into_enabled();
|
||||
|
||||
// Listing mode: empty name, "*", or "list" returns the full registry (#4651).
|
||||
if name.is_empty() || name == "*" || name == "list" {
|
||||
let skills = registry.list();
|
||||
// A `query` filters that listing over the same routing metadata the
|
||||
// ambient index carries, so a truncated index does not force the model
|
||||
// to pull every skill to find one.
|
||||
let query = input
|
||||
.get("query")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_lowercase();
|
||||
if !query.is_empty() || name.is_empty() || name == "*" || name == "list" {
|
||||
let all = registry.list();
|
||||
let skills: Vec<&_> = all
|
||||
.iter()
|
||||
.filter(|skill| {
|
||||
query.is_empty()
|
||||
|| skill.name.to_lowercase().contains(&query)
|
||||
|| skill.description.to_lowercase().contains(&query)
|
||||
})
|
||||
.collect();
|
||||
if skills.is_empty() {
|
||||
return Ok(ToolResult::success("No skills installed."));
|
||||
return Ok(ToolResult::success(if query.is_empty() {
|
||||
"No skills installed.".to_string()
|
||||
} else {
|
||||
format!("No skill matches {query:?}. Pass name=\"list\" for the catalogue.")
|
||||
}));
|
||||
}
|
||||
let mut listing = format!("Available skills ({}):\n", skills.len());
|
||||
let mut listing = if query.is_empty() {
|
||||
format!("Available skills ({}):\n", skills.len())
|
||||
} else {
|
||||
format!("Skills matching {:?} ({}):\n", query, skills.len())
|
||||
};
|
||||
for skill in skills {
|
||||
if skill.description.trim().is_empty() {
|
||||
listing.push_str(&format!(" - {}\n", skill.name));
|
||||
|
||||
@@ -7912,6 +7912,7 @@ fn small_surface_starts_with_core_tools_and_read_only_goal_control() {
|
||||
"bash",
|
||||
"edit",
|
||||
"get_goal",
|
||||
"load_skill",
|
||||
"read",
|
||||
"todo_write",
|
||||
"tool_search",
|
||||
@@ -8190,6 +8191,7 @@ fn small_surface_depth_cap_removes_only_agent() {
|
||||
"bash",
|
||||
"edit",
|
||||
"get_goal",
|
||||
"load_skill",
|
||||
"read",
|
||||
"todo_write",
|
||||
"tool_search",
|
||||
@@ -20519,7 +20521,10 @@ const READ_ONLY_CHILD_ENVELOPE_BYTE_CEILING: usize = 89_000;
|
||||
/// margin. Re-measured at 87,529B on 2026-09-17, with the bounded Git
|
||||
/// fetch / merge_tree verify tools (b89349286f) and this slice's grant
|
||||
/// text both in the shared catalog.
|
||||
const PARENT_SURFACE_BYTE_CEILING: usize = 88_398;
|
||||
// Re-measured when `load_skill` joined the eager catalog: +244B for its
|
||||
// name, description and schema, against the `## Skills` index the prefix
|
||||
// already carries and a `change:tool_surface` re-pin per skill use avoided.
|
||||
const PARENT_SURFACE_BYTE_CEILING: usize = 88_642;
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_only_child_envelope_stays_within_measured_ceiling() {
|
||||
|
||||
@@ -17,7 +17,7 @@ Implementation sources:
|
||||
|
||||
## Default-active contract
|
||||
|
||||
New turns start with ten eager native names plus synthetic `tool_search`:
|
||||
New turns start with eleven eager native names plus synthetic `tool_search`:
|
||||
|
||||
1. `read`
|
||||
2. `write`
|
||||
@@ -29,9 +29,10 @@ New turns start with ten eager native names plus synthetic `tool_search`:
|
||||
8. `create_goal`
|
||||
9. `get_goal`
|
||||
10. `update_goal`
|
||||
11. `tool_search` (synthetic, always active)
|
||||
11. `load_skill`
|
||||
12. `tool_search` (synthetic, always active)
|
||||
|
||||
The ten native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in
|
||||
The eleven native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in
|
||||
`crates/tui/src/core/engine/tool_catalog.rs`, pinned by
|
||||
`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority
|
||||
boundary may remove `agent` at the maximum child depth, but route size alone
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"todo_write",
|
||||
"create_goal",
|
||||
"get_goal",
|
||||
"update_goal"
|
||||
"update_goal",
|
||||
"load_skill"
|
||||
],
|
||||
"schemas": {
|
||||
"read": [
|
||||
|
||||
@@ -63,6 +63,7 @@ export const CHANGELOG: ChangelogRelease[] = [
|
||||
{
|
||||
"heading": "Added",
|
||||
"items": [
|
||||
"Skills are reachable in one call. The pinned ## Skills index told the model to call load_skill, but the tool was deferred behind tool_search, so it was never in the tool array that instruction was printed beside: using a skill cost a discovery hop, a name=\"list\" round trip, and a change:tool_surface re-pin of the whole cached prefix. load_skill is now eager for the parent and for children — +244B of pinned catalog, measured, against a re-prefill avoided every time a skill is…",
|
||||
"A fast lane the router cannot serve now says so. provider_router_candidates answers cheap: None for any pair its tables do not know, and a Faster/Auto child on such a pair used to run at the parent's model and price with no receipt and no way to tell \"single tier by design\" from misconfiguration. The fallback stays — the router must not invent a model — but the spawn receipt now carries a fallback_note naming the unserved lane, the same field the pinned-provider fallback…",
|
||||
"The runtime API tells a replayed submission apart from a new admission: POST /v1/threads/{id}/turns answers 200 with idempotent_replay: true when the operation key was already used, and 201 for a fresh admission, so a client that retries after a dropped response can no longer create a second turn. The flag is absent on a fresh admission, so responses existing clients already parse are unchanged (#76).",
|
||||
"Event streams resume where they left off: journal frames carry their durable seq as the SSE event id:, Last-Event-ID is honoured as the cursor when no explicit since_seq is asked for, and RuntimeCapabilities advertises event_stream_resume so a client can gate its reconnect controls on the capability instead of discovering it from a missing id (#76).",
|
||||
@@ -73,10 +74,9 @@ export const CHANGELOG: ChangelogRelease[] = [
|
||||
"Rust files that were already rustfmt-clean are re-normalized after an edit, so the next patch's anchors still match. Hand-formatted files are never rewritten, and every failure path skips and lets the edit land (#6205, #6151).",
|
||||
"Native clients can finish provider setup without dropping to the CLI: DELETE /v1/providers/{id}/key clears a Codewhale-owned credential through the same shared owner as codewhale auth clear, and GET /v1/providers now carries credentialSource / credentialWritable (plus a reason) so a client disables its control with a truthful explanation instead of letting a write fail late. A credential Codewhale does not own — a literal key in a config file, or an active external consent —…",
|
||||
"The interactive approval card can be bounded: [approval] timeout_seconds resolves an unanswered card to deny when the window elapses — the same fail-closed decision the external approval path takes — and the transcript says the bound denied the call, not the operator. Omitted or 0 keeps today's unbounded wait, so nothing changes unless you opt in (#6101).",
|
||||
"Transcript drag selection copies Markdown source by default: every cell the selection touches serializes through the same canonical path Ctrl-Y and /copy use, partial intersections round out to whole cells joined with blank lines, and the toast names the copied cell count. tui.selection_copy_markdown = false keeps the rendered-text payload (#6156).",
|
||||
"The Runtime API serves the workspace files a native client browses and edits: GET /v1/workspace/files lists one directory, GET /v1/workspace/files/read returns a bounded byte window with a whole-file SHA-256 revision, and PUT /v1/workspace/files writes atomically through the confined opener with revision-checked overwrites (409 on drift). .git is never served and symlinks are never followed. A saved session's oversized tool outputs are served as artifacts at GET…"
|
||||
"Transcript drag selection copies Markdown source by default: every cell the selection touches serializes through the same canonical path Ctrl-Y and /copy use, partial intersections round out to whole cells joined with blank lines, and the toast names the copied cell count. tui.selection_copy_markdown = false keeps the rendered-text payload (#6156)."
|
||||
],
|
||||
"itemCount": 25
|
||||
"itemCount": 26
|
||||
},
|
||||
{
|
||||
"heading": "Changed",
|
||||
|
||||
@@ -14,6 +14,7 @@ export const TOOLS_COPY = {
|
||||
{ name: "agent · workflow", detail: { en: "Delegate work and coordinate dependent phases.", zh: "委派工作并协调有依赖关系的阶段。" } },
|
||||
{ name: "todo_write", detail: { en: "Replace the task list with content and status entries.", zh: "用包含内容与状态的条目替换任务列表。" } },
|
||||
{ name: "create_goal · get_goal · update_goal", detail: { en: "Create, inspect and update the active goal.", zh: "创建、查看与更新当前目标。" } },
|
||||
{ name: "load_skill", detail: { en: "Load a skill by exact name, or pass query to search the catalogue.", zh: "按确切名称加载技能,或传入 query 搜索技能目录。" } },
|
||||
{ name: "tool_search", detail: { en: "Discover policy-allowed tools. Each child has its own search and activation cache.", zh: "发现策略允许的工具。每个子 Agent 拥有独立的搜索与激活缓存。" } },
|
||||
{ name: "Git · Run · tasks · remember · Web · MCP · plugins", detail: { en: "Deferred: loaded by tool_search only when policy permits.", zh: "延迟加载:仅在策略允许时由 tool_search 加载。" } },
|
||||
{ name: "8 names / 16 KiB", detail: { en: "Conversation toolbox cache; independent per child and revalidated every turn.", zh: "会话工具箱缓存;每个子 Agent 独立,每轮重新校验。" } },
|
||||
|
||||
Reference in new issue
Block a user