anpord 0.1.2 → 0.1.4
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 +69 -4
- package/dist/cli.cjs +274 -19
- package/dist/cli.mjs +275 -20
- package/dist/client-BCZEcfVE.cjs +759 -0
- package/dist/client-BfhmGRQl.mjs +718 -0
- package/dist/compiler-CKiewwUc.mjs +188 -0
- package/dist/compiler-CZoX4jkx.cjs +198 -0
- package/dist/config.cjs +1 -1
- package/dist/config.mjs +1 -1
- package/dist/{errors-C0E5A8fN.cjs → errors-Boum34f5.cjs} +30 -0
- package/dist/{errors-DEfS6oaQ.mjs → errors-C9bQUcA3.mjs} +25 -1
- package/dist/eval.cjs +4 -0
- package/dist/eval.d.cts +25 -0
- package/dist/eval.d.mts +25 -0
- package/dist/eval.mjs +2 -0
- package/dist/evals-api-DUNVUGUy.d.mts +93 -0
- package/dist/evals-api-sqVNeWet.d.cts +93 -0
- package/dist/index.cjs +428 -8
- package/dist/index.d.cts +2372 -195
- package/dist/index.d.mts +2372 -195
- package/dist/index.mjs +424 -10
- package/dist/source-DQ8HChlm.d.cts +478 -0
- package/dist/source-DQ8HChlm.d.mts +478 -0
- package/dist/source-DvlGawEw.cjs +76 -0
- package/dist/source-Fg1h2VNn.mjs +47 -0
- package/dist/source.cjs +5 -0
- package/dist/source.d.cts +2 -0
- package/dist/source.d.mts +2 -0
- package/dist/source.mjs +2 -0
- package/package.json +18 -4
- package/dist/client-Cno4LwRq.cjs +0 -277
- package/dist/client-bJBNwTKq.mjs +0 -236
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import "@effect/platform";
|
|
3
|
+
//#region ../schema/src/domain/errors.d.ts
|
|
4
|
+
declare const NotFound_base: Schema.TaggedErrorClass<NotFound, "NotFound", {
|
|
5
|
+
readonly _tag: Schema.tag<"NotFound">;
|
|
6
|
+
} & {
|
|
7
|
+
message: typeof Schema.String;
|
|
8
|
+
}>;
|
|
9
|
+
declare class NotFound extends NotFound_base {}
|
|
10
|
+
declare const Conflict_base: Schema.TaggedErrorClass<Conflict, "Conflict", {
|
|
11
|
+
readonly _tag: Schema.tag<"Conflict">;
|
|
12
|
+
} & {
|
|
13
|
+
message: typeof Schema.String;
|
|
14
|
+
}>;
|
|
15
|
+
declare class Conflict extends Conflict_base {}
|
|
16
|
+
declare const BadRequest_base: Schema.TaggedErrorClass<BadRequest, "BadRequest", {
|
|
17
|
+
readonly _tag: Schema.tag<"BadRequest">;
|
|
18
|
+
} & {
|
|
19
|
+
message: typeof Schema.String;
|
|
20
|
+
}>;
|
|
21
|
+
declare class BadRequest extends BadRequest_base {}
|
|
22
|
+
declare const Unauthorized_base: Schema.TaggedErrorClass<Unauthorized, "Unauthorized", {
|
|
23
|
+
readonly _tag: Schema.tag<"Unauthorized">;
|
|
24
|
+
} & {
|
|
25
|
+
message: typeof Schema.String;
|
|
26
|
+
}>;
|
|
27
|
+
declare class Unauthorized extends Unauthorized_base {}
|
|
28
|
+
declare const Forbidden_base: Schema.TaggedErrorClass<Forbidden, "Forbidden", {
|
|
29
|
+
readonly _tag: Schema.tag<"Forbidden">;
|
|
30
|
+
} & {
|
|
31
|
+
message: typeof Schema.String;
|
|
32
|
+
}>;
|
|
33
|
+
/** Distinct from Unauthorized: the caller is who they say they are, and the
|
|
34
|
+
* answer is still no. Signing in again cannot help, so the client must not
|
|
35
|
+
* treat this as an expired session. */
|
|
36
|
+
declare class Forbidden extends Forbidden_base {}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region ../schema/src/public/evals-api.d.ts
|
|
39
|
+
declare const PublicStartEvalRequest: Schema.Struct<{
|
|
40
|
+
cases: Schema.filter<Schema.Array$<Schema.refine<{
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly setup?: string | null | undefined;
|
|
43
|
+
readonly source?: {
|
|
44
|
+
readonly kind: "empty";
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: "repo";
|
|
47
|
+
readonly ref: string | null;
|
|
48
|
+
readonly url: string;
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: "files";
|
|
51
|
+
readonly files: {
|
|
52
|
+
readonly [x: string]: string;
|
|
53
|
+
};
|
|
54
|
+
} | undefined;
|
|
55
|
+
readonly validator?: {
|
|
56
|
+
readonly name: string;
|
|
57
|
+
readonly source: string;
|
|
58
|
+
} | null | undefined;
|
|
59
|
+
readonly variables?: {
|
|
60
|
+
readonly [x: string]: string;
|
|
61
|
+
} | undefined;
|
|
62
|
+
readonly verify: string | null;
|
|
63
|
+
}, Schema.Struct<{
|
|
64
|
+
name: typeof Schema.String;
|
|
65
|
+
setup: Schema.optional<Schema.NullOr<typeof Schema.String>>;
|
|
66
|
+
source: Schema.optional<Schema.Union<[Schema.Struct<{
|
|
67
|
+
kind: Schema.Literal<["empty"]>;
|
|
68
|
+
}>, Schema.Struct<{
|
|
69
|
+
kind: Schema.Literal<["repo"]>;
|
|
70
|
+
ref: Schema.NullOr<typeof Schema.String>;
|
|
71
|
+
url: Schema.refine<string, typeof Schema.String>;
|
|
72
|
+
}>, Schema.Struct<{
|
|
73
|
+
files: Schema.Record$<typeof Schema.String, typeof Schema.String>;
|
|
74
|
+
kind: Schema.Literal<["files"]>;
|
|
75
|
+
}>]>>;
|
|
76
|
+
validator: Schema.optional<Schema.NullOr<Schema.Struct<{
|
|
77
|
+
name: Schema.filter<Schema.filter<typeof Schema.String>>;
|
|
78
|
+
source: Schema.filter<Schema.filter<typeof Schema.String>>;
|
|
79
|
+
}>>>;
|
|
80
|
+
variables: Schema.optional<Schema.Record$<typeof Schema.String, typeof Schema.String>>;
|
|
81
|
+
verify: Schema.NullOr<typeof Schema.String>;
|
|
82
|
+
}>>>>;
|
|
83
|
+
prompt: typeof Schema.String;
|
|
84
|
+
tasks: Schema.filter<Schema.Array$<Schema.Struct<{
|
|
85
|
+
harness: Schema.Literal<["codex", "opencode", "pi", "fx", "claude", "gemini", "qwen", "cursor"]>;
|
|
86
|
+
model: Schema.filter<typeof Schema.String>;
|
|
87
|
+
provider: Schema.Literal<["daytona", "e2b", "upstash", "modal", "cloudflare", "vercel"]>;
|
|
88
|
+
}>>>;
|
|
89
|
+
trials: Schema.filter<typeof Schema.Int>;
|
|
90
|
+
}>;
|
|
91
|
+
type PublicStartEvalRequest = typeof PublicStartEvalRequest.Type;
|
|
92
|
+
//#endregion
|
|
93
|
+
export { NotFound as a, Forbidden as i, BadRequest as n, Unauthorized as o, Conflict as r, PublicStartEvalRequest as t };
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,373 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_client = require("./client-
|
|
3
|
-
const require_errors = require("./errors-
|
|
2
|
+
const require_client = require("./client-BCZEcfVE.cjs");
|
|
3
|
+
const require_errors = require("./errors-Boum34f5.cjs");
|
|
4
|
+
const require_source = require("./source-DvlGawEw.cjs");
|
|
4
5
|
let _effect_platform = require("@effect/platform");
|
|
5
6
|
let effect = require("effect");
|
|
7
|
+
//#region ../template/src/errors.ts
|
|
8
|
+
/**
|
|
9
|
+
* A template asked for something the caller did not supply.
|
|
10
|
+
*
|
|
11
|
+
* A plain `Error` rather than a tagged one: this package has no dependencies,
|
|
12
|
+
* and it is thrown across the published SDK boundary where callers reach for
|
|
13
|
+
* `instanceof`.
|
|
14
|
+
*/
|
|
15
|
+
var MissingVariables = class extends Error {
|
|
16
|
+
missing;
|
|
17
|
+
constructor(missing) {
|
|
18
|
+
const names = missing.map((name) => `{{${name}}}`).join(", ");
|
|
19
|
+
super(`No value for ${names}. Pass them, or set onMissing to "keep" or "empty".`);
|
|
20
|
+
this.name = "MissingVariables";
|
|
21
|
+
this.missing = missing;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region ../template/src/render.ts
|
|
26
|
+
/** Null and undefined read as absent rather than as the strings "null" and
|
|
27
|
+
* "undefined", because that is what `{ name: user.name }` produces when the
|
|
28
|
+
* field is not there. */
|
|
29
|
+
const suppliedValue = (values, name) => {
|
|
30
|
+
const value = values[name];
|
|
31
|
+
return value === null || value === void 0 ? void 0 : String(value);
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Fills `{{name}}` from a flat map.
|
|
35
|
+
*
|
|
36
|
+
* Dots and hyphens are part of the name rather than a path: `{{user.name}}`
|
|
37
|
+
* reads the key `"user.name"`. One rule beats two, and a caller who wants
|
|
38
|
+
* nesting writes the flat key themselves.
|
|
39
|
+
*/
|
|
40
|
+
function render(template, values, options = {}) {
|
|
41
|
+
const onMissing = options.onMissing ?? "throw";
|
|
42
|
+
const missing = [];
|
|
43
|
+
const content = template.replace(require_errors.tokenMatcher(), (match, open, close, name) => {
|
|
44
|
+
if (open !== void 0) return "{{";
|
|
45
|
+
if (close !== void 0) return "}}";
|
|
46
|
+
if (name === void 0) return match;
|
|
47
|
+
const value = suppliedValue(values, name);
|
|
48
|
+
if (value !== void 0) return value;
|
|
49
|
+
if (!missing.includes(name)) missing.push(name);
|
|
50
|
+
return onMissing === "empty" ? "" : match;
|
|
51
|
+
});
|
|
52
|
+
if (onMissing === "throw" && missing.length > 0) throw new MissingVariables(missing);
|
|
53
|
+
return {
|
|
54
|
+
content,
|
|
55
|
+
missing
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/client/cache/cache-key.ts
|
|
60
|
+
/** Structural rather than referential, so two identical selectors are one
|
|
61
|
+
* entry and one in-flight request rather than two. A plain object literal
|
|
62
|
+
* hashes by reference and every call would miss. */
|
|
63
|
+
const cacheKey = (selector) => effect.Data.struct({
|
|
64
|
+
channel: selector.channel,
|
|
65
|
+
id: selector.id,
|
|
66
|
+
includeVersions: selector.includeVersions,
|
|
67
|
+
version: selector.version
|
|
68
|
+
});
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/client/cache/keys.ts
|
|
71
|
+
const NAMESPACE = "prompt";
|
|
72
|
+
/**
|
|
73
|
+
* Which version a selector asks for.
|
|
74
|
+
*
|
|
75
|
+
* Asking for no channel is its own selector rather than a named one: the
|
|
76
|
+
* organisation decides which channel answers, it can rename or drop that
|
|
77
|
+
* channel, and naming a guess here would key the answer under a channel the
|
|
78
|
+
* server never resolved.
|
|
79
|
+
*/
|
|
80
|
+
const selectorSegment = (selector) => {
|
|
81
|
+
if (selector.version !== void 0) return `v:${selector.version}`;
|
|
82
|
+
return selector.channel === void 0 ? "default" : `c:${selector.channel}`;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* What makes one cached answer different from another.
|
|
86
|
+
*
|
|
87
|
+
* `includeVersions` belongs here even though the server's key omits it: this
|
|
88
|
+
* cache holds the response rather than the resolution, and a response asked
|
|
89
|
+
* for without history has no `versions` to give to a caller who wanted it.
|
|
90
|
+
*/
|
|
91
|
+
const promptKey = (selector) => [
|
|
92
|
+
NAMESPACE,
|
|
93
|
+
encodeURIComponent(selector.id),
|
|
94
|
+
selectorSegment(selector),
|
|
95
|
+
selector.includeVersions === true ? "full" : "bare"
|
|
96
|
+
].join(":");
|
|
97
|
+
/** Every key for one prompt, so a write invalidates all of them. */
|
|
98
|
+
const promptPrefix = (id) => `${NAMESPACE}:${encodeURIComponent(id)}:`;
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/client/cache/prompt-cache.ts
|
|
101
|
+
var PromptCache = class extends effect.Context.Tag("@anpord/sdk/PromptCache")() {};
|
|
102
|
+
const make$1 = (settings, fetch) => effect.Effect.gen(function* () {
|
|
103
|
+
const scope = yield* effect.Effect.scope;
|
|
104
|
+
const permits = yield* effect.Effect.makeSemaphore(settings.maxConcurrentRefresh);
|
|
105
|
+
const cache = yield* effect.Cache.makeWith({
|
|
106
|
+
capacity: settings.capacity,
|
|
107
|
+
lookup: (selector) => fetch(selector),
|
|
108
|
+
/** A failure is not an answer, so it is not kept: the next call tries
|
|
109
|
+
* again rather than waiting out a whole window on one bad response. */
|
|
110
|
+
timeToLive: (exit) => effect.Exit.isFailure(exit) ? effect.Duration.zero : effect.Duration.infinity
|
|
111
|
+
});
|
|
112
|
+
/** A pinned version cannot change, so only capacity ever evicts it. A
|
|
113
|
+
* channel can be repointed at any moment. */
|
|
114
|
+
const ttlFor = (selector) => selector.version === void 0 ? settings.ttlMs : Number.POSITIVE_INFINITY;
|
|
115
|
+
const ageOf = (key) => effect.Effect.gen(function* () {
|
|
116
|
+
const stats = yield* cache.entryStats(key);
|
|
117
|
+
if (effect.Option.isNone(stats)) return effect.Option.none();
|
|
118
|
+
const now = yield* effect.Clock.currentTimeMillis;
|
|
119
|
+
return effect.Option.some(Math.max(0, now - stats.value.loadedMillis));
|
|
120
|
+
});
|
|
121
|
+
/** Forked into the cache's own scope rather than as a daemon, so a client
|
|
122
|
+
* that is disposed takes its refreshes with it. A refresh that cannot get
|
|
123
|
+
* a permit is dropped: the next call will ask again. */
|
|
124
|
+
const revalidate = (key) => permits.withPermitsIfAvailable(1)(cache.refresh(key)).pipe(effect.Effect.catchAllCause(() => effect.Effect.void), effect.Effect.forkIn(scope), effect.Effect.asVoid);
|
|
125
|
+
const heldWithin = (selector, limitMs) => effect.Effect.gen(function* () {
|
|
126
|
+
const key = cacheKey(selector);
|
|
127
|
+
const ttl = ttlFor(key);
|
|
128
|
+
const value = yield* cache.getOptionComplete(key);
|
|
129
|
+
const age = yield* ageOf(key);
|
|
130
|
+
if (effect.Option.isNone(value) || effect.Option.isNone(age)) return effect.Option.none();
|
|
131
|
+
if (age.value > limitMs) return effect.Option.none();
|
|
132
|
+
if (age.value > ttl) yield* revalidate(key);
|
|
133
|
+
return effect.Option.some({
|
|
134
|
+
ageMs: age.value,
|
|
135
|
+
value: value.value
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
held: (selector) => heldWithin(selector, settings.maxStaleMs),
|
|
140
|
+
invalidate: (id) => effect.Effect.gen(function* () {
|
|
141
|
+
const prefix = promptPrefix(id);
|
|
142
|
+
const keys = yield* cache.keys;
|
|
143
|
+
yield* effect.Effect.forEach(keys.filter((key) => promptKey(key).startsWith(prefix)), (key) => cache.invalidate(key), { discard: true });
|
|
144
|
+
}).pipe(effect.Effect.withSpan("PromptCache.invalidate")),
|
|
145
|
+
load: (selector) => effect.Effect.gen(function* () {
|
|
146
|
+
const key = cacheKey(selector);
|
|
147
|
+
const value = yield* fetch(selector);
|
|
148
|
+
yield* cache.set(key, value);
|
|
149
|
+
return value;
|
|
150
|
+
}).pipe(effect.Effect.withSpan("PromptCache.load", { attributes: { key: promptKey(selector) } })),
|
|
151
|
+
stale: (selector) => heldWithin(selector, Number.POSITIVE_INFINITY)
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
const layer$1 = (settings, fetch) => effect.Layer.scoped(PromptCache, make$1(settings, fetch));
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/client/cache/noop.ts
|
|
157
|
+
/** Disabled caching is a cache that holds nothing, so the one path through
|
|
158
|
+
* `resolvePrompt` still runs rather than being wrapped in a condition. */
|
|
159
|
+
const noopLayer = (fetch) => effect.Layer.succeed(PromptCache, {
|
|
160
|
+
/** Effects rather than bare values: yielding an Option would make an empty
|
|
161
|
+
* one a failure, and every read would end in NoSuchElementException. */
|
|
162
|
+
held: () => effect.Effect.succeedNone,
|
|
163
|
+
invalidate: () => effect.Effect.void,
|
|
164
|
+
load: fetch,
|
|
165
|
+
stale: () => effect.Effect.succeedNone
|
|
166
|
+
});
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/client/cache/failure.ts
|
|
169
|
+
/** A status the server chose deliberately, telling the caller something true
|
|
170
|
+
* about their request rather than about its own health. */
|
|
171
|
+
const DEFINITIVE = /* @__PURE__ */ new Set([
|
|
172
|
+
400,
|
|
173
|
+
401,
|
|
174
|
+
403,
|
|
175
|
+
404,
|
|
176
|
+
409,
|
|
177
|
+
422
|
|
178
|
+
]);
|
|
179
|
+
/**
|
|
180
|
+
* Whether a failure means the answer is unavailable rather than no.
|
|
181
|
+
*
|
|
182
|
+
* A 404 is a successful reply carrying real information: this prompt does not
|
|
183
|
+
* exist. Covering it with a fallback turns a typo in a prompt id into a silent
|
|
184
|
+
* permanent substitution that ships and is found weeks later. Serving stale
|
|
185
|
+
* content covers the case where a prompt someone was using is removed, and it
|
|
186
|
+
* sits ahead of the fallback anyway.
|
|
187
|
+
*
|
|
188
|
+
* An unrecognised failure counts as unavailable: a transport error carries no
|
|
189
|
+
* status at all, and guessing the other way would turn a flaky network into an
|
|
190
|
+
* outage.
|
|
191
|
+
*/
|
|
192
|
+
const isAvailabilityFailure = (error) => error.status === void 0 || !DEFINITIVE.has(error.status);
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/client/cache/fallback.ts
|
|
195
|
+
/** No version the API can return is zero, so a caller who logs it knows
|
|
196
|
+
* without being told that they are reading a fallback rather than a prompt. */
|
|
197
|
+
const FALLBACK_VERSION = 0;
|
|
198
|
+
/**
|
|
199
|
+
* The shape of a prompt, built from what the caller had to hand.
|
|
200
|
+
*
|
|
201
|
+
* The branded fields are asserted rather than decoded: this value never came
|
|
202
|
+
* from the API and never goes back to it, and a fallback that could fail to
|
|
203
|
+
* parse would defeat the reason it exists.
|
|
204
|
+
*/
|
|
205
|
+
const fallbackPrompt = (selector, fallback, now) => {
|
|
206
|
+
const given = typeof fallback === "string" ? { content: fallback } : fallback;
|
|
207
|
+
return {
|
|
208
|
+
channel: selector.channel ?? null,
|
|
209
|
+
config: given.config ?? {},
|
|
210
|
+
content: given.content,
|
|
211
|
+
createdAt: effect.DateTime.unsafeMake(now),
|
|
212
|
+
id: selector.id,
|
|
213
|
+
message: null,
|
|
214
|
+
name: selector.id,
|
|
215
|
+
version: FALLBACK_VERSION
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/client/cache/resolve.ts
|
|
220
|
+
/**
|
|
221
|
+
* Which answer the caller gets, in the order the answers are worth having.
|
|
222
|
+
*
|
|
223
|
+
* Fresh cache first, because that is the whole point. Then the API, since it
|
|
224
|
+
* is the only source that can be right. Then whatever is held however old,
|
|
225
|
+
* because a prompt someone wrote and promoted beats a string written at deploy
|
|
226
|
+
* time. Then the fallback, which the caller asked for. Then the failure, since
|
|
227
|
+
* with nothing cached and no fallback there is no honest answer to give.
|
|
228
|
+
*/
|
|
229
|
+
const resolvePrompt = (options) => effect.Effect.gen(function* () {
|
|
230
|
+
const cache = yield* PromptCache;
|
|
231
|
+
const key = promptKey(options);
|
|
232
|
+
const held = yield* cache.held(options);
|
|
233
|
+
if (effect.Option.isSome(held)) return {
|
|
234
|
+
metadata: {
|
|
235
|
+
ageMs: held.value.ageMs,
|
|
236
|
+
/** Where the answer came from, not how old it is. An entry read in
|
|
237
|
+
* the same millisecond it was stored is still an entry, and calling
|
|
238
|
+
* it fresh made a cache hit indistinguishable from a network read. */
|
|
239
|
+
freshness: "cached",
|
|
240
|
+
key
|
|
241
|
+
},
|
|
242
|
+
value: held.value.value
|
|
243
|
+
};
|
|
244
|
+
const loaded = yield* effect.Effect.either(cache.load(options));
|
|
245
|
+
if (loaded._tag === "Right") return {
|
|
246
|
+
metadata: {
|
|
247
|
+
ageMs: 0,
|
|
248
|
+
freshness: "fresh",
|
|
249
|
+
key
|
|
250
|
+
},
|
|
251
|
+
value: loaded.right
|
|
252
|
+
};
|
|
253
|
+
const error = require_errors.asAnpordError(loaded.left);
|
|
254
|
+
if (!isAvailabilityFailure(error)) return yield* effect.Effect.fail(error);
|
|
255
|
+
const remembered = yield* cache.stale(options);
|
|
256
|
+
if (effect.Option.isSome(remembered)) return {
|
|
257
|
+
metadata: {
|
|
258
|
+
ageMs: remembered.value.ageMs,
|
|
259
|
+
freshness: "stale",
|
|
260
|
+
key,
|
|
261
|
+
reason: error.message
|
|
262
|
+
},
|
|
263
|
+
value: remembered.value.value
|
|
264
|
+
};
|
|
265
|
+
if (options.fallback === void 0) return yield* effect.Effect.fail(error);
|
|
266
|
+
const now = yield* effect.Clock.currentTimeMillis;
|
|
267
|
+
return {
|
|
268
|
+
metadata: {
|
|
269
|
+
ageMs: 0,
|
|
270
|
+
freshness: "fallback",
|
|
271
|
+
key,
|
|
272
|
+
reason: error.message
|
|
273
|
+
},
|
|
274
|
+
value: fallbackPrompt(options, options.fallback, now)
|
|
275
|
+
};
|
|
276
|
+
}).pipe(effect.Effect.withSpan("Prompts.resolve", { attributes: { key: promptKey(options) } }));
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/client/cache/settings.ts
|
|
279
|
+
/**
|
|
280
|
+
* How long a channel's answer is trusted without asking again.
|
|
281
|
+
*
|
|
282
|
+
* This is the delay between a promotion and callers seeing it, so it is short:
|
|
283
|
+
* the dashboard tells whoever promotes that callers receive the new version
|
|
284
|
+
* immediately, and a minute of drift makes that untrue exactly when someone is
|
|
285
|
+
* rolling back something bad. Serving stale while refreshing means the number
|
|
286
|
+
* costs no latency, only freshness.
|
|
287
|
+
*/
|
|
288
|
+
const TTL_MS = 15e3;
|
|
289
|
+
/** Long enough to cover a deploy or an incident, short enough that a value
|
|
290
|
+
* this old is worth saying out loud. Only reached while the API answers; when
|
|
291
|
+
* it cannot be reached at all there is no bound, since a day-old real prompt
|
|
292
|
+
* beats no prompt. */
|
|
293
|
+
const MAX_STALE_MS = 864e5;
|
|
294
|
+
/** Least recently used past this. A thousand prompts is far more than one
|
|
295
|
+
* process serves and still bounds the memory a long-running server holds. */
|
|
296
|
+
const CAPACITY = 1e3;
|
|
297
|
+
/** A cold cache going stale at once must not fork a refresh per prompt. */
|
|
298
|
+
const MAX_CONCURRENT_REFRESH = 8;
|
|
299
|
+
const positive = (value, fallback) => value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
300
|
+
const settingsFrom = (options) => {
|
|
301
|
+
const given = typeof options === "object" ? options : {};
|
|
302
|
+
return {
|
|
303
|
+
capacity: positive(given.capacity, CAPACITY),
|
|
304
|
+
maxConcurrentRefresh: positive(given.maxConcurrentRefresh, MAX_CONCURRENT_REFRESH),
|
|
305
|
+
maxStaleMs: positive(given.maxStaleMs, MAX_STALE_MS),
|
|
306
|
+
ttlMs: positive(given.ttlMs, TTL_MS)
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const cacheEnabled = (options) => options !== false;
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/client/promised.ts
|
|
312
|
+
const promised = (group) => Object.fromEntries(Object.entries(group).map(([method, run]) => [method, async (request) => {
|
|
313
|
+
const exit = await effect.Effect.runPromiseExit(run({ payload: request ?? {} }));
|
|
314
|
+
if (effect.Exit.isSuccess(exit)) return exit.value;
|
|
315
|
+
throw require_errors.asAnpordError(effect.Cause.failureOption(exit.cause).pipe(effect.Option.getOrElse(() => effect.Cause.squash(exit.cause))));
|
|
316
|
+
}]));
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/client/wait.ts
|
|
319
|
+
var EvalTimeout = class extends Error {
|
|
320
|
+
name = "EvalTimeout";
|
|
321
|
+
runId;
|
|
322
|
+
constructor(runId, elapsedMs) {
|
|
323
|
+
const elapsed = elapsedMs < 1e3 ? `${elapsedMs}ms` : `${Math.round(elapsedMs / 1e3)}s`;
|
|
324
|
+
super(`Eval ${runId} was still running after ${elapsed}. It was not cancelled — read it later with evals.get({ id: "${runId}" }).`);
|
|
325
|
+
this.runId = runId;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
var EvalAborted = class extends Error {
|
|
329
|
+
name = "EvalAborted";
|
|
330
|
+
runId;
|
|
331
|
+
constructor(runId) {
|
|
332
|
+
super(`Waiting on eval ${runId} was aborted. The run was not cancelled — read it later with evals.get({ id: "${runId}" }).`);
|
|
333
|
+
this.runId = runId;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const DEFAULT_INTERVAL = 2e3;
|
|
337
|
+
const DEFAULT_MAX_INTERVAL = 1e4;
|
|
338
|
+
const DEFAULT_TIMEOUT = 36e5;
|
|
339
|
+
const nextInterval = (current, max) => Math.min(Math.round(current * 1.5), max);
|
|
340
|
+
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
341
|
+
const timer = setTimeout(() => {
|
|
342
|
+
signal?.removeEventListener("abort", onAbort);
|
|
343
|
+
resolve();
|
|
344
|
+
}, ms);
|
|
345
|
+
function onAbort() {
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
reject(signal?.reason);
|
|
348
|
+
}
|
|
349
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
350
|
+
});
|
|
351
|
+
const waitForRun = async (get, id, options = {}) => {
|
|
352
|
+
const { maxIntervalMs = DEFAULT_MAX_INTERVAL, onProgress, pollIntervalMs = DEFAULT_INTERVAL, signal, timeoutMs = DEFAULT_TIMEOUT } = options;
|
|
353
|
+
const startedAt = Date.now();
|
|
354
|
+
let interval = pollIntervalMs;
|
|
355
|
+
for (;;) {
|
|
356
|
+
if (signal?.aborted) throw new EvalAborted(id);
|
|
357
|
+
const run = await get({ id });
|
|
358
|
+
onProgress?.(run);
|
|
359
|
+
if (run.status !== "running") return run;
|
|
360
|
+
const elapsed = Date.now() - startedAt;
|
|
361
|
+
if (elapsed >= timeoutMs) throw new EvalTimeout(id, elapsed);
|
|
362
|
+
try {
|
|
363
|
+
await sleep(Math.min(interval, timeoutMs - elapsed), signal);
|
|
364
|
+
} catch {
|
|
365
|
+
throw new EvalAborted(id);
|
|
366
|
+
}
|
|
367
|
+
interval = nextInterval(interval, maxIntervalMs);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
//#endregion
|
|
6
371
|
//#region src/client/anpord.ts
|
|
7
372
|
const resolveApiKey = (provided) => {
|
|
8
373
|
const apiKey = provided ?? globalThis.process?.env?.ANPORD_API_KEY;
|
|
@@ -10,26 +375,81 @@ const resolveApiKey = (provided) => {
|
|
|
10
375
|
return apiKey;
|
|
11
376
|
};
|
|
12
377
|
var Anpord = class {
|
|
378
|
+
evals;
|
|
13
379
|
prompts;
|
|
380
|
+
runtime;
|
|
14
381
|
constructor(options = {}) {
|
|
15
382
|
const apiKey = resolveApiKey(options.apiKey);
|
|
16
383
|
const client = require_client.make({
|
|
17
384
|
apiKey: effect.Redacted.make(apiKey),
|
|
18
385
|
baseUrl: options.baseUrl ?? "https://api.anpord.com"
|
|
19
386
|
}).pipe(effect.Effect.provide(_effect_platform.FetchHttpClient.layer), effect.Effect.runSync);
|
|
20
|
-
|
|
387
|
+
const fetch = (selector) => client.prompts.get({ payload: selector });
|
|
388
|
+
this.runtime = effect.ManagedRuntime.make(cacheEnabled(options.cache) ? layer$1(settingsFrom(options.cache), fetch) : noopLayer(fetch));
|
|
389
|
+
const group = promised(client.prompts);
|
|
390
|
+
const evals = promised(client.evals);
|
|
391
|
+
this.evals = {
|
|
392
|
+
...evals,
|
|
393
|
+
startAndWait: async (options) => {
|
|
394
|
+
const { maxIntervalMs, onProgress, pollIntervalMs, signal, timeoutMs, ...request } = options;
|
|
395
|
+
const { id } = await evals.start(request);
|
|
396
|
+
return await waitForRun(evals.get, id, {
|
|
397
|
+
maxIntervalMs,
|
|
398
|
+
onProgress,
|
|
399
|
+
pollIntervalMs,
|
|
400
|
+
signal,
|
|
401
|
+
timeoutMs
|
|
402
|
+
});
|
|
403
|
+
},
|
|
404
|
+
wait: ({ id, ...options }) => waitForRun(evals.get, id, options)
|
|
405
|
+
};
|
|
406
|
+
const forget = (id) => this.runtime.runPromise(effect.Effect.flatMap(PromptCache, (cache) => cache.invalidate(id))).catch(() => void 0);
|
|
407
|
+
this.prompts = {
|
|
408
|
+
...group,
|
|
409
|
+
get: (request) => this.resolve(request),
|
|
410
|
+
promote: invalidating(group.promote, forget),
|
|
411
|
+
update: invalidating(group.update, forget)
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
dispose() {
|
|
415
|
+
return this.runtime.dispose();
|
|
416
|
+
}
|
|
417
|
+
[Symbol.asyncDispose]() {
|
|
418
|
+
return this.runtime.dispose();
|
|
419
|
+
}
|
|
420
|
+
async resolve(options) {
|
|
421
|
+
const exit = await this.runtime.runPromiseExit(resolvePrompt(options));
|
|
422
|
+
if (effect.Exit.isFailure(exit)) throw require_errors.asAnpordError(effect.Cause.failureOption(exit.cause).pipe(effect.Option.getOrElse(() => effect.Cause.squash(exit.cause))));
|
|
423
|
+
const prompt = exit.value.value;
|
|
424
|
+
const value = options.variables === void 0 ? { ...prompt } : {
|
|
425
|
+
...prompt,
|
|
426
|
+
content: render(prompt.content, options.variables).content
|
|
427
|
+
};
|
|
428
|
+
return Object.defineProperty(value, "anpord", {
|
|
429
|
+
enumerable: false,
|
|
430
|
+
value: exit.value.metadata
|
|
431
|
+
});
|
|
21
432
|
}
|
|
22
433
|
};
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
434
|
+
const invalidating = (write, forget) => async (request) => {
|
|
435
|
+
const result = await write(request);
|
|
436
|
+
await forget(request.id);
|
|
437
|
+
return result;
|
|
438
|
+
};
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/evals/define.ts
|
|
441
|
+
const defineEval = (definition) => definition;
|
|
28
442
|
//#endregion
|
|
29
443
|
exports.Anpord = Anpord;
|
|
30
444
|
exports.AnpordApi = require_client.AnpordApi;
|
|
31
445
|
exports.AnpordError = require_errors.AnpordError;
|
|
32
446
|
exports.DEFAULT_BASE_URL = require_client.DEFAULT_BASE_URL;
|
|
447
|
+
exports.EvalAborted = EvalAborted;
|
|
448
|
+
exports.EvalTimeout = EvalTimeout;
|
|
33
449
|
exports.MissingApiKey = require_errors.MissingApiKey;
|
|
450
|
+
exports.defineEval = defineEval;
|
|
451
|
+
exports.empty = require_source.empty;
|
|
452
|
+
exports.files = require_source.files;
|
|
34
453
|
exports.layer = require_client.layer;
|
|
35
454
|
exports.make = require_client.make;
|
|
455
|
+
exports.repo = require_source.repo;
|