@opencode/util 0.0.0-dev-21907 → 0.0.0-dev-21915
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/dist/abort.d.ts +1 -0
- package/dist/abort.js +8 -0
- package/dist/duration.d.ts +2 -0
- package/dist/duration.js +22 -0
- package/dist/process-signal.d.ts +5 -0
- package/dist/process-signal.js +22 -0
- package/dist/session-mention.d.ts +11 -0
- package/dist/session-mention.js +18 -0
- package/dist/session-turn.d.ts +29 -0
- package/dist/session-turn.js +45 -0
- package/package.json +1 -1
package/dist/abort.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function abort(controller: AbortController | undefined): void;
|
package/dist/abort.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// `controller.abort()` makes a new DOMException that records the caller's stack, and V8 keeps every frame's function
|
|
2
|
+
// and closure alive until something reads `.stack`. Aborting from a Solid cleanup or update then pins the whole batch
|
|
3
|
+
// it ran in, disposed computations and detached DOM included, for as long as the signal is reachable. This reason is
|
|
4
|
+
// made once, so its stack holds only this module's load.
|
|
5
|
+
const reason = new DOMException("signal is aborted without reason", "AbortError");
|
|
6
|
+
export function abort(controller) {
|
|
7
|
+
controller?.abort(reason);
|
|
8
|
+
}
|
package/dist/duration.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Compact elapsed time, e.g. `850ms`, `12.4s`, `3m 5s`, `2h 10m` or `1d 4h`. */
|
|
2
|
+
export function duration(input) {
|
|
3
|
+
if (input < 1000) {
|
|
4
|
+
return `${input}ms`;
|
|
5
|
+
}
|
|
6
|
+
if (input < 60000) {
|
|
7
|
+
return `${(input / 1000).toFixed(1)}s`;
|
|
8
|
+
}
|
|
9
|
+
if (input < 3600000) {
|
|
10
|
+
const minutes = Math.floor(input / 60000);
|
|
11
|
+
const seconds = Math.floor((input % 60000) / 1000);
|
|
12
|
+
return `${minutes}m ${seconds}s`;
|
|
13
|
+
}
|
|
14
|
+
if (input < 86400000) {
|
|
15
|
+
const hours = Math.floor(input / 3600000);
|
|
16
|
+
const minutes = Math.floor((input % 3600000) / 60000);
|
|
17
|
+
return `${hours}h ${minutes}m`;
|
|
18
|
+
}
|
|
19
|
+
const days = Math.floor(input / 86400000);
|
|
20
|
+
const hours = Math.floor((input % 86400000) / 3600000);
|
|
21
|
+
return `${days}d ${hours}h`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * as ProcessSignal from "./process-signal.js";
|
|
2
|
+
/** Install once at process startup. Removing subscribers never restores the signal's default action. */
|
|
3
|
+
export declare function install(signal: NodeJS.Signals, unavailable: () => void): void;
|
|
4
|
+
/** Subscribe only to an installed signal. The returned cleanup removes this subscriber, not the process handler. */
|
|
5
|
+
export declare function subscribe(signal: NodeJS.Signals, listener: () => void): () => void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export * as ProcessSignal from "./process-signal.js";
|
|
2
|
+
const handlers = new Map();
|
|
3
|
+
/** Install once at process startup. Removing subscribers never restores the signal's default action. */
|
|
4
|
+
export function install(signal, unavailable) {
|
|
5
|
+
if (handlers.has(signal))
|
|
6
|
+
return;
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
handlers.set(signal, listeners);
|
|
9
|
+
process.on(signal, () => {
|
|
10
|
+
if (!listeners.size)
|
|
11
|
+
return unavailable();
|
|
12
|
+
listeners.forEach((listener) => listener());
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Subscribe only to an installed signal. The returned cleanup removes this subscriber, not the process handler. */
|
|
16
|
+
export function subscribe(signal, listener) {
|
|
17
|
+
const listeners = handlers.get(signal);
|
|
18
|
+
listeners?.add(listener);
|
|
19
|
+
return () => {
|
|
20
|
+
listeners?.delete(listener);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type SessionMention = {
|
|
2
|
+
id: `ses_${string}`;
|
|
3
|
+
};
|
|
4
|
+
export declare function sessionMentions(text: string): {
|
|
5
|
+
id: SessionMention["id"];
|
|
6
|
+
text: string;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}[];
|
|
10
|
+
export declare function parseSessionMention(text: string): SessionMention | undefined;
|
|
11
|
+
export declare function serializeSessionMention(mention: SessionMention): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Match complete generated IDs only, including while an assistant is streaming one.
|
|
2
|
+
const pattern = /(?<![\w/:@-])(?:@session:)?(ses_[A-Za-z0-9]{26})(?![\w/-])/g;
|
|
3
|
+
export function sessionMentions(text) {
|
|
4
|
+
return Array.from(text.matchAll(pattern), (match) => ({
|
|
5
|
+
// SAFETY: the capture above requires the session prefix and a complete generated suffix.
|
|
6
|
+
id: match[1],
|
|
7
|
+
text: match[0],
|
|
8
|
+
start: match.index,
|
|
9
|
+
end: match.index + match[0].length,
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
export function parseSessionMention(text) {
|
|
13
|
+
const match = sessionMentions(text)[0];
|
|
14
|
+
return match?.text === text ? { id: match.id } : undefined;
|
|
15
|
+
}
|
|
16
|
+
export function serializeSessionMention(mention) {
|
|
17
|
+
return `@session:${mention.id}`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export * as SessionTurn from "./session-turn.js";
|
|
2
|
+
/** The fields of a session message that turn measurements read. Client session messages satisfy it as they are. */
|
|
3
|
+
export interface Message {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly time: {
|
|
7
|
+
readonly created: number;
|
|
8
|
+
readonly streamed?: number;
|
|
9
|
+
readonly completed?: number;
|
|
10
|
+
};
|
|
11
|
+
readonly tokens?: {
|
|
12
|
+
readonly output: number;
|
|
13
|
+
readonly reasoning: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface Options {
|
|
17
|
+
/** The position of `message` in `messages`. Reactive callers should pass it so the lookup does not read every message. */
|
|
18
|
+
readonly index?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the session has no idle markers, so a turn ends at the next prompt. Reactive callers should pass it from a
|
|
21
|
+
* shared memo: the default scans every message, which subscribes the caller to the whole history.
|
|
22
|
+
*/
|
|
23
|
+
readonly legacy?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Milliseconds from the prompt that started the turn until `message` completed, or 0 while it runs. */
|
|
26
|
+
export declare function duration(messages: readonly Message[], message: Message, options?: Options): number;
|
|
27
|
+
/** Output and reasoning tokens per second across the turn's steps up to `message`, or undefined until every step has streamed. */
|
|
28
|
+
export declare function tokensPerSecond(messages: readonly Message[], message: Message, options?: Options): number | undefined;
|
|
29
|
+
export declare function legacy(messages: readonly Message[]): boolean;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export * as SessionTurn from "./session-turn.js";
|
|
2
|
+
/** Milliseconds from the prompt that started the turn until `message` completed, or 0 while it runs. */
|
|
3
|
+
export function duration(messages, message, options = {}) {
|
|
4
|
+
if (message.time.completed === undefined)
|
|
5
|
+
return 0;
|
|
6
|
+
const index = options.index ?? messages.findIndex((item) => item.id === message.id);
|
|
7
|
+
const input = messages[inputIndex(messages, index === -1 ? messages.length : index, options.legacy ?? legacy(messages))];
|
|
8
|
+
return Math.max(0, message.time.completed - (input?.time.created ?? message.time.created));
|
|
9
|
+
}
|
|
10
|
+
/** Output and reasoning tokens per second across the turn's steps up to `message`, or undefined until every step has streamed. */
|
|
11
|
+
export function tokensPerSecond(messages, message, options = {}) {
|
|
12
|
+
const index = options.index ?? messages.findIndex((item) => item.id === message.id);
|
|
13
|
+
const end = index === -1 ? messages.length : index + 1;
|
|
14
|
+
const start = inputIndex(messages, end, options.legacy ?? legacy(messages));
|
|
15
|
+
const steps = messages.slice(start + 1, end).filter((item) => item.type === "assistant");
|
|
16
|
+
const durations = steps.flatMap((step) => step.time.streamed === undefined ? [] : [Math.max(0, step.time.streamed - step.time.created)]);
|
|
17
|
+
if (steps.length === 0 || durations.length !== steps.length)
|
|
18
|
+
return;
|
|
19
|
+
const output = steps.reduce((total, step) => total + (step.tokens?.output ?? 0) + (step.tokens?.reasoning ?? 0), 0);
|
|
20
|
+
const total = durations.reduce((sum, value) => sum + value, 0);
|
|
21
|
+
if (output <= 0 || total <= 0)
|
|
22
|
+
return;
|
|
23
|
+
// Aggregate before dividing so each step is weighted by its provider-active duration.
|
|
24
|
+
return output / (total / 1_000);
|
|
25
|
+
}
|
|
26
|
+
export function legacy(messages) {
|
|
27
|
+
return !messages.some((message) => message.type === "idle");
|
|
28
|
+
}
|
|
29
|
+
function inputIndex(messages, end, legacy) {
|
|
30
|
+
// Reading a sliced prefix subscribes every caller to unrelated historical messages, so walk
|
|
31
|
+
// back only as far as the turn boundary: the nearest input in legacy sessions, otherwise the
|
|
32
|
+
// first input after the previous idle marker.
|
|
33
|
+
let input = -1;
|
|
34
|
+
for (let index = end - 1; index >= 0; index--) {
|
|
35
|
+
const message = messages[index];
|
|
36
|
+
if (message.type === "idle")
|
|
37
|
+
return input;
|
|
38
|
+
if (message.type !== "user" && message.type !== "synthetic")
|
|
39
|
+
continue;
|
|
40
|
+
if (legacy)
|
|
41
|
+
return index;
|
|
42
|
+
input = index;
|
|
43
|
+
}
|
|
44
|
+
return input;
|
|
45
|
+
}
|