sediment-cli 0.2.0__tar.gz → 0.3.0__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.
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/PKG-INFO +5 -5
- sediment_cli-0.3.0/hatch_build.py +21 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/pyproject.toml +8 -5
- sediment_cli-0.3.0/sediment_cli/_pi/LICENSE +21 -0
- sediment_cli-0.3.0/sediment_cli/_pi/README.md +204 -0
- sediment_cli-0.3.0/sediment_cli/_pi/index.ts +156 -0
- sediment_cli-0.3.0/sediment_cli/_pi/lib/contract.ts +189 -0
- sediment_cli-0.3.0/sediment_cli/_pi/lib/process.ts +118 -0
- sediment_cli-0.3.0/sediment_cli/_pi/lib/provider.ts +7 -0
- sediment_cli-0.3.0/sediment_cli/_pi/lib/register.ts +295 -0
- sediment_cli-0.3.0/sediment_cli/_pi/lib/retrieval.ts +424 -0
- sediment_cli-0.3.0/sediment_cli/_pi/package.json +20 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/attribution.py +154 -39
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/cli.py +11 -1
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/delivery.py +14 -2
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/ui.py +38 -5
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_ui.py +64 -1
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_delivery_transport.py +47 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_installed_wheel.py +91 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/install.txt +4 -2
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/sediment.txt +1 -1
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/.gitignore +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/LICENSE +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/__init__.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/client.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/evidence.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/local_postgres.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/sediment_cli/transcript.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/conftest.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_capture_authority.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_db.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_evidence.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_evidence_integration.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_help.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_libpq_unavailable.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_cli_remote.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_consumer_profile_cli.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_demo_verb.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_e2e_quickstart.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/test_local_postgres.py +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/commit.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/db-provision.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/db-status.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/db-upgrade.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/db.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/delivery-enqueue.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/delivery-replay.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/delivery-status.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/delivery.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/demo.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/derive.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/doctor.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/evidence-fetch.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/evidence-inspect.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/evidence-inventory.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/evidence.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/export-diff-sft.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/export-dpo.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/export-recovery.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/export-rlvr.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/export-sft.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/export.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/facts.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/login.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/logout.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/mirror-gc.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/quarantine-inference-calls.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/quarantine-log.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/quarantine.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/release.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-abandonment.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-attribution-share.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-dataset-diagnostics.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-label-confidence-inspection.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-lifecycle.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-merge-retention.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-model.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-precision.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report-recovery-yield.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/report.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/server.txt +0 -0
- {sediment_cli-0.2.0 → sediment_cli-0.3.0}/tests/testdata/help/uninstall.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sediment-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: The sediment command: login, install, server, facts, exports
|
|
5
5
|
Project-URL: Source, https://github.com/sediment-ai/sediment
|
|
6
6
|
Project-URL: Documentation, https://github.com/sediment-ai/sediment/tree/main/docs
|
|
@@ -10,7 +10,7 @@ License-Expression: AGPL-3.0-or-later
|
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Requires-Python: >=3.12
|
|
12
12
|
Requires-Dist: httpx>=0.27
|
|
13
|
-
Requires-Dist: sediment-api==0.
|
|
14
|
-
Requires-Dist: sediment-core==0.
|
|
15
|
-
Requires-Dist: sediment-derive==0.
|
|
16
|
-
Requires-Dist: sediment-export==0.
|
|
13
|
+
Requires-Dist: sediment-api==0.3.0
|
|
14
|
+
Requires-Dist: sediment-core==0.3.0
|
|
15
|
+
Requires-Dist: sediment-derive==0.3.0
|
|
16
|
+
Requires-Dist: sediment-export==0.3.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
"""Include the MIT pi runtime in wheels and source distributions."""
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PiExtensionHook(BuildHookInterface):
|
|
10
|
+
def initialize(self, version, build_data):
|
|
11
|
+
# Editable installs resolve shims/pi directly from the checkout.
|
|
12
|
+
if version == "editable":
|
|
13
|
+
return
|
|
14
|
+
root = Path(self.root)
|
|
15
|
+
bundled = root / "sediment_cli" / "_pi"
|
|
16
|
+
# A source distribution already contains the runtime as package data.
|
|
17
|
+
if bundled.is_dir():
|
|
18
|
+
return
|
|
19
|
+
source = root.parent / "shims" / "pi"
|
|
20
|
+
for name in ("index.ts", "lib", "package.json", "LICENSE", "README.md"):
|
|
21
|
+
build_data["force_include"][str(source / name)] = f"sediment_cli/_pi/{name}"
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# Depends on sediment-api deliberately: the operator verbs are DB-local by
|
|
6
6
|
# design (ADR 0001) and `server`/reports/mirror-gc forward into it.
|
|
7
7
|
name = "sediment-cli"
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.3.0"
|
|
9
9
|
description = "The sediment command: login, install, server, facts, exports"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
license = "AGPL-3.0-or-later"
|
|
@@ -14,10 +14,10 @@ license-files = ["LICENSE"]
|
|
|
14
14
|
# so an install never mixes member versions (the release workflow asserts
|
|
15
15
|
# the versions agree before publishing).
|
|
16
16
|
dependencies = [
|
|
17
|
-
"sediment-core==0.
|
|
18
|
-
"sediment-derive==0.
|
|
19
|
-
"sediment-export==0.
|
|
20
|
-
"sediment-api==0.
|
|
17
|
+
"sediment-core==0.3.0",
|
|
18
|
+
"sediment-derive==0.3.0",
|
|
19
|
+
"sediment-export==0.3.0",
|
|
20
|
+
"sediment-api==0.3.0",
|
|
21
21
|
"httpx>=0.27",
|
|
22
22
|
]
|
|
23
23
|
|
|
@@ -37,6 +37,9 @@ build-backend = "hatchling.build"
|
|
|
37
37
|
[tool.hatch.build.targets.wheel]
|
|
38
38
|
packages = ["sediment_cli"]
|
|
39
39
|
|
|
40
|
+
[tool.hatch.build.hooks.custom]
|
|
41
|
+
path = "hatch_build.py"
|
|
42
|
+
|
|
40
43
|
[tool.uv.sources]
|
|
41
44
|
sediment-core = { workspace = true }
|
|
42
45
|
sediment-derive = { workspace = true }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sediment
|
|
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.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# sediment-pi
|
|
2
|
+
|
|
3
|
+
The pi-harness shim captures agent work and retrieves evidence from a previous
|
|
4
|
+
Session. Capture follows the Sediment client capture contract
|
|
5
|
+
(`docs/agents/capture-clients.md`):
|
|
6
|
+
|
|
7
|
+
| seam | behavior |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Inference calls | `before_provider_headers` stamps the live Session identifier on requests for the configured provider and API. |
|
|
10
|
+
| decisions | edit/write `tool_execution_end` (no error) → `sediment.tool_decision` record to `POST $SEDIMENT_OTLP_ENDPOINT/v1/logs`. Always `accept` / `explicit=false` — stock pi has no human approval gesture, and a gesture we cannot observe is absent, never guessed at. |
|
|
11
|
+
| attribution | edit/write `tool_execution_end` (error or not) → `sediment mark --tool pi` with `{session_id, cwd}` on stdin |
|
|
12
|
+
| transcripts | When `SEDIMENT_PI_TRANSCRIPTS=1`, `session_shutdown` (except `reason: "reload"`, which tears down the extension runtime, not the Session) → `sediment transcript --agent pi` with `{session_id, transcript_path}` on stdin |
|
|
13
|
+
|
|
14
|
+
Capture failures never throw into the harness. Each channel reports failures
|
|
15
|
+
independently; Attribution doesn't prove a missing Decision or Edit observation.
|
|
16
|
+
With `SEDIMENT_DELIVERY_DIR`, the shared Python helper retains prepared Decision
|
|
17
|
+
and opted-in transcript payloads before delivery. Without it, delivery is best-effort.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
Use pi 0.84.1 or later with Node 24. Node 22.18 or later in the Node 22
|
|
22
|
+
release line is also supported. The shim rejects other Node release lines.
|
|
23
|
+
The shim has no runtime package dependencies and doesn't require `npm install`.
|
|
24
|
+
Your pi installation is a separate prerequisite: inventory its packages and
|
|
25
|
+
support status before installing it in your deployment.
|
|
26
|
+
|
|
27
|
+
The `sediment-cli` wheel includes the extension's TypeScript source, package
|
|
28
|
+
manifest, README, and MIT license. Its SPDX headers remain intact. No build or
|
|
29
|
+
dependency installation runs on the developer machine. Start pi once to create
|
|
30
|
+
`~/.pi/agent`, then run:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
sediment install <repo>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The command registers the packaged extension in `~/.pi/agent/settings.json`
|
|
37
|
+
(`extensions` list) alongside the Git hooks and detected agent hooks. Editable
|
|
38
|
+
source installations register `shims/pi/` instead. `uninstall --agents` removes
|
|
39
|
+
the entry. With pi present, `doctor` reports `FAIL` if the extension is missing
|
|
40
|
+
or unregistered. Keep `sediment` on PATH when you start pi.
|
|
41
|
+
|
|
42
|
+
## Opt in to Edit observations
|
|
43
|
+
|
|
44
|
+
Transcript capture sends applied edit text and observed file text. Decision
|
|
45
|
+
capture and Attribution don't authorize that content capture.
|
|
46
|
+
|
|
47
|
+
To opt in, run `sediment install --transcripts <repo>`.
|
|
48
|
+
Load the generated environment before you restart pi.
|
|
49
|
+
|
|
50
|
+
If you use `--no-env`, set the endpoint, token, and explicit content opt-in in
|
|
51
|
+
the environment that starts pi:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export SEDIMENT_OTLP_ENDPOINT=https://sediment-api.example.com
|
|
55
|
+
export SEDIMENT_INGEST_TOKEN='<deployment token>'
|
|
56
|
+
export SEDIMENT_PI_TRANSCRIPTS=1
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
An existing pi installation that supplies only an endpoint and token continues
|
|
60
|
+
to send Developer decisions and mark Attribution. To continue Edit observations,
|
|
61
|
+
you must opt in to transcript content with `SEDIMENT_PI_TRANSCRIPTS=1`.
|
|
62
|
+
An unset variable or any other value disables transcript extraction.
|
|
63
|
+
|
|
64
|
+
## Retrieve context from one previous Session
|
|
65
|
+
|
|
66
|
+
If the operator enables a fixed source Session on the API, supply its restricted
|
|
67
|
+
retrieval credential to the agent environment:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export SEDIMENT_RETRIEVAL_ENDPOINT=https://sediment-api.example.com
|
|
71
|
+
export SEDIMENT_RETRIEVAL_TOKEN='<Session retrieval token>'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The extension registers `sediment_retrieve_context` with `query` and optional
|
|
75
|
+
`max_bytes` arguments. The agent chooses its question. The tool uses English/code
|
|
76
|
+
keywords to select exact captured parts from the configured previous Session.
|
|
77
|
+
The response preserves occurrence references and reports unknown capture
|
|
78
|
+
completeness. An empty result doesn't prove that an event never happened.
|
|
79
|
+
|
|
80
|
+
The endpoint must be an API base URL. Remote hosts require HTTPS; HTTP accepts
|
|
81
|
+
only literal localhost or loopback addresses. The tool rejects redirects, sends
|
|
82
|
+
one request, and combines pi cancellation with a 35-second deadline. Its complete
|
|
83
|
+
JSON result defaults to 16,384 bytes, with a permitted range of 4,096–65,536 bytes.
|
|
84
|
+
The limit counts bytes, not model tokens. The tool returns whole parts without
|
|
85
|
+
summarizing or clipping them.
|
|
86
|
+
Keyword tools accept complete scan coverage through 16,384 parts; source limits
|
|
87
|
+
and refusal behavior follow the [keyword retrieval contract](../../docs/operate/resume-with-evidence.md).
|
|
88
|
+
|
|
89
|
+
Both settings are independent of capture enrollment. Neither falls back to an
|
|
90
|
+
ingest token, operator login, or workspace configuration. If both are absent,
|
|
91
|
+
the extension registers no retrieval tool. Incomplete or invalid configuration
|
|
92
|
+
logs a content-free reason and leaves capture active. The extension reports
|
|
93
|
+
request failures as content-free tool errors and doesn't retry automatically.
|
|
94
|
+
|
|
95
|
+
Keep operator credentials, deployment configuration, database credentials, and
|
|
96
|
+
old transcripts outside the agent's execution environment. Use a separate
|
|
97
|
+
container or operating-system account for that boundary. Keep the model and
|
|
98
|
+
gateway inside the customer perimeter as well. Historical roles and tool calls
|
|
99
|
+
remain evidence; the extension doesn't execute them. Retrieved text can contain
|
|
100
|
+
instructions, so ordinary harness tool controls still apply.
|
|
101
|
+
|
|
102
|
+
This tool adds no model dependency. It doesn't establish cost savings or a
|
|
103
|
+
continuation benefit by itself. The
|
|
104
|
+
[controlled continuation procedure](../../docs/operate/resume-with-evidence.md)
|
|
105
|
+
defines the validation boundary.
|
|
106
|
+
|
|
107
|
+
The same endpoint/token pair also registers `sediment_list_context_sessions`,
|
|
108
|
+
`sediment_evidence_inventory`, `sediment_evidence_manifest`, and
|
|
109
|
+
`sediment_read_evidence`. These tools enumerate the configured grant and fetch
|
|
110
|
+
explicit occurrences without a keyword query. An external selector judges which
|
|
111
|
+
evidence is useful. Exact reads preserve repeated occurrences and can include
|
|
112
|
+
readable reasoning. Every request rechecks authorization and Quarantine.
|
|
113
|
+
Fetch accepts at most 32 distinct references in a 64 KiB request and a 1 MiB
|
|
114
|
+
response. The tools forward validated original JSON text to preserve large
|
|
115
|
+
integers. See [Select exact evidence independently](../../docs/operate/resume-with-evidence.md#select-exact-evidence-independently).
|
|
116
|
+
|
|
117
|
+
## Environment
|
|
118
|
+
|
|
119
|
+
If the API authorizes several Sessions, set `SEDIMENT_RETRIEVAL_DISCOVERY=true`
|
|
120
|
+
with the independent retrieval endpoint/token pair. The extension registers
|
|
121
|
+
`sediment_discover_context` and requires `session_id` on `sediment_retrieve_context`.
|
|
122
|
+
Discover with task keywords, select a returned Session, then request its evidence.
|
|
123
|
+
An optional complete repository-qualified commit prioritizes an observed
|
|
124
|
+
relationship; it cannot expand the authorized set. The operator's Session list
|
|
125
|
+
stays on the API. See [Discover a previous Session](../../docs/operate/resume-with-evidence.md#discover-a-previous-session).
|
|
126
|
+
Absent or `false` preserves the fixed-Session tool. Other flag values disable
|
|
127
|
+
retrieval with a safe configuration diagnostic.
|
|
128
|
+
|
|
129
|
+
- `SEDIMENT_RETRIEVAL_ENDPOINT` / `SEDIMENT_RETRIEVAL_TOKEN` — independent
|
|
130
|
+
opt-in pair for Session retrieval tools. The endpoint is an API base
|
|
131
|
+
URL; capture credentials never supply retrieval authority.
|
|
132
|
+
- `SEDIMENT_RETRIEVAL_DISCOVERY` — `true` enables candidate discovery and explicit
|
|
133
|
+
Session selection; absent or `false` retains the fixed-Session schema.
|
|
134
|
+
- `SEDIMENT_PROVIDER_ID` / `SEDIMENT_PROVIDER_API` — provider and API that receive
|
|
135
|
+
the Session header (defaults `sediment` / `anthropic-messages`). The native
|
|
136
|
+
header hook reads the Session identifier for each request. It replaces stale
|
|
137
|
+
Session headers and preserves all other headers. If the identifier is absent
|
|
138
|
+
or invalid for an HTTP header, the shim omits it and logs a content-free
|
|
139
|
+
diagnostic. Decision capture and Attribution continue independently.
|
|
140
|
+
- `SEDIMENT_OTLP_ENDPOINT` — ingest base URL. Required for decision capture;
|
|
141
|
+
unset means not opted in (there is deliberately no
|
|
142
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` fallback — see the contract doc). Remote hosts
|
|
143
|
+
require HTTPS. Plain HTTP accepts only literal `localhost`, IPv4 loopback,
|
|
144
|
+
or `[::1]`; authenticated POSTs reject redirects.
|
|
145
|
+
- `SEDIMENT_INGEST_TOKEN` — bearer token (falls back to an `Authorization`
|
|
146
|
+
header in `OTEL_EXPORTER_OTLP_HEADERS`, only ever sent to the explicit
|
|
147
|
+
endpoint above).
|
|
148
|
+
- `SEDIMENT_PI_TRANSCRIPTS` — explicit content opt-in. Only the exact value
|
|
149
|
+
`1` enables transcript extraction. It doesn't change decision capture,
|
|
150
|
+
Attribution, or gateway Session identity.
|
|
151
|
+
- `SEDIMENT_EXTRACT_ON_SETTLE` — a nonempty value also extracts on
|
|
152
|
+
`agent_settled`, provided `SEDIMENT_PI_TRANSCRIPTS=1`. Set it only for hosts
|
|
153
|
+
that use one Session per task. An interactive Session can settle before its
|
|
154
|
+
final edit; first-write-wins storage would retain that earlier observation.
|
|
155
|
+
A runtime reload never triggers transcript extraction.
|
|
156
|
+
- `OTEL_RESOURCE_ATTRIBUTES` — `user.id` is forwarded as resource identity.
|
|
157
|
+
- `SEDIMENT_SCRIPT_DIR` — fallback directory containing
|
|
158
|
+
`sediment_attribution.py`, `sediment_transcript.py`, and `sediment_delivery.py`.
|
|
159
|
+
Copy the delivery implementation from `cli/sediment_cli/delivery.py`, not the
|
|
160
|
+
checkout shim. The shim prefers an
|
|
161
|
+
installed `sediment` executable on `PATH`. Without one, each source-checkout
|
|
162
|
+
script resolves independently, so a missing transcript client doesn't
|
|
163
|
+
disable attribution.
|
|
164
|
+
- `SEDIMENT_PYTHON` — interpreter for the Python clients (default
|
|
165
|
+
`python3`; stdlib-only, any 3.12+ works).
|
|
166
|
+
- `SEDIMENT_DELIVERY_DIR` — opt-in private prepared-payload storage. Run
|
|
167
|
+
`sediment delivery replay --watch` under your existing process supervisor.
|
|
168
|
+
Startup requests a bounded drain; it doesn't install a background service.
|
|
169
|
+
See [Sender replay operations](../../docs/capture/local-capture.md#preserve-prepared-payloads-through-outages)
|
|
170
|
+
for content consent, limits, retention, active credentials, and recovery.
|
|
171
|
+
|
|
172
|
+
Child processes have a bounded deadline. On macOS and Linux, the runner stops
|
|
173
|
+
its owned process group, including descendants. Spawn, stdin, timeout, and
|
|
174
|
+
nonzero-exit failures produce content-free channel diagnostics. The runner drains
|
|
175
|
+
stderr without relaying its contents. A successful enqueue requires a matching
|
|
176
|
+
structured acknowledgment; exit zero alone doesn't prove durable acceptance.
|
|
177
|
+
Pi explicitly requests the shared helper's storage-fault fallback. If private
|
|
178
|
+
storage is unsafe, unavailable, or busy, one direct attempt preserves the prepared
|
|
179
|
+
Decision. A validated direct acknowledgment reports `best_effort` and its storage
|
|
180
|
+
reason. Capacity and identity declines remain failures. Repair storage even when
|
|
181
|
+
the direct send succeeds; that send has no durable recovery guarantee.
|
|
182
|
+
|
|
183
|
+
## Develop
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
npm ci
|
|
187
|
+
npm test # node --test (type stripping; Node 24 or Node 22.18+)
|
|
188
|
+
npm run typecheck # tsc --noEmit
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `shims` workflow builds and installs Sediment wheels. Its tests load the
|
|
192
|
+
registered extension through pi's native loader outside the checkout and verify
|
|
193
|
+
capture through the installed command. Packaging, registration, and shared Python
|
|
194
|
+
delivery changes also trigger this workflow. To exercise the same tests locally,
|
|
195
|
+
set `SEDIMENT_PI_TEST_PYTHON` to the installed environment's Python executable
|
|
196
|
+
and `SEDIMENT_PI_TEST_INSTALLED_BIN` to its `bin` directory before `npm test`.
|
|
197
|
+
|
|
198
|
+
The lockfile pins pi 0.86.1 (`@earendil-works/pi-coding-agent`) as a development
|
|
199
|
+
dependency. The native retrieval test exercises its extension registration,
|
|
200
|
+
argument validation, agent loop, and model-visible tool result with a scripted
|
|
201
|
+
model stream. It makes no paid inference request and downloads no package during
|
|
202
|
+
the test. Run these native checks on Node 24; pi's own minimum is Node 22.19.
|
|
203
|
+
Runtime extension imports remain standard-library imports; pi types are erased.
|
|
204
|
+
Source installation still needs no shim `node_modules` directory.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// sediment-pi — the pi-harness shim for Sediment capture.
|
|
3
|
+
//
|
|
4
|
+
// Closes pi's capture gaps client-side, per docs/agents/capture-clients.md:
|
|
5
|
+
// decisions — edit-tool executions emit sediment.tool_decision records
|
|
6
|
+
// to $SEDIMENT_OTLP_ENDPOINT/v1/logs, through the Python
|
|
7
|
+
// delivery helper when buffering is enrolled (accepted,
|
|
8
|
+
// explicit=false: stock pi has no human approval gesture)
|
|
9
|
+
// attribution — edit-tool executions invoke sediment_attribution.py
|
|
10
|
+
// `mark --tool pi` (the stamper is tool-agnostic)
|
|
11
|
+
// transcripts — session_shutdown invokes sediment_transcript.py
|
|
12
|
+
// `--agent pi` (the pair is the fact; ADR 0007)
|
|
13
|
+
// completions — the native provider-header event attaches the current
|
|
14
|
+
// Session to requests for the configured gateway
|
|
15
|
+
//
|
|
16
|
+
// All capture is best-effort: a failure degrades the signal to the
|
|
17
|
+
// notes/jaccard backstop and must never break the agent.
|
|
18
|
+
|
|
19
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
20
|
+
import { delimiter, dirname, join } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
register,
|
|
25
|
+
type CaptureProgram,
|
|
26
|
+
type Deps,
|
|
27
|
+
type PiLike,
|
|
28
|
+
} from "./lib/register.ts";
|
|
29
|
+
import { runCaptureProcess } from "./lib/process.ts";
|
|
30
|
+
import { registerRetrieval } from "./lib/retrieval.ts";
|
|
31
|
+
|
|
32
|
+
const POST_TIMEOUT_MS = 10_000;
|
|
33
|
+
const MAX_ACKNOWLEDGMENT_BYTES = 16_384;
|
|
34
|
+
|
|
35
|
+
async function post(
|
|
36
|
+
url: string,
|
|
37
|
+
payload: unknown,
|
|
38
|
+
token: string | null,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
41
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers,
|
|
45
|
+
body: JSON.stringify(payload),
|
|
46
|
+
redirect: "error",
|
|
47
|
+
signal: AbortSignal.timeout(POST_TIMEOUT_MS),
|
|
48
|
+
});
|
|
49
|
+
if (response.status !== 200 || !response.body) {
|
|
50
|
+
await response.body?.cancel();
|
|
51
|
+
throw new Error("invalid_acknowledgment");
|
|
52
|
+
}
|
|
53
|
+
const reader = response.body.getReader();
|
|
54
|
+
const chunks: Uint8Array[] = [];
|
|
55
|
+
let bytes = 0;
|
|
56
|
+
try {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
bytes += value.length;
|
|
61
|
+
if (bytes > MAX_ACKNOWLEDGMENT_BYTES) {
|
|
62
|
+
throw new Error("invalid_acknowledgment");
|
|
63
|
+
}
|
|
64
|
+
chunks.push(value);
|
|
65
|
+
}
|
|
66
|
+
const acknowledgment = JSON.parse(
|
|
67
|
+
new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)),
|
|
68
|
+
);
|
|
69
|
+
// OTLP acknowledgment describes delivery, never the number of Facts.
|
|
70
|
+
if (
|
|
71
|
+
!acknowledgment || Array.isArray(acknowledgment) ||
|
|
72
|
+
typeof acknowledgment !== "object" || Object.keys(acknowledgment).length !== 0
|
|
73
|
+
) {
|
|
74
|
+
throw new Error("invalid_acknowledgment");
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
await reader.cancel();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runScript(
|
|
82
|
+
script: string | CaptureProgram,
|
|
83
|
+
args: string[],
|
|
84
|
+
stdin: unknown,
|
|
85
|
+
captureOutput = false,
|
|
86
|
+
): Promise<string> {
|
|
87
|
+
// ponytail: PATH python3 (overridable via SEDIMENT_PYTHON) — the scripts
|
|
88
|
+
// are stdlib-only, so any 3.12+ interpreter runs them; the stamper's hook
|
|
89
|
+
// fragments pin sys.executable instead because hooks fire in bare envs.
|
|
90
|
+
const python = process.env.SEDIMENT_PYTHON ?? "python3";
|
|
91
|
+
const executable = typeof script === "string" ? python : script.executable;
|
|
92
|
+
const commandArgs =
|
|
93
|
+
typeof script === "string"
|
|
94
|
+
? [script, ...args]
|
|
95
|
+
: [...script.arguments, ...args];
|
|
96
|
+
return runCaptureProcess(executable, commandArgs, stdin, captureOutput);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findSediment(): string | null {
|
|
100
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
101
|
+
if (!dir) continue;
|
|
102
|
+
const candidate = join(dir, "sediment");
|
|
103
|
+
try {
|
|
104
|
+
accessSync(candidate, constants.X_OK);
|
|
105
|
+
return candidate;
|
|
106
|
+
} catch {
|
|
107
|
+
// Try the next PATH entry.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveScripts(): Deps["scripts"] {
|
|
114
|
+
const sediment = findSediment();
|
|
115
|
+
if (sediment) {
|
|
116
|
+
return {
|
|
117
|
+
attribution: { executable: sediment, arguments: [] },
|
|
118
|
+
transcript: { executable: sediment, arguments: ["transcript"] },
|
|
119
|
+
delivery: { executable: sediment, arguments: ["delivery"] },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Default: installed in place, <repo>/shims/pi/index.ts → <repo>/scripts.
|
|
123
|
+
const dir =
|
|
124
|
+
process.env.SEDIMENT_SCRIPT_DIR ||
|
|
125
|
+
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "scripts");
|
|
126
|
+
const attribution = join(dir, "sediment_attribution.py");
|
|
127
|
+
const transcript = join(dir, "sediment_transcript.py");
|
|
128
|
+
const delivery = join(dir, "sediment_delivery.py");
|
|
129
|
+
const resolved = {
|
|
130
|
+
attribution: existsSync(attribution) ? attribution : null,
|
|
131
|
+
transcript: existsSync(transcript) ? transcript : null,
|
|
132
|
+
delivery: existsSync(delivery) ? delivery : null,
|
|
133
|
+
};
|
|
134
|
+
if (!resolved.attribution)
|
|
135
|
+
console.error("sediment-pi: attribution capture degraded (script not found)");
|
|
136
|
+
if (!resolved.transcript)
|
|
137
|
+
console.error("sediment-pi: transcript capture degraded (script not found)");
|
|
138
|
+
return resolved.attribution || resolved.transcript || resolved.delivery
|
|
139
|
+
? resolved : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default function (pi: PiLike & Parameters<typeof registerRetrieval>[0]) {
|
|
143
|
+
const version = /^(22|24)\.(\d+)\.\d+$/.exec(process.versions.node);
|
|
144
|
+
if (!version || (version[1] === "22" && Number(version[2]) < 18)) {
|
|
145
|
+
console.error("sediment-pi: unsupported Node runtime; use Node 24 or Node 22.18+");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
registerRetrieval(pi, process.env);
|
|
149
|
+
register(pi, {
|
|
150
|
+
post,
|
|
151
|
+
runScript,
|
|
152
|
+
scripts: resolveScripts(),
|
|
153
|
+
env: process.env,
|
|
154
|
+
now: Date.now,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The Sediment client capture contract (docs/agents/capture-clients.md) —
|
|
3
|
+
// pure builders and env resolution. No I/O here; the wiring lives in
|
|
4
|
+
// lib/register.ts. The shapes emitted here are pinned against the golden
|
|
5
|
+
// server-side conformance fixture
|
|
6
|
+
// (packages/capture/tests/fixtures/otlp/sediment/tool_decision.json).
|
|
7
|
+
|
|
8
|
+
import { dirname } from "node:path";
|
|
9
|
+
|
|
10
|
+
export const DECISION_EVENT = "sediment.tool_decision";
|
|
11
|
+
|
|
12
|
+
// The contract scopes decisions to edit tools; reads stay out.
|
|
13
|
+
export const EDIT_TOOLS: ReadonlySet<string> = new Set(["edit", "write"]);
|
|
14
|
+
|
|
15
|
+
export type AnyValue = { stringValue: string } | { boolValue: boolean };
|
|
16
|
+
|
|
17
|
+
export interface OtlpAttribute {
|
|
18
|
+
key: string;
|
|
19
|
+
value: AnyValue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface OtlpRecord {
|
|
23
|
+
body: { stringValue: string };
|
|
24
|
+
timeUnixNano: string;
|
|
25
|
+
attributes: OtlpAttribute[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LogsPayload {
|
|
29
|
+
resourceLogs: [
|
|
30
|
+
{
|
|
31
|
+
resource: { attributes: OtlpAttribute[] };
|
|
32
|
+
scopeLogs: [{ logRecords: OtlpRecord[] }];
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DecisionInput {
|
|
38
|
+
agent: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
callId: string;
|
|
41
|
+
toolName: string;
|
|
42
|
+
decision: "accept" | "reject";
|
|
43
|
+
explicit: boolean;
|
|
44
|
+
filePath: string;
|
|
45
|
+
timeUnixNano: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildDecisionRecord(input: DecisionInput): OtlpRecord {
|
|
49
|
+
const strings: Record<string, string> = {
|
|
50
|
+
agent: input.agent,
|
|
51
|
+
"session.id": input.sessionId,
|
|
52
|
+
tool_use_id: input.callId,
|
|
53
|
+
tool_name: input.toolName,
|
|
54
|
+
decision: input.decision,
|
|
55
|
+
file_path: input.filePath,
|
|
56
|
+
};
|
|
57
|
+
const attributes: OtlpAttribute[] = Object.entries(strings).map(
|
|
58
|
+
([key, value]) => ({ key, value: { stringValue: value } }),
|
|
59
|
+
);
|
|
60
|
+
// explicit must stay a real bool on the wire — the server requires it.
|
|
61
|
+
attributes.push({ key: "explicit", value: { boolValue: input.explicit } });
|
|
62
|
+
return {
|
|
63
|
+
body: { stringValue: DECISION_EVENT },
|
|
64
|
+
timeUnixNano: input.timeUnixNano,
|
|
65
|
+
attributes,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildLogsPayload(
|
|
70
|
+
records: OtlpRecord[],
|
|
71
|
+
resourceAttrs: Record<string, string>,
|
|
72
|
+
): LogsPayload {
|
|
73
|
+
return {
|
|
74
|
+
resourceLogs: [
|
|
75
|
+
{
|
|
76
|
+
resource: {
|
|
77
|
+
attributes: Object.entries(resourceAttrs).map(([key, value]) => ({
|
|
78
|
+
key,
|
|
79
|
+
value: { stringValue: value },
|
|
80
|
+
})),
|
|
81
|
+
},
|
|
82
|
+
scopeLogs: [{ logRecords: records }],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface Env {
|
|
89
|
+
[key: string]: string | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The ingest endpoint, or null when not opted in. Deliberately NO
|
|
94
|
+
* OTEL_EXPORTER_OTLP_ENDPOINT fallback: /v1/logs is the standard OTLP path,
|
|
95
|
+
* so any collector in the machine's generic telemetry config would silently
|
|
96
|
+
* accept decision records bound for Sediment. Explicit opt-in only (same
|
|
97
|
+
* posture as the Python clients).
|
|
98
|
+
*/
|
|
99
|
+
export function resolveEndpoint(env: Env): string | null {
|
|
100
|
+
// WHATWG URL parsing tolerates surrounding whitespace, so trim before the
|
|
101
|
+
// slash-strip — mirrors the Python sibling (cli/sediment_cli/transcript.py).
|
|
102
|
+
const base = (env.SEDIMENT_OTLP_ENDPOINT ?? "").trim().replace(/\/+$/, "");
|
|
103
|
+
if (!base) return null;
|
|
104
|
+
let endpoint: URL;
|
|
105
|
+
try {
|
|
106
|
+
endpoint = new URL(base);
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const host = endpoint.hostname.toLowerCase();
|
|
111
|
+
const ipv4Loopback = /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
112
|
+
const loopback =
|
|
113
|
+
host === "localhost" || host === "::1" || host === "[::1]" || ipv4Loopback;
|
|
114
|
+
if (
|
|
115
|
+
!["http:", "https:"].includes(endpoint.protocol) ||
|
|
116
|
+
endpoint.username ||
|
|
117
|
+
endpoint.password ||
|
|
118
|
+
endpoint.search ||
|
|
119
|
+
endpoint.hash ||
|
|
120
|
+
!["/", "/v1/logs"].includes(endpoint.pathname) ||
|
|
121
|
+
(endpoint.protocol === "http:" && !loopback)
|
|
122
|
+
) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return base.endsWith("/v1/logs") ? base : `${base}/v1/logs`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Pairs of an OTLP-spec list: `Key=url-encoded-value,Key2=...`. */
|
|
129
|
+
function* pairs(list: string | undefined): Generator<[string, string]> {
|
|
130
|
+
for (const part of (list ?? "").split(",")) {
|
|
131
|
+
const eq = part.indexOf("=");
|
|
132
|
+
if (eq >= 0) yield [part.slice(0, eq).trim(), part.slice(eq + 1).trim()];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resolveToken(env: Env): string | null {
|
|
137
|
+
const token = env.SEDIMENT_INGEST_TOKEN;
|
|
138
|
+
if (token) return token;
|
|
139
|
+
// Safe to read the machine's generic OTLP headers here: the token is only
|
|
140
|
+
// ever sent to the explicit sediment endpoint above.
|
|
141
|
+
for (const [key, value] of pairs(env.OTEL_EXPORTER_OTLP_HEADERS)) {
|
|
142
|
+
if (key.toLowerCase() !== "authorization") continue;
|
|
143
|
+
const decoded = decodeURIComponent(value);
|
|
144
|
+
const space = decoded.indexOf(" ");
|
|
145
|
+
const scheme = space < 0 ? decoded : decoded.slice(0, space);
|
|
146
|
+
const credential = space < 0 ? "" : decoded.slice(space + 1).trim();
|
|
147
|
+
if (scheme.toLowerCase() === "bearer" && credential) return credential;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function resourceUserId(env: Env): string | null {
|
|
153
|
+
for (const [key, value] of pairs(env.OTEL_RESOURCE_ATTRIBUTES)) {
|
|
154
|
+
if (key === "user.id" && value) return value;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The decision a tool_execution_end carries, or null when there is none to
|
|
161
|
+
* emit. Stock pi has no human approval gesture, so an applied edit is an
|
|
162
|
+
* implicit accept; an errored execution never touched the file and emits
|
|
163
|
+
* nothing (absent, never guessed at).
|
|
164
|
+
*/
|
|
165
|
+
export function decisionFromToolEnd(
|
|
166
|
+
toolName: string,
|
|
167
|
+
isError: boolean,
|
|
168
|
+
): { decision: "accept"; explicit: false } | null {
|
|
169
|
+
if (!EDIT_TOOLS.has(toolName) || isError) return null;
|
|
170
|
+
return { decision: "accept", explicit: false };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** file_path from tool args; "" when unobservable (the reject convention). */
|
|
174
|
+
export function filePathFromArgs(args: unknown): string {
|
|
175
|
+
if (typeof args !== "object" || args === null) return "";
|
|
176
|
+
const path = (args as Record<string, unknown>).path;
|
|
177
|
+
return typeof path === "string" ? path : "";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The cwd `mark` should run in: the edited file's directory when the path is
|
|
182
|
+
* absolute, else the session cwd. ACP agents edit clones beneath the
|
|
183
|
+
* workspace root — the session cwd is not a git repo, so `mark` there heals
|
|
184
|
+
* nothing. Any directory inside the repo works: git walks up to the repo
|
|
185
|
+
* root.
|
|
186
|
+
*/
|
|
187
|
+
export function markCwd(filePath: string, sessionCwd: string): string {
|
|
188
|
+
return filePath.startsWith("/") ? dirname(filePath) : sessionCwd;
|
|
189
|
+
}
|