jeo-code 0.8.23 → 0.8.24
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 +16 -0
- package/README.ja.md +5 -1
- package/README.ko.md +5 -1
- package/README.md +5 -1
- package/README.zh.md +5 -1
- package/package.json +1 -1
- package/src/agent/engine.ts +56 -0
- package/src/agent/eval-tool.ts +433 -0
- package/src/agent/memory.ts +24 -3
- package/src/agent/plan.ts +55 -0
- package/src/agent/skill-lessons.ts +2 -1
- package/src/agent/state.ts +26 -0
- package/src/agent/task-tool.ts +61 -14
- package/src/commands/launch.ts +79 -3
- package/src/commands/ralplan.ts +45 -12
- package/src/commands/team.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.8.24] - 2026-07-13
|
|
10
|
+
_Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Independent verifier: maker→verifier ORDERING enforced, not just present** (`src/agent/plan.ts`'s `PlanSchema`) — a plan could previously contain zero architect/critic steps at all, or place one BEFORE the mutating work it was supposed to check, and `team.ts`'s evidence-grounded role gate would never fire for it (nothing REQUIRED a verifier to exist in the right position). `PlanSchema` now walks the plan (parallel_group steps evaluated as one atomic unit — concurrent siblings cannot verify each other's still-in-flight changes) and fails the schema itself when a plan ends with an unverified mutation, at BOTH write time (`ralplan.ts`'s drafting/repair loop, feeding the real zod error back into the repair prompt instead of a generic message) and approval/execution time (`approve.ts`, `team.ts`) — a schema-invalid plan can no longer be approved or executed at all, closing the gap between "the gate exists" and "the gate cannot be skipped."
|
|
14
|
+
- **Evidence gate: independent verifiers must show their work** (`src/agent/task-tool.ts`'s `SubagentRunResult.readOnlyEvidenceCalls`, `src/commands/team.ts`'s `evaluateSubagentResult`, `src/commands/ralplan.ts`'s `runConsensusCriticGate`) — an architect/critic verdict (`[OKAY]`/`Architectural Status: CLEAR`) was text the model could emit with ZERO real inspection; both independent-verifier code paths in jeo (the per-step role gate AND ralplan's separate consensus-critic gate) now count OBSERVED `read`/`search`/`find`/`ast_grep`/`lsp` calls during the run and hard-fail any read-only-role verdict with zero evidence calls — unconditional, never softened by `--strict-mutations`, mirroring `goal-verifier.ts`'s existing `applyEvidenceGate` philosophy applied to the OTHER verifier path in the codebase.
|
|
15
|
+
- **Safety-boundary automatic model fallback** (`src/agent/engine.ts`'s `AgentLoopOptions.safetyFallbackAvailable`/`SAFETY_FALLBACK_TAG`, wired into `src/agent/task-tool.ts` and `src/commands/launch.ts`) — an UNCATEGORIZED refusal (bare `stop_reason=refusal`/`finish_reason=content_filter`/Gemini `SAFETY`, distinct from a `Refusal (<category>)`-shaped deterministic content-policy hit, which is UNTOUCHED and still hard-fails with zero fallback) previously backed off forever on the SAME model. When a genuinely different-PROVIDER candidate is available (same-provider siblings share the same classifier, so they're excluded too — stricter than the rate-limit fallback's same-credential-scope-only exclusion), the engine now bails with a distinctly-tagged doneReason and the caller switches models instead of riding the unbounded backoff — mirrors the already-shipped rate-limit fast-fallback's exact reroute-and-retry shape (never a bail-only predicate with nothing to switch to), reusing its provider-exclusion accumulation across repeated bails. `launch.ts`'s `routeFailureReason`/`equivalentRouteFallback` gained the same provider-level exclusion for the main interactive turn, and the internal tag is stripped from every user-visible surface (never leaks into a reply or a `[route]` notice).
|
|
16
|
+
- **Memory: earned confidence, active staleness tracking** (`src/agent/memory.ts`'s `isConceptStale`, `upsertConceptFile`'s new `opts.verified`, `distillSessionMemory`'s new `opts.sessionVerified`) — a concept's `last_verified` date was written unconditionally on every distillation pass, meaning "verified" was really just "written," never actually earned by a real verification event. `last_verified` is now written ONLY when the caller explicitly marks the session's distillation as verified; `isConceptStale` (pure, no I/O) treats a concept as stale when it has never been verified OR its last verification is older than 30 days (configurable) — 2026 industry pattern: confidence must be earned and staleness must be actively tracked, not read off a passive write-timestamp. Also fixed: `.jeo/` now self-ignores in git on first creation (`ensureJeoGitignore` in `src/agent/state.ts`) — memory/skill-lesson writes under `.jeo/` are agent-local working state, and without this a CI routine (`jeo routine init` + `create-pull-request`) shipped `.jeo/memory/*`/`.jeo/skills/*` churn into every generated PR.
|
|
17
|
+
- **Dynamic Workflows: `eval` tool, an agent-authored orchestration primitive** (`src/agent/eval-tool.ts`, wired into `src/commands/launch.ts`) — `task`'s `tasks[]` batch is ONE declarative, single-stage, parallel-only fan-out with no way to pipe one subagent's output into the next, branch on a result, or run N stages with a barrier between them. `eval {code, timeoutMs?}` lets the agent write an actual async JS function body composing THREE orchestration primitives — `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)` (plus `log`) — with real loops/conditionals/sequencing. Runs in an isolated `Worker` (own OS thread) with a genuinely preemptive wall-clock timeout via `worker.terminate()` — NOT the same-process `AsyncFunction` pattern `browser {run}` uses, which cannot be given a real timeout at all (a synchronous bug blocks jeo's own event loop; `Promise.race` can never preempt it — verified empirically against a `while(true){}` reproduction). `task()` bridges back to the main thread over a `postMessage` RPC (config/credentials never enter the worker); every dispatch — however the script shapes it, including N raw concurrent `task()` calls that bypass `parallel()`/`pipeline()` entirely — is bounded by a real concurrency semaphore at `MAX_FANOUT` (4) in the RPC handler itself, plus a 40-dispatch total-count cap and a 20-item queue cap per `parallel()`/`pipeline()` call. Trust model: full `bash`-tier process access (NOT a security sandbox — `node:vm`/`vm2` offer no real isolation in 2026; documented as such), gated by the identical deep-interview mutation lock; never exposed to a subagent's own toolset (mirrors `task`'s own recursion-prevention exclusion).
|
|
18
|
+
|
|
19
|
+
### Verified
|
|
20
|
+
- Full `bun test` — 3046 pass / 0 fail across 301 files (2997 baseline (post-audit fixture repair for the 20 pre-existing tests broken by the new PlanSchema ordering rule and evidence gate, none weakened — Pattern A: trailing verifier step; Pattern B: a real read-tool-call evidence turn) + 49 new: 8 dedicated PlanSchema ordering-rule tests covering the reject/accept boundary the fixture repairs alone never exercised, 4 safety-fallback engine tests, 3 task-tool safety-reroute tests, 4 launch.ts safety-fallback end-to-end tests, 20 eval-tool tests, plus the memory/gitignore additions).
|
|
21
|
+
- `bun run typecheck` — no errors.
|
|
22
|
+
- Live end-to-end (real CLI entrypoint, no mocks): `jeo approve` on a schema-invalid (unverified-mutation) plan refuses with exit code 1 and the exact ordering-rule message; the real (unmocked) `eval` tool composing `pipeline()` → `parallel()` in one script and returning a structured result; a genuine `while(true){}` script terminated by the Worker timeout at ~803ms for an 800ms budget (proving REAL preemption, not a same-thread race that would have hung indefinitely — confirmed the bug is real by first reproducing the hang against a bare `Promise.race`, which required an external OS-level `timeout` to recover from).
|
|
23
|
+
- Each new feature's highest-value contract mutation-tested against the ACTUAL source, not merely reasoned about: the ordering rule (neutered `ctx.addIssue` in `plan.ts`, full suite re-run — exactly the 4 reject-direction tests among the 8 new dedicated ordering-rule tests failed (13 pass / 4 fail), every accept-direction and unrelated test stayed green, confirming the 12 earlier fixture repairs only ever proved the ACCEPT direction and closing that real coverage gap before shipping; restored, all 3046 green again); the evidence gate (zero-evidence verdict correctly blocked, one real read call correctly clears it); the safety fallback (categorized `Refusal (<category>)` NEVER checks `safetyFallbackAvailable`, confirmed via a spy — true-positive path fully untouched); the eval concurrency semaphore (10 raw concurrent `task()` calls bypassing `parallel()`/`pipeline()` entirely still peak at ≤4 in-flight, proven via a real overlapping-delay probe, not a label).
|
|
24
|
+
|
|
9
25
|
## [0.8.23] - 2026-07-12
|
|
10
26
|
_Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause._
|
|
11
27
|
|
package/README.ja.md
CHANGED
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
- **低コスト層のグレーダールーティング** — `/goal` の検証器、`critic` サブエージェントロール、固定されていない `task` ファンアウトバッチは、採点・実行対象の作業と同じフルプライスモデルに黙って乗る代わりに、デフォルトで低コストのクレデンシャル済みモデルを使用します(`resolveVerifierModel`、ブラウザの `verify` アクションについてはビジョン対応能力でフィルタされ、テキストのみの低コストモデルが添付スクリーンショットを黙って取りこぼすことがありません)。
|
|
51
51
|
- **`jeo routine init`** — スケジュール/issue/PR トリガーで jeo をヘッドレス実行する(`jeo "<prompt>" -p`) GitHub Actions ワークフローを、GitHub 自身のランナー上に生成します — ラップトップは不要で、jeo 自体の内部に新しい攻撃対象領域も一切増えません(インプロセスのスケジューラや Webhook リスナーはありません)。`--dry-run` でプレビュー、`--no-pr` でデフォルトの実行ごとの PR の代わりに直接コミット。
|
|
52
52
|
- **リモートサブエージェント可視化(Telegram)** — ボットを一度ペアリング(`jeo notify setup`)すれば、`jeo daemon start` がサブエージェントの状態遷移(開始 → 完了/失敗/キャンセル)ごとにメッセージを送り、`/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` を受け付けます。Telegram フォーラムトピック、インラインキーボード、画像添付を含む `gjc` 完全パリティを提供し、コマンドはペアリングされたチャットのみ許可されます。
|
|
53
|
+
- **独立した検証者、実際に強制** — プランはもう architect/critic ステップをスキップできません: `PlanSchema` は未検証の変更で終わるプランを拒否し(検証対象の変更より前に置かれた verifier も無効)、`ralplan` のドラフト時点と `team`/`approve` の実行時点の両方で適用されます。すべての architect/critic 評決も実際の証拠を示す必要があり、観測された `read`/`search`/`find`/`ast_grep`/`lsp` 呼び出しがゼロなら、テキストが何を主張していても評決はブロックされます。
|
|
54
|
+
- **セーフティバウンダリの自動モデルフォールバック** — 未分類のセーフティ拒否(本物のコンテンツポリシー違反ではなく、分類器の誤検知の可能性)は、同じモデルで永遠に譲歩するのではなく、実際に別プロバイダのモデルへ切り替わるようになりました — 既存のレート制限時の高速フォールバックと同じパターンです。`Refusal (<category>)` 形式の確定的な拒否は影響を受けず、引き続きフォールバックなしでハードフェイルします。
|
|
55
|
+
- **メモリ: 勝ち取った信頼** — コンセプトの検証日付は、あらゆる書き込みではなく、蒸留パスが明示的に検証済みとマークした場合にのみ記録されるようになりました。`isConceptStale` は受動的なタイムスタンプを信頼する代わりに、未検証(または30日超の陳腐化)のコンセプトを再検証が必要なものとして扱います。
|
|
56
|
+
- **ダイナミックワークフロー(`eval` ツール)** — サブエージェントディスパッチの周りに実際の JS 制御フローを書けます: `task(role, taskText, context?)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`log(message)` を組み合わせ、`task` の単一ステージ `tasks[]` バッチでは表現できない逐次/分岐オーケストレーションを構成します。隔離された Worker スレッドで、真のプリエンプティブなタイムアウト(同一プロセス内のレースではなく `worker.terminate()`)を伴って実行され — `bash` と同じフルプロセスの信頼度で、サンドボックスのふりはせず、同じ interview 変更ロックでゲートされます。
|
|
53
57
|
|
|
54
58
|
## インストール
|
|
55
59
|
|
|
@@ -277,11 +281,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
277
281
|
## 変更履歴 (Changelog)
|
|
278
282
|
|
|
279
283
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
284
|
+
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
280
285
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
281
286
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
282
287
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
283
288
|
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
284
|
-
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
285
289
|
|
|
286
290
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
287
291
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
- **저비용 등급 채점 라우팅** — `/goal` 검증기, `critic` 서브에이전트 역할, 그리고 고정되지 않은 `task` 팬아웃 배치는 채점·실행 대상인 작업과 같은 풀프라이스 모델에 조용히 편승하는 대신 기본적으로 저비용 크레덴셜 모델을 사용합니다(`resolveVerifierModel`, 브라우저 `verify` 액션에 대해서는 비전 능력으로 필터링되어 텍스트 전용 저비용 모델이 첨부된 스크린샷을 조용히 누락시키지 않도록 합니다).
|
|
51
51
|
- **`jeo routine init`** — 스케줄/이슈/PR 트리거에서 jeo를 헤드리스로 실행하는(`jeo "<prompt>" -p`) GitHub Actions 워크플로를 생성합니다, GitHub 자체 러너 위에서 — 노트북이 필요 없고, jeo 내부에 새로운 공격 표면도 전혀 추가되지 않습니다(인프로세스 스케줄러도 웹훅 리스너도 없음). `--dry-run`으로 미리보기, `--no-pr`로 기본값인 실행당 PR 대신 직접 커밋.
|
|
52
52
|
- **원격 서브에이전트 가시성(Telegram)** — 봇을 한 번 페어링(`jeo notify setup`)하면, `jeo daemon start`가 서브에이전트 상태 전환(시작 → 완료/실패/취소)마다 메시지를 보내고 `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>`를 되받습니다. 이제 Telegram 포럼 토픽, 인라인 키보드, 이미지 첨부 파일 지원 등 `gjc`와의 완전한 패리티를 제공하며, 명령은 페어링된 채팅에서만 허용됩니다.
|
|
53
|
+
- **실제로 강제되는 독립 검증자** — 플랜은 이제 architect/critic 단계를 건너뛸 수 없습니다: `PlanSchema`는 미검증 변이로 끝나는 모든 플랜을 거부하며(검증 대상 변이보다 앞에 배치된 검증자도 인정하지 않음), `ralplan` 드래프트 시점과 `team`/`approve` 실행 시점 양쪽 모두에 적용됩니다. 모든 architect/critic 평결도 실제 증거를 제시해야 하며, 관찰된 `read`/`search`/`find`/`ast_grep`/`lsp` 호출이 0건이면 텍스트가 무엇을 주장하든 평결이 차단됩니다.
|
|
54
|
+
- **안전 경계 자동 모델 폴백** — 미분류 안전 거부(실제 콘텐츠 정책 위반이 아니라 분류기의 오탐일 가능성)가 발생하면 이제 같은 모델에서 영원히 물러서는 대신 실제로 다른 프로바이더의 모델로 전환합니다 — 기존 rate-limit 빠른 폴백과 동일한 방식입니다. `Refusal (<category>)` 형태의 결정론적 거부는 영향받지 않고 여전히 폴백 없이 하드 실패합니다.
|
|
55
|
+
- **메모리: 획득한 신뢰** — 개념의 검증 날짜는 이제 모든 쓰기 시점이 아니라 증류 패스가 명시적으로 검증됨으로 표시할 때만 기록됩니다. `isConceptStale`은 수동적인 타임스탬프를 신뢰하는 대신 미검증(또는 30일 초과) 개념을 재검증이 필요한 것으로 처리합니다.
|
|
56
|
+
- **동적 워크플로(`eval` 도구)** — 서브에이전트 디스패치 주변에 실제 JS 제어 흐름을 작성합니다: `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)`, `log(message)`로 `task`의 단일 스테이지 `tasks[]` 배치로는 표현할 수 없는 순차/분기 오케스트레이션을 구성합니다. 격리된 Worker 스레드에서 진정한 선점형 타임아웃(같은 프로세스 내 경쟁이 아닌 `worker.terminate()`)으로 실행되며 — `bash`와 동일한 풀프로세스 신뢰 수준으로, 샌드박스인 척하지 않고, 동일한 인터뷰 변이 락으로 게이트됩니다.
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
## 설치
|
|
@@ -278,11 +282,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
278
282
|
## 변경 이력 (Changelog)
|
|
279
283
|
|
|
280
284
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
285
|
+
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
281
286
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
282
287
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
283
288
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
284
289
|
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
285
|
-
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
286
290
|
|
|
287
291
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
288
292
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -55,6 +55,10 @@ Run `jeo` inside a repository and it reads files, edits them, runs commands, and
|
|
|
55
55
|
- **Cheap-tier grader routing** — the `/goal` verifier, the `critic` subagent role, and unpinned `task` fan-out batches default to a cheap credentialed model instead of silently riding the same full-price model as the work they're grading/executing (`resolveVerifierModel`, vision-capability-filtered for the browser `verify` action so a text-only cheap model never silently drops an attached screenshot).
|
|
56
56
|
- **`jeo routine init`** — generates a GitHub Actions workflow that runs jeo headlessly (`jeo "<prompt>" -p`) on a schedule/issue/PR trigger, on GitHub's own runners — no laptop required, and zero new attack surface inside jeo itself (no in-process scheduler or webhook listener). `--dry-run` to preview, `--no-pr` for a direct commit instead of the default PR-per-run.
|
|
57
57
|
- **Remote subagent visibility (Telegram)** — pair a bot once (`jeo notify setup`), then `jeo daemon start` pushes a message on every subagent state edge (started → done/failed/cancelled) and accepts `/subagents`, `/steer <id> <subagentId> <msg>`, `/cancel <id> <subagentId>` back. Telegram Daemon now supports full `gjc` parity, including forum topics, inline keyboards, and image attachments — commands are authorized to the paired chat only.
|
|
58
|
+
- **Independent verifier, actually enforced** — a plan can no longer skip its architect/critic step: `PlanSchema` rejects any plan that ends with an unverified mutation (a verifier placed BEFORE the mutation it should check doesn't count either), at both `ralplan` draft time and `team`/`approve` execution time. Every architect/critic verdict must also show real evidence — zero observed `read`/`search`/`find`/`ast_grep`/`lsp` calls blocks the verdict regardless of what the text claims.
|
|
59
|
+
- **Safety-boundary automatic model fallback** — an uncategorized safety refusal (a possible classifier false positive, not a genuine content-policy hit) now switches to a genuinely different-provider model instead of backing off forever on the same one — mirrors the existing rate-limit fast-fallback. A `Refusal (<category>)`-shaped deterministic hit is untouched and still hard-fails with zero fallback.
|
|
60
|
+
- **Memory: earned confidence** — a concept's verification date is now written only when a distillation pass is explicitly marked verified, not on every write; `isConceptStale` treats an unverified (or >30-day-stale) concept as needing re-verification instead of trusting a passive timestamp.
|
|
61
|
+
- **Dynamic Workflows (`eval` tool)** — write real JS control flow around subagent dispatch: `task(role, taskText, context?)`, `parallel(thunks)`, `pipeline(items, ...stages)`, and `log(message)`, composing sequential/branching orchestration that `task`'s single-stage `tasks[]` batch can't express. Runs in an isolated Worker thread with a genuinely preemptive timeout (`worker.terminate()`, not a same-process race) — same full-process trust as `bash`, no sandbox pretense, gated by the same interview mutation lock.
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
## Install
|
|
@@ -285,11 +289,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
285
289
|
## Changelog
|
|
286
290
|
|
|
287
291
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
292
|
+
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
288
293
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
289
294
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
290
295
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
291
296
|
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
292
|
-
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
293
297
|
|
|
294
298
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
295
299
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
- **低价位评分模型路由** — `/goal` 验证器、`critic` 子代理角色,以及未锁定的 `task` 扇出批次,现在默认使用低价位、已配置凭证的模型,而不是悄悄搭乘与它们所评分/执行的工作相同的全价模型(`resolveVerifierModel`,针对浏览器 `verify` 动作按视觉能力过滤,确保纯文本的低价模型不会悄悄丢弃附带的截图)。
|
|
51
51
|
- **`jeo routine init`** — 生成一个 GitHub Actions 工作流,在 schedule/issue/PR 触发器上以无头模式运行 jeo(`jeo "<prompt>" -p`),运行在 GitHub 自己的 runner 上 — 不需要笔记本电脑,并且不会给 jeo 自身增加任何新的攻击面(没有进程内调度器或 webhook 监听器)。`--dry-run` 可预览,`--no-pr` 则改为直接提交而非默认的每次运行一个 PR。
|
|
52
52
|
- **远程子代理可见性(Telegram)** — 配对一次机器人(`jeo notify setup`)后,`jeo daemon start` 会在子代理每次状态变化(启动 → 完成/失败/取消)时推送消息,并接受 `/subagents`、`/steer <id> <subagentId> <msg>`、`/cancel <id> <subagentId>` 回传。现在提供 Telegram 论坛主题、内联键盘、图片附件等完整 `gjc` parity;命令仅对配对的聊天授权。
|
|
53
|
+
- **独立验证器,真正强制执行** — 计划再也无法跳过 architect/critic 步骤: `PlanSchema` 会拒绝任何以未验证变更结尾的计划(把验证器放在它该检查的变更之前也不算数),在 `ralplan` 起草阶段和 `team`/`approve` 执行阶段都强制生效。每一次 architect/critic 裁决也都必须展示真实证据 — 观测到的 `read`/`search`/`find`/`ast_grep`/`lsp` 调用为零,无论文本怎么声称,裁决都会被拦下。
|
|
54
|
+
- **安全边界自动模型回退** — 未分类的安全拒绝(可能是分类器误报,而非真实的内容策略命中)现在会切换到真正不同提供商的模型,而不是在同一个模型上无限退让 — 与现有的 rate-limit 快速回退是同一套模式。`Refusal (<category>)` 这种形态的确定性拒绝不受影响,仍然零回退硬失败。
|
|
55
|
+
- **内存: 应得的信任** — 概念的验证日期现在只在蒸馏阶段被明确标记为已验证时才会写入,而不是每次写入都记录;`isConceptStale` 把未验证(或超过 30 天过期)的概念视为需要重新验证,而不是信任一个被动的时间戳。
|
|
56
|
+
- **动态工作流(`eval` 工具)** — 围绕子代理派遣编写真正的 JS 控制流: `task(role, taskText, context?)`、`parallel(thunks)`、`pipeline(items, ...stages)`、`log(message)`,组合出 `task` 单阶段 `tasks[]` 批次无法表达的顺序/分支编排。运行在隔离的 Worker 线程中,具备真正的抢占式超时(`worker.terminate()`,而非同进程内的竞争) — 与 `bash` 同等的全进程信任级别,不假装沙箱,受同一个 interview 变更锁门控。
|
|
53
57
|
|
|
54
58
|
## 安装
|
|
55
59
|
|
|
@@ -277,11 +281,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
277
281
|
## 更新日志 (Changelog)
|
|
278
282
|
|
|
279
283
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
284
|
+
- **[0.8.24]** (2026-07-13) — Follow-up gap analysis against the same external "self-improving agent system" framework (Fable-5-style loops), continuing 0.8.23's audit into 4 more primitives: independent-verifier ENFORCEMENT (0.8.23 had the gate logic but nothing forced a plan to actually contain one), model-tier safety-boundary fallback (a false-positive safety refusal previously backed off forever on the SAME model instead of trying a genuinely different one — the pattern already shipped for rate limits), memory confidence that was self-assigned once instead of earned via a real verification event, and Dynamic Workflows (jeo had parallel fan-out but no sequential composition or real control flow across subagent calls). Closed all 4, the last one non-trivially: an in-process `AsyncFunction` (the same pattern `browser {run}` already ships) cannot be given a real wall-clock timeout — a synchronous bug in agent-authored code blocks jeo's own event loop forever, and `Promise.race` can never preempt it (verified empirically). Fixed by running Dynamic Workflows scripts in an isolated `Worker` (a genuinely separate OS thread `worker.terminate()` can preempt unconditionally, mirroring `bashTool`'s own SIGTERM/SIGKILL escalation on a spawned process) with `task()` bridged back to the main thread over a `postMessage` RPC — config/credentials never leave the main thread, and every dispatch (however the script shapes it) stays bounded by a real concurrency semaphore, not just a per-call count cap.
|
|
280
285
|
- **[0.8.23]** (2026-07-12) — Gap analysis against an external "self-improving agent system" framework (Fable-5-style loops: independent verifiers, memory compounding, model-tier cost discipline, and scheduled routines) found jeo-code already had strong equivalents for most primitives — but 3 real gaps and 1 deliberately-scoped-safe gap. Closed all 4: skills that were "hand-authored, read-mostly, zero learning" now compound from real session failures; screenshots that were captured but never judged now close a real vision-verify loop; the `/goal` verifier and bulk fan-out dispatch that silently rode the same full-price model as the work they graded/executed now default to a cheap tier; and "runs without your laptop" is now achievable via a generated GitHub Actions template wrapping jeo's existing headless mode — with zero new attack surface inside jeo-code itself (no code-exec sandbox, no in-process scheduler/webhook receiver, both deliberately rejected as out of scope). Also absorbs 3 fixes shipped after 0.8.22 but never changelogged: a rate-limit fast-fallback audit that closed 4 real gaps, plus the cross-file `mock.module()` test-isolation class this release's own test additions collided with and fixed at its root cause.
|
|
281
286
|
- **[0.8.22]** (2026-07-11) — A rescan of the working tree turned up a resume-fidelity gap plus two unwired TUI safety-net helpers left by a concurrent session; each was traced, wired where needed, and verified before shipping.
|
|
282
287
|
- **[0.8.21]** (2026-07-10) — "반영할꺼 같은방식으로 체크하고 배포까지" — a rescan of the working tree (after the prior audit pass) turned up 15 uncommitted files: 3 genuinely distinct features left unwired by a concurrent session, partially casualty of an earlier accidental `git checkout` incident this session (disclosed in the 0.8.20 entry above). Each was independently traced from its existing plumbing, wired to a real call site, tested, and one live-verified end-to-end over a real PTY before shipping.
|
|
283
288
|
- **[0.8.20]** (2026-07-10) — "모든 검증 다시 리뷰하고 변경사항 모두 체크해" — 4 fresh, skeptical subagents independently re-audited every change from v0.8.17-0.8.19 (bc8768f..92c6b7d) with zero trust in prior claims: a code-correctness auditor manually traced the Antigravity routing logic against the live catalog, a test-integrity auditor mutation-tested the new tests (confirmed each one genuinely fails when its fix is reverted) and ran the suite fresh, a live-behavior verifier re-reproduced all 4 behavioral claims from scratch with self-generated data (own session ids, own mock server, own HTTP status code), and a docs-accuracy auditor cross-checked every README/CHANGELOG claim against current code in isolated git worktrees. Verdict: all core logic and tests GENUINE/CONFIRMED CORRECT — but the audit surfaced 1 real doc staleness gap, 1 changelog count error, and 2 minor precision gaps, all fixed here.
|
|
284
|
-
- **[0.8.19]** (2026-07-09) — "프롬프트 라우팅에 안티그라비티 프로바이더의 경우, 안티그라비티용 소넷과 오퍼스도 3.5급으로 라우팅될수있도록해줘" — Antigravity re-exports 3 distinct model families (Anthropic Claude, Google Gemini, OpenAI GPT-OSS) behind one credential, structurally unlike every other provider. `high`/`complex` tier auto-select always resolved to Google's Gemini 3.5 rows: Anthropic's real 64,000-token output ceiling lost a same-thinking-tier tie to Google's 65,536 by a margin with no practical significance, and Gemini's 1M-token context further outranked Claude's real 200K window — so `antigravity/claude-sonnet-4-6`/`antigravity/claude-opus-4-6-thinking` were NEVER reachable through auto-select, even though both were already correctly `sizeClass`-tagged into the `high`/`complex` pools.
|
|
285
289
|
|
|
286
290
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
287
291
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/agent/engine.ts
CHANGED
|
@@ -276,6 +276,29 @@ export interface AgentLoopEvents {
|
|
|
276
276
|
onSteer?(text: string): void;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
/** The doneReason PREFIX the engine emits when `safetyFallbackAvailable` bails an
|
|
280
|
+
* uncategorized refusal (see `AgentLoopOptions.safetyFallbackAvailable`'s doc
|
|
281
|
+
* comment) — the single source of truth for this tag's shape, so every caller
|
|
282
|
+
* (task-tool.ts, launch.ts) recognizes and strips it identically instead of each
|
|
283
|
+
* re-deriving its own regex that could drift out of sync. */
|
|
284
|
+
export const SAFETY_FALLBACK_TAG = "SafetyFallback (uncategorized): ";
|
|
285
|
+
|
|
286
|
+
/** True for the engine's OWN safety-fallback bail signal — NOT a general refusal
|
|
287
|
+
* detector (the engine already did that categorization; this only recognizes the
|
|
288
|
+
* specific case it decided to hand back for a caller-side model switch, distinct
|
|
289
|
+
* from a `Refusal (<category>)` hard-fail which never produces this tag). */
|
|
290
|
+
export function isSafetyFallbackBail(doneReason: string | undefined): boolean {
|
|
291
|
+
return (doneReason ?? "").startsWith(SAFETY_FALLBACK_TAG);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Strips the internal tag prefix, leaving the already-user-friendly
|
|
295
|
+
* `friendlyProviderError` text the engine wrapped it around. Idempotent/safe on
|
|
296
|
+
* a non-tagged string (returned unchanged) — callers can apply this
|
|
297
|
+
* unconditionally to any terminal doneReason before it reaches the user. */
|
|
298
|
+
export function stripSafetyFallbackTag(doneReason: string): string {
|
|
299
|
+
return doneReason.startsWith(SAFETY_FALLBACK_TAG) ? doneReason.slice(SAFETY_FALLBACK_TAG.length) : doneReason;
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
export interface AgentLoopOptions {
|
|
280
303
|
/** Optional system prompt: prepended to `history` when it has no system message. */
|
|
281
304
|
systemPrompt?: string;
|
|
@@ -325,6 +348,21 @@ export interface AgentLoopOptions {
|
|
|
325
348
|
* cut the budget short; a single-provider session still rides out a transient
|
|
326
349
|
* per-minute window as before). */
|
|
327
350
|
rateLimitFallbackAvailable?: () => boolean;
|
|
351
|
+
/** Sync predicate, same shape/contract as `rateLimitFallbackAvailable` (see its
|
|
352
|
+
* doc comment for the design precedent this mirrors): true when the caller has
|
|
353
|
+
* an equivalent-pool fallback model ready RIGHT NOW. Consulted ONLY at the
|
|
354
|
+
* refusal ladder's final rung (rung 4 — every context mutation already spent)
|
|
355
|
+
* for an UNCATEGORIZED refusal (bare `stop_reason=refusal`/content_filter/SAFETY,
|
|
356
|
+
* the "rolling classifier, can clear with time OR be a false positive" shape —
|
|
357
|
+
* NEVER for a `Refusal (<category>)`-shaped error, which is a deterministic
|
|
358
|
+
* classification of the REQUEST CONTENT and fails fast regardless of this
|
|
359
|
+
* option, exactly as before: switching models must never be a bypass for a
|
|
360
|
+
* genuine content-policy hit). When set and true, the engine bails with a
|
|
361
|
+
* distinctly-tagged `SafetyFallback (uncategorized): <message>` doneReason
|
|
362
|
+
* instead of entering the unbounded backoff-resend loop, so the caller's own
|
|
363
|
+
* equivalent-pool fallback can switch models immediately — the 2026 "false
|
|
364
|
+
* positive -> try a different model; true positive -> hard-halt" pattern. */
|
|
365
|
+
safetyFallbackAvailable?: () => boolean;
|
|
328
366
|
}
|
|
329
367
|
|
|
330
368
|
export interface AgentLoopResult {
|
|
@@ -841,6 +879,24 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
841
879
|
if (category) {
|
|
842
880
|
return finish({ done: false, steps: step, doneReason: `Error: ${friendlyProviderError(err)}` });
|
|
843
881
|
}
|
|
882
|
+
// Safety-boundary automatic model fallback (2026 pattern: an UNCATEGORIZED
|
|
883
|
+
// refusal is treated as a possible false-positive classifier trip — try a
|
|
884
|
+
// DIFFERENT model before committing to the full unbounded backoff below).
|
|
885
|
+
// Deliberately AFTER the category check above: a genuinely categorized
|
|
886
|
+
// refusal NEVER reaches here and NEVER gets a model switch, matching
|
|
887
|
+
// launch.ts's own `routeFailureReason` guard ("switching models must not
|
|
888
|
+
// be a safety bypass") — this only widens WHICH uncategorized refusals can
|
|
889
|
+
// escape the same-model backoff loop, it does not touch the true-positive
|
|
890
|
+
// path at all. Checked only once (the first time this rung is reached);
|
|
891
|
+
// if no fallback is available OR the caller didn't wire the predicate, the
|
|
892
|
+
// existing backoff-forever behavior is completely unchanged.
|
|
893
|
+
if (opts.safetyFallbackAvailable?.()) {
|
|
894
|
+
return finish({
|
|
895
|
+
done: false,
|
|
896
|
+
steps: step,
|
|
897
|
+
doneReason: `${SAFETY_FALLBACK_TAG}${friendlyProviderError(err)}`,
|
|
898
|
+
});
|
|
899
|
+
}
|
|
844
900
|
// Backoff-resend forever (abortable).
|
|
845
901
|
const attempt = Math.max(1, refusalRetries - MAX_REFUSAL_RETRIES);
|
|
846
902
|
const baseRaw = Number(jeoEnv("REFUSAL_BACKOFF_BASE_MS") ?? NaN);
|