roboshed 0.1.1.dev3__tar.gz → 0.1.1.dev4__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.
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/CHANGELOG.md +13 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/PKG-INFO +10 -8
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/README.md +8 -6
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/pyproject.toml +2 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/agents/librarian.py +2 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/agents/orchestrator.py +0 -3
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/capabilities.py +12 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/deployments/robosprawl.py +1 -3
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/identifiers.py +3 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/robosprawl.py +4 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/__init__.py +4 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/contexts.py +9 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/memory_files.py +33 -8
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/sleep_between_runs.py +22 -19
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/snapshot_conversation_example.py +1 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/snapshot_conversations.py +22 -22
- roboshed-0.1.1.dev4/src/roboshed/tools/stop_when_watched_agents_inactive.py +50 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/types.py +1 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/memory/test_librarian.py +239 -21
- roboshed-0.1.1.dev4/tests/memory/test_memory_files.py +87 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/memory/test_snapshot_conversations.py +65 -4
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_agent_deployment.py +3 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_agent_presets.py +2 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_capabilities.py +3 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_cli_command_chaining.py +0 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_cli_tools_prompt_examples.py +0 -4
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_compactify_messages.py +2 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_compaction_runtime.py +1 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_deployment_graph.py +5 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_email_factory.py +4 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_generic_guards.py +2 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_guard.py +53 -1
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_robosprawl.py +3 -4
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_tool_contexts.py +14 -2
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/.gitignore +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/LICENSE +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/agents/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/dependency_health.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/deployments/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/models.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/py.typed +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/sandbox.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/cli_tools/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/cli_tools/prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/email_tools/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/email_tools/prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/file_editing/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/skills/file_editing/prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/_snapshot_metadata.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/apply_patch.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/run_file_command/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/run_file_command/command.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/run_file_command/resolve.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/run_file_command/specs.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/utilities/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/utilities/cmd_spec.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/utilities/constants.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/utilities/formatting.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/cli_commands/utilities/path_extractors.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/compactification/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/compactification/compactify_messages.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/compactification/prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/compactification/summarize.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/compactification/summary_prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/consolidate_memory.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/consolidate_memory_example.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/consolidate_memory_prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/__init__.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/contracts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/drafts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/factory.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/inputs.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/messages.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/email/runtime.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/guard.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/guard_formatting.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/librarian_errors.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/purge_files.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/runner.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/snapshot_conversation_prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/snapshot_normalize.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/truncation.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/utils.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/memory/test_consolidate_memory.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/memory/test_memory_prompts.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/memory/test_purge_files.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/memory/test_summarize_conversation.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_apply_patch.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_build_guarded_tool_chain.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_cli_command_utils.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_dependency_health.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_path_extractors.py +0 -0
- {roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/tests/test_sandbox.py +0 -0
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.1.dev4 - 2026-09-20
|
|
6
|
+
|
|
7
|
+
- Migrate the Librarian and capability examples to Roboz's explicit
|
|
8
|
+
`mode=AgentMode.DETERMINISTIC` configuration. Breaking: remove
|
|
9
|
+
`interaction_mode` arguments from `orchestrator` and `robosprawl` calls;
|
|
10
|
+
invocation uses a bound host channel, defaulting to CLI. Registering a
|
|
11
|
+
`UserIO` adapter selects `Output.API` for the binding's lifetime.
|
|
12
|
+
|
|
13
|
+
- Keep the Librarian active for a final maintenance sweep after watched runs become
|
|
14
|
+
idle, so terminal conversation facts reach snapshots and memory before it stops.
|
|
15
|
+
Snapshot coverage now relies only on persisted message sequences, and maintenance
|
|
16
|
+
artifacts are published atomically.
|
|
17
|
+
|
|
5
18
|
## 0.1.1.dev3 - 2026-09-13
|
|
6
19
|
|
|
7
20
|
- Migrate capability bindings to the central typed contexts. Existing capability
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: roboshed
|
|
3
|
-
Version: 0.1.1.
|
|
3
|
+
Version: 0.1.1.dev4
|
|
4
4
|
Import-Name: roboshed
|
|
5
5
|
Summary: roboshed: optional building blocks for Roboz
|
|
6
6
|
Project-URL: Repository, https://github.com/Tachion-Oy/roboz
|
|
@@ -11,7 +11,7 @@ Classifier: Development Status :: 2 - Pre-Alpha
|
|
|
11
11
|
Classifier: Typing :: Typed
|
|
12
12
|
Requires-Python: >=3.13
|
|
13
13
|
Requires-Dist: pydantic>=2.12.4
|
|
14
|
-
Requires-Dist: roboz<0.2.0,>=0.1.2.
|
|
14
|
+
Requires-Dist: roboz<0.2.0,>=0.1.2.dev7
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# roboshed
|
|
@@ -25,7 +25,7 @@ install any model SDK, Proton, document SDK, web service, or backend framework.
|
|
|
25
25
|
|
|
26
26
|
```python
|
|
27
27
|
from pathlib import Path
|
|
28
|
-
from roboz import stop
|
|
28
|
+
from roboz.tools import stop
|
|
29
29
|
from roboz.deployment import DeployableAgent, Capability
|
|
30
30
|
from roboz.llm import MockLLMEndpoint
|
|
31
31
|
from roboshed.capabilities import FileCommands, FileEditing
|
|
@@ -81,7 +81,7 @@ migrated. The description below records the recipe behavior to preserve.
|
|
|
81
81
|
`roboshed.deployments.robosprawl.robosprawl` is the concrete lazy persistent
|
|
82
82
|
orchestrator and Librarian recipe. Call it with an already-scoped sandbox,
|
|
83
83
|
`endpoint_getter`, `memory_endpoint`, `additional_capabilities`, `specialists`,
|
|
84
|
-
|
|
84
|
+
and optional `event_sinks`. It returns a fresh root and
|
|
85
85
|
background-agent tuple.
|
|
86
86
|
|
|
87
87
|
The recipe loads project memory and supplies project locations through initial
|
|
@@ -168,10 +168,12 @@ agent configuration. Each build creates fresh runtime state; applications can
|
|
|
168
168
|
supply an agent-specific sink factory for persistence.
|
|
169
169
|
|
|
170
170
|
The Librarian constructor declares its standard maintenance sequence:
|
|
171
|
-
snapshots, consolidation, retention, then cadence.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
snapshots, consolidation, retention, an idle check, then cadence. The first idle
|
|
172
|
+
observation schedules one complete final sweep without sleeping; the second idle
|
|
173
|
+
observation stops the Librarian. Pass its sandbox and the recursive foreground
|
|
174
|
+
names directly to the constructor before attaching it as a background agent. The
|
|
175
|
+
orchestrator also takes the configured sandbox and captures its permission policy
|
|
176
|
+
when constructed.
|
|
175
177
|
|
|
176
178
|
Invoke the returned agent directly and retain the background agents for control.
|
|
177
179
|
Repeated builds create fresh runtimes and bindings, but supplied endpoints,
|
|
@@ -9,7 +9,7 @@ install any model SDK, Proton, document SDK, web service, or backend framework.
|
|
|
9
9
|
|
|
10
10
|
```python
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from roboz import stop
|
|
12
|
+
from roboz.tools import stop
|
|
13
13
|
from roboz.deployment import DeployableAgent, Capability
|
|
14
14
|
from roboz.llm import MockLLMEndpoint
|
|
15
15
|
from roboshed.capabilities import FileCommands, FileEditing
|
|
@@ -65,7 +65,7 @@ migrated. The description below records the recipe behavior to preserve.
|
|
|
65
65
|
`roboshed.deployments.robosprawl.robosprawl` is the concrete lazy persistent
|
|
66
66
|
orchestrator and Librarian recipe. Call it with an already-scoped sandbox,
|
|
67
67
|
`endpoint_getter`, `memory_endpoint`, `additional_capabilities`, `specialists`,
|
|
68
|
-
|
|
68
|
+
and optional `event_sinks`. It returns a fresh root and
|
|
69
69
|
background-agent tuple.
|
|
70
70
|
|
|
71
71
|
The recipe loads project memory and supplies project locations through initial
|
|
@@ -152,10 +152,12 @@ agent configuration. Each build creates fresh runtime state; applications can
|
|
|
152
152
|
supply an agent-specific sink factory for persistence.
|
|
153
153
|
|
|
154
154
|
The Librarian constructor declares its standard maintenance sequence:
|
|
155
|
-
snapshots, consolidation, retention, then cadence.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
snapshots, consolidation, retention, an idle check, then cadence. The first idle
|
|
156
|
+
observation schedules one complete final sweep without sleeping; the second idle
|
|
157
|
+
observation stops the Librarian. Pass its sandbox and the recursive foreground
|
|
158
|
+
names directly to the constructor before attaching it as a background agent. The
|
|
159
|
+
orchestrator also takes the configured sandbox and captures its permission policy
|
|
160
|
+
when constructed.
|
|
159
161
|
|
|
160
162
|
Invoke the returned agent directly and retain the background agents for control.
|
|
161
163
|
Repeated builds create fresh runtimes and bindings, but supplied endpoints,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "roboshed"
|
|
3
|
-
version = "0.1.1.
|
|
3
|
+
version = "0.1.1.dev4"
|
|
4
4
|
description = "roboshed: optional building blocks for Roboz"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -9,7 +9,7 @@ license = "Apache-2.0"
|
|
|
9
9
|
license-files = ["LICENSE"]
|
|
10
10
|
import-names = ["roboshed"]
|
|
11
11
|
classifiers = ["Development Status :: 2 - Pre-Alpha", "Typing :: Typed"]
|
|
12
|
-
dependencies = ["roboz>=0.1.2.
|
|
12
|
+
dependencies = ["roboz>=0.1.2.dev7,<0.2.0","pydantic>=2.12.4"]
|
|
13
13
|
|
|
14
14
|
[build-system]
|
|
15
15
|
requires = ["hatchling>=1.32"]
|
|
@@ -13,6 +13,7 @@ from roboshed.identifiers import LIBRARIAN_AGENT_NAME
|
|
|
13
13
|
from roboshed.sandbox import Sandbox
|
|
14
14
|
from roboz.deployment import DeployableAgent
|
|
15
15
|
from roboz.llm import EndpointLike
|
|
16
|
+
from roboz.models import AgentMode
|
|
16
17
|
|
|
17
18
|
LIBRARIAN_AGENT_DESCRIPTION: Final[str] = (
|
|
18
19
|
"Runs deterministic maintenance cycles using the configured capabilities."
|
|
@@ -29,7 +30,7 @@ def librarian(
|
|
|
29
30
|
agent = DeployableAgent(
|
|
30
31
|
name=LIBRARIAN_AGENT_NAME,
|
|
31
32
|
description=LIBRARIAN_AGENT_DESCRIPTION,
|
|
32
|
-
|
|
33
|
+
mode=AgentMode.DETERMINISTIC,
|
|
33
34
|
automatic_tool_prompt=False,
|
|
34
35
|
default_capabilities=(
|
|
35
36
|
ConversationSnapshots(),
|
|
@@ -39,7 +40,6 @@ def librarian(
|
|
|
39
40
|
),
|
|
40
41
|
)
|
|
41
42
|
agent.set_agent_endpoint(agent_endpoint)
|
|
42
|
-
agent.set_interaction_mode(None)
|
|
43
43
|
agent.set_attributes(
|
|
44
44
|
sandbox=sandbox,
|
|
45
45
|
watched_agent_names=frozenset(watched_agent_names),
|
|
@@ -7,7 +7,6 @@ from roboshed.capabilities import FileCommands, FileEditing
|
|
|
7
7
|
from roboshed.sandbox import Sandbox
|
|
8
8
|
from roboz.deployment import Capability, DeployableAgent
|
|
9
9
|
from roboz.llm import EndpointLike
|
|
10
|
-
from roboz.runtime import Output
|
|
11
10
|
from roboz.tools import stop
|
|
12
11
|
|
|
13
12
|
ORCHESTRATOR_PROMPT = """You are the user's persistent collaborator. Help them reach their
|
|
@@ -26,7 +25,6 @@ def orchestrator(
|
|
|
26
25
|
agent_endpoint: EndpointLike,
|
|
27
26
|
subagents: Sequence[DeployableAgent] = (),
|
|
28
27
|
background_agents: Sequence[DeployableAgent] = (),
|
|
29
|
-
interaction_mode: Output | None = Output.CLI,
|
|
30
28
|
initial_messages: Sequence[Path | str] = (),
|
|
31
29
|
) -> DeployableAgent:
|
|
32
30
|
"""Configure the orchestrator for an already-configured sandbox."""
|
|
@@ -43,7 +41,6 @@ def orchestrator(
|
|
|
43
41
|
background_agents=tuple(background_agents),
|
|
44
42
|
)
|
|
45
43
|
agent.set_agent_endpoint(agent_endpoint)
|
|
46
|
-
agent.set_interaction_mode(interaction_mode)
|
|
47
44
|
agent.set_initial_messages(initial_messages)
|
|
48
45
|
agent.set_attributes(permissions=sandbox.permissions())
|
|
49
46
|
return agent
|
|
@@ -12,6 +12,7 @@ from roboshed.identifiers import (
|
|
|
12
12
|
PURGE_SNAPSHOTS_TOOL_NAME,
|
|
13
13
|
SLEEP_BETWEEN_RUNS_TOOL_NAME,
|
|
14
14
|
SNAPSHOT_CONVERSATIONS_TOOL_NAME,
|
|
15
|
+
STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME,
|
|
15
16
|
)
|
|
16
17
|
from roboshed.skills import cli_skill, file_editing
|
|
17
18
|
from roboshed.tools import (
|
|
@@ -30,9 +31,13 @@ from roboshed.tools.contexts import (
|
|
|
30
31
|
PurgeFilesContext,
|
|
31
32
|
SleepBetweenRunsContext,
|
|
32
33
|
SnapshotConversationsContext,
|
|
34
|
+
StopWhenWatchedAgentsInactiveContext,
|
|
33
35
|
)
|
|
34
36
|
from roboshed.tools.purge_files import purge_files
|
|
35
37
|
from roboshed.tools.sleep_between_runs import sleep_between_runs
|
|
38
|
+
from roboshed.tools.stop_when_watched_agents_inactive import (
|
|
39
|
+
stop_when_watched_agents_inactive,
|
|
40
|
+
)
|
|
36
41
|
from roboshed.tools.snapshot_conversations import snapshot_conversations
|
|
37
42
|
from roboshed.sandbox import PermissionPolicy, Sandbox
|
|
38
43
|
from roboz.deployment import (
|
|
@@ -326,11 +331,17 @@ class MaintenanceCadence(AgentCapability):
|
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
def build(self, agent: DeployableAgent, pipe: EventPipe) -> Capability:
|
|
329
|
-
"""Bind cadence and
|
|
334
|
+
"""Bind cadence and inactive-agent stopping to cancellation state."""
|
|
330
335
|
sandbox = cast(Sandbox, agent.sandbox)
|
|
331
336
|
watched_agent_names = cast(Collection[str], agent.watched_agent_names)
|
|
332
337
|
return Capability(
|
|
333
338
|
default_tools=(
|
|
339
|
+
stop_when_watched_agents_inactive(
|
|
340
|
+
StopWhenWatchedAgentsInactiveContext(
|
|
341
|
+
conversation_root=sandbox.project_logs_dir(),
|
|
342
|
+
agent_names=set(watched_agent_names),
|
|
343
|
+
)
|
|
344
|
+
).copy(name=STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME),
|
|
334
345
|
sleep_between_runs(
|
|
335
346
|
SleepBetweenRunsContext(
|
|
336
347
|
seconds=self.seconds,
|
|
@@ -8,7 +8,7 @@ from roboshed.sandbox import Sandbox
|
|
|
8
8
|
from roboz.agent import Agent
|
|
9
9
|
from roboz.deployment import AgentCapability, DeployableAgent
|
|
10
10
|
from roboz.llm import EndpointLike, LLMEndpoint, LLMEndpointRoute
|
|
11
|
-
from roboz.runtime import EventSink,
|
|
11
|
+
from roboz.runtime import EventSink, default_event_sinks
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def robosprawl(
|
|
@@ -19,7 +19,6 @@ def robosprawl(
|
|
|
19
19
|
memory_endpoint: EndpointLike,
|
|
20
20
|
additional_capabilities: Sequence[AgentCapability],
|
|
21
21
|
specialists: Sequence[DeployableAgent],
|
|
22
|
-
interaction_mode: Output | None,
|
|
23
22
|
event_sinks: Sequence[EventSink] = (),
|
|
24
23
|
) -> tuple[Agent, tuple[Agent, ...]]:
|
|
25
24
|
"""Build a fresh fixed orchestrator and Librarian for a scoped project.
|
|
@@ -39,7 +38,6 @@ def robosprawl(
|
|
|
39
38
|
sandbox,
|
|
40
39
|
agent_endpoint=LLMEndpointRoute(endpoint_getter),
|
|
41
40
|
subagents=tuple(specialists),
|
|
42
|
-
interaction_mode=interaction_mode,
|
|
43
41
|
)
|
|
44
42
|
root.add_capabilities(*additional_capabilities)
|
|
45
43
|
watched_agent_names = root.agent_names(include_background=False)
|
|
@@ -23,5 +23,8 @@ PURGE_FILES_TOOL_NAME: Final[str] = "purge_files"
|
|
|
23
23
|
PURGE_LOGS_TOOL_NAME: Final[str] = "purge_logs"
|
|
24
24
|
PURGE_SNAPSHOTS_TOOL_NAME: Final[str] = "purge_snapshots"
|
|
25
25
|
PURGE_MEMORY_TOOL_NAME: Final[str] = "purge_memory"
|
|
26
|
+
STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME: Final[str] = (
|
|
27
|
+
"stop_when_watched_agents_inactive"
|
|
28
|
+
)
|
|
26
29
|
SLEEP_BETWEEN_RUNS_TOOL_NAME: Final[str] = "sleep_between_runs"
|
|
27
30
|
LIBRARIAN_AGENT_NAME: Final[str] = "librarian"
|
|
@@ -56,8 +56,10 @@ project directory. Use the supplied paths instead of guessing folder names.
|
|
|
56
56
|
|
|
57
57
|
The Librarian is a deterministic background pipeline. It snapshots conversations,
|
|
58
58
|
consolidates memory, applies retention limits, and waits between maintenance cycles.
|
|
59
|
-
|
|
60
|
-
and
|
|
59
|
+
After watched work becomes idle, it performs one complete final sweep without
|
|
60
|
+
sleeping and stops only after confirming that the project remains idle. The root
|
|
61
|
+
starts it through its background-start tool; the host owns cancellation and
|
|
62
|
+
shutdown. Maintenance is asynchronous, so a new conversation may not yet have
|
|
61
63
|
appeared in memory. Conversation logs are written by the runtime.
|
|
62
64
|
|
|
63
65
|
Do not duplicate that maintenance by writing your own session summaries or memory
|
|
@@ -12,6 +12,7 @@ from .contexts import (
|
|
|
12
12
|
PurgeFilesContext,
|
|
13
13
|
SleepBetweenRunsContext,
|
|
14
14
|
SnapshotConversationsContext,
|
|
15
|
+
StopWhenWatchedAgentsInactiveContext,
|
|
15
16
|
)
|
|
16
17
|
from .runner import ExecutableCommandCatalog
|
|
17
18
|
from .cli_commands.run_file_command import get_run_file_command
|
|
@@ -19,6 +20,7 @@ from .compactification import get_compactify_messages_when_needed_tool
|
|
|
19
20
|
from .consolidate_memory import consolidate_memory
|
|
20
21
|
from .purge_files import purge_files, purge_files_by_threshold
|
|
21
22
|
from .sleep_between_runs import sleep_between_runs
|
|
23
|
+
from .stop_when_watched_agents_inactive import stop_when_watched_agents_inactive
|
|
22
24
|
from .snapshot_conversations import (
|
|
23
25
|
SnapshotMode,
|
|
24
26
|
snapshot_conversations,
|
|
@@ -36,6 +38,7 @@ __all__ = [
|
|
|
36
38
|
"SnapshotConversationsContext",
|
|
37
39
|
"PurgeFilesContext",
|
|
38
40
|
"SleepBetweenRunsContext",
|
|
41
|
+
"StopWhenWatchedAgentsInactiveContext",
|
|
39
42
|
"SnapshotMode",
|
|
40
43
|
"consolidate_memory",
|
|
41
44
|
"get_apply_patch",
|
|
@@ -44,5 +47,6 @@ __all__ = [
|
|
|
44
47
|
"purge_files",
|
|
45
48
|
"purge_files_by_threshold",
|
|
46
49
|
"sleep_between_runs",
|
|
50
|
+
"stop_when_watched_agents_inactive",
|
|
47
51
|
"snapshot_conversations",
|
|
48
52
|
]
|
|
@@ -137,9 +137,17 @@ class PurgeFilesContext:
|
|
|
137
137
|
prune_empty_directories: bool = False
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
@dataclass(frozen=True, kw_only=True)
|
|
141
|
+
class StopWhenWatchedAgentsInactiveContext:
|
|
142
|
+
"""Watched conversations used to decide when maintenance may stop."""
|
|
143
|
+
|
|
144
|
+
conversation_root: Path
|
|
145
|
+
agent_names: set[str]
|
|
146
|
+
|
|
147
|
+
|
|
140
148
|
@dataclass(frozen=True, kw_only=True)
|
|
141
149
|
class SleepBetweenRunsContext:
|
|
142
|
-
"""Wait
|
|
150
|
+
"""Wait settings for observing activity between maintenance cycles."""
|
|
143
151
|
|
|
144
152
|
seconds: float
|
|
145
153
|
is_cancelled: Callable[[], bool] | None = None
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"""Filesystem helpers shared by snapshot and consolidation tools."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import os
|
|
4
|
+
from datetime import UTC, datetime, timedelta
|
|
5
|
+
from itertools import count
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Final
|
|
8
|
+
from uuid import uuid4
|
|
6
9
|
|
|
7
10
|
from pydantic import ValidationError
|
|
8
11
|
|
|
@@ -67,15 +70,37 @@ def write_timestamped_file(
|
|
|
67
70
|
replace: Path | None,
|
|
68
71
|
pipe: EventPipe | None = None,
|
|
69
72
|
) -> Path:
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
"""Atomically publish a complete timestamped artifact, then remove ``replace``."""
|
|
74
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
temporary_path = folder / f".librarian-{uuid4().hex}.tmp"
|
|
76
|
+
published_path: Path | None = None
|
|
77
|
+
try:
|
|
78
|
+
with temporary_path.open("x", encoding=UTF8_ENCODING) as temporary:
|
|
79
|
+
temporary.write(body)
|
|
80
|
+
temporary.flush()
|
|
81
|
+
os.fsync(temporary.fileno())
|
|
82
|
+
|
|
83
|
+
if pipe is not None:
|
|
84
|
+
pipe.raise_if_cancelled()
|
|
85
|
+
|
|
86
|
+
base_time = utc_now()
|
|
87
|
+
for offset in count():
|
|
88
|
+
stamp = base_time + timedelta(microseconds=offset)
|
|
89
|
+
path = folder / f"{stamp.strftime(TIMESTAMP_STEM_FORMAT)}{suffix}"
|
|
90
|
+
try:
|
|
91
|
+
os.link(temporary_path, path)
|
|
92
|
+
except FileExistsError:
|
|
93
|
+
continue
|
|
94
|
+
published_path = path
|
|
95
|
+
break
|
|
96
|
+
finally:
|
|
97
|
+
temporary_path.unlink(missing_ok=True)
|
|
98
|
+
|
|
99
|
+
if published_path is None:
|
|
100
|
+
raise RuntimeError("timestamp candidate generation exhausted")
|
|
76
101
|
if replace is not None:
|
|
77
102
|
replace.unlink(missing_ok=True)
|
|
78
|
-
return
|
|
103
|
+
return published_path
|
|
79
104
|
|
|
80
105
|
|
|
81
106
|
__all__ = [
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
"""Cancellable wait between deterministic Librarian maintenance cycles."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
|
-
from pathlib import Path
|
|
5
4
|
from time import sleep
|
|
6
5
|
from typing import Final
|
|
7
6
|
|
|
8
7
|
from roboshed.identifiers import SLEEP_BETWEEN_RUNS_TOOL_NAME
|
|
8
|
+
from roboshed.tools.contexts import SleepBetweenRunsContext
|
|
9
9
|
from roboz.exceptions import ExternalCallCancelledError
|
|
10
|
-
from roboz.models import NO_MESSAGE, All, Message,
|
|
10
|
+
from roboz.models import NO_MESSAGE, All, Message, Str
|
|
11
11
|
from roboz.runtime.persistence import active_marker_paths
|
|
12
|
-
from roboshed.tools.contexts import SleepBetweenRunsContext
|
|
13
12
|
from roboz.tooling.decorators import factory
|
|
14
13
|
|
|
15
14
|
SLEEP_POLL_SECONDS: Final[float] = 1.0
|
|
16
15
|
|
|
17
|
-
_PROJECT_IDLE_STATUS: Final[str] = "project idle"
|
|
18
|
-
_ACTIVE_RUN_ENDED_STATUS: Final[str] = "active run ended"
|
|
19
|
-
_MIN_SLEEP_SECONDS: Final[float] = 0.0
|
|
20
|
-
|
|
21
16
|
|
|
22
17
|
def _raise_if_cancelled(is_cancelled: Callable[[], bool] | None) -> None:
|
|
23
18
|
if is_cancelled is not None and is_cancelled():
|
|
@@ -27,19 +22,27 @@ def _raise_if_cancelled(is_cancelled: Callable[[], bool] | None) -> None:
|
|
|
27
22
|
@factory
|
|
28
23
|
def sleep_between_runs(
|
|
29
24
|
input: All, messages: list[Message], ctx: SleepBetweenRunsContext
|
|
30
|
-
) -> Str
|
|
31
|
-
"""Wait
|
|
25
|
+
) -> Str:
|
|
26
|
+
"""Wait between maintenance cycles, waking promptly when a watched run ends.
|
|
27
|
+
|
|
28
|
+
Return immediately when the watched agents are inactive. This tool never
|
|
29
|
+
decides whether maintenance is complete; use ``stop_when_watched_agents_inactive`` for that.
|
|
30
|
+
"""
|
|
32
31
|
del input, messages
|
|
33
|
-
seconds = max(_MIN_SLEEP_SECONDS, float(ctx.seconds))
|
|
34
32
|
_raise_if_cancelled(ctx.is_cancelled)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
active_markers = (
|
|
34
|
+
active_marker_paths(ctx.conversation_root, ctx.agent_names)
|
|
35
|
+
if ctx.conversation_root is not None
|
|
36
|
+
else ()
|
|
37
|
+
)
|
|
38
|
+
if ctx.conversation_root is not None and not active_markers:
|
|
39
|
+
return Str(
|
|
40
|
+
value=f"{SLEEP_BETWEEN_RUNS_TOOL_NAME}: watched agents inactive",
|
|
41
|
+
truncation=NO_MESSAGE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
seconds = max(0.0, float(ctx.seconds))
|
|
45
|
+
slept = 0.0
|
|
43
46
|
while slept < seconds:
|
|
44
47
|
_raise_if_cancelled(ctx.is_cancelled)
|
|
45
48
|
chunk = min(SLEEP_POLL_SECONDS, seconds - slept)
|
|
@@ -48,7 +51,7 @@ def sleep_between_runs(
|
|
|
48
51
|
_raise_if_cancelled(ctx.is_cancelled)
|
|
49
52
|
if any(not marker.exists() for marker in active_markers):
|
|
50
53
|
return Str(
|
|
51
|
-
value=
|
|
54
|
+
value=f"{SLEEP_BETWEEN_RUNS_TOOL_NAME}: active run ended",
|
|
52
55
|
truncation=NO_MESSAGE,
|
|
53
56
|
)
|
|
54
57
|
return Str(
|
{roboshed-0.1.1.dev3 → roboshed-0.1.1.dev4}/src/roboshed/tools/snapshot_conversation_example.py
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from typing import Final
|
|
4
4
|
|
|
5
5
|
SNAPSHOT_CONVERSATION_EXAMPLE: Final[str] = """## Goals and intent
|
|
6
|
-
- User wants a
|
|
6
|
+
- User wants a deterministic Librarian that runs in the background and maintains conversation memory without user involvement.
|
|
7
7
|
- User stated the snapshots exist to feed a later consolidation step, "the initial raw phase of information gathering".
|
|
8
8
|
|
|
9
9
|
## State of work
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Final
|
|
7
8
|
|
|
8
9
|
from roboshed.identifiers import SNAPSHOT_CONVERSATIONS_TOOL_NAME
|
|
@@ -47,8 +48,6 @@ TERMINAL_SYNCABLE_STATUSES: Final[frozenset[RunStatus]] = frozenset(
|
|
|
47
48
|
{RunStatus.COMPLETED, RunStatus.FAILED}
|
|
48
49
|
)
|
|
49
50
|
|
|
50
|
-
_ISO_Z_SUFFIX: Final[str] = "Z"
|
|
51
|
-
_ISO_UTC_OFFSET: Final[str] = "+00:00"
|
|
52
51
|
_NOT_ENDED: Final[str] = "not ended"
|
|
53
52
|
_UNKNOWN_TIME: Final[str] = "unknown"
|
|
54
53
|
_SOURCE_AGENT_KEY: Final[str] = "source_agent"
|
|
@@ -69,29 +68,28 @@ class SnapshotMode(StrEnum):
|
|
|
69
68
|
|
|
70
69
|
def _uncovered_rows(
|
|
71
70
|
rows: list[LoggedMessageRow],
|
|
72
|
-
latest_snapshot_time: datetime | None,
|
|
73
71
|
snapshot_document: SnapshotDocument | None,
|
|
74
72
|
) -> list[LoggedMessageRow]:
|
|
75
|
-
"""Return rows beyond
|
|
73
|
+
"""Return rows beyond valid stored coverage, reprocessing legacy artifacts."""
|
|
76
74
|
if snapshot_document is not None and snapshot_document.coverage_marker_present:
|
|
77
75
|
covered_sequence = snapshot_document.covered_through_sequence
|
|
78
76
|
if covered_sequence is None:
|
|
79
77
|
return rows
|
|
80
78
|
return [row for row in rows if row.sequence > covered_sequence]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
return rows
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _persisted_snapshot_covers(path: Path, *, sequence: int) -> bool:
|
|
83
|
+
"""Return whether a competing snapshot has valid sufficient coverage."""
|
|
84
|
+
try:
|
|
85
|
+
document = parse_snapshot_document(path.read_text(encoding=UTF8_ENCODING))
|
|
86
|
+
except OSError:
|
|
87
|
+
return False
|
|
88
|
+
return (
|
|
89
|
+
document.coverage_marker_present
|
|
90
|
+
and document.covered_through_sequence is not None
|
|
91
|
+
and document.covered_through_sequence >= sequence
|
|
92
|
+
)
|
|
95
93
|
|
|
96
94
|
|
|
97
95
|
def _normalize_temporal_messages(
|
|
@@ -191,9 +189,7 @@ def _snapshot_one_run(
|
|
|
191
189
|
if snapshot_location is not None
|
|
192
190
|
else None
|
|
193
191
|
)
|
|
194
|
-
uncovered_rows = _uncovered_rows(
|
|
195
|
-
list(run.messages), snapshot_time, snapshot_document
|
|
196
|
-
)
|
|
192
|
+
uncovered_rows = _uncovered_rows(list(run.messages), snapshot_document)
|
|
197
193
|
if not uncovered_rows:
|
|
198
194
|
return False
|
|
199
195
|
covered_through_sequence = max(row.sequence for row in uncovered_rows)
|
|
@@ -235,9 +231,13 @@ def _snapshot_one_run(
|
|
|
235
231
|
if ctx.pipe is not None:
|
|
236
232
|
ctx.pipe.raise_if_cancelled()
|
|
237
233
|
|
|
238
|
-
current_time,
|
|
234
|
+
current_time, current_location = latest_timestamped_file(
|
|
235
|
+
snapshot_folder, suffix=MARKDOWN_SUFFIX
|
|
236
|
+
)
|
|
239
237
|
if current_time is not None and (
|
|
240
238
|
snapshot_time is None or current_time > snapshot_time
|
|
239
|
+
) and current_location is not None and _persisted_snapshot_covers(
|
|
240
|
+
current_location, sequence=covered_through_sequence
|
|
241
241
|
):
|
|
242
242
|
return False
|
|
243
243
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Require a final maintenance sweep after watched agents become inactive."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from roboshed.identifiers import STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME
|
|
6
|
+
from roboshed.tools.contexts import StopWhenWatchedAgentsInactiveContext
|
|
7
|
+
from roboz.models import NO_MESSAGE, All, Message, Stop, Str, filter_messages
|
|
8
|
+
from roboz.runtime.persistence import active_marker_paths
|
|
9
|
+
from roboz.tooling.decorators import factory
|
|
10
|
+
|
|
11
|
+
_FINAL_SWEEP_REQUIRED = (
|
|
12
|
+
f"{STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME}: final sweep required"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@factory
|
|
17
|
+
def stop_when_watched_agents_inactive(
|
|
18
|
+
input: All, messages: list[Message], ctx: StopWhenWatchedAgentsInactiveContext
|
|
19
|
+
) -> Str | Stop:
|
|
20
|
+
"""Stop only after watched agents remain inactive for a full final sweep."""
|
|
21
|
+
del input
|
|
22
|
+
if active_marker_paths(ctx.conversation_root, ctx.agent_names):
|
|
23
|
+
return Str(
|
|
24
|
+
value=f"{STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME}: watched agents active",
|
|
25
|
+
truncation=NO_MESSAGE,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
previous_checks = filter_messages(
|
|
29
|
+
messages, caller=STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME
|
|
30
|
+
)
|
|
31
|
+
last_result = (
|
|
32
|
+
json.loads(previous_checks[-1].content).get("value")
|
|
33
|
+
if previous_checks
|
|
34
|
+
else None
|
|
35
|
+
)
|
|
36
|
+
if last_result == _FINAL_SWEEP_REQUIRED:
|
|
37
|
+
# A complete maintenance sweep has run since the previous inactive check.
|
|
38
|
+
return Stop(
|
|
39
|
+
value=f"{STOP_WHEN_WATCHED_AGENTS_INACTIVE_TOOL_NAME}: watched agents inactive"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# The preceding sweep may have missed a run's final output. Now that the
|
|
43
|
+
# watched agents are inactive, require another sweep before allowing shutdown.
|
|
44
|
+
return Str(
|
|
45
|
+
value=_FINAL_SWEEP_REQUIRED,
|
|
46
|
+
truncation=NO_MESSAGE,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["StopWhenWatchedAgentsInactiveContext", "stop_when_watched_agents_inactive"]
|