jeo-code 0.5.12 → 0.5.14
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 +19 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +3 -2
- package/src/agent/engine.ts +16 -3
- package/src/agent/loop.ts +2 -0
- package/src/agent/tool-schemas.ts +132 -0
- package/src/agent/tools.ts +9 -3
- package/src/ai/model-manager.ts +1 -0
- package/src/ai/providers/anthropic.ts +60 -3
- package/src/ai/providers/antigravity.ts +31 -1
- package/src/ai/providers/openai-responses.ts +55 -0
- package/src/ai/providers/openai.ts +46 -3
- package/src/ai/types.ts +19 -0
- package/src/cli/runner.ts +9 -0
- package/src/commands/launch.ts +207 -256
- package/src/commands/update.ts +12 -0
- package/src/commands/whats-new.ts +3 -2
- package/src/skills/catalog.ts +34 -70
- package/src/tui/app.ts +43 -61
- package/src/tui/components/autocomplete.ts +2 -8
- package/src/tui/components/slash.ts +1 -2
- package/src/util/whats-new.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.5.14] - 2026-06-16
|
|
10
|
+
_`jeo --tmux` live-verification harness — repeatable stability + behavior checks._
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `scripts/tmux-verify.sh` (and `bun run verify:tmux`) codifies the launch → send-keys → capture → cleanup loop into one repeatable command, so stability and behavior of the interactive TUI can be checked without hand-rolled one-off bash. macOS-safe (no GNU `timeout`; a bash watchdog polls for the session). Boots jeo in a DETACHED tmux session inside a throwaway cwd (never edits the real repo) and only ever kills the session it created — a user's `jeo-main-*` session is never touched. Subcommands: `smoke` (boot + assert the input box and model bar render, no crash — the stability gate), `check "<input>" "<regex>" [--ansi] [--wait N]` (type input, assert the pane matches a pattern — the behavior primitive; captures scrollback so long output like `/help` still matches), and `capture` (dump the settled frame).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `jeo whats-new` (and the post-upgrade update notice) now default to the **5 most recent** releases instead of only the single latest entry, so the notes no longer look static/hardcoded across upgrades. `--all` still prints the full history. New shared constant `RECENT_RELEASE_COUNT` (`src/util/whats-new.ts`) is the single source of truth for both the command and the launch notice (the launch notice is capped to it too, so a large version jump no longer dumps a wall). Mirrors gjc's "Recent Changes" pattern (latest-N + a full toggle) and the README's latest-5 digest.
|
|
17
|
+
|
|
18
|
+
### Maintainer notes
|
|
19
|
+
- Internal refactors landed since 0.5.13 (no behavior change): centralized workflow name/engine dispatch (`WORKFLOW_NAMES`/`runWorkflowEngine`), a shared `statusBoxData()` for the inline/non-inline status frames, and a `normalizeSlashAlias()` helper. Also fixed a flaky test where the light-tool ledger line briefly carried an elapsed `(Nms)` suffix — that detail belongs on the forge cards, the ledger line is a clean single line again.
|
|
20
|
+
|
|
21
|
+
## [0.5.13] - 2026-06-15
|
|
22
|
+
_Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name._
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Workflow skills listed in the `/` menu didn't run: a bare `/name` only resolved when the skill's SKILL.md happened to self-reference that slash token (so `/ralplan` worked by luck while `/deep-interview` and `/ultragoal` returned "Unknown command"). `parseSkillInvocation` now resolves a plain `/word` against skill NAMES (exact, then unique prefix) — the same entrypoint as `$name` and `/skill:name` (gjc parity) — so `/deep-interview`, `/ralplan`, `/team`, `/ultragoal` (and any loaded skill) dispatch from the slash menu. Dotted (`/speckit.plan`) and nested (`/a/b`) tokens keep their alias/file-path resolution untouched, and built-in commands still take precedence.
|
|
26
|
+
- The four bundled workflows are now always listed in the `/` menu as `/deep-interview`, `/ralplan`, `/team`, `/ultragoal`, even when their SKILL.md declares no slash alias, so they are discoverable as well as runnable.
|
|
27
|
+
|
|
9
28
|
## [0.5.12] - 2026-06-15
|
|
10
29
|
_Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card._
|
|
11
30
|
|
package/README.ja.md
CHANGED
|
@@ -150,11 +150,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
150
150
|
## 変更履歴 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
154
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
153
155
|
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
154
156
|
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
155
157
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
156
|
-
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
157
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -150,11 +150,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
150
150
|
## 변경 이력 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
154
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
153
155
|
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
154
156
|
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
155
157
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
156
|
-
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
157
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -150,11 +150,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
|
|
|
150
150
|
## Changelog
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
154
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
153
155
|
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
154
156
|
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
155
157
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
156
|
-
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
157
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -150,11 +150,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
150
150
|
## 更新日志 (Changelog)
|
|
151
151
|
|
|
152
152
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
153
|
+
- **[0.5.14]** (2026-06-16) — `jeo --tmux` live-verification harness — repeatable stability + behavior checks.
|
|
154
|
+
- **[0.5.13]** (2026-06-15) — Workflow `/` commands actually run — `/deep-interview`, `/team`, `/ultragoal`, `/ralplan` dispatch by name.
|
|
153
155
|
- **[0.5.12]** (2026-06-15) — Yellow status animation while a process runs, and elapsed `(Nms)` on every completed tool card.
|
|
154
156
|
- **[0.5.11]** (2026-06-15) — Backspace on an empty prompt line no longer quits jeo.
|
|
155
157
|
- **[0.5.10]** (2026-06-15) — `/resume` transcript no longer dumps raw JSON for batched tool calls.
|
|
156
|
-
- **[0.5.9]** (2026-06-15) — Bounded per-frame wrap for the live thinking/tool-output blocks — re-render cost no longer grows with stream length.
|
|
157
|
-
- **[0.5.8]** (2026-06-15) — Native Opik observability for the turn loop (opt-in `JEO_OPIK`, pure-TS no-op when unset) + autopilot convergence tracking.
|
|
158
158
|
|
|
159
159
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
160
160
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jeo-code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"description": "Clean, highly optimized AI coding agent using spec-first loop",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/cli.ts",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"pack:check": "npm pack --dry-run",
|
|
50
50
|
"publish:npm": "npm publish --access public --registry https://registry.npmjs.org/",
|
|
51
51
|
"changelog:sync": "bun scripts/sync-changelog.ts",
|
|
52
|
-
"test": "bun test"
|
|
52
|
+
"test": "bun test",
|
|
53
|
+
"verify:tmux": "bash scripts/tmux-verify.sh"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
56
|
"zod": "^3.24.1",
|
package/src/agent/engine.ts
CHANGED
|
@@ -11,6 +11,7 @@ import * as fs from "node:fs/promises";
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import type { Message } from "./loop";
|
|
13
13
|
import { extractJsonObject } from "./json";
|
|
14
|
+
import { nativeToolSchemasFor } from "./tool-schemas";
|
|
14
15
|
import { readTool, writeTool, editTool, bashTool, findTool, searchTool, lsTool, mkdirTool, deleteTool, type ToolResult } from "./tools";
|
|
15
16
|
import { webSearchTool, setWebSearchActiveModel } from "./web-search";
|
|
16
17
|
import { friendlyProviderError, isContextOverflowError, isRefusalError } from "../util/provider-error";
|
|
@@ -32,6 +33,7 @@ async function invokeCallLlm(history: Message[], options: {
|
|
|
32
33
|
onRetry?: (attempt: number, err: unknown, delayMs: number) => void;
|
|
33
34
|
onToken?: (delta: string) => void;
|
|
34
35
|
onReasoning?: (delta: string) => void;
|
|
36
|
+
tools?: import("../ai/types").NativeToolSchema[];
|
|
35
37
|
}): Promise<string> {
|
|
36
38
|
const mod = await import("./loop");
|
|
37
39
|
return mod.callLlm(history, options);
|
|
@@ -81,6 +83,7 @@ export const TOOL_PROTOCOL = [
|
|
|
81
83
|
"Batch only independent calls; NEVER batch 'done', and NEVER put a mutating tool (write/edit/bash) after another mutating tool in one batch whose inputs depend on the earlier one.",
|
|
82
84
|
"Tool calibration: scale calls to difficulty — one for a known fact, a few for a normal task, more only when evidence is genuinely missing. Locate before you open: search/find first, then read the hit, instead of guessing paths.",
|
|
83
85
|
"web_search reflex: if the request hinges on a name, version, library, or event you do not actually recognize, search before answering instead of guessing; never claim a result's absence proves nonexistence.",
|
|
86
|
+
"Quoting fetched/searched text: paraphrase by default — quote at most one short phrase per source, cite it, and never paste long passages.",
|
|
84
87
|
].join("\n");
|
|
85
88
|
|
|
86
89
|
/** Restricted protocol for read-only subagent roles (planner/architect/critic):
|
|
@@ -111,15 +114,19 @@ export const WORKING_DISCIPLINE = [
|
|
|
111
114
|
"- Correctness first, maintainability second, brevity third. Prefer boring, explicit code.",
|
|
112
115
|
"- Never present partial work as complete; never suppress tests or warnings to make code pass.",
|
|
113
116
|
"- Never fabricate tool results or test outcomes; verification claims must match what was actually run.",
|
|
114
|
-
"- Don't assume disk/state
|
|
117
|
+
"- Don't assume disk/state or that a referenced file exists — read to verify first.",
|
|
115
118
|
"- Don't fabricate API/library surfaces from memory; check the source or --help for unfamiliar APIs.",
|
|
116
119
|
"- Never ship stubs, placeholders, or TODO-only code as a delivered feature.",
|
|
117
120
|
"- Never substitute the requested problem with an easier adjacent one.",
|
|
121
|
+
"- On a failed tool or test, fix the cause and continue — capture the evidence first; no apology loops, no shrinking the task to dodge it.",
|
|
118
122
|
"- Update directly affected callsites, tests, and docs — or state why they are unchanged.",
|
|
119
123
|
"- Reuse existing patterns; parallel conventions are prohibited. Fix problems at their source.",
|
|
120
|
-
"-
|
|
121
|
-
"- Trust tool output
|
|
124
|
+
"- Not alone in the repo: treat unexpected changes as user work; never revert or delete them.",
|
|
125
|
+
"- Trust tool output, but re-read/re-run on failure, on a possible file change, or when output looks stale or self-contradictory.",
|
|
122
126
|
"- Prefer dedicated tools over shell pipelines: read (not cat), search (not grep), edit (not sed).",
|
|
127
|
+
"- For large files (>500 lines), read targeted sections first; use lineRange to avoid context bloat.",
|
|
128
|
+
"- Own mistakes plainly and fix them — no over-apology or self-abasement; report what went wrong and what you changed.",
|
|
129
|
+
"- Decline to build malware, exploits, or vulnerability-weaponization even under an educational or research framing.",
|
|
123
130
|
].join("\n");
|
|
124
131
|
|
|
125
132
|
/** Reply discipline (FABLE-5 tone + gjc communication/soul): shapes the agent's
|
|
@@ -429,6 +436,12 @@ export async function runAgentLoop(history: Message[], opts: AgentLoopOptions):
|
|
|
429
436
|
try {
|
|
430
437
|
responseText = await invokeCallLlm(history, {
|
|
431
438
|
jsonMode: true,
|
|
439
|
+
// NATIVE tool-calling: declare the ACTIVE toolset (read-only subagents
|
|
440
|
+
// expose only their non-mutating tools). Capable adapters (anthropic …)
|
|
441
|
+
// use these and re-serialize the structured call to canonical JSON; the
|
|
442
|
+
// antigravity/ollama fallback ignores them. Only on the main step — never
|
|
443
|
+
// the prose wrap-up call below.
|
|
444
|
+
tools: nativeToolSchemasFor(Object.keys(tools)),
|
|
432
445
|
model: opts.model,
|
|
433
446
|
maxTokens: opts.maxTokens,
|
|
434
447
|
signal: opts.signal,
|
package/src/agent/loop.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface ChatOptions {
|
|
|
21
21
|
onToken?: (delta: string) => void;
|
|
22
22
|
/** Streaming sink for native reasoning/thinking deltas (drives the dimmed live view). */
|
|
23
23
|
onReasoning?: (delta: string) => void;
|
|
24
|
+
/** NATIVE tool-calling function declarations (forwarded to capable adapters). */
|
|
25
|
+
tools?: import("../ai/types").NativeToolSchema[];
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const manager = createModelManager();
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { NativeToolSchema } from "../ai/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native function-calling schemas for jeo's tools, keyed by canonical tool name.
|
|
5
|
+
*
|
|
6
|
+
* The `properties` keys MUST match the argument names the DEFAULT_TOOLS handlers read
|
|
7
|
+
* (engine.ts) EXACTLY — a renamed parameter would land in a key the handler ignores and
|
|
8
|
+
* silently no-op the call. The model fills an API-validated schema, so this registry is
|
|
9
|
+
* the single source of truth for argument names on the native path.
|
|
10
|
+
*/
|
|
11
|
+
const STRING = { type: "string" } as const;
|
|
12
|
+
|
|
13
|
+
const SCHEMAS: Record<string, NativeToolSchema> = {
|
|
14
|
+
read: {
|
|
15
|
+
name: "read",
|
|
16
|
+
description: "Read a file. Optional lineRange ('a-b','a-','a','a+n','a-b,c-d'); raw=true skips line-number prefixes.",
|
|
17
|
+
parameters: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: { filePath: STRING, lineRange: STRING, raw: { type: "boolean" } },
|
|
20
|
+
required: ["filePath"],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
write: {
|
|
24
|
+
name: "write",
|
|
25
|
+
description: "Create or overwrite a file with the given content.",
|
|
26
|
+
parameters: { type: "object", properties: { filePath: STRING, content: STRING }, required: ["filePath", "content"] },
|
|
27
|
+
},
|
|
28
|
+
edit: {
|
|
29
|
+
name: "edit",
|
|
30
|
+
description: "Apply a line-anchored edit block to a file (≔A..B replace, ≔A+ insert after, ≔$ append).",
|
|
31
|
+
parameters: { type: "object", properties: { filePath: STRING, editBlock: STRING }, required: ["filePath", "editBlock"] },
|
|
32
|
+
},
|
|
33
|
+
bash: {
|
|
34
|
+
name: "bash",
|
|
35
|
+
description: "Run a shell command. Optional timeoutMs, cwd (subdir), env (extra vars).",
|
|
36
|
+
parameters: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: { command: STRING, timeoutMs: { type: "number" }, cwd: STRING, env: { type: "object" } },
|
|
39
|
+
required: ["command"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
find: {
|
|
43
|
+
name: "find",
|
|
44
|
+
description: "Find files by glob pattern.",
|
|
45
|
+
parameters: { type: "object", properties: { globPattern: STRING }, required: ["globPattern"] },
|
|
46
|
+
},
|
|
47
|
+
search: {
|
|
48
|
+
name: "search",
|
|
49
|
+
description: "Search file contents by regex (grep). Optional globPattern, ignoreCase, context, maxMatches.",
|
|
50
|
+
parameters: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
pattern: STRING,
|
|
54
|
+
globPattern: STRING,
|
|
55
|
+
ignoreCase: { type: "boolean" },
|
|
56
|
+
context: { type: "number" },
|
|
57
|
+
maxMatches: { type: "number" },
|
|
58
|
+
},
|
|
59
|
+
required: ["pattern"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
ls: {
|
|
63
|
+
name: "ls",
|
|
64
|
+
description: "List a directory's entries (directories first).",
|
|
65
|
+
parameters: { type: "object", properties: { dirPath: STRING }, required: ["dirPath"] },
|
|
66
|
+
},
|
|
67
|
+
mkdir: {
|
|
68
|
+
name: "mkdir",
|
|
69
|
+
description: "Create a directory (parents included; idempotent).",
|
|
70
|
+
parameters: { type: "object", properties: { dirPath: STRING }, required: ["dirPath"] },
|
|
71
|
+
},
|
|
72
|
+
delete: {
|
|
73
|
+
name: "delete",
|
|
74
|
+
description: "Remove a file, or a directory when recursive=true.",
|
|
75
|
+
parameters: { type: "object", properties: { path: STRING, recursive: { type: "boolean" } }, required: ["path"] },
|
|
76
|
+
},
|
|
77
|
+
web_search: {
|
|
78
|
+
name: "web_search",
|
|
79
|
+
description: "Search the web (synthesized answer + sources + citations). Optional recency, limit.",
|
|
80
|
+
parameters: { type: "object", properties: { query: STRING, recency: STRING, limit: { type: "number" } }, required: ["query"] },
|
|
81
|
+
},
|
|
82
|
+
done: {
|
|
83
|
+
name: "done",
|
|
84
|
+
description: "Call when the task is fully implemented AND verified. The reason is shown to the user as your message.",
|
|
85
|
+
parameters: { type: "object", properties: { reason: STRING }, required: [] },
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the native tool-schema list for the ACTIVE toolset. Pass the real tool names the
|
|
91
|
+
* turn is allowed to use (Object.keys of the engine's toolset); `done` is always appended
|
|
92
|
+
* so the model can signal completion natively. Read-only subagents therefore expose only
|
|
93
|
+
* their non-mutating tools — never write/edit/bash — on the native channel.
|
|
94
|
+
*/
|
|
95
|
+
export function nativeToolSchemasFor(toolNames: Iterable<string>): NativeToolSchema[] {
|
|
96
|
+
const out: NativeToolSchema[] = [];
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
for (const name of toolNames) {
|
|
99
|
+
const schema = SCHEMAS[name];
|
|
100
|
+
if (schema && !seen.has(name)) {
|
|
101
|
+
out.push(schema);
|
|
102
|
+
seen.add(name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!seen.has("done")) out.push(SCHEMAS.done!);
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Re-serialize parsed native tool calls into the engine's canonical JSON string. Coalesces
|
|
111
|
+
* a batched `done` to a single envelope (the engine rejects done-in-batch). Returns null
|
|
112
|
+
* when there are no calls. Shared by capable provider adapters (antigravity/openai/…).
|
|
113
|
+
*/
|
|
114
|
+
export function serializeToolCalls(calls: { tool: string; arguments: Record<string, unknown> }[]): string | null {
|
|
115
|
+
// Gemini (antigravity) intermittently namespaces native functions under `default_api`
|
|
116
|
+
// (e.g. functionCall.name = "default_api.done" / "default_api:done") when handed raw
|
|
117
|
+
// functionDeclarations, which the engine then rejects as an unknown tool. Strip that
|
|
118
|
+
// namespace back to the bare tool name so the call dispatches normally.
|
|
119
|
+
const valid = calls
|
|
120
|
+
.map(c => ({ ...c, tool: normalizeNativeToolName(c.tool) }))
|
|
121
|
+
.filter(c => c.tool);
|
|
122
|
+
if (valid.length === 0) return null;
|
|
123
|
+
const done = valid.find(c => c.tool === "done");
|
|
124
|
+
if (done) return JSON.stringify(done);
|
|
125
|
+
if (valid.length === 1) return JSON.stringify(valid[0]);
|
|
126
|
+
return JSON.stringify({ tools: valid });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Strip the Gemini `default_api.` / `default_api:` namespace prefix from a tool name. */
|
|
130
|
+
export function normalizeNativeToolName(name: string): string {
|
|
131
|
+
return (name ?? "").replace(/^default_api\s*[.:]\s*/, "").trim();
|
|
132
|
+
}
|
package/src/agent/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { applyBashFixups } from "./bash-fixups";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { readWorkflowStateStrict, type WorkflowState } from "./state";
|
|
5
5
|
import { jeoEnv } from "../util/env";
|
|
6
6
|
import { READ_OUTPUT_MAX } from "./tool-output";
|
|
7
7
|
|
|
@@ -787,9 +787,15 @@ export async function searchTool(
|
|
|
787
787
|
try {
|
|
788
788
|
const flags = ignoreCase ? "-rnIi" : "-rnI";
|
|
789
789
|
const gi = await readGitignore(cwd);
|
|
790
|
+
// A gitignore glob like `.*` (or a bare `*`/`**`) is meant to skip dotfiles, but as a
|
|
791
|
+
// grep --exclude/--exclude-dir it matches the `./`-prefixed traversal paths and silently
|
|
792
|
+
// excludes EVERY file on BSD grep (the field bug: search returned "No matches found" for
|
|
793
|
+
// text that existed). Drop these all-matching globs — IGNORED_DIRS still covers the key
|
|
794
|
+
// dotdirs (.git/.jeo/.next/.cache), and find() is unaffected (it matches via -name).
|
|
795
|
+
const safeGlob = (g: string) => !/^\.?\*+$/.test(g);
|
|
790
796
|
const excludes = [
|
|
791
|
-
...[...IGNORED_DIRS, ...gi.dirs].map(d => `--exclude-dir=${d}`),
|
|
792
|
-
...gi.fileGlobs.map(f => `--exclude=${f}`),
|
|
797
|
+
...[...IGNORED_DIRS, ...gi.dirs.filter(safeGlob)].map(d => `--exclude-dir=${d}`),
|
|
798
|
+
...gi.fileGlobs.filter(safeGlob).map(f => `--exclude=${f}`),
|
|
793
799
|
];
|
|
794
800
|
const n = (v: unknown): number | undefined =>
|
|
795
801
|
typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined;
|
package/src/ai/model-manager.ts
CHANGED
|
@@ -306,6 +306,7 @@ async function resolveCall(options: Partial<CallOptions>, kind: "request" | "str
|
|
|
306
306
|
signal: options.signal,
|
|
307
307
|
reasoningEffort: options.reasoningEffort ?? thinkingToReasoningEffort(config.thinkingLevel),
|
|
308
308
|
onReasoning: options.onReasoning,
|
|
309
|
+
tools: options.tools,
|
|
309
310
|
};
|
|
310
311
|
// Caller-supplied retry sink rides on the config-derived retry budget so the
|
|
311
312
|
// engine/TUI can surface "rate limited — retrying in Ns" instead of a silent wait.
|
|
@@ -115,6 +115,13 @@ export function anthropicPayload(
|
|
|
115
115
|
};
|
|
116
116
|
if (credential.kind === "oauth") payload.metadata = { user_id: createClaudeCloakingUserId() };
|
|
117
117
|
if (includeTemperature && options.temperature !== undefined) payload.temperature = options.temperature;
|
|
118
|
+
if (options.tools?.length) {
|
|
119
|
+
// NATIVE tool-calling: declare jeo's tools as Anthropic functions. tool_choice
|
|
120
|
+
// "auto" keeps prose-salvage reachable and lets the model call `done` (declared as
|
|
121
|
+
// a tool) — never "required", which would kill the plain-text final-answer path.
|
|
122
|
+
payload.tools = options.tools.map(t => ({ name: t.name, description: t.description, input_schema: t.parameters }));
|
|
123
|
+
payload.tool_choice = { type: "auto" };
|
|
124
|
+
}
|
|
118
125
|
if (stream) payload.stream = true;
|
|
119
126
|
const system = anthropicSystemBlocks(systemPrompt, model, credential, payload);
|
|
120
127
|
if (system) payload.system = system;
|
|
@@ -190,12 +197,36 @@ function emptyCompletionError(stopReason: string | undefined): Error {
|
|
|
190
197
|
: "";
|
|
191
198
|
return new Error(`Anthropic returned no content${stopReason ? ` (stop_reason=${stopReason})` : ""}${hint}.`);
|
|
192
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Re-serialize Anthropic native `tool_use` content block(s) into the engine's canonical
|
|
203
|
+
* JSON string — the linchpin of the adapter-internal-serialization design: the engine,
|
|
204
|
+
* anti-spin guards, and done-gate keep consuming the SAME {"tool":...}/{"tools":[...]}
|
|
205
|
+
* shape they parse from the JSON-in-prose path. A batched `done` is coalesced to a single
|
|
206
|
+
* done envelope (the engine rejects done-in-batch). Returns null when there is no tool_use.
|
|
207
|
+
*/
|
|
208
|
+
function serializeAnthropicToolUse(
|
|
209
|
+
content: { type: string; name?: string; input?: unknown }[],
|
|
210
|
+
): string | null {
|
|
211
|
+
const calls = content
|
|
212
|
+
.filter(c => c.type === "tool_use" && typeof c.name === "string")
|
|
213
|
+
.map(c => ({ tool: c.name as string, arguments: (c.input ?? {}) as Record<string, unknown> }));
|
|
214
|
+
if (calls.length === 0) return null;
|
|
215
|
+
const done = calls.find(c => c.tool === "done");
|
|
216
|
+
if (done) return JSON.stringify(done);
|
|
217
|
+
if (calls.length === 1) return JSON.stringify(calls[0]);
|
|
218
|
+
return JSON.stringify({ tools: calls });
|
|
219
|
+
}
|
|
193
220
|
export const anthropicAdapter: ProviderAdapter = {
|
|
194
221
|
name: "anthropic",
|
|
222
|
+
supportsNativeTools: true,
|
|
195
223
|
async call(messages, options, credential) {
|
|
196
224
|
const response = await postAnthropic(messages, options, credential, false);
|
|
197
|
-
const result = (await response.json()) as { content: { type: string; text
|
|
225
|
+
const result = (await response.json()) as { content: { type: string; text?: string; name?: string; input?: unknown }[]; stop_reason?: string; usage?: AnthropicUsage };
|
|
198
226
|
if (result.usage) options.onUsage?.({ inputTokens: totalInputTokens(result.usage), outputTokens: result.usage.output_tokens });
|
|
227
|
+
// Prefer a native tool call (re-serialized to canonical JSON) over any stray text.
|
|
228
|
+
const toolCall = serializeAnthropicToolUse(result.content);
|
|
229
|
+
if (toolCall) return toolCall;
|
|
199
230
|
const text = result.content.find(c => c.type === "text")?.text ?? "";
|
|
200
231
|
if (!text) throw emptyCompletionError(result.stop_reason);
|
|
201
232
|
return text;
|
|
@@ -206,10 +237,16 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
206
237
|
let cachedInput: number | undefined;
|
|
207
238
|
let yieldedAny = false;
|
|
208
239
|
let stopReason: string | undefined;
|
|
240
|
+
// Native tool_use streams as content_block_start (name) + input_json_delta fragments,
|
|
241
|
+
// never as text_delta — accumulate per block index, then re-serialize to canonical
|
|
242
|
+
// JSON and yield it once at the end (concatenation still equals call()).
|
|
243
|
+
const toolBlocks = new Map<number, { name: string; json: string }>();
|
|
209
244
|
for await (const data of readSse(response.body)) {
|
|
210
245
|
let evt: {
|
|
211
246
|
type?: string;
|
|
212
|
-
|
|
247
|
+
index?: number;
|
|
248
|
+
content_block?: { type?: string; name?: string };
|
|
249
|
+
delta?: { type?: string; text?: string; partial_json?: string; stop_reason?: string };
|
|
213
250
|
message?: { usage?: AnthropicUsage };
|
|
214
251
|
usage?: { output_tokens?: number };
|
|
215
252
|
};
|
|
@@ -218,7 +255,12 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
218
255
|
} catch {
|
|
219
256
|
continue;
|
|
220
257
|
}
|
|
221
|
-
if (evt.type === "
|
|
258
|
+
if (evt.type === "content_block_start" && evt.content_block?.type === "tool_use" && typeof evt.index === "number") {
|
|
259
|
+
toolBlocks.set(evt.index, { name: evt.content_block.name ?? "", json: "" });
|
|
260
|
+
} else if (evt.type === "content_block_delta" && evt.delta?.type === "input_json_delta" && typeof evt.index === "number") {
|
|
261
|
+
const b = toolBlocks.get(evt.index);
|
|
262
|
+
if (b) b.json += evt.delta.partial_json ?? "";
|
|
263
|
+
} else if (evt.type === "content_block_delta" && evt.delta?.type === "text_delta" && evt.delta.text) {
|
|
222
264
|
yieldedAny = true;
|
|
223
265
|
yield evt.delta.text;
|
|
224
266
|
} else if (evt.type === "message_start" && evt.message?.usage) {
|
|
@@ -231,6 +273,21 @@ export const anthropicAdapter: ProviderAdapter = {
|
|
|
231
273
|
if (evt.usage) options.onUsage?.({ inputTokens: cachedInput, outputTokens: evt.usage.output_tokens });
|
|
232
274
|
}
|
|
233
275
|
}
|
|
276
|
+
if (toolBlocks.size > 0) {
|
|
277
|
+
const calls = [...toolBlocks.values()]
|
|
278
|
+
.map(b => {
|
|
279
|
+
let args: Record<string, unknown> = {};
|
|
280
|
+
try { args = b.json ? JSON.parse(b.json) : {}; } catch { args = {}; }
|
|
281
|
+
return { tool: b.name, arguments: args };
|
|
282
|
+
})
|
|
283
|
+
.filter(c => c.tool);
|
|
284
|
+
if (calls.length > 0) {
|
|
285
|
+
const done = calls.find(c => c.tool === "done");
|
|
286
|
+
const envelope = done ? JSON.stringify(done) : calls.length === 1 ? JSON.stringify(calls[0]) : JSON.stringify({ tools: calls });
|
|
287
|
+
yieldedAny = true;
|
|
288
|
+
yield envelope;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
234
291
|
if (!yieldedAny) throw emptyCompletionError(stopReason);
|
|
235
292
|
},
|
|
236
293
|
};
|
|
@@ -3,6 +3,7 @@ import type { Credential } from "../../auth";
|
|
|
3
3
|
import type { CallOptions, Message, ProviderAdapter } from "../types";
|
|
4
4
|
import { readSse } from "../sse";
|
|
5
5
|
import { providerHttpError } from "./errors";
|
|
6
|
+
import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
6
7
|
|
|
7
8
|
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
8
9
|
const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
@@ -136,6 +137,12 @@ export function antigravityRequest(messages: Message[], options: CallOptions, cr
|
|
|
136
137
|
};
|
|
137
138
|
if (systemPrompt) request.systemInstruction = { role: "user", parts: [{ text: systemPrompt }] };
|
|
138
139
|
if (Object.keys(generationConfig).length > 0) request.generationConfig = generationConfig;
|
|
140
|
+
if (options.tools?.length) {
|
|
141
|
+
// NATIVE tool-calling: Gemini functionDeclarations through the CCA proxy. AUTO mode
|
|
142
|
+
// keeps prose answers + the `done` tool both reachable.
|
|
143
|
+
request.tools = [{ functionDeclarations: options.tools.map(t => ({ name: t.name, description: t.description, parameters: t.parameters })) }];
|
|
144
|
+
request.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
|
|
145
|
+
}
|
|
139
146
|
|
|
140
147
|
const body = JSON.stringify({
|
|
141
148
|
project,
|
|
@@ -160,7 +167,7 @@ export function antigravityRequest(messages: Message[], options: CallOptions, cr
|
|
|
160
167
|
type CcaUsage = { promptTokenCount?: number; candidatesTokenCount?: number; thoughtsTokenCount?: number };
|
|
161
168
|
interface CcaChunk {
|
|
162
169
|
response?: {
|
|
163
|
-
candidates?: { content?: { parts?: { text?: string; thought?: boolean }[] }; finishReason?: string }[];
|
|
170
|
+
candidates?: { content?: { parts?: { text?: string; thought?: boolean; functionCall?: { name?: string; args?: Record<string, unknown> } }[] }; finishReason?: string }[];
|
|
164
171
|
usageMetadata?: CcaUsage;
|
|
165
172
|
};
|
|
166
173
|
}
|
|
@@ -174,6 +181,18 @@ function thoughtOf(chunk: CcaChunk): string {
|
|
|
174
181
|
return chunk.response?.candidates?.[0]?.content?.parts?.filter(p => p.thought).map(p => p.text ?? "").join("") ?? "";
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
/** Native Gemini functionCall parts (Cloud Code Assist) → {tool, arguments}. */
|
|
185
|
+
function functionCallsOf(chunk: CcaChunk): { tool: string; arguments: Record<string, unknown> }[] {
|
|
186
|
+
const parts = chunk.response?.candidates?.[0]?.content?.parts ?? [];
|
|
187
|
+
const out: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
188
|
+
for (const p of parts) {
|
|
189
|
+
if (p.functionCall && typeof p.functionCall.name === "string") {
|
|
190
|
+
out.push({ tool: p.functionCall.name, arguments: (p.functionCall.args ?? {}) as Record<string, unknown> });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
|
|
177
196
|
async function fetchAntigravity(messages: Message[], options: CallOptions, credential: Credential): Promise<Response> {
|
|
178
197
|
// Resolve the project id up front: stored credential → env → lazy
|
|
179
198
|
// loadCodeAssist/onboardUser discovery (persisted for future sessions).
|
|
@@ -191,20 +210,26 @@ async function fetchAntigravity(messages: Message[], options: CallOptions, crede
|
|
|
191
210
|
|
|
192
211
|
export const antigravityAdapter: ProviderAdapter = {
|
|
193
212
|
name: "antigravity",
|
|
213
|
+
supportsNativeTools: true,
|
|
194
214
|
async call(messages, options, credential) {
|
|
195
215
|
const response = await fetchAntigravity(messages, options, credential);
|
|
196
216
|
if (!response.body) return "";
|
|
197
217
|
let out = "";
|
|
198
218
|
let usage: CcaUsage | undefined;
|
|
219
|
+
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
199
220
|
for await (const data of readSse(response.body)) {
|
|
200
221
|
let chunk: CcaChunk;
|
|
201
222
|
try { chunk = JSON.parse(data); } catch { continue; }
|
|
202
223
|
const thought = thoughtOf(chunk);
|
|
203
224
|
if (thought) options.onReasoning?.(thought);
|
|
204
225
|
out += textOf(chunk);
|
|
226
|
+
fnCalls.push(...functionCallsOf(chunk));
|
|
205
227
|
if (chunk.response?.usageMetadata) usage = chunk.response.usageMetadata;
|
|
206
228
|
}
|
|
207
229
|
if (usage) options.onUsage?.({ inputTokens: usage.promptTokenCount, outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0) });
|
|
230
|
+
// Prefer a native tool call (re-serialized to canonical JSON) over any stray text.
|
|
231
|
+
const envelope = serializeToolCalls(fnCalls);
|
|
232
|
+
if (envelope) return envelope;
|
|
208
233
|
if (!out) throw new Error("Antigravity Cloud Code Assist returned an empty response.");
|
|
209
234
|
return out;
|
|
210
235
|
},
|
|
@@ -213,6 +238,7 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
213
238
|
if (!response.body) return;
|
|
214
239
|
let yielded = false;
|
|
215
240
|
let usage: CcaUsage | undefined;
|
|
241
|
+
const fnCalls: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
216
242
|
for await (const data of readSse(response.body)) {
|
|
217
243
|
let chunk: CcaChunk;
|
|
218
244
|
try { chunk = JSON.parse(data); } catch { continue; }
|
|
@@ -220,9 +246,13 @@ export const antigravityAdapter: ProviderAdapter = {
|
|
|
220
246
|
if (thought) options.onReasoning?.(thought);
|
|
221
247
|
const delta = textOf(chunk);
|
|
222
248
|
if (delta) { yielded = true; yield delta; }
|
|
249
|
+
fnCalls.push(...functionCallsOf(chunk));
|
|
223
250
|
if (chunk.response?.usageMetadata) usage = chunk.response.usageMetadata;
|
|
224
251
|
}
|
|
225
252
|
if (usage) options.onUsage?.({ inputTokens: usage.promptTokenCount, outputTokens: (usage.candidatesTokenCount ?? 0) + (usage.thoughtsTokenCount ?? 0) });
|
|
253
|
+
// Native tool calls have no text deltas — yield the re-serialized envelope once at end.
|
|
254
|
+
const envelope = serializeToolCalls(fnCalls);
|
|
255
|
+
if (envelope) { yielded = true; yield envelope; }
|
|
226
256
|
if (!yielded) throw new Error("Antigravity Cloud Code Assist returned an empty response.");
|
|
227
257
|
},
|
|
228
258
|
};
|