@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.
- package/dist/contributors/contributors.types.d.ts +77 -0
- package/dist/contributors/contributors.types.js +1 -0
- package/dist/contributors/define-contributors.d.ts +7 -0
- package/dist/contributors/define-contributors.js +6 -0
- package/dist/contributors/index.d.ts +4 -0
- package/dist/contributors/index.js +2 -0
- package/dist/contributors/resolve-contributors.d.ts +51 -0
- package/dist/contributors/resolve-contributors.js +158 -0
- package/dist/lifecycle.d.ts +19 -0
- package/dist/lifecycle.js +54 -0
- package/dist/wirings/addon/addon-runner.js +11 -0
- package/dist/wirings/addon/{pikku-studio.d.ts → define-plugin.d.ts} +14 -2
- package/dist/wirings/addon/{pikku-studio.js → define-plugin.js} +3 -1
- package/dist/wirings/addon/index.d.ts +2 -2
- package/dist/wirings/addon/index.js +1 -1
- package/dist/wirings/addon/wire-addon.d.ts +7 -1
- package/dist/wirings/agent/agent-prepare.d.ts +9 -2
- package/dist/wirings/agent/agent-prepare.js +14 -6
- package/dist/wirings/agent/agent-rpc.js +1 -1
- package/dist/wirings/agent/agent-runner.js +2 -6
- package/dist/wirings/agent/agent-stream.js +2 -6
- package/package.json +3 -2
|
@@ -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,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,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
|
+
};
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
|
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 './
|
|
4
|
-
export type { StudioManifest, StudioScreen, StudioComponentScreen, StudioAppScreen, StudioNav, } from './
|
|
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 './
|
|
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 `
|
|
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:
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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"
|