@pikku/core 0.12.147 → 0.12.150

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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Where a target gets a contributor's bindings from. `env` is plain
3
+ * environment variables, which every target has; `cloudflare` is Cloudflare
4
+ * worker bindings, which only the Cloudflare deploy provides.
5
+ */
6
+ export type ContributorBindingSource = 'env' | 'cloudflare';
7
+ /** The database the generated app uses, as contributors see it at build time. */
8
+ export type ContributorDb = {
9
+ /** The SQL engine the app's database runs on. MySQL is not represented. */
10
+ engine: 'sqlite' | 'postgres';
11
+ /** Column coercion map from `pikku db generate`, keyed by table then column. */
12
+ coercionMap?: Record<string, Record<string, 'date' | 'bool' | 'json'>>;
13
+ };
14
+ /**
15
+ * Build-time facts about the app being generated. Handed to `when` while the
16
+ * generator selects contributors and, as plain data, to `run`.
17
+ */
18
+ export interface ContributorPlatform {
19
+ /** Service names the app (dev) or the deploy unit requires. */
20
+ serviceNames: string[];
21
+ /** True when the app uses the queue, so a contributor can provide or skip a queue service. */
22
+ needsQueue: boolean;
23
+ /** True when the app uses workflows, so a contributor can provide or skip workflow services. */
24
+ needsWorkflow: boolean;
25
+ /** True when the app uses AI agents, so a contributor can provide or skip agent services. */
26
+ needsAgent: boolean;
27
+ /**
28
+ * Baseline services the generated entry already holds before any
29
+ * contributor runs. They satisfy a contributor's `needs`, are read as
30
+ * `runtime.<name>`, and a contributor may provide the same key to replace
31
+ * them. Undefined means none.
32
+ */
33
+ runtimeServices?: string[];
34
+ /** The app's database, when it uses SQLite or Postgres. Unset otherwise. */
35
+ db?: ContributorDb;
36
+ }
37
+ export interface Contributor<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
38
+ /**
39
+ * Service keys `run` sets on `services`. May be empty for a contributor that
40
+ * only decorates services another contributor provides.
41
+ */
42
+ provides?: string[];
43
+ /**
44
+ * Service keys this contributor reads, from `runtime` or from `services`
45
+ * set by another contributor. A dotted key (`logSources.stage`) is met by a
46
+ * contributor providing it exactly or providing a prefix of it
47
+ * (`logSources`). Contributors that provide a need run first.
48
+ */
49
+ needs?: string[];
50
+ /**
51
+ * Binding sources the target must be able to supply for this contributor to
52
+ * apply. Defaults to `['env']`. A contributor the project opted in to whose
53
+ * `requires` the target cannot meet is a build error.
54
+ */
55
+ requires?: ContributorBindingSource[];
56
+ /** Build-time only. A contributor returning false is left out entirely. */
57
+ when?(platform: TPlatform): boolean;
58
+ /** Constructs services. Runtime `env` guards belong here, not in `when`. */
59
+ run(services: TServices, env: Record<string, string | undefined>, runtime: Record<string, any>, platform: TPlatform): void | Promise<void>;
60
+ /** Runtime setup, called where the server lifecycle runs. */
61
+ onServerStart?(services: TServices): void | Promise<void>;
62
+ /** Teardown, called in reverse `needs` order where the server lifecycle runs. */
63
+ onServerStop?(services: TServices): void | Promise<void>;
64
+ }
65
+ /** The per-target forms of one named contributor: a `dev` variant and a `deploy` variant. */
66
+ export interface ContributorVariants<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
67
+ /** The contributor `pikku dev` applies. Omit it to leave this contributor out of dev. */
68
+ dev?: Contributor<TServices, TPlatform>;
69
+ /** The contributor a deploy build applies. Omit it to leave this contributor out of deploys. */
70
+ deploy?: Contributor<TServices, TPlatform>;
71
+ }
72
+ /** The build a contributor is selected for: `pikku dev`, or a deploy. */
73
+ export type ContributorTarget = 'dev' | 'deploy';
74
+ export interface NamedContributor<TServices = any, TPlatform extends ContributorPlatform = ContributorPlatform> {
75
+ name: string;
76
+ contributor: Contributor<TServices, TPlatform>;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { ContributorPlatform, ContributorVariants } from './contributors.types.js';
2
+ /**
3
+ * Registers an addon's contributors by name. A project opts in to a name with
4
+ * `wireAddon({ contributors: ['name'] })`; only the selected variant for the
5
+ * build target is bundled.
6
+ */
7
+ export declare const defineContributors: <const T extends Record<string, ContributorVariants<any, ContributorPlatform>>>(contributors: T) => T;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Registers an addon's contributors by name. A project opts in to a name with
3
+ * `wireAddon({ contributors: ['name'] })`; only the selected variant for the
4
+ * build target is bundled.
5
+ */
6
+ export const defineContributors = (contributors) => contributors;
@@ -0,0 +1,4 @@
1
+ export { defineContributors } from './define-contributors.js';
2
+ export { ContributorDependencyError, DEFAULT_CONTRIBUTOR_BINDING_SOURCES, orderContributors, resolveContributors, selectContributors, } from './resolve-contributors.js';
3
+ export type { SkippedContributor } from './resolve-contributors.js';
4
+ export type { Contributor, ContributorBindingSource, ContributorDb, ContributorPlatform, ContributorTarget, ContributorVariants, NamedContributor, } from './contributors.types.js';
@@ -0,0 +1,2 @@
1
+ export { defineContributors } from './define-contributors.js';
2
+ export { ContributorDependencyError, DEFAULT_CONTRIBUTOR_BINDING_SOURCES, orderContributors, resolveContributors, selectContributors, } from './resolve-contributors.js';
@@ -0,0 +1,51 @@
1
+ import type { ContributorBindingSource, ContributorPlatform, NamedContributor } from './contributors.types.js';
2
+ export declare class ContributorDependencyError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare const DEFAULT_CONTRIBUTOR_BINDING_SOURCES: readonly ContributorBindingSource[];
6
+ export interface SkippedContributor {
7
+ name: string;
8
+ /**
9
+ * `when` returned false; the target cannot supply its `requires` (checked
10
+ * only once `when` passed, so this one is a contributor the build wanted);
11
+ * or it was `optional` and nothing meets a need of it.
12
+ */
13
+ reason: 'when' | 'requires' | 'needs';
14
+ }
15
+ /**
16
+ * Drops contributors whose `when` is false or whose `requires` the target
17
+ * cannot supply. The clash and ordering checks run on what is left, so an
18
+ * exclusive pair of `when`s never clashes.
19
+ */
20
+ export declare const selectContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options: {
21
+ platform: ContributorPlatform;
22
+ supported?: readonly ContributorBindingSource[];
23
+ }) => {
24
+ selected: T[];
25
+ skipped: SkippedContributor[];
26
+ };
27
+ /**
28
+ * Orders contributors so each runs after every contributor providing one of
29
+ * its `needs`. Contributors without needs keep their input order.
30
+ *
31
+ * Throws a `ContributorDependencyError` for a repeated name, a need nothing
32
+ * provides, a key two contributors both provide, or a cycle. A key listed in
33
+ * `options.runtime` is a baseline service: it meets a need and a contributor
34
+ * may provide it too, replacing it.
35
+ */
36
+ export declare const orderContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options?: {
37
+ runtime?: readonly string[];
38
+ }) => T[];
39
+ /**
40
+ * Filters by `when`/`requires`, then checks clashes and orders what is left.
41
+ * Names in `optional` are dropped, rather than failing the build, when a need
42
+ * of theirs is met by nothing.
43
+ */
44
+ export declare const resolveContributors: <T extends NamedContributor<any, any>>(contributors: readonly T[], options: {
45
+ platform: ContributorPlatform;
46
+ supported?: readonly ContributorBindingSource[];
47
+ optional?: ReadonlySet<string>;
48
+ }) => {
49
+ ordered: T[];
50
+ skipped: SkippedContributor[];
51
+ };
@@ -0,0 +1,158 @@
1
+ export class ContributorDependencyError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'ContributorDependencyError';
5
+ }
6
+ }
7
+ export const DEFAULT_CONTRIBUTOR_BINDING_SOURCES = ['env'];
8
+ const satisfies = (provided, need) => provided === need || need.startsWith(`${provided}.`);
9
+ /**
10
+ * Drops contributors whose `when` is false or whose `requires` the target
11
+ * cannot supply. The clash and ordering checks run on what is left, so an
12
+ * exclusive pair of `when`s never clashes.
13
+ */
14
+ export const selectContributors = (contributors, options) => {
15
+ const supported = options.supported ?? DEFAULT_CONTRIBUTOR_BINDING_SOURCES;
16
+ const selected = [];
17
+ const skipped = [];
18
+ for (const entry of contributors) {
19
+ const requires = entry.contributor.requires ?? DEFAULT_CONTRIBUTOR_BINDING_SOURCES;
20
+ // `when` first: a contributor that is not meant for this build is never
21
+ // reported as one the target failed to support.
22
+ if (entry.contributor.when?.(options.platform) === false) {
23
+ skipped.push({ name: entry.name, reason: 'when' });
24
+ }
25
+ else if (!requires.every((source) => supported.includes(source))) {
26
+ skipped.push({ name: entry.name, reason: 'requires' });
27
+ }
28
+ else {
29
+ selected.push(entry);
30
+ }
31
+ }
32
+ return { selected, skipped };
33
+ };
34
+ /**
35
+ * Orders contributors so each runs after every contributor providing one of
36
+ * its `needs`. Contributors without needs keep their input order.
37
+ *
38
+ * Throws a `ContributorDependencyError` for a repeated name, a need nothing
39
+ * provides, a key two contributors both provide, or a cycle. A key listed in
40
+ * `options.runtime` is a baseline service: it meets a need and a contributor
41
+ * may provide it too, replacing it.
42
+ */
43
+ export const orderContributors = (contributors, options = {}) => {
44
+ const runtime = options.runtime ?? [];
45
+ const names = new Set();
46
+ for (const { name } of contributors) {
47
+ if (names.has(name)) {
48
+ throw new ContributorDependencyError(`Contributor '${name}' is selected more than once.`);
49
+ }
50
+ names.add(name);
51
+ }
52
+ const owners = new Map();
53
+ for (const entry of contributors) {
54
+ for (const key of entry.contributor.provides ?? []) {
55
+ const owner = owners.get(key);
56
+ if (owner && owner !== entry) {
57
+ throw new ContributorDependencyError(`Contributors '${owner.name}' and '${entry.name}' both provide '${key}'. Each service key has one provider.`);
58
+ }
59
+ owners.set(key, entry);
60
+ }
61
+ }
62
+ const dependencies = new Map();
63
+ for (const entry of contributors) {
64
+ const deps = new Set();
65
+ // A contributor that provides what it needs (`provides: ['x'], needs: ['x']`,
66
+ // even for a baseline x) depends on itself, which is a cycle; a decorator
67
+ // declares empty `provides` and reads x from `services`/`runtime` instead.
68
+ for (const need of entry.contributor.needs ?? []) {
69
+ const providers = contributors.filter((other) => (other.contributor.provides ?? []).some((key) => satisfies(key, need)));
70
+ const fromRuntime = runtime.some((key) => satisfies(key, need));
71
+ if (providers.length === 0 && !fromRuntime) {
72
+ const near = contributors
73
+ .filter((other) => (other.contributor.provides ?? []).some((key) => key.startsWith(`${need}.`)))
74
+ .map((other) => other.name);
75
+ const hint = near.length > 0
76
+ ? ` ${near.join(', ')} provide${near.length === 1 ? 's' : ''} keys under it, but not '${need}' itself.`
77
+ : runtime.length > 0
78
+ ? ` No contributor provides it, and the runtime only has: ${runtime.join(', ')}.`
79
+ : ` No contributor provides it, and this runtime provides none.`;
80
+ throw new ContributorDependencyError(`Contributor '${entry.name}' needs '${need}', which nothing provides.${hint}`);
81
+ }
82
+ for (const provider of providers)
83
+ deps.add(provider);
84
+ }
85
+ dependencies.set(entry, [...deps]);
86
+ }
87
+ const ordered = [];
88
+ const placed = new Set();
89
+ while (ordered.length < contributors.length) {
90
+ const next = contributors.find((entry) => !placed.has(entry) &&
91
+ dependencies.get(entry).every((dep) => placed.has(dep)));
92
+ if (!next) {
93
+ throw new ContributorDependencyError(`Contributors depend on each other in a cycle: ${findCycle(contributors.filter((c) => !placed.has(c)), dependencies)
94
+ .map((c) => c.name)
95
+ .join(' -> ')}`);
96
+ }
97
+ placed.add(next);
98
+ ordered.push(next);
99
+ }
100
+ return ordered;
101
+ };
102
+ /**
103
+ * Drops, repeatedly, the `optional` contributors whose `needs` nothing left
104
+ * provides, so one that only decorates a service (a wrapper around
105
+ * `services.kysely`) applies when the service exists and is not an error when
106
+ * it does not.
107
+ */
108
+ const dropUnmetOptional = (selected, optional, runtime) => {
109
+ let kept = [...selected];
110
+ const dropped = [];
111
+ for (;;) {
112
+ const unmet = kept.find((entry) => optional.has(entry.name) &&
113
+ (entry.contributor.needs ?? []).some((need) => !runtime.some((key) => satisfies(key, need)) &&
114
+ !kept.some((other) => other !== entry &&
115
+ (other.contributor.provides ?? []).some((key) => satisfies(key, need)))));
116
+ if (!unmet)
117
+ return { kept, dropped };
118
+ kept = kept.filter((entry) => entry !== unmet);
119
+ dropped.push({ name: unmet.name, reason: 'needs' });
120
+ }
121
+ };
122
+ /**
123
+ * Filters by `when`/`requires`, then checks clashes and orders what is left.
124
+ * Names in `optional` are dropped, rather than failing the build, when a need
125
+ * of theirs is met by nothing.
126
+ */
127
+ export const resolveContributors = (contributors, options) => {
128
+ const { selected, skipped } = selectContributors(contributors, options);
129
+ const { kept, dropped } = dropUnmetOptional(selected, options.optional ?? new Set(), options.platform.runtimeServices ?? []);
130
+ return {
131
+ ordered: orderContributors(kept, {
132
+ runtime: options.platform.runtimeServices,
133
+ }),
134
+ skipped: [...skipped, ...dropped],
135
+ };
136
+ };
137
+ const findCycle = (remaining, dependencies) => {
138
+ const pending = new Set(remaining);
139
+ const visit = (node, path) => {
140
+ const at = path.indexOf(node);
141
+ if (at >= 0)
142
+ return [...path.slice(at), node];
143
+ for (const dep of dependencies.get(node) ?? []) {
144
+ if (!pending.has(dep))
145
+ continue;
146
+ const cycle = visit(dep, [...path, node]);
147
+ if (cycle)
148
+ return cycle;
149
+ }
150
+ return undefined;
151
+ };
152
+ for (const node of remaining) {
153
+ const cycle = visit(node, []);
154
+ if (cycle)
155
+ return cycle;
156
+ }
157
+ return remaining;
158
+ };
@@ -15,3 +15,22 @@ export declare const pikkuServerLifecycle: <SS extends CoreSingletonServices = C
15
15
  * @returns A function that removes the handlers.
16
16
  */
17
17
  export declare const onShutdownSignals: (run: (signal: NodeJS.Signals) => void | Promise<void>, signals?: NodeJS.Signals[]) => (() => void);
18
+ /** A contributor's `onServerStart` / `onServerStop`, as the generated platform-services file lists it. */
19
+ export interface ContributorServerHook {
20
+ name: string;
21
+ run: (services: any) => void | Promise<void>;
22
+ }
23
+ /**
24
+ * Composes the server lifecycle around the app's own: contributor start hooks
25
+ * run first, in order, then the app's `beforeStart`; the app's `afterStart` and
26
+ * `beforeStop` are unchanged; after the app's `afterStop` the stop hooks run
27
+ * (already in reverse run order, as the generated file lists them).
28
+ *
29
+ * A start hook that throws aborts startup with an error naming the contributor.
30
+ * Every stop hook is attempted even if an earlier one (or the app's
31
+ * `afterStop`) threw; the failures are then thrown together, each named.
32
+ */
33
+ export declare const composeServerLifecycle: <SS extends CoreSingletonServices = CoreSingletonServices>(app: ServerLifecycle<SS> | undefined, hooks: {
34
+ start?: readonly ContributorServerHook[];
35
+ stop?: readonly ContributorServerHook[];
36
+ }) => ServerLifecycle<SS>;
package/dist/lifecycle.js CHANGED
@@ -67,3 +67,57 @@ export const onShutdownSignals = (run, signals = ['SIGINT', 'SIGTERM']) => {
67
67
  proc.off(signal, handler);
68
68
  };
69
69
  };
70
+ const describeError = (error) => error instanceof Error ? error.message : String(error);
71
+ /**
72
+ * Composes the server lifecycle around the app's own: contributor start hooks
73
+ * run first, in order, then the app's `beforeStart`; the app's `afterStart` and
74
+ * `beforeStop` are unchanged; after the app's `afterStop` the stop hooks run
75
+ * (already in reverse run order, as the generated file lists them).
76
+ *
77
+ * A start hook that throws aborts startup with an error naming the contributor.
78
+ * Every stop hook is attempted even if an earlier one (or the app's
79
+ * `afterStop`) threw; the failures are then thrown together, each named.
80
+ */
81
+ export const composeServerLifecycle = (app, hooks) => ({
82
+ beforeStart: async (services) => {
83
+ for (const hook of hooks.start ?? []) {
84
+ try {
85
+ await hook.run(services);
86
+ }
87
+ catch (error) {
88
+ throw new Error(`Contributor '${hook.name}' failed in onServerStart: ${describeError(error)}`, { cause: error });
89
+ }
90
+ }
91
+ await app?.beforeStart?.(services);
92
+ },
93
+ afterStart: (services) => app?.afterStart?.(services),
94
+ beforeStop: (services) => app?.beforeStop?.(services),
95
+ afterStop: async (services) => {
96
+ const failures = [];
97
+ const errors = [];
98
+ let hookFailed = false;
99
+ try {
100
+ await app?.afterStop?.(services);
101
+ }
102
+ catch (error) {
103
+ failures.push(`the app's afterStop: ${describeError(error)}`);
104
+ errors.push(error);
105
+ }
106
+ for (const hook of hooks.stop ?? []) {
107
+ try {
108
+ await hook.run(services);
109
+ }
110
+ catch (error) {
111
+ hookFailed = true;
112
+ failures.push(`contributor '${hook.name}' onServerStop: ${describeError(error)}`);
113
+ errors.push(error);
114
+ }
115
+ }
116
+ if (errors.length === 1 && failures.length === 1 && !hookFailed) {
117
+ throw errors[0];
118
+ }
119
+ if (errors.length > 0) {
120
+ throw new AggregateError(errors, `Server stop failed: ${failures.join('; ')}`);
121
+ }
122
+ },
123
+ });
@@ -105,6 +105,13 @@ const scopeWebhookService = (service, namespace) => {
105
105
  scopedWebhookServices.add(scoped);
106
106
  return scoped;
107
107
  };
108
+ const authFromAppServices = () => {
109
+ const auth = pikkuState(null, 'package', 'singletonServices')?.auth;
110
+ if (typeof auth !== 'function') {
111
+ throw new Error('auth was called before the app had finished creating its singleton services');
112
+ }
113
+ return auth();
114
+ };
108
115
  export const getOrCreatePackageSingletonServices = async (packageName, parentServices, addonInstance) => {
109
116
  addonInstance ??= addonInstanceForNamespace(findAddonNamespaceForPackage(packageName) ?? undefined, packageName);
110
117
  const cacheKey = addonInstance?.namespace ?? packageName;
@@ -114,6 +121,10 @@ export const getOrCreatePackageSingletonServices = async (packageName, parentSer
114
121
  }
115
122
  const factories = pikkuState(packageName, 'package', 'factories');
116
123
  let existingServices = parentServices;
124
+ if (typeof existingServices.auth !== 'function' &&
125
+ pikkuState(null, 'package', 'authFactory')) {
126
+ existingServices = { ...existingServices, auth: authFromAppServices };
127
+ }
117
128
  if (addonInstance?.secretOverrides && parentServices.secrets) {
118
129
  existingServices = {
119
130
  ...existingServices,
@@ -22,6 +22,16 @@ type StudioScreenBase = {
22
22
  layout?: 'bare';
23
23
  /** Hides the screen and its nav for viewers without access. The addon's functions must enforce the same scopes: hiding a screen is not access control. */
24
24
  scopes?: string[];
25
+ /**
26
+ * The agent that answers in Studio's chat panel while this screen is open.
27
+ * `'studio:<name>'` names an agent Studio ships (e.g. `'studio:knowledge'`);
28
+ * `pikku all` only checks its format. A bare name is an agent the addon ships:
29
+ * it resolves to `agents/<name>.md` in the addon package: the agent's
30
+ * instructions, which name the skills it follows. Its frontmatter has a
31
+ * `title` and a `description`, both message keys in the addon's catalog.
32
+ * Lowercase letters, digits and hyphens, starting with a letter. Leave it out and the chat is unchanged.
33
+ */
34
+ agent?: string;
25
35
  } & ({
26
36
  /** `'studio'` gives `/<path>`, `'team'` gives `/<team>/<path>`. */
27
37
  scope: 'studio' | 'team';
@@ -45,7 +55,7 @@ export type StudioAppScreen = StudioScreenBase & {
45
55
  };
46
56
  /** One screen an addon ships: either a component the host renders or a static app it serves in a sandboxed iframe. */
47
57
  export type StudioScreen = StudioComponentScreen | StudioAppScreen;
48
- /** The manifest passed to `pikkuStudio`: the addon's title, a message key, and its screens. */
58
+ /** The manifest passed to `definePlugin`: the addon's title, a message key, and its screens. */
49
59
  export type StudioManifest = {
50
60
  title: string;
51
61
  screens: StudioScreen[];
@@ -56,5 +66,7 @@ export type StudioManifest = {
56
66
  * message keys in the addon's own catalog, `messages/{locale}.json`: a key
57
67
  * missing from `messages/en.json` fails the build.
58
68
  */
59
- export declare const pikkuStudio: (manifest: StudioManifest) => StudioManifest;
69
+ export declare const definePlugin: (manifest: StudioManifest) => StudioManifest;
70
+ /** @deprecated Renamed to `definePlugin`. */
71
+ export declare const pikkuStudio: typeof definePlugin;
60
72
  export {};
@@ -4,4 +4,6 @@
4
4
  * message keys in the addon's own catalog, `messages/{locale}.json`: a key
5
5
  * missing from `messages/en.json` fails the build.
6
6
  */
7
- export const pikkuStudio = (manifest) => manifest;
7
+ export const definePlugin = (manifest) => manifest;
8
+ /** @deprecated Renamed to `definePlugin`. */
9
+ export const pikkuStudio = definePlugin;
@@ -1,7 +1,7 @@
1
1
  export { wireAddon } from './wire-addon.js';
2
2
  export type { WireAddonConfig } from './wire-addon.js';
3
- export { pikkuStudio } from './pikku-studio.js';
4
- export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './pikku-studio.js';
3
+ export { definePlugin, pikkuStudio } from './define-plugin.js';
4
+ export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './define-plugin.js';
5
5
  export { wireRemoteAddon } from './wire-remote-addon.js';
6
6
  export type { WireRemoteAddonConfig, RemoteAddonAuth, } from './wire-remote-addon.js';
7
7
  export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
@@ -1,4 +1,4 @@
1
1
  export { wireAddon } from './wire-addon.js';
2
- export { pikkuStudio } from './pikku-studio.js';
2
+ export { definePlugin, pikkuStudio } from './define-plugin.js';
3
3
  export { wireRemoteAddon } from './wire-remote-addon.js';
4
4
  export { resolveRemoteAddonToken, RemoteAddonAuthError, } from './remote-addon-auth.js';
@@ -46,7 +46,7 @@ export type WireAddonConfig = {
46
46
  /** Filters this addon in and out of a build — see the `tags` option on `pikku all`. It has no effect at runtime. */
47
47
  tags?: string[];
48
48
  /**
49
- * Mounts the screens the addon ships (see `pikkuStudio`) in the host, each at
49
+ * Mounts the screens the addon ships (see `definePlugin`) in the host, each at
50
50
  * the path its `scope` gives it. Without it the addon's screens are ignored.
51
51
  * The package must declare at least one, or `pikku all` rejects the wiring.
52
52
  */
@@ -100,6 +100,12 @@ export type WireAddonConfig = {
100
100
  contentGrants?: Record<string, 'read' | 'write'>;
101
101
  /** Hands over the whole `ContentService` unscoped. The value is the reason, recorded in the deploy manifest. */
102
102
  globalContent?: string;
103
+ /**
104
+ * The contributors this project takes from the addon (those it registers with
105
+ * `defineContributors`), by name. None unless listed: an addon never injects
106
+ * services into a project that did not ask for them.
107
+ */
108
+ contributors?: string[];
103
109
  };
104
110
  /**
105
111
  * Installs an addon into this project: its functions, wirings and scopes become
@@ -1,6 +1,6 @@
1
1
  import type { CoreUserSession, GetCredential } from '../../types/core.types.js';
2
2
  import type { CoreAgent, AgentInput, AgentToolDef, AgentMessage, AgentStreamChannel, PikkuAgentMiddlewareHooks, SessionScope } from './agent.types.js';
3
- import type { AgentRunnerParams } from '../../services/agent-runner-service.js';
3
+ import type { AgentRunnerParams, AgentRunnerService } from '../../services/agent-runner-service.js';
4
4
  import { PikkuError } from '../../errors/error-handler.js';
5
5
  import type { SessionService } from '../../services/user-session-service.js';
6
6
  /**
@@ -155,11 +155,18 @@ export declare function buildToolDefs(params: RunAgentParams, agentSessionMap: M
155
155
  tools: AgentToolDef[];
156
156
  missingRpcs: string[];
157
157
  }>;
158
+ /**
159
+ * The singleton `agentRunner`, keyed with the caller's `AI_API_KEY` credential when one is set.
160
+ * Every entry that calls the model (run, stream, approve, resume) goes through this.
161
+ */
162
+ export declare function resolveAgentRunner(singletonServices: {
163
+ agentRunner?: AgentRunnerService;
164
+ }, params: RunAgentParams): Promise<AgentRunnerService>;
158
165
  export declare function prepareAgentRun(agentName: string, input: AgentInput, params: RunAgentParams, agentSessionMap: Map<string, string>, streamContext?: StreamContext): Promise<{
159
166
  agent: CoreAgent;
160
167
  packageName: string | null;
161
168
  resolvedName: string;
162
- agentRunner: import("../../services/agent-runner-service.js").AgentRunnerService;
169
+ agentRunner: AgentRunnerService;
163
170
  storage: import("../../services/agent-storage-service.js").AgentStorageService | undefined;
164
171
  memoryConfig: import("./agent.types.js").AgentMemoryConfig | undefined;
165
172
  threadId: string;
@@ -679,20 +679,28 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
679
679
  }
680
680
  return { tools, missingRpcs };
681
681
  }
682
- export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
683
- const singletonServices = getSingletonServices();
684
- const { agent, packageName, resolvedName } = resolveAgent(agentName);
685
- await assertAgentAuthorized(agent, params, packageName);
686
- let agentRunner = singletonServices.agentRunner;
682
+ /**
683
+ * The singleton `agentRunner`, keyed with the caller's `AI_API_KEY` credential when one is set.
684
+ * Every entry that calls the model (run, stream, approve, resume) goes through this.
685
+ */
686
+ export async function resolveAgentRunner(singletonServices, params) {
687
+ const agentRunner = singletonServices.agentRunner;
687
688
  if (!agentRunner) {
688
689
  throw new AIProviderNotConfiguredError();
689
690
  }
690
691
  if (params.getCredential && agentRunner.withApiKey) {
691
692
  const aiCredential = await params.getCredential('AI_API_KEY');
692
693
  if (aiCredential?.apiKey?.trim()) {
693
- agentRunner = agentRunner.withApiKey(aiCredential.apiKey);
694
+ return agentRunner.withApiKey(aiCredential.apiKey);
694
695
  }
695
696
  }
697
+ return agentRunner;
698
+ }
699
+ export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
700
+ const singletonServices = getSingletonServices();
701
+ const { agent, packageName, resolvedName } = resolveAgent(agentName);
702
+ await assertAgentAuthorized(agent, params, packageName);
703
+ const agentRunner = await resolveAgentRunner(singletonServices, params);
696
704
  const { storage } = resolveMemoryServices(agent, singletonServices);
697
705
  const memoryConfig = agent.memory;
698
706
  const threadId = input.threadId;
@@ -50,7 +50,7 @@ export const createAgentRPC = (wire, options) => {
50
50
  await resumeAgent({ runId, ...input }, wrapChannelWithAGUI(streamingChannel(), { runId }), params(), streamOptions);
51
51
  },
52
52
  interrupt: async (runId, reason) => interruptAgent({ runId, ...(reason ? { reason } : {}) }, { sessionService: options.sessionService }),
53
- approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName === undefined
53
+ approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, params(), expectedAgentName === undefined
54
54
  ? undefined
55
55
  : mapAddonCall(wire.addonNamespace, expectedAgentName))),
56
56
  };
@@ -1,5 +1,5 @@
1
1
  import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './agent-memory.js';
2
- import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourcePrincipalOwner, assertRunBelongsToAgent, assertAgentAuthorized, } from './agent-prepare.js';
2
+ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourcePrincipalOwner, assertRunBelongsToAgent, assertAgentAuthorized, resolveAgentRunner, } from './agent-prepare.js';
3
3
  import { checkForApprovals, appendStepMessages } from './agent-stream.js';
4
4
  import { AgentInterruptedError, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, trackInterruptNote, trackToolExecution, } from './agent-interrupt.js';
5
5
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
@@ -7,7 +7,6 @@ import { applyInputMiddleware, describeApprovals, notifyAfterStep, toAccumulated
7
7
  import { applyOutputMiddleware, finalizeAgentRun, lastUserMessageText, } from './agent-finalize.js';
8
8
  import { resolveModelConfig } from './agent-model-config.js';
9
9
  import { validateAgentOutput } from './agent-output.js';
10
- import { AIProviderNotConfiguredError } from '../../errors/errors.js';
11
10
  import { deniedToolResult, randomUUID } from './agent-utils.js';
12
11
  function stripNulls(obj) {
13
12
  if (obj === null)
@@ -308,10 +307,7 @@ export async function resumeAgentSync(runId, approvals, params, expectedAgentNam
308
307
  await assertAgentAuthorized(agent, params, packageName);
309
308
  const { storage } = resolveMemoryServices(agent, singletonServices);
310
309
  const memoryConfig = agent.memory;
311
- const agentRunner = singletonServices.agentRunner;
312
- if (!agentRunner) {
313
- throw new AIProviderNotConfiguredError();
314
- }
310
+ const agentRunner = await resolveAgentRunner(singletonServices, params);
315
311
  const approvedIds = new Set(approvals.filter((a) => a.approved).map((a) => a.toolCallId));
316
312
  const rejectedIds = new Set(approvals.filter((a) => !a.approved).map((a) => a.toolCallId));
317
313
  const savedPendingApprovals = [...(run.pendingApprovals ?? [])];
@@ -1,12 +1,11 @@
1
1
  import { finalizeAgentRun, lastUserMessageText } from './agent-finalize.js';
2
2
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
3
3
  import { applyInputMiddleware, describeApprovals } from './agent-turn.js';
4
- import { AIProviderNotConfiguredError } from '../../errors/errors.js';
5
4
  import { deniedToolResult, randomUUID } from './agent-utils.js';
6
5
  import { SPOKEN_TRANSCRIPT } from './voice-input.js';
7
6
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../../channel-middleware-runner.js';
8
7
  import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './agent-memory.js';
9
- import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertResourcePrincipalOwner, assertAgentAuthorized, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, CREDENTIAL_REQUIRED, } from './agent-prepare.js';
8
+ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertResourcePrincipalOwner, assertAgentAuthorized, resolveAgentRunner, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, CREDENTIAL_REQUIRED, } from './agent-prepare.js';
10
9
  import { resolveModelConfig } from './agent-model-config.js';
11
10
  import { getInFlightTools, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, signalRunInterrupt, trackInterruptNote, trackToolExecution, } from './agent-interrupt.js';
12
11
  function createPersistingChannel(parent, storage, threadId, logger) {
@@ -752,6 +751,7 @@ export async function resumeAgent(input, channel, params, options) {
752
751
  }
753
752
  const { agent, packageName, resolvedName } = resolveAgent(run.agentName);
754
753
  await assertAgentAuthorized(agent, params, packageName);
754
+ const agentRunner = await resolveAgentRunner(singletonServices, params);
755
755
  // The read above is not a claim — concurrent resumes all see the same pending
756
756
  // approval. `resolveApproval` is the claim, and the loser must not go on to
757
757
  // run the tool a second time.
@@ -761,10 +761,6 @@ export async function resumeAgent(input, channel, params, options) {
761
761
  }
762
762
  const { storage } = resolveMemoryServices(agent, singletonServices);
763
763
  const memoryConfig = agent.memory;
764
- const agentRunner = singletonServices.agentRunner;
765
- if (!agentRunner) {
766
- throw new AIProviderNotConfiguredError();
767
- }
768
764
  if (!input.approved) {
769
765
  if (pending.type === 'agent-call') {
770
766
  await agentRunState.updateRun(pending.agentRunId, { status: 'failed' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.147",
3
+ "version": "0.12.150",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -93,6 +93,7 @@
93
93
  "./state": "./dist/pikku-state.js",
94
94
  "./classification": "./dist/classification/index.js",
95
95
  "./utils": "./dist/utils.js",
96
+ "./contributors": "./dist/contributors/index.js",
96
97
  "./lifecycle": "./dist/lifecycle.js",
97
98
  "./schema": "./dist/schema.js",
98
99
  "./testing": "./dist/testing/index.js",
@@ -119,7 +120,7 @@
119
120
  },
120
121
  "devDependencies": {
121
122
  "@ag-ui/core": "0.0.59",
122
- "@pikku/deploy": "^0.12.17",
123
+ "@pikku/deploy": "^0.12.18",
123
124
  "@types/node": "^26.6.5",
124
125
  "esbuild": "~0.28.2",
125
126
  "tsx": "^4.23.15"