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.
Files changed (69) hide show
  1. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/PKG-INFO +4 -4
  2. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/pyproject.toml +4 -4
  3. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/conversation.py +3 -1
  4. optio_opencode-0.6.1/src/optio_opencode/prompt.py +48 -0
  5. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/session.py +18 -3
  6. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/PKG-INFO +4 -4
  7. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/SOURCES.txt +0 -1
  8. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/requires.txt +3 -3
  9. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_prompt.py +19 -9
  10. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_local.py +89 -4
  11. optio_opencode-0.5.0/src/optio_opencode/prompt.py +0 -185
  12. optio_opencode-0.5.0/tests/test_resume_sentence_opencode.py +0 -7
  13. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/README.md +0 -0
  14. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/setup.cfg +0 -0
  15. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/__init__.py +0 -0
  16. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/account.py +0 -0
  17. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/cred_watcher.py +0 -0
  18. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/fs_allowlist.py +0 -0
  19. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/host_actions.py +0 -0
  20. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/info.py +0 -0
  21. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/model_probe.py +0 -0
  22. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/__init__.py +0 -0
  23. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/anthropic.py +0 -0
  24. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/deepseek.py +0 -0
  25. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/openai.py +0 -0
  26. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/providers/xai.py +0 -0
  27. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/seed_manifest.py +0 -0
  28. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/snapshots.py +0 -0
  29. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/types.py +0 -0
  30. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode/verify.py +0 -0
  31. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
  32. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/src/optio_opencode.egg-info/top_level.txt +0 -0
  33. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_account.py +0 -0
  34. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_account_xai_apikey.py +0 -0
  35. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_agent_info.py +0 -0
  36. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_agent_sender_opencode.py +0 -0
  37. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_claustrum.py +0 -0
  38. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_config_harmonization.py +0 -0
  39. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_config.py +0 -0
  40. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_driver.py +0 -0
  41. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_session.py +0 -0
  42. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_ui_model.py +0 -0
  43. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_ui_session.py +0 -0
  44. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_conversation_upload.py +0 -0
  45. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_cred_watcher.py +0 -0
  46. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_file_download.py +0 -0
  47. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_file_upload.py +0 -0
  48. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_actions.py +0 -0
  49. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_local.py +0 -0
  50. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_primitives_local.py +0 -0
  51. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_primitives_remote.py +0 -0
  52. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_remote_resume.py +0 -0
  53. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_host_resume.py +0 -0
  54. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_model_probe.py +0 -0
  55. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_model_probe_wiring.py +0 -0
  56. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_provider_deepseek.py +0 -0
  57. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_purge_seed.py +0 -0
  58. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_sanity.py +0 -0
  59. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_seed_config.py +0 -0
  60. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_blob_hooks.py +0 -0
  61. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_hooks.py +0 -0
  62. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_remote.py +0 -0
  63. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_resume.py +0 -0
  64. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_seed.py +0 -0
  65. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_session_seed_saveback.py +0 -0
  66. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_smart_install.py +0 -0
  67. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_snapshots.py +0 -0
  68. {optio_opencode-0.5.0 → optio_opencode-0.6.1}/tests/test_types.py +0 -0
  69. {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.5.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.6,>=0.5
27
- Requires-Dist: optio-codex<0.4,>=0.3
28
- Requires-Dist: optio-grok<0.4,>=0.3
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.5.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.5,<0.6",
38
- "optio-codex>=0.3,<0.4",
39
- "optio-grok>=0.3,<0.4",
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
- new_config.consumer_instructions,
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.5.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.6,>=0.5
27
- Requires-Dist: optio-codex<0.4,>=0.3
28
- Requires-Dist: optio-grok<0.4,>=0.3
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
@@ -50,7 +50,6 @@ tests/test_model_probe_wiring.py
50
50
  tests/test_prompt.py
51
51
  tests/test_provider_deepseek.py
52
52
  tests/test_purge_seed.py
53
- tests/test_resume_sentence_opencode.py
54
53
  tests/test_sanity.py
55
54
  tests/test_seed_config.py
56
55
  tests/test_session_blob_hooks.py
@@ -1,9 +1,9 @@
1
1
  optio-core<0.5,>=0.4
2
2
  optio-host<0.4,>=0.3
3
3
  optio-agents<0.7,>=0.6
4
- optio-claudecode<0.6,>=0.5
5
- optio-codex<0.4,>=0.3
6
- optio-grok<0.4,>=0.3
4
+ optio-claudecode<0.7,>=0.6
5
+ optio-codex<0.5,>=0.4
6
+ optio-grok<0.5,>=0.4
7
7
  asyncssh>=2.14
8
8
  aiohttp>=3.9
9
9
 
@@ -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 the new mandatory args."""
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(host, None, config)
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(host, _FakeHookCtx(), config)
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(host, _FakeHookCtx(), config)
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(host, None, config)
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"
@@ -1,7 +0,0 @@
1
- from optio_opencode.prompt import _render_resume_section
2
-
3
-
4
- def test_resume_section_mentions_system_notification():
5
- section = _render_resume_section(None)
6
- assert "`System:` message" in section
7
- assert "resume.log" in section
File without changes
File without changes