agentic-workspace 1.0.0rc3__py3-none-win_amd64.whl
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.
- agentic_workspace/__init__.py +8 -0
- agentic_workspace/_binding.py +64 -0
- agentic_workspace/_native/agentic-workspace-core.exe +0 -0
- agentic_workspace/_native/agentic-workspace.exe +0 -0
- agentic_workspace/_native/artifact.json +1 -0
- agentic_workspace/cli.py +21 -0
- agentic_workspace/codex_provider.py +483 -0
- agentic_workspace/native_core.py +63 -0
- agentic_workspace/sealed_codex_transport.py +113 -0
- agentic_workspace-1.0.0rc3.dist-info/METADATA +211 -0
- agentic_workspace-1.0.0rc3.dist-info/RECORD +14 -0
- agentic_workspace-1.0.0rc3.dist-info/WHEEL +4 -0
- agentic_workspace-1.0.0rc3.dist-info/entry_points.txt +2 -0
- agentic_workspace-1.0.0rc3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""The installed Python projection of the native Agentic Workspace authority."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from ._binding import DecisionContractError, answer_carried, invoke, invoke_carried, resources, select_reference, start
|
|
6
|
+
|
|
7
|
+
__version__ = version("agentic-workspace")
|
|
8
|
+
__all__ = ["DecisionContractError", "answer_carried", "invoke", "invoke_carried", "resources", "select_reference", "start"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Private installed transport; ordinary meaning belongs to the paired Rust core."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .native_core import core_binary as native_core_binary
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DecisionContractError(ValueError):
|
|
13
|
+
"""Raised when the shared core rejects a source-decision request."""
|
|
14
|
+
|
|
15
|
+
def resources(context: Mapping[str, Any]) -> dict[str, Any]:
|
|
16
|
+
"""Bounded Rust-owned resource proposals and effects; no host policy reducer."""
|
|
17
|
+
return _request({"resources": context})
|
|
18
|
+
|
|
19
|
+
def start(context: Mapping[str, Any]) -> dict[str, Any]:
|
|
20
|
+
"""Consume repository sources through the native public owner boundary."""
|
|
21
|
+
return _request({"start": context})
|
|
22
|
+
|
|
23
|
+
def invoke(context: Mapping[str, Any]) -> dict[str, Any]:
|
|
24
|
+
"""Submit an exact public invocation to the same native owner boundary."""
|
|
25
|
+
return _request({"invoke": context})
|
|
26
|
+
|
|
27
|
+
def select_reference(context: Mapping[str, Any], reference: str, **material: Any) -> dict[str, Any]:
|
|
28
|
+
"""Forward an exact reference and optional bounded answer to Rust.
|
|
29
|
+
|
|
30
|
+
Context is copied, never retained as hidden session state. Rust alone
|
|
31
|
+
validates the answer and current owner identity.
|
|
32
|
+
"""
|
|
33
|
+
if set(material) - {"answer"}:
|
|
34
|
+
raise TypeError("select_reference accepts only answer material")
|
|
35
|
+
return start({**context, "reference": reference, **material})
|
|
36
|
+
|
|
37
|
+
def answer_carried(carriage: Mapping[str, Any], reference: str, answer: Any) -> dict[str, Any]:
|
|
38
|
+
"""Carry exact owner material; Rust binds only the returned bounded answer."""
|
|
39
|
+
return _request({"start": {"request": carriage, "reference": reference, "answer": answer, "projection": "carried"}})
|
|
40
|
+
|
|
41
|
+
def invoke_carried(carriage: Mapping[str, Any], reference: str) -> dict[str, Any]:
|
|
42
|
+
"""Execute one exact carried action, with native execution-time admission."""
|
|
43
|
+
return _request({"invoke": {"invocation": carriage, "reference": reference, "projection": "carried"}})
|
|
44
|
+
|
|
45
|
+
def _request(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
46
|
+
try:
|
|
47
|
+
binary = native_core_binary()
|
|
48
|
+
except (OSError, RuntimeError) as error:
|
|
49
|
+
raise DecisionContractError(str(error)) from error
|
|
50
|
+
completed = subprocess.run(
|
|
51
|
+
[str(binary)],
|
|
52
|
+
input=json.dumps(payload, separators=(",", ":")),
|
|
53
|
+
text=True,
|
|
54
|
+
encoding="utf-8",
|
|
55
|
+
capture_output=True,
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
if completed.returncode != 0:
|
|
59
|
+
try:
|
|
60
|
+
message = json.loads(completed.stderr)["error"]["message"]
|
|
61
|
+
except (KeyError, TypeError, json.JSONDecodeError):
|
|
62
|
+
message = completed.stderr.strip() or f"shared core exited with status {completed.returncode}"
|
|
63
|
+
raise DecisionContractError(str(message))
|
|
64
|
+
return dict(json.loads(completed.stdout))
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind": "agentic-workspace/native-python-artifact/v1", "package_version": "1.0.0rc3", "rust_host": "x86_64-pc-windows-msvc", "rust_target": "x86_64-pc-windows-msvc", "rust_toolchain": {"channel": "1.98.1", "declaration_sha256": "887f9be066a15585a2c583578e84b0fcb541126d81546276bad3d2ff00d61167", "release": "1.98.1", "commit_hash": "48a229ceaefd4985c50990b14116b6d856af0985", "commit_date": "2026-09-01", "host": "x86_64-pc-windows-msvc", "llvm_version": "22.1.8"}, "source_head": "85272df02be42707f6abd21cb120e25fffcd85f7", "source_dirty": false, "sha256": "5c436e86923fb8947bee4e7f1c3dbe478b9257a834aab9e2a95248bb113fcf67", "cli_sha256": "4792983d514835db22b8beb8cf9c4162c92bd9ef5fa65ad70eed8a1aeb100be9"}
|
agentic_workspace/cli.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from agentic_workspace.native_core import cli_binary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _run_cli(argv: list[str] | None = None) -> int:
|
|
10
|
+
"""Optional Python entry point; the paired Rust executable owns the command."""
|
|
11
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
12
|
+
try:
|
|
13
|
+
return subprocess.call([str(cli_binary()), *args])
|
|
14
|
+
except (OSError, RuntimeError) as exc:
|
|
15
|
+
print(str(exc), file=sys.stderr)
|
|
16
|
+
return 2
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
main = _run_cli
|
|
20
|
+
|
|
21
|
+
__all__ = ["main"]
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""Codex protocol I/O only; no Assignment policy, packet admission or repository semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import queue
|
|
9
|
+
import shutil
|
|
10
|
+
import signal
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from contextvars import ContextVar
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Iterator
|
|
19
|
+
|
|
20
|
+
_discovery_scope: ContextVar[dict[tuple[str, str], dict[str, Any]] | None] = ContextVar("native-discovery-scope", default=None)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@contextmanager
|
|
24
|
+
def discovery_scope() -> Iterator[None]:
|
|
25
|
+
"""Share one current snapshot within a bounded source-owner evaluation."""
|
|
26
|
+
token = _discovery_scope.set({})
|
|
27
|
+
try:
|
|
28
|
+
yield
|
|
29
|
+
finally:
|
|
30
|
+
_discovery_scope.reset(token)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def digest(value: Any) -> str:
|
|
34
|
+
return "sha256:" + hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _write(path: Path, value: Any) -> None:
|
|
38
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
40
|
+
temporary.write_text(json.dumps(value, sort_keys=True), encoding="utf-8")
|
|
41
|
+
temporary.replace(path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProviderError(ValueError):
|
|
45
|
+
def __init__(self, message: str, *, metrics: dict[str, Any] | None = None):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.metrics = metrics or {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CodexConnection:
|
|
51
|
+
"""One provider process. Only the provider owns conversation persistence."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, executable: str, *, timeout: float = 30, environment: dict[str, str] | None = None):
|
|
54
|
+
self.timeout = timeout
|
|
55
|
+
self.process = subprocess.Popen(
|
|
56
|
+
[executable, "app-server", "--stdio"],
|
|
57
|
+
stdin=subprocess.PIPE,
|
|
58
|
+
stdout=subprocess.PIPE,
|
|
59
|
+
stderr=subprocess.DEVNULL,
|
|
60
|
+
text=True,
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
env={**os.environ, **environment} if environment is not None else None,
|
|
63
|
+
start_new_session=os.name != "nt",
|
|
64
|
+
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0,
|
|
65
|
+
)
|
|
66
|
+
self.messages: queue.Queue[dict[str, Any]] = queue.Queue()
|
|
67
|
+
self.number = 0
|
|
68
|
+
self.events: list[dict[str, Any]] = []
|
|
69
|
+
threading.Thread(target=self._read, daemon=True).start()
|
|
70
|
+
try:
|
|
71
|
+
self.call("initialize", {"clientInfo": {"name": "aw-native-transport", "version": "1"}})
|
|
72
|
+
self.send({"method": "initialized"})
|
|
73
|
+
except Exception:
|
|
74
|
+
self.close()
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
def _read(self) -> None:
|
|
78
|
+
assert self.process.stdout is not None
|
|
79
|
+
for line in self.process.stdout:
|
|
80
|
+
try:
|
|
81
|
+
value = json.loads(line)
|
|
82
|
+
except ValueError:
|
|
83
|
+
continue
|
|
84
|
+
# Drop reasoning, deltas, commands and history at the adapter ingress.
|
|
85
|
+
if "id" in value or value.get("method") in {"item/completed", "turn/completed", "thread/tokenUsage/updated", "error"}:
|
|
86
|
+
if value.get("method") == "item/completed" and value.get("params", {}).get("item", {}).get("type") != "agentMessage":
|
|
87
|
+
continue
|
|
88
|
+
self.messages.put(value)
|
|
89
|
+
self.messages.put({"adapter_closed": True})
|
|
90
|
+
|
|
91
|
+
def send(self, payload: dict[str, Any]) -> None:
|
|
92
|
+
assert self.process.stdin is not None
|
|
93
|
+
self.process.stdin.write(json.dumps(payload) + "\n")
|
|
94
|
+
self.process.stdin.flush()
|
|
95
|
+
|
|
96
|
+
def next(self, timeout: float | None = None) -> dict[str, Any]:
|
|
97
|
+
try:
|
|
98
|
+
item = self.messages.get(timeout=self.timeout if timeout is None else timeout)
|
|
99
|
+
except queue.Empty as error:
|
|
100
|
+
raise ProviderError("provider-response-timeout") from error
|
|
101
|
+
if item.get("adapter_closed"):
|
|
102
|
+
raise ProviderError("provider-process-closed")
|
|
103
|
+
# This adapter does not admit interactive approvals or tool execution
|
|
104
|
+
# outside its fixed read-only sandbox. Deny provider requests explicitly.
|
|
105
|
+
if "id" in item and "method" in item:
|
|
106
|
+
self.send({"id": item["id"], "error": {"code": -32601, "message": "Unsupported by bounded read-only adapter"}})
|
|
107
|
+
return self.next(timeout)
|
|
108
|
+
return item
|
|
109
|
+
|
|
110
|
+
def call(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
|
|
111
|
+
self.number += 1
|
|
112
|
+
number = self.number
|
|
113
|
+
self.send({"id": number, "method": method, "params": params})
|
|
114
|
+
deadline = time.monotonic() + self.timeout
|
|
115
|
+
while time.monotonic() < deadline:
|
|
116
|
+
message = self.next(max(0.01, deadline - time.monotonic()))
|
|
117
|
+
if message.get("id") == number:
|
|
118
|
+
if "error" in message:
|
|
119
|
+
if (
|
|
120
|
+
method == "thread/archive"
|
|
121
|
+
and message["error"].get("message") == f"no rollout found for thread id {params.get('threadId')}"
|
|
122
|
+
):
|
|
123
|
+
raise ProviderError("native-active-thread-unavailable")
|
|
124
|
+
if (
|
|
125
|
+
method in {"thread/resume", "thread/fork"}
|
|
126
|
+
and message["error"].get("message") == f"no rollout found for thread id {params.get('threadId')}"
|
|
127
|
+
):
|
|
128
|
+
raise ProviderError("native-continuation-unavailable")
|
|
129
|
+
# No prompt or provider payload is retained in an error.
|
|
130
|
+
raise ProviderError(f"provider-operation-rejected:{method}:{message['error'].get('code')}")
|
|
131
|
+
return message["result"]
|
|
132
|
+
self.events.append(message)
|
|
133
|
+
raise ProviderError("provider-response-timeout")
|
|
134
|
+
|
|
135
|
+
def close(self) -> None:
|
|
136
|
+
if self.process.stdin is not None and not self.process.stdin.closed:
|
|
137
|
+
self.process.stdin.close()
|
|
138
|
+
try:
|
|
139
|
+
self.process.wait(timeout=5)
|
|
140
|
+
except subprocess.TimeoutExpired:
|
|
141
|
+
if os.name == "nt":
|
|
142
|
+
# The installed CLI may be a cmd/node launcher. Terminating
|
|
143
|
+
# only that wrapper does not prove its worker has stopped.
|
|
144
|
+
killed = subprocess.run(
|
|
145
|
+
["taskkill", "/PID", str(self.process.pid), "/T", "/F"],
|
|
146
|
+
capture_output=True,
|
|
147
|
+
timeout=10,
|
|
148
|
+
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
149
|
+
check=False,
|
|
150
|
+
)
|
|
151
|
+
if killed.returncode != 0:
|
|
152
|
+
raise ProviderError("native-process-tree-release-unconfirmed")
|
|
153
|
+
self.process.wait(timeout=5)
|
|
154
|
+
else:
|
|
155
|
+
try:
|
|
156
|
+
os.killpg(self.process.pid, signal.SIGKILL)
|
|
157
|
+
except ProcessLookupError:
|
|
158
|
+
pass
|
|
159
|
+
self.process.wait(timeout=5)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def discover(
|
|
163
|
+
root: Path, *, executable: str = "codex", refresh: bool = False, now: float | None = None, persist: bool = True
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
"""Local version signal plus bounded remote/account capability refresh."""
|
|
166
|
+
clock = time.time() if now is None else now
|
|
167
|
+
scope = _discovery_scope.get()
|
|
168
|
+
key = (str(root.resolve()), executable)
|
|
169
|
+
scoped = scope.get(key, {}) if scope is not None else {}
|
|
170
|
+
if not refresh and clock < scoped.get("expires_at", 0) and not scoped.get("invalidated"):
|
|
171
|
+
return scoped
|
|
172
|
+
resolved = shutil.which(executable)
|
|
173
|
+
if not resolved:
|
|
174
|
+
raise ProviderError("native-adapter-executable-unavailable")
|
|
175
|
+
version = subprocess.run([resolved, "--version"], capture_output=True, text=True, timeout=10, check=True).stdout.strip()
|
|
176
|
+
adapter_revision = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
|
|
177
|
+
identity = digest({"adapter": "codex-app-server/v1", "implementation": adapter_revision, "executable": resolved, "version": version})
|
|
178
|
+
path = root / ".agentic-workspace/local/transport-capabilities" / f"{identity[7:]}.json"
|
|
179
|
+
try:
|
|
180
|
+
cached = json.loads(path.read_text(encoding="utf-8"))
|
|
181
|
+
except (OSError, ValueError):
|
|
182
|
+
cached = {}
|
|
183
|
+
if not refresh and cached.get("identity") == identity and clock < cached.get("expires_at", 0) and not cached.get("invalidated"):
|
|
184
|
+
if scope is not None:
|
|
185
|
+
scope[key] = cached
|
|
186
|
+
return cached
|
|
187
|
+
# Discover actual installed protocol properties. In particular excludeTurns
|
|
188
|
+
# is required: AW never requests the provider's persisted conversation.
|
|
189
|
+
with tempfile.TemporaryDirectory(prefix="aw-provider-schema-") as directory:
|
|
190
|
+
subprocess.run([resolved, "app-server", "generate-json-schema", "--out", directory], capture_output=True, timeout=20, check=True)
|
|
191
|
+
schemas = {}
|
|
192
|
+
for name in (
|
|
193
|
+
"ThreadStartParams",
|
|
194
|
+
"ThreadResumeParams",
|
|
195
|
+
"ThreadForkParams",
|
|
196
|
+
"TurnStartParams",
|
|
197
|
+
"ThreadArchiveParams",
|
|
198
|
+
"ConfigReadParams",
|
|
199
|
+
):
|
|
200
|
+
schema_path = Path(directory) / "v2" / f"{name}.json"
|
|
201
|
+
if schema_path.is_file():
|
|
202
|
+
schemas[name] = json.loads(schema_path.read_text())["properties"]
|
|
203
|
+
turn_supported = {"threadId", "input", "outputSchema", "model", "approvalPolicy"} <= schemas.get("TurnStartParams", {}).keys()
|
|
204
|
+
thread_fields = {"cwd", "model", "sandbox", "approvalPolicy"}
|
|
205
|
+
modes = ["fresh"] if turn_supported and thread_fields <= schemas.get("ThreadStartParams", {}).keys() else []
|
|
206
|
+
for mode, name in (("resume", "ThreadResumeParams"), ("fork", "ThreadForkParams")):
|
|
207
|
+
if turn_supported and (thread_fields | {"excludeTurns", "threadId"}) <= schemas.get(name, {}).keys():
|
|
208
|
+
modes.append(mode)
|
|
209
|
+
if "resume" in modes:
|
|
210
|
+
modes.append("restart")
|
|
211
|
+
connection = CodexConnection(resolved)
|
|
212
|
+
try:
|
|
213
|
+
result = connection.call("model/list", {"limit": 100})
|
|
214
|
+
effective_effort = None
|
|
215
|
+
effective_settings_known = {"cwd", "includeLayers"} <= schemas.get("ConfigReadParams", {}).keys()
|
|
216
|
+
if effective_settings_known:
|
|
217
|
+
effective = connection.call("config/read", {"includeLayers": False, "cwd": str(root)})
|
|
218
|
+
effective_effort = effective.get("config", {}).get("model_reasoning_effort")
|
|
219
|
+
finally:
|
|
220
|
+
connection.close()
|
|
221
|
+
if result.get("nextCursor"):
|
|
222
|
+
raise ProviderError("provider-catalog-exceeds-bounded-discovery")
|
|
223
|
+
catalog = [
|
|
224
|
+
{
|
|
225
|
+
"model": m["model"],
|
|
226
|
+
"reasoning_efforts": [item["reasoningEffort"] for item in m.get("supportedReasoningEfforts", [])],
|
|
227
|
+
"default_reasoning_effort": m.get("defaultReasoningEffort"),
|
|
228
|
+
}
|
|
229
|
+
for m in result["data"]
|
|
230
|
+
if not m.get("hidden")
|
|
231
|
+
]
|
|
232
|
+
facts = {
|
|
233
|
+
"adapter": "codex-app-server/v1",
|
|
234
|
+
"implementation_revision": adapter_revision,
|
|
235
|
+
"executable": resolved,
|
|
236
|
+
"version": version,
|
|
237
|
+
"modes": modes,
|
|
238
|
+
"models": catalog,
|
|
239
|
+
"effective_reasoning_effort": effective_effort,
|
|
240
|
+
"effective_settings_known": effective_settings_known,
|
|
241
|
+
"protocol_revision": digest(schemas),
|
|
242
|
+
"parameters": ["model", "timeout_seconds"]
|
|
243
|
+
+ (["reasoning_effort"] if "effort" in schemas.get("TurnStartParams", {}) else [])
|
|
244
|
+
+ (["ephemeral"] if "ephemeral" in schemas.get("ThreadStartParams", {}) else []),
|
|
245
|
+
"ephemeral_modes": [
|
|
246
|
+
mode
|
|
247
|
+
for mode, name in (("fresh", "ThreadStartParams"), ("fork", "ThreadForkParams"))
|
|
248
|
+
if mode in modes and "ephemeral" in schemas.get(name, {})
|
|
249
|
+
],
|
|
250
|
+
"archive_supported": "threadId" in schemas.get("ThreadArchiveParams", {}),
|
|
251
|
+
}
|
|
252
|
+
snapshot = {
|
|
253
|
+
**facts,
|
|
254
|
+
"identity": identity,
|
|
255
|
+
"revision": digest(facts),
|
|
256
|
+
"observed_at": clock,
|
|
257
|
+
"expires_at": clock + 900,
|
|
258
|
+
"live_worker_continuity": False,
|
|
259
|
+
"exclusive_writer": True,
|
|
260
|
+
"process_lifetime": "one-dispatch",
|
|
261
|
+
"conversation_lifetime": "provider-owned",
|
|
262
|
+
"source": "installed-protocol-and-model-list",
|
|
263
|
+
}
|
|
264
|
+
if persist:
|
|
265
|
+
_write(path, snapshot)
|
|
266
|
+
if scope is not None:
|
|
267
|
+
scope[key] = snapshot
|
|
268
|
+
return snapshot
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def validate_selection(snapshot: dict[str, Any], selection: dict[str, Any], *, now: float | None = None) -> None:
|
|
272
|
+
clock = time.time() if now is None else now
|
|
273
|
+
if snapshot.get("invalidated") or clock >= snapshot["expires_at"] or selection.get("capability_revision") != snapshot["revision"]:
|
|
274
|
+
raise ProviderError("native-capability-not-current")
|
|
275
|
+
if set(selection) - {"capability_revision", "mode", "reference", "parameters", "semantic_scope", "lineage_revision"}:
|
|
276
|
+
raise ProviderError("unsupported-native-selection-field")
|
|
277
|
+
mode = selection.get("mode")
|
|
278
|
+
if mode not in snapshot["modes"]:
|
|
279
|
+
raise ProviderError("native-continuity-unavailable")
|
|
280
|
+
if mode != "fresh" and not selection.get("reference"):
|
|
281
|
+
raise ProviderError("native-continuation-reference-required")
|
|
282
|
+
if mode == "fresh" and selection.get("reference"):
|
|
283
|
+
raise ProviderError("fresh-cannot-inherit-reference")
|
|
284
|
+
validate_parameters(snapshot, mode, selection.get("parameters", {}))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def validate_parameters(snapshot: dict[str, Any], mode: str, parameters: dict[str, Any]) -> None:
|
|
288
|
+
if set(parameters) - set(snapshot.get("parameters", ["model"])):
|
|
289
|
+
raise ProviderError("native-parameter-unsupported")
|
|
290
|
+
if "ephemeral" in parameters and not isinstance(parameters["ephemeral"], bool):
|
|
291
|
+
raise ProviderError("native-parameter-unsupported")
|
|
292
|
+
if parameters.get("ephemeral") and mode not in snapshot.get("ephemeral_modes", []):
|
|
293
|
+
raise ProviderError("native-ephemeral-continuity-unavailable")
|
|
294
|
+
model = next((m for m in snapshot["models"] if m["model"] == parameters.get("model")), None)
|
|
295
|
+
if model is None:
|
|
296
|
+
raise ProviderError("native-model-unavailable")
|
|
297
|
+
if "reasoning_effort" in parameters and parameters["reasoning_effort"] not in model["reasoning_efforts"]:
|
|
298
|
+
raise ProviderError("native-parameter-unsupported")
|
|
299
|
+
if "timeout_seconds" in parameters:
|
|
300
|
+
timeout = parameters["timeout_seconds"]
|
|
301
|
+
if not isinstance(timeout, int) or isinstance(timeout, bool) or not 1 <= timeout <= 1800:
|
|
302
|
+
raise ProviderError("native-parameter-unsupported")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@contextmanager
|
|
306
|
+
def exclusive_lineage(reference: str) -> Iterator[None]:
|
|
307
|
+
"""Cross-worktree OS lock; released on process death, never guessed from TTL."""
|
|
308
|
+
directory = Path(tempfile.gettempdir()) / "agentic-workspace-provider-locks"
|
|
309
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
path = directory / (hashlib.sha256(("codex:" + reference).encode()).hexdigest() + ".lock")
|
|
311
|
+
with path.open("a+b") as lock:
|
|
312
|
+
try:
|
|
313
|
+
if path.stat().st_size == 0:
|
|
314
|
+
lock.write(b"0")
|
|
315
|
+
lock.flush()
|
|
316
|
+
lock.seek(0)
|
|
317
|
+
if os.name == "nt":
|
|
318
|
+
import msvcrt
|
|
319
|
+
|
|
320
|
+
msvcrt.locking(lock.fileno(), msvcrt.LK_NBLCK, 1)
|
|
321
|
+
else:
|
|
322
|
+
import fcntl
|
|
323
|
+
|
|
324
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
325
|
+
except OSError as error:
|
|
326
|
+
raise ProviderError("native-lineage-exclusive-writer-busy") from error
|
|
327
|
+
try:
|
|
328
|
+
yield
|
|
329
|
+
finally:
|
|
330
|
+
lock.seek(0)
|
|
331
|
+
if os.name == "nt":
|
|
332
|
+
import msvcrt
|
|
333
|
+
|
|
334
|
+
msvcrt.locking(lock.fileno(), msvcrt.LK_UNLCK, 1)
|
|
335
|
+
else:
|
|
336
|
+
import fcntl
|
|
337
|
+
|
|
338
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def execute(
|
|
342
|
+
root: Path,
|
|
343
|
+
snapshot: dict[str, Any],
|
|
344
|
+
selection: dict[str, Any],
|
|
345
|
+
prompt: str,
|
|
346
|
+
schema: dict[str, Any],
|
|
347
|
+
*,
|
|
348
|
+
timeout: int = 1800,
|
|
349
|
+
on_thread: Callable[[str], None] | None = None,
|
|
350
|
+
worker_environment: dict[str, str] | None = None,
|
|
351
|
+
on_closed: Callable[[], None] | None = None,
|
|
352
|
+
on_history: Callable[[bool], None] | None = None,
|
|
353
|
+
) -> dict[str, Any]:
|
|
354
|
+
"""Run an exact native topology; missing provider state never becomes fresh."""
|
|
355
|
+
validate_selection(snapshot, selection)
|
|
356
|
+
reference = selection.get("reference")
|
|
357
|
+
mode = selection["mode"]
|
|
358
|
+
started = time.monotonic()
|
|
359
|
+
metrics: dict[str, Any] = {"kind": "agentic-workspace/assignment-transport-metrics/v1"}
|
|
360
|
+
usage_current = True
|
|
361
|
+
deadline = None
|
|
362
|
+
with exclusive_lineage(reference or "fresh:" + os.urandom(16).hex()):
|
|
363
|
+
connection = (
|
|
364
|
+
CodexConnection(snapshot["executable"], environment=worker_environment)
|
|
365
|
+
if worker_environment is not None
|
|
366
|
+
else CodexConnection(snapshot["executable"])
|
|
367
|
+
)
|
|
368
|
+
try:
|
|
369
|
+
method = {"fresh": "thread/start", "resume": "thread/resume", "restart": "thread/resume", "fork": "thread/fork"}[mode]
|
|
370
|
+
params = {"cwd": str(root), "model": selection["parameters"]["model"], "sandbox": "read-only", "approvalPolicy": "never"}
|
|
371
|
+
ephemeral = selection["parameters"].get("ephemeral", False)
|
|
372
|
+
if mode in {"fresh", "fork"} and "ephemeral" in selection["parameters"]:
|
|
373
|
+
params["ephemeral"] = ephemeral
|
|
374
|
+
if reference:
|
|
375
|
+
params.update({"threadId": reference, "excludeTurns": True})
|
|
376
|
+
result = connection.call(method, params)
|
|
377
|
+
thread = result["thread"]
|
|
378
|
+
actual = thread["id"]
|
|
379
|
+
if mode in {"resume", "restart"} and actual != reference:
|
|
380
|
+
raise ProviderError("provider-continuation-identity-mismatch")
|
|
381
|
+
if mode == "fork" and actual == reference:
|
|
382
|
+
raise ProviderError("provider-fork-lineage-not-independent")
|
|
383
|
+
if on_thread is not None:
|
|
384
|
+
on_thread(actual)
|
|
385
|
+
if on_history is not None and isinstance(thread.get("ephemeral"), bool):
|
|
386
|
+
on_history(thread["ephemeral"])
|
|
387
|
+
if ephemeral and thread.get("ephemeral") is not True:
|
|
388
|
+
raise ProviderError("provider-ephemeral-guarantee-not-enforced")
|
|
389
|
+
if not ephemeral and thread.get("ephemeral") is True:
|
|
390
|
+
raise ProviderError("provider-persistence-guarantee-not-enforced")
|
|
391
|
+
if "ephemeral" in selection["parameters"] and thread.get("ephemeral") is not ephemeral:
|
|
392
|
+
raise ProviderError("provider-history-guarantee-unconfirmed")
|
|
393
|
+
turn_params = {
|
|
394
|
+
"threadId": actual,
|
|
395
|
+
"input": [{"type": "text", "text": prompt}],
|
|
396
|
+
"outputSchema": schema,
|
|
397
|
+
"model": selection["parameters"]["model"],
|
|
398
|
+
"approvalPolicy": "never",
|
|
399
|
+
}
|
|
400
|
+
if selection["parameters"].get("reasoning_effort"):
|
|
401
|
+
turn_params["effort"] = selection["parameters"]["reasoning_effort"]
|
|
402
|
+
turn = connection.call("turn/start", turn_params)["turn"]["id"]
|
|
403
|
+
output = ""
|
|
404
|
+
deadline = time.monotonic() + timeout
|
|
405
|
+
while time.monotonic() < deadline:
|
|
406
|
+
event = connection.events.pop(0) if connection.events else connection.next(max(0.01, deadline - time.monotonic()))
|
|
407
|
+
payload = event.get("params", {})
|
|
408
|
+
if payload.get("threadId") != actual:
|
|
409
|
+
continue
|
|
410
|
+
if event.get("method") == "thread/tokenUsage/updated" and payload.get("turnId") == turn and mode == "fresh":
|
|
411
|
+
# `last` covers one model response, not the entire tool loop.
|
|
412
|
+
# Fresh has a known zero lineage baseline. Reused contexts
|
|
413
|
+
# lack a proven pre-turn baseline, so their total is unknown.
|
|
414
|
+
usage = payload.get("tokenUsage", {}).get("total", {})
|
|
415
|
+
observed: dict[str, int] = {}
|
|
416
|
+
for source, target in (
|
|
417
|
+
("inputTokens", "effective_input_tokens"),
|
|
418
|
+
("cachedInputTokens", "cached_input_tokens"),
|
|
419
|
+
("outputTokens", "output_tokens"),
|
|
420
|
+
):
|
|
421
|
+
value = usage.get(source)
|
|
422
|
+
if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
|
|
423
|
+
observed[target] = value
|
|
424
|
+
fields = ("effective_input_tokens", "cached_input_tokens", "output_tokens")
|
|
425
|
+
if (
|
|
426
|
+
len(observed) != len(fields)
|
|
427
|
+
or any(observed.get(field, -1) < metrics.get(field, 0) for field in fields)
|
|
428
|
+
or observed.get("cached_input_tokens", 0) > observed.get("effective_input_tokens", 0)
|
|
429
|
+
):
|
|
430
|
+
usage_current = False
|
|
431
|
+
if usage_current:
|
|
432
|
+
metrics.update(observed)
|
|
433
|
+
else:
|
|
434
|
+
# A provider counter reset/estimate is not a cheaper run.
|
|
435
|
+
for field in fields:
|
|
436
|
+
metrics.pop(field, None)
|
|
437
|
+
if payload.get("turnId") == turn and event.get("method") == "item/completed":
|
|
438
|
+
output = payload["item"].get("text", "")
|
|
439
|
+
if event.get("method") == "turn/completed" and payload.get("turn", {}).get("id") == turn:
|
|
440
|
+
if payload["turn"].get("status") != "completed":
|
|
441
|
+
raise ProviderError("provider-turn-failed")
|
|
442
|
+
return {
|
|
443
|
+
"returned_work": json.loads(output),
|
|
444
|
+
"metrics": metrics,
|
|
445
|
+
"elapsed_ms": round((time.monotonic() - started) * 1000),
|
|
446
|
+
"continuation": {
|
|
447
|
+
"reference": actual,
|
|
448
|
+
"mode": mode,
|
|
449
|
+
"capability_revision": snapshot["revision"],
|
|
450
|
+
"parameters": selection["parameters"],
|
|
451
|
+
"live": False,
|
|
452
|
+
"exclusive": True,
|
|
453
|
+
"ephemeral": ephemeral,
|
|
454
|
+
},
|
|
455
|
+
"raw_transcript_stored": False,
|
|
456
|
+
}
|
|
457
|
+
raise ProviderError("provider-turn-timeout")
|
|
458
|
+
except (ProviderError, ValueError, OSError) as error:
|
|
459
|
+
reason = str(error) if isinstance(error, ProviderError) else "native-return-or-control-failed"
|
|
460
|
+
if reason == "provider-response-timeout" and deadline is not None and time.monotonic() >= deadline:
|
|
461
|
+
reason = "provider-turn-timeout"
|
|
462
|
+
raise ProviderError(reason, metrics=metrics) from error
|
|
463
|
+
finally:
|
|
464
|
+
connection.close()
|
|
465
|
+
if on_closed is not None:
|
|
466
|
+
on_closed()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def archive_reference(snapshot: dict[str, Any], reference: str) -> str:
|
|
470
|
+
"""Reversible cleanup of a caller-owned terminal worker; never delete state."""
|
|
471
|
+
if not snapshot.get("archive_supported"):
|
|
472
|
+
raise ProviderError("native-archive-unavailable")
|
|
473
|
+
connection = CodexConnection(snapshot["executable"])
|
|
474
|
+
try:
|
|
475
|
+
try:
|
|
476
|
+
connection.call("thread/archive", {"threadId": reference})
|
|
477
|
+
except ProviderError as error:
|
|
478
|
+
if str(error) != "native-active-thread-unavailable":
|
|
479
|
+
raise
|
|
480
|
+
return "already-absent"
|
|
481
|
+
return "archived"
|
|
482
|
+
finally:
|
|
483
|
+
connection.close()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Resolve verified package artifacts, a prepared source checkout, or an explicit core."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from importlib.metadata import version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _verify(path: Path, key: str) -> None:
|
|
13
|
+
packaged = Path(__file__).with_name("_native")
|
|
14
|
+
manifest_path = packaged / "artifact.json"
|
|
15
|
+
if packaged.is_dir():
|
|
16
|
+
if not manifest_path.is_file():
|
|
17
|
+
raise RuntimeError("packaged native artifact manifest missing")
|
|
18
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
19
|
+
if manifest["package_version"] != version("agentic-workspace"):
|
|
20
|
+
raise RuntimeError("packaged native artifact version mismatch")
|
|
21
|
+
if hashlib.sha256(path.read_bytes()).hexdigest() != manifest[key]:
|
|
22
|
+
raise RuntimeError("packaged native artifact digest mismatch")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def core_binary() -> Path:
|
|
26
|
+
name = "agentic-workspace-core.exe" if os.name == "nt" else "agentic-workspace-core"
|
|
27
|
+
configured = os.environ.get("AGENTIC_WORKSPACE_CORE_BINARY")
|
|
28
|
+
packaged = Path(__file__).with_name("_native")
|
|
29
|
+
path = Path(configured) if configured else packaged / name
|
|
30
|
+
# Only this module's source tree can supply an implicit development build.
|
|
31
|
+
# Never search cwd/PATH or fall back from a present packaged distribution.
|
|
32
|
+
root = Path(__file__).resolve().parents[2]
|
|
33
|
+
if (
|
|
34
|
+
not configured
|
|
35
|
+
and not packaged.exists()
|
|
36
|
+
and Path(__file__).resolve().parent.parent == root / "src"
|
|
37
|
+
and (root / "Cargo.lock").is_file()
|
|
38
|
+
and (root / "crates/agentic-workspace-core/Cargo.toml").is_file()
|
|
39
|
+
and (root / "crates/agentic-workspace-cli/Cargo.toml").is_file()
|
|
40
|
+
):
|
|
41
|
+
directory = root / "target/debug"
|
|
42
|
+
path = directory / name
|
|
43
|
+
cli = directory / ("agentic-workspace.exe" if os.name == "nt" else "agentic-workspace")
|
|
44
|
+
if not path.is_file() or not cli.is_file():
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
f"source-checkout native binary pair is unavailable at {directory}; "
|
|
47
|
+
"run cargo build --locked --workspace --bins from this checkout; "
|
|
48
|
+
"for a custom target directory explicitly set AGENTIC_WORKSPACE_CORE_BINARY to its paired core"
|
|
49
|
+
)
|
|
50
|
+
if not path.is_file():
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"shared Agentic Workspace core is unavailable; install the native package or explicitly set AGENTIC_WORKSPACE_CORE_BINARY for development"
|
|
53
|
+
)
|
|
54
|
+
_verify(path, "sha256")
|
|
55
|
+
return path
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cli_binary() -> Path:
|
|
59
|
+
path = core_binary().with_name("agentic-workspace.exe" if os.name == "nt" else "agentic-workspace")
|
|
60
|
+
if not path.is_file():
|
|
61
|
+
raise RuntimeError("paired native Agentic Workspace CLI is unavailable")
|
|
62
|
+
_verify(path, "cli_sha256")
|
|
63
|
+
return path
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Thin Codex app-server bridge for the native owner's sealed worker protocol.
|
|
2
|
+
|
|
3
|
+
Core owns packet validation, return identity, attempts and integration. This
|
|
4
|
+
adapter owns only provider discovery and one fresh read-only host turn.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from agentic_workspace import codex_provider as native_transport
|
|
17
|
+
from agentic_workspace.native_core import core_binary
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def assignment_packet(context: dict[str, Any]) -> dict[str, Any]:
|
|
21
|
+
"""Forward packet admission to the paired core; never interpret it in Python."""
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
[str(core_binary())],
|
|
24
|
+
input=json.dumps({"assignment_packet": context}),
|
|
25
|
+
text=True,
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
capture_output=True,
|
|
28
|
+
check=False,
|
|
29
|
+
)
|
|
30
|
+
if result.returncode:
|
|
31
|
+
raise ValueError(result.stderr.strip())
|
|
32
|
+
return json.loads(result.stdout)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def capability(root: Path, parameters: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
"""Fresh adapter facts only: no worker turn, session or retained observation."""
|
|
37
|
+
facts: dict[str, Any] = {
|
|
38
|
+
"kind": "agentic-workspace/host-transport-capability/v1",
|
|
39
|
+
"parameters": parameters,
|
|
40
|
+
"adapter_revision": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
|
41
|
+
}
|
|
42
|
+
try:
|
|
43
|
+
snapshot = native_transport.discover(root, refresh=True, persist=False)
|
|
44
|
+
facts["provider_revision"] = snapshot["revision"]
|
|
45
|
+
native_transport.validate_selection(
|
|
46
|
+
snapshot, {"mode": "fresh", "parameters": parameters, "capability_revision": snapshot["revision"]}
|
|
47
|
+
)
|
|
48
|
+
facts.update(status="available", reason="current-protocol-and-model", result_classes=["read-only", "unapplied-patch"])
|
|
49
|
+
except native_transport.ProviderError as error:
|
|
50
|
+
reason = str(error)
|
|
51
|
+
# Only settled adapter facts establish unavailability. Discovery errors,
|
|
52
|
+
# quota, timeouts and unknown protocol remain unresolved, never local fit.
|
|
53
|
+
unavailable = reason in {
|
|
54
|
+
"native-adapter-executable-unavailable",
|
|
55
|
+
"native-model-unavailable",
|
|
56
|
+
"native-continuity-unavailable",
|
|
57
|
+
"native-parameter-unsupported",
|
|
58
|
+
"native-ephemeral-continuity-unavailable",
|
|
59
|
+
}
|
|
60
|
+
facts.update(status="unavailable" if unavailable else "unknown", reason=reason, result_classes=[])
|
|
61
|
+
except (OSError, ValueError, KeyError, subprocess.SubprocessError):
|
|
62
|
+
facts.update(status="unknown", reason="host-capability-discovery-failed", result_classes=[])
|
|
63
|
+
facts["revision"] = native_transport.digest(facts)
|
|
64
|
+
return facts
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def dispatch(root: Path, packet: dict[str, Any]) -> dict[str, Any]:
|
|
68
|
+
entry = assignment_packet({"action": "entry", "packet": packet})
|
|
69
|
+
configuration = packet["assignment_identity"]["current_assignment"]["selected"]["configuration"]
|
|
70
|
+
adapter = configuration["execution"]["adapter"]
|
|
71
|
+
if adapter.get("adapter") != "codex-app-server/v1":
|
|
72
|
+
raise ValueError("unsupported sealed host adapter")
|
|
73
|
+
parameters = adapter["parameters"]
|
|
74
|
+
if configuration["execution"].get("host_capability") != capability(root, parameters):
|
|
75
|
+
raise ValueError("host capability changed; resolve current Assignment before dispatch")
|
|
76
|
+
snapshot = native_transport.discover(root, refresh=True)
|
|
77
|
+
if snapshot["revision"] != configuration["execution"]["host_capability"]["provider_revision"]:
|
|
78
|
+
raise ValueError("host capability changed; resolve current Assignment before dispatch")
|
|
79
|
+
selection = {"mode": "fresh", "parameters": parameters, "capability_revision": snapshot["revision"]}
|
|
80
|
+
native_transport.validate_selection(snapshot, selection)
|
|
81
|
+
# Expand the bounded captured capsule mechanically, never from the filesystem.
|
|
82
|
+
prompt = json.dumps({"worker": entry, "captured_inputs": packet["assignment_identity"]["input_capsule"]})
|
|
83
|
+
schema = {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"additionalProperties": False,
|
|
86
|
+
"properties": {
|
|
87
|
+
"summary": {"type": "string"},
|
|
88
|
+
"changed_paths": {"type": "array", "items": {"type": "string"}},
|
|
89
|
+
"patch": {"type": "string"},
|
|
90
|
+
"stop_conditions_hit": {"type": "array", "items": {"type": "string"}},
|
|
91
|
+
"result_delivery": {"const": "unapplied-patch"},
|
|
92
|
+
},
|
|
93
|
+
"required": ["summary", "changed_paths", "patch", "stop_conditions_hit", "result_delivery"],
|
|
94
|
+
}
|
|
95
|
+
result = native_transport.execute(root, snapshot, selection, prompt, schema, timeout=adapter["timeout_seconds"])
|
|
96
|
+
returned = assignment_packet({"action": "return", "packet": packet, "material": result["returned_work"]})
|
|
97
|
+
return next(
|
|
98
|
+
request["arguments"]["returned"]
|
|
99
|
+
for request in returned["reentry"]["request"]
|
|
100
|
+
if request["request_kind"] in {"assignment/observe-readonly-return/v1", "assignment/observe-patch-return/v1"}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> None:
|
|
105
|
+
if sys.argv[1:] == ["--aw-capability"]:
|
|
106
|
+
print(json.dumps(capability(Path.cwd(), json.load(sys.stdin))))
|
|
107
|
+
return
|
|
108
|
+
packet = json.load(sys.stdin)
|
|
109
|
+
print(json.dumps(dispatch(Path.cwd(), packet)))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
main()
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentic-workspace
|
|
3
|
+
Version: 1.0.0rc3
|
|
4
|
+
Summary: Repo-native continuity and execution layer for agents, combining workspace routing, active planning, and durable memory.
|
|
5
|
+
Project-URL: Homepage, https://github.com/rickardvh/agentic-workspace
|
|
6
|
+
Project-URL: Repository, https://github.com/rickardvh/agentic-workspace
|
|
7
|
+
Project-URL: Issues, https://github.com/rickardvh/agentic-workspace/issues
|
|
8
|
+
Project-URL: Support, https://github.com/rickardvh/agentic-workspace/issues
|
|
9
|
+
Project-URL: Documentation, https://github.com/rickardvh/agentic-workspace/blob/master/docs/agentic-workspace-install.md
|
|
10
|
+
Author: Rickard von Haugwitz
|
|
11
|
+
Maintainer: Rickard von Haugwitz
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: agents,developer-tools,memory,planning,repository,workflow
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Agentic Workspace
|
|
27
|
+
|
|
28
|
+
**Persistent operating context and dynamic control for coding agents.**
|
|
29
|
+
|
|
30
|
+
Agentic Workspace (AW) helps coding agents enter a repository with the right guidance, continue unfinished work across sessions, and preserve useful lessons without turning every task into a workflow.
|
|
31
|
+
|
|
32
|
+
It builds on repository instructions and skills with persistent, source-owned context and a small Rust-backed tool surface for exact current information and bounded operations. Keep your existing agent, editor, source tree, tests, and review process; AW connects them to the operating context that matters for the task at hand.
|
|
33
|
+
|
|
34
|
+
[Get started](#get-started) · [Everyday use](docs/everyday-use.md) · [Documentation](docs/index.md) · [Releases](https://github.com/rickardvh/agentic-workspace/releases)
|
|
35
|
+
|
|
36
|
+
## Why AW?
|
|
37
|
+
|
|
38
|
+
A repository instruction file can explain how to work in a project. By itself, it cannot tell a new session where a multi-step task stopped, which retained lesson is relevant now, whether earlier verification still supports a claim, or which deeper procedure is worth loading for this change.
|
|
39
|
+
|
|
40
|
+
As work spans sessions and agents, that context has to come from somewhere. Without a deliberate home, it tends to disappear into chat, duplicate into prose, or be reconstructed from source, issues, and history.
|
|
41
|
+
|
|
42
|
+
AW keeps only the operating context whose availability can materially change agent behavior, then makes the relevant part cheap to reach:
|
|
43
|
+
|
|
44
|
+
| Need | What AW provides |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| Find the right guidance | Task-relevant instructions, source references, and reusable skills. |
|
|
47
|
+
| Continue interrupted work | Retained outcomes, constraints, accepted progress, blockers, and next actions. |
|
|
48
|
+
| Know what to verify | Relevant checking procedures, recorded evidence, and visible gaps. |
|
|
49
|
+
| Hand work to another agent | Bounded assignments with explicit context, constraints, and return expectations. |
|
|
50
|
+
| Avoid repeated rediscovery | Useful lessons and corrections retained with the appropriate owner. |
|
|
51
|
+
|
|
52
|
+
The goal is less repeated explanation, searching, handoff reconstruction, and repair—not a larger prompt or a new workflow to manage.
|
|
53
|
+
|
|
54
|
+
**Small tasks stay small.** A typo fix does not need Planning, Memory, Verification, delegation, or another artifact merely because those capabilities are available.
|
|
55
|
+
|
|
56
|
+
## What using it looks like
|
|
57
|
+
|
|
58
|
+
Imagine an API change that spans two sessions:
|
|
59
|
+
|
|
60
|
+
> Add pagination to the users API without breaking existing clients.
|
|
61
|
+
|
|
62
|
+
In a repository with API guidance and verification procedures configured, the agent can use AW to find the relevant contract, load a useful implementation procedure, and identify the checks expected for the change. It still reads the source, reasons about the design, and implements with its ordinary tools.
|
|
63
|
+
|
|
64
|
+
If work stops partway through, Planning can preserve the intended outcome, accepted progress, unresolved questions, and next action.
|
|
65
|
+
|
|
66
|
+
In a later session:
|
|
67
|
+
|
|
68
|
+
> Continue the pagination work.
|
|
69
|
+
|
|
70
|
+
The next agent can recover that continuation rather than reconstructing the previous conversation. Changed assumptions and missing evidence remain things to check; a recorded result is not automatically fresh proof.
|
|
71
|
+
|
|
72
|
+
The same principle applies to a handoff: preserve enough for the receiving agent to do bounded work without copying the entire parent session. Returned work still needs appropriate integration and verification.
|
|
73
|
+
|
|
74
|
+
[See everyday examples →](docs/everyday-use.md)
|
|
75
|
+
|
|
76
|
+
## How it works
|
|
77
|
+
|
|
78
|
+
**Skills teach procedure. Repository instructions and configuration set policy. Domain owners hold current state and evidence. Tools provide exact current information and controlled operations. The agent supplies judgment.**
|
|
79
|
+
|
|
80
|
+
A small repository entry point leads the agent to AW's canonical `workspace-startup` skill. That skill explains how to reach relevant sources, ask the runtime for current facts when they matter, and load specialized procedures only when useful.
|
|
81
|
+
|
|
82
|
+
The underlying model is deliberately small:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Find the relevant context → Do the work → Update what matters
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Internally, AW resolves a compact operating contract, exposes supported actions, and reconciles their consequences afterward. That machinery is a substrate for the agent, not a phase machine the user has to operate around every task.
|
|
89
|
+
|
|
90
|
+
Source code, documentation, tests, decisions, and other canonical repository material remain in their existing homes. AW routes to those sources rather than importing the repository into a second knowledge system.
|
|
91
|
+
|
|
92
|
+
[Product model](docs/package/overview.md) · [Architecture](docs/architecture.md)
|
|
93
|
+
|
|
94
|
+
## Get started
|
|
95
|
+
|
|
96
|
+
Use the [installation and adoption guide](docs/agentic-workspace-install.md) for the release class you intend to run. Stable support, release candidates, and previews have different evidence and support boundaries; exact install commands and platform claims belong to the selected release and its receipts rather than this README.
|
|
97
|
+
|
|
98
|
+
For support-bearing installs, follow the [generated install projection](docs/reference/support-bearing-install.md) to the selected release's `distribution-install-readiness.json` receipt and its exact installation command.
|
|
99
|
+
|
|
100
|
+
### Adopt a repository
|
|
101
|
+
|
|
102
|
+
Installing the runtime and adopting a repository are separate operations. After installing the selected artifact, run ordinary `start` against the target Git repository:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
agentic-workspace start --target . \
|
|
106
|
+
--task "Inspect this repository" \
|
|
107
|
+
--format json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If the repository is not yet adopted and the selected artifact includes the current adoption owner, Configuration returns the exact repository-adoption request. Follow that request and execute only the returned authorized action. Adoption establishes the small package-owned host footprint and managed `AGENTS.md` fence; it does **not** invent repository policy, choose optional modules, or create Planning, Memory, or Verification state.
|
|
111
|
+
|
|
112
|
+
Historical `init`/setup command families are not the v1 adoption model.
|
|
113
|
+
|
|
114
|
+
### In an adopted repository
|
|
115
|
+
|
|
116
|
+
The repository's small entry point leads to the canonical `workspace-startup` skill. Agent hosts with supported native skill discovery can expose the same skill directly.
|
|
117
|
+
|
|
118
|
+
For an initial check, ask your agent:
|
|
119
|
+
|
|
120
|
+
> Use this repository's Agentic Workspace setup to identify the guidance and checks relevant to an API change. Do not modify anything yet.
|
|
121
|
+
|
|
122
|
+
For direct inspection, use the same `start` boundary with the actual task. After adoption, continue giving your agent ordinary work; the canonical skill teaches it when AW is useful, so you should not need to run a manual command sequence around every change.
|
|
123
|
+
|
|
124
|
+
[Installation and adoption](docs/agentic-workspace-install.md) · [Everyday use](docs/everyday-use.md) · [CLI reference](docs/reference/cli-catalogue.md)
|
|
125
|
+
|
|
126
|
+
## Your repository, your rules
|
|
127
|
+
|
|
128
|
+
AW is designed to fit around the project rather than reorganize it.
|
|
129
|
+
|
|
130
|
+
A simplified host repository looks like this:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
your-repository/
|
|
134
|
+
├── AGENTS.md # Small managed entry point inside a repo-owned file
|
|
135
|
+
├── src/, docs/, tests/ # Existing project contents
|
|
136
|
+
└── .agentic-workspace/ # Package integration plus optional owner state
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The current public adoption footprint is Configuration-owned and deliberately small. Package-managed skills, ownership/read-profile metadata, provenance, and adoption identity stay distinct from repo-owned configuration, module-owned state, local data, and promoted output.
|
|
140
|
+
|
|
141
|
+
You can express a repository-wide correction in ordinary language:
|
|
142
|
+
|
|
143
|
+
> For this repository, read the API contract before changing public response fields.
|
|
144
|
+
|
|
145
|
+
Or make the scope explicitly local:
|
|
146
|
+
|
|
147
|
+
> Only on this machine, use this executable path when invoking AW.
|
|
148
|
+
|
|
149
|
+
The agent can route the change to the appropriate instruction or configuration owner and verify whether it was retained. A promise in chat is not a saved rule, and a local preference should not silently become shared repository policy.
|
|
150
|
+
|
|
151
|
+
Reusable methods belong in skills. Binding rules belong in instructions or configuration. Current work and evidence stay with the capabilities responsible for them.
|
|
152
|
+
|
|
153
|
+
The same Configuration-owned footprint is used for adoption, refresh, and removal. De-adoption removes only authenticated package-owned integration, preserves repo-owned configuration and domain state, and refuses to erase edited or unowned content merely because it sits under an AW path.
|
|
154
|
+
|
|
155
|
+
[Configuration reference](docs/reference/workspace-config.md) · [Repository footprint](docs/reference/installed-surface-catalogue.md)
|
|
156
|
+
|
|
157
|
+
## Add structure where it helps
|
|
158
|
+
|
|
159
|
+
AW includes optional first-party capabilities for recurring coordination costs:
|
|
160
|
+
|
|
161
|
+
**Planning** preserves execution continuity: intended outcome, constraints, progress, blockers, and next action.
|
|
162
|
+
|
|
163
|
+
**Memory** retains useful observations and lessons that would otherwise be expensive to rediscover. Advice can be reconsidered when its dependencies change; a note is not automatically policy.
|
|
164
|
+
|
|
165
|
+
**Verification** makes checking procedures, evidence, and known gaps reusable. Passing a check supports the claim it actually tests, not every claim about the finished work.
|
|
166
|
+
|
|
167
|
+
These are peer capabilities, not mandatory workflow stages. Repositories can use what repays its cost and leave unrelated capabilities out of the current task's context.
|
|
168
|
+
|
|
169
|
+
Repository-specific rules and procedures do not require a new module. Modules are for independently reusable capabilities with their own domain state or operations.
|
|
170
|
+
|
|
171
|
+
[Modules and extensions →](docs/package/modules.md)
|
|
172
|
+
|
|
173
|
+
## Works with agents rather than replacing them
|
|
174
|
+
|
|
175
|
+
Agentic Workspace is not an autonomous coding agent. It gives the agent already working in the repository better operating context and safer repository-native control.
|
|
176
|
+
|
|
177
|
+
An agent with runtime access can query current owner state and use supported operations. A repository-only reviewer can follow the same canonical skill and generated read profile to recover relevant recorded intent, constraints, progress, and advice, while live machine state, fresh proof, and effect permission remain unknown.
|
|
178
|
+
|
|
179
|
+
The ordinary deterministic product semantics live in a shared Rust core. Native, Python, TypeScript, and JSON-facing surfaces project or bind that authority rather than defining separate workflow behavior. The host project's implementation language does not need to match AW's runtime implementation.
|
|
180
|
+
|
|
181
|
+
Host integrations still differ. Provider independence does not mean every agent host discovers skills identically or every model follows repository guidance perfectly; current support and evidence remain release- and environment-bound.
|
|
182
|
+
|
|
183
|
+
[Evidence and support →](docs/evidence-and-support.md)
|
|
184
|
+
|
|
185
|
+
## Trust and support
|
|
186
|
+
|
|
187
|
+
**AW is not a sandbox.** Repository-configured commands run with the caller's filesystem and credential authority. Review the repository and its execution routes before allowing them to run. Credentials belong in the host or environment's credential facilities, not checked-in AW state.
|
|
188
|
+
|
|
189
|
+
AW's operation boundaries and verification support do not replace human judgment, independent review, or existing security controls.
|
|
190
|
+
|
|
191
|
+
Exact package identities, installation commands, runtime versions, operating-system support, and prerelease/stable status are deliberately kept in release-bound or generated owners instead of copied into this landing page.
|
|
192
|
+
|
|
193
|
+
[Installation and adoption](docs/agentic-workspace-install.md) · [Threat model](docs/security/threat-model.md) · [Evidence and support](docs/evidence-and-support.md)
|
|
194
|
+
|
|
195
|
+
## Learn more
|
|
196
|
+
|
|
197
|
+
**Use AW:** [Installation and adoption](docs/agentic-workspace-install.md) · [Everyday use](docs/everyday-use.md) · [Configuration](docs/reference/workspace-config.md)
|
|
198
|
+
|
|
199
|
+
**Understand AW:** [Product overview](docs/package/overview.md) · [Architecture](docs/architecture.md) · [Design principles](docs/design-principles.md) · [Modules](docs/package/modules.md)
|
|
200
|
+
|
|
201
|
+
**Look up details:** [CLI reference](docs/reference/cli-catalogue.md) · [Installed surfaces](docs/reference/installed-surface-catalogue.md) · [Full documentation](docs/index.md)
|
|
202
|
+
|
|
203
|
+
## Contributing
|
|
204
|
+
|
|
205
|
+
For changes to Agentic Workspace itself, start with the [contributor playbook](docs/maintainer/contributor-playbook.md). Use the repository's [issue templates](https://github.com/rickardvh/agentic-workspace/issues/new/choose) to report a problem or propose a change.
|
|
206
|
+
|
|
207
|
+
Contributions should make useful work easier to complete, continue, or verify while keeping unnecessary context, framework surface, and repository residue low.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[MIT](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
agentic_workspace/cli.py,sha256=Cv-6qEhAGBdAqapqhCVvaJprnKMHWO7o69qPzLP08w4,520
|
|
2
|
+
agentic_workspace/codex_provider.py,sha256=RiP5C1yHkoaXqz_e1UL66sN1oVqrAzxAsR6vmGd_l8I,23003
|
|
3
|
+
agentic_workspace/native_core.py,sha256=bvOZGQOdXDqMI4814WESC0dn9EPzdOzGnC01D8onpDI,2815
|
|
4
|
+
agentic_workspace/sealed_codex_transport.py,sha256=n8rs15OXA4-3MTQtq3hnb7XETZosCPPzjUTKWaHMVBM,5192
|
|
5
|
+
agentic_workspace/__init__.py,sha256=REtU4aBp-goYOOX9z8_QroPiDO7S86rfInsibeSUNs8,410
|
|
6
|
+
agentic_workspace/_binding.py,sha256=gfyyjByLQHKoksg04FcH5w7iSCEBMb4gUCqdQaTFTOQ,2797
|
|
7
|
+
agentic_workspace/_native/agentic-workspace-core.exe,sha256=XENuhpI_uJR77k5_HD2-R4uSV6g0qrniqVJIuxE_z2c,18485760
|
|
8
|
+
agentic_workspace/_native/agentic-workspace.exe,sha256=R5KYPVFINdsiuL64z5xBYskr2e9fplrXDu2KGusQC-k,501248
|
|
9
|
+
agentic_workspace/_native/artifact.json,sha256=jvQ0JFr2mnmnvXfUfo-RvX9k1ADiLU1-tcGECcaLB4M,708
|
|
10
|
+
agentic_workspace-1.0.0rc3.dist-info/METADATA,sha256=m1wGW__UD5ZusXiPG9IA7sASXGb4JP9zASxF3GNkJYQ,13793
|
|
11
|
+
agentic_workspace-1.0.0rc3.dist-info/WHEEL,sha256=OA-gEgWbLnh0Tf6JrLOMFR4vOWY4QbBMSZuN6osy-Q0,94
|
|
12
|
+
agentic_workspace-1.0.0rc3.dist-info/entry_points.txt,sha256=3M_M5tF8MeVr98DcZBMvXFmOmXyrBrpKvdibWQ_lY6g,65
|
|
13
|
+
agentic_workspace-1.0.0rc3.dist-info/licenses/LICENSE,sha256=PE4wqbGPdD5Z1Y158xpSdRFzpEXdacvSzwJf7AD7PTQ,1077
|
|
14
|
+
agentic_workspace-1.0.0rc3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rickard von Haugwitz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|