Skip to content

Commit 3ed59d6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/gifted-hawking-03sdhe
# Conflicts: # aai_cli/AGENTS.md # tests/__snapshots__/test_snapshots_help_run.ambr
2 parents 5bc1907 + 371055d commit 3ed59d6

12 files changed

Lines changed: 91 additions & 61 deletions

File tree

REFERENCE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ missing `npx`/`uvx`, an offline host) drops only its own tools, so a single brok
160160
tool never sinks the session. MCP tools are a live-run feature and are not
161161
reflected in `--show-code` output.
162162

163-
`--files` lets the agent read, write, and run code in the directory you launch
164-
it from (off by default). Reads run immediately; a write, edit, or command run pauses
163+
The agent reads, writes, and runs code in the directory you launch it from (on by
164+
default; pass `--no-files` to disable). Reads run immediately; a write, edit, or command run pauses
165165
the turn for confirmation in the voice TUI — press `y`/`n` (`a` approves the rest of the
166166
session) or just say it ("approve" / "run it" / "go ahead"; anything unclear is treated as
167167
a no). Destructive commands (e.g. `rm -rf`, `sudo`) ignore the spoken answer and require a

aai_cli/AGENTS.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

aai_cli/agent_cascade/brain.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
(:func:`build_graph`) and driven turn-by-turn with the running history the
99
cascade already keeps (:func:`build_streamer`); tools are read-only and auto-approved,
1010
because a spoken turn can't pause for a keyboard confirmation, and the system prompt
11-
keeps every reply short and speakable.
11+
keeps every reply short and speakable. Context-window management is deepagents' job (its built-in
12+
``SummarizationMiddleware``), so the engine feeds the full untrimmed history each turn.
1213
1314
The graph is the only network seam: :func:`build_streamer` accepts an injected graph,
1415
so the per-turn streaming reply leg is unit-tested against a fake with no sockets — the

aai_cli/agent_cascade/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
"engine, so write plain spoken prose — no markdown, emoji, bullet lists, or code."
2828
)
2929
DEFAULT_GREETING = "Hi! I'm your AssemblyAI voice agent. What can I help you with?"
30-
# Sliding-window size: keep the last N messages of conversation as LLM context.
30+
# Sliding-window size for the standalone, hand-rolled cascade: keep the last N messages of
31+
# conversation as LLM context. Used by the `--show-code` generator and the `assembly init`
32+
# template, which talk to the gateway directly. The live `assembly live` brain does NOT window
33+
# client-side — it delegates context management to the deepagents `SummarizationMiddleware`
34+
# (summarize old turns, offload to a file), so this knob is inert on that path. See brain.py.
3135
DEFAULT_MAX_HISTORY = 40
3236
# Per-turn cap on how many tool calls the deepagents brain may make before it must answer.
3337
# Enforced by a ToolCallLimitMiddleware with exit_behavior="continue": once the budget is hit,

aai_cli/agent_cascade/engine.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
timeout_error as _timeout_error,
6262
)
6363
from aai_cli.agent_cascade.config import CascadeConfig
64-
from aai_cli.agent_cascade.text import pop_clauses, trim_history
64+
from aai_cli.agent_cascade.text import pop_clauses
6565
from aai_cli.core.errors import CLIError
6666
from aai_cli.ui import output
6767

@@ -150,7 +150,6 @@ def on_turn(self, event: object) -> None:
150150
self.renderer.user_final(text)
151151
self._barge_in()
152152
self.history.append({"role": "user", "content": text})
153-
trim_history(self.history, self.config.max_history)
154153
self._start_reply()
155154
else:
156155
self.renderer.user_partial(text)
@@ -419,11 +418,16 @@ def _feed(self, pcm: bytes) -> None:
419418
self.player.enqueue(pcm)
420419

421420
def _record_spoken(self, spoken: list[str]) -> None:
422-
"""Append what was actually spoken to the history (kept alternating after a barge-in)."""
421+
"""Append what was actually spoken to the history (kept alternating after a barge-in).
422+
423+
The transcript is no longer windowed client-side: the deepagents brain's built-in
424+
``SummarizationMiddleware`` does the context-window management (summarizing old turns,
425+
offloading the evicted history to a file), so the engine keeps the full running history
426+
and lets the graph compact it per turn — see :mod:`aai_cli.agent_cascade.brain`.
427+
"""
423428
spoken_text = " ".join(spoken).strip()
424429
if spoken_text:
425430
self.history.append({"role": "assistant", "content": spoken_text})
426-
trim_history(self.history, self.config.max_history)
427431

428432
def _surface_error(self, exc: CLIError, *, started: bool) -> None:
429433
"""Record a reply-leg failure (LLM/timeout). Before any audio, the error is also shown

aai_cli/agent_cascade/text.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
"""Pure text helpers for the cascade: sentence splitting and history trimming.
1+
"""Pure text helpers for the cascade: sentence and clause splitting.
22
33
Kept Rich-free and dependency-light so the orchestration logic in ``engine`` can
4-
be unit-tested without any I/O.
4+
be unit-tested without any I/O. (Conversation-history trimming used to live here as a
5+
client-side sliding window; the live brain now delegates context-window management to
6+
the deepagents ``SummarizationMiddleware`` instead — see :mod:`aai_cli.agent_cascade.brain`.)
57
"""
68

79
from __future__ import annotations
@@ -27,6 +29,17 @@ def _is_boundary(text: str, index: int) -> bool:
2729
return index + 1 < len(text) and text[index + 1].isspace()
2830

2931

32+
def _ends_sentence(text: str, index: int, char: str) -> bool:
33+
"""True when ``char`` at ``index`` closes a sentence: a terminator at end-of-text or
34+
followed by whitespace.
35+
36+
Unlike :func:`_is_boundary` (used for partial streamed chunks), end-of-text *does* close a
37+
sentence here: :func:`split_sentences` runs on a complete reply, so a trailing terminator is a
38+
real boundary, not a possibly-mid-token one.
39+
"""
40+
return char in _TERMINATORS and (index + 1 == len(text) or text[index + 1].isspace())
41+
42+
3043
def pop_clauses(buffer: str, *, min_chars: int) -> tuple[list[str], str]:
3144
"""Pull complete speakable clauses off the front of ``buffer`` for incremental TTS.
3245
@@ -68,7 +81,7 @@ def split_sentences(text: str) -> list[str]:
6881
sentences: list[str] = []
6982
start = 0
7083
for index, char in enumerate(text):
71-
if char in _TERMINATORS and (index + 1 == len(text) or text[index + 1].isspace()):
84+
if _ends_sentence(text, index, char):
7285
# Boundary confirmed (end-of-text or a following space); the slice includes
7386
# the terminator, so it is never blank after stripping leading whitespace.
7487
sentences.append(text[start : index + 1].strip())
@@ -77,13 +90,3 @@ def split_sentences(text: str) -> list[str]:
7790
if tail:
7891
sentences.append(tail)
7992
return sentences
80-
81-
82-
def trim_history[T](history: list[T], max_messages: int) -> None:
83-
"""Cap ``history`` to its most recent ``max_messages`` entries, in place.
84-
85-
A sliding window over the conversation so an unbounded chat doesn't grow the
86-
context (and the per-turn token cost) without limit.
87-
"""
88-
if len(history) > max_messages:
89-
del history[: len(history) - max_messages]

aai_cli/commands/agent_cascade/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def _emit_voice_list(_state: AppState, json_mode: bool) -> None:
6262
"assembly --sandbox live --mcp-config ~/.config/mcp/servers.json",
6363
),
6464
(
65-
"Let the agent read and write files in the current directory",
66-
"assembly --sandbox live --files",
65+
"Run a conversation without filesystem access",
66+
"assembly --sandbox live --no-files",
6767
),
6868
("See available voices", "assembly --sandbox live --list-voices"),
6969
(
@@ -172,9 +172,9 @@ def live(
172172
rich_help_panel=_PANEL_TOOLS,
173173
),
174174
files: bool = typer.Option(
175-
False,
176-
"--files",
177-
help="Let the agent read, write, and run code in the current directory, sandboxed (writes and runs need confirmation)",
175+
True,
176+
"--files/--no-files",
177+
help="Let the agent read, write, and run code in the current directory, sandboxed (writes and runs need confirmation). Use --no-files to disable",
178178
rich_help_panel=_PANEL_TOOLS,
179179
),
180180
auto_write: list[str] | None = typer.Option(

tests/__snapshots__/test_snapshots_help_run.ambr

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -630,14 +630,17 @@
630630
│ streaming fields │
631631
╰──────────────────────────────────────────────────────────────────────────────╯
632632
╭─ Tools ──────────────────────────────────────────────────────────────────────╮
633-
│ --mcp-config FILE MCP servers config JSON ({"mcpServers": {…}}) to │
634-
│ add (repeatable; none load by default) │
635-
│ --files Let the agent read, write, and run code in the │
636-
│ current directory, sandboxed (writes and runs need │
637-
│ confirmation) │
638-
│ --auto-write TEXT Auto-approve --files writes under this │
639-
│ subdirectory, skipping the confirmation │
640-
│ (repeatable) │
633+
│ --mcp-config FILE MCP servers config JSON ({"mcpServers": │
634+
│ {…}}) to add (repeatable; none load by │
635+
│ default) │
636+
│ --files --no-files Let the agent read, write, and run code │
637+
│ in the current directory, sandboxed │
638+
│ (writes and runs need confirmation). Use │
639+
│ --no-files to disable │
640+
│ [default: files] │
641+
│ --auto-write TEXT Auto-approve --files writes under this │
642+
│ subdirectory, skipping the confirmation │
643+
│ (repeatable) │
641644
╰──────────────────────────────────────────────────────────────────────────────╯
642645

643646
Examples
@@ -649,8 +652,8 @@
649652
$ assembly --sandbox live --system-prompt "You are a terse pirate."
650653
Add your own MCP servers (none load by default)
651654
$ assembly --sandbox live --mcp-config ~/.config/mcp/servers.json
652-
Let the agent read and write files in the current directory
653-
$ assembly --sandbox live --files
655+
Run a conversation without filesystem access
656+
$ assembly --sandbox live --no-files
654657
See available voices
655658
$ assembly --sandbox live --list-voices
656659
Print equivalent Python instead of running

tests/test_agent_cascade_config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def test_default_config_values():
3232
"without preamble or filler. Your reply is read aloud by a text-to-speech "
3333
"engine, so write plain spoken prose — no markdown, emoji, bullet lists, or code."
3434
)
35-
# The sliding-window default keeps the last 40 messages of context.
35+
# The standalone (--show-code / init template) sliding-window default; the live brain
36+
# delegates context management to the deepagents SummarizationMiddleware instead.
3637
assert config.max_history == 40
3738
assert DEFAULT_MAX_HISTORY == 40
3839
# Formatting is on by default, so the reply trigger waits for the formatted turn.

tests/test_agent_cascade_files.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import types
1212

1313
import pytest
14+
from typer.testing import CliRunner
1415

1516
from aai_cli.agent_cascade import brain, engine
1617
from aai_cli.agent_cascade.brain import ApprovalPause, SpeechDelta
@@ -19,9 +20,30 @@
1920
from aai_cli.commands.agent_cascade import _exec
2021
from aai_cli.commands.agent_cascade._exec import run_agent_cascade
2122
from aai_cli.core import config
23+
from aai_cli.main import app
2224
from tests._cascade_fakes import make_session
2325
from tests.test_agent_cascade_command import _opts
2426

27+
runner = CliRunner()
28+
29+
30+
@pytest.mark.parametrize(
31+
("argv", "expected"),
32+
[([], True), (["--no-files"], False), (["--files"], True)],
33+
)
34+
def test_files_flag_resolves_into_options(monkeypatch, argv, expected):
35+
# Filesystem access is on by default: omitting the flag yields files=True, and --no-files
36+
# opts out. Pinned at the argv->options seam so the True default isn't a silent mutation.
37+
captured = {}
38+
39+
def fake_run(opts, state, *, json_mode):
40+
captured["opts"] = opts
41+
42+
monkeypatch.setattr(_exec, "run_agent_cascade", fake_run)
43+
result = runner.invoke(app, ["live", *argv])
44+
assert result.exit_code == 0
45+
assert captured["opts"].files is expected
46+
2547

2648
def test_deny_writes_always_rejects():
2749
# The non-interactive approver declines every write (no channel to confirm one).

0 commit comments

Comments
 (0)