@ponythewhite/base-context 1.0.15 → 1.0.16
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.
- package/CHANGELOG.md +9 -0
- package/dist/base-context-runtime/pyproject.toml +1 -1
- package/dist/base-context-runtime/src/rlm/bash.py +19 -0
- package/dist/base-context-runtime/src/rlm/job_watch.py +156 -0
- package/dist/base-context-runtime/src/rlm/repl.md +28 -6
- package/dist/base-context-runtime/src/rlm/repl.py +6 -1
- package/dist/base-context-runtime/test/test_bash.py +4 -4
- package/dist/base-context-runtime/test/test_job_watch.py +67 -0
- package/dist/base-context-runtime/uv.lock +1 -1
- package/dist/build-info.json +1 -1
- package/dist/bundle/{bundled-modules-O67MEO74.js → bundled-modules-2DMTK7VK.js} +2 -2
- package/dist/bundle/{chunk-RQFC34FM.js → chunk-MSH6QRP5.js} +1601 -820
- package/dist/bundle/{chunk-DGO66I3L.js → chunk-T2OK7SRV.js} +3 -2
- package/dist/bundle/{cli-main-2RER6PBE.js → cli-main-WL2L5IEQ.js} +2 -2
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{main-YERLD3T4.js → main-XPIEIEQJ.js} +2 -2
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +155 -9
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/history-index-worker.d.ts.map +1 -1
- package/dist/core/history-index-worker.js +22 -1
- package/dist/core/history-index-worker.js.map +1 -1
- package/dist/core/history-index.d.ts +2 -0
- package/dist/core/history-index.d.ts.map +1 -1
- package/dist/core/history-index.js.map +1 -1
- package/dist/core/job-watch.d.ts +120 -0
- package/dist/core/job-watch.d.ts.map +1 -0
- package/dist/core/job-watch.js +648 -0
- package/dist/core/job-watch.js.map +1 -0
- package/dist/core/kernel/repl-manager.d.ts +5 -1
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +52 -0
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/kernel/shared.d.ts +12 -0
- package/dist/core/kernel/shared.d.ts.map +1 -1
- package/dist/core/kernel/shared.js.map +1 -1
- package/dist/core/prompts/rlm.d.ts.map +1 -1
- package/dist/core/prompts/rlm.js +49 -39
- package/dist/core/prompts/rlm.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +9 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/prime-context.d.ts.map +1 -1
- package/dist/core/tools/prime-context.js +1 -1
- package/dist/core/tools/prime-context.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/skills/agent-message/SKILL.md +8 -2
- package/dist/skills/bounded-inspect/SKILL.md +78 -0
- package/dist/skills/bounded-inspect/pyproject.toml +16 -0
- package/dist/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
- package/dist/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
- package/dist/skills/job-watch/SKILL.md +43 -0
- package/dist/skills/job-watch/pyproject.toml +13 -0
- package/dist/skills/job-watch/src/job_watch/__init__.py +61 -0
- package/dist/skills/rlm-heartbeat/SKILL.md +10 -6
- package/dist/utils/file-lines.d.ts.map +1 -1
- package/dist/utils/file-lines.js +6 -2
- package/dist/utils/file-lines.js.map +1 -1
- package/docs/rlm-runtime.md +4 -1
- package/docs/skills.md +18 -26
- package/package.json +4 -4
- package/skills/agent-message/SKILL.md +8 -2
- package/skills/bounded-inspect/SKILL.md +78 -0
- package/skills/bounded-inspect/pyproject.toml +16 -0
- package/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
- package/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
- package/skills/job-watch/SKILL.md +43 -0
- package/skills/job-watch/pyproject.toml +13 -0
- package/skills/job-watch/src/job_watch/__init__.py +61 -0
- package/skills/rlm-heartbeat/SKILL.md +10 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lines.js","sourceRoot":"","sources":["../../src/utils/file-lines.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"file-lines.js","sourceRoot":"","sources":["../../src/utils/file-lines.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE1E,MAAM,UAAU,uBAAuB,CACtC,QAAgB,EAChB,QAAQ,GAAG,EAAE,GAAG,IAAI,EACpB,eAAe,GAAG,KAAK,EACF;IACrB,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACnC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAClC,OAAO,QAAQ,GAAG,QAAQ,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC9F,IAAI,KAAK,KAAK,CAAC;gBAAE,MAAM;YACvB,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;YACxC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YACpC,IAAI,OAAO,KAAK,CAAC,CAAC,EAAE,CAAC;gBACpB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACzD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC9B,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;YAChC,QAAQ,IAAI,KAAK,CAAC;QACnB,CAAC;IACF,CAAC;YAAS,CAAC;QACV,SAAS,CAAC,EAAE,CAAC,CAAC;IACf,CAAC;IACD,OAAO,eAAe,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAAA,CAChF;AAED,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,QAAQ,GAAG,EAAE,GAAG,IAAI,EAAE,YAAY,GAAG,KAAK,EAAsB;IACnH,OAAO,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC,YAAY,CAAC;QAChE,EAAE,QAAQ,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;SAClB,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAAA,CACrB;AAED,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,kBAAkB,CACxC,QAAgB,EAChB,OAAO,GAAgF,EAAE,EAChE;IACzB,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,IAAI,QAAQ,CAAC;IAClD,IAAI,QAAQ,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAC9E,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC5C,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACnE,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,OAAO,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YACxC,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjF,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAC1C,IAAI,YAAY,GAAG,IAAI,CAAC,MAAM,GAAG,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;YACvF,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;gBAChB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACxB,YAAY,IAAI,IAAI,CAAC,MAAM,CAAC;gBAC5B,MAAM;YACP,CAAC;YACD,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC7B,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACxB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;gBACrE,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;gBACxB,YAAY,GAAG,CAAC,CAAC;gBACjB,MAAM,IAAI,CAAC;YACZ,CAAC;;gBAAM,MAAM,IAAI,CAAC;YAClB,KAAK,GAAG,GAAG,GAAG,CAAC,CAAC;QACjB,CAAC;IACF,CAAC;IACD,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;QACtD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QACvD,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;QACxB,YAAY,GAAG,CAAC,CAAC;QACjB,MAAM,IAAI,CAAC;IACZ,CAAC;AAAA,CACD","sourcesContent":["import { closeSync, createReadStream, openSync, readSync } from \"node:fs\";\n\nexport function readFirstLineBufferSync(\n\tfilePath: string,\n\tmaxBytes = 64 * 1024,\n\tallowIncomplete = false,\n): Buffer | undefined {\n\tconst fd = openSync(filePath, \"r\");\n\tconst chunks: Buffer[] = [];\n\tlet position = 0;\n\ttry {\n\t\tconst buffer = Buffer.alloc(1024);\n\t\twhile (position < maxBytes) {\n\t\t\tconst count = readSync(fd, buffer, 0, Math.min(buffer.length, maxBytes - position), position);\n\t\t\tif (count === 0) break;\n\t\t\tconst chunk = buffer.subarray(0, count);\n\t\t\tconst newline = chunk.indexOf(0x0a);\n\t\t\tif (newline !== -1) {\n\t\t\t\tchunks.push(Buffer.from(chunk.subarray(0, newline + 1)));\n\t\t\t\treturn Buffer.concat(chunks);\n\t\t\t}\n\t\t\tchunks.push(Buffer.from(chunk));\n\t\t\tposition += count;\n\t\t}\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n\treturn allowIncomplete && chunks.length > 0 ? Buffer.concat(chunks) : undefined;\n}\n\nexport function readFirstLineSync(filePath: string, maxBytes = 64 * 1024, completeOnly = false): string | undefined {\n\treturn readFirstLineBufferSync(filePath, maxBytes, !completeOnly)\n\t\t?.toString(\"utf8\")\n\t\t.replace(/\\n$/, \"\")\n\t\t.replace(/\\r$/, \"\");\n}\n\nexport async function* readLinesAsBuffers(\n\tfilePath: string,\n\toptions: { maxLineBytes?: number; includeNewline?: boolean; completeOnly?: boolean } = {},\n): AsyncGenerator<Buffer> {\n\tconst maxBytes = options.maxLineBytes ?? Infinity;\n\tif (maxBytes <= 0 || (maxBytes !== Infinity && !Number.isSafeInteger(maxBytes)))\n\t\tthrow new Error(\"Invalid line byte limit\");\n\tconst pendingParts: Buffer[] = [];\n\tlet pendingBytes = 0;\n\tfor await (const chunk of createReadStream(filePath)) {\n\t\tconst buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n\t\tlet start = 0;\n\t\twhile (start < buffer.length) {\n\t\t\tconst end = buffer.indexOf(0x0a, start);\n\t\t\tconst stop = end === -1 ? buffer.length : end + (options.includeNewline ? 1 : 0);\n\t\t\tconst part = buffer.subarray(start, stop);\n\t\t\tif (pendingBytes + part.length > maxBytes) throw new Error(\"Line byte limit exceeded\");\n\t\t\tif (end === -1) {\n\t\t\t\tpendingParts.push(part);\n\t\t\t\tpendingBytes += part.length;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (pendingParts.length > 0) {\n\t\t\t\tpendingParts.push(part);\n\t\t\t\tconst line = Buffer.concat(pendingParts, pendingBytes + part.length);\n\t\t\t\tpendingParts.length = 0;\n\t\t\t\tpendingBytes = 0;\n\t\t\t\tyield line;\n\t\t\t} else yield part;\n\t\t\tstart = end + 1;\n\t\t}\n\t}\n\tif (pendingParts.length > 0 && !options.completeOnly) {\n\t\tconst line = Buffer.concat(pendingParts, pendingBytes);\n\t\tpendingParts.length = 0;\n\t\tpendingBytes = 0;\n\t\tyield line;\n\t}\n}\n"]}
|
package/docs/rlm-runtime.md
CHANGED
|
@@ -90,6 +90,7 @@ Requests flow to the runtime on stdin and events return on stdout, one JSON obje
|
|
|
90
90
|
|
|
91
91
|
```text
|
|
92
92
|
requests execute, interrupt, host_reply, snapshot, restore, list_names, shutdown
|
|
93
|
+
optional job_watch_probe (job_watch_probe_v1 capability)
|
|
93
94
|
events ready, stdout, stderr, result, display, host_request, error, done
|
|
94
95
|
```
|
|
95
96
|
|
|
@@ -97,6 +98,8 @@ Output events carry the id of the cell that was running when the bytes were prod
|
|
|
97
98
|
|
|
98
99
|
Calls to `ReplKernelManager.execute()` are serialized. One kernel has one shared namespace and does not run two ordinary Python cells concurrently. RLM child agents can still run concurrently because each delegation uses a distinct host request and child runtime.
|
|
99
100
|
|
|
101
|
+
The optional `job_watch_probe_v1` ready capability supports contained read-only probes outside the cell FIFO and passive Bash completion callbacks. `AgentSession` schedules and compares observations before model admission, then uses ordinary prepared follow-ups. Explicit parking gates goal and autonomous continuation, not user input or unrelated work. No daemon protocol version changes; an older runtime without the capability is refused locally. See [job-watch](../skills/job-watch/SKILL.md) for reporting, restoration, and supported backlog limits.
|
|
102
|
+
|
|
100
103
|
## Host-Request Event Flow
|
|
101
104
|
|
|
102
105
|
A running cell can await task admission:
|
|
@@ -173,7 +176,7 @@ test_review = await rlm("review the tests", name="test-reviewer")
|
|
|
173
176
|
audit = await rlm("slow independent audit", name="audit-reviewer")
|
|
174
177
|
```
|
|
175
178
|
|
|
176
|
-
End the turn instead of waiting for completion.
|
|
179
|
+
End the turn instead of waiting for completion. Use `agent_message.send` for short coordination and `await agent_message.send_result(summary, findings, receiver_role="parent")` for substantial findings or final reports. Replies arrive over later turns; recover capsule details only when needed. Without that capability, use files for fan-in. The host runs each admitted child as an independent `AgentSession`; daemon-backed children can be retained as independently addressable session workers.
|
|
177
180
|
|
|
178
181
|
## Parent-Scoped Sub-Agent Registry
|
|
179
182
|
|
package/docs/skills.md
CHANGED
|
@@ -51,6 +51,8 @@ Base Context ships with built-in skills that load by default:
|
|
|
51
51
|
|
|
52
52
|
- `skill-creator` - teaches the agent to create new skills: markdown skill layout, frontmatter rules, placement and precedence, and the full Python-backed skill contract (package layout, `run()` convention, optional CLI, kernel venv behavior) with a working template in `references/python-skills.md`.
|
|
53
53
|
- `websearch` - a Python-backed Google search skill using the [Serper](https://serper.dev) API.
|
|
54
|
+
- [`job-watch`](../skills/job-watch/SKILL.md) - native job completion/probe monitoring, prepared reports, and scoped goal/autonomous waiting. Launch jobs through `bash()` or existing authorized helpers.
|
|
55
|
+
- [`bounded-inspect`](../skills/bounded-inspect/SKILL.md) - selected text/JSON evidence under shared source and serialized-output budgets. Historical conversation recovery remains with `prime_context`.
|
|
54
56
|
|
|
55
57
|
Built-in skills behave like any other skill but have the lowest precedence: a user, project, package, or `--skill` skill with the same name overrides the built-in one.
|
|
56
58
|
|
|
@@ -138,8 +140,9 @@ This is progressive disclosure: only descriptions are always in context, full in
|
|
|
138
140
|
|
|
139
141
|
For native epochs, select an advertised skill with
|
|
140
142
|
`prime_context` using `{"action":"skill","name":"..."}`. The response gives its
|
|
141
|
-
captured body and canonical source reference.
|
|
142
|
-
|
|
143
|
+
captured body and canonical source reference. If the complete body is already
|
|
144
|
+
in context, no duplicate read is needed. For later bounded recovery, use the
|
|
145
|
+
returned ref and field with `action="read"`; do not reopen the mutable location to replace
|
|
143
146
|
a selected version. A committed epoch keeps that selected descriptor and instruction
|
|
144
147
|
body. After an existing context transition commits a new epoch, a later selection
|
|
145
148
|
can capture updated contents. The original captured body remains recoverable from
|
|
@@ -157,18 +160,19 @@ Skills with `disable-model-invocation: true` are hidden from the startup skill l
|
|
|
157
160
|
|
|
158
161
|
For repeated multi-step work, start with an ordinary named Python function in
|
|
159
162
|
`ipython`. Keep one-off operations inline and prefer an existing project command
|
|
160
|
-
when it already does the work.
|
|
161
|
-
|
|
163
|
+
when it already does the work. Pass the current commit, configuration, paths,
|
|
164
|
+
resource identity, selectors and limits explicitly where applicable. Reuse the code, not its previous answer: read current
|
|
162
165
|
inputs on every call. Do not capture an open handle or hidden mutable REPL state.
|
|
163
166
|
|
|
164
|
-
After two useful occurrences
|
|
165
|
-
|
|
166
|
-
is a practical heuristic, not a repetition
|
|
167
|
+
After two useful occurrences, or an explicit request for reuse, save the function
|
|
168
|
+
in a small editable project `.py` file. Short project instructions are optional;
|
|
169
|
+
no new global skill is needed. This is a practical heuristic, not a repetition
|
|
170
|
+
detector. A small helper needs no
|
|
167
171
|
`pyproject.toml`, package installation, manifest or promotion step. Use the standard
|
|
168
172
|
library and dependencies already available in the selected environment.
|
|
169
173
|
|
|
170
|
-
For example, save `scripts/matching_lines.py` in
|
|
171
|
-
|
|
174
|
+
For example, save `scripts/matching_lines.py` in the project. This example is
|
|
175
|
+
for small files; use `bounded-inspect` for potentially large evidence:
|
|
172
176
|
|
|
173
177
|
```python
|
|
174
178
|
from pathlib import Path
|
|
@@ -188,26 +192,13 @@ def matching_lines(path: str, needle: str, limit: int = 20) -> list[tuple[int, s
|
|
|
188
192
|
return matches
|
|
189
193
|
```
|
|
190
194
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
```markdown
|
|
194
|
-
---
|
|
195
|
-
name: matching-lines
|
|
196
|
-
description: Read current UTF-8 files for repeated literal line lookups. Inputs are path, needle and limit; output is a list of one-based line numbers and text.
|
|
197
|
-
---
|
|
198
|
-
Load scripts/matching_lines.py afresh with runpy.run_path, then call
|
|
199
|
-
matching_lines(path, needle, limit=20). Resolve the script path against this
|
|
200
|
-
skill directory. No matches returns an empty list; ordinary errors propagate.
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
Only the short skill description enters discovery. Load the instructions and code
|
|
204
|
-
when the active task needs them. For this standard-library helper, a fresh load
|
|
205
|
-
and call in `ipython` can be:
|
|
195
|
+
For this standard-library helper, load the saved file afresh and call it in
|
|
196
|
+
`ipython`. No registration or package installation is needed:
|
|
206
197
|
|
|
207
198
|
```python
|
|
208
199
|
from runpy import run_path
|
|
209
200
|
|
|
210
|
-
matching_lines = run_path("/repo
|
|
201
|
+
matching_lines = run_path("/repo/scripts/matching_lines.py")["matching_lines"]
|
|
211
202
|
matches = matching_lines("/repo/current.txt", "needle", limit=20)
|
|
212
203
|
```
|
|
213
204
|
|
|
@@ -217,7 +208,8 @@ use the project's own environment instead of installing them into the kernel.
|
|
|
217
208
|
Fresh execution of editable code does not replace the captured instruction body
|
|
218
209
|
of an active native skill epoch.
|
|
219
210
|
|
|
220
|
-
|
|
211
|
+
Repetition never authorizes another deployment or experiment. Saving a helper
|
|
212
|
+
does not schedule or authorize execution. Invocation still needs
|
|
221
213
|
the normal model/tool decision or an explicit user workflow, under the existing
|
|
222
214
|
permissions. Do not add an import manager, dependency snapshots or per-call setup.
|
|
223
215
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ponythewhite/base-context",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Synerise base-context: a coding and research agent with durable context and a persistent Python REPL",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
45
|
-
"@ponythewhite/base-context-agent": "1.0.
|
|
46
|
-
"@ponythewhite/base-context-ai": "1.0.
|
|
47
|
-
"@ponythewhite/base-context-tui": "1.0.
|
|
45
|
+
"@ponythewhite/base-context-agent": "1.0.16",
|
|
46
|
+
"@ponythewhite/base-context-ai": "1.0.16",
|
|
47
|
+
"@ponythewhite/base-context-tui": "1.0.16",
|
|
48
48
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
49
49
|
"chalk": "^5.5.0",
|
|
50
50
|
"cli-highlight": "^2.1.11",
|
|
@@ -62,7 +62,8 @@ if child is not None:
|
|
|
62
62
|
|
|
63
63
|
## Result capsules
|
|
64
64
|
|
|
65
|
-
Use `send()` for short
|
|
65
|
+
Use `send()` for short steering, blockers, and decisions. Use `send_result()`
|
|
66
|
+
for substantial findings or a final public report, not for a short message:
|
|
66
67
|
|
|
67
68
|
```python
|
|
68
69
|
from pathlib import Path
|
|
@@ -74,7 +75,9 @@ receipt = await agent_message.send_result(
|
|
|
74
75
|
)
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
State
|
|
78
|
+
State the outcome, important caveats, decisions, and evidence references in the
|
|
79
|
+
capsule. Child findings are evidence, not independently verified truth. Retrieve
|
|
80
|
+
details only when needed. If it revises or
|
|
78
81
|
replaces earlier reports, name their exact report refs in the summary. With
|
|
79
82
|
several updates, skim the capsules first, then retrieve the last relevant
|
|
80
83
|
report or reports. Older reports remain readable. Do not treat unrelated
|
|
@@ -83,6 +86,9 @@ arrived. There is no automatic latest-wins, dropping, or coalescing.
|
|
|
83
86
|
|
|
84
87
|
## Safety
|
|
85
88
|
|
|
89
|
+
- Avoid routine acknowledgements and repeated status broadcasts. Messaging an
|
|
90
|
+
idle child can start another turn; not every message needs a reply.
|
|
91
|
+
|
|
86
92
|
- Do not delete a child immediately after `send`: delivered follow-ups may still
|
|
87
93
|
be running and queued receipts have not run yet. Wait until observation shows
|
|
88
94
|
the child is idle and its context is no longer needed before calling
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bounded-inspect
|
|
3
|
+
description: Inspect selected text windows and JSON fields from project files under shared source and serialized-output byte budgets. Emit one source-linked bundle; continue within oversized lines without repeating full reads.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Bounded Inspect
|
|
7
|
+
|
|
8
|
+
Use for repeated, selective file inspection. Keep tiny one-off reads inline.
|
|
9
|
+
Historical conversation evidence belongs to `prime_context`, not this skill.
|
|
10
|
+
Filesystem paths and byte cursors are **not native recovery refs**.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
bounded_inspect.emit(
|
|
14
|
+
bounded_inspect.text("runs/example/train.log", tail_lines=25),
|
|
15
|
+
bounded_inspect.json_fields("runs/example/metrics.json",
|
|
16
|
+
pointers=["/run", "/config", "/seed", "/eligible", "/failures",
|
|
17
|
+
"/units", "/updates", "/skips", "/wall"]),
|
|
18
|
+
max_bytes=4096, max_source_bytes=65536,
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Selectors are lazy; only `emit` reads. It prints one JSON object and returns
|
|
23
|
+
`None`, so a final cell expression does not duplicate the result. Stateless
|
|
24
|
+
extraction works in ordinary, goal, and AUTONOMOUS sessions. It does not park,
|
|
25
|
+
finish, or change autonomous limits. Consume the supplied evidence; do not
|
|
26
|
+
rerun its generating command merely to get a smaller answer.
|
|
27
|
+
|
|
28
|
+
## API and coverage
|
|
29
|
+
|
|
30
|
+
- `text(path)` selects the first 25 lines. Choose one of `head_lines=N`,
|
|
31
|
+
`tail_lines=N`, `start_line=N, end_line=M`, or `cursor=item["next"]`.
|
|
32
|
+
Line ranges are 1-based and inclusive; omitted `end_line` means through EOF.
|
|
33
|
+
- `json_fields(path, pointers=[...], max_source_bytes=65536)` uses exact JSON
|
|
34
|
+
pointers, including `~0` and `~1` escapes. An empty pointer selects the root.
|
|
35
|
+
Null stays null. Missing fields, invalid JSON, unavailable files, and oversized
|
|
36
|
+
values have distinct statuses. Selected values are never string-sliced. Numeric
|
|
37
|
+
tokens stay exact, including exponents and nested values; no float conversion.
|
|
38
|
+
- `emit(*selectors, max_bytes=4096, max_source_bytes=65536)` shares both budgets
|
|
39
|
+
across all selectors, in order. `max_bytes` must be at least 128 for refusal
|
|
40
|
+
metadata. Raise explicit limits when necessary, not by an unbounded retry.
|
|
41
|
+
|
|
42
|
+
`output_bytes` counts the complete UTF-8 JSON serialization, escaping, metadata,
|
|
43
|
+
paths, warnings, and final newline. `source_bytes` counts bytes actually read,
|
|
44
|
+
not file size or OS device traffic. The enclosing tool/protocol envelope is
|
|
45
|
+
outside this budget; normal `ipython` retention still applies. `omitted` identifies
|
|
46
|
+
unreturned selector indices in the original call; `omitted_fields` counts the
|
|
47
|
+
unreturned suffix of requested pointers. Order important metadata first.
|
|
48
|
+
|
|
49
|
+
Text includes a stat version, half-open byte range, starting line (or null),
|
|
50
|
+
and next byte cursor. Partial Unicode lines advance at UTF-8 boundaries.
|
|
51
|
+
A continuation advances, finishes, or explicitly refuses. Source-limited line
|
|
52
|
+
seeking can return an empty window with an advancing cursor. Reuse the same
|
|
53
|
+
path and returned cursor; modification or replacement returns `changed_source`.
|
|
54
|
+
Stat tokens are practical guards, not immutable captures.
|
|
55
|
+
|
|
56
|
+
Tail reads only a bounded suffix. If it cannot establish the requested start,
|
|
57
|
+
`tail_prefix_unread` marks missing prefix coverage; terminal `tail_window_limit`
|
|
58
|
+
means that coverage is incomplete. Absolute line numbers may be unknown. JSON parsing refuses files above the explicit source ceiling before
|
|
59
|
+
reading. Large histories need a project-native summary producer, not larger
|
|
60
|
+
unbounded `json.load` calls. Select scientific units, run/config/seed identifiers,
|
|
61
|
+
eligibility, and failures together. Extraction does not infer quality crossings,
|
|
62
|
+
repair failed controls, or turn unavailable measurements into zero.
|
|
63
|
+
|
|
64
|
+
## Run where the data lives
|
|
65
|
+
|
|
66
|
+
Use `python -m bounded_inspect`, or stage `src/bounded_inspect/__init__.py` once
|
|
67
|
+
and execute that file with standard-library Python through existing authorized
|
|
68
|
+
remote execution. Pass a JSON list on stdin:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
[{"kind":"text","path":"train.log","tail_lines":25},
|
|
72
|
+
{"kind":"json","path":"metrics.json","pointers":["/run","/units","/wall"]}]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
CLI flags: `--max-bytes` and `--max-source-bytes`. No SSH setup is provided.
|
|
76
|
+
Save a small parameterized project parser after demonstrated reuse. Log full
|
|
77
|
+
execution evidence to explicit files when needed: this skill cannot recover
|
|
78
|
+
bytes that Bash or another capture already discarded.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bounded-inspect"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Bounded text and selected JSON file evidence"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = []
|
|
7
|
+
|
|
8
|
+
[project.scripts]
|
|
9
|
+
bounded-inspect = "bounded_inspect:main"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/bounded_inspect"]
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Bounded file evidence. Select lazily; emit one shared-budget JSON bundle."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
__all__ = ["text", "json_fields", "emit"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class _Selection:
|
|
15
|
+
kind: str
|
|
16
|
+
path: str
|
|
17
|
+
options: dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def text(path, *, head_lines=None, tail_lines=None, start_line=None, end_line=None, cursor=None):
|
|
21
|
+
"""Select UTF-8 lines (1-based, inclusive), or resume a returned byte cursor."""
|
|
22
|
+
modes = sum(value is not None for value in (head_lines, tail_lines, start_line, cursor))
|
|
23
|
+
if modes > 1 or (end_line is not None and start_line is None):
|
|
24
|
+
raise ValueError("choose head, tail, line range, or cursor")
|
|
25
|
+
for value in (head_lines, tail_lines, start_line, end_line):
|
|
26
|
+
if value is not None and (not isinstance(value, int) or value < 1):
|
|
27
|
+
raise ValueError("line counts must be positive integers")
|
|
28
|
+
if start_line is not None and end_line is not None and end_line < start_line:
|
|
29
|
+
raise ValueError("end_line precedes start_line")
|
|
30
|
+
if modes == 0:
|
|
31
|
+
head_lines = 25
|
|
32
|
+
return _Selection("text", os.fspath(path), dict(head=head_lines, tail=tail_lines,
|
|
33
|
+
start=start_line, end=end_line, cursor=dict(cursor) if cursor else cursor))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def json_fields(path, *, pointers, max_source_bytes=65536):
|
|
37
|
+
"""Select exact RFC 6901 pointers; missing is distinct from JSON null."""
|
|
38
|
+
pointers = tuple(pointers)
|
|
39
|
+
if any(not isinstance(p, str) or (p and not p.startswith("/")) for p in pointers):
|
|
40
|
+
raise ValueError("pointers must be empty (root) or start with /")
|
|
41
|
+
if max_source_bytes < 0:
|
|
42
|
+
raise ValueError("max_source_bytes must be nonnegative")
|
|
43
|
+
return _Selection("json", os.fspath(path), dict(pointers=pointers, cap=max_source_bytes))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _version(info):
|
|
47
|
+
return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _JsonNumber(str):
|
|
51
|
+
"""A number token validated by json.loads, not converted through float/int."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _serialized(value):
|
|
55
|
+
if isinstance(value, _JsonNumber):
|
|
56
|
+
return str(value)
|
|
57
|
+
if isinstance(value, dict):
|
|
58
|
+
return "{" + ",".join(_serialized(key) + ":" + _serialized(item) for key, item in value.items()) + "}"
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return "[" + ",".join(_serialized(item) for item in value) + "]"
|
|
61
|
+
return json.dumps(value, ensure_ascii=True, allow_nan=False, separators=(",", ":"))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _encoded(bundle):
|
|
65
|
+
# Count the complete wire envelope, including escaping and the final newline.
|
|
66
|
+
while True:
|
|
67
|
+
rendered = _serialized(bundle) + "\n"
|
|
68
|
+
size = len(rendered.encode("utf-8"))
|
|
69
|
+
if bundle["output_bytes"] == size:
|
|
70
|
+
return rendered
|
|
71
|
+
bundle["output_bytes"] = size
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _read(handle, count, bundle):
|
|
75
|
+
data = handle.read(max(0, min(count, bundle["limits"]["source_bytes"] - bundle["source_bytes"])))
|
|
76
|
+
bundle["source_bytes"] += len(data)
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cursor(path, version, offset, line, start, end, warnings):
|
|
81
|
+
return dict(path=path, version=version, offset=offset, line=line,
|
|
82
|
+
start_line=start, end_line=end, warnings=warnings)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _text(handle, selection, version, bundle):
|
|
86
|
+
options = selection.options
|
|
87
|
+
size = version[2]
|
|
88
|
+
cap = bundle["limits"]["source_bytes"] - bundle["source_bytes"]
|
|
89
|
+
cursor = options["cursor"]
|
|
90
|
+
warnings = []
|
|
91
|
+
offset, line = 0, 1
|
|
92
|
+
start, end = options["start"] or 1, options["end"] or options["head"]
|
|
93
|
+
if cursor is not None:
|
|
94
|
+
if cursor.get("path") != selection.path or cursor.get("version") != version:
|
|
95
|
+
return {"status": "changed_source", "next": None}, None
|
|
96
|
+
try:
|
|
97
|
+
offset, line = cursor["offset"], cursor["line"]
|
|
98
|
+
start, end = cursor["start_line"], cursor["end_line"]
|
|
99
|
+
warnings = cursor["warnings"]
|
|
100
|
+
if not isinstance(offset, int) or not 0 <= offset <= size:
|
|
101
|
+
raise ValueError()
|
|
102
|
+
except (KeyError, TypeError, ValueError):
|
|
103
|
+
return {"status": "invalid_cursor", "next": None}, None
|
|
104
|
+
elif options["tail"] is not None:
|
|
105
|
+
offset = max(0, size - cap)
|
|
106
|
+
line = 1 if offset == 0 else None
|
|
107
|
+
end = None
|
|
108
|
+
handle.seek(offset)
|
|
109
|
+
raw = _read(handle, size - offset, bundle)
|
|
110
|
+
if options["tail"] is not None:
|
|
111
|
+
# Work backwards only within the bounded suffix, never scan the prefix.
|
|
112
|
+
boundaries = [i + 1 for i, value in enumerate(raw) if value == 10 and i + 1 < len(raw)]
|
|
113
|
+
needed = options["tail"]
|
|
114
|
+
cut = boundaries[-needed] if len(boundaries) >= needed else 0
|
|
115
|
+
if offset and not cut:
|
|
116
|
+
warnings.append("tail_prefix_unread; absolute_line_unavailable")
|
|
117
|
+
while cut < len(raw) and raw[cut] & 0xC0 == 0x80:
|
|
118
|
+
cut += 1
|
|
119
|
+
if line is not None:
|
|
120
|
+
line += raw[:cut].count(b"\n")
|
|
121
|
+
offset += cut
|
|
122
|
+
raw = raw[cut:]
|
|
123
|
+
# Seek a requested line in bounded chunks. Cursors can advance without text.
|
|
124
|
+
if line is not None:
|
|
125
|
+
while line < start:
|
|
126
|
+
newline = raw.find(b"\n")
|
|
127
|
+
if newline < 0:
|
|
128
|
+
offset += len(raw)
|
|
129
|
+
raw = b""
|
|
130
|
+
break
|
|
131
|
+
offset += newline + 1
|
|
132
|
+
raw = raw[newline + 1:]
|
|
133
|
+
line += 1
|
|
134
|
+
if end is not None:
|
|
135
|
+
limit = end - line + 1
|
|
136
|
+
if limit <= 0:
|
|
137
|
+
raw = b""
|
|
138
|
+
else:
|
|
139
|
+
pos = 0
|
|
140
|
+
for _ in range(limit):
|
|
141
|
+
newline = raw.find(b"\n", pos)
|
|
142
|
+
if newline < 0:
|
|
143
|
+
break
|
|
144
|
+
pos = newline + 1
|
|
145
|
+
else:
|
|
146
|
+
raw = raw[:pos]
|
|
147
|
+
try:
|
|
148
|
+
decoded = raw.decode("utf-8")
|
|
149
|
+
except UnicodeDecodeError as error:
|
|
150
|
+
if error.reason != "unexpected end of data" or offset + len(raw) == size:
|
|
151
|
+
return {"status": "invalid_utf8", "byte": offset + error.start, "next": None}, None
|
|
152
|
+
decoded = raw[:error.start].decode("utf-8")
|
|
153
|
+
context = (selection.path, version, offset, line, start, end, warnings)
|
|
154
|
+
if not decoded:
|
|
155
|
+
seeking = line is not None and line < start and offset < size
|
|
156
|
+
done = offset == size or (end is not None and line is not None and line > end)
|
|
157
|
+
status = "complete" if done else "seeking" if seeking and raw == b"" and cap else "source_limit"
|
|
158
|
+
next_cursor = _cursor(*context) if status == "seeking" else None
|
|
159
|
+
return dict(status=status, text="", range={"bytes": [offset, offset], "line": line},
|
|
160
|
+
next=next_cursor, warnings=warnings), None
|
|
161
|
+
return {}, (decoded, context)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _text_prefix(decoded, context, length):
|
|
165
|
+
path, version, offset, line, start, end, warnings = context
|
|
166
|
+
value = decoded[:length]
|
|
167
|
+
stop = offset + len(value.encode("utf-8"))
|
|
168
|
+
next_line = None if line is None else line + value.count("\n")
|
|
169
|
+
done = stop == version[2] or (end is not None and next_line is not None and next_line > end)
|
|
170
|
+
status = ("tail_window_limit" if warnings else "complete") if done else "partial" if length else "output_limit"
|
|
171
|
+
following = None if done or not length else _cursor(path, version, stop, next_line, start, end, warnings)
|
|
172
|
+
return dict(status=status, text=value, range={"bytes": [offset, stop], "line": line},
|
|
173
|
+
next=following, warnings=warnings)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _constant(value):
|
|
177
|
+
raise ValueError("nonstandard JSON constant")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _json(handle, selection, version, bundle):
|
|
181
|
+
cap = min(selection.options["cap"], bundle["limits"]["source_bytes"] - bundle["source_bytes"])
|
|
182
|
+
if version[2] > cap:
|
|
183
|
+
return {"status": "source_limit", "size_bytes": version[2], "source_ceiling": cap}, None
|
|
184
|
+
raw = _read(handle, version[2], bundle)
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(raw, parse_float=_JsonNumber, parse_int=_JsonNumber, parse_constant=_constant)
|
|
187
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
188
|
+
return {"status": "invalid_json"}, None
|
|
189
|
+
fields = []
|
|
190
|
+
for pointer in selection.options["pointers"]:
|
|
191
|
+
value = data
|
|
192
|
+
try:
|
|
193
|
+
for part in pointer.split("/")[1:] if pointer else []:
|
|
194
|
+
if any(part[i:i + 2] not in ("~0", "~1") for i, ch in enumerate(part) if ch == "~"):
|
|
195
|
+
raise ValueError()
|
|
196
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
197
|
+
if isinstance(value, list):
|
|
198
|
+
if not part.isascii() or not part.isdigit() or (len(part) > 1 and part[0] == "0"):
|
|
199
|
+
raise KeyError()
|
|
200
|
+
value = value[int(part)]
|
|
201
|
+
elif isinstance(value, dict):
|
|
202
|
+
value = value[part]
|
|
203
|
+
else:
|
|
204
|
+
raise KeyError()
|
|
205
|
+
fields.append(dict(pointer=pointer, status="ok", value=value))
|
|
206
|
+
except (KeyError, IndexError):
|
|
207
|
+
fields.append(dict(pointer=pointer, status="missing"))
|
|
208
|
+
except ValueError:
|
|
209
|
+
fields.append(dict(pointer=pointer, status="invalid_pointer"))
|
|
210
|
+
return {"status": "complete", "fields": [], "omitted_fields": len(fields)}, fields
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def emit(*selections, max_bytes=4096, max_source_bytes=65536):
|
|
214
|
+
"""Print one JSON bundle, within shared source/output byte limits. Return None."""
|
|
215
|
+
if not isinstance(max_bytes, int) or max_bytes < 128:
|
|
216
|
+
raise ValueError("max_bytes must be at least 128 (refusal envelope)")
|
|
217
|
+
if not isinstance(max_source_bytes, int) or max_source_bytes < 0:
|
|
218
|
+
raise ValueError("max_source_bytes must be nonnegative")
|
|
219
|
+
if any(not isinstance(selection, _Selection) for selection in selections):
|
|
220
|
+
raise TypeError("emit accepts text/json_fields selectors")
|
|
221
|
+
bundle = dict(items=[], omitted={"from": 0, "count": len(selections), "reason": "output_budget"},
|
|
222
|
+
source_bytes=0, output_bytes=0,
|
|
223
|
+
limits={"output_bytes": max_bytes, "source_bytes": max_source_bytes})
|
|
224
|
+
if len(_encoded(bundle).encode("utf-8")) > max_bytes:
|
|
225
|
+
bundle = dict(status="output_limit", omitted_count=len(selections), source_bytes=0, output_bytes=0)
|
|
226
|
+
print(_encoded(bundle), end="")
|
|
227
|
+
return None
|
|
228
|
+
for index, selection in enumerate(selections):
|
|
229
|
+
before = bundle["source_bytes"]
|
|
230
|
+
item = dict(index=index, path=selection.path, kind=selection.kind)
|
|
231
|
+
details = None
|
|
232
|
+
try:
|
|
233
|
+
# Unbuffered regular files: the counter is actual bytes read, not requested bytes.
|
|
234
|
+
with open(selection.path, "rb", buffering=0) as handle:
|
|
235
|
+
info = os.fstat(handle.fileno())
|
|
236
|
+
if not stat.S_ISREG(info.st_mode):
|
|
237
|
+
result = {"status": "not_regular_file"}
|
|
238
|
+
else:
|
|
239
|
+
version = _version(info)
|
|
240
|
+
item["version"] = version
|
|
241
|
+
if selection.kind == "text":
|
|
242
|
+
result, details = _text(handle, selection, version, bundle)
|
|
243
|
+
else:
|
|
244
|
+
result, details = _json(handle, selection, version, bundle)
|
|
245
|
+
if _version(os.fstat(handle.fileno())) != version or _version(os.stat(selection.path)) != version:
|
|
246
|
+
result, details = {"status": "changed_source", "next": None}, None
|
|
247
|
+
item.update(result)
|
|
248
|
+
except OSError as error:
|
|
249
|
+
item.update(status="unavailable", error=error.__class__.__name__, errno=error.errno)
|
|
250
|
+
item["source_bytes"] = bundle["source_bytes"] - before
|
|
251
|
+
bundle["items"].append(item)
|
|
252
|
+
bundle["omitted"].update({"from": index + 1, "count": len(selections) - index - 1})
|
|
253
|
+
|
|
254
|
+
def fits():
|
|
255
|
+
return len(_encoded(bundle).encode("utf-8")) <= max_bytes
|
|
256
|
+
|
|
257
|
+
if details is not None and selection.kind == "text":
|
|
258
|
+
decoded, context = details
|
|
259
|
+
item.update(_text_prefix(decoded, context, len(decoded)))
|
|
260
|
+
if not fits():
|
|
261
|
+
low, high = 0, len(decoded)
|
|
262
|
+
while low < high:
|
|
263
|
+
middle = (low + high + 1) // 2
|
|
264
|
+
item.update(_text_prefix(decoded, context, middle))
|
|
265
|
+
if fits():
|
|
266
|
+
low = middle
|
|
267
|
+
else:
|
|
268
|
+
high = middle - 1
|
|
269
|
+
item.update(_text_prefix(decoded, context, low))
|
|
270
|
+
elif details is not None:
|
|
271
|
+
for field_index, field in enumerate(details):
|
|
272
|
+
item["fields"].append(field)
|
|
273
|
+
item["omitted_fields"] = len(details) - field_index - 1
|
|
274
|
+
if not fits():
|
|
275
|
+
item["fields"][-1] = dict(pointer=field["pointer"], status="value_limit")
|
|
276
|
+
item["status"] = "partial"
|
|
277
|
+
if not fits():
|
|
278
|
+
item["fields"].pop()
|
|
279
|
+
item["omitted_fields"] += 1
|
|
280
|
+
break
|
|
281
|
+
if not fits():
|
|
282
|
+
bundle["items"].pop()
|
|
283
|
+
bundle["omitted"].update({"from": index, "count": len(selections) - index})
|
|
284
|
+
break
|
|
285
|
+
# Budget refusal may follow real I/O, so keep those source bytes in the envelope.
|
|
286
|
+
rendered = _encoded(bundle)
|
|
287
|
+
if len(rendered.encode("utf-8")) > max_bytes:
|
|
288
|
+
bundle = dict(status="output_limit", omitted_count=len(selections),
|
|
289
|
+
source_bytes=bundle["source_bytes"], output_bytes=0)
|
|
290
|
+
rendered = _encoded(bundle)
|
|
291
|
+
print(rendered, end="")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def main():
|
|
296
|
+
"""Standalone: stage this file once, then pass a small JSON request on stdin."""
|
|
297
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
298
|
+
parser.add_argument("--max-bytes", type=int, default=4096)
|
|
299
|
+
parser.add_argument("--max-source-bytes", type=int, default=65536)
|
|
300
|
+
args = parser.parse_args()
|
|
301
|
+
request = json.load(sys.stdin)
|
|
302
|
+
selections = []
|
|
303
|
+
for entry in request:
|
|
304
|
+
entry = dict(entry)
|
|
305
|
+
kind, path = entry.pop("kind"), entry.pop("path")
|
|
306
|
+
if kind not in ("text", "json"):
|
|
307
|
+
parser.error("kind must be text or json")
|
|
308
|
+
selections.append((text if kind == "text" else json_fields)(path, **entry))
|
|
309
|
+
emit(*selections, max_bytes=args.max_bytes, max_source_bytes=args.max_source_bytes)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
main()
|