@yaklabs-ai/plugin-sdk 0.12.5 → 0.13.1
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 +37 -33
- package/THIRD-PARTY-INVENTORY.json +1 -1
- package/dist/disclosure.d.ts +59 -77
- package/dist/disclosure.js +4 -14
- package/dist/fs.d.ts +156 -0
- package/dist/fs.js +895 -0
- package/dist/jev.d.ts +1 -1
- package/dist/kay-agent-testing.d.ts +58 -76
- package/dist/kay-agent-testing.js +4 -12
- package/dist/kay-agent.d.ts +65 -119
- package/dist/kay-agent.js +0 -11
- package/dist/kay-app.d.ts +156 -61
- package/dist/kay-app.js +22 -1
- package/dist/signals.d.ts +175 -1
- package/dist/signals.js +141 -2
- package/package.json +5 -1
- package/skills/directory-plugin-authoring/SKILL.md +18 -17
package/dist/kay-app.js
CHANGED
|
@@ -105,6 +105,9 @@ var RUN_ID = /^\P{Cc}+$/u;
|
|
|
105
105
|
var POINTER_RULE = `1 to ${MAX_POINTER} characters with no whitespace, \`/\`, \`?\`, \`#\`, or control or invisible character`;
|
|
106
106
|
var ObserveShape = z.looseObject({
|
|
107
107
|
detector: z.looseObject({ id: z.string(), version: z.string() }),
|
|
108
|
+
subject: z.looseObject({ kind: z.string(), key: z.string().optional() }).optional(),
|
|
109
|
+
after: z.looseObject({ observationId: z.unknown() }).optional(),
|
|
110
|
+
reuse: z.looseObject({ maxAgeMs: z.unknown() }).optional(),
|
|
108
111
|
source: z.looseObject({
|
|
109
112
|
sessionId: z.string(),
|
|
110
113
|
entryId: z.string(),
|
|
@@ -119,6 +122,7 @@ var ChoiceQuestion = z.looseObject({
|
|
|
119
122
|
criteria: z.record(z.string(), z.unknown())
|
|
120
123
|
});
|
|
121
124
|
var fits = (value, grammar) => value.length <= MAX_NAME && grammar.test(value);
|
|
125
|
+
var isWhole = (value, min) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= SIGNAL_TIME_MAX_MS;
|
|
122
126
|
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")}`);
|
|
123
127
|
function grammarProblem(input) {
|
|
124
128
|
const shape = ObserveShape.safeParse(input);
|
|
@@ -126,7 +130,7 @@ function grammarProblem(input) {
|
|
|
126
130
|
const field = shape.error.issues[0]?.path.join(".") ?? "";
|
|
127
131
|
return field === "" ? "`observe` takes an object with `detector`, `source`, `state`, and `questions`." : `\`${field}\` is missing or has the wrong type.`;
|
|
128
132
|
}
|
|
129
|
-
const { detector, source, questions } = shape.data;
|
|
133
|
+
const { detector, source, questions, subject, after, reuse } = shape.data;
|
|
130
134
|
const { state } = input;
|
|
131
135
|
if (typeof state !== "object" || state === null || Array.isArray(state)) {
|
|
132
136
|
return "`state` must be a JSON object.";
|
|
@@ -156,6 +160,23 @@ function grammarProblem(input) {
|
|
|
156
160
|
if (occurredAtMs !== void 0 && !(typeof occurredAtMs === "number" && Number.isSafeInteger(occurredAtMs) && occurredAtMs >= 0 && occurredAtMs <= SIGNAL_TIME_MAX_MS)) {
|
|
157
161
|
return `\`source.occurredAtMs\` must be whole epoch milliseconds from 0 to ${SIGNAL_TIME_MAX_MS}.`;
|
|
158
162
|
}
|
|
163
|
+
if (subject !== void 0 && !fits(subject.kind, DETECTOR_ID)) {
|
|
164
|
+
return "`subject.kind` must be a lowercase slug of at most 64 characters: a-z and 0-9 in words joined by single hyphens.";
|
|
165
|
+
}
|
|
166
|
+
if (subject?.key !== void 0 && !(subject.key.length <= MAX_POINTER && POINTER.test(subject.key))) {
|
|
167
|
+
return `\`subject.key\` must be ${POINTER_RULE}.`;
|
|
168
|
+
}
|
|
169
|
+
if (after !== void 0 && !isWhole(after.observationId, 1)) {
|
|
170
|
+
return "`after.observationId` must be a positive whole number: the id of a stored signal.";
|
|
171
|
+
}
|
|
172
|
+
if (reuse !== void 0) {
|
|
173
|
+
if (!isWhole(reuse.maxAgeMs, 1)) {
|
|
174
|
+
return `\`reuse.maxAgeMs\` must be whole milliseconds from 1 to ${SIGNAL_TIME_MAX_MS}.`;
|
|
175
|
+
}
|
|
176
|
+
if (subject?.key === void 0) {
|
|
177
|
+
return "`reuse` needs `subject.key`: a stored answer is reused only for the same subject.";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
159
180
|
const names = Object.keys(questions);
|
|
160
181
|
if (names.length === 0 || names.length > MAX_QUESTIONS) {
|
|
161
182
|
return `\`questions\` must ask 1 to ${MAX_QUESTIONS} questions.`;
|
package/dist/signals.d.ts
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function createFallbackOnlySignals(): KayAppSignals;
|
|
9
9
|
|
|
10
|
+
/** The engine over one plugin's `observe` (normally `app.signals.observe`). */
|
|
11
|
+
export declare function createSignalEngine(observe: KayAppSignals["observe"]): SignalEngine;
|
|
12
|
+
|
|
10
13
|
/**
|
|
11
14
|
* The `invalid` observation `observe` answers when a name in `input` breaks the recorded-name
|
|
12
15
|
* grammar, or `undefined` when every name can be recorded. A reader later types these names into
|
|
@@ -86,7 +89,7 @@ declare interface JevScoreQuestion {
|
|
|
86
89
|
* Those are host-side (`@kay/agent-runtime`'s `kay/jev/`).
|
|
87
90
|
* ---
|
|
88
91
|
*
|
|
89
|
-
* The vocabulary is
|
|
92
|
+
* The vocabulary is Bonsai's own (SDK 0.12.0). Before it, `use` received TypeSafe's `TypeSafeClient`
|
|
90
93
|
* and plugins coded against a vendor's types. It follows pi's classifier contract, which the host
|
|
91
94
|
* runs Jev through: a yes/no question is `bool` and answers a `probability`, every instruction and
|
|
92
95
|
* criterion is text, and the state is a JSON object.
|
|
@@ -106,6 +109,10 @@ export declare interface KayAppSignal {
|
|
|
106
109
|
readonly pluginId: string;
|
|
107
110
|
};
|
|
108
111
|
readonly source: KayAppSignalSource;
|
|
112
|
+
/** What the signal is about, when its recorder named a subject. */
|
|
113
|
+
readonly subject?: KayAppSignalSubject;
|
|
114
|
+
/** The id of the upstream signal this one was chained after, while that row exists. */
|
|
115
|
+
readonly upstreamId?: number;
|
|
109
116
|
readonly recordedAtMs: number;
|
|
110
117
|
/** Keyed by question name, full probability distributions included. */
|
|
111
118
|
readonly answers: Readonly<Record<string, KayAppSignalAnswer>>;
|
|
@@ -165,12 +172,20 @@ export declare interface KayAppSignalNoulAnswer {
|
|
|
165
172
|
*
|
|
166
173
|
* A repeat call over the same (plugin, detector, version, session, entry, input digest) returns
|
|
167
174
|
* the ALREADY-STORED row with `deduplicated: true` and never calls Jev again: replay is free.
|
|
175
|
+
* A call with `reuse` that finds a recent row for the same subject key returns that row with
|
|
176
|
+
* `deduplicated: true` and `reused: true`; its `source` is where the row was computed.
|
|
168
177
|
*/
|
|
169
178
|
export declare interface KayAppSignalObservation {
|
|
170
179
|
readonly id?: number | undefined;
|
|
171
180
|
readonly status: "classified" | "unavailable" | "invalid";
|
|
172
181
|
readonly detector: KayAppSignalsDetectorRef;
|
|
173
182
|
readonly source: KayAppSignalSource;
|
|
183
|
+
/** The row's subject, when it was recorded with one. */
|
|
184
|
+
readonly subject?: KayAppSignalSubject | undefined;
|
|
185
|
+
/** The row's upstream link: the id of the row it was chained after. */
|
|
186
|
+
readonly upstreamId?: number | undefined;
|
|
187
|
+
/** True when the answer is a stored row found through `reuse`, not this call's own. */
|
|
188
|
+
readonly reused?: boolean | undefined;
|
|
174
189
|
readonly answers?: Record<string, unknown> | undefined;
|
|
175
190
|
readonly model?: string | undefined;
|
|
176
191
|
readonly usage?: {
|
|
@@ -203,6 +218,18 @@ export declare interface KayAppSignalPage {
|
|
|
203
218
|
readonly nextCursor?: string;
|
|
204
219
|
}
|
|
205
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Reuse a stored answer for the same subject instead of asking Jev again. Before any Jev call
|
|
223
|
+
* the host looks for the newest row this plugin recorded with the same detector, version,
|
|
224
|
+
* subject key and input digest, recorded at most `maxAgeMs` ago, in ANY session. When one
|
|
225
|
+
* exists, that row is the answer: no Jev call and no new row. The returned observation keeps the
|
|
226
|
+
* row's own source (where it was computed), with `reused: true`.
|
|
227
|
+
*/
|
|
228
|
+
export declare interface KayAppSignalReuse {
|
|
229
|
+
/** How old a stored answer may be, in ms: a whole number from 1 to {@link SIGNAL_TIME_MAX_MS}. */
|
|
230
|
+
readonly maxAgeMs: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
206
233
|
/**
|
|
207
234
|
* One question of a stored rubric, as the recording plugin wrote it. Rubrics recorded before
|
|
208
235
|
* SDK 0.12.0 use TypeSafe's vocabulary (`noul` for yes/no, and instructions or criteria that
|
|
@@ -331,6 +358,12 @@ export declare interface KayAppSignalsDetectorRef extends KayAppSignalsDetectorI
|
|
|
331
358
|
export declare interface KayAppSignalsObserveInput {
|
|
332
359
|
readonly detector: KayAppSignalsDetectorInput;
|
|
333
360
|
readonly source: KayAppSignalSource;
|
|
361
|
+
/** What the signal is about. Stored on the row; not part of the dedupe key or the input digest. */
|
|
362
|
+
readonly subject?: KayAppSignalSubject;
|
|
363
|
+
/** The upstream row this observation is chained after. Stored on the row as its upstream link. */
|
|
364
|
+
readonly after?: KayAppSignalUpstreamLink;
|
|
365
|
+
/** Reuse a recent stored answer for the same subject key. Needs `subject.key`. */
|
|
366
|
+
readonly reuse?: KayAppSignalReuse;
|
|
334
367
|
/**
|
|
335
368
|
* The exact state to send, already shaped by the caller (a turn packet, a window of messages,
|
|
336
369
|
* or anything else). NOT stored: the row keeps its digest, and a reader re-derives it from the
|
|
@@ -428,6 +461,34 @@ export declare interface KayAppSignalsSessionQuery {
|
|
|
428
461
|
readonly limit?: number;
|
|
429
462
|
}
|
|
430
463
|
|
|
464
|
+
/**
|
|
465
|
+
* What a signal is about, named by the recording plugin. The host stores both values and never
|
|
466
|
+
* interprets them. A signal recorded without a subject has none; every reader treats it as before.
|
|
467
|
+
*/
|
|
468
|
+
export declare interface KayAppSignalSubject {
|
|
469
|
+
/**
|
|
470
|
+
* The kind of thing observed, e.g. `turn`: a lowercase slug of at most 64 characters, in the
|
|
471
|
+
* same grammar as a detector id.
|
|
472
|
+
*/
|
|
473
|
+
readonly kind: string;
|
|
474
|
+
/**
|
|
475
|
+
* The subject's identity ACROSS sessions, when it has one (two sessions can observe the same
|
|
476
|
+
* subject). Required for {@link KayAppSignalsObserveInput.reuse}. 1 to 256 characters with no
|
|
477
|
+
* whitespace, `/`, `?`, `#`, or control or invisible character.
|
|
478
|
+
*/
|
|
479
|
+
readonly key?: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The upstream signal a chained observation ran after: the stored row whose answers the caller
|
|
484
|
+
* found passing. The host checks that the row exists and was recorded by the same plugin, and
|
|
485
|
+
* stores the link on the new row; it never re-judges the caller's pass condition.
|
|
486
|
+
*/
|
|
487
|
+
export declare interface KayAppSignalUpstreamLink {
|
|
488
|
+
/** The upstream row's `id`: a positive whole number. */
|
|
489
|
+
readonly observationId: number;
|
|
490
|
+
}
|
|
491
|
+
|
|
431
492
|
/** The most labels a recorded choice question may offer, and so the most one filter names. */
|
|
432
493
|
export declare const MAX_CHOICE_LABELS = 64;
|
|
433
494
|
|
|
@@ -437,6 +498,119 @@ export declare const MAX_CHOICE_LABELS = 64;
|
|
|
437
498
|
*/
|
|
438
499
|
export declare const SIGNAL_TIME_MAX_MS = 8640000000000000;
|
|
439
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Whether stored answers satisfy every filter, with the store's own semantics: a yes/no
|
|
503
|
+
* question's P(yes) (`bool`'s `probability`, or `noul` in older rows), a choice question's label
|
|
504
|
+
* and confidence, a score question's expected score. An answer of another kind, a missing answer
|
|
505
|
+
* or a malformed one never passes. No filters always pass.
|
|
506
|
+
*/
|
|
507
|
+
export declare function signalAnswersPass(answers: Readonly<Record<string, unknown>> | undefined, filters: readonly KayAppSignalsAnswerFilter[]): boolean;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* One Jev signal a plugin records, over subjects of type `I`. `id`, `version` and `questions`
|
|
511
|
+
* are exactly what `observe` records under; `subjectKind` is stored on every row.
|
|
512
|
+
*/
|
|
513
|
+
export declare interface SignalDefinition<I> {
|
|
514
|
+
/** The detector id rows are recorded under (a lowercase slug, at most 64 characters). */
|
|
515
|
+
readonly id: string;
|
|
516
|
+
/** Bump it when a question changes meaning. */
|
|
517
|
+
readonly version: string;
|
|
518
|
+
/** What this signal is about, e.g. `turn`: a lowercase slug, stored on the row. */
|
|
519
|
+
readonly subjectKind: string;
|
|
520
|
+
readonly questions: JevQuestions;
|
|
521
|
+
/**
|
|
522
|
+
* The state Jev reads for this subject, or `undefined` when the subject carries nothing this
|
|
523
|
+
* signal can judge (no row is recorded). Must be a pure function of the subject: the row keeps
|
|
524
|
+
* only its digest.
|
|
525
|
+
*/
|
|
526
|
+
readonly project: (input: I) => JevState | undefined;
|
|
527
|
+
/**
|
|
528
|
+
* The subject's identity across sessions. Required with `maxAgeMs`; without it a row is tied
|
|
529
|
+
* to its own anchor only.
|
|
530
|
+
*/
|
|
531
|
+
readonly key?: (input: I) => string;
|
|
532
|
+
/**
|
|
533
|
+
* Reuse a stored answer for the same subject key and state for this long, in ms, in any
|
|
534
|
+
* session, instead of asking Jev again. Expiry belongs to the definition.
|
|
535
|
+
*/
|
|
536
|
+
readonly maxAgeMs?: number;
|
|
537
|
+
/** The upstream signal that must pass before this one is evaluated. */
|
|
538
|
+
readonly after?: SignalUpstream<I>;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* `sha256:<hex>` over a value serialised with sorted object keys, so two payloads that differ
|
|
543
|
+
* only in key order digest the same. The host stamps both digests a row carries with it: the
|
|
544
|
+
* input digest (the dedupe member of the natural key) and the rubric digest (the versioning
|
|
545
|
+
* tripwire). A caller may compute it too, to name a state without keeping it.
|
|
546
|
+
*
|
|
547
|
+
* The value is normalised THROUGH `JSON.stringify` first, deliberately. The digest must
|
|
548
|
+
* describe what the vendor actually received, and the vendor receives JSON: a `Date` goes over
|
|
549
|
+
* the wire as its ISO string, an `undefined` array slot as `null`, an `undefined` object value
|
|
550
|
+
* not at all. Canonicalising the raw JS value instead let two genuinely different states digest
|
|
551
|
+
* the same (`[]` and `[undefined]`; `{}` and any `Date`), and since the digest is a member of
|
|
552
|
+
* the store's natural key, a collision is not a crash but SILENT EVIDENCE LOSS: the second
|
|
553
|
+
* state dedupes against the first and is never observed. A value JSON cannot represent (a
|
|
554
|
+
* bigint) throws.
|
|
555
|
+
*/
|
|
556
|
+
export declare function signalDigest(value: unknown): string;
|
|
557
|
+
|
|
558
|
+
export declare interface SignalEngine {
|
|
559
|
+
/**
|
|
560
|
+
* Evaluate each definition over one subject, in order, and return one result per definition.
|
|
561
|
+
* `source` anchors every row to the transcript; `options` is passed to every observe, with
|
|
562
|
+
* `purpose` set to `signals/<id>`.
|
|
563
|
+
*/
|
|
564
|
+
evaluate<I>(definitions: readonly SignalDefinition<I>[], input: I, source: KayAppSignalSource, options?: WithJevOptions): Promise<readonly SignalEvaluation[]>;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* What evaluating one definition produced:
|
|
569
|
+
*
|
|
570
|
+
* - `observed`: `observe` returned; read `observation.status` (an unavailable or invalid outcome
|
|
571
|
+
* is still `observed`: the engine reports what the host answered).
|
|
572
|
+
* - `skipped`: the projection returned `undefined`; nothing was observed.
|
|
573
|
+
* - `blocked`: the upstream did not pass (or the subject has none); `upstream` is its result,
|
|
574
|
+
* absent when `find` named no upstream subject.
|
|
575
|
+
* - `failed`: the projection, `find`, `key` or `observe` threw.
|
|
576
|
+
*/
|
|
577
|
+
export declare type SignalEvaluation = {
|
|
578
|
+
readonly kind: "observed";
|
|
579
|
+
readonly signal: SignalRef;
|
|
580
|
+
readonly observation: KayAppSignalObservation;
|
|
581
|
+
} | {
|
|
582
|
+
readonly kind: "skipped";
|
|
583
|
+
readonly signal: SignalRef;
|
|
584
|
+
} | {
|
|
585
|
+
readonly kind: "blocked";
|
|
586
|
+
readonly signal: SignalRef;
|
|
587
|
+
readonly upstream?: SignalEvaluation;
|
|
588
|
+
} | {
|
|
589
|
+
readonly kind: "failed";
|
|
590
|
+
readonly signal: SignalRef;
|
|
591
|
+
readonly error: unknown;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
/** The signal a result is about. */
|
|
595
|
+
declare interface SignalRef {
|
|
596
|
+
readonly id: string;
|
|
597
|
+
readonly version: string;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The upstream a chained signal runs after. `find` names the upstream subject (and where it sits
|
|
602
|
+
* in the transcript) for a downstream subject, or `undefined` when it has none, which blocks the
|
|
603
|
+
* downstream. `pass` uses the same filters a signals read uses; every one must hold.
|
|
604
|
+
*/
|
|
605
|
+
export declare interface SignalUpstream<I> {
|
|
606
|
+
readonly signal: SignalDefinition<never>;
|
|
607
|
+
readonly find: (input: I, source: KayAppSignalSource) => {
|
|
608
|
+
readonly input: unknown;
|
|
609
|
+
readonly source: KayAppSignalSource;
|
|
610
|
+
} | undefined;
|
|
611
|
+
readonly pass: readonly KayAppSignalsAnswerFilter[];
|
|
612
|
+
}
|
|
613
|
+
|
|
440
614
|
/** Per-call knobs for {@link WithJev}. Every field is optional; the host applies its own default
|
|
441
615
|
* budget when none is given, so the Jev branch is never unbounded. */
|
|
442
616
|
declare interface WithJevOptions {
|
package/dist/signals.js
CHANGED
|
@@ -1,5 +1,120 @@
|
|
|
1
1
|
// packages/plugin-sdk/dist/signals.js
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// packages/plugin-sdk/dist/signal-digest.js
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
function signalDigest(value) {
|
|
7
|
+
const json = JSON.stringify(value);
|
|
8
|
+
const normalised = json === void 0 ? null : JSON.parse(json);
|
|
9
|
+
return `sha256:${createHash("sha256").update(canonicalJson(normalised)).digest("hex")}`;
|
|
10
|
+
}
|
|
11
|
+
function canonicalJson(value) {
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
14
|
+
}
|
|
15
|
+
if (value !== null && typeof value === "object") {
|
|
16
|
+
const record = value;
|
|
17
|
+
return `{${Object.keys(record).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// packages/plugin-sdk/dist/signal-engine.js
|
|
23
|
+
var MAX_CHAIN_DEPTH = 16;
|
|
24
|
+
function createSignalEngine(observe) {
|
|
25
|
+
return {
|
|
26
|
+
async evaluate(definitions, input, source, options) {
|
|
27
|
+
const memo = /* @__PURE__ */ new Map();
|
|
28
|
+
const run = async (definition, subject, at, chain) => {
|
|
29
|
+
const signal = { id: definition.id, version: definition.version };
|
|
30
|
+
try {
|
|
31
|
+
const state = definition.project(subject);
|
|
32
|
+
if (state === void 0)
|
|
33
|
+
return { kind: "skipped", signal };
|
|
34
|
+
const key = definition.key?.(subject);
|
|
35
|
+
const memoKey = JSON.stringify([
|
|
36
|
+
definition.id,
|
|
37
|
+
definition.version,
|
|
38
|
+
at.sessionId,
|
|
39
|
+
at.entryId,
|
|
40
|
+
key ?? null,
|
|
41
|
+
signalDigest(state)
|
|
42
|
+
]);
|
|
43
|
+
if (chain.includes(memoKey)) {
|
|
44
|
+
throw new Error(`signal '${definition.id}' is chained after itself`);
|
|
45
|
+
}
|
|
46
|
+
if (chain.length >= MAX_CHAIN_DEPTH) {
|
|
47
|
+
throw new Error(`signal '${definition.id}' is chained more than ${MAX_CHAIN_DEPTH} deep`);
|
|
48
|
+
}
|
|
49
|
+
const known = memo.get(memoKey);
|
|
50
|
+
if (known !== void 0)
|
|
51
|
+
return await known;
|
|
52
|
+
const pending = observeOne(definition, signal, subject, at, state, key, [...chain, memoKey]);
|
|
53
|
+
memo.set(memoKey, pending);
|
|
54
|
+
return await pending;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return { kind: "failed", signal, error };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const observeOne = async (definition, signal, subject, at, state, key, chain) => {
|
|
60
|
+
try {
|
|
61
|
+
let upstreamId;
|
|
62
|
+
if (definition.after !== void 0) {
|
|
63
|
+
const found = definition.after.find(subject, at);
|
|
64
|
+
if (found === void 0)
|
|
65
|
+
return { kind: "blocked", signal };
|
|
66
|
+
const upstream = await run(definition.after.signal, found.input, found.source, chain);
|
|
67
|
+
if (upstream.kind !== "observed" || upstream.observation.status !== "classified" || upstream.observation.id === void 0 || !signalAnswersPass(upstream.observation.answers, definition.after.pass)) {
|
|
68
|
+
return { kind: "blocked", signal, upstream };
|
|
69
|
+
}
|
|
70
|
+
upstreamId = upstream.observation.id;
|
|
71
|
+
}
|
|
72
|
+
const observation = await observe({
|
|
73
|
+
detector: signal,
|
|
74
|
+
source: at,
|
|
75
|
+
subject: {
|
|
76
|
+
kind: definition.subjectKind,
|
|
77
|
+
...key === void 0 ? {} : { key }
|
|
78
|
+
},
|
|
79
|
+
...upstreamId === void 0 ? {} : { after: { observationId: upstreamId } },
|
|
80
|
+
...definition.maxAgeMs === void 0 ? {} : { reuse: { maxAgeMs: definition.maxAgeMs } },
|
|
81
|
+
state,
|
|
82
|
+
questions: definition.questions,
|
|
83
|
+
options: { ...options, purpose: `signals/${definition.id}` }
|
|
84
|
+
});
|
|
85
|
+
return { kind: "observed", signal, observation };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { kind: "failed", signal, error };
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const results = [];
|
|
91
|
+
for (const definition of definitions) {
|
|
92
|
+
const result = await run(definition, input, source, []);
|
|
93
|
+
results.push(result);
|
|
94
|
+
}
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function signalAnswersPass(answers, filters) {
|
|
100
|
+
return filters.every((filter) => {
|
|
101
|
+
const answer = answers?.[filter.question];
|
|
102
|
+
if (typeof answer !== "object" || answer === null)
|
|
103
|
+
return false;
|
|
104
|
+
const record = answer;
|
|
105
|
+
const within = (value, range) => typeof value === "number" && (range.atLeast === void 0 || value >= range.atLeast) && (range.atMost === void 0 || value <= range.atMost);
|
|
106
|
+
if ("yes" in filter) {
|
|
107
|
+
const value = record["type"] === "bool" ? record["probability"] : record["type"] === "noul" ? record["noul"] : void 0;
|
|
108
|
+
return within(value, filter.yes);
|
|
109
|
+
}
|
|
110
|
+
if ("choice" in filter) {
|
|
111
|
+
return record["type"] === "choice" && typeof record["choice"] === "string" && filter.choice.includes(record["choice"]) && within(record["confidence"], { atLeast: filter.minConfidence ?? 0 });
|
|
112
|
+
}
|
|
113
|
+
return record["type"] === "score" && within(record["score"], filter.score);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// packages/plugin-sdk/dist/signals.js
|
|
3
118
|
function createFallbackOnlySignals() {
|
|
4
119
|
const noStore = { status: "unavailable", reason: "no-store" };
|
|
5
120
|
return {
|
|
@@ -31,6 +146,9 @@ var RUN_ID = /^\P{Cc}+$/u;
|
|
|
31
146
|
var POINTER_RULE = `1 to ${MAX_POINTER} characters with no whitespace, \`/\`, \`?\`, \`#\`, or control or invisible character`;
|
|
32
147
|
var ObserveShape = z.looseObject({
|
|
33
148
|
detector: z.looseObject({ id: z.string(), version: z.string() }),
|
|
149
|
+
subject: z.looseObject({ kind: z.string(), key: z.string().optional() }).optional(),
|
|
150
|
+
after: z.looseObject({ observationId: z.unknown() }).optional(),
|
|
151
|
+
reuse: z.looseObject({ maxAgeMs: z.unknown() }).optional(),
|
|
34
152
|
source: z.looseObject({
|
|
35
153
|
sessionId: z.string(),
|
|
36
154
|
entryId: z.string(),
|
|
@@ -45,6 +163,7 @@ var ChoiceQuestion = z.looseObject({
|
|
|
45
163
|
criteria: z.record(z.string(), z.unknown())
|
|
46
164
|
});
|
|
47
165
|
var fits = (value, grammar) => value.length <= MAX_NAME && grammar.test(value);
|
|
166
|
+
var isWhole = (value, min) => typeof value === "number" && Number.isSafeInteger(value) && value >= min && value <= SIGNAL_TIME_MAX_MS;
|
|
48
167
|
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
168
|
function grammarProblem(input) {
|
|
50
169
|
const shape = ObserveShape.safeParse(input);
|
|
@@ -52,7 +171,7 @@ function grammarProblem(input) {
|
|
|
52
171
|
const field = shape.error.issues[0]?.path.join(".") ?? "";
|
|
53
172
|
return field === "" ? "`observe` takes an object with `detector`, `source`, `state`, and `questions`." : `\`${field}\` is missing or has the wrong type.`;
|
|
54
173
|
}
|
|
55
|
-
const { detector, source, questions } = shape.data;
|
|
174
|
+
const { detector, source, questions, subject, after, reuse } = shape.data;
|
|
56
175
|
const { state } = input;
|
|
57
176
|
if (typeof state !== "object" || state === null || Array.isArray(state)) {
|
|
58
177
|
return "`state` must be a JSON object.";
|
|
@@ -82,6 +201,23 @@ function grammarProblem(input) {
|
|
|
82
201
|
if (occurredAtMs !== void 0 && !(typeof occurredAtMs === "number" && Number.isSafeInteger(occurredAtMs) && occurredAtMs >= 0 && occurredAtMs <= SIGNAL_TIME_MAX_MS)) {
|
|
83
202
|
return `\`source.occurredAtMs\` must be whole epoch milliseconds from 0 to ${SIGNAL_TIME_MAX_MS}.`;
|
|
84
203
|
}
|
|
204
|
+
if (subject !== void 0 && !fits(subject.kind, DETECTOR_ID)) {
|
|
205
|
+
return "`subject.kind` must be a lowercase slug of at most 64 characters: a-z and 0-9 in words joined by single hyphens.";
|
|
206
|
+
}
|
|
207
|
+
if (subject?.key !== void 0 && !(subject.key.length <= MAX_POINTER && POINTER.test(subject.key))) {
|
|
208
|
+
return `\`subject.key\` must be ${POINTER_RULE}.`;
|
|
209
|
+
}
|
|
210
|
+
if (after !== void 0 && !isWhole(after.observationId, 1)) {
|
|
211
|
+
return "`after.observationId` must be a positive whole number: the id of a stored signal.";
|
|
212
|
+
}
|
|
213
|
+
if (reuse !== void 0) {
|
|
214
|
+
if (!isWhole(reuse.maxAgeMs, 1)) {
|
|
215
|
+
return `\`reuse.maxAgeMs\` must be whole milliseconds from 1 to ${SIGNAL_TIME_MAX_MS}.`;
|
|
216
|
+
}
|
|
217
|
+
if (subject?.key === void 0) {
|
|
218
|
+
return "`reuse` needs `subject.key`: a stored answer is reused only for the same subject.";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
85
221
|
const names = Object.keys(questions);
|
|
86
222
|
if (names.length === 0 || names.length > MAX_QUESTIONS) {
|
|
87
223
|
return `\`questions\` must ask 1 to ${MAX_QUESTIONS} questions.`;
|
|
@@ -132,5 +268,8 @@ export {
|
|
|
132
268
|
MAX_CHOICE_LABELS,
|
|
133
269
|
SIGNAL_TIME_MAX_MS,
|
|
134
270
|
createFallbackOnlySignals,
|
|
135
|
-
|
|
271
|
+
createSignalEngine,
|
|
272
|
+
invalidObservation,
|
|
273
|
+
signalAnswersPass,
|
|
274
|
+
signalDigest
|
|
136
275
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaklabs-ai/plugin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "The Kay plugin SDK — types and helpers for authoring a Kay directory plugin.",
|
|
5
5
|
"author": "YakLabs",
|
|
6
6
|
"type": "module",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"types": "./dist/kay-app.d.ts",
|
|
18
18
|
"default": "./dist/kay-app.js"
|
|
19
19
|
},
|
|
20
|
+
"./fs": {
|
|
21
|
+
"types": "./dist/fs.d.ts",
|
|
22
|
+
"default": "./dist/fs.js"
|
|
23
|
+
},
|
|
20
24
|
"./kay-agent/testing": {
|
|
21
25
|
"types": "./dist/kay-agent-testing.d.ts",
|
|
22
26
|
"default": "./dist/kay-agent-testing.js"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: directory-plugin-authoring
|
|
3
|
-
description: "Author, scaffold, debug, or iterate on a
|
|
3
|
+
description: "Author, scaffold, debug, or iterate on a Bonsai directory plugin (one the user writes in ~/.kay/plugins/<name>). Use when creating a new local plugin, when a plugin does not appear in Bonsai after installation, when enabling it fails with 'pluginActivation names a plugin that is not installed', when Settings → Plugins shows 'failed to load' with no toggle, when @kay/plugin-sdk imports fail to resolve, when a plugin declares a ui.tsx that nothing typechecks, or when changes to a plugin do not take effect. For installing plugins from a git repository source, use the manage-plugins skill instead; for first-party bundled plugins under plugins/ in the kay repo, use the bundled-plugin guidance there."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Directory plugin authoring
|
|
7
7
|
|
|
8
|
-
A directory plugin is a plugin the user authors in `~/.kay/plugins/<name>/`, outside the
|
|
8
|
+
A directory plugin is a plugin the user authors in `~/.kay/plugins/<name>/`, outside the Bonsai
|
|
9
9
|
repo. The full authoring guide is [`docs/directory-plugins.md`](../../docs/directory-plugins.md)
|
|
10
10
|
and the SDK reference is `packages/plugin-sdk/README.md`; this skill is the routing layer that
|
|
11
11
|
keeps an agent on the paved path and out of the known pits.
|
|
@@ -13,7 +13,7 @@ keeps an agent on the paved path and out of the known pits.
|
|
|
13
13
|
## Confirm the checkout is current before you scaffold
|
|
14
14
|
|
|
15
15
|
`pnpm create-plugin` writes absolute type paths (`tsconfig.json`, `package.json`) pointing at
|
|
16
|
-
whichever
|
|
16
|
+
whichever Bonsai checkout invoked it, with no staleness warning. A checkout far behind `origin/main`
|
|
17
17
|
— one was 752 commits behind — typechecks the new plugin against stale `@kay/plugin-sdk` types,
|
|
18
18
|
and it looks clean: every wrong shape it produced (a `content` object instead of
|
|
19
19
|
`run`/`openBeside` on `PaneActionDescriptor`, `SearchField`'s `onChange` instead of
|
|
@@ -40,7 +40,7 @@ pnpm --filter @kay/plugin-sdk build # once per checkout, if not built
|
|
|
40
40
|
pnpm create-plugin --id com.example.widgets
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
Run from the root of a
|
|
43
|
+
Run from the root of a Bonsai checkout. It writes a working plugin (manifest, entrypoint, tsconfig,
|
|
44
44
|
README) that already typechecks and loads; `--tier starter` adds a URI module, a bundled skill,
|
|
45
45
|
and a standalone `node --test` test to copy from. Add `--ui` (either tier) when the plugin needs a
|
|
46
46
|
page: it writes `ui.tsx`, declares the `kay.ui` entrypoint, and generates the UI typecheck config
|
|
@@ -61,7 +61,7 @@ cannot happen.
|
|
|
61
61
|
pnpm plugin:check ~/.kay/plugins/widgets
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Run from the root of a
|
|
64
|
+
Run from the root of a Bonsai checkout, against the plugin directory. It runs the daemon's OWN
|
|
65
65
|
manifest schema and contract resolver (imported from `@kay/plugin-protocol`, not re-implemented),
|
|
66
66
|
so it answers "would Kay accept this manifest?" in about a second instead of after a load cycle.
|
|
67
67
|
Run it after every manifest edit, and before concluding a plugin is broken for some deeper reason.
|
|
@@ -87,10 +87,10 @@ disclosure audit, or an id another installed plugin already claims.
|
|
|
87
87
|
exports exist).
|
|
88
88
|
- The daemon's chord loader bundles each declared TypeScript entrypoint and its relative imports.
|
|
89
89
|
It keeps `@kay/plugin-sdk` imports external and resolves them to the running app's own built SDK.
|
|
90
|
-
Multi-file plugins are supported, and no plugin-local build is needed for
|
|
90
|
+
Multi-file plugins are supported, and no plugin-local build is needed for Bonsai to load them.
|
|
91
91
|
- Corollary: **bare `node` outside the daemon cannot resolve `@kay/plugin-sdk`** from the plugin
|
|
92
92
|
directory unless the plugin has its own SDK dependency. That failure says nothing about whether
|
|
93
|
-
the plugin will load in
|
|
93
|
+
the plugin will load in Bonsai. The `starter` tier's test setup creates the symlink needed for its
|
|
94
94
|
standalone test; a published SDK dependency is the other standalone option.
|
|
95
95
|
- Typecheck with `npm run typecheck` inside the plugin directory; the scaffold's generated
|
|
96
96
|
tsconfig maps the SDK's types via `paths`.
|
|
@@ -160,18 +160,18 @@ list` names the exact local root — because writing into the wrong one produces
|
|
|
160
160
|
never appears and no diagnostic anywhere.
|
|
161
161
|
|
|
162
162
|
2. **Enable it once.** Directory plugins are disabled on arrival (fail-closed: writing files in
|
|
163
|
-
your own plugin folder is how
|
|
164
|
-
Settings → Plugins → your plugin's detail pane, or ask
|
|
163
|
+
your own plugin folder is how Bonsai DISCOVERS it, not consent to run it). Flip the toggle in
|
|
164
|
+
Settings → Plugins → your plugin's detail pane, or ask Bonsai to write the `installs` row's
|
|
165
165
|
`enabled` field. Enabling reconciles live, and contributions reach the agent's **next**
|
|
166
166
|
session, not the current one.
|
|
167
167
|
3. **Edit, then commit.** `git add -A && git commit` makes the new code available.
|
|
168
|
-
|
|
168
|
+
Bonsai loads an isolated snapshot of HEAD and activates it live; no push or restart
|
|
169
169
|
is needed. Uncommitted edits, staged additions, deleted working files and runtime
|
|
170
170
|
writes do not execute, including at cold boot. The detail pane's **Running
|
|
171
171
|
commit** identifies the serving snapshot. Try new tools in a fresh session;
|
|
172
172
|
an existing session may still hold its previous generation.
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
Bonsai notices a commit within about 10 seconds even with no file events, and acquiring it may
|
|
175
175
|
take up to 30 seconds more. A **Running commit** still behind `git rev-parse --short HEAD` a
|
|
176
176
|
minute later, with a diagnostic on the plugin's row, is a failed read: fix what it names
|
|
177
177
|
instead of refreshing and waiting. Without a diagnostic, refresh the `local` source. A failed committed
|
|
@@ -208,7 +208,7 @@ list` names the exact local root — because writing into the wrong one produces
|
|
|
208
208
|
The commit is the activation boundary, so run these against the working tree first. Each catches
|
|
209
209
|
a failure class the load would otherwise be the first thing to surface:
|
|
210
210
|
|
|
211
|
-
1. **Run `pnpm plugin:check <dir>`** from a
|
|
211
|
+
1. **Run `pnpm plugin:check <dir>`** from a Bonsai checkout. It validates `kay-plugin.json` against
|
|
212
212
|
`KayPluginManifestV1` and resolves its contributions with the daemon's own resolver (both from
|
|
213
213
|
`@kay/plugin-protocol`), not just "it is valid JSON": that schema and resolver are the gate the
|
|
214
214
|
scan actually runs. A non-zero exit means the plugin would be refused.
|
|
@@ -230,12 +230,13 @@ the detail pane's **Running commit** row for the revision.
|
|
|
230
230
|
One thing no commit proves: whether the UI entrypoint bundles in a **packaged** build. esbuild's
|
|
231
231
|
native binary resolution can fail silently inside `app.asar` (`ui_entrypoint_bundle_failed`), and
|
|
232
232
|
the only trace is `ui-app.directory-load-failed` in the day's `kay-<date>.jsonl` log —
|
|
233
|
-
`plugin://<id>` still reads enabled with no failure noted, and the pane
|
|
234
|
-
|
|
235
|
-
path, so report the packaged case as unverified instead of assuming it
|
|
233
|
+
`plugin://<id>` still reads enabled with no failure noted, and the pane's overflow menu simply
|
|
234
|
+
lacks the plugin's row. Current builds carry the fix (#8014). A dev app runs unpackaged and
|
|
235
|
+
never exercises that path, so report the packaged case as unverified instead of assuming it
|
|
236
|
+
either way.
|
|
236
237
|
|
|
237
238
|
Dev-instance warning: a worktree dev app (`pnpm dev:kay`) is a **separate daemon** from the
|
|
238
|
-
user's installed
|
|
239
|
+
user's installed Bonsai, with its own config store, consent records, and plugin roster. A plugin
|
|
239
240
|
visible in one is invisible to the other, and a `config://settings/pluginActivation` write from
|
|
240
241
|
a session in one daemon validates against that daemon's roster only. #7732 was two daemons'
|
|
241
242
|
outputs conflated into one impossible-looking bug; always ask "which daemon am I looking at?"
|
|
@@ -283,4 +284,4 @@ file.
|
|
|
283
284
|
- [`docs/directory-plugins.md`](../../docs/directory-plugins.md) — the authoring guide:
|
|
284
285
|
manifest shape, the enable gate and what it does not gate, trust tiers, diagnostics.
|
|
285
286
|
- `packages/plugin-sdk/README.md` — the SDK surface.
|
|
286
|
-
- `packages/plugin-sdk/PUBLISHED.md` — installing the published SDK outside a
|
|
287
|
+
- `packages/plugin-sdk/PUBLISHED.md` — installing the published SDK outside a Bonsai checkout.
|