mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(cli): HF_TOOL_CONTEXT artifact events and project lock for agent harnesses (#5520)
* feat(cli): HF_TOOL_CONTEXT artifact events and project lock for agent harnesses * fix(cli): serialize stale project lock takeover Use a short-lived break lock so only one waiter can remove a dead holder's lock. Cover the contention path with six real processes. * fix(cli): hold snapshot lock through finalization Keep the project lock while contact sheets, artifact events, and frame descriptions read captured files so another snapshot cannot replace them. * fix(cli): recover a project lock after the breaker crashes A killed takeover left project.lock.break in place, so later commands waited out the timeout even though both holders were dead. Reclaim that file only when its recorded process is dead, and compare artifact paths after the platform resolves them. * fix(cli): accept a killed breaker process with no signal name Windows ends the process without reporting SIGKILL, so the regression was failing before it checked that the leftover breaker can be reclaimed. * fix(cli): leave a killed project-lock breaker in place Reclaiming a dead breaker unlinks a name another process may just have created, so two commands can hold the lock. The timeout names that file and its pid and says to delete it to continue.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: "Running the CLI from an agent harness"
|
||||
description: "How an agent harness declares what it supports through HF_TOOL_CONTEXT, and what the CLI reports back."
|
||||
---
|
||||
|
||||
An agent harness (Claude Code, Codex, your own) runs `hyperframes` commands on the agent's behalf.
|
||||
`HF_TOOL_CONTEXT` lets the harness tell one invocation what it supports, so the CLI can do more for
|
||||
it: report the images it produced, or wait its turn instead of racing another command in the same
|
||||
project. Without the variable, every command behaves exactly as it does in a terminal.
|
||||
|
||||
```bash
|
||||
export HF_TOOL_CONTEXT='{"version":1,"capabilities":{
|
||||
"events": {"transport": {"type": "ndjson-file", "path": "/tmp/call-42/events.ndjson"}},
|
||||
"project-lock": {"waitMs": 90000}
|
||||
}}'
|
||||
npx hyperframes snapshot --at 1,3
|
||||
```
|
||||
|
||||
The harness sets the variable for one tool call, reads what the call produced, and removes its files.
|
||||
|
||||
## The declaration
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| `version` | `1`. Any other version, or a value that is not JSON, is treated as no declaration. |
|
||||
| `capabilities` | An object keyed by capability name. Each value configures that capability. |
|
||||
|
||||
The CLI ignores capability names it does not know, and a known capability whose value is invalid, so
|
||||
a harness can declare capabilities newer than the CLI it runs. The variable carries data only: no
|
||||
capability runs code the harness names.
|
||||
|
||||
## `events`: what a command produced
|
||||
|
||||
```json
|
||||
{ "transport": { "type": "ndjson-file", "path": "/absolute/events.ndjson" } }
|
||||
```
|
||||
|
||||
The CLI appends one JSON object per line to `path`. It never creates the parent directory, and a
|
||||
write that fails prints a warning without failing the command. Read the file after the command
|
||||
exits; several commands in one shell call append to the same file.
|
||||
|
||||
Each line is an event:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"type": "artifact.created",
|
||||
"command": "snapshot",
|
||||
"artifact": { "role": "contact-sheet", "path": "/project/snapshots/contact-sheet.jpg", "mediaType": "image/jpeg" }
|
||||
}
|
||||
```
|
||||
|
||||
| Command | `role` | What it is |
|
||||
| --- | --- | --- |
|
||||
| `snapshot` | `frame` | Each captured frame (PNG) |
|
||||
| `snapshot` | `contact-sheet` | Each page of the grid of frames made for review (JPEG) |
|
||||
| `check` | `frame` | Each frame the browser check captured |
|
||||
| `check` | `finding` | Each crop around a finding |
|
||||
|
||||
Paths are absolute and stay where the command saved them. What to do with an artifact is the
|
||||
harness's decision: attach the contact sheet to the tool result, or ignore it.
|
||||
Ignore event types and roles you do not know; new ones are added without a version change.
|
||||
|
||||
## `project-lock`: one browser command at a time
|
||||
|
||||
```json
|
||||
{ "waitMs": 90000 }
|
||||
```
|
||||
|
||||
`check` and `snapshot` take a lock on the project before they start a browser, and release it when
|
||||
they finish. A second command waits up to `waitMs` milliseconds for the lock, then fails, naming the
|
||||
command and process that hold it. A lock whose process has exited is taken over. A
|
||||
`project.lock.break` left by a killed process is not: the timeout names that file and its process
|
||||
id, and says to delete it to continue. The lock is the file `.hyperframes/project.lock`.
|
||||
|
||||
Choose `waitMs` below your tool call's own timeout, so a wait ends in the CLI's message rather than
|
||||
the harness killing the call.
|
||||
|
||||
## Hooks that run code
|
||||
|
||||
Running your own command before or after a CLI command is not a `HF_TOOL_CONTEXT` capability and
|
||||
never will be: anything that sets an environment variable could then run code. Such hooks, when they
|
||||
exist, come only from configuration the person controls.
|
||||
|
||||
## Conformance
|
||||
|
||||
`packages/cli/src/utils/toolContext.test.ts` and `projectLock.test.ts` pin the parsing, event and
|
||||
lock behavior above. A harness is correct when it sets the variable for each call it wants the
|
||||
behavior on, reads `events` only after the call exits, and works the same when the CLI ignores a
|
||||
capability.
|
||||
Reference in New Issue
Block a user