optio-opencode 0.1.2__tar.gz → 0.1.4__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.1.2 → optio_opencode-0.1.4}/PKG-INFO +4 -3
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/pyproject.toml +4 -3
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode/__init__.py +1 -2
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode/prompt.py +23 -35
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode/session.py +63 -7
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode/types.py +13 -6
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode.egg-info/PKG-INFO +4 -3
- optio_opencode-0.1.4/src/optio_opencode.egg-info/requires.txt +8 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_host_local.py +3 -3
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_session_hooks.py +3 -3
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_session_local.py +179 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_session_resume.py +11 -3
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_smart_install.py +8 -8
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_types.py +13 -0
- optio_opencode-0.1.2/src/optio_opencode.egg-info/requires.txt +0 -7
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/README.md +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/setup.cfg +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode/host_actions.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_host_primitives_remote.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_prompt.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_sanity.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_session_blob_hooks.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.1.2 → optio_opencode-0.1.4}/tests/test_snapshots.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optio-opencode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -20,8 +20,9 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
20
20
|
Classifier: Framework :: AsyncIO
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
|
-
Requires-Dist: optio-core<0.
|
|
24
|
-
Requires-Dist: optio-host<0.
|
|
23
|
+
Requires-Dist: optio-core<0.3,>=0.2
|
|
24
|
+
Requires-Dist: optio-host<0.3,>=0.2
|
|
25
|
+
Requires-Dist: optio-agents<0.2,>=0.1
|
|
25
26
|
Requires-Dist: asyncssh>=2.14
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optio-opencode"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
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"
|
|
@@ -26,10 +26,11 @@ classifiers = [
|
|
|
26
26
|
"Framework :: AsyncIO",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
"optio-core>=0.
|
|
29
|
+
"optio-core>=0.2,<0.3",
|
|
30
30
|
# 0.1.1 introduces the bind_addr kwarg on Host.establish_tunnel —
|
|
31
31
|
# required for OPTIO_WIDGET_TUNNEL_BIND to work.
|
|
32
|
-
"optio-host>=0.
|
|
32
|
+
"optio-host>=0.2,<0.3",
|
|
33
|
+
"optio-agents>=0.1,<0.2",
|
|
33
34
|
"asyncssh>=2.14",
|
|
34
35
|
]
|
|
35
36
|
|
|
@@ -7,45 +7,20 @@ addressed. The consumer's own task description is then appended verbatim.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
from optio_agents.protocol.prompt import LOG_CHANNEL_PROMPT
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_OPENCODE_INTRO = """# Coordination protocol with the host (optio-opencode)
|
|
11
14
|
|
|
12
15
|
You are running inside a coordination harness. Follow these conventions
|
|
13
16
|
throughout the session.
|
|
14
17
|
|
|
15
|
-
## Log channel
|
|
16
|
-
|
|
17
|
-
Append one line per entry to `./optio.log` in this directory. Each line
|
|
18
|
-
must start with one of:
|
|
19
|
-
|
|
20
|
-
- `STATUS:` — progress update for the human. Optional leading percent,
|
|
21
|
-
e.g. `STATUS: 50% counting my fingers`.
|
|
22
|
-
- `DELIVERABLE:` — absolute or workdir-relative path to a file you've
|
|
23
|
-
just produced, e.g. `DELIVERABLE: ./deliverables/summary.md`.
|
|
24
|
-
- `DONE` — you have finished the task. May be followed by an optional
|
|
25
|
-
summary on the same line: `DONE: wrote the report`.
|
|
26
|
-
- `ERROR` — you cannot continue. May be followed by an optional
|
|
27
|
-
message: `ERROR: provider auth failed`.
|
|
28
|
-
|
|
29
|
-
**Every entry must end with a newline character (`\\n`).** The host
|
|
30
|
-
reads `optio.log` with a line-oriented tailer that only emits a line
|
|
31
|
-
once it sees `\\n`; an entry written without a trailing newline (e.g.
|
|
32
|
-
via `printf 'DONE'`) will be buffered indefinitely and never reach the
|
|
33
|
-
host. Use `echo`, `>>` redirection of a heredoc, or any other mechanism
|
|
34
|
-
that guarantees a trailing newline. If unsure, double-check with
|
|
35
|
-
`tail -c 1 ./optio.log` — the result must be a newline.
|
|
36
|
-
|
|
37
|
-
After writing `DONE` or `ERROR`, the session will terminate. Do not
|
|
38
|
-
write further lines.
|
|
39
|
-
|
|
40
|
-
## Deliverables
|
|
41
|
-
|
|
42
|
-
Place files you want to hand back to the host under `./deliverables/`.
|
|
43
|
-
For each file, write a `DELIVERABLE:` log line *after* the file exists
|
|
44
|
-
and its contents are final. The host fetches files by reading these
|
|
45
|
-
log lines.
|
|
46
18
|
"""
|
|
47
19
|
|
|
48
20
|
|
|
21
|
+
BASE_PROMPT_PRE = _OPENCODE_INTRO + LOG_CHANNEL_PROMPT
|
|
22
|
+
|
|
23
|
+
|
|
49
24
|
BASE_PROMPT_POST = """## Task
|
|
50
25
|
|
|
51
26
|
Here comes the description of your actual task to complete. Throughout
|
|
@@ -79,9 +54,18 @@ caveats:**
|
|
|
79
54
|
|
|
80
55
|
### Detecting a resume: `resume.log`
|
|
81
56
|
|
|
82
|
-
Each session start (fresh or resumed) appends one
|
|
83
|
-
|
|
84
|
-
|
|
57
|
+
Each session start (fresh or resumed) appends one line to
|
|
58
|
+
`./resume.log`. Line format:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
<ISO 8601 UTC timestamp>[ REFRESHED:<comma-separated filenames>]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The very first line is the original launch timestamp; each subsequent
|
|
65
|
+
line is a resume. The optional `REFRESHED:` suffix signals that the
|
|
66
|
+
harness rewrote the listed files on that resume (e.g.
|
|
67
|
+
`2026-05-28T13:15:42Z REFRESHED:AGENTS.md`) — your in-memory copy of
|
|
68
|
+
those files is stale and must be re-read before continuing.
|
|
85
69
|
|
|
86
70
|
**At the start of every new incoming user message, read
|
|
87
71
|
`./resume.log` first.** Compare the latest line to the value you
|
|
@@ -92,6 +76,10 @@ the situation as a resume:
|
|
|
92
76
|
outside the workdir are still where you left them.
|
|
93
77
|
- Re-establish anything that's gone (re-launch a server, re-fetch a
|
|
94
78
|
file, etc.) before continuing.
|
|
79
|
+
- **If the latest line carries a `REFRESHED:` suffix, re-read each
|
|
80
|
+
listed file** (e.g. `cat ./AGENTS.md`) — the harness updated it
|
|
81
|
+
since your last context snapshot and the version you remember is
|
|
82
|
+
out of date.
|
|
95
83
|
- Then resume the work you were doing.
|
|
96
84
|
|
|
97
85
|
If a resume slips past unnoticed, a failing tool call is the
|
|
@@ -7,7 +7,7 @@ Section 4 of the design spec. The public entry point is the factory
|
|
|
7
7
|
|
|
8
8
|
Most of the per-session work is generic log/deliverables protocol
|
|
9
9
|
plumbing (parse ``optio.log``, fetch deliverables, watch for cancel) and
|
|
10
|
-
lives in ``
|
|
10
|
+
lives in ``optio_agents.protocol.run_log_protocol_session``. This module
|
|
11
11
|
keeps only the opencode-specific work — write AGENTS.md / opencode.json,
|
|
12
12
|
install/launch the opencode binary, set up tunnel and widget, and the
|
|
13
13
|
resume/snapshot brackets around the protocol session.
|
|
@@ -29,10 +29,10 @@ from typing import AsyncIterator, Callable
|
|
|
29
29
|
from optio_core.context import ProcessContext
|
|
30
30
|
from optio_core.models import BasicAuth, TaskInstance
|
|
31
31
|
|
|
32
|
-
from
|
|
32
|
+
from optio_agents import HookContext
|
|
33
33
|
from optio_host.host import Host, LocalHost, ProcessHandle, RemoteHost
|
|
34
34
|
from optio_host.paths import task_dir
|
|
35
|
-
from
|
|
35
|
+
from optio_agents.protocol.session import _SessionFailed, run_log_protocol_session
|
|
36
36
|
from optio_opencode import host_actions
|
|
37
37
|
from optio_opencode.prompt import compose_agents_md
|
|
38
38
|
from optio_opencode.snapshots import (
|
|
@@ -160,6 +160,7 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
160
160
|
"""
|
|
161
161
|
nonlocal launched_handle, opencode_exec, session_id, preserved_session_id
|
|
162
162
|
|
|
163
|
+
refreshed_files: list[str] = []
|
|
163
164
|
if not resuming:
|
|
164
165
|
# Fresh start: the protocol driver has already created the
|
|
165
166
|
# workdir, deliverables/ subdir, and empty optio.log. Ensure
|
|
@@ -189,9 +190,14 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
189
190
|
# self-healing (snapshot lookup returns None → fresh-start
|
|
190
191
|
# fallback) handles the rare case where the flag is true but
|
|
191
192
|
# no snapshot exists.
|
|
193
|
+
else:
|
|
194
|
+
# Resume: when on_resume_refresh is wired, recompute AGENTS.md
|
|
195
|
+
# from the refreshed config and overwrite the workdir copy if
|
|
196
|
+
# the rendered text differs from the snapshot-restored file.
|
|
197
|
+
refreshed_files = await _maybe_refresh_on_resume(host, hook_ctx, config)
|
|
192
198
|
|
|
193
199
|
if config.supports_resume:
|
|
194
|
-
await _append_resume_log_entry(host)
|
|
200
|
+
await _append_resume_log_entry(host, refreshed=refreshed_files)
|
|
195
201
|
|
|
196
202
|
# opencode is already installed by run_opencode_session before
|
|
197
203
|
# this body runs (so resume restore can call opencode_import
|
|
@@ -469,16 +475,26 @@ async def _rotate_optio_log(host: Host) -> None:
|
|
|
469
475
|
await host.write_text("optio.log", "")
|
|
470
476
|
|
|
471
477
|
|
|
472
|
-
async def _append_resume_log_entry(
|
|
473
|
-
|
|
478
|
+
async def _append_resume_log_entry(
|
|
479
|
+
host, *, refreshed: list[str] | None = None,
|
|
480
|
+
) -> None:
|
|
481
|
+
"""Append one line to <workdir>/resume.log.
|
|
482
|
+
|
|
483
|
+
Line format: ``<ISO 8601 UTC timestamp>[ REFRESHED:<comma-separated names>]``.
|
|
484
|
+
The optional ``REFRESHED:`` suffix signals that the harness rewrote
|
|
485
|
+
the listed files on this session start. Agents are instructed (via
|
|
486
|
+
the resume section of AGENTS.md) to re-read tagged files.
|
|
474
487
|
|
|
475
488
|
Creates the file if missing (via shell `>>`). Caller is responsible
|
|
476
489
|
for gating this on config.supports_resume.
|
|
477
490
|
"""
|
|
478
491
|
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
492
|
+
line = ts
|
|
493
|
+
if refreshed:
|
|
494
|
+
line = f"{ts} REFRESHED:{','.join(refreshed)}"
|
|
479
495
|
target = f"{host.workdir}/resume.log"
|
|
480
496
|
result = await host.run_command(
|
|
481
|
-
f"echo {shlex.quote(
|
|
497
|
+
f"echo {shlex.quote(line)} >> {shlex.quote(target)}"
|
|
482
498
|
)
|
|
483
499
|
if result.exit_code != 0:
|
|
484
500
|
raise RuntimeError(
|
|
@@ -487,6 +503,46 @@ async def _append_resume_log_entry(host) -> None:
|
|
|
487
503
|
)
|
|
488
504
|
|
|
489
505
|
|
|
506
|
+
async def _maybe_refresh_on_resume(
|
|
507
|
+
host, hook_ctx, config: OpencodeTaskConfig,
|
|
508
|
+
) -> list[str]:
|
|
509
|
+
"""Run the on_resume_refresh hook (if any) and rewrite AGENTS.md when
|
|
510
|
+
the rendered content differs from the workdir copy.
|
|
511
|
+
|
|
512
|
+
Returns the list of filenames the harness rewrote (currently at most
|
|
513
|
+
``["AGENTS.md"]``), suitable for tagging the next ``resume.log`` line.
|
|
514
|
+
A hook that raises is logged and ignored — the resumed session keeps
|
|
515
|
+
whatever AGENTS.md the snapshot restored.
|
|
516
|
+
"""
|
|
517
|
+
if config.on_resume_refresh is None:
|
|
518
|
+
return []
|
|
519
|
+
try:
|
|
520
|
+
new_config = config.on_resume_refresh(config)
|
|
521
|
+
except Exception:
|
|
522
|
+
_LOG.exception(
|
|
523
|
+
"on_resume_refresh raised; keeping existing AGENTS.md from snapshot",
|
|
524
|
+
)
|
|
525
|
+
return []
|
|
526
|
+
new_agents_md = compose_agents_md(
|
|
527
|
+
new_config.consumer_instructions,
|
|
528
|
+
workdir_exclude=new_config.workdir_exclude,
|
|
529
|
+
supports_resume=new_config.supports_resume,
|
|
530
|
+
)
|
|
531
|
+
try:
|
|
532
|
+
existing = await hook_ctx.read_text_from_host("AGENTS.md", silent=True)
|
|
533
|
+
except FileNotFoundError:
|
|
534
|
+
existing = None
|
|
535
|
+
except Exception:
|
|
536
|
+
_LOG.exception(
|
|
537
|
+
"failed to read existing AGENTS.md on resume; rewriting unconditionally",
|
|
538
|
+
)
|
|
539
|
+
existing = None
|
|
540
|
+
if existing == new_agents_md:
|
|
541
|
+
return []
|
|
542
|
+
await host.write_text("AGENTS.md", new_agents_md)
|
|
543
|
+
return ["AGENTS.md"]
|
|
544
|
+
|
|
545
|
+
|
|
490
546
|
def _pick_local_workdir() -> str:
|
|
491
547
|
return tempfile.mkdtemp(prefix="optio-opencode-")
|
|
492
548
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
"""Public data types for optio-opencode consumers.
|
|
2
2
|
|
|
3
|
-
The generic ``DeliverableCallback`` / ``HookCallback`` types
|
|
4
|
-
``
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
imports keep working unchanged.
|
|
3
|
+
The generic ``DeliverableCallback`` / ``HookCallback`` types are owned by
|
|
4
|
+
``optio-agents`` (they describe the log/deliverables protocol); ``SSHConfig``
|
|
5
|
+
is owned by ``optio-host``. This module re-exports them so existing
|
|
6
|
+
``from optio_opencode.types import ...`` imports keep working unchanged.
|
|
8
7
|
"""
|
|
9
8
|
|
|
10
9
|
from dataclasses import dataclass, field
|
|
11
10
|
from typing import Any, Callable
|
|
12
11
|
|
|
13
|
-
from
|
|
12
|
+
from optio_agents.protocol.session import DeliverableCallback, HookCallback
|
|
14
13
|
from optio_host.types import SSHConfig
|
|
15
14
|
|
|
16
15
|
|
|
@@ -43,6 +42,14 @@ class OpencodeTaskConfig:
|
|
|
43
42
|
# raises a config error: asymmetric usage is always a mistake.
|
|
44
43
|
session_blob_encrypt: Callable[[bytes], bytes] | None = None
|
|
45
44
|
session_blob_decrypt: Callable[[bytes], bytes] | None = None
|
|
45
|
+
# Optional hook fired on resume only (never on fresh start). Receives
|
|
46
|
+
# the original task config; returns a (possibly mutated/replaced) config.
|
|
47
|
+
# The harness re-renders AGENTS.md from the returned config and writes
|
|
48
|
+
# it back to the workdir only when it differs from the file on disk.
|
|
49
|
+
# When written, the harness tags the new line in resume.log with
|
|
50
|
+
# `REFRESHED:AGENTS.md` so the agent knows to re-read. None (default)
|
|
51
|
+
# → no refresh; the resumed session keeps its original AGENTS.md.
|
|
52
|
+
on_resume_refresh: Callable[["OpencodeTaskConfig"], "OpencodeTaskConfig"] | None = None
|
|
46
53
|
|
|
47
54
|
def __post_init__(self) -> None:
|
|
48
55
|
e = self.session_blob_encrypt is not None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optio-opencode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -20,8 +20,9 @@ Classifier: Topic :: Software Development :: Code Generators
|
|
|
20
20
|
Classifier: Framework :: AsyncIO
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
|
-
Requires-Dist: optio-core<0.
|
|
24
|
-
Requires-Dist: optio-host<0.
|
|
23
|
+
Requires-Dist: optio-core<0.3,>=0.2
|
|
24
|
+
Requires-Dist: optio-host<0.3,>=0.2
|
|
25
|
+
Requires-Dist: optio-agents<0.2,>=0.1
|
|
25
26
|
Requires-Dist: asyncssh>=2.14
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -24,7 +24,7 @@ async def test_setup_workdir_creates_workdir(local_host):
|
|
|
24
24
|
assert os.path.isdir(local_host.workdir)
|
|
25
25
|
# As of the optio-host split, setup_workdir mkdirs the workdir only.
|
|
26
26
|
# The protocol-specific deliverables/ + optio.log are owned by the
|
|
27
|
-
# protocol session driver in
|
|
27
|
+
# protocol session driver in optio_agents.protocol.session.
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
@pytest.mark.asyncio
|
|
@@ -136,7 +136,7 @@ async def test_tail_file_yields_appended_lines(local_host):
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
async def test_fetch_deliverable_text(local_host):
|
|
139
|
-
from
|
|
139
|
+
from optio_agents.protocol.session import fetch_deliverable_text
|
|
140
140
|
await local_host.setup_workdir()
|
|
141
141
|
os.makedirs(os.path.join(local_host.workdir, "deliverables"), exist_ok=True)
|
|
142
142
|
target = os.path.join(local_host.workdir, "deliverables", "a.txt")
|
|
@@ -146,7 +146,7 @@ async def test_fetch_deliverable_text(local_host):
|
|
|
146
146
|
|
|
147
147
|
|
|
148
148
|
async def test_fetch_deliverable_non_utf8_raises(local_host):
|
|
149
|
-
from
|
|
149
|
+
from optio_agents.protocol.session import fetch_deliverable_text
|
|
150
150
|
await local_host.setup_workdir()
|
|
151
151
|
os.makedirs(os.path.join(local_host.workdir, "deliverables"), exist_ok=True)
|
|
152
152
|
target = os.path.join(local_host.workdir, "deliverables", "b.bin")
|
|
@@ -97,7 +97,7 @@ class _RecordingFakeHost:
|
|
|
97
97
|
return b""
|
|
98
98
|
|
|
99
99
|
async def run_command(self, *a, **kw):
|
|
100
|
-
from optio_host.
|
|
100
|
+
from optio_host.host import RunResult
|
|
101
101
|
self.timeline.append(f"run_command:{a[0]}")
|
|
102
102
|
return RunResult(stdout="", stderr="", exit_code=0)
|
|
103
103
|
|
|
@@ -255,8 +255,8 @@ async def test_on_deliverable_receives_hook_ctx_and_can_use_host_primitives(tmp_
|
|
|
255
255
|
)
|
|
256
256
|
# We don't run a full session here; we directly invoke
|
|
257
257
|
# _deliverable_fetch_loop with a constructed HookContext.
|
|
258
|
-
from
|
|
259
|
-
from
|
|
258
|
+
from optio_agents.protocol.session import _deliverable_fetch_loop
|
|
259
|
+
from optio_agents import HookContext
|
|
260
260
|
|
|
261
261
|
queue = asyncio.Queue()
|
|
262
262
|
await queue.put(("/wd/deliverables/x.txt", "x.txt"))
|
|
@@ -348,6 +348,185 @@ async def test_append_resume_log_entry_appends_on_repeat_call(tmp_workdir):
|
|
|
348
348
|
assert lines[0] != lines[1]
|
|
349
349
|
|
|
350
350
|
|
|
351
|
+
async def test_append_resume_log_entry_with_refreshed_tag(tmp_workdir):
|
|
352
|
+
"""Passing refreshed=[...] adds ` REFRESHED:<files>` suffix to the line."""
|
|
353
|
+
import os
|
|
354
|
+
import re
|
|
355
|
+
from optio_host.host import LocalHost
|
|
356
|
+
from optio_opencode.session import _append_resume_log_entry
|
|
357
|
+
|
|
358
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
359
|
+
await host.setup_workdir()
|
|
360
|
+
|
|
361
|
+
await _append_resume_log_entry(host, refreshed=["AGENTS.md"])
|
|
362
|
+
|
|
363
|
+
resume_log = os.path.join(host.workdir, "resume.log")
|
|
364
|
+
with open(resume_log) as f:
|
|
365
|
+
lines = [line for line in f.read().splitlines() if line]
|
|
366
|
+
assert len(lines) == 1
|
|
367
|
+
assert re.match(
|
|
368
|
+
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z REFRESHED:AGENTS\.md$",
|
|
369
|
+
lines[0],
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def test_append_resume_log_entry_with_multiple_refreshed_files(tmp_workdir):
|
|
374
|
+
"""refreshed=[a, b] → REFRESHED:a,b (comma-separated, no spaces)."""
|
|
375
|
+
import os
|
|
376
|
+
import re
|
|
377
|
+
from optio_host.host import LocalHost
|
|
378
|
+
from optio_opencode.session import _append_resume_log_entry
|
|
379
|
+
|
|
380
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
381
|
+
await host.setup_workdir()
|
|
382
|
+
|
|
383
|
+
await _append_resume_log_entry(host, refreshed=["AGENTS.md", "opencode.json"])
|
|
384
|
+
|
|
385
|
+
resume_log = os.path.join(host.workdir, "resume.log")
|
|
386
|
+
with open(resume_log) as f:
|
|
387
|
+
lines = [line for line in f.read().splitlines() if line]
|
|
388
|
+
assert len(lines) == 1
|
|
389
|
+
assert re.match(
|
|
390
|
+
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z REFRESHED:AGENTS\.md,opencode\.json$",
|
|
391
|
+
lines[0],
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
async def test_append_resume_log_entry_empty_refreshed_omits_tag(tmp_workdir):
|
|
396
|
+
"""refreshed=[] → bare timestamp, no REFRESHED: suffix (same as None)."""
|
|
397
|
+
import os
|
|
398
|
+
import re
|
|
399
|
+
from optio_host.host import LocalHost
|
|
400
|
+
from optio_opencode.session import _append_resume_log_entry
|
|
401
|
+
|
|
402
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
403
|
+
await host.setup_workdir()
|
|
404
|
+
|
|
405
|
+
await _append_resume_log_entry(host, refreshed=[])
|
|
406
|
+
|
|
407
|
+
resume_log = os.path.join(host.workdir, "resume.log")
|
|
408
|
+
with open(resume_log) as f:
|
|
409
|
+
lines = [line for line in f.read().splitlines() if line]
|
|
410
|
+
assert len(lines) == 1
|
|
411
|
+
assert re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$", lines[0])
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ---- resume refresh -------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def test_maybe_refresh_on_resume_no_hook(tmp_workdir):
|
|
418
|
+
"""on_resume_refresh=None → returns [] and does not write AGENTS.md."""
|
|
419
|
+
import os
|
|
420
|
+
from optio_host.host import LocalHost
|
|
421
|
+
from optio_opencode.session import _maybe_refresh_on_resume
|
|
422
|
+
from optio_opencode.types import OpencodeTaskConfig
|
|
423
|
+
|
|
424
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
425
|
+
await host.setup_workdir()
|
|
426
|
+
config = OpencodeTaskConfig(consumer_instructions="x")
|
|
427
|
+
|
|
428
|
+
refreshed = await _maybe_refresh_on_resume(host, None, config)
|
|
429
|
+
|
|
430
|
+
assert refreshed == []
|
|
431
|
+
assert not os.path.exists(os.path.join(host.workdir, "AGENTS.md"))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async def test_maybe_refresh_on_resume_unchanged_content_skips_write(tmp_workdir):
|
|
435
|
+
"""Hook return identical to existing AGENTS.md → no write, [] returned."""
|
|
436
|
+
import os
|
|
437
|
+
from optio_host.host import LocalHost
|
|
438
|
+
from optio_opencode.prompt import compose_agents_md
|
|
439
|
+
from optio_opencode.session import _maybe_refresh_on_resume
|
|
440
|
+
from optio_opencode.types import OpencodeTaskConfig
|
|
441
|
+
|
|
442
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
443
|
+
await host.setup_workdir()
|
|
444
|
+
config = OpencodeTaskConfig(
|
|
445
|
+
consumer_instructions="task X",
|
|
446
|
+
on_resume_refresh=lambda c: c,
|
|
447
|
+
)
|
|
448
|
+
expected = compose_agents_md(
|
|
449
|
+
config.consumer_instructions,
|
|
450
|
+
workdir_exclude=config.workdir_exclude,
|
|
451
|
+
supports_resume=config.supports_resume,
|
|
452
|
+
)
|
|
453
|
+
await host.write_text("AGENTS.md", expected)
|
|
454
|
+
mtime_before = os.path.getmtime(os.path.join(host.workdir, "AGENTS.md"))
|
|
455
|
+
|
|
456
|
+
class _FakeHookCtx:
|
|
457
|
+
async def read_text_from_host(self, path, *, silent=False):
|
|
458
|
+
full = os.path.join(host.workdir, path)
|
|
459
|
+
with open(full) as f:
|
|
460
|
+
return f.read()
|
|
461
|
+
|
|
462
|
+
refreshed = await _maybe_refresh_on_resume(host, _FakeHookCtx(), config)
|
|
463
|
+
|
|
464
|
+
assert refreshed == []
|
|
465
|
+
mtime_after = os.path.getmtime(os.path.join(host.workdir, "AGENTS.md"))
|
|
466
|
+
assert mtime_after == mtime_before # write was skipped
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
async def test_maybe_refresh_on_resume_changed_content_writes(tmp_workdir):
|
|
470
|
+
"""Hook returns new instructions → AGENTS.md rewritten, ['AGENTS.md']."""
|
|
471
|
+
import os
|
|
472
|
+
from dataclasses import replace
|
|
473
|
+
from optio_host.host import LocalHost
|
|
474
|
+
from optio_opencode.prompt import compose_agents_md
|
|
475
|
+
from optio_opencode.session import _maybe_refresh_on_resume
|
|
476
|
+
from optio_opencode.types import OpencodeTaskConfig
|
|
477
|
+
|
|
478
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
479
|
+
await host.setup_workdir()
|
|
480
|
+
config = OpencodeTaskConfig(
|
|
481
|
+
consumer_instructions="old task",
|
|
482
|
+
on_resume_refresh=lambda c: replace(c, consumer_instructions="new task"),
|
|
483
|
+
)
|
|
484
|
+
old_agents = compose_agents_md(
|
|
485
|
+
"old task",
|
|
486
|
+
workdir_exclude=config.workdir_exclude,
|
|
487
|
+
supports_resume=config.supports_resume,
|
|
488
|
+
)
|
|
489
|
+
await host.write_text("AGENTS.md", old_agents)
|
|
490
|
+
|
|
491
|
+
class _FakeHookCtx:
|
|
492
|
+
async def read_text_from_host(self, path, *, silent=False):
|
|
493
|
+
full = os.path.join(host.workdir, path)
|
|
494
|
+
with open(full) as f:
|
|
495
|
+
return f.read()
|
|
496
|
+
|
|
497
|
+
refreshed = await _maybe_refresh_on_resume(host, _FakeHookCtx(), config)
|
|
498
|
+
|
|
499
|
+
assert refreshed == ["AGENTS.md"]
|
|
500
|
+
with open(os.path.join(host.workdir, "AGENTS.md")) as f:
|
|
501
|
+
content = f.read()
|
|
502
|
+
assert "new task" in content
|
|
503
|
+
assert "old task" not in content
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
async def test_maybe_refresh_on_resume_hook_raises_keeps_existing(tmp_workdir):
|
|
507
|
+
"""Hook that raises → returns [] and leaves existing AGENTS.md alone."""
|
|
508
|
+
import os
|
|
509
|
+
from optio_host.host import LocalHost
|
|
510
|
+
from optio_opencode.session import _maybe_refresh_on_resume
|
|
511
|
+
from optio_opencode.types import OpencodeTaskConfig
|
|
512
|
+
|
|
513
|
+
def _boom(c):
|
|
514
|
+
raise RuntimeError("hook is broken")
|
|
515
|
+
|
|
516
|
+
host = LocalHost(taskdir=tmp_workdir)
|
|
517
|
+
await host.setup_workdir()
|
|
518
|
+
await host.write_text("AGENTS.md", "existing content")
|
|
519
|
+
config = OpencodeTaskConfig(
|
|
520
|
+
consumer_instructions="x", on_resume_refresh=_boom,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
refreshed = await _maybe_refresh_on_resume(host, None, config)
|
|
524
|
+
|
|
525
|
+
assert refreshed == []
|
|
526
|
+
with open(os.path.join(host.workdir, "AGENTS.md")) as f:
|
|
527
|
+
assert f.read() == "existing content"
|
|
528
|
+
|
|
529
|
+
|
|
351
530
|
async def test_session_local_supports_resume_false_skips_resume_log(
|
|
352
531
|
ctx_and_captures, _supply_scenario, tmp_workdir, monkeypatch,
|
|
353
532
|
):
|
|
@@ -184,7 +184,15 @@ async def test_resume_appends_second_line_to_resume_log(mongo_db, task_root):
|
|
|
184
184
|
lines = [line for line in contents.splitlines() if line]
|
|
185
185
|
assert len(lines) == 2, f"expected 2 lines, got {len(lines)}: {contents!r}"
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
# Line format: `<ISO 8601 timestamp>[ REFRESHED:<comma-separated names>]`.
|
|
188
|
+
# This test exercises the no-refresh path (no on_resume_refresh hook),
|
|
189
|
+
# so every line is a bare timestamp; the regex still accepts the
|
|
190
|
+
# extended form to stay valid as the format evolves.
|
|
191
|
+
line_re = re.compile(
|
|
192
|
+
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z(?: REFRESHED:\S+)?$"
|
|
193
|
+
)
|
|
188
194
|
for line in lines:
|
|
189
|
-
assert
|
|
190
|
-
|
|
195
|
+
assert line_re.match(line), f"unrecognized resume.log line: {line!r}"
|
|
196
|
+
# Timestamps (the leading token of each line) are monotonic.
|
|
197
|
+
timestamps = [line.split()[0] for line in lines]
|
|
198
|
+
assert timestamps[0] <= timestamps[1], f"timestamps not monotonic: {timestamps!r}"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
|
|
5
|
-
from optio_host.
|
|
5
|
+
from optio_host.host import RunResult
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class _FakeHost:
|
|
@@ -170,7 +170,7 @@ class _ExecutingFakeCtx:
|
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
async def test_install_opencode_from_zip_happy_path(zip_server, tmp_path):
|
|
173
|
-
from
|
|
173
|
+
from optio_agents import HookContext
|
|
174
174
|
from optio_host.host import LocalHost
|
|
175
175
|
from optio_opencode.host_actions import _install_opencode_from_zip
|
|
176
176
|
|
|
@@ -212,7 +212,7 @@ async def test_install_opencode_from_zip_happy_path(zip_server, tmp_path):
|
|
|
212
212
|
|
|
213
213
|
async def test_install_opencode_from_zip_cleans_up_tempdir(zip_server, tmp_path):
|
|
214
214
|
"""The temp dir created on the host should be removed after install."""
|
|
215
|
-
from
|
|
215
|
+
from optio_agents import HookContext
|
|
216
216
|
from optio_host.host import LocalHost
|
|
217
217
|
from optio_opencode.host_actions import _install_opencode_from_zip
|
|
218
218
|
|
|
@@ -264,7 +264,7 @@ async def test_install_opencode_from_zip_cleans_up_tempdir(zip_server, tmp_path)
|
|
|
264
264
|
|
|
265
265
|
async def test_ensure_opencode_installed_returns_existing_path_when_ok(monkeypatch):
|
|
266
266
|
"""When smart-install says 'ok', resolve the on-PATH path and return it."""
|
|
267
|
-
from
|
|
267
|
+
from optio_agents import HookContext
|
|
268
268
|
from optio_opencode import host_actions
|
|
269
269
|
|
|
270
270
|
async def stub_check(host, *, install_dir=None):
|
|
@@ -295,7 +295,7 @@ async def test_ensure_opencode_installed_returns_existing_path_when_ok(monkeypat
|
|
|
295
295
|
|
|
296
296
|
|
|
297
297
|
async def test_ensure_opencode_installed_raises_when_install_disabled(monkeypatch):
|
|
298
|
-
from
|
|
298
|
+
from optio_agents import HookContext
|
|
299
299
|
from optio_opencode import host_actions
|
|
300
300
|
|
|
301
301
|
async def stub_check(host, *, install_dir=None):
|
|
@@ -324,7 +324,7 @@ async def test_ensure_opencode_installed_installs_when_download_required(
|
|
|
324
324
|
monkeypatch, zip_server, tmp_path,
|
|
325
325
|
):
|
|
326
326
|
"""End-to-end with a real LocalHost + fake zip server: install path."""
|
|
327
|
-
from
|
|
327
|
+
from optio_agents import HookContext
|
|
328
328
|
from optio_host.host import LocalHost
|
|
329
329
|
from optio_opencode import host_actions
|
|
330
330
|
|
|
@@ -394,7 +394,7 @@ async def test_ensure_opencode_installed_respects_custom_install_dir(
|
|
|
394
394
|
):
|
|
395
395
|
"""End-to-end: an explicit ``install_dir`` flows through to the
|
|
396
396
|
install target and to the post-ok ``command -v`` PATH augmentation."""
|
|
397
|
-
from
|
|
397
|
+
from optio_agents import HookContext
|
|
398
398
|
from optio_host.host import LocalHost
|
|
399
399
|
from optio_opencode import host_actions
|
|
400
400
|
|
|
@@ -439,7 +439,7 @@ async def test_ensure_opencode_installed_default_install_dir_is_home_local_bin(
|
|
|
439
439
|
):
|
|
440
440
|
"""When ``install_dir`` is omitted, the default is
|
|
441
441
|
``<host_home>/.local/bin``."""
|
|
442
|
-
from
|
|
442
|
+
from optio_agents import HookContext
|
|
443
443
|
from optio_opencode import host_actions
|
|
444
444
|
|
|
445
445
|
fake_home = tmp_path / "home"
|
|
@@ -95,3 +95,16 @@ def test_opencode_task_config_supports_resume_default_true():
|
|
|
95
95
|
def test_opencode_task_config_supports_resume_can_be_disabled():
|
|
96
96
|
cfg = OpencodeTaskConfig(consumer_instructions="x", supports_resume=False)
|
|
97
97
|
assert cfg.supports_resume is False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_opencode_task_config_on_resume_refresh_default_none():
|
|
101
|
+
cfg = OpencodeTaskConfig(consumer_instructions="x")
|
|
102
|
+
assert cfg.on_resume_refresh is None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_opencode_task_config_on_resume_refresh_accepts_callable():
|
|
106
|
+
def _refresh(c):
|
|
107
|
+
return c
|
|
108
|
+
|
|
109
|
+
cfg = OpencodeTaskConfig(consumer_instructions="x", on_resume_refresh=_refresh)
|
|
110
|
+
assert cfg.on_resume_refresh is _refresh
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optio_opencode-0.1.2 → optio_opencode-0.1.4}/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
|