@data-matters.io/agent-harness 0.1.8 → 0.1.9
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/package.json
CHANGED
|
@@ -35,6 +35,18 @@ harness_load_env() {
|
|
|
35
35
|
HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID -u HARNESS_ANTHROPIC_API_KEY -u HARNESS_ANTHROPIC_BASE_URL claude)
|
|
36
36
|
|
|
37
37
|
# ------------------------------------------- optional API key (ADR 0020) --
|
|
38
|
+
# harness_session_error_text <session.json>: the text of an errored session (its
|
|
39
|
+
# result and api_error), or the raw output when it is not JSON. Empty when the
|
|
40
|
+
# session did not fail.
|
|
41
|
+
harness_session_error_text() {
|
|
42
|
+
node -e "$harness_session_js"'
|
|
43
|
+
const raw = (() => { try { return require("fs").readFileSync(process.argv[1], "utf8"); } catch { return ""; } })();
|
|
44
|
+
const d = session(process.argv[1]);
|
|
45
|
+
process.stdout.write(Object.keys(d).length
|
|
46
|
+
? (d.is_error ? [d.result, d.api_error].filter((x) => typeof x === "string").join(" ") : "")
|
|
47
|
+
: raw);' "$1" 2>/dev/null
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
# harness_api_refused <session.json>: the session ended on an error that says
|
|
39
51
|
# the API cannot serve it: no credit, a quota, a spend or usage limit, billing,
|
|
40
52
|
# an invalid key, or the gateway itself failing (503 or another 5xx, a
|
|
@@ -43,12 +55,7 @@ HARNESS_CLAUDE=(env -u HARNESS_TELEGRAM_BOT_TOKEN -u HARNESS_TELEGRAM_CHAT_ID -u
|
|
|
43
55
|
# a task about authentication or billing never matches by its own words.
|
|
44
56
|
harness_api_refused() {
|
|
45
57
|
local text
|
|
46
|
-
text="$(
|
|
47
|
-
const raw = (() => { try { return require("fs").readFileSync(process.argv[1], "utf8"); } catch { return ""; } })();
|
|
48
|
-
const d = session(process.argv[1]);
|
|
49
|
-
process.stdout.write(Object.keys(d).length
|
|
50
|
-
? (d.is_error ? [d.result, d.api_error].filter((x) => typeof x === "string").join(" ") : "")
|
|
51
|
-
: raw);' "$1" 2>/dev/null)" || return 1
|
|
58
|
+
text="$(harness_session_error_text "$1")" || return 1
|
|
52
59
|
[[ -n "$text" ]] || return 1
|
|
53
60
|
harness_api_unreachable "$text" && return 0
|
|
54
61
|
harness_quota_transient "$text" && return 1
|
|
@@ -56,86 +63,30 @@ harness_api_refused() {
|
|
|
56
63
|
grep -qiE "credit balance|spend(ing)? limit|billing|usage limit|rate limit|out of usage|invalid api key|authentication" <<<"$text"
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# first (the key goes to that process only, through its environment, never its
|
|
80
|
-
# arguments or a log); when the API refuses it (harness_api_refused), the SAME
|
|
81
|
-
# attempt repeats on the plan login. Which one produced the output is written to
|
|
82
|
-
# <session.json>.source (api|plan), which the usage ledger reads.
|
|
83
|
-
# HARNESS_ANTHROPIC_BASE_URL is the optional base URL of that key (a gateway):
|
|
84
|
-
# it reaches the API run only, as ANTHROPIC_BASE_URL, and the plan run never
|
|
85
|
-
# sees it. A key from a gateway without its base URL would be sent to the
|
|
86
|
-
# Anthropic API and refused.
|
|
87
|
-
harness_claude_session() {
|
|
88
|
-
local secs="$1" out="$2" key="${HARNESS_ANTHROPIC_API_KEY:-}" base="${HARNESS_ANTHROPIC_BASE_URL:-}" rc=0 prompt mode unset=()
|
|
89
|
-
shift 2
|
|
90
|
-
rm -f "$out.source"
|
|
91
|
-
# The machine mode (ADR 0025) names the authentication to start on: api → the
|
|
92
|
-
# key and its base URL, plan → the account login with both removed, blocked →
|
|
93
|
-
# nothing runs. The fallback below stays as the net for a refusal that
|
|
94
|
-
# happens after the mode was read.
|
|
95
|
-
mode="$(harness_mode)"
|
|
96
|
-
if [[ "$mode" == blocked ]]; then
|
|
97
|
-
echo '{"type":"result","is_error":true,"api_error":"usage_limit_reached","result":"harness: the machine is blocked (the API does not respond and the account is out of usage); usage limit reached"}' >"$out"
|
|
98
|
-
return 1
|
|
99
|
-
fi
|
|
100
|
-
if [[ -z "$key" || "$mode" == plan ]]; then
|
|
101
|
-
# No key configured, or the API is down: the session is the account login,
|
|
102
|
-
# on the Anthropic API. A base URL left in the environment would point that
|
|
103
|
-
# login at a gateway, and a key must not reach it.
|
|
104
|
-
[[ -z "$key" ]] || unset=(-u ANTHROPIC_API_KEY)
|
|
105
|
-
[[ -z "$base" ]] || unset+=(-u ANTHROPIC_BASE_URL)
|
|
106
|
-
if (( ${#unset[@]} )); then
|
|
107
|
-
timeout "$secs" env "${unset[@]}" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
|
|
108
|
-
else
|
|
109
|
-
timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
|
|
110
|
-
fi
|
|
111
|
-
[[ -z "$key" ]] || echo plan >"$out.source"
|
|
112
|
-
return "$rc"
|
|
113
|
-
fi
|
|
114
|
-
prompt="$(mktemp)"
|
|
115
|
-
cat >"$prompt"
|
|
116
|
-
if [[ -n "$base" ]]; then
|
|
117
|
-
ANTHROPIC_API_KEY="$key" ANTHROPIC_BASE_URL="$base" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
118
|
-
else
|
|
119
|
-
ANTHROPIC_API_KEY="$key" timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
120
|
-
fi
|
|
121
|
-
# Whatever the session printed, the key itself never stays in the log.
|
|
122
|
-
K="$key" node -e 'const fs = require("fs"); const f = process.argv[1];
|
|
123
|
-
try { const s = fs.readFileSync(f, "utf8"); if (s.includes(process.env.K)) fs.writeFileSync(f, s.split(process.env.K).join("[redacted]")); } catch {}' "$out" 2>/dev/null || true
|
|
124
|
-
if (( rc != 124 )) && harness_api_refused "$out"; then
|
|
125
|
-
echo "harness: the API did not serve the session (refused, or the gateway failed); repeating it on the plan login" >&2
|
|
126
|
-
rc=0
|
|
127
|
-
if [[ -n "$base" ]]; then
|
|
128
|
-
timeout "$secs" env -u ANTHROPIC_API_KEY -u ANTHROPIC_BASE_URL "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
129
|
-
else
|
|
130
|
-
timeout "$secs" env -u ANTHROPIC_API_KEY "${HARNESS_CLAUDE[@]}" "$@" <"$prompt" >"$out" 2>&1 || rc=$?
|
|
131
|
-
fi
|
|
132
|
-
echo plan >"$out.source"
|
|
133
|
-
else
|
|
66
|
+
# ----------------------------------- machine layer shim (ADR 0026, section 9) --
|
|
67
|
+
# The session wrapper and the machine mode live in the machine layer,
|
|
68
|
+
# machine/session.sh, in the VPS runtime (HARNESS_WATCH_RUNTIME, default
|
|
69
|
+
# ~/.local/share/agent-harness/watch). HARNESS_MACHINE_API=1 is the contract.
|
|
70
|
+
# Without a runtime (CI, a laptop, a clone elsewhere) a session is a plain claude
|
|
71
|
+
# with the caller's own environment and the mode is api: the degraded fallback of
|
|
72
|
+
# ADR 0025, section 8. A key from HARNESS_ANTHROPIC_API_KEY is not used then.
|
|
73
|
+
# shellcheck disable=SC2034 # the contract marker, read by the runtime
|
|
74
|
+
HARNESS_MACHINE_API=1
|
|
75
|
+
harness_machine_session_file() { printf '%s' "${HARNESS_WATCH_RUNTIME:-$HOME/.local/share/agent-harness/watch}/machine/session.sh"; }
|
|
76
|
+
if [[ -r "$(harness_machine_session_file)" ]]; then
|
|
77
|
+
# shellcheck source=/dev/null
|
|
78
|
+
. "$(harness_machine_session_file)"
|
|
79
|
+
else
|
|
80
|
+
harness_mode() { echo api; }
|
|
81
|
+
harness_claude_session() { # <timeout-seconds> <session.json> <claude args...>
|
|
82
|
+
local secs="$1" out="$2" rc=0
|
|
83
|
+
shift 2
|
|
84
|
+
rm -f "$out.source"
|
|
85
|
+
timeout "$secs" "${HARNESS_CLAUDE[@]}" "$@" >"$out" 2>&1 || rc=$?
|
|
134
86
|
echo api >"$out.source"
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
87
|
+
return "$rc"
|
|
88
|
+
}
|
|
89
|
+
fi
|
|
139
90
|
|
|
140
91
|
# Worktree where the task loop runs a change (ADR 0012), outside the checkout so
|
|
141
92
|
# lint, tests and type-checks of the main checkout never see it:
|
|
@@ -28,5 +28,6 @@ TASK_ci_fix_retry=R3 # failing checks after a failed fix (watcher, ADR 0019)
|
|
|
28
28
|
TASK_review_comments=R2 # owner comments on implementation/sync PRs (ADR 0019, 1b)
|
|
29
29
|
TASK_diagnosis=R3 # explaining an escalation
|
|
30
30
|
TASK_review=R3 # PR review
|
|
31
|
+
TASK_issue_fix=R3 # a fix for an open harness-fault issue (ADR 0027, decision 5); unused until fix mode is enabled
|
|
31
32
|
TASK_archive_fix=R1 # editorial split of a merged requirement over 500 characters (ADR 0022), gate-verified
|
|
32
33
|
TASK_routine=R1 # hand edits around sync and archive
|
|
@@ -34,8 +34,11 @@ harness_quota_markers='limite de uso do seu plano|cota semanal|sera resetado em|
|
|
|
34
34
|
# Transient markers, on normalized text: the request may be retried.
|
|
35
35
|
harness_quota_transient_markers='o consumo da requisicao anterior esta sendo atualizado|tente novamente em alguns segundos|temporarily at capacity|repeated [0-9]+ overloaded errors'
|
|
36
36
|
|
|
37
|
-
# Gateway failures, on normalized text: the API does not answer.
|
|
38
|
-
|
|
37
|
+
# Gateway failures, on normalized text: the API does not answer. This is the
|
|
38
|
+
# one list for the whole system (ADR 0026, section 6): the machine library
|
|
39
|
+
# (vps-bootstrap) loads this file and keeps a copy only as a degraded fallback,
|
|
40
|
+
# and a test there fails if the two disagree.
|
|
41
|
+
harness_gateway_markers='api error:? *\(?5[0-9]{2}|(status|error|http)[ :=]+5[0-9]{2}\b|"api_error_status": ?5[0-9]{2}|\b50[0-9] (service unavailable|bad gateway|gateway time-?out)|service unavailable|bad gateway|gateway time-?out|overloaded|upstream|econnrefused|econnreset|etimedout|enotfound|eai_again|socket hang up|fetch failed|unable to connect|connection (error|refused|reset|closed)|timed? ?out|could not resolve|network error|context validation'
|
|
39
42
|
|
|
40
43
|
# harness_quota_transient <text>: a retryable condition, not a quota.
|
|
41
44
|
harness_quota_transient() {
|
|
@@ -54,3 +57,14 @@ harness_quota_exhausted() {
|
|
|
54
57
|
harness_api_unreachable() {
|
|
55
58
|
grep -qE -- "$harness_gateway_markers" <<<"$(harness_quota_normalize "$1")"
|
|
56
59
|
}
|
|
60
|
+
|
|
61
|
+
# harness_api_fault_class <text>: how a session error counts for the machine
|
|
62
|
+
# mode (ADR 0025 section 8, ADR 0026 sections 3 and 6). Prints one word:
|
|
63
|
+
# transient a passing failure: counts only after several in a row
|
|
64
|
+
# down the API does not serve: a quota, or a gateway failure
|
|
65
|
+
# none not a failure of the API
|
|
66
|
+
harness_api_fault_class() {
|
|
67
|
+
if harness_quota_transient "$1"; then printf 'transient'; return 0; fi
|
|
68
|
+
if harness_quota_exhausted "$1" || harness_api_unreachable "$1"; then printf 'down'; return 0; fi
|
|
69
|
+
printf 'none'
|
|
70
|
+
}
|