@zenera/neo 1.1.10 → 1.1.12
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 +3 -3
- package/dist/agent.d.ts +8 -7
- package/dist/agent.js +15 -8
- package/dist/architecture.d.ts +7 -5
- package/dist/architecture.js +20 -12
- package/dist/embedding.d.ts +20 -1
- package/dist/embedding.js +3 -2
- package/dist/embeddings/fanout.d.ts +51 -0
- package/dist/embeddings/fanout.js +76 -0
- package/dist/embeddings/gemini.d.ts +3 -10
- package/dist/embeddings/gemini.js +46 -30
- package/dist/embeddings/index.d.ts +3 -0
- package/dist/embeddings/index.js +3 -0
- package/dist/embeddings/limiter.d.ts +36 -0
- package/dist/embeddings/limiter.js +182 -0
- package/dist/embeddings/openai.d.ts +3 -2
- package/dist/embeddings/openai.js +54 -13
- package/dist/embeddings/openrouter.d.ts +2 -1
- package/dist/embeddings/openrouter.js +52 -23
- package/dist/embeddings/rate-limit.d.ts +16 -0
- package/dist/embeddings/rate-limit.js +131 -0
- package/dist/failure.d.ts +82 -0
- package/dist/failure.js +323 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +13 -2
- package/dist/inspect/client.d.ts +1 -1
- package/dist/inspect/client.js +42 -26
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +40 -23
- package/dist/memory/files.d.ts +24 -0
- package/dist/memory/files.js +85 -0
- package/dist/memory/graph.d.ts +56 -0
- package/dist/memory/graph.js +159 -0
- package/dist/memory/index.d.ts +93 -0
- package/dist/memory/index.js +357 -0
- package/dist/memory/instructions.d.ts +15 -0
- package/dist/memory/instructions.js +103 -0
- package/dist/memory/page.d.ts +8 -0
- package/dist/memory/page.js +797 -0
- package/dist/memory/recall.d.ts +35 -0
- package/dist/memory/recall.js +161 -0
- package/dist/memory/render.d.ts +10 -0
- package/dist/memory/render.js +228 -0
- package/dist/memory/report.d.ts +57 -0
- package/dist/memory/report.js +113 -0
- package/dist/memory/store.d.ts +49 -0
- package/dist/memory/store.js +226 -0
- package/dist/memory/tools.d.ts +11 -0
- package/dist/memory/tools.js +310 -0
- package/dist/memory/types.d.ts +192 -0
- package/dist/memory/types.js +105 -0
- package/dist/memory/vectors.d.ts +29 -0
- package/dist/memory/vectors.js +152 -0
- package/dist/models/anthropic.d.ts +2 -1
- package/dist/models/anthropic.js +11 -4
- package/dist/models/factory.d.ts +15 -0
- package/dist/models/factory.js +49 -10
- package/dist/models/gemini.d.ts +2 -1
- package/dist/models/gemini.js +8 -3
- package/dist/models/openai-chat.d.ts +2 -1
- package/dist/models/openai-chat.js +10 -5
- package/dist/models/openai-responses.d.ts +2 -1
- package/dist/models/openai-responses.js +22 -7
- package/dist/models/openrouter.d.ts +2 -1
- package/dist/models/openrouter.js +14 -6
- package/dist/project/config.d.ts +55 -3
- package/dist/project/config.js +76 -1
- package/dist/project/index.d.ts +2 -2
- package/dist/project/index.js +1 -1
- package/dist/project/load.d.ts +16 -2
- package/dist/project/load.js +67 -1
- package/dist/runner.d.ts +4 -2
- package/dist/runner.js +29 -30
- package/dist/services.d.ts +23 -6
- package/dist/services.js +25 -15
- package/dist/tools/workspace.js +195 -17
- package/dist/trajectory.d.ts +14 -12
- package/dist/types.d.ts +5 -5
- package/dist/types.js +3 -3
- package/package.json +2 -5
- package/dist/memory-stores/factory.d.ts +0 -15
- package/dist/memory-stores/factory.js +0 -29
- package/dist/memory-stores/file.d.ts +0 -37
- package/dist/memory-stores/file.js +0 -174
- package/dist/memory-stores/in-memory.d.ts +0 -17
- package/dist/memory-stores/in-memory.js +0 -80
- package/dist/memory-stores/index.d.ts +0 -4
- package/dist/memory-stores/index.js +0 -5
- package/dist/memory.d.ts +0 -115
- package/dist/memory.js +0 -213
package/README.md
CHANGED
|
@@ -134,8 +134,9 @@ of surfacing three turns into a run as a confused model.
|
|
|
134
134
|
wildcard. Workspace file tools and a Podman sandbox ship with the library.
|
|
135
135
|
- **Skills** — instruction bundles discovered and loaded on demand instead of
|
|
136
136
|
permanently occupying the prompt, optionally owning their own tools.
|
|
137
|
-
- **Memory** —
|
|
138
|
-
|
|
137
|
+
- **Memory** — a timed, masked knowledge graph that outlives a run: recall
|
|
138
|
+
returns a stitched subgraph rather than a ranked list, remembered files are
|
|
139
|
+
mounted at `/memory`, and standing preferences go into the system prompt.
|
|
139
140
|
- **Trajectory** — an append-only log of everything that happened. What is sent
|
|
140
141
|
to a provider is derived from it, and compaction _covers_ nodes rather than
|
|
141
142
|
deleting them, so the full record survives even when older turns have to be
|
|
@@ -150,7 +151,6 @@ of surfacing three turns into a run as a confused model.
|
|
|
150
151
|
| `@zenera/neo` | agents, models, tools, kernel, inspection |
|
|
151
152
|
| `@zenera/neo/project` | loading a project folder |
|
|
152
153
|
| `@zenera/neo/skill-providers` | skill discovery and loading |
|
|
153
|
-
| `@zenera/neo/memory-stores` | `MemoryStore` implementations |
|
|
154
154
|
| `@zenera/neo/payload-stores` | payload persistence |
|
|
155
155
|
|
|
156
156
|
## Documentation
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MemoryBinding, type ResolvedMemoryBinding } from './memory/types.ts';
|
|
2
2
|
import type { Model } from './model.ts';
|
|
3
3
|
import type { Instructions } from './prompt.ts';
|
|
4
4
|
import type { SkillBinding } from './skills.ts';
|
|
@@ -22,8 +22,8 @@ export interface AgentOptions<TCtx = unknown> {
|
|
|
22
22
|
tools?: AnyTool<TCtx>[];
|
|
23
23
|
/** agents this one may hand the conversation over to (by name or instance) */
|
|
24
24
|
handoffs?: (string | Agent<TCtx>)[];
|
|
25
|
-
/**
|
|
26
|
-
memory?: MemoryBinding<TCtx
|
|
25
|
+
/** how this agent sees the shared memory graph */
|
|
26
|
+
memory?: MemoryBinding<TCtx>;
|
|
27
27
|
/** on-demand instruction bundles */
|
|
28
28
|
skills?: SkillBinding;
|
|
29
29
|
/** opt-in to parallel sub-agents */
|
|
@@ -40,16 +40,17 @@ export declare class Agent<TCtx = unknown> {
|
|
|
40
40
|
readonly model?: Model;
|
|
41
41
|
readonly tools: AnyTool<TCtx>[];
|
|
42
42
|
readonly handoffs: string[];
|
|
43
|
-
readonly memory
|
|
43
|
+
readonly memory?: MemoryBinding<TCtx>;
|
|
44
44
|
readonly skills?: SkillBinding;
|
|
45
45
|
readonly fork?: ForkOptions;
|
|
46
46
|
constructor(opts: AgentOptions<TCtx>);
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* The memory mask with `sees` resolved against the run context. The
|
|
49
49
|
* resolved value is what gets recorded in the trajectory, so a resumed run
|
|
50
|
-
* cannot drift into another
|
|
50
|
+
* cannot drift into another agent's slice. Everyone always sees the public
|
|
51
|
+
* label, so a binding can only ever widen from there.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
+
memoryBinding(ctx: TCtx): ResolvedMemoryBinding | undefined;
|
|
53
54
|
}
|
|
54
55
|
export declare class AgentRegistry<TCtx = unknown> {
|
|
55
56
|
readonly agents: Map<string, Agent<TCtx>>;
|
package/dist/agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ALL_AGENTS } from "./memory/types.js";
|
|
1
2
|
import { HANDOFF_PREFIX } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Note what is *not* here any more: `maxTurns`. A run ends on a final answer,
|
|
@@ -20,22 +21,28 @@ export class Agent {
|
|
|
20
21
|
this.model = opts.model;
|
|
21
22
|
this.tools = opts.tools ?? [];
|
|
22
23
|
this.handoffs = (opts.handoffs ?? []).map((h) => (typeof h === 'string' ? h : h.name));
|
|
23
|
-
this.memory = opts.memory
|
|
24
|
+
this.memory = opts.memory;
|
|
24
25
|
this.skills = opts.skills;
|
|
25
26
|
this.fork = opts.fork;
|
|
26
27
|
}
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
+
* The memory mask with `sees` resolved against the run context. The
|
|
29
30
|
* resolved value is what gets recorded in the trajectory, so a resumed run
|
|
30
|
-
* cannot drift into another
|
|
31
|
+
* cannot drift into another agent's slice. Everyone always sees the public
|
|
32
|
+
* label, so a binding can only ever widen from there.
|
|
31
33
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
memoryBinding(ctx) {
|
|
35
|
+
const b = this.memory;
|
|
36
|
+
if (!b) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const sees = typeof b.sees === 'function' ? b.sees(ctx) : (b.sees ?? []);
|
|
40
|
+
return {
|
|
36
41
|
access: b.access,
|
|
42
|
+
sees: [...new Set([ALL_AGENTS, ...sees])],
|
|
43
|
+
writes: b.writes ?? [ALL_AGENTS],
|
|
37
44
|
autoRecall: b.autoRecall,
|
|
38
|
-
}
|
|
45
|
+
};
|
|
39
46
|
}
|
|
40
47
|
}
|
|
41
48
|
// ---------------------------------------------------------------------------
|
package/dist/architecture.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentRegistry } from './agent.ts';
|
|
2
|
+
import { type MemoryAccess } from './memory/types.ts';
|
|
2
3
|
import type { Model } from './model.ts';
|
|
3
4
|
import type { Services } from './services.ts';
|
|
4
5
|
import type { SkillBinding } from './skills.ts';
|
|
@@ -18,10 +19,11 @@ export interface ArchSkill {
|
|
|
18
19
|
toolNames?: string[];
|
|
19
20
|
}
|
|
20
21
|
export interface ArchMemory {
|
|
21
|
-
|
|
22
|
-
/** `(per run)` when
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
access: MemoryAccess;
|
|
23
|
+
/** audience labels this agent may read; `(per run)` when resolved from context */
|
|
24
|
+
sees: string[] | '(per run)';
|
|
25
|
+
/** labels it may write */
|
|
26
|
+
writes: string[];
|
|
25
27
|
autoRecall?: boolean;
|
|
26
28
|
}
|
|
27
29
|
export interface ArchAgent {
|
|
@@ -42,7 +44,7 @@ export interface ArchAgent {
|
|
|
42
44
|
/** the binding names a provider the services do not know */
|
|
43
45
|
unresolved?: boolean;
|
|
44
46
|
};
|
|
45
|
-
memory
|
|
47
|
+
memory?: ArchMemory;
|
|
46
48
|
fork?: {
|
|
47
49
|
agents?: string[];
|
|
48
50
|
maxBranches?: number;
|
package/dist/architecture.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ALL_AGENTS } from "./memory/types.js";
|
|
1
2
|
import { HANDOFF_PREFIX } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* Projects a registry into a snapshot. Async only because a skill catalog is
|
|
@@ -83,23 +84,30 @@ function allowFilter(binding) {
|
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
86
|
* With a context in hand this is exactly what the run will record. Without
|
|
86
|
-
* one, a
|
|
87
|
-
* handed `undefined` and either throw or invent a
|
|
87
|
+
* one, a `sees` function is left symbolic rather than called: it would be
|
|
88
|
+
* handed `undefined` and either throw or invent a mask no run ever uses.
|
|
88
89
|
*/
|
|
89
90
|
function describeMemory(agent, context) {
|
|
91
|
+
const b = agent.memory;
|
|
92
|
+
if (!b) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
90
95
|
if (context !== undefined) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const r = agent.memoryBinding(context);
|
|
97
|
+
return {
|
|
98
|
+
access: r.access,
|
|
99
|
+
sees: r.sees,
|
|
100
|
+
writes: r.writes,
|
|
101
|
+
autoRecall: r.autoRecall ? true : undefined,
|
|
102
|
+
};
|
|
97
103
|
}
|
|
98
|
-
return
|
|
99
|
-
store: b.store,
|
|
100
|
-
scope: typeof b.scope === 'function' ? '(per run)' : (b.scope ?? `agent:${agent.name}`),
|
|
104
|
+
return {
|
|
101
105
|
access: b.access,
|
|
106
|
+
sees: typeof b.sees === 'function'
|
|
107
|
+
? '(per run)'
|
|
108
|
+
: [...new Set([ALL_AGENTS, ...(b.sees ?? [])])],
|
|
109
|
+
writes: b.writes ?? [ALL_AGENTS],
|
|
102
110
|
autoRecall: b.autoRecall ? true : undefined,
|
|
103
|
-
}
|
|
111
|
+
};
|
|
104
112
|
}
|
|
105
113
|
//# sourceMappingURL=architecture.js.map
|
package/dist/embedding.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ import type { TokenUsage } from './types.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
export type EmbeddingTaskType = 'query' | 'document';
|
|
13
13
|
export interface EmbeddingRequest {
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The texts to encode, in the order the vectors come back. Any length: the
|
|
16
|
+
* adapter splits it to whatever the model accepts per request and issues
|
|
17
|
+
* those in parallel, so a caller never has to know the cap or invent one.
|
|
18
|
+
*/
|
|
15
19
|
input: string[];
|
|
16
20
|
taskType?: EmbeddingTaskType;
|
|
17
21
|
/** truncate to this width, where the model supports it */
|
|
@@ -21,6 +25,19 @@ export interface EmbeddingRequest {
|
|
|
21
25
|
* Turn it off to see what the model actually returned.
|
|
22
26
|
*/
|
|
23
27
|
normalize?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Texts finished so far, out of the whole input. A large batch is minutes
|
|
30
|
+
* of one `await`, and a caller with nothing to say for that long is
|
|
31
|
+
* indistinguishable from a hung one.
|
|
32
|
+
*/
|
|
33
|
+
onProgress?: (done: number, total: number) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Each slice of the input as it comes back, `at` being where it started in
|
|
36
|
+
* `input`. A large request is many round trips, and a caller that persists
|
|
37
|
+
* them can survive a crash without paying for the same vectors twice.
|
|
38
|
+
* Vectors arrive normalised, exactly as the response will carry them.
|
|
39
|
+
*/
|
|
40
|
+
onSlice?: (at: number, vectors: number[][]) => void;
|
|
24
41
|
signal?: AbortSignal;
|
|
25
42
|
}
|
|
26
43
|
export interface EmbeddingResponse {
|
|
@@ -57,4 +74,6 @@ export interface Embedder {
|
|
|
57
74
|
* provider without three copies of the same loop.
|
|
58
75
|
*/
|
|
59
76
|
export declare function embeddingResponse(vectors: number[][], req: EmbeddingRequest, usage?: TokenUsage): EmbeddingResponse;
|
|
77
|
+
/** Idempotent, so settling a vector that is already unit length changes nothing. */
|
|
78
|
+
export declare function unitVector(vector: number[]): number[];
|
|
60
79
|
//# sourceMappingURL=embedding.d.ts.map
|
package/dist/embedding.js
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export function embeddingResponse(vectors, req, usage) {
|
|
6
6
|
return {
|
|
7
|
-
vectors: req.normalize === false ? vectors : vectors.map(
|
|
7
|
+
vectors: req.normalize === false ? vectors : vectors.map(unitVector),
|
|
8
8
|
dimensions: vectors[0]?.length ?? 0,
|
|
9
9
|
usage,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/** Idempotent, so settling a vector that is already unit length changes nothing. */
|
|
13
|
+
export function unitVector(vector) {
|
|
13
14
|
let sum = 0;
|
|
14
15
|
for (const x of vector) {
|
|
15
16
|
sum += x * x;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type EmbeddingRequest, type EmbeddingResponse } from '../embedding.ts';
|
|
2
|
+
import type { ProviderNamed } from '../failure.ts';
|
|
3
|
+
import { RateLimiter } from './limiter.ts';
|
|
4
|
+
/** What one request came back with, before it is joined to the others. */
|
|
5
|
+
export interface EmbeddingSlice {
|
|
6
|
+
vectors: number[][];
|
|
7
|
+
/** absent where the vendor reports characters, or nothing */
|
|
8
|
+
inputTokens?: number;
|
|
9
|
+
}
|
|
10
|
+
/** The per-model knobs every adapter shares, and the connection they share it with. */
|
|
11
|
+
export interface BatchOptions extends ProviderNamed {
|
|
12
|
+
/** texts per request; the model's own cap applies when unset */
|
|
13
|
+
maxBatch?: number;
|
|
14
|
+
/** estimated tokens per request; the model's own cap applies when unset */
|
|
15
|
+
maxBatchTokens?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Paces every embedding request on one connection. Supplied by the registry
|
|
18
|
+
* so two embedders on one account cannot each ramp up to the full limit;
|
|
19
|
+
* an adapter built by hand gets one of its own.
|
|
20
|
+
*/
|
|
21
|
+
limiter?: RateLimiter;
|
|
22
|
+
}
|
|
23
|
+
export interface FanoutPlan {
|
|
24
|
+
/** the embedder's id, for the one error message that needs it */
|
|
25
|
+
id: string;
|
|
26
|
+
limiter: RateLimiter;
|
|
27
|
+
maxBatch: number;
|
|
28
|
+
maxBatchTokens: number;
|
|
29
|
+
send(input: string[]): Promise<EmbeddingSlice>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Four characters to a token: within about 15% for English prose, wrong for CJK
|
|
33
|
+
* and for long identifiers. It only has to be right enough to stay under a cap
|
|
34
|
+
* that is already generous, and being wrong costs an extra request, not a
|
|
35
|
+
* failed one.
|
|
36
|
+
*/
|
|
37
|
+
export declare const estimateTokens: (text: string) => number;
|
|
38
|
+
export declare function fanout(req: EmbeddingRequest, plan: FanoutPlan): Promise<EmbeddingResponse>;
|
|
39
|
+
interface Part {
|
|
40
|
+
/** where these texts started in the caller's array */
|
|
41
|
+
at: number;
|
|
42
|
+
texts: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Greedy, on both caps at once. A single text over the token budget goes alone
|
|
46
|
+
* and is sent as it is: truncating it would answer with a vector for something
|
|
47
|
+
* the caller did not ask about, which is worse than the error it will get.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sliceBatch(input: readonly string[], maxBatch: number, maxBatchTokens: number): Part[];
|
|
50
|
+
export {};
|
|
51
|
+
//# sourceMappingURL=fanout.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { embeddingResponse, unitVector, } from "../embedding.js";
|
|
2
|
+
import { zeroUsage } from "../types.js";
|
|
3
|
+
import { RateLimiter } from "./limiter.js";
|
|
4
|
+
/**
|
|
5
|
+
* Four characters to a token: within about 15% for English prose, wrong for CJK
|
|
6
|
+
* and for long identifiers. It only has to be right enough to stay under a cap
|
|
7
|
+
* that is already generous, and being wrong costs an extra request, not a
|
|
8
|
+
* failed one.
|
|
9
|
+
*/
|
|
10
|
+
export const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
11
|
+
export async function fanout(req, plan) {
|
|
12
|
+
if (req.input.length === 0) {
|
|
13
|
+
return embeddingResponse([], req);
|
|
14
|
+
}
|
|
15
|
+
const parts = sliceBatch(req.input, plan.maxBatch, plan.maxBatchTokens);
|
|
16
|
+
const settle = req.normalize !== false;
|
|
17
|
+
let done = 0;
|
|
18
|
+
const slices = await Promise.all(parts.map((part) => plan.limiter.run(async () => {
|
|
19
|
+
const slice = await plan.send(part.texts);
|
|
20
|
+
if (slice.vectors.length !== part.texts.length) {
|
|
21
|
+
throw new Error(`embedder "${plan.id}": answered ${slice.vectors.length} vectors ` +
|
|
22
|
+
`for ${part.texts.length} texts`);
|
|
23
|
+
}
|
|
24
|
+
const unit = settle ? slice.vectors.map(unitVector) : slice.vectors;
|
|
25
|
+
done += part.texts.length;
|
|
26
|
+
req.onSlice?.(part.at, unit);
|
|
27
|
+
req.onProgress?.(done, req.input.length);
|
|
28
|
+
return { ...slice, vectors: unit };
|
|
29
|
+
}, req.signal)));
|
|
30
|
+
const vectors = new Array(req.input.length);
|
|
31
|
+
for (const [at, part] of parts.entries()) {
|
|
32
|
+
for (const [i, vector] of slices[at].vectors.entries()) {
|
|
33
|
+
vectors[part.at + i] = vector;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Settled slice by slice above. Doing it again over the join would hold a
|
|
37
|
+
// second copy of every vector at once, which for a large corpus is GBs.
|
|
38
|
+
return embeddingResponse(vectors, { ...req, normalize: false }, usageOf(slices));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Greedy, on both caps at once. A single text over the token budget goes alone
|
|
42
|
+
* and is sent as it is: truncating it would answer with a vector for something
|
|
43
|
+
* the caller did not ask about, which is worse than the error it will get.
|
|
44
|
+
*/
|
|
45
|
+
export function sliceBatch(input, maxBatch, maxBatchTokens) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
let texts = [];
|
|
48
|
+
let at = 0;
|
|
49
|
+
let tokens = 0;
|
|
50
|
+
for (const [i, text] of input.entries()) {
|
|
51
|
+
const cost = estimateTokens(text);
|
|
52
|
+
if (texts.length > 0 && (texts.length >= maxBatch || tokens + cost > maxBatchTokens)) {
|
|
53
|
+
parts.push({ at, texts });
|
|
54
|
+
texts = [];
|
|
55
|
+
at = i;
|
|
56
|
+
tokens = 0;
|
|
57
|
+
}
|
|
58
|
+
texts.push(text);
|
|
59
|
+
tokens += cost;
|
|
60
|
+
}
|
|
61
|
+
if (texts.length > 0) {
|
|
62
|
+
parts.push({ at, texts });
|
|
63
|
+
}
|
|
64
|
+
return parts;
|
|
65
|
+
}
|
|
66
|
+
/** Undefined unless something reported a number: a zero would read as free. */
|
|
67
|
+
function usageOf(slices) {
|
|
68
|
+
let total;
|
|
69
|
+
for (const slice of slices) {
|
|
70
|
+
if (slice.inputTokens !== undefined) {
|
|
71
|
+
total = (total ?? 0) + slice.inputTokens;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return total === undefined ? undefined : { ...zeroUsage(), inputTokens: total };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=fanout.js.map
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import type { GoogleGenAI } from '@google/genai';
|
|
2
|
-
import {
|
|
2
|
+
import type { Embedder, EmbeddingRequest, EmbeddingResponse } from '../embedding.ts';
|
|
3
|
+
import { type BatchOptions } from './fanout.ts';
|
|
3
4
|
/** Provider-specific knobs. */
|
|
4
|
-
export interface GeminiEmbedderOptions {
|
|
5
|
+
export interface GeminiEmbedderOptions extends BatchOptions {
|
|
5
6
|
/** default width; a request may override it */
|
|
6
7
|
dimensions?: number;
|
|
7
8
|
/** a document's title, which the retrieval task type takes into account */
|
|
8
9
|
title?: string;
|
|
9
|
-
/**
|
|
10
|
-
* Texts this model accepts per request. One is the default because that is
|
|
11
|
-
* what every `gemini-embedding-*` model takes — `embedContent` is a
|
|
12
|
-
* single-document endpoint there, and the batch path is a separate,
|
|
13
|
-
* job-based API. The older `text-embedding-*` models accept far more and
|
|
14
|
-
* can say so.
|
|
15
|
-
*/
|
|
16
|
-
maxBatch?: number;
|
|
17
10
|
}
|
|
18
11
|
export declare class GeminiEmbedder implements Embedder {
|
|
19
12
|
#private;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { callSites, called } from "../failure.js";
|
|
2
|
+
import { fanout } from "./fanout.js";
|
|
3
|
+
import { RateLimiter } from "./limiter.js";
|
|
2
4
|
// ---------------------------------------------------------------------------
|
|
3
5
|
// Google Gemini embeddings adapter
|
|
4
6
|
//
|
|
@@ -15,16 +17,36 @@ const TASK_TYPES = {
|
|
|
15
17
|
query: 'RETRIEVAL_QUERY',
|
|
16
18
|
document: 'RETRIEVAL_DOCUMENT',
|
|
17
19
|
};
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* One, because `embedContent` is a single-document endpoint for every
|
|
22
|
+
* `gemini-embedding-*` model — it answers more than one content with *"The
|
|
23
|
+
* embedContent API for this model only supports one content at a time"* — and
|
|
24
|
+
* the batch path is a separate, job-based API. The older `text-embedding-*`
|
|
25
|
+
* models take far more.
|
|
26
|
+
*
|
|
27
|
+
* This is why the fan-out matters most here: a corpus is one request per chunk,
|
|
28
|
+
* so how many may be in flight is the entire runtime of a build.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_BATCH = 1;
|
|
31
|
+
const LEGACY_MAX_BATCH = 250;
|
|
32
|
+
const MAX_BATCH_TOKENS = 20_000;
|
|
33
|
+
const LEGACY_MODEL = /^text-embedding-/;
|
|
20
34
|
export class GeminiEmbedder {
|
|
21
35
|
id;
|
|
22
36
|
#client;
|
|
23
37
|
#options;
|
|
38
|
+
#limiter;
|
|
39
|
+
#maxBatch;
|
|
40
|
+
#maxBatchTokens;
|
|
41
|
+
#site;
|
|
24
42
|
constructor(id, client, options = {}) {
|
|
25
43
|
this.id = id;
|
|
26
44
|
this.#client = client;
|
|
27
45
|
this.#options = options;
|
|
46
|
+
this.#limiter = options.limiter ?? new RateLimiter();
|
|
47
|
+
this.#site = callSites(id, options.provider);
|
|
48
|
+
this.#maxBatch = options.maxBatch ?? (LEGACY_MODEL.test(id) ? LEGACY_MAX_BATCH : MAX_BATCH);
|
|
49
|
+
this.#maxBatchTokens = options.maxBatchTokens ?? MAX_BATCH_TOKENS;
|
|
28
50
|
}
|
|
29
51
|
async embed(req) {
|
|
30
52
|
const config = {
|
|
@@ -34,38 +56,32 @@ export class GeminiEmbedder {
|
|
|
34
56
|
title: this.#options.title,
|
|
35
57
|
outputDimensionality: req.dimensions ?? this.#options.dimensions,
|
|
36
58
|
abortSignal: req.signal,
|
|
59
|
+
// The SDK retries nothing unless `retryOptions` is present, and the
|
|
60
|
+
// client was built with some; one attempt hands a refusal back out
|
|
61
|
+
// to the limiter, which is the only thing that can slow the run down.
|
|
62
|
+
httpOptions: { retryOptions: { attempts: 1 } },
|
|
37
63
|
};
|
|
38
|
-
// The contract is batch-first and this endpoint usually is not, so the
|
|
39
|
-
// splitting happens here rather than in every caller. Chunks are issued
|
|
40
|
-
// in waves and their results concatenated in order, so a caller cannot
|
|
41
|
-
// tell how many requests it took.
|
|
42
|
-
const size = Math.max(1, this.#options.maxBatch ?? 1);
|
|
43
|
-
const chunks = [];
|
|
44
|
-
for (let i = 0; i < req.input.length; i += size) {
|
|
45
|
-
chunks.push(req.input.slice(i, i + size));
|
|
46
|
-
}
|
|
47
|
-
const vectors = [];
|
|
48
|
-
for (let i = 0; i < chunks.length; i += CONCURRENCY) {
|
|
49
|
-
const wave = await Promise.all(chunks.slice(i, i + CONCURRENCY).map((chunk) => this.#send(chunk, config)));
|
|
50
|
-
for (const part of wave) {
|
|
51
|
-
vectors.push(...part);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
64
|
// Usage stays undefined on purpose: this API reports billable
|
|
55
65
|
// *characters*, on Vertex only, which is not a token count and would be
|
|
56
66
|
// a lie in `TokenUsage`.
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
return await fanout(req, {
|
|
68
|
+
id: this.id,
|
|
69
|
+
limiter: this.#limiter,
|
|
70
|
+
maxBatch: this.#maxBatch,
|
|
71
|
+
maxBatchTokens: this.#maxBatchTokens,
|
|
72
|
+
send: async (input) => {
|
|
73
|
+
const res = await called(this.#site('embedding', 'models.embedContent'), () => this.#client.models.embedContent({
|
|
74
|
+
model: this.id,
|
|
75
|
+
// Spelled out as one `Content` per text.
|
|
76
|
+
// `ContentListUnion` also accepts a `string[]`, and
|
|
77
|
+
// reads it as the *parts of a single document* — six
|
|
78
|
+
// sentences in, one vector back, and no error.
|
|
79
|
+
contents: input.map((text) => ({ parts: [{ text }] })),
|
|
80
|
+
config,
|
|
81
|
+
}));
|
|
82
|
+
return { vectors: (res.embeddings ?? []).map((e) => e.values ?? []) };
|
|
83
|
+
},
|
|
67
84
|
});
|
|
68
|
-
return (res.embeddings ?? []).map((e) => e.values ?? []);
|
|
69
85
|
}
|
|
70
86
|
}
|
|
71
87
|
//# sourceMappingURL=gemini.js.map
|
package/dist/embeddings/index.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface LimiterOptions {
|
|
2
|
+
/** never go below this many in flight */
|
|
3
|
+
min?: number;
|
|
4
|
+
/** where to start, before anything is known */
|
|
5
|
+
start?: number;
|
|
6
|
+
/** never go above this many, whatever the provider tolerates */
|
|
7
|
+
max?: number;
|
|
8
|
+
/** consecutive successes, while saturated, that earn one more slot */
|
|
9
|
+
stride?: number;
|
|
10
|
+
/** the first wait after a refusal, doubling while they continue */
|
|
11
|
+
initialBackoffMs?: number;
|
|
12
|
+
maxBackoffMs?: number;
|
|
13
|
+
/** attempts after the first, per task */
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_MIN = 1;
|
|
17
|
+
export declare const DEFAULT_START = 4;
|
|
18
|
+
export declare const DEFAULT_MAX = 16;
|
|
19
|
+
export declare const DEFAULT_STRIDE = 8;
|
|
20
|
+
export declare const DEFAULT_INITIAL_BACKOFF_MS = 1000;
|
|
21
|
+
export declare const DEFAULT_MAX_BACKOFF_MS = 60000;
|
|
22
|
+
export declare const DEFAULT_MAX_RETRIES = 4;
|
|
23
|
+
export declare class RateLimiter {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(options?: LimiterOptions);
|
|
26
|
+
/** How many may be in flight as things stand. Moves; read it for reporting only. */
|
|
27
|
+
get limit(): number;
|
|
28
|
+
get inFlight(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Runs the task when there is room, retries it when the failure allows, and
|
|
31
|
+
* gives up when it does not. The slot is released before any wait, so a
|
|
32
|
+
* task serving its backoff is not also holding the concurrency down.
|
|
33
|
+
*/
|
|
34
|
+
run<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=limiter.d.ts.map
|