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
|
@@ -14,6 +14,7 @@ import type { Credential } from "../../auth";
|
|
|
14
14
|
import type { CallOptions, Message } from "../types";
|
|
15
15
|
import { readSse } from "../sse";
|
|
16
16
|
import { providerHttpError } from "./errors";
|
|
17
|
+
import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
17
18
|
|
|
18
19
|
export const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
19
20
|
|
|
@@ -63,6 +64,11 @@ export function codexResponsesRequest(
|
|
|
63
64
|
stream: true, // the Codex backend only streams
|
|
64
65
|
store: false,
|
|
65
66
|
};
|
|
67
|
+
if (options.tools?.length) {
|
|
68
|
+
// Responses API function tools (flat shape). tool_choice "auto" keeps prose + `done`.
|
|
69
|
+
payload.tools = options.tools.map(t => ({ type: "function", name: t.name, description: t.description, parameters: t.parameters, strict: false }));
|
|
70
|
+
payload.tool_choice = "auto";
|
|
71
|
+
}
|
|
66
72
|
// Map thinkingLevel → reasoning effort for Codex reasoning models (gjc parity).
|
|
67
73
|
// Drop out-of-enum values instead of forwarding them — the backend 400s on unknown efforts.
|
|
68
74
|
if (options.reasoningEffort && VALID_REASONING_EFFORTS.has(options.reasoningEffort)) {
|
|
@@ -87,6 +93,10 @@ export interface ResponsesEvent {
|
|
|
87
93
|
/** `response.incomplete` cause (e.g. max_output_tokens) — surfaced when the
|
|
88
94
|
* whole response produced no text (round-5 #1). */
|
|
89
95
|
incompleteReason?: string;
|
|
96
|
+
/** NATIVE function_call output items (accumulated by the caller across SSE events). */
|
|
97
|
+
toolCallName?: string;
|
|
98
|
+
toolCallArgsDelta?: string;
|
|
99
|
+
toolCallIndex?: number;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
/** Parse one Responses SSE `data:` payload into a delta / usage / error. */
|
|
@@ -94,6 +104,8 @@ export function parseResponsesEvent(data: string): ResponsesEvent {
|
|
|
94
104
|
let o: {
|
|
95
105
|
type?: string;
|
|
96
106
|
delta?: unknown;
|
|
107
|
+
item?: { type?: string; name?: string };
|
|
108
|
+
output_index?: number;
|
|
97
109
|
response?: {
|
|
98
110
|
usage?: { input_tokens?: number; output_tokens?: number };
|
|
99
111
|
error?: { message?: string };
|
|
@@ -106,6 +118,12 @@ export function parseResponsesEvent(data: string): ResponsesEvent {
|
|
|
106
118
|
} catch {
|
|
107
119
|
return {};
|
|
108
120
|
}
|
|
121
|
+
if (o.type === "response.output_item.added" && o.item?.type === "function_call") {
|
|
122
|
+
return { toolCallName: o.item.name, toolCallIndex: o.output_index };
|
|
123
|
+
}
|
|
124
|
+
if (o.type === "response.function_call_arguments.delta" && typeof o.delta === "string") {
|
|
125
|
+
return { toolCallArgsDelta: o.delta, toolCallIndex: o.output_index };
|
|
126
|
+
}
|
|
109
127
|
if (o.type === "response.output_text.delta" && typeof o.delta === "string") return { delta: o.delta };
|
|
110
128
|
// `response.incomplete` (max_output_tokens / content filter) also carries usage — don't drop it.
|
|
111
129
|
if ((o.type === "response.completed" || o.type === "response.incomplete") && o.response?.usage) {
|
|
@@ -120,6 +138,33 @@ export function parseResponsesEvent(data: string): ResponsesEvent {
|
|
|
120
138
|
return {};
|
|
121
139
|
}
|
|
122
140
|
|
|
141
|
+
/** Accumulate Responses function_call name + streamed argument fragments by output index. */
|
|
142
|
+
function accumulateResponsesToolCall(acc: Map<number, { name: string; args: string }>, ev: ResponsesEvent): void {
|
|
143
|
+
if (ev.toolCallName !== undefined) {
|
|
144
|
+
const i = ev.toolCallIndex ?? 0;
|
|
145
|
+
const b = acc.get(i) ?? { name: "", args: "" };
|
|
146
|
+
b.name = ev.toolCallName;
|
|
147
|
+
acc.set(i, b);
|
|
148
|
+
}
|
|
149
|
+
if (ev.toolCallArgsDelta) {
|
|
150
|
+
const i = ev.toolCallIndex ?? 0;
|
|
151
|
+
const b = acc.get(i) ?? { name: "", args: "" };
|
|
152
|
+
b.args += ev.toolCallArgsDelta;
|
|
153
|
+
acc.set(i, b);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Re-serialize accumulated Responses function calls into the engine's canonical JSON. */
|
|
158
|
+
function serializeResponsesToolCalls(acc: Map<number, { name: string; args: string }>): string | null {
|
|
159
|
+
if (acc.size === 0) return null;
|
|
160
|
+
const calls = [...acc.values()].map(b => {
|
|
161
|
+
let args: Record<string, unknown> = {};
|
|
162
|
+
try { args = b.args ? JSON.parse(b.args) : {}; } catch { args = {}; }
|
|
163
|
+
return { tool: b.name, arguments: args };
|
|
164
|
+
});
|
|
165
|
+
return serializeToolCalls(calls);
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
/** Round-5 #1: no-text completions surface their cause instead of returning "". */
|
|
124
169
|
function emptyCompletionError(reason: string | undefined): Error {
|
|
125
170
|
const hint = reason === "max_output_tokens"
|
|
@@ -136,13 +181,18 @@ export async function codexResponsesCall(messages: Message[], options: CallOptio
|
|
|
136
181
|
if (!response.body) return "";
|
|
137
182
|
let out = "";
|
|
138
183
|
let incompleteReason: string | undefined;
|
|
184
|
+
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
139
185
|
for await (const data of readSse(response.body)) {
|
|
140
186
|
const ev = parseResponsesEvent(data);
|
|
141
187
|
if (ev.delta) out += ev.delta;
|
|
188
|
+
accumulateResponsesToolCall(toolAcc, ev);
|
|
142
189
|
if (ev.usage) options.onUsage?.(ev.usage);
|
|
143
190
|
if (ev.incompleteReason) incompleteReason = ev.incompleteReason;
|
|
144
191
|
if (ev.error) throw new Error(`OpenAI Codex response failed: ${ev.error}`);
|
|
145
192
|
}
|
|
193
|
+
// Prefer a native tool call (re-serialized to canonical JSON) over any stray text.
|
|
194
|
+
const envelope = serializeResponsesToolCalls(toolAcc);
|
|
195
|
+
if (envelope) return envelope;
|
|
146
196
|
if (!out) throw emptyCompletionError(incompleteReason);
|
|
147
197
|
return out;
|
|
148
198
|
}
|
|
@@ -159,15 +209,20 @@ export async function* codexResponsesStream(
|
|
|
159
209
|
if (!response.body) return;
|
|
160
210
|
let yieldedAny = false;
|
|
161
211
|
let incompleteReason: string | undefined;
|
|
212
|
+
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
162
213
|
for await (const data of readSse(response.body)) {
|
|
163
214
|
const ev = parseResponsesEvent(data);
|
|
164
215
|
if (ev.delta) {
|
|
165
216
|
yieldedAny = true;
|
|
166
217
|
yield ev.delta;
|
|
167
218
|
}
|
|
219
|
+
accumulateResponsesToolCall(toolAcc, ev);
|
|
168
220
|
if (ev.usage) options.onUsage?.(ev.usage);
|
|
169
221
|
if (ev.incompleteReason) incompleteReason = ev.incompleteReason;
|
|
170
222
|
if (ev.error) throw new Error(`OpenAI Codex response failed: ${ev.error}`);
|
|
171
223
|
}
|
|
224
|
+
// Native tool calls have no output_text deltas — yield the re-serialized envelope once.
|
|
225
|
+
const envelope = serializeResponsesToolCalls(toolAcc);
|
|
226
|
+
if (envelope) { yieldedAny = true; yield envelope; }
|
|
172
227
|
if (!yieldedAny) throw emptyCompletionError(incompleteReason);
|
|
173
228
|
}
|
|
@@ -3,6 +3,7 @@ import type { CallOptions, Message, ProviderAdapter } from "../types";
|
|
|
3
3
|
import { readSse } from "../sse";
|
|
4
4
|
import { providerHttpError } from "./errors";
|
|
5
5
|
import { codexResponsesCall, codexResponsesStream } from "./openai-responses";
|
|
6
|
+
import { serializeToolCalls } from "../../agent/tool-schemas";
|
|
6
7
|
|
|
7
8
|
export function openaiRequest(messages: Message[], options: CallOptions, credential: Credential, stream: boolean): { url: string; headers: Record<string, string>; body: string } {
|
|
8
9
|
const model = options.model.startsWith("openai/") ? options.model.slice(7) : options.model;
|
|
@@ -39,7 +40,11 @@ export function openaiRequest(messages: Message[], options: CallOptions, credent
|
|
|
39
40
|
payload.stream = true;
|
|
40
41
|
payload.stream_options = { include_usage: true };
|
|
41
42
|
}
|
|
42
|
-
if (options.
|
|
43
|
+
if (options.tools?.length) {
|
|
44
|
+
payload.tools = options.tools.map(t => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.parameters } }));
|
|
45
|
+
payload.tool_choice = "auto";
|
|
46
|
+
}
|
|
47
|
+
if (options.jsonMode && !options.tools?.length) payload.response_format = { type: "json_object" };
|
|
43
48
|
const base = (options.baseUrl ?? process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
44
49
|
return {
|
|
45
50
|
url: `${base}/chat/completions`,
|
|
@@ -59,14 +64,18 @@ function emptyCompletionError(finishReason: string | undefined): Error {
|
|
|
59
64
|
|
|
60
65
|
export const openaiAdapter: ProviderAdapter = {
|
|
61
66
|
name: "openai",
|
|
67
|
+
supportsNativeTools: true,
|
|
62
68
|
async call(messages, options, credential) {
|
|
63
69
|
// ChatGPT/Codex OAuth can't use /chat/completions — route to the Codex Responses backend.
|
|
64
70
|
if (credential.kind === "oauth") return codexResponsesCall(messages, options, credential);
|
|
65
71
|
const { url, headers, body } = openaiRequest(messages, options, credential, false);
|
|
66
72
|
const response = await fetch(url, { method: "POST", headers, body, signal: options.signal });
|
|
67
73
|
if (!response.ok) throw await providerHttpError("OpenAI", response);
|
|
68
|
-
const result = (await response.json()) as { choices: { message: { content
|
|
74
|
+
const result = (await response.json()) as { choices: { message: { content?: string; tool_calls?: { function?: { name?: string; arguments?: string } }[] }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
69
75
|
if (result.usage) options.onUsage?.({ inputTokens: result.usage.prompt_tokens, outputTokens: result.usage.completion_tokens });
|
|
76
|
+
// Prefer a native tool call (re-serialized to canonical JSON) over any stray text.
|
|
77
|
+
const envelope = serializeToolCalls(parseOpenaiToolCalls(result.choices[0]?.message?.tool_calls));
|
|
78
|
+
if (envelope) return envelope;
|
|
70
79
|
const text = result.choices[0]?.message?.content ?? "";
|
|
71
80
|
if (!text) throw emptyCompletionError(result.choices[0]?.finish_reason);
|
|
72
81
|
return text;
|
|
@@ -93,8 +102,9 @@ export const openaiAdapter: ProviderAdapter = {
|
|
|
93
102
|
if (!response.body) return;
|
|
94
103
|
let yieldedAny = false;
|
|
95
104
|
let finishReason: string | undefined;
|
|
105
|
+
const toolAcc = new Map<number, { name: string; args: string }>();
|
|
96
106
|
for await (const data of readSse(response.body)) {
|
|
97
|
-
let chunk: { choices?: { delta?: { content?: string }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
107
|
+
let chunk: { choices?: { delta?: { content?: string; tool_calls?: { index?: number; function?: { name?: string; arguments?: string } }[] }; finish_reason?: string }[]; usage?: { prompt_tokens?: number; completion_tokens?: number } };
|
|
98
108
|
try {
|
|
99
109
|
chunk = JSON.parse(data);
|
|
100
110
|
} catch {
|
|
@@ -105,13 +115,46 @@ export const openaiAdapter: ProviderAdapter = {
|
|
|
105
115
|
yieldedAny = true;
|
|
106
116
|
yield delta;
|
|
107
117
|
}
|
|
118
|
+
const tcs = chunk.choices?.[0]?.delta?.tool_calls;
|
|
119
|
+
if (tcs) {
|
|
120
|
+
for (const tc of tcs) {
|
|
121
|
+
const idx = tc.index ?? 0;
|
|
122
|
+
const b = toolAcc.get(idx) ?? { name: "", args: "" };
|
|
123
|
+
if (tc.function?.name) b.name = tc.function.name;
|
|
124
|
+
if (tc.function?.arguments) b.args += tc.function.arguments;
|
|
125
|
+
toolAcc.set(idx, b);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
108
128
|
if (chunk.choices?.[0]?.finish_reason) finishReason = chunk.choices[0].finish_reason;
|
|
109
129
|
if (chunk.usage) options.onUsage?.({ inputTokens: chunk.usage.prompt_tokens, outputTokens: chunk.usage.completion_tokens });
|
|
110
130
|
}
|
|
131
|
+
// Native tool calls stream as tool_calls argument fragments — re-serialize once at end.
|
|
132
|
+
if (toolAcc.size > 0) {
|
|
133
|
+
const calls = [...toolAcc.values()].map(b => {
|
|
134
|
+
let args: Record<string, unknown> = {};
|
|
135
|
+
try { args = b.args ? JSON.parse(b.args) : {}; } catch { args = {}; }
|
|
136
|
+
return { tool: b.name, arguments: args };
|
|
137
|
+
});
|
|
138
|
+
const envelope = serializeToolCalls(calls);
|
|
139
|
+
if (envelope) { yieldedAny = true; yield envelope; }
|
|
140
|
+
}
|
|
111
141
|
if (!yieldedAny) throw emptyCompletionError(finishReason);
|
|
112
142
|
},
|
|
113
143
|
};
|
|
114
144
|
|
|
145
|
+
function parseOpenaiToolCalls(toolCalls: { function?: { name?: string; arguments?: string } }[] | undefined): { tool: string; arguments: Record<string, unknown> }[] {
|
|
146
|
+
if (!toolCalls?.length) return [];
|
|
147
|
+
const out: { tool: string; arguments: Record<string, unknown> }[] = [];
|
|
148
|
+
for (const tc of toolCalls) {
|
|
149
|
+
const name = tc.function?.name;
|
|
150
|
+
if (!name) continue;
|
|
151
|
+
let args: Record<string, unknown> = {};
|
|
152
|
+
try { args = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {}; } catch { args = {}; }
|
|
153
|
+
out.push({ tool: name, arguments: args });
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
115
158
|
function bearerFor(credential: Credential): string {
|
|
116
159
|
if (credential.kind === "oauth") return credential.token;
|
|
117
160
|
if (credential.kind === "api_key") return credential.token;
|
package/src/ai/types.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface Usage {
|
|
|
26
26
|
durationMs?: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Provider-neutral function/tool schema for NATIVE tool-calling. Capable adapters
|
|
30
|
+
* (anthropic/openai/gemini) map this onto their wire format (Anthropic input_schema,
|
|
31
|
+
* OpenAI function.parameters, Gemini functionDeclarations); fallback adapters
|
|
32
|
+
* (antigravity/ollama) ignore it and keep the JSON-in-prose protocol. */
|
|
33
|
+
export interface NativeToolSchema {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
parameters: { type: "object"; properties: Record<string, unknown>; required?: string[] };
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export interface CallOptions {
|
|
30
40
|
model: string;
|
|
31
41
|
systemPrompt?: string;
|
|
@@ -47,10 +57,19 @@ export interface CallOptions {
|
|
|
47
57
|
* answer text). Surfaced as a transient dimmed view; absent for models that emit no
|
|
48
58
|
* thought text. */
|
|
49
59
|
onReasoning?: (delta: string) => void;
|
|
60
|
+
/** NATIVE tool-calling: function declarations the model may call. Present only on the
|
|
61
|
+
* main agent step (never the prose wrap-up). Adapters with `supportsNativeTools` send
|
|
62
|
+
* these on the wire and re-serialize the structured tool call back into the engine's
|
|
63
|
+
* canonical {"tool":...}/{"tools":[...]} string; others ignore it. */
|
|
64
|
+
tools?: NativeToolSchema[];
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
export interface ProviderAdapter {
|
|
53
68
|
readonly name: ProviderName;
|
|
69
|
+
/** True when this adapter implements native function-calling (re-serialized to the
|
|
70
|
+
* canonical JSON string). When false/absent, `CallOptions.tools` is ignored and the
|
|
71
|
+
* model drives tools via the JSON-in-prose protocol. */
|
|
72
|
+
readonly supportsNativeTools?: boolean;
|
|
54
73
|
/** Local providers ignore the credential argument; cloud adapters require it. */
|
|
55
74
|
call(messages: Message[], options: CallOptions, credential: Credential): Promise<string>;
|
|
56
75
|
/** Optional token streaming. Yields text deltas; concatenation equals the `call()` result. */
|
package/src/cli/runner.ts
CHANGED
|
@@ -172,6 +172,15 @@ export const COMMANDS: readonly CommandSpec[] = [
|
|
|
172
172
|
return args => m.runUpdateCommand(args);
|
|
173
173
|
},
|
|
174
174
|
},
|
|
175
|
+
{
|
|
176
|
+
name: "whats-new",
|
|
177
|
+
summary: "Show the release notes bundled with the installed jeo-code version.",
|
|
178
|
+
usage: "whats-new [--all] [--json]",
|
|
179
|
+
loader: async () => {
|
|
180
|
+
const m = await import("../commands/whats-new");
|
|
181
|
+
return args => m.runWhatsNewCommand(args);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
175
184
|
{
|
|
176
185
|
name: "ooo-seed",
|
|
177
186
|
summary: "Generate an immutable ooo seed from a specification (spec-first automation).",
|