Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,58 @@ All notable sagent changes are documented here. This project follows

## Unreleased

### Added

- A `Memory` tool saves and forgets project memories and keeps `MEMORY.md`
in step with them, newest first. It is in the default toolset wherever the
model is shown the memory index: an `--ephemeral` session, or a recipe
without the `memory` section, leaves it out and refuses `--tools Memory`.
- `--system-prompt` replaces the recipe's base prompt, and
`--append-system-prompt` adds instructions after it.
- `Agent(append_system=..., prompt_context=...)`. A `prompt_context`
(`prompt.SagentContext` for the CLI's) renders the project instructions
and the environment. `Agent.reminders` holds standing context a host keeps
in force, and tools may implement `types.tools.Reminding`.
- `SystemMessage`, a history entry for what the harness tells the model (the
environment, tool and host reminders, a tool-gate change). It is no one's
turn, is never merged into a human's or a peer's message, and persists as a
`system_message` record that session readers report as a system message. On the
wire it is a `<system-reminder>` in the user turn it follows, as before.
- `UserMessage.offered_tools`, set on the wire turn a gate-changing system
message folds into, and `ModelRequest.allowed_tools`, read from the latest
one. `UserMessage.cache_point` now persists, and survives a merge.

### Changed

- The system prompt is a tuple of blocks (`ModelRequest.system`), built once
per context and rebuilt on `/clear`, compaction and resume, and when
`replace_tool` rewords a tool's prompt. Anthropic marks
each block as a cache point, so a child with its parent's tools reads the
parent's cached prefix. What changes -- the environment, the agents one can
message, the spawn budget, the tool gate -- reaches the model as an
appended `SystemMessage`, retold whenever the context stops holding it.
- `Agent.tool_gate` masks tools instead of removing their schemas: Anthropic
withdraws them with the inline-tools beta, OpenAI receives
`tool_choice: allowed_tools`, and a call outside the gate its request
offered is refused on every provider.
- The root agent gets `BackgroundTask` with `AgentSpawn`, as a child does.
- A merge of user-side messages from different senders labels each part with
its own sender instead of giving the whole an `AgentSendMessage` source.

### Removed

- `sagent.lib.worker_count` is no longer part of the package. It moved, with
the pre-commit gates that use it, to `tools/gate/` at the repository root,
which the wheel does not ship.
- `--system`, which added instructions; it now fails with a pointer to
`--append-system-prompt` and `--system-prompt`.
- `AgentSpawn`'s `hot` argument and `Agent(frozen_system=...)`: every child
now shares its parent's system blocks. A persistent child a hot spawn saved
is not resumed: its saved prompt is its parent's whole rendered prompt.
- `Agent(system=<callable>)` and `SystemPromptArg`; `system` is a string.
- `prompt.build_system` and `prompt.build_system_dict`, replaced by
`prompt.static_prompt`, `prompt.project_context`,
`prompt.render_environment` and `prompt.SagentContext`.

## 0.1.20 - 2026-10-21

Expand Down
8 changes: 6 additions & 2 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ sagent --provider SelfHosted --model /opt/models/qwen3.6-27b+bfloat16+cuda
| `--auth METHOD` | Calls `Provider.from_<METHOD>()`; an explicit API-key provider defaults to `env`, while subscription providers default to `credentials`. |
| `--account NAME` | Optional named credential slot for providers that support one. |
| `--model ID` | Catalog model ID. Bare IDs select the largest context window. Models with smaller profiles accept `+200k` or `+272k`; an explicit `+1m` remains valid when the bare profile is already at least one million tokens. SelfHosted IDs may include `+cuda`, `+bfloat16`, or `+compile`. |
| `--system TEXT` | Extra system prompt instructions appended to Sagent's default prompt. |
| `--system-prompt TEXT` | System prompt replacing Sagent's default prompt. |
| `--append-system-prompt TEXT` | Instructions added after the system prompt. |
| `--effort LEVEL` | Reasoning effort: `none`, `min`, `low`, `medium`, `high`, `xhigh`, or `max`. Each model accepts a subset (GPT-5.6 takes all seven; GPT-6 Astra has no `none`), and an unsupported value raises. |
| `--max-response-tokens N` | Limit response tokens for each model call. |
| `--log-level LEVEL` | Enable stderr diagnostics. Same values as Python logging levels. |
Expand Down Expand Up @@ -88,9 +89,12 @@ sagent --tools Read Bash BackgroundTask
`--tools` accepts exact class names exported from `sagent.tools`. The default terminal tool set is:

```text
AgentSpawn AgentSend AgentSelf Bash Read Write Edit Grep Glob List WebSearch WebFetch PaperSearch PaperDetails PaperAuthor PaperFetch PlayAudio Skill
AgentSpawn AgentSend AgentSelf Bash Read Write Edit Grep Glob List WebSearch WebFetch PaperSearch PaperDetails PaperAuthor PaperFetch Memory PlayAudio Skill
```

`Memory` is left out where the model is shown no memory index: under
`--ephemeral`, or a recipe without the `memory` section.

`Wiki` is available only when requested explicitly, for example
`--tools Read Grep Wiki`.

Expand Down
2 changes: 1 addition & 1 deletion docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ sagent --ephemeral
sagent --session /tmp/disposable-sagent-session
```

`--ephemeral` also disables the auto-memory system prompt section, so existing project memories are not sent to providers and the model is not instructed to write new memories. Library users can leave `session_dir` unset for in-memory runs; call `build_system_dict(..., include_memory=False)` when persistent memory should be disabled too.
`--ephemeral` also disables the auto-memory system prompt section, so existing project memories are not sent to providers and the model is not instructed to write new memories. Library users can leave `session_dir` unset for in-memory runs; pass `prompt_context=SagentContext(include_memory=False)` to `Agent` when persistent memory should be disabled too.

Large tool outputs may be persisted separately to keep model requests within budget. Session runs store those under `session_dir/tool-results`; no-session runs use `/tmp/sagent_results`. Persisted files are written with owner-only permissions.

Expand Down
2 changes: 1 addition & 1 deletion docs/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ sagent --resume-all
sagent --ephemeral
```

Use this for prompts that should not write conversation state or auto-memory to disk. Library users can omit `session_dir` for in-memory runs and pass `include_memory=False` to `build_system_dict` when persistent memory should be disabled too.
Use this for prompts that should not write conversation state or auto-memory to disk. Library users can omit `session_dir` for in-memory runs and pass `prompt_context=SagentContext(include_memory=False)` to `Agent` when persistent memory should be disabled too.

No-session runs may still persist oversized tool results temporarily under `/tmp/sagent_results` when needed to keep model requests within context budgets.

Expand Down
5 changes: 3 additions & 2 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A tool exposes:
- `directive_schema`: JSON Schema for input directives.
- `supports_microcompaction`: whether old results may be shortened.
- `summary(msg)`: short label for UI/status output.
- `prompt()`: optional per-request system prompt section.
- `prompt()`: optional system prompt section, read when the system blocks are built: at the start of a context (clear, compaction, resume), and when `replace_tool` swaps in a tool with a different one.
- `run(msg)`: async execution.

Batch tools return one `Message`. Streaming tools set `streaming = True` and make `run(msg)` an async generator; yielded messages before the last are intermediate events, and the last yield is the final tool result.
Expand Down Expand Up @@ -92,6 +92,7 @@ class EchoTool:
| `PaperDetails` | Paper metadata, references, citations. | `id`; optional `operation`. |
| `PaperAuthor` | Author search and author papers. | `query` or `id`; optional `operation="papers"`. |
| `PaperFetch` | Download paper PDFs. | `id`. |
| `Memory` | Save or forget a project memory, keeping `MEMORY.md` in step. | `action`, `name`; a save also `type`, `description`, `content`. |
| `AgentSelf` | Let an agent inspect/mutate itself. | Flat patch object; all fields optional. |
| `AgentSpawn` | Run child agents. | `prompt`; optional model/tools/depth/session controls. |
| `AgentSend` | Send to a live named agent inbox. | `to`, `content`; optional `delay`. |
Expand Down Expand Up @@ -199,7 +200,7 @@ Paper tools use Semantic Scholar, OpenAlex, arXiv, and open-access PDF metadata.

`AgentSpawn` creates child agents. Children can inherit the parent model/tools or override provider, auth, model ID, account, tools, max tool-call rounds, and max depth. With `persistent=true`, a child stays alive and can receive messages through `AgentSend`.

By default (`hot=false`, "cold"), a child rebuilds its own system prompt from its own tools each request -- it auto-gains `BackgroundTask` alongside `AgentSpawn` and shows its own spawn-depth text, both of which differ from the parent's prompt from the first request and defeat provider prompt caching. Pass `hot=true` to instead freeze the child's system prompt to a byte-identical snapshot of the parent's current one, letting the child's first request land on the same cached prefix the parent already warmed. Trade-off: a hot child's prompt won't reflect its own live depth budget or a freshly bundled tool. Prefer hot for cheap, frequent, or parallel children (lookups, reviews) that don't need that dynamism.
A child with its parent's tools builds its system blocks from its parent's settings, so its first request reads the prefix the parent already cached -- unless what the blocks render changed since the parent last built them (its directory, an `AGENTS.md`, a saved memory). What differs per agent -- its spawn depth, its label, the agents it can message, a persistent child's reply rule -- is told as a reminder with its first turn and retold when it changes, never in the system blocks.

Persistent subagents have a durable lifecycle. The parent session
records each child's `kind=persistent_subagent` entry with a stable
Expand Down
8 changes: 6 additions & 2 deletions examples/agent_maze/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,14 @@ def summary(self, args: Mapping[str, object]) -> str:
return "comms broadcast"

def prompt(self) -> str:
"""Return the tool prompt for the agent.
"""Return no fixed guidance; teammates come and go, so they are a reminder."""
return ""

def reminder(self) -> str:
"""Return who this agent is and which teammates it can reach.

Returns:
prompt: Prompt describing the agent's reachable teammates.
reminder: The agent's label and its reachable teammates.

"""
me = agent_label_var.get("")
Expand Down
19 changes: 19 additions & 0 deletions examples/agent_maze/tools_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,25 @@ def fake(parent: str, xy: tuple[int, int]) -> str:
_clear(["a0"])


def test_comms_tells_reachable_peers_as_a_reminder() -> None:
"""The system blocks are built once; teammates spawned later need retelling."""
eng = _engine()
_ = _register(["a0", "a1"])
tok = agent_label_var.set("a0")
try:
mesh = CommsTool(eng, mesh=True)
assert mesh.prompt() == ""
assert mesh.reminder() == "You are 'a0'. Teammates currently reachable: ['a1']."
tree = CommsTool(eng, mesh=False, coordinator="lead")
assert (
tree.reminder()
== "You are 'a0'. You may only message the coordinator 'lead'."
)
finally:
agent_label_var.reset(tok)
_clear(["a0", "a1"])


if __name__ == "__main__":
from sagent.lib.testing.runner import test_main

Expand Down
2 changes: 0 additions & 2 deletions sagent/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
ERROR_MAX_TOOL_CALL_ROUNDS,
ActivityTracker,
Agent,
SystemPromptArg,
)
from sagent.agent.background import BackgroundTaskEntry
from sagent.agent.compaction import CompactionState
Expand All @@ -48,5 +47,4 @@
"Agent",
"BackgroundTaskEntry",
"CompactionState",
"SystemPromptArg",
]
Loading
Loading