@yaklabs-ai/plugin-sdk 0.13.4 → 0.13.6

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.
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * The ids on a session's active branch: the leaf chain walked parent-ward from
3
5
  * the tip, which a trailing `leaf` marker moves. Empty for an empty log.
@@ -8,11 +10,11 @@
8
10
  * moment it revisits an id, so a cycle yields the cycle's own ids rather than
9
11
  * looping forever.
10
12
  */
11
- export declare function branchEntryIds(entries: readonly TranscriptTreeEntry[]): ReadonlySet<string>;
13
+ export declare function branchEntryIds(entries: readonly TranscriptTreeEntryLike[]): ReadonlySet<string>;
12
14
 
13
15
  /**
14
16
  * Every `type: "custom"` entry of one `customType`, oldest→newest, with every
15
- * field ({@link TranscriptSourceEntry}'s id/timestamp/tree links plus the
17
+ * field ({@link TranscriptSourceEntryLike}'s id/timestamp/tree links plus the
16
18
  * entry's own `data`) preserved unchanged — `data` is returned exactly as it
17
19
  * arrived, opaque and unvalidated, never coerced toward a schema this module
18
20
  * cannot know.
@@ -24,12 +26,37 @@ export declare function branchEntryIds(entries: readonly TranscriptTreeEntry[]):
24
26
  * itself needs to reason about abandoned entries (an audit, a recovery
25
27
  * tool); an ordinary reader states `"active"` explicitly.
26
28
  */
27
- export declare function customEntriesFromEntries<T extends TranscriptSourceEntry = TranscriptSourceEntry>(entries: readonly T[], customType: string, options: {
29
+ export declare function customEntriesFromEntries<T extends TranscriptSourceEntryLike = TranscriptSourceEntryLike>(entries: readonly T[], customType: string, options: {
28
30
  readonly branch: "active" | "all";
29
31
  }): readonly T[];
30
32
 
33
+ export declare function matchesWorkBlock(coverage: WorkBlockCoverage, toolResultEntryIds: readonly string[]): boolean;
34
+
35
+ /**
36
+ * Tool names the transcript draws as their own surface, or lifts into the reply, instead of
37
+ * counting them as work in the closed bar (venue `inline` or `turn-tail`, or the parsed
38
+ * `submit_final`). A call to one of these is not a "visible call". The renderer's built-in tool
39
+ * registrations are the source of truth; its parity test fails when this list drifts from them.
40
+ * A tool a plugin registers with a non-activity venue is not listed, so the daemon may count it
41
+ * (a headline can appear on a turn whose bar shows fewer calls than the cutoff only in that
42
+ * corner, and the renderer re-checks its own count before displaying anything).
43
+ */
44
+ export declare const NON_ACTIVITY_TOOL_NAMES: readonly string[];
45
+
46
+ /**
47
+ * The daemon's own run-start record. A run's entries begin here, before the
48
+ * user message that opened it.
49
+ */
50
+ export declare const RUN_STARTED_CUSTOM_TYPE = "session_run_started";
51
+
52
+ /**
53
+ * The daemon's own terminal run record (completed / interrupted / cancelled /
54
+ * failed). A run's entries end here.
55
+ */
56
+ export declare const RUN_TERMINAL_CUSTOM_TYPE = "session_run_state";
57
+
31
58
  /** The fields the customType selector needs off a session entry. */
32
- export declare interface TranscriptEntry {
59
+ export declare interface TranscriptEntryLike {
33
60
  readonly id: string;
34
61
  readonly type: string;
35
62
  readonly customType?: string;
@@ -41,7 +68,7 @@ export declare interface TranscriptEntry {
41
68
  * A tree entry plus the ISO timestamp the timeline projections read alongside
42
69
  * the walk result (`turnSegments`, `phaseShiftTimelineFromEntries`).
43
70
  */
44
- export declare interface TranscriptSourceEntry extends TranscriptTreeEntry {
71
+ export declare interface TranscriptSourceEntryLike extends TranscriptTreeEntryLike {
45
72
  readonly timestamp: string;
46
73
  }
47
74
 
@@ -52,9 +79,91 @@ export declare interface TranscriptSourceEntry extends TranscriptTreeEntry {
52
79
  * v3 log carries an ISO string, so requiring either shape here would force one
53
80
  * class of caller to convert entries it only wants walked.
54
81
  */
55
- declare interface TranscriptTreeEntry extends TranscriptEntry {
82
+ declare interface TranscriptTreeEntryLike extends TranscriptEntryLike {
56
83
  readonly parentId?: string | null;
57
84
  readonly targetId?: string | null;
58
85
  }
59
86
 
87
+ /**
88
+ * The fields the turn walk reads. Deliberately narrower and looser than
89
+ * {@link TranscriptEntryLike}: the walk answers in INDEXES, so it never needs an
90
+ * `id`, and both callers hold entries typed by their own layer (herd's
91
+ * `TranscriptEntry`, the daemon's branch rows).
92
+ */
93
+ export declare interface TurnBoundaryEntry {
94
+ readonly type?: string;
95
+ readonly customType?: string;
96
+ /** Opaque here; only `message.role === "user"` is read, defensively. */
97
+ readonly message?: unknown;
98
+ }
99
+
100
+ /** One turn's bounds as {@link turnSegments} reports them, both inclusive. */
101
+ export declare interface TurnSegment {
102
+ /** The user message entry that opened the turn. A steer never opens one. */
103
+ readonly openingIndex: number;
104
+ /**
105
+ * The entry the turn settled on. Equal to `openingIndex` when nothing
106
+ * followed the opening message, which is a turn that never settled rather
107
+ * than a turn with an empty body.
108
+ */
109
+ readonly settleIndex: number;
110
+ }
111
+
112
+ /**
113
+ * THE definition of a turn, shared by every reader that has to agree about one:
114
+ * the settled-turn extractors that key a record to a turn, and the replay
115
+ * publisher that offers a stored session's turns as events. Two readers
116
+ * disagreeing here means a replayed "turn" is not the turn production would
117
+ * have extracted.
118
+ *
119
+ * **A turn is a run.** The daemon brackets every run in the transcript
120
+ * ({@link RUN_STARTED_CUSTOM_TYPE} then, at the end,
121
+ * {@link RUN_TERMINAL_CUSTOM_TYPE}), and the user message that opened the run
122
+ * opens the turn. A human steering or a queued message admitted mid-flight is
123
+ * persisted as another `role: "user"` entry INSIDE that bracket, and belongs to
124
+ * the turn it interrupted: it is a correction to work already under way, not a
125
+ * new request. Anchoring on the last user entry instead hands an extractor the
126
+ * steer as the turn's goal and demotes the real request to prior context.
127
+ *
128
+ * A transcript with no run brackets at all (a log older than run bookkeeping, a
129
+ * hand-built fixture) falls back to one turn per user message, which is the
130
+ * most this function can honestly say: with no record of when a run ended,
131
+ * nothing distinguishes a steer from a fresh request.
132
+ *
133
+ * `throughIndex` bounds the walk for a caller holding only a prefix; segments
134
+ * are returned oldest first.
135
+ */
136
+ export declare function turnSegments(entries: readonly TurnBoundaryEntry[], throughIndex?: number): readonly TurnSegment[];
137
+
138
+ export declare const WORK_HEADLINE_MAX_CHARS = 90;
139
+
140
+ /** The longest and shortest stored work headline, in characters. Shorter is filler; longer is a sentence. */
141
+ export declare const WORK_HEADLINE_MIN_CHARS = 12;
142
+
143
+ /**
144
+ * The fewest VISIBLE tool calls a settled turn needs before it earns a work headline (owner
145
+ * ruling, 2026-10-14: "it shouldn't do that if it ran 2 commands but could be helpful if it ran
146
+ * like 15"; the scope card's 10 was accepted). ONE value, read by the writer (the hindsight
147
+ * `work-headline` handler decides whether to spend a model call) and by the closed work bar's
148
+ * renderer (decides whether to show a stored headline), so the two can never disagree about which
149
+ * turns qualify.
150
+ */
151
+ export declare const WORK_HEADLINE_MIN_VISIBLE_CALLS = 10;
152
+
153
+ export declare type WorkBlockCoverage = z.infer<typeof WorkBlockCoverageSchema>;
154
+
155
+ export declare function workBlockCoverage(toolResultEntryIds: readonly string[]): WorkBlockCoverage | undefined;
156
+
157
+ /**
158
+ * Exact ordered completed executions, addressed by persisted tool-result entry IDs.
159
+ * Provider call IDs can repeat across retries and turns; they are never ownership keys.
160
+ */
161
+ export declare const WorkBlockCoverageSchema: z.ZodObject<{
162
+ firstToolResultEntryId: z.ZodString;
163
+ lastToolResultEntryId: z.ZodString;
164
+ toolResultEntryIds: z.ZodArray<z.ZodString>;
165
+ }, z.core.$strict>;
166
+
167
+ export declare function workBlockKey(coverage: WorkBlockCoverage): string;
168
+
60
169
  export { }
@@ -1,4 +1,26 @@
1
1
  // packages/plugin-protocol/dist/transcript.js
2
+ import { z } from "zod";
3
+ var WorkBlockCoverageSchema = z.strictObject({
4
+ firstToolResultEntryId: z.string().min(1),
5
+ lastToolResultEntryId: z.string().min(1),
6
+ toolResultEntryIds: z.array(z.string().min(1)).min(1).max(4096)
7
+ }).refine((value) => value.firstToolResultEntryId === value.toolResultEntryIds[0] && value.lastToolResultEntryId === value.toolResultEntryIds.at(-1) && new Set(value.toolResultEntryIds).size === value.toolResultEntryIds.length);
8
+ function workBlockCoverage(toolResultEntryIds) {
9
+ const parsed = WorkBlockCoverageSchema.safeParse({
10
+ firstToolResultEntryId: toolResultEntryIds[0],
11
+ lastToolResultEntryId: toolResultEntryIds.at(-1),
12
+ toolResultEntryIds
13
+ });
14
+ return parsed.success ? parsed.data : void 0;
15
+ }
16
+ function workBlockKey(coverage) {
17
+ return JSON.stringify(coverage.toolResultEntryIds);
18
+ }
19
+ function matchesWorkBlock(coverage, toolResultEntryIds) {
20
+ return coverage.toolResultEntryIds.length === toolResultEntryIds.length && coverage.toolResultEntryIds.every((id, index) => id === toolResultEntryIds[index]);
21
+ }
22
+ var RUN_STARTED_CUSTOM_TYPE = "session_run_started";
23
+ var RUN_TERMINAL_CUSTOM_TYPE = "session_run_state";
2
24
  function branchEntryIds(entries) {
3
25
  const last = entries[entries.length - 1];
4
26
  if (last === void 0)
@@ -13,11 +35,77 @@ function branchEntryIds(entries) {
13
35
  return onBranch;
14
36
  }
15
37
  var EMPTY_BRANCH_IDS = /* @__PURE__ */ new Set();
38
+ function isUserMessageEntry(entry) {
39
+ if (entry.type !== "message")
40
+ return false;
41
+ const message = entry.message;
42
+ return typeof message === "object" && message !== null && message.role === "user";
43
+ }
44
+ function runBoundaryOf(entry) {
45
+ if (entry.type !== "custom")
46
+ return void 0;
47
+ if (entry.customType === RUN_STARTED_CUSTOM_TYPE)
48
+ return "started";
49
+ if (entry.customType === RUN_TERMINAL_CUSTOM_TYPE)
50
+ return "terminal";
51
+ return void 0;
52
+ }
53
+ function turnSegments(entries, throughIndex = entries.length - 1) {
54
+ const segments = [];
55
+ let openingIndex;
56
+ let runOpen = false;
57
+ const close = (settleIndex) => {
58
+ if (openingIndex !== void 0 && settleIndex >= openingIndex) {
59
+ segments.push({ openingIndex, settleIndex });
60
+ }
61
+ openingIndex = void 0;
62
+ };
63
+ for (let index = 0; index <= throughIndex; index += 1) {
64
+ const entry = entries[index];
65
+ if (entry === void 0)
66
+ break;
67
+ const boundary = runBoundaryOf(entry);
68
+ if (boundary !== void 0) {
69
+ close(boundary === "terminal" ? index : index - 1);
70
+ runOpen = boundary === "started";
71
+ continue;
72
+ }
73
+ if (!isUserMessageEntry(entry))
74
+ continue;
75
+ if (runOpen && openingIndex !== void 0)
76
+ continue;
77
+ close(index - 1);
78
+ openingIndex = index;
79
+ }
80
+ close(throughIndex);
81
+ return segments;
82
+ }
16
83
  function customEntriesFromEntries(entries, customType, options) {
17
84
  const onBranch = options.branch === "all" ? null : branchEntryIds(entries);
18
85
  return entries.filter((entry) => entry.type === "custom" && entry.customType === customType && (onBranch === null || onBranch.has(entry.id)));
19
86
  }
87
+ var WORK_HEADLINE_MIN_VISIBLE_CALLS = 10;
88
+ var NON_ACTIVITY_TOOL_NAMES = [
89
+ "agent_spawn",
90
+ "send_message",
91
+ "service_connect",
92
+ "ui_render",
93
+ "submit_final"
94
+ ];
95
+ var WORK_HEADLINE_MIN_CHARS = 12;
96
+ var WORK_HEADLINE_MAX_CHARS = 90;
20
97
  export {
98
+ NON_ACTIVITY_TOOL_NAMES,
99
+ RUN_STARTED_CUSTOM_TYPE,
100
+ RUN_TERMINAL_CUSTOM_TYPE,
101
+ WORK_HEADLINE_MAX_CHARS,
102
+ WORK_HEADLINE_MIN_CHARS,
103
+ WORK_HEADLINE_MIN_VISIBLE_CALLS,
104
+ WorkBlockCoverageSchema,
21
105
  branchEntryIds,
22
- customEntriesFromEntries
106
+ customEntriesFromEntries,
107
+ matchesWorkBlock,
108
+ turnSegments,
109
+ workBlockCoverage,
110
+ workBlockKey
23
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaklabs-ai/plugin-sdk",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "The Kay plugin SDK — types and helpers for authoring a Kay directory plugin.",
5
5
  "author": "YakLabs",
6
6
  "type": "module",
@@ -44,6 +44,10 @@
44
44
  "./transcript": {
45
45
  "types": "./dist/transcript.d.ts",
46
46
  "default": "./dist/transcript.js"
47
+ },
48
+ "./time": {
49
+ "types": "./dist/time.d.ts",
50
+ "default": "./dist/time.js"
47
51
  }
48
52
  },
49
53
  "files": [
@@ -56,7 +60,8 @@
56
60
  ],
57
61
  "dependencies": {
58
62
  "zod": "^4.0.0",
59
- "yaml": "^2.9.0"
63
+ "yaml": "^2.9.0",
64
+ "@js-temporal/polyfill": "0.5.1"
60
65
  },
61
66
  "publishConfig": {
62
67
  "registry": "https://registry.npmjs.org",
@@ -6,7 +6,7 @@ description: "Author, scaffold, debug, or iterate on a Bonsai directory plugin (
6
6
  # Directory plugin authoring
7
7
 
8
8
  A directory plugin is a plugin the user authors in `~/.kay/plugins/<name>/`, outside the Bonsai
9
- repo. The full authoring guide is [`docs/directory-plugins.md`](../../docs/directory-plugins.md)
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.
12
12
 
@@ -55,37 +55,49 @@ and symlinking `node_modules/@bonsai` into the copy does not rescue it either: p
55
55
  layout does not expose workspace packages that way. The scaffold exists precisely so this
56
56
  cannot happen.
57
57
 
58
+ The entrypoint default-exports the function Bonsai calls with the plugin API:
59
+ `export default function activate(api: KayAgentPluginApi): void`, and the same with
60
+ `KayAppPluginApi` for `kay.app`. An older plugin's named `activate` export still loads. Set the
61
+ manifest's `entrypoint.export` only to run a different export, such as one module serving both
62
+ daemon targets. Bonsai reads the default export from plugin SDK 0.13.5 on. The scaffold's
63
+ `compatibility.sdk` range makes an older Bonsai refuse the plugin ("needs plugin SDK ^0.13.5");
64
+ a hand-written manifest with no range loads there and then fails with "has no 'activate' export".
65
+
58
66
  ## Check the manifest before you wait for a load
59
67
 
60
68
  ```
61
69
  pnpm plugin:check ~/.kay/plugins/widgets
62
70
  ```
63
71
 
64
- Run from the root of a Bonsai checkout, against the plugin directory. It runs the daemon's OWN
65
- manifest schema and contract resolver (imported from `@bonsai/plugin-protocol`, not re-implemented),
66
- so it answers "would Kay accept this manifest?" in about a second instead of after a load cycle.
67
- Run it after every manifest edit, and before concluding a plugin is broken for some deeper reason.
68
-
69
- It prints the resolved global contribution ids, exits non-zero when the plugin would be refused,
70
- and separates the two outcomes that look alike from outside:
71
-
72
- - **Refused outright** — schema-invalid manifest, a reserved `kay.*` / `herd.*` id, a declared
73
- path escaping the plugin directory, or contributions that do not resolve. Every contribution
74
- shares ONE flat `<namespace>.<localId>` space ACROSS kinds, so a tool and an endpoint both
75
- called `inspect` collide and the whole plugin is refused (#8936).
72
+ Run from the root of a Bonsai checkout, against the plugin directory. It prints the verdict the
73
+ daemon's directory scan reaches, from the same function (`judgeKayPluginDirectory`,
74
+ `@bonsai/plugin-protocol`), judged against that checkout's plugin SDK, whose version it prints. So
75
+ it answers "would Bonsai refuse this plugin?" in about a second instead of after a load cycle. Run
76
+ it after every manifest edit, and before concluding a plugin is broken for some deeper reason.
77
+
78
+ It prints the resolved global contribution ids, exits non-zero unless the plugin would load, and
79
+ separates:
80
+
81
+ - **Refused outright** — an SDK range or protocol this Bonsai does not support (checked before
82
+ the schema, so a manifest for another SDK names the version, not its unknown key), a
83
+ schema-invalid manifest, a reserved `kay.*` / `herd.*` id, a declared path escaping the plugin
84
+ directory, or contributions that do not resolve. Every contribution shares ONE flat
85
+ `<namespace>.<localId>` space ACROSS kinds, so a tool and an endpoint both called `inspect`
86
+ collide and the whole plugin is refused (#8936).
76
87
  - **Loads, minus something** — a retired `compatibility` / `permissions.byTarget` key the parser
77
- strips silently, a `toolName` that is not the namespace-derived name (that tool is excluded and
78
- nothing tells the user), a missing entrypoint or skill file.
88
+ strips silently, or a tool whose `toolName` is not the namespace-derived name (that tool is left
89
+ out); plus compatibility notes (no declared SDK range).
79
90
 
80
- It reads the manifest only. A clean verdict says nothing about `activate()` throwing, the
81
- disclosure audit, or an id another installed plugin already claims.
91
+ It never reads the declared files or applies the roster's rules: a missing entrypoint or skill
92
+ file, an `activate()` that throws, the disclosure audit, and an id another installed plugin already
93
+ claims all show only after enabling, in `read plugin://<id>`.
82
94
 
83
95
  ## The SDK resolution contract
84
96
 
85
97
  - Plugin source imports `@bonsai/plugin-sdk/<subpath>` as a **bare specifier**
86
- (`@bonsai/plugin-sdk/kay-agent`, `/kay-agent/testing`, `/kay-app`, `/disclosure` — only declared
98
+ (`@bonsai/plugin-sdk/kay-agent`, `/kay-agent/testing`, `/kay-app`, `/disclosure`, `/time` — only declared
87
99
  exports exist).
88
- - The daemon's chord loader bundles each declared TypeScript entrypoint and its relative imports.
100
+ - The daemon's plugin loader bundles each declared TypeScript entrypoint and its relative imports.
89
101
  It keeps `@bonsai/plugin-sdk` imports external and resolves them to the running app's own built SDK.
90
102
  Multi-file plugins are supported, and no plugin-local build is needed for Bonsai to load them.
91
103
  - Corollary: **bare `node` outside the daemon cannot resolve `@bonsai/plugin-sdk`** from the plugin
@@ -94,7 +106,7 @@ disclosure audit, or an id another installed plugin already claims.
94
106
  standalone test; a published SDK dependency is the other standalone option.
95
107
  - Typecheck with `npm run typecheck` inside the plugin directory; the scaffold's generated
96
108
  tsconfig maps the SDK's types via `paths`.
97
- - Chord compiles TypeScript syntax that native type stripping cannot execute, including `enum`,
109
+ - The loader compiles TypeScript syntax that native type stripping cannot execute, including `enum`,
98
110
  `namespace` blocks, constructor parameter properties, and decorators. Typecheck before enabling
99
111
  the plugin; a bundle error is reported as a target-load diagnostic.
100
112
 
@@ -208,10 +220,10 @@ list` names the exact local root — because writing into the wrong one produces
208
220
  The commit is the activation boundary, so run these against the working tree first. Each catches
209
221
  a failure class the load would otherwise be the first thing to surface:
210
222
 
211
- 1. **Run `pnpm plugin:check <dir>`** from a Bonsai checkout. It validates `kay-plugin.json` against
212
- `KayPluginManifestV1` and resolves its contributions with the daemon's own resolver (both from
213
- `@bonsai/plugin-protocol`), not just "it is valid JSON": that schema and resolver are the gate the
214
- scan actually runs. A non-zero exit means the plugin would be refused.
223
+ 1. **Run `pnpm plugin:check <dir>`** from a Bonsai checkout. It prints the verdict of the function
224
+ the daemon's directory scan runs (`judgeKayPluginDirectory`): SDK compatibility, the manifest
225
+ schema, reserved ids, contract resolution and path containment, not just "it is valid JSON". A
226
+ non-zero exit means the plugin would be refused.
215
227
  2. **Run the disclosure audit yourself**: `auditPluginDisclosure` from `@bonsai/plugin-sdk/disclosure`.
216
228
  It is not redundant with typecheck. A plugin shipped a `search` verb with no `verbDocs` entry
217
229
  that typechecked clean and would have failed the whole `kay.agent` target.
@@ -281,7 +293,7 @@ file.
281
293
 
282
294
  ## Reference map
283
295
 
284
- - [`docs/directory-plugins.md`](../../docs/directory-plugins.md) — the authoring guide:
296
+ - [`docs/directory-plugins.md`](../../../docs/directory-plugins.md) — the authoring guide:
285
297
  manifest shape, the enable gate and what it does not gate, trust tiers, diagnostics.
286
298
  - `packages/plugin-sdk/README.md` — the SDK surface.
287
299
  - `packages/plugin-sdk/PUBLISHED.md` — installing the published SDK outside a Bonsai checkout.