optio-opencode 0.5.0__tar.gz → 0.6.1__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.
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/PKG-INFO +4 -4
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/pyproject.toml +4 -4
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/conversation.py +3 -1
- optio_opencode-0.6.1/src/optio_opencode/prompt.py +48 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/session.py +18 -3
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/PKG-INFO +4 -4
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/SOURCES.txt +0 -1
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/requires.txt +3 -3
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_prompt.py +19 -9
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_local.py +89 -4
- optio_opencode-0.5.0/src/optio_opencode/prompt.py +0 -185
- optio_opencode-0.5.0/tests/test_resume_sentence_opencode.py +0 -7
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/README.md +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/setup.cfg +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/__init__.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/account.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/cred_watcher.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/fs_allowlist.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/host_actions.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/info.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/model_probe.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/__init__.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/anthropic.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/deepseek.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/openai.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/xai.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/seed_manifest.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/types.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/verify.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_account.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_account_xai_apikey.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_agent_info.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_agent_sender_opencode.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_claustrum.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_config_harmonization.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_config.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_driver.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_session.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_ui_model.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_ui_session.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_upload.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_cred_watcher.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_file_download.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_file_upload.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_actions.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_local.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_primitives_remote.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_model_probe.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_model_probe_wiring.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_provider_deepseek.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_purge_seed.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_sanity.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_seed_config.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_blob_hooks.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_hooks.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_resume.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_seed.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_seed_saveback.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_smart_install.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_snapshots.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_types.py +0 -0
- {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_verify_seed.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optio-opencode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
|
|
5
5
|
Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,9 +23,9 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
Requires-Dist: optio-core<0.5,>=0.4
|
|
24
24
|
Requires-Dist: optio-host<0.4,>=0.3
|
|
25
25
|
Requires-Dist: optio-agents<0.7,>=0.6
|
|
26
|
-
Requires-Dist: optio-claudecode<0.
|
|
27
|
-
Requires-Dist: optio-codex<0.
|
|
28
|
-
Requires-Dist: optio-grok<0.
|
|
26
|
+
Requires-Dist: optio-claudecode<0.7,>=0.6
|
|
27
|
+
Requires-Dist: optio-codex<0.5,>=0.4
|
|
28
|
+
Requires-Dist: optio-grok<0.5,>=0.4
|
|
29
29
|
Requires-Dist: asyncssh>=2.14
|
|
30
30
|
Requires-Dist: aiohttp>=3.9
|
|
31
31
|
Provides-Extra: dev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optio-opencode"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.1"
|
|
8
8
|
description = "Run opencode web as an optio task; local subprocess or remote via SSH."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -34,9 +34,9 @@ dependencies = [
|
|
|
34
34
|
# The account meta-analyzer's per-provider handlers reuse the extracted
|
|
35
35
|
# vendor map helpers (account_from_oauth_token / account_from_openai /
|
|
36
36
|
# account_from_xai) rather than reimplement each vendor's fetch+map.
|
|
37
|
-
"optio-claudecode>=0.
|
|
38
|
-
"optio-codex>=0.
|
|
39
|
-
"optio-grok>=0.
|
|
37
|
+
"optio-claudecode>=0.6,<0.7",
|
|
38
|
+
"optio-codex>=0.4,<0.5",
|
|
39
|
+
"optio-grok>=0.4,<0.5",
|
|
40
40
|
"asyncssh>=2.14",
|
|
41
41
|
"aiohttp>=3.9",
|
|
42
42
|
]
|
|
@@ -297,7 +297,9 @@ class OpencodeConversation:
|
|
|
297
297
|
|
|
298
298
|
# -- Conversation protocol surface --------------------------------------
|
|
299
299
|
|
|
300
|
-
async def send(self, text: str) -> None:
|
|
300
|
+
async def send(self, text: str, *, uuid: str | None = None) -> None:
|
|
301
|
+
"""``uuid`` is an advisory message id (Fix 13a/Fix 23): this backend
|
|
302
|
+
has no message identity of its own, so it is accepted and ignored."""
|
|
301
303
|
if self._closed.is_set():
|
|
302
304
|
raise ConversationClosed(self._close_reason or "conversation closed")
|
|
303
305
|
if self._http is None:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""AGENTS.md composition for optio-opencode: the profile, plus a thin wrapper
|
|
2
|
+
over the shared composer (``optio_agents.prompt.compose_instructions_file``),
|
|
3
|
+
which owns all of the text."""
|
|
4
|
+
|
|
5
|
+
from optio_agents.prompt import (
|
|
6
|
+
DEFAULT_CONVERSATION_INSTRUCTIONS,
|
|
7
|
+
AgentPromptProfile,
|
|
8
|
+
compose_instructions_file,
|
|
9
|
+
)
|
|
10
|
+
from optio_host.archive import DEFAULT_WORKDIR_EXCLUDES
|
|
11
|
+
|
|
12
|
+
__all__ = ["DEFAULT_CONVERSATION_INSTRUCTIONS", "PROFILE", "compose_agents_md"]
|
|
13
|
+
|
|
14
|
+
# opencode suppresses browser opens (its docs must not advertise BROWSER:), and
|
|
15
|
+
# keeps no state dir the agent needs to know about.
|
|
16
|
+
PROFILE = AgentPromptProfile(browser="suppress")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compose_agents_md(
|
|
20
|
+
consumer_instructions: str,
|
|
21
|
+
*,
|
|
22
|
+
workdir_exclude: list[str] | None = None,
|
|
23
|
+
documentation: str | None = None,
|
|
24
|
+
supports_resume: bool = True,
|
|
25
|
+
host_protocol: bool = True,
|
|
26
|
+
omit_task_framing: bool = False,
|
|
27
|
+
fs_isolation_dirs: list[tuple[str, str]] | None = None,
|
|
28
|
+
file_download: bool = False,
|
|
29
|
+
check_resume_log_every_message: bool = True,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Render <workdir>/AGENTS.md for an optio-opencode task.
|
|
32
|
+
|
|
33
|
+
``workdir_exclude=None`` means the framework defaults, the same list the
|
|
34
|
+
snapshot archive uses (``optio_host.archive``)."""
|
|
35
|
+
return compose_instructions_file(
|
|
36
|
+
consumer_instructions,
|
|
37
|
+
profile=PROFILE,
|
|
38
|
+
workdir_exclude=(
|
|
39
|
+
list(DEFAULT_WORKDIR_EXCLUDES) if workdir_exclude is None else workdir_exclude
|
|
40
|
+
),
|
|
41
|
+
documentation=documentation,
|
|
42
|
+
supports_resume=supports_resume,
|
|
43
|
+
host_protocol=host_protocol,
|
|
44
|
+
omit_task_framing=omit_task_framing,
|
|
45
|
+
fs_isolation_dirs=fs_isolation_dirs,
|
|
46
|
+
file_download=file_download,
|
|
47
|
+
check_resume_log_every_message=check_resume_log_every_message,
|
|
48
|
+
)
|
|
@@ -32,6 +32,7 @@ from optio_core.models import BasicAuth, TaskInstance
|
|
|
32
32
|
|
|
33
33
|
from optio_agents import HookContext
|
|
34
34
|
from optio_agents import RESUME_NOTICE, SYSTEM_MESSAGE_PREFIX
|
|
35
|
+
from optio_agents.fs_grants import fs_isolation_dirs
|
|
35
36
|
from optio_host.host import Host, LocalHost, ProcessHandle
|
|
36
37
|
from optio_host.paths import task_dir
|
|
37
38
|
from optio_agents.protocol.session import _SessionFailed, run_log_protocol_session
|
|
@@ -347,6 +348,7 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
347
348
|
supports_resume=config.supports_resume,
|
|
348
349
|
host_protocol=config.host_protocol,
|
|
349
350
|
omit_task_framing=omit_task_framing,
|
|
351
|
+
fs_isolation_dirs=fs_isolation_dirs(config, host.workdir),
|
|
350
352
|
file_download=config.file_download,
|
|
351
353
|
),
|
|
352
354
|
)
|
|
@@ -390,7 +392,7 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
390
392
|
# Resume: when on_resume_refresh is wired, recompute AGENTS.md
|
|
391
393
|
# from the refreshed config and overwrite the workdir copy if
|
|
392
394
|
# the rendered text differs from the snapshot-restored file.
|
|
393
|
-
refreshed_files = await _maybe_refresh_on_resume(host, hook_ctx, config)
|
|
395
|
+
refreshed_files = await _maybe_refresh_on_resume(host, hook_ctx, config, protocol)
|
|
394
396
|
|
|
395
397
|
# opencode.json is wholly optio-generated (the seed never carries the
|
|
396
398
|
# workdir-root copy — its manifest only includes files under home/),
|
|
@@ -1031,11 +1033,14 @@ async def _append_resume_log_entry(
|
|
|
1031
1033
|
|
|
1032
1034
|
|
|
1033
1035
|
async def _maybe_refresh_on_resume(
|
|
1034
|
-
host, hook_ctx, config: OpencodeTaskConfig,
|
|
1036
|
+
host, hook_ctx, config: OpencodeTaskConfig, protocol,
|
|
1035
1037
|
) -> list[str]:
|
|
1036
1038
|
"""Run the on_resume_refresh hook (if any) and rewrite AGENTS.md when
|
|
1037
1039
|
the rendered content differs from the workdir copy.
|
|
1038
1040
|
|
|
1041
|
+
``protocol`` is the session's protocol; its documentation is rendered so
|
|
1042
|
+
the refreshed file matches the fresh-start composition.
|
|
1043
|
+
|
|
1039
1044
|
Returns the list of filenames the harness rewrote (currently at most
|
|
1040
1045
|
``["AGENTS.md"]``), suitable for tagging the next ``resume.log`` line.
|
|
1041
1046
|
A hook that raises is logged and ignored — the resumed session keeps
|
|
@@ -1050,13 +1055,23 @@ async def _maybe_refresh_on_resume(
|
|
|
1050
1055
|
"on_resume_refresh raised; keeping existing AGENTS.md from snapshot",
|
|
1051
1056
|
)
|
|
1052
1057
|
return []
|
|
1058
|
+
# Mode-aware prompt kwargs, recomputed from the refreshed config (same
|
|
1059
|
+
# defaulting as the fresh-start composition above).
|
|
1060
|
+
instructions = new_config.consumer_instructions
|
|
1061
|
+
omit_task_framing = False
|
|
1062
|
+
if new_config.mode == "conversation" and not instructions:
|
|
1063
|
+
instructions = DEFAULT_CONVERSATION_INSTRUCTIONS
|
|
1064
|
+
omit_task_framing = True
|
|
1053
1065
|
new_agents_md = compose_agents_md(
|
|
1054
|
-
|
|
1066
|
+
instructions,
|
|
1067
|
+
documentation=protocol.documentation if new_config.host_protocol else None,
|
|
1055
1068
|
workdir_exclude=new_config.workdir_exclude,
|
|
1056
1069
|
supports_resume=new_config.supports_resume,
|
|
1057
1070
|
# Reflect the refreshed config so a resume keeps the downloadables block
|
|
1058
1071
|
# (with the right wording — host_protocol drives comparative vs standalone).
|
|
1059
1072
|
host_protocol=new_config.host_protocol,
|
|
1073
|
+
omit_task_framing=omit_task_framing,
|
|
1074
|
+
fs_isolation_dirs=fs_isolation_dirs(new_config, host.workdir),
|
|
1060
1075
|
file_download=new_config.file_download,
|
|
1061
1076
|
)
|
|
1062
1077
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optio-opencode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
4
4
|
Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
|
|
5
5
|
Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -23,9 +23,9 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
Requires-Dist: optio-core<0.5,>=0.4
|
|
24
24
|
Requires-Dist: optio-host<0.4,>=0.3
|
|
25
25
|
Requires-Dist: optio-agents<0.7,>=0.6
|
|
26
|
-
Requires-Dist: optio-claudecode<0.
|
|
27
|
-
Requires-Dist: optio-codex<0.
|
|
28
|
-
Requires-Dist: optio-grok<0.
|
|
26
|
+
Requires-Dist: optio-claudecode<0.7,>=0.6
|
|
27
|
+
Requires-Dist: optio-codex<0.5,>=0.4
|
|
28
|
+
Requires-Dist: optio-grok<0.5,>=0.4
|
|
29
29
|
Requires-Dist: asyncssh>=2.14
|
|
30
30
|
Requires-Dist: aiohttp>=3.9
|
|
31
31
|
Provides-Extra: dev
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
"""Tests for prompt composition."""
|
|
2
2
|
|
|
3
|
-
import pytest
|
|
4
|
-
|
|
5
3
|
from optio_opencode.prompt import compose_agents_md
|
|
6
4
|
|
|
7
5
|
|
|
8
6
|
def _compose(consumer="say hi", workdir_exclude=None, supports_resume=True):
|
|
9
|
-
"""Helper: call compose_agents_md with
|
|
7
|
+
"""Helper: call compose_agents_md with a default workdir_exclude/supports_resume."""
|
|
10
8
|
return compose_agents_md(
|
|
11
9
|
consumer,
|
|
12
10
|
workdir_exclude=workdir_exclude,
|
|
@@ -42,12 +40,6 @@ def test_compose_agents_md_empty_consumer_still_ends_cleanly():
|
|
|
42
40
|
assert out.endswith("\n")
|
|
43
41
|
|
|
44
42
|
|
|
45
|
-
def test_compose_agents_md_workdir_exclude_required():
|
|
46
|
-
"""workdir_exclude is mandatory — calling without it raises TypeError."""
|
|
47
|
-
with pytest.raises(TypeError):
|
|
48
|
-
compose_agents_md("hi") # type: ignore[call-arg]
|
|
49
|
-
|
|
50
|
-
|
|
51
43
|
def test_compose_agents_md_includes_resume_section_by_default():
|
|
52
44
|
"""Default supports_resume=True → resume section is present."""
|
|
53
45
|
out = _compose()
|
|
@@ -146,3 +138,21 @@ def test_default_composition_unchanged():
|
|
|
146
138
|
out = compose_agents_md("body", workdir_exclude=None)
|
|
147
139
|
assert "## Task" in out
|
|
148
140
|
assert "optio.log" in out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_sandbox_note_and_threaded_docs():
|
|
144
|
+
from optio_agents import get_protocol
|
|
145
|
+
docs = get_protocol(browser="suppress", client_messages=True).documentation
|
|
146
|
+
out = compose_agents_md(
|
|
147
|
+
"x", documentation=docs, fs_isolation_dirs=[("/wd", "rwx")],
|
|
148
|
+
)
|
|
149
|
+
assert "CLIENT_MESSAGE:" in out
|
|
150
|
+
assert "**Filesystem access:**" in out and "`/wd`" in out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_no_prompt_text_of_its_own():
|
|
154
|
+
import inspect
|
|
155
|
+
import optio_opencode.prompt as m
|
|
156
|
+
src = inspect.getsource(m)
|
|
157
|
+
assert "This harness may pause your session" not in src
|
|
158
|
+
assert "You are running inside a coordination harness" not in src
|
|
@@ -481,6 +481,7 @@ async def test_append_resume_log_entry_empty_refreshed_omits_tag(tmp_workdir):
|
|
|
481
481
|
async def test_maybe_refresh_on_resume_no_hook(tmp_workdir):
|
|
482
482
|
"""on_resume_refresh=None → returns [] and does not write AGENTS.md."""
|
|
483
483
|
import os
|
|
484
|
+
from optio_agents import get_protocol
|
|
484
485
|
from optio_host.host import LocalHost
|
|
485
486
|
from optio_opencode.session import _maybe_refresh_on_resume
|
|
486
487
|
from optio_opencode.types import OpencodeTaskConfig
|
|
@@ -492,7 +493,9 @@ async def test_maybe_refresh_on_resume_no_hook(tmp_workdir):
|
|
|
492
493
|
consumer_instructions="x", on_resume_refresh=None, fs_isolation=False,
|
|
493
494
|
)
|
|
494
495
|
|
|
495
|
-
refreshed = await _maybe_refresh_on_resume(
|
|
496
|
+
refreshed = await _maybe_refresh_on_resume(
|
|
497
|
+
host, None, config, get_protocol(browser="suppress"),
|
|
498
|
+
)
|
|
496
499
|
|
|
497
500
|
assert refreshed == []
|
|
498
501
|
assert not os.path.exists(os.path.join(host.workdir, "AGENTS.md"))
|
|
@@ -501,6 +504,7 @@ async def test_maybe_refresh_on_resume_no_hook(tmp_workdir):
|
|
|
501
504
|
async def test_maybe_refresh_on_resume_unchanged_content_skips_write(tmp_workdir):
|
|
502
505
|
"""Hook return identical to existing AGENTS.md → no write, [] returned."""
|
|
503
506
|
import os
|
|
507
|
+
from optio_agents import get_protocol
|
|
504
508
|
from optio_host.host import LocalHost
|
|
505
509
|
from optio_opencode.prompt import compose_agents_md
|
|
506
510
|
from optio_opencode.session import _maybe_refresh_on_resume
|
|
@@ -524,17 +528,63 @@ async def test_maybe_refresh_on_resume_unchanged_content_skips_write(tmp_workdir
|
|
|
524
528
|
with open(full) as f:
|
|
525
529
|
return f.read()
|
|
526
530
|
|
|
527
|
-
refreshed = await _maybe_refresh_on_resume(
|
|
531
|
+
refreshed = await _maybe_refresh_on_resume(
|
|
532
|
+
host, _FakeHookCtx(), config, get_protocol(browser="suppress"),
|
|
533
|
+
)
|
|
528
534
|
|
|
529
535
|
assert refreshed == []
|
|
530
536
|
mtime_after = os.path.getmtime(os.path.join(host.workdir, "AGENTS.md"))
|
|
531
537
|
assert mtime_after == mtime_before # write was skipped
|
|
532
538
|
|
|
533
539
|
|
|
540
|
+
async def test_maybe_refresh_on_resume_conversation_defaults_match_fresh(tmp_workdir):
|
|
541
|
+
"""Identity refresh of a conversation-mode session with empty instructions
|
|
542
|
+
must default/omit-task-frame exactly like the fresh-start path, so an
|
|
543
|
+
unchanged config produces no rewrite on resume."""
|
|
544
|
+
import os
|
|
545
|
+
from optio_agents import get_protocol
|
|
546
|
+
from optio_host.host import LocalHost
|
|
547
|
+
from optio_opencode.prompt import DEFAULT_CONVERSATION_INSTRUCTIONS, compose_agents_md
|
|
548
|
+
from optio_opencode.session import _maybe_refresh_on_resume
|
|
549
|
+
from optio_opencode.types import OpencodeTaskConfig
|
|
550
|
+
|
|
551
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
552
|
+
await host.setup_workdir()
|
|
553
|
+
# Identity on_resume_refresh (the default), conversation mode, no instructions.
|
|
554
|
+
config = OpencodeTaskConfig(
|
|
555
|
+
consumer_instructions="", mode="conversation", host_protocol=False,
|
|
556
|
+
fs_isolation=False,
|
|
557
|
+
)
|
|
558
|
+
# What the fresh-start path would have written for this config.
|
|
559
|
+
expected = compose_agents_md(
|
|
560
|
+
DEFAULT_CONVERSATION_INSTRUCTIONS,
|
|
561
|
+
workdir_exclude=config.workdir_exclude,
|
|
562
|
+
supports_resume=config.supports_resume,
|
|
563
|
+
host_protocol=config.host_protocol,
|
|
564
|
+
omit_task_framing=True,
|
|
565
|
+
)
|
|
566
|
+
await host.write_text("AGENTS.md", expected)
|
|
567
|
+
|
|
568
|
+
class _FakeHookCtx:
|
|
569
|
+
async def read_text_from_host(self, path, *, silent=False):
|
|
570
|
+
full = os.path.join(host.workdir, path)
|
|
571
|
+
with open(full) as f:
|
|
572
|
+
return f.read()
|
|
573
|
+
|
|
574
|
+
refreshed = await _maybe_refresh_on_resume(
|
|
575
|
+
host, _FakeHookCtx(), config, get_protocol(browser="suppress"),
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
assert refreshed == []
|
|
579
|
+
with open(os.path.join(host.workdir, "AGENTS.md")) as f:
|
|
580
|
+
assert f.read() == expected
|
|
581
|
+
|
|
582
|
+
|
|
534
583
|
async def test_maybe_refresh_on_resume_changed_content_writes(tmp_workdir):
|
|
535
584
|
"""Hook returns new instructions → AGENTS.md rewritten, ['AGENTS.md']."""
|
|
536
585
|
import os
|
|
537
586
|
from dataclasses import replace
|
|
587
|
+
from optio_agents import get_protocol
|
|
538
588
|
from optio_host.host import LocalHost
|
|
539
589
|
from optio_opencode.prompt import compose_agents_md
|
|
540
590
|
from optio_opencode.session import _maybe_refresh_on_resume
|
|
@@ -560,7 +610,9 @@ async def test_maybe_refresh_on_resume_changed_content_writes(tmp_workdir):
|
|
|
560
610
|
with open(full) as f:
|
|
561
611
|
return f.read()
|
|
562
612
|
|
|
563
|
-
refreshed = await _maybe_refresh_on_resume(
|
|
613
|
+
refreshed = await _maybe_refresh_on_resume(
|
|
614
|
+
host, _FakeHookCtx(), config, get_protocol(browser="suppress"),
|
|
615
|
+
)
|
|
564
616
|
|
|
565
617
|
assert refreshed == ["AGENTS.md"]
|
|
566
618
|
with open(os.path.join(host.workdir, "AGENTS.md")) as f:
|
|
@@ -572,6 +624,7 @@ async def test_maybe_refresh_on_resume_changed_content_writes(tmp_workdir):
|
|
|
572
624
|
async def test_maybe_refresh_on_resume_hook_raises_keeps_existing(tmp_workdir):
|
|
573
625
|
"""Hook that raises → returns [] and leaves existing AGENTS.md alone."""
|
|
574
626
|
import os
|
|
627
|
+
from optio_agents import get_protocol
|
|
575
628
|
from optio_host.host import LocalHost
|
|
576
629
|
from optio_opencode.session import _maybe_refresh_on_resume
|
|
577
630
|
from optio_opencode.types import OpencodeTaskConfig
|
|
@@ -586,13 +639,45 @@ async def test_maybe_refresh_on_resume_hook_raises_keeps_existing(tmp_workdir):
|
|
|
586
639
|
consumer_instructions="x", on_resume_refresh=_boom, fs_isolation=False,
|
|
587
640
|
)
|
|
588
641
|
|
|
589
|
-
refreshed = await _maybe_refresh_on_resume(
|
|
642
|
+
refreshed = await _maybe_refresh_on_resume(
|
|
643
|
+
host, None, config, get_protocol(browser="suppress"),
|
|
644
|
+
)
|
|
590
645
|
|
|
591
646
|
assert refreshed == []
|
|
592
647
|
with open(os.path.join(host.workdir, "AGENTS.md")) as f:
|
|
593
648
|
assert f.read() == "existing content"
|
|
594
649
|
|
|
595
650
|
|
|
651
|
+
async def test_maybe_refresh_on_resume_threads_session_docs(tmp_workdir):
|
|
652
|
+
"""The refreshed AGENTS.md carries the SESSION's protocol docs (client
|
|
653
|
+
messages documented when the task enables them), not rebuilt defaults."""
|
|
654
|
+
import os
|
|
655
|
+
from dataclasses import replace
|
|
656
|
+
from optio_agents import get_protocol
|
|
657
|
+
from optio_host.host import LocalHost
|
|
658
|
+
from optio_opencode.session import _maybe_refresh_on_resume
|
|
659
|
+
from optio_opencode.types import OpencodeTaskConfig
|
|
660
|
+
|
|
661
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
662
|
+
await host.setup_workdir()
|
|
663
|
+
config = OpencodeTaskConfig(
|
|
664
|
+
consumer_instructions="task X", fs_isolation=False, use_client_messages=True,
|
|
665
|
+
on_resume_refresh=lambda c: replace(c, consumer_instructions="task Y"),
|
|
666
|
+
)
|
|
667
|
+
await host.write_text("AGENTS.md", "stale")
|
|
668
|
+
|
|
669
|
+
class _FakeHookCtx:
|
|
670
|
+
async def read_text_from_host(self, path, *, silent=False):
|
|
671
|
+
with open(os.path.join(host.workdir, path)) as f:
|
|
672
|
+
return f.read()
|
|
673
|
+
|
|
674
|
+
protocol = get_protocol(browser="suppress", client_messages=True)
|
|
675
|
+
assert await _maybe_refresh_on_resume(host, _FakeHookCtx(), config, protocol) == ["AGENTS.md"]
|
|
676
|
+
with open(os.path.join(host.workdir, "AGENTS.md")) as f:
|
|
677
|
+
text = f.read()
|
|
678
|
+
assert "task Y" in text and "CLIENT_MESSAGE:" in text
|
|
679
|
+
|
|
680
|
+
|
|
596
681
|
async def test_session_local_supports_resume_false_skips_resume_log(
|
|
597
682
|
ctx_and_captures, _supply_scenario, tmp_workdir, monkeypatch,
|
|
598
683
|
):
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"""System-prompt composition for optio-opencode.
|
|
2
|
-
|
|
3
|
-
The base prompt teaches opencode (via AGENTS.md) how to coordinate with the
|
|
4
|
-
host harness: which log file to append status/deliverable/done/error lines
|
|
5
|
-
to, where to put deliverable files, and how the human expects to be
|
|
6
|
-
addressed. The consumer's own task description is then appended verbatim.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from optio_agents.prompt import downloadables_block
|
|
11
|
-
from optio_agents.protocol import ProtocolFeatures, build_log_channel_prompt
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
_OPENCODE_INTRO = """# Coordination protocol with the host harness
|
|
15
|
-
|
|
16
|
-
You are running inside a coordination harness. Follow these conventions
|
|
17
|
-
throughout the session.
|
|
18
|
-
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
DEFAULT_CONVERSATION_INSTRUCTIONS = "Let's have a conversation with the user."
|
|
23
|
-
|
|
24
|
-
_SYSTEM_PREFIX_EXPLAINER = """
|
|
25
|
-
(Messages prefixed `System:` on your input channel originate from the
|
|
26
|
-
harness coordinating this session, not from the human user.)
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
BASE_PROMPT_POST = """## Task
|
|
31
|
-
|
|
32
|
-
Here comes the description of your actual task to complete. Throughout
|
|
33
|
-
the task, you are encouraged to narrate progress — both on the normal
|
|
34
|
-
UI and in parallel using the `STATUS:` messages explained above — and
|
|
35
|
-
you are free to ask questions and dialogue with the human. They are
|
|
36
|
-
also working on the same task and will cooperate with you on achieving
|
|
37
|
-
the same goals. So:
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
RESUME_SECTION_TEMPLATE = """## Resumes
|
|
42
|
-
|
|
43
|
-
This harness may pause your session, save your context to a database,
|
|
44
|
-
terminate the underlying process, and later rehydrate it. From your
|
|
45
|
-
point of view the conversation is fully continuous — you keep your
|
|
46
|
-
prior context and will not "notice" the resume.
|
|
47
|
-
|
|
48
|
-
**A resume can happen at any point, not only at the start.** The host
|
|
49
|
-
environment may have changed across a resume — different host,
|
|
50
|
-
different running processes, files outside this workdir gone — even
|
|
51
|
-
though your context remembers everything as alive and well.
|
|
52
|
-
|
|
53
|
-
**The workdir (this directory) is preserved across resumes, with two
|
|
54
|
-
caveats:**
|
|
55
|
-
|
|
56
|
-
- {excludes_clause}
|
|
57
|
-
- **Anything outside the workdir is not preserved.**
|
|
58
|
-
|
|
59
|
-
{outside_clause}
|
|
60
|
-
|
|
61
|
-
### Detecting a resume: `resume.log`
|
|
62
|
-
|
|
63
|
-
Each session start (fresh or resumed) appends one line to
|
|
64
|
-
`./resume.log`. Line format:
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
<ISO 8601 UTC timestamp>[ REFRESHED:<comma-separated filenames>]
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
The very first line is the original launch timestamp; each subsequent
|
|
71
|
-
line is a resume. The optional `REFRESHED:` suffix signals that the
|
|
72
|
-
harness rewrote the listed files on that resume (e.g.
|
|
73
|
-
`2026-05-28T13:15:42Z REFRESHED:AGENTS.md`) — your in-memory copy of
|
|
74
|
-
those files is stale and must be re-read before continuing.
|
|
75
|
-
|
|
76
|
-
**At the start of every new incoming user message, read
|
|
77
|
-
`./resume.log` first.** Compare the latest line to the value you
|
|
78
|
-
remembered last time you checked. If a new line has appeared, treat
|
|
79
|
-
the situation as a resume:
|
|
80
|
-
|
|
81
|
-
- Verify any tools, processes, or files you previously gathered
|
|
82
|
-
outside the workdir are still where you left them.
|
|
83
|
-
- Re-establish anything that's gone (re-launch a server, re-fetch a
|
|
84
|
-
file, etc.) before continuing.
|
|
85
|
-
- **If the latest line carries a `REFRESHED:` suffix, re-read each
|
|
86
|
-
listed file** (e.g. `cat ./AGENTS.md`) — the harness updated it
|
|
87
|
-
since your last context snapshot and the version you remember is
|
|
88
|
-
out of date.
|
|
89
|
-
- Then resume the work you were doing.
|
|
90
|
-
|
|
91
|
-
If a resume slips past unnoticed, a failing tool call is the
|
|
92
|
-
next-best signal — re-check `./resume.log` then.
|
|
93
|
-
|
|
94
|
-
You may also be notified of a resume by a `System:` message on your input
|
|
95
|
-
channel; when you see one, follow the `resume.log` procedure above.
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _render_resume_section(workdir_exclude: list[str] | None) -> str:
|
|
100
|
-
"""Render the RESUME_SECTION_TEMPLATE with the effective exclude list."""
|
|
101
|
-
from optio_host.archive import DEFAULT_WORKDIR_EXCLUDES
|
|
102
|
-
effective = workdir_exclude if workdir_exclude is not None else DEFAULT_WORKDIR_EXCLUDES
|
|
103
|
-
if not effective:
|
|
104
|
-
excludes_clause = (
|
|
105
|
-
"**No paths are excluded** — every file in the workdir is preserved."
|
|
106
|
-
)
|
|
107
|
-
outside_clause = (
|
|
108
|
-
"If you need to stash large data, place it outside the workdir "
|
|
109
|
-
"(e.g. `/tmp/`) — but remember it may be missing when you next look."
|
|
110
|
-
)
|
|
111
|
-
else:
|
|
112
|
-
excludes_str = ", ".join(f"`{p}`" for p in effective)
|
|
113
|
-
excludes_clause = (
|
|
114
|
-
f"**Paths matching the snapshot exclude list are NOT preserved**, "
|
|
115
|
-
f"even inside the workdir. The current exclude list is: {excludes_str}."
|
|
116
|
-
)
|
|
117
|
-
outside_clause = (
|
|
118
|
-
"If you need to stash large data, place it outside the workdir "
|
|
119
|
-
"(e.g. `/tmp/`) or inside an excluded subdirectory — but remember "
|
|
120
|
-
"any such location may be missing when you next look."
|
|
121
|
-
)
|
|
122
|
-
return RESUME_SECTION_TEMPLATE.format(
|
|
123
|
-
excludes_clause=excludes_clause,
|
|
124
|
-
outside_clause=outside_clause,
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def compose_agents_md(
|
|
129
|
-
consumer_instructions: str,
|
|
130
|
-
*,
|
|
131
|
-
workdir_exclude: list[str] | None,
|
|
132
|
-
documentation: str | None = None,
|
|
133
|
-
supports_resume: bool = True,
|
|
134
|
-
host_protocol: bool = True,
|
|
135
|
-
omit_task_framing: bool = False,
|
|
136
|
-
file_download: bool = False,
|
|
137
|
-
) -> str:
|
|
138
|
-
"""Build the full AGENTS.md body.
|
|
139
|
-
|
|
140
|
-
Parameters:
|
|
141
|
-
consumer_instructions: the task author's prompt, appended verbatim.
|
|
142
|
-
workdir_exclude: the snapshot exclude list for this task. Mandatory:
|
|
143
|
-
callers must pass it explicitly to prevent silent desync between
|
|
144
|
-
archive.py defaults and the prompt's claims about what's preserved.
|
|
145
|
-
Pass None to render with the framework defaults.
|
|
146
|
-
documentation: the keyword-protocol block; the session passes
|
|
147
|
-
``get_protocol(browser="suppress").documentation``. Defaults (for
|
|
148
|
-
unit tests / standalone callers) to opencode's ``suppress`` docs.
|
|
149
|
-
supports_resume: when False, the resume-detection section is omitted
|
|
150
|
-
from the prompt. Default True.
|
|
151
|
-
|
|
152
|
-
``host_protocol=False`` omits the keyword-protocol documentation
|
|
153
|
-
block entirely (conversation mode without the optio.log channel);
|
|
154
|
-
the resume section then gains a self-contained ``System:`` explainer
|
|
155
|
-
that the protocol docs would otherwise provide.
|
|
156
|
-
|
|
157
|
-
``omit_task_framing=True`` drops the ``## Task`` framing block
|
|
158
|
-
(used when the conversation instructions were defaulted).
|
|
159
|
-
"""
|
|
160
|
-
if file_download:
|
|
161
|
-
consumer_instructions = (
|
|
162
|
-
consumer_instructions.rstrip() + downloadables_block(comparative=host_protocol)
|
|
163
|
-
)
|
|
164
|
-
if host_protocol:
|
|
165
|
-
if documentation is None:
|
|
166
|
-
documentation = build_log_channel_prompt(ProtocolFeatures(browser="suppress"))
|
|
167
|
-
base_prompt_pre = _OPENCODE_INTRO + documentation
|
|
168
|
-
else:
|
|
169
|
-
# Conversation mode without the optio.log channel: no keyword docs,
|
|
170
|
-
# and the intro that frames them is dropped too.
|
|
171
|
-
base_prompt_pre = ""
|
|
172
|
-
if supports_resume:
|
|
173
|
-
resume_block = _render_resume_section(workdir_exclude)
|
|
174
|
-
if not host_protocol:
|
|
175
|
-
# The protocol docs normally explain the `System:` convention;
|
|
176
|
-
# without them the resume section carries its own explainer.
|
|
177
|
-
resume_block = resume_block + _SYSTEM_PREFIX_EXPLAINER
|
|
178
|
-
resume_block = resume_block + "\n"
|
|
179
|
-
else:
|
|
180
|
-
resume_block = ""
|
|
181
|
-
body = consumer_instructions.rstrip()
|
|
182
|
-
pre = f"{base_prompt_pre}\n" if base_prompt_pre else ""
|
|
183
|
-
if omit_task_framing:
|
|
184
|
-
return f"{pre}{resume_block}{body}\n"
|
|
185
|
-
return f"{pre}{resume_block}{BASE_PROMPT_POST}\n{body}\n"
|
|
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
|
{optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/dependency_links.txt
RENAMED
|
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
|