@giovannijecha/jecode 0.7.3 → 0.8.0
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/README.md +104 -29
- package/dist/atomic.js +14 -0
- package/dist/batch.js +5 -2
- package/dist/cli-info.js +1 -1
- package/dist/config.js +4 -1
- package/dist/context/policy.js +14 -7
- package/dist/controller-request.js +1 -1
- package/dist/controller.js +39 -15
- package/dist/duration.js +8 -0
- package/dist/oauth-http.js +2 -1
- package/dist/openai-account.js +12 -8
- package/dist/openai-oauth-callback.js +2 -1
- package/dist/providers/anthropic-stream.js +8 -0
- package/dist/providers/anthropic-wire.js +22 -10
- package/dist/providers/http.js +4 -3
- package/dist/providers/ollama-stream.js +7 -0
- package/dist/providers/ollama-wire.js +11 -3
- package/dist/providers/openai-codex.js +0 -2
- package/dist/providers/openai-stream.js +57 -2
- package/dist/providers/openai-wire.js +27 -12
- package/dist/providers/sse.js +115 -38
- package/dist/sessions/codec.js +13 -5
- package/dist/sessions/store.js +2 -1
- package/dist/settings-command.js +0 -5
- package/dist/settings.js +0 -2
- package/dist/steering.js +58 -0
- package/dist/text-boundary.js +47 -0
- package/dist/timeline.js +2 -1
- package/dist/tools/args.js +1 -1
- package/dist/tools/fs.js +5 -0
- package/dist/tools/text-boundary.js +1 -31
- package/dist/transcript.js +6 -1
- package/dist/tui/activity.js +20 -5
- package/dist/tui/app-input.js +31 -6
- package/dist/tui/app-workflows.js +57 -14
- package/dist/tui/app.js +18 -3
- package/dist/tui/blocks.js +7 -2
- package/dist/tui/components/messages.js +20 -17
- package/dist/tui/components/misc.js +7 -6
- package/dist/tui/components/status.js +12 -1
- package/dist/tui/components/tool.js +152 -50
- package/dist/tui/help.js +1 -1
- package/dist/tui/motion.js +32 -0
- package/dist/tui/session-view.js +3 -2
- package/dist/tui/transcript-grammar.js +25 -0
- package/dist/tui/transcript-view.js +132 -7
- package/dist/tui/turn.js +78 -39
- package/dist/tui/view.js +4 -2
- package/dist/ui/diff.js +51 -16
- package/dist/ui/render.js +17 -27
- package/dist/ui/theme.js +19 -18
- package/dist/ui/width.js +31 -22
- package/dist/usage.js +5 -1
- package/package.json +1 -1
|
@@ -44,27 +44,39 @@ export function stopNotice(data) {
|
|
|
44
44
|
export function fromWireResponse(data) {
|
|
45
45
|
const raw = Array.isArray(data.content) ? data.content : [];
|
|
46
46
|
const content = [];
|
|
47
|
+
let suppressedToolCall = false;
|
|
47
48
|
for (const item of raw) {
|
|
48
49
|
const block = item;
|
|
49
50
|
if (block.type === "text" && typeof block.text === "string") {
|
|
50
51
|
content.push({ kind: "text", text: block.text });
|
|
51
52
|
}
|
|
52
|
-
else if (block.type === "tool_use"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
else if (block.type === "tool_use") {
|
|
54
|
+
if (data.stop_reason === "tool_use" &&
|
|
55
|
+
typeof block.id === "string" &&
|
|
56
|
+
typeof block.name === "string") {
|
|
57
|
+
content.push({
|
|
58
|
+
kind: "tool_call",
|
|
59
|
+
id: block.id,
|
|
60
|
+
name: block.name,
|
|
61
|
+
input: (block.input ?? {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
suppressedToolCall = true;
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
68
|
// thinking / redacted_thinking and anything future: carried in `raw` only.
|
|
63
69
|
}
|
|
64
70
|
const notice = stopNotice(data);
|
|
65
71
|
if (notice !== undefined)
|
|
66
72
|
content.push({ kind: "text", text: notice });
|
|
67
|
-
|
|
73
|
+
const retainRaw = data.stop_reason !== "max_tokens" && !suppressedToolCall;
|
|
74
|
+
return {
|
|
75
|
+
role: "assistant",
|
|
76
|
+
content,
|
|
77
|
+
...(retainRaw ? { raw, rawFrom: "anthropic" } : {}),
|
|
78
|
+
usage: normalizeUsage(data),
|
|
79
|
+
};
|
|
68
80
|
}
|
|
69
81
|
function normalizeUsage(data) {
|
|
70
82
|
const usage = data.usage;
|
package/dist/providers/http.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The entire HTTP layer: one bounded request, then either a JSON body or an
|
|
2
2
|
// event stream. Only idempotent reads retry. Once a POST starts or response
|
|
3
3
|
// bytes flow, a failure is surfaced rather than silently replayed.
|
|
4
|
+
import { leadingText } from "../text-boundary.js";
|
|
4
5
|
import { readSseJson } from "./sse.js";
|
|
5
6
|
import { sseStreamCharacterLimit } from "./stream-limits.js";
|
|
6
7
|
const RETRYABLE = new Set([408, 409, 429, 500, 502, 503, 504]);
|
|
@@ -31,7 +32,7 @@ async function asJson(url, res) {
|
|
|
31
32
|
return JSON.parse(text);
|
|
32
33
|
}
|
|
33
34
|
catch {
|
|
34
|
-
throw httpError(`${url} returned non-JSON`, res.status, text
|
|
35
|
+
throw httpError(`${url} returned non-JSON`, res.status, leadingText(text, 500));
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
export async function postSse(url, headers, body, maxOutputTokens, signal, onStatus) {
|
|
@@ -107,13 +108,13 @@ async function boundedText(url, res, max) {
|
|
|
107
108
|
if (done) {
|
|
108
109
|
text += decoder.decode();
|
|
109
110
|
return text.length > max
|
|
110
|
-
? { text: text
|
|
111
|
+
? { text: leadingText(text, max), truncated: true }
|
|
111
112
|
: { text, truncated: false };
|
|
112
113
|
}
|
|
113
114
|
text += decoder.decode(value, { stream: true });
|
|
114
115
|
if (text.length > max) {
|
|
115
116
|
await reader.cancel().catch(() => undefined);
|
|
116
|
-
return { text: text
|
|
117
|
+
return { text: leadingText(text, max), truncated: true };
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { addBounded, MAX_TOOL_ARGUMENT_CHARS } from "./stream-limits.js";
|
|
7
7
|
export async function assembleOllama(events, onStream) {
|
|
8
8
|
const calls = new Map();
|
|
9
|
+
const announcedTools = new Map();
|
|
9
10
|
let toolArgumentChars = 0;
|
|
10
11
|
let content = "";
|
|
11
12
|
let reasoning = "";
|
|
@@ -53,6 +54,12 @@ export async function assembleOllama(events, onStream) {
|
|
|
53
54
|
call.args += part.function.arguments;
|
|
54
55
|
}
|
|
55
56
|
calls.set(index, call);
|
|
57
|
+
const name = call.name === "" ? undefined : call.name;
|
|
58
|
+
if (!announcedTools.has(index) ||
|
|
59
|
+
(announcedTools.get(index) === undefined && name !== undefined)) {
|
|
60
|
+
announcedTools.set(index, name);
|
|
61
|
+
onStream?.({ kind: "tool", ...(name === undefined ? {} : { name }) });
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
if (finishReason === undefined) {
|
|
@@ -60,10 +60,18 @@ export function fromWireReply(reply) {
|
|
|
60
60
|
const content = [];
|
|
61
61
|
if (reply.content !== "")
|
|
62
62
|
content.push({ kind: "text", text: reply.content });
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const acceptsToolCalls = reply.finishReason === "tool_calls";
|
|
64
|
+
if (acceptsToolCalls) {
|
|
65
|
+
for (const call of reply.toolCalls) {
|
|
66
|
+
content.push({ kind: "tool_call", id: call.id, name: call.name, input: parseArgs(call.args) });
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
|
-
const
|
|
69
|
+
const notice = stopNotice(reply);
|
|
70
|
+
if (notice !== undefined)
|
|
71
|
+
content.push({ kind: "text", text: notice });
|
|
72
|
+
const suppressedToolCall = reply.toolCalls.length > 0 && !acceptsToolCalls;
|
|
73
|
+
const retainRaw = reply.finishReason !== "length" && !suppressedToolCall;
|
|
74
|
+
const raw = !retainRaw || reply.reasoning === "" ? undefined : { reasoning: reply.reasoning };
|
|
67
75
|
return {
|
|
68
76
|
role: "assistant",
|
|
69
77
|
content,
|
|
@@ -152,8 +152,6 @@ function modelContextWindow(entry) {
|
|
|
152
152
|
return undefined;
|
|
153
153
|
const percent = percentage(entry["effective_context_window_percent"]) ?? 95;
|
|
154
154
|
const tokens = Math.floor(resolved * percent / 100);
|
|
155
|
-
if (!validTokenCount(tokens))
|
|
156
|
-
return undefined;
|
|
157
155
|
const automatic = Math.floor(resolved * 9 / 10);
|
|
158
156
|
const advertised = tokenCount(entry["auto_compact_token_limit"]);
|
|
159
157
|
return Object.freeze({
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// those streamed items remain the fallback when the final envelope is empty.
|
|
7
7
|
export async function assembleOpenAI(events, onStream) {
|
|
8
8
|
const items = [];
|
|
9
|
+
const announcedTools = { identities: new Set(), anonymous: false };
|
|
9
10
|
let refusal = false;
|
|
10
11
|
for await (const raw of events) {
|
|
11
12
|
const event = raw;
|
|
@@ -24,14 +25,31 @@ export async function assembleOpenAI(events, onStream) {
|
|
|
24
25
|
if (typeof event.delta === "string")
|
|
25
26
|
onStream?.({ kind: "thinking", text: event.delta });
|
|
26
27
|
break;
|
|
28
|
+
case "response.output_item.added":
|
|
29
|
+
if (isFunctionCall(event.item)) {
|
|
30
|
+
announceTool(event, event.item, announcedTools, onStream);
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
case "response.function_call_arguments.delta":
|
|
34
|
+
case "response.function_call_arguments.done":
|
|
35
|
+
announceTool(event, undefined, announcedTools, onStream);
|
|
36
|
+
break;
|
|
27
37
|
case "response.output_item.done":
|
|
28
|
-
if (event.item !== undefined)
|
|
38
|
+
if (event.item !== undefined) {
|
|
39
|
+
if (isFunctionCall(event.item)) {
|
|
40
|
+
announceTool(event, event.item, announcedTools, onStream);
|
|
41
|
+
}
|
|
29
42
|
items.push(event.item);
|
|
43
|
+
}
|
|
30
44
|
break;
|
|
31
45
|
case "response.done":
|
|
32
46
|
case "response.completed":
|
|
47
|
+
return withDefaultStatus(reconcileOutput(event.response, items), "completed");
|
|
33
48
|
case "response.incomplete":
|
|
34
|
-
return
|
|
49
|
+
return {
|
|
50
|
+
...reconcileOutput(event.response, items),
|
|
51
|
+
status: "incomplete",
|
|
52
|
+
};
|
|
35
53
|
case "response.failed": {
|
|
36
54
|
const response = event.response;
|
|
37
55
|
throw new Error(`openai stream error: ${response?.error?.message ?? "unspecified"}`);
|
|
@@ -44,9 +62,46 @@ export async function assembleOpenAI(events, onStream) {
|
|
|
44
62
|
}
|
|
45
63
|
throw new Error("openai stream ended before a terminal response event");
|
|
46
64
|
}
|
|
65
|
+
function isFunctionCall(item) {
|
|
66
|
+
return typeof item === "object" && item !== null &&
|
|
67
|
+
item["type"] === "function_call";
|
|
68
|
+
}
|
|
69
|
+
function announceTool(event, item, announced, onStream) {
|
|
70
|
+
const identities = toolIdentities(event, item);
|
|
71
|
+
if (identities.length === 0) {
|
|
72
|
+
if (announced.anonymous)
|
|
73
|
+
return;
|
|
74
|
+
announced.anonymous = true;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const duplicate = identities.some((identity) => announced.identities.has(identity));
|
|
78
|
+
for (const identity of identities)
|
|
79
|
+
announced.identities.add(identity);
|
|
80
|
+
if (duplicate)
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const rawName = item?.name ?? event.name;
|
|
84
|
+
const name = typeof rawName === "string" && rawName !== "" ? rawName : undefined;
|
|
85
|
+
onStream?.({ kind: "tool", ...(name === undefined ? {} : { name }) });
|
|
86
|
+
}
|
|
87
|
+
function toolIdentities(event, item) {
|
|
88
|
+
const identities = [];
|
|
89
|
+
const itemId = event.item_id ?? item?.id;
|
|
90
|
+
if (typeof itemId === "string" && itemId !== "")
|
|
91
|
+
identities.push(`item:${itemId}`);
|
|
92
|
+
if (typeof event.output_index === "number")
|
|
93
|
+
identities.push(`output:${event.output_index}`);
|
|
94
|
+
if (typeof item?.call_id === "string" && item.call_id !== "") {
|
|
95
|
+
identities.push(`call:${item.call_id}`);
|
|
96
|
+
}
|
|
97
|
+
return identities;
|
|
98
|
+
}
|
|
47
99
|
function reconcileOutput(completed, items) {
|
|
48
100
|
if (completed === undefined)
|
|
49
101
|
return { output: items };
|
|
50
102
|
const finalCount = Array.isArray(completed.output) ? completed.output.length : 0;
|
|
51
103
|
return items.length > finalCount ? { ...completed, output: items } : completed;
|
|
52
104
|
}
|
|
105
|
+
function withDefaultStatus(response, status) {
|
|
106
|
+
return response.status === undefined ? { ...response, status } : response;
|
|
107
|
+
}
|
|
@@ -40,8 +40,9 @@ export function toWireItems(message, providerId = "openai") {
|
|
|
40
40
|
}
|
|
41
41
|
export function stopNotice(data) {
|
|
42
42
|
const reason = data.incomplete_details?.reason;
|
|
43
|
-
if (reason === undefined)
|
|
44
|
-
return undefined;
|
|
43
|
+
if (reason === undefined) {
|
|
44
|
+
return data.status === "incomplete" ? "[incomplete response]" : undefined;
|
|
45
|
+
}
|
|
45
46
|
return reason === "max_output_tokens"
|
|
46
47
|
? "[truncated: hit max_output_tokens — raise --max-tokens]"
|
|
47
48
|
: `[incomplete: ${reason}]`;
|
|
@@ -49,6 +50,7 @@ export function stopNotice(data) {
|
|
|
49
50
|
export function fromWireResponse(data, providerId = "openai") {
|
|
50
51
|
const raw = Array.isArray(data.output) ? data.output : [];
|
|
51
52
|
const content = [];
|
|
53
|
+
let suppressedFunctionCall = false;
|
|
52
54
|
for (const entry of raw) {
|
|
53
55
|
const item = entry;
|
|
54
56
|
if (item.type === "message" && Array.isArray(item.content)) {
|
|
@@ -62,22 +64,35 @@ export function fromWireResponse(data, providerId = "openai") {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
}
|
|
65
|
-
else if (item.type === "function_call"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
else if (item.type === "function_call") {
|
|
68
|
+
if (data.status === "completed" &&
|
|
69
|
+
(item.status === undefined || item.status === "completed") &&
|
|
70
|
+
typeof item.call_id === "string" &&
|
|
71
|
+
typeof item.name === "string") {
|
|
72
|
+
content.push({
|
|
73
|
+
kind: "tool_call",
|
|
74
|
+
id: item.call_id,
|
|
75
|
+
name: item.name,
|
|
76
|
+
input: parseArguments(item.arguments),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
suppressedFunctionCall = true;
|
|
81
|
+
}
|
|
74
82
|
}
|
|
75
83
|
// reasoning items and anything future: carried in `raw` only.
|
|
76
84
|
}
|
|
77
85
|
const notice = stopNotice(data);
|
|
78
86
|
if (notice !== undefined)
|
|
79
87
|
content.push({ kind: "text", text: notice });
|
|
80
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
role: "assistant",
|
|
90
|
+
content,
|
|
91
|
+
...(data.status === "completed" && !suppressedFunctionCall
|
|
92
|
+
? { raw, rawFrom: providerId }
|
|
93
|
+
: {}),
|
|
94
|
+
usage: normalizeUsage(data),
|
|
95
|
+
};
|
|
81
96
|
}
|
|
82
97
|
function normalizeUsage(data) {
|
|
83
98
|
const usage = data.usage;
|
package/dist/providers/sse.js
CHANGED
|
@@ -7,37 +7,26 @@ import { addBounded, MAX_SSE_EVENT_CHARS, } from "./stream-limits.js";
|
|
|
7
7
|
export async function* readSseJson(body, maximumChars) {
|
|
8
8
|
const reader = body.getReader();
|
|
9
9
|
const decoder = new TextDecoder();
|
|
10
|
-
|
|
10
|
+
const parser = new SseEventParser();
|
|
11
11
|
let finished = false;
|
|
12
12
|
let total = 0;
|
|
13
|
-
const append = (text) => {
|
|
14
|
-
total = addBounded(total, text.length, maximumChars, "SSE stream");
|
|
15
|
-
buffer += text;
|
|
16
|
-
};
|
|
17
13
|
try {
|
|
18
14
|
for (;;) {
|
|
19
15
|
const { done, value } = await reader.read();
|
|
20
16
|
if (done)
|
|
21
17
|
break;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
break;
|
|
27
|
-
assertEventSize(boundary.start);
|
|
28
|
-
const chunk = buffer.slice(0, boundary.start);
|
|
29
|
-
buffer = buffer.slice(boundary.end);
|
|
30
|
-
const payload = parseData(chunk);
|
|
31
|
-
if (payload !== undefined)
|
|
32
|
-
yield payload;
|
|
33
|
-
}
|
|
34
|
-
assertEventSize(buffer.length);
|
|
18
|
+
const text = decoder.decode(value, { stream: true });
|
|
19
|
+
total = addBounded(total, text.length, maximumChars, "SSE stream");
|
|
20
|
+
for (const payload of parser.push(text))
|
|
21
|
+
yield payload;
|
|
35
22
|
}
|
|
36
23
|
// A stream that ends without a trailing blank line still owes us its last
|
|
37
24
|
// event.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const payload
|
|
25
|
+
const text = decoder.decode();
|
|
26
|
+
total = addBounded(total, text.length, maximumChars, "SSE stream");
|
|
27
|
+
for (const payload of parser.push(text))
|
|
28
|
+
yield payload;
|
|
29
|
+
const payload = parser.finish();
|
|
41
30
|
if (payload !== undefined)
|
|
42
31
|
yield payload;
|
|
43
32
|
finished = true;
|
|
@@ -48,28 +37,116 @@ export async function* readSseJson(body, maximumChars) {
|
|
|
48
37
|
reader.releaseLock();
|
|
49
38
|
}
|
|
50
39
|
}
|
|
40
|
+
// Keep fragments in bounded groups. A provider may split one SSE line into
|
|
41
|
+
// hundreds of thousands of tiny chunks; repeatedly flattening the growing line
|
|
42
|
+
// would make parsing quadratic even if boundary scanning itself were linear.
|
|
43
|
+
class TextParts {
|
|
44
|
+
#groups = [];
|
|
45
|
+
#pieces = [];
|
|
46
|
+
#lastCodeUnit = "";
|
|
47
|
+
length = 0;
|
|
48
|
+
append(text) {
|
|
49
|
+
if (text === "")
|
|
50
|
+
return;
|
|
51
|
+
this.#pieces.push(text);
|
|
52
|
+
this.#lastCodeUnit = text.at(-1);
|
|
53
|
+
this.length += text.length;
|
|
54
|
+
if (this.#pieces.length >= 256)
|
|
55
|
+
this.#flush();
|
|
56
|
+
}
|
|
57
|
+
take() {
|
|
58
|
+
this.#flush();
|
|
59
|
+
const text = this.#groups.length === 1 ? this.#groups[0] : this.#groups.join("");
|
|
60
|
+
this.#groups.length = 0;
|
|
61
|
+
this.#lastCodeUnit = "";
|
|
62
|
+
this.length = 0;
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
endsWithCarriageReturn() {
|
|
66
|
+
return this.#lastCodeUnit === "\r";
|
|
67
|
+
}
|
|
68
|
+
#flush() {
|
|
69
|
+
if (this.#pieces.length === 0)
|
|
70
|
+
return;
|
|
71
|
+
this.#groups.push(this.#pieces.length === 1 ? this.#pieces[0] : this.#pieces.join(""));
|
|
72
|
+
this.#pieces = [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export class SseEventParser {
|
|
76
|
+
#line = new TextParts();
|
|
77
|
+
#data = [];
|
|
78
|
+
#eventLength = 0;
|
|
79
|
+
#hasLine = false;
|
|
80
|
+
#pendingTerminatorLength = 0;
|
|
81
|
+
*push(text) {
|
|
82
|
+
let start = 0;
|
|
83
|
+
for (;;) {
|
|
84
|
+
const newline = text.indexOf("\n", start);
|
|
85
|
+
if (newline === -1) {
|
|
86
|
+
this.#line.append(text.slice(start));
|
|
87
|
+
this.#assertPendingLineSize();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.#line.append(text.slice(start, newline));
|
|
91
|
+
const rawLine = this.#line.take();
|
|
92
|
+
const crlf = rawLine.endsWith("\r");
|
|
93
|
+
const line = crlf ? rawLine.slice(0, -1) : rawLine;
|
|
94
|
+
if (line === "") {
|
|
95
|
+
const payload = this.#finishEvent();
|
|
96
|
+
if (payload !== undefined)
|
|
97
|
+
yield payload;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this.#appendLine(line, crlf ? 2 : 1);
|
|
101
|
+
}
|
|
102
|
+
start = newline + 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
finish() {
|
|
106
|
+
const line = this.#line.take();
|
|
107
|
+
if (line !== "") {
|
|
108
|
+
this.#appendLine(line, 0);
|
|
109
|
+
}
|
|
110
|
+
else if (this.#hasLine) {
|
|
111
|
+
// A single trailing line terminator is part of an unterminated event.
|
|
112
|
+
assertEventSize(this.#eventLength + this.#pendingTerminatorLength);
|
|
113
|
+
}
|
|
114
|
+
return this.#finishEvent();
|
|
115
|
+
}
|
|
116
|
+
#appendLine(line, terminatorLength) {
|
|
117
|
+
const separatorLength = this.#hasLine ? this.#pendingTerminatorLength : 0;
|
|
118
|
+
assertEventSize(this.#eventLength + separatorLength + line.length);
|
|
119
|
+
this.#eventLength += separatorLength + line.length;
|
|
120
|
+
this.#hasLine = true;
|
|
121
|
+
this.#pendingTerminatorLength = terminatorLength;
|
|
122
|
+
if (line.startsWith("data:")) {
|
|
123
|
+
this.#data.push(line.slice("data:".length).trimStart());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
#assertPendingLineSize() {
|
|
127
|
+
const separatorLength = this.#hasLine ? this.#pendingTerminatorLength : 0;
|
|
128
|
+
const remaining = MAX_SSE_EVENT_CHARS - this.#eventLength - separatorLength;
|
|
129
|
+
// One trailing CR may turn out to be part of a split CRLF terminator.
|
|
130
|
+
const splitCrlf = this.#line.length === remaining + 1 && this.#line.endsWithCarriageReturn();
|
|
131
|
+
if (this.#line.length > remaining && !splitCrlf) {
|
|
132
|
+
assertEventSize(MAX_SSE_EVENT_CHARS + 1);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
#finishEvent() {
|
|
136
|
+
const data = this.#data.join("\n");
|
|
137
|
+
this.#data = [];
|
|
138
|
+
this.#eventLength = 0;
|
|
139
|
+
this.#hasLine = false;
|
|
140
|
+
this.#pendingTerminatorLength = 0;
|
|
141
|
+
return parseData(data);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
51
144
|
function assertEventSize(length) {
|
|
52
145
|
if (length > MAX_SSE_EVENT_CHARS) {
|
|
53
146
|
throw new Error(`SSE event exceeded ${MAX_SSE_EVENT_CHARS} characters`);
|
|
54
147
|
}
|
|
55
148
|
}
|
|
56
|
-
|
|
57
|
-
// normalising pass would have to cope with a \r\n split across two chunks.
|
|
58
|
-
function findBoundary(buffer) {
|
|
59
|
-
const lf = buffer.indexOf("\n\n");
|
|
60
|
-
const crlf = buffer.indexOf("\r\n\r\n");
|
|
61
|
-
if (lf === -1 && crlf === -1)
|
|
62
|
-
return undefined;
|
|
63
|
-
if (crlf !== -1 && (lf === -1 || crlf < lf))
|
|
64
|
-
return { start: crlf, end: crlf + 4 };
|
|
65
|
-
return { start: lf, end: lf + 2 };
|
|
66
|
-
}
|
|
67
|
-
function parseData(chunk) {
|
|
68
|
-
const data = chunk
|
|
69
|
-
.split(/\r?\n/)
|
|
70
|
-
.filter((line) => line.startsWith("data:"))
|
|
71
|
-
.map((line) => line.slice("data:".length).trimStart())
|
|
72
|
-
.join("\n");
|
|
149
|
+
function parseData(data) {
|
|
73
150
|
if (data === "" || data === "[DONE]")
|
|
74
151
|
return undefined;
|
|
75
152
|
try {
|
package/dist/sessions/codec.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// can become conversation or provider input.
|
|
4
4
|
import { Buffer } from "node:buffer";
|
|
5
5
|
import { CONTEXT_LIMITS } from "../context/projection.js";
|
|
6
|
-
export const SESSION_SCHEMA =
|
|
6
|
+
export const SESSION_SCHEMA = 4;
|
|
7
7
|
export const SESSION_FILE_LIMITS = Object.freeze({
|
|
8
8
|
text: 1_048_576,
|
|
9
9
|
metadataBytes: 64 * 1_024,
|
|
@@ -135,7 +135,7 @@ export function decodeNode(value) {
|
|
|
135
135
|
effort: identity["effort"],
|
|
136
136
|
}),
|
|
137
137
|
messages: Object.freeze(messages.map(messageFromRecord)),
|
|
138
|
-
blocks: Object.freeze(blocks.map(blockFromRecord)),
|
|
138
|
+
blocks: Object.freeze(blocks.map((block) => blockFromRecord(block, version))),
|
|
139
139
|
...(context === undefined ? {} : { context: Object.freeze(context) }),
|
|
140
140
|
...(failure === undefined ? {} : { failure: Object.freeze(failure) }),
|
|
141
141
|
});
|
|
@@ -266,18 +266,23 @@ function blockRecord(block) {
|
|
|
266
266
|
right: block.right,
|
|
267
267
|
tone: block.tone,
|
|
268
268
|
body: block.body?.map(detailRecord) ?? null,
|
|
269
|
+
durationMs: block.durationMs ?? null,
|
|
269
270
|
}];
|
|
270
271
|
}
|
|
271
|
-
function blockFromRecord(value) {
|
|
272
|
+
function blockFromRecord(value, version) {
|
|
272
273
|
if (!record(value) || typeof value["kind"] !== "string")
|
|
273
274
|
throw invalid();
|
|
274
275
|
if ((value["kind"] === "user" || value["kind"] === "answer" || value["kind"] === "reasoning") &&
|
|
275
276
|
keys(value, "kind,text") && boundedText(value["text"]))
|
|
276
277
|
return { kind: value["kind"], text: value["text"] };
|
|
277
|
-
|
|
278
|
+
const toolKeys = version >= 4
|
|
279
|
+
? "body,durationMs,kind,name,right,target,tone"
|
|
280
|
+
: "body,kind,name,right,target,tone";
|
|
281
|
+
if (value["kind"] === "tool" && keys(value, toolKeys) &&
|
|
278
282
|
bounded(value["name"], 256) && boundedText(value["target"]) &&
|
|
279
283
|
boundedText(value["right"], 1_024) &&
|
|
280
284
|
(value["tone"] === "ok" || value["tone"] === "fail" || value["tone"] === "deny") &&
|
|
285
|
+
(version < 4 || nullableInteger(value["durationMs"], 0)) &&
|
|
281
286
|
(value["body"] === null ||
|
|
282
287
|
(Array.isArray(value["body"]) && value["body"].length <= SESSION_FILE_LIMITS.details))) {
|
|
283
288
|
const body = value["body"] === null
|
|
@@ -290,6 +295,9 @@ function blockFromRecord(value) {
|
|
|
290
295
|
right: value["right"],
|
|
291
296
|
tone: value["tone"],
|
|
292
297
|
...(body === undefined ? {} : { body }),
|
|
298
|
+
...(version < 4 || value["durationMs"] === null
|
|
299
|
+
? {}
|
|
300
|
+
: { durationMs: value["durationMs"] }),
|
|
293
301
|
};
|
|
294
302
|
}
|
|
295
303
|
throw invalid();
|
|
@@ -398,7 +406,7 @@ function integer(value, minimum) {
|
|
|
398
406
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
399
407
|
}
|
|
400
408
|
function schema(value) {
|
|
401
|
-
return value === 1 || value === 2 || value === SESSION_SCHEMA;
|
|
409
|
+
return value === 1 || value === 2 || value === 3 || value === SESSION_SCHEMA;
|
|
402
410
|
}
|
|
403
411
|
function settlement(value, version) {
|
|
404
412
|
return value === "checkpointed" || value === "completed" ||
|
package/dist/sessions/store.js
CHANGED
|
@@ -8,6 +8,7 @@ import { chmod, lstat, mkdir, open, opendir, readFile, readdir, realpath, rename
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import { atomicWrite } from "../atomic.js";
|
|
10
10
|
import { CONVERSATION_LIMITS, ConversationTree } from "../conversation.js";
|
|
11
|
+
import { leadingText } from "../text-boundary.js";
|
|
11
12
|
import { userDataPath } from "../user-data.js";
|
|
12
13
|
import { decodeHead, decodeMeta, decodeNode, encodeHead, encodeMeta, encodeNode, SESSION_FILE_LIMITS, SESSION_SCHEMA, } from "./codec.js";
|
|
13
14
|
import { leaseOwner, leaseToken, pidIsAlive, removeLease, sessionLease, } from "./lease.js";
|
|
@@ -367,7 +368,7 @@ function firstUserText(conversation) {
|
|
|
367
368
|
const text = message.content.find((block) => block.kind === "text")?.text
|
|
368
369
|
.replace(/\s+/gu, " ").trim();
|
|
369
370
|
if (text !== undefined && text !== "")
|
|
370
|
-
return text
|
|
371
|
+
return leadingText(text, 160);
|
|
371
372
|
}
|
|
372
373
|
return "Untitled session";
|
|
373
374
|
}
|
package/dist/settings-command.js
CHANGED
|
@@ -41,9 +41,6 @@ export async function settingsCommand(session, host) {
|
|
|
41
41
|
case "maxTokens":
|
|
42
42
|
await numberSetting(session, host, "maxTokens", "max output tokens");
|
|
43
43
|
break;
|
|
44
|
-
case "maxSteps":
|
|
45
|
-
await numberSetting(session, host, "maxSteps", "max tool steps");
|
|
46
|
-
break;
|
|
47
44
|
case "compactionPercent":
|
|
48
45
|
await compactionSetting(session, host);
|
|
49
46
|
break;
|
|
@@ -81,7 +78,6 @@ function settingsItems(values) {
|
|
|
81
78
|
action: "maxTokens",
|
|
82
79
|
option: { label: "max output tokens", value: String(values.maxTokens) },
|
|
83
80
|
}]),
|
|
84
|
-
{ action: "maxSteps", option: { label: "max tool steps", value: String(values.maxSteps) } },
|
|
85
81
|
{
|
|
86
82
|
action: "compactionPercent",
|
|
87
83
|
option: { label: "context compaction", value: `${values.compactionPercent}%` },
|
|
@@ -102,7 +98,6 @@ function settingsValues(session) {
|
|
|
102
98
|
model: session.model,
|
|
103
99
|
effort: session.config.effort,
|
|
104
100
|
...(session.provider.id === "openai-codex" ? {} : { maxTokens: session.config.maxTokens }),
|
|
105
|
-
maxSteps: session.config.maxSteps,
|
|
106
101
|
compactionPercent: session.config.compactionPercent,
|
|
107
102
|
reducedMotion: session.config.reducedMotion,
|
|
108
103
|
};
|
package/dist/settings.js
CHANGED
|
@@ -59,7 +59,6 @@ function normalize(value) {
|
|
|
59
59
|
const effort = member(value["effort"], EFFORTS);
|
|
60
60
|
const reducedMotion = typeof value["reducedMotion"] === "boolean" ? value["reducedMotion"] : undefined;
|
|
61
61
|
const maxTokens = positiveInteger(value["maxTokens"]);
|
|
62
|
-
const maxSteps = positiveInteger(value["maxSteps"]);
|
|
63
62
|
const compactionPercent = percentage(value["compactionPercent"]);
|
|
64
63
|
return {
|
|
65
64
|
...(provider === undefined ? {} : { provider }),
|
|
@@ -68,7 +67,6 @@ function normalize(value) {
|
|
|
68
67
|
...(effort === undefined ? {} : { effort }),
|
|
69
68
|
...(reducedMotion === undefined ? {} : { reducedMotion }),
|
|
70
69
|
...(maxTokens === undefined ? {} : { maxTokens }),
|
|
71
|
-
...(maxSteps === undefined ? {} : { maxSteps }),
|
|
72
70
|
...(compactionPercent === undefined ? {} : { compactionPercent }),
|
|
73
71
|
};
|
|
74
72
|
}
|
package/dist/steering.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// A bounded cooperative inbox for guidance submitted during an active turn.
|
|
2
|
+
export const MAX_STEERING_MESSAGES = 8;
|
|
3
|
+
export const MAX_STEERING_CODE_UNITS = 32_768;
|
|
4
|
+
/**
|
|
5
|
+
* Build one inbox for one model turn. `drainOrClose` is the atomic completion
|
|
6
|
+
* handshake: pending guidance keeps the turn open; an empty queue closes it
|
|
7
|
+
* before the final checkpoint so late input cannot disappear.
|
|
8
|
+
*/
|
|
9
|
+
export function steeringInbox(changed = () => { }) {
|
|
10
|
+
let accepting = true;
|
|
11
|
+
let codeUnits = 0;
|
|
12
|
+
const messages = [];
|
|
13
|
+
const take = () => {
|
|
14
|
+
if (messages.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const drained = messages.splice(0);
|
|
17
|
+
codeUnits = 0;
|
|
18
|
+
changed(0, accepting);
|
|
19
|
+
return drained;
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
offer(text) {
|
|
23
|
+
if (!accepting)
|
|
24
|
+
return "closed";
|
|
25
|
+
if (messages.length >= MAX_STEERING_MESSAGES ||
|
|
26
|
+
codeUnits + text.length > MAX_STEERING_CODE_UNITS)
|
|
27
|
+
return "full";
|
|
28
|
+
messages.push(text);
|
|
29
|
+
codeUnits += text.length;
|
|
30
|
+
changed(messages.length, true);
|
|
31
|
+
return "queued";
|
|
32
|
+
},
|
|
33
|
+
drain: take,
|
|
34
|
+
drainOrClose() {
|
|
35
|
+
if (messages.length > 0)
|
|
36
|
+
return { messages: take(), closed: false };
|
|
37
|
+
accepting = false;
|
|
38
|
+
changed(0, false);
|
|
39
|
+
return { messages: [], closed: true };
|
|
40
|
+
},
|
|
41
|
+
close() {
|
|
42
|
+
if (!accepting && messages.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
accepting = false;
|
|
45
|
+
if (messages.length === 0) {
|
|
46
|
+
changed(0, false);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return take();
|
|
50
|
+
},
|
|
51
|
+
get pending() {
|
|
52
|
+
return messages.length;
|
|
53
|
+
},
|
|
54
|
+
get accepting() {
|
|
55
|
+
return accepting;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|