@yaklabs-ai/plugin-sdk 0.8.4 → 0.8.5
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 +1 -1
- package/THIRD-PARTY-INVENTORY.json +1 -1
- package/dist/jev.d.ts +76 -0
- package/dist/jev.js +63 -0
- package/dist/signals.d.ts +360 -0
- package/dist/signals.js +132 -0
- package/package.json +9 -1
package/README.md
CHANGED
package/dist/jev.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Questions } from '@typesafe-ai/sdk';
|
|
2
|
+
import { SystemOneRequest } from '@typesafe-ai/sdk';
|
|
3
|
+
import { SystemOneResult } from '@typesafe-ai/sdk';
|
|
4
|
+
import { TypeSafeClient } from '@typesafe-ai/sdk';
|
|
5
|
+
import { Usage } from '@typesafe-ai/sdk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The `withJev` a host with no Jev lane binds: `use` is never called, no client is ever built,
|
|
9
|
+
* and no network is ever touched. It keeps the cancellation contract identical to the real
|
|
10
|
+
* wrapper's so a caller's `signal`/`deadlineAt` handling does not change with host composition.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately here rather than in the runtime: this is the DEFAULT for the SDK's own host
|
|
13
|
+
* factory, which cannot import the runtime uphill. The runtime also delegates fallback execution
|
|
14
|
+
* here, so both paths share cancellation, deadline enforcement, and cleanup.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createFallbackOnlyWithJev(): WithJev;
|
|
17
|
+
|
|
18
|
+
export { Questions }
|
|
19
|
+
|
|
20
|
+
export { SystemOneRequest }
|
|
21
|
+
|
|
22
|
+
export { SystemOneResult }
|
|
23
|
+
|
|
24
|
+
export { TypeSafeClient }
|
|
25
|
+
|
|
26
|
+
export { Usage }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run `use` against a live Jev client, falling back to `fallback` whenever Jev cannot serve the
|
|
30
|
+
* call. The wrapper exists so a feature can ASK for a typed decision without ever depending on
|
|
31
|
+
* one: `fallback` is required, and it runs whenever no host-managed key is saved,
|
|
32
|
+
* the user has not granted `jev.use`, the branch exceeds its budget, `use` throws, or
|
|
33
|
+
* `use` declines by returning `undefined`. With valid options, this rejects on an explicit
|
|
34
|
+
* `signal` abort, reaching `deadlineAt`, or `fallback` itself throwing.
|
|
35
|
+
*
|
|
36
|
+
* ALWAYS PRESENT on the plugin API, on every host. A host that composed no Jev lane binds
|
|
37
|
+
* {@link createFallbackOnlyWithJev}, so a caller never branches on availability — branching is
|
|
38
|
+
* exactly how a fallback gets skipped.
|
|
39
|
+
*
|
|
40
|
+
* `jev` is the vendor's own `TypeSafeClient`, configured with the host's default model and base
|
|
41
|
+
* URL. Native `request.model` overrides remain supported. It
|
|
42
|
+
* is valid only for the duration of the callback: a reference retained past `use`'s scope is
|
|
43
|
+
* refused at the transport, and an in-flight request is aborted when the call stops waiting.
|
|
44
|
+
* The callback itself is ABANDONED, not interrupted — a host cannot stop arbitrary JavaScript
|
|
45
|
+
* that is already running, so `use` must not perform side effects it would mind completing
|
|
46
|
+
* unobserved. The same applies to a running fallback: abort/deadline stops waiting for it but
|
|
47
|
+
* cannot interrupt its JavaScript.
|
|
48
|
+
*/
|
|
49
|
+
export declare type WithJev = <T>(
|
|
50
|
+
/** Return `undefined` to decline (the fallback runs). A feature whose real result can itself be
|
|
51
|
+
* `undefined` should return `null` or its own wrapper type instead. */
|
|
52
|
+
use: (jev: TypeSafeClient) => T | undefined | PromiseLike<T | undefined>,
|
|
53
|
+
/** Required. The behaviour the feature has without Jev. */
|
|
54
|
+
fallback: () => T | PromiseLike<T>, options?: WithJevOptions) => Promise<T>;
|
|
55
|
+
|
|
56
|
+
/** Per-call knobs for {@link WithJev}. Every field is optional; the host applies its own default
|
|
57
|
+
* budget when none is given, so the Jev branch is never unbounded. */
|
|
58
|
+
export declare interface WithJevOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Explicit cancellation. Always propagates as a rejection; never substituted with `fallback`,
|
|
61
|
+
* because an aborted call means "stop", not "use the cheaper path".
|
|
62
|
+
*/
|
|
63
|
+
readonly signal?: AbortSignal;
|
|
64
|
+
/** Bounds the Jev branch alone, measured from the call. */
|
|
65
|
+
readonly timeoutMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Absolute epoch-ms hard deadline for the WHOLE call, `fallback` included. A deadline already
|
|
68
|
+
* passed rejects (like `signal`); one still ahead derives the Jev branch's budget from it and
|
|
69
|
+
* keeps a small reserve so `fallback` has a window to run.
|
|
70
|
+
*/
|
|
71
|
+
readonly deadlineAt?: number;
|
|
72
|
+
/** Attribution slug for host usage accounting. Never sent to Jev. */
|
|
73
|
+
readonly purpose?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { }
|
package/dist/jev.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// packages/plugin-sdk/dist/jev.js
|
|
2
|
+
function createFallbackOnlyWithJev() {
|
|
3
|
+
return async (_use, fallback, options = {}) => {
|
|
4
|
+
if (typeof fallback !== "function") {
|
|
5
|
+
throw new TypeError("withJev requires a `fallback` function as its second argument.");
|
|
6
|
+
}
|
|
7
|
+
const { signal, deadlineAt } = options;
|
|
8
|
+
if (deadlineAt !== void 0 && !Number.isFinite(deadlineAt)) {
|
|
9
|
+
throw new TypeError("withJev's `deadlineAt` must be a finite epoch-ms timestamp.");
|
|
10
|
+
}
|
|
11
|
+
const assertWithinBoundaries = () => {
|
|
12
|
+
if (signal?.aborted === true) {
|
|
13
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
14
|
+
}
|
|
15
|
+
if (deadlineAt !== void 0 && Date.now() >= deadlineAt) {
|
|
16
|
+
throw new Error("withJev's deadline has passed.");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
assertWithinBoundaries();
|
|
20
|
+
let timer;
|
|
21
|
+
let onAbort;
|
|
22
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
23
|
+
if (signal !== void 0) {
|
|
24
|
+
onAbort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
25
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
26
|
+
}
|
|
27
|
+
if (deadlineAt !== void 0) {
|
|
28
|
+
const checkDeadline = () => {
|
|
29
|
+
const remaining = deadlineAt - Date.now();
|
|
30
|
+
if (remaining <= 0) {
|
|
31
|
+
reject(new Error("withJev's deadline has passed."));
|
|
32
|
+
} else {
|
|
33
|
+
timer = setTimeout(checkDeadline, Math.min(remaining, 2147483647));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
checkDeadline();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
const result = await Promise.race([
|
|
41
|
+
Promise.resolve().then(() => {
|
|
42
|
+
assertWithinBoundaries();
|
|
43
|
+
return fallback();
|
|
44
|
+
}),
|
|
45
|
+
stopped
|
|
46
|
+
]);
|
|
47
|
+
assertWithinBoundaries();
|
|
48
|
+
return result;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
assertWithinBoundaries();
|
|
51
|
+
throw error;
|
|
52
|
+
} finally {
|
|
53
|
+
if (timer !== void 0)
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (signal !== void 0 && onAbort !== void 0) {
|
|
56
|
+
signal.removeEventListener("abort", onAbort);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
createFallbackOnlyWithJev
|
|
63
|
+
};
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import type { ChoiceResponse } from '@typesafe-ai/sdk';
|
|
2
|
+
import type { NoulResponse } from '@typesafe-ai/sdk';
|
|
3
|
+
import type { Questions } from '@typesafe-ai/sdk';
|
|
4
|
+
import type { ScoreResponse } from '@typesafe-ai/sdk';
|
|
5
|
+
|
|
6
|
+
export { ChoiceResponse }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `signals` a host with no Jev lane or no signals store binds: `observe` never touches the
|
|
10
|
+
* network, answers `invalid` for an input that breaks the recorded-name grammar exactly as a
|
|
11
|
+
* real host would, and otherwise `unavailable` with `stored: false`; both reads answer
|
|
12
|
+
* `unavailable` with reason `no-store`. So a caller's result handling does not change with host
|
|
13
|
+
* composition.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createFallbackOnlySignals(): KayAppSignals;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The `invalid` observation `observe` answers when a name in `input` breaks the recorded-name
|
|
19
|
+
* grammar, or `undefined` when every name can be recorded. A reader later types these names into
|
|
20
|
+
* an address or a `where` filter, so a row that broke the grammar would be unaddressable or
|
|
21
|
+
* unfilterable forever: the grammars are on {@link KayAppSignalsDetectorInput},
|
|
22
|
+
* {@link KayAppSignalSource}, and {@link KayAppSignalsObserveInput}. Checked first, before any
|
|
23
|
+
* digest, grant, store, or Jev call, by the host and by {@link createFallbackOnlySignals} alike.
|
|
24
|
+
*/
|
|
25
|
+
export declare function invalidObservation(input: KayAppSignalsObserveInput): KayAppSignalObservation | undefined;
|
|
26
|
+
|
|
27
|
+
/** One stored signal: the answers exactly as Jev gave them, and where the observed turn is. */
|
|
28
|
+
export declare interface KayAppSignal {
|
|
29
|
+
/** Recording order: a higher id was recorded later. */
|
|
30
|
+
readonly id: number;
|
|
31
|
+
readonly detector: KayAppSignalsDetectorRef & {
|
|
32
|
+
readonly pluginId: string;
|
|
33
|
+
};
|
|
34
|
+
readonly source: KayAppSignalSource;
|
|
35
|
+
readonly recordedAtMs: number;
|
|
36
|
+
/** Keyed by question name, full probability distributions included. */
|
|
37
|
+
readonly answers: Readonly<Record<string, NoulResponse | ChoiceResponse | ScoreResponse>>;
|
|
38
|
+
/** The inference that produced the answers; absent when the host recorded only part of it. */
|
|
39
|
+
readonly inference?: {
|
|
40
|
+
readonly model: string;
|
|
41
|
+
readonly inputTokens: number;
|
|
42
|
+
readonly outputTokens: number;
|
|
43
|
+
readonly tookMs: number;
|
|
44
|
+
};
|
|
45
|
+
/** Digest of the state Jev read: re-derive the state from the transcript and compare. */
|
|
46
|
+
readonly inputDigest: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One version of one detector, under one rubric, as the catalog lists it. */
|
|
50
|
+
export declare interface KayAppSignalDetector {
|
|
51
|
+
readonly pluginId: string;
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly version: string;
|
|
54
|
+
/**
|
|
55
|
+
* The questions never change under one digest, so cache them by it. Two digests under one
|
|
56
|
+
* version mean the rubric was edited without a version bump.
|
|
57
|
+
*/
|
|
58
|
+
readonly rubricDigest: string;
|
|
59
|
+
readonly questions: Questions;
|
|
60
|
+
readonly firstRecordedAtMs: number;
|
|
61
|
+
readonly lastRecordedAtMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The result of one observe call. `status` is the only field a caller must switch on:
|
|
66
|
+
*
|
|
67
|
+
* - `classified`: Jev answered; `answers`, `model`, and `usage` are the vendor's verbatim
|
|
68
|
+
* response, and `stored` says whether the row landed in the durable store.
|
|
69
|
+
* - `unavailable`: Jev could not answer (no key, refused grant, timeout, vendor error, or a
|
|
70
|
+
* malformed response). `reason` names the host's classification; no row is written, so the
|
|
71
|
+
* same input can be observed again later without a conflict.
|
|
72
|
+
* - `invalid`: the input breaks the recorded-name grammar (see {@link invalidObservation}), and
|
|
73
|
+
* `reason` says which name and why. The caller's mistake, not the environment's: Jev is never
|
|
74
|
+
* called, no row is written, and the same input is refused again. `detector` and `source`
|
|
75
|
+
* echo only the detector id and version and the session and entry ids, each empty when the
|
|
76
|
+
* input did not carry it as a string.
|
|
77
|
+
*
|
|
78
|
+
* A repeat call over the same (plugin, detector, version, session, entry, input digest) returns
|
|
79
|
+
* the ALREADY-STORED row with `deduplicated: true` and never calls Jev again: replay is free.
|
|
80
|
+
*/
|
|
81
|
+
export declare interface KayAppSignalObservation {
|
|
82
|
+
readonly id?: number | undefined;
|
|
83
|
+
readonly status: "classified" | "unavailable" | "invalid";
|
|
84
|
+
readonly detector: KayAppSignalsDetectorRef;
|
|
85
|
+
readonly source: KayAppSignalSource;
|
|
86
|
+
readonly answers?: Record<string, unknown> | undefined;
|
|
87
|
+
readonly model?: string | undefined;
|
|
88
|
+
readonly usage?: {
|
|
89
|
+
readonly inputTokens: number;
|
|
90
|
+
readonly outputTokens: number;
|
|
91
|
+
} | undefined;
|
|
92
|
+
readonly tookMs?: number | undefined;
|
|
93
|
+
readonly reason?: string | undefined;
|
|
94
|
+
/** True when the row landed in the durable store; false when the host composed no store. */
|
|
95
|
+
readonly stored: boolean;
|
|
96
|
+
/** True when this call returned an already-stored row instead of running inference. */
|
|
97
|
+
readonly deduplicated: boolean;
|
|
98
|
+
readonly inputDigest: string;
|
|
99
|
+
readonly recordedAtMs: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One page of signals, oldest recorded first. */
|
|
103
|
+
export declare interface KayAppSignalPage {
|
|
104
|
+
readonly signals: readonly KayAppSignal[];
|
|
105
|
+
/**
|
|
106
|
+
* Pass as `after` to continue, now or on a later run: the next read returns only signals
|
|
107
|
+
* recorded after this page, including any a detector backfilled since. Absent only when this
|
|
108
|
+
* page is empty, which means nothing newer than `after` exists right now; keep the cursor you
|
|
109
|
+
* already have.
|
|
110
|
+
*/
|
|
111
|
+
readonly nextCursor?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The plugin's signal door. `observe` runs the fused inference-and-record step; `list` and
|
|
116
|
+
* `listDetectors` read recorded signals back. The plugin id is stamped by the host at the
|
|
117
|
+
* boundary and is never a parameter.
|
|
118
|
+
*
|
|
119
|
+
* Reads are checked against the `signals.read` claim on every call, and a granted plugin reads
|
|
120
|
+
* EVERY plugin's signals, not only its own.
|
|
121
|
+
*/
|
|
122
|
+
export declare interface KayAppSignals {
|
|
123
|
+
observe(input: KayAppSignalsObserveInput): Promise<KayAppSignalObservation>;
|
|
124
|
+
/**
|
|
125
|
+
* Every detector that has recorded a signal: one entry per version and rubric, each with its
|
|
126
|
+
* questions. Call it once and cache questions by `rubricDigest`; result pages never repeat
|
|
127
|
+
* them.
|
|
128
|
+
*/
|
|
129
|
+
listDetectors(): Promise<KayAppSignalsRead<{
|
|
130
|
+
readonly detectors: readonly KayAppSignalDetector[];
|
|
131
|
+
}>>;
|
|
132
|
+
/** Stored signals for one session, or for one detector across sessions, oldest first. */
|
|
133
|
+
list(query: KayAppSignalsQuery): Promise<KayAppSignalsRead<KayAppSignalPage>>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One condition on one question's stored answer. Jev answers are probabilities, so each kind of
|
|
138
|
+
* question gets the comparison that makes sense for it:
|
|
139
|
+
*
|
|
140
|
+
* - `yes`: a yes/no question's probability of yes, from 0 to 1.
|
|
141
|
+
* - `choice`: a choice question's chosen label is one of these, with at least `minConfidence`
|
|
142
|
+
* (0 to 1) when given. At most {@link MAX_CHOICE_LABELS} labels, repeats included; a repeat
|
|
143
|
+
* changes nothing else.
|
|
144
|
+
* - `score`: a score question's expected score.
|
|
145
|
+
*
|
|
146
|
+
* The filter is refused as `invalid`, never answered with a silently empty page, when no
|
|
147
|
+
* version of the detector in the query's scope asks the question with the filter's kind, or when
|
|
148
|
+
* a `choice` filter names a label the rubric does not offer. A filter matches only rows whose
|
|
149
|
+
* stored answer to the question is of its own kind, so rows from a version where the question
|
|
150
|
+
* is absent or asks another kind never match; pin `version` to read one version.
|
|
151
|
+
*/
|
|
152
|
+
export declare type KayAppSignalsAnswerFilter = {
|
|
153
|
+
readonly question: string;
|
|
154
|
+
readonly yes: KayAppSignalsRange;
|
|
155
|
+
} | {
|
|
156
|
+
readonly question: string;
|
|
157
|
+
readonly choice: readonly string[];
|
|
158
|
+
readonly minConfidence?: number;
|
|
159
|
+
} | {
|
|
160
|
+
readonly question: string;
|
|
161
|
+
readonly score: KayAppSignalsRange;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* One detector as the recording plugin names it. The host never interprets these values;
|
|
166
|
+
* `id` + `version` are the plugin's half of the versioning contract that lets a consumer tell
|
|
167
|
+
* one rubric's answers from another's. The other half, the rubric digest, is the host's: it is
|
|
168
|
+
* computed from the `questions` actually sent, so a plugin can neither mis-stamp it nor forget
|
|
169
|
+
* it, and a rubric edit that did not bump `version` still shows up as a digest change.
|
|
170
|
+
*/
|
|
171
|
+
export declare interface KayAppSignalsDetectorInput {
|
|
172
|
+
/**
|
|
173
|
+
* Stable identifier, e.g. `memory-curator-gate`: a lowercase slug (a-z and 0-9 in words joined
|
|
174
|
+
* by single hyphens), at most 64 characters, plugin-scoped. It becomes a `signals://` path
|
|
175
|
+
* segment.
|
|
176
|
+
*/
|
|
177
|
+
readonly id: string;
|
|
178
|
+
/**
|
|
179
|
+
* The version the plugin bumps when its rubric's meaning changes, e.g. `v1.0.0`: letters and
|
|
180
|
+
* digits in parts joined by single `.`, `-`, or `_`, at most 64 characters.
|
|
181
|
+
*/
|
|
182
|
+
readonly version: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A detector a read names. `pluginId` is required because detector ids are unique only within
|
|
187
|
+
* the plugin that records them. Omit `version` to read every version; each returned signal
|
|
188
|
+
* carries the version it was recorded under.
|
|
189
|
+
*/
|
|
190
|
+
export declare interface KayAppSignalsDetectorMatch {
|
|
191
|
+
readonly pluginId: string;
|
|
192
|
+
readonly id: string;
|
|
193
|
+
readonly version?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** One detector's signals across every session, optionally in a turn-time window and filtered. */
|
|
197
|
+
export declare interface KayAppSignalsDetectorQuery {
|
|
198
|
+
readonly detector: KayAppSignalsDetectorMatch;
|
|
199
|
+
/** Never both anchors: a detector query reads that detector across every session. */
|
|
200
|
+
readonly sessionId?: never;
|
|
201
|
+
/**
|
|
202
|
+
* Epoch ms, inclusive, on TURN time: when the observed turn settled, or, for a signal whose
|
|
203
|
+
* recorder did not know that, when it was recorded. A detector backfilled over old history
|
|
204
|
+
* therefore places each signal on the day its turn happened. A whole number from 0 to
|
|
205
|
+
* {@link SIGNAL_TIME_MAX_MS}.
|
|
206
|
+
*/
|
|
207
|
+
readonly since?: number;
|
|
208
|
+
/** Epoch ms, exclusive, on turn time, in the same range as `since`. */
|
|
209
|
+
readonly until?: number;
|
|
210
|
+
/** Every filter must hold. At most 20. */
|
|
211
|
+
readonly where?: readonly KayAppSignalsAnswerFilter[];
|
|
212
|
+
/** A `nextCursor` from an earlier page: only signals recorded after it are returned. */
|
|
213
|
+
readonly after?: string;
|
|
214
|
+
/** Default 200, at most 1000 (a larger value is lowered to 1000). */
|
|
215
|
+
readonly limit?: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** A detector as a recorded row names it: the plugin's id and version plus the host's digest. */
|
|
219
|
+
export declare interface KayAppSignalsDetectorRef extends KayAppSignalsDetectorInput {
|
|
220
|
+
/**
|
|
221
|
+
* `sha256:<hex>` over the questions serialised with sorted keys, stamped by the host. Empty
|
|
222
|
+
* when nothing was stamped: on a fallback-only host, and on an `invalid` observation.
|
|
223
|
+
*/
|
|
224
|
+
readonly rubricDigest: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export declare interface KayAppSignalsObserveInput {
|
|
228
|
+
readonly detector: KayAppSignalsDetectorInput;
|
|
229
|
+
readonly source: KayAppSignalSource;
|
|
230
|
+
/**
|
|
231
|
+
* The exact state to send, already shaped by the caller (a turn packet, a window of messages,
|
|
232
|
+
* or anything else). NOT stored: the row keeps its digest, and a reader re-derives it from the
|
|
233
|
+
* transcript the source points at. So the projection must be a pure function of the
|
|
234
|
+
* transcript and the detector version, or the row's evidence cannot be reproduced.
|
|
235
|
+
*/
|
|
236
|
+
readonly state: unknown;
|
|
237
|
+
/**
|
|
238
|
+
* The typed questions to answer, as the vendor SDK accepts them. Stored once per digest. A
|
|
239
|
+
* rubric asks 1 to 64 questions. Each name is an identifier (letters, digits, and `_`, not
|
|
240
|
+
* starting with a digit), at most 64 characters, because a reader types it into a `where`
|
|
241
|
+
* filter. A choice question offers at most {@link MAX_CHOICE_LABELS} labels, each 1 to 64
|
|
242
|
+
* characters with no `,`, `@`, or control or invisible character (`\p{C}`), and no whitespace
|
|
243
|
+
* but single spaces between words, so a reader can copy it back exactly from rendered text.
|
|
244
|
+
*/
|
|
245
|
+
readonly questions: Questions;
|
|
246
|
+
readonly options?: WithJevOptions;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Where the observation came from: a pointer into the transcript, which is the only thing the
|
|
251
|
+
* row keeps of the state. The settle anchor is the entry the session event named; it may be a
|
|
252
|
+
* post-run bookkeeping entry (a settle event points at the run's last entry, not necessarily
|
|
253
|
+
* the opening message), so the packet helper folds it back to the turn it follows and reports
|
|
254
|
+
* that turn's opener here.
|
|
255
|
+
*/
|
|
256
|
+
export declare interface KayAppSignalSource {
|
|
257
|
+
/**
|
|
258
|
+
* The session, settle, and opener ids are each 1 to 256 characters with no whitespace, `/`,
|
|
259
|
+
* `?`, `#`, or control or invisible character (`\p{C}`), so each can be named in an address
|
|
260
|
+
* and renders exactly as stored.
|
|
261
|
+
*/
|
|
262
|
+
readonly sessionId: string;
|
|
263
|
+
/** The settle anchor: a member of the dedupe key. */
|
|
264
|
+
readonly entryId: string;
|
|
265
|
+
/** The message that opened the observed turn, when the caller's projection has one. */
|
|
266
|
+
readonly openerEntryId?: string;
|
|
267
|
+
/**
|
|
268
|
+
* The daemon run that produced the settle: 1 to 256 characters with no control character.
|
|
269
|
+
* No reader types it into an address or a filter, so nothing else about it is constrained.
|
|
270
|
+
*/
|
|
271
|
+
readonly runId?: string;
|
|
272
|
+
/**
|
|
273
|
+
* Epoch ms of the settle anchor, when the caller knows it: a whole number from 0 to
|
|
274
|
+
* {@link SIGNAL_TIME_MAX_MS}.
|
|
275
|
+
*/
|
|
276
|
+
readonly occurredAtMs?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Every read names one session or one detector. There is no read across every detector at
|
|
281
|
+
* once, because its cost grows with every detector anyone adds; the host refuses a query that
|
|
282
|
+
* names neither, or both, at runtime as well as in the type.
|
|
283
|
+
*/
|
|
284
|
+
export declare type KayAppSignalsQuery = KayAppSignalsSessionQuery | KayAppSignalsDetectorQuery;
|
|
285
|
+
|
|
286
|
+
/** A numeric range. An absent bound is open; a present bound is inclusive. */
|
|
287
|
+
export declare interface KayAppSignalsRange {
|
|
288
|
+
readonly atLeast?: number;
|
|
289
|
+
readonly atMost?: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The outcome of a read. Like `observe`, a read never rejects while the plugin is enabled; a
|
|
294
|
+
* handle kept after the plugin is disabled throws synchronously instead.
|
|
295
|
+
*
|
|
296
|
+
* - `ok`: the result.
|
|
297
|
+
* - `unavailable`: this host cannot serve reads right now. `reason` is `not-granted` (the
|
|
298
|
+
* `signals.read` claim was refused), `no-store` (this host records no signals), or
|
|
299
|
+
* `store-unavailable` (the store failed). Asking again later is reasonable.
|
|
300
|
+
* - `invalid`: the host refused the query itself, and `reason` says why in plain words: for
|
|
301
|
+
* example a question the detector's rubric does not ask, a filter of the wrong kind for its
|
|
302
|
+
* question, or a cursor this host did not issue. The same query will be refused again.
|
|
303
|
+
*/
|
|
304
|
+
export declare type KayAppSignalsRead<T> = ({
|
|
305
|
+
readonly status: "ok";
|
|
306
|
+
} & T) | {
|
|
307
|
+
readonly status: "unavailable";
|
|
308
|
+
readonly reason: string;
|
|
309
|
+
} | {
|
|
310
|
+
readonly status: "invalid";
|
|
311
|
+
readonly reason: string;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** One session's signals: every detector's, or only the detectors named. */
|
|
315
|
+
export declare interface KayAppSignalsSessionQuery {
|
|
316
|
+
readonly sessionId: string;
|
|
317
|
+
/** Never both anchors: a session query names detectors through `detectors`. */
|
|
318
|
+
readonly detector?: never;
|
|
319
|
+
/** Omit for every detector. At most 100, and never empty. */
|
|
320
|
+
readonly detectors?: readonly KayAppSignalsDetectorMatch[];
|
|
321
|
+
/** A `nextCursor` from an earlier page: only signals recorded after it are returned. */
|
|
322
|
+
readonly after?: string;
|
|
323
|
+
/** Default 200, at most 1000 (a larger value is lowered to 1000). */
|
|
324
|
+
readonly limit?: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** The most labels a recorded choice question may offer, and so the most one filter names. */
|
|
328
|
+
export declare const MAX_CHOICE_LABELS = 64;
|
|
329
|
+
|
|
330
|
+
export { NoulResponse }
|
|
331
|
+
|
|
332
|
+
export { ScoreResponse }
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The largest epoch-millisecond time a `Date` can hold (about the year 275,760). A time outside
|
|
336
|
+
* 0 to this bound has no date to show, so neither `observe` nor a read window accepts one.
|
|
337
|
+
*/
|
|
338
|
+
export declare const SIGNAL_TIME_MAX_MS = 8640000000000000;
|
|
339
|
+
|
|
340
|
+
/** Per-call knobs for {@link WithJev}. Every field is optional; the host applies its own default
|
|
341
|
+
* budget when none is given, so the Jev branch is never unbounded. */
|
|
342
|
+
declare interface WithJevOptions {
|
|
343
|
+
/**
|
|
344
|
+
* Explicit cancellation. Always propagates as a rejection; never substituted with `fallback`,
|
|
345
|
+
* because an aborted call means "stop", not "use the cheaper path".
|
|
346
|
+
*/
|
|
347
|
+
readonly signal?: AbortSignal;
|
|
348
|
+
/** Bounds the Jev branch alone, measured from the call. */
|
|
349
|
+
readonly timeoutMs?: number;
|
|
350
|
+
/**
|
|
351
|
+
* Absolute epoch-ms hard deadline for the WHOLE call, `fallback` included. A deadline already
|
|
352
|
+
* passed rejects (like `signal`); one still ahead derives the Jev branch's budget from it and
|
|
353
|
+
* keeps a small reserve so `fallback` has a window to run.
|
|
354
|
+
*/
|
|
355
|
+
readonly deadlineAt?: number;
|
|
356
|
+
/** Attribution slug for host usage accounting. Never sent to Jev. */
|
|
357
|
+
readonly purpose?: string;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export { }
|
package/dist/signals.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// packages/plugin-sdk/dist/signals.js
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function createFallbackOnlySignals() {
|
|
4
|
+
const noStore = { status: "unavailable", reason: "no-store" };
|
|
5
|
+
return {
|
|
6
|
+
observe: (input) => Promise.resolve(invalidObservation(input) ?? {
|
|
7
|
+
status: "unavailable",
|
|
8
|
+
reason: "no-jev-lane",
|
|
9
|
+
detector: { ...input.detector, rubricDigest: "" },
|
|
10
|
+
source: input.source,
|
|
11
|
+
stored: false,
|
|
12
|
+
deduplicated: false,
|
|
13
|
+
inputDigest: "",
|
|
14
|
+
recordedAtMs: Date.now()
|
|
15
|
+
}),
|
|
16
|
+
listDetectors: () => Promise.resolve(noStore),
|
|
17
|
+
list: () => Promise.resolve(noStore)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
var SIGNAL_TIME_MAX_MS = 864e13;
|
|
21
|
+
var MAX_CHOICE_LABELS = 64;
|
|
22
|
+
var MAX_NAME = 64;
|
|
23
|
+
var MAX_POINTER = 256;
|
|
24
|
+
var MAX_QUESTIONS = 64;
|
|
25
|
+
var DETECTOR_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
26
|
+
var VERSION = /^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$/u;
|
|
27
|
+
var QUESTION_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
28
|
+
var POINTER = /^[^\s\p{C}/?#]+$/u;
|
|
29
|
+
var LABEL = /^[^\s,@\p{C}]+(?: [^\s,@\p{C}]+)*$/u;
|
|
30
|
+
var RUN_ID = /^\P{Cc}+$/u;
|
|
31
|
+
var POINTER_RULE = `1 to ${MAX_POINTER} characters with no whitespace, \`/\`, \`?\`, \`#\`, or control or invisible character`;
|
|
32
|
+
var ObserveShape = z.looseObject({
|
|
33
|
+
detector: z.looseObject({ id: z.string(), version: z.string() }),
|
|
34
|
+
source: z.looseObject({
|
|
35
|
+
sessionId: z.string(),
|
|
36
|
+
entryId: z.string(),
|
|
37
|
+
openerEntryId: z.string().optional(),
|
|
38
|
+
runId: z.string().optional(),
|
|
39
|
+
occurredAtMs: z.unknown().optional()
|
|
40
|
+
}),
|
|
41
|
+
questions: z.record(z.string(), z.unknown())
|
|
42
|
+
});
|
|
43
|
+
var ChoiceQuestion = z.looseObject({
|
|
44
|
+
type: z.literal("choice"),
|
|
45
|
+
criteria: z.record(z.string(), z.unknown())
|
|
46
|
+
});
|
|
47
|
+
var fits = (value, grammar) => value.length <= MAX_NAME && grammar.test(value);
|
|
48
|
+
var quoted = (value) => JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}\u2026` : value).replace(/(?! )[\s\p{C}]/gu, (char) => `\\u${char.codePointAt(0).toString(16).padStart(4, "0")}`);
|
|
49
|
+
function grammarProblem(input) {
|
|
50
|
+
const shape = ObserveShape.safeParse(input);
|
|
51
|
+
if (!shape.success) {
|
|
52
|
+
const field = shape.error.issues[0]?.path.join(".") ?? "";
|
|
53
|
+
return field === "" ? "`observe` takes an object with `detector`, `source`, `state`, and `questions`." : `\`${field}\` is missing or has the wrong type.`;
|
|
54
|
+
}
|
|
55
|
+
const { detector, source, questions } = shape.data;
|
|
56
|
+
if (!fits(detector.id, DETECTOR_ID)) {
|
|
57
|
+
return "`detector.id` must be a lowercase slug of at most 64 characters: a-z and 0-9 in words joined by single hyphens.";
|
|
58
|
+
}
|
|
59
|
+
if (!fits(detector.version, VERSION)) {
|
|
60
|
+
return "`detector.version` must be at most 64 characters: letters and digits in parts joined by single `.`, `-`, or `_`.";
|
|
61
|
+
}
|
|
62
|
+
for (const [field, grammar, rule] of [
|
|
63
|
+
["sessionId", POINTER, POINTER_RULE],
|
|
64
|
+
["entryId", POINTER, POINTER_RULE],
|
|
65
|
+
["openerEntryId", POINTER, POINTER_RULE],
|
|
66
|
+
[
|
|
67
|
+
"runId",
|
|
68
|
+
RUN_ID,
|
|
69
|
+
`1 to ${MAX_POINTER} characters with no control character`
|
|
70
|
+
]
|
|
71
|
+
]) {
|
|
72
|
+
const id = source[field];
|
|
73
|
+
if (id !== void 0 && !(id.length <= MAX_POINTER && grammar.test(id))) {
|
|
74
|
+
return `\`source.${field}\` must be ${rule}.`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const occurredAtMs = source.occurredAtMs;
|
|
78
|
+
if (occurredAtMs !== void 0 && !(typeof occurredAtMs === "number" && Number.isSafeInteger(occurredAtMs) && occurredAtMs >= 0 && occurredAtMs <= SIGNAL_TIME_MAX_MS)) {
|
|
79
|
+
return `\`source.occurredAtMs\` must be whole epoch milliseconds from 0 to ${SIGNAL_TIME_MAX_MS}.`;
|
|
80
|
+
}
|
|
81
|
+
const names = Object.keys(questions);
|
|
82
|
+
if (names.length === 0 || names.length > MAX_QUESTIONS) {
|
|
83
|
+
return `\`questions\` must ask 1 to ${MAX_QUESTIONS} questions.`;
|
|
84
|
+
}
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
if (!fits(name, QUESTION_NAME)) {
|
|
87
|
+
return `The question name ${quoted(name)} must be an identifier of at most 64 characters: letters, digits, and \`_\`, not starting with a digit.`;
|
|
88
|
+
}
|
|
89
|
+
const choice = ChoiceQuestion.safeParse(questions[name]);
|
|
90
|
+
if (!choice.success)
|
|
91
|
+
continue;
|
|
92
|
+
const labels = Object.keys(choice.data.criteria);
|
|
93
|
+
if (labels.length > MAX_CHOICE_LABELS) {
|
|
94
|
+
return `The choice question \`${name}\` offers ${labels.length} labels; at most ${MAX_CHOICE_LABELS}.`;
|
|
95
|
+
}
|
|
96
|
+
const bad = labels.find((label) => !fits(label, LABEL));
|
|
97
|
+
if (bad !== void 0) {
|
|
98
|
+
return `The choice question \`${name}\` offers the label ${quoted(bad)}, which a \`where\` filter cannot name: a label is 1 to 64 characters with no \`,\`, \`@\`, or control or invisible character, and no whitespace but single spaces between words.`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
var EchoText = z.string().catch("");
|
|
104
|
+
var Echo = z.object({
|
|
105
|
+
detector: z.object({ id: EchoText, version: EchoText }).catch({ id: "", version: "" }),
|
|
106
|
+
source: z.object({ sessionId: EchoText, entryId: EchoText }).catch({ sessionId: "", entryId: "" })
|
|
107
|
+
}).catch({
|
|
108
|
+
detector: { id: "", version: "" },
|
|
109
|
+
source: { sessionId: "", entryId: "" }
|
|
110
|
+
});
|
|
111
|
+
function invalidObservation(input) {
|
|
112
|
+
const reason = grammarProblem(input);
|
|
113
|
+
if (reason === void 0)
|
|
114
|
+
return void 0;
|
|
115
|
+
const { detector, source } = Echo.parse(input);
|
|
116
|
+
return {
|
|
117
|
+
status: "invalid",
|
|
118
|
+
reason,
|
|
119
|
+
detector: { ...detector, rubricDigest: "" },
|
|
120
|
+
source,
|
|
121
|
+
stored: false,
|
|
122
|
+
deduplicated: false,
|
|
123
|
+
inputDigest: "",
|
|
124
|
+
recordedAtMs: Date.now()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
MAX_CHOICE_LABELS,
|
|
129
|
+
SIGNAL_TIME_MAX_MS,
|
|
130
|
+
createFallbackOnlySignals,
|
|
131
|
+
invalidObservation
|
|
132
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaklabs-ai/plugin-sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "The Kay plugin SDK — types and helpers for authoring a Kay directory plugin.",
|
|
5
5
|
"author": "YakLabs",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
"types": "./dist/disclosure.d.ts",
|
|
26
26
|
"default": "./dist/disclosure.js"
|
|
27
27
|
},
|
|
28
|
+
"./jev": {
|
|
29
|
+
"types": "./dist/jev.d.ts",
|
|
30
|
+
"default": "./dist/jev.js"
|
|
31
|
+
},
|
|
32
|
+
"./signals": {
|
|
33
|
+
"types": "./dist/signals.d.ts",
|
|
34
|
+
"default": "./dist/signals.js"
|
|
35
|
+
},
|
|
28
36
|
"./phase-shift": {
|
|
29
37
|
"types": "./dist/phase-shift.d.ts",
|
|
30
38
|
"default": "./dist/phase-shift.js"
|